Skip to content

Commit 77f3479

Browse files
authored
[lts] fix(LAB-3755): add width and height in patch_label_json_respons… (#1914)
1 parent db9125a commit 77f3479

7 files changed

Lines changed: 252 additions & 35 deletions

File tree

src/kili/adapters/kili_api_gateway/asset/operations_mixin.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
from kili.adapters.kili_api_gateway.label.annotation_to_json_response import (
2222
AnnotationsToJsonResponseConverter,
2323
)
24-
from kili.adapters.kili_api_gateway.label.common import get_annotation_fragment
24+
from kili.adapters.kili_api_gateway.label.common import (
25+
get_annotation_fragment,
26+
)
2527
from kili.adapters.kili_api_gateway.project.common import get_project
2628
from kili.domain.asset import AssetFilters
2729
from kili.domain.types import ListOrTuple
@@ -104,14 +106,16 @@ def list_assets_split(
104106
for asset in assets_gen:
105107
if "latestLabel.jsonResponse" in fields and asset.get("latestLabel"):
106108
converter.patch_label_json_response(
107-
asset["latestLabel"], asset["latestLabel"]["annotations"]
109+
asset,
110+
asset["latestLabel"],
111+
asset["latestLabel"]["annotations"],
108112
)
109113
if not is_requesting_annotations:
110114
asset["latestLabel"].pop("annotations")
111115

112116
if "labels.jsonResponse" in fields:
113117
for label in asset.get("labels", []):
114-
converter.patch_label_json_response(label, label["annotations"])
118+
converter.patch_label_json_response(asset, label, label["annotations"])
115119
if not is_requesting_annotations:
116120
label.pop("annotations")
117121
yield asset

src/kili/adapters/kili_api_gateway/label/annotation_to_json_response.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from dataclasses import dataclass
55
from typing import Dict, Generator, List, Optional, Tuple, TypeVar, Union, cast, overload
66

7+
from kili.core.helpers import get_video_and_frame_dimensions
78
from kili.domain.annotation import (
89
ClassicAnnotation,
910
ClassificationAnnotation,
@@ -23,6 +24,32 @@
2324
ASSET_LEVEL_KEY = "assetLevel"
2425

2526

27+
def convert_from_normalized_to_absolute(
28+
vertices: List[Vertice], width: int, height: int
29+
) -> List[Vertice]:
30+
"""Convert normalized vertices to absolute coordinates."""
31+
return [
32+
Vertice(
33+
x=vertex["x"] * width,
34+
y=vertex["y"] * height,
35+
)
36+
for vertex in vertices
37+
]
38+
39+
40+
def convert_from_absolute_to_normalized(
41+
vertices: List[Vertice], width: int, height: int
42+
) -> List[Vertice]:
43+
"""Convert absolute vertices to normalized coordinates."""
44+
return [
45+
Vertice(
46+
x=vertex["x"] / width,
47+
y=vertex["y"] / height,
48+
)
49+
for vertex in vertices
50+
]
51+
52+
2653
class AnnotationsToJsonResponseConverter:
2754
"""Convert annotations to JSON response."""
2855

@@ -47,7 +74,10 @@ def _label_has_json_response_data(self, label: Dict) -> bool:
4774
return False
4875

4976
def patch_label_json_response(
50-
self, label: Dict, annotations: Union[List[VideoAnnotation], List[ClassicAnnotation]]
77+
self,
78+
asset: Optional[Dict],
79+
label: Dict,
80+
annotations: Union[List[VideoAnnotation], List[ClassicAnnotation]],
5181
) -> None:
5282
"""Patch the label json response using the annotations.
5383
@@ -58,9 +88,17 @@ def patch_label_json_response(
5888
return
5989

6090
if self._project_input_type == "VIDEO":
91+
if not asset:
92+
raise ValueError(
93+
"Asset is required for video annotations to compute dimensions."
94+
)
95+
width, height = get_video_and_frame_dimensions(asset)
6196
annotations = cast(List[VideoAnnotation], annotations)
6297
converted_json_resp = _video_annotations_to_json_response(
63-
annotations=annotations, json_interface=self._project_json_interface
98+
annotations=annotations,
99+
json_interface=self._project_json_interface,
100+
width=width,
101+
height=height,
64102
)
65103
else:
66104
annotations = cast(List[ClassicAnnotation], annotations)
@@ -91,7 +129,7 @@ def _fill_empty_frames(json_response: Dict) -> None:
91129

92130

93131
def _video_annotations_to_json_response(
94-
annotations: List[VideoAnnotation], json_interface: Dict
132+
annotations: List[VideoAnnotation], json_interface: Dict, width: int, height: int
95133
) -> Dict[str, Dict[JobName, Dict]]:
96134
"""Convert video label annotations to a video json response."""
97135
json_resp = defaultdict(dict)
@@ -110,7 +148,7 @@ def _video_annotations_to_json_response(
110148
elif ann["__typename"] == "VideoObjectDetectionAnnotation":
111149
ann = cast(VideoObjectDetectionAnnotation, ann)
112150
ann_json_resp = _video_object_detection_annotation_to_json_response(
113-
ann, other_annotations, json_interface=json_interface
151+
ann, other_annotations, json_interface=json_interface, width=width, height=height
114152
)
115153
for frame_id, frame_json_resp in ann_json_resp.items():
116154
for job_name, job_resp in frame_json_resp.items():
@@ -532,6 +570,8 @@ def _video_object_detection_annotation_to_json_response(
532570
annotation: VideoObjectDetectionAnnotation,
533571
other_annotations: List[VideoAnnotation],
534572
json_interface: Dict,
573+
width: int,
574+
height: int,
535575
) -> Dict[str, Dict[JobName, Dict]]:
536576
# get the child annotations of the current annotation
537577
# and compute the json response of those child jobs
@@ -569,6 +609,8 @@ def _video_object_detection_annotation_to_json_response(
569609
object_final_state=object_final_state,
570610
final_state_frame_index=next_key_ann["frame"],
571611
at_frame=frame_id,
612+
width=width,
613+
height=height,
572614
)
573615

574616
if json_interface["jobs"][annotation["job"]]["tools"][0] == "marker":
@@ -616,6 +658,8 @@ def _interpolate_object(
616658
object_final_state: List[List[List[Vertice]]],
617659
final_state_frame_index: int,
618660
at_frame: int,
661+
width: int,
662+
height: int,
619663
) -> List[List[List[Vertice]]]:
620664
"""Interpolate an object between two key frames."""
621665
# if the two frames are consecutive, we do not interpolate
@@ -648,6 +692,8 @@ def _interpolate_object(
648692
next_vertices=object_final_state[0][0],
649693
weight=(at_frame - initial_state_frame_index)
650694
/ (final_state_frame_index - initial_state_frame_index),
695+
width=width,
696+
height=height,
651697
)
652698
]
653699
]
@@ -674,6 +720,8 @@ def _interpolate_rectangle(
674720
previous_vertices: List[Vertice],
675721
next_vertices: List[Vertice],
676722
weight: float,
723+
width: int,
724+
height: int,
677725
) -> List[Vertice]:
678726
"""Interpolate a rectangle.
679727
@@ -683,9 +731,18 @@ def _interpolate_rectangle(
683731
The interpolated properties are used to reconstruct the vertices of the interpolated rectangle,
684732
which are then converted back to normalized coordinates.
685733
"""
686-
permuted_new_vertices = _find_rectangle_vertices_bijection(previous_vertices, next_vertices)
734+
previous_absolute_vertices = convert_from_normalized_to_absolute(
735+
previous_vertices, height=height, width=width
736+
)
737+
next_absolute_vertices = convert_from_normalized_to_absolute(
738+
next_vertices, height=height, width=width
739+
)
740+
741+
permuted_new_vertices = _find_rectangle_vertices_bijection(
742+
previous_absolute_vertices, next_absolute_vertices
743+
)
687744

688-
previous_rectangle_properties = _find_rectangle_properties(previous_vertices)
745+
previous_rectangle_properties = _find_rectangle_properties(previous_absolute_vertices)
689746
next_rectangle_properties = _find_rectangle_properties(permuted_new_vertices)
690747

691748
interpolated_angle = _interpolate_angle(
@@ -712,7 +769,7 @@ def _interpolate_rectangle(
712769
interpolated_rectangle_properties
713770
)
714771

715-
return interpolated_rectangle
772+
return convert_from_absolute_to_normalized(interpolated_rectangle, height=height, width=width)
716773

717774

718775
def _find_rectangle_vertices_bijection(

src/kili/adapters/kili_api_gateway/label/common.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,38 @@
11
"""Label gateway common."""
2+
from typing import Dict
3+
4+
from kili.adapters.http_client import HttpClient
5+
from kili.adapters.kili_api_gateway.asset.formatters import load_asset_json_fields
6+
from kili.adapters.kili_api_gateway.asset.operations import get_assets_query
27
from kili.adapters.kili_api_gateway.helpers.queries import fragment_builder
38
from kili.adapters.kili_api_gateway.label.operations import (
49
get_annotations_partial_query,
510
)
11+
from kili.core.graphql.graphql_client import GraphQLClient
12+
from kili.domain.asset.asset import AssetId
13+
from kili.domain.types import ListOrTuple
14+
from kili.exceptions import NotFound
15+
16+
17+
def get_asset(
18+
graphql_client: GraphQLClient,
19+
http_client: HttpClient,
20+
asset_id: AssetId,
21+
fields: ListOrTuple[str],
22+
) -> Dict:
23+
"""Get asset."""
24+
fragment = fragment_builder(fields)
25+
query = get_assets_query(fragment)
26+
result = graphql_client.execute(
27+
query=query, variables={"where": {"id": asset_id}, "first": 1, "skip": 0}
28+
)
29+
assets = result["data"]
30+
31+
if len(assets) == 0:
32+
raise NotFound(
33+
f"asset ID: {asset_id}. The asset does not exist or you do not have access to it."
34+
)
35+
return load_asset_json_fields(assets[0], fields, http_client=http_client)
636

737

838
def get_annotation_fragment():

src/kili/adapters/kili_api_gateway/label/operations_mixin.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .annotation_to_json_response import (
2222
AnnotationsToJsonResponseConverter,
2323
)
24-
from .common import get_annotation_fragment
24+
from .common import get_annotation_fragment, get_asset
2525
from .formatters import load_label_json_fields
2626
from .mappers import (
2727
append_label_data_mapper,
@@ -56,6 +56,8 @@ def list_labels(
5656
) -> Generator[Dict, None, None]:
5757
"""List labels."""
5858
if "jsonResponse" in fields:
59+
if "labelOf" not in fields:
60+
fields = [*list(fields), "assetId"]
5961
project_info = get_project(
6062
self.graphql_client, filters.project_id, ("inputType", "jsonInterface")
6163
)
@@ -107,7 +109,16 @@ def list_labels_split(
107109
project_input_type=project_info["inputType"],
108110
)
109111
for label in labels_gen:
110-
converter.patch_label_json_response(label, label["annotations"])
112+
asset = None
113+
if project_info["inputType"] == "VIDEO":
114+
asset = get_asset(
115+
self.graphql_client,
116+
self.http_client,
117+
label["assetId"],
118+
["content", "jsonContent", "resolution.width", "resolution.height"],
119+
)
120+
121+
converter.patch_label_json_response(asset, label, label["annotations"])
111122
if "annotations" not in fields:
112123
label.pop("annotations")
113124
yield label

src/kili/core/helpers.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,19 @@
88
import re
99
import warnings
1010
from json import dumps, loads
11-
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union
11+
from pathlib import Path
12+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union
1213

14+
import ffmpeg
1315
import pyparsing as pp
1416
import requests
1517
import tenacity
18+
from PIL import Image
1619
from typing_extensions import get_args, get_origin
1720

1821
from kili.adapters.http_client import HttpClient
1922
from kili.core.constants import mime_extensions_for_IV2
23+
from kili.exceptions import NotFound
2024
from kili.log.logging import logger
2125

2226
T = TypeVar("T")
@@ -368,3 +372,69 @@ def get_response_json(response: requests.Response) -> dict:
368372
except json.JSONDecodeError:
369373
logger.exception("An error occurred while decoding the json response")
370374
return {}
375+
376+
377+
def _get_image_dimensions(filepath: str) -> Tuple:
378+
"""Get an image width and height."""
379+
image = Image.open(filepath)
380+
return image.size
381+
382+
383+
def get_frame_dimensions(asset: Dict) -> Tuple:
384+
"""Get a video asset frame width and height."""
385+
if "resolution" in asset and asset["resolution"] is not None:
386+
return (asset["resolution"]["width"], asset["resolution"]["height"])
387+
388+
if (
389+
isinstance(asset["jsonContent"], list)
390+
and len(asset["jsonContent"]) > 0
391+
and Path(asset["jsonContent"][0]).is_file()
392+
):
393+
return _get_image_dimensions(asset["jsonContent"][0])
394+
395+
raise NotFound(
396+
f"Could not find dimensions for asset with externalId '{asset['externalId']}'. Please"
397+
" use `kili.update_properties_in_assets()` to update the resolution of your asset. Or use"
398+
" `kili.export_labels(with_assets=True).`"
399+
)
400+
401+
402+
def get_video_and_frame_dimensions(
403+
asset: Dict,
404+
) -> Tuple[int, int]:
405+
"""Get a video width and height, and a frame width and height."""
406+
width = height = 0
407+
if "resolution" in asset and asset["resolution"] is not None:
408+
width = asset["resolution"]["width"]
409+
height = asset["resolution"]["height"]
410+
return width, height
411+
if isinstance(asset["jsonContent"], list) and Path(asset["jsonContent"][0]).is_file():
412+
width, height = get_frame_dimensions(asset)
413+
elif Path(asset["content"]).is_file():
414+
width, height = get_video_dimensions(asset)
415+
else:
416+
raise FileNotFoundError(
417+
f"Could not find video dimensions for asset with externalId '{asset['externalId']}'. Please"
418+
" use `kili.update_properties_in_assets()` to update the resolution of your asset. Or use"
419+
" `kili.export_labels(with_assets=True).`"
420+
)
421+
return width, height
422+
423+
424+
def get_video_dimensions(asset: Dict) -> Tuple:
425+
"""Get a video width and height."""
426+
if "resolution" in asset and asset["resolution"] is not None:
427+
return (asset["resolution"]["width"], asset["resolution"]["height"])
428+
429+
if Path(asset["content"]).is_file():
430+
probe = ffmpeg.probe(str(asset["content"]))
431+
video_info = next(s for s in probe["streams"] if s["codec_type"] == "video")
432+
width = video_info["width"]
433+
height = video_info["height"]
434+
return width, height
435+
436+
raise NotFound(
437+
f"Could not find video dimensions for asset with externalId '{asset['externalId']}'. Please"
438+
" use `kili.update_properties_in_assets()` to update the resolution of your asset. Or use"
439+
" `kili.export_labels(with_assets=True).`"
440+
)

0 commit comments

Comments
 (0)