Download src/dissection.py from Mindykkyan/Aesthetic_Dissection_Panel: direct link, hf CLI and curl.
- Browser
- Download file 6.45 kB
-
https://huggingface.co/spaces/Mindykkyan/Aesthetic_Dissection_Panel/resolve/main/src/dissection.py
- Command line
-
hf download hf://spaces/Mindykkyan/Aesthetic_Dissection_Panel/src/dissection.py
-
curl -L -o dissection.py https://huggingface.co/spaces/Mindykkyan/Aesthetic_Dissection_Panel/resolve/main/src/dissection.py
6.45 kB
| """ | |
| Dissection proxies — defensible, computable aesthetic dimensions (0–1 bars). | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import numpy as np | |
| from PIL import Image | |
| try: | |
| import cv2 | |
| _HAS_CV2 = True | |
| except ImportError: | |
| _HAS_CV2 = False | |
| def _rgb_array(img: Image.Image) -> np.ndarray: | |
| return np.array(img.convert("RGB"), dtype=np.float32) / 255.0 | |
| def _hsv_arrays(img: Image.Image): | |
| arr = _rgb_array(img) | |
| h, w, _ = arr.shape | |
| maxc = np.maximum(np.maximum(arr[:, :, 0], arr[:, :, 1]), arr[:, :, 2]) | |
| minc = np.minimum(np.minimum(arr[:, :, 0], arr[:, :, 1]), arr[:, :, 2]) | |
| delta = maxc - minc | |
| sat = delta / (maxc + 1e-6) | |
| hue = np.zeros_like(maxc) | |
| mask = delta > 1e-6 | |
| r, g, b = arr[:, :, 0], arr[:, :, 1], arr[:, :, 2] | |
| hue[mask & (maxc == r)] = ((g - b) / (delta + 1e-6))[mask & (maxc == r)] % 6 | |
| hue[mask & (maxc == g)] = ((b - r) / (delta + 1e-6) + 2)[mask & (maxc == g)] | |
| hue[mask & (maxc == b)] = ((r - g) / (delta + 1e-6) + 4)[mask & (maxc == b)] | |
| hue = hue / 6.0 | |
| val = maxc | |
| return hue, sat, val, arr | |
| def _clip01(x: float) -> float: | |
| return float(max(0.0, min(1.0, x))) | |
| def _color_harmony(img: Image.Image) -> dict: | |
| """HSV hue histogram spread (entropy) × luminance contrast ratio proxy.""" | |
| hue, sat, val, arr = _hsv_arrays(img) | |
| active = sat > 0.08 | |
| if active.sum() < 10: | |
| spread = 0.3 | |
| else: | |
| h_active = hue[active] | |
| hist, _ = np.histogram(h_active, bins=36, range=(0, 1), density=True) | |
| hist = hist + 1e-8 | |
| entropy = -float(np.sum(hist * np.log(hist))) / math.log(36) | |
| spread = _clip01(entropy / 0.85) | |
| lum = 0.2126 * arr[:, :, 0] + 0.7152 * arr[:, :, 1] + 0.0722 * arr[:, :, 2] | |
| p95, p05 = float(np.percentile(lum, 95)), float(np.percentile(lum, 5)) | |
| contrast = _clip01((p95 - p05) / 0.75) | |
| value = _clip01(0.55 * spread + 0.45 * contrast) | |
| return { | |
| "id": "color_harmony", | |
| "label": "Color Harmony", | |
| "value": round(value, 3), | |
| "raw": { | |
| "hue_entropy_norm": round(spread, 4), | |
| "luminance_contrast": round(contrast, 4), | |
| }, | |
| "detail": f"hue spread {spread:.2f}, luminance contrast {contrast:.2f}", | |
| } | |
| def _saliency_com(img: Image.Image) -> tuple[float, float, float]: | |
| """Return (cx, cy, offset) in 0–1 coords; offset = distance from center.""" | |
| arr = _rgb_array(img) | |
| h, w, _ = arr.shape | |
| if _HAS_CV2: | |
| bgr = (arr * 255).astype(np.uint8)[:, :, ::-1] | |
| try: | |
| sal = cv2.saliency.StaticSaliencySpectralResidual_create() | |
| ok, sal_map = sal.computeSaliency(bgr) | |
| if ok: | |
| mass = sal_map.astype(np.float32) | |
| mass = mass / (mass.sum() + 1e-6) | |
| ys = np.arange(h)[:, None] | |
| xs = np.arange(w)[None, :] | |
| cy = float((ys * mass).sum()) | |
| cx = float((xs * mass).sum()) | |
| offset = math.sqrt((cx / w - 0.5) ** 2 + (cy / h - 0.5) ** 2) | |
| return cx / w, cy / h, offset | |
| except Exception: | |
| pass | |
| lum = 0.2126 * arr[:, :, 0] + 0.7152 * arr[:, :, 1] + 0.0722 * arr[:, :, 2] | |
| mass = lum / (lum.sum() + 1e-6) | |
| ys = np.arange(h)[:, None] | |
| xs = np.arange(w)[None, :] | |
| cy = float((ys * mass).sum()) | |
| cx = float((xs * mass).sum()) | |
| offset = math.sqrt((cx / w - 0.5) ** 2 + (cy / h - 0.5) ** 2) | |
| return cx / w, cy / h, offset | |
| def _composition_balance(img: Image.Image) -> dict: | |
| cx, cy, offset = _saliency_com(img) | |
| # High bar = well balanced (near center); low = shifted | |
| value = _clip01(1.0 - offset / 0.45) | |
| return { | |
| "id": "composition_balance", | |
| "label": "Composition Balance", | |
| "value": round(value, 3), | |
| "raw": {"center_x": round(cx, 4), "center_y": round(cy, 4), "offset": round(offset, 4)}, | |
| "detail": f"visual mass at ({cx:.0%}, {cy:.0%}), offset {offset:.2f} from center", | |
| } | |
| def _saturation_intensity(img: Image.Image) -> dict: | |
| _, sat, _, _ = _hsv_arrays(img) | |
| mean_sat = float(sat.mean()) | |
| value = _clip01(mean_sat / 0.65) | |
| return { | |
| "id": "saturation_intensity", | |
| "label": "Saturation Intensity", | |
| "value": round(value, 3), | |
| "raw": {"mean_saturation": round(mean_sat, 4)}, | |
| "detail": f"mean saturation {mean_sat:.2f}", | |
| } | |
| def _edge_complexity(img: Image.Image) -> dict: | |
| arr = _rgb_array(img) | |
| gray = (0.2126 * arr[:, :, 0] + 0.7152 * arr[:, :, 1] + 0.0722 * arr[:, :, 2] * 255).astype( | |
| np.uint8 | |
| ) | |
| if _HAS_CV2: | |
| edges = cv2.Canny(gray, 80, 160) | |
| density = float(edges.mean()) / 255.0 | |
| else: | |
| gx = np.abs(np.diff(gray.astype(np.float32), axis=1)).mean() | |
| gy = np.abs(np.diff(gray.astype(np.float32), axis=0)).mean() | |
| density = _clip01((gx + gy) / 120.0) | |
| value = _clip01(density / 0.35) | |
| return { | |
| "id": "edge_complexity", | |
| "label": "Edge Complexity", | |
| "value": round(value, 3), | |
| "raw": {"edge_density": round(density, 4)}, | |
| "detail": f"edge density {density:.3f}", | |
| } | |
| def _warm_cool(img: Image.Image) -> dict: | |
| """Hue centroid mapped: 0=cool, 1=warm.""" | |
| hue, sat, _, _ = _hsv_arrays(img) | |
| active = sat > 0.06 | |
| if active.sum() < 10: | |
| centroid = 0.5 | |
| else: | |
| h = hue[active] | |
| # Circular mean on hue circle | |
| ang = h * 2 * math.pi | |
| centroid = (math.atan2(float(np.sin(ang).mean()), float(np.cos(ang).mean())) / (2 * math.pi)) % 1.0 | |
| # Map hue: blues/greens (~0.45–0.75) = cool, reds/oranges (~0.0–0.12, 0.88–1.0) = warm | |
| warm_score = 1.0 - abs(((centroid + 0.5) % 1.0) - 0.5) * 2 | |
| warm_score = _clip01(warm_score * 0.6 + 0.2 if centroid < 0.2 or centroid > 0.85 else 0.35) | |
| return { | |
| "id": "warm_cool", | |
| "label": "Warm / Cool", | |
| "value": round(warm_score, 3), | |
| "raw": {"hue_centroid": round(centroid, 4)}, | |
| "detail": f"hue centroid {centroid:.2f} ({'warm-leaning' if warm_score > 0.55 else 'cool-leaning' if warm_score < 0.45 else 'neutral'})", | |
| } | |
| def compute_dissection(img: Image.Image) -> list[dict]: | |
| """Return ordered list of dimension dicts with value in [0, 1].""" | |
| img = img.convert("RGB") | |
| return [ | |
| _color_harmony(img), | |
| _composition_balance(img), | |
| _saturation_intensity(img), | |
| _edge_complexity(img), | |
| _warm_cool(img), | |
| ] | |