HackDay勝ちましてん。
24時間でこういうのを作りました。今回は振り返りを含めて使った技術やアルゴリズムを中心にまとめようとおもいます。
仕組み
スマホを持ち上げてる間は自動撮影、周りの目を気にせずに料理の写真を撮れるカメラアプリ
最初アイデアがでてみんなでブレストしているときは、うわ。このアプリめっちゃ欲しいと思ってすごいテンション上がったんですけど、作ってみるとめちゃめちゃ大変で絶賛徹夜開発でした。
主にスコアリングによる画像とフィルターの選択を思いつく限りの盛り込みと、実測値からのシーンへの最適化で実現しています。
- 加速度・ジャイロセンサーでiPhoneが持ち上がったのと、置かれたのを検知してカメラを管理
- 起動中は映像を撮影しながら超音波を出す
- 撮影された映像を0.1秒ごとにキャプチャして、その時々の加速度やジャイロの値、超音波から取れる数値を取得
- めちゃめちゃ解析して画像とフィルター選択
設計
ハッカソンでの開発って仕様や設計なんてないようなもんですが、それは超ダメです。
短い時間で移り変わる仕様と設計を柔軟に受け入れチームで共有して、がしがし仕様変えながら実装していくのが大事です。じゃないとできないことをできるようになりません。
そんなときに頼りになるのがHackpadというサービスです。リアルタイムに複数人でMarkdown風のエディタを共有できます。
- 仕様
- 設計
- スキーマ定義
- 進捗
- setupコマンド
などなど、常に更新しながら開発を進めました。Githubのissueでもいいんですが、多機能すぎて24時間でやることを共有するのにはちょっとめんどうに感じました。これで常に共有すべきことだけシンプルに共有していました。
技術
使ったライブラリ
ビルド時間がもったいないのでcarthageを使おうと思っていたんですが、今回エンジニア2人でセットアップするのが面倒だったのと、慣れてないものを使ってトラブルになるのを避けてcocoapodsにしました。
platform :ios, "9.0"
use_frameworks!
pod 'OpenCV', '2.4.9'
pod 'MagicalRecord'
pod 'KSToastView'
pod 'SVProgressHUD'
pod 'GPUImage'
pod "CameraEngine"
OpenCV
画像処理はこれしかないと思ったので、とりあえず入れました。
画像のピクセルデータにアクセスして色情報を取得する方法もあるので、がんばれば画像処理もライブラリ使わないでもいけるようだけど、あまりパフフォーマンスがでないとどこかで聞いた。今回はOpenCVは研究で使っていたこともあったので、使うことにした。
ヒストグラム管理したり、比較するのをさらっと書ける。
// 画像の類似度を計算する
+(double)calculateSimilarity:(UIImage*)sourceImg :(UIImage*)targetImg{
cv::Mat src_img;
UIImageToMat(sourceImg, src_img);
cv::Mat target_img;
UIImageToMat(targetImg, target_img);
// グレースケールに変換
cv::Mat grayImage1;
cv::cvtColor(src_img, grayImage1, cv::COLOR_BGR2HSV);
cv::Mat grayImage2;
cv::cvtColor(target_img, grayImage2, cv::COLOR_BGR2HSV);
// ヒストグラムを計算
// and the saturation to 32 levels
int hbins = 30, sbins = 32;
int histSize[] = {hbins, sbins};
// hue varies from 0 to 179, see cvtColor
float hranges[] = { 0, 180 };
// saturation varies from 0 (black-gray-white) to
// 255 (pure spectrum color)
float sranges[] = { 0, 256 };
const float* ranges[] = { hranges, sranges };
cv::MatND sourceHist;
cv::MatND targetHist;
// we compute the histogram from the 0-th and 1-st channels
int channels[] = {0, 1};
calcHist( &grayImage1, 1, channels, cv::Mat(), // do not use mask
sourceHist, 2, histSize, ranges,
true, // the histogram is uniform
false );
calcHist( &grayImage2, 1, channels, cv::Mat(), // do not use mask
targetHist, 2, histSize, ranges,
true, // the histogram is uniform
false );
/*
比較手法.次のうちの1つ:
CV_COMP_CORREL 相関
CV_COMP_CHISQR カイ2乗
CV_COMP_INTERSECT 交差
CV_COMP_BHATTACHARYYA Bhattacharyya距離
*/
double similarity = cv::compareHist(sourceHist, targetHist, CV_COMP_BHATTACHARYYA);
NSLog(@"[Is Similarity Rate] %f", similarity);
return similarity;
}
こういう処理を無茶苦茶書いた。
GPUImage/CameraEngine
最初映像を保存してアプリ内に保存する処理を自前で書いたんですね。
でもずっと落ち続ける問題が出ていてあきらめました・・・
self.fileWriter.finishWritingWithCompletionHandler(callback)
fileWriterはAVAssetWriterなのですがこいつがクラッシュし続けてあきらめました・・・
昔は全く同じコードでうまくいったんですがね。これに2時間くらいはまってました。
「作り直そう・・・」
そう思って泣きながら探したライブラリがCameraEngineでした。
インターフェースがとってもきれいで「カメラを映像名を指定して録画して、録画後のアクションを実装する」というのがいい感じにかけます。細かいプロパティとかも指定できるので便利です。
func startCapture(sender: UIButton){
guard let url = CameraEngineFileManager.documentPath("video.mp4") else {
return
}
self.cameraEngine.startRecordingVideo(url, blockCompletion: { (url, error) -> (Void) in
// Run and Save Capture Image
self.sceneAnalyze = SceneAnalyze(moviePath: url!, accelerationList: accelerations, gravityList: gravities)
})
}
検証
- 画像の特定領域の明度の分散値
- 画像の緑色の割合
- 画像のHSVヒストグラムの類似度
...
みたいのを延々算出しては「あかん!」「ちがう!」「眠い!」っていうのを繰り返し続けてました。
思いついたメソッドとかちょっと試すのにはユニットテストめちゃめちゃ書いてました。
いろいろスコアリングするので、信頼性のないコードはいやだったんでばしばし書いてました。
コードをアクションに組み込んだ後にあれ?あってる?とかすぐ確かめられるので、効率がいいです。
たくさん検証する
アプリでは目につかない検証用の画面とかずっとみてました。
例えば「おいしそうな画像を選ぶ」ために、お寿司の画像を二値化して領域を楕円区分してシズル感のありそうな箇所をマッングしている様子です。
この二値化の部分に大津のアルゴリズムを使っていると書いてありますが、理論は正直よくわかってなくてorz 何回もテスト回して今回のターゲットに対していい精度が出ていることを実測値から確認したから採用しました。
こういうのをいくつもやりました、とにかく繰り返しです。実測値こそ命。
そういう感じでOpenCVの楕円検出とその領域の画素値を見て光沢っぽい場所=シズル感のある場所を見つけてました。
まとめ
要約するとめちゃめちゃいろいろやったらなんかいい感じのスコアリングの仕組みができてそれなりに動いたって感じです。優勝しておいて、ちょっと心もとない感想ですが・・・
でも短い時間にいかに検証につぎ込めるかが大事だと思うので、それ以外の基本的な実装とか検証用のインターフェースのすぐ作れるかとか、どうチームで作るかとかは、仕事でも個人の開発の中でも意識して勉強してそれを使わないとダメです。
反省
- ユニットテストのコードが汚い、関数名適当すぎてあとで探索の手間が・・・
- 検証用のUIとかがっつりOSS使えばよかった
- 必要のないpodfileいれすぎてビルドが重かった
- 設計と検証の前後が多かった
- デモは立ってやることを想定してなかったので、デモデイで失敗があった
- デモでのユーザーの動作の想定が漏れた
- 幾つかの実験に関わる定数はplistでもたせて状態管理をすればよかった
- Playgroundで検証した部分もコッミットしてRxSwiftのサンプルみたいにすればよかった