Skip to content

Library to help reconstructing sliced section images from volumetric medical scans

License

Notifications You must be signed in to change notification settings

bitdefender/dicom3d

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

About

dicom3d is a comprehensive Python package for reconstructing arbitrary defined 3D sections from volumetric medical scans (CTs or RMNs), scale accurate and with a builtin pixel-to-world coordinate mapping system.

It comes preloaded with the necessary mathematical backend to manipulate space information from medical scans, and provides a mapping system that can transparently handle different imaging properties such as pixel density, dataset thickness, patient orientation, patient positioning, etc.

Dependencies

It relies on pydicom for loading DICOM medical images and numpy for array manipulation.

Documentation

Documentation is available on dicom3d.readthedocs.org.

Installation

From PyPi package repository:

pip install dicom3d

Quick start

A quick way of understanding dicom3d is to explore its built-in examples.

To run them, execute the dicom3d.examples utility after installing the package:

$ python3 -m dicom3d.examples
[ 1] - dicomdir/loading.py
[ 2] - section/planar.py
[ 3] - section/basic.py
[ 4] - section/translation.py
[ 5] - section/make_gif.py
[ 6] - section/rotation.py
[ 7] - geometry/vectors.py
[ 8] - geometry/planes.py
[ 9] - geometry/basic.py
Select example [1-9]:

Some of the examples will require a volumetric scan to work with, stored in a folder with multiple .DCM files. You can download many free CT or RMN scans from canerimagingarchive.net

When you have them, navigate to the folder you downloaded them and run dicom3d.examples utility. The utility will look for those scans starting from the current working directory.

For example, to run the basic.py from dicom3d:

>$ cd dicom_data/test_files
>$ python3 -m dicom3d.examples section/basic.py

===-----------------------------------------------------===
 |                   BASIC SECTIONS                      |
===-----------------------------------------------------===

    This example shows how to create basic sections from
    a volumetric scan.

    It will create the sagittal, axial and coronal medical
    sections form the selected volumetric scan.

    The volumetric scan must a folder of .DCM files located
    in the current working directory.

    Each section image is plotted using matplotlib.

===-----------------------------------------------------===

Select a folder containing a list of '.dcm' files:
[ 1] - dicomdir_1
[ 2] - series_ct_lung_3
[ 3] - series_ct_lung_2
[ 4] - series_ct_heart_1
[ 5] - series_brain_1
[ 6] - series_ct_heart_2
[ 7] - series_ct_lung_1
[ 8] - series_brain_2
Select series [1-8]:

After selecting one series, the example will slice the volumetric scan and plot the resulted axial, sagittal and coronal sections.

Loading series from:  series_ct_lung_2
---
Origin: X:-9.65 Y:-158.65 Z:260.00
Axial    plane: 0.00X + 0.00Y + 1.00Z = 260.00 DPI: 17.86
Coronal  plane: 1.00X + 0.00Y + 0.00Z = -9.65
Sagittal plane: 0.00X + 1.00Y + 0.00Z = -158.65
---
Creating axial image..done.
Creating sagittal image..done.
Creating coronal image..done.
---
Plotting..

Sections Image

Basics

let's now see how the basic.py example is built, step by step.

Using dicom3d does not require a heavy prior-art in geometry. For defining sections, you need to work with planes, vectors, and three-dimensional coordinates, but dicom3d can help you build those relatively to existing geometry.

Defining a plane

First, let's define section planes:

axial    = d3d.Plane.from_axes("xy")
coronal  = d3d.Plane.from_axes("xz")
sagittal = d3d.Plane.from_axes("yz")

We start from the basic OXY, OXZ and OYZ planes. These are the planes parallel to the medical axial, coronal and sagittal section planes respectively. They describe the bottom, left and front planes of the volumetric scan. To become the medical planes their name suggests, we need to translate them to the center of the volumetric scan.

Simplest way to get the scan's center point is:

middle_dataset = series.middle()
origin = middle_dataset.center()

# this will not print a tuple as excepted, but
# it will print the string representation of
# dicom3d.geometry.Point object
print("Origin is: %s" % (origin))

This code gets the dataset in the middle of the scan's Z segment and then its corresponding geometric center in world coordinates.

And now, to make the translation:

# note: no operation on planes, vectors or points
# will alter the source object, rather it will create a copy 
axial    = axial.move(origin)
coronal  = coronal.move(origin)
sagittal = coronal.move(origin)

print("Axial: %s Coronal: %s Sagittal: %s" % (axial, coronal sagittal))

According to the DICOM standard, these are now the mathematical plane definitions for the corresponding medical planes axial, sagittal and coronal.

Constructing a section

The last step is to build the actual section. For this, we need a point to work as a center and a plane. The sliced section will be reconstructed around this point. For example, a section of 128x128 pixels will extend from (-64,-64) to (64,64) in the local coordinate system, with the point (0,0) mapped to the given origin, in world coordinates.

section_axial    = d3d.Section.from_plane(series, axial   , origin)
section_coronal  = d3d.Section.from_plane(series, coronal , origin)
section_sagittal = d3d.Section.from_plane(series, sagittal, origin)

Wrap-up

Putting it all together, we should end up with this:

from dicom3d.data import select_file
import dicom3d as d3d

def select_series_path():
    print("Select a folder containing a list of '.dcm' files:")
    return select_file(
		base_dir = os.path.abspath('.'), 
		pattern = "*",
		search_files = False,
		search_dirs = True,
		selection_name = "series" )

# retrieve a series sample, asuming your CT/RMN scans 
# are located in the current working directory
path   = select_series_path()
series = d3d.series.from_directory(path)

# get scan center
origin = series.middle().center()

# build medical sections
axial    = d3d.Plane.from_axes("xy").move(origin)
coronal  = d3d.Plane.from_axes("xz").move(origin)
sagittal = d3d.Plane.from_axes("yz").move(origin)

# construct sections
section_axial    = d3d.Section.from_plane(series, axial   , origin)
section_coronal  = d3d.Section.from_plane(series, coronal , origin)
section_sagittal = d3d.Section.from_plane(series, sagittal, origin)

# size of the resulting image will be 256 by 256 pixels
size = (256,256)
img_axial    = section_axial.image(size)
img_coronal  = section_coronal.image(size)
img_sagittal = section_sagittal.image(size)

That's it! We can now plot the numpy images using matplotlib.

Dimensions

Defining the size of a section in pixels has the disadvantage of not being "portable" across different DICOM datasets, if their PixelSpacing varies from one to another.

For example, a section with 100x100 pixels can map to 100x100 millimeters if the DICOM has the PixelSpacing attribute set to (1,1). The same size can translate to 50x50 millimeters if the PixelSpacing attribute is (0.5, 0.5), half the previous.

To handle this, dicom3d will calculate the distance in millimeters and convert it to pixels, when you want to construct section images.

To do that, you just need to pass floats instead of integers:

# this image will be 30 mm in width and 200 mm in height
size = (30.0, 200.0)
img_coronal = section_coronal.image(size)

Local Coordinate System

dicom3d attaches to each dataset, a dicom3d.LocalCoordinateSystem object for mapping pixel information, from the cartesian/local space (x,y), to world cooridnates measured in millimeters. Sections created via dicom3d have the same mapping feature, therefore to know where local point (-10,-10) lies in world coordinates, you could use:

# transform the given local point, in respect 
# to section's origin, to world coordinates
x,y,z = section_coronal.to_mm(-10,-10)

# we should get back the local point -10,-10
x,y = section_coronal.to_local((x,y,z))

Same for datasets:

# get a 'random' dataset
dataset = series.first()

# transfrom local to world and viceversa
x,y,z = dataset.to_mm(-10,-10)
x,y = dataset.to_local((x,y,z))

Rotation, translation and scaling

