Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ontology Box init by top left point and bottom right point #879

Merged
merged 17 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/notebook_tutorial/tutorial_MT_with_forte.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@
"* `AudioAnnotation` is inherited by all audio entries which usually has an audio span to retrieve partial audio from the full audio.\n",
" * `Recording` is an example subclass of `AudioAnnotation`, and it has extra `recording_class` field denoting the classes the audio belongs to.\n",
"* `ImageAnnotation` is inherited by all image entries which usually has payload index pointing to a loaded image array.\n",
" * `BoundingBox` is an example subclass of `ImageAnnotation`. As the picture shows, it has more inheritance relationships than other ontology classes due to the nature of CV objects. The advantage of forte ontology is that it supports complex inheritance, and users can inherit from existing ontology and add new ontology features for their needs.\n",
" * `Box` is an example subclass of `ImageAnnotation`. As the picture shows, it has more inheritance relationships than other ontology classes due to the nature of CV objects. The advantage of forte ontology is that it supports complex inheritance, and users can inherit from existing ontology and add new ontology features for their needs.\n",
"* `Link` is inherited by all link-like entries which has parent and child.\n",
" * `RelationLink` is an example subclass of `Link`, and it has a class attribute specifying the relation type. "
]
Expand Down
8 changes: 4 additions & 4 deletions examples/bounding_box/check_overlap.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
from forte.data.ontology.top import BoundingBox, Link, Annotation
from forte.data.ontology.top import Box, Link, Annotation
from forte.data.data_pack import DataPack

datapack = DataPack("image")
Expand All @@ -13,15 +13,15 @@
datapack.payloads.append(line)
# grid config: 3 x 4
# grid cell indices: (0, 0)
bb1 = BoundingBox(datapack, 0, 2, 2, 3, 4, 0, 0)
bb1 = Box(datapack, 0, 2, 2, 3, 4, 0, 0)
datapack.image_annotations.append(bb1)
# grid config: 3 x 4
# grid cell indices: (1, 0)
bb2 = BoundingBox(datapack, 0, 2, 2, 3, 4, 1, 0)
bb2 = Box(datapack, 0, 2, 2, 3, 4, 1, 0)
datapack.image_annotations.append(bb2)
# grid config: 4 x 4
# grid cell indices: (1, 0)
bb3 = BoundingBox(datapack, 0, 2, 2, 4, 4, 0, 0)
bb3 = Box(datapack, 0, 2, 2, 4, 4, 0, 0)

print(bb1.is_overlapped(bb2))
print(bb1.is_overlapped(bb3))
Expand Down
209 changes: 112 additions & 97 deletions forte/data/ontology/top.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"Grids",
"Region",
"Box",
"BoundingBox",
"Payload",
]

Expand Down Expand Up @@ -853,7 +852,7 @@ def get(
class ImageAnnotation(Entry):
def __init__(self, pack: PackType, image_payload_idx: int = 0):
"""
ImageAnnotation type entries, such as "edge" and "bounding box".
ImageAnnotation type entries, such as "edge" and "box".
Each ImageAnnotation has a ``image_payload_idx`` corresponding to its
image representation in the payload array.

Expand Down Expand Up @@ -1052,7 +1051,7 @@ def __init__(self, pack: PackType, image_payload_idx: int = 0):
else:
self._image_payload_idx = image_payload_idx

def compute_iou(self, other) -> int:
def compute_iou(self, other) -> float:
intersection = np.sum(np.logical_and(self.image, other.image))
union = np.sum(np.logical_or(self.image, other.image))
return intersection / union
Expand All @@ -1067,33 +1066,122 @@ class Box(Region):

Args:
pack: the container that this ``Box`` will be added to.
tl_point: the indices of top left point of the box
[row index, column index], the unit is one pixel.
br_point: the indices of bottom right point of the box
[row index, column index], the unit is one pixel.
image_payload_idx: the index of the image payload in the DataPack's
image payload list. If it's not set,
it defaults to 0 which meaning it will load the first image payload.
cy: the row index of the box center in the image array,
the unit is one image array entry.
cx: the column index of the box center in the image array,
the unit is one image array entry.
height: the height of the box, the unit is one image array entry.
width: the width of the box, the unit is one image array entry.
"""

def __init__(
self,
pack: PackType,
tl_point: List[int],
br_point: List[int],
image_payload_idx: int = 0,
):
super().__init__(pack, image_payload_idx)
if tl_point[0] < 0 or tl_point[1] < 0:
raise ValueError(
f"input parameter top left point indices ({tl_point}) must"
"be non-negative"
)
if br_point[0] < 0 or br_point[1] < 0:
raise ValueError(
f"input parameter bottom right point indices ({br_point}) must"
"be non-negative"
)
if tl_point[0] >= br_point[0]:
raise ValueError(
f"top left point y coordinate({tl_point[0]}) must be less than"
f" bottom right y coordinate({br_point[0]})"
)
if tl_point[1] >= br_point[1]:
raise ValueError(
f"top left point x coordinate({tl_point[1]}) must be less than"
f" bottom right x coordinate({br_point[1]})"
)

self._y0, self._x0 = tl_point
self._y1, self._x1 = br_point
self._cy = round((self._y0 + self._y1) / 2)
self._cx = round((self._x0 + self._x1) / 2)
self._height = self._y1 - self._y0
self._width = self._x1 - self._x0

@classmethod
def init_from_center_n_shape(
cls,
pack: PackType,
cy: int,
cx: int,
height: int,
width: int,
image_payload_idx: int = 0,
):
# assume Box is associated with Grids
super().__init__(pack, image_payload_idx)
"""
A class method to initialize a ``Box`` from a box's center position and
shape.

Note: all indices are zero-based and counted from top left corner of
image.

Args:
pack: the container that this ``Box`` will be added to.
cy: the row coordinate of the box's center, the unit is one pixel.
cx: the column coordinate of the box's center, the unit is one pixel.
height: the height of the box, the unit is one pixel.
width: the width of the box, the unit is one pixel.
image_payload_idx: the index of the image payload in the DataPack's
image payload list. If it's not set, it defaults to 0 which
meaning it will load the first image payload.

Returns:
A ``Box`` instance.
"""
# center location
self._cy = cy
self._cx = cx
self._height = height
self._width = width
return cls(
pack,
[cy - round(height / 2), cx - round(width / 2)],
[cy - round(height / 2) + height, cx - round(width / 2) + width],
image_payload_idx,
)

def compute_iou(self, other) -> float:
"""
A function computes iou(intersection over union) between two boxes
(unit: pixel).
It overwrites the ``compute_iou`` function in it's parent class
``Region``.

Args:
other: the other ``Box`` object to be computed with.
Returns:
A float value which is (intersection area/ union area) between two
boxes.
"""
if not isinstance(other, Box):
raise ValueError(
"The other object to compute iou with is"
" not a Box object."
"You need to check the type of the other object."
)

if not self.is_overlapped(other):
return 0
box_x_diff = min(
abs(other.box_max_x - self.box_min_x),
abs(other.box_min_x - self.box_max_x),
)
box_y_diff = min(
abs(other.box_max_y - self.box_min_y),
abs(other.box_min_y - self.box_max_y),
)
intersection = box_x_diff * box_y_diff
union = self.area + other.area - intersection
return intersection / union

@property
def center(self):
Expand All @@ -1105,26 +1193,27 @@ def corners(self):
Get corners of box.
"""
return [
(self._cy + h_offset, self._cx + w_offset)
for h_offset in [-0.5 * self._height, 0.5 * self._height]
for w_offset in [-0.5 * self._width, 0.5 * self._width]
(self._y0, self._x0),
(self._y0, self._x1),
(self._y1, self._x0),
(self._y1, self._x1),
]

@property
mylibrar marked this conversation as resolved.
Show resolved Hide resolved
def box_min_x(self):
return max(self._cx - round(0.5 * self._width), 0)
return self._x0

@property
def box_max_x(self):
return min(self._cx + round(0.5 * self._width), self.max_x)
return min(self._x1, self.max_x)

@property
def box_min_y(self):
return max(self._cy - round(0.5 * self._height), 0)
return self._y0

@property
def box_max_y(self):
return min(self._cy + round(0.5 * self._height), self.max_y)
return min(self._y1, self.max_y)

@property
def area(self):
Expand All @@ -1135,7 +1224,7 @@ def is_overlapped(self, other):
A function checks whether two boxes are overlapped(two box area have
intersections).

Note: in edges cases where two bounding boxes' boundaries share the
Note: in edges cases where two boxes' boundaries share the
same line segment/corner in the image array, it won't be considered
overlapped.

Expand All @@ -1154,80 +1243,6 @@ def is_overlapped(self, other):
return False
return True

def compute_iou(self, other):
"""
A function computes iou(intersection over union) between two boxes.

Args:
other: the other ``Box`` object to compared to.

Returns:
A float value which is (intersection area/ union area) between two
boxes.
"""
if not self.is_overlapped(other):
return 0
box_x_diff = min(
abs(other.box_max_x - self.box_min_x),
abs(other.box_min_x - self.box_max_x),
)
box_y_diff = min(
abs(other.box_max_y - self.box_min_y),
abs(other.box_min_y - self.box_max_y),
)
intersection = box_x_diff * box_y_diff
union = self.area + other.area - intersection
return intersection / union


class BoundingBox(Box):
"""
A bounding box class that associates with image payload and grids and
has a configuration of height and width.

Note: all indices are zero-based and counted from top left corner of
the image/grid.

Args:
pack: The container that this BoundingBox will
be added to.
image_payload_idx: the index of the image payload in the DataPack's
image payload list. If it's not set,
it defaults to 0 which means it will load the first image payload.
height: the height of the bounding box, the unit is one image array
entry.
width: the width of the bounding box, the unit is one image array entry.
grid_height: the height of the associated grid, the unit is one grid
cell.
grid_width: the width of the associated grid, the unit is one grid
cell.
grid_cell_h_idx: the height index of the associated grid cell in
the grid, the unit is one grid cell.
grid_cell_w_idx: the width index of the associated grid cell in
the grid, the unit is one grid cell.

"""

def __init__(
self,
pack: PackType,
height: int,
width: int,
grid_height: int,
grid_width: int,
grid_cell_h_idx: int,
grid_cell_w_idx: int,
image_payload_idx: int = 0,
):
self.grids = Grids(pack, grid_height, grid_width, image_payload_idx)
super().__init__(
pack,
*self.grids.get_grid_cell_center(grid_cell_h_idx, grid_cell_w_idx),
height,
width,
image_payload_idx,
)


class Payload(Entry):
"""
Expand Down
42 changes: 40 additions & 2 deletions tests/forte/image_annotation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import numpy as np

from numpy import array_equal
from forte.data.ontology.top import ImageAnnotation
from forte.data.ontology.top import Box, ImageAnnotation

from ft.onto.base_ontology import ImagePayload

Expand All @@ -29,7 +29,7 @@

class ImageAnnotationTest(unittest.TestCase):
"""
Test ImageAnnotation related ontologies like Edge and BoundingBox.
Test ImageAnnotation related ontologies like Edge and Box.
"""

def setUp(self):
Expand All @@ -56,3 +56,41 @@ def test_image_annotation(self):
self.assertEqual(
new_pack.audio_annotations, self.datapack.audio_annotations
)

def testBox(self):

b1 = Box(self.datapack, [2, 2], [7, 7], 0)
self.assertEqual(b1.corners, [(2, 2), (2, 7), (7, 2), (7, 7)])
self.assertEqual(b1.area, 25)
self.assertEqual(b1.center, (4, 4))

b2 = Box.init_from_center_n_shape(self.datapack, 4, 4, 5, 5)
self.assertEqual(b2.corners, [(2, 2), (2, 7), (7, 2), (7, 7)])
self.assertEqual(b2.area, 25)
self.assertEqual(b2.center, (4, 4))

def wrong_box():
# negative x coordinate
Box(self.datapack, [-2, 2], [7, 7], 0)

self.assertRaises(ValueError, wrong_box)

def wrong_box():
# negative y coordinate
Box(self.datapack, [2, -2], [7, 7], 0)

self.assertRaises(ValueError, wrong_box)

def wrong_box():
# negative width
Box(self.datapack, [3, 2], [2, 7], 0)

self.assertRaises(ValueError, wrong_box)

def wrong_box():
# negative height
Box(self.datapack, [2, 2], [2, 1], 0)

self.assertRaises(ValueError, wrong_box)

b3 = Box.init_from_center_n_shape(self.datapack, 4, 4, 5, 5)