From c443160d4c378aaea257d02336b2887f7d81d946 Mon Sep 17 00:00:00 2001 From: jinman Date: Tue, 25 Aug 2026 16:53:56 +0900 Subject: [PATCH 1/5] refactor: remove YOLOv6 models and update YOLOv8 image sizes for consistency --- mblt_vision/face_detection/__init__.py | 4 ---- mblt_vision/models/YOLOv6m-face.yaml | 27 -------------------------- mblt_vision/models/YOLOv6n-face.yaml | 27 -------------------------- mblt_vision/models/YOLOv8l-face.yaml | 6 ++++-- mblt_vision/models/YOLOv8m-face.yaml | 6 ++++-- 5 files changed, 8 insertions(+), 62 deletions(-) delete mode 100644 mblt_vision/models/YOLOv6m-face.yaml delete mode 100644 mblt_vision/models/YOLOv6n-face.yaml diff --git a/mblt_vision/face_detection/__init__.py b/mblt_vision/face_detection/__init__.py index 0e1ca62..10bfc4b 100644 --- a/mblt_vision/face_detection/__init__.py +++ b/mblt_vision/face_detection/__init__.py @@ -17,8 +17,6 @@ "YOLOv10m_face", "YOLOv10n_face", "YOLOv10s_face", - "YOLOv6m_face", - "YOLOv6n_face", "YOLOv8l_face", "YOLOv8m_face", "YOLOv8n_face", @@ -36,8 +34,6 @@ YOLOv10m_face = create_model_class("YOLOv10m_face", __name__) YOLOv10n_face = create_model_class("YOLOv10n_face", __name__) YOLOv10s_face = create_model_class("YOLOv10s_face", __name__) -YOLOv6m_face = create_model_class("YOLOv6m_face", __name__) -YOLOv6n_face = create_model_class("YOLOv6n_face", __name__) YOLOv8l_face = create_model_class("YOLOv8l_face", __name__) YOLOv8m_face = create_model_class("YOLOv8m_face", __name__) YOLOv8n_face = create_model_class("YOLOv8n_face", __name__) diff --git a/mblt_vision/models/YOLOv6m-face.yaml b/mblt_vision/models/YOLOv6m-face.yaml deleted file mode 100644 index 5c56065..0000000 --- a/mblt_vision/models/YOLOv6m-face.yaml +++ /dev/null @@ -1,27 +0,0 @@ -DEFAULT: - file_cfg: - repo_id: mobilint/YOLOv6m-face - filename: yolov6m-face.mxq - revision: main - pre_cfg: - Reader: - style: numpy - LetterBox: - img_size: - - 640 - - 640 - SetOrder: - shape: HWC - Normalize: - style: cv - post_cfg: - task: face_detection - dataset: widerface - nl: 3 - reg_max: 16 - conf_thres: 0.001 - iou_thres: 0.7 -TURBO: - update: DEFAULT - file_cfg: - revision: TURBO diff --git a/mblt_vision/models/YOLOv6n-face.yaml b/mblt_vision/models/YOLOv6n-face.yaml deleted file mode 100644 index f1e7541..0000000 --- a/mblt_vision/models/YOLOv6n-face.yaml +++ /dev/null @@ -1,27 +0,0 @@ -DEFAULT: - file_cfg: - repo_id: mobilint/YOLOv6n-face - filename: yolov6n-face.mxq - revision: main - pre_cfg: - Reader: - style: numpy - LetterBox: - img_size: - - 640 - - 640 - SetOrder: - shape: HWC - Normalize: - style: cv - post_cfg: - task: face_detection - dataset: widerface - nl: 3 - reg_max: 16 - conf_thres: 0.001 - iou_thres: 0.7 -TURBO: - update: DEFAULT - file_cfg: - revision: TURBO diff --git a/mblt_vision/models/YOLOv8l-face.yaml b/mblt_vision/models/YOLOv8l-face.yaml index 12f7093..b8b527a 100644 --- a/mblt_vision/models/YOLOv8l-face.yaml +++ b/mblt_vision/models/YOLOv8l-face.yaml @@ -7,9 +7,11 @@ DEFAULT: Reader: style: numpy LetterBox: + # yolov8l-face.pt's own embedded train_args records imgsz: 960, unlike + # every other face-detection size/family, which train at 640. img_size: - - 640 - - 640 + - 960 + - 960 SetOrder: shape: HWC Normalize: diff --git a/mblt_vision/models/YOLOv8m-face.yaml b/mblt_vision/models/YOLOv8m-face.yaml index 135340c..0e09342 100644 --- a/mblt_vision/models/YOLOv8m-face.yaml +++ b/mblt_vision/models/YOLOv8m-face.yaml @@ -7,9 +7,11 @@ DEFAULT: Reader: style: numpy LetterBox: + # yolov8m-face.pt's own embedded train_args records imgsz: 960, unlike + # every other face-detection size/family, which train at 640. img_size: - - 640 - - 640 + - 960 + - 960 SetOrder: shape: HWC Normalize: From f44d99b94c3313865bdb62af2f43e5a0da8ef454 Mon Sep 17 00:00:00 2001 From: jinman Date: Wed, 26 Aug 2026 09:30:09 +0900 Subject: [PATCH 2/5] feat: add face detection support for YOLO models with dedicated postprocessing classes --- mblt_vision/utils/postprocess/build_post.py | 36 ++++++- mblt_vision/utils/postprocess/common.py | 98 +++++++++++++++++++ .../utils/postprocess/yolo_anchor_post.py | 6 +- .../utils/postprocess/yolo_anchorless_post.py | 9 +- .../utils/postprocess/yolo_dflfree_post.py | 7 +- .../utils/postprocess/yolo_nmsfree_post.py | 6 +- tests/test_face_detection.py | 88 ++++++++++++++--- 7 files changed, 229 insertions(+), 21 deletions(-) diff --git a/mblt_vision/utils/postprocess/build_post.py b/mblt_vision/utils/postprocess/build_post.py index 305ac44..fe0097c 100644 --- a/mblt_vision/utils/postprocess/build_post.py +++ b/mblt_vision/utils/postprocess/build_post.py @@ -9,20 +9,26 @@ from .cls_post import ClsPost from .depth_post import DepthPost from .semantic_seg_post import SemanticSegPost -from .yolo_anchor_post import YOLOAnchorDetectionPost, YOLOAnchorSegPost +from .yolo_anchor_post import ( + YOLOAnchorDetectionPost, + YOLOAnchorFaceDetectionPost, + YOLOAnchorSegPost, +) from .yolo_anchorless_post import ( YOLOAnchorlessDetectionPost, + YOLOAnchorlessFaceDetectionPost, YOLOAnchorlessOBBPost, YOLOAnchorlessPosePost, YOLOAnchorlessSegPost, ) from .yolo_dflfree_post import ( YOLODFLFreeDetectionPost, + YOLODFLFreeFaceDetectionPost, YOLODFLFreeOBBPost, YOLODFLFreePosePost, YOLODFLFreeSegPost, ) -from .yolo_nmsfree_post import YOLONMSFreeDetectionPost +from .yolo_nmsfree_post import YOLONMSFreeDetectionPost, YOLONMSFreeFaceDetectionPost def build_postprocess( @@ -51,7 +57,31 @@ def build_postprocess( return DepthPost(pre_cfg, post_cfg) if task == "semantic_segmentation": return SemanticSegPost(pre_cfg, post_cfg) - if task in {"object_detection", "face_detection"}: + if task == "face_detection": + if post_cfg.get("anchors", False): + return YOLOAnchorFaceDetectionPost( + pre_cfg, + post_cfg, + **kwargs, + ) + if post_cfg.get("dflfree", False): # nms free is only available for detection + return YOLODFLFreeFaceDetectionPost( + pre_cfg, + post_cfg, + **kwargs, + ) + if post_cfg.get("nmsfree", False): + return YOLONMSFreeFaceDetectionPost( + pre_cfg, + post_cfg, + **kwargs, + ) + return YOLOAnchorlessFaceDetectionPost( + pre_cfg, + post_cfg, + **kwargs, + ) + if task == "object_detection": if post_cfg.get("anchors", False): return YOLOAnchorDetectionPost( pre_cfg, diff --git a/mblt_vision/utils/postprocess/common.py b/mblt_vision/utils/postprocess/common.py index dc6ed5a..632c3f2 100644 --- a/mblt_vision/utils/postprocess/common.py +++ b/mblt_vision/utils/postprocess/common.py @@ -1309,6 +1309,69 @@ def nmsout2eval( return labels_list, boxes_list, scores_list +def nmsout2eval_face( + nms_outs: list[torch.Tensor] | torch.Tensor, + img1_shape: tuple[int, int], + img0_shapes: tuple[int, int] | Sequence[tuple[int, int]], + ratio_pads: RatioPad | Sequence[RatioPad | None] | None = None, +) -> tuple[list[list[str]], list[list[list[float]]], list[list[float]]]: + """Converts single-class face-detection NMS output to evaluation format. + + WiderFace has exactly one class, so this mirrors :func:`nmsout2eval` without + routing label indices through COCO's 80-class category-id table: every row + is labeled ``"face"`` and its class index must be ``0``. + + Args: + nms_outs: NMS output of shape ``(n, 6)`` per image, where ``n`` is the + number of detected faces. + img1_shape: Processed image shape ``(H, W)``. + img0_shapes: Original image shape or shapes. + ratio_pads: Optional letterbox metadata. + + Returns: + tuple: A tuple containing: + - labels (list[list[str]]): ``"face"`` for every detection. + - boxes (list[list]): The bounding boxes (xywh) for each image. + - scores (list[list]): The confidence scores for each image. + """ + if not isinstance(nms_outs, list): + nms_outs = [nms_outs] + actual_img0_shapes = normalize_image_shapes(img0_shapes, len(nms_outs)) + actual_ratio_pads = normalize_ratio_pads(ratio_pads, len(nms_outs)) + labels_list: list[list[str]] = [] + boxes_list: list[list[list[float]]] = [] + scores_list: list[list[float]] = [] + for nms_out, img0_shape, ratio_pad in zip( + nms_outs, actual_img0_shapes, actual_ratio_pads + ): + boxes = nms_out[:, :4].clone() + scores = nms_out[:, 4] + labels = nms_out[:, 5] + valid_labels = ( + torch.isfinite(labels) & (labels == labels.round()) & (labels == 0) + ) + if not bool(valid_labels.all()): + invalid_labels = labels[~valid_labels].detach().cpu().tolist() + raise ValueError( + f"Face-detection class IDs must all be 0; got {invalid_labels}." + ) + boxes = scale_boxes( + img1_shape, boxes, img0_shape, ratio_pad=ratio_pad + ) # scale boxes to original image size + boxes[:, 2:] = boxes[:, 2:] - boxes[:, :2] # xyxy to xywh with corner xy + + boxes_tolist = [ + [round(float(value), 3) for value in box] for box in boxes.tolist() + ] + scores_tolist = [round(float(score), 5) for score in scores.tolist()] + + labels_list.append(["face"] * len(boxes_tolist)) + boxes_list.append(boxes_tolist) + scores_list.append(scores_tolist) + + return labels_list, boxes_list, scores_list + + def nmsout2eval_seg( nms_outs: Any, img1_shape: tuple[int, int], @@ -1493,6 +1556,41 @@ def nmsout2eval_obb( return labels_list, polygons_list, scores_list +class YOLOFaceDetectionMixin: + """Mixin class for single-class WiderFace face-detection postprocessing. + + Face detection reuses the object-detection decode/NMS pipeline of whatever + head family a model belongs to (anchor, anchorless, DFL-free, or NMS-free); + the only thing that differs is evaluation-format label conversion, since + WiderFace has one class and no COCO category-id mapping applies. Mix this + in over the matching detection postprocessor, for example:: + + class YOLOAnchorlessFaceDetectionPost( + YOLOFaceDetectionMixin, YOLOAnchorlessDetectionPost + ): + pass + """ + + def nmsout2eval( + self, + nms_out: Any, + img1_shape: tuple[int, int], + img0_shape: tuple[int, int] | list[tuple[int, int]], + ratio_pad: RatioPad | list[RatioPad | None] | None = None, + ) -> tuple[Any, ...]: + """Converts NMS output to evaluation format for face detection. + + Args: + nms_out: NMS output (single-class face detections). + img1_shape: Resized image shape. + img0_shape: List of original image shapes. + + Returns: + Tuple: (labels_list, boxes_list, scores_list). + """ + return nmsout2eval_face(nms_out, img1_shape, img0_shape, ratio_pads=ratio_pad) + + class YOLOSegPostMixin: """Mixin class for YOLO segmentation postprocessing.""" diff --git a/mblt_vision/utils/postprocess/yolo_anchor_post.py b/mblt_vision/utils/postprocess/yolo_anchor_post.py index 4888593..1894378 100644 --- a/mblt_vision/utils/postprocess/yolo_anchor_post.py +++ b/mblt_vision/utils/postprocess/yolo_anchor_post.py @@ -9,7 +9,7 @@ import torch from .base import YOLODetectionPostBase -from .common import YOLOSegPostMixin, non_max_suppression +from .common import YOLOFaceDetectionMixin, YOLOSegPostMixin, non_max_suppression class YOLOAnchorDetectionPost(YOLODetectionPostBase): @@ -487,3 +487,7 @@ def chop(self, npu_out: torch.Tensor, idx: int = 0) -> tuple[torch.Tensor, ...]: ) masks = masks * conf.sigmoid() return xy, wh, conf, scores, masks + + +class YOLOAnchorFaceDetectionPost(YOLOFaceDetectionMixin, YOLOAnchorDetectionPost): + """Postprocessing for anchor-based WiderFace face-detection models.""" diff --git a/mblt_vision/utils/postprocess/yolo_anchorless_post.py b/mblt_vision/utils/postprocess/yolo_anchorless_post.py index 8b90d31..4282a3a 100644 --- a/mblt_vision/utils/postprocess/yolo_anchorless_post.py +++ b/mblt_vision/utils/postprocess/yolo_anchorless_post.py @@ -12,6 +12,7 @@ from ..types import ListTensorLike, TensorLike from .base import YOLODetectionPostBase from .common import ( + YOLOFaceDetectionMixin, YOLOOBBPostMixin, YOLOPosePostMixin, YOLOSegPostMixin, @@ -566,7 +567,7 @@ class YOLOAnchorlessPosePost(YOLOPosePostMixin, YOLOAnchorlessDetectionPost): def extract_final_outputs( self, x: TensorLike | ListTensorLike - ) -> tuple[list[torch.Tensor] | None, torch.Tensor | None]: + ) -> tuple[list[torch.Tensor] | torch.Tensor | None, torch.Tensor | None]: """Accept QBCompiler's decode-enabled candidate-first pose output. Decode-enabled MXQs emit ``(B, anchors, 5 + keypoints)`` containing @@ -1006,4 +1007,10 @@ def nms_multilabel( return self.nms(x) +class YOLOAnchorlessFaceDetectionPost( + YOLOFaceDetectionMixin, YOLOAnchorlessDetectionPost +): + """Postprocessing for anchorless WiderFace face-detection models.""" + + YOLOAnchorlessPost = YOLOAnchorlessDetectionPost diff --git a/mblt_vision/utils/postprocess/yolo_dflfree_post.py b/mblt_vision/utils/postprocess/yolo_dflfree_post.py index 8cc52f2..54d32e1 100644 --- a/mblt_vision/utils/postprocess/yolo_dflfree_post.py +++ b/mblt_vision/utils/postprocess/yolo_dflfree_post.py @@ -7,6 +7,7 @@ from ..types import ListTensorLike, TensorLike from .base import YOLODetectionPostBase from .common import ( + YOLOFaceDetectionMixin, YOLOOBBPostMixin, YOLOPosePostMixin, YOLOSegPostMixin, @@ -558,7 +559,7 @@ class YOLODFLFreePosePost(YOLOPosePostMixin, YOLODFLFreeDetectionPost): def extract_final_outputs( self, x: TensorLike | ListTensorLike - ) -> tuple[list[torch.Tensor] | None, torch.Tensor | None]: + ) -> tuple[list[torch.Tensor] | torch.Tensor | None, torch.Tensor | None]: """Accept YOLO26's decode-enabled score, xyxy, and keypoint outputs.""" if self.e2e and isinstance(x, (list, tuple)) and len(x) == 4: tensors = [ @@ -1017,4 +1018,8 @@ def nms( return output +class YOLODFLFreeFaceDetectionPost(YOLOFaceDetectionMixin, YOLODFLFreeDetectionPost): + """Postprocessing for DFL-free WiderFace face-detection models.""" + + YOLODFLFreePost = YOLODFLFreeDetectionPost diff --git a/mblt_vision/utils/postprocess/yolo_nmsfree_post.py b/mblt_vision/utils/postprocess/yolo_nmsfree_post.py index f77ed9c..2392229 100644 --- a/mblt_vision/utils/postprocess/yolo_nmsfree_post.py +++ b/mblt_vision/utils/postprocess/yolo_nmsfree_post.py @@ -10,7 +10,7 @@ import numpy as np import torch -from .common import dist2bbox, dual_topk +from .common import YOLOFaceDetectionMixin, dist2bbox, dual_topk from .yolo_anchorless_post import YOLOAnchorlessDetectionPost, _AnchorlessNMSInput @@ -270,4 +270,8 @@ class per candidate. return [xi[xi[:, 4] > 0] for xi in x] +class YOLONMSFreeFaceDetectionPost(YOLOFaceDetectionMixin, YOLONMSFreeDetectionPost): + """Postprocessing for NMS-free WiderFace face-detection models (for example, YOLOv10-face).""" + + YOLONMSFreePost = YOLONMSFreeDetectionPost diff --git a/tests/test_face_detection.py b/tests/test_face_detection.py index 58a8091..5f20aac 100644 --- a/tests/test_face_detection.py +++ b/tests/test_face_detection.py @@ -8,17 +8,31 @@ import numpy as np import pytest import torch + +from mblt_vision import YOLO11m_face, list_models +from mblt_vision.face_detection import YOLO11m_face as FaceDetectionYOLO11mFace from mblt_vision.utils.postprocess import build_postprocess from mblt_vision.utils.postprocess.base import YOLODetectionPostBase -from mblt_vision.utils.postprocess.yolo_anchor_post import YOLOAnchorDetectionPost +from mblt_vision.utils.postprocess.common import ( + YOLOFaceDetectionMixin, + nmsout2eval_face, +) +from mblt_vision.utils.postprocess.yolo_anchor_post import ( + YOLOAnchorDetectionPost, + YOLOAnchorFaceDetectionPost, +) from mblt_vision.utils.postprocess.yolo_anchorless_post import ( YOLOAnchorlessDetectionPost, + YOLOAnchorlessFaceDetectionPost, +) +from mblt_vision.utils.postprocess.yolo_dflfree_post import ( + YOLODFLFreeDetectionPost, + YOLODFLFreeFaceDetectionPost, +) +from mblt_vision.utils.postprocess.yolo_nmsfree_post import ( + YOLONMSFreeDetectionPost, + YOLONMSFreeFaceDetectionPost, ) -from mblt_vision.utils.postprocess.yolo_dflfree_post import YOLODFLFreeDetectionPost -from mblt_vision.utils.postprocess.yolo_nmsfree_post import YOLONMSFreeDetectionPost - -from mblt_vision import YOLO11m_face, list_models -from mblt_vision.face_detection import YOLO11m_face as FaceDetectionYOLO11mFace from mblt_vision.utils.results import Results @@ -41,22 +55,47 @@ def _post_cfg(**overrides: Any) -> dict[str, Any]: @pytest.mark.parametrize( - ("post_cfg", "expected_type"), + ("post_cfg", "expected_type", "expected_family"), [ - ({"nl": 3, "reg_max": 16}, YOLOAnchorlessDetectionPost), - ({"nl": 3, "dflfree": True}, YOLODFLFreeDetectionPost), - ({"nl": 3, "nmsfree": True}, YOLONMSFreeDetectionPost), - ({"anchors": [[10, 13, 16, 30, 33, 23]]}, YOLOAnchorDetectionPost), + ( + {"nl": 3, "reg_max": 16}, + YOLOAnchorlessFaceDetectionPost, + YOLOAnchorlessDetectionPost, + ), + ( + {"nl": 3, "dflfree": True}, + YOLODFLFreeFaceDetectionPost, + YOLODFLFreeDetectionPost, + ), + ( + {"nl": 3, "nmsfree": True}, + YOLONMSFreeFaceDetectionPost, + YOLONMSFreeDetectionPost, + ), + ( + {"anchors": [[10, 13, 16, 30, 33, 23]]}, + YOLOAnchorFaceDetectionPost, + YOLOAnchorDetectionPost, + ), ], ) def test_face_detection_routes_postprocessors( - post_cfg: dict[str, Any], expected_type: type[YOLODetectionPostBase] + post_cfg: dict[str, Any], + expected_type: type[YOLODetectionPostBase], + expected_family: type[YOLODetectionPostBase], ) -> None: - """Route every supported face head family to its YOLO postprocessor.""" + """Route every supported face head family to its dedicated face postprocessor. + + Each dedicated class must inherit from both the matching detection-family + base (so it decodes/NMS-suppresses identically) and ``YOLOFaceDetectionMixin`` + (so it evaluates with a single ``"face"`` label instead of COCO categories). + """ postprocessor = build_postprocess(_pre_cfg(), _post_cfg(**post_cfg)) - assert isinstance(postprocessor, expected_type) + assert type(postprocessor) is expected_type + assert isinstance(postprocessor, expected_family) + assert isinstance(postprocessor, YOLOFaceDetectionMixin) assert cast(YOLODetectionPostBase, postprocessor).nc == 1 @@ -121,3 +160,24 @@ def test_face_detection_non_e2e_converted_and_raw_outputs() -> None: assert converted_result.shape == (1, 5, 3) assert isinstance(raw_result, torch.Tensor) assert raw_result.shape == (1, 5, 8400) + + +def test_nmsout2eval_face_labels_every_row_face() -> None: + """Convert single-class face rows without routing through COCO category IDs.""" + + detections = torch.tensor([[10.0, 10.0, 20.0, 20.0, 0.9, 0.0]]) + + labels, boxes, scores = nmsout2eval_face(detections, (100, 100), (100, 100)) + + assert labels == [["face"]] + assert scores == [[0.9]] + assert boxes[0][0] == pytest.approx([10.0, 10.0, 10.0, 10.0]) + + +def test_nmsout2eval_face_rejects_nonzero_class_ids() -> None: + """Reject any class id other than 0 instead of silently mislabeling it.""" + + detections = torch.tensor([[10.0, 10.0, 20.0, 20.0, 0.9, 1.0]]) + + with pytest.raises(ValueError, match="must all be 0"): + nmsout2eval_face(detections, (100, 100), (100, 100)) From c5ef739a9bf4a79fde63280173cfb7048c4a4e03 Mon Sep 17 00:00:00 2001 From: jinman Date: Tue, 1 Sep 2026 15:46:05 +0900 Subject: [PATCH 3/5] docs: sync face-detection contracts and input geometry with mblt-model-ops Document the single-class face_detection postprocessing contract and the face-model input geometry taken from mblt-model-ops pipeline.yaml, including the YOLOv8m-face/YOLOv8l-face 960x960 exception, across AGENTS.md, CLAUDE.md, both SKILL.md copies, and mblt_vision/README.md. Co-Authored-By: Claude Opus 5 (1M context) --- .agents/skills/mblt-vision/SKILL.md | 11 +++++++++++ .claude/skills/mblt-vision/SKILL.md | 11 +++++++++++ AGENTS.md | 18 ++++++++++++++++++ CLAUDE.md | 5 ++++- mblt_vision/README.md | 29 +++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/.agents/skills/mblt-vision/SKILL.md b/.agents/skills/mblt-vision/SKILL.md index 945aaf5..44eeebe 100644 --- a/.agents/skills/mblt-vision/SKILL.md +++ b/.agents/skills/mblt-vision/SKILL.md @@ -87,6 +87,17 @@ description: >- the Model Zoo reference. - Rank WiderFace evaluation by Hard-set AP. Expose Medium-set then Easy-set AP as secondary metrics, and do not compute mean AP across difficulty splits. +- Treat face_detection as a single-class WiderFace task. Each YOLO head family gets a thin + YOLOFaceDetectionMixin subclass over its object-detection postprocessor, so only + evaluation-format conversion differs: nmsout2eval_face labels every row "face" and rejects a + class index other than 0 rather than using the COCO category-id table. build_postprocess + dispatches face_detection before object_detection on the same anchors/dflfree/nmsfree keys. + The anchor branch is groundwork for YOLOv5-face and YOLOv7-face, covered by unit tests only. +- Source face-detection pre_cfg/post_cfg from ../mblt-model-ops/models//pipeline.yaml. + Every shipped face model is 640x640 except YOLOv8m-face and YOLOv8l-face at 960x960, whose + checkpoints record imgsz: 960 in their own train_args. Changing an input size is a durable + model-behavior change: update AGENTS.md, both SKILL.md copies, and mblt_vision/README.md + together. - Rank NYU Depth evaluation by delta1. Expose abs_rel then RMSE (m) as secondary metrics, with median-aligned metrics averaged per image. diff --git a/.claude/skills/mblt-vision/SKILL.md b/.claude/skills/mblt-vision/SKILL.md index 945aaf5..44eeebe 100644 --- a/.claude/skills/mblt-vision/SKILL.md +++ b/.claude/skills/mblt-vision/SKILL.md @@ -87,6 +87,17 @@ description: >- the Model Zoo reference. - Rank WiderFace evaluation by Hard-set AP. Expose Medium-set then Easy-set AP as secondary metrics, and do not compute mean AP across difficulty splits. +- Treat face_detection as a single-class WiderFace task. Each YOLO head family gets a thin + YOLOFaceDetectionMixin subclass over its object-detection postprocessor, so only + evaluation-format conversion differs: nmsout2eval_face labels every row "face" and rejects a + class index other than 0 rather than using the COCO category-id table. build_postprocess + dispatches face_detection before object_detection on the same anchors/dflfree/nmsfree keys. + The anchor branch is groundwork for YOLOv5-face and YOLOv7-face, covered by unit tests only. +- Source face-detection pre_cfg/post_cfg from ../mblt-model-ops/models//pipeline.yaml. + Every shipped face model is 640x640 except YOLOv8m-face and YOLOv8l-face at 960x960, whose + checkpoints record imgsz: 960 in their own train_args. Changing an input size is a durable + model-behavior change: update AGENTS.md, both SKILL.md copies, and mblt_vision/README.md + together. - Rank NYU Depth evaluation by delta1. Expose abs_rel then RMSE (m) as secondary metrics, with median-aligned metrics averaged per image. diff --git a/AGENTS.md b/AGENTS.md index dc75a77..37c6fba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,6 +75,24 @@ The current ownership boundary is deliberate: must remain lazy and report the appropriate package extra when unavailable. - For WiderFace evaluation, rank results by Hard-set AP and retain Medium-set then Easy-set AP as secondary metrics. Do not compute a mean across splits. +- `face_detection` is a single-class WiderFace task, not an 80-class COCO one. Each YOLO head + family reuses its own object-detection decode and NMS through a thin + `YOLOFaceDetectionMixin` subclass (`YOLOAnchorFaceDetectionPost`, + `YOLOAnchorlessFaceDetectionPost`, `YOLODFLFreeFaceDetectionPost`, + `YOLONMSFreeFaceDetectionPost`); only evaluation-format conversion differs, because + `nmsout2eval_face` labels every row `"face"` and rejects any class index other than `0` + instead of routing indices through the COCO category-id table. `build_postprocess` therefore + dispatches `face_detection` on its own branch, ahead of `object_detection`, using the same + `anchors` / `dflfree` / `nmsfree` `post_cfg` keys. The anchor-based branch is groundwork for + the YOLOv5-face and YOLOv7-face families and is exercised by unit tests, not yet by a shipped + model YAML. +- Take face-detection `pre_cfg`/`post_cfg` defaults from + `../mblt-model-ops/models//pipeline.yaml`, which is the source of truth for the + compiled artifacts. Face-detection input geometry is `640x640` for every shipped model except + `YOLOv8m-face` and `YOLOv8l-face`, which are `960x960` because those checkpoints' own embedded + `train_args` record `imgsz: 960`. Do not normalize the exception away; a size change here is a + durable model-behavior change requiring the guide, both skill copies, and + `mblt_vision/README.md` to be updated in the same commit. - `eval_sav` requires already-binarized candidate masks, enumerated per dtype (bool, integer `{0, 1}`/`{0, 255}`, float `{0.0, 1.0}`). A weaker "single positive value" rule still admits a probability map such as `{0.0, 0.5}`, or a uniform `0.5` candidate that `astype(bool)` turns diff --git a/CLAUDE.md b/CLAUDE.md index 680ff48..90ee372 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,10 @@ `AGENTS.md` is the canonical guide for this repository. Follow it for the Python-first implementation, PyPI-wheel, lifecycle, and mblt-model-zoo Vision compatibility requirements. This includes the WiderFace Hard-primary and Medium/Easy-secondary metric contract, -plus NYU Depth delta1-primary with abs_rel/RMSE secondary metrics. +the single-class `face_detection` postprocessing contract and its +`mblt-model-ops`-sourced input geometry (640x640, except `YOLOv8m-face` and +`YOLOv8l-face` at 960x960), plus NYU Depth delta1-primary with abs_rel/RMSE +secondary metrics. For focused model, preprocessing, postprocessing, and model-registry work, also read .claude/skills/mblt-vision/SKILL.md. diff --git a/mblt_vision/README.md b/mblt_vision/README.md index c4f923e..cdb79e7 100644 --- a/mblt_vision/README.md +++ b/mblt_vision/README.md @@ -226,6 +226,35 @@ WiderFace validation uses Hard-set AP as the primary metric. Medium-set AP and Easy-set AP are secondary metrics, in that order. Mean AP across the difficulty splits is not computed. +Face detection is a single-class task: every detection is labeled `face`, and no +COCO category mapping is applied. + +| Model | Input Size
(H,W,C) | Source | Note | +| --- | --- | --- | --- | +| YOLOv8n-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLOv8m-face | (960,960,3) | [Link](https://github.com/akanametov/yolo-face) | Trained at 960; see the note below. | +| YOLOv8l-face | (960,960,3) | [Link](https://github.com/akanametov/yolo-face) | Trained at 960; see the note below. | +| YOLO11n-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLO11s-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLO11m-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLO11l-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLO12n-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLO12s-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLO12m-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLO12l-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLOv10n-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLOv10s-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLOv10m-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLOv10l-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | + +Input geometry follows each checkpoint's own training resolution. Every face +model is trained and served at 640x640 except `YOLOv8m-face` and `YOLOv8l-face`, +whose released weights record `imgsz: 960` in their embedded `train_args`; those +two use 960x960 and must not be normalized down to 640. + +WiderFace accuracy columns are omitted until the full validation split has been +measured for each artifact. + ### Depth Estimation NYU Depth V2 validation uses median-aligned delta1 as the primary metric. From ace5f4cf143a6211771ef8a053677175c8910993 Mon Sep 17 00:00:00 2001 From: jinman Date: Tue, 1 Sep 2026 16:06:56 +0900 Subject: [PATCH 4/5] feat: add the anchor-based YOLOv5-face and YOLOv7-face model configs model-ops carries 24 face models; only 15 were listed here. Add the nine missing anchor-based configs from ../mblt-model-ops/models: YOLOv5{n,n-0.5,s,m}-face (deepcam-cn) and YOLOv7{,s,-tiny,-lite-s,-lite-t}-face (derronqi), taking pre_cfg/post_cfg verbatim from each model's pipeline.yaml. These are the first shipped YAMLs that reach YOLOAnchorFaceDetectionPost. Their published ONNX exports emit three raw (batch, 3, H, W, 6) heads with the upstream landmark pairs stripped, so they decode through the shared anchor path with nc = 1 and iou_thres 0.5. Verified end to end against both the model-ops model.onnx files and the Hub-resolved exports: all nine decode, agree with the shipped YOLOv8n-face reference to within ~2px on the same face, restore into original image coordinates in bounds, and label every row "face". Co-Authored-By: Claude Opus 5 (1M context) --- .agents/skills/mblt-vision/SKILL.md | 3 +- .claude/skills/mblt-vision/SKILL.md | 3 +- AGENTS.md | 10 +++-- mblt_vision/README.md | 16 ++++++++ mblt_vision/face_detection/__init__.py | 18 +++++++++ mblt_vision/models/YOLOv5m-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv5n-0.5-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv5n-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv5s-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv7-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv7-lite-s-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv7-lite-t-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv7-tiny-face.yaml | 44 ++++++++++++++++++++ mblt_vision/models/YOLOv7s-face.yaml | 44 ++++++++++++++++++++ tests/test_face_detection.py | 47 ++++++++++++++++++++++ 15 files changed, 487 insertions(+), 6 deletions(-) create mode 100644 mblt_vision/models/YOLOv5m-face.yaml create mode 100644 mblt_vision/models/YOLOv5n-0.5-face.yaml create mode 100644 mblt_vision/models/YOLOv5n-face.yaml create mode 100644 mblt_vision/models/YOLOv5s-face.yaml create mode 100644 mblt_vision/models/YOLOv7-face.yaml create mode 100644 mblt_vision/models/YOLOv7-lite-s-face.yaml create mode 100644 mblt_vision/models/YOLOv7-lite-t-face.yaml create mode 100644 mblt_vision/models/YOLOv7-tiny-face.yaml create mode 100644 mblt_vision/models/YOLOv7s-face.yaml diff --git a/.agents/skills/mblt-vision/SKILL.md b/.agents/skills/mblt-vision/SKILL.md index 44eeebe..c92d023 100644 --- a/.agents/skills/mblt-vision/SKILL.md +++ b/.agents/skills/mblt-vision/SKILL.md @@ -92,7 +92,8 @@ description: >- evaluation-format conversion differs: nmsout2eval_face labels every row "face" and rejects a class index other than 0 rather than using the COCO category-id table. build_postprocess dispatches face_detection before object_detection on the same anchors/dflfree/nmsfree keys. - The anchor branch is groundwork for YOLOv5-face and YOLOv7-face, covered by unit tests only. + The anchor branch serves the YOLOv5*-face and YOLOv7*-face families, whose ONNX exports emit + three raw (batch, 3, H, W, 6) heads with landmarks stripped and use iou_thres: 0.5. - Source face-detection pre_cfg/post_cfg from ../mblt-model-ops/models//pipeline.yaml. Every shipped face model is 640x640 except YOLOv8m-face and YOLOv8l-face at 960x960, whose checkpoints record imgsz: 960 in their own train_args. Changing an input size is a durable diff --git a/.claude/skills/mblt-vision/SKILL.md b/.claude/skills/mblt-vision/SKILL.md index 44eeebe..c92d023 100644 --- a/.claude/skills/mblt-vision/SKILL.md +++ b/.claude/skills/mblt-vision/SKILL.md @@ -92,7 +92,8 @@ description: >- evaluation-format conversion differs: nmsout2eval_face labels every row "face" and rejects a class index other than 0 rather than using the COCO category-id table. build_postprocess dispatches face_detection before object_detection on the same anchors/dflfree/nmsfree keys. - The anchor branch is groundwork for YOLOv5-face and YOLOv7-face, covered by unit tests only. + The anchor branch serves the YOLOv5*-face and YOLOv7*-face families, whose ONNX exports emit + three raw (batch, 3, H, W, 6) heads with landmarks stripped and use iou_thres: 0.5. - Source face-detection pre_cfg/post_cfg from ../mblt-model-ops/models//pipeline.yaml. Every shipped face model is 640x640 except YOLOv8m-face and YOLOv8l-face at 960x960, whose checkpoints record imgsz: 960 in their own train_args. Changing an input size is a durable diff --git a/AGENTS.md b/AGENTS.md index 37c6fba..3cbace8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,14 +83,16 @@ The current ownership boundary is deliberate: `nmsout2eval_face` labels every row `"face"` and rejects any class index other than `0` instead of routing indices through the COCO category-id table. `build_postprocess` therefore dispatches `face_detection` on its own branch, ahead of `object_detection`, using the same - `anchors` / `dflfree` / `nmsfree` `post_cfg` keys. The anchor-based branch is groundwork for - the YOLOv5-face and YOLOv7-face families and is exercised by unit tests, not yet by a shipped - model YAML. + `anchors` / `dflfree` / `nmsfree` `post_cfg` keys. The anchor-based branch serves the + `YOLOv5*-face` (deepcam-cn) and `YOLOv7*-face` (derronqi) families, whose published ONNX + exports emit three raw `(batch, 3, H, W, 6)` heads with the original repositories' five + landmark pairs stripped, so they decode through the shared anchor path with `nc = 1`. - Take face-detection `pre_cfg`/`post_cfg` defaults from `../mblt-model-ops/models//pipeline.yaml`, which is the source of truth for the compiled artifacts. Face-detection input geometry is `640x640` for every shipped model except `YOLOv8m-face` and `YOLOv8l-face`, which are `960x960` because those checkpoints' own embedded - `train_args` record `imgsz: 960`. Do not normalize the exception away; a size change here is a + `train_args` record `imgsz: 960`. The anchor-based families additionally use `iou_thres: 0.5` + where every other face model uses `0.7`. Do not normalize the exception away; a size change here is a durable model-behavior change requiring the guide, both skill copies, and `mblt_vision/README.md` to be updated in the same commit. - `eval_sav` requires already-binarized candidate masks, enumerated per dtype (bool, integer diff --git a/mblt_vision/README.md b/mblt_vision/README.md index cdb79e7..c45c554 100644 --- a/mblt_vision/README.md +++ b/mblt_vision/README.md @@ -246,12 +246,28 @@ COCO category mapping is applied. | YOLOv10s-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | | YOLOv10m-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | | YOLOv10l-face | (640,640,3) | [Link](https://github.com/akanametov/yolo-face) | | +| YOLOv5n-face | (640,640,3) | [Link](https://github.com/deepcam-cn/yolov5-face) | Anchor-based. | +| YOLOv5n-0.5-face | (640,640,3) | [Link](https://github.com/deepcam-cn/yolov5-face) | Anchor-based. | +| YOLOv5s-face | (640,640,3) | [Link](https://github.com/deepcam-cn/yolov5-face) | Anchor-based. | +| YOLOv5m-face | (640,640,3) | [Link](https://github.com/deepcam-cn/yolov5-face) | Anchor-based. | +| YOLOv7-lite-t-face | (640,640,3) | [Link](https://github.com/derronqi/yolov7-face) | Anchor-based. | +| YOLOv7-lite-s-face | (640,640,3) | [Link](https://github.com/derronqi/yolov7-face) | Anchor-based. | +| YOLOv7-tiny-face | (640,640,3) | [Link](https://github.com/derronqi/yolov7-face) | Anchor-based. | +| YOLOv7s-face | (640,640,3) | [Link](https://github.com/derronqi/yolov7-face) | Anchor-based. | +| YOLOv7-face | (640,640,3) | [Link](https://github.com/derronqi/yolov7-face) | Anchor-based. | Input geometry follows each checkpoint's own training resolution. Every face model is trained and served at 640x640 except `YOLOv8m-face` and `YOLOv8l-face`, whose released weights record `imgsz: 960` in their embedded `train_args`; those two use 960x960 and must not be normalized down to 640. +The `YOLOv5*-face` and `YOLOv7*-face` families are anchor-based and carry an +`anchors` list in `post_cfg` with `iou_thres: 0.5`; every other face model is +anchorless or NMS-free with `iou_thres: 0.7`. Their published ONNX exports emit +three raw detection heads of shape `(batch, 3, H, W, 6)` — the five landmark +pairs of the original repositories are stripped at export — so they decode +through the shared anchor detection path with `nc = 1`. + WiderFace accuracy columns are omitted until the full validation split has been measured for each artifact. diff --git a/mblt_vision/face_detection/__init__.py b/mblt_vision/face_detection/__init__.py index 10bfc4b..483e220 100644 --- a/mblt_vision/face_detection/__init__.py +++ b/mblt_vision/face_detection/__init__.py @@ -17,6 +17,15 @@ "YOLOv10m_face", "YOLOv10n_face", "YOLOv10s_face", + "YOLOv5m_face", + "YOLOv5n_0_5_face", + "YOLOv5n_face", + "YOLOv5s_face", + "YOLOv7_face", + "YOLOv7_lite_s_face", + "YOLOv7_lite_t_face", + "YOLOv7_tiny_face", + "YOLOv7s_face", "YOLOv8l_face", "YOLOv8m_face", "YOLOv8n_face", @@ -34,6 +43,15 @@ YOLOv10m_face = create_model_class("YOLOv10m_face", __name__) YOLOv10n_face = create_model_class("YOLOv10n_face", __name__) YOLOv10s_face = create_model_class("YOLOv10s_face", __name__) +YOLOv5m_face = create_model_class("YOLOv5m_face", __name__) +YOLOv5n_0_5_face = create_model_class("YOLOv5n_0_5_face", __name__) +YOLOv5n_face = create_model_class("YOLOv5n_face", __name__) +YOLOv5s_face = create_model_class("YOLOv5s_face", __name__) +YOLOv7_face = create_model_class("YOLOv7_face", __name__) +YOLOv7_lite_s_face = create_model_class("YOLOv7_lite_s_face", __name__) +YOLOv7_lite_t_face = create_model_class("YOLOv7_lite_t_face", __name__) +YOLOv7_tiny_face = create_model_class("YOLOv7_tiny_face", __name__) +YOLOv7s_face = create_model_class("YOLOv7s_face", __name__) YOLOv8l_face = create_model_class("YOLOv8l_face", __name__) YOLOv8m_face = create_model_class("YOLOv8m_face", __name__) YOLOv8n_face = create_model_class("YOLOv8n_face", __name__) diff --git a/mblt_vision/models/YOLOv5m-face.yaml b/mblt_vision/models/YOLOv5m-face.yaml new file mode 100644 index 0000000..d76acb8 --- /dev/null +++ b/mblt_vision/models/YOLOv5m-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv5m-face + filename: yolov5m-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 8 + - 10 + - 13 + - 16 + - - 23 + - 29 + - 43 + - 55 + - 73 + - 105 + - - 146 + - 217 + - 231 + - 300 + - 335 + - 433 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv5n-0.5-face.yaml b/mblt_vision/models/YOLOv5n-0.5-face.yaml new file mode 100644 index 0000000..b58455e --- /dev/null +++ b/mblt_vision/models/YOLOv5n-0.5-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv5n-0.5-face + filename: yolov5n-0.5-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 8 + - 10 + - 13 + - 16 + - - 23 + - 29 + - 43 + - 55 + - 73 + - 105 + - - 146 + - 217 + - 231 + - 300 + - 335 + - 433 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv5n-face.yaml b/mblt_vision/models/YOLOv5n-face.yaml new file mode 100644 index 0000000..d79bcbd --- /dev/null +++ b/mblt_vision/models/YOLOv5n-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv5n-face + filename: yolov5n-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 8 + - 10 + - 13 + - 16 + - - 23 + - 29 + - 43 + - 55 + - 73 + - 105 + - - 146 + - 217 + - 231 + - 300 + - 335 + - 433 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv5s-face.yaml b/mblt_vision/models/YOLOv5s-face.yaml new file mode 100644 index 0000000..476c35d --- /dev/null +++ b/mblt_vision/models/YOLOv5s-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv5s-face + filename: yolov5s-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 8 + - 10 + - 13 + - 16 + - - 23 + - 29 + - 43 + - 55 + - 73 + - 105 + - - 146 + - 217 + - 231 + - 300 + - 335 + - 433 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv7-face.yaml b/mblt_vision/models/YOLOv7-face.yaml new file mode 100644 index 0000000..cc81056 --- /dev/null +++ b/mblt_vision/models/YOLOv7-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv7-face + filename: yolov7-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 6 + - 8 + - 10 + - 12 + - - 15 + - 19 + - 23 + - 30 + - 39 + - 52 + - - 72 + - 97 + - 123 + - 164 + - 209 + - 297 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv7-lite-s-face.yaml b/mblt_vision/models/YOLOv7-lite-s-face.yaml new file mode 100644 index 0000000..f8fbbc0 --- /dev/null +++ b/mblt_vision/models/YOLOv7-lite-s-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv7-lite-s-face + filename: yolov7-lite-s-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 6 + - 8 + - 10 + - 12 + - - 15 + - 19 + - 23 + - 30 + - 39 + - 52 + - - 72 + - 97 + - 123 + - 164 + - 209 + - 297 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv7-lite-t-face.yaml b/mblt_vision/models/YOLOv7-lite-t-face.yaml new file mode 100644 index 0000000..c115b61 --- /dev/null +++ b/mblt_vision/models/YOLOv7-lite-t-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv7-lite-t-face + filename: yolov7-lite-t-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 6 + - 8 + - 10 + - 12 + - - 15 + - 19 + - 23 + - 30 + - 39 + - 52 + - - 72 + - 97 + - 123 + - 164 + - 209 + - 297 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv7-tiny-face.yaml b/mblt_vision/models/YOLOv7-tiny-face.yaml new file mode 100644 index 0000000..d8127ce --- /dev/null +++ b/mblt_vision/models/YOLOv7-tiny-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv7-tiny-face + filename: yolov7-tiny-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 6 + - 8 + - 10 + - 12 + - - 15 + - 19 + - 23 + - 30 + - 39 + - 52 + - - 72 + - 97 + - 123 + - 164 + - 209 + - 297 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/mblt_vision/models/YOLOv7s-face.yaml b/mblt_vision/models/YOLOv7s-face.yaml new file mode 100644 index 0000000..25c0299 --- /dev/null +++ b/mblt_vision/models/YOLOv7s-face.yaml @@ -0,0 +1,44 @@ +DEFAULT: + file_cfg: + repo_id: mobilint/YOLOv7s-face + filename: yolov7s-face.mxq + revision: main + pre_cfg: + Reader: + style: numpy + LetterBox: + img_size: + - 640 + - 640 + SetOrder: + shape: HWC + Normalize: + style: cv + post_cfg: + task: face_detection + dataset: widerface + anchors: + - - 4 + - 5 + - 6 + - 8 + - 10 + - 12 + - - 15 + - 19 + - 23 + - 30 + - 39 + - 52 + - - 72 + - 97 + - 123 + - 164 + - 209 + - 297 + conf_thres: 0.001 + iou_thres: 0.5 +TURBO: + update: DEFAULT + file_cfg: + revision: TURBO diff --git a/tests/test_face_detection.py b/tests/test_face_detection.py index 5f20aac..1864980 100644 --- a/tests/test_face_detection.py +++ b/tests/test_face_detection.py @@ -2,13 +2,16 @@ from __future__ import annotations +from pathlib import Path from typing import Any, cast import cv2 import numpy as np import pytest import torch +import yaml +import mblt_vision from mblt_vision import YOLO11m_face, list_models from mblt_vision.face_detection import YOLO11m_face as FaceDetectionYOLO11mFace from mblt_vision.utils.postprocess import build_postprocess @@ -35,6 +38,8 @@ ) from mblt_vision.utils.results import Results +MODEL_CONFIG_DIR = Path(mblt_vision.__file__).parent / "models" + def _pre_cfg() -> dict[str, Any]: """Return a representative face preprocessing configuration.""" @@ -181,3 +186,45 @@ def test_nmsout2eval_face_rejects_nonzero_class_ids() -> None: with pytest.raises(ValueError, match="must all be 0"): nmsout2eval_face(detections, (100, 100), (100, 100)) + + +ANCHOR_FACE_MODELS = ( + "YOLOv5n-face", + "YOLOv5n-0.5-face", + "YOLOv5s-face", + "YOLOv5m-face", + "YOLOv7-face", + "YOLOv7s-face", + "YOLOv7-tiny-face", + "YOLOv7-lite-s-face", + "YOLOv7-lite-t-face", +) + + +@pytest.mark.parametrize("model_name", ANCHOR_FACE_MODELS) +def test_anchor_face_yaml_builds_anchor_face_postprocessor(model_name: str) -> None: + """Route every shipped anchor-based face YAML to the anchor face postprocessor. + + The ``YOLOv5*-face`` and ``YOLOv7*-face`` families are the only shipped face + models that carry an ``anchors`` list, so they are what makes + ``YOLOAnchorFaceDetectionPost`` reachable from the registry rather than from + a synthetic ``post_cfg``. + """ + + config = yaml.safe_load( + (MODEL_CONFIG_DIR / f"{model_name}.yaml").read_text(encoding="utf-8") + )["DEFAULT"] + post_cfg = config["post_cfg"] + + postprocessor = build_postprocess(config["pre_cfg"], post_cfg) + + assert type(postprocessor) is YOLOAnchorFaceDetectionPost + assert config["pre_cfg"]["LetterBox"]["img_size"] == [640, 640] + assert post_cfg["dataset"] == "widerface" + assert post_cfg["iou_thres"] == 0.5 + assert len(post_cfg["anchors"]) == 3 + assert all(len(level) == 6 for level in post_cfg["anchors"]) + detection_post = cast(YOLODetectionPostBase, postprocessor) + assert detection_post.nc == 1 + assert detection_post.na == 3 + assert detection_post.nl == 3 From 7a86e09adfbf430f3ce999fee511362e7231376d Mon Sep 17 00:00:00 2001 From: jinman Date: Tue, 1 Sep 2026 16:11:53 +0900 Subject: [PATCH 5/5] feat: update .gitignore to include MXQ, MBLT, and ONNX file types --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 004b95e..0e981b3 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,8 @@ Thumbs.db *.tgz *.egg -runs/ \ No newline at end of file +runs/ + +*.mxq +*.mblt +*.onnx \ No newline at end of file