C#による画像処理の速度についての比較検証

#1. はじめに
この記事は、C#による画像処理の速度について比較している記事になります。
C#のフレームワークである「.NET Framework」における「GetPixel」「SetPixel」は一般的に速度が遅いと言われているため、実際に、エッジを検出する画像処理のアルゴリズムで、①と②を比較してみました。
① C# unsafeコード使用のアルゴリズム
② .NET FrameworkのGetPixelとSetPixel使用のアルゴリズム

C# unsafeとは、ざっくりC言語のポインタを使えるようにすることです。
ポインタを使うことで、速いアプリを開発することが可能になります。
C# unsafeはメモリを破壊する恐れがあり推奨されていません。画像処理などの重い処理で速度がでない箇所で
最終的な手段としての使用がお勧めかと思います。

#2. 結果
まずは結果からです。unsafeコードの方が早く、約32倍の速度がでました。
測定時間は下記の処理の総時間です。
・画像からPixelデータを取得する
・Pixelデータにエッジ検出用のフィルタをかける
・画像にPixelデータを設定する

②を①と全く同じアルゴリズムにしたら、200倍ほどの速度差になってしまったため、
②については、ある程度速度がでるように調整しています。

| ① unsafeコード | ② .NET Framework |
|:-----------|------------:|
| 170 ms | 5517 ms |
※検証対象の画像解像度：フルHD（1920×1080）

