""" image_analyzer.py - LOGOS Image Analysis Pipeline Batch-process architectural diagrams and UI screenshots. """ import os import cv2 import numpy as np from PIL import Image from typing import List, Dict, Tuple, Optional from dataclasses import dataclass from collections import Counter @dataclass class ImageAnalysis: """Result of analyzing a single image.""" filename: str path: str width: int height: int aspect_ratio: float classification: str # "diagram", "ui", "photo", "other" dominant_colors: List[Tuple[int, int, int]] edge_density: float text_region_ratio: float thumbnail: Optional[np.ndarray] = None def classify_image(edge_density: float, color_variance: float, aspect: float) -> str: """ Simple heuristic classification. - Diagrams: High edge density, low color variance, often wide aspect. - UI: Medium edge density, structured colors, standard aspect. - Photos: Low edge density, high color variance. """ if edge_density > 0.15 and color_variance < 50: return "diagram" elif 0.05 < edge_density < 0.20 and 0.5 < aspect < 2.0: return "ui" elif color_variance > 80: return "photo" else: return "other" def get_dominant_colors(image: np.ndarray, k: int = 3) -> List[Tuple[int, int, int]]: """Extract k dominant colors using k-means clustering.""" pixels = image.reshape(-1, 3).astype(np.float32) # Subsample for speed if len(pixels) > 10000: indices = np.random.choice(len(pixels), 10000, replace=False) pixels = pixels[indices] criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) _, labels, centers = cv2.kmeans(pixels, k, None, criteria, 3, cv2.KMEANS_PP_CENTERS) # Sort by frequency counts = Counter(labels.flatten()) sorted_centers = [centers[i] for i, _ in counts.most_common(k)] return [(int(c[2]), int(c[1]), int(c[0])) for c in sorted_centers] # BGR -> RGB def calculate_edge_density(gray: np.ndarray) -> float: """Calculate edge density using Canny edge detection.""" edges = cv2.Canny(gray, 50, 150) return np.count_nonzero(edges) / edges.size def estimate_text_regions(gray: np.ndarray) -> float: """ Estimate ratio of image containing text-like regions. Uses morphological operations to find text blocks. """ # Threshold _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) # Dilate to connect text characters kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 3)) dilated = cv2.dilate(binary, kernel, iterations=2) # Find contours contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # Filter by aspect ratio (text regions are usually wide) text_area = 0 for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) if w > h * 2 and w > 20: # Wide and reasonably sized text_area += w * h return text_area / (gray.shape[0] * gray.shape[1]) def analyze_image(path: str, thumbnail_size: int = 128) -> ImageAnalysis: """ Analyze a single image and return structured metadata. """ img = cv2.imread(path) if img is None: raise ValueError(f"Could not load image: {path}") height, width = img.shape[:2] aspect = width / height # Convert to RGB for color analysis rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Metrics dominant_colors = get_dominant_colors(rgb) edge_density = calculate_edge_density(gray) text_ratio = estimate_text_regions(gray) color_variance = np.std(rgb) # Classification classification = classify_image(edge_density, color_variance, aspect) # Thumbnail scale = thumbnail_size / max(width, height) thumb = cv2.resize(rgb, (int(width * scale), int(height * scale))) return ImageAnalysis( filename=os.path.basename(path), path=path, width=width, height=height, aspect_ratio=round(aspect, 2), classification=classification, dominant_colors=dominant_colors, edge_density=round(edge_density, 4), text_region_ratio=round(text_ratio, 4), thumbnail=thumb ) def batch_analyze(folder: str, extensions: List[str] = None) -> List[ImageAnalysis]: """ Analyze all images in a folder. Args: folder: Path to folder containing images. extensions: List of valid extensions (default: ['.png', '.jpg', '.jpeg']). Returns: List of ImageAnalysis results. """ if extensions is None: extensions = ['.png', '.jpg', '.jpeg', '.bmp', '.webp'] results = [] for filename in os.listdir(folder): ext = os.path.splitext(filename)[1].lower() if ext in extensions: path = os.path.join(folder, filename) try: analysis = analyze_image(path) results.append(analysis) except Exception as e: print(f"[ANALYZER] Error processing {filename}: {e}") return results def summarize_analysis(results: List[ImageAnalysis]) -> Dict: """Generate summary statistics from batch analysis.""" if not results: return {"count": 0} classifications = Counter(r.classification for r in results) avg_edge = sum(r.edge_density for r in results) / len(results) avg_text = sum(r.text_region_ratio for r in results) / len(results) return { "count": len(results), "classifications": dict(classifications), "avg_edge_density": round(avg_edge, 4), "avg_text_ratio": round(avg_text, 4), "total_size_mb": round(sum(r.width * r.height * 3 for r in results) / (1024 * 1024), 2) } # CLI for standalone testing if __name__ == "__main__": import sys if len(sys.argv) < 2: print("Usage: python image_analyzer.py ") sys.exit(1) folder = sys.argv[1] print(f"[ANALYZER] Processing folder: {folder}") results = batch_analyze(folder) summary = summarize_analysis(results) print(f"\n[SUMMARY]") print(f" Total Images: {summary['count']}") print(f" Classifications: {summary['classifications']}") print(f" Avg Edge Density: {summary['avg_edge_density']}") print(f" Avg Text Ratio: {summary['avg_text_ratio']}")