読者です 読者をやめる 読者になる 読者になる

React & Flummoxチュートリアル

Flummoxは、いくつもあるFlux実装のうちの1つです。

ドキュメントにはいくつかの特徴が挙げられていますが、 Isomorphicであること(サーバとクライアントのどちらでもOK!)、ES6記法に対応していることなどがあります。

今回は、簡単なTODOアプリを作りながらFlummoxの使い方を紹介しようと思います。

このチュートリアルでは、ReactやFluxの初心者を対象にしています。簡単な概要ぐらいは知っているけど、これから始めるために簡単なサンプルが欲しいというような人にちょうど良いと思いますが、React全く初めてでも順を追えばなんとなくやっていることがわかると思います。

ソースコードこちらです。

<完成イメージ>

f:id:gibachan03:20150429140911p:plain

準備

コードを書き始める前にいくつか必要となるツールがありますので紹介します。

Gulp

よく使われているタスクランナーです。この後でコードを変換する必要が出てくるので、このツールを使ってその処理を自動化させます。

こちらはグローバルな環境にインストールしておく必要があるので、無い場合は次のようにインストールしましょう。

npm install -g gulp

Browserify

JavaScriptでモジュール管理をするためのツールです。JavaScriptのファイルを複数に分けておき、必要なモジュールをrequireで取得できるようにしてくれます。

Babel

ここではJavaScriptをES6記法で記述していくのですが、実際にブラウザで動作させるにはES5に変換する必要があります。Babelはその変換をしてくれるツールです。

面倒ですが、これらのツールを使ってコードを書く準備をしましょう。 まずはnpmでReactとFlummoxをインストールし、別に開発に必要となるツールを開発環境用にインストールします。

mkdir flummox-excersize && cd flummox-excersize

npm init

npm install --save react flummox

npm install --save-dev babelify browserify gulp vinyl-source-stream

babelifyはBrowserifyでBabelの変換をしてくれるモジュールで、vinyl-source-streamは、最終的に1つのJavaScriptファイルにまとめるためのモジュールです。

インストールが済んだら、gulpfile.jsを作成し、Gulpで実行する作業を書きます。

gulp

詳細に説明はしませんが、これでコマンドにgulpと打つだけでコードの変換が行われます。 今の時点ではファイルが無いのでエラーになります。

React - Hello World -

まずはReactでブラウザにHello Worldを表示させましょう。

Appコンポーネント

アプリのルートとなるコンポーネントを/components/App.jsxとして作成します。

import React from 'react';

class App extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div>
        <h1>Hello World</h1>
      </div>
    );
  }
}

export default App;

ES6記法で書いています。そのためReactのバージョンが0.13以上である必要があります。

app.js

次に、このコンポーネントを使用して描画するコードをapp.jsに書きます。

import React from 'react';

import App from './components/App.jsx';

React.render(<App />,
  document.getElementById('app'));

こちらもES6記法である以外は問題無いと思います。

index.html

最後にブラウザで表示されるindex.htmlを書きます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Flummox TODO</title>
</head>
<body>
  <div id="app"></div>

  <script src="build/bundle.js"></script>
</body>
</html>

ここで読み込んでいるbundle.jsは、gulpで変換して出力されるファイルです。

gulp

ブラウザでindex.htmlを開いて、"Hello World"が表示されたら成功です。

次からFlummoxを使って行きます。また、次からスペースの都合上コードは全て掲載しません。 必要であればサンプルコードをダウンロードしてご覧ください。

Action

Actionはビュー(Component)等で発生したイベントに応じて動作します。 モデル(Store)はActionを監視するので、Actionが動作したらそれに応じて内部のデータを変化させます。

ここではTODOアプリを作るので、TODOの新規作成、削除、終了フラグの切り替えのActionを定義します。

actions/Actions.js

class TodoActions extends Actions {
  // Todo新規作成
  createTodo(text) {
    return text;
  }