① unsafeコード結果画像
![1.jpg](https://qiita-image-store.s3.amazonaws.com/0/352545/50d4dc10-11c0-1b67-50c9-c6873c1c19e0.jpeg)


② .NET Framework結果画像
![2.jpg](https://qiita-image-store.s3.amazonaws.com/0/352545/c6fe1130-b06d-d332-6346-1fefdb366b62.jpeg)



おまけ結果画像
今回、エッジを検出するアルゴリズムの精度は気にしていなかったが、そこそこエッジを検出できています。

タイルの線と、輪郭が検出できています！！！
自販機のKIRINの文字も検出できてます！！！
![3.jpg](https://qiita-image-store.s3.amazonaws.com/0/352545/06b7e880-c410-1437-09b0-e2a2eae3fb7d.jpeg)

時計の数字が検出できていて、これもなかなかいい感じです！！！
![4.jpg](https://qiita-image-store.s3.amazonaws.com/0/352545/120f2fec-c1f7-4cb3-0998-6c3e84a6e9e7.jpeg)

#3. 成果物
成果物のソースコードはGitHubにアップしております。ご自由に使用ください。
GitHub：https://github.com/toshinomi/ImageProcessingSpeedComp

| 開発環境 | 言語 | フレームワーク | アプリ種類 |
|:-----------|------------:|------------:|------------:|
| Visual Studio 2019 Preview 2 |  C#  | .NET Framework 4.6 | Windows From |

※サードパーティ製品ライブラリは未使用で、C#だけでアルゴリズムを書いているため
　他のバージョンのVisual Studioでも、コンパイルできます。
※今回のアプリは比較的重い処理のため、下記の工夫も入れています。説明は省略させて頂きます。
　・画像処理部分はタスクの非同期処理で行い、画面が固まらないようにしています。
　・非同期処理から画面の更新をしています。
　・非同期処理を画面から停止できるようにしています。

操作説明
![1.jpg](https://qiita-image-store.s3.amazonaws.com/0/352545/419b20d9-1972-d817-225b-76256a9af036.jpeg)

「File Select...」ボタン：画像を選択します
「All Clear」ボタン：画面表示全てをクリアします
「Filter Start(Unsafe)」ボタン：①のアルゴリズムを実行します。
「Filter Start(Normal)」ボタン：②のアルゴリズムを実行します。
「Stop」ボタン：①または②実行中の処理を停止させます。
「View」チェックボックス：処理の進捗を表示させます。

#4. C# unsafeについて
C#やJava（JVM系）のプログラム言語は、コンピュータのメモリ上の任意の場所に自由にアクセスする手段（ポインタ）の利用が禁止または制限されています。
ポインタは非常に便利な機能である反面、システム開発においてバグの原因になりやすく、大きな弊害になっており、昨今のプログラミング言語はポインタがないものが多くなりました。
ただし、C#に関しては、C言語、C++との相互運用性や、プログラムの実行効率向上のために、ポインタが残っています。

C#でポインタを使用するには、下記の手順が必要になります。ポインタがあるのは大変ありがたいです！！！
・unsafeキーワードで囲む

```cs
unsafe
{
    // ポインタなどの処理
}
```
・コンパイル時に、/unsafe オプションを付ける
　※Visual Studio 2019 Preview 2 の場合、赤枠で囲っているところにチェックを入れます。
![5.jpg](https://qiita-image-store.s3.amazonaws.com/0/352545/6b241565-1171-2781-5fd8-d7d2584a2a9d.jpeg)



#5. アルゴリズム比較
アルゴリズムについては大差ありません。unsafeで囲って、画像のPixelデータのアドレスを取得して
フィルタ処理後に計算値を設定するだけです。これだけで32倍速になるなら試す価値があると思います。

① unsafeコード


``````FormMain.cs
// unsafeで囲みます
unsafe
{
    for (nIndexHeight = 0; nIndexHeight < nHeightSize; nIndexHeight++)
    {
        for (nIndexWidth = 0; nIndexWidth < nWidthSize; nIndexWidth++)
        {
            // バイト型のポインタで、画像のPixelデータのアドレスを取得しています。
            byte* pPixel = (byte*)bitmapData.Scan0 + nIndexHeight * bitmapData.Stride + nIndexWidth * 4;

            double dCalB = 0.0;
            double dCalG = 0.0;
            double dCalR = 0.0;
            double dCalA = 0.0;
            int nIndexWidthMask;
            int nIndexHightMask;
            int nFilter = 0;

            while (nFilter < m_nFilterMax)
            {
                for (nIndexHightMask = 0; nIndexHightMask < nMasksize; nIndexHightMask++)
                {
                    for (nIndexWidthMask = 0; nIndexWidthMask < nMasksize; nIndexWidthMask++)
                    {
                        if (nIndexWidth + nIndexWidthMask > 0 &&
                            nIndexWidth + nIndexWidthMask < nWidthSize &&
                            nIndexHeight + nIndexHightMask > 0 &&
                            nIndexHeight + nIndexHightMask < nHeightSize)
                        {
                            // フィルタ処理のために、バイト型のポインタで、画像のPixelデータのアドレスを取得しています。
                            byte* pPixel2 = (byte*)bitmapData.Scan0 + (nIndexHeight + nIndexHightMask) * bitmapData.Stride + (nIndexWidth + nIndexWidthMask) * 4;

                            dCalB += pPixel2[0] * m_dMask[nIndexWidthMask, nIndexHightMask];
                            dCalG += pPixel2[1] * m_dMask[nIndexWidthMask, nIndexHightMask];
                            dCalR += pPixel2[2] * m_dMask[nIndexWidthMask, nIndexHightMask];
                            dCalA += pPixel2[3] * m_dMask[nIndexWidthMask, nIndexHightMask];
                        }
                    }
                }
                nFilter++;
            }
            // ポインタなので、SetPixelのようなメソッドを呼ばなくて、そのままフィルタ処理後の値を設定できます。
            pPixel[0] = DoubleToByte(dCalB);
            pPixel[1] = DoubleToByte(dCalG);
            pPixel[2] = DoubleToByte(dCalR);
            pPixel[3] = DoubleToByte(dCalA);
        }
    }
    m_bitmapImageFilter.UnlockBits(bitmapData);
}
```

② .NET Framework

``````FormMain.cs
for (nIndexHeight = 0; nIndexHeight < nHeightSize; nIndexHeight++)
{
    for (nIndexWidth = 0; nIndexWidth < nWidthSize; nIndexWidth++)
    {
        byte bytePixelB;
        byte bytePixelG;
        byte bytePixelR;
        byte bytePixelA;

        double dCalB = 0.0;
        double dCalG = 0.0;
        double dCalR = 0.0;
        double dCalA = 0.0;

        int nIndexWidthMask;
        int nIndexHightMask;
        int nFilter = 0;

        while (nFilter < m_nFilterMax)
        {
            for (nIndexHightMask = 0; nIndexHightMask < nMasksize; nIndexHightMask++)
            {
                for (nIndexWidthMask = 0; nIndexWidthMask < nMasksize; nIndexWidthMask++)
                {
                    if (nIndexWidth + nIndexWidthMask > 0 &&
                        nIndexWidth + nIndexWidthMask < nWidthSize &&
                        nIndexHeight + nIndexHightMask > 0 &&
                        nIndexHeight + nIndexHightMask < nHeightSize)
                    {
                        // Pixelデータの取得はフィルタ処理中でなく、事前に取得しています。
                        // ここでGetPixelするとオーバーヘッドが凄すぎて断念しました...
                        byte bytePixel2B = m_pixelData[nIndexWidth + nIndexWidthMask, nIndexHeight + nIndexHightMask, (int)Pixel.B];
                        byte bytePixel2G = m_pixelData[nIndexWidth + nIndexWidthMask, nIndexHeight + nIndexHightMask, (int)Pixel.G];
                        byte bytePixel2R = m_pixelData[nIndexWidth + nIndexWidthMask, nIndexHeight + nIndexHightMask, (int)Pixel.R];
                        byte bytePixel2A = m_pixelData[nIndexWidth + nIndexWidthMask, nIndexHeight + nIndexHightMask, (int)Pixel.A];

                        dCalB += bytePixel2B * m_dMask[nIndexWidthMask, nIndexHightMask];
                        dCalG += bytePixel2G * m_dMask[nIndexWidthMask, nIndexHightMask];
                        dCalR += bytePixel2R * m_dMask[nIndexWidthMask, nIndexHightMask];
                        dCalA += bytePixel2A * m_dMask[nIndexWidthMask, nIndexHightMask];
                    }
                }
            }
            nFilter++;
        }
        bytePixelB = DoubleToByte(dCalB);
        bytePixelG = DoubleToByte(dCalG);
        bytePixelR = DoubleToByte(dCalR);
        bytePixelA = DoubleToByte(dCalA);

        // フィルタ処理後に、SetPixelで画像のPixelデータを設定しています。
        m_bitmapImageFilter.SetPixel(nIndexWidth, nIndexHeight, Color.FromArgb(bytePixelA, bytePixelR, bytePixelG, bytePixelB));
    }
}
```

#6. まとめ
unsafeコードが圧倒的に早く（32倍速）、速度問題に対しての有効な解決手段の一つであることが示せました。
C#はマルチプラットフォームの言語の一つで、Web(ASP.NET)、Android、iOS(Xamarin)、
デスクトップ(.NET Framework)、ゲーム(Unity)などの広範囲に使用されており、
速度問題にあたる可能性が高い言語なので今後も有効な手段を模索していきたいと思います。

以上です。