Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 209 additions & 43 deletions KeyLighting/CPUImageProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,62 @@
using System;
using System.Collections.Concurrent;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using KeyboardLighting;
using Newtonsoft.Json;
using OpenRGB.NET;


public class CPUImageProcessor : IDisposable
{

private byte[]? pixelBuffer;
private int lastWidth;
private int lastHeight;
private readonly object bufferLock = new object();

private byte[] brightnessLut;
private byte[] contrastLut;
private bool lutsInitialized = false;
private byte[] brightnessLut = new byte[256];
private byte[] contrastLut = new byte[256];
private double lastBrightness = -1;
private double lastContrast = -1;

public OpenRGB.NET.Color[] ProcessImage(Bitmap image, int targetWidth, int targetHeight, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
{
private OpenRGB.NET.Color[] previousFrame;
private OpenRGB.NET.Color[] rawColors;
private OpenRGB.NET.Color[] resultBuffer;
private bool hasPreviousFrame = false;

private double fadeSpeed;

return ProcessImageOnCPU(image, targetWidth, targetHeight, brightness, vibrance, contrast, darkThreshold, darkFactor);
public CPUImageProcessor(LightingConfig config)

Check warning on line 29 in KeyLighting/CPUImageProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field 'resultBuffer' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 29 in KeyLighting/CPUImageProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field 'rawColors' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 29 in KeyLighting/CPUImageProcessor.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field 'previousFrame' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
fadeSpeed = config.FadeFactor;
}

private OpenRGB.NET.Color[] resultBuffer;
private bool lastFrameWasSolid = false;
private int lastSolidR, lastSolidG, lastSolidB;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetFadeSpeed(double speed)
{
fadeSpeed = Math.Clamp(speed, 0.0, 1.0);
}

private OpenRGB.NET.Color[] ProcessImageOnCPU(Bitmap image, int targetWidth, int targetHeight, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public OpenRGB.NET.Color[] ProcessImage(Bitmap image, int targetWidth, int targetHeight, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
{

if (resultBuffer == null || resultBuffer.Length != targetWidth)
{
resultBuffer = new OpenRGB.NET.Color[targetWidth];
rawColors = new OpenRGB.NET.Color[targetWidth];
previousFrame = new OpenRGB.NET.Color[targetWidth];
hasPreviousFrame = false;
}

if (!AreSettingsCached(brightness, contrast))
{
InitializeLuts(brightness, contrast);
lastBrightness = brightness;
lastContrast = contrast;
}

Bitmap bitmapToProcess = image;
Expand Down Expand Up @@ -74,15 +95,29 @@

Marshal.Copy(bmpData.Scan0, pixelBuffer, 0, byteCount);

if (bytesPerPixel == 4)
ExtractColumns(bmpData.Stride, targetWidth, targetHeight, bytesPerPixel);

bool isSolidColor = IsSolidColorFrame();

if (isSolidColor)
{
ProcessColumns32Bpp(bmpData.Stride, targetWidth, targetHeight, brightness, vibrance, contrast, darkThreshold, darkFactor);
ProcessSolidColor(rawColors[0].R, rawColors[0].G, rawColors[0].B,
targetWidth, brightness, vibrance, contrast,
darkThreshold, darkFactor);
}
else if (bytesPerPixel == 3)
else
{
ProcessColumns24Bpp(bmpData.Stride, targetWidth, targetHeight, brightness, vibrance, contrast, darkThreshold, darkFactor);

ProcessColumnsWithEffects(targetWidth, brightness, vibrance, contrast, darkThreshold, darkFactor);

if (hasPreviousFrame)
{
ApplyFading(targetWidth, lastFrameWasSolid ? 0.95 : fadeSpeed);
}
}

StoreFrameState(targetWidth, isSolidColor);

return resultBuffer;
}
finally
Expand All @@ -100,7 +135,112 @@
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void ProcessColumns24Bpp(int stride, int width, int height, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
private void ProcessSolidColor(byte r, byte g, byte b, int width, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
{

OpenRGB.NET.Color processedColor = FastApplyEffects(r, g, b, brightness, vibrance, contrast, darkThreshold, darkFactor);

bool needsFade = hasPreviousFrame && !(lastFrameWasSolid &&
lastSolidR == processedColor.R &&
lastSolidG == processedColor.G &&
lastSolidB == processedColor.B);

if (needsFade)
{

double fadeFactor = 0.95;

Parallel.For(0, width, i => {
resultBuffer[i] = FastBlendColors(previousFrame[i], processedColor, fadeFactor);
});
}
else
{

for (int i = 0; i < width; i++)
{
resultBuffer[i] = processedColor;
}
}

lastSolidR = processedColor.R;
lastSolidG = processedColor.G;
lastSolidB = processedColor.B;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool AreSettingsCached(double brightness, double contrast)
{
return Math.Abs(lastBrightness - brightness) <= 0.001 && Math.Abs(lastContrast - contrast) <= 0.001;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void StoreFrameState(int width, bool isSolidColor)
{

Array.Copy(resultBuffer, previousFrame, width);
hasPreviousFrame = true;
lastFrameWasSolid = isSolidColor;
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private bool IsSolidColorFrame()
{
if (rawColors.Length < 2) return true;

OpenRGB.NET.Color first = rawColors[0];
const int tolerance = 5;

int[] samplePoints = { 0, rawColors.Length / 3, rawColors.Length / 2, (rawColors.Length * 2) / 3, rawColors.Length - 1 };

foreach (int i in samplePoints)
{
if (i == 0) continue;

if (Math.Abs(first.R - rawColors[i].R) > tolerance ||
Math.Abs(first.G - rawColors[i].G) > tolerance ||
Math.Abs(first.B - rawColors[i].B) > tolerance)
{
return false;
}
}

return true;
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void ApplyFading(int width, double fadeFactor)
{
Parallel.For(0, width, i => {
resultBuffer[i] = FastBlendColors(previousFrame[i], resultBuffer[i], fadeFactor);
});
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private OpenRGB.NET.Color FastBlendColors(OpenRGB.NET.Color color1, OpenRGB.NET.Color color2, double factor)
{

factor = Math.Clamp(factor, 0.0, 1.0);
double inverseFactor = 1.0 - factor;

byte r = (byte)(color1.R * inverseFactor + color2.R * factor);
byte g = (byte)(color1.G * inverseFactor + color2.G * factor);
byte b = (byte)(color1.B * inverseFactor + color2.B * factor);

return new OpenRGB.NET.Color(r, g, b);
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void ExtractColumns(int stride, int width, int height, int bytesPerPixel)
{
if (bytesPerPixel == 4)
ExtractColumns32Bpp(stride, width, height);
else
ExtractColumns24Bpp(stride, width, height);
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void ExtractColumns24Bpp(int stride, int width, int height)
{
Parallel.For(0, width, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, x =>
{
Expand All @@ -110,7 +250,20 @@
int pixelCount = height;
int columnOffset = x * 3;

for (int y = 0; y < height; y++)
int y = 0;
for (; y < height - 3; y += 4)
{
int offset1 = y * stride + columnOffset;
int offset2 = (y + 1) * stride + columnOffset;
int offset3 = (y + 2) * stride + columnOffset;
int offset4 = (y + 3) * stride + columnOffset;

totalB += (uint)pixelBuffer[offset1] + pixelBuffer[offset2] + pixelBuffer[offset3] + pixelBuffer[offset4];
totalG += (uint)pixelBuffer[offset1 + 1] + pixelBuffer[offset2 + 1] + pixelBuffer[offset3 + 1] + pixelBuffer[offset4 + 1];
totalR += (uint)pixelBuffer[offset1 + 2] + pixelBuffer[offset2 + 2] + pixelBuffer[offset3 + 2] + pixelBuffer[offset4 + 2];
}

for (; y < height; y++)
{
int offset = y * stride + columnOffset;
totalB += pixelBuffer[offset];
Expand All @@ -122,13 +275,13 @@
byte avgG = (byte)(totalG / pixelCount);
byte avgB = (byte)(totalB / pixelCount);

resultBuffer[x] = FastApplyEffects(avgR, avgG, avgB, brightness, vibrance, contrast, darkThreshold, darkFactor);
rawColors[x] = new OpenRGB.NET.Color(avgR, avgG, avgB);
}
});
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void ProcessColumns32Bpp(int stride, int width, int height, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
private void ExtractColumns32Bpp(int stride, int width, int height)
{
Parallel.For(0, width, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, x =>
{
Expand All @@ -138,7 +291,20 @@
int pixelCount = height;
int columnOffset = x * 4;

for (int y = 0; y < height; y++)
int y = 0;
for (; y < height - 3; y += 4)
{
int offset1 = y * stride + columnOffset;
int offset2 = (y + 1) * stride + columnOffset;
int offset3 = (y + 2) * stride + columnOffset;
int offset4 = (y + 3) * stride + columnOffset;

totalB += (uint)(pixelBuffer[offset1] + pixelBuffer[offset2] + pixelBuffer[offset3] + pixelBuffer[offset4]);
totalG += (uint)pixelBuffer[offset1 + 1] + pixelBuffer[offset2 + 1] + pixelBuffer[offset3 + 1] + pixelBuffer[offset4 + 1];
totalR += (uint)pixelBuffer[offset1 + 2] + pixelBuffer[offset2 + 2] + pixelBuffer[offset3 + 2] + pixelBuffer[offset4 + 2];
}

for (; y < height; y++)
{
int offset = y * stride + columnOffset;
totalB += pixelBuffer[offset];
Expand All @@ -150,23 +316,24 @@
byte avgG = (byte)(totalG / pixelCount);
byte avgB = (byte)(totalB / pixelCount);

resultBuffer[x] = FastApplyEffects(avgR, avgG, avgB, brightness, vibrance, contrast, darkThreshold, darkFactor);
rawColors[x] = new OpenRGB.NET.Color(avgR, avgG, avgB);
}
});
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private OpenRGB.NET.Color FastApplyEffects(byte r, byte g, byte b, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
private void ProcessColumnsWithEffects(int width, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
{

if (!lutsInitialized || Math.Abs(lastBrightness - brightness) > 0.001 || Math.Abs(lastContrast - contrast) > 0.001)
Parallel.For(0, width, x =>
{
InitializeLuts(brightness, contrast);
lastBrightness = brightness;
lastContrast = contrast;
lutsInitialized = true;
}
resultBuffer[x] = FastApplyEffects(rawColors[x].R, rawColors[x].G, rawColors[x].B,
brightness, vibrance, contrast, darkThreshold, darkFactor);
});
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private OpenRGB.NET.Color FastApplyEffects(byte r, byte g, byte b, double brightness, double vibrance, double contrast, int darkThreshold, double darkFactor)
{
unchecked
{

Expand Down Expand Up @@ -217,29 +384,28 @@
bVal = bVal < darkThreshold ? bDark : bVal;
}

return new OpenRGB.NET.Color((byte)rVal, (byte)gVal, (byte)bVal);
return new OpenRGB.NET.Color(
(byte)Math.Min(Math.Max(rVal, 0), 255),
(byte)Math.Min(Math.Max(gVal, 0), 255),
(byte)Math.Min(Math.Max(bVal, 0), 255)
);
}
}

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void InitializeLuts(double brightness, double contrast)
{
if (brightnessLut == null)
{
brightnessLut = new byte[256];
contrastLut = new byte[256];
}

for (int i = 0; i < 256; i++)
{

int brightVal = (int)(i * brightness);
brightnessLut[i] = (byte)Math.Min(brightVal, 255);
brightnessLut[i] = (byte)Math.Min(Math.Max(brightVal, 0), 255);

if (Math.Abs(contrast - 1.0) > 0.001)
{
double normalized = i / 255.0;
int contrastVal = (int)(Math.Pow(normalized, contrast) * 255.0);
contrastLut[i] = (byte)Math.Min(Math.Max(contrastVal, 0), 255);
double adjusted = Math.Pow(normalized, contrast) * 255.0;
contrastLut[i] = (byte)Math.Min(Math.Max((int)adjusted, 0), 255);
}
else
{
Expand All @@ -252,9 +418,9 @@
{
pixelBuffer = null;
resultBuffer = null;
rawColors = null;
previousFrame = null;
brightnessLut = null;
contrastLut = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
3 changes: 2 additions & 1 deletion KeyLighting/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ static void Main(string[] args)
fadeProgress = new float[ledCount];
targetColors = new ORGBColor[ledCount];

using var processor = new CPUImageProcessor();
var processor = new CPUImageProcessor(config);

using var capturer = ConfigureScreenCapturer(config);

var updateTimer = new System.Timers.Timer(config.UpdateDelayMs);
Expand Down
2 changes: 1 addition & 1 deletion KeyLighting/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"brightnessMultiplier": 1.8,
"vibranceFactor": 1,
"contrastPower": 1.8,
"fadeFactor": 3,
"fadeFactor": 0.9,
"darkenThreshold": 65,
"darkenFactor": 0.4,
"wasdEnabled": false,
Expand Down
Loading
Loading