CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
step_d_workflow.cpp
Go to the documentation of this file.
1
7#include "step_d_workflow.h"
8#include "Array2D.h"
9#include "mat_writer.h"
10#include "mat_reader.h"
11#include "data_serializer.h"
13#include "ncorr.h"
14#include "utils.h"
15#include "matlab_functions.h"
16#include "logging.h"
17#ifdef CPPXDIC_HAVE_CUNCORR
18#include "dic/cuncorr_dic.h"
19#endif
20#include <iostream>
21#include <opencv2/imgcodecs.hpp>
22#include <sstream>
23#include <iomanip>
24#include <filesystem>
25#include <cmath>
26#include <map>
27#include <cctype>
28#include <array>
29#include <limits>
30#include <algorithm>
31
32namespace {
33
34cv::Mat makeDebugCanvas(const cv::Mat& gray) {
35 cv::Mat normalized;
36 if (gray.empty()) {
37 return normalized;
38 }
39 if (gray.type() == CV_8UC1) {
40 normalized = gray.clone();
41 } else {
42 cv::normalize(gray, normalized, 0, 255, cv::NORM_MINMAX, CV_8U);
43 }
44 cv::Mat color;
45 cv::cvtColor(normalized, color, cv::COLOR_GRAY2BGR);
46 return color;
47}
48
49void overlayMask(cv::Mat& canvas, const cv::Mat& mask, const cv::Scalar& color) {
50 if (canvas.empty() || mask.empty()) {
51 return;
52 }
53 cv::Mat mask_binary;
54 if (mask.type() == CV_8UC1) {
55 cv::threshold(mask, mask_binary, 0, 255, cv::THRESH_BINARY);
56 } else {
57 mask.convertTo(mask_binary, CV_8U);
58 cv::threshold(mask_binary, mask_binary, 0, 255, cv::THRESH_BINARY);
59 }
60 cv::Mat overlay(canvas.size(), canvas.type(), color);
61 cv::Mat blended;
62 cv::addWeighted(canvas, 0.75, overlay, 0.25, 0.0, blended);
63 blended.copyTo(canvas, mask_binary);
64}
65
66void drawSeed(cv::Mat& canvas, const cppxdic::SeedPoint& seed, const cv::Scalar& color) {
67 if (canvas.empty() || seed.pw.size() < 2) {
68 return;
69 }
70 const cv::Point center(seed.pw[0] - 1, seed.pw[1] - 1);
71 cv::drawMarker(canvas, center, color, cv::MARKER_CROSS, 18, 2);
72 cv::circle(canvas, center, 5, color, 2);
73}
74
75void drawSeedDisplacement(cv::Mat& canvas,
76 const cppxdic::SeedPoint& before,
77 const cppxdic::SeedPoint& after,
78 const cv::Scalar& color) {
79 if (canvas.empty() || before.pw.size() < 2 || after.pw.size() < 2) {
80 return;
81 }
82 const cv::Point p0(before.pw[0] - 1, before.pw[1] - 1);
83 const cv::Point p1(after.pw[0] - 1, after.pw[1] - 1);
84 cv::arrowedLine(canvas, p0, p1, color, 2, cv::LINE_AA, 0, 0.15);
85}
86
87std::string sanitizeStageName(const std::string& stage_name) {
88 std::string out = stage_name;
89 for (char& ch : out) {
90 if (!std::isalnum(static_cast<unsigned char>(ch))) {
91 ch = '_';
92 }
93 }
94 return out;
95}
96
97} // namespace
98
99namespace cppxdic {
100
101StepDWorkflow::StepDWorkflow(const Config& config) : config_(config) {
102 // Initialize with default DIC constants
103 setupStepParameters();
104}
105
106std::tuple<std::string, std::vector<int>, bool>
107StepDWorkflow::execute(const std::string& trial, int stereopair, const StagePlan& plan) {
108 LOG_INFO << "-------------------------------------------";
109 LOG_INFO << "-------------------------------------------";
110 LOG_INFO << "Digital Image Correlation analysis launch";
111
112 // CHECKPOINT SYSTEM (matching MATLAB implementation):
113 // The following checkpoint files are checked/created to avoid redundant computation:
114 // 1. REF_MASK_{reftrial}_{phase}_pair{stereopair}.mat - ROI mask (loaded from base path)
115 // 2. REF_SEED_{reftrial}_{phase}_pair{stereopair}.mat - Seed point (loaded from base path)
116 // 3. MATCHING2{reftrial}_pair{stereopair}.bin - REF-to-trial matching (in output path)
117 // 4. ncorr{cam1}{cam2}.bin - Inter-camera matching (in output path)
118 // 5. ncorr{cam1}.bin - Camera 1 tracking results (in output path)
119 // 6. ncorr{cam2}.bin - Camera 2 tracking results (in output path)
120 // 7. dic_info_data_target_pair{stereopair}.mat - Trial metadata (in output path)
121 //
122 // STAGE DECOMPOSITION (plan): every sub-step below checkpoints to / loads from
123 // disk, so a process can run only some sub-steps and rely on a prior process's
124 // outputs for the rest:
125 // plan.match -> ROI/seed + REF->trial matching (3) + camera matching (4) + dic_info (7)
126 // plan.track -> per-camera tracking (5,6); needs (3,4) on disk (loaded here)
127 // plan.format -> formatOutput (myDIC2DpairResults); needs (4,5,6) on disk
128
129 // I. Preps analysis
130 // 1. Load protocol and determine reference trial
131 if (!loadProtocol()) {
132 LOG_ERROR << "Failed to load protocol";
133 return {"", {}, false};
134 }
135
136 std::string reftrial = determineReferenceTrial(trial);
137 LOG_INFO << "Reference trial: " << reftrial;
138
139 // 2. Setup parameters
140 setupBaseParameters(trial, stereopair, reftrial);
141
142 // Resolve this pair's camera ids for per-camera tracking selection.
143 int cam_first_num = 0, cam_second_num = 0;
144 Utils::getCamerasForPair(stereopair, cam_first_num, cam_second_num);
145 const bool do_track1 = plan.track && (plan.only_cam == 0 || plan.only_cam == cam_first_num);
146 const bool do_track2 = plan.track && (plan.only_cam == 0 || plan.only_cam == cam_second_num);
147 // Camera matching is needed to (re)produce ncorr{cam1}{cam2} for the match stage,
148 // and to recover refmask_trial_matched + seed for camera-2 tracking.
149 const bool need_matching = plan.match || do_track2;
150 // The frame stack (import + saturation + ROI/seed) is needed by matching/tracking
151 // but NOT by a format-only stage (which just loads ncorr binaries from disk).
152 const bool need_frames = plan.match || plan.track;
153
154 if (need_frames) {
155 // 3. Import video frames
156 std::vector<cv::Mat> cam_first_raw, cam_second_raw;
157 LOG_INFO << "Reading video data... ";
158 if (!importVideoFrames(trial, stereopair, cam_first_raw, cam_second_raw)) {
159 LOG_ERROR << "Failed to import video frames";
160 return {"", {}, false};
161 }
162 LOG_INFO << "Reading done. Frames: " << cam_first_raw.size();
163
164 // 4. Phase-specific frame selection
165 if (config_.phase_id == "slide1") {
166 size_t keep = cam_first_raw.size() / 2 + 5;
167 cam_first_raw.resize(keep);
168 cam_second_raw.resize(keep);
169 LOG_INFO << "Phase 'slide1': keeping " << keep << " frames";
170 }
171
172 // 5. Saturation (satur.m). Skipped for pre-filtered inputs (im_saturation_mode=false).
173 std::vector<cv::Mat> cam_first_satur, cam_second_satur;
174 if (config_.im_saturation_mode) {
175 LOG_INFO << "Applying saturation (limit_grayscale=" << base_params_.limit_grayscale << ")...";
176 performSaturation(cam_first_raw, cam_second_raw, cam_first_satur, cam_second_satur);
177 } else {
178 LOG_INFO << "Skipping saturation (im_saturation_mode=false): raw frames used as-is";
179 cam_first_satur = cam_first_raw;
180 cam_second_satur = cam_second_raw;
181 }
182
183 // II. ROI, Seed, and Matching REF to Trial at frame 1
184 // (writes MATCHING2*.bin when absent; loads + applies it when present)
185 cv::Mat refmask_REF, refmask_trial;
186 SeedPoint ref_seed_point, initial_seed_point_set1;
187 LOG_INFO << "Initializing ROI, seed, and matching REF to Trial...";
188 if (!initializeROIAndSeed(cam_first_satur, refmask_REF, refmask_trial,
189 ref_seed_point, initial_seed_point_set1)) {
190 LOG_ERROR << "Failed to initialize ROI and seed";
191 return {"", {}, false};
192 }
193
194 LOG_INFO << "--> STEP: ROI loaded and formatted";
195 LOG_INFO << "--> STEP: SEED loaded and formatted";
196 LOG_INFO << "--> STEP: Matching REF to Trial loaded and formatted";
197
198 // post-III. Image filtering (only needed for tracking)
199 std::vector<cv::Mat> cam_first, cam_second;
200 if (plan.track) {
201 if (config_.im_filter_mode) {
202 LOG_INFO << "Applying image filtering...";
203 applyImageFiltering(cam_first_satur, cam_second_satur, refmask_trial,
204 cam_first, cam_second);
205 LOG_INFO << "--> STEP: filtering done";
206 } else {
207 LOG_INFO << "Skipping image filtering";
208 cam_first = cam_first_satur;
209 cam_second = cam_second_satur;
210 }
211 }
212
213 // III. Matching inside a Trial between cameras (cam1 -> cam2 at frame 1)
214 // (writes ncorr{cam1}{cam2}.bin when absent; loads it when present)
215 cv::Mat refmask_trial_matched;
216 SeedPoint initial_seed_point_set2;
217 if (need_matching) {
218 LOG_INFO << "\nPerforming camera matching...";
219 if (!performMatching(cam_first_satur, cam_second_satur, refmask_trial,
220 initial_seed_point_set1, refmask_trial_matched,
221 initial_seed_point_set2)) {
222 LOG_ERROR << "Failed matching step";
223 return {"", {}, false};
224 }
225 }
226
227 // IV. Save trial information (frame count metadata)
228 saveTrialInfo(trial, stereopair, cam_first_satur.size());
229
230 // V. Tracking camera 1
231 if (do_track1) {
232 LOG_INFO << "\nPerforming tracking camera 1...";
233 if (!performTracking1(cam_first, refmask_trial, initial_seed_point_set1)) {
234 LOG_ERROR << "Failed tracking1 step";
235 return {"", {}, false};
236 }
237 }
238
239 // VI. Tracking camera 2
240 if (do_track2) {
241 LOG_INFO << "\nPerforming tracking camera 2...";
242 if (!performTracking2(cam_second, refmask_trial_matched, initial_seed_point_set2)) {
243 LOG_ERROR << "Failed tracking2 step";
244 return {"", {}, false};
245 }
246 }
247 }
248
249 // Determine pair order
250 std::vector<int> pairOrder;
251 bool pairForced;
252 unsigned int trial_idx = std::stoi(trial);
253 if (trial_idx > 0 && trial_idx < protocol_info_.dircond.size()) {
254 protocol_info_.getPairOrder(protocol_info_.dircond[trial_idx], pairOrder, pairForced);
255 } else {
256 pairOrder = {1, 2};
257 pairForced = false;
258 }
259 // The protocol rule only orders the first two pairs; rigs with more stereopairs
260 // append the remaining ones in natural order so Step E stitches all of them.
261 for (int p = 1; p <= config_.num_pair; ++p) {
262 if (std::find(pairOrder.begin(), pairOrder.end(), p) == pairOrder.end()) {
263 pairOrder.push_back(p);
264 }
265 }
266
267 // Post-preps. Format output (loads ncorr{cam1},{cam2},{cam1cam2} -> myDIC2DpairResults)
268 if (plan.format) {
269 formatOutput(trial, stereopair, pairOrder, pairForced);
270 }
271
272 LOG_INFO << "--> STEP: Ncorr analysis completed";
273
274 return {base_params_.outputPath, pairOrder, pairForced};
275}
276
277void StepDWorkflow::setupBaseParameters(const std::string& trial,
278 int stereopair,
279 const std::string& reftrial) {
280 base_params_.baseDataPath = config_.data_path;
281 base_params_.baseResultPath = config_.dic_path;
282 base_params_.subject = config_.subject_id;
283 base_params_.material = config_.material;
284 base_params_.trial = trial;
285 base_params_.stereopair = stereopair;
286 base_params_.phase = config_.phase_id;
287 base_params_.reftrial = reftrial;
288 base_params_.jump = config_.frame_jump;
289 base_params_.idxstart_set = config_.idx_frame_start;
290 base_params_.idxend_set = config_.idx_frame_end;
291
292 // Grayscale limit: explicit config value wins; otherwise the legacy MATLAB rule
293 // keyed on the subject NUMBER (S01..S07 -> 70, S08+ -> 100). Subject ids that
294 // carry no digits (e.g. "Artem") count as 0 and get the S<8 default, so such
295 // datasets should set limit_grayscale (or im_saturation_mode=false) explicitly.
296 if (config_.limit_grayscale > 0) {
297 base_params_.limit_grayscale = config_.limit_grayscale;
298 } else {
299 int subject_num = 0;
300 for (char ch : config_.subject_id) {
301 if (std::isdigit(ch)) {
302 subject_num = subject_num * 10 + (ch - '0');
303 }
304 }
305 base_params_.limit_grayscale = (subject_num < 8) ?
306 DICConstants::LIMIT_GRAYSCALE_DEFAULT : DICConstants::LIMIT_GRAYSCALE_S8_PLUS;
307 }
308
309 // Set camera numbers
310 Utils::getCamerasForPair(stereopair, base_params_.cam_1, base_params_.cam_2);
311 // Set output path
312 base_params_.outputPath = Utils::buildPath(base_params_, true, true, true, false);
313
314 // Create output directory
315 std::filesystem::create_directories(base_params_.outputPath);
316
317 // Set file paths
318 base_params_.roifile = Utils::buildRoiFilePath(base_params_, reftrial, stereopair);
319 base_params_.matchingfile = Utils::buildMatchingFilePath(base_params_, reftrial, stereopair);
320 base_params_.seedfile = Utils::buildSeedFilePath(base_params_, reftrial, stereopair);
321}
322
323void StepDWorkflow::setupStepParameters() {
324 // Setup tracking parameters (camera 1) — defaults from DICConstants,
325 // then override with config values from dic_params.txt (step_d_* keys)
326 step1_params_.type = config_.step_d.analysis_type;
327 step1_params_.radius = config_.step_d.radius;
328 step1_params_.spacing = config_.step_d.spacing;
329 step1_params_.cutoff_diffnorm = config_.step_d.cutoff_diffnorm;
330 step1_params_.cutoff_iteration = config_.step_d.cutoff_iteration;
331 step1_params_.total_threads = config_.step_d.total_threads;
333 step1_params_.stepanalysis_params.type = config_.step_d.seed_type;
334 step1_params_.stepanalysis_params.auto_update = config_.step_d.auto_update;
335 step1_params_.stepanalysis_params.step = config_.step_d.step_ref_change;
336
337 // Setup tracking parameters (camera 2) - same as camera 1
338 step2_params_ = step1_params_;
339
340 // Setup matching parameters (camera 1 -> camera 2)
341 // Uses step_e config if available, with larger default radius
342 step1_2_params_ = step1_params_;
343 step1_2_params_.radius = config_.step_e.radius;
344 step1_2_params_.cutoff_diffnorm = config_.step_e.cutoff_diffnorm;
345}
346
347bool StepDWorkflow::loadProtocol() {
348 // Load protocol MAT file
349 std::string protocol_dir = Utils::buildProtocolDir(config_, true, true, true, true);
350 LOG_INFO << "protocol_path: " << protocol_dir;
351
352 // Find MAT file in protocol directory
353 auto mat_files = Utils::findFiles(protocol_dir, "*.mat");
354
355 if (mat_files.empty()) {
356 // Without a protocol the only things we cannot derive are the automatic
357 // reference trial and the direction-based pair order. A manual
358 // ref_trial_id makes the former unnecessary; the latter falls back to the
359 // natural 1..num_pair order. Datasets from other rigs (e.g. MNG .tsv
360 // protocols) therefore run with ref_trial_id set explicitly.
361 if (config_.ref_trial_id > 0) {
362 LOG_WARN << "No protocol .mat file in " << protocol_dir
363 << " — using ref_trial_id=" << config_.ref_trial_id
364 << " and the default pair order";
365 protocol_info_ = ProtocolInfo{};
366 return true;
367 }
368 LOG_ERROR << "No protocol file found in: " << protocol_dir
369 << " (set ref_trial_id / --reftrial to run without a protocol)";
370 return false;
371 }
372
373 // Load first MAT file found using MatReader
374 std::string protocol_file = mat_files[0];
375 LOG_INFO << "Loading protocol: " << protocol_file;
376
377 cppxdic::ProtocolFileData protocol_data;
378 if (!MatReader::loadProtocol(protocol_file, protocol_data)) {
379 LOG_ERROR << "Failed to parse protocol file: " << protocol_file;
380 return false;
381 }
382
383 // Convert ProtocolFileData -> ProtocolInfo
384 // Index 0 is a dummy entry so that 1-based trial indexing works directly
385 // (MATLAB trial "001" -> index 1, "002" -> index 2, etc.)
386 protocol_info_.dircond.clear();
387 protocol_info_.nfcond.clear();
388 protocol_info_.spdcond.clear();
389 protocol_info_.repcond.clear();
390 protocol_info_.spddxlcond.clear();
391
392 // Dummy index 0
393 protocol_info_.dircond.push_back("");
394 protocol_info_.nfcond.push_back(0);
395 protocol_info_.spdcond.push_back(0);
396 protocol_info_.repcond.push_back(0);
397 protocol_info_.spddxlcond.push_back(0.0);
398
399 for (const auto& entry : protocol_data.trials) {
400 protocol_info_.dircond.push_back(entry.direction);
401 protocol_info_.nfcond.push_back(static_cast<int>(entry.force));
402 protocol_info_.spdcond.push_back(static_cast<int>(entry.speed));
403 protocol_info_.repcond.push_back(entry.repetition);
404 protocol_info_.spddxlcond.push_back(0.08);
405 }
406
407 LOG_INFO << "Protocol loaded: " << protocol_data.trials.size() << " trials";
408 if (config_.debug_mode) {
409 for (size_t i = 1; i < protocol_info_.dircond.size(); ++i) {
410 LOG_DEBUG << " Trial " << i << ": dir=" << protocol_info_.dircond[i]
411 << " nf=" << protocol_info_.nfcond[i]
412 << " spd=" << protocol_info_.spdcond[i]
413 << " rep=" << protocol_info_.repcond[i];
414 }
415 }
416 return true;
417}
418
419std::string StepDWorkflow::determineReferenceTrial(const std::string& trial) {
420 // Manual override from config
421 if (config_.ref_trial_id > 0) {
422 std::ostringstream oss;
423 oss << std::setw(3) << std::setfill('0') << config_.ref_trial_id;
424 LOG_INFO << "Reference trial (manual override): " << oss.str();
425 return oss.str();
426 }
427
428 // Protocol-based reference trial selection (matching MATLAB stepD_2DDIC.m)
429 unsigned int trial_idx = std::stoi(trial);
430
431 if (trial_idx > 0 && trial_idx < protocol_info_.nfcond.size()) {
432 int trial_nf = protocol_info_.nfcond[trial_idx];
433
434 // Phase "loading" OR trial nf==1 → ref = first trial with nf==1 AND dir=="Ubnf"
435 if (config_.phase_id == "loading" || trial_nf == 1) {
436 for (size_t i = 1; i < protocol_info_.nfcond.size(); ++i) {
437 if (protocol_info_.nfcond[i] == 1 &&
438 protocol_info_.dircond[i] == "Ubnf") {
439 std::ostringstream oss;
440 oss << std::setw(3) << std::setfill('0') << i;
441 LOG_INFO << "Reference trial (nf=1, Ubnf): " << oss.str();
442 return oss.str();
443 }
444 }
445 }
446 // Phase "slide1" AND trial nf==5 → ref = first trial with nf==5 AND dir=="Ubnf"
447 else if (config_.phase_id == "slide1" && trial_nf == 5) {
448 for (size_t i = 1; i < protocol_info_.nfcond.size(); ++i) {
449 if (protocol_info_.nfcond[i] == 5 &&
450 protocol_info_.dircond[i] == "Ubnf") {
451 std::ostringstream oss;
452 oss << std::setw(3) << std::setfill('0') << i;
453 LOG_INFO << "Reference trial (nf=5, Ubnf): " << oss.str();
454 return oss.str();
455 }
456 }
457 }
458
459 LOG_WARN << "No matching reference trial found for trial " << trial
460 << " (phase=" << config_.phase_id << ", nf=" << trial_nf << ")";
461 } else {
462 LOG_WARN << "Trial index " << trial_idx << " out of protocol range";
463 }
464
465 // Fallback to current trial
466 LOG_INFO << "Reference trial (fallback): " << trial;
467 return trial;
468}
469
470bool StepDWorkflow::importVideoFrames(const std::string& trial,
471 int stereopair,
472 std::vector<cv::Mat>& cam_first_raw,
473 std::vector<cv::Mat>& cam_second_raw,
474 int maxFrames) {
475 // Use Utils::importVid to get frame paths
476 std::vector<std::string> cam1_frames, cam2_frames;
477
478 // Convert trial string to integer for Utils::importVid
479 int trial_num = std::stoi(trial);
480
481 // Import video frames using Utils
482 if (!Utils::importVid(config_, trial_num, stereopair, cam1_frames, cam2_frames, maxFrames)) {
483 LOG_ERROR << "Failed to import video frames for trial " << trial
484 << " stereopair " << stereopair;
485 return false;
486 }
487
488 // Check that we got frames
489 if (cam1_frames.empty() || cam2_frames.empty()) {
490 LOG_ERROR << "No frames imported for trial " << trial
491 << " stereopair " << stereopair;
492 return false;
493 }
494
495 // Check frame count consistency
496 if (cam1_frames.size() != cam2_frames.size()) {
497 LOG_ERROR << "Frame count mismatch: cam1=" << cam1_frames.size()
498 << " cam2=" << cam2_frames.size();
499 return false;
500 }
501
502 // Load frames into cv::Mat vectors
503 cam_first_raw.clear();
504 cam_second_raw.clear();
505 cam_first_raw.reserve(cam1_frames.size());
506 cam_second_raw.reserve(cam2_frames.size());
507
508 for (size_t i = 0; i < cam1_frames.size(); ++i) {
509 // Load first camera frame
510 cv::Mat img1 = cv::imread(cam1_frames[i], cv::IMREAD_GRAYSCALE);
511 if (img1.empty()) {
512 LOG_ERROR << "Failed to load frame: " << cam1_frames[i];
513 return false;
514 }
515
516 // Load second camera frame
517 cv::Mat img2 = cv::imread(cam2_frames[i], cv::IMREAD_GRAYSCALE);
518 if (img2.empty()) {
519 LOG_ERROR << "Failed to load frame: " << cam2_frames[i];
520 return false;
521 }
522
523 cam_first_raw.push_back(img1);
524 cam_second_raw.push_back(img2);
525 }
526
527 LOG_INFO << "Loaded " << cam_first_raw.size() << " frames for each camera";
528 return true;
529}
530
531void StepDWorkflow::performSaturation(const std::vector<cv::Mat>& cam_first_raw,
532 const std::vector<cv::Mat>& cam_second_raw,
533 std::vector<cv::Mat>& cam_first_satur,
534 std::vector<cv::Mat>& cam_second_satur) {
535 cam_first_satur = ImageProcessor::saturate(cam_first_raw, base_params_.limit_grayscale);
536 cam_second_satur = ImageProcessor::saturate(cam_second_raw, base_params_.limit_grayscale);
537}
538
539bool StepDWorkflow::initializeROIAndSeed(const std::vector<cv::Mat>& cam_first_satur,
540 cv::Mat& refmask_REF,
541 cv::Mat& refmask_trial,
542 SeedPoint& ref_seed_point,
543 SeedPoint& after_disp_seed_point) {
544 std::vector<cv::Mat> reftrial_cam_first_raw;
545
546 // Load or create ROI
547 refmask_REF = ROIManager::loadOrCreateROI(base_params_, cam_first_satur[0]);
548 if (refmask_REF.empty()) {
549 return false;
550 }
551
552 // Load or create seed (seed.pw = pixel world coordinates)
553 ref_seed_point = ROIManager::loadOrCreateSeed(base_params_, refmask_REF);
554 ref_seed_point.sw = ROIManager::mapPixel2Subset(ref_seed_point.pw,
555 step1_params_.spacing);
556
557 // Check for MATCHING file (ncorr binary, saved directly in output dir)
558 if (!std::filesystem::exists(base_params_.matchingfile)) {
559 // No matching file - compute it
560 LOG_INFO << "\n--> STEP: MATCHING file computation)";
561 std::vector<cv::Mat> reftrial_cam_second_raw;
562 LOG_INFO << "Reading REF Trial video data...";
563 // Only the REF trial's first frame is used for REF->trial matching, so
564 // import a single frame instead of decoding the entire reference video.
565 if (!importVideoFrames(base_params_.reftrial,
566 base_params_.stereopair,
567 reftrial_cam_first_raw,
568 reftrial_cam_second_raw,
569 /*maxFrames=*/1)) {
570 LOG_ERROR << "Failed to import REF Trial video frames";
571 return false;
572 }
573 LOG_INFO << "Reading REF Trial video data done. Frames: " << reftrial_cam_first_raw.size();
574
575 std::ostringstream message_oss;
576 message_oss << "Matching Pair " << base_params_.stereopair << ": trial " << base_params_.reftrial << "'s frame 1 TO trial " << base_params_.trial << "'s frame 1";
577 if (!matchingInitialFrame(reftrial_cam_first_raw,
578 cam_first_satur,
579 refmask_REF,
580 base_params_.matchingfile,
581 message_oss.str(),
582 ref_seed_point,
583 refmask_trial,
584 after_disp_seed_point)) {
585 return false;
586 }
587 } else {
588 LOG_INFO << "Checkpoint found: " << base_params_.matchingfile;
589 LOG_INFO << "--> STEP: MATCHING file loaded from checkpoint (skipped computation)";
590 }
591
592 // Load matching displacement fields from ncorr binary to map seed point
593 if (std::filesystem::exists(base_params_.matchingfile)) {
594 auto dic_output = ncorr::DIC_analysis_output::load(base_params_.matchingfile);
595
596 if (updateMaskAndSeedFromOutput(refmask_REF, ref_seed_point, dic_output,
597 refmask_trial, after_disp_seed_point)) {
598 LOG_INFO << "--> STEP: MATCHING transformation applied";
599 if (config_.debug_mode) {
600 if (reftrial_cam_first_raw.empty()) {
601 std::vector<cv::Mat> reftrial_cam_second_raw;
602 // Debug panel only needs the REF trial's first frame.
603 importVideoFrames(base_params_.reftrial,
604 base_params_.stereopair,
605 reftrial_cam_first_raw,
606 reftrial_cam_second_raw,
607 /*maxFrames=*/1);
608 }
609 writeMatchingDebugPanel("ipm",
610 reftrial_cam_first_raw.empty() ? cam_first_satur[0] : reftrial_cam_first_raw[0],
611 cam_first_satur[0],
612 refmask_REF,
613 refmask_trial,
614 ref_seed_point,
615 after_disp_seed_point,
616 dic_output);
617 }
618 return true;
619 } else {
620 LOG_WARN << "MATCHING file has no displacements";
621 }
622 }
623
624 LOG_WARN << "Something went wrong for the MATCHING";
625 return false;
626}
627
628bool StepDWorkflow::matchingInitialFrame(const std::vector<cv::Mat>& cam_ref,
629 const std::vector<cv::Mat>& cam_cur,
630 const cv::Mat& refmask_ref,
631 const std::string ncorr_matching_path,
632 const std::string message,
633 const SeedPoint& ref_seed_point,
634 cv::Mat& refmask_cur_matched,
635 SeedPoint& after_disp_seed_point) {
636 LOG_INFO << message;
637
638 // MATLAB uses a two-frame current stack during matching to avoid an ncorr edge case.
639 std::vector<cv::Mat> cur_imgs;
640 cur_imgs.reserve(2);
641 cur_imgs.push_back(cam_cur[0]);
642 cur_imgs.push_back(cam_ref[0]);
643
644 // Set seed for matching
645 step1_2_params_.initial_seed = {
646 static_cast<int>(ref_seed_point.pw[0]),
647 static_cast<int>(ref_seed_point.pw[1])
648 };
649
650 // Run ncorr analysis and save to cache-compatible path (for formatOutput to find).
651 // The user-drawn seed is chosen on the REFERENCE view; near the ROI boundary the
652 // other camera may see that spot under a very oblique angle and the inter-camera
653 // seed does not converge ("could not seed any current image"). In that case retry
654 // once from the ROI centre of mass, which sits in the best-overlapping part of the
655 // surface; the mapped seed for the second camera is then derived from that point
656 // too (the first camera keeps the user seed for tracking).
657 SeedPoint matching_seed = ref_seed_point;
658 ncorr::DIC_analysis_output dic_output;
659 try {
660 dic_output = runNcorrAnalysis(cam_ref[0], cur_imgs, refmask_ref,
661 matching_seed, step1_2_params_,
662 ncorr_matching_path, false, true); // false for parallel processing, true for no update
663 } catch (const std::runtime_error& e) {
664 const std::string what = e.what();
665 if (what.find("could not seed") == std::string::npos) throw;
666 const std::vector<int> c = ROIManager::findROICenter(refmask_ref); // [x, y]
667 matching_seed = SeedPoint(c[0], c[1]);
668 matching_seed.sw = ROIManager::mapPixel2Subset(matching_seed.pw, step1_2_params_.spacing);
669 LOG_WARN << "Matching seed (" << ref_seed_point.pw[0] << "," << ref_seed_point.pw[1]
670 << ") did not converge in the other camera (" << what
671 << "); retrying from the ROI centre of mass (" << matching_seed.pw[0] << ","
672 << matching_seed.pw[1] << ")";
673 step1_2_params_.initial_seed = {matching_seed.pw[0], matching_seed.pw[1]};
674 dic_output = runNcorrAnalysis(cam_ref[0], cur_imgs, refmask_ref,
675 matching_seed, step1_2_params_,
676 ncorr_matching_path, false, true);
677 }
678
679 if (!updateMaskAndSeedFromOutput(refmask_ref, matching_seed, dic_output,
680 refmask_cur_matched, after_disp_seed_point)) {
681 LOG_ERROR << "No displacement output from matching";
682 return false;
683 }
684
685 LOG_INFO << "--> STEP: " << message << " done";
686 return true;
687}
688
689bool StepDWorkflow::performMatching(const std::vector<cv::Mat>& cam_first_satur,
690 const std::vector<cv::Mat>& cam_second_satur,
691 cv::Mat& refmask,
692 SeedPoint& ref_seed_point,
693 cv::Mat& after_disp_mask,
694 SeedPoint& after_disp_seed_point) {
695 LOG_INFO << "MATCHING STEP - RUN #1";
696
697 // Get camera numbers
698 int cam_1, cam_2;
699 Utils::getCamerasForPair(base_params_.stereopair, cam_1, cam_2);
700
701 // ncorr matching output path (always .bin — ncorr native format)
702 std::string ncorr_matching_path = base_params_.outputPath + "/ncorr" +
703 std::to_string(cam_1) + std::to_string(cam_2) + ".bin";
704
705 // Compute matching if output doesn't exist
706 if (!std::filesystem::exists(ncorr_matching_path)) {
707 LOG_INFO << "\n--> STEP: MATCHING file computation)";
708
709 std::ostringstream message_oss;
710 message_oss << "Matching Inside Camera Pair (1-2) : Cam1's frame 1 VS cam2's frame 1";
711
712 matchingInitialFrame(cam_first_satur,
713 cam_second_satur,
714 refmask,
715 ncorr_matching_path,
716 message_oss.str(),
717 ref_seed_point,
718 after_disp_mask,
719 after_disp_seed_point);
720 } else {
721 LOG_INFO << "Checkpoint found: " << ncorr_matching_path;
722 LOG_INFO << "--> STEP: MATCHING loaded from checkpoint (skipped computation)";
723 }
724
725 // Load matching displacement fields from ncorr binary to map seed point
726 if (std::filesystem::exists(ncorr_matching_path)) {
727 auto dic_output = ncorr::DIC_analysis_output::load(ncorr_matching_path);
728
729 if (updateMaskAndSeedFromOutput(refmask, ref_seed_point, dic_output,
730 after_disp_mask, after_disp_seed_point)) {
731 LOG_INFO << "--> STEP: MATCHING transformation applied";
732 if (config_.debug_mode) {
733 writeMatchingDebugPanel("icm",
734 cam_first_satur[0],
735 cam_second_satur[0],
736 refmask,
737 after_disp_mask,
738 ref_seed_point,
739 after_disp_seed_point,
740 dic_output);
741 }
742 return true;
743 } else {
744 LOG_WARN << "Matching file has no displacements";
745 }
746 }
747
748 LOG_WARN << "Something went wrong with the matching";
749 return false;
750}
751
752bool StepDWorkflow::performTracking(const int tracking_number,
753 const std::vector<cv::Mat>& cam_frames,
754 const cv::Mat& refmask,
755 const SeedPoint& initial_seed_point) {
756 // Get actual camera number for this pair
757 int cam_1, cam_2;
758 Utils::getCamerasForPair(base_params_.stereopair, cam_1, cam_2);
759
760 auto cam_number = tracking_number == 1 ? cam_1 : cam_2;
761
762 LOG_INFO << "\nTRACKING STEP " << tracking_number;
763
764 std::string output_path = base_params_.outputPath + "/ncorr" + std::to_string(cam_number) + ".bin";
765
766 // Checkpoint: Check if ncorr binary already exists
767 if (std::filesystem::exists(output_path)) {
768 LOG_INFO << "Checkpoint found: " << output_path;
769 LOG_INFO << "--> STEP: Ncorr " << cam_number << " loaded from checkpoint (skipped computation)";
770 return true;
771 }
772
773 auto step_params_ = step2_params_;
774 if (tracking_number == 1) {
775 step_params_ = step1_params_;
776 } else {
777 step_params_ = step2_params_;
778 }
779 step_params_.initial_seed = {
780 static_cast<int>(initial_seed_point.pw[0]),
781 static_cast<int>(initial_seed_point.pw[1])
782 };
783
784 std::vector<cv::Mat> cur_frames = cam_frames;
785 if (tracking_number == 2 && !cur_frames.empty()) {
786 cur_frames.erase(cur_frames.begin());
787 }
788 if (cur_frames.empty()) {
789 LOG_ERROR << "No current frames available for tracking " << tracking_number;
790 return false;
791 }
792
793 // `no_update` (ncorr_params.txt, default true) keeps the NO_UPDATE preset:
794 // fixed reference within a segment. Setting it false selects
795 // KEEP_MOST_POINTS — combined with cutoff_corrcoef=0.5 this re-enables the
796 // correlation-based reference updates the original MATLAB runs used.
797 // (NOTE: the pre-existing `dic_config` key is deliberately NOT wired here —
798 // the shipped ncorr_params.txt says KEEP_MOST_POINTS but this call site has
799 // always ignored it; honoring it would silently change every deployment.)
800 runNcorrAnalysis(cam_frames[0], cur_frames, refmask,
801 initial_seed_point, step_params_,
802 output_path, config_.parallel_processing, config_.ncorr_no_update);
803
804 LOG_INFO << "--> STEP: Ncorr " << cam_number << " done and saved to " << output_path;
805 return true;
806}
807
808bool StepDWorkflow::performTracking1(const std::vector<cv::Mat>& cam_first,
809 const cv::Mat& refmask_trial,
810 const SeedPoint& initial_seed_point_set1) {
811 LOG_DEBUG << "Performing tracking camera 1..." << initial_seed_point_set1.pw[0] << "," << initial_seed_point_set1.pw[1];
812 return performTracking(1, cam_first, refmask_trial, initial_seed_point_set1);
813}
814
815bool StepDWorkflow::performTracking2(const std::vector<cv::Mat>& cam_second,
816 const cv::Mat& refmask_trial_matched,
817 const SeedPoint& initial_seed_point_set2) {
818 LOG_DEBUG << "Performing tracking camera 2..." << initial_seed_point_set2.pw[0] << "," << initial_seed_point_set2.pw[1];
819 return performTracking(2, cam_second, refmask_trial_matched, initial_seed_point_set2);
820}
821
822void StepDWorkflow::applyImageFiltering(const std::vector<cv::Mat>& cam_first_satur,
823 const std::vector<cv::Mat>& cam_second_satur,
824 const cv::Mat& refmask_trial,
825 std::vector<cv::Mat>& cam_first,
826 std::vector<cv::Mat>& cam_second) {
827 // Apply Ben's filtering
828 std::vector<int> param_filt = {25, 300};
829
830 auto [filtered_first, gs_bounds] = ImageProcessor::filterLikeBen(
831 cam_first_satur, refmask_trial, param_filt, nullptr);
832
833 // Use same boundaries for second camera
834 auto [filtered_second, _] = ImageProcessor::filterLikeBen(
835 cam_second_satur, refmask_trial, param_filt, &gs_bounds);
836
837 cam_first = filtered_first;
838 cam_second = filtered_second;
839}
840
841void StepDWorkflow::saveTrialInfo(const std::string& trial, int stereopair, int num_frames) {
842 (void)trial;
843 std::string filename = base_params_.outputPath + "/dic_info_data_target_pair" +
844 std::to_string(stereopair) + ".mat";
845
846 double actual_fps = DICConstants::TRUE_FPS / static_cast<double>(base_params_.jump);
847
848 std::vector<int> idxframe;
849 for (int i = base_params_.idxstart_set;
850 i <= base_params_.idxend_set && i < num_frames;
851 i += base_params_.jump) {
852 idxframe.push_back(i);
853 }
854
855 MatWriter::writeTrialInfoFile(filename, actual_fps, idxframe);
856}
857
858void StepDWorkflow::formatOutput(const std::string& trial,
859 int stereopair,
860 const std::vector<int>& pairOrder,
861 bool pairForced) {
862 (void)trial;
863 int cam_1, cam_2;
864 Utils::getCamerasForPair(stereopair, cam_1, cam_2);
865 formatDic2DPairResults(config_, step1_params_, base_params_.outputPath, cam_1, cam_2,
866 pairOrder, pairForced);
867}
868
870 const StepParameters& step_params,
871 const std::string& output_path,
872 int cam_1,
873 int cam_2,
874 const std::vector<int>& pairOrder,
875 bool pairForced) {
876 LOG_INFO << "Formatting output files (step2_dic_finish equivalent)...";
877
878 // ncorr outputs live directly in the output directory as .bin (ncorr native format)
879 std::string ncorr1_bin = output_path + "/ncorr" + std::to_string(cam_1) + ".bin";
880 std::string ncorr2_bin = output_path + "/ncorr" + std::to_string(cam_2) + ".bin";
881 std::string ncorr12_bin = output_path + "/ncorr" + std::to_string(cam_1) + std::to_string(cam_2) + ".bin";
882
883 if (!std::filesystem::exists(ncorr1_bin) || !std::filesystem::exists(ncorr2_bin) || !std::filesystem::exists(ncorr12_bin)) {
884 LOG_WARN << "cached ncorr result files not found";
885 if (!std::filesystem::exists(ncorr1_bin)) LOG_WARN << " Missing: " << ncorr1_bin;
886 if (!std::filesystem::exists(ncorr2_bin)) LOG_WARN << " Missing: " << ncorr2_bin;
887 if (!std::filesystem::exists(ncorr12_bin)) LOG_WARN << " Missing: " << ncorr12_bin;
888 return;
889 }
890
891 auto d_serializer = cppxdic::DataSerializer::create(config.data_format);
892 std::string output_file = output_path + "/myDIC2DpairResults_C_" +
893 std::to_string(cam_1) + "_C_" + std::to_string(cam_2) + d_serializer->extension();
894
895 if (std::filesystem::exists(output_file)) {
896 LOG_INFO << "Checkpoint found: " << output_file;
897 return;
898 }
899
900 LOG_INFO << " Loading cached DIC outputs...";
901 ncorr::DIC_analysis_output dic1 = ncorr::DIC_analysis_output::load(ncorr1_bin);
902 ncorr::DIC_analysis_output dic2 = ncorr::DIC_analysis_output::load(ncorr2_bin);
903 ncorr::DIC_analysis_output dic12 = ncorr::DIC_analysis_output::load(ncorr12_bin);
904
905 if (dic1.disps.empty() || dic2.disps.empty() || dic12.disps.empty()) {
906 LOG_ERROR << "DIC outputs are empty";
907 return;
908 }
909
910 // Apply replacebadcorr if enabled (MATLAB step2_dic_finish equivalent)
911 // This replaces badly correlated subsets with spatiotemporally filtered values
912 if (config.step_d_replacebadcorr) {
913 LOG_INFO << " Applying replacebadcorr (MATLAB step2_dic_finish)...";
917 }
918
919 const size_t n_frames_cam1 = dic1.disps.size();
920 const size_t n_frames_cam2 = dic2.disps.size();
921 const int Factor = dic1.disps[0].get_scalefactor(); // MATLAB: spacing + 1
922 const double nan = std::numeric_limits<double>::quiet_NaN();
923
924 LOG_INFO << " Processing " << n_frames_cam1 << " cam1 frames, "
925 << n_frames_cam2 << " cam2 frames, Factor=" << Factor;
926 if (n_frames_cam2 + 1 != n_frames_cam1) {
927 LOG_WARN << "cam2 tracking count differs from MATLAB expectation "
928 << "(expected " << (n_frames_cam1 - 1) << ", got " << n_frames_cam2 << ")";
929 }
930
931 DIC2DPairResults results;
932 results.nCamRef = cam_1;
933 results.nCamDef = cam_2;
934 results.nImages = static_cast<int>(n_frames_cam1);
935 results.pairOrder = pairOrder;
936 results.pairForced = pairForced;
937 results.ncorrInfo.cutoff_corrcoef = {
941 };
942 results.ncorrInfo.cutoff_diffnorm = step_params.cutoff_diffnorm;
943 results.ncorrInfo.cutoff_iteration = step_params.cutoff_iteration;
944 results.ncorrInfo.imgcorr = {"reference", "current"};
945 results.ncorrInfo.lenscoef = 0;
946 results.ncorrInfo.pixtounits = config.units_per_pixel;
947 results.ncorrInfo.radius = step_params.radius;
948 results.ncorrInfo.spacing = step_params.spacing;
950 results.ncorrInfo.stepanalysis.type = step_params.stepanalysis_params.type;
952 results.ncorrInfo.stepanalysis.step = step_params.stepanalysis_params.step;
953 results.ncorrInfo.subsettrunc = false;
954 results.ncorrInfo.total_threads = step_params.total_threads;
955 results.ncorrInfo.type = step_params.type;
956 results.ncorrInfo.units = "pixels";
957
958 const auto& roi1 = dic1.disps[0].get_roi();
959 const auto& roi_mask = roi1.get_mask();
960 results.ROImask = cv::Mat(roi_mask.height(), roi_mask.width(), CV_8U);
961 std::vector<std::pair<int, int>> roi_coords;
962 std::vector<cv::Point2d> Pref;
963
964 for (int y = 0; y < roi_mask.height(); ++y) {
965 for (int x = 0; x < roi_mask.width(); ++x) {
966 results.ROImask.at<uint8_t>(y, x) = roi_mask(y, x) ? 255 : 0;
967 if (roi_mask(y, x)) {
968 roi_coords.emplace_back(y, x);
969 Pref.emplace_back(static_cast<double>(x * Factor + 1),
970 static_cast<double>(y * Factor + 1));
971 }
972 }
973 }
974 LOG_INFO << " Reference points: " << Pref.size();
975
976 const size_t n_total_frames = n_frames_cam1 + 1 + n_frames_cam2;
977 results.Points.resize(n_total_frames);
978 results.CorCoeffVec.resize(n_total_frames);
979
980 auto fillDirectFrame = [&](const ncorr::Disp2D& disp, size_t out_idx) {
981 const auto& u_array = disp.get_u().get_array();
982 const auto& v_array = disp.get_v().get_array();
983 const auto& cc_array = disp.get_cc().get_array();
984 // Validity comes from the frame's own ROI (the set of converged points),
985 // NOT from a "value == 0" sentinel: ncorr zero-fills invalid points, but
986 // a VALID point can also hold an exact 0.0 — notably the whole first
987 // frame under cuNCorr, whose self-match yields u=v=0 exactly, which the
988 // old sentinel turned into an all-NaN Points{1} (MATLAB/ncorr store the
989 // seed grid there).
990 const auto& valid_mask = disp.get_roi().get_mask();
991
992 Points2D pts2d;
993 pts2d.x.reserve(Pref.size());
994 pts2d.y.reserve(Pref.size());
995 std::vector<double> corrcoef;
996 corrcoef.reserve(Pref.size());
997
998 for (size_t idx = 0; idx < roi_coords.size(); ++idx) {
999 const auto [y, x] = roi_coords[idx];
1000 const bool valid = valid_mask(y, x);
1001 double u = valid ? u_array(y, x) : nan;
1002 double v = valid ? v_array(y, x) : nan;
1003 double cc = valid ? cc_array(y, x) : nan;
1004
1005 pts2d.x.push_back(std::isnan(u) ? nan : (Pref[idx].x + u));
1006 pts2d.y.push_back(std::isnan(v) ? nan : (Pref[idx].y + v));
1007 corrcoef.push_back(cc);
1008 }
1009
1010 results.Points[out_idx] = std::move(pts2d);
1011 results.CorCoeffVec[out_idx] = std::move(corrcoef);
1012 };
1013
1014 LOG_INFO << " Processing cam1 frames...";
1015 for (size_t ii = 0; ii < n_frames_cam1; ++ii) {
1016 fillDirectFrame(dic1.disps[ii], ii);
1017 }
1018
1019 LOG_INFO << " Processing inter-camera matching frame...";
1020 fillDirectFrame(dic12.disps.front(), n_frames_cam1);
1021
1022 const auto& disp12_ref = dic12.disps.front();
1023 const auto& u12_full = disp12_ref.get_u().get_array();
1024 const auto& v12_full = disp12_ref.get_v().get_array();
1025 const auto& valid12 = disp12_ref.get_roi().get_mask();
1026
1027 LOG_INFO << " Processing cam2 frames (mapped through matching)...";
1028 for (size_t ii = 0; ii < n_frames_cam2; ++ii) {
1029 const auto& disp2 = dic2.disps[ii];
1030 const auto& u2_array = disp2.get_u().get_array();
1031 const auto& v2_array = disp2.get_v().get_array();
1032 const auto& cc2_array = disp2.get_cc().get_array();
1033 // Same validity principle as fillDirectFrame: use the frame's ROI, not a
1034 // "value == 0" sentinel (a valid point can hold an exact 0.0 — the whole
1035 // first frame under cuNCorr).
1036 const auto& valid2 = disp2.get_roi().get_mask();
1037 const int H2 = disp2.get_u().data_height();
1038 const int W2 = disp2.get_u().data_width();
1039
1040 auto interpolateWeighted = [&](const auto& arr, double rx, double ry) -> double {
1041 const int x0 = static_cast<int>(std::floor(rx));
1042 const int y0 = static_cast<int>(std::floor(ry));
1043 const int x1 = x0 + 1;
1044 const int y1 = y0 + 1;
1045
1046 double numerator = 0.0;
1047 double denominator = 0.0;
1048 const std::array<std::pair<int, int>, 4> samples = {{
1049 {x0, y0}, {x1, y0}, {x0, y1}, {x1, y1}
1050 }};
1051
1052 for (const auto& [sx, sy] : samples) {
1053 if (sx < 0 || sy < 0 || sx >= W2 || sy >= H2) {
1054 continue;
1055 }
1056 if (!valid2(sy, sx)) {
1057 continue;
1058 }
1059 const double value = arr(sy, sx);
1060 const double dx = rx - static_cast<double>(sx);
1061 const double dy = ry - static_cast<double>(sy);
1062 const double dist = std::sqrt(dx * dx + dy * dy);
1063 const double weight = dist < 1e-12 ? 1e12 : 1.0 / dist;
1064 numerator += value * weight;
1065 denominator += weight;
1066 }
1067
1068 if (denominator > 0.0) {
1069 return numerator / denominator;
1070 }
1071
1072 const int cx = std::clamp(static_cast<int>(std::round(rx)), 0, W2 - 1);
1073 const int cy = std::clamp(static_cast<int>(std::round(ry)), 0, H2 - 1);
1074 return valid2(cy, cx) ? arr(cy, cx) : nan;
1075 };
1076
1077 Points2D pts2d;
1078 pts2d.x.reserve(Pref.size());
1079 pts2d.y.reserve(Pref.size());
1080 std::vector<double> corrcoef;
1081 corrcoef.reserve(Pref.size());
1082
1083 for (size_t idx = 0; idx < roi_coords.size(); ++idx) {
1084 const auto [y, x] = roi_coords[idx];
1085 const double u12 = u12_full(y, x);
1086 const double v12 = v12_full(y, x);
1087 corrcoef.push_back(valid2(y, x) ? cc2_array(y, x) : nan);
1088
1089 if (!valid12(y, x)) {
1090 pts2d.x.push_back(nan);
1091 pts2d.y.push_back(nan);
1092 continue;
1093 }
1094
1095 const double cam2_rx = static_cast<double>(x) + u12 / static_cast<double>(Factor);
1096 const double cam2_ry = static_cast<double>(y) + v12 / static_cast<double>(Factor);
1097 const double u2_mapped = interpolateWeighted(u2_array, cam2_rx, cam2_ry);
1098 const double v2_mapped = interpolateWeighted(v2_array, cam2_rx, cam2_ry);
1099
1100 if (std::isnan(u2_mapped) || std::isnan(v2_mapped)) {
1101 pts2d.x.push_back(nan);
1102 pts2d.y.push_back(nan);
1103 continue;
1104 }
1105
1106 pts2d.x.push_back(Pref[idx].x + u12 + u2_mapped);
1107 pts2d.y.push_back(Pref[idx].y + v12 + v2_mapped);
1108 }
1109
1110 results.Points[n_frames_cam1 + 1 + ii] = std::move(pts2d);
1111 results.CorCoeffVec[n_frames_cam1 + 1 + ii] = std::move(corrcoef);
1112 }
1113
1114 LOG_INFO << " Creating Delaunay triangulation...";
1115 std::vector<cv::Point2f> pref_float;
1116 pref_float.reserve(Pref.size());
1117 for (const auto& p : Pref) {
1118 pref_float.emplace_back(static_cast<float>(p.x), static_cast<float>(p.y));
1119 }
1120 results.Faces = DelaunayTriangulation::compute(pref_float);
1121 double max_edge = 1.1 * std::sqrt(2.0) * Factor;
1122 results.Faces = DelaunayTriangulation::filterByEdgeLength(results.Faces, pref_float, max_edge);
1124 LOG_INFO << " Triangles: " << (results.Faces.size() / 3);
1125
1126 results.FaceColors.resize(results.Faces.size() / 3, 128.0);
1127
1128 LOG_INFO << " Writing results...";
1129 if (d_serializer->saveDIC2DPairResults(output_file, results)) {
1130 LOG_INFO << "Output formatting complete: " << output_file;
1131 } else {
1132 LOG_ERROR << "Failed to write DIC2DPairResults";
1133 }
1134}
1135
1136bool StepDWorkflow::updateMaskAndSeedFromOutput(const cv::Mat& input_mask,
1137 const SeedPoint& input_seed,
1138 const ncorr::DIC_analysis_output& dic_output,
1139 cv::Mat& output_mask,
1140 SeedPoint& output_seed) {
1141 if (dic_output.disps.empty()) {
1142 return false;
1143 }
1144
1145 const auto& disp = dic_output.disps.front();
1146 const auto& u_data = disp.get_u();
1147 const auto& v_data = disp.get_v();
1148 const auto& u_array = u_data.get_array();
1149 const auto& v_array = v_data.get_array();
1150 const double scale = static_cast<double>(disp.get_scalefactor());
1151
1152 cv::Mat U_mapped(u_data.data_height(), u_data.data_width(), CV_64F);
1153 cv::Mat V_mapped(v_data.data_height(), v_data.data_width(), CV_64F);
1154 for (size_t y = 0; y < static_cast<size_t>(u_data.data_height()); ++y) {
1155 for (size_t x = 0; x < static_cast<size_t>(u_data.data_width()); ++x) {
1156 U_mapped.at<double>(static_cast<int>(y), static_cast<int>(x)) = u_array(y, x) / scale;
1157 V_mapped.at<double>(static_cast<int>(y), static_cast<int>(x)) = v_array(y, x) / scale;
1158 }
1159 }
1160
1161 // The seed can only be transported through the field where the field is VALID.
1162 // A user seed near the ROI edge often sits on a grid point the matching never
1163 // converged at (u=v=0 there) and would be "mapped" onto itself, i.e. outside the
1164 // matched ROI of the other camera -> the next tracking call cannot seed. In that
1165 // case transport the valid grid point closest to the centroid of the valid field.
1166 const auto& valid_mask = disp.get_roi().get_mask();
1167 const int gw = static_cast<int>(u_data.data_width());
1168 const int gh = static_cast<int>(u_data.data_height());
1169 auto grid_valid = [&](int gx, int gy) {
1170 if (gx < 0 || gy < 0 || gx >= gw || gy >= gh) return false;
1171 return static_cast<bool>(valid_mask(gy, gx)) && std::isfinite(u_array(gy, gx)) &&
1172 std::isfinite(v_array(gy, gx));
1173 };
1174 SeedPoint seed_used = input_seed;
1175 if (!grid_valid(input_seed.sw[0], input_seed.sw[1])) {
1176 double sx = 0.0, sy = 0.0;
1177 long n = 0;
1178 for (int gy = 0; gy < gh; ++gy)
1179 for (int gx = 0; gx < gw; ++gx)
1180 if (grid_valid(gx, gy)) { sx += gx; sy += gy; ++n; }
1181 if (n == 0) {
1182 LOG_ERROR << "Displacement field has no valid point: cannot transport the seed";
1183 return false;
1184 }
1185 const double cx = sx / n, cy = sy / n;
1186 int bx = -1, by = -1;
1187 double best = 1e300;
1188 for (int gy = 0; gy < gh; ++gy)
1189 for (int gx = 0; gx < gw; ++gx)
1190 if (grid_valid(gx, gy)) {
1191 const double d = (gx - cx) * (gx - cx) + (gy - cy) * (gy - cy);
1192 if (d < best) { best = d; bx = gx; by = gy; }
1193 }
1194 seed_used.sw = {bx, by};
1195 seed_used.pw = ROIManager::mapSubset2Pixel(seed_used.sw, static_cast<int>(scale - 1.0));
1196 LOG_WARN << "Seed (" << input_seed.pw[0] << "," << input_seed.pw[1]
1197 << ") lies where the matching field is invalid; transporting the seed from the "
1198 "valid grid point nearest the field centroid instead: ("
1199 << seed_used.pw[0] << "," << seed_used.pw[1] << ")";
1200 }
1201
1202 output_seed.sw = ROIManager::mapPointCoordinate(seed_used.sw, U_mapped, V_mapped);
1203 output_seed.pw = ROIManager::mapSubset2Pixel(output_seed.sw, static_cast<int>(scale - 1.0));
1204
1205 output_mask = input_mask.clone();
1206 try {
1207 ncorr::ROI2D roi_current = ROIManager::matToNcorrROI(input_mask);
1208 ncorr::ROI2D roi_updated = ncorr::update(roi_current, disp, ncorr::INTERP::CUBIC_KEYS, ncorr::ROI_UPDATE_MODE::SKIP_INVALID);
1209 output_mask = ROIManager::ncorrROIToMat(roi_updated);
1210 } catch (const std::exception& e) {
1211 LOG_WARN << "failed to update ROI through displacement field: "
1212 << e.what();
1213 }
1214
1215 return true;
1216}
1217
1218void StepDWorkflow::writeNcorrMatSidecar(const std::string& output_path,
1219 const cv::Mat& ref_img,
1220 const std::vector<cv::Mat>& cur_imgs,
1221 const cv::Mat& roi_mask,
1222 const StepParameters& step_params,
1223 const ncorr::DIC_analysis_output& dic_output) const {
1224 if (cur_imgs.empty()) {
1225 return;
1226 }
1227
1228 const std::filesystem::path mat_path = std::filesystem::path(output_path).replace_extension(".mat");
1229 std::vector<cv::Mat> cur_rois(cur_imgs.size(), roi_mask.clone());
1230 std::vector<ncorr::DIC_analysis_output> dic_outputs = {dic_output};
1231
1232 std::map<std::string, double> dispinfo = {
1233 {"cutoff_corrcoef", config_.ncorr_cutoff_corrcoef},
1234 {"cutoff_diffnorm", step_params.cutoff_diffnorm},
1235 {"cutoff_iteration", static_cast<double>(step_params.cutoff_iteration)},
1236 {"lenscoef", 0.0},
1237 {"pixtounits", config_.units_per_pixel},
1238 {"radius", static_cast<double>(step_params.radius)},
1239 {"spacing", static_cast<double>(step_params.spacing)},
1240 {"subsettrunc", 0.0},
1241 {"total_threads", static_cast<double>(step_params.total_threads)}
1242 };
1243
1244 if (!MatWriter::writeMultiFrameNcorrFile(mat_path.string(),
1245 ref_img,
1246 cur_imgs,
1247 roi_mask,
1248 cur_rois,
1249 dic_outputs,
1250 dispinfo)) {
1251 LOG_WARN << "failed to write ncorr MAT sidecar: "
1252 << mat_path;
1253 }
1254}
1255
1256void StepDWorkflow::writeMatchingDebugPanel(const std::string& stage_name,
1257 const cv::Mat& ref_img,
1258 const cv::Mat& cur_img,
1259 const cv::Mat& mask_before,
1260 const cv::Mat& mask_after,
1261 const SeedPoint& seed_before,
1262 const SeedPoint& seed_after,
1263 const ncorr::DIC_analysis_output& dic_output) const {
1264 if (!config_.debug_mode || ref_img.empty() || cur_img.empty()) {
1265 return;
1266 }
1267
1268 cv::Mat ref_overlay = makeDebugCanvas(ref_img);
1269 cv::Mat cur_overlay = makeDebugCanvas(cur_img);
1270 cv::Mat before_mask_view = makeDebugCanvas(mask_before);
1271 cv::Mat after_mask_view = makeDebugCanvas(mask_after);
1272
1273 overlayMask(ref_overlay, mask_before, cv::Scalar(60, 200, 255));
1274 overlayMask(cur_overlay, mask_after, cv::Scalar(60, 255, 120));
1275 overlayMask(before_mask_view, mask_before, cv::Scalar(0, 0, 255));
1276 overlayMask(after_mask_view, mask_after, cv::Scalar(0, 255, 0));
1277
1278 drawSeed(ref_overlay, seed_before, cv::Scalar(0, 0, 255));
1279 drawSeed(cur_overlay, seed_after, cv::Scalar(0, 255, 255));
1280 drawSeedDisplacement(ref_overlay, seed_before, seed_after, cv::Scalar(255, 255, 0));
1281
1282 const cv::Size panel_size(std::max(ref_overlay.cols, cur_overlay.cols),
1283 std::max(ref_overlay.rows, cur_overlay.rows));
1284 cv::resize(ref_overlay, ref_overlay, panel_size, 0, 0, cv::INTER_NEAREST);
1285 cv::resize(cur_overlay, cur_overlay, panel_size, 0, 0, cv::INTER_NEAREST);
1286 cv::resize(before_mask_view, before_mask_view, panel_size, 0, 0, cv::INTER_NEAREST);
1287 cv::resize(after_mask_view, after_mask_view, panel_size, 0, 0, cv::INTER_NEAREST);
1288
1289 cv::Mat top_row;
1290 cv::Mat bottom_row;
1291 cv::hconcat(ref_overlay, cur_overlay, top_row);
1292 cv::hconcat(before_mask_view, after_mask_view, bottom_row);
1293
1294 cv::Mat panel;
1295 cv::vconcat(top_row, bottom_row, panel);
1296
1297 const cv::Point text_origin(18, 30);
1298 const cv::Scalar text_color(255, 255, 255);
1299 cv::putText(panel, "Stage: " + stage_name, text_origin,
1300 cv::FONT_HERSHEY_SIMPLEX, 0.8, text_color, 2, cv::LINE_AA);
1301
1302 const double dx = static_cast<double>(seed_after.pw[0] - seed_before.pw[0]);
1303 const double dy = static_cast<double>(seed_after.pw[1] - seed_before.pw[1]);
1304 std::ostringstream disp_text;
1305 disp_text << "Seed disp: (" << dx << ", " << dy << ")";
1306 cv::putText(panel, disp_text.str(), cv::Point(18, 62),
1307 cv::FONT_HERSHEY_SIMPLEX, 0.7, text_color, 2, cv::LINE_AA);
1308
1309 if (!dic_output.disps.empty()) {
1310 const auto& cc_array = dic_output.disps.front().get_cc().get_array();
1311 const int seed_x = std::clamp(seed_before.sw[0], 0, static_cast<int>(cc_array.width()) - 1);
1312 const int seed_y = std::clamp(seed_before.sw[1], 0, static_cast<int>(cc_array.height()) - 1);
1313 std::ostringstream cc_text;
1314 cc_text << "Seed corrcoef: " << cc_array(seed_y, seed_x);
1315 cv::putText(panel, cc_text.str(), cv::Point(18, 94),
1316 cv::FONT_HERSHEY_SIMPLEX, 0.7, text_color, 2, cv::LINE_AA);
1317 }
1318
1319 const std::filesystem::path debug_dir = std::filesystem::path(base_params_.outputPath) / "debug_stepd";
1320 std::filesystem::create_directories(debug_dir);
1321 const std::filesystem::path debug_file = debug_dir / (sanitizeStageName(stage_name) + ".png");
1322 cv::imwrite(debug_file.string(), panel);
1323}
1324
1325ncorr::DIC_analysis_output StepDWorkflow::runNcorrAnalysis(
1326 const cv::Mat& ref_img,
1327 const std::vector<cv::Mat>& cur_imgs,
1328 const cv::Mat& roi_mask,
1329 const SeedPoint& seed_point,
1330 const StepParameters& step_params,
1331 const std::string& output_path,
1332 const bool go_parallel,
1333 const bool use_no_update) {
1334
1335 LOG_INFO << "Running ncorr DIC analysis...";
1336 LOG_DEBUG << " Radius: " << step_params.radius << ", Spacing: " << step_params.spacing;
1337 LOG_DEBUG << " Scalefactor: " << (step_params.spacing + 1) << " (spacing + 1)";
1338 LOG_DEBUG << " Seed: (" << seed_point.pw[0] << ", " << seed_point.pw[1] << ")";
1339
1340 // Convert images to ncorr Image2D format
1341 // ncorr::Image2D expects file paths, so we need to save cv::Mat as temporary files
1342 std::vector<ncorr::Image2D> ncorr_images;
1343 std::vector<std::string> temp_image_paths;
1344
1345 // Create temporary directory for images
1346 std::string temp_dir = base_params_.outputPath + "/tmp_ncorr_images";
1347 std::filesystem::create_directories(temp_dir);
1348
1349 // Save reference image
1350 std::string ref_path = temp_dir + "/ref.png";
1351 cv::imwrite(ref_path, ref_img);
1352 ncorr_images.emplace_back(ref_path);
1353 temp_image_paths.push_back(ref_path);
1354
1355 // Save current images
1356 for (unsigned int i = 0; i < cur_imgs.size(); ++i) {
1357 std::ostringstream oss;
1358 oss << temp_dir << "/cur_" << std::setw(4) << std::setfill('0') << i << ".png";
1359 std::string cur_path = oss.str();
1360 cv::imwrite(cur_path, cur_imgs[i]);
1361 ncorr_images.emplace_back(cur_path);
1362 temp_image_paths.push_back(cur_path);
1363 }
1364
1365 // Convert ROI mask
1366 ncorr::ROI2D roi = ROIManager::matToNcorrROI(roi_mask);
1367
1368 cv::imwrite(temp_dir + "/roi_mask.png", get_cv_img(roi.get_mask(), 0, 255));
1369
1370 // Setup DIC input
1371 // Note: scalefactor = spacing + 1 (this is how ncorr downsamples the displacement field)
1372 int scalefactor = step_params.spacing + 1;
1373
1374 ncorr::DIC_analysis_input dic_input(
1375 ncorr_images,
1376 roi,
1377 scalefactor,
1378 ncorr::INTERP::QUINTIC_BSPLINE_PRECOMPUTE,
1379 ncorr::SUBREGION::CIRCLE,
1380 step_params.radius,
1381 step_params.total_threads,
1382 use_no_update ? ncorr::DIC_analysis_config::NO_UPDATE : ncorr::DIC_analysis_config::KEEP_MOST_POINTS,
1383 config_.debug_mode
1384 );
1385
1386 // CRITICAL FIX: Override update_corrcoef to disable correlation-based reference updates.
1387 // MATLAB ncorr uses fixed-step reference changes (step_ref_change=10), not correlation-based.
1388 // The KEEP_MOST_POINTS preset sets update_corrcoef=0.5, which causes frequent ref updates
1389 // when correlation drops, leading to ROI fragmentation and divergence around frame 60-70.
1390 // Setting this to ncorr_cutoff_corrcoef (default 10.0) effectively disables correlation-based updates.
1391 dic_input.update_corrcoef = config_.ncorr_cutoff_corrcoef;
1392
1393 // Seed-quality gate (ncorr_params cutoff_max_corrcoef / matching_cutoff_max_corrcoef).
1394 // A seed whose optimised corrcoef exceeds the gate is rejected ("could not seed any
1395 // current image"). Inter-view MATCHING calls (<= 2 current images) legitimately
1396 // correlate worse than frame-to-frame tracking because the two cameras see the
1397 // surface under different perspectives, so they get their own, looser gate.
1398 const bool is_matching_call = cur_imgs.size() <= 2;
1399 const double seed_cutoff_corrcoef = is_matching_call
1401 : config_.ncorr_cutoff_max_corrcoef;
1402 LOG_DEBUG << " Seed gate: cutoff_max_corrcoef=" << seed_cutoff_corrcoef
1403 << (is_matching_call ? " (matching)" : " (tracking)")
1404 << ", cutoff_max_diffnorm=" << config_.ncorr_cutoff_max_diffnorm;
1405
1406 // Run DIC analysis (returns Lagrangian perspective in pixels)
1407 ncorr::DIC_analysis_output dic_output_raw;
1408
1409#ifdef CPPXDIC_HAVE_CUNCORR
1410 // cuNCorr's sequence engine is built for frame-SERIES tracking: warm-started
1411 // local optimization plus a small coarse seed search (cuncorr_seed_search,
1412 // default 15 px). Inter-camera MATCHING calls (<= 2 current images) have
1413 // disparities of hundreds of pixels that only ncorr's GLOBAL seed
1414 // optimization can bridge — routing them through cuNCorr silently produced
1415 // an all-zero matching field with an empty valid ROI, which cascaded into
1416 // all-NaN cam-def Points, all-NaN 3D vertices, and a 0-face stitched mesh.
1417 // Matching is a handful of frames, so keeping it on ncorr costs nothing.
1418 if (config_.dic_engine != "ncorr" && cur_imgs.size() > 2) {
1419 // cuNCorr engine: CUDA if a device is present, else its CPU backend (identical
1420 // results). Produces the same raw Lagrangian/pixel DIC_analysis_output on the
1421 // reduced grid, so the downstream .bin + StepE reconstruction is unchanged.
1423 ccfg.scalefactor = scalefactor;
1424 ccfg.subregion_radius = step_params.radius;
1425 ccfg.num_threads = step_params.total_threads;
1426 ccfg.seed_x = seed_point.pw[0];
1427 ccfg.seed_y = seed_point.pw[1];
1428 ccfg.seed_search = config_.cuncorr_seed_search;
1429 ccfg.debug = config_.debug_mode;
1431 LOG_INFO << " Using cuNCorr DIC engine...";
1432 dic_output_raw = cppxdic::run_cuncorr_dic(ncorr_images, roi, ccfg, &cinfo);
1433 LOG_INFO << " cuNCorr backend: " << cinfo.backend << (cinfo.cuda ? " (CUDA)" : " (CPU)");
1434 } else
1435#endif
1436 if (go_parallel) {
1437 LOG_INFO << " Using parallel DIC processing"
1438 << (config_.ncorr_use_exact_matlab ? " (exact_matlab_*)" : " (matlab_*)")
1439 << "...";
1440
1441 // Create seed parameters from the seed point
1442 std::vector<ncorr::SeedParams> seeds;
1443 seeds.push_back(ncorr::SeedParams(seed_point.pw[0], seed_point.pw[1]));
1444
1445 // Create parallel input structure
1446 ncorr::DIC_analysis_parallel_input dic_parallel_input(dic_input, seeds);
1447 dic_parallel_input.fixed_step_ref = config_.ncorr_fixed_step_ref;
1448 dic_parallel_input.seeds_are_optimized = config_.ncorr_seeds_are_optimized;
1449 dic_parallel_input.cutoff_max_diffnorm = config_.ncorr_cutoff_max_diffnorm;
1450 dic_parallel_input.cutoff_max_corrcoef = seed_cutoff_corrcoef;
1451 if (config_.ncorr_fixed_step_ref > 0) {
1452 LOG_INFO << " Fixed-step reference updates: every "
1453 << config_.ncorr_fixed_step_ref << " frames";
1454 }
1455
1456 // Run parallel DIC analysis
1457 dic_output_raw = config_.ncorr_use_exact_matlab
1458 ? ncorr::exact_matlab_DIC_analysis_parallel(dic_parallel_input)
1459 : ncorr::matlab_DIC_analysis_parallel(dic_parallel_input);
1460 } else {
1461 LOG_INFO << " Using Matlab-style sequential DIC processing"
1462 << (config_.ncorr_use_exact_matlab ? " (exact_matlab_*)" : " (matlab_*)")
1463 << "...";
1464 ncorr::DIC_analysis_parallel_input dic_sequential_input(
1465 dic_input, {ncorr::SeedParams(seed_point.pw[0], seed_point.pw[1])},
1467 dic_sequential_input.fixed_step_ref = config_.ncorr_fixed_step_ref;
1468 dic_sequential_input.cutoff_max_diffnorm = config_.ncorr_cutoff_max_diffnorm;
1469 dic_sequential_input.cutoff_max_corrcoef = seed_cutoff_corrcoef;
1470 dic_output_raw = config_.ncorr_use_exact_matlab
1471 ? ncorr::exact_matlab_DIC_analysis_sequential(dic_sequential_input)
1472 : ncorr::matlab_DIC_analysis_sequential(dic_sequential_input);
1473 }
1474
1475 // Post-process with both perspectives
1476 LOG_INFO << "Post-processing displacements...";
1477
1478 // Step 1: Convert to Eulerian perspective with sign inversion (still in pixels).
1479 // Interp choice: bicubic (CUBIC_KEYS) is the historical workaround from when
1480 // the biquintic B-spline coefficients used FFT circular deconvolution and
1481 // carried a border bias (bicubic needs no prefilter, so it was immune).
1482 // That bias was fixed in CppNCorr (Unser recursive filter, mirror BCs), so
1483 // biquintic — what MATLAB ncorr uses here — is safe again; the previously
1484 // dangling `perspective_interp` ncorr-params tag now selects it.
1485 ncorr::DIC_analysis_output dic_eulerian_pixels = ncorr::change_perspective_with_inversion(
1486 dic_output_raw,
1487 config_.ncorr_perspective_interp ? ncorr::INTERP::QUINTIC_BSPLINE_PRECOMPUTE
1488 : ncorr::INTERP::CUBIC_KEYS);
1489
1490 // Step 3: Apply units to BOTH perspectives
1491 ncorr::DIC_analysis_output dic_lagrangian = ncorr::set_units(dic_output_raw, "mm", config_.units_per_pixel);
1492 ncorr::DIC_analysis_output dic_eulerian = ncorr::set_units(dic_eulerian_pixels, "mm", config_.units_per_pixel);
1493
1494 LOG_INFO << " Created both Lagrangian and Eulerian perspectives";
1495
1496 // Save debug videos if debug mode is enabled
1497 if (config_.debug_mode) {
1498 std::string video_dir = std::filesystem::path(output_path).parent_path().string() + "/debug_video/";
1499 std::filesystem::create_directories(video_dir);
1500 std::string base_name = std::filesystem::path(output_path).stem().string();
1501 double alpha = config_.video_alpha;
1502 double fps = static_cast<double>(config_.video_fps);
1503
1504 LOG_INFO << " Saving debug DIC videos to " << video_dir;
1505 try {
1506 ncorr::save_DIC_video(video_dir + base_name + "_v_eulerian.avi",
1507 dic_input, dic_eulerian, ncorr::DISP::V, alpha, fps);
1508 ncorr::save_DIC_video(video_dir + base_name + "_u_eulerian.avi",
1509 dic_input, dic_eulerian, ncorr::DISP::U, alpha, fps);
1510 LOG_INFO << " ✓ Debug DIC videos saved";
1511 } catch (const std::exception& e) {
1512 LOG_WARN << "Failed to save debug videos: " << e.what();
1513 }
1514 }
1515
1516 // Save ncorr output directly as binary (ncorr's native format)
1517 // output_path already has .bin extension
1518 // IMPORTANT: Save raw pixel displacements, NOT mm-scaled.
1519 // dic3DReconstruction and formatOutput need pixel-coordinate displacements
1520 // because they combine (x*scalefactor + displacement) for DLT reconstruction.
1521 save(dic_output_raw, output_path);
1522 writeNcorrMatSidecar(output_path, ref_img, cur_imgs, roi_mask, step_params, dic_output_raw);
1523 LOG_INFO << "DIC analysis saved: " << output_path;
1524
1525 return dic_output_raw;
1526}
1527
1528} // namespace cppxdic
StepConfig step_e
Definition config.h:139
std::string dic_engine
Definition config.h:211
int ref_trial_id
Definition config.h:75
std::string dic_path
Definition config.h:48
bool im_filter_mode
Definition config.h:51
bool ncorr_perspective_interp
Definition config.h:185
bool ncorr_seeds_are_optimized
Definition config.h:195
std::string subject_id
Definition config.h:65
std::string material
Definition config.h:258
int idx_frame_start
Definition config.h:78
bool parallel_processing
Definition config.h:62
int num_pair
Definition config.h:34
double ncorr_cutoff_max_corrcoef
Definition config.h:197
bool debug_mode
Definition config.h:84
std::string phase_id
Definition config.h:66
double video_alpha
Definition config.h:232
int limit_grayscale
Definition config.h:135
int frame_jump
Definition config.h:80
int video_fps
Definition config.h:229
bool ncorr_no_update
Definition config.h:175
bool im_saturation_mode
Definition config.h:56
StepConfig step_d
Definition config.h:138
bool ncorr_use_exact_matlab
Definition config.h:203
int idx_frame_end
Definition config.h:79
std::string data_path
Definition config.h:47
double ncorr_cutoff_max_diffnorm
Definition config.h:196
bool step_d_replacebadcorr
Definition config.h:159
std::string data_format
Definition config.h:168
int ncorr_fixed_step_ref
Definition config.h:179
double units_per_pixel
Definition config.h:130
int cuncorr_seed_search
Definition config.h:212
double ncorr_matching_cutoff_max_corrcoef
Definition config.h:201
double ncorr_cutoff_corrcoef
Definition config.h:181
static std::string buildPath(const Config &config, bool with_data_or_dic_path=false, bool with_rawdata=false, bool with_speckles=false, bool with_material=false, bool with_video=false, bool with_protocol=false, bool with_calib=false)
Build the path based on activated parameters and return the proper string.
Definition utils.cpp:1204
static std::vector< std::string > findFiles(const std::string &directory, const std::string &pattern)
Definition utils.cpp:193
static std::string buildProtocolDir(const Config &config, bool with_data_or_dic_path=false, bool with_rawdata=false, bool with_speckles=false, bool with_material=false)
Build protocol path.
Definition utils.cpp:1241
static std::string buildRoiFilePath(const cppxdic::BaseParameters &parameters, std::string reftrial, int stereopair, const std::string &extension=".mat")
Build the roi file path.
Definition utils.cpp:1323
static bool importVid(const Config &config, int trial, int stereopair, std::vector< std::string > &cam1Frames, std::vector< std::string > &cam2Frames, int maxFrames=0)
Definition utils.cpp:468
static std::string buildSeedFilePath(const cppxdic::BaseParameters &parameters, std::string reftrial, int stereopair, const std::string &extension=".mat")
Build the seed file path.
Definition utils.cpp:1328
static std::string buildMatchingFilePath(const cppxdic::BaseParameters &parameters, std::string reftrial, int stereopair, const std::string &extension=".bin")
Build the matching file path.
Definition utils.cpp:1334
static void getCamerasForPair(int stereopair, int &cam_first, int &cam_second)
Get camera numbers for a given stereopair using the registered table (setCameraPairs).
Definition utils.cpp:254
static std::unique_ptr< DataSerializer > create(const std::string &format)
Create a serializer for the given format string.
static std::vector< int > flipOrientation(const std::vector< int > &faces)
Flip triangle orientation Matches MATLAB: F = F(:, [1 3 2])
static std::vector< int > filterByEdgeLength(const std::vector< int > &faces, const std::vector< cv::Point2f > &vertices, double max_edge_threshold)
Filter irregular triangles by edge length Matches MATLAB: F(EdgeLengthsMax > threshold,...
static std::vector< int > compute(const std::vector< cv::Point2f > &points)
Compute Delaunay triangulation of 2D points Matches MATLAB: DT = delaunayTriangulation(P)
static std::vector< cv::Mat > saturate(const std::vector< cv::Mat > &input, int level)
Saturate image intensities Equivalent to satur.m in Matlab.
static std::pair< std::vector< cv::Mat >, std::pair< double, double > > filterLikeBen(const std::vector< cv::Mat > &input, const cv::Mat &mask, const std::vector< int > &param_filt, const std::pair< double, double > *gs_boundaries=nullptr)
Filter images like Ben's method Equivalent to filter_like_ben.m in Matlab.
static bool loadProtocol(const std::string &protocol_path, ProtocolFileData &protocol)
static bool writeMultiFrameNcorrFile(const std::string &filename, const cv::Mat &ref_img, const std::vector< cv::Mat > &cur_imgs, const cv::Mat &ref_roi, const std::vector< cv::Mat > &cur_rois, const std::vector< ncorr::DIC_analysis_output > &dic_outputs, const std::map< std::string, double > &dispinfo, const std::string &type_str="load", const std::string &ref_name="reference")
Write multi-frame ncorr files (ncorr1.mat, ncorr2.mat, etc.) For tracking DIC with multiple current f...
static bool writeTrialInfoFile(const std::string &filename, double fps, const std::vector< int > &frame_indices)
Write dic_info_data_target_pairX.mat file Structure:
static std::vector< int > mapSubset2Pixel(const std::vector< int > &subset_coords, int subset_spacing)
Map subset coordinates to pixel coordinates Equivalent to map_subset2pixel.m.
static std::vector< int > findROICenter(const cv::Mat &roi_mask)
Find center of ROI mask.
static std::vector< int > mapPointCoordinate(const std::vector< int > &point_sw, const cv::Mat &U_mapped, const cv::Mat &V_mapped)
Map point coordinates using displacement fields Equivalent to map_pointcoordinate....
static std::vector< int > mapPixel2Subset(const std::vector< int > &pixel_coords, int subset_spacing)
Map pixel coordinates to subset coordinates Equivalent to map_pixel2subset.m.
static ncorr::ROI2D matToNcorrROI(const cv::Mat &mask)
Convert OpenCV Mat to ncorr ROI2D.
static SeedPoint loadOrCreateSeed(const BaseParameters &params, const cv::Mat &roi_mask)
Load or create seed point If seed file doesn't exist, creates a default seed point at ROI center.
static cv::Mat loadOrCreateROI(const BaseParameters &params, const cv::Mat &reference_image)
Load or create ROI mask If ROI file doesn't exist, creates a full-image ROI Equivalent to checking an...
static cv::Mat ncorrROIToMat(const ncorr::ROI2D &roi)
Convert ncorr ROI2D to OpenCV Mat.
std::tuple< std::string, std::vector< int >, bool > execute(const std::string &trial, int stereopair, const StagePlan &plan=StagePlan{})
Execute Step D: 2D DIC Analysis Main entry point for 2D DIC processing.
static void formatDic2DPairResults(const Config &config, const StepParameters &step_params, const std::string &output_path, int cam_1, int cam_2, const std::vector< int > &pairOrder, bool pairForced)
Format cached ncorr outputs into myDIC2DpairResults (step2_dic_finish equivalent).
StepDWorkflow(const Config &config)
Constructor.
static bool updateMaskAndSeedFromOutput(const cv::Mat &input_mask, const SeedPoint &input_seed, const ncorr::DIC_analysis_output &dic_output, cv::Mat &output_mask, SeedPoint &output_seed)
Map an ROI mask and seed point through the first displacement field.
Adapter: run the cuNCorr DIC engine and package results as ncorr::DIC_analysis_output.
Lightweight, dependency-free logging facility for CPPxDIC.
#define LOG_DEBUG
Definition logging.h:169
#define LOG_ERROR
Definition logging.h:172
#define LOG_WARN
Definition logging.h:171
#define LOG_INFO
Definition logging.h:170
DataSerializer - Abstract base class for format-agnostic DIC data I/O.
ncorr::DIC_analysis_output run_cuncorr_dic(const std::vector< ncorr::Image2D > &imgs, const ncorr::ROI2D &roi, const CuncorrDicConfig &cfg, CuncorrDicInfo *info=nullptr)
Run cuNCorr in sequence mode (fixed reference = imgs[0], warm-start each frame) over the stack agains...
int matlab_replacebadcorr(ncorr::DIC_analysis_output &data, double level_corr_coef=1.0, double sigma_spatial=1.0, double sigma_temporal=2.0, double shrink_factor=0.9)
Replaces badly correlated displacement data with spatiotemporally filtered values.
StagePlan — selects which pipeline sub-steps a single process executes.
Definition stage_plan.h:21
bool match
ROI/seed + REF->trial matching + camera-to-camera matching.
Definition stage_plan.h:23
bool format
combine ncorr outputs -> myDIC2DpairResults
Definition stage_plan.h:25
int only_cam
0 = both cameras; otherwise restrict tracking to this camera id
Definition stage_plan.h:32
bool track
per-camera NCorr tracking
Definition stage_plan.h:24
double cutoff_diffnorm
Definition config.h:20
bool auto_update
Definition config.h:25
int cutoff_iteration
Definition config.h:21
int spacing
Definition config.h:19
std::string analysis_type
Definition config.h:17
std::string seed_type
Definition config.h:24
bool high_strain_enabled
Definition config.h:23
int total_threads
Definition config.h:22
int radius
Definition config.h:18
int step_ref_change
Definition config.h:26
std::string baseResultPath
Definition parameters.h:23
std::string baseDataPath
Definition parameters.h:22
std::string matchingfile
Definition parameters.h:36
Tunables mirrored onto cuncorr::SessionConfig.
Definition cuncorr_dic.h:26
int seed_search
coarse ZNCC seed search radius (px)
Definition cuncorr_dic.h:32
int seed_y
manual seed row (full-image px); -1 = auto
Definition cuncorr_dic.h:31
int seed_x
manual seed column (full-image px); -1 = auto
Definition cuncorr_dic.h:30
Which backend actually ran.
Definition cuncorr_dic.h:37
bool cuda
true if a CUDA device executed the run
Definition cuncorr_dic.h:39
std::string backend
e.g. "cpu" or "cuda:0 <name>"
Definition cuncorr_dic.h:38
DIC 2D Pair Results Equivalent to MATLAB's DIC2DpairResults.
std::vector< int > pairOrder
std::vector< Points2D > Points
std::vector< std::vector< double > > CorCoeffVec
std::vector< double > FaceColors
std::vector< int > Faces
static constexpr int LIMIT_GRAYSCALE_DEFAULT
Definition parameters.h:148
static constexpr int TRUE_FPS
Definition parameters.h:147
std::string units
std::vector< std::string > imgcorr
std::vector< double > cutoff_corrcoef
StepAnalysisInfo stepanalysis
2D Points per frame
std::vector< double > x
std::vector< double > y
std::vector< TrialEntry > trials
Definition mat_reader.h:43
std::vector< double > spddxlcond
Definition parameters.h:125
std::vector< int > spdcond
Definition parameters.h:123
std::vector< int > repcond
Definition parameters.h:124
std::vector< int > nfcond
Definition parameters.h:122
void getPairOrder(const std::string &dir, std::vector< int > &pairOrder, bool &pairForced) const
Definition parameters.h:128
std::vector< std::string > dircond
Definition parameters.h:121
Seed point structure Stores seed points in both pixel world (pw) and subset world (sw) coordinates.
Definition parameters.h:108
std::vector< int > sw
Definition parameters.h:110
std::vector< int > pw
Definition parameters.h:109
Step parameters structure for DIC analysis Contains DIC-specific parameters like radius,...
Definition parameters.h:79
std::vector< int > initial_seed
Definition parameters.h:92
StepAnalysisParams stepanalysis_params
Definition parameters.h:89