【.NET】C#でSIMDを使った高速演算

私は、.NET Frameworkを使う事が多いのですが、.NET FrameworkからSIMDを使うにはSystem.Numerics.Vectorsクラスを使ってSIMD演算ができるのですが、C言語で使うSIMDとは全く別物で使っていませんでした。

ところが、.NET Core 3.0から使えるSystem.Runtime.Intrinsics.X86 名前空間は、ほぼC言語のSIMDと似ているので、.NET 5.0 も登場した事だし、少し触ってみました。

 

そもそもSIMDって何?という話は私も説明できる程は理解していませんが、8bitの型(byteなど)や16bitの型(shortなど)を128bitや256bitでまとめて演算する事で、高速に処理する事が可能となります。

まずは、C言語の説明になりますが、ここ↓を見る事をおススメします。

 

C#のSIMDで出来る事は、加算、減算、シフトやビットの入れ替えなどになりますが、ここ↓を参照下さい。

https://docs.microsoft.com/ja-jp/dotnet/api/system.runtime.intrinsics.x86.avx2?view=net-5.0

 

ushort型配列をビットシフトし、byte配列へ代入する例

ushort型(16bit)の配列に格納された10bitの値(0~1023)を2bitシフトして、8bit(byte型)の配列に代入する例を示します。

 

SIMDのプログラムでは、ポインタを使うので、プロジェクトのプロパティで、アンセーフコードの許可にチェックを入れます。

 

まずは、作成したコンソールアプリのサンプルをご覧ください。

using System;
using System.Runtime.Intrinsics.X86;

namespace ConsoleApp1
{
    class Program
    {
        static unsafe void Main(string[] args)
        {
            // データの個数(16の倍数の個数)
            int dataCount = 1024;

            // ushort型(16bit)の配列
            var usData = new ushort[dataCount];
            // byte型(bit)の配列
            var bData = new byte[dataCount];

            // 評価用データの作成
            for (int i = 0; i < dataCount; i++)
            {
                usData[i] = (ushort)i;
            }

            fixed (ushort* pusData = usData)
            fixed (byte* pbData = bData)
            {
                for (int i = 0; i < dataCount; i += 16)
                {
                    // データをポインタから読み込む
                    var vVal1 = Avx2.LoadVector128((short*)pusData + i);
                    var vVal2 = Avx2.LoadVector128((short*)pusData + i + 8);
                    // 2ビット右側へシフト
                    vVal1 = Avx2.ShiftRightLogical(vVal1, 2);
                    vVal2 = Avx2.ShiftRightLogical(vVal2, 2);
                    // パック
                    var vDst = Avx2.PackUnsignedSaturate(vVal1, vVal2);
                    // 結果をbyte型配列に書き込む
                    Avx2.Store(pbData + i, vDst);
                }
            }

            // 結果出力
            for (int i = 0; i < bData.Length; i++)
            {
                Console.Write($"{bData[i],3}, ");
            }
        }
    }
}

今回は、AVX2を使っていますが、AVX2は第4世代Core(Haswell)以降のCPUであれば使用可能なので、今どきのPCであれば、ほとんど使用できると思います。

 

まず、LoadVector128メソッドで、ushort配列(16bit)のポインタを128bitのVetorと呼ばれる高速に読み書きできる領域へ読込しています。
16bitのデータを128bitへ読込しているので、この1回のメソッドで、128 / 16 = 8 の8個分のデータを読込しています。
ちなみに、ushortポインタ(ushort*)をshortポインタ(short*)へキャストしているのは、最後に出てくるPackUnsignedSaturateへ渡す引数にVector128<ushort>が無い(Vector128<short>はある)ため、short*へキャストしています。

var vVal1 = Avx2.LoadVector128((short*)pusData + i);

ShiftRightLogicalメソッドでは、右側へシフトしています。
下記の例は2ビット右側へシフトしています。(4で割るのと同じ)

vVal1 = Avx.ShiftRightLogical(vVal1, 2);

PackUnsignedSaturateメソッドでは、パックと呼ばれる処理になりますが、詳細は最初に示した参考文献を見て頂くのがいいかと思いますが、引数で渡された型の半分の下位ビットをつなぎ合わせた物を返します。

var vDst = Avx2.PackUnsignedSaturate(vVal1, vVal2);

 

今回の例では、最初の引数の8個の16bitの値のそれぞれ、下位8bitの部分を取得し、8個の8bitの値とし、第二引数も同様に8個の8bitの値を取得し、計16個の8bitの値(16 x 8 = 128bit)を返します。

 

StoreメソッドはVectorから配列を示しているポインタへ値を戻します。

Avx2.Store(pbData + i, vDst);

まとめ

現状では、.NETのSIMDの情報が少ないので、C言語のSIMDの情報を頼りにする事になるかと思いますが、今回、Vectorと言っていた部分をレジスタに読み替えれば、ある程度理解できると思います。

また、このVectorへ渡すメモリにアライメントという概念があり、LoadVector128と似たメソッドにLoadAlignedVector128というメソッドがあり、これを使うと処理が少し速くなるのですが、メモリのアドレス(番地)が128で割り切れる必要があり、通常、配列から取得したポインタのアドレスは、このアドレスが128では割り切れない番地になっているため、少し工夫しないと、そのままでは使用する事ができません。

 

 

 .NET 5 C# Windows Forms プログラム へ戻る

 

【.NET 5 C# WinForms】バージョン情報の設定

プログラムのバージョン情報を指定するのには、.NET Frameworkの時は、プロジェクトのプロパティをクリックし、

表示された画面の アプリケーションアセンブリ情報 をクリックすると

 

アセンブリ情報のウィンドウが表示され、バージョンを設定することができました。

 

しかしながら、.NET 5 で作成したプロジェクト(Windows Forms(.NET Core))ではプロジェクトのプロパティのアプリケーションの画面にアセンブリ情報のボタンがありません。

 

.NET 5 で作成したプロジェクト(Windows Forms(.NET Core))では、プロジェクトのプロパティで表示された画面に パッケージ が追加されているので、このパッケージを選択し、画面の下の方にアセンブリバージョンアセンブリファイルのバージョンがあるので、この値を編集します。

 

 .NET 5 C# Windows Forms プログラミング へ戻る

【.NET 5 C# WinForms】メニューとメニューイベントの追加

(2021.8.3追記)Visual Studio 2019を更新することにより下図のように従来の.NET Frameworkと同等にメニューを追加できるようになりました。本記事は旧バージョン操作方法となります。
下図は Visual Studio Community 2019 バージョン 16.10.4 の画面です。

 


 

.NET5のメニューは、.NET Frameworkのメニューと比べ、メニューの追加方法とイベントの追加方法が変更されています。

今回は、このよう↓にメニューを追加します。

 

メニューの追加

ツールボックスの MenuStrip をフォームへドラッグ&ドロップします。

 

すると、フォームに MenuStrip が追加されますが、.NET Framework のときのように、 ここへ入力 の文字が表示されていません。

(参考).NET FrameworkのMenuStrip

 

.NET 5において、メニューを追加するには、MenuStripプロパティ画面の ItemsCollection を追加するため、右側にある ボタンをクリックします。

 

すると、このようなエディタが表示されます。

 

次に、Addボタンをクリックし、MenuItemを追加します。

.NET5でも、.NET Frameworkと同じように MenuStrip にMenuItem,ComboBox,TextBoxが追加可能です。

 

MenuItemを追加すると、このようになります。

 

さらに後から分かりやすいように、 (Name)の名前を変更し、メニューの表示名(今回は ファイル(&F))を変更しておきます。

 

同様にして ヘルプ 用のメニューも追加した状態がこちら↓

 

さらにメニューの ファイルの下に 開く終了 を追加したいので、今度は DropDownItems の(Collection) の右側にある ボタンをクリックし、 開く終了 を、ファイルの時と同様に追加します。

 

メニューイベントの追加

次に、メニューのクリックイベントを追加したいのですが、.NET Frameworkの時にあった⚡マークがありません。

そこで、プロパティウィンドウの上部にある ▼マーク をクリックしすると、フォームに追加されているコントロール一覧が表示されるので、この中からメニューのイベントを追加したい項目(ここでは menuFileExit)を選択します。

 

すると、 menuFileExitのプロパティが表示され、⚡マークも表示されるので、⚡をクリックし、さらにメニューのクリックイベント(Click)をダブルクリックします。

 

すると、.NET Frameworkと同様に、Clickイベントのコードが生成されるので、イベント内の処理を追記していきます。

 

今回は 終了 のメニューイベントだったので、 this.Close() を追記しました。

 

このようにして、最初に示したようなメニューが追加できます。

 

 .NET 5 C# Windows Forms プログラミング へ戻る

【.NET 5 C# WinForms】新規プロジェクトの作成

.NET5 の Windows Forms のプロジェクトの作成はVisual Studio を起動し、新しいプロジェクトの作成をクリックします。
※.NET 5.0を使用するには Visual Studio 2019 Ver.16.8以降が必要になります。

 

表示されたプロジェクトの種類の中から、 Windows Forms App(.NET) を選択し、次へをクリックします。

 

Windows Forms App(.NET)とは別に Windows フォームアプリケーション(.NET Framework)もあるので、ご注意下さい。(こちらのプロジェクトでは.NET 5 は指定できません。)

また、プロジェクトの種類が多いので、探すのが大変な場合は、上の方にあるプルダウンから C#,Windows ,デスクトップ を選択すると、少し見つけやすくなります。

 

これで、最小限のプロジェクトが作成されますが、C#のソースコードを見ると、従来の.NET Frameworkのソースコードと違いはありません。

ちょっと違うのが、フォームのアイコンぐらいでしょうか??

 

プロジェクトのプロパティで、対象のフレームワーク を確認し、.NET 5.0 を使用する場合は、対象のフレームワークを .NET 5.0 へ変更してください。

 

また、対象のフレームワークを .NET Core から .NET へ変更すると、以下のような警告が表示されます。

この場合、プロジェクトの設定を変更するのですが、変更方法については、下記ページを参照下さい。

【NETSDK1137】Microsoft.NET.Sdk.WindowsDesktop SDKを使用する必要はなくなりました。

 

.NET 5 C# Windows Forms プログラミング へ戻る

【C#】ユーザーコントロールが無くなった?

久々にVisual Studio 2015のC#でユーザーコントロールを作ろうとして、新規プロジェクトを作成しようとしたら、ユーザーコントロールのテンプレートが無い!!

以前はテンプレート→Visual C#→Windowsの中にユーザーコントロールがあったはずなのに。。

 

と思ったら、テンプレート→Visual C#→Windowsの下の階層にクラシックデスクトップというフォルダが作成されていて、この中に Windowsフォームコントロールライブラリ(旧ユーザーコントロール)が移動していました。

 

クラシックデスクトップという表現が、かろうじてレガシーデスクトップとは言っていないだけ、まだマシですが、今後はUWPに移行したいのかな??

 

※この情報はVisual Studio 2015の場合です。

おそらくVisual Studio 2015と2017が同時にインストールされている場合だと思います。

【C#】フーリエ変換(FFT, DFT)プログラム

以前、Excelのマクロを使って、データ個数に応じて高速フーリエ変換(FFT)と離散フーリエ変換(DFT)の処理を自動で切り替えるマクロを作成したのですが、Excelではデータ数が多い時など、使いにくい場合もあるので、今度は、C#でフーリエ変換部分をライブラリ(*.dll)にし、そのライブラリを使ったプログラムを作成しました。

 

実行ブログラムはこちら FourierTransform.zip

ソースコードは、すべてGitHubで公開しています。

https://github.com/ImagingSolution/FourierCSharp

 

使用方法は以下の通りです。

 

プログラムの実行

zipファイルを解凍すると、

FourierTransform.exe

FourierCSharp.dll

sampledata

となっていますので、FourierTransform.exeをダブルクリックして実行してください。

 

フーリエ変換できるデータフォーマット

データは1行あたりに1つのデータのCSVファイルとなります。

例)

1
1.30939551
1.59096344
1.820538079
1.980767038
2.06331351
2.069803727
2.01139008
1.906994542
1.780482109
1.65716389
1.560123288
1.506883103
1.506883103
1.560123288

入力データに実数部と虚数部が含まれる場合は、1行あたりに

実数部、虚数部

とします。

例)

50, 0
0, -25
0, 0
0, -12.5
0, 0
0, 0

サンプルのデータとして、ZIPファイル内の sampledataフォルダ内にいくつかCSVファイルを入れてありますので、そちらを参考にしてください。

 

データの読込

メニューの File → LoadData をクリックし、CSVファイルを指定することで、データが読み込まれ、フーリエ変換の結果(フーリエ変換後の各周波数の大きさ)が表示されます。

 

フーリエ変換はデータの個数が2のn乗個の場合:FFT、その他の場合:DFTを行います。

 

フーリエ変換、逆フーリエ変換の切替

メニューの Fourier direction → Forward もしくは Backward をクリックすることで、離散フーリエ変換、逆離散フーリエ変換が切り替わります。

 

離散フーリエ変換は

$$F(t)=\sum _{ x=0 }^{ N-1 }{ f(x){ e }^{ -i\frac { 2\pi tx }{ N } } } $$

逆離散フーリエ変換は

$$f(t)=\frac { 1 }{ N } \sum _{ t=0 }^{ N-1 }{ F(t){ e }^{ i\frac { 2\pi xt }{ N } } } $$

で計算しています。

 

窓関数

メニューの Window→ Hamming, Hanning, Blackman のいづれかをクリックすることで、それぞれの窓関数を通します。

↓ 窓関数

 

ただし、窓関数を通すと、元に戻せないため、元に戻す場合は、再度、データ読込を行って下さい。

 

免責事項

ここに公開されているプログラムは自由に使って頂いて構いませんが、バグ等による責任は待てませんので、自己責任において参照下さい。

 

フリーウェアへ戻る

【C#】複素数の計算(Complex構造体)

C#では複素数を扱う Complex構造体(名前空間:System.Numerics)が用意されています。

ただし、プロジェクトの初期状態では、使えないため、プロジェクトの参照を右クリックし、参照の追加System.Numerics を追加してください。

 

 

コード例

var complex1 = new System.Numerics.Complex(Math.Sqrt(3), 1.0);
Console.WriteLine($"complex1.Magnitude = {complex1.Magnitude}");
Console.WriteLine($"complex1.Phase = {complex1.Phase * 180.0 / Math.PI}");

var complex2 = new System.Numerics.Complex(1.0, Math.Sqrt(3));
Console.WriteLine($"complex2.Magnitude = {complex2.Magnitude}");
Console.WriteLine($"complex2.Phase = {complex2.Phase * 180.0 / Math.PI}");

var complex3 = complex1 * complex2;
Console.WriteLine($"complex3.Magnitude = {complex3.Magnitude}");
Console.WriteLine($"complex3.Phase = {complex3.Phase * 180.0 / Math.PI}");

var complex4 = complex1 + complex2;
Console.WriteLine($"complex4.Magnitude = {complex4.Magnitude}");
Console.WriteLine($"complex4.Phase = {complex4.Phase * 180.0 / Math.PI}");

// 結果
//complex1.Magnitude = 2
//complex1.Phase = 30
//complex2.Magnitude = 2
//complex2.Phase = 60
//complex3.Magnitude = 4
//complex3.Phase = 90
//complex4.Magnitude = 3.86370330515627
//complex4.Phase = 45

 

複素数の大きさは Magnitudeプロパティで、位相はPhaseプロパティで拾えるし、複素数の演算(加算、減算、乗算、除算)も可能です。

複数の演算は、複素数と複素数、複素数と実数の演算が可能です。

 

これが出来るという事はDFT(離散フーリエ変換)も簡単に出来ちゃいますね。

 

関連ページ

複素数のイメージ

複素数の計算

離散フーリエ変換(DFT)の直感的理解

 

参考ページ

https://docs.microsoft.com/ja-jp/dotnet/api/system.numerics.complex?view=netframework-4.8

 

← C# へ戻る

【C#3.0~】自動実装プロパティ

C#3.0(Visual Studio 2008)からは、自動実装プロパティなる物が使えるようになりました。

 

C#2.0までのプロパティの実装は、こんな感じ↓

class Contrast
{
    private double _scale = 1.0;
    public double Scale
    {
        get
        {
            return _scale;
        }
        set
        {
            _scale = value;
        }
    }

    private double _offset = 0.0;
    public double Offset
    {
        get
        {
            return _offset;
        }
        set
        {
            _offset = value;
        }
    }
}

 

で行っていたかと思いますが、C#3.0からは、自動実装プロパティでは、

class Contrast
{
    public double Scale { get; set; }

    public double Offset { get; set; }
}

のように書くことができます。

これで、ずいぶんシンプルで見やすくなるかと思います。

 

また、プロパティを読み取り専用にするには set; の前に private を付けて

class Contrast
{
    public double Scale { get; set; }

    public double Offset { get;  private set; }
}

とします。

 

C#6.0(Visual Studio 2015)からは自動プロパティを以下のようにすることで、初期化を行う事ができます。

class Contrast
{
    public double Scale { get; set; } = 1.0;

    public double Offset { get; set; } = 0.0;
}

さらにC#6.0からは、読み取り専用にプロパティを get; のみの記述で可能となります。

ただし、値を変更できるのは、コンストラクタのみとなります。(他のメソッド等からは変更できません。)

class Contrast
{
    public double Scale { get; set; } = 1.0;

    public double Offset { get; } = 0.0;

    public Contrast(double scale, double offset)
    {
        this.Scale = scale;
        this.Offset = offset;
    }
}

 

パフォーマンスの比較

C#2.0まで、処理速度が必要な部分では、プロパティの参照は遅いので、クラス内での処理ではプロパティを参照せず、フィールドの値を参照するようにしていたのですが、フィールドの無い、自動プロパティではどうか?確認してみました。

 

以下のように、C#2.0の時のプロパティを使った Class1 と、自動実装プロパティを使った Class2 を作成し、ただ、値を取得するだけのメソッド(GetValue)を作成しました。

class Class1
{
    private double _a = 1.0;
    public double A
    {
        get
        {
            return _a;
        }
        set
        {
            _a = value;
        }
    }

    public double GetValue()
    {
        return _a;
    }
}

class Class2
{
    public double A { get; set; } = 1.0;

    public double GetValue()
    {
        return this.A;
    }
}

このGetValue()メソッドをそれぞれ、10億回繰り返した時の処理時間を5回計測したときの平均処理時間は、以下の通りでした。

 

Class1 273.6 msec
Class2 552 msec

 

プロパティ参照は、プロパティの内部でエラー処理などを行っている場合も多いので、そのために遅い場合もありますが、単純にプロパティ参照するだけでも遅い結果でした。

 

そのため、パフォーマンスが必要な場合は、昔の書き方もありですね。

 

参考ページ

