# contentClassifier.py - Lightweight Content Detection # FROZEN - DO NOT MODIFY # NOTE: Thresholds tuned 2026-01 to fix edge_density ratios. # A red circle at 150x150 has edge_density ~0.02-0.05, NOT 0.4. import numpy as np from PIL import Image from collections import Counter class ContentClassifier: """Lightweight content detection - FROZEN.""" def __init__(self): self.categories = [ 'simple_graphic', 'human_hair', 'human', 'anime', 'logo_icon', 'product_white_bg', 'general_photo', 'complex' ] def classify(self, image: Image.Image) -> dict: """Classify image content.""" if image.mode != 'RGB': image = image.convert('RGB') small = image.copy() small.thumbnail((150, 150), Image.Resampling.LANCZOS) np_img = np.array(small) h, w = np_img.shape[:2] signals = { 'dimensions': (w, h), 'aspect_ratio': h / w if w > 0 else 0, 'border_uniformity': self._border_uniformity(np_img), 'edge_density': self._edge_density(np_img), 'color_complexity': self._color_complexity(small), 'skin_score': self._skin_score(np_img), 'border_connected': self._border_connected(np_img), 'texture': self._texture(np_img) } category = self._classify(signals) confidence = self._calculate_confidence(category, signals) return { 'category': category, 'confidence': confidence, 'signals': signals } def _border_uniformity(self, np_img: np.ndarray) -> float: h, w = np_img.shape[:2] border_pixels = [] step = max(1, min(h, w) // 10) for x in range(0, w, step): border_pixels.append(tuple(np_img[0, x][:3])) border_pixels.append(tuple(np_img[h - 1, x][:3])) for y in range(0, h, step): border_pixels.append(tuple(np_img[y, 0][:3])) border_pixels.append(tuple(np_img[y, w - 1][:3])) if not border_pixels: return 0.0 unique = len(set(border_pixels)) return 1 - (unique / len(border_pixels)) def _edge_density(self, np_img: np.ndarray) -> float: gray = np.mean(np_img, axis=2).astype(np.float32) grad_x = np.abs(gray[:, 1:] - gray[:, :-1]) grad_y = np.abs(gray[1:, :] - gray[:-1, :]) edges = (grad_x > 25).sum() + (grad_y > 25).sum() total = (gray.shape[0] - 1) * gray.shape[1] + gray.shape[0] * (gray.shape[1] - 1) return edges / total if total > 0 else 0 def _color_complexity(self, image: Image.Image) -> float: quantized = image.quantize(colors=32) unique = len(quantized.getcolors()) return min(1.0, unique / 32) def _skin_score(self, np_img: np.ndarray) -> float: h, w = np_img.shape[:2] pixels = [] step = max(1, min(h, w) // 5) for y in range(0, h, step): for x in range(0, w, step): pixels.append(np_img[y, x][:3]) skin = 0 for r, g, b in pixels: if (r > 60 and g > 40 and b > 20 and r > g and r > b and abs(r - g) < 60 and abs(r - b) < 60): skin += 1 return skin / len(pixels) if pixels else 0 def _border_connected(self, np_img: np.ndarray) -> float: h, w = np_img.shape[:2] border_colors = set() border_colors.add(tuple(np_img[0, 0][:3])) border_colors.add(tuple(np_img[0, w - 1][:3])) border_colors.add(tuple(np_img[h - 1, 0][:3])) border_colors.add(tuple(np_img[h - 1, w - 1][:3])) interior_colors = set() step = max(1, min(h, w) // 4) for y in range(h // 4, 3 * h // 4, step): for x in range(w // 4, 3 * w // 4, step): interior_colors.add(tuple(np_img[y, x][:3])) overlap = border_colors & interior_colors return len(overlap) / max(len(border_colors), 1) def _texture(self, np_img: np.ndarray) -> float: gray = np.mean(np_img, axis=2).astype(np.float32) variance = np.var(gray) return min(1.0, variance / 5000) def _classify(self, signals: dict) -> str: s = signals # ===== SIMPLE GRAPHIC (circles, shapes, solid-background subjects) ===== # Lowered edge_density thresholds so simple shapes route here. # NOTE: 0.4 threshold was never reached on typical images. if (s['color_complexity'] < 0.35 and s['edge_density'] > 0.05 and s['border_uniformity'] > 0.6): return 'simple_graphic' # ===== LOGO / ICON (flat, low-colour, high-contrast) ===== if (s['color_complexity'] < 0.35 and s['edge_density'] > 0.10 and s['border_uniformity'] > 0.5 and s['texture'] < 0.3): return 'logo_icon' # ===== HUMAN WITH HAIR ===== if (s['skin_score'] > 0.12 and s['texture'] > 0.25 and s['edge_density'] > 0.04): return 'human_hair' # ===== HUMAN (SIMPLE) ===== if s['skin_score'] > 0.08: return 'human' # ===== ANIME ===== if (s['skin_score'] < 0.10 and s['color_complexity'] > 0.25 and s['texture'] < 0.35 and s['edge_density'] > 0.08): return 'anime' # ===== PRODUCT ON WHITE BG ===== if (s['border_uniformity'] > 0.55 and s['color_complexity'] < 0.55 and s['edge_density'] < 0.35): return 'product_white_bg' # ===== GENERAL PHOTO ===== if s['color_complexity'] > 0.3: return 'general_photo' return 'complex' def _calculate_confidence(self, category: str, signals: dict) -> float: base = 0.7 if category == 'simple_graphic': if signals['border_uniformity'] > 0.8: base += 0.2 elif category == 'human_hair': if signals['skin_score'] > 0.2 and signals['texture'] > 0.4: base += 0.2 elif category == 'human': if signals['skin_score'] > 0.15: base += 0.2 elif category == 'logo_icon': if signals['border_uniformity'] > 0.7: base += 0.15 elif category == 'product_white_bg': if signals['border_uniformity'] > 0.7: base += 0.15 return min(1.0, base)