午後わてんのブログ

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

C# .NET FrameworkでもCPUの拡張命令SIMDと並列処理で、画像の減色処理速度40倍

2020/03追記

処理速度40倍ってあるけど、実際はそんなに速くなっていない可能性

gogowaten.hatenablog.com

MathクラスのPowを使わないで普通に掛け算すれば、30~80倍くらい速くなる。逆に言うとPowを使うと遅くなる。SIMDでの処理ではMathPowを使っていないから、それもあって40倍も速くなったのかも。

追記ここまで

 

 

実際の画像処理を並列処理とSIMDを使ってどこまで速くなるのか

ufcpp.net

こちらを参考にして書いてみた

 

ランダムな16色に減色するアプリ

f:id:gogowaten:20200120114112p:plain

それぞれの処理時間を表示する

画像ファイルドロップで表示

画像を左クリック押しっぱなしで減色前の画像表示

保存ボタンでファイルに保存

 

アプリダウンロード

20200117gensyoku.zip

 

ギットハブ

github.com

 

f:id:gogowaten:20200120114209j:plain

使った画像はスマカメで撮った3264x2448のjpegファイル、これをランダムな16色に減色

 

結果

f:id:gogowaten:20200120114140p:plain

f:id:gogowaten:20200120114512p:plain

こんなに速くなるとは思わなかった
16/0.4=40倍!

使っているCPUのRyzen 5 2400Gは、物理4コア、論理8コア(スレッド)

電源の設定は

f:id:gogowaten:20200120163304p:plain

AMD Ryzen Balancedの初期設定

f:id:gogowaten:20200120163624p:plain

CPUの電源管理は90~100%になっていた、アクティブってだけある

クロックは色々起動した状態で3GHz前後

 

 

シングルスレッドからマルチスレッドで4倍速

1番2番はほとんど同じ処理で普通のForを使ったシングルスレッド

3~5番は1,2番をParallelクラスとTaskクラスを使ってマルチスレッドにした感じ

比較するとだいたい4倍くらい速くなっている、スレッド数は論理コア数と同じ8スレッドでの処理をしているはずなんだけど8倍速にはならなかった、論理コアは気休め程度ってことかしら

 

SIMDの使用で10倍速

1番にSIMDを使うようにしたのが6番

16/1.6=10倍!

色の距離の計算のところでSIMDを使うVectorクラスを使うようにしただけで10倍も速くなった!SIMDすごい

 

マルチスレッド+SIMDで40倍速

1番をマルチスレッド化+SIMDにしたのが9番

16/0.4=40倍!!!

いままでのマルチで4倍とSIMDで10倍になったんだから、単純に合わせて40倍になる?って本当に40倍速になるのがすごいw使い所がかっちり決まるとこんなに速くなるんだなあ

7,8番もマルチ+SIMDなんだけど、Parallelクラスでマルチスレッド化するときに、うまく書くことができなくて余計な処理が入っているので遅くなっている

 

CPU使用率

f:id:gogowaten:20200120154948p:plain

3番だと100%使い切っているのがわかる、9番は0.4秒で終わるから100%表示にならない、1番はシングルスレッドなので1/8=0.125、12.5%

クロックは1番だと最大で3.4GHz、3番は最大で3.7GHz、9番は一瞬だけ3.4GHz

 

 

.NET FrameworkSIMDを使うには設定が必要(.NET Coreなら設定しなくても使えるみたい)

f:id:gogowaten:20200120122953p:plain

メニューのツールから、NuGetパッケージマネージャー→ソリューションのNuGetパッケージの管理

 

f:id:gogowaten:20200120123117p:plain

参照を選択して

vectorsで検索

 

f:id:gogowaten:20200120123236p:plain

検索結果からSystem.Numerics.Vectorsを選択して右上の

 

f:id:gogowaten:20200120123401p:plain

プロジェクトにチェック入れてインストール

 

f:id:gogowaten:20200120123449p:plain

確認画面出るからおk押して完了

 

f:id:gogowaten:20200120123522p:plain

出力ウィンドウでも確認できる、これで

 

f:id:gogowaten:20200120123602p:plain

参照に2つ追加される

System.Numerics

System.Numerics.Vectors

準備できた

 

f:id:gogowaten:20200120123706p:plain

