6. NumPy を使用してデータを処理する

5 章 では、NumPyを使って配列にアクセスし、配列に対する操作を実行することに大きく依存していたデータ処理用のPythonスクリプトを作成するためのいくつかのレシピについて説明しました。この章では、2つのシステム間でデータ表現に大きな違いがあるにもかかわらず、VTKとNumPyを一緒に使用できるようにするVTK-NumPy統合レイヤーについて詳しく説明します。

6.1. ティーザー

次の使用例は、出力ウィンドウに次のコードを表示します。

from paraview.vtk.numpy_interface import dataset_adapter as dsa
from paraview.vtk.numpy_interface import algorithms as algs

data = inputs[0]
print(data.PointData.keys())
print(data.PointData['Elevation'])

次の使用例は、出力ウィンドウに次のコードを表示します。

['Normals', 'Elevation']
[ 0.67235619  0.32764378  0.72819519  0.7388373   0.70217478  0.62546903
  0.52391261  0.41762003  0.75839448  0.79325461  0.77003199  0.69332629
  0.57832992  0.44781935  0.72819519  0.7388373   0.70217478  0.62546903
  0.52391261  0.41762003  0.65528756  0.60746235  0.53835285  0.46164712
  0.39253765  0.34471244  0.58238     0.47608739  0.37453097  0.29782525
  0.2611627   0.27180481  0.55218065  0.42167011  0.30667371  0.22996798
  0.20674542  0.24160551  0.58238     0.47608739  0.37453097  0.29782525
  0.2611627   0.27180481  0.65528756  0.60746235  0.53835285  0.46164712
  0.39253765  0.34471244]

重要なのは最後の3行です。特に、最後の2行の PointData 配列と Elevation 配列にアクセスするために、異なるAPIをどのように使用したかに注意してください。また、配列 Elevation を出力したときには、出力は vtkDataArray の出力とは異なっていました。実際のところは以下になります。

>>> elevation = data.PointData['Elevation']
>>> print(type(elevation))
<class paraview.vtk.numpy_interface.dataset_adapter.VTKArray>

>>> import numpy
>>> print(isinstance(elevation, numpy.ndarray))
True

VTKの配列がNumPyの配列?これはどんな魔法なんでしょうか?次のようになるのはどんな魔法なのでしょうか?

data.PointData.append(elevation + 1, 'e plus one')
print(algs.max(elevation))
print(algs.max(data.PointData['e plus one']))
print(data.VTKObject)

出力は次のようになります。:

0.7932546138763428
1.7932546138763428
vtkPolyData (0x7fa20d011c60)
...
Point Data:
...
Number Of Arrays: 3
Array 0 name = Normals
Array 1 name = Elevation
Array 2 name = e plus one

全ては numpy_interface モジュールにあります。VTKデータセットとデータ配列をNumPy配列に結び付け、これらのオブジェクトで動作する多数のアルゴリズムを導入しています。このモジュールにはかなりの部分がありますので、この章の残りの部分で少しずつ紹介します。

この節を最後のティーザーで締めくくりましょう。

print(algs.gradient(data.PointData['Elevation']))

Output:

[[ 0.32640398  0.32640398  0.01982867]
 [ 0.32640402  0.32640402  0.01982871]
 ...
 [ 0.41252578  0.20134845  0.2212007 ]
 [ 0.41105482  0.21514832  0.0782456 ]]

この例は、純粋なNumPyを使って簡単に再現できるものではないことに注意してください。 gradient関数は、非構造格子(NumPyには存在しない概念)の勾配を返します。しかし、NumPyの使いやすさはそこにあります。

6.2. dataset_adapter モジュールを理解する

本節では、 dataset_adapter モジュールについて詳しく説明します。このモジュールは、PythonからVTKデータセットおよびアレイに簡単にアクセスできるように設計されており、NumPyスタイルのインターフェイスを提供します。

前節の例に進みましょう。このスクリプトは、 Sphere に接続された Programmable FilterScript に置かれ、その後に Elevation フィルタパイプラインが続きます。

