Skip to content

Commit 6123f30

Browse files
committed
feat(LAB-3719): add optional job and category, updated doc with 3 import modes
1 parent 48af72a commit 6123f30

2 files changed

Lines changed: 165 additions & 22 deletions

File tree

src/kili/presentation/client/label.py

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@
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
51+
from kili.utils.labels.geojson import (
52+
enrich_geojson_with_kili_properties,
53+
enrich_geojson_with_specific_mapping,
54+
)
5255
from kili.utils.labels.parsing import ParsedLabel
5356
from kili.utils.logcontext import for_all_methods, log_call
5457

@@ -1409,55 +1412,121 @@ def append_labels_from_geojson_files(
14091412
project_id: str,
14101413
asset_external_id: str,
14111414
geojson_file_paths: List[str],
1415+
job_names: Optional[List[str]] = None,
1416+
category_names: Optional[List[str]] = None,
14121417
):
14131418
"""Import and convert GeoJSON files into annotations for a specific asset in a Kili project.
14141419
14151420
This method processes GeoJSON feature collections, converts them to the appropriate
14161421
Kili annotation format, and appends them as labels to the specified asset.
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.
1422+
1423+
Three modes of import are supported:
1424+
1425+
1. **With `kili` properties**: GeoJSON features contain 'kili' metadata in their properties
1426+
with job, type, and category information.
1427+
1428+
2. **With specific job/category names**: Provide `job_names` and `category_names` to map
1429+
all compatible geometries from each file to the specified job and category.
1430+
1431+
3. **Automatic mapping**: When no 'kili' properties or specific names are provided,
1432+
geometries are automatically mapped based on type and available jobs.
14191433
14201434
Args:
14211435
project_id: The ID of the Kili project to add the labels to.
14221436
asset_external_id: The external ID of the asset to label.
14231437
geojson_file_paths: List of file paths to the GeoJSON files to be processed.
1424-
Each file should contain a FeatureCollection with features that optionally have
1425-
'kili' metadata in their properties, including 'job', 'type', and 'categories'.
1438+
Each file should contain a FeatureCollection with features.
1439+
job_names: Optional list of job names in the Kili project, one for each GeoJSON file.
1440+
When provided, all compatible geometries from the corresponding file will be
1441+
mapped to this job. Must have the same length as `geojson_file_paths`.
1442+
category_names: Optional list of category names, one for each GeoJSON file.
1443+
When provided, all geometries from the corresponding file will be assigned
1444+
to this category. Must have the same length as `geojson_file_paths`.
1445+
Each category must exist in the corresponding job's ontology.
14261446
14271447
Note:
1428-
The GeoJSON features can contain a 'kili' property with the following structure:
1429-
- 'job': The name of the job in the Kili project
1430-
- 'type': The annotation type ('marker', 'polyline', 'semantic', etc.)
1431-
- 'categories': List of category objects with 'name' field
1432-
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
1448+
**Geometry-to-job compatibility:**
1449+
- Point geometries → jobs with 'marker' tool
1450+
- LineString geometries → jobs with 'polyline' tool
1451+
- Polygon geometries → jobs with 'polygon' or 'semantic' tool
1452+
- MultiPolygon geometries → jobs with 'semantic' tool
1453+
1454+
**GeoJSON 'kili' properties structure (Mode 1):**
1455+
```json
1456+
{
1457+
"properties": {
1458+
"kili": {
1459+
"job": "job_name",
1460+
"type": "marker|polyline|polygon|semantic",
1461+
"categories": [{"name": "category_name"}]
1462+
}
1463+
}
1464+
}
1465+
```
14381466
1439-
Supported geometries: Point, LineString, Polygon, and MultiPolygon.
1467+
**Automatic mapping priority (Mode 3):**
1468+
- Point → first available 'marker' job
1469+
- LineString → first available 'polyline' job
1470+
- Polygon → first available 'polygon' job, fallback to 'semantic'
1471+
- MultiPolygon → first available 'semantic' job
14401472
14411473
Examples:
1474+
Mode 1 - With kili properties in GeoJSON:
14421475
>>> kili.append_labels_from_geojson_files(
14431476
project_id="project_id",
14441477
asset_external_id="asset_1",
14451478
geojson_file_paths=["annotations.geojson"]
14461479
)
1480+
1481+
Mode 2 - With specific job/category mapping:
1482+
>>> kili.append_labels_from_geojson_files(
1483+
project_id="project_id",
1484+
asset_external_id="asset_1",
1485+
geojson_file_paths=["points.geojson", "polygons.geojson"],
1486+
job_names=["MARKERS", "POLYGONS"],
1487+
category_names=["BUILDING", "ROAD"]
1488+
)
1489+
1490+
Mode 3 - Automatic mapping:
1491+
>>> kili.append_labels_from_geojson_files(
1492+
project_id="project_id",
1493+
asset_external_id="asset_1",
1494+
geojson_file_paths=["mixed_geometries.geojson"]
1495+
)
14471496
"""
1497+
if job_names is not None and category_names is not None:
1498+
if len(job_names) != len(geojson_file_paths):
1499+
raise ValueError("job_names must have the same length as geojson_file_paths")
1500+
if len(category_names) != len(geojson_file_paths):
1501+
raise ValueError("category_names must have the same length as geojson_file_paths")
1502+
if len(job_names) != len(category_names):
1503+
raise ValueError("job_names and category_names must have the same length")
1504+
elif job_names is not None or category_names is not None:
1505+
raise ValueError(
1506+
"Both job_names and category_names must be provided together, or both must be None"
1507+
)
1508+
14481509
json_interface = self.kili_api_gateway.get_project(
14491510
ProjectId(project_id), ("jsonInterface",)
14501511
)["jsonInterface"]
14511512

