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>
This commit is contained in:
arm_dev
2026-09-23 13:08:11 +08:00
parent 2b77ec127d
commit d289168dd0
12 changed files with 153 additions and 86 deletions
+4 -5
View File
@@ -109,13 +109,12 @@ paths:
# 2) 这一行改成那个绝对路径(写 .onnx 即可;TensorRT 会找同名 .engine)
# 3) backend=tensorrt 时必须先导出引擎(只需做一次):
# bash ~/ros2/scripts/export_yolo_trt.sh /home/USER/ros2/models/yolo/detect_best_0.onnx 640
# 4) yolo.imgsz 必须等于 ONNX 输入边长(本文件 640;旧 hc90 才是 416)
# 4) yolo.imgsz 必须等于 ONNX 输入边长(当前 640)
# 5) 改权重/imgsz 后重启:bash ~/ros2/scripts/run_visual_servo_rt.sh [--control]
# 当前:detect_best_0.onnx(2026-09-04 从 ~/下载 移入,640 输入)
yolo_weights: "/home/USER/ros2/models/yolo/detect_best_0.onnx"
# yolo_weights: "/home/USER/ros2/models/yolo/detect_best.onnx"
# yolo_weights: "/home/USER/ros2/models/yolo/detect_hc90.pt"
# yolo_weights: "/home/USER/ros2/models/yolo/detect_yolo26_gongjian.pt"
# 备选模型:直接换成 models/yolo/ 下的其他 .onnx / .pt 绝对路径即可。
# 换完建议确认标签没丢:bash ~/ros2/scripts/weights.sh doctor --check-engines
# 入孔 YOLO(backend=yolo 时把孔当框)。two_stage 用上面的工件权重。
hole_weights: "/home/USER/ros2/models/hole_insert/detect_best_1.onnx"
# 下面是缺省(未匹配到 robots[] 时)。真机以 robots[].paths 为准:89 / 91 各一份。
@@ -202,7 +201,7 @@ cameras:
yolo:
conf: 0.10 # 检测器门槛:略低于 min_conf,让运动模糊框也能进跟踪
iou: 0.45
imgsz: 640 # detect_best.onnx 固定 640;hc90.engine 才是 416
imgsz: 640 # 必须等于所用 ONNX 的输入边长(当前 detect_best_0 = 640)
device: "0"
half_precision: true
backend: "tensorrt" # 强制 .engine;没有则启动失败。导出:bash ~/ros2/scripts/export_yolo_trt.sh
+4 -2
View File
@@ -214,8 +214,10 @@ bash ~/ros2/scripts/weights.sh doctor --check-engines
bash scripts/weights.sh convert models/yolo/<名字>.onnx --name <名字> --to engine --force
```
> 实测本仓库里的 `detect_hc90.engine` 就是这样失效的(用旧版 TensorRT 构建,
> 当前 10.13 无法加载)。另外两个 engine 正常。
> 本仓库实测过一次:`detect_hc90.engine` 用旧版 TensorRT 构建,当前的 10.13 直接
> 加载不了,而 ONNX 完全正常。该模型已作为零引用模型移出仓库
> (见 `models/yolo/README.md`),但这个坑对**任何**要长期存放的 engine 都成立:
> 重装环境之后,第一件事就是 `doctor --check-engines`。
**「类名显示成 `class_0`」**
+11 -12
View File
@@ -9,22 +9,21 @@
## 现状
- `detect_best_1.onnx` —— 工件检测模型,被 `hole_detect.workpiece_weights` 使用。
- `detect_best_1.onnx` —— 被 `hole_detect.workpiece_weights` 使用(`two_stage` 后端)。
这是本目录唯一在用的模型。
## 历史遗留(已知问题)
## 已修掉的悬空引用
`config/site.yaml` 的这两处仍指向一个**不存在**的文件 `models/hole_insert/best.pt`:
`config/site.yaml` 的 `paths.hole_weights` 与 `hole_detect.weights` 原先都指向
**不存在**的文件 `models/hole_insert/best.pt`。`backend: two_stage` 时这两处不被读取,
所以一直没暴露;一旦切成 `backend: yolo` 就会启动失败。
```yaml
paths:
hole_weights: ".../models/hole_insert/best.pt" # ← 文件不存在
hole_detect:
weights: ".../models/hole_insert/best.pt" # ← 文件不存在
```
现已改为实际存在的 `detect_best_1.onnx`。同一个悬空的 `best.pt` 还藏在代码里
(`hole_detector.py` 的 `weights_from_site` / `_default_model_path`,那里三个候选
路径全都不存在,函数却总会返回第一个),也一并修成了以 site.yaml 为准、
再退到实际存在的模型。
`backend: two_stage` 时这两处不会被读,所以现在不影响运行;但一旦切成
`backend: yolo` 就会启动失败。用 `bash ~/ros2/scripts/weights.sh doctor`
能查出来。要么把真实权重按命名规则放进本目录,要么把这两处配置删掉。
改权重相关路径后,用 `bash ~/ros2/scripts/weights.sh doctor` 复查悬空引用。
## 加新模型 / 看标签
+15 -6
View File
@@ -38,6 +38,19 @@ bash ~/ros2/scripts/weights.sh labels detect_best_0
bash ~/ros2/scripts/weights.sh labels detect_best_0 --set "工件,通止规"
```
## 工作区里有哪些模型
只保留**真正在用**的两个,避免 `weights.sh list` 被无用模型干扰、也避免误选到废模型:
| 模型 | 用途 |
|---|---|
| `yolo/detect_best_0.onnx` + `.engine` | 工件/跟踪模型,即 `config/site.yaml → paths.yolo_weights` |
| `hole_insert/detect_best_1.onnx` | 孔/工件检测,即 `hole_detect.weights` 与 `workpiece_weights` |
曾经在这里、现已移出仓库的模型(`detect_hc90`、`detect_best`、`detect_yolo26_gongjian`)
是零引用的历史遗留,二进制权重不进 git,所以是**移出到备份目录**而非直接删除;
需要时搬回本目录即可,`weights.sh list` 会自动重新发现。
## 排错
```bash
@@ -46,9 +59,5 @@ bash ~/ros2/scripts/weights.sh doctor --check-engines
`engine` 和 TensorRT 版本绑死:换过 TensorRT 后旧 engine 会加载失败,重导即可
(`convert <同名>.onnx --name <名字> --to engine --force`)。
## 历史遗留
`active.pt` 是个指向 `detect_hc90.pt` 的符号链接,早期用来"切换当前模型",
现在没有任何节点会读它 —— 换模型请改 `config/site.yaml` 的 `paths.yolo_weights`。
留着只是不想动别人的文件,可以安全删除。
**注意 `doctor` 能查出这类静默失效** —— engine 反序列化返回空时不会报错,
只会安静地不退任何检测框,很容易被当成"模型效果差"。
-16
View File
@@ -1,16 +0,0 @@
# 模型类别标签(class names)
#
# 一个模型对应一份标签,放在权重旁边、同名前缀:
# detect_best_0.engine ← 权重
# detect_best_0.labels.yaml ← 本文件
#
# 写法:names 下面一行一个类名,第 1 行对应类别 0,第 2 行对应类别 1,依次类推。
# 改完保存即生效,不需要重新转换模型、也不用重启环境(重跑节点即可)。
#
# 命令行查看/修改(推荐,不用手改本文件):
# bash ~/ros2/scripts/weights.sh labels <模型名>
# bash ~/ros2/scripts/weights.sh labels <模型名> --set "工件,通止规"
model: detect_best
names:
- workpiece
-16
View File
@@ -1,16 +0,0 @@
# 模型类别标签(class names)
#
# 一个模型对应一份标签,放在权重旁边、同名前缀:
# detect_best_0.engine ← 权重
# detect_best_0.labels.yaml ← 本文件
#
# 写法:names 下面一行一个类名,第 1 行对应类别 0,第 2 行对应类别 1,依次类推。
# 改完保存即生效,不需要重新转换模型、也不用重启环境(重跑节点即可)。
#
# 命令行查看/修改(推荐,不用手改本文件):
# bash ~/ros2/scripts/weights.sh labels <模型名>
# bash ~/ros2/scripts/weights.sh labels <模型名> --set "工件,通止规"
model: detect_hc90
names:
- workpiece
@@ -1,16 +0,0 @@
# 模型类别标签(class names)
#
# 一个模型对应一份标签,放在权重旁边、同名前缀:
# detect_best_0.engine ← 权重
# detect_best_0.labels.yaml ← 本文件
#
# 写法:names 下面一行一个类名,第 1 行对应类别 0,第 2 行对应类别 1,依次类推。
# 改完保存即生效,不需要重新转换模型、也不用重启环境(重跑节点即可)。
#
# 命令行查看/修改(推荐,不用手改本文件):
# bash ~/ros2/scripts/weights.sh labels <模型名>
# bash ~/ros2/scripts/weights.sh labels <模型名> --set "工件,通止规"
model: detect_yolo26_gongjian
names:
- gongjian
+1 -1
View File
@@ -20,7 +20,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck disable=SC1091
source "${ROOT}/scripts/lib/load_site_config.sh" >/dev/null 2>&1 || true
WEIGHTS="${1:-${SITE_YOLO_WEIGHTS:-${ROOT}/models/yolo/detect_best.onnx}}"
WEIGHTS="${1:-${SITE_YOLO_WEIGHTS:-${ROOT}/models/yolo/detect_best_0.onnx}}"
IMGSZ="${2:-${SITE_YOLO_IMGSZ:-640}}"
if [[ ! -f "${WEIGHTS}" ]]; then
@@ -15,7 +15,13 @@ import yaml
_PKG_SHARE_FALLBACK = Path(__file__).resolve().parents[1] / "config"
DEFAULT_CONFIG = _PKG_SHARE_FALLBACK / "hole_insert.yaml"
DEFAULT_MODEL_DIR = Path.home() / "ros2" / "models" / "hole_insert"
# 仓库里**实际存在**的孔/工件检测模型,相对仓库根。
# 以前这个位置上写的是 ``models/hole_insert/best.pt`` —— 文件根本不存在,属于悬空引用:
# 配置齐全时永远走不到这条分支,一旦 site.yaml 缺键就报"模型文件不存在",
# 而打印出来的路径看着特别合理,很容易被误判成"文件被人误删了"。
# 改动这个常量时,顺手跑 ``bash scripts/weights.sh doctor`` 确认没有悬空引用。
_CANONICAL_HOLE_MODEL = "models/hole_insert/detect_best_1.onnx"
@dataclass
@@ -91,20 +97,29 @@ def weights_from_site(data: dict[str, Any] | None = None) -> str:
return str(
hd.get("weights")
or paths.get("hole_weights")
or str(ros2_root() / "models" / "hole_insert" / "best.pt")
or str(ros2_root() / _CANONICAL_HOLE_MODEL)
)
def _default_model_path() -> Path:
candidates = [
DEFAULT_MODEL_DIR / "best.pt",
DEFAULT_MODEL_DIR / "hole_best.pt",
Path.home() / "ros2" / "models" / "yolo" / "detect_best.pt",
]
for p in candidates:
if p.exists():
"""未显式给 ``model_path`` 时的兜底模型路径。
以前这里枚举了 ``hole_insert/best.pt`` / ``hole_best.pt`` / ``yolo/detect_best.pt``,
三个文件在本仓库里**都不存在**,函数却会 ``return candidates[0]`` ——
于是永远返回一个不存在的路径(同 ``weights_from_site`` 里那个悬空的 best.pt)。
现在以 site.yaml 为准(它才是真正的来源),它给的路径存在就用它;
否则退到仓库里实际存在的规范模型。这样报错信息里的路径至少是对的。
"""
configured = weights_from_site()
if configured:
p = Path(configured).expanduser()
if p.is_file():
return p
return candidates[0]
from arm_application.site_io import ros2_root
return ros2_root() / _CANONICAL_HOLE_MODEL
class HoleDetector:
@@ -15,7 +15,10 @@
所以 engine 构建完就只剩张量,标签无从附着。实测:
detect_best_0.engine 二进制里搜不到 "workpiece" → 标签已丢
detect_hc90.engine 能搜到 "workpiece" → 旧版 TRT 建的,且已无法加载
detect_hc90.engine 能搜到 "workpiece" → 旧版 TRT 建的,但当前环境已无法加载
(`detect_hc90` 已作为零引用模型移出仓库,这里保留这行是因为它同时印证了两件事:
旧版 TRT 建的 engine 里其实是有标签的,以及 engine 与 TRT 版本绑死。)
后果很隐蔽:`yolo_detector` 读不到名字时会退化成 `str(cid)`,也就是类名变成字符串
`"0"`;而预览界面又不绘制类名,所以**没有任何地方看得出标签丢了**。
+3 -1
View File
@@ -1,5 +1,7 @@
# 孔定位检测配置(改 conf / 内参 / 双孔间距即可,不必改代码)
# 权重默认:~/ros2/models/hole_insert/best.pt
# 权重来源:优先 site.yaml(hole_detect.weights / paths.hole_weights);
# 两者都缺时兜底到 models/hole_insert/detect_best_1.onnx。
# 原先这里注释写的是 best.pt —— 该文件不存在,已修正。
# backend: yolo | hough | two_stage(two_stage 请用 hole_detect.yaml)
model_path: ""
@@ -0,0 +1,86 @@
"""`hole_detector` 权重路径解析的回归测试。
重点是挡住一类**悬空引用**:代码里写死的默认权重路径指向一个根本不存在的文件。
这种 bug 特别难查,因为
* 配置齐全时永远走不到那条分支,日常运行完全正常;
* 一旦 site.yaml 缺键才会触发,而报错信息里打印的路径看起来非常合理
(`.../models/hole_insert/best.pt`),很容易被误判成"文件被人误删了"。
这里**不断言文件存在** —— 二进制权重不进 git,新克隆的仓库里 `models/` 是空的。
只断言"解析结果指向规范模型名",这样在任何环境下都成立。
"""
from __future__ import annotations
from arm_application.hole_detector import _default_model_path, weights_from_site
CANONICAL = "detect_best_1.onnx"
# ---------------------------------------------------------------------------
# weights_from_site:site.yaml 缺键时的兜底
# ---------------------------------------------------------------------------
def test_weights_from_site_prefers_explicit_config():
data = {
"hole_detect": {"backend": "yolo", "weights": "/custom/my_holes.onnx"},
"paths": {"hole_weights": "/custom/ignored.onnx"},
}
assert weights_from_site(data) == "/custom/my_holes.onnx"
def test_weights_from_site_falls_back_to_paths():
data = {
"hole_detect": {"backend": "yolo"},
"paths": {"hole_weights": "/custom/from_paths.onnx"},
}
assert weights_from_site(data) == "/custom/from_paths.onnx"
def test_weights_from_site_yolo_fallback_is_not_dangling():
"""回归:以前这里兜底到不存在 best.pt,现在必须指向规范模型。"""
out = weights_from_site({"hole_detect": {"backend": "yolo"}})
assert out.endswith(CANONICAL)
assert "best.pt" not in out
def test_weights_from_site_two_stage_uses_workpiece_weights():
data = {
"hole_detect": {"backend": "two_stage", "workpiece_weights": "/w/wp.onnx"},
"paths": {"hole_weights": "/should/not/be/used.onnx"},
}
assert weights_from_site(data) == "/w/wp.onnx"
def test_weights_from_site_two_stage_falls_back_to_yolo_weights():
data = {
"hole_detect": {"backend": "two_stage"},
"paths": {"yolo_weights": "/w/track.onnx"},
}
assert weights_from_site(data) == "/w/track.onnx"
# ---------------------------------------------------------------------------
# _default_model_path:没有任何配置时的兜底
# ---------------------------------------------------------------------------
def test_default_model_path_is_not_the_dangling_best_pt():
"""回归:以前枚举的三个候选全不存在,函数却 `return candidates[0]`。
于是它永远返回一个不存在的 `hole_insert/best.pt`,而调用方拿到之后
只会在真正加载模型时才炸,报错路径还很有迷惑性。
"""
p = _default_model_path()
assert p.name not in ("best.pt", "hole_best.pt"), f"又指回悬空文件了: {p}"
def test_default_model_path_is_canonical_name():
assert _default_model_path().name == CANONICAL
def test_default_model_path_is_absolute():
"""必须是绝对路径:相对路径会随 CWD 漂移,节点从不同目录启动就找不到。"""
assert _default_model_path().is_absolute()