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.BaseModelDefines 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
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.ViaShapeAttributesDefines 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
- 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
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
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
- class camfi.datamodel.geometry.PointShapeAttributes(*, name: camfi.datamodel.geometry.ConstrainedStrValue = 'point', cx: pydantic.types.NonNegativeFloat, cy: pydantic.types.NonNegativeFloat)¶
Bases:
camfi.datamodel.geometry.ViaShapeAttributesDefines 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
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
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.ViaShapeAttributesDefines 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
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
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
- 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.ABCAbstract 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
- 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