今度はiOSで散布図アニメーションを試してみた

セルフインスパイアな記事。先日Pythonによる散布図アニメーションを取り上げました。

pythonで散布図アニメーションを試してみた - 株式会社CFlatの明後日スタイルのブログpythonで散布図アニメーションを試してみた - 株式会社CFlatの明後日スタイルのブログはてなブックマーク - pythonで散布図アニメーションを試してみた - 株式会社CFlatの明後日スタイルのブログ

これと同じようなことをiPhoneでやってみました。

事前準備

アニメーションに使う動画を適当に入手し、フレームを画像にして出力しておきます。

$ ffmpeg -i input.mp4 -f image2 frame%03d.jpg

開発環境

iOS開発なのでXcodeを使います。XcodeでSingle View Applicationあたりで新規プロジェクトを作ります。

また、オープンソースのライブラリも必要となります。

CocoaPodsを導入し(CocoaPods.org - The Dependency Manager for Objective C.)、 依存するライブラリを$ pod installします。

プロジェクトのルートディレクトリにPodfileを作り次の内容を記載、からの$ pod installでインストールが完了するのでxcworkspaceのほうを開きます。

# Podfile
pod 'CorePlot' # グラフ描画
pod 'OpenCV'   # 画像処理

また、事前準備で作った画像ファイルをプログラムから使うためにプロジェクトに追加しておきます。

まずは結果から

コード紹介

全体の流れはこんな感じ

  1. 起動時に各画像を読み込んで散布図用の点を計算しておく
  2. スタートボタンでタイマーをスタート
  3. 時間が進むごとにグラフを書き換える

画像の読み込み

Single View Applicationのサンプルアプリケーションなので細かいことは気にせず、viewDidLoadで画像の読み込みを行います。

ここでは100個分を読み込み対象とし、OpenCVのFastFeatureDetectorで抽出した特徴点(X,Y)のvectorを得て、それを1コマ分として_plotsというvectorに保持しておきました。

// ViewController.mm <- C++を使うために.mではなく.mmに変更

#import "ViewController.h"

#import <opencv2/opencv.hpp>
#import <opencv2/highgui/ios.h>
#import <vector>

std::vector< std::vector<cv::KeyPoint> > _plots;

- (void)viewDidLoad
{
    // ... 

    // あらかじめ画像を読み込んで特徴量の検出を済ませておく
    [self loadFrames];
}

- (void)loadFrames
{
    for(NSUInteger i=1; i<=100; ++i){
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"frame%03d.jpg", i]];
        cv::Mat mat;
        UIImageToMat(image, mat);
        
        cv::FastFeatureDetector det = cv::FastFeatureDetector();
        
        std::vector<cv::KeyPoint> points;
        det.detect(mat, points);
        
        // グラフのY軸と画像のY軸が反対なので値を反転する
        for(size_t j=0; j<points.size(); ++j){
            points[j].pt.y = 360 - points[j].pt.y;
        }
        
        _plots.push_back(points);
    }
}

タイマー処理

続いてアニメーションに必要なタイマー処理の部分です。最初の処理で100枚分の画像データを読み込みました。アニメーションをするには時間経過にしたがって表示するデータを少しずつ変えていけばいいのでタイマーを使います。

タイマーは標準のNSTimer、0.1秒ごとにstepAnimationを呼び出し、インデックスをインクリメントしています。このインデックスは表示データの配列を指すインデックスで、タイマーによってこのインデックスを変えていくことで描画するデータを切り替えていくということをやっています。

インデックスを増やしたら(または最初に戻したら)、グラフの再描画を明示的に呼び出します。[self.graph reloadData]というのがそれに該当します。グラフについては次の節で。

NSUInteger _currentIndex; // 0に初期化しておく

- (IBAction)start:(UIButton*)sender
{
    if ([self.timer isValid] ){
        [self.timer invalidate];
        [sender setTitle:@"Start" forState:UIControlStateNormal];
    }else{
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(stepAnimation) userInfo:nil repeats:YES];
        [sender setTitle:@"Stop" forState:UIControlStateNormal];
    }
}

- (void)stepAnimation
{
    _currentIndex++;
    if( _currentIndex >= _plots.size() ){
        _currentIndex = 0;
    }
    [self.graph reloadData];
}

グラフ描画

Graphの描画にはCorePlotというライブラリを使用しました。汎用性の高いOSSのライブラリです。

core-plot - Cocoa plotting framework for OS X and iOS - Google Project Hosting

ビューやグラフを作る

グラフ表示周りのコードを紹介するにあたり、見た目上の細かい設定がかなり多くて煩雑になるので、その部分を省略して掲載します(最後にコード全文のリンクを紹介します)。

- (UIView*)createGraphView
{
    CPTGraphHostingView *graphView = [[CPTGraphHostingView alloc] initWithFrame:rect];
    CPTGraph *graph = [[CPTXYGraph alloc] initWithFrame:rect];
    graphView.hostedGraph = graph;
    
    CPTScatterPlot *scatterPlot = [CPTScatterPlot new];
    scatterPlot.dataSource = self;
    [graph addPlot:scatterPlot];
    
    self.graph = graph;
    return graphView;
}

イメージ的にはこんな感じの階層で参照を持つようになっています。

  • CPTGraphHostingView
    • CPTGraph
      • CPTScatterPlot

CPTGraphHostingViewには対応するCPTGraphがあって、そのCPTGraphのなかにはCPTScatterPlotやその他のプロット(棒グラフなど)を複数持てるようになっています。

生成したCPTGraphHostingViewaddSubView:しておきます。

- (void)viewDidLoad
{
    // ...

    // グラフ描画の設定
    UIView* graphView = [self createGraphView];
    [self.view addSubview:graphView];
}

表示データの取り扱い

CorePlotで表示するデータはDataSourceプロトコルを実装することでCorePlot側に教えてあげる仕組みになっています。UITableViewとほぼ同じ仕組みなのでiOS開発者であれば問題ないはず。

今回はViewControllerをそのままDataSourceにします。

// ViewController.h

#import <UIKit/UIKit.h>
#import <CorePlot/CorePlot-CocoaTouch.h>

@interface ViewController : UIViewController<CPTPlotDataSource>
@end

あとはDataSourceのインターフェースを実装してあげればいいのですが、そのときにタイマーによって変化するインデックスに応じたデータを返すことでアニメーションを実現しているわけです。

// プロットするデータの数を返す
- (NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot
{
    return _plots[_currentIndex].size();
}

// idx番目に関する値を返す
// XなのかYなのかはfieldNumに応じて変える
- (NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)idx
{
    NSNumber *num;
    std::vector<cv::KeyPoint>& points = _plots[_currentIndex];
    switch (fieldEnum) {
        case CPTScatterPlotFieldX:
            num = [NSNumber numberWithFloat:points[idx].pt.x];
            break;
        case CPTScatterPlotFieldY:
            num = [NSNumber numberWithFloat:points[idx].pt.y];
        default:
            break;
    }
    return num;
}

これでグラフの描画もできました。起動してボタンを押せばアニメーションがスタートします!

ソースコード

CorePlotAnimation — Bitbucket