Skip to content

Commit 1e71b93

Browse files
feat: discrete transfer functions
1 parent 35b4bd0 commit 1e71b93

File tree

4 files changed

+134
-7
lines changed

4 files changed

+134
-7
lines changed

ipyvolume/pylab.py

+55-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from __future__ import absolute_import
44
from __future__ import division
55
import pythreejs
6+
from typing import List, Union
7+
68

79
__all__ = [
810
'current',
@@ -29,6 +31,7 @@
2931
'animation_control',
3032
'gcc',
3133
'transfer_function',
34+
'transfer_function_discrete',
3235
'plot_isosurface',
3336
'volshow',
3437
'save',
@@ -894,6 +897,48 @@ def gcc():
894897
return current.container
895898

896899

900+
def transfer_function_discrete(
901+
n,
902+
colors: List[str] = ["red", "green", "blue"],
903+
labels: Union[None, List[str]] = None,
904+
opacity: Union[float, List[float]] = 0.1,
905+
enabled: Union[bool, List[bool]] = True,
906+
controls=True,
907+
):
908+
"""Create a discrete transfer function with n layers.
909+
910+
Each (integer) value of the volumetric data maps to a single color.
911+
912+
:param n: number of layers
913+
:param colors: list of colors, can be any valid HTML color string
914+
:param labels: list of labels, if None, labels will be "Layer 0", "Layer 1", etc.
915+
:param opacity: opacity of each layer, can be a single value or a list of values
916+
:param enabled: whether each layer is enabled, can be a single value or a list of values
917+
:param controls: whether to add the controls to the current container
918+
919+
"""
920+
if isinstance(opacity, float):
921+
opacity = [opacity] * len(colors)
922+
if isinstance(enabled, bool):
923+
enabled = [enabled] * len(colors)
924+
925+
def ensure_length(x):
926+
repeat = (n + len(colors) - 1) // len(colors)
927+
return (x * repeat)[:n]
928+
929+
if labels is None:
930+
labels = []
931+
for i in range(n):
932+
labels.append(f"Layer {i}")
933+
934+
tf = ipv.TransferFunctionDiscrete(colors=ensure_length(colors), opacities=ensure_length(opacity), enabled=ensure_length(enabled), labels=ensure_length(labels))
935+
gcf() # make sure a current container/figure exists
936+
if controls:
937+
current.container.children = [tf.control()] + current.container.children
938+
939+
return tf
940+
941+
897942
def transfer_function(
898943
level=[0.1, 0.5, 0.9], opacity=[0.01, 0.05, 0.1], level_width=0.1, controls=True, max_opacity=0.2
899944
):
@@ -1029,8 +1074,7 @@ def volshow(
10291074
):
10301075
"""Visualize a 3d array using volume rendering.
10311076
1032-
Currently only 1 volume can be rendered.
1033-
1077+
If the data is of type int8 or bool, :any:`a discrete transfer function will be used <ipv.discrete_transfer_function>`
10341078
10351079
:param data: 3d numpy array
10361080
:param origin: origin of the volume data, this is to match meshes which have a different origin
@@ -1040,7 +1084,7 @@ def volshow(
10401084
:param float data_max: maximum value to consider for data, if None, computed using np.nanmax
10411085
:parap int max_shape: maximum shape for the 3d cube, if larger, the data is reduced by skipping/slicing (data[::N]),
10421086
set to None to disable.
1043-
:param tf: transfer function (or a default one)
1087+
:param tf: transfer function (or a default one, based on the data)
10441088
:param bool stereo: stereo view for virtual reality (cardboard and similar VR head mount)
10451089
:param ambient_coefficient: lighting parameter
10461090
:param diffuse_coefficient: lighting parameter
@@ -1060,12 +1104,18 @@ def volshow(
10601104
"""
10611105
fig = gcf()
10621106

1063-
if tf is None:
1064-
tf = transfer_function(level, opacity, level_width, controls=controls, max_opacity=max_opacity)
10651107
if data_min is None:
10661108
data_min = np.nanmin(data)
10671109
if data_max is None:
10681110
data_max = np.nanmax(data)
1111+
if tf is None:
1112+
if (data.dtype == np.uint8) or (data.dtype == bool):
1113+
if data.dtype == bool:
1114+
data_max = 1
1115+
1116+
tf = transfer_function_discrete(n=data_max + 1)
1117+
else:
1118+
tf = transfer_function(level, opacity, level_width, controls=controls, max_opacity=max_opacity)
10691119
if memorder == 'F':
10701120
data = data.T
10711121

ipyvolume/test_all.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import pytest
1010

1111
import ipyvolume
12-
import ipyvolume.pylab as p3
1312
import ipyvolume as ipv
13+
import ipyvolume.pylab as p3
1414
import ipyvolume.examples
1515
import ipyvolume.datasets
1616
import ipyvolume.utils
@@ -303,6 +303,15 @@ def test_volshow():
303303
p3.save("tmp/ipyolume_volume.html")
304304

305305

306+
def test_volshow_discrete():
307+
boolean_volume = np.random.random((10, 10, 10)) > 0.5
308+
ipv.figure()
309+
vol = ipv.volshow(boolean_volume)
310+
assert isinstance(vol.tf, ipyvolume.TransferFunctionDiscrete)
311+
assert len(vol.tf.colors) == 2
312+
# int8_volume = np.random.randint(0, 255, size=(10, 10, 10), dtype=np.uint8)
313+
314+
306315
def test_volshow_max_shape():
307316
x, y, z = ipyvolume.examples.xyz(shape=32)
308317
Im = x * y * z

ipyvolume/transferfunction.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import absolute_import
44

5-
__all__ = ['TransferFunction', 'TransferFunctionJsBumps', 'TransferFunctionWidgetJs3', 'TransferFunctionWidget3']
5+
__all__ = ['TransferFunction', 'TransferFunctionDiscrete', 'TransferFunctionJsBumps', 'TransferFunctionWidgetJs3', 'TransferFunctionWidget3']
66

77
import numpy as np
88
import ipywidgets as widgets # we should not have widgets under two names
@@ -12,6 +12,7 @@
1212

1313
import ipyvolume._version
1414
from ipyvolume import serialize
15+
import ipyvuetify as v
1516

1617

1718
N = 1024
@@ -26,11 +27,29 @@ class TransferFunction(widgets.DOMWidget):
2627
_model_module = Unicode('ipyvolume').tag(sync=True)
2728
_view_module = Unicode('ipyvolume').tag(sync=True)
2829
style = Unicode("height: 32px; width: 100%;").tag(sync=True)
30+
# rgba should be a 2d array of shape (N, 4), where the last dimension is the rgba value
31+
# with values between 0 and 1
2932
rgba = Array(default_value=None, allow_none=True).tag(sync=True, **serialize.ndarray_serialization)
3033
_view_module_version = Unicode(semver_range_frontend).tag(sync=True)
3134
_model_module_version = Unicode(semver_range_frontend).tag(sync=True)
3235

3336

37+
class TransferFunctionDiscrete(TransferFunction):
38+
_model_name = Unicode('TransferFunctionDiscreteModel').tag(sync=True)
39+
colors = traitlets.List(traitlets.Unicode(), default_value=["red", "#0f0"]).tag(sync=True)
40+
opacities = traitlets.List(traitlets.CFloat(), default_value=[0.01, 0.01]).tag(sync=True)
41+
enabled = traitlets.List(traitlets.Bool(), default_value=[True, True]).tag(sync=True)
42+
labels = traitlets.List(traitlets.Unicode(), default_value=["label1", "label2"]).tag(sync=True)
43+
44+
def control(self):
45+
return TransferFunctionDiscreteView(tf=self)
46+
47+
48+
class TransferFunctionDiscreteView(v.VuetifyTemplate):
49+
template_file = (__file__, 'vue/tf_discrete.vue')
50+
tf = traitlets.Instance(TransferFunctionDiscrete).tag(sync=True, **widgets.widget_serialization)
51+
52+
3453
class TransferFunctionJsBumps(TransferFunction):
3554
_model_name = Unicode('TransferFunctionJsBumpsModel').tag(sync=True)
3655
_model_module = Unicode('ipyvolume').tag(sync=True)

js/src/tf.ts

+49
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as widgets from "@jupyter-widgets/base";
33
import {default as ndarray_pack} from "ndarray-pack";
44
import * as serialize from "./serialize.js";
55
import {semver_range} from "./utils";
6+
import _ from "underscore";
7+
import * as THREE from "three";
68

79
export
810
class TransferFunctionView extends widgets.DOMWidgetView {
@@ -112,6 +114,53 @@ class TransferFunctionJsBumpsModel extends TransferFunctionModel {
112114
}
113115
}
114116

117+
export
118+
class TransferFunctionDiscreteModel extends TransferFunctionModel {
119+
120+
constructor(...args) {
121+
super(...args);
122+
this.on("change:colors", this.recalculate_rgba, this);
123+
this.on("change:opacities", this.recalculate_rgba, this);
124+
this.on("change:enabled", this.recalculate_rgba, this);
125+
this.recalculate_rgba();
126+
}
127+
defaults() {
128+
return {
129+
...super.defaults(),
130+
_model_name : "TransferFunctionDiscreteModel",
131+
color: ["red", "#0f0"],
132+
opacities: [0.01, 0.01],
133+
enabled: [true, true],
134+
};
135+
}
136+
137+
recalculate_rgba() {
138+
const rgba = [];
139+
const colors = _.map(this.get("colors"), (color : string) => {
140+
return (new THREE.Color(color)).toArray();
141+
});
142+
const enabled = this.get("enabled");
143+
const opacities = this.get("opacities");
144+
(window as any).rgba = rgba;
145+
(window as any).tfjs = this;
146+
const N = colors.length;
147+
for (let i = 0; i < N; i++) {
148+
const color = [...colors[i], opacities[i]]; // red, green, blue and alpha
149+
color[3] = Math.min(1, color[3]); // clip alpha
150+
if(!enabled[i]) {
151+
color[3] = 0;
152+
}
153+
rgba.push(color);
154+
}
155+
// because we want the shader to sample the center pixel, if we add one extra pixel in the texture
156+
// all samples should be shiften by epsilon so the sample the center of the transfer function
157+
rgba.push([0, 0, 0, 0]);
158+
const rgba_array = ndarray_pack(rgba);
159+
this.set("rgba", rgba_array);
160+
this.save_changes();
161+
}
162+
}
163+
115164
export
116165
class TransferFunctionWidgetJs3Model extends TransferFunctionModel {
117166

0 commit comments

Comments
 (0)