Skip to content

Commit

Permalink
fix(LAB-3380): Correct vertices calculation for each type of export +…
Browse files Browse the repository at this point in the history
… creation of units tests
  • Loading branch information
Sihem Tchabi committed Jan 14, 2025
1 parent fb90304 commit 7a2dc3f
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 25 deletions.
47 changes: 38 additions & 9 deletions src/kili/services/export/format/coco/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ def _get_images_and_annotation_for_images(
width=width,
date_captured=None,
)

rotation_val = 0
if "ROTATION_JOB" in asset["latestLabel"]["jsonResponse"]:
rotation_val = asset["latestLabel"]["jsonResponse"]["ROTATION_JOB"]["rotation"]
coco_images.append(coco_image)
if is_single_job:
assert len(list(jobs.keys())) == 1
Expand All @@ -304,6 +306,7 @@ def _get_images_and_annotation_for_images(
annotation_offset,
coco_image,
annotation_modifier=annotation_modifier,
rotation=rotation_val,
)
coco_annotations.extend(coco_img_annotations)
else:
Expand All @@ -318,6 +321,7 @@ def _get_images_and_annotation_for_images(
annotation_offset,
coco_image,
annotation_modifier=annotation_modifier,
rotation=rotation_val,
)
coco_annotations.extend(coco_img_annotations)
return coco_images, coco_annotations
Expand Down Expand Up @@ -367,7 +371,6 @@ def _get_images_and_annotation_for_videos(
date_captured=None,
)
coco_images.append(coco_image)

if is_single_job:
job_name = next(iter(jobs.keys()))
if job_name not in json_response:
Expand Down Expand Up @@ -405,6 +408,7 @@ def _get_coco_image_annotations(
annotation_offset: int,
coco_image: CocoImage,
annotation_modifier: Optional[CocoAnnotationModifier],
rotation: Optional[int] = 0
) -> Tuple[List[CocoAnnotation], int]:
coco_annotations = []

Expand All @@ -418,7 +422,7 @@ def _get_coco_image_annotations(
continue
bounding_poly = annotation["boundingPoly"]
area, bbox, polygons = _get_coco_geometry_from_kili_bpoly(
bounding_poly, coco_image["width"], coco_image["height"]
bounding_poly, coco_image["width"], coco_image["height"], rotation
)
if len(polygons[0]) < 6: # twice the number of vertices
print("A polygon must contain more than 2 points. Skipping this polygon...")
Expand Down Expand Up @@ -466,13 +470,30 @@ def _get_shoelace_area(x: List[float], y: List[float]):

return area

def reverse_rotation_vertices(normalized_vertices, rotation_angle):
new_vertices = []
for vertice in normalized_vertices:
new_x = vertice["x"]
new_y = vertice["y"]
if rotation_angle == 90:
new_x = vertice["y"]
new_y = 1 - vertice["x"]
elif rotation_angle == 180:
new_x = 1 - vertice["x"]
new_y = 1 - vertice["y"]
elif rotation_angle == 270:
new_x = 1 - vertice["y"]
new_y = vertice["x"]
new_vertices.append({"x": new_x, "y": new_y})
return new_vertices

def _get_coco_geometry_from_kili_bpoly(
bounding_poly: List[Dict], asset_width: int, asset_height: int
bounding_poly: List[Dict], asset_width: int, asset_height: int, rotation_angle: int
):
normalized_vertices = bounding_poly[0]["normalizedVertices"]
p_x = [float(vertice["x"]) * asset_width for vertice in normalized_vertices]
p_y = [float(vertice["y"]) * asset_height for vertice in normalized_vertices]
new_vertices = reverse_rotation_vertices(normalized_vertices, rotation_angle)
p_x = [float(vertice["x"]) * asset_width for vertice in new_vertices]
p_y = [float(vertice["y"]) * asset_height for vertice in new_vertices]
poly_vertices = [(float(x), float(y)) for x, y in zip(p_x, p_y)]
x_min, y_min = min(p_x), min(p_y)
x_max, y_max = max(p_x), max(p_y)
Expand All @@ -484,12 +505,20 @@ def _get_coco_geometry_from_kili_bpoly(
if len(bounding_poly) > 1:
for negative_bounding_poly in bounding_poly[1:]:
negative_normalized_vertices = negative_bounding_poly["normalizedVertices"]
np_x = [float(vertice["x"]) * asset_width for vertice in negative_normalized_vertices]
np_y = [float(vertice["y"]) * asset_height for vertice in negative_normalized_vertices]
new_negative_vertices = reverse_rotation_vertices(
negative_normalized_vertices, rotation_angle
)
np_x = [
float(negative_vertice["x"]) * asset_width
for negative_vertice in new_negative_vertices
]
np_y = [
float(negative_vertice["y"]) * asset_height
for negative_vertice in new_negative_vertices
]
area -= _get_shoelace_area(np_x, np_y)
poly_negative_vertices = [(float(x), float(y)) for x, y in zip(np_x, np_y)]
polygons.append([p for vertice in poly_negative_vertices for p in vertice])

bbox = [int(x_min), int(y_min), int(bbox_width), int(bbox_height)]
return area, bbox, polygons

Expand Down
24 changes: 22 additions & 2 deletions src/kili/services/export/format/voc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ def _process_asset(
with open(filepath, "wb") as fout:
fout.write(f"{annotations}\n".encode())

def reverse_rotation_vertices(normalized_vertices, rotation_angle):
new_vertices = []
for vertice in normalized_vertices:
new_x = vertice["x"]
new_y = vertice["y"]
if rotation_angle == 90:
new_x = vertice["y"]
new_y = 1 - vertice["x"]
elif rotation_angle == 180:
new_x = 1 - vertice["x"]
new_y = 1 - vertice["y"]
elif rotation_angle == 270:
new_x = 1 - vertice["y"]
new_y = vertice["x"]
new_vertices.append({"x": new_x, "y": new_y})
return new_vertices

def _parse_annotations(
response: Dict,
Expand All @@ -136,6 +152,9 @@ def _parse_annotations(
valid_jobs: Sequence[str],
) -> None:
# pylint: disable=too-many-locals
rotation_val = 0
if "ROTATION_JOB" in response:
rotation_val = response["ROTATION_JOB"]["rotation"]
for job_name, job_response in response.items():
if job_name not in valid_jobs:
continue
Expand All @@ -159,8 +178,9 @@ def _parse_annotations(
occluded = ET.SubElement(annotation_category, "occluded")
occluded.text = "0"
bndbox = ET.SubElement(annotation_category, "bndbox")
x_vertices = [int(round(v["x"] * img_width)) for v in vertices]
y_vertices = [int(round(v["y"] * img_height)) for v in vertices]
new_vertices = reverse_rotation_vertices(vertices, rotation_val)
x_vertices = [int(round(v["x"] * img_width)) for v in new_vertices]
y_vertices = [int(round(v["y"] * img_height)) for v in new_vertices]
xmin = ET.SubElement(bndbox, "xmin")
xmin.text = str(min(x_vertices))
xmax = ET.SubElement(bndbox, "xmax")
Expand Down
27 changes: 25 additions & 2 deletions src/kili/services/export/format/yolo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,24 @@ def get_label_filename(self, idx: int) -> str:
return f"{self.external_id}_{str(idx + 1).zfill(self.get_leading_zeros())}"


def reverse_rotation_vertices(normalized_vertices, rotation_angle):
new_vertices = []
for vertice in normalized_vertices:
new_x = vertice["x"]
new_y = vertice["y"]
if rotation_angle == 90:
new_x = vertice["y"]
new_y = 1 - vertice["x"]
elif rotation_angle == 180:
new_x = 1 - vertice["x"]
new_y = 1 - vertice["y"]
elif rotation_angle == 270:
new_x = 1 - vertice["y"]
new_y = vertice["x"]
new_vertices.append({"x": new_x, "y": new_y})
return new_vertices


def _convert_from_kili_to_yolo_format(
job_id: str, label: Dict, category_ids: Dict[str, JobCategory]
) -> List[Tuple]:
Expand All @@ -259,6 +277,10 @@ def _convert_from_kili_to_yolo_format(
if label is None or "jsonResponse" not in label:
return []
json_response = label["jsonResponse"]
rotation_val = 0
if "ROTATION_JOB" in json_response:
rotation_val = json_response["ROTATION_JOB"]["rotation"]

if not (job_id in json_response and "annotations" in json_response[job_id]):
return []
annotations = json_response[job_id]["annotations"]
Expand All @@ -273,8 +295,9 @@ def _convert_from_kili_to_yolo_format(
if len(bounding_poly) < 1 or "normalizedVertices" not in bounding_poly[0]:
continue
normalized_vertices = bounding_poly[0]["normalizedVertices"]
x_s: List[float] = [vertice["x"] for vertice in normalized_vertices]
y_s: List[float] = [vertice["y"] for vertice in normalized_vertices]
new_vertices = reverse_rotation_vertices(normalized_vertices, rotation_val)
x_s: List[float] = [vertice["x"] for vertice in new_vertices]
y_s: List[float] = [vertice["y"] for vertice in new_vertices]

if annotation["type"] == JobTool.RECTANGLE:
x_min, y_min = min(x_s), min(y_s)
Expand Down
143 changes: 143 additions & 0 deletions tests/fakes/fake_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,105 @@
]
}
}
job_object_detection_with_0_rotation = {
"JOB_0": {
"annotations": [
{
"categories": [{"confidence": 100, "name": "OBJECT_A"}],
"jobName": "JOB_0",
"mid": "2022040515434712-7532",
"mlTask": "OBJECT_DETECTION",
"boundingPoly": [
{
"normalizedVertices": [
{"x": 0.07787903893951942, "y": 0.4746554191458914},
{"x": 0.07787903893951942, "y": 0.09429841078556889},
{"x": 0.3388566694283348, "y": 0.09429841078556889},
{"x": 0.3388566694283348, "y": 0.4746554191458914},
]
}
],
"type": "rectangle",
"children": {},
}
]
}
}
job_object_detection_with_90_rotation = {
"JOB_0": {
"annotations": [
{
"categories": [{"confidence": 100, "name": "OBJECT_A"}],
"jobName": "JOB_0",
"mid": "2022040515434712-7532",
"mlTask": "OBJECT_DETECTION",
"boundingPoly": [
{
"normalizedVertices": [
{"x": 0.5253445808541086, "y": 0.07787903893951942},
{"x": 0.9057015892144311, "y": 0.07787903893951942},
{"x": 0.9057015892144311, "y": 0.3388566694283348},
{"x": 0.5253445808541086, "y": 0.3388566694283348},
]
}
],
"type": "rectangle",
"children": {},
}
]
},
"ROTATION_JOB": {"rotation": 90},
}
job_object_detection_with_180_rotation = {
"JOB_0": {
"annotations": [
{
"categories": [{"confidence": 100, "name": "OBJECT_A"}],
"jobName": "JOB_0",
"mid": "2022040515434712-7532",
"mlTask": "OBJECT_DETECTION",
"boundingPoly": [
{
"normalizedVertices": [
{"x": 0.9221209610604806, "y": 0.5253445808541086},
{"x": 0.9221209610604806, "y": 0.9057015892144311},
{"x": 0.6611433305716652, "y": 0.9057015892144311},
{"x": 0.6611433305716652, "y": 0.5253445808541086},
]
}
],
"type": "rectangle",
"children": {},
}
]
},
"ROTATION_JOB": {"rotation": 180},
}
job_object_detection_with_270_rotation = {
"JOB_0": {
"annotations": [
{
"categories": [{"confidence": 100, "name": "OBJECT_A"}],
"jobName": "JOB_0",
"mid": "2022040515434712-7532",
"mlTask": "OBJECT_DETECTION",
"boundingPoly": [
{
"normalizedVertices": [
{"x": 0.4746554191458914, "y": 0.9221209610604806},
{"x": 0.09429841078556889, "y": 0.9221209610604806},
{"x": 0.09429841078556889, "y": 0.6611433305716652},
{"x": 0.4746554191458914, "y": 0.6611433305716652},
]
}
],
"type": "rectangle",
"children": {},
}
]
},
"ROTATION_JOB": {"rotation": 270},
}
job_object_detection_with_classification = {
"JOB_0": {
"annotations": [
Expand Down Expand Up @@ -85,6 +184,50 @@
"jsonContent": "",
}

asset_image_1_with_0_rotation = {
"latestLabel": {
"jsonResponse": job_object_detection_with_0_rotation,
"author": {"firstname": "Jean-Pierre", "lastname": "Dupont"},
"labelType": "DEFAULT",
},
"externalId": "car_1",
"content": "https://storage.googleapis.com/label-public-staging/car/car_1.jpg",
"jsonContent": "",
}

asset_image_1_with_90_rotation = {
"latestLabel": {
"jsonResponse": job_object_detection_with_90_rotation,
"author": {"firstname": "Jean-Pierre", "lastname": "Dupont"},
"labelType": "DEFAULT",
},
"externalId": "car_1",
"content": "https://storage.googleapis.com/label-public-staging/car/car_1.jpg",
"jsonContent": "",
}

asset_image_1_with_180_rotation = {
"latestLabel": {
"jsonResponse": job_object_detection_with_180_rotation,
"author": {"firstname": "Jean-Pierre", "lastname": "Dupont"},
"labelType": "DEFAULT",
},
"externalId": "car_1",
"content": "https://storage.googleapis.com/label-public-staging/car/car_1.jpg",
"jsonContent": "",
}

asset_image_1_with_270_rotation = {
"latestLabel": {
"jsonResponse": job_object_detection_with_270_rotation,
"author": {"firstname": "Jean-Pierre", "lastname": "Dupont"},
"labelType": "DEFAULT",
},
"externalId": "car_1",
"content": "https://storage.googleapis.com/label-public-staging/car/car_1.jpg",
"jsonContent": "",
}

asset_image_1_without_annotation = {
"latestLabel": {
"jsonResponse": {},
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/services/export/expected/car_1_with_0_rotation.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" ?>
<annotation>
<folder/>
<filename>car_1.xml</filename>
<path/>
<source>
<database>Kili Technology</database>
</source>
<size>
<width>1920</width>
<height>1080</height>
<depth>3</depth>
</size>
<segmented/>
<object>
<name>OBJECT_A</name>
<job_name>JOB_0</job_name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<occluded>0</occluded>
<bndbox>
<xmin>150</xmin>
<xmax>651</xmax>
<ymin>102</ymin>
<ymax>513</ymax>
</bndbox>
</object>
</annotation>
Loading

0 comments on commit 7a2dc3f

Please sign in to comment.