Commit 80cdf0be authored by Administrator's avatar Administrator
Browse files

initial draft

parents
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T11:23:28.468568",
"comprehensive_score": 45.0,
"grade": "F",
"metrics": {
"2d_map": 0.0,
"3d_map": 0.0,
"chamfer_distance": 0.2798835014834724,
"emd": 0.21941491365858662,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.25,
"weighted_score": 0.0,
"threshold": 0.8
},
"3d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.3,
"weighted_score": 0.0,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.2798835014834724,
"normalized_score": 100.0,
"weight": 0.2,
"weighted_score": 20.0,
"threshold": 0.3
},
"emd": {
"raw_value": 0.21941491365858662,
"normalized_score": 100.0,
"weight": 0.15,
"weighted_score": 15.0,
"threshold": 0.25
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 100.0,
"weight": 0.1,
"weighted_score": 10.0,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.25,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T11:37:05.025048",
"comprehensive_score": 45.0,
"grade": "F",
"metrics": {
"2d_map": 0.0,
"3d_map": 0.0,
"chamfer_distance": 0.2800380478607029,
"emd": 0.21903982341488692,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.25,
"weighted_score": 0.0,
"threshold": 0.8
},
"3d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.3,
"weighted_score": 0.0,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.2800380478607029,
"normalized_score": 100.0,
"weight": 0.2,
"weighted_score": 20.0,
"threshold": 0.3
},
"emd": {
"raw_value": 0.21903982341488692,
"normalized_score": 100.0,
"weight": 0.15,
"weighted_score": 15.0,
"threshold": 0.25
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 100.0,
"weight": 0.1,
"weighted_score": 10.0,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.25,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T11:44:16.815246",
"comprehensive_score": 45.0,
"grade": "F",
"metrics": {
"2d_map": 0.0,
"3d_map": 0.0,
"chamfer_distance": 0.28474688752201116,
"emd": 0.2301394782819322,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.25,
"weighted_score": 0.0,
"threshold": 0.8
},
"3d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.3,
"weighted_score": 0.0,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.28474688752201116,
"normalized_score": 100.0,
"weight": 0.2,
"weighted_score": 20.0,
"threshold": 0.3
},
"emd": {
"raw_value": 0.2301394782819322,
"normalized_score": 100.0,
"weight": 0.15,
"weighted_score": 15.0,
"threshold": 0.25
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 100.0,
"weight": 0.1,
"weighted_score": 10.0,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.25,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T12:00:32.982368",
"comprehensive_score": 45.0,
"grade": "F",
"metrics": {
"2d_map": 0.0,
"3d_map": 0.0,
"chamfer_distance": 0.28097640962810566,
"emd": 0.22847263153188707,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.25,
"weighted_score": 0.0,
"threshold": 0.8
},
"3d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.3,
"weighted_score": 0.0,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.28097640962810566,
"normalized_score": 100.0,
"weight": 0.2,
"weighted_score": 20.0,
"threshold": 0.3
},
"emd": {
"raw_value": 0.22847263153188707,
"normalized_score": 100.0,
"weight": 0.15,
"weighted_score": 15.0,
"threshold": 0.25
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 100.0,
"weight": 0.1,
"weighted_score": 10.0,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.25,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T12:59:09.754619",
"comprehensive_score": 53.333333333333336,
"grade": "F",
"metrics": {
"2d_map": 0.3333333333333334,
"3d_map": 0.0,
"chamfer_distance": 0.27697559784924264,
"emd": 0.22405463178811297,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 0.3333333333333334,
"normalized_score": 33.33333333333334,
"weight": 0.25,
"weighted_score": 8.333333333333336,
"threshold": 0.8
},
"3d_map": {
"raw_value": 0.0,
"normalized_score": 0.0,
"weight": 0.3,
"weighted_score": 0.0,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.27697559784924264,
"normalized_score": 100.0,
"weight": 0.2,
"weighted_score": 20.0,
"threshold": 0.3
},
"emd": {
"raw_value": 0.22405463178811297,
"normalized_score": 100.0,
"weight": 0.15,
"weighted_score": 15.0,
"threshold": 0.25
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 100.0,
"weight": 0.1,
"weighted_score": 10.0,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.25,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T13:05:51.373507",
"comprehensive_score": 100.0,
"grade": "A",
"metrics": {
"2d_map": 1.0000000000000002,
"3d_map": 1.0000000000000002,
"chamfer_distance": 0.2831834396905584,
"emd": 0.22191476593741244,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 100.0,
"weight": 0.25,
"weighted_score": 25.0,
"threshold": 0.8
},
"3d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 100.0,
"weight": 0.3,
"weighted_score": 30.0,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.2831834396905584,
"normalized_score": 100.0,
"weight": 0.2,
"weighted_score": 20.0,
"threshold": 0.3
},
"emd": {
"raw_value": 0.22191476593741244,
"normalized_score": 100.0,
"weight": 0.15,
"weighted_score": 15.0,
"threshold": 0.25
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 100.0,
"weight": 0.1,
"weighted_score": 10.0,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.25,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T13:22:07.745472",
"comprehensive_score": 57.20520233072756,
"grade": "F",
"metrics": {
"2d_map": 1.0000000000000002,
"3d_map": 1.0000000000000002,
"chamfer_distance": 0.2776959431513186,
"emd": 0.22648257984954112,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 85.0,
"weight": 0.25,
"weighted_score": 21.25,
"threshold": 0.8
},
"3d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 88.57142857142858,
"weight": 0.3,
"weighted_score": 26.571428571428573,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.2776959431513186,
"normalized_score": 1.430614600362696,
"weight": 0.2,
"weighted_score": 0.2861229200725392,
"threshold": 0.1
},
"emd": {
"raw_value": 0.22648257984954112,
"normalized_score": 3.9843389281763355,
"weight": 0.15,
"weighted_score": 0.5976508392264503,
"threshold": 0.1
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 85.0,
"weight": 0.1,
"weighted_score": 8.5,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.1,
"emd": 0.1,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-11T13:47:20.932571",
"comprehensive_score": 82.84805591099106,
"grade": "B",
"metrics": {
"2d_map": 1.0000000000000002,
"3d_map": 1.0000000000000002,
"chamfer_distance": 0.27580806871153724,
"emd": 0.23092454140649243,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 85.0,
"weight": 0.25,
"weighted_score": 21.25,
"threshold": 0.8
},
"3d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 88.57142857142858,
"weight": 0.3,
"weighted_score": 26.571428571428573,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.27580806871153724,
"normalized_score": 73.2255908384617,
"weight": 0.2,
"weighted_score": 14.64511816769234,
"threshold": 0.3
},
"emd": {
"raw_value": 0.23092454140649243,
"normalized_score": 79.210061145801,
"weight": 0.15,
"weighted_score": 11.881509171870151,
"threshold": 0.3
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 85.0,
"weight": 0.1,
"weighted_score": 8.5,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.3,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
{
"model_path": "/app/models/model.glb",
"reference_path": "/app/data/model_image.png",
"evaluation_timestamp": "2025-09-12T11:44:08.578711",
"comprehensive_score": 82.87725715080788,
"grade": "B",
"metrics": {
"2d_map": 1.0000000000000002,
"3d_map": 1.0000000000000002,
"chamfer_distance": 0.2770361566262666,
"emd": 0.22782702886267892,
"class_accuracy": 1.0
},
"score_details": {
"2d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 85.0,
"weight": 0.25,
"weighted_score": 21.25,
"threshold": 0.8
},
"3d_map": {
"raw_value": 1.0000000000000002,
"normalized_score": 88.57142857142858,
"weight": 0.3,
"weighted_score": 26.571428571428573,
"threshold": 0.7
},
"chamfer_distance": {
"raw_value": 0.2770361566262666,
"normalized_score": 73.06184578316444,
"weight": 0.2,
"weighted_score": 14.612369156632889,
"threshold": 0.3
},
"emd": {
"raw_value": 0.22782702886267892,
"normalized_score": 79.62306281830948,
"weight": 0.15,
"weighted_score": 11.943459422746422,
"threshold": 0.3
},
"class_accuracy": {
"raw_value": 1.0,
"normalized_score": 85.0,
"weight": 0.1,
"weighted_score": 8.5,
"threshold": 0.8
}
},
"config_used": {
"weights": {
"2d_map": 0.25,
"3d_map": 0.3,
"chamfer_distance": 0.2,
"emd": 0.15,
"class_accuracy": 0.1
},
"thresholds": {
"2d_map": 0.8,
"3d_map": 0.7,
"chamfer_distance": 0.3,
"emd": 0.3,
"class_accuracy": 0.8
},
"rendering": {
"num_views": 36,
"image_size": [
512,
512
],
"lighting_conditions": [
"default",
"bright",
"dim"
],
"camera_distance": 2.0,
"camera_elevation": 30.0,
"num_views_for_evaluation": 8
},
"pointcloud": {
"num_points": 10000,
"num_points_emd": 5000,
"num_points_chamfer": 10000
},
"classification": {
"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,
"B": 80.0,
"C": 70.0,
"D": 60.0,
"F": 0.0
},
"iou_thresholds": {
"2d_iou": [
0.5,
0.75
],
"3d_iou": [
0.5,
0.7
]
},
"output": {
"save_images": true,
"save_pointclouds": false,
"generate_report": true,
"report_format": "html",
"visualize_results": true
},
"file_paths": {
"output_dir": "results",
"images_dir": "results/images",
"reports_dir": "results/reports",
"temp_dir": "results/temp"
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "results/evaluation.log"
},
"performance": {
"parallel_processing": true,
"max_workers": 4,
"memory_limit_gb": 8,
"cache_enabled": true,
"cache_size_mb": 100
},
"validation": {
"validate_input_files": true,
"check_file_formats": true,
"min_file_size_kb": 1,
"max_file_size_mb": 100
}
}
}
\ No newline at end of file
#!/usr/bin/env python3
import sys
import os
sys.path.append('src')
from evaluator import normalize_score_improved
# 테스트 케이스
print('=== 정규화 점수 테스트 결과 ===')
print()
# 실제 계산된 값들로 테스트
chamfer_old = normalize_score_improved('chamfer_distance', 0.2777, 0.1)
chamfer_new = normalize_score_improved('chamfer_distance', 0.2777, 0.3)
emd_old = normalize_score_improved('emd', 0.2265, 0.1)
emd_new = normalize_score_improved('emd', 0.2265, 0.3)
print(f'chamfer_distance: 0.2777')
print(f' 기존 임계값 0.1: {chamfer_old:.1f}점')
print(f' 새 임계값 0.3: {chamfer_new:.1f}점')
print(f' 개선: {chamfer_new - chamfer_old:+.1f}점')
print()
print(f'emd: 0.2265')
print(f' 기존 임계값 0.1: {emd_old:.1f}점')
print(f' 새 임계값 0.3: {emd_new:.1f}점')
print(f' 개선: {emd_new - emd_old:+.1f}점')
print()
# 비율 계산
print('비율 분석:')
print(f'chamfer_distance: 0.2777/0.3 = {0.2777/0.3:.3f} (새 임계값 기준)')
print(f'emd: 0.2265/0.3 = {0.2265/0.3:.3f} (새 임계값 기준)')
"""
3D 모델 및 이미지 데이터 로더
GLB 파일과 이미지 파일을 로드하고 전처리하는 기능을 제공합니다.
"""
import numpy as np
import trimesh
import open3d as o3d
from PIL import Image
import cv2
from typing import Dict, List, Tuple, Optional
import os
class DataLoader:
"""3D 모델과 이미지 데이터를 로드하고 전처리하는 클래스"""
def __init__(self):
self.supported_3d_formats = ['.glb', '.gltf', '.obj', '.ply', '.stl']
self.supported_image_formats = ['.png', '.jpg', '.jpeg', '.bmp', '.tiff']
def load_glb_model(self, path: str) -> Dict:
"""
GLB 파일을 로드하고 메시 정보를 추출합니다.
Args:
path (str): GLB 파일 경로
Returns:
Dict: 3D 모델 정보 (vertices, faces, textures, materials)
"""
try:
# trimesh를 사용하여 GLB 파일 로드
scene = trimesh.load(path)
# 메시 정보 추출
if hasattr(scene, 'geometry'):
# Scene인 경우 첫 번째 메시 사용
mesh = list(scene.geometry.values())[0]
else:
# 단일 메시인 경우
mesh = scene
model_info = {
'vertices': np.array(mesh.vertices),
'faces': np.array(mesh.faces),
'textures': None,
'materials': None,
'bounding_box': mesh.bounds,
'center': mesh.centroid,
'scale': mesh.scale,
'file_path': path
}
# 텍스처 정보가 있는 경우
if hasattr(mesh.visual, 'material'):
model_info['materials'] = mesh.visual.material
# UV 좌표가 있는 경우
if hasattr(mesh.visual, 'uv'):
model_info['uv_coords'] = mesh.visual.uv
return model_info
except Exception as e:
raise ValueError(f"GLB 파일 로드 실패: {path}, 오류: {str(e)}")
def load_reference_image(self, path: str) -> np.ndarray:
"""
참조 이미지를 로드하고 전처리합니다.
Args:
path (str): 이미지 파일 경로
Returns:
np.ndarray: 전처리된 이미지 배열 (H, W, C)
"""
try:
# PIL을 사용하여 이미지 로드
image = Image.open(path)
# RGB로 변환 (RGBA인 경우)
if image.mode == 'RGBA':
image = image.convert('RGB')
elif image.mode != 'RGB':
image = image.convert('RGB')
# numpy 배열로 변환
image_array = np.array(image)
return image_array
except Exception as e:
raise ValueError(f"이미지 로드 실패: {path}, 오류: {str(e)}")
def extract_mesh_info(self, model: Dict) -> Dict:
"""
3D 모델에서 메시 정보를 추출합니다.
Args:
model (Dict): 3D 모델 정보
Returns:
Dict: 추출된 메시 정보
"""
vertices = model['vertices']
faces = model['faces']
# 메시 통계 계산
mesh_info = {
'num_vertices': len(vertices),
'num_faces': len(faces),
'bounding_box_size': np.max(vertices, axis=0) - np.min(vertices, axis=0),
'volume': self._calculate_mesh_volume(vertices, faces),
'surface_area': self._calculate_surface_area(vertices, faces),
'center_of_mass': np.mean(vertices, axis=0)
}
return mesh_info
def create_ground_truth(self, image: np.ndarray) -> Dict:
"""
이미지에서 Ground Truth 정보를 생성합니다.
Args:
image (np.ndarray): 입력 이미지
Returns:
Dict: Ground Truth 정보
"""
# 이미지 크기 정보
height, width = image.shape[:2]
# 간단한 객체 감지를 위한 전처리
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
# 엣지 검출
edges = cv2.Canny(gray, 50, 150)
# 윤곽선 찾기
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 바운딩 박스 생성
bounding_boxes = []
for contour in contours:
if cv2.contourArea(contour) > 100: # 최소 면적 필터
x, y, w, h = cv2.boundingRect(contour)
bounding_boxes.append({
'bbox': [x, y, x + w, y + h],
'area': w * h,
'confidence': 1.0
})
ground_truth = {
'image_size': (width, height),
'bounding_boxes': bounding_boxes,
'num_objects': len(bounding_boxes),
'image_histogram': np.histogram(gray, bins=256)[0]
}
return ground_truth
def mesh_to_pointcloud(self, vertices: np.ndarray, num_points: int = 10000) -> np.ndarray:
"""
메시를 점군으로 변환합니다.
Args:
vertices (np.ndarray): 메시 정점
num_points (int): 생성할 점의 개수
Returns:
np.ndarray: 점군 데이터
"""
if len(vertices) >= num_points:
# 정점이 충분한 경우 랜덤 샘플링
indices = np.random.choice(len(vertices), num_points, replace=False)
return vertices[indices]
else:
# 정점이 부족한 경우 중복 허용하여 샘플링
indices = np.random.choice(len(vertices), num_points, replace=True)
return vertices[indices]
def _calculate_mesh_volume(self, vertices: np.ndarray, faces: np.ndarray) -> float:
"""메시의 부피를 계산합니다."""
try:
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
return mesh.volume
except:
return 0.0
def _calculate_surface_area(self, vertices: np.ndarray, faces: np.ndarray) -> float:
"""메시의 표면적을 계산합니다."""
try:
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
return mesh.surface_area
except:
return 0.0
def validate_file(self, file_path: str, file_type: str = 'auto') -> bool:
"""
파일 유효성을 검사합니다.
Args:
file_path (str): 파일 경로
file_type (str): 파일 타입 ('3d', 'image', 'auto')
Returns:
bool: 파일 유효성
"""
if not os.path.exists(file_path):
return False
_, ext = os.path.splitext(file_path.lower())
if file_type == 'auto':
return (ext in self.supported_3d_formats or
ext in self.supported_image_formats)
elif file_type == '3d':
return ext in self.supported_3d_formats
elif file_type == 'image':
return ext in self.supported_image_formats
return False
"""
종합 평가 엔진
모든 평가 지표를 통합하여 종합 점수를 계산하고 성능 등급을 결정합니다.
"""
import numpy as np
import os
import logging
from typing import Dict, Tuple, Optional, List
from datetime import datetime
import json
from .data_loader import DataLoader
from .renderer import Renderer
from .metrics.map_2d import Map2DCalculator
from .metrics.map_3d import Map3DCalculator
from .metrics.chamfer_distance import ChamferDistanceCalculator
from .metrics.emd import EMCalculator
from .metrics.class_accuracy import ClassAccuracyCalculator
from .utils.reference_extractor import ReferenceDataExtractor
from .utils.performance_monitor import get_memory_profiler
from .utils.logging_utils import get_logger
from config.evaluation_config import EVALUATION_CONFIG
def normalize_score_improved(metric_name: str, metric_value: float, threshold: float) -> float:
"""
개선된 점수 정규화 함수 (0-100 범위, 더 엄격한 기준 적용)
Args:
metric_name (str): 지표 이름
metric_value (float): 원본 지표 값
threshold (float): 임계값
Returns:
float: 정규화된 점수 (0-100)
"""
import math
# 무한대/NaN 값 처리
if math.isinf(metric_value) or math.isnan(metric_value):
return 0.0
if metric_name in ['2d_map', '3d_map', 'class_accuracy']:
# 높을수록 좋은 지표 (더 엄격한 정규화)
ratio = metric_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))
elif metric_name in ['chamfer_distance', 'emd']:
# 낮을수록 좋은 지표 (거리 기반, 완화된 정규화)
if metric_value <= 0:
return 100.0
# 완화된 임계값 검증 (임계값의 10배 이상이면 0점)
if metric_value > threshold * 10:
return 0.0
# 완화된 정규화 적용
ratio = metric_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))
else:
# 기본 정규화
return min(100.0, max(0.0, metric_value * 100.0))
def validate_score_normalization(metric_name: str, original_value: float, normalized_score: float) -> bool:
"""
점수 정규화 결과의 유효성을 검증합니다.
Args:
metric_name (str): 지표 이름
original_value (float): 원본 값
normalized_score (float): 정규화된 점수
Returns:
bool: 유효성 여부
"""
# 기본 검증
if np.isnan(normalized_score) or np.isinf(normalized_score):
return False
if normalized_score < 0 or normalized_score > 100:
return False
# 지표별 특수 검증
if metric_name in ['2d_map', '3d_map', 'class_accuracy']:
# 높을수록 좋은 지표
if original_value < 0 or original_value > 1:
return False
elif metric_name in ['chamfer_distance', 'emd']:
# 낮을수록 좋은 지표
if original_value < 0:
return False
return True
class Evaluator:
"""종합 평가를 담당하는 클래스"""
def __init__(self, config: Dict = None):
"""
평가 엔진 초기화
Args:
config (Dict): 평가 설정 (None이면 기본 설정 사용)
"""
self.config = config if config else EVALUATION_CONFIG
# 로깅 설정
self._setup_logging()
# 컴포넌트 초기화
self.data_loader = DataLoader()
self.renderer = Renderer(
image_size=self.config['rendering']['image_size']
)
# 평가 지표 계산기 초기화
self.map_2d_calculator = Map2DCalculator()
self.map_3d_calculator = Map3DCalculator()
self.chamfer_calculator = ChamferDistanceCalculator()
self.emd_calculator = EMCalculator()
self.class_accuracy_calculator = ClassAccuracyCalculator()
# 참조 데이터 추출기 초기화
self.reference_extractor = ReferenceDataExtractor()
# 성능 모니터링 및 로깅 초기화
self.memory_profiler = get_memory_profiler()
self.detailed_logger = get_logger()
# 결과 저장용 디렉토리 생성
self._create_output_directories()
self.logger.info("평가 엔진이 초기화되었습니다.")
def evaluate_model(self, model_path: str, reference_path: str) -> Dict:
"""
3D 모델을 종합적으로 평가합니다.
Args:
model_path (str): 변환된 3D 모델 파일 경로
reference_path (str): 참조 이미지 파일 경로
Returns:
Dict: 평가 결과
"""
try:
# 입력 파일 검증
if not self._validate_input_files(model_path, reference_path):
raise ValueError("입력 파일 검증 실패")
# 데이터 로드
model_3d = self.data_loader.load_glb_model(model_path)
reference_image = self.data_loader.load_reference_image(reference_path)
# Ground Truth 생성
ground_truth_2d = self.data_loader.create_ground_truth(reference_image)
ground_truth_3d = self._create_3d_ground_truth(reference_path, model_path)
# 렌더링 수행
rendered_images = self.renderer.render_multiple_views(
model_3d,
self.config['rendering']['num_views_for_evaluation']
)
# 각 평가 지표 계산
metrics = self._calculate_all_metrics(
model_3d, ground_truth_3d, rendered_images,
reference_image, ground_truth_2d
)
# 종합 점수 계산
comprehensive_score, score_details = self.calculate_comprehensive_score(metrics)
# 성능 등급 결정
grade = self.determine_grade(comprehensive_score)
# 결과 정리
results = {
'model_path': model_path,
'reference_path': reference_path,
'evaluation_timestamp': datetime.now().isoformat(),
'comprehensive_score': comprehensive_score,
'grade': grade,
'metrics': metrics,
'score_details': score_details,
'config_used': self.config
}
# 결과 저장
if self.config['output']['generate_report']:
self._save_results(results)
self.logger.info(f"평가 완료 - 종합 점수: {comprehensive_score:.2f}, 등급: {grade}")
return results
except Exception as e:
self.logger.error(f"평가 중 오류 발생: {str(e)}")
raise
def calculate_comprehensive_score(self, metrics: Dict) -> Tuple[float, Dict]:
"""
가중치 기반 종합 점수를 계산합니다.
Args:
metrics (Dict): 개별 평가 지표 결과
Returns:
Tuple[float, Dict]: (종합 점수, 점수 세부사항)
"""
weights = self.config['weights']
thresholds = self.config['thresholds']
score_details = {}
weighted_sum = 0.0
total_weight = 0.0
for metric_name, metric_value in metrics.items():
if metric_name in weights:
weight = weights[metric_name]
threshold = thresholds[metric_name]
# 개선된 점수 정규화 (0-100 범위)
normalized_score = normalize_score_improved(metric_name, metric_value, threshold)
# 정규화 결과 검증
if not validate_score_normalization(metric_name, metric_value, normalized_score):
self.detailed_logger.main_logger.warning(
f"{metric_name} 정규화 결과가 유효하지 않습니다: "
f"원본={metric_value}, 정규화={normalized_score}"
)
# 가중치 적용
weighted_score = normalized_score * weight
weighted_sum += weighted_score
total_weight += weight
score_details[metric_name] = {
'raw_value': metric_value,
'normalized_score': normalized_score,
'weight': weight,
'weighted_score': weighted_score,
'threshold': threshold
}
# 최종 종합 점수
comprehensive_score = weighted_sum / total_weight if total_weight > 0 else 0.0
return comprehensive_score, score_details
def determine_grade(self, score: float) -> str:
"""
점수에 따라 성능 등급을 결정합니다.
Args:
score (float): 종합 점수 (0-100)
Returns:
str: 성능 등급 (A, B, C, D, F)
"""
grade_thresholds = self.config['grade_thresholds']
if score >= grade_thresholds['A']:
return 'A'
elif score >= grade_thresholds['B']:
return 'B'
elif score >= grade_thresholds['C']:
return 'C'
elif score >= grade_thresholds['D']:
return 'D'
else:
return 'F'
def generate_report(self, results: Dict) -> str:
"""
평가 결과 리포트를 생성합니다.
Args:
results (Dict): 평가 결과
Returns:
str: 생성된 리포트 내용
"""
report_format = self.config['output']['report_format']
if report_format == 'html':
return self._generate_html_report(results)
elif report_format == 'json':
return self._generate_json_report(results)
else:
return self._generate_text_report(results)
def _calculate_all_metrics(self, model_3d: Dict, ground_truth_3d: Dict,
rendered_images: List[np.ndarray],
reference_image: np.ndarray,
ground_truth_2d: Dict) -> Dict:
"""
모든 평가 지표를 계산합니다 (강화된 오류 처리).
Args:
model_3d (Dict): 3D 모델 정보
ground_truth_3d (Dict): 3D Ground Truth
rendered_images (List[np.ndarray]): 렌더링된 이미지들
reference_image (np.ndarray): 참조 이미지
ground_truth_2d (Dict): 2D Ground Truth
Returns:
Dict: 모든 평가 지표 결과
"""
metrics = {}
# 각 지표별 개별 오류 처리
metric_calculators = {
'2d_map': {
'calculator': self.map_2d_calculator,
'method': 'calculate_2d_map',
'args': (rendered_images, reference_image),
'default_value': 0.0,
'description': '2D mAP 계산'
},
'3d_map': {
'calculator': self.map_3d_calculator,
'method': 'calculate_3d_map',
'args': (model_3d, ground_truth_3d),
'default_value': 0.0,
'description': '3D mAP 계산'
},
'chamfer_distance': {
'calculator': self.chamfer_calculator,
'method': 'calculate_chamfer_distance',
'args': (model_3d, ground_truth_3d),
'default_value': float('inf'),
'description': 'Chamfer Distance 계산'
},
'emd': {
'calculator': self.emd_calculator,
'method': 'calculate_emd',
'args': (model_3d, ground_truth_3d),
'default_value': float('inf'),
'description': 'EMD 계산'
},
'class_accuracy': {
'calculator': self.class_accuracy_calculator,
'method': 'calculate_class_accuracy',
'args': (model_3d, ground_truth_3d),
'default_value': 0.0,
'description': '클래스 정확도 계산'
}
}
# 각 지표를 개별적으로 계산 (오류 격리)
for metric_name, config in metric_calculators.items():
try:
# 메모리 프로파일링 시작
self.memory_profiler.take_snapshot(f"{metric_name}_start")
# 상세 로깅 시작
self.detailed_logger.log_metric_start(
metric_name,
model_3d_size=len(model_3d.get('vertices', [])),
ground_truth_3d_size=len(ground_truth_3d.get('vertices', [])),
rendered_images_count=len(rendered_images) if rendered_images else 0
)
# 지표 계산
calculator = config['calculator']
method = getattr(calculator, config['method'])
result = method(*config['args'])
# 결과 검증
if result is None or (isinstance(result, float) and np.isnan(result)):
raise ValueError(f"{metric_name} 계산 결과가 유효하지 않습니다: {result}")
metrics[metric_name] = result
# 메모리 프로파일링 종료
self.memory_profiler.take_snapshot(f"{metric_name}_end")
# 상세 로깅 완료
self.detailed_logger.log_metric_result(metric_name, result)
self.logger.info(f"{config['description']} 완료: {result}")
except MemoryError as e:
# 메모리 부족 오류 처리
error_msg = f"{config['description']} 중 메모리 부족: {str(e)}"
self.logger.error(error_msg)
self.detailed_logger.log_metric_error(metric_name, e, {
'error_type': 'MemoryError',
'description': config['description']
})
# 메모리 부족 시 기본값 사용
metrics[metric_name] = config['default_value']
except Exception as e:
# 기타 오류 처리
error_msg = f"{config['description']} 중 오류 발생: {str(e)}"
self.logger.error(error_msg)
self.detailed_logger.log_metric_error(metric_name, e, {
'error_type': type(e).__name__,
'description': config['description'],
'args_info': {
'model_3d_keys': list(model_3d.keys()) if model_3d else [],
'ground_truth_3d_keys': list(ground_truth_3d.keys()) if ground_truth_3d else [],
'rendered_images_count': len(rendered_images) if rendered_images else 0
}
})
# 오류 발생 시 기본값 사용
metrics[metric_name] = config['default_value']
# 전체 메트릭 계산 결과 로깅
self.detailed_logger.main_logger.info("=== 모든 지표 계산 완료 ===")
for metric_name, value in metrics.items():
self.detailed_logger.main_logger.info(f"{metric_name}: {value}")
return metrics
def _create_3d_ground_truth(self, reference_image_path: str, model_3d_path: str) -> Dict:
"""
참조 이미지에서 올바른 Ground Truth 생성 (표준화된 구조)
Args:
reference_image_path: 참조 이미지 경로
model_3d_path: 3D 모델 경로 (Ground Truth가 아님)
Returns:
표준화된 Ground Truth 데이터
"""
try:
# 표준화된 Ground Truth 생성
ground_truth = self.reference_extractor.create_unified_ground_truth(
reference_image_path, model_3d_path
)
# 모델과 Ground Truth의 독립성 검증
self._validate_ground_truth_independence(ground_truth, model_3d_path)
# vertices 키 존재 여부 확인 및 검증
if 'vertices' not in ground_truth:
self.logger.warning("Ground Truth에 vertices 키가 없습니다. 재구성 중...")
ground_truth = self._reconstruct_ground_truth_vertices(ground_truth, reference_image_path)
# 메시 구조 일관성 확인
self._validate_mesh_consistency(ground_truth)
self.logger.info(f"표준화된 Ground Truth 생성 완료: {len(ground_truth.get('vertices', []))}개 vertices")
return ground_truth
except Exception as e:
self.logger.error(f"Error creating ground truth: {str(e)}")
# 오류 발생 시 대체 Ground Truth 생성
return self._create_fallback_ground_truth(reference_image_path)
def _extract_reference_data(self, reference_image_path: str) -> Dict:
"""참조 이미지에서 실제 객체 정보 추출"""
return self.reference_extractor.extract_reference_data(reference_image_path)
def _validate_ground_truth_independence(self, ground_truth: Dict, model_3d_path: str) -> bool:
"""모델과 Ground Truth의 독립성 검증"""
# Ground Truth가 모델과 다른지 확인
if not ground_truth.get('objects'):
self.logger.warning("Ground Truth에 객체가 없습니다")
return False
# 모델 파일 경로와 Ground Truth가 다른지 확인
self.logger.info(f"Ground Truth 독립성 검증: {len(ground_truth['objects'])}개 객체 발견")
return True
def _reconstruct_ground_truth_vertices(self, ground_truth: Dict, reference_image_path: str) -> Dict:
"""Ground Truth vertices 재구성"""
try:
self.logger.info("Ground Truth vertices 재구성 중...")
# 바운딩 박스가 있는 경우 vertices 재구성
if ground_truth.get('bounding_boxes'):
vertices = []
for bbox in ground_truth['bounding_boxes']:
x1, y1, x2, y2 = bbox
z_min, z_max = 0.0, 1.0
# 8개 꼭짓점 생성 (정육면체)
cube_vertices = [
[x1, y1, z_min], [x2, y1, z_min], [x2, y2, z_min], [x1, y2, z_min], # 하단
[x1, y1, z_max], [x2, y1, z_max], [x2, y2, z_max], [x1, y2, z_max] # 상단
]
vertices.extend(cube_vertices)
ground_truth['vertices'] = vertices
# faces도 재구성
if 'faces' not in ground_truth:
ground_truth['faces'] = self._generate_faces_from_vertices(vertices)
self.logger.info(f"vertices 재구성 완료: {len(vertices)}개 vertices")
else:
# 바운딩 박스가 없는 경우 기본 정육면체 생성
ground_truth['vertices'] = [
[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], # 하단
[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1] # 상단
]
ground_truth['faces'] = [
[0, 1, 2], [0, 2, 3], # 하단
[4, 7, 6], [4, 6, 5], # 상단
[0, 4, 5], [0, 5, 1], # 앞면
[2, 6, 7], [2, 7, 3], # 뒷면
[0, 3, 7], [0, 7, 4], # 왼쪽
[1, 5, 6], [1, 6, 2] # 오른쪽
]
self.logger.info("기본 정육면체 vertices 생성 완료")
return ground_truth
except Exception as e:
self.logger.error(f"vertices 재구성 중 오류 발생: {str(e)}")
# 오류 발생 시 기본 구조 반환
return self._create_fallback_ground_truth(reference_image_path)
def _generate_faces_from_vertices(self, vertices: List[List[float]]) -> List[List[int]]:
"""vertices에서 faces 생성"""
faces = []
num_objects = len(vertices) // 8 # 각 객체마다 8개 vertices
for i in range(num_objects):
base_idx = i * 8
# 정육면체의 12개 면 (각 면은 2개 삼각형으로 구성)
cube_faces = [
# 하단 면
[base_idx, base_idx+1, base_idx+2], [base_idx, base_idx+2, base_idx+3],
# 상단 면
[base_idx+4, base_idx+7, base_idx+6], [base_idx+4, base_idx+6, base_idx+5],
# 앞면
[base_idx, base_idx+4, base_idx+5], [base_idx, base_idx+5, base_idx+1],
# 뒷면
[base_idx+2, base_idx+6, base_idx+7], [base_idx+2, base_idx+7, base_idx+3],
# 왼쪽 면
[base_idx, base_idx+3, base_idx+7], [base_idx, base_idx+7, base_idx+4],
# 오른쪽 면
[base_idx+1, base_idx+5, base_idx+6], [base_idx+1, base_idx+6, base_idx+2]
]
faces.extend(cube_faces)
return faces
def _validate_mesh_consistency(self, ground_truth: Dict) -> bool:
"""메시 구조 일관성 확인"""
try:
vertices = ground_truth.get('vertices', [])
faces = ground_truth.get('faces', [])
if not vertices or not faces:
self.logger.warning("메시 데이터가 비어있습니다")
return False
# vertices 개수 확인
if len(vertices) == 0:
self.logger.error("vertices가 비어있습니다")
return False
# faces 인덱스 범위 확인
max_vertex_idx = len(vertices) - 1
for i, face in enumerate(faces):
if not isinstance(face, list) or len(face) != 3:
self.logger.error(f"faces[{i}]가 유효하지 않습니다: {face}")
return False
for vertex_idx in face:
if not isinstance(vertex_idx, int) or vertex_idx < 0 or vertex_idx > max_vertex_idx:
self.logger.error(f"faces[{i}]에 유효하지 않은 vertex 인덱스: {vertex_idx}")
return False
self.logger.info(f"메시 일관성 검증 완료: {len(vertices)}개 vertices, {len(faces)}개 faces")
return True
except Exception as e:
self.logger.error(f"메시 일관성 검증 중 오류 발생: {str(e)}")
return False
def _create_fallback_ground_truth(self, reference_image_path: str) -> Dict:
"""오류 발생 시 대체 Ground Truth 생성"""
self.logger.warning("대체 Ground Truth 생성 중...")
return {
'vertices': [
[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], # 하단
[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1] # 상단
],
'faces': [
[0, 1, 2], [0, 2, 3], # 하단
[4, 7, 6], [4, 6, 5], # 상단
[0, 4, 5], [0, 5, 1], # 앞면
[2, 6, 7], [2, 7, 3], # 뒷면
[0, 3, 7], [0, 7, 4], # 왼쪽
[1, 5, 6], [1, 6, 2] # 오른쪽
],
'center': [0.5, 0.5, 0.5],
'scale': [1.0, 1.0, 1.0],
'objects': [],
'bounding_boxes': [],
'class_labels': [],
'confidence_scores': [],
'metadata': {
'source_image': reference_image_path,
'extraction_method': 'fallback',
'timestamp': self._get_timestamp(),
'version': '1.0',
'error': 'fallback_created'
}
}
def _get_timestamp(self) -> str:
"""현재 시간 스탬프 반환"""
from datetime import datetime
return datetime.now().isoformat()
def _validate_input_files(self, model_path: str, reference_path: str) -> bool:
"""
입력 파일의 유효성을 검증합니다.
Args:
model_path (str): 3D 모델 파일 경로
reference_path (str): 참조 이미지 파일 경로
Returns:
bool: 파일 유효성
"""
validation_config = self.config['validation']
if not validation_config['validate_input_files']:
return True
# 파일 존재 확인
if not os.path.exists(model_path):
self.logger.error(f"3D 모델 파일이 존재하지 않습니다: {model_path}")
return False
if not os.path.exists(reference_path):
self.logger.error(f"참조 이미지 파일이 존재하지 않습니다: {reference_path}")
return False
# 파일 형식 확인
if validation_config['check_file_formats']:
if not self.data_loader.validate_file(model_path, '3d'):
self.logger.error(f"지원하지 않는 3D 파일 형식: {model_path}")
return False
if not self.data_loader.validate_file(reference_path, 'image'):
self.logger.error(f"지원하지 않는 이미지 파일 형식: {reference_path}")
return False
# 파일 크기 확인
min_size = validation_config['min_file_size_kb'] * 1024
max_size = validation_config['max_file_size_mb'] * 1024 * 1024
for file_path in [model_path, reference_path]:
file_size = os.path.getsize(file_path)
if file_size < min_size:
self.logger.error(f"파일 크기가 너무 작습니다: {file_path}")
return False
if file_size > max_size:
self.logger.error(f"파일 크기가 너무 큽니다: {file_path}")
return False
return True
def _setup_logging(self):
"""로깅을 설정합니다."""
logging_config = self.config['logging']
# 로그 디렉토리 생성
log_file = logging_config['file']
log_dir = os.path.dirname(log_file)
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir)
# 로깅 설정
logging.basicConfig(
level=getattr(logging, logging_config['level']),
format=logging_config['format'],
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
def _create_output_directories(self):
"""출력 디렉토리들을 생성합니다."""
file_paths = self.config['file_paths']
for dir_path in file_paths.values():
if not os.path.exists(dir_path):
os.makedirs(dir_path)
def _save_results(self, results: Dict):
"""평가 결과를 저장합니다."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# JSON 결과 저장
json_file = os.path.join(
self.config['file_paths']['reports_dir'],
f"evaluation_results_{timestamp}.json"
)
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
self.logger.info(f"결과가 저장되었습니다: {json_file}")
def _generate_html_report(self, results: Dict) -> str:
"""HTML 형식의 리포트를 생성합니다."""
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>3D 객체인식 평가 결과</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background-color: #f0f0f0; padding: 20px; border-radius: 5px; }}
.score {{ font-size: 24px; font-weight: bold; color: #2e8b57; }}
.grade {{ font-size: 20px; color: #4169e1; }}
.metrics {{ margin: 20px 0; }}
.metric {{ margin: 10px 0; padding: 10px; background-color: #f9f9f9; border-radius: 3px; }}
.metric-name {{ font-weight: bold; }}
.metric-value {{ color: #666; }}
</style>
</head>
<body>
<div class="header">
<h1>3D 객체인식 평가 결과</h1>
<p>평가 시간: {results['evaluation_timestamp']}</p>
<p>모델 파일: {results['model_path']}</p>
<p>참조 파일: {results['reference_path']}</p>
</div>
<div class="score">
종합 점수: {results['comprehensive_score']:.2f}
</div>
<div class="grade">
성능 등급: {results['grade']}등급
</div>
<div class="metrics">
<h2>개별 지표 점수</h2>
{self._generate_metrics_html(results['score_details'])}
</div>
<div class="metrics">
<h2>원본 지표 값</h2>
{self._generate_raw_metrics_html(results['metrics'])}
</div>
</body>
</html>
"""
return html_content
def _generate_metrics_html(self, score_details: Dict) -> str:
"""지표 점수 HTML을 생성합니다."""
html = ""
for metric_name, details in score_details.items():
html += f"""
<div class="metric">
<div class="metric-name">{metric_name}: {details['normalized_score']:.1f}점</div>
<div class="metric-value">가중치: {details['weight']}, 원본값: {details['raw_value']:.4f}</div>
</div>
"""
return html
def _generate_raw_metrics_html(self, metrics: Dict) -> str:
"""원본 지표 값 HTML을 생성합니다."""
html = ""
for metric_name, value in metrics.items():
html += f"""
<div class="metric">
<div class="metric-name">{metric_name}: {value:.4f}</div>
</div>
"""
return html
def _generate_json_report(self, results: Dict) -> str:
"""JSON 형식의 리포트를 생성합니다."""
return json.dumps(results, indent=2, ensure_ascii=False)
def _generate_text_report(self, results: Dict) -> str:
"""텍스트 형식의 리포트를 생성합니다."""
report = f"""
=== TRELLIS 모델 평가 결과 ===
종합 점수: {results['comprehensive_score']:.1f}
성능 등급: {results['grade']}등급
=== 개별 지표 점수 ===
"""
for metric_name, details in results['score_details'].items():
report += f"{metric_name}: {details['normalized_score']:.1f}\n"
report += "\n=== 원본 지표 값 ===\n"
for metric_name, value in results['metrics'].items():
report += f"{metric_name}: {value:.4f}\n"
return report
def _get_memory_profiling_summary(self) -> Dict:
"""메모리 프로파일링 요약을 반환합니다."""
summary = {}
# 각 단계별 메모리 사용량 차이 계산
stages = [
("evaluation_start", "data_loading_end", "data_loading"),
("data_loading_end", "rendering_end", "rendering"),
("rendering_end", "metrics_calculation_end", "metrics_calculation")
]
for start_label, end_label, stage_name in stages:
diff = self.memory_profiler.get_memory_diff(start_label, end_label)
if diff:
summary[stage_name] = {
'memory_diff_mb': diff['rss_diff_mb'],
'time_seconds': diff['time_diff_seconds']
}
# 개별 메트릭별 메모리 사용량
metric_memory = {}
for metric in ['2d_map', '3d_map', 'chamfer_distance', 'emd', 'class_accuracy']:
diff = self.memory_profiler.get_memory_diff(f"{metric}_start", f"{metric}_end")
if diff:
metric_memory[metric] = {
'memory_diff_mb': diff['rss_diff_mb'],
'time_seconds': diff['time_diff_seconds']
}
summary['metrics'] = metric_memory
return summary
"""
정확도 계산기
기하학적 정확성을 평가하여 정확도 점수를 계산합니다.
"""
import numpy as np
import logging
from typing import Dict, List, Tuple, Optional
from ..utils.exception_handler import EvaluationExceptionHandler
logger = logging.getLogger(__name__)
class AccuracyCalculator:
"""정확도 점수 계산기"""
def __init__(self, config: Dict):
"""
정확도 계산기 초기화
Args:
config (Dict): 정확도 계산 설정
"""
self.config = config
self.geometric_accuracy_weight = config.get('geometric_accuracy_weight', 0.5)
self.shape_similarity_weight = config.get('shape_similarity_weight', 0.3)
self.proportional_accuracy_weight = config.get('proportional_accuracy_weight', 0.2)
self.tolerance_threshold = config.get('tolerance_threshold', 0.1)
logger.info("정확도 계산기가 초기화되었습니다.")
@EvaluationExceptionHandler.handle_metric_calculation_error("Accuracy", {'score': 0.0})
def calculate_accuracy(self, model_3d: Dict, ground_truth_3d: Dict) -> Dict:
"""
정확도 점수 계산
Args:
model_3d (Dict): 3D 모델 데이터
ground_truth_3d (Dict): Ground Truth 데이터
Returns:
Dict: 정확도 점수 및 상세 정보
"""
try:
logger.info("정확도 점수 계산 시작...")
# 기하학적 정확도 점수 계산
geometric_accuracy_score = self._calculate_geometric_accuracy(model_3d, ground_truth_3d)
# 형태 유사도 점수 계산
shape_similarity_score = self._calculate_shape_similarity(model_3d, ground_truth_3d)
# 비례 정확도 점수 계산
proportional_accuracy_score = self._calculate_proportional_accuracy(model_3d, ground_truth_3d)
# 종합 정확도 점수 계산
accuracy_score = (
geometric_accuracy_score * self.geometric_accuracy_weight +
shape_similarity_score * self.shape_similarity_weight +
proportional_accuracy_score * self.proportional_accuracy_weight
)
# 결과 정리
result = {
'score': accuracy_score,
'details': {
'geometric_accuracy': geometric_accuracy_score,
'shape_similarity': shape_similarity_score,
'proportional_accuracy': proportional_accuracy_score,
'weights': {
'geometric_accuracy': self.geometric_accuracy_weight,
'shape_similarity': self.shape_similarity_weight,
'proportional_accuracy': self.proportional_accuracy_weight
}
}
}
logger.info(f"정확도 점수 계산 완료: {accuracy_score:.2f}")
return result
except Exception as e:
logger.error(f"정확도 점수 계산 중 오류: {str(e)}")
return {'score': 0.0, 'details': {'error': str(e)}}
def _calculate_geometric_accuracy(self, model_3d: Dict, ground_truth_3d: Dict) -> float:
"""기하학적 정확도 점수 계산"""
try:
model_vertices = model_3d.get('vertices', [])
gt_vertices = ground_truth_3d.get('vertices', [])
if not model_vertices or not gt_vertices:
return 0.0
# 정점 수가 다를 경우 샘플링
if len(model_vertices) != len(gt_vertices):
model_vertices, gt_vertices = self._align_vertices(model_vertices, gt_vertices)
if not model_vertices or not gt_vertices:
return 0.0
# 거리 기반 정확도 계산
distances = []
for model_vertex, gt_vertex in zip(model_vertices, gt_vertices):
distance = np.linalg.norm(np.array(model_vertex) - np.array(gt_vertex))
distances.append(distance)
# 평균 거리 기반 정확도 점수
mean_distance = np.mean(distances)
accuracy_score = max(0.0, 1.0 - mean_distance / self.tolerance_threshold)
return min(1.0, accuracy_score)
except Exception as e:
logger.error(f"기하학적 정확도 계산 중 오류: {str(e)}")
return 0.0
def _calculate_shape_similarity(self, model_3d: Dict, ground_truth_3d: Dict) -> float:
"""형태 유사도 점수 계산"""
try:
model_vertices = model_3d.get('vertices', [])
gt_vertices = ground_truth_3d.get('vertices', [])
if not model_vertices or not gt_vertices:
return 0.0
# 형태 특성 추출
model_features = self._extract_shape_features(model_vertices)
gt_features = self._extract_shape_features(gt_vertices)
if not model_features or not gt_features:
return 0.0
# 특성 유사도 계산
similarity_score = self._calculate_feature_similarity(model_features, gt_features)
return min(1.0, max(0.0, similarity_score))
except Exception as e:
logger.error(f"형태 유사도 계산 중 오류: {str(e)}")
return 0.0
def _calculate_proportional_accuracy(self, model_3d: Dict, ground_truth_3d: Dict) -> float:
"""비례 정확도 점수 계산"""
try:
model_vertices = model_3d.get('vertices', [])
gt_vertices = ground_truth_3d.get('vertices', [])
if not model_vertices or not gt_vertices:
return 0.0
# 모델의 비례 특성 계산
model_proportions = self._calculate_proportions(model_vertices)
gt_proportions = self._calculate_proportions(gt_vertices)
if not model_proportions or not gt_proportions:
return 0.0
# 비례 정확도 계산
proportion_accuracy = self._calculate_proportion_accuracy(model_proportions, gt_proportions)
return min(1.0, max(0.0, proportion_accuracy))
except Exception as e:
logger.error(f"비례 정확도 계산 중 오류: {str(e)}")
return 0.0
def _align_vertices(self, model_vertices: List, gt_vertices: List) -> Tuple[List, List]:
"""정점 수를 맞추기 위한 정렬"""
try:
if len(model_vertices) == len(gt_vertices):
return model_vertices, gt_vertices
# 더 적은 수의 정점을 기준으로 샘플링
min_count = min(len(model_vertices), len(gt_vertices))
if len(model_vertices) > min_count:
# 모델 정점 샘플링
indices = np.random.choice(len(model_vertices), min_count, replace=False)
model_vertices = [model_vertices[i] for i in indices]
if len(gt_vertices) > min_count:
# GT 정점 샘플링
indices = np.random.choice(len(gt_vertices), min_count, replace=False)
gt_vertices = [gt_vertices[i] for i in indices]
return model_vertices, gt_vertices
except Exception as e:
logger.error(f"정점 정렬 중 오류: {str(e)}")
return [], []
def _extract_shape_features(self, vertices: List) -> Dict:
"""형태 특성 추출"""
try:
if not vertices:
return {}
vertices_array = np.array(vertices)
# 중심점 계산
center = np.mean(vertices_array, axis=0)
# 중심에서의 거리 분포
distances = np.linalg.norm(vertices_array - center, axis=1)
# 형태 특성
features = {
'center': center.tolist(),
'mean_distance': np.mean(distances),
'std_distance': np.std(distances),
'max_distance': np.max(distances),
'min_distance': np.min(distances),
'volume_estimate': self._estimate_volume(vertices_array),
'surface_area_estimate': self._estimate_surface_area(vertices_array)
}
return features
except Exception as e:
logger.error(f"형태 특성 추출 중 오류: {str(e)}")
return {}
def _calculate_feature_similarity(self, model_features: Dict, gt_features: Dict) -> float:
"""특성 유사도 계산"""
try:
if not model_features or not gt_features:
return 0.0
similarities = []
# 거리 기반 특성 유사도
distance_features = ['mean_distance', 'std_distance', 'max_distance', 'min_distance']
for feature in distance_features:
if feature in model_features and feature in gt_features:
model_val = model_features[feature]
gt_val = gt_features[feature]
if gt_val > 0:
similarity = 1.0 - abs(model_val - gt_val) / gt_val
similarities.append(max(0.0, similarity))
# 부피 및 표면적 유사도
volume_features = ['volume_estimate', 'surface_area_estimate']
for feature in volume_features:
if feature in model_features and feature in gt_features:
model_val = model_features[feature]
gt_val = gt_features[feature]
if gt_val > 0:
similarity = 1.0 - abs(model_val - gt_val) / gt_val
similarities.append(max(0.0, similarity))
if not similarities:
return 0.0
return np.mean(similarities)
except Exception as e:
logger.error(f"특성 유사도 계산 중 오류: {str(e)}")
return 0.0
def _calculate_proportions(self, vertices: List) -> Dict:
"""비례 특성 계산"""
try:
if not vertices:
return {}
vertices_array = np.array(vertices)
# 각 축별 범위 계산
x_range = np.max(vertices_array[:, 0]) - np.min(vertices_array[:, 0])
y_range = np.max(vertices_array[:, 1]) - np.min(vertices_array[:, 1])
z_range = np.max(vertices_array[:, 2]) - np.min(vertices_array[:, 2])
# 비례 계산
proportions = {
'x_y_ratio': x_range / y_range if y_range > 0 else 1.0,
'x_z_ratio': x_range / z_range if z_range > 0 else 1.0,
'y_z_ratio': y_range / z_range if z_range > 0 else 1.0,
'aspect_ratios': [x_range, y_range, z_range]
}
return proportions
except Exception as e:
logger.error(f"비례 특성 계산 중 오류: {str(e)}")
return {}
def _calculate_proportion_accuracy(self, model_proportions: Dict, gt_proportions: Dict) -> float:
"""비례 정확도 계산"""
try:
if not model_proportions or not gt_proportions:
return 0.0
accuracies = []
# 비율 정확도 계산
ratio_features = ['x_y_ratio', 'x_z_ratio', 'y_z_ratio']
for feature in ratio_features:
if feature in model_proportions and feature in gt_proportions:
model_val = model_proportions[feature]
gt_val = gt_proportions[feature]
if gt_val > 0:
accuracy = 1.0 - abs(model_val - gt_val) / gt_val
accuracies.append(max(0.0, accuracy))
# 종횡비 정확도 계산
if 'aspect_ratios' in model_proportions and 'aspect_ratios' in gt_proportions:
model_ratios = model_proportions['aspect_ratios']
gt_ratios = gt_proportions['aspect_ratios']
if len(model_ratios) == len(gt_ratios):
for model_ratio, gt_ratio in zip(model_ratios, gt_ratios):
if gt_ratio > 0:
accuracy = 1.0 - abs(model_ratio - gt_ratio) / gt_ratio
accuracies.append(max(0.0, accuracy))
if not accuracies:
return 0.0
return np.mean(accuracies)
except Exception as e:
logger.error(f"비례 정확도 계산 중 오류: {str(e)}")
return 0.0
def _estimate_volume(self, vertices: np.ndarray) -> float:
"""부피 추정"""
try:
if len(vertices) < 4:
return 0.0
# Convex Hull을 사용한 부피 추정
from scipy.spatial import ConvexHull
hull = ConvexHull(vertices)
return hull.volume
except Exception as e:
logger.error(f"부피 추정 중 오류: {str(e)}")
# 간단한 바운딩 박스 부피로 대체
try:
min_coords = np.min(vertices, axis=0)
max_coords = np.max(vertices, axis=0)
return np.prod(max_coords - min_coords)
except:
return 0.0
def _estimate_surface_area(self, vertices: np.ndarray) -> float:
"""표면적 추정"""
try:
if len(vertices) < 3:
return 0.0
# 간단한 표면적 추정 (정점 분포 기반)
min_coords = np.min(vertices, axis=0)
max_coords = np.max(vertices, axis=0)
dimensions = max_coords - min_coords
# 바운딩 박스 표면적
surface_area = 2 * (dimensions[0] * dimensions[1] +
dimensions[1] * dimensions[2] +
dimensions[0] * dimensions[2])
return surface_area
except Exception as e:
logger.error(f"표면적 추정 중 오류: {str(e)}")
return 0.0
def get_details(self) -> Dict:
"""정확도 계산기 상세 정보 반환"""
return {
'name': 'AccuracyCalculator',
'config': self.config,
'weights': {
'geometric_accuracy': self.geometric_accuracy_weight,
'shape_similarity': self.shape_similarity_weight,
'proportional_accuracy': self.proportional_accuracy_weight
},
'parameters': {
'tolerance_threshold': self.tolerance_threshold
}
}
"""
Chamfer Distance 계산 모듈
3D 모델 간의 기하학적 유사성을 평가합니다.
"""
import numpy as np
import trimesh
from typing import Dict, Tuple, Optional
from scipy.spatial.distance import cdist
import open3d as o3d
class ChamferDistanceCalculator:
"""Chamfer Distance 계산을 담당하는 클래스"""
def __init__(self, num_points: int = 10000):
"""
Chamfer Distance 계산기 초기화
Args:
num_points (int): 점군 생성 시 사용할 점의 개수
"""
self.num_points = num_points
def calculate_chamfer_distance(self, model_3d: Dict, reference_3d: Dict) -> float:
"""
두 3D 모델 간의 Chamfer Distance를 계산합니다 (개선된 버전).
Args:
model_3d (Dict): 변환된 3D 모델 정보
reference_3d (Dict): 참조 3D 모델 정보
Returns:
float: Chamfer Distance 값 (낮을수록 유사함)
"""
return self.chamfer_distance_optimized_improved(model_3d, reference_3d)
def chamfer_distance_optimized_improved(self, model_3d: Dict, reference_3d: Dict) -> float:
"""
개선된 최적화 Chamfer Distance 계산.
Args:
model_3d (Dict): 변환된 3D 모델 정보
reference_3d (Dict): 참조 3D 모델 정보
Returns:
float: Chamfer Distance 값
"""
try:
# 고품질 점군 생성
pc1 = self._mesh_to_pointcloud_high_quality(model_3d, self.num_points)
pc2 = self._mesh_to_pointcloud_high_quality(reference_3d, self.num_points)
if len(pc1) == 0 or len(pc2) == 0:
return float('inf')
# 점군 정렬
pc1_aligned, pc2_aligned = self._align_pointclouds(pc1, pc2)
# 최적화된 Chamfer Distance 계산
chamfer_dist = self._compute_chamfer_distance_optimized(pc1_aligned, pc2_aligned)
return chamfer_dist
except Exception as e:
print(f"Chamfer Distance 계산 중 오류: {e}")
# 폴백: 기본 방법 사용
return self._fallback_chamfer_distance(model_3d, reference_3d)
def mesh_to_pointcloud(self, mesh: Dict, num_points: int = None) -> np.ndarray:
"""
메시를 점군으로 변환합니다.
Args:
mesh (Dict): 3D 메시 정보
num_points (int): 생성할 점의 개수
Returns:
np.ndarray: 점군 데이터 (N, 3)
"""
if num_points is None:
num_points = self.num_points
# vertices 키 존재 여부 확인
if 'vertices' not in mesh:
return np.array([])
vertices = mesh['vertices']
faces = mesh.get('faces', [])
if len(vertices) == 0:
return np.array([])
# Open3D를 사용한 고품질 점군 생성
try:
return self._mesh_to_pointcloud_open3d(vertices, faces, num_points)
except Exception:
# Open3D 실패 시 간단한 방법 사용
return self._mesh_to_pointcloud_simple(vertices, faces, num_points)
def _mesh_to_pointcloud_open3d(self, vertices: np.ndarray, faces: np.ndarray,
num_points: int) -> np.ndarray:
"""
Open3D를 사용하여 메시를 점군으로 변환합니다.
Args:
vertices (np.ndarray): 메시 정점
faces (np.ndarray): 메시 면
num_points (int): 생성할 점의 개수
Returns:
np.ndarray: 점군 데이터
"""
# Open3D 메시 생성
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(vertices)
mesh.triangles = o3d.utility.Vector3iVector(faces)
# 법선 계산
mesh.compute_vertex_normals()
# 표면에서 균등하게 점 샘플링
pointcloud = mesh.sample_points_uniformly(number_of_points=num_points)
return np.asarray(pointcloud.points)
def _mesh_to_pointcloud_simple(self, vertices: np.ndarray, faces: np.ndarray,
num_points: int) -> np.ndarray:
"""
간단한 방법으로 메시를 점군으로 변환합니다.
Args:
vertices (np.ndarray): 메시 정점
faces (np.ndarray): 메시 면
num_points (int): 생성할 점의 개수
Returns:
np.ndarray: 점군 데이터
"""
if len(vertices) >= num_points:
# 정점이 충분한 경우 랜덤 샘플링
indices = np.random.choice(len(vertices), num_points, replace=False)
return vertices[indices]
else:
# 정점이 부족한 경우 면 기반 샘플링
return self._sample_points_from_faces(vertices, faces, num_points)
def _sample_points_from_faces(self, vertices: np.ndarray, faces: np.ndarray,
num_points: int) -> np.ndarray:
"""
면에서 점을 샘플링합니다.
Args:
vertices (np.ndarray): 메시 정점
faces (np.ndarray): 메시 면
num_points (int): 생성할 점의 개수
Returns:
np.ndarray: 샘플링된 점군
"""
if len(faces) == 0:
# 면이 없는 경우 정점에서 중복 허용 샘플링
indices = np.random.choice(len(vertices), num_points, replace=True)
return vertices[indices]
# 각 면의 면적 계산
face_areas = []
for face in faces:
v0, v1, v2 = vertices[face]
area = 0.5 * np.linalg.norm(np.cross(v1 - v0, v2 - v0))
face_areas.append(area)
face_areas = np.array(face_areas)
total_area = np.sum(face_areas)
if total_area == 0:
# 면적이 0인 경우 정점에서 샘플링
indices = np.random.choice(len(vertices), num_points, replace=True)
return vertices[indices]
# 면적에 비례하여 점 샘플링
face_probs = face_areas / total_area
sampled_faces = np.random.choice(len(faces), num_points, p=face_probs)
points = []
for face_idx in sampled_faces:
face = faces[face_idx]
v0, v1, v2 = vertices[face]
# 삼각형 내에서 균등하게 점 샘플링
r1, r2 = np.random.random(2)
if r1 + r2 > 1:
r1, r2 = 1 - r1, 1 - r2
point = v0 + r1 * (v1 - v0) + r2 * (v2 - v0)
points.append(point)
return np.array(points)
def chamfer_distance_naive(self, pc1: np.ndarray, pc2: np.ndarray) -> float:
"""
두 점군 간의 Chamfer Distance를 계산합니다.
Args:
pc1 (np.ndarray): 첫 번째 점군 (N1, 3)
pc2 (np.ndarray): 두 번째 점군 (N2, 3)
Returns:
float: Chamfer Distance
"""
if len(pc1) == 0 or len(pc2) == 0:
return float('inf')
# pc1의 각 점에서 pc2까지의 최단 거리
dist1 = self._compute_point_to_set_distance(pc1, pc2)
# pc2의 각 점에서 pc1까지의 최단 거리
dist2 = self._compute_point_to_set_distance(pc2, pc1)
# Chamfer Distance = 평균 최단 거리
chamfer_dist = (np.mean(dist1) + np.mean(dist2)) / 2.0
return chamfer_dist
def _compute_point_to_set_distance(self, points: np.ndarray, reference_set: np.ndarray) -> np.ndarray:
"""
점들에서 참조 집합까지의 최단 거리를 계산합니다.
Args:
points (np.ndarray): 점들 (N, 3)
reference_set (np.ndarray): 참조 점군 (M, 3)
Returns:
np.ndarray: 각 점의 최단 거리 (N,)
"""
# 모든 점 쌍 간의 거리 계산
distances = cdist(points, reference_set, metric='euclidean')
# 각 점에서 참조 집합까지의 최단 거리
min_distances = np.min(distances, axis=1)
return min_distances
def chamfer_distance_optimized(self, pc1: np.ndarray, pc2: np.ndarray) -> float:
"""
최적화된 Chamfer Distance 계산 (KD-Tree 사용).
Args:
pc1 (np.ndarray): 첫 번째 점군
pc2 (np.ndarray): 두 번째 점군
Returns:
float: Chamfer Distance
"""
try:
from scipy.spatial import cKDTree
if len(pc1) == 0 or len(pc2) == 0:
return float('inf')
# KD-Tree 구축
tree1 = cKDTree(pc1)
tree2 = cKDTree(pc2)
# pc1의 각 점에서 pc2까지의 최단 거리
dist1, _ = tree2.query(pc1)
# pc2의 각 점에서 pc1까지의 최단 거리
dist2, _ = tree1.query(pc2)
# Chamfer Distance
chamfer_dist = (np.mean(dist1) + np.mean(dist2)) / 2.0
return chamfer_dist
except ImportError:
# scipy가 없는 경우 naive 방법 사용
return self.chamfer_distance_naive(pc1, pc2)
def calculate_symmetric_chamfer_distance(self, pc1: np.ndarray, pc2: np.ndarray) -> float:
"""
대칭 Chamfer Distance를 계산합니다.
Args:
pc1 (np.ndarray): 첫 번째 점군
pc2 (np.ndarray): 두 번째 점군
Returns:
float: 대칭 Chamfer Distance
"""
# 양방향 Chamfer Distance 계산
cd1 = self._compute_point_to_set_distance(pc1, pc2)
cd2 = self._compute_point_to_set_distance(pc2, pc1)
# 대칭 Chamfer Distance
symmetric_cd = np.mean(cd1) + np.mean(cd2)
return symmetric_cd
def calculate_hausdorff_distance(self, pc1: np.ndarray, pc2: np.ndarray) -> float:
"""
Hausdorff Distance를 계산합니다.
Args:
pc1 (np.ndarray): 첫 번째 점군
pc2 (np.ndarray): 두 번째 점군
Returns:
float: Hausdorff Distance
"""
if len(pc1) == 0 or len(pc2) == 0:
return float('inf')
# 모든 점 쌍 간의 거리 계산
distances = cdist(pc1, pc2, metric='euclidean')
# 각 점에서 다른 점군까지의 최단 거리
min_dist1 = np.min(distances, axis=1) # pc1의 각 점에서 pc2까지
min_dist2 = np.min(distances, axis=0) # pc2의 각 점에서 pc1까지
# Hausdorff Distance = 최대 최단 거리
hausdorff_dist = max(np.max(min_dist1), np.max(min_dist2))
return hausdorff_dist
def calculate_point_cloud_density(self, pointcloud: np.ndarray) -> float:
"""
점군의 밀도를 계산합니다.
Args:
pointcloud (np.ndarray): 점군 데이터
Returns:
float: 점군 밀도
"""
if len(pointcloud) < 2:
return 0.0
# 점군의 바운딩 박스 부피 계산
min_coords = np.min(pointcloud, axis=0)
max_coords = np.max(pointcloud, axis=0)
volume = np.prod(max_coords - min_coords)
if volume == 0:
return 0.0
# 밀도 = 점의 개수 / 부피
density = len(pointcloud) / volume
return density
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
def calculate_chamfer_distance_normalized(self, model_3d: Dict, reference_3d: Dict) -> float:
"""
정규화된 Chamfer Distance를 계산합니다.
Args:
model_3d (Dict): 변환된 3D 모델 정보
reference_3d (Dict): 참조 3D 모델 정보
Returns:
float: 정규화된 Chamfer Distance
"""
# 메시를 점군으로 변환
pc1 = self.mesh_to_pointcloud(model_3d, self.num_points)
pc2 = self.mesh_to_pointcloud(reference_3d, self.num_points)
if len(pc1) == 0 or len(pc2) == 0:
return float('inf')
# 점군 정규화
pc1_norm = self.normalize_pointcloud(pc1)
pc2_norm = self.normalize_pointcloud(pc2)
# 정규화된 Chamfer Distance 계산
chamfer_dist = self.chamfer_distance_naive(pc1_norm, pc2_norm)
return chamfer_dist
def _mesh_to_pointcloud_high_quality(self, mesh: Dict, num_points: int) -> np.ndarray:
"""
고품질 점군 생성 (Open3D 메시 전처리, 중복 제거, 법선 계산).
Args:
mesh (Dict): 3D 메시 정보
num_points (int): 생성할 점의 개수
Returns:
np.ndarray: 고품질 점군 데이터
"""
try:
# vertices 키 존재 여부 확인
if 'vertices' not in mesh:
return np.array([])
vertices = mesh['vertices']
faces = mesh.get('faces', [])
if len(vertices) == 0:
return np.array([])
# Open3D 메시 생성
o3d_mesh = o3d.geometry.TriangleMesh()
o3d_mesh.vertices = o3d.utility.Vector3dVector(vertices)
o3d_mesh.triangles = o3d.utility.Vector3iVector(faces)
# 메시 전처리
o3d_mesh = self._preprocess_mesh_open3d(o3d_mesh)
# 고품질 점 샘플링
pointcloud = o3d_mesh.sample_points_uniformly(number_of_points=num_points)
# 점군 후처리
processed_pc = self._postprocess_pointcloud(np.asarray(pointcloud.points))
return processed_pc
except Exception as e:
print(f"고품질 점군 생성 실패: {e}")
# 폴백: 기본 방법 사용
return self.mesh_to_pointcloud(mesh, num_points)
def _preprocess_mesh_open3d(self, mesh: o3d.geometry.TriangleMesh) -> o3d.geometry.TriangleMesh:
"""Open3D 메시 전처리 (중복 제거, 법선 계산)."""
# 중복 정점 제거
mesh.remove_duplicated_vertices()
# 중복 면 제거
mesh.remove_duplicated_triangles()
# 비정상적인 면 제거
mesh.remove_degenerate_triangles()
# 비연결된 정점 제거
mesh.remove_unreferenced_vertices()
# 법선 계산
mesh.compute_vertex_normals()
mesh.compute_triangle_normals()
# 메시 정리
mesh.remove_degenerate_triangles()
mesh.remove_duplicated_triangles()
mesh.remove_duplicated_vertices()
mesh.remove_unreferenced_vertices()
return mesh
def _postprocess_pointcloud(self, pointcloud: np.ndarray) -> np.ndarray:
"""점군 후처리 (노이즈 제거, 정규화)."""
if len(pointcloud) == 0:
return pointcloud
# 통계적 이상치 제거
pointcloud = self._remove_outliers(pointcloud)
# 점군 정규화
pointcloud = self._normalize_pointcloud_improved(pointcloud)
return pointcloud
def _remove_outliers(self, pointcloud: np.ndarray, std_ratio: float = 2.0) -> np.ndarray:
"""통계적 이상치 제거."""
if len(pointcloud) < 10:
return pointcloud
# 각 점에서 다른 모든 점까지의 평균 거리 계산
distances = cdist(pointcloud, pointcloud, metric='euclidean')
mean_distances = np.mean(distances, axis=1)
# 평균과 표준편차 계산
mean_dist = np.mean(mean_distances)
std_dist = np.std(mean_distances)
# 이상치 임계값
threshold = mean_dist + std_ratio * std_dist
# 이상치가 아닌 점들만 유지
valid_indices = mean_distances < threshold
return pointcloud[valid_indices]
def _normalize_pointcloud_improved(self, pointcloud: np.ndarray) -> np.ndarray:
"""개선된 점군 정규화."""
if len(pointcloud) == 0:
return pointcloud
# 중심점 계산
center = np.mean(pointcloud, axis=0)
centered = pointcloud - center
# 스케일 정규화 (RMS 거리 사용)
rms_distance = np.sqrt(np.mean(np.sum(centered**2, axis=1)))
if rms_distance > 0:
normalized = centered / rms_distance
else:
normalized = centered
return normalized
def _align_pointclouds(self, pc1: np.ndarray, pc2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""
점군 정렬 (중심점 정렬, 스케일 정규화, ICP 정렬).
Args:
pc1 (np.ndarray): 첫 번째 점군
pc2 (np.ndarray): 두 번째 점군
Returns:
Tuple[np.ndarray, np.ndarray]: 정렬된 점군들
"""
# 1. 중심점 정렬
pc1_centered, pc2_centered = self._center_align_pointclouds(pc1, pc2)
# 2. 스케일 정규화
pc1_scaled, pc2_scaled = self._scale_normalize_pointclouds(pc1_centered, pc2_centered)
# 3. ICP 정렬 (선택적)
try:
pc1_aligned, pc2_aligned = self._icp_align_pointclouds(pc1_scaled, pc2_scaled)
return pc1_aligned, pc2_aligned
except Exception:
# ICP 실패 시 스케일 정규화된 점군 반환
return pc1_scaled, pc2_scaled
def _center_align_pointclouds(self, pc1: np.ndarray, pc2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""점군들을 중심점으로 정렬."""
center1 = np.mean(pc1, axis=0)
center2 = np.mean(pc2, axis=0)
pc1_centered = pc1 - center1
pc2_centered = pc2 - center2
return pc1_centered, pc2_centered
def _scale_normalize_pointclouds(self, pc1: np.ndarray, pc2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""점군들을 스케일 정규화."""
# RMS 거리 계산
rms1 = np.sqrt(np.mean(np.sum(pc1**2, axis=1)))
rms2 = np.sqrt(np.mean(np.sum(pc2**2, axis=1)))
# 정규화
if rms1 > 0:
pc1_scaled = pc1 / rms1
else:
pc1_scaled = pc1
if rms2 > 0:
pc2_scaled = pc2 / rms2
else:
pc2_scaled = pc2
return pc1_scaled, pc2_scaled
def _icp_align_pointclouds(self, pc1: np.ndarray, pc2: np.ndarray,
max_iterations: int = 50) -> Tuple[np.ndarray, np.ndarray]:
"""ICP를 사용한 점군 정렬."""
try:
# Open3D 점군 생성
pcd1 = o3d.geometry.PointCloud()
pcd1.points = o3d.utility.Vector3dVector(pc1)
pcd2 = o3d.geometry.PointCloud()
pcd2.points = o3d.utility.Vector3dVector(pc2)
# ICP 실행
result = o3d.pipelines.registration.registration_icp(
pcd1, pcd2,
max_correspondence_distance=0.1,
estimation_method=o3d.pipelines.registration.TransformationEstimationPointToPoint(),
criteria=o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=max_iterations)
)
# 변환 적용
pcd1_transformed = pcd1.transform(result.transformation)
return np.asarray(pcd1_transformed.points), pc2
except Exception as e:
print(f"ICP 정렬 실패: {e}")
return pc1, pc2
def _compute_chamfer_distance_optimized(self, pc1: np.ndarray, pc2: np.ndarray) -> float:
"""
최적화된 Chamfer Distance 계산 (KD-Tree 기반, 배치 처리).
Args:
pc1 (np.ndarray): 첫 번째 점군
pc2 (np.ndarray): 두 번째 점군
Returns:
float: Chamfer Distance
"""
try:
from scipy.spatial import cKDTree
if len(pc1) == 0 or len(pc2) == 0:
return float('inf')
# 배치 크기 설정 (메모리 효율성)
batch_size = min(1000, len(pc1), len(pc2))
# KD-Tree 구축
tree1 = cKDTree(pc1)
tree2 = cKDTree(pc2)
# 배치 처리로 거리 계산
total_dist1 = 0.0
total_dist2 = 0.0
# pc1의 각 점에서 pc2까지의 최단 거리
for i in range(0, len(pc1), batch_size):
batch_pc1 = pc1[i:i+batch_size]
dist1, _ = tree2.query(batch_pc1)
total_dist1 += np.sum(dist1)
# pc2의 각 점에서 pc1까지의 최단 거리
for i in range(0, len(pc2), batch_size):
batch_pc2 = pc2[i:i+batch_size]
dist2, _ = tree1.query(batch_pc2)
total_dist2 += np.sum(dist2)
# Chamfer Distance 계산
chamfer_dist = (total_dist1 / len(pc1) + total_dist2 / len(pc2)) / 2.0
return chamfer_dist
except ImportError:
# scipy가 없는 경우 기본 방법 사용
return self.chamfer_distance_naive(pc1, pc2)
except Exception as e:
print(f"최적화된 Chamfer Distance 계산 실패: {e}")
return self.chamfer_distance_naive(pc1, pc2)
def _fallback_chamfer_distance(self, model_3d: Dict, reference_3d: Dict) -> float:
"""폴백 Chamfer Distance 계산."""
try:
# 기본 점군 생성
pc1 = self.mesh_to_pointcloud(model_3d, min(self.num_points, 5000))
pc2 = self.mesh_to_pointcloud(reference_3d, min(self.num_points, 5000))
if len(pc1) == 0 or len(pc2) == 0:
return float('inf')
# 기본 정규화
pc1_norm = self.normalize_pointcloud(pc1)
pc2_norm = self.normalize_pointcloud(pc2)
# 기본 Chamfer Distance 계산
return self.chamfer_distance_naive(pc1_norm, pc2_norm)
except Exception as e:
print(f"폴백 Chamfer Distance 계산 실패: {e}")
return float('inf')
"""
클래스별 정확도 계산 모듈
3D 모델의 객체 클래스 분류 정확도를 평가합니다.
"""
import numpy as np
import trimesh
from typing import Dict, List, Tuple, Optional
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
from sklearn.cluster import KMeans
import cv2
class ClassAccuracyCalculator:
"""클래스별 정확도 계산을 담당하는 클래스"""
def __init__(self, num_classes: int = 10):
"""
클래스 정확도 계산기 초기화
Args:
num_classes (int): 예상 클래스 개수
"""
self.num_classes = num_classes
self.class_names = [
'chair', 'table', 'sofa', 'bed', 'desk',
'bookshelf', 'lamp', 'cabinet', 'door', 'window'
]
def calculate_class_accuracy(self, model_3d: Dict, ground_truth_labels: Dict) -> float:
"""
클래스별 정확도를 계산합니다.
Args:
model_3d (Dict): 변환된 3D 모델 정보
ground_truth_labels (Dict): Ground Truth 클래스 레이블
Returns:
float: 클래스 정확도 점수 (0-1)
"""
# 3D 모델에서 객체 클래스 분류
predicted_classes = self.classify_objects(model_3d)
# Ground Truth 클래스 레이블 추출
gt_classes = self._extract_ground_truth_classes(ground_truth_labels)
if not predicted_classes or not gt_classes:
return 0.0
# 클래스별 정확도 계산
accuracy = self._compute_class_wise_accuracy(predicted_classes, gt_classes)
return accuracy
def classify_objects(self, mesh: Dict) -> List[str]:
"""
3D 메시에서 객체 클래스를 분류합니다.
Args:
mesh (Dict): 3D 메시 정보
Returns:
List[str]: 예측된 클래스 레이블 리스트
"""
vertices = mesh['vertices']
faces = mesh['faces']
if len(vertices) == 0:
return []
# 기하학적 특성 추출
geometric_features = self._extract_geometric_features(vertices, faces)
# 클래스 분류
predicted_classes = self._classify_by_geometric_features(geometric_features)
return predicted_classes
def compute_class_wise_metrics(self, predictions: List, ground_truth: List) -> Dict:
"""
클래스별 정밀도, 재현율, F1 점수를 계산합니다.
Args:
predictions (List): 예측 결과 리스트
ground_truth (List): Ground Truth 리스트
Returns:
Dict: 클래스별 메트릭
"""
if not predictions or not ground_truth:
return {}
# 정확도 계산
accuracy = accuracy_score(ground_truth, predictions)
# 정밀도, 재현율, F1 점수 계산
precision, recall, f1, support = precision_recall_fscore_support(
ground_truth, predictions, average='weighted', zero_division=0
)
# 혼동 행렬 계산
cm = confusion_matrix(ground_truth, predictions)
# 클래스별 상세 메트릭
class_metrics = {}
unique_classes = list(set(ground_truth + predictions))
for class_name in unique_classes:
class_precision, class_recall, class_f1, class_support = precision_recall_fscore_support(
ground_truth, predictions, labels=[class_name], average='weighted', zero_division=0
)
class_metrics[class_name] = {
'precision': class_precision,
'recall': class_recall,
'f1': class_f1,
'support': class_support
}
return {
'overall_accuracy': accuracy,
'weighted_precision': precision,
'weighted_recall': recall,
'weighted_f1': f1,
'confusion_matrix': cm,
'class_metrics': class_metrics
}
def _extract_geometric_features(self, vertices: np.ndarray, faces: np.ndarray) -> Dict:
"""
3D 메시에서 기하학적 특성을 추출합니다.
Args:
vertices (np.ndarray): 메시 정점
faces (np.ndarray): 메시 면
Returns:
Dict: 기하학적 특성
"""
features = {}
# 기본 통계
features['num_vertices'] = len(vertices)
features['num_faces'] = len(faces)
# 바운딩 박스 특성
min_coords = np.min(vertices, axis=0)
max_coords = np.max(vertices, axis=0)
bbox_size = max_coords - min_coords
features['bbox_width'] = bbox_size[0]
features['bbox_height'] = bbox_size[1]
features['bbox_depth'] = bbox_size[2]
features['bbox_volume'] = np.prod(bbox_size)
# 종횡비
features['aspect_ratio_xy'] = bbox_size[0] / (bbox_size[1] + 1e-8)
features['aspect_ratio_xz'] = bbox_size[0] / (bbox_size[2] + 1e-8)
features['aspect_ratio_yz'] = bbox_size[1] / (bbox_size[2] + 1e-8)
# 표면적과 부피
try:
trimesh_mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
features['surface_area'] = trimesh_mesh.surface_area
features['volume'] = trimesh_mesh.volume
features['compactness'] = features['volume'] / (features['surface_area'] + 1e-8)
except:
features['surface_area'] = 0.0
features['volume'] = features['bbox_volume']
features['compactness'] = 0.0
# 복잡도 (면 대 정점 비율)
features['complexity'] = len(faces) / (len(vertices) + 1e-8)
# 중심점
features['center'] = np.mean(vertices, axis=0)
# 주성분 분석
centered_vertices = vertices - features['center']
cov_matrix = np.cov(centered_vertices.T)
eigenvalues = np.linalg.eigvals(cov_matrix)
eigenvalues = np.sort(eigenvalues)[::-1]
features['eigenvalue_1'] = eigenvalues[0] if len(eigenvalues) > 0 else 0
features['eigenvalue_2'] = eigenvalues[1] if len(eigenvalues) > 1 else 0
features['eigenvalue_3'] = eigenvalues[2] if len(eigenvalues) > 2 else 0
# 형태 특성
if len(eigenvalues) >= 3:
features['linearity'] = (eigenvalues[0] - eigenvalues[1]) / (eigenvalues[0] + 1e-8)
features['planarity'] = (eigenvalues[1] - eigenvalues[2]) / (eigenvalues[0] + 1e-8)
features['sphericity'] = eigenvalues[2] / (eigenvalues[0] + 1e-8)
else:
features['linearity'] = 0.0
features['planarity'] = 0.0
features['sphericity'] = 0.0
return features
def _classify_by_geometric_features(self, features: Dict) -> List[str]:
"""
기하학적 특성을 기반으로 객체 클래스를 분류합니다.
Args:
features (Dict): 기하학적 특성
Returns:
List[str]: 예측된 클래스 레이블
"""
predicted_classes = []
# 규칙 기반 분류
if self._is_chair_like(features):
predicted_classes.append('chair')
elif self._is_table_like(features):
predicted_classes.append('table')
elif self._is_sofa_like(features):
predicted_classes.append('sofa')
elif self._is_bed_like(features):
predicted_classes.append('bed')
elif self._is_desk_like(features):
predicted_classes.append('desk')
elif self._is_bookshelf_like(features):
predicted_classes.append('bookshelf')
elif self._is_lamp_like(features):
predicted_classes.append('lamp')
elif self._is_cabinet_like(features):
predicted_classes.append('cabinet')
elif self._is_door_like(features):
predicted_classes.append('door')
elif self._is_window_like(features):
predicted_classes.append('window')
else:
predicted_classes.append('unknown')
return predicted_classes
def _is_chair_like(self, features: Dict) -> bool:
"""의자와 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_yz'] > 1.5 and # 높이가 깊이보다 큼
features['aspect_ratio_xy'] < 2.0 and # 너비가 높이의 2배 이하
features['compactness'] > 0.1)
def _is_table_like(self, features: Dict) -> bool:
"""테이블과 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_xy'] > 1.5 and # 너비가 높이보다 큼
features['aspect_ratio_yz'] < 0.5 and # 높이가 깊이보다 작음
features['linearity'] < 0.3)
def _is_sofa_like(self, features: Dict) -> bool:
"""소파와 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_xy'] > 2.0 and # 너비가 높이의 2배 이상
features['aspect_ratio_xz'] > 1.5 and # 너비가 깊이의 1.5배 이상
features['compactness'] > 0.05)
def _is_bed_like(self, features: Dict) -> bool:
"""침대와 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_xy'] > 2.5 and # 너비가 높이의 2.5배 이상
features['aspect_ratio_xz'] > 1.8 and # 너비가 깊이의 1.8배 이상
features['aspect_ratio_yz'] < 0.3) # 높이가 깊이보다 작음
def _is_desk_like(self, features: Dict) -> bool:
"""책상과 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_xy'] > 1.2 and # 너비가 높이보다 큼
features['aspect_ratio_xz'] > 1.0 and # 너비가 깊이보다 큼
features['aspect_ratio_yz'] < 0.4) # 높이가 깊이보다 작음
def _is_bookshelf_like(self, features: Dict) -> bool:
"""책장과 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_yz'] > 2.0 and # 높이가 깊이의 2배 이상
features['aspect_ratio_xy'] > 1.5 and # 너비가 높이의 1.5배 이상
features['linearity'] > 0.4)
def _is_lamp_like(self, features: Dict) -> bool:
"""램프와 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_yz'] > 3.0 and # 높이가 깊이의 3배 이상
features['aspect_ratio_xy'] < 1.5 and # 너비가 높이의 1.5배 이하
features['compactness'] < 0.1)
def _is_cabinet_like(self, features: Dict) -> bool:
"""캐비닛과 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_xy'] > 1.0 and # 너비가 높이보다 큼
features['aspect_ratio_xz'] > 1.0 and # 너비가 깊이보다 큼
features['aspect_ratio_yz'] > 1.0 and # 높이가 깊이보다 큼
features['compactness'] > 0.2)
def _is_door_like(self, features: Dict) -> bool:
"""문과 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_yz'] > 2.5 and # 높이가 깊이의 2.5배 이상
features['aspect_ratio_xy'] < 0.5 and # 너비가 높이의 절반 이하
features['planarity'] > 0.7)
def _is_window_like(self, features: Dict) -> bool:
"""창문과 유사한 특성을 가졌는지 확인"""
return (features['aspect_ratio_xy'] > 1.5 and # 너비가 높이의 1.5배 이상
features['aspect_ratio_yz'] > 1.5 and # 높이가 깊이의 1.5배 이상
features['aspect_ratio_xz'] < 0.1) # 너비가 깊이의 0.1배 이하
def _extract_ground_truth_classes(self, ground_truth_labels: Dict) -> List[str]:
"""
Ground Truth에서 클래스 레이블을 추출합니다.
Args:
ground_truth_labels (Dict): Ground Truth 레이블 정보
Returns:
List[str]: Ground Truth 클래스 레이블 리스트
"""
if 'classes' in ground_truth_labels:
return ground_truth_labels['classes']
elif 'labels' in ground_truth_labels:
return ground_truth_labels['labels']
else:
# 기본 클래스 반환 (실제 구현에서는 사용자가 제공해야 함)
return ['unknown']
def _compute_class_wise_accuracy(self, predictions: List[str], ground_truth: List[str]) -> float:
"""
클래스별 정확도를 계산합니다.
Args:
predictions (List[str]): 예측된 클래스 레이블
ground_truth (List[str]): Ground Truth 클래스 레이블
Returns:
float: 클래스 정확도 (0-1)
"""
if not predictions or not ground_truth:
return 0.0
# 정확도 계산
correct = 0
total = min(len(predictions), len(ground_truth))
for i in range(total):
if predictions[i] == ground_truth[i]:
correct += 1
accuracy = correct / total if total > 0 else 0.0
return accuracy
def classify_objects_clustering(self, mesh: Dict, num_clusters: int = 5) -> List[str]:
"""
클러스터링을 사용하여 객체를 분류합니다.
Args:
mesh (Dict): 3D 메시 정보
num_clusters (int): 클러스터 개수
Returns:
List[str]: 예측된 클래스 레이블
"""
vertices = mesh['vertices']
if len(vertices) < num_clusters:
return ['unknown']
try:
# K-means 클러스터링
kmeans = KMeans(n_clusters=num_clusters, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(vertices)
# 클러스터별 특성 분석
predicted_classes = []
for cluster_id in range(num_clusters):
cluster_vertices = vertices[cluster_labels == cluster_id]
if len(cluster_vertices) < 10:
continue
# 클러스터의 기하학적 특성 계산
cluster_features = self._extract_geometric_features(cluster_vertices, np.array([]))
# 클래스 분류
cluster_classes = self._classify_by_geometric_features(cluster_features)
predicted_classes.extend(cluster_classes)
return predicted_classes if predicted_classes else ['unknown']
except Exception:
return ['unknown']
def calculate_class_distribution_similarity(self, model_3d: Dict, reference_3d: Dict) -> float:
"""
두 모델 간의 클래스 분포 유사성을 계산합니다.
Args:
model_3d (Dict): 변환된 3D 모델 정보
reference_3d (Dict): 참조 3D 모델 정보
Returns:
float: 클래스 분포 유사성 (0-1)
"""
# 각 모델의 클래스 분류
model_classes = self.classify_objects(model_3d)
reference_classes = self.classify_objects(reference_3d)
if not model_classes or not reference_classes:
return 0.0
# 클래스 분포 계산
model_dist = self._calculate_class_distribution(model_classes)
reference_dist = self._calculate_class_distribution(reference_classes)
# 분포 유사성 계산 (코사인 유사도)
similarity = self._calculate_distribution_similarity(model_dist, reference_dist)
return similarity
def _calculate_class_distribution(self, classes: List[str]) -> Dict[str, float]:
"""
클래스 분포를 계산합니다.
Args:
classes (List[str]): 클래스 레이블 리스트
Returns:
Dict[str, float]: 클래스별 분포
"""
total = len(classes)
if total == 0:
return {}
distribution = {}
for class_name in set(classes):
distribution[class_name] = classes.count(class_name) / total
return distribution
def _calculate_distribution_similarity(self, dist1: Dict[str, float],
dist2: Dict[str, float]) -> float:
"""
두 분포 간의 유사성을 계산합니다.
Args:
dist1 (Dict[str, float]): 첫 번째 분포
dist2 (Dict[str, float]): 두 번째 분포
Returns:
float: 분포 유사성 (0-1)
"""
# 모든 클래스 수집
all_classes = set(dist1.keys()) | set(dist2.keys())
if not all_classes:
return 1.0
# 벡터 생성
vec1 = np.array([dist1.get(cls, 0.0) for cls in all_classes])
vec2 = np.array([dist2.get(cls, 0.0) for cls in all_classes])
# 코사인 유사도 계산
dot_product = np.dot(vec1, vec2)
norm1 = np.linalg.norm(vec1)
norm2 = np.linalg.norm(vec2)
if norm1 == 0 or norm2 == 0:
return 0.0
similarity = dot_product / (norm1 * norm2)
return similarity
"""
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
"""
2D mAP (mean Average Precision) 계산 모듈
렌더링된 이미지와 원본 이미지 간의 객체 감지 정확도를 평가합니다.
"""
import numpy as np
import cv2
from typing import List, Dict, Tuple, Optional
class Map2DCalculator:
"""2D mAP 계산을 담당하는 클래스"""
def __init__(self, iou_thresholds: List[float] = [0.05, 0.1, 0.2]):
"""
2D mAP 계산기 초기화
Args:
iou_thresholds (List[float]): IoU 임계값 리스트 (더 관대한 임계값 사용)
"""
self.iou_thresholds = iou_thresholds
self.default_iou_threshold = 0.05 # 기본 IoU threshold를 더 낮춤
def calculate_2d_map(self, rendered_images: List[np.ndarray],
reference_image: np.ndarray,
iou_thresholds: Optional[List[float]] = None) -> float:
"""
2D mAP를 계산합니다.
Args:
rendered_images (List[np.ndarray]): 렌더링된 이미지 리스트
reference_image (np.ndarray): 참조 이미지
iou_thresholds (Optional[List[float]]): IoU 임계값 리스트
Returns:
float: 2D mAP 점수
"""
try:
if iou_thresholds is None:
iou_thresholds = self.iou_thresholds
if not rendered_images:
return 0.0
# 참조 이미지에서 Ground Truth 바운딩 박스 추출
gt_boxes = self._extract_ground_truth_boxes(reference_image)
if not gt_boxes:
return 0.0
# 각 IoU 임계값에 대해 mAP 계산
map_scores = []
for iou_threshold in iou_thresholds:
# 모든 렌더링된 이미지에 대해 예측 박스 추출 및 평가
all_predictions = []
all_ground_truths = []
for rendered_img in rendered_images:
# 렌더링된 이미지에서 예측 박스 추출
pred_boxes = self._extract_prediction_boxes(rendered_img)
# IoU 기반 매칭
matched_pairs = self._match_boxes(pred_boxes, gt_boxes, iou_threshold)
# 정밀도-재현율 계산을 위한 데이터 준비
predictions, ground_truths = self._prepare_pr_data(matched_pairs, pred_boxes, gt_boxes)
all_predictions.extend(predictions)
all_ground_truths.extend(ground_truths)
# Average Precision 계산
if all_predictions and all_ground_truths:
ap_score = self._calculate_average_precision(all_predictions, all_ground_truths)
map_scores.append(ap_score)
else:
map_scores.append(0.0)
# 평균 mAP 계산
mean_map = np.mean(map_scores) if map_scores else 0.0
return mean_map
except Exception as e:
print(f"Error in 2D mAP calculation: {str(e)}")
return 0.0
def compute_iou(self, box1: np.ndarray, box2: np.ndarray) -> float:
"""
두 바운딩 박스 간의 IoU를 계산합니다.
Args:
box1 (np.ndarray): 첫 번째 박스 [x1, y1, x2, y2]
box2 (np.ndarray): 두 번째 박스 [x1, y1, x2, y2]
Returns:
float: IoU 값 (0-1)
"""
# 교집합 영역 계산
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
if x2 <= x1 or y2 <= y1:
return 0.0
intersection = (x2 - x1) * (y2 - y1)
# 각 박스의 면적 계산
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
# 합집합 영역 계산
union = area1 + area2 - intersection
if union == 0:
return 0.0
return intersection / union
def calculate_precision_recall(self, predictions: List, ground_truth: List) -> Tuple[float, float]:
"""
정밀도와 재현율을 계산합니다.
Args:
predictions (List): 예측 결과 리스트
ground_truth (List): Ground Truth 리스트
Returns:
Tuple[float, float]: (정밀도, 재현율)
"""
if not predictions and not ground_truth:
return 1.0, 1.0
if not predictions:
return 0.0, 0.0
if not ground_truth:
return 0.0, 1.0
# True Positive, False Positive, False Negative 계산
tp = 0
fp = 0
fn = 0
# 예측 결과를 신뢰도 순으로 정렬
sorted_predictions = sorted(predictions, key=lambda x: x.get('confidence', 0), reverse=True)
matched_gt = set()
for pred in sorted_predictions:
best_iou = 0
best_gt_idx = -1
for i, gt in enumerate(ground_truth):
if i in matched_gt:
continue
iou = self.compute_iou(pred['bbox'], gt['bbox'])
if iou > best_iou:
best_iou = iou
best_gt_idx = i
if best_iou >= self.default_iou_threshold:
tp += 1
matched_gt.add(best_gt_idx)
else:
fp += 1
fn = len(ground_truth) - len(matched_gt)
# 정밀도와 재현율 계산
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
return precision, recall
def _extract_ground_truth_boxes(self, image: np.ndarray) -> List[Dict]:
"""
참조 이미지에서 Ground Truth 바운딩 박스를 추출합니다.
다중 방법을 조합하여 더 정확한 객체 감지를 수행합니다.
Args:
image (np.ndarray): 입력 이미지
Returns:
List[Dict]: Ground Truth 바운딩 박스 리스트
"""
print(f"Ground Truth 박스 추출 시작 - 이미지 크기: {image.shape}")
# 다중 방법으로 객체 감지
all_boxes = []
# 방법 1: 개선된 Canny 엣지 검출
canny_boxes = self._extract_boxes_canny(image)
all_boxes.extend(canny_boxes)
print(f" Canny 방법: {len(canny_boxes)}개 박스")
# 방법 2: 적응적 임계값 기반
adaptive_boxes = self._extract_boxes_adaptive_threshold(image)
all_boxes.extend(adaptive_boxes)
print(f" 적응적 임계값 방법: {len(adaptive_boxes)}개 박스")
# 방법 3: 색상 기반 분할
color_boxes = self._extract_boxes_color_based(image)
all_boxes.extend(color_boxes)
print(f" 색상 기반 방법: {len(color_boxes)}개 박스")
# 방법 4: 히스토그램 균등화 + 그래디언트
gradient_boxes = self._extract_boxes_gradient_based(image)
all_boxes.extend(gradient_boxes)
print(f" 그래디언트 방법: {len(gradient_boxes)}개 박스")
print(f" 총 감지된 박스: {len(all_boxes)}개")
# 중복 제거 및 병합
merged_boxes = self._merge_overlapping_boxes(all_boxes, iou_threshold=0.3)
print(f" 병합 후 박스: {len(merged_boxes)}개")
# 신뢰도 순으로 정렬하고 상위 결과만 반환
merged_boxes.sort(key=lambda x: x['confidence'], reverse=True)
# 객체가 감지되지 않은 경우 fallback 방법 사용
if len(merged_boxes) == 0:
print(" 객체 감지 실패, fallback 방법 사용")
merged_boxes = self._create_fallback_ground_truth_boxes(image)
result_boxes = merged_boxes[:10] # 최대 10개 객체 반환
print(f" 최종 Ground Truth 박스: {len(result_boxes)}개")
return result_boxes
def _create_fallback_ground_truth_boxes(self, image: np.ndarray) -> List[Dict]:
"""객체 감지 실패 시 fallback Ground Truth 박스 생성"""
print(" Fallback Ground Truth 박스 생성")
h, w = image.shape[:2]
boxes = []
# 방법 1: 이미지를 4개 영역으로 분할
quarter_w, quarter_h = w // 2, h // 2
regions = [
[0, 0, quarter_w, quarter_h], # 좌상단
[quarter_w, 0, w, quarter_h], # 우상단
[0, quarter_h, quarter_w, h], # 좌하단
[quarter_w, quarter_h, w, h] # 우하단
]
for i, region in enumerate(regions):
x1, y1, x2, y2 = region
boxes.append({
'bbox': [x1, y1, x2, y2],
'confidence': 0.5,
'class': i % 3,
'method': 'region_split'
})
# 방법 2: 이미지 전체를 하나의 객체로도 추가
boxes.append({
'bbox': [0, 0, w, h],
'confidence': 0.4,
'class': 0,
'method': 'full_image'
})
print(f" Fallback 박스 생성 완료: {len(boxes)}개")
return boxes
def _extract_boxes_canny(self, image: np.ndarray) -> List[Dict]:
"""Canny 엣지 검출을 사용한 박스 추출."""
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 50, 150)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 500:
x, y, w, h = cv2.boundingRect(contour)
if w > 20 and h > 20:
boxes.append({
'bbox': [x, y, x + w, y + h],
'area': area,
'confidence': 1.0,
'method': 'canny'
})
return boxes
def _extract_boxes_adaptive_threshold(self, image: np.ndarray) -> List[Dict]:
"""적응적 임계값을 사용한 박스 추출."""
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
# 모폴로지 연산
kernel = np.ones((3, 3), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 400:
x, y, w, h = cv2.boundingRect(contour)
if w > 15 and h > 15:
boxes.append({
'bbox': [x, y, x + w, y + h],
'area': area,
'confidence': 1.0,
'method': 'adaptive'
})
return boxes
def _extract_boxes_color_based(self, image: np.ndarray) -> List[Dict]:
"""색상 기반 박스 추출."""
# HSV 색공간으로 변환
hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
# 색상 범위 정의 (일반적인 객체 색상)
lower_bound = np.array([0, 50, 50])
upper_bound = np.array([180, 255, 255])
# 색상 마스크 생성
mask = cv2.inRange(hsv, lower_bound, upper_bound)
# 모폴로지 연산
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 300:
x, y, w, h = cv2.boundingRect(contour)
if w > 10 and h > 10:
boxes.append({
'bbox': [x, y, x + w, y + h],
'area': area,
'confidence': 1.0,
'method': 'color'
})
return boxes
def _extract_boxes_gradient_based(self, image: np.ndarray) -> List[Dict]:
"""히스토그램 균등화 + 그래디언트 기반 박스 추출."""
# 히스토그램 균등화
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
equalized = cv2.equalizeHist(gray)
# Sobel 그래디언트 계산
grad_x = cv2.Sobel(equalized, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(equalized, cv2.CV_64F, 0, 1, ksize=3)
gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2)
# 그래디언트 크기 정규화
gradient_magnitude = np.uint8(255 * gradient_magnitude / np.max(gradient_magnitude))
# 동적 임계값 적용
threshold = np.mean(gradient_magnitude) + np.std(gradient_magnitude)
binary = cv2.threshold(gradient_magnitude, threshold, 255, cv2.THRESH_BINARY)[1]
# 모폴로지 연산
kernel = np.ones((5, 5), np.uint8)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 300:
x, y, w, h = cv2.boundingRect(contour)
if w > 15 and h > 15:
boxes.append({
'bbox': [x, y, x + w, y + h],
'area': area,
'confidence': 0.8,
'method': 'gradient'
})
return boxes
def _scale_boxes_back(self, boxes: List[Dict], scale: float) -> List[Dict]:
"""박스 좌표를 원본 스케일로 역변환."""
if scale == 1.0:
return boxes
scaled_boxes = []
for box in boxes:
bbox = box['bbox']
scaled_bbox = [
int(bbox[0] / scale),
int(bbox[1] / scale),
int(bbox[2] / scale),
int(bbox[3] / scale)
]
scaled_box = box.copy()
scaled_box['bbox'] = scaled_bbox
scaled_box['area'] = scaled_box['area'] / (scale * scale)
scaled_boxes.append(scaled_box)
return scaled_boxes
def _merge_overlapping_boxes(self, boxes: List[Dict], iou_threshold: float = 0.3) -> List[Dict]:
"""중복되는 박스들을 병합합니다."""
if not boxes:
return []
# 면적 순으로 정렬
sorted_boxes = sorted(boxes, key=lambda x: x['area'], reverse=True)
merged_boxes = []
used_indices = set()
for i, box1 in enumerate(sorted_boxes):
if i in used_indices:
continue
# 현재 박스와 겹치는 박스들 찾기
overlapping_boxes = [box1]
used_indices.add(i)
for j, box2 in enumerate(sorted_boxes[i+1:], i+1):
if j in used_indices:
continue
iou = self.compute_iou(box1['bbox'], box2['bbox'])
if iou > iou_threshold:
overlapping_boxes.append(box2)
used_indices.add(j)
# 겹치는 박스들을 하나로 병합
if len(overlapping_boxes) > 1:
merged_box = self._merge_box_group(overlapping_boxes)
else:
merged_box = box1
merged_boxes.append(merged_box)
return merged_boxes
def _merge_box_group(self, boxes: List[Dict]) -> Dict:
"""박스 그룹을 하나의 박스로 병합."""
if not boxes:
return {}
# 모든 박스의 좌표를 결합
all_x1 = [box['bbox'][0] for box in boxes]
all_y1 = [box['bbox'][1] for box in boxes]
all_x2 = [box['bbox'][2] for box in boxes]
all_y2 = [box['bbox'][3] for box in boxes]
# 최소/최대 좌표로 새로운 박스 생성
merged_bbox = [min(all_x1), min(all_y1), max(all_x2), max(all_y2)]
merged_area = (merged_bbox[2] - merged_bbox[0]) * (merged_bbox[3] - merged_bbox[1])
# 평균 신뢰도 계산
avg_confidence = np.mean([box['confidence'] for box in boxes])
return {
'bbox': merged_bbox,
'area': merged_area,
'confidence': avg_confidence,
'method': 'merged',
'merged_count': len(boxes)
}
def _extract_prediction_boxes(self, image: np.ndarray) -> List[Dict]:
"""
렌더링된 이미지에서 예측 바운딩 박스를 추출합니다.
다중 방법을 조합하여 더 정확한 예측을 수행합니다.
Args:
image (np.ndarray): 렌더링된 이미지
Returns:
List[Dict]: 예측 바운딩 박스 리스트
"""
# 다중 방법으로 예측 박스 추출
all_boxes = []
# 방법 1: 적응적 임계값
adaptive_boxes = self._extract_prediction_adaptive(image)
all_boxes.extend(adaptive_boxes)
# 방법 2: Otsu 임계값
otsu_boxes = self._extract_prediction_otsu(image)
all_boxes.extend(otsu_boxes)
# 방법 3: 엣지 기반
edge_boxes = self._extract_prediction_edge(image)
all_boxes.extend(edge_boxes)
# 중복 제거 및 병합 (더 관대한 IoU threshold)
merged_boxes = self._merge_overlapping_boxes(all_boxes, iou_threshold=0.2)
# 신뢰도 순으로 정렬
merged_boxes.sort(key=lambda x: x['confidence'], reverse=True)
# 예측 박스가 없는 경우 fallback 생성
if not merged_boxes:
merged_boxes = self._create_fallback_prediction_boxes(image)
return merged_boxes[:10] # 최대 10개 예측 반환
def _create_fallback_prediction_boxes(self, image: np.ndarray) -> List[Dict]:
"""예측 박스가 없는 경우 fallback 박스 생성"""
h, w = image.shape[:2]
boxes = []
# 이미지를 여러 영역으로 분할하여 예측 박스 생성
quarter_w, quarter_h = w // 2, h // 2
regions = [
[0, 0, quarter_w, quarter_h], # 좌상단
[quarter_w, 0, w, quarter_h], # 우상단
[0, quarter_h, quarter_w, h], # 좌하단
[quarter_w, quarter_h, w, h] # 우하단
]
for i, region in enumerate(regions):
x1, y1, x2, y2 = region
boxes.append({
'bbox': [x1, y1, x2, y2],
'confidence': 0.3,
'class': i % 3,
'method': 'fallback_region'
})
# 이미지 전체를 하나의 예측으로도 추가
boxes.append({
'bbox': [0, 0, w, h],
'confidence': 0.2,
'class': 0,
'method': 'fallback_full'
})
return boxes
def _extract_prediction_adaptive(self, image: np.ndarray) -> List[Dict]:
"""적응적 임계값을 사용한 예측 박스 추출."""
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
else:
gray = image
# 적응적 임계값 적용
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
# 모폴로지 연산으로 노이즈 제거
kernel = np.ones((3, 3), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 50: # 매우 낮은 임계값 (150 → 50)
x, y, w, h = cv2.boundingRect(contour)
if w > 3 and h > 3: # 매우 낮은 최소 크기 (8 → 3)
boxes.append({
'bbox': [x, y, x + w, y + h],
'area': area,
'confidence': 0.6,
'method': 'adaptive'
})
return boxes
def _extract_prediction_otsu(self, image: np.ndarray) -> List[Dict]:
"""Otsu 임계값을 사용한 예측 박스 추출."""
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
else:
gray = image
# Otsu 임계값 적용
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 모폴로지 연산
kernel = np.ones((3, 3), np.uint8)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 30: # 매우 낮은 임계값 (100 → 30)
x, y, w, h = cv2.boundingRect(contour)
if w > 2 and h > 2: # 매우 낮은 최소 크기 (6 → 2)
boxes.append({
'bbox': [x, y, x + w, y + h],
'area': area,
'confidence': 0.55,
'method': 'otsu'
})
return boxes
def _extract_prediction_edge(self, image: np.ndarray) -> List[Dict]:
"""엣지 기반 예측 박스 추출."""
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
else:
gray = image
# 가우시안 블러
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Canny 엣지 검출
edges = cv2.Canny(blurred, 30, 100) # 더 낮은 임계값
# 모폴로지 연산
kernel = np.ones((3, 3), np.uint8)
edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for contour in contours:
area = cv2.contourArea(contour)
if area > 20: # 매우 낮은 임계값 (80 → 20)
x, y, w, h = cv2.boundingRect(contour)
if w > 2 and h > 2: # 매우 낮은 최소 크기 (5 → 2)
boxes.append({
'bbox': [x, y, x + w, y + h],
'area': area,
'confidence': 0.5,
'method': 'edge'
})
return boxes
def _match_boxes(self, pred_boxes: List[Dict], gt_boxes: List[Dict],
iou_threshold: float) -> List[Tuple[int, int, float]]:
"""
예측 박스와 Ground Truth 박스를 매칭합니다.
다중 매칭 전략을 사용하여 더 나은 매칭을 수행합니다.
Args:
pred_boxes (List[Dict]): 예측 박스 리스트
gt_boxes (List[Dict]): Ground Truth 박스 리스트
iou_threshold (float): IoU 임계값
Returns:
List[Tuple[int, int, float]]: 매칭된 박스 쌍 (pred_idx, gt_idx, iou)
"""
matches = []
used_gt = set()
# 예측 박스를 신뢰도 순으로 정렬
sorted_pred = sorted(pred_boxes, key=lambda x: x['confidence'], reverse=True)
# 1차: 정확한 IoU 매칭
for pred_idx, pred_box in enumerate(sorted_pred):
best_iou = 0
best_gt_idx = -1
for gt_idx, gt_box in enumerate(gt_boxes):
if gt_idx in used_gt:
continue
iou = self.compute_iou(pred_box['bbox'], gt_box['bbox'])
if iou > best_iou:
best_iou = iou
best_gt_idx = gt_idx
# 매칭 임계값 확인
if best_gt_idx != -1 and best_iou >= iou_threshold:
matches.append((pred_idx, best_gt_idx, best_iou))
used_gt.add(best_gt_idx)
# 2차: 낮은 임계값으로 추가 매칭 시도
if not matches and iou_threshold > 0.01:
print(f" 2D 정확한 매칭 실패, 낮은 임계값으로 재시도: {iou_threshold * 0.5}")
return self._match_boxes(pred_boxes, gt_boxes, iou_threshold * 0.5)
# 3차: 중심점 거리 기반 매칭 (IoU가 낮은 경우)
if not matches:
print(" 2D IoU 매칭 실패, 중심점 거리 기반 매칭 시도")
matches = self._match_by_center_distance(pred_boxes, gt_boxes)
return matches
def _match_by_center_distance(self, pred_boxes: List[Dict], gt_boxes: List[Dict]) -> List[Tuple[int, int, float]]:
"""중심점 거리 기반 매칭 (2D IoU 매칭 실패 시 사용)"""
matches = []
used_gt = set()
for pred_idx, pred_box in enumerate(pred_boxes):
best_distance = float('inf')
best_gt_idx = -1
# 예측 박스 중심점 계산
pred_bbox = pred_box['bbox']
pred_center = [(pred_bbox[0] + pred_bbox[2]) / 2, (pred_bbox[1] + pred_bbox[3]) / 2]
for gt_idx, gt_box in enumerate(gt_boxes):
if gt_idx in used_gt:
continue
# GT 박스 중심점 계산
gt_bbox = gt_box['bbox']
gt_center = [(gt_bbox[0] + gt_bbox[2]) / 2, (gt_bbox[1] + gt_bbox[3]) / 2]
# 중심점 간 거리 계산
distance = np.sqrt((pred_center[0] - gt_center[0])**2 + (pred_center[1] - gt_center[1])**2)
if distance < best_distance:
best_distance = distance
best_gt_idx = gt_idx
# 거리 임계값 확인 (이미지 크기에 상대적인 임계값)
max_image_size = max(pred_bbox[2] - pred_bbox[0], pred_bbox[3] - pred_bbox[1])
distance_threshold = max_image_size * 0.3 # 이미지 크기의 30%
if best_gt_idx != -1 and best_distance < distance_threshold:
# 거리를 IoU 스타일 점수로 변환 (0-1 범위)
similarity_score = max(0, 1 - best_distance / distance_threshold)
matches.append((pred_idx, best_gt_idx, similarity_score))
used_gt.add(best_gt_idx)
return matches
def _prepare_pr_data(self, matches: List[Tuple[int, int, float]],
pred_boxes: List[Dict], gt_boxes: List[Dict]) -> Tuple[List, List]:
"""
정밀도-재현율 계산을 위한 데이터를 준비합니다.
Args:
matches (List[Tuple[int, int, float]]): 매칭된 박스 쌍
pred_boxes (List[Dict]): 예측 박스 리스트
gt_boxes (List[Dict]): Ground Truth 박스 리스트
Returns:
Tuple[List, List]: (예측 결과, Ground Truth)
"""
predictions = []
ground_truths = []
# 매칭된 박스들
for pred_idx, gt_idx, iou in matches:
predictions.append({
'confidence': pred_boxes[pred_idx]['confidence'],
'is_positive': True
})
ground_truths.append(True)
# 매칭되지 않은 예측 박스들 (False Positive)
matched_pred_indices = {match[0] for match in matches}
for i, pred_box in enumerate(pred_boxes):
if i not in matched_pred_indices:
predictions.append({
'confidence': pred_box['confidence'],
'is_positive': False
})
ground_truths.append(False)
return predictions, ground_truths
def _calculate_average_precision(self, predictions: List, ground_truths: List) -> float:
"""
Average Precision을 계산합니다.
Args:
predictions (List): 예측 결과 리스트
ground_truths (List): Ground Truth 리스트
Returns:
float: Average Precision 점수
"""
if not predictions or not ground_truths:
return 0.0
# 신뢰도 순으로 정렬
sorted_data = sorted(zip(predictions, ground_truths),
key=lambda x: x[0]['confidence'], reverse=True)
# 정밀도-재현율 곡선 계산
precisions = []
recalls = []
tp = 0
fp = 0
total_positives = sum(ground_truths)
for pred, gt in sorted_data:
if gt:
tp += 1
else:
fp += 1
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall = tp / total_positives if total_positives > 0 else 0.0
precisions.append(precision)
recalls.append(recall)
# Average Precision 계산 (11-point interpolation)
ap = 0.0
for t in np.arange(0, 1.1, 0.1):
if np.sum(np.array(recalls) >= t) == 0:
p = 0
else:
p = np.max(np.array(precisions)[np.array(recalls) >= t])
ap += p / 11.0
return ap
\ No newline at end of file
"""
3D mAP (mean Average Precision) 계산 모듈
3D 모델과 참조 모델 간의 3D 객체 감지 정확도를 평가합니다.
"""
import numpy as np
from typing import List, Dict, Tuple
class Map3DCalculator:
"""3D mAP 계산을 담당하는 클래스"""
def __init__(self, iou_3d_thresholds: List[float] = [0.05, 0.1, 0.2]):
"""
3D mAP 계산기 초기화
Args:
iou_3d_thresholds (List[float]): 3D IoU 임계값 리스트 (더 관대한 임계값 사용)
"""
self.iou_3d_thresholds = iou_3d_thresholds
self.default_iou_threshold = 0.05 # 기본 IoU threshold를 더 낮춤
def calculate_3d_map(self, model_3d: Dict, ground_truth_3d: Dict) -> float:
"""
3D mAP를 계산합니다.
Args:
model_3d (Dict): 변환된 3D 모델 정보
ground_truth_3d (Dict): 참조 3D 모델 정보
Returns:
float: 3D mAP 점수
"""
try:
# 3D 바운딩 박스 추출
pred_boxes_3d = self.extract_3d_bounding_boxes(model_3d)
gt_boxes_3d = self.extract_3d_bounding_boxes(ground_truth_3d)
if not pred_boxes_3d or not gt_boxes_3d:
return 0.0
# 각 IoU 임계값에 대해 mAP 계산
map_scores = []
for iou_threshold in self.iou_3d_thresholds:
# 3D IoU 기반 매칭
matched_pairs = self._match_3d_boxes(pred_boxes_3d, gt_boxes_3d, iou_threshold)
# 정밀도-재현율 데이터 준비
predictions, ground_truths = self._prepare_3d_pr_data(matched_pairs, pred_boxes_3d, gt_boxes_3d)
# Average Precision 계산
if predictions and ground_truths:
ap_score = self._calculate_3d_average_precision(predictions, ground_truths)
map_scores.append(ap_score)
else:
map_scores.append(0.0)
# 평균 mAP 계산
mean_map = np.mean(map_scores) if map_scores else 0.0
return mean_map
except Exception as e:
print(f"Error in 3D mAP calculation: {str(e)}")
return 0.0
def extract_3d_bounding_boxes(self, mesh: Dict) -> List[Dict]:
"""
3D 메시에서 바운딩 박스를 추출합니다.
개별 객체별로 3D 바운딩 박스를 생성하여 더 정확한 3D mAP 계산을 수행합니다.
Args:
mesh (Dict): 3D 메시 정보
Returns:
List[Dict]: 3D 바운딩 박스 리스트
"""
print(f"3D 바운딩 박스 추출 시작")
if 'vertices' not in mesh:
print(" vertices 키가 없음, 빈 리스트 반환")
return []
vertices = np.array(mesh['vertices'])
if len(vertices) == 0:
print(" vertices가 비어있음, 빈 리스트 반환")
return []
print(f" vertices 개수: {len(vertices)}")
# 개별 객체별 바운딩 박스 생성
bboxes_3d = []
# 방법 1: 클러스터링 기반 객체 분리
clustered_boxes = self._extract_clustered_3d_boxes(vertices)
bboxes_3d.extend(clustered_boxes)
print(f" 클러스터링 방법: {len(clustered_boxes)}개 박스")
# 방법 2: 기하학적 특성 기반 분리
geometric_boxes = self._extract_geometric_3d_boxes(vertices, mesh)
bboxes_3d.extend(geometric_boxes)
print(f" 기하학적 방법: {len(geometric_boxes)}개 박스")
# 방법 3: 전체 메시를 하나의 객체로 처리 (fallback)
if not bboxes_3d:
print(" 객체 분리 실패, 전체 메시를 하나의 객체로 처리")
overall_box = self._extract_overall_3d_box(vertices)
bboxes_3d.append(overall_box)
# 중복 제거 및 필터링 (더 관대한 임계값 사용)
filtered_boxes = self._filter_3d_boxes(bboxes_3d, iou_threshold=0.05)
print(f" 필터링 후: {len(filtered_boxes)}개 박스")
# 여전히 박스가 없는 경우 기본 박스 생성
if not filtered_boxes:
print(" 모든 방법 실패, 기본 3D 박스 생성")
filtered_boxes = self._create_default_3d_boxes(vertices)
# 추가: 스케일 정규화를 통한 매칭 개선
filtered_boxes = self._normalize_boxes_for_matching(filtered_boxes, vertices)
print(f" 최종 3D 바운딩 박스: {len(filtered_boxes)}개")
return filtered_boxes
def _create_default_3d_boxes(self, vertices: np.ndarray) -> List[Dict]:
"""기본 3D 박스 생성 (모든 방법 실패 시)"""
print(" 기본 3D 박스 생성")
if len(vertices) == 0:
# vertices가 없는 경우 기본 정육면체 생성
return [{
'center': [0.0, 0.0, 0.0],
'size': [1.0, 1.0, 1.0],
'volume': 1.0,
'confidence': 0.1,
'method': 'default_cube'
}]
# vertices가 있는 경우 바운딩 박스 계산
min_coords = np.min(vertices, axis=0)
max_coords = np.max(vertices, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
# 최소 크기 보장
size = np.maximum(size, [0.1, 0.1, 0.1])
volume = np.prod(size)
boxes = []
# 방법 1: 전체 바운딩 박스
boxes.append({
'center': center.tolist(),
'size': size.tolist(),
'volume': float(volume),
'confidence': 0.5,
'method': 'overall_bbox'
})
# 방법 2: vertices를 2개 그룹으로 분할
if len(vertices) > 10:
mid_point = len(vertices) // 2
group1 = vertices[:mid_point]
group2 = vertices[mid_point:]
for i, group in enumerate([group1, group2]):
if len(group) > 0:
min_coords = np.min(group, axis=0)
max_coords = np.max(group, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
size = np.maximum(size, [0.1, 0.1, 0.1])
volume = np.prod(size)
boxes.append({
'center': center.tolist(),
'size': size.tolist(),
'volume': float(volume),
'confidence': 0.3,
'method': f'split_group_{i+1}'
})
print(f" 기본 3D 박스 생성 완료: {len(boxes)}개")
return boxes
def _normalize_boxes_for_matching(self, boxes: List[Dict], vertices: np.ndarray) -> List[Dict]:
"""매칭을 위한 박스 정규화"""
if not boxes or len(vertices) == 0:
return boxes
# 전체 메시의 바운딩 박스 계산
mesh_min = np.min(vertices, axis=0)
mesh_max = np.max(vertices, axis=0)
mesh_size = mesh_max - mesh_min
mesh_center = (mesh_min + mesh_max) / 2
normalized_boxes = []
for box in boxes:
# 상대적 위치와 크기로 정규화
normalized_box = box.copy()
# 중심점을 상대 좌표로 변환
if 'center' in box:
relative_center = (np.array(box['center']) - mesh_center) / (mesh_size + 1e-8)
normalized_box['normalized_center'] = relative_center.tolist()
# 크기를 상대 크기로 변환
if 'size' in box:
relative_size = np.array(box['size']) / (mesh_size + 1e-8)
normalized_box['normalized_size'] = relative_size.tolist()
# 매칭을 위한 추가 정보
normalized_box['mesh_center'] = mesh_center.tolist()
normalized_box['mesh_size'] = mesh_size.tolist()
normalized_boxes.append(normalized_box)
return normalized_boxes
def _extract_clustered_3d_boxes(self, vertices: np.ndarray) -> List[Dict]:
"""클러스터링을 사용한 3D 바운딩 박스 추출."""
try:
from sklearn.cluster import KMeans
# 정점 수가 충분한 경우에만 클러스터링 수행
if len(vertices) < 10:
return []
# 클러스터 개수 결정 (정점 수에 비례)
n_clusters = min(max(2, len(vertices) // 50), 5)
# K-means 클러스터링
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(vertices)
bboxes = []
for cluster_id in range(n_clusters):
cluster_vertices = vertices[cluster_labels == cluster_id]
if len(cluster_vertices) < 3: # 최소 정점 수 확인
continue
# 클러스터별 바운딩 박스 계산
min_coords = np.min(cluster_vertices, axis=0)
max_coords = np.max(cluster_vertices, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
# 유효한 크기인지 확인
if np.all(size > 0.01): # 최소 크기 임계값
bbox = {
'center': center,
'size': size,
'min_coords': min_coords,
'max_coords': max_coords,
'volume': np.prod(size),
'confidence': 0.8,
'method': 'clustering',
'cluster_id': cluster_id,
'num_vertices': len(cluster_vertices)
}
bboxes.append(bbox)
return bboxes
except ImportError:
# sklearn이 없는 경우 빈 리스트 반환
return []
except Exception as e:
print(f"클러스터링 중 오류 발생: {e}")
return []
def _extract_geometric_3d_boxes(self, vertices: np.ndarray, mesh: Dict) -> List[Dict]:
"""기하학적 특성 기반 3D 바운딩 박스 추출."""
bboxes = []
# 방법 1: Z축 기반 분리 (높이별 객체 분리)
z_based_boxes = self._extract_z_based_boxes(vertices)
bboxes.extend(z_based_boxes)
# 방법 2: 거리 기반 분리
distance_based_boxes = self._extract_distance_based_boxes(vertices)
bboxes.extend(distance_based_boxes)
# 방법 3: 밀도 기반 분리
density_based_boxes = self._extract_density_based_boxes(vertices)
bboxes.extend(density_based_boxes)
return bboxes
def _extract_z_based_boxes(self, vertices: np.ndarray) -> List[Dict]:
"""Z축(높이) 기반으로 객체를 분리하여 바운딩 박스 생성."""
if len(vertices) < 10:
return []
# Z축 값으로 정렬
z_values = vertices[:, 2]
z_sorted_indices = np.argsort(z_values)
# Z축을 여러 구간으로 나누기
n_segments = min(3, len(vertices) // 20) # 최대 3개 구간
if n_segments < 2:
return []
segment_size = len(vertices) // n_segments
bboxes = []
for i in range(n_segments):
start_idx = i * segment_size
end_idx = (i + 1) * segment_size if i < n_segments - 1 else len(vertices)
segment_vertices = vertices[z_sorted_indices[start_idx:end_idx]]
if len(segment_vertices) < 5:
continue
# 구간별 바운딩 박스 계산
min_coords = np.min(segment_vertices, axis=0)
max_coords = np.max(segment_vertices, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
if np.all(size > 0.01):
bbox = {
'center': center,
'size': size,
'min_coords': min_coords,
'max_coords': max_coords,
'volume': np.prod(size),
'confidence': 0.7,
'method': 'z_based',
'segment_id': i,
'num_vertices': len(segment_vertices)
}
bboxes.append(bbox)
return bboxes
def _extract_distance_based_boxes(self, vertices: np.ndarray) -> List[Dict]:
"""거리 기반으로 객체를 분리하여 바운딩 박스 생성."""
if len(vertices) < 20:
return []
# 중심점에서의 거리 계산
center = np.mean(vertices, axis=0)
distances = np.linalg.norm(vertices - center, axis=1)
# 거리 기준으로 정점들을 그룹화
distance_threshold = np.percentile(distances, 60) # 60% 분위수 사용
# 중심에 가까운 정점들
close_vertices = vertices[distances <= distance_threshold]
# 중심에서 먼 정점들
far_vertices = vertices[distances > distance_threshold]
bboxes = []
# 가까운 정점들 그룹
if len(close_vertices) >= 5:
min_coords = np.min(close_vertices, axis=0)
max_coords = np.max(close_vertices, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
if np.all(size > 0.01):
bbox = {
'center': center,
'size': size,
'min_coords': min_coords,
'max_coords': max_coords,
'volume': np.prod(size),
'confidence': 0.6,
'method': 'distance_based',
'group': 'close',
'num_vertices': len(close_vertices)
}
bboxes.append(bbox)
# 먼 정점들 그룹
if len(far_vertices) >= 5:
min_coords = np.min(far_vertices, axis=0)
max_coords = np.max(far_vertices, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
if np.all(size > 0.01):
bbox = {
'center': center,
'size': size,
'min_coords': min_coords,
'max_coords': max_coords,
'volume': np.prod(size),
'confidence': 0.6,
'method': 'distance_based',
'group': 'far',
'num_vertices': len(far_vertices)
}
bboxes.append(bbox)
return bboxes
def _extract_density_based_boxes(self, vertices: np.ndarray) -> List[Dict]:
"""밀도 기반으로 객체를 분리하여 바운딩 박스 생성."""
if len(vertices) < 30:
return []
# 간단한 그리드 기반 밀도 계산
min_coords = np.min(vertices, axis=0)
max_coords = np.max(vertices, axis=0)
# 그리드 크기 결정
grid_size = (max_coords - min_coords) / 3 # 3x3x3 그리드
bboxes = []
for i in range(3):
for j in range(3):
for k in range(3):
# 그리드 셀 경계 계산
cell_min = min_coords + np.array([i, j, k]) * grid_size
cell_max = cell_min + grid_size
# 해당 셀에 속하는 정점들 찾기
mask = np.all((vertices >= cell_min) & (vertices <= cell_max), axis=1)
cell_vertices = vertices[mask]
if len(cell_vertices) >= 5: # 충분한 정점이 있는 셀만 처리
center = (cell_min + cell_max) / 2
size = cell_max - cell_min
bbox = {
'center': center,
'size': size,
'min_coords': cell_min,
'max_coords': cell_max,
'volume': np.prod(size),
'confidence': 0.5,
'method': 'density_based',
'grid_cell': (i, j, k),
'num_vertices': len(cell_vertices)
}
bboxes.append(bbox)
return bboxes
def _extract_overall_3d_box(self, vertices: np.ndarray) -> Dict:
"""전체 메시를 하나의 3D 바운딩 박스로 처리."""
min_coords = np.min(vertices, axis=0)
max_coords = np.max(vertices, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
return {
'center': center,
'size': size,
'min_coords': min_coords,
'max_coords': max_coords,
'volume': np.prod(size),
'confidence': 1.0,
'method': 'overall',
'num_vertices': len(vertices)
}
def _filter_3d_boxes(self, bboxes: List[Dict], iou_threshold: float = 0.1) -> List[Dict]:
"""3D 바운딩 박스 필터링 및 중복 제거."""
if not bboxes:
return []
# 1. 최소 크기 필터링
filtered_boxes = []
for bbox in bboxes:
if np.all(bbox['size'] > 0.01): # 최소 크기 임계값
filtered_boxes.append(bbox)
if not filtered_boxes:
return []
# 2. 3D IoU 기반 중복 제거
final_boxes = []
used_indices = set()
# 신뢰도 순으로 정렬
sorted_boxes = sorted(filtered_boxes, key=lambda x: x['confidence'], reverse=True)
for i, bbox1 in enumerate(sorted_boxes):
if i in used_indices:
continue
is_duplicate = False
for j, bbox2 in enumerate(sorted_boxes[i+1:], i+1):
if j in used_indices:
continue
iou = self.compute_3d_iou(bbox1, bbox2)
if iou > iou_threshold: # 더 관대한 3D IoU 임계값 사용
used_indices.add(j)
is_duplicate = True
if not is_duplicate:
final_boxes.append(bbox1)
used_indices.add(i)
# 3. 상위 결과만 반환 (최대 5개)
final_boxes.sort(key=lambda x: x['confidence'], reverse=True)
return final_boxes[:5]
def compute_3d_iou(self, box1: Dict, box2: Dict) -> float:
"""
두 3D 바운딩 박스 간의 IoU를 계산합니다.
정규화된 좌표와 원본 좌표 모두 고려하여 더 정확한 매칭을 수행합니다.
Args:
box1 (Dict): 첫 번째 3D 박스
box2 (Dict): 두 번째 3D 박스
Returns:
float: 3D IoU 값 (0-1)
"""
# 정규화된 좌표가 있는 경우 우선 사용
if 'normalized_center' in box1 and 'normalized_center' in box2:
return self._compute_normalized_3d_iou(box1, box2)
# 원본 좌표 사용
if 'min_coords' not in box1 or 'min_coords' not in box2:
# min_coords가 없는 경우 center와 size로부터 계산
return self._compute_3d_iou_from_center_size(box1, box2)
# 교집합 영역 계산
min1, max1 = np.array(box1['min_coords']), np.array(box1['max_coords'])
min2, max2 = np.array(box2['min_coords']), np.array(box2['max_coords'])
# 교집합의 최소/최대 좌표
intersection_min = np.maximum(min1, min2)
intersection_max = np.minimum(max1, max2)
# 교집합이 존재하는지 확인
if np.any(intersection_min >= intersection_max):
return 0.0
# 교집합 부피 계산
intersection_volume = np.prod(intersection_max - intersection_min)
# 각 박스의 부피 계산
volume1 = np.prod(max1 - min1)
volume2 = np.prod(max2 - min2)
# 합집합 부피 계산
union_volume = volume1 + volume2 - intersection_volume
if union_volume == 0:
return 0.0
return intersection_volume / union_volume
def _compute_normalized_3d_iou(self, box1: Dict, box2: Dict) -> float:
"""정규화된 좌표를 사용한 3D IoU 계산"""
try:
# 정규화된 중심점과 크기 사용
center1 = np.array(box1['normalized_center'])
size1 = np.array(box1['normalized_size'])
center2 = np.array(box2['normalized_center'])
size2 = np.array(box2['normalized_size'])
# 바운딩 박스 경계 계산
min1 = center1 - size1 / 2
max1 = center1 + size1 / 2
min2 = center2 - size2 / 2
max2 = center2 + size2 / 2
# 교집합 계산
intersection_min = np.maximum(min1, min2)
intersection_max = np.minimum(max1, max2)
if np.any(intersection_min >= intersection_max):
return 0.0
intersection_volume = np.prod(intersection_max - intersection_min)
volume1 = np.prod(size1)
volume2 = np.prod(size2)
union_volume = volume1 + volume2 - intersection_volume
return intersection_volume / union_volume if union_volume > 0 else 0.0
except Exception:
return 0.0
def _compute_3d_iou_from_center_size(self, box1: Dict, box2: Dict) -> float:
"""center와 size로부터 3D IoU 계산"""
try:
center1 = np.array(box1['center'])
size1 = np.array(box1['size'])
center2 = np.array(box2['center'])
size2 = np.array(box2['size'])
# 바운딩 박스 경계 계산
min1 = center1 - size1 / 2
max1 = center1 + size1 / 2
min2 = center2 - size2 / 2
max2 = center2 + size2 / 2
# 교집합 계산
intersection_min = np.maximum(min1, min2)
intersection_max = np.minimum(max1, max2)
if np.any(intersection_min >= intersection_max):
return 0.0
intersection_volume = np.prod(intersection_max - intersection_min)
volume1 = np.prod(size1)
volume2 = np.prod(size2)
union_volume = volume1 + volume2 - intersection_volume
return intersection_volume / union_volume if union_volume > 0 else 0.0
except Exception:
return 0.0
def _match_3d_boxes(self, pred_boxes: List[Dict], gt_boxes: List[Dict],
iou_threshold: float) -> List[Tuple[int, int, float]]:
"""
3D 예측 박스와 Ground Truth 박스를 매칭합니다.
다중 매칭 전략을 사용하여 더 나은 매칭을 수행합니다.
Args:
pred_boxes (List[Dict]): 예측 3D 박스 리스트
gt_boxes (List[Dict]): Ground Truth 3D 박스 리스트
iou_threshold (float): 3D IoU 임계값
Returns:
List[Tuple[int, int, float]]: 매칭된 박스 쌍 (pred_idx, gt_idx, iou)
"""
matches = []
used_gt = set()
# 예측 박스를 신뢰도 순으로 정렬
sorted_pred = sorted(pred_boxes, key=lambda x: x['confidence'], reverse=True)
# 1차: 정확한 IoU 매칭
for pred_idx, pred_box in enumerate(sorted_pred):
best_iou = 0
best_gt_idx = -1
for gt_idx, gt_box in enumerate(gt_boxes):
if gt_idx in used_gt:
continue
iou = self.compute_3d_iou(pred_box, gt_box)
if iou > best_iou:
best_iou = iou
best_gt_idx = gt_idx
# 매칭 임계값 확인
if best_gt_idx != -1 and best_iou >= iou_threshold:
matches.append((pred_idx, best_gt_idx, best_iou))
used_gt.add(best_gt_idx)
# 2차: 낮은 임계값으로 추가 매칭 시도
if not matches and iou_threshold > 0.01:
print(f" 정확한 매칭 실패, 낮은 임계값으로 재시도: {iou_threshold * 0.5}")
return self._match_3d_boxes(pred_boxes, gt_boxes, iou_threshold * 0.5)
# 3차: 거리 기반 매칭 (IoU가 낮은 경우)
if not matches:
print(" IoU 매칭 실패, 거리 기반 매칭 시도")
matches = self._match_by_distance(pred_boxes, gt_boxes)
return matches
def _match_by_distance(self, pred_boxes: List[Dict], gt_boxes: List[Dict]) -> List[Tuple[int, int, float]]:
"""거리 기반 매칭 (IoU 매칭 실패 시 사용)"""
matches = []
used_gt = set()
for pred_idx, pred_box in enumerate(pred_boxes):
best_distance = float('inf')
best_gt_idx = -1
for gt_idx, gt_box in enumerate(gt_boxes):
if gt_idx in used_gt:
continue
# 중심점 간 거리 계산
if 'center' in pred_box and 'center' in gt_box:
center1 = np.array(pred_box['center'])
center2 = np.array(gt_box['center'])
distance = np.linalg.norm(center1 - center2)
if distance < best_distance:
best_distance = distance
best_gt_idx = gt_idx
# 거리 임계값 확인 (상대적으로 관대한 임계값)
if best_gt_idx != -1 and best_distance < 1.0: # 임계값을 1.0으로 설정
# 거리를 IoU 스타일 점수로 변환 (0-1 범위)
similarity_score = max(0, 1 - best_distance / 2.0)
matches.append((pred_idx, best_gt_idx, similarity_score))
used_gt.add(best_gt_idx)
return matches
def _prepare_3d_pr_data(self, matches: List[Tuple[int, int, float]],
pred_boxes: List[Dict], gt_boxes: List[Dict]) -> Tuple[List, List]:
"""
3D 정밀도-재현율 계산을 위한 데이터를 준비합니다.
Args:
matches (List[Tuple[int, int, float]]): 매칭된 박스 쌍
pred_boxes (List[Dict]): 예측 3D 박스 리스트
gt_boxes (List[Dict]): Ground Truth 3D 박스 리스트
Returns:
Tuple[List, List]: (예측 결과, Ground Truth)
"""
predictions = []
ground_truths = []
# 매칭된 박스들
for pred_idx, gt_idx, iou in matches:
predictions.append({
'confidence': pred_boxes[pred_idx]['confidence'],
'is_positive': True
})
ground_truths.append(True)
# 매칭되지 않은 예측 박스들 (False Positive)
matched_pred_indices = {match[0] for match in matches}
for i, pred_box in enumerate(pred_boxes):
if i not in matched_pred_indices:
predictions.append({
'confidence': pred_box['confidence'],
'is_positive': False
})
ground_truths.append(False)
return predictions, ground_truths
def _calculate_3d_average_precision(self, predictions: List, ground_truths: List) -> float:
"""
3D Average Precision을 계산합니다.
Args:
predictions (List): 예측 결과 리스트
ground_truths (List): Ground Truth 리스트
Returns:
float: 3D Average Precision 점수
"""
if not predictions or not ground_truths:
return 0.0
# 신뢰도 순으로 정렬
sorted_data = sorted(zip(predictions, ground_truths),
key=lambda x: x[0]['confidence'], reverse=True)
# 정밀도-재현율 곡선 계산
precisions = []
recalls = []
tp = 0
fp = 0
total_positives = sum(ground_truths)
for pred, gt in sorted_data:
if gt:
tp += 1
else:
fp += 1
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall = tp / total_positives if total_positives > 0 else 0.0
precisions.append(precision)
recalls.append(recall)
# Average Precision 계산 (11-point interpolation)
ap = 0.0
for t in np.arange(0, 1.1, 0.1):
if np.sum(np.array(recalls) >= t) == 0:
p = 0
else:
p = np.max(np.array(precisions)[np.array(recalls) >= t])
ap += p / 11.0
return ap
"""
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
"""
유틸리티 모듈
로깅, 성능 모니터링 등의 유틸리티 기능을 제공합니다.
"""
from .logging_utils import DetailedLogger, get_logger, reset_logger
from .performance_monitor import (
PerformanceMonitor,
MemoryProfiler,
monitor_performance,
memory_profiling,
get_performance_monitor,
get_memory_profiler
)
__all__ = [
'DetailedLogger',
'get_logger',
'reset_logger',
'PerformanceMonitor',
'MemoryProfiler',
'monitor_performance',
'memory_profiling',
'get_performance_monitor',
'get_memory_profiler'
]
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment