diff --git a/apps/multi_view_tls_registration/multi_view_tls_registration.cpp b/apps/multi_view_tls_registration/multi_view_tls_registration.cpp index 941ca1be..a72a70b9 100644 --- a/apps/multi_view_tls_registration/multi_view_tls_registration.cpp +++ b/apps/multi_view_tls_registration/multi_view_tls_registration.cpp @@ -153,14 +153,14 @@ void save_trajectories_to_laz( } } -void createDXFPolyline(const std::string& filename, const std::vector& points) +bool createDXFPolyline(const std::string& filename, const std::vector& points) { std::ofstream dxfFile(filename); dxfFile << std::setprecision(20); if (!dxfFile.is_open()) { std::cerr << "Failed to open file: " << filename << std::endl; - return; + return false; } // DXF header @@ -196,10 +196,16 @@ void createDXFPolyline(const std::string& filename, const std::vector& points); +bool createDXFPolyline(const std::string& filename, const std::vector& points); // void load_available_geo_points(Session& session, std::string input_file_name); -void save_trajectories( +bool save_trajectories( Session& session, std::string output_file_name, float curve_consecutive_distance_meters, diff --git a/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp b/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp index 47092af1..7e6b5fb9 100644 --- a/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp +++ b/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp @@ -6548,22 +6548,150 @@ bool initGL(int* argc, char** argv, const std::string& winTitleArg, void (*)(), return true; } +int exportSessionToDirectory(const fs::path& input_file, const fs::path& output_directory, bool same_as_gui) +{ + const fs::path laz_file = output_directory / "all_step_2.laz"; + const fs::path trajectory_file = output_directory / "trajectories.csv"; + if (!fs::is_regular_file(input_file)) + { + spdlog::error("Session file does not exist or is not a regular file: '{}'", input_file.string()); + return 1; + } + if (fs::exists(laz_file) || fs::exists(trajectory_file)) + { + spdlog::error("Export files already exist in '{}'; refusing to overwrite them", output_directory.string()); + return 1; + } + + Session export_session; + if (!export_session.load( + input_file.string(), + same_as_gui && tls_registration.is_decimate, + tls_registration.bucket_x, + tls_registration.bucket_y, + tls_registration.bucket_z, + same_as_gui && tls_registration.calculate_offset)) + { + spdlog::error("Failed loading session: '{}'", input_file.string()); + return 1; + } + if (export_session.point_clouds_container.point_clouds.empty()) + { + spdlog::error("Session contains no point clouds: '{}'", input_file.string()); + return 1; + } + bool missing_unix_timestamps = false; + for (const auto& pc : export_session.point_clouds_container.point_clouds) + { + if ((!same_as_gui && pc.points_local.empty()) || pc.local_trajectory.empty()) + { + spdlog::error("Point cloud has no points or trajectory: '{}'", pc.file_name); + return 1; + } + missing_unix_timestamps |= std::any_of( + pc.local_trajectory.begin(), + pc.local_trajectory.end(), + [](const auto& node) + { + return node.timestamps.second == 0.0; + }); + } + if (missing_unix_timestamps) + spdlog::warn("Some Unix trajectory timestamps are missing; their exported values remain zero"); + + fs::create_directories(output_directory); + if (!save_all_to_las(export_session, laz_file.string(), false, true)) + { + spdlog::error("Failed exporting global LAZ: '{}'", laz_file.string()); + return 1; + } + if (!save_trajectories( + export_session, + trajectory_file.string(), + tls_registration.curve_consecutive_distance_meters, + tls_registration.not_curve_consecutive_distance_meters, + same_as_gui && tls_registration.is_trajectory_export_downsampling, + true, + true, + true, + false)) + { + spdlog::error("Failed exporting trajectory: '{}'", trajectory_file.string()); + return 1; + } + spdlog::info("Exported global LAZ to '{}' and trajectory to '{}'", laz_file.string(), trajectory_file.string()); + return 0; +} + int main(int argc, char* argv[]) { + bool export_requested = false; try { if (checkClHelp(argc, argv)) { std::cout << winTitle << "\n\n" << "USAGE:\n" - << std::filesystem::path(argv[0]).stem().string() << " /?\n\n" + << std::filesystem::path(argv[0]).stem().string() << " [--export [--same-as-gui]] /?\n\n" << "where\n" - << " Path to Mandeye JSON Session file (*.mjs)\n" + << " Path to Mandeye JSON Session file (*.mjs, *.json)\n" + << " --export Export global LAZ and quaternion CSV with Lidar/Unix timestamps, then exit;\n" + << " requires *.mjs or *.json, creates directory, refuses existing export files\n" + << " --same-as-gui Use GUI default loading/export settings (requires --export)\n" << " -h, /h, --help, /? Show this help and exit\n\n"; return 0; } + // Parse optional headless export without changing normal GUI startup. + fs::path export_directory; + bool same_as_gui = false; + std::vector input_arguments; + for (int i = 1; i < argc; ++i) + { + if (std::string(argv[i]) == "--export") + { + if (export_requested || i + 1 == argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).rfind("--", 0) == 0) + { + spdlog::error("--export requires exactly one directory argument"); + return 1; + } + export_requested = true; + export_directory = argv[++i]; + } + else if (std::string(argv[i]) == "--same-as-gui") + { + if (same_as_gui) + { + spdlog::error("--same-as-gui may only be specified once"); + return 1; + } + same_as_gui = true; + } + else + input_arguments.emplace_back(argv[i]); + } + if (same_as_gui && !export_requested) + { + spdlog::error("--same-as-gui requires --export"); + return 1; + } + if (export_requested) + { + if (input_arguments.size() != 1) + { + spdlog::error("--export requires exactly one session input"); + return 1; + } + const std::string ext = lowerExtension(input_arguments.front()); + if (ext != ".mjs" && ext != ".json") + { + spdlog::error("Unsupported session extension '{}'; expected .mjs or .json", ext); + return 1; + } + return exportSessionToDirectory(input_arguments.front(), export_directory, same_as_gui); + } + // search for available geoid models in the system and populate the menu geoids = GNSS::get_available_geoids(); @@ -6642,14 +6770,15 @@ int main(int argc, char* argv[]) } catch (const std::bad_alloc& e) { spdlog::error("System is out of memory : {}", e.what()); - mandeye::fd::OutOfMemMessage(); + if (!export_requested) + mandeye::fd::OutOfMemMessage(); } catch (const std::exception& e) { - spdlog::error(e.what()); + spdlog::error("{}", e.what()); } catch (...) { spdlog::error("Unknown fatal error occurred!"); } - return 0; + return export_requested ? 1 : 0; } \ No newline at end of file diff --git a/core/include/Core/export_laz.h b/core/include/Core/export_laz.h index b2545507..95084dc0 100644 --- a/core/include/Core/export_laz.h +++ b/core/include/Core/export_laz.h @@ -457,7 +457,7 @@ inline void save_intersection( } } -inline void save_all_to_las(const Session& session, std::string output_las_name, bool as_local, bool skip_ts_0) +inline bool save_all_to_las(const Session& session, std::string output_las_name, bool as_local, bool skip_ts_0) { Eigen::Affine3d first_pose = Eigen::Affine3d::Identity(); bool found_first_pose = false; @@ -474,7 +474,7 @@ inline void save_all_to_las(const Session& session, std::string output_las_name, if (!writer.open(output_las_name, offset.x(), offset.y(), offset.z())) { std::cout << "problem with saving file: " << output_las_name << std::endl; - return; + return false; } for (size_t scan_idx = 0; scan_idx < clouds.size(); ++scan_idx) @@ -506,10 +506,12 @@ inline void save_all_to_las(const Session& session, std::string output_las_name, double ts = (i < p.timestamps.size()) ? p.timestamps[i] : 0.0; if (!writer.writePoint(vp, inten, ts, offset.x(), offset.y(), offset.z(), psid)) - return; + { + return false; + } } } } - writer.close(); + return writer.close(); } diff --git a/core/src/point_clouds.cpp b/core/src/point_clouds.cpp index 33ca9853..9acb543a 100644 --- a/core/src/point_clouds.cpp +++ b/core/src/point_clouds.cpp @@ -1173,6 +1173,7 @@ bool PointClouds::load_whu_tls( if (!infile.good()) { // std::cout << "problem with file: '" << trj_path.string() << "'" << std::endl; + spdlog::error("Failed opening trajectory: '{}'", trj_path.string()); return false; } @@ -1268,6 +1269,7 @@ bool PointClouds::load_whu_tls( //// load actual pointclouds point_clouds.resize(point_clouds_nodata.size()); + std::vector load_success(point_clouds_nodata.size(), 1); std::transform( #if USE_EXECUTION_PAR_UNSEQ @@ -1292,11 +1294,21 @@ bool PointClouds::load_whu_tls( // << sum_points_after_decimation << std::endl; } } + else + { + load_success[&pc - point_clouds_nodata.data()] = 0; + } } return pc; }); + if (std::find(load_success.begin(), load_success.end(), 0) != load_success.end()) + { + spdlog::error("Failed loading one or more point clouds"); + return false; + } + // calculate average position of a subset of points from all clouds to center the point clouds around the origin if (calculate_offset) { diff --git a/core/src/session.cpp b/core/src/session.cpp index d903a4fa..524dfc2a 100644 --- a/core/src/session.cpp +++ b/core/src/session.cpp @@ -196,8 +196,12 @@ bool Session::load(const std::string& file_name, bool is_decimate, double bucket std::cout << "-----------------------------" << std::endl; // loading all data - point_clouds_container.load_whu_tls( - laz_file_names, is_decimate, bucket_x, bucket_y, bucket_z, calculate_offset, this->load_cache_mode); + if (!point_clouds_container.load_whu_tls( + laz_file_names, is_decimate, bucket_x, bucket_y, bucket_z, calculate_offset, this->load_cache_mode)) + { + std::cerr << "Failed loading session point clouds: " << file_name << std::endl; + return false; + } if (laz_file_names.size() > 0) {