С# Самый быстрый способ получить средние цвета экрана

В настоящее время я работаю над созданием Ambilight для моего компьютерного монитора с 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; }
        }
    
    }
    

Вы можете найти файл для реализации 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 Здесь.

Полную кодовую базу можно найти здесь.


person GrantByrne    schedule 23.10.2013    source источник
comment
Что касается заикания, вы профилировали, чтобы увидеть, что занимает так много времени? Вы можете увидеть более высокую скорость, читая, скажем, каждый 1000-й пиксель (все еще выборка ~ 2000 точек на экране 1080p; вы можете выбрать даже большее значение) и делая это только каждый 10-й кадр.   -  person Tim S.    schedule 23.10.2013
comment
Для варианта DirectX 90 % времени выполняется метод d.GetFrontBufferData(0, s);. Для варианта GDI 70% времени тратится на BitBlt(hDest, box.X, box.Y, (box.Width - box.X), (box.Height - box.Y), hSrce, 0, 0, CopyPixelOperation.SourceCopy);   -  person GrantByrne    schedule 23.10.2013
comment
Также относительно сроков; У меня есть методы, работающие по таймеру в приложении winforms, которое отсчитывает все 25 мс. Я увеличил его до каждых 100 мс без заметного улучшения.   -  person GrantByrne    schedule 23.10.2013
comment
В документации GetFrontBufferData говорится, что этот метод медленный по дизайну, и поэтому его не следует использовать на пути, критичном для производительности. Вы пробовали GetBackBuffer?   -  person Tim S.    schedule 23.10.2013
comment
Вместо того, чтобы получить один большой блок, сколько времени потребуется, чтобы получить много (например, 200) маленьких (например, 1 пиксель) блоков с BitBlt?   -  person Tim S.    schedule 23.10.2013
comment
На самом деле, вероятно, лучше использовать GetPixel, если вы хотите чтобы попробовать этот подход.   -  person Tim S.    schedule 23.10.2013
comment
Вы не сможете легко получить цвет на экране для полноэкранных приложений, таких как игры или некоторые видеоплееры. Они рисуют прямо на экране и не всегда предоставляют возможность чтения из своих буферов. Даже если это так, вам придется явно писать код, специфичный для DirectX/OpenGL/и т.д.   -  person Trevor Elliott    schedule 24.10.2013
comment
Для использования на рабочем столе, включая оконные приложения и т. д., вы должны иметь возможность просто использовать Graphics.CopyFromScreen в C#. Если вы скопируете весь экран, это займет некоторое время... может быть, около 100 мс +- 50 мс. Если вы делаете это только 5 раз в секунду, это не должно быть слишком плохо с точки зрения производительности. Какое отставание вы испытываете? Используете другие приложения в Windows во время работы?   -  person Trevor Elliott    schedule 24.10.2013
comment
Если вы хотите повысить производительность, вы должны вызывать Graphics.CopyFromScreen несколько раз, чтобы читать небольшие участки экрана. Вы бы получили экспоненциально лучшую производительность, если бы вы читали только около 10-20% экрана, распределенного по множеству маленьких равномерно распределенных прямоугольников. Хотя вы потеряете некоторую точность.   -  person Trevor Elliott    schedule 24.10.2013
comment
Я собираюсь протестировать все эти варианты сегодня вечером с тестами. Я сообщу сегодня вечером или завтра о результатах этих тестов в виде ответа, чтобы увидеть, какой из них самый быстрый.   -  person GrantByrne    schedule 24.10.2013
comment
Как насчет того, чтобы использовать шейдер для записи вывода экрана в текстуру, а затем использовать максимальный уровень мип-карты для создания среднего значения в один пиксель, а затем сэмплировать его обратно в ЦП из промежуточного ресурса? Использование мип-карт — это подход, используемый в методах HDR для получения средней яркости.   -  person gareththegeek    schedule 13.05.2014


Ответы (1)


Обновленный ответ

Проблема низкой производительности захвата экрана, скорее всего, вызвана 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 мс, в зависимости от размера экрана. Если это все еще слишком медленно, вы можете увеличить параметр sampleStep для GetAverageColor().

Оригинальный ответ

Недавно я сделал то же самое и придумал что-то, что сработало на удивление хорошо.

Хитрость заключается в том, чтобы создать дополнительное растровое изображение размером 1x1 пиксель и установить хороший режим интерполяции в его графическом контексте (билинейный или бикубический, но не ближайший сосед).

Затем вы рисуете захваченное растровое изображение в это растровое изображение 1x1, используя интерполяцию, и извлекаете этот пиксель, чтобы получить средний цвет.

Я делаю это со скоростью ~ 30 кадров в секунду. Когда на экране отображается рендеринг с помощью графического процессора (например, просмотр YouTube в полноэкранном режиме с включенным аппаратным ускорением в Chrome), видимых заиканий или чего-либо еще не наблюдается. На самом деле загрузка процессора приложением намного ниже 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. Несмотря на то, что я отказался от проекта из-за DRM-защиты Netflix, это может помочь кому-то еще.

person Good Night Nerd Pride    schedule 27.07.2020