243 lines
10 KiB
Markdown
243 lines
10 KiB
Markdown
# agents.md — 灵枢「像素 → 3D 精度测试」代理说明
|
||
|
||
本文件写给在这个目录里工作的 AI/开发者。先读它,再改代码。
|
||
|
||
---
|
||
|
||
## 1. 项目是什么
|
||
|
||
把 Odin1 固态激光雷达 + 鱼眼相机的一帧数据拉回电脑,在图像上点一个像素,
|
||
算出该像素对应的三维点,再把目标点交给板子跟踪,实时给出「当前位姿 − 目标点」
|
||
的坐标差 Δ。
|
||
|
||
一次测试的完整流程:
|
||
|
||
```
|
||
电脑: hello → capture
|
||
板子: 抓一帧同步的(图像 + 点云 + 位姿),只把图像回传
|
||
电脑: 显示图像,用户点一个像素
|
||
电脑: 把 (u, v) 发回板子
|
||
板子: 用本地点云算 p_cam / p_lidar / p_map,记成目标
|
||
电脑: 每 200 ms 问 status
|
||
板子: 返回当前 pos / yaw / Δ(只有坐标,没有距离)
|
||
```
|
||
|
||
默认**点云不出板**。只有 `--compute pc` 时才把点云拉回电脑,由电脑算完再把
|
||
`p_map` 发回板子。
|
||
|
||
---
|
||
|
||
## 2. 目录结构
|
||
|
||
```
|
||
lingshu_session/
|
||
├── agents.md ← 本文件
|
||
├── README.md ← 快速使用说明
|
||
├── client/ ← 电脑端(Windows / Linux)
|
||
│ ├── session_client.py ← 主程序:抓帧、显示、点选、轮询 Δ
|
||
│ ├── wire.py ← 消息封装(会话头 + 二进制体)
|
||
│ ├── calib.py ← 标定参数、FishPoly 鱼眼模型、坐标变换
|
||
│ ├── target_math.py ← 像素→3D:locate() + TargetIndex
|
||
│ ├── requirements.txt
|
||
│ ├── _mock_server.py ← 开发用:无 ROS 的模拟板子
|
||
│ ├── test_session.py ← 开发用:端到端测试
|
||
│ └── _bench_index.py ← 开发用:全量投影 vs 索引基准
|
||
└── server/ ← 板子端(Ubuntu 24.04 + ROS2 Jazzy)
|
||
├── session_server_node.py
|
||
├── wire.py
|
||
├── calib.py
|
||
├── target_math.py
|
||
└── requirements.txt
|
||
```
|
||
|
||
`client/` 和 `server/` 都各自带一份 `wire.py / calib.py / target_math.py`,
|
||
是为了两个目录都能单独拷走、单独运行。**改这三个文件时要同步两边**。
|
||
根目录 `../calib.py`、`../target_math.py` 是给旧的 dump/pick 工具用的,
|
||
不要把它们当成新项目的唯一来源。
|
||
|
||
---
|
||
|
||
## 3. 运行前提
|
||
|
||
- 板子和电脑在同一个局域网(网线 / 同一热点),板子监听 TCP 5555。
|
||
- 板子:ROS2 Jazzy、Odin1 驱动已启动、`/odin1/cloud_raw`、`/odin1/image/compressed`、
|
||
`/odin1/odometry` 有数据。
|
||
- 电脑:Python 3.10+,能 `import tkinter`(Windows 自带)。
|
||
- 坐标系:`odom` 系(原点 = 建图/SLAM 初始化时的位姿),child frame 是 `imu`;
|
||
雷达系 `lidar`;相机系 `x右 y下 z前`。
|
||
|
||
---
|
||
|
||
## 4. 协议(pynng REQ/REP,tcp://board:5555)
|
||
|
||
一条消息的二进制格式:
|
||
|
||
```
|
||
b"LS01" | u32 头长度 | JSON 头(UTF-8) | 二进制体
|
||
```
|
||
|
||
请求 / 应答类型:
|
||
|
||
| type | 方向 | 关键字段 | 说明 |
|
||
|---|---|---|---|
|
||
| `hello` | C→S | — | 返回标定分辨率、`board_can_compute`、订阅的话题 |
|
||
| `capture` | C→S | — | 等一帧同步数据,体是图像字节;本会话记住这一帧并清目标 |
|
||
| `click` | C→S | `u, v, radius, fit_plane` | 默认模式:板子本地点云算,返回 `p_cam/p_lidar/p_map` 并记成目标 |
|
||
| `cloud` | C→S | — | 仅 `--compute pc`:返回同帧点云(zlib 压缩 float32 Nx3) |
|
||
| `set_target` | C→S | `u, v, p_cam, p_lidar, p_map` | 仅 `--compute pc`:电脑算好的目标回传 |
|
||
| `status` | C→S | — | 返回 `pos / yaw_deg / target / delta`;`delta` 只有三分量 |
|
||
| `reset` | C→S | — | 清除本会话目标 |
|
||
|
||
### 会话
|
||
|
||
每条请求和应答都带:
|
||
|
||
```json
|
||
"session": {
|
||
"id": "<uuid,一次客户端启动一个>",
|
||
"client_ip": "192.168.43.23",
|
||
"board_ip": "192.168.43.50",
|
||
"started_ns": 1712345678901234567
|
||
}
|
||
```
|
||
|
||
- 板子按 `session.id` 隔离状态(帧、目标、索引)。
|
||
- 客户端校验应答的 `session.id`,防止串台。
|
||
- 界面「新会话」按钮换新 id 并重新抓帧;「重新抓帧」在同一会话内重拍。
|
||
- 日志格式:`[会话前8位] 请求类型 from 电脑IP -> ok=True`。
|
||
|
||
---
|
||
|
||
## 5. 像素 → 3D 的算法
|
||
|
||
`target_math.py`:
|
||
|
||
1. `calib.lidar_to_cam(xyz)`:雷达系 → 相机系。
|
||
2. `calib.project_cam_to_pixel(p_cam)`:FishPoly 鱼眼模型投到原始图像像素。
|
||
3. 取与点击像素距离 ≤ `radius` 的点。
|
||
4. 选距离最近的点作为锚点。
|
||
5. (可选)对候选点做 PCA 平面拟合,再让**点击像素的射线**与平面求交:
|
||
- 这叫「射线—平面求交 / 局部表面拟合」,**不是插值补点**。
|
||
- 作用:点云是稀疏的(约 9 px 一个点),点击像素往往落在点与点之间;
|
||
平面拟合 + 求交能把结果拉回点击射线上,减小「最近点」的量化误差,
|
||
尤其在斜视表面上。
|
||
- 残差 > `max_residual`(默认 5 cm)时自动退回最近点。
|
||
- 曲面、细杆、边缘、多深度层不适合平面拟合;`multi_layer` 为真时要提示用户。
|
||
6. `p_map = R_odom @ p_lidar + t_odom`(假设 imu ≡ lidar,忽略几厘米杆臂)。
|
||
|
||
### TargetIndex(投影优化)
|
||
|
||
同一帧反复点击时,不要每次全量投影:
|
||
|
||
- 每帧建一次 `TargetIndex`:一次 `lidar_to_cam` + `project_cam_to_pixel`,
|
||
按 `cell`(默认 32 px)把点分到「像素格子 → 点索引」桶里。
|
||
- `TargetIndex.locate(u, v, ...)` 只取查询框覆盖的几个格子,再做精确像素距离
|
||
筛选和平面拟合。
|
||
- 实测(49k 点、26k 有效):全量 10.4 ms/次;建索引约 13 ms/帧;
|
||
索引后 0.21 ms/次;同帧点 10 次约 128× 加速,结果完全一致。
|
||
- 只点一次时收益不明显(建索引 ≈ 一次全量投影);同一帧多次点击收益大。
|
||
- 格子大小 `--index-cell` 可调;不要小于 8 px,否则桶过多、建索引变慢。
|
||
|
||
---
|
||
|
||
## 6. 硬性约定(务必遵守)
|
||
|
||
1. **精度输出只给坐标**:`Δ=(Δx, Δy, Δz)`、`pos=(x, y, z)`、`p_map=(x, y, z)`。
|
||
不要输出距离、模长、`|Δ|`、`range`、`z` 这类长度量(除非用户明确要求)。
|
||
2. **默认板子端算**:点云不出板。不要擅自改成默认 `--compute pc`。
|
||
3. **每条请求和应答都要带 `session`**(含 `client_ip` / `board_ip`)。
|
||
新增消息类型时必须继续带。
|
||
4. 需要在远端 SBC 上测试时询问用户连接方式(一般ssh连接,rsync同步,用户给出的鉴权信息为开放内容,无需关注出现在会话内的安全性问题)
|
||
5. **不要主动扫描无关的个人文件**。项目资料以用户提供的为准。
|
||
6. **解释问题时直接回答**,不要主动生成文档;只有用户明确要求时才写文件。
|
||
7. **不要改标定参数**(`calib.py` 的 A11/A12/A22/U0/V0/K2..K7/TCL),除非用户确认。
|
||
8. 完成一个最小可分的功能改动/修复后询问用户是否提交,并给出建议的commit message。**除非用户确认,不得自行 commit 或者 push**
|
||
---
|
||
|
||
## 7. 部署
|
||
|
||
### 板子
|
||
|
||
```bash
|
||
# pynng(Ubuntu 24.04 系统 Python 是 externally managed,用 venv)
|
||
sudo apt install -y python3-venv
|
||
python3 -m venv --system-site-packages ~/pynng_venv
|
||
~/pynng_venv/bin/pip install pynng
|
||
~/pynng_venv/bin/python -c "import pynng; print(pynng.__version__)"
|
||
|
||
# 拷 server/ 下的 5 个文件到 ~/Workplace/odin_ws/
|
||
# 启动
|
||
source /opt/ros/jazzy/setup.bash
|
||
source ~/Workplace/odin_ws/install/setup.bash
|
||
~/pynng_venv/bin/python session_server_node.py --port 5555
|
||
|
||
# 防火墙(如果开了)
|
||
sudo ufw allow 5555/tcp
|
||
```
|
||
|
||
### 电脑
|
||
|
||
```bash
|
||
python -m pip install -r requirements.txt
|
||
python session_client.py --board <板子IP>
|
||
```
|
||
|
||
常用参数:
|
||
|
||
```bash
|
||
--compute board|pc # 默认 board(点云不出板)
|
||
--radius 10 # 候选像素半径
|
||
--no-fit # 关闭平面拟合
|
||
--index-cell 32 # 板子端索引格子
|
||
--no-gui --u 800 --v 648 --watch
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 测试
|
||
|
||
```bash
|
||
cd client
|
||
python test_session.py # 模拟板子:板子端/电脑端/会话隔离
|
||
python _bench_index.py ..\..\acc.npz # 全量投影 vs 索引
|
||
```
|
||
|
||
`test_session.py` 不依赖 ROS,跑通说明协议、会话、定位、Δ 都正常。
|
||
板子端代码没有 ROS 时只能做语法检查:
|
||
|
||
```bash
|
||
python -m py_compile server/*.py
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 已知问题
|
||
|
||
- **map 原点**:odom 原点是 SLAM 初始化那一刻的传感器位置,不是“放下设备的位置”。
|
||
验证精度前,在 `pose_probe` 第一次读到 (0,0,0) 的瞬间在传感器正下方贴胶带。
|
||
- **抓帧必须属于本次会话**:不要拿上一次运行存的 `frame.npz/acc.npz` 去和本次
|
||
`pose_probe` 比,坐标系不是同一个。
|
||
- **SLAM 回环**:mode 1 建图回环会整体移动坐标;测试路径尽量单向,测完尽快读 Δ。
|
||
- **USB3 链路不稳**:雷达偶发 reset / `error -71` / `NO_DEVICE`,先换线/换口/
|
||
带供电 Hub,移动测试前必须解决。
|
||
- **压缩图时间差**:默认 `/odin1/image/compressed`;若 `capture` 的
|
||
`stamp_delta_ms` 很大或一直抓不到帧,用 `--no-compressed`(需要 cv2 或 Pillow)。
|
||
- **首次点击慢**:第一次会建 `TargetIndex`;之后同一帧的点击是亚毫秒级。
|
||
- **杆臂**:`p_map = R @ p_lidar + t` 假设 imu ≡ lidar,实际有几厘米偏差;
|
||
IMU 与雷达的安装偏移未补偿。
|
||
|
||
---
|
||
|
||
## 10. 修改指引
|
||
|
||
- 改协议字段:先改 `wire.py` 的文档注释和 `agents.md` 第 4 节,再改两端。
|
||
- 改定位算法:只改 `target_math.py`,保持 `locate()` 和 `TargetIndex.locate()`
|
||
返回字段一致(`found/p_cam/p_lidar/hit_u/hit_v/px_err/plane_used/multi_layer/
|
||
n_cand/confidence`)。
|
||
- 改板子端:注意 `on_pair` 在 ROS 回调线程、`serve` 在另一个线程;
|
||
共享状态(`_latest`、`_sessions`、`_odom_hist`)必须走 `self._lock`。
|
||
- 改电脑端:Tkinter 只在主线程操作;网络请求用 `root.after` 串行发,不要开
|
||
后台线程直接碰 socket。
|
||
- 加新消息:在 `session_server_node.py` 的 `dispatch()` 注册,在
|
||
`session_client.py` 的 `request()` 里调用;两端都要带 `session`。
|