Skip to content

Commit f6feeba

Browse files
authored
Bounding box (#813)
* add Sketch class in forte/data/ontology/top.py * add hash function docstring in Sketch class in forte/data/ontology/top.py * add Sketch class in the __init__ variable in forte/data/ontology/top.py * black * add docstring for 'Sketch' in forte/data/ontology/top.py * add index_key() for 'Sketch' in forte/data/ontology/top.py * black format and add Grids * fix docstring for Sketch in forte/data/ontology/top.py * Sketch -> ImageAnnotation * only keep essential class variables: image_annotations, grids and payloads * remove array data from Grids * minor changes on class variables of Grids * add test cases for grids and image annotation * update docstring in forte/data/ontology/top.py * update grids test * grid_config -> height_n_width and associate Grids to payload * black * correct height_n_width constraint * remove the wrong import * add Grids in __init__ * debugged the parameter issues height_n_width -> height, width two paramters * adjust the test cases accordingly based on code changes * add tests for raise ValueErrror * pylint * pylint: line length * pylint * pylint * remove index_key() and move up super().__init__(pack) * self.height -> self._height and self.width -> self._width * rewrite __eq__() * remove hash function for ImageAnnotation and Grids * keep only one image_payload_idx in Grids and make it required in __init__ * add docstring in get_grid_cell and remove condition for checking image_payload_idx is None * adjust grids test accordingly * update docstring * remove wrong import * Region -> Box -> BoundingBox * example for checking overlapping between bounging boxes * add link between BoundingBox and Text externally * add function compute_iou() for Box and Region * add example code for compute_iou() * remove auto imports and adjust __init__ order * image_annotation and grids: SortedList -> List * add docstrings for BoundingBox related classes * add more definitions for overlapping * update image_payload_idx default values * inline BoundingBox init * inline BoundingBox init * add index definition to more places * add ImageAnnotation to SinglePackEntries * add BoundingBox related ontologies to __init__ * add BoundingBox related ontologies to __init__ * change docstring locations of bounding box related entries * add -> append * SortedList -> List for self.image_annotations and self.grids * add units for indices * fix pylint error
1 parent b8b2e5a commit f6feeba

File tree

5 files changed

+292
-20
lines changed

5 files changed

+292
-20
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import numpy as np
2+
from forte.data.ontology.top import BoundingBox, Link, Annotation
3+
from forte.data.data_pack import DataPack
4+
5+
datapack = DataPack("image")
6+
7+
# line = np.zeros((6, 12))
8+
line = np.zeros((20, 20))
9+
line[2, 2] = 1
10+
line[3, 3] = 1
11+
line[4, 4] = 1
12+
datapack.payloads.append(line)
13+
datapack.payloads.append(line)
14+
# grid config: 3 x 4
15+
# grid cell indices: (0, 0)
16+
bb1 = BoundingBox(datapack, 0, 2, 2, 3, 4, 0, 0)
17+
datapack.image_annotations.append(bb1)
18+
# grid config: 3 x 4
19+
# grid cell indices: (1, 0)
20+
bb2 = BoundingBox(datapack, 0, 2, 2, 3, 4, 1, 0)
21+
datapack.image_annotations.append(bb2)
22+
# grid config: 4 x 4
23+
# grid cell indices: (1, 0)
24+
bb3 = BoundingBox(datapack, 0, 2, 2, 4, 4, 0, 0)
25+
26+
print(bb1.is_overlapped(bb2))
27+
print(bb1.is_overlapped(bb3))
28+
29+
datapack.set_text("bb1, bb2, bb3")
30+
bb1_descrip = Annotation(datapack, 0, 3)
31+
32+
print(bb1_descrip.text)
33+
link1 = Link(datapack, bb1_descrip, bb1)
34+
datapack.add_entry(link1)
35+
print(list(datapack.all_links))
36+
37+
print(bb1.compute_iou(bb3))

forte/data/data_pack.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ def __init__(self, pack_name: Optional[str] = None):
169169

170170
self._data_store: DataStore = DataStore()
171171
self._entry_converter: EntryConverter = EntryConverter()
172-
self.image_annotations: SortedList[ImageAnnotation] = SortedList()
173-
self.grids: SortedList[Grids] = SortedList()
172+
self.image_annotations: List[ImageAnnotation] = []
173+
self.grids: List[Grids] = []
174174
self.payloads: List[np.ndarray] = []
175175

176176
self.__replace_back_operations: ReplaceOperationsType = []

forte/data/ontology/top.py

Lines changed: 246 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
"AudioAnnotation",
4949
"ImageAnnotation",
5050
"Grids",
51+
"Region",
52+
"Box",
53+
"BoundingBox",
5154
]
5255

5356
QueryType = Union[Dict[str, Any], np.ndarray]
@@ -806,8 +809,8 @@ def __init__(self, pack: PackType, image_payload_idx: int = 0):
806809
Args:
807810
pack: The container that this image annotation
808811
will be added to.
809-
image_payload_idx: A integer that represents the index of
810-
the image in the payloads.
812+
image_payload_idx: the index of the image payload. If it's not set,
813+
it defaults to 0 which means it will load the first image payload.
811814
"""
812815
self._image_payload_idx = image_payload_idx
813816
super().__init__(pack)
@@ -825,6 +828,14 @@ def image(self):
825828
)
826829
return self.pack.get_image_array(self._image_payload_idx)
827830

831+
@property
832+
def max_x(self):
833+
return self.image.shape[1] - 1
834+
835+
@property
836+
def max_y(self):
837+
return self.image.shape[0] - 1
838+
828839
def __eq__(self, other):
829840
if other is None:
830841
return False
@@ -837,17 +848,18 @@ class Grids(Entry):
837848
838849
Args:
839850
pack: The container that this grids will be added to.
840-
height: the number of grid cell per column.
841-
width: the number of grid cell per row.
842-
image_payload_idx: the index of image in the datapack payloads.
851+
height: the number of grid cell per column, the unit is one grid cell.
852+
width: the number of grid cell per row, the unit is one grid cell.
853+
image_payload_idx: the index of the image payload. If it's not set,
854+
it defaults to 0 which meaning it will load the first image payload.
843855
"""
844856

845857
def __init__(
846858
self,
847859
pack: PackType,
848860
height: int,
849861
width: int,
850-
image_payload_idx: Optional[int] = None,
862+
image_payload_idx: int = 0,
851863
):
852864
if height <= 0 or width <= 0:
853865
raise ValueError(
@@ -856,10 +868,7 @@ def __init__(
856868
)
857869
self._height = height
858870
self._width = width
859-
if image_payload_idx is None:
860-
self._image_payload_idx = 0
861-
else:
862-
self._image_payload_idx = image_payload_idx
871+
self._image_payload_idx = image_payload_idx
863872
super().__init__(pack)
864873
self.img_arr = self.pack.get_image_array(self._image_payload_idx)
865874
self.c_h, self.c_w = (
@@ -875,12 +884,14 @@ def get_grid_cell(self, h_idx: int, w_idx: int):
875884
within the grid cell will masked as zeros. The array entries that are
876885
within the grid cell will be copied to the zeros numpy array.
877886
887+
Note: all indices are zero-based and counted from top left corner of
888+
the image.
878889
879890
Args:
880-
h_idx: the zero-based index of the grid cell of the first
881-
dimension.
882-
w_idx: the zero-based index of the grid cell of the second
883-
dimension.
891+
h_idx: the zero-based height(row) index of the grid cell in the
892+
grid, the unit is one grid cell.
893+
w_idx: the zero-based width(column) index of the grid cell in the
894+
grid, the unit is one grid cell.
884895
885896
Raises:
886897
ValueError: ``h_idx`` is out of the range specified by ``height``.
@@ -918,6 +929,27 @@ def get_grid_cell(self, h_idx: int, w_idx: int):
918929
]
919930
return array
920931

932+
def get_grid_cell_center(self, h_idx: int, w_idx: int) -> Tuple[int, int]:
933+
"""
934+
Get the center position of the grid cell in the ``Grids``.
935+
936+
Note: all indices are zero-based and counted from top left corner of
937+
the grid.
938+
939+
Args:
940+
h_idx: the height(row) index of the grid cell in the grid,
941+
, the unit is one image array entry.
942+
w_idx (int): the width(column) index of the grid cell in the
943+
grid, the unit is one image array entry.
944+
945+
Returns:
946+
A tuple of (y index, x index)
947+
"""
948+
return (
949+
(h_idx * self.c_h + (h_idx + 1) * self.c_h) // 2,
950+
(w_idx * self.c_w + (w_idx + 1) * self.c_w) // 2,
951+
)
952+
921953
@property
922954
def image_payload_idx(self) -> int:
923955
return self._image_payload_idx
@@ -944,5 +976,204 @@ def __eq__(self, other):
944976
)
945977

