Skip to content

Commit 48af72a

Browse files
committed
feat(LAB-3719): add enrich_geojson_with_kili_properties
1 parent fdf59c5 commit 48af72a

2 files changed

Lines changed: 123 additions & 5 deletions

File tree

src/kili/presentation/client/label.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from kili.use_cases.label.process_shapefiles import get_json_response_from_shapefiles
4949
from kili.use_cases.label.types import LabelToCreateUseCaseInput
5050
from kili.use_cases.project.project import ProjectUseCases
51+
from kili.utils.labels.geojson import enrich_geojson_with_kili_properties
5152
from kili.utils.labels.parsing import ParsedLabel
5253
from kili.utils.logcontext import for_all_methods, log_call
5354

@@ -1413,23 +1414,29 @@ def append_labels_from_geojson_files(
14131414
14141415
This method processes GeoJSON feature collections, converts them to the appropriate
14151416
Kili annotation format, and appends them as labels to the specified asset.
1416-
The GeoJSON features must contain job names and category information in their properties.
1417+
The GeoJSON features can contain job names and category information in their properties,
1418+
or they will be automatically mapped based on geometry type and available jobs.
14171419
14181420
Args:
14191421
project_id: The ID of the Kili project to add the labels to.
14201422
asset_external_id: The external ID of the asset to label.
14211423
geojson_file_paths: List of file paths to the GeoJSON files to be processed.
1422-
Each file should contain a FeatureCollection with features that have
1424+
Each file should contain a FeatureCollection with features that optionally have
14231425
'kili' metadata in their properties, including 'job', 'type', and 'categories'.
14241426
14251427
Note:
1426-
The GeoJSON features must contain a 'kili' property with the following structure:
1428+
The GeoJSON features can contain a 'kili' property with the following structure:
14271429
- 'job': The name of the job in the Kili project
14281430
- 'type': The annotation type ('marker', 'polyline', 'semantic', etc.)
14291431
- 'categories': List of category objects with 'name' field
14301432
1433+
If 'kili' properties are missing, they will be automatically mapped based on:
1434+
- Point geometries → first 'marker' job
1435+
- LineString geometries → first 'polyline' job
1436+
- Polygon geometries → first 'polygon' job, or 'semantic' if no polygon job found
1437+
- MultiPolygon geometries → first job with 'semantic' tool
1438+
14311439
Supported geometries: Point, LineString, Polygon, and MultiPolygon.
1432-
Polygon and MultiPolygon are always mapped to semantic segmentation jobs in Kili.
14331440
14341441
Examples:
14351442
>>> kili.append_labels_from_geojson_files(
@@ -1438,13 +1445,23 @@ def append_labels_from_geojson_files(
14381445
geojson_file_paths=["annotations.geojson"]
14391446
)
14401447
"""
1448+
json_interface = self.kili_api_gateway.get_project(
1449+
ProjectId(project_id), ("jsonInterface",)
1450+
)["jsonInterface"]
1451+
14411452
merged_json_response = {}
14421453

14431454
for file_path in geojson_file_paths:
14441455
with open(file_path, encoding="utf-8") as f:
14451456
feature_collection = json.load(f)
14461457

1447-
json_response = geojson_feature_collection_to_kili_json_response(feature_collection)
1458+
enriched_feature_collection = enrich_geojson_with_kili_properties(
1459+
feature_collection, json_interface
1460+
)
1461+
1462+
json_response = geojson_feature_collection_to_kili_json_response(
1463+
enriched_feature_collection
1464+
)
14481465

14491466
for job_name, job_data in json_response.items():
14501467
if job_name not in merged_json_response:

src/kili/utils/labels/geojson.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from typing import Dict
2+
3+
4+
def enrich_geojson_with_kili_properties(feature_collection: Dict, json_interface: Dict) -> Dict:
5+
"""Enrich GeoJSON features with kili properties when missing.
6+
7+
Args:
8+
feature_collection: The GeoJSON feature collection to enrich
9+
json_interface: The project's JSON interface containing job definitions
10+
11+
Returns:
12+
The enriched feature collection with kili properties added where needed
13+
"""
14+
object_detection_jobs = {}
15+
if "jobs" in json_interface:
16+
for job_name, job_config in json_interface["jobs"].items():
17+
if job_config.get("mlTask") == "OBJECT_DETECTION":
18+
object_detection_jobs[job_name] = job_config
19+
20+
marker_job = None
21+
polyline_job = None
22+
polygon_job = None
23+
semantic_job = None
24+
25+
for job_name, job_config in object_detection_jobs.items():
26+
tools = job_config.get("tools", [])
27+
if "marker" in tools and marker_job is None:
28+
marker_job = (job_name, job_config)
29+
if "polyline" in tools and polyline_job is None:
30+
polyline_job = (job_name, job_config)
31+
if "polygon" in tools and polygon_job is None:
32+
polygon_job = (job_name, job_config)
33+
if "semantic" in tools and semantic_job is None:
34+
semantic_job = (job_name, job_config)
35+
36+
def get_first_category_name(job_config):
37+
categories = job_config.get("content", {}).get("categories", {})
38+
if categories:
39+
first_category_key = next(iter(categories.keys()))
40+
return first_category_key
41+
return None
42+
43+
def create_kili_property(job_name, job_config, annotation_type):
44+
category_name = get_first_category_name(job_config)
45+
if category_name:
46+
return {
47+
"kili": {
48+
"children": {},
49+
"categories": [{"name": category_name}],
50+
"type": annotation_type,
51+
"job": job_name,
52+
}
53+
}
54+
return None
55+
56+
enriched_features = []
57+
58+
for feature in feature_collection.get("features", []):
59+
# Skip if feature already has kili properties
60+
if feature.get("properties", {}).get("kili") is not None:
61+
enriched_features.append(feature)
62+
continue
63+
64+
# Skip features with null geometry unless they have kili properties
65+
if feature.get("geometry") is None:
66+
continue
67+
68+
geometry_type = feature.get("geometry", {}).get("type")
69+
kili_property = None
70+
71+
if geometry_type == "Point" and marker_job:
72+
job_name, job_config = marker_job
73+
kili_property = create_kili_property(job_name, job_config, "marker")
74+
75+
elif geometry_type == "LineString" and polyline_job:
76+
job_name, job_config = polyline_job
77+
kili_property = create_kili_property(job_name, job_config, "polyline")
78+
79+
elif geometry_type == "Polygon":
80+
if polygon_job:
81+
job_name, job_config = polygon_job
82+
kili_property = create_kili_property(job_name, job_config, "polygon")
83+
elif semantic_job:
84+
job_name, job_config = semantic_job
85+
kili_property = create_kili_property(job_name, job_config, "semantic")
86+
87+
elif geometry_type == "MultiPolygon" and semantic_job:
88+
job_name, job_config = semantic_job
89+
kili_property = create_kili_property(job_name, job_config, "semantic")
90+
91+
if kili_property:
92+
if "properties" not in feature:
93+
feature["properties"] = {}
94+
feature["properties"].update(kili_property)
95+
enriched_features.append(feature)
96+
else:
97+
continue
98+
99+
enriched_collection = feature_collection.copy()
100+
enriched_collection["features"] = enriched_features
101+
return enriched_collection

0 commit comments

Comments
 (0)