Canvas から getImageData で取得した RGB値は premultiplied-alpha で量子化される件
透明色を含む RGBA データを Canvas から getImageData で取得すると、RGB 値が期待と異なる事があります。なお、本エントリで説明する挙動は不具合でなく仕様です。
サンプル画像
% magick -size 256x256 xc:white -alpha on \
-channel RGB -fx "i/w" \
-channel A -fx "j/h" RGBA.png
RGBA.png | RGB.png | A.png |
---|---|---|
現象
この画像ファイルを取り込んだ Image を drawImage で Canvas に書き込み getImageData すると、以下のデータになります。RGBA は見た目変わらないので、そこから分離した RGB も並べます。
% magick RGBA-canvas.png -alpha off RGB-canvas.png
RGBA-canvas.png | RGB-canvas.png |
---|---|
見ての通り、A の値が小さな上の方ほど RGB の値が崩れています。
見やすくするため、縦方向に x8 で拡大してみます。
RGB-canvas-x8.png |
---|
- A:0 の時は RGB は全て 0
- A:1 だと RGB は2分割で、 0 か 255
- A:2 は RGB が3分割、0, 128(0x80), 255(0xFF)
- A:3 RGB: 4分割、0, 85(0x55), 170(0xAA), 255(0xFF)
- ...
A が小さいほど RGB の値が荒くなります。このように飛び飛びの値になる現象は量子化(Quantization) とも呼ばれます。
原因
Canvas は ImageData RGBA を premultiplied-alpha として保持するからです。
- GPUs prefer premultiplication
premultiplied-alpha は雑にいうと、アルファ合成する時に A で乗算するので、予め RGB に A を乗じておけば乗算をサボれる。といった高速化と、あと合成処理で色々と都合が良いので広く使われています。
- RGBA(straight alpha): 255, 128, 64, 128
の RGBA 値があるとすると、RGB に 128(/255) を乗ずる事で premultiplied になります。
- RGBA(premultiplied alpha): 128, 64, 32, 128
getImageData はその A で割ってしまった RGB 値を元に復元するので、A:0 だと RGB は全て 0 に、A:1 だと 0 と 255 の2種類しか復元できません。
回避策
Canvas に入れてしまった RGBA を getImageData で取り出す場合はどうしようもないですが、画像ファイルの透明度を正確に取得したいのであれば、PNG.js といったデコーダーを使うのも手ですし、Canvas の 2d context を介さず WebGL2 のテクスチャで PNG32 を取り込むと RGBA そのままの ImageData が得られます。
- How can I stop the alpha-premultiplication with canvas imageData?
備考
値が劣化して不便なので仕様を変えたい要望はあるようです。ただ、実現するには premultiplied の利点を捨てて遅くなるか、straight と premultiplied の2つの RGBA をメモリ保持するかの選択肢を迫られるので、おそらく無理かなと思います。
- ImageData alpha premultiplication #5365
参考
英語版記事) https://dev.to/yoya/canvas-getimagedata-premultiplied-alpha-150b
Discussion