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.
@@ -0,0 +1,247 @@
1
+ # Copyright 2026 3LC Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Shared helpers for discovering and reading class labels from 3LC tables.
4
+
5
+ Centralizes the label-handling pattern used across plugins. The actual value
6
+ map reading is core SDK API — ``table.get_value_map(path)`` and
7
+ ``table.get_simple_value_map(path)`` (which canonicalize on
8
+ ``MapElement.internal_name``). What core does not provide, and this module
9
+ centralizes, is *discovering the label value path*: the dot-path to the label
10
+ value differs by modality and table convention:
11
+
12
+ - classification: the label column itself (e.g. ``"label"``)
13
+ - detection: ``"{column}.instances_additional_data.label"`` (tlc 3.x) or
14
+ ``"{column}.bb_list.label"`` (legacy tables)
15
+ - segmentation: ``"{column}.instance_properties.label"``
16
+
17
+ Porting to tlc core
18
+ -------------------
19
+ Like ``shared/images.py``, this belongs in the SDK next to
20
+ ``Table.get_value_map``. Intended mapping: ``find_label_path`` →
21
+ ``Table.find_label_path(column=None)``; ``get_label_map`` /
22
+ ``get_label_names`` → thin conveniences over ``Table.get_simple_value_map``.
23
+ Plugins import only from this module, so the port touches exactly this file.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+
30
+ # Nested sub-paths where instance labels live, in preference order. Verified
31
+ # against tlc 3.1: BoundingBoxes2D produces instances_additional_data.label,
32
+ # SegmentationPolygons produces instance_properties.label; bb_list.label is
33
+ # kept for tables written by older SDKs.
34
+ _INSTANCE_LABEL_SUBPATHS = (
35
+ "instances_additional_data.label",
36
+ "bb_list.label",
37
+ "instance_properties.label",
38
+ )
39
+
40
+ # Conventional instance-column names to probe when no column is given.
41
+ _INSTANCE_COLUMN_CANDIDATES = (
42
+ "bbs",
43
+ "bbs_predicted",
44
+ "segmentations",
45
+ "segmentations_predicted",
46
+ "predicted_segmentations",
47
+ "segmentation",
48
+ "masks",
49
+ )
50
+
51
+ # Conventional plain (classification) label column, tried last.
52
+ _PLAIN_LABEL_COLUMN = "label"
53
+
54
+
55
+ def candidate_label_paths(column: str | None = None) -> list[str]:
56
+ """Enumerate candidate label value paths, most-specific first.
57
+
58
+ Args:
59
+ column: An explicit column to search under. When given, only that
60
+ column (direct, then nested) is considered; otherwise the
61
+ conventional instance columns and the plain ``label`` column.
62
+
63
+ Returns:
64
+ Dot-paths suitable for ``table.get_value_map``.
65
+
66
+ """
67
+ paths: list[str] = []
68
+ if column:
69
+ paths.append(column)
70
+ paths.extend(f"{column}.{sub}" for sub in _INSTANCE_LABEL_SUBPATHS)
71
+ return paths
72
+
73
+ for col in _INSTANCE_COLUMN_CANDIDATES:
74
+ paths.extend(f"{col}.{sub}" for sub in _INSTANCE_LABEL_SUBPATHS)
75
+ paths.append(_PLAIN_LABEL_COLUMN)
76
+ return paths
77
+
78
+
79
+ def find_label_path(table: Any, column: str | None = None) -> str | None:
80
+ """Find the dot-path to a table's label value map.
81
+
82
+ Args:
83
+ table: A loaded ``tlc.Table``.
84
+ column: Optional column to restrict the search to.
85
+
86
+ Returns:
87
+ The first candidate path with a non-empty value map, or ``None``.
88
+
89
+ """
90
+ for path in candidate_label_paths(column):
91
+ try:
92
+ if table.get_value_map(path):
93
+ return path
94
+ except Exception:
95
+ continue
96
+ return None
97
+
98
+
99
+ def get_label_map(table: Any, column: str | None = None, *, path: str | None = None) -> dict[int, str]:
100
+ """Read a table's label map as ``{class index: internal name}``.
101
+
102
+ Thin wrapper over ``table.get_simple_value_map`` with path discovery.
103
+
104
+ Args:
105
+ table: A loaded ``tlc.Table``.
106
+ column: Optional column to restrict path discovery to.
107
+ path: Explicit value path; skips discovery when given.
108
+
109
+ Returns:
110
+ The label map, or ``{}`` if the table has none.
111
+
112
+ """
113
+ label_path = path or find_label_path(table, column)
114
+ if not label_path:
115
+ return {}
116
+ try:
117
+ simple = table.get_simple_value_map(label_path)
118
+ except Exception:
119
+ return {}
120
+ return dict(simple) if simple else {}
121
+
122
+
123
+ def get_label_names(table: Any, column: str | None = None, *, path: str | None = None) -> list[str]:
124
+ """Read a table's class names, ordered by class index.
125
+
126
+ Args:
127
+ table: A loaded ``tlc.Table``.
128
+ column: Optional column to restrict path discovery to.
129
+ path: Explicit value path; skips discovery when given.
130
+
131
+ Returns:
132
+ Class names in index order, or ``[]`` if the table has no label map.
133
+
134
+ """
135
+ label_map = get_label_map(table, column, path=path)
136
+ return [label_map[k] for k in sorted(label_map)]
137
+
138
+
139
+ def get_class_name_lookup(table: Any, column: str | None = None, *, path: str | None = None) -> dict[str, str]:
140
+ """Read a table's label map as a string-keyed lookup table.
141
+
142
+ Keys include both the raw map key (``"1.0"``) and its integer form
143
+ (``"1"``), so callers can index with whichever flavor their data carries.
144
+ This is the shape the insights statistics pipeline consumes.
145
+
146
+ Args:
147
+ table: A loaded ``tlc.Table``.
148
+ column: Optional column to restrict path discovery to.
149
+ path: Explicit value path; skips discovery when given.
150
+
151
+ Returns:
152
+ ``{class key (str): internal name}``, or ``{}`` if no label map.
153
+
154
+ """
155
+ label_path = path or find_label_path(table, column)
156
+ if not label_path:
157
+ return {}
158
+ try:
159
+ value_map = table.get_value_map(label_path)
160
+ except Exception:
161
+ return {}
162
+ if not value_map:
163
+ return {}
164
+
165
+ names: dict[str, str] = {}
166
+ for key, element in value_map.items():
167
+ names[str(key)] = _element_name(key, element)
168
+ try:
169
+ names.setdefault(str(int(float(key))), names[str(key)])
170
+ except (ValueError, TypeError):
171
+ pass
172
+ return names
173
+
174
+
175
+ def get_display_value_map(table: Any, path: str) -> dict[float, str] | None:
176
+ """Read a value map as ``{float key: display name}`` for UI purposes.
177
+
178
+ Unlike the label helpers above (which canonicalize on ``internal_name``,
179
+ matching core's ``get_simple_value_map``), this prefers ``display_name``
180
+ — the shape used when presenting categorical columns to users.
181
+
182
+ Args:
183
+ table: A loaded ``tlc.Table``.
184
+ path: Value path (column name or dot-path).
185
+
186
+ Returns:
187
+ The display map, or ``None`` if the path has no value map.
188
+
189
+ """
190
+ try:
191
+ value_map = table.get_value_map(path)
192
+ except Exception:
193
+ return None
194
+ if not value_map:
195
+ return None
196
+ result: dict[float, str] = {}
197
+ for key, element in value_map.items():
198
+ name = _element_attr(element, "display_name") or _element_name(key, element)
199
+ result[float(key)] = name
200
+ return result or None
201
+
202
+
203
+ def find_label_column(table: Any) -> str | None:
204
+ """Find a top-level categorical label column in a table, if any.
205
+
206
+ A column qualifies if it carries a value map directly (categorical), or
207
+ as a name-based fallback, contains ``label`` in its name.
208
+
209
+ Args:
210
+ table: A loaded ``tlc.Table``.
211
+
212
+ Returns:
213
+ The column name, or ``None``.
214
+
215
+ """
216
+ try:
217
+ columns = [str(c) for c in table.columns]
218
+ except Exception:
219
+ return None
220
+ for col in columns:
221
+ try:
222
+ if table.get_value_map(col):
223
+ return col
224
+ except Exception:
225
+ continue
226
+ for col in columns:
227
+ if "label" in col.lower():
228
+ return col
229
+ return None
230
+
231
+
232
+ def _element_name(key: Any, element: Any) -> str:
233
+ """Canonical class name for a value-map element (internal_name first)."""
234
+ return (
235
+ _element_attr(element, "internal_name")
236
+ or _element_attr(element, "display_name")
237
+ or (str(element) if not isinstance(element, dict) else str(key))
238
+ )
239
+
240
+
241
+ def _element_attr(element: Any, attr: str) -> str | None:
242
+ """Read an attribute from a MapElement or a raw dict element."""
243
+ if isinstance(element, dict):
244
+ value = element.get(attr)
245
+ else:
246
+ value = getattr(element, attr, None)
247
+ return str(value) if value else None