701 lines
17 KiB
C++
701 lines
17 KiB
C++
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <pcl/PolygonMesh.h>
|
|
#include <pcl/common/io.h>
|
|
#include <pcl/features/normal_3d.h>
|
|
#include <pcl/io/pcd_io.h>
|
|
#include <pcl/io/vtk_io.h>
|
|
#include <pcl/kdtree/kdtree_flann.h>
|
|
#include <pcl/point_cloud.h>
|
|
#include <pcl/point_types.h>
|
|
#include <pcl/search/kdtree.h>
|
|
#include <pcl/surface/gp3.h>
|
|
#include <pcl/surface/organized_fast_mesh.h>
|
|
|
|
namespace {
|
|
|
|
enum class MeshAlgorithm
|
|
{
|
|
Ofm,
|
|
Gp3,
|
|
};
|
|
|
|
struct ToolOptions
|
|
{
|
|
std::vector<std::filesystem::path> inputPaths;
|
|
std::filesystem::path outputDirectory;
|
|
bool hasOutputDirectory = false;
|
|
MeshAlgorithm algorithm = MeshAlgorithm::Ofm;
|
|
int ofmTrianglePixelSize = 1;
|
|
float ofmMaxEdgeLength = 0.25f;
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>::TriangulationType ofmTriangulationType =
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_ADAPTIVE_CUT;
|
|
double gp3SearchRadius = 0.05;
|
|
double gp3Mu = 2.5;
|
|
int gp3MaxNeighbors = 100;
|
|
double gp3MaxSurfaceAngleDeg = 45.0;
|
|
double gp3MinAngleDeg = 10.0;
|
|
double gp3MaxAngleDeg = 120.0;
|
|
int gp3NormalK = 20;
|
|
bool gp3NormalConsistency = false;
|
|
};
|
|
|
|
struct ConversionStats
|
|
{
|
|
std::filesystem::path inputPath;
|
|
std::filesystem::path outputPath;
|
|
MeshAlgorithm algorithm = MeshAlgorithm::Ofm;
|
|
std::size_t pointCount = 0;
|
|
std::size_t finitePointCount = 0;
|
|
std::size_t polygonCount = 0;
|
|
double normalDurationMs = 0.0;
|
|
double meshDurationMs = 0.0;
|
|
double totalDurationMs = 0.0;
|
|
bool success = false;
|
|
std::string errorMessage;
|
|
};
|
|
|
|
constexpr double kPi = 3.14159265358979323846;
|
|
constexpr std::size_t kMinimumTriangulationPoints = 3;
|
|
|
|
double degreesToRadians(double degrees)
|
|
{
|
|
return degrees * kPi / 180.0;
|
|
}
|
|
|
|
std::string algorithmToString(MeshAlgorithm algorithm)
|
|
{
|
|
switch (algorithm)
|
|
{
|
|
case MeshAlgorithm::Ofm:
|
|
return "ofm";
|
|
case MeshAlgorithm::Gp3:
|
|
return "gp3";
|
|
}
|
|
|
|
throw std::runtime_error("Unsupported mesh algorithm enum");
|
|
}
|
|
|
|
void printUsage(const char* argv0)
|
|
{
|
|
std::cerr
|
|
<< "Usage: " << argv0
|
|
<< " [--algorithm ofm|gp3]"
|
|
<< " [--output-dir DIR]"
|
|
<< " [--ofm-triangle-pixel-size N]"
|
|
<< " [--ofm-max-edge-length F]"
|
|
<< " [--ofm-triangulation adaptive|left|right|quad]"
|
|
<< " [--gp3-search-radius F]"
|
|
<< " [--gp3-mu F]"
|
|
<< " [--gp3-max-neighbors N]"
|
|
<< " [--gp3-max-surface-angle-deg F]"
|
|
<< " [--gp3-min-angle-deg F]"
|
|
<< " [--gp3-max-angle-deg F]"
|
|
<< " [--gp3-normal-k N]"
|
|
<< " [--gp3-normal-consistency true|false]"
|
|
<< " input1.pcd [input2.pcd ...]\n";
|
|
}
|
|
|
|
bool parseBooleanValue(const std::string& value)
|
|
{
|
|
if (value == "true" || value == "1")
|
|
{
|
|
return true;
|
|
}
|
|
if (value == "false" || value == "0")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
throw std::runtime_error(
|
|
"Expected boolean value true|false|1|0, got: " + value);
|
|
}
|
|
|
|
MeshAlgorithm parseAlgorithm(const std::string& value)
|
|
{
|
|
if (value == "ofm")
|
|
{
|
|
return MeshAlgorithm::Ofm;
|
|
}
|
|
if (value == "gp3")
|
|
{
|
|
return MeshAlgorithm::Gp3;
|
|
}
|
|
|
|
throw std::runtime_error("Unsupported algorithm: " + value);
|
|
}
|
|
|
|
bool parseTriangulationType(
|
|
const std::string& value,
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>::TriangulationType& ofmTriangulationType)
|
|
{
|
|
if (value == "adaptive")
|
|
{
|
|
ofmTriangulationType =
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_ADAPTIVE_CUT;
|
|
return true;
|
|
}
|
|
if (value == "left")
|
|
{
|
|
ofmTriangulationType =
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_LEFT_CUT;
|
|
return true;
|
|
}
|
|
if (value == "right")
|
|
{
|
|
ofmTriangulationType =
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>::TRIANGLE_RIGHT_CUT;
|
|
return true;
|
|
}
|
|
if (value == "quad")
|
|
{
|
|
ofmTriangulationType =
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>::QUAD_MESH;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void parseToolOption(
|
|
ToolOptions& options,
|
|
const std::string& arg,
|
|
int& i,
|
|
int argc,
|
|
char** argv)
|
|
{
|
|
auto requireValue = [&](const char* optionName) -> std::string {
|
|
if (i + 1 >= argc)
|
|
{
|
|
throw std::runtime_error(std::string(optionName) + " requires a value");
|
|
}
|
|
return argv[++i];
|
|
};
|
|
|
|
if (arg == "--algorithm")
|
|
{
|
|
options.algorithm = parseAlgorithm(requireValue("--algorithm"));
|
|
return;
|
|
}
|
|
if (arg == "--output-dir")
|
|
{
|
|
options.outputDirectory = requireValue("--output-dir");
|
|
options.hasOutputDirectory = true;
|
|
return;
|
|
}
|
|
if (arg == "--ofm-triangle-pixel-size")
|
|
{
|
|
options.ofmTrianglePixelSize = std::stoi(
|
|
requireValue("--ofm-triangle-pixel-size"));
|
|
return;
|
|
}
|
|
if (arg == "--ofm-max-edge-length")
|
|
{
|
|
options.ofmMaxEdgeLength = std::stof(
|
|
requireValue("--ofm-max-edge-length"));
|
|
return;
|
|
}
|
|
if (arg == "--ofm-triangulation")
|
|
{
|
|
std::string triangulationValue = requireValue("--ofm-triangulation");
|
|
if (!parseTriangulationType(
|
|
triangulationValue, options.ofmTriangulationType))
|
|
{
|
|
throw std::runtime_error(
|
|
"Unsupported triangulation type: " + triangulationValue);
|
|
}
|
|
return;
|
|
}
|
|
if (arg == "--gp3-search-radius")
|
|
{
|
|
options.gp3SearchRadius = std::stod(
|
|
requireValue("--gp3-search-radius"));
|
|
return;
|
|
}
|
|
if (arg == "--gp3-mu")
|
|
{
|
|
options.gp3Mu = std::stod(requireValue("--gp3-mu"));
|
|
return;
|
|
}
|
|
if (arg == "--gp3-max-neighbors")
|
|
{
|
|
options.gp3MaxNeighbors = std::stoi(
|
|
requireValue("--gp3-max-neighbors"));
|
|
return;
|
|
}
|
|
if (arg == "--gp3-max-surface-angle-deg")
|
|
{
|
|
options.gp3MaxSurfaceAngleDeg = std::stod(
|
|
requireValue("--gp3-max-surface-angle-deg"));
|
|
return;
|
|
}
|
|
if (arg == "--gp3-min-angle-deg")
|
|
{
|
|
options.gp3MinAngleDeg = std::stod(
|
|
requireValue("--gp3-min-angle-deg"));
|
|
return;
|
|
}
|
|
if (arg == "--gp3-max-angle-deg")
|
|
{
|
|
options.gp3MaxAngleDeg = std::stod(
|
|
requireValue("--gp3-max-angle-deg"));
|
|
return;
|
|
}
|
|
if (arg == "--gp3-normal-k")
|
|
{
|
|
options.gp3NormalK = std::stoi(requireValue("--gp3-normal-k"));
|
|
return;
|
|
}
|
|
if (arg == "--gp3-normal-consistency")
|
|
{
|
|
options.gp3NormalConsistency = parseBooleanValue(
|
|
requireValue("--gp3-normal-consistency"));
|
|
return;
|
|
}
|
|
|
|
throw std::runtime_error("Unknown option: " + arg);
|
|
}
|
|
|
|
ToolOptions parseArgs(int argc, char** argv)
|
|
{
|
|
ToolOptions options;
|
|
|
|
for (int i = 1; i < argc; ++i)
|
|
{
|
|
std::string arg = argv[i];
|
|
if (!arg.empty() && arg[0] == '-')
|
|
{
|
|
parseToolOption(options, arg, i, argc, argv);
|
|
continue;
|
|
}
|
|
|
|
options.inputPaths.emplace_back(arg);
|
|
}
|
|
|
|
if (options.inputPaths.empty())
|
|
{
|
|
throw std::runtime_error("At least one input .pcd file is required");
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
std::filesystem::path computeOutputPath(
|
|
const ToolOptions& options,
|
|
const std::filesystem::path& inputPath)
|
|
{
|
|
std::filesystem::path outputDirectory = options.hasOutputDirectory
|
|
? options.outputDirectory
|
|
: inputPath.parent_path();
|
|
std::filesystem::path outputFileName = inputPath.filename();
|
|
outputFileName.replace_extension(".vtk");
|
|
return outputDirectory / outputFileName;
|
|
}
|
|
|
|
pcl::PointCloud<pcl::PointXYZ>::Ptr loadInputCloud(
|
|
const std::filesystem::path& inputPath)
|
|
{
|
|
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(
|
|
new pcl::PointCloud<pcl::PointXYZ>());
|
|
if (pcl::io::loadPCDFile(inputPath.string(), *cloud) != 0)
|
|
{
|
|
throw std::runtime_error("Failed to load input PCD");
|
|
}
|
|
|
|
return cloud;
|
|
}
|
|
|
|
void ensureOutputDirectoryExists(const std::filesystem::path& outputPath)
|
|
{
|
|
std::filesystem::create_directories(outputPath.parent_path());
|
|
}
|
|
|
|
void ensureOrganizedCloudForOfm(const pcl::PointCloud<pcl::PointXYZ>& cloud)
|
|
{
|
|
if (!cloud.isOrganized())
|
|
{
|
|
throw std::runtime_error("Input point cloud is not organized");
|
|
}
|
|
}
|
|
|
|
pcl::PointCloud<pcl::PointXYZ>::Ptr buildFinitePointCloud(
|
|
const pcl::PointCloud<pcl::PointXYZ>& cloud)
|
|
{
|
|
pcl::PointCloud<pcl::PointXYZ>::Ptr finiteCloud(
|
|
new pcl::PointCloud<pcl::PointXYZ>());
|
|
finiteCloud->reserve(cloud.size());
|
|
|
|
for (const auto& point : cloud.points)
|
|
{
|
|
if (!std::isfinite(point.x) || !std::isfinite(point.y) ||
|
|
!std::isfinite(point.z))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
finiteCloud->push_back(point);
|
|
}
|
|
|
|
finiteCloud->width = static_cast<std::uint32_t>(finiteCloud->size());
|
|
finiteCloud->height = 1;
|
|
finiteCloud->is_dense = false;
|
|
return finiteCloud;
|
|
}
|
|
|
|
void ensureEnoughFinitePoints(
|
|
const pcl::PointCloud<pcl::PointXYZ>& finiteCloud,
|
|
int normalK)
|
|
{
|
|
if (finiteCloud.size() < kMinimumTriangulationPoints)
|
|
{
|
|
throw std::runtime_error("Too few finite points to triangulate");
|
|
}
|
|
|
|
if (finiteCloud.size() <= static_cast<std::size_t>(normalK))
|
|
{
|
|
throw std::runtime_error(
|
|
"Too few finite points for requested GP3 normal-k");
|
|
}
|
|
}
|
|
|
|
pcl::PointCloud<pcl::Normal>::Ptr estimateNormals(
|
|
const pcl::PointCloud<pcl::PointXYZ>::Ptr& finiteCloud,
|
|
int normalK)
|
|
{
|
|
pcl::search::KdTree<pcl::PointXYZ>::Ptr searchTree(
|
|
new pcl::search::KdTree<pcl::PointXYZ>());
|
|
searchTree->setInputCloud(finiteCloud);
|
|
|
|
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;
|
|
normalEstimation.setInputCloud(finiteCloud);
|
|
normalEstimation.setSearchMethod(searchTree);
|
|
normalEstimation.setKSearch(normalK);
|
|
normalEstimation.setViewPoint(0.0f, 0.0f, 0.0f);
|
|
|
|
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>());
|
|
normalEstimation.compute(*normals);
|
|
return normals;
|
|
}
|
|
|
|
pcl::PointCloud<pcl::PointNormal>::Ptr buildPointNormalsCloud(
|
|
const pcl::PointCloud<pcl::PointXYZ>& finiteCloud,
|
|
const pcl::PointCloud<pcl::Normal>& normals)
|
|
{
|
|
pcl::PointCloud<pcl::PointNormal>::Ptr pointNormals(
|
|
new pcl::PointCloud<pcl::PointNormal>());
|
|
pcl::concatenateFields(finiteCloud, normals, *pointNormals);
|
|
return pointNormals;
|
|
}
|
|
|
|
void ensureGp3NormalsAreFinite(
|
|
const pcl::PointCloud<pcl::PointNormal>& pointNormals)
|
|
{
|
|
for (const auto& point : pointNormals.points)
|
|
{
|
|
if (std::isfinite(point.normal_x) && std::isfinite(point.normal_y) &&
|
|
std::isfinite(point.normal_z))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw std::runtime_error("Normal estimation produced no finite normals");
|
|
}
|
|
|
|
void saveMeshOrThrow(
|
|
const std::filesystem::path& outputPath,
|
|
const pcl::PolygonMesh& mesh)
|
|
{
|
|
if (mesh.polygons.empty())
|
|
{
|
|
throw std::runtime_error("Reconstruction produced no polygons");
|
|
}
|
|
|
|
if (pcl::io::saveVTKFile(outputPath.string(), mesh) != 0)
|
|
{
|
|
throw std::runtime_error("Failed to save output VTK");
|
|
}
|
|
}
|
|
|
|
void configureOfm(
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ>& ofm,
|
|
const ToolOptions& options,
|
|
const pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud)
|
|
{
|
|
ofm.setInputCloud(cloud);
|
|
ofm.setTriangulationType(options.ofmTriangulationType);
|
|
ofm.setTrianglePixelSize(options.ofmTrianglePixelSize);
|
|
ofm.setViewpoint(Eigen::Vector3f::Zero());
|
|
ofm.setMaxEdgeLength(options.ofmMaxEdgeLength);
|
|
}
|
|
|
|
void configureGp3(
|
|
pcl::GreedyProjectionTriangulation<pcl::PointNormal>& gp3,
|
|
const ToolOptions& options,
|
|
const pcl::PointCloud<pcl::PointNormal>::Ptr& pointNormals,
|
|
const pcl::search::KdTree<pcl::PointNormal>::Ptr& searchTree)
|
|
{
|
|
gp3.setInputCloud(pointNormals);
|
|
gp3.setSearchMethod(searchTree);
|
|
gp3.setSearchRadius(options.gp3SearchRadius);
|
|
gp3.setMu(options.gp3Mu);
|
|
gp3.setMaximumNearestNeighbors(options.gp3MaxNeighbors);
|
|
gp3.setMaximumSurfaceAngle(
|
|
degreesToRadians(options.gp3MaxSurfaceAngleDeg));
|
|
gp3.setMinimumAngle(degreesToRadians(options.gp3MinAngleDeg));
|
|
gp3.setMaximumAngle(degreesToRadians(options.gp3MaxAngleDeg));
|
|
gp3.setNormalConsistency(options.gp3NormalConsistency);
|
|
}
|
|
|
|
ConversionStats convertWithOfm(
|
|
const ToolOptions& options,
|
|
const std::filesystem::path& inputPath)
|
|
{
|
|
ConversionStats stats;
|
|
stats.algorithm = MeshAlgorithm::Ofm;
|
|
stats.inputPath = inputPath;
|
|
stats.outputPath = computeOutputPath(options, inputPath);
|
|
|
|
auto cloud = loadInputCloud(inputPath);
|
|
ensureOrganizedCloudForOfm(*cloud);
|
|
ensureOutputDirectoryExists(stats.outputPath);
|
|
|
|
pcl::OrganizedFastMesh<pcl::PointXYZ> ofm;
|
|
configureOfm(ofm, options, cloud);
|
|
|
|
pcl::PolygonMesh mesh;
|
|
auto meshStart = std::chrono::steady_clock::now();
|
|
ofm.reconstruct(mesh);
|
|
auto meshEnd = std::chrono::steady_clock::now();
|
|
|
|
saveMeshOrThrow(stats.outputPath, mesh);
|
|
|
|
stats.pointCount = cloud->size();
|
|
stats.polygonCount = mesh.polygons.size();
|
|
stats.meshDurationMs = std::chrono::duration<double, std::milli>(
|
|
meshEnd - meshStart).count();
|
|
stats.totalDurationMs = stats.meshDurationMs;
|
|
stats.success = true;
|
|
return stats;
|
|
}
|
|
|
|
ConversionStats convertWithGp3(
|
|
const ToolOptions& options,
|
|
const std::filesystem::path& inputPath)
|
|
{
|
|
ConversionStats stats;
|
|
stats.algorithm = MeshAlgorithm::Gp3;
|
|
stats.inputPath = inputPath;
|
|
stats.outputPath = computeOutputPath(options, inputPath);
|
|
|
|
auto cloud = loadInputCloud(inputPath);
|
|
auto finiteCloud = buildFinitePointCloud(*cloud);
|
|
ensureEnoughFinitePoints(*finiteCloud, options.gp3NormalK);
|
|
ensureOutputDirectoryExists(stats.outputPath);
|
|
|
|
auto normalStart = std::chrono::steady_clock::now();
|
|
auto normals = estimateNormals(finiteCloud, options.gp3NormalK);
|
|
auto normalEnd = std::chrono::steady_clock::now();
|
|
|
|
auto pointNormals = buildPointNormalsCloud(*finiteCloud, *normals);
|
|
ensureGp3NormalsAreFinite(*pointNormals);
|
|
|
|
pcl::search::KdTree<pcl::PointNormal>::Ptr searchTree(
|
|
new pcl::search::KdTree<pcl::PointNormal>());
|
|
searchTree->setInputCloud(pointNormals);
|
|
|
|
pcl::GreedyProjectionTriangulation<pcl::PointNormal> gp3;
|
|
configureGp3(gp3, options, pointNormals, searchTree);
|
|
|
|
pcl::PolygonMesh mesh;
|
|
auto meshStart = std::chrono::steady_clock::now();
|
|
gp3.reconstruct(mesh);
|
|
auto meshEnd = std::chrono::steady_clock::now();
|
|
|
|
saveMeshOrThrow(stats.outputPath, mesh);
|
|
|
|
stats.pointCount = cloud->size();
|
|
stats.finitePointCount = finiteCloud->size();
|
|
stats.polygonCount = mesh.polygons.size();
|
|
stats.normalDurationMs = std::chrono::duration<double, std::milli>(
|
|
normalEnd - normalStart).count();
|
|
stats.meshDurationMs = std::chrono::duration<double, std::milli>(
|
|
meshEnd - meshStart).count();
|
|
stats.totalDurationMs = stats.normalDurationMs + stats.meshDurationMs;
|
|
stats.success = true;
|
|
return stats;
|
|
}
|
|
|
|
ConversionStats convertSingleFile(
|
|
const ToolOptions& options,
|
|
const std::filesystem::path& inputPath)
|
|
{
|
|
try
|
|
{
|
|
if (options.algorithm == MeshAlgorithm::Ofm)
|
|
{
|
|
return convertWithOfm(options, inputPath);
|
|
}
|
|
|
|
return convertWithGp3(options, inputPath);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
ConversionStats stats;
|
|
stats.algorithm = options.algorithm;
|
|
stats.inputPath = inputPath;
|
|
stats.outputPath = computeOutputPath(options, inputPath);
|
|
stats.errorMessage = e.what();
|
|
return stats;
|
|
}
|
|
}
|
|
|
|
void printOfmStats(const ConversionStats& stats)
|
|
{
|
|
std::cout << std::fixed << std::setprecision(3)
|
|
<< "OK"
|
|
<< " algorithm=ofm"
|
|
<< " input=" << stats.inputPath
|
|
<< " output=" << stats.outputPath
|
|
<< " points=" << stats.pointCount
|
|
<< " polygons=" << stats.polygonCount
|
|
<< " mesh_ms=" << stats.meshDurationMs
|
|
<< "\n";
|
|
}
|
|
|
|
void printGp3Stats(const ConversionStats& stats)
|
|
{
|
|
std::cout << std::fixed << std::setprecision(3)
|
|
<< "OK"
|
|
<< " algorithm=gp3"
|
|
<< " input=" << stats.inputPath
|
|
<< " output=" << stats.outputPath
|
|
<< " points=" << stats.pointCount
|
|
<< " finite_points=" << stats.finitePointCount
|
|
<< " polygons=" << stats.polygonCount
|
|
<< " normal_ms=" << stats.normalDurationMs
|
|
<< " mesh_ms=" << stats.meshDurationMs
|
|
<< " total_ms=" << stats.totalDurationMs
|
|
<< "\n";
|
|
}
|
|
|
|
void printPerFileStats(const ConversionStats& stats)
|
|
{
|
|
if (!stats.success)
|
|
{
|
|
std::cout << "FAIL"
|
|
<< " algorithm=" << algorithmToString(stats.algorithm)
|
|
<< " input=" << stats.inputPath
|
|
<< " error=\"" << stats.errorMessage << "\"\n";
|
|
return;
|
|
}
|
|
|
|
if (stats.algorithm == MeshAlgorithm::Ofm)
|
|
{
|
|
printOfmStats(stats);
|
|
return;
|
|
}
|
|
|
|
printGp3Stats(stats);
|
|
}
|
|
|
|
void printAggregateStats(const ToolOptions& options,
|
|
const std::vector<ConversionStats>& statsList)
|
|
{
|
|
std::size_t successCount = 0;
|
|
std::size_t failureCount = 0;
|
|
double totalNormalMs = 0.0;
|
|
double totalMeshMs = 0.0;
|
|
double totalConversionMs = 0.0;
|
|
double minTotalMs = std::numeric_limits<double>::max();
|
|
double maxTotalMs = 0.0;
|
|
|
|
for (const auto& stats : statsList)
|
|
{
|
|
if (!stats.success)
|
|
{
|
|
++failureCount;
|
|
continue;
|
|
}
|
|
|
|
++successCount;
|
|
totalNormalMs += stats.normalDurationMs;
|
|
totalMeshMs += stats.meshDurationMs;
|
|
totalConversionMs += stats.totalDurationMs;
|
|
minTotalMs = std::min(minTotalMs, stats.totalDurationMs);
|
|
maxTotalMs = std::max(maxTotalMs, stats.totalDurationMs);
|
|
}
|
|
|
|
double averageTotalMs = successCount == 0
|
|
? 0.0
|
|
: totalConversionMs / static_cast<double>(successCount);
|
|
if (successCount == 0)
|
|
{
|
|
minTotalMs = 0.0;
|
|
}
|
|
|
|
std::cout << std::fixed << std::setprecision(3)
|
|
<< "SUMMARY"
|
|
<< " algorithm=" << algorithmToString(options.algorithm)
|
|
<< " processed=" << statsList.size()
|
|
<< " succeeded=" << successCount
|
|
<< " failed=" << failureCount;
|
|
|
|
if (options.algorithm == MeshAlgorithm::Gp3)
|
|
{
|
|
std::cout
|
|
<< " total_normal_ms=" << totalNormalMs
|
|
<< " total_mesh_ms=" << totalMeshMs
|
|
<< " total_conversion_ms=" << totalConversionMs
|
|
<< " avg_total_ms=" << averageTotalMs
|
|
<< " min_total_ms=" << minTotalMs
|
|
<< " max_total_ms=" << maxTotalMs
|
|
<< "\n";
|
|
return;
|
|
}
|
|
|
|
std::cout
|
|
<< " total_mesh_ms=" << totalMeshMs
|
|
<< " avg_mesh_ms=" << averageTotalMs
|
|
<< " min_mesh_ms=" << minTotalMs
|
|
<< " max_mesh_ms=" << maxTotalMs
|
|
<< "\n";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
try
|
|
{
|
|
ToolOptions options = parseArgs(argc, argv);
|
|
std::vector<ConversionStats> statsList;
|
|
statsList.reserve(options.inputPaths.size());
|
|
|
|
for (const auto& inputPath : options.inputPaths)
|
|
{
|
|
ConversionStats stats = convertSingleFile(options, inputPath);
|
|
printPerFileStats(stats);
|
|
statsList.push_back(std::move(stats));
|
|
}
|
|
|
|
printAggregateStats(options, statsList);
|
|
return 0;
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
printUsage(argv[0]);
|
|
std::cerr << e.what() << std::endl;
|
|
return 1;
|
|
}
|
|
}
|