• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 18389801194

09 Oct 2025 09:35PM UTC coverage: 71.943% (+0.1%) from 71.826%
18389801194

push

github

paulmthompson
add correlation matrix to filtering interface

207 of 337 new or added lines in 5 files covered. (61.42%)

867 existing lines in 31 files now uncovered.

49964 of 69449 relevant lines covered (71.94%)

1103.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/WhiskerToolbox/Analysis_Dashboard/Visualizers/Masks/MaskDataVisualization.cpp
1
#include "MaskDataVisualization.hpp"
2

3
#include "CoreGeometry/masks.hpp"
4
#include "CoreGeometry/polygon_adapter.hpp"
5
#include "DataManager/Masks/Mask_Data.hpp"
6

7
#include "Selection/LineSelectionHandler.hpp"
8
#include "Selection/NoneSelectionHandler.hpp"
9
#include "Selection/PointSelectionHandler.hpp"
10
#include "Selection/PolygonSelectionHandler.hpp"
11
#include "ShaderManager/ShaderManager.hpp"
12

13
#include <QDebug>
14
#include <QOpenGLShaderProgram>
15

16
#include <algorithm>
17
#include <cmath>
18
#include <iostream>
19
#include <limits>
20

21
MaskDataVisualization::MaskDataVisualization(QString const & data_key,
×
22
                                             std::shared_ptr<MaskData> const & mask_data)
×
23
    : key(data_key),
×
24
      mask_data(mask_data),
×
25
      quad_vertex_buffer(QOpenGLBuffer::VertexBuffer),
×
26
      color(1.0f, 0.0f, 0.0f, 1.0f),
×
27
      hover_union_polygon(std::vector<Point2D<float>>()) {
×
28

29
    if (!mask_data) {
×
30
        qDebug() << "MaskDataVisualization: Null mask data provided";
×
31
        return;
×
32
    }
33

34
    // Set world bounds based on image size
35
    auto image_size = mask_data->getImageSize();
×
36
    world_min_x = 0.0f;
×
37
    world_max_x = static_cast<float>(image_size.width);
×
38
    world_min_y = 0.0f;
×
39
    world_max_y = static_cast<float>(image_size.height);
×
40

41
    spatial_index = std::make_unique<RTree<MaskIdentifier>>();
×
42

43
    // Precompute all visualization data
44
    populateRTree();
×
45
    createBinaryImageTexture();
×
46

47
    initializeOpenGLResources();
×
48
}
×
49

50
MaskDataVisualization::~MaskDataVisualization() {
×
51
    cleanupOpenGLResources();
×
52
}
×
53

54
void MaskDataVisualization::initializeOpenGLResources() {
×
55
    if (!initializeOpenGLFunctions()) {
×
56
        qDebug() << "MaskDataVisualization: Failed to initialize OpenGL functions";
×
57
        return;
×
58
    }
59

60
    // Load necessary shaders from ShaderManager
61
    if (!ShaderManager::instance().loadProgram("texture",
×
62
                                               ":/shaders/texture.vert",
63
                                               ":/shaders/texture.frag",
64
                                               "",
65
                                               ShaderSourceType::Resource)) {
66
        qDebug() << "MaskDataVisualization: Failed to load texture shader program";
×
67
        return;
×
68
    }
69

70
    if (!ShaderManager::instance().loadProgram("line",
×
71
                                               ":/shaders/line.vert",
72
                                               ":/shaders/line.frag",
73
                                               "",
74
                                               ShaderSourceType::Resource)) {
75
        qDebug() << "MaskDataVisualization: Failed to load line shader program";
×
76
        return;
×
77
    }
78

79
    // Create quad vertex buffer for texture rendering
80
    quad_vertex_array_object.create();
×
81
    quad_vertex_array_object.bind();
×
82

83
    quad_vertex_buffer.create();
×
84
    quad_vertex_buffer.bind();
×
85
    quad_vertex_buffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
×
86

87
    // Quad vertices covering the world bounds
88
    // Note: Texture coordinates are flipped vertically to correct Y-axis orientation
89
    float quad_vertices[] = {
×
90
            world_min_x, world_min_y, 0.0f, 1.0f,// Bottom-left -> Top-left in texture
×
91
            world_max_x, world_min_y, 1.0f, 1.0f,// Bottom-right -> Top-right in texture
×
92
            world_max_x, world_max_y, 1.0f, 0.0f,// Top-right -> Bottom-right in texture
×
93
            world_min_x, world_max_y, 0.0f, 0.0f // Top-left -> Bottom-left in texture
×
94
    };
×
95

96
    quad_vertex_buffer.allocate(quad_vertices, sizeof(quad_vertices));
×
97

98
    // Position attribute
99
    glEnableVertexAttribArray(0);
×
100
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr);
×
101

102
    // Texture coordinate attribute
103
    glEnableVertexAttribArray(1);
×
104
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
×
105
                          reinterpret_cast<void *>(2 * sizeof(float)));
106

107
    quad_vertex_buffer.release();
×
108
    quad_vertex_array_object.release();
×
109

110

111
    // Create hover union polygon buffer
112
    hover_polygon_array_object.create();
×
113
    hover_polygon_array_object.bind();
×
114

115
    hover_polygon_buffer.create();
×
116
    hover_polygon_buffer.bind();
×
117
    hover_polygon_buffer.setUsagePattern(QOpenGLBuffer::DynamicDraw);
×
118
    glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
×
119

120
    glEnableVertexAttribArray(0);
×
121
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
×
122

123
    hover_polygon_buffer.release();
×
124
    hover_polygon_array_object.release();
×
125

126
    // Create binary image texture
127
    glGenTextures(1, &binary_image_texture);
×
128
    glBindTexture(GL_TEXTURE_2D, binary_image_texture);
×
129

130
    if (!binary_image_data.empty()) {
×
131
        auto image_size = mask_data->getImageSize();
×
132
        glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, image_size.width, image_size.height, 0,
×
133
                     GL_RED, GL_FLOAT, binary_image_data.data());
×
134
    }
135

136
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
×
137
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
×
138
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
×
139
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
×
140

141
    glBindTexture(GL_TEXTURE_2D, 0);
×
142
}
143

144
void MaskDataVisualization::cleanupOpenGLResources() {
×
145
    if (quad_vertex_buffer.isCreated()) {
×
146
        quad_vertex_buffer.destroy();
×
147
    }
148
    if (quad_vertex_array_object.isCreated()) {
×
149
        quad_vertex_array_object.destroy();
×
150
    }
151

152
    if (hover_polygon_buffer.isCreated()) {
×
153
        hover_polygon_buffer.destroy();
×
154
    }
155
    if (hover_polygon_array_object.isCreated()) {
×
156
        hover_polygon_array_object.destroy();
×
157
    }
158
    if (binary_image_texture != 0) {
×
159
        glDeleteTextures(1, &binary_image_texture);
×
160
        binary_image_texture = 0;
×
161
    }
162
    if (selection_binary_image_texture != 0) {
×
163
        glDeleteTextures(1, &selection_binary_image_texture);
×
164
        selection_binary_image_texture = 0;
×
165
    }
166
}
×
167

168
void MaskDataVisualization::clearSelection() {
×
169
    if (!selected_masks.empty()) {
×
170
        selected_masks.clear();
×
171
        updateSelectionBinaryImageTexture();
×
172
    }
173
}
×
174

175
void MaskDataVisualization::selectMasks(std::vector<MaskIdentifier> const & mask_ids) {
×
176
    qDebug() << "MaskDataVisualization: Selecting" << mask_ids.size() << "masks";
×
177

178
    for (auto const & mask_id: mask_ids) {
×
179
        selected_masks.insert(mask_id);
×
180
    }
181

182
    updateSelectionBinaryImageTexture();
×
183
    qDebug() << "MaskDataVisualization: Total selected masks:" << selected_masks.size();
×
184
}
×
185

186
bool MaskDataVisualization::toggleMaskSelection(MaskIdentifier const & mask_id) {
×
187
    auto it = selected_masks.find(mask_id);
×
188

189
    if (it != selected_masks.end()) {
×
190
        // Mask is selected, remove it
191
        selected_masks.erase(it);
×
192
        updateSelectionBinaryImageTexture();
×
193
        qDebug() << "MaskDataVisualization: Deselected mask" << mask_id.timeframe << "," << mask_id.mask_index
×
194
                 << "- Total selected:" << selected_masks.size();
×
195
        return false;// Mask was deselected
×
196
    } else {
197
        // Mask is not selected, add it
198
        selected_masks.insert(mask_id);
×
199
        updateSelectionBinaryImageTexture();
×
200
        qDebug() << "MaskDataVisualization: Selected mask" << mask_id.timeframe << "," << mask_id.mask_index
×
201
                 << "- Total selected:" << selected_masks.size();
×
202
        return true;// Mask was selected
×
203
    }
204
}
205

206
bool MaskDataVisualization::removeMaskFromSelection(MaskIdentifier const & mask_id) {
×
207
    auto it = selected_masks.find(mask_id);
×
208

209
    if (it != selected_masks.end()) {
×
210
        // Mask is selected, remove it
211
        selected_masks.erase(it);
×
212
        updateSelectionBinaryImageTexture();
×
213
        qDebug() << "MaskDataVisualization: Removed mask" << mask_id.timeframe << "," << mask_id.mask_index
×
214
                 << "from selection - Total selected:" << selected_masks.size();
×
215
        return true;// Mask was removed
×
216
    }
217

218
    return false;// Mask wasn't selected
×
219
}
220

221
size_t MaskDataVisualization::removeIntersectingMasks(std::vector<MaskIdentifier> const & mask_ids) {
×
222
    size_t removed_count = 0;
×
223

224
    // Find intersection between current selection and provided mask_ids
225
    for (auto const & mask_id: mask_ids) {
×
226
        auto it = selected_masks.find(mask_id);
×
227
        if (it != selected_masks.end()) {
×
228
            // This mask is in both sets - remove it from selection
229
            selected_masks.erase(it);
×
230
            removed_count++;
×
231
            qDebug() << "MaskDataVisualization: Removed intersecting mask"
×
232
                     << mask_id.timeframe << "," << mask_id.mask_index;
×
233
        }
234
    }
235

236
    if (removed_count > 0) {
×
237
        updateSelectionBinaryImageTexture();
×
238
        qDebug() << "MaskDataVisualization: Removed" << removed_count
×
239
                 << "intersecting masks - Total selected:" << selected_masks.size();
×
240
    }
241

242
    return removed_count;
×
243
}
244

245
void MaskDataVisualization::setHoverEntries(std::vector<RTreeEntry<MaskIdentifier>> const & entries) {
×
246
    current_hover_entries = entries;
×
247
    updateHoverUnionPolygon();
×
248
}
×
249

250
void MaskDataVisualization::clearHover() {
×
251
    if (!current_hover_entries.empty()) {
×
252
        current_hover_entries.clear();
×
253
        updateHoverUnionPolygon();
×
254
    }
255
}
×
256

257
std::vector<MaskIdentifier> MaskDataVisualization::findMasksContainingPoint(float world_x, float world_y) const {
×
258
    std::vector<MaskIdentifier> result;
×
259

260
    if (!spatial_index) return result;
×
261

262
    // Use R-tree to find candidate masks
263
    qDebug() << "MaskDataVisualization: Finding masks containing point" << world_x << world_y;
×
264
    BoundingBox point_bbox(world_x, world_y, world_x, world_y);
×
265
    std::vector<RTreeEntry<MaskIdentifier>> candidates;
×
266
    spatial_index->query(point_bbox, candidates);
×
267

268
    qDebug() << "MaskDataVisualization: Found" << candidates.size() << "candidates from R-tree";
×
269

270
    // Check each candidate mask for actual point containment
271
    uint32_t pixel_x = static_cast<uint32_t>(std::round(world_x));
×
272
    uint32_t pixel_y = static_cast<uint32_t>(std::round(world_y));
×
273

274
    for (auto const & candidate: candidates) {
×
275
        /*
276
        if (maskContainsPoint(candidate.data, pixel_x, pixel_y)) {
277
            result.push_back(candidate.data);
278
        }
279
        */
280
        result.push_back(candidate.data);// Use faster RTreeEntry data directly for now
×
281
    }
282

283
    qDebug() << "MaskDataVisualization: Found" << result.size() << "masks containing point";
×
284

285
    return result;
×
286
}
×
287

288
std::vector<MaskIdentifier> MaskDataVisualization::refineMasksContainingPoint(std::vector<RTreeEntry<MaskIdentifier>> const & entries, float world_x, float world_y) const {
×
289
    std::vector<MaskIdentifier> result;
×
290

291
    if (!mask_data) return result;
×
292

293
    qDebug() << "MaskDataVisualization: Refining" << entries.size() << "R-tree entries using precise point checking";
×
294

295
    // Check each candidate mask for actual point containment
296
    uint32_t pixel_x = static_cast<uint32_t>(std::round(world_x));
×
297
    uint32_t pixel_y = static_cast<uint32_t>(std::round(world_y));
×
298

299
    for (auto const & entry: entries) {
×
300
        if (maskContainsPoint(entry.data, pixel_x, pixel_y)) {
×
301
            result.push_back(entry.data);
×
302
        }
303
    }
304

305
    qDebug() << "MaskDataVisualization: Refined to" << result.size() << "masks containing point after precise checking";
×
306

307
    return result;
×
308
}
×
309

