suv_stats.py 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. """Simple SUV distribution summaries."""
  2. from __future__ import annotations
  3. import numpy as np
  4. import pandas as pd
  5. import nibabel as nib
  6. def get_suv_percentiles(
  7. image: nib.Nifti1Image,
  8. percentiles: tuple[float, ...] = (25, 50, 75),
  9. min_suv: float = 0.0,
  10. ) -> pd.DataFrame:
  11. """Compute SUV percentile thresholds and voxel counts.
  12. Parameters
  13. ----------
  14. image:
  15. 3D NIfTI image containing SUV values.
  16. percentiles:
  17. Percentiles to compute.
  18. min_suv:
  19. Only finite voxels with ``SUV > min_suv`` are used. For processed images
  20. where background is set to zero, the default ``min_suv=0`` uses only the
  21. non-background region.
  22. Returns
  23. -------
  24. pandas.DataFrame
  25. Columns: ``percentile``, ``suv_threshold``,
  26. ``n_voxels_ge_threshold`` and ``n_voxels_lt_threshold``.
  27. """
  28. if not isinstance(image, nib.Nifti1Image):
  29. raise TypeError("image must be a nib.Nifti1Image.")
  30. data = image.get_fdata(dtype=np.float64)
  31. if data.ndim != 3:
  32. raise ValueError("Input image must be 3D.")
  33. suv_values = data[np.isfinite(data) & (data > min_suv)]
  34. if suv_values.size == 0:
  35. return pd.DataFrame(
  36. {
  37. "percentile": percentiles,
  38. "suv_threshold": [np.nan] * len(percentiles),
  39. "n_voxels_ge_threshold": [0] * len(percentiles),
  40. "n_voxels_lt_threshold": [0] * len(percentiles),
  41. }
  42. )
  43. percentile_values = np.percentile(suv_values, percentiles)
  44. return pd.DataFrame(
  45. {
  46. "percentile": percentiles,
  47. "suv_threshold": percentile_values,
  48. "n_voxels_ge_threshold": [
  49. int(np.sum(suv_values >= val)) for val in percentile_values
  50. ],
  51. "n_voxels_lt_threshold": [
  52. int(np.sum(suv_values < val)) for val in percentile_values
  53. ],
  54. }
  55. )