ダウンロード先
画像の使用色数を数える
画像ファイルからの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;
TextBlockImageSize.Text = $"画像サイズ:{OriginBitmap.PixelWidth}x{OriginBitmap.PixelHeight}";
Keisoku();
}
}
private void Keisoku()
{
var list = new Func<BitmapSource, int>[]
{
GetColorCountA1,
GetColorCountA2,
GetColorCountA3,
GetColorCountA4,
GetColorCountB1,
GetColorCountC1,
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);
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;
}
}
}
方法A1、配列とif
16777216要素のint配列を用意して(114行目)
RGBの値を0から16777216までのintに変換して配列にカウント(117行目)
要素が0以外の要素が使用色数(123~126行目)
この方法は
方法A2
A1の配列にカウントする部分のforをParallel並列処理にしただけ
A1より速くなるはず
方法A3
A1の0以外の要
素数カウントの部分をParallelにしただけなんだけど
これはうまくカウントできなかった
方法A4
A2とA3の両方のParallel
これもカウント数が合わない
方法B1、ListとListのDistinctを使う
RGBをintに変換するのは同じ、変換したのをリストに追加していって(222行目)
Distinctメソッド?を使って重複しているのを取り除いて、配列に変換して、要
素数を返す(224行目)
方法B2、B1のParallelだけどエラーになる
Listに追加するところをParallelにしたんだけど、そこで配列の境界外エラーになる
Listはどうやら同時にAddされるとエラーになるみたい
方法C1、HashSetを使う
HashSetはAddで追加する時に追加値がすでにList内にあった場合は追加しないので重複しないリストがそのまま作成される、便利!
方法C2、C1をParallelにしたんだけどエラー
HashSetもそのままではParallelにはできなかった
方法D1、ConcurrentBagとDistinctを使う
ConcurrentBagはParallelでも境界がエラーにならないListみたいな感じ
なのでD1はB2のListをConcurrentBagに変更しただけ
方法E1、intじゃなくてColorに変換してカウント
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 今までの方法
計測結果
小さい画像
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が速い
小さい画像、色数が多め
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
普通サイズ画像
NEC_1533_2018_03_25_午後わてん.jpg
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はそれほど低下していない
大きめサイズの画像、使用色数も多め
2048x1536 2018/03/25
NEC_1533
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より遅くなった
大きめサイズ、使用色数少なめ
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より遅い
A1 画像サイズや色数が増えても速度低下が緩やか
C1 画像サイズや色数が少ないときは最速
実際に使い分けるとしたら色数が事前にわかっていないんだから、画像サイズで判断することになる、横ピクセル数が1024未満ならC1、以上ならA1かな
基本の配列とifを使ったのが速かったのが意外だった
HashSetは初めて使ったかも、DistinctやConcurrentも最近知ったばかり。
この前は速くなったからParallelは期待していたんだけどねえ、使い方が間違っているかも
もっといい方法ないかなあ
イミディエイトウィンドウ
Console.WriteLineの結果の表示を
イミディエイトウィンドウに変更してみた
出力ウィンドウは他の処理も表示されるから見にくい時があるけどイミディエイトウィンドウならスッキリ
変更は
メニューのツール→オプション→
デバッグ→全般→出力ウィンドウの文字を全てイミディエイトウィンドウにリダイレクトする、にチェック
Func<>が便利
関数を引数として渡せる
Funcを使わないとこんなに長くて同じようなこと書いててめんどくさいけど
前にも便利だって言っていたけど便利だからもう一度言った
参照したところ
関連記事
2018/05/09、ポインタも使ってみたのは43日後
12日前