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 here.