CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
mirrored_mode.cpp
Go to the documentation of this file.
1
24
25#include "config.h"
26#include "parameters.h"
27#include "utils.h"
28#include "image_processor.h"
29#include "roi_manager.h"
30#include "step_d_workflow.h"
31#include "mat_writer.h"
32#include "logging.h"
33
34#include <ncorr.h>
35#include <opencv2/opencv.hpp>
36
37#include <algorithm>
38#include <cmath>
39#include <filesystem>
40#include <fstream>
41#include <iomanip>
42#include <iostream>
43#include <regex>
44#include <sstream>
45#include <string>
46#include <vector>
47
48namespace xdic {
49namespace mirrored {
50
51namespace {
52
53namespace fs = std::filesystem;
54
55// MATLAB stepD_2DDIC_MNG.m constants.
56constexpr int kMngFps = 50; // theGlobalSettings_MNG.actual_FS_vid
57constexpr int kLimitGrayscale = 120; // LIMIT_GRAYSCALE
58const std::vector<int> kParamFiltIm = {25, 300}; // param_filt_im default
59
60// Default physical-camera ordering from theGlobalSettings_MNG.cam_order = [2 1 4 3],
61// used only if the config supplies an empty mirrored_cam_order.
62const std::vector<int> kDefaultCamOrder = {2, 1, 4, 3};
63
64bool ensureDir(const std::string& path) {
65 try {
66 if (!fs::exists(path)) {
67 return fs::create_directories(path);
68 }
69 return true;
70 } catch (...) {
71 return false;
72 }
73}
74
75std::string padNumber(int num, int width) {
76 std::ostringstream oss;
77 oss << std::setw(width) << std::setfill('0') << num;
78 return oss.str();
79}
80
81// Locate the single physical-camera video for a trial.
82//
83// MATLAB import_raw_vid_MNG.m matches: <expID>_<unitID>_<trial>_*_cam_<id>.mp4 .
84// The C++ camerapairs convention (find_video_file in utils.cpp) is
85// <subject>_<material>_speckles_<trial>_.*_cam_<id>.*.mp4 . We accept either, plus a
86// permissive fallback that just requires "_cam_<id>" before the .mp4 extension, so the
87// mode works across both the MNG raw naming and the speckle naming.
88std::string findCameraVideo(const std::string& video_dir, const std::string& subject,
89 const std::string& material, const std::string& trialname, int cam_id) {
90 if (!fs::exists(video_dir)) return "";
91
92 const std::regex pat_speckle(subject + "_" + material + "_speckles_" + trialname + "_.*_cam_" +
93 std::to_string(cam_id) + ".*\\.mp4$");
94 const std::regex pat_mng(subject + "_.*_" + trialname + "_.*_cam_" + std::to_string(cam_id) +
95 ".*\\.mp4$");
96 const std::regex pat_loose(".*_cam_" + std::to_string(cam_id) + "(\\.|_).*\\.mp4$");
97 const std::regex pat_loose_end(".*_cam_" + std::to_string(cam_id) + "\\.mp4$");
98
99 std::string loose_match;
100 for (const auto& entry : fs::directory_iterator(video_dir)) {
101 if (!entry.is_regular_file()) continue;
102 const std::string name = entry.path().filename().string();
103 if (std::regex_match(name, pat_speckle) || std::regex_match(name, pat_mng)) {
104 return entry.path().string();
105 }
106 if (loose_match.empty() &&
107 (std::regex_match(name, pat_loose) || std::regex_match(name, pat_loose_end))) {
108 // Only accept the loose fallback if the trial token appears in the name.
109 if (name.find(trialname) != std::string::npos) {
110 loose_match = entry.path().string();
111 }
112 }
113 }
114 return loose_match;
115}
116
117// Convert a decoded frame to the single-channel "grayscale" plane MATLAB uses
118// (iloc(:,:,1) -> red channel). Mirrors importRawVid in utils.cpp.
119cv::Mat toGrayPlane(const cv::Mat& frame) {
120 if (frame.channels() == 3) {
121 std::vector<cv::Mat> ch;
122 cv::split(frame, ch);
123 return ch[2]; // R channel (BGR -> index 2)
124 }
125 return frame;
126}
127
128} // namespace
129
130ViewPair resolveViewPair(int stereopair, const std::vector<int>& cam_order) {
131 // Port of get_cam_view_stereopair_param.m: stereopair -> (view_nbr_1, view_nbr_2).
132 int v1 = 0, v2 = 0;
133 switch (stereopair) {
134 case 1:
135 v1 = 1;
136 v2 = 2;
137 break;
138 case 2:
139 v1 = 2;
140 v2 = 3;
141 break;
142 case 3:
143 v1 = 3;
144 v2 = 4;
145 break;
146 case 4:
147 v1 = 4;
148 v2 = 5;
149 break;
150 case 5:
151 v1 = 6;
152 v2 = 5;
153 break;
154 case 6:
155 v1 = 7;
156 v2 = 6;
157 break;
158 case 7:
159 v1 = 8;
160 v2 = 7;
161 break;
162 default:
163 throw std::runtime_error("mirrored: invalid stereopair " + std::to_string(stereopair));
164 }
165
166 // Port of camera_info_from_view.m: view_nbr -> (cam_nbr via cam_order, half).
167 auto info_from_view = [&cam_order](int view_nbr) -> ViewInfo {
168 ViewInfo vi;
169 vi.view_nbr = view_nbr;
170 // views 1,2 -> cam_order[0]; 3,4 -> cam_order[1]; 5,6 -> cam_order[2]; 7,8 -> cam_order[3]
171 const int cam_slot = (view_nbr - 1) / 2; // 0..3
172 if (cam_slot < 0 || cam_slot >= static_cast<int>(cam_order.size())) {
173 throw std::runtime_error("mirrored: view " + std::to_string(view_nbr) +
174 " has no camera in cam_order");
175 }
176 vi.cam_nbr = cam_order[cam_slot];
177 // odd view_nbr -> left half (mask 1), even view_nbr -> right half (mask 2)
178 vi.half = (view_nbr % 2 == 1) ? MaskHalf::Left : MaskHalf::Right;
179 return vi;
180 };
181
182 ViewPair vp;
183 vp.stereopair = stereopair;
184 vp.view1 = info_from_view(v1);
185 vp.view2 = info_from_view(v2);
186 return vp;
187}
188
189cv::Mat extractViewFromFrame(const cv::Mat& frame, MaskHalf half) {
190 const cv::Mat gray = toGrayPlane(frame);
191 const int w = gray.cols;
192 const int half_w = w / 2;
193 // MATLAB im_mask_width{1} = 1:W/2 ; {2} = W/2+1:W
194 if (half == MaskHalf::Left) {
195 return gray(cv::Rect(0, 0, half_w, gray.rows)).clone();
196 }
197 return gray(cv::Rect(half_w, 0, w - half_w, gray.rows)).clone();
198}
199
200bool importRawViewMirrored(const Config& config, int trial, const ViewInfo& view, int frameStart,
201 int frameEnd, int frameJump, std::vector<std::string>& out_frames) {
202 try {
203 const std::string trialname = padNumber(trial, 3);
204 const std::string video_dir = Utils::buildVideoDir(config, true, true, true, true);
205
206 const std::string vid =
207 findCameraVideo(video_dir, config.subject_id, config.material, trialname, view.cam_nbr);
208 if (vid.empty()) {
209 LOG_ERROR << "mirrored: video not found for trial=" << trial << " cam=" << view.cam_nbr
210 << " in " << video_dir;
211 return false;
212 }
213
214 cv::VideoCapture cap(vid);
215 if (!cap.isOpened()) {
216 LOG_ERROR << "mirrored: failed to open video: " << vid;
217 return false;
218 }
219
220 LOG_INFO << " view " << view.view_nbr << " (cam " << view.cam_nbr << ", half "
221 << (view.half == MaskHalf::Left ? "L" : "R") << ") <- " << vid;
222
223 const int total = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_COUNT));
224 if (frameEnd <= 0) frameEnd = total;
225 frameStart = std::max(1, frameStart);
226 frameEnd = std::min(frameEnd, total);
227 if (frameJump <= 0) frameJump = 1;
228
229 // Per-view frame output dir (kept separate from the camerapairs tmp_frames layout).
230 // Views are shared between adjacent stereopairs (view 2 serves pairs 1 and 2), so
231 // the extracted PNG sequence is cached per (trial, view) and reused when a
232 // previous extraction with the same frame window completed.
233 std::ostringstream odir;
234 odir << config.dic_path << "/" << config.subject_id << "/" << config.material
235 << "/tmp_frames_mirrored/T" << trial << "/view" << view.view_nbr;
236 const std::string view_dir = odir.str();
237 if (!ensureDir(view_dir)) {
238 LOG_ERROR << "mirrored: cannot create frame dir " << view_dir;
239 return false;
240 }
241 const std::string done_marker = view_dir + "/.extracted";
242 std::ostringstream window_sig;
243 window_sig << vid << " " << frameStart << " " << frameEnd << " " << frameJump;
244 {
245 std::ifstream marker(done_marker);
246 std::string prev;
247 if (marker && std::getline(marker, prev) && prev == window_sig.str()) {
248 for (int f = frameStart; f <= frameEnd; f += frameJump) {
249 std::ostringstream fp;
250 fp << view_dir << "/frame_" << std::setw(6) << std::setfill('0') << f << ".png";
251 if (!fs::exists(fp.str())) { out_frames.clear(); break; }
252 out_frames.push_back(fp.str());
253 }
254 if (!out_frames.empty()) {
255 LOG_INFO << " view " << view.view_nbr << ": reusing " << out_frames.size()
256 << " cached frames in " << view_dir;
257 return true;
258 }
259 }
260 }
261 fs::remove(done_marker);
262
263 // Read sequentially instead of seeking before every frame. On the FFmpeg
264 // backend (Linux) cap.set(CAP_PROP_POS_FRAMES, n) re-seeks to the previous
265 // keyframe and re-decodes forward on every call, making import ~O(N * GOP).
266 // Seek once to the start, then read forward; grab (decode without Mat
267 // conversion) the frames skipped when frameJump > 1.
268 if (frameStart > 1) {
269 cap.set(cv::CAP_PROP_POS_FRAMES, frameStart - 1);
270 }
271
272 int cur = frameStart; // 1-based index of the frame the next read()/grab() yields
273 bool eof = false;
274 for (int f = frameStart; f <= frameEnd && !eof; f += frameJump) {
275 while (cur < f) {
276 if (!cap.grab()) { eof = true; break; }
277 ++cur;
278 }
279 if (eof) break;
280
281 cv::Mat raw;
282 if (!cap.read(raw)) break;
283 ++cur;
284 const cv::Mat region = extractViewFromFrame(raw, view.half);
285
286 std::ostringstream fp;
287 fp << view_dir << "/frame_" << std::setw(6) << std::setfill('0') << f << ".png";
288 cv::imwrite(fp.str(), region);
289 out_frames.push_back(fp.str());
290 }
291
292 if (!out_frames.empty() && !eof) {
293 std::ofstream marker(done_marker);
294 marker << window_sig.str() << "\n";
295 }
296 return !out_frames.empty();
297 } catch (const std::exception& e) {
298 LOG_ERROR << "mirrored: importRawViewMirrored error: " << e.what();
299 return false;
300 }
301}
302
303namespace {
304
305// Reuse the CppNCorr engine exactly like StepDWorkflow::runNcorrAnalysis: build Image2D
306// from PNG paths, build ROI2D from the mask, run the MATLAB-style sequential DIC, then
307// persist the raw (pixel) output. Returns the raw DIC output.
308ncorr::DIC_analysis_output runViewDic(const Config& config, const cv::Mat& ref_img,
309 const std::vector<cv::Mat>& cur_imgs, const cv::Mat& roi_mask,
310 const cppxdic::SeedPoint& seed_point,
311 const cppxdic::StepParameters& step_params,
312 const std::string& tmp_dir, const std::string& output_path) {
313 // Checkpoint: a completed DIC for this view/pair is reloaded instead of recomputed
314 // (same convention as StepDWorkflow's ncorr{cam}.bin caches).
315 if (fs::exists(output_path)) {
316 LOG_INFO << " Checkpoint found: " << output_path << " (loading)";
317 return ncorr::DIC_analysis_output::load(output_path);
318 }
319 ensureDir(tmp_dir);
320
321 std::vector<ncorr::Image2D> imgs;
322 const std::string ref_path = tmp_dir + "/ref.png";
323 cv::imwrite(ref_path, ref_img);
324 imgs.emplace_back(ref_path);
325 for (std::size_t i = 0; i < cur_imgs.size(); ++i) {
326 std::ostringstream oss;
327 oss << tmp_dir << "/cur_" << std::setw(4) << std::setfill('0') << i << ".png";
328 cv::imwrite(oss.str(), cur_imgs[i]);
329 imgs.emplace_back(oss.str());
330 }
331
332 ncorr::ROI2D roi = cppxdic::ROIManager::matToNcorrROI(roi_mask);
333
334 const int scalefactor = step_params.spacing + 1;
335 ncorr::DIC_analysis_input dic_input(
336 imgs, roi, scalefactor, ncorr::INTERP::QUINTIC_BSPLINE_PRECOMPUTE, ncorr::SUBREGION::CIRCLE,
337 step_params.radius, step_params.total_threads,
338 config.ncorr_no_update ? ncorr::DIC_analysis_config::NO_UPDATE
339 : ncorr::DIC_analysis_config::KEEP_MOST_POINTS,
340 config.debug_mode);
341 dic_input.update_corrcoef = config.ncorr_cutoff_corrcoef;
342
343 // Same engine configuration as StepDWorkflow::runNcorrAnalysis: seeds, fixed-step
344 // reference updates and the seed-quality gates come from ncorr_params.txt. The
345 // view1->view2 MATCHING call (<= 2 current images) uses the looser matching gate:
346 // two mirror views see the fingertip under different perspectives and the seed
347 // converges around corrcoef 0.5, which the tracking gate rejects.
348 const bool is_matching = cur_imgs.size() <= 2;
349 ncorr::DIC_analysis_parallel_input in(
350 dic_input, {ncorr::SeedParams(seed_point.pw[0], seed_point.pw[1])},
352 in.fixed_step_ref = config.ncorr_fixed_step_ref;
353 in.cutoff_max_diffnorm = config.ncorr_cutoff_max_diffnorm;
354 in.cutoff_max_corrcoef = is_matching ? config.ncorr_matching_cutoff_max_corrcoef
356 LOG_INFO << " ncorr " << (is_matching ? "matching" : "tracking") << ": "
357 << cur_imgs.size() << " frame(s), radius " << step_params.radius << ", threads "
358 << step_params.total_threads << ", seed (" << seed_point.pw[0] << ","
359 << seed_point.pw[1] << "), seed gate corrcoef<=" << in.cutoff_max_corrcoef
360 << (config.parallel_processing && !is_matching ? " [parallel]" : " [sequential]");
361
362 ncorr::DIC_analysis_output dic_out;
363 if (config.parallel_processing && !is_matching) {
364 dic_out = config.ncorr_use_exact_matlab ? ncorr::exact_matlab_DIC_analysis_parallel(in)
365 : ncorr::matlab_DIC_analysis_parallel(in);
366 } else {
367 dic_out = config.ncorr_use_exact_matlab ? ncorr::exact_matlab_DIC_analysis_sequential(in)
368 : ncorr::matlab_DIC_analysis_sequential(in);
369 }
370
371 // Persist the raw (pixel) displacements, matching StepDWorkflow's convention.
372 // `save` is a friend free function found via ADL on the ncorr argument type.
373 save(dic_out, output_path);
374 LOG_INFO << " DIC saved: " << output_path;
375 return dic_out;
376}
377
378// Process one stereopair end-to-end for one trial.
379bool processPair(const Config& config, int trial, const ViewPair& vp, int frameStart, int frameEnd,
380 int frameJump) {
381 LOG_INFO << "--- Trial " << trial << " stereopair " << vp.stereopair << " (views "
382 << vp.view1.view_nbr << " & " << vp.view2.view_nbr << ") ---";
383
384 // 1. Mask-extract both views as full frames (import_raw_vid_MNG.m).
385 std::vector<std::string> view1_paths, view2_paths;
386 if (!importRawViewMirrored(config, trial, vp.view1, frameStart, frameEnd, frameJump,
387 view1_paths)) {
388 LOG_ERROR << "mirrored: failed to extract view " << vp.view1.view_nbr;
389 return false;
390 }
391 if (!importRawViewMirrored(config, trial, vp.view2, frameStart, frameEnd, frameJump,
392 view2_paths)) {
393 LOG_ERROR << "mirrored: failed to extract view " << vp.view2.view_nbr;
394 return false;
395 }
396
397 const std::size_t n = std::min(view1_paths.size(), view2_paths.size());
398 if (n == 0) {
399 LOG_ERROR << "mirrored: no frames extracted for pair " << vp.stereopair;
400 return false;
401 }
402
403 // 2. Load + saturate (satur.m). LIMIT_GRAYSCALE is the MNG script constant (120)
404 // unless dic_params.txt sets limit_grayscale; im_saturation_mode=false skips the
405 // clipping entirely (pre-filtered videos).
406 const int limit_grayscale = (config.limit_grayscale > 0) ? config.limit_grayscale : kLimitGrayscale;
407 std::vector<cv::Mat> view1_satur, view2_satur;
408 view1_satur.reserve(n);
409 view2_satur.reserve(n);
410 for (std::size_t i = 0; i < n; ++i) {
411 cv::Mat a = cv::imread(view1_paths[i], cv::IMREAD_GRAYSCALE);
412 cv::Mat b = cv::imread(view2_paths[i], cv::IMREAD_GRAYSCALE);
413 if (a.empty() || b.empty()) {
414 LOG_ERROR << "mirrored: failed to read extracted frame " << i;
415 return false;
416 }
417 if (config.im_saturation_mode) {
418 view1_satur.push_back(cppxdic::ImageProcessor::saturate(a, limit_grayscale, "high"));
419 view2_satur.push_back(cppxdic::ImageProcessor::saturate(b, limit_grayscale, "high"));
420 } else {
421 view1_satur.push_back(a);
422 view2_satur.push_back(b);
423 }
424 }
426 ? "--> STEP : saturation done (limit_grayscale=" + std::to_string(limit_grayscale) + ")"
427 : std::string("--> STEP : saturation skipped (im_saturation_mode=false)"));
428
429 // 3. Output layout (per trial / per pair), mirroring buildPath conventions.
430 std::ostringstream odir;
431 odir << config.dic_path << "/" << config.subject_id << "/" << config.material << "/"
432 << padNumber(trial, 3) << "/" << config.phase_id << "/mirrored_pair" << vp.stereopair;
433 const std::string out_dir = odir.str();
434 ensureDir(out_dir);
435
436 // 4. ROI: reuse ROIManager (loads REF mask if present, else full-frame ROI over view1).
438 bp.subject = config.subject_id;
439 bp.material = config.material;
440 bp.trial = padNumber(trial, 3);
441 bp.stereopair = vp.stereopair;
442 bp.phase = config.phase_id;
443 bp.reftrial = padNumber(config.ref_trial_id, 3);
444 bp.outputPath = out_dir;
445 bp.baseResultPath = config.dic_path;
446 bp.limit_grayscale = limit_grayscale;
447 bp.roifile = Utils::buildRoiFilePath(bp, bp.reftrial, vp.stereopair);
448 bp.seedfile = Utils::buildSeedFilePath(bp, bp.reftrial, vp.stereopair);
449
450 cv::Mat roi_view1 = cppxdic::ROIManager::loadOrCreateROI(bp, view1_satur.front());
451
452 // 5. DIC parameters (MNG values from stepD_2DDIC_MNG.m, overridable via config.step_d).
453 cppxdic::StepParameters step_track;
454 step_track.type = "regular";
455 step_track.radius = (config.step_d.radius > 0) ? config.step_d.radius : 25;
456 step_track.spacing = (config.step_d.spacing > 0) ? config.step_d.spacing : 20;
457 step_track.cutoff_diffnorm = config.step_d.cutoff_diffnorm;
458 step_track.cutoff_iteration = config.step_d.cutoff_iteration;
459 step_track.total_threads = config.step_d.total_threads;
461 step_track.stepanalysis_params.type = config.step_d.seed_type;
464
465 cppxdic::StepParameters step_match = step_track;
466 step_match.radius =
467 (config.step_e.radius > 0) ? config.step_e.radius : 30; // matching radius (MNG=30)
468
469 // 6. Seed: reuse ROIManager (loads REF seed if present, else ROI centre), then map
470 // to subset world (map_pixel2subset.m) so the matching field can transport it.
472 seed1.sw = cppxdic::ROIManager::mapPixel2Subset(seed1.pw, step_track.spacing);
473
474 // 7. Image filtering (filter_like_ben.m): gs bounds from view1 over the ROI, reused
475 // for view2 -- identical to StepDWorkflow::applyImageFiltering.
476 std::vector<cv::Mat> view1, view2;
477 if (config.im_filter_mode) {
478 auto [f1, gs_bounds] =
479 cppxdic::ImageProcessor::filterLikeBen(view1_satur, roi_view1, kParamFiltIm, nullptr);
480 auto [f2, _] =
481 cppxdic::ImageProcessor::filterLikeBen(view2_satur, roi_view1, kParamFiltIm, &gs_bounds);
482 view1 = std::move(f1);
483 view2 = std::move(f2);
484 LOG_INFO << "--> STEP : filtering done";
485 } else {
486 view1 = view1_satur;
487 view2 = view2_satur;
488 LOG_INFO << "--> STEP : raw (saturated) data used";
489 }
490
491 // 8. Trial info sidecar (dic_info_data_target_pair<N>.mat), as saveTrialInfo does.
492 {
493 std::vector<int> idxframe;
494 for (int f = frameStart, k = 0; k < static_cast<int>(n); f += frameJump, ++k) {
495 idxframe.push_back(f);
496 }
498 out_dir + "/dic_info_data_target_pair" + std::to_string(vp.stereopair) + ".mat",
499 static_cast<double>(kMngFps) / frameJump, idxframe);
500 }
501
502 // 9. MATCHING view1 -> view2 at the reference frame (input2 = [view2_1, view1_1]).
503 // Mirrors the ncorr_dic_rewrited matching call in stepD_2DDIC_MNG.m. The matching
504 // is done on the filtered frames, exactly like the MATLAB script (im_view_*).
505 cv::Mat roi_view2_matched = roi_view1;
506 cppxdic::SeedPoint seed2 = seed1;
507 {
508 std::vector<cv::Mat> match_cur = {view2.front(), view1.front()};
509 const std::string match_out =
510 Utils::buildNcorrFilePath(out_dir, vp.view1.view_nbr, vp.view2.view_nbr, ".bin");
511 // Same fallback as StepDWorkflow::matchingInitialFrame: a seed near the ROI
512 // boundary may not converge in the other view; retry once from the ROI centre.
513 cppxdic::SeedPoint match_seed = seed1;
514 ncorr::DIC_analysis_output dic12;
515 try {
516 dic12 = runViewDic(config, view1.front(), match_cur, roi_view1, match_seed, step_match,
517 out_dir + "/tmp_ncorr_match", match_out);
518 } catch (const std::runtime_error& e) {
519 const std::string what = e.what();
520 if (what.find("could not seed") == std::string::npos) throw;
521 const std::vector<int> c = cppxdic::ROIManager::findROICenter(roi_view1); // [x, y]
522 match_seed = cppxdic::SeedPoint(c[0], c[1]);
523 match_seed.sw = cppxdic::ROIManager::mapPixel2Subset(match_seed.pw, step_track.spacing);
524 LOG_WARN << "mirrored: matching seed (" << seed1.pw[0] << "," << seed1.pw[1]
525 << ") did not converge in view " << vp.view2.view_nbr
526 << "; retrying from the ROI centre of mass (" << match_seed.pw[0] << ","
527 << match_seed.pw[1] << ")";
528 dic12 = runViewDic(config, view1.front(), match_cur, roi_view1, match_seed, step_match,
529 out_dir + "/tmp_ncorr_match", match_out);
530 }
531
532 // Port of: refmask_trial_matched = h12.current(1).roi.mask;
533 // initial_seed_point_set2 = map_pointcoordinate(seed1.sw, {U,V}/(spacing+1))
534 if (!cppxdic::StepDWorkflow::updateMaskAndSeedFromOutput(roi_view1, match_seed, dic12,
535 roi_view2_matched, seed2)) {
536 LOG_WARN << "mirrored: matching produced no displacement field; view2 will reuse "
537 "the view1 ROI/seed";
538 roi_view2_matched = roi_view1;
539 seed2 = seed1;
540 }
541 LOG_INFO << "--> STEP : Ncorr matching " << vp.view1.view_nbr << "-" << vp.view2.view_nbr
542 << " done (seed2=" << seed2.pw[0] << "," << seed2.pw[1] << ")";
543 }
544
545 // 10. TRACKING view1 across all frames (ncorr1).
546 {
547 const std::string track1_out =
548 Utils::buildNcorrFilePath(out_dir, vp.view1.view_nbr, -1, ".bin");
549 runViewDic(config, view1.front(), view1, roi_view1, seed1, step_track,
550 out_dir + "/tmp_ncorr_view1", track1_out);
551 LOG_INFO << "--> STEP : Ncorr " << vp.view1.view_nbr << " done";
552 }
553
554 // 11. TRACKING view2 across frames 2..end (ncorr2), seeded/masked by the matching.
555 {
556 std::vector<cv::Mat> cur(view2.begin() + (view2.size() > 1 ? 1 : 0), view2.end());
557 const std::string track2_out =
558 Utils::buildNcorrFilePath(out_dir, vp.view2.view_nbr, -1, ".bin");
559 runViewDic(config, view2.front(), cur, roi_view2_matched, seed2, step_track,
560 out_dir + "/tmp_ncorr_view2", track2_out);
561 LOG_INFO << "--> STEP : Ncorr " << vp.view2.view_nbr << " done";
562 }
563
564 // 12. Format output (step2_dic_finish.m): ncorr{v1}.bin + ncorr{v2}.bin +
565 // ncorr{v1}{v2}.bin -> myDIC2DpairResults_C_{v1}_C_{v2}.<ext>. No protocol-driven
566 // pair order in the MNG script, so use the neutral default.
567 cppxdic::StepDWorkflow::formatDic2DPairResults(config, step_track, out_dir, vp.view1.view_nbr,
568 vp.view2.view_nbr, {1, 2}, false);
569
570 LOG_INFO << "--- pair " << vp.stereopair << " done; results in " << out_dir << " ---";
571 return true;
572}
573
574} // namespace
575
576bool run(const Config& config, const std::vector<int>& trials_in) {
577 LOG_INFO << "-------------------------------------------";
578 LOG_INFO << "xDIC mirrored-camera (MNG) mode";
579 LOG_INFO << "-------------------------------------------";
580
581 // Rig geometry from config (theGlobalSettings_MNG: Npair, cam_order).
582 const int num_pair = config.mirrored_num_pair;
583 const std::vector<int>& cam_order =
584 config.mirrored_cam_order.empty() ? kDefaultCamOrder : config.mirrored_cam_order;
585 if (num_pair <= 0) {
586 LOG_ERROR << "mirrored: mirrored_num_pair must be > 0 (got " << num_pair << ")";
587 return false;
588 }
589
590 // Frame range from config (import_vid_MNG honours frame_idx_set / protocol; here we
591 // use the explicit config range, consistent with the camerapairs C++ path).
592 const int frameStart = config.idx_frame_start;
593 const int frameEnd = config.idx_frame_end;
594 const int frameJump = (config.frame_jump > 0) ? config.frame_jump : 1;
595 const double true_fps = static_cast<double>(kMngFps) / frameJump;
596 std::ostringstream order_str;
597 for (std::size_t i = 0; i < cam_order.size(); ++i) order_str << (i ? "," : "") << cam_order[i];
598 LOG_INFO << "Stereopairs: " << num_pair << ", cam_order: [" << order_str.str() << "]";
599 LOG_INFO << "True FPS: " << true_fps << ", frames " << frameStart << ".." << frameEnd
600 << " jump " << frameJump;
601
602 // Trials: the CLI selection (--trial/--trials/...) when given, else the reference
603 // trial only (single-trial entry, as in the MATLAB script).
604 std::vector<int> trials = trials_in;
605 if (trials.empty()) trials.push_back(config.ref_trial_id);
606 {
607 std::ostringstream tl;
608 for (std::size_t i = 0; i < trials.size(); ++i) tl << (i ? "," : "") << trials[i];
609 LOG_INFO << "Trials: [" << tl.str() << "], reference trial " << config.ref_trial_id;
610 }
611
612 bool all_ok = true;
613 for (int trial : trials) {
614 for (int pair = 1; pair <= num_pair; ++pair) {
615 ViewPair vp;
616 try {
617 vp = resolveViewPair(pair, cam_order);
618 } catch (const std::exception& e) {
619 LOG_ERROR << "mirrored: " << e.what() << " (skipping pair " << pair << ")";
620 all_ok = false;
621 continue;
622 }
623 // One pair's failure (including engine exceptions such as "could not seed any
624 // current image") must not abort the remaining pairs/trials.
625 bool ok = false;
626 try {
627 ok = processPair(config, trial, vp, frameStart, frameEnd, frameJump);
628 } catch (const std::exception& e) {
629 LOG_ERROR << "mirrored: trial " << trial << " pair " << pair << " threw: " << e.what();
630 }
631 if (!ok) {
632 LOG_ERROR << "mirrored: trial " << trial << " pair " << pair << " failed";
633 all_ok = false;
634 }
635 }
636 }
637
638 if (all_ok) {
639 LOG_INFO << "Mirrored-camera analysis completed.";
640 } else {
641 LOG_ERROR << "Mirrored-camera analysis completed with errors.";
642 }
643 return all_ok;
644}
645
646} // namespace mirrored
647} // namespace xdic
StepConfig step_e
Definition config.h:139
int mirrored_num_pair
Definition config.h:40
int ref_trial_id
Definition config.h:75
std::string dic_path
Definition config.h:48
bool im_filter_mode
Definition config.h:51
std::vector< int > mirrored_cam_order
Definition config.h:41
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
double ncorr_cutoff_max_corrcoef
Definition config.h:197
std::string phase_id
Definition config.h:66
int limit_grayscale
Definition config.h:135
int frame_jump
Definition config.h:80
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
double ncorr_cutoff_max_diffnorm
Definition config.h:196
int ncorr_fixed_step_ref
Definition config.h:179
double ncorr_matching_cutoff_max_corrcoef
Definition config.h:201
double ncorr_cutoff_corrcoef
Definition config.h:181
static std::string buildNcorrFilePath(const std::string pathdir, int cam_1, int cam_2=-1, const std::string &extension=".bin")
Definition utils.cpp:1363
static std::string buildVideoDir(const Config &config, bool with_data_or_dic_path=false, bool with_rawdata=false, bool with_speckles=false, bool with_material=false)
Build the video path.
Definition utils.cpp:1249
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 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::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 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 > findROICenter(const cv::Mat &roi_mask)
Find center of ROI mask.
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 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).
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.
Lightweight, dependency-free logging facility for CPPxDIC.
#define LOG_ERROR
Definition logging.h:172
#define LOG_WARN
Definition logging.h:171
#define LOG_INFO
Definition logging.h:170
Interface for the mirrored-camera xDIC mode ("MNG" variant).
cv::Mat extractViewFromFrame(const cv::Mat &frame, MaskHalf half)
Extract a single mirror view (full frame) from a decoded camera frame.
ViewPair resolveViewPair(int stereopair, const std::vector< int > &cam_order)
Resolve the two views that form a given mirrored stereopair.
bool importRawViewMirrored(const Config &config, int trial, const ViewInfo &view, int frameStart, int frameEnd, int frameJump, std::vector< std::string > &out_frames)
Decode a physical-camera video and write per-view masked frames to disk.
MaskHalf
Half-width mask region used to extract a single mirror view from a frame.
bool run(const Config &config, const std::vector< int > &trials={})
Run the mirrored-camera 2D DIC pipeline.
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 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
Base parameters structure Contains fundamental paths and settings for DIC analysis Equivalent to base...
Definition parameters.h:20
std::string baseResultPath
Definition parameters.h:23
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
StepAnalysisParams stepanalysis_params
Definition parameters.h:89
Logical view = (physical camera id, which half of that camera's frame).
int view_nbr
1..8 logical view index (MATLAB view_nbr)
int cam_nbr
physical camera id (from cam_order), used to find the video file
A mirrored stereopair: the two logical views to correlate against each other.
int stereopair
1-based stereopair index
ViewInfo view2
second view of the pair
ViewInfo view1
first view of the pair