14521513
merged_json_response = {}
14531514

1454-
for file_path in geojson_file_paths:
1455-
with open(file_path, encoding="utf-8") as f:
1456-
feature_collection = json.load(f)
1515+
for file_index, file_path in enumerate(geojson_file_paths):
1516+
with open(file_path, encoding="utf-8") as file:
1517+
feature_collection = json.load(file)
14571518

1458-
enriched_feature_collection = enrich_geojson_with_kili_properties(
1459-
feature_collection, json_interface
1460-
)
1519+
if job_names is not None and category_names is not None:
1520+
enriched_feature_collection = enrich_geojson_with_specific_mapping(
1521+
feature_collection,
1522+
json_interface,
1523+
job_names[file_index],
1524+
category_names[file_index],
1525+
)
1526+
else:
1527+
enriched_feature_collection = enrich_geojson_with_kili_properties(
1528+
feature_collection, json_interface
1529+
)
14611530

14621531
json_response = geojson_feature_collection_to_kili_json_response(
14631532
enriched_feature_collection

src/kili/utils/labels/geojson.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,77 @@ def create_kili_property(job_name, job_config, annotation_type):
9999
enriched_collection = feature_collection.copy()
100100
enriched_collection["features"] = enriched_features
101101
return enriched_collection
102+
103+
104+
def enrich_geojson_with_specific_mapping(
105+
feature_collection: Dict, json_interface: Dict, target_job_name: str, target_category_name: str
106+
) -> Dict:
107+
"""Enrich GeoJSON features with specific job and category mapping.
108+
109+
Args:
110+
feature_collection: The GeoJSON feature collection to enrich
111+
json_interface: The project's JSON interface containing job definitions
112+
target_job_name: The specific job name to map geometries to
113+
target_category_name: The specific category name to assign to all geometries
114+
115+
Returns:
116+
The enriched feature collection with kili properties added
117+
"""
118+
target_job_config = None
119+
if "jobs" in json_interface:
120+
target_job_config = json_interface["jobs"].get(target_job_name)
121+
122+
if not target_job_config:
123+
raise ValueError(f"Job '{target_job_name}' not found in project")
124+
125+
if target_job_config.get("mlTask") != "OBJECT_DETECTION":
126+
raise ValueError(f"Job '{target_job_name}' is not an OBJECT_DETECTION job")
127+
128+
categories = target_job_config.get("content", {}).get("categories", {})
129+
if target_category_name not in categories:
130+
raise ValueError(f"Category '{target_category_name}' not found in job '{target_job_name}'")
131+
132+
tools = target_job_config.get("tools", [])
133+
134+
def create_kili_property(annotation_type: str):
135+
return {
136+
"kili": {
137+
"children": {},
138+
"categories": [{"name": target_category_name}],
139+
"type": annotation_type,
140+
"job": target_job_name,
141+
}
142+
}
143+
144+
enriched_features = []
145+
146+
for feature in feature_collection.get("features", []):
147+
# Skip features with null geometry
148+
if feature.get("geometry") is None:
149+
continue
150+
151+
geometry_type = feature.get("geometry", {}).get("type")
152+
kili_property = None
153+
154+
# Map geometry types to annotation types based on available tools
155+
if geometry_type == "Point" and "marker" in tools:
156+
kili_property = create_kili_property("marker")
157+
elif geometry_type == "LineString" and "polyline" in tools:
158+
kili_property = create_kili_property("polyline")
159+
elif geometry_type == "Polygon":
160+
if "polygon" in tools:
161+
kili_property = create_kili_property("polygon")
162+
elif "semantic" in tools:
163+
kili_property = create_kili_property("semantic")
164+
elif geometry_type == "MultiPolygon" and "semantic" in tools:
165+
kili_property = create_kili_property("semantic")
166+
167+
if kili_property:
168+
if "properties" not in feature:
169+
feature["properties"] = {}
170+
feature["properties"].update(kili_property)
171+
enriched_features.append(feature)
172+
173+
enriched_collection = feature_collection.copy()
174+
enriched_collection["features"] = enriched_features
175+
return enriched_collection

0 commit comments

Comments
 (0)