午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、普通の写真画像を8色に減色

 
カラー画像を8色に減色
8色は白、黒、赤、緑、青、黄、水色、赤紫(マゼンタ)
イメージ 7
 
ドラッグアンドドロップで開いた画像を8色に減色しているところ
 
 
 

f:id:gogowaten:20191211223115p:plain

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;


namespace _20180126_8色ディザなし
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        BitmapSource OriginBitmap;//初期画像保持用
        double AverageBrightness;//輝度平均保持用
        public MainWindow()
        {
            InitializeComponent();
            this.Title = this.ToString();
            this.AllowDrop = true;
            this.Drop += MainWindow_Drop;

            ButtonOrigin.Click += ButtonOrigin_Click;
            ButtonNotDithering.Click += ButtonNotDithering_Click;
            ButtonThreshold128.Click += ButtonThreshold128_Click;
            ButtonAverageBrightness.Click += ButtonAverageBrightness_Click;
            NumericScrollBar.ValueChanged += NumericScrollBar_ValueChanged;
            NumericTextBox.TextChanged += NumericTextBox_TextChanged;

        }
        private void ButtonAverageBrightness_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            NumericScrollBar.Value = AverageBrightness;
            if (CheckBoxRealTime.IsChecked != true)
            {
                MyImage.Source = Change8Color(AverageBrightness, OriginBitmap);
            }
        }
        private void ButtonThreshold128_Click(object sender, RoutedEventArgs e)
        {
            NumericScrollBar.Value = 128;
            if (OriginBitmap == null) { return; }
            if (CheckBoxRealTime.IsChecked != true)
            {
                MyImage.Source = Change8Color(128, OriginBitmap);
            }
        }
        //ディザなしボタンクリック時
        private void ButtonNotDithering_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MyImage.Source = Change8Color(NumericScrollBar.Value, OriginBitmap);
        }

        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; }
            if (CheckBoxRealTime.IsChecked != true) { return; }
            MyImage.Source = Change8Color(NumericScrollBar.Value, OriginBitmap);
        }
        //元の画像を表示
        private void ButtonOrigin_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MyImage.Source = OriginBitmap;
        }
        //画像ファイルドロップ時
        //PixelFormatをPbgra32に変換して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);

            if (OriginBitmap == null)
            {
                MessageBox.Show("not Image");
            }
            else
            {
                //画像表示
                MyImage.Source = OriginBitmap;
                //Average平均輝度取得
                AverageBrightness = GetAverageBrightness(OriginBitmap);
                ButtonAverageBrightness.Content = "輝度平均:" + AverageBrightness.ToString();
            }
        }



        /// <summary>
        /// 画像を8色に減色する、RGB各色を指定したしきい値で0か255に変換する
        /// PixelFormatがPbgra32の画像だけに対応
        /// </summary>
        /// <param name="source">減色する画像</param>
        /// <param name="threshold">しきい値</param>
        private BitmapSource Change8Color(double threshold, BitmapSource source)
        {
            var wb = new WriteableBitmap(source);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            int stride = wb.BackBufferStride;
            byte[] pixels = new byte[h * stride];
            wb.CopyPixels(pixels, stride, 0);
            long p = 0;//参照ピクセルのPixels[]配列での位置
            int bytePP = wb.Format.BitsPerPixel / 8;// = 32 / 8 = 4
            for (int row = 0; row < h; ++row)
            {
                for (int col = 0; col < w; ++col)
                {
                    p = row * stride + (col * bytePP);
                    //青、緑、赤、透の順で並んでいる
                    for (int i = 0; i < bytePP - 1; ++i)//透明は変更しないので青から赤までの3ループ
                    {
                        if (pixels[p + i] < threshold)//しきい値未満なら0
                        {
                            pixels[p + i] = 0;
                        }
                        else
                        {
                            pixels[p + i] = 255;
                        }
                    }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            return wb;
        }



        //ヒストグラム作成
        private int[] GetHistogram(BitmapSource source)
        {
            int[] OriginHistogram = new int[256];
            int w = source.PixelWidth;
            int h = source.PixelHeight;
            int stride = w;
            byte[] pixels = new byte[h * w];
            source.CopyPixels(pixels, stride, 0);
            for (int i = 0; i < pixels.Length; ++i)
            {
                OriginHistogram[pixels[i]]++;
            }
            return OriginHistogram;
        }
        //平均輝度取得
        private int GetAverageBrightness(BitmapSource bitmap)
        {
            //PixelFormatをGray8に変換
            //var source = new FormatConvertedBitmap(bitmap, PixelFormats.Gray8, null, 0);
            int[] histogram = GetHistogram(new FormatConvertedBitmap(bitmap, PixelFormats.Gray8, null, 0));


            //平均輝度         
            long sum = 0;
            for (int i = 0; i < 256; ++i)
            {
                sum += histogram[i] * i;
            }
            long ave = sum / (bitmap.PixelHeight * bitmap.PixelWidth);
            return (int)ave;
        }

        /// <summary>
        ///  ファイルパスとPixelFormatを指定してBitmapSourceを取得、dpiの変更は任意、Windowsの標準は96dpi
        /// </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;
        }
    }
}
 
 
 
