antspymm 1.5.6__tar.gz → 1.5.7__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.
Files changed (71) hide show
  1. {antspymm-1.5.6 → antspymm-1.5.7}/PKG-INFO +1 -1
  2. {antspymm-1.5.6 → antspymm-1.5.7}/antspymm/__init__.py +2 -0
  3. {antspymm-1.5.6 → antspymm-1.5.7}/antspymm/mm.py +34 -0
  4. {antspymm-1.5.6 → antspymm-1.5.7}/antspymm.egg-info/PKG-INFO +1 -1
  5. {antspymm-1.5.6 → antspymm-1.5.7}/antspymm.egg-info/SOURCES.txt +1 -1
  6. antspymm-1.5.7/docs/dti_distortion_correction_voxelwise_varying_bvectors_example.py +353 -0
  7. {antspymm-1.5.6 → antspymm-1.5.7}/docs/dti_reconstruction_voxelwise_varying_bvectors_localint.py +3 -2
  8. {antspymm-1.5.6 → antspymm-1.5.7}/pyproject.toml +1 -1
  9. antspymm-1.5.6/docs/dti_distortion_correction_voxelwise_varying_bvectors_example_WIP.py +0 -238
  10. {antspymm-1.5.6 → antspymm-1.5.7}/MANIFEST.in +0 -0
  11. {antspymm-1.5.6 → antspymm-1.5.7}/README.md +0 -0
  12. {antspymm-1.5.6 → antspymm-1.5.7}/antspymm.egg-info/dependency_links.txt +0 -0
  13. {antspymm-1.5.6 → antspymm-1.5.7}/antspymm.egg-info/requires.txt +0 -0
  14. {antspymm-1.5.6 → antspymm-1.5.7}/antspymm.egg-info/top_level.txt +0 -0
  15. {antspymm-1.5.6 → antspymm-1.5.7}/docs/adni_rsfmri_2_nrg_conversion.py +0 -0
  16. {antspymm-1.5.6 → antspymm-1.5.7}/docs/antspymm_annotated_output_tree.pages +0 -0
  17. {antspymm-1.5.6 → antspymm-1.5.7}/docs/antspymm_annotated_output_tree.txt +0 -0
  18. {antspymm-1.5.6 → antspymm-1.5.7}/docs/antspymm_data_dictionary.csv +0 -0
  19. {antspymm-1.5.6 → antspymm-1.5.7}/docs/aslprep_perfusion_run_localint.py +0 -0
  20. {antspymm-1.5.6 → antspymm-1.5.7}/docs/bids_2_nrg.py +0 -0
  21. {antspymm-1.5.6 → antspymm-1.5.7}/docs/bids_cohort_example.py +0 -0
  22. {antspymm-1.5.6 → antspymm-1.5.7}/docs/bind_mm_wide.R +0 -0
  23. {antspymm-1.5.6 → antspymm-1.5.7}/docs/blind_qc.Rmd +0 -0
  24. {antspymm-1.5.6 → antspymm-1.5.7}/docs/blind_qc.html +0 -0
  25. {antspymm-1.5.6 → antspymm-1.5.7}/docs/blind_qc.py +0 -0
  26. {antspymm-1.5.6 → antspymm-1.5.7}/docs/convert_adni_dti_to_nrg.R +0 -0
  27. {antspymm-1.5.6 → antspymm-1.5.7}/docs/deepnbm.jpg +0 -0
  28. {antspymm-1.5.6 → antspymm-1.5.7}/docs/deformation_gradient_reo.py +0 -0
  29. {antspymm-1.5.6 → antspymm-1.5.7}/docs/describe_mm_data.R +0 -0
  30. {antspymm-1.5.6 → antspymm-1.5.7}/docs/dipy_dti_recon.py +0 -0
  31. {antspymm-1.5.6 → antspymm-1.5.7}/docs/dti_recon.py +0 -0
  32. {antspymm-1.5.6 → antspymm-1.5.7}/docs/dti_reg.py +0 -0
  33. {antspymm-1.5.6 → antspymm-1.5.7}/docs/dwi_rebasing.py +0 -0
  34. {antspymm-1.5.6 → antspymm-1.5.7}/docs/dwi_run.py +0 -0
  35. {antspymm-1.5.6 → antspymm-1.5.7}/docs/dwi_run_ptbp_scrub.py +0 -0
  36. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ex_rsfmri_run_minimal_ptbp.py +0 -0
  37. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ex_sr.py +0 -0
  38. {antspymm-1.5.6 → antspymm-1.5.7}/docs/example_antspymm_output.csv +0 -0
  39. {antspymm-1.5.6 → antspymm-1.5.7}/docs/example_run_from_directory.py +0 -0
  40. {antspymm-1.5.6 → antspymm-1.5.7}/docs/flair_run_localint.py +0 -0
  41. {antspymm-1.5.6 → antspymm-1.5.7}/docs/joint_dti_recon_localint.py +0 -0
  42. {antspymm-1.5.6 → antspymm-1.5.7}/docs/make_dict_table.Rmd +0 -0
  43. {antspymm-1.5.6 → antspymm-1.5.7}/docs/make_dict_table.html +0 -0
  44. {antspymm-1.5.6 → antspymm-1.5.7}/docs/mm.py +0 -0
  45. {antspymm-1.5.6 → antspymm-1.5.7}/docs/mm_csv_ex_2.py +0 -0
  46. {antspymm-1.5.6 → antspymm-1.5.7}/docs/mm_csv_localint.py +0 -0
  47. {antspymm-1.5.6 → antspymm-1.5.7}/docs/mm_nrg.py +0 -0
  48. {antspymm-1.5.6 → antspymm-1.5.7}/docs/nrg_cohort_example.py +0 -0
  49. {antspymm-1.5.6 → antspymm-1.5.7}/docs/parallel_study_aggregation_example.py +0 -0
  50. {antspymm-1.5.6 → antspymm-1.5.7}/docs/perfusion_ptbp.py +0 -0
  51. {antspymm-1.5.6 → antspymm-1.5.7}/docs/perfusion_run_nnl.py +0 -0
  52. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ppmi_step1_blind_qc.py +0 -0
  53. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ppmi_step2_outlierness.py +0 -0
  54. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ppmi_step3_mm_nrg_csv.py +0 -0
  55. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ppmi_step4_aggregate.py +0 -0
  56. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ptbp_nrg.py +0 -0
  57. {antspymm-1.5.6 → antspymm-1.5.7}/docs/roi_visualization.py +0 -0
  58. {antspymm-1.5.6 → antspymm-1.5.7}/docs/roi_visualization_ppmi.py +0 -0
  59. {antspymm-1.5.6 → antspymm-1.5.7}/docs/rsfmri_run_minimal_localint.py +0 -0
  60. {antspymm-1.5.6 → antspymm-1.5.7}/docs/run_local_integration_scripts.py +0 -0
  61. {antspymm-1.5.6 → antspymm-1.5.7}/docs/run_mm_example.sh +0 -0
  62. {antspymm-1.5.6 → antspymm-1.5.7}/docs/template_overlays.py +0 -0
  63. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ukbb_rsfmri.py +0 -0
  64. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ukbb_to_nrg_processing.py +0 -0
  65. {antspymm-1.5.6 → antspymm-1.5.7}/docs/ukbb_to_nrg_processing2.py +0 -0
  66. {antspymm-1.5.6 → antspymm-1.5.7}/docs/visualize_tractogram.py +0 -0
  67. {antspymm-1.5.6 → antspymm-1.5.7}/setup.cfg +0 -0
  68. {antspymm-1.5.6 → antspymm-1.5.7}/tests/test_loop.py +0 -0
  69. {antspymm-1.5.6 → antspymm-1.5.7}/tests/test_nrg_validation.py +0 -0
  70. {antspymm-1.5.6 → antspymm-1.5.7}/tests/test_reference_run.py +0 -0
  71. {antspymm-1.5.6 → antspymm-1.5.7}/tests/voxelwise_bvec_dti_recon_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: antspymm
3
- Version: 1.5.6
3
+ Version: 1.5.7
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
@@ -140,5 +140,7 @@ from .mm import pet3d_summary
140
140
  from .mm import deformation_gradient_optimized
141
141
  from .mm import efficient_dwi_fit_voxelwise
142
142
  from .mm import generate_voxelwise_bvecs
143
+ from .mm import distortion_correct_bvecs
144
+
143
145
 
144
146
 
@@ -2138,6 +2138,40 @@ def bvec_reorientation( motion_parameters, bvecs, rebase=None ):
2138
2138
  bvecs[myidx,:] = np.dot( rebase, bvecs[myidx,:] )
2139
2139
  return bvecs
2140
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
+
2141
2175
  def get_dti( reference_image, tensormodel, upper_triangular=True, return_image=False ):
2142
2176
  """
2143
2177
  extract DTI data from a dipy tensormodel
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: antspymm
3
- Version: 1.5.6
3
+ Version: 1.5.7
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
@@ -24,7 +24,7 @@ docs/deepnbm.jpg
24
24
  docs/deformation_gradient_reo.py
25
25
  docs/describe_mm_data.R
26
26
  docs/dipy_dti_recon.py
27
- docs/dti_distortion_correction_voxelwise_varying_bvectors_example_WIP.py
27
+ docs/dti_distortion_correction_voxelwise_varying_bvectors_example.py
28
28
  docs/dti_recon.py
29
29
  docs/dti_reconstruction_voxelwise_varying_bvectors_localint.py
30
30
  docs/dti_reg.py
@@ -0,0 +1,353 @@
1
+ import numpy as np
2
+ import ants
3
+ import os
4
+ from dipy.io.gradients import read_bvals_bvecs
5
+ from scipy.stats import pearsonr
6
+ import antspymm
7
+ import numpy as np
8
+ from scipy.stats import pearsonr
9
+ nt=8
10
+
11
+ ##################################################################
12
+ # for easier to access data with a full mm_csv example, see:
13
+ # github.com:stnava/ANTPD_antspymm
14
+ ##################################################################
15
+ from os.path import exists
16
+ import os
17
+ import signal
18
+ import urllib.request
19
+ import zipfile
20
+ import tempfile
21
+ from pathlib import Path
22
+ from tqdm import tqdm
23
+ import antspynet
24
+
25
+ REQUIRED_FILES = [
26
+ "PPMI/101018/20210412/T1w/1496225/PPMI-101018-20210412-T1w-1496225.nii.gz",
27
+ "PPMI/101018/20210412/DTI_LR/1496234/PPMI-101018-20210412-DTI_LR-1496234.nii.gz"
28
+ ]
29
+
30
+ def broadcast_bvecs_voxelwise(rotated_bvecs, shape):
31
+ return np.broadcast_to(rotated_bvecs, shape + rotated_bvecs.shape).copy()
32
+
33
+
34
+ def _validate_required_files(base_dir, required_files):
35
+ for rel_path in required_files:
36
+ full_path = os.path.join(base_dir, rel_path)
37
+ if not os.path.isfile(full_path):
38
+ print(f"❌ Missing required file: {rel_path}")
39
+ return False
40
+ return True
41
+
42
+ def _download_with_progress(url, destination):
43
+ with urllib.request.urlopen(url) as response, open(destination, 'wb') as out_file:
44
+ total = int(response.getheader('Content-Length', 0))
45
+ with tqdm(total=total, unit='B', unit_scale=True, desc="Downloading", ncols=80) as pbar:
46
+ while True:
47
+ chunk = response.read(8192)
48
+ if not chunk:
49
+ break
50
+ out_file.write(chunk)
51
+ pbar.update(len(chunk))
52
+
53
+ def find_data_dir(candidate_paths=None, max_tries=5, timeout=22, allow_download=None, required_files=REQUIRED_FILES):
54
+ """
55
+ Attempts to locate or download the ANTsPyMM testing dataset.
56
+
57
+ Parameters
58
+ ----------
59
+ candidate_paths : list of str or None
60
+ Directories to search for the data. If None, uses sensible defaults.
61
+ max_tries : int
62
+ Number of chances to enter a valid path manually.
63
+ timeout : int
64
+ Seconds to wait for user input before timing out.
65
+ allow_download : None | str
66
+ If not None, will download to {allow_download}/nrgdata_test if needed.
67
+ required_files : list of str
68
+ Relative paths that must exist inside the data directory.
69
+
70
+ Returns
71
+ -------
72
+ str
73
+ Path to a valid data directory.
74
+ """
75
+ if candidate_paths is None:
76
+ candidate_paths = [
77
+ "~/Downloads/temp/shortrun/nrgdata_test",
78
+ "~/Downloads/ANTsPyMM_testing_data/nrgdata_test",
79
+ "~/data/ppmi/nrgdata_test",
80
+ "/mnt/data/nrgdata_test"
81
+ ]
82
+
83
+ # First, search known paths
84
+ for path in candidate_paths:
85
+ full_path = os.path.expanduser(path)
86
+ if os.path.isdir(full_path) and _validate_required_files(full_path, required_files):
87
+ print(f"✅ Found valid data directory: {full_path}")
88
+ return full_path
89
+
90
+ # Handle automatic download
91
+ if isinstance(allow_download, str):
92
+ base_dir = os.path.expanduser(allow_download)
93
+ target_dir = os.path.join(base_dir, "nrgdata_test")
94
+ if not os.path.isdir(target_dir) or not _validate_required_files(target_dir, required_files):
95
+ print(f"📥 Will download data to: {target_dir}")
96
+ url = "https://figshare.com/ndownloader/articles/29391236/versions/1"
97
+ os.makedirs(base_dir, exist_ok=True)
98
+ zip_path = os.path.join(tempfile.gettempdir(), "antspymm_testdata.zip")
99
+
100
+ try:
101
+ _download_with_progress(url, zip_path)
102
+ print("📦 Extracting...")
103
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
104
+ zip_ref.extractall(base_dir)
105
+ print(f"✅ Extracted to {target_dir}")
106
+ except Exception as e:
107
+ raise RuntimeError(f"❌ Download or extraction failed: {e}")
108
+
109
+ if not _validate_required_files(target_dir, required_files):
110
+ raise RuntimeError(f"❌ Downloaded data is missing required files in {target_dir}")
111
+ return target_dir
112
+
113
+ # Timeout handler for POSIX
114
+ def timeout_handler(signum, frame):
115
+ raise TimeoutError("⏳ No input received in time.")
116
+
117
+ if os.name == 'posix':
118
+ signal.signal(signal.SIGALRM, timeout_handler)
119
+
120
+ # Manual user prompt
121
+ print("🔍 Could not find valid data. You may enter a directory manually.")
122
+ print("🔗 Dataset info: https://figshare.com/articles/dataset/ANTsPyMM_testing_data/29391236")
123
+
124
+ for attempt in range(1, max_tries + 1):
125
+ try:
126
+ if os.name == 'posix':
127
+ signal.alarm(timeout)
128
+ user_input = input(f"⏱️ Attempt {attempt}/{max_tries} — Enter data directory (or 'q' to quit): ").strip()
129
+ if os.name == 'posix':
130
+ signal.alarm(0)
131
+
132
+ if user_input.lower() == 'q':
133
+ break
134
+
135
+ path = os.path.expanduser(user_input)
136
+ if os.path.isdir(path) and _validate_required_files(path, required_files):
137
+ print(f"✅ Using user-provided directory: {path}")
138
+ return path
139
+ else:
140
+ print("❌ Invalid or incomplete directory.")
141
+
142
+ except TimeoutError as e:
143
+ raise RuntimeError(str(e))
144
+ except KeyboardInterrupt:
145
+ raise RuntimeError("User interrupted execution. Exiting.")
146
+
147
+ raise RuntimeError("❗ No valid data directory found and download not permitted.")
148
+
149
+ candidate_rdirs = [
150
+ "~/Downloads/nrgdata_test/",
151
+ "~/Downloads/temp/nrgdata_test/",
152
+ "~/nrgdata_test/",
153
+ "~/data/ppmi/nrgdata_test/",
154
+ "/mnt/data/ppmi_testing/nrgdata_test/"]
155
+
156
+
157
+ rdir = find_data_dir( candidate_rdirs, allow_download="~/Downloads" )
158
+ print(f"Using data directory: {rdir}")
159
+
160
+ nthreads = str(8)
161
+ os.environ["TF_NUM_INTEROP_THREADS"] = nthreads
162
+ os.environ["TF_NUM_INTRAOP_THREADS"] = nthreads
163
+ os.environ["ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS"] = nthreads
164
+ os.environ["OPENBLAS_NUM_THREADS"] = nthreads
165
+ os.environ["MKL_NUM_THREADS"] = nthreads
166
+ import numpy as np
167
+ import glob as glob
168
+ import antspymm
169
+ import ants
170
+ import random
171
+ import re
172
+
173
+ def read_bvecs_rotated(bvec_file, rotmat):
174
+ bvecs = np.loadtxt(bvec_file)
175
+ if bvecs.shape[0] != 3:
176
+ bvecs = bvecs.T
177
+ rotated_bvecs = (rotmat @ bvecs).T
178
+ return rotated_bvecs
179
+
180
+ def mean_rgb_correlation(img1, img2, mask):
181
+ """
182
+ Compute the mean correlation between two RGB images.
183
+
184
+ Parameters
185
+ ----------
186
+ img1 : np.ndarray
187
+ First RGB image as a (H, W, 3) NumPy array.
188
+ img2 : np.ndarray
189
+ Second RGB image as a (H, W, 3) NumPy array.
190
+
191
+ Returns
192
+ -------
193
+ float
194
+ Mean Pearson correlation across the three RGB channels.
195
+ """
196
+ if img1.shape != img2.shape:
197
+ raise ValueError("Input images must have the same shape.")
198
+ correlations = []
199
+ img1c = ants.split_channels(img1)
200
+ img2c = ants.split_channels(img2)
201
+ for c in range(3): # R, G, B
202
+ x = extract_masked_values( img1c[c], mask)
203
+ y = extract_masked_values( img2c[c], mask)
204
+ if np.std(x) == 0 or np.std(y) == 0:
205
+ corr = 0.0 # Handle flat images
206
+ else:
207
+ corr, _ = pearsonr(x, y)
208
+ correlations.append(corr)
209
+ return np.mean(correlations)
210
+
211
+ import numpy as np
212
+ import ants
213
+
214
+ def mean_rgb_mae(img1, img2, mask):
215
+ """
216
+ Compute the mean absolute error (MAE) between two RGB images.
217
+
218
+ Parameters
219
+ ----------
220
+ img1 : np.ndarray
221
+ First RGB image as a (H, W, 3) NumPy array.
222
+ img2 : np.ndarray
223
+ Second RGB image as a (H, W, 3) NumPy array.
224
+ mask : ants.ANTsImage
225
+ Binary mask defining valid pixels for error calculation.
226
+
227
+ Returns
228
+ -------
229
+ float
230
+ Mean absolute error across the three RGB channels.
231
+ """
232
+ if img1.shape != img2.shape:
233
+ raise ValueError("Input images must have the same shape.")
234
+
235
+ mae_values = []
236
+ img1c = ants.split_channels(img1)
237
+ img2c = ants.split_channels(img2)
238
+
239
+ for c in range(3): # R, G, B
240
+ x = extract_masked_values(img1c[c], mask)
241
+ y = extract_masked_values(img2c[c], mask)
242
+ mae = np.mean(np.abs(x - y))
243
+ mae_values.append(mae)
244
+
245
+ return np.mean(mae_values)
246
+
247
+ def extract_masked_values(image, mask):
248
+ return image.numpy()[mask.numpy() > 0]
249
+
250
+ import numpy as np
251
+
252
+ def verify_unit_bvecs(bvecs, tol=1e-5):
253
+ """
254
+ Verifies that each b-vector has unit norm within a tolerance.
255
+
256
+ Parameters
257
+ ----------
258
+ bvecs : array-like, shape (N, 3)
259
+ Array of b-vectors, one per diffusion direction.
260
+ tol : float
261
+ Tolerance for unit norm.
262
+
263
+ Returns
264
+ -------
265
+ is_unit : np.ndarray, shape (N,)
266
+ Boolean array indicating if each b-vector is unit norm.
267
+ norms : np.ndarray, shape (N,)
268
+ Norms of each b-vector.
269
+ """
270
+ bvecs = np.asarray(bvecs)
271
+ norms = np.linalg.norm(bvecs, axis=1)
272
+ is_unit = np.abs(norms - 1) < tol
273
+ return is_unit, norms
274
+
275
+ mydir = rdir + "PPMI/"
276
+ outdir = re.sub( 'nrgdata_test', 'antspymmoutput', rdir )
277
+ import glob as glob
278
+
279
+ t1fn=glob.glob(mydir+"101018/20210412/T1w/1496225/*.nii.gz")
280
+ if len(t1fn) > 0:
281
+ t1fn=t1fn[0]
282
+ print("Begin " + t1fn)
283
+ dtfn=glob.glob(mydir+"101018/20210412/DTI*/*/*.nii.gz")
284
+ dtfn.sort()
285
+
286
+ import re
287
+
288
+ # def test_efficient_dwi_fit_voxelwise_distortion_correction():
289
+ if len(dtfn) > 0:
290
+ img_LR_in = ants.image_read(dtfn[0])
291
+ img_LR_in_avg = ants.get_average_of_timeseries( img_LR_in )
292
+ mask = img_LR_in_avg.get_mask()
293
+ bvalfn = re.sub( 'nii.gz', 'bval', dtfn[0] )
294
+ bvecfn = re.sub( 'nii.gz', 'bvec', dtfn[0] )
295
+ if not exists(bvalfn) or not exists(bvecfn):
296
+ raise RuntimeError(f"Required bval/bvec files not found: {bvalfn}, {bvecfn}")
297
+ print(f"📁 Loading subject LR data from {bvalfn} ")
298
+ bvals, bvecs = read_bvals_bvecs(bvalfn, bvecfn)
299
+ bvecs = np.asarray(bvecs)
300
+ shape = img_LR_in.shape[:3]
301
+
302
+ print("📁 Loading subject T1 data...")
303
+ t1w = ants.image_read(t1fn)
304
+ t1w = ants.resample_image(t1w, [2, 2, 2], use_voxels=False)
305
+ bxt = antspynet.brain_extraction(t1w, modality='t1', verbose=False).threshold_image(0.5, 1.5)
306
+
307
+ if not "mytx" in globals():
308
+ dwianat = ants.slice_image( img_LR_in, idx=0, axis=3)
309
+ mytx = ants.registration( t1w, dwianat, 'SyNCC', syn_metric='CC', syn_sampling=2, total_sigma=0.5 )
310
+ mytx2 = ants.apply_transforms(t1w, img_LR_in_avg, mytx['fwdtransforms'],
311
+ interpolator='linear', imagetype=0, compose='/tmp/comptxDT2T1' )
312
+ print( mytx2 )
313
+
314
+ print("🔄 now with distortion correction...")
315
+ mydef = ants.image_read( mytx2 )
316
+ mywarp = ants.transform_from_displacement_field( mydef )
317
+ img_w = antspymm.timeseries_transform(mywarp, img_LR_in, reference=t1w)
318
+ print("🧠 Running warped fit...")
319
+
320
+ if not "FA_w" in globals():
321
+ mydefgrad = antspymm.deformation_gradient_optimized( mydef,
322
+ to_rotation=False, to_inverse_rotation=True )
323
+ bvecsdc = antspymm.distortion_correct_bvecs( bvecs, mydefgrad, t1w.direction, img_LR_in_avg.direction )
324
+ FA_w, MD_w, RGB_w = antspymm.efficient_dwi_fit_voxelwise(
325
+ imagein=img_w,
326
+ maskin=bxt,
327
+ bvals=bvals,
328
+ bvecs_5d=bvecsdc,
329
+ model_params={},
330
+ bvals_to_use=None,
331
+ num_threads=nt,
332
+ verbose=False
333
+ )
334
+
335
+ if not "FA_w2" in globals():
336
+ FA_w2, MD_w2, RGB_w2 = antspymm.efficient_dwi_fit_voxelwise(
337
+ imagein=img_w,
338
+ maskin=bxt,
339
+ bvals=bvals,
340
+ bvecs_5d=broadcast_bvecs_voxelwise(bvecs, t1w.shape),
341
+ model_params={},
342
+ bvals_to_use=None,
343
+ num_threads=nt,
344
+ verbose=False
345
+ )
346
+
347
+ print("📊 Comparing results...")
348
+ maske=ants.iMath(bxt,'ME',3)
349
+ fa_corr = mean_rgb_correlation( RGB_w, RGB_w2, maske )
350
+ print(f"✅ Direction-weighted FA correlation (original vs distortion corrected): {fa_corr:.4f}")
351
+
352
+ ants.image_write( t1w, '/tmp/t1w.nii.gz' )
353
+ ants.image_write( RGB_w, '/tmp/rgbw.nii.gz' )
@@ -234,9 +234,10 @@ if True:
234
234
  mydef=ants.image_read(comptx)
235
235
  mydefgrad = antspymm.deformation_gradient_optimized( mydef,
236
236
  to_rotation=False, to_inverse_rotation=True )
237
- bvecsRLw = antspymm.generate_voxelwise_bvecs( bvecs_rotated, mydefgrad, transpose=False )
238
- mywarp = ants.transform_from_displacement_field( mydef )
237
+ # bvecsRLw = antspymm.generate_voxelwise_bvecs( bvecs_rotated, mydefgrad, transpose=False )
239
238
  img_rotated_avg = ants.get_average_of_timeseries( img_rotated )
239
+ bvecsRLw = antspymm.distortion_correct_bvecs( bvecs_rotated, mydefgrad, img_LR_in_avg.direction, img_rotated_avg.direction )
240
+ mywarp = ants.transform_from_displacement_field( mydef )
240
241
  img_w = antspymm.timeseries_transform(mywarp, img_rotated, reference=img_rotated_avg )
241
242
 
242
243
  correlations = []
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "antspymm"
7
- version = "1.5.6"
7
+ version = "1.5.7"
8
8
  description = "multi-channel/time-series medical image processing with antspyx"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,238 +0,0 @@
1
- import numpy as np
2
- import ants
3
- import os
4
- from dipy.io.gradients import read_bvals_bvecs
5
- from scipy.stats import pearsonr
6
- import antspymm
7
- nt = 2
8
-
9
- import numpy as np
10
- from scipy.stats import pearsonr
11
-
12
- def read_bvecs_rotated(bvec_file, rotmat):
13
- bvecs = np.loadtxt(bvec_file)
14
- if bvecs.shape[0] != 3:
15
- bvecs = bvecs.T
16
- rotated_bvecs = (rotmat @ bvecs).T
17
- return rotated_bvecs
18
-
19
- def mean_rgb_correlation(img1, img2, mask):
20
- """
21
- Compute the mean correlation between two RGB images.
22
-
23
- Parameters
24
- ----------
25
- img1 : np.ndarray
26
- First RGB image as a (H, W, 3) NumPy array.
27
- img2 : np.ndarray
28
- Second RGB image as a (H, W, 3) NumPy array.
29
-
30
- Returns
31
- -------
32
- float
33
- Mean Pearson correlation across the three RGB channels.
34
- """
35
- if img1.shape != img2.shape:
36
- raise ValueError("Input images must have the same shape.")
37
- correlations = []
38
- img1c = ants.split_channels(img1)
39
- img2c = ants.split_channels(img2)
40
- for c in range(3): # R, G, B
41
- x = extract_masked_values( img1c[c], mask)
42
- y = extract_masked_values( img2c[c], mask)
43
- if np.std(x) == 0 or np.std(y) == 0:
44
- corr = 0.0 # Handle flat images
45
- else:
46
- corr, _ = pearsonr(x, y)
47
- correlations.append(corr)
48
- return np.mean(correlations)
49
-
50
- import numpy as np
51
- import ants
52
-
53
- def mean_rgb_mae(img1, img2, mask):
54
- """
55
- Compute the mean absolute error (MAE) between two RGB images.
56
-
57
- Parameters
58
- ----------
59
- img1 : np.ndarray
60
- First RGB image as a (H, W, 3) NumPy array.
61
- img2 : np.ndarray
62
- Second RGB image as a (H, W, 3) NumPy array.
63
- mask : ants.ANTsImage
64
- Binary mask defining valid pixels for error calculation.
65
-
66
- Returns
67
- -------
68
- float
69
- Mean absolute error across the three RGB channels.
70
- """
71
- if img1.shape != img2.shape:
72
- raise ValueError("Input images must have the same shape.")
73
-
74
- mae_values = []
75
- img1c = ants.split_channels(img1)
76
- img2c = ants.split_channels(img2)
77
-
78
- for c in range(3): # R, G, B
79
- x = extract_masked_values(img1c[c], mask)
80
- y = extract_masked_values(img2c[c], mask)
81
- mae = np.mean(np.abs(x - y))
82
- mae_values.append(mae)
83
-
84
- return np.mean(mae_values)
85
-
86
- def extract_masked_values(image, mask):
87
- return image.numpy()[mask.numpy() > 0]
88
-
89
- import numpy as np
90
-
91
- def verify_unit_bvecs(bvecs, tol=1e-5):
92
- """
93
- Verifies that each b-vector has unit norm within a tolerance.
94
-
95
- Parameters
96
- ----------
97
- bvecs : array-like, shape (N, 3)
98
- Array of b-vectors, one per diffusion direction.
99
- tol : float
100
- Tolerance for unit norm.
101
-
102
- Returns
103
- -------
104
- is_unit : np.ndarray, shape (N,)
105
- Boolean array indicating if each b-vector is unit norm.
106
- norms : np.ndarray, shape (N,)
107
- Norms of each b-vector.
108
- """
109
- bvecs = np.asarray(bvecs)
110
- norms = np.linalg.norm(bvecs, axis=1)
111
- is_unit = np.abs(norms - 1) < tol
112
- return is_unit, norms
113
-
114
- # def test_efficient_dwi_fit_voxelwise_distortion_correction():
115
- if True:
116
- print("simple test for distortion_correction consistency of efficient_dwi_fit_voxelwise")
117
- ex_path = os.path.expanduser( "~/.antspyt1w/" )
118
- ex_path_mm = os.path.expanduser( "~/.antspymm/" )
119
- #### Load in data ####
120
- print("Load in subject data ...")
121
- lrid = "I1499279_Anon_20210819142214_5"
122
- rlid = "I1499337_Anon_20210819142214_6"
123
- # Load paths
124
- print("📁 Loading subject LR data...")
125
- lrid = os.path.join(ex_path_mm, lrid )
126
- img_LR_in = ants.image_read(lrid + '.nii.gz')
127
- img_LR_in_avg = ants.get_average_of_timeseries( img_LR_in )
128
- mask = img_LR_in_avg.get_mask()
129
- bvals, bvecs = read_bvals_bvecs(lrid + '.bval', lrid + '.bvec')
130
- bvecs = np.asarray(bvecs)
131
- shape = img_LR_in.shape[:3]
132
-
133
- print("📁 Loading subject RL data...")
134
- rlid = os.path.join(ex_path_mm, rlid )
135
- img_RL_in = ants.image_read(rlid + '.nii.gz')
136
- bvalsRL, bvecsRL = read_bvals_bvecs(rlid + '.bval', rlid + '.bvec')
137
-
138
- img_RL_in_avg = ants.get_average_of_timeseries( img_RL_in )
139
- maskRL = img_RL_in_avg.get_mask()
140
-
141
- print("🧠 Running baseline LR fit...")
142
- bvecs_5d_orig = np.broadcast_to(bvecs, shape + bvecs.shape).copy()
143
- if not "FA_orig" in globals():
144
- FA_orig, MD_orig, RGB_orig = antspymm.efficient_dwi_fit_voxelwise(
145
- imagein=img_LR_in,
146
- maskin=mask,
147
- bvals=bvals,
148
- bvecs_5d=bvecs_5d_orig,
149
- model_params={},
150
- bvals_to_use=None,
151
- num_threads=nt,
152
- verbose=False
153
- )
154
-
155
- bvecs_5d_origRL = np.broadcast_to(np.asarray(bvecsRL), shape + bvecsRL.shape).copy()
156
- if not "FA_origRL" in globals():
157
- FA_origRL, MD_origRL, RGB_origRL = antspymm.efficient_dwi_fit_voxelwise(
158
- imagein=img_RL_in,
159
- maskin=maskRL,
160
- bvals=bvalsRL,
161
- bvecs_5d=bvecs_5d_origRL,
162
- model_params={},
163
- bvals_to_use=None,
164
- num_threads=nt,
165
- verbose=False
166
- )
167
-
168
- print("dist corr")
169
- if not "mytx" in globals():
170
- mytx = ants.registration( FA_orig, FA_origRL, 'SyNBold' )
171
- mytx2 = ants.apply_transforms(FA_orig, FA_origRL, mytx['fwdtransforms'],
172
- interpolator='linear', imagetype=0, compose='/tmp/comptx' )
173
- print( mytx2 )
174
-
175
- print("🔄 now with distortion correction...")
176
- mydef = ants.image_read( mytx2 )
177
- mywarp = ants.transform_from_displacement_field( mydef )
178
- img_w = antspymm.timeseries_transform(mywarp, img_RL_in, reference=img_LR_in_avg)
179
- mask_w = ants.apply_ants_transform_to_image(mywarp, maskRL, reference=img_LR_in_avg, interpolation='nearestNeighbor')
180
- print("🧠 Running warped fit...")
181
- if not "FA_w" in globals():
182
- bvecsRL = np.asarray(bvecsRL)
183
- mydefgrad = antspymm.deformation_gradient_optimized( mydef,
184
- to_rotation=False, to_inverse_rotation=True )
185
- bvecsRLw = antspymm.generate_voxelwise_bvecs( bvecsRL, mydefgrad, transpose=False )
186
- FA_w, MD_w, RGB_w = antspymm.efficient_dwi_fit_voxelwise(
187
- imagein=img_w,
188
- maskin=mask_w,
189
- bvals=bvalsRL,
190
- bvecs_5d=bvecsRLw,
191
- model_params={},
192
- bvals_to_use=None,
193
- num_threads=nt,
194
- verbose=False
195
- )
196
-
197
- if not "FA_w2" in globals():
198
- bvecsRL = np.asarray(bvecsRL)
199
- FA_w2, MD_w2, RGB_w2 = antspymm.efficient_dwi_fit_voxelwise(
200
- imagein=img_w,
201
- maskin=mask_w,
202
- bvals=bvalsRL,
203
- bvecs_5d=bvecs_5d_origRL,
204
- model_params={},
205
- bvals_to_use=None,
206
- num_threads=nt,
207
- verbose=False
208
- )
209
-
210
- fff = mean_rgb_correlation
211
- print("📊 Comparing results...")
212
- maskJoined = ants.threshold_image( mask + mask_w, 1.05, 2.0 )
213
- maske=ants.iMath(maskJoined,'ME',3)
214
- fa_corr = fff( RGB_orig, RGB_w, maske )
215
- print(f"✅ FA correlation (original vs distortion corrected): {fa_corr:.4f}")
216
-
217
- fa_corrX = fff( RGB_orig, RGB_w2, maske )
218
- print(f"✅ FA correlation (original vs distortion corrected global recon): {fa_corrX:.4f}")
219
-
220
- RGB_origRLc = ants.split_channels(RGB_origRL)
221
- for c in range(3):
222
- RGB_origRLc[c] = ants.apply_ants_transform_to_image(
223
- mywarp, RGB_origRLc[c],
224
- reference=img_LR_in_avg, interpolation='linear'
225
- )
226
-
227
- fa_corrY = fff( RGB_orig, ants.merge_channels(RGB_origRLc), maske )
228
- print(f"✅ FA correlation (original vs warped RGB global recon): {fa_corrY:.4f}")
229
-
230
- # assert fa_corr > 0.80, "FA correlation too low"
231
-
232
- print("🎉 Test passed: model is distortion-consistent.")
233
-
234
- # ants.image_write( FA_orig, '/tmp/xxx.nii.gz' )
235
- # ants.image_write( FA_w, '/tmp/yyy.nii.gz' )
236
- #
237
- # Example usage:
238
- # test_efficient_dwi_fit_voxelwise_distortion_correction()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes