デプスカメラを「雄弁なスチルカメラ」として利用する
デプスカメラ 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) |
![]() |