@kotyのブログ

PythonとかAWSとか勉強会のこととかを、田舎者SEがつづります。記事のライセンスは"CC BY"でお願いします。

テレビに近づき過ぎたら離れるよう注意する仕掛けを作った

ひととおり動くようになったので、記事にまとめます。

背景

子供はテレビが好きである。さらに、夢中になるとどんどんテレビに近づいていく。それをいちいち注意していた。 同じことを3回やったら自動化するのがプログラマーの鉄の掟であるため、 我が家のラズパイと超音波距離計か何かを使えばどうにかならんかなぁとぼんやり考えていた。

そんな中、ギーラボに置いてあった私物のスピーカーを取りに行ったときに「これも持ち帰ってくれ」と渡されたのが私物の初代kinectだった。 これには深度センサーが搭載されていることを思い出し、使ってみることを思いついたのだった。

実現方法の概要

作戦としては、以下を考えた

  1. テレビの上部にkinectを設置
  2. RGBカメラで顔を検知し顔のエリアの平均深度を計測
  3. 閾値よりも近かったら、離れるよう注意する音声をgoogle homeから流す

環境構築

そもそも手持ちのラズパイで使えるのか検証したのが以下の記事。 koty.hatenablog.com

ソースコードの解説

以下にupした。また、それぞれの要素技術に関する参考サイトはソース内のコメントに記載した。 github.com

kinectデータの取得

freenect.runloop というメソッドのコールバックから取得できる。センサーが別なためか、depthとRGBは同時には取得できない。 この仕様は動きの激しいものを相手にする場合は問題になりそうだけど今回は問題にならないだろうと考えて、それぞれの直近のコールバック呼び出しで取得したデータを使うことにした。 また、RGBのコールバックの方が頻繁に呼び出されていたためdepthのコールバックの方でメインロジックを呼ぶことにした。

from is_too_close import is_too_close
from warn_to_son import warn_to_son
#import frame_convert2
import freenect
#import numpy as np

latest_rgb = None
latest_depth = None
has_warned = False
keep_running = True

def process_depth(dev, data, timestamp):
    global has_warned
    global latest_depth
    global keep_running
    # raise freenect.Kill

    if has_warned:
        return
    if latest_rgb is None:
        return
    latest_depth = data

    if not is_too_close(latest_rgb, latest_depth):
        return
    
    warn_to_son()
    #has_warned = True
    #np.save('np_depth.npy', latest_depth)
    #np.save('np_rgb.npy', latest_rgb)
    #keep_running = False

def process_rgb(dev, data, timestamp):
    global latest_rgb
    latest_rgb = data

def body(*args):
    if not keep_running:
        raise freenect.Kill


print('Press ESC in window to stop')
freenect.runloop(depth=process_depth,
                 video=process_rgb,
                 body=body)

顔検出と距離計測

大まかな流れは、以下の通り。

  1. RGBをskimageにてグレースケールに変換
  2. OpenCVにて顔検出
  3. 顔の領域の切り出し
  4. 顔の領域の平均深度を算出
  5. 深度をメートルに変換
  6. 閾値判定

ありものの組み合わせで実現できてしまい驚いた。顔検出ロジックも完全にブラックボックスである。。。

import cv2
import numpy as np
# import matplotlib.pyplot as plt
import math

from skimage.color import rgb2gray
from skimage import io, exposure, img_as_float, img_as_ubyte
import warnings

# 顔検出器
face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml")
THRESHOLD_METER = 1.5

def _convert2meter(raw_depth):
    # https://openkinect.org/wiki/Imaging_Information#Depth_Camera
    if raw_depth > 1050:
        # 2.5m以内の場合にしか適用できない数式なので、おおむねそれ以上のraw_depthの場合は一律999mと返す
        return 999
    return 0.1236 * math.tan(raw_depth / 2842.5 + 1.1863)

def _convert2gray(img):
    # https://qiita.com/yoya/items/dba7c40b31f832e9bc2a
    img = img_as_float(img)  # np.array(img/255.0, dtype=np.float64)
    imgL = exposure.adjust_gamma(img, 2.2)  # pow(img, 2.2)
    img_grayL = rgb2gray(imgL)
    img_gray = exposure.adjust_gamma(img_grayL, 1.0/2.2)  # pow(img_grayL, 1.0/2.2)
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        img_gray = img_as_ubyte(img_gray)  # np.array(img_gray*255, dtype=np.uint8)
        return img_gray
    
def is_too_close(rgb, depth):
    # http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_objdetect/py_face_detection/py_face_detection.html
    # グレースケールに変換
    gray = _convert2gray(rgb)
    # 顔検出
    faces = face_cascade.detectMultiScale(gray)
    for face in faces:
        # array([[310, 332,  33,  33]], dtype=int32)
        # depth画像から顔部分を切り出し
        # depth[332:365, 310:343]
        x_start = face[1]
        x_end = x_start + face[3]
        y_start = face[0]
        y_end = y_start + face[2]
        depth_cropped = depth[x_start:x_end,
                              y_start:y_end]
        # 顔領域の平均距離
        distance = _convert2meter(np.average(depth_cropped))
        # 閾値より近かったら too close
        if distance < THRESHOLD_METER:
            return True
    return False

google homeの音声出力

狙ったgoogle homeをローカルネットワークから探し出し、公開URLに置いた音声を再生する。

import os
import pychromecast
# https://qiita.com/rukihena/items/8af9b8baed49542c033d

CHROMECAST_NAME = os.environ['CHROMECAST_NAME']
DIRECTION_MP3_URL = os.environ['DIRECTION_MP3_URL']

def warn_to_son():
    chromecasts = pychromecast.get_chromecasts()
    google_home = [c for c in chromecasts[0] if CHROMECAST_NAME in c.device.friendly_name][0]
    google_home.wait()
    google_home.media_controller.play_media(DIRECTION_MP3_URL, 'audio/mp3')
    google_home.media_controller.block_until_active()

動作している動画を見せたいところですが、部屋が丸見えになるのでご容赦ください。

今後の展望

実用に向けては細々とした残タスクがある。飽きっぽいので途中で放り出しがちだけどもやっていきたい。。

  • 一度注意したら5分チェックを停止するようにしたい
  • 30分以内に3回注意されたらテレビを消すようにしたい。(テレビは既にnature remoで操作できるようにしてある。)
  • mp3を再生しているが、AWS Pollyにしゃべらせても良いかも

ゴールデンウィークの良い自由研究になった。

おまけ

顔検出ロジックの実装を試行錯誤する際は jupyter lab を使った。とても便利。 f:id:kkotyy:20210507171442p:plain