diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fde08ee95..26ad79d8e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,55 +1,88 @@ name: MAN deploy-docker on: -# release: -# types: -# - published + release: + types: + - published workflow_dispatch: jobs: - deploy-gpu: - runs-on: ubuntu-latest + deploy-mac: + runs-on: macos-14 timeout-minutes: 120 + + env: + RELEASE_ASSETS: true + strategy: + matrix: + arch: [intel, arm] steps: + - name: Get repository name. + run: echo "FASTSURFER_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Checkout repository uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Login to Docker - uses: docker/login-action@v2 + - name: Set up python environment + uses: actions/setup-python@v6 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Build Docker image GPU - run: python Docker/build.py --device cuda --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} - - name: Add additional tags + python-version: '3.10' + - name: install dependencies + run: python -m pip install py2app pyyaml + - name: package app for ${{ matrix.arch }} + run: tools/macos_build/build_release_package.sh ${{ matrix.arch }} ${{ env.FASTSURFER_DIR }} + - name: Move assets. + if: env.RELEASE_ASSETS == 'true' run: | - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-latest - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:latest - - name: Push Docker image GPU - run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} - deploy-cpu: - runs-on: ubuntu-latest - timeout-minutes: 120 - steps: - - name: Checkout repository - uses: actions/checkout@v3 + mkdir assets + mv tools/macos_build/installer/* assets/ + - name: Upload release assets. + uses: softprops/action-gh-release@v2 + if: env.RELEASE_ASSETS == 'true' with: - fetch-depth: 0 - - name: Login to Docker - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Build Docker image CPU - run: python Docker/build.py --device cpu --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} - - name: Add additional tags - run: | - docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-latest - - name: Push Docker image CPU - run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} - + files: ${{ env.FASTSURFER_DIR }}/assets/* + # deploy-gpu: + # runs-on: ubuntu-latest + # timeout-minutes: 120 + # steps: + # - name: Checkout repository + # uses: actions/checkout@v3 + # with: + # fetch-depth: 0 + # - name: Login to Docker + # uses: docker/login-action@v2 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v2 + # - name: Build Docker image GPU + # run: python Docker/build.py --device cuda --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} + # - name: Add additional tags + # run: | + # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-latest + # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:latest + # - name: Push Docker image GPU + # run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:gpu-${{ github.event.release.tag_name }} + # deploy-cpu: + # runs-on: ubuntu-latest + # timeout-minutes: 120 + # steps: + # - name: Checkout repository + # uses: actions/checkout@v3 + # with: + # fetch-depth: 0 + # - name: Login to Docker + # uses: docker/login-action@v2 + # with: + # username: ${{ secrets.DOCKERHUB_USERNAME }} + # password: ${{ secrets.DOCKERHUB_TOKEN }} + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v2 + # - name: Build Docker image CPU + # run: python Docker/build.py --device cpu --tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} + # - name: Add additional tags + # run: | + # docker tag ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-latest + # - name: Push Docker image CPU + # run: docker push --all-tags ${{ secrets.DOCKERHUB_USERNAME }}/fastsurfer:cpu-${{ github.event.release.tag_name }} diff --git a/README.md b/README.md index 379b47134..a178a31dd 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,11 @@ All installation methods use the `run_fastsurfer.sh` call interface (replace `*f ``` The `--nv` flag is needed to allow FastSurfer to run on the GPU (otherwise FastSurfer will run on the CPU). - The `--no-home` flag tells singularity to not mount the home directory (see [Singularity documentation](Singularity/README.md#mounting-home) for more info). + The `--no-home` flag tells singularity to not mount the home directory (see [Singularity documentation](tools/Singularity/README.md#mounting-home) for more info). The `-B` flag is used to tell singularity, which folders FastSurfer can read and write to. - See also __[Example 2](doc/overview/EXAMPLES.md#example-2-fastsurfer-singularity)__ for a full singularity FastSurfer run command and [the Singularity documentation](Singularity/README.md#fastsurfer-singularity-image-usage) for details on more singularity flags. + See also __[Example 2](doc/overview/EXAMPLES.md#example-2-fastsurfer-singularity)__ for a full singularity FastSurfer run command and [the Singularity documentation](tools/Singularity/README.md#fastsurfer-singularity-image-usage) for details on more singularity flags. (b) For __docker__, the syntax is ``` @@ -96,7 +96,7 @@ All installation methods use the `run_fastsurfer.sh` call interface (replace `*f The `-v` flag is used to tell docker, which folders FastSurfer can read and write to. - See also __[Example 1](doc/overview/EXAMPLES.md#example-1-fastsurfer-docker)__ for a full FastSurfer run inside a Docker container and [the Docker documentation](Docker/README.md#docker-flags) for more details on the docker flags including `--rm` and `--user`. + See also __[Example 1](doc/overview/EXAMPLES.md#example-1-fastsurfer-docker)__ for a full FastSurfer run inside a Docker container and [the Docker documentation](tools/Docker/README.md#docker-flags) for more details on the docker flags including `--rm` and `--user`. 2. For a __native install__, you need to activate your FastSurfer environment (e.g. `conda activate fastsurfer_gpu`) and make sure you have added the FastSurfer path to your `PYTHONPATH` variable, e.g. `export PYTHONPATH=$(pwd)`. diff --git a/doc/images/fastsurfer.png b/doc/images/fastsurfer.png new file mode 100644 index 000000000..5d74c972b Binary files /dev/null and b/doc/images/fastsurfer.png differ diff --git a/doc/overview/EXAMPLES.md b/doc/overview/EXAMPLES.md index eb082c885..32e9a500d 100644 --- a/doc/overview/EXAMPLES.md +++ b/doc/overview/EXAMPLES.md @@ -39,10 +39,10 @@ Note, that the paths following `--fs_license`, `--t1`, and `--sd` are __inside__ A directory with the name as specified in `--sid` (here subjectX) will be created in the output directory if it does not exist. So in this example output will be written to /home/user/my_fastsurfer_analysis/subjectX/ . Make sure the output directory is empty, to avoid overwriting existing files. -If you do not have a GPU, you can also run our CPU-Docker by dropping the `--gpus all` flag and specifying `--device cpu` at the end as a FastSurfer flag, see also [FastSurfer's docker documentation](../../Docker/README.md) for more details. +If you do not have a GPU, you can also run our CPU-Docker by dropping the `--gpus all` flag and specifying `--device cpu` at the end as a FastSurfer flag, see also [FastSurfer's docker documentation](../../tools/Docker/README.md) for more details. ## Example 2: FastSurfer Singularity -After building the Singularity image (see below or [these instructions](../../Singularity/README.md)), you also need to register at the FreeSurfer website (https://surfer.nmr.mgh.harvard.edu/registration.html) to acquire a valid license (for free) - same as when using Docker. This license needs to be passed to the script via the `--fs_license` flag. This is not necessary if you want to run the segmentation only. +After building the Singularity image (see below or [these instructions](../../tools/Singularity/README.md)), you also need to register at the FreeSurfer website (https://surfer.nmr.mgh.harvard.edu/registration.html) to acquire a valid license (for free) - same as when using Docker. This license needs to be passed to the script via the `--fs_license` flag. This is not necessary if you want to run the segmentation only. To run FastSurfer on a given subject using the Singularity image with GPU access, execute the following commands from a directory where you want to store singularity images. This will create a singularity image from our Dockerhub image and execute it: diff --git a/doc/overview/INSTALL.md b/doc/overview/INSTALL.md index 333646b4a..d86a262b1 100644 --- a/doc/overview/INSTALL.md +++ b/doc/overview/INSTALL.md @@ -22,9 +22,9 @@ Assuming you have singularity installed already (by a system admin), you can bui ```bash singularity build fastsurfer-gpu.sif docker://deepmi/fastsurfer:latest ``` -Additionally, [the Singularity README](../../Singularity/README.md) contains detailed directions for building your own Singularity images from Docker. +Additionally, [the Singularity README](../../tools/Singularity/README.md) contains detailed directions for building your own Singularity images from Docker. -[Example 2](EXAMPLES.md#example-2-fastsurfer-singularity) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to build your own images here: [Docker](../../Docker/README.md) and [Singularity](../../Singularity/README.md). +[Example 2](EXAMPLES.md#example-2-fastsurfer-singularity) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to build your own images here: [Docker](../../tools/Docker/README.md) and [Singularity](../../tools/Singularity/README.md). ### Docker @@ -35,7 +35,7 @@ This is very similar to Singularity. Assuming you have Docker installed (by a sy docker pull deepmi/fastsurfer:latest ``` -[Example 1](EXAMPLES.md#example-1-fastsurfer-docker) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to [build your own image](https://github.com/Deep-MI/FastSurfer/blob/dev/Docker/README.md). +[Example 1](EXAMPLES.md#example-1-fastsurfer-docker) explains how to run FastSurfer (for the full pipeline you will also need a FreeSurfer .license file!) and you can find details on how to [build your own image](../../tools/Docker/README.md). If you are using the **rootless mode**, you have to install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) and follow the [configuration for the rootless mode](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#rootless-mode). Otherwise, running FastSurfer with Docker will give you this error message ```docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]]```. @@ -97,7 +97,7 @@ conda env create -f ./env/fastsurfer.yml conda activate fastsurfer ``` -If you do not have an NVIDIA GPU, you can create appropriate ymls on the fly with `python ./Docker/install_env.py -m $MODE -i ./env/FastSurfer.yml -o ./fastsurfer_$MODE.yml`. Here `$MODE` can be for example `cpu`, see also `python ./Docker/install_env.py --help` for other options like rocm or cuda versions. Finally, replace `./env/fastsurfer.yml` with your custom environment file `./fastsurfer_$MODE.yml`. +If you do not have an NVIDIA GPU, you can create appropriate ymls on the fly with `python ./tools/Docker/install_env.py -m $MODE -i ./env/FastSurfer.yml -o ./fastsurfer_$MODE.yml`. Here `$MODE` can be for example `cpu`, see also `python ./tools/Docker/install_env.py --help` for other options like rocm or cuda versions. Finally, replace `./env/fastsurfer.yml` with your custom environment file `./fastsurfer_$MODE.yml`. If you only want to run the surface pipeline, use `./env/fastsurfer_reconsurf.yml`. Next, add the fastsurfer directory to the python path (make sure you have changed into it already): @@ -129,7 +129,7 @@ We have successfully run the segmentation on an AMD GPU (Radeon Pro W6600) using Build the Docker container with ROCm support. ```bash -python Docker/build.py --device rocm --tag my_fastsurfer:rocm +python tools/Docker/build.py --device rocm --tag my_fastsurfer:rocm ``` You will need to add a couple of flags to your docker run command for AMD, see [Example 1](EXAMPLES.md#example-1-fastsurfer-docker) for `**other-docker-flags**` or `**fastsurfer-flags**`: diff --git a/doc/overview/MACOS.md b/doc/overview/MACOS.md new file mode 100644 index 000000000..60d585f21 --- /dev/null +++ b/doc/overview/MACOS.md @@ -0,0 +1,10 @@ +Running FastSurfer +================== + +If you want to run only segmentation (replace placeholders starting with "<" and ending with ">", see https://deep-mi.org/FastSurfer/stable): +`run_fastsurfer.sh --seg_only --sd --sid --t1 ` +To full run fastsurfer: +`run_fastsurfer.sh --device mps --sd --sid --t1 --fs_license ` +Some files of **FreeSurfer** binaries require bypassing MacOS security, which is +significantly easier to do with the following command than manually and one by one. +`

xattr -dr com.apple.quarantine /Applications/freesurfer/*` diff --git a/doc/overview/docker.rst b/doc/overview/docker.rst index 3cc4057e5..12328cd7a 100644 --- a/doc/overview/docker.rst +++ b/doc/overview/docker.rst @@ -1,7 +1,7 @@ Docker Support -------------- -.. include:: ../../Docker/README.md +.. include:: ../../tools/Docker/README.md :parser: fix_links.parser :relative-docs: . :relative-images: diff --git a/doc/overview/singularity.rst b/doc/overview/singularity.rst index afb11cb84..de76f9bf5 100644 --- a/doc/overview/singularity.rst +++ b/doc/overview/singularity.rst @@ -1,7 +1,7 @@ Singularity Support ------------------- -.. include:: ../../Singularity/README.md +.. include:: ../../tools/Singularity/README.md :parser: fix_links.parser :relative-docs: . :relative-images: diff --git a/recon_surf/README.md b/recon_surf/README.md index 281c8bb89..e452eb7dc 100644 --- a/recon_surf/README.md +++ b/recon_surf/README.md @@ -92,7 +92,7 @@ Check [Dockerhub](https://hub.docker.com/r/deepmi/fastsurfer/tags) to find out t * The `-B` commands mount your output, and directory with the FreeSurfer license file into the Singularity container. Inside the container these are visible under the name following the colon (in this case /data, /output, and /fs_license). -* The `--no-home` command disables the automatic mount of the users home directory (see [Best Practice](../Singularity/README.md#mounting-home)) +* The `--no-home` command disables the automatic mount of the users home directory (see [Best Practice](../tools/Singularity/README.md#mounting-home)) The `--t1` and `--asegdkt_segfile` flags point to the already existing conformed T1 input and segmentation from the segmentation module. Also other files from that pipeline will be reused (e.g. the `mask.mgz`, `orig_nu.mgz`). The diff --git a/Docker/Dockerfile b/tools/Docker/Dockerfile similarity index 99% rename from Docker/Dockerfile rename to tools/Docker/Dockerfile index 70930c596..48b608576 100644 --- a/Docker/Dockerfile +++ b/tools/Docker/Dockerfile @@ -116,7 +116,7 @@ RUN python /install/install_env.py -m ${DEVICE} -i /install/fastsurfer.yml -o /i FROM build_base AS build_freesurfer # get install scripts into docker -COPY ./Docker/install_fs_pruned.sh /install/ +COPY ./tools/install_fs_pruned.sh /install/ SHELL ["/bin/bash", "--login", "-c"] ARG FREESURFER_URL=default diff --git a/Docker/README.md b/tools/Docker/README.md similarity index 99% rename from Docker/README.md rename to tools/Docker/README.md index d26dcfcfc..f28cb6e83 100644 --- a/Docker/README.md +++ b/tools/Docker/README.md @@ -46,13 +46,13 @@ docker run --gpus all -v /home/user/my_mri_data:/data \ * The `--t1` points to the t1-weighted MRI image to analyse (full path, with mounted name inside docker: /home/user/my_mri_data => /data) * The `--sid` is the subject ID name (output folder name) * The `--sd` points to the output directory (its mounted name inside docker: /home/user/my_fastsurfer_analysis => /output) -* [more flags](../doc/overview/FLAGS.md#fastsurfer-flags) +* [more flags](../../doc/overview/FLAGS.md#fastsurfer-flags) Note, that the paths following `--fs_license`, `--t1`, and `--sd` are __inside__ the container, not global paths on your system, so they should point to the places where you mapped these paths above with the `-v` arguments. A directory with the name as specified in `--sid` (here subjectX) will be created in the output directory (specified via `--sd`). So in this example output will be written to /home/user/my_fastsurfer_analysis/subjectX/ . Make sure the output directory is empty, to avoid overwriting existing files. -All other available flags are identical to the ones explained on the main page [README](../README.md). +All other available flags are identical to the ones explained on the main page [README](../../README.md). ### Docker Best Practice * Do not mount the user home directory into the docker container as the home directory. diff --git a/Docker/build.py b/tools/Docker/build.py similarity index 100% rename from Docker/build.py rename to tools/Docker/build.py diff --git a/Docker/conda_pack.sh b/tools/Docker/conda_pack.sh similarity index 100% rename from Docker/conda_pack.sh rename to tools/Docker/conda_pack.sh diff --git a/Docker/entrypoint.sh b/tools/Docker/entrypoint.sh similarity index 100% rename from Docker/entrypoint.sh rename to tools/Docker/entrypoint.sh diff --git a/Docker/install_env.py b/tools/Docker/install_env.py similarity index 100% rename from Docker/install_env.py rename to tools/Docker/install_env.py diff --git a/Singularity/README.md b/tools/Singularity/README.md similarity index 100% rename from Singularity/README.md rename to tools/Singularity/README.md diff --git a/tools/config.yaml b/tools/config.yaml new file mode 100644 index 000000000..3bfd7ed02 --- /dev/null +++ b/tools/config.yaml @@ -0,0 +1,6 @@ +FASTSURFER: + VERSION: '242' + PYTHON_VERSION: '3.10' + +FREESURFER_LINK_LINUX: 'https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-linux-ubuntu22_amd64-7.4.1.tar.gz' +FREESURFER_LINK_MACOS: 'https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz' diff --git a/tools/get_config_value.py b/tools/get_config_value.py new file mode 100644 index 000000000..55786be4e --- /dev/null +++ b/tools/get_config_value.py @@ -0,0 +1,83 @@ +# Copyright 2024 Image Analysis Lab, German Center for Neurodegenerative Diseases (DZNE), Bonn +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import sys +from pathlib import Path + +import yaml + + +def make_parser() -> argparse.ArgumentParser: + """ + Create a command line interface and return command line options. + + Returns + ------- + options + Namespace object holding options. + """ + parser = argparse.ArgumentParser( + description="Tool for extracting values from configuration file", + ) + parser.add_argument( + "--file", "-f", + type=Path, + dest="file", + help="configuration file", + required=True, + ) + parser.add_argument( + "--key", "-k", + type=str, + dest="key", + help="key to lookup", + required=True, + ) + return parser + + +def extract_value(config_file: Path | str, key: str) -> str: + """ + Read configuration file and return the value for the given key. + + Parameters + ---------- + config_file : Path, str + Path to configuration file. + key : str + Key to lookup. + + Returns + ------- + value + Value under the given key. + """ + with open(config_file) as config: + try: + conf = yaml.safe_load(config) + value = conf[key.split('.')[0]] + for k in key.split('.')[1:]: + value = value[k] + return value + except yaml.YAMLError as e: + print(e) + sys.exit(1) + + +if __name__ == "__main__": + parser = make_parser() + args = parser.parse_args() + + print(extract_value(args.file,args.key)) + sys.exit(0) diff --git a/Docker/install_fs_pruned.sh b/tools/install_fs_pruned.sh similarity index 74% rename from Docker/install_fs_pruned.sh rename to tools/install_fs_pruned.sh index 0346be306..896254c61 100755 --- a/Docker/install_fs_pruned.sh +++ b/tools/install_fs_pruned.sh @@ -12,7 +12,7 @@ # Link where to find the FreeSurfer tarball: -fslink="https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-linux-ubuntu22_amd64-7.4.1.tar.gz" +fslink="https://surfer.nmr.mgh.harvard.edu/pub/dist/freesurfer/7.4.1/freesurfer-macOS-darwin_x86_64-7.4.1.tar.gz" if [[ "$#" -lt 1 ]]; then @@ -99,57 +99,61 @@ function run_parallel () # get FreeSurfer and unpack (some of it) echo "Downloading FS and unpacking portions ..." -wget --no-check-certificate -qO- $fslink | tar zxv --no-same-owner -C $where \ - --exclude='freesurfer/average/*.gca' \ - --exclude='freesurfer/average/Buckner_JNeurophysiol11_MNI152' \ - --exclude='freesurfer/average/Choi_JNeurophysiol12_MNI152' \ - --exclude='freesurfer/average/mult-comp-cor' \ - --exclude='freesurfer/average/samseg' \ - --exclude='freesurfer/average/Yeo_Brainmap_MNI152' \ - --exclude='freesurfer/average/Yeo_JNeurophysiol11_MNI152' \ - --exclude='freesurfer/bin/freeview.bin' \ - --exclude='freesurfer/bin/freeview' \ - --exclude='freesurfer/bin/fs_spmreg.glnxa64' \ - --exclude='freesurfer/bin/mris_decimate_gui.bin' \ - --exclude='freesurfer/bin/mris_decimate_gui' \ - --exclude='freesurfer/bin/qdec_glmfit' \ - --exclude='freesurfer/bin/qdec.bin' \ - --exclude='freesurfer/bin/qdec' \ - --exclude='freesurfer/bin/SegmentSubfieldsT1Longitudinal' \ - --exclude='freesurfer/bin/SegmentSubjectT1_autoEstimateAlveusML' \ - --exclude='freesurfer/bin/SegmentSubjectT1T2_autoEstimateAlveusML' \ - --exclude='freesurfer/bin/SegmentSubjectT2_autoEstimateAlveusML' \ - --exclude='freesurfer/diffusion' \ - --exclude='freesurfer/fsafd' \ - --exclude='freesurfer/fsfast' \ - --exclude='freesurfer/lib/cuda' \ - --exclude='freesurfer/lib/images' \ - --exclude='freesurfer/lib/qt' \ - --exclude='freesurfer/lib/tcl' \ - --exclude='freesurfer/lib/tktools' \ - --exclude='freesurfer/lib/vtk' \ - --exclude='freesurfer/matlab' \ - --exclude='freesurfer/mni-1.4' \ - --exclude='freesurfer/mni' \ - --exclude='freesurfer/models' \ - --exclude='freesurfer/python/bin' \ - --exclude='freesurfer/python/include' \ - --exclude='freesurfer/python/lib' \ - --exclude='freesurfer/python/share' \ - --exclude='freesurfer/subjects/bert' \ - --exclude='freesurfer/subjects/cvs_avg35_inMNI152' \ - --exclude='freesurfer/subjects/cvs_avg35' \ - --exclude='freesurfer/subjects/fsaverage_sym' \ - --exclude='freesurfer/subjects/fsaverage3' \ - --exclude='freesurfer/subjects/fsaverage4' \ - --exclude='freesurfer/subjects/fsaverage5' \ - --exclude='freesurfer/subjects/fsaverage6' \ - --exclude='freesurfer/subjects/lh.EC_average' \ - --exclude='freesurfer/subjects/rh.EC_average' \ - --exclude='freesurfer/subjects/V1_average' \ - --exclude='freesurfer/tktools' \ - --exclude='freesurfer/trctrain' +aria2c -x 16 -s 16 -c --check-certificate=false -o freesurfer.tar.gz "$fslink" +tar zxv --no-same-owner -C "$where" \ + --exclude='freesurfer/average/*.gca' \ + --exclude='freesurfer/average/Buckner_JNeurophysiol11_MNI152' \ + --exclude='freesurfer/average/Choi_JNeurophysiol12_MNI152' \ + --exclude='freesurfer/average/mult-comp-cor' \ + --exclude='freesurfer/average/samseg' \ + --exclude='freesurfer/average/Yeo_Brainmap_MNI152' \ + --exclude='freesurfer/average/Yeo_JNeurophysiol11_MNI152' \ + --exclude='freesurfer/bin/freeview.bin' \ + --exclude='freesurfer/bin/freeview' \ + --exclude='freesurfer/bin/fs_spmreg.glnxa64' \ + --exclude='freesurfer/bin/mris_decimate_gui.bin' \ + --exclude='freesurfer/bin/mris_decimate_gui' \ + --exclude='freesurfer/bin/qdec_glmfit' \ + --exclude='freesurfer/bin/qdec.bin' \ + --exclude='freesurfer/bin/qdec' \ + --exclude='freesurfer/bin/SegmentSubfieldsT1Longitudinal' \ + --exclude='freesurfer/bin/SegmentSubjectT1_autoEstimateAlveusML' \ + --exclude='freesurfer/bin/SegmentSubjectT1T2_autoEstimateAlveusML' \ + --exclude='freesurfer/bin/SegmentSubjectT2_autoEstimateAlveusML' \ + --exclude='freesurfer/diffusion' \ + --exclude='freesurfer/fsafd' \ + --exclude='freesurfer/fsfast' \ + --exclude='freesurfer/lib/cuda' \ + --exclude='freesurfer/lib/images' \ + --exclude='freesurfer/lib/qt' \ + --exclude='freesurfer/lib/tcl' \ + --exclude='freesurfer/lib/tktools' \ + --exclude='freesurfer/lib/vtk' \ + --exclude='freesurfer/matlab' \ + --exclude='freesurfer/mni-1.4' \ + --exclude='freesurfer/mni' \ + --exclude='freesurfer/models' \ + --exclude='freesurfer/python/bin' \ + --exclude='freesurfer/python/include' \ + --exclude='freesurfer/python/lib' \ + --exclude='freesurfer/python/share' \ + --exclude='freesurfer/subjects/bert' \ + --exclude='freesurfer/subjects/cvs_avg35_inMNI152' \ + --exclude='freesurfer/subjects/cvs_avg35' \ + --exclude='freesurfer/subjects/fsaverage_sym' \ + --exclude='freesurfer/subjects/fsaverage3' \ + --exclude='freesurfer/subjects/fsaverage4' \ + --exclude='freesurfer/subjects/fsaverage5' \ + --exclude='freesurfer/subjects/fsaverage6' \ + --exclude='freesurfer/subjects/lh.EC_average' \ + --exclude='freesurfer/subjects/rh.EC_average' \ + --exclude='freesurfer/subjects/V1_average' \ + --exclude='freesurfer/tktools' \ + --exclude='freesurfer/trctrain' \ + -f freesurfer.tar.gz + +rm -rf freesurfer.tar.gz # rename download to tmp mv $where/freesurfer $fss @@ -415,66 +419,5 @@ do touch $fsd/$file done -# FS calls these for version info, but we don't need them -# so we link them to mri_info to save space. -link_files=" - bin/mri_and - bin/mri_aparc2aseg - bin/mri_ca_label - bin/mri_ca_normalize - bin/mri_ca_register - bin/mri_compute_overlap - bin/mri_compute_seg_overlap - bin/mri_em_register - bin/mri_fwhm - bin/mri_gcut - bin/mri_log_likelihood - bin/mri_motion_correct.fsl - bin/mri_normalize_tp2 - bin/mri_or - bin/mri_relabel_nonwm_hypos - bin/mri_remove_neck - bin/mri_stats2seg - bin/mri_surf2vol - bin/mri_surfcluster - bin/mri_voldiff - bin/mri_watershed - bin/mris_divide_parcellation - bin/mris_left_right_register - bin/mris_surface_stats - bin/mris_thickness - bin/mris_thickness_diff - bin/nu_correct - bin/tkregister2_cmdl" - -# create target for link with ERROR message if called -ltrg=$fsd/bin/not-here.sh -echo '#!/bin/bash -if [ "$1" == "-all-info" ]; then - echo "$0 not included ..." - exit 0 -fi -echo -echo "ERROR: The binary $0 is not included, your call is forwarded to not-here.sh" -echo -exit 1 -' > $ltrg -chmod a+x $ltrg -echo -for file in $link_files -do - echo "linking $file" - ln -s $ltrg $fsd/$file -done - -# use our python (not really needed in recon-all anyway) -p3=$(which python3) -if [ "$p3" == "" ]; then - echo "No python3 found, please install first!" - echo - exit 1 -fi -ln -s $p3 $fsd/bin/fspython - #cleanup rm -rf $fss diff --git a/tools/macos_build/.gitignore b/tools/macos_build/.gitignore new file mode 100644 index 000000000..97da6dcc5 --- /dev/null +++ b/tools/macos_build/.gitignore @@ -0,0 +1,11 @@ +raw_package/ +installer/ +*.pkg +*.gz + +.eggs +build +scripts/postinstall +scripts_intell/postinstall +dist/ +resources/ \ No newline at end of file diff --git a/tools/macos_build/FastSurfer.py.template b/tools/macos_build/FastSurfer.py.template new file mode 100644 index 000000000..0c8bd2225 --- /dev/null +++ b/tools/macos_build/FastSurfer.py.template @@ -0,0 +1,12 @@ +from subprocess import run + +proc = run(["/usr/bin/osascript", +"-e", +"tell app \"Terminal\"", +"-e", +"do script \"source /Applications//macos_setup_fastsurfer.sh\"", +"-e", +"activate", +"-e", +"end tell" +]) diff --git a/tools/macos_build/README.md b/tools/macos_build/README.md new file mode 100644 index 000000000..b785b5fa1 --- /dev/null +++ b/tools/macos_build/README.md @@ -0,0 +1,40 @@ +# FastSurfer MacOS packaging +## Create MacOS package + +In order to build the MacOS package of FastSurfer, simply run: + +```bash +./build_release_package.sh [] +``` + +Script creates release package for MacOS, where `` is the release version, `` is `arm` for arm64 arch based chips and `intel` for `x86_64` arch based chips. +`` is the directory with the fastsurfer to package. +Link to specific freesurfer distribution might be provided with `` argument. + +### Dependencies for the script + +Script is using py2app python dependency, which isn't installed through any requirements file of FastSurfer, so in order to run the script, make sure that py2app is installed. + +### Running the package + +After the script is executed, `installer` folder will be created along with the MacOS package of FastSurfer inside. +Run the package by opening it and follow instructions. + +After successful installation, FastSurfer applet and its source code will appear under the `/Applications` folder. + +### Run FastSurfer + +If you would want to run FastSurfer, you could either use terminal or FastSurfer applet. Though, running applet is recommended as it opens shell terminal and sets up environment for FastSurfer. + +#### FastSurfer Flags +* The `--fs_license` points to your FreeSurfer license which needs to be available on your computer in the `my_fs_license_dir` that was mapped above. +* The `--t1` points to the t1-weighted MRI image to analyse (full path, with mounted name inside docker: /home/user/my_mri_data => /data) +* The `--sid` is the subject ID name (output folder name) +* The `--sd` points to the output directory (its mounted name inside docker: /home/user/my_fastsurfer_analysis => /output) +* [more flags](../../doc/overview/FLAGS.md#fastsurfer-flags) + +Note, that the paths following `--fs_license`, `--t1`, and `--sd` are __inside__ the container, not global paths on your system, so they should point to the places where you mapped these paths above with the `-v` arguments. + +A directory with the name as specified in `--sid` (here subjectX) will be created in the output directory (specified via `--sd`). So in this example output will be written to /home/user/my_fastsurfer_analysis/subjectX/ . Make sure the output directory is empty, to avoid overwriting existing files. + +All other available flags are identical to the ones explained on the main page [README](../../README.md). diff --git a/tools/macos_build/build_release_package.sh b/tools/macos_build/build_release_package.sh new file mode 100755 index 000000000..7bc7d30cf --- /dev/null +++ b/tools/macos_build/build_release_package.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +if [ "$#" -lt 2 ] ; then + echo + echo "Usage: build_release_package.sh " + echo + exit +fi + +# cd into directory with this script +dir=${0%/*} +if [ -d "$dir" ]; then + cd "$dir" +fi + +VERSION=$(python3 ../get_config_value.py --file ../config.yaml --key FASTSURFER.VERSION) # version of the project +URL_TO_FREESURFER=$(python3 ../get_config_value.py --file ../config.yaml --key FREESURFER_LINK_MACOS) # freesurfer install url +ARCH_TYPE=$1 # chip architecture - arm or intel +DIR_TO_FASTSURFER=$2 # directory to fastsurfer + +ARCH_TYPE_NAME="arm64" +if [ "$ARCH_TYPE" = "intel" ]; then + ARCH_TYPE_NAME="x86_64" +fi + +PACKAGE_NAME=FastSurfer$VERSION-macos-darwin_${ARCH_TYPE_NAME} # name of the package displayed in the installer +ID="ord.deep-mi.FastSurfer.${VERSION}_${ARCH_TYPE_NAME}" # package identifier (f.e. com.mycompany.productid) +INSTALLATION_DIR="/Applications" # install location for the content of the package +OUTPUT_PKG="raw_package/$PACKAGE_NAME.pkg" # raw package file to be created +INSTALLER_PKG="installer/$PACKAGE_NAME.pkg" # installer to be created + +# create temporary folder to package and copy FastSurfer over +STAGED_DIR="FastSurferPackageContent" +FASTSURFER_TO_PACKAGE="$STAGED_DIR/FastSurfer$VERSION" +mkdir $STAGED_DIR +rsync -av --progress $DIR_TO_FASTSURFER/ $FASTSURFER_TO_PACKAGE \ + --exclude requirements.txt \ + --exclude requirements.cpu.txt \ + --exclude tools + +# install freesurfer into temp folder +../install_fs_pruned.sh $STAGED_DIR --upx --url $URL_TO_FREESURFER + +SCRIPTS_DIR="./scripts" # directory with scripts executed during installation process (f.e. preinsatll postinstall) +PYTHON_VERSION=$(python3 ../get_config_value.py --file ../config.yaml --key FASTSURFER.PYTHON_VERSION) + +# substitute values in postinstall script +PATH_TO_FASTSURFER="$INSTALLATION_DIR/FastSurfer$VERSION" +cp $SCRIPTS_DIR/postinstall.template $SCRIPTS_DIR/postinstall + +sed -i '' -e "s||${PATH_TO_FASTSURFER}|g" $SCRIPTS_DIR/postinstall +sed -i '' -e "s||${PYTHON_VERSION}|g" $SCRIPTS_DIR/postinstall +if [ "$ARCH_TYPE" = "arm" ]; then + sed -i '' -e "s||/opt/homebrew|g" $SCRIPTS_DIR/postinstall +else + sed -i '' -e "s||/usr/local|g" $SCRIPTS_DIR/postinstall +fi + +# assemble resources +mkdir resources +cp $DIR_TO_FASTSURFER/doc/images/fastsurfer.png resources/ +cp $DIR_TO_FASTSURFER/doc/overview/MACOS.md resources/ +cp $DIR_TO_FASTSURFER/LICENSE resources/LICENSE.txt + +# create fastsurfer applet +cp FastSurfer.py.template FastSurfer.py +sed -i '' -e "s||FastSurfer${VERSION}|g" FastSurfer.py + +cp macos_setup_fastsurfer.sh.template macos_setup_fastsurfer.sh +sed -i '' -e "s||FastSurfer${VERSION}|g" macos_setup_fastsurfer.sh +sed -i '' -e "s||${PYTHON_VERSION}|g" macos_setup_fastsurfer.sh +if [ "$ARCH_TYPE" = "arm" ]; then + sed -i '' -e "s||1|g" macos_setup_fastsurfer.sh +else + sed -i '' -e "s||0|g" macos_setup_fastsurfer.sh +fi +mv macos_setup_fastsurfer.sh $FASTSURFER_TO_PACKAGE/ + +python3 setup.py py2app --iconfile resources/fastsurfer.png +mv dist/FastSurfer.app $STAGED_DIR/FastSurfer$VERSION.app + +rm -f FastSurfer.py +chmod -R 755 $STAGED_DIR/* + +# create raw package +mkdir raw_package +pkgbuild \ + --root $STAGED_DIR \ + --version $VERSION \ + --identifier $ID \ + --install-location $INSTALLATION_DIR \ + --scripts $SCRIPTS_DIR \ + $OUTPUT_PKG + +rm -f $SCRIPTS_DIR/postinstall + +# create distribution file template based on provided package +RESOURCES="./resources" +DISTRIBUTION_FILE="resources/distribution.xml" +productbuild --synthesize --package $OUTPUT_PKG $DISTRIBUTION_FILE + +# edit the distribution file +# set title to package name (f.e. package_name.pkg -> package_name) +python3 edit_distribution.py --file "$DISTRIBUTION_FILE" --title "$PACKAGE_NAME" + +# create installer package +mkdir installer +productbuild \ + --distribution $DISTRIBUTION_FILE \ + --resources $RESOURCES \ + --package-path raw_package \ + $INSTALLER_PKG + +# get rid of temporary folder +rm -rf $STAGED_DIR +rm -rf resources diff --git a/tools/macos_build/edit_distribution.py b/tools/macos_build/edit_distribution.py new file mode 100644 index 000000000..a634c4e8d --- /dev/null +++ b/tools/macos_build/edit_distribution.py @@ -0,0 +1,95 @@ +# Copyright 2024 Image Analysis Lab, German Center for Neurodegenerative Diseases (DZNE), Bonn +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import sys +from pathlib import Path +from xml.etree import ElementTree + + +def make_parser() -> argparse.ArgumentParser: + """ + Create a command line interface and return command line options. + + Returns + ------- + options + Namespace object holding options. + """ + parser = argparse.ArgumentParser( + description="Script to edit provided distribution.xml file.", + ) + parser.add_argument( + "--file", "-f", + type=Path, + dest="file", + help="path to distribution file to edit", + required=True, + ) + parser.add_argument( + "--title", "-t", + type=str, + dest="title", + help="value for the title of the distribution file", + required=True + ) + return parser + + +def edit_distribution(distribution_file: Path | str, title: str) -> None: + """ + Take path to distribution file and edit it. + + Parameters + ---------- + distribution_file : Path, str + Path to distribution file. + title : str + Value for the title tag. + """ + dist_elementtree = ElementTree.parse(distribution_file) + + root_tag = dist_elementtree.getroot() + + title_tag = ElementTree.SubElement(root_tag, "title") + title_tag.text = title + + background_tag = ElementTree.SubElement(root_tag, "background") + background_tag.attrib["file"] = "fastsurfer.png" + background_tag.attrib["uti"] = "public.png" + background_tag.attrib["scaling"] = "proportional" + background_tag.attrib["alignment"] = "bottomleft" + + background_darkAqua_tag = ElementTree.SubElement(root_tag, "background-darkAqua") + background_darkAqua_tag.attrib = background_tag.attrib + + license_tag = ElementTree.SubElement(root_tag, "license") + license_tag.attrib["file"] = "LICENSE.txt" + license_tag.attrib["mime-type"] = "text/txt" + + conclusion_tag = ElementTree.SubElement(root_tag, "conclusion") + conclusion_tag.attrib["file"] = "MACOS.md" + conclusion_tag.attrib["mime-type"] = "text/txt" + + ElementTree.indent(dist_elementtree, ' ') + + dist_elementtree.write(distribution_file, encoding="utf-8", xml_declaration=True) + + +if __name__ == "__main__": + parser = make_parser() + args = parser.parse_args() + + print(f"Editing distribution file: {args.file} ...") + edit_distribution(args.file, args.title) + sys.exit(0) diff --git a/tools/macos_build/macos_setup_fastsurfer.sh.template b/tools/macos_build/macos_setup_fastsurfer.sh.template new file mode 100644 index 000000000..f0cff739a --- /dev/null +++ b/tools/macos_build/macos_setup_fastsurfer.sh.template @@ -0,0 +1,27 @@ +#!/bin/bash + +export FASTSURFER_HOME=/Applications/ +source $FASTSURFER_HOME/venv/bin/activate +export PYTHONPATH=$FASTSURFER_HOME/venv/lib/python/site-packages:$PYTHONPATH +export PYTORCH_ENABLE_MPS_FALLBACK= +export FREESURFER_HOME=/Applications/freesurfer +source $FREESURFER_HOME/SetUpFreeSurfer.sh > /dev/null +echo +echo "This is the FastSurfer console. Call \`run_fastsurfer.sh -t1 ...\` in here! " +echo "See also https://deep-mi.org/FastSurfer/stable/overview/MACOS.html" + +if ! grep -q "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" ~/.bash_profile; then + echo "export PATH=\"$(brew --prefix)/opt/grep/libexec/gnubin:\$PATH\"" >> ~/.bash_profile; + export PATH="$(brew --prefix)/opt/grep/libexec/gnubin:$PATH" + echo "GNU grep has been added to path" +fi + +if ! grep -q "export PATH=\"$FASTSURFER_HOME:\$PATH\"" ~/.bash_profile; then + echo "export PATH=\"$FASTSURFER_HOME:\$PATH\"" >> ~/.bash_profile; + export PATH="$FASTSURFER_HOME:$PATH" + echo "FastSurfer dir has been added to path" +fi + +if [[ -z "${FS_LICENSE}" ]]; then + echo "Set FS_LICENSE environmental variable" +fi diff --git a/tools/macos_build/scripts/postinstall.template b/tools/macos_build/scripts/postinstall.template new file mode 100755 index 000000000..2e4c9b6e6 --- /dev/null +++ b/tools/macos_build/scripts/postinstall.template @@ -0,0 +1,77 @@ +#!/bin/bash + +FASTSURFER_HOME="" +PYTHON="python" + +if [[ ! -f "$FASTSURFER_HOME/venv" ]] +then + echo "Creating FastSurfer run environment" + /bin/$PYTHON -m venv --copies $FASTSURFER_HOME/venv +fi + +source $FASTSURFER_HOME/venv/bin/activate +$PYTHON -m pip install --upgrade pip + +export PYTHONPATH=${FASTSURFER_HOME}:${PYTHONPATH} + +$PYTHON -m pip install -r $FASTSURFER_HOME/requirements.mac.txt + +$PYTHON $FASTSURFER_HOME/FastSurferCNN/download_checkpoints.py --all + +sed -i '' -e "s|(venv)|($(basename $FASTSURFER_HOME))|g" $FASTSURFER_HOME/venv/bin/activate + +FREESURFER_HOME="/Applications/freesurfer" + +ln -sf $FASTSURFER_HOME/venv/bin/$PYTHON $FREESURFER_HOME/bin/fspython + +# FS calls these for version info, but we don't need them +# so we link them to mri_info to save space. +link_files=" + bin/mri_and + bin/mri_aparc2aseg + bin/mri_ca_label + bin/mri_ca_normalize + bin/mri_ca_register + bin/mri_compute_overlap + bin/mri_compute_seg_overlap + bin/mri_em_register + bin/mri_fwhm + bin/mri_gcut + bin/mri_log_likelihood + bin/mri_motion_correct.fsl + bin/mri_normalize_tp2 + bin/mri_or + bin/mri_relabel_nonwm_hypos + bin/mri_remove_neck + bin/mri_stats2seg + bin/mri_surf2vol + bin/mri_surfcluster + bin/mri_voldiff + bin/mri_watershed + bin/mris_divide_parcellation + bin/mris_left_right_register + bin/mris_surface_stats + bin/mris_thickness + bin/mris_thickness_diff + bin/nu_correct + bin/tkregister2_cmdl" + +# create target for link with ERROR message if called +ltrg=$FREESURFER_HOME/bin/not-here.sh +echo '#!/bin/bash +if [ "$1" == "-all-info" ]; then + echo "$0 not included ..." + exit 0 +fi +echo +echo "ERROR: The binary $0 is not included, your call is forwarded to not-here.sh" +echo +exit 1 +' > $ltrg +chmod a+x $ltrg +echo +for file in $link_files +do + echo "linking $file" + ln -sf $ltrg $FREESURFER_HOME/$file +done diff --git a/tools/macos_build/setup.py b/tools/macos_build/setup.py new file mode 100644 index 000000000..dea6d97d3 --- /dev/null +++ b/tools/macos_build/setup.py @@ -0,0 +1,19 @@ +""" +This is a setup.py script generated by py2applet + +Usage: + python setup.py py2app +""" + +from setuptools import setup + +APP = ['FastSurfer.py'] +DATA_FILES = [] +OPTIONS = {} + +setup( + app=APP, + data_files=DATA_FILES, + options={'py2app': OPTIONS}, + setup_requires=['py2app'], +)