CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
visualization.cpp
Go to the documentation of this file.
1
5#include "visualization.h"
6#include "dic_structures.h"
8#include "logging.h"
9#include <fstream>
10#include <iostream>
11#include <iomanip>
12#include <cmath>
13#include <algorithm>
14#include <functional>
15#include <filesystem>
16#include <map>
17#include <set>
18#include <queue>
19#include <array>
20
21namespace cppxdic {
22
24 : config_(config) {
25 export_format_ = parseExportFormat(config_.export_format);
26}
27
28ExportFormat Visualization::parseExportFormat(const std::string& format_str) {
29 if (format_str == "vtk" || format_str == "VTK") {
30 return ExportFormat::VTK;
31 } else if (format_str == "ply" || format_str == "PLY") {
32 return ExportFormat::PLY;
33 } else if (format_str == "csv" || format_str == "CSV") {
34 return ExportFormat::CSV;
35 }
36 LOG_WARN << "Unknown export format '" << format_str
37 << "', defaulting to VTK";
38 return ExportFormat::VTK;
39}
40
42 LOG_INFO << "Loading DIC results from binary cache: " << filepath;
43
44 if (!std::filesystem::exists(filepath)) {
45 throw std::runtime_error("Binary cache file not found: " + filepath);
46 }
47
48 // Use the static method from DIC3Dcombined
49 DIC3Dcombined combined = DIC3Dcombined::loadBinary(filepath);
50
51 // Convert to DIC3DPPresults
52 DIC3DPPresults results;
53 // Copy base class data
54 static_cast<DIC3Dcombined&>(results) = combined;
55
56 LOG_INFO << "Loaded " << results.Points3D.size() << " frames";
57
58 return results;
59}
60
62 if (!config_.smoothTimeLogic) {
63 LOG_INFO << "Temporal filtering disabled";
64 return;
65 }
66
67 LOG_INFO << "Applying temporal filter (cutoff: " << config_.filterFreq
68 << " Hz, fs: " << config_.vid_sample_freq << " Hz)...";
69
70 // Filter displacement vectors if needed
71 for (const auto& plot_field : config_.plotopt) {
72 if (plot_field == "DispX" || plot_field == "DispY" || plot_field == "DispZ") {
73 if (!results.Disp.DispVec.empty()) {
74 LOG_INFO << " Filtering displacement vector (" << plot_field << ")";
75 results.Disp.DispVec = butterworthFilter(
76 results.Disp.DispVec,
77 config_.filterFreq,
78 config_.vid_sample_freq);
79 }
80 } else if (plot_field == "DispMgn") {
81 if (!results.Disp.DispMgn.empty()) {
82 LOG_INFO << " Filtering displacement magnitude";
83 results.Disp.DispMgn = butterworthFilter(
84 results.Disp.DispMgn,
85 config_.filterFreq,
86 config_.vid_sample_freq);
87 }
88 }
89 }
90
91 // Filter correlation coefficients
92 if (!results.FaceCorrComb.empty()) {
93 LOG_INFO << " Filtering face correlation";
94 results.FaceCorrComb = butterworthFilter(
95 results.FaceCorrComb,
96 config_.filterFreq,
97 config_.vid_sample_freq);
98 }
99
100 LOG_INFO << "Temporal filtering complete";
101}
102
110static std::vector<std::vector<int>> buildFaceAdjacency(
111 const std::vector<int>& faces,
112 size_t n_faces) {
113
114 // Build vertex-to-face map
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));
123 }
124
125 // Build face adjacency (faces sharing at least one vertex)
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];
132
133 for (int adj_f : vertex_to_faces[v0]) {
134 if (adj_f != static_cast<int>(f)) neighbors.insert(adj_f);
135 }
136 for (int adj_f : vertex_to_faces[v1]) {
137 if (adj_f != static_cast<int>(f)) neighbors.insert(adj_f);
138 }
139 for (int adj_f : vertex_to_faces[v2]) {
140 if (adj_f != static_cast<int>(f)) neighbors.insert(adj_f);
141 }
142
143 adjacency[f] = std::vector<int>(neighbors.begin(), neighbors.end());
144 }
145
146 return adjacency;
147}
148
156static std::vector<std::array<double, 3>> computeFaceCentroids(
157 const Point3DFrame& points,
158 const std::vector<int>& faces,
159 size_t n_faces) {
160
161 std::vector<std::array<double, 3>> centroids(n_faces);
162
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];
167
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;
171 }
172
173 return centroids;
174}
175
183 const std::vector<std::array<double, 3>>& centroids,
184 const std::vector<std::vector<int>>& adjacency) {
185
186 double sum_dist = 0.0;
187 size_t count = 0;
188
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);
195 count++;
196 }
197 }
198
199 return (count > 0) ? (sum_dist / count) : 1.0;
200}
201
208static std::vector<std::vector<std::pair<int, int>>> findKNearestByHops(
209 const std::vector<std::vector<int>>& adjacency,
210 int k) {
211
212 size_t n_faces = adjacency.size();
213 std::vector<std::vector<std::pair<int, int>>> neighbors(n_faces);
214
215 for (size_t f = 0; f < n_faces; ++f) {
216 // BFS to find k nearest neighbors by graph distance
217 std::vector<int> dist(n_faces, -1);
218 std::queue<int> queue;
219 queue.push(static_cast<int>(f));
220 dist[f] = 0;
221
222 std::vector<std::pair<int, int>> found; // (face_idx, hop_distance)
223 found.push_back({static_cast<int>(f), 0});
224
225 while (!queue.empty() && static_cast<int>(found.size()) < k + 1) {
226 int curr = queue.front();
227 queue.pop();
228
229 for (int adj : adjacency[curr]) {
230 if (dist[adj] < 0) {
231 dist[adj] = dist[curr] + 1;
232 queue.push(adj);
233 found.push_back({adj, dist[adj]});
234 if (static_cast<int>(found.size()) >= k + 1) break;
235 }
236 }
237 }
238
239 neighbors[f] = found;
240 }
241
242 return neighbors;
243}
244
246 const std::vector<double>& face_data,
247 const std::vector<int>& faces) {
248
249 if (!config_.smoothSpaceLogic) {
250 return face_data;
251 }
252
253 size_t n_faces = face_data.size();
254 if (n_faces == 0 || faces.empty()) {
255 return face_data;
256 }
257
258 // Verify faces array size
259 if (faces.size() < n_faces * 3) {
260 LOG_WARN << "Faces array too small for spatial smoothing";
261 return face_data;
262 }
263
264 int num_neighbors = config_.smoothPar_n; // Number of neighbors (default: 30)
265 double sigma = config_.smoothPar_sigma; // Gaussian sigma (default: 2.0)
266
267 LOG_INFO << " Applying spatial smoothing (n=" << num_neighbors
268 << ", sigma=" << sigma << ")...";
269
270 // Build face adjacency graph
271 auto adjacency = buildFaceAdjacency(faces, n_faces);
272
273 // Find k-nearest neighbors by graph distance (hops)
274 auto neighbors = findKNearestByHops(adjacency, num_neighbors);
275
276 // Compute Gaussian weights based on hop distance
277 // W = exp(-(hop_dist^2) / sigma^2)
278 // This matches MATLAB's gaussmf(0:N, [sigma, 0])
279 std::vector<double> smoothed(n_faces);
280
281 for (size_t f = 0; f < n_faces; ++f) {
282 double weighted_sum = 0.0;
283 double weight_sum = 0.0;
284
285 for (const auto& [neighbor_idx, hop_dist] : neighbors[f]) {
286 double val = face_data[neighbor_idx];
287
288 // Skip NaN values
289 if (std::isnan(val)) continue;
290
291 // Gaussian weight based on hop distance
292 double weight = std::exp(-(static_cast<double>(hop_dist * hop_dist)) / (sigma * sigma));
293
294 weighted_sum += weight * val;
295 weight_sum += weight;
296 }
297
298 if (weight_sum > 0) {
299 smoothed[f] = weighted_sum / weight_sum;
300 } else {
301 smoothed[f] = face_data[f]; // Keep original if no valid neighbors
302 }
303 }
304
305 LOG_INFO << " Spatial smoothing complete";
306
307 return smoothed;
308}
309
310std::vector<std::vector<double>> Visualization::extractScalarField(
311 const DIC3DPPresults& results,
312 const std::string& field_name) {
313
314 std::vector<std::vector<double>> field_data;
315 size_t n_frames = results.Points3D.size();
316
317 if (n_frames == 0) {
318 LOG_WARN << "No frames in results";
319 return field_data;
320 }
321
322 // Extract based on field name
323 if (field_name == "DispX") {
324 // Extract X component from DispVec
325 for (size_t i = 0; i < n_frames; ++i) {
326 if (i < results.Disp.DispVec.size() && results.Disp.DispVec[i].size() >= 3) {
327 std::vector<double> frame_data;
328 for (size_t j = 0; j < results.Disp.DispVec[i].size(); j += 3) {
329 frame_data.push_back(results.Disp.DispVec[i][j]);
330 }
331 field_data.push_back(frame_data);
332 }
333 }
334 } else if (field_name == "DispY") {
335 for (size_t i = 0; i < n_frames; ++i) {
336 if (i < results.Disp.DispVec.size() && results.Disp.DispVec[i].size() >= 3) {
337 std::vector<double> frame_data;
338 for (size_t j = 1; j < results.Disp.DispVec[i].size(); j += 3) {
339 frame_data.push_back(results.Disp.DispVec[i][j]);
340 }
341 field_data.push_back(frame_data);
342 }
343 }
344 } else if (field_name == "DispZ") {
345 for (size_t i = 0; i < n_frames; ++i) {
346 if (i < results.Disp.DispVec.size() && results.Disp.DispVec[i].size() >= 3) {
347 std::vector<double> frame_data;
348 for (size_t j = 2; j < results.Disp.DispVec[i].size(); j += 3) {
349 frame_data.push_back(results.Disp.DispVec[i][j]);
350 }
351 field_data.push_back(frame_data);
352 }
353 }
354 } else if (field_name == "DispMgn") {
355 field_data = results.Disp.DispMgn;
356 } else if (field_name == "FaceCorrComb") {
357 field_data = results.FaceCorrComb;
358 } else if (!results.deform_full.frames.empty()) {
359 // Extract from deform_full (FrameDeformationResult) — all 15 scalar fields
360 size_t df_frames = results.deform_full.n_frames;
361
362 // Map field name to member accessor
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; };
379
380 if (accessor) {
381 for (size_t i = 0; i < df_frames && i < results.deform_full.frames.size(); ++i) {
382 field_data.push_back(accessor(results.deform_full.frames[i]));
383 }
384 } else {
385 LOG_WARN << "Unknown deformation field '" << field_name << "'";
386 }
387 } else {
388 LOG_WARN << "No deformation data available for field '" << field_name << "'";
389 }
390
391 return field_data;
392}
393
394void Visualization::applyCorrelationFilter(
395 std::vector<std::vector<double>>& face_data,
396 const std::vector<std::vector<double>>& corr_data) {
397
398 if (!config_.maxCorrCoeff || corr_data.empty()) {
399 return;
400 }
401
402 LOG_INFO << "Applying correlation filter (max coeff: "
403 << config_.maxCorrCoeff << ")";
404
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) {
407 if (corr_data[frame][i] > config_.maxCorrCoeff) {
408 face_data[frame][i] = std::numeric_limits<double>::quiet_NaN();
409 }
410 }
411 }
412}
413
414void Visualization::applyGapFilter(
415 std::vector<std::vector<double>>& face_data,
416 const std::vector<int>& FacePairInds) {
417
418 if (!config_.gapLogic || FacePairInds.empty()) {
419 return;
420 }
421
422 LOG_INFO << "Applying gap filter (suppressing stitched boundaries)";
423
424 // MATLAB anim8_rewrited.m line 336: FCnow(data.FacePairInds==3)=NaN
425 // FacePairInds==3 identifies faces at the stitched boundary between pairs
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();
431 }
432 }
433 }
434 }
435}
436
438 LOG_INFO << "Preparing visualization data...";
439
440 VisData vis_data;
441 vis_data.Points3D = results.Points3D;
442 vis_data.Faces = results.Faces;
443 vis_data.FaceColors = results.FaceCorrComb;
444 vis_data.n_frames = results.Points3D.size();
445
446 // Extract requested scalar fields
447 for (const auto& field_name : config_.plotopt) {
448 LOG_DEBUG << " Extracting field: " << field_name;
449 auto field_data = extractScalarField(results, field_name);
450
451 if (!field_data.empty()) {
452 // Apply filters
453 if (!results.FaceCorrComb.empty()) {
454 applyCorrelationFilter(field_data, results.FaceCorrComb);
455 }
456
457 if (!results.FacePairInds.empty()) {
458 applyGapFilter(field_data, results.FacePairInds);
459 }
460
461 // Apply spatial smoothing if enabled
462 if (config_.smoothSpaceLogic) {
463 for (auto& frame_data : field_data) {
464 frame_data = applySpatialSmooth(frame_data, results.Faces);
465 }
466 }
467
468 vis_data.FaceScalars[field_name] = field_data;
469 }
470 }
471
472 LOG_INFO << "Visualization data prepared (" << vis_data.n_frames
473 << " frames, " << vis_data.FaceScalars.size() << " fields)";
474
475 return vis_data;
476}
477
479 const VisData& vis_data,
480 const std::string& output_path,
481 int frame_idx) {
482
483 // Determine which frames to export
484 std::vector<int> frames_to_export;
485 if (frame_idx >= 0) {
486 frames_to_export.push_back(frame_idx);
487 } else if (!config_.export_frame_list.empty()) {
488 frames_to_export = config_.export_frame_list;
489 } else if (config_.export_each_frame) {
490 for (size_t i = 0; i < vis_data.n_frames; ++i) {
491 frames_to_export.push_back(static_cast<int>(i));
492 }
493 } else {
494 // Export all frames to a single file or last frame only
495 frames_to_export.push_back(static_cast<int>(vis_data.n_frames - 1));
496 }
497
498 // Export each frame
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";
502 continue;
503 }
504
505 std::string frame_suffix = config_.export_each_frame ?
506 ("_frame" + std::to_string(frame)) : "";
507
508 std::string filepath = output_path + frame_suffix;
509
510 switch (export_format_) {
512 exportFrameVTK(vis_data, filepath + ".vtk", frame);
513 break;
515 exportFramePLY(vis_data, filepath + ".ply", frame);
516 break;
518 exportFrameCSV(vis_data, filepath + ".csv", frame);
519 break;
520 }
521 }
522}
523
525 const VisData& vis_data,
526 const std::string& filepath,
527 int frame_idx) {
528
529 LOG_INFO << "Exporting frame " << frame_idx << " to VTK: " << filepath;
530
531 std::ofstream ofs(filepath);
532 if (!ofs) {
533 throw std::runtime_error("Cannot open file for writing: " + filepath);
534 }
535
536 // VTK Legacy format header
537 ofs << "# vtk DataFile Version 3.0\n";
538 ofs << "DIC 3D Results - Frame " << frame_idx << "\n";
539 ofs << "ASCII\n";
540 ofs << "DATASET POLYDATA\n";
541
542 // Write points
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";
548 }
549
550 // Write polygons (triangles)
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) {
554 ofs << "3 "
555 << vis_data.Faces[i * 3] << " "
556 << vis_data.Faces[i * 3 + 1] << " "
557 << vis_data.Faces[i * 3 + 2] << "\n";
558 }
559
560 // Write cell data (face scalars)
561 if (!vis_data.FaceScalars.empty()) {
562 ofs << "\nCELL_DATA " << n_faces << "\n";
563
564 for (const auto& [field_name, field_data] : vis_data.FaceScalars) {
565 if (frame_idx >= static_cast<int>(field_data.size())) continue;
566
567 const auto& frame_data = field_data[frame_idx];
568 if (frame_data.size() != n_faces) {
569 LOG_WARN << "Field " << field_name
570 << " size mismatch (" << frame_data.size()
571 << " vs " << n_faces << ")";
572 continue;
573 }
574
575 ofs << "\nSCALARS " << field_name << " float 1\n";
576 ofs << "LOOKUP_TABLE default\n";
577 for (double val : frame_data) {
578 ofs << val << "\n";
579 }
580 }
581 }
582
583 ofs.close();
584 LOG_INFO << " VTK export complete: " << n_points << " points, "
585 << n_faces << " faces";
586}
587
589 const VisData& vis_data,
590 const std::string& filepath,
591 int frame_idx) {
592
593 LOG_INFO << "Exporting frame " << frame_idx << " to PLY: " << filepath;
594
595 std::ofstream ofs(filepath);
596 if (!ofs) {
597 throw std::runtime_error("Cannot open file for writing: " + filepath);
598 }
599
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;
603
604 // PLY header
605 ofs << "ply\n";
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";
615
616 // Write vertices
617 for (size_t i = 0; i < n_points; ++i) {
618 ofs << pts.x[i] << " " << pts.y[i] << " " << pts.z[i] << "\n";
619 }
620
621 // Write faces
622 for (size_t i = 0; i < n_faces; ++i) {
623 ofs << "3 "
624 << vis_data.Faces[i * 3] << " "
625 << vis_data.Faces[i * 3 + 1] << " "
626 << vis_data.Faces[i * 3 + 2] << "\n";
627 }
628
629 ofs.close();
630 LOG_INFO << " PLY export complete";
631}
632
634 const VisData& vis_data,
635 const std::string& filepath,
636 int frame_idx) {
637
638 LOG_INFO << "Exporting frame " << frame_idx << " to CSV: " << filepath;
639
640 std::ofstream ofs(filepath);
641 if (!ofs) {
642 throw std::runtime_error("Cannot open file for writing: " + filepath);
643 }
644
645 const auto& pts = vis_data.Points3D[frame_idx];
646 size_t n_points = pts.x.size();
647
648 // CSV header
649 ofs << "point_id,x,y,z";
650 for (const auto& [field_name, _] : vis_data.FaceScalars) {
651 ofs << "," << field_name;
652 }
653 ofs << "\n";
654
655 // Write data (point-based)
656 for (size_t i = 0; i < n_points; ++i) {
657 ofs << i << "," << pts.x[i] << "," << pts.y[i] << "," << pts.z[i];
658
659 // Add scalar fields (if point-based conversion available)
660 for (const auto& [field_name, field_data] : vis_data.FaceScalars) {
661 // For simplicity, write NaN for now
662 // Full implementation would interpolate face data to points
663 ofs << ",NaN";
664 }
665 ofs << "\n";
666 }
667
668 ofs.close();
669 LOG_INFO << " CSV export complete";
670}
671
673 const DIC3DPPresults& results,
674 const std::string& output_path) {
675
676 LOG_INFO << "Generating summary statistics...";
677
678 std::ofstream ofs(output_path);
679 if (!ofs) {
680 throw std::runtime_error("Cannot open file for writing: " + output_path);
681 }
682
683 ofs << "DIC 3D Post-Processing Results Summary\n";
684 ofs << "========================================\n\n";
685
686 // Basic info
687 ofs << "Number of frames: " << results.Points3D.size() << "\n";
688 if (!results.Points3D.empty()) {
689 ofs << "Number of points per frame: " << results.Points3D[0].x.size() << "\n";
690 }
691 ofs << "Number of faces: " << (results.Faces.size() / 3) << "\n";
692 ofs << "Deformation type: " << results.deftype << "\n";
693 ofs << "\n";
694
695 // Helper lambda to compute statistics filtering out NaN/Inf values
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();
701 mean_val = 0.0;
702 num_valid = 0;
703 num_nan = 0;
704 num_zeros = 0;
705
706 double sum = 0.0;
707 for (const auto& val : data) {
708 if (std::isnan(val)) {
709 num_nan++;
710 } else if (std::isinf(val)) {
711 // Skip infinities
712 } else {
713 num_valid++;
714 sum += val;
715 min_val = std::min(min_val, val);
716 max_val = std::max(max_val, val);
717 if (std::abs(val) < 1e-10) {
718 num_zeros++;
719 }
720 }
721 }
722
723 if (num_valid > 0) {
724 mean_val = sum / num_valid;
725 } else {
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();
729 }
730 };
731
732 // Field statistics
733 if (!results.Disp.DispMgn.empty() && !results.Disp.DispMgn[0].empty()) {
734 ofs << "Displacement Statistics:\n";
735 ofs << "------------------------\n";
736
737 for (size_t frame = 0; frame < results.Disp.DispMgn.size(); ++frame) {
738 const auto& disp = results.Disp.DispMgn[frame];
739
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);
743
744 ofs << std::setprecision(6) << std::fixed;
745 ofs << " Frame " << frame << ": "
746 << "min=" << min_val << ", "
747 << "max=" << max_val << ", "
748 << "mean=" << mean_val;
749
750 if (num_nan > 0 || num_zeros > 0) {
751 ofs << " (valid=" << num_valid << "/" << disp.size();
752 if (num_nan > 0) {
753 ofs << ", NaN=" << num_nan;
754 }
755 if (num_zeros > 0) {
756 ofs << ", zeros=" << num_zeros;
757 }
758 ofs << ")";
759 }
760 ofs << "\n";
761 }
762 ofs << "\n";
763 } else {
764 ofs << "Displacement Statistics:\n";
765 ofs << "------------------------\n";
766 ofs << " No displacement data available\n\n";
767 }
768
769 // Correlation statistics
770 if (!results.FaceCorrComb.empty() && !results.FaceCorrComb[0].empty()) {
771 ofs << "Correlation Coefficient Statistics:\n";
772 ofs << "------------------------------------\n";
773
774 for (size_t frame = 0; frame < results.FaceCorrComb.size(); ++frame) {
775 const auto& corr = results.FaceCorrComb[frame];
776
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);
780
781 ofs << std::setprecision(6) << std::fixed;
782 ofs << " Frame " << frame << ": "
783 << "min=" << min_val << ", "
784 << "max=" << max_val << ", "
785 << "mean=" << mean_val;
786
787 if (num_nan > 0 || num_zeros > 0) {
788 ofs << " (valid=" << num_valid << "/" << corr.size();
789 if (num_nan > 0) {
790 ofs << ", NaN=" << num_nan;
791 }
792 if (num_zeros > 0) {
793 ofs << ", zeros=" << num_zeros;
794 }
795 ofs << ")";
796 }
797 ofs << "\n";
798 }
799 ofs << "\n";
800 } else {
801 ofs << "Correlation Coefficient Statistics:\n";
802 ofs << "------------------------------------\n";
803 ofs << " No correlation data available\n\n";
804 }
805
806 ofs.close();
807 LOG_INFO << "Summary statistics written to: " << output_path;
808}
809
811 LOG_INFO << "========================================";
812 LOG_INFO << "DIC 3D Post-Processing Results Info";
813 LOG_INFO << "========================================";
814 LOG_INFO << "Subject: " << config_.subject_id;
815 LOG_INFO << "Phase: " << config_.phase_id;
816 LOG_INFO << "Material: " << config_.material;
817 LOG_INFO << "Number of frames: " << results.Points3D.size();
818 LOG_INFO << "Number of pairs: " << config_.num_pair;
819 LOG_INFO << "File version: " << config_.fileversion;
820 LOG_INFO << "========================================";
821}
822
830static void designButterworth4(double Wn, std::vector<double>& b, std::vector<double>& a) {
831 // Pre-warp the cutoff frequency for bilinear transform
832 double Wn_clamped = std::max(0.001, std::min(0.999, Wn));
833 double Wp = std::tan(M_PI * Wn_clamped / 2.0);
834
835 // Scale by cutoff frequency
836 double Wp2 = Wp * Wp;
837 double Wp4 = Wp2 * Wp2;
838
839 // Pre-computed coefficients for 4th order Butterworth analog prototype
840 // Based on analog prototype: H(s) = 1 / (s^4 + 2.6131*s^3 + 3.4142*s^2 + 2.6131*s + 1)
841 double a1_analog = 2.6131; // 2*(cos(5Ï€/8) + cos(7Ï€/8)) with sign
842 double a2_analog = 3.4142; // 2 + 2*cos(5Ï€/8)*cos(7Ï€/8) + ...
843 double a3_analog = 2.6131;
844 double a4_analog = 1.0;
845
846 // Bilinear transform with frequency pre-warping
847 // s = Wp * (z-1)/(z+1)
848 double Wp1 = Wp;
849 double Wp3 = Wp2 * Wp;
850
851 // Denominator coefficients after bilinear transform
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;
857
858 // Normalize by d0
859 a.resize(5);
860 a[0] = 1.0;
861 a[1] = d1 / d0;
862 a[2] = d2 / d0;
863 a[3] = d3 / d0;
864 a[4] = d4 / d0;
865
866 // Numerator: all-pole filter, so numerator is Wp^4 * (1 + z^-1)^4
867 // = Wp^4 * (1 + 4*z^-1 + 6*z^-2 + 4*z^-3 + z^-4)
868 double gain = Wp4 / d0;
869 b.resize(5);
870 b[0] = gain;
871 b[1] = 4 * gain;
872 b[2] = 6 * gain;
873 b[3] = 4 * gain;
874 b[4] = gain;
875}
876
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;
889
890 size_t n = x.size();
891 size_t order = b.size() - 1;
892
893 // Pad length (3 times filter order, similar to MATLAB)
894 size_t npad = std::min(3 * order, n - 1);
895
896 // Create padded signal with reflected boundaries
897 std::vector<double> xpad(n + 2 * npad);
898
899 // Reflect at beginning: 2*x[0] - x[npad], ..., 2*x[0] - x[1]
900 for (size_t i = 0; i < npad; ++i) {
901 xpad[i] = 2.0 * x[0] - x[npad - i];
902 }
903 // Copy original signal
904 for (size_t i = 0; i < n; ++i) {
905 xpad[npad + i] = x[i];
906 }
907 // Reflect at end: 2*x[n-1] - x[n-2], ..., 2*x[n-1] - x[n-1-npad]
908 for (size_t i = 0; i < npad; ++i) {
909 xpad[npad + n + i] = 2.0 * x[n - 1] - x[n - 2 - i];
910 }
911
912 // Forward filter (Direct Form II Transposed)
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); // State variables
916
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];
921 }
922 output[i] = y;
923 }
924 return output;
925 };
926
927 // Forward pass
928 std::vector<double> y_fwd = filter(xpad);
929
930 // Reverse the signal
931 std::reverse(y_fwd.begin(), y_fwd.end());
932
933 // Backward pass
934 std::vector<double> y_bwd = filter(y_fwd);
935
936 // Reverse back and extract original length
937 std::reverse(y_bwd.begin(), y_bwd.end());
938
939 // Extract the central portion (remove padding)
940 std::vector<double> result(n);
941 for (size_t i = 0; i < n; ++i) {
942 result[i] = y_bwd[npad + i];
943 }
944
945 return result;
946}
947
952static void fillMissingLinear(std::vector<double>& data) {
953 size_t n = data.size();
954 if (n == 0) return;
955
956 // Find first valid value for start
957 size_t first_valid = 0;
958 while (first_valid < n && std::isnan(data[first_valid])) {
959 first_valid++;
960 }
961 if (first_valid == n) return; // All NaN
962
963 // Fill leading NaNs with first valid value
964 for (size_t i = 0; i < first_valid; ++i) {
965 data[i] = data[first_valid];
966 }
967
968 // Find last valid value for end
969 size_t last_valid = n - 1;
970 while (last_valid > 0 && std::isnan(data[last_valid])) {
971 last_valid--;
972 }
973
974 // Fill trailing NaNs with last valid value
975 for (size_t i = last_valid + 1; i < n; ++i) {
976 data[i] = data[last_valid];
977 }
978
979 // Linear interpolation for interior NaNs
980 size_t i = first_valid;
981 while (i < last_valid) {
982 if (std::isnan(data[i])) {
983 // Find next valid value
984 size_t j = i + 1;
985 while (j < n && std::isnan(data[j])) {
986 j++;
987 }
988 // Linear interpolate between i-1 and j
989 double v0 = data[i - 1];
990 double v1 = data[j];
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);
995 }
996 i = j;
997 } else {
998 i++;
999 }
1000 }
1001}
1002
1003std::vector<std::vector<double>> Visualization::butterworthFilter(
1004 const std::vector<std::vector<double>>& data,
1005 double cutoff_freq,
1006 double sample_freq) {
1007
1008 if (data.empty() || data[0].empty()) {
1009 return data;
1010 }
1011
1012 size_t n_frames = data.size();
1013 size_t n_points = data[0].size();
1014
1015 // Validate frequencies
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.";
1021 return data;
1022 }
1023
1024 // Normalized cutoff frequency (0 to 1, where 1 = Nyquist)
1025 double Wn = cutoff_freq / nyquist;
1026
1027 // Design 4th order Butterworth filter (matching MATLAB's butter(4, Wn))
1028 std::vector<double> b, a;
1029 designButterworth4(Wn, b, a);
1030
1031 LOG_INFO << " Applying 4th order Butterworth filter (fc=" << cutoff_freq
1032 << " Hz, fs=" << sample_freq << " Hz, Wn=" << Wn << ")";
1033
1034 // Transpose data: from [frame][point] to [point][frame] for temporal filtering
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];
1039 }
1040 }
1041
1042 // Filter each point's time series
1043 size_t filtered_count = 0;
1044 for (size_t p = 0; p < n_points; ++p) {
1045 // Check if this point has valid data (not all NaN)
1046 bool has_valid = false;
1047 for (size_t f = 0; f < n_frames; ++f) {
1048 if (!std::isnan(transposed[p][f])) {
1049 has_valid = true;
1050 break;
1051 }
1052 }
1053
1054 if (has_valid) {
1055 // Fill missing values with linear interpolation
1056 fillMissingLinear(transposed[p]);
1057
1058 // Apply zero-phase Butterworth filter
1059 transposed[p] = filtfilt(b, a, transposed[p]);
1060 filtered_count++;
1061 }
1062 }
1063
1064 // Transpose back: from [point][frame] to [frame][point]
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];
1069 }
1070 }
1071
1072 LOG_INFO << " Filtered " << filtered_count << "/" << n_points
1073 << " point time series";
1074
1075 return result;
1076}
1077
1078cv::Vec3b Visualization::applyJetColormap(double val) {
1079 // Clamp to [0, 1]
1080 val = std::max(0.0, std::min(1.0, val));
1081
1082 // Use OpenCV's built-in jet colormap via LUT
1083 cv::Mat gray(1, 1, CV_8UC1, cv::Scalar(static_cast<uchar>(val * 255)));
1084 cv::Mat color;
1085 cv::applyColorMap(gray, color, cv::COLORMAP_JET);
1086 return color.at<cv::Vec3b>(0, 0);
1087}
1088
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)); // white background
1094
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;
1098
1099 if (n_points == 0 || n_faces == 0) return img;
1100
1101 // Get scalar data for this frame (if available)
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];
1108 }
1109 }
1110
1111 // Compute bounding box for orthographic projection (skip NaN/Inf)
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]);
1122 }
1123 }
1124
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;
1129
1130 // Fit mesh into image with margin
1131 int margin = 40;
1132 int draw_w = width - 2 * margin;
1133 int draw_h = height - 2 * margin - 40; // extra space for colorbar
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;
1137
1138 // Project 3D points to 2D (orthographic: use X and Y, ignore Z for now)
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); // flip Y
1142 return cv::Point(px, py);
1143 };
1144
1145 // Helper: check if a vertex has valid (finite) coordinates
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]);
1149 };
1150
1151 // Sort faces by average Z (painter's algorithm: draw far faces first)
1152 // Skip faces with any invalid vertex (NaN/Inf/out-of-bounds)
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});
1163 }
1164 std::sort(sorted_faces.begin(), sorted_faces.end(),
1165 [](const FaceZ& a, const FaceZ& b) { return a.z < b.z; });
1166
1167 // Draw triangles
1168 double data_range = data_max - data_min;
1169 if (data_range < 1e-15) data_range = 1.0;
1170
1171 for (const auto& fz : sorted_faces) {
1172 size_t fi = fz.idx;
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];
1176
1177 // Vertices are already validated during sorting, so no bounds check needed
1178
1179 // Skip faces with NaN scalar data (matching MATLAB's gpatch behavior)
1180 if (scalar_data && fi < scalar_data->size() && std::isnan((*scalar_data)[fi])) {
1181 continue;
1182 }
1183
1184 cv::Point tri[3] = { project(v0), project(v1), project(v2) };
1185
1186 // Skip degenerate projected triangles (all vertices same pixel)
1187 if (tri[0] == tri[1] && tri[1] == tri[2]) continue;
1188
1189 // Determine face color
1190 cv::Scalar color(200, 200, 200); // default gray
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]);
1197 }
1198 }
1199
1200 cv::fillConvexPoly(img, tri, 3, color);
1201 // Draw wireframe edges
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);
1205 }
1206
1207 // Draw colorbar at bottom
1208 if (scalar_data) {
1209 int cb_x = margin;
1210 int cb_y = height - 35;
1211 int cb_w = width - 2 * margin;
1212 int cb_h = 15;
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]));
1218 }
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);
1221
1222 // Labels
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);
1233 }
1234 }
1235
1236 // Frame number label
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);
1240
1241 return img;
1242}
1243
1245 const std::string& output_path,
1246 const std::string& field_name) {
1247 if (vis_data.n_frames == 0) {
1248 LOG_WARN << "No frames to generate video";
1249 return;
1250 }
1251
1252 int width = 800;
1253 int height = 600;
1254 int fps = config_.video_fps;
1255
1256 // Parse codec from config (e.g., "MJPG")
1257 int fourcc = cv::VideoWriter::fourcc('M', 'J', 'P', 'G');
1258 if (config_.video_codec.size() >= 4) {
1259 fourcc = cv::VideoWriter::fourcc(
1260 config_.video_codec[0], config_.video_codec[1],
1261 config_.video_codec[2], config_.video_codec[3]);
1262 }
1263
1264 // Determine data range across all frames for consistent colormap
1265 double data_min = std::numeric_limits<double>::max();
1266 double data_max = std::numeric_limits<double>::lowest();
1267
1268 std::string active_field = field_name;
1269 if (active_field.empty() && !vis_data.FaceScalars.empty()) {
1270 active_field = vis_data.FaceScalars.begin()->first;
1271 }
1272
1273 if (!active_field.empty()) {
1274 auto it = vis_data.FaceScalars.find(active_field);
1275 if (it != vis_data.FaceScalars.end()) {
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);
1281 }
1282 }
1283 }
1284 }
1285 }
1286 if (data_min >= data_max) { data_min = 0; data_max = 1; }
1287
1288 // Override with manual range if configured
1289 if (config_.colormap_range_mode == "manual") {
1290 data_min = config_.colormap_min;
1291 data_max = config_.colormap_max;
1292 }
1293
1294 LOG_INFO << "Generating video: " << output_path
1295 << " (" << vis_data.n_frames << " frames, " << fps << " fps, field="
1296 << active_field << ", range=[" << data_min << ", " << data_max << "])";
1297
1298 // Create output directory if needed
1299 std::filesystem::create_directories(std::filesystem::path(output_path).parent_path());
1300
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;
1304 return;
1305 }
1306
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,
1310 width, height);
1311 writer.write(frame);
1312 }
1313
1314 writer.release();
1315 LOG_INFO << "Video saved: " << output_path;
1316}
1317
1318} // namespace cppxdic
double smoothPar_sigma
Definition config.h:106
bool gapLogic
Definition config.h:97
std::vector< int > export_frame_list
Definition config.h:122
double colormap_max
Definition config.h:238
double vid_sample_freq
Definition config.h:43
std::string subject_id
Definition config.h:65
double colormap_min
Definition config.h:237
std::string material
Definition config.h:258
std::string export_format
Definition config.h:120
int num_pair
Definition config.h:34
double filterFreq
Definition config.h:103
bool smoothTimeLogic
Definition config.h:102
std::vector< std::string > plotopt
Definition config.h:92
std::string phase_id
Definition config.h:66
std::string colormap_range_mode
Definition config.h:236
bool smoothSpaceLogic
Definition config.h:104
int video_fps
Definition config.h:229
std::string fileversion
Definition config.h:125
double maxCorrCoeff
Definition config.h:98
std::string video_codec
Definition config.h:230
bool export_each_frame
Definition config.h:121
int smoothPar_n
Definition config.h:105
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.
#define LOG_DEBUG
Definition logging.h:169
#define LOG_ERROR
Definition logging.h:172
#define LOG_WARN
Definition logging.h:171
#define LOG_INFO
Definition logging.h:170
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 > > &centroids, 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< int > Faces
std::vector< std::vector< double > > DispVec
std::vector< std::vector< double > > DispMgn
std::vector< DeformationResult > frames
3D Points per frame
std::vector< double > z
std::vector< double > x
std::vector< double > y
Visualization data container Holds filtered and processed data ready for export.
std::vector< int > Faces
std::vector< Point3DFrame > Points3D
std::vector< std::vector< double > > FaceColors
std::map< std::string, std::vector< std::vector< double > > > FaceScalars
std::vector< int > order
int count