Skip to content

Commit

Permalink
Bugfix - Object Detection IOU (#817)
Browse files Browse the repository at this point in the history
  • Loading branch information
czaloom authored Nov 8, 2024
1 parent 09f804b commit aaf352c
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 108 deletions.
59 changes: 55 additions & 4 deletions lite/tests/object_detection/test_dataloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,49 @@ def test_no_data():
loader.finalize()


def test_iou_computation():

detection = Detection(
uid="uid",
groundtruths=[
BoundingBox(xmin=0, xmax=10, ymin=0, ymax=10, labels=["0"]),
BoundingBox(xmin=100, xmax=110, ymin=100, ymax=110, labels=["0"]),
BoundingBox(
xmin=1000, xmax=1100, ymin=1000, ymax=1100, labels=["0"]
),
],
predictions=[
BoundingBox(
xmin=1,
xmax=11,
ymin=1,
ymax=11,
labels=["0", "1", "2"],
scores=[0.5, 0.25, 0.25],
),
BoundingBox(
xmin=105,
xmax=116,
ymin=105,
ymax=116,
labels=["0", "1", "2"],
scores=[0.5, 0.25, 0.25],
),
],
)

loader = DataLoader()
loader.add_bounding_boxes([detection])

assert len(loader.pairs) == 1

# show that three unique IOUs exist
unique_ious = np.unique(loader.pairs[0][:, 3])
assert np.isclose(
unique_ious, np.array([0.0, 0.12755102, 0.68067227])
).all()


def test_mixed_annotations(
rect1: tuple[float, float, float, float],
rect1_rotated_5_degrees_around_origin: tuple[float, float, float, float],
Expand Down Expand Up @@ -87,7 +130,15 @@ def test_mixed_annotations(

loader = DataLoader()

for input_ in mixed_detections:
with pytest.raises(ValueError) as e:
loader.add_bounding_boxes([input_])
assert "but annotation is of type" in str(e)
for detection in mixed_detections:

# anything can be converted to a bbox
loader.add_bounding_boxes([detection])

with pytest.raises(AttributeError) as e:
loader.add_polygons([detection])
assert "no attribute 'shape'" in str(e)

with pytest.raises(AttributeError) as e:
loader.add_bitmasks([detection])
assert "no attribute 'mask'" in str(e)
24 changes: 0 additions & 24 deletions lite/valor_lite/object_detection/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,6 @@ def extrema(self) -> tuple[float, float, float, float]:
xmin, ymin, xmax, ymax = self.shape.bounds
return (xmin, xmax, ymin, ymax)

@property
def annotation(self) -> ShapelyPolygon:
"""
Returns the annotation's data representation.
Returns
-------
shapely.geometry.Polygon
The polygon shape.
"""
return self.shape


@dataclass
class Bitmask:
Expand Down Expand Up @@ -222,18 +210,6 @@ def extrema(self) -> tuple[float, float, float, float]:
rows, cols = np.nonzero(self.mask)
return (cols.min(), cols.max(), rows.min(), rows.max())

@property
def annotation(self) -> NDArray[np.bool_]:
"""
Returns the annotation's data representation.
Returns
-------
NDArray[np.bool_]
The binary mask array.
"""
return self.mask


@dataclass
class Detection:
Expand Down
151 changes: 71 additions & 80 deletions lite/valor_lite/object_detection/manager.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
from collections import defaultdict
from dataclasses import dataclass
from typing import Type

import numpy as np
import valor_lite.object_detection.annotation as annotation
from numpy.typing import NDArray
from tqdm import tqdm
from valor_lite.object_detection.annotation import (
Bitmask,
BoundingBox,
Detection,
Polygon,
)
from valor_lite.object_detection.annotation import Detection
from valor_lite.object_detection.computation import (
compute_bbox_iou,
compute_bitmask_iou,
Expand Down Expand Up @@ -396,74 +389,47 @@ def _add_label(self, label: str) -> int:

return self._evaluator.label_to_index[label]

def _compute_ious_and_cache_pairs(
def _cache_pairs(
self,
uid_index: int,
groundtruths: list,
predictions: list,
annotation_type: Type[BoundingBox] | Type[Polygon] | Type[Bitmask],
ious: NDArray[np.float64],
) -> None:
"""
Compute IOUs between groundtruths and preditions before storing as pairs.
Parameters
----------
uid_index: int
uid_index : int
The index of the detection.
groundtruths: list
groundtruths : list
A list of groundtruths.
predictions: list
predictions : list
A list of predictions.
annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask]
The type of annotation to compute IOUs for.
ious : NDArray[np.float64]
An array with shape (n_preds, n_gts) containing IOUs.
"""

pairs = list()
n_predictions = len(predictions)
n_groundtruths = len(groundtruths)

all_pairs = np.array(
[
np.array([gann, pann])
for _, _, _, pann in predictions
for _, _, gann in groundtruths
]
)

match annotation_type:
case annotation.BoundingBox:
ious = compute_bbox_iou(all_pairs)
case annotation.Polygon:
ious = compute_polygon_iou(all_pairs)
case annotation.Bitmask:
ious = compute_bitmask_iou(all_pairs)
case _:
raise ValueError(
f"Invalid annotation type `{annotation_type}`."
)

ious = ious.reshape(n_predictions, n_groundtruths)
predictions_with_iou_of_zero = np.where((ious < 1e-9).all(axis=1))[0]
groundtruths_with_iou_of_zero = np.where((ious < 1e-9).all(axis=0))[0]

pairs.extend(
[
np.array(
[
float(uid_index),
float(gidx),
float(pidx),
ious[pidx, gidx],
float(glabel),
float(plabel),
float(score),
]
)
for pidx, plabel, score, _ in predictions
for gidx, glabel, _ in groundtruths
if ious[pidx, gidx] >= 1e-9
]
)
pairs = [
np.array(
[
float(uid_index),
float(gidx),
float(pidx),
ious[pidx, gidx],
float(glabel),
float(plabel),
float(score),
]
)
for pidx, plabel, score in predictions
for gidx, glabel in groundtruths
if ious[pidx, gidx] >= 1e-9
]
pairs.extend(
[
np.array(
Expand Down Expand Up @@ -496,13 +462,12 @@ def _compute_ious_and_cache_pairs(
for index in groundtruths_with_iou_of_zero
]
)

self.pairs.append(np.array(pairs))

def _add_data(
self,
detections: list[Detection],
annotation_type: type[Bitmask] | type[BoundingBox] | type[Polygon],
detection_ious: list[NDArray[np.float64]],
show_progress: bool = False,
):
"""
Expand All @@ -512,13 +477,15 @@ def _add_data(
----------
detections : list[Detection]
A list of Detection objects.
annotation_type : type[Bitmask] | type[BoundingBox] | type[Polygon]
The annotation type to process.
detection_ious : list[NDArray[np.float64]]
A list of arrays containing IOUs per detection.
show_progress : bool, default=False
Toggle for tqdm progress bar.
"""
disable_tqdm = not show_progress
for detection in tqdm(detections, disable=disable_tqdm):
for detection, ious in tqdm(
zip(detections, detection_ious), disable=disable_tqdm
):

# update metadata
self._evaluator.n_datums += 1
Expand All @@ -541,11 +508,6 @@ def _add_data(
predictions = list()

for gidx, gann in enumerate(detection.groundtruths):
if not isinstance(gann, annotation_type):
raise ValueError(
f"Expected {annotation_type}, but annotation is of type {type(gann)}."
)

self._evaluator.groundtruth_examples[uid_index][
gidx
] = gann.extrema
Expand All @@ -556,16 +518,10 @@ def _add_data(
(
gidx,
label_idx,
gann.annotation,
)
)

for pidx, pann in enumerate(detection.predictions):
if not isinstance(pann, annotation_type):
raise ValueError(
f"Expected {annotation_type}, but annotation is of type {type(pann)}."
)

self._evaluator.prediction_examples[uid_index][
pidx
] = pann.extrema
Expand All @@ -577,15 +533,14 @@ def _add_data(
pidx,
label_idx,
pscore,
pann.annotation,
)
)

self._compute_ious_and_cache_pairs(
self._cache_pairs(
uid_index=uid_index,
groundtruths=groundtruths,
predictions=predictions,
annotation_type=annotation_type,
ious=ious,
)

def add_bounding_boxes(
Expand All @@ -603,10 +558,22 @@ def add_bounding_boxes(
show_progress : bool, default=False
Toggle for tqdm progress bar.
"""
ious = [
compute_bbox_iou(
np.array(
[
[gt.extrema, pd.extrema]
for pd in detection.predictions
for gt in detection.groundtruths
]
)
).reshape(len(detection.predictions), len(detection.groundtruths))
for detection in detections
]
return self._add_data(
detections=detections,
detection_ious=ious,
show_progress=show_progress,
annotation_type=BoundingBox,
)

def add_polygons(
Expand All @@ -624,10 +591,22 @@ def add_polygons(
show_progress : bool, default=False
Toggle for tqdm progress bar.
"""
ious = [
compute_polygon_iou(
np.array(
[
[gt.shape, pd.shape] # type: ignore - using the AttributeError as a validator
for pd in detection.predictions
for gt in detection.groundtruths
]
)
).reshape(len(detection.predictions), len(detection.groundtruths))
for detection in detections
]
return self._add_data(
detections=detections,
detection_ious=ious,
show_progress=show_progress,
annotation_type=Polygon,
)

def add_bitmasks(
Expand All @@ -645,10 +624,22 @@ def add_bitmasks(
show_progress : bool, default=False
Toggle for tqdm progress bar.
"""
ious = [
compute_bitmask_iou(
np.array(
[
[gt.mask, pd.mask] # type: ignore - using the AttributeError as a validator
for pd in detection.predictions
for gt in detection.groundtruths
]
)
).reshape(len(detection.predictions), len(detection.groundtruths))
for detection in detections
]
return self._add_data(
detections=detections,
detection_ious=ious,
show_progress=show_progress,
annotation_type=Bitmask,
)

def finalize(self) -> Evaluator:
Expand Down

0 comments on commit aaf352c

Please sign in to comment.