Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.
2 changes: 2 additions & 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 Expand Up @@ -274,6 +275,7 @@ scil_viz_volume_screenshot_mosaic = "scilpy.cli.scil_viz_volume_screenshot_mosa
scil_viz_volume_screenshot = "scilpy.cli.scil_viz_volume_screenshot:main"
scil_volume_apply_transform = "scilpy.cli.scil_volume_apply_transform:main"
scil_volume_b0_synthesis = "scilpy.cli.scil_volume_b0_synthesis:main"
scil_volume_modify_voxel_order = "scilpy.cli.scil_volume_modify_voxel_order:main"
scil_volume_count_non_zero_voxels = "scilpy.cli.scil_volume_count_non_zero_voxels:main"
scil_volume_crop = "scilpy.cli.scil_volume_crop:main"
scil_volume_distance_map = "scilpy.cli.scil_volume_distance_map:main"
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
7 changes: 4 additions & 3 deletions src/scilpy/cli/scil_volume_apply_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from scilpy.io.utils import (add_overwrite_arg, assert_inputs_exist,
assert_outputs_exist, add_verbose_arg,
load_matrix_in_any_format)
from scilpy.io.stateful_image import StatefulImage
from scilpy.utils.filenames import split_name_with_nii
from scilpy.version import version_string

Expand Down Expand Up @@ -72,15 +73,15 @@ def main():
transfo = load_matrix_in_any_format(args.in_transfo)
if args.inverse:
transfo = np.linalg.inv(transfo)
moving = nib.load(args.in_file)
reference = nib.load(args.in_target_file)
moving = StatefulImage.load(args.in_file)
reference = StatefulImage.load(args.in_target_file)

# Processing, saving
warped_img = apply_transform(
transfo, reference, moving, keep_dtype=args.keep_dtype,
interp=args.interpolation)

nib.save(warped_img, args.out_name)
warped_img.save(args.out_name)


if __name__ == "__main__":
Expand Down
12 changes: 6 additions & 6 deletions src/scilpy/cli/scil_volume_crop.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import argparse
import logging

import nibabel as nib
import numpy as np

from scilpy.io.utils import (add_overwrite_arg,
Expand All @@ -32,6 +31,7 @@
from scilpy.image.utils import compute_nifti_bounding_box
from scilpy.image.volume_operations import crop_volume
from scilpy.version import version_string
from scilpy.io.stateful_image import StatefulImage


def _build_arg_parser():
Expand Down Expand Up @@ -73,23 +73,23 @@ def main():
assert_inputs_exist(parser, args.in_image, args.input_bbox)
assert_outputs_exist(parser, args, args.out_image, args.output_bbox)

img = nib.load(args.in_image)
simg = StatefulImage.load(args.in_image)
if args.input_bbox:
wbbox = WorldBoundingBox.load(args.input_bbox,
args.use_deprecated_pickle)
if not args.ignore_voxel_size:
voxel_size = img.header.get_zooms()[0:3]
voxel_size = simg.header.get_zooms()[0:3]
if not np.allclose(voxel_size, wbbox.voxel_size[0:3], atol=1e-03):
raise IOError("Bounding box and data voxel sizes are not "
"compatible. Use option --ignore_voxel_size "
"to ignore this test.")
else:
wbbox = compute_nifti_bounding_box(img)
wbbox = compute_nifti_bounding_box(simg)
if args.output_bbox:
wbbox.dump(args.output_bbox, args.use_deprecated_pickle)

out_nifti_file = crop_volume(img, wbbox)
nib.save(out_nifti_file, args.out_image)
out_simg = crop_volume(simg, wbbox)
out_simg.save(args.out_image)


if __name__ == "__main__":
Expand Down
14 changes: 12 additions & 2 deletions src/scilpy/cli/scil_volume_flip.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
#! /usr/bin/env python3

# -*- coding: utf-8 -*-
"""
Flip the volume according to the specified axis.
Flip the volume according to the specified axis. In this script, axes are
referred to as 'x', 'y' and 'z', but they simply correspond to the first,
second and third dimensions of the data array.

This script only flips the data array in memory and does not modify the
image's strides or orientation information in the header. It simply
flips the numpy data.


In contrast, `scil_volume_modify_voxel_order` modifies the image header's
voxel order, but does not modify the data array.
"""

import argparse
Expand Down
83 changes: 83 additions & 0 deletions src/scilpy/cli/scil_volume_modify_voxel_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Change the voxel order (strides) of a NIfTI image.

This script allows you to change the voxel order of a NIfTI image by modifying
its header. The voxel order, also known as strides, defines the orientation of
the image data in memory. This can be useful for compatibility with different
software packages that expect a specific voxel order.
In contrast, `scil_volume_flip` only flips the data array in memory,
without changing the header's orientation information.

The new voxel order can be specified in several ways:
- As a string of 3 characters, e.g., 'RAS', 'LPS', 'ASR'.
- As a comma-separated string of 3 characters, e.g., 'R,A,S'.
- As a string of 3 numbers, e.g., '123', '231', '-12-3'.
- As a comma-separated string of 3 numbers, e.g., '1,2,3', '-1,2,-3'.

For numeric input, 1, 2, and 3 correspond to the R, A, and S axes of the
image when loaded in RAS orientation. A negative sign flips the axis.
For example., '-1,2,-3' would correspond to a voxel order of 'LAS'.

For 4D images, the voxel order must be specified numerically.
e.g., '1,2,3,4' or '1,2,3' (if the 4th dimension is time and does not
need to be reordered). The 4th dimension must be 4 or -4.

To change the header of a tractogram (.trk), we recommend converting it to a
.tck file, then converting it back to .trk with the target NIfTI image as a
reference.
"""

import argparse
import logging
import nibabel as nib

from scilpy.io.utils import (add_overwrite_arg,
add_verbose_arg,
assert_inputs_exist,
assert_outputs_exist)
from scilpy.utils.orientation import parse_voxel_order
from scilpy.io.stateful_image import StatefulImage
from scilpy.version import version_string


def _build_arg_parser():
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawTextHelpFormatter,
epilog=version_string)

p.add_argument('in_image',
help='Path of the NIfTI file to modify.')
p.add_argument('out_image',
help='Path of the modified NIfTI file to write.')
p.add_argument('--new_voxel_order', required=True,
help='The new voxel order (e.g., "RAS", "1,2,3").')

add_verbose_arg(p)
add_overwrite_arg(p)

return p


def main():
parser = _build_arg_parser()
args = parser.parse_args()
logging.getLogger().setLevel(logging.getLevelName(args.verbose))

assert_inputs_exist(parser, args.in_image)
assert_outputs_exist(parser, args, args.out_image)

img = nib.load(args.in_image)
simg = StatefulImage.load(args.in_image)

parsed_voxel_order = parse_voxel_order(args.new_voxel_order,
dimensions=len(img.shape))

simg.reorient(parsed_voxel_order)

nib.save(simg, args.out_image)


if __name__ == "__main__":
main()
27 changes: 14 additions & 13 deletions src/scilpy/cli/scil_volume_resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
assert_inputs_exist, assert_outputs_exist)
from scilpy.image.volume_operations import resample_volume
from scilpy.version import version_string
from scilpy.io.stateful_image import StatefulImage


def _build_arg_parser():
Expand Down Expand Up @@ -95,39 +96,39 @@ def main():

logging.info('Loading raw data from %s', args.in_image)

img = nib.load(args.in_image)
simg = StatefulImage.load(args.in_image)

ref_img = None
if args.ref:
ref_img = nib.load(args.ref)

# Must not verify that headers are compatible. But can verify that, at
# least, the first columns of their affines are compatible.
img_zoom_invert = [1 / zoom for zoom in img.header.get_zooms()]
img_zoom_invert = [1 / zoom for zoom in simg.header.get_zooms()]
ref_zoom_invert = [1 / zoom for zoom in ref_img.header.get_zooms()]

img_affine = np.dot(img.affine[:3, :3], img_zoom_invert)
img_affine = np.dot(simg.affine[:3, :3], img_zoom_invert)
ref_affine = np.dot(ref_img.affine[:3, :3], ref_zoom_invert)

if not np.allclose(img_affine, ref_affine):
parser.error("The --ref image should have the same affine as the "
"input image (but with a different sampling).")

# Resampling volume
resampled_img = resample_volume(img, ref_img=ref_img,
volume_shape=args.volume_size,
iso_min=args.iso_min,
voxel_res=args.voxel_size,
interp=args.interp,
enforce_dimensions=args.enforce_dimensions)
resampled_simg = resample_volume(simg, ref_img=ref_img,
volume_shape=args.volume_size,
iso_min=args.iso_min,
voxel_res=args.voxel_size,
interp=args.interp,
enforce_dimensions=args.enforce_dimensions)

# Saving results
zooms = list(resampled_img.header.get_zooms())
zooms = list(resampled_simg.header.get_zooms())
if args.voxel_size:
if len(args.voxel_size) == 1:
args.voxel_size = args.voxel_size * 3

if not np.array_equal(zooms[:3], args.voxel_size):
if not np.allclose(zooms[:3], args.voxel_size, atol=1e-3):
logging.warning('Voxel size is different from expected.'
' Got: %s, expected: %s',
tuple(zooms), tuple(args.voxel_size))
Expand All @@ -137,10 +138,10 @@ def main():
zooms[0] = args.voxel_size[0]
zooms[1] = args.voxel_size[1]
zooms[2] = args.voxel_size[2]
resampled_img.header.set_zooms(tuple(zooms))
resampled_simg.header.set_zooms(tuple(zooms))

logging.info('Saving resampled data to %s', args.out_image)
nib.save(resampled_img, args.out_image)
resampled_simg.save(args.out_image)


if __name__ == '__main__':
Expand Down
13 changes: 7 additions & 6 deletions src/scilpy/cli/scil_volume_reshape.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
assert_inputs_exist, assert_outputs_exist)
from scilpy.image.volume_operations import reshape_volume
from scilpy.version import version_string
from scilpy.io.stateful_image import StatefulImage


def _build_arg_parser():
Expand Down Expand Up @@ -80,7 +81,7 @@ def main():

logging.info('Loading raw data from %s', args.in_image)

img = nib.load(args.in_image)
simg = StatefulImage.load(args.in_image)

ref_img = None
if args.ref:
Expand All @@ -93,14 +94,14 @@ def main():
volume_shape = args.volume_size

# Resampling volume
reshaped_img = reshape_volume(img, volume_shape,
mode=args.mode,
cval=args.constant_value,
dtype=args.data_type)
reshaped_simg = reshape_volume(simg, volume_shape,
mode=args.mode,
cval=args.constant_value,
dtype=args.data_type)

# Saving results
logging.info('Saving reshaped data to %s', args.out_image)
nib.save(reshaped_img, args.out_image)
reshaped_simg.save(args.out_image)


if __name__ == '__main__':
Expand Down
Loading