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

paulmthompson / WhiskerToolbox / 15716269894

17 Jun 2025 07:20PM UTC coverage: 67.505% (-0.02%) from 67.52%
15716269894

push

github

paulmthompson
increment to v0.3.1

8860 of 13125 relevant lines covered (67.5%)

1182.13 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 "Points/points.hpp"
5
#include "utils/string_manip.hpp"
6

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

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

16
std::shared_ptr<MaskData> load(ImageMaskLoaderOptions const & opts) {
×
17
    auto mask_data = std::make_shared<MaskData>();
×
18
    
19
    // Validate directory
20
    if (!std::filesystem::exists(opts.directory_path) || !std::filesystem::is_directory(opts.directory_path)) {
×
21
        std::cerr << "Error: Directory does not exist: " << opts.directory_path << std::endl;
×
22
        return mask_data;
×
23
    }
24
    
25
    // Get list of image files matching the pattern
26
    std::vector<std::filesystem::path> image_files;
×
27
    std::string pattern = opts.file_pattern;
×
28
    
29
    // Convert wildcard pattern to regex
30
    std::string regex_pattern = std::regex_replace(pattern, std::regex("\\*"), ".*");
×
31
    std::regex file_regex(regex_pattern, std::regex_constants::icase);
×
32
    
33
    for (auto const & entry : std::filesystem::directory_iterator(opts.directory_path)) {
×
34
        if (entry.is_regular_file()) {
×
35
            std::string filename = entry.path().filename().string();
×
36
            if (std::regex_match(filename, file_regex)) {
×
37
                image_files.push_back(entry.path());
×
38
            }
39
        }
×
40
    }
×
41
    
42
    if (image_files.empty()) {
×
43
        std::cerr << "Warning: No image files found matching pattern '" << opts.file_pattern 
×
44
                  << "' in directory: " << opts.directory_path << std::endl;
×
45
        return mask_data;
×
46
    }
47
    
48
    // Sort files to ensure consistent ordering
49
    std::sort(image_files.begin(), image_files.end());
×
50
    
51
    int files_loaded = 0;
×
52
    int files_skipped = 0;
×
53
    
54
    std::cout << "Loading mask images from directory: " << opts.directory_path << std::endl;
×
55
    std::cout << "Found " << image_files.size() << " image files matching pattern: " << opts.file_pattern << std::endl;
×
56
    
57
    for (auto const & file_path : image_files) {
×
58
        std::string filename = file_path.filename().string();
×
59
        std::string stem = file_path.stem().string(); // Remove extension
×
60
        
61
        // Remove prefix if specified
62
        if (!opts.filename_prefix.empty()) {
×
63
            if (stem.find(opts.filename_prefix) != 0) {
×
64
                std::cerr << "Warning: File '" << filename 
65
                          << "' does not start with expected prefix '" << opts.filename_prefix << "'" << std::endl;
×
66
                files_skipped++;
×
67
                continue;
×
68
            }
69
            stem = stem.substr(opts.filename_prefix.length()); // Remove prefix
×
70
        }
71
        
72
        // Parse frame number
73
        int frame_number;
74
        try {
75
            frame_number = std::stoi(stem);
×
76
        } catch (std::exception const & e) {
×
77
            std::cerr << "Warning: Could not parse frame number from filename: " << filename << std::endl;
×
78
            files_skipped++;
×
79
            continue;
×
80
        }
×
81
        
82
        // Load the image using OpenCV
83
        cv::Mat image = cv::imread(file_path.string(), cv::IMREAD_GRAYSCALE);
×
84
        if (image.empty()) {
×
85
            std::cerr << "Warning: Could not load image: " << file_path.string() << std::endl;
×
86
            files_skipped++;
×
87
            continue;
×
88
        }
89
        
90
        // Extract mask points from image
91
        std::vector<Point2D<float>> mask_points;
×
92
        int const width = image.cols;
×
93
        int const height = image.rows;
×
94
        
95
        for (int y = 0; y < height; ++y) {
×
96
            for (int x = 0; x < width; ++x) {
×
97
                // Get pixel intensity (0-255)
98
                uint8_t pixel_value = image.at<uint8_t>(y, x);
×
99
                
100
                // Apply threshold and inversion logic
101
                bool is_mask_pixel;
102
                if (opts.invert_mask) {
×
103
                    is_mask_pixel = pixel_value < opts.threshold_value;
×
104
                } else {
105
                    is_mask_pixel = pixel_value >= opts.threshold_value;
×
106
                }
107
                
108
                if (is_mask_pixel) {
×
109
                    //mask_points.emplace_back(static_cast<float>(x), static_cast<float>(y)); // This fails on mac
110
                    mask_points.push_back(Point2D<float>{static_cast<float>(x), static_cast<float>(y)});
×
111
                }
112
            }
113
        }
114
        
115
        // Add mask to data if we have points
116
        if (!mask_points.empty()) {
×
117
            mask_data->addAtTime(static_cast<size_t>(frame_number), std::move(mask_points), false);
×
118
            files_loaded++;
×
119
        } else {
120
            std::cout << "Warning: No mask pixels found in image: " << filename << std::endl;
×
121
            files_skipped++;
×
122
        }
123
    }
×
124
    
125
    // Notify observers once at the end
126
    if (files_loaded > 0) {
×
127
        mask_data->notifyObservers();
×
128
    }
129
    
130
    std::cout << "Image mask loading complete: " << files_loaded << " files loaded";
×
131
    if (files_skipped > 0) {
×
132
        std::cout << ", " << files_skipped << " files skipped";
×
133
    }
134
    std::cout << std::endl;
×
135
    
136
    return mask_data;
×
137
}
×
138

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

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