  // Todo削除
  deleteTodo(id) {
    return id;
  }

  // Todo終了フラグの切り替え
  toggleTodo(id) {
    return id;
  }
}

ActionsはFlummoxのActionsクラスを継承します。

Store

Storeはモデル(データ)を格納し、その状態を管理します。 Storeは、Actionsからの操作によりその状態を変化させ、その他からの影響は受けません。 また、Storeの状態が変化するとビュー(Component)がそれに応じて更新されます。

stores/TodoStore.js

import { Store } from 'flummox';

class TodoStore extends Store {
  ...
}

export default TodoStore;

StoreはFlummoxのStoreを継承します。

まずコンストラクタで、ActionとStoreの関連付けを行います。

constructor(flux) {
  super();

  // fluxインスタンスからActionsを取得
  const todoIds = flux.getActionIds('todo');

  // ActionとStoreメソッドを関連付け
  this.register(todoIds.createTodo, this.handleCreateTodo);
  this.register(todoIds.updateTodo, this.handleUpdateTodo);
  this.register(todoIds.deleteTodo, this.handleDeleteTodo);
  this.register(todoIds.toggleTodo, this.handleToggleTodo);

  // Storeの初期状態(初期データ)
  this.state = {
    todos: {}
  };

Actionが発生した場合、StoreはSetState状態を変化させます。

handleCreateTodo(text) {
  ...

  // 状態を変化
  this.setState({ todos });
}

setStateを呼ぶと状態が変化し、それがComponentへ伝えられる仕組みになっています。

Flux

ActionsとStoreを作ったので、これらを元にFluxインスタンスを作成します。

app.js

class Flux extends Flummox {
  constructor() {
    super();

    this.createActions('todo', Actions);
    this.createStore('todo', TodoStore, this);
  }
}

const flux = new Flux();

FluxはFlummoxを継承します。

fluxインスタンスを作成したらAppのComponentにインスタンスを渡します。

React.render(
  <FluxComponent flux={flux}>
    <App />
  </FluxComponent>,
  document.getElementById('app'));

ここで、FlummoxのFluxComponentというComponentを使用しています。 このComponentは、その配下のComponent(App)にFluxインスタンスを渡す役割があります。 つまり次のように書くのと同じイメージです。

<App flux={flux} />

ですがFluxComponentは便利な機能を持っています。 例えばAppの配下に子Componentがあり、その子ComponentもFluxインスタンスを必要とするのであれば、毎回このようにFluxインスタンスを渡してあげる必要があります。 ですがFluxComponentを使うとその記述を省くことができるようになります。 このような機能については次以降で確認できます。

Component

ComponentはStoreの状態を監視し、Storeの状態が変わったらComponentを更新します。 また、ボタンクリック等のユーザ操作に応じてActionを発生させます。

ここではComponentの階層構造を次のように考えます。

App
  - TodoEdit : Todo新規追加フォーム
  - TodoList : Todoリスト
    - TodoListItem

まず、components/App.jsxを書き換えます。

class App extends React.Component {
  ...
  render() {
    return (
      <div>
        <h1>Todo list</h1>
        <FluxComponent>
          <TodoEdit />
        </FluxComponent>
        <FluxComponent connectToStores={['todo']}>
          <TodoList />
        </FluxComponent>
      </div>
    );
  }
}

ここで、TodoEdit Componentの親としてFluxComponentを指定しています。 これにより、App Componentからfluxインスタンスが自動的に渡され、TodoEdit内でthis.props.fluxの形で使用できるようになります。

components/TodoEdit.jsx

...
handleSubmit(e) {
  e.preventDefault();
  if (this.state.newTodo.length === 0) return;

  // fluxインスタンスからActionsを取得し、Actionsを発生させる
  this.props.flux.getActions('todo').createTodo(this.state.newTodo);

  this.setState({
    newTodo: ''
  });
}
...

handleSubmitは新規Todo作成フォームのSubmitイベントハンドラです。 この中でActionを発生させ、その結果Storeの状態が変化することになります。

また、TodoList Componentの親のFluxComponentにはconnectToStoresの指定をしています。 これにより、'todo'で識別されるStore(TodoStore)がTodoListのpropsに展開されます。

components/TodoList.jsx

render() {
  var todos = this.props.todos;
  ...
}

結果

動作を確認しましょう。

gulpを実行した後にindex.htmlを開くとTodoアプリが動きます。 f:id:gibachan03:20150429140911p:plain

確認して頂きたいのはデータやイベントが常に1方向に流れていることです。

  • Todo新規作成 (ユーザがテキストを入力、ボタンクリック) => TodoActions(ceateTodo) => TodoStore => TodoList => TodoListItem

  • Todo削除 TodoListItem => (ユーザが削除ボタンクリック) => TodoActions(deleteTodo) => TodoStore => TodoList

このような構造でアプリを作る考え方がFluxというアーキテクチャであるということです。 より詳細な説明は公式をご確認ください。

まとめ

Reactを触りだすとFluxという概念が出てきます。初めてみると結構複雑そうで面倒な印象を持つかもしれません。 ですが、使ってみるとそれほど難しくないことがわかります。 なので、このチュートリアルがFlummoxもしくはReact/Fluxの最初の一歩として、ちょっとでも何かの参考になれば嬉しいです。

あと何か不足や間違いなどありましたらご連絡頂けるととてもありがたいです。 => Twitter@gibachan03

Flux入門してみた

Fluxとは?

Fluxはアプリケーションアーキテクチャの1つです。要は"こんな構造でアプリを構成すると見通しが良くなるよ"というような方針みたいなものですね。

元々はFacebookが提唱した考え方で、Reactと共に用いられるアーキテクチャです。

Reactでアプリを構築する際に、ある程度アプリが複雑になってくるとデータやイベントの流れが判りにくくなるので、Fluxの考え方を利用することで綺麗にまとめられるようになります。

こちらの記事にも少し詳しく紹介されています。最初見た時はちょっと複雑そうで拒否反応が起こりましたが、実際触ってみるとそんなでもないので大丈夫です。

で、Fluxの考え方を実際にコードに落とした実装はいくつもあり、どれを使うか迷います。 今回はわかり易そうだったFluxxorRefluxJSを使って簡単なアプリを作ってみました。

つくってたもの

B-Viewer はてなブックマークのホットエントリを閲覧するためのChrome拡張機能です。Fluxxorを使用しています。

f:id:gibachan03:20150406164038p:plain

piccheck 画像ファイルに埋め込まれているEXIFのデータを閲覧するWebアプリです。Refluxを使用しています。

f:id:gibachan03:20150406164033p:plain

感想

Fluxの考え方ではデータやイベントの流れが常に一方向に流れます。

データの流れ  : Store -> View
イベントの流れ:          View -> Action -> Store 

(かなり簡略化しています。詳しくは上記の記事をご覧下さい)

これが意識できるとアプリの構造をシンプルに考えられます。ReactはVirtualDOMについて良く取り上げられてるみたいですが、React & Fluxでアプリの構造全体がシンプルに記述できるところが個人的に気に入りました。

FluxxorもRefluxJSも基本的には考え方は同じだし、正直どちらも簡単なアプリだったので、あんまり比較はなりませんでしたね。 個人的にはRefluxの方がActioins等が簡略的に書けるので好みです。 ただRefluxJSについてはネット上の情報が少ないようです。 せっかくなので、後で使い方の解説みたいなものを書けたらと思っています。

もしこれから始めてみようという方の参考になればと思い、ここで公開しておきます。

Backbone.jsを使ってみて気になったこと

書籍の新着情報をチェックするWEBサービスmybookifyを作った時に、クライアントサイドのフレームワークとしてBackbone.jsを使用しました。

その時に気になったことがいくつかあったので、ここでまとめてみました。Backbone.jsの経験は少ないのでおかしな点があるかもです。何か気づいたことがありましたら教えてもらえると嬉しいです。

ここで使用したBackbone.jsのバージョンは1.1.2でした。

Model初期化時に引数を渡したい

Modelのメンバのデフォルト値は、defaultsを設定してあげれば良いです。

var Book = Backbone.Model.extend({
  defaults: {
    'title': 'MyBook'   // デフォルト値
  }
});

var book = new Book(); // => Object {title: "MyBook"}

ですが、インスタンス化するタイミングで値を設定したい時には次のようにオブジェクトを渡してあげます。

var Book = Backbone.Model.extend({
  defaults: {
    'title': 'MyBook'   // デフォルト値(この値は無視される)
  }
});

var book = new Book({
  'title': 'NewMyBook'
});
console.log(book.toJSON()); // => Object {title: "NewMyBook"}

ビューが2つあるとき、片方のビューで発生したイベントに対してもう片方のビューで処理したい時

これにはこちらで紹介されている方法が良さそうでした。

異なるビュー間のイベントを、どちらのビューからもアクセスできる別のオブジェクト(mediator)を経由する方法です。 次の例では、ViewAとViewBがある時に、ViewAでクリックされたイベントに対してViewBで処理を行っています。

// mediatorを定義
mediator = {};
_.extend(mediator, Backbone.Events);

// ViewAの定義
var ViewA = Backbone.View.extend({
  el: '#ViewA',

  events: {
    'click': 'OnViewAClick'            // 自分のビュー内のイベントを補足する
  },

  OnViewAClick: function() {
    mediator.trigger('OnViewAClick');  // mediatorにイベントを伝える
  }
});

// ViewBの定義
var ViewB = Backbone.View.extend({
  el: '#ViewB',

  initialize: function() {
    this.listenTo(mediator, 'OnViewAClick', this.OnViewBClick); // mediatorから伝わるイベントを拾う
  },

  OnViewBClick: function() {
    console.log('Hit!');
  }
});

ModelとREST APIとの連携

REST APIと対応付けるためには、Modelを次のように定義します。

var Book = Backbone.Model.extend({
  urlRoot: '/mybook-api',   // APIのurl

  idAttribute: '_id',       // ModelインスタンスのID
});

これによりGET、POST、PUT 、DELETEのリクエストを発行できるようになります。

発行先は/mybook-api/(_idの値)となり、それぞれ次のメソッドが対応しています。

var book = new Book();
// インスタンスがIDを持っていない時
console.log(book.isNew());  // => true

book.fetch();     // GET  .../mybook-api
book.save();      // POST .../mybook-api
book.destroy());  // リクエストは発行されない

