Skip to content

Bugfinder - #125

Merged
Hendrik-code merged 10 commits into
mainfrom
bugfinder
Aug 13, 2026
Merged

Bugfinder#125
Hendrik-code merged 10 commits into
mainfrom
bugfinder

Conversation

@Hendrik-code

@Hendrik-code Hendrik-code commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Bug sweep: 40+ correctness fixes across core, POI, registration and segmentation

A systematic read-through of the codebase looking for defects. Every finding below is a real bug, not a style preference. They are grouped into seven commits by kind, so each can be reviewed (or reverted) on its own.

unit_tests/test_regressions.py adds 47 tests that fail on main and pass here. The existing suite (417 tests) stays green.

One item needs a domain decision — see ⚠️ under "Wrong results" (np_filter_connected_components).


1. Core NII / np_utils methods broken on their default paths

(commit edd6ecf)

These are not edge cases — they are the ordinary call.

Bug Fix
NII.rescale(1.5) — a scalar spacing was expanded with a generator expression instead of a tuple, and the next line calls len() on it → TypeError. Every scalar-spacing call was broken. Wrap in tuple(...).
NII.threshold() — did arr[arr2 >= t] = 1 then arr[arr2 <= t] = 0. The second write erases the == case, so thresholding a binary mask at 1 returned an all-zero image. Single comparison (arr >= t).astype(np.uint8). Also drops a redundant full-volume copy.
NII.normalize() — divided by max_out instead of scaling into [min_out, max_out], so normalize(0, 255) produced values in [0, 1] and then tripped its own assert; max_out=0 raised ZeroDivisionError. Proper affine rescale, constant-image case handled, np.isclose instead of exact float equality in the asserts.
np_dilate_msk(mask=..., use_crop=True) — cropped mask with the global bbox but indexed it against the per-label bbox → IndexError on any multi-label segmentation. That is the default path of NII.dilate_msk(mask=...). Index mask with the per-label crop so the shapes line up.
np_dilate_msk_euclid(labels=...) — the label filter was only applied on the use_crop=False branch; the default path dilated every label. Use the label-filtered array on both branches.
Both dilate helpers binarised the caller's mask array in place (mask[mask != 0] = 1), silently mutating an argument. Rebind to mask != 0.

Plus four inverted inplace early-returns (nii_wrapper.py:811, 1127, 1140, 2700), all of the form return self.copy() if inplace else self. On the no-op shortcut of reorient, rescale (×2) and extract_label, an in-place call returned a different object and an out-of-place call returned an alias of self — the exact opposite of the contract, so a later mutation silently propagated back to the caller's
image.


2. Dtype selection: silently oversized or wrapped segmentations

(commit 49f4dfc)

Three float→int guards used isinstance() against numpy scalar types:

isinstance(self.dtype, np.floating)   # a np.dtype object — never a scalar                                                                  
isinstance(arr, np.floating)          # an ndarray — never a scalar                                                                         

Both are always False, so NII.__init__, NII.set_array and NII.save never downcast a float segmentation. A float64 segmentation stayed float64 for its whole lifetime — 8× the memory of the uint8 it should be, through every get_seg_array() copy and onto disk — and it defeated the unsigned-int fast paths in np_utils. Replaced with np.issubdtype(...).

set_dtype("smallest_int" / "smallest_uint") chose the target from arr.max() alone. Since the cast uses casting="unsafe", a negative minimum wrapped silently (-1 → 255), and "smallest_uint" fell back to the signed np.int32. Factored into _smallest_int_dtype(), which considers both bounds, asserts non-negativity for unsigned targets, and extends to 32/64-bit.

np_map_labels built its lookup table with dtype=arr.dtype, so any mapping target outside the input dtype's range wrapped without warning — on a uint8 mask, 1 → 300 produced 44 and 1 → -5 produced 251. The table dtype is now derived from the mapping targets too; in-range mappings keep the input dtype as before.

NII.save also no longer mutates self via set_dtype_ just to get its cast (a query should not mutate), and no longer re-copies the whole volume afterwards.


3. Off-by-one bbox, leaky return types, empty-mask border check

(commit b11343e)

  • np_bbox_binary clamped the stop index to the array shape and only then added 1, so a bbox touching the far border produced stop == shape + 1. Slicing tolerates that, but every consumer computing stop - start (np_center_of_bbox_binary, NII.compute_crop, is_segmentation_in_border) saw a size one voxel too large. Interior boxes were unaffected, which is why it survived. Clamp now happens after the +1.
  • Same function stored px_dist in a uint8 array → OverflowError for px_dist > 255 under NumPy 2. Now plain int.
  • np_unique / np_unique_withoutzero are annotated -> list[int] but returned three different scalar types depending on input dtype and which of four code paths ran: Python int from bincount, np.int64 from the fast path, np.float32/np.int16 from the np.unique fallbacks. The numpy scalars are not JSON-serializable, so serializing a label list failed for non-uint inputs. All paths go through .tolist().
  • NII.is_segmentation_in_border() guarded on slices is None, but compute_crop(raise_error=False) returns full-extent slices for an empty mask and never None — so an empty segmentation was reported as touching the border. Now short-circuits on the existing is_empty property.

4. Two caches leaking state across the whole process

(commit ffc42f4)

sag_cor_curve_projection did:

order = v_idx_order
order += [i for i in range(256) if i not in v_idx_order]

order aliases the module-level v_idx_order list from vert_constants.py — which is re-exported as TPTBox.v_idx_order — and += extends a list in place. Rendering a single snapshot permanently grew the shared global from 105 to 256 entries for everything else in the process. order was never read afterwards, so both lines are dead; removed.

POI._vert_orientation_pir was a bare class attribute, i.e. one dict shared by every POI instance in the process — despite the comment saying it "will not be copied". get_vert_direction_PIR compounded this by writing the cache onto a throwaway extract_subregion() copy, so a lookup could return vertebra directions computed for a different subject processed earlier. It is now a per-instance field(default_factory=dict, repr=False, compare=False), and the cache is written to the object the function was actually called with.


5. Crashes on public API entry points

(commit 69436cb)

  • from TPTBox import * raised AttributeError: __all__ advertised load_poi but nothing imported it. Now re-exported from core.poi_fun.save_load.
  • nii[0:5] delegated to a key containing Ellipsis, which __getitem__ itself rejects two branches earlier — and dropped the return. Now pads the trailing dimensions with full slices.
  • extract_label("12") / remove_labels("12")str is a Sequence, so the str branch was unreachable and the label was iterated character by character → IndexError. The str check now precedes the Sequence check.
  • ssim() / psnr() normalised via in-place img_1 /= img_1.max()UFuncTypeError on integer images. The neighbouring img_2 already used the out-of-place form; both now match.
  • sitk_utils.transform_centroid called the non-existent NII.get_empty_POI; the method is make_empty_POI (spelled correctly at eight other call sites). Also drops a duplicated assignment in the deformable branch.
  • stitching_tools.n4_bias passed dilate_msk_(mm=3), which is not a parameter of that method → TypeError on every call.
  • inference_nnunet forwarded stacklevel= into the logger, which passes **qargs to print()TypeError whenever the input affine is the identity (exactly the case the warning is about).
  • POI_Global.to_other tested isinstance(ref, Self); Self is a typing special form and isinstance() against it raises. Now checks POI_Global, and the method no longer falls off the end returning None.
  • POI_Global.__init__ gated level_two_info on level_one_info (copy-paste), so passing only level_two_info silently dropped it.
  • calc_centroids took type(stage) after unwrapping the enum to .value, so level_one_info/level_two_info were always int and the saved POI header recorded "int" instead of the enum class.
  • BIDS auto_add_run_id did info["run"] += 1 on a value that must stay a decimal string for validate_entities() (which calls .isdecimal()).

