Skip to content

Commit

Permalink
Merge pull request #6 from EMalagoli92/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
EMalagoli92 authored Apr 22, 2024
2 parents f374f7d + 67735b5 commit ff9d50a
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 19 deletions.
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ from od_metrics import iou
y_true = [[25, 16, 38, 56], [129, 123, 41, 62]]
y_pred = [[25, 27, 37, 54], [119, 111, 40, 67], [124, 9, 49, 67]]

result = iou(y_true, y_pred)
result = iou(y_true, y_pred, box_format="xywh")
print(result)
"""
array([[0.67655425, 0. ],
Expand Down
4 changes: 2 additions & 2 deletions samples/samples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
"y_true = [[25, 16, 38, 56], [129, 123, 41, 62]]\n",
"y_pred = [[25, 27, 37, 54], [119, 111, 40, 67], [124, 9, 49, 67]]\n",
"\n",
"result = iou(y_true, y_pred)\n",
"result = iou(y_true, y_pred, box_format=\"xywh\")\n",
"result"
]
}
Expand All @@ -361,7 +361,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.18"
"version": "3.11.5"
}
},
"nbformat": 4,
Expand Down
42 changes: 26 additions & 16 deletions src/od_metrics/od_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import numpy as np

from .constants import DEFAULT_COCO, _STANDARD_OUTPUT
from .utils import get_indexes, get_suffix, _Missing
from .utils import get_indexes, get_suffix, _Missing, to_xyxy
from .validators import ConstructorModel, ComputeModel, MeanModel


Expand Down Expand Up @@ -483,7 +483,8 @@ def _compute_iou(
ious = iou(
y_true=y_true_boxes,
y_pred=y_pred_boxes,
iscrowd=iscrowd
iscrowd=iscrowd,
box_format="xywh",
)
return ious

Expand Down Expand Up @@ -1050,13 +1051,11 @@ def iou(
y_true: np.ndarray | list,
y_pred: np.ndarray | list,
iscrowd: np.ndarray | list[bool] | list[int] | None = None,
box_format: Literal["xyxy", "xywh", "cxcywh"] = "xywh",
) -> np.ndarray:
"""
Calculate IoU between bounding boxes.
Single bounding boxes must be in `"xywh"` format, i.e.
[xmin, ymin, width, height]
The standard iou of a ground truth `y_true` and detected
`y_pred` object is:
Expand Down Expand Up @@ -1096,6 +1095,23 @@ def iou(
Whether `y_true` are crowd regions.
If `None`, it will be set to `False` for all `y_true`.
The default is `None`.
box_format: Literal["xyxy", "xywh", "cxcywh"], optional
Bounding box format.
Supported formats are:<br>
- `"xyxy"`: boxes are represented via corners,
x1, y1 being top left and x2, y2
being bottom right.<br>
- `"xywh"`: boxes are represented via corner,
width and height, x1, y2 being top
left, w, h being width and height.
This is the default format; all
input formats will be converted
to this.<br>
- `"cxcywh"`: boxes are represented via centre,
width and height, cx, cy being
center of box, w, h being width
and height.<br>
The default is `"xywh"`.
Returns
-------
Expand All @@ -1111,20 +1127,14 @@ def iou(
"length.")
else:
iscrowd = [False]*len(y_true)
# To np.ndarray
if not isinstance(y_pred, np.ndarray):
y_pred = np.array(y_pred)
if not isinstance(y_true, np.ndarray):
y_true = np.array(y_true)
# To np.ndarray and xyxy box format
y_true = np.array([to_xyxy(bbox_, box_format) for bbox_ in y_true])
y_pred = np.array([to_xyxy(bbox_, box_format) for bbox_ in y_pred])

# pylint: disable-next=W0632
xmin1, ymin1, width1, height1 = np.hsplit(y_pred, 4)
xmin1, ymin1, xmax1, ymax1 = np.hsplit(y_pred, 4)
# pylint: disable-next=W0632
xmin2, ymin2, width2, height2 = np.hsplit(y_true, 4)
xmax1 = xmin1 + width1
xmax2 = xmin2 + width2
ymax1 = ymin1 + height1
ymax2 = ymin2 + height2
xmin2, ymin2, xmax2, ymax2 = np.hsplit(y_true, 4)

# Intersection
xmin_i = np.maximum(xmin1.T, xmin2).T
Expand Down
83 changes: 83 additions & 0 deletions src/od_metrics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"_Missing",
"get_indexes",
"get_suffix",
"to_xywh",
"to_xyxy",
]

