Add rudimentary pcloud dumper and meshing with OFM & GP3
The OFM algo runs in fractions of a millisecond. GP3 runs in fractions of a second. I think if we can get more input data to the OFM or something akin to it, we will have a winner.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
option(COMPILE_CL_CHECKS "Compile CL checks" OFF)
|
||||
option(COMPILE_PCL_TOOLS "Compile PCL-based validation tools" ON)
|
||||
|
||||
if(COMPILE_CL_CHECKS)
|
||||
# Find OpenCL: try find_package first, fall back to pkg-config
|
||||
@@ -55,3 +56,18 @@ if(COMPILE_CL_CHECKS)
|
||||
target_link_libraries(clzerocopycheck
|
||||
${OPENCL_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(COMPILE_PCL_TOOLS)
|
||||
enable_language(C)
|
||||
find_package(MPI REQUIRED COMPONENTS C)
|
||||
find_package(PCL QUIET COMPONENTS common io surface features search kdtree)
|
||||
if(PCL_FOUND)
|
||||
add_executable(meshFromPcd meshFromPcd.cpp)
|
||||
target_include_directories(meshFromPcd PUBLIC ${PCL_INCLUDE_DIRS})
|
||||
target_link_directories(meshFromPcd PUBLIC ${PCL_LIBRARY_DIRS})
|
||||
target_link_libraries(meshFromPcd ${PCL_LIBRARIES})
|
||||
target_compile_options(meshFromPcd PRIVATE ${PCL_DEFINITIONS})
|
||||
else()
|
||||
message(WARNING "PCL not found; skipping meshFromPcd build")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,700 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ if(ENABLE_STIMBUFFAPI_livoxGen1)
|
||||
add_library(livoxGen1 SHARED
|
||||
livoxGen1.cpp
|
||||
pcloudStimulusProducer.cpp
|
||||
livoxPcloudFrameDumper.cpp
|
||||
ioUringAssemblyEngine.cpp
|
||||
openClCollatingAndMeshingEngine.cpp
|
||||
openClKernels.cl.S
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "livoxPcloudFrameDumper.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace smo {
|
||||
namespace stim_buff {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kDumpDirParamName = "pcloud-dump-dir";
|
||||
constexpr const char* kPcdExtension = ".pcd";
|
||||
constexpr std::size_t kXyzComponentsPerPoint = 3;
|
||||
|
||||
std::string makePcdHeader(std::size_t width, std::size_t height)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "# .PCD v0.7 - Point Cloud Data file format\n";
|
||||
oss << "VERSION 0.7\n";
|
||||
oss << "FIELDS x y z\n";
|
||||
oss << "SIZE 4 4 4\n";
|
||||
oss << "TYPE F F F\n";
|
||||
oss << "COUNT 1 1 1\n";
|
||||
oss << "WIDTH " << width << "\n";
|
||||
oss << "HEIGHT " << height << "\n";
|
||||
oss << "VIEWPOINT 0 0 0 1 0 0 0\n";
|
||||
oss << "POINTS " << (width * height) << "\n";
|
||||
oss << "DATA binary\n";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
float sanitizeCoordinate(float coordinate)
|
||||
{
|
||||
if (coordinate == 0.0f)
|
||||
{
|
||||
return std::numeric_limits<float>::quiet_NaN();
|
||||
}
|
||||
|
||||
return coordinate;
|
||||
}
|
||||
|
||||
bool pointIsZero(float x, float y, float z)
|
||||
{
|
||||
return x == 0.0f && y == 0.0f && z == 0.0f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LivoxPcloudFrameDumper::LivoxPcloudFrameDumper(
|
||||
const std::shared_ptr<device::DeviceAttachmentSpec>& deviceAttachmentSpec)
|
||||
: enabled(false),
|
||||
deviceSelector(deviceAttachmentSpec ? deviceAttachmentSpec->deviceSelector : ""),
|
||||
dumpDirectory()
|
||||
{
|
||||
if (!deviceAttachmentSpec)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string(__func__) + ": deviceAttachmentSpec is null");
|
||||
}
|
||||
|
||||
std::string dumpDirParam = parseDumpDirParam(
|
||||
deviceAttachmentSpec->stimBuffApiParams);
|
||||
if (dumpDirParam.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
enabled = true;
|
||||
dumpDirectory = resolveDumpDirectory(dumpDirParam);
|
||||
}
|
||||
|
||||
void LivoxPcloudFrameDumper::prepareForRun() const
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(dumpDirectory);
|
||||
}
|
||||
|
||||
void LivoxPcloudFrameDumper::dumpProducedFrame(
|
||||
const livoxProto1::Device& device,
|
||||
const StagingBuffer& collationBuffer,
|
||||
const sscl::AsynchronousLoop& frameAssemblyResult) const
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t nSucceeded = frameAssemblyResult.nSucceeded.load();
|
||||
if (nSucceeded == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::size_t pointsPerDgram = livoxProto1::Device::getNPointsPerDgram(
|
||||
static_cast<int>(device.currentReturnMode));
|
||||
if (pointsPerDgram == 0)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string(__func__) + ": pointsPerDgram resolved to 0");
|
||||
}
|
||||
|
||||
std::filesystem::path outputPath = makeUniqueFramePath(
|
||||
dumpDirectory, deviceSelector);
|
||||
writeBinaryPcdFile(
|
||||
outputPath, collationBuffer, nSucceeded, pointsPerDgram);
|
||||
}
|
||||
|
||||
std::string LivoxPcloudFrameDumper::parseDumpDirParam(
|
||||
const std::vector<std::pair<std::string, std::string>>& params)
|
||||
{
|
||||
for (auto it = params.rbegin(); it != params.rend(); ++it)
|
||||
{
|
||||
if (it->first == kDumpDirParamName)
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::filesystem::path LivoxPcloudFrameDumper::resolveDumpDirectory(
|
||||
const std::string& dumpDirParam)
|
||||
{
|
||||
std::filesystem::path dumpDirPath(dumpDirParam);
|
||||
if (dumpDirPath.is_absolute())
|
||||
{
|
||||
return dumpDirPath;
|
||||
}
|
||||
|
||||
return std::filesystem::current_path() / dumpDirPath;
|
||||
}
|
||||
|
||||
std::string LivoxPcloudFrameDumper::makeTimestampStem(
|
||||
const std::string& deviceSelector)
|
||||
{
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm nowTm = *std::localtime(&nowTime);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << deviceSelector << "-"
|
||||
<< std::put_time(&nowTm, "%Y-%m-%d--%H:%M:%S");
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::filesystem::path LivoxPcloudFrameDumper::makeUniqueFramePath(
|
||||
const std::filesystem::path& dumpDir,
|
||||
const std::string& deviceSelector)
|
||||
{
|
||||
std::string stem = makeTimestampStem(deviceSelector);
|
||||
std::filesystem::path candidate = dumpDir / (stem + kPcdExtension);
|
||||
if (!std::filesystem::exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
for (std::size_t suffix = 1; ; ++suffix)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << stem << "--" << std::setw(3) << std::setfill('0') << suffix
|
||||
<< kPcdExtension;
|
||||
candidate = dumpDir / oss.str();
|
||||
if (!std::filesystem::exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LivoxPcloudFrameDumper::writeBinaryPcdFile(
|
||||
const std::filesystem::path& outputPath,
|
||||
const StagingBuffer& collationBuffer,
|
||||
std::size_t nSucceeded,
|
||||
std::size_t pointsPerDgram)
|
||||
{
|
||||
std::ofstream output(outputPath, std::ios::binary);
|
||||
if (!output)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string(__func__) + ": failed to open " + outputPath.string());
|
||||
}
|
||||
|
||||
output << makePcdHeader(pointsPerDgram, nSucceeded);
|
||||
|
||||
struct iovec collationIov = collationBuffer.getClEngineIovec();
|
||||
const auto* bufferBase = static_cast<const std::uint8_t*>(collationIov.iov_base);
|
||||
const std::size_t rowNBytes =
|
||||
pointsPerDgram * kXyzComponentsPerPoint * sizeof(float);
|
||||
const std::size_t slotStrideNBytes = collationBuffer.slotStrideNBytes;
|
||||
|
||||
for (std::size_t rowIndex = 0; rowIndex < nSucceeded; ++rowIndex)
|
||||
{
|
||||
const auto* rowBase = reinterpret_cast<const float*>(
|
||||
bufferBase + (rowIndex * slotStrideNBytes));
|
||||
|
||||
for (std::size_t pointIndex = 0; pointIndex < pointsPerDgram; ++pointIndex)
|
||||
{
|
||||
const std::size_t pointOffset = pointIndex * kXyzComponentsPerPoint;
|
||||
float x = rowBase[pointOffset + 0];
|
||||
float y = rowBase[pointOffset + 1];
|
||||
float z = rowBase[pointOffset + 2];
|
||||
|
||||
if (pointIsZero(x, y, z))
|
||||
{
|
||||
x = sanitizeCoordinate(x);
|
||||
y = sanitizeCoordinate(y);
|
||||
z = sanitizeCoordinate(z);
|
||||
}
|
||||
|
||||
output.write(reinterpret_cast<const char*>(&x), sizeof(float));
|
||||
output.write(reinterpret_cast<const char*>(&y), sizeof(float));
|
||||
output.write(reinterpret_cast<const char*>(&z), sizeof(float));
|
||||
}
|
||||
|
||||
(void)rowNBytes;
|
||||
}
|
||||
|
||||
if (!output)
|
||||
{
|
||||
throw std::runtime_error(
|
||||
std::string(__func__) + ": failed while writing "
|
||||
+ outputPath.string());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace stim_buff
|
||||
} // namespace smo
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef _LIVOX_GEN1_PCLOUD_FRAME_DUMPER_H
|
||||
#define _LIVOX_GEN1_PCLOUD_FRAME_DUMPER_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <livoxProto1/device.h>
|
||||
#include <spinscale/asynchronousLoop.h>
|
||||
#include <user/deviceAttachmentSpec.h>
|
||||
#include <user/stagingBuffer.h>
|
||||
|
||||
namespace smo {
|
||||
namespace stim_buff {
|
||||
|
||||
class LivoxPcloudFrameDumper
|
||||
{
|
||||
public:
|
||||
explicit LivoxPcloudFrameDumper(
|
||||
const std::shared_ptr<device::DeviceAttachmentSpec>& deviceAttachmentSpec);
|
||||
~LivoxPcloudFrameDumper() = default;
|
||||
|
||||
LivoxPcloudFrameDumper(const LivoxPcloudFrameDumper&) = delete;
|
||||
LivoxPcloudFrameDumper& operator=(const LivoxPcloudFrameDumper&) = delete;
|
||||
LivoxPcloudFrameDumper(LivoxPcloudFrameDumper&&) = delete;
|
||||
LivoxPcloudFrameDumper& operator=(LivoxPcloudFrameDumper&&) = delete;
|
||||
|
||||
bool isEnabled() const { return enabled; }
|
||||
void prepareForRun() const;
|
||||
void dumpProducedFrame(
|
||||
const livoxProto1::Device& device,
|
||||
const StagingBuffer& collationBuffer,
|
||||
const sscl::AsynchronousLoop& frameAssemblyResult) const;
|
||||
|
||||
private:
|
||||
static std::string parseDumpDirParam(
|
||||
const std::vector<std::pair<std::string, std::string>>& params);
|
||||
static std::filesystem::path resolveDumpDirectory(
|
||||
const std::string& dumpDirParam);
|
||||
static std::string makeTimestampStem(const std::string& deviceSelector);
|
||||
static std::filesystem::path makeUniqueFramePath(
|
||||
const std::filesystem::path& dumpDir,
|
||||
const std::string& deviceSelector);
|
||||
static void writeBinaryPcdFile(
|
||||
const std::filesystem::path& outputPath,
|
||||
const StagingBuffer& collationBuffer,
|
||||
std::size_t nSucceeded,
|
||||
std::size_t pointsPerDgram);
|
||||
|
||||
private:
|
||||
bool enabled;
|
||||
std::string deviceSelector;
|
||||
std::filesystem::path dumpDirectory;
|
||||
};
|
||||
|
||||
} // namespace stim_buff
|
||||
} // namespace smo
|
||||
|
||||
#endif // _LIVOX_GEN1_PCLOUD_FRAME_DUMPER_H
|
||||
@@ -102,6 +102,7 @@ averageIntensityBuffer(
|
||||
nDgramsPerStagingBufferFrame),
|
||||
averageIntensityBufferMlockPinner(
|
||||
averageIntensityBuffer.makeMlockPinner()),
|
||||
pcloudFrameDumper(deviceAttachmentSpec),
|
||||
tempStimulusFrameMem(0),
|
||||
tempStimulusFrame(
|
||||
FrameAssemblyDesc::SlotDesc{
|
||||
@@ -154,6 +155,8 @@ void PcloudStimulusProducer::start()
|
||||
std::cout << __func__ << ": Starting PcloudStimulusProducer for device "
|
||||
<< device->discoveredDevice.deviceIdentifier << std::endl;
|
||||
|
||||
pcloudFrameDumper.prepareForRun();
|
||||
|
||||
// Call ioUringAssemblyEngine setup() as the first step
|
||||
if (!ioUringAssemblyEngine.setup())
|
||||
{
|
||||
@@ -513,6 +516,23 @@ public:
|
||||
std::cerr << __func__ << ": Failed to compact and collate frame" << std::endl;
|
||||
} else
|
||||
{
|
||||
lock.unlockPrematurely();
|
||||
if (pcloudProducer.pcloudFrameDumper.isEnabled())
|
||||
{
|
||||
try
|
||||
{
|
||||
pcloudProducer.pcloudFrameDumper.dumpProducedFrame(
|
||||
*pcloudProducer.device,
|
||||
pcloudProducer.collationBuffer,
|
||||
context->frameAssemblyResult);
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
std::cerr << __func__ << ": Failed to dump pcloud frame: "
|
||||
<< e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
// Print execution durations
|
||||
auto assemblyDuration = pcloudProducer.ioUringAssemblyEngine.getAssemblyDuration();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <spinscale/callback.h>
|
||||
#include <user/stagingBuffer.h>
|
||||
#include "ioUringAssemblyEngine.h"
|
||||
#include "livoxPcloudFrameDumper.h"
|
||||
#include "openClCollatingAndMeshingEngine.h"
|
||||
#include "meshStimulusBuffer.h"
|
||||
#include "pcloudIntensityStimulusBuffer.h"
|
||||
@@ -98,6 +99,7 @@ public:
|
||||
StagingBuffer averageIntensityBuffer;
|
||||
std::unique_ptr<StagingBuffer::MlockPinner>
|
||||
averageIntensityBufferMlockPinner;
|
||||
LivoxPcloudFrameDumper pcloudFrameDumper;
|
||||
size_t tempStimulusFrameMem;
|
||||
StimulusFrame tempStimulusFrame;
|
||||
std::atomic<std::shared_ptr<MeshStimulusBuffer>> meshStimulusBuffer;
|
||||
|
||||
Reference in New Issue
Block a user