OSCR

A Realistic In Silico Brain Phantom for Quantifying Susceptibility Anisotropy-Induced Error in Susceptibility Separation.

Code ↔ Paper

5 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 5 matches · 2 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Methods › Susceptibility Separation Phantom Validation Using In Vivo Data ↔ Manuscript_Figures.ipynb, lines 904–969 · score 0.67 · linear regression, simulated local field, local field map, slope, transformation, vivo
  2. [2] § Results › Comparison Between Simulated Versus In Vivo Susceptibility Maps ↔ Manuscript_Figures.ipynb, lines 904–969 · score 0.61 · vivo local field, linear regression, simulated local field, scatter, maps
  3. [3] § Methods › Phantom Creation › Susceptibility Maps ↔ PhantomCreation.m, lines 71–187 · score 0.56 · standard deviation, Gaussian noise, weighted, phantom, map, susceptibility
  4. [4] § Methods › Phantom Creation › Susceptibility Maps ↔ func/GRESimulation.m, the whole file · a weak match · score 0.56 · standard deviation, Gaussian noise, field, map
  5. [5] § Methods › Data Simulation › 3 T Simulations ↔ func/GRESimulation.m, the whole file · a weak match · score 0.54 · adding Gaussian noise, GRE, signal, TR, TE, phase

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,458 lines · 56 KB · MIT · 2 matches

  1. # %% [markdown]
  2. # # Install dependencies
  3. # %%
  4. import os
  5. import numpy as np
  6. import nibabel as nib
  7. import matplotlib.pyplot as plt
  8. from scipy.stats import linregress
  9. from pathlib import Path
  10. from scipy import stats
  11. from sklearn.linear_model import LinearRegression
  12. from scipy.stats import gaussian_kde
  13. from matplotlib.lines import Line2D
  14. from matplotlib.patches import Patch
  15. from pathlib import Path
  16. # %% [markdown]
  17. # # Download the data
  18. # %%
  19. base_dir = base_dir = Path.cwd()
  20. # Verify that the parent of the entered path exists
  21. if not base_dir.parent.exists():
  22. raise FileNotFoundError(f"The specified path {base_dir.parent} does not exist.")
  23. else:
  24. # Create the last folder if it doesn't exist
  25. base_dir.mkdir(parents=True, exist_ok=True)
  26. print(f"Using data directory: {base_dir}")
  27. !pip install osfclient
  28. # Clone the OSF project into the specified directory
  29. !osf -p 9xwhz clone "{base_dir}"
  30. # %% [markdown]
  31. # # Impact of susceptibility anisotropy
  32. # %%
  33. # ---------------------------
  34. # Parameters and File Paths
  35. # ---------------------------
  36. num_algorithms = 4
  37. algorithm_names = [
  38. '$\\chi$-separation',
  39. 'R2*-QSM',
  40. 'APART-QSM',
  41. 'DECOMPOSE-QSM'
  42. ]
  43. # Load segmentation mask (assumed to contain region labels 1 to 11)
  44. segmentation2_path = base_dir / 'osfstorage' / 'Masks' / 'white_matter_mask.nii.gz'
  45. segmentation2 = nib.load(segmentation2_path).get_fdata()
  46. region_labels = list(range(1, 12)) # regions 1 to 11
  47. # Load simulated maps (common for all algorithms)
  48. simulated_with_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_negative_with_anisotropy.nii.gz'
  49. simulated_without_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_negative.nii.gz'
  50. simulated_with = nib.load(simulated_with_path).get_fdata()
  51. simulated_without = nib.load(simulated_without_path).get_fdata()
  52. # Define measured maps for each algorithm
  53. measured_maps = {
  54. 0: { # Algorithm 1: χ-separation
  55. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiNegMap.nii',
  56. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'Without_anisotropy' / 'ChiNegMap.nii',
  57. },
  58. 1: { # Algorithm 2: R2*-QSM
  59. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiNegMap.nii',
  60. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'Without_anisotropy' / 'ChiNegMap.nii',
  61. },
  62. 2: { # Algorithm 3: APART-QSM
  63. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_dia_abs.nii',
  64. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'Without_anisotropy' / 'X_dia_abs.nii',
  65. },
  66. 3: { # Algorithm 4: DECOMPOSE-QSM
  67. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_DCS_abs.nii',
  68. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'Without_anisotropy' / 'results_DCS_abs.nii',
  69. },
  70. }
  71. fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12), dpi=300)
  72. t_test_results = {}
  73. for idx in range(num_algorithms):
  74. ax = axes[idx // 2, idx % 2]
  75. algorithm_name = algorithm_names[idx]
  76. measured_with = nib.load(measured_maps[idx]['x_minus_with_anisotropy']).get_fdata() * -1
  77. measured_without = nib.load(measured_maps[idx]['x_minus_without_anisotropy']).get_fdata() * -1
  78. errors_with_regions = []
  79. errors_without_regions = []
  80. for region in region_labels:
  81. region_mask = segmentation2 == region
  82. if np.any(region_mask):
  83. # For "With Anisotropy"
  84. sim_val_with = np.mean(simulated_with[region_mask])
  85. meas_val_with = np.mean(measured_with[region_mask])
  86. if sim_val_with != 0:
  87. error_with = ((meas_val_with - sim_val_with) / sim_val_with) ** 2 * 100
  88. else:
  89. error_with = np.nan
  90. # For "Without Anisotropy"
  91. sim_val_without = np.mean(simulated_without[region_mask])
  92. meas_val_without = np.mean(measured_without[region_mask])
  93. if sim_val_without != 0:
  94. error_without = ((meas_val_without - sim_val_without) / sim_val_without) ** 2 * 100
  95. else:
  96. error_without = np.nan
  97. errors_with_regions.append(error_with)
  98. errors_without_regions.append(error_without)
  99. errors_with_regions = np.array(errors_with_regions)
  100. errors_without_regions = np.array(errors_without_regions)
  101. errors_with_regions = errors_with_regions[~np.isnan(errors_with_regions)]
  102. errors_without_regions = errors_without_regions[~np.isnan(errors_without_regions)]
  103. x_vals = np.linspace(0, 70, 200)
  104. max_height = 0.4
  105. # Plot KDE for "With Anisotropy"
  106. if errors_with_regions.size > 1:
  107. kde_with = gaussian_kde(errors_with_regions)
  108. density_with = kde_with(x_vals)
  109. scaling_with = max_height / np.max(density_with) if np.max(density_with) > 0 else 1
  110. density_with_scaled = density_with * scaling_with
  111. ax.fill_between(x_vals, 0, density_with_scaled, color='tab:blue', alpha=0.4)
  112. ax.plot(x_vals, density_with_scaled, color='tab:blue', alpha=0.7)
  113. mean_mspe_with = np.mean(errors_with_regions)
  114. elif errors_with_regions.size == 1:
  115. mean_mspe_with = errors_with_regions[0]
  116. ax.plot([mean_mspe_with], [max_height/2], marker='o', color='tab:blue')
  117. else:
  118. mean_mspe_with = None # no data available
  119. # Plot KDE for "Without Anisotropy"
  120. if errors_without_regions.size > 1:
  121. kde_without = gaussian_kde(errors_without_regions)
  122. density_without = kde_without(x_vals)
  123. scaling_without = max_height / np.max(density_without) if np.max(density_without) > 0 else 1
  124. density_without_scaled = density_without * scaling_without
  125. ax.fill_between(x_vals, 0, density_without_scaled, color='tab:orange', alpha=0.4)
  126. ax.plot(x_vals, density_without_scaled, color='tab:orange', alpha=0.7)
  127. mean_mspe_without = np.mean(errors_without_regions)
  128. elif errors_without_regions.size == 1:
  129. mean_mspe_without = errors_without_regions[0]
  130. ax.plot([mean_mspe_without], [max_height/2], marker='o', color='tab:orange')
  131. else:
  132. mean_mspe_without = None
  133. if mean_mspe_with is not None:
  134. ax.text(0.95, 0.8, f"Mean MSPE: {mean_mspe_with:.1f}%", transform=ax.transAxes,
  135. color='tab:blue', ha='right', va='center', fontsize=12)
  136. if mean_mspe_without is not None:
  137. ax.text(0.95, 0.7, f"Mean MSPE: {mean_mspe_without:.1f}%", transform=ax.transAxes,
  138. color='tab:orange', ha='right', va='center', fontsize=12)
  139. # ------------------------------------------------
  140. # Create and add a legend for the subplot
  141. # ------------------------------------------------
  142. legend_elements = [
  143. Line2D([0], [0], color='tab:blue', lw=2, label='With Anisotropy'),
  144. Line2D([0], [0], color='tab:orange', lw=2, label='Without Anisotropy')
  145. ]
  146. ax.legend(handles=legend_elements, loc='upper right')
  147. # Set title and axis labels.
  148. ax.set_title(algorithm_name, fontsize=14)
  149. ax.set_xlabel("MSPE of $\\chi^-$ (%)", fontsize=12)
  150. ax.set_ylabel("Density (a.u.)", fontsize=12)
  151. ax.set_xlim(0, 70)
  152. ax.set_ylim(0, max_height * 1.2)
  153. ax.grid(alpha=0.3)
  154. # ---------------------------
  155. # Final figure adjustments
  156. # ---------------------------
  157. plt.tight_layout(rect=[0, 0, 1, 0.96])
  158. plt.show()
  159. # %% [markdown]
  160. # # Impact of noise
  161. # %%
  162. # -------------------------------------------
  163. # Define algorithms, SNR levels, and errors
  164. # -------------------------------------------
  165. algorithms = [
  166. '$\\chi$-separation',
  167. 'R2*-QSM',
  168. 'APART-QSM',
  169. 'DECOMPOSE-QSM'
  170. ]
  171. snr_levels = [50, 100, 200, 300]
  172. # Initialize dictionaries
  173. x_positive_errors = {alg: [] for alg in algorithms}
  174. x_negative_errors = {alg: [] for alg in algorithms}
  175. def construct_path(*args):
  176. """Construct a path with 'osfstorage' as a subdirectory of base_dir."""
  177. return base_dir / 'osfstorage' / Path(*args)
  178. # -------------------------------------------
  179. # Load Simulated Maps
  180. # -------------------------------------------
  181. simulated_positive_path = construct_path('Susceptibility_Separation_Results', 'Chi_positive.nii.gz')
  182. simulated_negative_path = construct_path('Susceptibility_Separation_Results', 'Chi_negative_with_anisotropy.nii.gz')
  183. simulated_positive = nib.load(simulated_positive_path).get_fdata()
  184. simulated_negative = nib.load(simulated_negative_path).get_fdata()
  185. # -------------------------------------------
  186. # Load Segmentation Masks
  187. # -------------------------------------------
  188. segmentation_positive_path = construct_path('Masks', 'SegmentedModel.nii.gz')
  189. segmentation_negative_path = construct_path('Masks', 'white_matter_mask.nii.gz')
  190. segmentation_positive = nib.load(segmentation_positive_path).get_fdata()
  191. segmentation_negative = nib.load(segmentation_negative_path).get_fdata()
  192. # -------------------------------------------
  193. # Measured Maps: Positive & Negative
  194. # -------------------------------------------
  195. measured_maps_positive = {
  196. '$\\chi$-separation': {
  197. 300: construct_path('Noise', 'X-separation', 'SNR_300', 'ChiPosMap.nii.gz'),
  198. 200: construct_path('Noise', 'X-separation', 'SNR_200', 'ChiPosMap.nii.gz'),
  199. 100: construct_path('Noise', 'X-separation', 'SNR_100', 'ChiPosMap.nii.gz'),
  200. 50: construct_path('Noise', 'X-separation', 'SNR_50', 'ChiPosMap.nii.gz'),
  201. },
  202. 'R2*-QSM': {
  203. 300: construct_path('Noise', 'R2star-QSM', 'SNR_300', 'ChiPosMap.nii.gz'),
  204. 200: construct_path('Noise', 'R2star-QSM', 'SNR_200', 'ChiPosMap.nii.gz'),
  205. 100: construct_path('Noise', 'R2star-QSM', 'SNR_100', 'ChiPosMap.nii.gz'),
  206. 50: construct_path('Noise', 'R2star-QSM', 'SNR_50', 'ChiPosMap.nii.gz'),
  207. },
  208. 'APART-QSM': {
  209. 300: construct_path('Noise', 'APART-QSM', 'SNR_300', 'X_para.nii.gz'),
  210. 200: construct_path('Noise', 'APART-QSM', 'SNR_200', 'X_para.nii.gz'),
  211. 100: construct_path('Noise', 'APART-QSM', 'SNR_100', 'X_para.nii.gz'),
  212. 50: construct_path('Noise', 'APART-QSM', 'SNR_50', 'X_para.nii.gz'),
  213. },
  214. 'DECOMPOSE-QSM': {
  215. 300: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_300', 'results_PCS.nii.gz'),
  216. 200: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_200', 'results_PCS.nii.gz'),
  217. 100: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_100', 'results_PCS.nii.gz'),
  218. 50: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_50', 'results_PCS.nii.gz'),
  219. },
  220. }
  221. measured_maps_negative = {
  222. '$\\chi$-separation': {
  223. 300: construct_path('Noise', 'X-separation', 'SNR_300', 'ChiNegMap.nii.gz'),
  224. 200: construct_path('Noise', 'X-separation', 'SNR_200', 'ChiNegMap.nii.gz'),
  225. 100: construct_path('Noise', 'X-separation', 'SNR_100', 'ChiNegMap.nii.gz'),
  226. 50: construct_path('Noise', 'X-separation', 'SNR_50', 'ChiNegMap.nii.gz'),
  227. },
  228. 'R2*-QSM': {
  229. 300: construct_path('Noise', 'R2star-QSM', 'SNR_300', 'ChiNegMap.nii.gz'),
  230. 200: construct_path('Noise', 'R2star-QSM', 'SNR_200', 'ChiNegMap.nii.gz'),
  231. 100: construct_path('Noise', 'R2star-QSM', 'SNR_100', 'ChiNegMap.nii.gz'),
  232. 50: construct_path('Noise', 'R2star-QSM', 'SNR_50', 'ChiNegMap.nii.gz'),
  233. },
  234. 'APART-QSM': {
  235. 300: construct_path('Noise', 'APART-QSM', 'SNR_300', 'X_dia_abs.nii.gz'),
  236. 200: construct_path('Noise', 'APART-QSM', 'SNR_200', 'X_dia_abs.nii.gz'),
  237. 100: construct_path('Noise', 'APART-QSM', 'SNR_100', 'X_dia_abs.nii.gz'),
  238. 50: construct_path('Noise', 'APART-QSM', 'SNR_50', 'X_dia_abs.nii.gz'),
  239. },
  240. 'DECOMPOSE-QSM': {
  241. 300: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_300', 'results_DCS_abs.nii.gz'),
  242. 200: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_200', 'results_DCS_abs.nii.gz'),
  243. 100: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_100', 'results_DCS_abs.nii.gz'),
  244. 50: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_50', 'results_DCS_abs.nii.gz'),
  245. },
  246. }
  247. # -------------------------------------------
  248. # Compute Errors
  249. # -------------------------------------------
  250. for alg in algorithms:
  251. for snr in snr_levels:
  252. # Load measured maps (X+ and X-)
  253. measured_positive_path = measured_maps_positive[alg][snr]
  254. measured_negative_path = measured_maps_negative[alg][snr]
  255. measured_positive = nib.load(measured_positive_path).get_fdata()
  256. measured_negative = nib.load(measured_negative_path).get_fdata()
  257. # Multiply X- by -1
  258. measured_negative = -1 * measured_negative
  259. # -----------------------------
  260. # Positive regions: 1 to 9
  261. # -----------------------------
  262. positive_errors = []
  263. for region in range(1, 10):
  264. mask = (segmentation_positive == region)
  265. measured_mean = np.mean(measured_positive[mask])
  266. simulated_mean = np.mean(simulated_positive[mask])
  267. error = ((measured_mean - simulated_mean) / simulated_mean)**2 * 100
  268. positive_errors.append(error)
  269. avg_positive_error = np.mean(positive_errors)
  270. x_positive_errors[alg].append(avg_positive_error)
  271. # -----------------------------
  272. # Negative regions
  273. # - 1 to 9 (segmentation_positive)
  274. # - 1 to 10 (segmentation_negative)
  275. # -----------------------------
  276. negative_errors = []
  277. # 1 to 9
  278. for region in range(1, 10):
  279. mask = (segmentation_positive == region)
  280. measured_mean = np.mean(measured_negative[mask])
  281. simulated_mean = np.mean(simulated_negative[mask])
  282. error = ((measured_mean - simulated_mean) / simulated_mean)**2 * 100
  283. negative_errors.append(error)
  284. # 1 to 10 (segmentation_negative)
  285. for region in range(1, 11):
  286. mask = (segmentation_negative == region)
  287. measured_mean = np.mean(measured_negative[mask])
  288. simulated_mean = np.mean(simulated_negative[mask])
  289. error = ((measured_mean - simulated_mean) / simulated_mean)**2 * 100
  290. negative_errors.append(error)
  291. avg_negative_error = np.mean(negative_errors)
  292. x_negative_errors[alg].append(avg_negative_error)
  293. # -------------------------------------------
  294. # Plotting
  295. # -------------------------------------------
  296. hatch_patterns = ['x', 'o', '*', '+']
  297. snr_colors = {
  298. 50: '#66c2a5',
  299. 100: '#8da0cb',
  300. 200: '#fc8d62',
  301. 300: '#e78ac3',
  302. }
  303. bar_width = 0.2
  304. snr_index = np.arange(len(snr_levels))
  305. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6), dpi=600)
  306. tick_font_size = 14
  307. # --- Plot χ+ ---
  308. for i, (alg, hatch) in enumerate(zip(algorithms, hatch_patterns)):
  309. for j, snr in enumerate(snr_levels):
  310. x_coord = snr_index[j] + i * bar_width
  311. y_value = x_positive_errors[alg][j]
  312. ax1.bar(
  313. x_coord,
  314. y_value,
  315. bar_width,
  316. hatch=hatch,
  317. edgecolor='black',
  318. color=snr_colors[snr],
  319. )
  320. ax1.set_xlabel('SNR (a.u.)', fontsize=tick_font_size)
  321. ax1.set_ylabel('MSPE of $\\chi^+$ (%)', fontsize=tick_font_size)
  322. ax1.set_xticks(snr_index + bar_width * 1.5)
  323. ax1.set_xticklabels(snr_levels, fontsize=tick_font_size)
  324. ax1.grid(alpha=0.3)
  325. ax1.tick_params(axis='both', which='major', labelsize=tick_font_size)
  326. # --- Plot χ- ---
  327. for i, (alg, hatch) in enumerate(zip(algorithms, hatch_patterns)):
  328. for j, snr in enumerate(snr_levels):
  329. x_coord = snr_index[j] + i * bar_width
  330. y_value = x_negative_errors[alg][j]
  331. ax2.bar(
  332. x_coord,
  333. y_value,
  334. bar_width,
  335. hatch=hatch,
  336. edgecolor='black',
  337. color=snr_colors[snr],
  338. )
  339. ax2.set_xlabel('SNR (a.u.)', fontsize=tick_font_size)
  340. ax2.set_ylabel('MSPE of $\\chi^-$ (%)', fontsize=tick_font_size)
  341. ax2.set_xticks(snr_index + bar_width * 1.5)
  342. ax2.set_xticklabels(snr_levels, fontsize=tick_font_size)
  343. ax2.grid(alpha=0.3)
  344. ax2.tick_params(axis='both', which='major', labelsize=tick_font_size)
  345. # For χ+ axis:
  346. algorithm_patches_pos = []
  347. for alg, hatch in zip(algorithms, hatch_patterns):
  348. patch = Patch(
  349. facecolor='white',
  350. edgecolor='black',
  351. hatch=hatch,
  352. label=alg
  353. )
  354. algorithm_patches_pos.append(patch)
  355. ax1.legend(
  356. handles=algorithm_patches_pos,
  357. fontsize=14,
  358. facecolor='white',
  359. framealpha=1,
  360. loc='upper right',
  361. handlelength=1.5,
  362. handleheight=1.5
  363. )
  364. # For χ- axis:
  365. algorithm_patches_neg = []
  366. for alg, hatch in zip(algorithms, hatch_patterns):
  367. patch = Patch(
  368. facecolor='white',
  369. edgecolor='black',
  370. hatch=hatch,
  371. label=alg
  372. )
  373. algorithm_patches_neg.append(patch)
  374. ax2.legend(
  375. handles=algorithm_patches_neg,
  376. fontsize=14,
  377. facecolor='white',
  378. framealpha=1,
  379. loc='upper right',
  380. handlelength=1.5,
  381. handleheight=1.5
  382. )
  383. plt.tight_layout()
  384. plt.show()
  385. # %% [markdown]
  386. # # Impact of noise on susceptibility anisotorpy
  387. # %%
  388. # Define the algorithms and SNR levels
  389. algorithms = ['x-separation', 'R2*-QSM', 'APART-QSM', 'DECOMPOSE-QSM']
  390. snr_levels = ['SNR 300', 'SNR 200', 'SNR 100', 'SNR 50']
  391. # Initialize a dictionary to store the full paths for measured images
  392. measured_image_paths = {
  393. 'x-separation': {
  394. 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_300' / 'ChiNegMap.nii.gz',
  395. 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_200' / 'ChiNegMap.nii.gz',
  396. 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_100' / 'ChiNegMap.nii.gz',
  397. 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_50' / 'ChiNegMap.nii.gz',
  398. },
  399. 'R2*-QSM': {
  400. 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_300' / 'ChiNegMap.nii.gz',
  401. 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_200' / 'ChiNegMap.nii.gz',
  402. 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_100' / 'ChiNegMap.nii.gz',
  403. 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_50' / 'ChiNegMap.nii.gz',
  404. },
  405. 'APART-QSM': {
  406. 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_300' / 'X_dia_abs.nii.gz',
  407. 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_200' / 'X_dia_abs.nii.gz',
  408. 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_100' / 'X_dia_abs.nii.gz',
  409. 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_50' / 'X_dia_abs.nii.gz',
  410. },
  411. 'DECOMPOSE-QSM': {
  412. 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_300' / 'results_DCS_abs.nii.gz',
  413. 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_200' / 'results_DCS_abs.nii.gz',
  414. 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_100' / 'results_DCS_abs.nii.gz',
  415. 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_50' / 'results_DCS_abs.nii.gz',
  416. },
  417. }
  418. # ----------------------------
  419. # 2) LOAD NIFTI DATA
  420. # ----------------------------
  421. # Load simulated (ground truth) chi, theta, and segmentation images
  422. simulated_nii = nib.load(base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_negative_with_anisotropy.nii.gz')
  423. simulated_img_with_anisotropy = simulated_nii.get_fdata()
  424. theta_nii = nib.load(base_dir / 'osfstorage' / 'Noise' / 'theta.nii.gz')
  425. theta_img = theta_nii.get_fdata()
  426. theta_img[np.isnan(theta_img)] = 0
  427. segmentation_nii = nib.load(base_dir / 'osfstorage' / 'Masks' / 'WM_fibers_seg.nii.gz')
  428. segmentation_img = segmentation_nii.get_fdata()
  429. theta_shape = theta_nii.shape
  430. theta_affine = theta_nii.affine
  431. # Load measured images for each algorithm and SNR
  432. measured_images = {}
  433. for algorithm in algorithms:
  434. measured_images[algorithm] = {}
  435. for snr in snr_levels:
  436. path = measured_image_paths[algorithm][snr]
  437. measured_images[algorithm][snr] = nib.load(path).get_fdata()
  438. # ----------------------------
  439. # 3) GATHER VALID VOXELS & COMPUTE ERRORS
  440. # ----------------------------
  441. all_valid_coords = []
  442. all_theta_values = []
  443. all_sim_values = []
  444. errors = {
  445. algorithm: {
  446. snr: [] for snr in snr_levels
  447. }
  448. for algorithm in algorithms
  449. }
  450. # Loop over the segmentation regions
  451. for region in range(1, 28):
  452. # Region mask
  453. region_mask = (segmentation_img == region)
  454. region_valid_mask = region_mask & (theta_img != 0)
  455. # Get the 3D coordinates of those valid voxels
  456. valid_coords = np.argwhere(region_valid_mask)
  457. if valid_coords.size == 0:
  458. continue
  459. # Extract the actual voxel values
  460. region_theta_vals = theta_img[region_valid_mask]
  461. region_sim_vals = simulated_img_with_anisotropy[region_valid_mask] * -1
  462. # Append these to our global lists
  463. all_valid_coords.append(valid_coords)
  464. all_theta_values.append(region_theta_vals)
  465. all_sim_values.append(region_sim_vals)
  466. # For each algorithm & SNR, calculate MSPE in this region and append
  467. for algorithm in algorithms:
  468. for snr in snr_levels:
  469. measured_vals = measured_images[algorithm][snr][region_valid_mask]
  470. epsilon = 1e-10
  471. relative_squared_error = np.sqrt(
  472. np.square(region_sim_vals - measured_vals) /
  473. (np.square(region_sim_vals) + epsilon)
  474. ) * 100
  475. errors[algorithm][snr].append(relative_squared_error)
  476. all_valid_coords = np.concatenate(all_valid_coords, axis=0)
  477. all_theta_values = np.concatenate(all_theta_values, axis=0)
  478. all_sim_values = np.concatenate(all_sim_values, axis=0)
  479. for algorithm in algorithms:
  480. for snr in snr_levels:
  481. errors[algorithm][snr] = np.concatenate(errors[algorithm][snr], axis=0)
  482. # ----------------------------
  483. # 4) BIN THE THETA VALUES
  484. # ----------------------------
  485. bins = np.arange(0, 100, 10)
  486. bin_indices = np.digitize(all_theta_values, bins)
  487. num_bins = len(bins) - 1
  488. # Calculate the mean bin angle
  489. bin_means_theta = []
  490. for b in range(1, len(bins)):
  491. mask_b = (bin_indices == b)
  492. if np.any(mask_b):
  493. bin_means_theta.append(np.mean(all_theta_values[mask_b]))
  494. bin_means_theta = np.array(bin_means_theta)
  495. # ----------------------------
  496. # 5) CALCULATE BIN MEANS OF MSPE FOR EACH ALGORITHM AND SNR
  497. # ----------------------------
  498. bin_means = {
  499. algorithm: {
  500. snr: [] for snr in snr_levels
  501. } for algorithm in algorithms
  502. }
  503. for b in range(1, len(bins)):
  504. mask_b = (bin_indices == b)
  505. if np.any(mask_b):
  506. for algorithm in algorithms:
  507. for snr in snr_levels:
  508. mspe_values_bin = errors[algorithm][snr][mask_b]
  509. bin_mean_error = np.mean(mspe_values_bin)
  510. bin_means[algorithm][snr].append(bin_mean_error)
  511. else:
  512. for algorithm in algorithms:
  513. for snr in snr_levels:
  514. bin_means[algorithm][snr].append(np.nan)
  515. # Convert bin means to numpy arrays
  516. for algorithm in algorithms:
  517. for snr in snr_levels:
  518. bin_means[algorithm][snr] = np.array(bin_means[algorithm][snr])
  519. # ----------------------------
  520. # 6) SAVE A SINGLE 3D MASK WITH BIN LABELS
  521. # ----------------------------
  522. all_bins_3d = np.zeros(theta_shape, dtype=np.uint8)
  523. for b in range(1, len(bins)):
  524. # Voxels in bin b
  525. mask_b = (bin_indices == b)
  526. # Coordinates of voxels in bin b
  527. bin_coords = all_valid_coords[mask_b]
  528. all_bins_3d[bin_coords[:, 0], bin_coords[:, 1], bin_coords[:, 2]] = b
  529. # ----------------------------
  530. # 7) PLOT THE RESULTS
  531. # ----------------------------
  532. markers = {
  533. 'SNR 300': ('#e78ac3', 'o'),
  534. 'SNR 200': ('#fc8d62', 'o'),
  535. 'SNR 100': ('#8da0cb', 'o'),
  536. 'SNR 50': ('#66c2a5', 'o')
  537. }
  538. fig, axs = plt.subplots(2, 2, figsize=(14, 10))
  539. algorithm_subplot_indices = {
  540. 'x-separation': (0, 0),
  541. 'R2*-QSM': (0, 1),
  542. 'APART-QSM': (1, 0),
  543. 'DECOMPOSE-QSM': (1, 1)
  544. }
  545. for algorithm in algorithms:
  546. ax = axs[algorithm_subplot_indices[algorithm]]
  547. # Dictionary to store percentage changes for each SNR
  548. percentage_changes = {}
  549. for snr in snr_levels:
  550. color, marker = markers[snr]
  551. # x-values = bin_means_theta
  552. x_vals = bin_means_theta
  553. # y-values = bin_means[algorithm][snr]
  554. y_vals = bin_means[algorithm][snr]
  555. # Scatter plot
  556. ax.scatter(
  557. x_vals,
  558. y_vals,
  559. color=color,
  560. marker=marker,
  561. label=snr
  562. )
  563. # 1) Sort the points by x so we can connect them in ascending order
  564. sort_idx = np.argsort(x_vals)
  565. x_sorted = x_vals[sort_idx]
  566. y_sorted = y_vals[sort_idx]
  567. # Calculate percentage change (max vs. min) for this SNR
  568. valid_mspe = y_vals[~np.isnan(y_vals)]
  569. if len(valid_mspe) > 0:
  570. max_mspe = y_vals[0]
  571. min_mspe = y_vals[-1]
  572. if max_mspe != 0:
  573. percentage_change = (max_mspe - min_mspe) / max_mspe * 100
  574. else:
  575. percentage_change = 0
  576. else:
  577. percentage_change = 0
  578. percentage_changes[snr] = percentage_change
  579. # Build the "MEV=" Variation text with Matplotlib colors
  580. x_position = 0.47
  581. y_position = 0.05
  582. ax.text(
  583. x_position, y_position,
  584. "MEV=",
  585. transform=ax.transAxes, fontsize=12, color='black', ha='left', va='top'
  586. )
  587. x_position += 0.1
  588. text_colors = ['#e78ac3', '#fc8d62', '#8da0cb', '#66c2a5']
  589. for idx, snr in enumerate(snr_levels):
  590. color_ = text_colors[idx]
  591. ax.text(
  592. x_position, y_position,
  593. f"{percentage_changes[snr]:.1f}%",
  594. transform=ax.transAxes, fontsize=12, color=color_, ha='left', va='top'
  595. )
  596. x_position += 0.1
  597. # Set titles and labels
  598. if algorithm == 'x-separation':
  599. ax.set_title(r"$\chi$-separation")
  600. else:
  601. ax.set_title(algorithm)
  602. ax.set_xlabel('Angle of Orientation (degrees)', fontsize=16)
  603. ax.set_ylabel('MSPE of $\\chi^-$ (%)', fontsize=16)
  604. ax.grid(True)
  605. ax.legend()
  606. ax.set_ylim(0, 130)
  607. ax.tick_params(axis='both', which='major', labelsize=16)
  608. plt.tight_layout()
  609. plt.show()
  610. # %% [markdown]
  611. # # Comparison between simulated vs in-vivo susceptibility maps
  612. # %%
  613. # Define regions of interest
  614. regions_of_interest = [1, 2, 3, 7, 8, 9]
  615. # Function to extract mean values per region
  616. def extract_mean_per_region(data_map, segmentation_map, regions):
  617. means = []
  618. for region in regions:
  619. region_mask = segmentation_map == region
  620. if np.any(region_mask):
  621. mean_value = np.mean(data_map[region_mask])
  622. means.append(mean_value)
  623. else:
  624. means.append(np.nan)
  625. return means
  626. # ------------------------------------------------------------------------------
  627. # Load segmentation map (for simulated data only)
  628. # ------------------------------------------------------------------------------
  629. segmentation_simulated = nib.load(
  630. base_dir / 'osfstorage' / 'Masks' / 'SegmentedModel.nii.gz'
  631. ).get_fdata()
  632. # ------------------------------------------------------------------------------
  633. # Hard-coded in-vivo mean values (from your table) for each algorithm and region
  634. # ------------------------------------------------------------------------------
  635. # Chi-positive in-vivo data
  636. chi_positive_in_vivo_data = {
  637. 'chi-separation': [0.0658392,0.113222,0.0550474,0.0475523,0.0270498,0.0299241],
  638. 'R2*-QSM': [0.0357698,0.0610981,0.0347473,0.0265003,0.0159944,0.0140221],
  639. 'APART-QSM': [0.0482199,0.0813391,0.0435077,0.0346825,0.0205208,0.02208],
  640. 'DECOMPOSE-QSM': [0.0337807,0.0538997,0.0325998,0.0301356,0.0157292,0.0112171]
  641. }
  642. # Chi-negative in-vivo data (already positive)
  643. chi_negative_in_vivo_data = {
  644. 'chi-separation': [-0.0175603,-0.0332972,-0.0260955,-0.0270215,-0.0303985,-0.0277441],
  645. 'R2*-QSM': [-0.00613275,-0.00185815,-0.00892519,-0.0140371,-0.0187294,-0.0130369],
  646. 'APART-QSM': [-0.0105914,-0.0195933,-0.0182477,-0.0190805,-0.0222699,-2.15E-02],
  647. 'DECOMPOSE-QSM': [-0.00877939,-0.00756195,-0.00899253,-0.00902613,-0.0163517,-0.00915724]
  648. }
  649. # ------------------------------------------------------------------------------
  650. # Initialize dictionaries to store mean values
  651. # ------------------------------------------------------------------------------
  652. chi_positive_in_vivo_means = {}
  653. chi_positive_simulated_means = {}
  654. chi_negative_in_vivo_means = {}
  655. chi_negative_simulated_means = {}
  656. # ------------------------------------------------------------------------------
  657. # File paths for simulated data
  658. # ------------------------------------------------------------------------------
  659. chi_positive_simulated_files = {
  660. 'chi-separation': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiPosMap.nii',
  661. 'R2*-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiPosMap.nii',
  662. 'APART-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_para.nii',
  663. 'DECOMPOSE-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM'/ 'With_anisotropy' / 'Results_PCS.nii'
  664. }
  665. chi_negative_simulated_files = {
  666. 'chi-separation': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiNegMap.nii',
  667. 'R2*-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiNegMap.nii',
  668. 'APART-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_dia_abs.nii',
  669. 'DECOMPOSE-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM'/ 'With_anisotropy' / 'Results_DCS_abs.nii'
  670. }
  671. algorithms = list(chi_positive_simulated_files.keys())
  672. # ------------------------------------------------------------------------------
  673. # Process chi-positive data
  674. # ------------------------------------------------------------------------------
  675. for algo in algorithms:
  676. # In-vivo: use the hard-coded values
  677. in_vivo_means = chi_positive_in_vivo_data[algo]
  678. # Simulated: load from file and extract
  679. simulated_map = nib.load(chi_positive_simulated_files[algo]).get_fdata()
  680. simulated_means = extract_mean_per_region(simulated_map, segmentation_simulated, regions_of_interest)
  681. chi_positive_in_vivo_means[algo] = in_vivo_means
  682. chi_positive_simulated_means[algo] = simulated_means
  683. # ------------------------------------------------------------------------------
  684. # Process chi-negative data
  685. # ------------------------------------------------------------------------------
  686. for algo in algorithms:
  687. # In-vivo: use the hard-coded values
  688. in_vivo_means = chi_negative_in_vivo_data[algo]
  689. # Simulated: load from file, multiply by -1
  690. simulated_map = nib.load(chi_negative_simulated_files[algo]).get_fdata()
  691. simulated_map *= -1
  692. simulated_means = extract_mean_per_region(simulated_map, segmentation_simulated, regions_of_interest)
  693. chi_negative_in_vivo_means[algo] = in_vivo_means
  694. chi_negative_simulated_means[algo] = simulated_means
  695. # ------------------------------------------------------------------------------
  696. # Define display names
  697. # ------------------------------------------------------------------------------
  698. display_names = {
  699. 'chi-separation': r'$\chi$-separation',
  700. 'R2*-QSM': 'R2*-QSM',
  701. 'APART-QSM': 'APART-QSM',
  702. 'DECOMPOSE-QSM': 'DECOMPOSE-QSM'
  703. }
  704. # ------------------------------------------------------------------------------
  705. # Plotting function
  706. # ------------------------------------------------------------------------------
  707. def plot_data(in_vivo_means_dict, simulated_means_dict, chi_type, ax):
  708. marker_styles = ['x', 'o', '*', '+'] # for the 4 algorithms in the order they appear in 'algorithms'
  709. for idx, algo in enumerate(algorithms):
  710. x = np.array(in_vivo_means_dict[algo])
  711. y = np.array(simulated_means_dict[algo])
  712. mask = ~np.isnan(x) & ~np.isnan(y)
  713. x = x[mask]
  714. y = y[mask]
  715. if len(x) > 1:
  716. slope, intercept, r_value, p_value, std_err = linregress(x, y)
  717. r_label = f"(r={r_value:.2f})"
  718. sorted_indices = np.argsort(x)
  719. x_sorted = x[sorted_indices]
  720. y_sorted = intercept + slope * x_sorted
  721. ax.plot(x_sorted, y_sorted, color='black', linestyle='--', label=None)
  722. else:
  723. r_label = "(R²=NaN)"
  724. label = f"{display_names[algo]} {r_label}"
  725. marker = marker_styles[idx]
  726. if marker == 'x':
  727. ax.scatter(x, y, marker=marker, s=80, color='black',
  728. linewidth=1.5, label=label)
  729. elif marker == 'o':
  730. ax.scatter(x, y, marker=marker, s=80,
  731. edgecolors='black', facecolors='none',
  732. linewidth=1.5, label=label)
  733. else:
  734. ax.scatter(x, y, marker=marker, s=80,
  735. color='black', linewidth=1.5, label=label)
  736. x_limits = ax.get_xlim()
  737. y_limits = ax.get_ylim()
  738. min_val = min(x_limits[0], y_limits[0])
  739. max_val = max(x_limits[1], y_limits[1])
  740. ax.set_xlim([min_val, max_val])
  741. ax.set_ylim([min_val, max_val])
  742. ax.plot([min_val, max_val], [min_val, max_val], '-', color='grey', alpha=0.3)
  743. ax.set_xlabel(f'Measured {chi_type} in-vivo (ppm)', fontsize=18)
  744. ax.set_ylabel(f'Simulated {chi_type} (ppm)', fontsize=18)
  745. ax.legend(loc='upper left', fontsize=14)
  746. ax.grid(True)
  747. ax.tick_params(axis='both', which='major', labelsize=14)
  748. # ------------------------------------------------------------------------------
  749. # Create the figure and subplots
  750. # ------------------------------------------------------------------------------
  751. fig, axes = plt.subplots(1, 2, figsize=(20, 6))
  752. plot_data(chi_positive_in_vivo_means, chi_positive_simulated_means, r'$\chi^+$', axes[0])
  753. plot_data(chi_negative_in_vivo_means, chi_negative_simulated_means, r'$\chi^-$', axes[1])
  754. for ax in axes:
  755. ax.tick_params(axis='both', which='major', labelsize=16)
  756. plt.tight_layout()
  757. plt.show()
  758. # %% [markdown]
  759. # # Comparison between simulated vs in-vivo field maps
  760. # %%
  761. # Construct file paths relative to the identified base directory
  762. simulated_map_path = base_dir / "osfstorage/LocalField/Simulated_Field.nii.gz"
  763. segmentation_map_path = base_dir / "osfstorage/Masks/white_matter_mask.nii.gz"
  764. # Load the NIfTI files for simulated local field map and the segmentation map
  765. simulated_map_nii = nib.load(simulated_map_path)
  766. segmentation_map_nii = nib.load(segmentation_map_path)
  767. # Extract the data arrays
  768. simulated_map = simulated_map_nii.get_fdata()
  769. segmentation_map = segmentation_map_nii.get_fdata()
  770. # Extract simulated values for scatter plot (regions 1 to 10 in the segmentation map)
  771. simulated_values = []
  772. for region in range(1, 11):
  773. region_mask = segmentation_map == region
  774. simulated_region_values = simulated_map[region_mask]
  775. simulated_values.append(np.mean(simulated_region_values))
  776. # Hard-coded in-vivo values (for the same 10 regions)
  777. in_vivo_values = np.array([
  778. 1.74348,
  779. -1.72861,
  780. -1.59869,
  781. 1.46938,
  782. -2.61981,
  783. -0.557609,
  784. -0.714494,
  785. -1.31104,
  786. -2.75768,
  787. 0.288124
  788. ])
  789. # Scatter plot with linear regression
  790. slope, intercept, r_value, p_value, std_err = stats.linregress(in_vivo_values, simulated_values)
  791. regression_line = slope * in_vivo_values + intercept
  792. # Create the scatter plot with a smaller figure size
  793. fig, ax = plt.subplots(figsize=(6, 6))
  794. # Sort the x-values (in-vivo local field values) and calculate corresponding y-values
  795. sorted_indices = np.argsort(in_vivo_values)
  796. sorted_in_vivo_values = in_vivo_values[sorted_indices]
  797. sorted_regression_line = slope * sorted_in_vivo_values + intercept
  798. # Scatter plot
  799. ax.scatter(in_vivo_values, simulated_values, color='black', zorder=2)
  800. # Plot the regression line
  801. ax.plot(sorted_in_vivo_values, sorted_regression_line, linestyle='--',
  802. color='black', linewidth=1.2, zorder=1)
  803. # Add labels, grid, and R² text
  804. ax.set_xlabel('In-vivo local field (Hz)', fontsize=10)
  805. ax.set_ylabel('Simulated local field (Hz)', fontsize=10)
  806. ax.grid(True, alpha=0.3)
  807. ax.text(0.05, 0.95,
  808. f'Correlation = {r_value:.2f}\nSlope = {slope:.1f}',
  809. ha='left', va='top', transform=ax.transAxes, fontsize=14)
  810. plt.tight_layout()
  811. plt.show()
  812. # %% [markdown]
  813. # # Comparison between simulated vs in-vivo T2 maps
  814. # %%
  815. # File paths for simulated T2 map and segmentation map
  816. simulated_t2_path = base_dir / "osfstorage/T2/T2_simulated_resampled.nii.gz"
  817. simulated_segmentation_path = base_dir / "osfstorage/Masks/SegmentedModel_resampled.nii.gz"
  818. # Load NIfTI files for simulated data
  819. simulated_t2_map = nib.load(simulated_t2_path).get_fdata()
  820. simulated_segmentation = nib.load(simulated_segmentation_path).get_fdata()
  821. # Replace NaN and Inf values with 0 in the simulated T2 map
  822. simulated_t2_map = np.nan_to_num(simulated_t2_map, nan=0, posinf=0, neginf=0)
  823. # Hard-coded in-vivo T2 values for regions 1-3 and 7-9 (6 regions)
  824. T2_in_vivo = np.array([67.2624, 44.9122, 52.7201, 59.789, 54.5211, 104.057])
  825. # Extract simulated T2 values for regions 1-3 and 7-9
  826. T2_simulated = []
  827. for region in list(range(1, 4)) + list(range(7, 10)):
  828. simulated_mask = simulated_segmentation == region
  829. if np.any(simulated_mask):
  830. simulated_mean = np.mean(simulated_t2_map[simulated_mask])
  831. T2_simulated.append(simulated_mean)
  832. T2_simulated = np.array(T2_simulated)
  833. # Reshape data for linear regression
  834. T2_in_vivo_reshaped = T2_in_vivo.reshape(-1, 1)
  835. T2_simulated_reshaped = T2_simulated.reshape(-1, 1)
  836. # Perform linear regression
  837. reg_model = LinearRegression()
  838. reg_model.fit(T2_in_vivo_reshaped, T2_simulated_reshaped)
  839. slope = reg_model.coef_[0][0]
  840. intercept = reg_model.intercept_[0]
  841. # Generate regression line values
  842. x_fit = np.linspace(T2_in_vivo.min(), T2_in_vivo.max(), 100)
  843. y_fit = slope * x_fit + intercept
  844. # Plotting
  845. plt.figure(figsize=(8, 6))
  846. plt.scatter(T2_in_vivo, T2_simulated, color='black', label='Data Points')
  847. plt.plot(x_fit, y_fit, '--', color='black', label='Regression Line')
  848. plt.xlabel(r'$T_2^{\mathrm{in-vivo}}$ (ms)', fontsize=16)
  849. plt.ylabel(r'$T_2^{\mathrm{simulated}}$ (ms)', fontsize=16)
  850. correlation = np.corrcoef(T2_in_vivo, T2_simulated)[0, 1]
  851. plt.text(45, 120, f'Correlation: {correlation:.2f}\nSlope: {slope:.2f}', fontsize=18)
  852. plt.grid(True)
  853. plt.xticks(fontsize=14)
  854. plt.yticks(fontsize=14)
  855. plt.tight_layout()
  856. plt.show()
  857. # %% [markdown]
  858. # # Supplimantary Material
  859. # %%
  860. num_algorithms = 4
  861. algorithm_names = [
  862. '$\\chi$-separation',
  863. 'R2*-QSM',
  864. 'APART-QSM',
  865. 'DECOMPOSE-QSM'
  866. ]
  867. segmentation2_path = base_dir / 'osfstorage' / 'Masks' / 'white_matter_mask.nii.gz'
  868. segmentation2 = nib.load(segmentation2_path).get_fdata()
  869. region_labels = list(range(1, 10))
  870. # Load simulated maps
  871. simulated_with_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
  872. simulated_without_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
  873. simulated_with = nib.load(simulated_with_path).get_fdata()
  874. simulated_without = nib.load(simulated_without_path).get_fdata()
  875. # Define measured maps for each algorithm
  876. measured_maps = {
  877. 0: { # Algorithm 1: χ-separation
  878. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiPosMap.nii',
  879. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'Without_anisotropy' / 'ChiPosMap.nii',
  880. },
  881. 1: { # Algorithm 2: R2*-QSM
  882. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiPosMap.nii',
  883. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'Without_anisotropy' / 'ChiPosMap.nii',
  884. },
  885. 2: { # Algorithm 3: APART-QSM
  886. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_para.nii',
  887. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'Without_anisotropy' / 'X_para.nii',
  888. },
  889. 3: { # Algorithm 4: DECOMPOSE-QSM
  890. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_PCS.nii',
  891. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'Without_anisotropy' / 'results_PCS.nii',
  892. },
  893. }
  894. fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12), dpi=300)
  895. for idx in range(num_algorithms):
  896. ax = axes[idx // 2, idx % 2]
  897. algorithm_name = algorithm_names[idx]
  898. # Load the measured maps for the current algorithm
  899. measured_with = nib.load(measured_maps[idx]['x_minus_with_anisotropy']).get_fdata()
  900. measured_without = nib.load(measured_maps[idx]['x_minus_without_anisotropy']).get_fdata()
  901. errors_with_regions = []
  902. errors_without_regions = []
  903. # --------------------------------------------------
  904. # 1) Compute errors
  905. # --------------------------------------------------
  906. for region in region_labels:
  907. region_mask = (segmentation2 == region)
  908. if region == 8:
  909. continue
  910. if np.any(region_mask):
  911. # "With Anisotropy"
  912. sim_val_with = np.mean(simulated_with[region_mask])
  913. if sim_val_with < 0:
  914. sim_val_with *= -1
  915. meas_val_with = np.mean(measured_with[region_mask])
  916. if sim_val_with != 0:
  917. error_with = ((meas_val_with - sim_val_with) / sim_val_with) ** 2 * 100
  918. else:
  919. error_with = np.nan
  920. # "Without Anisotropy"
  921. sim_val_without = np.mean(simulated_without[region_mask])
  922. meas_val_without = np.mean(measured_without[region_mask])
  923. if sim_val_without != 0:
  924. error_without = ((meas_val_without - sim_val_without) / sim_val_without) ** 2 * 100
  925. else:
  926. error_without = np.nan
  927. errors_with_regions.append(error_with)
  928. errors_without_regions.append(error_without)
  929. # Convert to numpy arrays and remove NaNs
  930. errors_with_regions = np.array(errors_with_regions)
  931. errors_without_regions = np.array(errors_without_regions)
  932. errors_with_regions = errors_with_regions[~np.isnan(errors_with_regions)]
  933. errors_without_regions = errors_without_regions[~np.isnan(errors_without_regions)]
  934. # Prepare for KDE
  935. x_vals = np.linspace(0, 30000, 200)
  936. max_height = 0.4
  937. # Plot KDE for "With Anisotropy"
  938. if errors_with_regions.size > 1:
  939. kde_with = gaussian_kde(errors_with_regions)
  940. density_with = kde_with(x_vals)
  941. scaling_with = max_height / np.max(density_with) if np.max(density_with) > 0 else 1
  942. density_with_scaled = density_with * scaling_with
  943. ax.fill_between(x_vals, 0, density_with_scaled, alpha=0.4, color='tab:blue')
  944. ax.plot(x_vals, density_with_scaled, alpha=0.7, color='tab:blue')
  945. mean_mspe_with = np.mean(errors_with_regions)
  946. elif errors_with_regions.size == 1:
  947. mean_mspe_with = errors_with_regions[0]
  948. ax.plot([mean_mspe_with], [max_height/2], marker='o', color='tab:blue')
  949. else:
  950. mean_mspe_with = None
  951. # Plot KDE for "Without Anisotropy"
  952. if errors_without_regions.size > 1:
  953. kde_without = gaussian_kde(errors_without_regions)
  954. density_without = kde_without(x_vals)
  955. scaling_without = max_height / np.max(density_without) if np.max(density_without) > 0 else 1
  956. density_without_scaled = density_without * scaling_without
  957. ax.fill_between(x_vals, 0, density_without_scaled, alpha=0.4, color='tab:orange')
  958. ax.plot(x_vals, density_without_scaled, alpha=0.7, color='tab:orange')
  959. mean_mspe_without = np.mean(errors_without_regions)
  960. elif errors_without_regions.size == 1:
  961. mean_mspe_without = errors_without_regions[0]
  962. ax.plot([mean_mspe_without], [max_height/2], marker='o', color='tab:orange')
  963. else:
  964. mean_mspe_without = None
  965. # Show mean MSPE for with/without anisotropy
  966. if mean_mspe_with is not None:
  967. ax.text(0.95, 0.8, f"Mean MSPE: {mean_mspe_with:.1f}%", transform=ax.transAxes,
  968. color='tab:blue', ha='right', va='center', fontsize=12)
  969. if mean_mspe_without is not None:
  970. ax.text(0.95, 0.7, f"Mean MSPE: {mean_mspe_without:.1f}%", transform=ax.transAxes,
  971. color='tab:orange', ha='right', va='center', fontsize=12)
  972. legend_elements = [
  973. Line2D([0], [0], color='tab:blue', lw=2, label='With Anisotropy'),
  974. Line2D([0], [0], color='tab:orange', lw=2, label='Without Anisotropy')
  975. ]
  976. ax.legend(handles=legend_elements, loc='upper right')
  977. ax.set_title(algorithm_name, fontsize=14)
  978. ax.set_xlabel("MSPE of $\\chi^+$ (%)", fontsize=12)
  979. ax.set_ylabel("Density (a.u.)", fontsize=12)
  980. ax.set_ylim(0, max_height * 1.2)
  981. ax.grid(alpha=0.3)
  982. # ---------------------------
  983. # Final figure adjustments
  984. # ---------------------------
  985. plt.tight_layout(rect=[0, 0, 1, 0.96])
  986. plt.show()
  987. # %%
  988. num_algorithms = 4
  989. algorithm_names = [
  990. '$\\chi$-separation',
  991. 'R2*-QSM',
  992. 'APART-QSM',
  993. 'DECOMPOSE-QSM'
  994. ]
  995. segmentation2_path = base_dir / 'osfstorage' / 'Masks' / 'SegmentedModel.nii.gz'
  996. segmentation2 = nib.load(segmentation2_path).get_fdata()
  997. region_labels = list(range(1, 10))
  998. # Load simulated maps
  999. simulated_with_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
  1000. simulated_without_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
  1001. simulated_with = nib.load(simulated_with_path).get_fdata()
  1002. simulated_without = nib.load(simulated_without_path).get_fdata()
  1003. # Define measured maps for each algorithm
  1004. measured_maps = {
  1005. 0: { # Algorithm 1: χ-separation
  1006. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiPosMap.nii',
  1007. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'Without_anisotropy' / 'ChiPosMap.nii',
  1008. },
  1009. 1: { # Algorithm 2: R2*-QSM
  1010. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiPosMap.nii',
  1011. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'Without_anisotropy' / 'ChiPosMap.nii',
  1012. },
  1013. 2: { # Algorithm 3: APART-QSM
  1014. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_para.nii',
  1015. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'Without_anisotropy' / 'X_para.nii',
  1016. },
  1017. 3: { # Algorithm 4: DECOMPOSE-QSM
  1018. 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_PCS.nii',
  1019. 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'Without_anisotropy' / 'results_PCS.nii',
  1020. },
  1021. }
  1022. fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12), dpi=300)
  1023. for idx in range(num_algorithms):
  1024. ax = axes[idx // 2, idx % 2]
  1025. algorithm_name = algorithm_names[idx]
  1026. # Load the measured maps for the current algorithm
  1027. measured_with = nib.load(measured_maps[idx]['x_minus_with_anisotropy']).get_fdata()
  1028. measured_without = nib.load(measured_maps[idx]['x_minus_without_anisotropy']).get_fdata()
  1029. errors_with_regions = []
  1030. errors_without_regions = []
  1031. # --------------------------------------------------
  1032. # 1) Compute errors
  1033. # --------------------------------------------------
  1034. for region in region_labels:
  1035. region_mask = (segmentation2 == region)
  1036. if region == 8:
  1037. continue
  1038. if np.any(region_mask):
  1039. # "With Anisotropy"
  1040. sim_val_with = np.mean(simulated_with[region_mask])
  1041. if sim_val_with < 0:
  1042. sim_val_with *= -1
  1043. meas_val_with = np.mean(measured_with[region_mask])
  1044. if sim_val_with != 0:
  1045. error_with = ((meas_val_with - sim_val_with) / sim_val_with) ** 2 * 100
  1046. else:
  1047. error_with = np.nan
  1048. # "Without Anisotropy"
  1049. sim_val_without = np.mean(simulated_without[region_mask])
  1050. meas_val_without = np.mean(measured_without[region_mask])
  1051. if sim_val_without != 0:
  1052. error_without = ((meas_val_without - sim_val_without) / sim_val_without) ** 2 * 100
  1053. else:
  1054. error_without = np.nan
  1055. errors_with_regions.append(error_with)
  1056. errors_without_regions.append(error_without)
  1057. # Convert to numpy arrays and remove NaNs
  1058. errors_with_regions = np.array(errors_with_regions)
  1059. errors_without_regions = np.array(errors_without_regions)
  1060. errors_with_regions = errors_with_regions[~np.isnan(errors_with_regions)]
  1061. errors_without_regions = errors_without_regions[~np.isnan(errors_without_regions)]
  1062. # Prepare for KDE
  1063. x_vals = np.linspace(0, 70, 70)
  1064. max_height = 0.4
  1065. # Plot KDE for "With Anisotropy"
  1066. if errors_with_regions.size > 1:
  1067. kde_with = gaussian_kde(errors_with_regions)
  1068. density_with = kde_with(x_vals)
  1069. scaling_with = max_height / np.max(density_with) if np.max(density_with) > 0 else 1
  1070. density_with_scaled = density_with * scaling_with
  1071. ax.fill_between(x_vals, 0, density_with_scaled, alpha=0.4, color='tab:blue')
  1072. ax.plot(x_vals, density_with_scaled, alpha=0.7, color='tab:blue')
  1073. mean_mspe_with = np.mean(errors_with_regions)
  1074. elif errors_with_regions.size == 1:
  1075. mean_mspe_with = errors_with_regions[0]
  1076. ax.plot([mean_mspe_with], [max_height/2], marker='o', color='tab:blue')
  1077. else:
  1078. mean_mspe_with = None
  1079. # Plot KDE for "Without Anisotropy"
  1080. if errors_without_regions.size > 1:
  1081. kde_without = gaussian_kde(errors_without_regions)
  1082. density_without = kde_without(x_vals)
  1083. scaling_without = max_height / np.max(density_without) if np.max(density_without) > 0 else 1
  1084. density_without_scaled = density_without * scaling_without
  1085. ax.fill_between(x_vals, 0, density_without_scaled, alpha=0.4, color='tab:orange')
  1086. ax.plot(x_vals, density_without_scaled, alpha=0.7, color='tab:orange')
  1087. mean_mspe_without = np.mean(errors_without_regions)
  1088. elif errors_without_regions.size == 1:
  1089. mean_mspe_without = errors_without_regions[0]
  1090. ax.plot([mean_mspe_without], [max_height/2], marker='o', color='tab:orange')
  1091. else:
  1092. mean_mspe_without = None
  1093. # Show mean MSPE for with/without anisotropy
  1094. if mean_mspe_with is not None:
  1095. ax.text(0.95, 0.8, f"Mean MSPE: {mean_mspe_with:.1f}%", transform=ax.transAxes,
  1096. color='tab:blue', ha='right', va='center', fontsize=12)
  1097. if mean_mspe_without is not None:
  1098. ax.text(0.95, 0.7, f"Mean MSPE: {mean_mspe_without:.1f}%", transform=ax.transAxes,
  1099. color='tab:orange', ha='right', va='center', fontsize=12)
  1100. legend_elements = [
  1101. Line2D([0], [0], color='tab:blue', lw=2, label='With Anisotropy'),
  1102. Line2D([0], [0], color='tab:orange', lw=2, label='Without Anisotropy')
  1103. ]
  1104. ax.legend(handles=legend_elements, loc='upper right')
  1105. ax.set_title(algorithm_name, fontsize=14)
  1106. ax.set_xlabel("MSPE of $\\chi^+$ (%)", fontsize=12)
  1107. ax.set_ylabel("Density (a.u.)", fontsize=12)
  1108. ax.set_ylim(0, max_height * 1.2)
  1109. ax.grid(alpha=0.3)
  1110. # ---------------------------
  1111. # Final figure adjustments
  1112. # ---------------------------
  1113. plt.tight_layout(rect=[0, 0, 1, 0.96])
  1114. plt.show()
  1115. # %%
  1116. methods = {
  1117. "χ-separation": (
  1118. base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiNegMap.nii',
  1119. base_dir / 'osfstorage' / '7T' / 'X-separation' / 'ChiNegMap.nii'
  1120. ),
  1121. "R2*-QSM": (
  1122. base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiNegMap.nii',
  1123. base_dir / 'osfstorage' / '7T' / 'R2star-QSM' / 'ChiNegMap.nii'
  1124. ),
  1125. "APART-QSM": (
  1126. base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_dia_abs.nii',
  1127. base_dir / 'osfstorage' / '7T' / 'APART-QSM' / 'X_dia_abs.nii'
  1128. ),
  1129. "DECOMPOSE-QSM": (
  1130. base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_DCS_abs.nii',
  1131. base_dir / 'osfstorage' / '7T' / 'DECOMPOSE-QSM' / 'Results_DCS_abs.nii'
  1132. ),
  1133. }
  1134. #-----------------------------------------------------------------
  1135. # Load segmentation file
  1136. #-----------------------------------------------------------------
  1137. segmentation_file = base_dir / 'osfstorage' / 'Masks' / 'white_matter_mask.nii.gz'
  1138. seg_nii = nib.load(segmentation_file)
  1139. seg_data = seg_nii.get_fdata()
  1140. labels = range(1, 12)
  1141. method_points = {}
  1142. all_mean_values = []
  1143. all_difference_values = []
  1144. for method_name, (nii_3T_path, nii_7T_path) in methods.items():
  1145. # --------------------
  1146. # Load the 3T and 7T
  1147. # --------------------
  1148. nii_3T = nib.load(nii_3T_path)
  1149. nii_7T = nib.load(nii_7T_path)
  1150. data_3T = nii_3T.get_fdata()*-1
  1151. data_7T = nii_7T.get_fdata()*-1
  1152. mean_values = np.zeros(len(labels))
  1153. diff_values = np.zeros(len(labels))
  1154. for i, label_val in enumerate(labels):
  1155. # Mask for current region
  1156. mask = (seg_data == label_val)
  1157. # Extract the region's data for 3T and 7T
  1158. region_3T = data_3T[mask]
  1159. region_7T = data_7T[mask]
  1160. # Compute the mean within that region
  1161. mean_3T = np.mean(region_3T) if region_3T.size > 0 else np.nan
  1162. mean_7T = np.mean(region_7T) if region_7T.size > 0 else np.nan
  1163. # Bland-Altman points
  1164. mean_values[i] = (mean_3T + mean_7T) / 2
  1165. diff_values[i] = (mean_3T - mean_7T)
  1166. # Store in dictionary
  1167. method_points[method_name] = (mean_values, diff_values)
  1168. # Accumulate for global axis limits
  1169. all_mean_values.extend(mean_values)
  1170. all_difference_values.extend(diff_values)
  1171. # Convert to NumPy for min/max
  1172. all_mean_values = np.array(all_mean_values)
  1173. all_difference_values = np.array(all_difference_values)
  1174. # Determine global x-limits and y-limits
  1175. x_min, x_max = np.min(all_mean_values), np.max(all_mean_values)
  1176. x_margin = 0.1 * (x_max - x_min) # 10% margin
  1177. x_lim = (x_min - x_margin, x_max + x_margin)
  1178. # Fixed y-limits as in your code
  1179. y_lim = (-0.05, 0.05)
  1180. # Create subplots
  1181. fig, axes = plt.subplots(2, 2, figsize=(10, 8))
  1182. axes = axes.flatten()
  1183. for ax, (method_name, (mean_values, difference_values)) in zip(axes, method_points.items()):
  1184. # Mean & LoA
  1185. mean_diff = np.mean(difference_values)
  1186. std_diff = np.std(difference_values, ddof=1)
  1187. loa_upper = mean_diff + 1.96 * std_diff
  1188. loa_lower = mean_diff - 1.96 * std_diff
  1189. ax.scatter(mean_values, difference_values, marker="x", s=50)
  1190. # Plot lines
  1191. ax.axhline(mean_diff, color="red", linestyle="--")
  1192. ax.axhline(loa_upper, color="green", linestyle="--")
  1193. ax.axhline(loa_lower, color="green", linestyle="--")
  1194. x_text = x_lim[0] + 0.05*(x_lim[1] - x_lim[0])
  1195. y_offset = 0.005 * (y_lim[1] - y_lim[0])
  1196. # (3) For each line, add text "above" the line value
  1197. def place_text_above_line(line_value, label_color):
  1198. y_text = line_value
  1199. # Clamp y_text so it doesn't exceed the top/bottom
  1200. y_text = max(min(y_text, y_lim[1] - 0.001), y_lim[0] + 0.0)
  1201. ax.text(x_text, y_text, f"{line_value:.4f}", color=label_color,
  1202. va="bottom", ha="left",)
  1203. place_text_above_line(mean_diff, "red") # Mean difference
  1204. place_text_above_line(loa_upper, "green") # +1.96 SD
  1205. place_text_above_line(loa_lower, "green") # -1.96 SD
  1206. # Labels, limits, etc.
  1207. ax.set_xlabel(r"Mean of $\chi^{-}_{\text{3 T}}$ and $\chi^{-}_{\text{7 T}} \:(ppm)$")
  1208. ax.set_ylabel(r"Difference $\chi^{-}_{\text{3 T}} - \chi^{-}_{\text{7 T}} \:(ppm)$")
  1209. ax.set_title(method_name)
  1210. ax.set_xlim(x_lim)
  1211. ax.set_ylim(y_lim)
  1212. plt.tight_layout()
  1213. plt.show()

Manuscript_Figures.ipynb at commit d009ab6, under MIT · at the source

Overview

Authors: Daniel Ridani1, Benjamin De Leener1,2,3, Eva Alonso‐Ortiz1,2,4
  1. NeuroPoly Lab, Institute of Biomedical Engineering, Polytechnique Montreal, Montreal, Quebec, Canada
  2. CHU Sainte‐Justine Research Center, Montreal, Quebec, Canada
  3. Department of Computer Engineering and Software Engineering, Polytechnique Montreal, Montreal, Quebec, Canada
  4. Department of Electrical Engineering, Polytechnique Montreal, Montreal, Quebec, Canada
Journal: Magnetic resonance in medicine, volume 96, issue 5, pages 2412-2425
Dates: received 31 December 2025; accepted 1 June 2026; published online 9 July 2026; in print November 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/mrm.70468 · PMID 42426959 · PMCID PMC13527262 · OpenAlex W7167935606
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), computational (subfield)
Methods: Connectivity, Statistics, fMRI & imaging
Keywords: brain phantom, MRI simulations, susceptibility anisotropy, susceptibility separation
MeSH: Brain*, Image Processing, Computer-Assisted*, Magnetic Resonance Imaging*, Phantoms, Imaging*, Algorithms, Anisotropy, Computer Simulation, Humans, Reproducibility of Results, Signal-To-Noise Ratio (* major topic)
Topic: Advanced MRI Techniques and Applications (Radiology, Nuclear Medicine and Imaging, Medicine), according to OpenAlex
Funding: Canadian Network for Research and Innovation in Machining Technology, Natural Sciences and Engineering Research Council of Canada (3101671); Fonds de Recherche du Québec; Polytechnique Montréal; Canada First Research Excellence Fund; Polytechnique Montreal; TransMedTech Institute
Citations: not cited yet (Europe PMC); 57 references in the paper

Abstract

Purpose: To create a realistic in silico brain phantom for positive and negative magnetic susceptibility that incorporates susceptibility anisotropy, enabling the evaluation of how susceptibility anisotropy influences susceptibility separation algorithm performance.

Methods: We expanded an existing QSM validation phantom by creating separate maps for positive and negative susceptibility, with the option of modeling susceptibility anisotropy. Multi‐echo gradient echo data were simulated to evaluate four susceptibility separation techniques (χ‐separation, DECOMPOSE‐QSM, APART‐QSM, and R2*‐QSM). To assess the impact of noise, simulations were performed at different SNR levels (50, 100, 200, 300).

Results: Our findings showed that the error in negative susceptibility estimates increased by up to 53% when susceptibility anisotropy was present, compared to the case without susceptibility anisotropy, with χ‐separation being the algorithm that was most sensitive to anisotropy. Robustness to noise varied across the assessed algorithms, with APART‐QSM and χ‐separation having the highest and lowest sensitivity to noise, respectively.

Conclusion: The modified phantom is open‐source and can serve as a numerical ground truth for evaluating susceptibility separation methods. Our findings emphasize the importance of incorporating susceptibility anisotropy into susceptibility separation models to improve their accuracy.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 5 matches between paragraphs and lines of code.

neuropoly/Susceptibility-Separation-Phantom

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d009ab67660fd6e5368d9f9811e04e19eca1636a, 17 April 2026
Languages: MATLAB (14), Jupyter (1)
Size: 20 files, 15 scripts
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, license file, 1 notebook
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Tools for NIfTI and ANALYZE image (MATLAB) (11 files), Image Processing Toolbox (1 file), Parallel Computing Toolbox (1 file), Matplotlib (1 file), NiBabel (1 file), NumPy (1 file), scikit-learn (1 file), SciPy (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
17 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 15 scripts, each with its path and the digest of its content;
  • 5 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data Availability Statement

The code used to create susceptibility and relaxation maps, and simulate GRE signals is available at: https://github.com/neuropoly/Susceptibility‐Separation‐Phantom (https://github.com/neuropoly/Susceptibility-Separation-Phantom), while the data used to generate the figures described in this paper can be found here: https://osf.io/9xwhz/.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Publisher: n/a → Wiley

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 3 authors, 4 keywords, 10 MeSH terms, 6 funders, 55 references.

Cite

This paper

Ridani, D., De Leener, B., & Alonso‐Ortiz, E. (2026). A Realistic In Silico Brain Phantom for Quantifying Susceptibility Anisotropy-Induced Error in Susceptibility Separation. Magnetic resonance in medicine, 96(5), 2412-2425. https://doi.org/10.1002/mrm.70468

BibTeX

@article{ridani2026realistic,
author = {Ridani, Daniel and De Leener, Benjamin and Alonso‐Ortiz, Eva},
title = {{A Realistic In Silico Brain Phantom for Quantifying Susceptibility Anisotropy-Induced Error in Susceptibility Separation}},
journal = {Magnetic resonance in medicine},
year = {2026},
month = jul,
volume = {96},
number = {5},
pages = {2412--2425},
publisher = {Wiley},
issn = {0740-3194},
doi = {10.1002/mrm.70468},
url = {https://doi.org/10.1002/mrm.70468},
pmid = {42426959},
pmcid = {PMC13527262}
}

RIS

TY - JOUR
AU - Ridani, Daniel
AU - De Leener, Benjamin
AU - Alonso‐Ortiz, Eva
TI - A Realistic In Silico Brain Phantom for Quantifying Susceptibility Anisotropy-Induced Error in Susceptibility Separation
T2 - Magnetic resonance in medicine
J2 - Magn Reson Med
PY - 2026
DA - 2026/07/09
VL - 96
IS - 5
SP - 2412
EP - 2425
SN - 0740-3194
PB - Wiley
DO - 10.1002/mrm.70468
UR - https://doi.org/10.1002/mrm.70468
LA - en
ER -

CSL-JSON

{
"id": "10.1002/mrm.70468",
"type": "article-journal",
"title": "A Realistic In Silico Brain Phantom for Quantifying Susceptibility Anisotropy-Induced Error in Susceptibility Separation",
"container-title": "Magnetic resonance in medicine",
"author": [
{
"family": "Ridani",
"given": "Daniel"
},
{
"family": "De Leener",
"given": "Benjamin"
},
{
"family": "Alonso‐Ortiz",
"given": "Eva"
}
],
"container-title-short": "Magn Reson Med",
"volume": "96",
"issue": "5",
"page": "2412-2425",
"DOI": "10.1002/mrm.70468",
"PMID": "42426959",
"PMCID": "PMC13527262",
"ISSN": "0740-3194",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/mrm.70468",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
9
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1002/nbm.70349 [code]
Susceptibility Source Separation Unveils Paramagnetic and Diamagnetic Trajectories in Healthy Brains From 5 to 90 Years.
Journal: NMR in biomedicine
In common: Tools for NIfTI and ANALYZE image (MATLAB), SciPy, Matplotlib, 1 other tool, structural MRI / diffusion, 9 references
[2] doi:10.1002/nbm.70301 [code]
Reducing Variability in Deep Gray Matter QSM Using Differential ROI Referencing: A Phantom and In Vivo Evaluation.
Journal: NMR in biomedicine
In common: structural MRI / diffusion, 7 references
[3] doi:10.1002/mrm.70336 [code]
Offline Reconstruction of Diffusion MRI Acquisitions for Comparison Between Complex PCA-Based and AI-Based Denoising.
Journal: Magnetic resonance in medicine
In common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 4 other tools, structural MRI / diffusion, 1 reference
[4] doi:10.1038/s41586-026-10631-3 [code]
A prognostic human brain network for diffuse midline glioma.
Journal: Nature
In common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 5 other tools, 1 reference
[5] doi:10.1002/nbm.70277 [code]
Hierarchical Bayesian Modelling Improves Microstructural Parameter Mapping in Diffusion and Exchange MRI Data.
Journal: NMR in biomedicine
In common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, NiBabel, 3 other tools, computational, structural MRI / diffusion, 1 reference
[6] doi:10.1016/j.neuron.2026.04.011 [code]
Precision fMRI reveals densely interdigitated network patches with conserved motifs in the lateral prefrontal cortex.
Journal: Neuron
In common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 5 other tools
[7] doi:10.1002/hbm.70483 [code]
Untamed: Unconstrained Tensor Decomposition and Graph Node Embedding for Cortical Parcellation.
Journal: Human brain mapping
In common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 5 other tools
[8] doi:10.1002/mrm.70405 [code]
DeepRelaxo: Fast Mono-Exponential Magnitude Brain R2* Mapping With Reduced Echoes Using Self-Supervised Deep Learning.
Journal: Magnetic resonance in medicine
In common: NiBabel, SciPy, Matplotlib, 1 other tool, computational, 3 references
[9] doi:10.1038/s41467-026-74215-5 [code]
Multi-metric evaluations of acute psychedelic effects on fMRI brain entropy.
Journal: Nature communications
In common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 4 other tools, computational
[10] doi:10.1002/hbm.70602 [code]
Neuroimaging Correlates of Post-Stroke Pain After Ischemic Stroke: Secondary Analysis of the INSPiRE-TMS Trial.
Journal: Human brain mapping
In common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 4 other tools

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.