from typing import Literal
Expand Down Expand Up @@ -124,6 +126,87 @@ def to_xywh(
)


def xywh_xyxy(bbox: list[float]) -> list[float]:
"""
Change bounding box format from `xywh` to `xyxy`.
Parameters
----------
bbox : list[float]
Input bounding box.
Returns
-------
list[float]
Bounding box in `"xyxy"` format.
"""
return [
bbox[0],
bbox[1],
bbox[0] + bbox[2],
bbox[1] + bbox[3]
]


def cxcywh_xyxy(bbox: list[float]) -> list[float]:
"""
Change bounding box format from `cxcywh` to `xyxy`.
Parameters
----------
bbox : list[float]
Input bounding box.
Returns
-------
list[float]
Bounding box in `"xyxy"` format.
"""
return [
bbox[0] - bbox[2] / 2,
bbox[1] - bbox[3] / 2,
bbox[0] + bbox[2] / 2,
bbox[1] + bbox[3] / 2
]


def to_xyxy(
bbox: list[float],
box_format: Literal["xyxy", "xywh", "cxcywh"],
) -> list[float]:
"""
Change bounding box format to `"xyxy"`.
Parameters
----------
bbox : list[float]
Input bounding box.
box_format : Literal["xyxy", "xywh", "cxcywh"]
Input bounding box format.
It can be `"xyxy"`, `"xywh"` or `"cxcywh"`.
Raises
------
ValueError
If `box_format` not one of `"xyxy"`, `"xywh"`, `"cxcywh"`.
Returns
-------
list[float]
Bounding box in `"xyxy"` format.
"""
if box_format == "xywh":
return xywh_xyxy(bbox)
if box_format == "xyxy":
return bbox
if box_format == "cxcywh":
return cxcywh_xyxy(bbox)
raise ValueError(
"`box_format` can be `'xyxy'`, `'xywh'`, `'cxcywh'`. "
f"Found {box_format}"
)


def get_suffix(
iou_threshold: np.ndarray,
area_range_key: np.ndarray,
Expand Down
28 changes: 28 additions & 0 deletions tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,34 @@
"to_cover": {"pycoco_converter": False, "box_format_converter": False},
"ids": "annotations_exception_ypred_no_boxes"
},
{
"compute_settings": {"extended_summary": True},
"y_true": [
{"labels": [0, 2],
"boxes": [[17, 83, 97, 47], [57, 86, 96, 73]]}
],
"y_pred": [
{
"labels": [0, 2],
"boxes": [[-17, -83, 0, 47], [0, 86, -96, 73]],
"scores": [.2, .3]}
],
"ids": "annotations_with_no_valid_y_pred_boxes"
},
{
"compute_settings": {"extended_summary": True},
"y_true": [
{"labels": [0, 2],
"boxes": [[-17, -83, 0, 47], [0, 86, -96, 73]]}
],
"y_pred": [
{
"labels": [0, 2],
"boxes": [[17, 83, 97, 47], [57, 86, 96, 73]],
"scores": [.2, .3]}
],
"ids": "annotations_with_no_valid_y_true_boxes"
},
]


Expand Down
110 changes: 110 additions & 0 deletions tests/test_odmetrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import copy
from typing import Any, Literal
from functools import partial
from itertools import product
import numpy as np
from parameterized import parameterized, parameterized_class