from vtk.numpy_interface import dataset_adapter as dsa
...
print(data)
print(isinstance(data, dsa.VTKObjectWrapper))

これは下記のように出力されます。:

<paraview.vtk.numpy_interface.dataset_adapter.PolyData object at 0x14b7caa50>
True

VTKObjectメンバを使用して、基礎となるVTKオブジェクトにアクセスできます。

print(type(data.VTKObject))

は下記のように出力されます。:

<type 'vtkCommonDataModelPython.vtkPolyData'>

Programmable Filter で$inputs$として取得されるものは、実際にはVTKデータ・オブジェクト自体をラップするPythonオブジェクトです。 Programmable Filter は、VTKデータオブジェクトの vtk.numpy_interface.dataset_adapter モジュールから WrapDataObject 関数を手動で呼び出すことでこれを行います。WrapDataObject 関数は、すべての vtkDataSet サブクラス、vtkTable サブクラス、およびすべての vtkCompositeData サブクラスの適切なラッパークラスを返します。 他の vtkDataObject サブクラスは現在サポートされていません。

VTKObjectWrapper は、VTKメソッドをVTKObjectに転送するので、以下のようにVTK APIに直接アクセスすることができます。

print(data.GetNumberOfCells())
96L

ただし、VTKObjectWrapper を引数としてVTKメソッドに直接渡すことはできません。

from paraview.vtk.vtkFiltersGeneral import vtkShrinkPolyData
s = vtkShrinkPolyData()
s.SetInputData(data)

データを設定しようとすると、エラーメッセージが表示されます。

TypeError: SetInputData argument 1: method requires a VTK object

代わりに、次のようにVTKオブジェクトをVTKフィルタに渡す必要があります。

s.SetInputData(data.VTKObject)

上記の例で注意すべき重要な点は、Pythonクラス vtkShrinkPolyData がスクリプトで使用するためにどのように読み込まれたかです。VTKでは、クラスは関連する機能の異なるグループに編成され、これらのグループはPythonモジュールとしてインポートすることができます。クラスを使用するには、まずそのクラスが存在するモジュールを特定します。これは、[KitwareInc] クラスのDoxygenドキュメントから確認できます。クラスのDoxygenページに移動し、ページの一番下にある dox/<first directory>/<second directory>/<class name> という形式のドキュメントが生成されたファイルのパスを探します。モジュールは vtk <最初のディレクトリ> <セカンドディレクトリ> として導出されます。例として、 vtkShrinkPolyData のドキュメントは dox/Filters/General/vtkShrinkPolyData から生成されているので、モジュールは vtkFiltersGeneral です。次に、フォームのステートメントを使用してクラスをインポートできます。

6.2.1. Dataset属性

これまでのところ、VTKデータオブジェクト用のラッパーがあります。 はVTKデータオブジェクトのように部分的に動作します。このデータセットに含まれるフィールド (配列) にアクセスする方法を調べ始めると、これが少し興味深いものになります。

簡単にするために、スクリプトによって生成された出力をコード自体に埋め込み、>>> プレフィックスを使用してコードと出力を区別します。

>>> print(data.PointData)
<vtk.numpy_interface.dataset_adapter.DataSetAttributes at 0x110f5b750>

>>> print(data.PointData.keys())
['Normals', 'Elevation']

>>> print(data.CellData.keys())
[]

>>> print(data.PointData['Elevation'])
VTKArray([ 0.5       ,  0.        ,  0.45048442,  0.3117449 ,  0.11126047,
        0.        ,  0.        ,  0.        ,  0.45048442,  0.3117449 ,
        0.11126047,  0.        ,  0.        ,  0.        ,  0.45048442,
        ...,
        0.11126047,  0.        ,  0.        ,  0.        ,  0.45048442,
        0.3117449 ,  0.11126047,  0.        ,  0.        ,  0.        ], dtype=float32)

>>> elevation = data.PointData['Elevation']

>>> print(elevation[:5])
VTKArray([0.5, 0., 0.45048442, 0.3117449, 0.11126047], dtype=float32)
# Note that this works with composite datasets as well:

>>> mb = vtk.vtkMultiBlockDataSet()
>>> mb.SetNumberOfBlocks(2)
>>> mb.SetBlock(0, data.VTKObject)
>>> mb.SetBlock(1, data.VTKObject)
>>> mbw = dsa.WrapDataObject(mb)
>>> print(mbw.PointData)
<vtk.numpy_interface.dataset_adapter.CompositeDataSetAttributes instance at 0x11109f758>

>>> print(mbw.PointData.keys())
['Normals', 'Elevation']

>>> print(mbw.PointData['Elevation'])
<vtk.numpy_interface.dataset_adapter.VTKCompositeDataArray at 0x1110a32d0>

この方法で、PointDataCellDataFieldDataPoints (vtkPointSet のみのサブクラス)、Polygons (vtkPolyData のみ)にアクセスすることができます。このAPIを使用して、さらに多くのタイプの配列にアクセサを追加します。

6.3. 配列の操作

本節では、テストパイプラインを、 Programmable Filter に接続された Wavelet ソースで構成するように変更します。

Script では、次のように RTData ポイントデータ配列にアクセスします。

# Code for 'Script'
from paraview.vtk.vtkFiltersGeneral import vtkDataSetTriangleFilter
image = inputs[0]
rtdata = image.PointData['RTData']

# Let's transform this data as well, using another VTK filter.
tets = vtkDataSetTriangleFilter()
tets.SetInputDataObject(image.VTKObject)
tets.Update()

# Here, now we need to explicitly wrap the output dataset to get a
# VTKObjectWrapper instance.
ugrid = dsa.WrapDataObject(tets.GetOutput())
rtdata2 = ugrid.PointData['RTData']

ここでは、画像データ( vtkImageData )と非構造格子( vtkUnstructuredGrid )の2つのデータセットを作成しました。基本的には同じデータを表しますが、非構造格子は画像データを四面体化することによって作成されます。したがって、非構造格子は、同じ点を持ちますが、より多くのセル(四面体)を持つことが予想されます。

from paraview.vtk.vtkFiltersGeneral import vtkDataSetTriangleFilter

6.3.1. 配列API

numpy_interface 配列オブジェクトの動作はNumPy配列と非常に似ています。実際、vtkDataSet サブクラスの配列はVTKArrayのインスタンスです。numpy.ndarray のサブクラスです。vtkCompositeDataSet とサブクラスの配列はNumPy配列ではありませんが、非常によく似た動作をします。相違点については、別の項で概説します。基本から始めましょう。次の作業はすべて予想どおりに行われました。

これまでと同様、単純化するために、スクリプトによって生成された出力をコード自体に埋め込み、>>> プレフィックスを使用してコードと出力を区別します。

>>> print(rtdata[0])
60.763466

>>> print(rtdata[-1])
57.113735

>>> print(repr(rtdata[0:10:3]))
VTKArray([  60.76346588,   95.53707886,   94.97672272,  108.49817657], dtype=float32)

>>> print(repr(rtdata + 1))
VTKArray([ 61.76346588,  86.87795258,  73.80931091, ...,  68.51051331,
        44.34006882,  58.1137352 ], dtype=float32)

>>> print(repr(rtdata < 70))
VTKArray([ True , False, False, ...,  True,  True,  True])

# We will cover algorithms later. This is to generate a vector field.
>>> avector = algs.gradient(rtdata)

# To demonstrate that avector is really a vector
>>> print(algs.shape(rtdata))
(9261,)

>>> print(algs.shape(avector))
(9261, 3)

>>> print(repr(avector[:, 0]))
VTKArray([ 25.69367027,   6.59600449,   5.38400745, ...,  -6.58120966,
        -5.77147198,  13.19447994])

この例では、次の点に注意してください。

  • 単一コンポーネントの配列は、常に次のような形をしています: (n-tuples,) であり、(n-tuples, 1)ではありません。

  • 複数のコンポーネント配列の形状は次のとおりです:(n-tuples, n-components)

  • テンソル配列の形状:(n-tuples, 3, 3)

  • 上記は、画像や他の構造データにも当てはまります。すべての配列は1つの次元(1成分配列)、2つの次元(多成分配列)または3つの次元(テンソル配列)を持ちます。

