antspymm 1.5.5__py3-none-any.whl → 1.5.6__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.
- antspymm/__init__.py +4 -0
- antspymm/mm.py +212 -0
- {antspymm-1.5.5.dist-info → antspymm-1.5.6.dist-info}/METADATA +1 -1
- antspymm-1.5.6.dist-info/RECORD +6 -0
- antspymm-1.5.5.dist-info/RECORD +0 -6
- {antspymm-1.5.5.dist-info → antspymm-1.5.6.dist-info}/WHEEL +0 -0
- {antspymm-1.5.5.dist-info → antspymm-1.5.6.dist-info}/top_level.txt +0 -0
antspymm/__init__.py
CHANGED
@@ -65,6 +65,8 @@ from .mm import mc_denoise
|
|
65
65
|
from .mm import mc_reg
|
66
66
|
from .mm import dti_reg
|
67
67
|
from .mm import timeseries_reg
|
68
|
+
from .mm import timeseries_transform
|
69
|
+
from .mm import copy_spatial_metadata_from_3d_to_4d
|
68
70
|
from .mm import concat_dewarp
|
69
71
|
from .mm import mc_resample_image_to_target
|
70
72
|
from .mm import trim_dti_mask
|
@@ -136,5 +138,7 @@ from .mm import segment_timeseries_by_bvalue
|
|
136
138
|
from .mm import shorten_pymm_names
|
137
139
|
from .mm import pet3d_summary
|
138
140
|
from .mm import deformation_gradient_optimized
|
141
|
+
from .mm import efficient_dwi_fit_voxelwise
|
142
|
+
from .mm import generate_voxelwise_bvecs
|
139
143
|
|
140
144
|
|
antspymm/mm.py
CHANGED
@@ -1794,6 +1794,89 @@ def merge_timeseries_data( img_LR, img_RL, allow_resample=True ):
|
|
1794
1794
|
mimg.append( temp )
|
1795
1795
|
return ants.list_to_ndimage( img_LR, mimg )
|
1796
1796
|
|
1797
|
+
def copy_spatial_metadata_from_3d_to_4d(spatial_img, timeseries_img):
|
1798
|
+
"""
|
1799
|
+
Copy spatial metadata (origin, spacing, direction) from a 3D image to the
|
1800
|
+
spatial dimensions (first 3) of a 4D image, preserving the 4th dimension's metadata.
|
1801
|
+
|
1802
|
+
Parameters
|
1803
|
+
----------
|
1804
|
+
spatial_img : ants.ANTsImage
|
1805
|
+
A 3D ANTsImage with the desired spatial metadata.
|
1806
|
+
timeseries_img : ants.ANTsImage
|
1807
|
+
A 4D ANTsImage to update.
|
1808
|
+
|
1809
|
+
Returns
|
1810
|
+
-------
|
1811
|
+
ants.ANTsImage
|
1812
|
+
A 4D ANTsImage with updated spatial metadata.
|
1813
|
+
"""
|
1814
|
+
if spatial_img.dimension != 3:
|
1815
|
+
raise ValueError("spatial_img must be a 3D ANTsImage.")
|
1816
|
+
if timeseries_img.dimension != 4:
|
1817
|
+
raise ValueError("timeseries_img must be a 4D ANTsImage.")
|
1818
|
+
# Get 3D metadata
|
1819
|
+
spatial_origin = list(spatial_img.origin)
|
1820
|
+
spatial_spacing = list(spatial_img.spacing)
|
1821
|
+
spatial_direction = spatial_img.direction # 3x3
|
1822
|
+
# Get original 4D metadata
|
1823
|
+
ts_spacing = list(timeseries_img.spacing)
|
1824
|
+
ts_origin = list(timeseries_img.origin)
|
1825
|
+
ts_direction = timeseries_img.direction # 4x4
|
1826
|
+
# Replace only the first 3 entries for origin and spacing
|
1827
|
+
new_origin = spatial_origin + [ts_origin[3]]
|
1828
|
+
new_spacing = spatial_spacing + [ts_spacing[3]]
|
1829
|
+
# Replace top-left 3x3 block of direction matrix, preserve last row/column
|
1830
|
+
new_direction = ts_direction.copy()
|
1831
|
+
new_direction[:3, :3] = spatial_direction
|
1832
|
+
# Create updated image
|
1833
|
+
updated_img = ants.from_numpy(
|
1834
|
+
timeseries_img.numpy(),
|
1835
|
+
origin=new_origin,
|
1836
|
+
spacing=new_spacing,
|
1837
|
+
direction=new_direction
|
1838
|
+
)
|
1839
|
+
return updated_img
|
1840
|
+
|
1841
|
+
def timeseries_transform(transform, image, reference, interpolation='linear'):
|
1842
|
+
"""
|
1843
|
+
Apply a spatial transform to each 3D volume in a 4D time series image.
|
1844
|
+
|
1845
|
+
Parameters
|
1846
|
+
----------
|
1847
|
+
transform : ants transform object
|
1848
|
+
Path(s) to ANTs-compatible transform(s) to apply.
|
1849
|
+
image : ants.ANTsImage
|
1850
|
+
4D input image with shape (X, Y, Z, T).
|
1851
|
+
reference : ants.ANTsImage
|
1852
|
+
Reference image to match in space.
|
1853
|
+
interpolation : str
|
1854
|
+
Interpolation method: 'linear', 'nearestNeighbor', etc.
|
1855
|
+
|
1856
|
+
Returns
|
1857
|
+
-------
|
1858
|
+
ants.ANTsImage
|
1859
|
+
4D transformed image.
|
1860
|
+
"""
|
1861
|
+
if image.dimension != 4:
|
1862
|
+
raise ValueError("Input image must be 4D (X, Y, Z, T).")
|
1863
|
+
n_volumes = image.shape[3]
|
1864
|
+
transformed_volumes = []
|
1865
|
+
for t in range(n_volumes):
|
1866
|
+
vol = ants.slice_image( image, 3, t )
|
1867
|
+
transformed = ants.apply_ants_transform_to_image(
|
1868
|
+
transform=transform,
|
1869
|
+
image=vol,
|
1870
|
+
reference=reference,
|
1871
|
+
interpolation=interpolation
|
1872
|
+
)
|
1873
|
+
transformed_volumes.append(transformed.numpy())
|
1874
|
+
# Stack along time axis and convert to ANTsImage
|
1875
|
+
transformed_array = np.stack(transformed_volumes, axis=-1)
|
1876
|
+
out_image = ants.from_numpy(transformed_array)
|
1877
|
+
out_image = ants.copy_image_info(image, out_image)
|
1878
|
+
out_image = copy_spatial_metadata_from_3d_to_4d(reference, out_image)
|
1879
|
+
return out_image
|
1797
1880
|
|
1798
1881
|
def timeseries_reg(
|
1799
1882
|
image,
|
@@ -3932,6 +4015,135 @@ def efficient_dwi_fit(gtab, diffusion_model, imagein, maskin,
|
|
3932
4015
|
return full_fit, FA_img, MD_img, RGB_img
|
3933
4016
|
|
3934
4017
|
|
4018
|
+
def efficient_dwi_fit_voxelwise(imagein, maskin, bvals, bvecs_5d, model_params=None,
|
4019
|
+
bvals_to_use=None, num_threads=1, verbose=True):
|
4020
|
+
"""
|
4021
|
+
Voxel-wise diffusion model fitting with individual b-vectors per voxel.
|
4022
|
+
|
4023
|
+
Parameters
|
4024
|
+
----------
|
4025
|
+
imagein : ants.ANTsImage
|
4026
|
+
4D DWI image (X, Y, Z, N).
|
4027
|
+
maskin : ants.ANTsImage
|
4028
|
+
3D binary mask.
|
4029
|
+
bvals : (N,) array-like
|
4030
|
+
Common b-values across volumes.
|
4031
|
+
bvecs_5d : (X, Y, Z, N, 3) ndarray
|
4032
|
+
Voxel-specific b-vectors.
|
4033
|
+
model_params : dict
|
4034
|
+
Extra arguments for model.
|
4035
|
+
bvals_to_use : list[int]
|
4036
|
+
Subset of b-values to include.
|
4037
|
+
num_threads : int
|
4038
|
+
Number of threads to use.
|
4039
|
+
verbose : bool
|
4040
|
+
Whether to print status.
|
4041
|
+
|
4042
|
+
Returns
|
4043
|
+
-------
|
4044
|
+
FA_img : ants.ANTsImage
|
4045
|
+
Fractional anisotropy.
|
4046
|
+
MD_img : ants.ANTsImage
|
4047
|
+
Mean diffusivity.
|
4048
|
+
RGB_img : ants.ANTsImage
|
4049
|
+
RGB FA image.
|
4050
|
+
"""
|
4051
|
+
import numpy as np
|
4052
|
+
import ants
|
4053
|
+
import dipy.reconst.dti as dti
|
4054
|
+
from dipy.core.gradients import gradient_table
|
4055
|
+
from dipy.reconst.dti import fractional_anisotropy, color_fa, mean_diffusivity
|
4056
|
+
from concurrent.futures import ThreadPoolExecutor
|
4057
|
+
from tqdm import tqdm
|
4058
|
+
|
4059
|
+
model_params = model_params or {}
|
4060
|
+
img = imagein.numpy()
|
4061
|
+
mask = maskin.numpy().astype(bool)
|
4062
|
+
X, Y, Z, N = img.shape
|
4063
|
+
|
4064
|
+
if bvals_to_use is not None:
|
4065
|
+
sel = np.isin(bvals, bvals_to_use)
|
4066
|
+
img = img[..., sel]
|
4067
|
+
bvals = bvals[sel]
|
4068
|
+
bvecs_5d = bvecs_5d[..., sel, :]
|
4069
|
+
|
4070
|
+
FA = np.zeros((X, Y, Z), dtype=np.float32)
|
4071
|
+
MD = np.zeros((X, Y, Z), dtype=np.float32)
|
4072
|
+
RGB = np.zeros((X, Y, Z, 3), dtype=np.float32)
|
4073
|
+
|
4074
|
+
def fit_voxel(ix, iy, iz):
|
4075
|
+
if not mask[ix, iy, iz]:
|
4076
|
+
return
|
4077
|
+
sig = img[ix, iy, iz, :]
|
4078
|
+
if np.all(sig == 0):
|
4079
|
+
return
|
4080
|
+
bv = bvecs_5d[ix, iy, iz, :, :]
|
4081
|
+
gtab = gradient_table(bvals, bv)
|
4082
|
+
try:
|
4083
|
+
model = dti.TensorModel(gtab, **model_params)
|
4084
|
+
fit = model.fit(sig)
|
4085
|
+
evals = fit.evals
|
4086
|
+
evecs = fit.evecs
|
4087
|
+
FA[ix, iy, iz] = fractional_anisotropy(evals)
|
4088
|
+
MD[ix, iy, iz] = mean_diffusivity(evals)
|
4089
|
+
RGB[ix, iy, iz, :] = color_fa(FA[ix, iy, iz], evecs)
|
4090
|
+
except Exception as e:
|
4091
|
+
if verbose:
|
4092
|
+
print(f"Voxel ({ix},{iy},{iz}) fit failed: {e}")
|
4093
|
+
|
4094
|
+
coords = np.argwhere(mask)
|
4095
|
+
if verbose:
|
4096
|
+
print(f"[INFO] Fitting {len(coords)} voxels using {num_threads} threads...")
|
4097
|
+
|
4098
|
+
if num_threads > 1:
|
4099
|
+
with ThreadPoolExecutor(max_workers=num_threads) as executor:
|
4100
|
+
list(tqdm(executor.map(lambda c: fit_voxel(*c), coords), total=len(coords)))
|
4101
|
+
else:
|
4102
|
+
for c in tqdm(coords):
|
4103
|
+
fit_voxel(*c)
|
4104
|
+
|
4105
|
+
ref = ants.slice_image(imagein, axis=3, idx=0)
|
4106
|
+
return (
|
4107
|
+
ants.copy_image_info(ref, ants.from_numpy(FA)),
|
4108
|
+
ants.copy_image_info(ref, ants.from_numpy(MD)),
|
4109
|
+
ants.merge_channels([ants.copy_image_info(ref, ants.from_numpy(RGB[..., i])) for i in range(3)])
|
4110
|
+
)
|
4111
|
+
|
4112
|
+
|
4113
|
+
def generate_voxelwise_bvecs(global_bvecs, voxel_rotations, transpose=False):
|
4114
|
+
"""
|
4115
|
+
Generate voxel-wise b-vectors from a global bvec and voxel-wise rotation field.
|
4116
|
+
|
4117
|
+
Parameters
|
4118
|
+
----------
|
4119
|
+
global_bvecs : ndarray of shape (N, 3)
|
4120
|
+
Global diffusion gradient directions.
|
4121
|
+
voxel_rotations : ndarray of shape (X, Y, Z, 3, 3)
|
4122
|
+
3x3 rotation matrix for each voxel (can come from Jacobian of deformation field).
|
4123
|
+
transpose : bool, optional
|
4124
|
+
If True, transpose the rotation matrices before applying them to the b-vectors.
|
4125
|
+
|
4126
|
+
|
4127
|
+
Returns
|
4128
|
+
-------
|
4129
|
+
bvecs_5d : ndarray of shape (X, Y, Z, N, 3)
|
4130
|
+
Voxel-specific b-vectors.
|
4131
|
+
"""
|
4132
|
+
X, Y, Z, _, _ = voxel_rotations.shape
|
4133
|
+
N = global_bvecs.shape[0]
|
4134
|
+
bvecs_5d = np.zeros((X, Y, Z, N, 3), dtype=np.float32)
|
4135
|
+
|
4136
|
+
for n in range(N):
|
4137
|
+
bvec = global_bvecs[n]
|
4138
|
+
for i in range(X):
|
4139
|
+
for j in range(Y):
|
4140
|
+
for k in range(Z):
|
4141
|
+
R = voxel_rotations[i, j, k]
|
4142
|
+
if transpose:
|
4143
|
+
R = R.T # Use transpose if needed
|
4144
|
+
bvecs_5d[i, j, k, n, :] = R @ bvec
|
4145
|
+
return bvecs_5d
|
4146
|
+
|
3935
4147
|
def dipy_dti_recon(
|
3936
4148
|
image,
|
3937
4149
|
bvalsfn,
|
@@ -0,0 +1,6 @@
|
|
1
|
+
antspymm/__init__.py,sha256=50wAFf04ZlF7wYg1h07dFLKebGOGFwKCJfAcdyHhSXw,4858
|
2
|
+
antspymm/mm.py,sha256=mtkFJi0ZwLFw32vCrBdZcGa4ITgIOSn_Vfwq1SXLdqE,543752
|
3
|
+
antspymm-1.5.6.dist-info/METADATA,sha256=0H4Olfd04VZNR56ErUfVd0hEo8DppIsDrWWbks0JgHs,26007
|
4
|
+
antspymm-1.5.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
5
|
+
antspymm-1.5.6.dist-info/top_level.txt,sha256=iyD1sRhCKzfwKRJLq5ZUeV9xsv1cGQl8Ejp6QwXM1Zg,9
|
6
|
+
antspymm-1.5.6.dist-info/RECORD,,
|
antspymm-1.5.5.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
antspymm/__init__.py,sha256=hynrdvZDlPQ0Wam8tU6mBtbEk0Worwz_bLZk9N7N1CM,4684
|
2
|
-
antspymm/mm.py,sha256=Wln4YRRbzlZwLBmXCWODtd0X_6S7-4PgvpahSg9RW54,536460
|
3
|
-
antspymm-1.5.5.dist-info/METADATA,sha256=5w6LSE2Hi2NAGvcIp0cXh87bvM_u072hIXRtAb_k5Pg,26007
|
4
|
-
antspymm-1.5.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
5
|
-
antspymm-1.5.5.dist-info/top_level.txt,sha256=iyD1sRhCKzfwKRJLq5ZUeV9xsv1cGQl8Ejp6QwXM1Zg,9
|
6
|
-
antspymm-1.5.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|