310
void MaskDataVisualization::render(QMatrix4x4 const & mvp_matrix) {
×
311
    auto textureProgram = ShaderManager::instance().getProgram("texture");
×
312
    if (textureProgram && textureProgram->getNativeProgram()->bind()) {
×
313
        textureProgram->getNativeProgram()->setUniformValue("u_mvp_matrix", mvp_matrix);
×
314

315
        glEnable(GL_BLEND);
×
316
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
×
317

318
        renderBinaryImage(textureProgram->getNativeProgram());
×
319
        renderSelectedMasks(textureProgram->getNativeProgram());
×
320

321
        textureProgram->getNativeProgram()->release();
×
322
    }
323

324
    auto lineProgram = ShaderManager::instance().getProgram("line");
×
325
    if (lineProgram && lineProgram->getNativeProgram()->bind()) {
×
326
        lineProgram->getNativeProgram()->setUniformValue("u_mvp_matrix", mvp_matrix);
×
327

328
        glEnable(GL_BLEND);
×
329
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
×
330

331
        renderHoverMaskUnionPolygon(lineProgram->getNativeProgram());
×
332

333
        lineProgram->getNativeProgram()->release();
×
334
    }
335
}
×
336

337
void MaskDataVisualization::renderBinaryImage(QOpenGLShaderProgram * shader_program) {
×
338
    if (!visible || binary_image_texture == 0) return;
×
339

340
    quad_vertex_array_object.bind();
×
341
    quad_vertex_buffer.bind();
×
342

343
    // Bind texture
344
    glActiveTexture(GL_TEXTURE0);
×
345
    glBindTexture(GL_TEXTURE_2D, binary_image_texture);
×
346
    shader_program->setUniformValue("u_texture", 0);
×
347
    shader_program->setUniformValue("u_color", color);
×
348

349
    // Draw quad
350
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
351

352
    glBindTexture(GL_TEXTURE_2D, 0);
×
353
    quad_vertex_buffer.release();
×
354
    quad_vertex_array_object.release();
×
355
}
356

357
BoundingBox MaskDataVisualization::calculateBounds() const {
×
358
    if (!mask_data) {
×
359
        return BoundingBox(0, 0, 0, 0);
×
360
    }
361

362
    auto image_size = mask_data->getImageSize();
×
363
    return BoundingBox(0, 0, static_cast<float>(image_size.width), static_cast<float>(image_size.height));
×
364
}
365

366
void MaskDataVisualization::createBinaryImageTexture() {
×
367
    if (!mask_data) return;
×
368

369
    qDebug() << "MaskDataVisualization: Creating binary image texture with" << mask_data->size() << "time frames";
×
370

371
    auto image_size = mask_data->getImageSize();
×
372
    binary_image_data.resize(image_size.width * image_size.height, 0.0f);
×
373

374
    // Aggregate all masks into the binary image
375
    for (auto const & time_masks_pair: mask_data->getAllAsRange()) {
×
376
        for (auto const & mask: time_masks_pair.masks) {
×
377
            for (auto const & point: mask) {
×
378
                if (point.x < image_size.width && point.y < image_size.height) {
×
379
                    size_t index = point.y * image_size.width + point.x;
×
380
                    binary_image_data[index] += 1.0f;
×
381
                }
382
            }
383
        }
UNCOV
384
    }
×
385

386
    qDebug() << "MaskDataVisualization: Binary image texture created with" << binary_image_data.size() << "pixels";
×
387

388
    // Apply non-linear scaling to improve visibility of sparse regions
389
    if (!binary_image_data.empty()) {
×
390
        float max_value = *std::max_element(binary_image_data.begin(), binary_image_data.end());
×
391
        qDebug() << "MaskDataVisualization: Max mask density:" << max_value;
×
392

393
        if (max_value > 0.0f) {
×
394
            // Use logarithmic scaling to compress the dynamic range
395
            // This makes sparse areas more visible while preserving dense areas
396
            for (auto & value: binary_image_data) {
×
397
                if (value > 0.0f) {
×
398
                    // Apply log scaling: log(1 + value) / log(1 + max_value)
399
                    // This ensures that even single masks (value=1) are visible
400
                    value = std::log(1.0f + value) / std::log(1.0f + max_value);
×
401
                }
402
            }
403

404
            // Debug: Check scaled values
405
            float min_scaled = *std::min_element(binary_image_data.begin(), binary_image_data.end());
×
406
            float max_scaled = *std::max_element(binary_image_data.begin(), binary_image_data.end());
×
407
            qDebug() << "MaskDataVisualization: Scaled texture range: min=" << min_scaled << "max=" << max_scaled;
×
408
        }
409
    }
410

411
    qDebug() << "MaskDataVisualization: Binary image texture scaled with logarithmic normalization";
×
412
}
413

