From d289168dd0fa9571bc7b01d3eeacc28507710fb0 Mon Sep 17 00:00:00 2001 From: arm_dev Date: Wed, 23 Sep 2026 13:08:11 +0800 Subject: [PATCH] =?UTF-8?q?P4s=EF=BC=9A=E6=A8=A1=E5=9E=8B=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=20=E2=80=94=E2=80=94=20=E7=A7=BB=E5=87=BA=E9=9B=B6?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E6=A8=A1=E5=9E=8B=EF=BC=8C=E4=BF=AE=E6=8E=89?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E9=87=8C=E4=B8=89=E5=A4=84=E6=82=AC=E7=A9=BA?= =?UTF-8?q?=20best.pt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 先回答"这些模型到底有没有人用",用全仓库引用扫描判定,不靠猜: 在用 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 --- config/site.example.yaml | 9 +- models/README.md | 6 +- models/hole_insert/README.md | 23 +++-- models/yolo/README.md | 21 +++-- models/yolo/detect_best.labels.yaml | 16 ---- models/yolo/detect_hc90.labels.yaml | 16 ---- .../yolo/detect_yolo26_gongjian.labels.yaml | 16 ---- scripts/export_yolo_trt.sh | 2 +- .../arm_application/hole_detector.py | 35 +++++--- .../arm_application/model_labels.py | 5 +- src/arm_application/config/hole_insert.yaml | 4 +- .../test/test_hole_detector.py | 86 +++++++++++++++++++ 12 files changed, 153 insertions(+), 86 deletions(-) delete mode 100644 models/yolo/detect_best.labels.yaml delete mode 100644 models/yolo/detect_hc90.labels.yaml delete mode 100644 models/yolo/detect_yolo26_gongjian.labels.yaml create mode 100644 src/arm_application/test/test_hole_detector.py diff --git a/config/site.example.yaml b/config/site.example.yaml index db47932..a0c91e0 100644 --- a/config/site.example.yaml +++ b/config/site.example.yaml @@ -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 diff --git a/models/README.md b/models/README.md index 06ab931..c68e6ab 100644 --- a/models/README.md +++ b/models/README.md @@ -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`」** diff --git a/models/hole_insert/README.md b/models/hole_insert/README.md index 65b9158..7aaeabb 100644 --- a/models/hole_insert/README.md +++ b/models/hole_insert/README.md @@ -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` 复查悬空引用。 ## 加新模型 / 看标签 diff --git a/models/yolo/README.md b/models/yolo/README.md index 3aca446..faa2009 100644 --- a/models/yolo/README.md +++ b/models/yolo/README.md @@ -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 反序列化返回空时不会报错, +只会安静地不退任何检测框,很容易被当成"模型效果差"。 diff --git a/models/yolo/detect_best.labels.yaml b/models/yolo/detect_best.labels.yaml deleted file mode 100644 index 9e68252..0000000 --- a/models/yolo/detect_best.labels.yaml +++ /dev/null @@ -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 diff --git a/models/yolo/detect_hc90.labels.yaml b/models/yolo/detect_hc90.labels.yaml deleted file mode 100644 index 1df098d..0000000 --- a/models/yolo/detect_hc90.labels.yaml +++ /dev/null @@ -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 diff --git a/models/yolo/detect_yolo26_gongjian.labels.yaml b/models/yolo/detect_yolo26_gongjian.labels.yaml deleted file mode 100644 index 051055a..0000000 --- a/models/yolo/detect_yolo26_gongjian.labels.yaml +++ /dev/null @@ -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 diff --git a/scripts/export_yolo_trt.sh b/scripts/export_yolo_trt.sh index 5135308..b9b7824 100755 --- a/scripts/export_yolo_trt.sh +++ b/scripts/export_yolo_trt.sh @@ -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 diff --git a/src/arm_application/arm_application/hole_detector.py b/src/arm_application/arm_application/hole_detector.py index 3713acb..4cd7742 100644 --- a/src/arm_application/arm_application/hole_detector.py +++ b/src/arm_application/arm_application/hole_detector.py @@ -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: diff --git a/src/arm_application/arm_application/model_labels.py b/src/arm_application/arm_application/model_labels.py index 9bd374a..a8c299b 100644 --- a/src/arm_application/arm_application/model_labels.py +++ b/src/arm_application/arm_application/model_labels.py @@ -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"`;而预览界面又不绘制类名,所以**没有任何地方看得出标签丢了**。 diff --git a/src/arm_application/config/hole_insert.yaml b/src/arm_application/config/hole_insert.yaml index 2bd3774..37ed698 100644 --- a/src/arm_application/config/hole_insert.yaml +++ b/src/arm_application/config/hole_insert.yaml @@ -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: "" diff --git a/src/arm_application/test/test_hole_detector.py b/src/arm_application/test/test_hole_detector.py new file mode 100644 index 0000000..94f4ae3 --- /dev/null +++ b/src/arm_application/test/test_hole_detector.py @@ -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()