はじめに
ピアノの練習で楽譜PDFを見ながら演奏してるときに、「手が塞がってページがめくれない」となったため、Webカメラの映像から顔の動きをざっくり検出して、楽譜PDFを表示しているAcrobat Readerに対して上下キーを送るWindowsアプリ WinGesTurner を作りました。
作ったもの
できること
-
上を見る(Looking up) →
↑キーコマンドを送る(前のページ) -
口を開く(Mouth open) →
↓キーコマンドを送る(次のページ) - 起動すると タスクトレイ常駐
- 検出状況を確認するための ステータスウィンドウ(最前面&非アクティブ表示)
- ステータスウィンドウをクリックすると モニターウィンドウ を表示/非表示
動作環境
- Windows 11
- .NET 10
- Webカメラ
- 依存ライブラリ
- OpenCvSharp4(画像処理/検出)
- Hardcodet.NotifyIcon.Wpf(タスクトレイアイコン)
※初回起動時にHaar Cascade XMLをダウンロードするため、インターネット接続が必要です。
使い方
- アプリ起動
- 初回は必要ファイル(Haar Cascade)が自動ダウンロードされる
- Acrobat Readerで楽譜PDFを開いて、そのウィンドウをアクティブにしておく
- カメラの前で
- 上を見る → 前ページ
- 口を開く → 次ページ
- タスクトレイのアイコンを右クリックして終了
ステータスウィンドウをクリックするとモニター表示が切り替わるため、認識状況を確認できます。
OpenCVとは
OpenCV(Open Source Computer Vision Library)は、画像処理・コンピュータビジョン向けの代表的なオープンソースライブラリで、カメラフレームの取得、色変換(BGR→グレースケール)、ヒストグラム平坦化、物体検出などの基本機能が揃っており、リアルタイム処理にも使いやすいです。
このアプリでは .NET(WPF)から扱いやすい OpenCvSharp を介して OpenCV を利用し、Webカメラ映像の前処理と検出処理を行っています。
Haar Cascadeとは
Haar Cascade(カスケード分類器)は、Haar-like特徴量とAdaBoostを用いた古典的な物体検出手法です。学習済みXML(例:顔・目・笑顔)を読み込み、画像内の矩形領域を走査して該当パターンを高速に検出します。深層学習ベースほど頑健ではありませんが、軽量で導入が簡単という利点があります。
このアプリでは CascadeClassifier に学習済みXMLを読み込んで 顔・目・口周辺(smile) を検出し、目の位置関係から「上を見ている」、口周辺の反応から「口を開いた」を判定してページ送りの入力につなげています。
Haar-like特徴量とは
Haar-like特徴量は、画像の中の「明るさ(輝度)の差」を、いくつかの単純な矩形領域の組み合わせで数値化した特徴量のことです。
代表的には「白い矩形領域の画素和 − 黒い矩形領域の画素和」のように計算を行います。
2分割(左右/上下)、3分割、4分割などの矩形パターンを、位置やサイズを変えながら画像上でスキャンして特徴量を作ります。目の周りは暗くて頬は明るい、鼻筋は明るい…のような、“明暗の配置”を捉えやすいため、顔検出のようなタスクで昔から使われてきました。
実装のポイント
1) 起動時にHaar Cascade XMLを自動取得する
OpenCVのカスケードファイル(haarcascade_*.xml)が無いと検出が始まらないため、起動時に存在チェックして、無ければOpenCV公式リポジトリ(raw)からダウンロードするようにしています。
- 実装:
WinGesTurner/Services/CascadeHelper.cs - 呼び出し:
WinGesTurner/App.xaml.csのApplication_Startup
bool cascadesReady = await CascadeHelper.EnsureCascadeFilesExist();
if (!cascadesReady)
{
MessageBox.Show("必要なファイルのダウンロードに失敗しました。\nインターネット接続を確認してください。");
Shutdown();
return;
}
ダウンロード先は AppDomain.CurrentDomain.BaseDirectory(実行フォルダ)としています。
2) 「フォーカスを奪わないUI」でPDFをアクティブのままにする
このアプリ側のウィンドウがアクティブになってしまうと、上下キーのコマンドが対象のアプリに送られないため、この仕組みが大切です。
そのため、ステータスウィンドウとモニターウィンドウは WS_EX_NOACTIVATE を足して 非アクティブでも表示できるようにしています。
- 実装:
StatusWindow.xaml.cs/MonitorWindow.xaml.cs
private const int WS_EX_NOACTIVATE = 0x08000000;
var helper = new WindowInteropHelper(this);
SetWindowLong(helper.Handle, GWL_EXSTYLE,
GetWindowLong(helper.Handle, GWL_EXSTYLE) | WS_EX_NOACTIVATE);
さらにモニターウィンドウは ShowInTaskbar="False" として、邪魔になりにくくしています。(MonitorWindow.xaml)
3) カメラ取得はバックエンドをフォールバック
環境によって VideoCapture のバックエンド相性が出ることあるため、
DSHOW → MSMF → ANY の順で試して利用できる環境を採用しています。
- 実装:
WinGesTurner/Services/CameraService.csのOpenCamera - 取得ループは30fps想定
Thread.Sleep(33)
4) 検出は「顔 → 目位置」「口(smile)で代用」
目の検出は、顔領域から haarcascade_eye.xml で目を検出し、各目ROI内で「暗い領域(瞳/虹彩)」を二値化して重心を求め、虹彩中心を推定します。
推定した虹彩中心のY座標を目ROIの高さで正規化し、左右の平均値が閾値より小さい場合に「上を見ている(LookingUp)」と判定します。
口の検出は、顔の下半分を口領域として切り出し、haarcascade_smile.xml で候補を検出します。誤検出を減らすため、検出矩形の横長比(幅/高さ)とROI内での位置でフィルタし、最大の候補のみ採用します。さらに、検出が一定フレーム連続した場合のみ「口が開いている(MouthOpen)」として確定します。
- 実装:
WinGesTurner/Services/FaceDetectionService.cs - 顔検出:
haarcascade_frontalface_default.xml - 目検出:
haarcascade_eye.xml - 口の開き:
haarcascade_smile.xml
5) キー送信は keybd_event + クールダウン
ページ送りのキーを連打しないように、1秒のクールダウンを入れています。
- 実装:
WinGesTurner/Services/KeyInputService.cs
private const int KEY_COOLDOWN_MS = 1000;
public void SendDownKey()
{
if ((DateTime.Now - lastDownKeyTime).TotalMilliseconds < KEY_COOLDOWN_MS)
return;
SendKey(VK_DOWN);
lastDownKeyTime = DateTime.Now;
}
画面構成
-
App.xaml:タスクトレイアイコン(Hardcodet.NotifyIcon.Wpf) + 終了メニュー -
StatusWindow:カメラ状態/検出状態の表示(クリックでモニターON/OFF) -
MonitorWindow:処理済みフレーム(顔枠とか)を表示
改善したいこと
- ジェスチャーの安定化(閾値の調整、連続フレームでの多数決、誤検出対策)
- 口の開閉は別手法(ランドマーク等)に置き換え
- 送信先を“今アクティブなウィンドウ”以外に固定するオプション
おわりに
もともとは、Python + OpenCV で開発を始めたのですが、ステータスウィンドウやモニターウィンドウをうまく制御できなかったため、.NET 10 + C# + WPF で作り直しました。
現時点で検出の精度があまりよくないため、他の検出手法なども試してみたいなと思います。