みさご解体新書

ポスタリゼーション

実行例

ソースコード

TypeScript

0.jpg / app.ts

解説/アルゴリズム

フルカラー画像の場合、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) * インデックス値 で求める。

割り切れない場合があるので、切り捨て、もしくは切り上げ処理を行う。

コード例(GLSL)

attribute vec2 aPosition;
attribute vec2 aTexCoord;
uniform vec2 uResolution;
varying vec2 vTexCoord;

void main() {
  vec2 clipSpace = aPosition / uResolution * 2.0 - 1.0;
  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
  vTexCoord = aTexCoord;
}
precision mediump float;
const float fromMax = 256.0;
const float toMax = 4.0;
uniform sampler2D uImage;
varying vec2 vTexCoord;

float posterize(float v) {
  v *= (fromMax - 1.0);
  float i = floor(toMax / fromMax * v);
  float a = ceil((fromMax - 1.0) / (toMax - 1.0) * i);
  return a / (fromMax - 1.0);
}

void main() {
  vec4 color = texture2D(uImage, vTexCoord);
  float r = posterize(color.r);
  float g = posterize(color.g);
  float b = posterize(color.b);
  gl_FragColor = vec4(r, g, b, 1.0);
}

コード例(p5.js)

import * as p5 from "p5";

new p5((p: p5) => {
  let image: p5.Image;

  p.preload = () => {
    image = p.loadImage("./0.jpg");
  };

  p.setup = () => {
    p.createCanvas(p.windowWidth, p.windowHeight);
    image.loadPixels();

    const fromMax = 256; // もとの階調
    const toMax = 4; // 変換後の階調

    // 画像の全ピクセルを走査
    for (let y = 0; y < image.height; y++) {
      for (let x = 0; x < image.width; x++) {
        // 対象のピクセルを取り出す
        const color = getPixel(x, y);

        // 取り出したピクセルからRGBを抽出
        const r = p.red(color);
        const g = p.green(color);
        const b = p.blue(color);

        // 減色したRGB値を取得
        const nr = posterize(r, fromMax, toMax);
        const ng = posterize(g, fromMax, toMax);
        const nb = posterize(b, fromMax, toMax);

        // 減色したRGB値をピクセルにセットし直す
        setPixel(x, y, [nr, ng, nb]);
      }
    }

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

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

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

  function setPixel(x: number, y: number, color: number[]): void {
    const i = (y * image.width + x) * 4;
    image.pixels[i + 0] = color[0];
    image.pixels[i + 1] = color[1];
    image.pixels[i + 2] = color[2];
  }
});

内部で利用しているアルゴリズム

床関数, 天井関数