Browse Source

Reorg code

Martin Horvat 2 days ago
parent
commit
08f0133bb2

+ 8 - 60
README.md

@@ -16,14 +16,12 @@ The main goal is to investigate whether high-SUV spatial patterns contain biomar
 ├── notebooks/            # exploratory notebooks
 ├── notes/                # methodological notes
 ├── shiny_app/            # interactive Shiny app
-├── spatial_suv_charact/  # Python package
+├── src/                  # Python functions
 ├── pyproject.toml
 └── README.md
-````
-
----
+```
 
-## Package modules
+## Function modules
 
 ```text
 metadata.py          -> dataframe / patient metadata utilities
@@ -33,39 +31,21 @@ spatial_features.py  -> spatial tail-region features
 plotting.py          -> Plotly visualization
 ```
 
----
-
-## Installation
-
-From the repository root:
-
-```bash
-pip install -e .
-```
-
-If needed:
-
-```bash
-pip install -r spatial_suv_charact/requirements.txt
-```
-
----
-
 ## Basic usage
 
 ```python
 from pathlib import Path
 
-from spatial_suv_charact.metadata import (
+from src.metadata import (
     get_meta_data,
     flag_corrupted_files,
     flag_AE_patients,
 )
 
-from spatial_suv_charact.image_io import get_processed_image
-from spatial_suv_charact.suv_stats import get_suv_percentiles
-from spatial_suv_charact.spatial_features import compute_tail_spatial_features
-from spatial_suv_charact.plotting import plot_suv_pdf_plotly, plot_hot_voxels_plotly
+from src.image_io import get_processed_image
+from src.suv_stats import get_suv_percentiles
+from src.spatial_features import compute_tail_spatial_features
+from src.plotting import plot_suv_pdf_plotly, plot_hot_voxels_plotly
 ```
 
 Collect metadata:
@@ -137,8 +117,6 @@ features = compute_tail_spatial_features(
 features
 ```
 
----
-
 ## Spatial features
 
 For a percentile threshold (q), the high-SUV tail region is
@@ -163,8 +141,6 @@ The package computes features such as:
 
 These features are intended to describe intensity, heterogeneity, fragmentation, and spatial organization of high-SUV uptake.
 
----
-
 ## Shiny app
 
 The Shiny app is in:
@@ -186,7 +162,6 @@ The app allows interactive inspection of:
 * hot-voxel plots,
 * computed spatial features.
 
----
 
 ## Notes
 
@@ -196,33 +171,6 @@ Methodological notes on testing candidate spatial metrics are in:
 notes/testing_spatial_metric.md
 ```
 
----
-
-## Development notes
-
-Recommended cleanup before committing:
-
-```bash
-find . -type d -name "__pycache__" -prune -exec rm -rf {} +
-find . -type f -name "*.pyc" -delete
-```
-
-Suggested `.gitignore` entries:
-
-```gitignore
-__pycache__/
-*.pyc
-.ipynb_checkpoints/
-data/raw/
-*.nii
-*.nii.gz
-.DS_Store
-```
-
-Generated outputs in `data/gen/` can be ignored or committed selectively, depending on size and reproducibility needs.
-
----
-
 ## Status
 
 This project is exploratory and intended for biomarker discovery. Candidate metrics should be validated carefully, especially when the number of AE cases is small.

+ 0 - 1
app.py

@@ -1 +0,0 @@
-shiny_app/app.py

+ 16 - 16
notebooks/review_data.ipynb

@@ -54,7 +54,7 @@
    "outputs": [],
    "source": [
     "import pandas as pd\n",
-    "import matplotlib.pyplot as plt\n"
+    "import matplotlib.pyplot as plt"
    ]
   },
   {
@@ -64,16 +64,16 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from spatial_suv_charact.metadata import (\n",
+    "from src.metadata import (\n",
     "    get_meta_data,\n",
     "    flag_corrupted_files,\n",
     "    flag_AE_patients,\n",
     "    get_AE_statistics,\n",
     ")\n",
-    "from spatial_suv_charact.image_io import get_processed_image\n",
-    "from spatial_suv_charact.suv_stats import get_suv_percentiles\n",
-    "from spatial_suv_charact.spatial_features import compute_tail_spatial_features\n",
-    "from spatial_suv_charact.plotting import plot_suv_pdf_plotly, plot_hot_voxels_plotly\n"
+    "from src.image_io import get_processed_image\n",
+    "from src.suv_stats import get_suv_percentiles\n",
+    "from src.spatial_features import compute_tail_spatial_features\n",
+    "from src.plotting import plot_suv_pdf_plotly, plot_hot_voxels_plotly\n"
    ]
   },
   {
@@ -86,7 +86,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 4,
    "id": "2d9b341d",
    "metadata": {},
    "outputs": [],
@@ -152,7 +152,7 @@
          "type": "boolean"
         }
        ],
-       "ref": "8f696d1f-e73b-4cdb-ac13-9018a553e950",
+       "ref": "3a1d3b90-44a4-4a7e-a7b1-2dd3c5a980b3",
        "rows": [
         [
          "('NIX-LJU-D2002-IRAE-A000', 'Colon', np.int64(0))",
@@ -891,7 +891,7 @@
          "type": "string"
         }
        ],
-       "ref": "51fb2b80-939c-45ef-af62-988587b378dc",
+       "ref": "8050cfa5-837c-4187-9bfa-816990060662",
        "rows": [
         [
          "0",
@@ -1139,7 +1139,7 @@
          "type": "integer"
         }
        ],
-       "ref": "6042811c-950d-456e-ab98-36792de332ee",
+       "ref": "8b0ed79c-1866-4ead-b9eb-4d9bbff49365",
        "rows": [
         [
          "Colon",
@@ -1222,7 +1222,7 @@
          "type": "unknown"
         }
        ],
-       "ref": "8fb62aaf-3280-40a8-854d-ebcd6a50c10f",
+       "ref": "cc3a78ce-c3ad-4407-a6a9-8ad38d320e41",
        "rows": [
         [
          "PET_filename",
@@ -1331,7 +1331,7 @@
          "type": "unknown"
         }
        ],
-       "ref": "69e7a6f8-1cea-41cd-8fce-5fe3eea670e6",
+       "ref": "4f0a5da4-0dbc-4529-8aa7-8497552478b4",
        "rows": [
         [
          "PET_filename",
@@ -1425,7 +1425,7 @@
          "type": "integer"
         }
        ],
-       "ref": "836c2a59-409b-40e0-995f-429a4cb17a4f",
+       "ref": "be0ce811-4065-4c77-a1e3-7caf0b12eb0e",
        "rows": [
         [
          "0",
@@ -2501,7 +2501,7 @@
          "type": "integer"
         }
        ],
-       "ref": "4cf81012-2164-460a-8781-2a8f58948cae",
+       "ref": "38ca312c-e596-47b7-bb10-4423dfc7dc9a",
        "rows": [
         [
          "0",
@@ -3509,7 +3509,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 20,
    "id": "101816c9",
    "metadata": {},
    "outputs": [
@@ -3653,7 +3653,7 @@
          "type": "float"
         }
        ],
-       "ref": "0388139a-3d72-40ec-8624-5fd873a0e82d",
+       "ref": "33b9eae7-4130-4971-8d61-460a862ba62c",
        "rows": [
         [
          "0",

+ 0 - 39
pyproject.toml

@@ -1,39 +0,0 @@
-[build-system]
-requires = ["setuptools>=68", "wheel"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "spatial-suv-charact"
-version = "0.1.0"
-description = "Utilities for spatial characterization of SUV distributions in PET NIfTI images."
-readme = "spatial_suv_charact/README.md"
-requires-python = ">=3.10"
-authors = [
-    { name = "Martin Horvat" }
-]
-license = { text = "MIT" }
-keywords = ["PET", "SUV", "NIfTI", "radiomics", "medical imaging"]
-dependencies = [
-    "numpy>=1.23",
-    "pandas>=1.5",
-    "nibabel>=5.0",
-    "scipy>=1.10",
-    "scikit-image>=0.20",
-    "plotly>=5.0"
-]
-
-[project.optional-dependencies]
-dev = [
-    "pytest",
-    "ipykernel",
-    "jupyterlab",
-    "ruff",
-]
-
-[tool.setuptools.packages.find]
-where = ["."]
-include = ["spatial_suv_charact*"]
-exclude = ["data*", "notebooks*"]
-
-[tool.ruff]
-line-length = 88

+ 3 - 0
spatial_suv_charact/requirements.txt → requirements.txt

@@ -4,3 +4,6 @@ nibabel
 scipy
 scikit-image
 plotly
+shiny
+shinywidgets
+

+ 13 - 38
shiny_app/app.py

@@ -1,29 +1,3 @@
-"""
-Interactive Shiny app for inspecting processed PET SUV NIfTI images.
-
-Run from project root:
-
-    shiny run --reload app.py
-
-Required packages:
-
-    pip install shiny shinywidgets shinyswatch plotly pandas nibabel scipy scikit-image
-
-Expected project structure:
-
-project_root/
-├── app.py
-├── data/
-│   ├── gen/
-│   └── raw/
-└── spatial_suv_charact/
-    ├── image_io.py
-    ├── metadata.py
-    ├── plotting.py
-    ├── spatial_features.py
-    └── suv_stats.py
-"""
-
 from __future__ import annotations
 
 from pathlib import Path
@@ -40,33 +14,31 @@ from shinywidgets import output_widget, render_widget
 # Project paths
 # -----------------------------------------------------------------------------
 
-APP_DIR = "."  # str(Path(__file__).resolve().parent)
-PROJECT_ROOT = APP_DIR
+APP_DIR = Path(__file__).resolve().parent
+PROJECT_ROOT = APP_DIR.parent
 
 if str(PROJECT_ROOT) not in sys.path:
     sys.path.insert(0, str(PROJECT_ROOT))
 
-import os
-
-DATA_RAW = os.path.join(PROJECT_ROOT, "data", "raw")
-DATA_GEN = os.path.join(PROJECT_ROOT, "data", "gen")
-DEFAULT_METADATA_PATH = os.path.join(DATA_GEN, "metadata.pkl")
+DATA_RAW = PROJECT_ROOT / "data" / "raw"
+DATA_GEN = PROJECT_ROOT / "data" / "gen"
+DEFAULT_METADATA_PATH = DATA_GEN / "metadata.pkl"
 
 # -----------------------------------------------------------------------------
 # Local package imports
 # -----------------------------------------------------------------------------
 
-from spatial_suv_charact.metadata import (  # noqa: E402
+from src.metadata import (  # noqa: E402
     get_meta_data,
     flag_corrupted_files,
     flag_AE_patients,
 )
-from spatial_suv_charact.image_io import get_processed_image  # noqa: E402
-from spatial_suv_charact.plotting import (  # noqa: E402
+from src.image_io import get_processed_image  # noqa: E402
+from src.plotting import (  # noqa: E402
     plot_suv_pdf_plotly,
     plot_hot_voxels_plotly,
 )
-from spatial_suv_charact.spatial_features import compute_tail_spatial_features  # noqa: E402
+from src.spatial_features import compute_tail_spatial_features  # noqa: E402
 
 
 # -----------------------------------------------------------------------------
@@ -492,4 +464,7 @@ app = App(app_ui, server)
 if __name__ == "__main__":
     from shiny import run_app
 
-    run_app("app:app", host="127.0.0.1", port=8000, reload=True)
+    # For development, prefer:
+    #     shiny run --reload shiny_app/app.py
+    # Direct execution via `python shiny_app/app.py` works without auto-reload.
+    run_app(app, host="127.0.0.1", port=8000, reload=False)

+ 0 - 216
spatial_suv_charact/README.md

@@ -1,216 +0,0 @@
-# spatial_suv_charact
-
-Utilities for PET SUV NIfTI analysis, with emphasis on spatial properties of high-SUV tail regions.
-
-The code is organized into focused modules instead of one long `utils.py` file. This keeps the data-management, image-processing, feature-extraction and plotting tasks separate and easier to test.
-
-## File organization
-
-```text
-utils.py              -> remove or keep only temporary imports
-metadata.py           -> dataframe / patient metadata utilities
-image_io.py           -> NIfTI loading and segmentation application
-suv_stats.py          -> simple SUV distribution features
-spatial_features.py   -> tail features and helper functions
-plotting.py           -> Plotly visualization
-```
-
-In this version, `utils.py` is kept as a thin compatibility layer that re-exports the functions from the focused modules. New code should import directly from the specific module.
-
-## Recommended imports
-
-```python
-from spatial_suv_charact.metadata import (
-    get_meta_data,
-    flag_corrupted_files,
-    flag_AE_patients,
-    get_AE_statistics,
-)
-from spatial_suv_charact.image_io import get_processed_image
-from spatial_suv_charact.suv_stats import get_suv_percentiles
-from spatial_suv_charact.spatial_features import compute_tail_spatial_features
-from spatial_suv_charact.plotting import plot_suv_pdf_plotly, plot_hot_voxels_plotly
-```
-
-Older notebooks can still use:
-
-```python
-from spatial_suv_charact.utils import compute_tail_spatial_features
-```
-
-but this should be treated as temporary.
-
-## Typical workflow
-
-```python
-from spatial_suv_charact.metadata import get_meta_data, flag_corrupted_files, flag_AE_patients
-from spatial_suv_charact.image_io import get_processed_image
-from spatial_suv_charact.suv_stats import get_suv_percentiles
-from spatial_suv_charact.spatial_features import compute_tail_spatial_features
-from spatial_suv_charact.plotting import plot_suv_pdf_plotly
-
-# 1. Build metadata table.
-df_meta = get_meta_data("/path/to/raw/data")
-df_meta = flag_corrupted_files(df_meta)
-df_meta = flag_AE_patients(df_meta)
-
-# 2. Load PET image and apply segmentation mask.
-row, image = get_processed_image(
-    df_meta,
-    patient_id="NIX-LJU-D2002-IRAE-A001",
-    organ="Lung",
-    visit=0,
-)
-
-# 3. Compute simple SUV percentiles.
-percentile_df = get_suv_percentiles(
-    image,
-    percentiles=(50, 75, 90, 95, 99),
-)
-
-# 4. Compute spatial high-tail features.
-features = compute_tail_spatial_features(
-    image,
-    percentiles=(90, 95, 97.5, 99),
-    connectivity=26,
-    min_component_voxels=3,
-    image_id="NIX-LJU-D2002-IRAE-A001_Lung_VISIT_0",
-)
-
-# 5. Plot SUV distribution.
-fig, percentile_df = plot_suv_pdf_plotly(
-    image,
-    percentiles=(50, 75, 90, 95, 99),
-    log_x=False,
-)
-fig.show()
-```
-
-## Main feature-extraction function
-
-The central function is:
-
-```python
-compute_tail_spatial_features(
-    image,
-    voxel_spacing=None,
-    percentiles=(90, 95, 97.5, 99),
-    connectivity=26,
-    min_component_voxels=1,
-    image_id=None,
-)
-```
-
-The input `image` is assumed to be a processed PET SUV NIfTI image. Finite positive voxels are treated as the region of interest. This works naturally if the image was produced by multiplying PET SUV by an organ segmentation mask.
-
-For each percentile `q`, the high-SUV tail is defined as:
-
-```text
-R_q = {voxel : SUV(voxel) >= percentile_q(SUV inside ROI)}
-```
-
-The function returns one dataframe row per percentile.
-
-## Tail features currently computed
-
-### Basic tail intensity
-
-- `threshold`
-- `tail_mean`
-- `tail_median`
-- `tail_max`
-- `tail_std`
-- `tail_cv`
-- `tail_sum`
-- `tail_excess_mean`
-- `tail_excess_sum`
-
-### Volume / voxel counts
-
-- `n_roi_voxels`
-- `roi_volume_mm3`
-- `n_tail_voxels`
-- `tail_volume_mm3`
-- `tail_fraction`
-
-Note: if the threshold is defined as a within-ROI percentile, `tail_fraction` is mostly determined by the chosen percentile. It is included mainly for checking and completeness.
-
-### Spatial spread
-
-- `tail_spread_mm2`
-- `tail_weighted_spread_mm2`
-
-These measure how spatially dispersed high-SUV voxels are.
-
-### Local contrast
-
-- `tail_local_contrast`
-- `tail_local_contrast_norm`
-
-`tail_local_contrast` is a continuous, non-discretized contrast-like measure:
-
-```text
-mean over neighboring voxel pairs inside R_q of (SUV_i - SUV_j)^2
-```
-
-`tail_local_contrast_norm` divides this value by `tail_mean ** 2`, giving a dimensionless version that is easier to compare across patients.
-
-### Connected-component features
-
-- `n_components`
-- `largest_component_voxels`
-- `largest_component_volume_mm3`
-- `largest_component_fraction`
-- `component_entropy`
-
-These describe whether high-SUV voxels form one dominant region or many disconnected foci.
-
-### Shape feature
-
-- `largest_component_sphericity`
-
-This is an approximate marching-cubes-based sphericity of the largest connected component.
-
-## Connectivity
-
-The `connectivity` argument controls both connected components and local contrast neighborhoods:
-
-```text
-6  -> face neighbors
-18 -> face + edge neighbors
-26 -> face + edge + corner neighbors
-```
-
-For small or noisy PET regions, `connectivity=26` and `min_component_voxels=3` is a reasonable starting point.
-
-## Dependencies
-
-Required Python packages:
-
-```text
-numpy
-pandas
-nibabel
-scipy
-scikit-image
-plotly
-```
-
-Install with:
-
-```bash
-pip install numpy pandas nibabel scipy scikit-image plotly
-```
-
-## Notes from code cleanup
-
-The original single file contained several useful functions but had started to mix several responsibilities:
-
-- metadata parsing,
-- patient/AE flags,
-- image loading and segmentation,
-- SUV summaries,
-- Plotly visualization,
-- spatial tail-feature extraction.
-
-It also contained duplicate imports and repeated sections. The new structure keeps functions simple and explicit. I would postpone introducing a class until the workflow stabilizes further. A class can be added later as a thin convenience wrapper, but the current function-based design is easier to test and use in notebooks.

+ 0 - 25
spatial_suv_charact/__init__.py

@@ -1,25 +0,0 @@
-"""Utilities for PET SUV NIfTI analysis."""
-
-from .metadata import (
-    get_meta_data,
-    flag_corrupted_files,
-    flag_AE_patients,
-    get_AE_statistics,
-)
-from .image_io import get_processed_image, check_same_grid
-from .suv_stats import get_suv_percentiles
-from .spatial_features import compute_tail_spatial_features
-from .plotting import plot_suv_pdf_plotly, plot_hot_voxels_plotly
-
-__all__ = [
-    "get_meta_data",
-    "flag_corrupted_files",
-    "flag_AE_patients",
-    "get_AE_statistics",
-    "get_processed_image",
-    "check_same_grid",
-    "get_suv_percentiles",
-    "compute_tail_spatial_features",
-    "plot_suv_pdf_plotly",
-    "plot_hot_voxels_plotly",
-]

+ 90 - 0
src/README.md

@@ -0,0 +1,90 @@
+# Functions of spatial_suv_charact
+
+Utilities for PET SUV NIfTI analysis, with emphasis on spatial properties of high-SUV tail regions.
+
+The code is organized into focused modules instead of one long `utils.py` file. This keeps the data-management, image-processing, feature-extraction and plotting tasks separate and easier to test.
+
+## File organization
+
+```text
+utils.py              -> remove or keep only temporary imports
+metadata.py           -> dataframe / patient metadata utilities
+image_io.py           -> NIfTI loading and segmentation application
+suv_stats.py          -> simple SUV distribution features
+spatial_features.py   -> tail features and helper functions
+plotting.py           -> Plotly visualization
+```
+
+In this version, `utils.py` is kept as a thin compatibility layer that re-exports the functions from the focused modules. New code should import directly from the specific module.
+
+## Tail features currently computed
+
+### Basic tail intensity
+
+- `threshold`
+- `tail_mean`
+- `tail_median`
+- `tail_max`
+- `tail_std`
+- `tail_cv`
+- `tail_sum`
+- `tail_excess_mean`
+- `tail_excess_sum`
+
+### Volume / voxel counts
+
+- `n_roi_voxels`
+- `roi_volume_mm3`
+- `n_tail_voxels`
+- `tail_volume_mm3`
+- `tail_fraction`
+
+Note: if the threshold is defined as a within-ROI percentile, `tail_fraction` is mostly determined by the chosen percentile. It is included mainly for checking and completeness.
+
+### Spatial spread
+
+- `tail_spread_mm2`
+- `tail_weighted_spread_mm2`
+
+These measure how spatially dispersed high-SUV voxels are.
+
+### Local contrast
+
+- `tail_local_contrast`
+- `tail_local_contrast_norm`
+
+`tail_local_contrast` is a continuous, non-discretized contrast-like measure:
+
+```text
+mean over neighboring voxel pairs inside R_q of (SUV_i - SUV_j)^2
+```
+
+`tail_local_contrast_norm` divides this value by `tail_mean ** 2`, giving a dimensionless version that is easier to compare across patients.
+
+### Connected-component features
+
+- `n_components`
+- `largest_component_voxels`
+- `largest_component_volume_mm3`
+- `largest_component_fraction`
+- `component_entropy`
+
+These describe whether high-SUV voxels form one dominant region or many disconnected foci.
+
+### Shape feature
+
+- `largest_component_sphericity`
+
+This is an approximate marching-cubes-based sphericity of the largest connected component.
+
+## Connectivity
+
+The `connectivity` argument controls both connected components and local contrast neighborhoods:
+
+```text
+6  -> face neighbors
+18 -> face + edge neighbors
+26 -> face + edge + corner neighbors
+```
+
+For small or noisy PET regions, `connectivity=26` and `min_component_voxels=3` is a reasonable starting point.

+ 0 - 0
spatial_suv_charact/image_io.py → src/image_io.py


+ 0 - 0
spatial_suv_charact/metadata.py → src/metadata.py


+ 0 - 0
spatial_suv_charact/plotting.py → src/plotting.py


+ 0 - 0
spatial_suv_charact/spatial_features.py → src/spatial_features.py


+ 0 - 0
spatial_suv_charact/suv_stats.py → src/suv_stats.py