Skip to content

Commit 8664ea5

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

File tree

5 files changed

+271
-7
lines changed

5 files changed

+271
-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)

ipyvolume/vue/tf_discrete.vue

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<template>
2+
<div class="ipyvolume-tf-root">
3+
<v-select v-model="enabled" :items="items" label="Layers" multiple chips clearable>
4+
</v-select>
5+
</div>
6+
</template>
7+
<style id="ipyvolume-tf">
8+
.ipyvolume-tf-root {
9+
display: flex;
10+
}
11+
12+
.ipyvolume-container-controls {
13+
display: flex;
14+
flex-direction: column;
15+
}
16+
</style>
17+
18+
<script>
19+
module.export = {
20+
data() {
21+
return {
22+
models: {
23+
tf: {
24+
}
25+
},
26+
}
27+
},
28+
computed: {
29+
items: {
30+
get: function () {
31+
if (!this.models.tf.colors) {
32+
return [];
33+
}
34+
return this.models.tf.colors.map((k, index) => {
35+
return { text: this.models.tf.labels[index], value: index }
36+
});
37+
}
38+
},
39+
enabled: {
40+
set: function (val) {
41+
const enabled = Array(this.models.tf.colors.length).fill(false);
42+
val.forEach((k) => {
43+
enabled[k] = true;
44+
})
45+
console.log('enabled set', val, enabled);
46+
this.models.tf.enabled = enabled;
47+
},
48+
get: function () {
49+
const enabled = [];
50+
if (!this.models.tf.enabled) {
51+
return enabled;
52+
}
53+
this.models.tf.enabled.forEach((k, index) => {
54+
if (k) {
55+
enabled.push(index);
56+
}
57+
});
58+
console.log('enabled get', enabled);
59+
return enabled;
60+
// console.log('enabled get', this.models.tf.enabled)
61+
// return this.models.tf.enabled;
62+
}
63+
}
64+
},
65+
created() {
66+
console.log('create tf', this.$refs)
67+
},
68+
69+
mounted() {
70+
const figureComponent = this.$refs.figure;
71+
(async () => {
72+
const tf = await this.viewCtx.getModelById(this.tf.substr(10));
73+
function bbproxy(model, attrs, widgetAttrs) {
74+
const proxy = {}
75+
76+
attrs.forEach((attr) => {
77+
console.log('tf setting', attr)
78+
let valueCopy = model.get(attr);
79+
model.on('change:' + attr, (_widget, value) => {
80+
proxy[attr] = value
81+
})
82+
Object.defineProperty(proxy, attr, {
83+
enumerable: true,
84+
configurable: true,
85+
get: () => {
86+
console.log('tf getting', attr, valueCopy);
87+
return valueCopy;
88+
},
89+
set: (value) => {
90+
console.log('tf setting', attr, value);
91+
valueCopy = value;
92+
model.set(attr, value);
93+
model.save_changes();
94+
},
95+
});
96+
})
97+
if (widgetAttrs) {
98+
Object.keys(widgetAttrs).forEach((attr) => {
99+
console.log('tf setting list', attr)
100+
let listValue = model.get(attr);
101+
let listValueProxy = [];
102+
if (listValue) {
103+
listValueProxy = listValue.map((k) => bbproxy(k, widgetAttrs[attr]));
104+
}
105+
proxy[attr] = listValueProxy;
106+
model.on('change:' + attr, (_widget, value) => {
107+
console.log('tf changed list', attr, value)
108+
if (value) {
109+
proxy[attr] = value.map((k) => bbproxy(k, widgetAttrs[attr]))
110+
} else {
111+
proxy[attr] = null;
112+
}
113+
});
114+
Object.defineProperty(proxy, attr, {
115+
enumerable: true,
116+
configurable: true,
117+
get: () => {
118+
return listValueProxy;
119+
},
120+
set: (value) => {
121+
listValueProxy = value;
122+
console.log('ignore propagating set')
123+
},
124+
});
125+
})
126+
}
127+
128+
return proxy;
129+
}
130+
this.$set(this.models, 'tf', bbproxy(tf, ['colors', 'enabled', 'labels']));
131+
})();
132+
},
133+
methods: {
134+
}
135+
}
136+
137+
</script>

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)