usingにSystem.Numericsを加えたりしておけば

 

f:id:gogowaten:20200120123746p:plain

vecとタイプしただけでSIMDを使うVectorクラスが候補に出てくる

 

f:id:gogowaten:20200120124053p:plain

色の距離はいろいろな方法があるけど、今回も一番単純なユークリッド距離を使った。単純でも計算は平方根を使うから重いみたいなので、これをSIMDにおまかせ。これだけで10倍速!

 

f:id:gogowaten:20200120124803p:plain

SIMDを使わないMathクラスで普通にユークリッド距離の計算、平方根を求めるSqrtが重いんだろうねえ

 

SIMD、CPU拡張命令

f:id:gogowaten:20200120140304p:plain

システムモニタ系のアプリで見ることができるCPUの詳細で、Featuresのところ
SIMDを使うってのはここの拡張命令を使うってことみたい

RyzenではAVX系に対応したことで画像処理や動画エンコードが速くなったってのを聞いていたから、今回のもそれが大きいのかも

 

 

比較

コードは

小ネタ 並列化 | ++C++; // 未確認飛行 C ブログ

こちらのをコピペ

f:id:gogowaten:20200120135843p:plain

f:id:gogowaten:20200120141246p:plain

singleがシングルスレッド、multiがマルチスレッド

scalarがSIMD未使用、vectorSIMD使用

f:id:gogowaten:20200120143707p:plain

4コア8スレッドのRyzen 5 2400Gのマルチスレッド化による速度アップは2.24倍と1.37倍、PhenomⅡ X3は3コア3スレッドで2.03倍と2.06倍だから、このベンチマークだとRyzenの伸びはいまいちな感じ

SIMDの効果はシングルスレッドなら1.9倍でPhenomよりは伸びたけど、画像処理での伸び率を見た後だと、もっと伸びてもいいような感じがする。どんな関数を使うかによって変わってくるのかしらねえ

Phenomでも今回の画像処理アプリを動かせればいいんだけど、Windows 10のライセンスはRyzenのPCに移しちゃったから動かせない

 

 

今回のアプリのコード

<Window x:Class="_20200117gensyoku.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:_20200117gensyoku"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" UseLayoutRounding="True">
      <Grid x:Name="MyGrid" PreviewMouseLeftButtonDown="MyGrid_PreviewMouseLeftButtonDown" PreviewMouseLeftButtonUp="MyGrid_PreviewMouseLeftButtonUp">
        <Image x:Name="MyImageOrigin" Stretch="None" UseLayoutRounding="True"/>
        <Image x:Name="MyImage" Stretch="None" UseLayoutRounding="True"/>
      </Grid>
    </ScrollViewer>
    <StackPanel Grid.Column="1">
      <StackPanel.Resources>
        <Style TargetType="Button">
          <Setter Property="HorizontalContentAlignment" Value="Left"/>
          <Setter Property="Margin" Value="2,6,2,0"/>
        </Style>
      </StackPanel.Resources>
      <Button Content="保存" Click="ButtonSaveImage_Click"/>
      <Button Content="1:普通、シングルスレッド" x:Name="ButtonExe1" Click="ButtonExe1_Click"/>
      <TextBlock x:Name="tbTime1" Text="time"/>
      <Button Content="2:普通、シングルスレッド" x:Name="ButtonExe2" Click="ButtonExe2_Click"/>
      <TextBlock x:Name="tbTime2" Text="time"/>
      <Button Content="3:Parallelでの並列処理(マルチスレッド)" x:Name="ButtonExe3" Click="ButtonExe3_Click"/>
      <TextBlock x:Name="tbTime3" Text="time"/>
      <Button Content="4:Taskを使った並列処理、1ピクセルごとのRGBAのByte配列のリスト" x:Name="ButtonExe4" Click="ButtonExe4_Click"/>
      <TextBlock x:Name="tbTime4" Text="time"/>
      <Button Content="5:Taskを使った並列処理" x:Name="ButtonExe5" Click="ButtonExe5_Click"/>
      <TextBlock x:Name="tbTime5" Text="time"/>
      <Button Content="6:シングルスレッド+SIMD" x:Name="ButtonExe6" Click="ButtonExe6_Click"/>
      <TextBlock x:Name="tbTime6" Text="time"/>
      <Button Content="7:Parallelでの並列処理+SIMD" x:Name="ButtonExe7" Click="ButtonExe7_Click"/>
      <TextBlock x:Name="tbTime7" Text="time"/>
      <Button Content="8:Taskを使った並列処理+SIMD、1ピクセルごとのRGBAのByte配列のリスト" x:Name="ButtonExe8" Click="ButtonExe8_Click"/>
      <TextBlock x:Name="tbTime8" Text="time"/>
      <Button Content="9:Taskを使った並列処理+SIMD" x:Name="ButtonExe9" Click="ButtonExe9_Click"/>
      <TextBlock x:Name="tbTime9" Text="time"/>

    </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.Diagnostics;//ストップウォッチ用
