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

paulmthompson / WhiskerToolbox / 17346268452

30 Aug 2025 04:51PM UTC coverage: 68.446% (+0.6%) from 67.856%
17346268452

push

github

paulmthompson
mask skeletonization tests added including with json transformation

188 of 194 new or added lines in 1 file covered. (96.91%)

169 existing lines in 4 files now uncovered.

28772 of 42036 relevant lines covered (68.45%)

1418.98 hits per line

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

96.91
/src/DataManager/transforms/Masks/Mask_Skeletonize/mask_skeletonize.test.cpp
1
#include "catch2/catch_test_macros.hpp"
2
#include "catch2/matchers/catch_matchers_vector.hpp"
3

4
#include "Masks/Mask_Data.hpp"
5
#include "transforms/Masks/Mask_Skeletonize/mask_skeletonize.hpp"
6
#include "transforms/data_transforms.hpp" // For ProgressCallback
7

8
#include <vector>
9
#include <memory> // std::make_shared
10
#include <functional> // std::function
11

12
// Using Catch::Matchers::Equals for vectors of floats.
13

14
TEST_CASE("Data Transform: Mask Skeletonize - Happy Path", "[transforms][mask_skeletonize]") {
3✔
15
    std::shared_ptr<MaskData> mask_data;
3✔
16
    std::shared_ptr<MaskData> result_skeletonized;
3✔
17
    MaskSkeletonizeParameters params;
3✔
18
    volatile int progress_val = -1; // Volatile to prevent optimization issues in test
3✔
19
    volatile int call_count = 0;    // Volatile for the same reason
3✔
20
    ProgressCallback cb = [&](int p) {
6✔
21
        progress_val = p;
3✔
22
        call_count = call_count + 1;
3✔
23
    };
3✔
24

25
    SECTION("Simple rectangular mask") {
3✔
26
        // Create a simple rectangular mask
27
        mask_data = std::make_shared<MaskData>();
1✔
28
        
29
        // Create a 10x10 rectangular mask at time 100 - all points in one mask
30
        std::vector<uint32_t> x_coords;
1✔
31
        std::vector<uint32_t> y_coords;
1✔
32
        
33
        // Build all points for a 10x10 rectangle
34
        for (uint32_t row = 1; row <= 10; ++row) {
11✔
35
            for (uint32_t col = 1; col <= 10; ++col) {
110✔
36
                x_coords.push_back(col);
100✔
37
                y_coords.push_back(row);
100✔
38
            }
39
        }
40
        
41
        mask_data->addAtTime(TimeFrameIndex(100), x_coords, y_coords);
1✔
42

43
        result_skeletonized = skeletonize_mask(mask_data.get(), &params);
1✔
44
        REQUIRE(result_skeletonized != nullptr);
1✔
45
        
46
        // Verify that skeletonization produced a result (should be thinner than original)
47
        auto original_masks = mask_data->getAtTime(TimeFrameIndex(100));
1✔
48
        auto skeletonized_masks = result_skeletonized->getAtTime(TimeFrameIndex(100));
1✔
49
        
50
        REQUIRE(!original_masks.empty());
1✔
51
        REQUIRE(!skeletonized_masks.empty());
1✔
52
        
53
        // The skeletonized version should have fewer points than the original
54
        size_t original_points = 0;
1✔
55
        for (const auto& mask : original_masks) {
2✔
56
            original_points += mask.size();
1✔
57
        }
58
        
59
        size_t skeletonized_points = 0;
1✔
60
        for (const auto& mask : skeletonized_masks) {
2✔
61
            skeletonized_points += mask.size();
1✔
62
        }
63
        
64
        REQUIRE(skeletonized_points < original_points);
1✔
65

66
        // Test with progress callback
67
        progress_val = -1;
1✔
68
        call_count = 0;
1✔
69
        result_skeletonized = skeletonize_mask(mask_data.get(), &params, cb);
1✔
70
        REQUIRE(result_skeletonized != nullptr);
1✔
71
        REQUIRE(progress_val == 100);
1✔
72
        REQUIRE(call_count > 0);
1✔
73
    }
4✔
74

75
    SECTION("Empty mask data") {
3✔
76
        mask_data = std::make_shared<MaskData>();
1✔
77
        
78
        result_skeletonized = skeletonize_mask(mask_data.get(), &params);
1✔
79
        REQUIRE(result_skeletonized != nullptr);
1✔
80
        
81
        // Should return empty result for empty input
82
        auto result_masks = result_skeletonized->getAtTime(TimeFrameIndex(100));
1✔
83
        REQUIRE(result_masks.empty());
1✔
84
    }
4✔
85

86
    SECTION("Null mask data") {
3✔
87
        result_skeletonized = skeletonize_mask(nullptr, &params);
1✔
88
        REQUIRE(result_skeletonized != nullptr);
1✔
89
        
90
        // Should return empty result for null input
91
        auto result_masks = result_skeletonized->getAtTime(TimeFrameIndex(100));
1✔
92
        REQUIRE(result_masks.empty());
1✔
93
    }
4✔
94
}
6✔
95

