using CartographerSharp.IO; using CartographerSharp.Transform; using RobotNet10.Shared.Geometry; using RobotNet10.Shared.Numbers; namespace RobotNet10.RobotApp.SLAM.Cartographer.Helpers; /// /// Helper class for transforming pbstream files. /// Centralizes the logic for updating TransformToMap in PoseGraph proto. /// public static class PbstreamTransformHelper { /// /// Transform pbstream file by updating TransformToMap in PoseGraph proto. /// This applies a coordinate transform to shift the map origin. /// /// Path to the pbstream file /// New origin pose to apply /// Optional logger for logging /// Thrown when pbstream read/write fails /// Thrown when newOrigin contains invalid values (NaN/Infinity) public static void TransformPbstreamOrigin(string pbstreamPath, Pose newOrigin, ILogger? logger = null) { // Validate newOrigin for NaN/Infinity ValidatePose(newOrigin); var tempPbstreamPath = pbstreamPath + ".tmp"; try { using (var reader = new ProtoStreamReader(pbstreamPath)) using (var writer = new ProtoStreamWriter(tempPbstreamPath)) { // Read and write header if (!reader.ReadProto(out var headerData)) { throw new InvalidOperationException("Failed to read serialization header"); } if (!headerData.SerializationHeader.HasValue) { throw new InvalidOperationException("Invalid serialization header"); } writer.WriteProto(headerData); // Read PoseGraph, update TransformToMap, and write if (!reader.ReadProto(out var poseGraphData)) { throw new InvalidOperationException("Failed to read pose graph"); } if (!poseGraphData.PoseGraph.HasValue) { throw new InvalidOperationException("Invalid pose graph data"); } var poseGraphProto = poseGraphData.PoseGraph.Value; // Convert Pose to Rigid3d and compose with current transform // Key logic from xloc.cc ChangeMapOrigin: // SetTransformToMap(GetTransformToMap() * Rigid3d(position, orientation)); // // In Cartographer: // - TransformToMap: converts map frame -> internal frame // - TransformToMapInverse: converts internal frame -> map frame (used in OccupancyGridGenerator) // // We want: newOrigin.Position (T) in old map becomes (0,0) in new map // Formula: newMapPoint = R^-1 * (oldMapPoint - T) where T = newOrigin.Position, R = newOrigin.Orientation var newOriginRigid = new Rigid3d( new Vector3(newOrigin.Position.X, newOrigin.Position.Y, newOrigin.Position.Z), new Quaternion(newOrigin.Orientation.X, newOrigin.Orientation.Y, newOrigin.Orientation.Z, newOrigin.Orientation.W)); var currentTransform = poseGraphProto.TransformToMap.HasValue ? (Rigid3d)poseGraphProto.TransformToMap.Value : Rigid3d.Identity; var newTransform = currentTransform * newOriginRigid; // Update TransformToMap in the proto poseGraphProto.TransformToMap = (CartographerSharp.Models.Transform.Rigid3dProto)newTransform; // Write updated pose graph var updatedPoseGraphData = new CartographerSharp.Models.Mapping.SerializedData { PoseGraph = poseGraphProto }; writer.WriteProto(updatedPoseGraphData); // Copy all remaining data unchanged (AllTrajectoryBuilderOptions, submaps, nodes, etc.) while (!reader.Eof) { if (!reader.ReadProto(out var data)) { break; } writer.WriteProto(data); } } // Replace original file with updated file File.Move(tempPbstreamPath, pbstreamPath, overwrite: true); logger?.LogInformation("PbstreamTransformHelper: Transformed pbstream origin successfully"); } catch (Exception) { // Clean up temp file on error if (File.Exists(tempPbstreamPath)) { try { File.Delete(tempPbstreamPath); } catch { } } throw; } } /// /// Check if pose is identity (no transform needed) /// public static bool IsIdentityPose(Pose pose) { const double tolerance = 1e-6; // Check position is (0, 0, 0) var posIsZero = Math.Abs(pose.Position.X) < tolerance && Math.Abs(pose.Position.Y) < tolerance && Math.Abs(pose.Position.Z) < tolerance; // Check orientation is identity quaternion (0, 0, 0, 1) var quatIsIdentity = Math.Abs(pose.Orientation.X) < tolerance && Math.Abs(pose.Orientation.Y) < tolerance && Math.Abs(pose.Orientation.Z) < tolerance && Math.Abs(pose.Orientation.W - 1.0) < tolerance; return posIsZero && quatIsIdentity; } /// /// Validate pose for NaN/Infinity values /// /// Thrown when pose contains invalid values public static void ValidatePose(Pose pose) { // Check position if (double.IsNaN(pose.Position.X) || double.IsInfinity(pose.Position.X) || double.IsNaN(pose.Position.Y) || double.IsInfinity(pose.Position.Y) || double.IsNaN(pose.Position.Z) || double.IsInfinity(pose.Position.Z)) { throw new ArgumentException($"Pose position contains invalid values (NaN/Infinity): ({pose.Position.X}, {pose.Position.Y}, {pose.Position.Z})", nameof(pose)); } // Check orientation if (double.IsNaN(pose.Orientation.X) || double.IsInfinity(pose.Orientation.X) || double.IsNaN(pose.Orientation.Y) || double.IsInfinity(pose.Orientation.Y) || double.IsNaN(pose.Orientation.Z) || double.IsInfinity(pose.Orientation.Z) || double.IsNaN(pose.Orientation.W) || double.IsInfinity(pose.Orientation.W)) { throw new ArgumentException($"Pose orientation contains invalid values (NaN/Infinity): ({pose.Orientation.X}, {pose.Orientation.Y}, {pose.Orientation.Z}, {pose.Orientation.W})", nameof(pose)); } } /// /// Convert Pose to Rigid3d /// public static Rigid3d PoseToRigid3d(Pose pose) { var translation = new Vector3(pose.Position.X, pose.Position.Y, pose.Position.Z); var rotation = new Quaternion(pose.Orientation.X, pose.Orientation.Y, pose.Orientation.Z, pose.Orientation.W); return new Rigid3d(translation, rotation); } }