午後わてんのブログ

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

画像の色相をバブルチャート風に表示するアプリ

f:id:gogowaten:20191214111752p:plain

シェフの気まぐれ極座標バブルチャート風画像の色相分布図
できた
なまえがわからん
値に応じて●の大きさと位置を変えているからバブルチャートはあっていると思う
 
前回は扇形だった

f:id:gogowaten:20191214111801p:plain

値の大きさを円の中心からの距離(半経)にして扇形のグラフにしていた
円の面積はパイ*半径^2、扇形も多分同じなせいか
値が2倍になると面積が4倍に、値が10倍なら面積は100倍とかになって
値と見た目が極端な差になっていた
今回の●は値の大きさを面積に合わせるようにしてみた
 
同じ値を表示した場合
イメージ 3
扇形だと殆ど目立たないものが値ぶん表示されるようになった
扇形も面積で表示すればいいのかも
 

f:id:gogowaten:20191214111814p:plain

色相12分割
 

f:id:gogowaten:20191214111823p:plain

360分割
 
●の大きさを変更
ここまでは
一番大きな●を色相円の0.1倍で表示
一番小さな●を色相円の0.0001倍で表示
これを一桁増やして小さくしてみると
 

f:id:gogowaten:20191214111832p:plain

これもいいねえ
中心が空きすぎている気がするので
もう少し中心から表示するように調整してみたら
 

f:id:gogowaten:20191214111847p:plain

かっこよさだとさっきのほうがいい気がする
 

f:id:gogowaten:20191214111901p:plain

黒ビニールが空の青を反射してるみたいで青が多い
白や黒の無彩色もカウントして表示するのもいいかなあ
 

f:id:gogowaten:20191214111909p:plain

黄色のなかでもある黄色が突出している
何色か気になるから●にマウスオーバーで表示できればいいかも
 

f:id:gogowaten:20191214111920p:plain

コードのスクリーンショット画像
ピンクと緑を中心に左右対称な感じ
 
 
白い皿にスイートバジルの葉っぱの写真

f:id:gogowaten:20191214111931p:plain

紫が多いらしいけど見当たらない
 
偽サーモグラ
イメージ 14
白い皿の左部分が紫の様子
 
イメージ 15
RGB(254,252,255)は、見た目ほぼ白なんだけど色相は280
こういうのはしきい値を設定して白に判定したら面白いかも
 

f:id:gogowaten:20191214111947p:plain

f:id:gogowaten:20191214111956p:plain

f:id:gogowaten:20191214112007p:plain

f:id:gogowaten:20191214112018p:plain

おもしろい
 
 
デザイン画面、MainWindow.xaml

f:id:gogowaten:20191214112028p:plain

前回の扇形のときと同じ
 
MainWindow.xaml.cs
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using MyHSV;//hsv.dllを参照に追加する必要がある


namespace _20190408_色相円の_グラフ
{
    public partial class MainWindow : Window
    {

        const double MyRadius = 200.0;//色相画像半径
        Point MyCenter = new Point(MyRadius, MyRadius);//色相画像中心座標
        byte[] MyPixels;//画像のPixelの色情報、BitmapSourceのCopyPixelsより
        double[] MyHuesList;//全ピクセルの色相の配列
        int DivideCount = 120;//色相分割数
        public MainWindow()
        {
            InitializeComponent();

            this.AllowDrop = true;
            Drop += MainWindow_Drop;

            MyGrid.Children.Add(MakeAuxLine(MyCenter));
            MyHueImage.Source = MakeHueBitmap((int)(MyRadius * 2));
            MyHueImage.Clip = new RectangleGeometry(new Rect(0, 0, 0, 0));


        }
       #region イベント
        private void RadioButton_Click(object sender, RoutedEventArgs e)
        {
            if (MyPixels == null) { return; }
            RadioButton rb = sender as RadioButton;
            int divCount = int.Parse((string)rb.Content);
            DivideCount = divCount;
            MyHueImage.Clip = MakeClipEllipse(HuePixelCount(MyHuesList, divCount));
        }
        private void MainWindow_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) { return; }
            string[] filePath = (string[])e.Data.GetData(DataFormats.FileDrop);
            (byte[] pixels, BitmapSource bitmap) = MakeBitmapSourceAndByteArray(filePath[0], PixelFormats.Bgra32, 96, 96);

            if (bitmap == null)
            {
                MessageBox.Show("画像ファイルじゃないみたい");
            }
            else
            {
                MyPixels = pixels;
                MyHuesList = GetHueList(pixels);
                var neko = HuePixelCount(MyHuesList, DivideCount);
                MyHueImage.Clip = MakeClipEllipse(neko);
                MyImage.Source = bitmap;
            }
        }
       #endregion

        /// <summary>
        /// EllipseGeometryのクリップ作成
        /// </summary>
        /// <param name="hues">色相範囲ごとのピクセル数カウントした配列</param>
        /// <returns></returns>
        private Geometry MakeClipEllipse(int[] hues)
        {
            double max = hues.Max();
            //無彩色画像はmaxが0になっているはず、なので0(クリップなし)を返して終わり
            if (max == 0) { return new RectangleGeometry(new Rect(0, 0, 0, 0)); }

            Point center = new Point(MyRadius, MyRadius);
            double divDeg = 360.0 / hues.Length;//  1分割あたりの角度
           
            var clip = new PathGeometry();
            clip.FillRule = FillRule.Nonzero;
            //●の位置、中心から0.5~0.75の位置に配置
            double minDistance = MyRadius * 0.5;
            double diffDistance = MyRadius * 0.75 - minDistance;
            //●の面積、色相円の0.01~0.00001倍
            double maxArea = (Math.PI * MyRadius * MyRadius) * 0.01;
            double minArea = (Math.PI * MyRadius * MyRadius) * 0.00001;
            double diffArea = maxArea - minArea;

            //色相カウント数から●作成
            //配列のIndexが色相
            for (int i = 0; i < hues.Length; i++)
            {
                //面積から半径を求める
                //面積=   パイ*半径^2
                //パイ*半径^2=  面積
                //半径^2= 面積/パイ
                //半径=   √(面積/パイ)

                //●の面積と、その半径
                double area = minArea + (diffArea * hues[i] / max);
                double radius = Math.Sqrt(area / Math.PI);
                //色相円中心からの距離、値が大きいほど遠くへ
                var distance = minDistance + (diffDistance * hues[i] / max);
                var degrees = i * divDeg;//●表示の角度
                Point clipCenter = MakePoint(degrees, center, distance);
                clip.AddGeometry(new EllipseGeometry(clipCenter, radius, radius));
            }
            return clip;
        }

        //補助線表示用のPath作成
        private Path MakeAuxLine(Point center)
        {

            var pg = new PathGeometry();
            pg.AddGeometry(new EllipseGeometry(center, center.X, center.Y));
            pg.AddGeometry(new EllipseGeometry(center, center.X * 3.0 / 4.0, center.Y * 3.0 / 4.0));
            pg.AddGeometry(new EllipseGeometry(center, center.X / 2.0, center.Y / 2.0));
            pg.AddGeometry(new EllipseGeometry(center, center.X / 4.0, center.Y / 4.0));
            var p = new Path();
            p.Stroke = Brushes.LightGray;
            p.StrokeThickness = 1.0;
            p.Opacity = 0.4;
            p.Data = pg;

            return p;
        }


       #region PathGeometry




        /// <summary>
        /// 距離と角度からその座標を返す
        /// </summary>
        /// <param name="degrees">360以上は359.99になる</param>
        /// <param name="center">中心点</param>
        /// <param name="distance">中心点からの距離</param>
        /// <returns></returns>
        private Point MakePoint(double degrees, Point center, double distance)
        {
            if (degrees >= 360) { degrees = 359.99; }
            var rad = Radian(degrees);
            var cos = Math.Cos(rad);
            var sin = Math.Sin(rad);
            var x = center.X + cos * distance;
            var y = center.Y + sin * distance;
            return new Point(x, y);
        }
        private double Radian(double degree)
        {
            return Math.PI / 180.0 * degree;
        }

       #endregion
       #region 色相環
        /// <summary>
        /// pixelsFormats.Rgb24の色相環作成用のBitmap作成
        /// 右が赤、時計回り
        /// </summary>
        /// <param name="size"></param>        
        /// <returns></returns>
        private BitmapSource MakeHueBitmap(int size)
        {
            var wb = new WriteableBitmap(size, size, 96, 96, PixelFormats.Rgb24, null);
            //色情報用のバイト配列作成
            int stride = wb.BackBufferStride;//横一列のバイト数、24bit = 8byteに横ピクセル数をかけた値
            byte[] pixels = new byte[size * stride * 8];//*8はbyteをbitにするから

            //100x100のとき中心は50,50
            //ピクセル位置と画像の中心との差
            double xDiff = size / 2.0;
            double yDiff = size / 2.0;
            int p = 0;//今のピクセル位置の配列での位置
            for (int y = 0; y < size; y++)//y座標
            {
                for (int x = 0; x < size; x++)//x座標
                {
                    //今の位置の角度を取得、これが色相になる
                    double radian = Math.Atan2(y - yDiff, x - xDiff);
                    double kakudo = Degrees(radian);
                    //色相をColorに変換
                    Color c = HSV.HSV2Color(kakudo, 1.0, 1.0);
                    //バイト配列に色情報を書き込み
                    p = y * stride + x * 3;
                    pixels[p] = c.R;
                    pixels[p + 1] = c.G;
                    pixels[p + 2] = c.B;
                }
            }
            //バイト配列をBitmapに書き込み
            wb.WritePixels(new Int32Rect(0, 0, size, size), pixels, stride, 0);
            return wb;
        }

        //ラジアンを0~360の角度に変換
        public double Degrees(double radian)
        {
            double deg = radian / Math.PI * 180;
            if (deg < 0) deg += 360;
            return deg;
        }
       #endregion

       #region 画像系


        //hueのリスト作成
        private double[] GetHueList(byte[] pixels)
        {
            double[] hueList = new double[pixels.Length / 4];
            int count = 0;
            for (int i = 0; i < pixels.Length; i += 4)
            {
                //ピクセルの色相取得
                hueList[count] = HSV.Color2HSV(pixels[i + 2], pixels[i + 1], pixels[i]).Hue;
                count++;
                //if (hue == 360.0) { continue; }//色相360は無彩色なのでパス
            }
            return hueList;
        }
        /// <summary>
        /// GethueListから作成した色相の配列から色相の分割範囲ごとの数をカウント、
        /// 分割数divCountが4なら、360/4=90度毎、範囲0(315~45)、範囲1(45~135)、範囲2(135~225)、範囲3(225~315)、
        /// 配列の「Index*360/分割数」が色相になる、4分割でIndex3なら、3*360/4=270、Index3の要素は色相270の範囲のもの
        /// <param name="hueList">色相の配列</param>
        /// <param name="divCount">分割数</param>
        /// <returns></returns>
        private int[] HuePixelCount(double[] hueList, int divCount)
        {
            int[] table = new int[divCount];
            double div = 360.0 / divCount;
            double divdiv = div / 2.0;
            for (int i = 0; i < hueList.Length; i++)
            {
                //ピクセルの色相取得
                double hue = hueList[i];
                if (hue == 360.0) { continue; }//色相360は無彩色なのでパス

                //色相の範囲ごとにカウント
                hue = Math.Floor((hue + divdiv) / div);
                hue = (hue >= divCount) ? 0 : hue;
                table[(int)hue]++;
            }
            return table;
        }


        /// <summary>
        /// 画像ファイルからbitmapと、そのbyte配列を取得、ピクセルフォーマットを指定したものに変換
        /// </summary>
        /// <param name="filePath">画像ファイルのフルパス</param>
        /// <param name="pixelFormat">PixelFormatsを指定</param>
        /// <param name="dpiX">96が基本、指定なしなら元画像と同じにする</param>
        /// <param name="dpiY">96が基本、指定なしなら元画像と同じにする</param>
        /// <returns></returns>
        private (byte[] array, BitmapSource source) MakeBitmapSourceAndByteArray(string filePath, PixelFormat pixelFormat, double dpiX = 0, double dpiY = 0)
        {
            byte[] pixels = null;
            BitmapSource source = null;
            try
            {
                using (System.IO.FileStream fs = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.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;
                    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 (pixels, source);
        }
       #endregion

    }
}
 
扇形のときとほとんど同じ
private Geometry MakeClip(int hues)
private Geometry MakeClipEllipse(int hues)
に変わっただけ
HSV.dllを参照に追加する必要があるのも同じ
 
 
イメージ 12
87~90行目で●の位置とサイズの下限、上限指定
 
 
 
 
ギットハブ
アプリ
 
 
関連記事
前回2019/4/4は4日前
画像の色相を円形ヒストグラム、扇形(パイ型)グラフで表示するアプリできた ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15923169.html