dosma.MedicalVolume

class dosma.MedicalVolume(volume, affine, headers=None)[source]

The class for medical images.

Medical volumes use ndarrays to represent medical data. However, unlike standard ndarrays, these volumes have inherent spatial metadata, such as pixel/voxel spacing, global coordinates, rotation information, all of which can be characterized by an affine matrix following the RAS+ coordinate system. The code below creates a random 300x300x40 medical volume with scanner origin (0, 0, 0) and voxel spacing of (1,1,1):

>>> mv = MedicalVolume(np.random.rand(300, 300, 40), np.eye(4))

Medical volumes can also store header information that accompanies pixel data (e.g. DICOM headers). These headers are used to expose metadata, which can be fetched and set using get_metadata() and set_metadata(), respectively. Headers are also auto-aligned, which means that headers will be aligned with the slice(s) of data from which they originated, which makes Python slicing feasible. Currently, medical volumes support DICOM headers using pydicom when loaded with dosma.DicomReader.

>>> mv.get_metadata("EchoTime")  # Returns EchoTime
>>> mv.set_metadata("EchoTime", 10.0)  # Sets EchoTime to 10.0

Standard math and boolean operations are supported with other MedicalVolume objects, numpy arrays (following standard broadcasting), and scalars. Boolean operations are performed elementwise, resulting in a volume with shape as self.volume.shape. If performing operations between MedicalVolume objects, both objects must have the same shape and affine matrix (spacing, direction, and origin). Header information is not deep copied when performing these operations to reduce computational and memory overhead. The affine matrix (self.affine) is copied as it is lightweight and often modified.

2D images are also supported when viewed trivial 3D volumes with shape (H, W, 1):

>>> mv = MedicalVolume(np.random.rand(10,20,1), np.eye(4))

Many operations are in-place and modify the instance directly (e.g. reformat(inplace=True)). To allow chaining operations, operations that are in-place return self.

>>> mv2 = mv.reformat(ornt, inplace=True)
>>> id(mv2) == id(mv)
True

Medical volumes can interface with the gpu using the cupy library. Volumes can be moved between devices (see Device) using the .to() method. Only the volume data will be moved to the gpu. Headers and affine matrix will remain on the cpu. The following code moves a MedicalVolume to gpu 0 and back to the cpu:

>>> from dosma import Device
>>> mv = MedicalVolume(np.random.rand((10,20,30)), np.eye(4))
>>> mv_gpu = mv.to(Device(0))
>>> mv_cpu = mv.cpu()

Note, moving data across devices results in a full copy. Above, mv_cpu.volume and mv.volume do not share memory. Saving volumes and converting to other images (e.g. SimpleITK.Image) are only supported for cpu volumes. Volumes can also only be compared when on the same device. For example, both commands below will raise a RuntimeError:

>>> mv_gpu == mv_cpu
>>> mv_gpu.is_identical(mv_cpu)

While CuPy requires the current device be set using cp.cuda.Device(X).use() or inside the with context, MedicalVolume automatically sets the appropriate context for performing operations. This means the CuPy current device need to be the same as the MedicalVolume object. For example, the following still works:

>>> cp.cuda.Device(0).use()
>>> mv_gpu = MedicalVolume(cp.ones((3,3,3)), np.eye(4))
>>> cp.cuda.Device(1).use()
>>> mv_gpu *= 2

MedicalVolumes also have a limited NumPy/CuPy-compatible interface. Standard numpy/cupy functions that preserve array shapes can be performed on MedicalVolume objects:

>>> log_arr = np.log(mv)
>>> type(log_arr)
<class 'dosma.io.MedicalVolume'>
>>> exp_arr_gpu = cp.exp(mv_gpu)
>>> type(exp_arr_gpu)
<class 'dosma.io.MedicalVolume'>

ALPHA: MedicalVolumes are also interoperable with popular image data structures with zero-copy, meaning array data will not be copied. Formats currently include the SimpleITK Image, Nibabel Nifti1Image, and PyTorch tensors:

