From 18d9f5aa8b3846437231cc1e36db38c316fc8c79 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Mon, 20 Jan 2025 15:22:36 -0500 Subject: [PATCH 01/22] added submodule --- .gitmodules | 3 +++ plastimatch | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 plastimatch 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/plastimatch b/plastimatch new file mode 160000 index 0000000..5036f97 --- /dev/null +++ b/plastimatch @@ -0,0 +1 @@ +Subproject commit 5036f97b213d37a0a07dd232c827fb5f35049ae4 From ee4d8c454272481051508efa1c0f0714526ccf44 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Mon, 20 Jan 2025 15:57:53 -0500 Subject: [PATCH 02/22] trying setup a clean docker file and installation script --- .gitignore | 1 + dockerfiles/.dockerignore | 3 +++ dockerfiles/Dockerfile | 14 ++++++++++---- dockerfiles/command_docker.sh | 7 +++++++ dockerfiles/docker-compose.yaml | 15 +++++++++++++++ plastimatch | 1 - pyplastimatch/pyplastimatch.py | 17 +++++++++++++++++ 7 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 dockerfiles/.dockerignore create mode 100644 dockerfiles/command_docker.sh create mode 100644 dockerfiles/docker-compose.yaml delete mode 160000 plastimatch diff --git a/.gitignore b/.gitignore index 1ff31a5..7373cf4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +plastimatch-ubuntu_24_04 # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/dockerfiles/.dockerignore b/dockerfiles/.dockerignore new file mode 100644 index 0000000..9d5f649 --- /dev/null +++ b/dockerfiles/.dockerignore @@ -0,0 +1,3 @@ +.trunk +.git +*build diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index 464a852..3ad2dc3 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -1,4 +1,4 @@ -from ubuntu:22.04 +FROM ubuntu:latest RUN apt-get update && apt-get install -y \ build-essential \ @@ -15,8 +15,14 @@ RUN apt-get update && apt-get install -y \ 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()' +RUN apt-get install -y g++ make git cmake-curses-gui \ + libblas-dev liblapack-dev libsqlite3-dev \ + libdcmtk-dev libdlib-dev libfftw3-dev \ + libinsighttoolkit5-dev \ + libpng-dev libtiff-dev uuid-dev zlib1g-dev +# 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"] +# ENTRYPOINT ["plastimatch"] diff --git a/dockerfiles/command_docker.sh b/dockerfiles/command_docker.sh new file mode 100644 index 0000000..dcd25a8 --- /dev/null +++ b/dockerfiles/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 BrachyUtils bash diff --git a/dockerfiles/docker-compose.yaml b/dockerfiles/docker-compose.yaml new file mode 100644 index 0000000..049d3e3 --- /dev/null +++ b/dockerfiles/docker-compose.yaml @@ -0,0 +1,15 @@ +services: + PyPlastimatch: + build: + context: ../ + dockerfile: dockerfiles/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/plastimatch b/plastimatch deleted file mode 160000 index 5036f97..0000000 --- a/plastimatch +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5036f97b213d37a0a07dd232c827fb5f35049ae4 diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index 53693c2..9626733 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -16,6 +16,7 @@ import json import subprocess from typing import Dict +from pathlib import Path ## ---------------------------------------- @@ -282,3 +283,19 @@ def compare(path_to_reference_img, path_to_test_img, verbose = True) -> Dict[str return comparison_dict ## ---------------------------------------- + +def register( + pth_fixed: Path | str, + pth_moving: Path | str, + pth_fixed_roi: Path | str = None, + pth_moving_roi: Path | str = None, + fixed_landmarks: Path | str = None, + moving_landmarks: Path | str = None, + warped_landmarks: Path | str = None, + xform_in + verbose = True, + ) -> Dict[str, float]: + """ + Purpose: + To register two images using the plastimatch register command. + """ \ No newline at end of file From 56b1c0b7e51de133fdb8f6700125d329a7ac97f1 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Mon, 20 Jan 2025 16:14:20 -0500 Subject: [PATCH 03/22] renamed docker folder --- {dockerfiles => docker_src}/.dockerignore | 0 {dockerfiles => docker_src}/Dockerfile | 0 {dockerfiles => docker_src}/command_docker.sh | 0 {dockerfiles => docker_src}/docker-compose.yaml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {dockerfiles => docker_src}/.dockerignore (100%) rename {dockerfiles => docker_src}/Dockerfile (100%) rename {dockerfiles => docker_src}/command_docker.sh (100%) rename {dockerfiles => docker_src}/docker-compose.yaml (100%) diff --git a/dockerfiles/.dockerignore b/docker_src/.dockerignore similarity index 100% rename from dockerfiles/.dockerignore rename to docker_src/.dockerignore diff --git a/dockerfiles/Dockerfile b/docker_src/Dockerfile similarity index 100% rename from dockerfiles/Dockerfile rename to docker_src/Dockerfile diff --git a/dockerfiles/command_docker.sh b/docker_src/command_docker.sh similarity index 100% rename from dockerfiles/command_docker.sh rename to docker_src/command_docker.sh diff --git a/dockerfiles/docker-compose.yaml b/docker_src/docker-compose.yaml similarity index 100% rename from dockerfiles/docker-compose.yaml rename to docker_src/docker-compose.yaml From 1258c08339b40c71c8094a734eb760969d6ac0e3 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Mon, 20 Jan 2025 17:00:38 -0500 Subject: [PATCH 04/22] got the image bulding to work. plastimatch command runs on the cli. it is to be tested from inside the python --- docker_src/Dockerfile | 6 ++---- docker_src/command_docker.sh | 6 +++--- docker_src/docker-compose.yaml | 2 +- pyplastimatch/pyplastimatch.py | 19 +++++++++++++++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/docker_src/Dockerfile b/docker_src/Dockerfile index 3ad2dc3..3164f8e 100644 --- a/docker_src/Dockerfile +++ b/docker_src/Dockerfile @@ -12,11 +12,9 @@ RUN apt-get update && apt-get install -y \ python3-pip \ python3-setuptools \ python3-wheel \ - wget \ - && rm -rf /var/lib/apt/lists/* + wget -RUN apt-get install -y g++ make git cmake-curses-gui \ - libblas-dev liblapack-dev libsqlite3-dev \ +RUN apt-get install -y g++ make git \ libdcmtk-dev libdlib-dev libfftw3-dev \ libinsighttoolkit5-dev \ libpng-dev libtiff-dev uuid-dev zlib1g-dev diff --git a/docker_src/command_docker.sh b/docker_src/command_docker.sh index dcd25a8..397a2d8 100644 --- a/docker_src/command_docker.sh +++ b/docker_src/command_docker.sh @@ -1,7 +1,7 @@ #!/bin/bash # to build the image and run the container -docker compose up --build -d +# docker compose up --build -d # to run the container without building the image -# docker compose up --no-build -d +docker compose up --no-build -d # to enter the container -docker exec -it BrachyUtils bash +docker exec -it PyPlastimatch bash diff --git a/docker_src/docker-compose.yaml b/docker_src/docker-compose.yaml index 049d3e3..7fb7587 100644 --- a/docker_src/docker-compose.yaml +++ b/docker_src/docker-compose.yaml @@ -2,7 +2,7 @@ services: PyPlastimatch: build: context: ../ - dockerfile: dockerfiles/Dockerfile + dockerfile: docker_src/Dockerfile image: pyplastimatch:latest container_name: PyPlastimatch stdin_open: true # docker run -i diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index 9626733..75534c1 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -292,10 +292,25 @@ def register( fixed_landmarks: Path | str = None, moving_landmarks: Path | str = None, warped_landmarks: Path | str = None, - xform_in + xform_in: str = None, + xform_out: str = None, + vf_out: str = None, + img_out: Path | str = None, + img_out_fmt: str = "auto", + img_out_type: str = "auto", + resample_when_linear: bool = True, + logfile: Path | str = None, verbose = True, ) -> Dict[str, float]: """ Purpose: To register two images using the plastimatch register command. - """ \ No newline at end of file + """ + + + def test(a , b, **kwargs): + print(a, b) + myj = load_defaulat_json() + for key, val in kwargs.items(): + if + myj[key] = val From 3da672b39a64d6f87b9574f7487f16b1a7ac313c Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Tue, 21 Jan 2025 11:03:01 -0500 Subject: [PATCH 05/22] made some slolid progress on the registration. we ge the input now, let's finish calling plastimatch --- pyplastimatch/pyplastimatch.py | 133 ++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 26 deletions(-) diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index 75534c1..a12b887 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -15,9 +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 @@ -285,32 +285,113 @@ def compare(path_to_reference_img, path_to_test_img, verbose = True) -> Dict[str ## ---------------------------------------- def register( - pth_fixed: Path | str, - pth_moving: Path | str, - pth_fixed_roi: Path | str = None, - pth_moving_roi: Path | str = None, - fixed_landmarks: Path | str = None, - moving_landmarks: Path | str = None, - warped_landmarks: Path | str = None, - xform_in: str = None, - xform_out: str = None, - vf_out: str = None, - img_out: Path | str = None, - img_out_fmt: str = "auto", - img_out_type: str = "auto", - resample_when_linear: bool = True, - logfile: Path | str = None, - verbose = True, + 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. + - 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. """ - - def test(a , b, **kwargs): - print(a, b) - myj = load_defaulat_json() - for key, val in kwargs.items(): - if - myj[key] = val + # 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", + "img_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", + "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_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 + parm_txt_path = out_dir.joinpath("parm.txt") + \ No newline at end of file From 9a56389f46102c31053d950ddd8e1b270880edbf Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Tue, 21 Jan 2025 11:53:13 -0500 Subject: [PATCH 06/22] put down the draft way to call register on plasti match. let's test it now --- pyplastimatch/pyplastimatch.py | 21 +++++++++++++++++++-- pyplastimatch/tests/__init__.py | 0 pyplastimatch/tests/test_register.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 pyplastimatch/tests/__init__.py create mode 100644 pyplastimatch/tests/test_register.py diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index a12b887..3161c91 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -352,7 +352,7 @@ def register( 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.") @@ -394,4 +394,21 @@ def register( # create the parm.txt file in the same directory as image_out out_dir = Path(final_global_params["image_out"]).parent parm_txt_path = out_dir.joinpath("parm.txt") - \ No newline at end of file + 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..6cdd671 --- /dev/null +++ b/pyplastimatch/tests/test_register.py @@ -0,0 +1,12 @@ +from PyPlastimatch import register + +def test_register(): + pth_static = "" + pth_moving = "" + pth_output = "" + + global_params = { + "fixed" : f"{pth_static}", + "moving" : f"{pth_moving}", + "img_out" : f"{pth_output}", + } From a0ea798ee3793cdaa4b801ecea273a7e68e8f425 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Tue, 21 Jan 2025 11:58:10 -0500 Subject: [PATCH 07/22] downloaded the sample data for testing of pyplastimatch --- .gitignore | 3 +++ data_test/.gitkeep | 0 2 files changed, 3 insertions(+) create mode 100644 data_test/.gitkeep diff --git a/.gitignore b/.gitignore index 7373cf4..1ab2bcd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +data_test/* +!data_test/.gitkeep + plastimatch-ubuntu_24_04 # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/data_test/.gitkeep b/data_test/.gitkeep new file mode 100644 index 0000000..e69de29 From 81d184547060b448e58d6adf17504a7183b148e9 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Tue, 21 Jan 2025 13:21:59 -0500 Subject: [PATCH 08/22] wrote the first test --- pyplastimatch/tests/test_register.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pyplastimatch/tests/test_register.py b/pyplastimatch/tests/test_register.py index 6cdd671..cd405f0 100644 --- a/pyplastimatch/tests/test_register.py +++ b/pyplastimatch/tests/test_register.py @@ -1,12 +1,22 @@ -from PyPlastimatch import register +from pyplastimatch import register def test_register(): - pth_static = "" - pth_moving = "" - pth_output = "" - + 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}", "img_out" : f"{pth_output}", } + + stage_params_list = [ + { + "xform": "bspline" + } + ] + register(global_params, stage_params) + +if __name__ == "__main__": + test_register() \ No newline at end of file From 3fe25163bb8cbd2867b252f9e02613e0bc1caeb5 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Tue, 21 Jan 2025 13:40:12 -0500 Subject: [PATCH 09/22] runnign registration worked for the basic example case --- docker_src/Dockerfile | 11 ++++++----- pyplastimatch/pyplastimatch.py | 5 +++-- pyplastimatch/tests/test_register.py | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docker_src/Dockerfile b/docker_src/Dockerfile index 3164f8e..62dd5d2 100644 --- a/docker_src/Dockerfile +++ b/docker_src/Dockerfile @@ -12,12 +12,13 @@ RUN apt-get update && apt-get install -y \ python3-pip \ python3-setuptools \ python3-wheel \ - wget - -RUN apt-get install -y g++ make git \ - libdcmtk-dev libdlib-dev libfftw3-dev \ + wget libdcmtk-dev libdlib-dev libfftw3-dev \ libinsighttoolkit5-dev \ - libpng-dev libtiff-dev uuid-dev zlib1g-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()' diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index 3161c91..78e4c46 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -344,7 +344,7 @@ def register( "moving_roi", "fixed_landmarks", "moving_landmarks","warped_landmarks", "xform_in", "xform_out", "vf_out", - "img_out", "img_out_fmt", "img_out_type", + "image_out", "img_out_fmt", "img_out_type", "resample_when_linear", "logfile" ] final_global_params = defaultdict(str) @@ -366,7 +366,7 @@ def register( # and create the command stage_param_possible_key_list = [ "fixed_landmarks", "moving_landmarks", "warped_landmarks", "xform_out", - "vf_out", "img_out", "img_out_fmt", "img_out_type", + "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", @@ -393,6 +393,7 @@ def register( # 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: diff --git a/pyplastimatch/tests/test_register.py b/pyplastimatch/tests/test_register.py index cd405f0..4e7e82e 100644 --- a/pyplastimatch/tests/test_register.py +++ b/pyplastimatch/tests/test_register.py @@ -8,7 +8,7 @@ def test_register(): global_params = { "fixed" : f"{pth_static}", "moving" : f"{pth_moving}", - "img_out" : f"{pth_output}", + "image_out" : f"{pth_output}", } stage_params_list = [ @@ -16,7 +16,7 @@ def test_register(): "xform": "bspline" } ] - register(global_params, stage_params) + register(global_params, stage_params_list) if __name__ == "__main__": test_register() \ No newline at end of file From b04c7d9e15a9d7617c23e501b28b34fcaf8db754 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Tue, 21 Jan 2025 17:12:59 -0500 Subject: [PATCH 10/22] just organized the docker file a bit better --- docker_src/Dockerfile | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docker_src/Dockerfile b/docker_src/Dockerfile index 62dd5d2..99589b4 100644 --- a/docker_src/Dockerfile +++ b/docker_src/Dockerfile @@ -1,17 +1,11 @@ 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 \ + 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 \ From a32c97b86dd3c764324ebb9b098c35e43845a661 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Wed, 22 Jan 2025 08:08:17 -0500 Subject: [PATCH 11/22] wrote a simple api for registration --- .gitignore | 3 ++- pyplsti_api.py | 41 +++++++++++++++++++++++++++++++++++++++++ temp_data/.gitkeep | 0 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 pyplsti_api.py create mode 100644 temp_data/.gitkeep diff --git a/.gitignore b/.gitignore index 1ab2bcd..8c17385 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ data_test/* !data_test/.gitkeep - +temp_data/* +!temp_data/.gitkeep plastimatch-ubuntu_24_04 # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/pyplsti_api.py b/pyplsti_api.py new file mode 100644 index 0000000..05b50b1 --- /dev/null +++ b/pyplsti_api.py @@ -0,0 +1,41 @@ +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 + +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]] + +app = FastAPI() + +@app.post("/register") +def register( + all_inputs: Inputs_register + ) -> None: + r""" + Purpose: + - To run image registration using pyplastimatch. + """ + register(all_inputs.global_params, all_inputs.stage_params_list) \ No newline at end of file diff --git a/temp_data/.gitkeep b/temp_data/.gitkeep new file mode 100644 index 0000000..e69de29 From 7659a4b0c1b5dae4d25f6e7a6248289e8825b522 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Wed, 22 Jan 2025 08:38:35 -0500 Subject: [PATCH 12/22] wrote a bash script to run the api --- run_api.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 run_api.sh 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 From 467221b2940a79c688136fd74404c4b7bd99303b Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Wed, 22 Jan 2025 11:49:03 -0500 Subject: [PATCH 13/22] updated the input model in the api to handle paths to the temp_data --- pyplsti_api.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/pyplsti_api.py b/pyplsti_api.py index 05b50b1..3041834 100644 --- a/pyplsti_api.py +++ b/pyplsti_api.py @@ -27,15 +27,44 @@ class Inputs_register(BaseModel): 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") + self.global_params["fixed"] = dir_temp_data.joinpath(self.global_params.get("fixed")) + self.global_params["moving"] = dir_temp_data.joinpath(self.global_params.get("moving")) + self.global_params["image_out"] = dir_temp_data.joinpath(self.global_params.get("image_out")) app = FastAPI() -@app.post("/register") -def register( +@app.post("/plastimatch_register") +def register_api( all_inputs: Inputs_register ) -> None: r""" Purpose: - To run image registration using pyplastimatch. """ - register(all_inputs.global_params, all_inputs.stage_params_list) \ No newline at end of file + register(all_inputs.global_params, all_inputs.stage_params_list) + + +def test_register_api(): + pth_static = "static.nrrd" + pth_moving = "moving.nrrd" + pth_output = "registered.nrrd" + + global_params = { + "fixed" : f"{pth_static}", + "moving" : f"{pth_moving}", + "image_out" : f"{pth_output}", + } + + stage_params_list = [ + { + "xform": "bspline" + } + ] + inputs = Inputs_register(global_params=global_params, stage_params_list=stage_params_list) + register_api(inputs) + +if __name__ == "__main__": + test_register_api() \ No newline at end of file From a9b460c1f5241fbf07b59705abb66cd2e617bb44 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Thu, 23 Jan 2025 10:48:17 -0500 Subject: [PATCH 14/22] added some print statement for the logs! --- pyplsti_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyplsti_api.py b/pyplsti_api.py index 3041834..916598a 100644 --- a/pyplsti_api.py +++ b/pyplsti_api.py @@ -44,6 +44,9 @@ def register_api( Purpose: - To run image registration using pyplastimatch. """ + print(f"static image: {all_inputs.global_params['fixed']}") + print(f"moving image: {all_inputs.global_params['moving']}") + print(f"output image: {all_inputs.global_params['image_out']}") register(all_inputs.global_params, all_inputs.stage_params_list) From 6508b187f6fc556cb4fe2237ee9ec660703e2e93 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Thu, 23 Jan 2025 11:26:05 -0500 Subject: [PATCH 15/22] put a .env file that helps out docker compose with environment variables --- .gitignore | 1 + docker_src/.env | 1 + 2 files changed, 2 insertions(+) create mode 100644 docker_src/.env diff --git a/.gitignore b/.gitignore index 8c17385..be824cd 100644 --- a/.gitignore +++ b/.gitignore @@ -108,6 +108,7 @@ celerybeat.pid # Environments .env +!docker_src/.env .venv env/ venv/ 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 From 81bdca7016b9de20d73598949513ab6a2c7d8e33 Mon Sep 17 00:00:00 2001 From: hosseinjafar <43022986+hosseinjafar@users.noreply.github.com> Date: Sat, 25 Jan 2025 07:59:45 -0500 Subject: [PATCH 16/22] Update README.md renamed dockerfiles with docker_src since this folder contains more than dockerfile. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ``` From 93410c5a13b896b44cfc18e06186141260d2d0b5 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Thu, 13 Feb 2025 11:08:25 -0500 Subject: [PATCH 17/22] exporing transforms as nrrd --- pyplsti_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyplsti_api.py b/pyplsti_api.py index 916598a..a16752b 100644 --- a/pyplsti_api.py +++ b/pyplsti_api.py @@ -33,6 +33,7 @@ def __init__(self, **data): self.global_params["fixed"] = dir_temp_data.joinpath(self.global_params.get("fixed")) self.global_params["moving"] = dir_temp_data.joinpath(self.global_params.get("moving")) self.global_params["image_out"] = dir_temp_data.joinpath(self.global_params.get("image_out")) + self.global_params["vf_out"] = dir_temp_data.joinpath(self.global_params.get("vf_out")) app = FastAPI() @@ -47,9 +48,9 @@ def register_api( print(f"static image: {all_inputs.global_params['fixed']}") print(f"moving image: {all_inputs.global_params['moving']}") print(f"output image: {all_inputs.global_params['image_out']}") + print(f"output vf: {all_inputs.global_params['vf_out']}") register(all_inputs.global_params, all_inputs.stage_params_list) - def test_register_api(): pth_static = "static.nrrd" pth_moving = "moving.nrrd" From 0c7d8c889af0c50eef4d296a371f9dcdf6654812 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Wed, 26 Feb 2025 14:29:23 -0500 Subject: [PATCH 18/22] made a bunch of changes to work with convert over the api. the changes include:\n 1. replacing _ with - in the inputs for the convert command.\n 2. automatically removing \temp\ temp_data/ from all the inputs and adding the correct path for temp_data. implementing the api call for conver --- pyplastimatch/pyplastimatch.py | 4 +- pyplsti_api.py | 108 +++++++++++++++++++++++++++------ 2 files changed, 91 insertions(+), 21 deletions(-) diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index 78e4c46..da11038 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -45,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(): diff --git a/pyplsti_api.py b/pyplsti_api.py index a16752b..813d4f3 100644 --- a/pyplsti_api.py +++ b/pyplsti_api.py @@ -15,11 +15,13 @@ from pyplastimatch import register +app = FastAPI() + class Inputs_register(BaseModel): r""" - Purpose: + ### Purpose: - To define the input parameters for running image registration using pyplastimatch. - Attributes: + ### 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. @@ -30,38 +32,94 @@ class Inputs_register(BaseModel): def __init__(self, **data): super().__init__(**data) dir_temp_data = Path(__file__).parent.joinpath("temp_data") - self.global_params["fixed"] = dir_temp_data.joinpath(self.global_params.get("fixed")) - self.global_params["moving"] = dir_temp_data.joinpath(self.global_params.get("moving")) - self.global_params["image_out"] = dir_temp_data.joinpath(self.global_params.get("image_out")) - self.global_params["vf_out"] = dir_temp_data.joinpath(self.global_params.get("vf_out")) - -app = FastAPI() + for key, value in self.global_params.items(): + if "temp_data/" in value: + value = value.split("temp_data/")[-1] + value = dir_temp_data.joinpath(value) + self.global_params[key] = value @app.post("/plastimatch_register") def register_api( - all_inputs: Inputs_register + all_registration_inputs: Inputs_register ) -> None: r""" - Purpose: + ### 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 + options: Dict[str, 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/" in value: + value = value.split("temp_data/")[-1] + value = dir_temp_data.joinpath(value) + if key == "options": + for key_opt, value_opt in value.items(): + if "temp_data/" in value_opt: + value_opt = value_opt.split("temp_data/")[-1] + value[key_opt] = dir_temp_data.joinpath(value_opt) + data[key] = value + + super().__init__() + self.pth_input = data.get("pth_input") + self.pth_output = data.get("pth_output") + self.xf = data.get("options").get("xf") + +@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. """ - print(f"static image: {all_inputs.global_params['fixed']}") - print(f"moving image: {all_inputs.global_params['moving']}") - print(f"output image: {all_inputs.global_params['image_out']}") - print(f"output vf: {all_inputs.global_params['vf_out']}") - register(all_inputs.global_params, all_inputs.stage_params_list) + 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 = "static.nrrd" - pth_moving = "moving.nrrd" - pth_output = "registered.nrrd" + 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" @@ -70,5 +128,15 @@ def test_register_api(): 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" + options = { + "xf": "vf.nrrd" + } + inputs = Inputs_convert(pth_input=pth_input, pth_output=pth_output, options=options) + convert_api(inputs) + if __name__ == "__main__": - test_register_api() \ No newline at end of file + # test_register_api() + test_convert_api() \ No newline at end of file From 695c4e81e483c871eceba5768aa94115d7650d18 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Wed, 26 Feb 2025 15:02:06 -0500 Subject: [PATCH 19/22] simplified the api and tested it over the web. good to go to the next stage --- pyplsti_api.py | 19 +++---------------- run_api.sh | 2 +- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/pyplsti_api.py b/pyplsti_api.py index 813d4f3..de9d107 100644 --- a/pyplsti_api.py +++ b/pyplsti_api.py @@ -66,8 +66,6 @@ class Inputs_convert(BaseModel): """ pth_input: Path | str = None pth_output: Path | str = None - options: Dict[str, str] = None - # these attributes will be filled from the options xf: Path | str = None @@ -78,17 +76,8 @@ def __init__(self, **data): if "temp_data/" in value: value = value.split("temp_data/")[-1] value = dir_temp_data.joinpath(value) - if key == "options": - for key_opt, value_opt in value.items(): - if "temp_data/" in value_opt: - value_opt = value_opt.split("temp_data/")[-1] - value[key_opt] = dir_temp_data.joinpath(value_opt) data[key] = value - - super().__init__() - self.pth_input = data.get("pth_input") - self.pth_output = data.get("pth_output") - self.xf = data.get("options").get("xf") + super().__init__(**data) @app.post("/plastimatch_convert") def convert_api( @@ -131,10 +120,8 @@ def test_register_api(): def test_convert_api(): pth_input = "../temp_data/moving.nrrd" pth_output = "../temp_data/warped.nrrd" - options = { - "xf": "vf.nrrd" - } - inputs = Inputs_convert(pth_input=pth_input, pth_output=pth_output, options=options) + xf = "../temp_data/vf.nrrd" + inputs = Inputs_convert(pth_input=pth_input, pth_output=pth_output, xf=xf) convert_api(inputs) if __name__ == "__main__": diff --git a/run_api.sh b/run_api.sh index 5513825..bc9644f 100644 --- a/run_api.sh +++ b/run_api.sh @@ -1,3 +1,3 @@ #!/bin/bash -fastapi run ${HOME}/Software/pyplastimatch/pyplsti_api.py > /var/log/pyplastimatch.log +fastapi dev ${HOME}/Software/pyplastimatch/pyplsti_api.py > /var/log/pyplastimatch.log From c411d5eda5ea2db604744aae9fadc0149e4e19a1 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Mon, 3 Mar 2025 15:58:00 -0500 Subject: [PATCH 20/22] very important bug fix. fastapi needs to be run with the run command and not the dev command for the other containers to notice it --- run_api.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_api.sh b/run_api.sh index bc9644f..5513825 100644 --- a/run_api.sh +++ b/run_api.sh @@ -1,3 +1,3 @@ #!/bin/bash -fastapi dev ${HOME}/Software/pyplastimatch/pyplsti_api.py > /var/log/pyplastimatch.log +fastapi run ${HOME}/Software/pyplastimatch/pyplsti_api.py > /var/log/pyplastimatch.log From 8a74f8c59e35ccb681a51db1e9606eeb3de1e862 Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Wed, 16 Apr 2025 16:18:28 -0400 Subject: [PATCH 21/22] played with the temp file paths --- pyplastimatch/pyplastimatch.py | 2 +- pyplsti_api.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyplastimatch/pyplastimatch.py b/pyplastimatch/pyplastimatch.py index da11038..c8e4421 100644 --- a/pyplastimatch/pyplastimatch.py +++ b/pyplastimatch/pyplastimatch.py @@ -377,7 +377,7 @@ def register( "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_subtype", "pgtol", "regularization", "diffusion_penalty", + "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", diff --git a/pyplsti_api.py b/pyplsti_api.py index de9d107..660c1ca 100644 --- a/pyplsti_api.py +++ b/pyplsti_api.py @@ -33,8 +33,8 @@ 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/" in value: - value = value.split("temp_data/")[-1] + if "temp_data/registration/" in value: + value = value.split("temp_data/registration/")[-1] value = dir_temp_data.joinpath(value) self.global_params[key] = value @@ -73,8 +73,8 @@ 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/" in value: - value = value.split("temp_data/")[-1] + 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) From 43726dc1dc3c28deb308989330513597dc30c98d Mon Sep 17 00:00:00 2001 From: hosseinjafar Date: Tue, 1 Jul 2025 16:51:33 -0400 Subject: [PATCH 22/22] simpler docker i guess --- docker_src/Dockerfile | 6 +++--- docker_src/command_docker.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker_src/Dockerfile b/docker_src/Dockerfile index 99589b4..55a9923 100644 --- a/docker_src/Dockerfile +++ b/docker_src/Dockerfile @@ -9,9 +9,9 @@ RUN apt-get update && apt-get install -y \ 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/* + plastimatch +# apt-get clean && \ + # rm -rf /var/lib/apt/lists/* # RUN apt-get install pipx RUN pip3 install --break-system-packages pyplastimatch diff --git a/docker_src/command_docker.sh b/docker_src/command_docker.sh index 397a2d8..eb5fa5b 100644 --- a/docker_src/command_docker.sh +++ b/docker_src/command_docker.sh @@ -1,7 +1,7 @@ #!/bin/bash # to build the image and run the container -# docker compose up --build -d +docker compose up --build -d # to run the container without building the image -docker compose up --no-build -d +# docker compose up --no-build -d # to enter the container docker exec -it PyPlastimatch bash