# Map 模块说明 `Map` 为粗路径规划提供只读、可复用的规划地图快照。它只负责把外部障碍物投影并栅格化,生成占据图和保守障碍距离场;不读取传感器、定位、UI 或系统时钟,也不写入 AMR 自身占据和安全外扩。 规划器应长期持有一个 `PlanningMapFactory`,通过一次 `Create` 调用取得 `PlanningGridMap`。 ## 文件结构 ```text Map/ ├── README.md # 本模块说明:结构、数据流、单位和调用方式 ├── PlanningMapRequest.cs # 公开建图输入:边界、分辨率、障碍来源、空图策略 ├── PlanningMapBuildResult.cs # 公开建图输出:状态、地图、来源结果、失败原因、缓存命中类型 ├── PlanningMapFactory.cs # 唯一公开建图门面;负责两级缓存和快照编号 ├── Core/ │ ├── MapBoundsMm.cs # 世界地图范围及行列尺寸计算 │ ├── MapBuildRequest.cs # EnvironmentMapBuilder 的内部建图输入 │ ├── EnvironmentMapBuildResult.cs # 环境栅格构建结果 │ ├── EnvironmentGridMap.cs # 构建期可写的环境占据栅格 │ └── EnvironmentMapBuilder.cs # 汇总障碍来源并事务性创建环境图 ├── Obstacles/ │ ├── IMapObstacle.cs # 世界坐标障碍物几何契约 │ ├── CircleObstacle.cs # 圆形障碍物几何 │ ├── AxisAlignedRectangleObstacle.cs # 与坐标轴平行的矩形障碍物几何 │ └── MapObstacleRasterizer.cs # 唯一允许写入环境栅格的障碍物栅格化器 ├── Sources/ │ ├── IMapObstacleSource.cs # 统一障碍来源接口 │ ├── ObstacleSourceStatus.cs # 来源投影状态:已应用、空、不可用、无效 │ ├── ObstacleProjectionResult.cs # 单个来源的世界几何和诊断结果 │ ├── ManualObstacleSource.cs # 手工输入的圆形/矩形障碍来源 │ ├── TwoLegProjectionInput.cs # 检测时刻的 TwoLeg 纯数据快照 │ ├── TwoLegObstacleProjector.cs # 将 TwoLeg 局部坐标投影为世界坐标圆障碍物 │ └── TwoLegObstacleSource.cs # 将 TwoLeg 快照包装为统一障碍来源 ├── Planning/ │ ├── PlanningGridMap.cs # 不可变规划快照;规划查询使用米 │ ├── PlanningMapAdapter.cs # 环境图到规划快照与距离场的适配器 │ ├── EuclideanDistanceTransform.cs # 二值栅格的精确平方欧氏距离变换 │ ├── ObstacleDistanceField.cs # 对规划器暴露的保守障碍净距 │ └── PlanningMapCache.cs # 容量为 4 的输入/占据两级 LRU 缓存 └── Test/ ├── MovementTest.MapTest.cs # Clumsy 手工建图测试入口与终端调试开关 └── Visualization/ ├── PlanningMapImageExportRequest.cs # PNG 导出输入:规划快照和输出目录 ├── PlanningMapImageExportResult.cs # PNG 导出状态、路径、尺寸和诊断 ├── PlanningMapImageExporter.cs # 可选 PNG 导出门面和输出保护 ├── PlanningMapImageRenderer.cs # 只读快照到 RGBA 像素的渲染器 └── ValidatedPngWriter.cs # 写入并校验 PNG 结构和 CRC ``` ## 建图数据流 ```text PlanningMapRequest │ ▼ IMapObstacleSource.ProjectToWorld() │ 输出世界坐标的圆形或矩形几何 ▼ EnvironmentMapBuilder + MapObstacleRasterizer │ 写入构建期 EnvironmentGridMap ▼ PlanningMapAdapter + ObstacleDistanceField │ 生成占据数组和保守距离数组 ▼ PlanningGridMap ``` 具体规则: 1. 调用者准备 `PlanningMapRequest` 和一个或多个 `IMapObstacleSource`。 2. 每个来源通过 `ProjectToWorld()` 输出世界坐标几何;Map 核心不主动读取 TwoLeg、定位或其他传感器。 3. `EnvironmentMapBuilder` 按来源 ID 排序,必需来源失败则整个建图失败;可选来源失败只保留诊断状态。 4. `MapObstacleRasterizer` 是唯一写入 `EnvironmentGridMap` 占据格的组件。 5. `PlanningMapAdapter` 生成不可变 `PlanningGridMap`,并附带保守障碍距离场。 6. `PlanningMapFactory` 返回最终快照及来源投影结果;粗路径规划只应消费这个快照。 ## 构建状态与停止 `PlanningMapBuildResult.Status` 的类型为 `PlanningMapBuildStatus`: - `Success`:成功发布不可变 `PlanningGridMap`; - `Failed`:输入、来源或常规建图失败,读取 `FailureReason`; - `Cancelled`:调用方取消了带预算的建图,`Map` 为 `null`,不会写入缓存; - `TimedOut`:建图耗尽调用方的总超时预算,`Map` 为 `null`,不会写入缓存。 公开的 `PlanningMapFactory.Create(request)` 保持兼容且不设置时间限制;粗规划门面使用内部预算入口,使取消和超时能够覆盖地图创建、距离场和后续搜索。 ## 坐标与单位 - 环境地图边界、障碍物几何、TwoLeg 投影输入均使用世界坐标,单位为 **mm**。 - `MapBoundsMm` 范围采用左闭右开:`[XMin, XMax) × [YMin, YMax)`;最大边界不属于地图。 - `EnvironmentGridMap` 查询使用 mm;`PlanningGridMap` 的世界查询使用 **m**。 - 行 `row` 对应 Y 方向,列 `col` 对应 X 方向,存储顺序为行主序 `row * Cols + col`。 - `PlanningGridMap` 的越界位置按占据处理,障碍净距返回 0 m。 - Map 不做车辆自身占据或安全外扩;车辆外形与安全裕度由后续碰撞检测负责。 ## 最小调用示例 ```csharp var mapFactory = new PlanningMapFactory(); // 长期持有,不要每次规划重新创建 IMapObstacle[] obstacles = { new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), new CircleObstacle(3600f, 1200f, 180f), }; var request = new PlanningMapRequest { Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), ResolutionMm = 50f, ObstacleSources = new IMapObstacleSource[] { new ManualObstacleSource("manual", 1L, true, obstacles), }, AllowExplicitEmptyMap = false, }; PlanningMapBuildResult result = mapFactory.Create(request); if (!result.Succeeded || result.Map == null || !result.Map.PlanningReady) throw new InvalidOperationException(result.FailureReason); PlanningGridMap map = result.Map; // 交给粗路径规划器 ``` 真实 TwoLeg 数据应由上层检测模块在检测时刻构造成 `TwoLegProjectionInput`,再交给 `TwoLegObstacleSource`。不要让 Map 模块主动读取检测器或定位器。 ## 缓存与版本 `PlanningMapFactory` 内部有容量为 4 的两级 LRU 缓存: - **输入命中(Input)**:边界、分辨率、空图策略、来源 ID、`SourceVersion` 和必需性均相同,直接返回同一个 `PlanningGridMap` 对象。 - **占据命中(Occupancy)**:来源版本变化,但最终占据栅格相同,复用不可变的占据/距离数组,并颁发新的 `SnapshotId`。 - **未命中(None)**:栅格内容变化,重新生成规划快照。 因此,障碍来源的快照内容发生变化时,调用方必须增加其 `SourceVersion`。未递增版本会错误复用旧地图;仅修改日志、PNG 开关、起终点或车辆参数不应改变地图版本。 ## 测试与调试 - `Test/MovementTest.MapTest.cs` 是手工 Clumsy 测试入口。文件顶部可设置地图范围、分辨率、TwoLeg 测试快照、`EnableTerminalDebugLog` 和 `SavePng`。 - `PlanningMapImageExporter` 仅在 `SavePng` 开启时输出 PNG;它只读取 `PlanningGridMap`,不参与建图、缓存键或规划结果。 - 自动检查脚本位于 `ClumsyPilot/tests`:工具、工厂、适配器、PNG 和 MapTest 配置分别有独立验证脚本。 - 旧版 `Occupancygird_Map/Map_test/TrapMapImageExporter.cs` 与 `MovementTest.Trapmaptest.cs` 仍保留作历史对照;它们不是新粗路径规划的运行时地图入口。 ## 详细使用指南 本节说明调用方如何从“障碍物数据”逐步得到可交给粗路径规划器的 `PlanningGridMap`。新地图的唯一创建入口是: ```csharp PlanningMapBuildResult result = mapFactory.Create(request); ``` 其中 `mapFactory` 是长期持有的 `PlanningMapFactory`,`request` 是本次建图输入,`result.Map` 是成功时的只读规划地图。 ### 第 1 步:长期创建地图工厂 地图工厂内部维护容量为 4 的缓存,因此不要在每次规划前重新创建它。应把它作为规划服务或 MovementTest 的字段长期保存: ```csharp using MultiWheelC.TrajectoryPlanning.Mapping; private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); ``` ### 第 2 步:准备手工障碍物 圆形和轴对齐矩形都实现 `IMapObstacle`。障碍物的坐标是**世界坐标**,单位都是 **mm**。 ```csharp IMapObstacle[] manualObstacles = { // 参数依次为:X最小值、X最大值、Y最小值、Y最大值,单位均为 mm。 new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), // 参数依次为:圆心X、圆心Y、半径,单位均为 mm。 new CircleObstacle(3600f, 1200f, 180f), }; ``` 目前 Map 只负责“环境障碍物”。不要在这里添加 AMR 自身外形,也不要在圆半径或矩形尺寸中叠加安全裕度;车辆外形与安全距离由粗路径的碰撞检查处理。 ### 第 3 步:包装为统一障碍来源 所有障碍物必须通过 `IMapObstacleSource` 进入地图。手工障碍使用 `ManualObstacleSource`: ```csharp var manualSource = new ManualObstacleSource( sourceId: "manual", sourceVersion: 1L, isRequired: true, obstacles: manualObstacles); ``` 参数意义如下: - `sourceId`:来源的唯一名称;同一次请求内不能重复。 - `sourceVersion`:来源快照版本。障碍物位置、数量、半径或尺寸变化后,必须递增。 - `isRequired`:`true` 表示来源无效时整次建图失败;`false` 表示仅记录该来源状态并继续建图。 - `obstacles`:当前时刻的不可变障碍物集合。 例如障碍物内容变化后,应创建带新版本号的来源: ```csharp var changedManualSource = new ManualObstacleSource( "manual", 2L, // 1L 变为 2L,通知工厂地图输入已改变 true, changedObstacles); ``` ### 第 4 步:可选地加入 TwoLeg 障碍物 Map 不主动调用 TwoLeg 检测器。上层检测模块应在检测时刻取得 AMR 世界位姿和两腿局部坐标,构造成 `TwoLegProjectionInput`: ```csharp var twoLegInput = new TwoLegProjectionInput( hasDetection: true, // 检测时 AMR 的世界位姿:位置单位 mm,航向单位 rad。 detectionWorldX: 1000f, detectionWorldY: 2000f, detectionHeadingRadians: 0d, // 两条腿相对于检测时 AMR 位姿的局部坐标,单位 mm。 firstLocalX: 300f, firstLocalY: 150f, secondLocalX: 300f, secondLocalY: -150f, // 每条腿最终投影为圆形障碍物的半径,单位 mm。 radiusMm: 80f, diagnostic: "TwoLeg 检测快照"); var twoLegSource = new TwoLegObstacleSource( sourceId: "two-leg", sourceVersion: 5L, isRequired: false, input: twoLegInput); ``` 没有检测结果时仍可传入空快照: ```csharp var noTwoLegInput = new TwoLegProjectionInput( false, 0f, 0f, 0d, 0f, 0f, 0f, 0f, 0f, "当前没有 TwoLeg 检测结果"); ``` `hasDetection` 为 `false` 时,该来源会返回“空”结果,不会将零坐标当作障碍物。若 TwoLeg 只是可选信息,建议 `isRequired` 设置为 `false`。 ### 第 5 步:创建建图请求 边界与分辨率仍使用 **mm**。范围采用左闭右开,例如 `XMax = 6000f` 时,`x = 6000f` 不属于地图。 ```csharp var request = new PlanningMapRequest { // 世界范围:[0, 6000) × [0, 4000),单位 mm。 Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), // 每格 50 mm。 ResolutionMm = 50f, ObstacleSources = new IMapObstacleSource[] { manualSource, twoLegSource, }, // false:没有任何有效障碍物时,地图不能直接交给规划器。 // true:调用方明确确认空地图安全时,才允许空图参与规划。 AllowExplicitEmptyMap = false, }; ``` ### 第 6 步:创建地图并处理失败 ```csharp PlanningMapBuildResult result = _mapFactory.Create(request); if (!result.Succeeded) { Console.WriteLine("建图失败:" + result.FailureReason); return; } if (result.Map == null || !result.Map.PlanningReady) { Console.WriteLine("地图不能用于规划:" + result.Map?.PlanningBlockReason); return; } PlanningGridMap planningMap = result.Map; ``` 此处必须同时检查: - `Succeeded`:请求、来源和栅格构建过程是否成功; - `Map != null`:是否产出了规划快照; - `PlanningReady`:是否允许把该快照交给规划器。隐式空图会在这一项被拦截。 需要查看每个来源是否成功投影时,读取 `result.SourceResults`;需要观察缓存效果时,读取 `result.CacheHit`。 ```csharp Console.WriteLine( "快照编号=" + planningMap.SnapshotId + ",缓存=" + result.CacheHit + ",栅格=" + planningMap.Cols + "×" + planningMap.Rows); ``` ### 第 7 步:交给粗路径规划器查询 `PlanningGridMap` 是不可变对象,可以安全地作为一次规划任务的输入。注意:它的世界查询坐标单位已经变成 **m**,而不是 mm。 ```csharp // 查询 (2.5 m, 1.0 m) 是否位于占据格;越界也会返回 true。 bool occupied = planningMap.IsOccupiedWorld(2.5d, 1.0d); // 查询该位置到最近障碍物的保守净距,单位 m;越界返回 0 m。 double clearanceMeters = planningMap.GetConservativeObstacleDistanceMeters(2.5d, 1.0d); ``` 粗路径规划器应只使用 `PlanningGridMap` 的占据与距离查询,不应直接修改或重建其中的栅格。 ### 完整实例 ```csharp private readonly PlanningMapFactory _mapFactory = new PlanningMapFactory(); private PlanningGridMap CreateMapForCoarsePlanning() { IMapObstacle[] obstacles = { new AxisAlignedRectangleObstacle(2400f, 2800f, 800f, 1800f), new CircleObstacle(3600f, 1200f, 180f), }; var manualSource = new ManualObstacleSource("manual", 1L, true, obstacles); var request = new PlanningMapRequest { Bounds = new MapBoundsMm(0f, 6000f, 0f, 4000f), ResolutionMm = 50f, ObstacleSources = new IMapObstacleSource[] { manualSource }, AllowExplicitEmptyMap = false, }; PlanningMapBuildResult result = _mapFactory.Create(request); if (!result.Succeeded) throw new InvalidOperationException("建图失败:" + result.FailureReason); if (result.Map == null || !result.Map.PlanningReady) throw new InvalidOperationException("地图不可规划:" + result.Map?.PlanningBlockReason); return result.Map; } ``` ### 常见错误 | 情况 | 原因 | 处理方式 | | --- | --- | --- | | 改了障碍物但地图仍复用旧快照 | 未递增 `SourceVersion` | 障碍内容每次变化后增加该来源版本号 | | 规划查询位置总是越界 | 将 mm 坐标传给了 `PlanningGridMap` | 规划查询前将 mm 除以 1000 转为 m | | 地图创建成功但不能规划 | 未明确允许空图且没有有效障碍物 | 补充有效来源,或确认安全后设置 `AllowExplicitEmptyMap = true` | | TwoLeg 出现在错误位置 | 未使用检测时刻位姿,或混用了 mm 与 m | 用检测时刻的世界位姿和局部 mm 坐标创建快照 | | 缓存总是未命中 | 每次都新建 `PlanningMapFactory`,或版本号无意义变化 | 长期复用工厂;仅在来源内容实际变化时递增版本 |