414
void MaskDataVisualization::updateSelectionBinaryImageTexture() {
×
415
    if (!mask_data) return;
×
416

417
    qDebug() << "MaskDataVisualization: Updating selection binary image texture with" << selected_masks.size() << "selected masks";
×
418

419
    auto image_size = mask_data->getImageSize();
×
420
    selection_binary_image_data.clear();
×
421
    selection_binary_image_data.resize(image_size.width * image_size.height, 0.0f);
×
422

423
    // Only include selected masks in the selection binary image
424
    for (auto const & mask_id: selected_masks) {
×
425
        auto const & masks = mask_data->getAtTime(TimeFrameIndex(mask_id.timeframe));
×
426
        if (mask_id.mask_index < masks.size()) {
×
427
            auto const & mask = masks[mask_id.mask_index];
×
428

429
            for (auto const & point: mask) {
×
430
                if (point.x < image_size.width && point.y < image_size.height) {
×
431
                    size_t index = point.y * image_size.width + point.x;
×
432
                    selection_binary_image_data[index] = 1.0f;// Uniform opacity for selected masks
×
433
                }
434
            }
435
        }
436
    }
437

438
    // Update the OpenGL texture if it exists
439
    if (selection_binary_image_texture != 0) {
×
440
        glBindTexture(GL_TEXTURE_2D, selection_binary_image_texture);
×
441
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image_size.width, image_size.height,
×
442
                        GL_RED, GL_FLOAT, selection_binary_image_data.data());
×
443
        glBindTexture(GL_TEXTURE_2D, 0);
×
444
    } else {
445
        // Create the texture if it doesn't exist
446
        glGenTextures(1, &selection_binary_image_texture);
×
447
        glBindTexture(GL_TEXTURE_2D, selection_binary_image_texture);
×
448
        glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, image_size.width, image_size.height, 0,
×
449
                     GL_RED, GL_FLOAT, selection_binary_image_data.data());
×
450
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
×
451
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
×
452
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
×
453
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
×
454
        glBindTexture(GL_TEXTURE_2D, 0);
×
455
    }
456

457
    qDebug() << "MaskDataVisualization: Selection binary image texture updated";
×
458
}
459

460
void MaskDataVisualization::populateRTree() {
×
461
    if (!mask_data || !spatial_index) return;
×
462

463
    qDebug() << "MaskDataVisualization: Populating R-tree with" << mask_data->size() << "time frames";
×
464

465
    for (auto const & time_masks_pair: mask_data->getAllAsRange()) {
×
466
        for (size_t mask_index = 0; mask_index < time_masks_pair.masks.size(); ++mask_index) {
×
467
            auto const & mask = time_masks_pair.masks[mask_index];
×
468

469
            if (mask.empty()) continue;
×
470

471
            // Calculate bounding box for this mask
472
            auto [min_point, max_point] = get_bounding_box(mask);
×
473

474
            BoundingBox bbox(static_cast<float>(min_point.x), static_cast<float>(min_point.y),
×
475
                             static_cast<float>(max_point.x), static_cast<float>(max_point.y));
×
476

477
            MaskIdentifier mask_id(time_masks_pair.time.getValue(), mask_index);
×
478
            spatial_index->insert(bbox, mask_id);
×
479
        }
UNCOV
480
    }
×
481

482
    qDebug() << "MaskDataVisualization: R-tree populated with" << spatial_index->size() << "masks";
×
483
}
484

485
bool MaskDataVisualization::maskContainsPoint(MaskIdentifier const & mask_id, uint32_t pixel_x, uint32_t pixel_y) const {
×
486
    if (!mask_data) return false;
×
487

488
    //return true;
489

490
    auto const & masks = mask_data->getAtTime(TimeFrameIndex(mask_id.timeframe));
×
491

492
    if (masks.empty()) return false;
×
493

494
    auto const & mask = masks[mask_id.mask_index];
×
495

496
    for (auto const & point: mask) {
×
497
        if (point.x == pixel_x && point.y == pixel_y) {
×
498
            return true;
×
499
        }
500
    }
501

502
    return false;
×
503
}
504

