HoloViews②(選択範囲の相互リンク)

選択範囲の相互リンク

HoloViewsでは、選択範囲のデータを相互リンクすることができます。

コールバック関数で定義する必要なく、グラフ間でデータ選択状態を自動的にリンク可能です。

実装手順は以下の通りです。

  1. link_selections.instanceメソッドを使用して link_selections インスタンス (以下のソースコードでは selection_linker) を作成する。(15行目)
  2. このオブジェクトをリンクする要素またはコンテナを含む関数として呼び出す。(16~21行目)

リンクされた要素が to_dash 関数に渡されると、Dash コールバックが自動的に生成されインタラクティブなリンク動作を実現できます。

また、to_dash関数のreset_buttonオプションTrueを設定することで、選択状態をリセットするためのボタンを表示することができます。(36行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding: utf-8 -*-
import dash
from dash import html
from plotly.data import iris

import holoviews as hv
from holoviews import opts
from holoviews.plotting.plotly.dash import to_dash

# Load dataset
df = iris()
dataset = hv.Dataset(df)

# Build selection linking object
selection_linker = hv.selection.link_selections.instance()
scatter = selection_linker(
hv.Scatter(dataset, kdims=["sepal_length"], vdims=["sepal_width"])
)
hist = selection_linker(
hv.operation.histogram(dataset, dimension="petal_width", normed=False)
)

# Use plot hook to set the default drag mode to box selection
def set_dragmode(plot, element):
fig = plot.state
fig['layout']['dragmode'] = "select"
if isinstance(element, hv.Histogram):
# Constrain histogram selection direction to horizontal
fig['layout']['selectdirection'] = "h"

scatter.opts(opts.Scatter(hooks=[set_dragmode]))
hist.opts(opts.Histogram(hooks=[set_dragmode]))

app = dash.Dash(__name__)
components = to_dash(
app, [scatter, hist], reset_button=True
)

app.layout = html.Div(components.children)

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

上図に散布図、下図に棒グラフが表示されました。

散布図で特定の範囲を選択すると、その選択範囲に応じたデータが折れ線グラフに反映されます。

折れ線グラフで特定の範囲を選択した場合も、その選択範囲に応じたデータが散布図に反映されます。

またresetボタンを押すことで、両グラフの選択状態をリセットできます。

HoloViews①(イントロダクション)

HoloViewsとは

HoloViewsは、さまざまなデータの視覚化や柔軟に相互作用するプロットを提供することを目的としたプロジェクトです。

HoloViewsでは、Plotly.jsDash などのさまざまなテクノロジーを使用してデータを可視化することができます。

また選択したデータを複数の図にまたがって自動的にリンクしたり、大規模なデータセットを表示したりする場合にとても便利です。

さまざまなデータ構造へのインターフェイスも提供するため、pandas DataFrameをはじめとして、RAPIDS cudf DataFramesMemory Dask DataFramesなどのデータを容易に扱うことができます。

HoloViewsのインストール

HoloViewsを使うためには、下記のコマンドでインストールを行います。

[コマンド]

1
pip install holoviews

次回からHoloViewsを使ったサンプルを実行していきます。

Dash VTK⑨(Clickコールバック)

Clickコールバック

Clickコールバックを使うと、3Dビューのクリックした位置の情報を取得することができます。

まずは下記のURLからcow-nonormals.objというファイルをダウンロードし、ソースコードと同じフォルダに格納します。

データセット - https://github.com/plotly/dash-vtk/tree/master/demos/data

次のソースコードでは、ダウンロードした牛のデータを読み込んで3Dビューで表示します。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import json

import dash
import dash_vtk
import dash_html_components as html
from dash.dependencies import Input, Output

with open("datasets/cow-nonormals.obj", 'r') as file:
txt_content = file.read()

view = dash_vtk.View(
id="click-info-view",
pickingModes=["click"],
children=[
dash_vtk.GeometryRepresentation(id="cow-geometry", children=[
dash_vtk.Reader(
vtkClass="vtkOBJReader",
parseAsText=txt_content,
),
]),
],
)

# Dash setup
app = dash.Dash(__name__)
server = app.server

app.layout = html.Div([
html.Div(view, style={"width": "100%", "height": "400px"}),
html.B("Output of clickInfo (try clicking on the object above):"),
html.Pre(
id="click-info-output",
style={'overflowX': 'scroll'}
)
])


@app.callback(
Output('click-info-output', 'children'),
Input('click-info-view', 'clickInfo')
)
def display_clicked_content(click_info):
return json.dumps(click_info, indent=2)

if __name__ == "__main__":
app.run_server(debug=True)

コールバック関数(38~43行目)では、クリックした牛の部位の情報をjson形式で表示しています。

[ブラウザで表示]

牛が3Dビューで表示され、特定の部位をクリックするとその位置の情報が表示されます。

またドラッグすることにより角度を変えて表示したり、スクロールで拡大・縮小したりすることも可能です。

Dash VTK⑧(Multi-View with slicing)

Multi-View with slicing

Multi-View with slicingを使うと、物体データを3面からスライスしたイメージを表示することができます。

頭部データとして下記のファイルをダウンロードし、ソースコードと同じフォルダに格納します。

頭部データ - https://github.com/plotly/dash-vtk/blob/master/demos/data/head.vti

下記のソースコードでは、人体の頭部を3面からスライスした3Dビューを表示します。18行目で、ダウンロードしたファイル(head.vti)の位置を指定しています。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import os
import random

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State

import dash_vtk
from dash_vtk.utils import to_volume_state

import vtk

random.seed(42)

# Data file path
head_vti = "head.vti"

# Load dataset from dist
reader = vtk.vtkXMLImageDataReader()
reader.SetFileName(head_vti)
reader.Update()

volume_state = to_volume_state(reader.GetOutput())

sliders = {
"Slice i": dcc.Slider(id="slider-i", min=0, max=256, value=128),
"Slice j": dcc.Slider(id="slider-j", min=0, max=256, value=128),
"Slice k": dcc.Slider(id="slider-k", min=0, max=95, value=47),
"Color Level": dcc.Slider(id="slider-lvl", min=0, max=4095, value=1000),
"Color Window": dcc.Slider(id="slider-window", min=0, max=4095, value=4095),
}

controls = dbc.Card(
body=True,
children=dbc.Row(
[
dbc.Col([dbc.Label(label), component], style={"width": "150px"})
for label, component in sliders.items()
]
),
)

slice_property = {"colorWindow": 4095, "colorLevel": 1000}

slice_view = dash_vtk.View(
id="slice-view",
cameraPosition=[1, 0, 0],
cameraViewUp=[0, 0, -1],
cameraParallelProjection=False,
background=[0.9, 0.9, 1],
children=[
dash_vtk.ShareDataSet(dash_vtk.Volume(state=volume_state)),
dash_vtk.SliceRepresentation(
id="slice-repr-i",
iSlice=128,
property=slice_property,
children=dash_vtk.ShareDataSet(),
),
dash_vtk.SliceRepresentation(
id="slice-repr-j",
jSlice=128,
property=slice_property,
children=dash_vtk.ShareDataSet(),
),
dash_vtk.SliceRepresentation(
id="slice-repr-k",
kSlice=47,
property=slice_property,
children=dash_vtk.ShareDataSet(),
),
],
)

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
server = app.server

app.layout = dbc.Container(
fluid=True,
style={"height": "calc(100vh - 30px)"},
children=[
html.Div(
style={"height": "20%", "display": "flex", "alignItems": "center"},
children=[
html.Br(),
controls,
html.Br(),
],
),
html.Div(slice_view, style={"height": "80%"}),
],
)

@app.callback(
[
Output("slice-view", "triggerRender"),
Output("slice-repr-i", "property"),
Output("slice-repr-i", "iSlice"),
Output("slice-repr-j", "property"),
Output("slice-repr-j", "jSlice"),
Output("slice-repr-k", "property"),
Output("slice-repr-k", "kSlice"),
],
[
Input("slider-i", "value"),
Input("slider-j", "value"),
Input("slider-k", "value"),
Input("slider-lvl", "value"),
Input("slider-window", "value"),
],
)
def update_slice_property(i, j, k, level, window):
render_call = random.random()
slice_prop = {"colorLevel": level, "colorWindow": window}
return render_call, slice_prop, i, slice_prop, j, slice_prop, k


if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

人体の頭部を3面からスライスした3Dビューが表示されました。

コントローラを使って各面のスライス位置色合いを変えたりすることができます。

またドラッグすることにより角度を変えて表示したり、スクロールで拡大・縮小したりすることも可能です。

Dash VTK⑦(Algorithmコンポーネント)

Algorithmコンポーネント

Algorithmコンポーネントを使うと、特定の物体を3Dで表示することができます。

今回はvtkClass“vtkConeSource”を指定し、三角コーンのような物体を表示します。(22行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import random

import dash
import dash_vtk
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State

random.seed(42)

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
server = app.server

vtk_view = dash_vtk.View(
id="geometry-view",
children=[
dash_vtk.GeometryRepresentation(
[
dash_vtk.Algorithm(
id="vtk-algorithm",
vtkClass="vtkConeSource",
state={"capping": False, "resolution": 60},
)
]
)
],
)

controls = dbc.Card(
[
dbc.CardHeader("Controls"),
dbc.CardBody(
[
html.P("Resolution:"),
dcc.Slider(
id="slider-resolution",
min=10,
max=100,
step=1,
value=60,
marks={10: "10", 100: "100"},
),
html.Br(),
dbc.Checklist(
options=[{"label": "Capping", "value": "capping"}],
value=[],
id="capping-checklist",
switch=True,
),
]
),
]
)

app.layout = dbc.Container(
fluid=True,
style={"marginTop": "15px", "height": "calc(100vh - 30px)"},
children=[
dbc.Row(
[
dbc.Col(width=4, children=controls),
dbc.Col(
width=8,
children=[
html.Div(
vtk_view,
style={"height": "100%", "width": "100%"},
)
],
),
],
style={"height": "100%"},
),
],
)

@app.callback(
[Output("vtk-algorithm", "state"), Output("geometry-view", "triggerResetCamera")],
[Input("slider-resolution", "value"), Input("capping-checklist", "value")],
)
def update_cone(slider_val, checked_values):
new_state = {"resolution": slider_val, "capping": "capping" in checked_values}
return new_state, random.random()

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

三角コーンのような物体を表示できました。

コントローラを使って解像度を変えたり底辺部分にフタをしたりと表示を変えられます。

またドラッグすることにより角度を変えて表示することも可能です。

Dash VTK⑥(VTK dynamic streamlines)

VTK dynamic streamlines

Dash VTKを使って、バイク走行中の風力イメージを表示してみます。

下記URLからbike.vtpファイルtunnel.vtuファイルをダウンロードし、ソースコードと同じフォルダに格納します。

データセット - https://github.com/plotly/dash-vtk/tree/master/demos/usage-vtk-cfd/data

下記のソースコードでは、PyVista/VTKを使用してデータセットを読み込み、Dash VTKを使用して 3D Web ビューで表示しています。

風力イメージを調整するためのコントローラも設定しています。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
import os
import random

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc

from dash.dependencies import Input, Output, State

import dash_vtk
from dash_vtk.utils import to_mesh_state, preset_as_options

import vtk

random.seed(42)

# -----------------------------------------------------------------------------
# VTK Pipeline
# -----------------------------------------------------------------------------
class Viz:
def __init__(self, data_directory):
self.color_range = [0, 1]
bike_filename = "bike.vtp"
tunnel_filename = "tunnel.vtu"

# Seeds settings
self.resolution = 10
self.point1 = [-0.4, 0, 0.05]
self.point2 = [-0.4, 0, 1.5]

# VTK Pipeline setup
bikeReader = vtk.vtkXMLPolyDataReader()
bikeReader.SetFileName(bike_filename)
bikeReader.Update()
self.bike_mesh = to_mesh_state(bikeReader.GetOutput())

tunnelReader = vtk.vtkXMLUnstructuredGridReader()
tunnelReader.SetFileName(tunnel_filename)
tunnelReader.Update()

self.lineSeed = vtk.vtkLineSource()
self.lineSeed.SetPoint1(*self.point1)
self.lineSeed.SetPoint2(*self.point2)
self.lineSeed.SetResolution(self.resolution)

streamTracer = vtk.vtkStreamTracer()
streamTracer.SetInputConnection(tunnelReader.GetOutputPort())
streamTracer.SetSourceConnection(self.lineSeed.GetOutputPort())
streamTracer.SetIntegrationDirectionToForward()
streamTracer.SetIntegratorTypeToRungeKutta45()
streamTracer.SetMaximumPropagation(3)
streamTracer.SetIntegrationStepUnit(2)
streamTracer.SetInitialIntegrationStep(0.2)
streamTracer.SetMinimumIntegrationStep(0.01)
streamTracer.SetMaximumIntegrationStep(0.5)
streamTracer.SetMaximumError(0.000001)
streamTracer.SetMaximumNumberOfSteps(2000)
streamTracer.SetTerminalSpeed(0.00000000001)

self.tubeFilter = vtk.vtkTubeFilter()
self.tubeFilter.SetInputConnection(streamTracer.GetOutputPort())
self.tubeFilter.SetRadius(0.01)
self.tubeFilter.SetNumberOfSides(6)
self.tubeFilter.CappingOn()
self.tubeFilter.Update()

def updateSeedPoints(self, p1_y, p2_y, resolution):
self.point1[1] = p1_y
self.point2[1] = p2_y
self.resolution = resolution

self.lineSeed.SetPoint1(*self.point1)
self.lineSeed.SetPoint2(*self.point2)
self.lineSeed.SetResolution(resolution)

def getTubesMesh(self, color_by_field_name):
self.tubeFilter.Update()
ds = self.tubeFilter.GetOutput()
mesh_state = to_mesh_state(ds, color_by_field_name)
self.color_range = mesh_state["field"]["dataRange"]
return mesh_state

def getBikeMesh(self):
return self.bike_mesh

def getColorRange(self):
return self.color_range

def getSeedState(self):
return {
"point1": self.point1,
"point2": self.point2,
"resolution": self.resolution,
}

# -----------------------------------------------------------------------------
# GUI setup
# -----------------------------------------------------------------------------
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
server = app.server
viz = Viz(os.path.join(os.path.dirname(__file__), "data"))

# -----------------------------------------------------------------------------
# 3D Viz
# -----------------------------------------------------------------------------
vtk_view = dash_vtk.View(
id="vtk-view",
children=[
dash_vtk.GeometryRepresentation(
id="bike-rep",
children=[
dash_vtk.Mesh(
id="bike",
state=viz.getBikeMesh(),
)
],
),
dash_vtk.GeometryRepresentation(
id="tubes-rep",
colorMapPreset="erdc_rainbow_bright",
colorDataRange=viz.getColorRange(),
children=[
dash_vtk.Mesh(
id="tubes-mesh",
state=viz.getTubesMesh("p"),
)
],
),
dash_vtk.GeometryRepresentation(
id="seed-rep",
property={
"color": [0.8, 0, 0],
"representation": 0,
"pointSize": 8,
},
children=[
dash_vtk.Algorithm(
id="seed-line",
vtkClass="vtkLineSource",
state=viz.getSeedState(),
)
],
),
],
)

# -----------------------------------------------------------------------------
# Control UI
# -----------------------------------------------------------------------------
high = 0.6
low = -0.55

controls = [
dbc.Card(
[
dbc.CardHeader("Seeds"),
dbc.CardBody(
[
html.P("Line seed position (from bottom):"),
dcc.Slider(
id="point-1",
min=low,
max=high,
step=0.05,
value=0,
marks={low: str(low), high: str(high)},
),
html.Br(),
html.P("Line seed position (from top):"),
dcc.Slider(
id="point-2",
min=low,
max=high,
step=0.05,
value=0,
marks={low: str(low), high: str(high)},
),
html.Br(),
html.P("Line resolution:"),
dcc.Slider(
id="seed-resolution",
min=5,
max=50,
step=1,
value=10,
marks={5: "5", 50: "50"},
),
]
),
]
),
html.Br(),
dbc.Card(
[
dbc.CardHeader("Color By"),
dbc.CardBody(
[
html.P("Field name"),
dcc.Dropdown(
id="color-by",
options=[
{"label": "p", "value": "p"},
{"label": "Rotation", "value": "Rotation"},
{"label": "U", "value": "U"},
{"label": "Vorticity", "value": "Vorticity"},
{"label": "k", "value": "k"},
],
value="p",
),
html.Br(),
html.P("Color Preset"),
dcc.Dropdown(
id="preset",
options=preset_as_options,
value="erdc_rainbow_bright",
),
]
),
]
),
]

# -----------------------------------------------------------------------------
# App UI
# -----------------------------------------------------------------------------
app.layout = dbc.Container(
fluid=True,
style={"marginTop": "15px", "height": "calc(100vh - 30px)"},
children=[
dbc.Row(
[
dbc.Col(width=4, children=controls),
dbc.Col(
width=8,
children=[
html.Div(vtk_view, style={"height": "100%", "width": "100%"})
],
),
],
style={"height": "100%"},
),
],
)

# -----------------------------------------------------------------------------
# Handle controls
# -----------------------------------------------------------------------------
@app.callback(
[
Output("seed-line", "state"),
Output("tubes-mesh", "state"),
Output("tubes-rep", "colorDataRange"),
Output("tubes-rep", "colorMapPreset"),
Output("vtk-view", "triggerRender"),
],
[
Input("point-1", "drag_value"),
Input("point-2", "drag_value"),
Input("point-1", "value"),
Input("point-2", "value"),
Input("seed-resolution", "value"),
Input("color-by", "value"),
Input("preset", "value"),
],
)
def update_seeds(y1_drag, y2_drag, y1, y2, resolution, colorByField, presetName):
triggered = dash.callback_context.triggered

if triggered and "drag_value" in triggered[0]["prop_id"]:
viz.updateSeedPoints(y1_drag, y2_drag, resolution)
return [
viz.getSeedState(),
dash.no_update,
dash.no_update,
dash.no_update,
random.random(), # trigger a render
]

viz.updateSeedPoints(y1, y2, resolution)
return [
viz.getSeedState(),
viz.getTubesMesh(colorByField),
viz.getColorRange(),
presetName,
random.random(), # trigger a render
]
# -----------------------------------------------------------------------------
if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

バイク走行中の風力イメージを表示できました。

各コントローラを使って風力イメージの表示を調整したり、ドラッグすることにより角度を変えて表示することが可能です。

Dash VTK⑤(Real medical image)

Real medical image

Dash VTKを使って、人体の頭部(医療データ)を表示してみます。

頭部データとして下記のファイルをダウンロードし、ソースコードと同じフォルダに格納します。

頭部データ - https://github.com/plotly/dash-vtk/blob/master/demos/data/head.vti

頭部を表示するソースコードは下記の通りです。11行目で、ダウンロードしたファイルを指定しています。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import os
import dash
import dash_html_components as html

import dash_vtk
from dash_vtk.utils import to_volume_state

import vtk

# Data file path
head_vti = "head.vti"

# Load dataset from dist
reader = vtk.vtkXMLImageDataReader()
reader.SetFileName(head_vti)
reader.Update()

volume_state = to_volume_state(reader.GetOutput())

vtk_view = dash_vtk.View(
dash_vtk.VolumeRepresentation(
children=[
dash_vtk.VolumeController(),
dash_vtk.Volume(state=volume_state),
]
)
)

app = dash.Dash(__name__)
server = app.server

app.layout = html.Div(
style={"height": "calc(100vh - 16px)", "width": "100%"},
children=[html.Div(vtk_view, style={"height": "100%", "width": "100%"})],
)

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

人体の頭部を表示することができました。

各コントローラを使って頭部の透過度を変えたり、ドラッグすることにより角度を変えて表示することが可能です。

Dash VTK④(地形追従メッシュ)

地形追従メッシュ

地形追従メッシュ(Terrain Following Mesh)を使うと、地形を3Dのメッシュで表示することができます。

地形追従メッシュは、水文学モデリングなどの環境科学でよく使われます。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# pip install dash_bootstrap_components

import dash
import dash_vtk
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State

import random
import json
import numpy as np
import pyvista as pv
from pyvista import examples
from vtk.util.numpy_support import vtk_to_numpy

from dash_vtk.utils import presets

random.seed(42)

def toDropOption(name):
return {"label": name, "value": name}

# Get point cloud data from PyVista
uniformGrid = examples.download_crater_topo()
subset = uniformGrid.extract_subset((500, 900, 400, 800, 0, 0), (5, 5, 1))

def updateWarp(factor=1):
terrain = subset.warp_by_scalar(factor=factor)
polydata = terrain.extract_geometry()
points = polydata.points.ravel()
polys = vtk_to_numpy(polydata.GetPolys().GetData())
elevation = polydata["scalar1of1"]
min_elevation = np.amin(elevation)
max_elevation = np.amax(elevation)
return [points, polys, elevation, [min_elevation, max_elevation]]

points, polys, elevation, color_range = updateWarp(1)

# Setup VTK rendering of PointCloud
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
server = app.server

vtk_view = dash_vtk.View(
id="vtk-view",
pickingModes=["hover"],
children=[
dash_vtk.GeometryRepresentation(
id="vtk-representation",
children=[
dash_vtk.PolyData(
id="vtk-polydata",
points=points,
polys=polys,
children=[
dash_vtk.PointData(
[
dash_vtk.DataArray(
id="vtk-array",
registration="setScalars",
name="elevation",
values=elevation,
)
]
)
],
)
],
colorMapPreset="erdc_blue2green_muted",
colorDataRange=color_range,
property={"edgeVisibility": True},
showCubeAxes=True,
cubeAxesStyle={"axisLabels": ["", "", "Altitude"]},
),
dash_vtk.GeometryRepresentation(
id="pick-rep",
actor={"visibility": False},
children=[
dash_vtk.Algorithm(
id="pick-sphere",
vtkClass="vtkSphereSource",
state={"radius": 100},
)
],
),
],
)

app.layout = dbc.Container(
fluid=True,
style={"height": "100vh"},
children=[
dbc.Row(
[
dbc.Col(
children=dcc.Slider(
id="scale-factor",
min=0.1,
max=5,
step=0.1,
value=1,
marks={0.1: "0.1", 5: "5"},
)
),
dbc.Col(
children=dcc.Dropdown(
id="dropdown-preset",
options=list(map(toDropOption, presets)),
value="erdc_rainbow_bright",
),
),
dbc.Col(
children=dcc.Checklist(
id="toggle-cube-axes",
options=[
{"label": " Show axis grid", "value": "grid"},
],
value=[],
labelStyle={"display": "inline-block"},
),
),
],
style={"height": "12%", "alignItems": "center"},
),
html.Div(
html.Div(vtk_view, style={"height": "100%", "width": "100%"}),
style={"height": "88%"},
),
html.Pre(
id="tooltip",
style={
"position": "absolute",
"bottom": "25px",
"left": "25px",
"zIndex": 1,
"color": "white",
},
),
],
)


@app.callback(
[
Output("vtk-representation", "showCubeAxes"),
Output("vtk-representation", "colorMapPreset"),
Output("vtk-representation", "colorDataRange"),
Output("vtk-polydata", "points"),
Output("vtk-polydata", "polys"),
Output("vtk-array", "values"),
Output("vtk-view", "triggerResetCamera"),
],
[
Input("dropdown-preset", "value"),
Input("scale-factor", "value"),
Input("toggle-cube-axes", "value"),
],
)
def updatePresetName(name, scale_factor, cubeAxes):
points, polys, elevation, color_range = updateWarp(scale_factor)
return [
"grid" in cubeAxes,
name,
color_range,
points,
polys,
elevation,
random.random(),
]

@app.callback(
[
Output("tooltip", "children"),
Output("pick-sphere", "state"),
Output("pick-rep", "actor"),
],
[
Input("vtk-view", "clickInfo"),
Input("vtk-view", "hoverInfo"),
],
)
def onInfo(clickData, hoverData):
info = hoverData if hoverData else clickData
if info:
if (
"representationId" in info
and info["representationId"] == "vtk-representation"
):
return (
[json.dumps(info, indent=2)],
{"center": info["worldPosition"]},
{"visibility": True},
)
return dash.no_update, dash.no_update, dash.no_update
return [""], {}, {"visibility": False}

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

地形を3Dのメッシュで表示することができました。

各コントローラを使って表示を変えたり、地形をドラッグすることにより角度を変えて表示することが可能です。

Dash VTK③(PyVista)

PyVista

PyVistaはVisualization Toolkit (VTK) 用のヘルパー・モジュールです。

VTKとのインターフェースを提供し,データセットの迅速なプロトタイピング分析,および視覚的統合を容易にします。

このモジュールは,プレゼンテーションや研究論文の科学的プロットにも使用できます。

PyVistaのインストール

PyVistaを使うためには、下記のコマンドでインストールを行います。

[コマンド]

1
pip install pyvista

サンプル実行

PyVistaを使ったサンプルを実行してみます。

PyVistaexpmanplesから lidarデータセット をダウンロードしています。(14行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import dash
import dash_vtk
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State

import numpy as np
import pyvista as pv
from pyvista import examples

np.random.seed(42)

# Get point cloud data from PyVista
dataset = examples.download_lidar()
subset = 0.2
selection = np.random.randint(
low=0, high=dataset.n_points - 1, size=int(dataset.n_points * subset)
)
points = dataset.points[selection]
xyz = points.ravel()
elevation = points[:, -1].ravel()
min_elevation = np.amin(elevation)
max_elevation = np.amax(elevation)
print(f"Number of points: {points.shape}")
print(f"Elevation range: [{min_elevation}, {max_elevation}]")

# Setup VTK rendering of PointCloud
app = dash.Dash(__name__)
server = app.server

vtk_view = dash_vtk.View(
[
dash_vtk.PointCloudRepresentation(
xyz=xyz,
scalars=elevation,
colorDataRange=[min_elevation, max_elevation],
property={"pointSize": 2},
)
]
)

app.layout = html.Div(
style={"height": "calc(100vh - 16px)"},
children=[html.Div(vtk_view, style={"height": "100%", "width": "100%"})],
)

if __name__ == "__main__":
app.run_server(debug=True)

起動に時間がかかるので気長にお待ちください。

[ブラウザで表示]

建物と木々のような風景がプロット(表示)され、ドラッグすることによりいろいろな角度から表示することができます。

Dash VTK②(Volume Rendering)

Volume Rendering

Volume Renderingを使うと、3次元表示をいろいろと変化させることができます。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import dash
import dash_html_components as html

import dash_vtk
from dash_vtk.utils import to_volume_state

try:
# VTK 9+
from vtkmodules.vtkImagingCore import vtkRTAnalyticSource
except ImportError:
# VTK =< 8
from vtk.vtkImagingCore import vtkRTAnalyticSource

# Use VTK to get some data
data_source = vtkRTAnalyticSource()
data_source.Update() # <= Execute source to produce an output
dataset = data_source.GetOutput()

# Use helper to get a volume structure that can be passed as-is to a Volume
volume_state = to_volume_state(dataset) # No need to select field

content = dash_vtk.View([
dash_vtk.VolumeRepresentation([
# GUI to control Volume Rendering
# + Setup good default at startup
dash_vtk.VolumeController(),
# Actual volume
dash_vtk.Volume(state=volume_state),
]),
])

# Dash setup
app = dash.Dash(__name__)
server = app.server

app.layout = html.Div(
style={"width": "100%", "height": "400px"},
children=[content],
)

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

画面左上にボリュームコントローラが表示され、このコントローラを操作することにより3次元表示をいろいろと変化させることができるようになりました。