もう1つの素晴らしい点は、配列のインデックスを作成するためにboolean配列を使用できることです。したがって、次のようにすると非常に効果的です。

>>> print(repr(rtdata[rtdata < 70]))
VTKArray([ 60.76346588,  66.75043488,  69.19681549,  50.62128448,
        64.8801651 ,  57.72655106,  49.75050354,  65.05570221,
        57.38450241,  69.51113129,  64.24596405,  67.54656982,
        ...,
        61.18143463,  66.61872864,  55.39360428,  67.51051331,
        43.34006882,  57.1137352 ], dtype=float32)

>>> print(repr(avector[avector[:,0] > 10]))
VTKArray([[ 25.69367027,   9.01253319,   7.51076698],
       [ 13.1944809 ,   9.01253128,   7.51076508],
       [ 25.98717642,  -4.49800825,   7.80427408],
       ...,
       [ 12.9009738 , -16.86548471,  -7.80427504],
       [ 25.69366837,  -3.48665428,  -7.51076889],
       [ 13.19447994,  -3.48665524,  -7.51076794]])

6.3.2. アルゴリズム

配列APIを使えば簡単にいろいろなことができますが、numpy_interface.algorithms モジュールを使い始めるともっと面白くなります。前の例で簡単に紹介しました。ここではさらに詳しく説明します。アルゴリズムの完全なリストについては、help(algs) を使用してください。以下に簡単に説明できる例をいくつか示します。

>>> import paraview.vtk.numpy_interface.algorithms as algs
>>> print(repr(algs.sin(rtdata)))
VTKArray([-0.87873501, -0.86987603, -0.52497   , ..., -0.99943125,
       -0.59898132,  0.53547275], dtype=float32)

>>> print(repr(algs.min(rtdata)))
VTKArray(37.35310363769531)

>>> print(repr(algs.max(avector)))
VTKArray(34.781060218811035)

>>> print(repr(algs.max(avector, axis=0)))
VTKArray([ 34.78106022,  29.01940918,  18.34743023])

>>> print(repr(algs.max(avector, axis=1)))
VTKArray([ 25.69367027,   9.30603981,   9.88350773, ...,  -4.35762835,
        -3.78016186,  13.19447994])

軸引数を使用したことがない場合は、非常に簡単です。軸の値を渡さない場合、関数は次元性を考慮せずに配列のすべての値に適用されます。axis=0 の場合、関数は配列の各要素に独立に適用されます。axis=1 の場合、関数は各タプルに独立に適用されます。これがはっきりしない場合は、実験してみてください。 このように動作する関数には、 summinmaxstdvar があります。

もう1つの興味深い便利な関数は、特定の条件が発生した場合に配列のインデックスが返される関数です。

>>> print(repr(algs.where(rtdata < 40)))
(array([ 420, 9240]),)
# For vectors, this will also return the component index if an axis is not
# defined.

>>> print(repr(algs.where(avector < -29.7)))
(VTKArray([4357, 4797, 4798, 4799, 5239]), VTKArray([1, 1, 1, 1, 1]))

これまで説明したすべての関数は、NumPyによって直接提供されています。 NumPyの関数の多くは、algorithmsモジュールに含まれています。これらはすべて単一配列と複合データ配列で動作します。アルゴリズムには、NumPyのアルゴリズムとは少し異なる動作をする関数もあります。これらの関数には、cross、dot、inverse、determinant、eigenvalue、eigenvectorなどがあります。 5.9.3 章 の非網羅的なリストを参照してください。 これらの関数はすべて、配列/行列全体ではなく各タプルに適用されます。例えば:

>>> amatrix = algs.gradient(avector)
>>> print(repr(algs.determinant(amatrix)))
VTKArray([-1221.2732624 ,  -648.48272183,    -3.55133937, ...,    28.2577152 ,
        -629.28507693, -1205.81370163])

