Chapter 78

ポスタリゼーション

miku
miku
2021.11.19に更新
このチャプターの目次

実行例


解説

フルカラー画像の場合、RGBそれぞれの値の範囲は0~255の256階調になる。このような階調を意図的に落として色の変化を行なう処理のことをポスタリゼーションと呼ぶ。

例えば2階調まで落とす場合を考える。2階調まで落とすということは、RGBそれぞれの値が2種類しか使用できない。たとえば適当に、2種類の値を0, 1にしてみた場合の色の組み合わせは下記の通りとなる。

  • 0x000000 (赤:0 緑:0 青:0)
  • 0x000001 (赤:0 緑:0 青:1)
  • 0x000100 (赤:0 緑:1 青:0)
  • 0x000101 (赤:0 緑:1 青:1)
  • 0x010000 (赤:1 緑:0 青:0)
  • 0x010001 (赤:1 緑:0 青:1)
  • 0x010100 (赤:1 緑:1 青:0)
  • 0x010101 (赤:1 緑:1 青:1)

RGBそれぞれ0, 1の2種類の値のみを使用しているので2階調にはなっているが、8種類の色はほとんど黒色になる。これで画像を変換した場合、真っ黒な画像ができあがる。それに加え、もともと0~255の値をどのようにして 0 と 1 に振り分けるかの条件も考えなければならない。

以上のことから見て、階調を落とす場合は、下記2つの要素をよく考える必要がある。

  • 使用できる値の選別
  • もともとある値をどのようにして使用できる値に振り分けるか

一般的に2階調の場合、使用できる値は 0 と 255 が選ばれる。この2つを選ばないと、上記で別の値を選んだ結果のように、明るさの最大と最小の範囲が狭くなり、いわゆるコントラストが低下してしまう。次にもともとの範囲0~255をどのように 0 と 255 に振り分けるかだが、これは振り分ける数がおなじになるように調整する。

256 階調 0 ~ 127 128 ~ 255
2 階調 0 255
振り分けた数 128 128

計算がしやすそうで、色が近い方に振り分けられており、理にかなっている。

256 階調 0 ~ 85 86 ~ 170 171 ~ 255
3 階調 0 128 255
振り分けた数 86 85 85

3階調の場合も振り分ける数を同じにしたいので、256 / 3 ≒ 85 を選ぶ。

0, 255以外に選べる値を決めないといけないが、これもコントラストの観点から、0 と 255 のちょうど間にある値を選ぶ。

(0 + 255) / 2 = 127.5 は整数ではないので、選ぶ値は 127 か 128 にする。

256 階調 0 ~ 63 64 ~ 127 128 ~ 191 192 ~ 255
4 階調 0 86 192 255
振り分けた数 64 64 64 64
256 階調 0 ~ 51 52 ~ 102 103 ~ 153 154 ~ 204 205 ~ 255
5 階調 0 64 128 192 255
振り分けた数 52 51 51 51 51

4階調、5階調、それ以上の場合も同じように振り分ける。

const value = 200; // (256階調時の)値
const fromMax = 256; // もとの階調
const toMax = 3; // 変換後の階調

const i = Math.floor((toMax / fromMax) * value);
const v = Math.ceil(((fromMax - 1) / (toMax - 1)) * i);
256 階調 0 ~ 85 86 ~ 170 171 ~ 255
インデックス(i) 0 1 2
3 階調 0 128 255

to 階調 / from 階調 * もとの値で、もとの値が to 階調になった場合の値を求める。その値の小数点を切り捨てると、上記表の区分に対するインデックス値になる。変換後の階調値は (from階調 - 1) / (to階調 - 1) * インデックス値 で求める。割り切れない場合があるので、切り捨て、もしくは切り上げ処理を行う。

コード例

let img;

function preload() {
  img = loadImage("./0.png");
}

function setup() {
  createCanvas(img.width, img.height);

  img.loadPixels();

  const fromMax = 256;
  const toMax = 4;

  for (let y = 0; y < img.height; y++) {
    for (let x = 0; x < img.width; x++) {
      const pixel = getPixel(x, y);

      const r = red(pixel);
      const g = green(pixel);
      const b = blue(pixel);

      const nr = posterize(r, fromMax, toMax);
      const ng = posterize(g, fromMax, toMax);
      const nb = posterize(b, fromMax, toMax);

      setPixel(x, y, [nr, ng, nb]);
    }
  }

  img.updatePixels();
  image(img, 0, 0);
}

function posterize(value, fromMax, toMax) {
  const i = floor((toMax / fromMax) * value);
  const v = ceil(((fromMax - 1) / (toMax - 1)) * i);
  return v;
}

function getPixel(x, y) {
  const i = (y * img.width + x) * 4;
  return [
    img.pixels[i],
    img.pixels[i + 1],
    img.pixels[i + 2],
    img.pixels[i + 3],
  ];
}

function setPixel(x, y, pixel) {
  const i = (y * img.width + x) * 4;
  img.pixels[i + 0] = pixel[0];
  img.pixels[i + 1] = pixel[1];
  img.pixels[i + 2] = pixel[2];
}