tiff
sleap_io.io.tiff
¶
TIFF I/O for LabelImage data.
Reads and writes integer label images as TIFF files (single, multi-page stacks, or directories of per-frame TIFFs) with optional JSON sidecar metadata for track names and categories.
Pages in a TIFF can represent either time (one frame per page) or classes
(one class per page, single frame). The reader distinguishes these layouts
via, in priority order: an explicit pages_as argument, a sidecar
"axes" hint, TIFF-level metadata (OME-XML, ImageJ hyperstack), and
finally a fall-back assumption of pages-as-time for plain multi-page files.
Functions:
| Name | Description |
|---|---|
read_label_images |
Read label images from TIFF file(s). |
write_label_images |
Write label images to TIFF. |
Attributes:
| Name | Type | Description |
|---|---|---|
TYPE_CHECKING |
Returns True when the argument is true, False otherwise. |
|
__cached__ |
str(object='') -> str |
|
__doc__ |
str(object='') -> str |
|
__file__ |
str(object='') -> str |
|
__name__ |
str(object='') -> str |
|
__package__ |
str(object='') -> str |
TYPE_CHECKING = False
module-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/io/__pycache__/tiff.cpython-313.pyc'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__doc__ = 'TIFF I/O for LabelImage data.\n\nReads and writes integer label images as TIFF files (single, multi-page stacks,\nor directories of per-frame TIFFs) with optional JSON sidecar metadata for track\nnames and categories.\n\nPages in a TIFF can represent either time (one frame per page) or classes\n(one class per page, single frame). The reader distinguishes these layouts\nvia, in priority order: an explicit ``pages_as`` argument, a sidecar\n``"axes"`` hint, TIFF-level metadata (OME-XML, ImageJ hyperstack), and\nfinally a fall-back assumption of pages-as-time for plain multi-page files.\n'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__file__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/io/tiff.py'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__name__ = 'sleap_io.io.tiff'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__package__ = 'sleap_io.io'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
read_label_images(path, video=None, tracks=None, categories=None, pages_as='auto')
¶
Read label images from TIFF file(s).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
One of:
|
required |
video
|
Video | None
|
Video to associate with all frames. |
None
|
tracks
|
dict[int, Track] | None
|
Global |
None
|
categories
|
list[str] | dict[int, str] | None
|
Category strings.
|
None
|
pages_as
|
str
|
How to interpret multi-page TIFFs.
|
'auto'
|
Returns:
| Type | Description |
|---|---|
list[LabelImage]
|
List of |
Raises:
| Type | Description |
|---|---|
ValueError
|
For unknown |
Source code in sleap_io/io/tiff.py
def read_label_images(
path: str | Path,
video: "Video | None" = None,
tracks: "dict[int, Track] | None" = None,
categories: "list[str] | dict[int, str] | None" = None,
pages_as: str = "auto",
) -> list["LabelImage"]:
"""Read label images from TIFF file(s).
Args:
path: One of:
- Multi-page TIFF stack: one page per frame (or per class, see
``pages_as``).
- Single TIFF: one frame.
- Directory of TIFFs: sorted alphanumerically, one per frame.
video: Video to associate with all frames.
tracks: Global ``label_id -> Track`` mapping. If ``None``, auto-creates
one ``Track`` per unique ID found across all frames. Ignored for
class-stacked layouts.
categories: Category strings.
- ``dict[int, str]`` — keyed by label ID (time mode).
- ``list[str]`` — positional, one per class (class mode).
- ``None`` — read from sidecar if present.
pages_as: How to interpret multi-page TIFFs.
- ``"auto"`` (default): consult sidecar ``"axes"``, then TIFF
metadata (OME-XML / ImageJ hyperstack). Falls back to
``"time"`` for plain multi-page TIFFs with a one-time warning.
- ``"time"``: force each page to be one frame (N pages -> N
``LabelImage`` objects).
- ``"classes"``: force pages to be per-class binary masks for a
single frame (N pages -> 1 ``LabelImage`` with label IDs 1..N).
Returns:
List of ``LabelImage``, one per frame, sorted by frame index.
Raises:
ValueError: For unknown ``pages_as`` values or unreadable pages
(non-2D, negative values, etc.).
"""
import tifffile
if pages_as not in ("auto", "time", "classes"):
raise ValueError(
f"pages_as must be 'auto', 'time', or 'classes'; got {pages_as!r}."
)
path = Path(path)
sidecar = _read_sidecar(path)
# Read spatial metadata from sidecar (v2+)
sidecar_scale: tuple[float, float] = (1.0, 1.0)
sidecar_offset: tuple[float, float] = (0.0, 0.0)
if sidecar is not None:
if "scale" in sidecar:
s = sidecar["scale"]
sidecar_scale = (float(s[0]), float(s[1]))
if "offset" in sidecar:
o = sidecar["offset"]
sidecar_offset = (float(o[0]), float(o[1]))
# --- Directory input ------------------------------------------------
if path.is_dir():
tiff_files = sorted(list(path.glob("*.tif")) + list(path.glob("*.tiff")))
if not tiff_files:
return []
frames_data: list[np.ndarray] = []
for tiff_path in tiff_files:
data = tifffile.imread(str(tiff_path)).astype(np.int32)
if data.ndim != 2:
raise ValueError(
f"Expected 2D array from {tiff_path}, got shape {data.shape}"
)
frames_data.append(data)
if pages_as == "classes":
return _read_single_class_stack(
frames_data, categories, sidecar, sidecar_scale, sidecar_offset
)
return _read_pages_as_time(
frames_data,
tracks,
categories,
sidecar,
sidecar_scale,
sidecar_offset,
)
# --- Single file (possibly multi-page) ------------------------------
# Decide layout. Priority: explicit pages_as -> sidecar axes -> TIFF
# metadata -> fallback ('time' with warning for plain multi-page).
sidecar_axes = None
if sidecar is not None and "axes" in sidecar:
sidecar_axes = _normalize_axes(sidecar["axes"])
tiff_axes, n_pages, has_metadata = _infer_tiff_axes(path)
if pages_as == "time":
layout = "TYX"
elif pages_as == "classes":
layout = "CYX"
elif sidecar_axes and sidecar_axes != "unknown":
layout = sidecar_axes
elif tiff_axes != "unknown":
layout = tiff_axes
else:
layout = "TYX" # fallback
# Read series data once for authoritative layouts (OME/ImageJ declare
# the full shape via series rather than per-page iteration).
def _iter_pages() -> list[np.ndarray]:
with tifffile.TiffFile(str(path)) as tif:
out = []
for page in tif.pages:
arr = page.asarray().astype(np.int32)
if arr.ndim != 2:
raise ValueError(
f"Expected 2D page in {path}, got shape {arr.shape}"
)
out.append(arr)
return out
def _read_series() -> np.ndarray:
with tifffile.TiffFile(str(path)) as tif:
return tif.series[0].asarray()
# --- Dispatch on layout ---------------------------------------------
if layout in ("YX", "TYX"):
frames_data = _iter_pages()
# Warn when we're falling back on an ambiguous plain multi-page,
# but only if the pages could plausibly be a binary class stack.
# Multi-valued integer pages rule out the class-stack reading, so
# the fallback is the only sensible interpretation and suggesting
# pages_as='classes' would be misleading.
used_fallback = (
pages_as == "auto" and sidecar_axes is None and tiff_axes == "unknown"
)
if used_fallback and _pages_could_be_class_stack(frames_data):
dtype_name = "unknown"
with tifffile.TiffFile(str(path)) as tif:
if tif.pages:
dtype_name = str(tif.pages[0].dtype)
_warn_ambiguous_pages(path, n_pages, dtype_name)
return _read_pages_as_time(
frames_data,
tracks,
categories,
sidecar,
sidecar_scale,
sidecar_offset,
)
if layout == "CYX":
pages_data = _iter_pages()
return _read_single_class_stack(
pages_data,
categories,
sidecar,
sidecar_scale,
sidecar_offset,
)
if layout == "TCYX":
# OME/ImageJ declared both T and C. Use the series array which
# reshapes pages into a coherent (T, C, H, W) block. tifffile drops
# size-1 axes, so a degenerate T=1 surfaces as layout="CYX" above
# and doesn't reach here.
series = _read_series()
if series.ndim != 4:
raise ValueError(
f"Expected 4D (T,C,H,W) series for TCYX, got shape {series.shape}"
)
from sleap_io.model.label_image import UserLabelImage
t_dim, c_dim = series.shape[0], series.shape[1]
cat_list = _categories_as_list(categories, c_dim)
if cat_list is None:
cat_list = _categories_list_from_sidecar(sidecar, c_dim)
result = []
for t in range(t_dim):
pages_t = [series[t, c] for c in range(c_dim)]
label_ids_t = _infer_label_ids_from_pages(pages_t)
stack_t = series[t].astype(bool)
result.append(
UserLabelImage.from_binary_masks(
stack_t,
label_ids=label_ids_t,
categories=cat_list,
scale=sidecar_scale,
offset=sidecar_offset,
)
)
return result
raise ValueError(f"Unhandled TIFF axes layout: {layout!r}")
write_label_images(path, label_images, stack=True)
¶
Write label images to TIFF.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output path. If stack=True, writes a single multi-page TIFF. If stack=False, writes per-frame files to this directory (named by zero-padded frame index). |
required |
label_images
|
list[LabelImage]
|
LabelImage objects to write. |
required |
stack
|
bool
|
Write as multi-page TIFF stack (True) or per-frame files in a directory (False). |
True
|
Source code in sleap_io/io/tiff.py
def write_label_images(
path: str | Path,
label_images: list["LabelImage"],
stack: bool = True,
) -> None:
"""Write label images to TIFF.
Args:
path: Output path. If stack=True, writes a single multi-page TIFF.
If stack=False, writes per-frame files to this directory (named
by zero-padded frame index).
label_images: LabelImage objects to write.
stack: Write as multi-page TIFF stack (True) or per-frame files in a
directory (False).
"""
import tifffile
path = Path(path)
if not label_images:
return
if stack:
path.parent.mkdir(parents=True, exist_ok=True)
with tifffile.TiffWriter(str(path)) as tw:
for li in label_images:
tw.write(li.data)
else:
path.mkdir(parents=True, exist_ok=True)
n_digits = max(1, len(str(len(label_images) - 1)))
for i, li in enumerate(label_images):
frame_path = path / f"{str(i).zfill(n_digits)}.tif"
tifffile.imwrite(str(frame_path), li.data)
_write_sidecar(path, label_images)