第6回 ディザリング

本題に入る前に、必ず 連絡 の動画を見てください。
Google Colabへのリンク

1. リサイズとグレースケール化

動画の解説を参照

「ディザリング」では、黒い点の密度でそのエリアの明るさを表現します。
元画像の解像度の違いによる影響を避けるため、今回もサイズ変更を行い、横幅が800ピクセルになるように調整します。
また、次の項から行う処理の基準はピクセルの明るさだけなので、処理を軽くするためにグレースケール化します。
グレースケール化には色変換の関数「cvtColor」を使います。第1引数は画像の変数、第2引数を「cv2.COLOR_BGR2GRAY」とすることでグレースケール化の処理が行われます。
import cv2
import numpy as np
from google.colab import files

file = files.upload()
filename = next(iter(file))
img = cv2.imread(filename)
h, w = img.shape[:2]

new_w = 800
new_h = int(h * (new_w / w))
img1 = cv2.resize(img, (new_w, new_h))

img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
cv2.imwrite('ex1.png', img1)
(次の項以降で各ピクセルの明るさの情報を使うため、ここでは劣化のないPNG形式で保存しています)
元画像の選び方についての注意
今回確認したいことは主に「二値化のパラメータの違いによる抽出範囲の変化」「ディザリングによる構造の維持」の2つです。
詳しくは次項以降で詳しく解説しますが、元画像には明るさが連続的に変わる (グラデーションがある) エリアがあり、何らかの形が判別できる被写体が入っていて、それなりに解像度が大きいものを使ってください。
例えばこのような画像です。
(今回は出力画像もこのページに掲載されているので、元画像としてこの画像は使わないでください)

課題1

上記の太枠の説明の条件を満たす元画像を元にして、横幅が800ピクセルのグレースケール画像 ex1.pngを出力してください。

2. 二値化

動画の解説を参照

二値化の処理では、出力画像の各ピクセルは「完全な白」「完全な黒」のどちらかになります。
どちらになるかは元画像のピクセルが閾値 (「いきち」、または「しきいち」と読みます) より明るいかどうかで決まります。
OpenCVでは「threshold」(スレッショルド) 関数で二値化の処理ができます。第1引数が画像の変数、第2引数が閾値、第3引数が閾値より明るいピクセルに設定される明るさ、第4引数は処理のタイプです (ここを変えると、例えば「閾値より明るいところは元のまま」のようなこともできます)。
例えばこのようにすると50より明るいピクセルは白になり、明るさが50未満のものは黒になります。
(関数の後ろに[1]を書くのは、この関数の戻り値がタプルであるためです。先頭の要素は適用された閾値、次の要素が変換された画像です)
img2_050 = cv2.threshold(img1, 50, 255, cv2.THRESH_BINARY)[1]

閾値を大きくすると黒のエリアが広くなります。
なぜなら、例えば元画像での明るさが78だった場合は閾値50では白になりますが、閾値を100にすれば「それより暗い」と判定されるため黒になるせいです。
(以下はそれぞれ閾値100, 150で二値化した画像です)
img2_050 = cv2.threshold(img1, 50, 255, cv2.THRESH_BINARY)[1]
img2_100 = cv2.threshold(img1, 100, 255, cv2.THRESH_BINARY)[1]
img2_150 = cv2.threshold(img1, 150, 255, cv2.THRESH_BINARY)[1]
cv2.imwrite('ex2_050.png', img2_050)
cv2.imwrite('ex2_100.png', img2_100)
cv2.imwrite('ex2_150.png', img2_150)

課題2

課題1で作成したグレースケール画像を元にして、閾値50, 100, 150で二値化した画像 ex2_050.png, ex2_100.png, ex2_150.png を出力してください。
※ 閾値が大きいほど黒いエリアが広くなるはずです。出力された画像を比較して確認してください。

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

動画の解説を参照

二値化の処理で閾値を調整すれば注目したい部分のエッジを残すことはできますが、それ以外の部分で「白つぶれ」「黒つぶれ」が起こり、全体の形が失われてしまいます。
それぞれのピクセルで閾値をランダムに変えれば、明るいエリアでは黒ピクセルがまばらになり、暗いエリアでは黒ピクセルが多くなるため、人間の目にはそれぞれ明るく・暗く見えます。
以下のコードでは「zeros」でグレースケール画像と同じ大きさの真っ黒な画像を作り、「random.randint」で 0~255 の閾値を作っています。
どのピクセルも初期値が黒 (0) なので、閾値より明るい場合だけそのピクセルを白 (255) に変えます。
img3 = np.zeros((new_h, new_w), dtype=np.uint8)
for j in range(new_h):
  for i in range(new_w):
    t = np.random.randint(0, 255)
    if img1[j, i] > t:
      img3[j, i] = 255
cv2.imwrite('ex3.png', img3)

課題3

課題1で作成したグレースケール画像を元にして、ランダムディザリングした画像 ex3.png を出力してください。
※ 二値化画像とは異なり、グラデーションが再現されているはずです。ただし、全体的にザラザラした感じになります。

4. 組織的ディザリング

動画の解説を参照

組織的ディザリングでは、4×4の「マスク」を使います。以下は「Bayer型マスク」と呼ばれるものです。
0 8 2 10
12 4 14 6
3 11 1 9
15 7 13 5
このマスクの値を元にして、「16倍して8を加える」という計算で閾値の値を決めます。
例えばマスクの値が 0 なら 閾値は 8, 15 なら 248 になります。

元画像の位置と、適用するマスクの位置は下の図のように対応します。
つまり、「横座標を4で割った余り」「縦座標を4で割った余り」の横位置・縦位置のマスクの位置を使うことになります。
img4 = np.zeros((new_h, new_w), dtype=np.uint8)
mask = [[0, 8, 2, 10],
        [12, 4, 14, 6],
        [3, 11, 1, 9],
        [15, 7, 13, 5]]
for j in range(new_h):
  for i in range(new_w):
    t = mask[j % 4][i % 4] * 16 + 8
    if img1[j, i] > t:
      img4[j, i] = 255
cv2.imwrite('ex4.png', img4)
組織的ディザリングに使われるマスクには、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

課題4

課題1で作成したグレースケール画像を元にして、Bayer型マスクで組織的ディザリングした画像 ex4.png を出力してください。
※ ランダムディザリングの結果と比べてザラザラ感は少なくなります。
※ グラデーションがあった部分は帯のように何段階かに分かれます。この境目を疑似エッジといいます。

提出

今回作成したノートブックを「imgprc2024@gmail.com」と共有してください。
※ 第2回で共有した、画像共有用の「画像処理」フォルダの中に「第6回」フォルダを作り、その中に今回の元画像と出力画像を入れてください。
※ 課題の再提出の際は、ノートブックの再度の共有はせずにチャットで連絡してください。