Create contentClassifier.py
Browse files- contentClassifier.py +176 -0
contentClassifier.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# contentClassifier.py - Lightweight Content Detection
|
| 2 |
+
# FROZEN - DO NOT MODIFY
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image
|
| 6 |
+
from collections import Counter
|
| 7 |
+
|
| 8 |
+
class ContentClassifier:
|
| 9 |
+
"""Lightweight content detection - FROZEN."""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.categories = [
|
| 13 |
+
'simple_graphic', 'human_hair', 'human', 'anime',
|
| 14 |
+
'logo_icon', 'product_white_bg', 'general_photo', 'complex'
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
def classify(self, image: Image.Image) -> dict:
|
| 18 |
+
"""Classify image content."""
|
| 19 |
+
if image.mode != 'RGB':
|
| 20 |
+
image = image.convert('RGB')
|
| 21 |
+
|
| 22 |
+
small = image.copy()
|
| 23 |
+
small.thumbnail((150, 150), Image.Resampling.LANCZOS)
|
| 24 |
+
np_img = np.array(small)
|
| 25 |
+
h, w = np_img.shape[:2]
|
| 26 |
+
|
| 27 |
+
signals = {
|
| 28 |
+
'dimensions': (w, h),
|
| 29 |
+
'aspect_ratio': h / w if w > 0 else 0,
|
| 30 |
+
'border_uniformity': self._border_uniformity(np_img),
|
| 31 |
+
'edge_density': self._edge_density(np_img),
|
| 32 |
+
'color_complexity': self._color_complexity(small),
|
| 33 |
+
'skin_score': self._skin_score(np_img),
|
| 34 |
+
'border_connected': self._border_connected(np_img),
|
| 35 |
+
'texture': self._texture(np_img)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
category = self._classify(signals)
|
| 39 |
+
confidence = self._calculate_confidence(category, signals)
|
| 40 |
+
|
| 41 |
+
return {
|
| 42 |
+
'category': category,
|
| 43 |
+
'confidence': confidence,
|
| 44 |
+
'signals': signals
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
def _border_uniformity(self, np_img: np.ndarray) -> float:
|
| 48 |
+
h, w = np_img.shape[:2]
|
| 49 |
+
border_pixels = []
|
| 50 |
+
step = max(1, min(h, w) // 10)
|
| 51 |
+
|
| 52 |
+
for x in range(0, w, step):
|
| 53 |
+
border_pixels.append(tuple(np_img[0, x][:3]))
|
| 54 |
+
border_pixels.append(tuple(np_img[h-1, x][:3]))
|
| 55 |
+
for y in range(0, h, step):
|
| 56 |
+
border_pixels.append(tuple(np_img[y, 0][:3]))
|
| 57 |
+
border_pixels.append(tuple(np_img[y, w-1][:3]))
|
| 58 |
+
|
| 59 |
+
if not border_pixels:
|
| 60 |
+
return 0.0
|
| 61 |
+
|
| 62 |
+
unique = len(set(border_pixels))
|
| 63 |
+
return 1 - (unique / len(border_pixels))
|
| 64 |
+
|
| 65 |
+
def _edge_density(self, np_img: np.ndarray) -> float:
|
| 66 |
+
gray = np.mean(np_img, axis=2).astype(np.float32)
|
| 67 |
+
grad_x = np.abs(gray[:, 1:] - gray[:, :-1])
|
| 68 |
+
grad_y = np.abs(gray[1:, :] - gray[:-1, :])
|
| 69 |
+
|
| 70 |
+
edges = (grad_x > 25).sum() + (grad_y > 25).sum()
|
| 71 |
+
total = (gray.shape[0] - 1) * gray.shape[1] + gray.shape[0] * (gray.shape[1] - 1)
|
| 72 |
+
|
| 73 |
+
return edges / total if total > 0 else 0
|
| 74 |
+
|
| 75 |
+
def _color_complexity(self, image: Image.Image) -> float:
|
| 76 |
+
quantized = image.quantize(colors=32)
|
| 77 |
+
unique = len(quantized.getcolors())
|
| 78 |
+
return min(1.0, unique / 32)
|
| 79 |
+
|
| 80 |
+
def _skin_score(self, np_img: np.ndarray) -> float:
|
| 81 |
+
h, w = np_img.shape[:2]
|
| 82 |
+
pixels = []
|
| 83 |
+
step = max(1, min(h, w) // 5)
|
| 84 |
+
for y in range(0, h, step):
|
| 85 |
+
for x in range(0, w, step):
|
| 86 |
+
pixels.append(np_img[y, x][:3])
|
| 87 |
+
|
| 88 |
+
skin = 0
|
| 89 |
+
for r, g, b in pixels:
|
| 90 |
+
if (r > 60 and g > 40 and b > 20 and
|
| 91 |
+
r > g and r > b and
|
| 92 |
+
abs(r - g) < 60 and abs(r - b) < 60):
|
| 93 |
+
skin += 1
|
| 94 |
+
|
| 95 |
+
return skin / len(pixels) if pixels else 0
|
| 96 |
+
|
| 97 |
+
def _border_connected(self, np_img: np.ndarray) -> float:
|
| 98 |
+
h, w = np_img.shape[:2]
|
| 99 |
+
border_colors = set()
|
| 100 |
+
border_colors.add(tuple(np_img[0, 0][:3]))
|
| 101 |
+
border_colors.add(tuple(np_img[0, w-1][:3]))
|
| 102 |
+
border_colors.add(tuple(np_img[h-1, 0][:3]))
|
| 103 |
+
border_colors.add(tuple(np_img[h-1, w-1][:3]))
|
| 104 |
+
|
| 105 |
+
interior_colors = set()
|
| 106 |
+
step = max(1, min(h, w) // 4)
|
| 107 |
+
for y in range(h//4, 3*h//4, step):
|
| 108 |
+
for x in range(w//4, 3*w//4, step):
|
| 109 |
+
interior_colors.add(tuple(np_img[y, x][:3]))
|
| 110 |
+
|
| 111 |
+
overlap = border_colors & interior_colors
|
| 112 |
+
return len(overlap) / max(len(border_colors), 1)
|
| 113 |
+
|
| 114 |
+
def _texture(self, np_img: np.ndarray) -> float:
|
| 115 |
+
gray = np.mean(np_img, axis=2).astype(np.float32)
|
| 116 |
+
variance = np.var(gray)
|
| 117 |
+
return min(1.0, variance / 5000)
|
| 118 |
+
|
| 119 |
+
def _classify(self, signals: dict) -> str:
|
| 120 |
+
s = signals
|
| 121 |
+
|
| 122 |
+
# Simple graphic
|
| 123 |
+
if (s['color_complexity'] < 0.3 and
|
| 124 |
+
s['edge_density'] > 0.3 and
|
| 125 |
+
s['border_uniformity'] > 0.7):
|
| 126 |
+
return 'simple_graphic'
|
| 127 |
+
|
| 128 |
+
# Human with hair
|
| 129 |
+
if (s['skin_score'] > 0.15 and
|
| 130 |
+
s['texture'] > 0.3 and
|
| 131 |
+
s['edge_density'] > 0.2):
|
| 132 |
+
return 'human_hair'
|
| 133 |
+
|
| 134 |
+
# Human
|
| 135 |
+
if s['skin_score'] > 0.1:
|
| 136 |
+
return 'human'
|
| 137 |
+
|
| 138 |
+
# Anime
|
| 139 |
+
if (s['skin_score'] < 0.08 and
|
| 140 |
+
s['color_complexity'] > 0.3 and
|
| 141 |
+
s['texture'] < 0.3 and
|
| 142 |
+
s['edge_density'] > 0.3):
|
| 143 |
+
return 'anime'
|
| 144 |
+
|
| 145 |
+
# Logo/icon
|
| 146 |
+
if (s['edge_density'] > 0.4 and
|
| 147 |
+
s['color_complexity'] < 0.3 and
|
| 148 |
+
s['border_uniformity'] > 0.6):
|
| 149 |
+
return 'logo_icon'
|
| 150 |
+
|
| 151 |
+
# Product on white bg
|
| 152 |
+
if (s['border_uniformity'] > 0.6 and
|
| 153 |
+
s['color_complexity'] < 0.5 and
|
| 154 |
+
s['edge_density'] < 0.4):
|
| 155 |
+
return 'product_white_bg'
|
| 156 |
+
|
| 157 |
+
# General photo
|
| 158 |
+
if s['color_complexity'] > 0.3:
|
| 159 |
+
return 'general_photo'
|
| 160 |
+
|
| 161 |
+
return 'complex'
|
| 162 |
+
|
| 163 |
+
def _calculate_confidence(self, category: str, signals: dict) -> float:
|
| 164 |
+
base = 0.7
|
| 165 |
+
|
| 166 |
+
if category == 'simple_graphic':
|
| 167 |
+
if signals['border_uniformity'] > 0.8:
|
| 168 |
+
base += 0.2
|
| 169 |
+
elif category == 'human_hair':
|
| 170 |
+
if signals['skin_score'] > 0.2 and signals['texture'] > 0.4:
|
| 171 |
+
base += 0.2
|
| 172 |
+
elif category == 'human':
|
| 173 |
+
if signals['skin_score'] > 0.15:
|
| 174 |
+
base += 0.2
|
| 175 |
+
|
| 176 |
+
return min(1.0, base)
|