# Overlaying Structures on CT Images in Axial, Coronal, and Sagittal Views

**URL:** https://discourse.itk.org/t/overlaying-structures-on-ct-images-in-axial-coronal-and-sagittal-views/7647
**Category:** Beginner Questions
**Tags:** dicom, simpleitk, python
**Created:** [October 21, 2025, 10:59am UTC](https://discourse.itk.org/t/overlaying-structures-on-ct-images-in-axial-coronal-and-sagittal-views/7647 "2025-10-21T10:59:46Z")
**Posts on this page:** 4
**Page:** 1

<div class="post-metadata">

### Author: ![blue\_sky](https://discourse.itk.org/letter_avatar_proxy/v4/letter/b/dbc845/32.png) [@blue\_sky](https://discourse.itk.org/u/blue_sky)
#### Post date: [October 21, 2025, 10:59am UTC](https://discourse.itk.org/t/overlaying-structures-on-ct-images-in-axial-coronal-and-sagittal-views/7647/1 "2025-10-21T10:59:47Z")

</div>

Hello everyone,

I am working on displaying CT images in the axial, coronal, and sagittal planes, along with dose maps and RTSTRUCT contours. While the CT and dose images are displayed correctly, I am having trouble overlaying the structures (contours) from the RTSTRUCT file on the CT images—the contours do not align properly with the CT, as shown in the attached image.

What steps should I take to ensure the contours are properly overlaid on the CT images in all three planes? Can you help me with this issue?

thanks a lot

my code:

```python
import pydicom
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from ipywidgets import interact, HBox, VBox, Output
import os
import SimpleITK as sitk
from rt_utils import RTStructBuilder
from scipy.ndimage import zoom
from skimage.measure import find_contours
import warnings
warnings.filterwarnings('ignore')

class MedicalImageViewer:
    def __init__ (self, ct_path, dose_paths=None, rtstruct_path=None):
        """
        Initialize the medical image viewer.
        """
        self.ct_path = ct_path
        self.dose_paths = dose_paths or {}
        self.rtstruct_path = rtstruct_path
        
        # Data storage
        self.ct_volume = None
        self.ct_metadata = None
        self.dose_volumes = {}
        self.contours = {}
        self.roi_names = []
        
        # Display parameters
        self.window_level = (40, 400)
        self.dose_colormap = plt.cm.jet
        self.dose_alpha = 0.4
        
        # Orientation
        self.patient_position = 'HFS'
        
        self.load_all_data()
        
    def load_ct(self):
        """Load CT volume."""
        print("Loading CT images...")
        reader = sitk.ImageSeriesReader()
        dicom_names = reader.GetGDCMSeriesFileNames(self.ct_path)
        if not dicom_names: raise ValueError("No DICOM files found in CT path")
        
        reader.SetFileNames(dicom_names)
        ct_image = reader.Execute()
        
        array = sitk.GetArrayFromImage(ct_image)
        self.ct_volume = np.transpose(array, (1, 2, 0))
        
        first_slice = pydicom.dcmread(dicom_names[0])
        self.patient_position = first_slice.PatientPosition if hasattr(first_slice, 'PatientPosition') else 'HFS'
        self.ct_metadata = {
            'pixel_spacing': first_slice.PixelSpacing if hasattr(first_slice, 'PixelSpacing') else [1.0, 1.0],
            'slice_thickness': float(first_slice.SliceThickness) if hasattr(first_slice, 'SliceThickness') else 1.0
        }
        print(f"CT loaded: {self.ct_volume.shape}")
        
    def detect_sagittal_flip(self):
        """Detect if sagittal flip is needed."""
        return 'FF' in self.patient_position.upper()
        
    def load_doses(self):
        """Load dose volumes."""
        for dose_name, dose_path in self.dose_paths.items():
            print(f"Loading {dose_name} dose...")
            ds = pydicom.dcmread(dose_path)
            dose_array = ds.pixel_array.astype(np.float32)
            dose_volume = dose_array * float(ds.DoseGridScaling) if hasattr(ds, 'DoseGridScaling') else dose_array
            dose_volume = np.transpose(dose_volume, (1, 2, 0))
            if dose_volume.shape != self.ct_volume.shape:
                zoom_factors = [self.ct_volume.shape[i] / dose_volume.shape[i] for i in range(3)]
                dose_volume = zoom(dose_volume, zoom_factors, order=1)
            self.dose_volumes[dose_name] = dose_volume
            print(f"{dose_name} dose loaded: {dose_volume.shape}")
        
    def load_rtstruct(self):
        """Load RTSTRUCT masks."""
        if not self.rtstruct_path or not os.path.exists(self.rtstruct_path): return
        print("Loading RTSTRUCT...")
        rtstruct = RTStructBuilder.create_from(dicom_series_path=self.ct_path, rt_struct_path=self.rtstruct_path)
        self.roi_names = rtstruct.get_roi_names()
        colors = plt.cm.rainbow(np.linspace(0, 1, len(self.roi_names)))
        for i, roi_name in enumerate(self.roi_names):
            mask = rtstruct.get_roi_mask_by_name(roi_name)
            mask = np.transpose(mask, (1, 0, 2))
            self.contours[roi_name] = {'mask': mask, 'color': colors[i]}
        print(f"Loaded {len(self.contours)} ROIs")
        
    def load_all_data(self):
        """Load all data."""
        self.load_ct()
        self.load_doses()
        self.load_rtstruct()
        
    def apply_window_level(self, image, window, level):
        """Apply window/level to CT."""
        img_min = level - window / 2
        img_max = level + window / 2
        windowed = np.clip(image, img_min, img_max)
        return (windowed - img_min) / (img_max - img_min)
    
    def create_interactive_viewer(self):
        """Create the fully interactive viewe."""
        ct_shape = self.ct_volume.shape
        pixel_spacing = self.ct_metadata['pixel_spacing']
        slice_thickness = self.ct_metadata['slice_thickness']
        
        axial_aspect = pixel_spacing[1] / pixel_spacing[0]
        sagittal_aspect = slice_thickness / pixel_spacing[1]
        coronal_aspect = slice_thickness / pixel_spacing[0]
        
        sagittal_flip = self.detect_sagittal_flip()
        
        output = Output()
        
        # --- Sliders
![ct and rt structure|690x217, 75%](upload://zJfDRspRrJhKPyexsKPl9qxqs06.png)
 ---
        axial_slider = widgets.IntSlider(min=0, max=ct_shape[2]-1, value=ct_shape[2]//2, description='Axial (Z):', continuous_update=False)
        coronal_slider = widgets.IntSlider(min=0, max=ct_shape[0]-1, value=ct_shape[0]//2, description='Coronal (Y):', continuous_update=False)
        sagittal_slider = widgets.IntSlider(min=0, max=ct_shape[1]-1, value=ct_shape[1]//2, description='Sagittal (X):', continuous_update=False)
        
        window_slider = widgets.IntSlider(min=1, max=2000, value=self.window_level[1], description='Window:')
        level_slider = widgets.IntSlider(min=-1000, max=1000, value=self.window_level[0], description='Level:')
        dose_selector = widgets.Dropdown(options=['None'] + list(self.dose_volumes.keys()), value='None', description='Dose:')
        dose_alpha_slider = widgets.FloatSlider(min=0, max=1, value=self.dose_alpha, step=0.1, description='Opacity:')
        roi_selector = widgets.SelectMultiple(options=self.roi_names, value=[], description='ROIs:', rows=5)
        
        
        def update_display(axial_idx, coronal_idx, sagittal_idx, window, level, dose_name, dose_alpha, selected_rois):
            with output:
                output.clear_output(wait=True)
                fig, axes = plt.subplots(1, 3, figsize=(18, 6))
                ax_axial, ax_coronal, ax_sagittal = axes # Correct plot order
                
                # --- Axial View ---
                axial_ct = np.flipud(self.ct_volume[:, :, axial_idx])
                axial_ct = self.apply_window_level(axial_ct, window, level)
                ax_axial.imshow(axial_ct, cmap='gray', aspect=axial_aspect, origin='lower')
                ax_axial.set_title(f'Axial (Z = {axial_idx})')
                
                # --- Coronal View ---
                coronal_ct = np.flipud(self.ct_volume[coronal_idx, :, :].T[::-1])
                coronal_ct = self.apply_window_level(coronal_ct, window, level)
                ax_coronal.imshow(coronal_ct, cmap='gray', aspect=coronal_aspect, origin='lower')
                ax_coronal.set_title(f'Coronal (Y = {coronal_idx})')
                
                # --- Sagittal View ---
                sagittal_ct = self.ct_volume[:, sagittal_idx, :].T
                if sagittal_flip: sagittal_ct = np.flipud(sagittal_ct)
                else: sagittal_ct = np.fliplr(sagittal_ct)
                sagittal_ct = self.apply_window_level(sagittal_ct, window, level)
                ax_sagittal.imshow(sagittal_ct, cmap='gray', aspect=sagittal_aspect, origin='lower')
                ax_sagittal.set_title(f'Sagittal (X = {sagittal_idx})')
                
                # --- Dose Overlay ---
                if dose_name != 'None' and dose_name in self.dose_volumes:
                    dose_vol = self.dose_volumes[dose_name]
                    max_dose = np.max(dose_vol)
                    
                    dose_axial = np.flipud(dose_vol[:, :, axial_idx])
                    im = ax_axial.imshow(np.ma.masked_equal(dose_axial, 0), cmap=self.dose_colormap, alpha=dose_alpha, vmin=0, vmax=max_dose, aspect=axial_aspect, origin='lower')
                    
                    dose_coronal = np.flipud(dose_vol[coronal_idx, :, :].T[::-1])
                    ax_coronal.imshow(np.ma.masked_equal(dose_coronal, 0), cmap=self.dose_colormap, alpha=dose_alpha, vmin=0, vmax=max_dose, aspect=coronal_aspect, origin='lower')
                    
                    dose_sagittal = dose_vol[:, sagittal_idx, :].T
                    if sagittal_flip: dose_sagittal = np.flipud(dose_sagittal)
                    else: dose_sagittal = np.fliplr(dose_sagittal)
                    ax_sagittal.imshow(np.ma.masked_equal(dose_sagittal, 0), cmap=self.dose_colormap, alpha=dose_alpha, vmin=0, vmax=max_dose, aspect=sagittal_aspect, origin='lower')
                    plt.colorbar(im, ax=axes, fraction=0.046, pad=0.04, label='Dose (GY)')
                
                # --- Structure Overlay ---
                for roi_name in selected_rois:
                    if roi_name in self.contours:
                        mask = self.contours[roi_name]['mask']
                        color = self.contours[roi_name]['color']
                        
                        # Axial Structure
                        axial_mask = np.flipud(mask[:, :, axial_idx])
                        contours_found = find_contours(axial_mask, 0.5)
                        for c in contours_found: ax_axial.plot(c[:, 1], c[:, 0], color=color, linewidth=1.5)
                        
                        # Coronal Structure
                        coronal_mask = np.flipud(mask[coronal_idx, :, :].T[::-1])
                        contours_found = find_contours(coronal_mask, 0.5)
                        for c in contours_found: ax_coronal.plot(c[:, 1], c[:, 0], color=color, linewidth=1.5)
                        
                        # Sagittal Structure
                        sagittal_mask = mask[:, sagittal_idx, :].T
                        if sagittal_flip: sagittal_mask = np.flipud(sagittal_mask)
                        else: sagittal_mask = np.fliplr(sagittal_mask)
                        contours_found = find_contours(sagittal_mask, 0.5)
                        for c in contours_found: ax_sagittal.plot(c[:, 1], c[:, 0], color=color, linewidth=1.5)

                for ax in axes: ax.axis('off')
                plt.tight_layout()
                plt.show()

        # --- Linking sliders to the correct function arguments ---
        interact(update_display, axial_idx=axial_slider, coronal_idx=coronal_slider, sagittal_idx=sagittal_slider,
                 window=window_slider, level=level_slider, dose_name=dose_selector, dose_alpha=dose_alpha_slider, selected_rois=roi_selector)
        display(output)

# directories
if __name__ == " __main__":
    ct_path = r'D:\TEST\CT'
    dose_paths = {
        'Physical': r'D:\TEST\RTDOSE\RTDOSE_Dose.dcm',
    
    }
    rtstruct_path = r'D:\TEST\RTSTRUCT\RTSTRUCT.dcm'
    
    viewer = MedicalImageViewer(ct_path=ct_path, dose_paths=dose_paths, rtstruct_path=rtstruct_path)
    viewer.create_interactive_viewer()

```

 ![ct and rt structure](https://discourse.itk.org/uploads/default/original/2X/f/fa699cfd69de4e227a6b41a5d509f890d3f54736.png)

---

<div class="post-metadata">

### Author: ![dzenanz](https://discourse.itk.org/user_avatar/discourse.itk.org/dzenanz/32/1093_2.png) [@dzenanz](https://discourse.itk.org/u/dzenanz)
#### Post date: [October 21, 2025, 1:08pm UTC](https://discourse.itk.org/t/overlaying-structures-on-ct-images-in-axial-coronal-and-sagittal-views/7647/2 "2025-10-21T13:08:21Z")

</div>

If you load these images in [3D Slicer](https://www.slicer.org/), do they line up correctly?

---

<div class="post-metadata">

### Author: ![blue\_sky](https://discourse.itk.org/letter_avatar_proxy/v4/letter/b/dbc845/32.png) [@blue\_sky](https://discourse.itk.org/u/blue_sky)
#### Post date: [October 21, 2025, 1:22pm UTC](https://discourse.itk.org/t/overlaying-structures-on-ct-images-in-axial-coronal-and-sagittal-views/7647/3 "2025-10-21T13:22:50Z")

</div>

Yes, these images are correctly displayed in 3D Slicer.

 ![3d slicer](https://discourse.itk.org/uploads/default/original/2X/5/57864d8c95ee9c02289fb6e61139bd128b345021.jpeg)

---

<div class="post-metadata">

### Author: ![dzenanz](https://discourse.itk.org/user_avatar/discourse.itk.org/dzenanz/32/1093_2.png) [@dzenanz](https://discourse.itk.org/u/dzenanz)
#### Post date: [October 21, 2025, 1:40pm UTC](https://discourse.itk.org/t/overlaying-structures-on-ct-images-in-axial-coronal-and-sagittal-views/7647/4 "2025-10-21T13:40:45Z")

</div>

`np.transpose(volume, (1, 2, 0))` - this looks so wrong! But you do it to all 3 of them, so that should not be the cause. As `matplotlib` does not respect direction matrix, I assume that RTStruct has it different than the other two? If you standardize the anatomical orientation of all 3 images using `OrientImageFilter` as per [this blog post](https://www.kitware.com/the-role-of-itk-in-the-era-of-deep-learning-in-medical-imaging/), does it solve the issue? You can choose whichever orientation you want, so you will not need ``np.transpose``.
