Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3a30883
Add maximum operation in volume_math
CHrlS98 Jan 6, 2026
40bbc7a
Merge branch 'master' into volume-operation
CHrlS98 Jan 6, 2026
e83596a
Transfert functions for reorient
frheault Jan 6, 2026
c55dd1b
Add pathspec==0.12.* to dependencies
arnaudbore Jan 7, 2026
0588741
Update paper figures with better resolution
EmmaRenauld Jan 8, 2026
d2558c1
Merge branch 'master' into volume-operation
CHrlS98 Jan 8, 2026
b4555cc
Merge pull request #1296 from EmmaRenauld/update_figs
arnaudbore Jan 8, 2026
30304ea
Small fix in checking files in explore_bundleseg
EmmaRenauld Jan 8, 2026
5bcba00
Option -v did nothing in compute_density_map. Added loggings
EmmaRenauld Jan 8, 2026
ecc0388
Verify dir exists in bundleseg
EmmaRenauld Jan 8, 2026
f950a6f
Value args.tractogram_clustering_thr was never used in recobundles
EmmaRenauld Jan 8, 2026
c6a8a5e
Merge pull request #1293 from CHrlS98/volume-operation
arnaudbore Jan 8, 2026
ff44804
Merge pull request #1297 from EmmaRenauld/small_changes_from_my_tests
arnaudbore Jan 8, 2026
97b440b
Merge branch 'master' of https://github.com/scilus/scilpy into fix_in…
frheault Jan 14, 2026
60e5dbb
Conversion to to state class
frheault Jan 14, 2026
dc30357
New attribtutes
frheault Jan 14, 2026
d4485fe
Fix tests 3.12
frheault Jan 15, 2026
bda03f4
fix install and tests
arnaudbore Jan 15, 2026
0ba0902
Merge pull request #1299 from arnaudbore/fix_test_
arnaudbore Jan 16, 2026
c7c26d2
Merge branch 'master' of https://github.com/scilus/scilpy into fix_in…
frheault Jan 16, 2026
c260249
Added basic tests to verify SFI behaves normally
frheault Jan 16, 2026
efd61de
Improved tests
frheault Jan 16, 2026
7eb3311
Additionnal tests and flake8
frheault Jan 16, 2026
a23efeb
Update src/scilpy/io/tests/test_stateful_image.py
frheault Jan 17, 2026
9d420b9
Update src/scilpy/io/tests/test_stateful_image.py
frheault Jan 17, 2026
917901e
Copilot comments
frheault Jan 19, 2026
a6c4fae
Remove hasattr check
frheault Jan 22, 2026
26e4ff5
Incorporation to scripts
frheault Jan 25, 2026
cc1ff82
Fix conflict
frheault Jan 25, 2026
262c527
Fix conflict
frheault Jan 25, 2026
52d5617
allequal to allclose
frheault Jan 25, 2026
44e4d3a
New voxels order modification script
frheault Jan 27, 2026
efbd0a8
feat: Add 4D support for voxel order modification
frheault Jan 27, 2026
4cc1860
Fixes due to switch to static functions and changes apply_transform s…
frheault Jan 28, 2026
37ae49b
Last fixes for comments and full testing with Arnaud
frheault Jan 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test-ml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ jobs:
libfreetype6-dev \
libdrm-dev \
libgl1-mesa-dev \
libosmesa6-dev
libosmesa6-dev \
python3-dev \

