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>
0
_unused/REF_BY_SEOK/__init__.py
Normal file
137
_unused/REF_BY_SEOK/logger.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""로거 - SQLite DB + structlog 기반 작업 이력 추적."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import Column, DateTime, Float, Integer, String, Text, create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||
|
||||
|
||||
# ──────────────────────────── ORM 모델 ────────────────────────────
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class JobRecord(Base):
|
||||
"""조감도 생성 작업 1건의 이력 레코드."""
|
||||
__tablename__ = "jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
dxf_path = Column(String(512), nullable=False)
|
||||
dxf_hash = Column(String(32))
|
||||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||||
seed = Column(Integer)
|
||||
prompt_version = Column(String(32))
|
||||
prompt_hash = Column(String(32))
|
||||
status = Column(String(16), default="pending") # pending / running / done / failed
|
||||
output_path = Column(String(512))
|
||||
quality_score = Column(Float)
|
||||
error_message = Column(Text)
|
||||
latency_ms = Column(Float)
|
||||
|
||||
|
||||
# ──────────────────────────── DB 세션 ────────────────────────────
|
||||
|
||||
_engine = None
|
||||
_SessionFactory = None
|
||||
|
||||
|
||||
def init_db(db_path: str | Path = "cad_aerial_gen.db"):
|
||||
global _engine, _SessionFactory
|
||||
_engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
||||
Base.metadata.create_all(_engine)
|
||||
_SessionFactory = sessionmaker(bind=_engine)
|
||||
|
||||
|
||||
def get_db_session() -> Session:
|
||||
if _SessionFactory is None:
|
||||
init_db()
|
||||
return _SessionFactory()
|
||||
|
||||
|
||||
# ──────────────────────────── structlog 설정 ────────────────────────────
|
||||
|
||||
def setup_logging(log_file: Optional[Path] = None, level: str = "INFO"):
|
||||
"""콘솔 + 파일 동시 로깅을 설정한다."""
|
||||
handlers = [logging.StreamHandler(sys.stdout)]
|
||||
if log_file:
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
handlers.append(logging.FileHandler(str(log_file), encoding="utf-8"))
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
level=getattr(logging, level.upper(), logging.INFO),
|
||||
handlers=handlers,
|
||||
)
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.processors.add_log_level,
|
||||
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
||||
structlog.dev.ConsoleRenderer(),
|
||||
],
|
||||
wrapper_class=structlog.make_filtering_bound_logger(
|
||||
getattr(logging, level.upper(), logging.INFO)
|
||||
),
|
||||
logger_factory=structlog.PrintLoggerFactory(),
|
||||
)
|
||||
|
||||
|
||||
def get_logger(name: str = "cad_aerial_gen"):
|
||||
return structlog.get_logger(name)
|
||||
|
||||
|
||||
# ──────────────────────────── 작업 이력 헬퍼 ────────────────────────────
|
||||
|
||||
class JobLogger:
|
||||
"""작업 이력 CRUD 래퍼."""
|
||||
|
||||
def create_job(self, db: Session, dxf_path: str, dxf_hash: str = "") -> JobRecord:
|
||||
record = JobRecord(dxf_path=dxf_path, dxf_hash=dxf_hash, status="pending")
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
return record
|
||||
|
||||
def start_job(self, db: Session, job_id: int, seed: int, prompt_version: str, prompt_hash: str):
|
||||
record = db.query(JobRecord).filter_by(id=job_id).first()
|
||||
if record:
|
||||
record.status = "running"
|
||||
record.seed = seed
|
||||
record.prompt_version = prompt_version
|
||||
record.prompt_hash = prompt_hash
|
||||
db.commit()
|
||||
|
||||
def complete_job(
|
||||
self,
|
||||
db: Session,
|
||||
job_id: int,
|
||||
output_path: str,
|
||||
quality_score: float,
|
||||
latency_ms: float,
|
||||
):
|
||||
record = db.query(JobRecord).filter_by(id=job_id).first()
|
||||
if record:
|
||||
record.status = "done"
|
||||
record.output_path = output_path
|
||||
record.quality_score = quality_score
|
||||
record.latency_ms = latency_ms
|
||||
db.commit()
|
||||
|
||||
def fail_job(self, db: Session, job_id: int, error: str):
|
||||
record = db.query(JobRecord).filter_by(id=job_id).first()
|
||||
if record:
|
||||
record.status = "failed"
|
||||
record.error_message = error
|
||||
db.commit()
|
||||
|
||||
def list_jobs(self, db: Session, limit: int = 50):
|
||||
return db.query(JobRecord).order_by(JobRecord.id.desc()).limit(limit).all()
|
||||
58
_unused/REF_BY_SEOK/prompt_registry.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""프롬프트 레지스트리 - 버전 관리 및 재현 가능성 보장."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class PromptRegistry:
|
||||
"""프롬프트 템플릿 버전을 관리하고 변경 이력을 추적한다."""
|
||||
|
||||
def __init__(self, templates_dir: Path):
|
||||
self.templates_dir = templates_dir
|
||||
|
||||
def list_versions(self) -> List[str]:
|
||||
"""사용 가능한 템플릿 버전 목록을 반환한다 (최신순)."""
|
||||
yamls = sorted(self.templates_dir.glob("v*.yaml"), reverse=True)
|
||||
return [p.stem for p in yamls]
|
||||
|
||||
def latest_version(self) -> Optional[str]:
|
||||
versions = self.list_versions()
|
||||
return versions[0] if versions else None
|
||||
|
||||
def load_template(self, version: str) -> Dict:
|
||||
path = self.templates_dir / f"{version}.yaml"
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"템플릿 버전 {version}이 없습니다.")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def compare(self, version_a: str, version_b: str) -> Dict:
|
||||
"""두 버전의 차이점을 반환한다."""
|
||||
a = self.load_template(version_a)
|
||||
b = self.load_template(version_b)
|
||||
diff = {}
|
||||
all_keys = set(a) | set(b)
|
||||
for key in all_keys:
|
||||
va, vb = a.get(key), b.get(key)
|
||||
if va != vb:
|
||||
diff[key] = {"old": va, "new": vb}
|
||||
return diff
|
||||
|
||||
def save_new_version(self, new_version: str, template: Dict) -> Path:
|
||||
"""새 버전 템플릿을 저장한다."""
|
||||
path = self.templates_dir / f"{new_version}.yaml"
|
||||
if path.exists():
|
||||
raise FileExistsError(f"버전 {new_version}이 이미 존재합니다.")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(template, f, allow_unicode=True, default_flow_style=False)
|
||||
return path
|
||||
|
||||
def get_version_for_hash(self, prompt_hash: str, db_session) -> Optional[str]:
|
||||
"""프롬프트 해시로 사용된 버전을 역조회한다."""
|
||||
from harness.logger import JobRecord
|
||||
record = db_session.query(JobRecord).filter_by(prompt_hash=prompt_hash).first()
|
||||
return record.prompt_version if record else None
|
||||
98
_unused/REF_BY_SEOK/quality_validator.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""품질 검증기 - 생성된 이미지가 기준을 충족하는지 자동 검사한다."""
|
||||
|
||||
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
|
||||
66
_unused/REF_BY_SEOK/seed_manager.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Seed 관리자 - 작업별 Seed 고정 및 추적."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from harness.logger import JobRecord, get_db_session
|
||||
|
||||
|
||||
class SeedManager:
|
||||
"""DXF 파일 해시 기반 결정론적 seed를 생성하고 이력을 관리한다."""
|
||||
|
||||
MAX_SEED = 2**32 - 1
|
||||
|
||||
def get_seed(
|
||||
self,
|
||||
file_hash: str,
|
||||
fixed_seed: Optional[int] = None,
|
||||
deterministic: bool = True,
|
||||
) -> int:
|
||||
"""
|
||||
Args:
|
||||
file_hash: DXF 파일의 SHA256 해시 앞 16자
|
||||
fixed_seed: 사용자가 직접 지정한 seed (None이면 자동)
|
||||
deterministic: True면 파일 해시 기반, False면 랜덤
|
||||
"""
|
||||
if fixed_seed is not None:
|
||||
return int(fixed_seed) % (self.MAX_SEED + 1)
|
||||
|
||||
if deterministic:
|
||||
return self._hash_to_seed(file_hash)
|
||||
|
||||
return random.randint(0, self.MAX_SEED)
|
||||
|
||||
def get_or_create_seed(
|
||||
self,
|
||||
db: Session,
|
||||
job_id: int,
|
||||
file_hash: str,
|
||||
fixed_seed: Optional[int] = None,
|
||||
deterministic: bool = True,
|
||||
) -> int:
|
||||
"""DB에서 기존 seed를 조회하거나 새로 생성한다."""
|
||||
existing = db.query(JobRecord).filter_by(id=job_id).first()
|
||||
if existing and existing.seed is not None:
|
||||
return existing.seed
|
||||
|
||||
seed = self.get_seed(file_hash, fixed_seed, deterministic)
|
||||
if existing:
|
||||
existing.seed = seed
|
||||
db.commit()
|
||||
return seed
|
||||
|
||||
@staticmethod
|
||||
def _hash_to_seed(file_hash: str) -> int:
|
||||
"""파일 해시를 정수 seed로 변환한다."""
|
||||
digest = hashlib.sha256(file_hash.encode()).digest()
|
||||
return int.from_bytes(digest[:4], "big")
|
||||
|
||||
@staticmethod
|
||||
def describe(seed: int) -> str:
|
||||
return f"seed={seed} (0x{seed:08X})"
|
||||
BIN
_unused/SCREENSHOT_lOG/1.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
_unused/SCREENSHOT_lOG/10.png
Normal file
|
After Width: | Height: | Size: 776 KiB |
BIN
_unused/SCREENSHOT_lOG/2.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
_unused/SCREENSHOT_lOG/3.png
Normal file
|
After Width: | Height: | Size: 373 KiB |
BIN
_unused/SCREENSHOT_lOG/4.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
_unused/SCREENSHOT_lOG/5.png
Normal file
|
After Width: | Height: | Size: 568 KiB |
BIN
_unused/SCREENSHOT_lOG/6.png
Normal file
|
After Width: | Height: | Size: 479 KiB |
BIN
_unused/SCREENSHOT_lOG/7.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
_unused/SCREENSHOT_lOG/8.png
Normal file
|
After Width: | Height: | Size: 591 KiB |
BIN
_unused/SCREENSHOT_lOG/9.png
Normal file
|
After Width: | Height: | Size: 1005 KiB |
BIN
_unused/SCREENSHOT_lOG/rainny.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
5
_unused/ai_studio_prompt.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Transform this aerial/satellite terrain image into a high-quality photorealistic bird's-eye view rendering. IMPORTANT: Keep the EXACT same terrain layout, structures, roads, water bodies from the input image. Only enhance the visual quality - add realistic textures, lighting, vegetation detail, water reflections. Do NOT change the composition or add/remove any major features.
|
||||
|
||||
Style: oblique aerial perspective, 3/4 view showing terrain depth, bright daylight, clear blue sky, sharp shadows, vivid green vegetation, enhance the existing satellite terrain texture and details, maintain exact terrain shape, contours, and layout from the input image, preserve water bodies, roads, and structural positions precisely, do NOT add or remove any major landscape features, photorealistic architectural visualization, professional drone photography quality, 8K ultra sharp detail, high dynamic range, realistic vegetation depth and canopy textures, natural water reflections and surface detail
|
||||
|
||||
Negative: blurry, low quality, distorted, watermark, text, logo, cartoon, anime, illustration, painting, sketch, oversaturated, underexposed, noisy, artifacts, completely different scene, unrelated content, changed terrain layout, moved structures, wrong topology
|
||||
221
_unused/install.cmd
Normal file
@@ -0,0 +1,221 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Claude Code Windows CMD Bootstrap Script
|
||||
REM Installs Claude Code for environments where PowerShell is not available
|
||||
|
||||
REM Parse command line argument
|
||||
set "TARGET=%~1"
|
||||
if "!TARGET!"=="" set "TARGET=latest"
|
||||
|
||||
REM Validate target parameter
|
||||
if /i "!TARGET!"=="stable" goto :target_valid
|
||||
if /i "!TARGET!"=="latest" goto :target_valid
|
||||
echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul
|
||||
if !ERRORLEVEL! equ 0 goto :target_valid
|
||||
|
||||
echo Usage: %0 [stable^|latest^|VERSION] >&2
|
||||
echo Example: %0 1.0.58 >&2
|
||||
exit /b 1
|
||||
|
||||
:target_valid
|
||||
|
||||
REM Check for 64-bit Windows
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid
|
||||
if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid
|
||||
|
||||
echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2
|
||||
exit /b 1
|
||||
|
||||
:arch_valid
|
||||
|
||||
REM Set constants
|
||||
set "GCS_BUCKET=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
|
||||
set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads"
|
||||
REM Use native ARM64 binary on ARM64 Windows, x64 otherwise
|
||||
if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
||||
set "PLATFORM=win32-arm64"
|
||||
) else (
|
||||
set "PLATFORM=win32-x64"
|
||||
)
|
||||
|
||||
REM Create download directory
|
||||
if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!"
|
||||
|
||||
REM Check for curl availability
|
||||
curl --version >nul 2>&1
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo curl is required but not available. Please install curl or use PowerShell installer. >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Always download latest version (which has the most up-to-date installer)
|
||||
call :download_file "!GCS_BUCKET!/latest" "!DOWNLOAD_DIR!\latest"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to get latest version >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Read version from file
|
||||
set /p VERSION=<"!DOWNLOAD_DIR!\latest"
|
||||
del "!DOWNLOAD_DIR!\latest"
|
||||
|
||||
REM Download manifest
|
||||
call :download_file "!GCS_BUCKET!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to get manifest >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Extract checksum from manifest
|
||||
call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Platform !PLATFORM! not found in manifest >&2
|
||||
del "!DOWNLOAD_DIR!\manifest.json" 2>nul
|
||||
exit /b 1
|
||||
)
|
||||
del "!DOWNLOAD_DIR!\manifest.json"
|
||||
|
||||
REM Download binary
|
||||
set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe"
|
||||
call :download_file "!GCS_BUCKET!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Failed to download binary >&2
|
||||
if exist "!BINARY_PATH!" del "!BINARY_PATH!"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Verify checksum
|
||||
call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!"
|
||||
if !ERRORLEVEL! neq 0 (
|
||||
echo Checksum verification failed >&2
|
||||
del "!BINARY_PATH!"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Run claude install to set up launcher and shell integration
|
||||
echo Setting up Claude Code...
|
||||
"!BINARY_PATH!" install "!TARGET!"
|
||||
set "INSTALL_RESULT=!ERRORLEVEL!"
|
||||
|
||||
REM Clean up downloaded file
|
||||
REM Wait a moment for any file handles to be released
|
||||
timeout /t 1 /nobreak >nul 2>&1
|
||||
del /f "!BINARY_PATH!" >nul 2>&1
|
||||
if exist "!BINARY_PATH!" (
|
||||
echo Warning: Could not remove temporary file: !BINARY_PATH!
|
||||
)
|
||||
|
||||
if !INSTALL_RESULT! neq 0 (
|
||||
echo Installation failed >&2
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Installation complete^^!
|
||||
echo.
|
||||
exit /b 0
|
||||
|
||||
REM ============================================================================
|
||||
REM SUBROUTINES
|
||||
REM ============================================================================
|
||||
|
||||
:download_file
|
||||
REM Downloads a file using curl
|
||||
REM Args: %1=URL, %2=OutputPath
|
||||
set "URL=%~1"
|
||||
set "OUTPUT=%~2"
|
||||
|
||||
curl -fsSL "!URL!" -o "!OUTPUT!"
|
||||
exit /b !ERRORLEVEL!
|
||||
|
||||
:parse_manifest
|
||||
REM Parse JSON manifest to extract checksum for platform
|
||||
REM Args: %1=ManifestPath, %2=Platform
|
||||
set "MANIFEST_PATH=%~1"
|
||||
set "PLATFORM_NAME=%~2"
|
||||
set "EXPECTED_CHECKSUM="
|
||||
|
||||
REM Use findstr to find platform section, then look for checksum
|
||||
set "FOUND_PLATFORM="
|
||||
set "IN_PLATFORM_SECTION="
|
||||
|
||||
REM Read the manifest line by line
|
||||
for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do (
|
||||
set "LINE=%%i"
|
||||
|
||||
REM Check if this line contains our platform
|
||||
echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
set "IN_PLATFORM_SECTION=1"
|
||||
)
|
||||
|
||||
REM If we're in the platform section, look for checksum
|
||||
if defined IN_PLATFORM_SECTION (
|
||||
echo !LINE! | findstr /c:"\"checksum\":" >nul
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
REM Extract checksum value
|
||||
for /f "tokens=2 delims=:" %%j in ("!LINE!") do (
|
||||
set "CHECKSUM_PART=%%j"
|
||||
REM Remove quotes, whitespace, and comma
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART: =!"
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART:"=!"
|
||||
set "CHECKSUM_PART=!CHECKSUM_PART:,=!"
|
||||
|
||||
REM Check if it looks like a SHA256 (64 hex chars)
|
||||
if not "!CHECKSUM_PART!"=="" (
|
||||
call :check_length "!CHECKSUM_PART!" 64
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
set "EXPECTED_CHECKSUM=!CHECKSUM_PART!"
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM Check if we've left the platform section (closing brace)
|
||||
echo !LINE! | findstr /c:"}" >nul
|
||||
if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION="
|
||||
)
|
||||
)
|
||||
|
||||
if "!EXPECTED_CHECKSUM!"=="" exit /b 1
|
||||
exit /b 0
|
||||
|
||||
:check_length
|
||||
REM Check if string length equals expected length
|
||||
REM Args: %1=String, %2=ExpectedLength
|
||||
set "STR=%~1"
|
||||
set "EXPECTED_LEN=%~2"
|
||||
set "LEN=0"
|
||||
:count_loop
|
||||
if "!STR:~%LEN%,1!"=="" goto :count_done
|
||||
set /a LEN+=1
|
||||
goto :count_loop
|
||||
:count_done
|
||||
if %LEN%==%EXPECTED_LEN% exit /b 0
|
||||
exit /b 1
|
||||
|
||||
:verify_checksum
|
||||
REM Verify file checksum using certutil
|
||||
REM Args: %1=FilePath, %2=ExpectedChecksum
|
||||
set "FILE_PATH=%~1"
|
||||
set "EXPECTED=%~2"
|
||||
|
||||
for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do (
|
||||
set "ACTUAL=%%i"
|
||||
set "ACTUAL=!ACTUAL: =!"
|
||||
if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done
|
||||
if "!ACTUAL!" neq "" (
|
||||
if /i "!ACTUAL!"=="!EXPECTED!" (
|
||||
exit /b 0
|
||||
) else (
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:verify_done
|
||||
exit /b 1
|
||||
26
_unused/nano_banana2.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from google import genai
|
||||
from google.genai import types as gtypes
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
client = genai.Client(
|
||||
vertexai=True,
|
||||
project=os.environ["GCP_PROJECT_ID"],
|
||||
location="global", # gemini-3.x 이미지 모델은 글로벌 전용
|
||||
)
|
||||
|
||||
prompt = "여기에 프롬프트"
|
||||
image = Image.open("input.png")
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-3-pro-image-preview", # Nano Banana 2
|
||||
contents=[prompt, image],
|
||||
config=gtypes.GenerateContentConfig(
|
||||
response_modalities=["IMAGE"],
|
||||
),
|
||||
)
|
||||
|
||||
for part in response.candidates[0].content.parts:
|
||||
if part.inline_data:
|
||||
with open("output.png", "wb") as f:
|
||||
f.write(part.inline_data.data)
|
||||
52
_unused/scratch/analyze_dxf.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import ezdxf
|
||||
import numpy as np
|
||||
|
||||
def analyze_dxf(filepath):
|
||||
print(f"Analyzing: {filepath}")
|
||||
doc = ezdxf.readfile(filepath)
|
||||
msp = doc.modelspace()
|
||||
|
||||
points = []
|
||||
|
||||
# 1. LWPOLYLINE, POLYLINE
|
||||
for entity in msp.query('LWPOLYLINE POLYLINE'):
|
||||
elevation = 0
|
||||
if hasattr(entity, 'dxf'):
|
||||
elevation = entity.dxf.elevation if hasattr(entity.dxf, 'elevation') else 0
|
||||
|
||||
for p in entity.get_points():
|
||||
if len(p) >= 3:
|
||||
points.append((p[0], p[1], p[2]))
|
||||
else:
|
||||
points.append((p[0], p[1], elevation))
|
||||
|
||||
# 2. LINE
|
||||
for entity in msp.query('LINE'):
|
||||
points.append(entity.dxf.start)
|
||||
points.append(entity.dxf.end)
|
||||
|
||||
if not points:
|
||||
print("No points found!")
|
||||
return
|
||||
|
||||
pts = np.array(points)
|
||||
min_vals = np.min(pts, axis=0)
|
||||
max_vals = np.max(pts, axis=0)
|
||||
ranges = max_vals - min_vals
|
||||
|
||||
print("\n[Statistics]")
|
||||
print(f"Total points: {len(pts)}")
|
||||
print(f"X: {min_vals[0]:.2f} to {max_vals[0]:.2f} (Range: {ranges[0]:.2f})")
|
||||
print(f"Y: {min_vals[1]:.2f} to {max_vals[1]:.2f} (Range: {ranges[1]:.2f})")
|
||||
print(f"Z: {min_vals[2]:.2f} to {max_vals[2]:.2f} (Range: {ranges[2]:.2f})")
|
||||
|
||||
# Ratio
|
||||
if ranges[0] > 0 and ranges[1] > 0:
|
||||
xy_avg_range = (ranges[0] + ranges[1]) / 2
|
||||
z_ratio = (ranges[2] / xy_avg_range) * 100
|
||||
print(f"Z-Ratio to XY: {z_ratio:.4f}%")
|
||||
if z_ratio < 0.1:
|
||||
print("WARNING: Z-range is extremely small compared to XY. Vertical exaggeration is required.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_dxf('사연댐 전체계획 평면도_contour.dxf')
|
||||
1080
_unused/structure_ui.py
Normal file
400
_unused/test_gate_render.py
Normal file
@@ -0,0 +1,400 @@
|
||||
"""구조물 조감도 스탠드얼론 테스트 스크립트.
|
||||
|
||||
Gate_Sample DXF → 파라미터 추출 → 3D 모델 → 다각도 캡처 → AI 렌더링.
|
||||
|
||||
사용법:
|
||||
python test_gate_render.py
|
||||
python test_gate_render.py --interactive # 인터랙티브 3D 뷰어
|
||||
python test_gate_render.py --ai # AI 렌더링 포함 (GEMINI_API_KEY 필요)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pyvista as pv
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
from spillway_parser import parse_spillway_dxf, SpillwayParams
|
||||
from spillway_3d_builder import SpillwayBuilder, COLORS
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 렌더링 유틸
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def add_all_meshes(plotter: pv.Plotter, meshes: list):
|
||||
"""메쉬 리스트를 플로터에 추가."""
|
||||
for mesh, color, opacity in meshes:
|
||||
try:
|
||||
plotter.add_mesh(
|
||||
mesh,
|
||||
color=color,
|
||||
opacity=opacity,
|
||||
smooth_shading=True,
|
||||
specular=0.3,
|
||||
specular_power=10,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f" mesh 추가 실패: {e}")
|
||||
|
||||
|
||||
def compute_camera_for_birdseye(params: SpillwayParams,
|
||||
elevation_deg: float = 35.0,
|
||||
azimuth_deg: float = 225.0,
|
||||
zoom: float = 1.2) -> tuple:
|
||||
"""여수로 구조물을 프레임에 담는 카메라 위치 계산.
|
||||
|
||||
elevation_deg: 앙각 (수평=0°, 수직 아래로=90°)
|
||||
azimuth_deg: 방위각 (북=0°, 동=90°, 남=180°, 서=270°)
|
||||
zoom: 값이 클수록 카메라가 멀어짐 (전체를 더 많이 담음)
|
||||
"""
|
||||
# 씬 전체 범위 (수면 + 구조물 + 하류 에이프런 포함)
|
||||
# 수면: Y ∈ [-40, 0.5], 구조물: Y ∈ [0, pier_length], 하류: Y ∈ [pier_length, pier_length+30]
|
||||
scene_y_min = -40.0
|
||||
scene_y_max = params.pier_length + 30.0
|
||||
scene_x_span = params.total_span
|
||||
scene_z_min = min(params.el_upstream_bed, params.el_downstream, params.el_gate_sill) - 1.0
|
||||
scene_z_max = params.el_bridge_top + 6.0 # 권양기 지붕까지
|
||||
|
||||
# 씬의 중심 (focal point) — 구조물 위주
|
||||
cx = scene_x_span / 2
|
||||
cy = (0 + params.pier_length) / 2 # 구조물 중심
|
||||
cz = (params.el_gate_sill + params.el_bridge_top) / 2
|
||||
|
||||
# 씬의 대각선 크기
|
||||
scene_dx = scene_x_span * 1.2 # X 방향 여유
|
||||
scene_dy = scene_y_max - scene_y_min
|
||||
scene_dz = scene_z_max - scene_z_min
|
||||
scene_diag = math.sqrt(scene_dx ** 2 + scene_dy ** 2 + scene_dz ** 2)
|
||||
|
||||
# 카메라 거리: PyVista 기본 viewAngle ≈ 30° 가정, tan(15°) ≈ 0.268
|
||||
# 프레임 꽉 채우려면 dist = (scene_diag/2) / tan(15°) ≈ scene_diag * 1.87
|
||||
dist = scene_diag * 1.0 * zoom
|
||||
|
||||
# 방위각/앙각 → 구면 좌표
|
||||
el_rad = math.radians(elevation_deg)
|
||||
az_rad = math.radians(azimuth_deg)
|
||||
|
||||
dx = dist * math.cos(el_rad) * math.sin(az_rad)
|
||||
dy = -dist * math.cos(el_rad) * math.cos(az_rad) # Y축 반전 (북→양)
|
||||
dz = dist * math.sin(el_rad)
|
||||
|
||||
camera_pos = (cx + dx, cy + dy, cz + dz)
|
||||
focal_point = (cx, cy, cz)
|
||||
view_up = (0, 0, 1)
|
||||
|
||||
return camera_pos, focal_point, view_up
|
||||
|
||||
|
||||
def capture_view(params: SpillwayParams, meshes: list,
|
||||
elevation_deg: float, azimuth_deg: float,
|
||||
size: int = 1536, bg_color: str = "#C8D4E0",
|
||||
zoom: float = 1.3) -> Image.Image:
|
||||
"""지정한 각도에서 3D 씬을 캡처."""
|
||||
plotter = pv.Plotter(off_screen=True, window_size=(size, size))
|
||||
plotter.set_background(bg_color)
|
||||
|
||||
add_all_meshes(plotter, meshes)
|
||||
|
||||
# 카메라 설정
|
||||
cam_pos, focal, up = compute_camera_for_birdseye(
|
||||
params, elevation_deg, azimuth_deg, zoom
|
||||
)
|
||||
plotter.camera_position = [cam_pos, focal, up]
|
||||
|
||||
# 조명 설정: 기본 헤드라이트 + 방향 조명
|
||||
plotter.enable_3_lights()
|
||||
|
||||
img_arr = plotter.screenshot(return_img=True, window_size=(size, size))
|
||||
plotter.close()
|
||||
|
||||
img = Image.fromarray(img_arr)
|
||||
return img
|
||||
|
||||
|
||||
def capture_depth(params: SpillwayParams, meshes: list,
|
||||
elevation_deg: float, azimuth_deg: float,
|
||||
size: int = 1536, zoom: float = 1.3) -> Image.Image:
|
||||
"""depth map 캡처 (제어맵용)."""
|
||||
plotter = pv.Plotter(off_screen=True, window_size=(size, size))
|
||||
plotter.set_background("black")
|
||||
|
||||
for mesh, _, _ in meshes:
|
||||
plotter.add_mesh(mesh, color="white", smooth_shading=False)
|
||||
|
||||
cam_pos, focal, up = compute_camera_for_birdseye(params, elevation_deg, azimuth_deg, zoom)
|
||||
plotter.camera_position = [cam_pos, focal, up]
|
||||
|
||||
# show()로 렌더 파이프라인 초기화 후 depth 추출
|
||||
plotter.show(auto_close=False)
|
||||
try:
|
||||
z_img = plotter.get_image_depth()
|
||||
except Exception:
|
||||
z_img = None
|
||||
plotter.close()
|
||||
|
||||
if z_img is None:
|
||||
return Image.new("L", (size, size), 0)
|
||||
|
||||
# NaN 처리 + 정규화
|
||||
z_img = np.array(z_img, dtype=np.float32)
|
||||
z_finite = z_img[np.isfinite(z_img)]
|
||||
if len(z_finite) == 0:
|
||||
return Image.new("L", (size, size), 0)
|
||||
|
||||
z_min, z_max = z_finite.min(), z_finite.max()
|
||||
if z_max - z_min < 1e-6:
|
||||
return Image.new("L", (size, size), 128)
|
||||
|
||||
z_norm = (z_img - z_min) / (z_max - z_min)
|
||||
z_norm = np.where(np.isfinite(z_norm), z_norm, 1.0)
|
||||
# 가까울수록 밝게 (invert)
|
||||
z_norm = 1.0 - z_norm
|
||||
z_8bit = (z_norm * 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(z_8bit, "L")
|
||||
|
||||
|
||||
def capture_lineart(params: SpillwayParams, meshes: list,
|
||||
elevation_deg: float, azimuth_deg: float,
|
||||
size: int = 1536, zoom: float = 1.3) -> Image.Image:
|
||||
"""라인아트 캡처 (흰 배경 + 검은 엣지)."""
|
||||
plotter = pv.Plotter(off_screen=True, window_size=(size, size))
|
||||
plotter.set_background("white")
|
||||
|
||||
for mesh, _, _ in meshes:
|
||||
plotter.add_mesh(mesh, color="white", show_edges=True, edge_color="black", line_width=1)
|
||||
|
||||
cam_pos, focal, up = compute_camera_for_birdseye(params, elevation_deg, azimuth_deg, zoom)
|
||||
plotter.camera_position = [cam_pos, focal, up]
|
||||
|
||||
img_arr = plotter.screenshot(return_img=True, window_size=(size, size))
|
||||
plotter.close()
|
||||
|
||||
return Image.fromarray(img_arr)
|
||||
|
||||
|
||||
def compose_guide_image(capture: Image.Image, depth: Image.Image, lineart: Image.Image) -> Image.Image:
|
||||
"""캡처 + depth + lineart를 가이드 이미지로 합성."""
|
||||
# 모두 동일 크기로 맞춤
|
||||
base = capture.convert("RGB")
|
||||
d = depth.convert("RGB").resize(base.size)
|
||||
la = lineart.convert("RGB").resize(base.size)
|
||||
|
||||
# 80% base + 20% depth, 그 위에 lineart 살짝
|
||||
arr_base = np.array(base, dtype=np.float32)
|
||||
arr_depth = np.array(d, dtype=np.float32)
|
||||
arr_line = np.array(la, dtype=np.float32)
|
||||
|
||||
blend = arr_base * 0.80 + arr_depth * 0.20
|
||||
# 라인아트: 검은 픽셀만 선택해서 덧씌움
|
||||
line_mask = (arr_line.mean(axis=2, keepdims=True) < 100).astype(np.float32)
|
||||
final = blend * (1 - line_mask * 0.4) + arr_line * (line_mask * 0.4)
|
||||
final = np.clip(final, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(final)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 인터랙티브 뷰어
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def show_interactive(params: SpillwayParams, meshes: list):
|
||||
"""PyVista 인터랙티브 뷰어. q로 종료."""
|
||||
plotter = pv.Plotter(title="EG-VIEW Gate: Interactive Preview")
|
||||
plotter.set_background("#2B3A4A")
|
||||
add_all_meshes(plotter, meshes)
|
||||
|
||||
# 카메라 초기 위치: bird's eye
|
||||
cam_pos, focal, up = compute_camera_for_birdseye(params, 35, 225, 1.2)
|
||||
plotter.camera_position = [cam_pos, focal, up]
|
||||
|
||||
plotter.enable_3_lights()
|
||||
plotter.show_grid(color="#555")
|
||||
plotter.add_axes()
|
||||
|
||||
plotter.add_text(
|
||||
params.summary().replace("\n", " "),
|
||||
font_size=10, color="white", position="upper_left",
|
||||
)
|
||||
|
||||
plotter.show()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI 렌더링 (Gemini)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_with_gemini(guide_img: Image.Image, prompt: str, api_key: str) -> Image.Image | None:
|
||||
"""Gemini API로 AI 렌더링. 실패 시 None."""
|
||||
try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
import io as _io
|
||||
|
||||
client = genai.Client(api_key=api_key)
|
||||
|
||||
# 이미지를 PNG 바이트로 변환
|
||||
buf = _io.BytesIO()
|
||||
guide_img.save(buf, format="PNG")
|
||||
img_bytes = buf.getvalue()
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash-image",
|
||||
contents=[
|
||||
prompt,
|
||||
types.Part.from_bytes(data=img_bytes, mime_type="image/png"),
|
||||
],
|
||||
)
|
||||
|
||||
# 응답에서 이미지 추출
|
||||
for part in response.candidates[0].content.parts:
|
||||
if hasattr(part, "inline_data") and part.inline_data:
|
||||
img_data = part.inline_data.data
|
||||
return Image.open(_io.BytesIO(img_data))
|
||||
|
||||
print(" Gemini 응답에 이미지 없음")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f" Gemini 렌더링 오류: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def build_gate_prompt(params: SpillwayParams, time_of_day: str = "daytime") -> str:
|
||||
"""수문 구조물용 AI 프롬프트."""
|
||||
return (
|
||||
f"Photorealistic bird's eye view of a dam spillway gate facility, "
|
||||
f"{params.n_gates} radial (Tainter) gates each {params.gate_width:.0f}m wide by {params.gate_height:.0f}m tall, "
|
||||
f"ogee-profile concrete weir, service bridge on top, "
|
||||
f"hoist houses above each gate, {time_of_day}, "
|
||||
f"crystal clear water upstream, concrete apron downstream, "
|
||||
f"maintain exact structural geometry, layout, and proportions from the input image, "
|
||||
f"preserve gate positions and pier locations precisely, "
|
||||
f"professional architectural rendering, "
|
||||
f"8K ultra sharp detail, high dynamic range, "
|
||||
f"realistic concrete texture, steel gate panels, "
|
||||
f"bright daylight with sharp shadows, clear blue sky"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 메인
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--plan", default=None, help="Plan DXF (1/2)")
|
||||
ap.add_argument("--section", default=None, help="Section DXF (2/2)")
|
||||
ap.add_argument("--interactive", action="store_true", help="인터랙티브 3D 뷰어")
|
||||
ap.add_argument("--ai", action="store_true", help="AI 렌더링 (Gemini)")
|
||||
ap.add_argument("--output", default="gate_render_output", help="출력 디렉토리")
|
||||
ap.add_argument("--time", default="daytime", choices=["daytime", "sunset", "overcast"])
|
||||
ap.add_argument("--size", type=int, default=1536, help="렌더 해상도")
|
||||
args = ap.parse_args()
|
||||
|
||||
# 기본 샘플 경로
|
||||
if args.plan is None:
|
||||
base = Path("Gate_Sample")
|
||||
args.plan = str(base / "12995740-M40-001 여수로 수문 설치도(1/2).dxf")
|
||||
args.section = str(base / "12995740-M40-002 여수로 수문 설치도(2/2).dxf")
|
||||
|
||||
# 출력 디렉토리
|
||||
out = Path(args.output)
|
||||
out.mkdir(exist_ok=True)
|
||||
|
||||
# 1) 파라미터 추출
|
||||
print("=" * 60)
|
||||
print("Step 1: DXF 파싱")
|
||||
print("=" * 60)
|
||||
params = parse_spillway_dxf(args.plan, args.section)
|
||||
print(params.summary())
|
||||
|
||||
# 2) 3D 모델 빌드
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 2: 3D 모델 빌드")
|
||||
print("=" * 60)
|
||||
builder = SpillwayBuilder(params)
|
||||
meshes = builder.build_all()
|
||||
print(f"{len(meshes)}개 메쉬 컴포넌트 생성")
|
||||
|
||||
# 3) 인터랙티브 모드?
|
||||
if args.interactive:
|
||||
print("\n인터랙티브 뷰어 실행 중...")
|
||||
show_interactive(params, meshes)
|
||||
return
|
||||
|
||||
# 4) 다각도 캡처
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 3: 다각도 캡처")
|
||||
print("=" * 60)
|
||||
|
||||
views = [
|
||||
("top_down", 75, 180, 1.0), # 수직 상부
|
||||
("bird_eye_1", 35, 225, 1.2), # 조감도 (북동)
|
||||
("bird_eye_2", 35, 135, 1.2), # 조감도 (북서)
|
||||
("bird_eye_3", 25, 180, 1.3), # 조감도 (정면)
|
||||
("elevation", 5, 180, 1.1), # 정면 입면
|
||||
]
|
||||
|
||||
for name, elev, azim, zoom in views:
|
||||
print(f" [{name}] elev={elev}°, azim={azim}°")
|
||||
img = capture_view(params, meshes, elev, azim, size=args.size, zoom=zoom)
|
||||
img_path = out / f"capture_{name}.png"
|
||||
img.save(img_path)
|
||||
print(f" → {img_path}")
|
||||
|
||||
# 5) 제어맵 추출 (bird_eye_1 기준)
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 4: 제어맵 추출 (bird_eye_1)")
|
||||
print("=" * 60)
|
||||
main_elev, main_azim, main_zoom = 35, 225, 1.2
|
||||
|
||||
capture = capture_view(params, meshes, main_elev, main_azim, args.size, zoom=main_zoom)
|
||||
capture.save(out / "capture_main.png")
|
||||
print(f" capture_main.png")
|
||||
|
||||
depth = capture_depth(params, meshes, main_elev, main_azim, args.size, zoom=main_zoom)
|
||||
depth.save(out / "depth_map.png")
|
||||
print(f" depth_map.png")
|
||||
|
||||
lineart = capture_lineart(params, meshes, main_elev, main_azim, args.size, zoom=main_zoom)
|
||||
lineart.save(out / "lineart_map.png")
|
||||
print(f" lineart_map.png")
|
||||
|
||||
guide = compose_guide_image(capture, depth, lineart)
|
||||
guide.save(out / "guide_composite.png")
|
||||
print(f" guide_composite.png")
|
||||
|
||||
# 6) AI 렌더링 (선택적)
|
||||
if args.ai:
|
||||
api_key = os.environ.get("GEMINI_API_KEY", "")
|
||||
if not api_key:
|
||||
print("\n경고: GEMINI_API_KEY 환경변수 필요 (AI 렌더링 건너뜀)")
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("Step 5: AI 렌더링 (Gemini)")
|
||||
print("=" * 60)
|
||||
prompt = build_gate_prompt(params, args.time)
|
||||
print(f" 프롬프트: {prompt[:100]}...")
|
||||
|
||||
ai_img = render_with_gemini(guide, prompt, api_key)
|
||||
if ai_img:
|
||||
ai_img.save(out / "ai_rendered.png")
|
||||
print(f" → ai_rendered.png")
|
||||
else:
|
||||
print(f" AI 렌더링 실패")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"완료. 출력 디렉토리: {out.absolute()}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
_unused/지형도 베이스맵/EG-VIEW_rendered_v2(eng).png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
_unused/지형도 베이스맵/V10.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
_unused/지형도 베이스맵/V11.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
_unused/지형도 베이스맵/V12.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
_unused/지형도 베이스맵/V13.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
_unused/지형도 베이스맵/V3.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
_unused/지형도 베이스맵/V4.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
_unused/지형도 베이스맵/V5.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
_unused/지형도 베이스맵/V6.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
_unused/지형도 베이스맵/V7.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
_unused/지형도 베이스맵/V8.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
_unused/지형도 베이스맵/V9.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |