ディザ、ディザリング
輝度が0から255までのグラデーションのグレースケールを
白黒2値化すると中間で白と黒に分かれるので
元の画像とはかなり違うものになる
ディザリングを使うと
なにより見た目がかっこいい!!
灰色を白黒2値で表現するには白と黒の割合が同じになるように並べると
それっぽく見える
ここからエクセル方眼紙
ディザパターン
輝度によって白と黒の割合を変える、なるべく偏らないように並べる
これをパターンに当てはめて2値化すると
こうなる
2x2マスのディザパターンを使うから
画像も2x2の4マスに分けて考える
輝度93は51~102の間なのでディザパターンbになる
上の93はマスの左上、パターンbの左上は白なので白判定
下の93はマスの左下、パターンbの左下は黒なので黒判定
輝度116はパターンc、当てはめると
上の116は黒、下の116は白
結果、このマスは
2x2に当てはまらない画像のとき
195はパターンdでdの左上は白なので白判定になる
正規化
輝度値は0~255で指定するけどしきい値にするには計算しにくいので0~1に変換して計算する
しきい値を4つ指定すると全体を5分割できることになるから
5段階の表現ができる
ここまでエクセル方眼紙
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;
namespace _20180123_パターンディザ2x2白黒2値
{
<summary>
</summary>
public partial class MainWindow : Window
{
BitmapSource OriginBitmap;
public MainWindow()
{
InitializeComponent();
this.Title = this.ToString();
this.AllowDrop = true;
this.Drop += MainWindow_Drop;
ButtonOrigin.Click += ButtonOrigin_Click;
ButtonTest1.Click += ButtonTest1_Click;
ButtonTest2.Click += ButtonTest2_Click;
ButtonTest3.Click += ButtonTest3_Click;
ButtonNotDithering.Click += ButtonNotDithering_Click;
NumericScrollBar.ValueChanged += NumericScrollBar_ValueChanged;
NumericTextBox.TextChanged += NumericTextBox_TextChanged;
}
private void ButtonNotDithering_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
MatrixThreshold();
}
private void NumericTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = (TextBox)sender;
double d;
if (!double.TryParse(textBox.Text, out d))
{
textBox.Text = System.Text.RegularExpressions.Regex.Replace(textBox.Text, "[^0-9]", "");
}
}
private void NumericScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
if (OriginBitmap == null) { return; }
MatrixThreshold();
}
private void ButtonTest3_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
Matrix3_Bayer2x2();
}
private void ButtonTest2_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
Matrix2_Bayer4x4();
}
private void ButtonTest1_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
Matrix1_Bayer2x2();
}
private void ButtonOrigin_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
MyImage.Source = OriginBitmap;
}
private void MainWindow_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) { return; }
string[] filePath = (string[])e.Data.GetData(DataFormats.FileDrop);
OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Gray8, 96, 96);
if (OriginBitmap == null)
{
MessageBox.Show("not Image");
}
else
{
MyImage.Source = OriginBitmap;
}
}
private void MatrixThreshold()
{
double[][] thresholdMap = new double[][]
{
new double[] { NumericScrollBar.Value / 255 }
};
DitheringGrayScale(thresholdMap);
}
private void Matrix1_Bayer2x2()
{
double[][] thresholdMap = new double[][]
{
new double[] { 1f / 5f, 3f / 5f },
new double[] { 4f / 5f, 2f / 5f }
};
DitheringGrayScale(thresholdMap);
}
private void Matrix2_Bayer4x4()
{
double[][] thresholdMap = new double[][]
{
new double[] { 1f / 17f, 13f / 17f, 4f / 17f, 16f / 17f },
new double[] { 9f / 17f, 5f / 17f, 12f / 17f, 8f / 17f },
new double[] { 3f / 17f, 15f / 17f, 2f / 17f, 14f / 17f },
new double[] { 11f / 17f, 7f / 17f, 10f / 17f, 6f / 17f }
};
DitheringGrayScale(thresholdMap);
}
private void Matrix3_Bayer2x2()
{
double[][] thresholdMap = new double[][]
{
new double[] { 1f / 5f, 3f / 5f },
new double[] { 2f / 5f, 4f / 5f }
};
DitheringGrayScale(thresholdMap);
}
private void DitheringGrayScale(double[][] thresholdMap)
{
var wb = new WriteableBitmap(OriginBitmap);
int h = wb.PixelHeight;
int w = wb.PixelWidth;
int stride = w;
byte[] pixels = new byte[h * stride];
wb.CopyPixels(pixels, stride, 0);
long p = 0;
int xx = thresholdMap[0].Length;
int yy = thresholdMap.Length;
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
p = y * stride + x;
if (thresholdMap[y % yy][x % xx] <= (double)pixels[p] / 255)
{ pixels[p] = 255; }
else { pixels[p] = 0; }
}
}
wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
MyImage.Source = wb;
}
<summary>
</summary>
<param name="filePath"></param>
<param name="pixelFormat"></param>
<param name="dpiX"></param>
<param name="dpiY"></param>
<returns></returns>
private BitmapSource GetBitmapSourceWithChangePixelFormat2(
string filePath, PixelFormat pixelFormat, double dpiX = 0, double dpiY = 0)
{
BitmapSource source = null;
try
{
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
var bf = BitmapFrame.Create(fs);
var convertedBitmap = new FormatConvertedBitmap(bf, pixelFormat, null, 0);
int w = convertedBitmap.PixelWidth;
int h = convertedBitmap.PixelHeight;
int stride = (w * pixelFormat.BitsPerPixel + 7) / 8;
byte[] pixels = new byte[h * stride];
convertedBitmap.CopyPixels(pixels, stride, 0);
if (dpiX == 0) { dpiX = bf.DpiX; }
if (dpiY == 0) { dpiY = bf.DpiY; }
source = BitmapSource.Create(
w, h, dpiX, dpiY,
convertedBitmap.Format,
convertedBitmap.Palette, pixels, stride);
};
}
catch (Exception)
{
}
return source;
}
}
}
画像ファイルのドロップで画像を開く
カラーの画像はグレースケールに変換したものが表示される
Testボタン 2x2のディザパターン
Test2 4x4ディザパターン
Test3 変則的な2x2ディザパターン
これが
って今見たら0.8と0.6の指定が逆になっている
けど対角線上で入れ替わっているだけで偏っていないから問題ない
ディザパターンを使って白黒2値化
WriteableBitmapのCopyPixelsで色(輝度)情報を配列にコピーして
ディザパターンの行列を使って白黒2値化したら
WriteableBitmapのWritePixelsで配列を書き込んで
Imageに表示
白黒判定は
if (thresholdMap[y % yy][x % xx] <= (double)pixels[p] / 255)
ここ
y % yyの%は割り算の余りを求めている
yyは行列の縦の数なので2で固定
yが0のときは0%2=0、yが1のときは1%2=1、yが2のときは2%2=0とかになる
x%xxは横で同じことをしている
ディザパターンは2x2の
0.2 0.6
0.8 0.4
こうなので縦の行列数2、横の行列数2
if (thresholdMap[y % yy][x % xx] <= 184 / 255)
y%yy = 3%2 = 1
x%xx = 4%2 = 0
if (thresholdMap[1][0] <= 184 / 255)
行列は0から数えるので、縦1、横0は0.8
if (0.8 <= 184 / 255)
輝度値184を正規化して、184/255=0.72
if (0.8 <= 0.72)
else { pixels[p] = 0; }
ピクセル位置(3, 4)の輝度値184は黒判定(0)になる
左:2x2ディザ、右:4x4ディザ
どちらもそれぞれの良さがある
変則的な2x2ディザパターン
縦縞が出るようになるみたい
おもしろい
エクセル方眼紙も最高なんだよなあ
画像を右クリックして新しいタブで開くで、もとの大きさで表示すると
きれいなパターンになっているのがわかる
2018/06/02追記
変換した画像を保存できるようにしてみた
ダウンロード
追記ここまで
参照したところ
ディザパターンを多段配列にして使う方法はここから
こういう方法を思いつく人はすごいと思う
英語だけど色んなパターンが紹介されている
ここの説明がわかりやすかった
関連記事
前回は2日前
次回は5日後
1ヶ月後