CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
image_processor.cpp
Go to the documentation of this file.
1
7#include "image_processor.h"
8#include "logging.h"
9#include <iostream>
10#include <algorithm>
11#include <cmath>
12#include <opencv2/imgcodecs.hpp>
13#include <sstream>
14#include <iomanip>
15
16namespace cppxdic {
17
18// Saturate images - equivalent to satur.m
19std::vector<cv::Mat> ImageProcessor::saturate(const std::vector<cv::Mat>& input, int level) {
20 std::vector<cv::Mat> output;
21 output.reserve(input.size());
22
23 for (const auto& img : input) {
24 output.push_back(saturate(img, level));
25 }
26
27 return output;
28}
29
30cv::Mat ImageProcessor::saturate(const cv::Mat& input, int level, const std::string& method) {
31 // Convert to grayscale if needed
32 cv::Mat gray;
33 if (input.channels() == 3) {
34 LOG_WARN << "Converting to grayscale";
35 cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY);
36 } else {
37 gray = input.clone();
38 }
39
40 // Ensure uint8 format
41 if (gray.type() != CV_8UC1) {
42 LOG_WARN << "Converting to uint8";
43 gray.convertTo(gray, CV_8UC1);
44 }
45
46 cv::Mat output = gray.clone();
47
48 if (method == "high") {
49 cv::threshold(output, output, level, level, cv::THRESH_TRUNC);
50 } else if (method == "low") {
51 output.setTo(level, output < level);
52 }
53
54 return output;
55}
56
57// Filter images like Ben's method - equivalent to filter_like_ben.m
58std::pair<std::vector<cv::Mat>, std::pair<double, double>>
59ImageProcessor::filterLikeBen(const std::vector<cv::Mat>& input,
60 const cv::Mat& mask,
61 const std::vector<int>& param_filt,
62 const std::pair<double, double>* gs_boundaries) {
63
64 std::vector<cv::Mat> filtered_images;
65 std::pair<double, double> boundaries;
66
67 // Apply bandpass filter to all images first
68 for (const auto& img : input) {
69 cv::Mat filtered = applyBandpassFilter(img, param_filt);
70 filtered_images.push_back(filtered);
71 }
72
73 // Compute boundaries from FIRST FILTERED image using percentiles (5th, 95th) if not provided
74 if (gs_boundaries == nullptr) {
75 boundaries = computePercentileBoundaries(filtered_images[0], mask, 5.0, 95.0);
76 LOG_DEBUG << " Computed filter boundaries: [" << boundaries.first << ", " << boundaries.second << "]";
77 } else {
78 boundaries = *gs_boundaries;
79 }
80
81 // Normalize each filtered image using the boundaries
82 for (size_t i = 0; i < filtered_images.size(); ++i) {
83 filtered_images[i] = normalizeAndClamp(filtered_images[i], boundaries);
84 }
85
86 return {filtered_images, boundaries};
87}
88
89cv::Mat ImageProcessor::applyBandpassFilter(const cv::Mat& input,
90 const std::vector<int>& param_filt) {
91 // Convert to float for processing (like MATLAB's double)
92 cv::Mat float_img;
93 input.convertTo(float_img, CV_64F);
94
95 // Apply Gaussian bandpass filter using FFT
96 // param_filt = [r1, r2] where r1 is high-pass threshold, r2 is low-pass threshold
97 // MATLAB: filter = exp(-r^2/(2*r2^2)) * (1 - exp(-r^2/(2*r1^2)))
98 double r1 = static_cast<double>(param_filt[0]);
99 double r2 = static_cast<double>(param_filt[1]);
100
101 int rows = float_img.rows;
102 int cols = float_img.cols;
103
104 // Create Gaussian bandpass filter (matching MATLAB bandpassfft.m)
105 cv::Mat filter_mask(rows, cols, CV_64F);
106 int cx = cols / 2;
107 int cy = rows / 2;
108
109 for (int y = 0; y < rows; ++y) {
110 for (int x = 0; x < cols; ++x) {
111 double dx = x - cx;
112 double dy = y - cy;
113 double r_sq = dx * dx + dy * dy;
114
115 // Gaussian bandpass: exp(-r^2/(2*r2^2)) * (1 - exp(-r^2/(2*r1^2)))
116 double low_pass = std::exp(-r_sq / (2.0 * r2 * r2));
117 double high_pass = (r1 != 0) ? (1.0 - std::exp(-r_sq / (2.0 * r1 * r1))) : 1.0;
118 filter_mask.at<double>(y, x) = low_pass * high_pass;
119 }
120 }
121
122 // Perform FFT (matching MATLAB: out = real(ifft2(fft2(in).*fftshift(filter))))
123 cv::Mat planes[] = {float_img, cv::Mat::zeros(float_img.size(), CV_64F)};
124 cv::Mat complex_img;
125 cv::merge(planes, 2, complex_img);
126 cv::dft(complex_img, complex_img);
127
128 // fftshift the filter to match MATLAB's fftshift(filter)
129 cv::Mat filter_shifted;
130 int cx2 = filter_mask.cols / 2;
131 int cy2 = filter_mask.rows / 2;
132 cv::Mat q0(filter_mask, cv::Rect(0, 0, cx2, cy2));
133 cv::Mat q1(filter_mask, cv::Rect(cx2, 0, cols - cx2, cy2));
134 cv::Mat q2(filter_mask, cv::Rect(0, cy2, cx2, rows - cy2));
135 cv::Mat q3(filter_mask, cv::Rect(cx2, cy2, cols - cx2, rows - cy2));
136
137 cv::Mat tmp;
138 filter_shifted = cv::Mat(rows, cols, CV_64F);
139 q3.copyTo(filter_shifted(cv::Rect(0, 0, cols - cx2, rows - cy2)));
140 q0.copyTo(filter_shifted(cv::Rect(cols - cx2, rows - cy2, cx2, cy2)));
141 q1.copyTo(filter_shifted(cv::Rect(0, rows - cy2, cols - cx2, cy2)));
142 q2.copyTo(filter_shifted(cv::Rect(cols - cx2, 0, cx2, rows - cy2)));
143
144 // Apply filter in frequency domain
145 cv::split(complex_img, planes);
146 planes[0] = planes[0].mul(filter_shifted);
147 planes[1] = planes[1].mul(filter_shifted);
148 cv::merge(planes, 2, complex_img);
149
150 // Inverse DFT
151 cv::Mat filtered_img;
152 cv::idft(complex_img, filtered_img, cv::DFT_SCALE);
153 cv::split(filtered_img, planes);
154
155 return planes[0]; // Return real part
156}
157
158std::pair<double, double> ImageProcessor::computeGrayscaleBoundaries(const cv::Mat& image,
159 const cv::Mat& mask) {
160 // Compute min/max grayscale values within masked region
161 cv::Mat float_img;
162 image.convertTo(float_img, CV_32F);
163
164 double min_val = std::numeric_limits<double>::max();
165 double max_val = std::numeric_limits<double>::lowest();
166
167 for (int y = 0; y < image.rows; ++y) {
168 for (int x = 0; x < image.cols; ++x) {
169 if (mask.at<uint8_t>(y, x) > 0) {
170 double val = float_img.at<float>(y, x);
171 min_val = std::min(min_val, val);
172 max_val = std::max(max_val, val);
173 }
174 }
175 }
176
177 return {min_val, max_val};
178}
179
180std::pair<double, double> ImageProcessor::computePercentileBoundaries(const cv::Mat& image,
181 const cv::Mat& mask,
182 double lower_percentile,
183 double upper_percentile) {
184 // Collect all pixel values within masked region
185 std::vector<double> values;
186
187 // Convert to double if needed
188 cv::Mat double_img;
189 if (image.type() == CV_64F) {
190 double_img = image;
191 } else {
192 image.convertTo(double_img, CV_64F);
193 }
194
195 for (int y = 0; y < double_img.rows; ++y) {
196 for (int x = 0; x < double_img.cols; ++x) {
197 if (mask.at<uint8_t>(y, x) > 0) {
198 values.push_back(double_img.at<double>(y, x));
199 }
200 }
201 }
202
203 if (values.empty()) {
204 LOG_WARN << "No pixels in mask for percentile computation";
205 return {0.0, 255.0};
206 }
207
208 // Sort values
209 std::sort(values.begin(), values.end());
210
211 // Compute percentile indices
212 size_t lower_idx = static_cast<size_t>(values.size() * lower_percentile / 100.0);
213 size_t upper_idx = static_cast<size_t>(values.size() * upper_percentile / 100.0);
214
215 lower_idx = std::min(lower_idx, values.size() - 1);
216 upper_idx = std::min(upper_idx, values.size() - 1);
217
218 return {values[lower_idx], values[upper_idx]};
219}
220
221cv::Mat ImageProcessor::normalizeAndClamp(const cv::Mat& image,
222 const std::pair<double, double>& boundaries) {
223 // Normalize: ((image - min) / (max - min))
224 // Then saturate to [0, 1] and scale to [0, 255]
225 // MATLAB: imbdp_norm = ((imbdp-y(1))/(y(2)-y(1)));
226 // imbdp_norm = satur(satur(imbdp_norm,'method','low','level',0),'method','high','level',1);
227
228 cv::Mat double_img;
229 if (image.type() == CV_64F) {
230 double_img = image;
231 } else {
232 image.convertTo(double_img, CV_64F);
233 }
234
235 cv::Mat normalized;
236 double range = boundaries.second - boundaries.first;
237 if (range > 0) {
238 normalized = (double_img - boundaries.first) / range;
239 // Saturate (clamp) to [0, 1] - matching MATLAB's satur calls
240 cv::threshold(normalized, normalized, 1.0, 1.0, cv::THRESH_TRUNC);
241 cv::threshold(normalized, normalized, 0.0, 0.0, cv::THRESH_TOZERO);
242 } else {
243 normalized = cv::Mat::zeros(image.size(), CV_64F);
244 }
245
246 // Convert to uint8 [0, 255]
247 cv::Mat output;
248 normalized.convertTo(output, CV_8UC1, 255.0);
249
250 return output;
251}
252
253std::vector<cv::Mat> ImageProcessor::loadImages(const std::vector<std::string>& paths,
254 bool grayscale) {
255 std::vector<cv::Mat> images;
256 images.reserve(paths.size());
257
258 for (const auto& path : paths) {
259 cv::Mat img = cv::imread(path, grayscale ? cv::IMREAD_GRAYSCALE : cv::IMREAD_COLOR);
260 if (img.empty()) {
261 LOG_ERROR << "Failed to load image: " << path;
262 continue;
263 }
264 images.push_back(img);
265 }
266
267 return images;
268}
269
270bool ImageProcessor::saveImages(const std::vector<cv::Mat>& images,
271 const std::string& base_path,
272 const std::string& prefix) {
273 for (size_t i = 0; i < images.size(); ++i) {
274 std::ostringstream filename;
275 filename << base_path << "/" << prefix << "_" << std::setw(6) << std::setfill('0') << i << ".png";
276
277 if (!cv::imwrite(filename.str(), images[i])) {
278 LOG_ERROR << "Failed to save image: " << filename.str();
279 return false;
280 }
281 }
282
283 return true;
284}
285
286cv::Mat ImageProcessor::applyHistogramEqualization(const cv::Mat& image, double clip_limit) {
287 cv::Mat output;
288
289 // Apply CLAHE (Contrast Limited Adaptive Histogram Equalization)
290 cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(clip_limit, cv::Size(8, 8));
291 clahe->apply(image, output);
292
293 return output;
294}
295
296cv::Mat ImageProcessor::normalizeImage(const cv::Mat& image, double min_val, double max_val) {
297 cv::Mat output;
298
299 if (max_val > min_val) {
300 // Normalize to [0, 255] using boundaries
301 image.convertTo(output, CV_32F, 255.0 / (max_val - min_val), -min_val * 255.0 / (max_val - min_val));
302
303 // Clip to [0, 255] range
304 cv::threshold(output, output, 255.0, 255.0, cv::THRESH_TRUNC);
305 cv::threshold(output, output, 0.0, 0.0, cv::THRESH_TOZERO);
306 } else {
307 output = image.clone();
308 }
309
310 return output;
311}
312
313
314cv::Mat ImageProcessor::blurOutsideMask(const cv::Mat& img, const cv::Mat& mask, int kwidth, int kheight) {
315 cv::Mat blurred, result;
316
317 // Apply Gaussian blur to the entire image
318 cv::GaussianBlur(img, blurred, cv::Size(kwidth, kheight), 15);
319
320 // Create result image
321 img.copyTo(result);
322
323 // Define the region to blur: where mask is 0 (or not present)
324 cv::Mat blurRegion = (mask == 0); // Inverted mask
325
326 // Copy blurred pixels to the non-masked area
327 blurred.copyTo(result, blurRegion);
328
329 return result;
330}
331
332} // namespace cppxdic
static bool saveImages(const std::vector< cv::Mat > &images, const std::string &base_path, const std::string &prefix)
Save images to disk.
static std::pair< double, double > computePercentileBoundaries(const cv::Mat &image, const cv::Mat &mask, double lower_percentile, double upper_percentile)
Compute grayscale boundaries using percentiles.
static std::vector< cv::Mat > saturate(const std::vector< cv::Mat > &input, int level)
Saturate image intensities Equivalent to satur.m in Matlab.
static std::pair< std::vector< cv::Mat >, std::pair< double, double > > filterLikeBen(const std::vector< cv::Mat > &input, const cv::Mat &mask, const std::vector< int > &param_filt, const std::pair< double, double > *gs_boundaries=nullptr)
Filter images like Ben's method Equivalent to filter_like_ben.m in Matlab.
static std::pair< double, double > computeGrayscaleBoundaries(const cv::Mat &image, const cv::Mat &mask)
Compute grayscale boundaries from masked region using min/max.
static cv::Mat applyBandpassFilter(const cv::Mat &input, const std::vector< int > &param_filt)
Apply bandpass filter to image using FFT.
static cv::Mat normalizeAndClamp(const cv::Mat &image, const std::pair< double, double > &boundaries)
Normalize and clamp image to [0, 255] using boundaries.
static cv::Mat blurOutsideMask(const cv::Mat &img, const cv::Mat &mask, int kwidth=25, int kheight=25)
Blur the outside of a mask.
static std::vector< cv::Mat > loadImages(const std::vector< std::string > &paths, bool grayscale=true)
Load images from file paths.
Lightweight, dependency-free logging facility for CPPxDIC.
#define LOG_DEBUG
Definition logging.h:169
#define LOG_ERROR
Definition logging.h:172
#define LOG_WARN
Definition logging.h:171
DataSerializer - Abstract base class for format-agnostic DIC data I/O.