boxes
dreem.inference.boxes
¶
Module containing Boxes class.
Boxes
¶
Adapted from https://github.com/facebookresearch/detectron2/blob/main/detectron2/structures/boxes.py.
This structure stores a list of boxes as a Nx4 torch.Tensor.
It supports some common methods about boxes
(area
, clip
, nonempty
, etc),
and also behaves like a Tensor
(support indexing, to(device)
, .device
, and iteration over all boxes)
Attributes:
Name | Type | Description |
---|---|---|
tensor |
Tensor
|
float matrix of Nx4. Each row is (x1, y1, x2, y2). |
Source code in dreem/inference/boxes.py
class Boxes:
"""Adapted from https://github.com/facebookresearch/detectron2/blob/main/detectron2/structures/boxes.py.
This structure stores a list of boxes as a Nx4 torch.Tensor.
It supports some common methods about boxes
(`area`, `clip`, `nonempty`, etc),
and also behaves like a Tensor
(support indexing, `to(device)`, `.device`, and iteration over all boxes)
Attributes:
tensor (torch.Tensor): float matrix of Nx4. Each row is (x1, y1, x2, y2).
"""
def __init__(self, tensor: torch.Tensor):
"""Initialize Boxes.
Args:
tensor (Tensor[float]): a Nx4 matrix. Each row is (x1, y1, x2, y2).
"""
if not isinstance(tensor, torch.Tensor):
tensor = torch.as_tensor(
tensor, dtype=torch.float32, device=torch.device("cpu")
)
else:
tensor = tensor.to(torch.float32)
if tensor.numel() == 0:
# Use reshape, so we don't end up creating a new tensor that does not depend on
# the inputs (and consequently confuses jit)
tensor = tensor.reshape((-1, 4)).to(dtype=torch.float32)
assert tensor.dim() == 3 and tensor.size(-1) == 4, tensor.size()
self.tensor = tensor
def clone(self) -> "Boxes":
"""Clone the Boxes.
Returns:
Boxes
"""
return Boxes(self.tensor.clone())
def to(self, device: torch.device) -> "Boxes":
"""Load boxes to gpu/cpu.
Args:
device: The device to load the boxes to
Returns: Boxes on device.
"""
# Boxes are assumed float32 and does not support to(dtype)
return Boxes(self.tensor.to(device=device))
def area(self) -> torch.Tensor:
"""Compute the area of all the boxes.
Returns:
torch.Tensor: a vector with areas of each box.
"""
box = self.tensor
area = (box[:, :, 2] - box[:, :, 0]) * (box[:, :, 3] - box[:, :, 1])
return area
def clip(self, box_size: Tuple[int, int]) -> None:
"""Clip (in place) the boxes.
Limits x coordinates to the range [0, width]
and y coordinates to the range [0, height].
Args:
box_size (height, width): The clipping box's size.
"""
assert torch.isfinite(self.tensor).all(), "Box tensor contains infinite or NaN!"
h, w = box_size
x1 = self.tensor[:, :, 0].clamp(min=0, max=w)
y1 = self.tensor[:, :, 1].clamp(min=0, max=h)
x2 = self.tensor[:, :, 2].clamp(min=0, max=w)
y2 = self.tensor[:, :, 3].clamp(min=0, max=h)
self.tensor = torch.stack((x1, y1, x2, y2), dim=-1)
def nonempty(self, threshold: float = 0.0) -> torch.Tensor:
"""Find boxes that are non-empty.
A box is considered empty, if either of its side is no larger than threshold.
Args:
threshold: the smallest a box can be.
Returns:
Tensor:
a binary vector which represents whether each box is empty
(False) or non-empty (True).
"""
box = self.tensor
widths = box[:, :, 2] - box[:, :, 0]
heights = box[:, :, 3] - box[:, :, 1]
keep = (widths > threshold) & (heights > threshold)
return keep
def __getitem__(self, item: Union[int, slice, torch.BoolTensor]) -> "Boxes":
"""Getter for boxes.
Args:
item: int, slice, or a BoolTensor
Returns:
Boxes: Create a new :class:`Boxes` by indexing.
Usage:
The following usage are allowed:
1. `new_boxes = boxes[3]`: return a `Boxes` which contains only one box.
2. `new_boxes = boxes[2:10]`: return a slice of boxes.
3. `new_boxes = boxes[vector]`, where vector is a torch.BoolTensor
with `length = len(boxes)`. Nonzero elements in the vector will be selected.
NOTE: that the returned Boxes might share storage with this Boxes,
subject to Pytorch's indexing semantics.
"""
if isinstance(item, int):
return Boxes(self.tensor[item])
b = self.tensor[item]
assert (
b.dim() == 3
), "Indexing on Boxes with {} failed to return a matrix!".format(item)
return Boxes(b)
def __len__(self) -> int:
"""Get the number of boxes stored in this object.
Returns:
the number of boxes stored in this object
"""
return self.tensor.shape[0]
def __repr__(self) -> str:
"""Override representation for printing.
Returns:
'Boxes(tensor)'
"""
return "Boxes(" + str(self.tensor) + ")"
def inside_box(
self, box_size: Tuple[int, int], boundary_threshold: int = 0
) -> torch.Tensor:
"""Check if box is inside reference box.
Args:
box_size (height, width): Size of the reference box.
boundary_threshold (int): Boxes that extend beyond the reference box
boundary by more than boundary_threshold are considered "outside".
Returns:
a binary vector, indicating whether each box is inside the reference box.
"""
height, width = box_size
inds_inside = (
(self.tensor[..., 0] >= -boundary_threshold)
& (self.tensor[..., 1] >= -boundary_threshold)
& (self.tensor[..., 2] < width + boundary_threshold)
& (self.tensor[..., 3] < height + boundary_threshold)
)
return inds_inside
def get_centers(self) -> torch.Tensor:
"""Get the centroid of the bbox.
Returns:
The box centers in a Nx2 array of (x, y).
"""
return (self.tensor[:, :, :2] + self.tensor[:, :, 2:]) / 2
def scale(self, scale_x: float, scale_y: float) -> None:
"""Scale the box with horizontal and vertical scaling factors."""
self.tensor[:, :, 0::2] *= scale_x
self.tensor[:, :, 1::2] *= scale_y
@classmethod
def cat(cls, boxes_list: List["Boxes"]) -> "Boxes":
"""Concatenates a list of Boxes into a single Boxes.
Arguments:
boxes_list: list of `Boxes`
Returns:
Boxes: the concatenated Boxes
"""
assert isinstance(boxes_list, (list, tuple))
if len(boxes_list) == 0:
return cls(torch.empty(0))
assert all([isinstance(box, Boxes) for box in boxes_list])
# use torch.cat (v.s. layers.cat) so the returned boxes never share storage with input
cat_boxes = cls(torch.cat([b.tensor for b in boxes_list], dim=0))
return cat_boxes
@property
def device(self) -> torch.device:
"""Get the device the box is on.
Returns: the device the box is on
"""
return self.tensor.device
# type "Iterator[torch.Tensor]", yield, and iter() not supported by torchscript
# https://github.com/pytorch/pytorch/issues/18627
@torch.jit.unused
def __iter__(self):
"""Yield a box as a Tensor of shape (4,) at a time."""
yield from self.tensor
device: torch.device
property
¶
Get the device the box is on.
Returns: the device the box is on
__getitem__(item)
¶
Getter for boxes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
item |
Union[int, slice, BoolTensor]
|
int, slice, or a BoolTensor |
required |
Returns:
Name | Type | Description |
---|---|---|
Boxes |
Boxes
|
Create a new :class: |
Usage
The following usage are allowed:
1. new_boxes = boxes[3]
: return a Boxes
which contains only one box.
2. new_boxes = boxes[2:10]
: return a slice of boxes.
3. new_boxes = boxes[vector]
, where vector is a torch.BoolTensor
with length = len(boxes)
. Nonzero elements in the vector will be selected.
NOTE: that the returned Boxes might share storage with this Boxes, subject to Pytorch's indexing semantics.
Source code in dreem/inference/boxes.py
def __getitem__(self, item: Union[int, slice, torch.BoolTensor]) -> "Boxes":
"""Getter for boxes.
Args:
item: int, slice, or a BoolTensor
Returns:
Boxes: Create a new :class:`Boxes` by indexing.
Usage:
The following usage are allowed:
1. `new_boxes = boxes[3]`: return a `Boxes` which contains only one box.
2. `new_boxes = boxes[2:10]`: return a slice of boxes.
3. `new_boxes = boxes[vector]`, where vector is a torch.BoolTensor
with `length = len(boxes)`. Nonzero elements in the vector will be selected.
NOTE: that the returned Boxes might share storage with this Boxes,
subject to Pytorch's indexing semantics.
"""
if isinstance(item, int):
return Boxes(self.tensor[item])
b = self.tensor[item]
assert (
b.dim() == 3
), "Indexing on Boxes with {} failed to return a matrix!".format(item)
return Boxes(b)
__init__(tensor)
¶
Initialize Boxes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
tensor |
Tensor[float]
|
a Nx4 matrix. Each row is (x1, y1, x2, y2). |
required |
Source code in dreem/inference/boxes.py
def __init__(self, tensor: torch.Tensor):
"""Initialize Boxes.
Args:
tensor (Tensor[float]): a Nx4 matrix. Each row is (x1, y1, x2, y2).
"""
if not isinstance(tensor, torch.Tensor):
tensor = torch.as_tensor(
tensor, dtype=torch.float32, device=torch.device("cpu")
)
else:
tensor = tensor.to(torch.float32)
if tensor.numel() == 0:
# Use reshape, so we don't end up creating a new tensor that does not depend on
# the inputs (and consequently confuses jit)
tensor = tensor.reshape((-1, 4)).to(dtype=torch.float32)
assert tensor.dim() == 3 and tensor.size(-1) == 4, tensor.size()
self.tensor = tensor
__iter__()
¶
__len__()
¶
Get the number of boxes stored in this object.
Returns:
Type | Description |
---|---|
int
|
the number of boxes stored in this object |
__repr__()
¶
area()
¶
Compute the area of all the boxes.
Returns:
Type | Description |
---|---|
Tensor
|
torch.Tensor: a vector with areas of each box. |
cat(boxes_list)
classmethod
¶
Concatenates a list of Boxes into a single Boxes.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
boxes_list |
List[Boxes]
|
list of |
required |
Returns:
Name | Type | Description |
---|---|---|
Boxes |
Boxes
|
the concatenated Boxes |
Source code in dreem/inference/boxes.py
@classmethod
def cat(cls, boxes_list: List["Boxes"]) -> "Boxes":
"""Concatenates a list of Boxes into a single Boxes.
Arguments:
boxes_list: list of `Boxes`
Returns:
Boxes: the concatenated Boxes
"""
assert isinstance(boxes_list, (list, tuple))
if len(boxes_list) == 0:
return cls(torch.empty(0))
assert all([isinstance(box, Boxes) for box in boxes_list])
# use torch.cat (v.s. layers.cat) so the returned boxes never share storage with input
cat_boxes = cls(torch.cat([b.tensor for b in boxes_list], dim=0))
return cat_boxes
clip(box_size)
¶
Clip (in place) the boxes.
Limits x coordinates to the range [0, width] and y coordinates to the range [0, height].
Parameters:
Name | Type | Description | Default |
---|---|---|---|
box_size |
(height, width)
|
The clipping box's size. |
required |
Source code in dreem/inference/boxes.py
def clip(self, box_size: Tuple[int, int]) -> None:
"""Clip (in place) the boxes.
Limits x coordinates to the range [0, width]
and y coordinates to the range [0, height].
Args:
box_size (height, width): The clipping box's size.
"""
assert torch.isfinite(self.tensor).all(), "Box tensor contains infinite or NaN!"
h, w = box_size
x1 = self.tensor[:, :, 0].clamp(min=0, max=w)
y1 = self.tensor[:, :, 1].clamp(min=0, max=h)
x2 = self.tensor[:, :, 2].clamp(min=0, max=w)
y2 = self.tensor[:, :, 3].clamp(min=0, max=h)
self.tensor = torch.stack((x1, y1, x2, y2), dim=-1)
get_centers()
¶
Get the centroid of the bbox.
Returns:
Type | Description |
---|---|
Tensor
|
The box centers in a Nx2 array of (x, y). |
inside_box(box_size, boundary_threshold=0)
¶
Check if box is inside reference box.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
box_size |
(height, width)
|
Size of the reference box. |
required |
boundary_threshold |
int
|
Boxes that extend beyond the reference box boundary by more than boundary_threshold are considered "outside". |
0
|
Returns:
Type | Description |
---|---|
Tensor
|
a binary vector, indicating whether each box is inside the reference box. |
Source code in dreem/inference/boxes.py
def inside_box(
self, box_size: Tuple[int, int], boundary_threshold: int = 0
) -> torch.Tensor:
"""Check if box is inside reference box.
Args:
box_size (height, width): Size of the reference box.
boundary_threshold (int): Boxes that extend beyond the reference box
boundary by more than boundary_threshold are considered "outside".
Returns:
a binary vector, indicating whether each box is inside the reference box.
"""
height, width = box_size
inds_inside = (
(self.tensor[..., 0] >= -boundary_threshold)
& (self.tensor[..., 1] >= -boundary_threshold)
& (self.tensor[..., 2] < width + boundary_threshold)
& (self.tensor[..., 3] < height + boundary_threshold)
)
return inds_inside
nonempty(threshold=0.0)
¶
Find boxes that are non-empty.
A box is considered empty, if either of its side is no larger than threshold.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
threshold |
float
|
the smallest a box can be. |
0.0
|
Returns:
Name | Type | Description |
---|---|---|
Tensor |
Tensor
|
|
Source code in dreem/inference/boxes.py
def nonempty(self, threshold: float = 0.0) -> torch.Tensor:
"""Find boxes that are non-empty.
A box is considered empty, if either of its side is no larger than threshold.
Args:
threshold: the smallest a box can be.
Returns:
Tensor:
a binary vector which represents whether each box is empty
(False) or non-empty (True).
"""
box = self.tensor
widths = box[:, :, 2] - box[:, :, 0]
heights = box[:, :, 3] - box[:, :, 1]
keep = (widths > threshold) & (heights > threshold)
return keep
scale(scale_x, scale_y)
¶
to(device)
¶
Load boxes to gpu/cpu.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
device |
device
|
The device to load the boxes to |
required |
Returns: Boxes on device.