>>> sitk_img = mv.to_sitk()  # Convert to SimpleITK Image
>>> mv_from_sitk = MedicalVolume.from_sitk(sitk_img)  # Convert back to MedicalVolume
>>> nib_img = mv.to_nib()  # Convert to nibabel Nifti1Image
>>> mv_from_nib = MedicalVolume.from_nib(nib_img)
>>> torch_tensor = mv.to_torch()  # Convert to torch tensor
>>> mv_from_tensor = MedicalVolume.from_torch(torch_tensor, affine)

ALPHA: MedicalVolumes can also be used with memmapped arrays. This makes loading much faster and allows interaction with larger-than-memory arrays. Only when the volume is modified will the volume be loaded into memory and modified. If you take a slice of the memmaped array, the underlying array will also remain memmapped:

>>> arr = np.load("/path/to/volume.npy", mmap_mode="r")
>>> mv = MedicalVolume(arr, np.eye(4))
>>> mv.is_mmap  # returns True

We also preserve Nibabel’s memmapping of certain file types (e.g. .nii):

>>> nib_img = nibabel.load("path/to/volume.nii")
>>> mv = MedicalVolume.from_nib(nib_img, mmap=True)
Parameters:
  • volume (array-like) – nD medical image.

  • affine (array-like) – 4x4 array corresponding to affine matrix transform in RAS+ coordinates. Must be on cpu (i.e. no cupy.ndarray).

  • headers (array-like[pydicom.FileDataset]) – Headers for DICOM files.

__init__(volume, affine, headers=None)[source]

Methods

__init__(volume, affine[, headers])

astype(dtype, **kwargs)

Modifies dtype of self._volume.

clone([headers])

Clones the medical volume.

cpu()

Move to cpu.

from_nib(image[, affine_precision, ...])

Constructs MedicalVolume from nibabel images.

from_sitk(image[, copy, transpose_inplane])

Constructs MedicalVolume from SimpleITK.Image.

from_torch(tensor, affine[, headers, to_complex])

Zero-copy construction from PyTorch tensor.

get_metadata(key[, dtype, default])

Get metadata value from first header.

headers([flatten])

Returns headers.

is_identical(mv)

Check if another medical volume is identical.

is_same_dimensions(mv[, precision, err])

Check if two volumes have the same dimensions.

match_orientation(mv)

Reorient another MedicalVolume to orientation specified by self.orientation.

match_orientation_batch(mvs)

Reorient a collection of MedicalVolumes to orientation specified by self.orientation.

materialize()

mean([axis, dtype, out, keepdims, where])

Compute the arithmetic mean along the specified axis.

reformat(new_orientation[, inplace])

Reorients volume to a specified orientation.

reformat_as(other[, inplace])

Reformat this to the same orientation as other.

round([decimals, affine])

Round array (and optionally affine matrix).

save_volume(file_path[, data_format])

Write volumes in specified data format.

set_metadata(key, value[, force])

Sets metadata for all headers.

sum([axis, dtype, out, keepdims, initial, where])

Compute the arithmetic sum along the specified axis.

to(device)

Move to device.

to_nib()

Converts to nibabel Nifti1Image.

to_sitk([vdim, transpose_inplane])

Converts to SimpleITK Image.

to_torch([requires_grad, contiguous, ...])

Zero-copy conversion to torch tensor.

Attributes

A

The pixel array.

affine

4x4 affine matrix for volume in current orientation.

device

The device the object is on.

dtype

The dtype of the ndarray.

is_mmap

Whether the volume is a memory-mapped array.

ndim

The number of dimensions of the underlying ndarray.

orientation

Image orientation in standard orientation format.

pixel_spacing

Pixel spacing in order of current orientation.

scanner_origin

Scanner origin in global RAS+ x,y,z coordinates.

shape

The shape of the underlying ndarray.

volume

ndarray representing volume values.