Files
s-canvas/harness/quality_validator.py
HYUNJUNGLEE b9342f6726 Import S-CANVAS source + iter=1~7 lint cleanup
S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:29:08 +09:00

98 lines
3.5 KiB
Python

"""품질 검증기 - 생성된 이미지가 기준을 충족하는지 자동 검사한다."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import numpy as np
try:
import cv2
from PIL import Image
except ImportError as e:
raise ImportError("opencv-python, Pillow이 필요합니다: pip install opencv-python Pillow") from e
@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