diff --git a/.gitignore b/.gitignore index 1ff31a5..be824cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +data_test/* +!data_test/.gitkeep +temp_data/* +!temp_data/.gitkeep +plastimatch-ubuntu_24_04 # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -103,6 +108,7 @@ celerybeat.pid # Environments .env +!docker_src/.env .venv env/ venv/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..fb13e0f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "plastimatch"] + path = plastimatch + url = https://gitlab.com/plastimatch/plastimatch.git diff --git a/README.md b/README.md index 9b155e8..70452fe 100644 --- a/README.md +++ b/README.md @@ -94,14 +94,14 @@ Note: provided you have a Google Cloud Platform project correctly set up, you wi # Ubuntu 22.04 LTS Plastimatch Docker Container -If you want to test Plastimatch for Ubuntu 22.04 LTS, you can use the Docker image we shared for this purpose under `dockerfiles`. +If you want to test Plastimatch for Ubuntu 22.04 LTS, you can use the Docker image we shared for this purpose under `docker_src`. ## Build the Docker Container To build the Ubuntu 22.04 LTS Platimatch Docker container, run the following commands from the root of the PyPlastimatch repository: ``` -cd dockerfiles/ +cd docker_src/ docker build --tag pypla_22.04 . --no-cache ``` diff --git a/data_test/.gitkeep b/data_test/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker_src/.dockerignore b/docker_src/.dockerignore new file mode 100644 index 0000000..9d5f649 --- /dev/null +++ b/docker_src/.dockerignore @@ -0,0 +1,3 @@ +.trunk +.git +*build diff --git a/docker_src/.env b/docker_src/.env new file mode 100644 index 0000000..74eaf50 --- /dev/null +++ b/docker_src/.env @@ -0,0 +1 @@ +HOST_HOME=${HOME} \ No newline at end of file diff --git a/docker_src/Dockerfile b/docker_src/Dockerfile new file mode 100644 index 0000000..55a9923 --- /dev/null +++ b/docker_src/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:latest + +RUN apt-get update && apt-get install -y \ + build-essential cmake git \ + libboost-all-dev libssl-dev \ + libzmq3-dev pkg-config \ + python3 python3-pip \ + python3-setuptools python3-wheel \ + wget libdcmtk-dev libdlib-dev libfftw3-dev \ + libinsighttoolkit5-dev \ + libpng-dev libtiff-dev uuid-dev zlib1g-dev \ + plastimatch +# apt-get clean && \ + # rm -rf /var/lib/apt/lists/* + +# RUN apt-get install pipx +RUN pip3 install --break-system-packages pyplastimatch +# RUN python3 -c 'from pyplastimatch.utils.install import install_precompiled_binaries; install_precompiled_binaries()' + +# ENTRYPOINT ["plastimatch"] + diff --git a/docker_src/command_docker.sh b/docker_src/command_docker.sh new file mode 100644 index 0000000..eb5fa5b --- /dev/null +++ b/docker_src/command_docker.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# to build the image and run the container +docker compose up --build -d +# to run the container without building the image +# docker compose up --no-build -d +# to enter the container +docker exec -it PyPlastimatch bash diff --git a/docker_src/docker-compose.yaml b/docker_src/docker-compose.yaml new file mode 100644 index 0000000..7fb7587 --- /dev/null +++ b/docker_src/docker-compose.yaml @@ -0,0 +1,15 @@ +services: + PyPlastimatch: + build: + context: ../ + dockerfile: docker_src/Dockerfile + image: pyplastimatch:latest + container_name: PyPlastimatch + stdin_open: true # docker run -i + tty: true # docker run -t + environment: + - HOST_HOME=${HOST_HOME} + - DEBIAN_FRONTEND=noninteractive + volumes: + - ../:/root/Software/pyplastimatch + - ${HOST_HOME}:/root/YourLocalHome \ No newline at end of file diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile deleted file mode 100644 index 464a852..0000000 --- a/dockerfiles/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -from ubuntu:22.04 - -RUN apt-get update && apt-get install -y \ - build-essential \ - cmake \ - git \ - libboost-all-dev \ - libssl-dev \ - libzmq3-dev \ - pkg-config \ - python3 \ - python3-pip \ - python3-setuptools \ - python3-wheel \ - wget \ - && rm -rf /var/lib/apt/lists/* - -RUN pip install pyplastimatch -RUN python3 -c 'from pyplastimatch.utils.install import install_precompiled_binaries; install_precompiled_binaries()' - -ENTRYPOINT ["plastimatch"] - diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index 53693c2..c8e4421 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -15,8 +15,9 @@ import os import json import subprocess -from typing import Dict - +from typing import Dict, List +from pathlib import Path +from collections import defaultdict ## ---------------------------------------- # FIXME: like this, every command is basically the same function with a line changed @@ -44,8 +45,10 @@ def convert(verbose = True, path_to_log_file = None, return_bash_command = False bash_command += ["plastimatch", "convert"] for key, val in kwargs.items(): + if "_" in key: + key = key.replace("_", "-") bash_command += ["--%s"%(key), val] - + if verbose: print("\nRunning 'plastimatch convert' with the specified arguments:") for key, val in kwargs.items(): @@ -282,3 +285,133 @@ def compare(path_to_reference_img, path_to_test_img, verbose = True) -> Dict[str return comparison_dict ## ---------------------------------------- + +def register( + global_params: Dict[str, str], + stage_params_list: List[Dict[str, str]] + ) -> Dict[str, float]: + """ + Purpose: + - To register two images using the plastimatch register command. The input to the command is a + text file called parm.txt. This text file has [Global] commands and [Stage] commands. + While the variables for the global commands stay the same throughout many stages of the registration, + the stage commands can be different for each stage. For the full list of these variables + look at https://plastimatch.org/registration_command_file_reference.html. + Here is an example of the parm.txt file: + [GLOBAL] + fixed=t5.mha + moving=t0.mha + image_out=warped.mha + vf_out=deformation.nrrd + + [STAGE] + xform=bspline + grid_spac=50 50 50 + + [STAGE] + grid_spac=20 20 20 + + Inputs: + - global_params: dict := a dictionary containing the global parameters for the registration. + The possible global parameters are: + - fixed: str := the path to the fixed image. + - moving: str := the path to the moving image. + - fixed_roi: str := the path to the fixed region of interest. + - moving_roi: str := the path to the moving region of interest. + - fixed_landmarks: str := the path to the fixed landmarks. + - moving_landmarks: str := the path to the moving landmarks. + - warped_landmarks: str := the path to the warped landmarks. + - xform_in: str := the path to the input transformation. + - xform_out: str := the path to the output transformation. + - vf_out: str := the path to the output vector field. + - img_out: str := the path to the output image. + - img_out_fmt: str := the format of the output image. + - img_out_type: str := the type of the output image. + - resample_when_linear: bool := whether to resample when linear. + - logfile: str := the path to the log file. + - stage_params_list: List[Dict[str, str]] := a list of dictionaries containing the stage parameters for the registration. + please look at the plastimatch documentation for the full list of possible stage parameters. + Outputs: + - registration_summary: Dict[str, float] := a dictionary containing the registration summary. + The possible keys are: + - pth_registered_data: str := the path to the registered data. + - log_file: str := the path to the log file. + """ + + # here are the possible global parameters for the registration + # some are optional, some are mandatory, we loop through them + # and create the command + global_param_possible_key_list = [ + "fixed", "moving", "fixed_roi", + "moving_roi", "fixed_landmarks", + "moving_landmarks","warped_landmarks", + "xform_in", "xform_out", "vf_out", + "image_out", "img_out_fmt", "img_out_type", + "resample_when_linear", "logfile" + ] + final_global_params = defaultdict(str) + # loop through the global parameters and create the command + for key in global_params: + if key in global_param_possible_key_list: + final_global_params[key] = global_params[key] + + # make sure the required global parameters are present + if "fixed" not in final_global_params: + raise ValueError("The fixed image is required.") + if "moving" not in final_global_params: + raise ValueError("The moving image is required.") + if "image_out" not in final_global_params: + raise ValueError("The output image is required.") + + # here are the possible stage parameters for the registration + # some are optional, some are mandatory, we loop through them + # and create the command + stage_param_possible_key_list = [ + "fixed_landmarks", "moving_landmarks", "warped_landmarks", "xform_out", + "xform", "vf_out", "img_out", "img_out_fmt", "img_out_type", + "resample_when_linear", "background_max", "convergence_tol", "default_value", + "demons_acceleration", "demons_filter_width", "demons_homogenization", "demons_std", + "demons_gradient_type", "demons_smooth_update_field", "demons_std_update_field", + "demons_smooth_deformation_field", "demons_std_deformation_field", "demons_step_length", + "grad_tol", "grid_spac", "gridsearch_min_overlap", "histoeq", "landmark_stiffness", + "lbfgsb_mmax", "mattes_fixed_minVal", "mattes_fixed_maxVal", "mattes_moving_minVal", + "mattes_moving_maxVal", "max_its", "max_step", "metric", "mi_histogram_bins", "min_its", + "min_step", "num_hist_levels_equal", "num_matching_points", "num_samples", "num_samples_pct", + "num_substages","optim", "optim_subtype", "pgtol", "regularization", "diffusion_penalty", + "curvature_penalty", "linear_elastic_multiplier", "third_order_penalty", + "total_displacement_penalty", "lame_coefficient_1", "lame_coefficient_2", "res", + "res_mm", "res_mm_fixed", "res_mm_moving", "res_vox", "res_vox_fixed", + "res_vox_moving", "rsg_grad_tol", "ss", "ss_fixed", "ss_moving", + "threading", "thresh_mean_intensity", "translation_scale_factor", + ] + # loop through the stage parameters and create the command for each stage + final_stage_params_list = [] + for stage_params in stage_params_list: + final_stage_params = defaultdict(str) + for key in stage_params: + if key in stage_param_possible_key_list: + final_stage_params[key] = stage_params[key] + final_stage_params_list.append(final_stage_params) + + # create the parm.txt file in the same directory as image_out + out_dir = Path(final_global_params["image_out"]).parent + os.makedirs(out_dir, exist_ok=True) + parm_txt_path = out_dir.joinpath("parm.txt") + param_txt = "[Global]\n" + for key in final_global_params: + param_txt += f"{key}={final_global_params[key]}\n" + param_txt += "\n" + for stage in final_stage_params_list: + param_txt += "[Stage]\n" + for key in stage: + param_txt += f"{key}={stage[key]}\n" + param_txt += "\n" + + with open(parm_txt_path, "w") as f: + f.write(param_txt) + + command = ["plastimatch", "register", str(parm_txt_path)] + try: + registration_summary = subprocess.run(command, capture_output = True, check = True) + except Exception as e: + print(e) diff --git a/pyplastimatch/tests/__init__.py b/pyplastimatch/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyplastimatch/tests/test_register.py b/pyplastimatch/tests/test_register.py new file mode 100644 index 0000000..4e7e82e --- /dev/null +++ b/pyplastimatch/tests/test_register.py @@ -0,0 +1,22 @@ +from pyplastimatch import register + +def test_register(): + pth_static = "../data_test/registration-tutorial/t5.mha" + pth_moving = "../data_test/registration-tutorial/t0.mha" + pth_output = "../data_test/test_output/registered.nrrd" + + global_params = { + "fixed" : f"{pth_static}", + "moving" : f"{pth_moving}", + "image_out" : f"{pth_output}", + } + + stage_params_list = [ + { + "xform": "bspline" + } + ] + register(global_params, stage_params_list) + +if __name__ == "__main__": + test_register() \ No newline at end of file diff --git a/pyplsti_api.py b/pyplsti_api.py new file mode 100644 index 0000000..660c1ca --- /dev/null +++ b/pyplsti_api.py @@ -0,0 +1,129 @@ +from fastapi import FastAPI, HTTPException +from subprocess import Popen, PIPE, run +# from fastapi import FastAPI +# from fastapi.responses import StreamingResponse + + +import subprocess +import os +from typing import Dict, List +from pathlib import Path +# from glob import glob +# from collections import defaultdict +# import json +from pydantic import BaseModel, field_validator, Field + +from pyplastimatch import register + +app = FastAPI() + +class Inputs_register(BaseModel): + r""" + ### Purpose: + - To define the input parameters for running image registration using pyplastimatch. + ### Attributes: + - global_params: a dictionary containing the global parameters for the registration. + - stage_params_list: a list of dictionaries defining the parameters for each stage of the registration. + please consult pyplastimatch.register() documentation for more information. + """ + + global_params: Dict[str, str] + stage_params_list: List[Dict[str, str]] + def __init__(self, **data): + super().__init__(**data) + dir_temp_data = Path(__file__).parent.joinpath("temp_data") + for key, value in self.global_params.items(): + if "temp_data/registration/" in value: + value = value.split("temp_data/registration/")[-1] + value = dir_temp_data.joinpath(value) + self.global_params[key] = value + +@app.post("/plastimatch_register") +def register_api( + all_registration_inputs: Inputs_register + ) -> None: + r""" + ### Purpose: + - To run image registration using pyplastimatch. + + ### Inputs: + - all_registration_inputs: an instance of Inputs_register containing the input parameters for the registration. + """ + print(f"static image: {all_registration_inputs.global_params['fixed']}") + print(f"moving image: {all_registration_inputs.global_params['moving']}") + print(f"output image: {all_registration_inputs.global_params['image_out']}") + print(f"output vf: {all_registration_inputs.global_params['vf_out']}") + register(all_registration_inputs.global_params, all_registration_inputs.stage_params_list) + +class Inputs_convert(BaseModel): + r""" + ### Purpose: + - To define the input parameters for running the convert command of pyplastimatch. + + ### Attributes: + - options: a dictionary containing the options for the convert command. + - input_file: the input file for the convert command. + """ + pth_input: Path | str = None + pth_output: Path | str = None + # these attributes will be filled from the options + xf: Path | str = None + + def __init__(self, **data): + dir_temp_data = Path(__file__).parent.joinpath("temp_data") + for key, value in data.items(): + if isinstance(value, str): + if "temp_data/registration/" in value: + value = value.split("temp_data/registration/")[-1] + value = dir_temp_data.joinpath(value) + data[key] = value + super().__init__(**data) + +@app.post("/plastimatch_convert") +def convert_api( + all_convert_inputs: Inputs_convert + ) -> None: + r""" + ### Purpose: + - To run the convert command of pyplastimatch. + ### Inputs: + - all_convert_inputs: an instance of Inputs_convert containing the input parameters for the convert command. + """ + from pyplastimatch import convert + convert( + input=all_convert_inputs.pth_input, + output_img=all_convert_inputs.pth_output, + xf=all_convert_inputs.xf + ) + +def test_register_api(): + pth_static = "../temp_data/static.nrrd" + pth_moving = "../temp_data/moving.nrrd" + pth_output = "../temp_data/registered.nrrd" + vf_out = "../temp_data/vf.nrrd" + + global_params = { + "fixed" : f"{pth_static}", + "moving" : f"{pth_moving}", + "image_out" : f"{pth_output}", + "vf_out" : f"{vf_out}", + } + + stage_params_list = [ + { + "xform": "bspline" + } + ] + inputs = Inputs_register(global_params=global_params, stage_params_list=stage_params_list) + register_api(inputs) + +def test_convert_api(): + pth_input = "../temp_data/moving.nrrd" + pth_output = "../temp_data/warped.nrrd" + xf = "../temp_data/vf.nrrd" + inputs = Inputs_convert(pth_input=pth_input, pth_output=pth_output, xf=xf) + convert_api(inputs) + +if __name__ == "__main__": + # test_register_api() + test_convert_api() \ No newline at end of file diff --git a/run_api.sh b/run_api.sh new file mode 100644 index 0000000..5513825 --- /dev/null +++ b/run_api.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +fastapi run ${HOME}/Software/pyplastimatch/pyplsti_api.py > /var/log/pyplastimatch.log diff --git a/temp_data/.gitkeep b/temp_data/.gitkeep new file mode 100644 index 0000000..e69de29