feat(fc-cleanup): audits/single_color.py + tests — Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""Single-color audit: matches images where one color dominates beyond
|
||||
the threshold (within the given Euclidean RGB tolerance). The first
|
||||
canonical implementation — the import-side filter (SkipReason.single_color)
|
||||
was never wired; FC-Cleanup's audit module is the source of truth and a
|
||||
future spec can adopt it on the import path too.
|
||||
"""
|
||||
|
||||
from PIL import Image
|
||||
|
||||
_THUMB_SIZE = (64, 64)
|
||||
|
||||
|
||||
def evaluate(
|
||||
pil_image,
|
||||
*,
|
||||
threshold: float,
|
||||
tolerance: int,
|
||||
) -> bool:
|
||||
"""True iff the fraction of pixels within `tolerance` (Euclidean RGB
|
||||
distance) of the dominant color exceeds `threshold`.
|
||||
|
||||
Downsamples to 64x64 for speed (~4ms regardless of source size).
|
||||
Alpha channels are stripped; only RGB is considered. Animated images
|
||||
use frame 0 (PIL's default after Image.open without seek).
|
||||
"""
|
||||
im = pil_image
|
||||
if im.mode == "RGBA":
|
||||
im = im.convert("RGB")
|
||||
elif im.mode not in ("RGB", "L"):
|
||||
im = im.convert("RGB")
|
||||
if im.size != _THUMB_SIZE:
|
||||
im = im.resize(_THUMB_SIZE, Image.Resampling.BILINEAR)
|
||||
pixels = list(im.getdata())
|
||||
if not pixels:
|
||||
return False
|
||||
# Normalize L-mode pixels to RGB tuples for distance math.
|
||||
if isinstance(pixels[0], int):
|
||||
pixels = [(p, p, p) for p in pixels]
|
||||
# Dominant color = mean RGB.
|
||||
n = len(pixels)
|
||||
sum_r = sum(p[0] for p in pixels)
|
||||
sum_g = sum(p[1] for p in pixels)
|
||||
sum_b = sum(p[2] for p in pixels)
|
||||
dom = (sum_r / n, sum_g / n, sum_b / n)
|
||||
tol_sq = tolerance * tolerance
|
||||
within = 0
|
||||
for r, g, b in pixels:
|
||||
dr = r - dom[0]
|
||||
dg = g - dom[1]
|
||||
db = b - dom[2]
|
||||
if dr * dr + dg * dg + db * db <= tol_sq:
|
||||
within += 1
|
||||
return (within / n) > threshold
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Tests for the single-color audit rule.
|
||||
|
||||
The rule downsamples + measures the fraction of pixels within `tolerance`
|
||||
(Euclidean RGB distance) of the dominant color. Matches if that fraction
|
||||
exceeds `threshold`. Single-color content is typically uploaded by
|
||||
mistake (placeholder/error/preview images) and should be flagged.
|
||||
"""
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from backend.app.services.audits import single_color
|
||||
|
||||
|
||||
def test_single_color_evaluate_true_for_uniform_image():
|
||||
im = Image.new("RGB", (50, 50), (128, 64, 200))
|
||||
assert single_color.evaluate(im, threshold=0.9, tolerance=10) is True
|
||||
|
||||
|
||||
def test_single_color_evaluate_false_for_diverse_image():
|
||||
# Half black, half white — no single color dominates.
|
||||
im = Image.new("RGB", (50, 50), (0, 0, 0))
|
||||
for x in range(25):
|
||||
for y in range(50):
|
||||
im.putpixel((x, y), (255, 255, 255))
|
||||
assert single_color.evaluate(im, threshold=0.9, tolerance=10) is False
|
||||
|
||||
|
||||
def test_single_color_evaluate_respects_tolerance_widening():
|
||||
# Gradient image: pixels span 0..50 in R channel. Tight tolerance
|
||||
# rejects (no concentration), wide tolerance accepts (all near 25).
|
||||
im = Image.new("RGB", (50, 50), (0, 0, 0))
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
im.putpixel((x, y), (x, 0, 0))
|
||||
assert single_color.evaluate(im, threshold=0.9, tolerance=5) is False
|
||||
assert single_color.evaluate(im, threshold=0.9, tolerance=50) is True
|
||||
|
||||
|
||||
def test_single_color_evaluate_handles_rgba_input():
|
||||
# Alpha channel should be ignored — only RGB matters for the rule.
|
||||
im = Image.new("RGBA", (50, 50), (100, 100, 100, 128))
|
||||
assert single_color.evaluate(im, threshold=0.9, tolerance=10) is True
|
||||
Reference in New Issue
Block a user