Skip to content

roi_confirmation

tit.roi_confirmation

Resolve a job's target to the files that describe it, and write its ROI scene.

Why this exists

A number about an ROI is only as good as the ROI. An MNI ROI is not the ROI that runs — every runner transforms it into the subject with the m2m_ registration first (mni2subject / :func:tit.opt.masks.prepare_mask), and a transform that put the putamen in the wrong hemisphere, or 15 mm anterior, or half outside the head, is the one error in this pipeline that no later number can reveal: the optimisation converges, the focality ratio is finite, the analyzer's table is full — and all of it is self-consistently about the wrong voxels.

A subject-space ROI has the same failure modes with none of the transform: an atlas label with detached islands, a hand-drawn mask off by a slice, a sphere whose centre was typed in guide coordinates. So the check is for every target, in every space, written before the expensive work starts.

This module is the thin place in the middle. It turns a target — an atlas file and a label, a hemisphere's .annot, a mask, a sphere — into

  • the layers a scene needs (which file, which label values, what colour),
  • the cursor and zoom (:func:tit.figures.roi_plate.plan_framing), and
  • the numbers (voxels, centroid, grey-matter overlap),

and hands them to :func:tit.figures.roi_plate.write_roi_scene, which writes one small roi.tetravox.json that points at files that already exist.

Nothing is rasterised or duplicated to do it, with exactly one exception: an MNI target's transformed mask is the ROI that runs and exists nowhere else, so it is written once as a compressed uint8 roi_mask.nii.gz on the atlas's voxel grid — about 100 KB, not the 125 MB a 0.5 mm T1 grid costs.

Two rules, each with the failure it prevents:

  • An artefact never fails a job. Everything here is wrapped. A failure is one log line.
  • It reports the mask that will actually be used, not a second computation of it — the same :func:tit.opt.masks.prepare_mask call the runner makes, on the same label. A confirmation derived independently could agree with the user and disagree with the run.

confirm_roi

confirm_roi(*, atlas_path: str | list[str], space: str | list[str], m2m: str, out_dir: str, label: int | None | list[int | None] = None, name: str | list[str] = '', sphere: tuple | None = None) -> dict | None

Write the ROI scene for one target and return its meta block.

Parameters:

Name Type Description Default
atlas_path str | list[str]

the ROI's source — an atlas, a mask, or a .annot.

required
space str | list[str]

"mni" or "subject"; only MNI is transformed on the way.

required
m2m str

the subject's m2m_ directory.

required
out_dir str

where the scene goes.

required
label int | None | list[int | None]

one label of atlas_path, or None for the whole mask.

None
name str | list[str]

what to call this ROI.

''
sphere tuple | None

(centre_ras, radius_mm) when the ROI is a sphere, so the framing uses the sphere the user typed rather than its rasterisation.

None

Returns:

Type Description
dict | None

The meta block as written, or None when the scene was switched off

dict | None

or could not be written — never raises, because a job must not fail

dict | None

because an artefact did.

Source code in tit/roi_confirmation.py
def confirm_roi(
    *,
    atlas_path: str | list[str],
    space: str | list[str],
    m2m: str,
    out_dir: str,
    label: int | None | list[int | None] = None,
    name: str | list[str] = "",
    sphere: tuple | None = None,
) -> dict | None:
    """Write the ROI scene for one target and return its ``meta`` block.

    Args:
        atlas_path: the ROI's source — an atlas, a mask, or a ``.annot``.
        space: ``"mni"`` or ``"subject"``; only MNI is transformed on the way.
        m2m: the subject's ``m2m_`` directory.
        out_dir: where the scene goes.
        label: one label of *atlas_path*, or ``None`` for the whole mask.
        name: what to call this ROI.
        sphere: ``(centre_ras, radius_mm)`` when the ROI is a sphere, so the
            framing uses the sphere the user typed rather than its rasterisation.

    Returns:
        The ``meta`` block as written, or ``None`` when the scene was switched off
        or could not be written — never raises, because a job must not fail
        because an artefact did.
    """
    if not enabled():
        return None
    entries = [
        {
            "atlas_path": p,
            "label": lb,
            "space": sp,
            "name": nm,
            "sphere": sphere,
        }
        for p, lb, sp, nm in zip(*_broadcast(atlas_path, label, space, name))
    ]
    written = confirm_rois(entries, m2m=m2m, out_dir=out_dir)
    return written[0] if written else None

confirm_rois

confirm_rois(entries, *, m2m: str, out_dir: str) -> list[dict]

Write one scene for a search's targets, however many regions it has.

A search treats several regions as one union target, so the confirmation is one scene too — never a directory per region. Each region keeps its own colour in the scene and its own voxel count in meta.

Source code in tit/roi_confirmation.py
def confirm_rois(entries, *, m2m: str, out_dir: str) -> list[dict]:
    """Write **one** scene for a search's targets, however many regions it has.

    A search treats several regions as one union target, so the confirmation is
    one scene too — never a directory per region.  Each region keeps its own
    colour in the scene and its own voxel count in ``meta``.
    """
    if not enabled():
        return []
    entries = [dict(e) for e in entries]
    if not entries:
        return []
    try:
        meta = _write(entries, m2m=m2m, out_dir=out_dir)
    except Exception as exc:  # noqa: BLE001 - never fail a job over a check
        logger.warning("ROI confirmation could not be written: %s", exc)
        return []
    return [meta] if meta else []

cortical_entries

cortical_entries(m2m: str, atlas: str, regions) -> list[dict]

.annot path and label index for each named cortical region.

The analyzer names a cortical target by atlas and region (DK40 / lh.superiorfrontal); the scene needs the file and the integer. Discovery goes through :class:tit.atlas.MeshAtlasManager, the same one GET /api/catalog/atlases uses, so the scene sees exactly the atlases the subject has. A name that resolves to neither hemisphere is skipped with one log line -- an artefact never fails a job.

Source code in tit/roi_confirmation.py
def cortical_entries(m2m: str, atlas: str, regions) -> list[dict]:
    """``.annot`` path and label index for each named cortical region.

    The analyzer names a cortical target by atlas and region (``DK40`` /
    ``lh.superiorfrontal``); the scene needs the file and the integer.  Discovery
    goes through :class:`tit.atlas.MeshAtlasManager`, the same one
    ``GET /api/catalog/atlases`` uses, so the scene sees exactly the atlases the
    subject has.  A name that resolves to neither hemisphere is skipped with one
    log line -- an artefact never fails a job.
    """
    from nibabel.freesurfer import read_annot

    from tit.atlas import MeshAtlasManager

    manager = MeshAtlasManager(str(Path(m2m) / "segmentation"))
    tables: dict[str, tuple[str, dict[str, int]]] = {}
    for hemi in ("lh", "rh"):
        path = manager.find_atlas_file(atlas, hemi)
        if not path or not Path(path).is_file():
            continue
        _, _, labels = read_annot(str(path))
        tables[hemi] = (
            str(path),
            {
                (name.decode() if isinstance(name, bytes) else str(name)).lower(): index
                for index, name in enumerate(labels)
            },
        )
    entries: list[dict] = []
    for raw in regions:
        text = str(raw)
        stem = text
        hemis = ("lh", "rh")
        for prefix in ("lh.", "rh."):
            if text.lower().startswith(prefix):
                hemis, stem = (prefix[:2],), text[3:]
        for suffix in ("-lh", "-rh", "_lh", "_rh"):
            if text.lower().endswith(suffix):
                hemis, stem = (suffix[1:],), text[: -len(suffix)]
        for hemi in hemis:
            table = tables.get(hemi)
            if table is None:
                continue
            index = table[1].get(stem.lower())
            if index is None:
                continue
            entries.append(
                {
                    "atlas_path": table[0],
                    "label": int(index),
                    "space": "subject",
                    "name": f"{hemi}.{stem}",
                }
            )
    if not entries:
        logger.warning("no .annot region of %s matched %s", atlas, list(regions))
    return entries