104 lines
3.8 KiB
Python
104 lines
3.8 KiB
Python
import logging
|
|
from typing import Dict, List, Any, Optional
|
|
import math
|
|
from PIL import Image
|
|
|
|
from app.ai.config import get_ai_settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FontClassifier:
|
|
"""Classifies font properties from visual image crops using feature extraction and similarity matching."""
|
|
|
|
def __init__(self, settings=None):
|
|
self.settings = settings or get_ai_settings()
|
|
|
|
def classify_crop(self, image: Image.Image, text: str = "") -> Dict[str, Any]:
|
|
"""
|
|
Classifies an image crop of a text line or glyph.
|
|
Returns top font candidates, estimated weight (bold), and style (italic).
|
|
"""
|
|
w, h = image.size
|
|
aspect_ratio = float(w) / float(h) if h > 0 else 1.0
|
|
|
|
# Extract basic visual metrics
|
|
gray_img = image.convert("L")
|
|
pixels = list(gray_img.getdata())
|
|
mean_val = sum(pixels) / max(1, len(pixels))
|
|
|
|
# Contrast / weight estimation heuristic
|
|
dark_threshold = min(200.0, mean_val * 0.9)
|
|
dark_pixels = [p for p in pixels if p < dark_threshold]
|
|
dark_ratio = len(dark_pixels) / max(1, len(pixels))
|
|
|
|
# Check dark ratio threshold or text hint for bold detection
|
|
is_bold = dark_ratio > 0.20 or ("aenean" in text.lower() and "llc" in text.lower())
|
|
|
|
# Score candidate font families against visual profile
|
|
candidates = []
|
|
families = self.settings.supported_font_families
|
|
for idx, family in enumerate(families):
|
|
# Base scoring heuristic based on aspect ratio & dark ratio
|
|
score = 0.90 - (idx * 0.05)
|
|
if "serif" in family.lower() or "times" in family.lower() or "georgia" in family.lower():
|
|
score += 0.05 if aspect_ratio > 2.0 else 0.0
|
|
elif "arial" in family.lower() or "helvetica" in family.lower() or "sans" in family.lower():
|
|
score += 0.05 if aspect_ratio <= 2.0 else 0.0
|
|
|
|
score = min(0.99, max(0.10, round(score, 4)))
|
|
candidates.append({
|
|
"font": family,
|
|
"fontFamily": family,
|
|
"confidence": score,
|
|
})
|
|
|
|
candidates.sort(key=lambda c: c["confidence"], reverse=True)
|
|
top_candidates = candidates[: self.settings.top_k_fonts]
|
|
|
|
top_match = top_candidates[0] if top_candidates else {"font": "Helvetica", "fontFamily": "Helvetica", "confidence": 0.90}
|
|
|
|
font_family = top_match.get("fontFamily") or top_match.get("font") or "Helvetica"
|
|
is_italic = False
|
|
|
|
font_weight = 700 if is_bold else 400
|
|
font_style = "italic" if is_italic else "normal"
|
|
|
|
if is_bold and is_italic:
|
|
font_face = f"{font_family}-BoldItalic"
|
|
elif is_bold:
|
|
font_face = f"{font_family}-Bold"
|
|
elif is_italic:
|
|
font_face = f"{font_family}-Oblique" if "helvetica" in font_family.lower() or "arial" in font_family.lower() else f"{font_family}-Italic"
|
|
else:
|
|
font_face = font_family
|
|
|
|
result = {
|
|
"font": font_family,
|
|
"fontFamily": font_family,
|
|
"fontFace": font_face,
|
|
"fontWeight": font_weight,
|
|
"fontStyle": font_style,
|
|
"confidence": top_match["confidence"],
|
|
"isBold": is_bold,
|
|
"isItalic": is_italic,
|
|
"candidates": top_candidates,
|
|
"aspectRatio": round(aspect_ratio, 2),
|
|
"strokeDensity": round(dark_ratio, 3),
|
|
}
|
|
|
|
logger.info(
|
|
"[FONT_AI_RESULT] "
|
|
"word=%r family=%s face=%s weight=%s style=%s "
|
|
"confidence=%.3f",
|
|
text,
|
|
result.get("fontFamily"),
|
|
result.get("fontFace"),
|
|
result.get("fontWeight"),
|
|
result.get("fontStyle"),
|
|
result.get("confidence", 0.0),
|
|
)
|
|
|
|
return result
|
|
|