{"record":{"id":"9ad8dee71dad4357","repo":"roboflow/supervision","slug":"all-keypoints-must-have-the-same-number-of-keypoin","errorCode":null,"errorMessage":"All KeyPoints must have the same number of keypoints per skeleton to be merged; got counts {sorted(keypoint_counts)}.","messagePattern":"All KeyPoints must have the same number of keypoints per skeleton to be merged; got counts (.+?)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/supervision/key_points/core.py","lineNumber":1261,"sourceCode":"            key_points for key_points in key_points_list if not key_points.is_empty()\n        ]\n\n        if len(key_points_list) == 0:\n            return cls.empty()\n\n        for key_points in key_points_list:\n            _validate_keypoints_fields(\n                xy=key_points.xy,\n                class_id=key_points.class_id,\n                confidence=key_points.keypoint_confidence,\n                detection_confidence=key_points.detection_confidence,\n                visible=key_points.visible,\n                data=key_points.data,\n            )\n\n        keypoint_counts = {key_points.xy.shape[1] for key_points in key_points_list}\n        if len(keypoint_counts) > 1:\n            raise ValueError(\n                \"All KeyPoints must have the same number of keypoints per \"\n                f\"skeleton to be merged; got counts {sorted(keypoint_counts)}.\"\n            )\n\n        keypoint_depths = {key_points.xy.shape[2] for key_points in key_points_list}\n        if len(keypoint_depths) > 1:\n            raise ValueError(\n                \"All KeyPoints must have the same coordinate depth per \"\n                f\"skeleton to be merged; got depths {sorted(keypoint_depths)}.\"\n            )\n\n        xy = np.vstack([key_points.xy for key_points in key_points_list])\n\n        def stack_or_none(name: str) -> npt.NDArray[np.generic] | None:\n            values = [getattr(key_points, name) for key_points in key_points_list]\n            if all(value is None for value in values):\n                return None\n            if any(value is None for value in values):","sourceCodeStart":1243,"sourceCodeEnd":1279,"githubUrl":"https://github.com/roboflow/supervision/blob/7f254d9784d4c37e0f03cd89ddee164c8db099c0/src/supervision/key_points/core.py#L1243-L1279","documentation":"When sv.F1Score is constructed with metric_target=MetricTarget.MASKS, _detections_content() returns detections.mask (dense bool array or CompactMask). If mask is None on a non-empty Detections object, mask IoU cannot be computed and this error is raised. Empty Detections without masks are allowed and get a (0,0,0) placeholder.","triggerScenarios":"sv.F1Score(metric_target=sv.MetricTarget.MASKS) fed with box-only Detections: manual sv.Detections(xyxy=...) without mask=, box-detector connectors, or COCO detection (non-segmentation) annotations.","commonSituations":"Toggling an existing box-metrics script to MASKS without switching the model or dataset to segmentation sources; manual Detections construction in tests that omit mask.","solutions":["Feed Detections from a segmentation connector (e.g. from_ultralytics on YOLO-seg output)","Pass mask=np.array((N,H,W), bool) when constructing Detections manually","Keep metric_target=MetricTarget.BOXES if you only have boxes","Ensure both predictions and targets carry masks — the check applies to each Detections passed in"],"exampleFix":"# before\nf1 = sv.F1Score(metric_target=sv.MetricTarget.MASKS)\ndet = sv.Detections(\n    xyxy=np.array([[30.0, 30.0, 100.0, 100.0]]),\n    class_id=np.array([0]),\n    confidence=np.array([0.9]),\n)\nf1.update(predictions=[det], targets=[gt])  # -> ValueError\n\n# after\nmasks = np.zeros((1, 480, 640), dtype=bool)\nmasks[0, 30:100, 30:100] = True\ndet = sv.Detections(\n    xyxy=np.array([[30.0, 30.0, 100.0, 100.0]]),\n    class_id=np.array([0]),\n    confidence=np.array([0.9]),\n    mask=masks,\n)\nf1.update(predictions=[det], targets=[gt])","handlingStrategy":"type-guard","validationCode":"def masks_ready(detections: sv.Detections) -> bool:\n    return detections.is_empty() or detections.mask is not None\n\nfor d in predictions + targets:\n    assert masks_ready(d), 'MASKS F1 requires non-empty Detections to carry mask'","typeGuard":"def is_mask_detections(detections: sv.Detections) -> bool:\n    \"\"\"True when Detections can be evaluated with MetricTarget.MASKS.\"\"\"\n    return detections.is_empty() or detections.mask is not None","tryCatchPattern":"try:\n    f1_mask.update(predictions=preds, targets=gts)\nexcept ValueError as e:\n    if 'requires detections to include masks' in str(e):\n        logger.warning('No masks found; falling back to BOXES F1')\n        f1_box = sv.F1Score()  # and re-run\n    else:\n        raise","preventionTips":["Select MASKS only when model and annotations both provide segmentation masks","Always pass mask= when hand-constructing Detections for mask metrics","Check detections.mask is not None in batch loops and fail with a contextual error naming the image"],"tags":["metrics","f1-score","masks","segmentation"],"backgroundTag":null,"analyzedSha":"7f254d9784d4c37e0f03cd89ddee164c8db099c0","analyzedAt":"2026-08-15T05:13:01.950Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}