ディザ、ディザリング
輝度が0から255までのグラデーションのグレースケールを
白黒2値化すると中間で白と黒に分かれるので
元の画像とはかなり違うものになる
ディザリングを使うと
同じ白黒2値でも元の画像に近くなる
なにより見た目がかっこいい!!
それっぽく見える
ディザパターン
横12ピクセル、縦2ピクセルの画像、数値は輝度
これをパターンに当てはめて2値化すると
こうなる
2x2マスのディザパターンを使うから
画像も2x2の4マスに分けて考える
輝度93は51~102の間なのでディザパターンbになる
上の93はマスの左上、パターンbの左上は白なので白判定
下の93はマスの左下、パターンbの左下は黒なので黒判定
輝度116はパターンc、当てはめると
上の116は黒、下の116は白
結果、このマスは
こうなればいい
2x2に当てはまらない画像のとき
5x5とか奇数のピクセルの時
左上から右へ2x2で考える
右下の輝度値195なら
195はパターンdでdの左上は白なので白判定になる
正規化
輝度値は0~255で指定するけどしきい値にするには計算しにくいので0~1に変換して計算する
しきい値を4つ指定すると全体を5分割できることになるから
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>
/// MainWindow.xaml の相互作用ロジック
/// </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();
}
//正規表現の基本 - .NET Tips(VB.NET, C#...)
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;
}
//画像ファイルドロップ時
//グレースケールに変換してBitmapSource取得
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 = GetBitmapSourceWithCangePixelFormat2(filePath[0], PixelFormats.Pbgra32, 96, 96);
//OriginBitmap = GetBitmapSourceWithCangePixelFormat2(filePath[0], PixelFormats.Bgr24, 96, 96);
OriginBitmap = GetBitmapSourceWithCangePixelFormat2(filePath[0], PixelFormats.Gray8, 96, 96);
//OriginBitmap = GetBitmapSourceWithCangePixelFormat2(filePath[0], PixelFormats.Rgb24, 96, 96);
if (OriginBitmap == null)
{
MessageBox.Show("not Image");
}
else
{
MyImage.Source = OriginBitmap;
}
}
//ディザなし、しきい値指定で白黒2値化
private void MatrixThreshold()
{
double[][] thresholdMap = new double[][]
{
new double[] { NumericScrollBar.Value / 255 }
};
DitheringGrayScale(thresholdMap);
}
//2x2ディザ
private void Matrix1_Bayer2x2()
{
double[][] thresholdMap = new double[][]
{
new double[] { 1f / 5f, 3f / 5f },
new double[] { 4f / 5f, 2f / 5f }
};
DitheringGrayScale(thresholdMap);
}
//4x4ディザ
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);
}
//2x2ディザの変則
private void Matrix3_Bayer2x2()
{
double[][] thresholdMap = new double[][]
{
new double[] { 1f / 5f, 3f / 5f },
new double[] { 2f / 5f, 4f / 5f }
};
DitheringGrayScale(thresholdMap);
}
//ディザパターン(行列)を使って白黒2値化
//8bppGrayScaleの画像に対応
private void DitheringGrayScale(double[][] thresholdMap)
{
var wb = new WriteableBitmap(OriginBitmap);
int h = wb.PixelHeight;
int w = wb.PixelWidth;
//strideは1ピクセル行のbyte数
//8bppのGrayScale画像は1ピクセル1byteだからstride = 1行のピクセル数になる
int stride = w;// wb.BackBufferStride;
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)//255
{ pixels[p] = 255; }
else { pixels[p] = 0; }
}
}
wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
MyImage.Source = wb;
}
/// <summary>
/// ファイルパスとPixelFormatを指定してBitmapSourceを取得、dpiの変更は任意
/// </summary>
/// <param name="filePath">画像ファイルのフルパス</param>
/// <param name="pixelFormat">PixelFormatsの中からどれかを指定</param>
/// <param name="dpiX">無指定なら画像ファイルで指定されているdpiになる</param>
/// <param name="dpiY">無指定なら画像ファイルで指定されているdpiになる</param>
/// <returns></returns>
private BitmapSource GetBitmapSourceWithCangePixelFormat2(
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);
//dpi指定がなければ元の画像と同じdpiにする
if (dpiX == 0) { dpiX = bf.DpiX; }
if (dpiY == 0) { dpiY = bf.DpiY; }
//dpiを指定してBitmapSource作成
source = BitmapSource.Create(
w, h, dpiX, dpiY,
convertedBitmap.Format,
convertedBitmap.Palette, pixels, stride);
};
}
catch (Exception)
{
}
return source;
}
}
}
画像ファイルのドロップで画像を開く
カラーの画像はグレースケールに変換したものが表示される
Testボタン2x2のディザパターン
Test24x4ディザパターン
Test3変則的な2x2ディザパターン
これが
このディザパターンの行列を作っているところ
って今見たら0.8と0.6の指定が逆になっている
けど対角線上で入れ替わっているだけで偏っていないから問題ない
ディザパターンの行列を使って白黒2値化したら
WriteableBitmapのWritePixelsで配列を書き込んで
Imageに表示
白黒判定は
if (thresholdMap[y % yy][x % xx] <= (double)pixels[p] / 255)
ここ
pは今のピクセル位置を配列の位置に変化したもの
y % yyの%は商の余りを求めている
yは縦のピクセル位置
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
ピクセル位置(3, 4)の輝度値184のときなら
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)
これはfalse、しきい値以下なので
else { pixels[p] = 0; }
ピクセル位置(3, 4)の輝度値184は黒判定(0)になる
左:元画像、右:ディザなし
左:2x2ディザ、右:4x4ディザ
どちらもそれぞれの良さがある
変則的な2x2ディザパターン
同じ2x2でも縦のシマシマが目立つ
通常は上下左右均等的だけど変則の方は
左右でしきい値が分かれる感じなので
縦縞が出るようになるみたい
おもしろい
参照したところ
2値化して、1bppの白黒画像を作成する - .NET Tips (VB.NET,C#...)
https://dobon.net/vb/dotnet/graphics/1bpp.html
ディザパターンを多段配列にして使う方法はここから
こういう方法を思いつく人はすごいと思う
Libcaca study - 2. Halftoning
http://caca.zoy.org/study/part2.html
英語だけど色んなパターンが紹介されている
ディザリング(パターン・ディザ): koujinz blog
http://koujinz.cocolog-nifty.com/blog/2009/04/post-ebdd.html
ここの説明がわかりやすかった
関連記事
前回
カラー画像を1bpp(1bit)白黒画像に変換して保存するアプリ作ってみた、しきい値は手動設定 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15335812.html