月別アーカイブ: 2014年6月

UIImagePickerControllerを使わずに写真を撮る

【これは古い情報です】iOS8ではもっと簡単にできるっぽい

iPhoneで写真を撮りたい時はUIImagePickerControllerを使うと簡単に写真が撮れるのだが、こいつだとちょっと問題がある場合がある。例えばUIをカスタマイズしたいとか。

私の場合、「写真が撮影された時のカメラの向き(方位)を知りたい」であった。
UIImagePickerControllerはダメだった。
どうダメかというと、シャッターボタンがパシャリと押されてからUIImagePickerControllerから処理が返ってくるまでにタイムラグがあるのだ。例えば0.3秒タイムラグがあったとして、その間に端末の向きが大きく変わってしまうということが往々にしてある。特に無理な体勢で撮影して、パシャっと撮った後うまく撮れたか確認しようと画面を見る、なんて動作をすると、撮って0.1秒後でも端末の向きが変わってしまってたりする。それでは「撮影した方位」を知りようがない。

なのでシャッターボタンが押されてすぐに処理が返るようなUIImagePickerController代替が欲しいなーってことでその辺のサンプル寄せ集めてAVFoundationで静止画像をキャプチャする何かを作ってみた。

https://github.com/kitzzking/ImageCaptureViewControllerSample

GitHubよくわかってないので作法的によろしくなったりするかもしれないがよくわからない。。。

UIImagePickerController代替のImageCaptureViewControllerと、それを使って位置情報やらを付加した写真を保存するViewController、というカンジのサンプルです。

写真を拡大表示する時にアニメーション途中で大きな画像に挿げ替える場合は最初の画像と置き換える画像とでExifの回転情報を揃えるべし

かなりビミョーすぎる問題

最近は写真のサムネールをタップして大きな画像を表示する時は別画面に遷移するのではなくサムネールがひゅっと拡大するのが流行り。

こんな。

拡大するには、UIViewのアニメーション機能でUIImageViewをちゅちゅーと拡大する。

この時、元の小さなサムネールをそのまま拡大するとボヤけるのでどっかで大きな画像に挿げ替えるんだけど、

  • 小さなサムネールを拡大してから大きな画像に挿げ替える場合、拡大し終わる直前あたりに解像度の足りてないもやっとしたものが表示されてしまう
  • 大きな画像を先に用意し、挿げ替えてから拡大する場合、拡大アニメーションが始まる前に画像ロードの時間のブランクが発生してしまう。iPhone4などの遅い端末だともの凄いブランクになる。

という問題が発生する。
端末を識別し、遅い端末ならアニメーションした後で挿げ替える、速い端末ならアニメーションする前に挿げ替える、というのも手だが正直あんま美しくない。

そこで、

  • とりあえず拡大アニメーションスタート
  • 非同期で同時に大きな画像をロード
  • 大きな画像が取得できたらアニメーション中かどうかおかまいなしに画像を挿げ替える

というのをやってみた。
おおまかなコードはこんな

これを走らせてみると、iPhone5だと大きい画像のロードが瞬時に終わるためアニメーションの最中、しかも早いうちに画像が挿げ替わる。これだと画像のロードを待たずにライムラグ無くアニメーションが始まり、拡大の最中の早いうちに画像が替わるので解像度の低い画像が拡大して見えてしまうこともない。パーフェクト。
iPhone4の場合シングルコアで拡大アニメーションしながらバックグランドタスクをこなすのは酷で、アニメーションが終わってちょい経ってから大きな画像に挿げ替わる。動きとしては「アニメーションが終わった後で画像を挿げ替える」とほとんど同じ。大きな画像を読み込むキューの優先度を低くしてあるのはシングルコア端末でアニメーションの滑らかさを優先するため。

これで不格好な条件分岐かまさずに速い端末でも遅い端末でもどちらでも最適なカンジで大きな画像に挿げ替えることができた。

ばんじゃーい。

で終われば良かったのだが、この「UIViewアニメーションしてる途中のUIImageViewの画像を挿げ替える」って方法に微妙な注意事項が。

結論から言うと、

・挿げ替える前と後の画像で、Exifの回転情報を揃えておかないといけない

っていう。これを守らないと画像を挿げ替えた時点でアニメーション中の画像の位置がおかしなところに飛ぶのだ。

どういうことかというと

  • UIViewのアニメーションはCALayerの機能を使ってる(たぶん)
  • Exif回転情報付きのUIImageは元の向きのままのラスター+回転情報を持っていて、それをUIImageViewで表示する際にCALayerの機能を使って回転を適用している(たぶん)

んで、UIViewのアニメーションしてる途中で画像のExif回転情報が変化するとかたぶん想定されてないので、挿げ替えた画像の回転は適用されるんだけど、アニメーション途中で画像の回転角が変化した時の回転中心。。。は考慮されてなくて、どっか(たぶんアニメーション開始前)を中心に回転してしまう。このため画像の位置が飛んでしまう。

ということが起きている。。。 と思われる。 推測ですがたぶんこんなんでしょう。

大きな画像は通常iPhoneでパシャっと撮った画像。これはラスターはCCDの向きのままでExifに回転情報を入れて向きを回してる。例えば90度回転するExif情報が付いてるとする。
そしてその画像からサムネールを作る場合、よくやるのはCoreGraphics使っての縮小だけど、Exif回転情報付きの画像を縮小すると回転情報を適用済みの縮小画像が得られるのだ。このサムネールは既にラスター自体が回転していてExifによる回転情報がない=回転0度Exif情報が付いてるのと同じ。

というワケでこの2つの画像を挿げ替えると上記の問題が発生してしまうのである。
このパターン(サムネール画像を作成する時に回転適用済みになってしまい回転情報が揃わなくなる)はわりと稀によくあると思うので気をつけて欲しい。まあアニメーションの最中に挿げ替えようとかアレなことしようと思わなければ気をつける必要もないのだが。

対策はとりあえず アニメーション途中で画像を挿げ替える時はExif回転情報を揃えるってのしかないカンジ。パターンとしては

  • 拡大画像もサムネールも両方回転適用済みにしておく
  • サムネールを作成する際に回転適用済みにならないようにする(サムネール画像が元と同じラスターの向きで元と同じExif回転情報を持つようにする)
  • 画像ロードし終わった時にまだアニメーション途中だった場合はアニメーション終わるのを待ってから画像を挿げ替える。←一番確実なんだけど、これだと拡大アニメーションが完了する直前にもやっとした画像が見えちゃうからやっぱりもっと早く挿げ替えたいじゃん?
  • サムネール画像なんか用意しない。全部大きい画像でやる。だから挿げ替えなんか必要ない。←iPhone4だとパフォーマンス遅くて死ぬけどiPhone4見限るならこれもアリか

のいずれか。

2番目の場合、元画像からExif回転情報を落とした画像をまず作り、それを縮小し(同じラスターの向きになる)、その縮小画像に元と同じExif回転情報を付ける。

3番目の場合、アニメーション完了ブロックと画像ロード完了後のメインキューブロックの2つの非同期ブロック同士で情報のやりとりが必要なんだけど、NSMutableDictionary使うのがマイブーム。

同期はこの場合sharedDic弄るのは全部メインキューにしてお手軽解決してる。キューが異なる場合はdispatch_semaphoreあたりで競合しないようにする必要がある。