roi_plate
tit.figures.roi_plate ¶
The ROI scene: one small *.tetravox.json that points at the files a job already has.
Why this exists¶
A number about an ROI is only as good as the ROI. A mask that landed in the wrong hemisphere, kept an island 40 mm away, or came back empty from a transform produces a self-consistent result about the wrong voxels — the optimisation converges, the focality ratio is finite, the analyzer's table is full. So every optimizer and every analyzer run leaves behind something a person can open and look at, written at the start of the run, for every ROI, in every space.
What it leaves behind is one file per target::
roi.tetravox.json the scene: the subject's own T1 plus the ROI layer
(An analysis writes no ROI-only scene: its one scene.tetravox.json shows the
field masked to the ROI over the anatomy -- :mod:tit.analyzer.scene, built on
the same helpers.)
A scene is the format Tetravox's own File ▸ Save Scene writes and Open in
Tetravox reads, so the artefact is the viewer's own document, not a picture of
one. It is a few kilobytes and it references files that already exist — the
subject's m2m/T1.nii.gz, the atlas the target names, the hemisphere's central
surface and its .annot. Nothing is
rasterised, resampled or duplicated to make it, with exactly one exception: an
MNI target is not the ROI that runs, so the transformed mask is written once,
as a compressed uint8 roi_mask.nii.gz on the atlas's voxel grid (about
100 KB), and the scene points at that.
Every dataset path is written relative to the scene file whenever it is inside the project, which is what makes one file work both in the container that wrote it and on the host that opens it: neither has to rewrite a path.
Framing¶
The user's requirement is that the ROI be centred in the point of view and fill
it. :func:plan_framing turns a mask into a :class:FramingPlan that says
where the cursor goes and how far the view reaches, by these rules
(:data:RULES names each one; the chosen one is recorded in the scene's meta):
single
One connected region. Cursor at its centroid, snapped to the nearest
in-mask voxel when the centroid falls outside the mask (a C-shaped
hippocampus's centroid sits in the ventricle). Zoom so the region's bounding
box fills :data:FILL_FRACTION of the panel.
union
Several regions whose union spans no more than :data:SPAN_LIMIT_MM
(bilateral thalami, a two-label union). Cursor at the centroid of the union,
snapped to the nearest in-mask voxel of the largest region; zoom to the
union's bounding box.
per-region
Several regions whose union spans more than :data:SPAN_LIMIT_MM. A scene
has one cursor, so it is framed on the largest region; the others are listed
in meta.regions with their own cursors.
sphere
A spherical ROI. Cursor at the sphere's centre (not the mask centroid — the
centre is what the user typed), radius drives the zoom.
empty
Nothing in the mask. No scene, one terminal line, and that is the failure
the user most needs to see.
Two rules, each with the failure it prevents:
- A scene never fails a job. Everything here is wrapped: the T1 may be unreadable, the output directory read-only. None of that is a reason to refuse to run an optimisation. A failure is one log line.
- It describes the ROI that will actually be used, never a second computation of it.
The optional picture¶
There is no renderer in this module and no matplotlib. On a desktop with
Tetravox installed, desktop/src/main/roiPlates.ts runs it once per scene when
the job finishes and writes <scene>.png beside it. No
Tetravox, no PNG — the scene is still there and Open in Tetravox still works.
Region
dataclass
¶
Region(name: str, voxels: int, centroid_ras: list[float], cursor_ras: list[float], bbox_min_ras: list[float], bbox_max_ras: list[float], color: str = PALETTE[0], value: int = 1, world: object = None)
One connected component or one label of the ROI, in world millimetres.
Row
dataclass
¶
One A/B/C row of the plate: a cursor, a zoom and the regions drawn in it.
FramingPlan
dataclass
¶
FramingPlan(rule: str, regions: list[Region], rows: list[Row] = list(), omitted_regions: list[str] = list(), reason: str = '', island_voxels: int = 0, region_map: object | None = None)
Where the cursor goes and how far the view reaches, and why.
plan_framing ¶
plan_framing(mask, affine, *, names=None, spheres=None, span_limit_mm: float = SPAN_LIMIT_MM) -> FramingPlan
Decide the cursor and the zoom for mask (a 3-D array in a RAS grid).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
integer array. Non-zero is in the ROI. Distinct positive values are treated as distinct regions; a binary mask is split into connected components instead. |
required | |
affine
|
the array's voxel-to-world (RAS, millimetres) affine. |
required | |
names
|
optional |
None
|
|
spheres
|
optional |
None
|
|
span_limit_mm
|
float
|
above this union span the plate gets one row per region. |
SPAN_LIMIT_MM
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
FramingPlan
|
class: |
Source code in tit/figures/roi_plate.py
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | |
plan_spheres ¶
plan_spheres(spheres, *, names=None) -> FramingPlan
The framing for a spherical target, from the centres and radii typed.
A sphere has no file and needs none: its centre is the cursor and its radius
is the zoom, both exactly as the user typed them, which is why this is the
one target whose framing is not derived from voxels at all. The scene it
produces is the subject's T1 with the crosshair on the centre -- Tetravox
0.5.2's ViewSpec has no sphere or marker primitive to draw the extent
with (packages/engine/src/scene/types.ts: sphere appears only inside
a mesh IsolateSpec), so the crosshair and the scale bar are what say
where and how big.
Source code in tit/figures/roi_plate.py
plan_surface ¶
plan_surface(groups, *, names=None, span_limit_mm: float = SPAN_LIMIT_MM) -> FramingPlan
The same framing for a cortical target, from its surface vertices.
groups is one (N, 3) array of world-millimetre vertices per region --
the labelled vertices of the hemisphere's central surface. A cortical target
is a set of vertices, not voxels, and this is what lets the scene reference
the .annot itself instead of a rasterisation of it: nothing is written to
get a cursor.
Half a millimetre of half-extent per vertex, because a vertex is a point and a zero-span bounding box would make the zoom infinite.
Source code in tit/figures/roi_plate.py
scene_path ¶
target as the scene addresses it: relative to the scene when it can be.
A relative path is the one addressing that is correct in the container that
wrote the scene and on the host that opens it, because both see the same
project tree under different roots. Anything outside the project (a bundled
MNI atlas) keeps its absolute path -- it is not part of what gets re-rooted,
and a ../../../.. chain out of the project would be wrong on both.
Source code in tit/figures/roi_plate.py
build_scene ¶
build_scene(*, scene_dir: str, anatomy: str, roi_layers: list[dict], plan: FramingPlan, meta: dict) -> dict
The ViewSpec for one target. Pure but for reading volume headers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scene_dir
|
str
|
the directory the scene file goes in (paths are relative to it). |
required |
anatomy
|
str
|
the subject's own |
required |
roi_layers
|
list[dict]
|
one entry per ROI source, each
|
required |
plan
|
FramingPlan
|
the framing -- its first row gives the cursor and the zoom. |
required |
meta
|
dict
|
what goes in the scene's |
required |
Source code in tit/figures/roi_plate.py
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 | |
anatomy_layer ¶
The grey T1 under everything, windowed so the brain is not washed out.
Source code in tit/figures/roi_plate.py
assemble_scene ¶
assemble_scene(*, datasets: list[dict], layers: list[dict], cursor: list[float], mm_per_px: float, bounds, meta: dict, layout: str = '2x2', transparency: str = 'twoPhase') -> dict
A version-2 ViewSpec around layers: the panes framed on cursor.
Shared by the ROI scene and the analysis scene (:mod:tit.analyzer.scene),
so both frame the same way. bounds is the (lo, hi) world box the
slice cameras are centred against and the 3D camera is fitted to.
Source code in tit/figures/roi_plate.py
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 | |
write_roi_scene ¶
write_roi_scene(*, out_dir: str, anatomy: str, roi_layers: list[dict], plan: FramingPlan, meta: dict, title: str = '') -> dict | None
Write one ROI scene and announce it. Never raises.
Returns the meta block as written, or None when the scene was
switched off or could not be written -- a job must not fail over an artefact
a person looks at.
Source code in tit/figures/roi_plate.py
write_mask_nii_gz ¶
write_mask_nii_gz(image, destination: Path) -> None
Save a NIfTI as gzip, without nibabel's own gzip writer.
nibabel writes a .nii.gz through a file object it seeks backwards in, which
fails on a bind-mounted project inside the container. Writing the plain
.nii to a scratch file and compressing it with the standard library is the
one thing that works on every mount this ships on.