C#获得平均屏幕颜色的最快方法

时间:2013-10-23 18:57:48

标签: c# directx screen-scraping gdi+ directx-11

我目前正在使用C#,arduino和Ikea Dioder为我的电脑显示器创建流光溢彩。目前硬件部分运行完美;但是,我在检测一部分屏幕的平均颜色方面遇到了问题。

我对我正在使用的实现有两个问题:

  1. 性能 - 这两种算法都会在屏幕上添加一些明显的口吃。什么都没有停止,但在观看视频时很烦人。
  2. 没有全屏游戏支持 - 当游戏处于全屏模式时,这两种方法都会返回白色。

    public class DirectxColorProvider : IColorProvider
    {
    
        private static Device d;
        private static Collection<long> colorPoints;
    
        public DirectxColorProvider()
        {
            PresentParameters present_params = new PresentParameters();
            if (d == null)
            {
                d = new Device(new Direct3D(), 0, DeviceType.Hardware, IntPtr.Zero, CreateFlags.SoftwareVertexProcessing, present_params);
            }
            if (colorPoints == null)
            {
                colorPoints = GetColorPoints();
            }
        }
    
        public byte[] GetColors()
        {
            var color = new byte[4];
    
            using (var screen = this.CaptureScreen())
            {
                DataRectangle dr = screen.LockRectangle(LockFlags.None);
                using (var gs = dr.Data)
                {
                    color = avcs(gs, colorPoints);
                }
            }
    
            return color;
        }
    
        private Surface CaptureScreen()
        {
            Surface s = Surface.CreateOffscreenPlain(d, Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, Format.A8R8G8B8, Pool.Scratch);
            d.GetFrontBufferData(0, s);
            return s;
        }
    
        private static byte[] avcs(DataStream gs, Collection<long> positions)
        {
            byte[] bu = new byte[4];
            int r = 0;
            int g = 0;
            int b = 0;
            int i = 0;
    
            foreach (long pos in positions)
            {
                gs.Position = pos;
                gs.Read(bu, 0, 4);
                r += bu[2];
                g += bu[1];
                b += bu[0];
                i++;
            }
    
            byte[] result = new byte[3];
            result[0] = (byte)(r / i);
            result[1] = (byte)(g / i);
            result[2] = (byte)(b / i);
    
            return result;
        }
    
        private Collection<long> GetColorPoints()
        {
            const long offset = 20;
            const long Bpp = 4;
    
            var box = GetBox();
    
            var colorPoints = new Collection<long>();
            for (var x = box.X; x < (box.X + box.Length); x += offset)
            {
                for (var y = box.Y; y < (box.Y + box.Height); y += offset)
                {
                    long pos = (y * Screen.PrimaryScreen.Bounds.Width + x) * Bpp;
                    colorPoints.Add(pos);
                }
            }
    
            return colorPoints;
        }
    
        private ScreenBox GetBox()
        {
            var box = new ScreenBox();
    
            int m = 8;
    
            box.X = (Screen.PrimaryScreen.Bounds.Width - m) / 3;
            box.Y = (Screen.PrimaryScreen.Bounds.Height - m) / 3;
    
            box.Length = box.X * 2;
            box.Height = box.Y * 2;
    
            return box;
        }
    
        private class ScreenBox
        {
            public long X { get; set; }
            public long Y { get; set; }
            public long Length { get; set; }
            public long Height { get; set; }
        }
    
    }
    
  3. 您可以找到directX implmentation here的文件。

    public class GDIColorProvider : Form, IColorProvider
    {
        private static Rectangle box;
        private readonly IColorHelper _colorHelper;
    
        public GDIColorProvider()
        {
            _colorHelper = new ColorHelper();
            box = _colorHelper.GetCenterBox();
        }
    
        public byte[] GetColors()
        {
            var colors = new byte[3];
    
            IntPtr hDesk = GetDesktopWindow();
            IntPtr hSrce = GetDC(IntPtr.Zero);
            IntPtr hDest = CreateCompatibleDC(hSrce);
            IntPtr hBmp = CreateCompatibleBitmap(hSrce, box.Width, box.Height);
            IntPtr hOldBmp = SelectObject(hDest, hBmp);
            bool b = BitBlt(hDest, box.X, box.Y, (box.Width - box.X), (box.Height - box.Y), hSrce, 0, 0, CopyPixelOperation.SourceCopy);
            using(var bmp = Bitmap.FromHbitmap(hBmp))
            {
                colors = _colorHelper.AverageColors(bmp);
            }
    
            SelectObject(hDest, hOldBmp);
            DeleteObject(hBmp);
            DeleteDC(hDest);
            ReleaseDC(hDesk, hSrce);
    
            return colors;
        }
    
        // P/Invoke declarations
        [DllImport("gdi32.dll")]
        static extern bool BitBlt(IntPtr hdcDest, int xDest, int yDest, int
        wDest, int hDest, IntPtr hdcSource, int xSrc, int ySrc, CopyPixelOperation rop);
        [DllImport("user32.dll")]
        static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDc);
        [DllImport("gdi32.dll")]
        static extern IntPtr DeleteDC(IntPtr hDc);
        [DllImport("gdi32.dll")]
        static extern IntPtr DeleteObject(IntPtr hDc);
        [DllImport("gdi32.dll")]
        static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
        [DllImport("gdi32.dll")]
        static extern IntPtr CreateCompatibleDC(IntPtr hdc);
        [DllImport("gdi32.dll")]
        static extern IntPtr SelectObject(IntPtr hdc, IntPtr bmp);
        [DllImport("user32.dll")]
        private static extern IntPtr GetDesktopWindow();
        [DllImport("user32.dll")]
        private static extern IntPtr GetWindowDC(IntPtr ptr);
        [DllImport("user32.dll")]
        private static extern IntPtr GetDC(IntPtr ptr);
    }
    

    您可以找到GDI实施的文件Here

    可以找到完整的代码库Here

1 个答案:

答案 0 :(得分:1)

更新的答案

屏幕捕获性能较慢的问题很可能是由于BitBlt()在源像素和目标像素格式不匹配时进行了像素转换而引起的。来自docs

如果源设备上下文和目标设备上下文的颜色格式不匹配,则BitBlt函数将源颜色格式转换为与目标格式匹配。

这是导致我的代码性能下降的原因,特别是在高分辨率下。

默认像素格式似乎为PixelFormat.Format32bppArgb,因此您应该使用该格式的缓冲区:

var screen = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
var gfx = Graphics.FromImage(screen);
gfx.CopyFromScreen(bounds.Location, new Point(0, 0), bounds.Size);

降低性能的下一个来源是Bitmap.GetPixel(),它会进行边界检查。分析每个像素时切勿使用它。而是锁定位图数据并获取指向它的指针:

public unsafe Color GetAverageColor(Bitmap image, int sampleStep = 1) {
    var data = image.LockBits(
        new Rectangle(Point.Empty, Image.Size),
        ImageLockMode.ReadOnly,
        PixelFormat.Format32bppArgb);

    var row = (int*)data.Scan0.ToPointer();
    var (sumR, sumG, sumB) = (0L, 0L, 0L);
    var stride = data.Stride / sizeof(int) * sampleStep;

    for (var y = 0; y < data.Height; y += sampleStep) {
        for (var x = 0; x < data.Width; x += sampleStep) {
            var argb = row[x];
            sumR += (argb & 0x00FF0000) >> 16;
            sumG += (argb & 0x0000FF00) >> 8;
            sumB += argb & 0x000000FF;
        }
        row += stride;
    }

    image.UnlockBits(data);

    var numSamples = data.Width / sampleStep * data.Height / sampleStep;
    var avgR = sumR / numSamples;
    var avgG = sumG / numSamples;
    var avgB = sumB / numSamples;
    return Color.FromArgb((int)avgR, (int)avgG, (int)avgB);
}

这应该使您远远低于10毫秒,具体取决于屏幕大小。如果仍然太慢,可以增加sampleStep的{​​{1}}参数。

原始答案

我最近也做了同样的事情,并且想到了效果出奇的好东西。

诀窍是创建一个额外的位图,其大小为1x1像素,并在其图形上下文(双线性或双三次,但不是最近的邻居)上设置良好的插值模式。

然后,利用插值将捕获的位图绘制到1x1位图中,并检索该像素以获取平均颜色。

我正在以〜30 fps的速度进行操作。当屏幕显示GPU渲染时(例如,在Chrome中启用了硬件加速的情况下观看YouTube全屏显示),则看不到任何卡顿现象。实际上,该应用程序的CPU利用率低于10%。但是,如果我关闭Chrome的硬件加速功能,那么如果您观看得足够近的话,肯定会有一些轻微的卡顿现象。

以下是代码的相关部分:

GetAverageColor()

请注意,这适用于GPU渲染,因为using var screen = new Bitmap(width, height); using var screenGfx = Graphics.FromImage(screen); using var avg = new Bitmap(1, 1); using var avgGfx = Graphics.FromImage(avg); avgGfx.InterpolationMode = InterpolationMode.HighQualityBicubic; while (true) { screenGfx.CopyFromScreen(left, top, 0, 0, screen.Size); avgGfx.DrawImage(screen, 0, 0, avg.Width, avg.Height); var color = avg.GetPixel(0, 0); var bright = (int)Math.Round(Math.Clamp(color.GetBrightness() * 100, 1, 100)); // set color and brightness on your device // wait 1000/fps milliseconds } 现在使用GDI +。但是,当内容受DRM保护时,它不起作用。因此,它不适用于Netflix,例如:(

我在GitHub上发布了代码。即使我由于Netflix的DRM保护而放弃了该项目,它也可能会帮助其他人。