渡されたBitmapSourceを8色に減色したBitmapSourceを返す
しきい値は0から256で指定
対応するBitmapSourceはPixelFormatがPbgra32のものだけ
イメージ 9
渡されたBitmapSourceを元にWriteableBitmapを作成
WriteableBitmapのCopyPixelsで色情報をbyte型配列にコピー
配列のRGB各色をしきい値で0か255に変換
変換した配列をWriteableBitmapに書き込んで返す
 
行っている処理は前回までの白黒2値化とほとんど同じ
違うのはグレースケールの1色からRGBの3色に増えたので
1ピクセルごとの判定が1回だったのが3回に増えているところだけかな

//青、緑、赤、透の順で並んでいる
for (int i = 0; i < bytePP - 1; ++i)//透明は変更しないので青から赤までの3ループ
{
if (pixels[p + i] < threshold)//しきい値未満なら0
{
pixels[p + i] = 0;
}
else
{
pixels[p + i] = 255;
}
}

 
ファイルから読み込んだ画像のPixelFormatはPbgra32に変換してあるので
CopyPixelsで得た色情報の並び順は青、緑、赤、透明
透明は変換しないので先頭の青から赤までの3つをそれぞれ判定する
bytePPはPixelFormatのBitsPerPixelを8で割った数値
今回のPixelFormatはPbgra32、これのBitsPerPixelは32なので32/8=4
4-1=3で3回ループにしている
 
イメージ 2
スイートバジルの葉っぱの画像のある一点(ピクセル)の色は
赤165,緑222、青117だった
これをしきい値128で判定すると
赤165はしきい値128より大きいので赤は255に変換
緑222も128より大きいので緑も255
青117は128より小さいので青は0
なのでこの点(ピクセル)の色
赤165,緑222、青117は
赤255、緑255、青0
に変換することになる、見た目だと黄色になる
イメージ 3
しきい値128で変換すると黄色が多くなるねえ
 
赤R緑G青B
白 255 255 255
黒 0 0 0
赤 255 0 0
緑 0 255 0
青 0 0 255
黄 255 255 0
水色 0 255 255
赤紫 255 0 255
 
 
イメージ 1
 
 
 
イメージ 4
この画像は8色全部使っている
 
 
イメージ 5
これは4色
 
 
イメージ 6
右下は左上の画像のPixelFormatをIndexed2に変換しただけのもので
色数は4、左下の8色より元画像を再現している
これはディザリングの効果もあるけど色の選び方が上手なんだよねえ
ディザリングの方は前回の記事のパターンを使ったものではなくて、誤差拡散って言う方式のどれかだと思うんだけど、色の選び方がさっぱりわからない
 
 
 
 
 
続きは1ヶ月後
単純減色(ポスタライズ?)試してみた、WPFC# ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15388558.html
 
 
過去の関連記事
WPF、画像ファイルを開く方法まとめ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15325331.html

WPFでもNumericUpDownが使いたい、簡単に作りたい ( パソコン ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15313853.html

カラー画像を1bpp(1bit)白黒画像に変換して保存するアプリのコード ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15335830.html