ニューラルネットでバーチャルキャンドルを作ろう(第1回)

(更新

Use RNN-based machine learning to simulate candlelight in this electronics project. Learn how to extract light data from videos using OpenCV and Python.

A minimalist flat-style illustration of a child sitting in front of a computer screen displaying a Christmas-themed candle with a flickering flame. The child is holding a magnifying glass, observing the flame closely. On the desk, there is a measuring device with moving red, green, and blue bar graphs. The child appears to be printing measurement results onto paper. The setting is cozy, with soft, muted color tones, emphasizing a calm and focused technological atmosphere. Illustrated by ChatGPT and DALL-E.

相模原市で IoT 設計を受託しているファームロジックスです。

夏休み工作シリーズといった記事は書いたことがありますが、今回は年末年始向けの工作をしてみましょう。名付けて、

AI(ニューラルネット)でバーチャルキャンドルを作ろう!

です。

年末までに完成すると良いのですが、他の仕事もしなくてはならないので、ちょっと難しいかも知れません。これを一つのまとまった記事にしようとすると、何も投稿できずに終わってしまうかも知れないので、何回かの記事に分けたいと思います。

電子キャンドルを作りませんか?

ネットで次のように検索すると、Arduino マイコンなどを使ってバーチャルキャンドルを作るといった記事が何件も見つかります。

  • Arduino LED キャンドル 作り方
  • Arduino electronic candle DIY

しかし、私がざっと探したところでは、AI、特に機械学習(machine learning)を使ってバーチャルキャンドルを作る、といった記事はあまりないようです。

そこで今回は AI バーチャルキャンドルに挑戦したいと思います。機械学習の技術の中でも、RNN(Recurrent Neural Network: 回帰型ニューラルネットワーク)というものを使いたいと思います。果たしてうまくいくのでしょうか。(お楽しみに…)

RNN とはなにか?

やや専門的な話になります。

RNN は、時系列データから未来の値を予測するための技術です。これに似た古典的な手法に「線形予測(Linear Prediction)」があります。線形予測は、過去のデータを基に予測に必要な係数を求め、固定された数の過去のデータから未来の値を計算します。

一方、RNN は過去のデータを基に「内部状態」を更新し続け、必要に応じて情報を保持しながら未来の値を予測します。内部状態を持つことで、原理的には無限の過去の影響を反映できます。また、RNN は非線形なデータの変化も学習できるため、音声認識や文章生成のような複雑なタスクも実現できます。

今回はこの RNN を使い、ニューラルネットワークにロウソクの炎の光り方を学習させることができないか、検討してみようと思います。

リアルロウソク(?)の明滅データを取得するには

今回の記事の主題はここです。RNN を学習させる(訓練する)には、本当のロウソク(リアルロウソクと呼びましょう)の光り方のデータを入手する必要があります。その手順だけでも興味深いテーマとなりそうですので、今回はその点に集中します。

この場合、最も理にかなった方法は、実際にロウソクを用意してウチワか何かで風を当てながら、その明るさデータを取得するというものです。明るさの取得には、CdS や フォトダイオードを使っても良いでしょうし、最近のツールであれば、たとえばスマートフォンのカメラで動画を撮影し、OpenCV などで明るさを取得しても良いでしょう。

もう少し簡単な方法としては、ネット上で、ロウソクが瞬くビデオを探すというものです。検索エンジンで探すと、そのような動画はたくさん見つかります。例えば、こんなものがあるでしょう。

※ なお、このようなネット上のリソースを利用する際には、ライセンスに十分配慮しましょう。

動画から明滅データを CSV に変換しよう

今回は、Python と OpenCV を使って明滅データを取得することにしましょう。ロウソクの炎の「色」にも興味があるので、RGB データを得ることにします。

具体的なやり方ですが、OpenCV を使い、動画データからフレーム単位の静止画を抜き出していきます。次に、画面全体を見てもしょうがないので、ロウソクの炎周辺の画像 cropped だけを抜き出します。次のようなイメージ(擬似コード)になるでしょうか。

import cv2

cap = cv2.VideoCapture(input_file)

while cap.isOpened() and frame_count < max_frames:
    ret, frame = cap.read()
    if not ret:
        break

    cropped = frame[top : top + height, left : left + width]

次は、この cropped を使って、各フレームにおけるロウソクの明るさと色データを取得しようと思います。

アプローチ 1(単純平均)

私が最初に考えたのは、上記 cropped に含まれる画素全てに対して、R, G, B それぞれの平均値を求める、という方法です。なあんだ、簡単だ!

上記ループの中で、次のように処理します。

    # Calculate average BGR
    mean = cropped.mean(axis=(0, 1))

    # Normalize to 0-1
    avg_b, avg_g, avg_r = (mean / 255.0).tolist()

結果は CSV ファイルに保存します。各行が動画の 1フレームに該当し、左から R, G, B の値となります。(0〜1に正規化済み。以下同様)

0.67,0.57,0.48
0.67,0.57,0.48
0.66,0.55,0.47
0.65,0.55,0.47
0.65,0.55,0.47
0.66,0.56,0.48
(略)

以下に、その時間軸プロットを示します。

グラフはもっともらしい形をしています。しかしこの方法はうまく行きませんでした。得られたデータで仮想 LED アプリを作って明滅させてみると、色が「土色」とでも呼ぶべき色(泥のような色?)となってしまい、色の時間変化もほとんどなく、とてもロウソクの炎には見えなかったのです。

そこで、ビデオから静止画を切り出した後、Gimp という画像編集ツールを使って元画像のあちこちのピクセルを覗いていたところ、原因が分かりました。それは、ロウソクの炎に対応するピクセルのほとんどが、R = 255, G = 255, B = 255(255 は明るさの最大値)にクリップしているのです。結果として、そのような色と、ロウソクの背景(今回は黒)の単純平均をとっても、ロウソクの炎の色には見えない、ということのようです。

アプローチ 2(彩度の高いピクセルのみ平均)

次に考えたのは、cropped に含まれる画素を全て HSV(Hue, Saturation, Value)に変換し、S(彩度)の高いピクセル(たとえば上位 1%)だけを使って色(H, S)の平均を求め、それとは別に cropped 内の「全画素」の V(明度)を使って、そのフレームの RGB を求めたら、というものです。

ループの中で、次のように処理します。

    # Calculate average brightness from grayscale
    gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY)
    brightness = gray.mean()

    # Convert to HSV and sort by saturation
    hsv = cv2.cvtColor(cropped, cv2.COLOR_BGR2HSV)
    hsv_flat = hsv.reshape(-1, 3)
    hsv_sorted = hsv_flat[hsv_flat[:, 1].argsort()][
        -int(0.01 * len(hsv_flat)) :
    ]

    # Average H and S from top 1% saturation
    avg_h = hsv_sorted[:, 0].mean()
    avg_s = hsv_sorted[:, 1].mean()

    # Convert back to BGR (values in range 0-255)
    final_hsv = np.uint8([[[avg_h, avg_s, brightness]]])
    final_bgr = cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)[0, 0]

    # Correctly unpack in RGB order
    avg_b, avg_g, avg_r = (final_bgr / 255.0).tolist()

 

