From ae460429b1e119df6f7e5245dd1c7e42ff1a72c2 Mon Sep 17 00:00:00 2001 From: Michal Pelka Date: Mon, 12 Oct 2026 17:47:17 +0200 Subject: [PATCH 1/2] Step 3: load a single LAZ/LAS file as a session Session::load now accepts a point cloud (*.laz, *.las) in addition to *.mjs/*.json and builds an in-memory session from it: one point cloud with identity pose and, unless a trajectory csv sits next to it, one identity trajectory node (multi-session code reads the first node). No session or poses files are written. Step 3 offers *.laz/*.las in "Add session(s)" and accepts them on drag & drop. Saving poses skips such sessions since they have no poses file. Co-Authored-By: Claude Opus 5.5 --- .../multi_session_registration.cpp | 24 +++++-- .../multi_session_registration_no_gui.cpp | 3 + core/include/Core/session.h | 5 ++ core/src/session.cpp | 67 +++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/apps/multi_session_registration/multi_session_registration.cpp b/apps/multi_session_registration/multi_session_registration.cpp index 6a2d1dbf..b66e0782 100644 --- a/apps/multi_session_registration/multi_session_registration.cpp +++ b/apps/multi_session_registration/multi_session_registration.cpp @@ -1365,6 +1365,12 @@ bool save_results(std::vector& sessions) { if (!session.is_ground_truth) { + // Sessions loaded from a single point cloud live in memory only and have no poses file + if (Session::is_point_cloud_file(session.session_file_name)) + { + std::cout << "not saving poses of point cloud session: " << session.session_file_name << std::endl; + continue; + } std::cout << "saving result to: " << session.point_clouds_container.poses_file_name << std::endl; if (!session.point_clouds_container.save_poses(fs::path(session.point_clouds_container.poses_file_name).string(), false)) failed += "'" + session.point_clouds_container.poses_file_name + "'\n"; @@ -1509,7 +1515,11 @@ void saveProject() void addSession() { - auto input_file_names = mandeye::fd::OpenFileDialog("Add session(s)", mandeye::fd::Session_filter, true); + // A single point cloud (*.laz, *.las) is loaded as an in-memory session, see Session::load_from_point_cloud + static const std::vector session_or_point_cloud_filter = { + "Session or point cloud (*.mjs, *.json, *.laz, *.las)", "*.mjs *.json *.laz *.las", "All files", "*" + }; + auto input_file_names = mandeye::fd::OpenFileDialog("Add session(s)", session_or_point_cloud_filter, true); if (input_file_names.size() > 0) { @@ -1542,8 +1552,8 @@ bool addSessionFile(const std::string& path) return true; } -// Drag & drop: a project (*.mjp) replaces the current one; session files (*.mjs, *.json) are added and loaded -// right away (appended to already loaded sessions, which keep their in-memory state). +// Drag & drop: a project (*.mjp) replaces the current one; session files (*.mjs, *.json) and single point clouds +// (*.laz, *.las) are added and loaded right away (appended to already loaded sessions, which keep their in-memory state). void loadDroppedFiles() { FilePathList dropped_files = LoadDroppedFiles(); @@ -1570,7 +1580,7 @@ void loadDroppedFiles() for (const auto& path : paths) { const std::string ext = ext_of(path); - if (ext != ".mjs" && ext != ".json") + if (ext != ".mjs" && ext != ".json" && !Session::is_point_cloud_file(path.string())) { std::cout << "Ignoring dropped file: '" << path.string() << "'" << std::endl; continue; @@ -1581,7 +1591,11 @@ void loadDroppedFiles() if (added.empty()) { - pfd::message("Unsupported file", "Drop a project (*.mjp) or session files (*.mjs, *.json).", pfd::choice::ok, pfd::icon::warning); + pfd::message( + "Unsupported file", + "Drop a project (*.mjp), session files (*.mjs, *.json) or point clouds (*.laz, *.las).", + pfd::choice::ok, + pfd::icon::warning); return; } diff --git a/apps/multi_session_registration/multi_session_registration_no_gui.cpp b/apps/multi_session_registration/multi_session_registration_no_gui.cpp index f4c4f4b0..08328baa 100644 --- a/apps/multi_session_registration/multi_session_registration_no_gui.cpp +++ b/apps/multi_session_registration/multi_session_registration_no_gui.cpp @@ -415,6 +415,9 @@ bool save_results(std::vector& sessions) { if (!session.is_ground_truth) { + // Sessions loaded from a single point cloud live in memory only and have no poses file + if (Session::is_point_cloud_file(session.session_file_name)) + continue; std::cout << "saving result to: " << session.point_clouds_container.poses_file_name << std::endl; session.point_clouds_container.save_poses(fs::path(session.point_clouds_container.poses_file_name).string(), false); } diff --git a/core/include/Core/session.h b/core/include/Core/session.h index 35e22c26..adab4954 100644 --- a/core/include/Core/session.h +++ b/core/include/Core/session.h @@ -30,7 +30,12 @@ class Session GroundControlPoints ground_control_points; ControlPoints control_points; + // file_name is a session (*.mjs, *.json) or a single point cloud (*.laz, *.las), see load_from_point_cloud bool load(const std::string& file_name, bool is_decimate, double bucket_x, double bucket_y, double bucket_z, bool calculate_offset); + // Builds a session in memory from one point cloud: identity pose, one identity trajectory node, no session files + bool load_from_point_cloud( + const std::string& file_name, bool is_decimate, double bucket_x, double bucket_y, double bucket_z, bool calculate_offset); + static bool is_point_cloud_file(const std::string& file_name); bool save( const std::string& file_name, const std::string& poses_file_name, const std::string& initial_poses_file_name, bool is_subsession); void fill_session_from_worker_data( diff --git a/core/src/session.cpp b/core/src/session.cpp index 1d55c85e..c0836724 100644 --- a/core/src/session.cpp +++ b/core/src/session.cpp @@ -8,8 +8,75 @@ namespace fs = std::filesystem; +bool Session::is_point_cloud_file(const std::string& file_name) +{ + std::string ext = fs::path(file_name).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + return ext == ".laz" || ext == ".las"; +} + +bool Session::load_from_point_cloud( + const std::string& file_name, bool is_decimate, double bucket_x, double bucket_y, double bucket_z, bool calculate_offset) +{ + this->session_file_name = file_name; + std::cout << "Loading point cloud as session: '" << file_name << "'" << std::endl; + + if (!fs::exists(file_name)) + { + std::cout << "point cloud file does not exist: '" << file_name << "'" << std::endl; + return false; + } + + if (!point_clouds_container.load_whu_tls( + { file_name }, is_decimate, bucket_x, bucket_y, bucket_z, calculate_offset, this->load_cache_mode) || + point_clouds_container.point_clouds.empty() || point_clouds_container.point_clouds[0].points_local.empty()) + { + std::cout << "can't load point cloud: '" << file_name << "'" << std::endl; + return false; + } + + working_directory = fs::path(file_name).parent_path().string(); + + // No poses files: the session exists only in memory + point_clouds_container.poses_file_name = ""; + point_clouds_container.initial_poses_file_name = ""; + + render_color[0] = float(rand() % 255) / 255.0; + render_color[1] = float(rand() % 255) / 255.0; + render_color[2] = float(rand() % 255) / 255.0; + + for (auto& pc : point_clouds_container.point_clouds) + { + pc.m_pose = Eigen::Affine3d::Identity(); + pc.m_initial_pose = pc.m_pose; + pc.m_pose_temp = pc.m_pose; + pc.pose = pose_tait_bryan_from_affine_matrix(pc.m_pose); + + // Multi-session code reads the first trajectory node (time offset, export), so a single scan gets one + if (pc.local_trajectory.empty()) + { + PointCloud::LocalTrajectoryNode node; + node.timestamps = { 0, 0 }; + node.m_pose = Eigen::Affine3d::Identity(); + node.imu_om_fi_ka = { 0, 0, 0 }; + node.imu_diff_angle_om_fi_ka_deg = { 0, 0, 0 }; + pc.local_trajectory.push_back(node); + } + + pc.render_color[0] = pc.traj_color[0] = render_color[0]; + pc.render_color[1] = pc.traj_color[1] = render_color[1]; + pc.render_color[2] = pc.traj_color[2] = render_color[2]; + } + + std::cout << "loaded " << point_clouds_container.point_clouds[0].points_local.size() << " points" << std::endl; + return true; +} + bool Session::load(const std::string& file_name, bool is_decimate, double bucket_x, double bucket_y, double bucket_z, bool calculate_offset) { + if (is_point_cloud_file(file_name)) + return load_from_point_cloud(file_name, is_decimate, bucket_x, bucket_y, bucket_z, calculate_offset); + this->session_file_name = file_name; std::cout << "Loading file: '" << file_name << "'" << std::endl; // point_clouds_container.point_clouds.clear(); From b8b4235415f685478826b9967e2388241e459c22 Mon Sep 17 00:00:00 2001 From: Michal Pelka Date: Mon, 12 Oct 2026 18:21:28 +0200 Subject: [PATCH 2/2] Save TUM Signed-off-by: Michal Pelka --- .../multi_session_registration.cpp | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/apps/multi_session_registration/multi_session_registration.cpp b/apps/multi_session_registration/multi_session_registration.cpp index b66e0782..6a57918d 100644 --- a/apps/multi_session_registration/multi_session_registration.cpp +++ b/apps/multi_session_registration/multi_session_registration.cpp @@ -1385,6 +1385,48 @@ bool save_results(std::vector& sessions) return true; } +// Writes the world poses of all visible scans (scan pose * local trajectory node) as TUM +// ("t[s] x y z qx qy qz qw"), sorted by time, with repeated timestamps dropped. +bool save_session_trajectory_as_tum(const Session& session, const std::string& file_name) +{ + std::vector> poses; + for (const auto& pc : session.point_clouds_container.point_clouds) + { + if (!pc.visible) + continue; + for (const auto& node : pc.local_trajectory) + poses.emplace_back(node.timestamps.first, pc.m_pose * node.m_pose); + } + std::stable_sort( + poses.begin(), + poses.end(), + [](const auto& a, const auto& b) + { + return a.first < b.first; + }); + + std::ofstream outfile(file_name); + if (!outfile.is_open()) + { + std::cerr << "Failed to create file: " << file_name << std::endl; + return false; + } + + size_t written = 0; + for (size_t i = 0; i < poses.size(); i++) + { + if (i > 0 && poses[i].first == poses[i - 1].first) + continue; + const Eigen::Vector3d pos = poses[i].second.translation(); + const Eigen::Quaterniond q(poses[i].second.rotation()); + outfile << std::fixed << std::setprecision(9) << poses[i].first / 1e9 << " " << std::setprecision(10) << pos.x() << " " << pos.y() + << " " << pos.z() << " " << q.x() << " " << q.y() << " " << q.z() << " " << q.w() << "\n"; + written++; + } + std::cout << "Saved " << written << " poses to TUM: " << file_name << std::endl; + return outfile.good(); +} + Eigen::Vector3d GLWidgetGetOGLPos(int x, int y, const ObservationPicking& observation_picking) { const auto laser_beam = GetLaserBeam(x, y); @@ -3080,6 +3122,31 @@ void display() if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Overwrite the poses file of every session except ground truth"); + if (ImGui::BeginMenu("Export trajectory as TUM", loaded_sessions && !sessions.empty())) + { + for (size_t i = 0; i < sessions.size(); i++) + { + const Session& session = sessions[i]; + // A point cloud session has no trajectory, only its scan pose + if (Session::is_point_cloud_file(session.session_file_name)) + continue; + + const fs::path session_path(session.session_file_name); + const std::string folder_name = session_path.parent_path().filename().string(); + const std::string label = folder_name + "/" + session_path.filename().string() + "##tum" + std::to_string(i); + if (ImGui::MenuItem(label.c_str())) + { + const std::string output_file_name = mandeye::fd::SaveFileDialog( + "Export trajectory as TUM", mandeye::fd::Tum_filter, ".txt", folder_name + "_trajectory_tum.txt"); + if (!output_file_name.empty() && !save_session_trajectory_as_tum(session, output_file_name)) + pfd::message("Export failed", "Could not write '" + output_file_name + "'", pfd::choice::ok, pfd::icon::error); + } + } + ImGui::EndMenu(); + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("Save the current poses of one session as a TUM trajectory (t[s] x y z qx qy qz qw)"); + ImGui::Separator(); if (ImGui::BeginMenu("Save all marked trajectories", sessions.size() > 0)) {