Help us understand the problem. What is going on with this article?

スプラトゥーンの勝敗を画像認識で記録するiOSアプリを作ってみました

作ったもの

ikascore
https://itunes.apple.com/jp/app/ikasukoa/id1097968132?l=ja&ls=1&mt=8

こんな感じで試合終了時に試合結果を判定して記録してくれるアプリ
aaa-compressor.gif

iPhoneを3脚に固定してカメラでゲーム画面を撮影しています

なんで作ったか

ちょうどOpenCVの勉強をしたかったので、いろいろ調べてみると

HDMIキャプチャ( https://github.com/hasegaw/IkaLog/blob/master/doc/IkaUI.md
Raspberry Pi( http://hogesuke.hateblo.jp/entry/ikashot

など、OpenCVでスプラトゥーンの試合結果を記録するものが見つかりました。
でも正直機材用意するの面倒だし、スマホでいいと思ったのと、常にストアにあるものは手書きで記録する系のアプリしか見つからなかったので作ってみました

スプラトゥーンの旬を逃しちゃってる感はあるけど、ついでなので。

仕組み

Artboard 1.png

作り方

Raspberry Pi版のコードを参考に、作るの大変なcascade xmlもgithubに公開されてたので使わせてもらっています。
https://github.com/hogesuke/ika-camera/tree/master/cascade

SwiftからOpenCVを扱う方法

CocoaPodsでOpenCVをインストール

podfile
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に吐き出すだけだけど、ちゃんとアプリの方にデータ記録したいですね。
コードもリファクタリング、というか適当に書き殴ってるので書き直さないと

プルリクください。

参考にさせてもらったURL

http://hogesuke.hateblo.jp/entry/ikashot
http://giveitashot.hatenadiary.jp/entry/2014/10/23/080930

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした