""" 평가 시스템 설정 파일 평가 지표별 가중치, 임계값, 렌더링 파라미터 등을 정의합니다. """ import math import numpy as np # 평가 지표별 가중치 설정 EVALUATION_WEIGHTS = { '2d_map': 0.25, # 2D mAP 가중치 '3d_map': 0.30, # 3D mAP 가중치 'chamfer_distance': 0.20, # Chamfer Distance 가중치 'emd': 0.15, # EMD 가중치 'class_accuracy': 0.10 # 클래스 정확도 가중치 } # 평가 지표별 임계값 설정 (합리적인 기준 적용) EVALUATION_THRESHOLDS = { '2d_map': 0.8, # 2D mAP 임계값 '3d_map': 0.7, # 3D mAP 임계값 'chamfer_distance': 0.3, # Chamfer Distance 임계값 (합리적으로 조정) 'emd': 0.3, # EMD 임계값 (합리적으로 조정) 'class_accuracy': 0.8 # 클래스 정확도 임계값 (80% 정확도에서 50점) } # 렌더링 파라미터 설정 RENDERING_CONFIG = { 'num_views': 36, # 렌더링할 뷰의 개수 'image_size': (512, 512), # 렌더링 이미지 크기 (width, height) 'lighting_conditions': ['default', 'bright', 'dim'], # 조명 조건 'camera_distance': 2.0, # 카메라 거리 'camera_elevation': 30.0, # 카메라 고도각 'num_views_for_evaluation': 8 # 평가용 뷰 개수 } # 점군 생성 설정 POINTCLOUD_CONFIG = { 'num_points': 10000, # 기본 점군 점의 개수 'num_points_emd': 5000, # EMD 계산용 점의 개수 'num_points_chamfer': 10000 # Chamfer Distance 계산용 점의 개수 } # 클래스 분류 설정 CLASSIFICATION_CONFIG = { 'num_classes': 10, # 예상 클래스 개수 'class_names': [ # 클래스 이름 리스트 'chair', 'table', 'sofa', 'bed', 'desk', 'bookshelf', 'lamp', 'cabinet', 'door', 'window' ], 'clustering_enabled': True, # 클러스터링 기반 분류 사용 여부 'num_clusters': 5 # 클러스터 개수 } # 성능 등급 기준 GRADE_THRESHOLDS = { 'A': 90.0, # A등급: 90점 이상 'B': 80.0, # B등급: 80점 이상 'C': 70.0, # C등급: 70점 이상 'D': 60.0, # D등급: 60점 이상 'F': 0.0 # F등급: 60점 미만 } # IoU 임계값 설정 IOU_THRESHOLDS = { '2d_iou': [0.5, 0.75], # 2D IoU 임계값 리스트 '3d_iou': [0.5, 0.7] # 3D IoU 임계값 리스트 } # 출력 설정 OUTPUT_CONFIG = { 'save_images': True, # 렌더링된 이미지 저장 여부 'save_pointclouds': False, # 점군 데이터 저장 여부 'generate_report': True, # 리포트 생성 여부 'report_format': 'html', # 리포트 형식 ('html', 'json', 'txt') 'visualize_results': True # 결과 시각화 여부 } # 파일 경로 설정 FILE_PATHS = { 'output_dir': 'results', # 결과 출력 디렉토리 'images_dir': 'results/images', # 이미지 저장 디렉토리 'reports_dir': 'results/reports', # 리포트 저장 디렉토리 'temp_dir': 'results/temp' # 임시 파일 디렉토리 } # 로깅 설정 LOGGING_CONFIG = { 'level': 'INFO', # 로그 레벨 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', 'file': 'results/evaluation.log' # 로그 파일 경로 } # 성능 최적화 설정 PERFORMANCE_CONFIG = { 'parallel_processing': True, # 병렬 처리 사용 여부 'max_workers': 4, # 최대 워커 수 'memory_limit_gb': 8, # 메모리 제한 (GB) 'cache_enabled': True, # 캐시 사용 여부 'cache_size_mb': 100 # 캐시 크기 (MB) } # 검증 설정 VALIDATION_CONFIG = { 'validate_input_files': True, # 입력 파일 검증 여부 'check_file_formats': True, # 파일 형식 검증 여부 'min_file_size_kb': 1, # 최소 파일 크기 (KB) 'max_file_size_mb': 100 # 최대 파일 크기 (MB) } # 전체 평가 설정 EVALUATION_CONFIG = { 'weights': EVALUATION_WEIGHTS, 'thresholds': EVALUATION_THRESHOLDS, 'rendering': RENDERING_CONFIG, 'pointcloud': POINTCLOUD_CONFIG, 'classification': CLASSIFICATION_CONFIG, 'grade_thresholds': GRADE_THRESHOLDS, 'iou_thresholds': IOU_THRESHOLDS, 'output': OUTPUT_CONFIG, 'file_paths': FILE_PATHS, 'logging': LOGGING_CONFIG, 'performance': PERFORMANCE_CONFIG, 'validation': VALIDATION_CONFIG } # 설정 검증 함수 def validate_config(config: dict) -> bool: """ 설정의 유효성을 검증합니다. Args: config (dict): 검증할 설정 Returns: bool: 설정 유효성 """ try: # 가중치 합이 1.0인지 확인 weights = config.get('weights', {}) weight_sum = sum(weights.values()) if abs(weight_sum - 1.0) > 1e-6: print(f"경고: 가중치 합이 1.0이 아닙니다: {weight_sum}") # 임계값이 유효한 범위인지 확인 thresholds = config.get('thresholds', {}) for metric, threshold in thresholds.items(): if not (0.0 <= threshold <= 1.0): print(f"경고: {metric} 임계값이 유효 범위를 벗어났습니다: {threshold}") # 렌더링 설정 검증 rendering = config.get('rendering', {}) if rendering.get('num_views', 0) <= 0: print("경고: 렌더링 뷰 개수가 0 이하입니다") return True except Exception as e: print(f"설정 검증 실패: {e}") return False # 설정 업데이트 함수 def update_config(new_config: dict) -> dict: """ 새로운 설정으로 기존 설정을 업데이트합니다. Args: new_config (dict): 새로운 설정 Returns: dict: 업데이트된 설정 """ updated_config = EVALUATION_CONFIG.copy() for key, value in new_config.items(): if key in updated_config: if isinstance(value, dict) and isinstance(updated_config[key], dict): updated_config[key].update(value) else: updated_config[key] = value else: updated_config[key] = value return updated_config # 설정 가져오기 함수 def get_config(config_name: str = None): """ 설정을 가져옵니다. Args: config_name (str): 가져올 설정 이름 (None이면 전체 설정) Returns: 설정 값 또는 전체 설정 """ if config_name is None: return EVALUATION_CONFIG else: return EVALUATION_CONFIG.get(config_name, {}) # 개선된 점수 정규화 함수 def normalize_score_improved(metric_name: str, raw_value: float, threshold: float = None) -> float: """ 개선된 점수 정규화 (동적 임계값, 무한대 값 처리, 지표별 특성 고려). Args: metric_name (str): 지표 이름 raw_value (float): 원본 값 threshold (float): 임계값 (None이면 기본값 사용) Returns: float: 정규화된 점수 (0-100) """ import math # 무한대 값 처리 (더 관대한 처리) if math.isinf(raw_value): if raw_value > 0: # 양의 무한대 # 무한대 값을 임계값의 10배로 간주하여 부분 점수 부여 if threshold is None: threshold = EVALUATION_THRESHOLDS.get(metric_name, 1.0) return max(5.0, 100.0 * threshold / (threshold * 10)) # 최소 5점 보장 else: # 음의 무한대 return 100.0 # NaN 값 처리 if math.isnan(raw_value): return 0.0 # 기본 임계값 설정 if threshold is None: threshold = EVALUATION_THRESHOLDS.get(metric_name, 1.0) # 지표별 특성에 따른 정규화 if metric_name in ['chamfer_distance', 'emd']: # 낮을수록 좋은 지표 (역정규화) return _normalize_lower_is_better(raw_value, threshold) else: # 높을수록 좋은 지표 return _normalize_higher_is_better(raw_value, threshold) def _normalize_lower_is_better(raw_value: float, threshold: float) -> float: """ 낮을수록 좋은 메트릭을 위한 정규화 함수 (완화된 기준 적용) Args: raw_value: 원본 값 threshold: 임계값 (이 값에서 70점) Returns: 정규화된 점수 (0-100) """ if raw_value <= 0: return 100.0 # 완화된 임계값 검증 (임계값의 10배 이상이면 0점) if raw_value > threshold * 10: return 0.0 # 완화된 정규화 적용 ratio = raw_value / threshold if ratio <= 0.5: # 임계값의 절반 이하: 90-100점 (우수) normalized = 100.0 - (ratio * 20.0) elif ratio <= 1.0: # 임계값 이하: 70-90점 (양호) normalized = 90.0 - ((ratio - 0.5) * 40.0) elif ratio <= 2.0: # 임계값의 2배 이하: 40-70점 (보통) normalized = 70.0 - ((ratio - 1.0) * 30.0) else: # 임계값의 2배 초과: 0-40점 (완만한 지수적 감소) normalized = 40.0 * math.exp(-(ratio - 2.0) * 1.0) return max(0.0, min(100.0, normalized)) def _normalize_higher_is_better(raw_value: float, threshold: float) -> float: """높을수록 좋은 지표의 정규화 (더 엄격한 기준 적용).""" if raw_value <= 0: return 0.0 # 더 엄격한 정규화 적용 ratio = raw_value / threshold if ratio >= 1.0: # 임계값 이상: 80-100점 (우수) normalized = 80.0 + min(20.0, (ratio - 1.0) * 20.0) elif ratio >= 0.8: # 임계값의 80% 이상: 60-80점 (양호) normalized = 60.0 + ((ratio - 0.8) * 100.0) elif ratio >= 0.5: # 임계값의 50% 이상: 30-60점 (보통) normalized = 30.0 + ((ratio - 0.5) * 100.0) else: # 임계값의 50% 미만: 0-30점 (부족) normalized = ratio * 60.0 return max(0.0, min(100.0, normalized)) def calculate_dynamic_threshold(metric_name: str, historical_values: list) -> float: """ 동적 임계값 계산 (과거 값들을 기반으로). Args: metric_name (str): 지표 이름 historical_values (list): 과거 값들의 리스트 Returns: float: 동적 임계값 """ if not historical_values: return EVALUATION_THRESHOLDS.get(metric_name, 1.0) import numpy as np # 이상치 제거 (IQR 방법) q1 = np.percentile(historical_values, 25) q3 = np.percentile(historical_values, 75) iqr = q3 - q1 if iqr > 0: lower_bound = q1 - 1.5 * iqr upper_bound = q3 + 1.5 * iqr filtered_values = [v for v in historical_values if lower_bound <= v <= upper_bound] else: filtered_values = historical_values if not filtered_values: return EVALUATION_THRESHOLDS.get(metric_name, 1.0) # 지표별 동적 임계값 계산 if metric_name in ['chamfer_distance', 'emd']: # 낮을수록 좋은 지표: 75% 분위수 사용 dynamic_threshold = np.percentile(filtered_values, 75) else: # 높을수록 좋은 지표: 25% 분위수 사용 dynamic_threshold = np.percentile(filtered_values, 25) # 기본 임계값과의 가중 평균 (안정성 확보) default_threshold = EVALUATION_THRESHOLDS.get(metric_name, 1.0) weight = 0.7 # 동적 임계값에 더 높은 가중치 final_threshold = weight * dynamic_threshold + (1 - weight) * default_threshold return max(0.001, final_threshold) # 최소값 보장 def get_metric_characteristics(metric_name: str) -> dict: """ 지표별 특성 정보를 반환합니다. Args: metric_name (str): 지표 이름 Returns: dict: 지표 특성 정보 """ characteristics = { '2d_map': { 'type': 'higher_is_better', 'range': (0.0, 1.0), 'description': '2D 객체 감지 정확도', 'normalization_method': 'sigmoid' }, '3d_map': { 'type': 'higher_is_better', 'range': (0.0, 1.0), 'description': '3D 객체 감지 정확도', 'normalization_method': 'sigmoid' }, 'chamfer_distance': { 'type': 'lower_is_better', 'range': (0.0, float('inf')), 'description': '3D 기하학적 유사성', 'normalization_method': 'exponential' }, 'emd': { 'type': 'lower_is_better', 'range': (0.0, float('inf')), 'description': '3D 분포 유사성', 'normalization_method': 'exponential' }, 'class_accuracy': { 'type': 'higher_is_better', 'range': (0.0, 1.0), 'description': '클래스 분류 정확도', 'normalization_method': 'linear' } } return characteristics.get(metric_name, { 'type': 'higher_is_better', 'range': (0.0, 1.0), 'description': '알 수 없는 지표', 'normalization_method': 'linear' }) def validate_score_normalization(metric_name: str, raw_value: float, normalized_score: float) -> bool: """ 점수 정규화 결과의 유효성을 검증합니다. Args: metric_name (str): 지표 이름 raw_value (float): 원본 값 normalized_score (float): 정규화된 점수 Returns: bool: 유효성 여부 """ # 기본 범위 검증 if not (0.0 <= normalized_score <= 100.0): return False # 무한대/NaN 값 처리 검증 import math if math.isinf(raw_value) or math.isnan(raw_value): return 0.0 <= normalized_score <= 100.0 # 지표별 특성 검증 characteristics = get_metric_characteristics(metric_name) if characteristics['type'] == 'higher_is_better': # 높을수록 좋은 지표: 원본 값이 높으면 정규화 점수도 높아야 함 if raw_value > 0 and normalized_score < 10: return False else: # 낮을수록 좋은 지표: 원본 값이 낮으면 정규화 점수는 높아야 함 if raw_value < 0.1 and normalized_score < 50: return False return True