Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 30 additions & 25 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ jobs:
CMD_BUILD: python -m build --wheel

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.9
uses: actions/setup-python@v5
- uses: actions/checkout@v5
- name: Set up Python 3.11
uses: actions/setup-python@v6
with:
python-version: "3.9"
python-version: "3.11"

- name: Install dependencies
run: |
Expand All @@ -33,7 +33,7 @@ jobs:
run: ${{matrix.CMD_BUILD}}

- name: Upload Python wheel
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: Python wheel
path: ${{github.workspace}}/dist/raidionicsval-*.whl
Expand All @@ -42,12 +42,12 @@ jobs:
setup-test-data:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.9
python-version: 3.11

- name: Download test resources
working-directory: tests
Expand All @@ -56,7 +56,7 @@ jobs:
python -c "from download_resources import download_resources; download_resources('../test_data')"

- name: Upload test resources
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: test-resources
path: ./test_data
Expand Down Expand Up @@ -106,14 +106,6 @@ jobs:
python-version: "3.12"
- os: windows-2025
python-version: "3.13"
- os: macos-13
python-version: "3.9"
- os: macos-13
python-version: "3.10"
- os: macos-13
python-version: "3.11"
- os: macos-13
python-version: "3.12"
- os: macos-14
python-version: "3.10"
- os: macos-14
Expand All @@ -130,15 +122,14 @@ jobs:
python-version: "3.12"
- os: macos-15
python-version: "3.13"

steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Download artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: "Python wheel"

Expand All @@ -152,22 +143,36 @@ jobs:
run: raidionicsval --help

- name: Clone repo
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Download test resources
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: test-resources
path: ./tests/unit_tests_results_dir

- name: Integration tests
env:
MPLBACKEND: Agg
run: |
pip install pytest pytest-cov pytest-timeout requests
pytest -vvv --cov=raidionicsval ${{github.workspace}}/tests/generic_tests --cov-report=xml --timeout=1000 --log-cli-level=DEBUG
pytest -vvv --cov=raidionicsval ${{github.workspace}}/tests/generic_tests --cov-report=xml --timeout=1000 --log-cli-level=INFO

- name: Upload integration test results
if: failure()
uses: actions/upload-artifact@v7
with:
name: integration-test-results-${{ matrix.os }}-py${{ matrix.python-version }}
path: |
tests/unit_tests_results_dir/**
!tests/unit_tests_results_dir/**/Inputs/**
!tests/unit_tests_results_dir/**/Predictions/**
if-no-files-found: ignore
retention-days: 1

- name: Upload coverage to Codecov
if: ${{ matrix.os == 'ubuntu-22.04' && matrix.python-version == '3.9' }}
uses: codecov/codecov-action@v4
if: ${{ matrix.os == 'ubuntu-22.04' && matrix.python-version == '3.11' }}
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: dbouget/validation_metrics_computation
Expand Down
2 changes: 2 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ def main(argv):


if __name__ == "__main__":
import matplotlib
matplotlib.use("Agg")
main(sys.argv[1:])
136 changes: 135 additions & 1 deletion raidionicsval/Utils/PatientMetricsStructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,87 @@ def init_from_file(self, study_folder: str):
else:
self.__init_from_file_classification(study_folder=study_folder)

def init_from_db(self, conn):
if self.objective == "segmentation":
self.__init_from_db_segmentation(conn)
else:
# @TODO. Add database-backed loading for classification objective
pass

def __init_from_db_segmentation(self, conn):
"""
Loads segmentation metrics for this patient from SQLite.
Fetches per-class results first, then the macro-averaged total_results rows.
Returns early if no data exists yet (i.e. first run).
"""
# Load per-class metrics using ClassMetrics.init_from_df
for c in list(self._class_metrics.keys()):
table_name = f"class_{c}"
try:
rows_df = pd.read_sql_query(
f"SELECT * FROM [{table_name}] WHERE [Patient] = ? AND [Fold] = ?",
conn,
params=(str(self._patient_id), self._fold_number)
)
self._class_metrics[c].init_from_df(rows_df)
except Exception:
return # Table does not exist yet on first run

# Load macro-averaged results from total_results
rows_df = pd.read_sql_query(
"SELECT * FROM [total_results] WHERE [Patient] = ? AND [Fold] = ?",
conn,
params=(str(self._patient_id), self._fold_number)
)

if len(rows_df) == 0:
return

def to_float(v):
# Convert to float since SQLite returns numeric values as strings
try:
return float(v) if v is not None else None
except (ValueError, TypeError):
return None

self._patientwise_metrics = []
self._pixelwise_metrics = []
self._objectwise_metrics = []
self._extra_metrics = []

upper_idx = SharedResources.getInstance().upper_default_metrics_index
extra_metric_names = []
for m in SharedResources.getInstance().validation_metric_names:
extra_metric_names.extend([f'PiW {m}', f'OW {m}'])

for thr in rows_df["Threshold"].values:
thr_results = rows_df.loc[rows_df["Threshold"] == thr].values[0]
thr_val = float(thr_results[2])
pixelwise_values = [to_float(x) for x in thr_results[3:7]]
patientwise_values = [to_float(x) for x in thr_results[7:10]]
objectwise_values = [to_float(x) for x in thr_results[10:upper_idx]]
extra_values = [to_float(x) for x in thr_results[upper_idx:]]

# Pad missing extra metrics with None to match expected length
while len(extra_values) < 2 * len(SharedResources.getInstance().validation_metric_names):
extra_values.append(None)

extra_values_description = list(rows_df.columns[upper_idx:])
for name in extra_metric_names:
if name not in extra_values_description:
extra_values_description.append(name)

self._pixelwise_metrics.append([thr_val] + pixelwise_values)
self._patientwise_metrics.append([thr_val] + patientwise_values)
self._objectwise_metrics.append([thr_val] + objectwise_values)

extra_values_cat = [[x, y] for x, y in zip(extra_values_description, extra_values)]
if extra_values_cat:
self._extra_metrics.append([thr_val] + extra_values_cat)

if len(self._extra_metrics) == 0:
self._extra_metrics = None

def __init_from_file_segmentation(self, study_folder: str):
all_scores_filename = os.path.join(study_folder, 'all_dice_scores.csv')

Expand Down Expand Up @@ -313,7 +394,7 @@ def set_results(self, results):
self._objectwise_metrics = []
for index in range(len(results)):
thr_results = results[index][0]
thr_val = thr_results[2]
thr_val = float(thr_results[2])
pixelwise_values = thr_results[3:7]
patientwise_values = thr_results[7:10]
objectwise_values = thr_results[10:SharedResources.getInstance().upper_default_metrics_index]
Expand Down Expand Up @@ -358,6 +439,59 @@ def init_from_file(self, scores_filename: str) -> None:
if len(self._extra_metrics) == 0:
self._extra_metrics = None

def init_from_df(self, rows_df: pd.DataFrame) -> None:
"""
Initializes class-level metrics from a pre-fetched DataFrame of per-threshold rows.
"""
if len(rows_df) == 0:
return

upper_idx = SharedResources.getInstance().upper_default_metrics_index

def to_float(v):
# Convert to float since SQLite returns numeric values as strings
try:
return float(v) if v is not None else None
except (ValueError, TypeError):
return None

self._patientwise_metrics = []
self._pixelwise_metrics = []
self._objectwise_metrics = []
self._extra_metrics = []

extra_metric_names = []
for m in SharedResources.getInstance().validation_metric_names:
extra_metric_names.extend([f'PiW {m}', f'OW {m}'])

for thr in np.unique(rows_df["Threshold"].values):
thr_results = rows_df.loc[rows_df["Threshold"] == thr].values[0]
thr_val = float(thr_results[2])
pixelwise_values = [to_float(x) for x in thr_results[3:7]]
patientwise_values = [to_float(x) for x in thr_results[7:10]]
objectwise_values = [to_float(x) for x in thr_results[10:upper_idx]]
extra_values = [to_float(x) for x in thr_results[upper_idx:]]

# Pad missing extra metrics with NaN to match expected length
while len(extra_values) < 2 * len(SharedResources.getInstance().validation_metric_names):
extra_values.append(float('nan'))

extra_values_description = list(rows_df.columns[upper_idx:])
for name in extra_metric_names:
if name not in extra_values_description:
extra_values_description.append(name)

self._pixelwise_metrics.append([thr_val] + pixelwise_values)
self._patientwise_metrics.append([thr_val] + patientwise_values)
self._objectwise_metrics.append([thr_val] + objectwise_values)

extra_values_cat = [[x, y] for x, y in zip(extra_values_description, extra_values)]
if extra_values_cat:
self._extra_metrics.append([thr_val] + extra_values_cat)

if len(self._extra_metrics) == 0:
self._extra_metrics = None

def is_complete(self):
"""

Expand Down
Loading