午後わてんのブログ

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

処理速度比較、画像の使用色数を数える、重複なしのリストのHashSetも速いけど配列+ifも速かった

ダウンロード先
画像の使用色数を数える
画像ファイルからのBitmapSourceからCopyPixelsして取得するbyte[]を使って
重複しない色の配列やリストを作って数える方法で処理速度比較してみた
 
条件
BitmapSourceのPixelFormatはPbgra32限定、アルファ値は無視するので256*256*256=16777216色のうち、何色使われているかを数える
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace _20180326_画像の使用色数の処理速度比較
{
    public partial class MainWindow : Window
    {
        BitmapSource OriginBitmap;

        public MainWindow()
        {
            InitializeComponent();
            this.Title = this.ToString();
            this.AllowDrop = true;
            this.Drop += MainWindow_Drop;
        }

        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;


                //TextBlockPixelsCount.Text = $" 画像の使用色数:{cCount}";
                TextBlockImageSize.Text = $"画像サイズ:{OriginBitmap.PixelWidth}x{OriginBitmap.PixelHeight}";
                Keisoku();
            }
        }

        private void Keisoku()
        {
            var list = new Func<BitmapSource, int>[]
            {
                GetColorCountA1,
                GetColorCountA2,
                GetColorCountA3,//エラーにはならないけどカウントがおかしい
                GetColorCountA4,//エラーにはならないけどカウントがおかしい
                GetColorCountB1,
                //GetColorCountB2,//配列の境界外エラー
                GetColorCountC1,
                //GetColorCountC2,//配列の境界外エラー
                GetColorCountD1,
                GetColorCountE1,
            };

            int cCount = 0;
            Stopwatch stopwatch = new Stopwatch();
            for (int i = 0; i < list.Length; ++i)
            {
                stopwatch.Restart();
                cCount = list[i](OriginBitmap);
                stopwatch.Stop();
                Console.WriteLine($"{list[i].Method.Name}:{stopwatch.Elapsed.Seconds}.{stopwatch.Elapsed.Milliseconds.ToString("000")}秒:{cCount}色");
            }
            TextBlockPixelsCount.Text = $" 画像の使用色数:{cCount}";
        }
        private int GetColorCountA1(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);
            int[] iColor = new int[256 * 256 * 256];
            for (int i = 0; i < pixels.Length; i += 4)
            {
                iColor[pixels[i] * 256 * 256 + pixels[i + 1] * 256 + pixels[i + 2]]++;
            }

            int colorCount = 0;
            for (int i = 0; i < iColor.Length; ++i)
            {
                if (iColor[i] != 0)
                {
                    colorCount++;
                }
            }
            return colorCount;
        }
        private int GetColorCountA2(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);
            int[] iColor = new int[256 * 256 * 256];
            Parallel.For(0, pixels.Length / 4, i =>
              {
                  iColor[pixels[i * 4] * 256 * 256 + pixels[i * 4 + 1] * 256 + pixels[i * 4 + 2]]++;
              });

            int colorCount = 0;
            for (int i = 0; i < iColor.Length; ++i)
            {
                if (iColor[i] != 0)
                {
                    colorCount++;
                }
            }
            return colorCount;
        }
        //エラーにはならないけどカウントがおかしい
        private int GetColorCountA3(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);
            int[] iColor = new int[256 * 256 * 256];
            for (int i = 0; i < pixels.Length; i += 4)
            {
                iColor[pixels[i] * 256 * 256 + pixels[i + 1] * 256 + pixels[i + 2]]++;
            }

            int colorCount = 0;
            Parallel.ForEach(iColor, item =>
             {
                 if (item != 0) { colorCount++; }
             });
            return colorCount;
        }
        //エラーにはならないけどカウントがおかしい
        private int GetColorCountA4(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);
            int[] iColor = new int[256 * 256 * 256];
            Parallel.For(0, pixels.Length / 4, i =>
              {
                  iColor[pixels[i * 4] * 256 * 256 + pixels[i * 4 + 1] * 256 + pixels[i * 4 + 2]]++;
              });

            int colorCount = 0;
            Parallel.For(0, iColor.Length, i =>
            {
                if (iColor[i] != 0)
                {
                    colorCount++;
                }
            });
            return colorCount;
        }


        private int GetColorCountB1(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);
            List<int> list = new List<int>();
            for (int i = 0; i < pixels.Length; i += 4)
            {
                list.Add(pixels[i] * 256 * 256 + pixels[i + 1] * 256 + pixels[i + 2]);
            }
            return list.Distinct().ToArray().Length;
        }
        private int GetColorCountB2(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);
            List<int> list = new List<int>();
            Parallel.For(0, pixels.Length / 4, i =>
              {
                  list.Add(pixels[i * 4] * 256 * 256 + pixels[i * 4 + 1] * 256 + pixels[i * 4 + 2]);//配列の境界外エラー
              });

            return list.Distinct().ToArray().Length;
        }
        private int GetColorCountC1(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);
            var list = new HashSet<int>();

            for (int i = 0; i < pixels.Length; i += 4)
            {
                list.Add(pixels[i] * 256 * 256 + pixels[i + 1] * 256 + pixels[i + 2]);
            }
            return list.Count;
        }
        private int GetColorCountC2(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);
            var list = new HashSet<int>();
            Parallel.For(0, pixels.Length / 4, i =>
                {
                    list.Add(pixels[i * 4] * 256 * 256 + pixels[i * 4 + 1] * 256 + pixels[i * 4 + 2]);//配列の境界外エラー
              });

            return list.Count;
        }
        private int GetColorCountD1(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);
            var list = new ConcurrentBag<int>();
            Parallel.For(0, pixels.Length / 4, i =>
            {
                list.Add(pixels[i * 4] * 256 * 256 + pixels[i * 4 + 1] * 256 + pixels[i * 4 + 2]);
            });

            return list.Distinct().ToArray().Length;
        }
        private int GetColorCountE1(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, c = 0;
            int[] iColor = new int[256 * 256 * 256];
            for (int y = 0; y < h; ++y)
            {
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + (x * 4);
                    iColor[c] = pixels[p] * 256 * 256 + pixels[p + 1] * 256 + pixels[p + 2];
                    c++;
                }
            }
            return iColor.Distinct().ToArray().Length;
        }
        private BitmapSource GetBitmapSourceWithChangePixelFormat2(
        string filePath, PixelFormat pixelFormat, double dpiX = 0, double dpiY = 0)
        {
            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;
                    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;
        }
    }
}
方法A1、配列とif

f:id:gogowaten:20191212132422p:plain

16777216要素のint配列を用意して(114行目)
RGBの値を0から16777216までのintに変換して配列にカウント(117行目)
要素が0以外の要素が使用色数(123~126行目)
この方法は
減色変換一覧表を使って処理時間を短縮してみた ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15412874.html
このときと同じ方法
 
方法A2

f:id:gogowaten:20191212132437p:plain

A1の配列にカウントする部分のforをParallel並列処理にしただけ
A1より速くなるはず
 
方法A3

f:id:gogowaten:20191212132531p:plain

A1の0以外の要素数カウントの部分をParallelにしただけなんだけど
これはうまくカウントできなかった
 
方法A4

f:id:gogowaten:20191212132544p:plain

A2とA3の両方のParallel
これもカウント数が合わない
 
方法B1、ListとListのDistinctを使う

f:id:gogowaten:20191212132602p:plain

RGBをintに変換するのは同じ、変換したのをリストに追加していって(222行目)
Distinctメソッド?を使って重複しているのを取り除いて、配列に変換して、要素数を返す(224行目)
 
方法B2、B1のParallelだけどエラーになる

f:id:gogowaten:20191212132638p:plain

Listに追加するところをParallelにしたんだけど、そこで配列の境界外エラーになる
Listはどうやら同時にAddされるとエラーになるみたい
 
 
方法C1、HashSetを使う

f:id:gogowaten:20191212132649p:plain

HashSetはAddで追加する時に追加値がすでにList内にあった場合は追加しないので重複しないリストがそのまま作成される、便利!
 
方法C2、C1をParallelにしたんだけどエラー

f:id:gogowaten:20191212132700p:plain

配列の境界外エラーになる
HashSetもそのままではParallelにはできなかった
 
方法D1、ConcurrentBagとDistinctを使う

f:id:gogowaten:20191212132711p:plain

ConcurrentBagはParallelでも境界がエラーにならないListみたいな感じ
なのでD1はB2のListをConcurrentBagに変更しただけ
 
 
方法E1、intじゃなくてColorに変換してカウント

f:id:gogowaten:20191212132725p:plain

RGBをColorに変換して配列に追加(310行目)
Distinctを使って重複を取り除いた要素数を返す(314行目)
今まではこの方法を使っていた、使用色数だけなら必要ない処理がたくさん
遅い
 
 
A1 配列とif
A2 配列とif、Parallel
A3 配列とif、Parallel
A4 配列とif、Parallel
B1 ListとDistinct
C1 HashSet
D1 ConcurrentとDistinct、Parallel
E1 今までの方法
 
 
計測結果
 
小さい画像
イメージ 11
GetColorCountA1:0.110秒:13544色
GetColorCountA2:0.164秒:13544色
GetColorCountA3:0.184秒:13520色
GetColorCountA4:0.170秒:13516色
GetColorCountB1:0.022秒:13544色
GetColorCountC1:0.005秒:13544色
GetColorCountD1:0.017秒:13544色
GetColorCountE1:0.690秒:13545色
 
この色はParallelを使っているもの
CのHashSetを使ったのが一番速い!
A3,A4は色数が間違っている
A2~A4はA1のParallelなんだけど逆に遅くなっている
E1で色数が1個多いのは0の要素も1個として数えたから?
うーん、A2がA1より遅くなったのは予想外
それにしてもHashSetが速い
 
 
 
小さい画像、色数が多め
イメージ 12
 
GetColorCountA1:0.111秒:65536色
GetColorCountA2:0.126秒:65536色
GetColorCountB1:0.113秒:65536色
GetColorCountC1:0.007秒:65536色
GetColorCountD1:0.048秒:65536色
GetColorCountE1:0.734秒:65536色
 
List+DistinctのB1がかなり遅くなってA1と逆転
Concurrent+DistinctのD1も遅くなったけど、2位
そして最速はHashSetのC1
 
 
 
普通サイズ画像

f:id:gogowaten:20200215180745j:plain

NEC_1533_2018_03_25_午後わてん.jpg
 
イメージ 13
 
GetColorCountA1:0.157秒:218545色
GetColorCountA2:0.172秒:218545色
GetColorCountB1:0.281秒:218545色
GetColorCountC1:0.116秒:218545色
GetColorCountD1:0.222秒:218545色
GetColorCountE1:0.934秒:218545色
 
サイズとともに使用色数も増えたら
Distinct勢のB1とD1、最速のHashSetでかなりの速度低下
配列とifのA1,2はそれほど低下していない
 
 
 
大きめサイズの画像、使用色数も多め

f:id:gogowaten:20200215180551j:plain

2048x1536 2018/03/25 NEC_1533
イメージ 14
 
GetColorCountA1:0.274秒:347172色
GetColorCountA2:0.414秒:347172色
GetColorCountB1:0.625秒:347172色
GetColorCountC1:0.497秒:347172色
GetColorCountD1:1.458秒:347172色
GetColorCountE1:1.253秒:347172色
 
速度低下が軽い配列とifのA1が最速になった
今まで最速だったHashSetのC1は3位
A1のParallel版のA2が遅くなるのはよくわかんないなあ、これでA1より速ければスゴイのになあ…
今までイマイチだったList+DistinctのB1が4位
Concurrent+DistinctのD1は最下位、余計な処理しているはずのE1より遅くなった
 
 
NEC_1496.jpg 2018/03/23
大きめサイズ、使用色数少なめ
イメージ 15
 
GetColorCountA1:0.215秒:136088色
GetColorCountA2:0.461秒:136088色
GetColorCountB1:0.430秒:136088色
GetColorCountC1:0.262秒:136088色
GetColorCountD1:1.238秒:136088色
GetColorCountE1:0.894秒:136089色
使用色数が減ったらHashSetのC1が速くなった、それでもA1より遅い
 
 
 
イメージ 16
使うとしたらA1かC1
A1 画像サイズや色数が増えても速度低下が緩やか
C1 画像サイズや色数が少ないときは最速
実際に使い分けるとしたら色数が事前にわかっていないんだから、画像サイズで判断することになる、横ピクセル数が1024未満ならC1、以上ならA1かな
 
基本の配列とifを使ったのが速かったのが意外だった
HashSetは初めて使ったかも、DistinctやConcurrentも最近知ったばかり。
この前は速くなったからParallelは期待していたんだけどねえ、使い方が間違っているかも
もっといい方法ないかなあ
 
 
イミディエイトウィンドウ

f:id:gogowaten:20191212132826p:plain

Console.WriteLineの結果の表示をイミディエイトウィンドウに変更してみた
出力ウィンドウは他の処理も表示されるから見にくい時があるけどイミディエイトウィンドウならスッキリ
 
変更は

f:id:gogowaten:20191212132843p:plain

メニューのツール→オプション→デバッグ→全般→出力ウィンドウの文字を全てイミディエイトウィンドウにリダイレクトする、にチェック
 
 
Func<>が便利
関数を引数として渡せる

f:id:gogowaten:20191212132853p:plain

Funcを使わないとこんなに長くて同じようなこと書いててめんどくさいけど
 

f:id:gogowaten:20191212132902p:plain

Funcを使えばこんなに短くなる
前にも便利だって言っていたけど便利だからもう一度言った
 
 
参照したところ
配列(またはコレクション)の重複する要素を削除して、一意にする - .NET Tips (VB.NET,C#...)
https://dobon.net/vb/dotnet/programing/arraydistinct.html

VB6のDebug.Printと同様の事を行うには? - .NET Tips (VB.NET,C#...)
https://dobon.net/vb/dotnet/vb6/debugprint.html

C#連想配列(Dictionary)を値 or キーでソート - ヽ|∵|ゝ(Fantom) の 開発blog?
http://fantom1x.blog130.fc2.com/blog-entry-233.html
 
コード(GitHub)
 
関連記事
2018/05/09、ポインタも使ってみたのは43日後
BitmapSourceでの画像処理速度、unsafe、ポインタ、ビルドモード ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15497771.html
 
12日前
減色変換一覧表を使って処理時間を短縮してみた ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15412874.html