これが結果です。

残念ながら、この方法でもうまくいきませんでした。仮想 LED を使って明滅させてみると、かなりの割合で緑色に近い光り方をして、あまりロウソクの炎らしく見えないのです。

アプローチ 3(パラメタによるフィルタリング)

最後に試したのは、cropped に含まれるピクセルをフィルタリング(マスキング)するという方法です。具体的には、S > 0.2 で、かつ、V < 0.98 のピクセルだけを抜き出し、それらピクセルだけを使って H, S の平均値を求めます。最後に、cropped 全体の V の平均を求め(アプローチ 2 と同様)、その H, S, V を RGB 値に変換します。これは Gimp で画像を眺め回して試行錯誤で決めたやり方なのですが、思惑としては、ある程度の彩度があって、しかし R, G, B が飽和していないピクセルだけを使って色を求めよう、というものです。)

   # Calculate average brightness from grayscale
    gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY)
    brightness = gray.mean()

    # Convert to HSV and filter by S and V thresholds
    hsv = cv2.cvtColor(cropped, cv2.COLOR_BGR2HSV)
    mask = (hsv[:, :, 1] > 0.2 * 255) & (hsv[:, :, 2] < 0.98 * 255)
    selected_pixels = hsv[mask]

    if len(selected_pixels) > 0:
        avg_h = selected_pixels[:, 0].mean()
        avg_s = selected_pixels[:, 1].mean()
    else:
        avg_h, avg_s = 0, 0

    # Convert back to BGR (values in range 0-255)
    final_hsv = np.uint8([[[avg_h, avg_s, brightness]]])
    final_bgr = cv2.cvtColor(final_hsv, cv2.COLOR_HSV2BGR)[0, 0]

    # Correctly unpack in RGB order
    avg_b, avg_g, avg_r = (final_bgr / 255.0).tolist()

次のような RGB グラフが得られました。

これを使って仮想 LED を明滅させてみたところ、割と本物のロウソクの炎のように見えるようになりました。まだまだ完全とは言えませんが、とりあえずこれで、RNN の訓練作業に移れそうです。

それでは、次回をお楽しみに…。(今年中に間に合うかな?)

お問い合わせはお気軽に!

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。必ず返信致しますが、時々アドレス誤りと思われる返信エラーがございます。返答が届かない場合、大変お手数ではございますが別のメールアドレスで督促頂けますと幸いです。

コメントを残す