diff --git a/NiChart_DLMUSE/__init__.py b/NiChart_DLMUSE/__init__.py index 7f41d576..7612d577 100644 --- a/NiChart_DLMUSE/__init__.py +++ b/NiChart_DLMUSE/__init__.py @@ -1,4 +1 @@ -from .dlmuse_pipeline import run_pipeline, run_dlicv, run_dlmuse - -if __name__ == "__main__": - pass +from .dlmuse_pipeline import run_dlicv, run_dlmuse, run_pipeline diff --git a/NiChart_DLMUSE/__main__.py b/NiChart_DLMUSE/__main__.py index 026353a0..f879750e 100644 --- a/NiChart_DLMUSE/__main__.py +++ b/NiChart_DLMUSE/__main__.py @@ -87,7 +87,7 @@ def main() -> None: "--cores", type=str, help="Number of cores", - default=4, + default=1, required=False, ) @@ -131,6 +131,14 @@ def main() -> None: help="Pass additional args to be sent to DLICV (ex. '-nps 1 -npp 1'). It is recommended to surround these args in a set of double quotes. See the DLICV documentation for details.", ) + parser.add_argument( + "--refaced-data", + action="store_true", + required=False, + default=False, + help="If set, refine DLICV mask by keeping only the largest connected component (for refaced data).", + ) + # HELP argument help = "Show this message and exit" parser.add_argument("-h", "--help", action="store_true", help=help) @@ -142,6 +150,7 @@ def main() -> None: device = args.device dlicv_extra_args = args.dlicv_args dlmuse_extra_args = args.dlmuse_args + refaced_data = args.refaced_data print() print("Arguments:") @@ -185,6 +194,7 @@ def main() -> None: device, dlmuse_extra_args, dlicv_extra_args, + refaced_data, i, ), ) @@ -198,7 +208,14 @@ def main() -> None: remove_subfolders("raw_temp_T1") remove_subfolders(out_dir) else: # No core parallelization - run_pipeline(in_dir, out_dir, device, dlmuse_extra_args, dlicv_extra_args) + run_pipeline( + in_dir, + out_dir, + device, + dlmuse_extra_args, + dlicv_extra_args, + refaced_data, + ) else: # Non-BIDS if int(args.cores) > 1: @@ -216,6 +233,7 @@ def main() -> None: device, dlmuse_extra_args, dlicv_extra_args, + refaced_data, i, ), ) @@ -229,7 +247,14 @@ def main() -> None: remove_subfolders(in_dir) remove_subfolders(out_dir) else: # No core parallelization - run_pipeline(in_dir, out_dir, device, dlmuse_extra_args, dlicv_extra_args) + run_pipeline( + in_dir, + out_dir, + device, + dlmuse_extra_args, + dlicv_extra_args, + refaced_data, + ) if __name__ == "__main__": diff --git a/NiChart_DLMUSE/dlmuse_pipeline.py b/NiChart_DLMUSE/dlmuse_pipeline.py index 8268fd79..c6642d89 100644 --- a/NiChart_DLMUSE/dlmuse_pipeline.py +++ b/NiChart_DLMUSE/dlmuse_pipeline.py @@ -1,5 +1,6 @@ import logging import os +import sys import pkg_resources # type: ignore @@ -32,7 +33,9 @@ ) logger = logging.getLogger(__name__) -logging.basicConfig(filename="pipeline.log", encoding="utf-8", level=logging.DEBUG) +logging.basicConfig(encoding="utf-8", level=logging.DEBUG, + handlers=[logging.FileHandler("pipeline.log"), + logging.StreamHandler(sys.stdout)]) def run_pipeline( @@ -41,6 +44,7 @@ def run_pipeline( device: str, dlmuse_extra_args: str = '', dlicv_extra_args: str = '', + refaced_data: bool = False, sub_fldr: int = 1, progress_bar = None, ) -> None: @@ -108,6 +112,24 @@ def run_pipeline( progress_bar.set_description("Running DLICV") run_dlicv(in_dir, in_suff, out_dir, out_suff, device, dlicv_extra_args) + # If refaced data is specified, refine the masks used in the next step (s3_masked) + if refaced_data: + import SimpleITK as sitk + + for _, tmp_row in df_img.iterrows(): + img_prefix = tmp_row.img_prefix + fpath = os.path.join(out_dir, img_prefix + SUFF_DLICV) + if os.path.exists(fpath): + s2_dlicv_output = sitk.ReadImage(fpath) + # Keep only the largest connected component + mask_component = sitk.ConnectedComponent(s2_dlicv_output) + mask_sorted_component = sitk.RelabelComponent( + mask_component, sortByObjectSize=True + ) + final_mask = sitk.Equal(mask_sorted_component, 1) + # Write refined mask back in-place within s2_dlicv + sitk.WriteImage(final_mask, fpath) + logging.info(f"Applying DLICV for batch [{sub_fldr}] done") logging.info(f"Applying mask for batch [{sub_fldr}]...") diff --git a/NiChart_DLMUSE/utils.py b/NiChart_DLMUSE/utils.py index f4c4e310..60966f66 100644 --- a/NiChart_DLMUSE/utils.py +++ b/NiChart_DLMUSE/utils.py @@ -65,7 +65,9 @@ def remove_common_suffix(list_files: list) -> list: bnames = list_files if len(list_files) == 1: - if list_files[0].endswith('_T1'): # If there is a single image with suffix _T1, remove it + if list_files[0].endswith( + "_T1" + ): # If there is a single image with suffix _T1, remove it bnames = [x[0:-3] for x in bnames] return bnames diff --git a/README.rst b/README.rst index d7b23f9f..b458232f 100644 --- a/README.rst +++ b/README.rst @@ -53,11 +53,11 @@ Or from out latest stable PyPI wheel: :: (If needed for your system) Install PyTorch with compatible CUDA. You only need to run this step if you experience errors with CUDA while running NiChart_DLMUSE. Run "pip uninstall torch torchaudio torchvision". -Then follow the `PyTorch installation instructions `_ for your CUDA version. -Note that we highly recommend matching the torch version you install to the version used by NiChart_DLMUSE. For example, after installing NiChart_DLMUSE: :: +Then follow the `PyTorch installation instructions `_ for your CUDA version. +Specific versions are needed for full compatibility. On Linux, download Torch 2.3.1, on Windows install 2.5.1. For example, after installing NiChart_DLMUSE: :: $ pip uninstall torch torchvision torchaudio - pip install torch==2.2.1 --index-url https://download.pytorch.org/whl/cu121 + pip install torch==2.3.1 --index-url https://download.pytorch.org/whl/cu121 ****************** Run NiChart_DLMUSE @@ -77,7 +77,7 @@ Docker build ************ The package comes already pre-built as a `docker container `_, for convenience. Please see `Usage <#usage>`_ for more information on how to use it. Alternatively, you can build the docker image -locally, like so: :: +locally from the source repo, like so: :: $ docker build -t cbica/nichart_dlmuse . diff --git a/docs/installation.rst b/docs/installation.rst index 82b9555b..da6f4a1e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -30,7 +30,9 @@ You can build the package by running the following command: :: $ docker build -t cbica/nichart_dlmuse . -.. _`Singularity/Apptainer build` +*************************** +Singularity/Apptainer build +*************************** Singularity and Apptainer images can be built for NiChart_DLMUSE, allowing for frozen versions of the pipeline and easier installation for end-users. Note that the Singularity project recently underwent a rename to "Apptainer", with a commercial @@ -43,7 +45,9 @@ After installing the container engine, run: :: This will take some time, but will build a containerized version of your current repo. Be aware that this includes any local changes! The nichart_dlmuse.sif file can be distributed via direct download, or pushed to a container registry that accepts SIF images. -.. _`Manual installation` +******************* +Manual installation +******************* You can manually build the package from source by running: :: @@ -51,5 +55,6 @@ You can manually build the package from source by running: :: $ cd NiChart_DLMUSE && python3 -m pip install -e . -We **do not** recomment installing the package directly from source as the repository above is under heavy development and can cause -crashes and bugs. +.. note:: + We **do not** recommend installing the package directly from source as the repository above is under heavy development and can cause + crashes and bugs. diff --git a/requirements.txt b/requirements.txt index 79414b1a..2428040f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ DLICV DLMUSE nibabel>=5.2 scipy +SimpleITK # Developer tools pytest diff --git a/scripts/wrapper.py b/scripts/wrapper.py new file mode 100644 index 00000000..254175bc --- /dev/null +++ b/scripts/wrapper.py @@ -0,0 +1,59 @@ +import argparse +import os +import shutil +import tempfile +from pathlib import Path + +# This wrapper script just adapts NiChart_DLMUSE to take two separate output args. Everything else is passed transparently + + +def main(): + parser = argparse.ArgumentParser(description="Wrapper", allow_abbrev=False) + parser.add_argument("-i", "--in_dir", required=True, help="Input directory") + parser.add_argument( + "-o1", + "--out_segs", + required=True, + help="Output directory for segmentation files", + ) + parser.add_argument( + "-o2", "--out_csvs", required=True, help="Output directory for CSV files" + ) + + # Parse known args; leave the rest for original app + args, extra_args = parser.parse_known_args() + + input_dir = args.in_dir + seg_dir = Path(args.out_segs) + csv_dir = Path(args.out_csvs) + + seg_dir.mkdir(parents=True, exist_ok=True) + csv_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory() as tmp_output: + tmp_output_path = Path(tmp_output) + + # Build command to run original application + cmd = [ + "NiChart_DLMUSE", + "-i", + input_dir, + "-o", + str(tmp_output_path), + ] + extra_args + command = " ".join(cmd) + os.system(command) + + # Copy output files + for item in tmp_output_path.rglob("*"): + if item.is_file(): + if item.suffix.lower() == ".csv": + shutil.copy2(item, csv_dir / item.name) + else: + dest_path = seg_dir / item.relative_to(tmp_output_path) + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(item, dest_path) + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index b353a48a..9698ec15 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="NiChart_DLMUSE", - version="1.0.8", + version="1.0.9", description="Run NiChart_DLMUSE on your data (currently only structural pipeline is supported).", long_description=long_description, long_description_content_type="text/markdown",