贴边 + 过小框过滤

This commit is contained in:
wanghanbo 2026-10-14 15:45:09 +08:00
parent e0a4f85f59
commit a095f0b776
2 changed files with 87 additions and 8 deletions

View File

@ -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])

View File

@ -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 相机,不受此项影响。