96
TEST_CASE("Data Transform: Mask Skeletonize - Error and Edge Cases", "[transforms][mask_skeletonize]") {
2✔
97
    std::shared_ptr<MaskData> mask_data;
2✔
98
    std::shared_ptr<MaskData> result_skeletonized;
2✔
99
    MaskSkeletonizeParameters params;
2✔
100
    volatile int progress_val = -1;
2✔
101
    volatile int call_count = 0;
2✔
102
    ProgressCallback cb = [&](int p) {
4✔
NEW
103
        progress_val = p;
×
NEW
104
        call_count = call_count + 1;
×
105
    };
2✔
106

107
    SECTION("Single point mask") {
2✔
108
        mask_data = std::make_shared<MaskData>();
1✔
109
        
110
        // Create a single point mask
111
        std::vector<uint32_t> x_coords = {5};
3✔
112
        std::vector<uint32_t> y_coords = {5};
3✔
113
        mask_data->addAtTime(TimeFrameIndex(100), x_coords, y_coords);
1✔
114

115
        result_skeletonized = skeletonize_mask(mask_data.get(), &params);
1✔
116
        REQUIRE(result_skeletonized != nullptr);
1✔
117
        
118
        // Single point should remain a single point after skeletonization
119
        auto result_masks = result_skeletonized->getAtTime(TimeFrameIndex(100));
1✔
120
        REQUIRE(!result_masks.empty());
1✔
121
        REQUIRE(result_masks[0].size() == 1);
1✔
122
    }
3✔
123

124
    SECTION("Multiple time frames") {
2✔
125
        mask_data = std::make_shared<MaskData>();
1✔
126
        
127
        // Create masks at multiple time frames
128
        for (int time = 100; time <= 105; time += 5) {
3✔
129
            // Create a 5x5 square mask at each time frame
130
            std::vector<uint32_t> x_coords;
2✔
131
            std::vector<uint32_t> y_coords;
2✔
132
            
133
            for (uint32_t row = 1; row <= 5; ++row) {
12✔
134
                for (uint32_t col = 1; col <= 5; ++col) {
60✔
135
                    x_coords.push_back(col);
50✔
136
                    y_coords.push_back(row);
50✔
137
                }
138
            }
139
            
140
            mask_data->addAtTime(TimeFrameIndex(time), x_coords, y_coords);
2✔
141
        }
2✔
142

143
        result_skeletonized = skeletonize_mask(mask_data.get(), &params);
1✔
144
        REQUIRE(result_skeletonized != nullptr);
1✔
145
        
146
        // Should process all time frames
147
        for (int time = 100; time <= 105; time += 5) {
3✔
148
            auto result_masks = result_skeletonized->getAtTime(TimeFrameIndex(time));
2✔
149
            REQUIRE(!result_masks.empty());
2✔
150
        }
2✔
151
    }
2✔
152
}
4✔
153

154
#include "DataManager.hpp"
155
#include "IO/LoaderRegistry.hpp"
156
#include "transforms/TransformPipeline.hpp"
157
#include "transforms/TransformRegistry.hpp"
158

159
#include <filesystem>
160
#include <fstream>
161
#include <iostream>
162

163
TEST_CASE("Data Transform: Mask Skeletonize - JSON pipeline", "[transforms][mask_skeletonize][json]") {
1✔
164
    const nlohmann::json json_config = {
1✔
165
        {"steps", {{
166
            {"step_id", "skeletonize_step_1"},
167
            {"transform_name", "Skeletonize Mask"},
168
            {"input_key", "TestMask"},
169
            {"output_key", "SkeletonizedMask"},
170
            {"parameters", {
171
                // No parameters needed for skeletonization
172
            }}
173
        }}}
174
    };
29✔
175

176
    DataManager dm;
1✔
177
    TransformRegistry registry;
1✔
178

179
    auto time_frame = std::make_shared<TimeFrame>();
1✔
180
    dm.setTime(TimeKey("default"), time_frame);
1✔
181

182
    // Create test mask data
183
    auto mask_data = std::make_shared<MaskData>();
1✔
184
    
185
    // Create a simple rectangular mask - all points in one mask
186
    std::vector<uint32_t> x_coords;
1✔
187
    std::vector<uint32_t> y_coords;
1✔
188
    
189
    // Build all points for a 10x10 rectangle
190
    for (uint32_t row = 1; row <= 10; ++row) {
11✔
191
        for (uint32_t col = 1; col <= 10; ++col) {
110✔
192
            x_coords.push_back(col);
100✔
193
            y_coords.push_back(row);
100✔
194
        }
195
    }
196
    
197
    mask_data->addAtTime(TimeFrameIndex(100), x_coords, y_coords);
1✔
198
    
199
    mask_data->setTimeFrame(time_frame);
1✔
200
    dm.setData("TestMask", mask_data, TimeKey("default"));
3✔
201

202
    TransformPipeline pipeline(&dm, &registry);
1✔
203
    pipeline.loadFromJson(json_config);
1✔
204
    pipeline.execute();
1✔
205

206
    // Verify the results
207
    auto skeletonized_mask = dm.getData<MaskData>("SkeletonizedMask");
3✔
208
    REQUIRE(skeletonized_mask != nullptr);
1✔
209

210
    // Verify that skeletonization produced a result
211
    auto original_masks = mask_data->getAtTime(TimeFrameIndex(100));
1✔
212
    auto result_masks = skeletonized_mask->getAtTime(TimeFrameIndex(100));
1✔
213
    
214
    REQUIRE(!original_masks.empty());
1✔
215
    REQUIRE(!result_masks.empty());
1✔
216
    
217
    // The skeletonized version should have fewer points than the original
218
    size_t original_points = 0;
1✔
219
    for (const auto& mask : original_masks) {
2✔
220
        original_points += mask.size();
1✔
221
    }
222
    
223
    size_t skeletonized_points = 0;
1✔
224
    for (const auto& mask : result_masks) {
2✔
225
        skeletonized_points += mask.size();
1✔
226
    }
227
    
228
    REQUIRE(skeletonized_points < original_points);
1✔
229
}
29✔
230

231
#include "transforms/ParameterFactory.hpp"
232
#include "transforms/TransformRegistry.hpp"
233

234
TEST_CASE("Data Transform: Mask Skeletonize - Parameter Factory", "[transforms][mask_skeletonize][factory]") {
1✔
235
    auto& factory = ParameterFactory::getInstance();
1✔
236
    factory.initializeDefaultSetters();
1✔
237

238
    auto params_base = std::make_unique<MaskSkeletonizeParameters>();
1✔
239
    REQUIRE(params_base != nullptr);
1✔
240

241
    // No parameters to set for skeletonization, but test the factory works
242
    const nlohmann::json params_json = {
1✔
243
        // Empty since skeletonization has no parameters
244
    };
1✔
245

246
    for (auto const& [key, val] : params_json.items()) {
1✔
NEW
247
        factory.setParameter("Skeletonize Mask", params_base.get(), key, val, nullptr);
×
248
    }
1✔
249

250
    auto* params = dynamic_cast<MaskSkeletonizeParameters*>(params_base.get());
1✔
251
    REQUIRE(params != nullptr);
1✔
252
}
2✔
253