505
std::pair<float, float> MaskDataVisualization::worldToTexture(float world_x, float world_y) const {
×
506
    if (!mask_data) return {0.0f, 0.0f};
×
507

508
    auto image_size = mask_data->getImageSize();
×
509

510
    float u = (world_x - world_min_x) / (world_max_x - world_min_x);
×
511
    float v = (world_y - world_min_y) / (world_max_y - world_min_y);
×
512

513
    return {u, v};
×
514
}
515

516
void MaskDataVisualization::renderHoverMaskUnionPolygon(QOpenGLShaderProgram * shader_program) {
×
517
    if (current_hover_entries.empty() || hover_polygon_data.empty()) return;
×
518

519
    hover_polygon_array_object.bind();
×
520
    hover_polygon_buffer.bind();
×
521

522
    // Set uniforms for polygon rendering (black outline)
523
    QVector4D polygon_color = QVector4D(0.0f, 0.0f, 0.0f, 1.0f);// Black
×
524
    shader_program->setUniformValue("u_color", polygon_color);
×
525

526
    glLineWidth(3.0f);// Thick lines for visibility
×
527

528
    // Draw the union polygon as a line loop
529
    glDrawArrays(GL_LINE_LOOP, 0, static_cast<GLsizei>(hover_polygon_data.size() / 2));
×
530

531
    hover_polygon_buffer.release();
×
532
    hover_polygon_array_object.release();
×
533
}
534

535
void MaskDataVisualization::renderSelectedMasks(QOpenGLShaderProgram * shader_program) {
×
536
    if (!visible || selection_binary_image_texture == 0 || selected_masks.empty()) return;
×
537

538
    quad_vertex_array_object.bind();
×
539
    quad_vertex_buffer.bind();
×
540

541
    // Bind selection texture
542
    glActiveTexture(GL_TEXTURE0);
×
543
    glBindTexture(GL_TEXTURE_2D, selection_binary_image_texture);
×
544
    shader_program->setUniformValue("u_texture", 0);
×
545

546
    // Set different color for selected masks (e.g., yellow with some transparency)
547
    QVector4D selection_color = QVector4D(1.0f, 1.0f, 0.0f, 0.7f);// Yellow with 70% opacity
×
548
    shader_program->setUniformValue("u_color", selection_color);
×
549

550
    // Enable blending for transparency
551
    glEnable(GL_BLEND);
×
552
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
×
553

554
    // Draw quad
555
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
556

557
    // Disable blending
558
    glDisable(GL_BLEND);
×
559

560
    glBindTexture(GL_TEXTURE_2D, 0);
×
561
    quad_vertex_buffer.release();
×
562
    quad_vertex_array_object.release();
×
563
}
564

565
void MaskDataVisualization::updateHoverUnionPolygon() {
×
566
    if (current_hover_entries.empty()) {
×
567
        hover_union_polygon = Polygon(std::vector<Point2D<float>>{});
×
568
        hover_polygon_data.clear();
×
569
    } else {
570
        hover_union_polygon = computeUnionPolygonFromEntries(current_hover_entries);
×
571
        hover_polygon_data = generatePolygonVertexData(hover_union_polygon);
×
572
    }
573

574
    hover_polygon_array_object.bind();
×
575
    hover_polygon_buffer.bind();
×
576

577
    if (hover_polygon_data.empty()) {
×
578
        glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
×
579
    } else {
580
        hover_polygon_buffer.allocate(hover_polygon_data.data(),
×
581
                                      static_cast<int>(hover_polygon_data.size() * sizeof(float)));
×
582
    }
583

584
    glEnableVertexAttribArray(0);
×
585
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
×
586

587
    hover_polygon_buffer.release();
×
588
    hover_polygon_array_object.release();
×
589
}
×
590

591
Polygon MaskDataVisualization::computeUnionPolygonFromEntries(std::vector<RTreeEntry<MaskIdentifier>> const & entries) const {
×
592
    // Use the new polygon containment-based algorithm
593
    return computeUnionPolygonUsingContainment(entries);
×
594
}
595

596
std::vector<float> MaskDataVisualization::generatePolygonVertexData(Polygon const & polygon) const {
×
597
    std::vector<float> vertex_data;
×
598

599
    if (!polygon.isValid()) {
×
600
        return vertex_data;
×
601
    }
602

603
    auto const & vertices = polygon.getVertices();
×
604
    vertex_data.reserve(vertices.size() * 2);
×
605

606
    for (auto const & vertex: vertices) {
×
607
        vertex_data.push_back(vertex.x);
×
608
        vertex_data.push_back(flipY(vertex.y));// Flip Y coordinate for OpenGL rendering
×
609
    }
610

611
    return vertex_data;
612
}
×
613

614
// Helper function to check if all 4 corners of a bounding box are contained in a polygon
615
static bool isBoundingBoxContainedInPolygon(BoundingBox const & bbox, Polygon const & polygon) {
×
616
    return polygon.containsPoint({bbox.min_x, bbox.min_y}) &&
×
617
           polygon.containsPoint({bbox.max_x, bbox.min_y}) &&
×
618
           polygon.containsPoint({bbox.max_x, bbox.max_y}) &&
×
619
           polygon.containsPoint({bbox.min_x, bbox.max_y});
×
620
}
621

622
/**
623
 * @brief Compute union polygon using polygon containment checking with raycasting
624
 * 
625
 * Algorithm:
626
 * 1. Sort bounding boxes by area (largest first)
627
 * 2. Start with largest box as "comparison polygon"
628
 * 3. Process smaller boxes from largest to smallest
629
 * 4. For each box, check if all 4 corners are contained in comparison polygon
630
 * 5. If contained, skip the box
631
 * 6. If not contained, union the box with comparison polygon and update comparison polygon
632
 * 7. Track number of union operations performed
633
 */
634
static Polygon computeUnionPolygonUsingContainment(std::vector<RTreeEntry<MaskIdentifier>> const & entries) {
×
635
    if (entries.empty()) {
×
636
        return Polygon(std::vector<Point2D<float>>{});
×
637
    }
638

639
    if (entries.size() == 1) {
×
640
        BoundingBox bbox(entries[0].min_x, entries[0].min_y, entries[0].max_x, entries[0].max_y);
×
641
        return Polygon(bbox);
×
642
    }
643

644
    std::cout << "MaskDataVisualization: Computing union using polygon containment with " << entries.size() << " bounding boxes" << std::endl;
×
645

646
    // Convert entries to BoundingBox objects with their areas
647
    std::vector<std::pair<BoundingBox, float>> bbox_with_areas;
×
648
    bbox_with_areas.reserve(entries.size());
×
649

650
    for (auto const & entry: entries) {
×
651
        BoundingBox bbox(entry.min_x, entry.min_y, entry.max_x, entry.max_y);
×
652
        float area = bbox.width() * bbox.height();
×
653
        bbox_with_areas.emplace_back(bbox, area);
×
654
    }
655

656
    // Sort by area (largest first)
657
    std::sort(bbox_with_areas.begin(), bbox_with_areas.end(),
×
658
              [](auto const & a, auto const & b) { return a.second > b.second; });
×
659

660
    Polygon comparison_polygon(bbox_with_areas[0].first);
×
661

662
    size_t union_operations = 0;
×
663

664
    // Process remaining boxes from largest to smallest
665
    for (size_t i = 1; i < bbox_with_areas.size(); ++i) {
×
666
        BoundingBox const & test_bbox = bbox_with_areas[i].first;
×
667

668
        // Check if all 4 corners of test_bbox are contained in comparison_polygon
669
        if (isBoundingBoxContainedInPolygon(test_bbox, comparison_polygon)) {
×
670
            // Test box is completely contained - skip it
671
            continue;
×
672
        }
673

674

675
        Polygon test_polygon(test_bbox);
×
676
        Polygon new_comparison = comparison_polygon.unionWith(test_polygon);
×
677

678
        if (!new_comparison.isValid()) {
×
679
            std::cout << "MaskDataVisualization: Union operation failed! Falling back to bounding box approximation" << std::endl;
×
680
            // Fall back to overall bounding box if union fails
681
            std::vector<BoundingBox> remaining_boxes;
×
682
            for (size_t j = 0; j <= i; ++j) {
×
683
                remaining_boxes.push_back(bbox_with_areas[j].first);
×
684
            }
685
            BoundingBox overall_bbox(
×
686
                    std::min_element(remaining_boxes.begin(), remaining_boxes.end(),
×
687
                                     [](BoundingBox const & a, BoundingBox const & b) { return a.min_x < b.min_x; })
×
688
                            ->min_x,
×
689
                    std::min_element(remaining_boxes.begin(), remaining_boxes.end(),
×
690
                                     [](BoundingBox const & a, BoundingBox const & b) { return a.min_y < b.min_y; })
×
691
                            ->min_y,
×
692
                    std::max_element(remaining_boxes.begin(), remaining_boxes.end(),
×
693
                                     [](BoundingBox const & a, BoundingBox const & b) { return a.max_x < b.max_x; })
×
694
                            ->max_x,
×
695
                    std::max_element(remaining_boxes.begin(), remaining_boxes.end(),
×
696
                                     [](BoundingBox const & a, BoundingBox const & b) { return a.max_y < b.max_y; })
×
697
                            ->max_y);
×
698
            return Polygon(overall_bbox);
×
699
        }
×
700

701
        comparison_polygon = new_comparison;
×
702
        union_operations++;
×
703
    }
×
704

705
    std::cout << "MaskDataVisualization: Algorithm completed. Total union operations: " << union_operations
×
706
              << " out of " << (entries.size() - 1) << " possible operations" << std::endl;
×
707
    std::cout << "MaskDataVisualization: Final polygon has " << comparison_polygon.vertexCount() << " vertices" << std::endl;
×
708

709
    return comparison_polygon;
×
710
}
×
711

712
void MaskDataVisualization::applySelection(SelectionVariant & selection_handler) {
×
713
    if (std::holds_alternative<std::unique_ptr<PolygonSelectionHandler>>(selection_handler)) {
×
714
        applySelection(*std::get<std::unique_ptr<PolygonSelectionHandler>>(selection_handler));
×
715
    } else if (std::holds_alternative<std::unique_ptr<PointSelectionHandler>>(selection_handler)) {
×
716
        applySelection(*std::get<std::unique_ptr<PointSelectionHandler>>(selection_handler));
×
717
    } else {
718
        std::cout << "MaskDataVisualization::applySelection: selection_handler is not a PolygonSelectionHandler" << std::endl;
×
719
    }
720
}
×
721

722
void MaskDataVisualization::applySelection(PolygonSelectionHandler const & selection_handler) {
×
723

724
    std::cout << "Mask Data Polygon Selection not implemented" << std::endl;
×
725
}
×
726

727
void MaskDataVisualization::applySelection(PointSelectionHandler const & selection_handler) {
×
728
    QVector2D world_pos = selection_handler.getWorldPos();
×
729
    Qt::KeyboardModifiers modifiers = selection_handler.getModifiers();
×
730

731
    BoundingBox point_bbox(world_pos.x(), world_pos.y(), world_pos.x(), world_pos.y());
×
732
    std::vector<RTreeEntry<MaskIdentifier>> entries;
×
733
    spatial_index->query(point_bbox, entries);
×
734

735
    if (entries.empty()) return;
×
736

737
    auto refined_masks = refineMasksContainingPoint(entries, world_pos.x(), world_pos.y());
×
738
    if (refined_masks.empty()) return;
×
739

740
    if (modifiers & Qt::ControlModifier) {
×
741
        // Toggle the first mask found
742
        toggleMaskSelection(refined_masks[0]);
×
743
    } else if (modifiers & Qt::ShiftModifier) {
×
744
        removeIntersectingMasks(refined_masks);
×
745
    }
746
}
×
747

748
QString MaskDataVisualization::getTooltipText() const {
×
749
    if (current_hover_entries.empty()) {
×
750
        return QString();
×
751
    }
752

753
    size_t total_hover_masks = current_hover_entries.size();
×
754
    QString tooltip_text = QString("%1: %2 masks").arg(key).arg(total_hover_masks);
×
755
    return tooltip_text;
×
756
}
×
757

758
bool MaskDataVisualization::handleHover(QVector2D const & world_pos) {
×
759
    BoundingBox point_bbox(world_pos.x(), world_pos.y(), world_pos.x(), world_pos.y());
×
760
    std::vector<RTreeEntry<MaskIdentifier>> entries;
×
761
    spatial_index->query(point_bbox, entries);
×
762

763
    // This comparison is simplified. For a more robust check, we would need to
764
    // compare the content of the vectors.
765
    bool hover_changed = (current_hover_entries.size() != entries.size());
×
766

767
    if (hover_changed) {
×
768
        setHoverEntries(entries);
×
769
    }
770

771
    return hover_changed;
×
772
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc