antspymm 1.5.5__py3-none-any.whl → 1.5.7__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 +6 -0
- antspymm/mm.py +246 -0
- {antspymm-1.5.5.dist-info → antspymm-1.5.7.dist-info}/METADATA +1 -1
- antspymm-1.5.7.dist-info/RECORD +6 -0
- antspymm-1.5.5.dist-info/RECORD +0 -6
- {antspymm-1.5.5.dist-info → antspymm-1.5.7.dist-info}/WHEEL +0 -0
- {antspymm-1.5.5.dist-info → antspymm-1.5.7.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,9 @@ 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
|
143
|
+
from .mm import distortion_correct_bvecs
|
144
|
+
|
139
145
|
|
140
146
|
|
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,
|
@@ -2055,6 +2138,40 @@ def bvec_reorientation( motion_parameters, bvecs, rebase=None ):
|
|
2055
2138
|
bvecs[myidx,:] = np.dot( rebase, bvecs[myidx,:] )
|
2056
2139
|
return bvecs
|
2057
2140
|
|
2141
|
+
|
2142
|
+
def distortion_correct_bvecs(bvecs, def_grad, A_img, A_ref):
|
2143
|
+
"""
|
2144
|
+
Vectorized computation of voxel-wise distortion corrected b-vectors.
|
2145
|
+
|
2146
|
+
Parameters
|
2147
|
+
----------
|
2148
|
+
bvecs : ndarray (N, 3)
|
2149
|
+
def_grad : ndarray (X, Y, Z, 3, 3) containing rotations derived from the deformation gradient
|
2150
|
+
A_img : ndarray (3, 3) direction matrix of the fixed image (target undistorted space)
|
2151
|
+
A_ref : ndarray (3, 3) direction matrix of the moving image (being corrected)
|
2152
|
+
|
2153
|
+
Returns
|
2154
|
+
-------
|
2155
|
+
bvecs_5d : ndarray (X, Y, Z, N, 3)
|
2156
|
+
"""
|
2157
|
+
X, Y, Z = def_grad.shape[:3]
|
2158
|
+
N = bvecs.shape[0]
|
2159
|
+
# Combined rotation: R_voxel = A_ref.T @ A_img @ def_grad
|
2160
|
+
A = A_ref.T @ A_img
|
2161
|
+
R_voxel = np.einsum('ij,xyzjk->xyzik', A, def_grad) # (X, Y, Z, 3, 3)
|
2162
|
+
# Apply R_voxel.T @ bvecs
|
2163
|
+
# First, reshape R_voxel: (X*Y*Z, 3, 3)
|
2164
|
+
R_voxel_reshaped = R_voxel.reshape(-1, 3, 3)
|
2165
|
+
# Rotate all bvecs for each voxel
|
2166
|
+
# Output: (X*Y*Z, N, 3)
|
2167
|
+
rotated = np.einsum('vij,nj->vni', R_voxel_reshaped, bvecs)
|
2168
|
+
# Normalize
|
2169
|
+
norms = np.linalg.norm(rotated, axis=2, keepdims=True)
|
2170
|
+
rotated /= np.clip(norms, 1e-8, None)
|
2171
|
+
# Reshape back to (X, Y, Z, N, 3)
|
2172
|
+
bvecs_5d = rotated.reshape(X, Y, Z, N, 3)
|
2173
|
+
return bvecs_5d
|
2174
|
+
|
2058
2175
|
def get_dti( reference_image, tensormodel, upper_triangular=True, return_image=False ):
|
2059
2176
|
"""
|
2060
2177
|
extract DTI data from a dipy tensormodel
|
@@ -3932,6 +4049,135 @@ def efficient_dwi_fit(gtab, diffusion_model, imagein, maskin,
|
|
3932
4049
|
return full_fit, FA_img, MD_img, RGB_img
|
3933
4050
|
|
3934
4051
|
|
4052
|
+
def efficient_dwi_fit_voxelwise(imagein, maskin, bvals, bvecs_5d, model_params=None,
|
4053
|
+
bvals_to_use=None, num_threads=1, verbose=True):
|
4054
|
+
"""
|
4055
|
+
Voxel-wise diffusion model fitting with individual b-vectors per voxel.
|
4056
|
+
|
4057
|
+
Parameters
|
4058
|
+
----------
|
4059
|
+
imagein : ants.ANTsImage
|
4060
|
+
4D DWI image (X, Y, Z, N).
|
4061
|
+
maskin : ants.ANTsImage
|
4062
|
+
3D binary mask.
|
4063
|
+
bvals : (N,) array-like
|
4064
|
+
Common b-values across volumes.
|
4065
|
+
bvecs_5d : (X, Y, Z, N, 3) ndarray
|
4066
|
+
Voxel-specific b-vectors.
|
4067
|
+
model_params : dict
|
4068
|
+
Extra arguments for model.
|
4069
|
+
bvals_to_use : list[int]
|
4070
|
+
Subset of b-values to include.
|
4071
|
+
num_threads : int
|
4072
|
+
Number of threads to use.
|
4073
|
+
verbose : bool
|
4074
|
+
Whether to print status.
|
4075
|
+
|
4076
|
+
Returns
|
4077
|
+
-------
|
4078
|
+
FA_img : ants.ANTsImage
|
4079
|
+
Fractional anisotropy.
|
4080
|
+
MD_img : ants.ANTsImage
|
4081
|
+
Mean diffusivity.
|
4082
|
+
RGB_img : ants.ANTsImage
|
4083
|
+
RGB FA image.
|
4084
|
+
"""
|
4085
|
+
import numpy as np
|
4086
|
+
import ants
|
4087
|
+
import dipy.reconst.dti as dti
|
4088
|
+
from dipy.core.gradients import gradient_table
|
4089
|
+
from dipy.reconst.dti import fractional_anisotropy, color_fa, mean_diffusivity
|
4090
|
+
from concurrent.futures import ThreadPoolExecutor
|
4091
|
+
from tqdm import tqdm
|
4092
|
+
|
4093
|
+
model_params = model_params or {}
|
4094
|
+
img = imagein.numpy()
|
4095
|
+
mask = maskin.numpy().astype(bool)
|
4096
|
+
X, Y, Z, N = img.shape
|
4097
|
+
|
4098
|
+
if bvals_to_use is not None:
|
4099
|
+
sel = np.isin(bvals, bvals_to_use)
|
4100
|
+
img = img[..., sel]
|
4101
|
+
bvals = bvals[sel]
|
4102
|
+
bvecs_5d = bvecs_5d[..., sel, :]
|
4103
|
+
|
4104
|
+
FA = np.zeros((X, Y, Z), dtype=np.float32)
|
4105
|
+
MD = np.zeros((X, Y, Z), dtype=np.float32)
|
4106
|
+
RGB = np.zeros((X, Y, Z, 3), dtype=np.float32)
|
4107
|
+
|
4108
|
+
def fit_voxel(ix, iy, iz):
|
4109
|
+
if not mask[ix, iy, iz]:
|
4110
|
+
return
|
4111
|
+
sig = img[ix, iy, iz, :]
|
4112
|
+
if np.all(sig == 0):
|
4113
|
+
return
|
4114
|
+
bv = bvecs_5d[ix, iy, iz, :, :]
|
4115
|
+
gtab = gradient_table(bvals, bv)
|
4116
|
+
try:
|
4117
|
+
model = dti.TensorModel(gtab, **model_params)
|
4118
|
+
fit = model.fit(sig)
|
4119
|
+
evals = fit.evals
|
4120
|
+
evecs = fit.evecs
|
4121
|
+
FA[ix, iy, iz] = fractional_anisotropy(evals)
|
4122
|
+
MD[ix, iy, iz] = mean_diffusivity(evals)
|
4123
|
+
RGB[ix, iy, iz, :] = color_fa(FA[ix, iy, iz], evecs)
|
4124
|
+
except Exception as e:
|
4125
|
+
if verbose:
|
4126
|
+
print(f"Voxel ({ix},{iy},{iz}) fit failed: {e}")
|
4127
|
+
|
4128
|
+
coords = np.argwhere(mask)
|
4129
|
+
if verbose:
|
4130
|
+
print(f"[INFO] Fitting {len(coords)} voxels using {num_threads} threads...")
|
4131
|
+
|
4132
|
+
if num_threads > 1:
|
4133
|
+
with ThreadPoolExecutor(max_workers=num_threads) as executor:
|
4134
|
+
list(tqdm(executor.map(lambda c: fit_voxel(*c), coords), total=len(coords)))
|
4135
|
+
else:
|
4136
|
+
for c in tqdm(coords):
|
4137
|
+
fit_voxel(*c)
|
4138
|
+
|
4139
|
+
ref = ants.slice_image(imagein, axis=3, idx=0)
|
4140
|
+
return (
|
4141
|
+
ants.copy_image_info(ref, ants.from_numpy(FA)),
|
4142
|
+
ants.copy_image_info(ref, ants.from_numpy(MD)),
|
4143
|
+
ants.merge_channels([ants.copy_image_info(ref, ants.from_numpy(RGB[..., i])) for i in range(3)])
|
4144
|
+
)
|
4145
|
+
|
4146
|
+
|
4147
|
+
def generate_voxelwise_bvecs(global_bvecs, voxel_rotations, transpose=False):
|
4148
|
+
"""
|
4149
|
+
Generate voxel-wise b-vectors from a global bvec and voxel-wise rotation field.
|
4150
|
+
|
4151
|
+
Parameters
|
4152
|
+
----------
|
4153
|
+
global_bvecs : ndarray of shape (N, 3)
|
4154
|
+
Global diffusion gradient directions.
|
4155
|
+
voxel_rotations : ndarray of shape (X, Y, Z, 3, 3)
|
4156
|
+
3x3 rotation matrix for each voxel (can come from Jacobian of deformation field).
|
4157
|
+
transpose : bool, optional
|
4158
|
+
If True, transpose the rotation matrices before applying them to the b-vectors.
|
4159
|
+
|
4160
|
+
|
4161
|
+
Returns
|
4162
|
+
-------
|
4163
|
+
bvecs_5d : ndarray of shape (X, Y, Z, N, 3)
|
4164
|
+
Voxel-specific b-vectors.
|
4165
|
+
"""
|
4166
|
+
X, Y, Z, _, _ = voxel_rotations.shape
|
4167
|
+
N = global_bvecs.shape[0]
|
4168
|
+
bvecs_5d = np.zeros((X, Y, Z, N, 3), dtype=np.float32)
|
4169
|
+
|
4170
|
+
for n in range(N):
|
4171
|
+
bvec = global_bvecs[n]
|
4172
|
+
for i in range(X):
|
4173
|
+
for j in range(Y):
|
4174
|
+
for k in range(Z):
|
4175
|
+
R = voxel_rotations[i, j, k]
|
4176
|
+
if transpose:
|
4177
|
+
R = R.T # Use transpose if needed
|
4178
|
+
bvecs_5d[i, j, k, n, :] = R @ bvec
|
4179
|
+
return bvecs_5d
|
4180
|
+
|
3935
4181
|
def dipy_dti_recon(
|
3936
4182
|
image,
|
3937
4183
|
bvalsfn,
|
@@ -0,0 +1,6 @@
|
|
1
|
+
antspymm/__init__.py,sha256=cTcqtGO0J5T2I0Chxe-Sy25QDlnHLDEQK8QEnJkkFRs,4900
|
2
|
+
antspymm/mm.py,sha256=eqMcRxQt03AbC3qRrubYBxGsbkKNfrS0dd9VgqXavCE,544991
|
3
|
+
antspymm-1.5.7.dist-info/METADATA,sha256=c-2SVYUIR2RGZ_XJXE8M1nxG94CpF9-fYBiRERaCHhQ,26007
|
4
|
+
antspymm-1.5.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
5
|
+
antspymm-1.5.7.dist-info/top_level.txt,sha256=iyD1sRhCKzfwKRJLq5ZUeV9xsv1cGQl8Ejp6QwXM1Zg,9
|
6
|
+
antspymm-1.5.7.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
|