Accurate estimation of intravoxel incoherent motion parameters based on implicit neural representation.
The 4 matches
- [1] § METHODS › Overview of the two‐stage IVIM‐INR framework ↔ train.py, lines 56–95 · score 0.76 · Xavier uniform initialization, hidden layers, ReLU, MLP, Weights, linear
- [2] § METHODS › Implementation details ↔ train.py, lines 450–490 · score 0.67 · cosine annealing learning, rate scheduling, Adam, GPU, Training, optimizer
- [3] § METHODS › Overview of the two‐stage IVIM‐INR framework ↔ train.py, lines 1–35 · score 0.60 · implicit neural representation, ReLU, IVIM INR, S2, global, S1
- [4] § METHODS › Overview of the two‐stage IVIM‐INR framework ↔ train.py, lines 56–95 · score 0.59 · layer MLP, hidden layers, ReLU, IVIM INR, SIREN, denoised
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
Python · 1,690 lines · 71 KB · no license · 4 matches
- #!/usr/bin/env python3
- """
- IVIM-INR: Two-stage implicit neural representation for IVIM parameter estimation.
- Stage 1 (S1): 4-layer ReLU MLP (512 hidden units per layer) that denoises each
- b-value image, taking as input spatial coordinates plus 3D patches
- from the other (non-target) b-value images.
- Stage 2 (S2): 4-layer SIREN (512 hidden units per layer) that maps spatial
- coordinates (plus a patch from the denoised b=0 image) to the
- IVIM parameters (D_p, D_t, F_p) and a global offset delta.
- Usage
- -----
- # Default: noisy SNR=50 brain phantom in ./data_1/SNR_50
- python ivim_fitting.py --data_dir data_1/SNR_50
- # Ground-truth (noise-free) fit
- python ivim_fitting.py --data_dir data_1 --no-use_noisy_data
- """
- import os
- import sys
- import time
- import json
- import argparse
- from typing import List, Tuple
- import numpy as np
- import nibabel as nib
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- from scipy import ndimage
- import pydicom
- from datetime import datetime
- # Configuration classes defined inline (previously from config_loader)
- # Default IVIM parameter ranges
- IVIM_PARAMETER_RANGES = {
- 'Dp': {'min': 0.001, 'max': 0.02}, # mm²/s
- 'Dt': {'min': 0.00001, 'max': 0.003}, # mm²/s
- 'Fp': {'min': 0.0, 'max': 0.5} # fraction
- }
- # ==================== Denoising Model Components ====================
- def fc_block(in_size, out_size, dropout, *args, **kwargs):
- """Fully connected block with ReLU and dropout."""
- return nn.Sequential(
- nn.Linear(in_size, out_size, *args, **kwargs),
- nn.ReLU(),
- nn.Dropout(dropout),
- )
- class MLPv1(nn.Module):
- """ReLU MLP used for DWI denoising (Stage 1 of IVIM-INR).
- num_layers counts TOTAL linear layers (input + hidden + output),
- matching the SirenNet convention. With the default num_layers=4 and
- hidden_size=512, this builds a 4-layer MLP where every hidden layer
- has 512 units (one input projection, two 512->512 hidden projections,
- and one output projection).
- """
- def __init__(self, input_size=3, hidden_size=512, output_size=1, dropout=0, num_layers=4):
- super(MLPv1, self).__init__()
- assert num_layers >= 2, "num_layers must be >= 2 (input + output)"
- self.input_size = input_size
- self.hidden_size = hidden_size
- self.output_size = output_size
- self.dropout = dropout
- self.num_layers = num_layers
- layers = [nn.Linear(input_size, hidden_size), nn.ReLU()]
- if dropout > 0:
- layers.append(nn.Dropout(dropout))
- for _ in range(num_layers - 2):
- layers.append(nn.Linear(hidden_size, hidden_size))
- layers.append(nn.ReLU())
- if dropout > 0:
- layers.append(nn.Dropout(dropout))
- layers.append(nn.Linear(hidden_size, output_size))
- # Xavier uniform initialization for all Linear layers
- for m in layers:
- if isinstance(m, nn.Linear):
- nn.init.xavier_uniform_(m.weight)
- if m.bias is not None:
- nn.init.zeros_(m.bias)
- self.net = nn.Sequential(*layers)
- def forward(self, x):
- x = x.view(-1, self.input_size)
- return self.net(x)
- class IntegratedPositionalEncoding(nn.Module):
- """Integrated Positional Encoding from MDINR."""
- def __init__(self, num_freqs: int, max_freq: float):
- super().__init__()
- self.freqs = torch.linspace(1.0, max_freq, steps=num_freqs)
- self.omega_max = max_freq
- def forward(self, x: torch.Tensor) -> torch.Tensor:
- """
- x: Tensor of shape [N, 3] (3D coordinates, normalized to [-1,1])
- Returns: Tensor of shape [N, 3 * F * 2] (concatenated sin/cos encoding)
- """
- N, D = x.shape
- device = x.device
- # Shape freqs to [1, 1, F]
- f = self.freqs.to(device).view(1, 1, -1)
- mu = x.view(N, D, 1)
- # Calculate sinc with d = π / ω_max
- d = torch.pi / self.omega_max
- sinc_arg = f * d
- sinc = torch.where(
- sinc_arg == 0,
- torch.ones_like(sinc_arg),
- torch.sin(sinc_arg) / sinc_arg
- )
- # Calculate sin and cos terms
- x_freqed = mu * f
- sin_term = torch.sin(x_freqed) * sinc
- cos_term = torch.cos(x_freqed) * sinc
- # Flatten and concatenate
- sin_term = sin_term.view(N, -1)
- cos_term = cos_term.view(N, -1)
- return torch.cat([sin_term, cos_term], dim=-1)
- # ==================== Scanner Type Detection ====================
- def detect_scanner_type(patient_id: str, date: str, dicom_base_dir: str) -> str:
- """
- Detect if the scanner is Unity (MR Linac) or MR Sim by checking DICOM header.
- Args:
- patient_id: Patient identifier
- date: Scan date
- dicom_base_dir: Base directory containing DICOM files
- Returns:
- 'unity' if Unity scanner, 'sim' if MR Sim scanner
- """
- # Find DWI directory
- patient_dir = os.path.join(dicom_base_dir, patient_id, date)
- if not os.path.exists(patient_dir):
- print(f" ⚠ Patient directory not found: {patient_dir}")
- return 'sim' # Default to sim
- # Look for DWI directories
- dwi_dirs = []
- for dirname in os.listdir(patient_dir):
- if 'DWI' in dirname.upper() and os.path.isdir(os.path.join(patient_dir, dirname)):
- dwi_dirs.append(os.path.join(patient_dir, dirname))
- if not dwi_dirs:
- print(f" ⚠ No DWI directories found in {patient_dir}")
- return 'sim' # Default to sim
- # Check first DICOM file in first DWI directory
- dwi_dir = dwi_dirs[0]
- dcm_files = [f for f in os.listdir(dwi_dir) if f.endswith('.dcm') or f.endswith('.DCM')]
- if not dcm_files:
- print(f" ⚠ No DICOM files found in {dwi_dir}")
- return 'sim' # Default to sim
- # Read first DICOM file
- dcm_path = os.path.join(dwi_dir, dcm_files[0])
- try:
- ds = pydicom.dcmread(dcm_path)
- # Check for (0008,1090) - ManufacturerModelName tag
- # Unity scanners have ManufacturerModelName = "Marlin"
- if hasattr(ds, 'ManufacturerModelName') or (0x0008, 0x1090) in ds:
- model_name = str(getattr(ds, 'ManufacturerModelName', '')).strip()
- print(f" ManufacturerModelName (0008,1090): {model_name}")
- if model_name.lower() == 'marlin':
- print(f" → Unity (MR Linac) detected")
- return 'unity'
- else:
- print(f" → MR Sim detected")
- return 'sim'
- else:
- print(f" No ManufacturerModelName tag (0008,1090) found - MR Sim detected")
- return 'sim'
- except Exception as e:
- print(f" ⚠ Error reading DICOM file {dcm_path}: {e}")
- return 'sim' # Default to sim
- # ==================== Noise Floor Detection ====================
- def detect_noise_floor_mask(b0_data: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
- """
- Detect noise floor regions in Unity data based on intensity thresholds.
- Args:
- b0_data: B0 image data
- Returns:
- background_mask: Pure background (intensity < 50)
- noise_floor_mask: Noise floor regions (50 <= intensity <= 300)
- tissue_mask: Signal regions (intensity > 300)
- """
- # Create masks based on thresholds
- background_mask = b0_data < 50
- noise_floor_mask = (b0_data >= 50) & (b0_data <= 300)
- tissue_mask = b0_data > 300
- # Get largest connected component of noise floor
- if np.any(noise_floor_mask):
- labeled, num_features = ndimage.label(noise_floor_mask)
- if num_features > 0:
- # Find largest component
- component_sizes = np.bincount(labeled.ravel())
- component_sizes[0] = 0 # Ignore background
- largest_component = component_sizes.argmax()
- # Keep only largest component as noise floor
- noise_floor_mask_clean = labeled == largest_component
- # Reassign small disconnected regions to tissue
- small_regions = noise_floor_mask & ~noise_floor_mask_clean
- tissue_mask = tissue_mask | small_regions
- noise_floor_mask = noise_floor_mask_clean
- return background_mask, noise_floor_mask, tissue_mask
- def rician_noise_correction(dwi_data: np.ndarray, noise_floor_mask: np.ndarray,
- method: str = "rayleigh_mean") -> Tuple[np.ndarray, float]:
- """
- Apply Rician noise floor correction to DWI data using noise floor regions.
- Args:
- dwi_data: DWI data array [X, Y, Z, B]
- noise_floor_mask: Binary mask indicating noise floor regions
- method: Correction method ('rayleigh_mean' or 'simple_subtraction')
- Returns:
- corrected_dwi: Corrected DWI data
- median_sigma: Estimated noise standard deviation
- """
- _, _, _, num_b = dwi_data.shape
- corrected_dwi = np.zeros_like(dwi_data)
- # Estimate noise from noise floor regions for each b-value
- sigma_values = []
- for b_idx in range(num_b):
- # Extract noise floor values
- noise_values = dwi_data[:, :, :, b_idx][noise_floor_mask]
- if len(noise_values) > 0:
- # For Rayleigh distribution: σ = mean / sqrt(π/2)
- mean_noise = np.mean(noise_values)
- sigma = mean_noise / np.sqrt(np.pi / 2)
- sigma_values.append(sigma)
- print(f" b={b_idx}: Noise floor mean={mean_noise:.3f}, σ={sigma:.3f}")
- if method == "rayleigh_mean":
- # Rician correction: S_corrected = sqrt(max(S^2 - 2σ^2, 0))
- S_squared = dwi_data[:, :, :, b_idx] ** 2
- S_corrected_squared = np.maximum(S_squared - 2 * sigma**2, 0)
- corrected_dwi[:, :, :, b_idx] = np.sqrt(S_corrected_squared)
- elif method == "simple_subtraction":
- # Simple subtraction method
- corrected_dwi[:, :, :, b_idx] = np.maximum(dwi_data[:, :, :, b_idx] - sigma, 0)
- else:
- print(f" b={b_idx}: No noise floor regions available, skipping correction")
- corrected_dwi[:, :, :, b_idx] = dwi_data[:, :, :, b_idx]
- sigma_values.append(0)
- # Use median sigma across b-values
- median_sigma = np.median(sigma_values) if sigma_values else 0
- print(f" Median σ across b-values: {median_sigma:.3f}")
- return corrected_dwi, median_sigma
- # ==================== Denoising Functions ====================
- def precompute_patch_features(noisy_dwi_padded: List[torch.Tensor], patch_coords: torch.Tensor,
- X_dim: int, Y_dim: int, Z_dim: int, num_b: int,
- device: torch.device, mask: np.ndarray = None) -> Tuple[torch.Tensor, np.ndarray]:
- """
- Precompute patch features for masked voxels and all b-values.
- Args:
- noisy_dwi_padded: List of padded DWI volumes for each b-value
- patch_coords: Patch coordinate offsets
- X_dim, Y_dim, Z_dim: Volume dimensions
- num_b: Number of b-values
- device: Device to use
- mask: Binary mask to select voxels (if None, use all voxels)
- Returns:
- all_patch_features: Tensor of shape [num_masked_voxels, num_b, patch_feature_size]
- voxel_coords: Array of voxel coordinates for masked voxels
- """
- print(" Precomputing patch features for masked voxels...")
- # Create coordinate grids
- all_indices = np.indices((X_dim, Y_dim, Z_dim))
- # Apply mask if provided
- if mask is not None:
- mask_indices = np.where(mask)
- voxel_coords = np.stack([mask_indices[0], mask_indices[1], mask_indices[2]], axis=1)
- print(f" Using mask: {len(voxel_coords):,} voxels out of {X_dim*Y_dim*Z_dim:,}")
- else:
- voxel_coords = np.stack([all_indices[0].ravel(),
- all_indices[1].ravel(),
- all_indices[2].ravel()], axis=1)
- num_voxels = len(voxel_coords)
- patch_feature_size = len(patch_coords)
- # Preallocate the feature tensor
- all_patch_features = torch.zeros((num_voxels, num_b, patch_feature_size),
- dtype=torch.float32, device=device)
- # Process in batches to manage memory
- batch_size = 5000
- for start_idx in range(0, num_voxels, batch_size):
- end_idx = min(start_idx + batch_size, num_voxels)
- batch_coords = voxel_coords[start_idx:end_idx]
- batch_coords_tensor = torch.tensor(batch_coords, dtype=torch.float32, device=device)
- # Extract patches for this batch
- # batch_coords are in original volume space, need to add padding offset
- # then add patch offsets (which already include padding)
- batch_coords_padded = batch_coords_tensor + 1 # Add padding offset to center coords
- coords_patch = batch_coords_padded[:, None, :] + patch_coords[None, :, :] - 1 # Subtract 1 because patch_coords already has padding
- x_idx = coords_patch[..., 0].reshape(-1).long()
- y_idx = coords_patch[..., 1].reshape(-1).long()
- z_idx = coords_patch[..., 2].reshape(-1).long()
- # Extract features for each b-value
- for b_idx in range(num_b):
- features = noisy_dwi_padded[b_idx][0, x_idx, y_idx, z_idx]
- features = features.reshape(len(batch_coords), -1)
- all_patch_features[start_idx:end_idx, b_idx, :] = features
- if (start_idx + batch_size) % 100000 == 0:
- print(f" Processed {end_idx}/{num_voxels} voxels")
- print(f" ✔ Precomputed patch features: shape {all_patch_features.shape}")
- print(f" Memory usage: {all_patch_features.element_size() * all_patch_features.nelement() / (1024**3):.2f} GB")
- return all_patch_features, voxel_coords
- def denoise_dwi_mdinr_optimized(dwi_data: np.ndarray, mask: np.ndarray, b_values: np.ndarray,
- args, device: torch.device, affine=None, save_intermediate=False,
- output_dir=None) -> np.ndarray:
- """
- Denoise DWI using MDINR INR model (Optimized version without DataLoader).
- Args:
- dwi_data: DWI data array [X, Y, Z, B]
- mask: Binary mask
- b_values: Array of b-values
- args: Command line arguments
- device: PyTorch device
- affine: Affine matrix for saving intermediate results (optional)
- save_intermediate: Whether to save each b-value after denoising (optional)
- output_dir: Directory for saving intermediate results (optional)
- Returns:
- denoised_dwi: Denoised DWI data
- """
- print(f"\n{'='*60}")
- print(f"DWI Denoising (MDINR INR - Optimized)")
- print(f"{'='*60}")
- X_dim, Y_dim, Z_dim, num_b = dwi_data.shape
- # Normalize DWI data by percentile
- percentiles = [np.percentile(dwi_data[:, :, :, b][mask], 99) for b in range(num_b)]
- noisy_dwi_norm = np.zeros_like(dwi_data)
- for b in range(num_b):
- noisy_dwi_norm[:, :, :, b] = dwi_data[:, :, :, b] / (percentiles[b] + 1e-6)
- # Setup positional encoding
- input_mapper = IntegratedPositionalEncoding(
- num_freqs=args.denoise_npe,
- max_freq=args.denoise_maxf
- ).to(device)
- # Convert to PyTorch tensors
- noisy_dwi_torch = [torch.from_numpy(noisy_dwi_norm[:, :, :, b]).float().unsqueeze(0).unsqueeze(0)
- for b in range(num_b)]
- # Pad volumes for patch extraction
- # For 3x3x3 patch, we need padding of 1 on each side
- pad_size = 1
- noisy_dwi_padded = [F.pad(img, (pad_size, pad_size, pad_size, pad_size, pad_size, pad_size),
- mode='constant', value=0) for img in noisy_dwi_torch]
- noisy_dwi_padded = [img.to(device)[0] for img in noisy_dwi_padded]
- # Create patch coordinates for 3x3x3 patch
- # Offsets should be [-1, 0, 1] in each dimension
- patch_offsets = [-1, 0, 1]
- patch_coords = np.array([(x, y, z) for x in patch_offsets for y in patch_offsets for z in patch_offsets])
- # Adjust for padding: add pad_size to make coordinates valid in padded volume
- patch_coords = patch_coords + pad_size
- patch_coords = torch.tensor(patch_coords, dtype=torch.float32).to(device)
- # Precompute all patch features (only for masked voxels)
- all_patch_features, masked_voxel_coords = precompute_patch_features(
- noisy_dwi_padded, patch_coords, X_dim, Y_dim, Z_dim, num_b, device, mask
- )
- # Process each b-value
- denoised_dwi = np.zeros_like(dwi_data)
- for b_idx in range(num_b):
- print(f"\nDenoising b-value {b_idx} (b={b_values[b_idx]})...")
- # Input size calculation
- # For 3x3x3 patch: 27 features per b-value, times (num_b - 1) excluding current b
- patch_feature_size = 27 * (num_b - 1)
- if args.denoise_use_coords:
- coord_encoding_size = args.denoise_npe * 2 * 3
- input_size = coord_encoding_size + patch_feature_size
- else:
- input_size = patch_feature_size
- # Create MLPv1 model
- model = MLPv1(
- input_size=input_size,
- output_size=1,
- hidden_size=args.denoise_hidden_size,
- num_layers=args.denoise_num_layers,
- dropout=args.denoise_dropout
- ).to(device)
- # Use masked voxel coordinates
- points = masked_voxel_coords.astype(np.float32)
- # Normalize coordinates
- points_normalized = np.empty_like(points, dtype=np.float32)
- points_normalized[:, 0] = 2.0 * points[:, 0] / (X_dim - 1) - 1.0
- points_normalized[:, 1] = 2.0 * points[:, 1] / (Y_dim - 1) - 1.0
- points_normalized[:, 2] = 2.0 * points[:, 2] / (Z_dim - 1) - 1.0
- # Move all data to GPU at once
- coords_normal = torch.tensor(points_normalized, dtype=torch.float32).to(device)
- coords_prior = torch.tensor(points, dtype=torch.float32).to(device)
- # Extract labels for masked voxels
- labels_masked = noisy_dwi_norm[:, :, :, b_idx][mask]
- labels = torch.tensor(labels_masked.reshape(-1, 1), dtype=torch.float32).to(device)
- if len(labels) == 0:
- continue
- # Get precomputed features for current b-value (excluding current b)
- current_b_features = torch.cat([
- all_patch_features[:, other_b, :]
- for other_b in range(num_b) if other_b != b_idx
- ], dim=1)
- # Training
- optimizer = torch.optim.Adam(model.parameters(), lr=args.denoise_lr)
- scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.denoise_epochs)
- criterion = nn.MSELoss()
- model.train()
- num_samples = len(coords_normal)
- batch_size = args.denoise_batch_size
- for epoch in range(args.denoise_epochs):
- epoch_loss = 0.0
- epoch_start_time = time.time()
- # Shuffle indices for this epoch
- indices = torch.randperm(num_samples, device=device)
- # Manual batching
- for start_idx in range(0, num_samples, batch_size):
- batch_start_time = time.time()
- end_idx = min(start_idx + batch_size, num_samples)
- batch_indices = indices[start_idx:end_idx]
- # Get batch data - already on GPU
- coords_normal_batch = coords_normal[batch_indices]
- # coords_prior_batch = coords_prior[batch_indices] # Not used in training
- label_batch = labels[batch_indices]
- patch_features_batch = current_b_features[batch_indices]
- # Build model input
- if args.denoise_use_coords:
- coords_encoded = input_mapper(coords_normal_batch)
- model_input = torch.cat([coords_encoded, patch_features_batch], dim=1)
- else:
- model_input = patch_features_batch
- # Forward pass
- pred = model(model_input)
- loss = criterion(pred, label_batch)
- # Backward pass
- optimizer.zero_grad()
- loss.backward()
- optimizer.step()
- epoch_loss += loss.item() * batch_indices.size(0)
- epoch_loss /= num_samples
- scheduler.step()
- if (epoch + 1) % 10 == 0:
- epoch_time = time.time() - epoch_start_time
- print(f" Epoch {epoch+1}/{args.denoise_epochs}, Loss: {epoch_loss:.6f}, Time: {epoch_time:.2f}s")
- # Inference
- model.eval()
- predictions = []
- with torch.no_grad():
- # Process in batches for inference
- for start_idx in range(0, num_samples, batch_size * 2): # Larger batch for inference
- end_idx = min(start_idx + batch_size * 2, num_samples)
- coords_norm_inf = coords_normal[start_idx:end_idx]
- patch_features_inf = current_b_features[start_idx:end_idx]
- # Build model input
- if args.denoise_use_coords:
- coords_encoded = input_mapper(coords_norm_inf)
- model_input = torch.cat([coords_encoded, patch_features_inf], dim=1)
- else:
- model_input = patch_features_inf
- pred = model(model_input).cpu().numpy()
- predictions.append(pred)
- # Reshape predictions - place them in the correct locations
- predictions = np.concatenate(predictions, axis=0)
- # Create full volume and place predictions at masked locations
- denoised_volume = np.zeros((X_dim, Y_dim, Z_dim), dtype=np.float32)
- for i, (x, y, z) in enumerate(masked_voxel_coords):
- denoised_volume[x, y, z] = predictions[i, 0] * percentiles[b_idx]
- denoised_dwi[:, :, :, b_idx] = denoised_volume
- # Save intermediate result if requested
- if save_intermediate and affine is not None and output_dir is not None:
- intermediate_path = os.path.join(output_dir, f'dwi_ivim_b{int(b_values[b_idx])}_denoised.nii.gz')
- if not os.path.exists(intermediate_path):
- intermediate_img = nib.Nifti1Image(denoised_volume, affine)
- nib.save(intermediate_img, intermediate_path)
- print(f" ✔ Saved intermediate denoised b={int(b_values[b_idx])} to: {intermediate_path}")
- else:
- print(f" ⏭ Skipping b={int(b_values[b_idx])} (already exists): {intermediate_path}")
- print("\n✔ Denoising completed")
- return denoised_dwi
- # ==================== Neural Network Components ====================
- class SirenLayer(nn.Module):
- """SIREN layer with sine activation."""
- def __init__(self, in_features: int, out_features: int,
- bias: bool = True, is_first: bool = False, omega_0: float = 30.0):
- super().__init__()
- self.omega_0 = omega_0
- self.is_first = is_first
- self.in_features = in_features
- self.linear = nn.Linear(in_features, out_features, bias=bias)
- self.init_weights()
- def init_weights(self):
- with torch.no_grad():
- if self.is_first:
- self.linear.weight.uniform_(-1 / self.in_features, 1 / self.in_features)
- else:
- bound = np.sqrt(6 / self.in_features) / self.omega_0
- self.linear.weight.uniform_(-bound, bound)
- def forward(self, x):
- return torch.sin(self.omega_0 * self.linear(x))
- class SirenNet(nn.Module):
- """Standard SIREN network (Sitzmann et al., NeurIPS 2020).
- num_layers counts TOTAL layers (first SIREN + hidden SIREN + final linear).
- """
- def __init__(self, input_size: int, output_size: int = 1,
- hidden_size: int = 512, num_layers: int = 4,
- first_omega_0: float = 30.0, hidden_omega_0: float = 30.0):
- super().__init__()
- assert num_layers >= 2, "num_layers must be >= 2"
- layers = []
- layers.append(SirenLayer(input_size, hidden_size,
- is_first=True, omega_0=first_omega_0))
- for _ in range(num_layers - 2):
- layers.append(SirenLayer(hidden_size, hidden_size,
- omega_0=hidden_omega_0))
- final_linear = nn.Linear(hidden_size, output_size)
- with torch.no_grad():
- bound = np.sqrt(6 / hidden_size) / hidden_omega_0
- final_linear.weight.uniform_(-bound, bound)
- layers.append(final_linear)
- self.net = nn.Sequential(*layers)
- def forward(self, x):
- return self.net(x)
- class IVIMNet(nn.Module):
- """Network for IVIM parameter estimation."""
- def __init__(self, backbone):
- super().__init__()
- self.backbone = backbone
- # Extract the output size from the last linear layer
- last_layer = backbone.net[-1]
- in_features = last_layer.in_features
- self.head = nn.Linear(in_features, 3)
- def forward(self, x):
- features = self.backbone.net[:-1](x) # All layers except last
- params = self.head(features)
- # Apply constraints to IVIM parameters using hardcoded ranges
- Dp = torch.sigmoid(params[:, 0:1]) * (IVIM_PARAMETER_RANGES['Dp']['max'] - IVIM_PARAMETER_RANGES['Dp']['min']) + IVIM_PARAMETER_RANGES['Dp']['min']
- Dt = torch.sigmoid(params[:, 1:2]) * (IVIM_PARAMETER_RANGES['Dt']['max'] - IVIM_PARAMETER_RANGES['Dt']['min']) + IVIM_PARAMETER_RANGES['Dt']['min']
- Fp = torch.sigmoid(params[:, 2:3]) * (IVIM_PARAMETER_RANGES['Fp']['max'] - IVIM_PARAMETER_RANGES['Fp']['min']) + IVIM_PARAMETER_RANGES['Fp']['min']
- return Dp, Dt, Fp
- class IVIMNetSoftConstraint(nn.Module):
- """Network for IVIM parameter estimation with soft b0 constraint."""
- def __init__(self, backbone, delta_min=-0.1, delta_max=0.1):
- super().__init__()
- self.backbone = backbone
- self.delta_min = delta_min
- self.delta_max = delta_max
- # Extract the output size from the last linear layer
- last_layer = backbone.net[-1]
- in_features = last_layer.in_features
- self.head = nn.Linear(in_features, 4) # Output 4 parameters: Dp, Dt, Fp, delta
- def forward(self, x):
- features = self.backbone.net[:-1](x) # All layers except last
- params = self.head(features)
- # Apply constraints to IVIM parameters using hardcoded ranges
- Dp = torch.sigmoid(params[:, 0:1]) * (IVIM_PARAMETER_RANGES['Dp']['max'] - IVIM_PARAMETER_RANGES['Dp']['min']) + IVIM_PARAMETER_RANGES['Dp']['min']
- Dt = torch.sigmoid(params[:, 1:2]) * (IVIM_PARAMETER_RANGES['Dt']['max'] - IVIM_PARAMETER_RANGES['Dt']['min']) + IVIM_PARAMETER_RANGES['Dt']['min']
- Fp = torch.sigmoid(params[:, 2:3]) * (IVIM_PARAMETER_RANGES['Fp']['max'] - IVIM_PARAMETER_RANGES['Fp']['min']) + IVIM_PARAMETER_RANGES['Fp']['min']
- delta = torch.tanh(params[:, 3:4]) * self.delta_max # tanh to get symmetric range [-delta_max, delta_max]
- return Dp, Dt, Fp, delta
- # ADCNet class removed - only IVIM fitting is performed
- # ==================== Signal Models ====================
- def compute_ivim_signal(Dp, Dt, Fp, b_vals):
- """Compute IVIM signal: Fp·exp(-b·Dp) + (1-Fp)·exp(-b·Dt)"""
- return Fp * torch.exp(-b_vals * Dp) + (1 - Fp) * torch.exp(-b_vals * Dt)
- def compute_ivim_signal_soft(Dp, Dt, Fp, delta, b_vals):
- """Compute IVIM signal with soft constraint: [Fp·exp(-b·Dp) + (1-Fp)·exp(-b·Dt)] + δ"""
- signal = Fp * torch.exp(-b_vals * Dp) + (1 - Fp) * torch.exp(-b_vals * Dt)
- return signal + delta
- # compute_adc_signal function removed - only IVIM fitting is performed
- # ==================== Parameter Fitting Functions ====================
- def fit_ivim_parameters(dwi_data: np.ndarray, mask: np.ndarray, b_values: np.ndarray,
- args, device: torch.device) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
- """
- Fit IVIM parameters using spatial SIREN network.
- Args:
- dwi_data: DWI data array [X, Y, Z, B]
- mask: Binary mask for fitting
- b_values: Array of b-values
- args: Command line arguments
- device: PyTorch device
- Returns:
- Dp_map, Dt_map, Fp_map: IVIM parameter maps
- """
- print(f"\n{'='*60}")
- print(f"IVIM Parameter Fitting")
- print(f"{'='*60}")
- # Filter b-values for IVIM based on hardcoded thresholds
- # Use b <= b_value_threshold and exclude b < min_b_value_threshold (except b=0)
- b_value_threshold = 1000 # Default threshold
- min_b_value_threshold = 10 # Default minimum threshold
- ivim_indices = np.where(
- (b_values <= b_value_threshold) &
- ((b_values >= min_b_value_threshold) | (b_values == 0))
- )[0]
- ivim_b_values = b_values[ivim_indices]
- ivim_dwi_data = dwi_data[:, :, :, ivim_indices]
- print(f" Using b-values <= {b_value_threshold} and >= {min_b_value_threshold} (except b=0) for IVIM fitting")
- print(f" Selected b-values: {ivim_b_values}")
- print(f" Number of b-values for IVIM: {len(ivim_b_values)}")
- X_dim, Y_dim, Z_dim, num_b_ivim = ivim_dwi_data.shape
- # Normalize by b0
- b0_expanded = ivim_dwi_data[:, :, :, 0:1]
- dwi_norm = np.clip(ivim_dwi_data / (b0_expanded + 1e-6), 0, 1)
- # Normalize B0 by its own maximum for use as additional input
- if args.ivim_use_b0_signal:
- b0_max = np.max(ivim_dwi_data[:, :, :, 0])
- b0_norm_by_max = ivim_dwi_data[:, :, :, 0] / (b0_max + 1e-6)
- print(f" B0 max value: {b0_max:.2f}, will use normalized B0 as additional input")
- # Prepare B0 for patch extraction if patch size > 1
- if args.ivim_b0_patch_size > 1:
- print(f" Using B0 patch size: {args.ivim_b0_patch_size}x{args.ivim_b0_patch_size}x{args.ivim_b0_patch_size}")
- # Convert to tensor and pad
- b0_tensor = torch.from_numpy(b0_norm_by_max).float().unsqueeze(0).unsqueeze(0)
- pad_size = args.ivim_b0_patch_size // 2
- b0_padded = F.pad(b0_tensor, (pad_size, pad_size, pad_size, pad_size, pad_size, pad_size),
- mode='constant', value=0)
- b0_padded = b0_padded.to(device)[0, 0] # Remove batch and channel dims
- # Create patch coordinates for B0
- patch_offsets = list(range(-pad_size, pad_size + 1))
- b0_patch_coords = np.array([(x, y, z) for x in patch_offsets for y in patch_offsets for z in patch_offsets])
- b0_patch_coords = b0_patch_coords + pad_size # Adjust for padding
- b0_patch_coords = torch.tensor(b0_patch_coords, dtype=torch.float32).to(device)
- b0_patch_size = len(b0_patch_coords)
- else:
- b0_patch_size = 1
- # Create coordinate grids
- X = np.arange(0, X_dim, 1)
- Y = np.arange(0, Y_dim, 1)
- Z = np.arange(0, Z_dim, 1)
- points = np.meshgrid(X, Y, Z, indexing='ij')
- points = np.stack(points).transpose(1, 2, 3, 0).reshape(-1, 3).astype(np.float32)
- # Normalize coordinates to [-1, 1]
- points_normalized = np.empty_like(points, dtype=np.float32)
- points_normalized[:, 0] = 2.0 * points[:, 0] / (X_dim - 1) - 1.0
- points_normalized[:, 1] = 2.0 * points[:, 1] / (Y_dim - 1) - 1.0
- points_normalized[:, 2] = 2.0 * points[:, 2] / (Z_dim - 1) - 1.0
- # Create IVIM model
- # Determine input size based on additional inputs
- input_size = 3 # Start with 3D coordinates
- if args.ivim_use_dwi_signals:
- input_size += num_b_ivim # Add normalized DWI signals
- print(f" Using DWI signals as additional input")
- if args.ivim_use_b0_signal:
- if args.ivim_b0_patch_size > 1:
- input_size += b0_patch_size # Add B0 patch features
- print(f" Using B0 patch features as additional input (size: {b0_patch_size})")
- else:
- input_size += 1 # Add single B0 value
- print(f" Using normalized B0 signal as additional input")
- print(f" Total input size: {input_size}")
- backbone = SirenNet(
- input_size=input_size,
- output_size=args.ivim_hidden_size,
- hidden_size=args.ivim_hidden_size,
- num_layers=args.ivim_num_layers,
- first_omega_0=args.ivim_first_omega_0,
- hidden_omega_0=args.ivim_hidden_omega_0,
- )
- model = IVIMNet(backbone).to(device)
- # Prepare data and move to GPU
- mask_flat = mask.reshape(-1)
- print(f" Mask voxels for fitting: {mask_flat.sum():,}")
- # Move masked data to GPU directly
- coords_tensor = torch.tensor(points_normalized[mask_flat], dtype=torch.float32).to(device)
- dwi_signals = torch.tensor(dwi_norm.reshape(-1, num_b_ivim)[mask_flat], dtype=torch.float32).to(device)
- if args.ivim_use_b0_signal:
- if args.ivim_b0_patch_size > 1:
- # Extract B0 patches for masked voxels
- print(f" Extracting B0 patches for {mask_flat.sum():,} masked voxels...")
- voxel_indices = np.where(mask_flat)[0]
- voxel_coords_full = points[voxel_indices]
- # Preallocate B0 patch features
- b0_patch_features = torch.zeros((len(voxel_coords_full), b0_patch_size), dtype=torch.float32, device=device)
- # Extract patches in batches
- batch_size = 5000
- for start_idx in range(0, len(voxel_coords_full), batch_size):
- end_idx = min(start_idx + batch_size, len(voxel_coords_full))
- batch_coords = voxel_coords_full[start_idx:end_idx]
- batch_coords_tensor = torch.tensor(batch_coords, dtype=torch.float32, device=device)
- # Extract B0 patches
- batch_coords_padded = batch_coords_tensor + pad_size
- coords_patch = batch_coords_padded[:, None, :] + b0_patch_coords[None, :, :] - pad_size
- x_idx = coords_patch[..., 0].reshape(-1).long()
- y_idx = coords_patch[..., 1].reshape(-1).long()
- z_idx = coords_patch[..., 2].reshape(-1).long()
- features = b0_padded[x_idx, y_idx, z_idx]
- features = features.reshape(len(batch_coords), -1)
- b0_patch_features[start_idx:end_idx, :] = features
- b0_signals = b0_patch_features
- else:
- b0_signals = torch.tensor(b0_norm_by_max.reshape(-1)[mask_flat], dtype=torch.float32).to(device).unsqueeze(1)
- # Exclude b0 from fitting (only use b>0)
- b_vals_t = torch.tensor(ivim_b_values[1:], device=device, dtype=torch.float32)
- # Training
- optimizer = torch.optim.Adam(model.parameters(), lr=args.ivim_lr)
- scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.ivim_epochs)
- criterion = nn.MSELoss()
- model.train()
- print(f"\nTraining for {args.ivim_epochs} epochs...")
- num_samples = len(coords_tensor)
- batch_size = args.ivim_batch_size
- for epoch in range(args.ivim_epochs):
- epoch_loss = 0.0
- epoch_start_time = time.time()
- # Shuffle indices for this epoch
- indices = torch.randperm(num_samples, device=device)
- # Manual batching
- for start_idx in range(0, num_samples, batch_size):
- end_idx = min(start_idx + batch_size, num_samples)
- batch_indices = indices[start_idx:end_idx]
- # Get batch data - already on GPU
- coords_batch = coords_tensor[batch_indices]
- signals_batch = dwi_signals[batch_indices]
- # Prepare model input
- model_input = coords_batch
- if args.ivim_use_dwi_signals:
- # Concatenate DWI signals
- model_input = torch.cat([model_input, signals_batch], dim=1)
- if args.ivim_use_b0_signal:
- # Concatenate B0 signal/patches
- b0_batch = b0_signals[batch_indices]
- model_input = torch.cat([model_input, b0_batch], dim=1)
- # Forward pass
- Dp, Dt, Fp = model(model_input)
- # Compute predicted signals (excluding b0)
- pred_signals = compute_ivim_signal(Dp, Dt, Fp, b_vals_t.unsqueeze(0))
- # Loss on normalized signals (excluding b0)
- loss = criterion(pred_signals, signals_batch[:, 1:])
- # Backward pass
- optimizer.zero_grad()
- loss.backward()
- torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
- optimizer.step()
- epoch_loss += loss.item() * batch_indices.size(0)
- epoch_loss /= num_samples
- scheduler.step()
- if (epoch + 1) % 10 == 0 or epoch == 0:
- epoch_time = time.time() - epoch_start_time
- print(f" Epoch {epoch+1}/{args.ivim_epochs}, Loss: {epoch_loss:.6f}, Time: {epoch_time:.2f}s")
- # Inference
- print("\nRunning inference...")
- model.eval()
- # Prepare full volume coordinates for inference
- coords_full = torch.tensor(points_normalized, dtype=torch.float32).to(device)
- nvox = coords_full.shape[0]
- step = 50000
- all_Dp, all_Dt, all_Fp = [], [], []
- with torch.no_grad():
- for i in range(0, nvox, step):
- coords_batch = coords_full[i:i+step]
- # Prepare model input for inference
- model_input = coords_batch
- if args.ivim_use_dwi_signals:
- # Get corresponding DWI signals for this batch
- dwi_signals_batch = torch.tensor(dwi_norm.reshape(-1, num_b_ivim)[i:i+step], dtype=torch.float32).to(device)
- model_input = torch.cat([model_input, dwi_signals_batch], dim=1)
- if args.ivim_use_b0_signal:
- if args.ivim_b0_patch_size > 1:
- # Extract B0 patches for inference batch
- batch_coords = points[i:i+step]
- batch_coords_tensor = torch.tensor(batch_coords, dtype=torch.float32, device=device)
- # Extract B0 patches
- batch_coords_padded = batch_coords_tensor + pad_size
- coords_patch = batch_coords_padded[:, None, :] + b0_patch_coords[None, :, :] - pad_size
- x_idx = coords_patch[..., 0].reshape(-1).long()
- y_idx = coords_patch[..., 1].reshape(-1).long()
- z_idx = coords_patch[..., 2].reshape(-1).long()
- features = b0_padded[x_idx, y_idx, z_idx]
- b0_signals_batch = features.reshape(coords_batch.shape[0], -1)
- else:
- # Get single B0 values
- b0_signals_batch = torch.tensor(b0_norm_by_max.reshape(-1)[i:i+step], dtype=torch.float32).to(device).unsqueeze(1)
- model_input = torch.cat([model_input, b0_signals_batch], dim=1)
- Dp, Dt, Fp = model(model_input)
- all_Dp.append(Dp.cpu().numpy())
- all_Dt.append(Dt.cpu().numpy())
- all_Fp.append(Fp.cpu().numpy())
- # Reshape to image dimensions and apply mask
- Dp_map = np.concatenate(all_Dp).reshape(X_dim, Y_dim, Z_dim) * mask
- Dt_map = np.concatenate(all_Dt).reshape(X_dim, Y_dim, Z_dim) * mask
- Fp_map = np.concatenate(all_Fp).reshape(X_dim, Y_dim, Z_dim) * mask
- print("✔ IVIM fitting completed")
- return Dp_map, Dt_map, Fp_map
- # fit_adc_parameters function removed - only IVIM fitting is performed
- def fit_ivim_parameters_soft(dwi_data: np.ndarray, mask: np.ndarray, b_values: np.ndarray,
- args, device: torch.device) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
- """
- Fit IVIM parameters using spatial SIREN network with soft b0 constraint.
- Args:
- dwi_data: DWI data array [X, Y, Z, B]
- mask: Binary mask for fitting
- b_values: Array of b-values
- args: Command line arguments
- device: PyTorch device
- fitting_config: Configuration object
- Returns:
- Dp_map, Dt_map, Fp_map, delta_map: IVIM parameter maps and delta offset map
- """
- print(f"\n{'='*60}")
- print(f"IVIM Parameter Fitting with Soft b0 Constraint")
- print(f"{'='*60}")
- # Filter b-values for IVIM based on hardcoded thresholds
- b_value_threshold = 1000 # Default threshold
- min_b_value_threshold = 10 # Default minimum threshold
- ivim_indices = np.where(
- (b_values <= b_value_threshold) &
- ((b_values >= min_b_value_threshold) | (b_values == 0))
- )[0]
- ivim_b_values = b_values[ivim_indices]
- ivim_dwi_data = dwi_data[:, :, :, ivim_indices]
- print(f" Using b-values <= {b_value_threshold} and >= {min_b_value_threshold} (except b=0) for IVIM fitting")
- print(f" Selected b-values: {ivim_b_values}")
- print(f" Number of b-values for IVIM: {len(ivim_b_values)}")
- X_dim, Y_dim, Z_dim, num_b_ivim = ivim_dwi_data.shape
- # Normalize by b0 (soft constraint approach)
- b0_expanded = ivim_dwi_data[:, :, :, 0:1]
- dwi_norm = np.clip(ivim_dwi_data / (b0_expanded + 1e-6), 0, 2) # Allow some range above 1
- print(f"\n DWI normalization for IVIM fitting:")
- print(f" B0 range: [{b0_expanded[mask].min():.2f}, {b0_expanded[mask].max():.2f}]")
- for i, b in enumerate(ivim_b_values):
- print(f" b={b}: normalized range = [{dwi_norm[:,:,:,i][mask].min():.4f}, {dwi_norm[:,:,:,i][mask].max():.4f}]")
- # Normalize B0 by its own maximum for use as additional input
- if args.ivim_use_b0_signal:
- b0_max = np.max(ivim_dwi_data[:, :, :, 0])
- b0_norm_by_max = ivim_dwi_data[:, :, :, 0] / (b0_max + 1e-6)
- print(f" B0 max value: {b0_max:.2f}, will use normalized B0 as additional input")
- # Prepare B0 for patch extraction if patch size > 1
- if args.ivim_b0_patch_size > 1:
- print(f" Using B0 patch size: {args.ivim_b0_patch_size}x{args.ivim_b0_patch_size}x{args.ivim_b0_patch_size}")
- # Convert to tensor and pad
- b0_tensor = torch.from_numpy(b0_norm_by_max).float().unsqueeze(0).unsqueeze(0)
- pad_size = args.ivim_b0_patch_size // 2
- b0_padded = F.pad(b0_tensor, (pad_size, pad_size, pad_size, pad_size, pad_size, pad_size),
- mode='constant', value=0)
- b0_padded = b0_padded.to(device)[0, 0] # Remove batch and channel dims
- # Create patch coordinates for B0
- patch_offsets = list(range(-pad_size, pad_size + 1))
- b0_patch_coords = np.array([(x, y, z) for x in patch_offsets for y in patch_offsets for z in patch_offsets])
- b0_patch_coords = b0_patch_coords + pad_size # Adjust for padding
- b0_patch_coords = torch.tensor(b0_patch_coords, dtype=torch.float32).to(device)
- b0_patch_size = len(b0_patch_coords)
- else:
- b0_patch_size = 1
- # Create coordinate grids
- X = np.arange(0, X_dim, 1)
- Y = np.arange(0, Y_dim, 1)
- Z = np.arange(0, Z_dim, 1)
- points = np.meshgrid(X, Y, Z, indexing='ij')
- points = np.stack(points).transpose(1, 2, 3, 0).reshape(-1, 3).astype(np.float32)
- # Normalize coordinates to [-1, 1]
- points_normalized = np.empty_like(points, dtype=np.float32)
- points_normalized[:, 0] = 2.0 * points[:, 0] / (X_dim - 1) - 1.0
- points_normalized[:, 1] = 2.0 * points[:, 1] / (Y_dim - 1) - 1.0
- points_normalized[:, 2] = 2.0 * points[:, 2] / (Z_dim - 1) - 1.0
- # Create IVIM model with soft constraint
- # Determine input size based on additional inputs
- input_size = 3 # Start with 3D coordinates
- if args.ivim_use_dwi_signals:
- input_size += num_b_ivim # Add normalized DWI signals
- print(f" Using DWI signals as additional input")
- if args.ivim_use_b0_signal:
- if args.ivim_b0_patch_size > 1:
- input_size += b0_patch_size # Add B0 patch features
- print(f" Using B0 patch features as additional input (size: {b0_patch_size})")
- else:
- input_size += 1 # Add single B0 value
- print(f" Using normalized B0 signal as additional input")
- print(f" Total input size: {input_size}")
- backbone = SirenNet(
- input_size=input_size,
- output_size=args.ivim_hidden_size,
- hidden_size=args.ivim_hidden_size,
- num_layers=args.ivim_num_layers,
- first_omega_0=args.ivim_first_omega_0,
- hidden_omega_0=args.ivim_hidden_omega_0,
- )
- # Use soft constraint model
- model = IVIMNetSoftConstraint(
- backbone,
- delta_min=args.delta_min,
- delta_max=args.delta_max
- ).to(device)
- print(f"\n Delta (offset) constraint: [{args.delta_min:.2f}, {args.delta_max:.2f}]")
- print(f" This allows the curve to deviate from exactly passing through (0,1)")
- print(f" Delta regularization weight: {args.delta_regularization}")
- # Prepare data and move to GPU
- mask_flat = mask.reshape(-1)
- print(f" Mask voxels for fitting: {mask_flat.sum():,}")
- # Move masked data to GPU directly
- coords_tensor = torch.tensor(points_normalized[mask_flat], dtype=torch.float32).to(device)
- dwi_signals = torch.tensor(dwi_norm.reshape(-1, num_b_ivim)[mask_flat], dtype=torch.float32).to(device)
- if args.ivim_use_b0_signal:
- if args.ivim_b0_patch_size > 1:
- # Extract B0 patches for masked voxels
- print(f" Extracting B0 patches for {mask_flat.sum():,} masked voxels...")
- voxel_indices = np.where(mask_flat)[0]
- voxel_coords_full = points[voxel_indices]
- # Preallocate B0 patch features
- b0_patch_features = torch.zeros((len(voxel_coords_full), b0_patch_size), dtype=torch.float32, device=device)
- # Extract patches in batches
- batch_size = 5000
- for start_idx in range(0, len(voxel_coords_full), batch_size):
- end_idx = min(start_idx + batch_size, len(voxel_coords_full))
- batch_coords = voxel_coords_full[start_idx:end_idx]
- batch_coords_tensor = torch.tensor(batch_coords, dtype=torch.float32, device=device)
- # Extract B0 patches
- batch_coords_padded = batch_coords_tensor + pad_size
- coords_patch = batch_coords_padded[:, None, :] + b0_patch_coords[None, :, :] - pad_size
- x_idx = coords_patch[..., 0].reshape(-1).long()
- y_idx = coords_patch[..., 1].reshape(-1).long()
- z_idx = coords_patch[..., 2].reshape(-1).long()
- features = b0_padded[x_idx, y_idx, z_idx]
- features = features.reshape(len(batch_coords), -1)
- b0_patch_features[start_idx:end_idx, :] = features
- b0_signals = b0_patch_features
- else:
- b0_signals = torch.tensor(b0_norm_by_max.reshape(-1)[mask_flat], dtype=torch.float32).to(device).unsqueeze(1)
- # Include ALL b-values for fitting with soft constraint (including b0)
- b_vals_t = torch.tensor(ivim_b_values, device=device, dtype=torch.float32)
- # Training
- optimizer = torch.optim.Adam(model.parameters(), lr=args.ivim_lr)
- scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.ivim_epochs)
- model.train()
- print(f"\nTraining for {args.ivim_epochs} epochs...")
- num_samples = len(coords_tensor)
- batch_size = args.ivim_batch_size
- for epoch in range(args.ivim_epochs):
- epoch_loss = 0.0
- epoch_start_time = time.time()
- # Shuffle indices for this epoch
- indices = torch.randperm(num_samples, device=device)
- # Manual batching
- for start_idx in range(0, num_samples, batch_size):
- end_idx = min(start_idx + batch_size, num_samples)
- batch_indices = indices[start_idx:end_idx]
- # Get batch data - already on GPU
- coords_batch = coords_tensor[batch_indices]
- signals_batch = dwi_signals[batch_indices]
- # Prepare model input
- model_input = coords_batch
- if args.ivim_use_dwi_signals:
- # Concatenate DWI signals
- model_input = torch.cat([model_input, signals_batch], dim=1)
- if args.ivim_use_b0_signal:
- # Concatenate B0 signal/patches
- b0_batch = b0_signals[batch_indices]
- model_input = torch.cat([model_input, b0_batch], dim=1)
- # Forward pass
- Dp, Dt, Fp, delta = model(model_input)
- # Compute predicted signals (including b0 with soft constraint)
- pred_signals = compute_ivim_signal_soft(Dp, Dt, Fp, delta, b_vals_t.unsqueeze(0))
- # Signal fitting loss
- signal_loss = F.mse_loss(pred_signals, signals_batch)
- # Delta regularization loss (L2 penalty on offset)
- delta_reg_loss = args.delta_regularization * (delta**2).mean()
- # Total loss
- loss = signal_loss + delta_reg_loss
- # Backward pass
- optimizer.zero_grad()
- loss.backward()
- torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
- optimizer.step()
- epoch_loss += loss.item() * batch_indices.size(0)
- epoch_loss /= num_samples
- scheduler.step()
- if (epoch + 1) % 10 == 0 or epoch == 0:
- epoch_time = time.time() - epoch_start_time
- print(f" Epoch {epoch+1}/{args.ivim_epochs}, Loss: {epoch_loss:.6f}, Time: {epoch_time:.2f}s")
- # Inference
- print("\nRunning inference...")
- model.eval()
- # Prepare full volume coordinates for inference
- coords_full = torch.tensor(points_normalized, dtype=torch.float32).to(device)
- nvox = coords_full.shape[0]
- step = 50000
- all_Dp, all_Dt, all_Fp, all_delta = [], [], [], []
- with torch.no_grad():
- for i in range(0, nvox, step):
- coords_batch = coords_full[i:i+step]
- # Prepare model input for inference
- model_input = coords_batch
- if args.ivim_use_dwi_signals:
- # Get corresponding DWI signals for this batch
- dwi_signals_batch = torch.tensor(dwi_norm.reshape(-1, num_b_ivim)[i:i+step], dtype=torch.float32).to(device)
- model_input = torch.cat([model_input, dwi_signals_batch], dim=1)
- if args.ivim_use_b0_signal:
- if args.ivim_b0_patch_size > 1:
- # Extract B0 patches for inference batch
- batch_coords = points[i:i+step]
- batch_coords_tensor = torch.tensor(batch_coords, dtype=torch.float32, device=device)
- # Extract B0 patches
- batch_coords_padded = batch_coords_tensor + pad_size
- coords_patch = batch_coords_padded[:, None, :] + b0_patch_coords[None, :, :] - pad_size
- x_idx = coords_patch[..., 0].reshape(-1).long()
- y_idx = coords_patch[..., 1].reshape(-1).long()
- z_idx = coords_patch[..., 2].reshape(-1).long()
- features = b0_padded[x_idx, y_idx, z_idx]
- b0_signals_batch = features.reshape(coords_batch.shape[0], -1)
- else:
- # Get single B0 values
- b0_signals_batch = torch.tensor(b0_norm_by_max.reshape(-1)[i:i+step], dtype=torch.float32).to(device).unsqueeze(1)
- model_input = torch.cat([model_input, b0_signals_batch], dim=1)
- Dp, Dt, Fp, delta = model(model_input)
- all_Dp.append(Dp.cpu().numpy())
- all_Dt.append(Dt.cpu().numpy())
- all_Fp.append(Fp.cpu().numpy())
- all_delta.append(delta.cpu().numpy())
- # Reshape to image dimensions and apply mask
- Dp_map = np.concatenate(all_Dp).reshape(X_dim, Y_dim, Z_dim) * mask
- Dt_map = np.concatenate(all_Dt).reshape(X_dim, Y_dim, Z_dim) * mask
- Fp_map = np.concatenate(all_Fp).reshape(X_dim, Y_dim, Z_dim) * mask
- delta_map = np.concatenate(all_delta).reshape(X_dim, Y_dim, Z_dim) * mask
- print("\n IVIM parameter statistics:")
- print(f" Dp range: [{Dp_map[mask].min():.6f}, {Dp_map[mask].max():.6f}] mm²/s")
- print(f" Dt range: [{Dt_map[mask].min():.6f}, {Dt_map[mask].max():.6f}] mm²/s")
- print(f" Fp range: [{Fp_map[mask].min():.4f}, {Fp_map[mask].max():.4f}]")
- print(f" Delta range: [{delta_map[mask].min():.4f}, {delta_map[mask].max():.4f}]")
- print(f" Mean delta: {delta_map[mask].mean():.4f} (mean absolute deviation: {np.abs(delta_map[mask]).mean():.4f})")
- print("✔ IVIM fitting with soft b0 constraint completed")
- return Dp_map, Dt_map, Fp_map, delta_map
- # ==================== Main Processing Function ====================
- def load_ivim_data(args):
- """Load IVIM data from specified directory."""
- print(f"\n{'#'*80}")
- print(f"Loading IVIM data")
- print(f"{'#'*80}")
- # Build paths for IVIM data files
- data_dir = args.data_dir if args.data_dir else args.input_dir
- # Check if we should load noisy or ground truth data
- if args.use_noisy_data:
- data_subdir = os.path.join(data_dir, 'noisy')
- print(f"Loading noisy IVIM data from: {data_subdir}")
- else:
- data_subdir = os.path.join(data_dir, 'ground_truth')
- print(f"Loading ground truth IVIM data from: {data_subdir}")
- # If subdirectory doesn't exist, fall back to main directory
- if not os.path.exists(data_subdir):
- data_subdir = data_dir
- print(f"Subdirectory not found, using main directory: {data_dir}")
- # Define b-values and corresponding files
- b_values = [0, 50, 100, 200, 400, 600, 800, 1000]
- dwi_files = {}
- for b_val in b_values:
- filename = f'dwi_ivim_b{b_val}.nii.gz'
- filepath = os.path.join(data_subdir, filename)
- if os.path.exists(filepath):
- dwi_files[b_val] = filepath
- else:
- print(f" ⚠ File not found: {filepath}")
- if not dwi_files:
- print(f" ⚠ No IVIM files found in {data_subdir}")
- return None, None, None
- # Sort by b-value
- b_values = sorted(dwi_files.keys())
- print(f"Found {len(b_values)} b-values: {b_values}")
- # Load DWI data
- dwi_data = []
- for b_val in b_values:
- img = nib.load(dwi_files[b_val])
- dwi_data.append(img.get_fdata().astype(np.float32))
- affine = img.affine
- dwi_data = np.stack(dwi_data, axis=-1) # Shape: [X, Y, Z, B]
- b_values_array = np.array(b_values, dtype=np.float32)
- print(f"DWI data shape: {dwi_data.shape}")
- return dwi_data, b_values_array, affine
- def process_ivim_fitting(args):
- """Process IVIM parameter fitting."""
- print(f"\n{'#'*80}")
- print(f"IVIM parameter fitting")
- print(f"{'#'*80}")
- # This function has been replaced by process_ivim
- # ADC fitting removed - only IVIM fitting is performed
- def process_ivim(args):
- """Process IVIM data (both loading and fitting)."""
- # Validate output directory and files before processing
- print(f"\nValidating output directory and file permissions...")
- # Check if output directory exists, create if needed
- try:
- os.makedirs(args.output_dir, exist_ok=True)
- except Exception as e:
- sys.exit(f"Error: Cannot create output directory {args.output_dir}: {e}")
- # Check write permissions
- test_file = os.path.join(args.output_dir, '.test_write_permission')
- try:
- with open(test_file, 'w') as f:
- f.write('test')
- os.remove(test_file)
- except Exception as e:
- sys.exit(f"Error: Cannot write to output directory {args.output_dir}: {e}")
- # Define expected output files
- if args.use_soft_constraint:
- output_files = ['Dp.nii.gz', 'Dt.nii.gz', 'Fp.nii.gz', 'delta.nii.gz', 'fitting_mask.nii.gz', 'parameter_config.json']
- else:
- output_files = ['Dp.nii.gz', 'Dt.nii.gz', 'Fp.nii.gz', 'fitting_mask.nii.gz', 'parameter_config.json']
- # Check if output files already exist
- existing_files = []
- for filename in output_files:
- filepath = os.path.join(args.output_dir, filename)
- if os.path.exists(filepath):
- existing_files.append(filename)
- if existing_files and not args.force:
- print(f"\n⚠ Warning: The following output files already exist:")
- for f in existing_files:
- print(f" - {f}")
- return
- # Check subdirectories if needed
- if args.use_noisy_data and not args.skip_denoise:
- denoised_dir = os.path.join(args.output_dir, 'denoised')
- try:
- os.makedirs(denoised_dir, exist_ok=True)
- except Exception as e:
- sys.exit(f"Error: Cannot create denoised directory {denoised_dir}: {e}")
- if args.apply_noise_correction and args.use_noisy_data:
- noise_corrected_dir = os.path.join(args.output_dir, 'noise_corrected')
- try:
- os.makedirs(noise_corrected_dir, exist_ok=True)
- except Exception as e:
- sys.exit(f"Error: Cannot create noise corrected directory {noise_corrected_dir}: {e}")
- print("✔ Output directory validation completed\n")
- # Load IVIM data
- dwi_data, b_values_array, affine = load_ivim_data(args)
- if dwi_data is None:
- return
- # Create fitting mask from b0
- b0_data = dwi_data[:, :, :, 0]
- # Load brain mask directly from data_1 directory (or the specific data directory being used)
- # Use the input_dir or data_dir to construct mask path
- base_data_dir = args.data_dir if args.data_dir else args.input_dir
- # Extract the base directory (data_1, data_2, or data_3)
- mask_path = os.path.join(os.path.dirname(base_data_dir) if 'SNR_' in base_data_dir or 'noisy' in base_data_dir or 'ground_truth' in base_data_dir else base_data_dir, 'brain_mask.nii.gz')
- # Try multiple possible mask locations
- possible_mask_paths = [
- mask_path,
- os.path.join(base_data_dir, 'brain_mask.nii.gz'),
- os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data_1', 'brain_mask.nii.gz'),
- ]
- fitting_mask = None
- for mp in possible_mask_paths:
- if os.path.exists(mp):
- mask_img = nib.load(mp)
- fitting_mask = mask_img.get_fdata().astype(bool)
- print(f"\nLoaded brain mask from: {mp}")
- break
- if fitting_mask is None:
- # Fallback to creating mask from b0 data
- fitting_mask = b0_data > args.background_threshold
- print(f"\nWarning: Brain mask not found, using b0 threshold: {args.background_threshold}")
- print(f"\nFitting mask coverage: {fitting_mask.sum() / fitting_mask.size * 100:.1f}%")
- # Apply noise correction first if requested and using noisy data
- if args.apply_noise_correction and args.use_noisy_data:
- print("\nApplying noise floor correction to noisy data...")
- # Detect noise floor regions from original b0
- background_mask, noise_floor_mask, tissue_mask = detect_noise_floor_mask(b0_data)
- # Apply Rician noise correction to original noisy data
- dwi_data_corrected, estimated_sigma = rician_noise_correction(
- dwi_data, noise_floor_mask, method=args.noise_correction_method
- )
- # Save noise corrected DWI data to separate folder
- noise_corrected_dir = os.path.join(args.output_dir, 'noise_corrected')
- os.makedirs(noise_corrected_dir, exist_ok=True)
- # Save the noise-corrected data
- for i, b_val in enumerate(b_values_array):
- nib.save(nib.Nifti1Image(dwi_data_corrected[:, :, :, i].astype(np.float32), affine),
- os.path.join(noise_corrected_dir, f'dwi_ivim_b{int(b_val)}_noise_corrected.nii.gz'))
- # Use noise corrected data for subsequent processing
- dwi_data = dwi_data_corrected
- b0_data = dwi_data[:, :, :, 0]
- print(f"\nNoise correction completed. Estimated sigma: {estimated_sigma:.4f}")
- # Apply denoising after noise correction if requested and using noisy data
- if args.use_noisy_data and not args.skip_denoise:
- print("\nChecking for existing denoised data...")
- device = torch.device(f'cuda:{args.gpu}' if torch.cuda.is_available() else 'cpu')
- # Create denoised output directory
- denoised_output_dir = os.path.join(args.output_dir, 'denoised')
- os.makedirs(denoised_output_dir, exist_ok=True)
- # Check if all denoised files exist
- all_denoised_exist = True
- denoised_data = []
- for i, b_val in enumerate(b_values_array):
- denoised_path = os.path.join(denoised_output_dir, f'dwi_ivim_b{int(b_val)}_denoised.nii.gz')
- if os.path.exists(denoised_path):
- print(f" ✔ Found existing denoised file for b={int(b_val)}")
- # Load the existing denoised data
- denoised_img = nib.load(denoised_path)
- denoised_data.append(denoised_img.get_fdata().astype(np.float32))
- else:
- all_denoised_exist = False
- break
- if all_denoised_exist:
- # All denoised files exist, use them
- print(" ✔ All denoised files found, skipping denoising step")
- dwi_data = np.stack(denoised_data, axis=-1)
- else:
- # Some denoised files missing, run denoising
- print(" Some denoised files missing, running MDINR denoising...")
- dwi_data = denoise_dwi_mdinr_optimized(dwi_data, fitting_mask, b_values_array, args, device,
- affine=affine, save_intermediate=True,
- output_dir=denoised_output_dir)
- print("\nDenoising completed.")
- # Update b0_data with denoised data
- b0_data = dwi_data[:, :, :, 0]
- elif args.use_noisy_data and args.skip_denoise:
- print("\nUsing noisy data without denoising.")
- else:
- if not args.use_noisy_data:
- print("\nUsing ground truth data (no noise correction or denoising needed).")
- # Set device
- device = torch.device(f'cuda:{args.gpu}' if torch.cuda.is_available() else 'cpu')
- print(f"Using device: {device}")
- # Configuration is now hardcoded in the script
- # No need to load external config files
- # Ensure output directory exists
- os.makedirs(args.output_dir, exist_ok=True)
- # Fit IVIM parameters
- if args.use_soft_constraint:
- Dp_map, Dt_map, Fp_map, delta_map = fit_ivim_parameters_soft(
- dwi_data, fitting_mask, b_values_array, args, device
- )
- # Save IVIM parameters
- nib.save(nib.Nifti1Image(Dp_map, affine), os.path.join(args.output_dir, 'Dp.nii.gz'))
- nib.save(nib.Nifti1Image(Dt_map, affine), os.path.join(args.output_dir, 'Dt.nii.gz'))
- nib.save(nib.Nifti1Image(Fp_map, affine), os.path.join(args.output_dir, 'Fp.nii.gz'))
- nib.save(nib.Nifti1Image(delta_map, affine), os.path.join(args.output_dir, 'delta.nii.gz'))
- print(f" ✔ Saved IVIM parameters with delta to: {args.output_dir}")
- else:
- Dp_map, Dt_map, Fp_map = fit_ivim_parameters(
- dwi_data, fitting_mask, b_values_array, args, device
- )
- # Save IVIM parameters
- nib.save(nib.Nifti1Image(Dp_map, affine), os.path.join(args.output_dir, 'Dp.nii.gz'))
- nib.save(nib.Nifti1Image(Dt_map, affine), os.path.join(args.output_dir, 'Dt.nii.gz'))
- nib.save(nib.Nifti1Image(Fp_map, affine), os.path.join(args.output_dir, 'Fp.nii.gz'))
- print(f" ✔ Saved IVIM parameters to: {args.output_dir}")
- # Save fitting mask
- nib.save(nib.Nifti1Image(fitting_mask.astype(np.float32), affine),
- os.path.join(args.output_dir, 'fitting_mask.nii.gz'))
- # Save parameter configuration as JSON
- config_info = {
- 'IVIM_parameters': IVIM_PARAMETER_RANGES,
- 'b_value_threshold': 1000,
- 'min_b_value_threshold': 10,
- 'soft_constraint': args.use_soft_constraint,
- 'delta_min': args.delta_min if args.use_soft_constraint else None,
- 'delta_max': args.delta_max if args.use_soft_constraint else None,
- 'delta_regularization': args.delta_regularization if args.use_soft_constraint else None
- }
- config_path = os.path.join(args.output_dir, 'parameter_config.json')
- with open(config_path, 'w') as f:
- json.dump(config_info, f, indent=4)
- print(f" ✔ Saved parameter configuration to: {config_path}")
- print(f"\n✔ Completed IVIM fitting")
- # process_patient function removed - direct IVIM processing only
- # Removed - functionality merged into process_ivim
- def main():
- parser = argparse.ArgumentParser(description='IVIM Parameter Fitting Pipeline')
- # Input/Output arguments
- _this_dir = os.path.dirname(os.path.abspath(__file__))
- parser.add_argument('--input_dir', type=str,
- default=os.path.join(_this_dir, 'data_1'),
- help='Input directory with IVIM data')
- parser.add_argument('--data_dir', type=str,
- default=None,
- help='Data directory (overrides input_dir)')
- parser.add_argument('--output_dir', type=str,
- default=os.path.join(_this_dir, 'results', 'inr'),
- help='Output directory for fitted parameters')
- parser.add_argument('--denoised_dir', type=str,
- default=None,
- help='Output directory for denoised data (defaults to output_dir if not specified)')
- parser.add_argument('--noise_corrected_dir', type=str,
- default=None,
- help='Output directory for noise corrected data (defaults to output_dir if not specified)')
- # Processing options
- parser.add_argument('--use_noisy_data', action='store_true', default=True,
- help='Use noisy data instead of ground truth')
- parser.add_argument('--apply_noise_correction', action='store_true', default=True,
- help='Apply noise floor correction')
- parser.add_argument('--noise_correction_method', choices=['rayleigh_mean', 'simple_subtraction'],
- default='rayleigh_mean', help='Noise correction method')
- parser.add_argument('--background_threshold', type=float, default=200,
- help='Background threshold for creating masks')
- parser.add_argument('--skip_denoise', action='store_true', help='Skip MDINR denoising step')
- parser.add_argument('--force', action='store_true', help='Force reprocessing even if output files exist')
- # Config parameter removed - configuration is now hardcoded
- # Denoising parameters
- parser.add_argument('--denoise_hidden_size', type=int, default=512)
- parser.add_argument('--denoise_num_layers', type=int, default=4)
- parser.add_argument('--denoise_dropout', type=float, default=0.0)
- parser.add_argument('--denoise_npe', type=int, default=64)
- parser.add_argument('--denoise_maxf', type=float, default=40)
- parser.add_argument('--denoise_use_coords', type=int, default=0)
- parser.add_argument('--denoise_epochs', type=int, default=800)
- parser.add_argument('--denoise_batch_size', type=int, default=8000)
- parser.add_argument('--denoise_lr', type=float, default=1e-4)
- parser.add_argument('--denoise_patch_size', type=int, default=3, help='Patch size for denoising (will create NxNxN patches)')
- # IVIM parameters
- parser.add_argument('--ivim_hidden_size', type=int, default=512)
- parser.add_argument('--ivim_num_layers', type=int, default=4)
- parser.add_argument('--ivim_first_omega_0', type=float, default=30.0)
- parser.add_argument('--ivim_hidden_omega_0', type=float, default=30.0)
- parser.add_argument('--ivim_epochs', type=int, default=800)
- parser.add_argument('--ivim_batch_size', type=int, default=6000)
- parser.add_argument('--ivim_lr', type=float, default=5e-5)
- parser.add_argument('--ivim_use_dwi_signals', action='store_true', default=False,
- help='Use DWI signals as additional input for IVIM fitting')
- parser.add_argument('--ivim_use_b0_signal', action='store_true', default=True,
- help='Use normalized B0 signal as additional input for IVIM fitting')
- parser.add_argument('--ivim_b0_patch_size', type=int, default=1,
- help='Patch size for B0 input in IVIM fitting (default: 3, creates 3x3x3 patches)')
- # Soft constraint parameters
- parser.add_argument('--use_soft_constraint', action='store_true', default=True,
- help='Use soft b0 constraint for IVIM fitting (allows curve to not pass exactly through (0,1))')
- parser.add_argument('--delta_min', type=float, default=-0.01,
- help='Minimum delta offset value for soft constraint')
- parser.add_argument('--delta_max', type=float, default=0.01,
- help='Maximum delta offset value for soft constraint')
- parser.add_argument('--delta_regularization', type=float, default=0.01,
- help='Regularization weight for delta offset (encourages delta close to 0)')
- # Removed ADC parameters - only IVIM fitting is performed
- # General parameters
- parser.add_argument('--gpu', type=int, default=0, help='GPU device to use')
- parser.add_argument('--seed', type=int, default=42, help='Random seed')
- args = parser.parse_args()
- # Set default directories if not provided
- if args.denoised_dir is None:
- args.denoised_dir = args.output_dir
- if args.noise_corrected_dir is None:
- args.noise_corrected_dir = args.output_dir
- # Set random seeds
- np.random.seed(args.seed)
- torch.manual_seed(args.seed)
- if torch.cuda.is_available():
- torch.cuda.manual_seed(args.seed)
- # Verify input directory
- if not os.path.isdir(args.input_dir):
- sys.exit(f'Error: Input directory not found: {args.input_dir}')
- # Create output directory
- os.makedirs(args.output_dir, exist_ok=True)
- # No patient processing - direct IVIM data processing
- print(f'='*80)
- print('IVIM Parameter Fitting Pipeline')
- print(f'='*80)
- print(f'Input directory: {args.input_dir}')
- print(f'Output directory: {args.output_dir}')
- print(f'Use noisy data: {"Yes" if args.use_noisy_data else "No (Ground Truth)"}')
- print(f'Denoising: {"Disabled" if args.skip_denoise else "Enabled"}')
- print(f'Noise correction: {"Enabled" if args.apply_noise_correction else "Disabled"}')
- print(f'Soft constraint: {"Enabled" if args.use_soft_constraint else "Disabled"}')
- if args.use_soft_constraint:
- print(f' Delta range: [{args.delta_min}, {args.delta_max}]')
- print(f' Delta regularization: {args.delta_regularization}')
- print(f'='*80)
- # Process IVIM data
- try:
- process_ivim(args)
- except Exception as e:
- print(f'✖ Processing failed: {e}')
- import traceback
- traceback.print_exc()
- print(f'\n{"="*80}')
- print('Pipeline completed!')
- print(f'{"="*80}')
- if __name__ == '__main__':
- main()
train.py at commit 9a92905, no license · at the source
Overview
Abstract
Background: Intravoxel incoherent motion (IVIM) diffusion‐weighted imaging has important value in treatment response monitoring. However, traditional voxel‐wise independent fitting methods are highly sensitive to noise and do not utilize spatial correlation, resulting in unstable parameter estimation. Although existing deep learning methods have shown improvements, they are still limited by local receptive fields.
Purpose: To address this, we propose a two‐stage IVIM parameter estimation framework based on Implicit Neural Representation (IVIM‐INR).
Methods: Our IVIM‐INR method achieves global spatial perception through coordinate encoding and enhances spatial context modeling by leveraging local 3D patch information from multi‐b‐value images. The first stage INR performs signal denoising, and the second stage INR accurately fits IVIM parameters.
Results: Evaluation on brain digital phantoms, AAPM breast IVIM‐dMRI Challenge data, and clinical Glioblastoma (GBM) patient data demonstrates significant advantages of the proposed method over existing techniques. In brain simulation data, when SNR = 50, the normalized mean absolute errors (NMAEs) in tumor regions were 0.16±0.10 for Dp, 0.02±0.01 for Dt, and 0.07±0.05 for Fp, all lower than comparison methods. In the tumor tissues of the 100 cases from the AAPM breast IVIM‐dMRI challenge dataset, Fp error was reduced by 58% compared to ConvNet. Intraclass correlation coefficient (ICC) analysis of real clinical data indicates that our method achieves the best performance with an ICC of Dt in normal tissues reaching 0.629.
Conclusions: By combining INR's continuous function modeling capability with spatial‐aware feature design, IVIM‐INR overcomes the inherent limitations of traditional methods under noisy conditions, providing a more reliable tool for clinical IVIM quantitative analysis.
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 4 matches between paragraphs and lines of code.
Kent0n-Li/IVIM-INR
9a92905c473df52d259d29f19aeec5e9f589e56d, 22 April 2026Availability: 1 check, the latest on 26 September 2026: the link answers
- 26 September 2026: the link answers
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;
- 1 script, each with its path and the digest of its content;
- 4 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
No dataset and no data link were found in the paper.
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 3, 28 September 2026
- Publisher: n/a → Wiley
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 3 keywords, 9 MeSH terms, 4 funders, 45 references.
Cite
This paper
Li, Y., Liao, Y., Dai, Y., Deng, J., & Zhang, Y. (2026). Accurate estimation of intravoxel incoherent motion parameters based on implicit neural representation. Medical physics, 53(8), e70599. https://
BibTeX
@article{li2026accurate,
author = {Li, Yunxiang and Liao, Yen‐Peng and Dai, Yan and Deng, Jie and Zhang, You},
title = {{Accurate estimation of intravoxel incoherent motion parameters based on implicit neural representation}},
journal = {Medical physics},
year = {2026},
month = aug,
volume = {53},
number = {8},
pages = {e70599},
publisher = {Wiley},
issn = {0094-2405},
doi = {10.1002/
url = {https://
pmid = {42519880},
pmcid = {PMC13411821}
}
RIS
TY - JOUR
AU - Li, Yunxiang
AU - Liao, Yen‐Peng
AU - Dai, Yan
AU - Deng, Jie
AU - Zhang, You
TI - Accurate estimation of intravoxel incoherent motion parameters based on implicit neural representation
T2 - Medical physics
J2 - Med Phys
PY - 2026
DA - 2026/
VL - 53
IS - 8
SP - e70599
SN - 0094-2405
PB - Wiley
DO - 10.1002/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1002/
"type": "article-journal",
"title": "Accurate estimation of intravoxel incoherent motion parameters based on implicit neural representation",
"container-title": "Medical physics",
"author": [
{
"family": "Li",
"given": "Yunxiang"
},
{
"family": "Liao",
"given": "Yen‐Peng"
},
{
"family": "Dai",
"given": "Yan"
},
{
"family": "Deng",
"given": "Jie"
},
{
"family": "Zhang",
"given": "You"
}
],
"container-title-short":
"volume": "53",
"issue": "8",
"page": "e70599",
"DOI": "10.1002/
"PMID": "42519880",
"PMCID": "PMC13411821",
"ISSN": "0094-2405",
"publisher": "Wiley",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
1
]
]
}
}
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.70277 [code]
- Hierarchical Bayesian Modelling Improves Microstructural Parameter Mapping in Diffusion and Exchange MRI Data.Journal: NMR in biomedicineIn common: NiBabel, SciPy, NumPy, structural MRI / diffusion, 6 references
- [2] doi:10.1038/s43856-026-01614-6 [code]
- Simulation-based inference at the theoretical limit for fast, robust microstructural MRI with minimal diffusion data.Journal: Communications medicineIn common: PyTorch, SciPy, NumPy, structural MRI / diffusion, 5 references
- [3] doi:10.1002/mrm.70496 [code]
- A Deep Nonlinear Subspace Modeling and Reconstruction for Diffusion-Weighted Imaging Using Denoising Auto-Encoder.Journal: Magnetic resonance in medicineIn common: pydicom, NiBabel, PyTorch, 2 other tools, structural MRI / diffusion, 1 reference
- [4] 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: pydicom, NiBabel, PyTorch, 2 other tools, 1 reference
- [5] doi:10.1080/07853890.2026.2685416 [code]
- Pulmonary and cerebral damage in COVID-19 survivors: is there any association?Journal: Annals of medicineIn common: pydicom, NiBabel, PyTorch, 2 other tools, structural MRI / diffusion, other condition
- [6] doi:10.1002/alz.71530 [code]
- Differential associations of plasma biomarkers with Alzheimer's disease and small vessel disease: A multimodal imaging study.Journal: Alzheimer's & dementia : the journal of the Alzheimer's AssociationIn common: NiBabel, PyTorch, SciPy, 1 other tool, structural MRI / diffusion, 2 references
- [7] doi:10.3389/fnins.2026.1841093 [code]
- Brain protein burden is related to intravoxel incoherent motion: PET-MR imaging study.Journal: Frontiers in neuroscienceIn common: NiBabel, SciPy, NumPy, structural MRI / diffusion, 2 references
- [8] doi:10.1186/s13244-026-02296-3 [code]
- A pre-trained foundation model framework for multiplanar MRI classification of extramural vascular invasion and mesorectal fascia invasion in rectal cancer.Journal: Insights into imagingIn common: pydicom, NiBabel, PyTorch, 2 other tools, structural MRI / diffusion, other condition
- [9] doi:10.1371/journal.pcbi.1014555 [code]
- Body surface potential driven personalisation of electrophysiological digital twins in hypertrophic cardiomyopathy.Journal: PLoS computational biologyIn common: pydicom, NiBabel, PyTorch, 2 other tools, structural MRI / diffusion
- [10] doi:10.3389/frai.2026.1771088 [code]
- Few-shot deployment of pretrained MRI transformers in brain imaging tasks.Journal: Frontiers in artificial intelligenceIn common: pydicom, NiBabel, PyTorch, 2 other tools, structural MRI / diffusion
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, 1 script, and 4 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:37cab969a93e0536…
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.
