niiprep 0.1.0__tar.gz

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-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: niiprep
3
+ Version: 0.1.0
4
+ Summary: A CLI wrapper for TorchIO and ANTsPyX for NIfTI image processing
5
+ Author: Jinghang Li
6
+ Author-email: jinghang.li@pitt.edu
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Requires-Python: >=3.7
14
+ Requires-Dist: torchio>=0.18.0
15
+ Requires-Dist: antspyx>=0.3.0
16
+ Requires-Dist: nibabel>=3.0.0
17
+ Requires-Dist: numpy>=1.19.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
niiprep-0.1.0/setup.py ADDED
@@ -0,0 +1,40 @@
1
+ from setuptools import setup, find_packages
2
+
3
+
4
+ setup(
5
+ name="niiprep",
6
+ version="0.1.0",
7
+ author="Jinghang Li",
8
+ author_email="jinghang.li@pitt.edu",
9
+ description="A CLI wrapper for TorchIO and ANTsPyX for NIfTI image processing",
10
+ package_dir={"": "src"},
11
+ packages=find_packages(where="src"),
12
+ package_data={
13
+ 'niiprep': [''],
14
+ },
15
+ include_package_data=True,
16
+ classifiers=[
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.7",
23
+ ],
24
+ python_requires=">=3.7",
25
+ install_requires=[
26
+ "torchio>=0.18.0",
27
+ "antspyx>=0.3.0",
28
+ "nibabel>=3.0.0",
29
+ "numpy>=1.19.0",
30
+ ],
31
+ entry_points={
32
+ 'console_scripts': [
33
+ 'resample=niiprep.cli:resample_cli',
34
+ 'registernii=niiprep.cli:register_cli',
35
+ 'nii2mp4=niiprep.cli:nii_to_mp4_cli',
36
+ 'roundnii=niiprep.cli:round_cli',
37
+ 'denoiseMP2RAGE=niiprep.cli:denoise_mp2rage'
38
+ ],
39
+ },
40
+ )
@@ -0,0 +1,6 @@
1
+ from .resample import resample
2
+ from .registration import register
3
+ from .nii2mp4 import nii_to_mp4
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["resample", "register", "nii_to_mp4"]
@@ -0,0 +1,113 @@
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
+
8
+ def resample_cli():
9
+ parser = argparse.ArgumentParser(description='Resample NIfTI image to specified resolution')
10
+ parser.add_argument('-i', '--input', required=True,
11
+ help='Path to input NIfTI file')
12
+ parser.add_argument('-o', '--output', required=True,
13
+ help='Path to save resampled NIfTI file')
14
+ parser.add_argument('-s', '--spacing', nargs=3, type=float, default=[1.0, 1.0, 1.0],
15
+ help='Target voxel spacing in mm (x y z), default: 1.0 1.0 1.0')
16
+ parser.add_argument('--interpolation', choices=['linear', 'nearest', 'bspline'],
17
+ default='linear',
18
+ help='Interpolation method (default: linear)')
19
+
20
+ args = parser.parse_args()
21
+
22
+ resample(
23
+ input_path=args.input,
24
+ output_path=args.output,
25
+ target_spacing=tuple(args.spacing),
26
+ interpolation=args.interpolation
27
+ )
28
+
29
+ def register_cli():
30
+ parser = argparse.ArgumentParser(description='Register moving image to fixed image')
31
+ parser.add_argument('-f', '--fixed', required=True,
32
+ help='Path to fixed/reference NIfTI file')
33
+ parser.add_argument('-m', '--moving', required=True,
34
+ help='Path to moving NIfTI file')
35
+ parser.add_argument('-o', '--output', required=True,
36
+ help='Path to save registered NIfTI file')
37
+ parser.add_argument('-t', '--type', choices=['rigid', 'affine', 'syn'],
38
+ default='syn',
39
+ help='Registration type (default: syn)')
40
+ parser.add_argument('--interpolation', default='linear',
41
+ help='Interpolation type (default: linear)')
42
+
43
+ args = parser.parse_args()
44
+
45
+ register(
46
+ fixed_path=args.fixed,
47
+ moving_path=args.moving,
48
+ output_path=args.output,
49
+ reg_type=args.type,
50
+ interpolation=args.interpolation
51
+ )
52
+
53
+ def nii_to_mp4_cli():
54
+ parser = argparse.ArgumentParser(description='Convert NIfTI file to MP4 video')
55
+ parser.add_argument('-i', '--input', required=True,
56
+ help='Path to input NIfTI file')
57
+ parser.add_argument('-o', '--output', required=True,
58
+ help='Path to save MP4 file')
59
+ parser.add_argument('-d', '--dimension', type=int, default=2, choices=[0, 1, 2],
60
+ help='Dimension to slice along (0: sagittal, 1: coronal, 2: axial (default))')
61
+ parser.add_argument('--fps', type=int, default=10,
62
+ help='Frames per second (default: 10)')
63
+ parser.add_argument('--no-normalize', action='store_false', dest='normalize',
64
+ help='Disable intensity normalization')
65
+
66
+ args = parser.parse_args()
67
+
68
+ # Ensure output path has .mp4 extension
69
+ output_path = args.output
70
+ if not output_path.endswith('.mp4'):
71
+ output_path += '.mp4'
72
+
73
+ nii_to_mp4(
74
+ input_path=args.input,
75
+ output_path=output_path,
76
+ dimension=args.dimension,
77
+ fps=args.fps,
78
+ normalize=args.normalize
79
+ )
80
+
81
+ def round_cli():
82
+ parser = argparse.ArgumentParser(description='Round NIfTI image pixel values')
83
+ parser.add_argument('-i', '--input', required=True,
84
+ help='Path to input NIfTI file (will be overwritten)')
85
+
86
+ args = parser.parse_args()
87
+
88
+ round_nifti(args.input)
89
+
90
+ def denoise_mp2rage():
91
+
92
+ parser = argparse.ArgumentParser(description='MP2RAGE robust combination processing')
93
+
94
+ parser.add_argument('--uni', type=str, required=True,
95
+ help='Path to UNI image (.nii or .nii.gz)')
96
+ parser.add_argument('--inv1', type=str, required=True,
97
+ help='Path to INV1 image (.nii or .nii.gz)')
98
+ parser.add_argument('--inv2', type=str, required=True,
99
+ help='Path to INV2 image (.nii or .nii.gz)')
100
+ parser.add_argument('--output', '-o', type=str, required=True,
101
+ help='Output path for processed image')
102
+ parser.add_argument('--regularization', '-r', type=float, default=None,
103
+ help='Noise regularization factor (default: None for interactive mode)')
104
+
105
+ args = parser.parse_args()
106
+
107
+ mp2rage_data = {
108
+ 'filenameUNI': args.uni,
109
+ 'filenameINV1': args.inv1,
110
+ 'filenameINV2': args.inv2,
111
+ 'filenameOUT': args.output
112
+ }
113
+ _, _ = robust_combination(mp2rage_data, regularization=args.regularization,)
@@ -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,)
@@ -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
@@ -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)
@@ -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,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: niiprep
3
+ Version: 0.1.0
4
+ Summary: A CLI wrapper for TorchIO and ANTsPyX for NIfTI image processing
5
+ Author: Jinghang Li
6
+ Author-email: jinghang.li@pitt.edu
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Requires-Python: >=3.7
14
+ Requires-Dist: torchio>=0.18.0
15
+ Requires-Dist: antspyx>=0.3.0
16
+ Requires-Dist: nibabel>=3.0.0
17
+ Requires-Dist: numpy>=1.19.0
@@ -0,0 +1,14 @@
1
+ setup.py
2
+ src/niiprep/__init__.py
3
+ src/niiprep/cli.py
4
+ src/niiprep/denoise_mp2rage.py
5
+ src/niiprep/nii2mp4.py
6
+ src/niiprep/registration.py
7
+ src/niiprep/resample.py
8
+ src/niiprep/round.py
9
+ src/niiprep.egg-info/PKG-INFO
10
+ src/niiprep.egg-info/SOURCES.txt
11
+ src/niiprep.egg-info/dependency_links.txt
12
+ src/niiprep.egg-info/entry_points.txt
13
+ src/niiprep.egg-info/requires.txt
14
+ src/niiprep.egg-info/top_level.txt
@@ -0,0 +1,6 @@
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
@@ -0,0 +1,4 @@
1
+ torchio>=0.18.0
2
+ antspyx>=0.3.0
3
+ nibabel>=3.0.0
4
+ numpy>=1.19.0
@@ -0,0 +1 @@
1
+ niiprep