Houdini 18.0 Pythonスクリプト

Pythonを使ったコンポジットノード(COP)の定義

On this page

How to

  1. File ▸ New Operator Type を選択します。

  2. Operator StylePythonに、 Network TypeCompositing Filter または Compositing Generator に設定します。

  3. Save To Library に、新しいノードタイプを保存するOTLライブラリファイルを設定します。

  4. Accept をクリックします。

    Edit Operator Type Propertiesウィンドウが表示されます。

  5. Edit Operator Type Propertiesウィンドウのオプションを使って新しいノードタイプのインターフェースを定義します。

  6. Code タブをクリックして、Pythonスクリプトを編集することでCOPの動作を定義します。

Tip

Type Propertiesウィンドウを閉じた後にスクリプトを編集したい場合、ノードのインスタンスをクリックして、 Type Properties を選択します。

COPフィルターの記述

Python COPフィルターは、複数の入力ノードの画像データに基づいて新しい画像データを生成します。COPの各ノードには、いくつかの画像平面が保存されていて、各平面は別々に処理されます。各COPには、C(カラー)とA(アルファ)平面がありますが、COPにはもっとたくさんの平面を作成することができます。COPの画像平面は必要に応じて処理され、いくつかの平面はビュアーや出力COPが要求するまでは処理されません。

PythonによるCOPノードタイプの記述は、PythonによるSOPやObjectノードタイプの記述とは異なります。Python SOPとObjectは、ノードが処理される度に Code タブ内のPythonコードを評価するのに対し、Python COPは Code タブ内に定義した特別な名前の関数をコールします。

Python COPフィルターを記述する時は、 Code タブに以下の関数の定義を記述します:

output_planes_to_cook(cop_node)

この関数は必須です。これは、このCOPが処理する画像平面の名前を含んだ文字列のシーケンスを返さなければなりません。

リストに入れていない平面が1番目の入力COPノードに存在していれば、修正なしで通過します。すべてのCOPには、CとAの平面が両方あります。つまり、これらの平面の1つまたは両方がリストにない場合は、1番目の入力の平面は無視されます。

リストに入れた平面が入力ノードにない場合は、その平面が作成されます。それらの特別な平面は、例えばビュアーで表示させるなどの要求がない限り処理されないことを知っておいてください。

required_input_planes(cop_node, output_plane)

この関数はCOPフィルターには必須です。これは、このノードを処理するために必要となる入力番号と平面の名前を識別する文字列のシーケンスを返さなければなりません。返されるシーケンスには、0番号から始めて、偶数番目が入力番号、奇数番目が平面の名前でなければなりません。

例えば、この関数が("0", "C", "0", "A", "1", "C")を返すとすると、1番目の入力は、CAの平面が、2番目の入力には、Cの平面が必要であることを意味します。

cook(cop_node, plane, resolution)

この関数は必須です。これは複数回コールされ、平面毎に1回、このノードによって処理されます。

planeは処理される平面の名前です。resolutionは、このノードの平面の解像度を意味する2つの整数値のシーケンスです。

入力ノードの平面の内容を取得するには、hou.CopNode.allPixels()またはhou.CopNode.allPixelsAsString()をコールします。前者の関数は、floatのシーケンスを返し、後者は高速で、リクエストしたフォーマットの画像データを含んだバイナリ文字列を返します。

required_input_planes関数で返されなかった入力と平面のallPixelsまたはallPixelsAsStringをコールする場合、Houdiniは、hou.OperationFailedの例外を起こします。

平面のピクセルデータを計算した後は、hou.CopNode.setPixelsOfCookingPlane()またはhou.CopNode.setPixelsOfCookingPlaneFromString()をコールします。

resolution(cop_node)

COPの画像解像度を変更する場合のみ、この関数を実装する必要があります。デフォルトでは、COPは1番目の入力の解像度を使用します。

この関数は、画像の幅と高さを意味する2つの整数値のシーケンスを返さなければなりません。画像の解像度は時間によって変更することができないことに注意してください。COPは、画像シーケンス内のすべてのフレームが同じ解像度でなければいけません。

depth(cop_node, plane)

既存の平面の画像深度を変更したい場合、または新しい平面の深度を32ビットfloat以外のタイプに変更したいのみ、この関数を実装する必要があります。デフォルトでは、COPは平面深度を変更せず、作成される平面は32ビットfloatです。

この関数は、指定した処理される平面の深度を意味するhou.imageDepthを列挙型の値で返さなければなりません。

frame_range(cop_node)

COPがフレーム範囲情報を変更したい場合のみ、この関数を実装する必要があります。デフォルトでは、COPは1番目の入力のフレーム範囲を使用します。

この関数がNoneを返せば、それは静止画像を生成したことを意味します。そうでない場合は、開始フレームとフレーム数を意味する2つの整数値のタプルを返さなければなりません。

remap_frame(cop_node, frame)

COPがタイミング情報を修正する場合のみ、この関数を実装する必要があります。このノードのフレーム範囲内のフレーム番号を指定すると、この関数は、入力ノードのフレーム範囲内でそれに該当するフレーム番号を返します。この関数により、タイミング情報をシフト、スケール、ワープすることができます。

この関数を実装する場合、必ずframe_rangeを実装して、正しい範囲を返してください。

Note

  • allPixels[AsString]またはsetPixelsOfCookingPlane[FromString]をコールする時、スキャンラインは底辺のスキャンラインを1番目に並べます。

  • 正しいデータサイズをsetPixelsOfCookingPlane*に設定しなければなりません。そうしないと、Houdiniはhou.OperationFailedの例外を発生します。この関数は、交互に混在したデータを受け入れたり、赤、緑、青のコンポーネントを別々でコールして設定することができます。詳細は、hou.CopNode.setPixelsOfCookingPlane()を参照してください。

  • allPixelsは、入力COPがデータの保存で使用するビット深度に関係なく、入力画像平面のピクセル毎の各コンポーネント(例:赤、緑、青、アルファなど)を32ビットfloat値で返します。例えば、入力COPの"C"平面が赤、緑、青のコンポーネントを8ビット符号なしの値で保存されていても、allPixelsを使って入力の"C"平面を問い合わせると、32ビットfloatが返されます。

  • allPixelsAsStringは、入力画像平面のピクセル毎の各コンポーネントをコンポジターでサポートされている画像深度(8ビットinteger、16ビットinteger、32ビットinteger、16ビットfloat、32ビットfloat)のどれかで返すことができます。特定のビット深度を要求するには、hou.imageDepth列挙型の値に深度パラメータを設定します。深度パラメータを指定しなかった場合、画像深度はCOPで処理される平面と同じになります。

  • output_planes_to_cookから返される値の平面を処理し、cook関数からsetPixelsOfCookingPlane*をコールしなかった場合は、Houdiniは見つからないコンポーネントのピクセルすべてをゼロで埋め尽くします。

  • Python COPはフレーム内のピクセルを取得・設定することだけが可能で、フルキャンバスエリア内のピクセルは許可されていません。COPでは、フレームが平面の可視領域であり、画像の解像度に相当します。しかし、ノードはフレーム外にピクセルを追加することが可能で、追加可能なピクセルすべての領域がキャンバスと呼ばれています。例えば、transform COPを使ってピクセルをフレームの右側からはみ出した場合、他のtransform COPを使ってピクセルを左に戻すと、オリジナルのピクセルがフレーム内に現れます。その理由は、最初のtransform COPのキャンパスがフレームより大きく拡大し、それにより、2番目のtransform COPがピクセルに対して処理するときに、フレーム外のピクセルを処理することができるようになっています。Python COPをもっと単純にするには、キャンバスは常にフレームと同じサイズにし、入力フレーム内に存在する入力データのみに対して処理を施すようにします。

  • 入力がないCOPからはピクセルデータにアクセスできません。言い分けると、Python COPに入力がないなら、allPixels*をコールしないでください。

  • Python COPに複数の入力があり、接続されている入力間にギャップがある場合、hou.Node.inputConnectors()メソッドを使用しくてださい。例えば、COPに最大2、最小0の入力を設定すると、1番目の入力が接続なしで、2番目の入力が接続ありにすることが可能です。hou.Node.inputs()をコールすれば、1つの入力のみが返されます。 以下のコードを記述すれば、ギャップがあっても入力にアクセスすることができます:

input_connections = cop_node.inputConnectors()[input_index]
if len(input_connections) == 0:
    input_node = None
else:
    input_node = input_connection[0].inputNode()

COPジェネレータの記述

ジェネレータとフィルターの違いは、単に入力の最小数です。Python COPノードタイプが同じなら、フィルターとジェネレータのどちらにでも動作します。この章では、入力なしで最小入力数がゼロの単純なCOPノードのジェネレータについて説明します。

Python COPジェネレータは通常では、output_planes_to_cookresolutiondepthcookメソッドを上書きします。

Python COPジェネレータがresolutionを上書きしなかった場合、解像度は1×1になります。frame_rangeを上書きしなかった場合は、静止画像を生成します。output_planes_to_cookから"C"と"A"の平面を返さなかった場合は、Houdiniは、それらの平面の名前でcookをコールします。

デフォルトでは、Python COPジェネレータが作成する平面は、常に32ビットfloatです。しかし、depth関数を使用すれば、コンポジターが対応している任意の画像深度を返すことができます。

COPノードのサンプル

このPython COPジェネレータは、定数カラーを生成し、追加で入力を受け取ることができます。入力を接続すると、resolutionパラメータが無視されて、その入力の解像度が使われます。

def output_planes_to_cook(cop_node):
    return ("C", "A")

def required_input_planes(cop_node, output_plane):
    return ()

def resolution(cop_node):
    # 入力がない場合は、resolutionパラメータの値を使用します。
    if len(cop_node.inputs()) == 0:
        return cop_node.parmTuple("resolution").eval()

    # 1番目に接続された入力の解像度を使用します。
    input_cop = cop_node.inputs()[0]
    return (input_cop.xRes(), input_cop.yRes())

def cook(cop_node, plane, resolution):
    num_pixels = resolution[0] * resolution[1]
    rgba = cop_node.parmTuple("color").eval()
    pixel = (rgba[3:] if plane == "A" else rgba[:3])
    cop_node.setPixelsOfCookingPlane(pixel * num_pixels)

以下のPython COPフィルターは画像データを処理しませんが、その代わり、指定したフレームの番号で入力シーケンスをシフトし、指定したスケール係数でタイミングをストレッチします。ノードには以下のパラメータがあると仮定します:

frameshift

入力シーケンスをシフトするフレーム番号

framescale

フレームの番号に適用するスケール係数

import math

def frame_range(cop_node):
    input_cop = cop_node.inputs()[0]
    if input_cop.isSingleImage():
        return None

    start_frame = int(input_cop.sequenceStartFrame() + cop_node.evalParm("frameshift"))
    length_in_frames = int(math.floor(
        input_cop.sequenceFrameLength() * cop_node.evalParm("framescale")))
    return (start_frame, length_in_frames)

def remap_frame(cop_node, frame):
    """このコードに渡されたフレームをクックするのに必要な入力のフレーム番号を返します。"""
    input_cop = cop_node.inputs()[0]
    input_start_frame = input_cop.sequenceStartFrame()
    frame_scale = cop_node.evalParm("framescale")
    frame_shift = cop_node.evalParm("frameshift")

    original_index_in_input = frame - input_start_frame
    remapped_frame_in_input = ((original_index_in_input / frame_scale) +
        input_start_frame - frame_shift)
    return remapped_frame_in_input

def output_planes_to_cook(cop_node):
    # 平面をクックしないことを返すことで、それらの平面が入力から渡されます。
    return ()

def cook(cop_node, plane, resolution):
    pass

Pythonのnumpyモジュールを使って、複数の箇所で背景上に同じ前景画像を合成する複雑なサンプルは、HOMクックブックMulti Stamp Python COPのサンプルを参照してください。

効率的なピクセルデータの処理

リストやタプルにfloatをたくさん使用すると、Pythonのオーバーヘッドが遅くなる可能性があります。つまり、可能であれば、hou.CopNode.allPixels()の代わりにhou.CopNode.allPixelsAsString()を使用するほうが良いです。以下のコードで示すように、Pythonのarrayモジュールを使えば、効率的に文字列データを32ビットfloatのシーケンスに変換することができます:

import array
str_data = input_cop_node.allPixelsAsString("C")
float_data = array.array("f", str_data)

同様に、floatのリストやタプルでhou.CopNode.setPixelsOfCookingPlane()をコールする代わりに、hou.CopNode.setPixelsOfCookingPlaneFromString()をコールするほうが効率的です。float_data.tostring()をコールすることで配列を文字列に変換すると同時に、setPixelsOfCookingPlaneFromStringは直接配列オブジェクトを受け入れることが可能です:

cop_node.setPixelsOfCookingPlaneFromString(float_data)

Houdiniにはnumpyライブラリが入っています。新しいデータのコピーを作成することなく、それを使ってバイナリ文字列の内容を32ビットfloatのシーケンスとして再解読することができます:

import numpy
str_data = input_cop_node.allPixelsAsString("C")
read_only_float_data = numpy.frombuffer(str_data, dtype=numpy.float32)

floatデータとして再解読された文字列データは、読み取り専用です。そのデータを修正するためにコピーしたいのであれば、単に、そのデータに対してcopy()をコールしてください:

writable_float_data = read_only_float_data.copy()

numpy配列を文字列に変換するには、str(writable_float_data.data)と記述します。しかし、numpy配列は直接setPixelsOfCookingPlaneFromStringに渡すことができます:

cop_node.setPixelsOfCookingPlaneFromString(writable_float_data)

Pythonループ内で1回に1ピクセル処理すると遅いです。numpyの配列には、Pythonのループを使わなくても複数ピクセルに対して効率的に処理する方法があります。以下のサンプルでは、画像のピクセルを明るくするノードを実装しています。COPにbrightという名前のfloatパラメータがあると仮定しています。

import numpy

def output_planes_to_cook(cop_node):
    return ("C",)

def required_input_planes(cop_node, output_plane):
    if output_plane == "C":
        return ("0", "C")
    return ()

def cook(cop_node, plane, resolution):
    input_cop = cop_node.inputs()[0]

    # 入力内の該当する平面からピクセルを取得し、そのデータからnumpy配列を構築します。
    pixels = numpy.frombuffer(
        input_cop.allPixelsAsString(plane), dtype=numpy.float32).reshape(
        resolution[1], resolution[0], 3).copy()

    # numpyを使用して、すべての値を輝度でスケールします。
    pixels *= cop_node.evalParm("bright")

    # numpy配列の内容をピクセルデータに戻します。
    cop_node.setPixelsOfCookingPlaneFromString(pixels.data)

COPの一部をC++で記述する

Houdiniのinlinecppモジュールを使うと、簡単にCOPの一部をC++で記述することができます。

入力平面の画像データを含むarray.arrayオブジェクトを構築する場合、その配列元となるバッファのアドレスをC++関数に渡すことでC++コードで配列の内容を修正することができます。arrayオブジェクトのbuffer_infoメソッドは、その元となるバッファ用に(address, length)タプルを返し、そのアドレスをfloat *としてinlinecpp C++関数に渡すことができます。

以下のサンプルも、numpyモジュールの代わりにC++を使って画像内のピクセルを明るくします。COPにはbrightという名前のfloatパラメータがあると仮定しています。

import array
import inlinecpp

def output_planes_to_cook(cop_node):
    return ("C",)

def required_input_planes(cop_node, output_plane):
    if output_plane == "C":
        return ("0", "C")
    return ()

