35#include <opencv2/opencv.hpp>
53namespace fs = std::filesystem;
56constexpr int kMngFps = 50;
57constexpr int kLimitGrayscale = 120;
58const std::vector<int> kParamFiltIm = {25, 300};
62const std::vector<int> kDefaultCamOrder = {2, 1, 4, 3};
64bool ensureDir(
const std::string& path) {
66 if (!fs::exists(path)) {
67 return fs::create_directories(path);
75std::string padNumber(
int num,
int width) {
76 std::ostringstream oss;
77 oss << std::setw(width) << std::setfill(
'0') << num;
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 "";
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) +
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$");
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();
106 if (loose_match.empty() &&
107 (std::regex_match(name, pat_loose) || std::regex_match(name, pat_loose_end))) {
109 if (name.find(trialname) != std::string::npos) {
110 loose_match = entry.path().string();
119cv::Mat toGrayPlane(
const cv::Mat& frame) {
120 if (frame.channels() == 3) {
121 std::vector<cv::Mat> ch;
122 cv::split(frame, ch);
133 switch (stereopair) {
163 throw std::runtime_error(
"mirrored: invalid stereopair " + std::to_string(stereopair));
167 auto info_from_view = [&cam_order](
int view_nbr) ->
ViewInfo {
171 const int cam_slot = (view_nbr - 1) / 2;
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");
176 vi.
cam_nbr = cam_order[cam_slot];
184 vp.
view1 = info_from_view(v1);
185 vp.
view2 = info_from_view(v2);
190 const cv::Mat gray = toGrayPlane(frame);
191 const int w = gray.cols;
192 const int half_w = w / 2;
195 return gray(cv::Rect(0, 0, half_w, gray.rows)).clone();
197 return gray(cv::Rect(half_w, 0, w - half_w, gray.rows)).clone();
201 int frameEnd,
int frameJump, std::vector<std::string>& out_frames) {
203 const std::string trialname = padNumber(trial, 3);
206 const std::string vid =
209 LOG_ERROR <<
"mirrored: video not found for trial=" << trial <<
" cam=" << view.
cam_nbr
210 <<
" in " << video_dir;
214 cv::VideoCapture cap(vid);
215 if (!cap.isOpened()) {
216 LOG_ERROR <<
"mirrored: failed to open video: " << vid;
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;
233 std::ostringstream odir;
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;
241 const std::string done_marker = view_dir +
"/.extracted";
242 std::ostringstream window_sig;
243 window_sig << vid <<
" " << frameStart <<
" " << frameEnd <<
" " << frameJump;
245 std::ifstream marker(done_marker);
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());
254 if (!out_frames.empty()) {
256 <<
" cached frames in " << view_dir;
261 fs::remove(done_marker);
268 if (frameStart > 1) {
269 cap.set(cv::CAP_PROP_POS_FRAMES, frameStart - 1);
272 int cur = frameStart;
274 for (
int f = frameStart; f <= frameEnd && !eof; f += frameJump) {
276 if (!cap.grab()) { eof =
true;
break; }
282 if (!cap.read(raw))
break;
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());
292 if (!out_frames.empty() && !eof) {
293 std::ofstream marker(done_marker);
294 marker << window_sig.str() <<
"\n";
296 return !out_frames.empty();
297 }
catch (
const std::exception& e) {
298 LOG_ERROR <<
"mirrored: importRawViewMirrored error: " << e.what();
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,
312 const std::string& tmp_dir,
const std::string& output_path) {
315 if (fs::exists(output_path)) {
316 LOG_INFO <<
" Checkpoint found: " << output_path <<
" (loading)";
317 return ncorr::DIC_analysis_output::load(output_path);
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());
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,
339 : ncorr::DIC_analysis_config::KEEP_MOST_POINTS,
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])},
356 LOG_INFO <<
" ncorr " << (is_matching ?
"matching" :
"tracking") <<
": "
357 << cur_imgs.size() <<
" frame(s), radius " << step_params.
radius <<
", threads "
359 << seed_point.
pw[1] <<
"), seed gate corrcoef<=" << in.cutoff_max_corrcoef
362 ncorr::DIC_analysis_output dic_out;
365 : ncorr::matlab_DIC_analysis_parallel(in);
368 : ncorr::matlab_DIC_analysis_sequential(in);
373 save(dic_out, output_path);
374 LOG_INFO <<
" DIC saved: " << output_path;
379bool processPair(
const Config& config,
int trial,
const ViewPair& vp,
int frameStart,
int frameEnd,
381 LOG_INFO <<
"--- Trial " << trial <<
" stereopair " << vp.stereopair <<
" (views "
382 << vp.view1.view_nbr <<
" & " << vp.view2.view_nbr <<
") ---";
385 std::vector<std::string> view1_paths, view2_paths;
388 LOG_ERROR <<
"mirrored: failed to extract view " << vp.view1.view_nbr;
393 LOG_ERROR <<
"mirrored: failed to extract view " << vp.view2.view_nbr;
397 const std::size_t n = std::min(view1_paths.size(), view2_paths.size());
399 LOG_ERROR <<
"mirrored: no frames extracted for pair " << vp.stereopair;
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;
421 view1_satur.push_back(a);
422 view2_satur.push_back(b);
426 ?
"--> STEP : saturation done (limit_grayscale=" + std::to_string(limit_grayscale) +
")"
427 : std::string(
"--> STEP : saturation skipped (im_saturation_mode=false)"));
430 std::ostringstream odir;
432 << padNumber(trial, 3) <<
"/" << config.
phase_id <<
"/mirrored_pair" << vp.stereopair;
433 const std::string out_dir = odir.str();
440 bp.
trial = padNumber(trial, 3);
454 step_track.
type =
"regular";
476 std::vector<cv::Mat> view1, view2;
478 auto [f1, gs_bounds] =
482 view1 = std::move(f1);
483 view2 = std::move(f2);
484 LOG_INFO <<
"--> STEP : filtering done";
488 LOG_INFO <<
"--> STEP : raw (saturated) data used";
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);
498 out_dir +
"/dic_info_data_target_pair" + std::to_string(vp.stereopair) +
".mat",
499 static_cast<double>(kMngFps) / frameJump, idxframe);
505 cv::Mat roi_view2_matched = roi_view1;
508 std::vector<cv::Mat> match_cur = {view2.front(), view1.front()};
509 const std::string match_out =
514 ncorr::DIC_analysis_output dic12;
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;
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);
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;
541 LOG_INFO <<
"--> STEP : Ncorr matching " << vp.view1.view_nbr <<
"-" << vp.view2.view_nbr
542 <<
" done (seed2=" << seed2.
pw[0] <<
"," << seed2.
pw[1] <<
")";
547 const std::string track1_out =
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";
556 std::vector<cv::Mat> cur(view2.begin() + (view2.size() > 1 ? 1 : 0), view2.end());
557 const std::string track2_out =
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";
568 vp.view2.view_nbr, {1, 2},
false);
570 LOG_INFO <<
"--- pair " << vp.stereopair <<
" done; results in " << out_dir <<
" ---";
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 <<
"-------------------------------------------";
583 const std::vector<int>& cam_order =
586 LOG_ERROR <<
"mirrored: mirrored_num_pair must be > 0 (got " << num_pair <<
")";
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;
604 std::vector<int> trials = trials_in;
605 if (trials.empty()) trials.push_back(config.
ref_trial_id);
607 std::ostringstream tl;
608 for (std::size_t i = 0; i < trials.size(); ++i) tl << (i ?
"," :
"") << trials[i];
613 for (
int trial : trials) {
614 for (
int pair = 1; pair <= num_pair; ++pair) {
618 }
catch (
const std::exception& e) {
619 LOG_ERROR <<
"mirrored: " << e.what() <<
" (skipping pair " << pair <<
")";
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();
632 LOG_ERROR <<
"mirrored: trial " << trial <<
" pair " << pair <<
" failed";
639 LOG_INFO <<
"Mirrored-camera analysis completed.";
641 LOG_ERROR <<
"Mirrored-camera analysis completed with errors.";
std::vector< int > mirrored_cam_order
bool ncorr_seeds_are_optimized
double ncorr_cutoff_max_corrcoef
bool ncorr_use_exact_matlab
double ncorr_cutoff_max_diffnorm
double ncorr_matching_cutoff_max_corrcoef
double ncorr_cutoff_corrcoef
static std::string buildNcorrFilePath(const std::string pathdir, int cam_1, int cam_2=-1, const std::string &extension=".bin")
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.
static std::string buildRoiFilePath(const cppxdic::BaseParameters ¶meters, std::string reftrial, int stereopair, const std::string &extension=".mat")
Build the roi file path.
static std::string buildSeedFilePath(const cppxdic::BaseParameters ¶meters, std::string reftrial, int stereopair, const std::string &extension=".mat")
Build the seed file path.
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 > ¶m_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 ¶ms, 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 ¶ms, 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.
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.
Base parameters structure Contains fundamental paths and settings for DIC analysis Equivalent to base...
std::string baseResultPath
Seed point structure Stores seed points in both pixel world (pw) and subset world (sw) coordinates.
Step parameters structure for DIC analysis Contains DIC-specific parameters like radius,...
StepAnalysisParams stepanalysis_params
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