ここから本文です
ベランダ菜園とWindows用アプリ作成とExcel用アプリ(アドイン)作成

書庫全体表示


ディザ、ディザリング

イメージ 1
輝度が0から255までのグラデーションのグレースケールを
白黒2値化すると中間で白と黒に分かれるので
元の画像とはかなり違うものになる


ディザリングを使うと
イメージ 2
同じ白黒2値でも元の画像に近くなる
なにより見た目がかっこいい!!



イメージ 3
灰色を白黒2値で表現するには白と黒の割合が同じになるように並べると
それっぽく見える

ここからエクセル方眼紙
ディザパターン
イメージ 4
輝度によって白と黒の割合を変える、なるべく偏らないように並べる



イメージ 5
横12ピクセル、縦2ピクセルの画像、数値は輝度
これをパターンに当てはめて2値化すると

イメージ 6
こうなる


イメージ 7
2x2マスのディザパターンを使うから
画像も2x2の4マスに分けて考える


イメージ 8
輝度93は51〜102の間なのでディザパターンbになる
上の93はマスの左上、パターンbの左上は白なので白判定
下の93はマスの左下、パターンbの左下は黒なので黒判定
輝度116はパターンc、当てはめると
上の116は黒、下の116は白
結果、このマスは
イメージ 9
こうなればいい


2x2に当てはまらない画像のとき
イメージ 10
5x5とか奇数のピクセルの時

イメージ 11
左上から右へ2x2で考える

イメージ 12
右下の輝度値195なら
195はパターンdでdの左上は白なので白判定になる

イメージ 24
だいたいこんな感じ


正規化
イメージ 13
輝度値は0〜255で指定するけどしきい値にするには計算しにくいので0〜1に変換して計算する
しきい値を4つ指定すると全体を5分割できることになるから
5段階の表現ができる

ここまでエクセル方眼紙

イメージ 14
5段階の表現



イメージ 15





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 = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Pbgra32, 96, 96);
//OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Bgr24, 96, 96);
OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Gray8, 96, 96);
//OriginBitmap = GetBitmapSourceWithChangePixelFormat2(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 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);
//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のディザパターン
Test2 4x4ディザパターン
Test3 変則的な2x2ディザパターン



イメージ 16
これが
イメージ 17
このディザパターンの行列を作っているところ
って今見たら0.8と0.6の指定が逆になっている
けど対角線上で入れ替わっているだけで偏っていないから問題ない


ディザパターンを使って白黒2値化
イメージ 18
WriteableBitmapのCopyPixelsで色(輝度)情報を配列にコピーして
ディザパターンの行列を使って白黒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)になる



イメージ 19
左:元画像、右:ディザなし

イメージ 20
左:2x2ディザ、右:4x4ディザ
どちらもそれぞれの良さがある

変則的な2x2ディザパターン
イメージ 21
同じ2x2でも縦のシマシマが目立つ
イメージ 22
通常は上下左右均等的だけど変則の方は
左右でしきい値が分かれる感じなので
縦縞が出るようになるみたい
おもしろい
エクセル方眼紙も最高なんだよなあ

イメージ 23


2018/06/02追記
変換した画像を保存できるようにしてみた
イメージ 25
選べる保存形式はpng,bmp,tiffの3形式

イメージ 27
ビットの深さは1(1bpp)

イメージ 26


ダウンロード
追記ここまで




参照したところ
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


WPF、画像をディザパターンを使って8色に減色して保存するアプリ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15346592.html



誤差拡散法を使ってディザリング、右隣だけへの誤差拡散、グレースケール画像だけ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15383023.html




本文はここまでですこのページの先頭へ
みんなの更新記事