antspymm 1.4.9__py3-none-any.whl → 1.5.1__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 +3 -0
- antspymm/mm.py +381 -129
- {antspymm-1.4.9.dist-info → antspymm-1.5.1.dist-info}/METADATA +2 -2
- antspymm-1.5.1.dist-info/RECORD +7 -0
- antspymm-1.4.9.dist-info/RECORD +0 -7
- {antspymm-1.4.9.dist-info → antspymm-1.5.1.dist-info}/WHEEL +0 -0
- {antspymm-1.4.9.dist-info → antspymm-1.5.1.dist-info}/licenses/LICENSE +0 -0
- {antspymm-1.4.9.dist-info → antspymm-1.5.1.dist-info}/top_level.txt +0 -0
antspymm/__init__.py
CHANGED
@@ -5,7 +5,10 @@ except:
|
|
5
5
|
pass
|
6
6
|
|
7
7
|
from .mm import get_data
|
8
|
+
from .mm import ants_to_nibabel_affine
|
8
9
|
from .mm import get_dti
|
10
|
+
from .mm import efficient_tensor_fit
|
11
|
+
from .mm import efficient_dwi_fit
|
9
12
|
from .mm import triangular_to_tensor
|
10
13
|
from .mm import dti_numpy_to_image
|
11
14
|
from .mm import transform_and_reorient_dti
|
antspymm/mm.py
CHANGED
@@ -93,6 +93,7 @@ __all__ = ['version',
|
|
93
93
|
'loop_timeseries_censoring',
|
94
94
|
'clean_tmp_directory',
|
95
95
|
'validate_nrg_file_format',
|
96
|
+
'ants_to_nibabel_affine',
|
96
97
|
'dict_to_dataframe']
|
97
98
|
|
98
99
|
from pathlib import Path
|
@@ -288,12 +289,44 @@ def get_antsimage_keys(dictionary):
|
|
288
289
|
"""
|
289
290
|
return [key for key, value in dictionary.items() if isinstance(value, ants.core.ants_image.ANTsImage)]
|
290
291
|
|
291
|
-
def to_nibabel(img: "ants.core.ants_image.ANTsImage"):
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
292
|
+
def to_nibabel(img: "ants.core.ants_image.ANTsImage") -> nib.Nifti1Image:
|
293
|
+
"""
|
294
|
+
Convert an ANTsPy image to a Nibabel Nifti1Image in-memory, using correct spatial affine.
|
295
|
+
|
296
|
+
Parameters:
|
297
|
+
img (ants.ANTsImage): An image from ANTsPy.
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
nib.Nifti1Image: The corresponding Nibabel image with spatial orientation in RAS.
|
301
|
+
"""
|
302
|
+
array_data = img.numpy() # get voxel data as NumPy array
|
303
|
+
affine = ants_to_nibabel_affine(img)
|
304
|
+
return nib.Nifti1Image(array_data, affine)
|
305
|
+
|
306
|
+
def ants_to_nibabel_affine(ants_img):
|
307
|
+
"""
|
308
|
+
Convert an ANTsPy image (in LPS space) to a Nibabel-compatible affine (in RAS space).
|
309
|
+
Handles 2D, 3D, 4D input (only spatial dimensions are encoded in the affine).
|
310
|
+
|
311
|
+
Returns:
|
312
|
+
4x4 np.ndarray affine matrix in RAS space.
|
313
|
+
"""
|
314
|
+
spatial_dim = ants_img.dimension
|
315
|
+
spacing = np.array(ants_img.spacing)
|
316
|
+
origin = np.array(ants_img.origin)
|
317
|
+
direction = np.array(ants_img.direction).reshape((spatial_dim, spatial_dim))
|
318
|
+
# Compute rotation-scale matrix
|
319
|
+
affine_linear = direction @ np.diag(spacing)
|
320
|
+
# Build full 4x4 affine with identity in homogeneous bottom row
|
321
|
+
affine = np.eye(4)
|
322
|
+
affine[:spatial_dim, :spatial_dim] = affine_linear
|
323
|
+
affine[:spatial_dim, 3] = origin
|
324
|
+
affine[3, 3]=1
|
325
|
+
# Convert LPS -> RAS by flipping x and y
|
326
|
+
lps_to_ras = np.diag([-1, -1, 1, 1])
|
327
|
+
affine = lps_to_ras @ affine
|
328
|
+
return affine
|
329
|
+
|
297
330
|
|
298
331
|
def dict_to_dataframe(data_dict, convert_lists=True, convert_arrays=True, convert_images=True, verbose=False):
|
299
332
|
"""
|
@@ -2195,7 +2228,7 @@ def transform_and_reorient_dti( fixed, moving_dti, composite_transform, py_based
|
|
2195
2228
|
if verbose:
|
2196
2229
|
print("reorient tensors locally: compose and get reo image")
|
2197
2230
|
locrot = ants.deformation_gradient( ants.image_read(composite_transform),
|
2198
|
-
to_rotation = True, py_based=py_based )
|
2231
|
+
to_rotation = True, py_based=py_based ).numpy()
|
2199
2232
|
rebaser = np.dot( np.transpose( fixed.direction ), moving_dti.direction )
|
2200
2233
|
if verbose:
|
2201
2234
|
print("convert UT to full tensor")
|
@@ -3522,6 +3555,239 @@ def trim_dti_mask( fa, mask, param=4.0 ):
|
|
3522
3555
|
trim_mask = ants.iMath(trim_mask,"MD",paramVox-1)
|
3523
3556
|
return trim_mask
|
3524
3557
|
|
3558
|
+
|
3559
|
+
|
3560
|
+
def efficient_tensor_fit( gtab, fit_method, imagein, maskin, diffusion_model='DTI',
|
3561
|
+
chunk_size=10, num_threads=1, verbose=True):
|
3562
|
+
"""
|
3563
|
+
Efficient and optionally parallelized tensor reconstruction using DiPy.
|
3564
|
+
|
3565
|
+
Parameters
|
3566
|
+
----------
|
3567
|
+
gtab : GradientTable
|
3568
|
+
Dipy gradient table.
|
3569
|
+
fit_method : str
|
3570
|
+
Tensor fitting method (e.g. 'WLS', 'OLS', 'RESTORE').
|
3571
|
+
imagein : ants.ANTsImage
|
3572
|
+
4D diffusion-weighted image.
|
3573
|
+
maskin : ants.ANTsImage
|
3574
|
+
Binary brain mask image.
|
3575
|
+
diffusion_model : string, optional
|
3576
|
+
DTI, FreeWater, DKI.
|
3577
|
+
chunk_size : int, optional
|
3578
|
+
Number of slices (along z-axis) to process at once.
|
3579
|
+
num_threads : int, optional
|
3580
|
+
Number of threads to use (1 = single-threaded).
|
3581
|
+
verbose : bool, optional
|
3582
|
+
Print status updates.
|
3583
|
+
|
3584
|
+
Returns
|
3585
|
+
-------
|
3586
|
+
tenfit : TensorFit or FreeWaterTensorFit
|
3587
|
+
Fitted tensor model.
|
3588
|
+
FA : ants.ANTsImage
|
3589
|
+
Fractional anisotropy image.
|
3590
|
+
MD : ants.ANTsImage
|
3591
|
+
Mean diffusivity image.
|
3592
|
+
RGB : ants.ANTsImage
|
3593
|
+
RGB FA map.
|
3594
|
+
"""
|
3595
|
+
assert imagein.dimension == 4, "Input image must be 4D"
|
3596
|
+
|
3597
|
+
import ants
|
3598
|
+
import numpy as np
|
3599
|
+
import dipy.reconst.dti as dti
|
3600
|
+
import dipy.reconst.fwdti as fwdti
|
3601
|
+
from dipy.reconst.dti import fractional_anisotropy
|
3602
|
+
from dipy.reconst.dti import color_fa
|
3603
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3604
|
+
|
3605
|
+
img_data = imagein.numpy()
|
3606
|
+
mask = maskin.numpy().astype(bool)
|
3607
|
+
X, Y, Z, N = img_data.shape
|
3608
|
+
if verbose:
|
3609
|
+
print(f"Input shape: {img_data.shape}, Processing in chunks of {chunk_size} slices.")
|
3610
|
+
|
3611
|
+
model = fwdti.FreeWaterTensorModel(gtab) if diffusion_model == 'FreeWater' else dti.TensorModel(gtab, fit_method=fit_method)
|
3612
|
+
|
3613
|
+
def process_chunk(z_start):
|
3614
|
+
z_end = min(Z, z_start + chunk_size)
|
3615
|
+
local_data = img_data[:, :, z_start:z_end, :]
|
3616
|
+
local_mask = mask[:, :, z_start:z_end]
|
3617
|
+
masked_data = local_data * local_mask[..., None]
|
3618
|
+
masked_data = np.nan_to_num(masked_data, nan=0)
|
3619
|
+
fit = model.fit(masked_data)
|
3620
|
+
FA_chunk = fractional_anisotropy(fit.evals)
|
3621
|
+
FA_chunk[np.isnan(FA_chunk)] = 1
|
3622
|
+
FA_chunk = np.clip(FA_chunk, 0, 1)
|
3623
|
+
MD_chunk = dti.mean_diffusivity(fit.evals)
|
3624
|
+
RGB_chunk = color_fa(FA_chunk, fit.evecs)
|
3625
|
+
return z_start, z_end, FA_chunk, MD_chunk, RGB_chunk
|
3626
|
+
|
3627
|
+
FA_vol = np.zeros((X, Y, Z), dtype=np.float32)
|
3628
|
+
MD_vol = np.zeros((X, Y, Z), dtype=np.float32)
|
3629
|
+
RGB_vol = np.zeros((X, Y, Z, 3), dtype=np.float32)
|
3630
|
+
|
3631
|
+
chunks = range(0, Z, chunk_size)
|
3632
|
+
if num_threads > 1:
|
3633
|
+
with ThreadPoolExecutor(max_workers=num_threads) as executor:
|
3634
|
+
futures = {executor.submit(process_chunk, z): z for z in chunks}
|
3635
|
+
for f in as_completed(futures):
|
3636
|
+
z_start, z_end, FA_chunk, MD_chunk, RGB_chunk = f.result()
|
3637
|
+
FA_vol[:, :, z_start:z_end] = FA_chunk
|
3638
|
+
MD_vol[:, :, z_start:z_end] = MD_chunk
|
3639
|
+
RGB_vol[:, :, z_start:z_end, :] = RGB_chunk
|
3640
|
+
else:
|
3641
|
+
for z in chunks:
|
3642
|
+
z_start, z_end, FA_chunk, MD_chunk, RGB_chunk = process_chunk(z)
|
3643
|
+
FA_vol[:, :, z_start:z_end] = FA_chunk
|
3644
|
+
MD_vol[:, :, z_start:z_end] = MD_chunk
|
3645
|
+
RGB_vol[:, :, z_start:z_end, :] = RGB_chunk
|
3646
|
+
|
3647
|
+
b0 = ants.slice_image(imagein, axis=3, idx=0)
|
3648
|
+
FA = ants.copy_image_info(b0, ants.from_numpy(FA_vol))
|
3649
|
+
MD = ants.copy_image_info(b0, ants.from_numpy(MD_vol))
|
3650
|
+
RGB_channels = [ants.copy_image_info(b0, ants.from_numpy(RGB_vol[..., i])) for i in range(3)]
|
3651
|
+
RGB = ants.merge_channels(RGB_channels)
|
3652
|
+
|
3653
|
+
return model.fit(img_data * mask[..., None]), FA, MD, RGB
|
3654
|
+
|
3655
|
+
|
3656
|
+
|
3657
|
+
def efficient_dwi_fit(gtab, diffusion_model, imagein, maskin,
|
3658
|
+
model_params=None, bvals_to_use=None,
|
3659
|
+
chunk_size=10, num_threads=1, verbose=True):
|
3660
|
+
"""
|
3661
|
+
Efficient and optionally parallelized diffusion model reconstruction using DiPy.
|
3662
|
+
|
3663
|
+
Parameters
|
3664
|
+
----------
|
3665
|
+
gtab : GradientTable
|
3666
|
+
DiPy gradient table.
|
3667
|
+
diffusion_model : str
|
3668
|
+
One of ['DTI', 'FreeWater', 'DKI'].
|
3669
|
+
imagein : ants.ANTsImage
|
3670
|
+
4D diffusion-weighted image.
|
3671
|
+
maskin : ants.ANTsImage
|
3672
|
+
Binary brain mask image.
|
3673
|
+
model_params : dict, optional
|
3674
|
+
Additional parameters passed to model constructors.
|
3675
|
+
bvals_to_use : list of int, optional
|
3676
|
+
Subset of b-values to use for the fit (e.g., [0, 1000, 2000]).
|
3677
|
+
chunk_size : int, optional
|
3678
|
+
Z-axis slice chunk size.
|
3679
|
+
num_threads : int, optional
|
3680
|
+
Number of parallel threads.
|
3681
|
+
verbose : bool, optional
|
3682
|
+
Whether to print status messages.
|
3683
|
+
|
3684
|
+
Returns
|
3685
|
+
-------
|
3686
|
+
fit : dipy ModelFit
|
3687
|
+
The fitted model object.
|
3688
|
+
FA : ants.ANTsImage or None
|
3689
|
+
Fractional anisotropy image (if applicable).
|
3690
|
+
MD : ants.ANTsImage or None
|
3691
|
+
Mean diffusivity image (if applicable).
|
3692
|
+
RGB : ants.ANTsImage or None
|
3693
|
+
Color FA image (if applicable).
|
3694
|
+
"""
|
3695
|
+
import ants
|
3696
|
+
import numpy as np
|
3697
|
+
import dipy.reconst.dti as dti
|
3698
|
+
import dipy.reconst.fwdti as fwdti
|
3699
|
+
import dipy.reconst.dki as dki
|
3700
|
+
from dipy.core.gradients import gradient_table
|
3701
|
+
from dipy.reconst.dti import fractional_anisotropy, color_fa, mean_diffusivity
|
3702
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3703
|
+
|
3704
|
+
assert imagein.dimension == 4, "Input image must be 4D"
|
3705
|
+
model_params = model_params or {}
|
3706
|
+
|
3707
|
+
img_data = imagein.numpy()
|
3708
|
+
mask = maskin.numpy().astype(bool)
|
3709
|
+
X, Y, Z, N = img_data.shape
|
3710
|
+
|
3711
|
+
if verbose:
|
3712
|
+
print(f"[INFO] Image shape: {img_data.shape}")
|
3713
|
+
print(f"[INFO] Using model: {diffusion_model}")
|
3714
|
+
print(f"[INFO] Chunk size: {chunk_size} | Threads: {num_threads}")
|
3715
|
+
|
3716
|
+
# Filter shells if specified
|
3717
|
+
if bvals_to_use is not None:
|
3718
|
+
bvals_to_use = set(bvals_to_use)
|
3719
|
+
sel = np.isin(gtab.bvals, list(bvals_to_use))
|
3720
|
+
img_data = img_data[..., sel]
|
3721
|
+
gtab = gradient_table(gtab.bvals[sel], gtab.bvecs[sel])
|
3722
|
+
if verbose:
|
3723
|
+
print(f"[INFO] Selected b-values: {sorted(bvals_to_use)}")
|
3724
|
+
print(f"[INFO] Selected volumes: {sel.sum()} / {N}")
|
3725
|
+
|
3726
|
+
# Choose model
|
3727
|
+
def get_model(name, gtab, **params):
|
3728
|
+
if name == 'DTI':
|
3729
|
+
return dti.TensorModel(gtab, **params)
|
3730
|
+
elif name == 'FreeWater':
|
3731
|
+
return fwdti.FreeWaterTensorModel(gtab)
|
3732
|
+
elif name == 'DKI':
|
3733
|
+
return dki.DiffusionKurtosisModel(gtab, **params)
|
3734
|
+
else:
|
3735
|
+
raise ValueError(f"Unsupported model: {name}")
|
3736
|
+
|
3737
|
+
model = get_model(diffusion_model, gtab, **model_params)
|
3738
|
+
|
3739
|
+
# Output volumes initialized to zero
|
3740
|
+
FA_vol = np.zeros((X, Y, Z), dtype=np.float32)
|
3741
|
+
MD_vol = np.zeros((X, Y, Z), dtype=np.float32)
|
3742
|
+
RGB_vol = np.zeros((X, Y, Z, 3), dtype=np.float32)
|
3743
|
+
has_tensor_metrics = diffusion_model in ['DTI', 'FreeWater']
|
3744
|
+
|
3745
|
+
def process_chunk(z_start):
|
3746
|
+
z_end = min(Z, z_start + chunk_size)
|
3747
|
+
local_data = img_data[:, :, z_start:z_end, :]
|
3748
|
+
local_mask = mask[:, :, z_start:z_end]
|
3749
|
+
masked_data = local_data * local_mask[..., None]
|
3750
|
+
masked_data = np.nan_to_num(masked_data, nan=0)
|
3751
|
+
fit = model.fit(masked_data)
|
3752
|
+
if has_tensor_metrics and hasattr(fit, 'evals') and hasattr(fit, 'evecs'):
|
3753
|
+
FA = fractional_anisotropy(fit.evals)
|
3754
|
+
FA[np.isnan(FA)] = 1
|
3755
|
+
FA = np.clip(FA, 0, 1)
|
3756
|
+
MD = mean_diffusivity(fit.evals)
|
3757
|
+
RGB = color_fa(FA, fit.evecs)
|
3758
|
+
return z_start, z_end, FA, MD, RGB
|
3759
|
+
return z_start, z_end, None, None, None
|
3760
|
+
|
3761
|
+
# Run processing
|
3762
|
+
chunks = range(0, Z, chunk_size)
|
3763
|
+
if num_threads > 1:
|
3764
|
+
with ThreadPoolExecutor(max_workers=num_threads) as executor:
|
3765
|
+
futures = {executor.submit(process_chunk, z): z for z in chunks}
|
3766
|
+
for f in as_completed(futures):
|
3767
|
+
z_start, z_end, FA, MD, RGB = f.result()
|
3768
|
+
if FA is not None:
|
3769
|
+
FA_vol[:, :, z_start:z_end] = FA
|
3770
|
+
MD_vol[:, :, z_start:z_end] = MD
|
3771
|
+
RGB_vol[:, :, z_start:z_end, :] = RGB
|
3772
|
+
else:
|
3773
|
+
for z in chunks:
|
3774
|
+
z_start, z_end, FA, MD, RGB = process_chunk(z)
|
3775
|
+
if FA is not None:
|
3776
|
+
FA_vol[:, :, z_start:z_end] = FA
|
3777
|
+
MD_vol[:, :, z_start:z_end] = MD
|
3778
|
+
RGB_vol[:, :, z_start:z_end, :] = RGB
|
3779
|
+
|
3780
|
+
b0 = ants.slice_image(imagein, axis=3, idx=0)
|
3781
|
+
FA_img = ants.copy_image_info(b0, ants.from_numpy(FA_vol)) if has_tensor_metrics else None
|
3782
|
+
MD_img = ants.copy_image_info(b0, ants.from_numpy(MD_vol)) if has_tensor_metrics else None
|
3783
|
+
RGB_img = (ants.merge_channels([
|
3784
|
+
ants.copy_image_info(b0, ants.from_numpy(RGB_vol[..., i])) for i in range(3)
|
3785
|
+
]) if has_tensor_metrics else None)
|
3786
|
+
|
3787
|
+
full_fit = model.fit(img_data * mask[..., None])
|
3788
|
+
return full_fit, FA_img, MD_img, RGB_img
|
3789
|
+
|
3790
|
+
|
3525
3791
|
def dipy_dti_recon(
|
3526
3792
|
image,
|
3527
3793
|
bvalsfn,
|
@@ -3532,7 +3798,7 @@ def dipy_dti_recon(
|
|
3532
3798
|
mask_closing = 5,
|
3533
3799
|
fit_method='WLS',
|
3534
3800
|
trim_the_mask=2.0,
|
3535
|
-
|
3801
|
+
diffusion_model='DTI',
|
3536
3802
|
verbose=False ):
|
3537
3803
|
"""
|
3538
3804
|
DiPy DTI reconstruction - building on the DiPy basic DTI example
|
@@ -3559,7 +3825,8 @@ def dipy_dti_recon(
|
|
3559
3825
|
|
3560
3826
|
trim_the_mask : float >=0 post-hoc method for trimming the mask
|
3561
3827
|
|
3562
|
-
|
3828
|
+
diffusion_model : string
|
3829
|
+
DTI, FreeWater, DKI
|
3563
3830
|
|
3564
3831
|
verbose : boolean
|
3565
3832
|
|
@@ -3612,47 +3879,22 @@ def dipy_dti_recon(
|
|
3612
3879
|
if verbose:
|
3613
3880
|
print("recon dti.TensorModel",flush=True)
|
3614
3881
|
|
3615
|
-
def justthefit( gtab, fit_method, imagein, maskin, free_water=False ):
|
3616
|
-
if fit_method is None:
|
3617
|
-
return None, None, None, None
|
3618
|
-
maskedimage=[]
|
3619
|
-
for myidx in range(imagein.shape[3]):
|
3620
|
-
b0 = ants.slice_image( imagein, axis=3, idx=myidx)
|
3621
|
-
maskedimage.append( b0 * maskin )
|
3622
|
-
maskedimage = ants.list_to_ndimage( imagein, maskedimage )
|
3623
|
-
maskdata = maskedimage.numpy()
|
3624
|
-
if free_water:
|
3625
|
-
tenmodel = fwdti.FreeWaterTensorModel(gtab)
|
3626
|
-
else:
|
3627
|
-
tenmodel = dti.TensorModel(gtab,fit_method=fit_method)
|
3628
|
-
tenfit = tenmodel.fit(maskdata)
|
3629
|
-
FA = fractional_anisotropy(tenfit.evals)
|
3630
|
-
FA[np.isnan(FA)] = 1
|
3631
|
-
FA = np.clip(FA, 0, 1)
|
3632
|
-
MD1 = dti.mean_diffusivity(tenfit.evals)
|
3633
|
-
MD1 = ants.copy_image_info( b0, ants.from_numpy( MD1.astype(np.float32) ) )
|
3634
|
-
FA = ants.copy_image_info( b0, ants.from_numpy( FA.astype(np.float32) ) )
|
3635
|
-
FA, MD1 = impute_fa( FA, MD1 )
|
3636
|
-
RGB = color_fa(FA.numpy(), tenfit.evecs)
|
3637
|
-
RGB = ants.from_numpy( RGB.astype(np.float32) )
|
3638
|
-
RGB0 = ants.copy_image_info( b0, ants.slice_image( RGB, axis=3, idx=0 ) )
|
3639
|
-
RGB1 = ants.copy_image_info( b0, ants.slice_image( RGB, axis=3, idx=1 ) )
|
3640
|
-
RGB2 = ants.copy_image_info( b0, ants.slice_image( RGB, axis=3, idx=2 ) )
|
3641
|
-
RGB = ants.merge_channels( [RGB0,RGB1,RGB2] )
|
3642
|
-
return tenfit, FA, MD1, RGB
|
3643
|
-
|
3644
3882
|
bvecs = repair_bvecs( bvecs )
|
3645
|
-
gtab = gradient_table(bvals, bvecs, atol=2.0 )
|
3646
|
-
|
3647
|
-
|
3648
|
-
|
3883
|
+
gtab = gradient_table(bvals, bvecs=bvecs, atol=2.0 )
|
3884
|
+
mynt=1
|
3885
|
+
threads_env = os.environ.get("ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS")
|
3886
|
+
if threads_env is not None:
|
3887
|
+
mynt = int(threads_env)
|
3888
|
+
tenfit, FA, MD1, RGB = efficient_dwi_fit( gtab, diffusion_model, image, maskdil,
|
3889
|
+
num_threads=mynt )
|
3649
3890
|
if verbose:
|
3650
3891
|
print("recon dti.TensorModel done",flush=True)
|
3651
3892
|
|
3652
3893
|
# change the brain mask based on high FA values
|
3653
3894
|
if trim_the_mask > 0 and fit_method is not None:
|
3654
3895
|
mask = trim_dti_mask( FA, mask, trim_the_mask )
|
3655
|
-
tenfit, FA, MD1, RGB =
|
3896
|
+
tenfit, FA, MD1, RGB = efficient_dwi_fit( gtab, diffusion_model, image, maskdil,
|
3897
|
+
num_threads=mynt )
|
3656
3898
|
|
3657
3899
|
return {
|
3658
3900
|
'tensormodel' : tenfit,
|
@@ -3736,7 +3978,7 @@ def joint_dti_recon(
|
|
3736
3978
|
fit_method='WLS',
|
3737
3979
|
impute = False,
|
3738
3980
|
censor = True,
|
3739
|
-
|
3981
|
+
diffusion_model = 'DTI',
|
3740
3982
|
verbose = False ):
|
3741
3983
|
"""
|
3742
3984
|
1. pass in subject data and 1mm JHU atlas/labels
|
@@ -3791,7 +4033,8 @@ def joint_dti_recon(
|
|
3791
4033
|
|
3792
4034
|
censor : boolean
|
3793
4035
|
|
3794
|
-
|
4036
|
+
diffusion_model : string
|
4037
|
+
DTI, FreeWater, DKI
|
3795
4038
|
|
3796
4039
|
verbose : boolean
|
3797
4040
|
|
@@ -3911,7 +4154,7 @@ def joint_dti_recon(
|
|
3911
4154
|
img_LRdwp, bval_LR, bvec_LR,
|
3912
4155
|
mask = brain_mask,
|
3913
4156
|
fit_method=fit_method,
|
3914
|
-
mask_dilation=0,
|
4157
|
+
mask_dilation=0, diffusion_model=diffusion_model, verbose=True )
|
3915
4158
|
if verbose:
|
3916
4159
|
print("recon done", flush=True)
|
3917
4160
|
|
@@ -4204,16 +4447,15 @@ def dwi_deterministic_tracking(
|
|
4204
4447
|
if verbose:
|
4205
4448
|
print("begin tracking",flush=True)
|
4206
4449
|
|
4207
|
-
|
4208
|
-
affine = dwi_img.affine
|
4450
|
+
affine = ants_to_nibabel_affine(dwi)
|
4209
4451
|
|
4210
4452
|
if isinstance( bvals, str ) or isinstance( bvecs, str ):
|
4211
4453
|
bvals, bvecs = read_bvals_bvecs(bvals, bvecs)
|
4212
4454
|
bvecs = repair_bvecs( bvecs )
|
4213
|
-
gtab = gradient_table(bvals, bvecs, atol=2.0 )
|
4455
|
+
gtab = gradient_table(bvals, bvecs=bvecs, atol=2.0 )
|
4214
4456
|
if mask is None:
|
4215
4457
|
mask = ants.threshold_image( fa, fa_thresh, 2.0 ).iMath("GetLargestComponent")
|
4216
|
-
dwi_data = dwi_img.get_fdata()
|
4458
|
+
dwi_data = dwi.numpy() # dwi_img.get_fdata()
|
4217
4459
|
dwi_mask = mask.numpy() == 1
|
4218
4460
|
dti_model = dti.TensorModel(gtab,fit_method=fit_method)
|
4219
4461
|
if verbose:
|
@@ -4223,7 +4465,7 @@ def dwi_deterministic_tracking(
|
|
4223
4465
|
from dipy.tracking.stopping_criterion import ThresholdStoppingCriterion
|
4224
4466
|
stopping_criterion = ThresholdStoppingCriterion(fa.numpy(), fa_thresh)
|
4225
4467
|
from dipy.data import get_sphere
|
4226
|
-
sphere = get_sphere('symmetric362')
|
4468
|
+
sphere = get_sphere(name='symmetric362')
|
4227
4469
|
from dipy.direction import peaks_from_model
|
4228
4470
|
if peak_indices is None:
|
4229
4471
|
# problems with multi-threading ...
|
@@ -4282,7 +4524,7 @@ def dwi_deterministic_tracking(
|
|
4282
4524
|
streamlines = Streamlines(streamlines_generator)
|
4283
4525
|
from dipy.io.stateful_tractogram import Space, StatefulTractogram
|
4284
4526
|
from dipy.io.streamline import save_tractogram
|
4285
|
-
sft = StatefulTractogram(streamlines, dwi_img, Space.RASMM)
|
4527
|
+
sft = None # StatefulTractogram(streamlines, dwi_img, Space.RASMM)
|
4286
4528
|
if verbose:
|
4287
4529
|
print("streamlines done", flush=True)
|
4288
4530
|
return {
|
@@ -4384,15 +4626,14 @@ def dwi_closest_peak_tracking(
|
|
4384
4626
|
if verbose:
|
4385
4627
|
print("begin tracking",flush=True)
|
4386
4628
|
|
4387
|
-
|
4388
|
-
affine = dwi_img.affine
|
4629
|
+
affine = ants_to_nibabel_affine(dwi)
|
4389
4630
|
if isinstance( bvals, str ) or isinstance( bvecs, str ):
|
4390
4631
|
bvals, bvecs = read_bvals_bvecs(bvals, bvecs)
|
4391
4632
|
bvecs = repair_bvecs( bvecs )
|
4392
|
-
gtab = gradient_table(bvals, bvecs, atol=2.0 )
|
4633
|
+
gtab = gradient_table(bvals, bvecs=bvecs, atol=2.0 )
|
4393
4634
|
if mask is None:
|
4394
4635
|
mask = ants.threshold_image( fa, fa_thresh, 2.0 ).iMath("GetLargestComponent")
|
4395
|
-
dwi_data =
|
4636
|
+
dwi_data = dwi.numpy()
|
4396
4637
|
dwi_mask = mask.numpy() == 1
|
4397
4638
|
|
4398
4639
|
|
@@ -4429,7 +4670,7 @@ def dwi_closest_peak_tracking(
|
|
4429
4670
|
streamlines = Streamlines(streamlines_generator)
|
4430
4671
|
from dipy.io.stateful_tractogram import Space, StatefulTractogram
|
4431
4672
|
from dipy.io.streamline import save_tractogram
|
4432
|
-
sft = StatefulTractogram(streamlines, dwi_img, Space.RASMM)
|
4673
|
+
sft = None # StatefulTractogram(streamlines, dwi_img, Space.RASMM)
|
4433
4674
|
if verbose:
|
4434
4675
|
print("streamlines done", flush=True)
|
4435
4676
|
return {
|
@@ -4465,8 +4706,7 @@ def dwi_streamline_pairwise_connectivity( streamlines, label_image, labels_to_co
|
|
4465
4706
|
from dipy.tracking.streamline import Streamlines
|
4466
4707
|
keep_streamlines = Streamlines()
|
4467
4708
|
|
4468
|
-
|
4469
|
-
affine = to_nibabel(label_image).affine
|
4709
|
+
affine = ants_to_nibabel_affine(label_image) # to_nibabel(label_image).affine
|
4470
4710
|
|
4471
4711
|
lin_T, offset = utils._mapping_to_voxel(affine)
|
4472
4712
|
label_image_np = label_image.numpy()
|
@@ -4526,7 +4766,7 @@ def dwi_streamline_pairwise_connectivity_old(
|
|
4526
4766
|
volUnit = np.prod( ants.get_spacing( label_image ) )
|
4527
4767
|
labels = label_image.numpy()
|
4528
4768
|
|
4529
|
-
affine = to_nibabel(label_image).affine
|
4769
|
+
affine = ants_to_nibabel_affine(label_image) # to_nibabel(label_image).affine
|
4530
4770
|
|
4531
4771
|
import numpy as np
|
4532
4772
|
from dipy.io.image import load_nifti_data, load_nifti, save_nifti
|
@@ -4618,7 +4858,7 @@ def dwi_streamline_connectivity(
|
|
4618
4858
|
volUnit = np.prod( ants.get_spacing( label_image ) )
|
4619
4859
|
labels = label_image.numpy()
|
4620
4860
|
|
4621
|
-
affine = to_nibabel(label_image).affine
|
4861
|
+
affine = ants_to_nibabel_affine(label_image) # to_nibabel(label_image).affine
|
4622
4862
|
|
4623
4863
|
import numpy as np
|
4624
4864
|
from dipy.io.image import load_nifti_data, load_nifti, save_nifti
|
@@ -4708,7 +4948,7 @@ def dwi_streamline_connectivity_old(
|
|
4708
4948
|
volUnit = np.prod( ants.get_spacing( label_image ) )
|
4709
4949
|
labels = label_image.numpy()
|
4710
4950
|
|
4711
|
-
affine = to_nibabel(label_image).affine
|
4951
|
+
affine = ants_to_nibabel_affine(label_image) # to_nibabel(label_image).affine
|
4712
4952
|
|
4713
4953
|
if verbose:
|
4714
4954
|
print("path length begin ... volUnit = " + str( volUnit ) )
|
@@ -7480,7 +7720,8 @@ def write_mm( output_prefix, mm, mm_norm=None, t1wide=None, separator='_', verbo
|
|
7480
7720
|
if 'tractography' in mm:
|
7481
7721
|
if mm['tractography'] is not None:
|
7482
7722
|
ofn = output_prefix + separator + 'tractogram.trk'
|
7483
|
-
|
7723
|
+
if mm['tractography']['tractogram'] is not None:
|
7724
|
+
save_tractogram( mm['tractography']['tractogram'], ofn )
|
7484
7725
|
cnxderk = None
|
7485
7726
|
if 'tractography_connectivity' in mm:
|
7486
7727
|
if mm['tractography_connectivity'] is not None:
|
@@ -10666,94 +10907,104 @@ def calculate_loop_scores_full(flattened_series, n_neighbors=20, verbose=True ):
|
|
10666
10907
|
return m.local_outlier_probabilities[:]
|
10667
10908
|
|
10668
10909
|
|
10669
|
-
def calculate_loop_scores(
|
10670
|
-
|
10910
|
+
def calculate_loop_scores(
|
10911
|
+
flattened_series,
|
10912
|
+
n_neighbors=20,
|
10913
|
+
n_features_sample=0.02,
|
10914
|
+
n_feature_repeats=5,
|
10915
|
+
seed=42,
|
10916
|
+
use_approx_knn=True,
|
10917
|
+
verbose=True,
|
10918
|
+
):
|
10671
10919
|
"""
|
10672
|
-
|
10920
|
+
Memory-efficient and robust LoOP score estimation with optional approximate KNN
|
10921
|
+
and averaging over multiple random feature subsets.
|
10673
10922
|
|
10674
10923
|
Parameters:
|
10675
|
-
flattened_series (np.ndarray): 2D array
|
10676
|
-
n_neighbors (int): Number of neighbors for
|
10677
|
-
n_features_sample (int): Number of features to sample
|
10678
|
-
|
10679
|
-
|
10924
|
+
flattened_series (np.ndarray): 2D array (n_samples x n_features)
|
10925
|
+
n_neighbors (int): Number of neighbors for LoOP
|
10926
|
+
n_features_sample (int or float): Number or fraction of features to sample
|
10927
|
+
n_feature_repeats (int): How many independent feature subsets to sample and average over
|
10928
|
+
seed (int): Random seed
|
10929
|
+
use_approx_knn (bool): Whether to use fast approximate KNN (via pynndescent)
|
10930
|
+
verbose (bool): Verbose output
|
10680
10931
|
|
10681
10932
|
Returns:
|
10682
|
-
np.ndarray:
|
10933
|
+
np.ndarray: Averaged local outlier probabilities (length n_samples)
|
10683
10934
|
"""
|
10684
10935
|
import numpy as np
|
10685
10936
|
from sklearn.preprocessing import StandardScaler
|
10686
|
-
from sklearn.neighbors import NearestNeighbors
|
10687
10937
|
from PyNomaly import loop
|
10688
10938
|
|
10689
|
-
#
|
10690
|
-
|
10691
|
-
|
10939
|
+
# Optional approximate nearest neighbors
|
10940
|
+
try:
|
10941
|
+
from pynndescent import NNDescent
|
10942
|
+
has_nn_descent = True
|
10943
|
+
except ImportError:
|
10944
|
+
has_nn_descent = False
|
10945
|
+
|
10946
|
+
rng = np.random.default_rng(seed)
|
10692
10947
|
X = np.nan_to_num(flattened_series, nan=0).astype(np.float32)
|
10693
10948
|
n_samples, n_features = X.shape
|
10694
10949
|
|
10695
|
-
|
10696
|
-
|
10697
|
-
|
10698
|
-
|
10699
|
-
|
10700
|
-
|
10701
|
-
n_features_sample = n_features
|
10702
|
-
if verbose:
|
10703
|
-
print(f"- Requested n_features_sample exceeds available features. Using all {n_features} features.")
|
10950
|
+
# Handle feature sampling
|
10951
|
+
if isinstance(n_features_sample, float):
|
10952
|
+
if 0 < n_features_sample <= 1.0:
|
10953
|
+
n_features_sample = max(1, int(n_features_sample * n_features))
|
10954
|
+
else:
|
10955
|
+
raise ValueError("If float, n_features_sample must be in (0, 1].")
|
10704
10956
|
|
10705
|
-
|
10706
|
-
# Step 2: Feature sampling
|
10707
|
-
# -------------------------------
|
10708
|
-
rng = np.random.default_rng(seed)
|
10709
|
-
sampled_indices = rng.choice(n_features, n_features_sample, replace=False)
|
10710
|
-
X_sampled = X[:, sampled_indices]
|
10957
|
+
n_features_sample = min(n_features, n_features_sample)
|
10711
10958
|
|
10712
|
-
if verbose:
|
10713
|
-
print(f"- Sampled feature shape: {X_sampled.shape} (samples x sampled_features)")
|
10714
|
-
print(f"- Random seed for reproducibility: {seed}")
|
10715
|
-
|
10716
|
-
# -------------------------------
|
10717
|
-
# Step 3: Standardization
|
10718
|
-
# -------------------------------
|
10719
|
-
scaler = StandardScaler(copy=False)
|
10720
|
-
X_sampled = scaler.fit_transform(X_sampled)
|
10721
|
-
X_sampled = np.nan_to_num(X_sampled, nan=0)
|
10722
|
-
|
10723
|
-
# -------------------------------
|
10724
|
-
# Step 4: KNN setup for LoOP
|
10725
|
-
# -------------------------------
|
10726
10959
|
if n_neighbors >= n_samples:
|
10727
10960
|
n_neighbors = max(1, n_samples // 2)
|
10728
|
-
if verbose:
|
10729
|
-
print(f"- Adjusted n_neighbors to {n_neighbors} (was too large for available samples).")
|
10730
10961
|
|
10731
10962
|
if verbose:
|
10732
|
-
print(f"
|
10733
|
-
print(f"
|
10963
|
+
print(f"[LoOP] Input shape: {X.shape}")
|
10964
|
+
print(f"[LoOP] Sampling {n_features_sample} features per repeat, {n_feature_repeats} repeats")
|
10965
|
+
print(f"[LoOP] Using {n_neighbors} neighbors")
|
10734
10966
|
|
10735
|
-
|
10736
|
-
neigh.fit(X_sampled)
|
10737
|
-
dists, indices = neigh.kneighbors(X_sampled, return_distance=True)
|
10967
|
+
loop_scores = []
|
10738
10968
|
|
10739
|
-
|
10740
|
-
|
10741
|
-
|
10742
|
-
if verbose:
|
10743
|
-
print(f"- Distance matrix shape: {dists.shape} (samples x n_neighbors)")
|
10744
|
-
print(f"- Neighbor index matrix shape: {indices.shape}")
|
10745
|
-
print("- Estimating Local Outlier Probabilities (LoOP)...")
|
10969
|
+
for rep in range(n_feature_repeats):
|
10970
|
+
feature_idx = rng.choice(n_features, n_features_sample, replace=False)
|
10971
|
+
X_sub = X[:, feature_idx]
|
10746
10972
|
|
10747
|
-
|
10748
|
-
|
10749
|
-
|
10750
|
-
|
10751
|
-
|
10973
|
+
scaler = StandardScaler(copy=False)
|
10974
|
+
X_sub = scaler.fit_transform(X_sub)
|
10975
|
+
X_sub = np.nan_to_num(X_sub, nan=0)
|
10976
|
+
|
10977
|
+
# Approximate or exact KNN
|
10978
|
+
if use_approx_knn and has_nn_descent and n_samples > 1000:
|
10979
|
+
if verbose:
|
10980
|
+
print(f" [Rep {rep+1}] Using NNDescent (approximate KNN)")
|
10981
|
+
ann = NNDescent(X_sub, n_neighbors=n_neighbors, random_state=seed + rep)
|
10982
|
+
indices, dists = ann.neighbor_graph
|
10983
|
+
else:
|
10984
|
+
from sklearn.neighbors import NearestNeighbors
|
10985
|
+
if verbose:
|
10986
|
+
print(f" [Rep {rep+1}] Using NearestNeighbors (exact KNN)")
|
10987
|
+
nn = NearestNeighbors(n_neighbors=n_neighbors)
|
10988
|
+
nn.fit(X_sub)
|
10989
|
+
dists, indices = nn.kneighbors(X_sub)
|
10990
|
+
|
10991
|
+
# LoOP score for this repeat
|
10992
|
+
model = loop.LocalOutlierProbability(
|
10993
|
+
distance_matrix=dists,
|
10994
|
+
neighbor_matrix=indices,
|
10995
|
+
n_neighbors=n_neighbors
|
10996
|
+
).fit()
|
10997
|
+
loop_scores.append(model.local_outlier_probabilities[:])
|
10998
|
+
|
10999
|
+
# Average over repeats
|
11000
|
+
loop_scores = np.stack(loop_scores)
|
11001
|
+
loop_scores_mean = loop_scores.mean(axis=0)
|
10752
11002
|
|
10753
11003
|
if verbose:
|
10754
|
-
print("
|
11004
|
+
print(f"[LoOP] Averaged over {n_feature_repeats} feature subsets. Final shape: {loop_scores_mean.shape}")
|
11005
|
+
|
11006
|
+
return loop_scores_mean
|
10755
11007
|
|
10756
|
-
return model.local_outlier_probabilities[:]
|
10757
11008
|
|
10758
11009
|
|
10759
11010
|
def score_fmri_censoring(cbfts, csf_seg, gm_seg, wm_seg ):
|
@@ -10823,7 +11074,7 @@ def score_fmri_censoring(cbfts, csf_seg, gm_seg, wm_seg ):
|
|
10823
11074
|
cbfts_recon_ants = ants.copy_image_info(cbfts, cbfts_recon_ants)
|
10824
11075
|
return cbfts_recon_ants, indx
|
10825
11076
|
|
10826
|
-
def loop_timeseries_censoring(x, threshold=0.5, mask=None, n_features_sample=
|
11077
|
+
def loop_timeseries_censoring(x, threshold=0.5, mask=None, n_features_sample=0.02, seed=42, verbose=True):
|
10827
11078
|
"""
|
10828
11079
|
Censor high leverage volumes from a time series using Local Outlier Probabilities (LoOP).
|
10829
11080
|
|
@@ -10831,7 +11082,8 @@ def loop_timeseries_censoring(x, threshold=0.5, mask=None, n_features_sample=100
|
|
10831
11082
|
x (ANTsImage): A 4D time series image.
|
10832
11083
|
threshold (float): Threshold for determining high leverage volumes based on LoOP scores.
|
10833
11084
|
mask (antsImage): restricts to a ROI
|
10834
|
-
n_features_sample (int): feature sample size default
|
11085
|
+
n_features_sample (int/float): feature sample size default 0.01; if less than one then this is interpreted as a percentage of the total features otherwise it sets the number of features to be used
|
11086
|
+
seed (int): random seed
|
10835
11087
|
verbose (bool)
|
10836
11088
|
|
10837
11089
|
Returns:
|
@@ -10847,7 +11099,7 @@ def loop_timeseries_censoring(x, threshold=0.5, mask=None, n_features_sample=100
|
|
10847
11099
|
flattened_series = ants.timeseries_to_matrix( x, mask )
|
10848
11100
|
if verbose:
|
10849
11101
|
print("loop_timeseries_censoring: flattened")
|
10850
|
-
loop_scores = calculate_loop_scores(flattened_series, n_features_sample=n_features_sample, verbose=verbose )
|
11102
|
+
loop_scores = calculate_loop_scores(flattened_series, n_features_sample=n_features_sample, seed=seed, verbose=verbose )
|
10851
11103
|
high_leverage_volumes = np.where(loop_scores > threshold)[0]
|
10852
11104
|
if verbose:
|
10853
11105
|
print("loop_timeseries_censoring: High Leverage Volumes:", high_leverage_volumes)
|
@@ -1,13 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: antspymm
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.5.1
|
4
4
|
Summary: multi-channel/time-series medical image processing with antspyx
|
5
5
|
Author-email: "Avants, Gosselin, Tustison, Reardon" <stnava@gmail.com>
|
6
6
|
License: Apache-2.0
|
7
7
|
Classifier: License :: OSI Approved :: Apache Software License
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Operating System :: OS Independent
|
10
|
-
Requires-Python: >=3.
|
10
|
+
Requires-Python: >=3.9
|
11
11
|
Description-Content-Type: text/markdown
|
12
12
|
License-File: LICENSE
|
13
13
|
Requires-Dist: h5py>=2.10.0
|
@@ -0,0 +1,7 @@
|
|
1
|
+
antspymm/__init__.py,sha256=DnkidUfEu3Dl0tuWNTA-9VOUkBtH_cROKiPGNNXNagU,4637
|
2
|
+
antspymm/mm.py,sha256=oPHhV70IhFXCKI26rP5HgKpkb2OjqCWkqoy4cSxJXqA,526866
|
3
|
+
antspymm-1.5.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
4
|
+
antspymm-1.5.1.dist-info/METADATA,sha256=_I4UmZGWLM6KkmTJc21wKW1DrjT2TY723C1jr7LA3Fw,25939
|
5
|
+
antspymm-1.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
+
antspymm-1.5.1.dist-info/top_level.txt,sha256=iyD1sRhCKzfwKRJLq5ZUeV9xsv1cGQl8Ejp6QwXM1Zg,9
|
7
|
+
antspymm-1.5.1.dist-info/RECORD,,
|
antspymm-1.4.9.dist-info/RECORD
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
antspymm/__init__.py,sha256=ZdNJyHwS6rzq59v0OK3tE3qSTD0za2iULzSLGkM_0uc,4527
|
2
|
-
antspymm/mm.py,sha256=tJXaT-81XEjNjCOhmGKCjSRB7HEM2z_mlAWxKwJlc3M,517529
|
3
|
-
antspymm-1.4.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
4
|
-
antspymm-1.4.9.dist-info/METADATA,sha256=6uEIRuATG8mqBBCQxdXHyd15xDeVBcyFOH3T-OnFYHw,25940
|
5
|
-
antspymm-1.4.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
6
|
-
antspymm-1.4.9.dist-info/top_level.txt,sha256=iyD1sRhCKzfwKRJLq5ZUeV9xsv1cGQl8Ejp6QwXM1Zg,9
|
7
|
-
antspymm-1.4.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|