自動実装するプロパティ (C# プログラミング ガイド)

自動実装するプロパティを使用して簡易クラスを実装する方法 (C# プログラミング ガイド)

 

← C#2.0からの脱却 へ戻る

【C#4.0~】Parallel.Forによる並列処理

C#4.0(Visual Studio 2010)からはParallel.For(名前空間:System.Threading.Tasks)による並列処理が可能となります。

2コアや4コアは当たり前の時代なので、C#のParallel.Forを使った方が良いかは別としても、並列処理はしないとCPUの無駄遣い状態になってしまいます。

 

Parallel.Forの構文については、定番の下記 ++C++のページ↓

https://ufcpp.net/study/csharp/lib_parallel.html#parallel

 

を見て頂くと分かりやすいかと思いますが、通常のfor文では

for (int i = 0; i < N; i++)
{
    // 通常の処理
}

と書くところを

Parallel.For(0, N, i =>
{
    // この部分の処理が並列化される
});

のように書くと、Parallel.Forの{ }内では並列処理が行われます。

 

ここでは、Parallel.Forを画像処理的に使うと、どうなるか?やってみます。

 

まず、通常のfor文でコントラスト調整をする以下のようなメソッドを作ります。

private void ContrastImage(Bitmap bmp, double scale, double offset)
{
    var width = bmp.Width;
    var height = bmp.Height;

    // Bitmapをロック
    var bmpData = bmp.LockBits(
            new Rectangle(0, 0, width, height),
            System.Drawing.Imaging.ImageLockMode.ReadWrite,
            bmp.PixelFormat
        );

    // メモリの幅のバイト数を取得
    var stride = Math.Abs(bmpData.Stride);

    int channel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;

    // 画像データ格納用配列
    var picData = new byte[stride * bmpData.Height];

    // Bitmapデータを配列へコピー
    System.Runtime.InteropServices.Marshal.Copy(
        bmpData.Scan0,
        picData,
        0,
        stride * bmpData.Height
        );

    for (int y = 0; y < height; y++)
    {
        int lineIndex = stride * y;

        for (int x = 0; x < width * channel; x++)
        {
            int value = picData[lineIndex + x];
            value = (int)(value * scale + offset);
            value = (value < 0) ? 0 : (value > 255) ? 255
                    : value
                    ;
            picData[lineIndex + x] = (byte)value;
        }
    }

    // 配列をBitmapデータへコピー
    System.Runtime.InteropServices.Marshal.Copy(
        picData,
        0,
        bmpData.Scan0,
        stride * bmpData.Height
        );

    // アンロック
    bmp.UnlockBits(bmpData);
}

このメソッドの二重ループの部分

for (int y = 0; y<height; y++)
{
    int lineIndex = stride * y;

    for (int x = 0; x<width* channel; x++)
    {
        int value = picData[lineIndex + x];
        value = (int)(value* scale + offset);
        value = (value< 0) ? 0 : (value > 255) ? 255
                : value
                ;
        picData[lineIndex + x] = (byte)value;
    }
}

をParallel.Forを使って並列処理にすると

Parallel.For(0, height, y =>
{
    int lineIndex = stride * y;

    for (int x = 0; x<width* channel; x++)
    {
        int value = picData[lineIndex + x];
        value = (int)(value* scale + offset);
        value = (value< 0) ? 0 : (value > 255) ? 255
                : value
                ;
        picData[lineIndex + x] = (byte)value;
    }
}
);

となります。

実際に、このメソッドを使って処理を行い、5回の平均処理時間を比べたところ

通常のfor文 Parallel.For
197.2msec 92.4msec

CPU:i7-7700K@4.2GHz(4コア)

処理画像サイズ:6000 x 4000 x 24bitカラー

という結果になりました。

 

評価ソフトのイメージ

(処理前) (処理後)

 

Parallel.Forを使うとCPUのコア数分ぐらいは速くなるのか?と期待していたのですが、他の処理を行っても、だいたい2倍ちょっと速くなるようです。

 

また、Parallel.Forを使い始めたばかりだとやってしまいがちな定番のミスですが、
ここでのサンプルの lineIndexvalue をParallel.Forよりも前で定義してしまうと、正しい結果を得られなくなるので、ご注意下さい。

 

← C#2.0からの脱却 へ戻る

【C#】24bitと32bitカラー画像の表示速度の比較

カラー画像の24bitカラーと32bitカラーの画像はR,G,Bの成分はそれぞれ8bitで表現され、32bitの時に、残りの8bit部分で画像の透過率として使われる場合もありますが、画像処理的には、あまり使われる事はありません。

そのため、カラー画像と言えばメモリ容量の少ない24bitカラー画像を扱う事が、個人的には多かったのですが、GPU処理やSIMD処理などを考慮すると、メモリアクセス的に有利な32bitカラーの方が都合が良い場合もあります。

 

そこで今回は、24bitカラーと32bitカラーとで、表示速度に違いがあるか?調べてみました。

 

評価に用いたプログラムのイメージはこちら↓

 

プログラムは以下のような物で、PictureBoxのImageに指定するBitmapクラスオブジェクトのPixelFormatが

  • Format24bppRgb
  • Format32bppRgb
  • Format32bppArgb
  • Format32bppPArgb

の違いによって違うか?

また、表示する画像データが24bitのカラー画像と32bitのカラー画像で違いが出るか?を検証してみました。

 

 

表示に用いた画像サイズは1024×1024画素で24bitと32bitのカラー画像となります。

この画像を1000回表示した時の表示時間から1秒あたりに表示できた画像枚数(フレームレート[fps])を算出し、この値を5回計測し平均したものが以下の結果となります。

 

24bitカラー画像 32bitカラー画像
Format24bppRgb 139.7 142.3
Format32bppRgb 140.3 150.1
Format32bppArgb 123.6 126.6
Format32bppPArgb 182.0 187.8

 

これを見ると、32bitカラー画像の方が少し表示速度が速く、Graphicsオブジェクトに用いたPixelFormatは Format32bppPArgb が少し突出して速い事が分かりました。

 

この Format32bppPArgb とは何か?調べてみると

 

1 ピクセルあたり 32 ビットの形式であることを指定します。つまり、アルファ、赤、緑、および青のコンポーネントに、それぞれ 8 ビットを使用します。 アルファ コンポーネントに応じて、赤、緑、および青のコンポーネントが事前乗算されます。

 

とのこと。

いまいち理解できませんが、事前乗算している事で速いのか??

 

という事で、FromImageメソッドで使うGraphicsオブジェクトは Format32bppPArgb を使うと良さそうです。

 

少し前(32bitOSがメインだった時代)では、搭載できるメモリサイズは2GBだし、実質的にプログラムが使用できるのは1GB程度しか無かったため、メモリサイズはできるだけ節約したかったため、カラー画像と言えば24bitと思っていたのですが、今では64bitOSが当たり前で、メモリも4~8GBぐらいは普通に搭載されているので、32bitカラーデータもありですね。

 

画像処理のためのC#テクニックへ戻る

【C#3.0~】拡張メソッド

C#3.0(Visual Studio 2008)からは、拡張メソッドよ呼ばれる、既存の型やクラスにメソッドを追加することが出来るようになりました。

 

と、聞くと、クラスを継承すれば出来るでしょ!という意見があろうかと思いますが、拡張メソッドでは、そもそも継承できない sealed のクラスに対しても、元の型やクラスを変更することなく、メソッドを追加することが出来ます。

 

この拡張メソッドの記述方法は

staticクラスに、staticのメソッド(これが拡張メソッドとなる)を追加し、最初の引数に拡張元の型やクラスの引数を書き、型やクラスの前に this を付けます。

 

具体的には以下のようになります。

static class ExtensionMethod
{
    public static void ScaleAt(
        this System.Drawing.Drawing2D.Matrix matrix, 
        float scaleX, 
        float scaleY, 
        System.Drawing.PointF point
        )
    {
        // 原点へ移動
        matrix.Translate(-point.X, -point.Y, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 倍率変換
        matrix.Scale(scaleX, scaleY, System.Drawing.Drawing2D.MatrixOrder.Append);
        // 元の位置へ戻す
        matrix.Translate(point.X, point.Y, System.Drawing.Drawing2D.MatrixOrder.Append);
    }
}

この例は、System.Drawing.Drawing2D.Matrixクラスに、指定した点を中心に拡大縮小を行うメソッド(ScaleAt)を追加しています。

(指定した点を中心に回転するRotateAtメソッドはあるのに何で??と思っていたんですよ)

 

この拡張メソッドを使用する側では、以下のように、通常のMatrixクラスのRotateAtメソッドと同じように、今回拡張したメソッド(ScaleAt)を使う事が出来るようになります。


var mat = new System.Drawing.Drawing2D.Matrix();

// (20,30)を中心に30°回転(通常のメソッド)
mat.RotateAt(30, new PointF(20, 30), System.Drawing.Drawing2D.MatrixOrder.Append);

// (100, 150)を中心にX方向に2倍、Y方向に3倍(拡張メソッド)
mat.ScaleAt(2, 3, new PointF(100, 150));

 

← C#2.0からの脱却 へ戻る

【C#4.0~】引数の省略と名前付き引数

C#4.0(Visual Studio 2010)からは引数の省略および名前付き引数が使えるようになりました。

 

引数の省略

なにはともあれ、サンプルをご覧ください。

public static double AddScale(double a, double b, 
                              double scale = 1.0, double offset = 0.0)
{
    return (a + b) * scale + offset;
}

このように、 引数=初期値 のように書くと、その引数は省略して呼びだすことができます。

呼び出し側は

double ans;
ans = AddScale(10, 5);            // ans = 15.0
ans = AddScale(10, 5, 1.0);       // ans = 15.0
ans = AddScale(10, 5, 1.0, 0.0);  // ans = 15.0

のようにすると、どれも同じ結果となります。

もちろん、初期値以外を以下のように

var ans = AddScale(10, 5, 2.0, 1.0); // ans = 31.0

指定することもできます。

 

ただし、省略する引き数の次に省略しない引数を続けることはできません。


public static double AddScale(double a, double b, 
                              double scale = 1.0, double offset)  // これはエラー
{
    return (a + b) * scale + offset;
}

 

名前付き引数

今度はメソッドを呼び出す側のテクニックです。

サンプルとして、前回と同じメソッドを用います。

public static double AddScale(double a, double b, 
                              double scale = 1.0, double offset = 0.0)
{
    return (a + b) * scale + offset;
}

名前付き引数では、引数の名前にコロン(:)を付けてメソッドを呼び出します。

var ans = AddScale(a: 10, b: 5, scale: 1.0, offset: 0.0); // ans = 15.0

この例だと面倒なだけですが、名前を付けて呼び出すと、引数を書く順番はどうでもよくなります。

var ans = AddScale(scale: 2.0, offset: 5.0, b: 5, a: 10); // ans = 35.0

また、引数の省略も同様に出来て、

var ans = AddScale(a: 10, b: 5, offset: 2.0); // ans = 17.0

var ans = AddScale(10, 5, offset: 3.0); // ans = 18.0

と書くことができます。

最初の引数の省略だけでは引数の順番を飛ばして省略(scaleだけを省略)することは出来ませんでしたが、名前付き引数では引数を飛ばして省略することが可能になります。

 

(参考)

https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments

 

← C#2.0からの脱却 へ戻る

【C#3.0~】varによる暗黙的な型宣言

C#3.0(Visual Studio 2008)からはvarを用いた暗黙的な型宣言を行う事ができます。

 

例えば、以下のような例

string text = "Sample";

では text の型は = の右辺を見ると 文字列型(string)である事は明白なので、 var を使って

var text = "Sample";

と書くことができます。

一番、使い勝手の良い使い方は、クラスを new する時に、

System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

と書いていたところを var を用いると

var sw = new System.Diagnostics.Stopwatch();

となります。

他にも

List<string> a = new List<string>();

が var だと

var a = new List<string>();

となります。

 

逆に使えないのは、

var a; // これはエラーになる

のように = で初期化されていないと、 var 単独では型がわからないためエラーとなります。

 

他にもクラスのフィールドでは用いる事ができず、メソッド内のローカル変数のみの対応となります。

class Test
{
    var a = "Sample";   // エラー

    public Test()
    {
        var b = "Sample";   // OK
    }
}

 

C#2.0からの脱却 へ戻る

C#2.0(Visual Studio 2005)からの脱却

私が本格的にC#を始めたのはC#2.0(Visual Studio 2005)からなのですが、会社でプログラムをしていると、お客様向けのサンプルプログラムを作成することから、むやみにVisual Studioのバージョンを上げる事もできず、Visual Studio 2005をかなり長い事使用してきました。

現在ではVisual Studio 2015をメインに使用しているのですが、C#の構文はC#2.0をひきずっておりまして。。

最近では、お客様が使っているVisual Studioは古くてもVisual Studio 2010ぐらいでしょうか。

ここでは、少しずつ、便利な機能は取り入れていこう!というのが目的でC#2.0より新しい構文についてまとめていこうかと思います。

 

varによる暗黙的な型宣言 C#3.0~
自動実装プロパティ C#3.0~
拡張メソッド C#3.0~
Parallel.Forによる並列処理 C#4.0~
引数の省略と名前付き引数 C#4.0~
文字列補間($を使った文字列書式設定) C#6.0~

ちなみに、C#のバージョンとVisual Studioのバージョンの対応は以下の通りです。

C#2.0 Visual Studio 2005
C#3.0 Visual Studio 2008
C#4.0 Visual Studio 2010
C#5.0 Visual Studio 2012,2013
C#6.0 Visual Studio 2015
C#7.0 Visual Studio 2017
C#8.0 Visual Studio 2019

(参考)https://en.wikipedia.org/wiki/C_Sharp_(programming_language)#Versions

 

画像処理100本ノックをC#でやってみた

画像処理100本ノック

https://github.com/yoyoyo-yo/Gasyori100knock

https://qiita.com/yoyoyo_/items/2ef53f47f87dcf5d1e14

 

というページがあるのをご存じでしょうか?

最近の画像処理と言えば、OpenCVをPythonでやってみた!

という情報にあふれているのですが、この画像処理100本ノックでは、画像処理の処理部分はOpenCVに頼らずにベタにPythonやC++でプログラムしています。

 

実際、今どきは画像処理のプログラムをベタに組んだところで、車輪の再開発になるだけで意味があるのか??という意見もあろうかと思いますが、1度はやっておくと画像処理の理解が深まるので、おススメです。

 

そこで私も画像処理100本ノックの問題をC#で作ってみました。

ソースコードはGitHubに置いてあります。

https://github.com/ImagingSolution/ImagingDotNet

 

表向きは画像処理100本ノックに沿ってプログラムしていますが、目指すところはC#だけで、そこそこ処理が速くて、OpenCVのPython版のように簡単に使えるC#の画像処理ライブラリ(ImagingDotNet)を作るのが目標です。

本気で使うとなると、エラー処理は甘いし、処理もすごく速い訳ではないので、あくまでも、C#で作る画像処理プログラムのお勉強用です。

 

Q.1.チャンネル入れ替え

var src = new Bitmap("Lena.bmp");
var dst = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2RGB);
入力 出力

C# >> CvtColor.cs cvt_COLOR_BGR2RGB部分

 

Q.2.グレースケール化

var src = new Bitmap("Lena.bmp");
var dst = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2GRAY);
入力 出力

C# >> CvtColor.cs cvt_COLOR_BGR2GRAY部分

 

Q.3.二値化

(参考)https://imagingsolution.net/imaging/binarization/

var src = new Bitmap("Lena.bmp");
var dst = (Bitmap)src.Clone();
ImagingDotNet.Threshold(src, dst, 128, 255, ImagingDotNet.THRESH_BINARY);
入力 出力

C# >> Threshold.cs

 

Q.4.大津の二値化

(参考)https://imagingsolution.net/imaging/discriminant-analysis-method/

var src = new Bitmap("Lena.bmp");
var gray = ImagingDotNet.CvtColor(src, ImagingDotNet.COLOR_BGR2GRAY);
var dst = (Bitmap)src.Clone();
ImagingDotNet.Threshold(gray, dst, 128, 255, ImagingDotNet.THRESH_OTSU);
入力 出力

C# >> Threshold.cs

 

 

4問やったところで、力尽きました…

100問分、問題を考えてプログラムを作成した100本ノックの著者は、ほんとスゴイ!!

 

画像処理のためのC#テクニック へ戻る

【C#】画像の輝度値の取得設定速度の比較

C#で画像の輝度値を取得/設定を行う場合は、GetPixel、SetPixelメソッドを使うと遅いのでLockBits~UnlockBitsでポインタをむき出して輝度値の取得/設定を行うのが定番となっていますが、自分自身でGetPixel、SetPixelメソッドを使った事が無かったので、いくつかの方法で処理時間を比較してみました。

 

【評価環境】

OS Windows10 64bit ver.1809
CPU Intel Core i7-7700K 4.2GHz
メモリ 32GB
.NET Framework 4.5.2
プラットフォーム Any CPU(32ビットを優先)
評価画像 jpeg 6000×4000画素 24bitカラー

です。

 

評価プログラムとしては、カラー画像の輝度値をR,G,Bそれぞれ取得し、値を明暗反転して輝度値を入れなおすプログラムを作成しています。

 

処理結果は、以下のようにポジ→ネガ→ポジ→ネガと繰り返すようになっています。

 

処理時間は処理を5回行い平均した時間となります。

 

 

GetPixel、SetPixelを使った方法

平均処理時間は27439msec

本当に遅かった。。

しかもGetPixel、SetPixelではモノクロ画像(Format8bppIndexed)に対応していないので、モノクロ画像も多く扱う画像処理には不向きとなります。

 

LockBits~UnlockBitsでポインタ(Scan0)を取得し、データを配列にコピーしてから処理を行い、結果を元にコピーしなおす方法

平均処理時間は101msec

さすがにGetPixel、SetPixelよりはぜんぜん速いです。

 

MarshalクラスのReadByte、WriteByteメソッドでポインタ(Scan0)に直接、値を読み書きする方法

平均処理時間は273.6msec

この方法に少し期待していたのですが、配列にコピーした方が速かった。。

 

unsafeを使ったポインタ(Scan0)を直接読み書きする方法

平均処理時間は25.4msec

やっぱりunsafeはできれば使いたくないのですが、ここまで速いと使うのもあり??

ポインタを使うなら、C言語のライブラリにしておきたい気もしますが、C#だけで完結できるのもちょっと惹かれます。

 

ポインタで読み書きし、Parallel.Forをつかって処理を並列化する方法

平均処理時間は11.4msec

もう、unsafeは使うしかないでしょ!というレベルですが、やっぱり抵抗がある。。

 

処理を呼び出す側のプログラム

 

まとめ

それぞれの処理時間をまとめると以下の通りでした。

 

方法 処理時間(msec)
GetPixel、SetPixel 27439
LockBits、UnlockBitsで配列を介して処理 101
MarshalクラスのReadByte、WriteByte 273.6
unsafeのポインタで参照 25.4
unsafeのポインタの並列処理 11.4

 

結局GetPixel、SetPixelは論外でした。

あとは好みというか、ポリシーというか、意見の分かれるところだと思いますが、ポインタで処理するのは、やはり魅力です。

 

(2019.11.8追記)

.NET Framework4.5.232ビットを優先で評価を行っていたので、この部分を変えると処理速度に差が出るか?確認しました。

 

.NET Framework 4.7.2  32ビットを優先

方法 処理時間(msec)
GetPixel、SetPixel 27293.4
LockBits、UnlockBitsで配列を介して処理 101.8
MarshalクラスのReadByte、WriteByte 290.8
unsafeのポインタで参照 24.4
unsafeのポインタの並列処理 9.8

 

.NET Framework 4.7.2  32ビットを優先なし

方法 処理時間(msec)
GetPixel、SetPixel 21281.2
LockBits、UnlockBitsで配列を介して処理 85.4
MarshalクラスのReadByte、WriteByte 213.6
unsafeのポインタで参照 27.2
unsafeのポインタの並列処理 9.2

 

となりました。

ということで、.NET Frameworkの4.5.2と4.7.2の差はほとんどありませんでしたが、「32ビットを優先」のあるなしでは優先しない(64bitで動作)方が少し速い結果となりました。

 

関連記事

【C#】Bitmap画像データのメモリ構造

 

画像処理のためのC#テクニックへ戻る