DICOM Viewer in Python

Facebook
Twitter
LinkedIn

How to use VTK to create a simple and easy DICOM viewer

In the field of medical imaging, the name DICOM stands as a cornerstone; approximately 80% of the data utilized in this domain is in DICOM format. However, as you may be aware, the DICOM format differs significantly from traditional image formats such as PNG, JPG, and others. Managing this type of data requires specialized handling, necessitating its loading, writing, and visualization through unique methods.

Engaging with these files through code (Python, for instance) has become relatively straightforward, thanks to a wealth of libraries and resources designed to facilitate this process. Yet, the challenge escalates when we consider visualization. Here, the task is not merely to display images but to render multiple slices across three distinct views: axial, coronal, and sagittal. While employing specific software like ITK-Snap or 3D Slicer for these purposes is standard practice, complications arise when attempting to visualize these files via code. For those manipulating DICOMs in Python and seeking to visualize their files in real-time, adopting a technology capable of integrating such visualization directly within Python is essential.

Enter the library VTK, a true marvel within the realm of medical imaging. It’s hard to overstate the impact of VTK in our field; frankly, its contributions are indispensable. In this blog post, I aim to demonstrate how to visualize DICOM series in Python using a straightforward code snippet.

Requirements

For the requirements, you will need to have Python installed in your environment + you will need to install the vtk library.

pip install vtk

Code Explanation

The code for this task is structured into two main sections. The first section covers the control mechanisms of the viewer, such as scrolling, zooming in, and zooming out. The second section delves into loading the DICOM files and rendering them.

The viewer controller:

class CustomInteractorStyle(vtk.vtkInteractorStyleImage):
    def __init__(self, image_viewer, status_actor):
        super().__init__()
        self.AddObserver('MouseWheelForwardEvent', self.move_slice_forward)
        self.AddObserver('MouseWheelBackwardEvent', self.move_slice_backward)
        self.AddObserver('KeyPressEvent', self.key_press_event)
        self.image_viewer = image_viewer
        self.status_actor = status_actor
        self.slice = image_viewer.GetSliceMin()
        self.min_slice = image_viewer.GetSliceMin()
        self.max_slice = image_viewer.GetSliceMax()
        self.update_status_message()

    def update_status_message(self):
        # Update the status message with the current slice
        message = f'Slice Number {self.slice + 1}/{self.max_slice + 1}'
        self.status_actor.GetMapper().SetInput(message)

    def move_slice_forward(self, obj, event):
        if self.slice < self.max_slice:
            self.slice += 1
            self.image_viewer.SetSlice(self.slice)
            self.update_status_message()
            self.image_viewer.Render()

    def move_slice_backward(self, obj, event):
        if self.slice > self.min_slice:
            self.slice -= 1
            self.image_viewer.SetSlice(self.slice)
            self.update_status_message()
            self.image_viewer.Render()

    def key_press_event(self, obj, event):
        key = self.GetInteractor().GetKeySym()
        if key == 'Up':
            self.move_slice_forward(obj, event)
        elif key == 'Down':
            self.move_slice_backward(obj, event)

The DICOM renderer:

class DICOMViewer:
    def __init__(self, dicom_folder, view_orientation='axial'):
        '''
        The view orientation contols which orientation we want to display, here are the options:\n
        - axial
        - coronal
        - sagittal
        '''
        self.dicom_folder = dicom_folder
        self.view_orientation = view_orientation  # New attribute for view orientation
        self.colors = vtkNamedColors()
        self.setup_reader()
        self.setup_reslice()  # Call setup_reslice instead of setup_viewer directly
        self.setup_text_labels()
        self.configure_interactor()

    def setup_reader(self):
        self.reader = vtkDICOMImageReader()
        self.reader.SetDirectoryName(self.dicom_folder)
        self.reader.Update()

    def setup_reslice(self):
        self.reslice = vtkImageReslice()
        self.reslice.SetInputConnection(self.reader.GetOutputPort())

        # Set the output spacing to be 1, 1, 1. This is not strictly necessary but can be useful.
        self.reslice.SetOutputSpacing(1, 1, 1)

        if self.view_orientation == 'coronal':
            self.reslice.SetResliceAxesDirectionCosines(1,0,0, 0,0,1, 0,-1,0)
        elif self.view_orientation == 'sagittal':
            self.reslice.SetResliceAxesDirectionCosines(0,1,0, 0,0,1, 1,0,0)
        # Default is axial; no changes needed

        self.setup_viewer()

    def setup_viewer(self):
        self.image_viewer = vtkImageViewer2()
        self.image_viewer.SetInputConnection(self.reslice.GetOutputPort())
        # Rest of the setup_viewer method remains unchanged


    def setup_text_labels(self):
        self.slice_text_actor = self.create_text_actor("", 15, 10, 20, align_bottom=True)
        self.usage_text_actor = self.create_text_actor(
            "- Slice with mouse wheel or Up/Down-Key\n- Zoom with pressed right mouse button while dragging",
            0.05, 0.95, 14, normalized=True)

    def configure_interactor(self):
        self.interactor = vtkRenderWindowInteractor()
        self.interactor_style = CustomInteractorStyle(self.image_viewer, self.slice_text_actor)
        self.image_viewer.SetupInteractor(self.interactor)
        self.interactor.SetInteractorStyle(self.interactor_style)

    def create_text_actor(self, text, x, y, font_size, align_bottom=False, normalized=False):
        text_prop = vtkTextProperty()
        text_prop.SetFontFamilyToCourier()
        text_prop.SetFontSize(font_size)
        text_prop.SetVerticalJustificationToBottom() if align_bottom else text_prop.SetVerticalJustificationToTop()
        text_prop.SetJustificationToLeft()

        text_mapper = vtkTextMapper()
        text_mapper.SetInput(text)
        text_mapper.SetTextProperty(text_prop)

        text_actor = vtkActor2D()
        text_actor.SetMapper(text_mapper)
        if normalized:
            text_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay()
        text_actor.SetPosition(x, y)

        return text_actor

    def update_status_message(self, message):
        self.slice_text_actor.GetMapper().SetInput(message)

    def render(self):
        self.image_viewer.GetRenderer().AddActor2D(self.slice_text_actor)
        self.image_viewer.GetRenderer().AddActor2D(self.usage_text_actor)
        self.image_viewer.Render()
        self.image_viewer.GetRenderer().ResetCamera()
        self.image_viewer.GetRenderer().SetBackground(self.colors.GetColor3d('Black'))
        self.image_viewer.GetRenderWindow().SetSize(800, 800)
        self.image_viewer.GetRenderWindow().SetWindowName('ReadDICOMSeries')
        self.interactor.Start()

Run the viewer

To run the viewer, you will need a folder containing DICOM files. Then, provide the path to this folder to the DICOMViewer as follows:

dicom_folder_path = r'path/to/dicom/folder'

# Create an instance of DICOMViewer with the DICOM folder path
viewer = DICOMViewer(dicom_folder=dicom_folder_path, view_orientation='sagittal')

# Render the DICOM series
viewer.render()

You can specify the desired orientation by changing the view_orientation argument.

Where to find the code?

You can find the whole code in our GitHub repository at this link

More to explorer

Making Sense of AI in Medical Images

Explore how AI revolutionizes medical imaging, enhancing diagnosis and treatment. Dive into real-world AI applications for better healthcare outcomes.