社会人研究者が色々頑張るブログ

pythonで画像処理やパターン認識をやっていきます

Webカメラを用いたタッチパッドの開発(1)

はじめに

会社が夏休み期間に入り余暇ができたので自由工作を作ってみました。
日頃より「パソコン用に大きなタッチパッドがほしいなぁ」と思っていたので、Webカメラを使って簡易的なタッチパッドを作ってみたいと思います。
イメージ図はこんな感じになります。
f:id:nsr_9:20210813002729p:plain

Web Cameraで指先をトラッキングして、マウスカースをそれに追従させるイメージです。

アルゴリズムの処理フロー

以下のような処理フローで開発していこうと思います。
f:id:nsr_9:20210813002753j:plain

1. カメラ画像入力

OpenCVのVideoCapture機能を用いて、リアルタイムにカメラ画像をキャプチャします。
何も指定せずにCaptureするとVGA画質(640x480[pix])で読み込まれてしまいます。
これだとタッチパッドの分解能がちょっと残念なことになりそうだったので、960x720[pix]で読み込むようにしました。

import cv2

if __name__ == "__main__":
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 960)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
    
    while cv2.waitKey(1) != ord(q):
        frame = cap.read()[1]
        cv2.imshow("img", frame)

ちなみにカメラはこんな感じで配置しています。
左の三脚に乗ってるのがWebカメラ(Logcool 920c)です。
f:id:nsr_9:20210813002830p:plain

2.タッチ領域検出

俯瞰視点のカメラ画像からタッチ領域を抽出します。
タッチ領域は事前に抽出座標を求めておき、射影変換で切り出します。
射影変換はOpenCVのPerspectiveTransform関数とwarpPerspectiveを用いて実装します。

import cv2
import numpy as np


def cropping_area(img):
    p1 = (100, 581) #左上
    p2 = (292, 26) #右上
    p3 = (730, 608) #左下
    p4 = (675, 23) #右下
    
    h = int(17 * 50)
    w = int(27 * 50)
    pp = np.float32([p1, p2, p3, p4])
    pn = np.float32([[0, 0], [w, 0], [0, h], [w, h]])
    
    M = cv2.getPerspectiveTransform(pp, pn)
    out = cv2.warpPerspective(img.copy(), M, (w, h))
    return out

以下に実行例を示します。

入力画像と矩形領域 射影変換後の画像
f:id:nsr_9:20210813003022j:plain f:id:nsr_9:20210813003109j:plain

いい感じに上から見下ろしたような画像を抽出できました。
今回は検出座標を決め打ちで設定しましたが、簡易なマーカー等を用いて自動で設定できるようにしたいですね。

3.手の領域検出

手の領域は背景差分法で検出しました。
背景差分法は動画処理アルゴリズムの一種であり、事前に登録した背景画像と現在のフレーム画像を比較することで、物体を効率的に検出できます。
背景画像と入力画像の差分を求めるだけなので実装自体は非常に簡単です。

def diff(base_img, img, threshold=30):
    hsv1 = cv2.cvtColor(base_img, cv2.COLOR_BGR2HSV).astype(np.int)
    hsv2 = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.int)
 
    index = np.where(np.abs(hsv1[:,:,0] - hsv2[:,:,0]) > threshold)
    out = np.zeros_like(img)
    out[index] = img[index]
    return out 

本実装ではHSV空間に変換した後に色相成分で背景差分を行っています。
色空間の変更は、手軽に画像処理の性能を向上させられる事が多々ある実用的な技術なので、いつかまとめたいと思います。
実行結果を以下に示します。

入力画像 背景差分結果
f:id:nsr_9:20210813003216j:plain f:id:nsr_9:20210813003229j:plain

きれいに手の領域だけ抽出することができました!

4.指先座標推定

背景差分結果から指先の座標を推定します。
右手でタッチ操作を行う場合、指先が左上に来る性質を用いて検出を行います。
f:id:nsr_9:20210813003259j:plain

手の領域の各画素に対して、左上の原点からの距離が最も近いものを指先としました。

def find_position(frame):
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    thresh = 100
    y, x = np.where(gray > thresh)

    #手の領域が極端に小さければ処理しない
    if len(y) < 30:
        return None, None 
    d = np.sqrt(y**2 + x**2)
    index = np.argmin(d)
    
    y, x = y[index], x[index]
    return x, y

以下に実行結果を示します。指先画像をピンク色の丸で示しています。
f:id:nsr_9:20210813003345g:plain

5.マウス制御

マウス制御はpyautoguiを使います。pyautoguiはマウスやキーボード操作、画面キャプチャ等々、pythonでRPA(自動化)を実装するのに適したライブラリです。
今回はpyautoguiのマウス操作機能を使用します。

def mouse_move(x, y):
    WIDTH = 1920
    HEIGHT = 1080
    pos_x = int(WIDTH * x)
    pos_y = int(HEIGHT * y)
    
    pyautogui.moveTo(pos_x, pos_y)

実装状況

1〜5までの処理を一気に動かすと次のようになります。
f:id:nsr_9:20210813003427g:plain

いい感じにマウスカーソルが追従していますね!
作ってる最中に気がついたのですが、クリックとかスクロール処理機能の存在を忘れていました。
どうやって実装しようか悩ましい所なので、夏休みの良い課題になりそうです。

最後に今回作成したコードを記載します。

コード全体

import cv2
import numpy as np
import os
import pyautogui


def cropping_area(img):
    p1 = (100, 581) #左上
    p2 = (292, 26) #右上
    p3 = (730, 608) #左下
    p4 = (675, 23) #右下
    
    h = int(17 * 50)
    w = int(27 * 50)
    pp = np.float32([p1, p2, p3, p4])
    pn = np.float32([[0, 0], [w, 0], [0, h], [w, h]])
    
    M = cv2.getPerspectiveTransform(pp, pn)
    out = cv2.warpPerspective(img.copy(), M, (w, h))
    return out


def diff(base_img, img, threshold=30):
    gray1 = cv2.cvtColor(base_img, cv2.COLOR_BGR2HSV)[:,:,0].astype(np.int)
    gray2 = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)[:,:,0].astype(np.int)
 
    index = np.where(np.abs(gray1 - gray2) > threshold)
    out = np.zeros_like(img)
    out[index] = img[index]
    return out 


def find_position(frame):
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    thresh = 100
    y, x = np.where(gray > thresh)
    
    if len(y) < 30:
        return None, None 
    d = np.sqrt(y**2 + x**2)
    index = np.argmin(d)
    
    y, x = y[index], x[index]
    return x, y


def mouse_move(x, y):
    WIDTH = 1920
    HEIGHT = 1080
    pos_x = int(WIDTH * x)
    pos_y = int(HEIGHT * y)
    
    pyautogui.moveTo(pos_x, pos_y)


def run():
    cap = cv2.VideoCapture(0)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 960)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
    
    count = 0
    os.makedirs("out", exist_ok=True)

    base_img = cv2.imread("./area.jpg")
    
    while True:
        frame = cap.read()[1]
        area_img = cropping_area(frame)
        
        area_diff = diff(base_img, area_img)
        x, y = find_position(area_diff)
        if x is not None:
            area_img = cv2.circle(area_img, (x, y), 10, (255, 0, 255), -1)
            x = x / area_img.shape[1]
            y = y / area_img.shape[0]
            
            mouse_move(x, y)

        cv2.imshow("", frame)
    
        #cv2.imwrite("out/{0:06d}.jpg".format(count), area_img)
        cv2.waitKey(1)
        count += 1



if __name__ == "__main__":
    run()