from src.od_metrics import ODMetrics, iou
from src.od_metrics.constants import DEFAULT_COCO
from src.od_metrics.utils import to_xywh
from tests.utils import annotations_generator, pycoco_converter, \
test_equality, rename_dict, xywh_to, apply_function
from tests.config import TESTS
Expand Down Expand Up @@ -331,6 +333,65 @@ def test_pycoco_equivalence(

self.assertTrue(test_equality(od_metrics_ious, pycoco_ious))

@parameterized.expand(list(product(
["random", None], ["xyxy", "xywh", "cxcywh", "error"])))
def test_box_formats(
self,
iscrowd_mode: Literal["random", None],
box_format: Literal["xyxy", "xywh", "cxcywh"],
) -> None:
"""Test `box_format` argument."""
if iscrowd_mode == "random":
iscrowd = list(map(
bool,
np.random.randint(
low=0,
high=2,
size=[self.SIZE]
).tolist()
)
)
iscrowd_pycoco = iscrowd
else:
iscrowd = None
iscrowd_pycoco = [False] * self.SIZE
y_pred = np.random.randint(
low=1,
high=self.HIGH,
size=[self.SIZE, 4]
)

y_true = np.random.randint(
low=1,
high=self.HIGH,
size=[self.SIZE, 4]
)

if box_format in ["xyxy", "xywh", "cxcywh"]:
od_metrics_ious = iou(
y_true=y_true,
y_pred=y_pred,
iscrowd=iscrowd,
box_format=box_format,
)

y_true_pycoco = np.array([to_xywh(bbox_, box_format)
for bbox_ in y_true])
y_pred_pycoco = np.array([to_xywh(bbox_, box_format)
for bbox_ in y_pred])
pycoco_ious = maskUtils.iou(y_pred_pycoco, y_true_pycoco,
iscrowd_pycoco)

self.assertTrue(test_equality(od_metrics_ious, pycoco_ious))
else:
with self.assertRaises(ValueError):
od_metrics_ious = iou(
y_true=y_true,
y_pred=y_pred,
iscrowd=iscrowd,
box_format=box_format,
)

def test_length_exception(self) -> None:
"""Test exception `iscrowd` and `y_true` different length."""
iscrowd = list(map(
Expand Down Expand Up @@ -360,6 +421,55 @@ def test_length_exception(self) -> None:
iscrowd=iscrowd,
)

@parameterized.expand(list(product(
["random", None], ["xyxy", "xywh", "cxcywh"])))
def test_not_valid_boxes(
self,
iscrowd_mode: Literal["random", None],
box_format: Literal["xyxy", "xywh", "cxcywh"],
) -> None:
"""Test equivalence for not valid (negative values) boxes."""
if iscrowd_mode == "random":
iscrowd = list(map(
bool,
np.random.randint(
low=0,
high=2,
size=[self.SIZE]
).tolist()
)
)
iscrowd_pycoco = iscrowd
else:
iscrowd = None
iscrowd_pycoco = [False] * self.SIZE
y_pred = np.random.randint(
low=-10,
high=1,
size=[self.SIZE, 4]
)
y_true = np.random.randint(
low=-10,
high=1,
size=[self.SIZE, 4]
)

od_metrics_ious = iou(
y_true=y_true,
y_pred=y_pred,
iscrowd=iscrowd,
box_format=box_format,
)

y_true_pycoco = np.array([to_xywh(bbox_, box_format)
for bbox_ in y_true])
y_pred_pycoco = np.array([to_xywh(bbox_, box_format)
for bbox_ in y_pred])
pycoco_ious = maskUtils.iou(y_pred_pycoco, y_true_pycoco,
iscrowd_pycoco)

self.assertTrue(test_equality(od_metrics_ious, pycoco_ious))


if __name__ == "__main__":
unittest.main() # pragma: no cover

0 comments on commit ff9d50a

Please sign in to comment.