// インスタンスがIDを持っている時
book.set('_id', 'myid');    // 仮にIDをmyidとしておく
console.log(book.isNew());  // => false

book.fetch();     // GET  .../mybook-api/myid
book.save();      // PUT .../mybook-api/myid
book.destroy();   // DELETE  .../mybook-api/myid

また、それぞれのメソッドではリクエスト成功時と失敗時の処理をコールバックで受け取ります。詳しくはドキュメントを確認して下さい。そしてこれらのメソッドは内部でsyncメソッドを呼んでいます。リクエストを細かくカスタマイズしたい場合はsyncメソッドを自分で再定義することになります。

続いて、モデルにparseメソッドを定義しておくと、サーバから返ってくるレスポンスを自分好みに処理できます。

var Book = Backbone.Model.extend({
    urlRoot: '/mybook-api',

    idAttribute: '_id',

    parse: function(response, options) {
      // レスポンス例: response == { status: 'success', book: { 'title': 'mybook' } }

      // レスポンスを処理して、モデルオブジェクトを返す
      return response.book;
    }
  });

複数のViewからアクセスしたいModelの持ち方(未解決)

BackboneではModelまたはCollectionはViewと1:1の関係になると思っています。

var MyModel = Backbone.Model.extend({});
var MyView = Backbone.View.extend({ model: MyModel });

var myModel = new MyModel();
var myView = new MyView({
  model: myModel
});

でも、Viewを越えて別のModelを参照したい時にはどうするのが良いのかわかりませんでした。

例えばAというModelとBというModelがあります。そしてViewAとViewBという、それぞれに対応するViewがあります。 このとき、ViewBでBを描画する際に、Aの値によって表示を変えるとします。

こんなときにどのような構造にすれば良いかわかりませんでした。 現状はAをグローバルなオブジェクトにして、どこからでもアクセスできるようにして対応しているのですが、あんまり綺麗じゃないような気がする。。。

取り敢えずこんなところです。まだちょっと思考錯誤しながらBackboneを使ってコードを綺麗にまとめられる方法を模索してます。もしかしたらAngularとか別のフレームワークに手を出したほうが良いのかなぁ?

ニコニコ動画のiPhoneアプリ作ってみた

初めてSwiftでアプリを作りました。せっかく作ったのでここで公開しときます。
試行錯誤しながらコツコツ頑張りました。

App Storeには公開するつもりはありません。著作権の問題がありそうですしね...
(よくわかんないけど個人で楽しむ分にはOKかな?)

機能

ニコニコ動画の「歌ってみた」カテゴリの動画をよく見るので、「歌ってみた」に特化したアプリです。
動画をダウンロードし、オーディオファイルに変換していつでも楽しめるようしています。
ミュージックプレイヤとしては基本的な機能しか実装できていないのですが、最初なのでここまでやれば取り敢えずいいかなぁと。

f:id:gibachan03:20141218213130p:plain f:id:gibachan03:20141218213137p:plain f:id:gibachan03:20141218213142p:plain

ソース

一応コードをgithubにあげておきます。swiftでの書き方に不安があるので、誰か奇特な方がダメ出しをしてくれるといいなぁ・・・
gibachan/NicoMusic · GitHub

Swift + OpenCVでリアルタイムに顔認識してみた2

前回に引き続き、今回はOpenCVを使って顔認識を行います。

分類器

画像データの中から顔を検出するための分類器となるファイルを用意します。
Githubからこちらをダウンロードします。
ダウンロードしたデータの中から、data/haarcascades/haarcascade_frontalface_alt.xmlファイルを取り出し、プロジェクトに追加します。

顔認識

OpenCVの処理はC++で書く必要があります。そのためにObjective-Cでラップしてあげます。
まずXcodeで「New File」から「Objective-C」のファイルを追加します。すると「Would you like to configure an Objective-C bridging header?」と聞かれるのでYesとします。 作成された.mファイルの名前を変更して.mmファイルとするとC++が使用できます。

では処理を書いていきます。

OpenCVSample-Bridging-Header.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface Detector: NSObject

- (id)init;
- (UIImage *)recognizeFace:(UIImage *)image;

@end

Detector.mm


#import "OpenCVSample-Bridging-Header.h"
#import <opencv2/opencv.hpp>
#import <opencv2/highgui/ios.h>

@interface Detector()
{
    cv::CascadeClassifier cascade;
}
@end

@implementation Detector: NSObject

- (id)init {
    self = [super init];
    
    // 分類器の読み込み
    NSBundle *bundle = [NSBundle mainBundle];
    NSString *path = [bundle pathForResource:@"haarcascade_frontalface_alt" ofType:@"xml"];
    std::string cascadeName = (char *)[path UTF8String];
    
    if(!cascade.load(cascadeName)) {
        return nil;
    }
    
    return self;
}

- (UIImage *)recognizeFace:(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> faces;
    cascade.detectMultiScale(mat, faces,
                             1.1, 2,
                             CV_HAAR_SCALE_IMAGE,
                             cv::Size(30, 30));

    // 顔の位置に丸を描く
    std::vector<cv::Rect>::const_iterator r = faces.begin();
    for(; r != faces.end(); ++r) {
        cv::Point center;
        int radius;
        center.x = cv::saturate_cast<int>((r->x + r->width*0.5));
        center.y = cv::saturate_cast<int>((r->y + r->height*0.5));
        radius = cv::saturate_cast<int>((r->width + r->height));
        cv::circle(mat, center, radius, cv::Scalar(80,80,255), 3, 8, 0 );
    }
    
    
    // cv::Mat -> UIImage変換
    UIImage *resultImage = MatToUIImage(mat);
    
    return resultImage;
}

@end

イニシャライザで分類器を読み込んでおきます。
recognizeFaceで受け取ったUIImageをOpenCVで扱うデータ構造のcv::Matへ変換し、顔検出の処理を行ったあと、最後にまたUIImageへ変換して返しています。

実は、UIImage->cv::Mat変換ではcvMatFromUIImageメソッドがあり、簡単に変換できるようなのですが、何度か繰り返しこのメソッドを実行していくと不意に実行時エラーとなってしまいました。原因不明だったので、その処理を置き換えています。

最後にカメラからの画像データを変換する処理を次のように変更して終わりです。

    // 顔検出オブジェクト
    let detector = Detector()

   // ---------- 省略 ---------- //


    // 毎フレーム実行される処理
    func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
    {
        dispatch_async(dispatch_get_main_queue(), {
            // UIImageへ変換
            let image = CameraUtil.imageFromSampleBuffer(sampleBuffer)
            
            // 顔認識
            let faceImage = self.detector.recognizeFace(image)
            
            // 表示
            self.imageView.image = faceImage
        })
    }

これで実行すると、カメラに写した顔の位置に丸が描かれるはずです。
(何にも場所に反応されるとちょっとビビります・・・)

たぶん顔認識の処理のところで工夫すると、パフォーマンスをあげられるんだろうけど良く分かりません!!
どなたか教えてくださいませませ。

Githubにソースを上げておきます。

参考

OpenCV iOS - Image Processing

2014/11/30 追記

上記のコードにはメモリ処理上の問題があることを教えていただきました!
[自在]OpenCVかCIDetectorを使ってiOSで顔認識してみよう!

dispatch_asyncをdispatch_syncに変更で、明らかにメモリ使用量が改善します。

感謝感謝!

Swift + OpenCVでリアルタイムに顔認識してみた1

iPhoneのカメラを使って、リアルタイムに顔認識する機能を調べてみました。

まずはカメラで撮影し、それを画面に表示させるところまで作ります。
その後にカメラから入ってきた画像データをOpenCVでごにょごにょして顔認識を行います。

プロジェクト作成

新しいSingle View Applicationのプロジェクトを作成します。
カメラを使うので実機で動作確認する必要がありますので、そのための必要な設定を行っておきます。

OpenCVインストール

OpenCVはCocoaPodsを使ってインストールします。 一旦プロジェクトを閉じてターミナルを実行し、プロジェクトのディレクトリへ移動します。
「Podfile」ファイルを作成し、次の通りに記述します。

platform :ios, "7.0"
pod 'OpenCV'

ターミナルでpod installとすると、OpenCVがインストールされ、.xcworkspaceファイルが作成されます。 Xcodeで.xcworkspaceファイルを開き、ここからプログラムを書いていきます。 f:id:gibachan03:20141019175317p:plain

UI

StoryBoardでImageViewを作成し、ViewController.swiftにOutletを作成します。 f:id:gibachan03:20141019185908p:plain

カメラから画像データを取得

ViewController.swiftにカメラで動画を処理するコードを記述します。

ViewController.swift

//
//  ViewController.swift
//  OpenCVSample
//
//  Created by gibachan on 2014/10/19.
//  Copyright (c) 2014年 gibachan. All rights reserved.
//

import UIKit
import AVFoundation

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {

    @IBOutlet weak var imageView: UIImageView!

    // セッション
    var mySession : AVCaptureSession!
    // カメラデバイス
    var myDevice : AVCaptureDevice!
    // 出力先
    var myOutput : AVCaptureVideoDataOutput!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // カメラを準備
        if initCamera() {
            // 撮影開始
            mySession.startRunning()
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    // カメラの準備処理
    func initCamera() -> Bool {
        // セッションの作成.
        mySession = AVCaptureSession()
        
        // 解像度の指定.
        mySession.sessionPreset = AVCaptureSessionPresetMedium
        
        
        // デバイス一覧の取得.
        let devices = AVCaptureDevice.devices()
        
        // バックカメラをmyDeviceに格納.
        for device in devices {
            if(device.position == AVCaptureDevicePosition.Back){
                myDevice = device as AVCaptureDevice
            }
        }
        if myDevice == nil {
            return false
        }
        
        // バックカメラからVideoInputを取得.
        let myInput = AVCaptureDeviceInput.deviceInputWithDevice(myDevice, error: nil) as AVCaptureDeviceInput
        
        
        // セッションに追加.
        if mySession.canAddInput(myInput) {
            mySession.addInput(myInput)
        } else {
            return false
        }
        
        // 出力先を設定
        myOutput = AVCaptureVideoDataOutput()
        myOutput.videoSettings = [ kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_32BGRA ]
        
        // FPSを設定
        var lockError: NSError?
        if myDevice.lockForConfiguration(&lockError) {
            if let error = lockError {
                println("lock error: \(error.localizedDescription)")
                return false
            } else {
                myDevice.activeVideoMinFrameDuration = CMTimeMake(1, 15)
                myDevice.unlockForConfiguration()
            }
        }
        
        // デリゲートを設定
        let queue: dispatch_queue_t = dispatch_queue_create("myqueue",  nil)
        myOutput.setSampleBufferDelegate(self, queue: queue)

        
        // 遅れてきたフレームは無視する
        myOutput.alwaysDiscardsLateVideoFrames = true
        
        
        // セッションに追加.
        if mySession.canAddOutput(myOutput) {
            mySession.addOutput(myOutput)
        } else {
            return false
        }
        
        // カメラの向きを合わせる
        for connection in myOutput.connections {
            if let conn = connection as? AVCaptureConnection {
                if conn.supportsVideoOrientation {
                    conn.videoOrientation = AVCaptureVideoOrientation.Portrait
                }
            }
        }
 
        return true
    }


    // 毎フレーム実行される処理
    func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
    {
        dispatch_async(dispatch_get_main_queue(), {

            /**
             *  ここでSampleBufferからUIImageを作成し、imageViewへ反映させる
             */

        })
    }
    

}

このようにすると、カメラで撮影しているデータが毎フレームcaptureOutputメソッドへ送られます。(引数のsampleBuffer)

画像データを表示

次に、sampleBufferをUIImageへ変換します。ここでは新しいクラスを作成してその中に処理を書きます。

CameraUtil.swift

//
//  CameraUtil.swift
//  OpenCVSample
//
//  Created by gibachan on 2014/10/19.
//  Copyright (c) 2014年 gibachan. All rights reserved.
//

import Foundation
import UIKit
import AVFoundation

class CameraUtil {
    
    // sampleBufferからUIImageへ変換
    class func imageFromSampleBuffer(sampleBuffer: CMSampleBufferRef) -> UIImage {
        let imageBuffer: CVImageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)
        
        // ベースアドレスをロック
        CVPixelBufferLockBaseAddress(imageBuffer, 0)
        
        // 画像データの情報を取得
        let baseAddress: UnsafeMutablePointer<Void> = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, UInt(0))
        
        let bytesPerRow: UInt = CVPixelBufferGetBytesPerRow(imageBuffer)
        let width: UInt = CVPixelBufferGetWidth(imageBuffer)
        let height: UInt = CVPixelBufferGetHeight(imageBuffer)
        
        // RGB色空間を作成
        let colorSpace: CGColorSpaceRef = CGColorSpaceCreateDeviceRGB()
        
        // Bitmap graphic contextを作成
        let bitsPerCompornent: UInt = 8
        var bitmapInfo = CGBitmapInfo((CGBitmapInfo.ByteOrder32Little.toRaw() | CGImageAlphaInfo.PremultipliedFirst.toRaw()) as UInt32)
        let newContext: CGContextRef = CGBitmapContextCreate(baseAddress, width, height, bitsPerCompornent, bytesPerRow, colorSpace, bitmapInfo) as CGContextRef
        
        // Quartz imageを作成
        let imageRef: CGImageRef = CGBitmapContextCreateImage(newContext)
        
        // UIImageを作成
        let resultImage: UIImage = UIImage(CGImage: imageRef)
        
        return resultImage
    }

}

そして、変換したUIImageをUIImageViewへ表示してあげれば撮影できている様子が確認できます。

ViewController.swift

    // 毎フレーム実行される処理
    func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!)
    {
        dispatch_async(dispatch_get_main_queue(), {
            // UIImageへ変換して表示させる
            self.imageView.image = CameraUtil.imageFromSampleBuffer(sampleBuffer)
        })
    }

参考

AV Foundation プログラミングガイド

長くなって来たで今回はここまでです。
これだけでも画像をいろいろいじって遊べそうですね!
次回にOpenCVを使って顔認識を試してみます。

Alloyでのmigrationについて調べてみた

Titanium mobileでAlloyを使ってiOSアプリの開発しています。
既存のモデルに属性を追加しようとしてちょこっと嵌ったのでメモ。

マイグレーションについての公式ドキュメントはこちら。
Titanium 3.X - Appcelerator Docs

まず、app/migrations下にマイグレーション処理を記述したファイルを作成します。
ファイル名にはルール(※重要)があり、"YYYYMMDDHHmmss_モデル名.js"とします。(例:20120610049877_book.js)
そしてこのファイルに次のように2つメソッドを定義します。

migration.up = function(migrator) {
  // ここにDB更新の処理を記述
};

migration.down = function(migrator) {
  // ここにDBロールバックの処理を記述
};

モデルに属性を追加する場合はmigration.upにDBテーブルを拡張する処理を書けば良かったんだけど、そこでいろいろトラブル。migration.upが呼ばれたり呼ばれなかったり、DBのカラム追加処理がエラーになったり。

調べた結果、次のようなことがわかりました。

  • migrations下にファイルがあると、アプリ起動時ではなく、DBアクセスのあったタイミングで処理が呼ばれる。
  • マイグレーション処理が行われると、DBにmigrationsテーブルが作成され、最後に実行されたマイグレーションの情報(モデル名と日付)が保存され、次回以降はこの日付以前の処理は行われない。


従って、何回も実行確認したいと思ってもファイル名の日付部分を変更するかDBを書き換えるかしないとmigration.upが呼ばれないということになります。


また、最初migrationファイルを1つ作って次のようにDBにフィールドを追加する処理を書いていたところ、この処理がエラーになっていました。

migration.up = function(migrator) {
  migrator.db.execute('ALTER TABLE ' + migrator.table + ' ADD COLUMN age INT;');
};

原因は、この処理を実行する時点で変更しようとしているテーブルが存在していないためでした。
アプリを一旦削除して何にもデータの無い状態で実行すると、モデルのDBテーブルが作成される前にこの処理が走ってしまうため、エラーとなります。

解決策としては、最初に呼ばれるマイグレーションの処理(ファイル名の日付が最も新しいもの)に、テーブルを新規作成する処理を書いて置きます。

migration.up = function(migrator) {
  migrator.createTable({
        "columns":  {
            "name": "TEXT"
        }
    });
};

これで、まっさらな状態からでもDB作成->カラム追加となって問題なくなります。