DAY2: 画像処理,Scripting,OpenCV

Example projectリンク

目次

画像について

画像(image)は画素(pixel)がグリッド状に配置されて成り立っています.それぞれの画素は異なる色に調整されて,その色と場所の組み合わせによって,
一つの絵のように見えている
のです.
以下の画像は顕微鏡で観たディスプレイの画素です.よく見るとRGBが横並びになっている最小限の構成要素がグリッド状に並び,それぞれの強さが若干違います.このように私達が普段デジタルツールなどでRGBの値を決めて作ったテクスチャは最終的にこの画素の色の強さに直結していることがわかります.

画像出典元: AV Watch

画素の情報を得る

画素の情報を決めることはこれまでデジタルツールで行なってきましたが,逆に画像表現においては画像やカメラなどの情報を取り込んだその瞬間のフレームの色情報を取得することでそのデータを使った解析をしたり表現をすることになります.TouchDesignerではnon-scriptingで画像情報を使用した表現も可能ですが,今回はどちらの事例も広く扱います.

non-scriptingで得る

TOP(テクスチャ)の色情報を数値として得る場合,前回扱ったオペレータタイプの変換を行なうことでCHOPに色のChannelを生成し画素1つ1つの値をSampleとして得ることができます.
詳しくは以下のオペレータタイプ変換のセクションを見て下さい.
オペレータタイプ変換リンク


画像からインスタンシング

参照するオペレータをTOPにすることでそのピクセルのRGBA値をインスタンシングのパラメータとして用いることができます.しかし,TOPの解像度が高すぎると,横x縦px分複製されるので最悪TouchDesigerが固まってしまいます.解像度をCommon > ResolutionからEighth(1/8)くらいに予め落としてからインスタンシングするようにしましょう.

scripting(Script TOP)で得る

フレームの情報を得て,計算したり,情報の一部を他に書き出したり,テクスチャとして吐き出すにはScript TOPを使用します.ここではScript TOPの使うための概念や方法を紹介します.


Pythonとは

Pythonはシンプルで読みやすく,覚えやすいプログラミング言語です.オープンソースで,手間のかかるコンパイルがない手軽さから,プログラミング初学者の教育用にも多く用いられています.
また,プロフェッショナルからも愛用され,ビッグデータ,機械学習などのデータサイエンス分野でも主要な言語として使われおり,豊富なライブラリが用意されています.
「プログラミング言語ランキング」で検索すると,その人気の度合いが見て取れると思います.
TouchDesignerではあらゆる場面でPythonスクリプトを書いて実行することができ(すでに少し書いてしまっていますが),必要な箇所でうまく使用していくことでテキストベースのほうが得意な処理はPythonで行なうというような使い分けをしていくと良いでしょう.


Textport and DATS

Processingで出ていたコンソールウィンドウに当たるものを出すには,Textport and DATSというウィンドウを出します.
Textportにはprint関数によって文字列が出力されたり,そのままPythonのコードも書けます.また,エラーが出たときにもエラーの内容を表示します.
常にどこかに表示しておくようにすると良いでしょう.
Geometry viewerの時と同じく,Splitしたウィンドウの表示を切り替えることで表示させることができます.


情報を得てテクスチャに吐き出す

以下の画像では最低限「対象となるフレームの情報を取得して,テクスチャとして吐き出す」ということをやっています.(対象のフレームはSRCと名付けたNull TOP)

Script TOPは出した瞬間から,script1_callbacksという名のText DATが付属します.基本的にはこちらにscriptingしていきます.
Text DAT内のコードは以下の通りです.

import numpy

def onCook(scriptOp):
    frame = op('SRC').numpyArray()
    scriptOp.copyNumpyArray(frame)
    return

それぞれの関数や変数については以下の表の通りです.

命令 概要
import numpy 配列計算用ライブラリのNumpyを使えるようにする
onCook(scriptOp) 毎フレーム自動で実行される関数.scriptOpはScript TOP自体を指す.
op(‘xxx’).numpyArray() 指定したOPの画素情報をNumpyで取得
scriptOp.copyNumpyArray(xxx) Script TOPにNumpy配列を指定してテクスチャとして吐き出す.

指定の画素の情報にアクセスする

指定した画素の情報にアクセスするために,まずNumpy配列の形状(各次元のサイズ)をTextportに表示してみます.

print(frame.shape)

結果は以下の画像のように1次元目が720でy軸,2次元目が1280でx軸,3次元目が4でRGBAのカラーチャンネルであることがわかります.

Numpy配列の形状が判明したところで,以下のように指定した画素の色情報を取得してみます.

print(frame.item(100, 100, 0))

結果は以下の画像のように0.0 – 1.0の小数で値が表示されます.これは(100, 100)のRの情報を示しています.
このときのx,y座標で注意する点は左下原点であることです.つまり,Processingと比較すると上下が逆の構造になっています.
また,3次元目の値はインデックス0,1,2,3が色の値R,G,B,Aに対応しています.


情報を得て数値として吐き出す

解析したTOPから何らかの情報を数値として吐き出すためにはScript CHOPを対象として,Channelを生成してSampleを追加していくことが必要になります.
以下のコードは例として,指定した場所のRGB値をCHOPに書き出したものです.

import numpy

def onCook(scriptOp):
    frame = op('SRC').numpyArray()
    scriptOp.copyNumpyArray(frame)

    scr = op('script2')
    scr.clear()

    r = scr.appendChan('r')
    g = scr.appendChan('g')
    b = scr.appendChan('b')

    scr.numSamples = 2

    x = 200
    y = 100

    r[0] = frame.item(y, x, 0)
    g[0] = frame.item(y, x, 1)
    b[0] = frame.item(y, x, 2)

    x1 = 400
    y1 = 300

    r[1] = frame.item(y1, x1, 0)
    g[1] = frame.item(y1, x1, 1)
    b[1] = frame.item(y1, x1, 2)

    return

ここではScript CHOPを出したときに付随するText DATを削除し,Script CHOPのパラメータウィンドウのCallbacks DATのテキストフィールドも削除することに注意します.

それぞれの命令や関数については以下の表の通りです.

命令 概要
clear() これまでの変更をクリアする
appendChan(‘xxx’) Channelを指定の名前で追加する
numSamples Sample数を指定する
xxx[y] 指定した配列の指定したインデックスの要素


しきい値を越えた画素の場所を示す

これまでのことを応用してしきい値を越えた画素を全て示すには,画素を1つ1つ監視して,設定したしきい値を超えた場所をSampleとして追加すると良いでしょう.

Script TOPに書いたコードは以下の通りです.

import numpy

def onCook(scriptOp):
    frame = op('SRC_ANALYZE').numpyArray(delayed=True)
    scriptOp.copyNumpyArray(frame)

    scr = op('script2')
    scr.clear()

    tx = scr.appendChan('tx')
    ty = scr.appendChan('ty')

    samplesX = []
    samplesY = []
    w = op('SRC_ANALYZE').width
    h = op('SRC_ANALYZE').height
    asp = w / h
    for y in range(frame.shape[0]):
        for x in range(frame.shape[1]):
            if frame.item(y, x, 0) >= op('constant2').par.value0:
                samplesX.append(x / w)
                samplesY.append((y / h) / asp)

    scr.numSamples = len(samplesX)

    for i in range(len(samplesX)):
        tx[i] = samplesX[i]
        ty[i] = samplesY[i]
    return

OpenCV

OpenCVとは

OpenCV(正式名称: Open Source Computer VisionLibrary)は、オープンソースのコンピューター・ビジョン・ライブラリです.商用利用まで可能であるため様々なプロジェクトで使用されています.しかも,C, C++, pythonなど様々な言語から利用可能なため,手軽に利用することができます.

OpenCVでできること

OpenCVを使用することで,以下のような先程までの画素を自分で取得する方法ではなかなかできなかった発展的な解析や画像処理が可能になります.

  • フィルター処理
  • 行列演算
  • オブジェクト追跡(Object Tracking)
  • 領域分割(Segmentation)
  • カメラキャリブレーション(Calibration)
  • 特徴点抽出
  • 物体認識(Object recognition)
  • 機械学習(Machine learning)
  • パノラマ合成(Stitching)
  • コンピュテーショナルフォトグラフィ(Computational Photography)
  • GUI(ウィンドウ表示、画像ファイル、動画ファイルの入出力、カメラキャプチャ)

Face tracking

画像から顔の位置と幅,高さを解析して示します.
このようなことができれば,自身の顔を知られない「笑い男」フィルターができます.

(C)士郎正宗・Production I.G/講談社・攻殻機動隊製作委員会


例は対象のTOPから顔があるだけその場所に赤枠を表示します.
Script TOPに書かれたコードは以下の通りです.

import numpy as np
import cv2

face_cascade = cv2.CascadeClassifier('data/haarcascade_frontalface_default.xml')

def onCook(scriptOp):
    frame = op('SRC_ANALYZE').numpyArray(delayed=True)
    scriptOp.copyNumpyArray(frame)
    gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
    gray = gray * 255.0
    gray = gray.astype(np.uint8)
    gray_invert = np.flipud(gray)

    faces = face_cascade.detectMultiScale(gray_invert)



    scr = op('script2')
    scr.clear()

    tx = scr.appendChan('tx')
    ty = scr.appendChan('ty')
    tw = scr.appendChan('tw')
    th = scr.appendChan('th')

    scr.numSamples = len(faces)

    SRC_ANALYZE_W = op('SRC_ANALYZE').width
    SRC_ANALYZE_H = op('SRC_ANALYZE').height
    asp = SRC_ANALYZE_W / SRC_ANALYZE_H

    if len(faces) > 0:
        for i in range(scr.numSamples):
            x = faces[i][0]
            y = faces[i][1]
            w = faces[i][2]
            h = faces[i][3]
            tx[i] = (x + w / 2) / SRC_ANALYZE_W
            ty[i] = (- (y + h / 2) / SRC_ANALYZE_H + 1) / asp
            tw[i] = w / SRC_ANALYZE_W
            th[i] = (h / SRC_ANALYZE_H) / asp
    return



関数や変数とその概要は以下の通りです.
特にFace tracking用の解析時にはOpenCVの画素の読み方に合わせるため,TOPから読んだNumpy配列を上下逆にすることに注意します.
解析後は,facesとしてインスタンス化したオブジェクトの配列の2次元目のインデックス0,1,2,3がx,y,w,hに対応することを意識してScript CHOPのSampleを追加していきます.

命令 概要
import cv2 OpenCVライブラリを使えるようにする
CascadeClassifier(‘xxx.xml’) 顔認識用のモデルを読み込む
cvtColor(xxx, cv2.COLOR_RGB2GRAY) 指定したRGBAフレームをグレースケールにする
astype(np.uint8) 符号なし8bit整数に変換する
np.flipud(xxx) 指定したフレームを上下反転する
xxx.detectMultiScale(yyy) 指定したフレームを読み込んだモデルで解析する

Threshold

しきい値を使用して簡単な画像処理を行なうこともできます.
Script TOPに書かれたコードは以下の通りです.

import numpy as np
import cv2

def onCook(scriptOp):
    frame = op('SRC').numpyArray(delayed=True)
    frame *= 255.
    ret, thresh = cv2.threshold(frame, 127, 255, cv2.THRESH_BINARY)
    scriptOp.copyNumpyArray(thresh)
    return


Brightset point

最も輝度の高い箇所1点を示すことができます.

Brightest pointを使用して作られた作品で有名なものとして,Graffiti Research LabのL.A.S.E.R Tagが上げられます.ビルに向かってプロジェクションしながら,画像解析も行なうことでレーザーポインタで描かれた点を検出してグラフィティを描いていける作品です.

例としてBrightest pointを赤い円で示してみます.
Script TOPに書かれたコードは以下の通りです.

import cv2
import numpy as np


def onCook(scriptOp):
    frame = op('SRC_ANALYZE').numpyArray()
    scriptOp.copyNumpyArray(frame)
    gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
    minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(gray)
    SRC_W = op('SRC').width
    SRC_H = op('SRC').height
    SRC_ANALYZE_W = op('SRC_ANALYZE').width
    rate = SRC_W / SRC_ANALYZE_W

    op('constant1').par.value0 = maxLoc[0] * rate - SRC_W / 2
    op('constant1').par.value1 = maxLoc[1] * rate - SRC_H / 2
    return



関数や変数とその概要は以下の通りです.
Brightest pointでは最高輝度の一点のみということもあり,Sampleは1つです.そのため,Script CHOPに書き出す必要がなく,単純なConstant CHOPへのパラメタリンクになっていることに注意します.

命令 概要
import cv2 OpenCVライブラリを使えるようにする
cvtColor(xxx, cv2.COLOR_RGB2GRAY) 指定したRGBAフレームをグレースケールにする
minMaxLoc(xxx) 指定したフレームの最大輝度,最小輝度,その場所をそれぞれ解析する
maxLoc[0] 最大輝度の場所のx座標
maxLoc[1] 最大輝度の場所のy座標

Template matching

Template matchingとは指定したテンプレートとなるテクスチャ(マークや人の顔など任意)と解析したい画像を比較して,類似度の高い場所を示す手法です.
これにより,特定のマーカーなどの位置を検知できるようになります.

例として複数の顔の画像に対して,そのうち一人だけの顔をテンプレートして比較してみます.
Script TOPに書かれたコードは以下の通りです.

import numpy as np
import cv2


def onCook(scriptOp):
    frame = op('SRC_ANALYZE').numpyArray(delayed=True)
    template = op('SRC_MARKER_ANALYZE').numpyArray(delayed=True)

    match = cv2.matchTemplate(frame, template, cv2.TM_CCOEFF_NORMED)
    colorMatch = match[:, :, np.newaxis]
    minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(match)

    scriptOp.copyNumpyArray(colorMatch)

    SRC_W = op('SRC').width
    SRC_H = op('SRC').height
    SRC_ANALYZE_W = op('SRC_ANALYZE').width
    rate = SRC_W / SRC_ANALYZE_W

    if maxVal >= 0.80:
        x = maxLoc[0]
        y = maxLoc[1]
        w = op('SRC_MARKER').width
        h = op('SRC_MARKER').height
        op('constant1').par.value0 = (x + w / 4) * rate - SRC_W / 2
        op('constant1').par.value1 = (y + h / 4) * rate - SRC_H / 2
        op('constant1').par.value2 = w
        op('constant1').par.value3 = h
    return



関数や変数とその概要は以下の通りです.
TM_CCOEFF_NORMEDというメソッドでフレームとテンプレートを比較しますが,この場合類似度が高いほうが明るくなるようなテクスチャが生成されるので,先程のBrightest pointの考え方で最大輝度の場所をConstant CHOPにパラメタリンクしています.同じく最大輝度は1点であることに注意します.

命令 概要
matchTemplate(frame, template, method) 指定したメソッドでフレームとテンプレートを比較する
TM_CCOEFF_NORMED 相関係数+正規化