このキャンプは、主にこれから就職活動を始める学生を対象にサーバーサイド開発を体験してもらい、今後の進路を考える上で参考にしてもらうことを目的としています。 そのため、Pythonでのある程度のプログラミング経験は前提としつつ、SQLやSSHなどを触ったことがない方でも参加できるように講義や課題を準備していました。
題材として、実際に遊べるリズムゲーム(音ゲー)を用意しています。
このゲームにはユーザー登録機能と、複数人で同一曲を同時にプレイする機能があります。この2つの機能のために、参加者にはサーバーサイド・アプリケーションを実装してもらいます。
開発環境として GitHub Codespaces を利用しました。 Codespacesのおかげで、参加者の利用するOSやターミナル、SSH等に依存せずに、すぐに開発に取り掛かれる環境を用意できました。 初日はとりあえず環境を動かして軽く触るまでにする予定だったのですが、全くトラブルが起きなかったので2日目の講義の一部を急遽前倒しで実施することになったほどです。
Codespacesは現在のところ個人利用が限定ベータ版ですが、申し込みして有効になれば無料で利用できます。参加者はキャンプ終了後も自分のリポジトリでCodespaceを作り直して開発を続けることができます。
言語はKLabのサーバーサイドでも使われている言語であるPythonを使いました。 一方で、フレームワークはKLabで使っているFlaskではなく、APIサーバーに特化したフレームワークとして FastAPI を選びました。 理由は、人気があり、とっつきやすいことです。特にType hintingを使って定義したデータモデルを使ってシリアライズとデシリアライズを自動でしてくれるので、複雑なことをしないのであればコントローラー部のコードがとても少なくて済みます。
キャンプで一番体験してもらいたかった事がSQLを使った開発だったので、KLabが主に利用しているMySQLを使いました。 CREATE TABLE, INSERT, SELECT, UPDATE, DELETE 文などごく基本的なクエリの文法を講義で紹介し、参加者には自力でクエリを書いてもらいました。
また、講義ではインデックスやロックの必要性や適切に扱う難しさを紹介しました。実装が早かった参加者は、実際にそこまで挑戦されていました。サーバーサイド開発者が普段どういった問題を考え解決しているのか、少しでも伝わったら幸いです。
SQLを読み書きする体験をしてもらいたかったので、ORMは使いませんでした。 しかしPythonからMySQLへ接続する低レベルのライブラリを直接使うのはさすがに問題があるので、SQL toolkitとして SQLALchemy の Core 部分だけを利用しました。
初めての使う技術を学びながら開発するためには、試行とフィードバックのサイクルを高速に回せるインタラクティブシェルが役に立ちます。 SQLを書くために mysql コマンドのシェルを、SQLAlchemyの使い方を調べるために ipython を使ってもらいました。
このキャンプのアイデアを持ちかけられたとき、「僕だけではショボいものしかできないから、ある程度見栄えのするゲームを作ってほしい」と返事していました。
すると、本当にゲーム開発者がそのために音ゲーを作成してくれました。さらにクリエイティブ部門も巻き込んで社内コンペが行われ、デザインやかわいいキャラクターまで作り込んでもらいました。関わってくださった皆様、本当にありがとうございます。
このゲーム単体で配布しても多くの人に楽しんでもらえそうなクオリティなのですが、残念ながらサーバーサイドとセットにならないと動かすことができません。このゲームは参加者だけの特典になります。
参加者に書いていただいたBlog記事を紹介します。
また、キャンプ中の様子を #KLabServerSideCamp というハッシュタグをつけてツイートしてくれています。
Codespacesの環境構築はいまいちベストプラクティスがわからずかなり試行錯誤しましたが、参加者が環境構築でつまづくことがなく、キャンプ終了後もCodespacesが利用できれば簡単に同じ環境を使えるので、採用して大正解でした。
音ゲーは本当によくできていて、音ゲーが好きな参加者が実装の間に(実装そっちのけで?)やりこんでくれていました。社内でたくさんの人を巻き込んで作ってもらったので、楽しんでもらえて良かったです。
あと、普段はSound Onlyでしかミーティングしてないので、インターン最終日の懇親会で、ビデオ通話で愛犬を見せびらかせたのが個人的に良かったです。
講義資料に足りない点があったり、APIを叩いて結果をprintするだけのテストコードにリクエストパラメータが1つ足りなかったりして、幾つかつまづきの原因を作ってしまったのが私の反省点です。 年末年始で忙しい中、一緒にサポートしてくれた開発者のメンバーはありがとうございました。
冒頭で述べた通り、今春3月中旬ごろに第2回を計画しています。この記事や、参加者のBlog、ツイートを見て興味が湧いた方はぜひご応募ください。(エントリー最終締切日は、1/24(月)23:59までです)
@methane
]]>RealSense D400 シリーズの出力からは、(1)一般的な RGB カラー画像 (2)自然光照度の影響を受けにくい赤外線画像、そして、(3)被写体の位置関係が色彩的に表現された深度情報画像を得ることができます。この三種類の情報を照合するとスコープ内の一瞬の状況を多角的にとらえることが可能です。例を示します。
このユニークな特性を以下の想定のもとで「ゆるい見守り」に活かせるのではないかと考えました。
前に人感センサを利用した安否確認のしくみを手がけたことがあります。これは現在もプライベートで利用を続けているのですが、そこにこれらの画像情報が加われば安心感がずっと大きくなりそうです。
上のような形で一式を設置・運用するとすれば圧迫感の小さい小振りなホスト機と組み合わせることが好ましいでしょう。幸い 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 を有効にしています。
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 記述をコメントとして引用しています(各種 ID はダミーです)。
(tanabe) |
このように期待どおりの反応は得られるのですが、次のことが気になっていました。
このあたりの事情は 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()
フルスクラッチで自作した 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」を所有していません。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バスのスキャン とか言うとなんだかすごく難しそうに聞こえますが、I/Oポートを叩いて情報を読む、という処理の繰り返しです。そして、この辺はOS自作界隈のみなさんが素晴らしい情報を公開してくれています。
コンフィグレーション領域ってなんぞ?という状態でしたが uchanさん の資料を読んで PCI を完全に理解しました(してません)。
以下の Linux Kernel を解説したドキュメントも参考になりました。
これらの解説を読みながら少しづつ進めていけば PCI バスをスキャンしてデバイスを検出するコードが書くのはさほど難しくないと思います。なお、xv6-net では PCI の最終的なコードは JOS という別の OS から拝借することにしました。
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 }, };
PCI のデバイスを検出できるようになったら、次は NIC のドライバを書きます。
JOS のソースには kernel/e1000.c と kernel/e1000.h が含まれていますが、どちらも空っぽで、Intel のドキュメントをちゃんと読めという圧が伝わってきます。
参考になりそうなコードがないか探してみると、xv6 の RISC-V 版のリポジトリには NIC の初期化処理まで書かれたドライバの雛形がありました。初期化処理を参考にしつつ、ヘッダファイルはそのまま利用させてもらいました。
また、大神さん が公開されている本がめちゃくちゃわかりやすくて助かりました。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
シェルからインタフェースの状態を確認したり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
さっそくこの 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 カメラソフトウェアと組み合わせれば、たとえばテレワーク環境からのビデオミーティング参加時に背後のプライベート空間の露出を避ける目的などに利用できるのではないかと考えました。
ちなみに所属部署ではリモート会議に Google Hangouts Meet を利用しています。上のデモを部内で紹介した折に Zoom にはこれと同様のことをスマートに実現できる「バーチャル背景」機能が用意されていることを知り、この手の需要が少なくないことをあらためて実感しました。
上記の SplitCam を使えば PC 上の所定のウィンドウの任意の領域をローカルのカメラ映像として流すことができます。最新版では領域選択方法が不明でしたが、「SplitCam links to download OLD VERSIONS!」ページから「SPLITCAM 8.1.4.1」をダウンロードして試すと期待する結果が得られました。他のプログラムからは普通の Web カメラとして認識されるため汎用的に扱えそうです。
以下にざっくりと組み合わせの手順を示します。(クリックで大きく表示)
2. ミーティング用クライアントからカメラとして SplitCam を選択
社内でその話をしたところ冗談まじりに「照明つながりで現代のスマートバルブを昭和期のひも式のスイッチで操作してみたりとか」という話題があり実際に試してみました。せっかくなので "出落ちネタ" として動作の様子を紹介します。制御に ESP32 ボードを使い IFTTT アプレットを呼び出して点灯・消灯を行っています。
|
動画:1分2秒
|
延長用のひもを取りつけ体の位置にあわせて伸縮させたり、風を送って振り子の要領でひもを手元に引き寄せるなど、ひもスイッチは家庭生活に馴染みのよい人間的な柔軟性と便宜をそなえたユーザインターフェイスでした。 スイッチ自体を駆動させるための電力や配電を必要とせず素朴なしくみでありながらある程度の遠隔操作を実現できるメリットは大きく、今後もこの形式のスイッチが姿を消すことはないでしょう。誰にでも扱うことのできる間口の広さは家電製品のスマート化の進む今の時代にも有用かもしれません。
(使用した資材)
SSR にはわずかな電流で制御できる手軽さがあり、機械式のリレーには絶縁性等での優位性があります。また、単品の機械式リレーをマイコンで取り回す際の手かずは必要な要素がまとめて実装されたリレーモジュールを利用することで省略できます。 このあたりの話題はネットを検索すると様々な実例とともに紹介されています。ただ、私自身はこういったものを「自作」するたびに一抹の不安を感じていました。上の 4年前の記事には次のように書いています。
個人的にはこのように高電圧を扱うものは本当はあまり自作したくないです。その方面に素養がなくても部品を揃えれば作るのは簡単ですが、「手作りの楽しさ」などよりも安全性がもっとも重要ですから、手頃な価格で堅牢な完成品を入手できるならそれを使いたいというのが正直なところです。残念ながら今のところそういう商品は見当たらないようですが(需要はあると思うのですが・・)、 :
こういった事情により単純なトリガー信号を送るだけで所定の機器への給電を取り回してくれるブラックボックス的な完成品をひとつの理想形としてイメージしていたのですが、すでにそういう製品が登場していることを最近知りました。Kickstarter 発の「IoT Relay」という米国の製品です。これはまさに上記のような需要に対応する内容で構成された "リレーの堅牢なラッパー" です。現物の写真を以下に示します。
残念ながら今のところ IoT Relay は日本の PSE 認定を受けておらず国内では販売されていません。個人的な好奇心から先日この製品を調達し手元で試してみました。今回はそのレポートです。
ちなみに、所定の機器への給電にはこうしたローカルなリレー類を取り回すばかりではなく、近年手頃になったスマートプラグを利用する選択もあります。ただし、スマートプラグにはインターネット接続環境が必須であることに加え、目の前にある機器を扱う際にも「遠回り」の結果として生じる微妙な反応の遅れに違和感を覚えるケースもあります。一方、スマートプラグには遠隔地の機器への給電制御にも対応できる大きなメリットがあります。そのため、両者は適材適所で使い分けるべきでしょう。
Kickstarter 上のプロジェクトページ
SparkFun での販売ページ
Do you want to control a standard wall outlet device with your microcontroller, but don’t want to mess with the high-voltage wiring? The IoT Power Relay is a controllable power relay equipped with four outputs that help you create an Internet of Things project with safe, reliable power control. With the IoT Power Relay you can easily control the power going to a device with an Arduino, Raspberry Pi or other single-board computer or microcontroller. It provides an alternative to the Power Switch Tail.
みらい翻訳 結果
The IoT Power Relay is designed to allow you to safely control an outlet device that operates at 3--48VDC or 12--120VAC. Each IoT Power Relay features a single input (from the included C13 power cable) to four outputs: one normally on, one always on, and two normally off. :
マイクロコントローラで標準的なコンセントデバイスを制御したいが、高電圧配線には手を出したくないと思うだろうか?IoT Power Relayは4つの出力を備えた制御可能なパワーリレーで、安全で信頼性の高い電源制御を備えたIoTプロジェクトの構築に役立ちます。IoT Power Relayを使えば、ArduinoやRaspberry Piなどのシングルボードコンピュータやマイクロコントローラを使って、デバイスへの電力供給を簡単に制御できる。電源スイッチテールの代わりに使用できます。
IoT Power Relayは、DC3~48Vまたは12~120VACで動作するコンセントデバイスを安全に制御できるように設計されています。各IoT Power Relayには、単一の入力(付属のC13電源ケーブルから)から四つの出力があります。一つは通常オン、一つは常時オン、二つは通常オフです。 :
Amazon.com での販売ページ ("does not ship to Japan.")
※ レビュー欄では評価の高さに加え好意的かつ長文のコメントが多く見受けられる。今のところ代替品の存在しないこの製品に対する利用者の関心の高さの一端が窺える
(2019-10-03 時点のスクリーンショット)
手元で IoT Relay の存在を知った 2019年7月の時点では SparkFun をはじめ主だったディストリビュータのサイトにおいても Amazon.com においてもことごとく在庫切れの状態だった。製造終了の可能性も想像しつつ SparkFun へ入荷通知をリクエストしておいたところ 2019-09-05 に下のメールが届いた。
本体の販売価格は既知のとおり 米 $26.95 であり、とりあえず購入手続きを進めてみると日本への送料は最安の「Economy(1-4 Weeks)」で $12.02 とのこと。微妙に悩ましい金額だがしばらく考えた末に PayPal 経由で $38.97 を支払った。
2019-09-06 にニュージャージー州ニューアークから発送され 2019-09-19 に自宅の郵便受けに届いた。スペックは外箱に記載されており説明書の類は付属しない。現物を見れば使い方がわかるように工夫されている。
(クリックで可視大縦表示)
|
(本体に印字された謎マーク:後述) |
以下の動画には IoT Relay のよっつのプラグ差込口それぞれに LED ライトを装着し ESP32 ボードから High, Low のトリガー信号を送って反応をみた様子を収めている。
動画: 35秒
関連して米アマゾンにはこういう Q&A も。上の事情は米国人にとっても「?」な様子。
同じくレビュー欄より。
Is this unit UL listed?
Showing 1-2 of 2 answers
UL について
This is a well designed and constructed product, but be aware that it is not yet UL listed. My only suggestion for improvement would be to add holes or tabs for secure mounting. :
UL LLC(英語: Underwriters Laboratories以下UL)は、アメリカ合衆国イリノイ州ノースブルックに本拠を構え、試験、検査および認証を行う企業。認証企業として、世界10位前後の規模を持つ。 :
今回ピックアップした IoT Relay は世界中の Maker の潜在的な需要のひとつを的確に反映した良い製品だと思います。ただし、利用者への安全性の提供を最大のアピールポイントとする一方で、現時点においてこの製品自体の米本国内での一般的な安全基準への適合状況が判然としない状況にはいささか自己矛盾の感もあり、その点がマイナスイメージにつながりかねない懸念もあります。今後その方面の整備が適切に行われればこの製品は IoT, スマートホームの波に乗りよりメジャーな存在になり得るかもしれません。特定の製品が目立つ形で成功を収めれば同等(あるいはそれ以上)の機能を持つ製品がより低価格で登場しジャンル化して普及の広がるケースは多々あり、そうした流れがいずれ PSE 認定製品の出現につながる可能性も想定されます。そういった期待を含め、この製品の今後の動向には随時目を向けていきたいと考えています。
KLab Expert Camp は、技術的に深いテーマに取り組んでいる学生の発掘・育成を目的とした KLab の新しい取り組みです。
記念すべき第1回のテーマは「TCP/IPプロトコルスタック自作開発」で、以下のような触れ込みで開催しました。/p>
「OSを作ってみたい」「コンパイラを作ってみたい」と思う人が多いように「プロトコルスタックを作ってみたい」と思う人も多いのではないでしょうか。今回の KLab Expert Camp のテーマは、そんな皆さんにピッタリの「TCP/IPプロトコルスタック自作開発」です。担当講師が開発している教育用のプロトコルスタック(https://github.com/pandax381/microps)を教材に、Ethernetフレームを組み立てて送受信するところから ARP、IP、ICMP、UDP、TCP などのプロトコルを全て自分の手で実装することで、これまでブラックボックスだったプロトコルスタックの処理を紐解いていきます。
参加にあたっては、開発環境やプログラミング言語に関する初歩的な知識は身につけていることを期待していますが、期間中は KLab の講師陣が手厚くサポートしますのでご安心下さい。また、意欲的な参加者においては、期間中に追加機能の実装や組み込み機器への移植などへのチャレンジングな取り組みも支援します。
また、参加者には各自の取り組みたい内容に応じて2つのコースの何れかを選択してもらう形をとりました。
【基本コース】 教材となるリファレンス実装の解説を通じてプロトコルスタックそのものへの理解を深める
【応用コース】 リファレンス実装の別言語への移植や機能追加など各自がテーマを決めて取り組む
定期的にこんなツイートをしながら、年に1~2人のペースで前途有望な学生を低レイヤ沼に引きずり込んでるんですけど、たまたま5人くらいの学生が同じタイミングでリアクションしてくれまして。それだけの反響があるのは嬉しいものの、個別に対応してあげるのはおじさんの体力的にちょっと厳しそうだなという感じもあって「もういっそのこと全員集めて一緒にやればいいのでは???」という考えに至ったわけです。
ネットワークプログラミングやりたい学生からのインターン応募いつでもお待ちしてますよ。TCPを自力で再実装したりオレオレトランスポートプロトコルやプロトコルスタックそのものを作ったり..楽しそうでしょ?一緒に面白いもの作ろうよ!
— YAMAMOTO Masaya (@pandax381) April 17, 2018
そして、インターン関連をとりまとめている人事の担当者に「こんな感じで何人か集めてやりたいんだけど」と雑に話をしたところ「イイっすね!なんならもう少し集めてイベントにしちゃいます???」みたいな凄いノリの良い返事をもらって、1週間くらい経ったら「例のイベントの企画、Go出ました!」と、あっさり開催が決まってしまった感じです。
取り急ぎ告知に向けてイベントの名称を決めようということになったのですが、これが結構難しくて... パッと思いつくものはだいたい既に他で使われてるんですよね。Expert Camp は僕の出した案なんですけど「Expert...ちょっとイキりすぎでは...?」みたいな意見もあったりして。最終的には「カッコ良さそうだからいいじゃん」「僕(自称)エキスパートだから無問題」という感じで落ち着きました。
そんなこんなで漕ぎ着けたイベント告知が以下のツイートです。
KLab Expert Camp 開催決定!第1回テーマは「TCP/IPプロトコルスタック自作開発」で、講師は僕が担当します。日程は 8/26~29 の4日間での開催、交通費・宿泊費は全て KLab が負担します。プロトコルスタック自作に興味のある学生のみなさん、一緒に楽しい夏を過ごしましょう!https://t.co/SVyMOzaeq8
— YAMAMOTO Masaya (@pandax381) April 26, 2019
万人に向けたイベントでもないし、SNSを通じてニッチな層に届けばいいやくらいの軽い気持ちだったので、イベントの告知は僕の個人アカウントによる上記のツイートのみです。 この時点ではマジでどのくらい集まるのか未知数で「追加で数名集まったらわいわい楽しくできそうだけど、果たして応募してくれる人はいるのだろうか...」と不安な気持ちでいっぱいでした。
そんな不安をよそに、つよつよの学生達や「拙者、この分野は素人でござるが」みたいなこと言い出しそうな強い大人の方々にもRTされ、それなりに拡散されることになりました。面白そう!と反応してくれたみなさん、本当にありがとうございました。
そして、いざ蓋を開けてみると総勢20名以上の学生がエントリーしてくれ、最終的に13名の参加者が決まりました。 募集しておいてなんですが「世にはプロトコルスタックを自作したい学生がそんなにいるのか...」と正直びっくりしましたね。
(多数エントリーいただいて嬉しかった一方で、運営のリソース的に希望者全員を受け入れることができず、参加者を絞らなければならなかった点はすごく心残りです。残念ながら今回は参加できなかった方も次回の開催につなげることができたらその際はまたエントリーして欲しいなと思います)
告知から開催まではちょうど4ヶ月くらいあったので、そのあいだに人事の担当者と細かなことを決めつつ準備を進めていきました。といっても、僕が雑に「あれやりたい」「これやりたい」と言ったものを、イベント慣れしている人事の担当者がよしなにアレンジして的確に落とし込んでくれるという連携プレーで進めていきました。だいぶ無茶振りもしたと思うのに、いい感じでまとめてくれて本当にありがとうございました。
以下は個人的に気に入っていて満足しているポイントです。
どれも過去に自分が参加したり運営に携わったイベントで「これ良かったなぁ」「嬉しかったなぁ」と印象に残っていたものを盛り込んでもらいました。 参加者からのアンケートでも「お弁当おいしかった!」とか「記念に残るものがもらえて嬉しかった!」とか、それなりに喜んでもらえていたようで、すごく嬉しいです。
あとは、教材として使う予定の「リファレンス実装」をもう少し理解しやすい作りに改良したり、解説用の資料を作ったりと、なかなか忙しくギリギリまで準備を進めていました。
運営スタッフや参加者が以下のハッシュタグを付けてツイートしてくれているので、これらのタイムラインを追ってもらうと、どんな雰囲気で開催されていたのか分かる思います。(ごはんのツイートが多いので飯テロ注意)
まず、これだけの人数の参加者に集まってもらえたことが驚きと共に一番うれしかったです。そして、皆さん起床ミッションに失敗することもなく初日から最終日まで黙々と開発に取り組んでくれていたのが本当に凄いと思いました。
あと、このイベントを開催した目的のうちの1つでもあるんですけど、参加者同士の交流が活発に行われていたようで良かったです!尊い。いや、これまで個別にインターン生として受け入れていた学生達とのやり取りの中で「ニッチな分野になるとなかなか周りで同じレベル感で話せる友達や仲間がいなくて寂しい(とくに地方の場合)」という話が上がることが多かったんですよ。なので、今回のイベントでは全国各地から同じニッチな分野に興味を持っている学生が集まる絶好の機会だし、そういった交流の場としても機能して欲しいなと思っていたのでした。
このイベントに参加して「TCP/IPを完全に理解した」となった方、その先に進んで「TCP/IPまるでわからん」となった方「TCP/IPチョットデキル」の域に達した方、得られたものはそれぞれ異なると思いますが、参加者全員に「楽しかった」と思ってもらえていたら開催した甲斐があったかなと思うし、第2回はもっとクオリティ高くヤルぞ!という気持ちになります。
もともとこのイベントは僕が一人で講師を務めるつもりで企画がスタートしたのですが、予想を上回る参加人数となったことから、社内のエンジニアに協力を仰いで講師やサポート役を引き受けてもらいました。
また、社内からだけではなく外部の方にも講師役を引き受けて頂きました。
チューターお待ちしています🤗
— YAMAMOTO Masaya (@pandax381) June 7, 2019
ねりさんはプロトコルスタック自作クラスタとして以前から認識していて、#tcfm のミートアップでのLTやサイボウズ・ラボユースでの活躍を見て「ヤバイ人がいる」とウォッチしていたのですが、こんな雑な絡み(なんとこれが初コンタクトなんですよ)にも関わらず、快く引き受けていただき本当にありがとうございました。
僕はR&D部門に所属しているものの、学問として研究をしたことがないため、研究者の立場として参加者に対してアドバイスや相談に対応してくれていたのはものすごく心強かったし、めちゃくちゃカッコいいなと感じました。
他にも、めんどくさい細々としたタスクを含めキッチリこなしてくれた人事の担当者や、仕事の合間にIDカードのデザインを引き受けてくれたデザイナーさんなど運営に関わってくれたみなさん、ほんとうにありがとうございました!
最後に、観測できている範囲で参加してくれたみなさんが書いてくれたブログ記事へのリンクを貼っておきます。
TCP/IP プロトコルスタックを自作した - kawasin73のブログ
KLab Expert Campに参加したよ。 - よくきたわね、いらっしゃい
KLab Expert Camp に参加しました - veqcc’s diary
KLab Expert Camp にチューターとして参加した - Around The Computer
KLab Expert Campに参加してきました - teru_0x01.log
第1回 KLab Expert Campに行ってきました(TCP/IPプロトコルスタック自作インターン) - 迫真の氷結晶
]]>自宅で株式会社ピクセラ様による下記の TV チューナを使っています。
コンパクトでありながら基本性能がとてもしっかりしていることが気に入りプライベートで愛用している製品です。ただ、このチューナは付属のリモコンには機敏に反応するものの、自宅で長年いろいろな機器の操作に使っている手持ち式の某学習リモコンの信号にはほとんど反応しないことを不思議に思っていました。今から 1 年ほど前に導入したあるメジャーなスマートリモコン製品からの操作においても同じ状況だったのですが、すでに純正リモコンでの操作に馴染んでいたこともありあまり気にかけずにいました。
その話題とは無関係に先日高齢の身内のためにできるだけシンプルな学習リモコンが必要になり国外のマーケットから写真の 6 ボタンのみの安価な学習リモコンを調達しました。メーカーは不明、多少割高ながら Amazon.co.jp のマーケットプレイスにも出品されているようです。
このスティック型のリモコンの動作確認を行う過程でふと上のチューナを試してみたところ、これまでの経験が嘘のようにあっさりと反応しました。この結果に驚き、たまたま最近赤外線通信の勉強がてらに ESP32 ボードを使って試作中だった学習リモコン(※)で試してみるとその結果も OK でした。
(※)この試作は所定のリモコン信号をサンプリングした結果をそのまま信号の再現に利用する素朴な内容のものです
この違いは一体何によるものなのか? にわかに興味が湧いてきました。今回はこの件について調べた内容とその結果を紹介します。手元ではこういった視点で事情を掘り下げた例をまだ目にしたことがありませんが、興味深い結果が得られました。
一連の実験での題材として、チューナ付属のリモコンの「電源」ボタン押下により照射される信号を利用する。
この信号を以下のよっつの学習リモコンに記憶させ、これらによって再現された信号の内容を検証する。
なお、ごのチューナのリモコン信号の形式は家製協フォーマットだった。
まず、各リモコンの信号の波形を確認する。オシロスコープで信号の全体像を観察する要領。以下に各信号をサンプリングした結果のデータとそれを波形の形にプロットした図、さらにそれぞれを元リモコンの信号波形に重ね合わせた図を示す。
ここでは以下の点に注目したい。
この結果から、手元のチューナに A, B の信号が受けいれられ、C, D の信号が弾かれる現象の主因は信号全体のタイミングのずれではないことが推察される。
前項ではまず各信号の全体の波形に注目したが、機器側の反応が異なる原因をそこから探り当てることはできなかった。別の評価軸として、それぞれの信号を構成するパルス群の粒度に目を向けることを思い立った。そのきっかけは、先だって赤外線リモコン信号からビットデータの抽出を試みていた折に次の点が気になったことにある。
赤外線発光素子から発信された信号を受光素子経由で受信した結果の信号に含まれるパルスの幅には理論値から外れた「揺れ」の要素がつきものであるため、そこからデータを読み取る際にはパルス幅および ON / OFF パルス比率の理論値とのずれを見越した許容範囲の設定に注意が必要となる。この許容範囲を逸脱したパルスからは適切にデータを読み取ることができないためその線を疑った。
残念ながら手元では今のところ家製協フォーマット規約の一次資料の発掘には至っていない。代わりに、このフォーマットを読み解くための情報源として、赤外線リモコンの信号形式に関する話題においてしばしば言及される次の貴重な資料を参照させて頂いている。
これらの資料から、家製協フォーマット信号に含まれるビットデータは以下の構成であることが解釈される。
そのため、家製協フォーマット信号に含まれる各パルスの品質の良否は次のふたつの尺度で客観的に判定することができると考えた。
以上の内容にもとづき、まずこのチューナの純正リモコン信号のサンプリングデータをもとに、そこに含まれる揺れの要素を排除し理論値に置き換えた「理想形」の信号データを構成することにした。その内容と各リモコンの発する信号データを比較すれば一連の評価を行う上で便宜があるだろう。
元信号のデータに含まれる揺れを取り除き理論値にそって 1T = 425 マイクロ秒, 3T = 1,275 マイクロ秒に置き換えたデータとそれをプロットした図を以下に示す。
A: 生データ:TSV 形式
B: 波形 |
|
C: データ中のパルス長を順番に 1T で除算した結果
|
D: データに出現するビット値 0, 1 を構成する後続パルス長と先行パルス長の比率
|
C: のヒストグラム (生データ:TSV 形式)
|
D: のヒストグラム (生データ:TSV 形式)
|
A: 生データ:TSV 形式
B: 波形 上の元信号(青)と理想形(赤)との重ね合わせ。このようにかなり理想形に近い。 |
|
C: データ中のパルス長を順番に 1T で除算した結果
|
D: データに出現するビット値 0, 1 を構成する後続パルス長と先行パルス長の比率
|
C: のヒストグラム (生データ:TSV 形式)
|
D: のヒストグラム (生データ:TSV 形式)
|
評価
波形の全体像が理想形にきわめて近いことに驚いた。パルス幅およびパルス比の粒度に注目。これが純正リモコンのクオリティ。
A: 生データ:TSV 形式
B: 波形 |
|
C: データ中のパルス長を順番に 1T で除算した結果
|
D: データに出現するビット値 0, 1 を構成する後続パルス長と先行パルス長の比率
|
C: のヒストグラム (生データ:TSV 形式)
|
D: のヒストグラム (生データ:TSV 形式)
|
評価
パルス幅・パルス比とも元信号と同等(以上?)に良好。入手以来漠然と品質の高さを感じていたが、今回の調査を通じてこの製品が廉価であるにもかかわらず非常に優れた学習リモコンであることが客観的に明らかになった。
A: 生データ:TSV 形式
B: 波形 |
|
C: データ中のパルス長を順番に 1T で除算した結果
|
D: データに出現するビット値 0, 1 を構成する後続パルス長と先行パルス長の比率
|
C: のヒストグラム (生データ:TSV 形式)
|
D: のヒストグラム (生データ:TSV 形式)
|
評価
おおむね元の信号に近いパルス品質を確保できている。自作の信号が受けいれられてホッとしました。
A: 生データ:TSV 形式
B: 波形 |
|
C: データ中のパルス長を順番に 1T で除算した結果
|
D: データに出現するビット値 0, 1 を構成する後続パルス長と先行パルス長の比率
|
C: のヒストグラム (生データ:TSV 形式)
|
D: のヒストグラム (生データ:TSV 形式)
|
評価
ビット値 0, 1 ともに先行パルス長がコンスタントに理論値をオーバー、0 の後続パルス長は逆に大きくアンダーとなる傾向が顕著にみられる。先行パルス長の影響でパルス比は全体的に目立って低い。かなり癖のある信号と考えられる。パルス幅のばらつき、パルス比の低さ、そのいずれかあるいは両方がチューナ側に許容されなかったことが反応 NG の原因と考えられる。
A: 生データ:TSV 形式
B: 波形 |
|
C: データ中のパルス長を順番に 1T で除算した結果
|
D: データに出現するビット値 0, 1 を構成する後続パルス長と先行パルス長の比率
|
C: のヒストグラム (生データ:TSV 形式)
|
D: のヒストグラム (生データ:TSV 形式)
|
評価
元信号との波形全体の照合でのタイミングずれは目立たなかったが、パルス幅にもパルス比にも非常にばらつきが大きい。はた目には多くの機器がこの製品の信号でコントロールできていることのほうが不思議にさえ感じられる。一般の家電製品においてリモコン信号データ解釈の許容範囲が広く設定されていることが察せられる。広く利用されている製品であるにもかかわらずこれほど乱れた信号を出しているとは想像していなかった。手元の個体固有の問題? ロットによる品質のばらつき? あるいはスマートリモコンの性質上複数の赤外線 LED を搭載していることの何らかの影響か? いずれにせよ、前掲の「手持ち型リモコン C」の信号を受けいれない機器にこの信号が通用するとは考えにくい。
前項に掲げたパルス幅・パルス比のヒストグラムの図をあらためて並べてみる。俯瞰すると「手持ち型リモコン C」「スマートリモコン D」の信号に含まれるパルス群の品質の乱調がはっきりと見てとれる。他のリモコン分の特徴との差違が顕著であることから、このふたつの学習リモコンの信号が手元のチューナに受けいれられない現象の主因はこのパルスの粒度の乱れにあるものと想定される。
今回の調査を通じて、おそらくは赤外線リモコンの信号に限らずパルス信号全般に共通する品質評価軸の一端を学ぶことができたように思います。目視では波形の全体像が元のリモコン信号のそれとよく似ているにもかかわらず、スマートリモコン D による再現信号(以下に元信号波形との照合図を再掲)が実在の機器で弾かれるケースのあることは象徴的です。
オシロスコープ等で巨視的に信号波形全体を観察することには便宜があり、またその確認はこういった調査を行う際には欠かせませんが、無線通信に用いられる信号はあくまでも先頭から順を追って内容を汲んでいくものであり時間の概念を含まないバーコードのように視覚を通じ一瞬で全体をとらえそこから情報を採取していく性質のものではありません。そのため、信号の品質を判断するためには時間軸にそってミクロに解釈を行う必要もあることを認識する良い機会となりました。
生活をより便利にするための製品やサービスの開発と普及が加速しています。新しいものは大いに活用したいところですが、一方で利便性への過剰な没入はしばしば怠惰・依存・不健康といった負の要素と背中合わせであることにも注意しておきたいものです。
近年、スマートホーム等のキーワードに象徴される光景とは逆向きにあえて利用者に不便を強いるための機器が出始めていることを知り関心を持ちました。
不自由さのあとの自由からはその恩恵をあらためて新鮮な思いで学ぶことができるでしょう。あるいは、今まで自分にとって重要で欠かせないと思っていたものとの関係を見直すきっかけとなるかもしれません。そのように、普段あまりに見なれた自由との間に距離をおいてみる試みは、自分の日常への向き合い方を考えるための材料のひとつになりえるのではないでしょうか。
中島 らも (著) 「とほほのほ」 1991/1 - www.amazon.co.jp
「楽園はどこにあるのか (4)」 より
そのマヤの「現世-楽園置換装置」というのは、かなり大きなドームの形をしている。内部はガランとした空洞で、この遺跡からは多量の炭化した「トウガラシ」が発見された。神官たちは支配下の善男善女たちをこのドームに入れ、大量のトウガラシをいぶした煙をドーム内にあおぎ入れたのだろう。人々は恐怖と苦痛で、発狂と死の直前まで追い込まれる。そのときドームの戸が開かれる。冷たくて香りのよい空気がなだれ込んでくる。人々は自分が立っている「いま」「ここ」がすなわち楽園にほかならないことを確信するのだ。製品の例
1. kSafe
概要
kSafe は Kitchen Safe Inc (米 カリフォルニア)による製品。 タイマーつきの電子ロック式小物入れ。あらかじめ設定した時間が経過しなければ中身を取り出せないしくみで、日常生活において依存対象となりがちなスマートフォンやもろもろの嗜好品へ接触する自由を自分自身で制限することができる。
以下、公式サイトの記事より。
- kSafe by Kitchen Safe | The Time Lock Safe - www.thekitchensafe.com
A powerful tool
to build good habits
Once the timer is set, and the button is pressed, the safe will remain locked until the timer reaches zero.
No overrides!
www.thekitchensafe.com
- frequently asked questions | kSafe by Kitchen Safe - www.thekitchensafe.com
Why do I need the kSafe?Google 訳
Short Answer - Because it’s really cool! And, it’s been scientifically proven to increase your chances of reaching your goals.
Long Answer - The kSafe was developed based on research published by scientists at MIT, Harvard, Stanford, Princeton, and Yale. They discovered that pre-commitment can significantly increase our chances of achieving our goals. :なぜkSafeが必要なのですか?
簡単な回答 - 本当にクールだから! そして、あなたの目標を達成する可能性を高めることが科学的に証明されています。
長い答え - kSafeは、MIT、ハーバード、スタンフォード、プリンストン、エールの科学者によって発表された研究に基づいて開発されました。 彼らは、事前コミットメントが私達の目標を達成する私達の可能性をかなり高めることができることを発見した。 :権威めいたものを引き合いに出しながら肝心の「いつ・どこで・誰が・どのように」が省略されていることが残念だが、製品そのものは興味ぶかい。
価格
ただし、この kSafe は結構値が張る。下記のように公式サイトからの直販でノーマルサイズの「Medium」が送料別 49米ドルという価格。
- Purchase | kSafe by Kitchen Safe - www.thekitchensafe.com (2019年2月時点)
www.thekitchensafe.com 逡巡とその後
この製品のコンセプトに関心がからまりしばらく直販サイトを徘徊した。 総額で $49 なら買ってもいいと思ったが、日本への最安送料 $29.34 が加わると Medium で計 $78.34。手元での費用対効果を想定するとこれはかなり微妙で悩ましい金額だった。
上のスクリーンショットのようにカートに入れたまましばらく好奇心と理性の間を行き来していると何日か後に販売元から以下のメールが届いた。 個人的にはこの内容に微妙な印象が残り結局買うのをやめた。
From: kSafe by Kitchen Safe
Date: 2019年2月15日(金) 4:40
Subject: Are you OK? Kitchen Safe is worried
To: xxxxxxxxx@gmail.com
Hey,
We noticed that you didn't complete your Kitchen Safe order! The only reasonable explanation we can think of is that your computer exploded right before you could click "Complete my Order". Don't worry, we saved your shopping cart so you can complete the order from a friend's computer or phone (see bottom of email).
If your computer didn't explode and you're just on the fence, be sure to read some of our customer reviews.
Also, you can apply this coupon: CommitToChange to save 10% on checkout. It expires in the next 24 hours.
Thank you,
The Kitchen Safe Team
www.thekitchensafe.com今となっては高額出費を抑えられたことにむしろ感謝している。
2. Timer Lock
概要
Timer Lock はノーブランドの中国製品。 名前のとおり上の kSafe と同様のタイマーロック製品であり、こちらは箱型ではなく錠前のスタイル。
価格
価格は kSafe に比べれば安い。アマゾンジャパンでは 2000円ほどの価格から販売されている。複数のセラーが存在。
- タイマー式南京錠 USB充電 安心 防犯グッズ - www.amazon.co.jp ¥ 2,099 (2019年2月時点)
ちなみに eBay ではその半額程度で出回っている。
- Timer Lock | eBay - www.ebay.co.uk
購入
買ってみることにした。当初は eBay の利用を考えたが、アマゾンのレビュー等を参照すると製品の大元の品質に対する一抹の懸念が残った。結局、価格差を保険料と割り切り、実際に問題に直面した場合に返品のしやすいアマゾンで購入した。
観察
以下、現物を手にした状態でのメモ。
- サイズ感は写真のとおり
- ワイヤを右側のホールへ装着して使う。差し込んだワイヤは本体右脇のボタン押下でリリース
- 左右ボタンでキッチンタイマー風に時分を設定(最長99時間99秒)し、中央ボタンで開始。5秒間の猶予後はタイマー満了まで解除不可となる
- 錠前の形状ではあるがこういう華奢なつくりなので非常時(?)には簡単に壊すことができる。言いかえれば防犯用途にはまったく適さない
- 内蔵バッテリーを付属の USB ケーブルで充電して使う。本体装着側が MicroB ではなくレアな外径 2.35mm / 内径0.7mm プラグなのがちょっと残念
- 手元では一度のフルチャージを経てこの 2か月ほど週に 3, 4 回、それぞれ 12時間程度のタイマー設定で使っているがまだ充電切れの予兆なし
使用例
自宅の観音開きの押入れをロック対象とすることにした。これなら小物入れ式の kSafe とは異なり楽器など大きなものでも最長 99時間99分後の未来へ預けることができる。 本体のワイヤは押入れの取手を直接ホールドできる長さではないため以前ホームセンターで買った硬質プラスティック素材のチェーンを併用した。写真のようにちょっと物々しい感じに。
動作の様子: 1分17秒 音量注意
所感
実際に使ってみるとこれは望外に良い製品だった。普通に丁寧に扱えば何の問題もない。予備をかねて eBay でもう一台購入。 説明書をみたところではこちらがより新しいバージョンらしい。少しデザインが異なるが機能は同じ。
この価格で買える不自由・非日常の価値は大きい。
付記
手元の一連のメモを上の記事にまとめていたところ、前掲の kSafe のアマゾンジャパンでの販売価格が高騰していることに気がつきました。 アマゾンでは以前からメーカー直販よりもかなり割高な 9,700円という価格で販売されていましたが、現在は 1万円を優に超えておりちょっと驚きました。
- Kitchen Safe タイムロッキングコンテナ (ホワイトクリア) - www.amazon.co.jp
¥15,792 (2019年4月29日時点)元々あまり目立つ製品ではないためかこれまでの観察の範囲ではアマゾンでの価格に変動はありませんでした。不思議に思って情報を探してみると @ryogomatsumaru さんによる 4月22日の次のツイートがこのブームの発端らしいことを知りました。
最近スマホをついつい触りすぎて時間を浪費してることに危機感を覚えて、「設定した時間の間は絶対にあけられない箱」を買ったんだけど、これ思ったよりもずっと仕事がはかどるようになるのでオススメです。
— 松丸 亮吾@東大ナゾトレ9巻発売中! (@ryogomatsumaru) 2019年4月22日
勉強中に携帯いじっちゃう人とか、仕事中に誘惑に負けやすい人。ぜひ。 pic.twitter.com/YBXW6ovpD8
すごい影響力ですね。まさに情報の時代であることをあらためて実感しました。話題が重なるのでこの記事はキャンセルしようかとも思いましたが、せっかくなので史上初の10連休と平成最後の日の記念(?)をかねて。