デプスカメラを「雄弁なスチルカメラ」として利用する
デプスカメラ Intel RealSense D415 を使ってあれこれを試しているうちにふとこんなことを考えました。
RealSense D400 シリーズの出力からは、(1)一般的な RGB カラー画像 (2)自然光照度の影響を受けにくい赤外線画像、そして、(3)被写体の位置関係が色彩的に表現された深度情報画像を得ることができます。この三種類の情報を照合するとスコープ内の一瞬の状況を多角的にとらえることが可能です。例を示します。
このユニークな特性を以下の想定のもとで「ゆるい見守り」に活かせるのではないかと考えました。
前に人感センサを利用した安否確認のしくみを手がけたことがあります。これは現在もプライベートで利用を続けているのですが、そこにこれらの画像情報が加われば安心感がずっと大きくなりそうです。
Raspberry Pi 3 B+ のセットアップ
上のような形で一式を設置・運用するとすれば圧迫感の小さい小振りなホスト機と組み合わせることが好ましいでしょう。幸い RealSense D400 シリーズは Raspberry PI 3 にも対応しています。
さっそく手持ちの Raspberry Pi 3 B+ の環境設定に着手しました。完了までに当初の想定よりも手がかかった事情もありメモを残しておきます。
「realsense raspberry pi 3」のキーワードで情報を検索すると、上の記事と同様にラズパイで RealSense D400 シリーズを利用するための OS 環境として Ubuntu Mate 16.x を使う記事が多くヒットする。まずはこれらの記事を参考に準備を進めたが、結論として Raspberry Pi 3 B+ で Ubuntu Mate 16.x を起動することはできなかった。不審に思いながら情報を探して目にした「まろにー」様による次の記事の内容は正確だった。
さらに情報をさぐり以下の記事に行き着いた。Raspbian の使用を前提とする内容で、「These steps are for D435 with Raspberry Pi3 and 3+」とある。
- Raspberry 3 B+ 現段階ではUbuntu Mate 16.04.2(Xenial)は起動しない (2018-09-09) - shittaca.seesaa.net
この記事の内容は最終的には正確だった。ただしこうした話題にありがちな落とし穴もいくつかあった。備忘をかねその内容を以下に控える。
- Raspbian(RaspberryPi3) Installation - github.com/IntelRealSense
- 現在 www.raspberrypi.org で配布されている「Raspbian Buster with desktop」には適用できない。記事中の「4.14.34-v7+」に近い旧バージョンを探し「2018-11-13-raspbian-stretch-full (4.14.79-v7+) 」を試したところ成功した
- 一連の手順でのビルドには時間がかかることに注意。手元では試行錯誤分のロスを含めトータルで 20時間程度を要した
- 記事末尾の指示どおりに raspi-config コマンドで OpenGL Driver を有効にすると OpenCV を利用したプログラムでの表示に不具合が生じる。@PINTO 様による以下の記事にこれに関連する記述がみられる
手元では OpenGL よりも OpenCV に馴染んでいるため逆に OpenGL ドライバを無効化 ([raspi-config] - [7.Advanced Options] - [A7 GL Driver] - [G3 Legacy])し、プログラミングには OpenCV を利用している。一方でこの設定では公式の RealSense Viewer 等が激重になるため適宜使い分けが必要。
- デプスカメラRealSenseD435で・・ (記事名を一部省略させて頂きました) - qiita.com/PINTO
【注意点】 OpenGL Driver を有効にすると、OpenCVの 「imshow」 メソッドが正常に動作しなくなる点を受け入れる必要がある。 こちらの記事の実装 のように、OpenCVの 「imshow」 メソッドに頼らないでOpenGLの描画系メソッドだけで描画処理を実装しなくてはならない点に注意。
こういった過程を経て手元の Raspberry Pi 3 B+ で D415 を利用できる状態になりました。手頃なサイズでいい感じです。
![]() |
![]() |
下の動画にはこの環境で公式の Intel RealSense Viewer を実行した様子を収めています。なお、ここでは前掲の事情によりラズパイ側で OpenGL Driver を有効にしています。
D400 からの赤外線投光と深度情報精度との関係
RealSense D400 シリーズは深度計測にふたつの赤外線センサ(カメラ)を用いたステレオアクティブ方式を採用しており、計測精度向上の補助を目的とするドットパターンの赤外線投光を行うことが可能となっています。この投光はデフォルトで有効です。
前述の想定のように今回は RGB, 赤外線, 深度のみっつの画像情報をあわせて捕捉することがポイントですが、この赤外線投光機能は赤外線カメラ経由で得られる画像と深度情報の両方に影響を及ぼします。手元での観察結果からこのことを整理してみます。
まず、赤外線投光を有効化 / 無効化した状態で D415 から採取した各フレームの静止画像例を以下に示します。赤外線画像はふたつの赤外線カメラの左側のものを使用。なお、この製品の赤外線カメラはあくまでも深度計測を目的とするものであり生のままの画像は視覚的に暗いため、OpenCV で明度とコントラストを補正した版を並べて掲げています。(画像はいずれもクリックで大きく表示)
RGB
![]() |
深度
![]() |
赤外線 L
![]() |
赤外線 L(補正ずみ)
![]() |
RGB
![]() |
深度
![]() |
赤外線 L
![]() |
赤外線 L (補正ずみ)
![]() |
上のサンプルから次のように判断しました。
ちなみに、赤外線投光の有効・無効をストリーミングの途中で切り替えることはできません。サンプルを採取するために用意した Python コードを以下に引用します。
#!/usr/bin/env python # -*- coding: utf-8 -*- # # D415_IRemitterTest.py # # for Intel RealSense D415 # # 2020-03 # import pyrealsense2 as rs import numpy as np import cv2 # 赤外線投光の有無 USE_IR_EMITTER = True #USE_IR_EMITTER = False WIDTH = 640 HEIGHT = 480 FPS = 15 # イメージの明度・コントラストを補正 # https://www.pynote.info/entry/opencv-change-contrast-and-brightness def adjust(img, alpha=1.0, beta=0.0): dst = alpha * img + beta return np.clip(dst, 0, 255).astype(np.uint8) def main(): # RealSense ストリーミング開始 pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.depth, WIDTH, HEIGHT, rs.format.z16, FPS) config.enable_stream(rs.stream.color, WIDTH, HEIGHT, rs.format.bgr8, FPS) config.enable_stream(rs.stream.infrared, 1, WIDTH, HEIGHT, rs.format.y8, FPS) profile = pipeline.start(config) device = profile.get_device() depth_sensor = device.first_depth_sensor() if USE_IR_EMITTER: # IR 投光を最大に depth_sensor.set_option(rs.option.emitter_enabled, 1) laser_range = depth_sensor.get_option_range(rs.option.laser_power) depth_sensor.set_option(rs.option.laser_power, laser_range.max) else: # IR 投光を無効に depth_sensor.set_option(rs.option.emitter_enabled, 0) try: while True: # RealSense フレームデータ取得 frames = pipeline.wait_for_frames() depth_frame = frames.get_depth_frame() color_frame = frames.get_color_frame() ir_frame = frames.get_infrared_frame() depth_img_raw = np.asanyarray(depth_frame.get_data()) color_img = np.asanyarray(color_frame.get_data()) ir_img_raw = np.asanyarray(ir_frame.get_data()) depth_img = cv2.applyColorMap(cv2.convertScaleAbs(depth_img_raw, alpha=0.08), cv2.COLORMAP_JET) # IR フレームイメージの明度を調整 ir_img = adjust(ir_img_raw, alpha=2.0, beta=40.0) # 各フレームイメージを表示 cv2.imshow('RGB', color_img) cv2.imshow('IRRaw', ir_img_raw) cv2.imshow('IR', ir_img) cv2.imshow('Depth', depth_img) # キー入力判定 key = cv2.waitKey(1) if key & 0xFF == ord('q') or key == 27: # ESC cv2.destroyAllWindows() break elif key & 0xFF == ord('s'): # フレームイメージを画像出力 cv2.imwrite('ImgRGB.jpg', color_img) cv2.imwrite('ImgIRRaw.jpg', ir_img_raw) cv2.imwrite('ImgIR.jpg', ir_img) cv2.imwrite('ImgDepth.jpg', depth_img) finally: pipeline.stop() if __name__ == '__main__': main()
動体検知について
見守りに際しもっともシリアスな状況と考えられるのは「対象者がまったく動かない状態」でしょう。そのため、室内の明るさにかかわらずスコープ内の動きの有無を適切に把握できるようにしておきたいと考えました。その手段として赤外線カメラからの出力をもとに動体検知を行うことを思い立ちました。
こういった処理を手元で扱った経験はありませんでしたが、ネット上の情報を参考に考え方を整理してまず以下の内容での簡易的な実装を試みました。あくまでもフレーム間の情報の変化量を根拠とする内容で人体検出等の要素は含みません。
※各パラメータは実地で適切な値を割り出す
上の要領を画像と動画の例で示してみます。
1. 前回分のフレーム画像
![]() |
2. 今回分の画像 (体と手を少し動かしてみた)
![]() |
3. 上記 1, 2 画像の差分
![]() |
4. ピクセル値を所定の閾値で 0 と 255 に分ける
![]() |
試作
内容
ここまでの話題を元に以下の想定に基づく内容のアプリケーションを試作しました。MQTT ブローカーには定番の Beebotte を利用します。JavaScript 用の API セットも提供されているため Web ブラウザ上のコンテンツでメッセージを取り回すことが可能です。
メモ
関連する話題を控えます。
以下のように書き換えると表示されます。
http://drive.google.com/uc?export=view&id={ID}
ソースコード
以上を踏まえざっくり用意したプログラムです。
#!/usr/bin/env python # -*- coding: utf-8 -*- # # AnpiRealSense.py # # for Intel RealSense D415 # # 2020-03 # import threading import datetime import time import os import requests import pyrealsense2 as rs import numpy as np import cv2 import paho.mqtt.client as mqtt import json from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive # for RealSense WIDTH = 640 HEIGHT = 480 FPS = 15 # for MQTT MQTT_TOKEN = 'token_w5xxxxxxxxxx' MQTT_HOSTNAME = 'mqtt.beebotte.com' MQTT_PORT = 8883 MQTT_TOPIC = 'Anpi/q01' MQTT_CACERT = 'mqtt.beebotte.com.pem' MQTT_URL_PUB = 'https://script.google.com/xxxx/exec' # for Google Drive ID_RGB = '1YTEcpNyVv5axxxxxxxxxx' ID_IR = '1qt0tzv5-DfJxxxxxxxxxx' ID_DEPTH = '17A8Xybfz6qoxxxxxxxxxx' ID_RGB_PREV = '1sxb67Zspim2xxxxxxxxxx' ID_IR_PREV = '1A9C-9vgo97Kxxxxxxxxxx' ID_DEPTH_PREV = '1oJSOeebzL0exxxxxxxxxx' ID_RGB_MOVE = '1cMLgJB3_zcDxxxxxxxxxx' ID_IR_MOVE = '1rbaVHzpD4m-xxxxxxxxxx' ID_DEPTH_MOVE = '1dSdfAOy4oHqxxxxxxxxxx' ID_IR_DIFF = '1JuZLW1g8eaOxxxxxxxxxx' FN_RGB = 'RGB' FN_IR = 'IR' FN_DEPTH = 'Depth' FN_SUFFIX = '.jpg' FN_SUFFIX_PREV = '_prev.jpg' FN_SUFFIX_MOVE = '_move.jpg' FN_SUFFIX_DIFF = '_diff.jpg' QUEUE = 0 Q_UPDATE = 100 Q_DETECT = 200 # MQTT コネクション確立時コールバック def on_mqtt_connect(client, userdata, flags, respons_code): client.subscribe(MQTT_TOPIC) print('mqtt on_connect') # MQTT メッセージ受信時コールバック def on_mqtt_message(client, userdata, msg): global QUEUE QUEUE = Q_UPDATE print('mqtt on_message') print(msg.topic + ' ' + str(msg.payload)) # イメージに日付時分表示を挿入 def putDateTime(img): dt_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') cv2.putText(img, dt_str, (20, 36), cv2.FONT_HERSHEY_DUPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA ) cv2.putText(img, dt_str, (18, 34), cv2.FONT_HERSHEY_DUPLEX, 1, (255, 255, 255), 2, cv2.LINE_AA ) # jpg ファイル群の世代を更新 def rotateFile(fname, img): fn = fname + FN_SUFFIX fn_prev = fname + FN_SUFFIX_PREV if os.path.exists(fn): if os.path.exists(fn_prev): os.remove(fn_prev) os.rename(fn, fn_prev) cv2.imwrite(fn, img) if os.path.exists(fn_prev): return True return False # Google Drive へ各フレームの jpg データをアップロード def upload_data(id, fname, gdrive): f = gdrive.CreateFile({'id': id}) f.SetContentFile(fname) f.Upload() print('upload_data [' + fname +']') def do_upload(mode, rgb_img, ir_img, depth_img, ir_img_diff, gdrive): print('do_upload start') putDateTime(rgb_img) putDateTime(ir_img) putDateTime(depth_img) if mode == Q_UPDATE: # 任意の時点でのスナップショット if rotateFile(FN_RGB, rgb_img): upload_data(ID_RGB_PREV, FN_RGB + FN_SUFFIX_PREV, gdrive) upload_data(ID_RGB, FN_RGB + FN_SUFFIX, gdrive) if rotateFile(FN_DEPTH, depth_img): upload_data(ID_DEPTH_PREV, FN_DEPTH + FN_SUFFIX_PREV, gdrive) upload_data(ID_DEPTH, FN_DEPTH + FN_SUFFIX, gdrive) if rotateFile(FN_IR, ir_img): upload_data(ID_IR_PREV, FN_IR + FN_SUFFIX_PREV, gdrive) upload_data(ID_IR, FN_IR + FN_SUFFIX, gdrive) elif mode == Q_DETECT: # 動体検知時 cv2.imwrite(FN_RGB + FN_SUFFIX_MOVE, rgb_img) cv2.imwrite(FN_DEPTH + FN_SUFFIX_MOVE, depth_img) cv2.imwrite(FN_IR + FN_SUFFIX_MOVE, ir_img) cv2.imwrite(FN_IR + FN_SUFFIX_DIFF, ir_img_diff) upload_data(ID_RGB_MOVE, FN_RGB + FN_SUFFIX_MOVE, gdrive) upload_data(ID_DEPTH_MOVE, FN_DEPTH + FN_SUFFIX_MOVE, gdrive) upload_data(ID_IR_MOVE, FN_IR + FN_SUFFIX_MOVE, gdrive) upload_data(ID_IR_DIFF, FN_IR + FN_SUFFIX_DIFF, gdrive) requests.post(MQTT_URL_PUB, data={'hoge':'1'}) print('do_upload done') # イメージの明度・コントラストを補正 # https://www.pynote.info/entry/opencv-change-contrast-and-brightness def adjust(img, alpha=1.0, beta=0.0): dst = alpha * img + beta return np.clip(dst, 0, 255).astype(np.uint8) def main(): global QUEUE # init MQTT print('init mqtt start') mqtt_client = mqtt.Client() mqtt_client.username_pw_set('token:%s'%MQTT_TOKEN) mqtt_client.on_connect = on_mqtt_connect mqtt_client.on_message = on_mqtt_message mqtt_client.tls_set(MQTT_CACERT) mqtt_client.connect(MQTT_HOSTNAME, port=MQTT_PORT, keepalive=60) print('init mqtt done') # init Google Drive print('init gdrive start') gauth = GoogleAuth() gauth.CommandLineAuth() gdrive = GoogleDrive(gauth) print('init gdrive done') # RealSense ストリーミング開始 print('rs streaming starting') pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.depth, WIDTH, HEIGHT, rs.format.z16, FPS) config.enable_stream(rs.stream.color, WIDTH, HEIGHT, rs.format.bgr8, FPS) config.enable_stream(rs.stream.infrared, 1, WIDTH, HEIGHT, rs.format.y8, FPS) profile = pipeline.start(config) print('rs streaming started') # IR 投光を最大に device = profile.get_device() depth_sensor = device.first_depth_sensor() depth_sensor.set_option(rs.option.emitter_enabled, 1) laser_range = depth_sensor.get_option_range(rs.option.laser_power) depth_sensor.set_option(rs.option.laser_power, laser_range.max) ir_img_prev = np.empty(0) ir_img_diff = np.empty(0) time_start = time.time() time_deetected = 0 try: while True: mqtt_client.loop(0.1) # RealSense フレームデータ取得 frames = pipeline.wait_for_frames() depth_frame = frames.get_depth_frame() color_frame = frames.get_color_frame() ir_frame = frames.get_infrared_frame() if not depth_frame or not color_frame or not ir_frame: continue depth_img_raw = np.asanyarray(depth_frame.get_data()) color_img = np.asanyarray(color_frame.get_data()) ir_img_raw = np.asanyarray(ir_frame.get_data()) depth_img = cv2.applyColorMap(cv2.convertScaleAbs(depth_img_raw, alpha=0.08), cv2.COLORMAP_JET) # IR フレームイメージの明度を調整 ir_img = adjust(ir_img_raw, alpha=2.0, beta=40.0) # 動体検知 # ノイズよけにストリーミング開始から 10秒程度は看過 if ir_img_prev.size != 0 and time.time() - time_start > 10: # 直近の IR フレームイメージとの差分イメージを取得 ir_diff = cv2.absdiff(ir_img, ir_img_prev) # 明度 80 を閾値に 0, 255 に二値化 ret, ir_img_diff = cv2.threshold(ir_diff, 80, 255, cv2.THRESH_BINARY) cv2.imshow('diff', ir_img_diff) # 255 値ポイントのみの配列を生成 ar = ir_img_diff[ir_img_diff == 255] # 所定の件数以上なら動きありと判定 if ar.size > 200: #print(ar.size) # 前回アップロードから 30分未経過ならスキップ if time.time() - time_deetected >= 1800: print('motion detected') time_deetected = time.time() QUEUE = Q_DETECT # 今回分 IR フレームイメージを次回の比較用にコピー ir_img_prev = ir_img.copy() # 各フレームイメージを表示 cv2.imshow('RGB', color_img) cv2.imshow('IR', ir_img) cv2.imshow('Depth', depth_img) # キー入力判定 key = cv2.waitKey(1) if key & 0xFF == ord('q') or key == 27: # ESC cv2.destroyAllWindows() break elif key & 0xFF == ord('s'): QUEUE = Q_UPDATE # アップロード指示あり if QUEUE != 0: # Google Drive へ各フレームの jpg データをアップロード do_upload(QUEUE, color_img, ir_img, depth_img, ir_img_diff, gdrive) QUEUE = 0 time.sleep(0.2) # 200ms finally: pipeline.stop() print('exit') if __name__ == '__main__': main()
HTML リソース(アーカイブ)
下の動画で使用している HTML ページと実際に撮影した各フレーム画像を静的に再構成したアーカイブを下記へ収めています。
- デモページ ( target="_blank" )
上のデモページの末尾に今回の試作で使用した実際の HTML 記述をコメントとして引用しています(各種 ID はダミーです)。
動作の様子
(tanabe) |
![]() |
オプティカルフローを簡易ジェスチャ認識に利用する
少し前に Intel 製デプスカメラ RealSense D415 と 3DiVi 社製 Nuitrack SDK の組み合わせでジェスチャ認識を試していました。下の動画には三脚に据えた D415 ごしに所定のジェスチャでスマート照明の操作をテストした様子を収めています。
このように期待どおりの反応は得られるのですが、次のことが気になっていました。
このあたりの事情は Kinect も同様のようです。もちろん骨格の検出は腕や手を適切に識別するために必要ですが、一方でもっとラフなものもあればと考えました。日常感覚ではコタツや布団にもぐり込んだまま姿勢を正すことなくスッと指示を出すといったこともできれば便利そうです。そんなわけで試しに簡単なしくみを作ってみることにしました。
考え方として、スコープ内の被写体に所定の水準以上の移動をざっくり検知した場合にその向きを判定することを想定しました。実現方法を考えながら社内でそういう話をしたところ、「OpenCV でオプティカルフローを利用してはどうか?」というレスポンスがありました。手元ではトラッキング API (リンク: @nonbiri15 様によるドキュメントの和訳) を試していたところでオプティカルフローのことは知らずにおり、@icoxfog417 様の次の記事中の比較がちょうどわかりやすくとても参考になりました。
画像間の動きの解析については、様々な目的とそれを実現する手法があります。ここでは、まずOptical Flowがその中でどのような位置づけになるのか説明しておきます。
:
以下は GitHub 上の OpenCV 公式のサンプルプログラムです。これは Dense(密)型で Gunnar Farneback 法が用いられています。
一般的なウェブカメラを PC へ接続してこのプログラムを実行した様子を以下の動画に収めています。スコープ内の被写体の動きが格子点の座標群を起点に捕捉されている様子が視覚的に表現されます。
このサンプルプログラムに上下左右四方向への移動を大きく判定する処理を加えてみました。次の動画の要領で動作します。
加筆したプログラムのソースコードです。まだまだ改良の余地はあるものの今回オプティカルフローの利便性に触れたことは大きな収穫でした。いろいろな使い方ができそうです。
#!/usr/bin/env python # -*- coding: utf-8 -*- # # 高密度オプティカルフローを利用した簡易ジェスチャ認識の試み # # Web カメラ視野内の被写体全般について # Left, Right, Up, Down 四方向への移動を検知する内容 # # OpenCV 公式の下記サンプルに処理を追加したもの # https://github.com/opencv/opencv/blob/master/samples/python/opt_flow.py # # 2020-04 # import sys import time import numpy as np import cv2 # 有効移動量の閾値 THRESHOLD_DETECT = 300 # 切り捨ての閾値 THRESHOLD_IGNORE = 2 # 所定アクション区間内の x, y 方向への総移動量 MOVE = [0, 0] # 移動発生カウンタ COUNT_MOVE = 0 # 直近のジェスチャ検出システム時間 msec TIME_DETECT = 0 def millis(): return int(round(time.time() * 1000)) def put_header(img, str): cv2.putText(img, str, (26, 50), cv2.FONT_HERSHEY_SIMPLEX, 2, (255,0,0), 3) cv2.putText(img, str, (24, 48), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0,255), 3) # 指定イメージに高密度オプティカルフローのベクトル情報を重ねて線描(原作) # あわせてスコープ内の左右上下 四方向への移動状況を判定 def draw_flow(img, flow, step=16): global MOVE, COUNT_MOVE, TIME_DETECT # 当該イメージの Hight, Width h, w = img.shape[:2] # イメージの縦横サイズと step 間隔指定に基づき縦横格子点座標の配列を生成 y, x = np.mgrid[step/2:h:step, step/2:w:step].reshape(2,-1).astype(int) # 格子点座標配列要素群に対応する移動量配列を得る fx, fy = flow[y,x].T # 各格子点を始点として描画する線分情報(ベクトル)の配列を生成 lines = np.vstack([x, y, x+fx, y+fy]).T.reshape(-1, 2, 2) lines = np.int32(lines + 0.5) vis = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # 移動ベクトル線を描画 cv2.polylines(vis, lines, 0, (0, 255, 0)) #for (x1, y1), (_x2, _y2) in lines: # cv2.circle(vis, (x1, y1), 2, (0, 0, 255), -1) # 線分情報配列の全要素について # 始点から終点までの x, y 各方向への移動量の累計を得る vx = vy = 0 for i in range(len(lines)): val = lines[i][1][0]-lines[i][0][0] if abs(val) >= THRESHOLD_IGNORE: vx += val val = lines[i][1][1]-lines[i][0][1] if abs(val) >= THRESHOLD_IGNORE: vy += val # 総移動量に加算 MOVE[0] += vx MOVE[1] += vy # 移動量の累計が所定の閾値以上なら移動中と判断 if abs(vx) >= THRESHOLD_DETECT or abs(vy) >= THRESHOLD_DETECT: # 移動発生カウンタを加算 COUNT_MOVE += 1 # 所定の閾値未満なら移動終了状態と仮定 else: mx = my = 0 if COUNT_MOVE > 0 and \ millis() - TIME_DETECT > 1000: # ノイズ除け # x, y 各方向への移動量の平均を求める mx = int(MOVE[0]/COUNT_MOVE) my = int(MOVE[1]/COUNT_MOVE) # x, y 方向いずれかの移動量平均が所定の閾値以上なら # 左右上下のどの方向への移動かを判定して表示 if abs(mx) >= THRESHOLD_DETECT or abs(my) >= THRESHOLD_DETECT: TIME_DETECT = millis() if abs(mx) >= abs(my): if mx >= 0: put_header(vis, 'LEFT') else: put_header(vis, 'RIGHT') else: if my >= 0: put_header(vis, 'DOWN') else: put_header(vis, 'UP') # 総移動量と移動発生カウンタをクリア MOVE = [0, 0] COUNT_MOVE = 0 return vis def main(): cam = cv2.VideoCapture(0) # 最初のフレームを読み込む _ret, prev = cam.read() # グレイスケール化して保持 prevgray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY) while True: # フレーム読み込み _ret, img = cam.read() # グレイスケール化 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 前回分と今回分のイメージから高密度オプティカルフローを求める flow = cv2.calcOpticalFlowFarneback(prevgray, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0) # 今回分を保持しておく prevgray = gray # flow を可視化 # https://docs.opencv.org/3.1.0/d7/d8b/tutorial_py_lucas_kanade.html """ hsv = np.zeros_like(img) hsv[...,1] = 255 mag, ang = cv2.cartToPolar(flow[...,0], flow[...,1]) hsv[...,0] = ang*180/np.pi/2 hsv[...,2] = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX) rgb = cv2.cvtColor(hsv,cv2.COLOR_HSV2BGR) cv2.imshow('hsv', rgb) """ # 画像にフローのベクトル情報を重ねて表示 cv2.imshow('flow', draw_flow(gray, flow)) ch = cv2.waitKey(1) if ch & 0xFF == ord('q') or ch == 27: # ESC break if __name__ == '__main__': main() cv2.destroyAllWindows()
(tanabe)
xv6にネットワーク機能を実装した
在宅勤務に移行してから1ヶ月半ほど経過しました。通勤という概念が消滅したおかげで午前中から活動できるようになった @pandax381 です。
要約
フルスクラッチで自作した TCP/IP プロトコルスタックを xv6 に組み込み、一通りの機能が動作するようになりました。
I publish the implementation of TCP/IP network stack on xv6. I ported my user-mode TCP/IP stack, which was originally developed for learning, and added the e1000 driver and socket system calls. Some parts are still not enough, but they are working.https://t.co/nht9JDMVbl
— YAMAMOTO Masaya (@pandax381) March 11, 2020
経緯
かれこれ7〜8年くらい経つんですけど、ライフワークとしてお勉強用のTCP/IPプロトコルスタックの開発をほそぼそと続けています。
一般的に、プロトコルスタックはOSの機能の一部としてカーネル内部に存在しているのですが、この自作プロトコルスタックは、作りやすさを重視した都合でユーザ空間のアプリケーション用のライブラリとして実装しています。

最下層に位置するリンクレベルの入出力は PF_PACKET / BPF / TAP を利用するお手軽仕様で、純粋にパケット処理に専念できるようになっています。

やや作りが粗い部分もありますが、Ethernetフレームの送受信からTCPセグメントのやり取りまで全てを自前で処理しています。「プロトコルスタックってどんな作りになっているんだろう」と興味を持った人が、手を出して雰囲気を掴むのに丁度いいくらいのボリュームになっているんじゃないかと思います。
(物理デバイスや論理インタフェースの抽象化とかそれなりに作り込んであるので、興味があったらコード見てください)
毎年、インターンシップを希望する学生を受け入れていて、彼らの頑張りによって DHCPクライアント機能やパケット転送機能、更にはDPDKサポートといった改良が加えられいますが、いずれも1〜2週間という短い期間で成果を出してくれました。
その流れで、昨夏には KLab Expert Camp という合宿イベントを開催しました。
KLab Expert Camp 開催決定!第1回テーマは「TCP/IPプロトコルスタック自作開発」で、講師は僕が担当します。日程は 8/26〜29 の4日間での開催、交通費・宿泊費は全て KLab が負担します。プロトコルスタック自作に興味のある学生のみなさん、一緒に楽しい夏を過ごしましょう!https://t.co/SVyMOzaeq8
— YAMAMOTO Masaya (@pandax381) April 26, 2019
4日間、ひたすらプロトコルスタックを開発するというマニアックな合宿イベントにもかかわらず、全国からたくさんの学生が参加してくれ、実装解説を通じて基本的な仕組みを学んだり、別言語への移植や機能追加に取り組んでくれました。
参加者からのフィードバックがすごく良くて今後も定期的に開催したいなと思う中で、最近の動向を見ていると「自作CPUで動かしている自作コンパイラでビルドした自作OSに自作プロトコルスタックを組み込みたい」というヤバイ人が出現する可能性も十分ありそうな気がしてきます。
少なくとも、ユーザ空間での動作を前提としてたライブラリの状態からOSのカーネルに組み込もうとした際にどんな罠が潜んでいるかを把握しておかないことには、十分にサポートしてあげられないことは目に見えています。そんなわけで「まずは自分でやってみるか」というのが、この取り組みのそもそもの動機です。
手頃なOSを選定
恥ずかしながら、僕はOS自作を嗜んでおらず「自分のOS」を所有していません。30日OS自作本も「いつか読もう」と積んである始末なので、OS自作からはじめていると時間が掛かりすぎてしまします。だからと言って Linux や BSD だと、ベースにするには規模が大きすぎます。
こういった用途には、コンパクトな xv6 が向いていそうな気がします。
CPU自作関連の話題でも頻繁に登場していますし、いまさら説明するまでもないと思いますが、xv6 は Version 6 UNIX (V6) を x86(マルチプロセッサ環境)向けに再実装したものです。MITがオペレーティングシステムの講義用に開発し、MIT以外にも数多くの教育機関が教材として採用しています。
コード量は10,000行程度で、OSの実装にさほど詳しくなくても全体を見渡すことができるボリュームです。充実したドキュメントと先人たちが公開してくれている情報が豊富にあるのも心強いです。
最近は RISC-V 向けの実装に移行したようで、MITの講義資料も最新のものは RISC-V 版がベースになっています。x86 版は 2018 年度以前のアーカイブを参照しましょう。
どこからはじめるか
xv6 にはネットワーク機能が一切含まれていないため、NICのドライバから書く必要があります。自作プロトコルスタックではリンクアクセスはOSの機能(PF_PACKET や BPF)を利用していてドライバにはノータッチだったので、もう一段下の世界へ降りることになります。
xv6 は QEMU 上で動作させながら開発を進めることになるので、NIC は QEMU で使えて情報も豊富そうな e1000 (Intel 8254x) をターゲットにするのが良さそうです。
まずは、ドライバのエントリポイントのダミーを用意して Hello, World! でも出力するようなコードを書き、PCI デバイスのドライバのリストに追加してあげるところからはじめたら良さそう ...と思ったのですが、 探してもそれっぽいコードが全く見当たりません。
はい、xv6 は PCI をサポートしていないのでした。
したがって xv6 で NIC を扱うためには
- PCIバスをスキャン
- 接続されているデバイスを検出
- 対応するドライバを呼び出す
という一連の処理から作り込む必要があり、更にもう一段下の世界へ降りることになるわけです。だんだんと上昇負荷が厳しくなりそうですね。
PCI周りの処理
PCIバスのスキャン とか言うとなんだかすごく難しそうに聞こえますが、I/Oポートを叩いて情報を読む、という処理の繰り返しです。そして、この辺はOS自作界隈のみなさんが素晴らしい情報を公開してくれています。
コンフィグレーション領域ってなんぞ?という状態でしたが uchanさん の資料を読んで PCI を完全に理解しました(してません)。
以下の Linux Kernel を解説したドキュメントも参考になりました。
これらの解説を読みながら少しづつ進めていけば PCI バスをスキャンしてデバイスを検出するコードが書くのはさほど難しくないと思います。なお、xv6-net では PCI の最終的なコードは JOS という別の OS から拝借することにしました。
- https://pdos.csail.mit.edu/6.828/2018/jos.git
JOS は、xv6 と同様に MIT がオペレーティングシステムの講義で使うために開発している OS です。JOS は一部が意図的に削られている未完成のOSであり、xv6 の解説を聞いたあとに、JOS の実装を進めて完成させるという構成のようです。また、xv6 がモノリシックなカーネルであるのに対して JOS はマイクロカーネルという違いもあるようです。
読み比べてみると JOS の方が諸々整理されていてキレイに書かれている気がするものの、全体の雰囲気はかなり似ています。そして、JOS には PCI 周りのコードと e1000 ドライバのスケルトン(ほぼなにも書かれていない空っぽのファイル)が用意されているため、これを使わせてもらうことにしました。
上記の pci.c は JOS のコードそのものです。xv6 に手を出す人は高確率で JOS のコードも読んでいるはずで、それであれば僕が書き散らかしたコードよりも JOS のコードを使わせてもらったほうが読む人が理解しやすいだろうと考えた結果です(コードをそのまま組み込むため、辻褄合わせに少し手間を掛けていたりします)。
pciinit() を main() の中から呼んであげると、起動時に PCI バスをスキャンして接続されているデバイスが検出されます。
PCI: 0:0.0: 8086:1237: class: 6.0 (Bridge device) irq: 0 PCI: 0:1.0: 8086:7000: class: 6.1 (Bridge device) irq: 0 PCI: 0:1.1: 8086:7010: class: 1.1 (Storage controller) irq: 0 PCI: 0:1.3: 8086:7113: class: 6.80 (Bridge device) irq: 9 PCI: 0:2.0: 1234:1111: class: 3.0 (Display controller) irq: 0 PCI: 0:3.0: 8086:100e: class: 2.0 (Network controller) irq: 11
ベンダIDとデバイスIDをキーにドライバのエントリポイントを定義しておくと、デバイス検出時にそれを叩いてくれます。
struct pci_driver pci_attach_vendor[] = { { 0x8086, 0x100e, &e1000_init }, { 0, 0, 0 }, };
NICのドライバ
PCI のデバイスを検出できるようになったら、次は NIC のドライバを書きます。
JOS のソースには kernel/e1000.c と kernel/e1000.h が含まれていますが、どちらも空っぽで、Intel のドキュメントをちゃんと読めという圧が伝わってきます。
参考になりそうなコードがないか探してみると、xv6 の RISC-V 版のリポジトリには NIC の初期化処理まで書かれたドライバの雛形がありました。初期化処理を参考にしつつ、ヘッダファイルはそのまま利用させてもらいました。
- https://github.com/mit-pdos/xv6-riscv-fall19/blob/net/kernel/e1000.c
- https://github.com/mit-pdos/xv6-riscv-fall19/blob/net/kernel/e1000_dev.h
また、大神さん が公開されている本がめちゃくちゃわかりやすくて助かりました。PCIの情報も詳しく書かれていて、こんな情報をタダで読ませてもらって良いのか...と感動しながら e1000 を完全に理解しました(してません)。EPUB版でたら買います。
OSDev.org で紹介されている以下のコードも参考にしました。
他にも、世の中には xv6 向けの e1000 ドライバを書いている人は沢山いるので、github 上でもいくつかプロジェクトを見つけることができます。ただし、どれもポーリングする前提で、割り込みがちゃんと動く状態のものは見つけられませんでした。
一連の作業の中で一番苦戦したのが、パケット受信時に割り込みを発生させることで、1週間くらいハマっていました。ちゃんと書いたつもりでもなかなか割り込みが発生しなく心が折れそうになっていたので、動いたときはめちゃくちゃ嬉しかったです。
(たぶん送受信で使うDMA用のバッファ設定がうまくできていなくて動いていなかったんじゃないかと思っています)
xv6 用の e1000 ドライバが動いたーーー!!!これで自作プロトコルスタックを xv6 へ載せるための準備が整った💪https://t.co/nht9JDMVbl pic.twitter.com/TJYkWVAKAV
— YAMAMOTO Masaya (@pandax381) February 29, 2020
この辺の詳しいことは、また別の記事か薄い本でも書こうかなと思っています。
プロトコルスタック本体の移植
NIC のドライバが動いたので、いよいよ本題のプロトコルスタック本体の移植に取り掛かるのですが、あまり躓くことなくあっさり移植できてしまいまったので、あまり書くことがなかったりします。
pthread を使っていたので mutex を spinlock に置き換えたり、cond をタスクの sleep/wakeup に置き換えたりしたくらいです。あと、タイマーが使えていないので、TCPの再送とかタイマーに依存した処理はまだ動かせていません。
まず、ARP に応答できるようになって
ARPに応答できるようになった!#プロトコルスタック自作#xv6 pic.twitter.com/zjBszDkK6t
— YAMAMOTO Masaya (@pandax381) March 2, 2020
ICMP に応答できるようになって
順調に進んで ping に応答できるようになった!#プロトコルスタック自作#xv6 https://t.co/I7BGOz4jVw pic.twitter.com/0xBivGA9LZ
— YAMAMOTO Masaya (@pandax381) March 3, 2020
UDP で通信できるようになり
自作プロトコルスタック on xv6 の進捗が素晴らしくて UDP 通信に成功した🎉🎉🎉(雑に実装したソケット風のシステムコールを通じてユーザ空間のアプリケーションがカーネル内のプロトコルスタックを利用して通信してる) pic.twitter.com/VUbbeWr0TO
— YAMAMOTO Masaya (@pandax381) March 6, 2020
最終的に TCP も動くようになりました
うぉぉぉ!!!TCP も動いたぞ!!!もともとユーザ空間で動かす前提でこしらえた自作プロトコルスタックが xv6 のカーネル空間で完全に動作してる!!! https://t.co/juL0ntccyY pic.twitter.com/5tDU1T0B8K
— YAMAMOTO Masaya (@pandax381) March 6, 2020
ソケット
カーネル内でプロトコルスタックが動くようになってもソケットがなければユーザ空間で通信アプリケーションを書くことができません。
自作プロトコルスタックにはソケット風のAPIもあるのですが、これは単なるライブラリ関数なのでプロトコルスタックをカーネルに組み込んだ状態ではユーザ空間のアプリケーションから呼びだせません。
そんなわけで、ソケット風 ではなくガチのソケット(関連のシステムコール)を実装しました。
システムコールの追加にあたっては、xv6 の既存のシステムコールの中から似たようなプロトタイプのものを探して同じように実装しています。
socket() で作られるディスクリプタは、もともと存在しているファイルディスクリプタと互換性を持つように作っているので、ソケットのディスクリプタを close() で閉じたり、recv() / senc() の代わりに read() / write() を使うことが出来ます。
単純なソケット通信のプログラムであれば、Linux 用に書いたコードがそのまま動く程度にはちゃんと作っています。
自作プロトコルスタック on xv6 がいい感じに仕上がった!ソケット関連のシステムコールを真面目に実装したのでよくあるこんなコードがそのまま動くようになった!ちゃんとファイルディスクリプタに紐づけているので read/write を使っても動くし fork しても大丈夫!https://t.co/nht9JDMVbl pic.twitter.com/LQNvWwOpj9
— YAMAMOTO Masaya (@pandax381) March 10, 2020
ifconfig コマンド
シェルからインタフェースの状態を確認したりIPアドレスを設定するために、当初は適当なコマンド(ifget / ifset / ifup / ifdown)と対応するシステムコールを作って、その場をしのいでいました。
雑なシステムコールを実装してコマンドでネットワークインタフェースを制御できるようになった💪#xv6#プロトコルスタック自作 pic.twitter.com/T6LZNEtwOK
— YAMAMOTO Masaya (@pandax381) March 5, 2020
ただ、これだとちょっとダサいので、最終的に ifconfig コマンドを作りました。ip コマンドじゃないのは NETLINK の実装はさすがに厳しいからという理由です。
だいぶ雑だけど ifconfig 作って動くようになった(たぶん大幅に書き直すと思うけどとりあえず push した)#xv6#networking https://t.co/TEbKeGWe3m pic.twitter.com/2mh5tVGiuy
— YAMAMOTO Masaya (@pandax381) March 30, 2020
ifconfig は ioctl() を通じてインタフェースの情報を取得/設定しているので、ioctl のシステムコールを追加し、SIOCGXXX や SIOCSXXX をひたすら作り込んでいます。
おわりに
あれもこれもと作り込んでいたら「自作プロトコルスタックを xv6 に移植した」というよりは「xv6 にネットワーク機能を実装した」という気持ちになったので、このようなタイトルになりました。
あと、勢い余って reddit デビューもしました。
reddit でコメントくれた方の自作OSがしっかり作られていて感動したので紹介しておきます。
まだ先の予定を立てるのは難しいですが、イベントが開催できるような状況になったら、また KLab Expert Camp でプロトコルスタック自作の合宿をやりたいと思っています。その際は自作OSへの組み込みもサポートできるように準備しておきますので楽しみにしていてください!
@pandax381
デプスカメラを「バーチャル背景」用 Web カメラとして使う
先日ちょっとしたきっかけから Intel 製 RealSense D415 を買いました。デプスカメラに触れるのは初めてでしたが、Microsoft Azure Kinect の国内販売がまもなく開始される旨もアナウンスされておりこの分野の今後の動向が楽しみです。
試作の内容
さっそくこの D415 をあれこれ試しています。公式の RealSense SDK 2.0 の勉強をかね習作のひとつとして次の内容のプログラムを作ってみました。
静止状態でも常に微妙な揺れを伴う深度情報を RGB 画像切り抜き領域の判定に用いているため静的なクロマキー合成とは異なり境界にノイズが発生しがちですが、デプスカメラ利用の一例として紹介します。
動作の様子
デモ動画:1分4秒 無音
ソースコード
プログラムでは画像処理と表示に OpenCV を併用しています。
// // Intel RealSense D400 シリーズ // // カメラから所定の距離内の RGB 画像を抽出して表示. // スペースキー押下で背景画像を差し替え上の画像と合成する. // // 2020-02 // #include <librealsense2/rs.hpp> #include <opencv2/dnn.hpp> #include <opencv2/highgui.hpp> #include <opencv2/imgproc.hpp> #include <iostream> #include <cmath> #define Width 640 #define Height 480 #define Fps 15 // RGB 画像抽出対象圏内の距離 #define Depth_Clipping_Distance 1.0f // 1メートル // 背景画像ファイル数 #define NumBgImages 8 // Depth スケール情報を取得 float get_depth_scale(rs2::device dev) { // センサ情報を走査 for (rs2::sensor& sensor : dev.query_sensors()) { // 深度センサなら Depth スケール情報を返す if (rs2::depth_sensor dpt = sensor.as<rs2::depth_sensor>()) { return dpt.get_depth_scale(); } } throw std::runtime_error("Device does not have a depth sensor"); } // RGB フレーム中の Depth_Clipping_Distance 圏外を塗りつぶし // 圏内 = 255, 圏外 = 0 のマスクイメージを得る cv::Mat remove_background(rs2::video_frame& video_frame, const rs2::depth_frame& depth_frame, float depth_scale) { const uint16_t* p_depth_frame = (const uint16_t*)(depth_frame.get_data()); uint8_t* p_video_frame = (uint8_t*)((void*)(video_frame.get_data())); // マスク用の Matrix をモノクロで用意 cv::Mat m = cv::Mat(cv::Size(Width, Height), CV_8UC1); // 当該 video_frame の幅, 高さ, 1ピクセルのバイト長 int width = video_frame.get_width(); int height = video_frame.get_height(); int other_bpp = video_frame.get_bytes_per_pixel(); // RGB につき 3 // Depth フレームを走査して RGB フレームの画像情報を加工 // OpenMP で二重 for ループを並列に処理 // VC++ ではコンパイルオプション /openmp が必要 // https://docs.microsoft.com/ja-jp/cpp/parallel/openmp/openmp-in-visual-cpp?view=vs-2019 // https://docs.microsoft.com/ja-jp/cpp/parallel/openmp/d-using-the-schedule-clause?view=vs-2019 #pragma omp parallel for schedule(dynamic) for (int y = 0; y < height; y++) { auto depth_pixel_index = y * width; for (int x = 0; x < width; x++, ++depth_pixel_index) { // 現座標箇所のセンサからのメートル距離を得る auto pixels_distance = depth_scale * p_depth_frame[depth_pixel_index]; // Depth の死角領域 (<=0) および 対象圏外に該当の場合 if (pixels_distance <= 0.f || pixels_distance > Depth_Clipping_Distance) { // RGB フレーム内の対象オフセット auto offset = depth_pixel_index * other_bpp; // 0x999999 で塗りつぶす std::memset(&p_video_frame[offset], 0x99, other_bpp); // 背景部分としてマーク m.at<uchar>(y, x) = 0; } else { // 前景部分としてマーク m.at<uchar>(y, x) = 255; } } } // 作成したマスクイメージを CV_8UC1 から CV_8UC3 に変換して返す cv::Mat mask; cv::cvtColor(m, mask, CV_GRAY2BGR); return mask; } // 所定の画像データをロード cv::Mat loadImage(int n) { char fn[16]; sprintf(fn, "%02d.jpg", n); return cv::imread(fn); } int main(int argc, char * argv[]) try { rs2::log_to_console(RS2_LOG_SEVERITY_ERROR); int photoNumber = 0; // ストリーミング用のパイプライン rs2::pipeline pipeline; rs2::config cfg; cfg.enable_stream(RS2_STREAM_COLOR); // RGB ストリーム cfg.enable_stream(RS2_STREAM_DEPTH); // 深度ストリーム rs2::pipeline_profile profile = pipeline.start(cfg); // このカメラの Depth スケール情報を取得 // "Depth スケール * Depth フレーム内の各ピクセルの値" がセンサからのメートル距離 float depth_scale = get_depth_scale(profile.get_device()); // info rs2::device rs_dev = profile.get_device(); std::cout << "Device Name" << ": " << rs_dev.get_info(RS2_CAMERA_INFO_NAME) << std::endl; std::cout << "Firmware Version" << ": " << rs_dev.get_info(RS2_CAMERA_INFO_FIRMWARE_VERSION) << std::endl; std::cout << "Recomended Firmware Version" << ": " << rs_dev.get_info(RS2_CAMERA_INFO_RECOMMENDED_FIRMWARE_VERSION) << std::endl; std::cout << "Serial Number" << ": " << rs_dev.get_info(RS2_CAMERA_INFO_SERIAL_NUMBER) << std::endl; std::cout << "Product Id" << ": " << rs_dev.get_info(RS2_CAMERA_INFO_PRODUCT_ID) << std::endl; std::cout << "USB Type" << ": " << rs_dev.get_info(RS2_CAMERA_INFO_USB_TYPE_DESCRIPTOR) << std::endl; std::cout << "Depth Scale" << ": " << depth_scale << std::endl; // RGB ストリーム分の align オブジェクトを用意 rs2::align align(RS2_STREAM_COLOR); // 最初の背景画像をロード cv::Mat photo = loadImage(photoNumber); while (1) { // カメラからのフレームセット受信を待つ rs2::frameset frameset = pipeline.wait_for_frames(); // アライメントを RGB ストリーム分のビューポートに揃える frameset = align.process(frameset); // RGB フレームを取得 (video_frame クラスに注意) rs2::video_frame video_frame = frameset.get_color_frame(); // Depth フレームを取得 rs2::depth_frame depth_frame = frameset.get_depth_frame(); // RGB フレーム中の Depth_Clipping_Distance 圏外を塗りつぶし // 圏内 = 255, 圏外 = 0 のマスクイメージを得る cv::Mat mask = remove_background(video_frame, depth_frame, depth_scale); cv::Mat rgbCvMatsrc, rgbCvMatDst; // RGB フレームからピクセルデータを取得し OpenCV のマトリックスに変換 rgbCvMatsrc = cv::Mat(cv::Size(Width, Height), CV_8UC3, (void*)video_frame.get_data(), cv::Mat::AUTO_STEP); // チャネル並びを RGB から BGR に cv::cvtColor(rgbCvMatsrc, rgbCvMatDst, cv::COLOR_RGB2BGR, 0); // 抽出した RGB 画像 //cv::imshow("Src", rgbCvMatDst); // 背景画像 //cv::imshow("pic", photo); // マスクと反転マスク cv::Mat mask_inv, fg, bg, mix; cv::bitwise_not(mask, mask_inv); //cv::imshow("mask", mask); //cv::imshow("mask_inv", mask_inv); // 前景となる抽出画像にマスクをかけて背景色を 0 に cv::bitwise_and(rgbCvMatDst, mask, fg); //cv::imshow("fg", fg); // 背景画像に反転マスクをかけて前景部分を 0 に cv::bitwise_and(photo, mask_inv, bg); //cv::imshow("bg", bg); // 加工した前景と背景をマージして表示 //cv::bitwise_or(fg, bg, mix); cv::add(fg, bg, mix); cv::imshow("Enter", mix); // キー押下チェック int key = cv::waitKey(1); if (key == 32) { // SPACE // 背景画像を変更 if (++photoNumber > NumBgImages) { photoNumber = 0; } photo = loadImage(photoNumber); } else if (key == 'q' || key == 27) { // 'q' or ESC cv::destroyAllWindows(); break; } } return EXIT_SUCCESS; } catch (const rs2::error & e) { std::cerr << "RealSense error calling " << e.get_failed_function() << "(" << e.get_failed_args() << "):\n " << e.what() << std::endl; return EXIT_FAILURE; } catch (const std::exception& e) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; }
メモ:マスク合成の手順
A: 圏内の RGB 画像
![]() |
B: 0, 255 のマスク
![]() |
C: 元画像 & マスク
![]() |
C: + c:
![]() |
a: 背景画像
![]() |
b: 255, 0 のマスク
![]() |
c: 背景 & マスク
![]() |
仮想 Web カメラと組み合わせてみる
上のプログラムはあくまでも習作ですが、ふと、以下のような仮想 Web カメラソフトウェアと組み合わせれば、たとえばテレワーク環境からのビデオミーティング参加時に背後のプライベート空間の露出を避ける目的などに利用できるのではないかと考えました。
ちなみに所属部署ではリモート会議に Google Hangouts Meet を利用しています。上のデモを部内で紹介した折に Zoom にはこれと同様のことをスマートに実現できる「バーチャル背景」機能が用意されていることを知り、この手の需要が少なくないことをあらためて実感しました。
上記の SplitCam を使えば PC 上の所定のウィンドウの任意の領域をローカルのカメラ映像として流すことができます。最新版では領域選択方法が不明でしたが、「SplitCam links to download OLD VERSIONS!」ページから「SPLITCAM 8.1.4.1」をダウンロードして試すと期待する結果が得られました。他のプログラムからは普通の Web カメラとして認識されるため汎用的に扱えそうです。
以下にざっくりと組み合わせの手順を示します。(クリックで大きく表示)
2. ミーティング用クライアントからカメラとして SplitCam を選択
(tanabe)
ひもスイッチでスマート照明を操作する
Philips Hue を代表格とするスマートバルブ(電球)は随所でその利便性がうたわれてきました。手元ではこれまでこの方面の製品には触れずにいましたが、先日 Meross 社の「MSL120」を買ってみました。常用している同社のスマートプラグ MSS110 と同一の公式アプリで利用できることも都合がよく楽しみながら使っています
社内でその話をしたところ冗談まじりに「照明つながりで現代のスマートバルブを昭和期のひも式のスイッチで操作してみたりとか」という話題があり実際に試してみました。せっかくなので "出落ちネタ" として動作の様子を紹介します。制御に ESP32 ボードを使い IFTTT アプレットを呼び出して点灯・消灯を行っています。
![]() |
動画:1分2秒
|
延長用のひもを取りつけ体の位置にあわせて伸縮させたり、風を送って振り子の要領でひもを手元に引き寄せるなど、ひもスイッチは家庭生活に馴染みのよい人間的な柔軟性と便宜をそなえたユーザインターフェイスでした。 スイッチ自体を駆動させるための電力や配電を必要とせず素朴なしくみでありながらある程度の遠隔操作を実現できるメリットは大きく、今後もこの形式のスイッチが姿を消すことはないでしょう。誰にでも扱うことのできる間口の広さは家電製品のスマート化の進む今の時代にも有用かもしれません。
(使用した資材)
(tanabe)