using System.Numerics;//SIMDのVector用

//参照したところ
//小ネタ 並列化 | ++C++; // 未確認飛行 C ブログ
//https://ufcpp.net/blog/2016/12/tipsparallelism/


namespace _20200117gensyoku
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        BitmapSource OriginBitmapSource;
        byte[] OriginPixels;
        string ImageFileFullPath;

        public MainWindow()
        {
            InitializeComponent();

            this.AllowDrop = true;
            this.Drop += MainWindow_Drop;

            //string path = @"D:\ブログ用\チェック用2\WP_20200111_09_25_36_Pro_2020_01_11_午後わてん.jpg";
            //(OriginPixels, OriginBitmapSource) = MakeBitmapSourceAndPixelData(path, PixelFormats.Bgra32, 96, 96);
            //MyImageOrigin.Source = OriginBitmapSource;


        }

        private void MainWindow_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) { return; }
            string[] filePath = (string[])e.Data.GetData(DataFormats.FileDrop);
            (OriginPixels, OriginBitmapSource) = MakeBitmapSourceAndPixelData(filePath[0], PixelFormats.Bgra32, 96, 96);

            if (OriginBitmapSource == null)
            {
                MessageBox.Show("画像として開くことができなかった");
            }
            else
            {
                MyImageOrigin.Source = OriginBitmapSource;
                MyImage.Source = OriginBitmapSource;
                ImageFileFullPath = filePath[0];
            }
        }

        //白、黒、赤、緑、青、黄色、水色、赤紫の固定8色パレット作成
        private List<byte[]> MakePalette()
        {
            List<byte[]> palette = new List<byte[]> {
                new byte[] { 255, 255, 255 },
                new byte[] { 0, 0, 0 },
                new byte[] { 255, 0, 0 },
                new byte[] { 0, 255, 0 },
                new byte[] { 0, 0, 255 },
                new byte[] { 255, 255, 0 },
                new byte[] { 0, 255, 255 },
                new byte[] { 255, 0, 255 } };
            return palette;
        }
        //ランダム色パレット作成
        private List<byte[]> MakePalette(int count)
        {
            byte[] temp = new byte[count * 3];
            // Random r = new Random((int)DateTime.Now.Ticks);
            Random r = new Random();
            r.NextBytes(temp);
            List<byte[]> palette = new List<byte[]>();
            for (int i = 0; i < temp.Length; i += 3)
            {
                palette.Add(new byte[] { temp[i], temp[i + 1], temp[i + 2] });
            }
            return palette;
        }

        //1:普通、シングルスレッド
        private BitmapSource Zs1減色1(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            for (int i = 0; i < OriginPixels.Length; i += 4)
            {
                //パレットの中から、今のPixelの色に一番近い色を取得
                double min = ColorDistance(pixels[i + 2], pixels[i + 1], pixels[i], palette[0]);
                int pIndex = 0;
                for (int pc = 1; pc < palette.Count; pc++)
                {
                    var distance = ColorDistance(pixels[i + 2], pixels[i + 1], pixels[i], palette[pc]);
                    if (min > distance)
                    {
                        min = distance;
                        pIndex = pc;
                    }
                }
                //一番近い色に置き換える
                zPixels[i + 3] = 255;//A
                zPixels[i + 2] = palette[pIndex][0];//R
                zPixels[i + 1] = palette[pIndex][1];//G
                zPixels[i] = palette[pIndex][2];//B
            }
            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //2:普通、シングルスレッド
        private BitmapSource Zs2減色2(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            byte r, g, b;
            for (int i = 0; i < OriginPixels.Length; i += 4)
            {
                r = pixels[i + 2]; g = pixels[i + 1]; b = pixels[i];//1と違うところ
                double min = ColorDistance(r, g, b, palette[0]);
                int pIndex = 0;
                //パレットから一番近い色
                for (int pc = 1; pc < palette.Count; pc++)
                {
                    var distance = ColorDistance(r, g, b, palette[pc]);
                    if (min > distance)
                    {
                        min = distance;
                        pIndex = pc;
                    }
                }
                zPixels[i + 3] = 255;//A
                zPixels[i + 2] = palette[pIndex][0];//R
                zPixels[i + 1] = palette[pIndex][1];//G
                zPixels[i] = palette[pIndex][2];//B
            }
            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //3:Parallelでの並列処理
        private BitmapSource ZmP減色3(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            List<byte[]> MyList = MakeByteRGBA(pixels);//1ピクセルごとのRGBAのByte配列のリスト
            Parallel.For(0, MyList.Count, n =>
            {
                double min = ColorDistance(MyList[n], palette[0]);
                int pIndex = 0;
                for (int i = 1; i < palette.Count; i++)
                {
                    var distance = ColorDistance(MyList[n], palette[i]);
                    if (min > distance)
                    {
                        min = distance;
                        pIndex = i;
                    }
                }
                var p = n * 4;
                zPixels[p] = palette[pIndex][2];
                zPixels[p + 1] = palette[pIndex][1];
                zPixels[p + 2] = palette[pIndex][0];
                zPixels[p + 3] = 255;
            });
            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //4:Taskを使った並列処理、1ピクセルごとのRGBAのByte配列のリスト
        private BitmapSource ZmT1減色4(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            List<byte[]> MyList = MakeByteRGBA(pixels);//1ピクセルごとのRGBAのByte配列のリスト
            int MyThread = Environment.ProcessorCount;
            int windowSize = MyList.Count / MyThread;//1スレッドに割り当てる量

            Task.WhenAll(Enumerable.Range(0, MyThread)
                .Select(n => Task.Run(() =>
                {
                    for (int np = n * windowSize; np < (n + 1) * windowSize; np++)
                    {
                        double min = ColorDistance(MyList[np], palette[0]);
                        int pIndex = 0;
                        for (int i = 1; i < palette.Count; i++)
                        {
                            var distance = ColorDistance(MyList[np], palette[i]);
                            if (min > distance)
                            {
                                min = distance;
                                pIndex = i;
                            }
                        }
                        var p = np * 4;
                        zPixels[p] = palette[pIndex][2];
                        zPixels[p + 1] = palette[pIndex][1];
                        zPixels[p + 2] = palette[pIndex][0];
                        zPixels[p + 3] = 255;
                    }
                }))).Wait();

            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //5:Taskを使った並列処理
        private BitmapSource ZmT2減色5(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            int MyThread = Environment.ProcessorCount;
            int windowSize = pixels.Length / MyThread;//1スレッドに割り当てる量

            Task.WhenAll(Enumerable.Range(0, MyThread)
                .Select(n => Task.Run(() =>
                {
                    for (int p = n * windowSize; p < (n + 1) * windowSize; p += 4)
                    {
                        var r = pixels[p + 2];
                        var g = pixels[p + 1];
                        var b = pixels[p];
                        double min = ColorDistance(r, g, b, palette[0]);
                        int pIndex = 0;
                        for (int i = 1; i < palette.Count; i++)
                        {
                            var distance = ColorDistance(r, g, b, palette[i]);
                            if (min > distance)
                            {
                                min = distance;
                                pIndex = i;
                            }
                        }

                        zPixels[p] = palette[pIndex][2];
                        zPixels[p + 1] = palette[pIndex][1];
                        zPixels[p + 2] = palette[pIndex][0];
                        zPixels[p + 3] = 255;
                    }
                }))).Wait();

            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //6:シングルスレッド+SIMD
        private BitmapSource Zs1SIMD減色6(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            for (int i = 0; i < OriginPixels.Length; i += 4)
            {
                double min = ColorDistanceV3(pixels[i + 2], pixels[i + 1], pixels[i], palette[0]);
                int pIndex = 0;
                //パレットから一番近い色
                for (int pc = 1; pc < palette.Count; pc++)
                {
                    //色の距離取得でSIMDのVectorを使用
                    var distance = ColorDistanceV3(pixels[i + 2], pixels[i + 1], pixels[i], palette[pc]);
                    if (min > distance)
                    {
                        min = distance;
                        pIndex = pc;
                    }
                }
                //一番近い色に置き換える
                zPixels[i + 3] = 255;//A
                zPixels[i + 2] = palette[pIndex][0];//R
                zPixels[i + 1] = palette[pIndex][1];//G
                zPixels[i] = palette[pIndex][2];//B
            }
            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //7:Parallelでの並列処理+SIMD
        private BitmapSource ZmPSIMD減色7(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            List<byte[]> MyList = MakeByteRGBA(pixels);//1ピクセルごとのRGBAのByte配列のリスト
            Parallel.For(0, MyList.Count, n =>
            {
                double min = ColorDistanceV3(MyList[n], palette[0]);
                int pIndex = 0;
                for (int i = 1; i < palette.Count; i++)
                {
                    var distance = ColorDistanceV3(MyList[n], palette[i]);
                    if (min > distance)
                    {
                        min = distance;
                        pIndex = i;
                    }
                }
                var p = n * 4;
                zPixels[p] = palette[pIndex][2];
                zPixels[p + 1] = palette[pIndex][1];
                zPixels[p + 2] = palette[pIndex][0];
                zPixels[p + 3] = 255;
            });
            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //8:Taskを使った並列処理+SIMD、1ピクセルごとのRGBAのByte配列のリスト
        private BitmapSource ZmT1SIMD減色8(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            List<byte[]> MyList = MakeByteRGBA(pixels);//1ピクセルごとのRGBAのByte配列のリスト
            int MyThread = Environment.ProcessorCount;
            int windowSize = MyList.Count / MyThread;//1スレッドに割り当てる量

            Task.WhenAll(Enumerable.Range(0, MyThread)
                .Select(n => Task.Run(() =>
                {
                    for (int np = n * windowSize; np < (n + 1) * windowSize; np++)
                    {
                        double min = ColorDistanceV3(MyList[np], palette[0]);
                        int pIndex = 0;
                        for (int i = 1; i < palette.Count; i++)
                        {
                            var distance = ColorDistanceV3(MyList[np], palette[i]);
                            if (min > distance)
                            {
                                min = distance;
                                pIndex = i;
                            }
                        }
                        var p = np * 4;
                        zPixels[p] = palette[pIndex][2];
                        zPixels[p + 1] = palette[pIndex][1];
                        zPixels[p + 2] = palette[pIndex][0];
                        zPixels[p + 3] = 255;
                    }
                }))).Wait();

            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }

        //9:Taskを使った並列処理+SIMD
        private BitmapSource ZmT2SIMD減色9(BitmapSource source, byte[] pixels, List<byte[]> palette)
        {
            byte[] zPixels = new byte[pixels.Length];
            int MyThread = Environment.ProcessorCount;
            int windowSize = pixels.Length / MyThread;//1スレッドに割り当てる量

            Task.WhenAll(Enumerable.Range(0, MyThread)
                .Select(n => Task.Run(() =>
                {
                    for (int p = n * windowSize; p < (n + 1) * windowSize; p += 4)
                    {
                        var r = pixels[p + 2];
                        var g = pixels[p + 1];
                        var b = pixels[p];
                        double min = ColorDistanceV3(r, g, b, palette[0]);
                        int pIndex = 0;
                        for (int i = 1; i < palette.Count; i++)
                        {
                            var distance = ColorDistanceV3(r, g, b, palette[i]);
                            if (min > distance)
                            {
                                min = distance;
                                pIndex = i;
                            }
                        }

                        zPixels[p] = palette[pIndex][2];
                        zPixels[p + 1] = palette[pIndex][1];
                        zPixels[p + 2] = palette[pIndex][0];
                        zPixels[p + 3] = 255;
                    }
                }))).Wait();

            return BitmapSource.Create(source.PixelWidth, source.PixelHeight, 96, 96, source.Format, null, zPixels, source.PixelWidth * 4);
        }



        //BGRAの配列をRGBAの4つごとのByteにする、ParallelForでの処理で使う
        private List<byte[]> MakeByteRGBA(byte[] pixels)
        {
            List<byte[]> MyList = new List<byte[]>();
            for (int i = 0; i < pixels.Length; i += 4)
            {
                MyList.Add(new byte[] { pixels[i + 2], pixels[i + 1], pixels[i], pixels[i + 3] });
            }
            return MyList;
        }



        //色の距離、Mathクラスで
        private double ColorDistance(byte r1, byte g1, byte b1, byte[] palette)
        {
            return Math.Sqrt(
                Math.Pow(r1 - palette[0], 2) +
                Math.Pow(g1 - palette[1], 2) +
                Math.Pow(b1 - palette[2], 2));
        }
        private double ColorDistance(byte[] RGBA, byte[] palette)
        {
            return Math.Sqrt(
                Math.Pow(RGBA[0] - palette[0], 2) +
                Math.Pow(RGBA[1] - palette[1], 2) +
                Math.Pow(RGBA[2] - palette[2], 2));
        }
        //色の距離、SIMDを使うVectorクラスで計算
        private float ColorDistanceV3(byte r1, byte g1, byte b1, byte[] palette)
        {
            Vector3 value1 = new Vector3(r1, g1, b1);
            Vector3 value2 = new Vector3(palette[0], palette[1], palette[2]);
            return Vector3.Distance(value1, value2);
        }
        private float ColorDistanceV3(byte[] RGBA, byte[] palette)
        {
            Vector3 value1 = new Vector3(RGBA[0], RGBA[1], RGBA[2]);
            Vector3 value2 = new Vector3(palette[0], palette[1], palette[2]);
            return Vector3.Distance(value1, value2);
        }


        /// <summary>
        /// 画像ファイルからbitmapと、そのbyte配列を返す、ピクセルフォーマットは指定したものに変換
        /// </summary>
        /// <param name="filePath">画像ファイルのフルパス</param>
        /// <param name="pixelFormat">PixelFormatsを指定、null指定ならBgra32で作成する</param>
        /// <param name="dpiX">96が基本、指定なしなら元画像と同じにする</param>
        /// <param name="dpiY">96が基本、指定なしなら元画像と同じにする</param>
        /// <returns></returns>
        private (byte[] pixels, BitmapSource source) MakeBitmapSourceAndPixelData(
            string filePath,
            PixelFormat pixelFormat,
            double dpiX = 0, double dpiY = 0)
        {
            byte[] pixels = null;//PixelData
            BitmapSource source = null;
            if (pixelFormat == null) { pixelFormat = PixelFormats.Bgra32; }
            try
            {
                using (System.IO.FileStream fs = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read))
                {
                    var frame = BitmapFrame.Create(fs);
                    var tempBitmap = new FormatConvertedBitmap(frame, pixelFormat, null, 0);
                    int w = tempBitmap.PixelWidth;
                    int h = tempBitmap.PixelHeight;
                    int stride = (w * pixelFormat.BitsPerPixel + 7) / 8;
                    pixels = new byte[h * stride];
                    tempBitmap.CopyPixels(pixels, stride, 0);
                    //dpi指定がなければ元の画像と同じdpiにする
                    if (dpiX == 0) { dpiX = frame.DpiX; }
                    if (dpiY == 0) { dpiY = frame.DpiY; }
                    //dpiを指定してBitmapSource作成
                    source = BitmapSource.Create(
                        w, h, dpiX, dpiY,
                        tempBitmap.Format,
                        tempBitmap.Palette, pixels, stride);
                };
            }
            catch (Exception)
            {
            }
            return (pixels, source);
        }

        private void MyGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Panel.SetZIndex(MyImageOrigin, 1);
        }

        private void MyGrid_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Panel.SetZIndex(MyImageOrigin, -1);
        }
        private void ButtonSaveImage_Click(object sender, RoutedEventArgs e)
        {
            SaveImage((BitmapSource)MyImage.Source);
        }

        //処理時間計測
        private void MyTime(Func<BitmapSource, byte[], List<byte[]>, BitmapSource> func, TextBlock textBlock)
        {
            if (OriginBitmapSource == null) return;
            var sw = new Stopwatch();
            sw.Start();
            //MyImage.Source = func(OriginBitmapSource, OriginPixels, MakePalette());
            MyImage.Source = func(OriginBitmapSource, OriginPixels, MakePalette(16));
            sw.Stop();
            textBlock.Text = $"{sw.Elapsed.Seconds}秒{sw.Elapsed.Milliseconds.ToString("000")}";
        }
        private void ButtonExe1_Click(object sender, RoutedEventArgs e)
        {
            MyTime(Zs1減色1, tbTime1);
        }


        private void ButtonExe2_Click(object sender, RoutedEventArgs e)
        {
            MyTime(Zs2減色2, tbTime2);
        }

        private void ButtonExe3_Click(object sender, RoutedEventArgs e)
        {
            MyTime(ZmP減色3, tbTime3);
        }

        private void ButtonExe4_Click(object sender, RoutedEventArgs e)
        {
            MyTime(ZmT1減色4, tbTime4);
        }

        private void ButtonExe5_Click(object sender, RoutedEventArgs e)
        {
            MyTime(ZmT2減色5, tbTime5);
        }

        private void ButtonExe6_Click(object sender, RoutedEventArgs e)
        {
            MyTime(Zs1SIMD減色6, tbTime6);
        }

        private void ButtonExe7_Click(object sender, RoutedEventArgs e)
        {
            MyTime(ZmPSIMD減色7, tbTime7);
        }

        private void ButtonExe8_Click(object sender, RoutedEventArgs e)
        {
            MyTime(ZmT1SIMD減色8, tbTime8);
        }

        private void ButtonExe9_Click(object sender, RoutedEventArgs e)
        {
            MyTime(ZmT2SIMD減色9, tbTime9);
        }


        private void SaveImage(BitmapSource source)
        {
            var saveFileDialog = new Microsoft.Win32.SaveFileDialog();
            saveFileDialog.Filter = "*.png|*.png|*.bmp|*.bmp|*.tiff|*.tiff";
            saveFileDialog.AddExtension = true;
            saveFileDialog.FileName = System.IO.Path.GetFileNameWithoutExtension(ImageFileFullPath) + "_";
            saveFileDialog.InitialDirectory = System.IO.Path.GetDirectoryName(ImageFileFullPath);
            if (saveFileDialog.ShowDialog() == true)
            {
                BitmapEncoder encoder = new BmpBitmapEncoder();
                if (saveFileDialog.FilterIndex == 1)
                {
                    encoder = new PngBitmapEncoder();
                }
                else if (saveFileDialog.FilterIndex == 2)
                {
                    encoder = new BmpBitmapEncoder();
                }
                else if (saveFileDialog.FilterIndex == 3)
                {
                    encoder = new TiffBitmapEncoder();
                }
                encoder.Frames.Add(BitmapFrame.Create(source));

                using (var fs = new System.IO.FileStream(saveFileDialog.FileName, System.IO.FileMode.Create, System.IO.FileAccess.Write))
                {
                    encoder.Save(fs);
                }
            }
        }
    }
}

 

 

 

f:id:gogowaten:20200120145721p:plain

16秒かかるこれを

 

f:id:gogowaten:20200120145841p:plain

マルチスレッド化してSIMDを使うように、こう書くと0.4秒ってのは魔法みたい、っていうのもTaskを使ったマルチスレッド処理の部分は、コピペ改変なのでよくわかっていない。処理する配列の要素をスレッド数で割って(344行目)、各スレッドに割り当てる範囲を決めている(349行目のfor)みたいなのはなんとなくわかるとして、Task.WhenAllとTask.Runのあたりが、ラムダ式LINQも相まってわからなさを加速させている。いつかはこういうコードもサラッと書けるようになりたいねえ

2020/01/22追記ここから

Taskを使った並列処理のところは、ピクセル数がCPUスレッド数で割り切れなかった場合は、処理されないピクセルが出る?

ピクセル数10でスレッド数が8の場合、10/8=1…余り2で、最後の2ピクセルは処理されないはずだけど、言わなければバレへんか…

2020/01/22追記ここまで

 

f:id:gogowaten:20200120153316p:plain

いいねえ

 

 

 

関連記事

System.Numerics.Vectorクラス使ってみたその2は1週間後

gogowaten.hatenablog.com

3日前

 

 

2年前

中途半端なできのまま放置されているこの減色アプリを作り直したい