A Realistic In Silico Brain Phantom for Quantifying Susceptibility Anisotropy-Induced Error in Susceptibility Separation.
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] § 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] § 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] § Methods › Phantom Creation › Susceptibility Maps ↔ PhantomCreation.m, lines 71–187 · score 0.56 · standard deviation, Gaussian noise, weighted, phantom, map, susceptibility
- [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] § 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
- # %% [markdown]
- # # Install dependencies
- # %%
- import os
- import numpy as np
- import nibabel as nib
- import matplotlib.pyplot as plt
- from scipy.stats import linregress
- from pathlib import Path
- from scipy import stats
- from sklearn.linear_model import LinearRegression
- from scipy.stats import gaussian_kde
- from matplotlib.lines import Line2D
- from matplotlib.patches import Patch
- from pathlib import Path
- # %% [markdown]
- # # Download the data
- # %%
- base_dir = base_dir = Path.cwd()
- # Verify that the parent of the entered path exists
- if not base_dir.parent.exists():
- raise FileNotFoundError(f"The specified path {base_dir.parent} does not exist.")
- else:
- # Create the last folder if it doesn't exist
- base_dir.mkdir(parents=True, exist_ok=True)
- print(f"Using data directory: {base_dir}")
- !pip install osfclient
- # Clone the OSF project into the specified directory
- !osf -p 9xwhz clone "{base_dir}"
- # %% [markdown]
- # # Impact of susceptibility anisotropy
- # %%
- # ---------------------------
- # Parameters and File Paths
- # ---------------------------
- num_algorithms = 4
- algorithm_names = [
- '$\\chi$-separation',
- 'R2*-QSM',
- 'APART-QSM',
- 'DECOMPOSE-QSM'
- ]
- # Load segmentation mask (assumed to contain region labels 1 to 11)
- segmentation2_path = base_dir / 'osfstorage' / 'Masks' / 'white_matter_mask.nii.gz'
- segmentation2 = nib.load(segmentation2_path).get_fdata()
- region_labels = list(range(1, 12)) # regions 1 to 11
- # Load simulated maps (common for all algorithms)
- simulated_with_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_negative_with_anisotropy.nii.gz'
- simulated_without_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_negative.nii.gz'
- simulated_with = nib.load(simulated_with_path).get_fdata()
- simulated_without = nib.load(simulated_without_path).get_fdata()
- # Define measured maps for each algorithm
- measured_maps = {
- 0: { # Algorithm 1: χ-separation
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiNegMap.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'Without_anisotropy' / 'ChiNegMap.nii',
- },
- 1: { # Algorithm 2: R2*-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiNegMap.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'Without_anisotropy' / 'ChiNegMap.nii',
- },
- 2: { # Algorithm 3: APART-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_dia_abs.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'Without_anisotropy' / 'X_dia_abs.nii',
- },
- 3: { # Algorithm 4: DECOMPOSE-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_DCS_abs.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'Without_anisotropy' / 'results_DCS_abs.nii',
- },
- }
- fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12), dpi=300)
- t_test_results = {}
- for idx in range(num_algorithms):
- ax = axes[idx // 2, idx % 2]
- algorithm_name = algorithm_names[idx]
- measured_with = nib.load(measured_maps[idx]['x_minus_with_anisotropy']).get_fdata() * -1
- measured_without = nib.load(measured_maps[idx]['x_minus_without_anisotropy']).get_fdata() * -1
- errors_with_regions = []
- errors_without_regions = []
- for region in region_labels:
- region_mask = segmentation2 == region
- if np.any(region_mask):
- # For "With Anisotropy"
- sim_val_with = np.mean(simulated_with[region_mask])
- meas_val_with = np.mean(measured_with[region_mask])
- if sim_val_with != 0:
- error_with = ((meas_val_with - sim_val_with) / sim_val_with) ** 2 * 100
- else:
- error_with = np.nan
- # For "Without Anisotropy"
- sim_val_without = np.mean(simulated_without[region_mask])
- meas_val_without = np.mean(measured_without[region_mask])
- if sim_val_without != 0:
- error_without = ((meas_val_without - sim_val_without) / sim_val_without) ** 2 * 100
- else:
- error_without = np.nan
- errors_with_regions.append(error_with)
- errors_without_regions.append(error_without)
- errors_with_regions = np.array(errors_with_regions)
- errors_without_regions = np.array(errors_without_regions)
- errors_with_regions = errors_with_regions[~np.isnan(errors_with_regions)]
- errors_without_regions = errors_without_regions[~np.isnan(errors_without_regions)]
- x_vals = np.linspace(0, 70, 200)
- max_height = 0.4
- # Plot KDE for "With Anisotropy"
- if errors_with_regions.size > 1:
- kde_with = gaussian_kde(errors_with_regions)
- density_with = kde_with(x_vals)
- scaling_with = max_height / np.max(density_with) if np.max(density_with) > 0 else 1
- density_with_scaled = density_with * scaling_with
- ax.fill_between(x_vals, 0, density_with_scaled, color='tab:blue', alpha=0.4)
- ax.plot(x_vals, density_with_scaled, color='tab:blue', alpha=0.7)
- mean_mspe_with = np.mean(errors_with_regions)
- elif errors_with_regions.size == 1:
- mean_mspe_with = errors_with_regions[0]
- ax.plot([mean_mspe_with], [max_height/2], marker='o', color='tab:blue')
- else:
- mean_mspe_with = None # no data available
- # Plot KDE for "Without Anisotropy"
- if errors_without_regions.size > 1:
- kde_without = gaussian_kde(errors_without_regions)
- density_without = kde_without(x_vals)
- scaling_without = max_height / np.max(density_without) if np.max(density_without) > 0 else 1
- density_without_scaled = density_without * scaling_without
- ax.fill_between(x_vals, 0, density_without_scaled, color='tab:orange', alpha=0.4)
- ax.plot(x_vals, density_without_scaled, color='tab:orange', alpha=0.7)
- mean_mspe_without = np.mean(errors_without_regions)
- elif errors_without_regions.size == 1:
- mean_mspe_without = errors_without_regions[0]
- ax.plot([mean_mspe_without], [max_height/2], marker='o', color='tab:orange')
- else:
- mean_mspe_without = None
- if mean_mspe_with is not None:
- ax.text(0.95, 0.8, f"Mean MSPE: {mean_mspe_with:.1f}%", transform=ax.transAxes,
- color='tab:blue', ha='right', va='center', fontsize=12)
- if mean_mspe_without is not None:
- ax.text(0.95, 0.7, f"Mean MSPE: {mean_mspe_without:.1f}%", transform=ax.transAxes,
- color='tab:orange', ha='right', va='center', fontsize=12)
- # ------------------------------------------------
- # Create and add a legend for the subplot
- # ------------------------------------------------
- legend_elements = [
- Line2D([0], [0], color='tab:blue', lw=2, label='With Anisotropy'),
- Line2D([0], [0], color='tab:orange', lw=2, label='Without Anisotropy')
- ]
- ax.legend(handles=legend_elements, loc='upper right')
- # Set title and axis labels.
- ax.set_title(algorithm_name, fontsize=14)
- ax.set_xlabel("MSPE of $\\chi^-$ (%)", fontsize=12)
- ax.set_ylabel("Density (a.u.)", fontsize=12)
- ax.set_xlim(0, 70)
- ax.set_ylim(0, max_height * 1.2)
- ax.grid(alpha=0.3)
- # ---------------------------
- # Final figure adjustments
- # ---------------------------
- plt.tight_layout(rect=[0, 0, 1, 0.96])
- plt.show()
- # %% [markdown]
- # # Impact of noise
- # %%
- # -------------------------------------------
- # Define algorithms, SNR levels, and errors
- # -------------------------------------------
- algorithms = [
- '$\\chi$-separation',
- 'R2*-QSM',
- 'APART-QSM',
- 'DECOMPOSE-QSM'
- ]
- snr_levels = [50, 100, 200, 300]
- # Initialize dictionaries
- x_positive_errors = {alg: [] for alg in algorithms}
- x_negative_errors = {alg: [] for alg in algorithms}
- def construct_path(*args):
- """Construct a path with 'osfstorage' as a subdirectory of base_dir."""
- return base_dir / 'osfstorage' / Path(*args)
- # -------------------------------------------
- # Load Simulated Maps
- # -------------------------------------------
- simulated_positive_path = construct_path('Susceptibility_Separation_Results', 'Chi_positive.nii.gz')
- simulated_negative_path = construct_path('Susceptibility_Separation_Results', 'Chi_negative_with_anisotropy.nii.gz')
- simulated_positive = nib.load(simulated_positive_path).get_fdata()
- simulated_negative = nib.load(simulated_negative_path).get_fdata()
- # -------------------------------------------
- # Load Segmentation Masks
- # -------------------------------------------
- segmentation_positive_path = construct_path('Masks', 'SegmentedModel.nii.gz')
- segmentation_negative_path = construct_path('Masks', 'white_matter_mask.nii.gz')
- segmentation_positive = nib.load(segmentation_positive_path).get_fdata()
- segmentation_negative = nib.load(segmentation_negative_path).get_fdata()
- # -------------------------------------------
- # Measured Maps: Positive & Negative
- # -------------------------------------------
- measured_maps_positive = {
- '$\\chi$-separation': {
- 300: construct_path('Noise', 'X-separation', 'SNR_300', 'ChiPosMap.nii.gz'),
- 200: construct_path('Noise', 'X-separation', 'SNR_200', 'ChiPosMap.nii.gz'),
- 100: construct_path('Noise', 'X-separation', 'SNR_100', 'ChiPosMap.nii.gz'),
- 50: construct_path('Noise', 'X-separation', 'SNR_50', 'ChiPosMap.nii.gz'),
- },
- 'R2*-QSM': {
- 300: construct_path('Noise', 'R2star-QSM', 'SNR_300', 'ChiPosMap.nii.gz'),
- 200: construct_path('Noise', 'R2star-QSM', 'SNR_200', 'ChiPosMap.nii.gz'),
- 100: construct_path('Noise', 'R2star-QSM', 'SNR_100', 'ChiPosMap.nii.gz'),
- 50: construct_path('Noise', 'R2star-QSM', 'SNR_50', 'ChiPosMap.nii.gz'),
- },
- 'APART-QSM': {
- 300: construct_path('Noise', 'APART-QSM', 'SNR_300', 'X_para.nii.gz'),
- 200: construct_path('Noise', 'APART-QSM', 'SNR_200', 'X_para.nii.gz'),
- 100: construct_path('Noise', 'APART-QSM', 'SNR_100', 'X_para.nii.gz'),
- 50: construct_path('Noise', 'APART-QSM', 'SNR_50', 'X_para.nii.gz'),
- },
- 'DECOMPOSE-QSM': {
- 300: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_300', 'results_PCS.nii.gz'),
- 200: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_200', 'results_PCS.nii.gz'),
- 100: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_100', 'results_PCS.nii.gz'),
- 50: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_50', 'results_PCS.nii.gz'),
- },
- }
- measured_maps_negative = {
- '$\\chi$-separation': {
- 300: construct_path('Noise', 'X-separation', 'SNR_300', 'ChiNegMap.nii.gz'),
- 200: construct_path('Noise', 'X-separation', 'SNR_200', 'ChiNegMap.nii.gz'),
- 100: construct_path('Noise', 'X-separation', 'SNR_100', 'ChiNegMap.nii.gz'),
- 50: construct_path('Noise', 'X-separation', 'SNR_50', 'ChiNegMap.nii.gz'),
- },
- 'R2*-QSM': {
- 300: construct_path('Noise', 'R2star-QSM', 'SNR_300', 'ChiNegMap.nii.gz'),
- 200: construct_path('Noise', 'R2star-QSM', 'SNR_200', 'ChiNegMap.nii.gz'),
- 100: construct_path('Noise', 'R2star-QSM', 'SNR_100', 'ChiNegMap.nii.gz'),
- 50: construct_path('Noise', 'R2star-QSM', 'SNR_50', 'ChiNegMap.nii.gz'),
- },
- 'APART-QSM': {
- 300: construct_path('Noise', 'APART-QSM', 'SNR_300', 'X_dia_abs.nii.gz'),
- 200: construct_path('Noise', 'APART-QSM', 'SNR_200', 'X_dia_abs.nii.gz'),
- 100: construct_path('Noise', 'APART-QSM', 'SNR_100', 'X_dia_abs.nii.gz'),
- 50: construct_path('Noise', 'APART-QSM', 'SNR_50', 'X_dia_abs.nii.gz'),
- },
- 'DECOMPOSE-QSM': {
- 300: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_300', 'results_DCS_abs.nii.gz'),
- 200: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_200', 'results_DCS_abs.nii.gz'),
- 100: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_100', 'results_DCS_abs.nii.gz'),
- 50: construct_path('Noise', 'DECOMPOSE-QSM', 'SNR_50', 'results_DCS_abs.nii.gz'),
- },
- }
- # -------------------------------------------
- # Compute Errors
- # -------------------------------------------
- for alg in algorithms:
- for snr in snr_levels:
- # Load measured maps (X+ and X-)
- measured_positive_path = measured_maps_positive[alg][snr]
- measured_negative_path = measured_maps_negative[alg][snr]
- measured_positive = nib.load(measured_positive_path).get_fdata()
- measured_negative = nib.load(measured_negative_path).get_fdata()
- # Multiply X- by -1
- measured_negative = -1 * measured_negative
- # -----------------------------
- # Positive regions: 1 to 9
- # -----------------------------
- positive_errors = []
- for region in range(1, 10):
- mask = (segmentation_positive == region)
- measured_mean = np.mean(measured_positive[mask])
- simulated_mean = np.mean(simulated_positive[mask])
- error = ((measured_mean - simulated_mean) / simulated_mean)**2 * 100
- positive_errors.append(error)
- avg_positive_error = np.mean(positive_errors)
- x_positive_errors[alg].append(avg_positive_error)
- # -----------------------------
- # Negative regions
- # - 1 to 9 (segmentation_positive)
- # - 1 to 10 (segmentation_negative)
- # -----------------------------
- negative_errors = []
- # 1 to 9
- for region in range(1, 10):
- mask = (segmentation_positive == region)
- measured_mean = np.mean(measured_negative[mask])
- simulated_mean = np.mean(simulated_negative[mask])
- error = ((measured_mean - simulated_mean) / simulated_mean)**2 * 100
- negative_errors.append(error)
- # 1 to 10 (segmentation_negative)
- for region in range(1, 11):
- mask = (segmentation_negative == region)
- measured_mean = np.mean(measured_negative[mask])
- simulated_mean = np.mean(simulated_negative[mask])
- error = ((measured_mean - simulated_mean) / simulated_mean)**2 * 100
- negative_errors.append(error)
- avg_negative_error = np.mean(negative_errors)
- x_negative_errors[alg].append(avg_negative_error)
- # -------------------------------------------
- # Plotting
- # -------------------------------------------
- hatch_patterns = ['x', 'o', '*', '+']
- snr_colors = {
- 50: '#66c2a5',
- 100: '#8da0cb',
- 200: '#fc8d62',
- 300: '#e78ac3',
- }
- bar_width = 0.2
- snr_index = np.arange(len(snr_levels))
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6), dpi=600)
- tick_font_size = 14
- # --- Plot χ+ ---
- for i, (alg, hatch) in enumerate(zip(algorithms, hatch_patterns)):
- for j, snr in enumerate(snr_levels):
- x_coord = snr_index[j] + i * bar_width
- y_value = x_positive_errors[alg][j]
- ax1.bar(
- x_coord,
- y_value,
- bar_width,
- hatch=hatch,
- edgecolor='black',
- color=snr_colors[snr],
- )
- ax1.set_xlabel('SNR (a.u.)', fontsize=tick_font_size)
- ax1.set_ylabel('MSPE of $\\chi^+$ (%)', fontsize=tick_font_size)
- ax1.set_xticks(snr_index + bar_width * 1.5)
- ax1.set_xticklabels(snr_levels, fontsize=tick_font_size)
- ax1.grid(alpha=0.3)
- ax1.tick_params(axis='both', which='major', labelsize=tick_font_size)
- # --- Plot χ- ---
- for i, (alg, hatch) in enumerate(zip(algorithms, hatch_patterns)):
- for j, snr in enumerate(snr_levels):
- x_coord = snr_index[j] + i * bar_width
- y_value = x_negative_errors[alg][j]
- ax2.bar(
- x_coord,
- y_value,
- bar_width,
- hatch=hatch,
- edgecolor='black',
- color=snr_colors[snr],
- )
- ax2.set_xlabel('SNR (a.u.)', fontsize=tick_font_size)
- ax2.set_ylabel('MSPE of $\\chi^-$ (%)', fontsize=tick_font_size)
- ax2.set_xticks(snr_index + bar_width * 1.5)
- ax2.set_xticklabels(snr_levels, fontsize=tick_font_size)
- ax2.grid(alpha=0.3)
- ax2.tick_params(axis='both', which='major', labelsize=tick_font_size)
- # For χ+ axis:
- algorithm_patches_pos = []
- for alg, hatch in zip(algorithms, hatch_patterns):
- patch = Patch(
- facecolor='white',
- edgecolor='black',
- hatch=hatch,
- label=alg
- )
- algorithm_patches_pos.append(patch)
- ax1.legend(
- handles=algorithm_patches_pos,
- fontsize=14,
- facecolor='white',
- framealpha=1,
- loc='upper right',
- handlelength=1.5,
- handleheight=1.5
- )
- # For χ- axis:
- algorithm_patches_neg = []
- for alg, hatch in zip(algorithms, hatch_patterns):
- patch = Patch(
- facecolor='white',
- edgecolor='black',
- hatch=hatch,
- label=alg
- )
- algorithm_patches_neg.append(patch)
- ax2.legend(
- handles=algorithm_patches_neg,
- fontsize=14,
- facecolor='white',
- framealpha=1,
- loc='upper right',
- handlelength=1.5,
- handleheight=1.5
- )
- plt.tight_layout()
- plt.show()
- # %% [markdown]
- # # Impact of noise on susceptibility anisotorpy
- # %%
- # Define the algorithms and SNR levels
- algorithms = ['x-separation', 'R2*-QSM', 'APART-QSM', 'DECOMPOSE-QSM']
- snr_levels = ['SNR 300', 'SNR 200', 'SNR 100', 'SNR 50']
- # Initialize a dictionary to store the full paths for measured images
- measured_image_paths = {
- 'x-separation': {
- 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_300' / 'ChiNegMap.nii.gz',
- 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_200' / 'ChiNegMap.nii.gz',
- 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_100' / 'ChiNegMap.nii.gz',
- 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'X-separation' / 'SNR_50' / 'ChiNegMap.nii.gz',
- },
- 'R2*-QSM': {
- 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_300' / 'ChiNegMap.nii.gz',
- 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_200' / 'ChiNegMap.nii.gz',
- 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_100' / 'ChiNegMap.nii.gz',
- 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'R2star-QSM' / 'SNR_50' / 'ChiNegMap.nii.gz',
- },
- 'APART-QSM': {
- 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_300' / 'X_dia_abs.nii.gz',
- 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_200' / 'X_dia_abs.nii.gz',
- 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_100' / 'X_dia_abs.nii.gz',
- 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'APART-QSM' / 'SNR_50' / 'X_dia_abs.nii.gz',
- },
- 'DECOMPOSE-QSM': {
- 'SNR 300': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_300' / 'results_DCS_abs.nii.gz',
- 'SNR 200': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_200' / 'results_DCS_abs.nii.gz',
- 'SNR 100': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_100' / 'results_DCS_abs.nii.gz',
- 'SNR 50': base_dir / 'osfstorage' / 'Noise' / 'DECOMPOSE-QSM' / 'SNR_50' / 'results_DCS_abs.nii.gz',
- },
- }
- # ----------------------------
- # 2) LOAD NIFTI DATA
- # ----------------------------
- # Load simulated (ground truth) chi, theta, and segmentation images
- simulated_nii = nib.load(base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_negative_with_anisotropy.nii.gz')
- simulated_img_with_anisotropy = simulated_nii.get_fdata()
- theta_nii = nib.load(base_dir / 'osfstorage' / 'Noise' / 'theta.nii.gz')
- theta_img = theta_nii.get_fdata()
- theta_img[np.isnan(theta_img)] = 0
- segmentation_nii = nib.load(base_dir / 'osfstorage' / 'Masks' / 'WM_fibers_seg.nii.gz')
- segmentation_img = segmentation_nii.get_fdata()
- theta_shape = theta_nii.shape
- theta_affine = theta_nii.affine
- # Load measured images for each algorithm and SNR
- measured_images = {}
- for algorithm in algorithms:
- measured_images[algorithm] = {}
- for snr in snr_levels:
- path = measured_image_paths[algorithm][snr]
- measured_images[algorithm][snr] = nib.load(path).get_fdata()
- # ----------------------------
- # 3) GATHER VALID VOXELS & COMPUTE ERRORS
- # ----------------------------
- all_valid_coords = []
- all_theta_values = []
- all_sim_values = []
- errors = {
- algorithm: {
- snr: [] for snr in snr_levels
- }
- for algorithm in algorithms
- }
- # Loop over the segmentation regions
- for region in range(1, 28):
- # Region mask
- region_mask = (segmentation_img == region)
- region_valid_mask = region_mask & (theta_img != 0)
- # Get the 3D coordinates of those valid voxels
- valid_coords = np.argwhere(region_valid_mask)
- if valid_coords.size == 0:
- continue
- # Extract the actual voxel values
- region_theta_vals = theta_img[region_valid_mask]
- region_sim_vals = simulated_img_with_anisotropy[region_valid_mask] * -1
- # Append these to our global lists
- all_valid_coords.append(valid_coords)
- all_theta_values.append(region_theta_vals)
- all_sim_values.append(region_sim_vals)
- # For each algorithm & SNR, calculate MSPE in this region and append
- for algorithm in algorithms:
- for snr in snr_levels:
- measured_vals = measured_images[algorithm][snr][region_valid_mask]
- epsilon = 1e-10
- relative_squared_error = np.sqrt(
- np.square(region_sim_vals - measured_vals) /
- (np.square(region_sim_vals) + epsilon)
- ) * 100
- errors[algorithm][snr].append(relative_squared_error)
- all_valid_coords = np.concatenate(all_valid_coords, axis=0)
- all_theta_values = np.concatenate(all_theta_values, axis=0)
- all_sim_values = np.concatenate(all_sim_values, axis=0)
- for algorithm in algorithms:
- for snr in snr_levels:
- errors[algorithm][snr] = np.concatenate(errors[algorithm][snr], axis=0)
- # ----------------------------
- # 4) BIN THE THETA VALUES
- # ----------------------------
- bins = np.arange(0, 100, 10)
- bin_indices = np.digitize(all_theta_values, bins)
- num_bins = len(bins) - 1
- # Calculate the mean bin angle
- bin_means_theta = []
- for b in range(1, len(bins)):
- mask_b = (bin_indices == b)
- if np.any(mask_b):
- bin_means_theta.append(np.mean(all_theta_values[mask_b]))
- bin_means_theta = np.array(bin_means_theta)
- # ----------------------------
- # 5) CALCULATE BIN MEANS OF MSPE FOR EACH ALGORITHM AND SNR
- # ----------------------------
- bin_means = {
- algorithm: {
- snr: [] for snr in snr_levels
- } for algorithm in algorithms
- }
- for b in range(1, len(bins)):
- mask_b = (bin_indices == b)
- if np.any(mask_b):
- for algorithm in algorithms:
- for snr in snr_levels:
- mspe_values_bin = errors[algorithm][snr][mask_b]
- bin_mean_error = np.mean(mspe_values_bin)
- bin_means[algorithm][snr].append(bin_mean_error)
- else:
- for algorithm in algorithms:
- for snr in snr_levels:
- bin_means[algorithm][snr].append(np.nan)
- # Convert bin means to numpy arrays
- for algorithm in algorithms:
- for snr in snr_levels:
- bin_means[algorithm][snr] = np.array(bin_means[algorithm][snr])
- # ----------------------------
- # 6) SAVE A SINGLE 3D MASK WITH BIN LABELS
- # ----------------------------
- all_bins_3d = np.zeros(theta_shape, dtype=np.uint8)
- for b in range(1, len(bins)):
- # Voxels in bin b
- mask_b = (bin_indices == b)
- # Coordinates of voxels in bin b
- bin_coords = all_valid_coords[mask_b]
- all_bins_3d[bin_coords[:, 0], bin_coords[:, 1], bin_coords[:, 2]] = b
- # ----------------------------
- # 7) PLOT THE RESULTS
- # ----------------------------
- markers = {
- 'SNR 300': ('#e78ac3', 'o'),
- 'SNR 200': ('#fc8d62', 'o'),
- 'SNR 100': ('#8da0cb', 'o'),
- 'SNR 50': ('#66c2a5', 'o')
- }
- fig, axs = plt.subplots(2, 2, figsize=(14, 10))
- algorithm_subplot_indices = {
- 'x-separation': (0, 0),
- 'R2*-QSM': (0, 1),
- 'APART-QSM': (1, 0),
- 'DECOMPOSE-QSM': (1, 1)
- }
- for algorithm in algorithms:
- ax = axs[algorithm_subplot_indices[algorithm]]
- # Dictionary to store percentage changes for each SNR
- percentage_changes = {}
- for snr in snr_levels:
- color, marker = markers[snr]
- # x-values = bin_means_theta
- x_vals = bin_means_theta
- # y-values = bin_means[algorithm][snr]
- y_vals = bin_means[algorithm][snr]
- # Scatter plot
- ax.scatter(
- x_vals,
- y_vals,
- color=color,
- marker=marker,
- label=snr
- )
- # 1) Sort the points by x so we can connect them in ascending order
- sort_idx = np.argsort(x_vals)
- x_sorted = x_vals[sort_idx]
- y_sorted = y_vals[sort_idx]
- # Calculate percentage change (max vs. min) for this SNR
- valid_mspe = y_vals[~np.isnan(y_vals)]
- if len(valid_mspe) > 0:
- max_mspe = y_vals[0]
- min_mspe = y_vals[-1]
- if max_mspe != 0:
- percentage_change = (max_mspe - min_mspe) / max_mspe * 100
- else:
- percentage_change = 0
- else:
- percentage_change = 0
- percentage_changes[snr] = percentage_change
- # Build the "MEV=" Variation text with Matplotlib colors
- x_position = 0.47
- y_position = 0.05
- ax.text(
- x_position, y_position,
- "MEV=",
- transform=ax.transAxes, fontsize=12, color='black', ha='left', va='top'
- )
- x_position += 0.1
- text_colors = ['#e78ac3', '#fc8d62', '#8da0cb', '#66c2a5']
- for idx, snr in enumerate(snr_levels):
- color_ = text_colors[idx]
- ax.text(
- x_position, y_position,
- f"{percentage_changes[snr]:.1f}%",
- transform=ax.transAxes, fontsize=12, color=color_, ha='left', va='top'
- )
- x_position += 0.1
- # Set titles and labels
- if algorithm == 'x-separation':
- ax.set_title(r"$\chi$-separation")
- else:
- ax.set_title(algorithm)
- ax.set_xlabel('Angle of Orientation (degrees)', fontsize=16)
- ax.set_ylabel('MSPE of $\\chi^-$ (%)', fontsize=16)
- ax.grid(True)
- ax.legend()
- ax.set_ylim(0, 130)
- ax.tick_params(axis='both', which='major', labelsize=16)
- plt.tight_layout()
- plt.show()
- # %% [markdown]
- # # Comparison between simulated vs in-vivo susceptibility maps
- # %%
- # Define regions of interest
- regions_of_interest = [1, 2, 3, 7, 8, 9]
- # Function to extract mean values per region
- def extract_mean_per_region(data_map, segmentation_map, regions):
- means = []
- for region in regions:
- region_mask = segmentation_map == region
- if np.any(region_mask):
- mean_value = np.mean(data_map[region_mask])
- means.append(mean_value)
- else:
- means.append(np.nan)
- return means
- # ------------------------------------------------------------------------------
- # Load segmentation map (for simulated data only)
- # ------------------------------------------------------------------------------
- segmentation_simulated = nib.load(
- base_dir / 'osfstorage' / 'Masks' / 'SegmentedModel.nii.gz'
- ).get_fdata()
- # ------------------------------------------------------------------------------
- # Hard-coded in-vivo mean values (from your table) for each algorithm and region
- # ------------------------------------------------------------------------------
- # Chi-positive in-vivo data
- chi_positive_in_vivo_data = {
- 'chi-separation': [0.0658392,0.113222,0.0550474,0.0475523,0.0270498,0.0299241],
- 'R2*-QSM': [0.0357698,0.0610981,0.0347473,0.0265003,0.0159944,0.0140221],
- 'APART-QSM': [0.0482199,0.0813391,0.0435077,0.0346825,0.0205208,0.02208],
- 'DECOMPOSE-QSM': [0.0337807,0.0538997,0.0325998,0.0301356,0.0157292,0.0112171]
- }
- # Chi-negative in-vivo data (already positive)
- chi_negative_in_vivo_data = {
- 'chi-separation': [-0.0175603,-0.0332972,-0.0260955,-0.0270215,-0.0303985,-0.0277441],
- 'R2*-QSM': [-0.00613275,-0.00185815,-0.00892519,-0.0140371,-0.0187294,-0.0130369],
- 'APART-QSM': [-0.0105914,-0.0195933,-0.0182477,-0.0190805,-0.0222699,-2.15E-02],
- 'DECOMPOSE-QSM': [-0.00877939,-0.00756195,-0.00899253,-0.00902613,-0.0163517,-0.00915724]
- }
- # ------------------------------------------------------------------------------
- # Initialize dictionaries to store mean values
- # ------------------------------------------------------------------------------
- chi_positive_in_vivo_means = {}
- chi_positive_simulated_means = {}
- chi_negative_in_vivo_means = {}
- chi_negative_simulated_means = {}
- # ------------------------------------------------------------------------------
- # File paths for simulated data
- # ------------------------------------------------------------------------------
- chi_positive_simulated_files = {
- 'chi-separation': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiPosMap.nii',
- 'R2*-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiPosMap.nii',
- 'APART-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_para.nii',
- 'DECOMPOSE-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM'/ 'With_anisotropy' / 'Results_PCS.nii'
- }
- chi_negative_simulated_files = {
- 'chi-separation': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiNegMap.nii',
- 'R2*-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiNegMap.nii',
- 'APART-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_dia_abs.nii',
- 'DECOMPOSE-QSM': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM'/ 'With_anisotropy' / 'Results_DCS_abs.nii'
- }
- algorithms = list(chi_positive_simulated_files.keys())
- # ------------------------------------------------------------------------------
- # Process chi-positive data
- # ------------------------------------------------------------------------------
- for algo in algorithms:
- # In-vivo: use the hard-coded values
- in_vivo_means = chi_positive_in_vivo_data[algo]
- # Simulated: load from file and extract
- simulated_map = nib.load(chi_positive_simulated_files[algo]).get_fdata()
- simulated_means = extract_mean_per_region(simulated_map, segmentation_simulated, regions_of_interest)
- chi_positive_in_vivo_means[algo] = in_vivo_means
- chi_positive_simulated_means[algo] = simulated_means
- # ------------------------------------------------------------------------------
- # Process chi-negative data
- # ------------------------------------------------------------------------------
- for algo in algorithms:
- # In-vivo: use the hard-coded values
- in_vivo_means = chi_negative_in_vivo_data[algo]
- # Simulated: load from file, multiply by -1
- simulated_map = nib.load(chi_negative_simulated_files[algo]).get_fdata()
- simulated_map *= -1
- simulated_means = extract_mean_per_region(simulated_map, segmentation_simulated, regions_of_interest)
- chi_negative_in_vivo_means[algo] = in_vivo_means
- chi_negative_simulated_means[algo] = simulated_means
- # ------------------------------------------------------------------------------
- # Define display names
- # ------------------------------------------------------------------------------
- display_names = {
- 'chi-separation': r'$\chi$-separation',
- 'R2*-QSM': 'R2*-QSM',
- 'APART-QSM': 'APART-QSM',
- 'DECOMPOSE-QSM': 'DECOMPOSE-QSM'
- }
- # ------------------------------------------------------------------------------
- # Plotting function
- # ------------------------------------------------------------------------------
- def plot_data(in_vivo_means_dict, simulated_means_dict, chi_type, ax):
- marker_styles = ['x', 'o', '*', '+'] # for the 4 algorithms in the order they appear in 'algorithms'
- for idx, algo in enumerate(algorithms):
- x = np.array(in_vivo_means_dict[algo])
- y = np.array(simulated_means_dict[algo])
- mask = ~np.isnan(x) & ~np.isnan(y)
- x = x[mask]
- y = y[mask]
- if len(x) > 1:
- slope, intercept, r_value, p_value, std_err = linregress(x, y)
- r_label = f"(r={r_value:.2f})"
- sorted_indices = np.argsort(x)
- x_sorted = x[sorted_indices]
- y_sorted = intercept + slope * x_sorted
- ax.plot(x_sorted, y_sorted, color='black', linestyle='--', label=None)
- else:
- r_label = "(R²=NaN)"
- label = f"{display_names[algo]} {r_label}"
- marker = marker_styles[idx]
- if marker == 'x':
- ax.scatter(x, y, marker=marker, s=80, color='black',
- linewidth=1.5, label=label)
- elif marker == 'o':
- ax.scatter(x, y, marker=marker, s=80,
- edgecolors='black', facecolors='none',
- linewidth=1.5, label=label)
- else:
- ax.scatter(x, y, marker=marker, s=80,
- color='black', linewidth=1.5, label=label)
- x_limits = ax.get_xlim()
- y_limits = ax.get_ylim()
- min_val = min(x_limits[0], y_limits[0])
- max_val = max(x_limits[1], y_limits[1])
- ax.set_xlim([min_val, max_val])
- ax.set_ylim([min_val, max_val])
- ax.plot([min_val, max_val], [min_val, max_val], '-', color='grey', alpha=0.3)
- ax.set_xlabel(f'Measured {chi_type} in-vivo (ppm)', fontsize=18)
- ax.set_ylabel(f'Simulated {chi_type} (ppm)', fontsize=18)
- ax.legend(loc='upper left', fontsize=14)
- ax.grid(True)
- ax.tick_params(axis='both', which='major', labelsize=14)
- # ------------------------------------------------------------------------------
- # Create the figure and subplots
- # ------------------------------------------------------------------------------
- fig, axes = plt.subplots(1, 2, figsize=(20, 6))
- plot_data(chi_positive_in_vivo_means, chi_positive_simulated_means, r'$\chi^+$', axes[0])
- plot_data(chi_negative_in_vivo_means, chi_negative_simulated_means, r'$\chi^-$', axes[1])
- for ax in axes:
- ax.tick_params(axis='both', which='major', labelsize=16)
- plt.tight_layout()
- plt.show()
- # %% [markdown]
- # # Comparison between simulated vs in-vivo field maps
- # %%
- # Construct file paths relative to the identified base directory
- simulated_map_path = base_dir / "osfstorage/LocalField/Simulated_Field.nii.gz"
- segmentation_map_path = base_dir / "osfstorage/Masks/white_matter_mask.nii.gz"
- # Load the NIfTI files for simulated local field map and the segmentation map
- simulated_map_nii = nib.load(simulated_map_path)
- segmentation_map_nii = nib.load(segmentation_map_path)
- # Extract the data arrays
- simulated_map = simulated_map_nii.get_fdata()
- segmentation_map = segmentation_map_nii.get_fdata()
- # Extract simulated values for scatter plot (regions 1 to 10 in the segmentation map)
- simulated_values = []
- for region in range(1, 11):
- region_mask = segmentation_map == region
- simulated_region_values = simulated_map[region_mask]
- simulated_values.append(np.mean(simulated_region_values))
- # Hard-coded in-vivo values (for the same 10 regions)
- in_vivo_values = np.array([
- 1.74348,
- -1.72861,
- -1.59869,
- 1.46938,
- -2.61981,
- -0.557609,
- -0.714494,
- -1.31104,
- -2.75768,
- 0.288124
- ])
- # Scatter plot with linear regression
- slope, intercept, r_value, p_value, std_err = stats.linregress(in_vivo_values, simulated_values)
- regression_line = slope * in_vivo_values + intercept
- # Create the scatter plot with a smaller figure size
- fig, ax = plt.subplots(figsize=(6, 6))
- # Sort the x-values (in-vivo local field values) and calculate corresponding y-values
- sorted_indices = np.argsort(in_vivo_values)
- sorted_in_vivo_values = in_vivo_values[sorted_indices]
- sorted_regression_line = slope * sorted_in_vivo_values + intercept
- # Scatter plot
- ax.scatter(in_vivo_values, simulated_values, color='black', zorder=2)
- # Plot the regression line
- ax.plot(sorted_in_vivo_values, sorted_regression_line, linestyle='--',
- color='black', linewidth=1.2, zorder=1)
- # Add labels, grid, and R² text
- ax.set_xlabel('In-vivo local field (Hz)', fontsize=10)
- ax.set_ylabel('Simulated local field (Hz)', fontsize=10)
- ax.grid(True, alpha=0.3)
- ax.text(0.05, 0.95,
- f'Correlation = {r_value:.2f}\nSlope = {slope:.1f}',
- ha='left', va='top', transform=ax.transAxes, fontsize=14)
- plt.tight_layout()
- plt.show()
- # %% [markdown]
- # # Comparison between simulated vs in-vivo T2 maps
- # %%
- # File paths for simulated T2 map and segmentation map
- simulated_t2_path = base_dir / "osfstorage/T2/T2_simulated_resampled.nii.gz"
- simulated_segmentation_path = base_dir / "osfstorage/Masks/SegmentedModel_resampled.nii.gz"
- # Load NIfTI files for simulated data
- simulated_t2_map = nib.load(simulated_t2_path).get_fdata()
- simulated_segmentation = nib.load(simulated_segmentation_path).get_fdata()
- # Replace NaN and Inf values with 0 in the simulated T2 map
- simulated_t2_map = np.nan_to_num(simulated_t2_map, nan=0, posinf=0, neginf=0)
- # Hard-coded in-vivo T2 values for regions 1-3 and 7-9 (6 regions)
- T2_in_vivo = np.array([67.2624, 44.9122, 52.7201, 59.789, 54.5211, 104.057])
- # Extract simulated T2 values for regions 1-3 and 7-9
- T2_simulated = []
- for region in list(range(1, 4)) + list(range(7, 10)):
- simulated_mask = simulated_segmentation == region
- if np.any(simulated_mask):
- simulated_mean = np.mean(simulated_t2_map[simulated_mask])
- T2_simulated.append(simulated_mean)
- T2_simulated = np.array(T2_simulated)
- # Reshape data for linear regression
- T2_in_vivo_reshaped = T2_in_vivo.reshape(-1, 1)
- T2_simulated_reshaped = T2_simulated.reshape(-1, 1)
- # Perform linear regression
- reg_model = LinearRegression()
- reg_model.fit(T2_in_vivo_reshaped, T2_simulated_reshaped)
- slope = reg_model.coef_[0][0]
- intercept = reg_model.intercept_[0]
- # Generate regression line values
- x_fit = np.linspace(T2_in_vivo.min(), T2_in_vivo.max(), 100)
- y_fit = slope * x_fit + intercept
- # Plotting
- plt.figure(figsize=(8, 6))
- plt.scatter(T2_in_vivo, T2_simulated, color='black', label='Data Points')
- plt.plot(x_fit, y_fit, '--', color='black', label='Regression Line')
- plt.xlabel(r'$T_2^{\mathrm{in-vivo}}$ (ms)', fontsize=16)
- plt.ylabel(r'$T_2^{\mathrm{simulated}}$ (ms)', fontsize=16)
- correlation = np.corrcoef(T2_in_vivo, T2_simulated)[0, 1]
- plt.text(45, 120, f'Correlation: {correlation:.2f}\nSlope: {slope:.2f}', fontsize=18)
- plt.grid(True)
- plt.xticks(fontsize=14)
- plt.yticks(fontsize=14)
- plt.tight_layout()
- plt.show()
- # %% [markdown]
- # # Supplimantary Material
- # %%
- num_algorithms = 4
- algorithm_names = [
- '$\\chi$-separation',
- 'R2*-QSM',
- 'APART-QSM',
- 'DECOMPOSE-QSM'
- ]
- segmentation2_path = base_dir / 'osfstorage' / 'Masks' / 'white_matter_mask.nii.gz'
- segmentation2 = nib.load(segmentation2_path).get_fdata()
- region_labels = list(range(1, 10))
- # Load simulated maps
- simulated_with_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
- simulated_without_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
- simulated_with = nib.load(simulated_with_path).get_fdata()
- simulated_without = nib.load(simulated_without_path).get_fdata()
- # Define measured maps for each algorithm
- measured_maps = {
- 0: { # Algorithm 1: χ-separation
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiPosMap.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'Without_anisotropy' / 'ChiPosMap.nii',
- },
- 1: { # Algorithm 2: R2*-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiPosMap.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'Without_anisotropy' / 'ChiPosMap.nii',
- },
- 2: { # Algorithm 3: APART-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_para.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'Without_anisotropy' / 'X_para.nii',
- },
- 3: { # Algorithm 4: DECOMPOSE-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_PCS.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'Without_anisotropy' / 'results_PCS.nii',
- },
- }
- fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12), dpi=300)
- for idx in range(num_algorithms):
- ax = axes[idx // 2, idx % 2]
- algorithm_name = algorithm_names[idx]
- # Load the measured maps for the current algorithm
- measured_with = nib.load(measured_maps[idx]['x_minus_with_anisotropy']).get_fdata()
- measured_without = nib.load(measured_maps[idx]['x_minus_without_anisotropy']).get_fdata()
- errors_with_regions = []
- errors_without_regions = []
- # --------------------------------------------------
- # 1) Compute errors
- # --------------------------------------------------
- for region in region_labels:
- region_mask = (segmentation2 == region)
- if region == 8:
- continue
- if np.any(region_mask):
- # "With Anisotropy"
- sim_val_with = np.mean(simulated_with[region_mask])
- if sim_val_with < 0:
- sim_val_with *= -1
- meas_val_with = np.mean(measured_with[region_mask])
- if sim_val_with != 0:
- error_with = ((meas_val_with - sim_val_with) / sim_val_with) ** 2 * 100
- else:
- error_with = np.nan
- # "Without Anisotropy"
- sim_val_without = np.mean(simulated_without[region_mask])
- meas_val_without = np.mean(measured_without[region_mask])
- if sim_val_without != 0:
- error_without = ((meas_val_without - sim_val_without) / sim_val_without) ** 2 * 100
- else:
- error_without = np.nan
- errors_with_regions.append(error_with)
- errors_without_regions.append(error_without)
- # Convert to numpy arrays and remove NaNs
- errors_with_regions = np.array(errors_with_regions)
- errors_without_regions = np.array(errors_without_regions)
- errors_with_regions = errors_with_regions[~np.isnan(errors_with_regions)]
- errors_without_regions = errors_without_regions[~np.isnan(errors_without_regions)]
- # Prepare for KDE
- x_vals = np.linspace(0, 30000, 200)
- max_height = 0.4
- # Plot KDE for "With Anisotropy"
- if errors_with_regions.size > 1:
- kde_with = gaussian_kde(errors_with_regions)
- density_with = kde_with(x_vals)
- scaling_with = max_height / np.max(density_with) if np.max(density_with) > 0 else 1
- density_with_scaled = density_with * scaling_with
- ax.fill_between(x_vals, 0, density_with_scaled, alpha=0.4, color='tab:blue')
- ax.plot(x_vals, density_with_scaled, alpha=0.7, color='tab:blue')
- mean_mspe_with = np.mean(errors_with_regions)
- elif errors_with_regions.size == 1:
- mean_mspe_with = errors_with_regions[0]
- ax.plot([mean_mspe_with], [max_height/2], marker='o', color='tab:blue')
- else:
- mean_mspe_with = None
- # Plot KDE for "Without Anisotropy"
- if errors_without_regions.size > 1:
- kde_without = gaussian_kde(errors_without_regions)
- density_without = kde_without(x_vals)
- scaling_without = max_height / np.max(density_without) if np.max(density_without) > 0 else 1
- density_without_scaled = density_without * scaling_without
- ax.fill_between(x_vals, 0, density_without_scaled, alpha=0.4, color='tab:orange')
- ax.plot(x_vals, density_without_scaled, alpha=0.7, color='tab:orange')
- mean_mspe_without = np.mean(errors_without_regions)
- elif errors_without_regions.size == 1:
- mean_mspe_without = errors_without_regions[0]
- ax.plot([mean_mspe_without], [max_height/2], marker='o', color='tab:orange')
- else:
- mean_mspe_without = None
- # Show mean MSPE for with/without anisotropy
- if mean_mspe_with is not None:
- ax.text(0.95, 0.8, f"Mean MSPE: {mean_mspe_with:.1f}%", transform=ax.transAxes,
- color='tab:blue', ha='right', va='center', fontsize=12)
- if mean_mspe_without is not None:
- ax.text(0.95, 0.7, f"Mean MSPE: {mean_mspe_without:.1f}%", transform=ax.transAxes,
- color='tab:orange', ha='right', va='center', fontsize=12)
- legend_elements = [
- Line2D([0], [0], color='tab:blue', lw=2, label='With Anisotropy'),
- Line2D([0], [0], color='tab:orange', lw=2, label='Without Anisotropy')
- ]
- ax.legend(handles=legend_elements, loc='upper right')
- ax.set_title(algorithm_name, fontsize=14)
- ax.set_xlabel("MSPE of $\\chi^+$ (%)", fontsize=12)
- ax.set_ylabel("Density (a.u.)", fontsize=12)
- ax.set_ylim(0, max_height * 1.2)
- ax.grid(alpha=0.3)
- # ---------------------------
- # Final figure adjustments
- # ---------------------------
- plt.tight_layout(rect=[0, 0, 1, 0.96])
- plt.show()
- # %%
- num_algorithms = 4
- algorithm_names = [
- '$\\chi$-separation',
- 'R2*-QSM',
- 'APART-QSM',
- 'DECOMPOSE-QSM'
- ]
- segmentation2_path = base_dir / 'osfstorage' / 'Masks' / 'SegmentedModel.nii.gz'
- segmentation2 = nib.load(segmentation2_path).get_fdata()
- region_labels = list(range(1, 10))
- # Load simulated maps
- simulated_with_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
- simulated_without_path = base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'Chi_positive.nii.gz'
- simulated_with = nib.load(simulated_with_path).get_fdata()
- simulated_without = nib.load(simulated_without_path).get_fdata()
- # Define measured maps for each algorithm
- measured_maps = {
- 0: { # Algorithm 1: χ-separation
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiPosMap.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'Without_anisotropy' / 'ChiPosMap.nii',
- },
- 1: { # Algorithm 2: R2*-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiPosMap.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'Without_anisotropy' / 'ChiPosMap.nii',
- },
- 2: { # Algorithm 3: APART-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_para.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'Without_anisotropy' / 'X_para.nii',
- },
- 3: { # Algorithm 4: DECOMPOSE-QSM
- 'x_minus_with_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_PCS.nii',
- 'x_minus_without_anisotropy': base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'Without_anisotropy' / 'results_PCS.nii',
- },
- }
- fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12), dpi=300)
- for idx in range(num_algorithms):
- ax = axes[idx // 2, idx % 2]
- algorithm_name = algorithm_names[idx]
- # Load the measured maps for the current algorithm
- measured_with = nib.load(measured_maps[idx]['x_minus_with_anisotropy']).get_fdata()
- measured_without = nib.load(measured_maps[idx]['x_minus_without_anisotropy']).get_fdata()
- errors_with_regions = []
- errors_without_regions = []
- # --------------------------------------------------
- # 1) Compute errors
- # --------------------------------------------------
- for region in region_labels:
- region_mask = (segmentation2 == region)
- if region == 8:
- continue
- if np.any(region_mask):
- # "With Anisotropy"
- sim_val_with = np.mean(simulated_with[region_mask])
- if sim_val_with < 0:
- sim_val_with *= -1
- meas_val_with = np.mean(measured_with[region_mask])
- if sim_val_with != 0:
- error_with = ((meas_val_with - sim_val_with) / sim_val_with) ** 2 * 100
- else:
- error_with = np.nan
- # "Without Anisotropy"
- sim_val_without = np.mean(simulated_without[region_mask])
- meas_val_without = np.mean(measured_without[region_mask])
- if sim_val_without != 0:
- error_without = ((meas_val_without - sim_val_without) / sim_val_without) ** 2 * 100
- else:
- error_without = np.nan
- errors_with_regions.append(error_with)
- errors_without_regions.append(error_without)
- # Convert to numpy arrays and remove NaNs
- errors_with_regions = np.array(errors_with_regions)
- errors_without_regions = np.array(errors_without_regions)
- errors_with_regions = errors_with_regions[~np.isnan(errors_with_regions)]
- errors_without_regions = errors_without_regions[~np.isnan(errors_without_regions)]
- # Prepare for KDE
- x_vals = np.linspace(0, 70, 70)
- max_height = 0.4
- # Plot KDE for "With Anisotropy"
- if errors_with_regions.size > 1:
- kde_with = gaussian_kde(errors_with_regions)
- density_with = kde_with(x_vals)
- scaling_with = max_height / np.max(density_with) if np.max(density_with) > 0 else 1
- density_with_scaled = density_with * scaling_with
- ax.fill_between(x_vals, 0, density_with_scaled, alpha=0.4, color='tab:blue')
- ax.plot(x_vals, density_with_scaled, alpha=0.7, color='tab:blue')
- mean_mspe_with = np.mean(errors_with_regions)
- elif errors_with_regions.size == 1:
- mean_mspe_with = errors_with_regions[0]
- ax.plot([mean_mspe_with], [max_height/2], marker='o', color='tab:blue')
- else:
- mean_mspe_with = None
- # Plot KDE for "Without Anisotropy"
- if errors_without_regions.size > 1:
- kde_without = gaussian_kde(errors_without_regions)
- density_without = kde_without(x_vals)
- scaling_without = max_height / np.max(density_without) if np.max(density_without) > 0 else 1
- density_without_scaled = density_without * scaling_without
- ax.fill_between(x_vals, 0, density_without_scaled, alpha=0.4, color='tab:orange')
- ax.plot(x_vals, density_without_scaled, alpha=0.7, color='tab:orange')
- mean_mspe_without = np.mean(errors_without_regions)
- elif errors_without_regions.size == 1:
- mean_mspe_without = errors_without_regions[0]
- ax.plot([mean_mspe_without], [max_height/2], marker='o', color='tab:orange')
- else:
- mean_mspe_without = None
- # Show mean MSPE for with/without anisotropy
- if mean_mspe_with is not None:
- ax.text(0.95, 0.8, f"Mean MSPE: {mean_mspe_with:.1f}%", transform=ax.transAxes,
- color='tab:blue', ha='right', va='center', fontsize=12)
- if mean_mspe_without is not None:
- ax.text(0.95, 0.7, f"Mean MSPE: {mean_mspe_without:.1f}%", transform=ax.transAxes,
- color='tab:orange', ha='right', va='center', fontsize=12)
- legend_elements = [
- Line2D([0], [0], color='tab:blue', lw=2, label='With Anisotropy'),
- Line2D([0], [0], color='tab:orange', lw=2, label='Without Anisotropy')
- ]
- ax.legend(handles=legend_elements, loc='upper right')
- ax.set_title(algorithm_name, fontsize=14)
- ax.set_xlabel("MSPE of $\\chi^+$ (%)", fontsize=12)
- ax.set_ylabel("Density (a.u.)", fontsize=12)
- ax.set_ylim(0, max_height * 1.2)
- ax.grid(alpha=0.3)
- # ---------------------------
- # Final figure adjustments
- # ---------------------------
- plt.tight_layout(rect=[0, 0, 1, 0.96])
- plt.show()
- # %%
- methods = {
- "χ-separation": (
- base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'X-separation' / 'With_anisotropy' / 'ChiNegMap.nii',
- base_dir / 'osfstorage' / '7T' / 'X-separation' / 'ChiNegMap.nii'
- ),
- "R2*-QSM": (
- base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'R2star-QSM' / 'With_anisotropy' / 'ChiNegMap.nii',
- base_dir / 'osfstorage' / '7T' / 'R2star-QSM' / 'ChiNegMap.nii'
- ),
- "APART-QSM": (
- base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'APART-QSM' / 'With_anisotropy' / 'X_dia_abs.nii',
- base_dir / 'osfstorage' / '7T' / 'APART-QSM' / 'X_dia_abs.nii'
- ),
- "DECOMPOSE-QSM": (
- base_dir / 'osfstorage' / 'Susceptibility_Separation_Results' / 'DECOMPOSE-QSM' / 'With_anisotropy' / 'Results_DCS_abs.nii',
- base_dir / 'osfstorage' / '7T' / 'DECOMPOSE-QSM' / 'Results_DCS_abs.nii'
- ),
- }
- #-----------------------------------------------------------------
- # Load segmentation file
- #-----------------------------------------------------------------
- segmentation_file = base_dir / 'osfstorage' / 'Masks' / 'white_matter_mask.nii.gz'
- seg_nii = nib.load(segmentation_file)
- seg_data = seg_nii.get_fdata()
- labels = range(1, 12)
- method_points = {}
- all_mean_values = []
- all_difference_values = []
- for method_name, (nii_3T_path, nii_7T_path) in methods.items():
- # --------------------
- # Load the 3T and 7T
- # --------------------
- nii_3T = nib.load(nii_3T_path)
- nii_7T = nib.load(nii_7T_path)
- data_3T = nii_3T.get_fdata()*-1
- data_7T = nii_7T.get_fdata()*-1
- mean_values = np.zeros(len(labels))
- diff_values = np.zeros(len(labels))
- for i, label_val in enumerate(labels):
- # Mask for current region
- mask = (seg_data == label_val)
- # Extract the region's data for 3T and 7T
- region_3T = data_3T[mask]
- region_7T = data_7T[mask]
- # Compute the mean within that region
- mean_3T = np.mean(region_3T) if region_3T.size > 0 else np.nan
- mean_7T = np.mean(region_7T) if region_7T.size > 0 else np.nan
- # Bland-Altman points
- mean_values[i] = (mean_3T + mean_7T) / 2
- diff_values[i] = (mean_3T - mean_7T)
- # Store in dictionary
- method_points[method_name] = (mean_values, diff_values)
- # Accumulate for global axis limits
- all_mean_values.extend(mean_values)
- all_difference_values.extend(diff_values)
- # Convert to NumPy for min/max
- all_mean_values = np.array(all_mean_values)
- all_difference_values = np.array(all_difference_values)
- # Determine global x-limits and y-limits
- x_min, x_max = np.min(all_mean_values), np.max(all_mean_values)
- x_margin = 0.1 * (x_max - x_min) # 10% margin
- x_lim = (x_min - x_margin, x_max + x_margin)
- # Fixed y-limits as in your code
- y_lim = (-0.05, 0.05)
- # Create subplots
- fig, axes = plt.subplots(2, 2, figsize=(10, 8))
- axes = axes.flatten()
- for ax, (method_name, (mean_values, difference_values)) in zip(axes, method_points.items()):
- # Mean & LoA
- mean_diff = np.mean(difference_values)
- std_diff = np.std(difference_values, ddof=1)
- loa_upper = mean_diff + 1.96 * std_diff
- loa_lower = mean_diff - 1.96 * std_diff
- ax.scatter(mean_values, difference_values, marker="x", s=50)
- # Plot lines
- ax.axhline(mean_diff, color="red", linestyle="--")
- ax.axhline(loa_upper, color="green", linestyle="--")
- ax.axhline(loa_lower, color="green", linestyle="--")
- x_text = x_lim[0] + 0.05*(x_lim[1] - x_lim[0])
- y_offset = 0.005 * (y_lim[1] - y_lim[0])
- # (3) For each line, add text "above" the line value
- def place_text_above_line(line_value, label_color):
- y_text = line_value
- # Clamp y_text so it doesn't exceed the top/bottom
- y_text = max(min(y_text, y_lim[1] - 0.001), y_lim[0] + 0.0)
- ax.text(x_text, y_text, f"{line_value:.4f}", color=label_color,
- va="bottom", ha="left",)
- place_text_above_line(mean_diff, "red") # Mean difference
- place_text_above_line(loa_upper, "green") # +1.96 SD
- place_text_above_line(loa_lower, "green") # -1.96 SD
- # Labels, limits, etc.
- ax.set_xlabel(r"Mean of $\chi^{-}_{\text{3 T}}$ and $\chi^{-}_{\text{7 T}} \:(ppm)$")
- ax.set_ylabel(r"Difference $\chi^{-}_{\text{3 T}} - \chi^{-}_{\text{7 T}} \:(ppm)$")
- ax.set_title(method_name)
- ax.set_xlim(x_lim)
- ax.set_ylim(y_lim)
- plt.tight_layout()
- plt.show()
Manuscript_Figures.ipynb at commit d009ab6, under MIT · at the source
Overview
- NeuroPoly Lab, Institute of Biomedical Engineering, Polytechnique Montreal, Montreal, Quebec, Canada
- CHU Sainte‐Justine Research Center, Montreal, Quebec, Canada
- Department of Computer Engineering and Software Engineering, Polytechnique Montreal, Montreal, Quebec, Canada
- Department of Electrical Engineering, Polytechnique Montreal, Montreal, Quebec, Canada
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
d009ab67660fd6e5368d9f9811e04e19eca1636a, 17 April 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
17 files
- DataSimulation.m, MATLAB, 60 lines
- GUI.m, MATLAB, 706 lines
- Manuscript_Figures.ipynb
, Jupyter, 1,458 lines, 2 matches - PhantomCreation.m, MATLAB, 209 lines, 1 match
- func/
Anisotropy.m , MATLAB, 18 lines - func/
CalculateR2Prime.m , MATLAB, 45 lines - func/
DataSimulationFunction.m , MATLAB, 161 lines - func/
DataSimulation_GUI.m , MATLAB, 56 lines - func/
GRESimulation.m , MATLAB, 108 lines, 2 matches - func/
GenerateRgbMap.m , MATLAB, 30 lines - func/
Map_creation_3T.m , MATLAB, 35 lines - func/
Mask.m , MATLAB, 24 lines - func/
PhantomCreationFunction. , MATLAB, 204 linesm - func/
T2_simulation.m , MATLAB, 89 lines - func/
calculate_Dr.m , MATLAB, 60 lines - LICENSE, License, 21 lines
- README.md, Text, 201 lines
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://
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://
BibTeX
@article{ridani2026reali
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/
url = {https://
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/
VL - 96
IS - 5
SP - 2412
EP - 2425
SN - 0740-3194
PB - Wiley
DO - 10.1002/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1002/
"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":
"volume": "96",
"issue": "5",
"page": "2412-2425",
"DOI": "10.1002/
"PMID": "42426959",
"PMCID": "PMC13527262",
"ISSN": "0740-3194",
"publisher": "Wiley",
"URL": "https://
"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 biomedicineIn 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 biomedicineIn common: structural MRI / diffusion, 7 references
- [3] doi:10.1038/s41586-026-10631-3 [code]
- A prognostic human brain network for diffuse midline glioma.Journal: NatureIn common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 5 other tools, 1 reference
- [4] doi:10.1002/nbm.70277 [code]
- Hierarchical Bayesian Modelling Improves Microstructural Parameter Mapping in Diffusion and Exchange MRI Data.Journal: NMR in biomedicineIn common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, NiBabel, 3 other tools, computational, structural MRI / diffusion, 1 reference
- [5] 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: NeuronIn common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 5 other tools
- [6] 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 medicineIn common: NiBabel, SciPy, Matplotlib, 1 other tool, computational, 3 references
- [7] doi:10.1038/s41467-026-74215-5 [code]
- Multi-metric evaluations of acute psychedelic effects on fMRI brain entropy.Journal: Nature communicationsIn common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 4 other tools, computational
- [8] 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 mappingIn common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 4 other tools
- [9] doi:10.1111/ene.70678 [code]
- Who Falls After a Stroke? Evidence From a Prospective Stroke Cohort.Journal: European journal of neurologyIn common: Tools for NIfTI and ANALYZE image (MATLAB), Parallel Computing Toolbox, Image Processing Toolbox, 4 other tools
- [10] doi:10.1016/j.celrep.2026.117404 [code]
- Action and rest tremor map to distinct networks within the primary motor cortex.Journal: Cell reportsIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 15 scripts, and 5 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:34a7e8ad9d2242fc…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
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.
