""" EMD (Earth Mover's Distance) 계산 모듈 3D 모델 간의 분포 유사성을 평가합니다. """ import numpy as np from typing import Dict from scipy.spatial.distance import cdist class EMCalculator: """Earth Mover's Distance 계산을 담당하는 클래스""" def __init__(self, num_points: int = 1000): """ EMD 계산기 초기화 Args: num_points (int): 점군 생성 시 사용할 점의 개수 """ self.num_points = num_points def calculate_emd(self, model_3d: Dict, reference_3d: Dict) -> float: """ 두 3D 모델 간의 EMD를 계산합니다. Args: model_3d (Dict): 변환된 3D 모델 정보 reference_3d (Dict): 참조 3D 모델 정보 Returns: float: EMD 값 (낮을수록 유사함) """ try: # 입력 데이터 검증 if not isinstance(model_3d, dict) or not isinstance(reference_3d, dict): print("EMD 계산: 잘못된 입력 데이터 형식") return float('inf') if 'vertices' not in model_3d or 'vertices' not in reference_3d: print("EMD 계산: vertices 키가 없음") return float('inf') # 메시를 점군으로 변환 pc1 = self._mesh_to_pointcloud(model_3d) pc2 = self._mesh_to_pointcloud(reference_3d) if len(pc1) == 0 or len(pc2) == 0: print("EMD 계산: 점군 변환 실패") return float('inf') # EMD 계산 emd_value = self._compute_emd(pc1, pc2) # 결과 검증 if np.isnan(emd_value) or np.isinf(emd_value): print(f"EMD 계산: 잘못된 결과 값 {emd_value}") return float('inf') return emd_value except Exception as e: print(f"EMD 계산 중 오류 발생: {e}") return float('inf') def _mesh_to_pointcloud(self, mesh: Dict) -> np.ndarray: """ 메시를 점군으로 변환합니다. Args: mesh (Dict): 3D 메시 정보 Returns: np.ndarray: 점군 데이터 """ if 'vertices' not in mesh: return np.array([]) vertices = mesh['vertices'] if len(vertices) == 0: return np.array([]) # vertices를 numpy 배열로 변환 (리스트인 경우 처리) if not isinstance(vertices, np.ndarray): try: vertices = np.array(vertices, dtype=np.float32) except (ValueError, TypeError) as e: print(f"EMD: vertices 변환 실패: {e}") return np.array([]) # vertices가 2D 배열인지 확인 if vertices.ndim != 2 or vertices.shape[1] != 3: print(f"EMD: 잘못된 vertices 형태: {vertices.shape}") return np.array([]) # 정점이 충분한 경우 랜덤 샘플링 if len(vertices) >= self.num_points: indices = np.random.choice(len(vertices), self.num_points, replace=False) return vertices[indices] else: # 정점이 부족한 경우 중복 허용 샘플링 indices = np.random.choice(len(vertices), self.num_points, replace=True) return vertices[indices] def _compute_emd(self, pc1: np.ndarray, pc2: np.ndarray) -> float: """ 두 점군 간의 EMD를 계산합니다. Args: pc1 (np.ndarray): 첫 번째 점군 pc2 (np.ndarray): 두 번째 점군 Returns: float: EMD 값 """ # 점군을 정규화 pc1_norm = self._normalize_pointcloud(pc1) pc2_norm = self._normalize_pointcloud(pc2) # 근사 EMD 계산 (최근접 이웃 기반) distances = cdist(pc1_norm, pc2_norm, metric='euclidean') min_distances_1 = np.min(distances, axis=1) min_distances_2 = np.min(distances, axis=0) # 근사 EMD = 평균 최단 거리 emd_value = (np.mean(min_distances_1) + np.mean(min_distances_2)) / 2.0 return emd_value def _normalize_pointcloud(self, pointcloud: np.ndarray) -> np.ndarray: """ 점군을 정규화합니다. Args: pointcloud (np.ndarray): 입력 점군 Returns: np.ndarray: 정규화된 점군 """ if len(pointcloud) == 0: return pointcloud # 중심을 원점으로 이동 center = np.mean(pointcloud, axis=0) centered = pointcloud - center # 스케일 정규화 (최대 거리를 1로) max_dist = np.max(np.linalg.norm(centered, axis=1)) if max_dist > 0: normalized = centered / max_dist else: normalized = centered return normalized