bioimage_py.segmentation.relabel

Block-wise relabeling of a segmentation.

Two operations live here:

  • relabel() applies an explicit, caller-provided relabeling (a labeling) to a segmentation: a mapping from each old segment id to a new id (e.g. the node-to-label result of a graph-based segmentation such as multicut, or a merge/split assignment). It is a single-stage, disjoint per-block point op with no halo, so it accepts block_ids / resume_from and may be applied in place (the default when output is omitted). The labeling is applied per block with bioimage_cpp.utils.take_dict (when given as a dict) or with numpy.take (when given as a dense 1D array/source where labeling[old_id] is the new id); a dense array passed as an in-memory numpy array for a distributed job is persisted to a temporary zarr under the runner's temp root so worker tasks can reopen it.

  • relabel_consecutive() relabels a segmentation so its ids are consecutive. It derives the mapping from the data (a global unique reduction), then delegates the block-wise write to relabel(). Being multi-stage, it re-runs whole (it does not accept block_ids / resume_from); like relabel() it relabels in place when output is omitted.

  1"""Block-wise relabeling of a segmentation.
  2
  3Two operations live here:
  4
  5- :func:`relabel` applies an explicit, caller-provided relabeling (a *labeling*) to a segmentation:
  6  a mapping from each old segment id to a new id (e.g. the node-to-label result of a graph-based
  7  segmentation such as multicut, or a merge/split assignment). It is a single-stage, disjoint
  8  per-block point op with no halo, so it accepts ``block_ids`` / ``resume_from`` and may be applied
  9  **in place** (the default when ``output`` is omitted). The labeling is applied per block with
 10  ``bioimage_cpp.utils.take_dict`` (when given as a ``dict``) or with ``numpy.take`` (when given as a
 11  dense 1D array/source where ``labeling[old_id]`` is the new id); a dense array passed as an
 12  in-memory numpy array for a distributed job is persisted to a temporary zarr under the runner's
 13  temp root so worker tasks can reopen it.
 14
 15- :func:`relabel_consecutive` relabels a segmentation so its ids are consecutive. It *derives* the
 16  mapping from the data (a global ``unique`` reduction), then delegates the block-wise write to
 17  :func:`relabel`. Being multi-stage, it re-runs whole (it does not accept ``block_ids`` /
 18  ``resume_from``); like :func:`relabel` it relabels in place when ``output`` is omitted.
 19"""
 20from __future__ import annotations
 21
 22import os
 23import shutil
 24import tempfile
 25from typing import Callable, Dict, Mapping, Optional, Sequence, Tuple, Union
 26
 27import bioimage_cpp as bic
 28import numpy as np
 29
 30from ..runner import get_runner
 31from ..runner.config import RunnerConfig
 32from ..sources import Source, SourceLike, SourceSpec, as_source, from_spec
 33from ..stats.unique import unique
 34from ..util import (BlockDescriptor, ComputeFn, check_rerun_args, full_roi, is_direct, take_mapping,
 35                    to_roi)
 36
 37__all__ = ["relabel", "relabel_consecutive"]
 38
 39
 40def _require_integer(source: Source, message: str) -> None:
 41    """Raise ``ValueError`` unless ``source`` has an integer dtype."""
 42    if not np.issubdtype(np.dtype(source.dtype), np.integer):
 43        raise ValueError(f"{message}, got dtype {source.dtype}.")
 44
 45
 46def _is_inmemory_numpy(source: Source) -> bool:
 47    """Return whether ``source`` wraps a plain in-memory numpy array (local-only, not reopenable)."""
 48    return isinstance(getattr(source, "array", None), np.ndarray)
 49
 50
 51def _make_relabel_block(mapping: Dict[int, int]) -> ComputeFn:
 52    """Build the per-block write function applying the label mapping (picklable dict)."""
 53
 54    def _compute(block: BlockDescriptor, inputs: Sequence[Source], outputs: Sequence[Source],
 55                 mask: Optional[Source]) -> None:
 56        input_, output_ = inputs[0], outputs[0]
 57        roi = to_roi(block)
 58        seg = input_[roi]
 59        if mask is None:
 60            output_[roi] = take_mapping(mapping, seg)
 61            return None
 62        m = mask[roi].astype(bool)
 63        if not m.any():
 64            return None
 65        # Only in-mask voxels are in the mapping; out-of-mask output voxels are left unchanged.
 66        out_block = output_[roi].copy()
 67        out_block[m] = take_mapping(mapping, seg[m])
 68        output_[roi] = out_block
 69        return None
 70
 71    return _compute
 72
 73
 74def _resume_placeholder(block: BlockDescriptor, inputs: Sequence[Source],
 75                        outputs: Sequence[Source], mask: Optional[Source]) -> None:
 76    """No-op compute fn for the resume path: ``run(resume_from=...)`` never calls it."""
 77    return None
 78
 79
 80def _make_cleanup(tmp_dir: str) -> Callable[[str], None]:
 81    """Build a success-path callback that removes the persisted labeling temp dir."""
 82
 83    def _cleanup(_tmp: str) -> None:
 84        shutil.rmtree(tmp_dir, ignore_errors=True)
 85
 86    return _cleanup
 87
 88
 89def _persist_labeling(labeling: np.ndarray, tmp_root: Optional[str]) -> Tuple[SourceSpec, str]:
 90    """Persist a 1D labeling array to a temp zarr so distributed workers can reopen it.
 91
 92    Args:
 93        labeling: The dense 1D labeling array to persist.
 94        tmp_root: The runner temp root to create the temp dir under (shared filesystem for slurm);
 95            ``None`` uses the system default.
 96
 97    Returns:
 98        A ``(spec, tmp_dir)`` tuple: the reopen spec of the persisted array and the temp directory
 99        (removed on the run's success path, preserved on failure so ``resume_from`` can reopen it).
100    """
101    import zarr
102
103    tmp_dir = tempfile.mkdtemp(prefix="bioimage_py_labeling_", dir=tmp_root)
104    path = os.path.join(tmp_dir, "labeling.zarr")
105    array = zarr.open_array(path, mode="w", shape=labeling.shape, dtype=labeling.dtype,
106                            chunks=labeling.shape)
107    array[:] = labeling
108    return as_source(array).to_spec(), tmp_dir
109
110
111def _make_take_array_block(labeling: Optional[np.ndarray],
112                           spec: Optional[SourceSpec]) -> ComputeFn:
113    """Build the per-block write function mapping ids through a dense 1D labeling array.
114
115    Exactly one of ``labeling`` (captured directly, for local execution) and ``spec`` (reopened on
116    the worker, for distributed execution) is set; the labeling is read once and cached per worker.
117    """
118    cache: Dict[str, np.ndarray] = {}
119
120    def _labeling() -> np.ndarray:
121        labels = cache.get("labeling")
122        if labels is None:
123            labels = labeling if labeling is not None else np.asarray(from_spec(spec)[full_roi(1)])
124            cache["labeling"] = labels
125        return labels
126
127    def _compute(block: BlockDescriptor, inputs: Sequence[Source], outputs: Sequence[Source],
128                 mask: Optional[Source]) -> None:
129        input_, output_ = inputs[0], outputs[0]
130        roi = to_roi(block)
131        seg = input_[roi]
132        labels = _labeling()
133        if mask is None:
134            output_[roi] = np.take(labels, seg)
135            return None
136        m = mask[roi].astype(bool)
137        if not m.any():
138            return None
139        # Only in-mask voxels are remapped; out-of-mask output voxels are left unchanged.
140        out_block = output_[roi].copy()
141        out_block[m] = np.take(labels, seg[m])
142        output_[roi] = out_block
143        return None
144
145    return _compute
146
147
148def relabel(
149    input: SourceLike,
150    labeling: Union[SourceLike, Mapping[int, int]],
151    output: Optional[SourceLike] = None,
152    *,
153    block_shape: Optional[Tuple[int, ...]] = None,
154    job_type: str = "local",
155    job_config: Optional[RunnerConfig] = None,
156    num_workers: int = 1,
157    mask: Optional[SourceLike] = None,
158    block_ids: Optional[Sequence[int]] = None,
159    resume_from: Optional[str] = None,
160) -> SourceLike:
161    """Apply a labeling (relabeling map) to a segmentation, block-wise.
162
163    Each block of ``input`` is read, its ids are mapped through ``labeling``, and the result is
164    written to ``output`` (or back to ``input`` when ``output`` is omitted). This is a single-stage,
165    disjoint per-block point operation, so it may be applied in place and supports ``block_ids`` /
166    ``resume_from`` re-runs.
167
168    Args:
169        input: The input segmentation (a numpy/zarr/n5 array or a `Source`); must be integer-typed.
170        labeling: The relabeling to apply. Either a ``dict`` ``{old_id: new_id}`` (applied with
171            ``bioimage_cpp.utils.take_dict``; every id present in ``input`` must be a key) or a dense
172            1D array/source where ``labeling[old_id]`` is the new id (applied with ``numpy.take``; it
173            must be long enough to index every id present in ``input``). A dict must map every id of
174            the (masked) input; a dense array must cover the id range ``0 .. max_id``.
175        output: The output array to write the relabeled segmentation into. Optional -- when omitted
176            the relabeling is applied **in place** to ``input`` (which must then be writable, and
177            file-backed for distributed execution). As an exception, a plain in-memory numpy input
178            (which is local-only) is never mutated: a fresh array is allocated and returned. When
179            given, ``output`` must match the input shape; ids are cast to its dtype on write.
180        block_shape: Shape of the processing blocks. Defaults to the input chunk shape; required for
181            unchunked data.
182        job_type: Execution backend: one of ``"local"``, ``"subprocess"`` or ``"slurm"``.
183        job_config: Backend configuration (a `RunnerConfig` / `SlurmConfig`).
184        num_workers: Number of parallel workers (threads for ``local``, tasks for distributed
185            backends).
186        mask: Optional binary mask; only voxels within the mask are relabeled (out-of-mask output
187            voxels are left unchanged).
188        block_ids: Restrict processing to these block ids (e.g. to re-run previously failed blocks
189            into the existing ``output``). Mutually exclusive with ``resume_from``.
190        resume_from: Distributed only; the preserved temp folder of a failed run to resume (see
191            ``runner.run``); the missing blocks are relabeled using the original run's labeling.
192            Mutually exclusive with ``block_ids``. (A numpy labeling array persisted by the failed
193            run is preserved with that temp folder; after a successful resume it is best-effort left
194            behind since the labeling is small.)
195
196    Returns:
197        The output array: the provided ``output`` if given, else ``input`` itself when relabeling a
198        file-backed source in place, or a freshly allocated array for an in-memory numpy input.
199    """
200    check_rerun_args(job_type, resume_from, block_ids)
201    src = as_source(input)
202    _require_integer(src, "relabel expects an integer label image")
203    ndim = src.ndim
204
205    # Resolve the output. By default relabel in place (a disjoint per-block point op with no halo,
206    # so this is safe). Exception: a plain in-memory numpy input is local-only (distributed rejects
207    # it), and silently mutating a passed-in array is surprising -- so allocate a fresh copy instead.
208    if output is not None:
209        out_array: SourceLike = output
210    elif job_type == "local" and _is_inmemory_numpy(src):
211        out_array = np.array(src.array)
212    else:
213        out_array = input
214    out = as_source(out_array)
215
216    runner = get_runner(job_type, job_config)
217
218    # Resume short-circuits inside run() to the preserved run's payload (its own sources and
219    # labeling), so the function/inputs passed here are placeholders that run() does not use.
220    if resume_from is not None:
221        runner.run(_resume_placeholder, [src], outputs=[out], resume_from=resume_from,
222                   name="relabel")
223        return out_array
224
225    direct = is_direct(job_type, num_workers, block_shape) and mask is None and block_ids is None
226
227    # Dict mode: apply the mapping with take_dict.
228    if isinstance(labeling, Mapping):
229        mapping = dict(labeling)
230        if direct:
231            out[full_roi(ndim)] = bic.utils.take_dict(mapping, src[full_roi(ndim)])
232            return out_array
233        runner.run(_make_relabel_block(mapping), [src], outputs=[out], block_shape=block_shape,
234                   mask=mask, num_workers=num_workers, block_ids=block_ids, name="relabel")
235        return out_array
236
237    # Dense 1D labeling array/source: applied with numpy.take.
238    labeling_src = as_source(labeling)
239    if labeling_src.ndim != 1:
240        raise ValueError(f"Dense labeling must be a 1D array; got shape {labeling_src.shape}.")
241    _require_integer(labeling_src, "labeling must be integer-typed")
242
243    if direct:
244        labels = np.asarray(labeling_src[full_roi(1)])
245        out[full_roi(ndim)] = np.take(labels, src[full_roi(ndim)])
246        return out_array
247
248    # Carry the labeling in the (cloudpickled) closure: the array directly for local execution, or
249    # a reopen spec for distributed workers -- persisting an in-memory numpy array to a temp zarr.
250    labeling_array: Optional[np.ndarray] = None
251    spec: Optional[SourceSpec] = None
252    pre_cleanup: Optional[Callable[[str], None]] = None
253    if job_type == "local":
254        labeling_array = np.asarray(labeling_src[full_roi(1)])
255    else:
256        try:
257            spec = labeling_src.to_spec()
258        except ValueError:  # an in-memory numpy array: persist it so worker tasks can reopen it.
259            spec, tmp_dir = _persist_labeling(np.asarray(labeling_src[full_roi(1)]),
260                                              runner.config.tmp_root)
261            pre_cleanup = _make_cleanup(tmp_dir)
262
263    runner.run(_make_take_array_block(labeling_array, spec), [src], outputs=[out],
264               block_shape=block_shape, mask=mask, num_workers=num_workers, block_ids=block_ids,
265               pre_cleanup=pre_cleanup, name="relabel")
266    return out_array
267
268
269def relabel_consecutive(
270    input: SourceLike,
271    output: Optional[SourceLike] = None,
272    *,
273    start_label: int = 0,
274    keep_zeros: bool = True,
275    block_shape: Optional[Tuple[int, ...]] = None,
276    job_type: str = "local",
277    job_config: Optional[RunnerConfig] = None,
278    num_workers: int = 1,
279    mask: Optional[SourceLike] = None,
280) -> Tuple[SourceLike, int, Dict[int, int]]:
281    """Relabel a segmentation to consecutive ids, block-wise.
282
283    This is multi-stage: a global ``unique`` reduction derives the ``{old_id: new_id}`` mapping, then
284    the block-wise write is delegated to :func:`relabel`. Because of the reduction it does **not**
285    accept ``block_ids`` or ``resume_from``: a failed run is re-run whole (it is idempotent given the
286    same ``output``).
287
288    Args:
289        input: The input label image (a numpy/zarr/n5 array or a `Source`); must be integer-typed.
290        output: The output array to write the relabeled segmentation into. Optional -- when omitted
291            the relabeling is applied **in place** to ``input`` (which must then be writable, and
292            file-backed for distributed execution); a plain in-memory numpy input (local-only) is
293            never mutated -- a fresh array is allocated and returned. When given, it must match the
294            input shape.
295        start_label: The value the smallest unique id is mapped to (subsequent ids follow
296            consecutively).
297        keep_zeros: Whether to always keep ``0`` mapped to ``0`` (background), regardless of
298            ``start_label``.
299        block_shape: Shape of the processing blocks. Defaults to the input chunk shape; required
300            for unchunked data.
301        job_type: Execution backend: one of ``"local"``, ``"subprocess"`` or ``"slurm"``.
302        job_config: Backend configuration (a `RunnerConfig` / `SlurmConfig`).
303        num_workers: Number of parallel workers (threads for ``local``, tasks for distributed
304            backends).
305        mask: Optional binary mask; values outside the mask are excluded from the computation and
306            their output voxels are left unchanged.
307
308    Returns:
309        A ``(output, max_id, mapping)`` tuple: the relabeled output array (``input`` itself when
310        relabeling a file-backed source in place, or a freshly allocated array for a numpy input),
311        the maximum label id after relabeling, and the ``{old_id: new_id}`` mapping that was applied.
312
313    """
314    src = as_source(input)
315    _require_integer(src, "relabel_consecutive expects an integer label image")
316    ndim = src.ndim
317
318    # Pass 1: the global set of unique values.
319    direct = is_direct(job_type, num_workers, block_shape) and mask is None
320    if direct:
321        uniques = np.unique(src[full_roi(ndim)])
322    else:
323        uniques = unique(input, block_shape=block_shape, job_type=job_type, job_config=job_config,
324                         num_workers=num_workers, mask=mask)
325
326    # In-process: build the old -> new mapping (consecutive ids from start_label).
327    mapping: Dict[int, int] = {int(v): i for i, v in enumerate(uniques.tolist(), start_label)}
328    if keep_zeros and 0 in mapping:
329        mapping[0] = 0
330    max_id = max(mapping.values()) if mapping else 0
331
332    # Pass 2: apply the mapping (in place when output is omitted), reusing relabel's dict path.
333    out = relabel(input, mapping, output, block_shape=block_shape, job_type=job_type,
334                  job_config=job_config, num_workers=num_workers, mask=mask)
335    return out, max_id, mapping
def relabel( input: 'SourceLike', labeling: 'Union[SourceLike, Mapping[int, int]]', output: 'Optional[SourceLike]' = None, *, block_shape: Optional[Tuple[int, ...]] = None, job_type: str = 'local', job_config: Optional[bioimage_py.runner.RunnerConfig] = None, num_workers: int = 1, mask: 'Optional[SourceLike]' = None, block_ids: Optional[Sequence[int]] = None, resume_from: Optional[str] = None) -> 'SourceLike':
149def relabel(
150    input: SourceLike,
151    labeling: Union[SourceLike, Mapping[int, int]],
152    output: Optional[SourceLike] = None,
153    *,
154    block_shape: Optional[Tuple[int, ...]] = None,
155    job_type: str = "local",
156    job_config: Optional[RunnerConfig] = None,
157    num_workers: int = 1,
158    mask: Optional[SourceLike] = None,
159    block_ids: Optional[Sequence[int]] = None,
160    resume_from: Optional[str] = None,
161) -> SourceLike:
162    """Apply a labeling (relabeling map) to a segmentation, block-wise.
163
164    Each block of ``input`` is read, its ids are mapped through ``labeling``, and the result is
165    written to ``output`` (or back to ``input`` when ``output`` is omitted). This is a single-stage,
166    disjoint per-block point operation, so it may be applied in place and supports ``block_ids`` /
167    ``resume_from`` re-runs.
168
169    Args:
170        input: The input segmentation (a numpy/zarr/n5 array or a `Source`); must be integer-typed.
171        labeling: The relabeling to apply. Either a ``dict`` ``{old_id: new_id}`` (applied with
172            ``bioimage_cpp.utils.take_dict``; every id present in ``input`` must be a key) or a dense
173            1D array/source where ``labeling[old_id]`` is the new id (applied with ``numpy.take``; it
174            must be long enough to index every id present in ``input``). A dict must map every id of
175            the (masked) input; a dense array must cover the id range ``0 .. max_id``.
176        output: The output array to write the relabeled segmentation into. Optional -- when omitted
177            the relabeling is applied **in place** to ``input`` (which must then be writable, and
178            file-backed for distributed execution). As an exception, a plain in-memory numpy input
179            (which is local-only) is never mutated: a fresh array is allocated and returned. When
180            given, ``output`` must match the input shape; ids are cast to its dtype on write.
181        block_shape: Shape of the processing blocks. Defaults to the input chunk shape; required for
182            unchunked data.
183        job_type: Execution backend: one of ``"local"``, ``"subprocess"`` or ``"slurm"``.
184        job_config: Backend configuration (a `RunnerConfig` / `SlurmConfig`).
185        num_workers: Number of parallel workers (threads for ``local``, tasks for distributed
186            backends).
187        mask: Optional binary mask; only voxels within the mask are relabeled (out-of-mask output
188            voxels are left unchanged).
189        block_ids: Restrict processing to these block ids (e.g. to re-run previously failed blocks
190            into the existing ``output``). Mutually exclusive with ``resume_from``.
191        resume_from: Distributed only; the preserved temp folder of a failed run to resume (see
192            ``runner.run``); the missing blocks are relabeled using the original run's labeling.
193            Mutually exclusive with ``block_ids``. (A numpy labeling array persisted by the failed
194            run is preserved with that temp folder; after a successful resume it is best-effort left
195            behind since the labeling is small.)
196
197    Returns:
198        The output array: the provided ``output`` if given, else ``input`` itself when relabeling a
199        file-backed source in place, or a freshly allocated array for an in-memory numpy input.
200    """
201    check_rerun_args(job_type, resume_from, block_ids)
202    src = as_source(input)
203    _require_integer(src, "relabel expects an integer label image")
204    ndim = src.ndim
205
206    # Resolve the output. By default relabel in place (a disjoint per-block point op with no halo,
207    # so this is safe). Exception: a plain in-memory numpy input is local-only (distributed rejects
208    # it), and silently mutating a passed-in array is surprising -- so allocate a fresh copy instead.
209    if output is not None:
210        out_array: SourceLike = output
211    elif job_type == "local" and _is_inmemory_numpy(src):
212        out_array = np.array(src.array)
213    else:
214        out_array = input
215    out = as_source(out_array)
216
217    runner = get_runner(job_type, job_config)
218
219    # Resume short-circuits inside run() to the preserved run's payload (its own sources and
220    # labeling), so the function/inputs passed here are placeholders that run() does not use.
221    if resume_from is not None:
222        runner.run(_resume_placeholder, [src], outputs=[out], resume_from=resume_from,
223                   name="relabel")
224        return out_array
225
226    direct = is_direct(job_type, num_workers, block_shape) and mask is None and block_ids is None
227
228    # Dict mode: apply the mapping with take_dict.
229    if isinstance(labeling, Mapping):
230        mapping = dict(labeling)
231        if direct:
232            out[full_roi(ndim)] = bic.utils.take_dict(mapping, src[full_roi(ndim)])
233            return out_array
234        runner.run(_make_relabel_block(mapping), [src], outputs=[out], block_shape=block_shape,
235                   mask=mask, num_workers=num_workers, block_ids=block_ids, name="relabel")
236        return out_array
237
238    # Dense 1D labeling array/source: applied with numpy.take.
239    labeling_src = as_source(labeling)
240    if labeling_src.ndim != 1:
241        raise ValueError(f"Dense labeling must be a 1D array; got shape {labeling_src.shape}.")
242    _require_integer(labeling_src, "labeling must be integer-typed")
243
244    if direct:
245        labels = np.asarray(labeling_src[full_roi(1)])
246        out[full_roi(ndim)] = np.take(labels, src[full_roi(ndim)])
247        return out_array
248
249    # Carry the labeling in the (cloudpickled) closure: the array directly for local execution, or
250    # a reopen spec for distributed workers -- persisting an in-memory numpy array to a temp zarr.
251    labeling_array: Optional[np.ndarray] = None
252    spec: Optional[SourceSpec] = None
253    pre_cleanup: Optional[Callable[[str], None]] = None
254    if job_type == "local":
255        labeling_array = np.asarray(labeling_src[full_roi(1)])
256    else:
257        try:
258            spec = labeling_src.to_spec()
259        except ValueError:  # an in-memory numpy array: persist it so worker tasks can reopen it.
260            spec, tmp_dir = _persist_labeling(np.asarray(labeling_src[full_roi(1)]),
261                                              runner.config.tmp_root)
262            pre_cleanup = _make_cleanup(tmp_dir)
263
264    runner.run(_make_take_array_block(labeling_array, spec), [src], outputs=[out],
265               block_shape=block_shape, mask=mask, num_workers=num_workers, block_ids=block_ids,
266               pre_cleanup=pre_cleanup, name="relabel")
267    return out_array

Apply a labeling (relabeling map) to a segmentation, block-wise.

Each block of input is read, its ids are mapped through labeling, and the result is written to output (or back to input when output is omitted). This is a single-stage, disjoint per-block point operation, so it may be applied in place and supports block_ids / resume_from re-runs.

Args: input: The input segmentation (a numpy/zarr/n5 array or a Source); must be integer-typed. labeling: The relabeling to apply. Either a dict {old_id: new_id} (applied with bioimage_cpp.utils.take_dict; every id present in input must be a key) or a dense 1D array/source where labeling[old_id] is the new id (applied with numpy.take; it must be long enough to index every id present in input). A dict must map every id of the (masked) input; a dense array must cover the id range 0 .. max_id. output: The output array to write the relabeled segmentation into. Optional -- when omitted the relabeling is applied in place to input (which must then be writable, and file-backed for distributed execution). As an exception, a plain in-memory numpy input (which is local-only) is never mutated: a fresh array is allocated and returned. When given, output must match the input shape; ids are cast to its dtype on write. block_shape: Shape of the processing blocks. Defaults to the input chunk shape; required for unchunked data. job_type: Execution backend: one of "local", "subprocess" or "slurm". job_config: Backend configuration (a RunnerConfig / SlurmConfig). num_workers: Number of parallel workers (threads for local, tasks for distributed backends). mask: Optional binary mask; only voxels within the mask are relabeled (out-of-mask output voxels are left unchanged). block_ids: Restrict processing to these block ids (e.g. to re-run previously failed blocks into the existing output). Mutually exclusive with resume_from. resume_from: Distributed only; the preserved temp folder of a failed run to resume (see runner.run); the missing blocks are relabeled using the original run's labeling. Mutually exclusive with block_ids. (A numpy labeling array persisted by the failed run is preserved with that temp folder; after a successful resume it is best-effort left behind since the labeling is small.)

Returns: The output array: the provided output if given, else input itself when relabeling a file-backed source in place, or a freshly allocated array for an in-memory numpy input.

def relabel_consecutive( input: 'SourceLike', output: 'Optional[SourceLike]' = None, *, start_label: int = 0, keep_zeros: bool = True, block_shape: Optional[Tuple[int, ...]] = None, job_type: str = 'local', job_config: Optional[bioimage_py.runner.RunnerConfig] = None, num_workers: int = 1, mask: 'Optional[SourceLike]' = None) -> 'Tuple[SourceLike, int, Dict[int, int]]':
270def relabel_consecutive(
271    input: SourceLike,
272    output: Optional[SourceLike] = None,
273    *,
274    start_label: int = 0,
275    keep_zeros: bool = True,
276    block_shape: Optional[Tuple[int, ...]] = None,
277    job_type: str = "local",
278    job_config: Optional[RunnerConfig] = None,
279    num_workers: int = 1,
280    mask: Optional[SourceLike] = None,
281) -> Tuple[SourceLike, int, Dict[int, int]]:
282    """Relabel a segmentation to consecutive ids, block-wise.
283
284    This is multi-stage: a global ``unique`` reduction derives the ``{old_id: new_id}`` mapping, then
285    the block-wise write is delegated to :func:`relabel`. Because of the reduction it does **not**
286    accept ``block_ids`` or ``resume_from``: a failed run is re-run whole (it is idempotent given the
287    same ``output``).
288
289    Args:
290        input: The input label image (a numpy/zarr/n5 array or a `Source`); must be integer-typed.
291        output: The output array to write the relabeled segmentation into. Optional -- when omitted
292            the relabeling is applied **in place** to ``input`` (which must then be writable, and
293            file-backed for distributed execution); a plain in-memory numpy input (local-only) is
294            never mutated -- a fresh array is allocated and returned. When given, it must match the
295            input shape.
296        start_label: The value the smallest unique id is mapped to (subsequent ids follow
297            consecutively).
298        keep_zeros: Whether to always keep ``0`` mapped to ``0`` (background), regardless of
299            ``start_label``.
300        block_shape: Shape of the processing blocks. Defaults to the input chunk shape; required
301            for unchunked data.
302        job_type: Execution backend: one of ``"local"``, ``"subprocess"`` or ``"slurm"``.
303        job_config: Backend configuration (a `RunnerConfig` / `SlurmConfig`).
304        num_workers: Number of parallel workers (threads for ``local``, tasks for distributed
305            backends).
306        mask: Optional binary mask; values outside the mask are excluded from the computation and
307            their output voxels are left unchanged.
308
309    Returns:
310        A ``(output, max_id, mapping)`` tuple: the relabeled output array (``input`` itself when
311        relabeling a file-backed source in place, or a freshly allocated array for a numpy input),
312        the maximum label id after relabeling, and the ``{old_id: new_id}`` mapping that was applied.
313
314    """
315    src = as_source(input)
316    _require_integer(src, "relabel_consecutive expects an integer label image")
317    ndim = src.ndim
318
319    # Pass 1: the global set of unique values.
320    direct = is_direct(job_type, num_workers, block_shape) and mask is None
321    if direct:
322        uniques = np.unique(src[full_roi(ndim)])
323    else:
324        uniques = unique(input, block_shape=block_shape, job_type=job_type, job_config=job_config,
325                         num_workers=num_workers, mask=mask)
326
327    # In-process: build the old -> new mapping (consecutive ids from start_label).
328    mapping: Dict[int, int] = {int(v): i for i, v in enumerate(uniques.tolist(), start_label)}
329    if keep_zeros and 0 in mapping:
330        mapping[0] = 0
331    max_id = max(mapping.values()) if mapping else 0
332
333    # Pass 2: apply the mapping (in place when output is omitted), reusing relabel's dict path.
334    out = relabel(input, mapping, output, block_shape=block_shape, job_type=job_type,
335                  job_config=job_config, num_workers=num_workers, mask=mask)
336    return out, max_id, mapping

Relabel a segmentation to consecutive ids, block-wise.

This is multi-stage: a global unique reduction derives the {old_id: new_id} mapping, then the block-wise write is delegated to relabel(). Because of the reduction it does not accept block_ids or resume_from: a failed run is re-run whole (it is idempotent given the same output).

Args: input: The input label image (a numpy/zarr/n5 array or a Source); must be integer-typed. output: The output array to write the relabeled segmentation into. Optional -- when omitted the relabeling is applied in place to input (which must then be writable, and file-backed for distributed execution); a plain in-memory numpy input (local-only) is never mutated -- a fresh array is allocated and returned. When given, it must match the input shape. start_label: The value the smallest unique id is mapped to (subsequent ids follow consecutively). keep_zeros: Whether to always keep 0 mapped to 0 (background), regardless of start_label. block_shape: Shape of the processing blocks. Defaults to the input chunk shape; required for unchunked data. job_type: Execution backend: one of "local", "subprocess" or "slurm". job_config: Backend configuration (a RunnerConfig / SlurmConfig). num_workers: Number of parallel workers (threads for local, tasks for distributed backends). mask: Optional binary mask; values outside the mask are excluded from the computation and their output voxels are left unchanged.

Returns: A (output, max_id, mapping) tuple: the relabeled output array (input itself when relabeling a file-backed source in place, or a freshly allocated array for a numpy input), the maximum label id after relabeling, and the {old_id: new_id} mapping that was applied.