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

## 作ったもの
[![ikascore](https://qiita-image-store.s3.amazonaws.com/0/7132/75c47628-2628-f6d3-24b3-fdc24cc41eb8.png)](https://itunes.apple.com/jp/app/ikasukoa/id1097968132?l=ja&ls=1&mt=8)
https://itunes.apple.com/jp/app/ikasukoa/id1097968132?l=ja&ls=1&mt=8

こんな感じで試合終了時に試合結果を判定して記録してくれるアプリ
![イカスコア.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7132/3a033f8d-f32c-f341-acbd-4d0479fdfa80.gif)

iPhoneを３脚に固定してカメラでゲーム画面を撮影しています
<img src="https://qiita-image-store.s3.amazonaws.com/0/7132/1e5a8fe4-cc16-e543-771f-09f845382fa0.png" width=400>

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

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

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

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

## 仕組み
![Artboard 1.png](https://qiita-image-store.s3.amazonaws.com/0/7132/10a937d8-605a-f750-5319-270f078fbdbe.png)

## 作り方

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


### SwiftからOpenCVを扱う方法

CocoaPodsでOpenCVをインストール

```rb:podfile
platform :ios, '8.0'
pod 'OpenCV'
```

Swiftから直接OpenCVは動かせず、C++で書かなきゃだめなのです
なのでheaderファイルを作ります

```objc
#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それぞれで検出させます。
勝敗を文字列、認識しなかった場合は空文字で返すように実装しています。

```objc
#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を随時渡す感じです。

```swift
// 顔検出オブジェクト
let detector:Detector = Detector()
let res:NSString = detector.recognize(image)

if (res == "win") {
  // 勝ち
} else if (res == "lose") {
  // 負け
} else {
  // 試合中
}
```


### 気をつけることろ

かなり高負荷で動かしっぱなしになるので端末が熱くなります。
最初やった時に熱で電源落ちたので少し対策をする必要がありました。

カメラの解像度が高いと検出に時間がかかってしまうため、少し落としておく

```swift
mySession = AVCaptureSession()
mySession.sessionPreset = AVCaptureSessionPresetMedium
```

遅れてきたフレームは無視する

```swift
myOutput = AVCaptureVideoDataOutput()
myOutput.alwaysDiscardsLateVideoFrames = true
```


試合終了後しばらくはインターバルなので端末を休ませる

```swift
NSTimer.scheduledTimerWithTimeInterval(60.0, target: self, selector:#selector(restore), userInfo: nil, repeats: false)
```


## ソースコード
出来上がったものはこちら
https://github.com/narikei/ikascore

## おわり
台形補正がないので、カメラを真正面に向けないと認識してくれないです。位置合わせは慣れるまでシビア。

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

プルリクください。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">スプラトゥーン対戦結果：3勝3敗 <a href="https://twitter.com/hashtag/%E3%82%B9%E3%83%97%E3%83%A9%E3%83%88%E3%82%A5%E3%83%BC%E3%83%B3?src=hash&amp;ref_src=twsrc%5Etfw">#スプラトゥーン</a> <a href="https://twitter.com/hashtag/%E3%82%A4%E3%82%AB%E3%82%B9%E3%82%B3%E3%82%A2?src=hash&amp;ref_src=twsrc%5Etfw">#イカスコア</a> <a href="https://t.co/UlPui0gnyD">pic.twitter.com/UlPui0gnyD</a></p>&mdash; なりたけいすけ (@narikei1030) <a href="https://twitter.com/narikei1030/status/782315151672299520?ref_src=twsrc%5Etfw">October 1, 2016</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


### 参考にさせてもらったURL
http://hogesuke.hateblo.jp/entry/ikashot
http://giveitashot.hatenadiary.jp/entry/2014/10/23/080930