"""Read/Write Nifti-1 files
"""
# Copyright (c) 2013-2022 Erling Andersen, Haukeland University Hospital, Bergen, Norway
import os.path
import tempfile
import logging
import math
import nibabel
import nibabel.spatialimages
import numpy as np
from . import NotImageError, WriteNotImplemented, input_order_to_dirname_str,\
shape_to_str, sort_on_to_str,\
SORT_ON_SLICE
from ..axis import UniformLengthAxis
from .abstractplugin import AbstractPlugin
# import nitransforms
logger = logging.getLogger(__name__)
NIFTI_XFORM_UNKNOWN = 0
NIFTI_XFORM_SCANNER_ANAT = 1
NIFTI_XFORM_ALIGNED_ANAT = 2
NIFTI_XFORM_TALAIRACH = 3
NIFTI_XFORM_MNI_152 = 4
[docs]class NiftiPlugin(AbstractPlugin):
"""Read/write Nifti-1 files.
"""
name = "nifti"
description = "Read and write Nifti-1 files."
authors = "Erling Andersen"
version = "1.0.0"
url = "www.helse-bergen.no"
"""
data - getter and setter - NumPy array
read() method
write() method
"""
def __init__(self):
super(NiftiPlugin, self).__init__(self.name, self.description,
self.authors, self.version, self.url)
self.shape = None
self.slices = None
self.spacing = None
self.transformationMatrix = None
self.imagePositions = None
self.tags = None
self.origin = None
self.orientation = None
self.normal = None
self.output_sort = None
def _read_image(self, f, opts, hdr):
"""Read image data from given file handle
Args:
self: format plugin instance
f: file handle or filename (depending on self._need_local_file)
opts: Input options (dict)
hdr: Header
Returns:
Tuple of
hdr: Header
Return values:
- info: Internal data for the plugin
None if the given file should not be included (e.g. raw file)
si: numpy array (multi-dimensional)
"""
logger.debug("niftiplugin::read filehandle {}".format(f))
# TODO: Read nifti directly from open file object
# Should be able to do something like:
#
# with archive.open(member_name) as member:
# # Create a nibabel image using
# # the existing file handle.
# fmap = nibabel.nifti1.Nifti1Image.make_file_map()
# #nibabel.nifti1.Nifti1Header
# fmap['image'].fileobj = member
# img = nibabel.Nifti1Image.from_file_map(fmap)
#
logger.debug("niftiplugin::read load f {}".format(f))
try:
img = nibabel.load(f)
except nibabel.spatialimages.ImageFileError:
raise NotImageError(
'{} does not look like a nifti file.'.format(f))
except Exception:
raise
info = img
if hdr.input_order == 'auto':
hdr.input_order = 'none'
hdr.color = False
si = self._reorder_to_dicom(
np.asanyarray(img.dataobj),
flip=False,
flipud=True)
return info, si
def _need_local_file(self):
"""Do the plugin need access to local files?
Returns:
Boolean:
- True: The plugin need access to local filenames
- False: The plugin can access files given by an open file handle
"""
return True
def _set_tags(self, image_list, hdr, si):
"""Set header tags.
Args:
self: format plugin instance
image_list: list with (img,si) tuples
hdr: Header
si: numpy array (multi-dimensional)
Returns:
hdr: Header
"""
img, si = image_list[0]
info = img.header
_data_shape = info.get_data_shape()
nt = nz = 1
nx, ny = _data_shape[:2]
if len(_data_shape) > 2:
nz = _data_shape[2]
if len(_data_shape) > 3:
nt = _data_shape[3]
logger.debug("_set_tags: ny {}, nx {}, nz {}, nt {}".format(ny, nx, nz, nt))
logger.debug('NiftiPlugin.read: get_qform\n{}'.format(info.get_qform()))
logger.debug('NiftiPlugin.read: info.get_zooms() {}'.format(info.get_zooms()))
_xyzt_units = info.get_xyzt_units()
_data_zooms = info.get_zooms()
# _dim_info = info.get_dim_info()
logger.debug("_set_tags: get_dim_info(): {}".format(info.get_dim_info()))
logger.debug("_set_tags: get_xyzt_units(): {}".format(info.get_xyzt_units()))
dt = dz = 1
dx, dy = _data_zooms[:2]
if len(_data_zooms) > 2:
dz = _data_zooms[2]
if len(_data_zooms) > 3:
dt = _data_zooms[3]
if _xyzt_units[0] == 'meter':
dx, dy, dz = dx * 1000., dy * 1000., dz * 1000.
elif _xyzt_units[0] == 'micron':
dx, dy, dz = dx / 1000., dy / 1000., dz / 1000.
if _xyzt_units[1] == 'msec':
dt = dt / 1000.
elif _xyzt_units[1] == 'usec':
dt = dt / 1000000.
self.spacing = (float(dz), float(dy), float(dx))
hdr.spacing = (float(dz), float(dy), float(dx))
# Simplify shape
self._reduce_shape(si)
sform, scode = info.get_sform(coded=True)
qform, qcode = info.get_qform(coded=True)
qfac = info['pixdim'][0]
if qfac not in (-1, 1):
raise ValueError('qfac (pixdim[0]) should be 1 or -1')
# Image orientation and positions
hdr.imagePositions = {}
if sform is not None and scode != 0:
logger.debug("Method 3 - sform: orientation")
for c in range(4): # NIfTI is RAS+, DICOM is LPS+
for r in range(2):
sform[r, c] = - sform[r, c]
Q = sform[:3, :3]
# p = sform[:3, 3]
p = nibabel.affines.apply_affine(sform, (0, ny - 1, 0))
if np.linalg.det(Q) < 0:
Q[:3, 1] = - Q[:3, 1]
# Note: rz, ry, rx, cz, cy, cx
iop = np.array([
Q[2, 0] / dx, Q[1, 0] / dx, Q[0, 0] / dx,
Q[2, 1] / dy, Q[1, 1] / dy, Q[0, 1] / dy
])
for _slice in range(nz):
_p = np.array([
(Q[0, 2] * _slice + p[0]), # NIfTI is RAS+, DICOM is LPS+
(Q[1, 2] * _slice + p[1]),
(Q[2, 2] * _slice + p[2])
])
hdr.imagePositions[_slice] = _p[::-1]
elif qform is not None and qcode != 0:
logger.debug("Method 2 - qform: orientation")
qoffset_x, qoffset_y, qoffset_z = qform[0:3, 3]
a, b, c, d = info.get_qform_quaternion()
rx = - (a * a + b * b - c * c - d * d)
ry = - (2 * b * c + 2 * a * d)
rz = (2 * b * d - 2 * a * c)
cx = - (2 * b * c - 2 * a * d)
cy = - (a * a + c * c - b * b - d * d)
cz = (2 * c * d + 2 * a * b)
# normal from quaternion derived once and saved for position calculation ...
# ... do not handle qfac here ... do it later
tx = - (2 * b * d + 2 * a * c) # NIfTI is RAS+, DICOM is LPS+
ty = - (2 * c * d - 2 * a * b) # NIfTI is RAS+, DICOM is LPS+
tz = (a * a + d * d - c * c - b * b)
iop = np.array([rz, ry, rx, cz, cy, cx])
for _slice in range(nz):
_p = np.array([
tx * qfac * dz * _slice - qoffset_x, # NIfTI is RAS+, DICOM is LPS+
ty * qfac * dz * _slice - qoffset_y, # NIfTI is RAS+, DICOM is LPS+
tz * qfac * dz * _slice + qoffset_z
])
hdr.imagePositions[_slice] = _p[::-1] # Reverse x,y,z
else:
logger.debug("Method 1 - assume axial: orientation")
iop = np.array([0, 0, 1, 0, 1, 0])
for _slice in range(nz):
_p = np.array([
0, # NIfTI is RAS+, DICOM is LPS+
0, # NIfTI is RAS+, DICOM is LPS+
dz * _slice
])
hdr.imagePositions[_slice] = _p[::-1] # Reverse x,y,z
hdr.orientation = iop
self.shape = si.shape
times = [0]
if nt > 1:
times = np.arange(0, nt * dt, dt)
assert len(times) == nt,\
"Wrong timeline calculated (times={}) (nt={})".format(len(times), nt)
logger.debug("_set_tags: times {}".format(times))
tags = {}
for z in range(nz):
tags[z] = np.array(times)
hdr.tags = tags
axes = list()
if si.ndim > 3:
axes.append(UniformLengthAxis(
input_order_to_dirname_str(hdr.input_order),
0,
nt,
dt)
)
if si.ndim > 2:
axes.append(UniformLengthAxis(
'slice',
0,
nz,
dz)
)
axes.append(UniformLengthAxis(
'row',
0,
ny,
dy)
)
axes.append(UniformLengthAxis(
'column',
0,
nx,
dx)
)
hdr.axes = axes
hdr.photometricInterpretation = 'MONOCHROME2'
hdr.color = False
# Set dummy DicomHeaderDict
hdr.DicomHeaderDict = {}
for _slice in range(nz):
hdr.DicomHeaderDict[_slice] = []
for tag in range(nt):
hdr.DicomHeaderDict[_slice].append(
(times[tag], None, hdr.empty_ds())
)
# def nifti_to_affine(self, affine, shape):
#
# if len(shape) != 4:
# raise ValueError("4D only (was: %dD)" % len(shape))
#
# q = affine.copy()
#
# logger.debug("q from nifti_to_affine():\n{}".format(q))
# # Swap row 0 (z) and 2 (x)
# q[[0, 2],:] = q[[2, 0],:]
# # Swap column 0 (z) and 2 (x)
# q[:,[0, 2]] = q[:,[2, 0]]
# logger.debug("q swap nifti_to_affine():\n{}".format(q))
#
# analyze_to_dicom = np.eye(4)
# analyze_to_dicom[0,3] = 1
# analyze_to_dicom[1,3] = 1
# analyze_to_dicom[2,3] = 1
# dicom_to_analyze = np.linalg.inv(analyze_to_dicom)
# q = np.dot(q,dicom_to_analyze)
# logger.debug("q after dicom_to_analyze:\n{}".format(q))
#
# analyze_to_dicom = np.eye(4)
# analyze_to_dicom[0,3] = -1
# analyze_to_dicom[1,1] = -1
# rows = shape[2]
# analyze_to_dicom[1,3] = rows
# analyze_to_dicom[2,3] = -1
# dicom_to_analyze = np.linalg.inv(analyze_to_dicom)
# q = np.dot(q,dicom_to_analyze)
# logger.debug("q after rows dicom_to_analyze:\n{}".format(q))
#
# patient_to_tal = np.eye(4)
# patient_to_tal[0,0] = -1
# patient_to_tal[1,1] = -1
# tal_to_patient = np.linalg.inv(patient_to_tal)
# q = np.dot(tal_to_patient,q)
# logger.debug("q after tal_to_patient:\n{}".format(q))
#
# return q
# def affine_to_nifti(self, shape):
# q = self.transformationMatrix.copy()
# logger.debug("Affine from self.transformationMatrix:\n{}".format(q))
# # Swap row 0 (z) and 2 (x)
# q[[0, 2],:] = q[[2, 0],:]
# # Swap column 0 (z) and 2 (x)
# q[:,[0, 2]] = q[:,[2, 0]]
# logger.debug("Affine swap self.transformationMatrix:\n{}".format(q))
#
# # q now equals dicom_to_patient in spm_dicom_convert
#
# # Convert space
# analyze_to_dicom = np.eye(4)
# analyze_to_dicom[0,3] = -1
# analyze_to_dicom[1,1] = -1
# #if len(shape) == 3:
# # rows = shape[1]
# #else:
# # rows = shape[2]
# rows = shape[-2]
# analyze_to_dicom[1,3] = rows
# analyze_to_dicom[2,3] = -1
# logger.debug("analyze_to_dicom:\n{}".format(analyze_to_dicom))
#
# patient_to_tal = np.eye(4)
# patient_to_tal[0,0] = -1
# patient_to_tal[1,1] = -1
# logger.debug("patient_to_tal:\n{}".format(patient_to_tal))
#
# q = np.dot(patient_to_tal,q)
# logger.debug("q with patient_to_tal:\n{}".format(q))
# q = np.dot(q,analyze_to_dicom)
# # q now equals mat in spm_dicom_convert
#
# analyze_to_dicom = np.eye(4)
# analyze_to_dicom[0,3] = 1
# analyze_to_dicom[1,3] = 1
# analyze_to_dicom[2,3] = 1
# logger.debug("analyze_to_dicom:\n{}".format(analyze_to_dicom))
# q = np.dot(q,analyze_to_dicom)
#
# logger.debug("q nifti:\n{}".format(q))
# return q
@staticmethod
def _get_geometry_from_affine(hdr, q):
"""Extract geometry attributes from Nifti header
Args:
self: NiftiPlugin instance
q: nifti Qform
hdr.spacing
Returns:
hdr: header
- hdr.imagePositions[0]
- hdr.orientation
- hdr.transformationMatrix
"""
# Swap back from nifti patient space, flip x and y directions
affine = np.dot(np.diag([-1, -1, 1, 1]), q)
# Set imagePositions for first slice
x, y, z = affine[0:3, 3]
hdr.imagePositions = {0: np.array([z, y, x])}
logger.debug("getGeometryFromAffine: hdr imagePositions={}".format(hdr.imagePositions))
# Set slice orientation
ds, dr, dc = hdr.spacing
logger.debug("getGeometryFromAffine: spacing ds {}, dr {}, dc {}".format(ds, dr, dc))
colr = affine[:3, 0][::-1] / dr
colc = affine[:3, 1][::-1] / dc
# T0 = affine[:3,3][::-1]
orient = []
logger.debug("getGeometryFromAffine: affine\n{}".format(affine))
for i in range(3):
orient.append(colc[i])
for i in range(3):
orient.append(colr[i])
logger.debug("getGeometryFromAffine: orient {}".format(orient))
hdr.orientation = orient
return
# noinspection PyPep8Naming
[docs] def create_affine_xyz(self):
"""Create affine in xyz.
"""
def normalize(v):
"""Normalize a vector
https://stackoverflow.com/questions/21030391/how-to-normalize-an-array-in-numpy
Args:
v: 3D vector
Returns:
normalized 3D vector
"""
norm = np.linalg.norm(v, ord=1)
if norm == 0:
norm = np.finfo(v.dtype).eps
return v / norm
ds, dr, dc = self.spacing
# NIfTI is RAS+, DICOM is LPS+
colr = normalize(np.array(self.orientation[3:6])).reshape((3,)) * [1, 1, -1]
colc = normalize(np.array(self.orientation[0:3])).reshape((3,)) * [-1, -1, 1]
# T0 = self.imagePositions[0][::-1].reshape(3, ) # x,y,z
if self.slices > 1:
# Tn = self.imagePositions[self.slices - 1][::-1].reshape(3, ) # x,y,z
# k = Tn
k = np.cross(colc, colr, axis=0)
k = k * ds
else:
k = np.cross(colc, colr, axis=0)
k = k * ds
L = np.zeros((4, 4))
L[:3, 1] = colr * dr
L[:3, 0] = colc * dc
L[:3, 2] = -k
ny = self.shape[-2]
p = self.getPositionForVoxel((0, ny - 1, 0))[::-1]
# L[:3, 3] = self.origin * [-1, -1, 1]
L[:3, 3] = p * [-1, -1, 1]
L[3, 3] = 1
return L
# def getQformFromTransformationMatrix(self):
# # def matrix_from_orientation(orientation, normal):
# # oT = orientation.reshape((2,3)).T
# # colr = oT[:,0].reshape((3,1))
# # colc = oT[:,1].reshape((3,1))
# # coln = normal.reshape((3,1))
# # if len(self.shape) < 3:
# # M = np.hstack((colr[:2], colc[:2])).reshape((2,2))
# # else:
# # M = np.hstack((colr, colc, coln)).reshape((3,3))
# # return M
#
# def normalize(v):
# """Normalize a vector
#
# https://stackoverflow.com/questions/21030391/how-to-normalize-an-array-in-numpy
#
# :param v: 3D vector
# :return: normalized 3D vector
# """
# norm = np.linalg.norm(v, ord=1)
# if norm == 0:
# norm = np.finfo(v.dtype).eps
# return v / norm
#
# def L_from_orientation(orientation, normal, spacing):
# """
# orientation: row, then column index direction cosines
# """
# _ds, _dr, _dc = spacing
# _colr = normalize(np.array(orientation[3:6])).reshape((3,))
# _colc = normalize(np.array(orientation[0:3])).reshape((3,))
# _t0 = self.imagePositions[0][::-1].reshape(3, ) # x,y,z
# if self.slices > 1:
# _tn = self.imagePositions[self.slices - 1][::-1].reshape(3, ) # x,y,z
# # k = _tn
# _k = np.cross(_colr, _colc, axis=0)
# _k = _k * _ds
# else:
# _k = np.cross(_colr, _colc, axis=0)
# _k = _k * _ds
#
# _L = np.zeros((4, 4))
# _L[:3, 0] = _t0[:]
# _L[3, 0] = 1
# _L[:3, 1] = _k
# _L[3, 1] = 1 if self.slices > 1 else 0
# _L[:3, 2] = _colr * [-1, -1, 1] * _dr
# _L[:3, 3] = _colc * [-1, -1, 1] * _dc
# return _L
#
# # M = self.transformationMatrix
# # M = matrix_from_orientation(self.orientation, self.normal)
# # ipp = self.origin
# # q = np.array([[M[2,2], M[2,1], M[2,0], ipp[0]],
# # [M[1,2], M[1,1], M[1,0], ipp[1]],
# # [M[0,2], M[0,1], M[0,0], ipp[2]],
# # [ 0, 0, 0, 1 ]]
# # )
#
# if self.slices > 1:
# r = np.array([[1, 1, 1, 0], [1, 1, 0, 1], [1, self.slices, 0, 0], [1, 1, 0, 0]])
# else:
# r = np.array([[1, 0, 1, 0], [1, 0, 0, 1], [1, self.slices, 0, 0], [1, 0, 0, 0]])
# l = L_from_orientation(self.orientation, self.normal, self.spacing)
#
# # Linv = np.linalg.inv(L)
# # Aspm = np.dot(r, np.linalg.inv(l))
# to_ones = np.eye(4)
# to_ones[:, 3] = 1
# # A = np.dot(Aspm, to_ones)
#
# ds, dr, dc = self.spacing
# colr = normalize(np.array(self.orientation[3:6])).reshape((3,))
# colc = normalize(np.array(self.orientation[0:3])).reshape((3,))
# coln = normalize(np.cross(colc, colr, axis=0))
# t_0 = self.imagePositions[0][::-1].reshape(3, ) # x,y,z
# if self.slices > 1:
# t_n = self.imagePositions[self.slices - 1][::-1].reshape((3,)) # x,y,z
# abcd = np.array([1, 1, self.slices, 1]).reshape((4,))
# one = np.ones((1,))
# efgh = np.concatenate((t_n, one))
# else:
# abcd = np.array([0, 0, 1, 0]).reshape((4,))
# # zero = np.zeros((1,))
# efgh = np.concatenate((n * ds, zeros))
#
# # From derivations/spm_dicom_orient.py
#
# # premultiplication matrix to go from 0 to 1 based indexing
# one_based = np.eye(4)
# one_based[:3, 3] = (1, 1, 1)
# # premult for swapping row and column indices
# row_col_swap = np.eye(4)
# row_col_swap[:, 0] = np.eye(4)[:, 1]
# row_col_swap[:, 1] = np.eye(4)[:, 0]
#
# # various worming matrices
# orient_pat = np.hstack([colr.reshape(3, 1), colc.reshape(3, 1)])
# orient_cross = coln
# pos_pat_0 = t_0
# if self.slices > 1:
# missing_r_col = (t_0 - t_n) / (1 - self.slices)
# pos_pat_N = t_n
# pixel_spacing = [dr, dc]
# NZ = self.slices
# slice_thickness = ds
#
# R3 = np.dot(orient_pat, np.diag(pixel_spacing))
# # R3 = orient_pat * np.diag(pixel_spacing)
# r = np.zeros((4, 2))
# r[:3, :] = R3
#
# # The following is specific to the SPM algorithm.
# x1 = np.ones(4)
# y1 = np.ones(4)
# y1[:3] = pos_pat_0
#
# to_inv = np.zeros((4, 4))
# to_inv[:, 0] = x1
# to_inv[:, 1] = abcd
# to_inv[0, 2] = 1
# to_inv[1, 3] = 1
# inv_lhs = np.zeros((4, 4))
# inv_lhs[:, 0] = y1
# inv_lhs[:, 1] = efgh
# inv_lhs[:, 2:] = r
#
# def spm_full_matrix(x2, y2):
# rhs = to_inv[:, :]
# rhs[:, 1] = x2
# lhs = inv_lhs[:, :]
# lhs[:, 1] = y2
# return np.dot(lhs, np.linalg.inv(rhs))
#
# if self.slices > 1:
# x2_ms = np.array([1, 1, NZ, 1])
# y2_ms = np.ones((4,))
# y2_ms[:3] = pos_pat_N
# A_ms = spm_full_matrix(x2_ms, y2_ms)
# A = A_ms
# else:
# orient = np.zeros((3, 3))
# orient[:3, :2] = orient_pat
# orient[:, 2] = orient_cross
# x2_ss = np.array([0, 0, 1, 0])
# y2_ss = np.zeros((4,))
# # y2_ss[:3] = orient * np.array([0, 0, slice_thickness])
# y2_ss[:3] = np.dot(orient, np.array([0, 0, slice_thickness]))
# A_ss = spm_full_matrix(x2_ss, y2_ss)
# A = A_ss
#
# A = np.dot(A, row_col_swap)
#
# multi_aff = np.eye(4)
# multi_aff[:3, :2] = R3
# trans_z_N = np.array([0, 0, self.slices - 1, 1])
# multi_aff[:3, 2] = missing_r_col
# multi_aff[:3, 3] = pos_pat_0
# # est_pos_pat_N = np.dot(multi_aff, trans_z_N)
#
# # Flip voxels in y
# analyze_to_dicom = np.eye(4)
# analyze_to_dicom[1, 1] = -1
# # analyze_to_dicom[1,3] = shape[1]+1
# analyze_to_dicom[1, 3] = self.slices
# logger.debug("getQformFromTransformationMatrix: analyze_to_dicom\n{}".format(
# analyze_to_dicom))
# # dicom_to_analyze = np.linalg.inv(analyze_to_dicom)
# # q = np.dot(q,dicom_to_analyze)
# q = np.dot(A, analyze_to_dicom)
# # ## 2019.07.03 # q = np.dot(q,analyze_to_dicom)
# # ## 2019.07.03 # logger.debug("q after rows dicom_to_analyze:\n{}".format(q))
# # Flip mm coords in x and y directions
# patient_to_tal = np.diag([1, -1, -1, 1])
# # patient_to_tal = np.eye(4)
# # patient_to_tal[0,0] = -1
# # patient_to_tal[1,1] = -1
# # tal_to_patient = np.linalg.inv(patient_to_tal)
# # q = np.dot(tal_to_patient,q)
# logger.debug("getQformFromTransformationMatrix: patient_to_tal\n{}".format(
# patient_to_tal))
# q = np.dot(patient_to_tal, q)
# logger.debug("getQformFromTransformationMatrix: q after\n{}".format(q))
#
# return q
# def create_affine(self, sorted_dicoms):
# """
# Function to generate the affine matrix for a dicom series
# From dicom2nifti:common.py:
# https://github.com/icometrix/dicom2nifti/blob/master/dicom2nifti/common.py
# This method was based on (http://nipy.org/nibabel/dicom/dicom_orientation.html)
# :param sorted_dicoms: list with sorted dicom files
# """
#
# # Create affine matrix
# (http://nipy.sourceforge.net/nibabel/dicom/dicom_orientation.html#dicom-slice-affine)
# image_orient1 = np.array(sorted_dicoms[0].ImageOrientationPatient)[0:3]
# image_orient2 = np.array(sorted_dicoms[0].ImageOrientationPatient)[3:6]
#
# delta_r = float(sorted_dicoms[0].PixelSpacing[0])
# delta_c = float(sorted_dicoms[0].PixelSpacing[1])
#
# image_pos = np.array(sorted_dicoms[0].ImagePositionPatient)
#
# last_image_pos = np.array(sorted_dicoms[-1].ImagePositionPatient)
#
# if len(sorted_dicoms) == 1:
# # Single slice
# step = [0, 0, -1]
# else:
# step = (image_pos - last_image_pos) / (1 - len(sorted_dicoms))
#
# # check if this is actually a volume and not all slices on the same location
# if np.linalg.norm(step) == 0.0:
# raise NotImageError("Not a volume")
#
# affine = np.array(
# [[-image_orient1[0] * delta_c, -image_orient2[0] * delta_r, -step[0], -image_pos[0]],
# [-image_orient1[1] * delta_c, -image_orient2[1] * delta_r, -step[1], -image_pos[1]],
# [image_orient1[2] * delta_c, image_orient2[2] * delta_r, step[2], image_pos[2]],
# [0, 0, 0, 1]]
# )
# return affine, np.linalg.norm(step)
[docs] def write_3d_numpy(self, si, destination, opts):
"""Write 3D numpy image as Nifti file
Args:
self: NiftiPlugin instance
si: Series array (3D or 4D), including these attributes:
slices,
spacing,
imagePositions,
transformationMatrix,
orientation,
tags
destination: dict of archive and filenames
opts: Output options (dict)
"""
if si.color:
raise WriteNotImplemented(
"Writing color Nifti images not implemented.")
logger.debug('NiftiPlugin.write_3d_numpy: destination {}'.format(destination))
archive = destination['archive']
filename_template = 'Image.nii.gz'
if len(destination['files']) > 0 and len(destination['files'][0]) > 0:
filename_template = destination['files'][0]
# TODO # self._save_dicom_to_nifti(si)
self.shape = si.shape
self.slices = si.slices
self.spacing = si.spacing
self.transformationMatrix = si.transformationMatrix
self.imagePositions = si.imagePositions
self.tags = si.tags
self.origin, self.orientation, self.normal = si.get_transformation_components_xyz()
# slice_direction = _find_slice_direction(si, self.transformationMatrix, self.normal)
logger.info("Data shape write: {}".format(shape_to_str(si.shape)))
assert si.ndim == 2 or si.ndim == 3,\
"write_3d_series: input dimension %d is not 3D." % si.ndim
fsi = self._reorder_from_dicom(si, flip=False, flipud=True)
shape = fsi.shape
affine_xyz = self.create_affine_xyz()
nifti_header = nibabel.Nifti1Header()
nifti_header.set_dim_info(freq=0, phase=1, slice=2)
nifti_header.set_data_shape(shape)
dz, dy, dx = self.spacing
if si.ndim < 3:
nifti_header.set_zooms((dx, dy))
else:
nifti_header.set_zooms((dx, dy, dz))
nifti_header.set_data_dtype(fsi.dtype)
nifti_header.set_sform(affine_xyz, code=1)
nifti_header.set_xyzt_units(xyz='mm')
img = nibabel.Nifti1Image(fsi, None, nifti_header)
try:
filename = filename_template % 0
except TypeError:
filename = filename_template
self.write_numpy_nifti(img, archive, filename)
[docs] def write_4d_numpy(self, si, destination, opts):
"""Write 4D numpy image as Nifti file
Args:
self: NiftiPlugin instance
si[tag,slice,rows,columns]: Series array, including these attributes:
slices,
spacing,
imagePositions,
transformationMatrix,
orientation,
tags
destination: dict of archive and filenames
opts: Output options (dict)
"""
if si.color:
raise WriteNotImplemented(
"Writing color Nifti images not implemented.")
logger.debug('ITKPlugin.write_4d_numpy: destination {}'.format(destination))
archive = destination['archive']
filename_template = 'Image.nii.gz'
if len(destination['files']) > 0 and len(destination['files'][0]) > 0:
filename_template = destination['files'][0]
self.shape = si.shape
self.slices = si.slices
self.spacing = si.spacing
self.transformationMatrix = si.transformationMatrix
self.imagePositions = si.imagePositions
self.tags = si.tags
self.origin, self.orientation, self.normal = si.get_transformation_components_xyz()
# Defaults
self.output_sort = SORT_ON_SLICE
if 'output_sort' in opts:
self.output_sort = opts['output_sort']
# Should we allow to write 3D volume?
if si.ndim == 2:
si.shape = (1, 1,) + si.shape
elif si.ndim == 3:
si.shape = (1,) + si.shape
if si.ndim != 4:
raise ValueError("write_4d_numpy: input dimension {} is not 4D.".format(si.ndim))
logger.debug("write_4d_numpy: si dtype {}, shape {}, sort {}".format(
si.dtype, si.shape,
sort_on_to_str(self.output_sort)))
steps = si.shape[0]
slices = si.shape[1]
if steps != len(si.tags[0]):
raise ValueError(
"write_4d_series: tags of dicom template ({}) differ "
"from input array ({}).".format(len(si.tags[0]), steps))
if slices != si.slices:
raise ValueError(
"write_4d_series: slices of dicom template ({}) differ "
"from input array ({}).".format(si.slices, slices))
fsi = self._reorder_from_dicom(si, flip=False, flipud=True)
shape = fsi.shape
affine_xyz = self.create_affine_xyz()
nifti_header = nibabel.Nifti1Header()
nifti_header.set_dim_info(freq=0, phase=1, slice=2)
nifti_header.set_data_shape(shape)
dz, dy, dx = self.spacing
nifti_header.set_zooms((dx, dy, dz, 1))
nifti_header.set_data_dtype(fsi.dtype)
nifti_header.set_sform(affine_xyz, code=1)
# NiftiHeader.set_slice_duration()
# NiftiHeader.set_slice_times(times)
nifti_header.set_xyzt_units(xyz='mm', t='sec')
img = nibabel.Nifti1Image(fsi, None, nifti_header)
try:
filename = filename_template % 0
except TypeError:
filename = filename_template
self.write_numpy_nifti(img, archive, filename)
[docs] @staticmethod
def write_numpy_nifti(img, archive, filename):
"""Write nifti data to file
Args:
self: ITKPlugin instance, including these attributes:
- slices (not used)
- spacing
- imagePositions
- transformationMatrix
- orientation (not used)
- tags (not used)
img: Nifti1Image
archive: archive object
filename: file name, possibly without extentsion
"""
if len(os.path.splitext(filename)[1]) == 0:
filename = filename + '.nii.gz'
ext = os.path.splitext(filename)[1]
if filename.endswith('.nii.gz'):
ext = '.nii.gz'
logger.debug('write_numpy_nifti: ext %s' % ext)
f = tempfile.NamedTemporaryFile(
suffix=ext, delete=False)
logger.debug('write_numpy_nifti: write local file %s' % f.name)
img.to_filename(f.name)
f.close()
logger.debug('write_numpy_nifti: copy to file %s' % filename)
_ = archive.add_localfile(f.name, filename)
os.unlink(f.name)
def _save_dicom_to_nifti(self, si):
"""Convert DICOM to Nifti"""
hdr = nibabel.Nifti1Header()
img = si
if si.slices > 1:
hdr, slice_direction = self._header_dicom_to_nifti(hdr, si)
if slice_direction < 0:
hdr, img = self._nii_flip_z(hdr, si)
slice_direction = abs(slice_direction)
img = self._nii_set_ortho(hdr, img)
self._nii_save_attributes(si, hdr)
def _header_dicom_to_nifti(self, hdr, si):
# COL/ROW
inPlanePhaseEncodingDirection = si.getDicomAttribute('InPlanePhaseEncodingDirection')
if inPlanePhaseEncodingDirection == 'ROW':
hdr.set_dim_info(freq=1, phase=0, slice=2)
elif inPlanePhaseEncodingDirection == 'COL':
hdr.set_dim_info(freq=0, phase=1, slice=2)
slice_direction = 0
if si.slices < 2:
q44, slice_direction = self._nifti_dicom_mat(si)
hdr.set_sform(q44, code=NIFTI_XFORM_UNKNOWN)
hdr.set_qform(q44, code=NIFTI_XFORM_UNKNOWN)
else:
q44, slice_direction = self._nifti_dicom_mat(si)
hdr.set_sform(q44, NIFTI_XFORM_SCANNER_ANAT)
hdr.set_qform(q44, NIFTI_XFORM_SCANNER_ANAT)
return hdr, slice_direction
def _nifti_dicom_mat(self, si):
"""Create NIfTI header based on values from DICOM header"""
def normalize(v):
"""Normalize a vector
https://stackoverflow.com/questions/21030391/how-to-normalize-an-array-in-numpy
Args:
v: 3D vector
Returns:
normalized 3D vector
"""
norm = np.linalg.norm(v, ord=1)
if norm == 0:
norm = np.finfo(v.dtype).eps
return v / norm
origin, orientation, normal = si.get_transformation_components_xyz()
spacing = si.spacing[::-1] # x,y,z
q = np.zeros((3, 3))
q[0] = normalize(orientation[:3])
q[1] = normalize(orientation[3:])
q[2] = np.cross(q[0], q[1], axis=0)
q = np.transpose(q)
if np.linalg.det(q) < 0:
q[:2, 2] = - q[:2, 2]
diagVox = np.diag(spacing)
q = np.matmul(q, diagVox)
q44 = np.zeros((4, 4))
q44[:3, :3] = q
q44[:3, 3] = origin
q44[3, 3] = 1
slice_direction = self._find_slice_direction(si, q44, normal)
for c in range(4): # LPS to nifti RAS
for r in range(2): # Swap rows 0 and 1
q44[r, c] = - q44[r, c]
return q44, slice_direction
def _nii_flip_z(self, hdr, si):
"""Flip slice order"""
if si.slices < 2:
return si
# LOAD_MAT33(s,h->srow_x[0],h->srow_x[1],h->srow_x[2],
# h->srow_y[0],h->srow_y[1], h->srow_y[2],
# h->srow_z[0],h->srow_z[1],h->srow_z[2]);
sform = hdr.get_sform()[:3, :3]
# LOAD_MAT44(Q44,h->srow_x[0],h->srow_x[1],h->srow_x[2],h->srow_x[3],
# h->srow_y[0],h->srow_y[1],h->srow_y[2],h->srow_y[3],
# h->srow_z[0],h->srow_z[1],h->srow_z[2],h->srow_z[3]);
# q44 = np.eye(4)
# q44[:3, :3] = sform
q44 = hdr.get_sform()
# vec4 v= setVec4(0.0f,0.0f,(float) h->dim[3]-1.0f);
v = np.array([0, 0, si.slices - 1, 1], dtype=float)
# v = nifti_vect44mat44_mul(v, Q44); //after flip this voxel will be the origin
v = np.matmul(v, q44) # after flip this voxel will be the origin
# mat33 mFlipZ;
# LOAD_MAT33(mFlipZ,1.0f, 0.0f, 0.0f, 0.0f,1.0f,0.0f, 0.0f,0.0f,-1.0f);
mFlipZ = np.array([[1, 0, 0], [0, 1, 0], [0, 0, -1]], dtype=float)
# s= nifti_mat33_mul( s , mFlipZ );
sform = np.matmul(sform, mFlipZ)
# LOAD_MAT44(Q44, s.m[0][0],s.m[0][1],s.m[0][2],v.v[0],
# s.m[1][0],s.m[1][1],s.m[1][2],v.v[1],
# s.m[2][0],s.m[2][1],s.m[2][2],v.v[2]);
q44[:3, :3] = sform
q44[:, 3] = v
# setQSForm(h,Q44, true);
hdr.set_sform(q44, NIFTI_XFORM_SCANNER_ANAT)
hdr.set_qform(q44, NIFTI_XFORM_SCANNER_ANAT)
# printMessage("nii_flipImgY dims %dx%dx%d %d \n",h->dim[1],h->dim[2],
# dim3to7,h->bitpix/8);
# return self._nii_flip_image_z(hdr, si)
return hdr, self._reorder_from_dicom(si, flipud=True)
def _nii_set_ortho(self, hdr, img):
def isMat44Canonical(R):
# returns true if diagonals >0 and all others =0
# no rotation is necessary - already in perfect orthogonal alignment
for i in range(3):
for j in range(3):
if (i == j) and (R[i, j] <= 0):
return False
if (i != j) and (R[i, j] != 0):
return False
return True
def xyz2mm(R, v):
ret = np.zeros(3)
for i in range(3):
ret[i] = R[i, 0] * v[0] + R[i, 1] * v[1] + R[i, 2] * v[2] + R[i, 3]
return ret
def getDistance(v, _min):
# Scalar distance between two 3D points - Pythagorean theorem
return math.sqrt(math.pow((v[0] - _min[0]), 2) +
math.pow((v[1] - _min[1]), 2) +
math.pow((v[2] - _min[2]), 2))
def minCornerFlip(h):
# Orthogonal rotations and reflections applied as 3x3 matrices will cause the origin
# to shift. A simple solution is to first compute the most left, posterior, inferior
# voxel in the source image. This voxel will be at location i,j,k = 0,0,0, so we can
# simply use this as the offset for the final 4x4 matrix...
# vec3i flipVecs[8]
# vec3 corner[8], min
flipVecs = {}
corner = {}
# mat44 s = sFormMat(h);
s = h.get_sform()
for i in range(8):
flipVecs[i] = np.zeros(3)
flipVecs[i][0] = -1 if (i & 1) == 1 else 1
flipVecs[i][1] = -1 if (i & 2) == 1 else 1
flipVecs[i][2] = -1 if (i & 4) == 1 else 1
corner[i] = np.array([0., 0., 0.]) # assume no reflections
if (flipVecs[i][0]) < 1:
corner[i][0] = h.dim[1] - 1 # reflect X
if (flipVecs[i][1]) < 1:
corner[i][1] = h.dim[2] - 1 # reflect Y
if (flipVecs[i][2]) < 1:
corner[i][2] = h.dim[3] - 1 # reflect Z
corner[i] = xyz2mm(s, corner[i])
# find extreme edge from ALL corners....
_min = corner[0]
for i in range(8):
for j in range(3):
if corner[i][j] < _min[j]:
_min[j] = corner[i][j]
# dx: observed distance from corner
min_dx = getDistance(corner[0], _min)
min_index = 0 # index of corner closest to _min
# see if any corner is closer to absmin than the first one...
for i in range(8):
dx = getDistance(corner[i], _min)
if dx < min_dx:
min_dx = dx
min_index = i
# _min = corner[minIndex] # this is the single corner closest to _min from all
return corner[min_index], flipVecs[min_index]
def getOrthoResidual(orig, transform):
# mat33 mat = matDotMul33(orig, transform);
mat = orig @ transform
return np.sum(mat)
def getBestOrient(R, flipVec):
# flipVec reports flip: [1 1 1]=no flips, [-1 1 1] flip X dimension
# LOAD_MAT33(orig,R.m[0][0],R.m[0][1],R.m[0][2],
# R.m[1][0],R.m[1][1],R.m[1][2],
# R.m[2][0],R.m[2][1],R.m[2][2]);
ret = np.eye(3) * flipVec
orig = R[:3, :3]
best = 0.0
for rot in range(6): # 6 rotations
if rot == 0:
# LOAD_MAT33(newmat,flipVec.v[0],0,0, 0,flipVec.v[1],0, 0,0,flipVec.v[2])
newmat = np.eye(3) * flipVec
elif rot == 1:
# LOAD_MAT33(newmat,flipVec.v[0],0,0, 0,0,flipVec.v[1], 0,flipVec.v[2],0)
newmat = np.array([[flipVec[0], 0, 0], [0, 0, flipVec[1]], [0, flipVec[2], 0]])
elif rot == 2:
# LOAD_MAT33(newmat,0,flipVec.v[0],0, flipVec.v[1],0,0, 0,0,flipVec.v[2])
newmat = np.array([[0, flipVec[0], 0], [flipVec[1], 0, 0], [0, 0, flipVec[2]]])
elif rot == 3:
# LOAD_MAT33(newmat,0,flipVec.v[0],0, 0,0,flipVec.v[1], flipVec.v[2],0,0)
newmat = np.array([[0, flipVec[0], 0], [0, 0, flipVec[1]], [flipVec[2], 0, 0]])
elif rot == 4:
# LOAD_MAT33(newmat,0,0,flipVec.v[0], flipVec.v[1],0,0, 0,flipVec.v[2],0)
newmat = np.array([[0, 0, flipVec[0]], [flipVec[1], 0, 0], [0, flipVec[2], 0]])
elif rot == 5:
# LOAD_MAT33(newmat,0,0,flipVec.v[0], 0,flipVec.v[1],0, flipVec.v[2],0,0)
newmat = np.array([[0, 0, flipVec[0]], [0, flipVec[1], 0], [flipVec[2], 0, 0]])
newval = getOrthoResidual(orig, newmat)
if newval > best:
best = newval
ret = newmat
return ret
def setOrientVec(m):
# Assumes isOrthoMat NOT computed on INVERSE, hence return INVERSE of solution...
# e.g. [-1,2,3] means reflect x axis, [2,1,3] means swap x and y dimensions
ret = np.array([0, 0, 0])
for i in range(3):
for j in range(3):
if m[i, j] > 0:
ret[j] = i + 1
elif m[i, j] < 0:
ret[j] = - (i + 1)
return ret
def orthoOffsetArray(dim, stepBytesPerVox):
# return lookup table of length dim with values incremented by stepBytesPerVox
# e.g. if Dim=10 and stepBytes=2: 0,2,4..18, is stepBytes=-2 18,16,14...0
# size_t *lut= (size_t *)malloc(dim*sizeof(size_t));
lut = np.zeros(dim)
if stepBytesPerVox > 0:
lut[0] = 0
else:
lut[0] = -stepBytesPerVox * (dim - 1)
if dim > 1:
for i in range(1, dim):
lut[i] = lut[i - 1] + stepBytesPerVox
return lut
def reOrientImg(img, outDim, outInc, bytePerVox, nvol):
# Reslice data to new orientation
# Generate look up tables
xLUT = orthoOffsetArray(outDim[0], bytePerVox * outInc[0])
yLUT = orthoOffsetArray(outDim[1], bytePerVox * outInc[1])
zLUT = orthoOffsetArray(outDim[2], bytePerVox * outInc[2])
# Convert data
# number of voxels in spatial dimensions [1,2,3]
# bytePerVol = bytePerVox*outDim[0]*outDim[1]*outDim[2]
# o = 0 # output address
# inbuf = (uint8_t *) malloc(bytePerVol) # we convert 1 volume at a time
# outbuf = (uint8_t *) img # source image
for vol in range(nvol): # for each volume
# memcpy(&inbuf[0], &outbuf[vol*bytePerVol], bytePerVol) # copy source volume
inbuf = np.copy(img[vol])
for z in range(outDim[2]):
for y in range(outDim[1]):
for x in range(outDim[0]):
logger.error('Has not verified adressing')
# memcpy(&outbuf[o], &inbuf[xLUT[x]+yLUT[y]+zLUT[z]], bytePerVox)
img[vol, z, y, x] = inbuf[xLUT[x], yLUT[y], zLUT[z]]
# o += bytePerVox
def reOrient(img, h, orientVec, orient, minMM):
# e.g. [-1,2,3] means reflect x axis, [2,1,3] means swap x and y dimensions
nvox = img.columns * img.rows * img.slices
if nvox < 1:
return img
outDim = np.zeros(3)
outInc = np.zeros(3)
for i in range(3): # set dimensions, pixdim
outDim[i] = h.dim[abs(orientVec[i])]
if abs(orientVec[i]) == 1:
outInc[i] = 1
elif abs(orientVec[i]) == 2:
outInc[i] = h.dim[1]
elif abs(orientVec[i]) == 3:
outInc[i] = h.dim[1] * h.dim[2]
if orientVec[i] < 0:
outInc[i] = -outInc[i] # flip
nvol = 1 # convert all non-spatial volumes from source to destination
for vol in range(4, 8):
if h.dim[vol] > 1:
nvol = nvol * h.dim[vol]
reOrientImg(img, outDim, outInc, h.bitpix / 8, nvol)
# now change the header....
outPix = np.array([h.pixdim[abs(orientVec[0])],
h.pixdim[abs(orientVec[1])],
h.pixdim[abs(orientVec[2])]])
for i in range(3):
h.dim[i + 1] = outDim[i]
h.pixdim[i + 1] = outPix[i]
# mat44 s = sFormMat(h);
s = h.get_sform()
# mat33 mat; //computer transform
# LOAD_MAT33(mat, s.m[0][0],s.m[0][1],s.m[0][2],
# s.m[1][0],s.m[1][1],s.m[1][2],
# s.m[2][0],s.m[2][1],s.m[2][2]);
mat = s[:3, :3] # Computer transform
# mat = matMul33( mat, orient);
mat = mat @ orient
# s = setMat44Vec(mat, minMM); //add offset
s = np.eye(4)
s[:3, :3] = mat
s[:3, 3] = minMM # Add offset
# mat2sForm(h,s);
h.set_sform(s)
# h->qform_code = h->sform_code; //apply to the quaternion as well
_, sform_code = h.get_sform(coded=True)
# float dumdx, dumdy, dumdz;
# nifti_mat44_to_quatern(s, &h->quatern_b, &h->quatern_c, &h->quatern_d,
# &h->qoffset_x, &h->qoffset_y, &h->qoffset_z,
# &dumdx, &dumdy, &dumdz,&h->pixdim[0]) ;
h.set_qform(s, code=sform_code)
return img
# mat44 s = sFormMat(h);
s = hdr.get_sform()
h = hdr # TODO
if isMat44Canonical(s):
logger.debug("Image in perfect alignment: no need to reorient")
return img
# vec3i flipV;
flipV = np.zeros(3)
minMM, flipV = minCornerFlip(hdr)
orient = getBestOrient(s, flipV)
orientVec = setOrientVec(orient)
if orientVec[0] == 1 and orientVec[1] == 2 and orientVec[2] == 3:
logger.debug("Image already near best orthogonal alignment: no need to reorient")
return img
is24 = False
if h.bitpix == 24: # RGB stored as planar data. Treat as 3 8-bit slices
return img
is24 = True
h.bitpix = 8
h.dim[3] = h.dim[3] * 3
img = reOrient(img, h, orientVec, orient, minMM)
if is24:
h.bitpix = 24
h.dim[3] = h.dim[3] / 3
logger.debug("NewRotation= %d %d %d\n", orientVec.v[0], orientVec.v[1], orientVec.v[2])
logger.debug("MinCorner= %.2f %.2f %.2f\n", minMM.v[0], minMM.v[1], minMM.v[2])
return img
def _nii_save_attributes(self, si, hdr):
pass
def _find_slice_direction(self, si, affine, normal):
"""Return slice direction
Returns
None : unknown
1 : sag,
2 : cor
3 : axial
- : flipped
"""
if si.ndim < 3:
return None
slice_direction = 1
if abs(normal[1]) >= abs(normal[0]) and abs(normal[1]) >= abs(normal[2]):
slice_direction = 2
if abs(normal[2]) >= abs(normal[0]) and abs(normal[2]) >= abs(normal[1]):
slice_direction = 3
# pos = si.patientPosition(slice_direction)
pos = si.imagePositions[0][::-1][slice_direction - 1]
x = np.array([0, 0, si.ndim - 1, 1], dtype=float).reshape((1, 4))
# pos1v = nifti_vect44mat44_mul(x, affine)
pos1v = x @ affine
pos1 = pos1v[0, slice_direction - 1]
# Same direction? Note Python indices from 0
flip = (pos > affine[slice_direction - 1, 3]) != (pos1 > affine[slice_direction - 1, 3])
if flip:
slice_direction = - slice_direction
return slice_direction