Skip to content

Commit bdc17bc

Browse files
committed
ENH: Add lut trait
1 parent 6f9c448 commit bdc17bc

File tree

4 files changed

+101
-4
lines changed

4 files changed

+101
-4
lines changed

itkwidgets/lut.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Some aliases similar to matplotlib.cm
2+
glasbey = "glasbey"
3+
glasbey_light = "glasbey_light"
4+
glasbey_warm = "glasbey_warm"
5+
modulate = "modulate"
6+
7+
glasbey_bw = "glasbey_bw"
8+
glasbey_dark = "glasbey_dark"
9+
glasbey_cool = "glasbey_cool"
10+
modulate_dark = "modulate_dark"

itkwidgets/trait_types.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,3 +680,38 @@ def validate(self, obj, value):
680680
'Custom'):
681681
raise self.error('Invalid colormap')
682682
return super(Colormap, self).validate(obj, value)
683+
684+
class LookupTable(traitlets.Unicode):
685+
"""A trait type holding a lookup table."""
686+
687+
info_text = 'A lookup table, either a itk-vtk-viewer categorical colormap preset, todo: np.ndarray of RGB points, or matplotlib ListedColormap.'
688+
689+
_lookup_table_presets = ('glasbey',
690+
'glasbey_light',
691+
'glasbey_warm',
692+
'modulate',
693+
'glasbey_bw',
694+
'glasbey_dark',
695+
'glasbey_cool',
696+
'modulate_dark',
697+
)
698+
699+
def validate(self, obj, value):
700+
if value is None:
701+
return None
702+
# elif isinstance(value, np.ndarray):
703+
# custom_cmap = value.astype(np.float32)
704+
# custom_cmap = custom_cmap[:, :3]
705+
# obj._custom_cmap = custom_cmap
706+
# timestamp = str(datetime.timestamp(datetime.now()))
707+
# return 'Custom NumPy ' + timestamp
708+
# elif isinstance(value, matplotlib.colors.ListedColormap):
709+
# custom_cmap = value(np.linspace(0.0, 1.0, 64)).astype(np.float32)
710+
# custom_cmap = custom_cmap[:, :3]
711+
# obj._custom_cmap = custom_cmap
712+
# timestamp = str(datetime.timestamp(datetime.now()))
713+
# return 'Custom matplotlib ' + timestamp
714+
if value not in self._lookup_table_presets and not value.startswith(
715+
'Custom'):
716+
raise self.error('Invalid lookup table')
717+
return super(LookupTable, self).validate(obj, value)

itkwidgets/widget_viewer.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
In the future, will add optional segmentation mesh overlay.
66
"""
77

8-
from . import cm
98
import colorcet
109
import matplotlib
1110
import collections
@@ -17,7 +16,7 @@
1716
import ipywidgets as widgets
1817
from traitlets import CBool, CFloat, CInt, Unicode, CaselessStrEnum, List, validate, TraitError, Tuple
1918
from ipydatawidgets import NDArray, array_serialization, shape_constraints
20-
from .trait_types import ITKImage, ImagePointTrait, ImagePoint, PointSetList, PolyDataList, itkimage_serialization, image_point_serialization, polydata_list_serialization, Colormap
19+
from .trait_types import ITKImage, ImagePointTrait, ImagePoint, PointSetList, PolyDataList, itkimage_serialization, image_point_serialization, polydata_list_serialization, Colormap, LookupTable
2120

2221
try:
2322
import ipywebrtc
@@ -167,6 +166,12 @@ class Viewer(ViewerParent):
167166
help="RGB triples from 0.0 to 1.0 that define a custom linear, sequential colormap")\
168167
.tag(sync=True, **array_serialization)\
169168
.valid(shape_constraints(None, 3))
169+
lut = LookupTable('glasbey',
170+
help='Lookup table for the label map.').tag(sync=True)
171+
_custom_cmap = NDArray(dtype=np.float32, default_value=None, allow_none=True,
172+
help="RGB triples from 0.0 to 1.0 that define a custom linear, sequential colormap")\
173+
.tag(sync=True, **array_serialization)\
174+
.valid(shape_constraints(None, 3))
170175
shadow = CBool(
171176
default_value=True,
172177
help="Use shadowing in the volume rendering.").tag(sync=True)
@@ -752,6 +757,7 @@ def view(image=None, # noqa: C901
752757
label_map_weights=None, # noqa: C901
753758
label_map_blend=0.5,
754759
cmap=None,
760+
lut='glasbey',
755761
select_roi=False,
756762
interpolation=True,
757763
gradient_opacity=0.22, opacity_gaussians=None, channels=None,
@@ -843,12 +849,20 @@ def view(image=None, # noqa: C901
843849
Value that maps to the minimum of image colormap. A single value can
844850
be provided or a list for multi-component images.
845851
846-
cmap: list of strings
852+
cmap: list of colormaps
847853
default:
848854
- single component: 'viridis', 'grayscale' with a label map,
849855
- two components: 'BkCy', 'BkMa'
850856
- three components: 'BkRd', 'BkGn', 'BkBu'
851-
Colormap for each image component. Some valid values available at itkwidgets.cm.*
857+
Colormap for each image component. Some valid values available at
858+
itkwidgets.cm.*
859+
Colormaps can also be Nx3 float NumPy arrays from 0.0 to 1.0 for the
860+
red, green, blue points on the map or a
861+
matplotlib.colors.LinearSegmentedColormap.
862+
863+
lut: lookup table, default: 'glasbey'
864+
Lookup table for the label map. Some valid values available at
865+
itkwidgets.lut.*
852866
853867
select_roi: bool, default: False
854868
Enable an interactive region of interest widget for the image.
@@ -1017,6 +1031,7 @@ def view(image=None, # noqa: C901
10171031
label_map_names=label_map_names,
10181032
label_map_weights=label_map_weights,
10191033
cmap=cmap,
1034+
lut=lut,
10201035
select_roi=select_roi,
10211036
interpolation=interpolation,
10221037
gradient_opacity=gradient_opacity, slicing_planes=slicing_planes,

js/lib/viewer.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ const ViewerView = widgets.DOMWidgetView.extend({
746746
this.label_map_names_changed()
747747
this.label_map_weights_changed()
748748
this.label_map_blend_changed()
749+
this.lut_changed()
749750
}
750751

751752
const onUserInterfaceCollapsedToggle = (collapsed) => {
@@ -796,6 +797,17 @@ const ViewerView = widgets.DOMWidgetView.extend({
796797
}
797798
this.model.itkVtkViewer.on('selectColorMap', onSelectColorMap)
798799

800+
const onSelectLookupTable = (lookupTable) => {
801+
let lut = this.model.get('lut')
802+
if (
803+
lut !== null && lookupTable !== lut
804+
) {
805+
this.model.set('lut', lookupTable)
806+
this.model.save_changes()
807+
}
808+
}
809+
this.model.itkVtkViewer.on('selectLookupTable', onSelectLookupTable)
810+
799811
const onChangeColorRange = (component, colorRange) => {
800812
const vmin = this.model.get('vmin')
801813
if (vmin === null) {
@@ -1076,6 +1088,7 @@ const ViewerView = widgets.DOMWidgetView.extend({
10761088
this
10771089
)
10781090
this.model.on('change:cmap', this.cmap_changed, this)
1091+
this.model.on('change:lut', this.lut_changed, this)
10791092
this.model.on('change:vmin', this.vmin_changed, this)
10801093
this.model.on('change:vmax', this.vmax_changed, this)
10811094
this.model.on('change:shadow', this.shadow_changed, this)
@@ -1519,6 +1532,30 @@ const ViewerView = widgets.DOMWidgetView.extend({
15191532
}
15201533
},
15211534

1535+
lut_changed: function () {
1536+
const lut = this.model.get('lut')
1537+
if (lut !== null && this.model.hasOwnProperty('itkVtkViewer')) {
1538+
//if (lut.startsWith('Custom')) {
1539+
// -> from cmap, to be updated for lookup table
1540+
//const lutProxies = this.model.itkVtkViewer.getLookupTableProxies()
1541+
//const lutProxy = lutProxies[index]
1542+
//const customCmap = this.model.get('_custom_cmap')
1543+
//const numPoints = customCmap.shape[0]
1544+
//const rgbPoints = new Array(numPoints)
1545+
//const cmapArray = customCmap.array
1546+
//const step = 1.0 / (numPoints - 1)
1547+
//let xx = 0.0
1548+
//for (let pointIndex = 0; pointIndex < numPoints; pointIndex++) {
1549+
//const rgb = cmapArray.slice(pointIndex * 3, (pointIndex + 1) * 3)
1550+
//rgbPoints[pointIndex] = [xx, rgb[0], rgb[1], rgb[2]]
1551+
//xx += step
1552+
//}
1553+
//lutProxy.setRGBPoints(rgbPoints)
1554+
//}
1555+
this.model.itkVtkViewer.setLookupTable(lut)
1556+
}
1557+
},
1558+
15221559
vmin_changed: function () {
15231560
const vmin = this.model.get('vmin')
15241561
if (vmin !== null && this.model.hasOwnProperty('itkVtkViewer')) {

0 commit comments

Comments
 (0)