""" 3D 모델 렌더링 모듈 다각도 렌더링, 조명 조건 적용, 카메라 파라미터 조절 기능을 제공합니다. """ import numpy as np import trimesh from typing import List, Dict, Tuple, Optional import cv2 from PIL import Image import math import os # Docker 환경에서 렌더링을 위한 설정 (OSMesa 백엔드) # 환경 변수는 한 번만 설정하고 순서를 고려 os.environ['PYOPENGL_PLATFORM'] = 'osmesa' os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3' os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330' os.environ['OSMESA_HEADLESS'] = '1' os.environ['OPEN3D_HEADLESS'] = '1' os.environ['DISPLAY'] = ':99' os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1' os.environ['GALLIUM_DRIVER'] = 'llvmpipe' class Renderer: """3D 모델 렌더링을 담당하는 클래스""" # 클래스 변수로 모듈 로드 상태 추적 _modules_loaded = False _osmesa_configured = False def __init__(self, image_size: Tuple[int, int] = (512, 512)): """ 렌더러 초기화 Args: image_size (Tuple[int, int]): 렌더링 이미지 크기 (width, height) """ self.image_size = image_size self.width, self.height = image_size # OSMesa 설정 강화 (한 번만 실행) if not Renderer._modules_loaded: self._setup_osmesa() Renderer._modules_loaded = True # 기본 카메라 파라미터 self.camera_distance = 2.0 self.camera_elevation = 30.0 self.camera_azimuth = 0.0 # 조명 설정 self.lighting_conditions = { 'default': {'intensity': 1.0, 'direction': [1, 1, 1]}, 'bright': {'intensity': 1.5, 'direction': [1, 1, 1]}, 'dim': {'intensity': 0.5, 'direction': [1, 1, 1]}, 'side': {'intensity': 1.0, 'direction': [1, 0, 0]}, 'top': {'intensity': 1.0, 'direction': [0, 1, 0]}, 'front': {'intensity': 1.0, 'direction': [0, 0, 1]} } def _setup_osmesa(self): """OSMesa 렌더링 환경 설정 (개선된 버전)""" try: # OSMesa 설치 상태 확인 self._verify_osmesa_installation() # OSMesa 관련 환경 변수 재설정 (강화된 설정) os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3' os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330' os.environ['OSMESA_HEADLESS'] = '1' os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1' os.environ['GALLIUM_DRIVER'] = 'llvmpipe' os.environ['LIBGL_ALWAYS_INDIRECT'] = '1' os.environ['MESA_LOADER_DRIVER_OVERRIDE'] = 'llvmpipe' # matplotlib 백엔드 설정 (GUI 없는 환경) try: import matplotlib matplotlib.use('Agg') print("matplotlib 백엔드를 Agg로 설정했습니다.") except ImportError: print("matplotlib을 찾을 수 없습니다.") # trimesh OSMesa 백엔드 설정 try: import trimesh if hasattr(trimesh, 'rendering'): import trimesh.rendering print("trimesh 렌더링 모듈이 로드되었습니다.") else: print("trimesh 렌더링 모듈을 찾을 수 없습니다.") except ImportError as ie: print(f"trimesh 렌더링 모듈 import 실패: {ie}") # Open3D 헤드리스 설정 try: import open3d as o3d print(f"Open3D 버전: {o3d.__version__}") except ImportError: print("Open3D를 찾을 수 없습니다.") except Exception as e: print(f"OSMesa 설정 중 경고: {e}") # 설정 실패해도 계속 진행 def _verify_osmesa_installation(self): """OSMesa 설치 상태 확인""" try: import subprocess import ctypes import glob print("OSMesa 설치 상태를 확인하는 중...") # 1. 시스템 라이브러리 확인 osmesa_found = False for lib_path in ['/usr/lib/x86_64-linux-gnu/libOSMesa.so.8', '/usr/lib/x86_64-linux-gnu/libOSMesa.so.6', '/usr/lib/x86_64-linux-gnu/libOSMesa.so']: if os.path.exists(lib_path): try: osmesa_lib = ctypes.CDLL(lib_path) print(f"✓ OSMesa 라이브러리가 발견되었습니다: {lib_path}") osmesa_found = True break except OSError: continue if not osmesa_found: print("⚠ OSMesa 라이브러리를 찾을 수 없습니다.") # 라이브러리 경로 검색 lib_paths = glob.glob('/usr/lib/x86_64-linux-gnu/libOSMesa*') if lib_paths: print(f"발견된 OSMesa 라이브러리: {lib_paths}") # 2. OpenGL 라이브러리 확인 try: gl_lib = ctypes.CDLL('libGL.so.1') print("✓ OpenGL 라이브러리가 발견되었습니다.") except OSError: print("⚠ OpenGL 라이브러리를 찾을 수 없습니다.") # 3. PyOpenGL 확인 try: import OpenGL import OpenGL.GL print(f"✓ PyOpenGL 버전: {OpenGL.__version__}") except ImportError: print("⚠ PyOpenGL을 가져올 수 없습니다.") # 4. 환경 변수 확인 platform = os.environ.get('PYOPENGL_PLATFORM', 'None') print(f"✓ PYOPENGL_PLATFORM: {platform}") # 5. LD_LIBRARY_PATH 확인 ld_path = os.environ.get('LD_LIBRARY_PATH', 'None') print(f"✓ LD_LIBRARY_PATH: {ld_path}") except Exception as e: print(f"OSMesa 설치 확인 중 오류: {e}") def _ensure_osmesa_context(self): """OSMesa 컨텍스트가 올바르게 초기화되었는지 확인하고 설정""" # 이미 설정된 경우 중복 실행 방지 if Renderer._osmesa_configured: return try: # OSMesa 관련 환경 변수 재확인 if os.environ.get('PYOPENGL_PLATFORM') != 'osmesa': os.environ['PYOPENGL_PLATFORM'] = 'osmesa' print("PYOPENGL_PLATFORM을 osmesa로 재설정했습니다.") # OSMesa 렌더링을 위한 추가 환경 변수 설정 os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3' os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330' os.environ['OSMESA_HEADLESS'] = '1' os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1' os.environ['GALLIUM_DRIVER'] = 'llvmpipe' # trimesh의 OSMesa 렌더링 백엔드 확인 (한 번만) import trimesh if hasattr(trimesh, 'rendering'): # OSMesa 렌더러 사용 가능 여부 확인 try: # trimesh의 렌더링 모듈에서 OSMesa 사용 가능 여부 확인 import trimesh.rendering print("trimesh 렌더링 모듈이 로드되었습니다.") except Exception as re: print(f"trimesh 렌더링 모듈 로드 실패: {re}") Renderer._osmesa_configured = True except Exception as e: print(f"OSMesa 컨텍스트 설정 중 오류: {e}") def _initialize_osmesa_context(self): """OSMesa 컨텍스트를 명시적으로 초기화""" try: # PyOpenGL을 사용하여 OSMesa 컨텍스트 직접 초기화 import OpenGL import OpenGL.GL import OpenGL.osmesa # OSMesa 컨텍스트 생성 context = OpenGL.osmesa.OSMesaCreateContext(OpenGL.GL.GL_RGBA, None) if context is None: raise RuntimeError("OSMesa 컨텍스트 생성 실패") # 버퍼 생성 buffer = (OpenGL.GL.GLubyte * (self.width * self.height * 4))() # 컨텍스트를 버퍼에 바인딩 if not OpenGL.osmesa.OSMesaMakeCurrent(context, buffer, OpenGL.GL.GL_UNSIGNED_BYTE, self.width, self.height): raise RuntimeError("OSMesa 컨텍스트 바인딩 실패") # OpenGL 상태 초기화 OpenGL.GL.glClearColor(0.0, 0.0, 0.0, 1.0) OpenGL.GL.glClear(OpenGL.GL.GL_COLOR_BUFFER_BIT | OpenGL.GL.GL_DEPTH_BUFFER_BIT) OpenGL.GL.glEnable(OpenGL.GL.GL_DEPTH_TEST) OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHTING) OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHT0) print("OSMesa 컨텍스트가 성공적으로 초기화되었습니다.") except Exception as e: print(f"OSMesa 컨텍스트 초기화 실패: {e}") # 실패해도 계속 진행 (trimesh가 자체적으로 처리할 수 있음) def _normalize_mesh(self, mesh: trimesh.Trimesh) -> trimesh.Trimesh: """메시를 적절한 크기로 정규화 (OSMesa 렌더링을 위해)""" try: # 메시 복사 normalized_mesh = mesh.copy() # 바운딩 박스 계산 bbox = normalized_mesh.bounds center = (bbox[0] + bbox[1]) / 2 size = bbox[1] - bbox[0] max_size = np.max(size) # 메시가 너무 작거나 큰 경우 정규화 if max_size < 0.1 or max_size > 10.0: # 중심을 원점으로 이동 normalized_mesh.vertices = normalized_mesh.vertices - center # 적절한 크기로 스케일링 (대각선 길이가 2가 되도록) scale_factor = 2.0 / max_size normalized_mesh.vertices = normalized_mesh.vertices * scale_factor print(f"메시를 정규화했습니다. 스케일 팩터: {scale_factor}") return normalized_mesh except Exception as e: print(f"메시 정규화 실패: {e}") return mesh def _create_empty_image(self) -> np.ndarray: """빈 이미지 생성""" return np.zeros((self.height, self.width, 3), dtype=np.uint8) def _configure_trimesh_rendering(self): """trimesh 렌더링을 위한 설정 (디스플레이 연결 방지)""" # 이미 설정된 경우 중복 실행 방지 if Renderer._osmesa_configured: return try: # trimesh가 디스플레이에 연결하지 않도록 강화된 환경 변수 설정 os.environ['PYOPENGL_PLATFORM'] = 'osmesa' os.environ['MESA_GL_VERSION_OVERRIDE'] = '3.3' os.environ['MESA_GLSL_VERSION_OVERRIDE'] = '330' os.environ['OSMESA_HEADLESS'] = '1' os.environ['LIBGL_ALWAYS_SOFTWARE'] = '1' os.environ['GALLIUM_DRIVER'] = 'llvmpipe' os.environ['DISPLAY'] = '' # 디스플레이 연결 방지 os.environ['QT_QPA_PLATFORM'] = 'offscreen' # Qt 오프스크린 모드 os.environ['MPLBACKEND'] = 'Agg' # matplotlib 백엔드 강제 설정 # trimesh의 렌더링 모듈에서 OSMesa 사용 가능 여부 확인 try: import trimesh import trimesh.rendering if hasattr(trimesh.rendering, 'SceneViewer'): print("trimesh SceneViewer 사용 가능") except ImportError as ie: print(f"trimesh 렌더링 모듈 import 실패: {ie}") # PyOpenGL OSMesa 모듈 직접 초기화 try: import OpenGL.osmesa import OpenGL.GL # OSMesa 라이브러리 로드 확인 print("OSMesa 라이브러리 로드 성공") except ImportError as ie: print(f"OSMesa 모듈 import 실패: {ie}") except Exception as e: print(f"trimesh 렌더링 설정 실패: {e}") def _render_with_direct_osmesa(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray: """직접 OSMesa를 사용한 렌더링 (GLU 없이, 메모리 안전)""" context = None try: import OpenGL.GL as GL import OpenGL.osmesa as OSMesa import numpy as np # OSMesa 컨텍스트 생성 context = OSMesa.OSMesaCreateContext(OSMesa.OSMESA_RGBA, None) if context is None: raise RuntimeError("OSMesa 컨텍스트 생성 실패") # 렌더링 버퍼 생성 buffer = (GL.GLubyte * (self.width * self.height * 4))() # 컨텍스트를 버퍼에 바인딩 if not OSMesa.OSMesaMakeCurrent(context, buffer, GL.GL_UNSIGNED_BYTE, self.width, self.height): raise RuntimeError("OSMesa 컨텍스트 바인딩 실패") # OpenGL 상태 설정 GL.glClearColor(0.0, 0.0, 0.0, 1.0) GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) GL.glEnable(GL.GL_DEPTH_TEST) GL.glEnable(GL.GL_LIGHTING) GL.glEnable(GL.GL_LIGHT0) # 뷰포트 설정 GL.glViewport(0, 0, self.width, self.height) # 투영 행렬 설정 (GLU 없이 직접 계산) GL.glMatrixMode(GL.GL_PROJECTION) GL.glLoadIdentity() # gluPerspective 대신 직접 투영 행렬 계산 fov = 45.0 aspect = self.width / self.height near = 0.1 far = 100.0 f = 1.0 / np.tan(np.radians(fov) / 2.0) projection_matrix = np.array([ [f/aspect, 0, 0, 0], [0, f, 0, 0], [0, 0, (far+near)/(near-far), (2*far*near)/(near-far)], [0, 0, -1, 0] ], dtype=np.float32) GL.glLoadMatrixf(projection_matrix.flatten()) # 모델뷰 행렬 설정 GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() # gluLookAt 대신 직접 뷰 행렬 계산 target = np.array([0, 0, 0]) up = np.array([0, 0, 1]) forward = target - camera_pos forward = forward / np.linalg.norm(forward) right = np.cross(forward, up) if np.linalg.norm(right) < 1e-6: right = np.array([1, 0, 0]) right = right / np.linalg.norm(right) up = np.cross(right, forward) view_matrix = np.array([ [right[0], up[0], -forward[0], 0], [right[1], up[1], -forward[1], 0], [right[2], up[2], -forward[2], 0], [-np.dot(right, camera_pos), -np.dot(up, camera_pos), np.dot(forward, camera_pos), 1] ], dtype=np.float32) GL.glLoadMatrixf(view_matrix.flatten()) # 메시 렌더링 vertices = mesh.vertices faces = mesh.faces GL.glBegin(GL.GL_TRIANGLES) for face in faces: for vertex_idx in face: vertex = vertices[vertex_idx] GL.glVertex3f(vertex[0], vertex[1], vertex[2]) GL.glEnd() # 버퍼에서 이미지 데이터 추출 image_data = np.frombuffer(buffer, dtype=np.uint8) image_data = image_data.reshape((self.height, self.width, 4)) # RGBA를 RGB로 변환 image_rgb = image_data[:, :, :3] # 이미지 뒤집기 (OpenGL은 아래에서 위로 렌더링) image_rgb = np.flipud(image_rgb) return image_rgb except Exception as e: print(f"직접 OSMesa 렌더링 실패: {e}") raise finally: # 메모리 정리 (반드시 실행) if context is not None: try: OSMesa.OSMesaDestroyContext(context) except Exception as cleanup_error: print(f"OSMesa 컨텍스트 정리 중 오류: {cleanup_error}") def render_multiple_views(self, model: Dict, num_views: int = 36) -> List[np.ndarray]: """ 360도 다각도 렌더링을 수행합니다. Args: model (Dict): 3D 모델 정보 num_views (int): 렌더링할 뷰의 개수 Returns: List[np.ndarray]: 렌더링된 이미지 리스트 """ vertices = model['vertices'] faces = model['faces'] # trimesh 메시 객체 생성 mesh = trimesh.Trimesh(vertices=vertices, faces=faces) rendered_images = [] for i in range(num_views): # 각도 계산 (0도부터 360도까지) angle = (360.0 / num_views) * i # 카메라 위치 계산 camera_pos = self._calculate_camera_position(angle) # 렌더링 수행 rendered_image = self._render_single_view(mesh, camera_pos) rendered_images.append(rendered_image) return rendered_images def apply_lighting_conditions(self, model: Dict, lighting: str = 'default') -> Dict: """ 다양한 조명 조건을 적용합니다. Args: model (Dict): 3D 모델 정보 lighting (str): 조명 조건 ('default', 'bright', 'dim', 'side', 'top', 'front') Returns: Dict: 조명이 적용된 모델 정보 """ if lighting not in self.lighting_conditions: lighting = 'default' # 조명 정보를 모델에 추가 model_with_lighting = model.copy() model_with_lighting['lighting'] = self.lighting_conditions[lighting] return model_with_lighting def adjust_camera_parameters(self, distance: float, elevation: float, azimuth: float = 0.0) -> Dict: """ 카메라 파라미터를 조절합니다. Args: distance (float): 카메라 거리 elevation (float): 카메라 고도각 (도) azimuth (float): 카메라 방위각 (도) Returns: Dict: 카메라 파라미터 정보 """ self.camera_distance = distance self.camera_elevation = elevation self.camera_azimuth = azimuth camera_params = { 'distance': distance, 'elevation': elevation, 'azimuth': azimuth, 'position': self._calculate_camera_position(azimuth, elevation) } return camera_params def render_with_lighting(self, model: Dict, lighting: str = 'default', num_views: int = 8) -> List[np.ndarray]: """ 조명 조건을 적용하여 다각도 렌더링을 수행합니다. Args: model (Dict): 3D 모델 정보 lighting (str): 조명 조건 num_views (int): 렌더링할 뷰의 개수 Returns: List[np.ndarray]: 렌더링된 이미지 리스트 """ # 조명 적용 model_with_lighting = self.apply_lighting_conditions(model, lighting) # 다각도 렌더링 rendered_images = self.render_multiple_views(model_with_lighting, num_views) return rendered_images def _render_single_view(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray: """ trimesh OSMesa 기반 단일 뷰 렌더링 (수정된 버전) Args: mesh (trimesh.Trimesh): 렌더링할 메시 camera_pos (np.ndarray): 카메라 위치 Returns: np.ndarray: 렌더링된 이미지 """ try: # OSMesa 렌더링을 위한 추가 설정 (한 번만 실행) self._ensure_osmesa_context() self._configure_trimesh_rendering() # 메시 유효성 검사 및 정규화 if not hasattr(mesh, 'vertices') or len(mesh.vertices) == 0: print("메시에 유효한 정점이 없습니다.") return self._create_empty_image() # 메시 정규화 (중요: OSMesa가 제대로 작동하려면 메시가 적절한 크기여야 함) mesh = self._normalize_mesh(mesh) # 직접 OSMesa 렌더링 시도 try: return self._render_with_direct_osmesa(mesh, camera_pos) except Exception as e: print(f"OSMesa 렌더링 실패: {e}") # 간단한 정사영 렌더링으로 폴백 return self._render_simple_fallback(mesh, camera_pos) except Exception as e: print(f"렌더링 실패: {e}") # 간단한 정사영 렌더링으로 폴백 return self._render_simple_fallback(mesh, camera_pos) def _get_camera_transform_simple(self, camera_pos: np.ndarray) -> np.ndarray: """간단한 카메라 변환 행렬 생성 (trimesh용)""" # 카메라가 원점을 바라보도록 설정 target = np.array([0, 0, 0]) up = np.array([0, 0, 1]) # 카메라 방향 벡터 계산 forward = target - camera_pos forward = forward / np.linalg.norm(forward) # 오른쪽 벡터 계산 right = np.cross(forward, up) if np.linalg.norm(right) < 1e-6: # forward와 up이 평행한 경우 right = np.array([1, 0, 0]) right = right / np.linalg.norm(right) # 위쪽 벡터 재계산 up = np.cross(right, forward) # 변환 행렬 생성 transform = np.eye(4) transform[:3, 0] = right transform[:3, 1] = up transform[:3, 2] = -forward transform[:3, 3] = camera_pos return transform def _get_rotation_matrix(self, direction: np.ndarray) -> np.ndarray: """카메라 방향에 따른 회전 행렬 생성""" # 기본 up 벡터 up = np.array([0, 0, 1]) # right 벡터 계산 right = np.cross(direction, up) if np.linalg.norm(right) < 1e-6: # direction이 up과 평행한 경우 right = np.array([1, 0, 0]) right = right / np.linalg.norm(right) # up 벡터 재계산 up = np.cross(right, direction) up = up / np.linalg.norm(up) # 회전 행렬 구성 rotation = np.eye(3) rotation[:, 0] = right rotation[:, 1] = up rotation[:, 2] = -direction # OpenGL 좌표계에 맞게 반전 return rotation def _render_simple_projection(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray: """가장 안정적인 정사영 렌더링""" try: vertices = np.array(mesh.vertices) # 메시 정규화 bbox = mesh.bounds center = (bbox[0] + bbox[1]) / 2 size = bbox[1] - bbox[0] max_size = np.max(size) vertices_norm = (vertices - center) / max_size # 배경을 중간 회색으로 설정하여 엣지 검출 개선 image = np.full((self.height, self.width, 3), 128, dtype=np.uint8) # 카메라 방향 정사영 camera_direction = camera_pos / np.linalg.norm(camera_pos) projected = vertices_norm @ camera_direction # 2D 좌표 변환 scale = min(self.width, self.height) / 2.4 for i, (vertex, proj) in enumerate(zip(vertices_norm, projected)): if proj > 0: # 카메라 앞쪽만 x = int((vertex[0] * scale) + self.width / 2) y = int((vertex[1] * scale) + self.height / 2) if 0 <= x < self.width and 0 <= y < self.height: # 거리에 따른 색상 계산 (더 명확한 대비) intensity = max(50, min(255, int(255 * (1 - proj / 2)))) # 점 대신 작은 원으로 그리기 cv2.circle(image, (x, y), 2, (intensity, intensity, intensity), -1) # 인접한 점들과 선으로 연결 if i > 0: prev_vertex = vertices_norm[i-1] prev_proj = projected[i-1] if prev_proj > 0: prev_x = int((prev_vertex[0] * scale) + self.width / 2) prev_y = int((prev_vertex[1] * scale) + self.height / 2) if (0 <= prev_x < self.width and 0 <= prev_y < self.height and 0 <= x < self.width and 0 <= y < self.height): cv2.line(image, (prev_x, prev_y), (x, y), (intensity, intensity, intensity), 1) return image except Exception as e: print(f"정사영 렌더링 실패: {e}") return np.zeros((self.height, self.width, 3), dtype=np.uint8) def _render_simple_fallback(self, mesh: trimesh.Trimesh, camera_pos: np.ndarray) -> np.ndarray: """렌더링 실패 시 사용할 간단한 대체 방법 (메모리 안전)""" try: # 메시의 바운딩 박스 계산 bbox = mesh.bounds center = (bbox[0] + bbox[1]) / 2 size = bbox[1] - bbox[0] max_size = np.max(size) # 최소 크기 보장 if max_size < 1e-6: max_size = 1.0 # 간단한 정사영 렌더링 image = np.full((self.height, self.width, 3), 128, dtype=np.uint8) # 카메라 방향에 따른 정사영 camera_distance = np.linalg.norm(camera_pos) if camera_distance < 1e-6: camera_distance = 1.0 camera_direction = camera_pos / camera_distance # 정점들을 카메라 방향으로 정사영 vertices = mesh.vertices - center projected = vertices @ camera_direction # 정사영된 점들을 이미지 좌표로 변환 scale = min(self.width, self.height) / (max_size * 1.2) # 성능을 위해 샘플링 (너무 많은 정점이 있는 경우) num_vertices = len(vertices) if num_vertices > 10000: step = num_vertices // 10000 vertices = vertices[::step] projected = projected[::step] for i, (vertex, proj) in enumerate(zip(vertices, projected)): if proj > 0: # 카메라 앞쪽에 있는 점만 # 정사영 좌표 계산 x = int((vertex[0] * scale) + self.width / 2) y = int((vertex[1] * scale) + self.height / 2) if 0 <= x < self.width and 0 <= y < self.height: # 거리에 따른 색상 계산 intensity = max(0, min(255, int(255 * (1 - proj / max_size)))) image[y, x] = [intensity, intensity, intensity] return image except Exception as e: print(f"대체 렌더링도 실패: {e}") return np.zeros((self.height, self.width, 3), dtype=np.uint8) def _calculate_camera_position(self, azimuth: float, elevation: float = None) -> np.ndarray: """ 카메라 위치를 계산합니다. Args: azimuth (float): 방위각 (도) elevation (float): 고도각 (도) Returns: np.ndarray: 카메라 위치 [x, y, z] """ if elevation is None: elevation = self.camera_elevation # 각도를 라디안으로 변환 azimuth_rad = math.radians(azimuth) elevation_rad = math.radians(elevation) # 구면 좌표를 직교 좌표로 변환 x = self.camera_distance * math.cos(elevation_rad) * math.cos(azimuth_rad) y = self.camera_distance * math.cos(elevation_rad) * math.sin(azimuth_rad) z = self.camera_distance * math.sin(elevation_rad) return np.array([x, y, z]) def create_depth_map(self, model: Dict, camera_pos: np.ndarray) -> np.ndarray: """ 깊이 맵을 생성합니다. Args: model (Dict): 3D 모델 정보 camera_pos (np.ndarray): 카메라 위치 Returns: np.ndarray: 깊이 맵 """ vertices = model['vertices'] faces = model['faces'] # 메시를 점군으로 변환 pointcloud = vertices # 카메라 방향에 따른 간단한 변환 camera_distance = np.linalg.norm(camera_pos) camera_direction = camera_pos / camera_distance # 점들을 카메라 방향으로 정사영 camera_points = pointcloud - camera_pos # 깊이 값 추출 (카메라로부터의 거리) depths = np.linalg.norm(camera_points, axis=1) # 깊이 맵 생성 (간단한 투영) depth_map = np.zeros((self.height, self.width)) # 점들을 이미지 평면에 투영 for i, point in enumerate(camera_points): if depths[i] > 0: # 유효한 깊이를 가진 점만 # 간단한 정사영 x = int((point[0] * 100) + self.width / 2) y = int((point[1] * 100) + self.height / 2) if 0 <= x < self.width and 0 <= y < self.height: depth_map[y, x] = depths[i] return depth_map def get_rendering_statistics(self, rendered_images: List[np.ndarray]) -> Dict: """ 렌더링 결과의 통계를 계산합니다. Args: rendered_images (List[np.ndarray]): 렌더링된 이미지 리스트 Returns: Dict: 렌더링 통계 정보 """ if not rendered_images: return {} # 각 이미지의 통계 계산 stats = { 'num_images': len(rendered_images), 'image_size': rendered_images[0].shape, 'mean_brightness': [], 'std_brightness': [], 'mean_contrast': [] } for img in rendered_images: if len(img.shape) == 3: gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) else: gray = img stats['mean_brightness'].append(np.mean(gray)) stats['std_brightness'].append(np.std(gray)) stats['mean_contrast'].append(np.std(gray) / (np.mean(gray) + 1e-8)) # 전체 통계 계산 stats['avg_brightness'] = np.mean(stats['mean_brightness']) stats['avg_contrast'] = np.mean(stats['mean_contrast']) stats['brightness_std'] = np.std(stats['mean_brightness']) return stats