第06回 ディザリング

ディザリングとは、実際には少ない色だけを使いながら、遠目には多くの色を使っているかのように見せる手法のことである。

今回のプログラムの最終的な機能

プログラムを実行して少し待つとコンソールに「完了」が表示され、元画像が実行画面上に表示される。
dataフォルダには8つの画像が作られる。
そのあとでキーボードの1~8のキーを押すとこれらの画像が画面に表示される。
(すべての画像は自動的に作成されるので、キーボードを使うのは結果確認のためだけ)
キー 画像 意味 特徴
0 元.jpg 元画像
1 1二値化0.3.png 明度30%を閾値として二値化した画像 元画像の暗いところが黒、明るいところが白に置き換わる
2 1二値化0.5.png 明度50%を閾値として二値化した画像 1キーの画像より黒い部分が多い
3 1二値化0.7.png 明度70%を閾値として二値化した画像 2キーの画像より黒い部分が多い
4 1減色.png 白・黒・赤・緑・青・シアン・マゼンタ・黄の8色に減色した画像 もともとの色合いや形状の情報が大きく失われる
5 2ランダムグレー.png 明度を基準にランダムディザリングをかけた画像 遠目で見ると元画像の明度が再現されているが、ザラザラ感がある
6 2ランダムカラー.png R, G, Bの値を基準にランダムディザリングをかけた画像 遠目で見ると元画像の色が再現されているが、ザラザラ感がある
7 3組織グレー.png 明度を基準に組織的ディザリングをかけた画像 5キーの画像よりザラザラ感がなくなる
8 3組織カラー.png R, G, Bの値を基準に組織的ディザリングをかけた画像 6キーの画像よりザラザラ感がなくなる
キーと表示される画像

1. 二値化・減色

概要

単純にピクセルの色数を減らすと情報が大きく失われることを確認するために、まずは二値化・減色の処理を行う。
Processingでは、これらの処理はfilter関数で簡単に実行できる。
引数に「THRESHOLD」を入れると二値化、「POSTERIZE」を入れると減色 (ポスタリゼーション) の処理が行われる。

二値化の処理では元画像のそれぞれのピクセルの明度が閾値 (「いきち」、または「しきいち」と読む) 以上であれば白、それ未満であれば黒に変える。 結果として出力画像のピクセルは白か黒のどちらかになる。
一方、減色の処理は階調を指定して行う。階調は赤・緑・青それぞれの成分についての減色後のレベル数のことで、たとえば階調を2にして減色すると以下の表のような8色のどれかになる。
階調が3であれば色の数は \(3^3=27\)、4であれば\(4^3=64\) となる。階調を増やすほど出力画像の色は元画像に近づく。
色名
0 0 0
255 0 0
0 255 0
0 0 255
255 255 0
255 0 255 マゼンタ
0 255 255 シアン
255 255 255

