Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
14 changes: 5 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/rordenlab/dcm2niix>`_ (image conversion from DICOM to NIfTI) and/or `niftyreg <https://github.com/KCL-BMEIS/niftyreg>`_ (image registration) support, simply install them separately (``pip install dcm2niix niftyreg``).

External CMake Projects
~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -68,7 +64,7 @@ Licence
Copyright 2018-21

- `Pawel J. Markiewicz <https://github.com/pjmark>`__ @ University College London
- `Casper O. da Costa-Luis <https://github.com/casperdcl>`__ @ King's College London
- `Casper O. da Costa-Luis <https://github.com/casperdcl>`__ @ University College London/King's College London
- `Contributors <https://github.com/NiftyPET/NIMPA/graphs/contributors>`__

.. |Docs| image:: https://readthedocs.org/projects/niftypet/badge/?version=latest
Expand Down
9 changes: 7 additions & 2 deletions niftypet/nimpa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -68,13 +71,15 @@
dice_coeff,
dice_coeff_multiclass,
fwhm2sig,
getmgh,
getnii,
getnii_descr,
im_cut,
imfill,
imsmooth,
isub,
iyang,
mgh2nii,
motion_reg,
nii_gzip,
nii_modify,
Expand Down
6 changes: 4 additions & 2 deletions niftypet/nimpa/prc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -26,8 +26,10 @@
dcminfo,
dcmsort,
fwhm2sig,
getmgh,
getnii,
getnii_descr,
mgh2nii,
nii_gzip,
nii_ugzip,
niisort,
Expand Down
119 changes: 113 additions & 6 deletions niftypet/nimpa/prc/imio.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""image input/output functionalities."""
import datetime
import logging
import numbers
import os
import pathlib
import re
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
# ---------------------------------------------
Expand All @@ -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]

Expand Down
4 changes: 3 additions & 1 deletion niftypet/nimpa/prc/prc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down
26 changes: 15 additions & 11 deletions niftypet/nimpa/prc/regseg.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import shutil
import sys
from os import fspath
from subprocess import call
from textwrap import dedent

Expand Down Expand Up @@ -276,7 +277,7 @@ def affine_niftyreg(
fname_aff='',
pickname='ref',
fcomment='',
executable='',
executable=None,
omp=1,
rigOnly=False,
affDirect=False,
Expand All @@ -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 != '':
Expand Down Expand Up @@ -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 != '':
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
21 changes: 5 additions & 16 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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
Expand Down
Loading