フルカラーの画像を2値化するときに使う、誤差拡散法を試してみた。誤差を単に右に拡散していくのではなく、下や斜め下にも拡散させるFloyd–Steinberg ditheringのほうがよいらしい。

元画像:lenna
lena

単純に誤差拡散した結果:
lena

なんか元画像より白っぽくなってるように見える。これは誤差を単に隣接するピクセルに足してしまうと、ガンマ値でべき乗する前の値同士を足し算していることになるので、明るさが変わってしまうのが原因だろう。

誤差拡散法にガンマ補正を施した結果:
lena

単純に足しあわせたよりはそれっぽい(逆に暗くなった気がするが…ガンマは2.2じゃないのか?) 他の画像処理、例えば拡大縮小とかでもガンマ補正が必要になるってことだよなぁ。

Processingのソース

void setup() {
  PImage img = loadImage("lenna.jpg");
  size(img.width, img.height);
  RenderImageUsingErrorDiffusion(img);
  save("result.png");
}

void RenderImageUsingErrorDiffusion(PImage img) {
  float[] error = new float[img.width * img.height * 3]; 
  for (int y = 0; y < img.height; ++y) {
    for (int x = 0; x < img.width; ++x) {
      color cc = DiffuseError(x, y, img.pixels[y * img.width + x],
                              img.width, img.height, error);
      set(x, y, cc);
    }
  }
}

color DiffuseError(int x, int y, color c, int w, int h, float[] error) {
  float r = max(0, min(1, gamma(red(c) / 255)   + error[(y * w + x) * 3 + 0]));
  float g = max(0, min(1, gamma(green(c) / 255) + error[(y * w + x) * 3 + 1]));
  float b = max(0, min(1, gamma(blue(c) / 255)  + error[(y * w + x) * 3 + 2]));
  int rr = 0, gg = 0, bb = 0;
  if (r >= 0.5)  { rr = 255; r -= 1.0; }
  if (g >= 0.5)  { gg = 255; g -= 1.0; }
  if (b >= 0.5)  { bb = 255; b -= 1.0; }
  Propagate(x + 1, y,     r, g, b, 7.0 / 16, w, h, error);
  Propagate(x - 1, y + 1, r, g, b, 3.0 / 16, w, h, error);
  Propagate(x,     y + 1, r, g, b, 5.0 / 16, w, h, error);
  Propagate(x + 1, y + 1, r, g, b, 1.0 / 16, w, h, error);
  return color(rr, gg, bb);
}

void Propagate(int x, int y, float r, float g, float b, float ratio,
               int w, int h, float[] error) {
  if (0 <= x && x < w && 0 <= y && y < h) {
    error[(y * w + x) * 3 + 0] += r * ratio;
    error[(y * w + x) * 3 + 1] += g * ratio;
    error[(y * w + x) * 3 + 2] += b * ratio;
  }
}

float gamma(float x) {
  return pow(x, 2.2);
}

追記