ROI System¶
optari.roi.roi_records ¶
ROIRecord: one ROI shape on one frame, the source-of-truth unit RoiController persists.
Records are frame-owned, not shape-owned: a region followed across frames is several
records sharing one roi_group_uid, not one record holding a list of frames.
ROIRecord
dataclass
¶
ROIRecord(roi_id, track_id, frame_id, verts, kind, source=OPTARI_SOURCE_TAG, tissue_class='undefined', roi_group_uid=new_roi_group_uid())
One ROI shape on one frame.
Has 3 identifiers, for 3 scopes:
roi_id
This shape on this frame. A session-local counter, shown on the shape in
the viewer and used for display ordering.
track_id
Which tracked group the shape belongs to, i.e. the same ROI followed across
frames. Session-local, used for analysis plots across frames, and for coloring shapes in the viewer.
roi_group_uid
The group's unique id. Persists through export, import and other
machines. Shared by all shapes in the same ROI group, so they can be grouped together even if the session-local ids change.
__post_init__ ¶
__post_init__()
Coerce fields to their declared types. An empty roi_group_uid mints a fresh one.
Source code in src/optari/roi/roi_records.py
55 56 57 58 59 60 61 62 63 64 65 66 | |
new_roi_group_uid ¶
new_roi_group_uid()
Create a stable identity for one ROI group.
track_id is just a session-specific counter. roi_group_uid persists through
export, import and a move to another machine, so rows measured from the same
ROI stay groupable wherever they end up.
Created with a timestamp and 48 random bits to avoid collisions when multiple ROIs are created at the same time.
Source code in src/optari/roi/roi_records.py
18 19 20 21 22 23 24 25 26 | |
optari.roi.roi_geometry ¶
ROI geometry, shared by the viewer, presets, the saved analysis table and HDF5 export.
Described a shape, not a uniqe measurement
Every persisted form of an ROI uses PATATO coordinates. napari millimetres exist only inside the viewer, where they are relative to the field of view corner and therefore inconsistent once the FOV changes.
So we have 3 coordinate systems in OPTARI:
world mm
What ROIRecord.verts always holds. The Shapes layer is never translated, so
world millimetres are the shared physical frame every layer is registered into.
Scan-loaded layers sit at translate 0, which makes world mm identical to the
scan frame.
PATATO metres
What this module stores. Scan-anchored, like PATATO. origin at the image centre,
derived from world mm and the scan's field of view.
layer pixels
Indices into a layer's array. A runtime-derived layer may sit at a nonzero
translate (like deepmb) to register it against the others,
so converting world mm to that layer's pixels means subtracting its
translate first.
RoiGeometry
dataclass
¶
RoiGeometry(verts_m, kind='polygon', tissue_class='undefined', source=OPTARI_SOURCE_TAG)
An ROI shape in PATATO coordinates: metres, origin at the image centre.
FOV-independent, so the same geometry restores to the same physical location on any reconstruction whatever its field of view or pixel grid.
__post_init__ ¶
__post_init__()
Coerce verts_m to an (N, 2) array and the other fields to their types. Rejects an empty geometry.
Source code in src/optari/roi/roi_geometry.py
86 87 88 89 90 91 92 93 | |
from_dict
classmethod
¶
from_dict(data)
Rebuild from :meth:to_dict's output, defaulting any missing key.
Source code in src/optari/roi/roi_geometry.py
159 160 161 162 163 164 165 166 167 | |
from_json
classmethod
¶
from_json(text)
Inverse of :meth:to_json.
Source code in src/optari/roi/roi_geometry.py
173 174 175 176 | |
from_record
classmethod
¶
from_record(record, fov_x_m, fov_y_m)
Build from a record's world-space vertices.
Source code in src/optari/roi/roi_geometry.py
95 96 97 98 99 100 101 102 103 104 105 106 107 | |
repositioned ¶
repositioned(source_fov, target_fov)
Move the ROI to the same relative spot in a different FOV, keeping its size.
Source code in src/optari/roi/roi_geometry.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
to_dict ¶
to_dict()
Serialize to a plain dict of JSON-safe values.
Source code in src/optari/roi/roi_geometry.py
150 151 152 153 154 155 156 157 | |
to_json ¶
to_json()
Serialize to the JSON blob stored in the roi_geometry table column.
Source code in src/optari/roi/roi_geometry.py
169 170 171 | |
to_record ¶
to_record(*, roi_id, track_id, frame_id, fov_x_m, fov_y_m, roi_group_uid='')
Rebuild a record. An empty roi_group_uid mints a new group identity.
Source code in src/optari/roi/roi_geometry.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
verts_mm ¶
verts_mm(fov_x_m, fov_y_m)
Vertices as napari (y_mm, x_mm) for a target field of view.
Source code in src/optari/roi/roi_geometry.py
109 110 111 | |
napari_to_patato ¶
napari_to_patato(verts_yx_mm, fov_x_m, fov_y_m)
Inverse of :func:patato_to_napari.
Source code in src/optari/roi/roi_geometry.py
57 58 59 60 61 62 63 64 65 66 67 68 69 | |
patato_to_napari ¶
patato_to_napari(pts_m, fov_x_m, fov_y_m)
Convert PATATO (x_m, y_m) polygon vertices to napari (y_mm, x_mm).
PATATO: origin at image centre, x→right, y→up, metres. napari: origin at top-left, y→down, mm.
Source code in src/optari/roi/roi_geometry.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
optari.roi.roi_features ¶
Feature registry: computes every saved and live ROI statistic from a shared context.
FEATURE_REGISTRY maps a column name to a FeatureSpec, so adding a measurement
is a one-line registry entry rather than a change to the measurement loop itself.
FeatureSpec
dataclass
¶
FeatureSpec(dtype, fn)
Pairs a dtype with a function from an :class:ROIContext to the column's value.
ROIContext
dataclass
¶
ROIContext(roi_id, track_id, roi_group_uid, src_layer, kind, roi_source, study_folder, scan_folder, scan_name, frame, channel, scan_ts, roi_centroid, roi_geometry, filepath, vals_raw, vals, clamp, sy, sx, verts)
Everything one measurement needs: identifiers, provenance, the geometry blob
and the pixel values a FeatureSpec reads.
Provenance covers scan_name, frame, channel, src_layer, timestamps
and the intensity filter. Pixel values are vals_raw and the filtered vals,
alongside the pixel scale and the ROI's vertices.
numeric_feature_ids ¶
numeric_feature_ids()
IDs of the features whose dtype is int or float.
Source code in src/optari/roi/roi_features.py
143 144 145 146 147 148 149 | |
optari.roi.roi_table ¶
The Saved Analysis table consisting of saved ROI measurements.
- Saving never overwrites. Re-measuring an ROI appends a second set of rows under
the same
roi_group_uid, separated byroi_ts. TODO: should this be kept or cause error/delete prompt? - Every change writes to disk. Each change to the saved table autosaves
- Re-importing the same file adds nothing. Identical rows are skipped, so analysis sheets can be combined
All the Qt stuff is in the controller
SavedRoiTable ¶
SavedRoiTable(autosave_path=None)
Measured ROI rows, mirrored to autosave_path after every change.
autosave_path may be None to disable the save (e.g. for test cases)
Create an empty table that autosaves to autosave_path, if given.
Source code in src/optari/roi/roi_table.py
34 35 36 37 | |
__len__ ¶
__len__()
Number of saved rows.
Source code in src/optari/roi/roi_table.py
40 41 42 | |
add_measurements ¶
add_measurements(rows)
Stamp rows with the measurement time and append them.
Returns the roi_group_uids that already had earlier measurements.
Source code in src/optari/roi/roi_table.py
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
delete_at ¶
delete_at(positions)
Drop rows by view position. Returns how many went.
Source code in src/optari/roi/roi_table.py
97 98 99 100 101 102 103 104 105 106 | |
merge_imported ¶
merge_imported(rows)
Append an imported table, skipping rows already present.
Returns (added, skipped). Appending rather than replacing lets analyses
from several sessions or machines combine in one table. roi_group_uid keeps
every row attributable to the ROI it was measured from.
Source code in src/optari/roi/roi_table.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
rows_at ¶
rows_at(positions)
Rows by view position. The view is unsorted, so position maps 1:1.
Source code in src/optari/roi/roi_table.py
60 61 62 | |
visible_rows ¶
visible_rows()
Only the columns the saved table displays, for the widget.
Source code in src/optari/roi/roi_table.py
54 55 56 57 58 | |
optari.roi.roi_utils ¶
Functions that measure ROIs against reconstructed image data.
iter_roi_masks turns world-space ROIs into pixel indices, IntensityClamp
filters outlier values before a statistic is computed, and compute_roi_stats
builds an ROIContext per ROI and evaluates the requested FEATURE_REGISTRY
entries.
IntensityClamp
dataclass
¶
IntensityClamp(minimum=None, maximum=None, mode='clip')
How ROI pixel values are filtered before any statistic is computed.
clip bounds out-of-range values back into given range (keeping the pixel count).
exclude drops them, which also changes n_pixels and size_mm.
The default instance is a no-op, so callers that do not filter pass nothing.
apply ¶
apply(values)
Apply the clip or exclude policy to values.
Source code in src/optari/roi/roi_utils.py
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
MeasureScope
dataclass
¶
MeasureScope(all_layers=False, all_frames=False, all_channels=False, follow_track=False)
How wide a save reaches: which layers, frames and channels get measured.
The annotation dock builds one from its checkboxes; batch mode builds one from its plan. Keeping it a value rather than reading widgets inside the measurement loop is what lets both drive the identical code.
clear_mask_cache ¶
clear_mask_cache()
Drop cached masks, e.g. when a scan is closed.
Source code in src/optari/roi/roi_utils.py
134 135 136 | |
compute_roi_spectra ¶
compute_roi_spectra(records, active_recon_layer, frame_idx, *, clamp=NO_CLAMP)
Compute per-ROI mean intensity over channels for a fixed frame.
Source code in src/optari/roi/roi_utils.py
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 | |
compute_roi_stats ¶
compute_roi_stats(records, active_recon_layer, frame_idx, channel_idx, *, clamp=NO_CLAMP, feature_ids=None)
Compute ROI statistics for all shapes for a specific frame/channel.
Source code in src/optari/roi/roi_utils.py
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 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | |
compute_roi_time_series ¶
compute_roi_time_series(records, active_recon_layer, channel_idx, feature_id, *, clamp=NO_CLAMP)
Compute per-ROI feature over time for a fixed channel.
Each of records is measured with the same fixed shape on every frame. Used for the "Selected ROI" scope: whatever a shape looks like right now is used for the whole sequence.
Source code in src/optari/roi/roi_utils.py
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 | |
compute_roi_track_time_series ¶
compute_roi_track_time_series(track_id, records, active_recon_layer, channel_idx, feature_id, *, clamp=NO_CLAMP)
Compute one feature over time for a tracked ROI.
Unlike compute_roi_time_series, each frame is measured on its own record.
Used fot the "Track ID" scope. E.g. for a shape that was moved or restored frame by frame to
follow anatomy. A frame the track has no record on is left as a gap (NaN)
Returns the same (x, {key: y}) shape as compute_roi_time_series, here
with a single entry, keyed by track_id
Source code in src/optari/roi/roi_utils.py
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 | |
ellipse_mask ¶
ellipse_mask(verts_px, image_shape)
Rasterize napari's 4-corner ellipse box (may be rotated) to a boolean mask.
Source code in src/optari/roi/roi_utils.py
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | |
extract_roi_pixels_for_slice ¶
extract_roi_pixels_for_slice(records, active_recon_layer, frame_idx, channel_idx, *, clamp=NO_CLAMP)
Extract pixel values per ROI for the given frame/channel.
Source code in src/optari/roi/roi_utils.py
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 | |
iter_roi_masks ¶
iter_roi_masks(records, active_recon_layer, image_shape)
Yield each usable ROI with its boolean mask over image_shape.
Only place to trasnform world-space ROI to pixel indices. skips broken geometry and any ROI that fails to rasterize, so the four measurement functions below all agree on which ROIs are measurable.
Source code in src/optari/roi/roi_utils.py
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
layer_fov_m ¶
layer_fov_m(active_recon_layer, image_shape)
Return the layer's (fov_x_m, fov_y_m), derived from its own grid.
scale_from_patato_obj sets the scale to fov / n_pixels, so the napari
extent shape * scale is the FOV. Deriving it here rather
than reading scan metadata also covers layers reconstructed or unmixed at
runtime, and layers that fell back to a configured default scale.
Source code in src/optari/roi/roi_utils.py
173 174 175 176 177 178 179 180 181 182 | |
polygon_mask ¶
polygon_mask(verts_px, image_shape)
Rasterize a polygon (verts in pixel coords) to a boolean mask.
Source code in src/optari/roi/roi_utils.py
356 357 358 359 360 361 | |
records_by_track_and_frame ¶
records_by_track_and_frame(records, track_id)
One track's records, indexed by the frame each was measured on.
Source code in src/optari/roi/roi_utils.py
192 193 194 195 196 | |
saved_export_columns ¶
saved_export_columns()
Columns stored and exported: origin columns plus every feature
Source code in src/optari/roi/roi_utils.py
55 56 57 | |
saved_table_columns ¶
saved_table_columns()
Columns shown in the saved table: origin columns plus the enabled features.
Source code in src/optari/roi/roi_utils.py
50 51 52 | |
slice_datetime ¶
slice_datetime(layer, frame_idx, channel_idx)
Wall-clock acquisition time of one slice, or "N/A" if the scan cannot be dated.
See patato_bridge.acquisition_start for when a scan counts as datable.
Source code in src/optari/roi/roi_utils.py
94 95 96 97 98 99 100 101 102 103 104 | |
time_axis ¶
time_axis(active_recon_layer, channel_idx)
Resolve the frame indices to plot and the x-axis value for each.
Source code in src/optari/roi/roi_utils.py
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 | |
visible_feature_columns ¶
visible_feature_columns()
Feature columns the user has enabled in settings, determines the live table's columns.
Source code in src/optari/roi/roi_utils.py
34 35 36 37 38 39 40 | |
optari.roi.roi_shapes ¶
ROI shapes generated from a segmentation class mask, used by the segmentation dock.
roi_verts_from_mask derives a new shape's geometry from the mask (largest-component
isolation, centre-column anchoring, depth offset, contour extraction). It does not
participate in persistence, unlike RoiGeometry.
ROIPlacementConfig
dataclass
¶
ROIPlacementConfig(width_mm, height_mm, depth_mm)
ROI placement configuration in mm.
class_top_at_center_column ¶
class_top_at_center_column(mask)
Row and column of the topmost pixel of mask's largest component, at the image's horizontal centre.
used below by the box-shaped ROI generators, and by ROI preset auto-placement
(RoiController._place_roi_preset_auto) to align a saved preset the same way.
Source code in src/optari/roi/roi_shapes.py
52 53 54 55 56 57 58 59 | |
largest_component ¶
largest_component(mask)
Boolean mask of mask's largest connected component.
Source code in src/optari/roi/roi_shapes.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
roi_verts_from_mask ¶
roi_verts_from_mask(shape_type, class_mask, config, *, sy, sx, ty=0.0, tx=0.0)
World-mm vertices of a shape_type ROI placed in class_mask's largest component.
rectangle and ellipse are boxes anchored to the class's top at the centre
column; polygon follows the class outline, trimmed to the configured bounds.
sy/sx is the mask's pixel size and ty/tx its layer's translate, in mm.
Raises ValueError when the shape does not fit the class.
Source code in src/optari/roi/roi_shapes.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
optari.roi.roi_presets ¶
ROI presets: reusable templates stored as JSON under presets/roi/.
A preset pairs a named RoiGeometry with the source_fov_m it was drawn at and
a description. It carries no identifier, so placing it creates a fresh ROI each time.
RoiPreset
dataclass
¶
RoiPreset(name, description, created, geometry, source_fov_m, placement='static', top_margin_mm=0.0)
A named, reusable ROI template.
The shape itself is a :class:RoiGeometry, the same FOV-independent
representation used by the saved analysis table and HDF5 export.
source_fov_m is kept so it can be re-placed proportionally
in a scan with a different fov.
placement is how the ROI wants to be positioned: static at its saved
coordinates, or auto anchored onto the segmentation class named by
geometry.tissue_class (shown as its location in the UI). It lives here rather
than at each call site so that placing a preset means the same thing in the dock
and in a batch run.
top_margin_mm is how far below the class's top edge (at the image centre) an
auto preset's top is placed, horizontally centred. The save dialog prefills it
with where the ROI sits when saved, and the user can change it.
__post_init__ ¶
__post_init__()
Coerce source_fov_m and top_margin_mm to floats and reject a non-positive FOV or unknown placement.
Source code in src/optari/roi/roi_presets.py
47 48 49 50 51 52 53 54 55 56 57 58 59 | |
from_dict
classmethod
¶
from_dict(name, data)
Rebuild from a preset file's dict. Raises if source_fov_m is missing.
Source code in src/optari/roi/roi_presets.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
to_dict ¶
to_dict()
Serialize to the dict written to the preset's JSON file.
Source code in src/optari/roi/roi_presets.py
76 77 78 79 80 81 82 83 84 85 | |
RoiPresetStore ¶
RoiPresetStore(directory)
Bases: PresetStore
Load and save named ROI presets in the user preset directory.
Source code in src/optari/utils/presets.py
27 28 | |
get ¶
get(name)
Load the preset named name.
Source code in src/optari/roi/roi_presets.py
98 99 100 | |
list_presets ¶
list_presets()
Load every saved preset.
Source code in src/optari/roi/roi_presets.py
91 92 93 94 95 96 | |
save_preset ¶
save_preset(*, name, description, geometry, source_fov_m, placement='static', top_margin_mm=0.0)
Build a preset from the given fields, stamp it with the current time, and save it to disk.
Source code in src/optari/roi/roi_presets.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |