RescaleReverseLUT
Repository source: RescaleReverseLUT
Description¶
This example shows how to adjust a colormap so that the colormap scalar range matches the scalar range on the object. This is done by adjusting the colormap so that the colormap scalar range matches the scalar range of the object by rescaling the control points and, optionally, reversing the order of the colors.
Of course, if you are generating the scalars, it may be easier to just change the scalar range of your filter. However, this may not be possible in some cases.
Here, we generate the original Color Transfer Function (CTF) corresponding to the seven colors that Isaac Newton labeled when dividing the spectrum of visible light in 1672. There are seven colors and the scalar range is [-1, 1].
The cylinder has a vtkElevationFilter applied to it with a scalar range of [0, 1].
There are four images:
- Original - The cylinder is colored by only the top four colors from the CTF. This is because the elevation scalar range on the cylinder is [0, 1] and the CTF scalar range is [-1, 1]. So the coloring is green->violet.
- Reversed - We have reversed the colors from the original CTF and the lower four colors in the original CTF are now the top four colors used to color the cylinder. The coloring is now green->red.
- Rescaled - The original CTF is rescaled to the range [0, 1] to match the scalar range of the elevation filter. The coloring is red->violet.
- Rescaled and Reversed - The original CTF is rescaled to the range [0, 1] and the colors reversed. The coloring is violet->red.
Question
If you have a question about this example, please use the VTK Discourse Forum
Code¶
RescaleReverseLUT.py
#!/usr/bin/env python3
from dataclasses import dataclass
# noinspection PyUnresolvedReferences
import vtkmodules.vtkInteractionStyle
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingFreeType
# noinspection PyUnresolvedReferences
import vtkmodules.vtkRenderingOpenGL2
from vtkmodules.vtkCommonColor import vtkNamedColors
from vtkmodules.vtkFiltersCore import vtkElevationFilter
from vtkmodules.vtkFiltersSources import vtkCylinderSource
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera
from vtkmodules.vtkInteractionWidgets import (
vtkScalarBarRepresentation,
vtkScalarBarWidget,
vtkTextRepresentation,
vtkTextWidget
)
from vtkmodules.vtkRenderingAnnotation import vtkScalarBarActor
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkDiscretizableColorTransferFunction,
vtkPolyDataMapper,
vtkRenderWindow,
vtkRenderWindowInteractor,
vtkRenderer,
vtkTextActor,
vtkTextProperty
)
def main():
colors = vtkNamedColors()
colors.SetColor('ParaViewBkg', 82, 87, 110, 255)
ren_win = vtkRenderWindow(size=(640 * 2, 480 * 2), window_name='RescaleReverseLUT')
iren = vtkRenderWindowInteractor()
iren.render_window = ren_win
style = vtkInteractorStyleTrackballCamera()
iren.interactor_style = style
# Define titles.
titles = ['Original', 'Rescaled', 'Reversed', 'Rescaled and Reversed']
text_positions = get_text_positions(titles, justification=TextProperty.Justification.VTK_TEXT_CENTERED, width=0.95,
height=0.1)
# Create text properties.
text_property = vtkTextProperty(color=colors.GetColor3d('LightGoldenrodYellow'),
bold=False, italic=False, shadow=False,
font_size=24,
justification=TextProperty.Justification.VTK_TEXT_CENTERED,
vertical_justification=TextProperty.VerticalJustification.VTK_TEXT_CENTERED)
title_text_property = vtkTextProperty(color=colors.GetColor3d('LightGoldenrodYellow'),
bold=True, italic=False, shadow=False,
font_size=24, font_family_as_string='Courier',
justification=TextProperty.Justification.VTK_TEXT_CENTERED,
vertical_justification=TextProperty.VerticalJustification.VTK_TEXT_CENTERED)
label_text_property = vtkTextProperty(color=colors.GetColor3d('LightGoldenrodYellow'),
bold=False, italic=False, shadow=False,
font_size=24,
justification=TextProperty.Justification.VTK_TEXT_CENTERED,
vertical_justification=TextProperty.VerticalJustification.VTK_TEXT_CENTERED)
# Set up the scalar bar properties.
scalar_bar_properties = list()
for i in range(0, 4):
scalar_bar_properties.append(ScalarBarProperties())
# Define viewport ranges.
viewports = {'Original': (0.0, 0.5, 0.5, 1.0),
'Rescaled': (0.0, 0.0, 0.5, 0.5),
'Reversed': (0.5, 0.5, 1.0, 1.0),
'Rescaled and Reversed': (0.5, 0.0, 1.0, 0.5),
}
ctf = dict()
ctf['Original'] = get_ctf(False)
ctf['Rescaled'] = rescale_ctf(ctf['Original'], 0, 1, False)
ctf['Reversed'] = rescale_ctf(ctf['Original'], *ctf['Original'].GetRange(), True)
ctf['Rescaled and Reversed'] = rescale_ctf(ctf['Original'], 0, 1, True)
cylinder = vtkCylinderSource(center=(0.0, 0.0, 0.0), resolution=6)
bounds = cylinder.update().output.bounds
renderers = list()
text_widgets = list()
sb_widgets = list()
for title in titles:
elevation_filter = vtkElevationFilter(scalar_range=(0, 1), low_point=(0, bounds[2], 0),
high_point=(0, bounds[3], 0))
mapper = vtkPolyDataMapper(lookup_table=ctf[title], color_mode=Mapper.ColorMode.VTK_COLOR_MODE_MAP_SCALARS,
interpolate_scalars_before_mapping=True)
cylinder >> elevation_filter >> mapper
actor = vtkActor(mapper=mapper)
ren = vtkRenderer(viewport=viewports[title], background=colors.GetColor3d('ParaViewBkg'))
ren.AddActor(actor)
# Add a title.
text_actor = vtkTextActor(input=title,
text_scale_mode=vtkTextActor.TEXT_SCALE_MODE_VIEWPORT,
text_property=text_property)
# Create the text representation. Used for positioning the text actor.
text_representation = vtkTextRepresentation(enforce_normalized_viewport_bounds=True)
text_representation.position_coordinate.value = text_positions[title]['p']
text_representation.position2_coordinate.value = text_positions[title]['p2']
# Create the text widget, setting the default renderer and interactor.
text_widgets.append(
vtkTextWidget(representation=text_representation, text_actor=text_actor,
default_renderer=ren, interactor=iren, selectable=False))
# Add a scalar bar.
sb_properties = ScalarBarProperties()
sb_properties.lut = ctf[title]
sb_properties.orientation = True
sb_properties.number_of_labels = 7
sb_properties.position_v = {'point1': (0.75, 0.15), 'point2': (0.2, 0.75)}
# Create the scalar bar, setting the default renderer and interactor.
sb_widgets.append(make_scalar_bar_widget(sb_properties, title_text_property, label_text_property,
default_renderer=ren, interactor=iren))
renderers.append(ren)
for i in range(0, len(renderers)):
ren_win.AddRenderer(renderers[i])
text_widgets[i].On()
sb_widgets[i].On()
ren_win.Render()
iren.Start()
def get_ctf(modern=False):
"""
Generate the color transfer function.
The seven colors corresponding to the colors that Isaac Newton labelled
when dividing the spectrum of visible light in 1672 are used.
The modern variant of these colors can be selected and used instead.
See: [Rainbow](https://en.wikipedia.org/wiki/Rainbow)
:param modern: Selects either Newton's original seven colors or modern version.
:return: The color transfer function.
"""
# name: Rainbow, creator: Andrew Maclean
# interpolationspace: RGB, space: rgb
# file name:
ctf = vtkDiscretizableColorTransferFunction(color_space=ColorTransferFunction.ColorSpace.VTK_CTF_RGB,
scale=ColorTransferFunction.Scale.VTK_CTF_LINEAR,
nan_color=(0.5, 0.5, 0.5),
below_range_color=(0.0, 0.0, 0.0), use_below_range_color=True,
above_range_color=(1.0, 1.0, 1.0), use_above_range_color=True,
number_of_values=7, discretize=True)
if modern:
ctf.AddRGBPoint(-1.0, 1.0, 0.0, 0.0) # Red
ctf.AddRGBPoint(-2.0 / 3.0, 1.0, 128.0 / 255.0, 0.0) # Orange #ff8000
ctf.AddRGBPoint(-1.0 / 3.0, 1.0, 1.0, 0.0) # Yellow
ctf.AddRGBPoint(0.0, 0.0, 1.0, 0.0) # Green #00ff00
ctf.AddRGBPoint(1.0 / 3.0, 0.0, 1.0, 1.0) # Cyan
ctf.AddRGBPoint(2.0 / 3.0, 0.0, 0.0, 1.0) # Blue
ctf.AddRGBPoint(1.0, 128.0 / 255.0, 0.0, 1.0) # Violet #8000ff
else:
ctf.AddRGBPoint(-1.0, 1.0, 0.0, 0.0) # Red
ctf.AddRGBPoint(-2.0 / 3.0, 1.0, 165.0 / 255.0, 0.0) # Orange #00a500
ctf.AddRGBPoint(-1.0 / 3.0, 1.0, 1.0, 0.0) # Yellow
ctf.AddRGBPoint(0.0, 0.0, 125.0 / 255.0, 0.0) # Green #008000
ctf.AddRGBPoint(1.0 / 3.0, 0.0, 153.0 / 255.0, 1.0) # Blue #0099ff
ctf.AddRGBPoint(2.0 / 3.0, 68.0 / 255.0, 0, 153.0 / 255.0) # Indigo #4400ff
ctf.AddRGBPoint(1.0, 153.0 / 255.0, 0.0, 1.0) # Violet #9900ff
return ctf
def generate_new_ctf(old_ctf, new_x, new_rgb, reverse=False):
"""
Generate a new color transfer function from the old one,
adding in the new x and rgb values.
:param old_ctf: The old color transfer function.
:param new_x: The new color x-values.
:param new_rgb: The color RGB values.
:param reverse: If true, reverse the colors.
:return: The new color transfer function.
"""
new_ctf = vtkDiscretizableColorTransferFunction(color_space=old_ctf.color_space, scale=old_ctf.scale,
nan_color=old_ctf.nan_color,
number_of_values=len(new_x), discretize=True
)
if not reverse:
new_ctf.below_range_color = old_ctf.below_range_color
new_ctf.use_below_range_color = old_ctf.use_below_range_color
new_ctf.above_range_color = old_ctf.above_range_color
new_ctf.use_above_range_color = old_ctf.use_above_range_color
else:
new_ctf.below_range_color = old_ctf.above_range_color
new_ctf.use_below_range_color = old_ctf.use_above_range_color
new_ctf.above_range_color = old_ctf.below_range_color
new_ctf.use_above_range_color = old_ctf.use_below_range_color
if not reverse:
for i in range(0, len(new_x)):
new_ctf.AddRGBPoint(new_x[i], *new_rgb[i])
else:
sz = len(new_x)
for i in range(0, sz):
j = sz - (i + 1)
new_ctf.AddRGBPoint(new_x[i], *new_rgb[j])
new_ctf.Build()
return new_ctf
def rescale(values, new_min=0, new_max=1):
"""
Rescale the values.
See: https://stats.stackexchange.com/questions/25894/changing-the-scale-of-a-variable-to-0-100
:param values: The values to be rescaled.
:param new_min: The new minimum value.
:param new_max: The new maximum value.
:return: The rescaled values.
"""
res = list()
old_min, old_max = min(values), max(values)
for v in values:
new_v = (new_max - new_min) / (old_max - old_min) * (v - old_min) + new_min
# new_v1 = (new_max - new_min) / (old_max - old_min) * (v - old_max) + new_max
res.append(new_v)
return res
def rescale_ctf(ctf, new_min=0, new_max=1, reverse=False):
"""
Rescale and, optionally, reverse the colors in the color transfer function.
:param ctf: The color transfer function to rescale.
:param new_min: The new minimum value.
:param new_max: The new maximum value.
:param reverse: If true, reverse the colors.
:return: The rescaled color transfer function.
"""
if new_min > new_max:
r0 = new_max
r1 = new_min
else:
r0 = new_min
r1 = new_max
xv = list()
rgbv = list()
nv = [0] * 6
for i in range(0, ctf.GetNumberOfValues()):
ctf.GetNodeValue(i, nv)
x = nv[0]
rgb = nv[1:4]
xv.append(x)
rgbv.append(rgb)
xvr = rescale(xv, r0, r1)
return generate_new_ctf(ctf, xvr, rgbv, reverse=reverse)
def get_text_positions(names, justification=0, vertical_justification=0, width=0.96, height=0.1):
"""
Get viewport positioning information for a list of names.
:param names: The list of names.
:param justification: Horizontal justification of the text, default is left.
:param vertical_justification: Vertical justification of the text, default is bottom.
:param width: Width of the bounding_box of the text in screen coordinates.
:param height: Height of the bounding_box of the text in screen coordinates.
:return: A list of positioning information.
"""
# The gap between the left or right edge of the screen and the text.
dx = 0.02
width = abs(width)
if width > 0.96:
width = 0.96
y0 = 0.01
height = abs(height)
if height > 0.9:
height = 0.9
dy = height
if vertical_justification == TextProperty.VerticalJustification.VTK_TEXT_TOP:
y0 = 1.0 - (dy + y0)
dy = height
if vertical_justification == TextProperty.VerticalJustification.VTK_TEXT_CENTERED:
y0 = 0.5 - (dy / 2.0 + y0)
dy = height
name_len_min = 0
name_len_max = 0
first = True
for k in names:
sz = len(k)
if first:
name_len_min = name_len_max = sz
first = False
else:
name_len_min = min(name_len_min, sz)
name_len_max = max(name_len_max, sz)
text_positions = dict()
for k in names:
sz = len(k)
delta_sz = width * sz / name_len_max
if delta_sz > width:
delta_sz = width
if justification == TextProperty.Justification.VTK_TEXT_CENTERED:
x0 = 0.5 - delta_sz / 2.0
elif justification == TextProperty.Justification.VTK_TEXT_RIGHT:
x0 = 1.0 - dx - delta_sz
else:
# Default is left justification.
x0 = dx
# For debugging!
# print(
# f'{k:16s}: (x0, y0) = ({x0:3.2f}, {y0:3.2f}), (x1, y1) = ({x0 + delta_sz:3.2f}, {y0 + dy:3.2f})'
# f', width={delta_sz:3.2f}, height={dy:3.2f}')
text_positions[k] = {'p': [x0, y0, 0], 'p2': [delta_sz, dy, 0]}
return text_positions
class ScalarBarProperties:
"""
The properties needed for scalar bars.
"""
named_colors = vtkNamedColors()
lut = None
# These are in pixels
maximum_dimensions = {'width': 100, 'height': 260}
title_text = '',
number_of_labels: int = 5
# Orientation vertical=True, horizontal=False
orientation: bool = True
# Horizontal and vertical positioning
position_v = {'point1': (0.85, 0.1), 'point2': (0.1, 0.7)}
position_h = {'point1': (0.10, 0.1), 'point2': (0.7, 0.1)}
def make_scalar_bar_widget(scalar_bar_properties, title_text_property, label_text_property, default_renderer,
interactor):
"""
Make a scalar bar widget.
:param scalar_bar_properties: The lookup table, title name, maximum dimensions in pixels and position.
:param title_text_property: The properties for the title.
:param label_text_property: The properties for the labels.
:param default_renderer: The default renderer.
:param interactor: The vtkInteractor.
:return: The scalar bar widget.
"""
sb_actor = vtkScalarBarActor(lookup_table=scalar_bar_properties.lut, title=scalar_bar_properties.title_text,
unconstrained_font_size=True, number_of_labels=scalar_bar_properties.number_of_labels,
title_text_property=title_text_property, label_text_property=label_text_property
)
sb_rep = vtkScalarBarRepresentation(enforce_normalized_viewport_bounds=True,
orientation=scalar_bar_properties.orientation)
# Set the position.
sb_rep.position_coordinate.SetCoordinateSystemToNormalizedViewport()
sb_rep.position2_coordinate.SetCoordinateSystemToNormalizedViewport()
if scalar_bar_properties.orientation:
sb_rep.position_coordinate.value = scalar_bar_properties.position_v['point1']
sb_rep.position2_coordinate.value = scalar_bar_properties.position_v['point2']
else:
sb_rep.position_coordinate.value = scalar_bar_properties.position_h['point1']
sb_rep.position2_coordinate.value = scalar_bar_properties.position_h['point2']
widget = vtkScalarBarWidget(representation=sb_rep, scalar_bar_actor=sb_actor, default_renderer=default_renderer,
interactor=interactor, enabled=True)
return widget
@dataclass(frozen=True)
class ColorTransferFunction:
@dataclass(frozen=True)
class ColorSpace:
VTK_CTF_RGB: int = 0
VTK_CTF_HSV: int = 1
VTK_CTF_LAB: int = 2
VTK_CTF_DIVERGING: int = 3
VTK_CTF_LAB_CIEDE2000: int = 4
VTK_CTF_STEP: int = 5
@dataclass(frozen=True)
class Scale:
VTK_CTF_LINEAR: int = 0
VTK_CTF_LOG10: int = 1
@dataclass(frozen=True)
class Mapper:
@dataclass(frozen=True)
class ColorMode:
VTK_COLOR_MODE_DEFAULT: int = 0
VTK_COLOR_MODE_MAP_SCALARS: int = 1
VTK_COLOR_MODE_DIRECT_SCALARS: int = 2
@dataclass(frozen=True)
class ResolveCoincidentTopology:
VTK_RESOLVE_OFF: int = 0
VTK_RESOLVE_POLYGON_OFFSET: int = 1
VTK_RESOLVE_SHIFT_ZBUFFER: int = 2
@dataclass(frozen=True)
class ScalarMode:
VTK_SCALAR_MODE_DEFAULT: int = 0
VTK_SCALAR_MODE_USE_POINT_DATA: int = 1
VTK_SCALAR_MODE_USE_CELL_DATA: int = 2
VTK_SCALAR_MODE_USE_POINT_FIELD_DATA: int = 3
VTK_SCALAR_MODE_USE_CELL_FIELD_DATA: int = 4
VTK_SCALAR_MODE_USE_FIELD_DATA: int = 5
@dataclass(frozen=True)
class TextProperty:
@dataclass(frozen=True)
class Justification:
VTK_TEXT_LEFT: int = 0
VTK_TEXT_CENTERED: int = 1
VTK_TEXT_RIGHT: int = 2
@dataclass(frozen=True)
class VerticalJustification:
VTK_TEXT_BOTTOM: int = 0
VTK_TEXT_CENTERED: int = 1
VTK_TEXT_TOP: int = 2
if __name__ == '__main__':
main()