デュアルディスプレイなときに顔の向きでアプリ切り替えてみた
SAO The Beginningのαテストに落選したふじもと (@masaki_fujimoto) です。当選されたかたは、無事ログアウトできるといいですね (負け惜しみ感)。
相変わらず長い前置き
それはそれとして、最近ディスプレイが余った (別のフロアに1台おきっぱにしてたのを回収してきた) ので、久々にデュアルディスプレイにしてみました。で、画面がひろびろとするのはいいのですが、なんか思ったより快適じゃない感じがしまして、なんでかなぁと思ったら、隣のディスプレイを見たときにアクティブなウィンドウをスイッチするのがかったるいんですよね。一応図にしてみるとこういう感じで:
それぞれ27inchで結構大きいので、基本は左側のディスプレイを正面にみておしごとしてます。で、右側にはchatを表示させてて、通知きたらそっちみて、って感じでまぁありがちな感じですね。
で、それはいいんですが、問題はchatでchannel選んだり返信するときで、当然振る舞いとしては:
- ちょっと右を向く
- アプリを切り替える
- chat操作する (返信したりいろいろ)
になるんですが、問題は2.にあるのです:
- なまじ全画面で表示されていて、アクティブっぽく見えるので「あ、このウィンドウアクティブだはー」と思ってキーボード操作すると、当然そんなわけはなくて、左側のディスプレイにキーボード入力が送信されていらいらする
- そもそもアプリ切り替えるのがめんどう
別に絵にしたところでわかりやすくもないんですが、まとめるとこんな感じです:
なんか絶望的に絵のセンスがないですが、それはさて置きそんなことをFacebookに書いたら:
久々にデュアルディスプレイにして、とりあえず思ったのがいちいちアプリケーションを切り替えるのがめんどすぎるという (左が作業用、右が全画面でチャットとかにしてるので、右見てchat返事しようと思ったらfocusが左の画面のままだった事件続出...
Posted by 藤本 真樹 on Monday, March 14, 2016
あれこれアドバイスをいただくも、ちょうど良さそうなアプリもないし、抽選も落ちるし、ってことで昨日の夜おしごとひと段落したのでえいやー、と書いてたら意外と一晩で便利にうごくのができたので、こうしてblog書いてみてます。
とりあえずできた
コードはGitHubにあります、初めてosxのアプリ書きました。プロダクションコードとは程遠い遊びっぽいコードなので、そのおつもりで (どれくらいプロダクションじゃないかというと、パラメータ調整するためにはbuildしなきゃいけないくらいプロダクションじゃないですね)。
で、だいぶ手抜きしたんですが、意外に便利に動いていて、右の画面見るとわりとスムースに (でもってほぼ誤検知なく) 右画面のウィンドウがアクティブになるので (でもって正面向くともとのウィンドウがアクティブになる) だいぶ幸せです。一晩がんばった甲斐がありました。ほんとは動画とかあると素敵なんですが、はずかしいのでパスです。
振る舞いの要素としては、カメラから画像とってきて、顔認識して、顔の向き認識して、(それぞれの) ディスプレイの一番手前に表示されてるウィンドウを取得して、そのウィンドウをアクティブにする、って処理が必要で、せっかくなんでメモ書いておきます。
(0) はじめてのosxアプリ
わかってはいましたが、iOSアプリと一緒なので、今回のようなちょっとしたアプリではとくに困ることもなかったのでした (UIKit -> CocoaになっててUIがNSになってるくらい?)。
(1) カメラから画像とってくる
そもそもカメラあるのかってところについては、ディスプレイについてたのでこれをそのまま使えばいいってことになりました。で、そのデバイスから画像データを取得するのも、(osxのは全然ないですが) iOSのアプリであれこれやってるかたがたくさんいらっしゃるのでサンプルには事欠かず、とりあえず Swift + OpenCVでリアルタイムに顔認識してみた1 というまんまなエントリがあるので、Referenceみながらこちら参考にさせていただきました。
ここはもう半分くらいお約束の世界なので、とくに難しいことはなく、ウィンドウにカメラからの画像を表示するのはすぐでした...というのはちょっと見栄はってて、実はAvCaptureVideoOutputからデータを受け取るcallbackにsignatureが2つあるのに気付かず、ずっと:
1 |
func captureOutput(captureOutput: AVCaptureOutput!, didDropSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) |
で、bufferとれねー、と悩んでました...。正解はこっちですね:
1 |
func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) |
ほぼなんも意識せずに補完されたコードで動かしてたらはまりました、今振り返ればそういえば候補2つあったな、とか、そもそもちゃんと読めよ、とかあるんですが。やれやれです。
(2) 顔認識する
これも、昔よく遊んでたし、OpenCV標準ので問題なくできるだろうなー、と思ったら (まぁこういう遊びアプリ程度なら) 余裕でした。先人の知恵に頼りまくって、特にがんばったところもありません。コード的にもすごく短くて:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
NSString* pathFrontal = [bundle pathForResource:@"haarcascade_frontalface_alt" ofType:@"xml"]; std::string cascadeFrontalPath = (char*)[pathFrontal UTF8String]; if (cascadeFrontal.load(cascadeFrontalPath) == false) { return nil; } // ... cv::Mat cvImage = [image CVMat]; std::vector<cv::Rect> faces; int found = 0; // frontal cascadeFrontal.detectMultiScale(cvImage, faces, 1.1, 3, CV_HAAR_SCALE_IMAGE | CV_HAAR_DO_ROUGH_SEARCH | CV_HAAR_FIND_BIGGEST_OBJECT, cv::Size(28, 28)); if (faces.size() > 0) { found |= 0x01 << 0; } |
これくらいです。
なお、デフォルトの解像度であるところの1280x720で動かしてるとCPU 1コア占領するくらいになりました、そりゃそうですね、ってことで160x120にしてますが、十分精度でます。なお、解像度を変更するにはAVCaptureVideoOutputのvideoSettigsプロパティを利用します:
1 2 3 4 5 6 |
self.videoOutput = AVCaptureVideoDataOutput() self.videoOutput.videoSettings = [ kCVPixelBufferPixelFormatTypeKey:Int(kCVPixelFormatType_32BGRA), kCVPixelBufferWidthKey:160, kCVPixelBufferHeightKey:120, ] |
(3) 顔の向き認識する
これが絶対大変だろうなー、と思っていて、正直一晩ではこれは無理だろう、週末のんびりがんばろうかなーと思っていました。さきのFacebook postのコメントで教えていただいた FaceTrackNoIR を参考にがんばってみるか、このあたりの手法/コードをみてライブラリつくるか、あとはHeadPosePnPっていうのもすごく参考になりそう、だけど、ちょっとがんばらなきゃ感があります。
...と思いつつ、OpenCVで遊んでたらいつの間にか、というか3年くらい前に横顔用のモデルデータが追加されてたんですね、あれ、これで前見てるか横見てるかで判別しちゃえば (ディスプレイ2つなので左右どちらかは判別しなくてもいいや、という) いける...ようなきがする、とおもって試したら思いのほかちょうどよく動きそうだったので、とりあえずこれでストレスなく使えるか試してみることにしました (そして結果、わりとこれで十分でした)。
ただ、ナイーブにやっちゃってるので、画像の各フレームごとに、正面用の検出と横顔の検出で2パス走ってちょっとコストが高いんですよね、結果として1コアの10-15%程度のCPUリソースを消費し続けるアプリが出来上がりました、モバイルだと絶対使いたくないやつですね)。
(4) ウィンドウをアクティブにする
これも教えていただいた stack overflowの記事を参考にしてます。もうここ最後につくったので、正直動けばいいや感出てますね。
- CGWindowListCopyWindowInfo()でウィンドウを取得して
- 左上のX座標でどちらのディスプレイにウィンドウがあるかを判別して
- 一番上にきてるやつを判別してます (ウィンドウの並び順の取り方がよくわかんなかったのですが、リストが上から順になってるぽいので、それでよいことにしました)
で、あとはそのウィンドウのアプリケーションに対して
1 2 |
let element:AXUIElementRef = AXUIElementCreateApplication(Int32(windowOwnerPID)).takeRetainedValue() as AXUIElementRef AXUIElementSetAttributeValue(element, kAXFrontmostAttribute, kCFBooleanTrue as CFTypeRef) |
って感じでAttributeを設定しています。
その他はまったところ
Unsafe(Mutable)Pointerとかの扱いに慣れてなくて (いやそもそもswiftそんな慣れてなくて)、ちょと悩みました。CGWindowListCopyWindowInfo()が、CFDictionaryRefの配列 (CFArrayRef) を返すんですが、得られたCFArrayRefに対してCFArrayGetValueAtIndex()の戻り値はUnsafePointer
1 |
unsafeBitCast(CFArrayGetValueAtIndex(windows, i), CFDictionaryRef.self) |
で、unsafeBitCast()使いましょう、ってことなんですね。
ということで
全うなアプリに改善してくれるかたがいたらうれしいです。