946978

947-
SinglePackEntries = (Link, Group, Annotation, Generics, AudioAnnotation)
979+
class Region(ImageAnnotation):
980+
"""
981+
A region class associated with an image payload.
982+
983+
Args:
984+
pack: the container that this ``Region`` will be added to.
985+
image_payload_idx: the index of the image payload. If it's not set,
986+
it defaults to 0 which meaning it will load the first image payload.
987+
"""
988+
989+
def __init__(self, pack: PackType, image_payload_idx: int = 0):
990+
super().__init__(pack, image_payload_idx)
991+
if image_payload_idx is None:
992+
self._image_payload_idx = 0
993+
else:
994+
self._image_payload_idx = image_payload_idx
995+
996+
def compute_iou(self, other) -> int:
997+
intersection = np.sum(np.logical_and(self.image, other.image))
998+
union = np.sum(np.logical_or(self.image, other.image))
999+
return intersection / union
1000+
1001+
1002+
class Box(Region):
1003+
"""
1004+
A box class with a center position and a box configuration.
1005+
1006+
Note: all indices are zero-based and counted from top left corner of
1007+
image.
1008+
1009+
Args:
1010+
pack: the container that this ``Box`` will be added to.
1011+
image_payload_idx: the index of the image payload. If it's not set,
1012+
it defaults to 0 which meaning it will load the first image payload.
1013+
cy: the row index of the box center in the image array,
1014+
the unit is one image array entry.
1015+
cx: the column index of the box center in the image array,
1016+
the unit is one image array entry.
1017+
height: the height of the box, the unit is one image array entry.
1018+
width: the width of the box, the unit is one image array entry.
1019+
"""
1020+
1021+
def __init__(
1022+
self,
1023+
pack: PackType,
1024+
cy: int,
1025+
cx: int,
1026+
height: int,
1027+
width: int,
1028+
image_payload_idx: int = 0,
1029+
):
1030+
# assume Box is associated with Grids
1031+
super().__init__(pack, image_payload_idx)
1032+
# center location
1033+
self._cy = cy
1034+
self._cx = cx
1035+
self._height = height
1036+
self._width = width
1037+
1038+
@property
1039+
def center(self):
1040+
return (self._cy, self._cx)
1041+
1042+
@property
1043+
def corners(self):
1044+
"""
1045+
Get corners of box.
1046+
"""
1047+
return [
1048+
(self._cy + h_offset, self._cx + w_offset)
1049+
for h_offset in [-0.5 * self._height, 0.5 * self._height]
1050+
for w_offset in [-0.5 * self._width, 0.5 * self._width]
1051+
]
1052+
1053+
@property
1054+
def box_min_x(self):
1055+
return max(self._cx - round(0.5 * self._width), 0)
1056+
1057+
@property
1058+
def box_max_x(self):
1059+
return min(self._cx + round(0.5 * self._width), self.max_x)
1060+
1061+
@property
1062+
def box_min_y(self):
1063+
return max(self._cy - round(0.5 * self._height), 0)
1064+
1065+
@property
1066+
def box_max_y(self):
1067+
return min(self._cy + round(0.5 * self._height), self.max_y)
1068+
1069+
@property
1070+
def area(self):
1071+
return self._height * self._width
1072+
1073+
def is_overlapped(self, other):
1074+
"""
1075+
A function checks whether two boxes are overlapped(two box area have
1076+
intersections).
1077+
1078+
Note: in edges cases where two bounding boxes' boundaries share the
1079+
same line segment/corner in the image array, it won't be considered
1080+
overlapped.
1081+
1082+
Args:
1083+
other: the other ``Box`` object to compared to.
1084+
1085+
Returns:
1086+
A boolean value indicating whether there is overlapped.
1087+
"""
1088+
# If one box is on left side of other
1089+
if self.box_min_x > other.box_max_x or other.box_min_x > self.box_max_x:
1090+
return False
1091+
1092+
# If one box is above other
1093+
if self.box_min_y > other.box_max_y or other.box_min_y > self.box_max_y:
1094+
return False
1095+
return True
1096+
1097+
def compute_iou(self, other):
1098+
"""
1099+
A function computes iou(intersection over union) between two boxes.
1100+
1101+
Args:
1102+
other: the other ``Box`` object to compared to.
1103+
1104+
Returns:
1105+
A float value which is (intersection area/ union area) between two
1106+
boxes.
1107+
"""
1108+
if not self.is_overlapped(other):
1109+
return 0
1110+
box_x_diff = min(
1111+
abs(other.box_max_x - self.box_min_x),
1112+
abs(other.box_min_x - self.box_max_x),
1113+
)
1114+
box_y_diff = min(
1115+
abs(other.box_max_y - self.box_min_y),
1116+
abs(other.box_min_y - self.box_max_y),
1117+
)
1118+
intersection = box_x_diff * box_y_diff
1119+
union = self.area + other.area - intersection
1120+
return intersection / union
1121+
1122+
1123+
class BoundingBox(Box):
1124+
"""
1125+
A bounding box class that associates with image payload and grids and
1126+
has a configuration of height and width.
1127+
1128+
Note: all indices are zero-based and counted from top left corner of
1129+
the image/grid.
1130+
1131+
Args:
1132+
pack: The container that this BoundingBox will
1133+
be added to.
1134+
image_payload_idx: the index of the image payload. If it's not set,
1135+
it defaults to 0 which means it will load the first image payload.
1136+
height: the height of the bounding box, the unit is one image array
1137+
entry.
1138+
width: the width of the bounding box, the unit is one image array entry.
1139+
grid_height: the height of the associated grid, the unit is one grid
1140+
cell.
1141+
grid_width: the width of the associated grid, the unit is one grid
1142+
cell.
1143+
grid_cell_h_idx: the height index of the associated grid cell in
1144+
the grid, the unit is one grid cell.
1145+
grid_cell_w_idx: the width index of the associated grid cell in
1146+
the grid, the unit is one grid cell.
1147+
1148+
"""
1149+
1150+
def __init__(
1151+
self,
1152+
pack: PackType,
1153+
height: int,
1154+
width: int,
1155+
grid_height: int,
1156+
grid_width: int,
1157+
grid_cell_h_idx: int,
1158+
grid_cell_w_idx: int,
1159+
image_payload_idx: int = 0,
1160+
):
1161+
self.grids = Grids(pack, grid_height, grid_width, image_payload_idx)
1162+
super().__init__(
1163+
pack,
1164+
*self.grids.get_grid_cell_center(grid_cell_h_idx, grid_cell_w_idx),
1165+
height,
1166+
width,
1167+
image_payload_idx,
1168+
)
1169+
1170+
1171+
SinglePackEntries = (
1172+
Link,
1173+
Group,
1174+
Annotation,
1175+
Generics,
1176+
AudioAnnotation,
1177+
ImageAnnotation,
1178+
)
9481179
MultiPackEntries = (MultiPackLink, MultiPackGroup, MultiPackGeneric)

tests/forte/grids_test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ def setUp(self):
3535
line[3, 3] = 1
3636
line[4, 4] = 1
3737
self.datapack.payloads.append(line)
38-
self.datapack.image_annotations.add(ImageAnnotation(self.datapack, 0))
38+
self.datapack.image_annotations.append(
39+
ImageAnnotation(self.datapack, 0)
40+
)
3941

4042
grids = Grids(self.datapack, 3, 4)
4143

42-
self.datapack.grids.add(grids)
44+
self.datapack.grids.append(grids)
4345
self.zeros = np.zeros((6, 12))
4446
self.ref_arr = np.zeros((6, 12))
4547
self.ref_arr[2, 2] = 1

tests/forte/image_annotation_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ def setUp(self):
3636
self.line[3, 3] = 1
3737
self.line[4, 4] = 1
3838
self.datapack.payloads.append(self.line)
39-
self.datapack.image_annotations.add(ImageAnnotation(self.datapack, 0))
39+
self.datapack.image_annotations.append(
40+
ImageAnnotation(self.datapack, 0)
41+
)
4042

4143
def test_image_annotation(self):
4244
self.assertEqual(

0 commit comments

Comments
 (0)