diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82a69e61..038ab33c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,8 +69,6 @@ jobs: gpg_key: ${{ secrets.GPG_KEY }} password: ${{ secrets.PYPI_TOKEN }} upload: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} - env: - PATHTOOLS: ${{ github.workspace }}/NiftyPET_tools - id: meta name: Changelog run: | diff --git a/README.rst b/README.rst index 4598b356..e529f29e 100644 --- a/README.rst +++ b/README.rst @@ -14,26 +14,22 @@ The upsampling is needed for more accurate extraction (sampling) of PET data usi PVC is needed to correct for the spill-in and spill-out of PET signal from defined ROIs (specific for any given application). -In order to facilitate these operations, NIMPA relies on third-party software for image conversion from DICOM to NIfTI (dcm2niix) and image registration (NiftyReg). The additional software is installed automatically to a user specified location. - **Documentation with installation manual and tutorials**: https://niftypet.readthedocs.io/ Quick Install ~~~~~~~~~~~~~ -Note that installation prompts for setting the path to ``NiftyPET_tools``. -This can be avoided by setting the environment variables ``PATHTOOLS``. -It's also recommended (but not required) to use `conda`. +Note that it's recommended (but not required) to use `conda`. .. code:: sh - # optional (Linux syntax) to avoid prompts - export PATHTOOLS=$HOME/NiftyPET_tools # cross-platform install conda install -c conda-forge python=3 \ - ipykernel numpy scipy scikit-image matplotlib ipywidgets + ipykernel numpy scipy scikit-image matplotlib ipywidgets dcm2niix pip install "nimpa>=2" +For optional `dcm2niix `_ (image conversion from DICOM to NIfTI) and/or `niftyreg `_ (image registration) support, simply install them separately (``pip install dcm2niix niftyreg``). + External CMake Projects ~~~~~~~~~~~~~~~~~~~~~~~ @@ -68,7 +64,7 @@ Licence Copyright 2018-21 - `Pawel J. Markiewicz `__ @ University College London -- `Casper O. da Costa-Luis `__ @ King's College London +- `Casper O. da Costa-Luis `__ @ University College London/King's College London - `Contributors `__ .. |Docs| image:: https://readthedocs.org/projects/niftypet/badge/?version=latest diff --git a/niftypet/nimpa/__init__.py b/niftypet/nimpa/__init__.py index dc036549..d29a243a 100644 --- a/niftypet/nimpa/__init__.py +++ b/niftypet/nimpa/__init__.py @@ -30,14 +30,17 @@ 'centre_mass_img', 'centre_mass_corr', 'coreg_spm', 'coreg_vinci', 'create_dir', 'create_mask', 'ct2mu', 'dcm2im', 'dcm2nii', 'dcmanonym', 'dcminfo', 'dcmsort', - 'dice_coeff', 'dice_coeff_multiclass', 'fwhm2sig', 'getnii', + 'dice_coeff', 'dice_coeff_multiclass', 'fwhm2sig', 'getmgh', 'getnii', 'mgh2nii', 'getnii_descr', 'im_cut', 'imfill', 'imsmooth', 'iyang', 'motion_reg', 'nii_gzip', 'nii_modify', 'nii_ugzip', 'niisort', 'orientnii', 'pet2pet_rigid', 'pick_t1w', 'psf_gaussian', 'psf_measured', 'pvc_iyang', 'realign_mltp_spm', 'resample_fsl', 'resample_mltp_spm', 'resample_niftyreg', 'resample_spm', 'resample_vinci', 'resample_dipy', 'time_stamp'] # yapf: disable -from numcu import add, div, mul +try: + from numcu import add, div, mul +except ImportError: + pass from pkg_resources import resource_filename from niftypet.ninst import cudasetup as cs @@ -68,6 +71,7 @@ dice_coeff, dice_coeff_multiclass, fwhm2sig, + getmgh, getnii, getnii_descr, im_cut, @@ -75,6 +79,7 @@ imsmooth, isub, iyang, + mgh2nii, motion_reg, nii_gzip, nii_modify, diff --git a/niftypet/nimpa/prc/__init__.py b/niftypet/nimpa/prc/__init__.py index a3d36665..57463c04 100644 --- a/niftypet/nimpa/prc/__init__.py +++ b/niftypet/nimpa/prc/__init__.py @@ -2,8 +2,8 @@ __all__ = [ # imio 'array2nii', 'create_dir', 'dcm2im', 'dcm2nii', 'dcmanonym', 'dcminfo', 'dcmsort', 'fwhm2sig', - 'getnii', 'getnii_descr', 'nii_gzip', 'nii_ugzip', 'niisort', 'orientnii', 'pick_t1w', - 'time_stamp', + 'mgh2nii', 'getmgh', 'getnii', 'getnii_descr', 'nii_gzip', 'nii_ugzip', 'niisort', 'orientnii', + 'pick_t1w', 'time_stamp', # prc 'bias_field_correction', 'centre_mass_img', 'centre_mass_corr', 'ct2mu', 'im_cut', 'imsmooth', 'imtrimup', @@ -26,8 +26,10 @@ dcminfo, dcmsort, fwhm2sig, + getmgh, getnii, getnii_descr, + mgh2nii, nii_gzip, nii_ugzip, niisort, diff --git a/niftypet/nimpa/prc/imio.py b/niftypet/nimpa/prc/imio.py index 81b60ab9..79327c5d 100644 --- a/niftypet/nimpa/prc/imio.py +++ b/niftypet/nimpa/prc/imio.py @@ -1,6 +1,7 @@ """image input/output functionalities.""" import datetime import logging +import numbers import os import pathlib import re @@ -50,6 +51,107 @@ def fwhm2sig(fwhm, voxsize=2.0): return (fwhm/voxsize) / (2 * (2 * np.log(2))**.5) +def mgh2nii(fim, fout=None, output=None): + ''' Convert `*.mgh` or `*.mgz` FreeSurfer image to NIfTI. + Arguments: + fim: path to the input MGH file + fout: path to the output NIfTI file, if None then + creates based on `fim` + output: if not None and an applicable string it will + output a dictionary or an array (see below) + Return: + None: returns nothing + 'image' or 'im': outputs just the image + 'affine': outputs just the affine matrix + 'all': outputs all as a dictionary + ''' + + if not os.path.isfile(fim): + raise ValueError('The input path is incorrect!') + + # > get the image dictionary + mghd = getmgh(fim, output='all') + im = mghd['im'] + + # > sort out the output + if fout is None: + fout = fim.parent / (fim.name.split('.')[0] + '.nii.gz') + + out = fout + if output == 'image' or output == 'im': + out = fout, im + elif output == 'affine': + out = fout, mghd['affine'] + elif output == 'all': + out = mghd + out['fout'] = fout + + array2nii( + mghd['im'], mghd['affine'], fout, + trnsp=(mghd['transpose'].index(0), mghd['transpose'].index(1), mghd['transpose'].index(2)), + flip=mghd['flip']) + + return out + + +def getmgh(fim, nan_replace=None, output='image'): + ''' + Get image from `*.mgz` or `*.mgh` file (FreeSurfer). + Arguments: + fim: input file name for the MGH/Z image + output: option for choosing output: 'image', 'affine' matrix or + 'all' for a dictionary with all the info. + Return: + 'image': outputs just the image + 'affine': outputs just the affine matrix + 'all': outputs all as a dictionary + ''' + + if not os.path.isfile(fim): + raise ValueError('The input path is incorrect!') + + mgh = nib.freesurfer.load(str(fim)) + + if output == 'image' or output == 'all': + + imr = np.asanyarray(mgh.dataobj) + # replace NaNs if requested + if isinstance(nan_replace, numbers.Number): + imr[np.isnan(imr)] = nan_replace + + imr = np.squeeze(imr) + dimno = imr.ndim + + # > get orientations from the affine + ornt = nib.io_orientation(mgh.affine) + trnsp = tuple(np.flip(np.argsort(ornt[:, 0]))) + flip = tuple(np.int8(ornt[:, 1])) + + # > flip y-axis and z-axis and then transpose + if dimno == 4: # dynamic + imr = np.transpose(imr[::-flip[0], ::-flip[1], ::-flip[2], :], (3,) + trnsp) + elif dimno == 3: # static + imr = np.transpose(imr[::-flip[0], ::-flip[1], ::-flip[2]], trnsp) + + # # > voxel size + # voxsize = mgh.header.get('pixdim')[1:mgh.header.get('dim')[0] + 1] + # # > rearrange voxel size according to the orientation + # voxsize = voxsize[np.array(trnsp)] + + if output == 'all': + out = { + 'im': imr, 'affine': mgh.affine, 'fim': fim, 'dtype': mgh.get_data_dtype(), + 'shape': imr.shape, 'hdr': mgh.header, 'transpose': trnsp, 'flip': flip} + elif output == 'image': + out = imr + elif output == 'affine': + out = mgh.affine + else: + raise NameError("Unrecognised output request!") + + return out + + def getnii_descr(fim): '''Extracts the custom description header field to dictionary''' nim = nib.load(fim) @@ -183,6 +285,11 @@ def dcminfo(dcmvar, Cnt=None, output='detail', t1_name='mprage'): cmmnt = dhdr[0x0020, 0x4000].value log.debug(' Comments: {}'.format(cmmnt)) + # > institution + inst = '' + if [0x008, 0x080] in dhdr: + inst = dhdr[0x008, 0x080].value + prtcl = '' if [0x18, 0x1030] in dhdr: prtcl = dhdr[0x18, 0x1030].value @@ -297,7 +404,7 @@ def dcminfo(dcmvar, Cnt=None, output='detail', t1_name='mprage'): if validTs: mrdct = { - 'series': srs, 'protocol': prtcl, 'units': unt, 'study_time': study_time, + 'series': srs, 'protocol': prtcl, 'units': unt, 'study_time': study_time, 'inst': inst, 'series_time': series_time, 'acq_time': acq_time, 'scanner_id': scanner_id, 'TR': TR, 'TE': TE} # --------------------------------------------- @@ -324,11 +431,11 @@ def dcminfo(dcmvar, Cnt=None, output='detail', t1_name='mprage'): elif isPET: petdct = { 'series': srs, 'protocol': prtcl, 'study_time': study_time, 'series_time': series_time, - 'acq_time': acq_time, 'scanner_id': scanner_id, 'type': srs_type, 'units': unt, - 'recon': recon, 'decay_corr': decay_corr, 'dcf': dcf, 'attenuation': atten, - 'scatter': scat, 'scf': scf, 'randoms': rand, 'dose_calib': dscf, 'dead_time': dt, - 'tracer': tracer, 'total_dose': tdose, 'half_life': hlife, 'positron_fract': pfract, - 'radio_start_time': ttime0, 'radio_stop_time': ttime1} + 'inst': inst, 'acq_time': acq_time, 'scanner_id': scanner_id, 'type': srs_type, + 'units': unt, 'recon': recon, 'decay_corr': decay_corr, 'dcf': dcf, + 'attenuation': atten, 'scatter': scat, 'scf': scf, 'randoms': rand, 'dose_calib': dscf, + 'dead_time': dt, 'tracer': tracer, 'total_dose': tdose, 'half_life': hlife, + 'positron_fract': pfract, 'radio_start_time': ttime0, 'radio_stop_time': ttime1} out = ['pet', tracer.lower(), srs_type.lower(), scanner_id, petdct] diff --git a/niftypet/nimpa/prc/prc.py b/niftypet/nimpa/prc/prc.py index 87f70141..fd6d586d 100644 --- a/niftypet/nimpa/prc/prc.py +++ b/niftypet/nimpa/prc/prc.py @@ -727,7 +727,6 @@ def pvc_iyang( ft1w, outpath=os.path.join(outpath, 'PET', 'positioning'), fcomment=fcomment, - executable=Cnt['REGPATH'], omp=multiprocessing.cpu_count() / 2, rigOnly=True, affDirect=False, @@ -1286,6 +1285,9 @@ def bias_field_correction(fmr, fimout='', outpath='', fcomment='_N4bias', execut if len(outdct['fim']) == 1: outdct['fim'] = outdct['fim'][0] + if 'fmsk' in outdct: + outdct['fmsk'] = outdct['fmsk'][0] + return outdct diff --git a/niftypet/nimpa/prc/regseg.py b/niftypet/nimpa/prc/regseg.py index d1dcb51b..e89b252f 100644 --- a/niftypet/nimpa/prc/regseg.py +++ b/niftypet/nimpa/prc/regseg.py @@ -7,6 +7,7 @@ import os import shutil import sys +from os import fspath from subprocess import call from textwrap import dedent @@ -276,7 +277,7 @@ def affine_niftyreg( fname_aff='', pickname='ref', fcomment='', - executable='', + executable=None, omp=1, rigOnly=False, affDirect=False, @@ -294,10 +295,13 @@ def affine_niftyreg( fthrsh=0.05, verbose=True, ): - - # check if the executable exists: + if not executable: + executable = getattr(rs, 'REGPATH', None) + if not executable: + from niftyreg import bin_path + executable = fspath(next(bin_path.glob("reg_aladin*"))) if not os.path.isfile(executable): - raise IOError('Incorrect path to executable file for registration.') + raise IOError(f"executable not found:{executable}") # create a folder for images registered to ref if outpath != '': @@ -377,16 +381,16 @@ def resample_niftyreg( fcomment='', pickname='ref', intrp=1, - executable='', + executable=None, verbose=True, ): - - # check if the executable exists: - # if executable=='' and 'RESPATH' in Cnt and os.path.isfile(Cnt['RESPATH']): - # executable = Cnt['RESPATH'] - + if not executable: + executable = getattr(rs, 'RESPATH', None) + if not executable: + from niftyreg import bin_path + executable = fspath(next(bin_path.glob("reg_resample*"))) if not os.path.isfile(executable): - raise IOError('Incorrect path to executable file for registration.') + raise IOError(f"executable not found:{executable}") # > output path if outpath == '' and fimout != '': diff --git a/pyproject.toml b/pyproject.toml index 0a28fa3c..6582a23d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] +# cuvec>=2.8.0 requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4", - "ninst>=0.8.0", "cuvec>=2.8.0", "miutil[cuda]>=0.4.0", + "ninst>=0.9.0", "cuvec-base", "miutil[cuda]>=0.4.0", "scikit-build>=0.11.0", "cmake>=3.18", "ninja"] [tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg index d0a5c3f2..0ea1fbed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,26 +39,12 @@ setup_requires= setuptools>=42 wheel setuptools_scm[toml] - ninst>=0.8.0 + ninst>=0.9.0 scikit-build>=0.11.0 cmake>=3.18 ninja - cuvec>=2.8.0 + cuvec-base miutil[cuda]>=0.4.0 -install_requires= - cuvec>=2.3.1 - pydcm2niix>=1.0.20220116 - dipy>=1.3.0 - miutil[nii]>=0.10.0 - nibabel>=2.4.0 - ninst>=0.4.0 - numcu - numpy>=1.14 - pydicom>=1.0.2 - scipy - setuptools - spm12 - # SimpleITK>=1.2.0 python_requires=>=3.6 [options.extras_require] dev= @@ -68,6 +54,9 @@ dev= pytest-timeout pytest-xdist plot=miutil[plot] +cuda=cuvec>=2.3.1; numcu +dcm2niix=dcm2niix>=1.0.20220116 +niftyreg=niftyreg [flake8] max_line_length=99 diff --git a/setup.py b/setup.py index d8af94a4..063b36b2 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ import re import sys from pathlib import Path -from textwrap import dedent from setuptools import find_packages, setup from setuptools_scm import get_version @@ -24,79 +23,34 @@ tls.check_platform() ext = tls.check_depends() # external dependencies -if not ext["git"]: - raise SystemError( - dedent("""\ - -------------------------------------------------------------- - Git is not installed but is required for tools installation. - --------------------------------------------------------------""")) - cs.resources_setup(gpu=False) # install resources.py try: - gpuarch = cs.dev_setup() # update resources.py with a supported GPU device + cs.dev_setup() # update resources.py with a supported GPU device except Exception as exc: log.error("could not set up CUDA:\n%s", exc) - gpuarch = None -# First install third party apps for NiftyPET tools -log.info( - dedent("""\ - -------------------------------------------------------------- - Setting up NiftyPET tools ... - --------------------------------------------------------------""")) # get the local path to NiftyPET resources.py path_resources = cs.path_niftypet_local() # if exists, import the resources and get the constants resources = cs.get_resources() # get the current setup, if any Cnt = resources.get_setup() -# check the installation of tools -chck_tls = tls.check_version(Cnt, chcklst=["RESPATH", "REGPATH"]) - -# ------------------------------------------- -# NiftyPET tools: -# ------------------------------------------- -if "sdist" not in sys.argv or any(i in sys.argv for i in ["build", "bdist", "wheel"]): - # NiftyReg - if not chck_tls["REGPATH"] or not chck_tls["RESPATH"]: - reply = True - if not gpuarch: - try: - reply = tls.query_yesno( - "q> the latest compatible version of NiftyReg seems to be missing.\n" - " Do you want to install it?") - except BaseException: - pass - - if reply: - log.info( - dedent("""\ - -------------------------------------------------------------- - Installing NiftyReg ... - --------------------------------------------------------------""")) - Cnt = tls.install_tool("niftyreg", Cnt) - log.info( - dedent("""\ - -------------------------------------------------------------- - Installation of NiftyPET-tools is done. - --------------------------------------------------------------""")) -else: - log.info( - dedent("""\ - -------------------------------------------------------------- - Skipping installation of NiftyPET-tools. - --------------------------------------------------------------""")) build_ver = ".".join(__version__.split('.')[:3]).split(".dev")[0] setup_kwargs = { "use_scm_version": True, "packages": find_packages(exclude=["tests"]), - "package_data": {"niftypet": ["nimpa/auxdata/*"]}} + "package_data": {"niftypet": ["nimpa/auxdata/*"]}, "install_requires": [ + 'dipy>=1.3.0', 'miutil[nii]>=0.10.0', 'nibabel>=2.4.0', 'ninst>=0.4.0', 'numpy>=1.14', + 'pydicom>=1.0.2', 'scipy', 'setuptools', 'spm12']} +# 'SimpleITK>=1.2.0' cmake_args = [ f"-DNIMPA_BUILD_VERSION={build_ver}", f"-DPython3_ROOT_DIR={sys.prefix}", f"-DNIMPA_KERNEL_RADIUS={getattr(resources, 'RSZ_PSF_KRNL', 8)}"] try: + import cuvec as cu from skbuild import setup as sksetup + assert cu.include_path.is_dir() nvcc_arches = {"{2:d}{3:d}".format(*i) for i in dinf.gpuinfo() if i[2:4] >= (3, 5)} if nvcc_arches: cmake_args.append("-DCMAKE_CUDA_ARCHITECTURES=" + ";".join(sorted(nvcc_arches))) @@ -104,6 +58,7 @@ log.warning("Import or CUDA device detection error:\n%s", exc) setup(**setup_kwargs) else: + setup_kwargs['install_requires'].extend(["cuvec>=2.3.1", "numcu"]) for i in (Path(__file__).resolve().parent / "_skbuild").rglob("CMakeCache.txt"): i.write_text(re.sub("^//.*$\n^[^#].*pip-build-env.*$", "", i.read_text(), flags=re.M)) sksetup(cmake_source_dir="niftypet", cmake_languages=("C", "CXX", "CUDA"),