贴边 + 过小框过滤
This commit is contained in:
parent
e0a4f85f59
commit
a095f0b776
|
|
@ -50,6 +50,25 @@ def _load_filter_names(config):
|
||||||
return {str(n).strip() for n in (names or []) if str(n).strip()}
|
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:
|
class YOLOv11PlateExtractor:
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
@ -64,10 +83,18 @@ class YOLOv11PlateExtractor:
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.filter_class_names = _load_filter_names(config)
|
self.filter_class_names = _load_filter_names(config)
|
||||||
|
self.edge_filter = _load_edge_filter_params(config)
|
||||||
print(f"✅ Plate 模型加载成功: {weights}")
|
print(f"✅ Plate 模型加载成功: {weights}")
|
||||||
print(f"[OK] detect classes: {self.config.LABEL_NAMES}")
|
print(f"[OK] detect classes: {self.config.LABEL_NAMES}")
|
||||||
if self.filter_class_names:
|
if self.filter_class_names:
|
||||||
print(f"[OK] detect filter drop: {sorted(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):
|
def predict(self, source):
|
||||||
"""source 可为图片路径或 BGR ndarray。"""
|
"""source 可为图片路径或 BGR ndarray。"""
|
||||||
|
|
@ -92,8 +119,42 @@ class YOLOv11PlateExtractor:
|
||||||
return False
|
return False
|
||||||
return name in self.filter_class_names
|
return name in self.filter_class_names
|
||||||
|
|
||||||
def iter_kept_detections(self, results):
|
def _should_drop_incomplete_box(self, box, img_w, img_h):
|
||||||
"""产出保留的检测框:(box, conf, class_name)。手/空盘等已过滤。"""
|
"""贴边且过小 / 面积过小 → 视为入画不完整,丢弃。"""
|
||||||
|
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:
|
if results is None or results.boxes is None:
|
||||||
return
|
return
|
||||||
boxes = results.boxes.xyxy.cpu().numpy()
|
boxes = results.boxes.xyxy.cpu().numpy()
|
||||||
|
|
@ -102,20 +163,29 @@ class YOLOv11PlateExtractor:
|
||||||
clss = results.boxes.cls.cpu().numpy()
|
clss = results.boxes.cls.cpu().numpy()
|
||||||
else:
|
else:
|
||||||
clss = [0] * len(boxes)
|
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):
|
for box, conf, cls_id in zip(boxes, confs, clss):
|
||||||
cname = self._class_name(results, cls_id)
|
cname = self._class_name(results, cls_id)
|
||||||
if self._should_drop_class(cname):
|
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
|
continue
|
||||||
yield box, float(conf), cname
|
yield box, float(conf), cname
|
||||||
if dropped:
|
if dropped_cls:
|
||||||
print(f"[detect] filtered {dropped} box(es) in {sorted(self.filter_class_names)}")
|
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):
|
def crop_plate(self, image, results):
|
||||||
cropped_regions = []
|
cropped_regions = []
|
||||||
h, w = image.shape[:2]
|
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, y1, x2, y2 = map(int, box)
|
||||||
x1 = max(0, x1)
|
x1 = max(0, x1)
|
||||||
y1 = max(0, y1)
|
y1 = max(0, y1)
|
||||||
|
|
@ -197,7 +267,7 @@ class YOLOv11PlateExtractor:
|
||||||
"""
|
"""
|
||||||
img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||||
font = self._load_font()
|
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):
|
for i, (box, conf, cname) in enumerate(kept):
|
||||||
if labels is not None and i < len(labels):
|
if labels is not None and i < len(labels):
|
||||||
text = str(labels[i])
|
text = str(labels[i])
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,15 @@ DISH_DETECT_FILTER_NAMES = [
|
||||||
x.strip() for x in os.environ.get('DISH_DETECT_FILTER_NAMES', '手,空盘').split(',')
|
x.strip() for x in os.environ.get('DISH_DETECT_FILTER_NAMES', '手,空盘').split(',')
|
||||||
if x.strip()
|
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 相机,不受此项影响。
|
# 1=读 DISH_VIDEO_SOURCE_DIR,0=读相机。usb_camera_app 固定读 USB 相机,不受此项影响。
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue