Skip to content
Merged
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
8 changes: 0 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
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 Down
89 changes: 84 additions & 5 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,118 @@ name: Build and Push Docker Image

on:
release:
types: [published, created]
types: [published]
workflow_dispatch:

jobs:
setup-test-data:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Download test resources
working-directory: tests
run: |
pip install requests
python -c "from download_resources import download_resources; download_resources('../test_data')"

- name: Upload test resources
uses: actions/upload-artifact@v4
with:
name: test-resources
path: ./test_data

build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Free disk space
run: |
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
docker system prune -af || true
docker builder prune -af || true
docker volume prune -f || true
sudo apt clean || true

- name: Install Python + toml parser
run: |
python -m pip install --upgrade pip
pip install toml
pip install toml nibabel numpy

- name: Extract package version
id: vars
run: |
PACKAGE_VERSION=$(python -c "import re; f=open('setup.py'); m=re.search(r'version\s*=\s*[\"\\\'](.+?)[\"\\\']', f.read()); print(m.group(1)) if m else exit(1)")
PACKAGE_VERSION=$(python setup.py --version)
PYTHON_VERSION=$(grep FROM Dockerfile | grep -oP 'python:\K[0-9]+\.[0-9]+' | tr -d '.')
FLAVOR=cpu
CUDA_VERSION=$(grep FROM Dockerfile_gpu | grep -oP 'cuda\K[0-9]+\.[0-9]+')
echo "tag=v${PACKAGE_VERSION}-py${PYTHON_VERSION}-${FLAVOR}" >> $GITHUB_OUTPUT
echo "tag_gpu=v${PACKAGE_VERSION}-py${PYTHON_VERSION}-cuda${CUDA_VERSION}" >> $GITHUB_OUTPUT

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}

- name: Build and push Docker image
- name: Build Docker image cpu
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: false
tags: dbouget/raidionics-rads:${{ steps.vars.outputs.tag }}

- name: Build Docker image gpu
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile_gpu
push: false
tags: dbouget/raidionics-rads:${{ steps.vars.outputs.tag_gpu }}

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

- name: List test resources
run: |
ls -R tests/unit_tests_results_dir

- name: Integration tests
env:
GITHUB_ACTIONS: true
IMAGE_TAG: ${{ steps.vars.outputs.tag }}
IMAGE_TAG_GPU: ${{ steps.vars.outputs.tag_gpu }}
run: |
pip install pytest
python -u -m pytest -s -vvv tests/local_docker_tests --log-cli-level=DEBUG

- name: Push Docker image cpu
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: dbouget/raidionics-rads:${{ steps.vars.outputs.tag }}
load: true

- name: Push Docker image gpu
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile_gpu
push: true
tags: dbouget/raidionics-rads:${{ steps.vars.outputs.tag }}
tags: dbouget/raidionics-rads:${{ steps.vars.outputs.tag_gpu }}
load: true
26 changes: 26 additions & 0 deletions Dockerfile_gpu
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Has to match the cuda version from your machine
FROM pytorch/pytorch:2.4.0-cuda12.4-cudnn9-runtime

MAINTAINER David Bouget <david.bouget@sintef.no>

ENV LANG=C.UTF-8 LC_ALL=C.UTF-8

RUN apt-get update -y
RUN apt-get upgrade -y
RUN apt-get -y install sudo
RUN apt-get update && apt-get install -y git

WORKDIR /workspace

# downloading source code (not necessary, mostly to run the test scripts)
RUN git clone https://github.com/dbouget/raidionics_rads_lib.git

# Python packages
RUN pip3 install --upgrade pip
RUN pip3 install -e raidionics_rads_lib
RUN pip3 install onnxruntime-gpu

RUN mkdir /workspace/resources

# CMD ["/bin/bash"]
ENTRYPOINT ["python3","/workspace/raidionics_rads_lib/main.py"]
4 changes: 3 additions & 1 deletion blank_main_config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ caller= # raidionics, slicer

[System]
ants_root= #Path to the install folder of a local cpp ANTs
gpu_id= #
gpu_id= # Number identifying the device to use
acceleration= # Processing acceleration which is cpu by default, to sample from: ["cpu", "torch"]
input_folder= # Folder path to the patient folder containing various radiological volumes
output_folder= # Destination folder where the results will be saved
model_folder= # Folder path containing the model to use
pipeline_filename= # Filepath for the pipeline to execute

[Runtime]
overlapping_ratio= # For patch-wise model, ratio between 0. and 1. indicating the amount of overlap for two consecutive patches
batch_size= # For running inference in parallel, specify an even number for more than 1
reconstruction_method= # String indicating how the final inference results should be presented, to sample from [thresholding, probabilities]
reconstruction_order= # # String indicating if the predictions should be resampled to the original space before reconstruction, to sample from [resample_first, resample_second]
use_stripped_data= # Boolean indicating if the inputs for have already been preprocessed (e.g., skull-stripped)
Expand Down
1 change: 1 addition & 0 deletions raidionicsrads/Pipelines/ClassificationStep.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def __perform_classification(self) -> None:
classification_config = configparser.ConfigParser()
classification_config.add_section('System')
classification_config.set('System', 'gpu_id', ResourcesConfiguration.getInstance().gpu_id)
classification_config.set('System', 'acceleration', ResourcesConfiguration.getInstance().system_acceleration)
classification_config.set('System', 'inputs_folder', os.path.join(self.working_folder, 'inputs'))
classification_config.set('System', 'output_folder', os.path.join(self.working_folder, 'outputs'))
classification_config.set('System', 'model_folder',
Expand Down
46 changes: 40 additions & 6 deletions raidionicsrads/Pipelines/SegmentationStep.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ def setup(self, patient_parameters: PatientParameters) -> None:
# for specifying the volume the annotation is linked to, if multiple inputs.
if not self._input_volume_uid:
self._input_volume_uid = volume_uid
existing_anno_uid = self._patient_parameters.get_all_annotations_uids_class_radiological_volume(
volume_uid=self._input_volume_uid,
annotation_class=get_type_from_enum_name(AnnotationClassType,
self._segmentation_targets[0]))
if len(existing_anno_uid) != 0:
self.skip = True
return

# Use-case where the input is actually an annotation and not a raw radiological volume
if input_json["labels"]:
annotation_type = get_type_from_enum_name(AnnotationClassType, input_json["labels"])
Expand Down Expand Up @@ -154,11 +162,20 @@ def execute(self) -> PatientParameters:
PatientParameters
Updated placeholder with the results of the current step.
"""
if self._input_volume_uid:
if ResourcesConfiguration.getInstance().diagnosis_task == 'neuro_diagnosis':
self.__perform_neuro_segmentation()
else:
self.__perform_mediastinum_segmentation()
if self.skip:
# An annotation object matching the request already exists, hence skipping the step.
logging.info("[SegmentationStep] Automatic segmentation skipped, results already existing.")
self.cleanup()
return self._patient_parameters

try:
if self._input_volume_uid:
if ResourcesConfiguration.getInstance().diagnosis_task == 'neuro_diagnosis':
self.__perform_neuro_segmentation()
else:
self.__perform_mediastinum_segmentation()
except Exception as e:
raise ValueError(f"[SegmentationStep] Process failed to run with: {e}.")
return self._patient_parameters

def cleanup(self):
Expand All @@ -182,11 +199,16 @@ def __perform_neuro_segmentation(self) -> None:
seg_config = configparser.ConfigParser()
seg_config.add_section('System')
seg_config.set('System', 'gpu_id', ResourcesConfiguration.getInstance().gpu_id)
seg_config.set('System', 'acceleration', ResourcesConfiguration.getInstance().system_acceleration)
seg_config.set('System', 'inputs_folder', os.path.join(self._working_folder, 'inputs'))
seg_config.set('System', 'output_folder', os.path.join(self._working_folder, 'outputs'))
seg_config.set('System', 'model_folder',
os.path.join(ResourcesConfiguration.getInstance().model_folder, self._model_name))
seg_config.add_section('Runtime')
seg_config.set('Runtime', 'batch_size',
str(ResourcesConfiguration.getInstance().predictions_batch_size))
seg_config.set('Runtime', 'overlapping_ratio',
str(ResourcesConfiguration.getInstance().predictions_overlapping_ratio))
seg_config.set('Runtime', 'reconstruction_method',
ResourcesConfiguration.getInstance().predictions_reconstruction_method)
if self._segmentation_output_type:
Expand Down Expand Up @@ -297,18 +319,30 @@ def __perform_mediastinum_segmentation(self):
seg_config = configparser.ConfigParser()
seg_config.add_section('System')
seg_config.set('System', 'gpu_id', ResourcesConfiguration.getInstance().gpu_id)
# seg_config.set('System', 'input_filename', self._input_volume_filepath)
seg_config.set('System', 'acceleration', ResourcesConfiguration.getInstance().system_acceleration)
seg_config.set('System', 'inputs_folder', os.path.join(self._working_folder, 'inputs'))
seg_config.set('System', 'output_folder', os.path.join(self._working_folder, 'outputs'))
seg_config.set('System', 'model_folder',
os.path.join(ResourcesConfiguration.getInstance().model_folder, self._model_name))
seg_config.add_section('Runtime')
seg_config.set('Runtime', 'batch_size',
str(ResourcesConfiguration.getInstance().predictions_batch_size))
seg_config.set('Runtime', 'overlapping_ratio',
str(ResourcesConfiguration.getInstance().predictions_overlapping_ratio))
seg_config.set('Runtime', 'reconstruction_method',
ResourcesConfiguration.getInstance().predictions_reconstruction_method)
if self._segmentation_output_type:
seg_config.set('Runtime', 'reconstruction_method', self._segmentation_output_type)
seg_config.set('Runtime', 'reconstruction_order', ResourcesConfiguration.getInstance().predictions_reconstruction_order)
seg_config.set('Runtime', 'use_preprocessed_data', "True" if ResourcesConfiguration.getInstance().predictions_use_stripped_data else "False")
seg_config.set('Runtime', 'folds_ensembling',
str(ResourcesConfiguration.getInstance().predictions_folds_ensembling))
seg_config.set('Runtime', 'ensembling_strategy',
ResourcesConfiguration.getInstance().predictions_ensembling_strategy)
seg_config.set('Runtime', 'test_time_augmentation_iteration',
str(ResourcesConfiguration.getInstance().predictions_test_time_augmentation_iterations))
seg_config.set('Runtime', 'test_time_augmentation_fusion_mode',
ResourcesConfiguration.getInstance().predictions_test_time_augmentation_fusion_mode)

# @TODO. Have to be slightly improved, but should be working for our use-cases for now.
existing_lungs_annotations = self._patient_parameters.get_all_annotations_uids_class_radiological_volume(
Expand Down
20 changes: 20 additions & 0 deletions raidionicsrads/Utils/configuration_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,15 @@ def __reset(self):
self.caller = None

self.gpu_id = "-1"
self.system_acceleration = "cpu"
self.input_folder = None
self.output_folder = None
self.model_folder = None
self.pipeline_filename = None

# Parameters matching the main_config parameters from the raidionics_seg backend
self.predictions_overlapping_ratio = 0.
self.predictions_batch_size = 1
self.predictions_reconstruction_method = None
self.predictions_reconstruction_order = None
self.predictions_use_stripped_data = False
Expand Down Expand Up @@ -462,6 +464,16 @@ def __parse_system_parameters(self):
logging.debug(f"ANTs scripts directory: {self.ants_reg_dir}")
logging.debug(f"ANTs binary directory: {self.ants_apply_dir}")

if self.config.has_option("System", "gpu_id"):
if self.config["System"]["gpu_id"].split("#")[0].strip() != "":
self.gpu_id = self.config["System"]["gpu_id"].split("#")[0].strip()

if self.config.has_option("System", "acceleration"):
if self.config["System"]["acceleration"].split("#")[0].strip() != "":
self.system_acceleration = self.config["System"]["acceleration"].split("#")[0].strip()
if self.system_acceleration not in ["cpu", "torch"]:
self.system_acceleration = "cpu"

if self.config.has_option('System', 'output_folder'):
if self.config['System']['output_folder'].split('#')[0].strip() != '':
self.output_folder = self.config['System']['output_folder'].split('#')[0].strip()
Expand All @@ -482,6 +494,14 @@ def __parse_runtime_parameters(self):
if self.config.has_option('Runtime', 'overlapping_ratio'):
if self.config['Runtime']['overlapping_ratio'].split('#')[0].strip() != '':
self.predictions_overlapping_ratio = float(self.config['Runtime']['overlapping_ratio'].split('#')[0].strip())
if self.predictions_overlapping_ratio < 0.:
self.predictions_overlapping_ratio = 0.

if self.config.has_option('Runtime', 'batch_size'):
if self.config['Runtime']['batch_size'].split('#')[0].strip() != '':
self.predictions_batch_size = int(self.config['Runtime']['batch_size'].split('#')[0].strip())
if self.predictions_batch_size != 1 and self.predictions_batch_size % 2 != 0:
self.predictions_batch_size = self.predictions_batch_size + 1

if self.config.has_option('Runtime', 'reconstruction_method'):
if self.config['Runtime']['reconstruction_method'].split('#')[0].strip() != '':
Expand Down
Binary file modified requirements.txt
Binary file not shown.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
],
},
python_requires=">=3.9",
version='1.3.0',
version='1.3.1',
author='David Bouget (david.bouget@sintef.no)',
license='BSD 2-Clause',
description='Raidionics reporting and data system backend (RADS)',
Expand Down
6 changes: 3 additions & 3 deletions tests/download_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

def download_resources(test_dir: str):
try:
test1_image_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-RaidionicsRADSLib-UnitTest1.zip'
test2_image_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-RaidionicsRADSLib-UnitTest2.zip'
test3_image_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-RaidionicsRADSLib-UnitTest3-Mediastinum.zip'
test1_image_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-RaidionicsRADSLib-UnitTest1-v1.3.1.zip'
test2_image_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-RaidionicsRADSLib-UnitTest2-v1.3.1.zip'
test3_image_url = 'https://github.com/raidionics/Raidionics-models/releases/download/v1.3.0-rc/Samples-RaidionicsRADSLib-UnitTest3-Mediastinum-v1.3.1.zip'
dest_dir = os.path.join(test_dir, "patients")
if os.path.exists(dest_dir):
shutil.rmtree(dest_dir)
Expand Down
Loading