|
|
|
# Ultralytics YOLO 🚀, AGPL-3.0 license
|
|
|
|
|
|
|
|
from collections import abc
|
|
|
|
from itertools import repeat
|
|
|
|
from numbers import Number
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
from .ops import ltwh2xywh, ltwh2xyxy, resample_segments, xywh2ltwh, xywh2xyxy, xyxy2ltwh, xyxy2xywh
|
|
|
|
|
|
|
|
|
|
|
|
def _ntuple(n):
|
|
|
|
"""From PyTorch internals."""
|
|
|
|
|
|
|
|
def parse(x):
|
|
|
|
"""Parse bounding boxes format between XYWH and LTWH."""
|
|
|
|
return x if isinstance(x, abc.Iterable) else tuple(repeat(x, n))
|
|
|
|
|
|
|
|
return parse
|
|
|
|
|
|
|
|
|
|
|
|
to_4tuple = _ntuple(4)
|
|
|
|
|
|
|
|
# `xyxy` means left top and right bottom
|
|
|
|
# `xywh` means center x, center y and width, height(yolo format)
|
|
|
|
# `ltwh` means left top and width, height(coco format)
|
|
|
|
_formats = ['xyxy', 'xywh', 'ltwh']
|
|
|
|
|
|
|
|
__all__ = 'Bboxes', # tuple or list
|
|
|
|
|
|
|
|
|
|
|
|
class Bboxes:
|
|
|
|
"""Now only numpy is supported."""
|
|
|
|
|
|
|
|
def __init__(self, bboxes, format='xyxy') -> None:
|
|
|
|
assert format in _formats, f'Invalid bounding box format: {format}, format must be one of {_formats}'
|
|
|
|
bboxes = bboxes[None, :] if bboxes.ndim == 1 else bboxes
|
|
|
|
assert bboxes.ndim == 2
|
|
|
|
assert bboxes.shape[1] == 4
|
|
|
|
self.bboxes = bboxes
|
|
|
|
self.format = format
|
|
|
|
# self.normalized = normalized
|
|
|
|
|
|
|
|
# def convert(self, format):
|
|
|
|
# assert format in _formats
|
|
|
|
# if self.format == format:
|
|
|
|
# bboxes = self.bboxes
|
|
|
|
# elif self.format == "xyxy":
|
|
|
|
# if format == "xywh":
|
|
|
|
# bboxes = xyxy2xywh(self.bboxes)
|
|
|
|
# else:
|
|
|
|
# bboxes = xyxy2ltwh(self.bboxes)
|
|
|
|
# elif self.format == "xywh":
|
|
|
|
# if format == "xyxy":
|
|
|
|
# bboxes = xywh2xyxy(self.bboxes)
|
|
|
|
# else:
|
|
|
|
# bboxes = xywh2ltwh(self.bboxes)
|
|
|
|
# else:
|
|
|
|
# if format == "xyxy":
|
|
|
|
# bboxes = ltwh2xyxy(self.bboxes)
|
|
|
|
# else:
|
|
|
|
# bboxes = ltwh2xywh(self.bboxes)
|
|
|
|
#
|
|
|
|
# return Bboxes(bboxes, format)
|
|
|
|
|
|
|
|
def convert(self, format):
|
|
|
|
"""Converts bounding box format from one type to another."""
|
|
|
|
assert format in _formats, f'Invalid bounding box format: {format}, format must be one of {_formats}'
|
|
|
|
if self.format == format:
|
|
|
|
return
|
|
|
|
elif self.format == 'xyxy':
|
|
|
|
bboxes = xyxy2xywh(self.bboxes) if format == 'xywh' else xyxy2ltwh(self.bboxes)
|
|
|
|
elif self.format == 'xywh':
|
|
|
|
bboxes = xywh2xyxy(self.bboxes) if format == 'xyxy' else xywh2ltwh(self.bboxes)
|
|
|
|
else:
|
|
|
|
bboxes = ltwh2xyxy(self.bboxes) if format == 'xyxy' else ltwh2xywh(self.bboxes)
|
|
|
|
self.bboxes = bboxes
|
|
|
|
self.format = format
|
|
|
|
|
|
|
|
def areas(self):
|
|
|
|
"""Return box areas."""
|
|
|
|
self.convert('xyxy')
|
|
|
|
return (self.bboxes[:, 2] - self.bboxes[:, 0]) * (self.bboxes[:, 3] - self.bboxes[:, 1])
|
|
|
|
|
|
|
|
# def denormalize(self, w, h):
|
|
|
|
# if not self.normalized:
|
|
|
|
# return
|
|
|
|
# assert (self.bboxes <= 1.0).all()
|
|
|
|
# self.bboxes[:, 0::2] *= w
|
|
|
|
# self.bboxes[:, 1::2] *= h
|
|
|
|
# self.normalized = False
|
|
|
|
#
|
|
|
|
# def normalize(self, w, h):
|
|
|
|
# if self.normalized:
|
|
|
|
# return
|
|
|
|
# assert (self.bboxes > 1.0).any()
|
|
|
|
# self.bboxes[:, 0::2] /= w
|
|
|
|
# self.bboxes[:, 1::2] /= h
|
|
|
|
# self.normalized = True
|
|
|
|
|
|
|
|
def mul(self, scale):
|
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
scale (tuple) or (list) or (int): the scale for four coords.
|
|
|
|
"""
|
|
|
|
if isinstance(scale, Number):
|
|
|
|
scale = to_4tuple(scale)
|
|
|
|
assert isinstance(scale, (tuple, list))
|
|
|
|
assert len(scale) == 4
|
|
|
|
self.bboxes[:, 0] *= scale[0]
|
|
|
|
self.bboxes[:, 1] *= scale[1]
|
|
|
|
self.bboxes[:, 2] *= scale[2]
|
|
|
|
self.bboxes[:, 3] *= scale[3]
|
|
|
|
|
|
|
|
def add(self, offset):
|
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
offset (tuple) or (list) or (int): the offset for four coords.
|
|
|
|
"""
|
|
|
|
if isinstance(offset, Number):
|
|
|
|
offset = to_4tuple(offset)
|
|
|
|
assert isinstance(offset, (tuple, list))
|
|
|
|
assert len(offset) == 4
|
|
|
|
self.bboxes[:, 0] += offset[0]
|
|
|
|
self.bboxes[:, 1] += offset[1]
|
|
|
|
self.bboxes[:, 2] += offset[2]
|
|
|
|
self.bboxes[:, 3] += offset[3]
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
"""Return the number of boxes."""
|
|
|
|
return len(self.bboxes)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def concatenate(cls, boxes_list: List['Bboxes'], axis=0) -> 'Bboxes':
|
|
|
|
"""
|
|
|
|
Concatenate a list of Bboxes objects into a single Bboxes object.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
boxes_list (List[Bboxes]): A list of Bboxes objects to concatenate.
|
|
|
|
axis (int, optional): The axis along which to concatenate the bounding boxes.
|
|
|
|
Defaults to 0.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Bboxes: A new Bboxes object containing the concatenated bounding boxes.
|
|
|
|
|
|
|
|
Note:
|
|
|
|
The input should be a list or tuple of Bboxes objects.
|
|
|
|
"""
|
|
|
|
assert isinstance(boxes_list, (list, tuple))
|
|
|
|
if not boxes_list:
|
|
|
|
return cls(np.empty(0))
|
|
|
|
assert all(isinstance(box, Bboxes) for box in boxes_list)
|
|
|
|
|
|
|
|
if len(boxes_list) == 1:
|
|
|
|
return boxes_list[0]
|
|
|
|
return cls(np.concatenate([b.bboxes for b in boxes_list], axis=axis))
|
|
|
|
|
|
|
|
def __getitem__(self, index) -> 'Bboxes':
|
|
|
|
"""
|
|
|
|
Retrieve a specific bounding box or a set of bounding boxes using indexing.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
index (int, slice, or np.ndarray): The index, slice, or boolean array to select
|
|
|
|
the desired bounding boxes.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Bboxes: A new Bboxes object containing the selected bounding boxes.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
AssertionError: If the indexed bounding boxes do not form a 2-dimensional matrix.
|
|
|
|
|
|
|
|
Note:
|
|
|
|
When using boolean indexing, make sure to provide a boolean array with the same
|
|
|
|
length as the number of bounding boxes.
|
|
|
|
"""
|
|
|
|
if isinstance(index, int):
|
|
|
|
return Bboxes(self.bboxes[index].view(1, -1))
|
|
|
|
b = self.bboxes[index]
|
|
|
|
assert b.ndim == 2, f'Indexing on Bboxes with {index} failed to return a matrix!'
|
|
|
|
return Bboxes(b)
|
|
|
|
|
|
|
|
|
|
|
|
class Instances:
|
|
|
|
|
|
|
|
def __init__(self, bboxes, segments=None, keypoints=None, bbox_format='xywh', normalized=True) -> None:
|
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
bboxes (ndarray): bboxes with shape [N, 4].
|
|
|
|
segments (list | ndarray): segments.
|
|
|
|
keypoints (ndarray): keypoints(x, y, visible) with shape [N, 17, 3].
|
|
|
|
"""
|
|
|
|
if segments is None:
|
|
|
|
segments = []
|
|
|
|
self._bboxes = Bboxes(bboxes=bboxes, format=bbox_format)
|
|
|
|
self.keypoints = keypoints
|
|
|
|
self.normalized = normalized
|
|
|
|
|
|
|
|
if len(segments) > 0:
|
|
|
|
# list[np.array(1000, 2)] * num_samples
|
|
|
|
segments = resample_segments(segments)
|
|
|
|
# (N, 1000, 2)
|
|
|
|
segments = np.stack(segments, axis=0)
|
|
|
|
else:
|
|
|
|
segments = np.zeros((0, 1000, 2), dtype=np.float32)
|
|
|
|
self.segments = segments
|
|
|
|
|
|
|
|
def convert_bbox(self, format):
|
|
|
|
"""Convert bounding box format."""
|
|
|
|
self._bboxes.convert(format=format)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def bbox_areas(self):
|
|
|
|
"""Calculate the area of bounding boxes."""
|
|
|
|
return self._bboxes.areas()
|
|
|
|
|
|
|
|
def scale(self, scale_w, scale_h, bbox_only=False):
|
|
|
|
"""this might be similar with denormalize func but without normalized sign."""
|
|
|
|
self._bboxes.mul(scale=(scale_w, scale_h, scale_w, scale_h))
|
|
|
|
if bbox_only:
|
|
|
|
return
|
|
|
|
self.segments[..., 0] *= scale_w
|
|
|
|
self.segments[..., 1] *= scale_h
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints[..., 0] *= scale_w
|
|
|
|
self.keypoints[..., 1] *= scale_h
|
|
|
|
|
|
|
|
def denormalize(self, w, h):
|
|
|
|
"""Denormalizes boxes, segments, and keypoints from normalized coordinates."""
|
|
|
|
if not self.normalized:
|
|
|
|
return
|
|
|
|
self._bboxes.mul(scale=(w, h, w, h))
|
|
|
|
self.segments[..., 0] *= w
|
|
|
|
self.segments[..., 1] *= h
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints[..., 0] *= w
|
|
|
|
self.keypoints[..., 1] *= h
|
|
|
|
self.normalized = False
|
|
|
|
|
|
|
|
def normalize(self, w, h):
|
|
|
|
"""Normalize bounding boxes, segments, and keypoints to image dimensions."""
|
|
|
|
if self.normalized:
|
|
|
|
return
|
|
|
|
self._bboxes.mul(scale=(1 / w, 1 / h, 1 / w, 1 / h))
|
|
|
|
self.segments[..., 0] /= w
|
|
|
|
self.segments[..., 1] /= h
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints[..., 0] /= w
|
|
|
|
self.keypoints[..., 1] /= h
|
|
|
|
self.normalized = True
|
|
|
|
|
|
|
|
def add_padding(self, padw, padh):
|
|
|
|
"""Handle rect and mosaic situation."""
|
|
|
|
assert not self.normalized, 'you should add padding with absolute coordinates.'
|
|
|
|
self._bboxes.add(offset=(padw, padh, padw, padh))
|
|
|
|
self.segments[..., 0] += padw
|
|
|
|
self.segments[..., 1] += padh
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints[..., 0] += padw
|
|
|
|
self.keypoints[..., 1] += padh
|
|
|
|
|
|
|
|
def __getitem__(self, index) -> 'Instances':
|
|
|
|
"""
|
|
|
|
Retrieve a specific instance or a set of instances using indexing.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
index (int, slice, or np.ndarray): The index, slice, or boolean array to select
|
|
|
|
the desired instances.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Instances: A new Instances object containing the selected bounding boxes,
|
|
|
|
segments, and keypoints if present.
|
|
|
|
|
|
|
|
Note:
|
|
|
|
When using boolean indexing, make sure to provide a boolean array with the same
|
|
|
|
length as the number of instances.
|
|
|
|
"""
|
|
|
|
segments = self.segments[index] if len(self.segments) else self.segments
|
|
|
|
keypoints = self.keypoints[index] if self.keypoints is not None else None
|
|
|
|
bboxes = self.bboxes[index]
|
|
|
|
bbox_format = self._bboxes.format
|
|
|
|
return Instances(
|
|
|
|
bboxes=bboxes,
|
|
|
|
segments=segments,
|
|
|
|
keypoints=keypoints,
|
|
|
|
bbox_format=bbox_format,
|
|
|
|
normalized=self.normalized,
|
|
|
|
)
|
|
|
|
|
|
|
|
def flipud(self, h):
|
|
|
|
"""Flips the coordinates of bounding boxes, segments, and keypoints vertically."""
|
|
|
|
if self._bboxes.format == 'xyxy':
|
|
|
|
y1 = self.bboxes[:, 1].copy()
|
|
|
|
y2 = self.bboxes[:, 3].copy()
|
|
|
|
self.bboxes[:, 1] = h - y2
|
|
|
|
self.bboxes[:, 3] = h - y1
|
|
|
|
else:
|
|
|
|
self.bboxes[:, 1] = h - self.bboxes[:, 1]
|
|
|
|
self.segments[..., 1] = h - self.segments[..., 1]
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints[..., 1] = h - self.keypoints[..., 1]
|
|
|
|
|
|
|
|
def fliplr(self, w):
|
|
|
|
"""Reverses the order of the bounding boxes and segments horizontally."""
|
|
|
|
if self._bboxes.format == 'xyxy':
|
|
|
|
x1 = self.bboxes[:, 0].copy()
|
|
|
|
x2 = self.bboxes[:, 2].copy()
|
|
|
|
self.bboxes[:, 0] = w - x2
|
|
|
|
self.bboxes[:, 2] = w - x1
|
|
|
|
else:
|
|
|
|
self.bboxes[:, 0] = w - self.bboxes[:, 0]
|
|
|
|
self.segments[..., 0] = w - self.segments[..., 0]
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints[..., 0] = w - self.keypoints[..., 0]
|
|
|
|
|
|
|
|
def clip(self, w, h):
|
|
|
|
"""Clips bounding boxes, segments, and keypoints values to stay within image boundaries."""
|
|
|
|
ori_format = self._bboxes.format
|
|
|
|
self.convert_bbox(format='xyxy')
|
|
|
|
self.bboxes[:, [0, 2]] = self.bboxes[:, [0, 2]].clip(0, w)
|
|
|
|
self.bboxes[:, [1, 3]] = self.bboxes[:, [1, 3]].clip(0, h)
|
|
|
|
if ori_format != 'xyxy':
|
|
|
|
self.convert_bbox(format=ori_format)
|
|
|
|
self.segments[..., 0] = self.segments[..., 0].clip(0, w)
|
|
|
|
self.segments[..., 1] = self.segments[..., 1].clip(0, h)
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints[..., 0] = self.keypoints[..., 0].clip(0, w)
|
|
|
|
self.keypoints[..., 1] = self.keypoints[..., 1].clip(0, h)
|
|
|
|
|
|
|
|
def remove_zero_area_boxes(self):
|
|
|
|
"""Remove zero-area boxes, i.e. after clipping some boxes may have zero width or height. This removes them."""
|
|
|
|
good = self.bbox_areas > 0
|
|
|
|
if not all(good):
|
|
|
|
self._bboxes = self._bboxes[good]
|
|
|
|
if len(self.segments):
|
|
|
|
self.segments = self.segments[good]
|
|
|
|
if self.keypoints is not None:
|
|
|
|
self.keypoints = self.keypoints[good]
|
|
|
|
return good
|
|
|
|
|
|
|
|
def update(self, bboxes, segments=None, keypoints=None):
|
|
|
|
"""Updates instance variables."""
|
|
|
|
self._bboxes = Bboxes(bboxes, format=self._bboxes.format)
|
|
|
|
if segments is not None:
|
|
|
|
self.segments = segments
|
|
|
|
if keypoints is not None:
|
|
|
|
self.keypoints = keypoints
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
"""Return the length of the instance list."""
|
|
|
|
return len(self.bboxes)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def concatenate(cls, instances_list: List['Instances'], axis=0) -> 'Instances':
|
|
|
|
"""
|
|
|
|
Concatenates a list of Instances objects into a single Instances object.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
instances_list (List[Instances]): A list of Instances objects to concatenate.
|
|
|
|
axis (int, optional): The axis along which the arrays will be concatenated. Defaults to 0.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Instances: A new Instances object containing the concatenated bounding boxes,
|
|
|
|
segments, and keypoints if present.
|
|
|
|
|
|
|
|
Note:
|
|
|
|
The `Instances` objects in the list should have the same properties, such as
|
|
|
|
the format of the bounding boxes, whether keypoints are present, and if the
|
|
|
|
coordinates are normalized.
|
|
|
|
"""
|
|
|
|
assert isinstance(instances_list, (list, tuple))
|
|
|
|
if not instances_list:
|
|
|
|
return cls(np.empty(0))
|
|
|
|
assert all(isinstance(instance, Instances) for instance in instances_list)
|
|
|
|
|
|
|
|
if len(instances_list) == 1:
|
|
|
|
return instances_list[0]
|
|
|
|
|
|
|
|
use_keypoint = instances_list[0].keypoints is not None
|
|
|
|
bbox_format = instances_list[0]._bboxes.format
|
|
|
|
normalized = instances_list[0].normalized
|
|
|
|
|
|
|
|
cat_boxes = np.concatenate([ins.bboxes for ins in instances_list], axis=axis)
|
|
|
|
cat_segments = np.concatenate([b.segments for b in instances_list], axis=axis)
|
|
|
|
cat_keypoints = np.concatenate([b.keypoints for b in instances_list], axis=axis) if use_keypoint else None
|
|
|
|
return cls(cat_boxes, cat_segments, cat_keypoints, bbox_format, normalized)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def bboxes(self):
|
|
|
|
"""Return bounding boxes."""
|
|
|
|
return self._bboxes.bboxes
|