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

paulmthompson / WhiskerToolbox / 15856747945

24 Jun 2025 04:52PM UTC coverage: 68.538% (+0.5%) from 68.069%
15856747945

push

github

paulmthompson
feat: Enhance Mask_Widget copy/move with automatic image size resizing

- Update _moveMasksToTarget() to handle different image sizes between source and target
- Update _copyMasksToTarget() to handle different image sizes between source and target
- Use new resize_mask() function when source and target MaskData have different ImageSizes
- Maintain existing behavior when image sizes match (use built-in moveTo/copyTo methods)
- Add comprehensive logging for resize operations and error handling
- Validate image sizes before attempting resize operations
- Preserve mask data integrity during cross-resolution transfers

When copying or moving masks between MaskData objects with different ImageSizes,
the masks are automatically resized using OpenCV nearest neighbor interpolation
to maintain binary mask properties while adapting to the target resolution.

This enhancement enables seamless mask data transfer between datasets of
different resolutions, which is common when working with multiple video sources
or when masks need to be applied to media of different scales.

9339 of 13626 relevant lines covered (68.54%)

1139.2 hits per line

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

0.0
/src/WhiskerToolbox/DataManager/Masks/IO/Image/Mask_Data_Image.cpp
1
#include "Mask_Data_Image.hpp"
2

3
#include "Masks/Mask_Data.hpp"
4
#include "Masks/masks.hpp"
5
#include "Points/points.hpp"
6
#include "utils/string_manip.hpp"
7

8
#include <opencv2/imgcodecs.hpp>
9
#include <opencv2/opencv.hpp>
10

11
#include <algorithm>
12
#include <filesystem>
13
#include <iostream>
14
#include <regex>
15
#include <vector>
16

17
std::shared_ptr<MaskData> load(ImageMaskLoaderOptions const & opts) {
×
18
    auto mask_data = std::make_shared<MaskData>();
×
19

20
    // Validate directory
21
    if (!std::filesystem::exists(opts.directory_path) || !std::filesystem::is_directory(opts.directory_path)) {
×
22
        std::cerr << "Error: Directory does not exist: " << opts.directory_path << std::endl;
×
23
        return mask_data;
×
24
    }
25

26
    // Get list of image files matching the pattern
27
    std::vector<std::filesystem::path> image_files;
×
28
    std::string pattern = opts.file_pattern;
×
29

30
    // Convert wildcard pattern to regex
31
    std::string regex_pattern = std::regex_replace(pattern, std::regex("\\*"), ".*");
×
32
    std::regex file_regex(regex_pattern, std::regex_constants::icase);
×
33

34
    for (auto const & entry: std::filesystem::directory_iterator(opts.directory_path)) {
×
35
        if (entry.is_regular_file()) {
×
36
            std::string filename = entry.path().filename().string();
×
37
            if (std::regex_match(filename, file_regex)) {
×
38
                image_files.push_back(entry.path());
×
39
            }
40
        }
×
41
    }
×
42

43
    if (image_files.empty()) {
×
44
        std::cerr << "Warning: No image files found matching pattern '" << opts.file_pattern
×
45
                  << "' in directory: " << opts.directory_path << std::endl;
×
46
        return mask_data;
×
47
    }
48

49
    // Sort files to ensure consistent ordering
50
    std::sort(image_files.begin(), image_files.end());
×
51

52
    int files_loaded = 0;
×
53
    int files_skipped = 0;
×
54

55
    std::cout << "Loading mask images from directory: " << opts.directory_path << std::endl;
×
56
    std::cout << "Found " << image_files.size() << " image files matching pattern: " << opts.file_pattern << std::endl;
×
57

58
    for (auto const & file_path: image_files) {
×
59
        std::string filename = file_path.filename().string();
×
60
        std::string stem = file_path.stem().string();// Remove extension
×
61

62
        // Remove prefix if specified
63
        if (!opts.filename_prefix.empty()) {
×
64
            if (stem.find(opts.filename_prefix) != 0) {
×
65
                std::cerr << "Warning: File '" << filename
66
                          << "' does not start with expected prefix '" << opts.filename_prefix << "'" << std::endl;
×
67
                files_skipped++;
×
68
                continue;
×
69
            }
70
            stem = stem.substr(opts.filename_prefix.length());// Remove prefix
×
71
        }
72

73
        // Parse frame number
74
        int frame_number;
75
        try {
76
            frame_number = std::stoi(stem);
×
77
        } catch (std::exception const & e) {
×
78
            std::cerr << "Warning: Could not parse frame number from filename: " << filename << std::endl;
×
79
            files_skipped++;
×
80
            continue;
×
81
        }
×
82

83
        // Load the image using OpenCV
84
        cv::Mat image = cv::imread(file_path.string(), cv::IMREAD_GRAYSCALE);
×
85
        if (image.empty()) {
×
86
            std::cerr << "Warning: Could not load image: " << file_path.string() << std::endl;
×
87
            files_skipped++;
×
88
            continue;
×
89
        }
90

91
        // Extract mask points from image
92
        std::vector<Point2D<uint32_t>> mask_points;
×
93
        int const width = image.cols;
×
94
        int const height = image.rows;
×
95

96
        for (int y = 0; y < height; ++y) {
×
97
            for (int x = 0; x < width; ++x) {
×
98
                // Get pixel intensity (0-255)
99
                uint8_t pixel_value = image.at<uint8_t>(y, x);
×
100

101
                // Apply threshold and inversion logic
102
                bool is_mask_pixel;
103
                if (opts.invert_mask) {
×
104
                    is_mask_pixel = pixel_value < opts.threshold_value;
×
105
                } else {
106
                    is_mask_pixel = pixel_value >= opts.threshold_value;
×
107
                }
108

109
                if (is_mask_pixel) {
×
110
                    //mask_points.emplace_back(static_cast<float>(x), static_cast<float>(y)); // This fails on mac
111
                    mask_points.push_back(Point2D<uint32_t>{static_cast<uint32_t>(x), static_cast<uint32_t>(y)});
×
112
                }
113
            }
114
        }
115

116
        // Add mask to data if we have points
117
        if (!mask_points.empty()) {
×
118
            mask_data->addAtTime(static_cast<size_t>(frame_number), std::move(mask_points), false);
×
119
            files_loaded++;
×
120
        } else {
121
            std::cout << "Warning: No mask pixels found in image: " << filename << std::endl;
×
122
            files_skipped++;
×
123
        }
124
    }
×
125

126
    // Notify observers once at the end
127
    if (files_loaded > 0) {
×
128
        mask_data->notifyObservers();
×
129
    }
130

131
    std::cout << "Image mask loading complete: " << files_loaded << " files loaded";
×
132
    if (files_skipped > 0) {
×
133
        std::cout << ", " << files_skipped << " files skipped";
×
134
    }
135
    std::cout << std::endl;
×
136

137
    return mask_data;
×
138
}
×
139

