TissueLens
Repository source: TissueLens
Description¶
Here a "magnifying lens" or "window" effect is generated, allowing users to peek inside a 3D medical dataset—such as a human head CT or MRI scan—by cutting a clean hole into the outer layer (the skin) to reveal the underlying structures (like the brain or bone) inside a specific region.
This example uses two vtkClipDataSet filters to achieve a "tissue lens" effect. First, a vtkSphere implicit function is used to clip a spherical hole in the isosurface extracted with vtkFlyingEdges3D or vtkMarchingCubes. Then a geometric vtkSphereSource samples the original volume data using a vtkProbeFilter. vtkClipDataSet uses the resulting scalar point data to clip the sphere surface with the isosurface value.
Usage
TissueLens FullHead.mhd
Note
The skin color was selected from Table 7 in Improvement of Haar Feature Based Face Detection in OpenCV Incorporating Human Skin Color Characteristic
Info
The example uses src/Testing/Data/FullHead.mhd which references src/Testing/Data/FullHead.raw.gz.
Question
If you have a question about this example, please use the VTK Discourse Forum
Code¶
TissueLens.py
#!/usr/bin/env python
import sys
from pathlib import Path
# noinspection PyUnresolvedReferences
import vtkmodules.vtkInteractionStyle
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkCommonCore import (
vtkLookupTable
)
from vtkmodules.vtkCommonDataModel import vtkSphere
from vtkmodules.vtkFiltersCore import (
vtkFlyingEdges3D,
vtkMarchingCubes,
vtkProbeFilter
)
from vtkmodules.vtkFiltersGeneral import vtkClipDataSet
from vtkmodules.vtkFiltersSources import vtkSphereSource
from vtkmodules.vtkIOImage import vtkMetaImageReader
from vtkmodules.vtkInteractionWidgets import vtkCameraOrientationWidget
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkCamera,
vtkDataSetMapper,
vtkProperty,
vtkRenderWindow,
vtkRenderWindowInteractor,
vtkRenderer
)
def main():
colors = vtkNamedColors()
file_name, use_flying_edges = get_program_parameters()
colors.SetColor('SkinColor', 240, 184, 160, 255)
colors.SetColor('BackfaceColor', 255, 229, 200, 255)
colors.SetColor('BkgColor', 51, 77, 102, 255)
# Create the renderer, the render window, and the interactor. The renderer
# draws into the render window, the interactor enables mouse- and
# keyboard-based interaction with the data within the render window.
#
ren = vtkRenderer()
ren_win = vtkRenderWindow()
ren_win.AddRenderer(ren)
# Set a background color for the renderer and set the name and
# size of the render window (expressed in pixels).
ren = vtkRenderer(background=colors.GetColor3d('BkgColor'))
ren_win = vtkRenderWindow(size=(640, 480),
window_name=f'{Path(sys.argv[0]).stem:s}')
ren_win.AddRenderer(ren)
iren = vtkRenderWindowInteractor()
iren.SetRenderWindow(ren_win)
# Since we import vtkmodules.vtkInteractionStyle we can do this
# because vtkInteractorStyleSwitch is automatically imported:
iren.GetInteractorStyle().SetCurrentStyleToTrackballCamera()
# Read the volume data
reader = vtkMetaImageReader(file_name=file_name)
reader.update()
# An isosurface, or contour value of 500 is known to correspond to the
# skin of the patient.
if use_flying_edges:
try:
skin_extractor = vtkFlyingEdges3D()
except AttributeError:
skin_extractor = vtkMarchingCubes()
else:
skin_extractor = vtkMarchingCubes()
skin_extractor.input_connection = reader.output_port
skin_extractor.SetValue(0, 500)
clip_center = (-70, 60, -10)
# Define a spherical clip function to clip the isosurface
clip_function = vtkSphere(radius=50, center=clip_center)
# Clip the isosurface with a sphere
skin_clip = vtkClipDataSet(input_connection=skin_extractor.output_port, clip_function=clip_function, value=0,
generate_clip_scalars=True)
skin_clip.update()
skin_mapper = vtkDataSetMapper(input_connection=skin_clip.output_port, scalar_visibility=False)
skin_prop = vtkProperty(diffuse_color=colors.GetColor3d('SkinColor'))
back_prop = vtkProperty(diffuse_color=colors.GetColor3d('BackfaceColor'))
skin = vtkActor(mapper=skin_mapper, property=skin_prop, backface_property=back_prop)
# Define a model for the "lens". Its geometry matches the implicit
# sphere used to clip the isosurface
lens_model = vtkSphereSource(radius=50, center=clip_center, phi_resolution=201, theta_resolution=101)
# Sample the input volume with the lens model geometry
lens_probe = vtkProbeFilter(input_connection=lens_model.output_port, source_connection=reader.output_port)
# Clip the lens data with the isosurface value
lens_clip = vtkClipDataSet(input_connection=lens_probe.output_port, value=500, generate_clip_scalars=False)
lens_clip.update()
# Define a suitable grayscale lut
bw_lut = vtkLookupTable()
bw_lut.SetTableRange(0, 2048)
bw_lut.SetSaturationRange(0, 0)
bw_lut.SetHueRange(0, 0)
bw_lut.SetValueRange(0.2, 1)
bw_lut.Build()
lens_mapper = vtkDataSetMapper(input_connection=lens_clip.output_port, scalar_range=lens_clip.output.scalar_range,
lookup_table=bw_lut)
lens = vtkActor(mapper=lens_mapper)
# lens.SetMapper(lens_mapper)
# It is convenient to create an initial view of the data. The FocalPoint
# and Position form a vector direction. Later on (ResetCamera() method)
# this vector is used to position the camera to look at the data in
# this direction.
camera = vtkCamera(view_up=(0, 0, 1), position=(0, -1, 0), focal_point=(0, 0, 0))
camera.ComputeViewPlaneNormal()
camera.Azimuth(30.0)
camera.Elevation(30.0)
# Actors are added to the renderer. An initial camera view is created.
# The Dolly() method moves the camera towards the FocalPoint,
# thereby enlarging the image.
ren.AddActor(lens)
ren.AddActor(skin)
ren.SetActiveCamera(camera)
ren.ResetCamera()
camera.Dolly(1.5)
# Note that when camera movement occurs (as it does in the Dolly()
# method), the clipping planes often need adjusting. Clipping planes
# consist of two planes: near and far along the view direction. The
# near plane clips out objects in front of the plane the far plane
# clips out objects behind the plane. This way only what is drawn
# between the planes is actually rendered.
ren.ResetCameraClippingRange()
ren_win.Render()
cow = make_camera_orientation_widget(ren)
cow.On()
category = 'lrpasi'
cow1 = make_camera_orientation_widget(ren, category, 1)
cow1.On()
# Initialize the event loop and then start it.
iren.Initialize()
iren.Start()
def get_program_parameters():
import argparse
description = ('Generate a "magnifying lens" or "window" effect, allowing'
' users to peek inside a 3D medical dataset')
epilogue = '''
'''
parser = argparse.ArgumentParser(description=description, epilog=epilogue,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('filename', help='FullHead.mhd.')
parser.add_argument('-m', '--marching_cubes', action='store_false',
help='Use Marching Cubes instead of Flying Edges.')
args = parser.parse_args()
return args.filename, args.marching_cubes
def set_axes_labels():
"""
Define the axes labels.
:return: The axes labels.
"""
return {
# Labels are: Anterior, Posterior, Dorsal, Ventral, Left, Right
'apdvlr': {'+X': 'A', '-X': 'P', '+Y': 'D', '-Y': 'V', '+Z': 'L', '-Z': 'R'},
'apdvrl': {'+X': 'A', '-X': 'P', '+Y': 'D', '-Y': 'V', '+Z': 'R', '-Z': 'L'},
'padvlr': {'+X': 'P', '-X': 'A', '+Y': 'D', '-Y': 'V', '+Z': 'L', '-Z': 'R'},
# Labels are: Left, Right, Superior, Inferior, Anterior, Posterior
'lrsiap': {'+X': 'L', '-X': 'R', '+Y': 'S', '-Y': 'I', '+Z': 'A', '-Z': 'P'},
'lrpasi': {'+X': 'L', '-X': 'R', '+Y': 'P', '-Y': 'A', '+Z': 'S', '-Z': 'I'},
'rlpais': {'+X': 'R', '-X': 'L', '+Y': 'P', '-Y': 'A', '+Z': 'I', '-Z': 'S'},
# Default labels
'xyz': None
}
def set_axes_colors():
"""
Define the axes colors.
:return: The axes colors.
"""
color1 = {'+X': 'IndianRed', '-X': 'FireBrick',
'+Y': 'LimeGreen', '-Y': 'DarkGreen',
'+Z': 'Blue', '-Z': 'SteelBlue'}
return {
'apdvlr': color1,
'apdvrl': color1,
'padvlr': color1,
'lrsiap': color1,
'lrpasi': None,
'rlpais': None,
# Default colors.
'xyz': None
}
def get_axes_params():
"""
Gather the defined axes labels and colors into a dictionary.
:return: The dictionary of axes labels and colors.
"""
# The keys must be the same.
axes_labels = set_axes_labels()
axes_colors = set_axes_colors()
label_keys = set(axes_labels.keys())
color_keys = set(axes_colors.keys())
common_keys = label_keys.intersection(color_keys)
alc = dict()
for k in common_keys:
alc[k] = (axes_labels[k], axes_colors[k])
return alc
def make_camera_orientation_widget(ren, alc_key='xyz', position=3): # , reposition):
"""
Make a camera orientation widget for a given renderer.
position has these values 0: LowerLeft, 1: UpperLeft, 2: LowerRight, 3: UpperRight
:param ren: The renderer.
:param alc_key: The key specifying the desired labels and colors for the axes.
:param position: Position the camera orientation widget.
:return: The camera orientation widget.
"""
cow = vtkCameraOrientationWidget(parent_renderer=ren, enabled=True)
rep = cow.representation
axes_parameters = get_axes_params()
if alc_key not in axes_parameters:
print(
f'Invalid key for axes labels and colors.'
f'\nValid keys are: {sorted(axes_parameters.keys())}'
f'\nUsing the key: xyz.')
alc_key = 'xyz'
alc = axes_parameters[alc_key]
match position:
case 0:
rep.AnchorToLowerLeft()
case 1:
rep.AnchorToUpperLeft()
case 2:
rep.AnchorToLowerRight()
case _:
rep.AnchorToUpperRight()
if not alc[0] is None:
rep.x_plus_label_text = alc[0]['+X']
rep.x_minus_label_text = alc[0]['-X']
rep.y_plus_label_text = alc[0]['+Y']
rep.y_minus_label_text = alc[0]['-Y']
rep.z_plus_label_text = alc[0]['+Z']
rep.z_minus_label_text = alc[0]['-Z']
if not alc[1] is None:
colors = vtkNamedColors()
rep.x_axis_color = colors.GetColor3d(alc[1]['+X'])
rep.y_axis_color = colors.GetColor3d(alc[1]['+Y'])
rep.z_axis_color = colors.GetColor3d(alc[1]['+Z'])
cow.SetRepresentation(rep)
cow.Off()
return cow
if __name__ == '__main__':
main()