Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions mblt_vision/face_detection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
"YOLOv10m_face",
"YOLOv10n_face",
"YOLOv10s_face",
"YOLOv6m_face",
"YOLOv6n_face",
"YOLOv8l_face",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve compatibility exports for the removed YOLOv6 models

Existing code importing YOLOv6m_face or YOLOv6n_face from either mblt_vision.face_detection or the lazy top-level namespace now fails at import time because these entries are removed from __all__. Even if their YAML artifacts must be withdrawn, retain deprecated compatibility classes—the existing create_model_class missing-YAML path can provide an actionable construction error—rather than silently deleting public names.

AGENTS.md reference: AGENTS.md:L41-L43

Useful? React with 👍 / 👎.

"YOLOv8m_face",
"YOLOv8n_face",
Expand All @@ -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__)
27 changes: 0 additions & 27 deletions mblt_vision/models/YOLOv6m-face.yaml

This file was deleted.

27 changes: 0 additions & 27 deletions mblt_vision/models/YOLOv6n-face.yaml

This file was deleted.

6 changes: 4 additions & 2 deletions mblt_vision/models/YOLOv8l-face.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions mblt_vision/models/YOLOv8m-face.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +13 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Synchronize the new face-model input defaults with maintained guides

Changing the YOLOv8m/YOLOv8l preprocessing default from 640 to 960 is a durable model-behavior change affecting normal inference and compilation, but this commit updates none of the required agent guide, canonical skill, Claude guidance, or relevant README. Update those maintained surfaces in the same change so package documentation and future compatibility work do not continue assuming the old geometry.

AGENTS.md reference: AGENTS.md:L160-L163

Useful? React with 👍 / 👎.

SetOrder:
shape: HWC
Normalize:
Expand Down
36 changes: 33 additions & 3 deletions mblt_vision/utils/postprocess/build_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
98 changes: 98 additions & 0 deletions mblt_vision/utils/postprocess/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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."""

Expand Down
6 changes: 5 additions & 1 deletion mblt_vision/utils/postprocess/yolo_anchor_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."""
9 changes: 8 additions & 1 deletion mblt_vision/utils/postprocess/yolo_anchorless_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ..types import ListTensorLike, TensorLike
from .base import YOLODetectionPostBase
from .common import (
YOLOFaceDetectionMixin,
YOLOOBBPostMixin,
YOLOPosePostMixin,
YOLOSegPostMixin,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1006,4 +1007,10 @@ def nms_multilabel(
return self.nms(x)


class YOLOAnchorlessFaceDetectionPost(
YOLOFaceDetectionMixin, YOLOAnchorlessDetectionPost
):
"""Postprocessing for anchorless WiderFace face-detection models."""


YOLOAnchorlessPost = YOLOAnchorlessDetectionPost
7 changes: 6 additions & 1 deletion mblt_vision/utils/postprocess/yolo_dflfree_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..types import ListTensorLike, TensorLike
from .base import YOLODetectionPostBase
from .common import (
YOLOFaceDetectionMixin,
YOLOOBBPostMixin,
YOLOPosePostMixin,
YOLOSegPostMixin,
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -1017,4 +1018,8 @@ def nms(
return output


class YOLODFLFreeFaceDetectionPost(YOLOFaceDetectionMixin, YOLODFLFreeDetectionPost):
"""Postprocessing for DFL-free WiderFace face-detection models."""


YOLODFLFreePost = YOLODFLFreeDetectionPost
6 changes: 5 additions & 1 deletion mblt_vision/utils/postprocess/yolo_nmsfree_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Loading