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 +24 -0
- niiprep/cli.py +159 -0
- niiprep/convert_models.py +71 -0
- niiprep/denoise_mp2rage.py +145 -0
- niiprep/nii2mp4.py +56 -0
- niiprep/registration.py +51 -0
- niiprep/resample.py +30 -0
- niiprep/round.py +22 -0
- niiprep/voxelmorph_reg.py +264 -0
- niiprep/vxm_dense_brain_T1_3D_mse.h5 +0 -0
- niiprep-0.1.1.dist-info/LICENSE +21 -0
- niiprep-0.1.1.dist-info/METADATA +202 -0
- niiprep-0.1.1.dist-info/RECORD +16 -0
- niiprep-0.1.1.dist-info/WHEEL +6 -0
- niiprep-0.1.1.dist-info/entry_points.txt +7 -0
- niiprep-0.1.1.dist-info/top_level.txt +1 -0
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()
|
niiprep/registration.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
niiprep
|