その他

2020年04月30日

デプスカメラを「雄弁なスチルカメラ」として利用する

はてなブックマークに登録


デプスカメラ Intel RealSense D415 を使ってあれこれを試しているうちにふとこんなことを考えました。

  • 一般に、デプスカメラの使途はその出力から連続的に得られるフレームデータを時間軸にそって「動画」として処理することにある
  • 一方で、これが所定のシーンについて異なる切り口の複数の情報を画像として同時に取得可能なデバイスであることはスチルカメラとしても有用ではないか?

RealSense D400 シリーズの出力からは、(1)一般的な RGB カラー画像 (2)自然光照度の影響を受けにくい赤外線画像、そして、(3)被写体の位置関係が色彩的に表現された深度情報画像を得ることができます。この三種類の情報を照合するとスコープ内の一瞬の状況を多角的にとらえることが可能です。例を示します。

このユニークな特性を以下の想定のもとで「ゆるい見守り」に活かせるのではないかと考えました。

  • D400 シリーズを遠隔の高齢者や病人・怪我人の寝所を対象とする見守りに利用する
  • 常時監視の重い印象のあるビデオカメラではなく必要時にのみ静止画を撮影するスチルカメラとして扱うことにより拘束感を抑えられる
  • 赤外線カメラベースでの動体検知を取り入れることで不安要素の多い深夜にも有効な安否確認を実現できる

前に人感センサを利用した安否確認のしくみを手がけたことがあります。これは現在もプライベートで利用を続けているのですが、そこにこれらの画像情報が加われば安心感がずっと大きくなりそうです。

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+」とある。
この記事の内容は最終的には正確だった。ただしこうした話題にありがちな落とし穴もいくつかあった。備忘をかねその内容を以下に控える。
  • 現在 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 様による以下の記事にこれに関連する記述がみられる
    • デプスカメラRealSenseD435で・・ (記事名を一部省略させて頂きました) - qiita.com/PINTO
      【注意点】 OpenGL Driver を有効にすると、OpenCVの 「imshow」 メソッドが正常に動作しなくなる点を受け入れる必要がある。 こちらの記事の実装 のように、OpenCVの 「imshow」 メソッドに頼らないでOpenGLの描画系メソッドだけで描画処理を実装しなくてはならない点に注意。
    手元では OpenGL よりも OpenCV に馴染んでいるため逆に OpenGL ドライバを無効化 ([raspi-config] - [7.Advanced Options] - [A7 GL Driver] - [G3 Legacy])し、プログラミングには OpenCV を利用している。一方でこの設定では公式の RealSense Viewer 等が激重になるため適宜使い分けが必要。

こういった過程を経て手元の Raspberry Pi 3 B+ で D415 を利用できる状態になりました。手頃なサイズでいい感じです。

  • 余談ながら RealSense D400 シリーズ製品に付属のミニ三脚(下のふたつめの写真)は少々華奢なため手元ではケンコーコム社製の三脚を使用しています

下の動画にはこの環境で公式の Intel RealSense Viewer を実行した様子を収めています。なお、ここでは前掲の事情によりラズパイ側で OpenGL Driver を有効にしています。

D400 からの赤外線投光と深度情報精度との関係

RealSense D400 シリーズは深度計測にふたつの赤外線センサ(カメラ)を用いたステレオアクティブ方式を採用しており、計測精度向上の補助を目的とするドットパターンの赤外線投光を行うことが可能となっています。この投光はデフォルトで有効です。

前述の想定のように今回は RGB, 赤外線, 深度のみっつの画像情報をあわせて捕捉することがポイントですが、この赤外線投光機能は赤外線カメラ経由で得られる画像と深度情報の両方に影響を及ぼします。手元での観察結果からこのことを整理してみます。

まず、赤外線投光を有効化 / 無効化した状態で D415 から採取した各フレームの静止画像例を以下に示します。赤外線画像はふたつの赤外線カメラの左側のものを使用。なお、この製品の赤外線カメラはあくまでも深度計測を目的とするものであり生のままの画像は視覚的に暗いため、OpenCV で明度とコントラストを補正した版を並べて掲げています。(画像はいずれもクリックで大きく表示)

  • 赤外線投光あり
    RGB
    深度 赤外線 L 赤外線 L(補正ずみ)
  • 赤外線投光なし
    RGB
    深度 赤外線 L 赤外線 L (補正ずみ)

    上のサンプルから次のように判断しました。

    • D415 を赤外線カメラの用途のみで扱うとすればドットパターンの映り込みを避けるために赤外線投光を無効化するほうが好ましい様子
    • ただし、赤外線投光を無効化すると上の結果のように深度情報の質が際立って劣化する
    • この深度情報画像はスコープ内の被写体の位置関係を把握できる重要な情報であるためクリアな内容であることが望まれる
    • 一方、赤外線画像はドットパターンの投光ありでも補正をかければ例のように相応の品質の確保が可能
    • 以上の事情を考えあわせると赤外線投光を有効とするほうが総合的に有利と考えられる

    ちなみに、赤外線投光の有効・無効をストリーミングの途中で切り替えることはできません。サンプルを採取するために用意した 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. フレームデータ取得用のループに若干のディレイ(パラメータ A)を設けておく
    2. ループ内で赤外線フレーム画像情報を取得時にそのコピーを保持しておく
    3. 次回取得した赤外線フレーム画像情報と上の前回分との差分を得る
    4. 上記差分に含まれる各ピクセルの値について所定の閾値(パラメータ B)を基準に 0, 255 の二値にふるい分ける
    5. 上の二値化の結果、255 の数が所定の件数(パラメータ C)以上であればこの間のスコープ内に何らかの変化があったものと判定する
      ※各パラメータは実地で適切な値を割り出す

    上の要領を画像と動画の例で示してみます。

    1. 前回分のフレーム画像
    2. 今回分の画像 (体と手を少し動かしてみた)
    3. 上記 1, 2 画像の差分
    4. ピクセル値を所定の閾値で 0 と 255 に分ける

     

    試作

    内容

    ここまでの話題を元に以下の想定に基づく内容のアプリケーションを試作しました。MQTT ブローカーには定番の Beebotte を利用します。JavaScript 用の API セットも提供されているため Web ブラウザ上のコンテンツでメッセージを取り回すことが可能です。

    • 見守り対象とする場所にラズパイ + D415 を設置し所定のプログラム A を稼働
    • 手元では Web ブラウザ上の HTML リソース B で状況をモニタする
    • A は D415 からのフレームデータで常時動体検知を実施
      • 変化があれば RGB, 赤外線, 深度, 二値化の各画像データを Google Drive へアップロード
      • アップロード完了時に MQTT メッセージを送出 〜 B はこのメッセージを受信するとページをリロード
      • 見守り側は HTML リソース B 上のフォームのボタン押下により任意のタイミングで A へのメッセージングで RGB, 赤外線, 深度 画像の撮影と Google Drive へアップロードを指示可能
        • A は上記メッセージを受信すると RGB, 赤外線, 深度の各画像データを Google Drive へアップロード
          • このとき一世代前の画像セットも保持
        • A アップロード完了時に MQTT メッセージを送出 〜 B はこのメッセージを受信するとページをリロード

    メモ

    関連する話題を控えます。

    • 撮影画像の保存先に Google Drive を選んだのは以下の理由による
    • 今回は RGB, 深度フレームの情報を串刺しにしてデータ処理を行う必要がないためフレーム間のアライメントは行なわず各フレームの情報を欠損なく参照することを優先する
    • 赤外線エミッタの出力は明示的に MAX に設定
    • 赤外線フレームの画像は目視するためには暗く見づらいため OpenCV による画像処理で明度とコントラストを補正

    ソースコード

    以上を踏まえざっくり用意したプログラムです。

    #!/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 ページと実際に撮影した各フレーム画像を静的に再構成したアーカイブを下記へ収めています。

    上のデモページの末尾に今回の試作で使用した実際の HTML 記述をコメントとして引用しています(各種 ID はダミーです)。

    動作の様子


    (tanabe)
           
  • klab_gijutsu2 at 09:30|この記事のURLComments(0)
    2020年04月27日

    オプティカルフローを簡易ジェスチャ認識に利用する

    はてなブックマークに登録

    少し前に Intel 製デプスカメラ RealSense D4153DiVi 社Nuitrack SDK の組み合わせでジェスチャ認識を試していました。下の動画には三脚に据えた D415 ごしに所定のジェスチャでスマート照明の操作をテストした様子を収めています。

    このように期待どおりの反応は得られるのですが、次のことが気になっていました。

    • ここでのジェスチャ認識は人体の骨格検出を前提としている
    • そのため立位であれ座位であれおおむね全身をカメラに呈示する必要がある

    このあたりの事情は Kinect も同様のようです。もちろん骨格の検出は腕や手を適切に識別するために必要ですが、一方でもっとラフなものもあればと考えました。日常感覚ではコタツや布団にもぐり込んだまま姿勢を正すことなくスッと指示を出すといったこともできれば便利そうです。そんなわけで試しに簡単なしくみを作ってみることにしました。

    考え方として、スコープ内の被写体に所定の水準以上の移動をざっくり検知した場合にその向きを判定することを想定しました。実現方法を考えながら社内でそういう話をしたところ、「OpenCV でオプティカルフローを利用してはどうか?」というレスポンスがありました。手元ではトラッキング API (リンク: @nonbiri15 様によるドキュメントの和訳 を試していたところでオプティカルフローのことは知らずにおり、@icoxfog417 様の次の記事中の比較がちょうどわかりやすくとても参考になりました。

    • OpenCVでとらえる画像の躍動、Optical Flow - qiita.com/icoxfog417
      画像間の動きの解析については、様々な目的とそれを実現する手法があります。ここでは、まずOptical Flowがその中でどのような位置づけになるのか説明しておきます。
      • トラッキング
        • 目的: 画像の中の特定の物体(人やオブジェクト)を追跡したい
        • 手法: リアルタイムに行うものとそうでないものの、大別して2種類
              :
      • フロー推定
        • 目的: 画像の中で何がどう動いたのかを検知したい(観測対象が決まっているトラッキングとは異なる)
        • 手法: 画像の中の特徴的な点に絞って解析するsparse型と、画素全体の動きを解析するdense型に大別できる
        • -> sparse型: Lucas-Kanade法など
        • -> dense型: Horn-Schunck法、Gunnar Farneback法など

    以下は GitHub 上の OpenCV 公式のサンプルプログラムです。これは Dense(密)型で Gunnar Farneback 法が用いられています。

    一般的なウェブカメラを PC へ接続してこのプログラムを実行した様子を以下の動画に収めています。スコープ内の被写体の動きが格子点の座標群を起点に捕捉されている様子が視覚的に表現されます。

    (ScreenShot)

    このサンプルプログラムに上下左右四方向への移動を大きく判定する処理を加えてみました。次の動画の要領で動作します。

    加筆したプログラムのソースコードです。まだまだ改良の余地はあるものの今回オプティカルフローの利便性に触れたことは大きな収穫でした。いろいろな使い方ができそうです。

    #!/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)
    klab_gijutsu2 at 14:56|この記事のURLComments(0)
    2020年03月04日

    デプスカメラを「バーチャル背景」用 Web カメラとして使う

    はてなブックマークに登録

    先日ちょっとしたきっかけから Intel 製 RealSense D415 を買いました。デプスカメラに触れるのは初めてでしたが、Microsoft Azure Kinect の国内販売がまもなく開始される旨もアナウンスされておりこの分野の今後の動向が楽しみです。

    試作の内容

    さっそくこの D415 をあれこれ試しています。公式の RealSense SDK 2.0 の勉強をかね習作のひとつとして次の内容のプログラムを作ってみました。

    • RGB カラーフレームの画角へ Depth フレームを位置合わせして両方の情報を使用
    • カメラのスコープから「1m 以内」にあるものの RGB 画像を抽出して表示する
    • スペースキー押下で背景画像を切り替え上の画像との合成を行う

    静止状態でも常に微妙な揺れを伴う深度情報を RGB 画像切り抜き領域の判定に用いているため静的なクロマキー合成とは異なり境界にノイズが発生しがちですが、デプスカメラ利用の一例として紹介します。

    動作の様子

    デモ動画:1分4秒 無音

    ソースコード

    プログラムでは画像処理と表示に OpenCV を併用しています。

    • SDK 内の以下のサンプルプログラムを参考にしておりコードの一部を引用しています
    • 手元の開発環境は Windows 10 + Visual Studio 2019 Community です
    • プロジェクトファイルは上記サンプルのものをコピー〜編集して利用するのが手早いでしょう
    //
    // 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 カメラとして認識されるため汎用的に扱えそうです。

    以下にざっくりと組み合わせの手順を示します。(クリックで大きく表示)

    1. SplitCam を起動して領域を設定

    2. ミーティング用クライアントからカメラとして SplitCam を選択

    3. こんな感じで利用できる


    (tanabe)
    klab_gijutsu2 at 17:46|この記事のURLComments(0)
    2019年04月30日

    生活を「不自由」にするためのソリューション

    はてなブックマークに登録

    自分の自由を拘束してみることの価値

    生活をより便利にするための製品やサービスの開発と普及が加速しています。新しいものは大いに活用したいところですが、一方で利便性への過剰な没入はしばしば怠惰・依存・不健康といった負の要素と背中合わせであることにも注意しておきたいものです。
    近年、スマートホーム等のキーワードに象徴される光景とは逆向きにあえて利用者に不便を強いるための機器が出始めていることを知り関心を持ちました。
    不自由さのあとの自由からはその恩恵をあらためて新鮮な思いで学ぶことができるでしょう。あるいは、今まで自分にとって重要で欠かせないと思っていたものとの関係を見直すきっかけとなるかもしれません。そのように、普段あまりに見なれた自由との間に距離をおいてみる試みは、自分の日常への向き合い方を考えるための材料のひとつになりえるのではないでしょうか。

    • ずいぶん昔に読んだ、故・中島らもさんのエッセイを思い出しました。要旨のみを本文から引用します
      中島 らも (著) 「とほほのほ」 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?

      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.   :
      Google 訳
      なぜkSafeが必要なのですか?

      簡単な回答 - 本当にクールだから! そして、あなたの目標を達成する可能性を高めることが科学的に証明されています。

      長い答え - kSafeは、MIT、ハーバード、スタンフォード、プリンストン、エールの科学者によって発表された研究に基づいて開発されました。 彼らは、事前コミットメントが私達の目標を達成する私達の可能性をかなり高めることができることを発見した。   :

    権威めいたものを引き合いに出しながら肝心の「いつ・どこで・誰が・どのように」が省略されていることが残念だが、製品そのものは興味ぶかい。

    価格

    ただし、この kSafe は結構値が張る。下記のように公式サイトからの直販でノーマルサイズの「Medium」が送料別 49米ドルという価格。

    逡巡とその後

    この製品のコンセプトに関心がからまりしばらく直販サイトを徘徊した。 総額で $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 と同様のタイマーロック製品であり、こちらは箱型ではなく錠前のスタイル。


    www.amazon.co.jp

    価格

    価格は kSafe に比べれば安い。アマゾンジャパンでは 2000円ほどの価格から販売されている。複数のセラーが存在。

    ちなみに eBay ではその半額程度で出回っている。

    購入

    買ってみることにした。当初は eBay の利用を考えたが、アマゾンのレビュー等を参照すると製品の大元の品質に対する一抹の懸念が残った。結局、価格差を保険料と割り切り、実際に問題に直面した場合に返品のしやすいアマゾンで購入した。

    観察

    以下、現物を手にした状態でのメモ。

    • サイズ感は写真のとおり
    • ワイヤを右側のホールへ装着して使う。差し込んだワイヤは本体右脇のボタン押下でリリース
    • 左右ボタンでキッチンタイマー風に時分を設定(最長99時間99秒)し、中央ボタンで開始。5秒間の猶予後はタイマー満了まで解除不可となる
    • 錠前の形状ではあるがこういう華奢なつくりなので非常時(?)には簡単に壊すことができる。言いかえれば防犯用途にはまったく適さない
    • 内蔵バッテリーを付属の USB ケーブルで充電して使う。本体装着側が MicroB ではなくレアな外径 2.35mm / 内径0.7mm プラグなのがちょっと残念
    • 手元では一度のフルチャージを経てこの 2か月ほど週に 3, 4 回、それぞれ 12時間程度のタイマー設定で使っているがまだ充電切れの予兆なし

    使用例

    自宅の観音開きの押入れをロック対象とすることにした。これなら小物入れ式の kSafe とは異なり楽器など大きなものでも最長 99時間99分後の未来へ預けることができる。 本体のワイヤは押入れの取手を直接ホールドできる長さではないため以前ホームセンターで買った硬質プラスティック素材のチェーンを併用した。写真のようにちょっと物々しい感じに。

    動作の様子: 1分17秒 音量注意

    所感

    実際に使ってみるとこれは望外に良い製品だった。普通に丁寧に扱えば何の問題もない。予備をかねて eBay でもう一台購入。 説明書をみたところではこちらがより新しいバージョンらしい。少しデザインが異なるが機能は同じ。

    この価格で買える不自由・非日常の価値は大きい。

    付記

    手元の一連のメモを上の記事にまとめていたところ、前掲の kSafe のアマゾンジャパンでの販売価格が高騰していることに気がつきました。 アマゾンでは以前からメーカー直販よりもかなり割高な 9,700円という価格で販売されていましたが、現在は 1万円を優に超えておりちょっと驚きました。

    元々あまり目立つ製品ではないためかこれまでの観察の範囲ではアマゾンでの価格に変動はありませんでした。不思議に思って情報を探してみると @ryogomatsumaru さんによる 4月22日の次のツイートがこのブームの発端らしいことを知りました。

    すごい影響力ですね。まさに情報の時代であることをあらためて実感しました。話題が重なるのでこの記事はキャンセルしようかとも思いましたが、せっかくなので史上初の10連休と平成最後の日の記念(?)をかねて。


    (tanabe)
    klab_gijutsu2 at 02:53|この記事のURLComments(0)
    2017年09月21日

    40年前の「子供の科学」誌との再会を通じて

    はてなブックマークに登録

    子供の科学」は株式会社誠文堂新光社様の発行する小中学生向けの月刊科学雑誌です。創刊は関東大震災の翌年 1924年(大正13年)とふるく、世代をまたいで読み継がれています。この記事をご覧になっている方の中にもかつての読者(現役の読者も?)が少なくないことでしょう。

    手元では 1977年前後にこの「子供の科学」(以下 "同誌")を購読していました。それからずっと長い間忘れていたのですが、先日実家に残っていた当時の何冊かを見つけました。
    懐かしい気持ちで久しぶりに 40年前の誌面を読み返したところ、子供と同じ目の高さで科学や技術、未来に向き合っていた当時の大人たちの懐の深さと真剣さを現在の年齢なりに強く感じました。商業誌である以上、誌面には小さな読者たちからの要望のいくばくかも反映されていたであろうことを考え合わせるとさらに興味ぶかく感じられます。

    この機会に印象に残ったいくつかの記事を電子化しておくことにしました。その内容を同誌編集部様のご承諾のもとに紹介します。あわせて、40年を経た 2017年現在の誌面と 93年前の創刊号にも目を向けてみたいと思います。


    1977年 第12号より

    表紙

    遺物の写真のまわりに 4つの記事のタイトルのみが淡々と並ぶシンプルな装丁。「マリモはどうして丸くなる?」という取り立てて派手さのないタイトルが堂々と表紙に据えられていることも印象的。

    目次

    同誌の内容は多岐に渡る。「原子力製鉄」という記事と「実用工作 ちりとり」という記事が何の違和感もなく共存していることに守備範囲の広さが感じられる。

    「針のいらないレコードができた」

             :
    もし, プレイヤーにレコードをのせ, 回転させるだけで, 針の付いたピックアップがないのに, 実際に演奏を聞くのと少しも変わらない, すばらしい音質(これを超ハイ・ファイと言います)で再生できたら, おそらくみなさんはびっくりさせられることでしょう.
             :
    このレーザー光線による微小ビットの記録再生技術は, さらにいろいろな応用が考えられています.
     テレビに接続すると映像の出るレコードとか, 超長時間録音とか, 例えば100曲ほど入ったジュークボックスが, たった1枚のディスクでできるとか, あるいは楽器別に同時記録ができるマルチ・チャネル方式への応用など、研究者達の夢は限りなく広がっています.

    PCM レーザサウンドディスク」は、コンパクトディスク(CD)が製品化されるよりも前に三菱電機ティアック東京電化の三社によって開発が進められていたディジタルオーディオシステム。音楽愛好者の重要な音源だった FM 放送の周辺で「PCM 録音」のキーワードが話題になり始めていた時分であり現在が過去の未来であることをあらためて感じさせられる。時代を経てアナログオーディオの価値が再認識された現代ではむしろ「針を使うレコード」のほうが新鮮で贅沢であることもまた興味深い。

    関連記事

    「自由に曲線が切れるカッター」

    ナイフなどで紙や布を曲線に切る事は, たいへんむずかしい事です. この道具は, ゴムのローラーを利用して自由に曲線が切れるようにしたものです.
             :
    ローラーが付いているので, 紙や布の上を自由に動かす事ができ, ジグザグにでも, 曲線状にでも切れるというわけです.
             :

    世界初のロータリーカッターである オルファ株式会社の「マルカッター」の解説。短くさりげない記事だが曲線の切れるカッターのしくみが丁寧にわかりやすく説明されている。 この記事を書いた青木国夫氏(故人)は当時国立科学博物館工学研究部長の要職にあった著名な研究者。

    関連記事

    「自動焦点カメラのしくみ」

    ひと昔前にはまったくの夢だった自動焦点カメラの出現です.
             :
    月の探索用に積み込まれた無人操作カメラや, 8ミリシネマカメラには前から使われていましたが, この種類の普及カメラに組み込まれたのはこれが最初で, 文字どおり「シャッターを押すだけ」のカメラがここで実現したわけです.
             :
    その仕組みは, これまでの距離計とレンズの焦点調節用の伸縮を自動連動させ, 二つの距離計映像の合致を2個の電子の目が見くらべて, 同じになるとそこでストップ, 実際には, シャッターを押すだけでピントも露出もピッタリ, その間は一瞬の手早さで, 撮影完了ということになります.
    その仕組みを, もう少しくわしく説明しますと, ざっとこんなところです(図1)
             :
    この方式は, 短焦点レンズ用(もともとピント幅が広い)には好適ですが,(中略)一眼レフなどの高級機用としては, 今後の研究をまたねばなりません.

    コニカ株式会社(現コニカミノルタ株式会社)の C35AF は 1977年に発売された世界初の普及型オートフォーカスカメラ。この記事は発売されたばかりの同製品に用いられている当時最新の AF 技術を子供に理解することの可能な表現と内容で正面から解説している。ちなみに、記事を執筆した故・松田二三男氏は同誌で人気のあった読者投稿写真コーナー 目次 の選者を長らく務めた。

    関連記事

    「触媒について調べよう」

    触媒とは, そのもの自体, 反応の始めと終わりで少しも変化しないで, ほかの物質の反応の速さを変えるものをいうのです.
             :
    塩素酸カリウムという薬は, 熱すると酸素を発生するのですが, ガスバーナーで熱するぐらいではなかなか酸素はでてきません.    :
    あらかじめ, 二酸化マンガン(触媒)を少し加えておくと, 200℃くらいの温度で完全に分解します.
             :
    過酸化水素水(市販品のオキシフルとかオキシドールという消毒薬)を使って触媒の研究をしてみましょう.
             :

    特にこれといった脈絡もなく「触媒」を扱う化学実験の話題が急にさらりと出てくるのがまた面白いところ。前述のように同誌は広く自然科学分野全般をカバーしており(時には社会科学方面の話題も)、毎回こうした単発の記事が何本も掲載されていた。当時市立中学校の理科教諭だった執筆者の岩崎幸敏氏は 70年代から 90年代にかけて中学生向けの多くの著書を上梓している。

    「ぼくの発明 きみのくふう」(読者投稿コーナー)

    ● カッターナイフの改良 ●

    今までカッターの刃を折る時, 手がしびれることがあったので, つめ切りと同じしくみのものをとりつけ, 手がしびれることなく簡単に刃が折れるようにしました. (B)のねじを回しカッターの刃に, (1)を近づけ, あとはつめを切るのと同じ方法で折る. (川上○○ / 北海道沙流郡)


    この案は不安解消策として有効です. ただし, カッターにこのようなしくみを付けると, 価格が高くなったり, 使用しにくくなるなどの問題点も出てきそうです. 小学生にしてはいい着眼です.

    ネットも PC もなく一個人が自由に情報発信を行う手段がほぼ皆無だった時代には新聞・雑誌等の投稿欄の存在感が現代よりもずっと大きく、同誌のこのコーナーにも人気があった。

    子供が真剣に何かを考え工夫をこらして自分なりの結論を出し、それを第三者が理解できるように説明する努力を経て意見を求めるという一連の構図の素晴らしさをあらためて考えさせられる。 さらに、子供から寄せられたアイディアに決して安易に迎合することなく、むしろ大人である自分の視点での率直な意見を時に助言を添えつつストレートに伝えようとする選者の一貫した姿勢がそのことをさらに際立たせている。この号に掲載された応募作品は以下のとおり。彼らは今どんな大人になっているのだろうか。

    • コンパスの改良
    • つめが飛ばないつめ切り器
    • ボックス・ドライバーの改良
    • カッターナイフの改良
    • 豆英単語練習機
    • 自転車用高圧発生機
    • 小鳥のさえずりまくら
    • 黒板用三角定規
    • 音振動力紙ずもう

    余談:エポック社「システム10」の広告ページ

    この号には発売から間もないエポック社製の家庭用ビデオゲーム機「システム10」の広告が掲載されている。スペースインベーダーが大ヒットしたのが翌 1978年。ちなみに「機動戦士ガンダム」の初放映は翌々年の 1979年。のち 1983年のファミコンの登場によりビデオゲーム機市場の様相は一変するが、そこに至るにはまだしばらくの時間を要した。

    • 当時この「システム10」がとても欲しかったのですが自分でまかなうことはもちろん子供心にも親にねだれるような値段ではなく、ゼロがひとつ少なければ、、と何度も思っていました (> <)

    2017年 6月号より

    1977年当時の誌面との印象的な再会からほどなくして、本年 2017年の6月号を購入しました。創刊からすでに 90余年、現在は紙媒体とは別に Kindle 電子版も発行されていることを知って驚きました。

    40年ぶりに買った同誌はこの時代相応に表現や文体がマイルドになってはいるものの、ガンとして変わらないロゴマークと同様に、誌面から伝わってくるテイストがあの頃のそれとあまり変わっていないことに安心しました。支障のない範囲で一部を抜粋してみます。

    表紙と目次

      

    記事より

    • 「水中の食虫植物 タヌキモ」
      定番の水棲動植物の特集記事。前掲の 1977年12号では「マリモ」でしたね :−)
    • 「ジブン専用パソコン 第3回ラズビアン(OS)を設定しよう!」
      食虫植物の解説とラズパイの連載記事がやっぱり普通に共存
    • 「ぼくの発明 きみの工夫」
      ボリュームは減ったもののこのコーナーが今も健在であることが嬉しい

    創刊号(1924年 10月 第1巻 第1號)より

    この記事の冒頭にもリンクを掲載した「子供の科学」公式サイトの次のページから 1924年(大正13年)の創刊号を閲覧することができます。

    非常に貴重な誌面がこのような形で公開されていることはとても興味深く読み入ってしまいます。とりわけ、最初のページに掲載されている「この雑誌の役目」という文章に感銘を受けました。93年前に書かれたその全文を以下に引用します。

    巻頭の辞「この雑誌の役目」


    www.kodomonokagaku.com

    この雑誌の役目  主幹

     愛らしき少年少女諸君!!!子供科学画報は、皆さんのために、次のような役目をもって生まれました。
     およそ天地の間は、びっくりするような不思議なことや、面白いことで、満ちているのでありますが、これを知っているのは学者だけで、その学者のかたは、研究がいそがしいものですから、皆さんにお知らせするひまがありません。したがって、多くのかたは、それを知らずに居ります。そのなかで特に少年少女諸君の喜びそうなことを学者のかたにうかがって、のせて行くのも、この雑誌の役目の一つです。

     皆さんが学校で学んでいる理科を、一そうわかりやすく、面白くするために、その月々に教わることがらについての写真や絵を皆さんのためにそろえるのも、この雑誌の一つの役目でもあります。理科の本にかいてある事がらに限りません。読本のなかにある理科の事がらに関するものも、のせておきます。

     毎日のように見たり使ったりしているもの事について、皆さんは、くわしく知りたいと思われることがありましょう。皆さんの御望みを満たすため、絵を入れてできるだけわかりやすく、そういうもの事を説明するのも、また、一つの役目であります。
     簡単な器械の造りかたをお伝えして皆さんの発明の才をあらわし、面白い理科の遊びのできるようにするのも、役目の一つであります。

     しかし、この雑誌の一ばん大切な目的は、ほんとうの科学というものが、どういうものであるかを、皆さんに知っていただくことであります。ちかごろは、「科学科学」とやかましくいいますが、ほんとうに科学というものを知っている人は、沢山ないようです。人は生まれながら、美しいものを好む心を持っておりますが、それと同じように、自然のもの事についてくわしく知り、深くきわめようとする欲があります。昔から、その欲の強い人々がしらべた結果、自然のもの事のあいだには、沢山の定まった規則のあることがわかりました。科学ということは、この規則を明らかにすることであります。多くの人が科学といっているのは、大ていは、その応用に過ぎません。この規則を知ることによって、人間は、自然にしたがって、無理のないように生き、楽しく暮らすことができ、これを応用して世が文明におもむくのです。
    ※書き起こしに際し、旧かな遣い・旧漢字・旧かな送りをあらためています

    個人的な雑感

    久しぶりに「子供の科学」誌に接し、私自身がもっとも強く印象に残ったのは、それぞれの分野の専門家の大人たちがそれぞれに子供たちに本気で向き合い、本気で何事かを伝えようとしている姿勢です。過剰に機嫌をとりながら話を聞かせようとするのではなく、興味をもった子供たちへ度合いに応じた「努力」の余地を残しながら適度な粒度まで知識や情報を砕いた上でそれを示し、あとは読み手側の好奇心と探求心にゆだねつつ同時にそれらを育んでいこうとする共通の意思のようなものを感じました。

    難しいことをわかりやすく説明できることが最良とよく言われますが、そこに相手を受け身にすることなく相手の向上心を呼び誘うための配慮を加える余裕があればさらに素晴らしいことでしょう。そして、そういった配慮こそがその道に精通していなければなかなか果たすことの難しい命題ではないかと思います。同誌の記事の執筆者はすべて第一線の専門家であり、「子供だまし」ではなく真剣に世代のバトンを引き渡すためにはまさに適役でしょう。この誌面に限らずこういったあまり表には出てこない場所にもまたこれまで静かにこの国を支えてきた多くの大切な要素が脈々と息づいているのかもしれません。

    どの世代にもそれぞれの役割があります。現役の大人の世代は年齢とともにいずれ順番に次の世代と交代していくことになりますが、科学や技術、学問の分野の話題に限らず、自分を含め今の大人たちが新しい世代に対する役割を適切に果たせているのか? 彼ら彼女らからの問いかけや疑問に対して都合よく言いくるめたりはぐらかしたりせずまっすぐに答えることができているのか? そういったことは大人から子供への一方的なプレゼントではなく、やがて次の世代との交代を終えた未来の自分の生きる世の中を支える礎にもなるものでしょう。誰もが先人たちから受け継いだこの大きなループの中で生きています。その片隅で自分が少年時代に読んだこの雑誌をすっかり大人になった今の年齢でふたたび読み返し、ふとそんなことを考えました。あの頃の大人たちに、あらためて、謹んで、御礼申し上げます。


    (tanabe)
    klab_gijutsu2 at 12:45|この記事のURLComments(0)
    Blog内検索
    Archives
    このブログについて
    DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
    このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
    最新コメント