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
@@ -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()