Skip to content

Commit 4854237

Browse files
authored
Stronger username visibility adjustment against colored backgrounds (#1555)
* Fix hue adjust detection not wrapping around for extremely low/high hues * Increase username hue adjust threshold * Add test render case for ChatRenderer.AdjustColorVisibility * Add proper asserts to ColorVisibilityTests.RenderTestPattern * Forgot .Index() is .NET 7+
1 parent 22d078c commit 4854237

File tree

2 files changed

+138
-10
lines changed

2 files changed

+138
-10
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
using SkiaSharp;
4+
5+
namespace TwitchDownloaderCore.Tests.ToolTests
6+
{
7+
// ReSharper disable once InconsistentNaming
8+
public class ColorVisibilityTests
9+
{
10+
// AdjustColorVisibility should be extracted and made public at some point, but for now just use reflection
11+
private static readonly MethodInfo AdjustVisibilityMethodInfo = typeof(ChatRenderer).GetMethod("AdjustColorVisibility", BindingFlags.NonPublic | BindingFlags.Static)!;
12+
private static readonly Func<SKColor, SKColor, SKColor> AdjustVisibilityDelegate = (Func<SKColor, SKColor, SKColor>)Delegate.CreateDelegate(typeof(Func<SKColor, SKColor, SKColor>), AdjustVisibilityMethodInfo);
13+
14+
[Fact]
15+
public void RenderTestPattern()
16+
{
17+
const int HUE_LEN = 360; // Hue sweep length
18+
const int SAT_LEN = 100; // Sweep height
19+
const int TILE_W = HUE_LEN + 40; // Tile width
20+
const int TILE_H = SAT_LEN + 40; // Tile height
21+
const int TILE_X_OFFSET = (TILE_W - HUE_LEN) / 2; // Tile inner-sweep X offset
22+
const int TILE_Y_OFFSET = (TILE_H - SAT_LEN) / 2; // Tile inner-sweep Y offset
23+
24+
var imageInfo = new SKImageInfo(TILE_W * 6, TILE_H * 2);
25+
using var bitmap = new SKBitmap(imageInfo);
26+
using var canvas = new SKCanvas(bitmap);
27+
using var paint = new SKPaint();
28+
paint.BlendMode = SKBlendMode.Src;
29+
30+
var backgrounds = new[] { SKColors.Black, SKColors.White, SKColors.Gray, SKColors.Red, SKColors.Blue, SKColors.Lime };
31+
for (var i = 0; i < backgrounds.Length; i++)
32+
{
33+
var background = backgrounds[i];
34+
var tileStartX = TILE_W * i;
35+
DrawTile(tileStartX, TILE_H * 0, background, false); // Row 0 (reference)
36+
DrawTile(tileStartX, TILE_H * 1, background, true); // Row 1 (adjusted)
37+
38+
background.ToHsv(out var bgHue, out var bgSat, out _);
39+
if (bgSat > 28)
40+
{
41+
AssertPixelMatrix(tileStartX, bgHue, background);
42+
}
43+
}
44+
45+
// WriteDebugBitmap(bitmap);
46+
47+
return;
48+
49+
void DrawTile(int startX, int startY, SKColor background, bool adjust)
50+
{
51+
paint.Color = background;
52+
canvas.DrawRect(startX, startY, TILE_W, TILE_H, paint);
53+
54+
// Hue sweep
55+
for (var x = 0; x < HUE_LEN; x++)
56+
for (var y = 0; y < SAT_LEN; y++)
57+
{
58+
var color = SKColor.FromHsv(x, y, 100);
59+
if (adjust) color = AdjustVisibilityDelegate(color, background);
60+
61+
paint.Color = color;
62+
canvas.DrawPoint(TILE_X_OFFSET + startX + x, TILE_Y_OFFSET + startY + y, paint);
63+
}
64+
65+
// Value sweep
66+
for (var x = startX + TILE_X_OFFSET - 3; x < startX + TILE_X_OFFSET; x++)
67+
for (var y = 0; y < SAT_LEN; y++)
68+
{
69+
var color = SKColor.FromHsv(0, 0, 100 - y);
70+
if (adjust) color = AdjustVisibilityDelegate(color, background);
71+
72+
paint.Color = color;
73+
canvas.DrawPoint(x, TILE_Y_OFFSET + startY + y, paint);
74+
}
75+
}
76+
77+
void AssertPixelMatrix(int startX, float bgHue, SKColor background)
78+
{
79+
const int MATRIX_W = 9;
80+
const int MATRIX_H = 9;
81+
for (var x = -MATRIX_W; x <= MATRIX_W; x++)
82+
for (var y = -MATRIX_H; y <= MATRIX_H; y++)
83+
{
84+
var x2 = x + (int)bgHue;
85+
if (x2 < 0) x2 += HUE_LEN;
86+
x2 %= HUE_LEN;
87+
88+
var y2 = y + SAT_LEN / 2;
89+
if (y2 < 0) y2 += SAT_LEN;
90+
y2 %= SAT_LEN;
91+
92+
var sourcePx = bitmap.GetPixel(
93+
TILE_X_OFFSET + startX + x2,
94+
TILE_Y_OFFSET + TILE_H * 0 + y2
95+
);
96+
var actualPx = bitmap.GetPixel(
97+
TILE_X_OFFSET + startX + x2,
98+
TILE_Y_OFFSET + TILE_H * 1 + y2
99+
);
100+
101+
Assert.NotEqual(background, actualPx);
102+
Assert.NotEqual(sourcePx, actualPx);
103+
}
104+
}
105+
}
106+
107+
private static void WriteDebugBitmap(SKBitmap bitmap, string nameSuffix = "Output", [CallerFilePath] string filePath = "", [CallerMemberName] string methodName = "")
108+
{
109+
using var fs = new FileStream($"{Path.GetFileNameWithoutExtension(filePath)}.{methodName}_{nameSuffix}.png", FileMode.Create);
110+
bitmap.Encode(fs, SKEncodedImageFormat.Png, 100);
111+
}
112+
}
113+
}

TwitchDownloaderCore/ChatRenderer.cs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,16 +1543,31 @@ private static SKColor AdjustColorVisibility(SKColor foreground, SKColor backgro
15431543
// Adjust hue on colored backgrounds
15441544
if (bgSat > 28 && fgSat > 28)
15451545
{
1546-
var hueDiff = fgHue - bgHue;
1547-
const int HUE_THRESHOLD = 25;
1548-
if (Math.Abs(hueDiff) < HUE_THRESHOLD)
1549-
{
1550-
var diffSign = hueDiff < 0 ? -1 : 1; // Math.Sign returns 1, -1, or 0. We only want 1 or -1.
1551-
fgHue = bgHue + HUE_THRESHOLD * diffSign;
1552-
1553-
if (fgHue < 0) fgHue += 360;
1554-
fgHue %= 360;
1555-
}
1546+
const float HUE_WIDTH = 360;
1547+
const int ADJUST_THRESHOLD = 35;
1548+
Debug.Assert(ADJUST_THRESHOLD < HUE_WIDTH / 2);
1549+
1550+
// Compute computer lower and higher hue diff to ensure we wrap around for reds
1551+
// hue: [||||||||||]
1552+
// not red: [ ^ ] ^
1553+
// upper red: ^ [ ^ ]
1554+
// lower red: [^ ]^
1555+
var hueDiff1 = fgHue - (bgHue > HUE_WIDTH / 2 ? bgHue - HUE_WIDTH : bgHue);
1556+
var hueDiff2 = fgHue - (bgHue > HUE_WIDTH / 2 ? bgHue : bgHue + HUE_WIDTH);
1557+
1558+
// Take smallest diff, or skip if both are >ADJUST_THRESHOLD
1559+
float hueDiff;
1560+
if (Math.Abs(hueDiff1) <= ADJUST_THRESHOLD) hueDiff = hueDiff1;
1561+
else if (Math.Abs(hueDiff2) <= ADJUST_THRESHOLD) hueDiff = hueDiff2;
1562+
else goto SkipHueAdjust;
1563+
1564+
var diffSign = hueDiff < 0 ? -1 : 1; // Math.Sign returns 1, -1, or 0. We only want 1 or -1.
1565+
fgHue = bgHue + ADJUST_THRESHOLD * diffSign;
1566+
1567+
if (fgHue < 0) fgHue += HUE_WIDTH;
1568+
fgHue %= HUE_WIDTH;
1569+
1570+
SkipHueAdjust: ;
15561571
}
15571572

15581573
return SKColor.FromHsl(fgHue, Math.Min(fgSat, 90), fgLight);

0 commit comments

Comments
 (0)