
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、Python
が世界的に人気の言語になってから結構時間が経ちますが、正直なところLaravel + Vue
がメインの開発をさせていただいていることもあり、気がつくと「あれ、Pythonのループってどうやるんだっけ…」となったりもします。
プログラミング言語は、似ている部分も多いですが少しクセがあったりもするので使わないと忘れちゃうんですよね。
…なんて考えていると、ふとPython
を使ってやってみたいことが天から降りてきました
それは・・・・・・
指ジェスチャーでブラウザを操作したい!
です。
というのも、少し前に「MediaPipe」というGoogle
がつくった「いろいろな物体検出ができるライブラリ」の存在を知り、興味が出ていたからなんですね。
手の物体検出はこちら
↓↓↓
(MediaPipe ウェブサイトからの引用)
そこで
今回は、MediaPipe
(Python
)で「親指の向きが上 or 下」を判別し、ブラウザのスクロールを操作してみることにしました。
ぜひ何かの参考になりましたら嬉しいです。
「クライアント様たち
から頂いたお歳暮で満腹
ホント幸せ」
開発環境: Python 3.8.10、Laravel 8.x
前提として
今回の機能は以下の手順で実装します。
- Python で親指が「上 or 下」をリアルタイムに判別
- Pusher へ親指の情報を送信
- JavaScript(ブラウザ側)がそのデータを受けとり自動的にスクロール
そのため、事前にPusher
に登録し、さらにアクセス情報が準備済みであることが前提になっています。
もしまだの方は、以下を参考にして準備しておいてください。
参考ページ: リアルタイム・チャットをつくる:リアルタイムに更新する部分をつくる
パッケージをインストールする
今回の機能を実装するために以下3つのPython
パッケージを使います。
- OpenCV: 画像編集
- MediaPipe: いろいろな物体を認識する。Google製。
- Pusher: Python から Pusher にアクセスする。
そのため、前もってインストールをしておいてください。
pip3 install opencv-python
pip3 install mediapipe
pip3 install pusher
では、実際に作業をしていきましょう
Python 側の作業
では、まずはPython
の作業です。
finger_gestures.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import time
import mediapipe as mp
import pusher
# 定数
PUSHER_APP_ID = '(ここに Pusher の APP ID)'
PUSHER_APP_KEY = '(ここに Pusher のキー)'
PUSHER_APP_SECRET = '(ここに Pusher の秘密キー)'
PUSHER_APP_CLUSTER = '(ここに Pusher の地方名(おそらく ap3 ))'
PUSHER_CHANNEL = 'finger-gestures'
PUSHER_EVENT = 'Scroll'
def get_status(results):
if results.multi_hand_landmarks:
hand_landmarks = results.multi_hand_landmarks[0].landmark
thumb = hand_landmarks[4] # 親指
index_finger = hand_landmarks[8] # 人差し指
middle_finger = hand_landmarks[12] # 中指
ring_finger = hand_landmarks[16] # 薬指
pinky_finger = hand_landmarks[20] # 小指
# 親指が他の指より上にある場合(上部からのパーセンテージが少ない場合)
if thumb.y < index_finger.y and \
thumb.y < middle_finger.y and \
thumb.y < ring_finger.y and \
thumb.y < pinky_finger.y:
return 'up'
else:
return 'down'
else:
return 'none'
hands = mp.solutions.hands.Hands(static_image_mode=True, max_num_hands=1, min_detection_confidence=0.5)
cap = cv2.VideoCapture(0) # 一番目のカメラ
pusher_client = pusher.Pusher(
app_id=PUSHER_APP_ID,
key=PUSHER_APP_KEY,
secret=PUSHER_APP_SECRET,
cluster=PUSHER_APP_CLUSTER
)
if not cap.isOpened():
print('カメラが起動していません')
exit()
status = 'none'
last_triggerd_at = 0
while True:
ret, frame = cap.read()
if ret == True:
im = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
results = hands.process(im)
status = get_status(results)
cv2.putText(
frame,
text=status,
org=(25, 50),
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=1,
color=(0, 0, 255),
thickness=2,
lineType=cv2.LINE_4)
cv2.imshow('video', frame)
now = time.time()
delta_time = now - last_triggerd_at
# ステータスがあれば、1秒間に一回だけ実行する
if status in ['up', 'down'] and delta_time > 1:
# pusher_client.trigger(PUSHER_CHANNEL, PUSHER_EVENT, { 'status': status }) # Pusher へ送信
last_triggerd_at = now # 最後に Pusher へ送信した時間
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
この中で重要なのが、指のジェスチャーを判別しているget_status()
の部分です。
今回はシンプルに「親指が上 or 下」が分かればいいだけですので、MediaPipe
で取得した「親指の位置が他の指よりも上か下か?」をチェックすることで判別をしています。
なお、1点気をつけないといけないのが、取得できるY座標は「数値が小さいほど上にある」ということです。
以下の画像で見てみましょう。
このように、Y座標は「画像上部からの位置」ですので直感的なものとは逆になるということを注意してください。
ちなみに、取得できる指の位置は次のとおりです。
(MediaPipe ウェブサイトからの引用)
これで、以下のコマンドを実行するとビデオキャプチャ画像をリアルタイムで表示され、さらに「親指が上か下か?」もup
、down
、none
として表示されることになります。
python3 auto_scroll.py
実際に試してみると次のようになります。
JavaScript 側の作業
では続いてJavaScript
側の作業になりますが、必ずウェブサーバーを通して実行してください。そうしないと、おそらくPusher
が実行できないと思います。
※ ちなみに、私の場合はLaravel 8.x
で実行しましたので、サンプルはBlade
テンプレートファイルです。
では、実際のコードです。
resources/views/auto_scroll.blade.php
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app">
@for($i = 0 ; $i < 1000 ; $i++)
@if($i%2 === 0)
<div class="bg-danger text-light p-2">{{ $i }}</div>
@else
<div class="bg-success text-light p-2">{{ $i }}</div>
@endif
@endfor
</div>
<script src="/js/app.js"></script>
<script>
window.onload = () => {
Echo.channel('finger-gestures')
.listen('.Scroll', e => {
const status = e.status;
const movingLength = 100;
let top = window.scrollY;
if(status === 'up') {
top = Math.max(0, top - movingLength);
scrollTo(0, top);
} else if(status === 'down') {
top = Math.min(document.body.scrollHeight, top + movingLength);
scrollTo(0, top);
}
});
}
</script>
</body>
</html>
なお、実は私も少しハマってしまったのですが、listen()
内にはイベント名をいれることになっているのですが、先頭にドットが入っているのはミスタイプではなく、「Echo.channel()
を使う場合は、ドットを含めたプリフィックスが必要」というルールがあるからです。(以前はなかった気がするんですけどね。。)
詳しくは以下のページをご覧ください。
参考ページ: https://github.com/tlaverdure/laravel-echo-server/issues/116#issuecomment-275432879
テストしてみる
それでは、実際にテストしてみましょう
今回は動画にしてみましたので以下をご覧ください。
↓↓↓
はい
想定していた以上にキビキビ切り替わってくれました。
成功です
企業様へのご提案
今回の技術を使うと、例えば工場内で作業をしながらパソコンを操作する場合に「カメラに手のジェスチャーを見せるだけ」で入力&実行できようになります。
また、今回ご紹介したのは「上 or 下」だけのジェスチャーでしたが、機械学習を使えば「グー」「チョキ」「パー」「メロイック・サイン」「スパイダーマンの手の形」などより多くのジェスチャにも対応させることができます。
もしそういった機能をご希望でしたらぜひお問い合わせからご連絡ください。
お待ちしております。m(_ _)m
おわりに
ということで、今回は少しいつもとは違ってPython
を使った内容をご紹介しました。
なお、実はMediaPipe
はブラウザのJavaScript
でも動くのですが、以前別の開発出ブラウザからウェブカメラを使うとCPU
使用率が大きくなりすぎてしまったため、今回はPython
を選択しました。
とはいえ、今回のコードを実行するために15%
ほどのCPU
使用率になっていましたので、ウェブカメラから画像を取得する部分を1秒に1回ごとに変更してみたところ7%
ほどに抑えることができました。
実際に使う場合は、この辺りの「負荷と便利さ」のバランスが必要になってくるかもしれません。(カメラ部分は専用でRaspberry Pi
を使っても良いかもですね)
ぜひ皆さんも自由研究的なカンジで試して見てくださいね。
ではでは〜
「大掃除後、
【不要品配りおじさん】
としてモノを配りました」