feat: 2.18.10 靶规格复检与置信度门槛

- 检出靶框裁剪放大后二次 detect 复检(conf 0.25),以复检结果为准
- 全图结果上报门槛 MIN_REPORT_CONF=0.5,复检结果 >0.25 即采纳
- 0 结果时按 RETRY_CONF_TH 自动重检;class_id=1 解析阶段过滤
- 换用 model_326162,去除调试 print 改走 logger
This commit is contained in:
2026-10-23 15:13:48 +08:00
parent d1a1d43bd8
commit 8d35bf3068
6 changed files with 154 additions and 39 deletions
+141 -33
View File
@@ -312,7 +312,8 @@ def _det_obj_score(o):
def try_get_target_class_from_yolo(maix_frame, logger=None):
"""调用 YOLO 检测:class_id∈IGNORE_CLASS_IDS(默认 1) 的框在解析时直接过滤;
detections 输出其余全部框(不按置信度过滤);
上报字段 label/class_id/confidence 仅在有效靶规格(20/40)中取置信度最高的一条。"""
上报以「检出框裁剪放大后的复检结果」为主,复检不可用时用全图结果;
有效靶规格(20/40)中取置信度最高的一条,且低于 MIN_REPORT_CONF 不上报类别。"""
try:
import config as cfg
except Exception:
@@ -365,7 +366,6 @@ def try_get_target_class_from_yolo(maix_frame, logger=None):
try:
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 None
@@ -408,57 +408,165 @@ def try_get_target_class_from_yolo(maix_frame, logger=None):
# 第 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]} 条)"
)
# 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}")
if logger:
logger.info(f"[YOLO-TARGET] 0 结果,重新检测 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
detections = detections_retry
if logger:
logger.info(f"[YOLO-TARGET] 检测结果={detections}")
logger.info(
f"[YOLO-TARGET] 检测结果={detections}"
f"(已忽略 class_id∈{tuple(sorted(ignore_class_ids))} 共{n_ignored[0]} 条)"
)
if not detections:
if logger:
logger.warning("[YOLO-TARGET] 未检测到目标(已重检 1 次)")
return None
# 上报口径:只在有效靶规格(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,
}
def _best_valid(dets):
valid = [d for d in dets if d["label"] in valid_labels]
if not valid:
return None
return max(valid, key=lambda d: d["confidence"])
print(
f"[YOLO-TARGET] 上报={picked['label']}, class_id={picked['class_id']}, "
f"conf={picked['confidence']}, 有效候选={[(d['label'], d['confidence']) for d in valid]}"
)
def _crop_target(box, margin_frac):
"""按检出框(带 margin)裁出放大 ROI;返回 Maix image,失败返回 None。"""
if not box or len(box) != 4:
return None
x, y, w, h = [float(v) for v in box]
try:
fw, fh = int(maix_frame.width()), int(maix_frame.height())
except Exception:
fw = fh = 0
mx, my = w * margin_frac, h * margin_frac
x0, y0 = int(max(0, x - mx)), int(max(0, y - my))
x1 = int(min(fw, x + w + mx)) if fw else int(x + w + mx)
y1 = int(min(fh, y + h + my)) if fh else int(y + h + my)
if (x1 - x0) < 32 or (y1 - y0) < 32:
if logger:
logger.warning(
f"[YOLO-TARGET] 复检裁剪过小 [{x0},{y0},{x1},{y1}]"
)
return None
try:
cropped = maix_frame.crop(x0, y0, x1 - x0, y1 - y0)
if cropped is not None:
return cropped
except Exception:
pass
try:
from maix import image as maix_image
arr = maix_frame.to_numpy()
slab = np.ascontiguousarray(arr[y0:y1, x0:x1], dtype=np.uint8)
if slab.size == 0:
return None
return maix_image.cv2image(slab, False, True)
except Exception as exc:
if logger:
logger.warning(f"[YOLO-TARGET] 复检裁剪失败: {exc}")
return None
# 上报口径:只在有效靶规格(20/40)里取置信度最高的一条,class_id=1 已在解析阶段过滤
full_best = _best_valid(detections)
# 二次裁剪复检:把全图检出的靶框裁剪放大后再 detect 一次,与全图结果互相印证
# 复检用更低阈值 RECHECK_CONF_TH,置信度大于该值即接受
recheck_th = float(getattr(cfg, "TARGET_CLASS_YOLO_RECHECK_CONF_TH", 0.25))
crop_best = None
recheck_status = "off"
if full_best is not None and bool(
getattr(cfg, "TARGET_CLASS_YOLO_RECHECK_CROP", True)
):
recheck_status = "fail"
margin_frac = float(
getattr(cfg, "TARGET_CLASS_YOLO_RECHECK_MARGIN_FRAC", 0.1)
)
crop_img = _crop_target(full_best["box_xywh"], margin_frac)
if crop_img is not None:
try:
raw_crop = detector.detect(
crop_img, conf_th=recheck_th, iou_th=iou_th
)
except Exception as exc:
raw_crop = None
if logger:
logger.warning(f"[YOLO-TARGET] 复检 detect 异常: {exc}")
if raw_crop is not None:
crop_dets = _parse(raw_crop)
recheck_status = "ok"
crop_best = _best_valid(crop_dets)
if crop_best is not None and crop_best["confidence"] <= recheck_th:
crop_best = None
if logger:
logger.info(
f"[YOLO-TARGET] 复检(conf_th={recheck_th}, "
f"裁剪框={full_best['box_xywh']}) 结果={crop_dets}"
)
# 合并:以复检(裁剪放大)结果为主;复检无结果才用全图
picked = full_best
picked_from_crop = False
note = ""
if recheck_status == "ok":
if crop_best is None:
note = f"复检无 conf>{recheck_th} 的有效框,沿用全图"
else:
picked = crop_best
picked_from_crop = True
if crop_best["label"] == full_best["label"]:
note = f"复检一致({crop_best['label']}),以复检为准"
else:
note = (
f"复检与全图不一致(全图={full_best['label']}@{full_best['confidence']}, "
f"裁剪={crop_best['label']}@{crop_best['confidence']}),以复检为准"
)
# 置信度门槛:低于阈值不上报类别,避免硬猜(复检结果只需 > RECHECK_CONF_TH)
if picked is not None:
if picked_from_crop:
min_report_conf = recheck_th
below = picked["confidence"] <= min_report_conf
else:
min_report_conf = float(
getattr(cfg, "TARGET_CLASS_YOLO_MIN_REPORT_CONF", 0.5)
)
below = picked["confidence"] < min_report_conf
if below:
note = (note + "; " if note else "") + (
f"conf={picked['confidence']} 未达门槛{min_report_conf},置信度不足"
)
picked = None
if picked is None:
label, class_id, confidence = None, None, None
else:
label = picked["label"]
class_id = picked["class_id"]
confidence = picked["confidence"]
if logger:
logger.info(
f"[YOLO-TARGET] 上报={label}, class_id={class_id}, conf={confidence}, "
f"全图候选={[(d['label'], d['confidence']) for d in detections if d['label'] in valid_labels]}, "
f"复检={recheck_status} {note}"
)
if note:
logger.warning(f"[YOLO-TARGET] {note}")
return {
"label": picked["label"],
"class_id": picked["class_id"],
"confidence": picked["confidence"],
"label": label,
"class_id": class_id,
"confidence": confidence,
"detections": detections,
}