feat: 2.18.9 靶规格YOLO分类重构

- 换用 model_325964(class_id: 0=40cm, 1=靶心, 2=20cm)
- class_id=1 解析阶段直接过滤,不参与上报选择
- 上报仅在有效靶规格(20/40)中取置信度最高的一条
- 0 结果时按 RETRY_CONF_TH 自动重检一次
- 增加检测调试打印(raw/detections/上报/忽略计数)
This commit is contained in:
2026-10-22 18:13:47 +08:00
parent 2459bb3b67
commit 496085cb93
6 changed files with 131 additions and 57 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
id: t11
name: t11
version: 2.18.6
version: 2.18.9
author: t11
icon: ''
desc: t11
@@ -18,8 +18,8 @@ files:
- laser_manager.py
- logger_manager.py
- main.py
- model_317828.cvimodel
- model_317828.mud
- model_325964.cvimodel
- model_325964.mud
- network.py
- ota_curl.sh
- ota_manager.py
+10 -6
View File
@@ -262,14 +262,18 @@ TRIANGLE_SAMPLE_PATCH_HALF_PX = 2
# 开机阶段预加载 YOLO detector;detect 使用 dual_buff=False,避免返回上一帧结果。
TRIANGLE_YOLO_PRELOAD_ON_BOOT = False
# YOLO target size classification: class 0=20cm, class 1=40cm.
# YOLO target size classification: class_id → label,下标即 class_id:0=40cm, 1=0(靶心), 2=20cm.
TARGET_CLASS_YOLO_ENABLE = True
TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_317828.mud"
TARGET_CLASS_YOLO_LABELS = (20, 40)
TARGET_CLASS_YOLO_CONF_TH = 0.66
TARGET_CLASS_YOLO_IOU_TH = 0.45
TARGET_CLASS_YOLO_RETRY_ON_EMPTY = False
TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_325964.mud"
TARGET_CLASS_YOLO_LABELS = (40, 0, 20)
# 只有映射结果落在该集合内才用于上报(如 class 1→label 0 直接忽略)
TARGET_CLASS_YOLO_VALID_LABELS = (20, 40)
# 解析阶段直接丢弃的 class_id(1=靶心),不参与任何选择/上报
TARGET_CLASS_YOLO_IGNORE_CLASS_IDS = (1,)
TARGET_CLASS_YOLO_CONF_TH = 0.5
# 首次检测 0 结果时用该阈值重新检测一次(≥CONF_TH 时按 CONF_TH 同参重跑)
TARGET_CLASS_YOLO_RETRY_CONF_TH = 0.25
TARGET_CLASS_YOLO_IOU_TH = 0.45
TARGET_CLASS_YOLO_PRELOAD_ON_BOOT = True
# ── 第二段 YOLO:仅在 Stage1 裁切出的靶环图上推理(与合成 stage2 训练数据一致)→ 子框内传统算法取直角点 ──
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
[basic]
type = cvimodel
model = model_317828.cvimodel
model = model_325964.cvimodel
[extra]
model_type = yolov5
@@ -9,5 +9,5 @@ input_type = rgb
mean = 0, 0, 0
scale = 0.00392156862745098, 0.00392156862745098, 0.00392156862745098
anchors = 10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326
labels = 20, 40
labels = 40, 00, 20
+108 -38
View File
@@ -310,7 +310,9 @@ def _det_obj_score(o):
def try_get_target_class_from_yolo(maix_frame, logger=None):
"""Classify the current target as 20cm or 40cm; return None if unknown."""
"""调用 YOLO 检测:class_id∈IGNORE_CLASS_IDS(默认 1) 的框在解析时直接过滤;
detections 输出其余全部框(不按置信度过滤);
上报字段 label/class_id/confidence 仅在有效靶规格(20/40)中取置信度最高的一条。"""
try:
import config as cfg
except Exception:
@@ -333,64 +335,132 @@ def try_get_target_class_from_yolo(maix_frame, logger=None):
conf_th = float(getattr(cfg, "TARGET_CLASS_YOLO_CONF_TH", 0.5))
iou_th = float(getattr(cfg, "TARGET_CLASS_YOLO_IOU_TH", 0.45))
labels = getattr(cfg, "TARGET_CLASS_YOLO_LABELS", (20, 40))
# class_id → 靶规格标签,下标即 class_id:0→40, 1→0, 2→20
labels = getattr(cfg, "TARGET_CLASS_YOLO_LABELS", (40, 0, 20))
if isinstance(labels, str):
labels = tuple(x.strip() for x in labels.split(",") if x.strip())
labels = tuple(labels)
def _detect(threshold):
# 只有映射到这些标签的检测才算有效靶规格(class 1→label 0 直接忽略)
valid_labels = getattr(cfg, "TARGET_CLASS_YOLO_VALID_LABELS", (20, 40))
if isinstance(valid_labels, str):
valid_labels = tuple(
int(float(x.strip())) for x in valid_labels.split(",") if x.strip()
)
valid_labels = frozenset(int(float(v)) for v in valid_labels)
# 解析阶段就丢弃这些 class_id(1=靶心),不进入 detections,也不参与上报选择
ignore_class_ids = getattr(cfg, "TARGET_CLASS_YOLO_IGNORE_CLASS_IDS", (1,))
if isinstance(ignore_class_ids, int):
ignore_class_ids = (ignore_class_ids,)
if isinstance(ignore_class_ids, str):
ignore_class_ids = tuple(
int(float(x.strip())) for x in ignore_class_ids.split(",") if x.strip()
)
ignore_class_ids = frozenset(int(float(v)) for v in ignore_class_ids)
n_ignored = [0]
def _run(threshold):
try:
raw = detector.detect(maix_frame, conf_th=threshold, iou_th=iou_th)
return detector.detect(maix_frame, conf_th=threshold, iou_th=iou_th)
except Exception as exc:
print(f"[YOLO-TARGET] detect 异常: {exc}")
if logger:
logger.warning(f"[YOLO-TARGET] detect 异常: {exc}")
return []
return _normalize_objs(raw if raw is not None else [])
return None
def _candidates(objs):
found = []
for obj in objs:
def _obj_box(o):
try:
return (
round(float(o.x), 1),
round(float(o.y), 1),
round(float(o.w), 1),
round(float(o.h), 1),
)
except (TypeError, ValueError, AttributeError):
return None
def _parse(raw_result):
out = []
n_ignored[0] = 0
for obj in _normalize_objs(raw_result if raw_result is not None else []):
class_id = _det_obj_class_id(obj)
if class_id is None or class_id < 0 or class_id >= len(labels):
if class_id is None or class_id in ignore_class_ids:
n_ignored[0] += 1
continue
label = None
if 0 <= class_id < len(labels):
try:
label = int(float(labels[class_id]))
except (TypeError, ValueError):
continue
if label in (20, 40):
found.append((label, class_id, _det_obj_score(obj)))
return found
objects = _detect(conf_th)
candidates = _candidates(objects)
if logger and objects:
logger.info(
"[YOLO-TARGET] 原始框=%d, 解析类别=%s"
% (
len(objects),
[(_det_obj_class_id(o), _det_obj_score(o)) for o in objects[:8]],
label = None
out.append(
{
"class_id": class_id,
"label": label,
"confidence": round(_det_obj_score(obj), 4),
"box_xywh": _obj_box(obj),
}
)
return out
# 第 1 次检测
raw = _run(conf_th)
detections = _parse(raw)
print(f"[YOLO-TARGET] 第1次 conf_th={conf_th} iou_th={iou_th} raw={raw!r}")
print(
f"[YOLO-TARGET] 检测结果={detections}"
f"(已忽略 class_id∈{tuple(sorted(ignore_class_ids))} 共{n_ignored[0]} 条)"
)
if not candidates and bool(
getattr(cfg, "TARGET_CLASS_YOLO_RETRY_ON_EMPTY", False)
):
retry_th = float(getattr(cfg, "TARGET_CLASS_YOLO_RETRY_CONF_TH", conf_th))
if 0 < retry_th < conf_th:
candidates = _candidates(_detect(retry_th))
# 0 结果时重新检测一次(阈值取更小值,仍为 0 则同参数重跑)
if not detections:
retry_th = float(getattr(cfg, "TARGET_CLASS_YOLO_RETRY_CONF_TH", 0.25) or 0)
if not (0 < retry_th < conf_th):
retry_th = conf_th
print(f"[YOLO-TARGET] 0 结果,重新检测 第2次 conf_th={retry_th}")
raw_retry = _run(retry_th)
detections_retry = _parse(raw_retry)
print(f"[YOLO-TARGET] 第2次 raw={raw_retry!r}")
print(
f"[YOLO-TARGET] 检测结果={detections_retry}"
f"(已忽略 class_id∈{tuple(sorted(ignore_class_ids))} 共{n_ignored[0]} 条)"
)
if detections_retry:
raw, detections = raw_retry, detections_retry
if not candidates:
if logger:
logger.warning("[YOLO-TARGET] 当前帧未识别到 20/40,按未知处理")
logger.info(f"[YOLO-TARGET] 检测结果={detections}")
if not detections:
if logger:
logger.warning("[YOLO-TARGET] 未检测到目标(已重检 1 次)")
return None
label, class_id, confidence = max(candidates, key=lambda item: item[2])
result = {"label": label, "class_id": class_id, "confidence": confidence}
if logger:
logger.info(
f"[YOLO-TARGET] 当前帧分类={label}, class_id={class_id}, "
f"conf={confidence:.3f}"
# 上报口径:只在有效靶规格(20/40)里选置信度最高的一条,忽略 label=0 等无效类
valid = [d for d in detections if d["label"] in valid_labels]
if valid:
picked = max(valid, key=lambda d: d["confidence"])
else:
picked = {
"class_id": None,
"label": None,
"confidence": None,
"box_xywh": None,
}
print(
f"[YOLO-TARGET] 上报={picked['label']}, class_id={picked['class_id']}, "
f"conf={picked['confidence']}, 有效候选={[(d['label'], d['confidence']) for d in valid]}"
)
return result
return {
"label": picked["label"],
"class_id": picked["class_id"],
"confidence": picked["confidence"],
"detections": detections,
}
def _det_to_src_xyxy(o, coord_mode: str, src_w: int, src_h: int, net_w: int, net_h: int):
+1 -1
View File
@@ -4,6 +4,6 @@
应用版本号
每次 OTA 更新时,只需要更新这个文件中的版本号
"""
VERSION = '2.18.6'
VERSION = '2.18.9'