作ったもの
https://itunes.apple.com/jp/app/ikasukoa/id1097968132?l=ja&ls=1&mt=8
こんな感じで試合終了時に試合結果を判定して記録してくれるアプリ
iPhoneを3脚に固定してカメラでゲーム画面を撮影しています
なんで作ったか
ちょうどOpenCVの勉強をしたかったので、いろいろ調べてみると
HDMIキャプチャ( https://github.com/hasegaw/IkaLog/blob/master/doc/IkaUI.md )
Raspberry Pi( http://hogesuke.hateblo.jp/entry/ikashot )
など、OpenCVでスプラトゥーンの試合結果を記録するものが見つかりました。
でも正直機材用意するの面倒だし、スマホでいいと思ったのと、常にストアにあるものは手書きで記録する系のアプリしか見つからなかったので作ってみました
スプラトゥーンの旬を逃しちゃってる感はあるけど、ついでなので。
仕組み
作り方
Raspberry Pi版のコードを参考に、作るの大変なcascade xmlもgithubに公開されてたので使わせてもらっています。
https://github.com/hogesuke/ika-camera/tree/master/cascade
SwiftからOpenCVを扱う方法
CocoaPodsでOpenCVをインストール
platform :ios, '8.0'
pod 'OpenCV'
Swiftから直接OpenCVは動かせず、C++で書かなきゃだめなのです
なのでheaderファイルを作ります
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Detector: NSObject
- (id)init;
- (NSString *)recognize:(UIImage *)image;
@end
そしてこっちがOpenCVを扱うコードになります。
UIImageを渡して、OpenCVで扱うデータ構造のcv::Matに変換。
そしてika_result_winとika_result_loseそれぞれで検出させます。
勝敗を文字列、認識しなかった場合は空文字で返すように実装しています。
#import <Foundation/Foundation.h>
#import "ikascore-Bridging-Header.h"
#import <opencv2/opencv.hpp>
@interface Detector()
{
cv::CascadeClassifier cascadeWin;
cv::CascadeClassifier cascadeLose;
}
@end
@implementation Detector: NSObject
- (id)init {
self = [super init];
// 分類器の読み込み
NSBundle *bundle = [NSBundle mainBundle];
NSString *pathWin = [bundle pathForResource:@"ika_result_win" ofType:@"xml"];
NSString *pathLose = [bundle pathForResource:@"ika_result_lose" ofType:@"xml"];
std::string cascadeNameWin = (char *)[pathWin UTF8String];
std::string cascadeNameLose = (char *)[pathLose UTF8String];
if(!cascadeWin.load(cascadeNameWin) || !cascadeLose.load(cascadeNameLose)) {
return nil;
}
return self;
}
- (NSString *)recognize:(UIImage *)image {
// UIImage -> cv::Mat変換
CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
CGFloat cols = image.size.width;
CGFloat rows = image.size.height;
cv::Mat mat(rows, cols, CV_8UC4);
CGContextRef contextRef = CGBitmapContextCreate(mat.data,
cols,
rows,
8,
mat.step[0],
colorSpace,
kCGImageAlphaNoneSkipLast |
kCGBitmapByteOrderDefault);
CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
CGContextRelease(contextRef);
std::vector<cv::Rect> res;
// Win検出
cascadeWin.detectMultiScale(mat, res,
1.1, 2,
CV_HAAR_SCALE_IMAGE,
cv::Size(50, 50));
std::vector<cv::Rect>::const_iterator w = res.begin();
if (w != res.end()) {
return @"win";
}
// Lose検出
cascadeLose.detectMultiScale(mat, res,
1.1, 2,
CV_HAAR_SCALE_IMAGE,
cv::Size(50, 50));
std::vector<cv::Rect>::const_iterator r = res.begin();
if (r != res.end()) {
return @"lose";
}
// 検出されない
return @"";
}
@end
あとはSwift側でカメラから作ったUIImageを随時渡す感じです。
// 顔検出オブジェクト
let detector:Detector = Detector()
let res:NSString = detector.recognize(image)
if (res == "win") {
// 勝ち
} else if (res == "lose") {
// 負け
} else {
// 試合中
}
気をつけることろ
かなり高負荷で動かしっぱなしになるので端末が熱くなります。
最初やった時に熱で電源落ちたので少し対策をする必要がありました。
カメラの解像度が高いと検出に時間がかかってしまうため、少し落としておく
mySession = AVCaptureSession()
mySession.sessionPreset = AVCaptureSessionPresetMedium
遅れてきたフレームは無視する
myOutput = AVCaptureVideoDataOutput()
myOutput.alwaysDiscardsLateVideoFrames = true
試合終了後しばらくはインターバルなので端末を休ませる
NSTimer.scheduledTimerWithTimeInterval(60.0, target: self, selector:#selector(restore), userInfo: nil, repeats: false)
ソースコード
出来上がったものはこちら
https://github.com/narikei/ikascore
おわり
台形補正がないので、カメラを真正面に向けないと認識してくれないです。位置合わせは慣れるまでシビア。
今はTwitterに吐き出すだけだけど、ちゃんとアプリの方にデータ記録したいですね。
コードもリファクタリング、というか適当に書き殴ってるので書き直さないと
プルリクください。
スプラトゥーン対戦結果:3勝3敗 #スプラトゥーン #イカスコア pic.twitter.com/UlPui0gnyD
— なりたけいすけ (@narikei1030) October 1, 2016
参考にさせてもらったURL
http://hogesuke.hateblo.jp/entry/ikashot
http://giveitashot.hatenadiary.jp/entry/2014/10/23/080930