The dicom3d.LocalCoordinateSystem has built-in support for these operations, therefore to rotate the LCS around a given axis, you can write:

    # rotate the LCS around Z axis by 30 degrees
    section.transform = \
        section.transform.rotate("z", d3d.radians(30))

Or to translate it on a given axis:

    # move LCS on the Z axis by 100
    section.transform = section.transform.move(
        d3d.Vector.from_axis("z"), 100.0)

To modify its scaling factor:

    # the scaling factor is divided by 2
    section.transform = section.transform.scale(0.5)

Direct alteration of a Local Coordinate System is also possible, if needed. The only requirement is to call its update function after modifying internal parameters, to calculate the internal transformation matrix.

    # swap X and Y axis
    x_axis = section.transform.x_vector
    section.transform.x_vector = section.transform.y_vector
    section.transform_y_vector = x_axis
    section.transform.update() 

Planar datasets

If you are not interested in building sections for volumetric 3D scans and you work only with planar DICOM datasets, you can still use dicom3d for its mapping system of pixel to world coordinates or for creating translated, rotated or scaled planar sections.

For taking measurements using the cartesian mapping, you can use a pseudo-series of dicom3d datasets, as follows:

    import dicom3d as d3d
    import pydicom
    from pydicom.data import get_testdata_files

    def getMyDataset():
        files = get_testdata_files("MR_small_bigendian.dcm")
        print("Loading dataset from: ", files[0])
        return pydicom.dcmread(files[0])
        
    # assuming you have a function returning sa pydicom dataset
    dataset = getMyDataset()

    # construct a section from a single dataset
    planar_section = d3d.Section.from_dataset(dataset)

    # get wrapped dicom3d.Dataset class
    dataset = planar_section.series.first()
    origin = dataset.center()

    # navigate through dataset
    x,y,z = dataset.to_mm(0,0)
    print("Pixel at (0,0) is at X:%.2f Y:%.2f Z:%.2f" % (
        x,y,z))

    x,y,z = dataset.to_mm(dataset.Columns,dataset.Rows)
    print("Pixel at (%d,%d) is at X:%.2f Y:%.2f Z:%.2f" % (
        dataset.Columns, dataset.Rows, x,y,z))

And now you may get the corresponding numpy image of the section:

    # extract an image of 96x96 pixels containing dataset center
    width, height = 96, 96
    img = planar_section.image((width,height))

Now that we have a dicom3d.Section for the planar dataset that we can use it to produce an image, rotated, translated or scaled or to use its coordinating system to map pixels to world coordinates, like this:

    # top left and bottom right pixel coordinates
    topleft = planar_section.to_mm(-width/2, -height/2)
    btmright = planar_section.to_mm(width/2, height/2)

    # center coordinates
    center = planar_section.to_mm(0,0)

    # because datasets are parallel to OXY, we can ignore the Z coordinate
    xs,ys,_ = topleft
    xe,ye,_ = topright
    ox,oy,oz = center

    print("The %d x %d pixels section it's %.2fmm x %.2fmm in size" % (
        width, height,
        abs(xe-xs), abs(ye-ys)
    ))

    print("Center of section is at X:%.2f Y:%.2f Z:%.2f" % (
        ox, oy, oz
    ))

If you want to run this example, run:

python3 -m dicom3d.examples section/planar.py

And you will should see the following images plotted:

Sections Image

For the scaled sections:

Sections Image

Roadmap

Features we expect to develop in the near future for dicom3d:

  • performance improvement for section reconstruction
  • depth-information for sections
  • a set of predefined, configurable image filters for segmenting information after creating a section
  • overlaying information over reconstructed sections

License

Licence for this Python package is the MIT License under Copyright (c) 2020 Bitdefender.

Author

This library was developed by Alex Mircescu (amircescu@bitdefender.com) for an experimental machine learning application conducted by Florin Gogianu (fgogianu@bitdefender.com).

About

Library to help reconstructing sliced section images from volumetric medical scans

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages