[tools] 整理导航打点工具与抽样PCD

This commit is contained in:
2026-07-27 13:39:52 +08:00
parent 4ee4af028c
commit 55094bf682
48 changed files with 428481 additions and 15 deletions
+2
View File
@@ -37,6 +37,8 @@ wandb/
sim2sim_log_*.txt
**/sim2sim_temp.xml
**/route_check_runs/
**/route_experiments/suite_*/
**/tools/nav_tools/points/auto_candidates/
# IDE and operating system files
.idea/
+10
View File
@@ -13,6 +13,7 @@
| `v0.6.0` | 比赛训练架构 | 分轴奖励、自适应指令课程、障碍释放课程和比赛站姿 |
| `v0.7.0` | MuJoCo 工具 | 姿态优化、IK 扫描、动力学、MPC 和 GUI 调试工具 |
| `v0.8.0` | 后期 Sim2Sim | ONNX 回放、IK/路线检查工具和比赛最终 Rough 策略 |
| `v0.8.1` | 导航打点工具 | 地图/航点编辑、路线迭代和抽样 PCD 补充包 |
## `v0.4.0` 的模型变化
@@ -61,3 +62,12 @@
- 增加 PT→ONNX 导出、IK 补偿扫描、纯 IK 绕桩和 ONNX 批量路线检查入口。
- 归档比赛最终 Rough 策略 `model_6800.onnx`;其 SHA-256 为 `3C994BDD3434AD15770A52AC0E8D229F502F00D6511CDD42C2E2C742301AEF13`
- Crawl 权重、运行日志、临时 XML 和大量重复路线实验不在本阶段归档。
## `v0.8.1` 的导航打点工具
- 补充 Pygame 地图/PCD/航点综合编辑器、避障区域编辑器和坐标变换工具。
- 补充路线安全检查、候选航点优化、XML/航点镜像和批量 Sim2Sim 实验入口。
- 按源文件时间保留 14 份比赛路线 JSON,不将开发期文件名误解释为正式版本号。
- 补充 `1hao.xml``2hao.xml``A_C.xml`,并为 `1B_FF.json` 补齐其引用的 `B_C.xml`
- 将两份约 915 MiB 的原始 ASCII PCD 确定性抽样为各小于 10 MB 的预览点云;抽样参数、点数和哈希记录在工具 README。
- 训练代码、MJCF、比赛策略和历史依赖锁保持 `v0.8.0` 状态不变。
+1 -1
View File
@@ -28,7 +28,7 @@ MJCF + mjlab task
IK real --------------------------------> 电机
```
`rc_mjlab` 是自包含工程。训练、MJCF、MuJoCo、Sim2Sim 和策略权重通过相对路径绑定,因此保留其内部布局,没有为了目录外观拆散。第一代完整闭环见 `v0.3.0`,第一份新版 MJCF 与训练框架见 `v0.4.0`,随机化增强版见 `v0.5.0`,比赛最终训练架构见 `v0.6.0`,后期 MuJoCo 工具集见 `v0.7.0`,后期 Sim2Sim 与比赛 Rough 策略见 `v0.8.0`
`rc_mjlab` 是自包含工程。训练、MJCF、MuJoCo、Sim2Sim、导航工具和策略权重通过相对路径绑定,因此保留其内部布局,没有为了目录外观拆散。第一代完整闭环见 `v0.3.0`,第一份新版 MJCF 与训练框架见 `v0.4.0`,随机化增强版见 `v0.5.0`,比赛最终训练架构见 `v0.6.0`,后期 MuJoCo 工具集见 `v0.7.0`,后期 Sim2Sim 与比赛 Rough 策略见 `v0.8.0`,完整导航打点工具见 `v0.8.1`
详细说明见:
+4 -1
View File
@@ -2,7 +2,7 @@
`rc_mjlab/` 保存 16DOF 轮足机器人的当前训练与 Sim2Sim 工程。历史快照由 Git Tag 保留,不在目录中复制 `old``new``final` 版本。
当前内容对应 `v0.8.0`:训练代码保持 `v0.6.0` 的比赛架构,包含 `v0.7.0` 的 MuJoCo 独立工具,并新增后期 Sim2Sim、路线检查和比赛最终 Rough ONNX 策略。训练过程可能先获得基模,再调整奖励、课程和环境参数继续训练;模型 checkpoint 的变化不等同于软件架构变化。
当前内容对应 `v0.8.1`:训练代码保持 `v0.6.0` 的比赛架构,包含后期 MuJoCo、Sim2Sim、比赛最终 Rough ONNX 策略,并补充完整导航打点工具、路线迭代和抽样 PCD。训练过程可能先获得基模,再调整奖励、课程和环境参数继续训练;模型 checkpoint 的变化不等同于软件架构变化。
## 内容
@@ -10,6 +10,7 @@
- `mjcf`:轮足机器人 MuJoCo 模型和网格
- `sim2sim`:策略加载、交互控制和比赛地形验证
- `mujoco_sim`:不依赖训练循环的姿态、IK、动力学和 MPC 分析
- `tools/nav_tools`:地图/PCD 查看、航点编辑、路线检查和比赛路线数据
- `mjlab`:固定版本的本地训练框架依赖
- `model_rough.pt`:本阶段 Rough 策略权重
- `model_6800.onnx`:比赛最终使用的 Rough 策略
@@ -25,4 +26,6 @@
`v0.8.0` 继续保持训练架构和 MJCF 不变,归档后期 Sim2Sim 增量与比赛 Rough ONNX 策略;入口和归档边界见 [`rc_mjlab/sim2sim/README.md`](rc_mjlab/sim2sim/README.md)。
`v0.8.1` 补充完整导航打点工具和小体积预览点云;入口、路线清单和抽样边界见 [`rc_mjlab/tools/nav_tools/README.md`](rc_mjlab/tools/nav_tools/README.md)。
工程命令和任务说明见 [`rc_mjlab/README.md`](rc_mjlab/README.md),本地依赖来源见 [`rc_mjlab/DEPENDENCIES.md`](rc_mjlab/DEPENDENCIES.md)。
@@ -9,6 +9,7 @@
- PyTorch CUDA 12.8 环境
- `pynput`
- 后期 Sim2Sim 可选依赖:Pygame、ONNX Runtime
- 导航打点可选依赖:Pygame、Pillow
精确解析结果保存在 `uv.lock`。项目使用本地可编辑的 `mjlab`
@@ -46,4 +47,10 @@ uv run play Robot-Rough-v0
uv run --with-requirements sim2sim/requirements.txt python sim2sim/nav_sim2sim.py
```
导航打点工具同样不修改历史锁文件:
```bash
uv run --with-requirements tools/nav_tools/requirements.txt python tools/nav_tools/nav_map_viewer.py
```
GPU、CUDA、MuJoCo development wheel 和驱动版本必须满足 `pyproject.toml``uv.lock` 的约束。
+4 -2
View File
@@ -2,7 +2,7 @@
基于 [mjlab](https://github.com/google-deepmind/mjlab) 框架的四轮腿混合机器人强化学习训练与部署部署项目,面向机器人竞赛场景(如越障、匍匐、斜坡、台阶等复合任务)。
> 当前目录对应 `v0.8.0`:保留 `v0.6.0` 的比赛训练架构与 `v0.7.0` 的 MuJoCo 独立工具集,加入后期 Sim2Sim 工具和比赛最终 Rough 策略 `model_6800.onnx`。`model_rough.pt` 仍作为早期参考权重保留。
> 当前目录对应 `v0.8.1`:保留比赛训练架构、后期 MuJoCo/Sim2Sim 和比赛最终 Rough 策略,并补充导航地图、打点工具、路线迭代与抽样 PCD。`model_rough.pt` 仍作为早期参考权重保留。
---
@@ -58,7 +58,7 @@ rc_mjlab/
│ ├── scene.xml # mjlab 场景入口文件
│ └── meshes/ # STL/OBJ 碰撞与外观网格
├── mujoco_sim/ # 姿态、IK、动力学和 MPC 独立工具
├── tools/nav_tools/ # 路线安全检查公共模块
├── tools/nav_tools/ # 地图/PCD/航点编辑、路线检查与比赛路线数据
├── model_rough.pt # 早期 Rough 参考 checkpoint
├── model_6800.onnx # 比赛最终 Rough 策略
├── pyproject.toml # 项目依赖(uv 管理,含清华镜像源加速)
@@ -97,6 +97,8 @@ uv run python nav_sim2sim.py
后期 Sim2Sim 的入口、模型边界和批量检查命令见 [`sim2sim/README.md`](sim2sim/README.md)。
导航打点工具、路线快照和抽样点云说明见 [`tools/nav_tools/README.md`](tools/nav_tools/README.md)。
---
## 🖥️ 交互式自动导航平台 (sim2sim/nav_sim2sim.py)
+2 -2
View File
@@ -53,6 +53,6 @@ uv run --with-requirements .\sim2sim\requirements.txt python .\sim2sim\export_on
- `../model_6800.onnx``last_not_slalom_1050` 最终真机工程使用的比赛 Rough 策略,SHA-256 为 `3C994BDD3434AD15770A52AC0E8D229F502F00D6511CDD42C2E2C742301AEF13`
- `../model_rough.pt` 是较早阶段的参考 checkpoint,两者不是同一版本的权重。
- Crawl 模型未在本阶段归档;需要 Crawl 策略的入口会查找 `model_crawl.onnx``model_crawl.pt`
- `nav_route_sim2sim_check.py` 依赖 `../tools/nav_tools/route_safety_check.py` 的航点和避障几何定义。
- `nav_route_sim2sim_check.py` 依赖 `../tools/nav_tools/route_safety_check.py` 的航点和避障几何定义;默认使用 `points_20260715_120154.json``1hao.xml`
运行时生成的日志、临时 XML`route_check_runs/` 不纳入版本库。源目录中的大量路线试验结果也未复制;它们包含重复轨迹和本机绝对路径,不属于可复用程序源码
运行时生成的日志、临时 XML`route_check_runs/` 和批量实验输出不纳入版本库。人工打点形成的路线快照保存在 `../tools/nav_tools/points/`,大量重复仿真轨迹仍不复制
@@ -1235,8 +1235,8 @@ class SlalomScriptFollower(NavGoodFollower):
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run MuJoCo+ONNX route validation.")
parser.add_argument("--terrain-xml", type=Path, default=PROJECT_ROOT / "tools/nav_tools/xml/A.xml")
parser.add_argument("--points", type=Path, default=PROJECT_ROOT / "tools/nav_tools/points/points_20260705_174627.json")
parser.add_argument("--terrain-xml", type=Path, default=PROJECT_ROOT / "tools/nav_tools/xml/1hao.xml")
parser.add_argument("--points", type=Path, default=PROJECT_ROOT / "tools/nav_tools/points/points_20260715_120154.json")
parser.add_argument("--onnx", type=Path, default=PROJECT_ROOT / "model_6800.onnx")
parser.add_argument("--crawl-onnx", type=Path, default=PROJECT_ROOT / "model_crawl.onnx")
parser.add_argument(
@@ -1,7 +1,93 @@
# 路线检查公共模块
# 导航地图与打点工具
`route_safety_check.py` 提供航点、避障区域、机器人平面包络和几何距离计算,供 `sim2sim/nav_route_sim2sim_check.py` 复用
本目录保存比赛后期使用的独立导航地图、PCD 查看、航点编辑、避障区域编辑和路线验证工具。它同时为 `sim2sim/nav_route_sim2sim_check.py` 提供航点与避障几何定义
该模块只依赖 Python 标准库。原开发目录中的地图编辑器、PCD、比赛路线 JSON、备份和批量实验结果不属于本次后期 Sim2Sim 里程碑,未在这里复制。
## 目录
外部路线文件需要包含 `waypoints`(或 `segments[].waypoints`)以及 `regions` / `avoid_regions`。也可以不提供路线文件,直接使用 Sim2Sim 检查器的内置任务。
```text
nav_tools/
├─ nav_map_viewer.py # 地图、PCD、XML、航点和避障区综合编辑器
├─ avoid_region_tool.py # 独立避障多边形编辑器
├─ route_safety_check.py # 离线路线净空检查
├─ route_candidate_optimizer.py # 航点候选优化
├─ run_route_experiments.py # 安全检查与 Sim2Sim 批量实验
├─ mirror_nav_xml_points.py # XML 与航点镜像
├─ pcd_transform_tool.py # PCD 平移和旋转
├─ transform_pcd_xy.py # PCD 坐标原点变换
├─ downsample_ascii_pcd.py # ASCII PCD 确定性抽样
├─ annotate_odin1_relocalization_frame.py
├─ pcd/ # 小于 10 MB 的打点预览点云
├─ points/ # 比赛期间的路线迭代 JSON
├─ xml/ # 1 号、2 号和 A/B 场地 XML
├─ regions/ # 独立避障区域输出目录
└─ assets/ # 坐标和机构示意图
```
## 运行
`rc_mjlab` 根目录执行:
```powershell
# 地图与航点综合编辑器
uv run --with-requirements .\tools\nav_tools\requirements.txt `
python .\tools\nav_tools\nav_map_viewer.py
# 指定 PCD 的独立避障区域编辑器
uv run --with-requirements .\tools\nav_tools\requirements.txt `
python .\tools\nav_tools\avoid_region_tool.py --pcd 1hao.pcd
# 默认检查 points_20260715_120154.json 与 1hao.xml
uv run python .\tools\nav_tools\route_safety_check.py
# 使用最终 Rough 策略进行默认路线 Sim2Sim 检查
uv run --with-requirements .\sim2sim\requirements.txt `
python .\sim2sim\nav_route_sim2sim_check.py
```
候选路线和批量实验必须显式保存为新文件,不应覆盖历史路线:
```powershell
uv run python .\tools\nav_tools\route_candidate_optimizer.py
uv run --with-requirements .\sim2sim\requirements.txt `
python .\tools\nav_tools\run_route_experiments.py `
--points .\tools\nav_tools\points\points_20260715_120154.json
```
## PCD 抽样
仓库中的两份 PCD 从原始 ASCII 点云按固定步长均匀抽样,字段、坐标和 PCD 头结构保持不变。它们面向地图显示和人工打点,不替代原始高密度点云用于建图、定位精度评估或点云算法基准。
| 文件 | 原始点数 | 抽样步长 | 仓库点数 | 仓库大小 | SHA-256 |
| --- | ---: | ---: | ---: | ---: | --- |
| `pcd/1hao.pcd` | 9,163,893 | 46 | 199,215 | 9,876,010 B | `48B231C52BECA51316F352300C8B2046133E92359E0855227D93DEB0D927AD34` |
| `pcd/2hao.pcd` | 10,043,048 | 52 | 193,136 | 9,719,736 B | `714516A7A726D46311A58507149FBC93D622616BE79C59FFED622274D6526B1F` |
复现抽样:
```powershell
python .\tools\nav_tools\downsample_ascii_pcd.py `
<原始PCD> <输出PCD> --max-bytes 9900000
```
## 航点数据
`points/` 保留原文件名和时间顺序,没有把多个路线重命名成 `old``new``final`。各文件的航点数、避障区和 XML 绑定见 [`points/README.md`](points/README.md)。
- 请求目录中的 `route_safety_check.py` 是较早版本,因此保留 `v0.8.0` 已归档的后期兼容版本。
- `1B_FF.json` 引用了请求目录中缺失的 `xml/B_C.xml`,本次从后期整合目录补齐该文件。
- `.uv-cache``__pycache__`、自动候选、实验日志和重复轨迹输出不归档。
整理时使用默认圆形包络和 `0.05 m` 额外净空检查 `points_20260715_120154.json`,报告了 3 组线段—避障区净空不足。该结果按原样保留,未自动移动航点;它表示保守几何检查仍有待复核,不等同于路线没有经过实机使用。
## 主要操作
- `Pan`:拖动地图;滚轮缩放;`F` 适应窗口。
- `Point`:添加、插入、选择和编辑航点;`Backspace` 删除。
- `Terrain`:选择并调整 XML 障碍组的位置和偏航。
- `Avoid`:绘制避障多边形;`Enter` 闭合;`Delete` 删除。
- `Load JSON`:载入 `waypoints``segments[].waypoints`
- `Save JSON` / `Save All JSON`:同时保存航点和避障区域。
- `M`:切换 XML`O`:切换坐标原点;`Esc`:退出。
任务字段约定:`none``slalom``gravel``wall``low_bar``stairs``ramp_bridge``spawn``return`
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Generate an annotated Odin1 relocalization-frame PNG.
The wiki defines Odin1 frames as:
I: IMU frame
L: LiDAR / point-cloud frame
C = camera frame
Relocalization poses are tied to the SLAM point-cloud map, so the device pose
should be treated as T_map_L unless a driver-specific TF remaps it.
"""
from __future__ import annotations
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
TOOL_DIR = Path(__file__).resolve().parent
IMG_DIR = TOOL_DIR / "assets"
OUT = TOOL_DIR / "odin1_relocalization_frame.png"
def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
return ImageFont.load_default()
def arrow(draw: ImageDraw.ImageDraw, start: tuple[int, int], end: tuple[int, int], color: tuple[int, int, int], width: int = 5) -> None:
draw.line([start, end], fill=color, width=width)
sx, sy = start
ex, ey = end
dx = ex - sx
dy = ey - sy
length = max((dx * dx + dy * dy) ** 0.5, 1.0)
ux, uy = dx / length, dy / length
px, py = -uy, ux
head = 18
wing = 9
points = [
(ex, ey),
(int(ex - ux * head + px * wing), int(ey - uy * head + py * wing)),
(int(ex - ux * head - px * wing), int(ey - uy * head - py * wing)),
]
draw.polygon(points, fill=color)
def label_box(
draw: ImageDraw.ImageDraw,
xy: tuple[int, int],
text: str,
fill: tuple[int, int, int],
text_color: tuple[int, int, int] = (255, 255, 255),
size: int = 24,
) -> None:
x, y = xy
fnt = font(size, bold=True)
lines = text.splitlines()
widths = [draw.textbbox((0, 0), line, font=fnt)[2] for line in lines]
heights = [draw.textbbox((0, 0), line, font=fnt)[3] - draw.textbbox((0, 0), line, font=fnt)[1] for line in lines]
box_w = max(widths) + 28
box_h = sum(heights) + 14 * (len(lines) - 1) + 24
draw.rounded_rectangle((x, y, x + box_w, y + box_h), radius=10, fill=fill, outline=(255, 255, 255), width=2)
cy = y + 12
for line, h in zip(lines, heights):
draw.text((x + 14, cy), line, fill=text_color, font=fnt)
cy += h + 14
def main() -> int:
coordinate = Image.open(IMG_DIR / "coordinate.png").convert("RGB")
structure = Image.open(IMG_DIR / "structure1.png").convert("RGB")
target_w = 1180
coordinate = coordinate.resize((target_w, int(coordinate.height * target_w / coordinate.width)))
structure = structure.resize((target_w, int(structure.height * target_w / structure.width)))
gap = 24
margin = 30
title_h = 118
canvas = Image.new("RGB", (target_w + margin * 2, title_h + coordinate.height + gap + structure.height + margin), (245, 247, 250))
draw = ImageDraw.Draw(canvas)
title_font = font(34, bold=True)
body_font = font(22)
draw.text((margin, 22), "Odin1 relocalization frame: LiDAR / point-cloud frame L", fill=(15, 23, 42), font=title_font)
draw.text(
(margin, 68),
"Use the relocalization pose as T_map_L. Convert to robot base with your measured T_L_base.",
fill=(51, 65, 85),
font=body_font,
)
top_y = title_h
bot_y = title_h + coordinate.height + gap
canvas.paste(coordinate, (margin, top_y))
canvas.paste(structure, (margin, bot_y))
red = (220, 38, 38)
blue = (37, 99, 235)
green = (22, 163, 74)
amber = (217, 119, 6)
# coordinate.png positions after scaling to 1180 px.
lidar_origin = (margin + 690, top_y + 255)
imu_origin = (margin + 286, top_y + 655)
camera_origin = (margin + 885, top_y + 500)
draw.ellipse((lidar_origin[0] - 12, lidar_origin[1] - 12, lidar_origin[0] + 12, lidar_origin[1] + 12), fill=red, outline=(255, 255, 255), width=3)
arrow(draw, (margin + 900, top_y + 150), lidar_origin, red, width=6)
label_box(draw, (margin + 725, top_y + 70), "Relocalization pose\nis here: frame L", red)
draw.ellipse((imu_origin[0] - 9, imu_origin[1] - 9, imu_origin[0] + 9, imu_origin[1] + 9), fill=blue)
arrow(draw, (margin + 160, top_y + 555), imu_origin, blue, width=4)
label_box(draw, (margin + 40, top_y + 460), "IMU frame I\nnot the relocalization origin", blue, size=20)
draw.ellipse((camera_origin[0] - 9, camera_origin[1] - 9, camera_origin[0] + 9, camera_origin[1] + 9), fill=green)
arrow(draw, (margin + 1015, top_y + 565), camera_origin, green, width=4)
label_box(draw, (margin + 880, top_y + 585), "Camera frame C\nseparate optical frame", green, size=20)
# structure1.png positions after scaling to 1180 px.
struct_imu = (margin + 398, bot_y + 357)
struct_lidar_hint = (margin + 677, bot_y + 350)
draw.ellipse((struct_imu[0] - 9, struct_imu[1] - 9, struct_imu[0] + 9, struct_imu[1] + 9), fill=blue)
arrow(draw, (margin + 245, bot_y + 250), struct_imu, blue, width=4)
label_box(draw, (margin + 45, bot_y + 165), "Wiki marks IMU separately", blue, size=20)
arrow(draw, (margin + 820, bot_y + 230), struct_lidar_hint, red, width=5)
label_box(draw, (margin + 830, bot_y + 145), "L is the point-cloud/LiDAR frame\nshown in the coordinate diagram", red, size=20)
note = (
"Fixed wiki extrinsic: T^imu_lidar translation = [-0.02663, 0.03447, 0.02174] m, rotation = identity.\n"
"So L and I axes are parallel, but their origins are offset. Do not use shell center as pose origin."
)
note_box = (margin, canvas.height - margin - 78, canvas.width - margin, canvas.height - margin)
draw.rounded_rectangle(note_box, radius=10, fill=(255, 251, 235), outline=amber, width=2)
draw.text((margin + 16, canvas.height - margin - 64), note, fill=(120, 53, 15), font=font(19))
canvas.save(OUT)
print(OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,373 @@
#!/usr/bin/env python3
"""Pygame tool for marking no-go / avoid regions on a PCD map."""
from __future__ import annotations
import argparse
import json
import math
import os
import pkgutil
import time
from dataclasses import dataclass, field
from pathlib import Path
os.environ.setdefault("PYGAME_HIDE_SUPPORT_PROMPT", "1")
if not hasattr(pkgutil, "ImpImporter"):
pkgutil.ImpImporter = pkgutil.zipimporter # type: ignore[attr-defined]
import pygame
TOOL_DIR = Path(__file__).resolve().parent
PCD_DIR = TOOL_DIR / "pcd"
REGION_DIR = TOOL_DIR / "regions"
COLOR_BG = (10, 15, 30)
COLOR_GRID = (22, 29, 48)
COLOR_PANEL = (18, 24, 42)
COLOR_BORDER = (50, 65, 95)
COLOR_TEXT = (248, 250, 252)
COLOR_MUTED = (148, 163, 184)
COLOR_PCD = (92, 160, 255)
COLOR_REGION = (244, 63, 94)
COLOR_CURRENT = (234, 179, 8)
@dataclass
class PointCloud:
path: Path | None = None
points: list[tuple[float, float, float]] = field(default_factory=list)
total_count: int = 0
sampled_count: int = 0
error: str = ""
@dataclass
class Region:
name: str
points: list[tuple[float, float]]
kind: str = "avoid"
class Camera:
def __init__(self, width: int, height: int, panel_width: int = 330) -> None:
self.width = width
self.height = height
self.panel_width = panel_width
self.zoom = 55.0
self.pan_x = 0.0
self.pan_y = 0.0
@property
def map_width(self) -> int:
return max(200, self.width - self.panel_width)
def world_to_screen(self, x: float, y: float) -> tuple[int, int]:
return int(self.map_width / 2 + x * self.zoom + self.pan_x), int(self.height / 2 - y * self.zoom + self.pan_y)
def screen_to_world(self, sx: float, sy: float) -> tuple[float, float]:
return (sx - self.map_width / 2 - self.pan_x) / self.zoom, -(sy - self.height / 2 - self.pan_y) / self.zoom
def zoom_at(self, factor: float, pos: tuple[int, int]) -> None:
before = self.screen_to_world(*pos)
self.zoom = max(8.0, min(260.0, self.zoom * factor))
after = self.screen_to_world(*pos)
self.pan_x += (after[0] - before[0]) * self.zoom
self.pan_y -= (after[1] - before[1]) * self.zoom
def get_font(size: int, bold: bool = False) -> pygame.font.Font:
return pygame.font.Font(None, size)
def scan_pcd_files() -> list[Path]:
PCD_DIR.mkdir(parents=True, exist_ok=True)
return sorted(PCD_DIR.glob("*.pcd"), key=lambda item: item.name.lower())
def iter_ascii_pcd_points(path: Path):
with path.open("r", encoding="utf-8", errors="ignore") as handle:
data = False
for raw in handle:
line = raw.strip()
if data and line:
parts = line.split()
if len(parts) >= 3:
yield float(parts[0]), float(parts[1]), float(parts[2])
elif line.upper().startswith("DATA"):
if "ascii" not in line.lower():
raise RuntimeError(f"Only ASCII PCD is supported: {path}")
data = True
def load_pcd(path: Path, max_points: int = 90000, z_min: float = -5.0, z_max: float = 0.5) -> PointCloud:
cloud = PointCloud(path=path)
try:
filtered = []
for x, y, z in iter_ascii_pcd_points(path):
cloud.total_count += 1
if z_min <= z <= z_max:
filtered.append((x, y, z))
stride = max(1, math.ceil(len(filtered) / max_points)) if filtered else 1
cloud.points = filtered[::stride]
cloud.sampled_count = len(cloud.points)
except Exception as exc:
cloud.error = str(exc)
return cloud
def fit_camera(camera: Camera, cloud: PointCloud, regions: list[Region]) -> None:
xs = [p[0] for p in cloud.points]
ys = [p[1] for p in cloud.points]
for region in regions:
xs.extend(p[0] for p in region.points)
ys.extend(p[1] for p in region.points)
if not xs or not ys:
return
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
span_x = max(max_x - min_x, 1.0)
span_y = max(max_y - min_y, 1.0)
margin = 70
camera.zoom = max(8.0, min(260.0, min((camera.map_width - margin * 2) / span_x, (camera.height - margin * 2) / span_y)))
camera.pan_x = -((min_x + max_x) / 2) * camera.zoom
camera.pan_y = ((min_y + max_y) / 2) * camera.zoom
def draw_grid(surface: pygame.Surface, camera: Camera, font: pygame.font.Font) -> None:
min_x, min_y = camera.screen_to_world(0, camera.height)
max_x, max_y = camera.screen_to_world(camera.map_width, 0)
for gx in range(math.floor(min_x), math.ceil(max_x) + 1):
sx, _ = camera.world_to_screen(gx, 0)
color = (38, 50, 78) if gx == 0 else COLOR_GRID
pygame.draw.line(surface, color, (sx, 0), (sx, camera.height), 2 if gx == 0 else 1)
if gx % 2 == 0 and 0 < sx < camera.map_width - 30:
surface.blit(font.render(f"{gx}m", True, COLOR_MUTED), (sx + 4, camera.height - 22))
for gy in range(math.floor(min_y), math.ceil(max_y) + 1):
_, sy = camera.world_to_screen(0, gy)
color = (38, 50, 78) if gy == 0 else COLOR_GRID
pygame.draw.line(surface, color, (0, sy), (camera.map_width, sy), 2 if gy == 0 else 1)
def draw_pcd(surface: pygame.Surface, camera: Camera, cloud: PointCloud) -> None:
for x, y, z in cloud.points:
sx, sy = camera.world_to_screen(x, y)
if 0 <= sx < camera.map_width and 0 <= sy < camera.height:
color = (148, 210, 255) if z > 0.3 else COLOR_PCD
surface.set_at((sx, sy), color)
def draw_regions(surface: pygame.Surface, camera: Camera, regions: list[Region], current: list[tuple[float, float]]) -> None:
overlay = pygame.Surface(surface.get_size(), pygame.SRCALPHA)
for region in regions:
points = [camera.world_to_screen(x, y) for x, y in region.points]
if len(points) >= 3:
pygame.draw.polygon(overlay, (*COLOR_REGION, 75), points)
pygame.draw.polygon(surface, COLOR_REGION, points, width=2)
if current:
points = [camera.world_to_screen(x, y) for x, y in current]
for point in points:
pygame.draw.circle(surface, COLOR_CURRENT, point, 5)
if len(points) >= 2:
pygame.draw.lines(surface, COLOR_CURRENT, False, points, width=2)
surface.blit(overlay, (0, 0))
def save_regions(regions: list[Region], cloud: PointCloud) -> Path:
REGION_DIR.mkdir(parents=True, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
path = REGION_DIR / f"avoid_regions_{stamp}.json"
payload = {
"name": f"avoid_regions_{stamp}",
"map": cloud.path.stem if cloud.path else "",
"frame_id": "map",
"regions": [
{
"id": index,
"name": region.name,
"kind": region.kind,
"polygon": [{"x": x, "y": y} for x, y in region.points],
}
for index, region in enumerate(regions, start=1)
],
}
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
return path
def draw_panel(
surface: pygame.Surface,
camera: Camera,
title_font: pygame.font.Font,
small_font: pygame.font.Font,
pcd_files: list[Path],
selected_pcd: int,
cloud: PointCloud,
regions: list[Region],
current: list[tuple[float, float]],
saved: Path | None,
) -> list[tuple[pygame.Rect, str]]:
x0 = camera.map_width
pygame.draw.rect(surface, COLOR_PANEL, (x0, 0, camera.panel_width, camera.height))
pygame.draw.line(surface, COLOR_BORDER, (x0, 0), (x0, camera.height), 2)
x = x0 + 18
y = 16
buttons: list[tuple[pygame.Rect, str]] = []
surface.blit(title_font.render("avoid regions", True, COLOR_TEXT), (x, y))
y += 34
label = pcd_files[selected_pcd].name if pcd_files and selected_pcd >= 0 else "put .pcd in nav_tools/pcd"
surface.blit(small_font.render(f"PCD: {label[:32]}", True, COLOR_MUTED), (x, y))
y += 24
if cloud.error:
surface.blit(small_font.render(cloud.error[:36], True, COLOR_REGION), (x, y))
else:
surface.blit(small_font.render(f"points: {cloud.sampled_count}/{cloud.total_count}", True, COLOR_MUTED), (x, y))
y += 34
for text, action in (("Prev PCD", "prev_pcd"), ("Next PCD", "next_pcd"), ("Fit", "fit"), ("Save JSON", "save")):
rect = pygame.Rect(x, y, 128, 26)
pygame.draw.rect(surface, (30, 41, 59), rect, border_radius=5)
pygame.draw.rect(surface, COLOR_BORDER, rect, width=1, border_radius=5)
surface.blit(small_font.render(text, True, COLOR_TEXT), (rect.x + 10, rect.y + 6))
buttons.append((rect, action))
y += 32
y += 12
lines = [
f"regions: {len(regions)}",
f"current vertices: {len(current)}",
"",
"Left click: add vertex",
"Enter: close polygon",
"Backspace: undo vertex",
"Delete: remove last region",
"Right/Middle drag: pan",
"Wheel: zoom",
"S: save, F: fit",
]
for line in lines:
color = COLOR_TEXT if line and ":" not in line else COLOR_MUTED
surface.blit(small_font.render(line, True, color), (x, y))
y += 20
if saved:
y += 8
surface.blit(small_font.render(f"saved: {saved.name[:28]}", True, (16, 185, 129)), (x, y))
return buttons
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Mark avoid/no-go regions on a PCD map.")
parser.add_argument("--width", type=int, default=1280)
parser.add_argument("--height", type=int, default=820)
parser.add_argument("--pcd", type=Path, help="PCD file name/path. Relative paths are resolved from tools/nav_tools/pcd.")
return parser.parse_args()
def main() -> int:
args = parse_args()
pcd_files = scan_pcd_files()
selected_pcd = 0
if args.pcd:
if args.pcd.is_absolute():
raise SystemExit("Use a PCD file name under tools/nav_tools/pcd, not an absolute path.")
requested = PCD_DIR / args.pcd
if requested.exists() and requested not in pcd_files:
pcd_files.append(requested)
if requested in pcd_files:
selected_pcd = pcd_files.index(requested)
cloud = load_pcd(pcd_files[selected_pcd]) if pcd_files else PointCloud()
regions: list[Region] = []
current: list[tuple[float, float]] = []
saved: Path | None = None
pygame.init()
pygame.font.init()
screen = pygame.display.set_mode((args.width, args.height), pygame.RESIZABLE)
pygame.display.set_caption("nav_tools - avoid region editor")
title_font = get_font(18, bold=True)
small_font = get_font(13)
map_font = get_font(12)
camera = Camera(args.width, args.height)
fit_camera(camera, cloud, regions)
clock = pygame.time.Clock()
dragging = False
last_mouse = (0, 0)
buttons: list[tuple[pygame.Rect, str]] = []
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.VIDEORESIZE:
camera.width, camera.height = event.w, event.h
screen = pygame.display.set_mode((event.w, event.h), pygame.RESIZABLE)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
if len(current) >= 3:
regions.append(Region(f"avoid_{len(regions) + 1}", current[:]))
current.clear()
elif event.key == pygame.K_BACKSPACE and current:
current.pop()
elif event.key == pygame.K_DELETE and regions:
regions.pop()
elif event.key == pygame.K_f:
fit_camera(camera, cloud, regions)
elif event.key == pygame.K_s:
saved = save_regions(regions, cloud)
elif event.type == pygame.MOUSEBUTTONDOWN:
clicked = None
for rect, action in buttons:
if rect.collidepoint(event.pos):
clicked = action
break
if event.button == 1 and clicked:
if clicked == "prev_pcd" and pcd_files:
selected_pcd = (selected_pcd - 1) % len(pcd_files)
cloud = load_pcd(pcd_files[selected_pcd])
fit_camera(camera, cloud, regions)
elif clicked == "next_pcd" and pcd_files:
selected_pcd = (selected_pcd + 1) % len(pcd_files)
cloud = load_pcd(pcd_files[selected_pcd])
fit_camera(camera, cloud, regions)
elif clicked == "fit":
fit_camera(camera, cloud, regions)
elif clicked == "save":
saved = save_regions(regions, cloud)
elif event.button == 1 and event.pos[0] < camera.map_width:
current.append(camera.screen_to_world(*event.pos))
elif event.button in (2, 3) or (event.button == 1 and event.pos[0] >= camera.map_width):
dragging = True
last_mouse = event.pos
elif event.button == 4 and event.pos[0] < camera.map_width:
camera.zoom_at(1.12, event.pos)
elif event.button == 5 and event.pos[0] < camera.map_width:
camera.zoom_at(1 / 1.12, event.pos)
elif event.type == pygame.MOUSEBUTTONUP:
dragging = False
elif event.type == pygame.MOUSEMOTION and dragging:
dx = event.pos[0] - last_mouse[0]
dy = event.pos[1] - last_mouse[1]
camera.pan_x += dx
camera.pan_y += dy
last_mouse = event.pos
screen.fill(COLOR_BG)
draw_grid(screen, camera, map_font)
draw_pcd(screen, camera, cloud)
draw_regions(screen, camera, regions, current)
buttons = draw_panel(screen, camera, title_font, small_font, pcd_files, selected_pcd, cloud, regions, current, saved)
pygame.display.flip()
clock.tick(60)
pygame.quit()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Deterministically downsample an ASCII PCD file to a byte-size limit."""
from __future__ import annotations
import argparse
import math
import os
from pathlib import Path
DEFAULT_MAX_BYTES = 9_900_000
def read_header(path: Path) -> tuple[list[bytes], int, int]:
header: list[bytes] = []
declared_points: int | None = None
with path.open("rb") as stream:
while True:
line = stream.readline()
if not line:
raise ValueError(f"PCD header has no DATA line: {path}")
header.append(line)
fields = line.strip().split(maxsplit=1)
if fields and fields[0].upper() == b"POINTS" and len(fields) == 2:
declared_points = int(fields[1])
if fields and fields[0].upper() == b"DATA":
if len(fields) != 2 or fields[1].lower() != b"ascii":
raise ValueError("Only DATA ascii PCD files are supported")
data_offset = stream.tell()
break
if declared_points is None:
raise ValueError(f"PCD header has no POINTS field: {path}")
return header, data_offset, declared_points
def render_header(header: list[bytes], point_count: int) -> bytes:
rendered: list[bytes] = []
replaced_width = False
replaced_points = False
for line in header:
newline = b"\r\n" if line.endswith(b"\r\n") else b"\n"
fields = line.strip().split(maxsplit=1)
key = fields[0].upper() if fields else b""
if key == b"WIDTH":
rendered.append(f"WIDTH {point_count}".encode("ascii") + newline)
replaced_width = True
elif key == b"POINTS":
rendered.append(f"POINTS {point_count}".encode("ascii") + newline)
replaced_points = True
elif key == b"HEIGHT":
rendered.append(b"HEIGHT 1" + newline)
else:
rendered.append(line)
if not replaced_width or not replaced_points:
raise ValueError("PCD header must contain WIDTH and POINTS fields")
return b"".join(rendered)
def measure_sample(
path: Path,
data_offset: int,
stride: int,
) -> tuple[int, int, int]:
offset = stride // 2
source_count = 0
selected_count = 0
selected_bytes = 0
with path.open("rb") as stream:
stream.seek(data_offset)
for line in stream:
if not line.strip():
continue
if source_count % stride == offset:
selected_count += 1
selected_bytes += len(line)
source_count += 1
return source_count, selected_count, selected_bytes
def write_sample(
source: Path,
output: Path,
header: bytes,
data_offset: int,
stride: int,
) -> None:
offset = stride // 2
point_index = 0
temporary = output.with_suffix(output.suffix + ".tmp")
output.parent.mkdir(parents=True, exist_ok=True)
try:
with source.open("rb") as src, temporary.open("wb") as dst:
dst.write(header)
src.seek(data_offset)
for line in src:
if not line.strip():
continue
if point_index % stride == offset:
dst.write(line)
point_index += 1
os.replace(temporary, output)
finally:
if temporary.exists():
temporary.unlink()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path, help="Source DATA ascii PCD file")
parser.add_argument("output", type=Path, help="Downsampled output PCD file")
parser.add_argument(
"--max-bytes",
type=int,
default=DEFAULT_MAX_BYTES,
help=f"Maximum output size in bytes (default: {DEFAULT_MAX_BYTES})",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
source = args.source.expanduser().resolve()
output = args.output.expanduser().resolve()
max_bytes = int(args.max_bytes)
if source == output:
raise ValueError("Source and output paths must be different")
if max_bytes <= 8192:
raise ValueError("--max-bytes must be greater than 8192")
header, data_offset, declared_points = read_header(source)
data_bytes = source.stat().st_size - data_offset
payload_budget = max_bytes - 8192
stride = max(1, math.ceil(data_bytes / payload_budget))
while True:
source_count, selected_count, selected_bytes = measure_sample(
source,
data_offset,
stride,
)
if source_count != declared_points:
raise ValueError(
f"POINTS declares {declared_points}, but {source_count} data rows were read"
)
output_header = render_header(header, selected_count)
if len(output_header) + selected_bytes <= max_bytes:
break
stride += 1
write_sample(source, output, output_header, data_offset, stride)
output_bytes = output.stat().st_size
if output_bytes > max_bytes:
raise RuntimeError(f"Generated file exceeds limit: {output_bytes} > {max_bytes}")
print(f"source={source}")
print(f"output={output}")
print(f"source_points={source_count}")
print(f"output_points={selected_count}")
print(f"stride={stride}")
print(f"output_bytes={output_bytes}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Mirror a nav_tools terrain XML and route JSON together."""
from __future__ import annotations
import argparse
import copy
import json
import math
import time
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
TOOL_DIR = Path(__file__).resolve().parent
XML_DIR = TOOL_DIR / "xml"
POINTS_DIR = TOOL_DIR / "points"
DEFAULT_XML = XML_DIR / "1hao.xml"
DEFAULT_POINTS = POINTS_DIR / "points_20260715_120154.json"
def format_float(value: float) -> str:
if abs(value) < 0.0000005:
value = 0.0
return f"{value:.6f}".rstrip("0").rstrip(".")
def parse_float_tuple(text: str | None) -> tuple[float, ...]:
if not text:
return ()
return tuple(float(item) for item in text.split())
def mirror_coord(value: float, axis_value: float) -> float:
return 2.0 * axis_value - value
def normalize_yaw_deg(value: float) -> float:
while value > 180.0:
value -= 360.0
while value <= -180.0:
value += 360.0
return value
def mirror_yaw_deg(value: float, axis: str) -> float:
if axis == "x":
return normalize_yaw_deg(180.0 - value)
return normalize_yaw_deg(-value)
def mirror_quat(quat: tuple[float, ...], axis: str) -> tuple[float, float, float, float]:
if axis == "x":
return quat[0], quat[1], -quat[2], quat[3]
return quat[0], -quat[1], quat[2], quat[3]
def mirror_xml(source: Path, target: Path, axis: str, axis_value: float) -> None:
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
tree = ET.parse(source, parser=parser)
mirrored = copy.deepcopy(tree)
root = mirrored.getroot()
model_name = root.get("model", source.stem)
if "mirror" not in model_name.lower():
root.set("model", f"{model_name} mirror")
for element in root.iter():
pos = parse_float_tuple(element.get("pos"))
if len(pos) >= 1:
mirrored_pos = list(pos)
coord_index = 0 if axis == "x" else 1
if len(mirrored_pos) > coord_index:
mirrored_pos[coord_index] = mirror_coord(mirrored_pos[coord_index], axis_value)
element.set("pos", " ".join(format_float(value) for value in mirrored_pos))
center = parse_float_tuple(element.get("center"))
if len(center) >= 1:
mirrored_center = list(center)
coord_index = 0 if axis == "x" else 1
if len(mirrored_center) > coord_index:
mirrored_center[coord_index] = mirror_coord(mirrored_center[coord_index], axis_value)
element.set("center", " ".join(format_float(value) for value in mirrored_center))
quat = parse_float_tuple(element.get("quat"))
if len(quat) == 4:
mirrored_quat = mirror_quat(quat, axis)
element.set("quat", " ".join(format_float(value) for value in mirrored_quat))
ET.indent(mirrored, space=" ")
target.parent.mkdir(parents=True, exist_ok=True)
mirrored.write(target, encoding="utf-8", xml_declaration=False)
def mirror_point_fields(row: dict[str, Any], axis: str, axis_value: float) -> None:
coord_keys = ("x", "world_x") if axis == "x" else ("y", "world_y")
for key in coord_keys:
if key in row and isinstance(row[key], (int, float)):
row[key] = mirror_coord(float(row[key]), axis_value)
if "yawDeg" in row and isinstance(row["yawDeg"], (int, float)):
row["yawDeg"] = mirror_yaw_deg(float(row["yawDeg"]), axis)
if "yaw" in row and isinstance(row["yaw"], (int, float)):
row["yaw"] = mirror_yaw_deg(float(row["yaw"]), axis)
def mirror_polygon(points: Any, axis: str, axis_value: float) -> None:
if not isinstance(points, list):
return
for point in points:
if isinstance(point, dict):
key = "x" if axis == "x" else "y"
if isinstance(point.get(key), (int, float)):
point[key] = mirror_coord(float(point[key]), axis_value)
elif isinstance(point, list):
coord_index = 0 if axis == "x" else 1
if len(point) > coord_index and isinstance(point[coord_index], (int, float)):
point[coord_index] = mirror_coord(float(point[coord_index]), axis_value)
def mirror_regions(payload: dict[str, Any], axis: str, axis_value: float) -> None:
for key in ("regions", "avoid_regions"):
rows = payload.get(key)
if not isinstance(rows, list):
continue
for region in rows:
if isinstance(region, dict):
mirror_polygon(region.get("polygon", region.get("points")), axis, axis_value)
def mirror_waypoint_rows(rows: Any, axis: str, axis_value: float) -> None:
if not isinstance(rows, list):
return
for row in rows:
if isinstance(row, dict):
mirror_point_fields(row, axis, axis_value)
def mirror_points_json(source: Path, target: Path, axis: str, axis_value: float, mirrored_xml: Path | None) -> None:
payload = json.loads(source.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise ValueError(f"Expected top-level JSON object: {source}")
payload["name"] = f"{payload.get('name', source.stem)}_mirror"
payload["mirroredFrom"] = source.name
payload["mirror"] = {
"axis": axis,
f"axis_{axis}": axis_value,
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%S"),
}
if mirrored_xml is not None:
payload["xml"] = f"xml/{mirrored_xml.name}"
origin = payload.get("origin")
if isinstance(origin, dict):
coord_key = "x" if axis == "x" else "y"
if isinstance(origin.get(coord_key), (int, float)):
origin[coord_key] = mirror_coord(float(origin[coord_key]), axis_value)
if isinstance(origin.get("yaw_deg"), (int, float)):
origin["yaw_deg"] = mirror_yaw_deg(float(origin["yaw_deg"]), axis)
origin["yaw_rad"] = math.radians(float(origin["yaw_deg"]))
elif isinstance(origin.get("yaw_rad"), (int, float)):
origin["yaw_rad"] = math.radians(mirror_yaw_deg(math.degrees(float(origin["yaw_rad"])), axis))
origin["yaw_deg"] = math.degrees(float(origin["yaw_rad"]))
mirror_waypoint_rows(payload.get("waypoints"), axis, axis_value)
segments = payload.get("segments")
if isinstance(segments, list):
for segment in segments:
if isinstance(segment, dict):
mirror_waypoint_rows(segment.get("waypoints"), axis, axis_value)
mirror_regions(payload, axis, axis_value)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
def default_output_path(source: Path, suffix: str) -> Path:
return source.with_name(f"{source.stem}{suffix}{source.suffix}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Mirror a nav_tools XML and matching points JSON.")
parser.add_argument("--xml", type=Path, default=DEFAULT_XML, help="Input XML path.")
parser.add_argument("--points", type=Path, default=DEFAULT_POINTS, help="Input points JSON path.")
parser.add_argument("--mirror-axis", choices=("x", "y"), default="x", help="Coordinate axis to mirror.")
parser.add_argument("--axis-x", type=float, default=0.0, help="Mirror axis x value. Default: 0.")
parser.add_argument("--axis-y", type=float, default=0.0, help="Mirror axis y value.")
parser.add_argument("--out-xml", type=Path, help="Output XML path. Default: <input>_mirror.xml.")
parser.add_argument("--out-points", type=Path, help="Output JSON path. Default: <input>_mirror.json.")
return parser.parse_args()
def main() -> int:
args = parse_args()
xml_path = args.xml if args.xml.is_absolute() else TOOL_DIR / args.xml
points_path = args.points if args.points.is_absolute() else TOOL_DIR / args.points
out_xml = args.out_xml if args.out_xml else default_output_path(xml_path, "_mirror")
out_points = args.out_points if args.out_points else default_output_path(points_path, "_mirror")
if not out_xml.is_absolute():
out_xml = TOOL_DIR / out_xml
if not out_points.is_absolute():
out_points = TOOL_DIR / out_points
axis_value = args.axis_x if args.mirror_axis == "x" else args.axis_y
mirror_xml(xml_path, out_xml, args.mirror_axis, axis_value)
mirror_points_json(points_path, out_points, args.mirror_axis, axis_value, out_xml)
print(f"XML: {out_xml}")
print(f"JSON: {out_points}")
print(f"axis_{args.mirror_axis}: {axis_value}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Transform ASCII PCD maps by XY translation and yaw rotation.
Examples:
python pcd_transform_tool.py pcd/1hao.pcd --dx -9.34 --dy -0.88 --yaw-deg -90 --output pcd/1hao_tf.pcd
python pcd_transform_tool.py pcd/1hao.pcd --yaw-deg 90 --in-place
"""
from __future__ import annotations
import argparse
import math
import shutil
import time
from pathlib import Path
TOOL_DIR = Path(__file__).resolve().parent
def tool_relative(path: Path) -> Path:
if path.is_absolute():
raise ValueError("Use a path relative to this nav_tools folder.")
return TOOL_DIR / path
def format_float(value: float) -> str:
if abs(value) < 5e-10:
value = 0.0
return f"{value:.8f}".rstrip("0").rstrip(".")
def transform_xy(x: float, y: float, dx: float, dy: float, yaw_rad: float) -> tuple[float, float]:
tx = x + dx
ty = y + dy
c = math.cos(yaw_rad)
s = math.sin(yaw_rad)
return tx * c - ty * s, tx * s + ty * c
def transform_ascii_pcd(source: Path, target: Path, dx: float, dy: float, yaw_deg: float) -> int:
yaw_rad = math.radians(yaw_deg)
data_started = False
transformed = 0
with source.open("r", encoding="utf-8", errors="ignore") as fin, target.open("w", encoding="utf-8", newline="\n") as fout:
for raw in fin:
line = raw.strip()
if data_started and line:
parts = line.split()
if len(parts) >= 2:
x, y = transform_xy(float(parts[0]), float(parts[1]), dx, dy, yaw_rad)
parts[0] = format_float(x)
parts[1] = format_float(y)
fout.write(" ".join(parts) + "\n")
transformed += 1
else:
fout.write(raw)
else:
fout.write(raw)
if line.upper().startswith("DATA"):
if "ascii" not in line.lower():
raise RuntimeError(f"Only ASCII PCD is supported: {source}")
data_started = True
if transformed == 0:
raise RuntimeError(f"No points transformed: {source}")
return transformed
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Translate and yaw-rotate an ASCII PCD file.")
parser.add_argument("pcd", type=Path, help="Input PCD path relative to this nav_tools folder.")
parser.add_argument("--dx", type=float, default=0.0, help="X translation before rotation, meters.")
parser.add_argument("--dy", type=float, default=0.0, help="Y translation before rotation, meters.")
parser.add_argument("--yaw-deg", type=float, default=0.0, help="Yaw rotation after translation, degrees.")
parser.add_argument("--output", type=Path, help="Output PCD path relative to this nav_tools folder.")
parser.add_argument("--in-place", action="store_true", help="Overwrite input PCD after creating a .bak copy.")
return parser.parse_args()
def main() -> int:
args = parse_args()
source = tool_relative(args.pcd)
if not source.exists():
raise FileNotFoundError(source)
if args.in_place:
target = source.with_suffix(source.suffix + ".tmp")
elif args.output:
target = tool_relative(args.output)
else:
target = source.with_name(f"{source.stem}_tf{source.suffix}")
target.parent.mkdir(parents=True, exist_ok=True)
count = transform_ascii_pcd(source, target, args.dx, args.dy, args.yaw_deg)
if args.in_place:
backup = source.with_suffix(source.suffix + f".bak_{time.strftime('%Y%m%d_%H%M%S')}")
shutil.copy2(source, backup)
target.replace(source)
print(f"Transformed {count} points in-place: {source}")
print(f"Backup: {backup}")
else:
print(f"Transformed {count} points: {target}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
# 航点路线演进
本目录按原始文件名保存导航打点阶段的路线快照。文件名中的 `new``real``reall` 等是开发期原名,仅表示当时的迭代语境,不统一解释为正式版本或最终验证状态。
| 源文件时间 | 文件 | 航点 | 避障区 | 绑定 XML |
| --- | --- | ---: | ---: | --- |
| 2026-07-10 23:15 | `1B_FF.json` | 48 | 26 | `B_C.xml` |
| 2026-07-11 01:35 | `A_FFF.json` | 46 | 26 | `A_C.xml` |
| 2026-07-12 23:55 | `1hao_wallspeed08_桥A适用于重定位.json` | 49 | 19 | `1hao.xml` |
| 2026-07-13 00:10 | `1hao_not_raogan.json` | 32 | 19 | `1hao.xml` |
| 2026-07-13 20:36 | `1hao_n.json` | 48 | 19 | `1hao.xml` |
| 2026-07-14 09:57 | `1hao_new.json` | 47 | 19 | `1hao.xml` |
| 2026-07-14 11:20 | `1hao_bridge_a_crawl.json` | 47 | 19 | `1hao.xml` |
| 2026-07-14 12:01 | `1hao_crawl2.json` | 47 | 19 | `1hao.xml` |
| 2026-07-14 12:16 | `1hao_new1.json` | 47 | 19 | `A_C.xml` |
| 2026-07-14 12:19 | `1hao_jianyi.json` | 47 | 19 | `1hao.xml` |
| 2026-07-15 11:42 | `1hao_real.json` | 30 | 19 | `1hao.xml` |
| 2026-07-15 11:43 | `2hao_not_raogan.json` | 29 | 21 | `2hao.xml` |
| 2026-07-15 12:01 | `1hao_reall.json` | 29 | 19 | `1hao.xml` |
| 2026-07-15 12:01 | `points_20260715_120154.json` | 29 | 19 | `1hao.xml` |
这些文件各有参数和路线差异,全部保留以呈现打点演进。默认工具入口使用时间戳文件 `points_20260715_120154.json`;它不是对其他文件的替代,其他路线仍可通过 `--points` 或界面下拉框载入。
默认路线在当前保守圆形包络检查下存在 3 组净空不足记录,因此这里称为“默认归档路线”,不额外标记为几何安全最终版。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
@@ -0,0 +1,2 @@
pygame>=2.6.1
Pillow>=10.0.0
@@ -0,0 +1,558 @@
#!/usr/bin/env python3
"""Generate safer waypoint candidates for nav_tools route JSON files.
The optimizer keeps the original JSON intact, adjusts copied waypoints away
from avoid polygons, recomputes yaw, and writes candidate JSON files for
offline/sim2sim validation.
"""
from __future__ import annotations
import argparse
import copy
import json
import math
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from route_safety_check import (
AvoidRegion,
Waypoint,
analyze_route,
default_lateral_footprint_radius,
load_regions,
load_waypoints,
point_in_polygon,
point_segment_distance,
)
@dataclass
class MutablePoint:
id: str
x: float
y: float
yaw_deg: float
speed: float | None
policy: str
tolerance: float | None
row_refs: list[dict[str, Any]]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate safer route candidates.")
parser.add_argument("--points", type=Path, default=Path("tools/nav_tools/points/points_20260715_120154.json"))
parser.add_argument("--out-dir", type=Path, default=Path("tools/nav_tools/points/auto_candidates"))
parser.add_argument("--avoid-margin", type=float, default=0.05)
parser.add_argument("--extra-margin", type=float, default=0.08)
parser.add_argument("--footprint-radius", type=float, default=None)
parser.add_argument("--iterations", type=int, default=240)
parser.add_argument("--step-scale", type=float, default=0.45)
parser.add_argument("--max-move-per-iter", type=float, default=0.08)
parser.add_argument("--max-total-move", type=float, default=0.55)
parser.add_argument("--smooth-weight", type=float, default=0.12)
parser.add_argument("--lock-ends", action="store_true", default=True)
parser.add_argument("--no-lock-ends", dest="lock_ends", action="store_false")
parser.add_argument("--slow-near-risk", action="store_true", default=True)
parser.add_argument("--no-slow-near-risk", dest="slow_near_risk", action="store_false")
parser.add_argument("--min-speed", type=float, default=0.22)
parser.add_argument("--max-segment-length", type=float, default=0.65)
parser.add_argument(
"--focus-start-index",
type=int,
default=None,
help="1-based first waypoint index allowed to move; points outside the focus range are locked.",
)
parser.add_argument(
"--focus-end-index",
type=int,
default=None,
help="1-based last waypoint index allowed to move; points outside the focus range are locked.",
)
parser.add_argument(
"--policy-range",
action="append",
default=[],
metavar="START:END:POLICY",
help="Override waypoint policy by 1-based inclusive index range, e.g. 21:26:crawl. Can be repeated.",
)
parser.add_argument(
"--speed-range",
action="append",
default=[],
metavar="START:END:SPEED",
help="Override waypoint speed by 1-based inclusive index range, e.g. 21:26:0.22. Can be repeated.",
)
parser.add_argument(
"--shift-range",
action="append",
default=[],
metavar="START:END:DX:DY",
help="Shift waypoint positions by 1-based inclusive index range. Applied after optimization.",
)
parser.add_argument("--top", type=int, default=12)
return parser.parse_args()
def load_payload(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as f:
payload = json.load(f)
if not isinstance(payload, dict):
raise ValueError(f"JSON root must be object: {path}")
return payload
def iter_waypoint_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
if isinstance(payload.get("waypoints"), list):
rows.extend(row for row in payload["waypoints"] if isinstance(row, dict))
if isinstance(payload.get("segments"), list):
for segment in payload["segments"]:
if isinstance(segment, dict) and isinstance(segment.get("waypoints"), list):
rows.extend(row for row in segment["waypoints"] if isinstance(row, dict))
return rows
def load_mutable_points(payload: dict[str, Any]) -> list[MutablePoint]:
unique: list[MutablePoint] = []
refs_by_key: dict[tuple[str, float, float], MutablePoint] = {}
for index, row in enumerate(iter_waypoint_rows(payload), start=1):
x = float(row.get("world_x", row.get("x", 0.0)))
y = float(row.get("world_y", row.get("y", 0.0)))
key = (str(row.get("id", index)), round(x, 9), round(y, 9))
point = refs_by_key.get(key)
if point is None:
point = MutablePoint(
id=str(row.get("id", index)),
x=x,
y=y,
yaw_deg=float(row.get("yawDeg", row.get("yaw_deg", row.get("yaw", 0.0)))),
speed=float(row["speed"]) if row.get("speed") is not None else None,
policy=str(row.get("policy", "rough")),
tolerance=float(row["tolerance"]) if row.get("tolerance") is not None else None,
row_refs=[],
)
refs_by_key[key] = point
unique.append(point)
point.row_refs.append(row)
if len(unique) < 2:
raise ValueError("Route must contain at least two unique waypoints")
return unique
def mutable_to_waypoints(points: list[MutablePoint]) -> list[Waypoint]:
return [
Waypoint(
index=i,
id=p.id,
x=p.x,
y=p.y,
yaw_deg=p.yaw_deg,
speed=p.speed,
policy=p.policy,
tolerance=p.tolerance,
)
for i, p in enumerate(points, start=1)
]
def closest_point_on_segment(px: float, py: float, ax: float, ay: float, bx: float, by: float) -> tuple[float, float]:
dx = bx - ax
dy = by - ay
length_sq = dx * dx + dy * dy
if length_sq <= 1.0e-12:
return ax, ay
t = ((px - ax) * dx + (py - ay) * dy) / length_sq
t = max(0.0, min(1.0, t))
return ax + t * dx, ay + t * dy
def closest_segment_polygon_pair(
ax: float,
ay: float,
bx: float,
by: float,
polygon: tuple[tuple[float, float], ...],
) -> tuple[float, float, float, float, float]:
best = (float("inf"), ax, ay, polygon[0][0], polygon[0][1])
samples = 17
for i in range(samples):
t = i / (samples - 1)
sx = ax + (bx - ax) * t
sy = ay + (by - ay) * t
for px, py in polygon:
dist = math.hypot(sx - px, sy - py)
if dist < best[0]:
best = (dist, sx, sy, px, py)
for (ex0, ey0), (ex1, ey1) in zip(polygon, polygon[1:] + polygon[:1]):
qx, qy = closest_point_on_segment(sx, sy, ex0, ey0, ex1, ey1)
dist = math.hypot(sx - qx, sy - qy)
if dist < best[0]:
best = (dist, sx, sy, qx, qy)
for sx, sy in ((ax, ay), (bx, by)):
for (ex0, ey0), (ex1, ey1) in zip(polygon, polygon[1:] + polygon[:1]):
qx, qy = closest_point_on_segment(sx, sy, ex0, ey0, ex1, ey1)
dist = math.hypot(sx - qx, sy - qy)
if dist < best[0]:
best = (dist, sx, sy, qx, qy)
return best
def polygon_centroid(polygon: tuple[tuple[float, float], ...]) -> tuple[float, float]:
return (
sum(x for x, _ in polygon) / len(polygon),
sum(y for _, y in polygon) / len(polygon),
)
def point_inside_any_region(x: float, y: float, regions: list[AvoidRegion]) -> bool:
return any(point_in_polygon(x, y, region.polygon) for region in regions)
def nearest_region_clearance(x: float, y: float, regions: list[AvoidRegion]) -> float:
best = float("inf")
for region in regions:
if point_in_polygon(x, y, region.polygon):
return 0.0
polygon = region.polygon
for (ax, ay), (bx, by) in zip(polygon, polygon[1:] + polygon[:1]):
best = min(best, point_segment_distance(x, y, ax, ay, bx, by))
return best
def optimize_points(
points: list[MutablePoint],
regions: list[AvoidRegion],
required_clearance: float,
args: argparse.Namespace,
) -> tuple[list[MutablePoint], dict[str, Any]]:
original = [(p.x, p.y) for p in points]
target_clearance = required_clearance + float(args.extra_margin)
locked = {0, len(points) - 1} if args.lock_ends else set()
if args.focus_start_index is not None or args.focus_end_index is not None:
focus_start = max(1, int(args.focus_start_index or 1)) - 1
focus_end = min(len(points), int(args.focus_end_index or len(points))) - 1
if focus_start > focus_end:
raise ValueError(
f"Invalid focus range: start={args.focus_start_index} end={args.focus_end_index}"
)
locked.update(i for i in range(len(points)) if i < focus_start or i > focus_end)
risk_hit_counts = [0 for _ in points]
for _ in range(int(args.iterations)):
deltas = [[0.0, 0.0] for _ in points]
weights = [0.0 for _ in points]
waypoints = mutable_to_waypoints(points)
risks = analyze_route(waypoints, regions, target_clearance)
active_risks = [risk for risk in risks if risk.margin_m < 0.0]
if not active_risks:
break
id_to_index = {p.id: i for i, p in enumerate(points)}
for risk in active_risks:
ia = id_to_index.get(risk.start_id)
ib = id_to_index.get(risk.end_id)
region = next((item for item in regions if item.name == risk.region), None)
if ia is None or ib is None or region is None:
continue
ax, ay = points[ia].x, points[ia].y
bx, by = points[ib].x, points[ib].y
dist, sx, sy, qx, qy = closest_segment_polygon_pair(ax, ay, bx, by, region.polygon)
vx = sx - qx
vy = sy - qy
norm = math.hypot(vx, vy)
if norm < 1.0e-6 or risk.centerline_intersects:
cx, cy = polygon_centroid(region.polygon)
mx = 0.5 * (ax + bx)
my = 0.5 * (ay + by)
vx = mx - cx
vy = my - cy
norm = math.hypot(vx, vy)
if norm < 1.0e-6:
vx, vy, norm = 1.0, 0.0, 1.0
ux = vx / norm
uy = vy / norm
push = min(float(args.max_move_per_iter), max(0.0, target_clearance - dist) * float(args.step_scale))
for idx in (ia, ib):
if idx in locked:
continue
deltas[idx][0] += ux * push
deltas[idx][1] += uy * push
weights[idx] += 1.0
risk_hit_counts[idx] += 1
if args.smooth_weight > 0.0 and len(points) > 2:
for i in range(1, len(points) - 1):
if i in locked:
continue
avg_x = 0.5 * (points[i - 1].x + points[i + 1].x)
avg_y = 0.5 * (points[i - 1].y + points[i + 1].y)
deltas[i][0] += (avg_x - points[i].x) * float(args.smooth_weight)
deltas[i][1] += (avg_y - points[i].y) * float(args.smooth_weight)
weights[i] += 1.0
for i, point in enumerate(points):
if i in locked or weights[i] <= 0.0:
continue
dx = deltas[i][0] / weights[i]
dy = deltas[i][1] / weights[i]
ox, oy = original[i]
next_x = point.x + dx
next_y = point.y + dy
total_dx = next_x - ox
total_dy = next_y - oy
total = math.hypot(total_dx, total_dy)
if total > float(args.max_total_move):
scale = float(args.max_total_move) / total
next_x = ox + total_dx * scale
next_y = oy + total_dy * scale
point.x = next_x
point.y = next_y
for i, point in enumerate(points[:-1]):
nxt = points[i + 1]
point.yaw_deg = math.degrees(math.atan2(nxt.y - point.y, nxt.x - point.x))
points[-1].yaw_deg = points[-2].yaw_deg
if args.slow_near_risk:
for i, point in enumerate(points):
clearance = nearest_region_clearance(point.x, point.y, regions)
if clearance < target_clearance + 0.12:
current = point.speed if point.speed is not None else 0.35
point.speed = max(float(args.min_speed), min(current, 0.28))
if risk_hit_counts[i] > 0:
current = point.speed if point.speed is not None else 0.35
point.speed = max(float(args.min_speed), min(current, 0.25))
final_risks = analyze_route(mutable_to_waypoints(points), regions, required_clearance)
violations = [risk for risk in final_risks if risk.margin_m < 0.0 or risk.centerline_intersects]
moves = [
{
"id": point.id,
"dx": round(point.x - ox, 4),
"dy": round(point.y - oy, 4),
"dist": round(math.hypot(point.x - ox, point.y - oy), 4),
"risk_hits": risk_hit_counts[i],
}
for i, (point, (ox, oy)) in enumerate(zip(points, original))
if math.hypot(point.x - ox, point.y - oy) > 1.0e-4 or risk_hit_counts[i] > 0
]
return points, {
"violations": len(violations),
"min_margin": round(final_risks[0].margin_m, 6) if final_risks else None,
"focus_range": [
args.focus_start_index,
args.focus_end_index,
],
"moves": moves,
"top_risks": [
{
"start_id": risk.start_id,
"end_id": risk.end_id,
"region": risk.region,
"margin_m": round(risk.margin_m, 6),
"clearance_m": round(risk.clearance_m, 6),
"centerline_intersects": risk.centerline_intersects,
}
for risk in final_risks[: int(args.top)]
],
}
def apply_policy_ranges(points: list[MutablePoint], ranges: list[str]) -> list[dict[str, Any]]:
applied: list[dict[str, Any]] = []
for item in ranges:
parts = [part.strip() for part in str(item).split(":")]
if len(parts) != 3 or not parts[2]:
raise ValueError(f"Invalid --policy-range {item!r}; expected START:END:POLICY")
start = max(1, int(parts[0]))
end = min(len(points), int(parts[1]))
if start > end:
raise ValueError(f"Invalid --policy-range {item!r}; start is after end")
policy = parts[2]
for index in range(start - 1, end):
points[index].policy = policy
applied.append({"start": start, "end": end, "policy": policy})
return applied
def apply_speed_ranges(points: list[MutablePoint], ranges: list[str]) -> list[dict[str, Any]]:
applied: list[dict[str, Any]] = []
for item in ranges:
parts = [part.strip() for part in str(item).split(":")]
if len(parts) != 3:
raise ValueError(f"Invalid --speed-range {item!r}; expected START:END:SPEED")
start = max(1, int(parts[0]))
end = min(len(points), int(parts[1]))
if start > end:
raise ValueError(f"Invalid --speed-range {item!r}; start is after end")
speed = float(parts[2])
for index in range(start - 1, end):
points[index].speed = speed
applied.append({"start": start, "end": end, "speed": speed})
return applied
def apply_shift_ranges(points: list[MutablePoint], ranges: list[str]) -> list[dict[str, Any]]:
applied: list[dict[str, Any]] = []
for item in ranges:
parts = [part.strip() for part in str(item).split(":")]
if len(parts) != 4:
raise ValueError(f"Invalid --shift-range {item!r}; expected START:END:DX:DY")
start = max(1, int(parts[0]))
end = min(len(points), int(parts[1]))
if start > end:
raise ValueError(f"Invalid --shift-range {item!r}; start is after end")
dx = float(parts[2])
dy = float(parts[3])
for index in range(start - 1, end):
points[index].x += dx
points[index].y += dy
applied.append({"start": start, "end": end, "dx": dx, "dy": dy})
if applied:
for i, point in enumerate(points[:-1]):
nxt = points[i + 1]
point.yaw_deg = math.degrees(math.atan2(nxt.y - point.y, nxt.x - point.x))
points[-1].yaw_deg = points[-2].yaw_deg
return applied
def refresh_safety_summary(
summary: dict[str, Any],
points: list[MutablePoint],
regions: list[AvoidRegion],
required_clearance: float,
args: argparse.Namespace,
) -> None:
final_risks = analyze_route(mutable_to_waypoints(points), regions, required_clearance)
violations = [risk for risk in final_risks if risk.margin_m < 0.0 or risk.centerline_intersects]
summary["violations"] = len(violations)
summary["min_margin"] = round(final_risks[0].margin_m, 6) if final_risks else None
summary["top_risks"] = [
{
"start_id": risk.start_id,
"end_id": risk.end_id,
"region": risk.region,
"margin_m": round(risk.margin_m, 6),
"clearance_m": round(risk.clearance_m, 6),
"centerline_intersects": risk.centerline_intersects,
}
for risk in final_risks[: int(args.top)]
]
def apply_points_to_payload(payload: dict[str, Any], points: list[MutablePoint]) -> None:
for point in points:
for row in point.row_refs:
if "world_x" in row:
row["world_x"] = point.x
row["x"] = point.x
if "world_y" in row:
row["world_y"] = point.y
row["y"] = point.y
row["yawDeg"] = point.yaw_deg
if point.speed is not None:
row["speed"] = point.speed
if point.tolerance is not None:
row["tolerance"] = point.tolerance
if point.policy:
row["policy"] = point.policy
def densify_segments(payload: dict[str, Any], max_len: float) -> None:
if max_len <= 0.0:
return
if not isinstance(payload.get("segments"), list):
return
for segment in payload["segments"]:
rows = segment.get("waypoints") if isinstance(segment, dict) else None
if not isinstance(rows, list) or len(rows) < 2:
continue
new_rows: list[dict[str, Any]] = []
for a, b in zip(rows, rows[1:]):
new_rows.append(a)
ax, ay = float(a.get("x", 0.0)), float(a.get("y", 0.0))
bx, by = float(b.get("x", 0.0)), float(b.get("y", 0.0))
dist = math.hypot(bx - ax, by - ay)
inserts = max(0, int(math.ceil(dist / max_len)) - 1)
for j in range(inserts):
t = (j + 1) / (inserts + 1)
row = copy.deepcopy(a)
row["id"] = f"{a.get('id')}_{j + 1}"
row["x"] = ax + (bx - ax) * t
row["y"] = ay + (by - ay) * t
row["yawDeg"] = math.degrees(math.atan2(by - ay, bx - ax))
if a.get("speed") is not None and b.get("speed") is not None:
row["speed"] = min(float(a["speed"]), float(b["speed"]))
new_rows.append(row)
new_rows.append(rows[-1])
segment["waypoints"] = new_rows
if isinstance(payload.get("waypoints"), list):
flat: list[dict[str, Any]] = []
for segment in payload["segments"]:
rows = segment.get("waypoints") if isinstance(segment, dict) else None
if isinstance(rows, list):
flat.extend(copy.deepcopy(row) for row in rows if isinstance(row, dict))
if flat:
payload["waypoints"] = flat
def main() -> int:
args = parse_args()
payload = load_payload(args.points)
regions = load_regions(payload)
points = load_mutable_points(payload)
footprint = float(args.footprint_radius) if args.footprint_radius is not None else default_lateral_footprint_radius()
required = footprint + float(args.avoid_margin)
candidate_payload = copy.deepcopy(payload)
candidate_points = load_mutable_points(candidate_payload)
optimized, summary = optimize_points(candidate_points, regions, required, args)
shift_overrides = apply_shift_ranges(optimized, list(args.shift_range))
policy_overrides = apply_policy_ranges(optimized, list(args.policy_range))
speed_overrides = apply_speed_ranges(optimized, list(args.speed_range))
if shift_overrides:
summary["shift_overrides"] = shift_overrides
if policy_overrides:
summary["policy_overrides"] = policy_overrides
if speed_overrides:
summary["speed_overrides"] = speed_overrides
if shift_overrides or policy_overrides or speed_overrides:
refresh_safety_summary(summary, optimized, regions, required, args)
apply_points_to_payload(candidate_payload, optimized)
densify_segments(candidate_payload, float(args.max_segment_length))
stamp = time.strftime("%Y%m%d_%H%M%S")
args.out_dir.mkdir(parents=True, exist_ok=True)
out_path = args.out_dir / f"{args.points.stem}_auto_{stamp}.json"
candidate_payload["name"] = f"{payload.get('name', args.points.stem)}_auto_{stamp}"
candidate_payload["autoOptimize"] = {
"source": str(args.points),
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%S"),
"footprintRadius": footprint,
"avoidMargin": args.avoid_margin,
"extraMargin": args.extra_margin,
"summary": summary,
}
out_path.write_text(json.dumps(candidate_payload, ensure_ascii=False, indent=2), encoding="utf-8")
print("Route candidate optimizer")
print(f" source: {args.points}")
print(f" output: {out_path}")
print(f" required_clearance: {required:.3f} m, target: {required + args.extra_margin:.3f} m")
print(f" final_violations: {summary['violations']}")
print(f" min_margin: {summary['min_margin']} m")
print(f" moved_points: {len(summary['moves'])}")
for item in summary["top_risks"][: int(args.top)]:
print(
" risk "
f"{item['start_id']}->{item['end_id']} {item['region']} "
f"margin={item['margin_m']:.3f} clearance={item['clearance_m']:.3f}"
)
return 0 if summary["violations"] == 0 else 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -90,13 +90,13 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--points",
type=Path,
default=Path("tools/nav_tools/points/points_20260705_174627.json"),
default=Path("tools/nav_tools/points/points_20260715_120154.json"),
help="Route JSON exported by nav_map_viewer.",
)
parser.add_argument(
"--xml",
type=Path,
default=Path("tools/nav_tools/xml/A.xml"),
default=Path("tools/nav_tools/xml/1hao.xml"),
help="Optional MuJoCo terrain XML used for metadata checks.",
)
parser.add_argument(
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""Run repeatable safety and sim2sim experiments for a route candidate."""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[2]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run route safety + sim2sim experiment suite.")
parser.add_argument("--points", type=Path, required=True)
parser.add_argument("--terrain-xml", type=Path, default=PROJECT_ROOT / "tools/nav_tools/xml/1hao.xml")
parser.add_argument("--onnx", type=Path, default=PROJECT_ROOT / "model_6800.onnx")
parser.add_argument("--out-dir", type=Path, default=PROJECT_ROOT / "sim2sim/route_experiments")
parser.add_argument("--start-yaw-offset-deg", type=float, default=-180.0)
parser.add_argument("--heading-offset-deg", type=float, default=180.0)
parser.add_argument("--start-z", type=float, default=0.75)
parser.add_argument("--settle-steps", type=int, default=500)
parser.add_argument("--follower", choices=("waypoint", "pure-pursuit"), default="pure-pursuit")
parser.add_argument("--lookahead", type=float, default=0.45)
parser.add_argument("--max-vx", type=float, default=0.22)
parser.add_argument("--min-cmd-vx", type=float, default=0.04)
parser.add_argument("--creep-cmd-vx", type=float, default=0.04)
parser.add_argument("--yaw-stop-threshold-deg", type=float, default=45.0)
parser.add_argument("--turn-in-place-enter-deg", type=float, default=70.0)
parser.add_argument("--cmd-vx-scale", type=float, default=1.0)
parser.add_argument("--no-local-safety", action="store_true")
parser.add_argument("--include-reverse-vx", action="store_true")
parser.add_argument("--quick", action="store_true", help="Use shorter timeouts for fast iteration.")
return parser.parse_args()
def run_command(args: list[str], cwd: Path) -> tuple[int, str]:
proc = subprocess.run(
args,
cwd=str(cwd),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
return proc.returncode, proc.stdout
def newest_report(out_dir: Path) -> Path | None:
reports = sorted(out_dir.glob("route_check_*/report.json"), key=lambda p: p.stat().st_mtime, reverse=True)
return reports[0] if reports else None
def load_report(path: Path | None) -> dict[str, Any]:
if path is None:
return {}
return json.loads(path.read_text(encoding="utf-8"))
def run_sim_case(
name: str,
args: argparse.Namespace,
extra: list[str],
max_time: int,
waypoint_timeout: int,
) -> dict[str, Any]:
case_out = args.out_dir / name
case_out.mkdir(parents=True, exist_ok=True)
cmd = [
sys.executable,
"sim2sim/nav_route_sim2sim_check.py",
"--terrain-xml",
str(args.terrain_xml),
"--points",
str(args.points),
"--onnx",
str(args.onnx),
"--policy-backend",
"policy-runner",
"--mission",
"json",
"--start-yaw-offset-deg",
str(args.start_yaw_offset_deg),
"--heading-offset-deg",
str(args.heading_offset_deg),
"--start-z",
str(args.start_z),
"--settle-steps",
str(args.settle_steps),
"--follower",
str(args.follower),
"--lookahead",
str(args.lookahead),
"--max-vx",
str(args.max_vx),
"--min-cmd-vx",
str(args.min_cmd_vx),
"--creep-cmd-vx",
str(args.creep_cmd_vx),
"--yaw-stop-threshold-deg",
str(args.yaw_stop_threshold_deg),
"--turn-in-place-enter-deg",
str(args.turn_in_place_enter_deg),
"--cmd-vx-scale",
str(args.cmd_vx_scale),
"--max-time",
str(max_time),
"--waypoint-timeout",
str(waypoint_timeout),
"--sample-every",
"10",
"--out-dir",
str(case_out),
*extra,
]
if args.no_local_safety:
cmd.append("--no-local-safety")
code, output = run_command(cmd, PROJECT_ROOT)
report_path = newest_report(case_out)
report = load_report(report_path)
return {
"name": name,
"exit_code": code,
"success": bool(report.get("success", False)),
"reason": report.get("reason", "no report"),
"reached": f"{report.get('reached_count', '?')}/{report.get('waypoint_count', '?')}",
"min_margin": report.get("min_margin"),
"report": str(report_path) if report_path else None,
"output_tail": "\n".join(output.splitlines()[-12:]),
}
def main() -> int:
args = parse_args()
args.out_dir.mkdir(parents=True, exist_ok=True)
suite_dir = args.out_dir / f"suite_{time.strftime('%Y%m%d_%H%M%S')}"
suite_dir.mkdir(parents=True, exist_ok=True)
args.out_dir = suite_dir
safety_cmd = [
sys.executable,
"tools/nav_tools/route_safety_check.py",
"--points",
str(args.points),
"--xml",
str(args.terrain_xml),
"--onnx",
str(args.onnx),
"--top",
"12",
]
safety_code, safety_output = run_command(safety_cmd, PROJECT_ROOT)
cases = []
full_time = 60 if args.quick else 180
slice_time = 45 if args.quick else 90
cases.append(run_sim_case("full", args, [], full_time, 20))
cases.append(run_sim_case("start_1_8", args, ["--start-id", "1", "--end-id", "8"], slice_time, 20))
cases.append(run_sim_case("mid_13_22", args, ["--start-id", "13", "--end-id", "22"], slice_time, 20))
cases.append(run_sim_case("slalom_30_45", args, ["--start-id", "30", "--end-id", "45"], slice_time, 20))
if args.include_reverse_vx:
old_scale = args.cmd_vx_scale
args.cmd_vx_scale = -abs(old_scale)
cases.append(run_sim_case("reverse_start_1_8", args, ["--start-id", "1", "--end-id", "8"], slice_time, 20))
cases.append(run_sim_case("reverse_slalom_30_45", args, ["--start-id", "30", "--end-id", "45"], slice_time, 20))
args.cmd_vx_scale = old_scale
summary = {
"points": str(args.points),
"terrain_xml": str(args.terrain_xml),
"onnx": str(args.onnx),
"follower": args.follower,
"lookahead": args.lookahead,
"max_vx": args.max_vx,
"cmd_vx_scale": args.cmd_vx_scale,
"no_local_safety": args.no_local_safety,
"suite_dir": str(suite_dir),
"safety_exit_code": safety_code,
"safety_tail": "\n".join(safety_output.splitlines()[-18:]),
"cases": cases,
}
summary_path = suite_dir / "summary.json"
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
print("Route experiment suite")
print(f" points: {args.points}")
print(f" suite: {suite_dir}")
print(f" safety: {'PASS' if safety_code == 0 else 'FAIL'}")
for case in cases:
print(
f" {case['name']}: success={case['success']} "
f"reached={case['reached']} reason={case['reason']} "
f"margin={case['min_margin']} report={case['report']}"
)
print(f" summary: {summary_path}")
return 0 if safety_code == 0 and all(case["success"] for case in cases) else 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Translate and yaw-rotate an ASCII PCD in XY.
Example:
python transform_pcd_xy.py pcd/1hao.pcd --in-place --origin-x 9.34 --origin-y 0.88 --yaw-deg -90
The transform is:
1. subtract origin from x/y
2. rotate around (0, 0) by yaw-deg
Only ASCII PCD files are supported. Non-x/y fields are preserved.
"""
from __future__ import annotations
import argparse
import math
from pathlib import Path
TOOL_DIR = Path(__file__).resolve().parent
def tool_relative(path: Path) -> Path:
if path.is_absolute():
raise ValueError("Use a path relative to this nav_tools folder.")
return TOOL_DIR / path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Translate and yaw-rotate an ASCII PCD in XY.")
parser.add_argument("input", type=Path, help="Input .pcd file.")
parser.add_argument("--output", type=Path, help="Output .pcd file. Required unless --in-place is used.")
parser.add_argument("--in-place", action="store_true", help="Replace the input file after a successful transform.")
parser.add_argument("--origin-x", type=float, default=0.0, help="X value to subtract before rotation.")
parser.add_argument("--origin-y", type=float, default=0.0, help="Y value to subtract before rotation.")
parser.add_argument("--yaw-deg", type=float, default=0.0, help="Yaw rotation in degrees after origin subtraction.")
parser.add_argument("--precision", type=int, default=8, help="Decimal precision for transformed x/y.")
return parser.parse_args()
def fmt(value: float, precision: int) -> str:
if abs(value) < 0.5 * 10 ** (-precision):
value = 0.0
return f"{value:.{precision}f}".rstrip("0").rstrip(".")
def transform_file(
source: Path,
target: Path,
origin_x: float,
origin_y: float,
yaw_deg: float,
precision: int,
) -> int:
yaw = math.radians(yaw_deg)
cos_yaw = math.cos(yaw)
sin_yaw = math.sin(yaw)
data_started = False
fields: list[str] = []
x_idx = 0
y_idx = 1
count = 0
with source.open("r", encoding="utf-8", errors="ignore") as fin, target.open("w", encoding="utf-8", newline="\n") as fout:
for line in fin:
stripped = line.strip()
upper = stripped.upper()
if data_started and stripped:
parts = stripped.split()
if len(parts) > max(x_idx, y_idx):
x0 = float(parts[x_idx]) - origin_x
y0 = float(parts[y_idx]) - origin_y
x1 = x0 * cos_yaw - y0 * sin_yaw
y1 = x0 * sin_yaw + y0 * cos_yaw
parts[x_idx] = fmt(x1, precision)
parts[y_idx] = fmt(y1, precision)
fout.write(" ".join(parts) + "\n")
count += 1
else:
fout.write(line)
else:
fout.write(line)
if upper.startswith("FIELDS "):
fields = stripped.split()[1:]
if "x" in fields and "y" in fields:
x_idx = fields.index("x")
y_idx = fields.index("y")
elif upper.startswith("DATA "):
if "ASCII" not in upper:
raise RuntimeError(f"Only ASCII PCD is supported: {source}")
data_started = True
return count
def main() -> int:
args = parse_args()
source = tool_relative(args.input)
if args.in_place:
target = source.with_suffix(source.suffix + ".tmp")
elif args.output:
target = tool_relative(args.output)
else:
raise SystemExit("--output is required unless --in-place is used")
count = transform_file(source, target, args.origin_x, args.origin_y, args.yaw_deg, args.precision)
if count <= 0:
target.unlink(missing_ok=True)
raise SystemExit("No PCD points transformed")
if args.in_place:
target.replace(source)
print(f"transformed {count} points: origin=({args.origin_x}, {args.origin_y}), yaw={args.yaw_deg} deg")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,72 @@
<mujoco model="nav_tools terrain A_C">
<statistic center="-5.5 1.9 0.4" extent="5.0" />
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0" />
<rgba haze="0.15 0.25 0.35 1" />
<global azimuth="90" elevation="-20" />
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072" />
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3" markrgb="0.8 0.8 0.8" width="300" height="300" />
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2" />
</asset>
<worldbody>
<!-- 比赛地图基准地面。 -->
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane" />
<!-- 14m x 6m outer boundary transformed from A_bian: x_C=y_bian, y_C=-x_bian. -->
<geom name="border_x_min" pos="-9.5 1.9 0.15" type="box" size="0.025 3.0 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_x_max" pos="4.5 1.9 0.15" type="box" size="0.025 3.0 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_y_min" pos="-2.5 -1.1 0.15" type="box" size="7.0 0.025 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_y_max" pos="-2.5 4.9 0.15" type="box" size="7.0 0.025 0.15" rgba="0.05 0.05 0.05 1" />
<!-- 高墙:机器人需从上方跃过或攀爬通过,自动/遥控均可计分。 -->
<geom pos="-3.620583 -0.010049 0.15" type="box" size="0.025 0.5 0.15" quat="1 0 0 0" rgba="1.0 0.9 0.4 1.0" />
<!-- 砂砾碎木坑:L 形障碍,需从 1m 短边进入或离开,完整通过才计分。 -->
<geom pos="-8.470246 2.97224 0.075" type="box" size="0.5 0.5 0.075" quat="0.707106781 0 0 0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.970246 3.97224 0.075" type="box" size="0.5 1.0 0.075" quat="0.707106781 0 0 0.707106781" rgba="0.75 0.72 0.55 1.0" />
<!-- 限高杆:机器人需从横杆下方通过,碰落横杆则越障失败。 -->
<geom pos="-5.219014 4.406386 0.155" type="cylinder" size="0.025 0.155" quat="0.707106781 0 0 -0.707106781" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.219014 3.406386 0.155" type="cylinder" size="0.025 0.155" quat="0.707106781 0 0 -0.707106781" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.219014 3.906386 0.325" type="cylinder" size="0.015 0.5" quat="0.5 -0.5 -0.5 -0.5" rgba="1.0 0.9 0.4 1.0" />
<!-- 启动区:规则允许地面和 T 字形台阶上各一个启动区,机器人需完全纳入启动区后开始。 -->
<geom pos="-5.5 1.9 0" type="box" size="0.5 0.5 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<!-- T 字形台阶:可选启动区之一在最高平台;通过时每一级台阶顶面需至少接触一次。 -->
<geom pos="-1.325629 -0.037815 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-1.025629 -0.037815 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.725629 -0.037815 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.075629 -0.037815 0.35" type="box" size="0.5 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.574371 -0.037815 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.874371 -0.037815 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.174371 -0.037815 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.075629 0.612185 0.25" type="box" size="0.15 0.5 0.05" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.075629 0.912185 0.15" type="box" size="0.15 0.5 0.05" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.075629 1.212185 0.05" type="box" size="0.15 0.5 0.05" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<!-- 大斜坡、木桥 A / 木桥 B:大斜坡需满足长边行走距离要求;木桥需从一侧平台经木桥到达另一侧平台。 -->
<geom pos="2.540216 0.040352 0.1" type="box" size="0.40 0.5 0.005" quat="0.992546088 0.121869866 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.420216 0.040352 0.1" type="box" size="0.5 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.420216 0.890352 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.420216 1.440352 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.420216 1.990352 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.420216 2.540352 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.420216 3.090352 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="2.920216 3.940352 0.1" type="box" size="0.5 1.0 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="2.420216 3.040552 0.0951" type="box" size="0.4133 0.5 0.005" quat="0.701836142 0.086174413 0.086174413 -0.701836142" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.170216 3.590352 0.1" type="box" size="0.1 0.75 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.170216 3.890352 0.1" type="box" size="0.1 0.75 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.170216 4.190352 0.1" type="box" size="0.1 0.75 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.079784 3.940352 0.1" type="box" size="0.5 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-1.579784 2.874152 0.0951" type="box" size="0.5759 2.0 0.005" quat="0.704416012 0.061628579 0.061628579 -0.704416012" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-2.079784 4.008452 0.0951" type="box" size="0.5759 1.5 0.005" quat="0.704416012 -0.061628579 -0.061628579 -0.704416012" rgba="0.75 0.72 0.55 1.0" />
<!-- 直角绕杆:需按 S 形绕过竖杆,并经过两端及拐角必达区。 -->
<geom pos="-6.121128 0.005994 0" type="cylinder" size="0.15 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.521128 1.405994 0" type="cylinder" size="0.15 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.771128 -0.244006 0" type="cylinder" size="0.15 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom name="slalom_pole_1_base" pos="-6.521128 0.005994 0.02" type="cylinder" size="0.115 0.02" rgba="0.75 0.72 0.55 1.0" />
<geom name="slalom_pole_1_shaft" pos="-6.521128 0.005994 0.37" type="cylinder" size="0.115 0.33" rgba="0.75 0.72 0.55 1.0" />
<geom name="slalom_pole_2_base" pos="-7.521128 0.005994 0.02" type="cylinder" size="0.115 0.02" rgba="0.75 0.72 0.55 1.0" />
<geom name="slalom_pole_2_shaft" pos="-7.521128 0.005994 0.37" type="cylinder" size="0.115 0.33" rgba="0.75 0.72 0.55 1.0" />
<geom name="slalom_pole_3_base" pos="-8.521128 0.005994 0.02" type="cylinder" size="0.115 0.02" rgba="0.75 0.72 0.55 1.0" />
<geom name="slalom_pole_3_shaft" pos="-8.521128 0.005994 0.37" type="cylinder" size="0.115 0.33" rgba="0.75 0.72 0.55 1.0" />
<geom name="slalom_pole_4_base" pos="-8.521128 1.005994 0.02" type="cylinder" size="0.115 0.02" rgba="0.75 0.72 0.55 1.0" />
<geom name="slalom_pole_4_shaft" pos="-8.521128 1.005994 0.37" type="cylinder" size="0.115 0.33" rgba="0.75 0.72 0.55 1.0" />
</worldbody>
</mujoco>
@@ -0,0 +1,72 @@
<mujoco model="nav_tools terrain B_C">
<statistic center="-5.5 -1.9 0.4" extent="5.0" />
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0" />
<rgba haze="0.15 0.25 0.35 1" />
<global azimuth="90" elevation="-20" />
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072" />
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3" markrgb="0.8 0.8 0.8" width="300" height="300" />
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2" />
</asset>
<worldbody>
<!-- 比赛地图基准地面。 -->
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane" />
<!-- 14m x 6m outer boundary, mirrored from A_C across the x axis. -->
<geom name="border_x_min" pos="-9.5 -1.9 0.15" type="box" size="0.025 3.0 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_x_max" pos="4.401477 -1.937395 0.15" type="box" size="0.025 3.0 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_y_min" pos="-2.5 -4.9 0.15" type="box" size="7.0 0.025 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_y_max" pos="-2.5 1.1 0.15" type="box" size="7.0 0.025 0.15" rgba="0.05 0.05 0.05 1" />
<!-- 高墙:机器人需从上方跃过或攀爬通过,自动/遥控均可计分。 -->
<geom pos="-3.589248 0.022509 0.15" type="box" size="0.025 0.5 0.15" quat="1 0 0 0" rgba="1.0 0.9 0.4 1.0" />
<!-- 砂砾碎木坑:L 形障碍,需从 1m 短边进入或离开,完整通过才计分。 -->
<geom pos="-8.430146 -2.855226 0.075" type="box" size="0.5 0.5 0.075" quat="0.707107 0 0 0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.930146 -3.855226 0.075" type="box" size="0.5 1.0 0.075" quat="0.707107 0 0 0.707107" rgba="0.75 0.72 0.55 1.0" />
<!-- 限高杆:机器人需从横杆下方通过,碰落横杆则越障失败。 -->
<geom pos="-5.231818 -4.372727 0.155" type="cylinder" size="0.025 0.155" quat="0.707107 0 0 -0.707107" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.231818 -3.372727 0.155" type="cylinder" size="0.025 0.155" quat="0.707107 0 0 -0.707107" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.231818 -3.872727 0.325" type="cylinder" size="0.015 0.5" quat="0.5 0.5 -0.5 -0.5" rgba="1.0 0.9 0.4 1.0" />
<!-- 启动区:规则允许地面和 T 字形台阶上各一个启动区,机器人需完全纳入启动区后开始。 -->
<geom pos="-5.5 -1.9 0" type="box" size="0.5 0.5 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<!-- T 字形台阶:可选启动区之一在最高平台;通过时每一级台阶顶面需至少接触一次。 -->
<geom pos="-1.25 0 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.95 0 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.65 0 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 0 0.35" type="box" size="0.5 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.65 0 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.95 0 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 0 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 -0.65 0.25" type="box" size="0.15 0.5 0.05" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 -0.95 0.15" type="box" size="0.15 0.5 0.05" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 -1.25 0.05" type="box" size="0.15 0.5 0.05" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<!-- 大斜坡、木桥 A / 木桥 B:大斜坡需满足长边行走距离要求;木桥需从一侧平台经木桥到达另一侧平台。 -->
<geom pos="2.521477 -0.037395 0.1" type="box" size="0.40 0.5 0.005" quat="0.992546 -0.12187 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.401477 -0.037395 0.1" type="box" size="0.5 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.401477 -0.887395 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.401477 -1.437395 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.401477 -1.987395 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.401477 -2.537395 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.401477 -3.087395 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="2.901477 -3.937395 0.1" type="box" size="0.5 1.0 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="2.401477 -3.037595 0.0951" type="box" size="0.4133 0.5 0.005" quat="0.701836 -0.086174 0.086174 -0.701836" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.151477 -3.587395 0.1" type="box" size="0.1 0.75 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.151477 -3.887395 0.1" type="box" size="0.1 0.75 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.151477 -4.187395 0.1" type="box" size="0.1 0.75 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.098523 -3.937395 0.1" type="box" size="0.5 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-1.598523 -2.871195 0.0951" type="box" size="0.5759 2.0 0.005" quat="0.704416 -0.061629 0.061629 -0.704416" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-2.098523 -4.005495 0.0951" type="box" size="0.5759 1.5 0.005" quat="0.704416 0.061629 -0.061629 -0.704416" rgba="0.75 0.72 0.55 1.0" />
<!-- 直角绕杆:需按 S 形绕过竖杆,并经过两端及拐角必达区。 -->
<geom pos="-6.075243 0.034483 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.475243 -1.365517 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.725243 0.284483 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-6.475243 0.034483 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-6.475243 0.034483 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.475243 0.034483 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.475243 0.034483 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.475243 0.034483 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.475243 0.034483 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.475243 -0.965517 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.475243 -0.965517 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
</worldbody>
</mujoco>
@@ -0,0 +1,67 @@
<mujoco model="nav_tools terrain A_C">
<statistic center="-5.5 1.9 0.4" extent="5.0" />
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0" />
<rgba haze="0.15 0.25 0.35 1" />
<global azimuth="90" elevation="-20" />
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072" />
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3" markrgb="0.8 0.8 0.8" width="300" height="300" />
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2" />
</asset>
<worldbody>
<!-- 比赛地图基准地面。 -->
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane" />
<!-- 高墙:机器人需从上方跃过或攀爬通过,自动/遥控均可计分。 -->
<geom pos="-3.5 0 0.15" type="box" size="0.025 0.5 0.15" quat="1 0 0 0" rgba="1.0 0.9 0.4 1.0" />
<!-- 砂砾碎木坑:L 形障碍,需从 1m 短边进入或离开,完整通过才计分。 -->
<geom pos="-8.4 3.0361 0.075" type="box" size="0.5 0.5 0.075" quat="0.707106781 0 0 0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.9 4.0361 0.075" type="box" size="0.5 1.0 0.075" quat="0.707106781 0 0 0.707106781" rgba="0.75 0.72 0.55 1.0" />
<!-- 限高杆:机器人需从横杆下方通过,碰落横杆则越障失败。 -->
<geom pos="-5.5 4.4 0.155" type="cylinder" size="0.025 0.155" quat="0.707106781 0 0 -0.707106781" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.5 3.4 0.155" type="cylinder" size="0.025 0.155" quat="0.707106781 0 0 -0.707106781" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.5 3.9 0.325" type="cylinder" size="0.015 0.5" quat="0.5 -0.5 -0.5 -0.5" rgba="1.0 0.9 0.4 1.0" />
<!-- 启动区:规则允许地面和 T 字形台阶上各一个启动区,机器人需完全纳入启动区后开始。 -->
<geom pos="-5.5 1.9 0" type="box" size="0.5 0.5 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<!-- T 字形台阶:可选启动区之一在最高平台;通过时每一级台阶顶面需至少接触一次。 -->
<geom pos="-1.25 0 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.95 0 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.65 0 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 0 0.35" type="box" size="0.5 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.65 0 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.95 0 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 0 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 0.65 0.25" type="box" size="0.15 0.5 0.05" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 0.95 0.15" type="box" size="0.15 0.5 0.05" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 1.25 0.05" type="box" size="0.15 0.5 0.05" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<!-- 大斜坡、木桥 A / 木桥 B:大斜坡需满足长边行走距离要求;木桥需从一侧平台经木桥到达另一侧平台。 -->
<geom pos="2.62 0 0.1" type="box" size="0.40 0.5 0.005" quat="0.992546088 0.121869866 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 0 0.1" type="box" size="0.5 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 0.85 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 1.4 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 1.95 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 2.5 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 3.05 0.1" type="box" size="0.2 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3 3.9 0.1" type="box" size="0.5 1.0 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="2.5 3.0002 0.0951" type="box" size="0.4133 0.5 0.005" quat="0.701836142 0.086174413 0.086174413 -0.701836142" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 3.55 0.1" type="box" size="0.1 0.75 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 3.85 0.1" type="box" size="0.1 0.75 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 4.15 0.1" type="box" size="0.1 0.75 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 3.9 0.1" type="box" size="0.5 0.5 0.10" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-1.5 2.8338 0.0951" type="box" size="0.5759 2.0 0.005" quat="0.704416012 0.061628579 0.061628579 -0.704416012" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-2 3.9681 0.0951" type="box" size="0.5759 1.5 0.005" quat="0.704416012 -0.061628579 -0.061628579 -0.704416012" rgba="0.75 0.72 0.55 1.0" />
<!-- 直角绕杆:需按 S 形绕过竖杆,并经过两端及拐角必达区。 -->
<geom pos="-6.025 0.0861 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.425 1.4861 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.675 -0.1639 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-6.425 0.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-6.425 0.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.425 0.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.425 0.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 0.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 0.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 1.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 1.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707106781 0 0 -0.707106781" rgba="0.75 0.72 0.55 1.0" />
</worldbody>
</mujoco>
@@ -0,0 +1,72 @@
<mujoco model="nav_tools terrain B_C">
<statistic center="-5.5 -1.9 0.4" extent="5.0" />
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0" />
<rgba haze="0.15 0.25 0.35 1" />
<global azimuth="90" elevation="-20" />
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072" />
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3" markrgb="0.8 0.8 0.8" width="300" height="300" />
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2" />
</asset>
<worldbody>
<!-- 比赛地图基准地面。 -->
<geom name="floor" size="0 0 0.05" type="plane" material="groundplane" />
<!-- 14m x 6m outer boundary, mirrored from A_C across the x axis. -->
<geom name="border_x_min" pos="-9.5 -1.9 0.15" type="box" size="0.025 3.0 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_x_max" pos="4.5 -1.9 0.15" type="box" size="0.025 3.0 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_y_min" pos="-2.5 -4.9 0.15" type="box" size="7.0 0.025 0.15" rgba="0.05 0.05 0.05 1" />
<geom name="border_y_max" pos="-2.5 1.1 0.15" type="box" size="7.0 0.025 0.15" rgba="0.05 0.05 0.05 1" />
<!-- 高墙:机器人需从上方跃过或攀爬通过,自动/遥控均可计分。 -->
<geom pos="-3.5 0 0.15" type="box" size="0.025 0.5 0.15" quat="1 0 0 0" rgba="1.0 0.9 0.4 1.0" />
<!-- 砂砾碎木坑:L 形障碍,需从 1m 短边进入或离开,完整通过才计分。 -->
<geom pos="-8.4 -3.0361 0.075" type="box" size="0.5 0.5 0.075" quat="0.707107 0 0 0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.9 -4.0361 0.075" type="box" size="0.5 1.0 0.075" quat="0.707107 0 0 0.707107" rgba="0.75 0.72 0.55 1.0" />
<!-- 限高杆:机器人需从横杆下方通过,碰落横杆则越障失败。 -->
<geom pos="-5.5 -4.4 0.155" type="cylinder" size="0.025 0.155" quat="0.707107 0 0 -0.707107" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.5 -3.4 0.155" type="cylinder" size="0.025 0.155" quat="0.707107 0 0 -0.707107" rgba="0.8 0.1 0.1 1.0" />
<geom pos="-5.5 -3.9 0.325" type="cylinder" size="0.015 0.5" quat="0.5 0.5 -0.5 -0.5" rgba="1.0 0.9 0.4 1.0" />
<!-- 启动区:规则允许地面和 T 字形台阶上各一个启动区,机器人需完全纳入启动区后开始。 -->
<geom pos="-5.5 -1.9 0" type="box" size="0.5 0.5 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<!-- T 字形台阶:可选启动区之一在最高平台;通过时每一级台阶顶面需至少接触一次。 -->
<geom pos="-1.25 0 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.95 0 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-0.65 0 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 0 0.35" type="box" size="0.5 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.65 0 0.25" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0.95 0 0.15" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 0 0.05" type="box" size="0.15 0.5 0.05" quat="1 0 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 -0.65 0.25" type="box" size="0.15 0.5 0.05" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 -0.95 0.15" type="box" size="0.15 0.5 0.05" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 -1.25 0.05" type="box" size="0.15 0.5 0.05" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<!-- 大斜坡、木桥 A / 木桥 B:大斜坡需满足长边行走距离要求;木桥需从一侧平台经木桥到达另一侧平台。 -->
<geom pos="2.62 0 0.1" type="box" size="0.40 0.5 0.005" quat="0.992546 -0.12187 0 0" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 0 0.1" type="box" size="0.5 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 -0.85 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 -1.4 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 -1.95 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 -2.5 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3.5 -3.05 0.1" type="box" size="0.2 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="3 -3.9 0.1" type="box" size="0.5 1.0 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="2.5 -3.0002 0.0951" type="box" size="0.4133 0.5 0.005" quat="0.701836 -0.086174 0.086174 -0.701836" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 -3.55 0.1" type="box" size="0.1 0.75 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 -3.85 0.1" type="box" size="0.1 0.75 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="1.25 -4.15 0.1" type="box" size="0.1 0.75 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="0 -3.9 0.1" type="box" size="0.5 0.5 0.10" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-1.5 -2.8338 0.0951" type="box" size="0.5759 2.0 0.005" quat="0.704416 -0.061629 0.061629 -0.704416" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-2 -3.9681 0.0951" type="box" size="0.5759 1.5 0.005" quat="0.704416 0.061629 -0.061629 -0.704416" rgba="0.75 0.72 0.55 1.0" />
<!-- 直角绕杆:需按 S 形绕过竖杆,并经过两端及拐角必达区。 -->
<geom pos="-6.025 -0.0861 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.425 -1.4861 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-8.675 0.1639 0" type="cylinder" size="0.1 0.001" rgba="1.0 0.0 0.0 0.35" contype="0" conaffinity="0" />
<geom pos="-6.425 -0.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-6.425 -0.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.425 -0.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-7.425 -0.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 -0.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 -0.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 -1.0861 0.02" type="cylinder" size="0.05 0.02" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
<geom pos="-8.425 -1.0861 0.37" type="cylinder" size="0.015 0.33" quat="0.707107 0 0 -0.707107" rgba="0.75 0.72 0.55 1.0" />
</worldbody>
</mujoco>
+2 -1
View File
@@ -2,7 +2,7 @@
RC_WheelLeg 是山东华宇工学院 16DOF 串联轮足机器人项目。
当前 `16dof` 分支用于整理 16DOF 机械、强化学习训练、Sim2Sim、Sim2Real、ROS 2 部署和比赛版本。机械资料、比赛训练架构、后期 MuJoCo 工具集和后期 Sim2Sim 已经完成整理。
当前 `16dof` 分支用于整理 16DOF 机械、强化学习训练、Sim2Sim、Sim2Real、ROS 2 部署和比赛版本。机械资料、比赛训练架构、后期 MuJoCo/Sim2Sim 和导航打点工具已经完成整理。
## 平台概览
@@ -38,6 +38,7 @@ RC_WheelLeg/
- [x] 整理比赛最终训练代码架构
- [x] 整理后期 MuJoCo 姿态、IK、动力学和 MPC 工具
- [x] 整理后期 Sim2Sim、路线检查与比赛 Rough ONNX 策略
- [x] 整理导航地图、打点工具、路线迭代和抽样 PCD
- [ ] 核对比赛机械与仿真模型参数
- [ ] 整理 URDF/MJCF 机器人描述
- [ ] 整理后续统一训练、ROS 2 和比赛版本