Skip to content

Commit 33687fa

Browse files
committed
Add Faiss ANN option + small bug fix in fullDataMaterial toggle
1 parent 36ee3c3 commit 33687fa

5 files changed

Lines changed: 104 additions & 103 deletions

File tree

DVRViewPlugin/res/shaders/FullDataMaterialBlending.frag

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ uniform vec2 invMatTexSize; // Pre-divided matTexSize (1.0 / matTexSize)
2020
uniform int numRays; // The number of rays in the batch (this is the same for all rays in the batch)
2121
uniform float stepSize; // The step size used for the ray marching, this is used to compensate for the alpha blending in the shader
2222

23+
uniform bool useClutterRemover; // If true, the clutter remover is used to smoothen the visualization
24+
2325
// holds the start index for the meanPositions array for each rayID (rayIDs are decided per batch and have no relation to the pixelPos)
2426
// The values it holds are the samplePosition, since each meanPos is a vec2, the start index needs to be multiplied by 2 in the shader
2527
// It also holds that total number of samples at the end such that the final ray length can also be calculated
@@ -41,7 +43,7 @@ float getMaterialID(inout float[5] materials, vec3 samplePos) {
4143
float nextMaterial = materials[3];
4244
float lastMaterial = materials[4];
4345

44-
if(firstMaterial == previousMaterial && nextMaterial == lastMaterial && currentMaterial != previousMaterial && currentMaterial != nextMaterial){
46+
if(useClutterRemover && firstMaterial == previousMaterial && nextMaterial == lastMaterial && currentMaterial != previousMaterial && currentMaterial != nextMaterial){
4547
currentMaterial = texture(materialVolumeData, samplePos).r + 0.5f;
4648

4749
// Update the materials array with the new material

DVRViewPlugin/res/shaders/MaterialTransition2D.frag

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ uniform vec3 u_maxClippingPlane;
2323
uniform bool useShading;
2424
uniform bool useClutterRemover;
2525

26-
float getMaterialID(inout float[5] materials, inout vec3[5] samplePositions) {
26+
float getMaterialID(inout float[5] materials, vec3[5] samplePositions) {
2727
float firstMaterial = materials[0];
2828
float previousMaterial = materials[1];
2929

@@ -39,7 +39,6 @@ float getMaterialID(inout float[5] materials, inout vec3[5] samplePositions) {
3939

4040
// Update the materials array with the new material
4141
materials[2] = currentMaterial;
42-
samplePositions[2] = samplePos;
4342
}
4443

4544
return currentMaterial;

DVRViewPlugin/res/shaders/NNMaterialTransition.frag

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,9 @@ void updateArrays(
5757
// Get the current material using the sliding window (like MaterialTransition2D.frag)
5858
// Refinement step: copy previous value instead of re-sampling
5959
float getMaterialID(inout float[5] materials, inout vec3[5] samplePositions) {
60-
float firstMaterial = materials[0];
6160
float previousMaterial = materials[1];
6261
float currentMaterial = materials[2];
6362
float nextMaterial = materials[3];
64-
float lastMaterial = materials[4];
6563

6664
// If a transition is detected, copy the previous value instead of re-sampling
6765
if (currentMaterial != previousMaterial && currentMaterial != nextMaterial

DVRViewPlugin/src/VolumeRenderer.cpp

Lines changed: 82 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -736,86 +736,53 @@ void VolumeRenderer::renderDirections()
736736
}
737737

738738

739-
void VolumeRenderer::prepareHNSW()
739+
void VolumeRenderer::prepareANN()
740740
{
741741
if (!_volumeDataset.isValid()) {
742-
qCritical() << "Volume dataset is not valid. Cannot prepare HNSW.";
742+
qCritical() << "Volume dataset is not valid. Cannot prepare ANN.";
743743
return;
744744
}
745745

746-
// Get the number of voxels and dimensions from the dataset
747746
uint32_t numVoxels = _volumeDataset->getNumberOfVoxels();
748747
uint32_t dimensions = _volumeDataset->getComponentsPerVoxel();
749748

750-
qDebug() << "Preparing HNSW with" << numVoxels << "voxels and" << dimensions << "dimensions.";
751-
752-
// Initialize HNSW space and index
753-
_hnswSpace = std::make_unique<hnswlib::L2Space>(dimensions); //If we use a local parameter here instead of a member variable we get a crash later on in the program when calling the hwnsIndex again
754-
_hnswIndex = std::make_unique<hnswlib::HierarchicalNSW<float>>(
755-
_hnswSpace.get(),
756-
numVoxels,
757-
_hnswM,
758-
_hnswEfConstruction
759-
);
760-
761-
// Populate HNSW index with volume data.
749+
// Populate ANN index with volume data.
762750
std::vector<float> voxelData(dimensions * numVoxels);
763751
QPair<float, float> scalarDataRange;
764752
_volumeDataset->getVolumeData(_compositeIndices, voxelData, scalarDataRange);
765753

766-
// Generate random data in the range [0, 1]
767-
//std::random_device rd;
768-
//std::mt19937 gen(rd());
769-
//std::uniform_real_distribution<float> dis(0.0f, 1.0f);
770-
771-
//// Parallelize data generation using OpenMP.
772-
//#pragma omp parallel for schedule(static)
773-
//for (int32_t i = 0; i < numVoxels; i++) {
774-
// for (int32_t j = 0; j < dimensions; j++) {
775-
// // Each element is a random float in [0, 1]
776-
// voxelData[i * dimensions + j] = dis(gen);
777-
// }
778-
//}
779-
780-
// Optionally, print the first few voxel vectors for inspection.
781-
//for (uint32_t i = 10000; i < std::min(numVoxels, (uint32_t)100000); i++) {
782-
// QString s = "Voxel " + QString::number(i) + ":";
783-
// for (uint32_t j = 0; j < dimensions; j++) {
784-
// s += " " + QString::number(voxelData[i * dimensions + j]);
785-
// }
786-
// qDebug() << s;
787-
//}
788-
789-
//for (uint32_t i = 0; i < numVoxels; ++i) {
790-
// // Normalize the voxel data to the range [0, 1]
791-
// for (uint32_t j = 0; j < dimensions; ++j) {
792-
// voxelData[i * dimensions + j] = (voxelData[i * dimensions + j] - scalarDataRange.first) / (scalarDataRange.second - scalarDataRange.first);
793-
// }
794-
//}
795-
796-
// Add points to the HNSW index
797-
for (uint32_t i = 0; i < numVoxels; ++i) {
798-
_hnswIndex->addPoint(voxelData.data() + i * dimensions, i);
754+
if (_useFaissANN) {
755+
_nlist = std::clamp(static_cast<int>(numVoxels / 1000), 32, 4096); // nlist is the number of clusters in Faiss
756+
//_nprobe = std::clamp(static_cast<int>(numVoxels / 1000000), 1, 64); // nprobe is the number of clusters to search in Faiss
757+
758+
// IVF index for large datasets
759+
_faissIndexFlat = std::make_unique<faiss::IndexFlatL2>(dimensions);
760+
_faissIndexIVF = std::make_unique<faiss::IndexIVFFlat>(_faissIndexFlat.get(), dimensions, _nlist, faiss::METRIC_L2);
761+
_faissIndexIVF->train(numVoxels, voxelData.data());
762+
_faissIndexIVF->add(numVoxels, voxelData.data());
763+
764+
//_faissIndexIVF->nprobe = _nprobe; // Set the number of clusters to search
765+
799766
}
767+
else {
800768

801-
// Set a high ef for query-time (improves recall, at the expense of query latency)
802-
//_hnswIndex->setEf(_hwnsEfSearch);
803-
804-
// Test recall: query each point for its nearest neighbor.
805-
float correct = 0;
806-
#pragma omp parallel for schedule(guided)
807-
for (int32_t i = 0; i < numVoxels; i++) {
808-
const float* queryPoint = voxelData.data() + i * dimensions;
809-
std::priority_queue<std::pair<float, hnswlib::labeltype>> result = _hnswIndex->searchKnn(queryPoint, 1);
810-
float distance = result.top().first;
811-
#pragma omp critical
812-
{
813-
if (distance == 0)
814-
correct++;
769+
// Initialize HNSW space and index
770+
_hnswSpace = std::make_unique<hnswlib::L2Space>(dimensions); //If we use a local parameter here instead of a member variable we get a crash later on in the program when calling the hwnsIndex again
771+
_hnswIndex = std::make_unique<hnswlib::HierarchicalNSW<float>>(
772+
_hnswSpace.get(),
773+
numVoxels,
774+
_hnswM,
775+
_hnswEfConstruction
776+
);
777+
778+
// Add points to the HNSW index
779+
for (uint32_t i = 0; i < numVoxels; ++i) {
780+
_hnswIndex->addPoint(voxelData.data() + i * dimensions, i);
815781
}
782+
783+
// Set a high ef for query-time (improves recall, at the expense of query latency)
784+
_hnswIndex->setEf(_hwnsEfSearch);
816785
}
817-
float recall = correct / numVoxels;
818-
std::cout << "Recall: " << recall << "\n";
819786
}
820787

821788

@@ -835,34 +802,56 @@ void VolumeRenderer::batchSearch(
835802
qCritical() << "Query data size is not a multiple of dimensions.";
836803
}
837804

838-
// Check that the index is valid.
839-
if (!_hnswIndex) {
840-
qCritical() << "HNSW index is not initialized.";
841-
}
842-
843805
int64_t numQueries = static_cast<int64_t>(queryData.size() / dimensions);
844806

845-
// Prepare a container to hold result vectors (one per query) with k elements each containing a pair of distance and label.
846-
std::vector<std::vector<std::pair<float, hnswlib::labeltype>>> batchResults(numQueries);
847-
qDebug() << "Batch search size: " << batchResults.size();
848-
849-
#pragma omp parallel for schedule(guided)
850-
for (int64_t i = 0; i < numQueries; i++) { // it is important to use int64_t here to avoid overflow crashes
851-
// Find pointer to the start of the i-th query.
852-
const float* query = queryData.data() + static_cast<int64_t>(i * dimensions);
853-
std::priority_queue<std::pair<float, hnswlib::labeltype>> resultQueue = _hnswIndex->searchKnn(query, k);
854-
855-
// Convert the priority queue to a vector.
856-
std::vector<std::pair<float, hnswlib::labeltype>> answers;
857-
while (!resultQueue.empty()) {
858-
answers.push_back(resultQueue.top());
859-
resultQueue.pop();
807+
if (_useFaissANN) {
808+
if (!_faissIndexIVF->is_trained) {
809+
qCritical() << "Faiss IVF index is not trained!";
810+
return;
860811
}
861812

862-
QVector2D meanPos = ComputeMeanOfNN(answers, k, positionData);
863-
meanPositionData[i * 2] = meanPos.x();
864-
meanPositionData[i * 2 + 1] = meanPos.y();
813+
std::vector<faiss::idx_t> labels(numQueries * k);
814+
std::vector<float> distances(numQueries * k);
865815

816+
qDebug() << "Searching for" << k << "nearest neighbors for" << numQueries << "queries using Faiss IVF index.";
817+
_faissIndexIVF->search(numQueries, queryData.data(), k, distances.data(), labels.data());
818+
qDebug() << "Faiss IVF search completed.";
819+
820+
for (int64_t i = 0; i < numQueries; i++) {
821+
std::vector<std::pair<float, int64_t>> answers;
822+
for (int j = 0; j < k; j++) {
823+
answers.emplace_back(distances[i * k + j], labels[i * k + j]);
824+
}
825+
// Compute the mean position of the nearest neighbors.
826+
QVector2D meanPos = ComputeMeanOfNN(answers, k, positionData);
827+
// Store the mean position in the output vector.
828+
meanPositionData[i * 2] = meanPos.x();
829+
meanPositionData[i * 2 + 1] = meanPos.y();
830+
}
831+
}
832+
else {
833+
// Check that the index is valid.
834+
if (!_hnswIndex) {
835+
qCritical() << "HNSW index is not initialized.";
836+
}
837+
838+
#pragma omp parallel for schedule(guided)
839+
for (int64_t i = 0; i < numQueries; i++) { // it is important to use int64_t here to avoid overflow crashes
840+
// Find pointer to the start of the i-th query.
841+
const float* query = queryData.data() + static_cast<int64_t>(i * dimensions);
842+
std::priority_queue<std::pair<float, hnswlib::labeltype>> resultQueue = _hnswIndex->searchKnn(query, k);
843+
844+
// Convert the priority queue to a vector.
845+
std::vector<std::pair<float, int64_t>> answers;
846+
while (!resultQueue.empty()) {
847+
answers.push_back({ resultQueue.top().first, static_cast<int64_t>(resultQueue.top().second) });
848+
resultQueue.pop();
849+
}
850+
QVector2D meanPos = ComputeMeanOfNN(answers, k, positionData);
851+
meanPositionData[i * 2] = meanPos.x();
852+
meanPositionData[i * 2 + 1] = meanPos.y();
853+
854+
}
866855
}
867856
}
868857

@@ -1248,6 +1237,7 @@ void VolumeRenderer::renderBatchToScreen(int batchIndex, uint32_t sampleDim, std
12481237
_fullDataCompositeShader.uniform2f("invFaceTexSize", 1.0f / float(width), 1.0f / float(height));
12491238
_fullDataCompositeShader.uniform2f("invTfTexSize", 1.0f / _tfDataset->getImageSize().width(), 1.0f / _tfDataset->getImageSize().height());
12501239
_fullDataCompositeShader.uniform1f("stepSize", _stepSize);
1240+
_fullDataCompositeShader.uniform1i("useClutterRemover", _useClutterRemover);
12511241

12521242
// Render a full-screen quad to composite the current batch's results over prevFullCompositeTexture.
12531243
drawDVRQuad(_fullDataCompositeShader);
@@ -1311,18 +1301,17 @@ void VolumeRenderer::renderBatchToScreen(int batchIndex, uint32_t sampleDim, std
13111301
// @param neighbours: A vector of pairs containing the distance and label of each neighbour.
13121302
// @param k: The number of neighbours to consider.
13131303
// @param positionData: A vector containing the 2D positions of the neighbours.
1314-
QVector2D VolumeRenderer::ComputeMeanOfNN(const std::vector<std::pair<float, hnswlib::labeltype>>& neighbors, int k, const std::vector<float>& positionData) {
1304+
QVector2D VolumeRenderer::ComputeMeanOfNN(const std::vector<std::pair<float, int64_t>>& neighbors, int k, const std::vector<float>& positionData) {
13151305
float epsilon = 1.0f; // To avoid division by zero and limit the impact of very close neighbours.
13161306
float clusterSlack = 0.1f; // Slack for cluster thresholding, can be adjusted based on the dataset.
13171307

13181308
std::vector<float> weights(k, 0.0f);
13191309
std::vector<QVector2D> candidatePositions(k, QVector2D(0.0f, 0.0f));
13201310
int j = 0;
13211311
for (const auto& entry : neighbors) {
1322-
int32_t index = static_cast<int32_t>(entry.second);
13231312
weights[j] = (1.0f / (entry.first + epsilon)); // Inverse distance is used as weight
1324-
float posX = positionData[index * 2];
1325-
float posY = positionData[index * 2 + 1];
1313+
float posX = positionData[entry.second * 2];
1314+
float posY = positionData[entry.second * 2 + 1];
13261315
candidatePositions[j] = QVector2D(posX, posY);
13271316
j++;
13281317
}
@@ -1479,8 +1468,9 @@ void VolumeRenderer::renderFullData()
14791468

14801469
// Make sure the ANN (e.g. hnswlib) is prepared for the dataset.
14811470
if (!_ANNAlgorithmTrained) {
1482-
prepareHNSW();
1471+
prepareANN();
14831472
_ANNAlgorithmTrained = true;
1473+
qDebug() << "ANN algorithm trained for full data mode.";
14841474
}
14851475

14861476
// Initialize the GPU full data mode parameters if not already done.

DVRViewPlugin/src/VolumeRenderer.h

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
#include "MCArrays.h"
2323

2424
#include <hnswlib.h>
25+
#include <faiss/IndexIVFFlat.h>
26+
#include <faiss/IndexFlat.h>
27+
#include <faiss/Index.h>
28+
2529
#include <QOpenGLFunctions_4_3_Core>
2630

2731
namespace mv {
@@ -117,13 +121,13 @@ class VolumeRenderer : protected QOpenGLFunctions_4_3_Core
117121
void drawDVRQuad(mv::ShaderProgram& shader);
118122

119123
// Full data render mode methods
120-
void prepareHNSW();
124+
void prepareANN();
121125
void batchSearch(const std::vector<float>& queryData, std::vector<float>& positionData, uint32_t dimensions, int k, bool useWeightedMean, std::vector<float>& meanPositionData);
122126
void getFacesTextureData(std::vector<float>& frontfacesData, std::vector<float>& backfacesData);
123127
void getGPUFullDataModeBatches(std::vector<float>& frontfacesData, std::vector<float>& backfacesData);
124128
void retrieveBatchFullData(std::vector<float>& cpuOutput, int batchIndex, bool deleteBuffers);
125129
void renderBatchToScreen(int batchIndex, uint32_t sampleDim, std::vector<float>& meanPositions);
126-
QVector2D ComputeMeanOfNN(const std::vector<std::pair<float, hnswlib::labeltype>>& neighbors, int k, const std::vector<float>& positionData);
130+
QVector2D ComputeMeanOfNN(const std::vector<std::pair<float, int64_t>>& neighbors, int k, const std::vector<float>& positionData);
127131
void updateRenderModeParameters();
128132

129133
void renderFullData();
@@ -230,14 +234,22 @@ class VolumeRenderer : protected QOpenGLFunctions_4_3_Core
230234
mv::Vector3f _cameraPos;
231235

232236
size_t _fullDataMemorySize = 0; // The size of the full data in bytes
233-
size_t _fullGPUMemorySize = static_cast<size_t>(2 * 1024 * 1024) * 1024; // The size of the full data in bytes on the GPU if we use normal int it causes a overflow; The SSBOs are limited to 2GB, so even if the GPU has more VRAM we limit the size to 2GB for the full data mode.
237+
size_t _fullGPUMemorySize = static_cast<size_t>(14 * 1024 * 1024) * 1024; // The size of the full data in bytes on the GPU if we use normal int it causes a overflow; The SSBOs are limited to 2GB, so even if the GPU has more VRAM we limit the size to 2GB for the full data mode.
234238

235-
// HNSWLib-related members
239+
// ANN-related members
236240
std::unique_ptr<hnswlib::L2Space> _hnswSpace;
237241
std::unique_ptr<hnswlib::HierarchicalNSW<float>> _hnswIndex;
238-
int _hnswM = 4;
239-
int _hnswEfConstruction = 50;
242+
std::unique_ptr<faiss::IndexIVFFlat> _faissIndexIVF;
243+
std::unique_ptr<faiss::IndexFlatL2> _faissIndexFlat;
244+
int _hnswM = 8;
245+
int _hnswEfConstruction = 100;
240246
int _hwnsEfSearch = 50;
247+
248+
int _nlist = 1000;
249+
int _nprobe = 100; // Number of probes for Faiss IVF index
250+
251+
// Boolean to select ANN library
252+
bool _useFaissANN = true;
241253

242254
// Full Data Rendermode Parameters
243255
std::vector<std::vector<int>> _GPUBatches; // Batches of pixel indices for the full data mode as it is not always possible to fit all pixels in one batch

0 commit comments

Comments
 (0)