CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
matlab_functions.cpp
Go to the documentation of this file.
1
6#include "matlab_functions.h"
7#include "logging.h"
8#include <opencv2/imgproc.hpp>
9#if CV_VERSION_MAJOR >= 5
10#include <opencv2/geometry.hpp> // cv::moments / cv::contourArea moved out of imgproc in OpenCV 5
11#endif
12#include <iostream>
13#include <cmath>
14#include <algorithm>
15
16namespace cppxdic {
17
18// ---------------------------------------------------------------------------
19// matlab_shrink_mask
20// ---------------------------------------------------------------------------
21cv::Mat matlab_shrink_mask(const cv::Mat& mask, double factor) {
22 if (mask.empty() || factor >= 1.0) {
23 return mask.clone();
24 }
25
26 // Find contours (equivalent to MATLAB bwboundaries on imgradient)
27 cv::Mat binary;
28 if (mask.type() != CV_8U) {
29 mask.convertTo(binary, CV_8U);
30 } else {
31 binary = mask.clone();
32 }
33 cv::threshold(binary, binary, 0, 255, cv::THRESH_BINARY);
34
35 std::vector<std::vector<cv::Point>> contours;
36 cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
37
38 if (contours.empty()) {
39 return cv::Mat::zeros(mask.size(), CV_8U);
40 }
41
42 // Find the largest contour (main boundary)
43 size_t largest_idx = 0;
44 double largest_area = 0;
45 for (size_t i = 0; i < contours.size(); ++i) {
46 double area = cv::contourArea(contours[i]);
47 if (area > largest_area) {
48 largest_area = area;
49 largest_idx = i;
50 }
51 }
52
53 // Compute centroid using moments (equivalent to MATLAB centroid(polyshape))
54 cv::Moments m = cv::moments(contours[largest_idx]);
55 if (m.m00 < 1e-10) {
56 return cv::Mat::zeros(mask.size(), CV_8U);
57 }
58 double cx = m.m10 / m.m00;
59 double cy = m.m01 / m.m00;
60
61 // Scale contour around centroid (equivalent to MATLAB scale(polyshape, factor, [cx,cy]))
62 std::vector<cv::Point> scaled_contour;
63 scaled_contour.reserve(contours[largest_idx].size());
64 for (const auto& pt : contours[largest_idx]) {
65 double sx = cx + factor * (pt.x - cx);
66 double sy = cy + factor * (pt.y - cy);
67 scaled_contour.emplace_back(
68 static_cast<int>(std::round(sx)),
69 static_cast<int>(std::round(sy))
70 );
71 }
72
73 // Fill the scaled polygon (equivalent to MATLAB poly2mask)
74 cv::Mat result = cv::Mat::zeros(mask.size(), CV_8U);
75 std::vector<std::vector<cv::Point>> scaled_contours = {scaled_contour};
76 cv::fillPoly(result, scaled_contours, cv::Scalar(255));
77
78 return result;
79}
80
81// ---------------------------------------------------------------------------
82// matlab_imgaussfilt3
83// ---------------------------------------------------------------------------
84std::vector<cv::Mat> matlab_imgaussfilt3(const std::vector<cv::Mat>& volume,
85 double sigma_spatial,
86 double sigma_temporal) {
87 if (volume.empty()) {
88 return {};
89 }
90
91 const int N = static_cast<int>(volume.size());
92 const int H = volume[0].rows;
93 const int W = volume[0].cols;
94
95 // Step 1: Spatial Gaussian filter on each frame
96 // Kernel size from sigma: MATLAB uses ceil(sigma * 2) * 2 + 1
97 int ksize_spatial = static_cast<int>(std::ceil(sigma_spatial * 2.0)) * 2 + 1;
98 if (ksize_spatial < 3) ksize_spatial = 3;
99
100 std::vector<cv::Mat> spatially_filtered(N);
101 for (int f = 0; f < N; ++f) {
102 cv::Mat frame;
103 if (volume[f].type() != CV_64F) {
104 volume[f].convertTo(frame, CV_64F);
105 } else {
106 frame = volume[f];
107 }
108 // BORDER_REPLICATE matches MATLAB 'padding','replicate'
109 cv::GaussianBlur(frame, spatially_filtered[f],
110 cv::Size(ksize_spatial, ksize_spatial),
111 sigma_spatial, sigma_spatial,
112 cv::BORDER_REPLICATE);
113 }
114
115 if (N == 1 || sigma_temporal < 1e-10) {
116 return spatially_filtered;
117 }
118
119 // Step 2: Temporal Gaussian filter per pixel
120 // Build 1D Gaussian kernel for temporal dimension
121 int ksize_temporal = static_cast<int>(std::ceil(sigma_temporal * 2.0)) * 2 + 1;
122 if (ksize_temporal < 3) ksize_temporal = 3;
123 int half_k = ksize_temporal / 2;
124
125 std::vector<double> kernel(ksize_temporal);
126 double kernel_sum = 0;
127 for (int k = 0; k < ksize_temporal; ++k) {
128 double t = static_cast<double>(k - half_k);
129 kernel[k] = std::exp(-0.5 * t * t / (sigma_temporal * sigma_temporal));
130 kernel_sum += kernel[k];
131 }
132 for (auto& v : kernel) {
133 v /= kernel_sum;
134 }
135
136 // Apply temporal filter with replicate padding
137 std::vector<cv::Mat> result(N);
138 for (int f = 0; f < N; ++f) {
139 result[f] = cv::Mat::zeros(H, W, CV_64F);
140 }
141
142 for (int y = 0; y < H; ++y) {
143 for (int x = 0; x < W; ++x) {
144 for (int f = 0; f < N; ++f) {
145 double val = 0;
146 for (int k = 0; k < ksize_temporal; ++k) {
147 int src_f = f + k - half_k;
148 // Replicate padding: clamp to [0, N-1]
149 src_f = std::clamp(src_f, 0, N - 1);
150 val += spatially_filtered[src_f].at<double>(y, x) * kernel[k];
151 }
152 result[f].at<double>(y, x) = val;
153 }
154 }
155 }
156
157 return result;
158}
159
160// ---------------------------------------------------------------------------
161// matlab_replacebadcorr
162// ---------------------------------------------------------------------------
163int matlab_replacebadcorr(ncorr::DIC_analysis_output& data,
164 double level_corr_coef,
165 double sigma_spatial,
166 double sigma_temporal,
167 double shrink_factor) {
168 if (data.disps.empty()) {
169 return 0;
170 }
171
172 const int Nframe = static_cast<int>(data.disps.size());
173 const int H = data.disps[0].data_height();
174 const int W = data.disps[0].data_width();
175
176 LOG_DEBUG << " matlab_replacebadcorr: " << Nframe << " frames, "
177 << H << "x" << W << " grid, cc_threshold=" << level_corr_coef;
178
179 // Step 1: Stack displacement fields into 3D volumes (H x W x Nframe)
180 std::vector<cv::Mat> u_vol(Nframe), v_vol(Nframe), cc_vol(Nframe);
181 for (int f = 0; f < Nframe; ++f) {
182 u_vol[f] = cv::Mat(H, W, CV_64F, 0.0);
183 v_vol[f] = cv::Mat(H, W, CV_64F, 0.0);
184 cc_vol[f] = cv::Mat(H, W, CV_64F, 0.0);
185
186 const auto& u_arr = data.disps[f].get_u().get_array();
187 const auto& v_arr = data.disps[f].get_v().get_array();
188 const auto& cc_arr = data.disps[f].get_cc().get_array();
189
190 for (int y = 0; y < H; ++y) {
191 for (int x = 0; x < W; ++x) {
192 u_vol[f].at<double>(y, x) = u_arr(y, x);
193 v_vol[f].at<double>(y, x) = v_arr(y, x);
194 cc_vol[f].at<double>(y, x) = cc_arr(y, x);
195 }
196 }
197 }
198
199 // Step 2: Create bad correlation mask (3D: corrcoef > threshold)
200 // MATLAB: mask_corr_coef = logical(corr_coef > level_corr_coef);
201 std::vector<cv::Mat> mask_bad(Nframe);
202 for (int f = 0; f < Nframe; ++f) {
203 mask_bad[f] = cv::Mat(H, W, CV_8U, cv::Scalar(0));
204 for (int y = 0; y < H; ++y) {
205 for (int x = 0; x < W; ++x) {
206 if (cc_vol[f].at<double>(y, x) > level_corr_coef) {
207 mask_bad[f].at<uint8_t>(y, x) = 255;
208 }
209 }
210 }
211 }
212
213 // Step 3: Compute shrunk mask to avoid border effects
214 // MATLAB: mask_in = u(:,:,end)~=0;
215 // [mask_out] = shrink_mask(mask_in, 0.9);
216 // mask_out = repmat(mask_out, [1 1 Nframe]);
217 cv::Mat mask_in(H, W, CV_8U, cv::Scalar(0));
218 for (int y = 0; y < H; ++y) {
219 for (int x = 0; x < W; ++x) {
220 if (u_vol[Nframe - 1].at<double>(y, x) != 0.0) {
221 mask_in.at<uint8_t>(y, x) = 255;
222 }
223 }
224 }
225 cv::Mat mask_shrunk = matlab_shrink_mask(mask_in, shrink_factor);
226
227 // Step 4: First pass - Gaussian filter and replace
228 // MATLAB: uf = imgaussfilt3(u, sigma, 'FilterDomain','spatial','padding','replicate');
229 std::vector<cv::Mat> uf = matlab_imgaussfilt3(u_vol, sigma_spatial, sigma_temporal);
230 std::vector<cv::Mat> vf = matlab_imgaussfilt3(v_vol, sigma_spatial, sigma_temporal);
231
232 // MATLAB: umodif1(mask_corr_coef & mask_out) = uf(mask_corr_coef & mask_out);
233 std::vector<cv::Mat> u_modif1(Nframe), v_modif1(Nframe);
234 int total_replaced = 0;
235 for (int f = 0; f < Nframe; ++f) {
236 u_modif1[f] = u_vol[f].clone();
237 v_modif1[f] = v_vol[f].clone();
238 for (int y = 0; y < H; ++y) {
239 for (int x = 0; x < W; ++x) {
240 if (mask_bad[f].at<uint8_t>(y, x) && mask_shrunk.at<uint8_t>(y, x)) {
241 u_modif1[f].at<double>(y, x) = uf[f].at<double>(y, x);
242 v_modif1[f].at<double>(y, x) = vf[f].at<double>(y, x);
243 total_replaced++;
244 }
245 }
246 }
247 }
248
249 // Step 5: Second pass - filter the already-modified data and replace again
250 // MATLAB: uf = imgaussfilt3(umodif1, sigma, ...);
251 // umodif2(mask_corr_coef & mask_out) = uf(mask_corr_coef & mask_out);
252 uf = matlab_imgaussfilt3(u_modif1, sigma_spatial, sigma_temporal);
253 vf = matlab_imgaussfilt3(v_modif1, sigma_spatial, sigma_temporal);
254
255 std::vector<cv::Mat> u_modif2(Nframe), v_modif2(Nframe);
256 for (int f = 0; f < Nframe; ++f) {
257 u_modif2[f] = u_vol[f].clone();
258 v_modif2[f] = v_vol[f].clone();
259 for (int y = 0; y < H; ++y) {
260 for (int x = 0; x < W; ++x) {
261 if (mask_bad[f].at<uint8_t>(y, x) && mask_shrunk.at<uint8_t>(y, x)) {
262 u_modif2[f].at<double>(y, x) = uf[f].at<double>(y, x);
263 v_modif2[f].at<double>(y, x) = vf[f].at<double>(y, x);
264 }
265 }
266 }
267 }
268
269 // Step 6: Reconstruct Disp2D objects with modified displacement data
270 // Disp2D is immutable, so we must create new instances
271 for (int f = 0; f < Nframe; ++f) {
272 const auto& old_disp = data.disps[f];
273 const auto& old_roi = old_disp.get_roi();
274 auto scalefactor = old_disp.get_scalefactor();
275
276 // Copy arrays from cv::Mat back to Array2D
277 ncorr::Array2D<double> new_u(H, W);
278 ncorr::Array2D<double> new_v(H, W);
279 ncorr::Array2D<double> new_cc(H, W);
280
281 const auto& old_cc = old_disp.get_cc().get_array();
282 for (int y = 0; y < H; ++y) {
283 for (int x = 0; x < W; ++x) {
284 new_u(y, x) = u_modif2[f].at<double>(y, x);
285 new_v(y, x) = v_modif2[f].at<double>(y, x);
286 new_cc(y, x) = old_cc(y, x); // Preserve original correlation coefficients
287 }
288 }
289
290 data.disps[f] = ncorr::Disp2D(
291 std::move(new_v), std::move(new_u), std::move(new_cc),
292 old_roi, scalefactor
293 );
294 }
295
296 LOG_DEBUG << " matlab_replacebadcorr: replaced " << total_replaced
297 << " subsets across " << Nframe << " frames";
298
299 return total_replaced;
300}
301
302} // namespace cppxdic
Lightweight, dependency-free logging facility for CPPxDIC.
#define LOG_DEBUG
Definition logging.h:169
DataSerializer - Abstract base class for format-agnostic DIC data I/O.
std::vector< cv::Mat > matlab_imgaussfilt3(const std::vector< cv::Mat > &volume, double sigma_spatial, double sigma_temporal)
Applies a 3D Gaussian filter (spatial + temporal) to a volume of 2D fields.
int matlab_replacebadcorr(ncorr::DIC_analysis_output &data, double level_corr_coef=1.0, double sigma_spatial=1.0, double sigma_temporal=2.0, double shrink_factor=0.9)
Replaces badly correlated displacement data with spatiotemporally filtered values.
cv::Mat matlab_shrink_mask(const cv::Mat &mask, double factor)
Shrinks a binary mask by a scale factor around its centroid.