"""Common utilities for bindings."""
from __future__ import annotations
import importlib.util
import logging
import shutil
import subprocess
from pathlib import Path
import nibabel as nib
import numpy as np
from numpy.typing import NDArray
from patch_denoise.denoise import (
adaptive_thresholding,
hybrid_pca,
mp_pca,
nordic,
optimal_thresholding,
raw_svt,
)
DENOISER_MAP = {
"mp-pca": mp_pca,
"hybrid-pca": hybrid_pca,
"raw": raw_svt,
"optimal-fro": lambda *args, **kwargs: optimal_thresholding(
*args, loss="fro", **kwargs
),
"optimal-fro-noise": lambda *args, **kwargs: optimal_thresholding(
*args, loss="fro", **kwargs
),
"optimal-nuc": lambda *args, **kwargs: optimal_thresholding(
*args, loss="nuc", **kwargs
),
"optimal-ope": lambda *args, **kwargs: optimal_thresholding(
*args, loss="ope", **kwargs
),
"nordic": nordic,
"adaptive-qut": lambda *args, **kwargs: adaptive_thresholding(
*args, method="qut", **kwargs
),
}
_RECOMBINATION = {"w": "weighted", "c": "center", "a": "average"}
[docs]
def load_as_array(input: Path) -> tuple[np.ndarray, np.ndarray]:
"""Load a file as a numpy array, and return affine matrix if available."""
import nibabel as nib
if input is None:
return None, None
if input.suffix == ".npy":
return np.load(input), np.eye(4)
elif ".nii" in input.suffixes:
nii = nib.Nifti1Image.from_filename(input)
return nii.get_fdata(dtype=np.float32), nii.affine
else:
raise ValueError("Unsupported file format. use numpy or nifti formats.")
[docs]
def save_array(data: NDArray, affine: NDArray, filename: Path) -> Path:
"""Save array to file, with affine matrix if required."""
if ".nii" in filename.suffixes:
if affine is None:
affine = np.eye(len(data.shape))
nii_img = nib.Nifti1Image(data, affine)
nii_img.to_filename(filename)
elif filename.suffix == ".npy":
np.save(filename, data)
return filename
[docs]
def load_complex_nifti(
mag_file: Path, phase_file: Path
) -> tuple[NDArray, NDArray]: # pragma: no cover
"""Load two nifti image (magnitude and phase) to create a complex valued array.
Optionally, the result can be save as a .npy file
Parameters
----------
mag_file: str
The source magnitude file
phase_file: str
The source phase file
filename: str, default None
The output filename
"""
mag, mag_affine = load_as_array(mag_file)
phase, phase_affine = load_as_array(phase_file)
if not np.allclose(mag_affine, phase_affine):
logging.warning("Affine matrices for magnitude and phase are not the same")
logging.info("Phase data range is [%.2f %.2f]", np.min(phase), np.max(phase))
logging.info("Mag data range is [%.2f %.2f]", np.min(mag), np.max(mag))
img = mag * np.exp(1j * phase)
return img, mag_affine
[docs]
def fast_cuda_check() -> bool:
"""
Instantly checks if PyTorch is installed and CUDA is supported by the system.
Without paying the heavy time/memory cost of 'import torch'.
"""
# 1. Instant check: Is PyTorch even installed in this environment? (~1ms)
if importlib.util.find_spec("torch") is None:
return False
# 2. Fast check: Does an NVIDIA GPU driver exist on the system? (~0ms)
if shutil.which("nvidia-smi") is None:
return False
# 3. Quick check: Is the GPU actually responsive/functional? (~10-20ms)
try:
subprocess.run(
["nvidia-smi"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False