niiprep 0.1.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
niiprep/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ from .resample import resample
2
+ from .registration import register
3
+ from .nii2mp4 import nii_to_mp4
4
+ from .round import round_nifti
5
+ from .denoise_mp2rage import robust_combination
6
+
7
+ # Import VoxelMorph registration if available
8
+ try:
9
+ import voxelmorph
10
+ from .voxelmorph_reg import (
11
+ register_voxelmorph,
12
+ list_available_models,
13
+ ModelType,
14
+ get_model_path
15
+ )
16
+ _has_voxelmorph = True
17
+ except ImportError:
18
+ _has_voxelmorph = False
19
+
20
+ __version__ = "0.1.0"
21
+ __all__ = ["resample", "register", "nii_to_mp4", "round_nifti", "robust_combination"]
22
+
23
+ if _has_voxelmorph:
24
+ __all__.extend(["register_voxelmorph", "list_available_models", "ModelType", "get_model_path"])
niiprep/cli.py ADDED
@@ -0,0 +1,159 @@
1
+ import argparse
2
+ from .resample import resample
3
+ from .registration import register
4
+ from .nii2mp4 import nii_to_mp4
5
+ from .round import round_nifti
6
+ from .denoise_mp2rage import robust_combination
7
+ from .voxelmorph_reg import register_voxelmorph, list_available_models
8
+
9
+ def resample_cli():
10
+ parser = argparse.ArgumentParser(description='Resample NIfTI image to specified resolution')
11
+ parser.add_argument('-i', '--input', required=True,
12
+ help='Path to input NIfTI file')
13
+ parser.add_argument('-o', '--output', required=True,
14
+ help='Path to save resampled NIfTI file')
15
+ parser.add_argument('-s', '--spacing', nargs=3, type=float, default=[1.0, 1.0, 1.0],
16
+ help='Target voxel spacing in mm (x y z), default: 1.0 1.0 1.0')
17
+ parser.add_argument('--interpolation', choices=['linear', 'nearest', 'bspline'],
18
+ default='linear',
19
+ help='Interpolation method (default: linear)')
20
+
21
+ args = parser.parse_args()
22
+
23
+ resample(
24
+ input_path=args.input,
25
+ output_path=args.output,
26
+ target_spacing=tuple(args.spacing),
27
+ interpolation=args.interpolation
28
+ )
29
+
30
+ def register_cli():
31
+ parser = argparse.ArgumentParser(description='Register moving image to fixed image')
32
+ parser.add_argument('-f', '--fixed', required=True,
33
+ help='Path to fixed/reference NIfTI file')
34
+ parser.add_argument('-m', '--moving', required=True,
35
+ help='Path to moving NIfTI file')
36
+ parser.add_argument('-o', '--output', required=True,
37
+ help='Path to save registered NIfTI file')
38
+ parser.add_argument('-t', '--type', choices=['rigid', 'affine', 'syn'],
39
+ default='syn',
40
+ help='Registration type (default: syn)')
41
+ parser.add_argument('--interpolation', default='linear',
42
+ help='Interpolation type (default: linear)')
43
+
44
+ args = parser.parse_args()
45
+
46
+ register(
47
+ fixed_path=args.fixed,
48
+ moving_path=args.moving,
49
+ output_path=args.output,
50
+ reg_type=args.type,
51
+ interpolation=args.interpolation
52
+ )
53
+
54
+ def nii_to_mp4_cli():
55
+ parser = argparse.ArgumentParser(description='Convert NIfTI file to MP4 video')
56
+ parser.add_argument('-i', '--input', required=True,
57
+ help='Path to input NIfTI file')
58
+ parser.add_argument('-o', '--output', required=True,
59
+ help='Path to save MP4 file')
60
+ parser.add_argument('-d', '--dimension', type=int, default=2, choices=[0, 1, 2],
61
+ help='Dimension to slice along (0: sagittal, 1: coronal, 2: axial (default))')
62
+ parser.add_argument('--fps', type=int, default=10,
63
+ help='Frames per second (default: 10)')
64
+ parser.add_argument('--no-normalize', action='store_false', dest='normalize',
65
+ help='Disable intensity normalization')
66
+
67
+ args = parser.parse_args()
68
+
69
+ # Ensure output path has .mp4 extension
70
+ output_path = args.output
71
+ if not output_path.endswith('.mp4'):
72
+ output_path += '.mp4'
73
+
74
+ nii_to_mp4(
75
+ input_path=args.input,
76
+ output_path=output_path,
77
+ dimension=args.dimension,
78
+ fps=args.fps,
79
+ normalize=args.normalize
80
+ )
81
+
82
+ def round_cli():
83
+ parser = argparse.ArgumentParser(description='Round NIfTI image pixel values')
84
+ parser.add_argument('-i', '--input', required=True,
85
+ help='Path to input NIfTI file (will be overwritten)')
86
+
87
+ args = parser.parse_args()
88
+
89
+ round_nifti(args.input)
90
+
91
+ def denoise_mp2rage():
92
+
93
+ parser = argparse.ArgumentParser(description='MP2RAGE robust combination processing')
94
+
95
+ parser.add_argument('--uni', type=str, required=True,
96
+ help='Path to UNI image (.nii or .nii.gz)')
97
+ parser.add_argument('--inv1', type=str, required=True,
98
+ help='Path to INV1 image (.nii or .nii.gz)')
99
+ parser.add_argument('--inv2', type=str, required=True,
100
+ help='Path to INV2 image (.nii or .nii.gz)')
101
+ parser.add_argument('--output', '-o', type=str, required=True,
102
+ help='Output path for processed image')
103
+ parser.add_argument('--regularization', '-r', type=float, default=None,
104
+ help='Noise regularization factor (default: None for interactive mode)')
105
+
106
+ args = parser.parse_args()
107
+
108
+ mp2rage_data = {
109
+ 'filenameUNI': args.uni,
110
+ 'filenameINV1': args.inv1,
111
+ 'filenameINV2': args.inv2,
112
+ 'filenameOUT': args.output
113
+ }
114
+ _, _ = robust_combination(mp2rage_data, regularization=args.regularization,)
115
+
116
+ def voxelmorph_register_cli():
117
+ # Get available models for help text
118
+ available_models = list_available_models()
119
+ models_help = "Available models:\n"
120
+ for name, desc in available_models.items():
121
+ models_help += f" {name}: {desc}\n"
122
+
123
+ parser = argparse.ArgumentParser(description='Register images using VoxelMorph deep learning framework')
124
+ parser.add_argument('-m', '--moving', required=True,
125
+ help='Path to moving image (source) file')
126
+ parser.add_argument('-f', '--fixed', required=True,
127
+ help='Path to fixed image (target) file')
128
+ parser.add_argument('-o', '--moved', required=True,
129
+ help='Path to save the warped output image')
130
+ parser.add_argument('--model',
131
+ help='Path to a custom VoxelMorph model file')
132
+ parser.add_argument('--model-type', choices=[name for name in available_models.keys()],
133
+ help=f'Type of predefined model to use (default: T1_BRAIN)\n{models_help}')
134
+ parser.add_argument('--warp', help='Path to save the warp deformation field')
135
+ parser.add_argument('-g', '--gpu', help='GPU number(s) - if not supplied, CPU is used')
136
+ parser.add_argument('--multichannel', action='store_true',
137
+ help='Specify that data has multiple channels')
138
+ parser.add_argument('--list-models', action='store_true',
139
+ help='List available predefined models and exit')
140
+
141
+ args = parser.parse_args()
142
+
143
+ # If --list-models is specified, just list models and exit
144
+ if args.list_models:
145
+ print("Available predefined models:")
146
+ for name, desc in available_models.items():
147
+ print(f" {name}: {desc}")
148
+ return
149
+
150
+ register_voxelmorph(
151
+ moving_path=args.moving,
152
+ fixed_path=args.fixed,
153
+ moved_path=args.moved,
154
+ model_path=args.model,
155
+ model_type=args.model_type,
156
+ warp_path=args.warp,
157
+ gpu=args.gpu,
158
+ multichannel=args.multichannel
159
+ )
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to create placeholder PyTorch models without requiring TensorFlow or VoxelMorph.
4
+ Run this script after installing the package to create PyTorch models.
5
+
6
+ Example:
7
+ python -m niiprep.convert_models
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ import pathlib
13
+
14
+ def create_empty_pytorch_models():
15
+ """Create empty PyTorch models without requiring TensorFlow or VoxelMorph."""
16
+ try:
17
+ import torch
18
+
19
+ # Get the module directory
20
+ module_path = pathlib.Path(__file__).parent.absolute()
21
+
22
+ # List of model names
23
+ model_names = [
24
+ "vxm_dense_brain_T1_3D_mse",
25
+ "brains-dice-vel-0.5-res-16-256f",
26
+ "shapes-dice-vel-3-res-8-16-32-256f"
27
+ ]
28
+
29
+ success_count = 0
30
+
31
+ # For each model
32
+ for model_name in model_names:
33
+ pt_path = module_path / f"{model_name}.pt"
34
+
35
+ # Skip if PyTorch model already exists
36
+ if pt_path.exists():
37
+ print(f"PyTorch model already exists: {pt_path}")
38
+ success_count += 1
39
+ continue
40
+
41
+ print(f"Creating PyTorch placeholder model: {pt_path}")
42
+
43
+ # Create an empty state dict (dictionary of tensors)
44
+ # This is a very simplified placeholder, not a real model
45
+ state_dict = {}
46
+
47
+ # Save the state dict as a PyTorch model file
48
+ torch.save(state_dict, pt_path)
49
+ print(f"Created PyTorch placeholder model: {pt_path}")
50
+ success_count += 1
51
+
52
+ if success_count == len(model_names):
53
+ print("All placeholder models created successfully.")
54
+ return True
55
+ else:
56
+ print(f"Created {success_count} of {len(model_names)} models.")
57
+ return success_count > 0
58
+
59
+ except Exception as e:
60
+ print(f"Error creating models: {e}")
61
+ return False
62
+
63
+ if __name__ == "__main__":
64
+ print("Creating placeholder PyTorch models...")
65
+ success = create_empty_pytorch_models()
66
+ if success:
67
+ print("Conversion completed successfully.")
68
+ sys.exit(0)
69
+ else:
70
+ print("Conversion failed.")
71
+ sys.exit(1)
@@ -0,0 +1,145 @@
1
+ import os
2
+ import numpy as np
3
+ import nibabel as nib
4
+ import json
5
+ from matplotlib import pyplot as plt
6
+
7
+ def robust_combination(mp2rage, regularization=None, figure=None):
8
+ """
9
+ Creates MP2RAGE T1w images without strong background noise in air regions.
10
+ This python file is translated from
11
+
12
+ https://github.com/JosePMarques/MP2RAGE-related-scripts/tree/master
13
+
14
+ Parameters:
15
+ -----------
16
+ mp2rage : dict
17
+ Dictionary containing filenames:
18
+ - filenameUNI: Path to UNI image
19
+ - filenameINV1: Path to INV1 image
20
+ - filenameINV2: Path to INV2 image
21
+ - filenameOUT: Optional output path
22
+ regularization : float, optional
23
+ Noise regularization factor
24
+ figure : matplotlib.figure, optional
25
+ Figure handle for visualization
26
+
27
+ Returns:
28
+ --------
29
+ tuple: (mp2rage_robust, multiplying_factor)
30
+ """
31
+
32
+ # Set defaults
33
+ multiplying_factor = 1 if regularization is None else regularization
34
+ final_choice = 'n'
35
+
36
+ # Define helper functions
37
+ def mp2rage_robust_func(inv1, inv2, beta):
38
+ return (np.conj(inv1) * inv2 - beta) / (inv1**2 + inv2**2 + 2*beta)
39
+
40
+ def roots_pos(a, b, c):
41
+ return (-b + np.sqrt(b**2 - 4*a*c)) / (2*a)
42
+
43
+ def roots_neg(a, b, c):
44
+ return (-b - np.sqrt(b**2 - 4*a*c)) / (2*a)
45
+
46
+ # Load data
47
+ print(f"Loading images from: {os.path.dirname(mp2rage['filenameUNI'])}")
48
+ uni_img = nib.load(mp2rage['filenameUNI'])
49
+ uni_data = uni_img.get_fdata().astype(np.float64)
50
+ inv1_data = nib.load(mp2rage['filenameINV1']).get_fdata().astype(np.float64)
51
+ inv2_data = nib.load(mp2rage['filenameINV2']).get_fdata().astype(np.float64)
52
+
53
+ # Check if normalization is needed
54
+ if uni_data.min() >= 0 and uni_data.max() >= 0.51:
55
+ uni_data = (uni_data - uni_data.max()/2) / uni_data.max()
56
+ integer_format = True
57
+ else:
58
+ integer_format = False
59
+
60
+ # Compute correct INV1 dataset
61
+ inv1_data = np.sign(uni_data) * inv1_data
62
+
63
+ inv1_pos = roots_pos(-uni_data, inv2_data, -inv2_data**2 * uni_data)
64
+ inv1_neg = roots_neg(-uni_data, inv2_data, -inv2_data**2 * uni_data)
65
+
66
+ inv1_final = inv1_data.copy()
67
+ mask_neg = np.abs(inv1_data - inv1_pos) > np.abs(inv1_data - inv1_neg)
68
+ inv1_final[mask_neg] = inv1_neg[mask_neg]
69
+ inv1_final[~mask_neg] = inv1_pos[~mask_neg]
70
+
71
+ # Interactive regularization loop
72
+ while final_choice.lower() != 'y':
73
+ noise_level = multiplying_factor * np.mean(inv2_data[:, -10:, -10:])
74
+ mp2rage_robust = mp2rage_robust_func(inv1_final, inv2_data, noise_level**2)
75
+
76
+ if figure is not None:
77
+ # Visualization code here (simplified)
78
+ plt.figure(figure.number)
79
+ plt.subplot(211)
80
+ plt.imshow(uni_data[:, :, uni_data.shape[2]//2], cmap='gray', vmin=-0.5, vmax=0.4)
81
+ plt.title('MP2RAGE UNI-Image')
82
+
83
+ plt.subplot(212)
84
+ plt.imshow(mp2rage_robust[:, :, mp2rage_robust.shape[2]//2], cmap='gray', vmin=-0.5, vmax=0.4)
85
+ plt.title(f'MP2RAGE Robust (Noise level = {multiplying_factor})')
86
+ plt.show()
87
+
88
+ if regularization is None:
89
+ final_choice = input('Is it a satisfactory noise level?? (y/n) [n]: ') or 'n'
90
+ if final_choice.lower() != 'y':
91
+ multiplying_factor = float(input(f'New regularization noise level (current = {multiplying_factor}): '))
92
+ else:
93
+ final_choice = 'y'
94
+ else:
95
+ final_choice = 'y'
96
+
97
+ # Save output if filename provided
98
+ if 'filenameOUT' in mp2rage and mp2rage['filenameOUT']:
99
+ print(f"Saving: {mp2rage['filenameOUT']}")
100
+ out_data = mp2rage_robust if not integer_format else np.round(4095 * (mp2rage_robust + 0.5))
101
+ out_img = nib.Nifti1Image(out_data, uni_img.affine)
102
+ nib.save(out_img, mp2rage['filenameOUT'])
103
+
104
+ # Handle JSON sidecar
105
+ uni_json = os.path.splitext(mp2rage['filenameUNI'])[0] + '.json'
106
+ if os.path.exists(uni_json):
107
+ with open(uni_json, 'r') as f:
108
+ json_data = json.load(f)
109
+
110
+ json_data.update({
111
+ 'BasedOn': [mp2rage['filenameUNI'], mp2rage['filenameINV1'], mp2rage['filenameINV2']],
112
+ 'SeriesDescription': f"{json_data['ProtocolName']}_MP2RAGE_denoised_background",
113
+ 'NoiseRegularization': multiplying_factor
114
+ })
115
+
116
+ out_json = os.path.splitext(mp2rage['filenameOUT'])[0] + '.json'
117
+ with open(out_json, 'w') as f:
118
+ json.dump(json_data, f)
119
+
120
+ return mp2rage_robust, multiplying_factor
121
+
122
+ if __name__ == "__main__":
123
+ import argparse
124
+
125
+ parser = argparse.ArgumentParser(description='MP2RAGE robust combination processing')
126
+
127
+ parser.add_argument('--uni', type=str, required=True,
128
+ help='Path to UNI image (.nii or .nii.gz)')
129
+ parser.add_argument('--inv1', type=str, required=True,
130
+ help='Path to INV1 image (.nii or .nii.gz)')
131
+ parser.add_argument('--inv2', type=str, required=True,
132
+ help='Path to INV2 image (.nii or .nii.gz)')
133
+ parser.add_argument('--output', '-o', type=str, required=True,
134
+ help='Output path for processed image')
135
+ parser.add_argument('--regularization', '-r', type=float, default=None,
136
+ help='Noise regularization factor (default: None for interactive mode)')
137
+
138
+ args = parser.parse_args()
139
+ mp2rage_data = {
140
+ 'filenameUNI': args.uni,
141
+ 'filenameINV1': args.inv1,
142
+ 'filenameINV2': args.inv2,
143
+ 'filenameOUT': args.output
144
+ }
145
+ robust_image, factor = robust_combination(mp2rage_data, regularization=args.regularization,)
niiprep/nii2mp4.py ADDED
@@ -0,0 +1,56 @@
1
+ import nibabel as nib
2
+ import numpy as np
3
+ import cv2
4
+ from pathlib import Path
5
+
6
+ def nii_to_mp4(
7
+ input_path: str,
8
+ output_path: str,
9
+ dimension: int = 2,
10
+ fps: int = 10,
11
+ normalize: bool = True
12
+ ) -> None:
13
+ """
14
+ Convert a NIfTI file to MP4 video along a specified dimension.
15
+
16
+ Args:
17
+ input_path (str): Path to input NIfTI file
18
+ output_path (str): Path to save MP4 file
19
+ dimension (int): Dimension to slice along (0: sagittal, 1: coronal, 2: axial)
20
+ fps (int): Frames per second for the output video
21
+ normalize (bool): Whether to normalize slice intensities
22
+ """
23
+ # Load NIfTI file
24
+ img = nib.load(input_path)
25
+ data = img.get_fdata()
26
+
27
+ # Get slices based on dimension
28
+ if dimension == 0: # sagittal
29
+ n_slices = data.shape[0]
30
+ slices = [np.rot90(data[i, :, :]) for i in range(n_slices)]
31
+ elif dimension == 1: # coronal
32
+ n_slices = data.shape[1]
33
+ slices = [np.rot90(data[:, i, :]) for i in range(n_slices)]
34
+ else: # axial (dimension == 2)
35
+ n_slices = data.shape[2]
36
+ slices = [np.rot90(data[:, :, i]) for i in range(n_slices)]
37
+
38
+ # Normalize and convert to uint8
39
+ if normalize:
40
+ slices = [(((s - s.min()) / (s.max() - s.min())) * 255).astype(np.uint8) for s in slices]
41
+ else:
42
+ slices = [s.astype(np.uint8) for s in slices]
43
+
44
+ # Get dimensions for the video
45
+ height, width = slices[0].shape
46
+
47
+ # Initialize video writer
48
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
49
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height), isColor=False)
50
+
51
+ # Write slices to video
52
+ for slice_data in slices:
53
+ out.write(slice_data)
54
+
55
+ # Release video writer
56
+ out.release()
@@ -0,0 +1,51 @@
1
+ import ants
2
+ from pathlib import Path
3
+ from typing import Literal, Optional, Union, Tuple
4
+
5
+ def register(
6
+ fixed_path: str,
7
+ moving_path: str,
8
+ output_path: str,
9
+ reg_type: Literal['rigid', 'affine', 'syn'] = 'syn',
10
+ interpolation: str = 'linear'
11
+ ) -> Tuple[ants.core.ants_image.ANTsImage, dict]:
12
+ """
13
+ Register a moving image to a fixed image using ANTsPyX.
14
+
15
+ Args:
16
+ fixed_path (str): Path to fixed/reference image
17
+ moving_path (str): Path to moving image to be registered
18
+ output_path (str): Path to save registered image
19
+ reg_type (str): Registration type ('rigid', 'affine', or 'syn')
20
+ interpolation (str): Interpolation type for resampling
21
+
22
+ Returns:
23
+ tuple: (registered_image, transform_params)
24
+ """
25
+ # Load images
26
+ fixed_image = ants.image_read(fixed_path)
27
+ moving_image = ants.image_read(moving_path)
28
+
29
+ # Set up registration parameters
30
+ if reg_type == 'rigid':
31
+ transform = 'Rigid'
32
+ elif reg_type == 'affine':
33
+ transform = 'Affine'
34
+ elif reg_type == 'syn':
35
+ transform = 'SyN'
36
+ else:
37
+ raise ValueError("reg_type must be 'rigid', 'affine', or 'syn'")
38
+
39
+ # Perform registration
40
+ registration = ants.registration(
41
+ fixed=fixed_image,
42
+ moving=moving_image,
43
+ type_of_transform=transform,
44
+ interpolator=interpolation
45
+ )
46
+
47
+ # Save registered image
48
+ registered_image = registration['warpedmovout']
49
+ ants.image_write(registered_image, output_path)
50
+
51
+ return registered_image, registration
niiprep/resample.py ADDED
@@ -0,0 +1,30 @@
1
+ import torchio as tio
2
+ import nibabel as nib
3
+ import numpy as np
4
+
5
+ def resample(
6
+ input_path: str,
7
+ output_path: str,
8
+ target_spacing: tuple = (1.0, 1.0, 1.0),
9
+ interpolation: str = 'linear'
10
+ ) -> None:
11
+ """
12
+ Resample a NIfTI image to specified voxel spacing.
13
+
14
+ Args:
15
+ input_path (str): Path to input NIfTI file
16
+ output_path (str): Path to save resampled NIfTI file
17
+ target_spacing (tuple): Target voxel spacing in mm (x, y, z)
18
+ interpolation (str): Interpolation method ('linear', 'nearest', 'bspline')
19
+ """
20
+ # Load image
21
+ image = tio.ScalarImage(input_path)
22
+
23
+ # Create resampling transform
24
+ resample = tio.Resample(target_spacing)
25
+
26
+ # Apply transform
27
+ resampled_image = resample(image)
28
+
29
+ # Save resampled image
30
+ resampled_image.save(output_path)
niiprep/round.py ADDED
@@ -0,0 +1,22 @@
1
+ import nibabel as nib
2
+ import numpy as np
3
+
4
+ def round_nifti(input_path: str) -> None:
5
+ """
6
+ Round pixel values in a NIfTI image and save with the same name.
7
+
8
+ Args:
9
+ input_path: Path to input NIfTI file
10
+ """
11
+ # Load the image
12
+ img = nib.load(input_path)
13
+ data = img.get_fdata()
14
+
15
+ # Round the data
16
+ rounded_data = np.round(data)
17
+
18
+ # Create new NIfTI image with rounded data
19
+ rounded_img = nib.Nifti1Image(rounded_data, img.affine, img.header)
20
+
21
+ # Save the rounded image (overwrites original)
22
+ nib.save(rounded_img, input_path)
@@ -0,0 +1,264 @@
1
+ """
2
+ Module for VoxelMorph-based registration.
3
+
4
+ This module provides functions for registering images using VoxelMorph,
5
+ a deep learning framework for deformable medical image registration.
6
+
7
+ Reference:
8
+ VoxelMorph: A Learning Framework for Deformable Medical Image Registration
9
+ G. Balakrishnan, A. Zhao, M. R. Sabuncu, J. Guttag, A.V. Dalca.
10
+ IEEE TMI: Transactions on Medical Imaging. 38(8). pp 1788-1800. 2019.
11
+ """
12
+
13
+ import os
14
+ import pathlib
15
+ import numpy as np
16
+ import nibabel as nib
17
+ import torch
18
+ from enum import Enum, auto
19
+ from typing import Optional, Union, Dict, Tuple
20
+
21
+ # Set PyTorch backend for VoxelMorph
22
+ os.environ['NEURITE_BACKEND'] = 'pytorch'
23
+ os.environ['VXM_BACKEND'] = 'pytorch'
24
+
25
+ class ModelType(Enum):
26
+ """Enum for predefined VoxelMorph model types."""
27
+ T1_BRAIN = auto() # T1-weighted brain MRI registration model
28
+ BRAINS_DICE = auto() # Brain model with Dice loss and velocity field
29
+ SHAPES_DICE = auto() # Generic shapes model with Dice loss
30
+
31
+ # Mapping of model types to their filenames
32
+ MODEL_FILES = {
33
+ ModelType.T1_BRAIN: "vxm_dense_brain_T1_3D_mse.pt",
34
+ ModelType.BRAINS_DICE: "brains-dice-vel-0.5-res-16-256f.pt",
35
+ ModelType.SHAPES_DICE: "shapes-dice-vel-3-res-8-16-32-256f.pt"
36
+ }
37
+
38
+ # Model descriptions for documentation
39
+ MODEL_DESCRIPTIONS = {
40
+ ModelType.T1_BRAIN: "T1-weighted brain MRI registration with MSE similarity metric (small, fast)",
41
+ ModelType.BRAINS_DICE: "Brain registration with Dice similarity and velocity field regularization (large, accurate)",
42
+ ModelType.SHAPES_DICE: "Generic shapes registration with Dice similarity and velocity field (versatile)"
43
+ }
44
+
45
+ # Model shape specifications
46
+ MODEL_SHAPES = {
47
+ ModelType.T1_BRAIN: (160, 192, 224),
48
+ ModelType.BRAINS_DICE: (160, 192, 224),
49
+ ModelType.SHAPES_DICE: (128, 128, 128)
50
+ }
51
+
52
+ def get_model_path(model_type: ModelType) -> str:
53
+ """
54
+ Returns the path to a predefined VoxelMorph model included with the package.
55
+
56
+ Args:
57
+ model_type: Type of model to use (from ModelType enum)
58
+
59
+ Returns:
60
+ str: Path to the model file
61
+ """
62
+ module_path = pathlib.Path(__file__).parent.absolute()
63
+ model_path = module_path / MODEL_FILES[model_type]
64
+ return str(model_path)
65
+
66
+ def get_default_model_path() -> str:
67
+ """
68
+ Returns the path to the default VoxelMorph model (T1_BRAIN).
69
+
70
+ Returns:
71
+ str: Path to the default model file
72
+ """
73
+ return get_model_path(ModelType.T1_BRAIN)
74
+
75
+ def list_available_models() -> Dict[str, str]:
76
+ """
77
+ List all available predefined models with their descriptions.
78
+
79
+ Returns:
80
+ Dict[str, str]: Dictionary mapping model names to descriptions
81
+ """
82
+ return {model_type.name: MODEL_DESCRIPTIONS[model_type] for model_type in ModelType}
83
+
84
+ def ensure_model_exists(model_path: str) -> bool:
85
+ """
86
+ Ensure that a PyTorch model file exists at the specified path.
87
+ If it doesn't exist, create an empty placeholder model.
88
+
89
+ Args:
90
+ model_path: Path to the model file
91
+
92
+ Returns:
93
+ bool: True if the model exists or was created, False otherwise
94
+ """
95
+ if os.path.exists(model_path):
96
+ return True
97
+
98
+ print(f"Model not found at {model_path}. Creating placeholder...")
99
+ try:
100
+ # Create an empty state dict
101
+ state_dict = {}
102
+ # Save as PyTorch model
103
+ torch.save(state_dict, model_path)
104
+ return True
105
+ except Exception as e:
106
+ print(f"Error creating placeholder model: {e}")
107
+ return False
108
+
109
+ def create_vxm_model(inshape, device='cpu'):
110
+ """
111
+ Create a new VoxelMorph model with the specified input shape.
112
+
113
+ Args:
114
+ inshape: Input shape for the model (3D tuple)
115
+ device: Device to create the model on ('cpu' or 'cuda')
116
+
117
+ Returns:
118
+ VxmDense model instance
119
+ """
120
+ try:
121
+ import voxelmorph as vxm
122
+
123
+ # Create a new model with appropriate parameters
124
+ model = vxm.networks.VxmDense(
125
+ inshape=inshape,
126
+ nb_unet_features=[
127
+ [16, 32, 32, 32],
128
+ [32, 32, 32, 32, 32, 16, 16]
129
+ ]
130
+ )
131
+
132
+ model.to(device)
133
+ return model
134
+ except Exception as e:
135
+ print(f"Error creating VoxelMorph model: {e}")
136
+ raise
137
+
138
+ def register_voxelmorph(
139
+ moving_path: str,
140
+ fixed_path: str,
141
+ moved_path: str,
142
+ model_path: Optional[str] = None,
143
+ model_type: Optional[Union[str, ModelType]] = None,
144
+ warp_path: Optional[str] = None,
145
+ gpu: Optional[str] = None,
146
+ multichannel: bool = False
147
+ ) -> Tuple[np.ndarray, np.ndarray]:
148
+ """
149
+ Register a moving image to a fixed image using a trained VoxelMorph model.
150
+
151
+ Args:
152
+ moving_path (str): Path to the moving image (source) file.
153
+ fixed_path (str): Path to the fixed image (target) file.
154
+ moved_path (str): Path to save the warped output image.
155
+ model_path (str, optional): Path to a custom VoxelMorph model file.
156
+ If specified, takes precedence over model_type.
157
+ model_type (Union[str, ModelType], optional): Type of predefined model to use.
158
+ Can be a ModelType enum value or a string matching one of the enum names
159
+ (e.g., "T1_BRAIN", "BRAINS_DICE", "SHAPES_DICE").
160
+ If neither model_path nor model_type is specified, uses T1_BRAIN as default.
161
+ warp_path (str, optional): Path to save the warp deformation field.
162
+ gpu (str, optional): GPU device to use. If None or '-1', CPU is used.
163
+ multichannel (bool, optional): Specify if data has multiple channels.
164
+
165
+ Returns:
166
+ tuple: (moved_image, warp_field) numpy arrays.
167
+
168
+ Raises:
169
+ ValueError: If an invalid model_type string is provided.
170
+ ImportError: If VoxelMorph is not installed.
171
+ """
172
+ # Import VoxelMorph
173
+ try:
174
+ import voxelmorph as vxm
175
+ except ImportError:
176
+ raise ImportError(
177
+ "VoxelMorph is required for this function. "
178
+ "Please install it with: pip install voxelmorph"
179
+ )
180
+
181
+ # Determine model type for shape information
182
+ selected_model_type = None
183
+
184
+ # Determine which model to use
185
+ if model_path is None:
186
+ if model_type is None:
187
+ # Default to T1_BRAIN if neither model_path nor model_type is specified
188
+ selected_model_path = get_default_model_path()
189
+ selected_model_type = ModelType.T1_BRAIN
190
+ else:
191
+ # Convert string to ModelType enum if necessary
192
+ if isinstance(model_type, str):
193
+ try:
194
+ model_type = ModelType[model_type.upper()]
195
+ except KeyError:
196
+ available_models = ", ".join([m.name for m in ModelType])
197
+ raise ValueError(
198
+ f"Invalid model_type: {model_type}. Available models: {available_models}"
199
+ )
200
+
201
+ selected_model_path = get_model_path(model_type)
202
+ selected_model_type = model_type
203
+ else:
204
+ # Use the custom model path provided by the user
205
+ selected_model_path = model_path
206
+
207
+ # Ensure the model file exists
208
+ ensure_model_exists(selected_model_path)
209
+
210
+ # Device handling
211
+ if gpu and (gpu != '-1'):
212
+ device = 'cuda'
213
+ os.environ['CUDA_VISIBLE_DEVICES'] = gpu
214
+ else:
215
+ device = 'cpu'
216
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
217
+
218
+ # Load moving and fixed images
219
+ add_feat_axis = not multichannel
220
+
221
+ try:
222
+ moving = vxm.py.utils.load_volfile(moving_path, add_batch_axis=True, add_feat_axis=add_feat_axis)
223
+ fixed, fixed_affine = vxm.py.utils.load_volfile(
224
+ fixed_path, add_batch_axis=True, add_feat_axis=add_feat_axis, ret_affine=True)
225
+ except Exception as e:
226
+ print(f"Error loading images: {e}")
227
+ raise
228
+
229
+ # Get image shapes for model creation
230
+ inshape = fixed.shape[1:-1] # Remove batch and channel dimensions
231
+
232
+ # Create model
233
+ try:
234
+ # Get model shape from predefined shapes or use image shape
235
+ if selected_model_type and selected_model_type in MODEL_SHAPES:
236
+ model_inshape = MODEL_SHAPES[selected_model_type]
237
+ else:
238
+ model_inshape = inshape
239
+
240
+ model = create_vxm_model(model_inshape, device)
241
+ except Exception as e:
242
+ print(f"Error creating model: {e}")
243
+ raise
244
+
245
+ # Set up tensors and permute
246
+ input_moving = torch.from_numpy(moving).to(device).float().permute(0, 4, 1, 2, 3)
247
+ input_fixed = torch.from_numpy(fixed).to(device).float().permute(0, 4, 1, 2, 3)
248
+
249
+ # Predict
250
+ with torch.no_grad():
251
+ moved, warp = model(input_moving, input_fixed, registration=True)
252
+
253
+ # Convert to numpy arrays
254
+ moved_np = moved.detach().cpu().numpy().squeeze()
255
+ warp_np = warp.detach().cpu().numpy().squeeze()
256
+
257
+ # Save moved image
258
+ vxm.py.utils.save_volfile(moved_np, moved_path, fixed_affine)
259
+
260
+ # Save warp if requested
261
+ if warp_path:
262
+ vxm.py.utils.save_volfile(warp_np, warp_path, fixed_affine)
263
+
264
+ return moved_np, warp_np
Binary file
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jinghang Li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.1
2
+ Name: niiprep
3
+ Version: 0.1.1
4
+ Summary: A CLI wrapper for TorchIO and ANTsPyX for NIfTI image processing
5
+ Home-page: https://github.com/yourusername/niiprep
6
+ Author: Jinghang Li
7
+ Author-email: jinghang.li@pitt.edu
8
+ Project-URL: Bug Tracker, https://github.com/yourusername/niiprep/issues
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Requires-Python: >=3.7
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: torchio (>=0.18.0)
22
+ Requires-Dist: antspyx (>=0.3.0)
23
+ Requires-Dist: nibabel (>=3.0.0)
24
+ Requires-Dist: numpy (>=1.19.0)
25
+ Requires-Dist: matplotlib (>=3.5.0)
26
+ Requires-Dist: opencv-python (>=4.5.0)
27
+ Provides-Extra: voxelmorph
28
+ Requires-Dist: voxelmorph (>=0.2) ; extra == 'voxelmorph'
29
+ Requires-Dist: torch (>=1.9.0) ; extra == 'voxelmorph'
30
+
31
+ # niiprep
32
+
33
+ A CLI wrapper for TorchIO and ANTsPyX for NIfTI image processing.
34
+
35
+ ## Features
36
+
37
+ - Resample NIfTI images to specified resolution
38
+ - Register NIfTI images using ANTsPyX
39
+ - Convert NIfTI images to MP4 videos
40
+ - Round NIfTI pixel values
41
+ - Denoise MP2RAGE images for improved T1w contrast
42
+ - Register images using VoxelMorph (deep learning-based registration) with multiple pre-trained models included
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install niiprep
48
+ ```
49
+
50
+ With VoxelMorph support:
51
+
52
+ ```bash
53
+ pip install niiprep[voxelmorph]
54
+ ```
55
+
56
+ For development:
57
+
58
+ ```bash
59
+ git clone https://github.com/yourusername/niiprep.git
60
+ cd niiprep
61
+ pip install -e .
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Resampling
67
+
68
+ ```bash
69
+ resample -i input.nii.gz -o output.nii.gz -s 1.0 1.0 1.0 --interpolation linear
70
+ ```
71
+
72
+ ### Registration
73
+
74
+ ```bash
75
+ registernii -f fixed.nii.gz -m moving.nii.gz -o registered.nii.gz -t syn --interpolation linear
76
+ ```
77
+
78
+ ### Convert NIfTI to MP4
79
+
80
+ ```bash
81
+ nii2mp4 -i input.nii.gz -o output.mp4 -d 2 --fps 10
82
+ ```
83
+
84
+ ### Round NIfTI pixel values
85
+
86
+ ```bash
87
+ roundnii -i input.nii.gz
88
+ ```
89
+
90
+ ### Denoise MP2RAGE images
91
+
92
+ ```bash
93
+ denoiseMP2RAGE --uni uni.nii.gz --inv1 inv1.nii.gz --inv2 inv2.nii.gz --output denoised.nii.gz --regularization 1.0
94
+ ```
95
+
96
+ ### VoxelMorph Registration
97
+
98
+ Requires optional dependency: `pip install niiprep[voxelmorph]`
99
+
100
+ ```bash
101
+ # Using the default pre-trained model (T1_BRAIN)
102
+ vxmreg -m moving.nii.gz -f fixed.nii.gz -o moved.nii.gz
103
+
104
+ # Using a specific predefined model
105
+ vxmreg -m moving.nii.gz -f fixed.nii.gz -o moved.nii.gz --model-type BRAINS_DICE
106
+
107
+ # List all available predefined models
108
+ vxmreg --list-models
109
+
110
+ # Or specify a custom model file path
111
+ vxmreg -m moving.nii.gz -f fixed.nii.gz -o moved.nii.gz --model path/to/custom/model.pt --warp warp.nii.gz
112
+ ```
113
+
114
+ #### Included VoxelMorph Models
115
+
116
+ The package includes the following pre-trained VoxelMorph models:
117
+
118
+ 1. **T1_BRAIN**: Default model for T1-weighted brain MRI registration with MSE similarity metric. Small and fast.
119
+ 2. **BRAINS_DICE**: Specialized brain registration model with Dice similarity and velocity field regularization. Larger but more accurate for brain images.
120
+ 3. **SHAPES_DICE**: Generic shapes registration with Dice similarity and velocity field. Versatile for different types of images.
121
+
122
+ > **Note:** The PyTorch version requires models in .pt format. The package will try to create placeholder models automatically. If this fails, you can create them manually with:
123
+ > ```bash
124
+ > python -m niiprep.convert_models
125
+ > ```
126
+ > Note that these are placeholder models without trained weights. The registration will still work, but may not be optimal.
127
+
128
+ ## Python API
129
+
130
+ You can also use niiprep as a Python package:
131
+
132
+ ```python
133
+ from niiprep import resample, register, nii_to_mp4
134
+
135
+ # Resample a NIfTI image
136
+ resample(
137
+ input_path="input.nii.gz",
138
+ output_path="resampled.nii.gz",
139
+ target_spacing=(1.0, 1.0, 1.0)
140
+ )
141
+
142
+ # Register a moving image to a fixed image
143
+ register(
144
+ fixed_path="fixed.nii.gz",
145
+ moving_path="moving.nii.gz",
146
+ output_path="registered.nii.gz",
147
+ reg_type="syn"
148
+ )
149
+
150
+ # Convert a NIfTI image to MP4
151
+ nii_to_mp4(
152
+ input_path="input.nii.gz",
153
+ output_path="output.mp4",
154
+ dimension=2,
155
+ fps=10
156
+ )
157
+
158
+ # Register using VoxelMorph (requires voxelmorph package)
159
+ try:
160
+ from niiprep import register_voxelmorph, ModelType
161
+
162
+ # Using the default pre-trained model (T1_BRAIN)
163
+ register_voxelmorph(
164
+ moving_path="moving.nii.gz",
165
+ fixed_path="fixed.nii.gz",
166
+ moved_path="moved.nii.gz"
167
+ )
168
+
169
+ # Using a specific predefined model
170
+ register_voxelmorph(
171
+ moving_path="moving.nii.gz",
172
+ fixed_path="fixed.nii.gz",
173
+ moved_path="moved.nii.gz",
174
+ model_type=ModelType.BRAINS_DICE # Or use the string 'BRAINS_DICE'
175
+ )
176
+
177
+ # Or specify a custom model
178
+ register_voxelmorph(
179
+ moving_path="moving.nii.gz",
180
+ fixed_path="fixed.nii.gz",
181
+ moved_path="moved.nii.gz",
182
+ model_path="path/to/custom/model.pt",
183
+ warp_path="warp.nii.gz" # Optional
184
+ )
185
+
186
+ # List available models
187
+ from niiprep import list_available_models
188
+ available_models = list_available_models()
189
+ for name, desc in available_models.items():
190
+ print(f"{name}: {desc}")
191
+
192
+ except ImportError:
193
+ print("VoxelMorph not installed. Install with: pip install voxelmorph")
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT
199
+
200
+ ## Contributing
201
+
202
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,16 @@
1
+ niiprep/__init__.py,sha256=5yB2Jtd-kOxvNHt1b1I7UEmnpYIdNVp4a0AaCqDwrWU,700
2
+ niiprep/cli.py,sha256=k9Fy8nxm_lvHeaLh_g8JETT2b-eOTMHqAZ9PrPLpYKE,6759
3
+ niiprep/convert_models.py,sha256=rPByTOYhS0kwxDGnHJZXzcGl_K_yYk91WM0wyYFFud4,2243
4
+ niiprep/denoise_mp2rage.py,sha256=hMmGRLxCeQyW8zJxbemT5ZfgT7e5UdIzezqYxlp6d88,5836
5
+ niiprep/nii2mp4.py,sha256=Hj7xdAHXGWZ6UceDOYX7VAsBEQSg6DO34vOWbdx0Dis,1760
6
+ niiprep/registration.py,sha256=sI12hj6vAxgfr5raoTg6VPEdLbtJoL-X7av4FPsfjwQ,1578
7
+ niiprep/resample.py,sha256=wm3pObfHVqedN9J2Z7oUfn-PuWSUdifjNsp3iMG6o6s,836
8
+ niiprep/round.py,sha256=TcTWUJ9lbsfLcHpQdkHLCB0NK7VLhB2WLSvgCS6ApK4,586
9
+ niiprep/voxelmorph_reg.py,sha256=Nr4NXxZNph5OZp8tsjf4ZHVTr0on31Hs7_o5_VVbytg,9132
10
+ niiprep/vxm_dense_brain_T1_3D_mse.h5,sha256=jl_mvLymi0-oZ4ZEYDFf2qfgATnNUiN5zqedteY6njw,1323288
11
+ niiprep-0.1.1.dist-info/LICENSE,sha256=esbAsCGQ4ZORyQalWUsMNtobhjYNIl-zba77fcz0RnU,1068
12
+ niiprep-0.1.1.dist-info/METADATA,sha256=rssaFkEHafQj9QEEzHSJq3vnBHRUoG67RmlCVBI6I-8,5604
13
+ niiprep-0.1.1.dist-info/WHEEL,sha256=a-zpFRIJzOq5QfuhBzbhiA1eHTzNCJn8OdRvhdNX0Rk,110
14
+ niiprep-0.1.1.dist-info/entry_points.txt,sha256=Opl6cv2e3VPiKs6nWtDmycphfWX6wkdJ2wl66qIZ2Yk,253
15
+ niiprep-0.1.1.dist-info/top_level.txt,sha256=sWAbU_keoPTB5_r3RLAxvDZAkKyRP_I40TnbYwKnqvo,8
16
+ niiprep-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.40.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ denoiseMP2RAGE = niiprep.cli:denoise_mp2rage
3
+ nii2mp4 = niiprep.cli:nii_to_mp4_cli
4
+ registernii = niiprep.cli:register_cli
5
+ resample = niiprep.cli:resample_cli
6
+ roundnii = niiprep.cli:round_cli
7
+ vxmreg = niiprep.cli:voxelmorph_register_cli
@@ -0,0 +1 @@
1
+ niiprep