6. Wrong results, no exception

(commit b2ee83b) — none of these raised; they just computed the wrong thing.

vert_constants

  • Full_Body_Instance_Vibe.get_Full_Body_Instance_mapping() referenced cls.hip_left / cls.hip_right, which do not exist on that enum — the labels are pelvis_left/pelvis_right (50/51), matching the inverse map. The whole classmethod raised AttributeError.
  • The same dict literal had duplicate keys: lung_left twice, lung_right three times, channel twice. Only the last of each survived. The shadowed entries are removed and the previously-winning target kept, so behaviour is unchanged — with a note that one FBI lung label cannot address several Vibe lobes. Deciding which lobe should win is a modelling question, left as-is.
  • Abstract_lvl._get_id passed cls positionally into a classmethod, shifting every argument by one → TypeError on every lookup, swallowed by a bare except Exception. Name resolution therefore never worked: Any._get_id("L1") fell through to int("L1"). Now resolves to 20. The bare except is narrowed so the next such bug is not silent.

np_utils

  • ⚠️ np_filter_connected_components compared label_volume_pairs (a list[tuple]) to largest_k_components (an int), so that shortcut branch was unreachable. Fixing it to len(...) means the shortcut now fires when the component count equals largest_k_components, which changes which components are preserved in that case. This is the one change with a behavioural consequence I could not settle from the code alone — please sanity-check it against your expectations.
  • The background_threshold in the label-smoothing helper was applied to the argmax indices rather than the winning confidence, so it removed labels by index number rather than by probability. Now thresholds arr_stack.max(axis=0).

Elsewhere

  • vertebra_direction dropped the result of set_array().reorient().rescale_() (all out-of-place) and then read the array back off the un-reoriented object, writing the fill-back image in the wrong orientation and spacing. Now mirrors the correct chained form used in calc_center_spinal_cord.
  • SegmentationMesh discarded int_arr.astype(np.uint16), so the float→int conversion it prints a message about never happened.
  • snapshot_modular: cmap(color - 1 % LABEL_MAX % cmap.N) parses as color - (1 % … % …) = color - 1, losing the wrap-around. Parenthesised to match the correct site above it.
  • angles: if (vert, 50) not in poi: cord = poi[vert, 50] was inverted, so no lordosis/kyphosis label was ever emitted.
  • body_quadrants requested Vertebra_Direction_Inferior twice but reads Vertebra_Direction_Right; the resulting KeyError was swallowed, so every vertebra was skipped and make_quadrants returned an all-zero image.
  • inference_nnunet built its label mapping with the raw string key instead of the parsed int, so map_labels_ never matched.
  • predictor accumulated the chunk bounding-box max from self.min_s.
  • deepali_model gated source_seg on fixed_seg and target_seg on moving_seg — each branch tested the other variable.
  • point_registration.load_ assigned the dumped (moving, fixed) pair to (_img_fixed, _img_moving), so every reloaded registration resampled into the wrong space.
  • save_mkr passed split_by_region=split_by_subregion, so with the documented defaults neither branch was taken and markers got random colours.
  • ray_casting negated z instead of flipping the half-space inequality — not the same operation once the plane does not pass through the origin.

7. Performance, resource leaks, encoding

(commit 3f5162a)

Import time. nii_wrapper_math imported peak_signal_noise_ratio and structural_similarity from skimage.metrics at module level, though they are used only inside NII.ssim / NII.psnr. That import pulls in scipy.stats, which accounted for ~35% of import TPTBox. Moved into the methods, following the existing skimage.exposure precedent in nii_wrapper.py:

import TPTBox    948 ms → 613 ms    (scipy.stats no longer loaded at all)

Memory.

  • vertebra_direction allocated subreg_iso.get_array() * 0 once per vertebraget_array() copies the whole volume and * 0 allocates a second one. Now a single np.zeros of the same shape and dtype. Two sites.
  • set_dtype called get_array() (a full copy) twice on the smallest_int/smallest_uint path; now fetched at most once.

Resource leaks.

  • _help.make_spine_plot created a figure it never closed — one leaked figure per call.
  • snapshot_modular.create_snapshot used a bare plt.close(), which closes the current figure rather than the one just saved, and was skipped entirely if savefig raised. Both now close their own figure in a finally.

Encoding. The Logger opened its .log with the platform default encoding, so print_statistic's ± raised UnicodeEncodeError under a C/POSIX locale (Docker, cron, CI). Its logs/ directory is now created with parents=True, exist_ok=True — parallel jobs were racing on it. The same missing encoding="utf-8" is fixed for the POI, BIDS-sidecar and DICOM JSON readers.


8. Stale duplicate package, over-broad import guards

(commit 0b0d14c)

TPTBox/registration/ridged_intensity/ was meant to be renamed to _ridged_intensity/ in e919218 ("rename folder to _ to show that this is not the recommended way to import things"), but the old directory stayed tracked. Both copies then received the docstring pass in 81db55a, while only the underscore copy received 1b8e0fb ("fix bug for very elongated segmentations") — so the duplicate still carries the pre-fix w = max(target.shape[2:]) and the un-scaled delta comparison. Nothing imports it and its __init__.py is empty, so it is removed, along with the stale __pycache__-only leftovers at registration/{deepali,deformable,ridged_points}/.

The optional-dependency guards in registration/__init__.py and _deepali/__init__.py caught bare Exception, which makes a real NameError or AttributeError inside those modules indistinguishable from "torch is not installed" — the symbol just silently vanishes from the public API. That is exactly how the stale duplicate above, and a NameError in deepali_trainer, went unnoticed. Narrowed to ImportError; all five exported names still resolve.

Also drops the duplicate MODES definition in nii_wrapper.py, which shadowed the identical import 13 lines above it, and two leftover debug prints — one in POI buffer loading, one emitted once per bisection step per point from ray_casting.


Testing

pytest unit_tests/test_regressions.py     47 passed
pytest unit_tests/                        417 passed, 4 skipped, 81 subtests passed

Each of the 47 new tests targets one bug above and fails on main. Fixes in code paths that need optional dependencies (deepali, nnU-Net, S
impleITK, ANTs) or GPU are covered by inspection rather than tests.

Review notes

  • The np_filter_connected_components change (§6) is the only fix that alters behaviour in a way that could be intentional. Everything else restores what the code plainly says it does.
  • The lung-lobe mapping in §6 preserves the current (accidental) behaviour rather than guessing at the right target — worth a second opinion
    .
  • §1's inplace fixes change what object is returned on the no-op shortcut path. If any caller was relying on the inverted behaviour, it was relying on a bug, but it is worth a grep on your side across downstream code.

Hendrik-code and others added 8 commits August 5, 2026 06:30
Four core operations failed or returned wrong results on ordinary inputs:

- NII.rescale(scalar) built a generator, so the following len() raised
  TypeError. Every scalar-spacing call was broken.
- NII.threshold() set arr[arr2>=t]=1 then arr[arr2<=t]=0, so the second
  write erased the == case; thresholding a binary mask at 1 returned an
  all-zero image. Rewritten as a single comparison, which also drops a
  redundant full-volume copy.
- NII.normalize() divided by max_out instead of scaling into the target
  range and offset by min_out/max_out, so normalize(0,255) produced values
  in [0,1] and tripped its own assert (and max_out=0 raised
  ZeroDivisionError). Now scales properly, handles constant images, and
  uses np.isclose instead of exact float equality.
- np_dilate_msk(mask=..., use_crop=True) cropped `mask` with the global
  crop but indexed it against the per-label crop, raising IndexError for
  any multi-label segmentation - i.e. NII.dilate_msk(mask=...) on its
  default path.
- np_dilate_msk_euclid(labels=...) applied the label filter only when
  use_crop=False; the default path dilated every label.

Both dilate helpers also stopped binarising the caller's mask array in
place. Additionally corrects three early-return branches that had `inplace`
inverted (nii_wrapper.py:811, 1127, 1140, 2700), so an in-place call no
longer returns a different object and an out-of-place call no longer
returns an alias of self.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…apped

Three float->int guards used isinstance() against numpy scalar types:

    isinstance(self.dtype, np.floating)   # a np.dtype object, never a scalar
    isinstance(arr, np.floating)          # an ndarray, never a scalar

Both are always False, so NII.__init__, NII.set_array and NII.save never
downcast float segmentations. A float64 segmentation therefore stayed
float64 for its whole lifetime - 8x the memory of the uint8 it should be,
through every get_seg_array() copy and onto disk - and it defeated the
unsigned-int fast paths in np_utils. Replaced with np.issubdtype(...).

set_dtype("smallest_int"/"smallest_uint") chose the target type from
arr.max() alone. Since the cast uses casting="unsafe", a negative minimum
wrapped silently (-1 -> 255), and "smallest_uint" fell back to the *signed*
np.int32. Dtype selection is now factored into _smallest_int_dtype(), which
considers both bounds, asserts non-negativity for unsigned targets, and
extends to 32/64-bit.

np_map_labels built its lookup table with dtype=arr.dtype, so any mapping
target outside the input dtype's range wrapped without warning - on a uint8
mask, 1 -> 300 produced 44 and 1 -> -5 produced 251. The table dtype is now
derived from the mapping targets as well; in-range mappings keep the input
dtype as before.

NII.save no longer mutates self via set_dtype_ to achieve its cast, and no
longer re-copies the whole volume afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…der check

np_bbox_binary clamped the stop index to the array shape and only then added
1, so a bounding box touching the far border produced stop == shape + 1.
Slicing tolerates that, but every consumer computing stop - start
(np_center_of_bbox_binary, NII.compute_crop, is_segmentation_in_border) saw
a size one voxel too large. It also stored px_dist in a uint8 array, so any
px_dist > 255 raised OverflowError under NumPy 2. Interior boxes are
unaffected.

np_unique / np_unique_withoutzero are annotated -> list[int] but returned
three different scalar types depending on the input dtype and which of the
four code paths was taken: Python int from bincount, np.int64 from the
withoutzero fast path, and np.float32/np.int16 from the np.unique fallbacks.
The numpy scalars are not JSON-serializable, so serializing label lists
failed for non-uint inputs. All paths now go through .tolist(), which yields
native scalars without truncating float values.

is_segmentation_in_border() guarded on `slices is None`, but
compute_crop(raise_error=False) returns full-extent slices for an empty mask
and never None, so an empty segmentation was reported as touching the
border. Now short-circuits on the existing NII.is_empty property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sag_cor_curve_projection did:

    order = v_idx_order
    order += [i for i in range(256) if i not in v_idx_order]

`order` aliases the module-level v_idx_order list (vert_constants.py), which
is re-exported as TPTBox.v_idx_order, and `+=` extends a list in place. So
rendering a single snapshot permanently grew the shared global from 105 to
256 entries, affecting anything else that reads it. `order` was never used
afterwards, so both lines are dead - removed, along with the now-unused
import.

POI._vert_orientation_pir was a bare class attribute, i.e. one dict shared by
every POI instance in the process. get_vert_direction_PIR compounded this by
writing the cache onto a temporary extract_subregion() copy, so a lookup
could return vertebra directions computed for a different subject processed
earlier. It is now a per-instance dataclass field (default_factory=dict,
excluded from repr/compare, so a copy still starts empty as documented), and
the cache is written to the object the function was called with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `from TPTBox import *` raised AttributeError: __all__ advertised load_poi
  but nothing imported it. Now re-exported from core.poi_fun.save_load.
- nii[0:5] delegated to a key containing Ellipsis, which __getitem__ itself
  rejects two branches earlier, and dropped the `return`. It now pads the
  trailing dimensions with full slices.
- extract_label("12") / remove_labels("12"): str is a Sequence, so the str
  branch was unreachable and the label was iterated character by character
  (IndexError). The str check now precedes the Sequence check.
- ssim()/psnr() normalised via in-place `img_1 /= img_1.max()`, which raises
  UFuncTypeError on integer images. The neighbouring img_2 already used the
  out-of-place form; both now match.
- sitk_utils.transform_centroid called the non-existent NII.get_empty_POI;
  the method is make_empty_POI (used correctly at eight other call sites).
  Also drops a duplicated assignment in the deformable branch.
- stitching_tools.n4_bias passed dilate_msk_(mm=3), which is not a parameter
  of that method -> TypeError on every call. Now n_pixel=3.
- inference_nnunet forwarded stacklevel= into the logger, which passes
  **qargs to print() -> TypeError whenever the input affine is the identity.
- poi_global.to_other tested `isinstance(ref, Self)`; Self is a typing
  special form and isinstance() against it raises. Now checks POI_Global,
  and the method no longer falls off the end returning None.
- poi_global.__init__ gated level_two_info on level_one_info (copy-paste),
  so passing only level_two_info silently dropped it.
- calc_centroids took type(stage) after unwrapping the enum to .value, so
  level_one_info/level_two_info were always int and the saved POI header
  recorded "int" instead of the enum class.
- BIDS auto_add_run_id did info["run"] += 1 on a value that must remain a
  decimal string for validate_entities().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
None of these raised; they just computed the wrong thing.

vert_constants:
- Full_Body_Instance_Vibe.get_Full_Body_Instance_mapping() referenced
  cls.hip_left/hip_right, which do not exist on that enum (they are
  pelvis_left/pelvis_right, 50/51, matching the inverse map) -> the whole
  classmethod raised AttributeError.
- The same dict literal had duplicate keys: lung_left twice, lung_right three
  times, channel twice. Only the last of each survived, so the shadowed
  entries were dead. Removed them, keeping the previously-winning target so
  behaviour is unchanged, with a note that one FBI lung label cannot address
  several Vibe lobes.
- Abstract_lvl._get_id passed `cls` positionally into a classmethod, shifting
  the arguments and raising TypeError on every lookup - swallowed by a bare
  `except Exception`. Name resolution therefore never worked:
  Any._get_id("L1") fell through to int("L1"). Now resolves to 20.

np_utils:
- np_filter_connected_components compared a list[tuple] to an int, so that
  shortcut branch was unreachable. NOTE: fixing this to len(...) changes
  which components are preserved when the count equals largest_k_components;
  worth a domain review.
- The background_threshold in the label-smoothing helper was applied to the
  argmax *indices* rather than the winning confidence, so it removed labels
  by index rather than by probability.

Other:
- vertebra_direction dropped the result of
  set_array().reorient().rescale_(), then read the array back off the
  un-reoriented object, writing the fill-back image in the wrong orientation
  and spacing. Mirrors the correct chained form in calc_center_spinal_cord.
- SegmentationMesh discarded `int_arr.astype(np.uint16)`, so the float->int
  conversion it announces never happened.
- snapshot_modular: cmap(color - 1 % LABEL_MAX % cmap.N) parses as color - 1,
  losing the wrap-around; parenthesised to match the correct site above it.
- angles: `if (vert, 50) not in poi: cord = poi[vert, 50]` was inverted, so
  no lordosis/kyphosis label was ever emitted.
- body_quadrants requested Vertebra_Direction_Inferior twice but reads
  Vertebra_Direction_Right; the KeyError was swallowed, so every vertebra was
  skipped and an all-zero image returned.
- inference_nnunet built its label mapping with the raw string key instead of
  the parsed int.
- predictor accumulated the chunk bounding-box max from self.min_s.
- deepali_model gated source_seg on fixed_seg and target_seg on moving_seg.
- point_registration.load_ assigned the dumped (moving, fixed) pair to
  (_img_fixed, _img_moving), so reloaded registrations mapped the wrong way.
- save_mkr passed split_by_region=split_by_subregion, so with the documented
  defaults neither branch was taken and markers got random colours.
- ray_casting negated z instead of flipping the half-space inequality.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…copies

Import cost: nii_wrapper_math imported peak_signal_noise_ratio and
structural_similarity from skimage.metrics at module level, but they are used
only inside NII.ssim and NII.psnr. That import pulls in scipy.stats, which
accounted for ~35% of `import TPTBox`. Moved both into their methods,
following the existing skimage.exposure precedent in nii_wrapper.py.

    import TPTBox   948 ms -> 613 ms   (scipy.stats no longer loaded at all)

Memory:
- vertebra_direction allocated `subreg_iso.get_array() * 0` once per vertebra:
  get_array() copies the whole volume and `* 0` allocates a second one. Now a
  single np.zeros of the same shape and dtype. Two sites.
- set_dtype called get_array() (a full copy) twice on the smallest_int/uint
  path; it is now fetched at most once.

Resource leaks:
- _help.py created a figure that was never closed - one leaked figure per call.
- snapshot_modular used a bare plt.close(), which closes the *current* figure
  rather than the one just saved, and was skipped entirely if savefig raised.
  Both now close their own figure in a finally block.

