Skip to content

Commit 4323be4

Browse files
committed
Add uncompressed in-memory size to input image summary
prod(shape) * dtype.itemsize, formatted with binary units (B/KiB/MiB/GiB). The on-disk .nii.gz size hides this; it's the number that matters when the pipeline loads the array.
1 parent 0272e42 commit 4323be4

2 files changed

Lines changed: 35 additions & 9 deletions

File tree

src/rbc/core/nifti.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -546,14 +546,24 @@ def _space_label(code: int) -> str:
546546
return str(code)
547547

548548

549+
def _human_bytes(n: int) -> str:
550+
"""Format a byte count with a binary (B/KiB/MiB/GiB) suffix."""
551+
size = float(n)
552+
for unit in ("B", "KiB", "MiB"):
553+
if size < 1024:
554+
return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
555+
size /= 1024
556+
return f"{size:.1f} GiB"
557+
558+
549559
def log_image_summary(in_file: str | Path, *, label: str = "Raw input") -> None:
550560
"""Log array shape, dtype, and geometry of a raw NIfTI input.
551561
552562
Reads only the NIfTI header (no voxel data is loaded), then emits an
553563
INFO-level summary so the run log records exactly what entered the
554-
pipeline: array shape, on-disk dtype, voxel size, axis orientation,
555-
sform/qform coordinate spaces, and (for 4D images) volume count, slice
556-
count, and TR.
564+
pipeline: array shape, on-disk dtype, uncompressed in-memory size,
565+
voxel size, axis orientation, sform/qform coordinate spaces, and (for
566+
4D images) volume count, slice count, and TR.
557567
558568
Args:
559569
in_file: Path to a ``.nii``/``.nii.gz`` file.
@@ -564,20 +574,23 @@ def log_image_summary(in_file: str | Path, *, label: str = "Raw input") -> None:
564574
img = nib.nifti1.load(path)
565575
hdr = img.header
566576
shape = img.shape
577+
dtype = hdr.get_data_dtype()
567578
zooms = hdr.get_zooms()
568579
spatial_unit = hdr.get_xyzt_units()[0]
569580

581+
n_bytes = int(np.prod(shape, dtype=np.int64)) * dtype.itemsize
570582
voxel = " x ".join(f"{z:.3g}" for z in zooms[:3])
571583
if spatial_unit != "unknown":
572584
voxel = f"{voxel} {spatial_unit}"
573585
orientation = "".join(nib.aff2axcodes(img.affine))
574586

575587
_logger.info("%s: %s", label, path)
576588
_logger.info(
577-
"%s: shape=%s, dtype=%s, voxel size=%s",
589+
"%s: shape=%s, dtype=%s, uncompressed size=%s, voxel size=%s",
578590
label,
579591
shape,
580-
hdr.get_data_dtype(),
592+
dtype,
593+
_human_bytes(n_bytes),
581594
voxel,
582595
)
583596
_logger.info(

tests/unit/test_nifti.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -634,13 +634,14 @@ class TestLogImageSummary:
634634
"""Tests for log_image_summary()."""
635635

636636
def test_3d_summary(self, nifti_3d: Path, caplog: pytest.LogCaptureFixture) -> None:
637-
"""3D input logs shape, dtype, voxel size, orientation, and spaces."""
637+
"""3D input logs shape, dtype, size, voxel size, orientation, spaces."""
638638
caplog.set_level(logging.INFO, logger="rbc.core.nifti")
639639
log_image_summary(nifti_3d, label="Anatomical T1w")
640640
text = "\n".join(caplog.messages)
641641
assert "Anatomical T1w" in text
642642
assert "shape=(5, 6, 7)" in text
643643
assert "dtype=float64" in text
644+
assert "uncompressed size=1.6 KiB" in text # 5*6*7 * 8 = 1680 bytes
644645
assert "voxel size=1 x 1 x 1 mm" in text
645646
assert "orientation=RAS" in text
646647
assert "sform=MNI" in text
@@ -663,18 +664,30 @@ def test_4d_summary_includes_volumes_and_tr(
663664
log_image_summary(nifti_4d, label="Functional BOLD")
664665
text = "\n".join(caplog.messages)
665666
assert "shape=(5, 6, 7, 10)" in text
667+
assert "uncompressed size=16.4 KiB" in text # 5*6*7*10 * 8 bytes
666668
assert "volumes=10" in text
667669
assert "slices=7" in text
668670
assert "header TR=2 s" in text
669671

670-
def test_dtype_reflects_on_disk_type(
672+
def test_dtype_and_size_reflect_on_disk_type(
671673
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
672674
) -> None:
673-
"""Logged dtype is the on-disk dtype, not float from get_fdata()."""
675+
"""Logged dtype/size use the on-disk dtype, not float64 get_fdata()."""
674676
path = _make_nifti(tmp_path, "int16.nii.gz", (4, 5, 6), dtype=np.int16)
675677
caplog.set_level(logging.INFO, logger="rbc.core.nifti")
676678
log_image_summary(path)
677-
assert any("dtype=int16" in m for m in caplog.messages)
679+
text = "\n".join(caplog.messages)
680+
assert "dtype=int16" in text
681+
assert "uncompressed size=240 B" in text # 4*5*6 * 2 bytes
682+
683+
def test_size_uses_binary_units(
684+
self, tmp_path: Path, caplog: pytest.LogCaptureFixture
685+
) -> None:
686+
"""Uncompressed size scales to binary units."""
687+
path = _make_nifti(tmp_path, "big.nii.gz", (64, 64, 64), dtype=np.int16)
688+
caplog.set_level(logging.INFO, logger="rbc.core.nifti")
689+
log_image_summary(path)
690+
assert any("uncompressed size=512.0 KiB" in m for m in caplog.messages)
678691

679692
def test_emitted_at_info_level(
680693
self, nifti_3d: Path, caplog: pytest.LogCaptureFixture

0 commit comments

Comments
 (0)