254
TEST_CASE("Data Transform: Mask Skeletonize - load_data_from_json_config", "[transforms][mask_skeletonize][json_config]") {
1✔
255
    // Create DataManager and populate it with MaskData in code
256
    DataManager dm;
1✔
257

258
    // Create a TimeFrame for our data
259
    auto time_frame = std::make_shared<TimeFrame>();
1✔
260
    dm.setTime(TimeKey("default"), time_frame);
1✔
261
    
262
    // Create test mask data in code
263
    auto test_mask = std::make_shared<MaskData>();
1✔
264
    
265
    // Create a simple rectangular mask - all points in one mask
266
    std::vector<uint32_t> x_coords;
1✔
267
    std::vector<uint32_t> y_coords;
1✔
268
    
269
    // Build all points for a 10x10 rectangle
270
    for (uint32_t row = 1; row <= 10; ++row) {
11✔
271
        for (uint32_t col = 1; col <= 10; ++col) {
110✔
272
            x_coords.push_back(col);
100✔
273
            y_coords.push_back(row);
100✔
274
        }
275
    }
276
    
277
    test_mask->addAtTime(TimeFrameIndex(100), x_coords, y_coords);
1✔
278
    
279
    test_mask->setTimeFrame(time_frame);
1✔
280
    
281
    // Store the mask data in DataManager with a known key
282
    dm.setData("test_mask", test_mask, TimeKey("default"));
3✔
283
    
284
    // Create JSON configuration for transformation pipeline using unified format
285
    const char* json_config = 
1✔
286
        "[\n"
287
        "{\n"
288
        "    \"transformations\": {\n"
289
        "        \"metadata\": {\n"
290
        "            \"name\": \"Mask Skeletonization Pipeline\",\n"
291
        "            \"description\": \"Test mask skeletonization on rectangular mask\",\n"
292
        "            \"version\": \"1.0\"\n"
293
        "        },\n"
294
        "        \"steps\": [\n"
295
        "            {\n"
296
        "                \"step_id\": \"1\",\n"
297
        "                \"transform_name\": \"Skeletonize Mask\",\n"
298
        "                \"phase\": \"analysis\",\n"
299
        "                \"input_key\": \"test_mask\",\n"
300
        "                \"output_key\": \"skeletonized_mask\",\n"
301
        "                \"parameters\": {}\n"
302
        "            }\n"
303
        "        ]\n"
304
        "    }\n"
305
        "}\n"
306
        "]";
307
    
308
    // Create temporary directory and write JSON config to file
309
    std::filesystem::path test_dir = std::filesystem::temp_directory_path() / "mask_skeletonize_pipeline_test";
1✔
310
    std::filesystem::create_directories(test_dir);
1✔
311
    
312
    std::filesystem::path json_filepath = test_dir / "pipeline_config.json";
1✔
313
    {
314
        std::ofstream json_file(json_filepath);
1✔
315
        REQUIRE(json_file.is_open());
1✔
316
        json_file << json_config;
1✔
317
        json_file.close();
1✔
318
    }
1✔
319
    
320
    // Execute the transformation pipeline using load_data_from_json_config
321
    auto data_info_list = load_data_from_json_config(&dm, json_filepath.string());
1✔
322
    
323
    // Verify the transformation was executed and results are available
324
    auto result_skeletonized = dm.getData<MaskData>("skeletonized_mask");
3✔
325
    REQUIRE(result_skeletonized != nullptr);
1✔
326
    
327
    // Verify the skeletonization results
328
    auto original_masks = test_mask->getAtTime(TimeFrameIndex(100));
1✔
329
    auto result_masks = result_skeletonized->getAtTime(TimeFrameIndex(100));
1✔
330
    
331
    REQUIRE(!original_masks.empty());
1✔
332
    REQUIRE(!result_masks.empty());
1✔
333
    
334
    // The skeletonized version should have fewer points than the original
335
    size_t original_points = 0;
1✔
336
    for (const auto& mask : original_masks) {
2✔
337
        original_points += mask.size();
1✔
338
    }
339
    
340
    size_t skeletonized_points = 0;
1✔
341
    for (const auto& mask : result_masks) {
2✔
342
        skeletonized_points += mask.size();
1✔
343
    }
344
    
345
    REQUIRE(skeletonized_points < original_points);
1✔
346
    
347
    // Test another pipeline with multiple time frames
348
    const char* json_config_multiframe = 
1✔
349
        "[\n"
350
        "{\n"
351
        "    \"transformations\": {\n"
352
        "        \"metadata\": {\n"
353
        "            \"name\": \"Multi-frame Mask Skeletonization\",\n"
354
        "            \"description\": \"Test mask skeletonization on multiple time frames\",\n"
355
        "            \"version\": \"1.0\"\n"
356
        "        },\n"
357
        "        \"steps\": [\n"
358
        "            {\n"
359
        "                \"step_id\": \"1\",\n"
360
        "                \"transform_name\": \"Skeletonize Mask\",\n"
361
        "                \"phase\": \"analysis\",\n"
362
        "                \"input_key\": \"test_mask\",\n"
363
        "                \"output_key\": \"skeletonized_mask_multiframe\",\n"
364
        "                \"parameters\": {}\n"
365
        "            }\n"
366
        "        ]\n"
367
        "    }\n"
368
        "}\n"
369
        "]";
370
    
371
    std::filesystem::path json_filepath_multiframe = test_dir / "pipeline_config_multiframe.json";
1✔
372
    {
373
        std::ofstream json_file(json_filepath_multiframe);
1✔
374
        REQUIRE(json_file.is_open());
1✔
375
        json_file << json_config_multiframe;
1✔
376
        json_file.close();
1✔
377
    }
1✔
378
    
379
    // Execute the multi-frame pipeline
380
    auto data_info_list_multiframe = load_data_from_json_config(&dm, json_filepath_multiframe.string());
1✔
381
    
382
    // Verify the multi-frame results
383
    auto result_skeletonized_multiframe = dm.getData<MaskData>("skeletonized_mask_multiframe");
3✔
384
    REQUIRE(result_skeletonized_multiframe != nullptr);
1✔
385
    
386
    auto result_masks_multiframe = result_skeletonized_multiframe->getAtTime(TimeFrameIndex(100));
1✔
387
    REQUIRE(!result_masks_multiframe.empty());
1✔
388
    
389
    // Cleanup
390
    try {
391
        std::filesystem::remove_all(test_dir);
1✔
NEW
392
    } catch (const std::exception& e) {
×
NEW
393
        std::cerr << "Warning: Cleanup failed: " << e.what() << std::endl;
×
NEW
394
    }
×
395
}
2✔
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