Skip to content

Commit 3e8ba39

Browse files
josselinbuilsJosselin BUILS
and
Josselin BUILS
authored
test(LAB-2558): add negative polygon in COCO format and fix/add tests (#1739)
Co-authored-by: Josselin BUILS <[email protected]>
1 parent 669277e commit 3e8ba39

File tree

3 files changed

+206
-16
lines changed

3 files changed

+206
-16
lines changed

src/kili/services/export/format/coco/__init__.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,10 @@ def _get_coco_image_annotations(
417417
print("continue")
418418
continue
419419
bounding_poly = annotation["boundingPoly"]
420-
area, bbox, poly = _get_coco_geometry_from_kili_bpoly(
420+
area, bbox, polygons = _get_coco_geometry_from_kili_bpoly(
421421
bounding_poly, coco_image["width"], coco_image["height"]
422422
)
423-
if len(poly) < 6: # twice the number of vertices
423+
if len(polygons[0]) < 6: # twice the number of vertices
424424
print("A polygon must contain more than 2 points. Skipping this polygon...")
425425
continue
426426
if bbox[2] == 0 and bbox[3] == 0:
@@ -436,7 +436,7 @@ def _get_coco_image_annotations(
436436
# Objects have only one connected part.
437437
# But a type of object can appear several times on the same image.
438438
# The limitation of the single connected part comes from Kili.
439-
segmentation=[poly],
439+
segmentation=polygons,
440440
area=area,
441441
iscrowd=0,
442442
)
@@ -478,17 +478,20 @@ def _get_coco_geometry_from_kili_bpoly(
478478
x_max, y_max = max(p_x), max(p_y)
479479
bbox_width, bbox_height = x_max - x_min, y_max - y_min
480480
area = _get_shoelace_area(p_x, p_y)
481+
polygons = [[p for vertice in poly_vertices for p in vertice]]
481482

482483
# Compute and remove negative area
483484
if len(bounding_poly) > 1:
484-
negative_normalized_vertices = bounding_poly[1]["normalizedVertices"]
485-
np_x = [float(vertice["x"]) * asset_width for vertice in negative_normalized_vertices]
486-
np_y = [float(vertice["y"]) * asset_height for vertice in negative_normalized_vertices]
487-
area -= _get_shoelace_area(np_x, np_y)
485+
for negative_bounding_poly in bounding_poly[1:]:
486+
negative_normalized_vertices = negative_bounding_poly["normalizedVertices"]
487+
np_x = [float(vertice["x"]) * asset_width for vertice in negative_normalized_vertices]
488+
np_y = [float(vertice["y"]) * asset_height for vertice in negative_normalized_vertices]
489+
area -= _get_shoelace_area(np_x, np_y)
490+
poly_negative_vertices = [(float(x), float(y)) for x, y in zip(np_x, np_y)]
491+
polygons.append([p for vertice in poly_negative_vertices for p in vertice])
488492

489493
bbox = [int(x_min), int(y_min), int(bbox_width), int(bbox_height)]
490-
poly = [p for vertice in poly_vertices for p in vertice]
491-
return area, bbox, poly
494+
return area, bbox, polygons
492495

493496

494497
def _get_coco_categories(cat_kili_id_to_coco_id, merged) -> List[CocoCategory]:

tests/unit/services/export/helpers/coco.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import math
22
from pathlib import Path
3-
from typing import Dict, List, Optional
3+
from typing import Dict, List, Optional, Union
44

55
from kili.services.types import Job
66

77

88
def get_asset(
99
content_path: Path,
1010
with_annotation: Optional[List[Dict]],
11+
negative_polygons: Union[List[List[Dict]], None] = None,
1112
) -> Dict:
1213
# without annotation means that: there is a label for the asset
1314
# but there is no labeling data for the job.
1415
# `annotations=[]` should not exist.
1516
json_response = {"author": {"firstname": "Jean-Pierre", "lastname": "Dupont"}}
17+
1618
if with_annotation:
1719
json_response = {
1820
**json_response,
@@ -30,6 +32,10 @@ def get_asset(
3032
]
3133
},
3234
}
35+
if negative_polygons:
36+
json_response["JOB_0"]["annotations"][0]["boundingPoly"] += map(
37+
lambda negative_polygon: {"normalizedVertices": negative_polygon}, negative_polygons
38+
)
3339

3440
return {
3541
"latestLabel": {"jsonResponse": json_response},

tests/unit/services/export/test_coco.py

+187-6
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ def test__get_coco_image_annotations():
8989
assert coco_annotation["annotations"][0]["segmentation"] == [
9090
[0.0, 0.0, 960.0, 0.0, 0.0, 540.0]
9191
]
92-
assert coco_annotation["annotations"][0]["area"] == 2073600
92+
# Area of a triangle: base * height / 2
93+
assert coco_annotation["annotations"][0]["area"] == 960.0 * 540.0 / 2
9394

9495
good_date = True
9596
try:
@@ -101,6 +102,182 @@ def test__get_coco_image_annotations():
101102
)
102103

103104

105+
def test__get_coco_image_annotation_area_with_self_intersecting_polygon():
106+
with TemporaryDirectory() as tmp_dir:
107+
job_name = "JOB_0"
108+
output_file = Path(tmp_dir) / job_name / "labels.json"
109+
local_file_path = tmp_dir / Path("image1.jpg")
110+
image_width = 1920
111+
image_height = 1080
112+
Image.new("RGB", (image_width, image_height)).save(local_file_path)
113+
_, paths = _convert_kili_semantic_to_coco(
114+
jobs={
115+
JobName(job_name): {
116+
"mlTask": "OBJECT_DETECTION",
117+
"content": {
118+
"categories": {
119+
"OBJECT_A": {"name": "Object A"},
120+
"OBJECT_B": {"name": "Object B"},
121+
}
122+
},
123+
"instruction": "",
124+
"isChild": False,
125+
"isNew": False,
126+
"isVisible": True,
127+
"models": {},
128+
"required": True,
129+
"tools": ["semantic"],
130+
}
131+
},
132+
assets=[
133+
helpers.get_asset(
134+
local_file_path,
135+
with_annotation=[
136+
{
137+
"x": 0.0,
138+
"y": 0.0,
139+
},
140+
{
141+
"x": 0.5,
142+
"y": 0.0,
143+
},
144+
{
145+
"x": 0.0,
146+
"y": 0.5,
147+
},
148+
{
149+
"x": 0.5,
150+
"y": 0.5,
151+
},
152+
{
153+
"x": 0.0,
154+
"y": 0.0,
155+
},
156+
],
157+
)
158+
],
159+
output_dir=Path(tmp_dir),
160+
title="Test project",
161+
project_input_type="IMAGE",
162+
annotation_modifier=lambda x, _, _1: x,
163+
merged=False,
164+
)
165+
166+
assert paths[0] == output_file
167+
with output_file.open("r", encoding="utf-8") as f:
168+
coco_annotation = json.loads(f.read())
169+
170+
assert coco_annotation["annotations"][0]["bbox"] == [0, 0, 960, 540]
171+
assert coco_annotation["annotations"][0]["segmentation"] == [
172+
[0.0, 0.0, 960.0, 0.0, 0.0, 540.0, 960.0, 540.0, 0.0, 0.0]
173+
]
174+
# Here we have a self-intersecting polygon with 2 opposites triangles, so the area is
175+
# the sum of the areas of the 2 triangles.
176+
# Area of a triangle: base * height / 2
177+
assert coco_annotation["annotations"][0]["area"] == (960.0 * 270.0 / 2) * 2
178+
179+
180+
def test__get_coco_image_annotation_area_with_negative_polygons():
181+
with TemporaryDirectory() as tmp_dir:
182+
job_name = "JOB_0"
183+
output_file = Path(tmp_dir) / job_name / "labels.json"
184+
local_file_path = tmp_dir / Path("image1.jpg")
185+
image_width = 1920
186+
image_height = 1080
187+
Image.new("RGB", (image_width, image_height)).save(local_file_path)
188+
_, paths = _convert_kili_semantic_to_coco(
189+
jobs={
190+
JobName(job_name): {
191+
"mlTask": "OBJECT_DETECTION",
192+
"content": {
193+
"categories": {
194+
"OBJECT_A": {"name": "Object A"},
195+
"OBJECT_B": {"name": "Object B"},
196+
}
197+
},
198+
"instruction": "",
199+
"isChild": False,
200+
"isNew": False,
201+
"isVisible": True,
202+
"models": {},
203+
"required": True,
204+
"tools": ["semantic"],
205+
}
206+
},
207+
assets=[
208+
helpers.get_asset(
209+
local_file_path,
210+
with_annotation=[
211+
{
212+
"x": 0.0,
213+
"y": 0.0,
214+
},
215+
{
216+
"x": 0.5,
217+
"y": 0.0,
218+
},
219+
{
220+
"x": 0.0,
221+
"y": 0.5,
222+
},
223+
],
224+
negative_polygons=[
225+
[
226+
{
227+
"x": 0.1,
228+
"y": 0.1,
229+
},
230+
{
231+
"x": 0.4,
232+
"y": 0.1,
233+
},
234+
{
235+
"x": 0.1,
236+
"y": 0.4,
237+
},
238+
],
239+
[
240+
{
241+
"x": 0.0,
242+
"y": 0.0,
243+
},
244+
{
245+
"x": 0.1,
246+
"y": 0.0,
247+
},
248+
{
249+
"x": 0.0,
250+
"y": 0.1,
251+
},
252+
],
253+
],
254+
)
255+
],
256+
output_dir=Path(tmp_dir),
257+
title="Test project",
258+
project_input_type="IMAGE",
259+
annotation_modifier=lambda x, _, _1: x,
260+
merged=False,
261+
)
262+
263+
assert paths[0] == output_file
264+
with output_file.open("r", encoding="utf-8") as f:
265+
coco_annotation = json.loads(f.read())
266+
267+
assert coco_annotation["annotations"][0]["bbox"] == [0, 0, 960, 540]
268+
assert coco_annotation["annotations"][0]["segmentation"] == [
269+
[0.0, 0.0, 960.0, 0.0, 0.0, 540.0],
270+
[192.0, 108.0, 768.0, 108.0, 192.0, 432.0],
271+
[0.0, 0.0, 192.0, 0.0, 0.0, 108.0],
272+
]
273+
# Here we have a positive triangle with 2 negative triangles inside, so the area is the
274+
# area of the positive triangle minus the area of the negative triangles.
275+
# Area of a triangle: base * height / 2
276+
assert coco_annotation["annotations"][0]["area"] == (960.0 * 540.0 / 2) - (
277+
576.0 * 324.0 / 2
278+
) - (192.0 * 108.0 / 2)
279+
280+
104281
@pytest.mark.parametrize(
105282
("name", "normalized_vertices", "expected_angle", "expected_bounding_box"),
106283
[
@@ -139,8 +316,6 @@ def test__get_coco_image_annotations_with_label_modifier(
139316
local_file_path = tmp_dir / Path("image1.jpg")
140317
Image.new("RGB", (image_width, image_height)).save(local_file_path)
141318

142-
area = 2073600
143-
144319
expected_segmentation = [
145320
a for p in normalized_vertices for a in [p["x"] * image_width, p["y"] * image_height]
146321
]
@@ -199,7 +374,10 @@ def test__get_coco_image_annotations_with_label_modifier(
199374
assert coco_annotation["annotations"][0]["segmentation"][0] == pytest.approx(
200375
expected_segmentation
201376
)
202-
assert coco_annotation["annotations"][0]["area"] == area
377+
# Area of a rectangle: width * height
378+
assert coco_annotation["annotations"][0]["area"] == pytest.approx(
379+
expected_bounding_box[2] * expected_bounding_box[3]
380+
)
203381

204382
good_date = True
205383
try:
@@ -409,13 +587,16 @@ def test_get_coco_geometry_from_kili_bpoly():
409587
}
410588
]
411589
image_width, image_height = 1920, 1080
412-
bbox, poly = _get_coco_geometry_from_kili_bpoly(boundingPoly, image_width, image_height)
590+
area, bbox, polygons = _get_coco_geometry_from_kili_bpoly(
591+
boundingPoly, image_width, image_height
592+
)
413593
assert bbox == [192, 108, 1344, 324]
594+
assert area == bbox[2] * bbox[3] # Area of a rectangle: width * height
414595
assert bbox[0] == int(0.1 * image_width)
415596
assert bbox[1] == int(0.1 * image_height)
416597
assert bbox[2] == int((0.8 - 0.1) * image_width)
417598
assert bbox[3] == int((0.4 - 0.1) * image_height)
418-
assert poly == [192.0, 108.0, 192.0, 432.0, 1536.0, 432.0, 1536.0, 108.0]
599+
assert polygons == [[192.0, 108.0, 192.0, 432.0, 1536.0, 432.0, 1536.0, 108.0]]
419600

420601

421602
def test__get_kili_cat_id_to_coco_cat_id_mapping_with_split_jobs():

0 commit comments

Comments
 (0)