第9回: リアルタイム画像処理,Python

目次

画像について

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

画像出典元: AV Watch

リアルタイム画像処理 Non-scripting編

動体検知

動体検知は動くモノを検知することです.背景と前景との差分あるいは異なるフレーム間の差分を見る,つまりある瞬間と別の瞬間のピクセルの差を見ることによって,動いたモノ(変化したピクセル)を知ることができます.動画では

今のフレーム - 数フレーム前のフレーム = 差分のピクセル

を行なっています.

Blob Tracking (OpenCV)

Blob TrackingとはOpenCV(コンピュータビジョンのライブラリ)ベースの動体検知システムです.前景と背景をインプットに繋ぐことで,動体とその幅と高さの値も取得することができます.画面内での動体の情報についてはInfo DATを配置し,参照先をBlob Track TOPにすると取得できます.Info DATで取得できるデータについて表に示します.non-commercialライセンスでは同時に2つの動体までしか検知できないようです.

取得データ 概要
id 検知開始時からの取得番号
u 横座標
v 縦座標
width
height 高さ

フレームキャッシュ

フレームをキャッシュしておくことで,過去のフレームを使用した残像系の表現につなげることができます.

画面分割

入力画像をCache TOPに繋げ,Cache Select TOPでキャッシュしたフレームを選ぶことで分割した画面ごとに少しずつ時間を遅らせたような表現ができます.

Feedback

Feedback TOPは自分より後のオペレータを指定することで,フィードバック効果を得られるTOPです.Feedback TOP後に付け加えた効果付きのTOPとFeedback前のTOPが合成されているTOPを参照する必要があります.

Time Machine

フレームキャッシュの表現の中に,フレームを指定分キャッシュしてそのどれを選ぶかエリアごとに決められるTime Machine TOPを使ったものがあります.Texture 3D TOPでキャッシュするフレームを連番で保持しておいて,Time Machine TOPのインプット1つめに素材,2つ目にグレースケールのイメージを入れることでそのイメージの階調によってフレームを選ぶエリアを決めることができます.白と黒のフレームのインデックスは決めることができます.

テクスチャを貼る

2Dのまま解析したり効果を加える以外にも3Dの構成要素として適用することもできます.

Color Map

MATのColor Mapにテクスチャを貼ることでオブジェクトの表面に意図したイメージを貼り付けることができます.

LITEというバンドが前編オンラインライブを行ないました.そのうちの1曲だけKezzardrixがライブエフェクトとして参加した模様が公開されています.2Dのノッペリとした定点の映像だけでもうまく効果をつけたり,3D上に配置したりすることで大きく印象が変化していることがわかります.

Post Processing

PostProcessingとはレンダリング前後の情報を使用して,効果を加えることを言います.代表的なものを以下の表に示します.

名称 概要
Depth of Field 被写界深度,いわゆるぼけみ
Film grain フィルムの粒状のノイズ
Glow 光のにじみ
Vignette ビネット.画面端の暗み
SSR スクリーン空間上の反射

Glow

光のにじみを表現するGlowを,間的に実装してみます.厳密なアルゴリズムとは異なるのですが,うまくBlur TOPでボケさせて加算合成すると光のにじみらしきものが表現できます.
Blur TOPのPre Shrinkは増やしすぎないように注意しましょう.負荷がかかりすぎて重くなります.

Film Grain

フィルム特有の粒状のノイズをこれも簡易的ですが実装します.Noise TOPのType > Random(GPU)とすることでホワイトノイズを作り出し,乗算合成することで実現します.

Vignette

ビネットの読みます.Ramp TOPを使ってVignetteは周りを黒くぼかすことで,カメラで起こる微ベッティングをシミュレーションします.

RGB Shift

RGB Shiftは映像の乱れのような効果を起こします.Channel Mix TOPでRGBそれぞれの値を取り出し,それぞれを横方向に移動させることで実現します.


リアルタイム画像処理 scripting編

scripting(Script TOP)で画像の情報を得る

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


Pythonとは

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


PythonとProcessingの比較

PythonとProcessingでは型の宣言の仕方から関数の書き方やクラスの書き方,インデントなどあらゆるところが似てはいますが,異なるので注意が必要です.

Processing:

int num; //整数型を宣言
num = 10; //10を代入
print(num); //numをコンソールに出力

Python:

num = 10
print(num) //numをコンソールに出力

Pythonは「動的型付き言語」に分類される言語で,変数の型が状況に応じて動的に決定されます.
つまり,Processingで宣言していた型を明示的に指定する必要がありません.

文末とインデント

Pythonの文末にはセミコロンが不要です.

条件分岐

Processing if文:

int num = 10;
int thre = 5;
if (num >= thre) {
    print("numは" + str(thre) + "以上です");
}
else {
    print("numは" + str(thre) + "未満です");
}

Python if文:

num = 10
thre = 5
if num >= thre:
    print('numは' + str(thre) + '以上です')
else:
    print('numは' + str(thre) + '未満です')

条件分岐の条件式にカッコは不要です.
また,条件式の後にはコロンを書きます.
次の文はタブによる空白があります.これは4文字のスペースでも代用できます.
TouchDesignerでは関数などがタブで作られているので,自分でインデントするときはタブキーで対応しましょう.
(ただ,業界全体としては,スペースで統一することが推奨されているようです.)

繰り返し

Processing forループ:

int num = 10;
for (int i = 0; i < num; i++) {
    print(i);
}

Python forループ:

num = 10
for i in range(num):
    print(i)

in range(数)によって繰り返し回数を指定できます.
pythonのforループには他にも色々なループの回し方があり,用途で使い分けられて便利です.

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()

    scr.lock = True

    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)

    scr.lock = False
    return

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

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

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


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()

    scr.lock = True

    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

    scr.lock = False



関数や変数とその概要は以下の通りです.
特に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座標


課題

授業内課題

カメラからのインプットを使ってスマホの光で絵がかけるアプリを作りなさい.

要件

  • ‘r’キーで書いた絵をリセットできること
  • Feedbackを使用すること
  • ファイル名を学籍番号+名前(ローマ字).toe として添付して提出(例: 11111111SeiyaAoki.toe)

締切: 2024年6月15日16:10
提出先: 情デサーバ内/09_授業内

来週までの課題

今日の内容を使って,リアルタイム画像処理しなさい.

要件

  • ファイル名を学籍番号+名前(ローマ字).toe として添付して提出(例: 11111111SeiyaAoki.toe)
  • カメラのインプットを必ず入れること(Video Device In TOPが使えない学生はMovie File In TOPでOK)
  • 使用したファイルがあればプロジェクトファイルと同一フォルダに入れて,Zip化して添付すること

締切: 2024年6月20日23:59
提出先: 情デサーバ内/09_週課題