# Copyright 2026 3LC Inc.
# SPDX-License-Identifier: Apache-2.0
"""Shared helpers for discovering and reading class labels from 3LC tables.
Centralizes the label-handling pattern used across plugins. The actual value
map reading is core SDK API — ``table.get_value_map(path)`` and
``table.get_simple_value_map(path)`` (which canonicalize on
``MapElement.internal_name``). What core does not provide, and this module
centralizes, is *discovering the label value path*: the dot-path to the label
value differs by modality and table convention:
- classification: the label column itself (e.g. ``"label"``)
- detection: ``"{column}.instances_additional_data.label"`` (tlc 3.x) or
``"{column}.bb_list.label"`` (legacy tables)
- segmentation: ``"{column}.instance_properties.label"``
Porting to tlc core
-------------------
Like ``shared/images.py``, this belongs in the SDK next to
``Table.get_value_map``. Intended mapping: ``_find_label_path`` →
``Table.find_label_path(column=None)``; ``get_label_map`` /
``get_label_names`` → thin conveniences over ``Table.get_simple_value_map``.
Plugins import only the public helpers (``get_label_map`` / ``get_label_names`` /
``get_class_name_lookup`` / ``get_display_value_map`` / ``find_label_column``) from
this module, so the port touches exactly this file.
"""
from __future__ import annotations
from typing import Any
# Nested sub-paths where instance labels live, in preference order. Verified
# against tlc 3.1: BoundingBoxes2D produces instances_additional_data.label,
# SegmentationPolygons produces instance_properties.label; bb_list.label is
# kept for tables written by older SDKs.
_INSTANCE_LABEL_SUBPATHS = (
"instances_additional_data.label",
"bb_list.label",
"instance_properties.label",
)
# Conventional instance-column names to probe when no column is given.
_INSTANCE_COLUMN_CANDIDATES = (
"bbs",
"bbs_predicted",
"segmentations",
"segmentations_predicted",
"predicted_segmentations",
"segmentation",
"masks",
)
# Conventional plain (classification) label column, tried last.
_PLAIN_LABEL_COLUMN = "label"
def _candidate_label_paths(column: str | None = None) -> list[str]:
"""Enumerate candidate label value paths, most-specific first (internal).
Args:
column: An explicit column to search under. When given, only that
column (direct, then nested) is considered; otherwise the
conventional instance columns and the plain ``label`` column.
Returns:
Dot-paths suitable for ``table.get_value_map``.
"""
paths: list[str] = []
if column:
paths.append(column)
paths.extend(f"{column}.{sub}" for sub in _INSTANCE_LABEL_SUBPATHS)
return paths
for col in _INSTANCE_COLUMN_CANDIDATES:
paths.extend(f"{col}.{sub}" for sub in _INSTANCE_LABEL_SUBPATHS)
paths.append(_PLAIN_LABEL_COLUMN)
return paths
def _find_label_path(table: Any, column: str | None = None) -> str | None:
"""Find the dot-path to a table's label value map (internal).
Args:
table: A loaded ``tlc.Table``.
column: Optional column to restrict the search to.
Returns:
The first candidate path with a non-empty value map, or ``None``.
"""
for path in _candidate_label_paths(column):
try:
if table.get_value_map(path):
return path
except Exception:
continue
return None
[docs]
def get_label_map(table: Any, column: str | None = None, *, path: str | None = None) -> dict[int, str]:
"""Read a table's label map as ``{class index: internal name}``.
Thin wrapper over ``table.get_simple_value_map`` with path discovery.
Args:
table: A loaded ``tlc.Table``.
column: Optional column to restrict path discovery to.
path: Explicit value path; skips discovery when given.
Returns:
The label map, or ``{}`` if the table has none.
"""
label_path = path or _find_label_path(table, column)
if not label_path:
return {}
try:
simple = table.get_simple_value_map(label_path)
except Exception:
return {}
return dict(simple) if simple else {}
[docs]
def get_label_names(table: Any, column: str | None = None, *, path: str | None = None) -> list[str]:
"""Read a table's class names, ordered by class index.
Args:
table: A loaded ``tlc.Table``.
column: Optional column to restrict path discovery to.
path: Explicit value path; skips discovery when given.
Returns:
Class names in index order, or ``[]`` if the table has no label map.
"""
label_map = get_label_map(table, column, path=path)
return [label_map[k] for k in sorted(label_map)]
[docs]
def get_class_name_lookup(table: Any, column: str | None = None, *, path: str | None = None) -> dict[str, str]:
"""Read a table's label map as a string-keyed lookup table.
Keys include both the raw map key (``"1.0"``) and its integer form
(``"1"``), so callers can index with whichever flavor their data carries.
This is the shape the insights statistics pipeline consumes.
Args:
table: A loaded ``tlc.Table``.
column: Optional column to restrict path discovery to.
path: Explicit value path; skips discovery when given.
Returns:
``{class key (str): internal name}``, or ``{}`` if no label map.
"""
label_path = path or _find_label_path(table, column)
if not label_path:
return {}
try:
value_map = table.get_value_map(label_path)
except Exception:
return {}
if not value_map:
return {}
names: dict[str, str] = {}
for key, element in value_map.items():
names[str(key)] = _element_name(key, element)
try:
names.setdefault(str(int(float(key))), names[str(key)])
except (ValueError, TypeError):
pass
return names
[docs]
def get_display_value_map(table: Any, path: str) -> dict[float, str] | None:
"""Read a value map as ``{float key: display name}`` for UI purposes.
Unlike the label helpers above (which canonicalize on ``internal_name``,
matching core's ``get_simple_value_map``), this prefers ``display_name``
— the shape used when presenting categorical columns to users.
Args:
table: A loaded ``tlc.Table``.
path: Value path (column name or dot-path).
Returns:
The display map, or ``None`` if the path has no value map.
"""
try:
value_map = table.get_value_map(path)
except Exception:
return None
if not value_map:
return None
result: dict[float, str] = {}
for key, element in value_map.items():
name = _element_attr(element, "display_name") or _element_name(key, element)
result[float(key)] = name
return result or None
[docs]
def find_label_column(table: Any) -> str | None:
"""Find a top-level categorical label column in a table, if any.
A column qualifies if it carries a value map directly (categorical), or
as a name-based fallback, contains ``label`` in its name.
Args:
table: A loaded ``tlc.Table``.
Returns:
The column name, or ``None``.
"""
try:
columns = [str(c) for c in table.columns]
except Exception:
return None
for col in columns:
try:
if table.get_value_map(col):
return col
except Exception:
continue
for col in columns:
if "label" in col.lower():
return col
return None
def _element_name(key: Any, element: Any) -> str:
"""Canonical class name for a value-map element (internal_name first)."""
return (
_element_attr(element, "internal_name")
or _element_attr(element, "display_name")
or (str(element) if not isinstance(element, dict) else str(key))
)
def _element_attr(element: Any, attr: str) -> str | None:
"""Read an attribute from a MapElement or a raw dict element."""
if isinstance(element, dict):
value = element.get(attr)
else:
value = getattr(element, attr, None)
return str(value) if value else None