Files
ros_contorl/models/README.md
arm_dev d289168dd0 P4s:模型审计 —— 移出零引用模型,修掉代码里三处悬空 best.pt
先回答"这些模型到底有没有人用",用全仓库引用扫描判定,不靠猜:

  在用   yolo/detect_best_0.onnx + .engine   ← paths.yolo_weights
  在用   hole_insert/detect_best_1.onnx      ← hole_detect.weights / workpiece_weights
  零引用 detect_hc90(pt/onnx/engine)       ← 只剩注释里的历史说明
  零引用 detect_yolo26_gongjian.pt
  零引用 detect_best(onnx/engine)          ← 仅代码兜底候选,而那个候选是 .pt,不存在

所以 detect_hc90 不需要修复,是不用的东西:连同另两个一起移出仓库。
注意**二进制权重被 gitignore**,删掉就找不回来了,因此是移出到
~/ros2-models-unused-20260923/ 而非 rm。搬回 models/ 即可被 list 重新发现。

顺手挖出并修掉三处悬空引用(和之前 site.yaml 里那个 best.pt 同一类问题):
- hole_detector.weights_from_site:兜底路径写死 models/hole_insert/best.pt(不存在)
- hole_detector._default_model_path:三个候选路径**全都不存在**,函数却
  `return candidates[0]`,于是永远返回不存在的 best.pt;报错路径看着很合理,
  极易被误判成"文件被误删"。现改为以 site.yaml 为准,再退到规范模型。
- config/hole_insert.yaml 顶部注释同样写的是 best.pt

新增 test_hole_detector.py(8 项)锁住这个回归。测试刻意**不断言文件存在**
——二进制不进 git,新克隆的仓库里 models/ 是空的——只断言解析结果指向规范模型名。

体检从 4 处问题降到 0:bash scripts/weights.sh doctor --check-engines → "没有发现问题"

测试 233 项通过;18 个包构建通过。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 13:08:11 +08:00

276 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 算法权重模块(models/)
这个目录放**算法模型权重**和它们的**类别标签**。机器人软件里"算法"和"机械/电气"
变化节奏完全不同:机械改一次要几个月,模型换一版可能一周几次。所以这一块单独
成模块,有自己的一套约定和工具,改模型不需要动任何 C++/节点代码。
> **要改这个模块的人,先读完本文。** 下面是规则,不是建议。
---
## 1. 目录长什么样
```
models/
├── README.md ← 本文(模块规则)
├── staging/ ← 非标准格式先丢这里,等转换
│ ├── .gitkeep
│ └── done/ ← 转换完成后源文件自动归档到这里
├── yolo/ ← 一个"分组"(就是个子目录,名字随意)
│ ├── detect_best_0.onnx
│ ├── detect_best_0.engine
│ └── detect_best_0.labels.yaml
└── hole_insert/
├── detect_best_1.onnx
└── detect_best_1.labels.yaml
```
**分组**只是一个子目录名,用来分门别类,没有程序语义。默认用 `yolo`。
---
## 2. 命名规则(重要)
同一个模型的**所有格式 + 标签**必须用**同一个名字主干**、放在**同一个目录**:
| 文件 | 作用 | 进 git? |
|---|---|---|
| `<名字>.pt` | PyTorch 原始权重 | 否 |
| `<名字>.onnx` | 中间格式,CPU 推理后端也用 | 否 |
| `<名字>.engine` | TensorRT 引擎,GPU 推理用 | 否 |
| `<名字>.labels.yaml` | **类别标签** | **是** |
因为推理代码是按"同目录同主干"去找配套文件的(`resolve_weights` 找 `.engine`,
标签找 `.labels.yaml`)。所以:
- ✅ `detect_best_0.onnx` + `detect_best_0.engine` + `detect_best_0.labels.yaml`
- ❌ `detect_best_0.onnx` + `detect_best_0_fp16.engine`(名字对不上,找不到)
- ❌ `detect_best_0.onnx` + `labels_detect_best_0.yaml`(标签名对不上)
**一份标签服务同一个模型的所有格式。** 换 `.pt`→`.engine` 后端时类名不会变。
---
## 3. 标签(class names)
### 为什么要有单独的标签文件
模型输出的是**类别索引**(0/1/2…),要翻译成人看得懂的名字。这件事原先没有统一
做法,三种后端各来一套,而且**TensorRT 那一环会把标签弄丢**。
丢的原因是硬的:TensorRT 引擎要用 `trt.OnnxParser` 重新构建,而
**当前安装的 TensorRT(10.13)Python API 根本没有 metadata 读写接口** ——
`INetworkDefinition` / `IBuilderConfig` / `ICudaEngine` 上都没有任何 metadata 方法。
实测二进制里搜一下就能看出来:
```
detect_best_0.engine 搜不到 "workpiece" → 标签已丢
```
丢了的后果很隐蔽:读不到名字时代码会退化成把索引当名字用(类名变成 `"0"`),
而预览界面又不画类名 —— **没有任何地方看得出标签丢了**。
所以标签放在**旁边的文本文件**里。它不是"退而求其次",而是唯一可行且有好处:
1. **可修改** —— 改标签不用重新转换模型,存盘即生效;
2. **跨格式统一** —— `.pt`/`.onnx`/`.engine` 共用一份,换后端名字不变。
### 写法
`<名字>.labels.yaml`:
```yaml
model: detect_best_0 # 可选,写对了能帮你发现"标签文件拷错了模型"
names:
- workpiece # 第 1 行 = 类别 0
- tongzhigui # 第 2 行 = 类别 1
```
第 1 行对应类别 0,第 2 行对应类别 1,依次类推。也认这些写法(从别处拷配置常见):
```yaml
names: {0: workpiece, 1: tongzhigui} # 字典形式(Ultralytics data.yaml 风格)
```
```yaml
names: "workpiece, tongzhigui" # 逗号串
```
```yaml
workpiece # 纯文本,一行一个(classes.txt 风格,注释行以 # 开头)
```
### 读取优先级
```
<名字>.labels.yaml > ONNX 内嵌 metadata > 模型内嵌 metadata > 占位符 class_0
```
旁挂文件**必须**赢,因为它是人能改的那一份。否则"我明明改了标签为什么不生效"
会变成一个很难查的问题。
全都读不到时会用占位名 `class_0`、`class_1`…,并且**打一条 warning**。
刻意不用裸数字 `"0"` —— 那个和"真的有个叫 0 的类"分不出来。
### 怎么改 / 怎么看
```bash
bash scripts/weights.sh labels detect_best_0 # 看
bash scripts/weights.sh labels detect_best_0 --set "工件,通止规" # 直接改
bash scripts/weights.sh labels detect_best_0 --set-file classes.txt # 从文本文件导入
bash scripts/weights.sh labels detect_best_0 --from-model # 把模型内嵌标签抄出来变成可编辑
bash scripts/weights.sh labels detect_best_0 --json # 给程序读
bash scripts/weights.sh labels detect_best_0 --clear # 删掉,回到模型内嵌
```
改完**重跑一下节点**就生效,不用重新转换模型、不用重建 engine。
---
## 4. 加一个新模型(完整流程)
### 步骤 1:把原始文件丢进 staging
不管它原来叫什么、什么格式,先扔进 `models/staging/`:
```bash
cp ~/下载/best_v3_final\(1\).pt ~/ros2/models/staging/
```
### 步骤 2:一条命令转换 + 改名 + 归位
```bash
bash ~/ros2/scripts/weights.sh convert staging/best_v3_final\(1\).pt --name workpiece --group yolo
```
这条命令会:
1. `--name workpiece` → 产出统一叫 `workpiece.onnx` / `workpiece.engine`(**自动改名**)
2. 依次跑 `PT → ONNX → engine`(TensorRT 只能由 ONNX 构建,所以中间产物必出)
3. 把产出放到 `models/yolo/`(**自动归位**)
4. **顺带把标签落成 `workpiece.labels.yaml`**(从源模型内嵌 metadata 抄)
5. 把 staging 里的源文件挪到 `models/staging/done/`(staging 永远是一份"待办清单")
常用选项:
| 选项 | 用途 |
|---|---|
| `--to onnx` | 只要 ONNX,不建 engine(不需要 GPU) |
| `--to engine` | 只要 engine(仍会先产出 onnx 做中间产物) |
| `--imgsz 640` | 输入尺寸(默认取 `config/site.yaml` 的 `yolo.imgsz`) |
| `--labels "工件,通止规"` | 顺手写标签 |
| `--group <分组>` | 输出到别的子目录 |
| `--force` | 覆盖已存在的产出(不加则拒绝覆盖,防误删) |
| `--dry-run` | 只看计划,不动文件 |
| `--no-fp16` | engine 不用 FP16 |
| `--keep-staging` | 保留 staging 里的源文件 |
### 步骤 3:让配置指向它
改 `config/site.yaml`:
```yaml
paths:
yolo_weights: "/home/USER/ros2/models/yolo/workpiece.onnx" # 写 onnx;engine 会自动找同名的
yolo:
backend: "tensorrt" # tensorrt / onnx / auto
imgsz: 640 # 必须等于该模型的输入尺寸
```
> 写 `.onnx` 路径即可。`backend: tensorrt` 时程序会自动找同名的 `.engine`;
> 写 `.pt` 也一样能找到 `.engine`。所以换后端不用改配置。
### 步骤 4:确认
```bash
bash ~/ros2/scripts/weights.sh list # 模型在不在、标签读不读得出来
bash ~/ros2/scripts/weights.sh doctor # 体检
```
---
## 5. 体检与排错
```bash
bash ~/ros2/scripts/weights.sh doctor --check-engines
```
会检查:
- 每个模型的标签来源;缺标签的会指出期望的文件名
- **engine 能不能被当前 TensorRT 加载**(见下)
- `config/site.yaml` 里指向不存在的文件的**悬空引用**
- `backend: tensorrt` 但同名 `.engine` 不存在(启动会直接失败)
- staging 里有没有待转换的文件
### 常见问题
**「engine 明明在,启动却说加载失败」**
**引擎和 TensorRT 版本是绑死的。** 换了 TensorRT(重装环境、升级 CUDA)
之后旧引擎会直接反序列化失败,报 `incompatible serialization version` 之类。
表现是"文件在、大小也对,就是不能用"。用 `doctor --check-engines` 能查出来,
重导一次即可:
```bash
bash scripts/weights.sh convert models/yolo/<名字>.onnx --name <名字> --to engine --force
```
> 本仓库实测过一次:`detect_hc90.engine` 用旧版 TensorRT 构建,当前的 10.13 直接
> 加载不了,而 ONNX 完全正常。该模型已作为零引用模型移出仓库
> (见 `models/yolo/README.md`),但这个坑对**任何**要长期存放的 engine 都成立:
> 重装环境之后,第一件事就是 `doctor --check-engines`。
**「类名显示成 `class_0`」**
标签没配上。`weights.sh labels <名字>` 会告诉你缺什么,然后
`--from-model` 或 `--set` 补一份。
**「同一个模型,换个后端类名就变了」**
不该发生。如果出现了,说明少了 `.labels.yaml`,各后端的回退来源不一致。
补上旁挂文件即可。
**「`convert` 说目标已存在」**
默认不覆盖,防止把别人的模型冲掉。确认要覆盖加 `--force`。
---
## 6. 规则(改动本模块时必须遵守)
1. **二进制不进 git。** 靠 `.gitignore` 的通配规则挡住。新加分组不用改 `.gitignore`。
2. **`.labels.yaml` 要进 git。** 它是配置,体积小,是团队间的唯一事实来源。
3. **不要把类名写死在代码里。** 一律走 `arm_application.model_labels`。
已清理过的反例:`onnx_workpiece.py` 里曾硬编码 `{0: "workpiece"}`,
`hole_detect_hough.py` 里曾硬编码框上画的字 `"gongjian"`(而真类名是 `workpiece`)。
4. **不要在"就地"改权重名字来换模型。** 用 `weights.sh convert --name`,
让工具保证"名字主干一致 + 同目录"这条不变量。
5. **不要手工搬 engine 到别的目录或改名。** 改名会切断和 onnx/标签的关联。
6. **新增推理后端时,标签必须走 `model_labels.read_labels()`**,
否则又会造出一个"某个后端读不到标签"的坑。
7. **`models/staging/` 不是永久存储。** 它只放"待转换"的东西。
---
## 7. 代码在哪
| 位置 | 作用 |
|---|---|
| `arm_application/model_labels.py` | 标签读写、来源优先级、占位符(纯逻辑,无 ROS) |
| `arm_application/weights_layout.py` | 目录布局、模型发现、转换链规划(纯逻辑,无 ROS) |
| `scripts/weights.py` | 命令行实现(转换真正干活的地方) |
| `scripts/weights.sh` | 薄封装:准备环境 → 转发给 `weights.py` |
| `scripts/export_yolo_trt.sh` | **旧命令**,现在转发到 `weights.sh`,保持兼容 |
| `arm_application/yolo_detector.py` | 运行时加载模型 + `resolve_class_labels()` |
| `arm_application/onnx_workpiece.py` | ONNX Runtime 后端,读标签同样走 `model_labels` |
测试:`src/arm_application/test/test_model_labels.py`(标签)、
`test_weights_layout.py`(布局/发现/CLI)。
---
## 8. 相关文档
- `docs/SITE_CONFIG.md` —— `paths.yolo_weights` / `yolo.backend` 等配置项
- `docs/WORKSPACE.md` —— 工作区总览
- `docs/TRACKING.md` —— 换模型要同时改哪几处