課題 1

  1. Processingを起動する。
  2. 以下の未完成状態のコードをコピー&ペーストする。
  3. // 画像用の変数
    PImage[] img = new PImage[9];
    // 出力ファイル名
    String[] fName = {"元", "1二値化0.3", "1二値化0.5", "1二値化0.7", "1減色",
      "2ランダムグレー", "2ランダムカラー", "3組織グレー", "3組織カラー"};
    int w, h;
    
    void setup() {
      size(800, 600);
      w = width;
      h = height;
      img[0] = loadImage("元.jpg");
      img[0].resize(w, h);
      // 1~8番を元画像と同じ画像にする
      for (int i=1; i<=8; i++) {
        img[i] = img[0].get();
      }
      binarize(0.3, 1);   // 閾値0.3で二値化した画像をimg[1]に保存
      binarize(0.5, 2);   // 閾値0.5で二値化した画像をimg[2]に保存
      binarize(0.7, 3);   // 閾値0.7で二値化した画像をimg[3]に保存
      reduceColor(2, 4);  // R, G, Bそれぞれ2色に減色した画像をimg[4]に保存
      randomDither(true, 5);   // ランダムディザリング(白黒)した画像をimg[5]に保存
      randomDither(false, 6);  // ランダムディザリング(カラー)した画像をimg[6]に保存
      orderedDither(true, 7);  // 組織的ディザリング(白黒)した画像をimg[7]に保存
      orderedDither(false, 8); // 組織的ディザリング(カラー)した画像をimg[8]に保存
      println("完了");
      textFont(createFont("MS Pゴシック", 48));
      image(img[0], 0, 0, width, height);
    }
    
    void draw() {
    }
    
    // 閾値tで二値化する(課題1)
    void binarize(float t, int n) {
      img[n].save("data/"+ fName[n] +".png");
    }
    
    // RGBそれぞれcn色に減色する(課題1)
    void reduceColor(int cn, int n) {
      img[n].save("data/"+ fName[n] +".png");
    }
    
    // ランダムディザリングをかける(課題2)
    // isMonochromeがtrueなら白黒、falseならカラー
    void randomDither(boolean isMonochrome, int n) {
      for (int j=0; j<h; j++) {
        for (int i=0; i<w; i++) {
          color c = img[n].pixels[i+j*w];
          // 0~255のランダムな整数を作ってtに入れる
          if (isMonochrome) {
            // cの明度がtより小さければ0, そうでなければ255をbrに入れる
            // n番目の画像の(i, j)のピクセルに、明るさがbrの色を設定する
          } else {
            // cの赤成分がtより小さければ0, そうでなければ255をrに入れる
            // cの緑成分がtより小さければ0, そうでなければ255をgに入れる
            // cの青成分がtより小さければ0, そうでなければ255をbに入れる
            // n番目の画像の(i, j)のピクセルに、色成分がr, g, bの色を設定する
          }
        }
      }
      img[n].save("data/"+ fName[n] +".png");
    }
    
    // 組織的ディザリングをかける(課題3)
    // isMonochromeがtrueなら白黒、falseならカラー
    void orderedDither(boolean isMonochrome, int n) {
    }
    
    void keyPressed() {
      int k = key-'0';
      if (k>=0 && k<=8) {
        background(0);
        image(img[k], 0, 0, width, height);
        fill(255, 0, 0);
        text(fName[k], 30, height-30);
        fill(255);
      }
    }
  4. 「img06」という名前で保存する。
  5. 適当に画像検索して元画像 (概要の表の8色それぞれに近い色を含むもの) を用意する。
  6. (最初の説明の実行例では、著作権的な理由で教員の手持ち写真を使っているが、その元画像は減色の結果の検証にはあまり適切ではない。「繁華街」や「花壇」などで画像検索すれば良いものが見つかる)

  7. 画像の形式に応じて以下の変更を加える。
    • JPG形式の場合→名前を元.jpgに変更
    • それ以外の場合→ペイントで開き、形式をJPGに指定して「元」という名前で保存
  8. 「元.jpg」をProcessingのウインドウにドラッグ&ドロップする。
  9. binarize関数の最初に
      img[n].filter(THRESHOLD, t);
    を追加する。
    (第2引数で閾値を指定する。これが白と黒のどちらになるかの境目の値なので、これが大きいほど出力画像の黒部分が多くなる)

  10. reduceColor関数の最初に
      img[n].filter(POSTERIZE, cn);
    を追加する。
    (第2引数で階調を指定する。これが多くなるほど色数が多くなるが、この課題では2を指定する。結果として8色になる)

  11. プログラムを実行する。
    • コンソールに「完了」が表示されてから1~3キーを押すと閾値0.3, 0.5, 0.7で二値化した画像が表示される。
    • 4キーを押すと階調2に減色した画像、つまり「黒・赤・緑・青・黄・マゼンタ(紫)・シアン(水色)・白」の8色に減色した画像が表示される。

2. ランダムディザリング

概要

課題1で見たように、普通に二値化を行うと、元画像の情報が大きく失われてしまう。
そこで、ピクセルによって0~255の範囲でランダムに閾値を変えると、同じ明るさの場所でも閾値のばらつきのためにあるときは白、あるときは黒になるが、暗いところでは黒の割合が多く、明るいところでは白の割合が多くなる。 そのため、出力画像を離れてみると元の構造が残るようになる。
8色への減色の場合も、赤・緑・青の成分それぞれの値について閾値をランダムに変えれば、使う色を8色にしたまま見た目の色合いを残すことができる。

課題 2

  1. randomDither関数の「// 0~255のランダムな整数を作ってtに入れる」の左に以下のコードを追加する。
    int t = (int)random(256);
    (「random(256)」では0以上256未満の実数が得られる)
    (それを「(int)」で実数化すると小数部分が切り捨てられて0~255の整数になる)

  2. randomDither関数の「// cの明度がtより小さければ0, そうでなければ255をbrに入れる」の左に以下のコードを追加する。
    float br = brightness(c) < t ? 0 : 255;
    補足
    「?」は条件演算子と呼ばれるもの。「条件 ? 値1 : 値2;」のように記述すると、条件が満たされるときは値1、満たされないときは値2が得られる。このコードでは「brightness(c) < t」つまり「元画像のそのピクセルの明るさが t より小さい」という条件が満たされれば0, そうでなければ255が変数 br に代入される。条件演算子を使わずにこの処理を書くと以下のようになる。
    float br;
    if (brightness(c) < t){
      br = 0;
    } else {
      br = 255;
    }
    

  3. randomDither関数の「// n番目の画像の(i, j)のピクセルに、明るさがbrの色を設定する」の左に以下のコードを追加する。
    (color関数で色を作るとき、引数を1つだけにするとその値の明度の灰色(この場合は0か255が入るので黒か白)になる)
    img[n].pixels[i+j*w] = color(br);

  4. randomDither関数の「// cの赤成分がtより小さければ0, そうでなければ255をrに入れる」の左に以下のコードを追加する。
    (2番目のステップのときと考え方は同じ。赤成分 r を0か255のどちらかにする)
    float r = red(c) < t ? 0 : 255;

  5. 同様にして「// cの緑成分がtより小さければ0, そうでなければ255をgに入れる」「// cの青成分がtより小さければ0, そうでなければ255をbに入れる」の左に必要なコードを追加する。
    (緑成分はgreen関数、青成分はblue関数で取り出す)

  6. randomDither関数の「// n番目の画像の(i, j)のピクセルに、色成分がr, g, bの色を設定する」の左に以下のコードを追加する。
    img[n].pixels[i+j*w] = color(r, g, b);

  7. プログラムを実行する。
    • コンソールに「完了」が表示されてから5キーを押すと明度を基準にランダムディザリングした画像、6キーを押すとR, G, Bそれぞれの成分についてランダムディザリングした画像が表示される。
    • 白黒の方は二値化画像と同様にピクセルの色は白と黒のどちらかだが、密度で濃淡が再現されているために元画像の構造が残る。
    • カラーの方は階調2で減色した画像と同様にピクセルの色は8色のどれかだが、こちらも減色画像にくらべて元画像の構造が残っていることがわかる。

3. 組織的ディザリング

概要

課題2で見たように、ランダムディザリングでは元画像の情報を残すことができるが、全体にザラザラした感じになってしまう。これは、乱数がならされるのに広い範囲が必要なので、狭い範囲では偏りが生じてしまうためである。そこで、狭い範囲で閾値のパターンを繰り返すことでこのザラザラ感をなくすことができる。具体的には、
0 8 2 10
12 4 14 6
3 11 1 9
15 7 13 5
のようなマスクを用意し、元画像を4×4のブロックに分割して、対応するマスクの値に応じて閾値を変える。


それぞれのピクセルに対する閾値は、

「そこに対応するマスクのマスの数値」×16 + 8

で求められる。例えば、上の図の赤矢印、青矢印のマスク側の値は10, 1なので、矢印の先のピクセルに対する閾値はそれぞれ168, 24になる。マスクの値は平均して7.5なので、全体としての閾値の平均値は7.5×16+8=128になる。
この値をそれぞれのピクセルについて閾値として使えば、狭い範囲でも「ならされた」明るさが得られる。
明るさではなくR, G, B成分の値について同様の処理を行えば、8色だけのピクセルをつかいながらも、元画像の色情報を多く残した画像になる。

ここで上げたマスクは組織的ディザリングで使われるものの一つでBayer型と呼ばれるもの。このほかにも

渦型
6 7 8 9
5 0 1 10
4 3 2 11
15 14 13 12

網点型
11 4 6 9
12 0 2 14
7 8 10 5
3 15 13 1
などが存在する。

課題 3

  1. randomDither関数の中のコードをorderedDither関数の中にコピー&ペーストする。

  2. orderedDither関数の最初に以下のコードを追加する。
      int[] mask = {
        0, 8, 2, 10,
        12, 4, 14, 6,
        3, 11, 1, 9,
        15, 7, 13, 5
      };
    (これが概要にあるBayer型のマスクの値)

  3. orderedDither関数の
          int t = (int)random(256);// 0~255のランダムな整数を作ってtに入れる
          int t = mask[i%4+j%4*4]*16+8; // マスク上の位置に応じた閾値をtに入れる
    に変更する。
    (マスク上の横位置・縦位置は0~3の値)
    (たとえば元画像の横座標6のピクセルは、4x4の区切りでいうと左から2つ目のグループの中で左から3番目 (0から始まる数え方では2番目)にある)
    (つまり、マスク上の横位置は画像上の横座標 i を4で割った余り (i%4) で求められる)
    (同様に、マスク上の縦位置は画像上の縦座標 j を4で割った余り (j%4) で求められる)
    (マスク上の通し番号は「マスク上の横位置 + マスク上の縦位置*4」で求められる → このコードの[]の中)

  4. プログラムを実行する。
    • コンソールに「完了」が表示されてから7キーを押すと明度を基準に、8キーを押すとR, G, Bそれぞれの成分についてBayer型のマスクでディザリングした画像が表示される。
    • 白黒・カラーのどちらも、ランダムディザリングに比べてザラザラ感が軽減される。

提出

締切後提出用フォーム
※ 点数はつきますが、欠席だった場合は出席にはなりません。
※ コードはTeamsのClass Notebookに保存してください。
戻る