diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b69d2dd..179f0312 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ # docker pull circleci/picard # docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):$(pwd) -v ~/.circleci/:/root/.circleci --workdir $(pwd) circleci/picard circleci build -version: 2 +version: 2.1 jobs: test-notebooks: working_directory: /pyradiomics @@ -32,10 +32,72 @@ jobs: command: | jupyter nbconvert --ExecutePreprocessor.kernel_name=python3 --ExecutePreprocessor.timeout=-1 --to notebook --output-dir /tmp --execute notebooks/helloRadiomics.ipynb notebooks/helloFeatureClass.ipynb notebooks/PyRadiomicsExample.ipynb - build-3.5: &build_template + build-mac-37: &build_mac_template + working_directory: ~/pyradiomics + macos: + xcode: 12.5.1 + environment: + PYTHON_VERSION: 3.7.10 + PYTHON_SHORT_VERSION: 3.7 + steps: + - run: + name: Setup MAC OS environment + # Workaround the following error occurring because python installation is cached but gettext dependency is not + # dyld: Library not loaded: /usr/local/opt/gettext/lib/libintl.8.dylib + # Referenced from: /Users/travis/.pyenv/versions/3.7.2/bin/python + # Reason: Incompatible library version: python requires version 11.0.0 or later, but libintl.8.dylib provides version 10.0.0 + # See https://github.com/scikit-build/cmake-python-distributions/issues/112 and + # https://github.com/scikit-build/cmake-python-distributions/pull/113 + command: | + echo "HOMEBREW_NO_AUTO_UPDATE=1" >> $BASH_ENV + brew install gettext + brew install pyenv + echo 'export PATH=$HOME/.pyenv/versions/$PYTHON_VERSION/bin:$HOME/bin:$PATH' >> $BASH_ENV + mkdir -p $HOME/bin + ln -s $(which pip3) $HOME/bin/pip + ln -s $(which python3) $HOME/bin/python + pyenv install --list + pyenv install $PYTHON_VERSION + - run: + name: Setup SciKit-CI + command: | + pip install scikit-ci scikit-ci-addons + ci_addons --install ../addons + - run: + name: Setup PyEnv + command: python ../addons/travis/install_pyenv.py + - checkout + - attach_workspace: + at: ~/pyradiomics + - run: + name: Install + command: ci install + - run: + name: Test + command: ci test + - run: + name: Build Distribution + command: ci after_test + - persist_to_workspace: + root: . + paths: [dist] + + build-mac-38: + <<: *build_mac_template + environment: + PYTHON_VERSION: 3.8.10 + PYTHON_SHORT_VERSION: 3.7 + + build-mac-39: + <<: *build_mac_template + environment: + PYTHON_VERSION: 3.9.5 + PYTHON_SHORT_VERSION: 3.9 + + build-37: &build_template working_directory: /pyradiomics docker: - - image: circleci/python:3.5-jessie + - image: cimg/python:3.7 user: root steps: - checkout @@ -57,34 +119,79 @@ jobs: command: ci after_test - persist_to_workspace: root: . - paths: dist + paths: [dist] - build-3.6: + build-38: <<: *build_template docker: - - image: circleci/python:3.6-jessie + - image: cimg/python:3.8 user: root - build-3.7: + build-39: <<: *build_template docker: - - image: circleci/python:3.7 - user: root + - image: cimg/python:3.9 + user: root + + test_deploy: + working_directory: /pyradiomics + docker: + - image: cimg/python:3.8 + user: root + steps: + - run: + name: Check Repo User + command: if [[ $CIRCLE_PROJECT_USERNAME != "AIM-Harvard" ]]; then circleci step halt; fi + - checkout + - run: + name: Setup SciKit-CI + command: | + pip install scikit-ci scikit-ci-addons + ci_addons --install ../addons + - run: + name: Install + command: ci install + - run: + name: Install patchelf auditwheel, twine + command: | + apt update + apt-get install patchelf # needed to run auditwheel + python -m pip install "auditwheel<3.2.0" + python -m pip install twine + # only attach the workspace at this point to prevent the removal of source distributions + - attach_workspace: + at: /pyradiomics + - run: + name: Create sdist + command: python setup.py sdist + - run: + name: Fix Distribution Wheels + command: | + ls ./dist/*-linux_$(uname -m).whl # This will prevent further deployment if no wheels are found + # Since there are no external shared libraries to bundle into the wheels + # this step will fixup the wheel switching from 'linux' to 'manylinux1' tag + for whl in $(ls ./dist/*-linux_$(uname -m).whl); do + python -m auditwheel repair $whl -w ./dist/ + rm $whl + done + - run: + name: Deploy source and linux wheels + command: python -m twine upload ./dist/*.whl ./dist/*.tar.gz -u $PYPI_TEST_USER -p $PYPI_TEST_PASSWORD -r testpypi deploy: working_directory: /pyradiomics docker: - - image: circleci/python:3.6-jessie + - image: cimg/python:3.6 user: root steps: - run: name: Check Repo User - command: if [[ $CIRCLE_PROJECT_USERNAME != "Radiomics" ]]; then circleci step halt; fi + command: if [[ $CIRCLE_PROJECT_USERNAME != "AIM-Harvard" ]]; then circleci step halt; fi - checkout - run: name: Setup SciKit-CI command: | - pip install scikit-ci==0.13.0 scikit-ci-addons==0.11.0 + pip install scikit-ci scikit-ci-addons ci_addons --install ../addons - run: name: Install @@ -92,8 +199,9 @@ jobs: - run: name: Install patchelf auditwheel, twine command: | + apt update apt-get install patchelf # needed to run auditwheel - python -m pip install auditwheel + python -m pip install "auditwheel<3.2.0" python -m pip install twine # only attach the workspace at this point to prevent the removal of source distributions - attach_workspace: @@ -108,22 +216,22 @@ jobs: # Since there are no external shared libraries to bundle into the wheels # this step will fixup the wheel switching from 'linux' to 'manylinux1' tag for whl in $(ls ./dist/*-linux_$(uname -m).whl); do - auditwheel repair $whl -w ./dist/ + python -m auditwheel repair $whl -w ./dist/ rm $whl done - run: name: Deploy source and linux wheels - command: twine upload ./dist/*.whl ./dist/*.tar.gz -u $PYPI_USER -p $PYPI_PASSWORD + command: python -m twine upload ./dist/*.whl ./dist/*.tar.gz -u $PYPI_USER -p $PYPI_PASSWORD deploy_conda: working_directory: /pyradiomics docker: - - image: circleci/python:3.6-jessie + - image: cimg/python:3.8 user: root steps: - run: name: Check Repo User - command: if [[ $CIRCLE_PROJECT_USERNAME != "Radiomics" ]]; then circleci step halt; fi + command: if [[ $CIRCLE_PROJECT_USERNAME != "AIM-Harvard" ]]; then circleci step halt; fi - checkout - run: name: Install Miniconda @@ -141,9 +249,10 @@ jobs: name: Build Conda packages command: | mkdir /conda-bld - conda build ./conda --python=3.5 --croot /conda-bld conda build ./conda --python=3.6 --croot /conda-bld conda build ./conda --python=3.7 --croot /conda-bld + conda build ./conda --python=3.8 --croot /conda-bld + conda build ./conda --python=3.9 --croot /conda-bld - run: name: Deploy Conda packages command: | @@ -153,31 +262,53 @@ workflows: version: 2 build_and_deploy: jobs: - - build-3.5: &build_job_template + - build-mac-37: &build_job_template filters: tags: only: - - /^v?[0-9]+(\.[0-9]+)*(rc[0-9]+)?/ - - build-3.6: + - /^v?[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)?/ + - build-mac-38: <<: *build_job_template - - build-3.7: + - build-mac-39: <<: *build_job_template - - test-notebooks: - requires: - - build-3.5 - - build-3.6 - - build-3.7 - - deploy: &deploy_template + - build-37: + <<: *build_job_template + - build-38: + <<: *build_job_template + - build-39: + <<: *build_job_template + - test-notebooks: &requires_template requires: - - build-3.5 - - build-3.6 - - build-3.7 + - build-37 + - build-38 + - build-39 + - build-mac-37 + - build-mac-38 + - build-mac-39 + - test_deploy: + <<: *requires_template filters: branches: ignore: - /.*/ tags: only: - - /^v?[0-9]+(\.[0-9]+)*(rc[0-9]+)?/ + - /^v?[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)/ + - deploy: + <<: *requires_template + filters: + branches: + ignore: + - /.*/ + tags: + only: + - /^v?[0-9]+(\.[0-9]+)*/ - deploy_conda: - <<: *deploy_template + <<: *requires_template + filters: + branches: + ignore: + - /.*/ + tags: + only: + - /^v?[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)?/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3a0c0929..00000000 --- a/.travis.yml +++ /dev/null @@ -1,88 +0,0 @@ -# Config file for automatic testing at travis-ci.org - -language: python - -matrix: - include: - - - os: osx - language: generic - env: - - PYTHON_VERSION=3.5.5 - - PYTHON_SHORT_VERSION=3.5 - - - os: osx - language: generic - env: - - PYTHON_VERSION=3.6.5 - - PYTHON_SHORT_VERSION=3.6 - - - os: osx - language: generic - env: - - PYTHON_VERSION=3.7.8 - - PYTHON_SHORT_VERSION=3.7 - -before_cache: - # Cleanup to avoid the cache to grow indefinitely as new package versions are released - # see https://stackoverflow.com/questions/39930171/cache-brew-builds-with-travis-ci - - brew cleanup - -cache: - directories: - # Cache downloaded bottles - - $HOME/Library/Caches/Homebrew - # pyenv - - $HOME/.pyenv_cache - - $HOME/.pyenv/versions/3.7.8 - - $HOME/.pyenv/versions/3.6.5 - - $HOME/.pyenv/versions/3.5.5 - # scikit-ci-addons - - $HOME/downloads - -before_install: - # Workaround the following error occuring because python installation is cached but gettext dependency is not - # dyld: Library not loaded: /usr/local/opt/gettext/lib/libintl.8.dylib - # Referenced from: /Users/travis/.pyenv/versions/3.7.2/bin/python - # Reason: Incompatible library version: python requires version 11.0.0 or later, but libintl.8.dylib provides version 10.0.0 - # See https://github.com/scikit-build/cmake-python-distributions/issues/112 and - # https://github.com/scikit-build/cmake-python-distributions/pull/113 - - brew update - - brew install gettext - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mkdir $HOME/bin; ln -s $(which pip2) $HOME/bin/pip; fi - - pip install scikit-ci scikit-ci-addons - - ci_addons --install ../addons - -install: - - ci install - -script: - - ci test - -after_success: - - ci after_test - -before_deploy: - - sudo pip install twine # Twine installation requires sudo to get access to /usr/local/man - -deploy: - - provider: script - skip_cleanup: true - script: twine upload dist/*.whl -u $PYPI_USER -p $PYPI_PASSWORD - on: - tags: true - condition: $TRAVIS_TAG =~ ^v?[0-9]+(\.[0-9]+)*(rc[0-9]+)?$ && $TRAVIS_REPO_SLUG == Radiomics/pyradiomics - - provider: script - script: - wget https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p $HOME/miniconda; - hash -r; - export PATH=$HOME/miniconda/bin:$PATH; - conda config --set always_yes yes; - conda install gcc libgcc; - bash ./conda/configure_conda.sh; - conda build ./conda --python=$PYTHON_SHORT_VERSION --croot $HOME/conda-bld; - anaconda -t $ANACONDA_TOKEN upload -u Radiomics $HOME/conda-bld/osx-64/pyradiomics-*.tar.bz2 --force - on: - tags: true - condition: $TRAVIS_TAG =~ ^v?[0-9]+(\.[0-9]+)*(rc[0-9]+)?$ && $TRAVIS_REPO_SLUG == Radiomics/pyradiomics diff --git a/MANIFEST.in b/MANIFEST.in index fb19cd77..f50e20be 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,7 +6,7 @@ include requirements-dev.txt include requirements-setup.txt include versioneer.py -recursive-include radiomics * +recursive-include src/radiomics * recursive-include data/baseline * recursive-include data *_image.nrrd @@ -24,4 +24,3 @@ recursive-include bin *.py recursive-exclude * __pycache__ recursive-exclude * *.py[cod] -recursive-exclude * nosetests.xml diff --git a/README.md b/README.md index fdf28f9c..23b56f1d 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,16 @@ ## Build Status -| Linux | macOS | Windows | -|--------------------------------|-------------------------------|-------------------------------| -| [![][circleci]][circleci-lnk] | [![][travisci]][travisci-lnk] | [![][appveyor]][appveyor-lnk] | +| Linux / MacOS | Windows | +| ----------------------------- | ----------------------------- | +| [![][circleci]][circleci-lnk] | [![][appveyor]][appveyor-lnk] | -[appveyor]: https://ci.appveyor.com/api/projects/status/tw69xbbeyluk7fl7/branch/master?svg=true -[appveyor-lnk]: https://ci.appveyor.com/project/Radiomics/pyradiomics/branch/master +[appveyor]: https://ci.appveyor.com/api/projects/status/kvu7897q0v4imwdc?svg=true +[appveyor-lnk]: https://ci.appveyor.com/project/AIM-Harvard/pyradiomics-k4sto -[circleci]: https://circleci.com/gh/Radiomics/pyradiomics.svg?style=svg&circle-token=a4748cf0de5fad2c12bc93a485282378551c3584 -[circleci-lnk]: https://circleci.com/gh/Radiomics/pyradiomics - -[travisci]: https://travis-ci.org/Radiomics/pyradiomics.svg?branch=master -[travisci-lnk]: https://travis-ci.org/Radiomics/pyradiomics +[circleci]: https://dl.circleci.com/status-badge/img/gh/AIM-Harvard/pyradiomics/tree/master.svg?style=shield +[circleci-lnk]: https://circleci.com/gh/AIM-Harvard/pyradiomics ## Radiomics feature extraction in Python This is an open-source python package for the extraction of Radiomics features from medical imaging. @@ -120,7 +117,7 @@ For more information on using docker, see PyRadiomics can be easily used in a Python script through the `featureextractor` module. Furthermore, PyRadiomics provides a commandline script, `pyradiomics`, for both single image extraction and batchprocessing. Finally, a convenient front-end interface is provided as the 'Radiomics' -extension for 3D Slicer, available [here](https://github.com/Radiomics/SlicerRadiomics). +extension for 3D Slicer, available [here](https://github.com/AIM-Harvard/SlicerRadiomics). ### 3rd-party packages used in pyradiomics: - SimpleITK (Image loading and preprocessing) @@ -135,7 +132,7 @@ extension for 3D Slicer, available [here](https://github.com/Radiomics/SlicerRad See also the [requirements file](requirements.txt). ### 3D Slicer -PyRadiomics is also available as an [extension](https://github.com/Radiomics/SlicerRadiomics) to [3D Slicer](slicer.org). +PyRadiomics is also available as an [extension](https://github.com/AIM-Harvard/SlicerRadiomics) to [3D Slicer](slicer.org). Download and install the 3D slicer [nightly build](http://download.slicer.org/), the extension is then available in the extension manager under "SlicerRadiomics". diff --git a/appveyor.yml b/appveyor.yml index 39c7c1ab..cfcda140 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,47 +1,47 @@ -version: "0.0.1.{build}" +version: "3.0.{build}" +image: Visual Studio 2019 environment: + PYTHON_ARCH: "64" + BLOCK: "0" + matrix: # Visual Studio (Python 3, 64 bit) - - PYTHON_DIR: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.x" - PYTHON_SHORT_VERSION: "3.5" - PYTHON_ARCH: "64" - BLOCK: "0" - - - PYTHON_DIR: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.x" - PYTHON_SHORT_VERSION: "3.6" - PYTHON_ARCH: "64" - BLOCK: "0" - - PYTHON_DIR: "C:\\Python37-x64" PYTHON_VERSION: "3.7.x" PYTHON_SHORT_VERSION: "3.7" - PYTHON_ARCH: "64" - BLOCK: "0" + + - PYTHON_DIR: "C:\\Python38-x64" + PYTHON_VERSION: "3.8.x" + PYTHON_SHORT_VERSION: "3.8" + + - PYTHON_DIR: "C:\\Python39-x64" + PYTHON_VERSION: "3.9.x" + PYTHON_SHORT_VERSION: "3.9" init: - - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - - python -m pip install scikit-ci==0.13.0 scikit-ci-addons==0.11.0 + - ps: $env:PATH=$env:PYTHON_DIR + ";" + $env:PYTHON_DIR + "\\Scripts;" + $env:PATH + # - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + - ps: python -c "import sys;print(sys.version, sys.executable)" + - python -m pip install scikit-ci scikit-ci-addons - python -m ci_addons --install ../addons - python -m pip install twine - ps: ../addons/appveyor/rolling-build.ps1 install: - - python -m ci install + - ci install build_script: - - python -m ci build + - ci build test_script: - - python -m ci test + - ci test after_test: - - python -m ci after_test + - ci after_test artifacts: - path: dist/* @@ -52,13 +52,19 @@ on_finish: deploy_script: - echo checking deployment - - ps: if ($env:APPVEYOR_REPO_NAME -notmatch 'Radiomics/pyradiomics') { appveyor exit } - - ps: if ($env:APPVEYOR_REPO_TAG_NAME -notmatch '^v?[0-9]+(\.[0-9]+)*(rc\d+)?$') { appveyor exit } + - ps: if ($env:APPVEYOR_REPO_NAME -notmatch 'AIM-Harvard/pyradiomics') { appveyor exit } + - ps: if ($env:APPVEYOR_REPO_TAG_NAME -notmatch '^v?[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)?$') { appveyor exit } - echo starting PyPi deployment - - twine upload dist/*.whl -u %PYPI_USER% -p %PYPI_PASSWORD% + - ps: if ($env:APPVEYOR_REPO_TAG_NAME -match '^v?[0-9]+(\.[0-9]+)*$') { twine upload dist/*.whl -u $Env:PYPI_USER -p $Env:PYPI_PASSWORD } + - ps: if ($env:APPVEYOR_REPO_TAG_NAME -match '^v?[0-9]+(\.[0-9]+)*((a|b|rc)[0-9]+)$') { twine upload dist/*.whl -u $Env:PYPI_TEST_USER -p $Env:PYPI_TEST_PASSWORD -r testpypi } - echo starting Anaconda deployment - - SET PATH=C:\Miniconda-x64\Scripts;%PATH% - - ./conda/configure_conda.bat && conda build ./conda --python=%PYTHON_SHORT_VERSION% --croot C:/conda-bld + - CALL C:\Miniconda3-x64\condabin\conda.bat activate + - conda config --set always_yes yes --set changeps1 no --set anaconda_upload no + - conda config --add channels simpleitk --add channels conda-forge + - conda install conda-build + - conda install anaconda-client + - conda update -q conda + - conda build ./conda --python=%PYTHON_SHORT_VERSION% --croot C:/conda-bld - anaconda -t %ANACONDA_TOKEN% upload -u Radiomics C:/conda-bld/win-64/pyradiomics-*.tar.bz2 --force - echo finished deployment diff --git a/conda/meta.yaml b/conda/meta.yaml index 1534c833..6872cc82 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -12,6 +12,7 @@ build: requirements: build: + - {{ compiler('c') }} - python - git host: diff --git a/labs/pyradiomics-dcm/pyradiomics-dcm.py b/labs/pyradiomics-dcm/pyradiomics-dcm.py index 8b459e97..e27eaca6 100644 --- a/labs/pyradiomics-dcm/pyradiomics-dcm.py +++ b/labs/pyradiomics-dcm/pyradiomics-dcm.py @@ -319,17 +319,18 @@ def saveJSONToFile(self, fileName): def main(): parser = argparse.ArgumentParser( - usage="%(prog)s --input-image --input-seg --output-sr \n\n" - + "Warning: This is a \"pyradiomics labs\" script, which means it is an experimental feature in development!\n" - + "The intent of this helper script is to enable pyradiomics feature extraction directly from/to DICOM data.\n" - + "The segmentation defining the region of interest must be defined as a DICOM Segmentation image.\n" - + "Support for DICOM Radiotherapy Structure Sets for defining region of interest may be added in the future.\n") + usage="""%(prog)s --input-image --input-seg --output-sr \n\n + +Warning: This is a \"pyradiomics labs\" script, which means it is an experimental feature in development! +The intent of this helper script is to enable pyradiomics feature extraction directly from/to DICOM data. +The segmentation defining the region of interest must be defined as a DICOM Segmentation image. +Support for DICOM Radiotherapy Structure Sets for defining region of interest may be added in the future.""") parser.add_argument( '--input-image-dir', dest="inputDICOMImageDir", metavar="", help="Path to the directory with the input DICOM series." - + " It is expected that a single series is corresponding to a single scalar volume.", + " It is expected that a single series is corresponding to a single scalar volume.", required=True) parser.add_argument( '--input-seg-file', @@ -363,8 +364,8 @@ def main(): dest="volumeReconstructor", metavar="", help="Choose the tool to be used for reconstructing image volume from the DICOM image series." - + " Allowed options are plastimatch or dcm2niix (should be installed on the system). plastimatch" - + " will be used by default.", + " Allowed options are plastimatch or dcm2niix (should be installed on the system). plastimatch" + " will be used by default.", choices=['plastimatch', 'dcm2niix'], default="plastimatch") parser.add_argument( @@ -377,7 +378,7 @@ def main(): '--correct-mask', dest="correctMask", help="Boolean flag argument. If present, PyRadiomics will attempt to resample the mask to the image" - + " geometry if the mask check fails.", + " geometry if the mask check fails.", action='store_true', default=False) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..40034f9b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +[build-system] +requires = ["setuptools>=61.0", "numpy", "versioneer"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyradiomics" +version = "3.0.1a1" +authors = [ + { name = "PyRadimiomics Community", email = "pyradiomics@googlegroups.com"} +] +description = "Radiomics features library for python" +readme = "README.md" +requires-python =">=3.5" +license = { file = "LICENSE.txt"} +keywords = [ "radiomics", "cancerimaging", "medicalresearch", "computationalimaging" ] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: C', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Topic :: Scientific/Engineering :: Bio-Informatics' +] +dependencies = [ + "numpy", + "SimpleITK", + "PyWavelets", + "pykwalify", + "six" +] + +[project.scripts] +pyradiomics = "radiomics.scripts.__init__:parse_args" + +[project.urls] +"Homepaget" = "http://github.com/AIM-Harvard/pyradiomics#readme" +"Radiomics.io" = "https://www.radiomics.io/" +"Documentation" = "https://pyradiomics.readthedocs.io/en/latest/index.html" +"Docker" = "https://hub.docker.com/r/radiomics/pyradiomics/" +"Github" = "https://github.com/AIM-Harvard/pyradiomics" diff --git a/radiomics/generalinfo.py b/radiomics/generalinfo.py index 0034d8cb..58c1c6e0 100644 --- a/radiomics/generalinfo.py +++ b/radiomics/generalinfo.py @@ -113,8 +113,8 @@ def addMaskElements(self, image, mask, label, prefix='original'): lssif = sitk.LabelShapeStatisticsImageFilter() lssif.Execute(mask) - self.generalInfo[self.generalInfo_prefix + 'Mask-' + prefix + '_BoundingBox'] = lssif.GetBoundingBox(label) - self.generalInfo[self.generalInfo_prefix + 'Mask-' + prefix + '_VoxelNum'] = lssif.GetNumberOfPixels(label) + self.generalInfo[self.generalInfo_prefix + 'Mask-' + prefix + '_BoundingBox'] = lssif.GetBoundingBox(int(label)) + self.generalInfo[self.generalInfo_prefix + 'Mask-' + prefix + '_VoxelNum'] = lssif.GetNumberOfPixels(int(label)) labelMap = (mask == label) ccif = sitk.ConnectedComponentImageFilter() diff --git a/radiomics/imageoperations.py b/radiomics/imageoperations.py index 82a8e28a..621996d2 100644 --- a/radiomics/imageoperations.py +++ b/radiomics/imageoperations.py @@ -19,6 +19,13 @@ def getMask(mask, **kwargs): In this case, the mask at index ``label_channel`` is extracted. The resulting 3D volume is then treated as it were a scalar input volume (i.e. with the region of interest defined by voxels with value matching ``label``). + .. note:: + If only one or non-overlapping Segments are defined when using 3D Slicer, it may be the case that it is stored as a + labelmap (i.e. only 1 ``label_channel``, with different segmentations identified by different values for ``label``). + This is easy to check by loading the mask as a SimpleITK image and checking the `GetNumberOfComponentsPerPixel()`, + if the return value is ``1``, it is a label map (i.e. use ``label``), otherwise it is a VectorImage (i.e. use + ``label_channel``). + Finally, checks if the mask volume contains an ROI identified by ``label``. Raises a value error if the label is not present (including a list of valid labels found). @@ -38,7 +45,7 @@ def getMask(mask, **kwargs): logger.info('Extracting mask at index %i', label_channel) selector = sitk.VectorIndexSelectionCastImageFilter() - selector.SetIndex(label_channel) + selector.SetIndex(int(label_channel)) mask = selector.Execute(mask) logger.debug('Force casting mask to UInt32 to ensure correct datatype.') @@ -216,7 +223,7 @@ def checkMask(imageNode, maskNode, **kwargs): correctedMask = None - label = kwargs.get('label', 1) + label = int(kwargs.get('label', 1)) minDims = kwargs.get('minimumROIDimensions', 2) minSize = kwargs.get('minimumROISize', None) @@ -316,7 +323,7 @@ def _checkROI(imageNode, maskNode, **kwargs): returned. Otherwise, a ValueError is raised. """ global logger - label = kwargs.get('label', 1) + label = int(kwargs.get('label', 1)) logger.debug('Checking ROI validity') @@ -442,7 +449,7 @@ def resampleImage(imageNode, maskNode, **kwargs): resampledPixelSpacing = kwargs['resampledPixelSpacing'] interpolator = kwargs.get('interpolator', sitk.sitkBSpline) padDistance = kwargs.get('padDistance', 5) - label = kwargs.get('label', 1) + label = int(kwargs.get('label', 1)) logger.debug('Resampling image and mask') diff --git a/radiomics/scripts/__init__.py b/radiomics/scripts/__init__.py index a9d3c222..fa7d9730 100644 --- a/radiomics/scripts/__init__.py +++ b/radiomics/scripts/__init__.py @@ -15,6 +15,7 @@ import pykwalify.core import six.moves +import radiomics import radiomics.featureextractor from . import segment, voxel diff --git a/radiomics/scripts/segment.py b/radiomics/scripts/segment.py index 8de06a63..8afc56f4 100644 --- a/radiomics/scripts/segment.py +++ b/radiomics/scripts/segment.py @@ -8,7 +8,7 @@ import SimpleITK as sitk import six -import radiomics.featureextractor +import radiomics caseLogger = logging.getLogger('radiomics.script') _parallel_extraction_configured = False diff --git a/radiomics/scripts/voxel.py b/radiomics/scripts/voxel.py index e35f2d5f..60490eeb 100644 --- a/radiomics/scripts/voxel.py +++ b/radiomics/scripts/voxel.py @@ -7,7 +7,7 @@ import SimpleITK as sitk import six -import radiomics.featureextractor +import radiomics caseLogger = logging.getLogger('radiomics.script') _parallel_extraction_configured = False @@ -39,7 +39,7 @@ def extractVoxel(case_idx, case, extractor, **kwargs): label = int(label) label_channel = case.get('Label_channel', None) # Optional if isinstance(label_channel, six.string_types): - label_channel = int(label) + label_channel = int(label_channel) # Extract features result = extractor.execute(imageFilepath, maskFilepath, label, label_channel, voxelBased=True) diff --git a/requirements-dev.txt b/requirements-dev.txt index 687f293d..d3315d69 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,4 @@ -nose>=1.3.7 -parameterized +pytest flake8 flake8-import-order sphinx>=1.4 diff --git a/requirements.txt b/requirements.txt index 1d09b540..e533ab68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -numpy>=1.9.2 -SimpleITK>=0.9.1 -PyWavelets>=0.4.0 -pykwalify>=1.6.0 -six>=1.10.0 +numpy +SimpleITK +PyWavelets +pykwalify +six diff --git a/scikit-ci.yml b/scikit-ci.yml index fba7450a..8aabcda7 100644 --- a/scikit-ci.yml +++ b/scikit-ci.yml @@ -4,7 +4,6 @@ before_install: appveyor: environment: - PATH: $;$\\Scripts;$ RUN_ENV: .\\..\\addons\\appveyor\\run-with-visual-studio.cmd commands: - python ../addons/appveyor/patch_vs2008.py @@ -31,7 +30,7 @@ install: - $ pip install wheel>=0.29.0 - $ pip install setuptools>=38.6.0 - $ pip install numpy>=1.9.2 - - $ pip install --trusted-host www.itk.org -f https://itk.org/SimpleITKDoxygen/html/PyDownloadPage.html SimpleITK>=0.9.1 + - $ pip install SimpleITK>=0.9.1 - $ python -c "import SimpleITK; print('SimpleITK Version:' + SimpleITK.Version_VersionString())" - $ pip install -r requirements.txt - $ pip install -r requirements-dev.txt @@ -42,15 +41,11 @@ before_build: build: commands: - - $ python setup.py build_ext + - $ python setup.py develop test: commands: - - $ python setup.py test --args="--with-xunit --logging-level=DEBUG" - - circleci: - commands: - - cp nosetests.xml $CIRCLE_TEST_REPORTS + - $ pytest after_test: commands: diff --git a/setup.cfg b/setup.cfg index d973f4c7..d4b47dff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,26 @@ [metadata] -description-file = README.md +description = Radiomics features library for python +long_description = file: README.md +long_description_content_type = text/markdown +license = 3-Clause BSD -[nosetests] -verbosity=3 -where=tests +[options] +install_requires = + numpy + SimpleITK + PyWavelets + pykwalify + six +include_package_data=False + +[options.packages.find] +include=radiomics* +exclude=radiomics.schemas + +[options.package_data] +radiomics = + schemas/paramSchema.yaml + schemas/schemaFuncs.py [versioneer] VCS = git diff --git a/setup.py b/setup.py index d922a42d..300c1be6 100644 --- a/setup.py +++ b/setup.py @@ -2,59 +2,15 @@ from distutils import sysconfig import platform -import sys import numpy from setuptools import Extension, setup -from setuptools.command.test import test as TestCommand import versioneer -# Check if current PyRadiomics is compatible with current python installation (> 2.6, 64 bits) -if sys.version_info < (2, 6, 0): - raise Exception("pyradiomics > 0.9.7 requires python 2.6 or later") - if platform.architecture()[0].startswith('32'): raise Exception('PyRadiomics requires 64 bits python') -with open('requirements.txt', 'r') as fp: - requirements = list(filter(bool, (line.strip() for line in fp))) - -with open('requirements-dev.txt', 'r') as fp: - dev_requirements = list(filter(bool, (line.strip() for line in fp))) - -with open('requirements-setup.txt', 'r') as fp: - setup_requirements = list(filter(bool, (line.strip() for line in fp))) - -with open('README.md', 'rb') as fp: - long_description = fp.read().decode('utf-8') - - -class NoseTestCommand(TestCommand): - """Command to run unit tests using nose driver after in-place build""" - - user_options = TestCommand.user_options + [ - ("args=", None, "Arguments to pass to nose"), - ] - - def initialize_options(self): - self.args = [] - TestCommand.initialize_options(self) - - def finalize_options(self): - TestCommand.finalize_options(self) - if self.args: - self.args = __import__('shlex').split(self.args) - - def run_tests(self): - # Run nose ensuring that argv simulates running nosetests directly - nose_args = ['nosetests'] - nose_args.extend(self.args) - __import__('nose').run_exit(argv=nose_args) - - commands = versioneer.get_cmdclass() -commands['test'] = NoseTestCommand - incDirs = [sysconfig.get_python_inc(), numpy.get_include()] ext = [Extension("radiomics._cmatrices", ["radiomics/src/_cmatrices.c", "radiomics/src/cmatrices.c"], @@ -65,56 +21,10 @@ def run_tests(self): setup( name='pyradiomics', - url='http://github.com/Radiomics/pyradiomics#readme', - project_urls={ - 'Radiomics.io': 'https://www.radiomics.io/', - 'Documentation': 'https://pyradiomics.readthedocs.io/en/latest/index.html', - 'Docker': 'https://hub.docker.com/r/radiomics/pyradiomics/', - 'Github': 'https://github.com/Radiomics/pyradiomics' - }, - - author='pyradiomics community', - author_email='pyradiomics@googlegroups.com', - version=versioneer.get_version(), cmdclass=commands, packages=['radiomics', 'radiomics.scripts'], ext_modules=ext, - zip_safe=False, - package_data={'radiomics': ['schemas/paramSchema.yaml', 'schemas/schemaFuncs.py']}, - - entry_points={ - 'console_scripts': [ - 'pyradiomics=radiomics.scripts.__init__:parse_args' - ]}, - - description='Radiomics features library for python', - long_description=long_description, - long_description_content_type='text/markdown', - - license='BSD License', - - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: C', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Scientific/Engineering :: Bio-Informatics', - ], - - keywords='radiomics cancerimaging medicalresearch computationalimaging', - - install_requires=requirements, - test_suite='nose.collector', - tests_require=dev_requirements, - setup_requires=setup_requirements + zip_safe=False ) diff --git a/tests/testUtils.py b/tests/testUtils.py index 3b885782..7e9d491b 100644 --- a/tests/testUtils.py +++ b/tests/testUtils.py @@ -1,11 +1,9 @@ - import ast import csv import logging import math import os -from nose_parameterized import parameterized import numpy import SimpleITK as sitk import six @@ -16,38 +14,6 @@ logger = logging.getLogger('radiomics.testing') -def custom_name_func(testcase_func, param_num, param): - """ - A custom test name function that will ensure that the tests are run such that they're batched with all tests for a - given data set are run together, avoiding re-reading the data more than necessary. Tests are run in alphabetical - order, so put the test case first. An alternate option is to right justify the test number (param_num) with zeroes - so that the numerical and alphabetical orders are the same. Not providing this method when there are more than 10 - tests results in tests running in an order similar to: - - test_*.test_scenario_0_* - - test_*.test_scenario_10_* - - test_*.test_scenario_11_* - - ... - - test_*.test_scenario_19_* - - test_*.test_scenario_1_* - - test_*.test_scenario_20_* - """ - global logger - - logger.debug('custom_name_func: function name = %s, param_num = {0:0>3}, param.args = %s'.format(param_num), - testcase_func.__name__, param.args) - return str("%s_%s" % ( - testcase_func.__name__, - parameterized.to_safe_name("_".join(str(x) for x in param.args)), - )) - - class RadiomicsTestUtils: """ This utility class reads in and stores the baseline files stored in 'data/baseline' (one per feature class) diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 019f16c3..06be6364 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -1,39 +1,19 @@ -# to run this test, from directory above: -# setenv PYTHONPATH /path/to/pyradiomics/radiomics -# nosetests --nocapture -v tests/test_docstrings.py - import logging -from nose_parameterized import parameterized import six from radiomics import getFeatureClasses -from testUtils import custom_name_func featureClasses = getFeatureClasses() -def setup_module(module): - # runs before anything in this file - print("") # this is to get a newline after the dots - return +def pytest_generate_tests(metafunc): + metafunc.parametrize(["featureClassName", "featureName"], metafunc.cls.generate_scenarios()) class TestDocStrings: - def setup(self): - # setup before each test method - print("") # this is to get a newline after the dots - - @classmethod - def setup_class(cls): - # called before any methods in this class - print("") # this is to get a newline after the dots - - @classmethod - def teardown_class(cls): - # run after any methods in this class - print("") # this is to get a newline after the dots + @staticmethod def generate_scenarios(): global featureClasses for featureClassName, featureClass in six.iteritems(featureClasses): @@ -45,7 +25,6 @@ def generate_scenarios(): for f in featureNames: yield (featureClassName, f) - @parameterized.expand(generate_scenarios(), testcase_func_name=custom_name_func) def test_class(self, featureClassName, featureName): global featureClasses logging.info('%s', featureName) diff --git a/tests/test_exampleSettings.py b/tests/test_exampleSettings.py index eba2c226..ceeb3b0b 100644 --- a/tests/test_exampleSettings.py +++ b/tests/test_exampleSettings.py @@ -1,24 +1,24 @@ -# to run this test, from directory above: -# setenv PYTHONPATH /path/to/pyradiomics/radiomics -# nosetests --nocapture -v tests/test_exampleSettings.py - import os -from nose_parameterized import parameterized import pykwalify.core from radiomics import getParameterValidationFiles +schemaFile, schemaFuncs = getParameterValidationFiles() + +def pytest_generate_tests(metafunc): + metafunc.parametrize("settingsFile", metafunc.cls.generate_scenarios()) + + def exampleSettings_name_func(testcase_func, param_num, param): return '%s_%s' % (testcase_func.__name__, os.path.splitext(os.path.basename(param.args[0]))[0]) class TestExampleSettings: - def __init__(self): - self.schemaFile, self.schemaFuncs = getParameterValidationFiles() - def generateScenarios(): + @staticmethod + def generate_scenarios(): dataDir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'examples', 'exampleSettings') if os.path.isdir(dataDir): settingsFiles = [fname for fname in os.listdir(dataDir) if fname.endswith('.yaml') or fname.endswith('.yml')] @@ -26,10 +26,10 @@ def generateScenarios(): for fname in settingsFiles: yield os.path.join(dataDir, fname) - @parameterized.expand(generateScenarios(), testcase_func_name=exampleSettings_name_func) def test_scenarios(self, settingsFile): + global schemaFile, schemaFuncs - assert os.path.isfile(self.schemaFile) - assert os.path.isfile(self.schemaFuncs) - c = pykwalify.core.Core(source_file=settingsFile, schema_files=[self.schemaFile], extensions=[self.schemaFuncs]) + assert os.path.isfile(schemaFile) + assert os.path.isfile(schemaFuncs) + c = pykwalify.core.Core(source_file=settingsFile, schema_files=[schemaFile], extensions=[schemaFuncs]) c.validate() diff --git a/tests/test_features.py b/tests/test_features.py index 05e88d9b..29f94b78 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,15 +1,10 @@ -# to run this test, from directory above: -# setenv PYTHONPATH /path/to/pyradiomics/radiomics -# nosetests --nocapture -v tests/test_features.py - import logging import os -from nose_parameterized import parameterized import six from radiomics import getFeatureClasses -from testUtils import custom_name_func, RadiomicsTestUtils +from testUtils import RadiomicsTestUtils testUtils = RadiomicsTestUtils() tests = sorted(testUtils.getTests()) @@ -18,8 +13,13 @@ featureClasses = getFeatureClasses() +def pytest_generate_tests(metafunc): + metafunc.parametrize(["testCase", "featureName"], metafunc.cls.generate_scenarios()) + + class TestFeatures: + @staticmethod def generate_scenarios(): global tests, featureClasses @@ -51,17 +51,16 @@ def generate_scenarios(): for featureName in baselineFeatureNames: yield test, featureName - @parameterized.expand(generate_scenarios(), testcase_func_name=custom_name_func) - def test_scenario(self, test, featureName): + def test_scenario(self, testCase, featureName): print("") global testUtils, featureClass, featureClasses featureName = featureName.split('_') - logging.debug('test_scenario: test = %s, featureClassName = %s, featureName = %s', test, featureName[1], + logging.debug('test_scenario: test = %s, featureClassName = %s, featureName = %s', testCase, featureName[1], featureName[-1]) - testOrClassChanged = testUtils.setFeatureClassAndTestCase(featureName[1], test) + testOrClassChanged = testUtils.setFeatureClassAndTestCase(featureName[1], testCase) testImage = testUtils.getImage(featureName[0]) testMask = testUtils.getMask(featureName[0]) diff --git a/tests/test_matrices.py b/tests/test_matrices.py index b4858307..00ab3969 100644 --- a/tests/test_matrices.py +++ b/tests/test_matrices.py @@ -1,16 +1,11 @@ -# to run this test, from directory above: -# setenv PYTHONPATH /path/to/pyradiomics/radiomics -# nosetests --nocapture -v tests/test_features.py - import logging import os -from nose_parameterized import parameterized import numpy import six from radiomics import getFeatureClasses, testCases -from testUtils import custom_name_func, RadiomicsTestUtils +from testUtils import RadiomicsTestUtils testUtils = RadiomicsTestUtils() @@ -18,8 +13,13 @@ featureClasses = getFeatureClasses() +def pytest_generate_tests(metafunc): + metafunc.parametrize(["testCase", "featureClassName"], metafunc.cls.generate_scenarios()) + + class TestMatrices: + @staticmethod def generate_scenarios(): global featureClasses @@ -27,23 +27,22 @@ def generate_scenarios(): if testCase.startswith('test'): continue for className, featureClass in six.iteritems(featureClasses): - assert(featureClass is not None) + assert featureClass is not None if "_calculateMatrix" in dir(featureClass): logging.debug('generate_scenarios: featureClass = %s', className) yield testCase, className - @parameterized.expand(generate_scenarios(), testcase_func_name=custom_name_func) - def test_scenario(self, test, featureClassName): + def test_scenario(self, testCase, featureClassName): global testUtils, featureClasses - logging.debug('test_scenario: testCase = %s, featureClassName = %s', test, featureClassName) + logging.debug('test_scenario: testCase = %s, featureClassName = %s', testCase, featureClassName) - baselineFile = os.path.join(testUtils.getDataDir(), 'baseline', '%s_%s.npy' % (test, featureClassName)) + baselineFile = os.path.join(testUtils.getDataDir(), 'baseline', '%s_%s.npy' % (testCase, featureClassName)) assert os.path.isfile(baselineFile) baselineMatrix = numpy.load(baselineFile) - testUtils.setFeatureClassAndTestCase(featureClassName, test) + testUtils.setFeatureClassAndTestCase(featureClassName, testCase) testImage = testUtils.getImage('original') testMask = testUtils.getMask('original') diff --git a/tests/test_wavelet.py b/tests/test_wavelet.py index 149f2f9e..2d2c71aa 100644 --- a/tests/test_wavelet.py +++ b/tests/test_wavelet.py @@ -1,11 +1,6 @@ -# to run this test, from directory above: -# setenv PYTHONPATH /path/to/pyradiomics/radiomics -# nosetests --nocapture -v tests/test_features.py - import logging import os -from nose_parameterized import parameterized import numpy import SimpleITK as sitk @@ -13,43 +8,16 @@ logger = logging.getLogger('radiomics.testing') testCases = ('test_wavelet_64x64x64', 'test_wavelet_37x37x37') -baselineFile = '../data/baseline/wavelet.npy' - - -def custom_name_func(testcase_func, param_num, param): - """ - A custom test name function that will ensure that the tests are run such that they're batched with all tests for a - given data set are run together, avoiding re-reading the data more than necessary. Tests are run in alphabetical - order, so put the test case first. An alternate option is to right justify the test number (param_num) with zeroes - so that the numerical and alphabetical orders are the same. Not providing this method when there are more than 10 - tests results in tests running in an order similar to: - - test_*.test_scenario_0_* - - test_*.test_scenario_10_* - - test_*.test_scenario_11_* - - ... - - test_*.test_scenario_19_* - - test_*.test_scenario_1_* +baselineFile = os.path.join(os.path.dirname(__file__), '../data/baseline/wavelet.npy') - test_*.test_scenario_20_* - """ - global logger - logger.debug('custom_name_func: function name = %s, param_num = {0:0>3}, param.args = %s'.format(param_num), - testcase_func.__name__, param.args) - return str("%s_%s" % ( - testcase_func.__name__, - parameterized.to_safe_name(param.args[0]), - )) +def pytest_generate_tests(metafunc): + metafunc.parametrize(["testCase", "image", "mask", "baseline"], metafunc.cls.generate_scenarios()) class TestWavelet: + @staticmethod def generate_scenarios(): global logger, testCases, baselineFile @@ -85,11 +53,10 @@ def generate_scenarios(): level = wavelet_name.split('-')[1] yield '_'.join((testCase, 'preCropped', wavelet_name)), image, mask, baselineDict[level] - @parameterized.expand(generate_scenarios(), testcase_func_name=custom_name_func) - def test_scenario(self, test, image, mask, baseline): + def test_scenario(self, testCase, image, mask, baseline): global logger, testUtils, featureClasses - logger.debug('test_scenario: testCase = %s,', test) + logger.debug('test_scenario: testCase = %s,', testCase) im_arr = sitk.GetArrayFromImage(image) ma_arr = sitk.GetArrayFromImage(mask) == 1 # Conver to boolean array, label = 1