diff --git a/app.yaml b/app.yaml index ff58127..47eeb50 100644 --- a/app.yaml +++ b/app.yaml @@ -1,6 +1,6 @@ id: t11 name: t11 -version: 2.18.9 +version: 2.18.10 author: t11 icon: '' desc: t11 @@ -18,8 +18,8 @@ files: - laser_manager.py - logger_manager.py - main.py - - model_325964.cvimodel - - model_325964.mud + - model_326162.cvimodel + - model_326162.mud - network.py - ota_curl.sh - ota_manager.py diff --git a/config.py b/config.py index 1cc0109..dc0d081 100644 --- a/config.py +++ b/config.py @@ -264,7 +264,7 @@ TRIANGLE_YOLO_PRELOAD_ON_BOOT = False # 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_325964.mud" +TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_326162.mud" TARGET_CLASS_YOLO_LABELS = (40, 0, 20) # 只有映射结果落在该集合内才用于上报(如 class 1→label 0 直接忽略) TARGET_CLASS_YOLO_VALID_LABELS = (20, 40) @@ -273,6 +273,13 @@ 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 +# 检出靶框后裁剪放大再 detect 一次,与全图结果互相印证(不一致以复检为准) +TARGET_CLASS_YOLO_RECHECK_CROP = True +TARGET_CLASS_YOLO_RECHECK_MARGIN_FRAC = 0.1 +# 复检的 detect 阈值与接受门槛:置信度大于该值即采纳复检结果 +TARGET_CLASS_YOLO_RECHECK_CONF_TH = 0.25 +# 全图结果(无复检时)的上报置信度门槛,None/0 表示不启用 +TARGET_CLASS_YOLO_MIN_REPORT_CONF = 0.5 TARGET_CLASS_YOLO_IOU_TH = 0.45 TARGET_CLASS_YOLO_PRELOAD_ON_BOOT = True diff --git a/model_325964.cvimodel b/model_326162.cvimodel similarity index 51% rename from model_325964.cvimodel rename to model_326162.cvimodel index ca6d035..180cb15 100644 Binary files a/model_325964.cvimodel and b/model_326162.cvimodel differ diff --git a/model_325964.mud b/model_326162.mud similarity index 89% rename from model_325964.mud rename to model_326162.mud index 97d06d2..e291305 100644 --- a/model_325964.mud +++ b/model_326162.mud @@ -1,7 +1,7 @@ [basic] type = cvimodel -model = model_325964.cvimodel +model = model_326162.cvimodel [extra] model_type = yolov5 diff --git a/target_roi_yolo.py b/target_roi_yolo.py index 913d78d..2914bd6 100644 --- a/target_roi_yolo.py +++ b/target_roi_yolo.py @@ -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, } diff --git a/version.py b/version.py index 71a68bb..ebb9fbc 100644 --- a/version.py +++ b/version.py @@ -4,6 +4,6 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '2.18.9' +VERSION = '2.18.10'