camfi.datamodel.geometry module

Defines geometry related classes and methods used throughout camfi. Depends on camfi.util.

class camfi.datamodel.geometry.BoundingBox(*, x0: pydantic.types.NonNegativeInt, y0: pydantic.types.NonNegativeInt, x1: pydantic.types.NonNegativeInt, y1: pydantic.types.NonNegativeInt)

Bases: pydantic.main.BaseModel

Defines a bounding box, and provides various convenience methods for working with boxes on images.

Parameters
  • x0 (NonNegativeInt) – Minimum inclusive (horizontal) x-coordinate of box.

  • y0 (NonNegativeInt) – Minimum inclusive (vertical) y-coordinate of box.

  • x1 (NonNegativeInt) – Maximum exclusive (horizontal) x-coordinate of box.

  • y1 (NonNegativeInt) – Maximum exclusive (vertical) y-coordinate of box.

add_margin(margin: pydantic.types.NonNegativeInt, shape: Optional[tuple] = None) None

Expands self by a fixed margin. Operates in-place.

Parameters
  • margin (PositiveInt) – Margin to add to self.

  • shape (Optional[tuple[PositiveInt, PositiveInt]] = (height, width)) – Shape of image. If set, will constrain self to image shape.

crop_image(image: torch.Tensor) torch.Tensor

Returns a view of an image cropped to the bounding box.

Parameters

image (torch.Tensor) – With shape […, height, width].

Returns

with shape […, self.y1 - self.y0, self.x1 - self.x0], assuming height <= self.y1 - self.y0 and width <= self.x1 - self.x0.

Return type

torch.Tensor

Examples

>>> box = BoundingBox(x0=7, x1=15, y0=3, y1=7)
>>> grey_image = torch.zeros(10, 20)
>>> box.crop_image(grey_image).shape
torch.Size([4, 8])
>>> colour_image = torch.zeros(3, 10, 20)
>>> box.crop_image(colour_image).shape
torch.Size([3, 4, 8])

If BoundingBox goes outside image.shape, then output size will be truncated in the expected way

>>> box = BoundingBox(x0=15, x1=25, y0=3, y1=7)
>>> box.crop_image(grey_image).shape
torch.Size([4, 5])
>>> box = BoundingBox(x0=7, x1=15, y0=7, y1=13)
>>> box.crop_image(grey_image).shape
torch.Size([3, 8])
classmethod from_shape(shape: tuple, border: pydantic.types.NonNegativeInt = 0) camfi.datamodel.geometry.BoundingBox

Creates an instance of BoundingBox from an image shape, useful for defining a region of interest within an image, not too close to the edge.

Parameters
  • shape (tuple[PositiveInt, PositiveInt]) – Shape of image (height, width).

  • border (NonNegativeInt) – Width of border. If 0 (default), then the bounding box will contain the entire image.

Returns

box – Optionally contracted bounding box of image.

Return type

BoundingBox

Examples

>>> BoundingBox.from_shape((10, 15))
BoundingBox(x0=0, y0=0, x1=15, y1=10)
>>> BoundingBox.from_shape((10, 15), border=3)
BoundingBox(x0=3, y0=3, x1=12, y1=7)
get_area() pydantic.types.PositiveInt

Get the area enclosed by self.

Returns

area – Area enclosed by box, in pixels ** 2.

Return type

PositiveInt

Examples

>>> box = BoundingBox(x0=0, y0=1, x1=2, y1=3)
>>> box.get_area()
4
in_box(box: camfi.datamodel.geometry.BoundingBox) bool

Returns True if self is contained in box.

Parameters

box (BoundingBox) – Other box to test against.

Returns

is_in_box – True if self is completely contained within box.

Return type

bool

Examples

>>> box0 = BoundingBox(x0=1, y0=2, x1=3, y1=4)
>>> box1 = BoundingBox(x0=0, y0=1, x1=4, y1=5)
>>> box0.in_box(box1)
True
>>> box1.in_box(box0)
False

A box is always in itself

>>> box0.in_box(box0)
True
intersection(box: camfi.datamodel.geometry.BoundingBox) pydantic.types.NonNegativeInt

Get the intersectional area of two boxes.

Parameters

box (BoundingBox) – Another bounding box to compare to.

Returns

intersectional_area – Area of intersection of two boxes.

Return type

NonNegativeInt

Examples

>>> box0 = BoundingBox(x0=0, y0=0, x1=1, y1=1)
>>> box1 = BoundingBox(x0=2, y0=2, x1=3, y1=3)
>>> box2 = BoundingBox(x0=0, y0=0, x1=2, y1=2)
>>> box3 = BoundingBox(x0=1, y0=1, x1=3, y1=3)
>>> box0.intersection(box1)
0
>>> box2.intersection(box3)
1

Intersection is commutative

>>> from itertools import product
>>> pairs = product([box0, box1, box2, box3], repeat=2)
>>> all(b0.intersection(b1) == b1.intersection(b0) for b0, b1 in pairs)
True
intersection_over_union(box: camfi.datamodel.geometry.BoundingBox) pydantic.types.NonNegativeFloat

Get the intersection over union of two boxes.

Parameters

box (BoundingBox) – Another bounding box to compare to.

Returns

iou – Intersection over Union of two boxes, between 0.0 and 1.0.

Return type

NonNegativeFloat

Examples

>>> box0 = BoundingBox(x0=0, y0=0, x1=1, y1=1)
>>> box1 = BoundingBox(x0=2, y0=2, x1=3, y1=3)
>>> box2 = BoundingBox(x0=0, y0=0, x1=2, y1=2)
>>> box3 = BoundingBox(x0=1, y0=0, x1=4, y1=2)
>>> box0.intersection_over_union(box1)
0.0
>>> box1.intersection_over_union(box2)
0.0
>>> box2.intersection_over_union(box3)
0.25
>>> box0.intersection_over_union(box2)
0.25

Intersection over union is commutative

>>> from itertools import product
>>> pairs = product([box0, box1, box2, box3], repeat=2)
>>> all(
...     b0.intersection_over_union(b1) == b1.intersection_over_union(b0)
...     for b0, b1 in pairs
... )
True
is_portrait() bool

Returns True if bounding box is at least as tall as it is wide.

Returns

portrait – True if bounding box is at least as tall as it is wide.

Return type

bool

Examples

>>> BoundingBox(x0=1, y0=0, x1=11, y1=10).is_portrait()
True
>>> BoundingBox(x0=0, y0=0, x1=11, y1=10).is_portrait()
False
overlaps(box: camfi.datamodel.geometry.BoundingBox) bool

Returns True if two bounding boxes overlap, and False otherwise.

Parameters

box (BoundingBox) – Another bounding box to compare to.

Returns

overlap – True if self and box intersect.

Return type

bool

Examples

>>> box0 = BoundingBox(x0=0, y0=0, x1=1, y1=1)
>>> box1 = BoundingBox(x0=2, y0=2, x1=3, y1=3)
>>> box2 = BoundingBox(x0=0, y0=0, x1=2, y1=2)
>>> box3 = BoundingBox(x0=1, y0=1, x1=3, y1=3)
>>> box0.overlaps(box1)
False
>>> box2.overlaps(box3)
True

Overlaps can happen in either dimension:

>>> box0 = BoundingBox(x0=0, y0=0, x1=2, y1=2)
>>> box1 = BoundingBox(x0=0, y0=1, x1=1, y1=3)
>>> box2 = BoundingBox(x0=1, y0=0, x1=3, y1=1)
>>> box3 = BoundingBox(x0=0, y0=2, x1=2, y1=4)
>>> box4 = BoundingBox(x0=2, y0=0, x1=4, y1=2)
>>> box0.overlaps(box1)
True
>>> box0.overlaps(box2)
True

Overlaps are not inclusive of edges:

>>> box0.overlaps(box3)
False
>>> box0.overlaps(box4)
False
property shape: tuple

Gets the (height, width) of the bounding box.

Returns

shape – Height and width of the bounding box

Return type

tuple[int, int]

Examples

>>> BoundingBox(x0=5, y0=1, x1=15, y1=3).shape
(2, 10)
classmethod x1_gt_x0(v, values)

Pydantic validation method, called when instantiating BoundingBox. Ensures that x1 > x0.

classmethod y1_gt_y0(v, values)

Pydantic validation method, called when instantiating BoundingBox. Ensures that y1 > y0.

class camfi.datamodel.geometry.CircleShapeAttributes(*, name: camfi.datamodel.geometry.ConstrainedStrValue = 'circle', cx: pydantic.types.NonNegativeFloat, cy: pydantic.types.NonNegativeFloat, r: pydantic.types.NonNegativeFloat)

Bases: camfi.datamodel.geometry.ViaShapeAttributes

Defines a circle geometry.

Parameters
  • cx (NonNegativeInt) – x-coordinate of centre of circle.

  • cy (NonNegativeInt) – y-coordinate of centre of circle.

  • name (str) – Name of shape (must be “circle” for CircleShapeAttributes instances).

  • r (NonNegativeFloat) – Radius of circle.

as_point()

Converts a CircleShapeAttributes instance to a PointShapeAttributes instance.

Returns

point – Point at centre of circle.

Return type

PointShapeAttributes

classmethod from_bounding_box(box: camfi.datamodel.geometry.BoundingBox) camfi.datamodel.geometry.CircleShapeAttributes

Takes a BoundingBox and returns a CircleShapeAttributes, which encloses the BoundingBox.

Parameters

box (BoundingBox) – Box to enclose with circle

Returns

circle – Smallest circle enclosing box.

Return type

CircleShapeAttributes

Examples

>>> from pytest import approx
>>> box = BoundingBox(x0=0, y0=0, x1=2, y1=2)
>>> circle = CircleShapeAttributes.from_bounding_box(box)
>>> circle.cx == approx(1.0)
True
>>> circle.cy == approx(1.0)
True
>>> circle.r == approx(sqrt(2))
True
get_bounding_box() camfi.datamodel.geometry.BoundingBox

Finds the bounding box of the point at the centre of the circle.

Note: this does not return the bounding box of the entire circle, just it’s centre.

Returns

box – Bounding box of point at the centre of the circle.

Return type

BoundingBox

Examples

>>> circle = CircleShapeAttributes(cx=10, cy=15, r=10)
>>> circle.get_bounding_box()
BoundingBox(x0=10, y0=15, x1=11, y1=16)
in_box(box: camfi.datamodel.geometry.BoundingBox) bool

Returns True if all points in self are within bounding box.

Parameters

box (BoundingBox) – Box to test against.

Returns

is_in_box – True if centre of circle is contained in box.

Return type

bool

Examples

>>> circle = CircleShapeAttributes(cx=2, cy=13, r=10)
>>> circle.in_box(BoundingBox(x0=2, y0=13, x1=4, y1=15))
True
>>> circle.in_box(BoundingBox(x0=3, y0=13, x1=4, y1=15))
False
>>> circle.in_box(BoundingBox(x0=2, y0=14, x1=4, y1=15))
False
>>> circle.in_box(BoundingBox(x0=1, y0=13, x1=2, y1=15))
False
>>> circle.in_box(BoundingBox(x0=2, y0=12, x1=4, y1=13))
False
snap_to_bounds(bounds: camfi.datamodel.geometry.BoundingBox) camfi.datamodel.geometry.CircleShapeAttributes

Returns a CircleShapeAttributes instance inside bounds, at the closest point to self. If already in bounds, returns self.

Parameters

bounds (BoundingBox) – Bounds to snap to.

Returns

circle – New circle inside bounds.

Return type

CircleShapeAttributes

class camfi.datamodel.geometry.PointShapeAttributes(*, name: camfi.datamodel.geometry.ConstrainedStrValue = 'point', cx: pydantic.types.NonNegativeFloat, cy: pydantic.types.NonNegativeFloat)

Bases: camfi.datamodel.geometry.ViaShapeAttributes

Defines a point geometry.

Parameters
  • cx (NonNegativeFloat) – x-coordinate of point.

  • cy (NonNegativeFloat) – y-coordinate of point.

  • name (str) – Name of shape (must be “point” for PointShapeAttributes instances).

get_bounding_box() camfi.datamodel.geometry.BoundingBox

Finds the bounding box of the point.

Returns

box – Bounding box of point

Return type

BoundingBox

Examples

>>> point = PointShapeAttributes(cx=10, cy=15)
>>> point.get_bounding_box()
BoundingBox(x0=10, y0=15, x1=11, y1=16)
in_box(box: camfi.datamodel.geometry.BoundingBox) bool

Returns True if all points in self are within bounding box.

Parameters

box (BoundingBox) – Box to test against..

Returns

is_in_box – True if point is within box.

Return type

bool

Examples

>>> point = PointShapeAttributes(cx=2, cy=13)
>>> point.in_box(BoundingBox(x0=2, y0=13, x1=4, y1=15))
True
>>> point.in_box(BoundingBox(x0=3, y0=13, x1=4, y1=15))
False
>>> point.in_box(BoundingBox(x0=2, y0=14, x1=4, y1=15))
False
>>> point.in_box(BoundingBox(x0=1, y0=13, x1=2, y1=15))
False
>>> point.in_box(BoundingBox(x0=2, y0=12, x1=4, y1=13))
False
snap_to_bounds(bounds: camfi.datamodel.geometry.BoundingBox) camfi.datamodel.geometry.PointShapeAttributes

Returns a PointShapeAttributes instance inside bounds, at the closest point to self. If already in bounds, returns self.

Parameters

bounds (BoundingBox) – Bounds to snap to.

Returns

point – New point inside bounds.

Return type

PointShapeAttributes

Examples

>>> point = PointShapeAttributes(cx=2, cy=13)
>>> bounds = BoundingBox(x0=0, y0=0, x1=5, y1=7)
>>> point.snap_to_bounds(bounds)
PointShapeAttributes(name='point', cx=2.0, cy=6.0)
>>> bounds = BoundingBox(x0=0, y0=0, x1=5, y1=17)
>>> point.snap_to_bounds(bounds) is point
True
class camfi.datamodel.geometry.PolylineShapeAttributes(*, name: camfi.datamodel.geometry.ConstrainedStrValue = 'polyline', all_points_x: list, all_points_y: list)

Bases: camfi.datamodel.geometry.ViaShapeAttributes

Defines a polyline geometry.

Parameters
  • all_points_x (list[NonNegativeFloat]) – list of the x-coordinates of the points defining the polyline.

  • all_points_y (list[NonNegativeFloat]) – list of the y-coordinates of the points defining the polyline.

  • name (str) – Name of shape (must be “polyline” for PolylineShapeAttributes instances).

as_circle() camfi.datamodel.geometry.CircleShapeAttributes

Calculates the smallest enclosing circle of the polyline.

Returns

smallest_enclosing_circle – Smallest enclosing circle of polyline.

Return type

CircleShapeAttributes

Examples

>>> polyline = PolylineShapeAttributes(all_points_x=[0, 1], all_points_y=[0, 0])
>>> polyline.as_circle()
CircleShapeAttributes(name='circle', cx=0.5, cy=0.0, r=0.5)
extract_region_of_interest(image: torch.Tensor, scan_distance: pydantic.types.PositiveInt) torch.Tensor

Extracts region of interest (ROI) from an image tensor.

Parameters
  • image (torch.Tensor) – Image to extract region of interest from. Should be greyscale (ie. just have two axes)

  • scan_distance (PositiveInt) – Half-width of rois for motion blurs.

Returns

roi – Rotated, cropped, and straightened region of interest.

Return type

torch.Tensor

Examples

>>> image = torch.tensor([
...     [0.0, 0.1, 0.2, 0.3, 0.4],
...     [1.0, 1.1, 1.2, 1.3, 1.4],
...     [2.0, 2.1, 2.2, 2.3, 2.4],
...     [3.0, 3.1, 3.2, 3.3, 3.4],
...     [4.0, 4.1, 4.2, 4.3, 4.4],
... ])
>>> polyline = PolylineShapeAttributes(
...     all_points_x=[1, 4],
...     all_points_y=[2, 2],
... )
>>> polyline.extract_region_of_interest(image, 1)
tensor([[2.1000, 2.2000, 2.3000]])
>>> polyline.extract_region_of_interest(image, 2)
tensor([[1.1000, 1.2000, 1.3000],
        [2.1000, 2.2000, 2.3000],
        [3.1000, 3.2000, 3.3000]])

Also works for multi-segment polylines

>>> polyline = PolylineShapeAttributes(
...     all_points_x=[1, 2, 4],
...     all_points_y=[2, 2, 2],
... )
>>> polyline.extract_region_of_interest(image, 1)
tensor([[2.1000, 2.2000, 2.3000]])
>>> polyline.extract_region_of_interest(image, 2)
tensor([[1.1000, 1.2000, 1.3000],
        [2.1000, 2.2000, 2.3000],
        [3.1000, 3.2000, 3.3000]])

Segments can have different angles to each other

>>> polyline = PolylineShapeAttributes(
...     all_points_x=[1, 3, 3],
...     all_points_y=[2, 2, 0],
... )
>>> polyline.extract_region_of_interest(image, 1)
tensor([[2.1000, 2.2000, 2.3000, 1.3000]])
>>> polyline.extract_region_of_interest(image, 2)
tensor([[1.1000, 1.2000, 2.2000, 1.2000],
        [2.1000, 2.2000, 2.3000, 1.3000],
        [3.1000, 3.2000, 2.4000, 1.4000]])

And segments can be at arbitrary angles. This example starts towards the top- right corner and travels towards the bottom-left.

>>> polyline = PolylineShapeAttributes(
...     all_points_x=[3, 0],
...     all_points_y=[1, 4],
... )
>>> polyline.extract_region_of_interest(image, 2)
tensor([[2.0778, 2.7142, 3.3506, 3.9870],
        [1.3000, 1.9364, 2.5728, 3.2092],
        [0.5222, 1.1586, 1.7950, 2.4314]])

And this one from the top-left, heading down and left

>>> polyline = PolylineShapeAttributes(
...     all_points_x=[1, 4],
...     all_points_y=[1, 4],
... )
>>> polyline.extract_region_of_interest(image, 2)
tensor([[0.4636, 1.2414, 2.0192, 2.7971],
        [1.1000, 1.8778, 2.6556, 3.4335],
        [1.7364, 2.5142, 3.2920, 4.0698]])
get_bounding_box() camfi.datamodel.geometry.BoundingBox

Finds the bounding box of all the points in the polyline.

Returns

box – Bounding box of all the points in the polyline.

Return type

BoundingBox

Examples

>>> polyline = PolylineShapeAttributes(all_points_x=[0, 1], all_points_y=[0, 0])
>>> polyline.get_bounding_box()
BoundingBox(x0=0, y0=0, x1=2, y1=1)
hausdorff_distance(polyline: camfi.datamodel.geometry.PolylineShapeAttributes) pydantic.types.NonNegativeFloat

Returns the Hausdorff distance between two PolylineShapeAttributes instances.

Parameters

polyline (PolylineShapeAttributes) – Other polyline to compare to

Returns

h_dist – Hausdorff distance between self and polyline.

Return type

NonNegativeFloat

Examples

>>> polyline0 = PolylineShapeAttributes(
...     all_points_x=[0, 1],
...     all_points_y=[0, 0],
... )
>>> polyline1 = PolylineShapeAttributes(
...     all_points_x=[0, 1],
...     all_points_y=[1, 1],
... )
>>> polyline0.hausdorff_distance(polyline1)
1.0
in_box(box: camfi.datamodel.geometry.BoundingBox) bool

Returns True if all points in self are within bounding box.

Parameters

box (BoundingBox) – Box to test against.

Returns

is_in_box – True if entire polyline is contained in box.

Return type

bool

Examples

>>> polyline = PolylineShapeAttributes(
...     all_points_x=[1, 3],
...     all_points_y=[15, 13],
... )
>>> polyline.in_box(BoundingBox(x0=1, y0=13, x1=4, y1=16))
True
>>> polyline.in_box(BoundingBox(x0=2, y0=13, x1=4, y1=16))
False
>>> polyline.in_box(BoundingBox(x0=1, y0=14, x1=4, y1=16))
False
>>> polyline.in_box(BoundingBox(x0=1, y0=13, x1=3, y1=16))
False
>>> polyline.in_box(BoundingBox(x0=1, y0=13, x1=4, y1=15))
False
length()

Get the sum of lengths of all the polyline segments.

Returns

length – Length of polyline in pixels.

Return type

float

Examples

>>> from pytest import approx
>>> polyline = PolylineShapeAttributes(
...     all_points_x=[0, 1, 2],
...     all_points_y=[0, 1, 1],
... )
>>> polyline.length() == approx(sqrt(2) + 1)
True
snap_to_bounds(bounds: camfi.datamodel.geometry.BoundingBox) Union[camfi.datamodel.geometry.PolylineShapeAttributes, camfi.datamodel.geometry.CircleShapeAttributes]

If self.in_box(bounds) is True, then returns self. Otherwise, returns a CircleShapeAttributes within bounds.

Parameters

bounds (BoundingBox) – Bounds to snap to.

Returns

shape – New circle inside bounds, or self.

Return type

Union[PolylineShapeAttributes, CircleShapeAttributes]

to_shapely() shapely.geometry.linestring.LineString

Casts self to a shapely.geometry.LineString instance.

Returns

line_string – Shapely representation of a polyline.

Return type

LineString

Examples

>>> polyline = PolylineShapeAttributes(
...     all_points_x=[0, 1, 1],
...     all_points_y=[1, 1, 2],
... )
>>> line_string = polyline.to_shapely()
>>> isinstance(line_string, LineString)
True
>>> print(line_string)
LINESTRING (0 1, 1 1, 1 2)
class camfi.datamodel.geometry.ViaShapeAttributes(*, name: str)

Bases: pydantic.main.BaseModel, abc.ABC

Abstract base class for via region shapes. These define the geometry data of annotations of flying insects in images.

Parameters

name (str) – Name of shape type (e.g. “point”, “circle”, or “polyline”).

abstract get_bounding_box() camfi.datamodel.geometry.BoundingBox

Returns a BoundingBox object which contains the coordinates in self. Note that CircleShapeAttributes are treated like PointShapeAttributes (i.e. r is ignored).

Returns

box – Bounding box of ViaShapeAttributes instance.

Return type

BoundingBox

abstract in_box(box: camfi.datamodel.geometry.BoundingBox) bool

Returns True if all points in self are within bounding box.

Parameters

box (BoundingBox) – Bounding box to test against.

Returns

is_in_box – True if within box.

Return type

bool

intersection_over_union(other: camfi.datamodel.geometry.ViaShapeAttributes) pydantic.types.NonNegativeFloat

Get the intersection over union of bounding boxes.

Parameters

other (ViaShapeAttributes) – Other shape to compare to.

Returns

iou – Intersection over Union of two bounding boxes, between 0.0 and 1.0.

Return type

NonNegativeFloat

Examples

>>> class MockShapeAttributes(ViaShapeAttributes):
...     bounding_box: BoundingBox
...     def get_bounding_box(self) -> BoundingBox:
...         return self.bounding_box
...     def in_box(self, box: BoundingBox) -> bool:
...         return self.bounding_box.in_box(box)
>>> shape_attributes0 = MockShapeAttributes(
...     bounding_box=BoundingBox(x0=0, y0=0, x1=2, y1=1),
...     name="mock_shape",
... )
>>> shape_attributes1 = MockShapeAttributes(
...     bounding_box=BoundingBox(x0=1, y0=0, x1=4, y1=1),
...     name="mock_shape",
... )
>>> shape_attributes0.intersection_over_union(shape_attributes1)
0.25
y_diff() pydantic.types.PositiveInt

Returns the total height (y-dimension) of the annotation (in pixels).

Returns

y_diff – Height of self in pixels.

Return type

PositiveInt