From a095f0b77643fc4550290058da75361a196e10f3 Mon Sep 17 00:00:00 2001 From: wanghanbo <1411232085@qq.com> Date: Wed, 14 Oct 2026 15:45:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=B4=E8=BE=B9=20+=20=E8=BF=87=E5=B0=8F?= =?UTF-8?q?=E6=A1=86=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/detect_plate_process.py | 86 ++++++++++++++++++++++++++++++++---- dao/settings.py | 9 ++++ 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/core/detect_plate_process.py b/core/detect_plate_process.py index ca6bf15..08c5448 100644 --- a/core/detect_plate_process.py +++ b/core/detect_plate_process.py @@ -50,6 +50,25 @@ def _load_filter_names(config): return {str(n).strip() for n in (names or []) if str(n).strip()} +def _load_edge_filter_params(config): + """贴边/过小框过滤参数;可用 Config 覆盖,否则读 settings。""" + def _get(name, default): + if hasattr(config, name): + return getattr(config, name) + try: + from dao import settings as S + return getattr(S, name, default) + except Exception: + return default + + return { + "edge_margin": int(_get("DISH_DETECT_EDGE_MARGIN_PX", 8)), + "edge_min_area_ratio": float(_get("DISH_DETECT_EDGE_MIN_AREA_RATIO", 0.02)), + "edge_min_side": int(_get("DISH_DETECT_EDGE_MIN_SIDE_PX", 48)), + "min_area_ratio": float(_get("DISH_DETECT_MIN_AREA_RATIO", 0.008)), + } + + class YOLOv11PlateExtractor: def __init__(self, config): self.config = config @@ -64,10 +83,18 @@ class YOLOv11PlateExtractor: except Exception: pass self.filter_class_names = _load_filter_names(config) + self.edge_filter = _load_edge_filter_params(config) print(f"✅ Plate 模型加载成功: {weights}") print(f"[OK] detect classes: {self.config.LABEL_NAMES}") if self.filter_class_names: print(f"[OK] detect filter drop: {sorted(self.filter_class_names)}") + ef = self.edge_filter + print( + f"[OK] edge/small box filter: margin={ef['edge_margin']}px " + f"edge_area<{ef['edge_min_area_ratio']:.3f} " + f"edge_side<{ef['edge_min_side']} " + f"min_area<{ef['min_area_ratio']:.3f}" + ) def predict(self, source): """source 可为图片路径或 BGR ndarray。""" @@ -92,8 +119,42 @@ class YOLOv11PlateExtractor: return False return name in self.filter_class_names - def iter_kept_detections(self, results): - """产出保留的检测框:(box, conf, class_name)。手/空盘等已过滤。""" + def _should_drop_incomplete_box(self, box, img_w, img_h): + """贴边且过小 / 面积过小 → 视为入画不完整,丢弃。""" + try: + x1, y1, x2, y2 = [float(v) for v in box[:4]] + except Exception: + return True + bw = max(0.0, x2 - x1) + bh = max(0.0, y2 - y1) + if bw <= 1 or bh <= 1 or img_w <= 0 or img_h <= 0: + return True + area_ratio = (bw * bh) / float(img_w * img_h) + ef = self.edge_filter or {} + min_area = float(ef.get("min_area_ratio") or 0) + if min_area > 0 and area_ratio < min_area: + return True + + margin = max(0, int(ef.get("edge_margin") or 0)) + touches = ( + x1 <= margin + or y1 <= margin + or x2 >= (img_w - 1 - margin) + or y2 >= (img_h - 1 - margin) + ) + if not touches: + return False + edge_area = float(ef.get("edge_min_area_ratio") or 0) + edge_side = int(ef.get("edge_min_side") or 0) + # 贴边且「面积小」或「短边过短」→ 多半是只露一角 + if edge_area > 0 and area_ratio < edge_area: + return True + if edge_side > 0 and min(bw, bh) < edge_side: + return True + return False + + def iter_kept_detections(self, results, image_shape=None): + """产出保留的检测框:(box, conf, class_name)。类别/贴边不完整已过滤。""" if results is None or results.boxes is None: return boxes = results.boxes.xyxy.cpu().numpy() @@ -102,20 +163,29 @@ class YOLOv11PlateExtractor: clss = results.boxes.cls.cpu().numpy() else: clss = [0] * len(boxes) - dropped = 0 + img_h = img_w = None + if image_shape is not None and len(image_shape) >= 2: + img_h, img_w = int(image_shape[0]), int(image_shape[1]) + dropped_cls = 0 + dropped_edge = 0 for box, conf, cls_id in zip(boxes, confs, clss): cname = self._class_name(results, cls_id) if self._should_drop_class(cname): - dropped += 1 + dropped_cls += 1 + continue + if img_w and img_h and self._should_drop_incomplete_box(box, img_w, img_h): + dropped_edge += 1 continue yield box, float(conf), cname - if dropped: - print(f"[detect] filtered {dropped} box(es) in {sorted(self.filter_class_names)}") + if dropped_cls: + print(f"[detect] filtered {dropped_cls} box(es) by class {sorted(self.filter_class_names)}") + if dropped_edge: + print(f"[detect] filtered {dropped_edge} incomplete/edge box(es)") def crop_plate(self, image, results): cropped_regions = [] h, w = image.shape[:2] - for box, conf, cname in self.iter_kept_detections(results): + for box, conf, cname in self.iter_kept_detections(results, image_shape=image.shape): x1, y1, x2, y2 = map(int, box) x1 = max(0, x1) y1 = max(0, y1) @@ -197,7 +267,7 @@ class YOLOv11PlateExtractor: """ img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) font = self._load_font() - kept = list(self.iter_kept_detections(results)) + kept = list(self.iter_kept_detections(results, image_shape=image.shape)) for i, (box, conf, cname) in enumerate(kept): if labels is not None and i < len(labels): text = str(labels[i]) diff --git a/dao/settings.py b/dao/settings.py index 7516b52..3cb5a1a 100644 --- a/dao/settings.py +++ b/dao/settings.py @@ -130,6 +130,15 @@ DISH_DETECT_FILTER_NAMES = [ x.strip() for x in os.environ.get('DISH_DETECT_FILTER_NAMES', '手,空盘').split(',') if x.strip() ] +# 贴边不完整框过滤:托盘刚入画只露一角时 YOLO 常会检出,直接丢弃 +# 框任一边缘距画面边界 ≤ margin 像素,视为贴边 +DISH_DETECT_EDGE_MARGIN_PX = int(os.environ.get('DISH_DETECT_EDGE_MARGIN_PX', '8')) +# 贴边且框面积占整图比例低于此值 → 丢弃(默认 2%) +DISH_DETECT_EDGE_MIN_AREA_RATIO = float(os.environ.get('DISH_DETECT_EDGE_MIN_AREA_RATIO', '0.02')) +# 贴边且框较短边像素低于此值 → 丢弃 +DISH_DETECT_EDGE_MIN_SIDE_PX = int(os.environ.get('DISH_DETECT_EDGE_MIN_SIDE_PX', '48')) +# 任意框(不论是否贴边)面积占比低于此值也丢弃;0=关闭 +DISH_DETECT_MIN_AREA_RATIO = float(os.environ.get('DISH_DETECT_MIN_AREA_RATIO', '0.008')) # 用本地视频文件夹代替相机实时流(调试/回放,主要给海康入口用)。 # 1=读 DISH_VIDEO_SOURCE_DIR,0=读相机。usb_camera_app 固定读 USB 相机,不受此项影响。