Skip to content

Commit 247007a

Browse files
committed
fix(LAB-4125): handle all the formats with latestLabels
1 parent d9988d5 commit 247007a

8 files changed

Lines changed: 543 additions & 115 deletions

File tree

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,13 @@ def images_folder(self) -> Path:
5151
def process_and_save(self, assets: list[dict], output_filename: Path):
5252
"""Extract formatted annotations from labels."""
5353
clean_assets = self.preprocess_assets(assets)
54+
# Expand assets with latestLabels into multiple assets
55+
expanded_assets = self._expand_assets_with_multiple_labels(clean_assets)
5456
try:
5557
self._save_assets_export(
56-
clean_assets, self.export_root_folder, annotation_modifier=self.annotation_modifier
58+
expanded_assets,
59+
self.export_root_folder,
60+
annotation_modifier=self.annotation_modifier,
5761
)
5862
except ImportError as e:
5963
raise ImportError("Install with `pip install kili[coco]` to use this feature.") from e
@@ -62,6 +66,43 @@ def process_and_save(self, assets: list[dict], output_filename: Path):
6266

6367
self.logger.warning(output_filename)
6468

69+
def _expand_assets_with_multiple_labels(self, assets: list[dict]) -> list[dict]:
70+
"""Expand assets with latestLabels into multiple asset entries with latestLabel.
71+
72+
When an asset has multiple labels (latestLabels), create separate asset entries
73+
for each label with a unique externalId suffix (_label0, _label1, etc.).
74+
"""
75+
expanded_assets = []
76+
for asset in assets:
77+
# Collect all labels to process (handle both latestLabel and latestLabels)
78+
labels_to_process = []
79+
if "latestLabel" in asset and asset["latestLabel"]:
80+
labels_to_process.append(asset["latestLabel"])
81+
if "latestLabels" in asset and asset["latestLabels"]:
82+
for label in asset["latestLabels"]:
83+
if label is not None:
84+
labels_to_process.append(label)
85+
86+
if not labels_to_process:
87+
continue
88+
89+
# Create asset copy for each label
90+
for label_idx, latest_label in enumerate(labels_to_process, start=1):
91+
asset_copy = asset.copy()
92+
# Add label suffix if we have multiple labels
93+
if len(labels_to_process) > 1:
94+
label_suffix = f"_label{label_idx}"
95+
asset_copy["externalId"] = f"{asset['externalId']}{label_suffix}"
96+
97+
# Set latestLabel and remove latestLabels
98+
asset_copy["latestLabel"] = latest_label
99+
if "latestLabels" in asset_copy:
100+
del asset_copy["latestLabels"]
101+
102+
expanded_assets.append(asset_copy)
103+
104+
return expanded_assets
105+
65106
def _save_assets_export(
66107
self,
67108
assets: list[dict],

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,27 @@ def _process_asset(
9999
json_interface: dict | None = None,
100100
flatten_properties: bool = False,
101101
) -> None:
102-
geojson_feature_collection = convert_from_kili_to_geojson_format(
103-
asset["latestLabel"]["jsonResponse"], json_interface, flatten_properties
104-
)
105-
filepath = labels_folder / f"{asset['externalId']}.geojson"
106-
filepath.parent.mkdir(parents=True, exist_ok=True)
107-
with open(filepath, "w", encoding="utf-8") as file:
108-
json.dump(geojson_feature_collection, file)
102+
# Collect all labels to process (handle both latestLabel and latestLabels)
103+
labels_to_process = []
104+
if "latestLabel" in asset and asset["latestLabel"]:
105+
labels_to_process.append(asset["latestLabel"])
106+
if "latestLabels" in asset and asset["latestLabels"]:
107+
for label in asset["latestLabels"]:
108+
if label is not None:
109+
labels_to_process.append(label)
110+
111+
if not labels_to_process:
112+
return
113+
114+
# Process each label
115+
for label_idx, latest_label in enumerate(labels_to_process, start=1):
116+
# Add label suffix if we have multiple labels
117+
label_suffix = f"_label{label_idx}" if len(labels_to_process) > 1 else ""
118+
119+
geojson_feature_collection = convert_from_kili_to_geojson_format(
120+
latest_label["jsonResponse"], json_interface, flatten_properties
121+
)
122+
filepath = labels_folder / f"{asset['externalId']}{label_suffix}.geojson"
123+
filepath.parent.mkdir(parents=True, exist_ok=True)
124+
with open(filepath, "w", encoding="utf-8") as file:
125+
json.dump(geojson_feature_collection, file)

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

Lines changed: 87 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -74,60 +74,98 @@ def process_and_save(self, assets: list[dict], output_filename: Path) -> None:
7474

7575

7676
# pylint: disable=too-many-locals
77-
def _process_asset(
78-
asset: dict, labels_folder: Path, project_input_type: str, valid_jobs: Sequence[str]
77+
def _process_video_asset(
78+
asset: dict,
79+
latest_label: dict,
80+
labels_folder: Path,
81+
label_suffix: str,
82+
valid_jobs: Sequence[str],
7983
) -> None:
80-
"""Process an asset."""
81-
if project_input_type == "VIDEO":
82-
nbr_frames = len(asset.get("latestLabel", {}).get("jsonResponse", {}))
83-
if nbr_frames < 1:
84-
return
85-
leading_zeros = len(str(nbr_frames))
86-
87-
width = height = 0
88-
frame_ext = ""
89-
# jsonContent with frames
90-
if isinstance(asset["jsonContent"], list) and Path(asset["jsonContent"][0]).is_file():
91-
width, height = get_frame_dimensions(asset)
92-
frame_ext = Path(asset["jsonContent"][0]).suffix
93-
94-
# video with shouldUseNativeVideo set to True (no frames available)
95-
elif Path(asset["content"]).is_file():
96-
try:
97-
width, height = get_video_dimensions(asset)
98-
cut_video(asset["content"], asset, leading_zeros, Path(asset["content"]).parent)
99-
frame_ext = ".jpg"
100-
except ImportError as e:
101-
raise ImportError(
102-
"Install with `pip install kili[video]` to use this feature."
103-
) from e
104-
105-
else:
106-
raise FileNotFoundError(f"Could not find frames or video for asset {asset}")
107-
108-
for frame_id, json_response in asset["latestLabel"]["jsonResponse"].items():
109-
frame_name = f'{asset["externalId"]}_{str(int(frame_id)+1).zfill(leading_zeros)}'
110-
parameters = {"filename": f"{frame_name}{frame_ext}"}
111-
annotations = convert_from_kili_to_voc_format(
112-
json_response, width, height, parameters, valid_jobs
113-
)
114-
filepath = labels_folder / f"{frame_name}.xml"
115-
filepath.parent.mkdir(parents=True, exist_ok=True)
116-
with open(filepath, "wb") as fout:
117-
fout.write(f"{annotations}\n".encode())
118-
119-
elif project_input_type == "IMAGE":
120-
json_response = asset["latestLabel"]["jsonResponse"]
121-
width, height = get_image_dimensions(asset)
122-
filename = (
123-
Path(asset["content"]).name if Path(asset["content"]).is_file() else asset["externalId"]
84+
"""Process a video asset and save annotations."""
85+
nbr_frames = len(latest_label.get("jsonResponse", {}))
86+
if nbr_frames < 1:
87+
return
88+
leading_zeros = len(str(nbr_frames))
89+
90+
width = height = 0
91+
frame_ext = ""
92+
# jsonContent with frames
93+
if isinstance(asset["jsonContent"], list) and Path(asset["jsonContent"][0]).is_file():
94+
width, height = get_frame_dimensions(asset)
95+
frame_ext = Path(asset["jsonContent"][0]).suffix
96+
97+
# video with shouldUseNativeVideo set to True (no frames available)
98+
elif Path(asset["content"]).is_file():
99+
try:
100+
width, height = get_video_dimensions(asset)
101+
cut_video(asset["content"], asset, leading_zeros, Path(asset["content"]).parent)
102+
frame_ext = ".jpg"
103+
except ImportError as e:
104+
raise ImportError("Install with `pip install kili[video]` to use this feature.") from e
105+
106+
else:
107+
raise FileNotFoundError(f"Could not find frames or video for asset {asset}")
108+
109+
for frame_id, json_response in latest_label["jsonResponse"].items():
110+
frame_name = (
111+
f'{asset["externalId"]}_{str(int(frame_id)+1).zfill(leading_zeros)}{label_suffix}'
124112
)
125-
parameters = {"filename": filename}
113+
parameters = {"filename": f"{frame_name}{frame_ext}"}
126114
annotations = convert_from_kili_to_voc_format(
127115
json_response, width, height, parameters, valid_jobs
128116
)
129-
xml_filename = f'{asset["externalId"]}.xml'
130-
filepath = labels_folder / xml_filename
117+
filepath = labels_folder / f"{frame_name}.xml"
131118
filepath.parent.mkdir(parents=True, exist_ok=True)
132119
with open(filepath, "wb") as fout:
133120
fout.write(f"{annotations}\n".encode())
121+
122+
123+
def _process_image_asset(
124+
asset: dict,
125+
latest_label: dict,
126+
labels_folder: Path,
127+
label_suffix: str,
128+
valid_jobs: Sequence[str],
129+
) -> None:
130+
"""Process an image asset and save annotations."""
131+
json_response = latest_label["jsonResponse"]
132+
width, height = get_image_dimensions(asset)
133+
filename = (
134+
Path(asset["content"]).name if Path(asset["content"]).is_file() else asset["externalId"]
135+
)
136+
parameters = {"filename": filename}
137+
annotations = convert_from_kili_to_voc_format(
138+
json_response, width, height, parameters, valid_jobs
139+
)
140+
xml_filename = f'{asset["externalId"]}{label_suffix}.xml'
141+
filepath = labels_folder / xml_filename
142+
filepath.parent.mkdir(parents=True, exist_ok=True)
143+
with open(filepath, "wb") as fout:
144+
fout.write(f"{annotations}\n".encode())
145+
146+
147+
def _process_asset(
148+
asset: dict, labels_folder: Path, project_input_type: str, valid_jobs: Sequence[str]
149+
) -> None:
150+
"""Process an asset."""
151+
# Collect all labels to process (handle both latestLabel and latestLabels)
152+
labels_to_process = []
153+
if "latestLabel" in asset and asset["latestLabel"]:
154+
labels_to_process.append(asset["latestLabel"])
155+
if "latestLabels" in asset and asset["latestLabels"]:
156+
for label in asset["latestLabels"]:
157+
if label is not None:
158+
labels_to_process.append(label)
159+
160+
if not labels_to_process:
161+
return
162+
163+
# Process each label
164+
for label_idx, latest_label in enumerate(labels_to_process, start=1):
165+
# Add label suffix if we have multiple labels
166+
label_suffix = f"_label{label_idx}" if len(labels_to_process) > 1 else ""
167+
168+
if project_input_type == "VIDEO":
169+
_process_video_asset(asset, latest_label, labels_folder, label_suffix, valid_jobs)
170+
elif project_input_type == "IMAGE":
171+
_process_image_asset(asset, latest_label, labels_folder, label_suffix, valid_jobs)

0 commit comments

Comments
 (0)