- name: stdlib checkout
if: ${{ !contains(steps.python-selector.outputs.python-version, '3.12') }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ jobs:
libfreetype6-dev \
libdrm-dev \
libgl1-mesa-dev \
libosmesa6-dev
libosmesa6-dev \
python3-dev \

- name: stdlib checkout
if: ${{ !contains(steps.python-selector.outputs.python-version, '3.12') }}
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/test_tutorials.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ jobs:
libfreetype6-dev \
libdrm-dev \
libgl1-mesa-dev \
libosmesa6-dev \
wget
libosmesa6-dev \
python3-dev \
wget

- name: stdlib checkout
if: ${{ !contains(steps.python-selector.outputs.python-version, '3.12') }}
Expand Down
Binary file modified docs/source/_static/images/scilpy_paper_figure1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/_static/images/scilpy_paper_figure2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/_static/images/scilpy_paper_figure3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/_static/images/scilpy_paper_figure4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/_static/images/scilpy_paper_figure5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dependencies = [
"numpy==1.26.*",
"openpyxl==3.1.*",
"packaging==24.*",
"pathspec==0.12.*",
"pybids==0.18.*",
"PyMCubes==0.1.*",
"pyparsing==3.2.*",
Expand Down
13 changes: 9 additions & 4 deletions src/scilpy/cli/scil_bundle_explore_bundleseg.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,11 +478,16 @@ def main():
offset = 0
count = 0
for bundle in mapping.keys():
filename = glob.glob(f'{os.path.join(args.in_folder, bundle)}.t?k')[0]

if not os.path.exists(filename):
logging.warning(f'File {filename} not found.')
files = glob.glob(f'{os.path.join(args.in_folder, bundle)}.t?k')
if len(files) == 0:
logging.warning("Could not find any file fitting pattern {}"
.format(os.path.join(args.in_folder, bundle)))
continue
elif len(files) > 1:
logging.warning("Found two files for bundle {}. Selecting the "
"first one. Verify your files!".format(bundle))

filename = files[0]
count += 1

tmp_sft = load_tractogram(filename, ref_img)
Expand Down
2 changes: 2 additions & 0 deletions src/scilpy/cli/scil_tractogram_compute_density_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,15 @@ def main():
transformation, dimensions, _, _ = sft.space_attributes

# Processing
logging.info("Computing density map...")
if args.endpoints_only:
streamline_count = get_endpoints_density_map(sft)
else:
streamline_count = compute_tract_counts_map(sft.streamlines,
dimensions)

# Saving
logging.info("Saving density map {}".format(args.out_img))
dtype_to_use = np.int32
if args.binary is not None:
if args.binary == 1:
Expand Down
4 changes: 3 additions & 1 deletion src/scilpy/cli/scil_tractogram_segment_with_bundleseg.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
add_reference_arg, add_verbose_arg,
assert_inputs_exist,
assert_output_dirs_exist_and_empty,
load_matrix_in_any_format, ranged_type)
load_matrix_in_any_format, ranged_type,
assert_inputs_dirs_exist)
from scilpy.segment.voting_scheme import VotingScheme
from scilpy.version import version_string

Expand Down Expand Up @@ -136,6 +137,7 @@ def main():
logging.getLogger().setLevel(logging.getLevelName('INFO'))

# Verifications
assert_inputs_dirs_exist(parser, args.in_directory)
in_models_directories = [
os.path.join(args.in_directory, x)
for x in os.listdir(args.in_directory)
Expand Down
4 changes: 2 additions & 2 deletions src/scilpy/cli/scil_tractogram_segment_with_recobundles.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ def main():
if args.tractogram_clustering_thr and args.in_pickle:
parser.error("Option --tractogram_clustering_thr should not be "
"used with --in_pickle.")
else:
# Setting default value. (Will be ignored in args.in_pickle)
elif args.tractogram_clustering_thr is not None:
# Setting default value. (Will be ignored if args.in_pickle)
args.tractogram_clustering_thr = 8.0

# Loading
Expand Down
22 changes: 22 additions & 0 deletions src/scilpy/image/volume_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def get_array_ops():
('subtraction', subtraction),
('multiplication', multiplication),
('division', division),
('maximum', maximum),
('mean', mean),
('std', std),
('correlation', neighborhood_correlation),
Expand Down Expand Up @@ -442,6 +443,27 @@ def convert(input_list, ref_img):
return input_list[0].get_fdata(dtype=np.float64)


def maximum(input_list, ref_img):
"""
maximum: IMGs
Compute the voxel-wise maximum across images.
"""
_validate_length(input_list, 2, at_least=True)
_validate_imgs_type(*input_list, all_imgs=False)
_validate_same_shape(*input_list, ref_img, all_imgs=False)

output_data = np.zeros(ref_img.header.get_data_shape(), dtype=np.float64)
for img in input_list:
if isinstance(img, nib.Nifti1Image):
data = img.get_fdata(dtype=np.float64)
output_data = np.maximum(output_data, data)
img.uncache()
else:
output_data = np.maximum(output_data, img)

return output_data


def addition(input_list, ref_img):
"""
addition: IMGs
Expand Down
1 change: 0 additions & 1 deletion src/scilpy/io/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from scilpy.utils import is_float


def load_img(arg):
"""
Function to create the variable for scil_volume_math main function.
Expand Down
192 changes: 192 additions & 0 deletions src/scilpy/io/stateful_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-

import nibabel as nib
from dipy.io.utils import get_reference_info
from scilpy.utils.orientation import validate_axcodes


class StatefulImage(nib.Nifti1Image):
"""
A class that extends nib.Nifti1Image to manage image orientation state.

This class ensures that image data loaded into memory is always in a
consistent orientation (RAS by default), while preserving the original
on-disk orientation information. When saving, the image is automatically
reverted to its original orientation, ensuring non-destructive operations.
"""

def __init__(self, dataobj, affine, header=None, extra=None,
file_map=None, original_affine=None,
original_dimensions=None, original_voxel_sizes=None,
original_axcodes=None):
"""
Initialize a StatefulImage object.

Extends the Nifti1Image constructor to store original orientation info.
"""
super().__init__(dataobj, affine, header, extra, file_map)

# Store original image information
self._original_affine = original_affine
self._original_dimensions = original_dimensions
self._original_voxel_sizes = original_voxel_sizes
self._original_axcodes = original_axcodes

@classmethod
def load(cls, filename, to_orientation="RAS"):
"""
Load a NIfTI image, store its original orientation, and reorient it.

Parameters
----------
filename : str
Path to the NIfTI file.
to_orientation : str or tuple, optional
The target orientation for the in-memory data. Default is "RAS".

Returns
-------
StatefulImage
An instance of StatefulImage with data in the target orientation.
"""
img = nib.load(filename)

original_affine = img.affine.copy()
original_axcodes = nib.orientations.aff2axcodes(img.affine)
original_dims = img.header.get_data_shape()
original_voxel_sizes = img.header.get_zooms()

if to_orientation:
validate_axcodes(to_orientation)
start_ornt = nib.orientations.io_orientation(img.affine)
target_ornt = nib.orientations.axcodes2ornt(to_orientation)
transform = nib.orientations.ornt_transform(start_ornt,
target_ornt)
reoriented_img = img.as_reoriented(transform)
else:
reoriented_img = img

return cls(reoriented_img.dataobj, reoriented_img.affine,
reoriented_img.header, original_affine=original_affine,
original_dimensions=original_dims,
original_voxel_sizes=original_voxel_sizes,
original_axcodes=original_axcodes)

def save(self, filename):
"""
Save the image to a file, reverting to its original orientation.

Parameters
----------
filename : str
Path to save the NIfTI file.
"""
if not hasattr(self, "_original_axcodes") or \
self._original_axcodes is None:
raise ValueError(
"Unknown original orientation. Ensure the image was loaded"
"with StatefulImage.load() or that original_axcodes was"
"provided when creating the StatefulImage instance.")

self.reorient_to_original()
nib.save(self, filename)

def reorient_to_original(self):
"""
Reorient the in-memory image to its original orientation.
This method modifies the image in place. It does not return a new
Nifti1Image instance.

Raises
------
ValueError
If the original axis codes are not set.
"""
if self._original_axcodes is None:
raise ValueError(
"Original axis codes are not set cannot reorient to original"
"orientation.")
self.reorient(self._original_axcodes)

def reorient(self, target_axcodes):
"""
Reorient the in-memory image to a target orientation.

Parameters
----------
target_axcodes : str or tuple
The target orientation axis codes (e.g., "LPS", ("R", "A", "S")).
"""
validate_axcodes(target_axcodes)

current_axcodes = nib.orientations.aff2axcodes(self.affine)
if current_axcodes == tuple(target_axcodes):
return

start_ornt = nib.orientations.axcodes2ornt(current_axcodes)
target_ornt = nib.orientations.axcodes2ornt(target_axcodes)
transform = nib.orientations.ornt_transform(start_ornt, target_ornt)

reoriented_img = self.as_reoriented(transform)
self.__init__(reoriented_img.dataobj, reoriented_img.affine,
reoriented_img.header,
original_affine=self._original_affine,
original_dimensions=self._original_dimensions,
original_voxel_sizes=self._original_voxel_sizes,
original_axcodes=self._original_axcodes)

def to_ras(self):
"""Convenience method to reorient in-memory data to RAS."""
self.reorient(("R", "A", "S"))

def to_lps(self):
"""Convenience method to reorient in-memory data to LPS."""
self.reorient(("L", "P", "S"))

def to_reference(self, obj):
"""
Reorient the in-memory image to match the orientation of a reference
object.

Parameters
----------
obj : object
Reference object from which orientation information can be obtained.
Must not be an instance of ``StatefulImage``.

Raises
------
TypeError
If ``obj`` is an instance of ``StatefulImage``.
"""

if isinstance(obj, StatefulImage):
raise TypeError("Reference object cannot be a StatefulImage.")

_, _, _, voxel_order = get_reference_info(obj)
self.reorient(voxel_order)

@property
def axcodes(self):
"""Get the axis codes for the current image orientation."""
return nib.orientations.aff2axcodes(self.affine)

@property
def original_axcodes(self):
"""Get the axis codes for the original image orientation."""
return self._original_axcodes

def __str__(self):
"""Return a string representation of the image, including orientation."""
base_str = super().__str__()
current_axcodes = self.axcodes
reoriented = current_axcodes != self._original_axcodes

orientation_info = (
f"Reorientation Information:\n"
f" Original axis codes: {self._original_axcodes}\n"
f" Current axis codes: {current_axcodes}\n"
f" Reoriented from original: {reoriented}"
)

return f"{base_str}\n{orientation_info}"
Empty file added src/scilpy/io/tests/__init__.py
Empty file.
Loading