今回のアプリ
ダウンロード先
ここの20180302_.zip
前回の
単純減色に誤差拡散でディザリング
これと
これの組み合わせ
いつものこの画像を変換
RGB各3階調で全27色に減色
ゴワゴワ感
きれいにできている、元画像に近い
この前のパターンを使ったディザリングと比較
RGB各3階調で全27色に減色
27色と言っても元画像に赤や緑がないから
使われているのは9色くらいだと思う
RGB各階調数ごとの変化
右隣誤差拡散はあんまりきれいじゃないねえ
フロイドスタインバーグ式誤差拡散はどれもきれい
グラデーション画像の場合
フロイドスタインバーグ式は中央の境目がはっきり
5階調
模様と横線の境目
8階調、512色
16階調、4096色
グラデーション画像だと右隣誤差拡散はありだなあ
模様が逆にかっこいい
RGB(255,244,240)から黒(0,0,0)のグラデーション
RGB各色で誤差の蓄積で
しきい値を超える場所が違うからだろうねえ
一方向のグラデーションだとわかりやすい
右隣誤差拡散
1階調あたりの値は255/(階調数-1)=255/(4-1)=85
なので順番に
0, 85, 170, 255の4段階
すべての
ピクセルの色の値をこのどれかに変換することになる
1階調当たりの
閾値は255/階調数=255/4=63.75
なので順番に
0, 63.75, 127.5, 191.25, 255
0と255は使わないので実際は中の3つ
元の色が63.75未満なら0に変換
元の色が127.5未満なら85に変換
元の色が191.25未満なら170に変換
元の色が255未満なら255に変換
ってしたい
あとは特別に
元の色が0以下なら0に変換
元の色が255以上なら255に変換
変換前と変換後の差を誤差として、これを右隣の
ピクセルの色の値に足(拡散)していく
元の色が0以下なら0に変換
元の色が255以上なら255に変換
しているところ、oldValueが元の色の値、、newValueが変換後の値、gosaが誤差記録用
(元の色の値/1階調当たりの
閾値)の小数点切り捨ての値(倍率rate)を取得
元の色の値が150だったら150/63.75≒2.35=2
あとはこれに1階調当たりの値を掛けたものが変換後の値になる
130行目
85*2=170
131行目
誤差は150を170にしたので150-170=-20
128行目は元の色の値/1階調当たりの
閾値がぴったり割り切れた時に
得られた値に-1している、これは
閾値未満で分けるため
変換と誤差拡散
133行目、新しい値が決まったので元の色の値をこれに変換
134行目は右下(最後の)
ピクセルの右隣はないのでそれを越えないように
フロイドスタインバーグ式誤差拡散
誤差拡散法も1/1がx/16になって左下、真下、右下が増えただけ
このへんは以前の2階調限定のときと全く同じかな
なので他の誤差拡散法も同じようにできそう
処理速度
2048x1536の画像を変換
1秒 右隣誤差拡散
2秒 フロイドスタインバーグ式
階調数は関係ないみたい
一瞬で終わるパターンディザに比べると時間がかかるけど
思っていたよりは速い
画像右クリックから新しいタブで開くで見ると、きれいにディザリングされているのがわかる
デザイン画面
<Window x:Class="_20180302_単純減色と誤差拡散ディザ.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:_20180302_単純減色と誤差拡散ディザ"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Image Name="MyImage" UseLayoutRounding="True" Stretch="None"/>
</ScrollViewer>
<StackPanel Grid.Column="1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="RGB各色数" VerticalAlignment="Center" Margin="4,0" FontSize="18"/>
<TextBox Name="NumericTextBox" VerticalContentAlignment="Center" HorizontalContentAlignment="Right"
Text="{Binding ElementName=NumericScrollBar, Path=Value, UpdateSourceTrigger=PropertyChanged}"
Width="40" FontSize="18"/>
<ScrollBar Name="NumericScrollBar" Value="3" Minimum="1" Maximum="256" SmallChange="1" LargeChange="1"
RenderTransformOrigin="0.5,0.5">
<ScrollBar.RenderTransform>
<RotateTransform Angle="180"/>
</ScrollBar.RenderTransform>
</ScrollBar>
<TextBlock Name="TextBlockColorCount" Text="colorCount"/>
</StackPanel>
<Button Name="ButtonConvertErrorToRight" Content="右隣へ誤差拡散"/>
<Button Name="ButtonConvertFloydSteinberg" Content="FloydSteinberg式誤差拡散"/>
<Button Name="ButtonConvert" Content="ディザ無し"/>
<Button Name="ButtonOrigin" Content="元の画像"/>
</StackPanel>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
namespace _20180302_単純減色と誤差拡散ディザ
{
<summary>
</summary>
public partial class MainWindow : Window
{
BitmapSource OriginBitmap;
string ImageFileFullPath;
public MainWindow()
{
InitializeComponent();
this.Title = this.ToString();
this.AllowDrop = true;
this.Drop += MainWindow_Drop;
ButtonConvert.Click += ButtonConvert_Click;
ButtonConvertErrorToRight.Click += ButtonConvertErrorToRight_Click;
ButtonConvertFloydSteinberg.Click += ButtonConvertFloydSteinberg_Click;
ButtonOrigin.Click += ButtonOrigin_Click;
NumericScrollBar.ValueChanged += NumericScrollBar_ValueChanged;
NumericScrollBar.MouseWheel += NumericScrollBar_MouseWheel;
NumericTextBox.MouseWheel += NumericTextBox_MouseWheel;
NumericTextBox.GotFocus += NumericTextBox_GotFocus;
NumericTextBox.TextChanged += NumericTextBox_TextChanged;
}
private void ButtonConvertFloydSteinberg_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
MyImage.Source= FloydSteinberg(OriginBitmap,(int)NumericScrollBar.Value);
}
<summary>
</summary>
<param name="source"></param>
<param name="division"></param>
private BitmapSource ErrorDiffusionToRight(BitmapSource source, int division)
{
float frequencyThreshold = 255f / division;
float frequencyValue = 255f / (division - 1f);
var wb = new WriteableBitmap(source);
int h = wb.PixelHeight;
int w = wb.PixelWidth;
int stride = wb.BackBufferStride;
var pixels = new byte[h * stride];
wb.CopyPixels(pixels, stride, 0);
float[] iPixels = new float[pixels.Length];
for (int i = 0; i < iPixels.Length; ++i)
{
iPixels[i] = pixels[i];
}
long p = 0;
int newValue;
float gosa = 0, oldValue = 0, rate = 0;
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
p = y * stride + (x * 4);
for (int i = 0; i < 3; ++i)
{
oldValue = iPixels[p + i];
if (oldValue <= 0)
{
gosa = oldValue;
newValue = 0;
}
else if (oldValue >= 255)
{
gosa = oldValue - 255f;
newValue = 255;
}
else
{
rate = (int)Math.Floor(oldValue / frequencyThreshold);
if (oldValue % frequencyThreshold == 0) { rate--; }
newValue = (int)(frequencyValue * rate);
gosa = oldValue - newValue;
}
iPixels[p + i] = newValue;
if (p + i + 4 < iPixels.Length)
{
iPixels[p + i + 4] += gosa;
}
}
}
}
for (int i = 0; i < pixels.Length; ++i)
{
pixels[i] = (byte)iPixels[i];
}
wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
return wb;
}
<summary>
</summary>
<param name="source"></param>
<param name="division"></param>
<returns></returns>
private BitmapSource FloydSteinberg(BitmapSource source, int division)
{
float frequencyThreshold = 255f / division;
float frequencyValue = 255f / (division - 1f);
var wb = new WriteableBitmap(source);
int h = wb.PixelHeight;
int w = wb.PixelWidth;
int stride = wb.BackBufferStride;
var pixels = new byte[h * stride];
wb.CopyPixels(pixels, stride, 0);
float[] iPixels = new float[pixels.Length];
for (int i = 0; i < iPixels.Length; ++i)
{
iPixels[i] = pixels[i];
}
long p = 0;
float gosa = 0, oldValue = 0, newValue = 0, rate = 0;
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
p = y * stride + (x * 4);
for (int i = 0; i < 3; ++i)
{
oldValue = iPixels[p + i];
if (oldValue <= 0)
{
gosa = oldValue;
newValue = 0;
}
else if (oldValue >= 255)
{
gosa = oldValue - 255f;
newValue = 255;
}
else
{
rate = (int)Math.Floor(oldValue / frequencyThreshold);
if (oldValue % frequencyThreshold == 0) { rate--; }
newValue = (frequencyValue * rate);
gosa = oldValue - newValue;
}
iPixels[p + i] = newValue;
if (p + i + 4 < iPixels.Length)
{
iPixels[p + i + 4] += (gosa / 16f) * 7f;
}
if (y < h - 1)
{
if (x != 0)
{
iPixels[p + i + stride - 4] += (gosa / 16f) * 3f;
}
iPixels[p + i + stride] += (gosa / 16f) * 5f;
if (x < w - 1)
{
iPixels[p + i + stride + 4] += (gosa / 16f) * 1f;
}
}
}
}
}
for (int i = 0; i < pixels.Length; ++i)
{
pixels[i] = (byte)Math.Round(iPixels[i], MidpointRounding.AwayFromZero);
}
wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
return wb;
}
<summary>
</summary>
<param name="division"></param>
<returns></returns>
private byte[] GetConverterArray(int division)
{
float frequency = 256f / division;
float[] range = new float[division + 1];
for (int i = 0; i < range.Length; ++i)
{
range[i] = i * frequency;
}
frequency = 255f / (division - 1);
byte[] color = new byte[division];
for (int i = 0; i < color.Length; ++i)
{
color[i] = (byte)(i * frequency);
}
byte[] converter = new byte[256];
int j = 0;
for (int i = 0; i < 256; ++i)
{
if (i >= range[j + 1])
{
j++;
}
converter[i] = color[j];
}
return converter;
}
private void GensyokuNumeric2Table()
{
int division = (int)NumericScrollBar.Value;
byte[] converter = GetConverterArray(division);
var wb = new WriteableBitmap(OriginBitmap);
int h = wb.PixelHeight;
int w = wb.PixelWidth;
int stride = wb.BackBufferStride;
byte[] pixles = new byte[h * stride];
wb.CopyPixels(pixles, stride, 0);
long p = 0;
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
p = y * stride + (x * 4);
pixles[p + 2] = converter[pixles[p + 2]];
pixles[p + 1] = converter[pixles[p + 1]];
pixles[p + 0] = converter[pixles[p + 0]];
}
}
wb.WritePixels(new Int32Rect(0, 0, w, h), pixles, stride, 0);
MyImage.Source = wb;
TextBlockColorCount.Text = Math.Pow(division, 3).ToString();
}
#region イベント
private void ButtonConvertErrorToRight_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
MyImage.Source = ErrorDiffusionToRight(OriginBitmap, (int)NumericScrollBar.Value);
}
private void NumericTextBox_GotFocus(object sender, RoutedEventArgs e)
{
TextBox box = (TextBox)sender;
this.Dispatcher.InvokeAsync(() => { Task.Delay(10); box.SelectAll(); });
}
private void NumericScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
TextBlockColorCount.Text = Math.Pow(NumericScrollBar.Value, 3).ToString();
}
private void ButtonConvert_Click(object sender, RoutedEventArgs e)
{
if (OriginBitmap == null) { return; }
GensyokuNumeric2Table();
}
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 NumericTextBox_MouseWheel(object sender, MouseWheelEventArgs e)
{
NumericScrollBar.Value = (e.Delta > 0) ? NumericScrollBar.Value + 1 : NumericScrollBar.Value - 1;
}
private void NumericScrollBar_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta > 0) { NumericScrollBar.Value++; }
else { NumericScrollBar.Value--; }
}
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.Pbgra32, 96, 96);
if (OriginBitmap == null)
{
MessageBox.Show("not Image");
}
else
{
MyImage.Source = OriginBitmap;
ImageFileFullPath = filePath[0];
}
}
#endregion
<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;
}
}
}
関連記事
2018/02/22
誤差拡散法を使ってディザリング、右隣だけへの誤差拡散、グレースケール画像だけ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
2018/02/23
FloydSteinberg他いくつかの誤差拡散を試してみた、白黒2値をディザリング ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
2018/02/26
単純減色(ポスタライズ?)試してみた、WPFとC# ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