140
void save(MaskData const * mask_data, ImageMaskSaverOptions const & opts) {
×
141
    if (!mask_data) {
×
142
        std::cerr << "Error: MaskData pointer is null" << std::endl;
×
143
        return;
×
144
    }
145

146
    // Create output directory if it doesn't exist
147
    if (!std::filesystem::exists(opts.parent_dir)) {
×
148
        std::filesystem::create_directories(opts.parent_dir);
×
149
        std::cout << "Created directory: " << opts.parent_dir << std::endl;
×
150
    }
151

152
    int files_saved = 0;
×
153
    int files_skipped = 0;
×
154

155
    auto mask_data_size = mask_data->getImageSize();
×
156

157
    std::cout << "Saving mask images to directory: " << opts.parent_dir << std::endl;
×
158

159
    // Iterate through all masks with their timestamps
160
    for (auto const & time_mask_pair: mask_data->getAllAsRange()) {
×
161
        int const frame_number = time_mask_pair.time;
×
162
        std::vector<Mask2D> const & masks = time_mask_pair.masks;
×
163

164
        if (masks.empty()) {
×
165
            files_skipped++;
×
166
            continue;
×
167
        }
168

169
        // Create output image with desired dimensions
170
        cv::Mat output_img = cv::Mat::zeros(opts.image_height, opts.image_width, CV_8UC1);
×
171
        output_img.setTo(opts.background_value);
×
172

173
        // Resize each mask and draw it on the output image
174
        ImageSize dest_size{opts.image_width, opts.image_height};
×
175
        for (Mask2D const & mask: masks) {
×
176
            // Resize the mask using our new resize function
177
            Mask2D resized_mask = resize_mask(mask, mask_data_size, dest_size);
×
178

179
            // Draw the resized mask on the output image
180
            for (Point2D<uint32_t> const & point: resized_mask) {
×
181
                int x = static_cast<int>(point.x);
×
182
                int y = static_cast<int>(point.y);
×
183

184
                // Check bounds (should already be valid after resize, but be safe)
185
                if (x >= 0 && x < opts.image_width && y >= 0 && y < opts.image_height) {
×
186
                    output_img.at<uint8_t>(y, x) = static_cast<uint8_t>(opts.mask_value);
×
187
                }
188
            }
189
        }
×
190

191
        // Generate filename using the pad_frame_id utility
192
        std::string filename;
×
193
        if (!opts.filename_prefix.empty()) {
×
194
            filename = opts.filename_prefix;
×
195
        }
196

197
        // Add zero-padded frame number
198
        filename += pad_frame_id(frame_number, opts.frame_number_padding);
×
199

200
        // Add extension based on format
201
        std::string extension = "." + opts.image_format;
×
202
        std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
×
203
        filename += extension;
×
204

205
        // Full path
206
        std::filesystem::path full_path = std::filesystem::path(opts.parent_dir) / filename;
×
207

208
        // Check if file exists and handle according to overwrite setting
209
        if (std::filesystem::exists(full_path) && !opts.overwrite_existing) {
×
210
            std::cout << "Skipping existing file: " << full_path.string() << std::endl;
×
211
            files_skipped++;
×
212
            continue;
×
213
        }
214

215
        // Save the image using OpenCV
216
        if (cv::imwrite(full_path.string(), output_img)) {
×
217
            files_saved++;
×
218
            if (std::filesystem::exists(full_path) && opts.overwrite_existing) {
×
219
                std::cout << "Overwritten existing file: " << full_path.string() << std::endl;
×
220
            }
221
        } else {
222
            std::cerr << "Warning: Failed to save image: " << full_path.string() << std::endl;
×
223
            files_skipped++;
×
224
        }
225
    }
×
226

227
    std::cout << "Image mask saving complete: " << files_saved << " files saved";
×
228
    if (files_skipped > 0) {
×
229
        std::cout << ", " << files_skipped << " files skipped";
×
230
    }
231
    std::cout << std::endl;
×
232
}
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