24#ifdef CPPXDIC_HAVE_CUNCORR
29#include <ncorr/frame_reader.h>
32#include <opencv2/opencv.hpp>
42namespace fs = std::filesystem;
54bool SingleDicWorkflow::importFrames(std::vector<cv::Mat>& frames)
const {
61 std::vector<std::string> video_candidates;
62 if (fs::exists(trial_dir) && fs::is_regular_file(trial_dir)) {
63 video_candidates.push_back(trial_dir);
65 for (
const char* ext : {
".mp4",
".avi",
".mov",
".mkv"}) {
66 std::string p = trial_dir + ext;
67 if (fs::exists(p)) video_candidates.push_back(p);
70 if (fs::exists(trial_dir) && fs::is_directory(trial_dir)) {
71 for (
const auto& e : fs::directory_iterator(trial_dir)) {
72 if (!e.is_regular_file())
continue;
73 std::string ext = e.path().extension().string();
74 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
75 if (ext ==
".mp4" || ext ==
".avi" || ext ==
".mov" || ext ==
".mkv")
76 video_candidates.push_back(e.path().string());
83 auto window_done = [&](
int count_kept_input_index,
int frame_1based) {
84 (void)count_kept_input_index;
88 if (!video_candidates.empty()) {
89 const std::string& vpath = video_candidates.front();
90 cv::VideoCapture cap(vpath);
91 if (!cap.isOpened()) {
92 LOG_ERROR <<
"[singledic] Cannot open video: " << vpath;
97 while (cap.read(bgr)) {
99 if (frame_idx < start)
continue;
100 if (window_done(0, frame_idx))
break;
101 if (((frame_idx - start) % jump) != 0)
continue;
103 if (bgr.channels() == 3)
104 cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY);
107 if (gray.type() != CV_8UC1) gray.convertTo(gray, CV_8UC1);
108 frames.push_back(gray);
111 }
else if (fs::exists(trial_dir) && fs::is_directory(trial_dir)) {
113 std::vector<std::string> paths;
115 paths = ncorr::discover_frames(trial_dir,
"",
"");
116 }
catch (
const std::exception& e) {
120 for (
size_t i = 0; i < paths.size(); ++i) {
121 int frame_1based =
static_cast<int>(i) + 1;
122 if (frame_1based < start)
continue;
123 if (window_done(0, frame_1based))
break;
124 if (((frame_1based - start) % jump) != 0)
continue;
125 cv::Mat gray = cv::imread(paths[i], cv::IMREAD_GRAYSCALE);
127 LOG_ERROR <<
"[singledic] Failed to read frame: " << paths[i];
130 frames.push_back(gray);
133 LOG_ERROR <<
"[singledic] No video or image folder at: " << trial_dir;
137 if (frames.empty()) {
138 LOG_ERROR <<
"[singledic] No frames imported from " << trial_dir;
144 std::reverse(frames.begin(), frames.end());
147 LOG_INFO <<
"[singledic] Imported " << frames.size() <<
" frame(s) from " << trial_dir;
154std::vector<cv::Mat> SingleDicWorkflow::saturate(
const std::vector<cv::Mat>& frames)
const {
156 std::vector<cv::Mat> out;
157 out.reserve(frames.size());
158 for (
const auto& f : frames) {
171std::vector<cv::Mat> SingleDicWorkflow::maybeFilter(
const std::vector<cv::Mat>& frames,
172 const cv::Mat& roi_mask)
const {
181 cv::Mat ref_bp = ImageProcessor::applyBandpassFilter(frames.front(), param);
182 auto bounds = ImageProcessor::computePercentileBoundaries(ref_bp, roi_mask, 1.0, 99.0);
184 std::vector<cv::Mat> out;
185 out.reserve(frames.size());
186 for (
const auto& f : frames) {
187 cv::Mat bp = ImageProcessor::applyBandpassFilter(f, param);
188 out.push_back(ImageProcessor::normalizeAndClamp(bp, bounds));
197cv::Mat SingleDicWorkflow::loadOrCreateRoi(
const cv::Mat& reference)
const {
205std::vector<int> SingleDicWorkflow::loadOrCreateSeed(
const cv::Mat& roi_mask)
const {
224bool SingleDicWorkflow::track(
const std::vector<cv::Mat>& frames,
const cv::Mat& roi_mask,
225 const std::vector<int>& seed_pw, SingleDicResult& out)
const {
226 if (frames.empty())
return false;
229 const int scalefactor = spacing + 1;
234 auto as_gray = [](
const cv::Mat& m) -> cv::Mat {
236 if (g.channels() != 1) cv::cvtColor(g, g, cv::COLOR_BGR2GRAY);
237 if (g.type() != CV_8UC1) g.convertTo(g, CV_8UC1);
238 if (!g.isContinuous()) g = g.clone();
242 cv::Mat ref8 = as_gray(frames.front());
244 if (!roi_mask.empty()) {
246 if (roi8.type() != CV_8UC1) roi8.convertTo(roi8, CV_8UC1);
247 if (!roi8.isContinuous()) roi8 = roi8.clone();
250 out.image_size = ref8.size();
251 out.roi_mask = roi8.empty() ? cv::Mat() : roi8.clone();
252 out.subset_spacing = spacing;
253 out.seed_pw = seed_pw;
258 std::vector<ncorr::Image2D> imgs;
259 imgs.reserve(frames.size());
260 imgs.emplace_back(ref8);
261 for (
size_t i = 1; i < frames.size(); ++i) {
262 imgs.emplace_back(as_gray(frames[i]));
266 ncorr::ROI2D roi = roi8.empty()
267 ? ncorr::ROI2D(ncorr::Array2D<bool>(ref8.rows, ref8.cols,
true))
268 :
cppxdic::ROIManager::matToNcorrROI(roi8);
270 ncorr::DIC_analysis_input dic_input(imgs, roi, scalefactor,
271 ncorr::INTERP::QUINTIC_BSPLINE_PRECOMPUTE,
272 ncorr::SUBREGION::CIRCLE, radius, threads,
280 std::vector<ncorr::SeedParams> seeds;
281 if (seed_pw.size() >= 2) {
282 seeds.emplace_back(seed_pw[0], seed_pw[1]);
288 ncorr::DIC_analysis_output dic_output;
290#ifdef CPPXDIC_HAVE_CUNCORR
298 if (seed_pw.size() >= 2) {
305 LOG_INFO <<
"[singledic] running cuNCorr DIC over " << (imgs.size() - 1)
306 <<
" frame(s), " << threads <<
" thread(s), seed=("
307 << (seed_pw.size() > 0 ? seed_pw[0] : 0) <<
", "
308 << (seed_pw.size() > 1 ? seed_pw[1] : 0) <<
")";
311 << (cinfo.
cuda ?
" (CUDA)" :
" (CPU)");
315 LOG_INFO <<
"[singledic] running " << (go_parallel ?
"parallel" :
"sequential")
316 << (exact ?
" exact-matlab" :
" matlab") <<
" (ncorr) DIC over "
317 << (imgs.size() - 1) <<
" frame(s), " << threads <<
" thread(s), seed=("
318 << (seed_pw.size() > 0 ? seed_pw[0] : 0) <<
", "
319 << (seed_pw.size() > 1 ? seed_pw[1] : 0) <<
")";
321 ncorr::DIC_analysis_parallel_input pin(dic_input, seeds);
322 dic_output = exact ? ncorr::exact_matlab_DIC_analysis_parallel(pin)
323 : ncorr::matlab_DIC_analysis_parallel(pin);
326 ? ncorr::exact_matlab_DIC_analysis_sequential(dic_input, seeds,
false)
327 : ncorr::matlab_DIC_analysis_sequential(dic_input, seeds, false);
330 }
catch (
const std::exception& e) {
331 out.message = std::string(
"DIC analysis failed: ") + e.what();
336 const double nan = std::numeric_limits<double>::quiet_NaN();
337 out.frames.reserve(dic_output.disps.size());
338 for (
const auto& disp : dic_output.disps) {
339 const auto& u_arr = disp.get_u().get_array();
340 const auto& v_arr = disp.get_v().get_array();
341 const auto& cc_arr = disp.get_cc().get_array();
342 const auto& mask = disp.get_roi().get_mask();
343 const int h = u_arr.height();
344 const int w = u_arr.width();
349 const std::size_t n =
static_cast<std::size_t
>(w) * h;
352 fr.corrcoef.assign(n, nan);
353 for (
int i = 0; i < h; ++i) {
354 for (
int j = 0; j < w; ++j) {
355 if (i < mask.height() && j < mask.width() && mask(i, j)) {
356 const std::size_t idx =
static_cast<std::size_t
>(i) * w + j;
357 fr.u[idx] = u_arr(i, j);
358 fr.v[idx] = v_arr(i, j);
359 fr.corrcoef[idx] = cc_arr(i, j);
364 out.frames.push_back(std::move(fr));
368 out.message =
"tracking complete";
375bool SingleDicWorkflow::writeCsv(
const SingleDicResult& res)
const {
376 const std::string path = cfg_.
ncorrStem() +
".csv";
377 std::ofstream f(path);
379 LOG_ERROR <<
"[singledic] cannot write CSV: " << path;
382 f <<
"frame,grid_x,grid_y,u_px,v_px,corrcoef\n";
383 f.setf(std::ios::fmtflags(0), std::ios::floatfield);
384 for (
size_t fi = 0; fi < res.frames.size(); ++fi) {
385 const FrameResult& fr = res.frames[fi];
386 for (
int gy = 0; gy < fr.grid_height; ++gy) {
387 for (
int gx = 0; gx < fr.grid_width; ++gx) {
388 size_t idx =
static_cast<size_t>(gy) * fr.grid_width + gx;
389 if (idx >= fr.u.size())
continue;
390 double u = fr.u[idx];
391 if (std::isnan(u))
continue;
392 double v = (idx < fr.v.size()) ? fr.v[idx] : 0.0;
393 double c = (idx < fr.corrcoef.size()) ? fr.corrcoef[idx] : 0.0;
394 f << (fi + 1) <<
',' << gx <<
',' << gy <<
',' << u <<
',' << v <<
',' << c <<
'\n';
398 LOG_INFO <<
"[singledic] wrote " << path;
405bool put_matrix(mat_t* mat,
const std::string& name,
const std::vector<double>& data,
size_t rows,
407 if (rows * cols != data.size())
return false;
408 std::vector<double> col_major(rows * cols);
409 for (
size_t r = 0; r < rows; ++r)
410 for (
size_t c = 0; c < cols; ++c) col_major[c * rows + r] = data[r * cols + c];
411 size_t dims[2] = {rows, cols};
413 Mat_VarCreate(name.c_str(), MAT_C_DOUBLE, MAT_T_DOUBLE, 2, dims, col_major.data(), 0);
414 if (!var)
return false;
415 Mat_VarWrite(mat, var, MAT_COMPRESSION_NONE);
420bool put_scalar(mat_t* mat,
const std::string& name,
double value) {
421 size_t dims[2] = {1, 1};
422 matvar_t* var = Mat_VarCreate(name.c_str(), MAT_C_DOUBLE, MAT_T_DOUBLE, 2, dims, &value, 0);
423 if (!var)
return false;
424 Mat_VarWrite(mat, var, MAT_COMPRESSION_NONE);
439bool SingleDicWorkflow::writeMat(
const SingleDicResult& res)
const {
440 const std::string path = cfg_.
ncorrStem() +
".mat";
441 mat_t* mat = Mat_CreateVer(path.c_str(),
nullptr, MAT_FT_MAT5);
443 LOG_ERROR <<
"[singledic] cannot create MAT: " << path;
447 put_scalar(mat,
"spacing",
static_cast<double>(res.subset_spacing));
448 put_scalar(mat,
"Nframe",
static_cast<double>(res.frames.size()));
449 if (res.seed_pw.size() >= 2) {
450 std::vector<double> seed = {
static_cast<double>(res.seed_pw[0]),
451 static_cast<double>(res.seed_pw[1])};
452 put_matrix(mat,
"seed", seed, 1, 2);
455 for (
size_t fi = 0; fi < res.frames.size(); ++fi) {
456 const FrameResult& fr = res.frames[fi];
457 const size_t rows =
static_cast<size_t>(std::max(0, fr.grid_height));
458 const size_t cols =
static_cast<size_t>(std::max(0, fr.grid_width));
459 const std::string k = std::to_string(fi + 1);
460 if (rows * cols == fr.u.size() && !fr.u.empty()) {
461 put_matrix(mat,
"U_" + k, fr.u, rows, cols);
462 if (fr.v.size() == rows * cols) put_matrix(mat,
"V_" + k, fr.v, rows, cols);
463 if (fr.corrcoef.size() == rows * cols)
464 put_matrix(mat,
"C_" + k, fr.corrcoef, rows, cols);
469 LOG_INFO <<
"[singledic] wrote " << path;
479 LOG_INFO <<
"-------------------------------------------";
480 LOG_INFO <<
"singledic: single-camera 2D DIC analysis";
490 std::vector<cv::Mat> raw;
491 if (!importFrames(raw)) {
492 result.
message =
"frame import failed";
497 std::vector<cv::Mat> satur = saturate(raw);
500 cv::Mat roi = loadOrCreateRoi(satur.front());
503 std::vector<cv::Mat> proc = maybeFilter(satur, roi);
506 std::vector<int> seed = loadOrCreateSeed(roi);
507 LOG_INFO <<
"[singledic] seed = (" << (seed.size() > 0 ? seed[0] : 0) <<
", "
508 << (seed.size() > 1 ? seed[1] : 0) <<
")";
516 LOG_INFO <<
"[singledic] (matching-to-reference requested; using trial ROI/seed — "
517 "cross-trial mapping is a documented gap)";
521 if (!track(proc, roi, seed, result)) {
530 LOG_INFO <<
"--> singledic analysis completed";
bool ncorr_use_exact_matlab
double ncorr_cutoff_corrcoef
ImageProcessor class Provides image preprocessing operations for DIC analysis.
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...
SingleDicResult run()
Run the full pipeline: import, saturate, (filter), ROI/seed, (optional matching), single tracking pas...
SingleDicWorkflow(const SinglediConfig &config)
Wraps a CPPxDIC Config and adds a single-camera sub-path scheme.
int limit_grayscale_high
Upper saturation clamp (satur high).
std::string reftrial
Reference trial name (shared per bloc).
bool do_matching
If true, run a matching-to-reference-trial pass before tracking (Vik ncorr_matching2ref_single....
cppxdic::BaseParameters toBaseParameters() const
Build a BaseParameters view so existing CPPxDIC helpers (ROIManager, ImageProcessor,...
int param_filt_low
Bandpass low cutoff.
std::string inputTrialDir() const
Directory for the current trial's input (subject/bloc/trial).
int idx_frame_start
First frame to analyse (1-based, MATLAB-style).
bool write_mat
Whether to write a MATLAB-compatible .mat sidecar next to outputs.
std::string bloc
Bloc / block folder name.
std::string outputBlocDir() const
Output directory shared per (subject, bloc) — holds ROI/seed reference files.
std::string ncorrStem() const
ncorr tracking output stem (no extension). Mirrors Vik: ncorr<ncorr_number>
std::string tracking_dir
Tracking direction: "forward" or "backward" (backward flips the sequence).
bool filter_im_mode
Apply bandpass+normalise filtering before DIC.
bool write_csv
Whether to write a CSV of the tracked feature trajectories.
std::string outputTrialDir() const
Output directory for the current trial (subject/bloc/trial under dic_path).
int limit_grayscale_low
Lower saturation clamp (satur low).
int frame_jump
Frame stride.
int idx_frame_end
Last frame (0 = "to end of sequence").
std::string trial
Current trial name (video stem).
Config base
Underlying engine configuration (owns all DIC/ncorr/viz parameters).
int param_filt_high
Bandpass high cutoff.
std::string subject
Subject folder name (e.g. "S01").
Adapter: run the cuNCorr DIC engine and package results as ncorr::DIC_analysis_output.
Lightweight, dependency-free logging facility for CPPxDIC.
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...
Single-camera 2D DIC step-D pipeline (no stereo, no 3D).
Base parameters structure Contains fundamental paths and settings for DIC analysis Equivalent to base...
Tunables mirrored onto cuncorr::SessionConfig.
int seed_search
coarse ZNCC seed search radius (px)
int seed_y
manual seed row (full-image px); -1 = auto
int seed_x
manual seed column (full-image px); -1 = auto
Which backend actually ran.
bool cuda
true if a CUDA device executed the run
std::string backend
e.g. "cpu" or "cuda:0 <name>"
Seed point structure Stores seed points in both pixel world (pw) and subset world (sw) coordinates.
Full single-trial output: one reference + N tracked frames.
std::string message
Human-readable status.