トップAndroid Java > 画像演算 PorterDuff
PorterDuff演算よるポリゴンのパターン塗りつぶし

画像演算 PorterDuff

マルチポリゴン

OSM(OpenStreetMap)のマルチポリゴンは一つの outer polygon の中に、 任意の数の inner polygon が含まれる。outer polygon をある色で塗りつぶし、 それぞれの inner polygon は透明な穴とする。

自前で画素の操作を行う

自前でマルチポリゴンを描画するには、まず、outer polygon を塗りつぶす。

その後、inner polygon を透明な色で塗りつぶせばよいように思うが、そうはいかない。 アルファ値が透明な場合、RGB がいかなる値であっても、塗りつぶしは起きない。

したがって、outer polygon の色とは異なる色(下の例では白色)で塗りつぶす。 その後、全画素の値を調べて、inner polygon の色であれば、アルファ値を 0、すなわち、透明に変える。

マイOSM地図のマルチポリゴンのレンダリングは当初からこの方法を用いている。

レンダリングとしては問題ないが、パフォーマンス上は効率がよくない。 まず、JavaプログラムはCなどネイティブ言語プログラムに比べて遅い。 第二にキャンバス全体の画素をチェックするため処理量が多い。 outer polygon の境界ボックス内だけでよいが、そうするにはそれなりのプログラムが必要になり、 その処理時間が必要になる。

  Bitmap bmp = tile.bmpWork;
  bmp.eraseColor(Color.TRANSPARENT);
  Canvas work = tile.cvWork;
  fillPolygon(work, tile, paint, bgnData, multi[0]); // outer polygonの塗りつぶし

  int offset = bgnData + multi[0] * (slim ? 1 : 2);  // outer polygon のノード数
  for (int n = 1; n < multi.length; n++) {
      fillPolygon(work, tile, paintWhite, offset, multi[n]);
      offset += multi[n] * (slim ? 1 : 2);
  }

  // Pixel 操作部分
  int[] pixels = tile.getWorkPixels();  // bmpWork の pixels を取り出す(参照)
  for (int k = 0; k < pixels.length; k++) {
      if (pixels[k] == 0xffffffff) {
          pixels[k] = 0x00ffffff; // 透明
      }
  }

  // Bitmap に Pixel を設定
  tile.setWorkPixels();  // pxWork(== pixels) を bmpWork にセット
  canvas.drawBitmap(bmp, 0, 0, null);

PorterDuffを使う

PorterDuffの実行プログラムはネイティブ言語で作られているであろうから、 自前の Javaプログラムより効率がいいであろう。 また、キャンバス全体の画素をチェックするような非効率なものではないであろう。

ただし、drawPath によるライン描画は非効率なものであるから、 PorterDuffが効率的かどうかは実際に試してみないと分からない。

記事[1]を参考にすると、 マルチポリゴンの場合、outer polygon を DST、inner polygon を SRC に指定して、 以下のようにすればよいようだ。

x, y は 0、W, H はキャンバスサイズでいいだろう。

もう少し事例を調べてから、プログラムを試したい。

  int sc = canvas.saveLayer(x, y, x + W, y + H, paint,
           Canvas.MATRIX_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
           Canvas.CLIP_TO_LAYER_SAVE_FLAG
  );

  // DST
  canvas.drawBitmap(bmpOuter, 0, 0, paint);

  paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

  // SRC
  canvas.drawBitmap(bmpInner, 0, 0, paint);

  paint.setXfermode(null);
  canvas.restoreToCount(sc);

タイル地図画像のレンダリングでは、単純な単色ポリゴンの場合、直接、タイル Canvas に塗りつぶし処理を行えばいいが、 マルチポリゴンのレンダリングでは、タイルCanvasの Bitmap とは別に、SRCビットマップとDSTビットマップを使う。 DSTビットマップに outer polygon を描画する。これは現行プログラムと同じである。

問題はSRCビットマップに透明な inner polygon をどのようにして描画するかである。 PorterDuffには何か手があるだろうと思うが、まだ不明である。

createPolygonWithHoles androidで検索してみたが、これぞという記事が見つからない。

SRCビットマップは透明でなくてもいいのかもしれない。

Example #4
Source Project: android-map-sdk   Author: navermaps   File: PolygonOverlayActivity.java    License: Apache License 2.0  6 votes vote down vote up

@Override
public void onMapReady(@NonNull NaverMap naverMap) {
    int color = ResourcesCompat.getColor(getResources(), R.color.primary, getTheme());

    PolygonOverlay polygon = new PolygonOverlay();
    polygon.setCoords(COORDS_1);
    polygon.setColor(ColorUtils.setAlphaComponent(color, 31));
    polygon.setOutlineColor(color);
    polygon.setOutlineWidth(getResources().getDimensionPixelSize(R.dimen.overlay_line_width));
    polygon.setMap(naverMap);

    PolygonOverlay polygonWithHole = new PolygonOverlay();
    polygonWithHole.setCoords(COORDS_2);
    polygonWithHole.setHoles(HOLES);
    polygonWithHole.setColor(
        ColorUtils.setAlphaComponent(ResourcesCompat.getColor(getResources(), R.color.gray, getTheme()), 127));
    polygonWithHole.setMap(naverMap);
}

Pixel操作の時間短縮

PorterDuffの使い方がまだ正確に掴めないため、自作プログラムの改良を行った。 Pixel操作はタイル全体(256x256画素)に対して行うのではなく、実際の描画範囲に限定する。

    for (int i = px0; i < px1; i++) {
        for (int j = py0; j < py1; j++) {
            if (pixels[i * PX + j] == 0xffffffff) {
                pixels[i * PX + j] = 0x00ffffff; // 透明
            }
        }
    }

当然、事前にこの範囲 px0, px1, py0, py1 を求めておく必要がある。

タイルのレンダリングに当たって、以下の三つの矩形を宣言して置き、これを再利用する。 タイル毎ではなく、プログラム起動時の方が、ガーベージコレクションの対象にならなくてよい。

レコード毎の処理での new は極力避けた方がよい。

  // 初期化
  RectF rectTile = new RectF(0, 0, PX, PX);
  RectF rectRecord = new RectF();     // 0, 0, 0, 0
  RectF rect = new RectF();           // 0, 0, 0, 0

rectTile は全レコードを通して同じである。レコード毎に、境界ボックス(外接矩形)の相対座標(画素単位)を rectRecord に設定する。rect.setIntersect(rectTile, rectRecord)によって、rectTile と rectRecord が交差する部分が rect に設定される。

以上のようにすれば、実際にレンダリングが起きる矩形範囲(px0,py0)−(px1,py1)が効率的に算出できる。

  // レコード(osm)毎の処理
  rect.set(0, 0, 0, 0);
  rectRecord.set(x0*fact-x*PX, y0*fact-y*PX, x1*fact-x*PX, y1*fact-y*PX);
  rect.setIntersect(rectTile, rectRecord);
  osm.px0 = (short)rect.left;
  osm.py0 = (short)rect.top;
  osm.px1 = (short)rect.right;
  osm.py1 = (short)rect.bottom;

これによる時間短縮は以下のようになった。 共に一回の計測であり、実行時間は絶えず変動するため、絶対的ではないが、予想以上の結果となった。 PorterDuffではこれほどの成果は得られないかも知れない。

drawLine とより高度な drawPath の場合、低ズームでは drawPath の方が高速であるが、 高ズームでは、実際には描画が起きないものが増え、drawLine の方が高速になる。drawPath の場合、 空振りの描画を省くのが苦手なようである。

デジタル地図のレンダリングでは、ハイズームになるほど、空振りの描画が極端に多くなる。 このため、描画されることを前提としているような汎用プログラムの場合、効率が悪い。

PorterDuffのアルゴリズムがどうなっているか分からないが、空振りが多いとき効率的でない可能性はある。 よって、無理はせず、PorterDuffの適切なプログラム事例にであったときに試すことにする。

[改良前]
zoom   6    7    8    9    10   11   12   13   14   15   16   17   18   19   20   平均
郊外  239  296  191  509  256  194  121  326  227  212  145  151  151   98  125   216ms
東京  259  287  226  423  329  156  120  333  412  489  298  157  169  138  129   262ms
[改良後]
zoom   6    7    8    9    10   11   12   13   14   15   16   17   18   19   20   平均
郊外  206  240  210  424  240  191  124  323  243  223  137  158  160  111  123   208ms
東京  162  230  140  392  382  117   86  319  432  542  259  132  166  124  124   240ms

タイル数は 8〜12 であり、zoom により異なる。

リファレンス

[1] AndroidのCanvasを使いこなす! - PorterDuff
[2] その7 PorterDuff.Modeとは何なのか
[3] PorterDuff.Mode
[4] 何かの時にスッと使える力技 - PorterDuffXfermode 編
[5] android.support.v4.graphics.ColorUtils Java Examples
[6] 2015-06-12 Android の Canvas#saveLayer メソッドと xfermode について
[7] Android での実際の画像 PorterDuff モードの使用法[Kotlin]
[8] 矩形の重なりを求めるプログラム