本記事では、ディザリングの中でも最も基本的な Ordered Dithering(規則ディザ) を取り上げ、Processingで実際に動くコードとして実装します。さらに、Bayer行列の意味(白になる順番)と、規則ディザ特有の「模様感」をどのように扱うかまで整理します。

この記事の完全なサンプルコードは、Patreonからダウンロードできます。
Coffee Supplier on Patreon

1. ディザリングとは何か?

ディザリング(dithering)とは、限られた色数(あるいは階調数)しか出せない出力に対して、人間の視覚特性を利用し、擬似的に中間階調を表現する技術です。

たとえば白と黒の2値しか出せない状況で、画像を単純に「明るいか暗いか」で閾値処理してしまうと、グラデーションは階段状に分断され、滑らかさを失ってしまいます。これは ポスタリゼーション(階調飛び)バンディング と呼ばれる現象で、特に空や影のような、なだらかな変化を持つ領域で顕著に現れます。

一方で、白と黒の配置を意図的に分散させると、近距離では「粒」や「模様」として見えるにもかかわらず、少し離れた距離ではそれらが平均化され、中間の明るさが存在するように知覚されます。このとき画面上には実際には中間色が存在していないにもかかわらず、視覚が空間的な平均を取ることで階調が補完されます。ディザリングは、まさにこの「錯覚」を設計する技術です。

この考え方は、新聞や雑誌などの印刷で用いられる 網点(ハーフトーン) にもよく似ています。印刷ではインクの濃淡を連続的に変えることが難しいため、ドットの密度や配置によって濃淡を表現してきました。ディザリングは、こうした網点の思想をデジタル画像へ移植し、ピクセル単位のアルゴリズムとして実装したものだとも言えます。

さらに重要なのは、ディザリングが単なる「古い低解像度表現」ではないという点です。ディザリングは現在でも、たとえば以下のような目的で利用されています。

  • 低ビット深度の表示(e-ink、組み込みディスプレイ、LEDなど)
  • 画像圧縮や表現の簡略化(アート表現としての二値化・少色化)
  • バンディングの回避(滑らかな階調を“粒”に変換する)
  • 生成表現としての質感設計(模様・ノイズ・レトロ感)

つまりディザリングは、技術的制約を埋めるだけでなく、画像の見え方そのものをデザインする手法でもあります。本記事ではまず、ディザリングの中でも最も基本的で実装しやすい Ordered Dithering(規則ディザ) を取り上げ、Processingで動くコードへ落とし込みながら理解していきます。

2. Ordered Dithering(規則ディザ)とは?

ディザリングにはいくつかの系統がありますが、本記事で扱う Ordered Dithering は次のような特徴を持ちます。

  • ピクセルを白黒(または少階調)に量子化する
  • ただし閾値を固定せず、位置によって変える
  • 結果として、規則的なパターンで階調が表現される

通常の2値化では「明るさ > 128 なら白」のように、画像全体で同じ閾値を使います。しかし Ordered dithering では、閾値がピクセル位置によって変化します。

つまり「あらかじめ、位置ごとの白になりやすさ/なりにくさを決めておく」という発想です。その結果、画像の明るさが少し変わったときに、白がまとまって増えるのではなく、空間的に散りながら増えるようになります。これが、白黒2値にもかかわらず階調があるように見える理由です。

Ordered dithering は計算が軽く、実装も単純であるため、リアルタイム用途にも向いています。また、別系統の誤差拡散型(Floyd–Steinbergなど)と比べて「粒が安定する」傾向があり、動画・アニメーションにおいても扱いやすいのが特徴です。

3. Bayer行列とは?(4×4の例)

Ordered dithering の代表例として、Bayer(ベイヤー)ディザリングがよく知られています。Bayerディザでは、以下のような 閾値行列(threshold map) を繰り返し敷き詰めて使用します。

int[][] bayer4 = {
  { 0, 8, 2, 10},
  {12, 4, 14, 6},
  { 3, 11, 1, 9},
  {15, 7, 13, 5}
};

ここで重要なのは、この数字が単なるランダムな並びではなく、「白になる順番」を表している点です。画面の明るさが 0 → 255 へ少しずつ上がっていくと、4×4のピクセルタイルの中で、番号の小さい位置から順に白へ変わっていきます。

この「白になる順番」を空間的に分散させることで、白が局所的に固まるのを防ぎ、階調が滑らかに見えるようになります。また、規則的なパターンに基づいて変化するため、面としての明るさが安定しやすいという特徴もあります。

4. Processingでの実装(Bayer 4×4)

ProcessingでBayerディザを実装する場合、基本は以下の手順になります。

  1. 元画像の brightness(明るさ)を取得する
  2. Bayer行列から、ピクセル位置に対応する値を取り出す
  3. それを閾値として白黒に変換する

以下は最小構成の実装例です。


void applyBayerDithering4(PGraphics source) {
  loadPixels();
  source.loadPixels();

  int[][] bayer4 = {
    { 0, 8, 2, 10},
    {12, 4, 14, 6},
    { 3, 11, 1, 9},
    {15, 7, 13, 5}
  };

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int loc = x + y * width;

      float b = brightness(source.pixels[loc]); // 0..255

      float t = bayer4[x % 4][y % 4];           // 0..15
      float threshold = (t / 15.0) * 255.0;

      pixels[loc] = (b > threshold) ? color(255) : color(0);
    }
  }

  updatePixels();
}

Processingの pixels[] 1次元配列 です。そのため、(x, y) という2次元座標を「配列の何番目か」に変換する必要があります。

int loc = x + y * width;

loc は、「このピクセルが pixels[] の何番目か」を表しています。

次の行が、Bayer行列を画面全体へ繰り返す処理です。

float t = bayer4[x % 4][y % 4];

x % 4y % 4 によって、x座標とy座標が 0, 1, 2, 3 の範囲に折り返されます。その結果、4×4のBayer行列が画面全体にタイル状に繰り返し適用され、位置ごとに異なる閾値が割り当てられます。

次の行では、Bayer行列の値を閾値として使うためにスケールを合わせています。

float threshold = (t / 15.0) * 255.0;
  • Bayer行列の値 t は 0..15
  • brightness() の値 b は 0..255

このままではスケールが一致せず、比較が成立しません。そこで、

  • t / 15.0 によって 0..15 を 0..1 に正規化し
  • * 255.0 を掛けて 0..255 に戻します

こうして得られた thresholdb を比較することで、

  • 位置によって閾値が変わる
  • その結果、規則的なパターンで白が増えていく

という Ordered dithering の効果が得られます。

5-1. 8×8 / 16×16への拡張

Bayerディザは4×4だけではなく、8×8や16×16に拡張することができます。行列サイズを大きくすると、同じ明るさの領域でも白黒の配置がより細かくなり、階調が滑らかに見えます。

  • 4×4:模様が目立つ(レトロ感が強い)
  • 8×8:模様が細かくなり階調が増える
  • 16×16:さらに滑らかになるが、画像によってはぼやける場合もある

Bayer 8×8 行列(0..63)は以下のようになります。

int[][] bayer8 = {
  { 0, 48, 12, 60,  3, 51, 15, 63},
  {32, 16, 44, 28, 35, 19, 47, 31},
  { 8, 56,  4, 52, 11, 59,  7, 55},
  {40, 24, 36, 20, 43, 27, 39, 23},
  { 2, 50, 14, 62,  1, 49, 13, 61},
  {34, 18, 46, 30, 33, 17, 45, 29},
  {10, 58,  6, 54,  9, 57,  5, 53},
  {42, 26, 38, 22, 41, 25, 37, 21}
};

このような行列サイズを扱う場合は、ProcessingでBayer行列を自動生成し、切り替えられるようにしておくと便利です。以下はその実装例です。


void applyBayerDithering(PGraphics source, int matrixSize) {
  if (!isPowerOfTwo(matrixSize) || matrixSize < 2) return;

  int[][] bayer = makeBayerMatrix(matrixSize);
  float maxV = matrixSize * matrixSize - 1;

  loadPixels();
  source.loadPixels();

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int loc = x + y * width;

      float b = brightness(source.pixels[loc]);
      float t = bayer[x % matrixSize][y % matrixSize];
      float threshold = (t / maxV) * 255.0;

      pixels[loc] = (b > threshold) ? color(255) : color(0);
    }
  }
  updatePixels();
}

int[][] makeBayerMatrix(int n) {
  if (n == 2) {
    return new int[][] {
      {0, 2},
      {3, 1}
    };
  }

  int half = n / 2;
  int[][] prev = makeBayerMatrix(half);
  int[][] m = new int[n][n];

  for (int y = 0; y < half; y++) {
    for (int x = 0; x < half; x++) {
      int v = prev[x][y];

      m[x][y]               = 4 * v + 0;
      m[x + half][y]        = 4 * v + 2;
      m[x][y + half]        = 4 * v + 3;
      m[x + half][y + half] = 4 * v + 1;
    }
  }
  return m;
}

