描画系」カテゴリーアーカイブ

写真を拡大表示する時にアニメーション途中で大きな画像に挿げ替える場合は最初の画像と置き換える画像とで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あたりで競合しないようにする必要がある。

OpenGL VAO と VBO

もし検索で来たひとがいたらすんませんここにはあんま有益な情報はありません。

ホイール欲しい ハンドル欲しい OpenGL ES 3.0 / OpenGL 4.3 VertexArrayObject と VertexAttribBinding
こちらのブログが大変参考になりました。本家サイトの VertexSpecification と睨めっこしつつ。

VAOの中に何が格納されるのか。そんで何が格納されないのか。
OpenGLの「バインドしてなんちゃらする」の方式はそのバインドが何に及ぶのかが大変判りづらい。
GL_ARRAY_BUFFERとGL_ELEMENT_ARRAY_BUFFERのバインドの影響範囲の違いとかもうね。

仕様書を隅々まで読めってハナシではあるんですが、作用する範囲がコードからまるで見えてこないというのはどう考えても良い設計ではない。一度後方互換ばしっと切っちゃったんだからこのバインド形式ももっとマシな別のものにできんかったのかのう。。。。。

GPSログ(トラック)の点を間引く

GPSの座標情報は大抵の場合1秒に1回やってきます。これを全部保存すると1時間で3600、半日で43200もの位置情報になり、保存するにもファイルが無駄に大きくなるし表示するにも無駄に重くなる。

このため実用に必要十分な数に点を間引くのですが、主にやられるのがこの二つ。

・一定時間で間引く(例えば10秒毎に記録
・一定距離で間引く(例えば10m進んだら記録

ただし、この2つはどちらもあんましよろしくない。
単に等時間や等距離で無造作に点をピックアップしていくため、データを軽くしようと時間や距離を大きくとると元の軌跡との乖離がどんどん大きくなってしまいます。かといって間隔を狭めるとそれに反比例してデータが肥大します。
等間隔による間引き

これはかなりいただけないのでできるだけ元の軌跡との乖離が少なくなるような別の方法を考えます。でも「乖離が一番小さくなる最適解」を求めるのは計算が面倒くなるのでやりません。GPSから送られてくる位置情報を保存する手前に挟むフィルターとして実装しやすいってことで、次のような方法で間引きます。
 「開始点P(0)から連続する点が一定の幅に収まる一番先の点P(N)を探し、その間のP(1)〜P(N-1)を削除」
P(N)を探してその間の点を削除したら、P(N)を基準(P(0))にして次の間引きをやります。

名称未設定-1
位置情報がGPSから徐々に送られてきます。ベースの点と最新の点に線を引き、その間の点がこの線の幅wに収まる間は取り敢えずバッファに溜めときます。

名称未設定-2
まだまだ収まるのでバッファに溜めます。

名称未設定-3
ところが次の点は中間の点が幅wに収まらなくなってしまいました。
なのでその手前の点が「幅wに収まる一番遠い点」になります。

名称未設定-4
というわけでその点を保存します。中間の点はフィルタアウト。

名称未設定-5
最後に確定した点をベースとして、同様の作業を続けます。

名称未設定-6
で、こんなカンジにデータを間引くことができます。

GPSのログは真っすぐ進む時は割と真っすぐ進み曲がる時はカクっと曲がることが多いので等時や等距離でやるよりもより少ない点数でより好ましい形にすることができます。
先に書いたように最適解じゃないけど楽に実装できる現実的な落としどころということで。

幅wはおいらのアプリでは2mくらいです。テキトーなのは緯度経度から平面座標への変換をものすごいいい加減にやってるためw

蛇足ながら実際の計算はというとまず基点(P0)と最新の点(P3)を結ぶ線の単位法線ベクトル(N3)を計算して、あとは基点と中間の点を結ぶベクトルとその法線ベクトルの内積をとればその絶対値が基点と最新の点を結ぶ線に降ろした垂線の長さになるよね的な。これが閾値を越えたらアウアウ。
名称未設定-7

んで、このアルゴリズムはログの保存だけでなく画面に表示する用に点を間引くためにも使います。一言で説明が終わるハナシではあるんですがもったいぶって次のエントリーで。