154
    auto mask_data_size = mask_data->getImageSize();
×
155
    
156
    std::cout << "Saving mask images to directory: " << opts.parent_dir << std::endl;
×
157
    
158
    // Iterate through all masks with their timestamps
159
    for (auto const & time_mask_pair : mask_data->getAllAsRange()) {
×
160
        int const frame_number = time_mask_pair.time;
×
161
        std::vector<Mask2D> const & masks = time_mask_pair.masks;
×
162
        
163
        if (masks.empty()) {
×
164
            files_skipped++;
×
165
            continue;
×
166
        }
167
        
168
        // Create a blank image using OpenCV
169
        cv::Mat image = cv::Mat::zeros(mask_data_size.height, mask_data_size.width, CV_8UC1);
×
170
        image.setTo(opts.background_value);
×
171
        
172
        // Draw all masks for this frame
173
        for (Mask2D const & mask : masks) {
×
174
            for (Point2D<float> const & point : mask) {
×
175
                int x = static_cast<int>(std::round(point.x));
×
176
                int y = static_cast<int>(std::round(point.y));
×
177
                
178
                // Check bounds
179
                if (x >= 0 && x < opts.image_width && y >= 0 && y < opts.image_height) {
×
180
                    image.at<uint8_t>(y, x) = static_cast<uint8_t>(opts.mask_value);
×
181
                    //image.at<uint8_t>(x, y) = static_cast<uint8_t>(opts.mask_value);
182
                }
183
            }
184
        }
185

186
        cv::Mat output_img;
×
187
        cv::resize(image, output_img, cv::Size(opts.image_width, opts.image_height), cv::INTER_NEAREST);
×
188
        
189
        // Generate filename using the pad_frame_id utility
190
        std::string filename;
×
191
        if (!opts.filename_prefix.empty()) {
×
192
            filename = opts.filename_prefix;
×
193
        }
194
        
195
        // Add zero-padded frame number
196
        filename += pad_frame_id(frame_number, opts.frame_number_padding);
×
197
        
198
        // Add extension based on format
199
        std::string extension = "." + opts.image_format;
×
200
        std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
×
201
        filename += extension;
×
202
        
203
        // Full path
204
        std::filesystem::path full_path = std::filesystem::path(opts.parent_dir) / filename;
×
205
        
206
        // Check if file exists and handle according to overwrite setting
207
        if (std::filesystem::exists(full_path) && !opts.overwrite_existing) {
×
208
            std::cout << "Skipping existing file: " << full_path.string() << std::endl;
×
209
            files_skipped++;
×
210
            continue;
×
211
        }
212
        
213
        // Save the image using OpenCV
214
        if (cv::imwrite(full_path.string(), output_img)) {
×
215
            files_saved++;
×
216
            if (std::filesystem::exists(full_path) && opts.overwrite_existing) {
×
217
                std::cout << "Overwritten existing file: " << full_path.string() << std::endl;
×
218
            }
219
        } else {
220
            std::cerr << "Warning: Failed to save image: " << full_path.string() << std::endl;
×
221
            files_skipped++;
×
222
        }
223
    }
×
224
    
225
    std::cout << "Image mask saving complete: " << files_saved << " files saved";
×
226
    if (files_skipped > 0) {
×
227
        std::cout << ", " << files_skipped << " files skipped";
×
228
    }
229
    std::cout << std::endl;
×
230
} 
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