3lc-compute-plugin-sdk 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 3lc_compute_plugin_sdk-0.2.2.dist-info/METADATA +81 -0
- 3lc_compute_plugin_sdk-0.2.2.dist-info/RECORD +28 -0
- 3lc_compute_plugin_sdk-0.2.2.dist-info/WHEEL +4 -0
- 3lc_compute_plugin_sdk-0.2.2.dist-info/licenses/LICENSE +202 -0
- tlc_plugin_sdk/__init__.py +58 -0
- tlc_plugin_sdk/asgi_app.py +103 -0
- tlc_plugin_sdk/contract/plugin-api.d.ts +429 -0
- tlc_plugin_sdk/contract.py +108 -0
- tlc_plugin_sdk/job_context.py +120 -0
- tlc_plugin_sdk/py.typed +0 -0
- tlc_plugin_sdk/shared/__init__.py +11 -0
- tlc_plugin_sdk/shared/alias_override_ui.py +175 -0
- tlc_plugin_sdk/shared/alias_ui.py +198 -0
- tlc_plugin_sdk/shared/aliases.py +227 -0
- tlc_plugin_sdk/shared/config_store.py +140 -0
- tlc_plugin_sdk/shared/config_ui.py +256 -0
- tlc_plugin_sdk/shared/data_source_routes.py +221 -0
- tlc_plugin_sdk/shared/data_source_ui.py +365 -0
- tlc_plugin_sdk/shared/generic_job.py +58 -0
- tlc_plugin_sdk/shared/images.py +350 -0
- tlc_plugin_sdk/shared/job_tracker.py +174 -0
- tlc_plugin_sdk/shared/labels.py +247 -0
- tlc_plugin_sdk/shared/modality.py +551 -0
- tlc_plugin_sdk/shared/model_storage.py +160 -0
- tlc_plugin_sdk/shared/naming.py +84 -0
- tlc_plugin_sdk/shared/ui_inject.py +63 -0
- tlc_plugin_sdk/shared/url_utils.py +112 -0
- tlc_plugin_sdk/worker.py +295 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# Copyright 2026 3LC Inc.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shared modality detection — single source of truth for all table/schema inspection.
|
|
4
|
+
|
|
5
|
+
Detects whether a table represents a detection, classification, segmentation,
|
|
6
|
+
pose, or OBB task by walking the schema tree. Used by insights, training, and
|
|
7
|
+
auto-labeling plugins.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Canonical sample_type → modality mapping
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_SAMPLE_TYPE_MAP: dict[str, str] = {
|
|
24
|
+
"bounding_boxes": "detection",
|
|
25
|
+
"bounding_box": "detection",
|
|
26
|
+
"instance_segmentation_masks": "segmentation",
|
|
27
|
+
"instance_segmentation": "segmentation",
|
|
28
|
+
"semantic_segmentation": "segmentation",
|
|
29
|
+
"segmentation_masks": "segmentation",
|
|
30
|
+
"segmentation": "segmentation",
|
|
31
|
+
"categorical_label": "classification",
|
|
32
|
+
"classification": "classification",
|
|
33
|
+
"pose": "pose",
|
|
34
|
+
"keypoints": "pose",
|
|
35
|
+
"obb": "obb",
|
|
36
|
+
"oriented_bounding_boxes": "obb",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ModalityInfo:
|
|
42
|
+
"""Result of modality detection."""
|
|
43
|
+
|
|
44
|
+
modality: str = "unknown"
|
|
45
|
+
"""Detected modality: detection, segmentation, classification, pose, obb, or unknown."""
|
|
46
|
+
|
|
47
|
+
gt_col: str | None = None
|
|
48
|
+
"""Ground-truth column name (e.g. ``bbs``, ``segmentations``, ``label``)."""
|
|
49
|
+
|
|
50
|
+
pred_col: str | None = None
|
|
51
|
+
"""Prediction column name (e.g. ``bbs_predicted``, ``predicted``)."""
|
|
52
|
+
|
|
53
|
+
class_names: dict[str, str] = field(default_factory=dict)
|
|
54
|
+
"""Class index → display name mapping."""
|
|
55
|
+
|
|
56
|
+
image_column: str | None = None
|
|
57
|
+
"""Image column name (if detected — first one found)."""
|
|
58
|
+
|
|
59
|
+
image_columns: list[str] = field(default_factory=list)
|
|
60
|
+
"""All image columns detected in the schema."""
|
|
61
|
+
|
|
62
|
+
label_column: str | None = None
|
|
63
|
+
"""Label column name for classification tasks."""
|
|
64
|
+
|
|
65
|
+
num_classes: int = 0
|
|
66
|
+
"""Number of classes detected from value maps."""
|
|
67
|
+
|
|
68
|
+
classification_columns: list[str] = field(default_factory=list)
|
|
69
|
+
"""All columns identified as classification-type."""
|
|
70
|
+
|
|
71
|
+
detection_columns: list[str] = field(default_factory=list)
|
|
72
|
+
"""All columns identified as detection-type."""
|
|
73
|
+
|
|
74
|
+
segmentation_columns: list[str] = field(default_factory=list)
|
|
75
|
+
"""All columns identified as segmentation-type."""
|
|
76
|
+
|
|
77
|
+
all_columns: list[str] = field(default_factory=list)
|
|
78
|
+
"""All column names in the schema."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Core detection from a schema object
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def detect_modality_from_schema(schema: Any) -> ModalityInfo:
|
|
87
|
+
"""Walk a schema object and detect modality.
|
|
88
|
+
|
|
89
|
+
This is the core detection function. It does not load any table or URL —
|
|
90
|
+
callers pass in ``table.rows_schema`` directly.
|
|
91
|
+
|
|
92
|
+
Detection priority:
|
|
93
|
+
1. ``sample_type`` attribute (canonical SDK identifier)
|
|
94
|
+
2. Nested schema keys (``bb_list``, ``rles``, ``instance_properties``)
|
|
95
|
+
3. Column name conventions (``bbs``, ``segmentations``, ``masks``)
|
|
96
|
+
4. Python class names (``BoundingBoxListValue``, ``CategoricalLabel``)
|
|
97
|
+
5. ``value.type`` string, ``number_role``, ``description``
|
|
98
|
+
6. Heuristic: image column + label map → classification
|
|
99
|
+
7. JSON fallback (``schema.to_json()``) if Python introspection fails
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
schema: A 3LC schema object with a ``values`` dict of column schemas.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
ModalityInfo with all detected fields.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
info = ModalityInfo()
|
|
109
|
+
|
|
110
|
+
if not schema or not hasattr(schema, "values"):
|
|
111
|
+
return info
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
return _detect_from_python(schema, info)
|
|
115
|
+
except Exception:
|
|
116
|
+
logger.debug("Python schema introspection failed, trying JSON fallback", exc_info=True)
|
|
117
|
+
try:
|
|
118
|
+
return _detect_from_json(schema, info)
|
|
119
|
+
except Exception:
|
|
120
|
+
logger.debug("JSON fallback also failed", exc_info=True)
|
|
121
|
+
return info
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _detect_from_python(schema: Any, info: ModalityInfo) -> ModalityInfo:
|
|
125
|
+
"""Detect modality using Python attribute access on the schema."""
|
|
126
|
+
values = schema.values
|
|
127
|
+
info.all_columns = sorted(values.keys())
|
|
128
|
+
|
|
129
|
+
best_cls_col: str | None = None
|
|
130
|
+
best_det_col: str | None = None
|
|
131
|
+
best_seg_col: str | None = None
|
|
132
|
+
|
|
133
|
+
for key, col_schema in values.items():
|
|
134
|
+
# 1. Check column-level sample_type
|
|
135
|
+
col_task = _sample_type_to_modality(_get_sample_type(col_schema))
|
|
136
|
+
if col_task:
|
|
137
|
+
_assign_column(info, col_task, key)
|
|
138
|
+
if col_task == "classification" and best_cls_col is None:
|
|
139
|
+
best_cls_col = key
|
|
140
|
+
elif col_task == "detection" and best_det_col is None:
|
|
141
|
+
best_det_col = key
|
|
142
|
+
elif col_task == "segmentation" and best_seg_col is None:
|
|
143
|
+
best_seg_col = key
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# 1b. Walk nested schemas for sample_type (e.g. instance_properties)
|
|
147
|
+
if hasattr(col_schema, "values") and isinstance(col_schema.values, dict):
|
|
148
|
+
nested_hit = False
|
|
149
|
+
for _sk, sub_schema in col_schema.values.items():
|
|
150
|
+
sub_task = _sample_type_to_modality(_get_sample_type(sub_schema))
|
|
151
|
+
if sub_task:
|
|
152
|
+
_assign_column(info, sub_task, key)
|
|
153
|
+
if sub_task == "classification" and best_cls_col is None:
|
|
154
|
+
best_cls_col = key
|
|
155
|
+
elif sub_task == "detection" and best_det_col is None:
|
|
156
|
+
best_det_col = key
|
|
157
|
+
elif sub_task == "segmentation" and best_seg_col is None:
|
|
158
|
+
best_seg_col = key
|
|
159
|
+
nested_hit = True
|
|
160
|
+
break
|
|
161
|
+
if nested_hit:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# 2. Nested schema keys: bb_list, rles, instance_properties, oriented_bbs, keypoints
|
|
165
|
+
if hasattr(col_schema, "values") and isinstance(col_schema.values, dict):
|
|
166
|
+
nested_keys = col_schema.values
|
|
167
|
+
if "rles" in nested_keys or "instance_properties" in nested_keys:
|
|
168
|
+
info.segmentation_columns.append(key)
|
|
169
|
+
if best_seg_col is None:
|
|
170
|
+
best_seg_col = key
|
|
171
|
+
continue
|
|
172
|
+
if "bb_list" in nested_keys:
|
|
173
|
+
info.detection_columns.append(key)
|
|
174
|
+
if best_det_col is None:
|
|
175
|
+
best_det_col = key
|
|
176
|
+
continue
|
|
177
|
+
# Check nested instances.values for pose/obb keys
|
|
178
|
+
all_nested = set(nested_keys.keys())
|
|
179
|
+
instances_sub = nested_keys.get("instances")
|
|
180
|
+
if instances_sub and hasattr(instances_sub, "values") and isinstance(instances_sub.values, dict):
|
|
181
|
+
all_nested.update(instances_sub.values.keys())
|
|
182
|
+
for nk in all_nested:
|
|
183
|
+
nkl = nk.lower()
|
|
184
|
+
if "oriented_bb" in nkl or nkl == "obb":
|
|
185
|
+
info.modality = "obb"
|
|
186
|
+
info.gt_col = key
|
|
187
|
+
return info
|
|
188
|
+
if "keypoint" in nkl or "vertices_2d" in nkl or "vertices_3d" in nkl:
|
|
189
|
+
info.modality = "pose"
|
|
190
|
+
info.gt_col = key
|
|
191
|
+
return info
|
|
192
|
+
|
|
193
|
+
# 2b. display_name / internal_name hints
|
|
194
|
+
display_name = (getattr(col_schema, "display_name", "") or "").lower()
|
|
195
|
+
internal_name = (getattr(col_schema, "internal_name", "") or "").lower()
|
|
196
|
+
for hint_name in (display_name, internal_name):
|
|
197
|
+
if not hint_name:
|
|
198
|
+
continue
|
|
199
|
+
if hint_name == "obb" or "oriented_b" in hint_name:
|
|
200
|
+
info.modality = "obb"
|
|
201
|
+
info.gt_col = key
|
|
202
|
+
return info
|
|
203
|
+
if "keypoint" in hint_name or "pose" in hint_name:
|
|
204
|
+
info.modality = "pose"
|
|
205
|
+
info.gt_col = key
|
|
206
|
+
return info
|
|
207
|
+
|
|
208
|
+
# 3. Column name conventions
|
|
209
|
+
col_name_mod = _column_name_to_modality(key)
|
|
210
|
+
if col_name_mod == "segmentation":
|
|
211
|
+
info.segmentation_columns.append(key)
|
|
212
|
+
if best_seg_col is None:
|
|
213
|
+
best_seg_col = key
|
|
214
|
+
continue
|
|
215
|
+
if col_name_mod == "detection":
|
|
216
|
+
info.detection_columns.append(key)
|
|
217
|
+
if best_det_col is None:
|
|
218
|
+
best_det_col = key
|
|
219
|
+
continue
|
|
220
|
+
if col_name_mod in ("pose", "obb"):
|
|
221
|
+
info.modality = col_name_mod
|
|
222
|
+
info.gt_col = key
|
|
223
|
+
return info
|
|
224
|
+
|
|
225
|
+
if not hasattr(col_schema, "value"):
|
|
226
|
+
# Check description for bounding box hints
|
|
227
|
+
desc = getattr(col_schema, "description", "") or ""
|
|
228
|
+
if "bounding" in desc.lower():
|
|
229
|
+
info.detection_columns.append(key)
|
|
230
|
+
if best_det_col is None:
|
|
231
|
+
best_det_col = key
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
value_obj = col_schema.value
|
|
235
|
+
type_name = type(value_obj).__name__.lower()
|
|
236
|
+
|
|
237
|
+
# 4. Python class names
|
|
238
|
+
if "boundingbox" in type_name or "bounding_box" in type_name:
|
|
239
|
+
info.detection_columns.append(key)
|
|
240
|
+
if best_det_col is None:
|
|
241
|
+
best_det_col = key
|
|
242
|
+
continue
|
|
243
|
+
if "segmentation" in type_name or "mask" in type_name:
|
|
244
|
+
info.segmentation_columns.append(key)
|
|
245
|
+
if best_seg_col is None:
|
|
246
|
+
best_seg_col = key
|
|
247
|
+
continue
|
|
248
|
+
if "pose" in type_name or "keypoint" in type_name:
|
|
249
|
+
# pose/obb don't go into column lists — just set modality directly
|
|
250
|
+
info.modality = "pose"
|
|
251
|
+
info.gt_col = key
|
|
252
|
+
return info
|
|
253
|
+
|
|
254
|
+
# 5a. value.type string
|
|
255
|
+
if hasattr(value_obj, "type"):
|
|
256
|
+
vtype = str(value_obj.type).lower()
|
|
257
|
+
if "bounding_box" in vtype or "boundingbox" in vtype:
|
|
258
|
+
info.detection_columns.append(key)
|
|
259
|
+
if best_det_col is None:
|
|
260
|
+
best_det_col = key
|
|
261
|
+
continue
|
|
262
|
+
if "segmentation" in vtype or "mask" in vtype:
|
|
263
|
+
info.segmentation_columns.append(key)
|
|
264
|
+
if best_seg_col is None:
|
|
265
|
+
best_seg_col = key
|
|
266
|
+
continue
|
|
267
|
+
if "pose" in vtype or "keypoint" in vtype:
|
|
268
|
+
info.modality = "pose"
|
|
269
|
+
info.gt_col = key
|
|
270
|
+
return info
|
|
271
|
+
|
|
272
|
+
# 5b. sample_type on col_schema (not value) for classification
|
|
273
|
+
sample_type = getattr(col_schema, "sample_type", "") or ""
|
|
274
|
+
if "categorical" in str(sample_type).lower():
|
|
275
|
+
info.classification_columns.append(key)
|
|
276
|
+
if best_cls_col is None:
|
|
277
|
+
best_cls_col = key
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# 5c. number_role == "label"
|
|
281
|
+
number_role = getattr(value_obj, "number_role", "") or ""
|
|
282
|
+
if "label" in str(number_role).lower():
|
|
283
|
+
info.classification_columns.append(key)
|
|
284
|
+
if best_cls_col is None:
|
|
285
|
+
best_cls_col = key
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# 5d. value.map dict present with entries (classification)
|
|
289
|
+
value_map = getattr(value_obj, "map", None)
|
|
290
|
+
if isinstance(value_map, dict) and len(value_map) > 0:
|
|
291
|
+
info.classification_columns.append(key)
|
|
292
|
+
if best_cls_col is None:
|
|
293
|
+
best_cls_col = key
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# 5e. Description containing "bounding"
|
|
297
|
+
desc = getattr(col_schema, "description", "") or ""
|
|
298
|
+
if "bounding" in desc.lower():
|
|
299
|
+
info.detection_columns.append(key)
|
|
300
|
+
if best_det_col is None:
|
|
301
|
+
best_det_col = key
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
# Track image column
|
|
305
|
+
if hasattr(value_obj, "string_role"):
|
|
306
|
+
role = str(value_obj.string_role).lower()
|
|
307
|
+
if "image" in role:
|
|
308
|
+
info.image_columns.append(key)
|
|
309
|
+
if info.image_column is None:
|
|
310
|
+
info.image_column = key
|
|
311
|
+
|
|
312
|
+
# Priority: segmentation > detection > classification
|
|
313
|
+
if info.segmentation_columns:
|
|
314
|
+
info.modality = "segmentation"
|
|
315
|
+
info.gt_col = best_seg_col
|
|
316
|
+
elif info.detection_columns:
|
|
317
|
+
info.modality = "detection"
|
|
318
|
+
info.gt_col = best_det_col
|
|
319
|
+
elif info.classification_columns:
|
|
320
|
+
info.modality = "classification"
|
|
321
|
+
info.gt_col = best_cls_col
|
|
322
|
+
info.label_column = best_cls_col
|
|
323
|
+
# Assign pred_col by convention
|
|
324
|
+
if info.gt_col:
|
|
325
|
+
pred_candidate = info.gt_col + "_predicted"
|
|
326
|
+
if pred_candidate in values:
|
|
327
|
+
info.pred_col = pred_candidate
|
|
328
|
+
elif "predicted" in values:
|
|
329
|
+
info.pred_col = "predicted"
|
|
330
|
+
elif info.gt_col + "Predicted" in values:
|
|
331
|
+
info.pred_col = info.gt_col + "Predicted"
|
|
332
|
+
|
|
333
|
+
return info
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _detect_from_json(schema: Any, info: ModalityInfo) -> ModalityInfo:
|
|
337
|
+
"""Fallback: detect modality by parsing schema.to_json()."""
|
|
338
|
+
schema_json = json.loads(schema.to_json())
|
|
339
|
+
vals = schema_json.get("values", {})
|
|
340
|
+
info.all_columns = sorted(vals.keys())
|
|
341
|
+
|
|
342
|
+
for key, col_def in vals.items():
|
|
343
|
+
key_lower = key.lower()
|
|
344
|
+
nested_vals = col_def.get("values", {})
|
|
345
|
+
|
|
346
|
+
# Segmentation
|
|
347
|
+
if isinstance(nested_vals, dict) and ("rles" in nested_vals or "instance_properties" in nested_vals):
|
|
348
|
+
info.segmentation_columns.append(key)
|
|
349
|
+
if info.gt_col is None:
|
|
350
|
+
info.gt_col = key
|
|
351
|
+
continue
|
|
352
|
+
if key_lower in ("segmentations", "segmentation", "masks", "segmentations_predicted"):
|
|
353
|
+
info.segmentation_columns.append(key)
|
|
354
|
+
if info.gt_col is None:
|
|
355
|
+
info.gt_col = key
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
# Detection
|
|
359
|
+
if isinstance(nested_vals, dict) and "bb_list" in nested_vals:
|
|
360
|
+
info.detection_columns.append(key)
|
|
361
|
+
if info.gt_col is None:
|
|
362
|
+
info.gt_col = key
|
|
363
|
+
continue
|
|
364
|
+
if key_lower in ("bbs", "bounding_boxes"):
|
|
365
|
+
info.detection_columns.append(key)
|
|
366
|
+
if info.gt_col is None:
|
|
367
|
+
info.gt_col = key
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# Classification
|
|
371
|
+
if col_def.get("sample_type", "") == "categorical_label":
|
|
372
|
+
info.classification_columns.append(key)
|
|
373
|
+
if info.gt_col is None:
|
|
374
|
+
info.gt_col = key
|
|
375
|
+
continue
|
|
376
|
+
val_def = col_def.get("value", {})
|
|
377
|
+
if isinstance(val_def, dict):
|
|
378
|
+
if val_def.get("number_role", "") == "label":
|
|
379
|
+
info.classification_columns.append(key)
|
|
380
|
+
if info.gt_col is None:
|
|
381
|
+
info.gt_col = key
|
|
382
|
+
continue
|
|
383
|
+
if isinstance(val_def.get("map"), dict) and val_def["map"]:
|
|
384
|
+
info.classification_columns.append(key)
|
|
385
|
+
if info.gt_col is None:
|
|
386
|
+
info.gt_col = key
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
if info.segmentation_columns:
|
|
390
|
+
info.modality = "segmentation"
|
|
391
|
+
elif info.detection_columns:
|
|
392
|
+
info.modality = "detection"
|
|
393
|
+
elif info.classification_columns:
|
|
394
|
+
info.modality = "classification"
|
|
395
|
+
|
|
396
|
+
return info
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
# Higher-level entry points
|
|
401
|
+
# ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def detect_modality_from_table(table: Any) -> ModalityInfo:
|
|
405
|
+
"""Detect modality from a loaded table object.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
table: A loaded ``tlc.Table`` object.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
ModalityInfo with modality, columns, and class names extracted.
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
info = detect_modality_from_schema(table.rows_schema)
|
|
415
|
+
|
|
416
|
+
# Extract class names from value map
|
|
417
|
+
if info.gt_col:
|
|
418
|
+
info.class_names = _extract_class_names(table, info.gt_col)
|
|
419
|
+
if info.class_names:
|
|
420
|
+
info.num_classes = len(info.class_names)
|
|
421
|
+
|
|
422
|
+
return info
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def detect_modality_from_url(url: str) -> ModalityInfo:
|
|
426
|
+
"""Load a table from URL and detect its modality.
|
|
427
|
+
|
|
428
|
+
Convenience wrapper that loads the table first, then delegates to
|
|
429
|
+
``detect_modality_from_table``.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
url: 3LC table URL (will be normalized).
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
ModalityInfo with full detection results.
|
|
436
|
+
|
|
437
|
+
"""
|
|
438
|
+
import tlc
|
|
439
|
+
|
|
440
|
+
from tlc_plugin_sdk.shared.url_utils import normalize_url
|
|
441
|
+
|
|
442
|
+
table = tlc.Table.from_url(normalize_url(url))
|
|
443
|
+
return detect_modality_from_table(table)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def classify_metrics_columns(columns: set[str]) -> str:
|
|
447
|
+
"""Classify a metrics table by its column set.
|
|
448
|
+
|
|
449
|
+
Used by run statistics to quickly determine the modality of a metrics
|
|
450
|
+
table before doing full schema inspection.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
columns: Set of column names from the table.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
One of: classification, detection_aggregate, detection_sample,
|
|
457
|
+
segmentation_sample, or unknown.
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
if "example_id" in columns and "predicted" in columns:
|
|
461
|
+
return "classification"
|
|
462
|
+
if "label" in columns and ("mAP" in columns or "mAP50-95" in columns):
|
|
463
|
+
return "detection_aggregate"
|
|
464
|
+
if "segmentations_predicted" in columns:
|
|
465
|
+
return "segmentation_sample"
|
|
466
|
+
if "bbs_predicted" in columns:
|
|
467
|
+
return "detection_sample"
|
|
468
|
+
# Pose / keypoints
|
|
469
|
+
for c in columns:
|
|
470
|
+
cl = c.lower()
|
|
471
|
+
if "keypoint" in cl or cl.startswith("pose") or "vertices_2d" in cl or "vertices_3d" in cl:
|
|
472
|
+
return "pose"
|
|
473
|
+
if cl in ("obb", "oriented_bounding_boxes") or cl.startswith("obb_"):
|
|
474
|
+
return "obb"
|
|
475
|
+
return "unknown"
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# ---------------------------------------------------------------------------
|
|
479
|
+
# Helpers
|
|
480
|
+
# ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _get_sample_type(obj: Any) -> str:
|
|
484
|
+
"""Extract sample_type string from a schema object."""
|
|
485
|
+
st = getattr(obj, "sample_type", None)
|
|
486
|
+
if st and isinstance(st, str):
|
|
487
|
+
return st.lower()
|
|
488
|
+
return ""
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _sample_type_to_modality(sample_type: str) -> str:
|
|
492
|
+
"""Convert a sample_type string to a modality name, or empty string."""
|
|
493
|
+
if not sample_type:
|
|
494
|
+
return ""
|
|
495
|
+
if sample_type in _SAMPLE_TYPE_MAP:
|
|
496
|
+
return _SAMPLE_TYPE_MAP[sample_type]
|
|
497
|
+
for key, modality in _SAMPLE_TYPE_MAP.items():
|
|
498
|
+
if key in sample_type:
|
|
499
|
+
return modality
|
|
500
|
+
return ""
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _column_name_to_modality(col_name: str) -> str:
|
|
504
|
+
"""Detect modality from column name using conventions."""
|
|
505
|
+
try:
|
|
506
|
+
import tlc
|
|
507
|
+
|
|
508
|
+
if col_name == getattr(tlc, "BOUNDING_BOXES", "bbs") or col_name == "bbs":
|
|
509
|
+
return "detection"
|
|
510
|
+
if col_name == getattr(tlc, "SEGMENTATIONS", "segmentations") or col_name == "segmentations":
|
|
511
|
+
return "segmentation"
|
|
512
|
+
except ImportError:
|
|
513
|
+
pass
|
|
514
|
+
|
|
515
|
+
cl = col_name.lower()
|
|
516
|
+
if cl in ("bbs", "bounding_boxes", "bboxes"):
|
|
517
|
+
return "detection"
|
|
518
|
+
if cl in ("segmentations", "segmentation", "masks", "segmentations_predicted", "instance_segmentation"):
|
|
519
|
+
return "segmentation"
|
|
520
|
+
if "keypoint" in cl or cl.startswith("pose") or "vertices_2d" in cl or "vertices_3d" in cl:
|
|
521
|
+
return "pose"
|
|
522
|
+
if cl in ("obb", "oriented_bounding_boxes") or cl.startswith("obb_"):
|
|
523
|
+
return "obb"
|
|
524
|
+
return ""
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _assign_column(info: ModalityInfo, modality: str, col_name: str) -> None:
|
|
528
|
+
"""Add a column to the appropriate modality list in ModalityInfo."""
|
|
529
|
+
if modality == "detection":
|
|
530
|
+
info.detection_columns.append(col_name)
|
|
531
|
+
elif modality == "segmentation":
|
|
532
|
+
info.segmentation_columns.append(col_name)
|
|
533
|
+
elif modality == "classification":
|
|
534
|
+
info.classification_columns.append(col_name)
|
|
535
|
+
elif modality == "pose":
|
|
536
|
+
info.modality = "pose"
|
|
537
|
+
info.gt_col = col_name
|
|
538
|
+
elif modality == "obb":
|
|
539
|
+
info.modality = "obb"
|
|
540
|
+
info.gt_col = col_name
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _extract_class_names(table: Any, column_name: str) -> dict[str, str]:
|
|
544
|
+
"""Extract class names from a column's value map.
|
|
545
|
+
|
|
546
|
+
Tries the column directly, then nested instance-label paths (centralized
|
|
547
|
+
in ``tlc_plugin_sdk.shared.labels``).
|
|
548
|
+
"""
|
|
549
|
+
from tlc_plugin_sdk.shared.labels import get_label_map
|
|
550
|
+
|
|
551
|
+
return {str(index): name for index, name in get_label_map(table, column_name).items()}
|