bgremove / contentClassifier.py
andevs's picture
Update contentClassifier.py
32576ad verified
Raw History Blame Contribute Delete
6.59 kB
# 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)