28ExportFormat Visualization::parseExportFormat(
const std::string& format_str) {
29 if (format_str ==
"vtk" || format_str ==
"VTK") {
31 }
else if (format_str ==
"ply" || format_str ==
"PLY") {
33 }
else if (format_str ==
"csv" || format_str ==
"CSV") {
36 LOG_WARN <<
"Unknown export format '" << format_str
37 <<
"', defaulting to VTK";
42 LOG_INFO <<
"Loading DIC results from binary cache: " << filepath;
44 if (!std::filesystem::exists(filepath)) {
45 throw std::runtime_error(
"Binary cache file not found: " + filepath);
63 LOG_INFO <<
"Temporal filtering disabled";
71 for (
const auto& plot_field : config_.
plotopt) {
72 if (plot_field ==
"DispX" || plot_field ==
"DispY" || plot_field ==
"DispZ") {
74 LOG_INFO <<
" Filtering displacement vector (" << plot_field <<
")";
80 }
else if (plot_field ==
"DispMgn") {
82 LOG_INFO <<
" Filtering displacement magnitude";
93 LOG_INFO <<
" Filtering face correlation";
100 LOG_INFO <<
"Temporal filtering complete";
111 const std::vector<int>& faces,
115 std::map<int, std::vector<int>> vertex_to_faces;
116 for (
size_t f = 0; f < n_faces; ++f) {
117 int v0 = faces[f * 3];
118 int v1 = faces[f * 3 + 1];
119 int v2 = faces[f * 3 + 2];
120 vertex_to_faces[v0].push_back(
static_cast<int>(f));
121 vertex_to_faces[v1].push_back(
static_cast<int>(f));
122 vertex_to_faces[v2].push_back(
static_cast<int>(f));
126 std::vector<std::vector<int>> adjacency(n_faces);
127 for (
size_t f = 0; f < n_faces; ++f) {
128 std::set<int> neighbors;
129 int v0 = faces[f * 3];
130 int v1 = faces[f * 3 + 1];
131 int v2 = faces[f * 3 + 2];
133 for (
int adj_f : vertex_to_faces[v0]) {
134 if (adj_f !=
static_cast<int>(f)) neighbors.insert(adj_f);
136 for (
int adj_f : vertex_to_faces[v1]) {
137 if (adj_f !=
static_cast<int>(f)) neighbors.insert(adj_f);
139 for (
int adj_f : vertex_to_faces[v2]) {
140 if (adj_f !=
static_cast<int>(f)) neighbors.insert(adj_f);
143 adjacency[f] = std::vector<int>(neighbors.begin(), neighbors.end());
158 const std::vector<int>& faces,
161 std::vector<std::array<double, 3>> centroids(n_faces);
163 for (
size_t f = 0; f < n_faces; ++f) {
164 int v0 = faces[f * 3];
165 int v1 = faces[f * 3 + 1];
166 int v2 = faces[f * 3 + 2];
168 centroids[f][0] = (points.
x[v0] + points.
x[v1] + points.
x[v2]) / 3.0;
169 centroids[f][1] = (points.
y[v0] + points.
y[v1] + points.
y[v2]) / 3.0;
170 centroids[f][2] = (points.
z[v0] + points.
z[v1] + points.
z[v2]) / 3.0;
183 const std::vector<std::array<double, 3>>& centroids,
184 const std::vector<std::vector<int>>& adjacency) {
186 double sum_dist = 0.0;
189 for (
size_t f = 0; f < centroids.size(); ++f) {
190 for (
int adj_f : adjacency[f]) {
191 double dx = centroids[f][0] - centroids[adj_f][0];
192 double dy = centroids[f][1] - centroids[adj_f][1];
193 double dz = centroids[f][2] - centroids[adj_f][2];
194 sum_dist += std::sqrt(dx*dx + dy*dy + dz*dz);
199 return (
count > 0) ? (sum_dist /
count) : 1.0;
209 const std::vector<std::vector<int>>& adjacency,
212 size_t n_faces = adjacency.size();
213 std::vector<std::vector<std::pair<int, int>>> neighbors(n_faces);
215 for (
size_t f = 0; f < n_faces; ++f) {
217 std::vector<int> dist(n_faces, -1);
218 std::queue<int> queue;
219 queue.push(
static_cast<int>(f));
222 std::vector<std::pair<int, int>> found;
223 found.push_back({
static_cast<int>(f), 0});
225 while (!queue.empty() &&
static_cast<int>(found.size()) < k + 1) {
226 int curr = queue.front();
229 for (
int adj : adjacency[curr]) {
231 dist[adj] = dist[curr] + 1;
233 found.push_back({adj, dist[adj]});
234 if (
static_cast<int>(found.size()) >= k + 1)
break;
239 neighbors[f] = found;
246 const std::vector<double>& face_data,
247 const std::vector<int>& faces) {
253 size_t n_faces = face_data.size();
254 if (n_faces == 0 || faces.empty()) {
259 if (faces.size() < n_faces * 3) {
260 LOG_WARN <<
"Faces array too small for spatial smoothing";
267 LOG_INFO <<
" Applying spatial smoothing (n=" << num_neighbors
268 <<
", sigma=" << sigma <<
")...";
279 std::vector<double> smoothed(n_faces);
281 for (
size_t f = 0; f < n_faces; ++f) {
282 double weighted_sum = 0.0;
283 double weight_sum = 0.0;
285 for (
const auto& [neighbor_idx, hop_dist] : neighbors[f]) {
286 double val = face_data[neighbor_idx];
289 if (std::isnan(val))
continue;
292 double weight = std::exp(-(
static_cast<double>(hop_dist * hop_dist)) / (sigma * sigma));
294 weighted_sum += weight * val;
295 weight_sum += weight;
298 if (weight_sum > 0) {
299 smoothed[f] = weighted_sum / weight_sum;
301 smoothed[f] = face_data[f];
305 LOG_INFO <<
" Spatial smoothing complete";
310std::vector<std::vector<double>> Visualization::extractScalarField(
312 const std::string& field_name) {
314 std::vector<std::vector<double>> field_data;
315 size_t n_frames = results.
Points3D.size();
323 if (field_name ==
"DispX") {
325 for (
size_t i = 0; i < n_frames; ++i) {
327 std::vector<double> frame_data;
328 for (
size_t j = 0; j < results.
Disp.
DispVec[i].size(); j += 3) {
331 field_data.push_back(frame_data);
334 }
else if (field_name ==
"DispY") {
335 for (
size_t i = 0; i < n_frames; ++i) {
337 std::vector<double> frame_data;
338 for (
size_t j = 1; j < results.
Disp.
DispVec[i].size(); j += 3) {
341 field_data.push_back(frame_data);
344 }
else if (field_name ==
"DispZ") {
345 for (
size_t i = 0; i < n_frames; ++i) {
347 std::vector<double> frame_data;
348 for (
size_t j = 2; j < results.
Disp.
DispVec[i].size(); j += 3) {
351 field_data.push_back(frame_data);
354 }
else if (field_name ==
"DispMgn") {
356 }
else if (field_name ==
"FaceCorrComb") {
363 std::function<const std::vector<double>&(
const DeformationResult&)> accessor;
364 if (field_name ==
"Epc1") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Epc1; };
365 else if (field_name ==
"Epc2") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Epc2; };
366 else if (field_name ==
"epc1") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.epc1; };
367 else if (field_name ==
"epc2") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.epc2; };
368 else if (field_name ==
"J") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.J; };
369 else if (field_name ==
"EShearMax") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.EShearMax; };
370 else if (field_name ==
"eShearMax") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.eShearMax; };
371 else if (field_name ==
"Emgn") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Emgn; };
372 else if (field_name ==
"emgn") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.emgn; };
373 else if (field_name ==
"Eeq") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Eeq; };
374 else if (field_name ==
"eeq") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.eeq; };
375 else if (field_name ==
"Lamda1") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Lamda1; };
376 else if (field_name ==
"Lamda2") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Lamda2; };
377 else if (field_name ==
"Area") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Area; };
378 else if (field_name ==
"Dnorm") accessor = [](
const DeformationResult& d) ->
const std::vector<double>& {
return d.Dnorm; };
381 for (
size_t i = 0; i < df_frames && i < results.
deform_full.
frames.size(); ++i) {
385 LOG_WARN <<
"Unknown deformation field '" << field_name <<
"'";
388 LOG_WARN <<
"No deformation data available for field '" << field_name <<
"'";
394void Visualization::applyCorrelationFilter(
395 std::vector<std::vector<double>>& face_data,
396 const std::vector<std::vector<double>>& corr_data) {
402 LOG_INFO <<
"Applying correlation filter (max coeff: "
405 for (
size_t frame = 0; frame < face_data.size() && frame < corr_data.size(); ++frame) {
406 for (
size_t i = 0; i < face_data[frame].size() && i < corr_data[frame].size(); ++i) {
408 face_data[frame][i] = std::numeric_limits<double>::quiet_NaN();
414void Visualization::applyGapFilter(
415 std::vector<std::vector<double>>& face_data,
416 const std::vector<int>& FacePairInds) {
418 if (!config_.
gapLogic || FacePairInds.empty()) {
422 LOG_INFO <<
"Applying gap filter (suppressing stitched boundaries)";
426 for (
size_t i = 0; i < FacePairInds.size(); ++i) {
427 if (FacePairInds[i] == 3) {
428 for (
size_t frame = 0; frame < face_data.size(); ++frame) {
429 if (i < face_data[frame].size()) {
430 face_data[frame][i] = std::numeric_limits<double>::quiet_NaN();
438 LOG_INFO <<
"Preparing visualization data...";
447 for (
const auto& field_name : config_.
plotopt) {
448 LOG_DEBUG <<
" Extracting field: " << field_name;
449 auto field_data = extractScalarField(results, field_name);
451 if (!field_data.empty()) {
454 applyCorrelationFilter(field_data, results.
FaceCorrComb);
463 for (
auto& frame_data : field_data) {
473 <<
" frames, " << vis_data.
FaceScalars.size() <<
" fields)";
480 const std::string& output_path,
484 std::vector<int> frames_to_export;
485 if (frame_idx >= 0) {
486 frames_to_export.push_back(frame_idx);
490 for (
size_t i = 0; i < vis_data.
n_frames; ++i) {
491 frames_to_export.push_back(
static_cast<int>(i));
495 frames_to_export.push_back(
static_cast<int>(vis_data.
n_frames - 1));
499 for (
int frame : frames_to_export) {
500 if (frame < 0 || frame >=
static_cast<int>(vis_data.
n_frames)) {
501 LOG_WARN <<
"Frame " << frame <<
" out of range";
506 (
"_frame" + std::to_string(frame)) :
"";
508 std::string filepath = output_path + frame_suffix;
510 switch (export_format_) {
526 const std::string& filepath,
529 LOG_INFO <<
"Exporting frame " << frame_idx <<
" to VTK: " << filepath;
531 std::ofstream ofs(filepath);
533 throw std::runtime_error(
"Cannot open file for writing: " + filepath);
537 ofs <<
"# vtk DataFile Version 3.0\n";
538 ofs <<
"DIC 3D Results - Frame " << frame_idx <<
"\n";
540 ofs <<
"DATASET POLYDATA\n";
543 const auto& pts = vis_data.
Points3D[frame_idx];
544 size_t n_points = pts.x.size();
545 ofs <<
"POINTS " << n_points <<
" float\n";
546 for (
size_t i = 0; i < n_points; ++i) {
547 ofs << pts.x[i] <<
" " << pts.y[i] <<
" " << pts.z[i] <<
"\n";
551 size_t n_faces = vis_data.
Faces.size() / 3;
552 ofs <<
"\nPOLYGONS " << n_faces <<
" " << (n_faces * 4) <<
"\n";
553 for (
size_t i = 0; i < n_faces; ++i) {
555 << vis_data.
Faces[i * 3] <<
" "
556 << vis_data.
Faces[i * 3 + 1] <<
" "
557 << vis_data.
Faces[i * 3 + 2] <<
"\n";
562 ofs <<
"\nCELL_DATA " << n_faces <<
"\n";
564 for (
const auto& [field_name, field_data] : vis_data.
FaceScalars) {
565 if (frame_idx >=
static_cast<int>(field_data.size()))
continue;
567 const auto& frame_data = field_data[frame_idx];
568 if (frame_data.size() != n_faces) {
570 <<
" size mismatch (" << frame_data.size()
571 <<
" vs " << n_faces <<
")";
575 ofs <<
"\nSCALARS " << field_name <<
" float 1\n";
576 ofs <<
"LOOKUP_TABLE default\n";
577 for (
double val : frame_data) {
584 LOG_INFO <<
" VTK export complete: " << n_points <<
" points, "
585 << n_faces <<
" faces";
590 const std::string& filepath,
593 LOG_INFO <<
"Exporting frame " << frame_idx <<
" to PLY: " << filepath;
595 std::ofstream ofs(filepath);
597 throw std::runtime_error(
"Cannot open file for writing: " + filepath);
600 const auto& pts = vis_data.
Points3D[frame_idx];
601 size_t n_points = pts.x.size();
602 size_t n_faces = vis_data.
Faces.size() / 3;
606 ofs <<
"format ascii 1.0\n";
607 ofs <<
"comment DIC 3D Results - Frame " << frame_idx <<
"\n";
608 ofs <<
"element vertex " << n_points <<
"\n";
609 ofs <<
"property float x\n";
610 ofs <<
"property float y\n";
611 ofs <<
"property float z\n";
612 ofs <<
"element face " << n_faces <<
"\n";
613 ofs <<
"property list uchar int vertex_indices\n";
614 ofs <<
"end_header\n";
617 for (
size_t i = 0; i < n_points; ++i) {
618 ofs << pts.x[i] <<
" " << pts.y[i] <<
" " << pts.z[i] <<
"\n";
622 for (
size_t i = 0; i < n_faces; ++i) {
624 << vis_data.
Faces[i * 3] <<
" "
625 << vis_data.
Faces[i * 3 + 1] <<
" "
626 << vis_data.
Faces[i * 3 + 2] <<
"\n";
635 const std::string& filepath,
638 LOG_INFO <<
"Exporting frame " << frame_idx <<
" to CSV: " << filepath;
640 std::ofstream ofs(filepath);
642 throw std::runtime_error(
"Cannot open file for writing: " + filepath);
645 const auto& pts = vis_data.
Points3D[frame_idx];
646 size_t n_points = pts.x.size();
649 ofs <<
"point_id,x,y,z";
650 for (
const auto& [field_name, _] : vis_data.
FaceScalars) {
651 ofs <<
"," << field_name;
656 for (
size_t i = 0; i < n_points; ++i) {
657 ofs << i <<
"," << pts.x[i] <<
"," << pts.y[i] <<
"," << pts.z[i];
660 for (
const auto& [field_name, field_data] : vis_data.
FaceScalars) {
674 const std::string& output_path) {
676 LOG_INFO <<
"Generating summary statistics...";
678 std::ofstream ofs(output_path);
680 throw std::runtime_error(
"Cannot open file for writing: " + output_path);
683 ofs <<
"DIC 3D Post-Processing Results Summary\n";
684 ofs <<
"========================================\n\n";
687 ofs <<
"Number of frames: " << results.
Points3D.size() <<
"\n";
689 ofs <<
"Number of points per frame: " << results.
Points3D[0].x.size() <<
"\n";
691 ofs <<
"Number of faces: " << (results.
Faces.size() / 3) <<
"\n";
692 ofs <<
"Deformation type: " << results.
deftype <<
"\n";
696 auto computeStats = [](
const std::vector<double>& data,
697 double& min_val,
double& max_val,
double& mean_val,
698 size_t& num_valid,
size_t& num_nan,
size_t& num_zeros) {
699 min_val = std::numeric_limits<double>::infinity();
700 max_val = -std::numeric_limits<double>::infinity();
707 for (
const auto& val : data) {
708 if (std::isnan(val)) {
710 }
else if (std::isinf(val)) {
715 min_val = std::min(min_val, val);
716 max_val = std::max(max_val, val);
717 if (std::abs(val) < 1e-10) {
724 mean_val = sum / num_valid;
726 min_val = std::numeric_limits<double>::quiet_NaN();
727 max_val = std::numeric_limits<double>::quiet_NaN();
728 mean_val = std::numeric_limits<double>::quiet_NaN();
734 ofs <<
"Displacement Statistics:\n";
735 ofs <<
"------------------------\n";
737 for (
size_t frame = 0; frame < results.
Disp.
DispMgn.size(); ++frame) {
740 double min_val, max_val, mean_val;
741 size_t num_valid, num_nan, num_zeros;
742 computeStats(disp, min_val, max_val, mean_val, num_valid, num_nan, num_zeros);
744 ofs << std::setprecision(6) << std::fixed;
745 ofs <<
" Frame " << frame <<
": "
746 <<
"min=" << min_val <<
", "
747 <<
"max=" << max_val <<
", "
748 <<
"mean=" << mean_val;
750 if (num_nan > 0 || num_zeros > 0) {
751 ofs <<
" (valid=" << num_valid <<
"/" << disp.size();
753 ofs <<
", NaN=" << num_nan;
756 ofs <<
", zeros=" << num_zeros;
764 ofs <<
"Displacement Statistics:\n";
765 ofs <<
"------------------------\n";
766 ofs <<
" No displacement data available\n\n";
771 ofs <<
"Correlation Coefficient Statistics:\n";
772 ofs <<
"------------------------------------\n";
774 for (
size_t frame = 0; frame < results.
FaceCorrComb.size(); ++frame) {
777 double min_val, max_val, mean_val;
778 size_t num_valid, num_nan, num_zeros;
779 computeStats(corr, min_val, max_val, mean_val, num_valid, num_nan, num_zeros);
781 ofs << std::setprecision(6) << std::fixed;
782 ofs <<
" Frame " << frame <<
": "
783 <<
"min=" << min_val <<
", "
784 <<
"max=" << max_val <<
", "
785 <<
"mean=" << mean_val;
787 if (num_nan > 0 || num_zeros > 0) {
788 ofs <<
" (valid=" << num_valid <<
"/" << corr.size();
790 ofs <<
", NaN=" << num_nan;
793 ofs <<
", zeros=" << num_zeros;
801 ofs <<
"Correlation Coefficient Statistics:\n";
802 ofs <<
"------------------------------------\n";
803 ofs <<
" No correlation data available\n\n";
807 LOG_INFO <<
"Summary statistics written to: " << output_path;
811 LOG_INFO <<
"========================================";
812 LOG_INFO <<
"DIC 3D Post-Processing Results Info";
813 LOG_INFO <<
"========================================";
820 LOG_INFO <<
"========================================";
832 double Wn_clamped = std::max(0.001, std::min(0.999, Wn));
833 double Wp = std::tan(M_PI * Wn_clamped / 2.0);
836 double Wp2 = Wp * Wp;
837 double Wp4 = Wp2 * Wp2;
841 double a1_analog = 2.6131;
842 double a2_analog = 3.4142;
843 double a3_analog = 2.6131;
844 double a4_analog = 1.0;
849 double Wp3 = Wp2 * Wp;
852 double d0 = Wp4 + a1_analog*Wp3 + a2_analog*Wp2 + a3_analog*Wp1 + a4_analog;
853 double d1 = 4*Wp4 + 2*a1_analog*Wp3 - 2*a3_analog*Wp1 - 4*a4_analog;
854 double d2 = 6*Wp4 - 2*a2_analog*Wp2 + 6*a4_analog;
855 double d3 = 4*Wp4 - 2*a1_analog*Wp3 + 2*a3_analog*Wp1 - 4*a4_analog;
856 double d4 = Wp4 - a1_analog*Wp3 + a2_analog*Wp2 - a3_analog*Wp1 + a4_analog;
868 double gain = Wp4 / d0;
885static std::vector<double>
filtfilt(
const std::vector<double>& b,
886 const std::vector<double>& a,
887 const std::vector<double>& x) {
888 if (x.empty())
return x;
891 size_t order = b.size() - 1;
894 size_t npad = std::min(3 *
order, n - 1);
897 std::vector<double> xpad(n + 2 * npad);
900 for (
size_t i = 0; i < npad; ++i) {
901 xpad[i] = 2.0 * x[0] - x[npad - i];
904 for (
size_t i = 0; i < n; ++i) {
905 xpad[npad + i] = x[i];
908 for (
size_t i = 0; i < npad; ++i) {
909 xpad[npad + n + i] = 2.0 * x[n - 1] - x[n - 2 - i];
913 auto filter = [&](
const std::vector<double>& input) -> std::vector<double> {
914 std::vector<double> output(input.size());
915 std::vector<double> z(
order + 1, 0.0);
917 for (
size_t i = 0; i < input.size(); ++i) {
918 double y = b[0] * input[i] + z[0];
919 for (
size_t j = 0; j <
order; ++j) {
920 z[j] = b[j + 1] * input[i] - a[j + 1] * y + z[j + 1];
928 std::vector<double> y_fwd =
filter(xpad);
931 std::reverse(y_fwd.begin(), y_fwd.end());
934 std::vector<double> y_bwd =
filter(y_fwd);
937 std::reverse(y_bwd.begin(), y_bwd.end());
940 std::vector<double> result(n);
941 for (
size_t i = 0; i < n; ++i) {
942 result[i] = y_bwd[npad + i];
953 size_t n = data.size();
957 size_t first_valid = 0;
958 while (first_valid < n && std::isnan(data[first_valid])) {
961 if (first_valid == n)
return;
964 for (
size_t i = 0; i < first_valid; ++i) {
965 data[i] = data[first_valid];
969 size_t last_valid = n - 1;
970 while (last_valid > 0 && std::isnan(data[last_valid])) {
975 for (
size_t i = last_valid + 1; i < n; ++i) {
976 data[i] = data[last_valid];
980 size_t i = first_valid;
981 while (i < last_valid) {
982 if (std::isnan(data[i])) {
985 while (j < n && std::isnan(data[j])) {
989 double v0 = data[i - 1];
991 double span =
static_cast<double>(j - i + 1);
992 for (
size_t k = i; k < j; ++k) {
993 double t =
static_cast<double>(k - i + 1) / span;
994 data[k] = v0 + t * (v1 - v0);
1003std::vector<std::vector<double>> Visualization::butterworthFilter(
1004 const std::vector<std::vector<double>>& data,
1006 double sample_freq) {
1008 if (data.empty() || data[0].empty()) {
1012 size_t n_frames = data.size();
1013 size_t n_points = data[0].size();
1016 double nyquist = sample_freq / 2.0;
1017 if (cutoff_freq <= 0 || cutoff_freq >= nyquist) {
1018 LOG_WARN <<
"Invalid cutoff frequency (" << cutoff_freq
1019 <<
" Hz), must be between 0 and Nyquist (" << nyquist
1020 <<
" Hz). Returning unfiltered data.";
1025 double Wn = cutoff_freq / nyquist;
1028 std::vector<double> b, a;
1031 LOG_INFO <<
" Applying 4th order Butterworth filter (fc=" << cutoff_freq
1032 <<
" Hz, fs=" << sample_freq <<
" Hz, Wn=" << Wn <<
")";
1035 std::vector<std::vector<double>> transposed(n_points, std::vector<double>(n_frames));
1036 for (
size_t f = 0; f < n_frames; ++f) {
1037 for (
size_t p = 0; p < n_points && p < data[f].size(); ++p) {
1038 transposed[p][f] = data[f][p];
1043 size_t filtered_count = 0;
1044 for (
size_t p = 0; p < n_points; ++p) {
1046 bool has_valid =
false;
1047 for (
size_t f = 0; f < n_frames; ++f) {
1048 if (!std::isnan(transposed[p][f])) {
1059 transposed[p] =
filtfilt(b, a, transposed[p]);
1065 std::vector<std::vector<double>> result(n_frames, std::vector<double>(n_points));
1066 for (
size_t f = 0; f < n_frames; ++f) {
1067 for (
size_t p = 0; p < n_points; ++p) {
1068 result[f][p] = transposed[p][f];
1072 LOG_INFO <<
" Filtered " << filtered_count <<
"/" << n_points
1073 <<
" point time series";
1078cv::Vec3b Visualization::applyJetColormap(
double val) {
1080 val = std::max(0.0, std::min(1.0, val));
1083 cv::Mat gray(1, 1, CV_8UC1, cv::Scalar(
static_cast<uchar
>(val * 255)));
1085 cv::applyColorMap(gray, color, cv::COLORMAP_JET);
1086 return color.at<cv::Vec3b>(0, 0);
1089cv::Mat Visualization::renderFrame(
const VisData& vis_data,
int frame_idx,
1090 const std::string& field_name,
1091 double data_min,
double data_max,
1092 int width,
int height) {
1093 cv::Mat img(height, width, CV_8UC3, cv::Scalar(255, 255, 255));
1095 const auto& pts = vis_data.Points3D[frame_idx];
1096 size_t n_points = pts.x.size();
1097 size_t n_faces = vis_data.Faces.size() / 3;
1099 if (n_points == 0 || n_faces == 0)
return img;
1102 const std::vector<double>* scalar_data =
nullptr;
1103 if (!field_name.empty()) {
1104 auto it = vis_data.FaceScalars.find(field_name);
1105 if (it != vis_data.FaceScalars.end() &&
1106 frame_idx <
static_cast<int>(it->second.size())) {
1107 scalar_data = &it->second[frame_idx];
1112 double xmin = std::numeric_limits<double>::infinity();
1113 double xmax = -std::numeric_limits<double>::infinity();
1114 double ymin = std::numeric_limits<double>::infinity();
1115 double ymax = -std::numeric_limits<double>::infinity();
1116 for (
size_t i = 0; i < n_points; ++i) {
1117 if (std::isfinite(pts.x[i]) && std::isfinite(pts.y[i])) {
1118 xmin = std::min(xmin, pts.x[i]);
1119 xmax = std::max(xmax, pts.x[i]);
1120 ymin = std::min(ymin, pts.y[i]);
1121 ymax = std::max(ymax, pts.y[i]);
1125 double range_x = xmax - xmin;
1126 double range_y = ymax - ymin;
1127 if (range_x < 1e-10) range_x = 1.0;
1128 if (range_y < 1e-10) range_y = 1.0;
1132 int draw_w = width - 2 * margin;
1133 int draw_h = height - 2 * margin - 40;
1134 double scale = std::min(draw_w / range_x, draw_h / range_y);
1135 double cx = (xmin + xmax) / 2.0;
1136 double cy = (ymin + ymax) / 2.0;
1139 auto project = [&](
size_t idx) -> cv::Point {
1140 int px = margin +
static_cast<int>((pts.x[idx] - cx) * scale + draw_w / 2.0);
1141 int py = margin +
static_cast<int>((cy - pts.y[idx]) * scale + draw_h / 2.0);
1142 return cv::Point(px, py);
1146 auto vertex_valid = [&](
int v) ->
bool {
1147 return v >= 0 && v < static_cast<int>(n_points) &&
1148 std::isfinite(pts.x[v]) && std::isfinite(pts.y[v]) && std::isfinite(pts.z[v]);
1153 struct FaceZ {
size_t idx;
double z; };
1154 std::vector<FaceZ> sorted_faces;
1155 sorted_faces.reserve(n_faces);
1156 for (
size_t i = 0; i < n_faces; ++i) {
1157 int v0 = vis_data.Faces[i * 3];
1158 int v1 = vis_data.Faces[i * 3 + 1];
1159 int v2 = vis_data.Faces[i * 3 + 2];
1160 if (!vertex_valid(v0) || !vertex_valid(v1) || !vertex_valid(v2))
continue;
1161 double avg_z = (pts.z[v0] + pts.z[v1] + pts.z[v2]) / 3.0;
1162 sorted_faces.push_back({i, avg_z});
1164 std::sort(sorted_faces.begin(), sorted_faces.end(),
1165 [](
const FaceZ& a,
const FaceZ& b) { return a.z < b.z; });
1168 double data_range = data_max - data_min;
1169 if (data_range < 1e-15) data_range = 1.0;
1171 for (
const auto& fz : sorted_faces) {
1173 int v0 = vis_data.Faces[fi * 3];
1174 int v1 = vis_data.Faces[fi * 3 + 1];
1175 int v2 = vis_data.Faces[fi * 3 + 2];
1180 if (scalar_data && fi < scalar_data->size() && std::isnan((*scalar_data)[fi])) {
1184 cv::Point tri[3] = { project(v0), project(v1), project(v2) };
1187 if (tri[0] == tri[1] && tri[1] == tri[2])
continue;
1190 cv::Scalar color(200, 200, 200);
1191 if (scalar_data && fi < scalar_data->size()) {
1192 double val = (*scalar_data)[fi];
1193 if (!std::isnan(val)) {
1194 double norm = (val - data_min) / data_range;
1195 cv::Vec3b c = applyJetColormap(norm);
1196 color = cv::Scalar(c[0], c[1], c[2]);
1200 cv::fillConvexPoly(img, tri, 3, color);
1202 cv::line(img, tri[0], tri[1], cv::Scalar(60, 60, 60), 1, cv::LINE_AA);
1203 cv::line(img, tri[1], tri[2], cv::Scalar(60, 60, 60), 1, cv::LINE_AA);
1204 cv::line(img, tri[2], tri[0], cv::Scalar(60, 60, 60), 1, cv::LINE_AA);
1210 int cb_y = height - 35;
1211 int cb_w = width - 2 * margin;
1213 for (
int x = 0; x < cb_w; ++x) {
1214 double norm =
static_cast<double>(x) / cb_w;
1215 cv::Vec3b c = applyJetColormap(norm);
1216 cv::line(img, cv::Point(cb_x + x, cb_y), cv::Point(cb_x + x, cb_y + cb_h),
1217 cv::Scalar(c[0], c[1], c[2]));
1219 cv::rectangle(img, cv::Point(cb_x, cb_y), cv::Point(cb_x + cb_w, cb_y + cb_h),
1220 cv::Scalar(0, 0, 0), 1);
1223 std::ostringstream ss_min, ss_max;
1224 ss_min << std::fixed << std::setprecision(4) << data_min;
1225 ss_max << std::fixed << std::setprecision(4) << data_max;
1226 cv::putText(img, ss_min.str(), cv::Point(cb_x, cb_y + cb_h + 12),
1227 cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
1228 cv::putText(img, ss_max.str(), cv::Point(cb_x + cb_w - 50, cb_y + cb_h + 12),
1229 cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
1230 if (!field_name.empty()) {
1231 cv::putText(img, field_name, cv::Point(cb_x + cb_w / 2 - 20, cb_y - 5),
1232 cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
1237 cv::putText(img,
"Frame " + std::to_string(frame_idx),
1238 cv::Point(width - 100, 20), cv::FONT_HERSHEY_SIMPLEX, 0.4,
1239 cv::Scalar(0, 0, 0), 1);
1245 const std::string& output_path,
1246 const std::string& field_name) {
1248 LOG_WARN <<
"No frames to generate video";
1257 int fourcc = cv::VideoWriter::fourcc(
'M',
'J',
'P',
'G');
1259 fourcc = cv::VideoWriter::fourcc(
1265 double data_min = std::numeric_limits<double>::max();
1266 double data_max = std::numeric_limits<double>::lowest();
1268 std::string active_field = field_name;
1269 if (active_field.empty() && !vis_data.
FaceScalars.empty()) {
1270 active_field = vis_data.
FaceScalars.begin()->first;
1273 if (!active_field.empty()) {
1274 auto it = vis_data.
FaceScalars.find(active_field);
1276 for (
const auto& frame_data : it->second) {
1277 for (
double v : frame_data) {
1278 if (std::isfinite(v)) {
1279 data_min = std::min(data_min, v);
1280 data_max = std::max(data_max, v);
1286 if (data_min >= data_max) { data_min = 0; data_max = 1; }
1294 LOG_INFO <<
"Generating video: " << output_path
1295 <<
" (" << vis_data.
n_frames <<
" frames, " << fps <<
" fps, field="
1296 << active_field <<
", range=[" << data_min <<
", " << data_max <<
"])";
1299 std::filesystem::create_directories(std::filesystem::path(output_path).parent_path());
1301 cv::VideoWriter writer(output_path, fourcc, fps, cv::Size(width, height));
1302 if (!writer.isOpened()) {
1303 LOG_ERROR <<
"Failed to open video writer: " << output_path;
1307 for (
size_t i = 0; i < vis_data.
n_frames; ++i) {
1308 cv::Mat frame = renderFrame(vis_data,
static_cast<int>(i),
1309 active_field, data_min, data_max,
1311 writer.write(frame);
1315 LOG_INFO <<
"Video saved: " << output_path;
std::vector< int > export_frame_list
std::string export_format
std::vector< std::string > plotopt
std::string colormap_range_mode
std::vector< double > applySpatialSmooth(const std::vector< double > &face_data, const std::vector< int > &faces)
Apply spatial smoothing to face measures.
void exportFramePLY(const VisData &vis_data, const std::string &filepath, int frame_idx)
Export single frame to PLY format.
void exportFrameCSV(const VisData &vis_data, const std::string &filepath, int frame_idx)
Export single frame to CSV format.
void generateVideo(const VisData &vis_data, const std::string &output_path, const std::string &field_name="")
Generate video from visualization data Renders each frame as a 2D projection of the colored mesh and ...
void exportData(const VisData &vis_data, const std::string &output_path, int frame_idx=-1)
Export visualization data to file.
VisData prepareVisualizationData(const DIC3DPPresults &results)
Prepare visualization data from DIC results Extracts requested fields (Epc1, Epc2,...
void printTrialInfo(const DIC3DPPresults &results)
Print trial information (equivalent to MATLAB's fprintf info)
void applyTemporalFilter(DIC3DPPresults &results)
Apply temporal filtering to measures Equivalent to MATLAB's filter_measures function.
void generateSummaryStats(const DIC3DPPresults &results, const std::string &output_path)
Generate summary statistics file.
DIC3DPPresults loadFromBinaryCache(const std::string &filepath)
Load DIC results from binary cache file.
Visualization(const Config &config)
void exportFrameVTK(const VisData &vis_data, const std::string &filepath, int frame_idx)
Export single frame to VTK format.
Lightweight, dependency-free logging facility for CPPxDIC.
std::vector< double > filter(const std::vector< double > &b, const std::vector< double > &a, const std::vector< double > &x)
Forward IIR filter.
std::vector< double > filtfilt(const std::vector< double > &b, const std::vector< double > &a, const std::vector< double > &x)
Zero-phase digital filtering (MATLAB filtfilt equivalent)
DataSerializer - Abstract base class for format-agnostic DIC data I/O.
static std::vector< std::array< double, 3 > > computeFaceCentroids(const Point3DFrame &points, const std::vector< int > &faces, size_t n_faces)
Compute face centroids from vertices and faces.
static double computeAverageResolution(const std::vector< std::array< double, 3 > > ¢roids, const std::vector< std::vector< int > > &adjacency)
Compute average resolution (spacing) of face centroids.
static void designButterworth4(double Wn, std::vector< double > &b, std::vector< double > &a)
Design 4th order Butterworth low-pass filter coefficients Equivalent to MATLAB's [B,...
static void fillMissingLinear(std::vector< double > &data)
Fill missing values (NaN) with linear interpolation Equivalent to MATLAB's fillmissing(d,...
ExportFormat
Visualization export formats.
static std::vector< std::vector< int > > buildFaceAdjacency(const std::vector< int > &faces, size_t n_faces)
Build face adjacency map from triangle connectivity For each face, find all faces that share at least...
static std::vector< std::vector< std::pair< int, int > > > findKNearestByHops(const std::vector< std::vector< int > > &adjacency, int k)
Find k-nearest neighbors for each face using BFS on adjacency graph.
DIC 3D Post-Processing Results Equivalent to MATLAB's DIC3DPPresults.
FrameDeformationResult deform_full
DIC 3D Combined Results Equivalent to MATLAB's DIC3Dcombined This represents stitched results from mu...
std::vector< int > FacePairInds
std::vector< Point3DFrame > Points3D
std::vector< std::vector< double > > FaceCorrComb
static DIC3Dcombined loadBinary(const std::string &filepath)
std::vector< std::vector< double > > DispVec
std::vector< std::vector< double > > DispMgn
Visualization data container Holds filtered and processed data ready for export.
std::vector< Point3DFrame > Points3D
std::vector< std::vector< double > > FaceColors
std::map< std::string, std::vector< std::vector< double > > > FaceScalars