ビットマップの支配的なRGBカラーを取得する

私は現在、RGB値で私のスクリーン上の支配的な色を得るために毎秒60回この機能を実行しています。 30FPSではCPUの約15%、60FPSではCPUの25%を使用しています。このループの効率を向上させることができる方法はありますか、それとも色を完全に取得するためのより良い方法がありますか?

public Color getDominantColor(System.Drawing.Bitmap bmp) {
            BitmapData srcData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

            int stride = srcData.Stride;

            IntPtr Scan0 = srcData.Scan0;

            int[] totals = new int[] { 0, 0, 0 };

            int width = bmp.Width;
            int height = bmp.Height;

            unsafe
            {
                byte* p = (byte*)(void*)Scan0;

                for (int y = 0; y < height; y++) {
                    for (int x = 0; x < width; x++) {
                        for (int color = 0; color < 3; color++) {
                            int idx = (y * stride) + x * 4 + color;
                            totals[color] += p[idx];
                        }
                    }
                }
            }

            int avgB = totals[0]/(width * height);
            int avgG = totals[1]/(width * height);
            int avgR = totals[2]/(width * height);

            bmp.UnlockBits(srcData);

            return Color.FromArgb(avgR, avgG, avgB);
        }
12
Cuda/OpenGlを考えてください。これがGPUの目的です。
追加された 著者 Ricardo Amaral,
独立した計算をループの外に移動し、ものを再計算しようとしませんでしたか?
追加された 著者 BJ Homer,
インデントは非常に違って見えます - それはあなたのIDEのそれと全く同じに見えますか?
追加された 著者 JohnnyMo1,

5 答え

このような低レベルのピクセル操作を扱うときに役立つパフォーマンスのトリックの1つは、ギャップとして緑の8ビットを使用して赤と青を一緒に処理することが可能なことが多いということです。ここで追加しているだけなので、256個の青い値を追加して、それらが緑色を超えて赤色にオーバーフローすることはありません。

John Wuのコメントは、ストライドがあなたには無関係だというコメントです(テストされていない、特にエンディアンのバグがあるかもしれません。私がこの種のコードを書いたのは数年です。

        unsafe
        {
            uint* p = (uint*)(void*)Scan0;

            uint pixelCount = width * height;
            uint idx = 0;
            while (idx < (pixelCount & ~0xff)) {
                uint sumRR00BB = 0;
                uint sum00GG00 = 0;
                for (int j = 0; j < 0x100; j++) {
                    sumRR00BB += p[idx] & 0xff00ff;
                    sum00GG00 += p[idx] & 0x00ff00;
                    idx++;
                }

                totals[0] += sumRR00BB >> 16;
                totals[1] += sum00GG00 >> 8;
                totals[2] += sumRR00BB & 0xffff;
            }

           //And the final partial block of fewer than 0x100 pixels.
            {
                uint sumRR00BB = 0;
                uint sum00GG00 = 0;
                while (idx < pixelCount) {
                    sumRR00BB += p[idx] & 0xff00ff;
                    sum00GG00 += p[idx] & 0x00ff00;
                    idx++;
                }

                totals[0] += sumRR00BB >> 16;
                totals[1] += sum00GG00 >> 8;
                totals[2] += sumRR00BB & 0xffff;
            }
        }
6
追加された
色成分はで保存されているので、 totals [0] + = sumRR00BB&0xffff; と totals [2] + = sumRR00BB >> 16; に変更する必要があります RGB の代わりにBGR 。
追加された 著者 NateJ,

for ループを非常によく見てみると、2つの乗算を排除できることがわかります。

for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        for (int color = 0; color < 3; color++) {
            int idx = (y * stride) + x * 4 + color;
            totals[color] += p[idx];
        }
    }
}

y * stride を除いて y を使わず、 x * 4を除いて x を使わないでください。 code>、 for ループを書き換えると、それらを完全に排除できます。

var heightLimit = height * stride;
var widthLimit = width * 4;

