CppNCorr
C++ ncorr Digital Image Correlation engine
Loading...
Searching...
No Matches
ncorr.cpp
Go to the documentation of this file.
1/*
2 * File: ncorr.cpp
3 * Author: justin
4 *
5 * Created on May 12, 2015, 1:33 AM
6 */
7
8#include "ncorr.h"
9#include "ncorr/log.h"
10#include <algorithm>
11#include <cmath>
12#include <cstdlib>
13#include <iostream>
14#include <omp.h>
15#include <ostream>
16#include <set>
17#include <thread>
18#include <vector>
19
20namespace ncorr {
21
22namespace details {
23 // nonlinear optimizer ---------------------------------------------------//
24 std::pair<const Array2D<double>&, bool> nloptimizer_base::global(const Array2D<double>& params_init) const {
25 chk_input_params_size(params_init);
26
27 // Copy initial params into params
28 std::copy(params_init.get_pointer(), params_init.get_pointer() + params_init.size(), params.get_pointer());
29
30 // Find initial guess
31 bool success = initial_guess();
32
33 // Performs iterative search only if initial guess is successful
34 return success ? (*this)(params) : std::pair<const Array2D<double>&, bool>(params, false);
35 }
36
37 std::pair<const Array2D<double>&, bool> nloptimizer_base::operator()(const Array2D<double>& params_guess) const {
38 chk_input_params_size(params_guess);
39
40 // Copy initial guess params into params
41 std::copy(params_guess.get_pointer(), params_guess.get_pointer() + params_guess.size(), params.get_pointer());
42
43 // Perform iterative search
44 bool success = iterative_search();
45
46 return { params, success };
47 }
48
50 if (!params.same_size(this->params)) {
51 throw std::invalid_argument("Attempted to input params of size: " + params.size_2D_string() +
52 " in nonlinear optimizer. Input params must have size of: " + this->params.size_2D_string() + ".");
53 }
54 }
55
56 bool disp_nloptimizer::initial_guess() const {
57 // Note: params = {p1_new, p2_new, p1_old, p2_old, v_old, u_old, dv_dp1_old, dv_dp2_old, du_dp1_old, du_dp2_old, dist, grad_norm}
58
59 // Get nlinfo corresponding to region_idx.
60 const auto &nlinfo_old = disp.get_roi().get_nlinfo(region_idx);
61 if (nlinfo_old.empty()) {
62 // If nlinfo_old is empty then an initial guess cannot be found.
63 NLOG_WARN << "WARNING - d_initial_guess : nlinfo_old of region " << region_idx << " is empty params initial guess cannot be found";
64 return false;
65 }
66
67 // Cycle over nlinfo_old and find integer coordinates where displacement
68 // maps closest to p1_new and p2_new, This will give the global estimate.
69 double min_distance = std::numeric_limits<double>::max(); // Set to max so it gets updated
70 for (difference_type nl_idx = 0; nl_idx < nlinfo_old.nodelist.width(); ++nl_idx) {
71 difference_type p2_old_unscaled = nl_idx + nlinfo_old.left_nl;
72 for (difference_type np_idx = 0; np_idx < nlinfo_old.noderange(nl_idx); np_idx += 2) {
73 difference_type np_top_unscaled = nlinfo_old.nodelist(np_idx,nl_idx);
74 difference_type np_bottom_unscaled = nlinfo_old.nodelist(np_idx + 1,nl_idx);
75 for (difference_type p1_old_unscaled = np_top_unscaled; p1_old_unscaled <= np_bottom_unscaled; ++p1_old_unscaled) {
76 // Get displacement directly from data array
77 double v_old = disp.get_v().get_array()(p1_old_unscaled, p2_old_unscaled);
78 double u_old = disp.get_u().get_array()(p1_old_unscaled, p2_old_unscaled);
79
80 // Get old position
81 difference_type p1_old = p1_old_unscaled * disp.get_scalefactor();
82 difference_type p2_old = p2_old_unscaled * disp.get_scalefactor();
83
84 // Calculate distance
85 double distance = std::sqrt(std::pow(params(0) - (p1_old + v_old), 2.0) +
86 std::pow(params(1) - (p2_old + u_old), 2.0));
87
88 if (distance < min_distance) {
89 // Store params
90 params(2) = p1_old;
91 params(3) = p2_old;
92 params(4) = v_old;
93 params(5) = u_old;
94 min_distance = distance;
95 }
96 }
97 }
98 }
99 // Initial guess will always succeed if nlinfo_old is nonempty
100 return true;
101 }
102
103 bool disp_nloptimizer::iterative_search() const {
104 // Note: params = {p1_new, p2_new, p1_old, p2_old, v_old, u_old, dv_dp1_old, dv_dp2_old, du_dp1_old, du_dp2_old, dist, grad_norm}
105
106 bool success = newton(); // Initial iteration - always perform at least 1
107 for (difference_type counter = 1; success && params(11) > cutoff_norm && counter < cutoff_iterations; ++counter) {
108 success = newton();
109 }
110
111 return success;
112 }
113
114 bool disp_nloptimizer::newton() const {
115 // Note: params = {p1_new, p2_new, p1_old, p2_old, v_old, u_old, dv_dp1_old, dv_dp2_old, du_dp1_old, du_dp2_old, dist, grad_norm}
116
117 // Interpolate value
118 auto fo_pair = disp_interp.first_order(params(2), params(3));
119
120 if (std::isnan(fo_pair.first(0))) {
121 // Interpolated out of bounds; return as failure.
122 NLOG_WARN << "WARNING - d_newton : Interpolated out of bounds (first order returns NaN); for old (p1, p2) = " << params(2) << " ," << params(3);
123 return false;
124 }
125
126 // Get first order u and v references for simplicity
127 const auto &fo_v = fo_pair.first;
128 const auto &fo_u = fo_pair.second;
129
130 // Determine gradient
131 grad_buf(0) = 2 * (params(0) - params(2) - fo_v(0)) * (-1 - fo_v(1)) + 2 * (params(1) - params(3) - fo_u(0)) * (-fo_u(1));
132 grad_buf(1) = 2 * (params(0) - params(2) - fo_v(0)) * (-fo_v(2)) + 2 * (params(1) - params(3) - fo_u(0)) * (-1 - fo_u(2));
133
134 // Determine hessian
135 hess_buf(0,0) = 2 * std::pow(-1 - fo_v(1), 2) + 2 * std::pow(-fo_u(1), 2);
136 hess_buf(1,0) = 2 * -fo_v(2) * (-1 - fo_v(1)) + 2 * (-1 - fo_u(2)) * -fo_u(1);
137 hess_buf(0,1) = hess_buf(1,0); // symmetric
138 hess_buf(1,1) = 2 * std::pow(-fo_v(2), 2) + 2 * std::pow(-1 - fo_u(2), 2);
139
140 // Solve - hessian should be symmetric positive definite so use Cholesky
141 // decomposition.
142 auto hess_buf_linsolver = hess_buf.get_linsolver(LINSOLVER::CHOL);
143 if (hess_buf_linsolver) { // Tests if hessian is actually positive definite
144 // Update p1_old and p2_old
145 const auto &delta_params = hess_buf_linsolver.solve(grad_buf);
146
147 params(2) -= delta_params(0); // update p1_old
148 params(3) -= delta_params(1); // update p2_old
149
150 // Compute displacements/gradients at updated p1_old and p2_old positions
151 auto fo_pair_updated = disp_interp.first_order(params(2), params(3));
152 if (!std::isnan(fo_pair_updated.first(0))) {
153 // Store values
154 params(4) = fo_pair_updated.first(0); // updated v
155 params(5) = fo_pair_updated.second(0); // updated u
156 params(6) = fo_pair_updated.first(1); // updated dv/dp1
157 params(7) = fo_pair_updated.first(2); // updated dv/dp2
158 params(8) = fo_pair_updated.second(1); // updated du/dp1
159 params(9) = fo_pair_updated.second(2); // updated du/dp2
160 params(11) = std::sqrt(dot(grad_buf,grad_buf)); // norm of gradient
161
162 return true;
163 }
164 }
165 // Something failed
166 NLOG_WARN << "WARNING - d_newton : hessian failed; for old (p1, p2) = " << params(2) << " ," << params(3);
167 return false;
168 }
169
170 subregion_nloptimizer::subregion_nloptimizer(const Array2D<double> &A_ref, const Array2D<double> &A_cur, const ROI2D &roi, difference_type scalefactor, INTERP interp_type, SUBREGION subregion_type, difference_type r) :
171 nloptimizer_base(6, 10),
172 A_ref_ptr(std::make_shared<Array2D<double>>(A_ref)),
173 A_cur_ptr(std::make_shared<Array2D<double>>(A_cur)),
174 scalefactor(scalefactor),
175 A_cur_interp(A_cur.get_interpolator(interp_type)),
176 subregion_gen(roi.get_contig_subregion_generator(subregion_type, r)),
177 A_cur_cumsum_p1_ptr(std::make_shared<Array2D<double>>(A_cur)),
178 A_cur_pow_cumsum_p1_ptr(std::make_shared<Array2D<double>>(pow(A_cur, 2.0))),
179 A_ref_template(2*r+1,2*r+1),
180 A_dref_dp1_ptr(std::make_shared<Array2D<double>>(A_ref.height(), A_ref.width())),
181 A_dref_dp2_ptr(std::make_shared<Array2D<double>>(A_ref.height(), A_ref.width())),
182 ref_template_avg(),
183 ref_template_ssd_inv(),
184 A_dref_dv(2*r+1,2*r+1),
185 A_dref_du(2*r+1,2*r+1),
186 A_dref_dv_dp1(2*r+1,2*r+1),
187 A_dref_dv_dp2(2*r+1,2*r+1),
188 A_dref_du_dp1(2*r+1,2*r+1),
189 A_dref_du_dp2(2*r+1,2*r+1),
190 A_cur_template(2*r+1,2*r+1) {
191 if (A_cur_ptr->height() < 2*r+1 || A_cur_ptr->width() < 2*r+1) {
192 // A_cur must be larger than the ref template for cross correlation
193 // function used in initial guess.
194 throw std::invalid_argument("Input current image to subregion optimizer has size of: " + A_cur_ptr->size_2D_string() +
195 ". Size must be bigger than twice the radius plus 1, which is: " + std::to_string(2*r+1) + ".");
196 }
197
198 // Precompute cumsum tables across p1 dimension
199 for (difference_type p2 = 0; p2 < this->A_cur_ptr->width(); ++p2) {
200 for (difference_type p1 = 1; p1 < this->A_cur_ptr->height(); ++p1) { // Skip first row
201 (*this->A_cur_cumsum_p1_ptr)(p1,p2) += (*this->A_cur_cumsum_p1_ptr)(p1-1,p2);
202 (*this->A_cur_pow_cumsum_p1_ptr)(p1,p2) += (*this->A_cur_pow_cumsum_p1_ptr)(p1-1,p2);
203 }
204 }
205 // Precompute reference image gradients at integer locations
206 auto A_ref_interp = A_ref.get_interpolator(interp_type);
207 for (difference_type p2 = 0; p2 < this->A_ref_ptr->width(); ++p2) {
208 for (difference_type p1 = 0; p1 < this->A_ref_ptr->height(); ++p1) {
209 // Note that for some forms of interpolation, the entire array
210 // cannot be interpolated (near boundaries - these values
211 // will be NaNs). Possibly update this later.
212 const auto &fo_ref = A_ref_interp.first_order(p1,p2);
213
214 (*this->A_dref_dp1_ptr)(p1,p2) = fo_ref(1);
215 (*this->A_dref_dp2_ptr)(p1,p2) = fo_ref(2);
216 }
217 }
218 }
219
220 bool subregion_nloptimizer::initial_guess() const {
221 // Perform (slightly modified) JP Lewis NCC for initial guess.
222 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
223
224 // Get subregion nlinfo for input p1 and p2
225 const auto &subregion_nlinfo = subregion_gen(std::round(params(0)), std::round(params(1)));
226
227 // Form image patch for this subregion
228 A_ref_template() = 0;
229 ref_template_avg = 0.0;
230 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
231 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
232 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
233 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
234 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
235 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
236 difference_type p1_shifted = p1 - params(0) + subregion_gen.get_r();
237 difference_type p2_shifted = p2 - params(1) + subregion_gen.get_r();
238
239 A_ref_template(p1_shifted, p2_shifted) = (*A_ref_ptr)(p1,p2);
240 ref_template_avg += A_ref_template(p1_shifted, p2_shifted);
241 }
242 }
243 }
244 ref_template_avg /= subregion_nlinfo.points; // Finish
245
246 // Calculate template_ssd_inv (ssd_inv = inverse sqrt of squared difference)
247 // and subtract average from template. Note that A_ref_template will hold
248 // the ref template with its average subtracted from it. Look at JP Lewis'
249 // fast normalized cross correlation paper to see why.
250 ref_template_ssd_inv = 0.0;
251 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
252 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
253 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
254 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
255 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
256 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
257 difference_type p1_shifted = p1 - params(0) + subregion_gen.get_r();
258 difference_type p2_shifted = p2 - params(1) + subregion_gen.get_r();
259
260 A_ref_template(p1_shifted, p2_shifted) -= ref_template_avg;
261 ref_template_ssd_inv += std::pow(A_ref_template(p1_shifted, p2_shifted), 2.0);
262 }
263 }
264 }
265 ref_template_ssd_inv = std::sqrt(ref_template_ssd_inv);
266
267 if (std::abs(ref_template_ssd_inv) >= std::numeric_limits<double>::epsilon()) {
268 ref_template_ssd_inv = 1/ref_template_ssd_inv; // Finish
269
270 // Must perform cross correlation of ref template with cur array
271 auto A_xcorr = xcorr(*A_cur_ptr, A_ref_template);
272
273 // Cycle and calculate maximum NCC - only cycle over non-overlapping
274 // regions since xcorr uses a FFT and is circular.
275 double NCC_max = -1.0; // range is [-1,1]
276 difference_type p1_NCC_max = -1;
277 difference_type p2_NCC_max = -1;
278 for (difference_type p2 = params(1) - subregion_nlinfo.left; p2 < A_cur_ptr->width() - (subregion_nlinfo.right - params(1)); ++p2) {
279 for (difference_type p1 = params(0) - subregion_nlinfo.top; p1 < A_cur_ptr->height() - (subregion_nlinfo.bottom - params(0)); ++p1) {
280 // Compute A_cur_sum and A_cur_pow_sum using cumsum tables
281 double A_cur_sum = 0;
282 double A_cur_pow_sum = 0;
283 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
284 difference_type p2_shifted = nl_idx + subregion_nlinfo.left_nl - params(1) + p2;
285 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
286 difference_type np_top_shifted = subregion_nlinfo.nodelist(np_idx,nl_idx) - params(0) + p1;
287 difference_type np_bottom_shifted = subregion_nlinfo.nodelist(np_idx + 1,nl_idx) - params(0) + p1;
288
289 if (np_top_shifted > 0) {
290 A_cur_sum -= (*A_cur_cumsum_p1_ptr)(np_top_shifted-1, p2_shifted);
291 A_cur_pow_sum -= (*A_cur_pow_cumsum_p1_ptr)(np_top_shifted-1, p2_shifted);
292 }
293
294 A_cur_sum += (*A_cur_cumsum_p1_ptr)(np_bottom_shifted, p2_shifted);
295 A_cur_pow_sum += (*A_cur_pow_cumsum_p1_ptr)(np_bottom_shifted, p2_shifted);
296 }
297 }
298
299 double A_cur_ssd_inv = std::sqrt(A_cur_pow_sum - std::pow(A_cur_sum,2.0)/subregion_nlinfo.points);
300 if (std::abs(A_cur_ssd_inv) >= std::numeric_limits<double>::epsilon()) {
301 A_cur_ssd_inv = 1/A_cur_ssd_inv; // Finish
302
303 // Calculate NCC value
304 double NCC_buf = A_xcorr(p1,p2) * ref_template_ssd_inv * A_cur_ssd_inv;
305 if (NCC_buf > NCC_max) {
306 NCC_max = NCC_buf;
307 p1_NCC_max = p1;
308 p2_NCC_max = p2;
309 }
310 }
311 }
312 }
313
314 // Get displacements and return - note that there is guaranteed to
315 // be at least one NCC point so no checking for max/min needs to be
316 // done.
317 params(2) = p1_NCC_max - params(0); // v
318 params(3) = p2_NCC_max - params(1); // u
319
320 return true;
321 }
322 // Something failed
323 NLOG_WARN << "WARNING - sr_inital_guess : ref_template_ssd_inv less than eps; for (p1, p2) = " << params(0) << " ," << params(1);
324 return false;
325 }
326
327 bool subregion_nloptimizer::iterative_search() const {
328 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
329
330 // Reset iteration counter for MATLAB-style saturation check
331 last_iteration_count = 0;
332
333 // Get nlinfo for input p1 and p2 - note that initial guess may have not
334 // been called before iterative_search(), so recalculate subregion.
335 const auto &subregion_nlinfo = subregion_gen(std::round(params(0)), std::round(params(1)));
336
337 // Calculate average of ref template
338 ref_template_avg = 0.0;
339 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
340 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
341 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
342 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
343 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
344 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
345 ref_template_avg += (*A_ref_ptr)(p1,p2);
346 }
347 }
348 }
349 ref_template_avg /= subregion_nlinfo.points; // Finish
350
351 // Calculate template_ssd_inv
352 ref_template_ssd_inv = 0.0;
353 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
354 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
355 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
356 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
357 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
358 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
359 ref_template_ssd_inv += std::pow((*A_ref_ptr)(p1,p2) - ref_template_avg, 2.0);
360 }
361 }
362 }
363 ref_template_ssd_inv = std::sqrt(ref_template_ssd_inv);
364
365 if (std::abs(ref_template_ssd_inv) >= std::numeric_limits<double>::epsilon()) {
366 ref_template_ssd_inv = 1/ref_template_ssd_inv; // Finish
367
368 // Precompute steepest descent images
369 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
370 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
371 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
372 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
373 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
374 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
375 difference_type p1_shifted = p1 - params(0) + subregion_gen.get_r();
376 difference_type p2_shifted = p2 - params(1) + subregion_gen.get_r();
377
378 double p1_delta = p1 - params(0);
379 double p2_delta = p2 - params(1);
380
381 if (std::isnan((*A_dref_dp1_ptr)(p1,p2))) {
382 // Note that for some forms of interpolation, the entire array
383 // cannot be interpolated (near boundaries - these values
384 // will be NaNs). Possibly update this later.
385 NLOG_DEBUG << "INFO - sr_iterative_search: near boundaries interpolation yield NaN; for (p1, p2) = " << p1 << " ," << p2;
386 return false;
387 }
388
389 A_dref_dv(p1_shifted,p2_shifted) = (*A_dref_dp1_ptr)(p1,p2);
390 A_dref_du(p1_shifted,p2_shifted) = (*A_dref_dp2_ptr)(p1,p2);
391 A_dref_dv_dp1(p1_shifted,p2_shifted) = (*A_dref_dp1_ptr)(p1,p2) * p1_delta;
392 A_dref_dv_dp2(p1_shifted,p2_shifted) = (*A_dref_dp1_ptr)(p1,p2) * p2_delta;
393 A_dref_du_dp1(p1_shifted,p2_shifted) = (*A_dref_dp2_ptr)(p1,p2) * p1_delta;
394 A_dref_du_dp2(p1_shifted,p2_shifted) = (*A_dref_dp2_ptr)(p1,p2) * p2_delta;
395 }
396 }
397 }
398
399 // Precompute GN Hessian
400 hess_buf() = 0;
401 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
402 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
403 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
404 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
405 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
406 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
407 difference_type p1_shifted = p1 - params(0) + subregion_gen.get_r();
408 difference_type p2_shifted = p2 - params(1) + subregion_gen.get_r();
409
410 // Hessian - only calculate lower half since hessian is symmetric
411 hess_buf(0,0) += A_dref_dv(p1_shifted,p2_shifted) * A_dref_dv(p1_shifted,p2_shifted);
412 hess_buf(1,0) += A_dref_dv(p1_shifted,p2_shifted) * A_dref_du(p1_shifted,p2_shifted);
413 hess_buf(2,0) += A_dref_dv(p1_shifted,p2_shifted) * A_dref_dv_dp1(p1_shifted,p2_shifted);
414 hess_buf(3,0) += A_dref_dv(p1_shifted,p2_shifted) * A_dref_dv_dp2(p1_shifted,p2_shifted);
415 hess_buf(4,0) += A_dref_dv(p1_shifted,p2_shifted) * A_dref_du_dp1(p1_shifted,p2_shifted);
416 hess_buf(5,0) += A_dref_dv(p1_shifted,p2_shifted) * A_dref_du_dp2(p1_shifted,p2_shifted);
417
418 hess_buf(1,1) += A_dref_du(p1_shifted,p2_shifted) * A_dref_du(p1_shifted,p2_shifted);
419 hess_buf(2,1) += A_dref_du(p1_shifted,p2_shifted) * A_dref_dv_dp1(p1_shifted,p2_shifted);
420 hess_buf(3,1) += A_dref_du(p1_shifted,p2_shifted) * A_dref_dv_dp2(p1_shifted,p2_shifted);
421 hess_buf(4,1) += A_dref_du(p1_shifted,p2_shifted) * A_dref_du_dp1(p1_shifted,p2_shifted);
422 hess_buf(5,1) += A_dref_du(p1_shifted,p2_shifted) * A_dref_du_dp2(p1_shifted,p2_shifted);
423
424 hess_buf(2,2) += A_dref_dv_dp1(p1_shifted,p2_shifted) * A_dref_dv_dp1(p1_shifted,p2_shifted);
425 hess_buf(3,2) += A_dref_dv_dp1(p1_shifted,p2_shifted) * A_dref_dv_dp2(p1_shifted,p2_shifted);
426 hess_buf(4,2) += A_dref_dv_dp1(p1_shifted,p2_shifted) * A_dref_du_dp1(p1_shifted,p2_shifted);
427 hess_buf(5,2) += A_dref_dv_dp1(p1_shifted,p2_shifted) * A_dref_du_dp2(p1_shifted,p2_shifted);
428
429 hess_buf(3,3) += A_dref_dv_dp2(p1_shifted,p2_shifted) * A_dref_dv_dp2(p1_shifted,p2_shifted);
430 hess_buf(4,3) += A_dref_dv_dp2(p1_shifted,p2_shifted) * A_dref_du_dp1(p1_shifted,p2_shifted);
431 hess_buf(5,3) += A_dref_dv_dp2(p1_shifted,p2_shifted) * A_dref_du_dp2(p1_shifted,p2_shifted);
432
433 hess_buf(4,4) += A_dref_du_dp1(p1_shifted,p2_shifted) * A_dref_du_dp1(p1_shifted,p2_shifted);
434 hess_buf(5,4) += A_dref_du_dp1(p1_shifted,p2_shifted) * A_dref_du_dp2(p1_shifted,p2_shifted);
435
436 hess_buf(5,5) += A_dref_du_dp2(p1_shifted,p2_shifted) * A_dref_du_dp2(p1_shifted,p2_shifted);
437 }
438 }
439 }
440 // Finish hessian
441 for (difference_type p2 = 0; p2 < hess_buf.width(); p2++) {
442 for (difference_type p1 = p2; p1 < hess_buf.height(); p1++) {
443 hess_buf(p1,p2) *= 2 * std::pow(ref_template_ssd_inv, 2.0); // Finish
444 hess_buf(p2,p1) = hess_buf(p1,p2); // Fill upperhalf
445 }
446 }
447
448 // Get linsolver - use Cholesky decomposition since hessian should
449 // be symmetric positive definite.
450 hess_linsolver = hess_buf.get_linsolver(LINSOLVER::CHOL);
451 if (hess_linsolver) {
452 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
453 bool success = newton(); // Initial iteration - always perform at least 1
454 last_iteration_count = 1;
455 for (difference_type counter = 1; success && params(9) > cutoff_norm && counter < cutoff_iterations; ++counter) {
456 success = newton();
457 last_iteration_count = counter + 1;
458 }
459
460 return success;
461 }
462 }
463 // Something failed
464 //std::cout << "WARNING - sr_iterative_search: Hessian probably failed; for (p1, p2) = " << params(0) << " ," << params(1) << std::endl;
465 return false;
466 }
467
468 bool subregion_nloptimizer::newton() const {
469 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
470
471 // subregion_nlinfo is already guaranteed to be set since
472 // iterative_search() is always called before newton(), so just get a
473 // reference to the subregion from subregion_gen.
474 const auto &subregion_nlinfo = subregion_gen.get_subregion_nlinfo();
475
476 A_cur_template() = 0;
477 double cur_template_avg = 0.0;
478 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
479 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
480 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
481 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
482 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
483 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
484 difference_type p1_shifted = p1 - params(0) + subregion_gen.get_r();
485 difference_type p2_shifted = p2 - params(1) + subregion_gen.get_r();
486
487 double p1_delta = p1 - params(0);
488 double p2_delta = p2 - params(1);
489
490 // Find transformed coordinates
491 double p1_transformed = p1 + params(2) + params(4) * p1_delta + params(5) * p2_delta;
492 double p2_transformed = p2 + params(3) + params(6) * p1_delta + params(7) * p2_delta;
493
494 // Interpolate and store value
495 A_cur_template(p1_shifted, p2_shifted) = A_cur_interp(p1_transformed, p2_transformed);
496
497 if (std::isnan(A_cur_template(p1_shifted, p2_shifted))) {
498 // If interpolated out of range, return false.
499 NLOG_DEBUG << "INFO - sr_newton: near boundaries interpolation yield NaN; for (p1, p2) = " << p1_shifted << " ," << p2_shifted;
500 return false;
501 }
502
503 cur_template_avg += A_cur_template(p1_shifted, p2_shifted);
504 }
505 }
506 }
507 cur_template_avg /= subregion_nlinfo.points; // Finish
508
509 // Calculate cur_template_ssd_inv
510 double cur_template_ssd_inv = 0.0;
511 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
512 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
513 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
514 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
515 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
516 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
517 difference_type p1_shifted = p1 - params(0) + subregion_gen.get_r();
518 difference_type p2_shifted = p2 - params(1) + subregion_gen.get_r();
519
520 cur_template_ssd_inv += std::pow(A_cur_template(p1_shifted,p2_shifted) - cur_template_avg, 2.0);
521 }
522 }
523 }
524 cur_template_ssd_inv = std::sqrt(cur_template_ssd_inv);
525
526 if (std::abs(cur_template_ssd_inv) >= std::numeric_limits<double>::epsilon()) {
527 cur_template_ssd_inv = 1/cur_template_ssd_inv; // Finish
528
529 // Calculate gradient and correlation coefficient
530 grad_buf() = 0.0;
531 params(8) = 0.0; // correlation coefficient
532 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
533 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
534 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
535 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
536 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
537 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
538 difference_type p1_shifted = p1 - params(0) + subregion_gen.get_r();
539 difference_type p2_shifted = p2 - params(1) + subregion_gen.get_r();
540
541 double normalized_diff = ((*A_ref_ptr)(p1,p2) - ref_template_avg) * ref_template_ssd_inv - (A_cur_template(p1_shifted,p2_shifted) - cur_template_avg) * cur_template_ssd_inv;
542
543 grad_buf(0) += normalized_diff * A_dref_dv(p1_shifted,p2_shifted);
544 grad_buf(1) += normalized_diff * A_dref_du(p1_shifted,p2_shifted);
545 grad_buf(2) += normalized_diff * A_dref_dv_dp1(p1_shifted,p2_shifted);
546 grad_buf(3) += normalized_diff * A_dref_dv_dp2(p1_shifted,p2_shifted);
547 grad_buf(4) += normalized_diff * A_dref_du_dp1(p1_shifted,p2_shifted);
548 grad_buf(5) += normalized_diff * A_dref_du_dp2(p1_shifted,p2_shifted);
549
550 // correlation coefficient
551 params(8) += std::pow(normalized_diff, 2.0);
552 }
553 }
554 }
555
556 // Finish gradient (note: make this negative since delta_p is solved
557 // with the negative gradient)
558 for (difference_type p = 0; p < grad_buf.height(); ++p) {
559 grad_buf(p) *= -2 * ref_template_ssd_inv;
560 }
561
562 // Find delta_p
563 const auto &delta_p = hess_linsolver.solve(grad_buf);
564
565 // Calculate norm of delta_p
566 params(9) = std::sqrt(dot(delta_p,delta_p));
567
568 // Update parameters using inverse composition
569 double denominator = (delta_p(5) + delta_p(2) + delta_p(5)*delta_p(2) - delta_p(4)*delta_p(3) + 1);
570 if (std::abs(denominator) >= std::numeric_limits<double>::epsilon()) {
571 double v = params(2);
572 double u = params(3);
573 double dv_dp1 = params(4);
574 double dv_dp2 = params(5);
575 double du_dp1 = params(6);
576 double du_dp2 = params(7);
577
578 params(2) = v - ((dv_dp1+1)*(delta_p(0)*(1+delta_p(5))-delta_p(1)*delta_p(3)) + dv_dp2*(delta_p(1)*(1+delta_p(2))-delta_p(0)*delta_p(4)))/denominator;
579 params(3) = u - ((du_dp2+1)*(delta_p(1)*(1+delta_p(2))-delta_p(0)*delta_p(4)) + du_dp1*(delta_p(0)*(1+delta_p(5))-delta_p(1)*delta_p(3)))/denominator;
580 params(4) = ((delta_p(5)+1)*(dv_dp1+1) - delta_p(4)*dv_dp2)/denominator - 1;
581 params(5) = (dv_dp2*(delta_p(2)+1) - delta_p(3)*(dv_dp1+1))/denominator;
582 params(6) = (du_dp1*(delta_p(5)+1) - delta_p(4)*(du_dp2+1))/denominator;
583 params(7) = ((delta_p(2)+1)*(du_dp2+1) - delta_p(3)*du_dp1)/denominator - 1;
584
585 return true;
586 }
587 }
588 // Something failed
589 NLOG_WARN << "WARNING - sr_newton: hessian probably failed; for (p1, p2) = " << params(0) << " ," << params(1);
590 return false;
591 }
592}
593
594// Interface functions -------------------------------------------------------//
595namespace details {
596 // Update boundary with SKIP_ALL mode (original behavior)
597 // Returns empty boundary if ANY point returns NaN
599 typedef ROI2D::difference_type difference_type;
600
601 auto boundary_updated = boundary; // Make a copy
602 for (difference_type idx = 0; idx < boundary_updated.height(); ++idx) {
603 // Get displacement at position of boundary
604 auto disp_pair = disp_interp(boundary_updated(idx,0) * roi_scalefactor, boundary_updated(idx,1) * roi_scalefactor);
605
606 if (std::isnan(disp_pair.first)) {
607 // Boundary interpolated out of range - return an empty boundary
608 // to signal failure.
609 return Array2D<double>(0,2);
610 }
611
612 // Add displacement to boundary position.
613 boundary_updated(idx,0) += disp_pair.first / roi_scalefactor;
614 boundary_updated(idx,1) += disp_pair.second / roi_scalefactor;
615 }
616
617 return boundary_updated;
618 }
619
620 // Update boundary with SKIP_INVALID mode (new robust behavior)
621 // Skips NaN points and out-of-bounds points, only returns empty if ALL points invalid
623 const Array2D<double> &boundary,
624 const Disp2D::nlinfo_interpolator &disp_interp,
625 const ROI2D::difference_type roi_scalefactor,
626 const ROI2D::difference_type roi_height,
627 const ROI2D::difference_type roi_width,
628 const ROI2D::difference_type margin = 0) { // margin for bounds checking
629
630 typedef ROI2D::difference_type difference_type;
631
632 if (boundary.height() == 0) {
633 return Array2D<double>(0,2);
634 }
635
636 // Collect valid updated points
637 std::vector<std::pair<double, double>> valid_points;
638 valid_points.reserve(boundary.height());
639
640 for (difference_type idx = 0; idx < boundary.height(); ++idx) {
641 // Get displacement at position of boundary
642 double p1 = boundary(idx,0) * roi_scalefactor;
643 double p2 = boundary(idx,1) * roi_scalefactor;
644 auto disp_pair = disp_interp(p1, p2);
645
646 // Skip NaN points (interpolation failed)
647 if (std::isnan(disp_pair.first) || std::isnan(disp_pair.second)) {
648 continue;
649 }
650
651 // Calculate new position
652 double new_p1 = boundary(idx,0) + disp_pair.first / roi_scalefactor;
653 double new_p2 = boundary(idx,1) + disp_pair.second / roi_scalefactor;
654
655 // Bounds checking: skip points that move outside valid range
656 // Using margin like MATLAB does (typically radius size)
657 if (new_p1 < margin || new_p1 >= roi_height - margin ||
658 new_p2 < margin || new_p2 >= roi_width - margin) {
659 continue;
660 }
661
662 valid_points.emplace_back(new_p1, new_p2);
663 }
664
665 // Return empty boundary if all points were invalid
666 if (valid_points.empty()) {
667 return Array2D<double>(0,2);
668 }
669
670 // Create result array from valid points
671 Array2D<double> boundary_updated(valid_points.size(), 2);
672 for (difference_type i = 0; i < static_cast<difference_type>(valid_points.size()); ++i) {
673 boundary_updated(i,0) = valid_points[i].first;
674 boundary_updated(i,1) = valid_points[i].second;
675 }
676
677 return boundary_updated;
678 }
679}
680
681ROI2D update(const ROI2D &roi, const Disp2D &disp, INTERP interp_type, ROI_UPDATE_MODE mode) {
682 typedef ROI2D::difference_type difference_type;
683
684 // regions in 'roi' and 'disp' must correspond
685 if (roi.size_regions() != disp.get_roi().size_regions()) {
686 throw std::invalid_argument("Attempted to update ROI2D which has " + std::to_string(roi.size_regions()) +
687 " regions with a Disp2D which has " + std::to_string(disp.get_roi().size_regions()) +
688 " regions. The number of regions must be the same and they must correspond to each other.");
689 }
690
691 // Update only supported for:
692 // 1) Reduced ROI2D with the same data size as the Disp2D. In this case,
693 // the scalefactor of the ROI2D is assumed to be the same as Disp2D's
694 // scalefactor.
695 // 2) Full sized ROI2D with a reduced size (based on Disp2D's scalefactor)
696 // the same as Disp2D. In this case, the scalefactor of the ROI2D is
697 // assumed to be 1.
698 difference_type roi_scalefactor;
699 if (roi.height() == disp.data_height() && roi.width() == disp.data_width()) {
700 roi_scalefactor = disp.get_scalefactor();
701 } else if (std::ceil(double(roi.height()) / disp.get_scalefactor()) == disp.data_height() &&
702 std::ceil(double(roi.width()) / disp.get_scalefactor()) == disp.data_width()) {
703 roi_scalefactor = 1;
704 } else {
705 throw std::invalid_argument("Attempted to update ROI2D which has size of: " + roi.size_2D_string() +
706 " with a Disp2D which has data size of: " + disp.size_2D_string() + " and a scale factor of: " + std::to_string(disp.get_scalefactor()) +
707 ". Size of ROI2D or reduced ROI2D must match data size of Disp2D.");
708 }
709
710 // Update roi by updating each region boundary
711 std::vector<ROI2D::region_boundary> boundaries_updated(roi.size_regions());
712 for (difference_type region_idx = 0; region_idx < roi.size_regions(); ++region_idx) {
713 if (disp.get_roi().get_nlinfo(region_idx).empty()) {
714 // If nlinfo in disp is empty for this region, continue since no
715 // interpolation can be done.
716 continue;
717 }
718
719 // Get nlinfo interpolator from disp
720 auto disp_interp = disp.get_nlinfo_interpolator(region_idx, interp_type);
721
722 // Get region_boundary
723 const auto &boundary = roi.get_boundary(region_idx);
724
725 // Form new boundary -------------------------------------------------//
726 ROI2D::region_boundary boundary_updated;
727
728 if (mode == ROI_UPDATE_MODE::SKIP_ALL) {
729 // Original behavior: fail entire boundary if any point returns NaN
730 boundary_updated.add = details::update_boundary_skip_all(boundary.add, disp_interp, roi_scalefactor);
731
732 for (auto &sub_boundary : boundary.sub) {
733 boundary_updated.sub.emplace_back(details::update_boundary_skip_all(sub_boundary, disp_interp, roi_scalefactor));
734 }
735 } else {
736 // SKIP_INVALID mode: skip individual NaN/out-of-bounds points
737 // Use margin=0 for now (could be made configurable if needed)
739 boundary.add, disp_interp, roi_scalefactor, roi.height(), roi.width(), 0);
740
741 for (auto &sub_boundary : boundary.sub) {
742 boundary_updated.sub.emplace_back(details::update_boundary_skip_invalid(
743 sub_boundary, disp_interp, roi_scalefactor, roi.height(), roi.width(), 0));
744 }
745 }
746
747 // Store boundary
748 boundaries_updated[region_idx] = std::move(boundary_updated);
749 }
750
751 return ROI2D(std::move(boundaries_updated), roi.height(), roi.width());
752}
753
754namespace details {
756 const Disp2D::nlinfo_interpolator& disp_interp,
757 const ROI2D::difference_type roi_scalefactor,
758 const ROI2D::difference_type size_mask_height,
759 const ROI2D::difference_type size_mask_width,
760 const ROI2D::difference_type radius) {
761 typedef ROI2D::difference_type difference_type;
762
763 if (boundary.height() == 0) {
764 return Array2D<double>(0, 2);
765 }
766
767 std::vector<std::pair<double, double>> valid_points;
768 valid_points.reserve(boundary.height());
769
770 for (difference_type idx = 0; idx < boundary.height(); ++idx) {
771 const double p1 = boundary(idx, 0) * roi_scalefactor;
772 const double p2 = boundary(idx, 1) * roi_scalefactor;
773 const auto disp_pair = disp_interp(p1, p2);
774
775 if (std::isnan(disp_pair.first) || std::isnan(disp_pair.second)) {
776 continue;
777 }
778
779 const double new_p1 = boundary(idx, 0) + disp_pair.first / roi_scalefactor;
780 const double new_p2 = boundary(idx, 1) + disp_pair.second / roi_scalefactor;
781
782 if (new_p1 < radius || new_p1 > size_mask_height - radius ||
783 new_p2 < radius || new_p2 > size_mask_width - radius) {
784 continue;
785 }
786
787 valid_points.emplace_back(new_p1, new_p2);
788 }
789
790 if (valid_points.empty()) {
791 return Array2D<double>(0, 2);
792 }
793
794 Array2D<double> boundary_updated(valid_points.size(), 2);
795 for (difference_type idx = 0; idx < difference_type(valid_points.size()); ++idx) {
796 boundary_updated(idx, 0) = valid_points[idx].first;
797 boundary_updated(idx, 1) = valid_points[idx].second;
798 }
799
800 return boundary_updated;
801 }
802}
803
805 const Disp2D& disp,
806 INTERP interp_type,
807 ROI2D::difference_type radius) {
808 typedef ROI2D::difference_type difference_type;
809
810 if (roi.size_regions() != disp.get_roi().size_regions()) {
811 throw std::invalid_argument("Attempted to matlab_update_roi with ROI2D regions: " +
812 std::to_string(roi.size_regions()) + " and Disp2D regions: " +
813 std::to_string(disp.get_roi().size_regions()) + ".");
814 }
815
816 difference_type roi_scalefactor;
817 if (roi.height() == disp.data_height() && roi.width() == disp.data_width()) {
818 roi_scalefactor = disp.get_scalefactor();
819 } else if (std::ceil(double(roi.height()) / disp.get_scalefactor()) == disp.data_height() &&
820 std::ceil(double(roi.width()) / disp.get_scalefactor()) == disp.data_width()) {
821 roi_scalefactor = 1;
822 } else {
823 throw std::invalid_argument("Attempted to matlab_update_roi with ROI2D size of: " + roi.size_2D_string() +
824 " and Disp2D data size of: " + disp.size_2D_string() +
825 " and scale factor of: " + std::to_string(disp.get_scalefactor()) + ".");
826 }
827
828 std::vector<ROI2D::region_boundary> boundaries_updated(roi.size_regions());
829 for (difference_type region_idx = 0; region_idx < roi.size_regions(); ++region_idx) {
830 if (disp.get_roi().get_nlinfo(region_idx).empty()) {
831 continue;
832 }
833
834 const auto disp_interp = disp.get_nlinfo_interpolator(region_idx, interp_type);
835 const auto& boundary = roi.get_boundary(region_idx);
836
837 ROI2D::region_boundary boundary_updated;
838 boundary_updated.add = details::matlab_update_boundary(
839 boundary.add,
840 disp_interp,
841 roi_scalefactor,
842 roi.height(),
843 roi.width(),
844 radius
845 );
846
847 boundary_updated.sub.reserve(boundary.sub.size());
848 for (const auto& sub_boundary : boundary.sub) {
849 boundary_updated.sub.emplace_back(details::matlab_update_boundary(
850 sub_boundary,
851 disp_interp,
852 roi_scalefactor,
853 roi.height(),
854 roi.width(),
855 radius
856 ));
857 }
858
859 boundaries_updated[region_idx] = std::move(boundary_updated);
860 }
861
862 return ROI2D(std::move(boundaries_updated), roi.height(), roi.width());
863}
864
865namespace details {
867 typedef ROI2D::difference_type difference_type;
868
869 if (nlinfo.empty()) {
870 // For empty nlinfo nothing is changed
871 return SDA;
872 }
873
874 // Computes signed distance array for an nlinfo. Note that this is
875 // probably not the fastest way to this, but it is simple/easy/correct
876 // and calculating the SDA is typically not a bottleneck.
877
878 mask_buf() = true;
879 fill(mask_buf, nlinfo, false);
880 mask_buf_new() = false;
881 difference_type dist = 1; // Initialize to 1 since SDA will be 0 outside ROI
882 bool points_added = true; // Keeps track if points were added to the border
883 while (points_added) {
884 // While loop will expand border by 1 pixel per iteration and exit
885 // when the ROI is filled.
886 points_added = false;
887 for (difference_type nl_idx = 0; nl_idx < nlinfo.nodelist.width(); ++nl_idx) {
888 difference_type p2 = nl_idx + nlinfo.left_nl;
889 for (difference_type np_idx = 0; np_idx < nlinfo.noderange(nl_idx); np_idx += 2) {
890 difference_type np_top = nlinfo.nodelist(np_idx,nl_idx);
891 difference_type np_bottom = nlinfo.nodelist(np_idx + 1,nl_idx);
892 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
893 if (!mask_buf(p1,p2)) {
894 // This point has not been analyzed yet. See if
895 // any of the four neighboring points is true.
896 if ((mask_buf.in_bounds(p1 - 1,p2) && mask_buf(p1 - 1, p2)) || // Up
897 (mask_buf.in_bounds(p1 + 1,p2) && mask_buf(p1 + 1, p2)) || // Down
898 (mask_buf.in_bounds(p1,p2 - 1) && mask_buf(p1, p2 - 1)) || // Left
899 (mask_buf.in_bounds(p1,p2 + 1) && mask_buf(p1, p2 + 1))) { // Right
900 // This is a boundary point, so set its value and
901 // set mask_buf_new to mark that it has been
902 // analyzed.
903 SDA(p1,p2) = dist;
904 mask_buf_new(p1,p2) = true;
905 points_added = true;
906 }
907 }
908 }
909 }
910 }
911
912 // Transfer nlinfo of mask_buf_new to mask_buf
913 for (difference_type nl_idx = 0; nl_idx < nlinfo.nodelist.width(); ++nl_idx) {
914 difference_type p2 = nl_idx + nlinfo.left_nl;
915 for (difference_type np_idx = 0; np_idx < nlinfo.noderange(nl_idx); np_idx += 2) {
916 difference_type np_top = nlinfo.nodelist(np_idx,nl_idx);
917 difference_type np_bottom = nlinfo.nodelist(np_idx + 1,nl_idx);
918 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
919 mask_buf(p1,p2) = mask_buf_new(p1,p2);
920 }
921 }
922 }
923
924 // Increment dist
925 ++dist;
926 }
927
928 return SDA;
929 }
930
932 typedef ROI2D::difference_type difference_type;
933
934 // Initialize SDA and two mask buffers - be sure to initialize SDA to 0
935 Array2D<difference_type> SDA(roi.height(), roi.width());
936 Array2D<bool> mask_buf1(roi.height(), roi.width());
937 Array2D<bool> mask_buf2(roi.height(), roi.width());
938 for (difference_type region_idx = 0; region_idx < roi.size_regions(); ++region_idx) {
939 // Form SDA for each region
940 if (!roi.get_nlinfo(region_idx).empty()) {
941 get_nlinfo_SDA(SDA, roi.get_nlinfo(region_idx), mask_buf1, mask_buf2);
942 }
943 }
944
945 return SDA;
946 }
947
949 const ROI2D::region_nlinfo &nlinfo,
950 const disp_nloptimizer &d_nloptimizer,
952 Array2D<double> &params_buf) {
953 // Note: params = {p1_new, p2_new, p1_old, p2_old, v_old, u_old, dv_dp1_old, dv_dp2_old, du_dp1_old, du_dp2_old, dist, grad_norm}
954
955 if (nlinfo.empty()) {
956 // If nlinfo is empty, cannot get seed params. Return empty Array2D
957 // to signal failure
958 return Array2D<double>();
959 }
960
961 // Find maximum SDA location inside this region - use this as initial seed point
962 auto max_info = max(SDA, nlinfo);
963
964 // Set params for nonlinear optimizer
965 params_buf(0) = max_info.second.first * data.get_scalefactor();
966 params_buf(1) = max_info.second.second * data.get_scalefactor();
967 params_buf(10) = max_info.first;
968
969 // Get rest of params using global search
970 auto seed_params_pair = d_nloptimizer.global(params_buf);
971 if (seed_params_pair.second) {
972 // Successful
973 return seed_params_pair.first;
974 }
975 // Something failed
976 return Array2D<double>();
977 }
978
979 bool analyze_point(const Array2D<double> &queue_params,
980 ROI2D::difference_type p1_new_delta,
981 ROI2D::difference_type p2_new_delta,
982 const ROI2D::region_nlinfo &nlinfo,
983 ROI2D::difference_type roi_scalefactor,
984 const disp_nloptimizer &d_nloptimizer,
986 std::priority_queue<Array2D<double>, std::vector<Array2D<double>>, std::function<bool(const Array2D<double>&,const Array2D<double>&)>> &queue,
987 Array2D<bool> &A_new_ap,
988 Array2D<double> &params_buf) {
989 typedef ROI2D::difference_type difference_type;
990
991 // Note: params = {p1_new, p2_new, p1_old, p2_old, v_old, u_old, dv_dp1_old, dv_dp2_old, du_dp1_old, du_dp2_old, dist, grad_norm}
992
993 // Make sure points are in bounds of roi_new's nlinfo and haven't been
994 // analyzed yet.
995 difference_type p1_new = queue_params(0) + p1_new_delta;
996 difference_type p2_new = queue_params(1) + p2_new_delta;
997 if (nlinfo.in_nlinfo(p1_new / roi_scalefactor, p2_new / roi_scalefactor) &&
998 !A_new_ap(p1_new / roi_scalefactor, p2_new / roi_scalefactor)) {
999 A_new_ap(p1_new / roi_scalefactor, p2_new / roi_scalefactor) = true; // Mark as analyzed
1000
1001 // Make sure denominator isn't close to zero
1002 double denominator = (queue_params(8) * queue_params(7) - (1 + queue_params(6)) * (1 + queue_params(9)));
1003 if (std::abs(denominator) >= std::numeric_limits<double>::epsilon()) {
1004 // Get initial guess using gradients
1005 double p1_old_delta = (queue_params(7) * (p2_new - (queue_params(3) + queue_params(5))) - (1 + queue_params(9)) * (p1_new - (queue_params(2) + queue_params(4)))) / denominator;
1006 double p2_old_delta = (queue_params(8) * (p1_new - (queue_params(2) + queue_params(4))) - (1 + queue_params(6)) * (p2_new - (queue_params(3) + queue_params(5)))) / denominator;
1007
1008 // Fill in params
1009 params_buf(0) = p1_new;
1010 params_buf(1) = p2_new;
1011 params_buf(2) = queue_params(2) + p1_old_delta;
1012 params_buf(3) = queue_params(3) + p2_old_delta;
1013 params_buf(10) = SDA(p1_new / roi_scalefactor, p2_new / roi_scalefactor);
1014
1015 // Get optimized params from nonlinear optimizer
1016 auto params_pair = d_nloptimizer(params_buf);
1017 if (params_pair.second) {
1018 // Insert into queue
1019 queue.push(params_pair.first);
1020
1021 return true;
1022 } else {
1023 // Optimization failed using guess, so try global optimization.
1024 auto params_global_pair = d_nloptimizer.global(params_buf);
1025 if (params_global_pair.second) {
1026 // Insert into queue
1027 queue.push(params_global_pair.first);
1028
1029 return true;
1030 }
1031 }
1032 }
1033 }
1034 // Something failed
1035 //std::cout << "WARNING - d_analyze_point failed" << std::endl;
1036 return false;
1037 }
1038}
1039
1040Data2D update(const Data2D &data, const Disp2D &disp, INTERP interp_type, ROI_UPDATE_MODE mode) {
1041 typedef ROI2D::difference_type difference_type;
1042
1043 // regions in 'data' and 'disp' must correspond
1044 if (data.get_roi().size_regions() != disp.get_roi().size_regions()) {
1045 throw std::invalid_argument("Attempted to update Data2D which has " + std::to_string(data.get_roi().size_regions()) +
1046 " regions with a Disp2D which has " + std::to_string(disp.get_roi().size_regions()) +
1047 " regions. The number of regions must be the same and they must correspond to each other.");
1048 }
1049
1050 // Update only supported for:
1051 // 1) Reduced Data2D with the same data size as the Disp2D and equal
1052 // scalefactors.
1053 // 2) Full sized Data2D with a reduced size the same Disp2D
1054 if (!(data.get_scalefactor() == disp.get_scalefactor() && data.data_height() == disp.data_height() && data.data_width() == disp.data_width()) &&
1055 !(data.get_scalefactor() == 1 && std::ceil(double(data.data_height()) / disp.get_scalefactor()) == disp.data_height() && std::ceil(double(data.data_width()) / disp.get_scalefactor()) == disp.data_width())) {
1056 throw std::invalid_argument("Attempted to update Data2D which has a data size of " + data.size_2D_string() + " and scale factor of: " + std::to_string(data.get_scalefactor()) +
1057 " with a Disp2D which has a data size of " + disp.size_2D_string() + " and scale factor of: " + std::to_string(disp.get_scalefactor()) +
1058 ". Data2D must either have the same scalefactor as Disp2D with the same data size, or Data2D must be full sized with a reduced size the same as Disp2D.");
1059 }
1060
1061 // Update ROI and then fill in updated regions.
1062 auto roi_new = update(data.get_roi(), disp, interp_type, mode);
1063
1064 // Check for empty ROI after update and warn
1065 if (roi_new.get_points() == 0) {
1066 NLOG_WARN << "WARNING: ROI update in Data2D::update() produced an empty ROI!";
1067 NLOG_WARN << " Original ROI had " << data.get_roi().get_points() << " points.";
1068 NLOG_WARN << " Mode: " << (mode == ROI_UPDATE_MODE::SKIP_ALL ? "SKIP_ALL" : "SKIP_INVALID");
1069 }
1070
1071 // Get signed distance array for roi_new - this guides queue such that
1072 // interior points are updated first.
1073 auto SDA = details::get_ROI_SDA(roi_new);
1074
1075 // Form A_new and other buffers and then fill in values in updated ROI
1076 Array2D<double> A_new(data.data_height(), data.data_width()); // Newly interpolated array
1077 Array2D<bool> A_new_ap(data.data_height(), data.data_width()); // Analyzed points
1078 Array2D<bool> A_new_vp(data.data_height(), data.data_width()); // Valid points
1079 Array2D<double> params_buf(12, 1);
1080 for (difference_type region_idx = 0; region_idx < roi_new.size_regions(); ++region_idx) {
1081 if (disp.get_roi().get_nlinfo(region_idx).empty() || roi_new.get_nlinfo(region_idx).empty()) {
1082 // If either nlinfo is empty, there cannot be any updates for this region
1083 continue;
1084 }
1085
1086 // Get nonlinear displacement optimizer for this region
1087 details::disp_nloptimizer d_nloptimizer(disp, region_idx, interp_type);
1088
1089 // Get data nlinfo interpolator
1090 auto data_interp = data.get_nlinfo_interpolator(region_idx, interp_type);
1091
1092 // Get seed params for queue
1093 auto seed_params = details::get_seed_params(data, roi_new.get_nlinfo(region_idx), d_nloptimizer, SDA, params_buf);
1094 if (!seed_params.empty()) {
1095 A_new_ap(seed_params(0) / data.get_scalefactor(), seed_params(1) / data.get_scalefactor()) = true; // Mark as analyzed
1096
1097 // Form queue - make it a priority queue based on SDA value. This
1098 // will analyze points of high distance from boundary first.
1099 auto comp = [](const Array2D<double> &a, const Array2D<double> &b ) { return a(10) < b(10); };
1100 std::priority_queue<Array2D<double>, std::vector<Array2D<double>>, std::function<bool(const Array2D<double>&,const Array2D<double>&)>> queue(comp);
1101
1102 // Perform flood fill around seed
1103 queue.push(seed_params);
1104 while (!queue.empty()) {
1105 // 1) pop info from queue
1106 auto queue_params = std::move(queue.top()); queue.pop();
1107
1108 // 2) Validate and interpolate point
1109 A_new(queue_params(0) / data.get_scalefactor(), queue_params(1) / data.get_scalefactor()) = data_interp(queue_params(2), queue_params(3));
1110 if (!std::isnan(A_new(queue_params(0) / data.get_scalefactor(), queue_params(1) / data.get_scalefactor()))) {
1111 A_new_vp(queue_params(0) / data.get_scalefactor(), queue_params(1) / data.get_scalefactor()) = true;
1112 }
1113
1114 // 3) Analyze four surrounding points - up, down, left right;
1115 details::analyze_point(queue_params, -data.get_scalefactor(), 0, roi_new.get_nlinfo(region_idx), data.get_scalefactor(), d_nloptimizer, SDA, queue, A_new_ap, params_buf);
1116 details::analyze_point(queue_params, data.get_scalefactor(), 0, roi_new.get_nlinfo(region_idx), data.get_scalefactor(), d_nloptimizer, SDA, queue, A_new_ap, params_buf);
1117 details::analyze_point(queue_params, 0, -data.get_scalefactor(), roi_new.get_nlinfo(region_idx), data.get_scalefactor(), d_nloptimizer, SDA, queue, A_new_ap, params_buf);
1118 details::analyze_point(queue_params, 0, data.get_scalefactor(), roi_new.get_nlinfo(region_idx), data.get_scalefactor(), d_nloptimizer, SDA, queue, A_new_ap, params_buf);
1119 }
1120 }
1121 }
1122 // Must form union with valid points
1123 return Data2D(std::move(A_new), roi_new.form_union(A_new_vp), data.get_scalefactor());
1124}
1125
1126Disp2D add(const std::vector<Disp2D> &disps, INTERP interp_type) {
1127 typedef ROI2D::difference_type difference_type;
1128
1129 // This will add displacements WRT the configuration of the first displacement
1130 // plot.
1131
1132 if (disps.empty()) {
1133 // Return empty disp
1134 return Disp2D();
1135 } else if (disps.size() == 1) {
1136 // Return input disp
1137 return disps.front();
1138 }
1139
1140 // Adding displacements only supported for displacements which have the same
1141 // scalefactor, data size, and number of regions.
1142 difference_type scalefactor = disps.front().get_scalefactor();
1143 const auto &roi = disps.front().get_roi();
1144 for (difference_type disp_idx = 1; disp_idx < difference_type(disps.size()); ++disp_idx) {
1145 if (disps[disp_idx].get_scalefactor() != scalefactor ||
1146 disps[disp_idx].data_height() != disps.front().data_height() ||
1147 disps[disp_idx].data_width() != disps.front().data_width() ||
1148 disps[disp_idx].get_roi().size_regions() != roi.size_regions()) {
1149 throw std::invalid_argument("Attempted to add displacements with differing scalefactors, sizes, or number of regions. All scalefactors, sizes, and number of regions must be the same.");
1150 }
1151 }
1152
1153 // Form buffers and add one region at a time
1154 Array2D<bool> A_vp(roi.height(), roi.width());
1155 Array2D<double> A_v_added(roi.height(), roi.width());
1156 Array2D<double> A_u_added(roi.height(), roi.width());
1157 Array2D<double> A_cc_combined(roi.height(), roi.width()); // Combined correlation (minimum = worst case)
1158 A_cc_combined() = 1.0; // Initialize to best possible correlation
1159
1160 for (difference_type region_idx = 0; region_idx < roi.size_regions(); ++region_idx) {
1161 // Get nlinfo interpolators for each displacement field for this region
1162 std::vector<Disp2D::nlinfo_interpolator> disp_nlinfo_interps;
1163 for (difference_type disp_idx = 0; disp_idx < difference_type(disps.size()); ++disp_idx) {
1164 disp_nlinfo_interps.push_back(disps[disp_idx].get_nlinfo_interpolator(region_idx, interp_type));
1165 }
1166
1167 // Add displacements for this region
1168 for (difference_type nl_idx = 0; nl_idx < roi.get_nlinfo(region_idx).nodelist.width(); ++nl_idx) {
1169 difference_type p2_unscaled = nl_idx + roi.get_nlinfo(region_idx).left_nl;
1170 difference_type p2 = p2_unscaled * scalefactor;
1171 for (difference_type np_idx = 0; np_idx < roi.get_nlinfo(region_idx).noderange(nl_idx); np_idx += 2) {
1172 difference_type np_top = roi.get_nlinfo(region_idx).nodelist(np_idx,nl_idx);
1173 difference_type np_bottom = roi.get_nlinfo(region_idx).nodelist(np_idx + 1,nl_idx);
1174 for (difference_type p1_unscaled = np_top; p1_unscaled <= np_bottom; ++p1_unscaled) {
1175 difference_type p1 = p1_unscaled * scalefactor;
1176
1177 // Cycle over displacements and add displacement values
1178 double v_added = 0;
1179 double u_added = 0;
1180 double cc_min = 1.0; // Track minimum correlation (worst quality)
1181 for (difference_type disp_idx = 0; disp_idx < difference_type(disps.size()); ++disp_idx) {
1182 // Make sure coords are close to this nlinfo - the
1183 // further away points are from the roi, the more
1184 // extrapolation is required and hence less accurate.
1185 bool near_nlinfo = false;
1186 difference_type window = 1;
1187 difference_type p1_unscaled = std::round((p1 + v_added) / scalefactor);
1188 difference_type p2_unscaled = std::round((p2 + u_added) / scalefactor);
1189 for (difference_type p2_window = p2_unscaled - window; p2_window <= p2_unscaled + window; ++p2_window) {
1190 for (difference_type p1_window = p1_unscaled - window; p1_window <= p1_unscaled + window; ++p1_window) {
1191 if (disps[disp_idx].get_roi().get_nlinfo(region_idx).in_nlinfo(p1_window,p2_window)) {
1192 near_nlinfo = true;
1193 }
1194 if (near_nlinfo) { break; }
1195 }
1196 if (near_nlinfo) { break; }
1197 }
1198
1199 if (near_nlinfo) {
1200 auto disp_pair = disp_nlinfo_interps[disp_idx](p1 + v_added, p2 + u_added);
1201 double cc_val = disp_nlinfo_interps[disp_idx].get_cc(p1 + v_added, p2 + u_added);
1202 cc_min = std::min(cc_min, cc_val); // Take minimum correlation
1203
1204 v_added += disp_pair.first;
1205 u_added += disp_pair.second;
1206 } else {
1207 // Set to NaN if position is not within ROI - this
1208 // will trigger a break
1209 v_added = std::numeric_limits<double>::quiet_NaN();
1210 u_added = std::numeric_limits<double>::quiet_NaN();
1211 }
1212
1213 if (std::isnan(v_added)) {
1214 // This interpolated out of bounds, so break
1215 break;
1216 }
1217 }
1218
1219 if (!std::isnan(v_added)) {
1220 // Only add point if it was interpolated in bounds
1221 A_v_added(p1_unscaled, p2_unscaled) = v_added;
1222 A_u_added(p1_unscaled, p2_unscaled) = u_added;
1223 A_cc_combined(p1_unscaled, p2_unscaled) = cc_min; // Store worst-case correlation
1224 A_vp(p1_unscaled, p2_unscaled) = true;
1225 }
1226 }
1227 }
1228 }
1229 }
1230 // Must form union with valid points
1231 return { std::move(A_v_added), std::move(A_u_added), std::move(A_cc_combined), roi.form_union(A_vp), scalefactor };
1232}
1233
1234// MATLAB-style add function - uses each displacement's own ROI for bounds checking.
1235// This is the canonical chain-composition entry point. It delegates to
1236// exact_add_with_rois(), which mirrors MATLAB ncorr_alg_addanalysis +
1237// ncorr_alg_adddisp:
1238// - per-region 4-neighbour expand_filt extrapolation into a border halo,
1239// - biquintic B-spline interpolation on the extrapolated plot,
1240// - reduced-grid walk with a strict in_nlinfo mask check at every link.
1241// The legacy implementation that lived here (window-halo bounds check +
1242// Disp2D::nlinfo_interpolator's Laplace inpaint) introduced a per-pixel jump
1243// at segment boundaries with large bridge magnitudes; see seam diagnostics
1244// in matlab_DIC_analysis_*. The INTERP argument is preserved for ABI
1245// compatibility but is ignored: MATLAB always uses biquintic here.
1246Disp2D add_with_rois(const std::vector<Disp2D> &disps,
1247 const std::vector<ROI2D> &rois,
1248 INTERP /*interp_type*/) {
1249 return exact_add_with_rois(disps, rois);
1250}
1251
1252// Legacy implementation kept under a different name for A/B comparison /
1253// regression checking. NOT used anywhere; safe to delete once the
1254// exact_add_with_rois output has been validated end-to-end.
1255namespace legacy_detail {
1256Disp2D add_with_rois_legacy(const std::vector<Disp2D> &disps,
1257 const std::vector<ROI2D> &rois,
1258 INTERP interp_type) {
1259 typedef ROI2D::difference_type difference_type;
1260
1261 if (disps.empty()) {
1262 return Disp2D();
1263 } else if (disps.size() == 1) {
1264 return disps.front();
1265 }
1266
1267 // Validate inputs
1268 if (disps.size() != rois.size()) {
1269 throw std::invalid_argument("Number of displacements (" + std::to_string(disps.size()) +
1270 ") must match number of ROIs (" + std::to_string(rois.size()) + ").");
1271 }
1272
1273 // Check that all have same scalefactor, data size, and number of regions
1274 difference_type scalefactor = disps.front().get_scalefactor();
1275 const auto &roi_first = rois.front();
1276 for (difference_type idx = 1; idx < difference_type(disps.size()); ++idx) {
1277 if (disps[idx].get_scalefactor() != scalefactor ||
1278 disps[idx].data_height() != disps.front().data_height() ||
1279 disps[idx].data_width() != disps.front().data_width() ||
1280 rois[idx].size_regions() != roi_first.size_regions()) {
1281 throw std::invalid_argument("Attempted to add displacements with differing scalefactors, sizes, or number of regions.");
1282 }
1283 }
1284
1285 // Form buffers using the first ROI as the reference frame
1286 Array2D<bool> A_vp(roi_first.height(), roi_first.width());
1287 Array2D<double> A_v_added(roi_first.height(), roi_first.width());
1288 Array2D<double> A_u_added(roi_first.height(), roi_first.width());
1289 Array2D<double> A_cc_combined(roi_first.height(), roi_first.width());
1290 A_cc_combined() = 1.0;
1291
1292 // Border for extrapolation (matching MATLAB's border_interp = 20)
1293 const difference_type border_interp = 20;
1294
1295 for (difference_type region_idx = 0; region_idx < roi_first.size_regions(); ++region_idx) {
1296 // Get nlinfo interpolators for each displacement field for this region
1297 std::vector<Disp2D::nlinfo_interpolator> disp_nlinfo_interps;
1298 for (difference_type disp_idx = 0; disp_idx < difference_type(disps.size()); ++disp_idx) {
1299 disp_nlinfo_interps.push_back(disps[disp_idx].get_nlinfo_interpolator(region_idx, interp_type));
1300 }
1301
1302 // Cycle over each point in the FIRST ROI
1303 for (difference_type nl_idx = 0; nl_idx < roi_first.get_nlinfo(region_idx).nodelist.width(); ++nl_idx) {
1304 difference_type p2_unscaled = nl_idx + roi_first.get_nlinfo(region_idx).left_nl;
1305 difference_type p2 = p2_unscaled * scalefactor;
1306 for (difference_type np_idx = 0; np_idx < roi_first.get_nlinfo(region_idx).noderange(nl_idx); np_idx += 2) {
1307 difference_type np_top = roi_first.get_nlinfo(region_idx).nodelist(np_idx,nl_idx);
1308 difference_type np_bottom = roi_first.get_nlinfo(region_idx).nodelist(np_idx + 1,nl_idx);
1309 for (difference_type p1_unscaled = np_top; p1_unscaled <= np_bottom; ++p1_unscaled) {
1310 difference_type p1 = p1_unscaled * scalefactor;
1311
1312 // Forward propagate this point through all displacements
1313 bool prop_success = true;
1314 double v_added = 0;
1315 double u_added = 0;
1316 double cc_min = 1.0;
1317
1318 double x_cur = static_cast<double>(p1);
1319 double y_cur = static_cast<double>(p2);
1320
1321 for (difference_type disp_idx = 0; disp_idx < difference_type(disps.size()); ++disp_idx) {
1322 // Check if current position is within bounds of THIS displacement's ROI
1323 // (with some tolerance for interpolation near boundaries)
1324 difference_type x_cur_unscaled = std::round(x_cur / scalefactor);
1325 difference_type y_cur_unscaled = std::round(y_cur / scalefactor);
1326
1327 // Check bounds against THIS displacement's ROI (not the first one)
1328 bool in_bounds = false;
1329 const difference_type window = border_interp / scalefactor; // Allow some extrapolation
1330 for (difference_type y_win = y_cur_unscaled - window; y_win <= y_cur_unscaled + window && !in_bounds; ++y_win) {
1331 for (difference_type x_win = x_cur_unscaled - window; x_win <= x_cur_unscaled + window && !in_bounds; ++x_win) {
1332 if (rois[disp_idx].get_nlinfo(region_idx).in_nlinfo(x_win, y_win)) {
1333 in_bounds = true;
1334 }
1335 }
1336 }
1337
1338 if (in_bounds) {
1339 // Interpolate displacement at current position
1340 auto disp_pair = disp_nlinfo_interps[disp_idx](x_cur, y_cur);
1341
1342 if (!std::isnan(disp_pair.first) && !std::isnan(disp_pair.second)) {
1343 double cc_val = disp_nlinfo_interps[disp_idx].get_cc(x_cur, y_cur);
1344 cc_min = std::min(cc_min, cc_val);
1345
1346 // Accumulate displacements
1347 v_added += disp_pair.first;
1348 u_added += disp_pair.second;
1349
1350 // Update current position for next displacement
1351 x_cur += disp_pair.first;
1352 y_cur += disp_pair.second;
1353 } else {
1354 prop_success = false;
1355 break;
1356 }
1357 } else {
1358 prop_success = false;
1359 break;
1360 }
1361 }
1362
1363 // If point was successfully propagated through all displacements, store it
1364 if (prop_success) {
1365 A_v_added(p1_unscaled, p2_unscaled) = v_added;
1366 A_u_added(p1_unscaled, p2_unscaled) = u_added;
1367 A_cc_combined(p1_unscaled, p2_unscaled) = cc_min;
1368 A_vp(p1_unscaled, p2_unscaled) = true;
1369 }
1370 }
1371 }
1372 }
1373 }
1374
1375 // Form result ROI from valid points
1376 return { std::move(A_v_added), std::move(A_u_added), std::move(A_cc_combined), roi_first.form_union(A_vp), scalefactor };
1377}
1378} // namespace legacy_detail
1379
1380// DIC_analysis --------------------------------------------------------------//
1381namespace details {
1383 typedef ROI2D::difference_type difference_type;
1384
1385 if (nlinfo.empty()) {
1386 // Will return NaN if nlinfo is empty.
1387 return Array2D<double>({ std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN() });
1388 }
1389
1390 double p1_centroid = 0;
1391 double p2_centroid = 0;
1392 for (difference_type nl_idx = 0; nl_idx < nlinfo.nodelist.width(); ++nl_idx) {
1393 difference_type p2 = nl_idx + nlinfo.left_nl;
1394 for (difference_type np_idx = 0; np_idx < nlinfo.noderange(nl_idx); np_idx += 2) {
1395 difference_type np_top = nlinfo.nodelist(np_idx,nl_idx);
1396 difference_type np_bottom = nlinfo.nodelist(np_idx + 1,nl_idx);
1397 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
1398 p1_centroid += p1;
1399 p2_centroid += p2;
1400 }
1401 }
1402 }
1403 // Finish centroids
1404 p1_centroid /= nlinfo.points;
1405 p2_centroid /= nlinfo.points;
1406
1407 return Array2D<double>({ p1_centroid, p2_centroid });
1408 }
1409
1410 std::pair<std::vector<ROI2D::region_nlinfo>,std::vector<ROI2D::region_nlinfo>> nlinfo_line_split(const Array2D<double> &direc,
1411 const Array2D<double> &origin,
1412 ROI2D::difference_type partition_offset,
1413 const ROI2D::region_nlinfo &nlinfo,
1414 Array2D<ROI2D::difference_type> &partition_diagram,
1415 Array2D<bool> &mask_buf) {
1416 typedef ROI2D::difference_type difference_type;
1417
1418 // Divides regions across input vector at specified origin - note that
1419 // this split may not be contiguous.
1420
1421 for (difference_type nl_idx = 0; nl_idx < nlinfo.nodelist.width(); ++nl_idx) {
1422 difference_type p2 = nl_idx + nlinfo.left_nl;
1423 for (difference_type np_idx = 0; np_idx < nlinfo.noderange(nl_idx); np_idx += 2) {
1424 difference_type np_top = nlinfo.nodelist(np_idx,nl_idx);
1425 difference_type np_bottom = nlinfo.nodelist(np_idx + 1,nl_idx);
1426 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
1427 // See if (p1,p2) is on the left or right side of the input
1428 // vector.
1429 double point1_p1 = origin(0), point1_p2 = origin(1);
1430 double point2_p1 = origin(0) + direc(0), point2_p2 = origin(1) + direc(1);
1431 double point3_p1 = p1, point3_p2 = p2;
1432
1433 // Note that this "turn" predicate is simple and not robust,
1434 // but the values used here will generally be well behaved
1435 // so this shouldn't be an issue.
1436 double turn_val = point2_p2*point3_p1 - point2_p1*point3_p2 - point1_p2*(point3_p1-point2_p1) + point1_p1*(point3_p2-point2_p2);
1437 if (std::abs(turn_val) < std::numeric_limits<double>::epsilon()) {
1438 // Lies on the axis
1439 partition_diagram(p1,p2) = partition_offset;
1440 } else if (turn_val > 0){
1441 // Right side of axis
1442 partition_diagram(p1,p2) = partition_offset;
1443 } else {
1444 // Left side of axis
1445 partition_diagram(p1,p2) = partition_offset+1;
1446 }
1447
1448 }
1449 }
1450 }
1451
1452 // Return split nlinfos
1453 mask_buf() = false;
1454 fill(mask_buf, nlinfo, true);
1455 mask_buf = (std::move(mask_buf) & (partition_diagram == partition_offset));
1456 auto nlinfo_part1_pair = ROI2D::region_nlinfo::form_nlinfos(mask_buf);
1457
1458 mask_buf() = false;
1459 fill(mask_buf, nlinfo, true);
1460 mask_buf = (std::move(mask_buf) & (partition_diagram == partition_offset+1));
1461 auto nlinfo_part2_pair = ROI2D::region_nlinfo::form_nlinfos(mask_buf);
1462
1463 return { std::move(nlinfo_part1_pair.first), std::move(nlinfo_part2_pair.first) };
1464 }
1465
1467 const ROI2D::region_nlinfo &nlinfo,
1470 Array2D<bool> &A_ap) {
1471 typedef ROI2D::difference_type difference_type;
1472
1473 // Performs contiguous expansion for the sub_nlinfo which is within
1474 // nlinfo.
1475
1476 std::queue<difference_type> queue;
1477 for (difference_type nl_idx = 0; nl_idx < sub_nlinfo.nodelist.width(); ++nl_idx) {
1478 difference_type p2 = nl_idx + sub_nlinfo.left_nl;
1479 for (difference_type np_idx = 0; np_idx < sub_nlinfo.noderange(nl_idx); np_idx += 2) {
1480 difference_type np_top = sub_nlinfo.nodelist(np_idx,nl_idx);
1481 difference_type np_bottom = sub_nlinfo.nodelist(np_idx + 1,nl_idx);
1482 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
1483 if (nlinfo.in_nlinfo(p1-1,p2) && A_ap(p1-1,p2)) { A_ap(p1-1,p2) = false; queue.push(p1-1); queue.push(p2); }
1484 if (nlinfo.in_nlinfo(p1+1,p2) && A_ap(p1+1,p2)) { A_ap(p1+1,p2) = false; queue.push(p1+1); queue.push(p2); }
1485 if (nlinfo.in_nlinfo(p1,p2+1) && A_ap(p1,p2+1)) { A_ap(p1,p2+1) = false; queue.push(p1); queue.push(p2+1); }
1486 if (nlinfo.in_nlinfo(p1,p2-1) && A_ap(p1,p2-1)) { A_ap(p1,p2-1) = false; queue.push(p1); queue.push(p2-1); }
1487 }
1488 }
1489 }
1490
1491 while (!queue.empty()) {
1492 difference_type p1 = queue.front(); queue.pop();
1493 difference_type p2 = queue.front(); queue.pop();
1494
1495 // Fill plot
1496 A(p1,p2) = val;
1497
1498 // Check four surrounding neighbors
1499 if (nlinfo.in_nlinfo(p1-1,p2) && A_ap(p1-1,p2)) { A_ap(p1-1,p2) = false; queue.push(p1-1); queue.push(p2); }
1500 if (nlinfo.in_nlinfo(p1+1,p2) && A_ap(p1+1,p2)) { A_ap(p1+1,p2) = false; queue.push(p1+1); queue.push(p2); }
1501 if (nlinfo.in_nlinfo(p1,p2+1) && A_ap(p1,p2+1)) { A_ap(p1,p2+1) = false; queue.push(p1); queue.push(p2+1); }
1502 if (nlinfo.in_nlinfo(p1,p2-1) && A_ap(p1,p2-1)) { A_ap(p1,p2-1) = false; queue.push(p1); queue.push(p2-1); }
1503 }
1504 }
1505
1506 std::pair<std::pair<ROI2D::region_nlinfo,ROI2D::region_nlinfo>, bool> nlinfo_contig_split(ROI2D::difference_type partition_offset,
1507 const ROI2D::region_nlinfo &nlinfo,
1508 Array2D<ROI2D::difference_type> &partition_diagram,
1509 Array2D<bool> &mask_buf) {
1510 typedef ROI2D::difference_type difference_type;
1511
1512 // This will attempt to split the nlinfo contiguously by finding the
1513 // centroid, major axis, and minor axis and then partitioning the region
1514 // base on these values. If successful, the two split regions are
1515 // returned as nlinfos.
1516
1517 if (nlinfo.empty()) {
1518 // Cannot split an empty nlinfo, so return as failed.
1519 return { { ROI2D::region_nlinfo(), ROI2D::region_nlinfo() } , false };
1520 }
1521
1522 // Find centroid
1523 auto centroid = get_centroid(nlinfo);
1524
1525 // Find moments
1526 double Mp1p1 = 0;
1527 double Mp1p2 = 0;
1528 double Mp2p2 = 0;
1529 for (difference_type nl_idx = 0; nl_idx < nlinfo.nodelist.width(); ++nl_idx) {
1530 difference_type p2 = nl_idx + nlinfo.left_nl;
1531 for (difference_type np_idx = 0; np_idx < nlinfo.noderange(nl_idx); np_idx += 2) {
1532 difference_type np_top = nlinfo.nodelist(np_idx,nl_idx);
1533 difference_type np_bottom = nlinfo.nodelist(np_idx + 1,nl_idx);
1534 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
1535 Mp1p1 += std::pow(p1 - centroid(0),2);
1536 Mp1p2 += (p1 - centroid(0)) * (p2 - centroid(1));
1537 Mp2p2 += std::pow(p2 - centroid(1),2);
1538 }
1539 }
1540 }
1541 // Finish moments
1542 Mp1p1 /= nlinfo.points;
1543 Mp1p2 /= nlinfo.points;
1544 Mp2p2 /= nlinfo.points;
1545
1546 // Obtain eigenvectors and eigenvalues - since this is a simple 2x2
1547 // matrix, a direct analytic solution can be used.
1548 double eig_val1 = 0.0;
1549 double eig_val2 = 0.0;
1550 Array2D<double> eig_vec1(2,1);
1551 Array2D<double> eig_vec2(2,1);
1552 if (std::abs(Mp1p2) >= std::numeric_limits<double>::epsilon()) {
1553 double trace = Mp1p1 + Mp2p2;
1554 double det = Mp1p1*Mp2p2 - std::pow(Mp1p2,2);
1555
1556 eig_val1 = trace/2.0 + std::sqrt((std::pow(trace,2.0)/4.0 - det));
1557 eig_val2 = trace/2.0 - std::sqrt((std::pow(trace,2.0)/4.0 - det));
1558
1559 eig_vec1(0,0) = eig_val1-Mp2p2; eig_vec1(1,0) = Mp1p2;
1560 eig_vec2(0,0) = eig_val2-Mp2p2; eig_vec2(1,0) = Mp1p2;
1561
1562 // Normalize eigenvectors
1563 eig_vec1 = normalize(std::move(eig_vec1));
1564 eig_vec2 = normalize(std::move(eig_vec2));
1565 } else {
1566 // Edge condition - matrix is diagonal
1567 eig_val1 = Mp1p1;
1568 eig_val2 = Mp2p2;
1569 eig_vec1(0,0) = 1; eig_vec1(1,0) = 0;
1570 eig_vec2(0,0) = 0; eig_vec2(1,0) = 1;
1571 }
1572
1573 // Get major and minor axes
1574 const auto &minor_axis = eig_val1 <= eig_val2 ? eig_vec1 : eig_vec2;
1575 const auto &major_axis = eig_val1 > eig_val2 ? eig_vec1 : eig_vec2;
1576
1577 // 1) Attempt to split across minor axis first.
1578 // 2) If this results in more than two regions, then attempt to split
1579 // across the major axis
1580 // 3) If this results in more than two regions, then use the split that
1581 // results in the two largest regions and then contiguously grow the
1582 // regions (starting with the smaller one first) until the entire
1583 // nlinfo is split in two.
1584
1585 // 1) Try minor axis split
1586 auto nlinfos_minor_split_pair = nlinfo_line_split(minor_axis, centroid, partition_offset, nlinfo, partition_diagram, mask_buf);
1587 if (nlinfos_minor_split_pair.first.size() == 0 || nlinfos_minor_split_pair.second.size() == 0) {
1588 // Region is too small to be split, so return as failed
1589 return { { ROI2D::region_nlinfo(), ROI2D::region_nlinfo() } , false };
1590 }
1591
1592 if (nlinfos_minor_split_pair.first.size() != 1 || nlinfos_minor_split_pair.second.size() != 1) {
1593 // 2) Try major axis split next
1594 auto nlinfos_major_split_pair = nlinfo_line_split(major_axis, centroid, partition_offset, nlinfo, partition_diagram, mask_buf);
1595 if (nlinfos_major_split_pair.first.size() == 0 || nlinfos_major_split_pair.second.size() == 0) {
1596 // Region is too small to be split, so return as failed.
1597 return { { ROI2D::region_nlinfo(), ROI2D::region_nlinfo() } , false };
1598 }
1599
1600 if (nlinfos_major_split_pair.first.size() != 1 || nlinfos_major_split_pair.second.size() != 1) {
1601 // 3) See which split has the two largest contiguous regions and
1602 // then perform contiguous expansion.
1603 auto nlinfo_compare = [](const ROI2D::region_nlinfo &a, const ROI2D::region_nlinfo &b) { return a.points < b.points; };
1604
1605 const auto &minor_largest1 = *std::max_element(nlinfos_minor_split_pair.first.begin(), nlinfos_minor_split_pair.first.end(), nlinfo_compare);
1606 const auto &minor_largest2 = *std::max_element(nlinfos_minor_split_pair.second.begin(), nlinfos_minor_split_pair.second.end(), nlinfo_compare);
1607
1608 const auto &major_largest1 = *std::max_element(nlinfos_major_split_pair.first.begin(), nlinfos_major_split_pair.first.end(), nlinfo_compare);
1609 const auto &major_largest2 = *std::max_element(nlinfos_major_split_pair.second.begin(), nlinfos_major_split_pair.second.end(), nlinfo_compare);
1610
1611 difference_type minor_sum = minor_largest1.points + minor_largest2.points;
1612 difference_type major_sum = major_largest1.points + major_largest2.points;
1613
1614 // Must make contiguous again --------------------------------//
1615 // Get the nlinfos used for contiguous expansion
1616 const auto &nlinfo_larger = minor_sum > major_sum ? (minor_largest2.points >= minor_largest1.points ? minor_largest2 : minor_largest1) :
1617 (major_largest2.points >= major_largest1.points ? major_largest2 : major_largest1);
1618 const auto &nlinfo_smaller = minor_sum > major_sum ? (minor_largest2.points < minor_largest1.points ? minor_largest2 : minor_largest1) :
1619 (major_largest2.points < major_largest1.points ? major_largest2 : major_largest1);
1620
1621 auto partition_num_larger = minor_sum > major_sum ? (minor_largest2.points >= minor_largest1.points ? partition_offset+1 : partition_offset) :
1622 (major_largest2.points >= major_largest1.points ? partition_offset+1 : partition_offset);
1623 auto partition_num_smaller = minor_sum > major_sum ? (minor_largest2.points < minor_largest1.points ? partition_offset+1 : partition_offset) :
1624 (major_largest2.points < major_largest1.points ? partition_offset+1 : partition_offset);
1625
1626 // Use mask buf to keep track of which points are left that need
1627 // to be analyzed (like A_ap).
1628 mask_buf() = false;
1629 fill(mask_buf, nlinfo, true);
1630 fill(mask_buf, nlinfo_larger, false);
1631 fill(mask_buf, nlinfo_smaller, false);
1632
1633 // Refill partition diagram
1634 fill(partition_diagram, nlinfo_larger, partition_num_larger);
1635 fill(partition_diagram, nlinfo_smaller, partition_num_smaller);
1636
1637 // Perform a contiguous expansion for each region, starting with
1638 // the smaller nlinfo first
1639 nlinfo_contig_expansion(nlinfo_smaller, nlinfo, partition_num_smaller, partition_diagram, mask_buf);
1640 nlinfo_contig_expansion(nlinfo_larger, nlinfo, partition_num_larger, partition_diagram, mask_buf);
1641 }
1642 }
1643
1644 // Get nlinfo for first partition
1645 mask_buf() = false;
1646 fill(mask_buf, nlinfo, true);
1647 mask_buf = (std::move(mask_buf) & (partition_diagram == partition_offset));
1648 auto nlinfo_part1_pair = ROI2D::region_nlinfo::form_nlinfos(mask_buf);
1649
1650 // Get nlinfo for second partition
1651 mask_buf() = false;
1652 fill(mask_buf, nlinfo, true);
1653 mask_buf = (std::move(mask_buf) & (partition_diagram == partition_offset+1));
1654 auto nlinfo_part2_pair = ROI2D::region_nlinfo::form_nlinfos(mask_buf);
1655
1656 if (nlinfo_part1_pair.first.size() != 1 || nlinfo_part2_pair.first.size() != 1) {
1657 // This is a programmer error
1658 throw std::invalid_argument("nlinfo_contig_split() did not actually form 2 contiguous subregions");
1659 }
1660
1661 return { { std::move(nlinfo_part1_pair.first.front()), std::move(nlinfo_part2_pair.first.front()) }, true };
1662 }
1663
1665 ROI2D::difference_type part_num2,
1666 const ROI2D::region_nlinfo &nlinfo,
1667 Array2D<ROI2D::difference_type> &partition_diagram,
1668 Array2D<bool> &mask_buf) {
1669 typedef ROI2D::difference_type difference_type;
1670
1671 // Exit condition
1672 if (part_num2 == part_num1) {
1673 // Finish partition and then return
1674 fill(partition_diagram, nlinfo, part_num1);
1675
1676 return true;
1677 } else {
1678 // Partition again
1679 auto nlinfo_split_pair = nlinfo_contig_split(part_num1, nlinfo, partition_diagram, mask_buf);
1680
1681 if (nlinfo_split_pair.second) {
1682 // Region was split successfully, continue to recurse.
1683 difference_type part_num_middle = (part_num1 + part_num2)/2;
1684 return recursive_nlinfo_partition_diagram(part_num1, part_num_middle, nlinfo_split_pair.first.first, partition_diagram, mask_buf) &&
1685 recursive_nlinfo_partition_diagram(part_num_middle+1, part_num2, nlinfo_split_pair.first.second, partition_diagram, mask_buf);
1686 }
1687 }
1688 // Something failed
1689 //std::cout << "INFO - recursive nlinfo partition diagram failed " << std::endl;
1690 return false;
1691 }
1692
1694 ROI2D::difference_type num_partitions,
1695 Array2D<ROI2D::difference_type> &partition_diagram,
1696 Array2D<bool> &mask_buf) {
1697
1698 // Perform divide and conquer algorithm to partition this region.
1699 bool success = recursive_nlinfo_partition_diagram(0,
1700 num_partitions-1,
1701 nlinfo,
1702 partition_diagram,
1703 mask_buf);
1704
1705 if (!success) {
1706 // Fill this nlinfo back to -1 if it was not successfully partitioned
1707 fill(partition_diagram, nlinfo, -1);
1708 }
1709
1710 return partition_diagram;
1711 }
1712
1714 typedef ROI2D::difference_type difference_type;
1715
1716 // If ROI is partitioned successfully, partitioned regions will contain
1717 // numbers from 0 to num_partitions-1. Outside of ROI, partition diagram
1718 // will be -1. If partitioning for a region fails (if too many
1719 // partitions are requested and region is small), that region will
1720 // contain -1s.
1721
1722 // Initialize partition_diagram and buffers
1723 Array2D<difference_type> partition_diagram(roi.height(), roi.width(), -1); // Initialize to -1
1724 Array2D<bool> mask_buf(roi.height(), roi.width());
1725 for (difference_type region_idx = 0; region_idx < roi.size_regions(); ++region_idx) {
1726 if (!roi.get_nlinfo(region_idx).empty()) {
1727 get_nlinfo_partition_diagram(roi.get_nlinfo(region_idx), num_partitions, partition_diagram, mask_buf);
1728 }
1729 }
1730
1731 return partition_diagram;
1732 }
1733
1735 const ROI2D::region_nlinfo &nlinfo,
1736 ROI2D::difference_type num_partitions,
1738 Array2D<bool> &mask_buf1,
1739 Array2D<bool> &mask_buf2) {
1740 typedef ROI2D::difference_type difference_type;
1741
1742 // Cycle over each partition number and get its seed
1743 Array2D<difference_type> seed_buf(num_partitions,2);
1744 for (difference_type partition_idx = 0; partition_idx < num_partitions; ++partition_idx) {
1745 // Form nlinfo corresponding to this partition within input nlinfo
1746 mask_buf1() = false;
1747 fill(mask_buf1, nlinfo, true);
1748 mask_buf1 = (std::move(mask_buf1) & (partition_diagram == partition_idx));
1749 auto nlinfos_pair = ROI2D::region_nlinfo::form_nlinfos(mask_buf1);
1750
1751 // Test to make sure there is only one 4-way contiguous region
1752 if (nlinfos_pair.first.size() == 0) {
1753 // This nlinfo was not partitioned (or is empty), return empty seeds
1754 return Array2D<difference_type>(0,2);
1755 } else if (nlinfos_pair.first.size() != 1) {
1756 // This is a programmer error.
1757 throw std::invalid_argument("While calculating partition seeds, it was determined that one of the partitioned regions was not 4-way contiguous.");
1758 }
1759
1760 // Get SDA corresponding to the partition within this nlinfo
1761 get_nlinfo_SDA(SDA, nlinfos_pair.first.front(), mask_buf1, mask_buf2);
1762
1763 // Find maximum SDA location - use this as the seed point. Note that
1764 // this nlinfo is non-empty so max is guaranteed to exist.
1765 auto max_info = max(SDA, nlinfos_pair.first.front());
1766
1767 // Store max location
1768 seed_buf(partition_idx,0) = max_info.second.first;
1769 seed_buf(partition_idx,1) = max_info.second.second;
1770 }
1771
1772 return seed_buf;
1773 }
1774
1775 std::vector<Array2D<ROI2D::difference_type>> get_ROI_partition_diagram_seeds(const Array2D<ROI2D::difference_type> &partition_diagram, const ROI2D &roi, ROI2D::difference_type num_partitions) {
1776 typedef ROI2D::difference_type difference_type;
1777
1778 // This will obtain seed positions for each region's partitions by
1779 // finding the most interior point of each partition.
1780
1781 // Cycle over regions and get seeds for each partition
1782 std::vector<Array2D<difference_type>> partition_seeds(roi.size_regions());
1783 Array2D<difference_type> SDA(roi.height(), roi.width()); // Make sure to initialize to 0
1784 Array2D<bool> mask_buf1(roi.height(), roi.width());
1785 Array2D<bool> mask_buf2(roi.height(), roi.width());
1786 for (difference_type region_idx = 0; region_idx < roi.size_regions(); ++region_idx) {
1787 if (!roi.get_nlinfo(region_idx).empty()) {
1788 partition_seeds[region_idx] = get_nlinfo_partition_diagram_seeds(partition_diagram, roi.get_nlinfo(region_idx), num_partitions, SDA, mask_buf1, mask_buf2);
1789 }
1790 }
1791
1792 return partition_seeds;
1793 }
1794
1796 const subregion_nloptimizer &sr_nloptimizer,
1797 Array2D<double> &params_buf) {
1798 typedef ROI2D::difference_type difference_type;
1799
1800 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
1801
1802 // Cycle over seeds and return the seed which has the lowest correlation
1803 // coefficient
1804
1805 Array2D<double> seed_params;
1806 for (difference_type seed_idx = 0; seed_idx < seeds_pos.height(); ++seed_idx) {
1807 params_buf(0) = seeds_pos(seed_idx,0);
1808 params_buf(1) = seeds_pos(seed_idx,1);
1809
1810 auto seed_params_pair = sr_nloptimizer.global(params_buf);
1811 if (seed_params_pair.second && (seed_params.empty() || seed_params_pair.first(8) < seed_params(8))) {
1812 // This is either the first seed, or a seed which has a lower
1813 // correlation coefficient - so store it.
1814 seed_params = seed_params_pair.first;
1815 }
1816 }
1817
1818 return seed_params;
1819 }
1820
1821 bool analyze_point(const Array2D<double> &queue_params,
1822 ROI2D::difference_type p1_delta,
1823 ROI2D::difference_type p2_delta,
1824 const ROI2D::region_nlinfo &nlinfo,
1825 ROI2D::difference_type scalefactor,
1826 const subregion_nloptimizer &sr_nloptimizer,
1827 double cutoff_corrcoef,
1828 double cutoff_delta_disp,
1829 std::priority_queue<Array2D<double>, std::vector<Array2D<double>>, std::function<bool(const Array2D<double>&,const Array2D<double>&)>> &queue,
1830 Array2D<bool> &A_ap,
1831 Array2D<double> &params_buf) {
1832 typedef ROI2D::difference_type difference_type;
1833
1834 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
1835
1836 // Make sure points are in bounds of roi_reduced's nlinfo and havent
1837 // been analyzed yet.
1838 difference_type p1 = queue_params(0) + p1_delta;
1839 difference_type p2 = queue_params(1) + p2_delta;
1840 if (nlinfo.in_nlinfo(p1 / scalefactor, p2 / scalefactor) &&
1841 A_ap(p1 / scalefactor, p2 / scalefactor)) {
1842 A_ap(p1 / scalefactor, p2 / scalefactor) = false; // Inactivate
1843
1844 // Fill in params - get initial guess for displacements using
1845 // gradients; just reuse gradients as their initial guess.
1846 params_buf(0) = p1;
1847 params_buf(1) = p2;
1848 params_buf(2) = queue_params(2) + p1_delta * queue_params(4) + p2_delta * queue_params(5);
1849 params_buf(3) = queue_params(3) + p1_delta * queue_params(6) + p2_delta * queue_params(7);
1850 params_buf(4) = queue_params(4);
1851 params_buf(5) = queue_params(5);
1852 params_buf(6) = queue_params(6);
1853 params_buf(7) = queue_params(7);
1854
1855 // Get optimized params from nonlinear optimizer
1856 auto params_pair = sr_nloptimizer(params_buf);
1857 if (params_pair.second) {
1858 // Insert into queue if it meets the criterion
1859 if (std::sqrt(std::pow(params_pair.first(2) - params_buf(2),2) + std::pow(params_pair.first(3) - params_buf(3),2)) < cutoff_delta_disp &&
1860 params_pair.first(8) < cutoff_corrcoef) {
1861 queue.push(params_pair.first);
1862
1863 return true;
1864 }
1865 }
1866 // Do not attempt global optimization if the guess fails since it
1867 // is slow - just skip this point.
1868 }
1869 // Something failed
1870 //std::cout << "INFO - sr_analyze_point failed"<< std::endl;
1871 return false;
1872 }
1873
1874 void worker_RGDIC(subregion_nloptimizer sr_nloptimizer, // passed by value
1875 ROI2D roi_reduced, // passed by value
1876 ROI2D::difference_type scalefactor,
1877 double cutoff_corrcoef,
1878 double cutoff_delta_disp,
1879 Array2D<double> &A_v,
1880 Array2D<double> &A_u,
1881 Array2D<double> &A_cc,
1882 Array2D<bool> &A_ap,
1883 Array2D<bool> &A_vp) {
1884 typedef ROI2D::difference_type difference_type;
1885
1886 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
1887
1888 // For robustness, multiple seeds are placed within this region and the
1889 // seed with the highest correlation coefficient is used in the
1890 // analysis. The higher the number of seeds, the more robust (but slower)
1891 // the analysis will be.
1892 difference_type num_redundant_seeds = 4;
1893 auto roi_partition_diagram = get_ROI_partition_diagram(roi_reduced, num_redundant_seeds);
1894 auto redundant_seeds = get_ROI_partition_diagram_seeds(roi_partition_diagram, roi_reduced, num_redundant_seeds);
1895 for (auto &region_seeds: redundant_seeds) {
1896 // Must scale seed position
1897 region_seeds = std::move(region_seeds) * scalefactor;
1898 }
1899
1900 // Cycle over regions and perform RGDIC.
1901 Array2D<double> params_buf(10, 1); // buffer for nloptimizer
1902 for (difference_type region_idx = 0; region_idx < roi_reduced.size_regions(); ++region_idx) {
1903 if (redundant_seeds[region_idx].empty()) {
1904 // This region couldn't be seeded - most likely because it was
1905 // too small.
1906 continue;
1907 }
1908
1909 // Get seed params for queue
1910 auto seed_params = get_seed_params(redundant_seeds[region_idx],
1911 sr_nloptimizer,
1912 params_buf);
1913 if (!seed_params.empty()) {
1914 A_ap(seed_params(0) / scalefactor, seed_params(1) / scalefactor) = false;
1915
1916 // Form queue - make it a priority queue such that the lowest
1917 // correlation coefficient values are processed first.
1918 auto comp = [](const Array2D<double> &a, const Array2D<double> &b ) { return a(8) > b(8); };
1919 std::priority_queue<Array2D<double>, std::vector<Array2D<double>>, std::function<bool(const Array2D<double>&,const Array2D<double>&)>> queue(comp);
1920
1921 // Perform flood fill around seed
1922 queue.push(seed_params);
1923 while (!queue.empty()) {
1924 // 1) pop info from queue
1925 auto queue_params = std::move(queue.top()); queue.pop();
1926
1927 // 2) Validate and store information
1928 A_vp(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = true;
1929 A_v(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(2);
1930 A_u(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(3);
1931 A_cc(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(8);
1932
1933 // 3) Analyze four surrounding points - up, down, left right;
1934 analyze_point(queue_params, -scalefactor, 0, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
1935 analyze_point(queue_params, scalefactor, 0, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
1936 analyze_point(queue_params, 0, -scalefactor, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
1937 analyze_point(queue_params, 0, scalefactor, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
1938 }
1939 }
1940 }
1941
1942 // Clear all active points for this ROI as a way of indicating this
1943 // thread has completed its work.
1944 A_ap(roi_reduced.get_mask()) = false;
1945 }
1946}
1947
1949 const Array2D<double> &A_cur,
1950 const ROI2D &roi,
1951 ROI2D::difference_type scalefactor,
1952 INTERP interp_type,
1953 SUBREGION subregion_type,
1955 ROI2D::difference_type num_threads,
1956 double cutoff_corrcoef,
1957 bool debug) {
1958 ncorr::log::set_debug(debug);
1959 typedef ROI2D::difference_type difference_type;
1960
1961 NLOG_DEBUG << "DEBUG: RGDIC called with num_threads=" << num_threads;
1962
1963 if (!A_ref.same_size(A_cur)) {
1964 throw std::invalid_argument("Attempted to perform RGDIC on reference image input of size: " + A_ref.size_2D_string() +
1965 " with current image input of size: " + A_cur.size_2D_string() + ". Sizes must be the same.");
1966 }
1967
1968 if (!A_ref.same_size(roi.get_mask())) {
1969 throw std::invalid_argument("Attempted to perform RGDIC on reference image input of size: " + A_ref.size_2D_string() +
1970 " with ROI input of size: " + roi.size_2D_string() + ". Sizes must be the same.");
1971 }
1972
1973 if (scalefactor < 1) {
1974 throw std::invalid_argument("Attempted to perform RGDIC with scalefactor: " + std::to_string(scalefactor) +
1975 ". scalefactor must be 1 or greater.");
1976 }
1977
1978 if (interp_type < INTERP::CUBIC_KEYS) {
1979 throw std::invalid_argument("Interpolation used for RGDIC must be cubic or greater.");
1980 }
1981
1982 if (r < 5) {
1983 throw std::invalid_argument("Attempted to perform RGDIC with radius: " + std::to_string(r) +
1984 ". radius must be 5 or greater.");
1985 }
1986
1987 if (num_threads < 1) {
1988 throw std::invalid_argument("Attempted to perform RGDIC with number of threads: " + std::to_string(num_threads) +
1989 ". Number of threads must be 1 or greater.");
1990 }
1991
1992 if (cutoff_corrcoef < 0 || cutoff_corrcoef > 4.0) {
1993 throw std::invalid_argument("Input correlation coefficient cutoff of: " + std::to_string(cutoff_corrcoef) +
1994 " is not between [0,4].");
1995 }
1996
1997 // cutoff_delta_disp is a cutoff for the allowable change in displacement
1998 // between analyzed points. Generally if a point is analyzed incorrectly,
1999 // it will result in a large change in displacement, so just filter these
2000 // points out.
2001 double cutoff_delta_disp = scalefactor;
2002
2003 // Reduce ROI
2004 auto roi_reduced = roi.reduce(scalefactor);
2005
2006 // Get partition diagram of reduced ROI
2007 auto partition_diagram = details::get_ROI_partition_diagram(roi_reduced, num_threads);
2008
2009 // Get subregion nonlinear optimizer
2010 auto sr_nloptimizer = details::subregion_nloptimizer(A_ref, A_cur, roi, scalefactor, interp_type, subregion_type, r);
2011
2012 // Initialize displacement and correlation coefficient arrays
2013 Array2D<double> A_v(roi_reduced.height(), roi_reduced.width());
2014 Array2D<double> A_u(roi_reduced.height(), roi_reduced.width());
2015 Array2D<double> A_cc(roi_reduced.height(), roi_reduced.width());
2016
2017 // -----------------------------------------------------------------------//
2018 // Perform reliability guided DIC ----------------------------------------//
2019 // -----------------------------------------------------------------------//
2020 // Initialize buffers - note that only regions that were partitioned by
2021 // partition diagram can be analyzed, so set A_ap to all points which are
2022 // in the partition diagram.
2023 Array2D<bool> A_ap(partition_diagram != -1); // Active Points
2024 Array2D<bool> A_vp(roi_reduced.height(), roi_reduced.width()); // Valid Points
2025
2026 // Form threads
2027 std::vector<std::thread> threads(num_threads);
2028 for (difference_type thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
2029 threads[thread_idx] = std::thread(details::worker_RGDIC,
2030 sr_nloptimizer, // Send in copy of subregion nonlinear optimizer
2031 roi_reduced.form_union(partition_diagram == thread_idx), // Dont use std::ref since r-value
2032 scalefactor,
2033 cutoff_corrcoef,
2034 cutoff_delta_disp,
2035 std::ref(A_v),
2036 std::ref(A_u),
2037 std::ref(A_cc),
2038 std::ref(A_ap),
2039 std::ref(A_vp));
2040 }
2041
2042 // Debugging stuff -------------------------------------------------------//
2043 if (false && debug) {
2044 // These debugging tools are displayed during the RGDIC calculation and
2045 // give real-time updates to help assist with analysis.
2046
2047 // Display preview of a subset - display 1 per region
2048 auto subregion_gen = roi.get_contig_subregion_generator(subregion_type, r);
2049 auto subset_seeds = details::get_ROI_partition_diagram_seeds(details::get_ROI_partition_diagram(roi_reduced, 1), roi_reduced, 1);
2050 Array2D<double> subset(2*r+1,2*r+1);
2051 for (const auto &subset_seed : subset_seeds) {
2052 if (!subset_seed.empty()) {
2053 // Clear subset
2054 subset() = 0;
2055 // Get subregion and the fill
2056 const auto &subregion_nlinfo = subregion_gen(subset_seed(0) * scalefactor, subset_seed(1) * scalefactor);
2057 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
2058 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
2059 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
2060 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
2061 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
2062 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
2063 difference_type p1_shifted = p1 - (subset_seed(0) * scalefactor) + subregion_gen.get_r();
2064 difference_type p2_shifted = p2 - (subset_seed(1) * scalefactor) + subregion_gen.get_r();
2065 // Set subset value
2066 subset(p1_shifted, p2_shifted) = A_ref(p1,p2);
2067 }
2068 }
2069 }
2070 }
2071 // Display subset
2072 imshow(subset, 2000);
2073 }
2074
2075 // Display partition diagram
2076 imshow(partition_diagram,2000);
2077
2078 // Show v-plot getting updated - this acts as a waitbar. Typically
2079 // deformation happens in the y-axis so this is a good plot to show
2080 // for updates. Note that threads clear out their portion of A_ap when
2081 // they are finished, so this condition will work correctly.
2082 while (any_true(A_ap)) {
2083 imshow(A_v,50);
2084 }
2085 imshow(A_v,50);
2086 }
2087 // -----------------------------------------------------------------------//
2088
2089 // Join threads
2090 for (difference_type thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
2091 threads[thread_idx].join();
2092 }
2093 // -----------------------------------------------------------------------//
2094 // -----------------------------------------------------------------------//
2095 // -----------------------------------------------------------------------//
2096
2097 // Go back over plot and clear any datapoints that have large displacement
2098 // jumps. Not all are cleared through the RGDIC algorithm because of the
2099 // way the search is conducted.
2100 auto A_vp_buf = A_vp; // Make copy of valid points since it gets overwritten
2101 for (difference_type region_idx = 0; region_idx < roi_reduced.size_regions(); ++region_idx) {
2102 for (difference_type nl_idx = 0; nl_idx < roi_reduced.get_nlinfo(region_idx).nodelist.width(); ++nl_idx) {
2103 difference_type p2 = nl_idx + roi_reduced.get_nlinfo(region_idx).left_nl;
2104 for (difference_type np_idx = 0; np_idx < roi_reduced.get_nlinfo(region_idx).noderange(nl_idx); np_idx += 2) {
2105 difference_type np_top = roi_reduced.get_nlinfo(region_idx).nodelist(np_idx,nl_idx);
2106 difference_type np_bottom = roi_reduced.get_nlinfo(region_idx).nodelist(np_idx + 1,nl_idx);
2107 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
2108 if ((roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1-1,p2) && A_vp_buf(p1-1,p2) && std::sqrt(std::pow(A_v(p1-1,p2) - A_v(p1,p2),2) + std::pow(A_u(p1-1,p2) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
2109 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1+1,p2) && A_vp_buf(p1+1,p2) && std::sqrt(std::pow(A_v(p1+1,p2) - A_v(p1,p2),2) + std::pow(A_u(p1+1,p2) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
2110 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1,p2-1) && A_vp_buf(p1,p2-1) && std::sqrt(std::pow(A_v(p1,p2-1) - A_v(p1,p2),2) + std::pow(A_u(p1,p2-1) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
2111 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1,p2+1) && A_vp_buf(p1,p2+1) && std::sqrt(std::pow(A_v(p1,p2+1) - A_v(p1,p2),2) + std::pow(A_u(p1,p2+1) - A_u(p1,p2),2)) > cutoff_delta_disp)) {
2112 A_vp(p1,p2) = false;
2113 }
2114 }
2115 }
2116 }
2117 }
2118
2119 // Must form union with valid points
2120 auto roi_valid = roi_reduced.form_union(A_vp);
2121
2122 return Disp2D(std::move(A_v), std::move(A_u), std::move(A_cc), roi_valid, scalefactor);
2123}
2124
2126 const Array2D<double> &A_cur,
2127 const ROI2D &roi,
2128 ROI2D::difference_type scalefactor,
2129 INTERP interp_type,
2130 SUBREGION subregion_type,
2132 double cutoff_corrcoef,
2133 bool debug,
2134 const std::vector<SeedParams>& seeds_by_region = {},
2135 bool seeds_are_optimized = false) {
2136 ncorr::log::set_debug(debug);
2137 typedef ROI2D::difference_type difference_type;
2138
2139 if (!A_ref.same_size(A_cur)) {
2140 throw std::invalid_argument("Attempted to perform RGDIC on reference image input of size: " + A_ref.size_2D_string() +
2141 " with current image input of size: " + A_cur.size_2D_string() + ". Sizes must be the same.");
2142 }
2143
2144 if (!A_ref.same_size(roi.get_mask())) {
2145 throw std::invalid_argument("Attempted to perform RGDIC on reference image input of size: " + A_ref.size_2D_string() +
2146 " with ROI input of size: " + roi.size_2D_string() + ". Sizes must be the same.");
2147 }
2148
2149 if (scalefactor < 1) {
2150 throw std::invalid_argument("Attempted to perform RGDIC with scalefactor: " + std::to_string(scalefactor) +
2151 ". scalefactor must be 1 or greater.");
2152 }
2153
2154 if (interp_type < INTERP::CUBIC_KEYS) {
2155 throw std::invalid_argument("Interpolation used for RGDIC must be cubic or greater.");
2156 }
2157
2158 if (r < 5) {
2159 throw std::invalid_argument("Attempted to perform RGDIC with radius: " + std::to_string(r) +
2160 ". radius must be 5 or greater.");
2161 }
2162
2163 if (cutoff_corrcoef < 0 || cutoff_corrcoef > 4.0) {
2164 throw std::invalid_argument("Input correlation coefficient cutoff of: " + std::to_string(cutoff_corrcoef) +
2165 " is not between [0,4].");
2166 }
2167
2168 // cutoff_delta_disp is a cutoff for the allowable change in displacement
2169 // between analyzed points. Generally if a point is analyzed incorrectly,
2170 // it will result in a large change in displacement, so just filter these
2171 // points out.
2172 double cutoff_delta_disp = scalefactor;
2173
2174 // Reduce ROI
2175 auto roi_reduced = roi.reduce(scalefactor);
2176
2177 // Get subregion nonlinear optimizer
2178 auto sr_nloptimizer = details::subregion_nloptimizer(A_ref, A_cur, roi, scalefactor, interp_type, subregion_type, r);
2179
2180 // Initialize displacement and correlation coefficient arrays
2181 Array2D<double> A_v(roi_reduced.height(), roi_reduced.width());
2182 Array2D<double> A_u(roi_reduced.height(), roi_reduced.width());
2183 Array2D<double> A_cc(roi_reduced.height(), roi_reduced.width());
2184
2185 // -----------------------------------------------------------------------//
2186 // Perform reliability guided DIC without threading ----------------------//
2187 // -----------------------------------------------------------------------//
2188 // Initialize buffers - set A_ap to all points in ROI
2189 Array2D<bool> A_ap(roi_reduced.get_mask()); // Active Points
2190 Array2D<bool> A_vp(roi_reduced.height(), roi_reduced.width()); // Valid Points
2191
2192 // Process all regions sequentially (without threading)
2193 // Note: params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
2194
2195 // Check if user provided seeds
2196 bool use_provided_seeds = !seeds_by_region.empty();
2197
2198 // Storage for auto-generated redundant seeds (only used if seeds not provided)
2199 std::vector<Array2D<difference_type>> redundant_seeds;
2200
2201 if (!use_provided_seeds) {
2202 // For robustness, multiple seeds are placed within each region and the
2203 // seed with the highest correlation coefficient is used in the
2204 // analysis. The higher the number of seeds, the more robust (but slower)
2205 // the analysis will be.
2206 difference_type num_redundant_seeds = 4;
2207 auto roi_partition_diagram = details::get_ROI_partition_diagram(roi_reduced, num_redundant_seeds);
2208 redundant_seeds = details::get_ROI_partition_diagram_seeds(roi_partition_diagram, roi_reduced, num_redundant_seeds);
2209
2210 for (auto &region_seeds: redundant_seeds) {
2211 // Must scale seed position
2212 region_seeds = std::move(region_seeds) * scalefactor;
2213 }
2214 }
2215
2216 // Cycle over regions and perform RGDIC sequentially
2217 Array2D<double> params_buf(10, 1); // buffer for nloptimizer
2218 for (difference_type region_idx = 0; region_idx < roi_reduced.size_regions(); ++region_idx) {
2219 Array2D<double> seed_params;
2220
2221 if (use_provided_seeds) {
2222 NLOG_WARN << "WARNING - Using provided seed for region " << region_idx;
2223 // Use user-provided seed for this region
2224 if (region_idx < static_cast<difference_type>(seeds_by_region.size())) {
2225 const SeedParams& provided_seed = seeds_by_region[region_idx];
2226 NLOG_DEBUG << "DEBUG::Provided seed: " << provided_seed.y << " " << provided_seed.x;
2227
2228 if (seeds_are_optimized) {
2229 // Seeds are already optimized - use directly without optimization
2230 NLOG_DEBUG << "DEBUG::Already Optimized seed: " << provided_seed.y << " " << provided_seed.x;
2231 seed_params = provided_seed.to_array();
2232 } else {
2233 // Seeds need optimization - create seed array and optimize
2234 Array2D<double> single_seed(2, 1);
2235 single_seed(0) = static_cast<double>(provided_seed.y); // p1
2236 single_seed(1) = static_cast<double>(provided_seed.x); // p2
2237
2238 // Initialize params_buf with seed position and provided initial guess
2239 params_buf(0) = single_seed(0);
2240 params_buf(1) = single_seed(1);
2241 params_buf(2) = provided_seed.v;
2242 params_buf(3) = provided_seed.u;
2243 params_buf(4) = provided_seed.dv_dy;
2244 params_buf(5) = provided_seed.dv_dx;
2245 params_buf(6) = provided_seed.du_dy;
2246 params_buf(7) = provided_seed.du_dx;
2247 params_buf(8) = 0.0; // corrcoef (will be computed)
2248 params_buf(9) = 0.0; // diff_norm (will be computed)
2249
2250 NLOG_DEBUG << "Params_buf = " << params_buf;
2251
2252 // Optimize the seed using nloptimizer
2253 auto result = sr_nloptimizer(params_buf);
2254 if (result.second) { // converged successfully
2255 seed_params = result.first;
2256 if (debug) {
2257 NLOG_DEBUG << "DEBUG::Optimized seed: " << seed_params(0) << " " << seed_params(1);
2258 }
2259
2260 } else {
2261 NLOG_WARN << "Warning - Seed optimization doesn't converge. Use non optimized seed";
2262 }
2263 // If optimization failed, seed_params remains empty
2264 }
2265 }
2266 } else {
2267 // Use auto-generated redundant seeds
2268 if (redundant_seeds[region_idx].empty()) {
2269 // This region couldn't be seeded - most likely because it was
2270 // too small.
2271 continue;
2272 }
2273
2274 // Get seed params for queue (optimizes and selects best seed)
2275 seed_params = details::get_seed_params(redundant_seeds[region_idx],
2276 sr_nloptimizer,
2277 params_buf);
2278 }
2279
2280 if (!seed_params.empty()) {
2281 A_ap(seed_params(0) / scalefactor, seed_params(1) / scalefactor) = false;
2282
2283 // Form queue - make it a priority queue such that the lowest
2284 // correlation coefficient values are processed first.
2285 auto comp = [](const Array2D<double> &a, const Array2D<double> &b ) { return a(8) > b(8); };
2286 std::priority_queue<Array2D<double>, std::vector<Array2D<double>>, std::function<bool(const Array2D<double>&,const Array2D<double>&)>> queue(comp);
2287
2288 // Perform flood fill around seed
2289 queue.push(seed_params);
2290 while (!queue.empty()) {
2291 // 1) pop info from queue
2292 auto queue_params = std::move(queue.top()); queue.pop();
2293
2294 // 2) Validate and store information
2295 A_vp(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = true;
2296 A_v(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(2);
2297 A_u(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(3);
2298 A_cc(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(8);
2299
2300 // 3) Analyze four surrounding points - up, down, left right;
2301 details::analyze_point(queue_params, -scalefactor, 0, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
2302 details::analyze_point(queue_params, scalefactor, 0, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
2303 details::analyze_point(queue_params, 0, -scalefactor, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
2304 details::analyze_point(queue_params, 0, scalefactor, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
2305 }
2306 }
2307 }
2308
2309 // Debugging stuff -------------------------------------------------------//
2310 if (false && debug) {
2311 // These debugging tools are displayed during the RGDIC calculation and
2312 // give real-time updates to help assist with analysis.
2313
2314 // Display preview of a subset - display 1 per region
2315 auto subregion_gen = roi.get_contig_subregion_generator(subregion_type, r);
2316 auto subset_seeds = details::get_ROI_partition_diagram_seeds(details::get_ROI_partition_diagram(roi_reduced, 1), roi_reduced, 1);
2317 Array2D<double> subset(2*r+1,2*r+1);
2318 for (const auto &subset_seed : subset_seeds) {
2319 if (!subset_seed.empty()) {
2320 // Clear subset
2321 subset() = 0;
2322 // Get subregion and the fill
2323 const auto &subregion_nlinfo = subregion_gen(subset_seed(0) * scalefactor, subset_seed(1) * scalefactor);
2324 for (difference_type nl_idx = 0; nl_idx < subregion_nlinfo.nodelist.width(); ++nl_idx) {
2325 difference_type p2 = nl_idx + subregion_nlinfo.left_nl;
2326 for (difference_type np_idx = 0; np_idx < subregion_nlinfo.noderange(nl_idx); np_idx += 2) {
2327 difference_type np_top = subregion_nlinfo.nodelist(np_idx,nl_idx);
2328 difference_type np_bottom = subregion_nlinfo.nodelist(np_idx + 1,nl_idx);
2329 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
2330 difference_type p1_shifted = p1 - (subset_seed(0) * scalefactor) + subregion_gen.get_r();
2331 difference_type p2_shifted = p2 - (subset_seed(1) * scalefactor) + subregion_gen.get_r();
2332 // Set subset value
2333 subset(p1_shifted, p2_shifted) = A_ref(p1,p2);
2334 }
2335 }
2336 }
2337 }
2338 // Display subset
2339 imshow(subset, 2000);
2340 }
2341
2342 // Display v-plot - show final result
2343 imshow(A_v,50);
2344 }
2345 // -----------------------------------------------------------------------//
2346
2347 // Go back over plot and clear any datapoints that have large displacement
2348 // jumps. Not all are cleared through the RGDIC algorithm because of the
2349 // way the search is conducted.
2350 auto A_vp_buf = A_vp; // Make copy of valid points since it gets overwritten
2351 for (difference_type region_idx = 0; region_idx < roi_reduced.size_regions(); ++region_idx) {
2352 for (difference_type nl_idx = 0; nl_idx < roi_reduced.get_nlinfo(region_idx).nodelist.width(); ++nl_idx) {
2353 difference_type p2 = nl_idx + roi_reduced.get_nlinfo(region_idx).left_nl;
2354 for (difference_type np_idx = 0; np_idx < roi_reduced.get_nlinfo(region_idx).noderange(nl_idx); np_idx += 2) {
2355 difference_type np_top = roi_reduced.get_nlinfo(region_idx).nodelist(np_idx,nl_idx);
2356 difference_type np_bottom = roi_reduced.get_nlinfo(region_idx).nodelist(np_idx + 1,nl_idx);
2357 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
2358 if ((roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1-1,p2) && A_vp_buf(p1-1,p2) && std::sqrt(std::pow(A_v(p1-1,p2) - A_v(p1,p2),2) + std::pow(A_u(p1-1,p2) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
2359 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1+1,p2) && A_vp_buf(p1+1,p2) && std::sqrt(std::pow(A_v(p1+1,p2) - A_v(p1,p2),2) + std::pow(A_u(p1+1,p2) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
2360 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1,p2-1) && A_vp_buf(p1,p2-1) && std::sqrt(std::pow(A_v(p1,p2-1) - A_v(p1,p2),2) + std::pow(A_u(p1,p2-1) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
2361 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1,p2+1) && A_vp_buf(p1,p2+1) && std::sqrt(std::pow(A_v(p1,p2+1) - A_v(p1,p2),2) + std::pow(A_u(p1,p2+1) - A_u(p1,p2),2)) > cutoff_delta_disp)) {
2362 A_vp(p1,p2) = false;
2363 }
2364 }
2365 }
2366 }
2367 }
2368
2369 // Must form union with valid points
2370 auto roi_valid = roi_reduced.form_union(A_vp);
2371 return Disp2D(std::move(A_v), std::move(A_u), std::move(A_cc), roi_valid, scalefactor);
2372}
2373
2374DIC_analysis_input::DIC_analysis_input(const std::vector<Image2D> &imgs,
2375 const ROI2D &roi,
2376 ROI2D::difference_type scalefactor,
2377 INTERP interp_type,
2378 SUBREGION subregion_type,
2380 ROI2D::difference_type num_threads,
2381 DIC_analysis_config config_type,
2382 bool debug,
2383 ROI_UPDATE_MODE roi_update_mode_override,
2384 ACCUMULATION_MODE accumulation_mode_override,
2385 bool save_disps_steps_override) : imgs(imgs),
2386 roi(roi),
2387 scalefactor(scalefactor),
2388 interp_type(interp_type),
2389 subregion_type(subregion_type),
2390 r(r),
2391 num_threads(num_threads),
2392 debug(debug) {
2393 // Set parameters to some preset configuration
2394 switch (config_type) {
2396 this->cutoff_corrcoef = 2.0;
2397 this->update_corrcoef = 4.0; // Don't ever update
2398 this->prctile_corrcoef = 1.0;
2401 break;
2403 this->cutoff_corrcoef = 2.0;
2404 this->update_corrcoef = 0.5;
2405 this->prctile_corrcoef = 1.0;
2408 break;
2410 this->cutoff_corrcoef = 0.7;
2411 this->update_corrcoef = 0.35;
2412 this->prctile_corrcoef = 0.9;
2415 break;
2416 }
2417
2418 // Apply manual overrides if provided (use -1 as sentinel for "not specified")
2419 if (roi_update_mode_override != static_cast<ROI_UPDATE_MODE>(-1)) {
2420 this->roi_update_mode = roi_update_mode_override;
2421 NLOG_INFO << "Manual override: roi_update_mode = " << (roi_update_mode_override == ROI_UPDATE_MODE::SKIP_ALL ? "SKIP_ALL" : "SKIP_INVALID");
2422 }
2423 if (accumulation_mode_override != static_cast<ACCUMULATION_MODE>(-1)) {
2424 this->accumulation_mode = accumulation_mode_override;
2425 NLOG_INFO << "Manual override: accumulation_mode = " << (accumulation_mode_override == ACCUMULATION_MODE::ON_THE_FLY ? "ON_THE_FLY" : "POST_PROCESS");
2426 }
2427
2428 // Set save_disps_steps (enforced if debug is true)
2429 this->save_disps_steps = save_disps_steps_override || debug;
2430 if (this->save_disps_steps) {
2431 NLOG_INFO << "Step displacement data will be saved (save_disps_steps=true)";
2432 }
2433}
2434
2436 // Form empty DIC_analysis_input then fill in values in accordance to how they are saved
2437 DIC_analysis_input DIC_input;
2438
2439 // Load Images
2440 difference_type num_images = 0;
2441 is.read(reinterpret_cast<char*>(&num_images), std::streamsize(sizeof(difference_type)));
2442 DIC_input.imgs.resize(num_images);
2443 for (auto &img : DIC_input.imgs) {
2444 img = Image2D::load(is);
2445 }
2446
2447 // Load roi
2448 DIC_input.roi = ROI2D::load(is);
2449
2450 // Load scalefactor
2451 is.read(reinterpret_cast<char*>(&DIC_input.scalefactor), std::streamsize(sizeof(difference_type)));
2452
2453 // Load interp_type
2454 is.read(reinterpret_cast<char*>(&DIC_input.interp_type), std::streamsize(sizeof(INTERP)));
2455
2456 // Load subregion_type
2457 is.read(reinterpret_cast<char*>(&DIC_input.subregion_type), std::streamsize(sizeof(SUBREGION)));
2458
2459 // Load radius
2460 is.read(reinterpret_cast<char*>(&DIC_input.r), std::streamsize(sizeof(difference_type)));
2461
2462 // Load number of threads
2463 is.read(reinterpret_cast<char*>(&DIC_input.num_threads), std::streamsize(sizeof(difference_type)));
2464
2465 // Load cutoff_corrcoef
2466 is.read(reinterpret_cast<char*>(&DIC_input.cutoff_corrcoef), std::streamsize(sizeof(double)));
2467
2468 // Load update_corrcoef
2469 is.read(reinterpret_cast<char*>(&DIC_input.update_corrcoef), std::streamsize(sizeof(double)));
2470
2471 // Load prctile_corrcoef
2472 is.read(reinterpret_cast<char*>(&DIC_input.prctile_corrcoef), std::streamsize(sizeof(double)));
2473
2474 // Load roi_update_mode
2475 is.read(reinterpret_cast<char*>(&DIC_input.roi_update_mode), std::streamsize(sizeof(ROI_UPDATE_MODE)));
2476
2477 // Load accumulation_mode
2478 is.read(reinterpret_cast<char*>(&DIC_input.accumulation_mode), std::streamsize(sizeof(ACCUMULATION_MODE)));
2479
2480 // Load save_disps_steps
2481 is.read(reinterpret_cast<char*>(&DIC_input.save_disps_steps), std::streamsize(sizeof(bool)));
2482
2483 // Load debug
2484 is.read(reinterpret_cast<char*>(&DIC_input.debug), std::streamsize(sizeof(bool)));
2485
2486 return DIC_input;
2487}
2488
2489DIC_analysis_input DIC_analysis_input::load(const std::string &filename) {
2490 // Form stream
2491 std::ifstream is(filename.c_str(), std::ios::in | std::ios::binary);
2492 if (!is.is_open()) {
2493 throw std::invalid_argument("Could not open " + filename + " for loading DIC_analysis_input.");
2494 }
2495
2496 // Form DIC_analysis_input using stream static factory method
2497 auto DIC_input = DIC_analysis_input::load(is);
2498
2499 // Close stream
2500 is.close();
2501
2502 return DIC_input;
2503}
2504
2505void save(const DIC_analysis_input &DIC_input, std::ofstream &os) {
2507
2508 // Save imgs -> roi -> scalefactor -> interp_type -> subregion_type ->
2509 // radius -> num_threads -> cutoff_corrcoef -> update_corrcoef ->
2510 // prctile_corrcoef -> debug
2511
2512 difference_type num_imgs = DIC_input.imgs.size();
2513 os.write(reinterpret_cast<const char*>(&num_imgs), std::streamsize(sizeof(difference_type)));
2514 for (const auto &img : DIC_input.imgs) {
2515 save(img, os);
2516 }
2517
2518 save(DIC_input.roi, os);
2519
2520 os.write(reinterpret_cast<const char*>(&DIC_input.scalefactor), std::streamsize(sizeof(difference_type)));
2521
2522 os.write(reinterpret_cast<const char*>(&DIC_input.interp_type), std::streamsize(sizeof(INTERP)));
2523
2524 os.write(reinterpret_cast<const char*>(&DIC_input.subregion_type), std::streamsize(sizeof(SUBREGION)));
2525
2526 os.write(reinterpret_cast<const char*>(&DIC_input.r), std::streamsize(sizeof(difference_type)));
2527
2528 os.write(reinterpret_cast<const char*>(&DIC_input.num_threads), std::streamsize(sizeof(difference_type)));
2529
2530 os.write(reinterpret_cast<const char*>(&DIC_input.cutoff_corrcoef), std::streamsize(sizeof(double)));
2531
2532 os.write(reinterpret_cast<const char*>(&DIC_input.update_corrcoef), std::streamsize(sizeof(double)));
2533
2534 os.write(reinterpret_cast<const char*>(&DIC_input.prctile_corrcoef), std::streamsize(sizeof(double)));
2535
2536 os.write(reinterpret_cast<const char*>(&DIC_input.roi_update_mode), std::streamsize(sizeof(ROI_UPDATE_MODE)));
2537
2538 os.write(reinterpret_cast<const char*>(&DIC_input.accumulation_mode), std::streamsize(sizeof(ACCUMULATION_MODE)));
2539
2540 os.write(reinterpret_cast<const char*>(&DIC_input.save_disps_steps), std::streamsize(sizeof(bool)));
2541
2542 os.write(reinterpret_cast<const char*>(&DIC_input.debug), std::streamsize(sizeof(bool)));
2543}
2544
2545void save(const DIC_analysis_input &DIC_input, const std::string &filename) {
2546 // Form stream
2547 std::ofstream os(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
2548 if (!os.is_open()) {
2549 throw std::invalid_argument("Could not open " + filename + " for saving DIC_analysis_input.");
2550 }
2551
2552 // Save DIC_input into stream
2553 save(DIC_input, os);
2554
2555 // Close stream
2556 os.close();
2557}
2558
2560 // Form empty DIC_analysis_output then fill in values in accordance to how they are saved
2561 DIC_analysis_output DIC_output;
2562
2563 // Load disps
2564 difference_type num_disps = 0;
2565 is.read(reinterpret_cast<char*>(&num_disps), std::streamsize(sizeof(difference_type)));
2566 DIC_output.disps.resize(num_disps);
2567 for (auto &disp : DIC_output.disps) {
2568 disp = Disp2D::load(is);
2569 }
2570
2571 // Load perspective type
2572 is.read(reinterpret_cast<char*>(&DIC_output.perspective_type), std::streamsize(sizeof(PERSPECTIVE)));
2573
2574 // Load units
2575 difference_type length = 0;
2576 is.read(reinterpret_cast<char*>(&length), std::streamsize(sizeof(difference_type)));
2577 DIC_output.units = std::string(length,' ');
2578 is.read(const_cast<char*>(DIC_output.units.c_str()), std::streamsize(length));
2579
2580 // Load units per pixel
2581 is.read(reinterpret_cast<char*>(&DIC_output.units_per_pixel), std::streamsize(sizeof(double)));
2582
2583 return DIC_output;
2584}
2585
2587 // Form stream
2588 std::ifstream is(filename.c_str(), std::ios::in | std::ios::binary);
2589 if (!is.is_open()) {
2590 throw std::invalid_argument("Could not open " + filename + " for loading DIC_analysis_output.");
2591 }
2592
2593 // Form DIC_analysis_output using stream static factory method
2594 auto DIC_output = DIC_analysis_output::load(is);
2595
2596 // Close stream
2597 is.close();
2598
2599 return DIC_output;
2600}
2601
2602void save(const DIC_analysis_output &DIC_output, std::ofstream &os) {
2604
2605 // Save disps
2606 difference_type num_disps = DIC_output.disps.size();
2607 os.write(reinterpret_cast<const char*>(&num_disps), std::streamsize(sizeof(difference_type)));
2608 for (const auto &disp : DIC_output.disps) {
2609 save(disp, os);
2610 }
2611
2612 // Save perspective type
2613 os.write(reinterpret_cast<const char*>(&DIC_output.perspective_type), std::streamsize(sizeof(PERSPECTIVE)));
2614
2615 // Save units
2616 difference_type length = DIC_output.units.size();
2617 os.write(reinterpret_cast<const char*>(&length), std::streamsize(sizeof(difference_type)));
2618 os.write(DIC_output.units.c_str(), std::streamsize(length));
2619
2620 // Save units per pixel
2621 os.write(reinterpret_cast<const char*>(&DIC_output.units_per_pixel), std::streamsize(sizeof(double)));
2622}
2623
2624void save(const DIC_analysis_output &DIC_output, const std::string &filename) {
2625 // Form stream
2626 std::ofstream os(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
2627 if (!os.is_open()) {
2628 throw std::invalid_argument("Could not open " + filename + " for saving DIC_analysis_output.");
2629 }
2630
2631 // Save DIC_output into stream
2632 save(DIC_output, os);
2633
2634 // Close stream
2635 os.close();
2636}
2637
2638// DIC_analysis_step_data save/load -------------------------------------------//
2641
2642 DIC_analysis_step_data step_data;
2643
2644 // Load step_disps
2645 difference_type num_disps = 0;
2646 is.read(reinterpret_cast<char*>(&num_disps), std::streamsize(sizeof(difference_type)));
2647 step_data.step_disps.resize(num_disps);
2648 for (auto &disp : step_data.step_disps) {
2649 disp = Disp2D::load(is);
2650 }
2651
2652 // Load step_rois
2653 difference_type num_rois = 0;
2654 is.read(reinterpret_cast<char*>(&num_rois), std::streamsize(sizeof(difference_type)));
2655 step_data.step_rois.resize(num_rois);
2656 for (auto &roi : step_data.step_rois) {
2657 roi = ROI2D::load(is);
2658 }
2659
2660 // Load step_ref_idx
2661 difference_type num_ref_idx = 0;
2662 is.read(reinterpret_cast<char*>(&num_ref_idx), std::streamsize(sizeof(difference_type)));
2663 step_data.step_ref_idx.resize(num_ref_idx);
2664 for (auto &idx : step_data.step_ref_idx) {
2665 is.read(reinterpret_cast<char*>(&idx), std::streamsize(sizeof(difference_type)));
2666 }
2667
2668 return step_data;
2669}
2670
2672 std::ifstream is(filename.c_str(), std::ios::in | std::ios::binary);
2673 if (!is.is_open()) {
2674 throw std::invalid_argument("Could not open " + filename + " for loading DIC_analysis_step_data.");
2675 }
2676
2677 auto step_data = DIC_analysis_step_data::load(is);
2678 is.close();
2679
2680 return step_data;
2681}
2682
2683void save(const DIC_analysis_step_data &step_data, std::ofstream &os) {
2685
2686 // Save step_disps
2687 difference_type num_disps = step_data.step_disps.size();
2688 os.write(reinterpret_cast<const char*>(&num_disps), std::streamsize(sizeof(difference_type)));
2689 for (const auto &disp : step_data.step_disps) {
2690 save(disp, os);
2691 }
2692
2693 // Save step_rois
2694 difference_type num_rois = step_data.step_rois.size();
2695 os.write(reinterpret_cast<const char*>(&num_rois), std::streamsize(sizeof(difference_type)));
2696 for (const auto &roi : step_data.step_rois) {
2697 save(roi, os);
2698 }
2699
2700 // Save step_ref_idx
2701 difference_type num_ref_idx = step_data.step_ref_idx.size();
2702 os.write(reinterpret_cast<const char*>(&num_ref_idx), std::streamsize(sizeof(difference_type)));
2703 for (const auto &idx : step_data.step_ref_idx) {
2704 os.write(reinterpret_cast<const char*>(&idx), std::streamsize(sizeof(difference_type)));
2705 }
2706}
2707
2708void save(const DIC_analysis_step_data &step_data, const std::string &filename) {
2709 std::ofstream os(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
2710 if (!os.is_open()) {
2711 throw std::invalid_argument("Could not open " + filename + " for saving DIC_analysis_step_data.");
2712 }
2713
2714 save(step_data, os);
2715 os.close();
2716}
2717
2719 typedef ROI2D::difference_type difference_type;
2720
2721 if (DIC_input.imgs.size() < 2) {
2722 // Must have at least two images for DIC analysis
2723 throw std::invalid_argument("Only " + std::to_string(DIC_input.imgs.size()) + " images provided to DIC_analysis(). 2 or more are required.");
2724 }
2725
2726 // Start timer for entire analysis
2727 std::chrono::time_point<std::chrono::system_clock> start_analysis = std::chrono::system_clock::now();
2728
2729 // Initialize output - note that output will be WRT the first (reference)
2730 // image which is assumed to be the Lagrangian perspective.
2731 DIC_analysis_output DIC_output;
2732 DIC_output.disps.resize(DIC_input.imgs.size()-1);
2734 DIC_output.units = "pixels";
2735 DIC_output.units_per_pixel = 1.0;
2736
2737 // For POST_PROCESS mode: store step displacements and their ROIs
2738 // step_disps[i] = displacement from frame step_ref_idx[i] to frame i+1
2739 // step_rois[i] = ROI used when computing step_disps[i]
2740 std::vector<Disp2D> step_disps;
2741 std::vector<ROI2D> step_rois;
2742 std::vector<difference_type> step_ref_idx; // ref_idx for each step
2743
2744 const bool use_post_process = (DIC_input.accumulation_mode == ACCUMULATION_MODE::POST_PROCESS);
2745 if (use_post_process) {
2746 step_disps.resize(DIC_input.imgs.size()-1);
2747 step_rois.resize(DIC_input.imgs.size()-1);
2748 step_ref_idx.resize(DIC_input.imgs.size()-1);
2749 NLOG_INFO << "Using POST_PROCESS (MATLAB-style) accumulation mode.";
2750 }
2751
2752 // Set ROI for the reference image - this gets updated if reference image
2753 // gets updated. Then, cycle over images and perform DIC.
2754 ROI2D roi_ref = DIC_input.roi;
2755 for (difference_type ref_idx = 0, cur_idx = 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
2756 // -------------------------------------------------------------------//
2757 // Perform RGDIC -----------------------------------------------------//
2758 // -------------------------------------------------------------------//
2759 NLOG_INFO << std::endl << "Processing displacement field " << cur_idx << " of " << DIC_input.imgs.size() - 1 << ".";
2760 NLOG_INFO << "Reference image: " << DIC_input.imgs[ref_idx] << ".";
2761 NLOG_INFO << "Current image: " << DIC_input.imgs[cur_idx] << ".";
2762
2763 std::chrono::time_point<std::chrono::system_clock> start_rgdic = std::chrono::system_clock::now();
2764
2765 auto disps = RGDIC(DIC_input.imgs[ref_idx].get_gs(),
2766 DIC_input.imgs[cur_idx].get_gs(),
2767 roi_ref,
2768 DIC_input.scalefactor,
2769 DIC_input.interp_type,
2770 DIC_input.subregion_type,
2771 DIC_input.r,
2772 DIC_input.num_threads,
2773 DIC_input.cutoff_corrcoef,
2774 DIC_input.debug);
2775
2776 std::chrono::time_point<std::chrono::system_clock> end_rgdic = std::chrono::system_clock::now();
2777 std::chrono::duration<double> elapsed_seconds_rgdic = end_rgdic - start_rgdic;
2778 NLOG_INFO << "Time: " << elapsed_seconds_rgdic.count() << ".";
2779 // -------------------------------------------------------------------//
2780 // -------------------------------------------------------------------//
2781 // -------------------------------------------------------------------//
2782
2783 // Store displacements based on accumulation mode
2784 if (use_post_process) {
2785 // POST_PROCESS mode: Store step displacement with its ROI
2786 step_disps[cur_idx-1] = disps;
2787 step_rois[cur_idx-1] = roi_ref; // ROI used for this step
2788 step_ref_idx[cur_idx-1] = ref_idx;
2789 // Temporarily store current disps (will be replaced in post-processing)
2790 DIC_output.disps[cur_idx-1] = disps;
2791 } else {
2792 // ON_THE_FLY mode: Accumulate displacements immediately
2793 if (ref_idx > 0) {
2794 auto added_disps = add(std::vector<Disp2D>({ DIC_output.disps[ref_idx-1], disps }), DIC_input.interp_type);
2795
2796 if (added_disps.get_roi().get_points() == 0) {
2797 NLOG_WARN << "WARNING: add() at frame " << cur_idx << " produced empty ROI!";
2798 NLOG_WARN << " Previous disp[" << ref_idx-1 << "] had " << DIC_output.disps[ref_idx-1].get_roi().get_points() << " points.";
2799 NLOG_WARN << " Current disps had " << disps.get_roi().get_points() << " points.";
2800 NLOG_WARN << " Using current disps directly instead of combined result.";
2801 DIC_output.disps[cur_idx-1] = disps;
2802 } else if (added_disps.get_roi().get_points() < disps.get_roi().get_points() / 2) {
2803 NLOG_WARN << "WARNING: add() at frame " << cur_idx << " lost significant points!";
2804 NLOG_WARN << " Current disps: " << disps.get_roi().get_points() << ", Combined: " << added_disps.get_roi().get_points();
2805 DIC_output.disps[cur_idx-1] = added_disps;
2806 } else {
2807 DIC_output.disps[cur_idx-1] = added_disps;
2808 }
2809 } else {
2810 DIC_output.disps[cur_idx-1] = disps;
2811 }
2812 }
2813
2814 // Test to see if selected correlation coefficient value (based on input
2815 // "prctile_corrcoef") for the displacement plot exceeds correlation
2816 // coefficient cutoff value; if it does, then update the reference image.
2817 Array2D<double> cc_values = disps.get_cc().get_array()(disps.get_cc().get_roi().get_mask());
2818 if (!cc_values.empty()) {
2819 double selected_corrcoef = prctile(cc_values, DIC_input.prctile_corrcoef);
2820 NLOG_INFO << "Selected correlation coefficient value: " << selected_corrcoef << ". Correlation coefficient update value: " << DIC_input.update_corrcoef << ".";
2821 if (selected_corrcoef > DIC_input.update_corrcoef) {
2822 NLOG_INFO << "DA::Updating reference image..." << ref_idx << " -> " << cur_idx;
2823 auto prev_ref_idx = ref_idx;
2824 // Update the reference image index as well as the reference roi
2825 ref_idx = cur_idx;
2826 auto prev_roi = roi_ref;
2827 roi_ref = update(prev_roi, use_post_process ? disps : DIC_output.disps[cur_idx-1], DIC_input.interp_type, DIC_input.roi_update_mode);
2828
2829 // Check if updated ROI is empty and warn/handle
2830 if (roi_ref.get_points() == 0) {
2831 NLOG_WARN << "WARNING: ROI update at frame " << cur_idx << " produced an empty ROI!";
2832 NLOG_WARN << " Previous ROI had " << prev_roi.get_points() << " points.";
2833 NLOG_WARN << " Mode: " << (DIC_input.roi_update_mode == ROI_UPDATE_MODE::SKIP_ALL ? "SKIP_ALL" : "SKIP_INVALID");
2834 // Revert to previous ROI to prevent crash in subsequent analysis
2835 roi_ref = prev_roi;
2836 ref_idx = prev_ref_idx; // Keep previous reference
2837 NLOG_WARN << " Reverting to previous ROI (frame " << ref_idx << ") to continue analysis.";
2838 }
2839
2840 if (DIC_input.debug) {
2841 cv::Mat diff;
2842 cv::Mat prev_img = get_cv_img(prev_roi.get_mask(), 0, 255);
2843 cv::Mat curr_img = get_cv_img(roi_ref.get_mask(), 0, 255);
2844 cv::absdiff(prev_img, curr_img, diff);
2845 cv::imwrite("roi_" + std::to_string(cur_idx) + "_prev.png", prev_img);
2846 cv::imwrite("roi_" + std::to_string(cur_idx) + "_curr.png", curr_img);
2847 cv::imwrite("diff_roi_" + std::to_string(cur_idx) + ".png", diff);
2848 NLOG_DEBUG << "DEBUG::Saved difference image for frame " << cur_idx;
2849 }
2850 }
2851 }
2852 }
2853
2854 // POST_PROCESS mode: Combine step displacements at the end
2855 if (use_post_process) {
2856 NLOG_INFO << std::endl << "Post-processing: Combining step displacements...";
2857
2858 for (difference_type cur_idx = 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
2859 // Find chain of step displacements needed to reach this frame
2860 std::vector<Disp2D> chain_disps;
2861 std::vector<ROI2D> chain_rois;
2862
2863 // Build chain backwards from cur_idx to frame 0
2864 difference_type idx = cur_idx - 1;
2865 while (idx >= 0) {
2866 chain_disps.insert(chain_disps.begin(), step_disps[idx]);
2867 chain_rois.insert(chain_rois.begin(), step_rois[idx]);
2868
2869 if (step_ref_idx[idx] == 0) {
2870 break; // Reached the original reference
2871 }
2872 idx = step_ref_idx[idx] - 1; // Go to the reference frame's step
2873 }
2874
2875 if (chain_disps.size() == 1) {
2876 // Single step, no need to add
2877 DIC_output.disps[cur_idx-1] = chain_disps[0];
2878 } else {
2879 // Multiple steps - use add_with_rois for proper ROI handling
2880 auto combined = add_with_rois(chain_disps, chain_rois, DIC_input.interp_type);
2881
2882 if (combined.get_roi().get_points() == 0) {
2883 NLOG_WARN << "WARNING: Post-process add_with_rois for frame " << cur_idx << " produced empty ROI!";
2884 NLOG_WARN << " Using last step displacement instead.";
2885 DIC_output.disps[cur_idx-1] = step_disps[cur_idx-1];
2886 } else {
2887 DIC_output.disps[cur_idx-1] = combined;
2888 NLOG_INFO << "Frame " << cur_idx << ": Combined " << chain_disps.size() << " steps, "
2889 << combined.get_roi().get_points() << " valid points.";
2890 }
2891 }
2892 }
2893 }
2894
2895 // Save step data if requested
2896 if (DIC_input.save_disps_steps && use_post_process && !step_disps.empty()) {
2897 DIC_analysis_step_data step_data;
2898 step_data.step_disps = step_disps;
2899 step_data.step_rois = step_rois;
2900 step_data.step_ref_idx = step_ref_idx;
2901
2902 std::string step_filename = "DIC_analysis_step_data.bin";
2903 save(step_data, step_filename);
2904 NLOG_INFO << "Step displacement data saved to " << step_filename;
2905 }
2906
2907 // End timer for entire analysis
2908 std::chrono::time_point<std::chrono::system_clock> end_analysis = std::chrono::system_clock::now();
2909 std::chrono::duration<double> elapsed_seconds_analysis = end_analysis - start_analysis;
2910 NLOG_INFO << std::endl << "Total DIC analysis time: " << elapsed_seconds_analysis.count() << ".";
2911
2912 return DIC_output;
2913}
2914
2915
2916DIC_analysis_output DIC_analysis_sequential(const DIC_analysis_input &DIC_input, const std::vector<SeedParams>& seeds_by_region, bool seeds_are_optimized) {
2917 typedef ROI2D::difference_type difference_type;
2918
2919 if (DIC_input.imgs.size() < 2) {
2920 throw std::invalid_argument("Only " + std::to_string(DIC_input.imgs.size()) + " images provided to DIC_analysis(). 2 or more are required.");
2921 }
2922
2923 std::chrono::time_point<std::chrono::system_clock> start_analysis = std::chrono::system_clock::now();
2924
2925 DIC_analysis_output DIC_output;
2926 DIC_output.disps.resize(DIC_input.imgs.size()-1);
2928 DIC_output.units = "pixels";
2929 DIC_output.units_per_pixel = 1.0;
2930
2931 // For POST_PROCESS mode: store step displacements and their ROIs
2932 std::vector<Disp2D> step_disps;
2933 std::vector<ROI2D> step_rois;
2934 std::vector<difference_type> step_ref_idx;
2935
2936 const bool use_post_process = (DIC_input.accumulation_mode == ACCUMULATION_MODE::POST_PROCESS);
2937 if (use_post_process) {
2938 step_disps.resize(DIC_input.imgs.size()-1);
2939 step_rois.resize(DIC_input.imgs.size()-1);
2940 step_ref_idx.resize(DIC_input.imgs.size()-1);
2941 NLOG_INFO << "Using POST_PROCESS (MATLAB-style) accumulation mode.";
2942 }
2943
2944 ROI2D roi_ref = DIC_input.roi;
2945 // Mutable copy of seeds so we can propagate them when ROI updates
2946 std::vector<SeedParams> current_seeds = seeds_by_region;
2947 bool current_seeds_optimized = seeds_are_optimized;
2948 for (difference_type ref_idx = 0, cur_idx = 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
2949 NLOG_INFO << std::endl << "Processing displacement field " << cur_idx << " of " << DIC_input.imgs.size() - 1 << ".";
2950 NLOG_INFO << "Reference image: " << DIC_input.imgs[ref_idx] << ".";
2951 NLOG_INFO << "Current image: " << DIC_input.imgs[cur_idx] << ".";
2952
2953 std::chrono::time_point<std::chrono::system_clock> start_rgdic = std::chrono::system_clock::now();
2954
2955 NLOG_DEBUG << "Debug:: " << current_seeds[0].x << " " << current_seeds[0].y;
2956 auto disps = RGDIC_without_thread(DIC_input.imgs[ref_idx].get_gs(),
2957 DIC_input.imgs[cur_idx].get_gs(),
2958 roi_ref,
2959 DIC_input.scalefactor,
2960 DIC_input.interp_type,
2961 DIC_input.subregion_type,
2962 DIC_input.r,
2963 DIC_input.cutoff_corrcoef,
2964 DIC_input.debug,
2965 current_seeds,
2966 current_seeds_optimized);
2967
2968 std::chrono::time_point<std::chrono::system_clock> end_rgdic = std::chrono::system_clock::now();
2969 std::chrono::duration<double> elapsed_seconds_rgdic = end_rgdic - start_rgdic;
2970 NLOG_INFO << "Time: " << elapsed_seconds_rgdic.count() << ".";
2971
2972 // Store displacements based on accumulation mode
2973 if (use_post_process) {
2974 step_disps[cur_idx-1] = disps;
2975 step_rois[cur_idx-1] = roi_ref;
2976 step_ref_idx[cur_idx-1] = ref_idx;
2977 DIC_output.disps[cur_idx-1] = disps;
2978 } else {
2979 if (ref_idx > 0) {
2980 auto added_disps = add(std::vector<Disp2D>({ DIC_output.disps[ref_idx-1], disps }), DIC_input.interp_type);
2981 if (added_disps.get_roi().get_points() == 0) {
2982 NLOG_WARN << "WARNING: add() at frame " << cur_idx << " produced empty ROI! Using current disps.";
2983 DIC_output.disps[cur_idx-1] = disps;
2984 } else {
2985 DIC_output.disps[cur_idx-1] = added_disps;
2986 }
2987 } else {
2988 DIC_output.disps[cur_idx-1] = disps;
2989 }
2990 }
2991
2992 Array2D<double> cc_values = disps.get_cc().get_array()(disps.get_cc().get_roi().get_mask());
2993 if (!cc_values.empty()) {
2994 double selected_corrcoef = prctile(cc_values, DIC_input.prctile_corrcoef);
2995 NLOG_INFO << "Selected correlation coefficient value: " << selected_corrcoef << ". Correlation coefficient update value: " << DIC_input.update_corrcoef << ".";
2996 if (selected_corrcoef > DIC_input.update_corrcoef) {
2997 NLOG_INFO << "DAS::Updating reference image..." << ref_idx << " -> " << cur_idx;
2998 auto prev_ref_idx = ref_idx;
2999 // Update the reference image index as well as the reference roi
3000 ref_idx = cur_idx;
3001 auto prev_roi = roi_ref;
3002 roi_ref = update(prev_roi, use_post_process ? disps : DIC_output.disps[cur_idx-1], DIC_input.interp_type, DIC_input.roi_update_mode);
3003
3004 // Update the seeds_by_region according to the ROI change:
3005 // Move each seed based on last displacement, snap to grid, validate in new ROI
3006 if (false && !current_seeds.empty()) {
3007 const auto& disp_for_seeds = use_post_process ? disps : DIC_output.disps[cur_idx-1];
3008 const auto& u_arr = disp_for_seeds.get_u().get_array();
3009 const auto& v_arr = disp_for_seeds.get_v().get_array();
3010 difference_type sf = DIC_input.scalefactor;
3011 std::vector<SeedParams> updated_seeds;
3012 updated_seeds.reserve(current_seeds.size());
3013 for (const auto& seed : current_seeds) {
3014 // Sample displacement at current seed position (reduced coords)
3015 difference_type ry = seed.y / sf;
3016 difference_type rx = seed.x / sf;
3017 if (ry >= 0 && ry < u_arr.height() && rx >= 0 && rx < u_arr.width()) {
3018 double du = u_arr(ry, rx) * sf;
3019 double dv = v_arr(ry, rx) * sf;
3020 // New position snapped to spacing grid
3021 difference_type new_x = seed.x + static_cast<difference_type>(std::round(du / sf) * sf);
3022 difference_type new_y = seed.y + static_cast<difference_type>(std::round(dv / sf) * sf);
3023 // Validate new position is inside the updated ROI
3024 difference_type nry = new_y / sf;
3025 difference_type nrx = new_x / sf;
3026 const auto& new_mask = roi_ref.get_mask();
3027 if (nry >= 0 && nry < new_mask.height() && nrx >= 0 && nrx < new_mask.width() && new_mask(nry, nrx)) {
3028 SeedParams new_seed;
3029 new_seed.x = new_x;
3030 new_seed.y = new_y;
3031 // Reset displacement guess (new reference frame)
3032 new_seed.u = 0.0;
3033 new_seed.v = 0.0;
3034 new_seed.du_dx = 0.0;
3035 new_seed.du_dy = 0.0;
3036 new_seed.dv_dx = 0.0;
3037 new_seed.dv_dy = 0.0;
3038 new_seed.corrcoef = 0.0;
3039 updated_seeds.push_back(new_seed);
3040 }
3041 }
3042 }
3043 if (!updated_seeds.empty()) {
3044 current_seeds = updated_seeds;
3045 current_seeds_optimized = false; // Seeds need re-optimization with new reference
3046 NLOG_INFO << " Seeds propagated: " << updated_seeds.size() << "/" << seeds_by_region.size() << " remain in updated ROI.";
3047 } else {
3048 NLOG_WARN << " WARNING: No seeds remain in updated ROI. Clearing seeds (auto-detect will be used).";
3049 current_seeds.clear();
3050 }
3051 }
3052
3053 // Check if updated ROI is empty and warn/handle
3054 if (false && roi_ref.get_points() == 0) {
3055 NLOG_WARN << "WARNING: ROI update at frame " << cur_idx << " produced an empty ROI!";
3056 NLOG_WARN << " Previous ROI had " << prev_roi.get_points() << " points.";
3057 NLOG_WARN << " Mode: " << (DIC_input.roi_update_mode == ROI_UPDATE_MODE::SKIP_ALL ? "SKIP_ALL" : "SKIP_INVALID");
3058 // Revert to previous ROI to prevent crash in subsequent analysis
3059 roi_ref = prev_roi;
3060 ref_idx = prev_ref_idx; // Keep previous reference
3061 current_seeds = seeds_by_region; // Revert seeds too
3062 current_seeds_optimized = seeds_are_optimized;
3063 NLOG_WARN << " Reverting to previous ROI (frame " << ref_idx << ") to continue analysis.";
3064 }
3065
3066 if (DIC_input.debug) {
3067 cv::Mat diff;
3068 cv::Mat prev_img = get_cv_img(prev_roi.get_mask(), 0, 255);
3069 cv::Mat curr_img = get_cv_img(roi_ref.get_mask(), 0, 255);
3070 cv::absdiff(prev_img, curr_img, diff);
3071 cv::imwrite("roi_" + std::to_string(cur_idx) + "_prev.png", prev_img);
3072 cv::imwrite("roi_" + std::to_string(cur_idx) + "_curr.png", curr_img);
3073 cv::imwrite("diff_roi_" + std::to_string(cur_idx) + ".png", diff);
3074 NLOG_DEBUG << "DEBUG::Saved difference image for frame " << cur_idx;
3075 }
3076 }
3077 }
3078 }
3079
3080 // POST_PROCESS mode: Combine step displacements at the end
3081 if (use_post_process) {
3082 NLOG_INFO << std::endl << "Post-processing: Combining step displacements...";
3083 for (difference_type cur_idx = 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
3084 std::vector<Disp2D> chain_disps;
3085 std::vector<ROI2D> chain_rois;
3086 difference_type idx = cur_idx - 1;
3087 while (idx >= 0) {
3088 chain_disps.insert(chain_disps.begin(), step_disps[idx]);
3089 chain_rois.insert(chain_rois.begin(), step_rois[idx]);
3090 if (step_ref_idx[idx] == 0) break;
3091 idx = step_ref_idx[idx] - 1;
3092 }
3093 if (chain_disps.size() == 1) {
3094 DIC_output.disps[cur_idx-1] = chain_disps[0];
3095 } else {
3096 auto combined = add_with_rois(chain_disps, chain_rois, DIC_input.interp_type);
3097 if (combined.get_roi().get_points() == 0) {
3098 NLOG_WARN << "WARNING: Post-process add_with_rois for frame " << cur_idx << " produced empty ROI!";
3099 DIC_output.disps[cur_idx-1] = step_disps[cur_idx-1];
3100 } else {
3101 DIC_output.disps[cur_idx-1] = combined;
3102 NLOG_INFO << "Frame " << cur_idx << ": Combined " << chain_disps.size() << " steps.";
3103 }
3104 }
3105 }
3106 }
3107
3108 // Save step data if requested
3109 if (DIC_input.save_disps_steps && use_post_process && !step_disps.empty()) {
3110 DIC_analysis_step_data step_data;
3111 step_data.step_disps = step_disps;
3112 step_data.step_rois = step_rois;
3113 step_data.step_ref_idx = step_ref_idx;
3114
3115 std::string step_filename = "DIC_analysis_sequential_step_data.bin";
3116 save(step_data, step_filename);
3117 NLOG_INFO << "Step displacement data saved to " << step_filename;
3118 }
3119
3120 std::chrono::time_point<std::chrono::system_clock> end_analysis = std::chrono::system_clock::now();
3121 std::chrono::duration<double> elapsed_seconds_analysis = end_analysis - start_analysis;
3122 NLOG_INFO << std::endl << "Total DIC analysis time: " << elapsed_seconds_analysis.count() << ".";
3123
3124 return DIC_output;
3125}
3126
3127// Sequential DIC analysis with user-provided seeds --------------------------//
3129 typedef ROI2D::difference_type difference_type;
3130
3131 const auto& DIC_input = parallel_input.base_input;
3132
3133 if (DIC_input.imgs.size() < 2) {
3134 throw std::invalid_argument("Only " + std::to_string(DIC_input.imgs.size()) + " images provided to DIC_analysis(). 2 or more are required.");
3135 }
3136
3137 std::chrono::time_point<std::chrono::system_clock> start_analysis = std::chrono::system_clock::now();
3138
3139 DIC_analysis_output DIC_output;
3140 DIC_output.disps.resize(DIC_input.imgs.size()-1);
3142 DIC_output.units = "pixels";
3143 DIC_output.units_per_pixel = 1.0;
3144
3145 // For POST_PROCESS mode: store step displacements and their ROIs
3146 std::vector<Disp2D> step_disps;
3147 std::vector<ROI2D> step_rois;
3148 std::vector<difference_type> step_ref_idx;
3149
3150 const bool use_post_process = (DIC_input.accumulation_mode == ACCUMULATION_MODE::POST_PROCESS);
3151 if (use_post_process) {
3152 step_disps.resize(DIC_input.imgs.size()-1);
3153 step_rois.resize(DIC_input.imgs.size()-1);
3154 step_ref_idx.resize(DIC_input.imgs.size()-1);
3155 NLOG_INFO << "Using POST_PROCESS (MATLAB-style) accumulation mode.";
3156 }
3157
3158 ROI2D roi_ref = DIC_input.roi;
3159 // Mutable copy of seeds so we can propagate them when ROI updates
3160 std::vector<SeedParams> current_seeds = parallel_input.seeds_by_region;
3161 bool current_seeds_optimized = parallel_input.seeds_are_optimized;
3162 for (difference_type ref_idx = 0, cur_idx = 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
3163 NLOG_INFO << std::endl << "Processing displacement field " << cur_idx << " of " << DIC_input.imgs.size() - 1 << ".";
3164 NLOG_INFO << "Reference image: " << DIC_input.imgs[ref_idx] << ".";
3165 NLOG_INFO << "Current image: " << DIC_input.imgs[cur_idx] << ".";
3166
3167 std::chrono::time_point<std::chrono::system_clock> start_rgdic = std::chrono::system_clock::now();
3168
3169 auto disps = RGDIC_without_thread(DIC_input.imgs[ref_idx].get_gs(),
3170 DIC_input.imgs[cur_idx].get_gs(),
3171 roi_ref,
3172 DIC_input.scalefactor,
3173 DIC_input.interp_type,
3174 DIC_input.subregion_type,
3175 DIC_input.r,
3176 DIC_input.cutoff_corrcoef,
3177 DIC_input.debug,
3178 current_seeds,
3179 current_seeds_optimized);
3180
3181 std::chrono::time_point<std::chrono::system_clock> end_rgdic = std::chrono::system_clock::now();
3182 std::chrono::duration<double> elapsed_seconds_rgdic = end_rgdic - start_rgdic;
3183 NLOG_INFO << "Time: " << elapsed_seconds_rgdic.count() << ".";
3184
3185 // Store displacements based on accumulation mode
3186 if (use_post_process) {
3187 step_disps[cur_idx-1] = disps;
3188 step_rois[cur_idx-1] = roi_ref;
3189 step_ref_idx[cur_idx-1] = ref_idx;
3190 DIC_output.disps[cur_idx-1] = disps;
3191 } else {
3192 if (ref_idx > 0) {
3193 auto added_disps = add(std::vector<Disp2D>({ DIC_output.disps[ref_idx-1], disps }), DIC_input.interp_type);
3194 if (added_disps.get_roi().get_points() == 0) {
3195 NLOG_WARN << "WARNING: add() at frame " << cur_idx << " produced empty ROI! Using current disps.";
3196 DIC_output.disps[cur_idx-1] = disps;
3197 } else {
3198 DIC_output.disps[cur_idx-1] = added_disps;
3199 }
3200 } else {
3201 DIC_output.disps[cur_idx-1] = disps;
3202 }
3203 }
3204
3205 Array2D<double> cc_values = disps.get_cc().get_array()(disps.get_cc().get_roi().get_mask());
3206 if (!cc_values.empty()) {
3207 double selected_corrcoef = prctile(cc_values, DIC_input.prctile_corrcoef);
3208 NLOG_INFO << "Selected correlation coefficient value: " << selected_corrcoef << ". Correlation coefficient update value: " << DIC_input.update_corrcoef << ".";
3209 if (selected_corrcoef > DIC_input.update_corrcoef) {
3210 NLOG_INFO << "DASP::Updating reference image..." << ref_idx << " -> " << cur_idx;
3211 auto prev_ref_idx = ref_idx;
3212 // Update the reference image index as well as the reference roi
3213 ref_idx = cur_idx;
3214 auto prev_roi = roi_ref;
3215 roi_ref = update(prev_roi, use_post_process ? disps : DIC_output.disps[cur_idx-1], DIC_input.interp_type, DIC_input.roi_update_mode);
3216
3217 // Update seeds according to the ROI change:
3218 // Move each seed based on last displacement, snap to grid, validate in new ROI
3219 if (!current_seeds.empty()) {
3220 const auto& disp_for_seeds = use_post_process ? disps : DIC_output.disps[cur_idx-1];
3221 const auto& u_arr = disp_for_seeds.get_u().get_array();
3222 const auto& v_arr = disp_for_seeds.get_v().get_array();
3223 difference_type sf = DIC_input.scalefactor;
3224 std::vector<SeedParams> updated_seeds;
3225 updated_seeds.reserve(current_seeds.size());
3226 for (const auto& seed : current_seeds) {
3227 difference_type ry = seed.y / sf;
3228 difference_type rx = seed.x / sf;
3229 if (ry >= 0 && ry < u_arr.height() && rx >= 0 && rx < u_arr.width()) {
3230 double du = u_arr(ry, rx) * sf;
3231 double dv = v_arr(ry, rx) * sf;
3232 difference_type new_x = seed.x + static_cast<difference_type>(std::round(du / sf) * sf);
3233 difference_type new_y = seed.y + static_cast<difference_type>(std::round(dv / sf) * sf);
3234 difference_type nry = new_y / sf;
3235 difference_type nrx = new_x / sf;
3236 const auto& new_mask = roi_ref.get_mask();
3237 if (nry >= 0 && nry < new_mask.height() && nrx >= 0 && nrx < new_mask.width() && new_mask(nry, nrx)) {
3238 SeedParams new_seed;
3239 new_seed.x = new_x;
3240 new_seed.y = new_y;
3241 new_seed.u = 0.0;
3242 new_seed.v = 0.0;
3243 new_seed.du_dx = 0.0;
3244 new_seed.du_dy = 0.0;
3245 new_seed.dv_dx = 0.0;
3246 new_seed.dv_dy = 0.0;
3247 new_seed.corrcoef = 0.0;
3248 updated_seeds.push_back(new_seed);
3249 }
3250 }
3251 }
3252 if (!updated_seeds.empty()) {
3253 current_seeds = updated_seeds;
3254 current_seeds_optimized = false;
3255 NLOG_INFO << " Seeds propagated: " << updated_seeds.size() << "/" << parallel_input.seeds_by_region.size() << " remain in updated ROI.";
3256 } else {
3257 NLOG_WARN << " WARNING: No seeds remain in updated ROI. Clearing seeds (auto-detect will be used).";
3258 current_seeds.clear();
3259 }
3260 }
3261
3262 // Check if updated ROI is empty and warn/handle
3263 if (roi_ref.get_points() == 0) {
3264 NLOG_WARN << "WARNING: ROI update at frame " << cur_idx << " produced an empty ROI!";
3265 NLOG_WARN << " Previous ROI had " << prev_roi.get_points() << " points.";
3266 NLOG_WARN << " Mode: " << (DIC_input.roi_update_mode == ROI_UPDATE_MODE::SKIP_ALL ? "SKIP_ALL" : "SKIP_INVALID");
3267 // Revert to previous ROI and seeds to prevent crash
3268 roi_ref = prev_roi;
3269 ref_idx = prev_ref_idx;
3270 current_seeds = parallel_input.seeds_by_region;
3271 current_seeds_optimized = parallel_input.seeds_are_optimized;
3272 NLOG_WARN << " Reverting to previous ROI (frame " << ref_idx << ") to continue analysis.";
3273 }
3274
3275 if (DIC_input.debug) {
3276 cv::Mat diff;
3277 cv::Mat prev_img = get_cv_img(prev_roi.get_mask(), 0, 255);
3278 cv::Mat curr_img = get_cv_img(roi_ref.get_mask(), 0, 255);
3279 cv::absdiff(prev_img, curr_img, diff);
3280 cv::imwrite("roi_" + std::to_string(cur_idx) + "_prev.png", prev_img);
3281 cv::imwrite("roi_" + std::to_string(cur_idx) + "_curr.png", curr_img);
3282 cv::imwrite("diff_roi_" + std::to_string(cur_idx) + ".png", diff);
3283 NLOG_DEBUG << "DEBUG::Saved difference image for frame " << cur_idx;
3284 }
3285 }
3286 }
3287 }
3288
3289 // POST_PROCESS mode: Combine step displacements at the end
3290 if (use_post_process) {
3291 NLOG_INFO << std::endl << "Post-processing: Combining step displacements...";
3292 for (difference_type cur_idx = 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
3293 std::vector<Disp2D> chain_disps;
3294 std::vector<ROI2D> chain_rois;
3295 difference_type idx = cur_idx - 1;
3296 while (idx >= 0) {
3297 chain_disps.insert(chain_disps.begin(), step_disps[idx]);
3298 chain_rois.insert(chain_rois.begin(), step_rois[idx]);
3299 if (step_ref_idx[idx] == 0) break;
3300 idx = step_ref_idx[idx] - 1;
3301 }
3302 if (chain_disps.size() == 1) {
3303 DIC_output.disps[cur_idx-1] = chain_disps[0];
3304 } else {
3305 auto combined = add_with_rois(chain_disps, chain_rois, DIC_input.interp_type);
3306 if (combined.get_roi().get_points() == 0) {
3307 NLOG_WARN << "WARNING: Post-process add_with_rois for frame " << cur_idx << " produced empty ROI!";
3308 DIC_output.disps[cur_idx-1] = step_disps[cur_idx-1];
3309 } else {
3310 DIC_output.disps[cur_idx-1] = combined;
3311 NLOG_INFO << "Frame " << cur_idx << ": Combined " << chain_disps.size() << " steps.";
3312 }
3313 }
3314 }
3315 }
3316
3317 // Save step data if requested
3318 if (DIC_input.save_disps_steps && use_post_process && !step_disps.empty()) {
3319 DIC_analysis_step_data step_data;
3320 step_data.step_disps = step_disps;
3321 step_data.step_rois = step_rois;
3322 step_data.step_ref_idx = step_ref_idx;
3323
3324 std::string step_filename = "DIC_analysis_sequential_seeds_step_data.bin";
3325 save(step_data, step_filename);
3326 NLOG_INFO << "Step displacement data saved to " << step_filename;
3327 }
3328
3329 std::chrono::time_point<std::chrono::system_clock> end_analysis = std::chrono::system_clock::now();
3330 std::chrono::duration<double> elapsed_seconds_analysis = end_analysis - start_analysis;
3331 NLOG_INFO << std::endl << "Total DIC analysis time: " << elapsed_seconds_analysis.count() << ".";
3332
3333 return DIC_output;
3334}
3335
3336// RGDIC with user-provided seeds (single frame pair) ------------------------//
3338 const Array2D<double>& A_cur,
3339 const ROI2D& roi,
3340 const DIC_analysis_parallel_input& input) {
3341 return RGDIC_without_thread(A_ref,
3342 A_cur,
3343 roi,
3344 input.base_input.scalefactor,
3345 input.base_input.interp_type,
3347 input.base_input.r,
3349 input.base_input.debug,
3350 input.seeds_by_region,
3351 input.seeds_are_optimized);
3352}
3353
3354// Conversion between Lagrangian and Eulerian displacements ------------------//
3355namespace details {
3357 // Update v and u - use SKIP_INVALID by default for perspective change robustness
3358 auto v_updated = update(disp.get_v(), disp, interp_type, mode);
3359 auto u_updated = update(disp.get_u(), disp, interp_type, mode);
3360
3361 // Note that moving arrays is safe because update() creates new Data2Ds
3362 return { std::move(v_updated.get_array()), std::move(u_updated.get_array()), v_updated.get_roi(), disp.get_scalefactor() };
3363 }
3364}
3365
3367 typedef ROI2D::difference_type difference_type;
3368
3369 if (DIC_output.perspective_type == PERSPECTIVE::EULERIAN) {
3370 // For now, do not support this perspective change, because by
3371 // default, DIC_analysis() returns the displacement fields in the
3372 // Lagrangian perspective, so this change would be redundant and less
3373 // accurate.
3374 throw std::invalid_argument("Changing from Eulerian perspective back to Lagrangian is currently not supported. Just use the original output data from DIC_analysis().");
3375 }
3376
3377 if (DIC_output.units != "pixels") {
3378 // Perform change_perspective() before applying units. This makes things
3379 // simpler.
3380 throw std::invalid_argument("Changing from Lagrangian to Eulerian perspective must be done before applying units.");
3381 }
3382
3383 // Initialize output
3384 DIC_analysis_output DIC_output_updated;
3385 DIC_output_updated.perspective_type = PERSPECTIVE::EULERIAN; // This will be Eulerian since only Lagrangian inputs are supported for now.
3386 DIC_output_updated.units = DIC_output.units;
3387 DIC_output_updated.units_per_pixel = DIC_output.units_per_pixel;
3388
3389 // Cycle over displacement fields and convert
3390 NLOG_INFO << std::endl << "Changing perspective...";
3391 for (difference_type disp_idx = 0; disp_idx < difference_type(DIC_output.disps.size()); ++disp_idx) {
3392 NLOG_INFO << "Displacement field " << disp_idx+1 << " of " << DIC_output.disps.size() << ".";
3393 DIC_output_updated.disps.push_back(details::update(DIC_output.disps[disp_idx], interp_type));
3394 }
3395
3396 return DIC_output_updated;
3397}
3398
3399// Conversion with Matlab-compatible sign inversion -------------------------//
3401 typedef ROI2D::difference_type difference_type;
3402
3403 if (DIC_output.perspective_type == PERSPECTIVE::EULERIAN) {
3404 // For now, do not support this perspective change, because by
3405 // default, DIC_analysis() returns the displacement fields in the
3406 // Lagrangian perspective, so this change would be redundant and less
3407 // accurate.
3408 throw std::invalid_argument("Changing from Eulerian perspective back to Lagrangian is currently not supported. Just use the original output data from DIC_analysis().");
3409 }
3410
3411 if (DIC_output.units != "pixels") {
3412 // Perform change_perspective_with_inversion() before applying units. This makes things
3413 // simpler.
3414 throw std::invalid_argument("Changing from Lagrangian to Eulerian perspective must be done before applying units.");
3415 }
3416
3417 // Initialize output
3418 DIC_analysis_output DIC_output_updated;
3419 DIC_output_updated.perspective_type = PERSPECTIVE::EULERIAN; // This will be Eulerian since only Lagrangian inputs are supported for now.
3420 DIC_output_updated.units = DIC_output.units;
3421 DIC_output_updated.units_per_pixel = DIC_output.units_per_pixel;
3422
3423 // Cycle over displacement fields and convert
3424 NLOG_INFO << std::endl << "Changing perspective with sign inversion (Matlab-compatible)...";
3425 for (difference_type disp_idx = 0; disp_idx < difference_type(DIC_output.disps.size()); ++disp_idx) {
3426 NLOG_INFO << "Displacement field " << disp_idx+1 << " of " << DIC_output.disps.size() << ".";
3427
3428 // Apply perspective conversion via interpolation
3429 Disp2D disp_eulerian = details::update(DIC_output.disps[disp_idx], interp_type);
3430
3431 // Apply sign inversion to match Matlab convention
3432 // Eulerian displacements are negated: u_eulerian = -u_lagrangian
3433 // This reflects the change from material (reference) to spatial (current) description
3434 auto A_v = disp_eulerian.get_v().get_array() * -1.0;
3435 auto A_u = disp_eulerian.get_u().get_array() * -1.0;
3436 auto A_cc = disp_eulerian.get_cc().get_array(); // Correlation unchanged
3437
3438 DIC_output_updated.disps.push_back(Disp2D(
3439 std::move(A_v),
3440 std::move(A_u),
3441 std::move(A_cc),
3442 disp_eulerian.get_roi(),
3443 disp_eulerian.get_scalefactor()
3444 ));
3445 }
3446
3447 return DIC_output_updated;
3448}
3449
3450// set_units -----------------------------------------------------------------//
3451DIC_analysis_output set_units(const DIC_analysis_output &DIC_output, const std::string &units, double units_per_pixel) {
3452 typedef ROI2D::difference_type difference_type;
3453
3454 if (DIC_output.units != "pixels") {
3455 // Units have already been set - do not do this twice for simplicity.
3456 throw std::invalid_argument("Units have already been set for this DIC_analysis_output.");
3457 }
3458
3459 // Initialize output
3460 DIC_analysis_output DIC_output_updated;
3461 DIC_output_updated.perspective_type = DIC_output.perspective_type;
3462 DIC_output_updated.units = units;
3463 DIC_output_updated.units_per_pixel = units_per_pixel;
3464
3465 // Cycle over displacements and convert
3466 for (difference_type disp_idx = 0; disp_idx < difference_type(DIC_output.disps.size()); ++disp_idx) {
3467 // Get copies of displacement fields and apply conversion
3468 auto A_v = DIC_output.disps[disp_idx].get_v().get_array() * units_per_pixel;
3469 auto A_u = DIC_output.disps[disp_idx].get_u().get_array() * units_per_pixel;
3470 auto A_cc = DIC_output.disps[disp_idx].get_cc().get_array(); // Correlation doesn't scale
3471
3472 DIC_output_updated.disps.push_back(Disp2D(std::move(A_v),
3473 std::move(A_u),
3474 std::move(A_cc),
3475 DIC_output.disps[disp_idx].get_roi(),
3476 DIC_output.disps[disp_idx].get_scalefactor()));
3477 }
3478
3479 return DIC_output_updated;
3480}
3481
3482// Correlation filtering -----------------------------------------------------//
3483DIC_analysis_output filter_by_correlation(const DIC_analysis_output &DIC_output, double cutoff_corrcoef) {
3484 typedef ROI2D::difference_type difference_type;
3485
3486 if (cutoff_corrcoef < -1.0 || cutoff_corrcoef > 1.0) {
3487 throw std::invalid_argument("Correlation coefficient cutoff must be in range [-1.0, 1.0]. Got: " + std::to_string(cutoff_corrcoef));
3488 }
3489
3490 // Initialize output with same metadata
3491 DIC_analysis_output DIC_output_filtered;
3492 DIC_output_filtered.perspective_type = DIC_output.perspective_type;
3493 DIC_output_filtered.units = DIC_output.units;
3494 DIC_output_filtered.units_per_pixel = DIC_output.units_per_pixel;
3495
3496 NLOG_INFO << std::endl << "Filtering by correlation coefficient (threshold: " << cutoff_corrcoef << ")...";
3497
3498 // Process each displacement field
3499 for (difference_type disp_idx = 0; disp_idx < difference_type(DIC_output.disps.size()); ++disp_idx) {
3500 const auto& disp = DIC_output.disps[disp_idx];
3501 const auto& cc_array = disp.get_cc().get_array();
3502 const auto& roi_mask = disp.get_roi().get_mask();
3503
3504 // Create filtered mask: keep only points above correlation threshold
3505 Array2D<bool> mask_filtered(roi_mask.height(), roi_mask.width(), false);
3506 difference_type points_kept = 0;
3507 difference_type points_total = 0;
3508
3509 for (difference_type i = 0; i < roi_mask.height(); ++i) {
3510 for (difference_type j = 0; j < roi_mask.width(); ++j) {
3511 if (roi_mask(i, j)) {
3512 points_total++;
3513 // Keep point if correlation is above threshold
3514 if (cc_array(i, j) >= cutoff_corrcoef) {
3515 mask_filtered(i, j) = true;
3516 points_kept++;
3517 }
3518 }
3519 }
3520 }
3521
3522 NLOG_INFO << " Displacement " << disp_idx+1 << ": kept " << points_kept
3523 << " / " << points_total << " points"
3524 << (points_total > 0
3525 ? " (" + std::to_string(100.0 * points_kept / points_total) + "%)"
3526 : std::string())
3527 << ".";
3528
3529 // Create new ROI with filtered mask
3530 ROI2D roi_filtered(std::move(mask_filtered));
3531
3532 // Store displacement with updated ROI
3533 // Copy arrays since we're only changing the ROI, not the data
3534 DIC_output_filtered.disps.push_back(Disp2D(
3535 disp.get_v().get_array(),
3536 disp.get_u().get_array(),
3537 disp.get_cc().get_array(),
3538 roi_filtered,
3539 disp.get_scalefactor()
3540 ));
3541 }
3542
3543 return DIC_output_filtered;
3544}
3545
3546// strain_analysis -----------------------------------------------------------//
3547Strain2D LS_strain(const Disp2D &disp, PERSPECTIVE perspective_type, double units_per_pixel, SUBREGION subregion_type, ROI2D::difference_type r) {
3548 typedef ROI2D::difference_type difference_type;
3549
3550 // This will calculate Green-Langrangian strains for displacement fields in
3551 // the Lagrangian perspective; Eulerian-Almansi strains for displacement
3552 // fields in the Eulerian perspective. Displacement gradients are calculated
3553 // using a least squares plane fit on a contiguous subregion of displacement
3554 // points, and then these values are used to calculate the strain.
3555
3556 if (units_per_pixel <= 0) {
3557 throw std::invalid_argument("Attempted to calculate least squares strain with units_per_pixel: " + std::to_string(units_per_pixel) +
3558 ". units_per_pixel must be greater than zero.");
3559 }
3560
3561 if (r < 1) {
3562 throw std::invalid_argument("Attempted to calculate least squares strain with radius: " + std::to_string(r) +
3563 ". radius must be 1 or greater.");
3564 }
3565
3566 // Initialize outputs
3567 Array2D<double> A_eyy(disp.data_height(), disp.data_width());
3568 Array2D<double> A_exy(disp.data_height(), disp.data_width());
3569 Array2D<double> A_exx(disp.data_height(), disp.data_width());
3570 Array2D<bool> A_vp(disp.data_height(), disp.data_width());
3571
3572 // Set buffers for the least squares plane fit
3573 Array2D<double> mat_LS(3,3);
3574 Array2D<double> v_LS(3,1);
3575 Array2D<double> u_LS(3,1);
3576
3577 // Get contig subregion generator
3578 auto subregion_gen = disp.get_roi().get_contig_subregion_generator(subregion_type, r);
3579
3580 // Cycle over entire roi and calculate strains
3581 for (difference_type region_idx = 0; region_idx < disp.get_roi().size_regions(); ++region_idx) {
3582 for (difference_type nl_idx = 0; nl_idx < disp.get_roi().get_nlinfo(region_idx).nodelist.width(); ++nl_idx) {
3583 difference_type p2 = nl_idx + disp.get_roi().get_nlinfo(region_idx).left_nl;
3584 for (difference_type np_idx = 0; np_idx < disp.get_roi().get_nlinfo(region_idx).noderange(nl_idx); np_idx += 2) {
3585 difference_type np_top = disp.get_roi().get_nlinfo(region_idx).nodelist(np_idx,nl_idx);
3586 difference_type np_bottom = disp.get_roi().get_nlinfo(region_idx).nodelist(np_idx + 1,nl_idx);
3587 for (difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
3588 // Reset buffers
3589 mat_LS() = 0;
3590 v_LS() = 0;
3591 u_LS() = 0;
3592
3593 // Get subregion nlinfo for input p1 and p2
3594 const auto &subregion_nlinfo = subregion_gen(p1, p2);
3595
3596 // Cycle over subregion and calculate mat_LS, u_LS, and v_LS
3597 for (difference_type nl_idx_subregion = 0; nl_idx_subregion < subregion_nlinfo.nodelist.width(); ++nl_idx_subregion) {
3598 difference_type p2_subregion = nl_idx_subregion + subregion_nlinfo.left_nl;
3599 for (difference_type np_idx_subregion = 0; np_idx_subregion < subregion_nlinfo.noderange(nl_idx_subregion); np_idx_subregion += 2) {
3600 difference_type np_top_subregion = subregion_nlinfo.nodelist(np_idx_subregion,nl_idx_subregion);
3601 difference_type np_bottom_subregion = subregion_nlinfo.nodelist(np_idx_subregion + 1,nl_idx_subregion);
3602 for (difference_type p1_subregion = np_top_subregion; p1_subregion <= np_bottom_subregion; ++p1_subregion) {
3603 difference_type delta_p1 = p1_subregion - p1;
3604 difference_type delta_p2 = p2_subregion - p2;
3605 double v = disp.get_v().get_array()(p1_subregion,p2_subregion);
3606 double u = disp.get_u().get_array()(p1_subregion,p2_subregion);
3607
3608 // LS matrix is symmetric so just fill lower half
3609 mat_LS(0,0) += std::pow(delta_p1,2);
3610 mat_LS(1,0) += delta_p1*delta_p2;
3611 mat_LS(2,0) += delta_p1;
3612 mat_LS(1,1) += std::pow(delta_p2,2);
3613 mat_LS(2,1) += delta_p2;
3614 mat_LS(2,2) += 1;
3615
3616 v_LS(0) += delta_p1 * v;
3617 v_LS(1) += delta_p2 * v;
3618 v_LS(2) += v;
3619
3620 u_LS(0) += delta_p1 * u;
3621 u_LS(1) += delta_p2 * u;
3622 u_LS(2) += u;
3623 }
3624 }
3625 }
3626
3627 // Fill upperhalf of matrix
3628 mat_LS(0,1) = mat_LS(1,0);
3629 mat_LS(0,2) = mat_LS(2,0);
3630 mat_LS(1,2) = mat_LS(2,1);
3631
3632 // Solve for displacement gradients using Cholesky
3633 // decomposition since matrix should be symmetric positive
3634 // definite.
3635 auto mat_LS_linsolver = mat_LS.get_linsolver(LINSOLVER::CHOL);
3636 if (mat_LS_linsolver) {
3637 // Make copies of output since they will be scaled to
3638 // account for scalefactor
3639 auto dv_dp = mat_LS_linsolver.solve(v_LS);
3640 auto du_dp = mat_LS_linsolver.solve(u_LS);
3641
3642 // Must scale displacement gradients to account for the
3643 // scalefactor and units per pixel
3644 dv_dp = std::move(dv_dp) / disp.get_scalefactor() / units_per_pixel;
3645 du_dp = std::move(du_dp) / disp.get_scalefactor() / units_per_pixel;
3646
3647 // Now calculate strains based on configuration
3648 switch (perspective_type) {
3650 // This is Green-Lagrangian strain
3651 A_eyy(p1,p2) = 0.5*(2*dv_dp(0) + std::pow(du_dp(0),2) + std::pow(dv_dp(0),2));
3652 A_exy(p1,p2) = 0.5*(du_dp(0) + dv_dp(1) + du_dp(1)*du_dp(0) + dv_dp(1)*dv_dp(0));
3653 A_exx(p1,p2) = 0.5*(2*du_dp(1) + std::pow(du_dp(1),2) + std::pow(dv_dp(1),2));
3654 break;
3656 // This is Eulerian-Almansi strain
3657 A_eyy(p1,p2) = 0.5*(2*dv_dp(0) - std::pow(du_dp(0),2) - std::pow(dv_dp(0),2));
3658 A_exy(p1,p2) = 0.5*(du_dp(0) + dv_dp(1) - du_dp(1)*du_dp(0) - dv_dp(1)*dv_dp(0));
3659 A_exx(p1,p2) = 0.5*(2*du_dp(1) - std::pow(du_dp(1),2) - std::pow(dv_dp(1),2));
3660 break;
3661 }
3662 // Point is valid
3663 A_vp(p1,p2) = true;
3664 }
3665 }
3666 }
3667 }
3668 }
3669 // Must form union with valid points
3670 return { std::move(A_eyy), std::move(A_exy), std::move(A_exx), disp.get_roi().form_union(A_vp), disp.get_scalefactor() };
3671}
3672
3674 // Form empty strain_analysis_input then fill in values in accordance to how they are saved
3675 strain_analysis_input strain_input;
3676
3677 // Load DIC_input
3678 strain_input.DIC_input = DIC_analysis_input::load(is);
3679
3680 // Load DIC_output
3681 strain_input.DIC_output = DIC_analysis_output::load(is);
3682
3683 // Load subregion type
3684 is.read(reinterpret_cast<char*>(&strain_input.subregion_type), std::streamsize(sizeof(SUBREGION)));
3685
3686 // Load radius
3687 is.read(reinterpret_cast<char*>(&strain_input.r), std::streamsize(sizeof(difference_type)));
3688
3689 return strain_input;
3690}
3691
3693 // Form stream
3694 std::ifstream is(filename.c_str(), std::ios::in | std::ios::binary);
3695 if (!is.is_open()) {
3696 throw std::invalid_argument("Could not open " + filename + " for loading strain_analysis_input.");
3697 }
3698
3699 // Form strain_analysis_input using stream static factory method
3700 auto strain_input = strain_analysis_input::load(is);
3701
3702 // Close stream
3703 is.close();
3704
3705 return strain_input;
3706}
3707
3708void save(const strain_analysis_input &strain_input, std::ofstream &os) {
3710
3711 // Save DIC_input -> DIC_output -> subregion_type -> r
3712
3713 save(strain_input.DIC_input, os);
3714
3715 save(strain_input.DIC_output, os);
3716
3717 os.write(reinterpret_cast<const char*>(&strain_input.subregion_type), std::streamsize(sizeof(SUBREGION)));
3718
3719 os.write(reinterpret_cast<const char*>(&strain_input.r), std::streamsize(sizeof(difference_type)));
3720}
3721
3722void save(const strain_analysis_input &strain_input, const std::string &filename) {
3723 // Form stream
3724 std::ofstream os(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
3725 if (!os.is_open()) {
3726 throw std::invalid_argument("Could not open " + filename + " for saving strain_analysis_input.");
3727 }
3728
3729 // Save strain_input into stream
3730 save(strain_input, os);
3731
3732 // Close stream
3733 os.close();
3734}
3735
3737 // Form empty strain_analysis_output then fill in values in accordance to how they are saved
3738 strain_analysis_output strain_output;
3739
3740 // Load strains
3741 difference_type num_strains = 0;
3742 is.read(reinterpret_cast<char*>(&num_strains), std::streamsize(sizeof(difference_type)));
3743 strain_output.strains.resize(num_strains);
3744 for (auto &strain : strain_output.strains) {
3745 strain = Strain2D::load(is);
3746 }
3747
3748 return strain_output;
3749}
3750
3752 // Form stream
3753 std::ifstream is(filename.c_str(), std::ios::in | std::ios::binary);
3754 if (!is.is_open()) {
3755 throw std::invalid_argument("Could not open " + filename + " for loading strain_analysis_output.");
3756 }
3757
3758 // Form strain_analysis_output using stream static factory method
3759 auto strain_output = strain_analysis_output::load(is);
3760
3761 // Close stream
3762 is.close();
3763
3764 return strain_output;
3765}
3766
3767void save(const strain_analysis_output &strain_output, std::ofstream &os) {
3769
3770 // Save strains
3771 difference_type num_strains = strain_output.strains.size();
3772 os.write(reinterpret_cast<const char*>(&num_strains), std::streamsize(sizeof(difference_type)));
3773 for (const auto &strain : strain_output.strains) {
3774 save(strain, os);
3775 }
3776}
3777
3778void save(const strain_analysis_output &strain_output, const std::string &filename) {
3779 // Form stream
3780 std::ofstream os(filename.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
3781 if (!os.is_open()) {
3782 throw std::invalid_argument("Could not open " + filename + " for saving strain_analysis_output.");
3783 }
3784
3785 // Save strain_output into stream
3786 save(strain_output, os);
3787
3788 // Close stream
3789 os.close();
3790}
3791
3793 typedef ROI2D::difference_type difference_type;
3794
3795 // Initialize output
3796 strain_analysis_output strain_output;
3797
3798 // Cycle over each displacement field and calculate corresponding strains
3799 NLOG_INFO << std::endl << "Processing strain fields...";
3800 for (difference_type disp_idx = 0; disp_idx < difference_type(strain_input.DIC_output.disps.size()); ++disp_idx) {
3801 NLOG_INFO << "Strain field " << disp_idx+1 << " of " << strain_input.DIC_output.disps.size() << ".";
3802 // Calculate strain field for this Disp2D
3803 strain_output.strains.push_back(LS_strain(strain_input.DIC_output.disps[disp_idx],
3804 strain_input.DIC_output.perspective_type,
3805 strain_input.DIC_output.units_per_pixel,
3806 strain_input.subregion_type,
3807 strain_input.r));
3808 }
3809
3810 return strain_output;
3811}
3812
3813// Interface functions for viewing and saving data ---------------------------//
3814namespace details {
3815 cv::Mat cv_ncorr_data_over_img(const Image2D &img,
3816 const Data2D &data,
3817 double alpha,
3818 double min_data,
3819 double max_data,
3820 bool enable_colorbar = true,
3821 bool enable_axes = true,
3822 bool enable_scalebar = true,
3823 const std::string &units = "pixels",
3824 double units_per_pixel = 1.0,
3825 double num_units = -1.0,
3826 double font_size = 1.0,
3827 ROI2D::difference_type num_tick_marks = 11,
3828 int colormap = cv::COLORMAP_JET) {
3829 typedef ROI2D::difference_type difference_type;
3830
3831 // Data2D's used with Ncorr interface functions use the p1 direction for
3832 // the y-axis (i.e. pointing downwards) and the p2 direction for the
3833 // x-axis (i.e pointing rightwards) with the origin at the top-left.
3834 // This dictates how the axes are displayed and correspond to the data.
3835 // For general Data2D's x and y axis have no assumptions about their
3836 // directions, so disable axes if general Data2D inputs are used.
3837
3838 if (alpha < 0 || alpha > 1) {
3839 throw std::invalid_argument("alpha input for cv_ncorr_data_over_img() must be between 0 and 1.");
3840 }
3841
3842 if (units_per_pixel <= 0) {
3843 throw std::invalid_argument("units_per_pixel input for cv_ncorr_data_over_img() must be greater than 0.");
3844 }
3845
3846 if (num_units != -1 && num_units <= 0) {
3847 throw std::invalid_argument("num_units input for cv_ncorr_data_over_img() must be greater than 0.");
3848 }
3849
3850 if (font_size <= 0) {
3851 throw std::invalid_argument("font_size input for cv_ncorr_data_over_img() must be greater than 0.");
3852 }
3853
3854 if (num_tick_marks < 2) {
3855 throw std::invalid_argument("num_tick_marks input for cv_ncorr_data_over_img() must be greater than or equal to 2.");
3856 }
3857
3858 // Set up image first - must resize it to the data
3859 auto A_img = img.get_gs();
3860 cv::Mat cv_img = get_cv_img(A_img, min(A_img), max(A_img));
3861 cv::resize(cv_img, cv_img, { int(data.data_width()), int(data.data_height()) });
3862
3863 // Set up data - must apply color map.
3864 cv::Mat cv_data = get_cv_img(data.get_array(), min_data, max_data);
3865 cv::applyColorMap(cv_data, cv_data, colormap);
3866
3867 // Set up plot by drawing the image with data over it
3868 difference_type border = 20;
3869 difference_type plot_height = data.data_height() + 2*border;
3870 difference_type plot_width = data.data_width() + 2*border;
3871 cv::Mat cv_plot(plot_height, plot_width, CV_8UC3, cv::Vec3b(255,255,255)); // CV_8UC3 means 3-channel image
3872 for (difference_type p2 = border; p2 < data.data_width() + border; ++p2) {
3873 difference_type p2_data = p2 - border;
3874 for (difference_type p1 = border; p1 < data.data_height() + border; ++p1) {
3875 difference_type p1_data = p1 - border;
3876
3877 // Get data values
3878 auto data_rgb = cv_data.at<cv::Vec3b>(p1_data,p2_data);
3879 auto img_gs = cv_img.at<uchar>(p1_data,p2_data);
3880
3881 if (data.get_roi()(p1_data,p2_data)) {
3882 // Set data overlay if point is within ROI
3883 cv_plot.at<cv::Vec3b>(p1,p2) = cv::Vec3b(img_gs*(1-alpha) + alpha*data_rgb.val[0],
3884 img_gs*(1-alpha) + alpha*data_rgb.val[1],
3885 img_gs*(1-alpha) + alpha*data_rgb.val[2]);
3886 } else {
3887 // Just draw background image
3888 cv_plot.at<cv::Vec3b>(p1,p2) = cv::Vec3b(img_gs, img_gs, img_gs);
3889 }
3890 }
3891 }
3892
3893 if (enable_colorbar) {
3894 // Create color bar (if enabled) and horizontally concatenate it
3895 // with cv_plot.
3896
3897 // These are opencv parameters for text drawing
3898 auto font_face = cv::FONT_HERSHEY_PLAIN;
3899 int font_thickness = 1;
3900 int baseline = 0;
3901
3902 // Colorbar parameters - note that division by (num_tick_marks-1) is
3903 // safe because num_tick_marks has been checked to ensure it is
3904 // greater than or equal to 2.
3905 difference_type num_chars = 8; // number of chars printed for colorbar tick labels
3906 difference_type text_offset_left = 5;
3907 difference_type colorbar_width = 20;
3908 // Set colorbar background width based on widest string width
3909 difference_type colorbar_bg_width = 0;
3910 for (difference_type num_mark = 0; num_mark < num_tick_marks; ++num_mark) {
3911 auto text_width = cv::getTextSize(std::to_string(double(num_tick_marks-num_mark-1)/(num_tick_marks-1) * (min_data-max_data) + max_data).substr(0,num_chars), font_face, 0.75*font_size, font_thickness, &baseline).width;
3912 if (text_width > colorbar_bg_width) {
3913 colorbar_bg_width = text_width;
3914 }
3915 }
3916 colorbar_bg_width += colorbar_width + text_offset_left + border; // Finish width to account for spacing and offsets
3917
3918 // Draw the color portion of colorbar first and then apply colormap
3919 cv::Mat cv_colorbar(plot_height, colorbar_bg_width, cv::DataType<uchar>::type);
3920 for (difference_type p2 = 0; p2 < colorbar_width; ++p2) {
3921 for (difference_type p1 = border; p1 < data.data_height() + border; ++p1) {
3922 // Applies value such that color will be max at the top of
3923 // the colorbar after the colormap is applied.
3924 cv_colorbar.at<uchar>(p1,p2) = double((data.data_height() + border - 1) - p1)/(data.data_height()-1) * 255;
3925 }
3926 }
3927 cv::applyColorMap(cv_colorbar, cv_colorbar, colormap);
3928
3929 // Paint region outside colorbar white
3930 cv_colorbar(cv::Range(0,border),cv::Range::all()) = cv::Vec3b(255,255,255);
3931 cv_colorbar(cv::Range(data.data_height()+border,data.data_height()+2*border),cv::Range::all()) = cv::Vec3b(255,255,255);
3932 cv_colorbar(cv::Range::all(),cv::Range(colorbar_width,colorbar_bg_width)) = cv::Vec3b(255,255,255);
3933
3934 // Paint black border inside colorbar
3935 cv_colorbar(cv::Range(border, border+1),cv::Range(0,colorbar_width)) = cv::Vec3b(0,0,0);
3936 cv_colorbar(cv::Range(data.data_height()+border-1,data.data_height()+border),cv::Range(0,colorbar_width)) = cv::Vec3b(0,0,0);
3937 cv_colorbar(cv::Range(border,data.data_height()+border),cv::Range(0,1)) = cv::Vec3b(0,0,0);
3938 cv_colorbar(cv::Range(border,data.data_height()+border),cv::Range(colorbar_width-1,colorbar_width)) = cv::Vec3b(0,0,0);
3939
3940 // Paint tick-marks and corresponding labels
3941 difference_type tick_mark_width = 4;
3942 for (difference_type num_mark = 0; num_mark < num_tick_marks; ++num_mark) {
3943 difference_type p1 = (num_mark*(data.data_height()-1))/(num_tick_marks-1) + border;
3944
3945 // Paint left mark, then right mark
3946 cv_colorbar(cv::Range(p1, p1+1),cv::Range(0,tick_mark_width)) = cv::Vec3b(0,0,0);
3947 cv_colorbar(cv::Range(p1, p1+1),cv::Range(colorbar_width-tick_mark_width,colorbar_width)) = cv::Vec3b(0,0,0);
3948
3949 // Draw tick mark value
3950 std::string tick_mark_label_str = std::to_string(num_mark*(min_data-max_data)/(num_tick_marks-1) + max_data).substr(0,num_chars);
3951 auto text_size = cv::getTextSize(tick_mark_label_str, font_face, 0.75*font_size, font_thickness, &baseline);
3952 cv::putText(cv_colorbar,
3953 tick_mark_label_str,
3954 cv::Point(colorbar_width + text_offset_left, num_mark*(data.data_height()-1)/(num_tick_marks-1) + border + text_size.height/2),
3955 font_face,
3956 0.75*font_size,
3957 cv::Scalar::all(0),
3958 font_thickness);
3959 }
3960
3961 // Horizontally concatenate with cv_plot
3962 cv::hconcat(cv_plot, cv_colorbar, cv_plot);
3963 }
3964
3965 if (enable_axes) {
3966 // axes_length is the longest dimension of the background axes
3967 difference_type axes_length = 0.25 * std::min(data.data_height(), data.data_width());
3968
3969 // background axes is painted black
3970 cv::Point bg_axes_pts[1][8];
3971 bg_axes_pts[0][0] = cv::Point(border, border);
3972 bg_axes_pts[0][1] = cv::Point(border, border+axes_length);
3973 bg_axes_pts[0][2] = cv::Point(border+0.20*axes_length, border+0.80*axes_length);
3974 bg_axes_pts[0][3] = cv::Point(border+0.10*axes_length, border+0.80*axes_length);
3975 bg_axes_pts[0][4] = cv::Point(border+0.10*axes_length, border+0.10*axes_length);
3976 bg_axes_pts[0][5] = cv::Point(border+0.80*axes_length, border+0.10*axes_length);
3977 bg_axes_pts[0][6] = cv::Point(border+0.80*axes_length, border+0.20*axes_length);
3978 bg_axes_pts[0][7] = cv::Point(border+axes_length, border);
3979 const cv::Point* pts1[1] = { bg_axes_pts[0] };
3980 int npts1[] = { 8 };
3981 cv::fillPoly(cv_plot, pts1, npts1, 1, cv::Scalar(255,255,255));
3982
3983 // foreground axes is painted white over the black background axes
3984 cv::Point fg_axes_pts[1][8];
3985 fg_axes_pts[0][0] = cv::Point(border, border);
3986 fg_axes_pts[0][1] = cv::Point(border, border+0.95*axes_length);
3987 fg_axes_pts[0][2] = cv::Point(border+0.125*axes_length, border+0.825*axes_length);
3988 fg_axes_pts[0][3] = cv::Point(border+0.070*axes_length, border+0.825*axes_length);
3989 fg_axes_pts[0][4] = cv::Point(border+0.070*axes_length, border+0.070*axes_length);
3990 fg_axes_pts[0][5] = cv::Point(border+0.825*axes_length, border+0.070*axes_length);
3991 fg_axes_pts[0][6] = cv::Point(border+0.825*axes_length, border+0.125*axes_length);
3992 fg_axes_pts[0][7] = cv::Point(border+0.95*axes_length, border);
3993 const cv::Point* pts2[1] = { fg_axes_pts[0] };
3994 int npts2[] = { 8 };
3995 cv::fillPoly(cv_plot, pts2, npts2, 1, cv::Scalar(0,0,0));
3996
3997 // Draw Y and X labels
3998 difference_type label_border = 5;
3999 difference_type label_offset = 5;
4000 double label_alpha = 0.5;
4001
4002 // These are opencv parameters for text drawing
4003 auto font_face = cv::FONT_HERSHEY_PLAIN;
4004 int font_thickness = 1;
4005 int baseline = 0;
4006
4007 // Draw "Y" label
4008 std::string y_str = "Y";
4009 auto text_size_y = cv::getTextSize(y_str, font_face, font_size, font_thickness, &baseline);
4010 for (difference_type p2 = border+label_offset; p2 < border+label_offset+2.0*label_border+text_size_y.width; ++p2) {
4011 for (difference_type p1 = border+axes_length+label_offset; p1 < border+axes_length+label_offset+2.0*label_border+text_size_y.height; ++p1) {
4012 if (p1 >= 0 && p1 < cv_plot.rows &&
4013 p2 >= 0 && p2 < cv_plot.cols) {
4014 auto cv_plot_rgb = cv_plot.at<cv::Vec3b>(p1,p2);
4015
4016 cv_plot.at<cv::Vec3b>(p1,p2) = cv::Vec3b(label_alpha*cv_plot_rgb.val[0],
4017 label_alpha*cv_plot_rgb.val[1],
4018 label_alpha*cv_plot_rgb.val[2]);
4019 }
4020 }
4021 }
4022 cv::putText(cv_plot,
4023 y_str,
4024 cv::Point(border+label_offset+label_border, border+axes_length+label_offset+label_border+text_size_y.height),
4025 font_face,
4026 font_size,
4027 cv::Scalar::all(255),
4028 font_thickness);
4029
4030 // Draw "X" label
4031 std::string x_str = "X";
4032 auto text_size_x = cv::getTextSize(x_str, font_face, font_size, font_thickness, &baseline);
4033 for (difference_type p2 = border+axes_length+label_offset; p2 < border+axes_length+label_offset+2.0*label_border+text_size_x.width; ++p2) {
4034 for (difference_type p1 = border+label_offset; p1 < border+label_offset+2.0*label_border+text_size_x.height; ++p1) {
4035 if (p1 >= 0 && p1 < cv_plot.rows &&
4036 p2 >= 0 && p2 < cv_plot.cols) {
4037 auto cv_plot_rgb = cv_plot.at<cv::Vec3b>(p1,p2);
4038
4039 cv_plot.at<cv::Vec3b>(p1,p2) = cv::Vec3b(label_alpha*cv_plot_rgb.val[0],
4040 label_alpha*cv_plot_rgb.val[1],
4041 label_alpha*cv_plot_rgb.val[2]);
4042 }
4043 }
4044 }
4045 cv::putText(cv_plot,
4046 x_str,
4047 cv::Point(border+axes_length+label_offset+label_border, border+label_offset+label_border+text_size_x.height),
4048 font_face,
4049 font_size,
4050 cv::Scalar::all(255),
4051 font_thickness);
4052 }
4053
4054 if (enable_scalebar) {
4055 // Set scalebar dimensions
4056 // Both ternary branches resolve to difference_type. The second
4057 // branch performs the divisions in floating-point (because
4058 // units_per_pixel is double) and truncates at the final cast,
4059 // matching the assignment's target type.
4060 difference_type scalebar_width =
4061 (num_units == -1)
4062 ? (data.data_width() / 2)
4063 : static_cast<difference_type>(
4064 static_cast<double>(num_units) /
4065 units_per_pixel /
4066 static_cast<double>(data.get_scalefactor()));
4067 difference_type scalebar_height = 5;
4068
4069 // Set num_units if it isn't -1
4070 if (num_units == -1) {
4071 num_units = scalebar_width*units_per_pixel*data.get_scalefactor();
4072 }
4073
4074 // Form text first so we can use its size in determining the scalebar
4075 // bg height.
4076 difference_type decimal_places = 2;
4077 std::stringstream ss_scalebar;
4078 ss_scalebar << std::fixed << std::setprecision(std::numeric_limits<double>::digits10) << num_units;
4079 std::string scalebar_str_unformatted = ss_scalebar.str();
4080 auto idx_period = scalebar_str_unformatted.find(".");
4081 std::string scalebar_str = idx_period == std::string::npos ? scalebar_str_unformatted : scalebar_str_unformatted.substr(0, idx_period + decimal_places + 1) + " " + units;
4082
4083 // These are opencv parameters for text drawing
4084 auto font_face = cv::FONT_HERSHEY_PLAIN;
4085 int font_thickness = 1;
4086 int baseline = 0;
4087 auto text_size = cv::getTextSize(scalebar_str, font_face, font_size, font_thickness, &baseline);
4088
4089 // Draw background first
4090 difference_type scalebar_bg_border = 10;
4091 difference_type scalebar_bg_width = scalebar_width + 2*scalebar_bg_border;
4092 difference_type scalebar_bg_height = scalebar_height + 3*scalebar_bg_border + text_size.height;
4093 difference_type scalebar_bg_offset = 10;
4094 double scalebar_bg_alpha = 0.5;
4095 for (difference_type p2 = border+scalebar_bg_offset; p2 < border+scalebar_bg_offset+scalebar_bg_width; ++p2) {
4096 for (difference_type p1 = border+data.data_height()-scalebar_bg_offset-scalebar_bg_height; p1 < border+data.data_height()-scalebar_bg_offset; ++p1) {
4097 if (p1 >= 0 && p1 < cv_plot.rows &&
4098 p2 >= 0 && p2 < cv_plot.cols) {
4099 auto cv_plot_rgb = cv_plot.at<cv::Vec3b>(p1,p2);
4100
4101 cv_plot.at<cv::Vec3b>(p1,p2) = cv::Vec3b(scalebar_bg_alpha*cv_plot_rgb.val[0],
4102 scalebar_bg_alpha*cv_plot_rgb.val[1],
4103 scalebar_bg_alpha*cv_plot_rgb.val[2]);
4104 }
4105 }
4106 }
4107
4108 // Draw scalebar
4109 for (difference_type p2 = border+scalebar_bg_offset+scalebar_bg_border; p2 < border+scalebar_bg_offset+scalebar_bg_border+scalebar_width; ++p2) {
4110 for (difference_type p1 = border+data.data_height()-scalebar_bg_offset-scalebar_bg_border-scalebar_height; p1 < border+data.data_height()-scalebar_bg_offset-scalebar_bg_border; ++p1) {
4111 if (p1 >= 0 && p1 < cv_plot.rows &&
4112 p2 >= 0 && p2 < cv_plot.cols) {
4113 cv_plot.at<cv::Vec3b>(p1,p2) = cv::Vec3b(255,255,255);
4114 }
4115 }
4116 }
4117
4118 // Draw scalebar text
4119 cv::putText(cv_plot,
4120 scalebar_str,
4121 cv::Point(border+scalebar_bg_offset+scalebar_bg_width/2.0-text_size.width/2.0, border+data.data_height()-scalebar_bg_offset-2*scalebar_bg_border-scalebar_height),
4122 font_face,
4123 font_size,
4124 cv::Scalar::all(255),
4125 font_thickness);
4126 }
4127
4128 return cv_plot;
4129 }
4130}
4131
4133 // Set parameters
4134 double min_data = 0, max_data = 0;
4135 Array2D<double> data_values = data.get_array()(data.get_roi().get_mask());
4136 if (!data_values.empty()) {
4137 min_data = prctile(data_values,0.01);
4138 max_data = prctile(data_values,0.99);
4139 }
4140
4141 // Get cv style matrix and show it - disable scalebar for simplicity since
4142 // that requires units and unit conversion parameters and this function is
4143 // mainly used for debugging purposes.
4144 auto cv_img = details::cv_ncorr_data_over_img(img,
4145 data,
4146 0.5, // Alpha
4147 min_data,
4148 max_data,
4149 true, // enable_colorbar
4150 true, // enable_axes
4151 false); // enable_scalebar
4152 cv::imshow("Ncorr data", cv_img);
4153 delay == -1 ? cv::waitKey() : cv::waitKey(delay);
4154}
4155
4156void save_ncorr_data_over_img(const std::string &filename,
4157 const Image2D &img,
4158 const Data2D &data,
4159 double alpha,
4160 double min_data,
4161 double max_data,
4162 bool enable_colorbar,
4163 bool enable_axes,
4164 bool enable_scalebar,
4165 const std::string &units,
4166 double units_per_pixel,
4167 double num_units,
4168 double font_size,
4169 ROI2D::difference_type num_tick_marks,
4170 int colormap) {
4171 // Get cv style matrix and save it
4172 auto cv_data_img = details::cv_ncorr_data_over_img(img,
4173 data,
4174 alpha,
4175 min_data,
4176 max_data,
4177 enable_colorbar,
4178 enable_axes,
4179 enable_scalebar,
4180 units,
4181 units_per_pixel,
4182 num_units,
4183 font_size,
4184 num_tick_marks,
4185 colormap);
4186 cv::imwrite(filename, cv_data_img);
4187}
4188
4189void save_ncorr_data_over_img_video(const std::string &filename,
4190 const std::vector<Image2D> &imgs,
4191 const std::vector<Data2D> &data,
4192 double alpha,
4193 double fps,
4194 double min_data,
4195 double max_data,
4196 bool enable_colorbar,
4197 bool enable_axes,
4198 bool enable_scalebar,
4199 const std::string &units,
4200 double units_per_pixel,
4201 double num_units,
4202 double font_size,
4203 ROI2D::difference_type num_tick_marks,
4204 int colormap,
4205 double end_delay,
4206 int fourcc) {
4207 typedef ROI2D::difference_type difference_type;
4208
4209 // Note that if only 1 image was input, interpret this as using the same
4210 // image for each Data2D. If not, use the img the data corresponds to.
4211
4212 if (imgs.size() != 1 && imgs.size() != data.size()) {
4213 // Number of images must either be 1, or equal to the number of input data
4214 throw std::invalid_argument("Number of images used in save_data_over_img_video() must either be 1, or equal to the number of input data.");
4215 }
4216
4217 if (data.empty()) {
4218 throw std::invalid_argument("Number of Data2D used in save_data_over_img_video() must be greater than or equal to 1.");
4219 }
4220
4221 // All data must have the same size
4222 for (difference_type data_idx = 1; data_idx < difference_type(data.size()); ++data_idx) {
4223 if (data[data_idx].data_height() != data.front().data_height() ||
4224 data[data_idx].data_width() != data.front().data_width()) {
4225 throw std::invalid_argument("Attempted to use save_data_over_img_video() with data of differing sizes. All data must be the same size.");
4226 }
4227 }
4228
4229 if (fps <= 0) {
4230 throw std::invalid_argument("fps input for save_data_over_img_video() must be greater than 0.");
4231 }
4232
4233 if (end_delay < 0) {
4234 throw std::invalid_argument("end_delay input for save_data_over_img_video() must be greater than or equal to 0.");
4235 }
4236
4237 // All other parameters will be checked when calling cv_ncorr_data_over_img()
4238
4239 NLOG_INFO << std::endl << "Saving video: " << filename << "...";
4240
4241 // Initialize video
4242 cv::VideoWriter output_video;
4243
4244 // Cycle over data and save
4245 for (difference_type data_idx = 0; data_idx < difference_type(data.size()); ++data_idx) {
4246 NLOG_INFO << "Frame " << data_idx+1 << " of " << data.size() << ".";
4247
4248 auto cv_data_img = details::cv_ncorr_data_over_img(imgs.size() == 1 ? imgs.front() : imgs[data_idx],
4249 data[data_idx],
4250 alpha,
4251 min_data,
4252 max_data,
4253 enable_colorbar,
4254 enable_axes,
4255 enable_scalebar,
4256 units,
4257 units_per_pixel,
4258 num_units,
4259 font_size,
4260 num_tick_marks,
4261 colormap);
4262
4263 if (data_idx == 0) {
4264 // Open video file - do this here because the size of the cv_data_img
4265 // depends on its inputs and the size is needed to open the video.
4266 output_video.open(filename,
4267 fourcc,
4268 fps,
4269 { cv_data_img.cols, cv_data_img.rows },
4270 true);
4271
4272 if (!output_video.isOpened()) {
4273 throw std::invalid_argument("Cannot open video file: " + filename + " for save_data_over_img_video().");
4274 }
4275 }
4276
4277 output_video << cv_data_img;
4278 }
4279
4280 // Add the last frame again multiple times to provide a delay
4281 for (difference_type idx = 0; idx < fps*end_delay; ++idx) {
4282 output_video << details::cv_ncorr_data_over_img(imgs.size() == 1 ? imgs.front() : imgs.back(),
4283 data.back(),
4284 alpha,
4285 min_data,
4286 max_data,
4287 enable_colorbar,
4288 enable_axes,
4289 enable_scalebar,
4290 units,
4291 units_per_pixel,
4292 num_units,
4293 font_size,
4294 num_tick_marks,
4295 colormap);
4296 }
4297}
4298
4299void save_DIC_video(const std::string &filename,
4300 const DIC_analysis_input &DIC_input,
4301 const DIC_analysis_output &DIC_output,
4302 DISP disp_type,
4303 double alpha,
4304 double fps,
4305 double min_disp,
4306 double max_disp,
4307 bool enable_colorbar,
4308 bool enable_axes,
4309 bool enable_scalebar,
4310 double num_units,
4311 double font_size,
4312 ROI2D::difference_type num_tick_marks,
4313 int colormap,
4314 double end_delay,
4315 int fourcc) {
4316 typedef ROI2D::difference_type difference_type;
4317
4318 // Use get_disp() function to obtain the specified displacement field
4319 std::function<const Data2D&(const Disp2D&)> get_disp;
4320 switch (disp_type) {
4321 case DISP::V :
4322 get_disp = &Disp2D::get_v;
4323 break;
4324 case DISP::U :
4325 get_disp = &Disp2D::get_u;
4326 break;
4327 }
4328
4329 // Set min and max if they are NaN. They are set so that all data fits
4330 // approximately between these bounds. This assumes maxima occur at the
4331 // first or last displacement plot for simplicity.
4332 Array2D<double> data_values_first = get_disp(DIC_output.disps.front()).get_array()(DIC_output.disps.front().get_roi().get_mask());
4333 Array2D<double> data_values_last = get_disp(DIC_output.disps.back()).get_array()(DIC_output.disps.back().get_roi().get_mask());
4334 if (std::isnan(min_disp) && !data_values_first.empty() && !data_values_last.empty()) {
4335 min_disp = std::min(prctile(data_values_first,0.01), prctile(data_values_last,0.01));
4336 }
4337 if (std::isnan(max_disp) && !data_values_first.empty() && !data_values_last.empty()) {
4338 max_disp = std::max(prctile(data_values_first,0.99), prctile(data_values_last,0.99));
4339 }
4340
4341 // Get data plots
4342 std::vector<Data2D> data;
4343 for (difference_type disp_idx = 0; disp_idx < difference_type(DIC_output.disps.size()); ++disp_idx) {
4344 data.push_back(get_disp(DIC_output.disps[disp_idx]));
4345 }
4346
4347 // Get imgs - this depends on perspective
4348 std::vector<Image2D> imgs;
4349 switch (DIC_output.perspective_type) {
4351 imgs.push_back(DIC_input.imgs.front());
4352 break;
4353 case PERSPECTIVE::EULERIAN :
4354 for (difference_type img_idx = 1; img_idx < difference_type(DIC_input.imgs.size()); ++img_idx) { // Starts from 1
4355 imgs.push_back(DIC_input.imgs[img_idx]);
4356 }
4357 break;
4358 }
4359
4360 // Save the video
4362 imgs,
4363 data,
4364 alpha,
4365 fps,
4366 min_disp,
4367 max_disp,
4368 enable_colorbar,
4369 enable_axes,
4370 enable_scalebar,
4371 DIC_output.units,
4372 DIC_output.units_per_pixel,
4373 num_units,
4374 font_size,
4375 num_tick_marks,
4376 colormap,
4377 end_delay,
4378 fourcc);
4379}
4380
4381void save_strain_video(const std::string &filename,
4382 const strain_analysis_input &strain_input,
4383 const strain_analysis_output &strain_output,
4384 STRAIN strain_type,
4385 double alpha,
4386 double fps,
4387 double min_strain,
4388 double max_strain,
4389 bool enable_colorbar,
4390 bool enable_axes,
4391 bool enable_scalebar,
4392 double num_units,
4393 double font_size,
4394 ROI2D::difference_type num_tick_marks,
4395 int colormap,
4396 double end_delay,
4397 int fourcc) {
4398 typedef ROI2D::difference_type difference_type;
4399
4400 // Use get_strain function to obtain the specified strain field
4401 std::function<const Data2D&(const Strain2D&)> get_strain;
4402 switch (strain_type) {
4403 case STRAIN::EYY :
4404 get_strain = &Strain2D::get_eyy;
4405 break;
4406 case STRAIN::EXY :
4407 get_strain = &Strain2D::get_exy;
4408 break;
4409 case STRAIN::EXX :
4410 get_strain = &Strain2D::get_exx;
4411 break;
4412 }
4413
4414 // Set min and max if they are NaN. They are set so that all data fits
4415 // approximately between these bounds This assumes maxima occur at the
4416 // first or last strain plot for simplicity.
4417 Array2D<double> data_values_first = get_strain(strain_output.strains.front()).get_array()(strain_output.strains.front().get_roi().get_mask());
4418 Array2D<double> data_values_last = get_strain(strain_output.strains.back()).get_array()(strain_output.strains.back().get_roi().get_mask());
4419 if (std::isnan(min_strain) && !data_values_first.empty() && !data_values_last.empty()) {
4420 min_strain = std::min(prctile(data_values_first,0.01), prctile(data_values_last,0.01));
4421 }
4422 if (std::isnan(max_strain) && !data_values_first.empty() && !data_values_last.empty()) {
4423 max_strain = std::max(prctile(data_values_first,0.99), prctile(data_values_last,0.99));
4424 }
4425
4426 // Get data plots
4427 std::vector<Data2D> data;
4428 for (difference_type strain_idx = 0; strain_idx < difference_type(strain_output.strains.size()); ++strain_idx) {
4429 data.push_back(get_strain(strain_output.strains[strain_idx]));
4430 }
4431
4432 // Get imgs - this depends on perspective
4433 std::vector<Image2D> imgs;
4434 switch (strain_input.DIC_output.perspective_type) {
4436 imgs.push_back(strain_input.DIC_input.imgs.front());
4437 break;
4438 case PERSPECTIVE::EULERIAN :
4439 for (difference_type img_idx = 1; img_idx < difference_type(strain_input.DIC_input.imgs.size()); ++img_idx) { // Starts from 1
4440 imgs.push_back(strain_input.DIC_input.imgs[img_idx]);
4441 }
4442 break;
4443 }
4444
4445 // Save the video
4447 imgs,
4448 data,
4449 alpha,
4450 fps,
4451 min_strain,
4452 max_strain,
4453 enable_colorbar,
4454 enable_axes,
4455 enable_scalebar,
4456 strain_input.DIC_output.units,
4457 strain_input.DIC_output.units_per_pixel,
4458 num_units,
4459 font_size,
4460 num_tick_marks,
4461 colormap,
4462 end_delay,
4463 fourcc);
4464}
4465
4466
4467// Seed-based parallel DIC analysis implementation ---------------------------//
4468
4469// Compute displacements using precomputed seed parameters
4471 const details::subregion_nloptimizer &sr_nloptimizer,
4472 const ROI2D& roi_reduced,
4473 const SeedParams& seedparams,
4474 ROI2D::difference_type scalefactor,
4475 double cutoff_corrcoef,
4476 ROI2D::difference_type region_idx,
4477 bool debug
4478) {
4479
4480 // Perform RGDIC with manual seed (reliability-guided expansion from seed point)
4481 auto H = roi_reduced.height();
4482 auto W = roi_reduced.width();
4483
4484 // cutoff_delta_disp is a cutoff for the allowable change in displacement
4485 // between analyzed points. Generally if a point is analyzed incorrectly,
4486 // it will result in a large change in displacement, so just filter these
4487 // points out.
4488 double cutoff_delta_disp = scalefactor;
4489
4490 // -----------------------------------------------------------------------//
4491 // Perform reliability guided DIC without threading ----------------------//
4492 // -----------------------------------------------------------------------//
4493 // Initialize buffers - set A_ap to all points in ROI
4494 // Create arrays for displacement computation
4495 Array2D<double> A_v(H, W);
4496 Array2D<double> A_u(H, W);
4497 Array2D<double> A_cc(H, W);
4498 Array2D<bool> A_ap(roi_reduced.get_mask()); // Active Points
4499 Array2D<bool> A_vp(H, W); // Valid Points
4500
4501 // Cycle over regions and perform RGDIC sequentially
4502 Array2D<double> params_buf(10, 1); // buffer for nloptimizer
4503
4504 // Get seed params for queue
4505 // Must match SeedParams::to_array() convention:
4506 // params = {p1(y), p2(x), v, u, dv_dp1(dv_dy), dv_dp2(dv_dx), du_dp1(du_dy), du_dp2(du_dx), corr_coef, diff_norm}
4507 Array2D<double> seed_params(10, 1);
4508 seed_params(0) = seedparams.y; // p1 (row)
4509 seed_params(1) = seedparams.x; // p2 (col)
4510 seed_params(2) = seedparams.v;
4511 seed_params(3) = seedparams.u;
4512 seed_params(4) = seedparams.dv_dy; // dv_dp1
4513 seed_params(5) = seedparams.dv_dx; // dv_dp2
4514 seed_params(6) = seedparams.du_dy; // du_dp1
4515 seed_params(7) = seedparams.du_dx; // du_dp2
4516 seed_params(8) = seedparams.corrcoef;
4517 seed_params(9) = 0;
4518
4519 if (!seed_params.empty()) {
4520 A_ap(seed_params(0) / scalefactor, seed_params(1) / scalefactor) = false;
4521
4522 // Form queue - make it a priority queue such that the lowest
4523 // correlation coefficient values are processed first.
4524 auto comp = [](const Array2D<double> &a, const Array2D<double> &b ) { return a(8) > b(8); };
4525 std::priority_queue<Array2D<double>, std::vector<Array2D<double>>, std::function<bool(const Array2D<double>&,const Array2D<double>&)>> queue(comp);
4526
4527 // Perform flood fill around seed
4528 queue.push(seed_params);
4529 while (!queue.empty()) {
4530 // 1) pop info from queue
4531 auto queue_params = std::move(queue.top()); queue.pop();
4532
4533 // 2) Validate and store information
4534 A_vp(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = true;
4535 A_v(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(2);
4536 A_u(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(3);
4537 A_cc(queue_params(0) / scalefactor, queue_params(1) / scalefactor) = queue_params(8);
4538
4539 // 3) Analyze four surrounding points - up, down, left right;
4540 details::analyze_point(queue_params, -scalefactor, 0, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
4541 details::analyze_point(queue_params, scalefactor, 0, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
4542 details::analyze_point(queue_params, 0, -scalefactor, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
4543 details::analyze_point(queue_params, 0, scalefactor, roi_reduced.get_nlinfo(region_idx), scalefactor, sr_nloptimizer, cutoff_corrcoef, cutoff_delta_disp, queue, A_ap, params_buf);
4544 }
4545 }
4546
4547 // Go back over plot and clear any datapoints that have large displacement
4548 // jumps. Not all are cleared through the RGDIC algorithm because of the
4549 // way the search is conducted.
4550 auto A_vp_buf = A_vp; // Make copy of valid points since it gets overwritten
4551 for (ROI2D::difference_type region_idx = 0; region_idx < roi_reduced.size_regions(); ++region_idx) {
4552 for (ROI2D::difference_type nl_idx = 0; nl_idx < roi_reduced.get_nlinfo(region_idx).nodelist.width(); ++nl_idx) {
4553 ROI2D::difference_type p2 = nl_idx + roi_reduced.get_nlinfo(region_idx).left_nl;
4554 for (ROI2D::difference_type np_idx = 0; np_idx < roi_reduced.get_nlinfo(region_idx).noderange(nl_idx); np_idx += 2) {
4555 ROI2D::difference_type np_top = roi_reduced.get_nlinfo(region_idx).nodelist(np_idx,nl_idx);
4556 ROI2D::difference_type np_bottom = roi_reduced.get_nlinfo(region_idx).nodelist(np_idx + 1,nl_idx);
4557 for (ROI2D::difference_type p1 = np_top; p1 <= np_bottom; ++p1) {
4558 if ((roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1-1,p2) && A_vp_buf(p1-1,p2) && std::sqrt(std::pow(A_v(p1-1,p2) - A_v(p1,p2),2) + std::pow(A_u(p1-1,p2) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
4559 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1+1,p2) && A_vp_buf(p1+1,p2) && std::sqrt(std::pow(A_v(p1+1,p2) - A_v(p1,p2),2) + std::pow(A_u(p1+1,p2) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
4560 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1,p2-1) && A_vp_buf(p1,p2-1) && std::sqrt(std::pow(A_v(p1,p2-1) - A_v(p1,p2),2) + std::pow(A_u(p1,p2-1) - A_u(p1,p2),2)) > cutoff_delta_disp) ||
4561 (roi_reduced.get_nlinfo(region_idx).in_nlinfo(p1,p2+1) && A_vp_buf(p1,p2+1) && std::sqrt(std::pow(A_v(p1,p2+1) - A_v(p1,p2),2) + std::pow(A_u(p1,p2+1) - A_u(p1,p2),2)) > cutoff_delta_disp)) {
4562 A_vp(p1,p2) = false;
4563 }
4564 }
4565 }
4566 }
4567 }
4568
4569 // Must form union with valid points
4570 auto roi_valid = roi_reduced.form_union(A_vp);
4571 if (debug) {
4572 NLOG_DEBUG << "RGDIC completed with manual seed";
4573 }
4574 return Disp2D(std::move(A_v), std::move(A_u), std::move(A_cc), roi_valid, scalefactor);
4575}
4576
4577// Compute seed parameters for all frames with ROI updates
4578std::vector<SeedComputationData> compute_only_seed_points(
4579 const Array2D<double>& A_ref,
4580 const std::vector<Array2D<double>>& A_curs,
4581 const ROI2D& roi,
4582 ROI2D::difference_type scalefactor,
4583 INTERP interp_type,
4584 SUBREGION subregion_type,
4586 const std::vector<SeedParams>& seeds_by_region,
4587 double cutoff_corrcoef,
4588 ROI2D::difference_type region_idx,
4589 bool debug
4590) {
4591
4592 std::vector<SeedComputationData> selected_data;
4593
4594 if (debug) {
4595 NLOG_DEBUG << "\n=== Compute seed parameters for all frames with ROI updates ===> ";
4596 NLOG_DEBUG << "\n starting with " << seeds_by_region.size() << " seeds";
4597 }
4598
4599 // Start with initial reference and ROI
4600 Array2D<double> A_ref_current = A_ref;
4601 ROI2D roi_current = roi;
4602 std::vector<SeedParams> seeds_current = seeds_by_region;
4603
4604 size_t idx = 0;
4605 while (idx < A_curs.size()) {
4606 const Array2D<double>& A_cur = A_curs[idx];
4607
4608 if (debug) {
4609 NLOG_DEBUG << "\n=== Seeding analyzing frame " << (idx + 1) << " ===> ";
4610 }
4611
4612 // Get subregion nonlinear optimizer
4613 auto sr_nloptimizer = details::subregion_nloptimizer(A_ref_current, A_cur, roi_current, scalefactor, interp_type, subregion_type, r);
4614
4615 // Analyze seeds for this frame
4616 SeedAnalysisResult seed_results = analyze_seeds(
4617 sr_nloptimizer,
4618 A_ref_current,
4619 roi_current,
4620 seeds_current,
4621 r,
4622 50, // cutoff_iteration
4623 0.1, // cutoff_max_diffnorm
4624 0.5 // cutoff_max_corrcoef
4625 );
4626
4627 if (seed_results.success) {
4628 // Seed analysis successful - store data
4629 if (debug) {
4630 NLOG_DEBUG << "Successful!";
4631 }
4632
4633 selected_data.emplace_back(roi_current, seed_results.seeds, sr_nloptimizer);
4634 idx++;
4635 } else {
4636 // Seed analysis failed - update reference and propagate seeds
4637 if (debug) {
4638 NLOG_DEBUG << "Failed! Updating reference ===>";
4639 }
4640
4641 auto A_prev = A_curs[idx-1];
4642
4643 // Get subregion nonlinear optimizer from the last added selected_data
4644 auto prev_sr_nloptimizer = selected_data.back().sr_nloptimizer;
4645 auto prev_seedparams = selected_data.back().seed_params_by_region;
4646 auto prev_roi = selected_data.back().roi;
4647
4648 // Compute displacements between at the previous frame
4649 auto disps = compute_displacements(
4650 prev_sr_nloptimizer,
4651 prev_roi.reduce(scalefactor),
4652 prev_seedparams[region_idx],
4653 scalefactor,
4654 cutoff_corrcoef,
4655 region_idx,
4656 debug
4657 );
4658
4659 // Update reference image and ROI
4660 A_ref_current = A_prev;
4661 roi_current = update(prev_roi, disps, interp_type);
4662
4663 // Propagate seeds to track material deformation
4664 seeds_current = propagate_seeds(prev_seedparams, scalefactor);
4665
4666 if (debug) {
4667 NLOG_DEBUG << "Reference updated, seeds propagated <====";
4668 }
4669 }
4670 }
4671
4672 if (debug) {
4673 NLOG_DEBUG << "\nComputed seed parameters for " << selected_data.size() << " frames";
4674 }
4675
4676 return selected_data;
4677}
4678
4679// Update seed positions based on displacement (seed propagation)
4680std::vector<SeedParams> propagate_seeds(const std::vector<SeedParams>& seeds, ROI2D::difference_type spacing) {
4681 std::vector<SeedParams> propagated_seeds;
4682 propagated_seeds.reserve(seeds.size());
4683
4684 for (const auto& seed : seeds) {
4685 SeedParams new_seed = seed;
4686
4687 // Update position based on displacement, rounding to spaced grid
4688 // This matches the Matlab logic: pos + round(u/(spacing+1))*(spacing+1)
4689 new_seed.x = seed.x + std::round(seed.u / (spacing + 1)) * (spacing + 1);
4690 new_seed.y = seed.y + std::round(seed.v / (spacing + 1)) * (spacing + 1);
4691
4692 propagated_seeds.push_back(new_seed);
4693 }
4694
4695 return propagated_seeds;
4696}
4697
4698// Lightweight seed analysis for predicting reference updates
4700 const details::subregion_nloptimizer &sr_nloptimizer,
4701 const Array2D<double>& ref_gs,
4702 const ROI2D& roi,
4703 const std::vector<SeedParams>& seed_positions,
4705 int cutoff_iteration,
4706 double cutoff_max_diffnorm,
4707 double cutoff_max_corrcoef,
4708 bool debug
4709) {
4710
4711 SeedAnalysisResult result;
4712 result.seeds.reserve(seed_positions.size());
4713 result.convergence.reserve(seed_positions.size());
4714 result.success = true;
4715
4716 // Analyze each seed point using existing subregion optimizer
4717 int region_idx = 0;
4718 for (const auto& seed_pos : seed_positions) {
4719 // Check if seed is within valid bounds
4720 if (seed_pos.x < radius || seed_pos.x >= ref_gs.width() - radius ||
4721 seed_pos.y < radius || seed_pos.y >= ref_gs.height() - radius) {
4722 result.success = false;
4723 continue;
4724 }
4725
4726 // Check if seed is in ROI
4727 if (!roi.get_mask()(seed_pos.y, seed_pos.x)) {
4728 result.success = false;
4729 continue;
4730 }
4731
4732 // Initialize parameters for optimizer
4733 // params = {p1, p2, v, u, dv_dp1, dv_dp2, du_dp1, du_dp2, corr_coef, diff_norm}
4734 Array2D<double> params_init(10, 1);
4735 params_init(0) = seed_pos.y; // p1 in full resolution
4736 params_init(1) = seed_pos.x; // p2 in full resolution
4737 params_init(2) = 0.0; // v
4738 params_init(3) = 0.0; // u
4739 params_init(4) = 0.0; // dv_dp1
4740 params_init(5) = 0.0; // dv_dp2
4741 params_init(6) = 0.0; // du_dp1
4742 params_init(7) = 0.0; // du_dp2
4743 params_init(8) = 0.0; // corr_coef
4744 params_init(9) = 0.0; // diff_norm
4745
4746 // Run IC-GN optimization via global() method
4747 auto result_pair = sr_nloptimizer.global(params_init);
4748 const auto& params_guess = result_pair.first;
4749
4750 if (!result_pair.second) {
4751 if (debug) {
4752 NLOG_DEBUG << region_idx << ": Seed analysis failed at seed " << seed_pos.x << ", " << seed_pos.y << "=> global failed";
4753 }
4754 result.success = false;
4755 continue;
4756 }
4757
4758 // Run iterative-search
4759 auto result_iter = sr_nloptimizer(params_guess);
4760 const auto& params_result = result_iter.first;
4761
4762 if (!result_iter.second) {
4763 if (debug) {
4764 NLOG_DEBUG << region_idx << ": Seed analysis failed at seed " << seed_pos.x << ", " << seed_pos.y << "=> iterative search failed";
4765 }
4766 result.success = false;
4767 continue;
4768 }
4769
4770 // Extract optimized parameters from result
4771 SeedParams seed_result;
4772 seed_result.x = seed_pos.x;
4773 seed_result.y = seed_pos.y;
4774 seed_result.v = params_result(2);
4775 seed_result.u = params_result(3);
4776 seed_result.dv_dx = params_result(4);
4777 seed_result.dv_dy = params_result(5);
4778 seed_result.du_dx = params_result(6);
4779 seed_result.du_dy = params_result(7);
4780 seed_result.corrcoef = params_result(8);
4781
4782 // Extract convergence metrics
4783 SeedConvergence convergence;
4784 convergence.num_iterations = sr_nloptimizer.get_last_iteration_count(); // Actual count
4785 convergence.diffnorm = params_result(9);
4786
4787 result.seeds.push_back(seed_result);
4788 result.convergence.push_back(convergence);
4789
4790 // Check quality thresholds
4791 if (seed_result.corrcoef > cutoff_max_corrcoef || convergence.diffnorm > cutoff_max_diffnorm) {
4792 if (debug) {
4793 NLOG_DEBUG << region_idx << ": Seed analysis failed at seed " << seed_pos.x << ", " << seed_pos.y << "=> corrcoef: " << seed_result.corrcoef << ", diffnorm: " << convergence.diffnorm;
4794 }
4795 NLOG_DEBUG << result.success;
4796 result.success = false; //TODO: Solve this
4797 }
4798 region_idx++;
4799 }
4800
4801 return result;
4802}
4803
4804namespace {
4805
4806using matlab_difference_type = ROI2D::difference_type;
4807
4808struct matlab_seed_segment final {
4809 matlab_difference_type ref_idx = 0;
4810 std::vector<std::vector<SeedParams>> seeds_by_frame;
4811 std::vector<SeedParams> terminal_seeds;
4812};
4813
4814Array2D<double> matlab_make_seed_params_init(const SeedParams& seed) {
4815 Array2D<double> params_init(10, 1);
4816 params_init(0) = seed.y;
4817 params_init(1) = seed.x;
4818 params_init(2) = 0.0;
4819 params_init(3) = 0.0;
4820 params_init(4) = 0.0;
4821 params_init(5) = 0.0;
4822 params_init(6) = 0.0;
4823 params_init(7) = 0.0;
4824 params_init(8) = 0.0;
4825 params_init(9) = 0.0;
4826 return params_init;
4827}
4828
4829bool matlab_seed_matches_region(const ROI2D& roi_reduced,
4830 const SeedParams& seed,
4831 matlab_difference_type scalefactor,
4832 matlab_difference_type region_idx) {
4833 if (scalefactor <= 0) {
4834 return false;
4835 }
4836 const matlab_difference_type reduced_y = seed.y / scalefactor;
4837 const matlab_difference_type reduced_x = seed.x / scalefactor;
4838 const auto region_idx_pair = roi_reduced.get_region_idx(reduced_y, reduced_x);
4839 return region_idx_pair.first == region_idx;
4840}
4841
4842bool matlab_seed_positions_are_unique(const std::vector<SeedParams>& seeds,
4843 matlab_difference_type scalefactor) {
4844 for (std::size_t i = 0; i < seeds.size(); ++i) {
4845 for (std::size_t j = i + 1; j < seeds.size(); ++j) {
4846 if (seeds[i].x / scalefactor == seeds[j].x / scalefactor &&
4847 seeds[i].y / scalefactor == seeds[j].y / scalefactor) {
4848 return false;
4849 }
4850 }
4851 }
4852 return true;
4853}
4854
4855std::vector<SeedParams> matlab_propagate_seeds(const std::vector<SeedParams>& seeds,
4856 matlab_difference_type scalefactor) {
4857 std::vector<SeedParams> propagated_seeds;
4858 propagated_seeds.reserve(seeds.size());
4859
4860 for (const auto& seed : seeds) {
4861 SeedParams propagated = seed;
4862 propagated.x = seed.x + static_cast<matlab_difference_type>(std::round(seed.u / scalefactor) * scalefactor);
4863 propagated.y = seed.y + static_cast<matlab_difference_type>(std::round(seed.v / scalefactor) * scalefactor);
4864 propagated.u = 0.0;
4865 propagated.v = 0.0;
4866 propagated.du_dx = 0.0;
4867 propagated.du_dy = 0.0;
4868 propagated.dv_dx = 0.0;
4869 propagated.dv_dy = 0.0;
4870 propagated.corrcoef = 0.0;
4871 propagated_seeds.push_back(propagated);
4872 }
4873
4874 return propagated_seeds;
4875}
4876
4877matlab_seed_segment matlab_compute_seed_segment(const DIC_analysis_parallel_input& input,
4878 matlab_difference_type ref_idx,
4879 const ROI2D& roi_ref,
4880 const std::vector<SeedParams>& seeds_by_region,
4881 bool seeds_are_optimized) {
4883
4884 const auto& DIC_input = input.base_input;
4885 matlab_seed_segment segment;
4886 segment.ref_idx = ref_idx;
4887
4888 if (seeds_by_region.empty()) {
4889 return segment;
4890 }
4891
4892 auto roi_reduced = roi_ref.reduce(DIC_input.scalefactor);
4893 if (difference_type(seeds_by_region.size()) != roi_reduced.size_regions()) {
4894 throw std::invalid_argument("matlab_DIC_analysis requires seeds_by_region.size() to match the number of ROI regions.");
4895 }
4896
4897 std::vector<SeedParams> predicted_seeds = seeds_are_optimized ?
4898 matlab_propagate_seeds(seeds_by_region, DIC_input.scalefactor) :
4899 seeds_by_region;
4900
4901 if (!matlab_seed_positions_are_unique(predicted_seeds, DIC_input.scalefactor)) {
4902 throw std::invalid_argument("matlab_DIC_analysis received duplicate seed positions on the reduced grid.");
4903 }
4904
4905 segment.seeds_by_frame.reserve(DIC_input.imgs.size() - ref_idx - 1);
4906
4907 const auto A_ref = DIC_input.imgs[ref_idx].get_gs();
4908 for (difference_type cur_idx = ref_idx + 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
4909 const auto A_cur = DIC_input.imgs[cur_idx].get_gs();
4910 auto sr_nloptimizer = details::subregion_nloptimizer(
4911 A_ref,
4912 A_cur,
4913 roi_ref,
4914 DIC_input.scalefactor,
4915 DIC_input.interp_type,
4916 DIC_input.subregion_type,
4917 DIC_input.r
4918 );
4919
4920 std::vector<SeedParams> optimized_seeds;
4921 optimized_seeds.reserve(predicted_seeds.size());
4922 bool frame_success = matlab_seed_positions_are_unique(predicted_seeds, DIC_input.scalefactor);
4923
4924 for (difference_type region_idx = 0;
4925 frame_success && region_idx < difference_type(predicted_seeds.size());
4926 ++region_idx) {
4927 const auto& predicted_seed = predicted_seeds[region_idx];
4928 if (!matlab_seed_matches_region(roi_reduced, predicted_seed, DIC_input.scalefactor, region_idx)) {
4929 if (DIC_input.debug) {
4930 NLOG_DEBUG << "matlab_DIC_analysis: seed for region " << region_idx
4931 << " is outside its ROI at frame " << cur_idx << ".";
4932 }
4933 frame_success = false;
4934 break;
4935 }
4936
4937 auto params_init = matlab_make_seed_params_init(predicted_seed);
4938 auto global_result = sr_nloptimizer.global(params_init);
4939 if (!global_result.second) {
4940 if (DIC_input.debug) {
4941 NLOG_DEBUG << "matlab_DIC_analysis: global seed optimization failed for region "
4942 << region_idx << " at frame " << cur_idx << ".";
4943 }
4944 frame_success = false;
4945 break;
4946 }
4947
4948 SeedParams optimized_seed = SeedParams::from_array(global_result.first);
4949 const double diffnorm = global_result.first(9);
4950 const int num_iterations = sr_nloptimizer.get_last_iteration_count();
4951
4952 // MATLAB-style checks: corrcoef, diffnorm, and iteration saturation
4953 // MATLAB ncorr_abr_alg_seedanalysis.m line 85: (i > 0 && any([convergence_buffer.num_iterations] == cutoff_iteration))
4954 bool iteration_saturated = (cur_idx > ref_idx + 1) && (num_iterations >= 100); // Default cutoff_iterations = 100
4955
4956 if (optimized_seed.corrcoef > input.cutoff_max_corrcoef ||
4957 diffnorm > input.cutoff_max_diffnorm ||
4958 iteration_saturated ||
4959 !matlab_seed_matches_region(roi_reduced, optimized_seed, DIC_input.scalefactor, region_idx)) {
4960 if (DIC_input.debug) {
4961 NLOG_DEBUG << "matlab_DIC_analysis: seed quality failed for region " << region_idx
4962 << " at frame " << cur_idx
4963 << " corrcoef=" << optimized_seed.corrcoef
4964 << " diffnorm=" << diffnorm
4965 << " iterations=" << num_iterations
4966 << " saturated=" << (iteration_saturated ? "yes" : "no") << ".";
4967 }
4968 frame_success = false;
4969 break;
4970 }
4971
4972 optimized_seeds.push_back(optimized_seed);
4973 }
4974
4975 if (!frame_success || !matlab_seed_positions_are_unique(optimized_seeds, DIC_input.scalefactor)) {
4976 if (DIC_input.debug) {
4977 NLOG_DEBUG << "matlab_DIC_analysis: stopping seed schedule at frame " << cur_idx << ".";
4978 }
4979 break;
4980 }
4981
4982 segment.seeds_by_frame.push_back(optimized_seeds);
4983 // NOTE: Do NOT propagate seeds within a segment.
4984 // MATLAB uses the SAME fixed pos_seed for ALL frames in a segment
4985 // (ncorr_abr_alg_seedanalysis.m calls ncorr_alg_calcseeds with the
4986 // same pos_seed for each current image). Propagation only happens
4987 // BETWEEN segments in matlab_DIC_analysis_impl.
4988 }
4989
4990 if (!segment.seeds_by_frame.empty()) {
4991 segment.terminal_seeds = segment.seeds_by_frame.back();
4992 }
4993
4994 return segment;
4995}
4996
4997std::vector<Disp2D> matlab_run_segment_dic(const DIC_analysis_parallel_input& input,
4998 const ROI2D& roi_ref,
4999 const matlab_seed_segment& segment,
5000 bool run_in_parallel) {
5002
5003 std::vector<Disp2D> segment_disps(segment.seeds_by_frame.size());
5004 if (segment.seeds_by_frame.empty()) {
5005 return segment_disps;
5006 }
5007
5008 const auto& DIC_input = input.base_input;
5009 const auto A_ref = DIC_input.imgs[segment.ref_idx].get_gs();
5010 const difference_type num_frames = static_cast<difference_type>(segment.seeds_by_frame.size());
5011
5012 auto compute_frame = [&](difference_type frame_idx) {
5013 const difference_type cur_idx = segment.ref_idx + frame_idx + 1;
5014 const auto A_cur = DIC_input.imgs[cur_idx].get_gs();
5015 return RGDIC_without_thread(
5016 A_ref,
5017 A_cur,
5018 roi_ref,
5019 DIC_input.scalefactor,
5020 DIC_input.interp_type,
5021 DIC_input.subregion_type,
5022 DIC_input.r,
5023 DIC_input.cutoff_corrcoef,
5024 DIC_input.debug,
5025 segment.seeds_by_frame[frame_idx],
5026 true
5027 );
5028 };
5029
5030 if (!run_in_parallel || num_frames <= 1) {
5031 for (difference_type frame_idx = 0; frame_idx < num_frames; ++frame_idx) {
5032 segment_disps[frame_idx] = compute_frame(frame_idx);
5033 }
5034 return segment_disps;
5035 }
5036
5037 const difference_type requested_threads = std::min(num_frames, DIC_input.num_threads);
5038 NLOG_DEBUG << "\n[matlab_run_segment_dic] Parallel dispatch:"
5039 << "\n num_frames = " << num_frames
5040 << "\n DIC num_threads = " << DIC_input.num_threads
5041 << "\n OMP_NUM_THREADS = " << (std::getenv("OMP_NUM_THREADS") ? std::getenv("OMP_NUM_THREADS") : "not set")
5042 << "\n omp_get_max_threads() = " << omp_get_max_threads()
5043 << "\n Requesting = " << requested_threads << " threads";
5044
5045 std::vector<double> per_thread_seconds(requested_threads, 0.0);
5046 std::vector<long> per_thread_frames (requested_threads, 0);
5047 const double t_wall_begin = omp_get_wtime();
5048
5049 #pragma omp parallel for num_threads(requested_threads) schedule(dynamic)
5050 for (difference_type frame_idx = 0; frame_idx < num_frames; ++frame_idx) {
5051 const int tid = omp_get_thread_num();
5052 const int nthr = omp_get_num_threads();
5053 const double t0 = omp_get_wtime();
5054
5055 segment_disps[frame_idx] = compute_frame(frame_idx);
5056
5057 const double dt = omp_get_wtime() - t0;
5058 per_thread_seconds[tid] += dt;
5059 per_thread_frames [tid] += 1;
5060 #pragma omp critical
5061 {
5062 NLOG_DEBUG << " [segment_dic] frame " << frame_idx
5063 << " on thread " << tid << " / " << nthr
5064 << " took " << dt << " s";
5065 }
5066 }
5067
5068 const double t_wall = omp_get_wtime() - t_wall_begin;
5069 double total_cpu = 0.0;
5070 NLOG_DEBUG << "[matlab_run_segment_dic] per-thread summary:";
5071 for (int t = 0; t < requested_threads; ++t) {
5072 NLOG_DEBUG << " thread " << t
5073 << ": " << per_thread_frames[t] << " frames, "
5074 << per_thread_seconds[t] << " s";
5075 total_cpu += per_thread_seconds[t];
5076 }
5077 NLOG_DEBUG << " wall = " << t_wall << " s, sum-of-threads = "
5078 << total_cpu << " s, speedup = "
5079 << (t_wall > 0.0 ? total_cpu / t_wall : 0.0)
5080 << " (ideal " << requested_threads << ")";
5081
5082 return segment_disps;
5083}
5084
5085DIC_analysis_output matlab_DIC_analysis_impl(const DIC_analysis_parallel_input& input,
5086 bool run_in_parallel,
5087 const std::string& mode_name) {
5089
5090 const auto& DIC_input = input.base_input;
5091 if (DIC_input.imgs.size() < 2) {
5092 throw std::invalid_argument("matlab_DIC_analysis requires at least 2 images.");
5093 }
5094 if (input.seeds_by_region.empty()) {
5095 throw std::invalid_argument("matlab_DIC_analysis requires one manual/preset seed per ROI region.");
5096 }
5097
5098 DIC_analysis_output DIC_output;
5099 DIC_output.disps.resize(DIC_input.imgs.size() - 1);
5100 DIC_output.perspective_type = PERSPECTIVE::LAGRANGIAN;
5101 DIC_output.units = "pixels";
5102 DIC_output.units_per_pixel = 1.0;
5103
5104 // Always populate step_disps / step_rois / step_ref_idx: they are required
5105 // for the post-segment chaining (multi-reference -> global reference frame 0).
5106 // Their on-disk persistence remains gated by DIC_input.save_disps_steps.
5107 std::vector<Disp2D> step_disps(DIC_output.disps.size());
5108 std::vector<ROI2D> step_rois(DIC_output.disps.size());
5109 std::vector<difference_type> step_ref_idx(DIC_output.disps.size());
5110
5112 ROI2D roi_ref = DIC_input.roi;
5113 std::vector<SeedParams> current_seeds = input.seeds_by_region;
5114 bool current_seeds_optimized = input.seeds_are_optimized;
5115 int num_segments = 0;
5116
5117 while (ref_idx < difference_type(DIC_input.imgs.size()) - 1) {
5118 const auto segment = matlab_compute_seed_segment(
5119 input,
5120 ref_idx,
5121 roi_ref,
5122 current_seeds,
5123 current_seeds_optimized
5124 );
5125
5126 if (segment.seeds_by_frame.empty()) {
5127 if (ref_idx == 0) {
5128 throw std::runtime_error(mode_name + " could not seed any current image.");
5129 }
5130 throw std::runtime_error(
5131 mode_name + " could not seed the segment starting at reference frame " +
5132 std::to_string(ref_idx + 1) + "."
5133 );
5134 }
5135
5136 const auto segment_disps = matlab_run_segment_dic(input, roi_ref, segment, run_in_parallel);
5137 ROI2D next_roi_ref;
5138 for (difference_type frame_idx = 0; frame_idx < difference_type(segment_disps.size()); ++frame_idx) {
5139 const difference_type cur_idx = ref_idx + frame_idx + 1;
5140 // Provisional assignment; will be replaced by chained displacement below
5141 // when more than one segment was needed (multi-reference case).
5142 DIC_output.disps[cur_idx - 1] = segment_disps[frame_idx];
5143 next_roi_ref = matlab_update_roi(roi_ref, segment_disps[frame_idx], DIC_input.interp_type, DIC_input.r);
5144
5145 step_disps[cur_idx - 1] = segment_disps[frame_idx];
5146 // Store the disp's OWN (reduced-grid, valid-only) ROI rather than
5147 // the full-resolution input roi_ref. add_with_rois iterates points
5148 // in reduced-grid coordinates (p = p_unscaled * scalefactor), so it
5149 // needs ROIs at the disp's reduced grid; passing a full-res ROI
5150 // makes every bounds check fail and add_with_rois returns an empty
5151 // ROI -> chaining silently falls back to segment-local displacements
5152 // (reproducing the pre-fix segment jump).
5153 step_rois[cur_idx - 1] = segment_disps[frame_idx].get_roi();
5154 step_ref_idx[cur_idx - 1] = ref_idx;
5155 }
5156
5157 const difference_type segment_end_idx = ref_idx + difference_type(segment.seeds_by_frame.size());
5158 if (DIC_input.debug) {
5159 NLOG_DEBUG << "matlab_DIC_analysis: processed segment "
5160 << (ref_idx + 1) << " -> " << (segment_end_idx + 1)
5161 << " (" << segment.seeds_by_frame.size() << " frame(s)).";
5162 }
5163
5164 ++num_segments;
5165 ref_idx = segment_end_idx;
5166 if (ref_idx >= difference_type(DIC_input.imgs.size()) - 1) {
5167 break;
5168 }
5169
5170 roi_ref = next_roi_ref;
5171 current_seeds = segment.terminal_seeds;
5172 current_seeds_optimized = true;
5173 }
5174
5175 // ---------------------------------------------------------------------
5176 // Multi-reference chaining (mirrors MATLAB ncorr_alg_addanalysis).
5177 // When the seed-quality check forced a reference change mid-sequence,
5178 // segment_disps in segment k>1 are expressed wrt the *segment's* reference
5179 // frame, not the global reference (frame 0). Each per-frame displacement
5180 // therefore needs to be composed with the chain of preceding step
5181 // displacements back to frame 0 via add_with_rois (B-spline composition).
5182 // Without this step the .bin output exhibits a discontinuous jump in the
5183 // Lagrangian field at every reference change (visible as a sudden mask /
5184 // 3D-surface / strain divergence around the first segment boundary).
5185 // ---------------------------------------------------------------------
5186 if (num_segments > 1) {
5187 if (DIC_input.debug) {
5188 NLOG_DEBUG << "matlab_DIC_analysis: chaining " << num_segments
5189 << " segments back to global reference (frame 1).";
5190 }
5191 for (difference_type cur_idx = 1; cur_idx < difference_type(DIC_input.imgs.size()); ++cur_idx) {
5192 // Build chain of step displacements from frame 0 to cur_idx.
5193 std::vector<Disp2D> chain_disps;
5194 std::vector<ROI2D> chain_rois;
5195 difference_type idx = cur_idx - 1;
5196 while (idx >= 0) {
5197 chain_disps.insert(chain_disps.begin(), step_disps[idx]);
5198 chain_rois.insert(chain_rois.begin(), step_rois[idx]);
5199 if (step_ref_idx[idx] == 0) {
5200 break;
5201 }
5202 idx = step_ref_idx[idx] - 1;
5203 }
5204
5205 if (chain_disps.size() <= 1) {
5206 // Single segment back to frame 0, no chaining required.
5207 continue;
5208 }
5209
5210 auto combined = add_with_rois(chain_disps, chain_rois, DIC_input.interp_type);
5211 if (combined.get_roi().get_points() == 0) {
5212 NLOG_WARN << "WARNING: matlab_DIC_analysis multi-ref chaining produced empty ROI at frame "
5213 << (cur_idx + 1) << "; keeping segment-local displacement.";
5214 continue;
5215 }
5216 DIC_output.disps[cur_idx - 1] = std::move(combined);
5217 }
5218 }
5219
5220 // ---------------------------------------------------------------------
5221 // (Seam diagnostic previously inlined here has been moved to
5222 // test/src/ncorr_test_chain_seam.cpp as a focused regression test for
5223 // exact_add_with_rois + Array2D::quintic_interp.)
5224 // ---------------------------------------------------------------------
5225
5226 if (DIC_input.save_disps_steps && !step_disps.empty()) {
5227 DIC_analysis_step_data step_data;
5228 step_data.step_disps = step_disps;
5229 step_data.step_rois = step_rois;
5230 step_data.step_ref_idx = step_ref_idx;
5231
5232 const std::string step_filename = run_in_parallel ?
5233 "matlab_DIC_analysis_parallel_step_data.bin" :
5234 "matlab_DIC_analysis_sequential_step_data.bin";
5235 save(step_data, step_filename);
5236 NLOG_INFO << "Step displacement data saved to " << step_filename;
5237 }
5238
5239 return DIC_output;
5240}
5241
5242} // namespace
5243
5244// Matlab-ABR-style DIC analysis -------------------------------------------//
5246 const std::vector<SeedParams>& seeds_by_region,
5247 bool seeds_are_optimized) {
5248 return matlab_DIC_analysis_sequential(DIC_analysis_parallel_input(DIC_input, seeds_by_region, seeds_are_optimized));
5249}
5250
5252 return matlab_DIC_analysis_impl(input, false, "matlab_DIC_analysis_sequential");
5253}
5254
5256 return matlab_DIC_analysis_impl(input, true, "matlab_DIC_analysis_parallel");
5257}
5258
5259// Parallel DIC analysis using seed-based failure prediction
5261 typedef std::ptrdiff_t difference_type;
5262
5263 const auto& DIC_input = input.base_input;
5264
5265 if (DIC_input.imgs.size() < 2) {
5266 throw std::invalid_argument("DIC_analysis_parallel requires at least 2 images.");
5267 }
5268
5269 // Seeds should be provided per region (not generated here)
5270 if (input.seeds_by_region.empty()) {
5271 throw std::invalid_argument("DIC_analysis_parallel requires seeds_by_region to be provided.");
5272 }
5273
5274 NLOG_INFO << std::endl << "Starting seed-based parallel DIC analysis...";
5275 NLOG_INFO << "Number of regions: " << input.seeds_by_region.size();
5276 NLOG_INFO << "Cutoff max diffnorm: " << input.cutoff_max_diffnorm;
5277 NLOG_INFO << "Cutoff max corrcoef: " << input.cutoff_max_corrcoef;
5278
5279 std::chrono::time_point<std::chrono::system_clock> start_analysis = std::chrono::system_clock::now();
5280
5281 // Initialize output
5282 DIC_analysis_output DIC_output;
5283 DIC_output.disps.resize(DIC_input.imgs.size() - 1);
5285 DIC_output.units = "pixels";
5286 DIC_output.units_per_pixel = 1.0;
5287
5288 // PHASE 1: Compute seed parameters for all frames with ROI updates
5289 NLOG_INFO << "\nPhase 1: Computing seed parameters for all frames...";
5290
5291 // Prepare current images
5292 std::vector<Array2D<double>> A_curs;
5293 A_curs.reserve(DIC_input.imgs.size() - 1);
5294 for (size_t i = 1; i < DIC_input.imgs.size(); ++i) {
5295 A_curs.push_back(DIC_input.imgs[i].get_gs());
5296 }
5297
5298 // Compute seed parameters upfront
5299 std::vector<SeedComputationData> seed_data = compute_only_seed_points(
5300 DIC_input.imgs[0].get_gs(),
5301 A_curs,
5302 DIC_input.roi,
5303 DIC_input.scalefactor,
5304 DIC_input.interp_type,
5305 DIC_input.subregion_type,
5306 DIC_input.r,
5307 input.seeds_by_region,
5308 DIC_input.cutoff_corrcoef,
5309 0, // region_idx (working with first region, but structure supports n regions)
5310 DIC_input.debug
5311 );
5312
5313 if (seed_data.empty()) {
5314 throw std::runtime_error("No valid seed parameters could be computed.");
5315 }
5316
5317 NLOG_INFO << "Successfully computed seed parameters for " << seed_data.size() << " frames";
5318
5319 // PHASE 2: Parallel displacement computation using precomputed seed data
5320 NLOG_INFO << "\nPhase 2: Computing displacements in parallel...";
5321
5322 size_t safe_batch_size = seed_data.size();
5323
5324 // Use OpenMP for parallel execution (Python uses ThreadPoolExecutor, C++ uses OpenMP)
5325 NLOG_INFO << "Safe batch size: " << safe_batch_size;
5326 NLOG_INFO << "Number of threads: " << DIC_input.num_threads;
5327 NLOG_DEBUG << "static_cast<difference_type>(safe_batch_size): " << static_cast<difference_type>(safe_batch_size);
5328 NLOG_DEBUG << "Number of threads: " << std::min(static_cast<difference_type>(safe_batch_size), DIC_input.num_threads);
5329 #pragma omp parallel for num_threads(std::min(static_cast<difference_type>(safe_batch_size), DIC_input.num_threads)) schedule(dynamic)
5330 for (size_t i = 0; i < safe_batch_size; ++i) {
5331 difference_type frame_idx = static_cast<difference_type>(i) + 1;
5332 int thread_id = omp_get_thread_num();
5333 int total_threads = omp_get_num_threads();
5334 NLOG_DEBUG << "Running on thread " << thread_id << " of " << total_threads;
5335
5336 #pragma omp critical
5337 {
5338 NLOG_INFO << " Processing frame " << frame_idx << " (parallel)";
5339 }
5340
5341 // Use precomputed seed data for this frame
5342 const auto& frame_data = seed_data[i];
5343 const auto& roi_for_frame = frame_data.roi;
5344 const auto& sr_nloptimizer_for_frame = frame_data.sr_nloptimizer;
5345
5346 // Safety check: ensure seed_params_by_region is not empty
5347 if (frame_data.seed_params_by_region.empty()) {
5348 #pragma omp critical
5349 {
5350 NLOG_WARN << " WARNING: No seed params for frame " << frame_idx << ", skipping.";
5351 }
5352 continue;
5353 }
5354 const auto& seed_params_for_frame = frame_data.seed_params_by_region[0];
5355
5356 try {
5357 // Compute displacements using precomputed seed parameters
5358 auto disps = compute_displacements(
5359 sr_nloptimizer_for_frame,
5360 roi_for_frame.reduce(DIC_input.scalefactor),
5361 seed_params_for_frame,
5362 DIC_input.scalefactor,
5363 DIC_input.cutoff_corrcoef,
5364 0, // region_idx
5365 DIC_input.debug
5366 );
5367
5368 // Store displacement
5369 #pragma omp critical
5370 {
5371 DIC_output.disps[frame_idx - 1] = disps;
5372 }
5373 } catch (const std::exception& e) {
5374 #pragma omp critical
5375 {
5376 NLOG_ERROR << " ERROR: Frame " << frame_idx << " failed: " << e.what();
5377 }
5378 }
5379 }
5380
5381 // End timer
5382 std::chrono::time_point<std::chrono::system_clock> end_analysis = std::chrono::system_clock::now();
5383 std::chrono::duration<double> elapsed_seconds = end_analysis - start_analysis;
5384 NLOG_INFO << std::endl << "Total parallel DIC analysis time: " << elapsed_seconds.count() << " seconds";
5385
5386 return DIC_output;
5387}
5388
5389// ===========================================================================
5390// EXACT MATLAB-NCORR-MIRRORING CHAIN COMPOSITION + DIC ANALYSIS
5391// ===========================================================================
5392// These functions reproduce the MATLAB ncorr 2D pipeline byte-for-byte for
5393// the multi-reference chaining step (the only place add_with_rois was shown
5394// to inject a per-pixel jump at segment boundaries).
5395//
5396// Mirrors:
5397// - ncorr_alg_extrapdata.cpp + ncorr_lib.cpp::expand_filt:
5398// per-region 4-neighbor mean expansion of the displacement plot into a
5399// border_interp = 20 reduced-grid border around the region.
5400// - ncorr_class_img.form_bcoef + ncorr_lib.cpp::interp_qbs:
5401// biquintic B-spline interpolation. We reuse Array2D's QUINTIC_BSPLINE
5402// interpolator (Array2D builds the bcoef internally with a 20-cell
5403// border, identical kernel to MATLAB's form_bcoef).
5404// - ncorr_alg_addanalysis.m + ncorr_alg_adddisp.cpp:
5405// walk in REDUCED-grid coords, dividing each step by scalefactor
5406// ( = MATLAB spacing+1), with a STRICT in_nlinfo mask check at each
5407// chain link before interpolating.
5408// ===========================================================================
5409namespace exact_detail {
5410
5412
5413// MATLAB ncorr_lib.cpp::expand_filt rewritten on Array2D<double>.
5414// `inside_region(y, x)` is true for pixels INSIDE the region; the rest get
5415// filled iteratively by the average of their currently-filled neighbours.
5417 const Array2D<bool>& inside_region) {
5418 const difference_type H = plot.height();
5419 const difference_type W = plot.width();
5420 if (H < 2 || W < 2) return;
5421 Array2D<bool> active_old(H, W);
5422 Array2D<bool> active_new(H, W);
5423 for (difference_type x = 0; x < W; ++x) {
5424 for (difference_type y = 0; y < H; ++y) {
5425 const bool a = !inside_region(y, x);
5426 active_old(y, x) = a;
5427 active_new(y, x) = a;
5428 }
5429 }
5430 while (true) {
5431 difference_type total = 0;
5432 for (difference_type x = 0; x < W; ++x) {
5433 for (difference_type y = 0; y < H; ++y) {
5434 if (!active_old(y, x)) continue;
5435 double sum = 0.0;
5436 int n = 0;
5437 if (y > 0 && !active_old(y - 1, x)) { sum += plot(y - 1, x); ++n; ++total; }
5438 if (y < H - 1 && !active_old(y + 1, x)) { sum += plot(y + 1, x); ++n; ++total; }
5439 if (x > 0 && !active_old(y, x - 1)) { sum += plot(y, x - 1); ++n; ++total; }
5440 if (x < W - 1 && !active_old(y, x + 1)) { sum += plot(y, x + 1); ++n; ++total; }
5441 if (n > 0) {
5442 plot(y, x) = sum / double(n);
5443 active_new(y, x) = false;
5444 }
5445 }
5446 }
5447 if (total == 0) break;
5448 for (difference_type x = 0; x < W; ++x) {
5449 for (difference_type y = 0; y < H; ++y) {
5450 active_old(y, x) = active_new(y, x);
5451 }
5452 }
5453 }
5454}
5455
5456// Per-region extrapolated displacement plot, biquintic interpolators, and
5457// the source mask used for the strict in_nlinfo check during chain walks.
5473
5474// Builds the extrapolated v/u/cc plots into `out` IN PLACE. The
5475// interpolators must be (re)bound AFTER `out` has reached its final
5476// storage location (e.g. by bind_interpolators below), because the
5477// Array2D interpolators store a raw pointer to their source Array2D and
5478// moving the struct would otherwise leave them pointing at the moved-from
5479// address.
5480inline void fill_region_extrap(const Disp2D& disp,
5481 const ROI2D::region_nlinfo& nlinfo,
5482 difference_type border,
5483 exact_region_disp& out) {
5484 out.border = border;
5485 out.nlinfo = &nlinfo;
5486 out.empty = nlinfo.empty();
5487 if (out.empty) return;
5488
5489 const difference_type bbox_h = nlinfo.bottom - nlinfo.top + 1;
5490 const difference_type bbox_w = nlinfo.right - nlinfo.left + 1;
5491 const difference_type H = bbox_h + 2 * border;
5492 const difference_type W = bbox_w + 2 * border;
5493 out.region_top = nlinfo.top;
5494 out.region_left = nlinfo.left;
5495 out.extrap_height = H;
5496 out.extrap_width = W;
5497
5498 out.plot_v_extrap = Array2D<double>(H, W);
5499 out.plot_u_extrap = Array2D<double>(H, W);
5500 out.plot_cc_extrap = Array2D<double>(H, W);
5501 Array2D<bool> inside(H, W);
5502
5503 const auto& v_arr = disp.get_v().get_array();
5504 const auto& u_arr = disp.get_u().get_array();
5505 const auto& cc_arr = disp.get_cc().get_array();
5506
5507 for (difference_type nl_idx = 0; nl_idx < nlinfo.nodelist.width(); ++nl_idx) {
5508 const difference_type p2 = nl_idx + nlinfo.left_nl;
5509 const difference_type lx = p2 - nlinfo.left + border;
5510 if (lx < 0 || lx >= W) continue;
5511 for (difference_type np_idx = 0;
5512 np_idx < nlinfo.noderange(nl_idx);
5513 np_idx += 2) {
5514 const difference_type top = nlinfo.nodelist(np_idx, nl_idx);
5515 const difference_type bot = nlinfo.nodelist(np_idx + 1, nl_idx);
5516 for (difference_type p1 = top; p1 <= bot; ++p1) {
5517 const difference_type ly = p1 - nlinfo.top + border;
5518 if (ly < 0 || ly >= H) continue;
5519 out.plot_v_extrap(ly, lx) = v_arr(p1, p2);
5520 out.plot_u_extrap(ly, lx) = u_arr(p1, p2);
5521 out.plot_cc_extrap(ly, lx) = cc_arr(p1, p2);
5522 inside(ly, lx) = true;
5523 }
5524 }
5525 }
5526
5527 expand_filt(out.plot_v_extrap, inside);
5528 expand_filt(out.plot_u_extrap, inside);
5529 expand_filt(out.plot_cc_extrap, inside);
5530}
5531
5532// (Re)bind the biquintic interpolators to the *current* address of the
5533// extrapolated plots. MUST be called after the struct has been placed in
5534// its final storage (e.g. inside the per_link vector); calling it inside
5535// fill_region_extrap and then moving the struct would leave the
5536// interpolator's raw pointer dangling.
5538 if (out.empty) return;
5539 // Biquintic B-spline matches MATLAB ncorr's interp_qbs exactly. The
5540 // underlying Array2D::quintic_interp uses the Unser-Aldroubi-Eden
5541 // recursive bcoef filter (see Array2D.h), which round-trips at integer
5542 // query points regardless of the source array's edge gradient. (An
5543 // earlier version of that filter used a circular FFT deconv that
5544 // produced a ~+29% spectral bias on disp fields with large motion;
5545 // that has been fixed at the source.)
5549}
5550
5551} // namespace exact_detail
5552
5553// MATLAB-style chain composition that mirrors ncorr_alg_addanalysis +
5554// ncorr_alg_adddisp: extrapdata + biquintic + reduced-grid walk + strict
5555// in_nlinfo mask check at every link.
5556Disp2D exact_add_with_rois(const std::vector<Disp2D>& disps,
5557 const std::vector<ROI2D>& rois) {
5558 using difference_type = ROI2D::difference_type;
5562
5563 if (disps.empty()) return Disp2D();
5564 if (disps.size() == 1) return disps.front();
5565 if (disps.size() != rois.size()) {
5566 throw std::invalid_argument(
5567 "exact_add_with_rois: disps.size() (" + std::to_string(disps.size()) +
5568 ") != rois.size() (" + std::to_string(rois.size()) + ").");
5569 }
5570
5571 const difference_type scalefactor = disps.front().get_scalefactor();
5572 const auto& roi_first = rois.front();
5573 for (std::size_t k = 1; k < disps.size(); ++k) {
5574 if (disps[k].get_scalefactor() != scalefactor ||
5575 disps[k].data_height() != disps.front().data_height() ||
5576 disps[k].data_width() != disps.front().data_width() ||
5577 rois[k].size_regions() != roi_first.size_regions()) {
5578 throw std::invalid_argument(
5579 "exact_add_with_rois: scalefactor / data size / region count mismatch.");
5580 }
5581 }
5582
5583 const difference_type border = 20; // MATLAB border_interp = 20
5584
5585 Array2D<bool> A_vp(roi_first.height(), roi_first.width());
5586 Array2D<double> A_v (roi_first.height(), roi_first.width());
5587 Array2D<double> A_u (roi_first.height(), roi_first.width());
5588 Array2D<double> A_cc(roi_first.height(), roi_first.width());
5589 A_cc() = 1.0;
5590
5591 for (difference_type region_idx = 0;
5592 region_idx < roi_first.size_regions();
5593 ++region_idx) {
5594 // Two-phase construction: fill the plots into final storage first,
5595 // THEN bind the interpolators (Array2D's interpolator holds a raw
5596 // pointer to its source array, so we can't bind it before the
5597 // struct reaches its final location in `per_link`).
5598 std::vector<exact_region_disp> per_link(disps.size());
5599 for (std::size_t k = 0; k < disps.size(); ++k) {
5600 fill_region_extrap(disps[k],
5601 rois[k].get_nlinfo(region_idx),
5602 border,
5603 per_link[k]);
5604 }
5605 for (auto& link : per_link) bind_interpolators(link);
5606 if (per_link.front().empty) continue;
5607
5608 const auto& nlinfo0 = roi_first.get_nlinfo(region_idx);
5609 if (nlinfo0.empty()) continue;
5610
5611 for (difference_type nl_idx = 0;
5612 nl_idx < nlinfo0.nodelist.width();
5613 ++nl_idx) {
5614 const difference_type p2_red = nl_idx + nlinfo0.left_nl;
5615 for (difference_type np_idx = 0;
5616 np_idx < nlinfo0.noderange(nl_idx);
5617 np_idx += 2) {
5618 const difference_type top_n = nlinfo0.nodelist(np_idx, nl_idx);
5619 const difference_type bot_n = nlinfo0.nodelist(np_idx + 1, nl_idx);
5620 for (difference_type p1_red = top_n; p1_red <= bot_n; ++p1_red) {
5621
5622 // Walk the chain in REDUCED-grid coords (MATLAB style).
5623 double y_red = double(p1_red);
5624 double x_red = double(p2_red);
5625 double v_added = 0.0;
5626 double u_added = 0.0;
5627 double cc_min = 1.0;
5628 bool ok = true;
5629
5630 for (std::size_t k = 0; k < per_link.size(); ++k) {
5631 const auto& e = per_link[k];
5632 if (e.empty) { ok = false; break; }
5633
5634 const difference_type y_round =
5635 static_cast<difference_type>(std::round(y_red));
5636 const difference_type x_round =
5637 static_cast<difference_type>(std::round(x_red));
5638 if (!e.nlinfo->in_nlinfo(y_round, x_round)) {
5639 ok = false; break;
5640 }
5641
5642 const double ly = y_red - double(e.region_top) + double(e.border);
5643 const double lx = x_red - double(e.region_left) + double(e.border);
5644 if (ly < 0.0 || lx < 0.0 ||
5645 ly > double(e.extrap_height - 1) ||
5646 lx > double(e.extrap_width - 1)) {
5647 ok = false; break;
5648 }
5649
5650 const double dv = e.v_interp(ly, lx);
5651 const double du = e.u_interp(ly, lx);
5652 if (std::isnan(dv) || std::isnan(du)) { ok = false; break; }
5653
5654 const double cc = e.cc_interp(ly, lx);
5655 if (!std::isnan(cc) && cc < cc_min) cc_min = cc;
5656
5657 v_added += dv;
5658 u_added += du;
5659 // MATLAB ncorr_alg_adddisp: x_cur_reduced += u/(spacing+1).
5660 y_red += dv / double(scalefactor);
5661 x_red += du / double(scalefactor);
5662 }
5663
5664 if (ok) {
5665 A_v (p1_red, p2_red) = v_added;
5666 A_u (p1_red, p2_red) = u_added;
5667 A_cc(p1_red, p2_red) = cc_min;
5668 A_vp(p1_red, p2_red) = true;
5669 }
5670 }
5671 }
5672 }
5673
5674 }
5675
5676 return { std::move(A_v), std::move(A_u), std::move(A_cc),
5677 roi_first.form_union(A_vp), scalefactor };
5678}
5679
5680// ---------------------------------------------------------------------------
5681// exact_matlab_DIC_analysis_impl: identical to matlab_DIC_analysis_impl up to
5682// the multi-reference chaining step, which uses exact_add_with_rois().
5683// ---------------------------------------------------------------------------
5685 bool run_in_parallel,
5686 const std::string& mode_name) {
5687 typedef ROI2D::difference_type difference_type;
5688
5689 const auto& DIC_input = input.base_input;
5690 if (DIC_input.imgs.size() < 2) {
5691 throw std::invalid_argument(mode_name + " requires at least 2 images.");
5692 }
5693 if (input.seeds_by_region.empty()) {
5694 throw std::invalid_argument(mode_name + " requires one manual/preset seed per ROI region.");
5695 }
5696
5697 DIC_analysis_output DIC_output;
5698 DIC_output.disps.resize(DIC_input.imgs.size() - 1);
5700 DIC_output.units = "pixels";
5701 DIC_output.units_per_pixel = 1.0;
5702
5703 std::vector<Disp2D> step_disps (DIC_output.disps.size());
5704 std::vector<ROI2D> step_rois (DIC_output.disps.size());
5705 std::vector<difference_type> step_ref_idx (DIC_output.disps.size());
5706
5707 difference_type ref_idx = 0;
5708 ROI2D roi_ref = DIC_input.roi;
5709 std::vector<SeedParams> current_seeds = input.seeds_by_region;
5710 bool current_seeds_optimized = input.seeds_are_optimized;
5711 int num_segments = 0;
5712
5713 while (ref_idx < difference_type(DIC_input.imgs.size()) - 1) {
5714 const auto segment = matlab_compute_seed_segment(
5715 input, ref_idx, roi_ref, current_seeds, current_seeds_optimized);
5716
5717 if (segment.seeds_by_frame.empty()) {
5718 if (ref_idx == 0) {
5719 throw std::runtime_error(mode_name + " could not seed any current image.");
5720 }
5721 throw std::runtime_error(
5722 mode_name + " could not seed the segment starting at reference frame " +
5723 std::to_string(ref_idx + 1) + ".");
5724 }
5725
5726 const auto segment_disps = matlab_run_segment_dic(input, roi_ref, segment, run_in_parallel);
5727 ROI2D next_roi_ref;
5728 for (difference_type frame_idx = 0;
5729 frame_idx < difference_type(segment_disps.size());
5730 ++frame_idx) {
5731 const difference_type cur_idx = ref_idx + frame_idx + 1;
5732 DIC_output.disps[cur_idx - 1] = segment_disps[frame_idx];
5733 next_roi_ref = matlab_update_roi(roi_ref, segment_disps[frame_idx],
5734 DIC_input.interp_type, DIC_input.r);
5735 step_disps [cur_idx - 1] = segment_disps[frame_idx];
5736 step_rois [cur_idx - 1] = segment_disps[frame_idx].get_roi();
5737 step_ref_idx[cur_idx - 1] = ref_idx;
5738 }
5739
5740 const difference_type segment_end_idx = ref_idx + difference_type(segment.seeds_by_frame.size());
5741 if (DIC_input.debug) {
5742 NLOG_DEBUG << mode_name << ": processed segment "
5743 << (ref_idx + 1) << " -> " << (segment_end_idx + 1)
5744 << " (" << segment.seeds_by_frame.size() << " frame(s)).";
5745 }
5746 ++num_segments;
5747 ref_idx = segment_end_idx;
5748 if (ref_idx >= difference_type(DIC_input.imgs.size()) - 1) break;
5749 roi_ref = next_roi_ref;
5750 current_seeds = segment.terminal_seeds;
5751 current_seeds_optimized = true;
5752 }
5753
5754 if (num_segments > 1) {
5755 if (DIC_input.debug) {
5756 NLOG_DEBUG << mode_name << ": chaining " << num_segments
5757 << " segments back to global reference (frame 1) "
5758 << "via exact_add_with_rois (MATLAB ncorr_alg_addanalysis).";
5759 }
5760 for (difference_type cur_idx = 1;
5761 cur_idx < difference_type(DIC_input.imgs.size());
5762 ++cur_idx) {
5763 std::vector<Disp2D> chain_disps;
5764 std::vector<ROI2D> chain_rois;
5765 difference_type idx = cur_idx - 1;
5766 while (idx >= 0) {
5767 chain_disps.insert(chain_disps.begin(), step_disps[idx]);
5768 chain_rois .insert(chain_rois .begin(), step_rois [idx]);
5769 if (step_ref_idx[idx] == 0) break;
5770 idx = step_ref_idx[idx] - 1;
5771 }
5772 if (chain_disps.size() <= 1) continue;
5773
5774 auto combined = exact_add_with_rois(chain_disps, chain_rois);
5775 if (combined.get_roi().get_points() == 0) {
5776 NLOG_WARN << "WARNING: " << mode_name
5777 << " multi-ref chaining produced empty ROI at frame "
5778 << (cur_idx + 1)
5779 << "; keeping segment-local displacement.";
5780 continue;
5781 }
5782 DIC_output.disps[cur_idx - 1] = std::move(combined);
5783 }
5784 }
5785
5786 if (DIC_input.save_disps_steps && !step_disps.empty()) {
5787 DIC_analysis_step_data step_data;
5788 step_data.step_disps = step_disps;
5789 step_data.step_rois = step_rois;
5790 step_data.step_ref_idx = step_ref_idx;
5791
5792 const std::string filename = run_in_parallel ?
5793 "exact_matlab_DIC_analysis_parallel_step_data.bin" :
5794 "exact_matlab_DIC_analysis_sequential_step_data.bin";
5795 save(step_data, filename);
5796 NLOG_INFO << "Step displacement data saved to " << filename;
5797 }
5798
5799 return DIC_output;
5800}
5801
5803 const std::vector<SeedParams>& seeds_by_region,
5804 bool seeds_are_optimized) {
5806 DIC_analysis_parallel_input(DIC_input, seeds_by_region, seeds_are_optimized));
5807}
5809 return exact_matlab_DIC_analysis_impl(input, false, "exact_matlab_DIC_analysis_sequential");
5810}
5812 return exact_matlab_DIC_analysis_impl(input, true, "exact_matlab_DIC_analysis_parallel");
5813}
5814
5815}
difference_type width() const
Definition Array2D.h:410
std::string size_2D_string() const
Definition Array2D.h:423
pointer get_pointer() const
Definition Array2D.h:412
std::enable_if< std::is_floating_point< value_type >::value, T_output >::type get_linsolver(LINSOLVER linsolver_type) const
Definition Array2D.h:267
difference_type height() const
Definition Array2D.h:409
bool empty() const
Definition Array2D.h:415
std::enable_if< std::is_floating_point< value_type >::value, T_output >::type get_interpolator(INTERP interp_type) const
Definition Array2D.h:251
bool same_size(const T_container &A) const
Definition Array2D.h:417
difference_type size() const
Definition Array2D.h:411
bool in_bounds(difference_type p) const
Definition Array2D.h:420
const Array2D< double > & get_array() const
Definition Data2D.h:64
const ROI2D & get_roi() const
Definition Data2D.h:65
difference_type get_scalefactor() const
Definition Data2D.h:66
difference_type data_width() const
Definition Data2D.h:63
nlinfo_interpolator get_nlinfo_interpolator(difference_type, INTERP) const
Definition Data2D.cpp:88
difference_type data_height() const
Definition Data2D.h:62
std::string size_2D_string() const
Definition Data2D.h:73
std::string size_2D_string() const
Definition Disp2D.h:75
difference_type data_width() const
Definition Disp2D.h:63
static Disp2D load(std::ifstream &)
Definition Disp2D.cpp:13
difference_type get_scalefactor() const
Definition Disp2D.h:68
const Data2D & get_u() const
Definition Disp2D.h:65
difference_type data_height() const
Definition Disp2D.h:62
nlinfo_interpolator get_nlinfo_interpolator(difference_type, INTERP) const
Definition Disp2D.cpp:58
const ROI2D & get_roi() const
Definition Disp2D.h:67
details::Disp2D_nlinfo_interpolator nlinfo_interpolator
Definition Disp2D.h:29
const Data2D & get_v() const
Definition Disp2D.h:64
const Data2D & get_cc() const
Definition Disp2D.h:66
static Image2D load(std::ifstream &)
Definition Image2D.cpp:25
Array2D< double > get_gs() const
Definition Image2D.cpp:134
contig_subregion_generator get_contig_subregion_generator(SUBREGION, difference_type) const
Definition ROI2D.cpp:288
std::ptrdiff_t difference_type
Definition ROI2D.h:31
ROI2D reduce(difference_type) const
Definition ROI2D.cpp:238
difference_type size_regions() const
Definition ROI2D.h:85
difference_type width() const
Definition ROI2D.h:72
const Array2D< bool > & get_mask() const
Definition ROI2D.h:81
const region_nlinfo & get_nlinfo(difference_type) const
Definition ROI2D.cpp:198
static ROI2D load(std::ifstream &)
Definition ROI2D.cpp:129
ROI2D form_union(const Array2D< bool > &) const
Definition ROI2D.cpp:258
difference_type height() const
Definition ROI2D.h:71
difference_type get_points() const
Definition ROI2D.h:84
std::string size_2D_string() const
Definition ROI2D.h:101
const region_boundary & get_boundary(difference_type) const
Definition ROI2D.cpp:204
const Data2D & get_exx() const
Definition Strain2D.h:58
const Data2D & get_exy() const
Definition Strain2D.h:57
const Data2D & get_eyy() const
Definition Strain2D.h:56
static Strain2D load(std::ifstream &)
Definition Strain2D.cpp:13
const ROI2D::region_nlinfo & get_subregion_nlinfo() const
Definition ROI2D.h:358
nloptimizer_base::difference_type difference_type
Definition ncorr.h:61
Array2D< double > grad_buf
Definition ncorr.h:52
void chk_input_params_size(const Array2D< double > &) const
Definition ncorr.cpp:49
Array2D< double > params
Definition ncorr.h:54
virtual bool iterative_search() const =0
std::pair< const Array2D< double > &, bool > global(const Array2D< double > &) const
Definition ncorr.cpp:24
std::pair< const Array2D< double > &, bool > operator()(const Array2D< double > &) const
Definition ncorr.cpp:37
difference_type cutoff_iterations
Definition ncorr.h:56
virtual bool initial_guess() const =0
Array2D< double > hess_buf
Definition ncorr.h:53
nloptimizer_base::difference_type difference_type
Definition ncorr.h:90
Lightweight, dependency-free logging facility for CppNCorr.
#define NLOG_INFO
Definition log.h:162
#define NLOG_DEBUG
Definition log.h:161
#define NLOG_ERROR
Definition log.h:164
#define NLOG_WARN
Definition log.h:163
std::pair< std::vector< ROI2D::region_nlinfo >, std::vector< ROI2D::region_nlinfo > > nlinfo_line_split(const Array2D< double > &direc, const Array2D< double > &origin, ROI2D::difference_type partition_offset, const ROI2D::region_nlinfo &nlinfo, Array2D< ROI2D::difference_type > &partition_diagram, Array2D< bool > &mask_buf)
Definition ncorr.cpp:1410
Array2D< ROI2D::difference_type > get_ROI_SDA(const ROI2D &roi)
Definition ncorr.cpp:931
cv::Mat cv_ncorr_data_over_img(const Image2D &img, const Data2D &data, double alpha, double min_data, double max_data, bool enable_colorbar=true, bool enable_axes=true, bool enable_scalebar=true, const std::string &units="pixels", double units_per_pixel=1.0, double num_units=-1.0, double font_size=1.0, ROI2D::difference_type num_tick_marks=11, int colormap=cv::COLORMAP_JET)
Definition ncorr.cpp:3815
Array2D< double > matlab_update_boundary(const Array2D< double > &boundary, const Disp2D::nlinfo_interpolator &disp_interp, const ROI2D::difference_type roi_scalefactor, const ROI2D::difference_type size_mask_height, const ROI2D::difference_type size_mask_width, const ROI2D::difference_type radius)
Definition ncorr.cpp:755
Array2D< ROI2D::difference_type > & get_nlinfo_SDA(Array2D< ROI2D::difference_type > &SDA, const ROI2D::region_nlinfo &nlinfo, Array2D< bool > &mask_buf, Array2D< bool > &mask_buf_new)
Definition ncorr.cpp:866
std::vector< Array2D< ROI2D::difference_type > > get_ROI_partition_diagram_seeds(const Array2D< ROI2D::difference_type > &partition_diagram, const ROI2D &roi, ROI2D::difference_type num_partitions)
Definition ncorr.cpp:1775
bool recursive_nlinfo_partition_diagram(ROI2D::difference_type part_num1, ROI2D::difference_type part_num2, const ROI2D::region_nlinfo &nlinfo, Array2D< ROI2D::difference_type > &partition_diagram, Array2D< bool > &mask_buf)
Definition ncorr.cpp:1664
Array2D< double > get_seed_params(const Data2D &data, const ROI2D::region_nlinfo &nlinfo, const disp_nloptimizer &d_nloptimizer, const Array2D< ROI2D::difference_type > &SDA, Array2D< double > &params_buf)
Definition ncorr.cpp:948
Array2D< double > get_centroid(const ROI2D::region_nlinfo &nlinfo)
Definition ncorr.cpp:1382
void nlinfo_contig_expansion(const ROI2D::region_nlinfo &sub_nlinfo, const ROI2D::region_nlinfo &nlinfo, ROI2D::difference_type val, Array2D< ROI2D::difference_type > &A, Array2D< bool > &A_ap)
Definition ncorr.cpp:1466
Array2D< double > update_boundary_skip_all(const Array2D< double > &boundary, const Disp2D::nlinfo_interpolator &disp_interp, const ROI2D::difference_type roi_scalefactor)
Definition ncorr.cpp:598
Array2D< ROI2D::difference_type > & get_nlinfo_partition_diagram(const ROI2D::region_nlinfo &nlinfo, ROI2D::difference_type num_partitions, Array2D< ROI2D::difference_type > &partition_diagram, Array2D< bool > &mask_buf)
Definition ncorr.cpp:1693
std::pair< std::pair< ROI2D::region_nlinfo, ROI2D::region_nlinfo >, bool > nlinfo_contig_split(ROI2D::difference_type partition_offset, const ROI2D::region_nlinfo &nlinfo, Array2D< ROI2D::difference_type > &partition_diagram, Array2D< bool > &mask_buf)
Definition ncorr.cpp:1506
Disp2D update(const Disp2D &disp, INTERP interp_type, ROI_UPDATE_MODE mode=ROI_UPDATE_MODE::SKIP_INVALID)
Definition ncorr.cpp:3356
void worker_RGDIC(subregion_nloptimizer sr_nloptimizer, ROI2D roi_reduced, ROI2D::difference_type scalefactor, double cutoff_corrcoef, double cutoff_delta_disp, Array2D< double > &A_v, Array2D< double > &A_u, Array2D< double > &A_cc, Array2D< bool > &A_ap, Array2D< bool > &A_vp)
Definition ncorr.cpp:1874
Array2D< ROI2D::difference_type > get_ROI_partition_diagram(const ROI2D &roi, ROI2D::difference_type num_partitions)
Definition ncorr.cpp:1713
bool analyze_point(const Array2D< double > &queue_params, ROI2D::difference_type p1_new_delta, ROI2D::difference_type p2_new_delta, const ROI2D::region_nlinfo &nlinfo, ROI2D::difference_type roi_scalefactor, const disp_nloptimizer &d_nloptimizer, const Array2D< ROI2D::difference_type > &SDA, std::priority_queue< Array2D< double >, std::vector< Array2D< double > >, std::function< bool(const Array2D< double > &, const Array2D< double > &)> > &queue, Array2D< bool > &A_new_ap, Array2D< double > &params_buf)
Definition ncorr.cpp:979
Array2D< ROI2D::difference_type > get_nlinfo_partition_diagram_seeds(const Array2D< ROI2D::difference_type > &partition_diagram, const ROI2D::region_nlinfo &nlinfo, ROI2D::difference_type num_partitions, Array2D< ROI2D::difference_type > &SDA, Array2D< bool > &mask_buf1, Array2D< bool > &mask_buf2)
Definition ncorr.cpp:1734
Array2D< double > update_boundary_skip_invalid(const Array2D< double > &boundary, const Disp2D::nlinfo_interpolator &disp_interp, const ROI2D::difference_type roi_scalefactor, const ROI2D::difference_type roi_height, const ROI2D::difference_type roi_width, const ROI2D::difference_type margin=0)
Definition ncorr.cpp:622
void expand_filt(Array2D< double > &plot, const Array2D< bool > &inside_region)
Definition ncorr.cpp:5416
void bind_interpolators(exact_region_disp &out)
Definition ncorr.cpp:5537
ROI2D::difference_type difference_type
Definition ncorr.cpp:5411
void fill_region_extrap(const Disp2D &disp, const ROI2D::region_nlinfo &nlinfo, difference_type border, exact_region_disp &out)
Definition ncorr.cpp:5480
Disp2D add_with_rois_legacy(const std::vector< Disp2D > &disps, const std::vector< ROI2D > &rois, INTERP interp_type)
Definition ncorr.cpp:1256
void set_debug(bool on)
Lower the console threshold to Debug when on is true (used to honour the engine's existing debug flag...
Definition log.cpp:239
ROI2D matlab_update_roi(const ROI2D &, const Disp2D &, INTERP, ROI2D::difference_type radius)
Definition ncorr.cpp:804
T_container & fill(T_container &A, const ROI2D::region_nlinfo &nlinfo, const T &val)
Definition ROI2D.h:422
void imshow(const Data2D &data, Data2D::difference_type delay)
Definition Data2D.cpp:58
Strain2D LS_strain(const Disp2D &, PERSPECTIVE, double, SUBREGION, ROI2D::difference_type)
Definition ncorr.cpp:3547
DISP
Definition ncorr.h:628
DIC_analysis_output DIC_analysis_sequential(const DIC_analysis_input &)
DIC_analysis_output DIC_analysis_parallel(const DIC_analysis_parallel_input &)
Definition ncorr.cpp:5260
Disp2D add(const std::vector< Disp2D > &, INTERP)
Definition ncorr.cpp:1126
std::vector< SeedComputationData > compute_only_seed_points(const Array2D< double > &A_ref, const std::vector< Array2D< double > > &A_curs, const ROI2D &roi, ROI2D::difference_type scalefactor, INTERP interp_type, SUBREGION subregion_type, const std::vector< SeedParams > &seeds_by_region, double cutoff_corrcoef, ROI2D::difference_type region_idx=0, bool debug=false)
Disp2D RGDIC_with_seeds(const Array2D< double > &A_ref, const Array2D< double > &A_cur, const ROI2D &roi, const DIC_analysis_parallel_input &input)
Definition ncorr.cpp:3337
SUBREGION
Definition ROI2D.h:27
strain_analysis_output strain_analysis(const strain_analysis_input &)
Definition ncorr.cpp:3792
std::pair< typename T_container::value_type, typename T_container::coords > min(T_container &A, const ROI2D::region_nlinfo &nlinfo)
Definition ROI2D.h:538
std::pair< typename T_container::value_type, typename T_container::coords > max(T_container &A, const ROI2D::region_nlinfo &nlinfo)
Definition ROI2D.h:509
DIC_analysis_output exact_matlab_DIC_analysis_sequential(const DIC_analysis_input &DIC_input, const std::vector< SeedParams > &seeds_by_region={}, bool seeds_are_optimized=false)
Definition ncorr.cpp:5802
Disp2D exact_add_with_rois(const std::vector< Disp2D > &disps, const std::vector< ROI2D > &rois)
Definition ncorr.cpp:5556
ROI_UPDATE_MODE
Definition ncorr.h:152
DIC_analysis_config
Definition ncorr.h:176
void save(const Data2D &data, std::ofstream &os)
Definition Data2D.cpp:78
void save_strain_video(const std::string &, const strain_analysis_input &, const strain_analysis_output &, STRAIN, double, double, double=std::numeric_limits< double >::quiet_NaN(), double=std::numeric_limits< double >::quiet_NaN(), bool=true, bool=true, bool=true, double=-1.0, double=1.0, ROI2D::difference_type=11, int=cv::COLORMAP_JET, double=2.0, int=cv::VideoWriter::fourcc('M', 'J', 'P', 'G'))
Definition ncorr.cpp:4381
Disp2D RGDIC(const Array2D< double > &, const Array2D< double > &, const ROI2D &, ROI2D::difference_type, INTERP, SUBREGION, ROI2D::difference_type, ROI2D::difference_type, double, bool)
Definition ncorr.cpp:1948
Disp2D compute_displacements(const details::subregion_nloptimizer &sr_nloptimizer, const ROI2D &roi_reduced, const SeedParams &seedparams, ROI2D::difference_type scalefactor, double cutoff_corrcoef, ROI2D::difference_type region_idx, bool debug)
Definition ncorr.cpp:4470
ACCUMULATION_MODE
Definition ncorr.h:160
INTERP
Definition Array2D.h:77
DIC_analysis_output filter_by_correlation(const DIC_analysis_output &, double)
Definition ncorr.cpp:3483
void save_ncorr_data_over_img(const std::string &, const Image2D &, const Data2D &, double, double, double, bool, bool, bool, const std::string &, double, double, double, ROI2D::difference_type, int)
Definition ncorr.cpp:4156
DIC_analysis_output matlab_DIC_analysis_parallel(const DIC_analysis_parallel_input &)
Definition ncorr.cpp:5255
DIC_analysis_output exact_matlab_DIC_analysis_parallel(const DIC_analysis_parallel_input &)
Definition ncorr.cpp:5811
std::vector< SeedParams > propagate_seeds(const std::vector< SeedParams > &seeds, ROI2D::difference_type spacing)
Definition ncorr.cpp:4680
void save_DIC_video(const std::string &, const DIC_analysis_input &, const DIC_analysis_output &, DISP, double, double, double=std::numeric_limits< double >::quiet_NaN(), double=std::numeric_limits< double >::quiet_NaN(), bool=true, bool=true, bool=true, double=-1.0, double=1.0, ROI2D::difference_type=11, int=cv::COLORMAP_JET, double=2.0, int=cv::VideoWriter::fourcc('M', 'J', 'P', 'G'))
Definition ncorr.cpp:4299
DIC_analysis_output change_perspective(const DIC_analysis_output &, INTERP)
Definition ncorr.cpp:3366
void save_ncorr_data_over_img_video(const std::string &, const std::vector< Image2D > &, const std::vector< Data2D > &, double, double, double, double, bool, bool, bool, const std::string &, double, double, double, ROI2D::difference_type, int, double, int)
Definition ncorr.cpp:4189
Disp2D add_with_rois(const std::vector< Disp2D > &disps, const std::vector< ROI2D > &rois, INTERP interp_type)
Definition ncorr.cpp:1246
DIC_analysis_output set_units(const DIC_analysis_output &, const std::string &, double)
Definition ncorr.cpp:3451
void imshow_ncorr_data_over_img(const Image2D &, const Data2D &, ROI2D::difference_type=-1)
Definition ncorr.cpp:4132
STRAIN
Definition ncorr.h:647
ROI2D update(const ROI2D &, const Disp2D &, INTERP, ROI_UPDATE_MODE mode=ROI_UPDATE_MODE::SKIP_ALL)
Definition ncorr.cpp:681
DIC_analysis_output DIC_analysis(const DIC_analysis_input &)
Definition ncorr.cpp:2718
DIC_analysis_output exact_matlab_DIC_analysis_impl(const DIC_analysis_parallel_input &input, bool run_in_parallel, const std::string &mode_name)
Definition ncorr.cpp:5684
Disp2D RGDIC_without_thread(const Array2D< double > &A_ref, const Array2D< double > &A_cur, const ROI2D &roi, ROI2D::difference_type scalefactor, INTERP interp_type, SUBREGION subregion_type, ROI2D::difference_type r, double cutoff_corrcoef, bool debug, const std::vector< SeedParams > &seeds_by_region={}, bool seeds_are_optimized=false)
Definition ncorr.cpp:2125
SeedAnalysisResult analyze_seeds(const details::subregion_nloptimizer &sr_nloptimizer, const Array2D< double > &ref_gs, const ROI2D &roi, const std::vector< SeedParams > &seed_positions, ROI2D::difference_type radius, int cutoff_iteration, double cutoff_max_diffnorm, double cutoff_max_corrcoef, bool debug=true)
Definition ncorr.cpp:4699
PERSPECTIVE
Definition ncorr.h:249
DIC_analysis_output matlab_DIC_analysis_sequential(const DIC_analysis_input &DIC_input, const std::vector< SeedParams > &seeds_by_region={}, bool seeds_are_optimized=false)
Definition ncorr.cpp:5245
DIC_analysis_output change_perspective_with_inversion(const DIC_analysis_output &, INTERP)
Definition ncorr.cpp:3400
matlab_difference_type ref_idx
Definition ncorr.cpp:4809
std::vector< std::vector< SeedParams > > seeds_by_frame
Definition ncorr.cpp:4810
std::vector< SeedParams > terminal_seeds
Definition ncorr.cpp:4811
ROI2D::difference_type difference_type
Definition ncorr.h:179
ROI_UPDATE_MODE roi_update_mode
Definition ncorr.h:243
difference_type r
Definition ncorr.h:238
SUBREGION subregion_type
Definition ncorr.h:237
difference_type scalefactor
Definition ncorr.h:235
ACCUMULATION_MODE accumulation_mode
Definition ncorr.h:244
static DIC_analysis_input load(std::ifstream &)
Definition ncorr.cpp:2435
difference_type num_threads
Definition ncorr.h:239
std::vector< Image2D > imgs
Definition ncorr.h:233
std::vector< Disp2D > disps
Definition ncorr.h:274
PERSPECTIVE perspective_type
Definition ncorr.h:275
ROI2D::difference_type difference_type
Definition ncorr.h:252
static DIC_analysis_output load(std::ifstream &)
Definition ncorr.cpp:2559
std::vector< SeedParams > seeds_by_region
Definition ncorr.h:446
DIC_analysis_input base_input
Definition ncorr.h:443
std::vector< ROI2D > step_rois
Definition ncorr.h:303
ROI2D::difference_type difference_type
Definition ncorr.h:282
std::vector< difference_type > step_ref_idx
Definition ncorr.h:305
static DIC_analysis_step_data load(std::ifstream &)
Definition ncorr.cpp:2639
std::vector< Disp2D > step_disps
Definition ncorr.h:301
std::vector< Array2D< double > > sub
Definition ROI2D.h:228
Array2D< double > add
Definition ROI2D.h:227
difference_type top
Definition ROI2D.h:180
Array2D< difference_type > noderange
Definition ROI2D.h:187
difference_type points
Definition ROI2D.h:188
difference_type bottom
Definition ROI2D.h:181
bool in_nlinfo(difference_type, difference_type) const
Definition ROI2D.cpp:387
difference_type right
Definition ROI2D.h:183
Array2D< difference_type > nodelist
Definition ROI2D.h:186
difference_type left_nl
Definition ROI2D.h:184
static std::pair< std::vector< ROI2D::region_nlinfo >, bool > form_nlinfos(const Array2D< bool > &, ROI2D::difference_type=0)
Definition ROI2D.cpp:466
difference_type left
Definition ROI2D.h:182
std::vector< SeedParams > seeds
Definition ncorr.h:376
std::vector< SeedConvergence > convergence
Definition ncorr.h:377
difference_type x
Definition ncorr.h:316
static SeedParams from_array(const Array2D< double > &params)
Definition ncorr.h:352
difference_type y
Definition ncorr.h:317
double corrcoef
Definition ncorr.h:326
Array2D< double >::interpolator v_interp
Definition ncorr.cpp:5462
Array2D< double >::interpolator cc_interp
Definition ncorr.cpp:5464
Array2D< double >::interpolator u_interp
Definition ncorr.cpp:5463
const ROI2D::region_nlinfo * nlinfo
Definition ncorr.cpp:5471
DIC_analysis_output DIC_output
Definition ncorr.h:558
DIC_analysis_input DIC_input
Definition ncorr.h:557
difference_type r
Definition ncorr.h:560
ROI2D::difference_type difference_type
Definition ncorr.h:530
static strain_analysis_input load(std::ifstream &)
Definition ncorr.cpp:3673
ROI2D::difference_type difference_type
Definition ncorr.h:564
static strain_analysis_output load(std::ifstream &)
Definition ncorr.cpp:3736
std::vector< Strain2D > strains
Definition ncorr.h:585