def cook(cop_node, plane, resolution):
    input_cop = cop_node.inputs()[0]
    color_pixels = array.array("f", input_cop.allPixelsAsString(plane))

    cpp_lib = inlinecpp.createLibrary("py_brighten_cop", function_sources=["""
    void brighten(float *color_pixels, int num_pixels, float amount)
    {
        for (int i=0; i<num_pixels; ++i)
        {
            color_pixels[0] *= amount;
            color_pixels[1] *= amount;
            color_pixels[2] *= amount;
            color_pixels += 3;
        }
    }
    """])
    cpp_lib.brighten(
        color_pixels.buffer_info()[0], resolution[0] * resolution[1],
        cop_node.evalParm("bright"))

    cop_node.setPixelsOfCookingPlaneFromString(color_pixels)

numpy配列のデータのアドレスを取得するには、単にnumpy配列に対して.ctypes.dataをコールするだけです。上記のサンプルのarrayモジュールの代わりにnumpyを使用するには、以下のコードでnumpy配列を作成します:

color_pixels = numpy.array(input_cop.allPixelsAsString("C", dtype=numpy.float32))

そして、以下のコードでC++関数をコールします:

cpp_lib.brighten(color_pixels.ctypes.data, cop_node.evalParm("bright"),
    resolution[0] * resolution[1])

交互に混在しないピクセルデータへのアクセス

ここに載せているサンプルは、交互に混在したデータ(カラーデータがRGBRGBRGB...のように交互に並んで保存されている)を使用していることに注目してください。しかし、アルゴリズムによっては、交互に混在しないデータ(RRR..., GGG..., BBB...)を使って記述した方が簡単であり、入力のCOPに、この形式のデータを要求して、この形式を使ってデータを処理するように設定することが可能です。

以下のサンプルでは、一度に1コンポーネントに対して処理することで、入力平面のピクセルを明るくしています:

import array
import inlinecpp

def output_planes_to_cook(cop_node):
    return ("C",)

def required_input_planes(cop_node, output_plane):
    if output_plane == "C":
        return ("0", "C")
    return ()

def cook(cop_node, plane, resolution):
    input_cop = cop_node.inputs()[0]
    component_values_dict = dict(
        (component, array.array(
            "f", input_cop.allPixelsAsString(plane, component)))
        for component in input_cop.components(plane))

    cpp_lib = inlinecpp.createLibrary("py_brighten_cop", function_sources=["""
    void brighten(float *component_values, int num_pixels, float amount)
    {
        for (int i=0; i<num_pixels; ++i)
            component_values[i] *= amount;
    }
    """])

    for component, component_values in component_values_dict.items():
        cpp_lib.brighten(
            component_values.buffer_info()[0], resolution[0] * resolution[1],
            cop_node.evalParm("bright"))
        cop_node.setPixelsOfCookingPlaneFromString(component_values, component)

異なる画像深度の取り扱い

以下に、入力画像平面すべてをメニューパラメータで指定したフォーマットに変換する簡単なCOPのサンプルを載せます。COPには、メニューパラメータ"depth"があり、その値が"Int8", "Int16", "Int32", "Float16", "Float32"であると仮定しています(Houdiniには既に画像深度を変換するconvert COPが用意されていることを覚えておいてください)。

import inlinecpp

def output_planes_to_cook(cop_node):
    return cop_node.inputs()[0].planes()

def required_input_planes(cop_node, output_plane):
    return ("0", output_plane)

def depth(cop_node, plane):
    return getattr(hou.imageDepth, cop_node.parm("depth").evalAsString())

def cook(cop_node, plane, resolution):
    # allPixelsAsStringをコールした時に特定のデプスを指定しなかった場合は、
    # その入力平面のデプスを、クックする平面のデプスに変換します。
    # (これは、上記のdepth関数の戻り値で決まります。)
    input_cop = cop_node.inputs()[0]
    pixels = input_cop.allPixelsAsString(plane)
    cop_node.setPixelsOfCookingPlaneFromString(pixels)

以下のサンプルでは、入力平面の深度を保持する方法を説明しています。各入力平面は、Python COPが処理している間に32ビットfloatデータに変換されます。そして、その結果が入力平面の深度に反映されます。

import numpy

def input_cop(cop_node):
    return cop_node.inputs()[0]

def output_planes_to_cook(cop_node):
    input_planes = input_cop(cop_node).planes()
    return [plane for plane in cop_node.evalParm("planes").split()
        if plane in input_planes]

def required_input_planes(cop_node, output_plane):
    return ("0", output_plane)

def depth(cop_node, plane):
    return input_cop(cop_node).depth(plane)

def cook(cop_node, plane, resolution):
    input = input_cop(cop_node)
    pixels = numpy.frombuffer(
        input.allPixelsAsString(plane, depth=hou.imageData.Float32),
        dtype=numpy.float32)

    # ここにいくつかの処理を実行して、ピクセルの内容を修正します。

    cop_node.setPixelsOfCookingPlaneFromString(
        pixels, depth=hou.imageData.Float32)

以下のテーブルは、COPでサポートされている異なる画像深度に対応した配列フォーマットの指定子のリストです: table>>

Houdiniの列挙型の値

arrayモジュールのフォーマット文字列

numpyモジュールのフォーマット定数

numpyモジュールのフォーマット文字列

hou.imageDepth.Int8

"B"

numpy.uint8

"u1"

hou.imageDepth.Int16

"H"

numpy.uint16

"u2"

hou.imageDepth.Int32

"I"

numpy.uint32

"u4"

hou.imageDepth.Float16

利用不可

numpy.float16

"f2" (numpy 1.5.1+)

hou.imageDepth.Float32

"f"

numpy.float32

"f4"

以下のサンプルでは、numpyで操作するために必要となる、そのネイティブフォーマットの入力平面のnumpy配列の作成方法を説明しています:

import numpy

def input_cop(cop_node):
    return cop_node.inputs()[0]

def output_planes_to_cook(cop_node):
    input_planes = input_cop(cop_node).planes()
    return [plane for plane in cop_node.evalParm("planes").split()
        if plane in input_planes]

def required_input_planes(cop_node, output_plane):
    return ("0", output_plane)

def depth(cop_node, plane):
    return input_cop(cop_node).depth(plane)

depths_to_numpy_types = {
    hou.imageDepth.Int8: numpy.uint8,
    hou.imageDepth.Int16: numpy.uint16,
    hou.imageDepth.Int32: numpy.uint32,
    hou.imageDepth.Float16: numpy.float16,
    hou.imageDepth.Float32: numpy.float32
}

def cook(cop_node, plane, resolution):
    input = input_cop(cop_node)
    pixels = numpy.frombuffer(
        input.allPixelsAsString(plane),
        dtype=depths_to_numpy_types[input.depth(plane)]).copy()

    # ここにいくつかの処理を実行して、ピクセルの内容を修正します。

    cop_node.setPixelsOfCookingPlaneFromString(pixels)

COPのエラーと警告

Python COPに例外が発生すれば、ノードがエラーとして赤く表示されます。ノードをクリックすることで、そのエラーのスタックトレースを見ることができます。

ユーザ側にPythonスタックトレースを表示させないようにエラーメッセージを生成するには、hou.NodeErrorの例外を発生させてください。例えば、

raise hou.NodeError("Invalid parameter settings")

を実行すると、"Invalid parameter settings"のエラーメッセージでノードが赤く表示されます。同様に、ノードの警告を追加するには、hou.NodeWarningのインスタンスを発生させます。

COPで使用する特別な関数名

参考のために、以下に特別な意味を持つ関数を載せています:

  • output_planes_to_cook(cop_node)

  • required_input_planes(cop_node, output_plane)

  • cook(cop_node, plane, resolution)

  • remap_frame(cop_node, frame)

  • frame_range(cop_node)

  • resolution(cop_node)

  • output_planes_to_cook(cop_node)

  • depth(cop_node, plane)

  • frame_range(cop_node)

Pythonスクリプト

はじめよう

次のステップ

Pythonビューアステート

Pythonでビューアステートを記述することで、ビューポート内でノードのユーザ操作をカスタマイズすることができます。

導師レベル

リファレンス

  • hou

    Houdiniにアクセスできるサブモジュール、クラス、ファンクションを含んだモジュール。

  • Alembic拡張関数

    Alembicファイルから情報を抽出するための便利な関数です。