"""품질 검증기 - 생성된 이미지가 기준을 충족하는지 자동 검사한다.""" from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import List import numpy as np try: import cv2 from PIL import Image except ImportError: raise ImportError("opencv-python, Pillow이 필요합니다: pip install opencv-python Pillow") @dataclass class ValidationResult: passed: bool score: float # 0.0 ~ 1.0 종합 품질 점수 resolution_ok: bool sharpness_ok: bool color_diversity_ok: bool messages: List[str] @property def summary(self) -> str: status = "PASS" if self.passed else "FAIL" return f"[{status}] score={self.score:.2f} | " + " | ".join(self.messages) class QualityValidator: """이미지 품질을 자동으로 검증한다.""" def __init__( self, min_resolution: int = 2048, sharpness_threshold: float = 100.0, color_diversity_threshold: float = 0.15, ): self.min_resolution = min_resolution self.sharpness_threshold = sharpness_threshold self.color_diversity_threshold = color_diversity_threshold def validate(self, image_path: Path) -> ValidationResult: if not image_path.exists(): return ValidationResult( passed=False, score=0.0, resolution_ok=False, sharpness_ok=False, color_diversity_ok=False, messages=["파일이 존재하지 않음"], ) img_pil = Image.open(image_path).convert("RGB") img_cv = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) resolution_ok, res_msg = self._check_resolution(img_pil) sharpness_ok, sharp_score, sharp_msg = self._check_sharpness(img_cv) color_ok, color_msg = self._check_color_diversity(img_cv) scores = [ 1.0 if resolution_ok else 0.0, min(sharp_score / (self.sharpness_threshold * 3), 1.0), 1.0 if color_ok else 0.3, ] overall_score = float(np.mean(scores)) passed = resolution_ok and sharpness_ok and color_ok return ValidationResult( passed=passed, score=overall_score, resolution_ok=resolution_ok, sharpness_ok=sharpness_ok, color_diversity_ok=color_ok, messages=[res_msg, sharp_msg, color_msg], ) def _check_resolution(self, img: Image.Image): w, h = img.size ok = w >= self.min_resolution and h >= self.min_resolution msg = f"해상도={w}x{h} {'OK' if ok else f'(최소 {self.min_resolution} 필요)'}" return ok, msg def _check_sharpness(self, img_cv): gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) lap_var = cv2.Laplacian(gray, cv2.CV_64F).var() ok = lap_var >= self.sharpness_threshold msg = f"선명도={lap_var:.1f} {'OK' if ok else f'(임계값 {self.sharpness_threshold})'}" return ok, float(lap_var), msg def _check_color_diversity(self, img_cv): """색상 다양성 검사 - 단색 평면 출력을 탐지한다.""" hsv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2HSV) s_channel = hsv[:, :, 1].astype(np.float32) / 255.0 mean_saturation = float(s_channel.mean()) ok = mean_saturation >= self.color_diversity_threshold msg = f"색상다양성={mean_saturation:.3f} {'OK' if ok else f'(임계값 {self.color_diversity_threshold})'}" return ok, msg