for (int y = 0; y < heightLimit; y += stride) {
    for (int x = 0; x < widthLimit; x += 4) {
        for (int color = 0; color < 3; color++) {
            int idx = y + x + color;
            totals[color] += p[idx];
        }
    }
}

3つの乗算演算をすべて削除することで、処理量を大幅に削減できます。 (オリジナルにはおよそ8つの命令があり、縮小されたものには6つの命令があるので、作業の25%を排除しました)

それ以外に、できることは本当にたくさんありません。 作業をまとめることを検討し、それが合理的であれば再描画された領域のみを再計算する ことができます。これをチャンクしてスレッド化することもできます。この方法)。

このメソッドを呼び出す頻度が多い場合は、次の行も最適化の可能性があります。

  int avgB =合計[0] /(幅*高さ);
int avgG = totals [1] /(幅*高さ);
int avgR = totals [2] /(幅*高さ);
 

すべての3つの計算width * height が行われるのはなぜですか。 var pixelCount = width * height; を保存してから pixelCount で除算しないのはなぜですか。もちろん、除算はまだ遅いですが、あなたは浮動小数点演算を使っていないので、その逆数を使うことはできません。

You could consider, as mentioned in a comment, using CUDA/OpenGL/GPU-level work. Basically, operating on the GPU itself instead of using the CPU to do what could be very efficient on the GPU. (It's built specifically for this type of processing.) There is at least one Stack Overflow question on running C# code on the GPU, it's not very easy or simple, but it can give you a lot of power.

5
追加された

簡単なものから難しいものまでの5つのアイデア:

  • You can simplify your x/y loop to run in one dimension instead of two-- for ( i = 0; i < y * x * c; i += 4 ). Since you're looking at the whole image, you don't need to worry about the stride. Not only will this reduce the number of operations required, but you may get better performance from your CPU due to better pipelining and branch prediction.

  • If you can, use a lower color depth (I don't think you need 24 bits of color depth if you're just computing an average). The smaller storage size will yield a smaller memory area to scan. You will have to shift bits around to do the math, but that sort of thing is faster than memory access.

  • You could try resizing or scaling the bitmap to something lower rez. The resize operation will interpolate color. In theory you could scale it to a 1x1 image and just read that one pixel. If you use GDI+ to perform the scale it could use hardware acceleration and be very fast.

  • Keep a copy of the last bitmap and its totals. Use REPE CMPSD (yes, this is assembly) to compare your new bitmap to the old one and find non-matching cells. Adjust totals and recompute average. This is probably a little harder than it sounds but the scan would be incredibly fast. This option would work better if most pixels are expected to stay the same from frame to frame.

  • Do the entire scan in assembly, four bytes at a time. DWord operations, believe it or not, are faster than byte operations, for a modern CPU. You can get the byte you need through bit shifting, which take very few clock cycles. Been a while for me, but would look something like this:

        MOV ECX, ArrayLength ;ECX is our counter (= bytecount ÷ 4)
        MOV EDX, Scan0       ;EDX is our data pointer
        SUB BX, BX           ;Set BX to 0 for later
    Loop:
        LODSL                ;Load EAX from array, increment pointer
        SHRL 8, EAX          ;Dump the least eight bits
        ADDB GreenTotal, AL  ;Add least 8 bits to green total
        ADCW GreenTotal+1,BX ;Carry the 1
        SHRL 8, EAX          ;Shift right 8 more bits
        ADDB BlueTotal, AL   ;Add least 8 bits to blue total
        ADCW BlueTotal+1, BX ;Carry the 1
        SHRL 8, EAX          ;Shift right 8 more bits
        ADDB RedTotal, AL    ;Add least 8 bits to red total
        ADCW RedTotal+1, BX  ;Carry the 1
        LOOPNZ Loop          ;Decrement CX and keep going until it is zero
    

    If the assembly is too much to take on, you can try to do the same in C++ and maybe the compiler will do a pretty good job of it. At the very least, we have gotten rid of all of your multiplication operations (which can take up 5-20x the number of clock cycles compared to a shift), two of your loops, and a whole bunch of if conditionals (which would mess up your CPU's branch prediction). Also we will get nice big cache bursts regardless of the dword alignment of the byte buffer, because it is a single-dimensional contiguous blob.

4
追加された
これによりパフォーマンスが向上するとは考えにくいです。
追加された 著者 Nic Hartley,
ああ、文脈のために、答えは私がそれに答えたときから編集されていた - 私が答えたとき、それはちょうどサイズ変更についてのポイントでした。
追加された 著者 Nic Hartley,
母、その通りです。とにかく、この会話のほとんどは無関係です。私は私の最初の2つのコメントを除いてすべてをパージしました。 C ++をC#にリンクする方法の潜在的なリソースとして、あなたがあなたの答えにコメントしたものを編集することをお勧めします。スピードを上げるのに便利です。
追加された 著者 Nic Hartley,
多分そうでないかもしれません。あなたのやり方によって異なります。ビットマップを(実際には表示されないことがある)フレームバッファにレンダリングして変換すると、操作にGPUハードウェアアクセラレーションが利用される可能性があります。
追加された 著者 Tossed Corona Salad,

検証

このメソッドは public なので、有効なnull以外の null Bitmap を取得することを前提としないでください。 null チェックを追加する必要があります。それ以外の場合は、メソッドの実装の詳細を公開しています。

ネーミング

Based on the C# ネーミング guidelines methods should be named using PascalCase casing. Method level variables should be named using camelCase casing. Hence getDominantColor->GetDominantColor and IntPtr Scan0->IntPtr scan0.

考えられる問題

あなたはあなたの質問の中でこの方法があなたのデスクトップの支配的な色を得るために使われていると述べています。あなたがそれだけのためにそれを使うならば、それからすべては良いでしょう。

この方法を異なるビットマップで使用すると、問題が発生する可能性があります。

  • 渡されたビットマップが、例えば300dpiのDIN A4サイズの場合、 int [] total はオーバーフローします。

パフォーマンス

毎回 idx 値を計算するのではなく、ポインタ演算を使用することをお勧めします。 @Zefickが投稿したような最も内側のループも削除します。

public System.Drawing.Color GetDominantColor(Bitmap bmp)
{
    if (bmp == null)
    {
        throw new ArgumentNullException("bmp");
    }

    BitmapData srcData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);

    int bytesPerPixel = Image.GetPixelFormatSize(srcData.PixelFormat)/8;

    int stride = srcData.Stride;

    IntPtr scan0 = srcData.Scan0;

    long[] totals = new long[] { 0, 0, 0 };

    int width = bmp.Width * bytesPerPixel;
    int height = bmp.Height;

    unsafe
    {
        byte* p = (byte*)(void*)scan0;

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x += bytesPerPixel)
            {
                totals[0] += p[x + 0];
                totals[1] += p[x + 1];
                totals[2] += p[x + 2];
            }

            p += stride;
        }
    }

    long pixelCount = bmp.Width * height;

    int avgB = Convert.ToInt32(totals[0]/pixelCount);
    int avgG = Convert.ToInt32(totals[1]/pixelCount);
    int avgR = Convert.ToInt32(totals[2]/pixelCount);

    bmp.UnlockBits(srcData);

    return Color.FromArgb(avgR, avgG, avgB);

}

BechnmarkDotNet を使用したx64コンパイル済みの収益のベンチマーク

あなたのもの:17.5252ミリ秒
EBrown's:14.6109 ms
地雷:8.4846ミリ秒
Peter Taylor's:4.6419ミリ秒

@PeterTylorがそのコードを変更しなくなるまで、私のコメントを見てください。 #comment298573_157704 ">ビットマップの支配的なRGBカラーを取得する

2
追加された

少なくとも次のようにして品質を損なうことなく最も内側のループを展開することができます。

for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
         int idx = (y * stride) + x * 4;
         totals[color] += p[idx];
         totals[color+1] += p[idx+1];
         totals[color+2] += p[idx+2];
    }
}

潜在的には、コンパイラはそれ自身でこの最適化を行うことができますが、それが「安全でない」ブロックの中で行われるかどうかはわかりません。

1
追加された