C# 获取屏幕平均颜色的最快方法
本文关键字:方法 颜色 获取 屏幕 | 更新日期: 2023-09-27 18:36:40
我目前正在用C#,arduino和宜家Dioder为我的电脑显示器创建一个流光溢彩。目前,硬件部分运行完美;但是,我在检测屏幕部分的平均颜色时遇到问题。
我正在使用的实现有两个问题:
- 性能 - 这两种算法都会在屏幕上添加一些明显的卡顿。没有什么引人注目的,但在观看视频时很烦人。
-
不支持全屏游戏 - 当游戏处于全屏模式时,这两种方法都只返回白色。
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; } } }
您可以在此处找到 directX 实现的文件。
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 实现的文件。
完整的代码库可以在这里找到。
更新的答案
屏幕捕获性能缓慢的问题很可能是由于BitBlt()
源和目标的像素格式不匹配时进行像素转换引起的。从文档中:
如果源和目标设备上下文的颜色格式不匹配,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 毫秒,具体取决于屏幕尺寸。如果它仍然太慢,您可以增加GetAverageColor()
的sampleStep
参数。
原始答案
我最近做了同样的事情,想出了一些效果出奇的好东西。
诀窍是创建一个大小为 1x1 像素的附加位图,并在其图形上下文(双线性或双立方,但不是最近邻)上设置良好的插值模式。
然后,利用插值将捕获的位图绘制到该 1x1 位图中,并检索该像素以获得平均颜色。
我以 ~30 fps 的速度这样做。当屏幕显示 GPU 渲染时(例如,在 Chrome 中启用硬件加速的情况下全屏观看 YouTube),没有明显的卡顿或任何东西。事实上,应用程序的 CPU 利用率远低于 10%。但是,如果我关闭Chrome的硬件加速,那么如果您观察得足够近,肯定会有一些轻微的卡顿。
以下是代码的相关部分:
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
}
请注意,这适用于 GPU 渲染,因为System.Drawing.Common
现在使用 GDI+。但是,当内容受 DRM 保护时,它不起作用。因此,例如,它不适用于Netflix:(
我在GitHub上发布了代码。尽管由于Netflix的DRM保护,我放弃了该项目,但它可能会帮助其他人。