上記のすべてはタプルごとの情報のみを利用しており、メッシュには依存していないことに注意してください。VTKの最大の強みの1つは、そのデータ・モデルが多種多様なメッシュをサポートする一方で、そのアルゴリズムがこれらすべてのメッシュ・タイプで汎用的に機能することです。algorithmsモジュールは、この機能の一部を公開しています。その他の機能は、既存のVTKフィルタを利用することで容易に実装できます。以前に勾配を用いてベクトルと行列を生成しました。これでまた行います:

>>> avector = algs.gradient(rtdata)
>>> amatrix = algs.gradient(avector)

このような機能を使用するには、配列と関連するメッシュを含むデータセットにアクセスする必要があります。これは、dataset_adapterで ndarray のサブクラスを使用する理由の1つです。

>>> print(repr(rtdata.DataSet))
<paraview.vtk.numpy_interface.dataset_adapter.DataSet at 0x11b61e9d0>

各配列は、それを含むデータセットを指します。gradientなどの関数は、メッシュと配列を一緒に使用します。NumPyにはgradient関数も提供されています。

>>> print(repr(algs.gradient(rtdata2)))
VTKArray([[ 25.46767712,   8.78654003,   7.28477383],
       [  6.02292252,   8.99845123,   7.49668884],
       [  5.23528767,   9.80230141,   8.3005352 ],
       ...,
       [ -6.43249083,  -4.27642155,  -8.30053616],
       [ -5.19838905,  -3.47257614,  -7.49668884],
       [ 13.42047501,  -3.26066017,  -7.28477287]])
>>> print(rtdata2.DataSet.GetClassName())
vtkUnstructuredGrid

メッシュへのアクセスを必要とする勾配とアルゴリズムは、メッシュが一様な格子、曲線格子、またはVTKのデータモデルのおかげで非構造格子のいずれであっても機能します。アルゴリズムモジュールのさまざまな関数を見てください。残りのセクションでは、これらのモジュールを使用して特定の問題を解決する方法について説明します。

6.4. 複合データセットの処理

本節では、複合データセットについて詳しく説明します。この例では、パイプラインは Sphere ソースであり、 Cone ソースは Programmable Filter への2つの入力として設定されています。

次のように、 Programmable FilterScript にマルチブロックデータセットを作成できます。

# Let's assume inputs[0] is the output from Sphere and
# inputs[1] is the output from Cone.
mb = vtk.vtkMultiBlockDataSet()
mb.SetBlock(0, inputs[0].VTKObject)
mb.SetBlock(1, inputs[1].VTKObject)

VTKのアルゴリズムの多くは、変更することなく複合データセットで動作します。例えば:

e = vtk.vtkElevationFilter()
e.SetInputData(mb)
e.Update()

mbe = e.GetOutputDataObject(0)
print(mbe.GetClassName())

これは vtkMultiBlockDataSet を出力します。

スカラーを持つ複合データセットができたので、numpy_interface を使うことができます。 これまでと同様、単純化するために、スクリプトによって生成された出力をコード自体に埋め込み、>>> プレフィックスを使用してコードと出力を区別します。

>>> from paraview.vtk.numpy_interface import dataset_adapter as dsa
>>> mbw = dsa.WrapDataObject(mbe)
>>> print(repr(mbw.PointData.keys()))
['Normals', 'Elevation']
>>> elev = mbw.PointData['Elevation']
>>> print(repr(elev))
<paraview.vtk.numpy_interface.dataset_adapter.VTKCompositeDataArray at 0x1189ee410>

配列型がこれまで見てきたもの (`VTKArray`) と異なることに注意してください。ただし、同じように動作します。

>>> from paraview.vtk.numpy_interface import algorithms as algs
>>> print(algs.max(elev))
0.5
>>> print(algs.max(elev + 1))
1.5

各ブロックの配列には、次の方法で個別にアクセスできます。

>>> print(repr(elev.Arrays[0]))
VTKArray([ 0.5       ,  0.        ,  0.45048442,  0.3117449 ,  0.11126047,
        0.        ,  0.        ,  0.        ,  0.45048442,  0.3117449 ,
        0.11126047,  0.        ,  0.        ,  0.        ,  0.45048442,
        0.3117449 ,  0.11126047,  0.        ,  0.        ,  0.        ,
        0.45048442,  0.3117449 ,  0.11126047,  0.        ,  0.        ,
        0.        ,  0.45048442,  0.3117449 ,  0.11126047,  0.        ,
        0.        ,  0.        ,  0.45048442,  0.3117449 ,  0.11126047,
        0.        ,  0.        ,  0.        ,  0.45048442,  0.3117449 ,
        0.11126047,  0.        ,  0.        ,  0.        ,  0.45048442,
        0.3117449 ,  0.11126047,  0.        ,  0.        ,  0.        ], dtype=float32)

インデックス作成は少し異なることに注意してください。

>>> print(elev[0:3])
[VTKArray([ 0.5,  0.,  0.45048442], dtype=float32),
 VTKArray([ 0.,  0.,  0.43301269], dtype=float32)]

戻り値は、2つのVTKArrayで構成される複合配列です。[] 演算子は単に各配列の最初の4つの値を返しました。一般に、すべてのインデックス作成操作は、複合配列コレクション内の各VTKArrayに適用されます。アルゴリズムの場合も同様です。

>>> print(algs.where(elev < 0.5))
[(array([ 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]),),
       (array([0, 1, 2, 3, 4, 5, 6]),)]

次に、Normalsと呼ばれるもう1つの配列を見てみましょう。

>>> normals = mbw.PointData['Normals']
>>> print(repr(normals.Arrays[0]))
VTKArray([[  0.00000000e+00,   0.00000000e+00,   1.00000000e+00],
       [  0.00000000e+00,   0.00000000e+00,  -1.00000000e+00],
       [  4.33883727e-01,   0.00000000e+00,   9.00968850e-01],
       [  7.81831503e-01,   0.00000000e+00,   6.23489797e-01],
       [  9.74927902e-01,   0.00000000e+00,   2.22520933e-01],
       ...
       [  6.89378142e-01,  -6.89378142e-01,   2.22520933e-01],
       [  6.89378142e-01,  -6.89378142e-01,  -2.22520933e-01],
       [  5.52838326e-01,  -5.52838326e-01,  -6.23489797e-01],
       [  3.06802124e-01,  -3.06802124e-01,  -9.00968850e-01]], dtype=float32)
>>> print(repr(normals.Arrays[1]))
<paraview.vtk.numpy_interface.dataset_adapter.VTKNoneArray at 0x1189e7790>

2番目の配列が VTKNoneArray であることに注目してください。これは、vtkConeSource がNormalsを生成しないためです。配列が存在しない場合は、VTKNoneArrayをプレースホルダとして使用します。これにより、複合データセットのデータセットとVTKCompositeDataArray内のアレイ間の1対1のマッピングを維持することができます。また、特別なコードをたくさん使わなくても、アルゴリズムを並列に動かすことができます。

多くのアルゴリズムは、コレクション内の各配列に個別に適用されますが、一部のアルゴリズムはグローバルです。例えば、上で説明したように、minmax を使用します。 ブロック単位の回答を得ることが役立つ場合があります。これには _per_block アルゴリズムを使うことができます。

>>> print(algs.max_per_block(elev))
[0.5, 0.4330127]

これらは他のオペレーションと非常にうまく連携して動作します。たとえば、各ブロックの標高値を正規化する方法を次に示します。

>>> _min = algs.min_per_block(elev)
>>> _max = algs.max_per_block(elev)
>>> _norm = (elev - _min) / (_max - _min)
>>> print(algs.min(_norm))
0.0
>>> print(algs.max(_norm))
1.0

これらの機能を理解すると、単一の配列と非常によく似た複合配列を使用できるようになります。

複合データセットに関する最後の注意: numpy_interface.dataset_adapter によって提供される複合データラッパーには、複合データセットをトラバースするための便利な関数がいくつか用意されています。以下に簡単な例を示します。

>>> for ds in mbw:
>>>    print(type(ds))
<class 'paraview.vtk.numpy_interface.dataset_adapter.PolyData'>
<class 'paraview.vtk.numpy_interface.dataset_adapter.PolyData'>