add wbf
you immediately see what subject materials im reading and where im at
This commit is contained in:
1 parent
792eda9ab3
commit
538276a5f0
4 files changed
+123
-5
No files matched your search
@@ -17,12 +17,14 @@ from pipeline.ops.geometry import (
|
||||
point_in_box,
|
||||
point_in_polygon,
|
||||
)
|
||||
from pipeline.ops.nms import nms, nms_by_class
|
||||
from pipeline.ops.nms import nms, nms_by_class, wbf, wbf_by_class
|
||||
|
||||
__all__ = [
|
||||
# NMS
|
||||
# NMS / Box Fusion
|
||||
"nms",
|
||||
"nms_by_class",
|
||||
"wbf",
|
||||
"wbf_by_class",
|
||||
# Geometry
|
||||
"expand_box",
|
||||
"point_in_box",
|
||||
|
||||
+117
-1
@@ -1,4 +1,4 @@
|
||||
"""Non-Maximum Suppression algorithms for detection filtering."""
|
||||
"""Box filtering algorithms: NMS and Weighted Boxes Fusion."""
|
||||
|
||||
from typing import List
|
||||
|
||||
@@ -59,3 +59,119 @@ def nms_by_class(
|
||||
result.extend(nms(dets, iou_threshold))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def wbf(
|
||||
detections: List[Detection],
|
||||
iou_threshold: float = 0.5,
|
||||
skip_threshold: float = 0.0,
|
||||
) -> List[Detection]:
|
||||
"""
|
||||
Weighted Boxes Fusion: Merge overlapping boxes instead of suppressing.
|
||||
|
||||
Unlike NMS which picks the best box and discards overlapping ones,
|
||||
WBF fuses overlapping boxes by averaging their coordinates weighted
|
||||
by confidence. This often gives better localization.
|
||||
|
||||
Args:
|
||||
detections: List of Detection objects
|
||||
iou_threshold: Boxes with IoU > threshold are fused together
|
||||
skip_threshold: Ignore boxes with confidence below this
|
||||
|
||||
Returns:
|
||||
Fused list of detections
|
||||
|
||||
Reference:
|
||||
https://arxiv.org/abs/1910.13302
|
||||
"""
|
||||
if not detections:
|
||||
return []
|
||||
|
||||
# Filter low confidence
|
||||
dets = [d for d in detections if d.score >= skip_threshold]
|
||||
if not dets:
|
||||
return []
|
||||
|
||||
# Sort by confidence (highest first)
|
||||
dets = sorted(dets, key=lambda d: d.score, reverse=True)
|
||||
|
||||
# Clusters of fused boxes
|
||||
clusters: List[List[Detection]] = []
|
||||
|
||||
for det in dets:
|
||||
matched_cluster = None
|
||||
|
||||
# Find a cluster this box belongs to
|
||||
for cluster in clusters:
|
||||
# Check IoU with the fused box (first element is always the fused result)
|
||||
if cluster[0].box.iou(det.box) >= iou_threshold:
|
||||
matched_cluster = cluster
|
||||
break
|
||||
|
||||
if matched_cluster is not None:
|
||||
# Add to existing cluster and recompute fused box
|
||||
matched_cluster.append(det)
|
||||
_update_fused_box(matched_cluster)
|
||||
else:
|
||||
# Start new cluster
|
||||
clusters.append([det])
|
||||
|
||||
# Return the fused box from each cluster (first element)
|
||||
return [cluster[0] for cluster in clusters]
|
||||
|
||||
|
||||
def _update_fused_box(cluster: List[Detection]):
|
||||
"""
|
||||
Recompute the fused box for a cluster.
|
||||
The fused box is stored as cluster[0].
|
||||
"""
|
||||
if len(cluster) == 1:
|
||||
return
|
||||
|
||||
# Weighted average of coordinates
|
||||
total_weight = sum(d.score for d in cluster)
|
||||
|
||||
x1 = sum(d.box.x1 * d.score for d in cluster) / total_weight
|
||||
y1 = sum(d.box.y1 * d.score for d in cluster) / total_weight
|
||||
x2 = sum(d.box.x2 * d.score for d in cluster) / total_weight
|
||||
y2 = sum(d.box.y2 * d.score for d in cluster) / total_weight
|
||||
|
||||
# Fused confidence: average boosted by number of boxes
|
||||
# More boxes agreeing = higher confidence (capped at 1.0)
|
||||
avg_conf = total_weight / len(cluster)
|
||||
fused_conf = min(1.0, avg_conf * (1 + 0.1 * (len(cluster) - 1)))
|
||||
|
||||
# Update the first element with fused values
|
||||
fused = cluster[0]
|
||||
fused.box.x1 = int(x1)
|
||||
fused.box.y1 = int(y1)
|
||||
fused.box.x2 = int(x2)
|
||||
fused.box.y2 = int(y2)
|
||||
fused.box.conf = fused_conf
|
||||
fused.score = fused_conf
|
||||
fused.metadata["wbf_count"] = len(cluster)
|
||||
|
||||
|
||||
def wbf_by_class(
|
||||
detections: List[Detection],
|
||||
iou_threshold: float = 0.5,
|
||||
skip_threshold: float = 0.0,
|
||||
) -> List[Detection]:
|
||||
"""
|
||||
Run WBF separately for each class label.
|
||||
This prevents boxes of different classes from being fused together.
|
||||
"""
|
||||
if not detections:
|
||||
return []
|
||||
|
||||
# Group by label
|
||||
by_label: dict[str, List[Detection]] = {}
|
||||
for det in detections:
|
||||
by_label.setdefault(det.label, []).append(det)
|
||||
|
||||
# Run WBF per class
|
||||
result = []
|
||||
for dets in by_label.values():
|
||||
result.extend(wbf(dets, iou_threshold, skip_threshold))
|
||||
|
||||
return result
|
||||
@@ -148,7 +148,7 @@ class APIPersonDetector(APIStageBase):
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
# haiku 4.5 is multimodal but i fear its going to ping 65 million images per second (we need cache control? who are we? why cant we upload video?)
|
||||
class VLMVerifier(APIStageBase):
|
||||
"""
|
||||
Vision-Language Model verification stage.
|
||||
|
||||
@@ -60,7 +60,7 @@ class StagePipeline:
|
||||
stage.name,
|
||||
ctx.stop_reason,
|
||||
)
|
||||
continue
|
||||
continue # break?
|
||||
|
||||
try:
|
||||
result = stage.process(ctx)
|
||||
|
||||
Reference in new issue
Block a user