Encoding: the Logger opened its .log with the platform default encoding, so
print_statistic's "±" raised UnicodeEncodeError under a C/POSIX locale
(Docker, cron, CI). Its logs directory is also created with
parents=True, exist_ok=True, which parallel jobs were racing on. The same
missing encoding= is fixed for the POI, BIDS-sidecar and DICOM JSON readers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TPTBox/registration/ridged_intensity/ was meant to be renamed to
_ridged_intensity/ in e919218 ("rename folder to _ to show that this is not
the recommended way to import things"), but the old directory stayed tracked.
Both copies then received the docstring pass in 81db55a, while only the
underscore copy received 1b8e0fb ("fix bug for very elongated segmentations")
- so the duplicate still carries the pre-fix `w = max(target.shape[2:])` and
the un-scaled delta comparison. Nothing imports it and its __init__.py is
empty, so it is removed, along with the stale __pycache__-only leftovers at
registration/{deepali,deformable,ridged_points}/.

The optional-dependency guards in registration/__init__.py and
_deepali/__init__.py caught bare `Exception`, which makes a real NameError or
AttributeError inside those modules indistinguishable from "torch is not
installed" - the symbol just silently disappears from the public API. That is
how the stale duplicate above, and the NameError in deepali_trainer, went
unnoticed. Narrowed to ImportError; all five exported names still resolve.

Also drops the duplicate MODES definition in nii_wrapper.py, which shadowed
the identical import 13 lines above it, and two leftover debug prints - one
in POI buffer loading, one emitted once per bisection step per point from
ray_casting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Hendrik-code Hendrik-code self-assigned this Aug 10, 2026
@Hendrik-code Hendrik-code added the bug Something isn't working label Aug 10, 2026
@Hendrik-code
Hendrik-code marked this pull request as ready for review August 10, 2026 08:05
Copilot AI lite review requested due to automatic review settings August 10, 2026 08:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown

Benchmark comparison

Speed (wall time per call)

baseline cd07009 vs head cd07009 · python 3.11.15 · 5 repeats + 1 warmup

Showing the 5 most-changed measurements per case; the rest are collapsed. Rows with a baseline below 3 ms are tagged (noise) — runner jitter on those swamps any real change.

ct_3d — shape (73, 47, 73)

Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
nii_fill_holes 4.60 ±0.04 4.32 ±0.03 -6.2% 0.000
poi_calc_centroids_nocrop 4.38 ±0.09 4.15 ±0.18 -5.3% 0.099
nii_erode_msk 6.31 ±0.07 5.99 ±0.03 -5.1% 0.000
nii_load_img 10.74 ±0.08 10.50 ±0.06 -2.2% 0.001
nii_rescale 40.21 ±0.28 39.35 ±0.08 -2.1% 0.001
… 31 more measurements
Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
nii_filter_connected_components 4.21 ±0.05 4.13 ±0.03 -1.9% 0.021
nii_save 11.51 ±0.16 11.29 ±0.05 -1.8% 0.025
nii_rescale_seg 3.05 ±0.08 3.02 ±0.02 -0.9% 0.229
poi_calc_poi_from_subreg_vert 12.37 ±0.19 12.48 ±0.29 +0.8% 0.298
nii_resample_from_to 42.15 ±0.26 41.80 ±1.61 -0.8% 0.519
nii_dilate_msk 124.86 ±0.14 124.59 ±0.22 -0.2% 0.119
metric_voxels 250463.00 ±0.00 250463.00 ±0.00 +0.0%
metric_labels 3.00 ±0.00 3.00 ±0.00 +0.0%
metric_foreground_pct 14.51 ±0.00 14.51 ±0.00 +0.0%
poi_load 0.21 ±0.01 0.92 ±0.03 +341.5% (noise) 0.000
nii_extract_label 0.73 ±0.02 0.31 ±0.03 -57.9% (noise) 0.000
nii_get_array 0.04 ±0.01 0.03 ±0.00 -29.2% (noise) 0.046
poi_rescale 0.19 ±0.02 0.17 ±0.01 -12.0% (noise) 0.040
poi_to_global 0.17 ±0.01 0.15 ±0.01 -10.7% (noise) 0.029
nii_apply_crop 0.58 ±0.03 0.52 ±0.02 -10.6% (noise) 0.006
nii_pad_to 0.56 ±0.03 0.50 ±0.04 -9.8% (noise) 0.092
poi_local_to_global_arr 0.29 ±0.02 0.27 ±0.02 -8.6% (noise) 0.043
nii_map_labels 0.95 ±0.04 0.88 ±0.02 -7.9% (noise) 0.005
nii_unique 0.64 ±0.02 0.59 ±0.01 -7.8% (noise) 0.005
nii_compute_crop 0.29 ±0.02 0.27 ±0.02 -6.8% (noise) 0.279
nii_set_dtype 0.39 ±0.04 0.36 ±0.01 -6.4% (noise) 0.082
nii_get_connected_components 2.26 ±0.11 2.13 ±0.09 -5.9% (noise) 0.060
poi_map_labels 0.15 ±0.01 0.14 ±0.01 -5.4% (noise) 0.212
nii_load_seg 2.67 ±0.05 2.55 ±0.02 -4.8% (noise) 0.001
nii_reorient 0.69 ±0.02 0.67 ±0.07 -3.1% (noise) 0.659
poi_reorient 0.33 ±0.02 0.32 ±0.01 -2.6% (noise) 0.110
poi_calc_centroids 2.33 ±0.03 2.28 ±0.06 -2.4% (noise) 0.728
poi_resample_from_to 0.60 ±0.02 0.58 ±0.04 -2.2% (noise) 0.878
poi_save 0.36 ±0.04 0.37 ±0.04 +2.1% (noise) 0.684
nii_center_of_masses 1.69 ±0.03 1.66 ±0.01 -1.7% (noise) 0.065
nii_volumes 1.80 ±0.04 1.79 ±0.02 -0.2% (noise) 0.202

ct_2d — shape (73, 47, 1)

Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
nii_rescale 5.50 ±0.06 5.17 ±0.05 -6.0% 0.000
nii_resample_from_to 7.49 ±0.06 7.23 ±0.08 -3.4% 0.002
nii_dilate_msk 14.74 ±0.17 14.66 ±0.09 -0.5% 0.170
metric_voxels 3431.00 ±0.00 3431.00 ±0.00 +0.0%
metric_labels 3.00 ±0.00 3.00 ±0.00 +0.0%
… 30 more measurements
Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
metric_foreground_pct 37.60 ±0.00 37.60 ±0.00 +0.0%
poi_load 0.24 ±0.03 1.06 ±0.03 +334.8% (noise) 0.000
nii_volumes 0.29 ±0.02 0.26 ±0.02 -10.8% (noise) 0.105
nii_compute_crop 0.13 ±0.01 0.12 ±0.02 -8.3% (noise) 0.609
nii_save 1.36 ±0.02 1.25 ±0.01 -8.1% (noise) 0.000
poi_local_to_global_arr 0.35 ±0.03 0.33 ±0.01 -7.5% (noise) 0.069
nii_set_dtype 0.38 ±0.02 0.35 ±0.02 -6.8% (noise) 0.077
poi_save 0.46 ±0.03 0.43 ±0.03 -6.0% (noise) 0.504
nii_unique 0.12 ±0.01 0.11 ±0.00 -5.8% (noise) 0.294
nii_filter_connected_components 0.77 ±0.02 0.81 ±0.03 +4.8% (noise) 0.038
poi_to_global 0.21 ±0.01 0.22 ±0.01 +4.8% (noise) 0.184
poi_resample_from_to 0.70 ±0.02 0.67 ±0.02 -3.9% (noise) 0.116
poi_rescale 0.23 ±0.01 0.24 ±0.02 +3.8% (noise) 0.162
poi_map_labels 0.19 ±0.01 0.18 ±0.01 -3.4% (noise) 0.112
nii_load_seg 1.60 ±0.04 1.65 ±0.78 +3.4% (noise) 0.216
poi_reorient 0.43 ±0.02 0.41 ±0.03 -3.3% (noise) 0.980
nii_map_labels 0.44 ±0.03 0.42 ±0.02 -3.2% (noise) 0.353
poi_calc_centroids_nocrop 1.09 ±0.03 1.06 ±0.02 -3.1% (noise) 0.046
nii_get_array 0.02 ±0.00 0.02 ±0.00 +2.6% (noise) 0.731
nii_apply_crop 0.61 ±0.01 0.60 ±0.02 -2.4% (noise) 0.211
nii_load_img 1.45 ±0.03 1.43 ±0.09 -1.7% (noise) 0.922
nii_rescale_seg 0.72 ±0.03 0.71 ±0.02 -1.7% (noise) 0.216
nii_get_connected_components 0.59 ±0.04 0.60 ±0.03 +1.7% (noise) 0.502
nii_erode_msk 0.94 ±0.02 0.95 ±0.05 +1.4% (noise) 0.463
nii_pad_to 0.50 ±0.04 0.50 ±0.02 +1.3% (noise) 0.265
nii_fill_holes 0.82 ±0.04 0.83 ±0.02 +1.2% (noise) 0.917
nii_extract_label 0.40 ±0.02 0.40 ±0.02 -1.0% (noise) 0.285
nii_center_of_masses 0.20 ±0.01 0.20 ±0.01 -0.9% (noise) 0.795
nii_reorient 0.76 ±0.03 0.77 ±0.03 +0.8% (noise) 0.679
poi_calc_centroids 0.87 ±0.02 0.87 ±0.03 +0.2% (noise) 0.874

mri_3d — shape (68, 52, 67)

Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
poi_calc_centroids_nocrop 9.20 ±0.08 8.53 ±0.08 -7.3% 0.000
nii_fill_holes 5.75 ±0.06 5.35 ±0.06 -7.0% 0.000
nii_erode_msk 8.22 ±0.08 7.67 ±0.12 -6.7% 0.000
nii_filter_connected_components 4.69 ±0.10 4.62 ±0.09 -1.5% 0.218
nii_dilate_msk 162.83 ±1.16 160.66 ±0.47 -1.3% 0.005
… 31 more measurements
Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
nii_rescale 38.03 ±0.33 37.71 ±0.35 -0.9% 0.159
nii_save 9.25 ±0.08 9.18 ±0.08 -0.8% 0.059
nii_resample_from_to 39.87 ±0.24 40.13 ±0.34 +0.6% 0.647
nii_rescale_seg 3.35 ±0.06 3.36 ±0.10 +0.3% 0.676
poi_calc_poi_from_subreg_vert 11.71 ±0.05 11.73 ±0.15 +0.2% 0.233
nii_load_img 8.84 ±0.17 8.84 ±0.03 -0.1% 0.331
metric_voxels 236912.00 ±0.00 236912.00 ±0.00 +0.0%
metric_labels 8.00 ±0.00 8.00 ±0.00 +0.0%
metric_foreground_pct 18.21 ±0.00 18.21 ±0.00 +0.0%
poi_load 0.32 ±0.01 2.26 ±0.10 +609.8% (noise) 0.000
nii_extract_label 0.82 ±0.01 0.43 ±0.03 -47.4% (noise) 0.000
nii_get_array 0.05 ±0.01 0.04 ±0.01 -11.5% (noise) 0.472
poi_local_to_global_arr 0.36 ±0.01 0.33 ±0.02 -7.7% (noise) 0.010
poi_map_labels 0.24 ±0.01 0.23 ±0.00 -6.9% (noise) 0.023
poi_resample_from_to 0.77 ±0.04 0.72 ±0.04 -6.1% (noise) 0.224
nii_apply_crop 0.69 ±0.02 0.65 ±0.01 -5.4% (noise) 0.012
poi_save 0.52 ±0.03 0.55 ±0.03 +4.7% (noise) 0.776
nii_compute_crop 0.33 ±0.01 0.31 ±0.02 -4.6% (noise) 0.087
nii_unique 0.66 ±0.02 0.64 ±0.01 -4.0% (noise) 0.041
poi_to_global 0.26 ±0.01 0.25 ±0.01 -3.0% (noise) 0.973
poi_rescale 0.27 ±0.02 0.27 ±0.01 -2.8% (noise) 0.434
nii_map_labels 1.08 ±0.04 1.10 ±0.04 +2.1% (noise) 0.773
poi_calc_centroids 2.74 ±0.04 2.69 ±0.06 -1.8% (noise) 0.126
poi_reorient 0.44 ±0.02 0.43 ±0.02 -1.2% (noise) 0.652
nii_get_connected_components 2.25 ±0.06 2.22 ±0.05 -1.1% (noise) 0.835
nii_reorient 0.82 ±0.02 0.81 ±0.02 -1.1% (noise) 0.188
nii_center_of_masses 1.95 ±0.01 1.97 ±0.01 +1.0% (noise) 0.134
nii_pad_to 0.68 ±0.02 0.68 ±0.06 -0.6% (noise) 0.708
nii_set_dtype 0.49 ±0.03 0.49 ±0.02 -0.6% (noise) 0.244
nii_load_seg 2.88 ±0.15 2.88 ±0.03 -0.3% (noise) 0.608
nii_volumes 1.94 ±0.02 1.94 ±0.04 +0.2% (noise) 0.416

mri_2d — shape (68, 52, 1)

Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
nii_dilate_msk 19.86 ±0.06 19.53 ±0.46 -1.6% 0.780
nii_rescale 5.39 ±0.08 5.47 ±0.09 +1.6% 0.061
nii_resample_from_to 7.48 ±0.07 7.39 ±0.07 -1.2% 0.048
metric_voxels 3536.00 ±0.00 3536.00 ±0.00 +0.0%
metric_labels 7.00 ±0.00 7.00 ±0.00 +0.0%
… 30 more measurements
Measurement baseline ms (median ±½·range) head ms (median ±½·range) Δ % p
metric_foreground_pct 50.17 ±0.00 50.17 ±0.00 +0.0%
poi_load 0.31 ±0.01 2.22 ±0.06 +625.6% (noise) 0.000
nii_get_array 0.02 ±0.00 0.01 ±0.00 -18.6% (noise) 0.040
poi_rescale 0.29 ±0.02 0.25 ±0.01 -11.8% (noise) 0.016
nii_unique 0.12 ±0.01 0.10 ±0.01 -11.5% (noise) 0.009
nii_map_labels 0.42 ±0.02 0.47 ±0.04 +10.3% (noise) 0.065
poi_calc_centroids 1.18 ±0.02 1.06 ±0.02 -10.2% (noise) 0.000
nii_erode_msk 1.40 ±0.05 1.27 ±0.13 -9.2% (noise) 0.286
poi_local_to_global_arr 0.35 ±0.02 0.32 ±0.01 -9.0% (noise) 0.009
nii_fill_holes 1.15 ±0.05 1.05 ±0.04 -8.9% (noise) 0.010
poi_reorient 0.46 ±0.02 0.43 ±0.02 -8.2% (noise) 0.018
nii_filter_connected_components 0.87 ±0.04 0.81 ±0.02 -7.2% (noise) 0.049
poi_calc_centroids_nocrop 1.45 ±0.04 1.35 ±0.02 -7.2% (noise) 0.001
nii_get_connected_components 0.61 ±0.03 0.58 ±0.01 -5.4% (noise) 0.125
nii_volumes 0.52 ±0.02 0.49 ±0.02 -5.4% (noise) 0.034
poi_map_labels 0.24 ±0.02 0.22 ±0.01 -5.3% (noise) 0.253
nii_extract_label 0.40 ±0.03 0.38 ±0.03 -5.0% (noise) 0.315
poi_save 0.51 ±0.01 0.49 ±0.02 -3.6% (noise) 0.265
nii_load_img 1.39 ±0.03 1.44 ±0.04 +3.5% (noise) 0.400
nii_center_of_masses 0.46 ±0.03 0.44 ±0.02 -3.5% (noise) 0.427
poi_resample_from_to 0.74 ±0.02 0.72 ±0.03 -3.2% (noise) 0.479
nii_reorient 0.78 ±0.01 0.76 ±0.02 -3.0% (noise) 0.217
nii_load_seg 1.57 ±0.06 1.61 ±0.05 +2.8% (noise) 0.190
nii_rescale_seg 0.73 ±0.03 0.71 ±0.03 -2.7% (noise) 0.573
nii_save 1.31 ±0.04 1.34 ±0.07 +2.6% (noise) 0.293
nii_compute_crop 0.13 ±0.01 0.13 ±0.01 +1.6% (noise) 0.805
nii_apply_crop 0.61 ±0.02 0.62 ±0.04 +1.6% (noise) 0.539
nii_set_dtype 0.37 ±0.01 0.36 ±0.01 -1.1% (noise) 0.324
poi_to_global 0.24 ±0.01 0.25 ±0.01 +0.9% (noise) 0.915
nii_pad_to 0.49 ±0.02 0.49 ±0.01 +0.1% (noise) 0.824

Gate: a measurement fails when the baseline is ≥ 1 ms, the median grows by ≥ 50%, and Welch's t-test gives p < 0.05. metric_* rows are context only.

Memory (peak RSS growth per call)

baseline cd07009 vs head cd07009 · python 3.11.15 · 5 repeats + 1 warmup · sampler proc-status, isolation fork · measurement floor ≈ 0.90 MiB

Showing the 5 most-changed measurements per case; the rest are collapsed. Rows with a baseline below 3 MiB are tagged (noise) — runner jitter on those swamps any real change.

ct_3d — shape (73, 47, 73)

Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_unique 3.14 ±0.00 2.96 ±0.00 -5.7%
nii_fill_holes 6.40 ±0.00 6.30 ±0.00 -1.5%
nii_load_img 3.56 ±0.00 3.53 ±0.00 -0.9%
nii_set_dtype 5.23 ±0.00 5.26 ±0.00 +0.6%
nii_erode_msk 5.98 ±0.00 5.94 ±0.00 -0.6%
… 31 more measurements
Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_reorient 5.85 ±0.00 5.88 ±0.00 +0.5%
nii_apply_crop 5.86 ±0.00 5.89 ±0.00 +0.5%
nii_rescale_seg 5.93 ±0.00 5.96 ±0.00 +0.5%
poi_resample_from_to 5.22 ±0.00 5.25 ±0.00 +0.5%
poi_calc_centroids 6.04 ±0.00 6.07 ±0.00 +0.5%
nii_rescale 6.11 ±0.00 6.14 ±0.00 +0.5%
nii_extract_label 5.42 ±0.00 5.45 ±0.00 +0.5%
nii_resample_from_to 6.24 ±0.00 6.27 ±0.00 +0.5%
nii_map_labels 5.48 ±0.00 5.51 ±0.00 +0.5%
nii_filter_connected_components 6.92 ±0.00 6.89 ±0.00 -0.5%
poi_calc_poi_from_subreg_vert 7.11 ±0.00 7.14 ±0.00 +0.4% 0.000
nii_get_connected_components 6.61 ±0.00 6.63 ±0.00 +0.4%
nii_pad_to 5.41 ±0.00 5.43 ±0.00 +0.4%
poi_calc_centroids_nocrop 5.91 ±0.00 5.93 ±0.00 +0.3%
poi_to_global 3.08 ±0.00 3.09 ±0.00 +0.3%
nii_center_of_masses 3.27 ±0.00 3.28 ±0.00 +0.2%
nii_volumes 3.52 ±0.00 3.53 ±0.00 +0.2%
nii_save 3.71 ±0.00 3.71 ±0.00 +0.2%
poi_reorient 3.83 ±0.00 3.84 ±0.00 +0.2%
nii_dilate_msk 5.92 ±0.00 5.93 ±0.00 +0.1%
nii_load_seg 6.00 ±0.00 5.99 ±0.00 -0.1%
metric_voxels 250463.00 ±0.00 250463.00 ±0.00 +0.0%
metric_labels 3.00 ±0.00 3.00 ±0.00 +0.0%
metric_foreground_pct 14.51 ±0.00 14.51 ±0.00 +0.0%
poi_load 1.67 ±0.00 1.61 ±0.00 -3.5% (noise)
nii_get_array 1.64 ±0.00 1.65 ±0.00 +0.5% (noise)
poi_rescale 2.60 ±0.00 2.61 ±0.00 +0.3% (noise)
poi_save 1.40 ±0.00 1.40 ±0.00 +0.3% (noise)
nii_compute_crop 2.89 ±0.00 2.90 ±0.00 +0.3% (noise)
poi_local_to_global_arr 2.95 ±0.00 2.96 ±0.00 +0.3% (noise)
poi_map_labels 1.77 ±0.00 1.77 ±0.00 +0.2% (noise)

ct_2d — shape (73, 47, 1)

Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_unique 3.08 ±0.00 2.90 ±0.00 -6.0%
nii_fill_holes 6.34 ±0.00 6.24 ±0.00 -1.5%
nii_load_img 3.50 ±0.00 3.46 ±0.00 -1.0%
nii_erode_msk 5.91 ±0.00 5.88 ±0.00 -0.7%
poi_resample_from_to 5.16 ±0.00 5.18 ±0.00 +0.5%
… 30 more measurements
Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_set_dtype 5.17 ±0.00 5.20 ±0.00 +0.5%
nii_filter_connected_components 6.86 ±0.00 6.82 ±0.00 -0.5%
nii_extract_label 5.36 ±0.00 5.38 ±0.00 +0.5%
nii_map_labels 5.42 ±0.00 5.45 ±0.00 +0.5%
nii_reorient 5.79 ±0.00 5.81 ±0.00 +0.5%
nii_apply_crop 5.79 ±0.00 5.82 ±0.00 +0.5%
nii_rescale_seg 5.86 ±0.00 5.89 ±0.00 +0.5%
poi_calc_centroids 5.98 ±0.00 6.01 ±0.00 +0.5%
nii_rescale 6.05 ±0.00 6.08 ±0.00 +0.5%
nii_resample_from_to 6.18 ±0.00 6.21 ±0.00 +0.4%
nii_get_connected_components 6.54 ±0.00 6.57 ±0.00 +0.4%
nii_pad_to 5.35 ±0.00 5.37 ±0.00 +0.3%
poi_calc_centroids_nocrop 5.91 ±0.00 5.93 ±0.00 +0.3%
nii_load_seg 5.94 ±0.00 5.93 ±0.00 -0.2%
poi_to_global 3.02 ±0.00 3.02 ±0.00 +0.1%
nii_center_of_masses 3.21 ±0.00 3.21 ±0.00 +0.1%
nii_volumes 3.46 ±0.00 3.46 ±0.00 +0.1%
nii_save 3.64 ±0.00 3.65 ±0.00 +0.1%
poi_reorient 3.77 ±0.00 3.77 ±0.00 +0.1%
nii_dilate_msk 5.86 ±0.00 5.86 ±0.00 +0.1%
metric_voxels 3431.00 ±0.00 3431.00 ±0.00 +0.0%
metric_labels 3.00 ±0.00 3.00 ±0.00 +0.0%
metric_foreground_pct 37.60 ±0.00 37.60 ±0.00 +0.0%
poi_load 1.61 ±0.00 1.55 ±0.00 -3.6% (noise)
poi_save 1.34 ±0.00 1.34 ±0.00 +0.3% (noise)
nii_get_array 1.58 ±0.00 1.59 ±0.00 +0.2% (noise)
poi_map_labels 1.71 ±0.00 1.71 ±0.00 +0.2% (noise)
poi_rescale 2.54 ±0.00 2.54 ±0.00 +0.2% (noise)
nii_compute_crop 2.83 ±0.00 2.84 ±0.00 +0.1% (noise)
poi_local_to_global_arr 2.89 ±0.00 2.89 ±0.00 +0.1% (noise)

mri_3d — shape (68, 52, 67)

Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_unique 3.08 ±0.00 2.90 ±0.00 -6.0%
nii_fill_holes 6.34 ±0.00 6.24 ±0.00 -1.5%
nii_load_img 3.56 ±0.00 3.53 ±0.00 -1.0%
nii_erode_msk 5.91 ±0.00 5.88 ±0.00 -0.7%
poi_resample_from_to 5.16 ±0.00 5.18 ±0.00 +0.5%
… 31 more measurements
Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_set_dtype 5.23 ±0.00 5.26 ±0.00 +0.5%
nii_filter_connected_components 6.86 ±0.00 6.82 ±0.00 -0.5%
nii_extract_label 5.36 ±0.00 5.38 ±0.00 +0.5%
nii_map_labels 5.42 ±0.00 5.45 ±0.00 +0.5%
nii_reorient 5.79 ±0.00 5.81 ±0.00 +0.5%
nii_apply_crop 5.79 ±0.00 5.82 ±0.00 +0.5%
nii_rescale_seg 5.86 ±0.00 5.89 ±0.00 +0.5%
poi_calc_centroids 5.98 ±0.00 6.01 ±0.00 +0.5%
nii_rescale 6.05 ±0.00 6.08 ±0.00 +0.5%
nii_resample_from_to 6.18 ±0.00 6.20 ±0.00 +0.4%
nii_get_connected_components 6.54 ±0.00 6.57 ±0.00 +0.4%
poi_calc_poi_from_subreg_vert 7.10 ±0.00 7.13 ±0.00 +0.4%
nii_pad_to 5.35 ±0.00 5.37 ±0.00 +0.3%
poi_calc_centroids_nocrop 5.85 ±0.00 5.87 ±0.00 +0.3%
nii_load_seg 5.94 ±0.00 5.93 ±0.00 -0.2%
poi_to_global 3.02 ±0.00 3.02 ±0.00 +0.1%
nii_center_of_masses 3.21 ±0.00 3.21 ±0.00 +0.1%
nii_volumes 3.46 ±0.00 3.46 ±0.00 +0.1%
nii_save 3.64 ±0.00 3.65 ±0.00 +0.1%
poi_reorient 3.77 ±0.00 3.77 ±0.00 +0.1%
nii_dilate_msk 5.86 ±0.00 5.86 ±0.00 +0.1%
metric_voxels 236912.00 ±0.00 236912.00 ±0.00 +0.0%
metric_labels 8.00 ±0.00 8.00 ±0.00 +0.0%
metric_foreground_pct 18.21 ±0.00 18.21 ±0.00 +0.0%
poi_load 1.61 ±0.00 1.55 ±0.00 -3.6% (noise)
poi_save 1.40 ±0.00 1.40 ±0.00 +0.3% (noise)
nii_get_array 1.58 ±0.00 1.59 ±0.00 +0.2% (noise)
poi_map_labels 1.71 ±0.00 1.71 ±0.00 +0.2% (noise)
poi_rescale 2.54 ±0.00 2.54 ±0.00 +0.2% (noise)
nii_compute_crop 2.83 ±0.00 2.84 ±0.00 +0.1% (noise)
poi_local_to_global_arr 2.89 ±0.00 2.89 ±0.00 +0.1% (noise)

mri_2d — shape (68, 52, 1)

Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_unique 3.08 ±0.00 2.90 ±0.00 -6.0%
nii_fill_holes 6.34 ±0.00 6.24 ±0.00 -1.5%
nii_load_img 3.56 ±0.00 3.53 ±0.00 -1.0%
nii_erode_msk 5.91 ±0.00 5.88 ±0.00 -0.7%
poi_resample_from_to 5.16 ±0.00 5.18 ±0.00 +0.5%
… 30 more measurements
Measurement baseline MiB (median ±½·range) head MiB (median ±½·range) Δ % p
nii_set_dtype 5.23 ±0.00 5.26 ±0.00 +0.5%
nii_filter_connected_components 6.86 ±0.00 6.82 ±0.00 -0.5%
nii_extract_label 5.36 ±0.00 5.38 ±0.00 +0.5%
nii_map_labels 5.42 ±0.00 5.45 ±0.00 +0.5%
nii_reorient 5.79 ±0.00 5.81 ±0.00 +0.5%
nii_apply_crop 5.79 ±0.00 5.82 ±0.00 +0.5%
nii_rescale_seg 5.86 ±0.00 5.89 ±0.00 +0.5%
poi_calc_centroids 5.98 ±0.00 6.01 ±0.00 +0.5%
nii_rescale 6.05 ±0.00 6.08 ±0.00 +0.5%
nii_resample_from_to 6.18 ±0.00 6.20 ±0.00 +0.4%
nii_get_connected_components 6.54 ±0.00 6.57 ±0.00 +0.4%
nii_pad_to 5.35 ±0.00 5.37 ±0.00 +0.3%
poi_calc_centroids_nocrop 5.91 ±0.00 5.93 ±0.00 +0.3%
nii_load_seg 5.94 ±0.00 5.93 ±0.00 -0.2%
poi_to_global 3.02 ±0.00 3.02 ±0.00 +0.1%
nii_center_of_masses 3.21 ±0.00 3.21 ±0.00 +0.1%
nii_volumes 3.46 ±0.00 3.46 ±0.00 +0.1%
nii_save 3.64 ±0.00 3.65 ±0.00 +0.1%
poi_reorient 3.77 ±0.00 3.77 ±0.00 +0.1%
nii_dilate_msk 5.86 ±0.00 5.86 ±0.00 +0.1%
metric_voxels 3536.00 ±0.00 3536.00 ±0.00 +0.0%
metric_labels 7.00 ±0.00 7.00 ±0.00 +0.0%
metric_foreground_pct 50.17 ±0.00 50.17 ±0.00 +0.0%
poi_load 1.61 ±0.00 1.55 ±0.00 -3.6% (noise)
poi_save 1.40 ±0.00 1.40 ±0.00 +0.3% (noise)
nii_get_array 1.58 ±0.00 1.59 ±0.00 +0.2% (noise)
poi_map_labels 1.71 ±0.00 1.71 ±0.00 +0.2% (noise)
poi_rescale 2.54 ±0.00 2.54 ±0.00 +0.2% (noise)
nii_compute_crop 2.83 ±0.00 2.84 ±0.00 +0.1% (noise)
poi_local_to_global_arr 2.89 ±0.00 2.89 ±0.00 +0.1% (noise)

Gate: a measurement fails when the baseline is ≥ 1 MiB, the median grows by ≥ 50%, and Welch's t-test gives p < 0.05. metric_* rows are context only.

@Hendrik-code
Hendrik-code merged commit a97ecb7 into main Aug 13, 2026
6 checks passed
@Hendrik-code
Hendrik-code deleted the bugfinder branch August 13, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants