🚨

OpenLayersで特殊な画像を読み込む

2022/10/14に公開

Rotated GeoTIFF

v6.15.0でめでたくローカルのGeoTIFFを読み込むことができるようになりました。

が、たまにうまく表示ができないファイルが存在します。


こんなの。下の地図(OSM)と陸地の形があっていない。


本当はこうなってほしい。

なんでこうなってしまうかというと、OpenLayersのol.source.GeoTIFFは内部でgeotiff.jsを利用しており、座標の計算にもgeotiff.jsのgetBoundingBox()の値を利用していることに起因します。

このGeoTIFFは内部に回転情報(ModelTransformation)を持っています(以降、こういうGeoTIFFをRotated GeoTIFFと呼ぶ)。
なお、ほとんどのGeoTIFFはModelTransformationを持たないため、この事象に悩まされるのは一部の人だけです。

geotiff.jsだとimage.getFileDirectory().ModelTransformationで参照することができます。

[
  8.92997575301205, 
  -4.500614786418409, 
  0, 
  389511.2734, 
  -4.500614912499997, 
  -8.929975724999965, 
  0, 
  7697141.2062, 
  0, 0, 0, 0, 0, 0, 0, 1, 
]

ようはアフィン行列で、これを元に計算したBBOXがこれ。

[353506.35510865273, 7592828.9090791, 454735.8163, 7697141.2062] 

対して、geotiff.jsのgetBoundingBox()はgetOrigin()とwidth, heightの値からBBOXを計算する

getOrigin()の値は以下で、先程ModelTransformationから計算した値とはずれてしまう。

[389511.2734, 7697141.2062, 0]

ol.source.GeoTIFF(内部のgeotiff.js)は読みだした画像を回転させることもないため、イメージとしてはこんな感じでずれて表示されてしまうわけである[1]

処理内容

  1. 画像全体を読み込む。
  2. ModelTransformationから回転角などを計算。
  3. 画像をAffin変換で回転させた状態で読み込んだcanvasをsource内に保持。
  4. TileGridでタイルの読み込みがあるたびにcanvasからタイルの範囲を切り出していく。

回転角の計算
https://github.com/yonda-yonda/ol-processed-source/blob/b557ac812cd81a1fac64dde95739ea4eb73ebad6/src/utils.ts#L194-L222

canvasからタイルの範囲を切り出していく処理
https://github.com/yonda-yonda/ol-processed-source/blob/b557ac812cd81a1fac64dde95739ea4eb73ebad6/src/source/GeoTIFF.ts#L63-L213

本来であれば、タイルのリクエストがあったときにタイルの範囲のピクセルのみをファイルから読み込むことができればいいのだが、タイルとして読み込む範囲が回転前のファイルのピクセル座標においてどこからどこまでなのか、といった計算が煩雑でいったん全部を読み込んで回転させたものを保持しておくしかやりようが思いつかなかった。

そのせいで表示している間はメモリを大量に食うことになります。
実装では、アホみたいなサイズのファイルの読み込みでブラウザがクラッシュしないよう、保持する最大ピクセルを指定できるようにしている。それを上回るファイルが選択した場合は縮小した状態で保持される。
だが、1レイヤーあたりの上限を設けても同時に何ファイルも読み込んだ場合はやはりメモリを食うことになるため、スマートな処理とは言えないです。

TileGridでなくImageStaticを使えばcanvasを保持し続ける必要はなくなるのだが、ImageStaticも現状は投影周りでうまく行かないケース(後述)があるため、TileGridを使うのが最適解だとv6.15.0時点では思う。

投影座標系と座標の組み合わせの相性が悪いときのImageStatic

ol.source.ImageStaticは投影座標系とextent(左下と右上の座標)を指定することでpngやjpegといった画像を地図上に表示できます。
公式のImageStaticサンプル

便利な機能です。けれどsourceの投影座標系とmapの投影座標系の組み合わせによってはうまく表示ができないケースがあります。

OpenLayersは地図(map)と画像(source)それぞれに投影座標系(projection)を指定します。
この投影座標系が同じ場合はImageStaticはまったく問題なく動きます。

問題ない例
mapのprojectionがEPSG:4326(緯度経度座標系)
sourceのprojectionがEPSG:4326で、extentが[160, -10, 190, 20]

地図と画像が異なる投影系となる場合にうまくいかないことがあります。
例えば、180度線をまたぐようなextentを指定しているケースです。

うまくいかない例1
mapのprojectionがEPSG:3857(ウェブメルカトル)
sourceのprojectionがEPSG:4326で、extentが[160, -10, 190, 20]


180度線で切れてしまう。

mapのprojectionにあわせてImageStaticのextentも変換されるのだが、ここの処理がそこまで丁寧ではないためにこんなことが起こってしまう[2]

同様に、極点を中心とするような座標系で表示する際もうまくいかない。

うまくいかない例2
mapのprojectionがEPSG:3031(南極中心)
sourceのprojectionがEPSG:4326で、extentが[140, -10, 170, 20]

このケースでは何も表示がされなくなってしまう。
本当は右下(オーストラリアの北)当たりに画像が表示されてほしい。

これらのケースでも正しく表示をするためには、ImageStaticの座標変換周りをゴリゴリ直すか、内部にcanvasとして画像を保持しそこからTileGridで切り出していく(切り出す際に180度線まわりでは特殊な分岐をいれる)しかv6.15.0時点では手がない。

必ず画像の投影座標系で地図も表示する、という人であれば素のImageStaticを使うべきであろう。

だが、投影座標系が異なる複数の画像を同時に地図上でどんなケースでも表示したい場合は、現時点ではメモリの消費に目をつぶってcanvasを保持してTileGridで読み出すほか手段がないと思われる。
Rotated GeoTIFFを読み込む際にImageStaticでなくTileGridを採用したのもこうした理由による。

処理内容

  1. 画像全体を読み込む。
  2. 画像を読み込んだcanvasをsource内に保持。
  3. canvasに含まれる範囲が各タイルと重なるか判定する際に、wrapする(横方向でループする)ような座標系では、世界を一周した裏側の座標でも重なり合い判定を行う。
  4. canvasに含まれる範囲とタイルが重なりあう場合は、canvasから画像を切り出していく。

warpさせる場合のタイルの座標計算
https://github.com/yonda-yonda/ol-processed-source/blob/b557ac812cd81a1fac64dde95739ea4eb73ebad6/src/source/Image.ts#L243-L268

実装ではwrapする座標系ではTileGridのextentを画像の四隅でなく座標系全体で作るなどの工夫をしています。

課題

ここで紹介してるどちらのケースも一度ファイル全体を読み込んでcanvas上に保持する方法でしか実現できなかった。
アフィン行列をうまく使うなどしてタイル画像を作るときに必要な部分だけ読み出すようにできるとパフォーマンスが上がるのだが、ちょっと思いつかない。

脚注
  1. geotiff.js 自体が矩形の画像から矩形の画像を切り出すことを主なスコープとしており、変換行列まではサポートしきれていないようである。そのうち対応してくれるかもしれない。
    https://github.com/geotiffjs/geotiff.js/issues/124 ↩︎

  2. というより作者たちがここまでの使い方を想定していない。
    具体的にはrendererのprepareFrame内で呼ばれているTriangulationの処理がwarpされる座標系を考慮していない。
    https://github.com/openlayers/openlayers/blob/v6.15.0/src/ol/renderer/canvas/ImageLayer.js#L58
    https://github.com/openlayers/openlayers/blob/v6.15.0/src/ol/reproj/Image.js#L73
    https://github.com/openlayers/openlayers/blob/v6.12.0/src/ol/reproj/Triangulation.js#L147 ↩︎

Discussion