boolean isPowerOfTwo(int n) {
  return (n & (n - 1)) == 0;
}

5-2. levels(階調数)への拡張

ここまでの実装は、出力が「黒 or 白」の2値に固定されていました。しかしディザリングは本来、2値だけでなく少ない階調数(4階調、8階調、16階調…)でも利用できます。

ここで言う levels は「量子化の段数(出力できる明るさの数)」を意味します。

  • levels = 2 → 2値(0 / 255)
  • levels = 4 → 4階調(0 / 85 / 170 / 255)
  • levels = 8 → 8階調
  • levels = 16 → 16階調

ディザリングの役割は「階調を増やす」ことではなく、少ない階調に落とす際に発生するバンディング(階段状の段差)を、空間的な粒へ分散させることにあります。

Ordered dithering の本質は「量子化の境界を位置によってずらす」ことです。2値の場合は「白になるか黒になるか」を決める閾値を位置で変えていましたが、levels を増やす場合は「どの段階に丸めるか」を決める境界を位置で変えます。

つまり、次のように一般化できます。

  • 入力 brightness は 0..255(連続的)
  • 出力は levels 段階(離散的)
  • Bayer行列は「どの位置が先に明るくなるか」の順番を持つ
  • その順番を使って、量子化の境界をわずかにずらす

Processingでの実装(levels対応版)

以下は、Bayer行列のサイズ(4/8/16…)に加えて、階調数 levels も指定できるようにした実装例です。


void applyBayerDitheringLevels(PGraphics source, int matrixSize, int levels) {
  if (!isPowerOfTwo(matrixSize) || matrixSize < 2) return;
  if (levels < 2) return;

  int[][] bayer = makeBayerMatrix(matrixSize);

  // Bayer値は 0..(N^2-1) だが、正規化の都合で N^2 を使う
  float maxV = matrixSize * matrixSize;

  loadPixels();
  source.loadPixels();

  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int loc = x + y * width;

      float b = brightness(source.pixels[loc]); // 0..255

      // Bayer値を 0..1 に正規化
      float t = bayer[x % matrixSize][y % matrixSize];
      float d = (t + 0.5) / maxV;  // 0..1(中心寄せ)

      // 明るさを 0..1 に正規化
      float bn = b / 255.0;

      // levels段階に量子化(境界を d でずらす)
      float q = floor(bn * (levels - 1) + d);
      q = constrain(q, 0, levels - 1);

      // 量子化値を 0..255 に戻す
      float out = (q / (levels - 1)) * 255.0;

      pixels[loc] = color(out);
    }
  }

  updatePixels();
}

この処理は、次の3段階に分解できます。

1) Bayer行列の値を 0..1 に正規化する

Bayer行列は 0..(N^2-1) の整数です。これを 0..1 の範囲に正規化し、位置ごとに異なる「微小な補正値」として使います。

float d = (t + 0.5) / maxV;

ここで +0.5 を入れているのは、各セルの値を「区間の中心」に寄せるためです。これにより、ディザリング結果の偏りが減り、パターンがより均一になりやすくなります。

2) 明るさを levels 段階へ丸める(ただし境界をずらす)

通常の量子化では、

q = floor(bn * (levels - 1));

のように、明るさを levels 段階へ丸めます。しかしこの方法だと境界が固定されるため、バンディングが発生しやすくなります。

Ordered dithering ではこの境界を、Bayer行列の値 d によって位置ごとにずらします。

q = floor(bn * (levels - 1) + d);

この + d によって、

  • ある位置では一段階明るくなりやすい
  • 別の位置では一段階暗くなりやすい

という偏りが生まれます。結果として境界が空間的に散らばり、段差が粒へ分解されます。

3) 量子化値を 0..255 に戻す

量子化した段階 q は 0..(levels-1) の整数なので、それを 0..255 の明るさに戻して出力します。

float out = (q / (levels - 1)) * 255.0;

使い方例

以下のように、行列サイズと階調数を独立に調整できます。

applyBayerDitheringLevels(pg, 4, 2);   // 4x4 + 2値
applyBayerDitheringLevels(pg, 4, 4);   // 4x4 + 4階調
applyBayerDitheringLevels(pg, 8, 8);   // 8x8 + 8階調
applyBayerDitheringLevels(pg, 16, 16); // 16x16 + 16階調

matrixSize と levels の違い

混同しやすい点として、matrixSizelevels は役割が異なります。

  • matrixSize:模様(パターン)の細かさを決めます
    → 大きいほどパターンが細かくなります
  • levels:出力の階調数(量子化段数)を決めます
    → 大きいほど階調が滑らかになります

そのため、階調の滑らかさを強く変えたい場合は、matrixSize を上げるより levels を上げた方が効果が分かりやすいです。

6. Bayer行列の意味:「白になる順番」

Bayer行列は「閾値マップ」として説明されることが多いですが、理解としては 優先順位マップ と捉えるほうが分かりやすいです。

  • 値が小さい場所ほど早く白になる
  • 値が大きい場所ほど最後まで黒のまま残る

これにより、画像の明るさが増えていく過程で、白が空間的に散らばりながら増えます。結果として、白黒2値にも関わらず中間階調が存在するように見えます。

7. 規則ディザの「模様感」をどう扱うか

Ordered dithering の最大の特徴は、階調表現と引き換えに 規則的な模様 が必ず現れることです。この模様は状況によって、欠点にも強みにもなります。

7-1. 欠点:模様が気になるケース

次のような領域では規則パターンが目立ちやすく、画像が「処理された」印象になりやすいです。

  • 広いベタ面
  • ゆっくりしたグラデーション
  • 細い線や文字の周辺

7-2. 強み:動画で安定する

一方で、誤差拡散ディザ(Floyd–Steinbergなど)は動画にすると粒がフレーム間で動きやすく、右下方向へ“わらわら動く”ような印象が出ることがあります。

それに対して Ordered dithering は「模様が固定」されるため、動画・アニメーションにおいて視覚的に安定しやすいのが特徴です。

7-3. 表現として使う

規則ディザは欠点を隠すのではなく、模様を積極的にデザイン要素として使うこともできます。

  • レトロな質感
  • 印刷物のような網点感
  • 生成表現としてのパターン

特にProcessingで扱う場合、ディザリングは画像処理というより「質感生成」に近い側面を持ちます。

8. Ordered dithering(Bayer)

本記事では、ディザリングの基本として Ordered dithering(Bayer)を扱い、Processingでの実装まで落とし込みました。

  • ディザリングは少ない階調で中間階調を錯覚させる技術です
  • Ordered dithering は位置によって閾値を変えます
  • Bayer行列は「白になる順番」を設計した優先順位マップです
  • 4×4、8×8、16×16で模様と階調の性質が変化します
  • 規則ディザの模様感は欠点にも表現にもなります

次回は、もう一つの柱である Error diffusion(誤差拡散) を取り上げ、Floyd–SteinbergをProcessingで実装し、Ordered ditheringとの違いを比較します。

9. 作品例|Sound visualization

Recommended Book / barbe_generative_library

Grid-Systems-Raster-Systeme

Grid systems in graphic design – Josef Müller-Brockmann

グリッドシステムとは。

グリッドシステムとは、連続した行と列に基づいてテキストやイメージを配置しデザインの整合性と視認性を高めるレイアウト手法です。元々は印刷レイアウトのために考えられたものですが、現在ではWebデザインなど、あらゆるデザイン設計において利用され、ProcessingやP5jsなど2Dでグラフィックを制作する場合にも、このグリッドシステムから学べる事は多い。

ーーーーー
► 『Grid systems in graphic design 』(Josef Müller-Brockmann

1981年に刊行された完全日本語訳版。
初版/ 2019.11.9
ページ数/184ページ
出版社/ボーンデジタル
言語/日本語

BGD_SOUNDS on bandcamp

BGD_SOUNDSでは、アートワークで使用するために録音された様々な音を公開しています。

Link / BGD_SOUNDS on bandcamp

安価で利用できる膨大な著作権フリーのサウンドライブラリーを目指し、定期的に音源ライブラリーを増やしています。音源のほとんどは、192kHzの32bitにて録音。音楽制作や映像制作など、それぞれの用途に合わせて利用できます。(メタデータも含まれています。)

BGD_CLUB(月額サブスクリプション)では、低価格からすべてのライブラリーにアクセスし自由にダウンロードすることが可能です。