feat: update algorithm integrations and point cloud overlays
This commit is contained in:
25
alg/train_measurement_system_v3.2_0629/src/__init__.py
Normal file
25
alg/train_measurement_system_v3.2_0629/src/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Train Measurement System
|
||||
|
||||
配置驱动的列车点云测量系统
|
||||
"""
|
||||
|
||||
__version__ = "1.1.0"
|
||||
|
||||
from .config_loader import ConfigLoader
|
||||
from .pointcloud_loader import PointCloudLoader
|
||||
from .roi_extractor import ROIExtractor
|
||||
from .plane_fitter import PlaneFitter
|
||||
from .task_manager import TaskManager
|
||||
from .excel_exporter import ExcelExporter
|
||||
from .visualizer import Visualizer
|
||||
|
||||
__all__ = [
|
||||
"ConfigLoader",
|
||||
"PointCloudLoader",
|
||||
"ROIExtractor",
|
||||
"PlaneFitter",
|
||||
"TaskManager",
|
||||
"ExcelExporter",
|
||||
"Visualizer",
|
||||
]
|
||||
82
alg/train_measurement_system_v3.2_0629/src/config_loader.py
Normal file
82
alg/train_measurement_system_v3.2_0629/src/config_loader.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""配置加载模块"""
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
"""加载和验证YAML配置文件"""
|
||||
|
||||
def __init__(self, config_path: str):
|
||||
"""
|
||||
Args:
|
||||
config_path: 配置文件路径
|
||||
"""
|
||||
self.config_path = Path(config_path)
|
||||
self.config = None
|
||||
|
||||
def load(self) -> Dict[str, Any]:
|
||||
"""加载配置文件"""
|
||||
if not self.config_path.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
|
||||
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
self._validate()
|
||||
return self.config
|
||||
|
||||
def _validate(self):
|
||||
"""验证配置文件格式"""
|
||||
if not isinstance(self.config, dict):
|
||||
raise ValueError("配置文件格式错误:根节点必须是字典")
|
||||
|
||||
# 检查必需字段
|
||||
if 'point_cloud' not in self.config:
|
||||
raise ValueError("配置文件缺少 'point_cloud' 字段")
|
||||
|
||||
if 'tasks' not in self.config:
|
||||
raise ValueError("配置文件缺少 'tasks' 字段")
|
||||
|
||||
if not isinstance(self.config['tasks'], list):
|
||||
raise ValueError("'tasks' 必须是列表")
|
||||
|
||||
# 验证每个任务
|
||||
for i, task in enumerate(self.config['tasks']):
|
||||
self._validate_task(task, i)
|
||||
|
||||
def _validate_task(self, task: Dict[str, Any], index: int):
|
||||
"""验证单个任务配置"""
|
||||
required_fields = ['name', 'type']
|
||||
for field in required_fields:
|
||||
if field not in task:
|
||||
raise ValueError(f"任务 {index} 缺少必需字段: {field}")
|
||||
|
||||
# 验证任务类型
|
||||
valid_types = ['plane_to_plane', 'point_to_plane', 'point_to_point']
|
||||
if task['type'] not in valid_types:
|
||||
raise ValueError(f"任务 {index} 类型无效: {task['type']}, 必须是 {valid_types} 之一")
|
||||
|
||||
def get_point_cloud_path(self) -> str:
|
||||
"""获取点云文件路径"""
|
||||
return self.config['point_cloud']
|
||||
|
||||
def get_tasks(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有任务配置"""
|
||||
return self.config['tasks']
|
||||
|
||||
def get_global_settings(self) -> Dict[str, Any]:
|
||||
"""获取全局设置"""
|
||||
return self.config.get('global_settings', {})
|
||||
|
||||
def is_visualization_enabled(self) -> bool:
|
||||
"""是否启用可视化"""
|
||||
return self.get_global_settings().get('enable_visualization', False)
|
||||
|
||||
def get_visualization_dir(self) -> str:
|
||||
"""获取可视化输出目录"""
|
||||
return self.get_global_settings().get('visualization_dir', 'visualizations')
|
||||
|
||||
def get_results_dir(self) -> str:
|
||||
"""获取结果输出目录"""
|
||||
return self.get_global_settings().get('results_dir', 'results')
|
||||
@@ -0,0 +1,126 @@
|
||||
"""边缘线ROI提取模块 - 两步法提取边缘线附近的点"""
|
||||
import numpy as np
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class EdgeLineROIExtractor:
|
||||
"""边缘线ROI提取器 - 两步法"""
|
||||
|
||||
@staticmethod
|
||||
def create_line_bbox(point1: np.ndarray, point2: np.ndarray, margin: float = 5.0) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
根据线的两个端点创建包围盒
|
||||
|
||||
原则:如果某维度跨度>10mm就保持,否则该维度±margin
|
||||
|
||||
Args:
|
||||
point1: 线的起点 [x, y, z]
|
||||
point2: 线的终点 [x, y, z]
|
||||
margin: 跨度小于10mm的维度的扩展边距(默认5mm)
|
||||
|
||||
Returns:
|
||||
bbox_min, bbox_max: 包围盒的最小和最大坐标
|
||||
"""
|
||||
point1 = np.array(point1)
|
||||
point2 = np.array(point2)
|
||||
|
||||
# 计算每个维度的范围
|
||||
mins = np.minimum(point1, point2)
|
||||
maxs = np.maximum(point1, point2)
|
||||
spans = maxs - mins
|
||||
|
||||
# 应用规则:跨度>10就保持,否则±margin
|
||||
bbox_min = np.zeros(3)
|
||||
bbox_max = np.zeros(3)
|
||||
|
||||
for i in range(3):
|
||||
if spans[i] > 10.0:
|
||||
# 跨度足够大,保持原样
|
||||
bbox_min[i] = mins[i]
|
||||
bbox_max[i] = maxs[i]
|
||||
else:
|
||||
# 跨度太小,扩展±margin
|
||||
center = (mins[i] + maxs[i]) / 2
|
||||
bbox_min[i] = center - margin
|
||||
bbox_max[i] = center + margin
|
||||
|
||||
return bbox_min, bbox_max
|
||||
|
||||
@staticmethod
|
||||
def extract_two_step_roi(
|
||||
points: np.ndarray,
|
||||
line_point1: np.ndarray,
|
||||
line_point2: np.ndarray,
|
||||
spatial_index,
|
||||
distance_threshold: float = 0.5,
|
||||
bbox_margin: float = 5.0
|
||||
) -> Tuple[np.ndarray, np.ndarray, dict]:
|
||||
"""
|
||||
两步法提取边缘线ROI
|
||||
|
||||
步骤1:用包围盒粗筛选
|
||||
步骤2:用点到线距离精筛选(距离<threshold)
|
||||
|
||||
Args:
|
||||
points: 完整点云 (N, 3)
|
||||
line_point1: 线的起点 [x, y, z]
|
||||
line_point2: 线的终点 [x, y, z]
|
||||
spatial_index: 空间索引对象
|
||||
distance_threshold: 点到线距离阈值(默认2mm)
|
||||
bbox_margin: 包围盒扩展边距(默认5mm)
|
||||
|
||||
Returns:
|
||||
fine_points: 精筛选后的点 (M, 3)
|
||||
fine_indices: 精筛选点在原始点云中的索引
|
||||
stats: 统计信息字典
|
||||
"""
|
||||
line_point1 = np.array(line_point1)
|
||||
line_point2 = np.array(line_point2)
|
||||
|
||||
# 步骤1:创建包围盒并粗筛选
|
||||
bbox_min, bbox_max = EdgeLineROIExtractor.create_line_bbox(
|
||||
line_point1, line_point2, margin=bbox_margin
|
||||
)
|
||||
|
||||
if spatial_index is not None:
|
||||
coarse_indices = spatial_index.query_bbox(bbox_min, bbox_max)
|
||||
else:
|
||||
# 回退到线性扫描
|
||||
mask = np.all((points >= bbox_min) & (points <= bbox_max), axis=1)
|
||||
coarse_indices = np.where(mask)[0]
|
||||
|
||||
coarse_points = points[coarse_indices]
|
||||
|
||||
if len(coarse_points) == 0:
|
||||
return np.array([]).reshape(0, 3), np.array([], dtype=int), {
|
||||
'coarse_count': 0,
|
||||
'fine_count': 0,
|
||||
'bbox_min': bbox_min.tolist(),
|
||||
'bbox_max': bbox_max.tolist()
|
||||
}
|
||||
|
||||
# 步骤2:计算点到线的距离,精筛选
|
||||
line_direction = line_point2 - line_point1
|
||||
line_direction = line_direction / np.linalg.norm(line_direction)
|
||||
|
||||
vectors = coarse_points - line_point1
|
||||
projections = np.dot(vectors, line_direction)[:, np.newaxis] * line_direction
|
||||
perpendicular_vectors = vectors - projections
|
||||
distances = np.linalg.norm(perpendicular_vectors, axis=1)
|
||||
|
||||
# 筛选距离<threshold的点
|
||||
fine_mask = distances < distance_threshold
|
||||
fine_indices_in_coarse = np.where(fine_mask)[0]
|
||||
fine_indices = coarse_indices[fine_indices_in_coarse]
|
||||
fine_points = coarse_points[fine_indices_in_coarse]
|
||||
|
||||
stats = {
|
||||
'coarse_count': len(coarse_points),
|
||||
'fine_count': len(fine_points),
|
||||
'bbox_min': bbox_min.tolist(),
|
||||
'bbox_max': bbox_max.tolist(),
|
||||
'distance_threshold': distance_threshold,
|
||||
'bbox_margin': bbox_margin
|
||||
}
|
||||
|
||||
return fine_points, fine_indices, stats
|
||||
380
alg/train_measurement_system_v3.2_0629/src/excel_exporter.py
Normal file
380
alg/train_measurement_system_v3.2_0629/src/excel_exporter.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""Excel结果导出器"""
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
from measurements.base import MeasurementResult
|
||||
|
||||
|
||||
class ExcelExporter:
|
||||
"""将测量结果导出为Excel文件"""
|
||||
|
||||
def __init__(self, output_dir: str):
|
||||
"""
|
||||
初始化导出器
|
||||
|
||||
Args:
|
||||
output_dir: 输出目录路径
|
||||
"""
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def export(self, results: List[MeasurementResult], filename: str = None, task_configs: List[Dict[str, Any]] = None) -> str:
|
||||
"""
|
||||
导出测量结果到Excel
|
||||
|
||||
Args:
|
||||
results: 测量结果列表
|
||||
filename: 输出文件名(不含扩展名),默认使用时间戳
|
||||
task_configs: 任务配置列表(用于提取描述和参数)
|
||||
|
||||
Returns:
|
||||
str: 导出的Excel文件路径
|
||||
"""
|
||||
if filename is None:
|
||||
filename = f"measurement_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
output_path = self.output_dir / f"{filename}.xlsx"
|
||||
|
||||
# 创建任务名称到配置的映射
|
||||
task_config_map = {}
|
||||
if task_configs:
|
||||
for config in task_configs:
|
||||
task_config_map[config.get('name')] = config
|
||||
|
||||
# 创建工作簿
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "测量结果"
|
||||
|
||||
# 写入标题
|
||||
self._write_header(ws)
|
||||
|
||||
# 写入数据
|
||||
row = 2
|
||||
for result in results:
|
||||
task_config = task_config_map.get(result.task_name, {})
|
||||
row = self._write_result(ws, result, row, task_config)
|
||||
|
||||
# 调整列宽
|
||||
self._adjust_column_widths(ws)
|
||||
|
||||
# 保存文件
|
||||
wb.save(output_path)
|
||||
|
||||
return str(output_path)
|
||||
|
||||
def _write_header(self, ws):
|
||||
"""写入表头"""
|
||||
headers = [
|
||||
"项点名称",
|
||||
"理论尺寸 (mm)",
|
||||
"公差要求",
|
||||
"测量结果 (mm)",
|
||||
"精度 (mm)",
|
||||
"误差范围 (mm)",
|
||||
"测量结论",
|
||||
"执行时间 (秒)",
|
||||
"错误信息"
|
||||
]
|
||||
|
||||
# 样式
|
||||
header_font = Font(bold=True, color="FFFFFF")
|
||||
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
||||
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||
border = Border(
|
||||
left=Side(style='thin'),
|
||||
right=Side(style='thin'),
|
||||
top=Side(style='thin'),
|
||||
bottom=Side(style='thin')
|
||||
)
|
||||
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col)
|
||||
cell.value = header
|
||||
cell.font = header_font
|
||||
cell.fill = header_fill
|
||||
cell.alignment = header_alignment
|
||||
cell.border = border
|
||||
|
||||
def _format_task_params(self, task_config: Dict[str, Any]) -> str:
|
||||
"""
|
||||
格式化任务参数为字符串
|
||||
|
||||
Args:
|
||||
task_config: 任务配置字典
|
||||
|
||||
Returns:
|
||||
str: 格式化后的参数字符串
|
||||
"""
|
||||
task_type = task_config.get('type', '')
|
||||
params_parts = []
|
||||
|
||||
if task_type == 'plane_to_plane':
|
||||
# 平面到平面:显示源ROI和目标ROI
|
||||
source_roi = task_config.get('source_roi', {})
|
||||
target_roi = task_config.get('target_roi', {})
|
||||
if source_roi:
|
||||
params_parts.append(f"源ROI: min{source_roi.get('bbox_min', [])} max{source_roi.get('bbox_max', [])}")
|
||||
if target_roi:
|
||||
params_parts.append(f"目标ROI: min{target_roi.get('bbox_min', [])} max{target_roi.get('bbox_max', [])}")
|
||||
|
||||
elif task_type == 'point_to_plane':
|
||||
mode = task_config.get('mode', '')
|
||||
if mode == 'exact_points':
|
||||
# 精确点模式:显示目标点坐标
|
||||
target_points = task_config.get('target_points', [])
|
||||
if target_points:
|
||||
params_parts.append(f"目标点({len(target_points)}个): {target_points}")
|
||||
plane = task_config.get('plane', {})
|
||||
if plane:
|
||||
params_parts.append(f"平面: {plane.get('a', 0)}x+{plane.get('b', 0)}y+{plane.get('c', 0)}z+{plane.get('d', 0)}=0")
|
||||
elif mode == 'bbox_filtered':
|
||||
# 包围盒过滤模式:显示包围盒
|
||||
bboxes = task_config.get('bboxes', [])
|
||||
if bboxes:
|
||||
bbox_strs = [f"{bbox.get('name', '')}: min{bbox.get('min', [])} max{bbox.get('max', [])}" for bbox in bboxes]
|
||||
params_parts.append(f"包围盒: {'; '.join(bbox_strs)}")
|
||||
filter_info = task_config.get('filter', {})
|
||||
if filter_info:
|
||||
params_parts.append(f"过滤: {filter_info.get('type', '')} axis={filter_info.get('axis', '')} top_n={filter_info.get('top_n', '')}")
|
||||
|
||||
elif task_type == 'point_to_point':
|
||||
# 点到点:显示点对
|
||||
point_pairs = task_config.get('point_pairs', [])
|
||||
if point_pairs:
|
||||
pair_strs = [f"{pair.get('name', '')}: {pair.get('point1', [])} <-> {pair.get('point2', [])}" for pair in point_pairs]
|
||||
params_parts.append(f"点对({len(point_pairs)}对): {'; '.join(pair_strs)}")
|
||||
|
||||
return '\n'.join(params_parts) if params_parts else "无"
|
||||
|
||||
def _write_result(self, ws, result: MeasurementResult, row: int, task_config: Dict[str, Any]) -> int:
|
||||
"""
|
||||
写入单个测量结果
|
||||
|
||||
Args:
|
||||
ws: 工作表
|
||||
result: 测量结果
|
||||
row: 起始行号
|
||||
task_config: 任务配置
|
||||
|
||||
Returns:
|
||||
int: 下一行号
|
||||
"""
|
||||
border = Border(
|
||||
left=Side(style='thin'),
|
||||
right=Side(style='thin'),
|
||||
top=Side(style='thin'),
|
||||
bottom=Side(style='thin')
|
||||
)
|
||||
|
||||
# 1. 项点名称
|
||||
ws.cell(row=row, column=1).value = result.task_name
|
||||
|
||||
# 检查是否是任务19(宽度差值检查任务)
|
||||
is_task19 = result.success and result.data and 'diff_upper_middle' in result.data
|
||||
|
||||
# 检查是否是点对差值比较任务
|
||||
is_comparison_task = result.success and result.data and 'comparison' in result.data
|
||||
|
||||
# 2. 理论尺寸
|
||||
if is_task19:
|
||||
# 任务19的理论尺寸为0
|
||||
ws.cell(row=row, column=2).value = 0
|
||||
elif is_comparison_task:
|
||||
# 点对差值比较任务的理论尺寸为0(期望差值为0)
|
||||
ws.cell(row=row, column=2).value = 0
|
||||
else:
|
||||
theoretical_dim = task_config.get('theoretical_dimension', None)
|
||||
if theoretical_dim is not None:
|
||||
ws.cell(row=row, column=2).value = round(theoretical_dim, 2)
|
||||
else:
|
||||
ws.cell(row=row, column=2).value = "未配置"
|
||||
|
||||
# 3. 公差要求
|
||||
if is_task19:
|
||||
# 任务19的公差从数据中获取
|
||||
tolerance_val = result.data.get('tolerance', 2.0)
|
||||
tolerance_str = f"±{tolerance_val}"
|
||||
elif is_comparison_task:
|
||||
# 点对差值比较任务的公差
|
||||
comparison = result.data.get('comparison', {})
|
||||
tolerance_val = comparison.get('tolerance', 0)
|
||||
tolerance_str = f"+{tolerance_val}/-{tolerance_val}"
|
||||
else:
|
||||
tolerance = task_config.get('tolerance', None)
|
||||
tolerance_str = "未配置"
|
||||
|
||||
if tolerance is not None:
|
||||
if isinstance(tolerance, dict):
|
||||
# 非对称公差
|
||||
tolerance_upper = tolerance.get('upper', 0)
|
||||
tolerance_lower = tolerance.get('lower', 0)
|
||||
tolerance_str = f"+{tolerance_upper}/{tolerance_lower}"
|
||||
else:
|
||||
# 对称公差
|
||||
tolerance_str = f"±{tolerance}"
|
||||
|
||||
ws.cell(row=row, column=3).value = tolerance_str
|
||||
|
||||
# 4. 测量结果(均值)
|
||||
# 5. 精度(标准差)
|
||||
# 6. 误差范围
|
||||
# 7. 测量结论
|
||||
if result.success and result.data:
|
||||
data = result.data
|
||||
|
||||
# 检查是否是宽度差值检查任务(任务19)
|
||||
if 'diff_upper_middle' in data:
|
||||
# 任务19:宽度差值检查
|
||||
max_diff = data.get('max_diff', 0)
|
||||
tolerance = data.get('tolerance', 2.0)
|
||||
pass_check = data.get('pass', False)
|
||||
execution_time = data.get('execution_time', 0)
|
||||
|
||||
# 测量结果:最大差值
|
||||
ws.cell(row=row, column=4).value = round(max_diff, 4)
|
||||
|
||||
# 精度:显示三个差值
|
||||
diff_str = f"上-中:{data.get('diff_upper_middle', 0):.4f}, 中-下:{data.get('diff_middle_lower', 0):.4f}, 上-下:{data.get('diff_upper_lower', 0):.4f}"
|
||||
ws.cell(row=row, column=5).value = diff_str
|
||||
|
||||
# 误差范围:显示三个差值的最小和最大值
|
||||
all_diffs = [data.get('diff_upper_middle', 0), data.get('diff_middle_lower', 0), data.get('diff_upper_lower', 0)]
|
||||
min_diff = min(all_diffs)
|
||||
max_diff_val = max(all_diffs)
|
||||
diff_range_str = f"[{min_diff:.4f}, {max_diff_val:.4f}]"
|
||||
ws.cell(row=row, column=6).value = diff_range_str
|
||||
|
||||
# 测量结论
|
||||
conclusion_cell = ws.cell(row=row, column=7)
|
||||
if pass_check:
|
||||
conclusion_cell.value = "通过"
|
||||
conclusion_cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
||||
conclusion_cell.font = Font(color="006100", bold=True)
|
||||
else:
|
||||
conclusion_cell.value = "不通过"
|
||||
conclusion_cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
|
||||
conclusion_cell.font = Font(color="9C0006", bold=True)
|
||||
|
||||
# 执行时间
|
||||
ws.cell(row=row, column=8).value = round(execution_time, 2) if execution_time else 0
|
||||
|
||||
elif 'comparison' in data:
|
||||
# 点对差值比较任务
|
||||
comparison = data.get('comparison', {})
|
||||
difference = comparison.get('difference', 0)
|
||||
tolerance = comparison.get('tolerance', 0)
|
||||
passed = comparison.get('passed', False)
|
||||
execution_time = data.get('execution_time', 0)
|
||||
|
||||
pair1_name = comparison.get('pair1_name', '点对1')
|
||||
pair2_name = comparison.get('pair2_name', '点对2')
|
||||
distance1 = comparison.get('pair1_distance', 0)
|
||||
distance2 = comparison.get('pair2_distance', 0)
|
||||
|
||||
# 测量结果:差值
|
||||
ws.cell(row=row, column=4).value = round(difference, 4)
|
||||
|
||||
# 精度:显示两个距离值之差(简洁格式)
|
||||
ws.cell(row=row, column=5).value = round(abs(distance1 - distance2), 4)
|
||||
|
||||
# 误差范围:只显示数字
|
||||
ws.cell(row=row, column=6).value = round(difference, 4)
|
||||
|
||||
# 测量结论
|
||||
conclusion_cell = ws.cell(row=row, column=7)
|
||||
if passed:
|
||||
conclusion_cell.value = "通过"
|
||||
conclusion_cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
||||
conclusion_cell.font = Font(color="006100", bold=True)
|
||||
else:
|
||||
conclusion_cell.value = "不通过"
|
||||
conclusion_cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
|
||||
conclusion_cell.font = Font(color="9C0006", bold=True)
|
||||
|
||||
# 执行时间
|
||||
ws.cell(row=row, column=8).value = round(execution_time, 2) if execution_time else 0
|
||||
|
||||
else:
|
||||
# 普通测量任务
|
||||
mean_distance = data.get('mean_distance', 0)
|
||||
std_distance = data.get('std_distance', 0)
|
||||
min_distance = data.get('min_distance', 0)
|
||||
max_distance = data.get('max_distance', 0)
|
||||
execution_time = data.get('execution_time', 0)
|
||||
|
||||
# 测量结果
|
||||
ws.cell(row=row, column=4).value = round(mean_distance, 2)
|
||||
|
||||
# 精度(标准差)
|
||||
ws.cell(row=row, column=5).value = round(std_distance, 2)
|
||||
|
||||
# 误差范围(相对于理论值)
|
||||
if theoretical_dim is not None:
|
||||
error_min = min_distance - theoretical_dim
|
||||
error_max = max_distance - theoretical_dim
|
||||
error_range_str = f"[{error_min:.2f}, {error_max:.2f}]"
|
||||
else:
|
||||
error_range_str = "无理论值"
|
||||
ws.cell(row=row, column=6).value = error_range_str
|
||||
|
||||
# 测量结论
|
||||
conclusion = "未配置"
|
||||
conclusion_cell = ws.cell(row=row, column=7)
|
||||
|
||||
if theoretical_dim is not None:
|
||||
# 判断是否通过:误差范围必须在 [-0.11, +0.1] mm 内
|
||||
error_min = min_distance - theoretical_dim
|
||||
error_max = max_distance - theoretical_dim
|
||||
|
||||
if error_min >= -0.11 and error_max <= 0.1:
|
||||
conclusion = "通过"
|
||||
conclusion_cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
||||
conclusion_cell.font = Font(color="006100", bold=True)
|
||||
else:
|
||||
conclusion = "不通过"
|
||||
conclusion_cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
|
||||
conclusion_cell.font = Font(color="9C0006", bold=True)
|
||||
|
||||
conclusion_cell.value = conclusion
|
||||
|
||||
# 8. 执行时间
|
||||
ws.cell(row=row, column=8).value = round(execution_time, 2) if execution_time else 0
|
||||
else:
|
||||
# 失败时显示错误信息
|
||||
ws.cell(row=row, column=9).value = result.error_message
|
||||
|
||||
# 标记为失败
|
||||
conclusion_cell = ws.cell(row=row, column=7)
|
||||
conclusion_cell.value = "失败"
|
||||
conclusion_cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid")
|
||||
conclusion_cell.font = Font(color="9C0006", bold=True)
|
||||
|
||||
# 应用边框和对齐
|
||||
for col in range(1, 10):
|
||||
cell = ws.cell(row=row, column=col)
|
||||
cell.border = border
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
return row + 1
|
||||
|
||||
def _adjust_column_widths(self, ws):
|
||||
"""自动调整列宽"""
|
||||
column_widths = {
|
||||
1: 30, # 项点名称
|
||||
2: 15, # 理论尺寸
|
||||
3: 20, # 公差要求
|
||||
4: 15, # 测量结果
|
||||
5: 12, # 精度
|
||||
6: 20, # 误差范围
|
||||
7: 12, # 测量结论
|
||||
8: 15, # 执行时间
|
||||
9: 40 # 错误信息
|
||||
}
|
||||
|
||||
for col, width in column_widths.items():
|
||||
ws.column_dimensions[get_column_letter(col)].width = width
|
||||
139
alg/train_measurement_system_v3.2_0629/src/hole_center_fitter.py
Normal file
139
alg/train_measurement_system_v3.2_0629/src/hole_center_fitter.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
圆孔中心拟合工具 - 使用V2高精度算法
|
||||
适配器层:保持原有API,底层使用hole_detector
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
# 导入V2版本的高精度检测器
|
||||
from hole_detector import HoleDetectorV2
|
||||
|
||||
|
||||
class HoleCenterFitter:
|
||||
"""圆孔中心拟合器(适配器)"""
|
||||
|
||||
@staticmethod
|
||||
def fit_hole_center(points: np.ndarray, axis_idx=None) -> dict:
|
||||
"""
|
||||
拟合圆柱孔的中心位置(使用V2算法)
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
axis_idx: 圆柱轴向索引 (0=X, 1=Y, 2=Z),None则自动检测
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'center_2d': [x, y], # 投影平面上的圆心(缺少轴向坐标)
|
||||
'center_3d': [x, y, z], # 3D圆心坐标(轴向坐标为该轴的中点)
|
||||
'radius': float, # 半径
|
||||
'axis_idx': int, # 轴向索引
|
||||
'axis_name': str, # 轴名称 (X/Y/Z)
|
||||
'other_axes_indices': [int, int], # 非轴向的两个轴索引
|
||||
'stats': dict # 统计信息(兼容性字段)
|
||||
}
|
||||
"""
|
||||
# 转换axis_idx到轴名称
|
||||
axis_name = None
|
||||
if axis_idx is not None:
|
||||
axis_names = ['X', 'Y', 'Z']
|
||||
axis_name = axis_names[axis_idx]
|
||||
|
||||
# 使用V2检测器
|
||||
detector = HoleDetectorV2(axis=axis_name)
|
||||
result_v2 = detector.detect(points)
|
||||
|
||||
# 转换结果格式以兼容旧API
|
||||
center_3d = np.array(result_v2['center'])
|
||||
axis_name_v2 = result_v2['axis']
|
||||
axis_idx_v2 = {'X': 0, 'Y': 1, 'Z': 2}[axis_name_v2]
|
||||
|
||||
# 计算其他轴的索引
|
||||
other_axes = [0, 1, 2]
|
||||
other_axes.remove(axis_idx_v2)
|
||||
|
||||
# 提取2D圆心(投影平面上的坐标)
|
||||
center_2d = center_3d[other_axes]
|
||||
|
||||
return {
|
||||
'center_2d': center_2d,
|
||||
'center_3d': center_3d,
|
||||
'radius': result_v2['radius'],
|
||||
'axis_idx': axis_idx_v2,
|
||||
'axis_name': axis_name_v2,
|
||||
'other_axes_indices': other_axes,
|
||||
'stats': {
|
||||
'inside_count': 0,
|
||||
'ring_count': 0,
|
||||
'outside_count': 0,
|
||||
'inside_ratio': 0.0,
|
||||
'ring_ratio': 0.0,
|
||||
'total_points': len(points)
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fit_4_holes_reference_plane(hole_points_list: list) -> dict:
|
||||
"""
|
||||
使用4个孔的圆心拟合参考平面(垂直于Y轴)
|
||||
|
||||
Args:
|
||||
hole_points_list: 4个孔的点云列表,每个元素是一个点云数组 (N, 3)
|
||||
顺序:[左上孔, 右上孔, 左下孔, 右下孔]
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'plane_y': float, # 参考平面的Y坐标(垂直于Y轴的平面方程:y = plane_y)
|
||||
'centers_3d': list, # 4个圆心的3D坐标
|
||||
'centers_2d': list, # 4个圆心的2D坐标(投影平面)
|
||||
'center_point': [x, y, z], # 对角线交点(4个圆心的中心)
|
||||
'radii': list, # 4个孔的半径
|
||||
}
|
||||
"""
|
||||
if len(hole_points_list) != 4:
|
||||
raise ValueError(f"需要4个孔的点云,但收到了{len(hole_points_list)}个")
|
||||
|
||||
centers_3d = []
|
||||
centers_2d = []
|
||||
radii = []
|
||||
|
||||
print(f" 正在拟合4个孔的圆心...")
|
||||
|
||||
for i, points in enumerate(hole_points_list):
|
||||
result = HoleCenterFitter.fit_hole_center(points)
|
||||
centers_3d.append(result['center_3d'])
|
||||
centers_2d.append(result['center_2d'])
|
||||
radii.append(result['radius'])
|
||||
print(f" 孔{i+1}: 圆心3D={result['center_3d']}, 半径={result['radius']:.2f}")
|
||||
|
||||
# 计算4个圆心的中心点(对角线交点)
|
||||
centers_array = np.array(centers_3d)
|
||||
center_point = np.mean(centers_array, axis=0)
|
||||
|
||||
# 取Y坐标作为参考平面的位置
|
||||
plane_y = center_point[1]
|
||||
|
||||
print(f" 4孔中心点: {center_point}")
|
||||
print(f" 参考平面: y = {plane_y:.3f}")
|
||||
|
||||
return {
|
||||
'plane_y': float(plane_y),
|
||||
'centers_3d': [c.tolist() if isinstance(c, np.ndarray) else c for c in centers_3d],
|
||||
'centers_2d': [c.tolist() if isinstance(c, np.ndarray) else c for c in centers_2d],
|
||||
'center_point': center_point.tolist(),
|
||||
'radii': radii
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def compute_distance_to_y_plane(point_3d, plane_y):
|
||||
"""
|
||||
计算点到垂直于Y轴的平面的距离
|
||||
|
||||
Args:
|
||||
point_3d: 3D点坐标 [x, y, z]
|
||||
plane_y: 平面的Y坐标
|
||||
|
||||
Returns:
|
||||
float: 距离(带符号)
|
||||
"""
|
||||
if isinstance(point_3d, np.ndarray):
|
||||
point_3d = point_3d.tolist()
|
||||
return abs(point_3d[1] - plane_y)
|
||||
288
alg/train_measurement_system_v3.2_0629/src/hole_detector.py
Normal file
288
alg/train_measurement_system_v3.2_0629/src/hole_detector.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
高精度孔检测算法 v2.0
|
||||
输入:点云文件路径或点云数组
|
||||
输出:圆心3D坐标和半径
|
||||
|
||||
精度:圆心误差 < 0.1mm
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
from scipy.optimize import least_squares
|
||||
|
||||
|
||||
class HoleDetectorV2:
|
||||
"""
|
||||
高精度圆柱孔检测器
|
||||
|
||||
特点:
|
||||
- 自动检测圆柱轴向
|
||||
- 分层精化拟合
|
||||
- 圆心精度 < 0.1mm
|
||||
"""
|
||||
|
||||
def __init__(self, axis=None):
|
||||
"""
|
||||
初始化检测器
|
||||
|
||||
Args:
|
||||
axis: 圆柱轴向 ('X', 'Y', 'Z'),None则自动检测
|
||||
"""
|
||||
self.axis = axis
|
||||
self._axis_idx = None
|
||||
|
||||
def detect(self, input_data):
|
||||
"""
|
||||
检测孔的圆心和半径
|
||||
|
||||
Args:
|
||||
input_data: 点云文件路径(str)或点云数组(Nx3 numpy array)
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'center': [x, y, z], # 3D圆心坐标
|
||||
'radius': float, # 半径
|
||||
'axis': str # 轴向 ('X', 'Y', 'Z')
|
||||
}
|
||||
"""
|
||||
# 加载点云
|
||||
if isinstance(input_data, str):
|
||||
points = self._load_from_file(input_data)
|
||||
else:
|
||||
points = np.asarray(input_data)
|
||||
|
||||
# 检测轴向
|
||||
if self.axis is None:
|
||||
self._axis_idx = self._auto_detect_axis(points)
|
||||
else:
|
||||
self._axis_idx = {'X': 0, 'Y': 1, 'Z': 2}[self.axis.upper()]
|
||||
|
||||
# 投影到垂直平面
|
||||
other_axes = [0, 1, 2]
|
||||
other_axes.remove(self._axis_idx)
|
||||
proj_2d = points[:, other_axes]
|
||||
|
||||
# 拟合圆(多次RANSAC + 精化)
|
||||
center_2d, radius = self._fit_circle_robust(proj_2d)
|
||||
|
||||
# 构建3D圆心
|
||||
axis_center = (points[:, self._axis_idx].min() +
|
||||
points[:, self._axis_idx].max()) / 2
|
||||
|
||||
center_3d = np.zeros(3)
|
||||
center_3d[other_axes[0]] = center_2d[0]
|
||||
center_3d[other_axes[1]] = center_2d[1]
|
||||
center_3d[self._axis_idx] = axis_center
|
||||
|
||||
axis_names = ['X', 'Y', 'Z']
|
||||
|
||||
return {
|
||||
'center': center_3d.tolist(),
|
||||
'radius': float(radius),
|
||||
'axis': axis_names[self._axis_idx]
|
||||
}
|
||||
|
||||
def _load_from_file(self, ply_path):
|
||||
"""从PLY文件加载点云"""
|
||||
pcd = o3d.io.read_point_cloud(str(ply_path))
|
||||
return np.asarray(pcd.points)
|
||||
|
||||
def _auto_detect_axis(self, points):
|
||||
"""自动检测圆柱轴向"""
|
||||
coef_vars = []
|
||||
|
||||
for i in range(3):
|
||||
other_axes = [0, 1, 2]
|
||||
other_axes.remove(i)
|
||||
proj = points[:, other_axes]
|
||||
|
||||
center = proj.mean(axis=0)
|
||||
distances = np.sqrt(((proj - center) ** 2).sum(axis=1))
|
||||
|
||||
# 变异系数:圆环小,圆盘大
|
||||
cv = distances.std() / distances.mean() if distances.mean() > 0 else float('inf')
|
||||
coef_vars.append(cv)
|
||||
|
||||
return np.argmin(coef_vars)
|
||||
|
||||
def _fit_circle_algebraic(self, points_2d):
|
||||
"""代数拟合(快速初始估计)"""
|
||||
n = len(points_2d)
|
||||
x = points_2d[:, 0]
|
||||
y = points_2d[:, 1]
|
||||
|
||||
A = np.column_stack([x, y, np.ones(n)])
|
||||
b = -(x**2 + y**2)
|
||||
params, _, _, _ = np.linalg.lstsq(A, b, rcond=None)
|
||||
a, b_param, c = params
|
||||
|
||||
cx = -a / 2
|
||||
cy = -b_param / 2
|
||||
r = np.sqrt(cx**2 + cy**2 - c)
|
||||
|
||||
return np.array([cx, cy]), r
|
||||
|
||||
def _fit_circle_geometric(self, points_2d, initial_guess=None):
|
||||
"""几何拟合(最小化几何距离)"""
|
||||
if initial_guess is None:
|
||||
center, radius = self._fit_circle_algebraic(points_2d)
|
||||
else:
|
||||
center, radius = initial_guess
|
||||
|
||||
x = points_2d[:, 0]
|
||||
y = points_2d[:, 1]
|
||||
|
||||
def residual_func(params):
|
||||
cx, cy, r = params
|
||||
distances = np.sqrt((x - cx)**2 + (y - cy)**2)
|
||||
return distances - r
|
||||
|
||||
result = least_squares(residual_func, [center[0], center[1], radius],
|
||||
loss='soft_l1', f_scale=1.0)
|
||||
cx_opt, cy_opt, r_opt = result.x
|
||||
|
||||
return np.array([cx_opt, cy_opt]), r_opt
|
||||
|
||||
def _fit_circle_ransac(self, points_2d, threshold=None, max_iterations=1000):
|
||||
"""RANSAC圆拟合"""
|
||||
n = len(points_2d)
|
||||
|
||||
if threshold is None:
|
||||
center_approx = points_2d.mean(axis=0)
|
||||
distances = np.sqrt(((points_2d - center_approx)**2).sum(axis=1))
|
||||
threshold = max(0.5, np.median(distances) * 0.1)
|
||||
|
||||
best_inliers = None
|
||||
best_center = None
|
||||
best_radius = None
|
||||
best_n_inliers = 0
|
||||
|
||||
for _ in range(max_iterations):
|
||||
if n < 3:
|
||||
break
|
||||
|
||||
indices = np.random.choice(n, 3, replace=False)
|
||||
sample = points_2d[indices]
|
||||
|
||||
try:
|
||||
center, radius = self._fit_circle_algebraic(sample)
|
||||
except:
|
||||
continue
|
||||
|
||||
distances = np.sqrt(((points_2d - center)**2).sum(axis=1))
|
||||
residuals = np.abs(distances - radius)
|
||||
inliers = residuals < threshold
|
||||
n_inliers = np.sum(inliers)
|
||||
|
||||
if n_inliers > best_n_inliers:
|
||||
best_n_inliers = n_inliers
|
||||
best_inliers = inliers
|
||||
best_center = center
|
||||
best_radius = radius
|
||||
|
||||
if best_inliers is not None and np.sum(best_inliers) >= 3:
|
||||
inlier_points = points_2d[best_inliers]
|
||||
best_center, best_radius = self._fit_circle_geometric(inlier_points)
|
||||
|
||||
distances = np.sqrt(((points_2d - best_center)**2).sum(axis=1))
|
||||
residuals = np.abs(distances - best_radius)
|
||||
best_inliers = residuals < threshold
|
||||
else:
|
||||
best_center, best_radius = self._fit_circle_geometric(points_2d)
|
||||
distances = np.sqrt(((points_2d - best_center)**2).sum(axis=1))
|
||||
residuals = np.abs(distances - best_radius)
|
||||
best_inliers = residuals < threshold
|
||||
|
||||
return best_center, best_radius, best_inliers
|
||||
|
||||
def _fit_circle_robust(self, points_2d, n_runs=5, refine_threshold=1.0):
|
||||
"""
|
||||
鲁棒圆拟合(分层精化)
|
||||
|
||||
Args:
|
||||
points_2d: Nx2点云
|
||||
n_runs: RANSAC运行次数
|
||||
refine_threshold: 高质量点阈值(mm)
|
||||
|
||||
Returns:
|
||||
center: [x, y]
|
||||
radius: float
|
||||
"""
|
||||
# 步骤1: 多次RANSAC粗拟合
|
||||
centers_list = []
|
||||
radii_list = []
|
||||
|
||||
for _ in range(n_runs):
|
||||
center, radius, _ = self._fit_circle_ransac(points_2d, max_iterations=1000)
|
||||
centers_list.append(center)
|
||||
radii_list.append(radius)
|
||||
|
||||
centers_array = np.array(centers_list)
|
||||
radii_array = np.array(radii_list)
|
||||
|
||||
center_rough = np.median(centers_array, axis=0)
|
||||
radius_rough = np.median(radii_array)
|
||||
|
||||
# 步骤2: 筛选高质量点
|
||||
distances = np.sqrt((points_2d[:, 0] - center_rough[0])**2 +
|
||||
(points_2d[:, 1] - center_rough[1])**2)
|
||||
deviations = np.abs(distances - radius_rough)
|
||||
|
||||
high_quality_mask = deviations < refine_threshold
|
||||
high_quality_points = points_2d[high_quality_mask]
|
||||
|
||||
if len(high_quality_points) < 10:
|
||||
return center_rough, radius_rough
|
||||
|
||||
# 步骤3: 几何拟合精化
|
||||
centers_fine_list = []
|
||||
radii_fine_list = []
|
||||
|
||||
for _ in range(3):
|
||||
center_f, radius_f = self._fit_circle_geometric(
|
||||
high_quality_points,
|
||||
initial_guess=(center_rough, radius_rough)
|
||||
)
|
||||
centers_fine_list.append(center_f)
|
||||
radii_fine_list.append(radius_f)
|
||||
|
||||
centers_fine_array = np.array(centers_fine_list)
|
||||
radii_fine_array = np.array(radii_fine_list)
|
||||
|
||||
center_final = np.median(centers_fine_array, axis=0)
|
||||
radius_final = np.median(radii_fine_array)
|
||||
|
||||
return center_final, radius_final
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def detect_hole(ply_path, axis=None):
|
||||
"""
|
||||
检测孔的圆心和半径(便捷函数)
|
||||
|
||||
Args:
|
||||
ply_path: 点云文件路径或点云数组
|
||||
axis: 圆柱轴向 ('X', 'Y', 'Z'),None则自动检测
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'center': [x, y, z],
|
||||
'radius': float,
|
||||
'axis': str
|
||||
}
|
||||
|
||||
Example:
|
||||
result = detect_hole('hole.ply')
|
||||
print(f"圆心: {result['center']}")
|
||||
print(f"半径: {result['radius']}")
|
||||
"""
|
||||
detector = HoleDetectorV2(axis=axis)
|
||||
return detector.detect(ply_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例用法
|
||||
result = detect_hole("test.ply")
|
||||
print(f"圆心: {result['center']}")
|
||||
print(f"半径: {result['radius']:.3f}")
|
||||
print(f"轴向: {result['axis']}")
|
||||
169
alg/train_measurement_system_v3.2_0629/src/line_fitter.py
Normal file
169
alg/train_measurement_system_v3.2_0629/src/line_fitter.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""直线拟合模块 - 使用RANSAC算法拟合3D直线"""
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class LineModel:
|
||||
"""直线模型"""
|
||||
point: np.ndarray # 直线上的点 (3,)
|
||||
direction: np.ndarray # 方向向量(单位向量) (3,)
|
||||
inliers: np.ndarray # 内点索引
|
||||
inlier_ratio: float # 内点比例
|
||||
|
||||
def distance_to_point(self, point: np.ndarray) -> float:
|
||||
"""计算点到直线的距离"""
|
||||
vector = point - self.point
|
||||
projection = np.dot(vector, self.direction) * self.direction
|
||||
perpendicular = vector - projection
|
||||
return np.linalg.norm(perpendicular)
|
||||
|
||||
def distance_to_points(self, points: np.ndarray) -> np.ndarray:
|
||||
"""计算多个点到直线的距离"""
|
||||
vectors = points - self.point
|
||||
projections = np.dot(vectors, self.direction)[:, np.newaxis] * self.direction
|
||||
perpendicular_vectors = vectors - projections
|
||||
distances = np.linalg.norm(perpendicular_vectors, axis=1)
|
||||
return distances
|
||||
|
||||
def sample_points_on_line(self, n_samples: int, point_cloud: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
从拟合的直线对应的真实点云中采样点
|
||||
|
||||
Args:
|
||||
n_samples: 采样点数
|
||||
point_cloud: 用于拟合的原始点云(内点)
|
||||
|
||||
Returns:
|
||||
采样的点 (n_samples, 3)
|
||||
"""
|
||||
if len(point_cloud) == 0:
|
||||
return np.array([]).reshape(0, 3)
|
||||
|
||||
n_samples = min(n_samples, len(point_cloud))
|
||||
indices = np.random.choice(len(point_cloud), size=n_samples, replace=False)
|
||||
return point_cloud[indices]
|
||||
|
||||
|
||||
class LineFitter:
|
||||
"""RANSAC直线拟合器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
threshold: float = 2.0,
|
||||
max_iterations: int = 1000,
|
||||
min_inlier_ratio: float = 0.6
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
threshold: 内点距离阈值(mm)
|
||||
max_iterations: 最大迭代次数
|
||||
min_inlier_ratio: 最小内点比例
|
||||
"""
|
||||
self.threshold = threshold
|
||||
self.max_iterations = max_iterations
|
||||
self.min_inlier_ratio = min_inlier_ratio
|
||||
|
||||
def fit(self, points: np.ndarray) -> Optional[LineModel]:
|
||||
"""
|
||||
使用RANSAC拟合3D直线
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
|
||||
Returns:
|
||||
LineModel或None(拟合失败)
|
||||
"""
|
||||
if len(points) < 2:
|
||||
return None
|
||||
|
||||
best_inliers = None
|
||||
best_point = None
|
||||
best_direction = None
|
||||
best_inlier_count = 0
|
||||
|
||||
for _ in range(self.max_iterations):
|
||||
# 随机选择2个点
|
||||
sample_indices = np.random.choice(len(points), 2, replace=False)
|
||||
p1, p2 = points[sample_indices]
|
||||
|
||||
# 计算直线方向
|
||||
direction = p2 - p1
|
||||
if np.linalg.norm(direction) < 1e-6:
|
||||
continue
|
||||
direction = direction / np.linalg.norm(direction)
|
||||
|
||||
# 计算所有点到直线的距离
|
||||
vectors = points - p1
|
||||
projections = np.dot(vectors, direction)[:, np.newaxis] * direction
|
||||
distances = np.linalg.norm(vectors - projections, axis=1)
|
||||
|
||||
# 找到内点
|
||||
inliers = np.where(distances < self.threshold)[0]
|
||||
|
||||
if len(inliers) > best_inlier_count:
|
||||
best_inlier_count = len(inliers)
|
||||
best_inliers = inliers
|
||||
best_point = p1
|
||||
best_direction = direction
|
||||
|
||||
if best_inliers is None or len(best_inliers) < 2:
|
||||
return None
|
||||
|
||||
# 使用所有内点重新拟合直线(最小二乘)
|
||||
inlier_points = points[best_inliers]
|
||||
final_point, final_direction = self._fit_line_least_squares(inlier_points)
|
||||
|
||||
if final_direction is None:
|
||||
return None
|
||||
|
||||
# 重新计算内点
|
||||
vectors = points - final_point
|
||||
projections = np.dot(vectors, final_direction)[:, np.newaxis] * final_direction
|
||||
distances = np.linalg.norm(vectors - projections, axis=1)
|
||||
final_inliers = np.where(distances < self.threshold)[0]
|
||||
|
||||
inlier_ratio = len(final_inliers) / len(points)
|
||||
|
||||
# 检查内点比例
|
||||
if inlier_ratio < self.min_inlier_ratio:
|
||||
return None
|
||||
|
||||
return LineModel(
|
||||
point=final_point,
|
||||
direction=final_direction,
|
||||
inliers=final_inliers,
|
||||
inlier_ratio=inlier_ratio
|
||||
)
|
||||
|
||||
def _fit_line_least_squares(self, points: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
使用最小二乘法拟合直线
|
||||
|
||||
Returns:
|
||||
(point, direction): 直线上的点和方向向量
|
||||
"""
|
||||
if len(points) < 2:
|
||||
return None, None
|
||||
|
||||
# 计算质心
|
||||
centroid = np.mean(points, axis=0)
|
||||
|
||||
# 中心化点云
|
||||
centered = points - centroid
|
||||
|
||||
# PCA:找主方向
|
||||
try:
|
||||
# 协方差矩阵
|
||||
cov = np.dot(centered.T, centered) / len(points)
|
||||
# 特征值分解
|
||||
eigenvalues, eigenvectors = np.linalg.eig(cov)
|
||||
# 最大特征值对应的特征向量就是直线方向
|
||||
max_idx = np.argmax(eigenvalues)
|
||||
direction = eigenvectors[:, max_idx].real
|
||||
direction = direction / np.linalg.norm(direction)
|
||||
|
||||
return centroid, direction
|
||||
except np.linalg.LinAlgError:
|
||||
return None, None
|
||||
192
alg/train_measurement_system_v3.2_0629/src/measurement_logger.py
Normal file
192
alg/train_measurement_system_v3.2_0629/src/measurement_logger.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""测量详细日志记录器 - 记录测量过程的所有详细信息"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class MeasurementLogger:
|
||||
"""测量过程详细日志记录器"""
|
||||
|
||||
def __init__(self, output_dir: str = "results"):
|
||||
"""
|
||||
初始化测量日志记录器
|
||||
|
||||
Args:
|
||||
output_dir: 输出目录路径
|
||||
"""
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成日志文件名
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
self.log_file = self.output_dir / f"measurement_detail_{timestamp}.log"
|
||||
|
||||
# 打开日志文件
|
||||
self.file_handle = open(self.log_file, 'w', encoding='utf-8')
|
||||
|
||||
# 写入头部
|
||||
self._write_header()
|
||||
|
||||
def _write_header(self):
|
||||
"""写入日志头部"""
|
||||
self.log("=" * 80)
|
||||
self.log("点云测量系统 - 详细测量日志")
|
||||
self.log("=" * 80)
|
||||
self.log(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
self.log("=" * 80)
|
||||
self.log("")
|
||||
|
||||
def log(self, message: str, also_print: bool = True):
|
||||
"""
|
||||
记录日志信息
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
also_print: 是否同时输出到控制台
|
||||
"""
|
||||
# 写入文件
|
||||
self.file_handle.write(message + "\n")
|
||||
self.file_handle.flush() # 立即刷新到文件
|
||||
|
||||
# 输出到控制台
|
||||
if also_print:
|
||||
print(message)
|
||||
|
||||
def log_task_start(self, task_index: int, total_tasks: int, task_name: str):
|
||||
"""记录任务开始"""
|
||||
self.log("")
|
||||
self.log("=" * 80)
|
||||
self.log(f"[{task_index}/{total_tasks}] 开始执行任务: {task_name}")
|
||||
self.log("=" * 80)
|
||||
|
||||
def log_roi_extraction(self, roi_name: str, point_count: int, bbox_min: list, bbox_max: list):
|
||||
"""
|
||||
记录ROI提取信息
|
||||
|
||||
Args:
|
||||
roi_name: ROI名称(如"源平面ROI"、"目标平面ROI")
|
||||
point_count: 提取到的点数
|
||||
bbox_min: ROI最小边界 [x, y, z]
|
||||
bbox_max: ROI最大边界 [x, y, z]
|
||||
"""
|
||||
self.log(f" {roi_name}:")
|
||||
self.log(f" 边界: X[{bbox_min[0]:.2f}, {bbox_max[0]:.2f}], "
|
||||
f"Y[{bbox_min[1]:.2f}, {bbox_max[1]:.2f}], "
|
||||
f"Z[{bbox_min[2]:.2f}, {bbox_max[2]:.2f}]")
|
||||
self.log(f" 提取点数: {point_count:,} 个点")
|
||||
|
||||
def log_ransac_fitting(self, plane_name: str, total_points: int,
|
||||
inlier_count: int, inlier_ratio: float,
|
||||
plane_equation: Optional[tuple] = None):
|
||||
"""
|
||||
记录RANSAC平面拟合信息
|
||||
|
||||
Args:
|
||||
plane_name: 平面名称(如"源平面"、"目标平面")
|
||||
total_points: 参与拟合的总点数
|
||||
inlier_count: 内点数量
|
||||
inlier_ratio: 内点率(0-1之间的浮点数)
|
||||
plane_equation: 平面方程 (a, b, c, d),满足 ax + by + cz + d = 0
|
||||
"""
|
||||
self.log(f" {plane_name} RANSAC拟合:")
|
||||
self.log(f" 拟合点数: {total_points:,} 个点")
|
||||
self.log(f" 内点数量: {inlier_count:,} 个点")
|
||||
self.log(f" 内点率: {inlier_ratio:.2%}")
|
||||
|
||||
if plane_equation:
|
||||
a, b, c, d = plane_equation
|
||||
self.log(f" 平面方程: {a:.6f}x + {b:.6f}y + {c:.6f}z + {d:.6f} = 0")
|
||||
# 计算法向量
|
||||
import numpy as np
|
||||
normal = np.array([a, b, c])
|
||||
norm = np.linalg.norm(normal)
|
||||
if norm > 0:
|
||||
normal = normal / norm
|
||||
self.log(f" 法向量: ({normal[0]:.6f}, {normal[1]:.6f}, {normal[2]:.6f})")
|
||||
|
||||
def log_sampling(self, sample_count: int, method: str = "均匀采样"):
|
||||
"""
|
||||
记录采样信息
|
||||
|
||||
Args:
|
||||
sample_count: 采样点数
|
||||
method: 采样方法
|
||||
"""
|
||||
self.log(f" 采样策略: {method}")
|
||||
self.log(f" 采样点数: {sample_count} 个点")
|
||||
|
||||
def log_distance_result(self, mean_distance: float, std_distance: float,
|
||||
min_distance: float, max_distance: float):
|
||||
"""
|
||||
记录距离测量结果
|
||||
|
||||
Args:
|
||||
mean_distance: 平均距离
|
||||
std_distance: 标准差
|
||||
min_distance: 最小距离
|
||||
max_distance: 最大距离
|
||||
"""
|
||||
self.log(f" 测量结果:")
|
||||
self.log(f" 平均距离: {mean_distance:.4f} mm")
|
||||
self.log(f" 标准差: {std_distance:.4f} mm")
|
||||
self.log(f" 最小距离: {min_distance:.4f} mm")
|
||||
self.log(f" 最大距离: {max_distance:.4f} mm")
|
||||
self.log(f" 距离范围: [{min_distance:.4f}, {max_distance:.4f}] mm")
|
||||
|
||||
def log_task_success(self, execution_time: float):
|
||||
"""记录任务成功"""
|
||||
self.log(f" 任务状态: ✅ 成功")
|
||||
self.log(f" 执行时间: {execution_time:.2f} 秒")
|
||||
|
||||
def log_task_failure(self, error_message: str, execution_time: float):
|
||||
"""记录任务失败"""
|
||||
self.log(f" 任务状态: ❌ 失败")
|
||||
self.log(f" 错误信息: {error_message}")
|
||||
self.log(f" 执行时间: {execution_time:.2f} 秒")
|
||||
|
||||
def log_circle_fitting(self, circle_name: str, center: tuple, radius: float,
|
||||
point_count: int):
|
||||
"""
|
||||
记录圆拟合信息
|
||||
|
||||
Args:
|
||||
circle_name: 圆的名称
|
||||
center: 圆心坐标 (x, y, z)
|
||||
radius: 半径
|
||||
point_count: 拟合点数
|
||||
"""
|
||||
self.log(f" {circle_name}:")
|
||||
self.log(f" 圆心: ({center[0]:.4f}, {center[1]:.4f}, {center[2]:.4f})")
|
||||
self.log(f" 半径: {radius:.4f} mm")
|
||||
self.log(f" 拟合点数: {point_count:,} 个点")
|
||||
|
||||
def log_kdtree_optimization(self, original_points: int, filtered_points: int,
|
||||
percentage: float):
|
||||
"""
|
||||
记录KDTree优化信息
|
||||
|
||||
Args:
|
||||
original_points: 原始点数
|
||||
filtered_points: 筛选后点数
|
||||
percentage: 保留百分比
|
||||
"""
|
||||
self.log(f" KDTree优化:")
|
||||
self.log(f" 原始点数: {original_points:,} 个点")
|
||||
self.log(f" 筛选后: {filtered_points:,} 个点 ({percentage:.2f}%)")
|
||||
|
||||
def close(self):
|
||||
"""关闭日志文件"""
|
||||
self.log("")
|
||||
self.log("=" * 80)
|
||||
self.log(f"结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
self.log("=" * 80)
|
||||
|
||||
if self.file_handle:
|
||||
self.file_handle.close()
|
||||
print(f"\n详细测量日志已保存到: {self.log_file}")
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数,确保文件被关闭"""
|
||||
if hasattr(self, 'file_handle') and self.file_handle:
|
||||
self.file_handle.close()
|
||||
@@ -0,0 +1,7 @@
|
||||
"""measurements包初始化"""
|
||||
from .base import BaseMeasurement, MeasurementResult
|
||||
from .plane_distance import PlaneToPlaneDistance
|
||||
from .point_to_plane import PointToPlaneDistance
|
||||
from .point_to_point import PointToPointDistance
|
||||
|
||||
__all__ = ['BaseMeasurement', 'MeasurementResult', 'PlaneToPlaneDistance', 'PointToPlaneDistance', 'PointToPointDistance']
|
||||
@@ -0,0 +1,49 @@
|
||||
"""测量任务基类"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass
|
||||
class MeasurementResult:
|
||||
"""测量结果数据类"""
|
||||
task_name: str
|
||||
task_type: str
|
||||
success: bool
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class BaseMeasurement(ABC):
|
||||
"""测量任务基类"""
|
||||
|
||||
def __init__(self, name: str, point_cloud: np.ndarray, config: Dict[str, Any], spatial_index=None):
|
||||
"""
|
||||
初始化测量任务
|
||||
|
||||
Args:
|
||||
name: 任务名称
|
||||
point_cloud: 完整点云数组 (N, 3)
|
||||
config: 任务配置字典
|
||||
spatial_index: 空间索引对象(可选)
|
||||
"""
|
||||
self.name = name
|
||||
self.point_cloud = point_cloud
|
||||
self.config = config
|
||||
self.task_type = config.get('type', 'unknown')
|
||||
self.spatial_index = spatial_index # 保存空间索引
|
||||
|
||||
@abstractmethod
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""
|
||||
执行测量任务
|
||||
|
||||
Returns:
|
||||
MeasurementResult: 测量结果
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""检查任务是否启用"""
|
||||
return self.config.get('enabled', True)
|
||||
@@ -0,0 +1,280 @@
|
||||
"""线到线距离测量"""
|
||||
import numpy as np
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from edge_line_roi_extractor import EdgeLineROIExtractor
|
||||
from line_fitter import LineFitter
|
||||
from visualizer import Visualizer
|
||||
|
||||
|
||||
class LineToLineDistance(BaseMeasurement):
|
||||
"""测量两条边缘线之间的距离"""
|
||||
|
||||
def _log(self, msg: str):
|
||||
"""同时输出到控制台和日志文件"""
|
||||
print(msg)
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log(msg, also_print=False)
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行线到线距离测量"""
|
||||
try:
|
||||
timing_breakdown = {}
|
||||
|
||||
# 获取logger
|
||||
logger = getattr(self, 'detail_logger', None)
|
||||
|
||||
# 获取参数
|
||||
params = self.config.get('params', {})
|
||||
source_roi = self.config.get('source_roi', {})
|
||||
target_roi = self.config.get('target_roi', {})
|
||||
|
||||
# 1. 提取源边缘线ROI(两步法)
|
||||
msg = f"\n 步骤1: 提取源边缘线ROI"
|
||||
if logger: logger.log(msg)
|
||||
print(msg)
|
||||
start_time = time.time()
|
||||
|
||||
# 获取线的端点(优先使用line_start/line_end,否则从bbox推导)
|
||||
if 'line_start' in source_roi and 'line_end' in source_roi:
|
||||
source_p1 = np.array(source_roi.get('line_start'))
|
||||
source_p2 = np.array(source_roi.get('line_end'))
|
||||
else:
|
||||
# 从bbox推导线的端点
|
||||
source_bbox_min = np.array(source_roi.get('bbox_min'))
|
||||
source_bbox_max = np.array(source_roi.get('bbox_max'))
|
||||
source_p1 = source_bbox_min
|
||||
source_p2 = source_bbox_max
|
||||
|
||||
msg = f" 线起点: [{source_p1[0]:.2f}, {source_p1[1]:.2f}, {source_p1[2]:.2f}]"
|
||||
if logger: logger.log(msg)
|
||||
print(msg)
|
||||
msg = f" 线终点: [{source_p2[0]:.2f}, {source_p2[1]:.2f}, {source_p2[2]:.2f}]"
|
||||
if logger: logger.log(msg)
|
||||
print(msg)
|
||||
|
||||
source_points, source_indices, source_stats = EdgeLineROIExtractor.extract_two_step_roi(
|
||||
self.point_cloud,
|
||||
source_p1,
|
||||
source_p2,
|
||||
self.spatial_index,
|
||||
distance_threshold=params.get('roi_distance_threshold', 2.0),
|
||||
bbox_margin=5.0
|
||||
)
|
||||
timing_breakdown['提取源线ROI'] = time.time() - start_time
|
||||
|
||||
self._log(f" 粗ROI bbox: [{source_stats['bbox_min'][0]:.2f}, {source_stats['bbox_min'][1]:.2f}, {source_stats['bbox_min'][2]:.2f}] -> [{source_stats['bbox_max'][0]:.2f}, {source_stats['bbox_max'][1]:.2f}, {source_stats['bbox_max'][2]:.2f}]")
|
||||
self._log(f" 粗筛选: {source_stats['coarse_count']:,} 点")
|
||||
self._log(f" 精筛选(距离<{source_stats['distance_threshold']}mm): {source_stats['fine_count']:,} 点")
|
||||
self._log(f" 耗时: {timing_breakdown['提取源线ROI']:.2f}秒")
|
||||
|
||||
if len(source_points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message="源边缘线ROI区域内没有点"
|
||||
)
|
||||
|
||||
# 2. 提取目标边缘线ROI(两步法)
|
||||
self._log(f"\n 步骤2: 提取目标边缘线ROI")
|
||||
start_time = time.time()
|
||||
|
||||
# 获取线的端点
|
||||
if 'line_start' in target_roi and 'line_end' in target_roi:
|
||||
target_p1 = np.array(target_roi.get('line_start'))
|
||||
target_p2 = np.array(target_roi.get('line_end'))
|
||||
else:
|
||||
target_bbox_min = np.array(target_roi.get('bbox_min'))
|
||||
target_bbox_max = np.array(target_roi.get('bbox_max'))
|
||||
target_p1 = target_bbox_min
|
||||
target_p2 = target_bbox_max
|
||||
|
||||
self._log(f" 线起点: [{target_p1[0]:.2f}, {target_p1[1]:.2f}, {target_p1[2]:.2f}]")
|
||||
self._log(f" 线终点: [{target_p2[0]:.2f}, {target_p2[1]:.2f}, {target_p2[2]:.2f}]")
|
||||
|
||||
target_points, target_indices, target_stats = EdgeLineROIExtractor.extract_two_step_roi(
|
||||
self.point_cloud,
|
||||
target_p1,
|
||||
target_p2,
|
||||
self.spatial_index,
|
||||
distance_threshold=params.get('roi_distance_threshold', 2.0),
|
||||
bbox_margin=5.0
|
||||
)
|
||||
timing_breakdown['提取目标线ROI'] = time.time() - start_time
|
||||
|
||||
self._log(f" 粗ROI bbox: [{target_stats['bbox_min'][0]:.2f}, {target_stats['bbox_min'][1]:.2f}, {target_stats['bbox_min'][2]:.2f}] -> [{target_stats['bbox_max'][0]:.2f}, {target_stats['bbox_max'][1]:.2f}, {target_stats['bbox_max'][2]:.2f}]")
|
||||
self._log(f" 粗筛选: {target_stats['coarse_count']:,} 点")
|
||||
self._log(f" 精筛选(距离<{target_stats['distance_threshold']}mm): {target_stats['fine_count']:,} 点")
|
||||
self._log(f" 耗时: {timing_breakdown['提取目标线ROI']:.2f}秒")
|
||||
|
||||
if len(target_points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message="目标边缘线ROI区域内没有点"
|
||||
)
|
||||
|
||||
# 3. 拟合源边缘线(用全部精筛选点)
|
||||
self._log(f"\n 步骤3: 拟合源边缘线")
|
||||
start_time = time.time()
|
||||
self._log(f" 输入点数: {len(source_points)} 个点")
|
||||
source_fitter = LineFitter(
|
||||
threshold=params.get('ransac_threshold', 2.0),
|
||||
max_iterations=params.get('ransac_max_iterations', 1000),
|
||||
min_inlier_ratio=params.get('min_inlier_ratio', 0.6)
|
||||
)
|
||||
source_line = source_fitter.fit(source_points)
|
||||
timing_breakdown['源线RANSAC拟合'] = time.time() - start_time
|
||||
|
||||
if source_line is None:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"源边缘线拟合失败(点数: {len(source_points)},要求内点比例>={params.get('min_inlier_ratio', 0.6)*100:.0f}%)"
|
||||
)
|
||||
|
||||
self._log(f" RANSAC参数: 阈值={params.get('ransac_threshold', 2.0)}mm, 迭代={params.get('ransac_max_iterations', 1000)}次, 最小内点比例={params.get('min_inlier_ratio', 0.6)*100:.0f}%")
|
||||
self._log(f" 拟合结果: 内点{len(source_line.inliers)}/{len(source_points)} ({source_line.inlier_ratio*100:.1f}%)")
|
||||
self._log(f" 线上一点: [{source_line.point[0]:.4f}, {source_line.point[1]:.4f}, {source_line.point[2]:.4f}]")
|
||||
self._log(f" 线方向: [{source_line.direction[0]:.4f}, {source_line.direction[1]:.4f}, {source_line.direction[2]:.4f}]")
|
||||
self._log(f" 耗时: {timing_breakdown['源线RANSAC拟合']:.2f}秒")
|
||||
|
||||
# 4. 拟合目标边缘线(用全部精筛选点)
|
||||
self._log(f"\n 步骤4: 拟合目标边缘线")
|
||||
start_time = time.time()
|
||||
self._log(f" 输入点数: {len(target_points)} 个点")
|
||||
target_fitter = LineFitter(
|
||||
threshold=params.get('ransac_threshold', 2.0),
|
||||
max_iterations=params.get('ransac_max_iterations', 1000),
|
||||
min_inlier_ratio=params.get('min_inlier_ratio', 0.6)
|
||||
)
|
||||
target_line = target_fitter.fit(target_points)
|
||||
timing_breakdown['目标线RANSAC拟合'] = time.time() - start_time
|
||||
|
||||
if target_line is None:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"目标边缘线拟合失败(点数: {len(target_points)},要求内点比例>={params.get('min_inlier_ratio', 0.6)*100:.0f}%)"
|
||||
)
|
||||
|
||||
self._log(f" RANSAC参数: 阈值={params.get('ransac_threshold', 2.0)}mm, 迭代={params.get('ransac_max_iterations', 1000)}次, 最小内点比例={params.get('min_inlier_ratio', 0.6)*100:.0f}%")
|
||||
self._log(f" 拟合结果: 内点{len(target_line.inliers)}/{len(target_points)} ({target_line.inlier_ratio*100:.1f}%)")
|
||||
self._log(f" 线上一点: [{target_line.point[0]:.4f}, {target_line.point[1]:.4f}, {target_line.point[2]:.4f}]")
|
||||
self._log(f" 线方向: [{target_line.direction[0]:.4f}, {target_line.direction[1]:.4f}, {target_line.direction[2]:.4f}]")
|
||||
self._log(f" 耗时: {timing_breakdown['目标线RANSAC拟合']:.2f}秒")
|
||||
|
||||
# 5. 从源线采样点(从内点中采样)
|
||||
self._log(f"\n 步骤5: 从源线采样点")
|
||||
start_time = time.time()
|
||||
n_samples = params.get('sample_points', 100)
|
||||
source_inlier_points = source_points[source_line.inliers]
|
||||
sampled_points = source_line.sample_points_on_line(n_samples, source_inlier_points)
|
||||
timing_breakdown['采样点选择'] = time.time() - start_time
|
||||
|
||||
self._log(f" 采样数量: {len(sampled_points)} 个点")
|
||||
self._log(f" 耗时: {timing_breakdown['采样点选择']:.4f}秒")
|
||||
|
||||
# 6. 计算采样点到目标线的距离
|
||||
self._log(f"\n 步骤6: 计算距离")
|
||||
start_time = time.time()
|
||||
distances = target_line.distance_to_points(sampled_points)
|
||||
timing_breakdown['距离计算'] = time.time() - start_time
|
||||
|
||||
self._log(f" 计算 {len(sampled_points)} 个采样点到目标线的距离")
|
||||
self._log(f" 耗时: {timing_breakdown['距离计算']:.4f}秒")
|
||||
|
||||
# 7. 统计结果
|
||||
self._log(f"\n 步骤7: 统计结果")
|
||||
mean_distance = np.mean(distances)
|
||||
std_distance = np.std(distances)
|
||||
min_distance = np.min(distances)
|
||||
max_distance = np.max(distances)
|
||||
|
||||
self._log(f" 平均距离: {mean_distance:.4f} mm")
|
||||
self._log(f" 标准差: {std_distance:.4f} mm")
|
||||
self._log(f" 最小距离: {min_distance:.4f} mm")
|
||||
self._log(f" 最大距离: {max_distance:.4f} mm")
|
||||
|
||||
# 8. 合格性判断
|
||||
self._log(f"\n 步骤8: 合格性判断")
|
||||
theoretical_dimension = self.config.get('theoretical_dimension')
|
||||
tolerance = self.config.get('tolerance', {})
|
||||
tolerance_upper = tolerance.get('upper', 0)
|
||||
tolerance_lower = tolerance.get('lower', 0)
|
||||
|
||||
is_qualified = None
|
||||
deviation = None
|
||||
if theoretical_dimension is not None:
|
||||
deviation = mean_distance - theoretical_dimension
|
||||
is_qualified = (deviation >= -0.11) and (deviation <= 0.1)
|
||||
|
||||
self._log(f" 理论尺寸: {theoretical_dimension:.4f} mm")
|
||||
self._log(f" 实测平均: {mean_distance:.4f} mm")
|
||||
self._log(f" 偏差: {deviation:+.4f} mm")
|
||||
self._log(f" 公差范围: [{tolerance_lower:+.1f}, {tolerance_upper:+.1f}] mm")
|
||||
status = "✅ 合格" if is_qualified else "❌ 不合格"
|
||||
self._log(f" 判定结果: {status}")
|
||||
|
||||
# 9. 生成可视化(如果配置了输出目录)
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
source_ply_path = vis_dir / f"{self.name}_source_line_roi.ply"
|
||||
target_ply_path = vis_dir / f"{self.name}_target_line_roi.ply"
|
||||
Visualizer.save_roi_pointcloud(source_points, str(source_ply_path))
|
||||
Visualizer.save_roi_pointcloud(target_points, str(target_ply_path))
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
result_data = {
|
||||
'mean_distance': float(mean_distance),
|
||||
'std_distance': float(std_distance),
|
||||
'min_distance': float(min_distance),
|
||||
'max_distance': float(max_distance),
|
||||
'sample_count': len(sampled_points),
|
||||
'source_roi_points': len(source_points),
|
||||
'target_roi_points': len(target_points),
|
||||
'source_inlier_ratio': float(source_line.inlier_ratio),
|
||||
'target_inlier_ratio': float(target_line.inlier_ratio),
|
||||
'source_line_direction': source_line.direction.tolist(),
|
||||
'target_line_direction': target_line.direction.tolist(),
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
|
||||
# 添加合格性判断结果
|
||||
if theoretical_dimension is not None:
|
||||
result_data['theoretical_dimension'] = float(theoretical_dimension)
|
||||
result_data['deviation'] = float(deviation)
|
||||
result_data['is_qualified'] = is_qualified
|
||||
result_data['tolerance_upper'] = float(tolerance_upper)
|
||||
result_data['tolerance_lower'] = float(tolerance_lower)
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data=result_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
@@ -0,0 +1,252 @@
|
||||
"""线到面距离测量"""
|
||||
import numpy as np
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from edge_line_roi_extractor import EdgeLineROIExtractor
|
||||
from line_fitter import LineFitter
|
||||
from roi_extractor import ROIExtractor
|
||||
from plane_fitter import PlaneFitter
|
||||
from visualizer import Visualizer
|
||||
|
||||
|
||||
class LineToPlaneDistance(BaseMeasurement):
|
||||
"""测量边缘线到平面的距离"""
|
||||
|
||||
def _log(self, msg: str):
|
||||
"""同时输出到控制台和日志文件"""
|
||||
print(msg)
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log(msg, also_print=False)
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行线到面距离测量"""
|
||||
try:
|
||||
timing_breakdown = {}
|
||||
|
||||
# 获取参数
|
||||
params = self.config.get('params', {})
|
||||
source_roi = self.config.get('source_roi', {})
|
||||
target_roi = self.config.get('target_roi', {})
|
||||
|
||||
# 1. 提取源边缘线ROI(两步法)
|
||||
self._log(f"\n 步骤1: 提取源边缘线ROI")
|
||||
start_time = time.time()
|
||||
|
||||
# 获取线的端点(优先使用line_start/line_end,否则从bbox推导)
|
||||
if 'line_start' in source_roi and 'line_end' in source_roi:
|
||||
source_p1 = np.array(source_roi.get('line_start'))
|
||||
source_p2 = np.array(source_roi.get('line_end'))
|
||||
else:
|
||||
source_bbox_min = np.array(source_roi.get('bbox_min'))
|
||||
source_bbox_max = np.array(source_roi.get('bbox_max'))
|
||||
source_p1 = source_bbox_min
|
||||
source_p2 = source_bbox_max
|
||||
|
||||
self._log(f" 线起点: [{source_p1[0]:.2f}, {source_p1[1]:.2f}, {source_p1[2]:.2f}]")
|
||||
self._log(f" 线终点: [{source_p2[0]:.2f}, {source_p2[1]:.2f}, {source_p2[2]:.2f}]")
|
||||
|
||||
source_points, source_indices, source_stats = EdgeLineROIExtractor.extract_two_step_roi(
|
||||
self.point_cloud,
|
||||
source_p1,
|
||||
source_p2,
|
||||
self.spatial_index,
|
||||
distance_threshold=params.get('roi_distance_threshold', 2.0),
|
||||
bbox_margin=5.0
|
||||
)
|
||||
timing_breakdown['提取源线ROI'] = time.time() - start_time
|
||||
|
||||
self._log(f" 粗ROI bbox: [{source_stats['bbox_min'][0]:.2f}, {source_stats['bbox_min'][1]:.2f}, {source_stats['bbox_min'][2]:.2f}] -> [{source_stats['bbox_max'][0]:.2f}, {source_stats['bbox_max'][1]:.2f}, {source_stats['bbox_max'][2]:.2f}]")
|
||||
self._log(f" 粗筛选: {source_stats['coarse_count']:,} 点")
|
||||
self._log(f" 精筛选(距离<{source_stats['distance_threshold']}mm): {source_stats['fine_count']:,} 点")
|
||||
self._log(f" 耗时: {timing_breakdown['提取源线ROI']:.2f}秒")
|
||||
|
||||
if len(source_points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message="源边缘线ROI区域内没有点"
|
||||
)
|
||||
|
||||
# 2. 提取目标平面ROI(用bbox)
|
||||
self._log(f"\n 步骤2: 提取目标平面ROI")
|
||||
start_time = time.time()
|
||||
target_points, _ = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud,
|
||||
target_roi.get('bbox_min'),
|
||||
target_roi.get('bbox_max'),
|
||||
self.spatial_index
|
||||
)
|
||||
timing_breakdown['提取目标平面ROI'] = time.time() - start_time
|
||||
|
||||
self._log(f" bbox: {target_roi.get('bbox_min')} -> {target_roi.get('bbox_max')}")
|
||||
self._log(f" 提取点数: {len(target_points):,} 点")
|
||||
self._log(f" 耗时: {timing_breakdown['提取目标平面ROI']:.2f}秒")
|
||||
|
||||
if len(target_points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message="目标平面ROI区域内没有点"
|
||||
)
|
||||
|
||||
# 3. 拟合源边缘线(用全部精筛选点)
|
||||
self._log(f"\n 步骤3: 拟合源边缘线")
|
||||
start_time = time.time()
|
||||
self._log(f" 输入点数: {len(source_points)} 个点")
|
||||
source_fitter = LineFitter(
|
||||
threshold=params.get('ransac_threshold', 2.0),
|
||||
max_iterations=params.get('ransac_max_iterations', 1000),
|
||||
min_inlier_ratio=params.get('min_inlier_ratio', 0.6)
|
||||
)
|
||||
source_line = source_fitter.fit(source_points)
|
||||
timing_breakdown['源线RANSAC拟合'] = time.time() - start_time
|
||||
|
||||
if source_line is None:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"源边缘线拟合失败(点数: {len(source_points)},要求内点比例>={params.get('min_inlier_ratio', 0.6)*100:.0f}%)"
|
||||
)
|
||||
|
||||
self._log(f" RANSAC参数: 阈值={params.get('ransac_threshold', 2.0)}mm, 迭代={params.get('ransac_max_iterations', 1000)}次, 最小内点比例={params.get('min_inlier_ratio', 0.6)*100:.0f}%")
|
||||
self._log(f" 拟合结果: 内点{len(source_line.inliers)}/{len(source_points)} ({source_line.inlier_ratio*100:.1f}%)")
|
||||
self._log(f" 线上一点: [{source_line.point[0]:.4f}, {source_line.point[1]:.4f}, {source_line.point[2]:.4f}]")
|
||||
self._log(f" 线方向: [{source_line.direction[0]:.4f}, {source_line.direction[1]:.4f}, {source_line.direction[2]:.4f}]")
|
||||
self._log(f" 耗时: {timing_breakdown['源线RANSAC拟合']:.2f}秒")
|
||||
|
||||
# 4. 拟合目标平面(降采样优化)
|
||||
self._log(f"\n 步骤4: 拟合目标平面")
|
||||
start_time = time.time()
|
||||
self._log(f" 输入点数: {len(target_points)} 个点")
|
||||
|
||||
max_points_for_ransac = 100000
|
||||
target_points_for_fit = target_points
|
||||
if len(target_points) > max_points_for_ransac:
|
||||
sample_indices = np.random.choice(len(target_points), max_points_for_ransac, replace=False)
|
||||
target_points_for_fit = target_points[sample_indices]
|
||||
self._log(f" 降采样优化: {len(target_points):,} -> {len(target_points_for_fit):,} 个点")
|
||||
|
||||
target_fitter = PlaneFitter(
|
||||
threshold=params.get('ransac_threshold', 2.0),
|
||||
max_iterations=params.get('ransac_max_iterations', 1000),
|
||||
min_inlier_ratio=params.get('min_inlier_ratio', 0.6)
|
||||
)
|
||||
target_plane = target_fitter.fit(target_points_for_fit)
|
||||
timing_breakdown['目标平面RANSAC拟合'] = time.time() - start_time
|
||||
|
||||
if target_plane is None:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"目标平面拟合失败(点数: {len(target_points)},要求内点比例>={params.get('min_inlier_ratio', 0.6)*100:.0f}%)"
|
||||
)
|
||||
|
||||
self._log(f" RANSAC参数: 阈值={params.get('ransac_threshold', 2.0)}mm, 迭代={params.get('ransac_max_iterations', 1000)}次, 最小内点比例={params.get('min_inlier_ratio', 0.6)*100:.0f}%")
|
||||
self._log(f" 拟合结果: 内点{len(target_plane.inliers)}/{len(target_points_for_fit)} ({target_plane.inlier_ratio*100:.1f}%)")
|
||||
self._log(f" 平面上一点: [{target_plane.point[0]:.4f}, {target_plane.point[1]:.4f}, {target_plane.point[2]:.4f}]")
|
||||
self._log(f" 平面法向量: [{target_plane.normal[0]:.4f}, {target_plane.normal[1]:.4f}, {target_plane.normal[2]:.4f}]")
|
||||
self._log(f" 平面方程: {target_plane.normal[0]:.6f}*x + {target_plane.normal[1]:.6f}*y + {target_plane.normal[2]:.6f}*z + {target_plane.d:.6f} = 0")
|
||||
self._log(f" 耗时: {timing_breakdown['目标平面RANSAC拟合']:.2f}秒")
|
||||
|
||||
# 5. 从源线采样点(从内点中采样)
|
||||
start_time = time.time()
|
||||
n_samples = params.get('sample_points', 100)
|
||||
source_inlier_points = source_points[source_line.inliers]
|
||||
sampled_points = source_line.sample_points_on_line(n_samples, source_inlier_points)
|
||||
timing_breakdown['采样点选择'] = time.time() - start_time
|
||||
|
||||
self._log(f" 采样{len(sampled_points)}个点")
|
||||
|
||||
# 6. 计算采样点到目标平面的距离
|
||||
start_time = time.time()
|
||||
distances = target_plane.distance_to_points(sampled_points)
|
||||
timing_breakdown['距离计算'] = time.time() - start_time
|
||||
|
||||
# 7. 统计结果(使用绝对值)
|
||||
mean_distance = np.mean(np.abs(distances))
|
||||
std_distance = np.std(distances)
|
||||
min_distance = np.min(np.abs(distances))
|
||||
max_distance = np.max(np.abs(distances))
|
||||
|
||||
# 8. 合格性判断
|
||||
theoretical_dimension = self.config.get('theoretical_dimension')
|
||||
tolerance = self.config.get('tolerance', {})
|
||||
tolerance_upper = tolerance.get('upper', 0)
|
||||
tolerance_lower = tolerance.get('lower', 0)
|
||||
|
||||
is_qualified = None
|
||||
deviation = None
|
||||
if theoretical_dimension is not None:
|
||||
deviation = mean_distance - theoretical_dimension
|
||||
is_qualified = (deviation >= -0.11) and (deviation <= 0.1)
|
||||
|
||||
self._log(f" 平均距离: {mean_distance:.4f} mm")
|
||||
if theoretical_dimension is not None:
|
||||
self._log(f" 理论尺寸: {theoretical_dimension:.4f} mm")
|
||||
self._log(f" 偏差: {deviation:+.4f} mm")
|
||||
self._log(f" 公差范围: [{tolerance_lower:+.1f}, {tolerance_upper:+.1f}] mm")
|
||||
status = "✅ 合格" if is_qualified else "❌ 不合格"
|
||||
self._log(f" 判定: {status}")
|
||||
|
||||
# 9. 生成可视化(如果配置了输出目录)
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
source_ply_path = vis_dir / f"{self.name}_source_line_roi.ply"
|
||||
target_ply_path = vis_dir / f"{self.name}_target_plane_roi.ply"
|
||||
Visualizer.save_roi_pointcloud(source_points, str(source_ply_path))
|
||||
Visualizer.save_roi_pointcloud(target_points, str(target_ply_path))
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
result_data = {
|
||||
'mean_distance': float(mean_distance),
|
||||
'std_distance': float(std_distance),
|
||||
'min_distance': float(min_distance),
|
||||
'max_distance': float(max_distance),
|
||||
'sample_count': len(sampled_points),
|
||||
'source_roi_points': len(source_points),
|
||||
'target_roi_points': len(target_points),
|
||||
'source_inlier_ratio': float(source_line.inlier_ratio),
|
||||
'target_inlier_ratio': float(target_plane.inlier_ratio),
|
||||
'source_line_direction': source_line.direction.tolist(),
|
||||
'target_plane_normal': target_plane.normal.tolist(),
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
|
||||
# 添加合格性判断结果
|
||||
if theoretical_dimension is not None:
|
||||
result_data['theoretical_dimension'] = float(theoretical_dimension)
|
||||
result_data['deviation'] = float(deviation)
|
||||
result_data['is_qualified'] = is_qualified
|
||||
result_data['tolerance_upper'] = float(tolerance_upper)
|
||||
result_data['tolerance_lower'] = float(tolerance_lower)
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data=result_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
@@ -0,0 +1,277 @@
|
||||
"""平面到平面距离测量"""
|
||||
import numpy as np
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from roi_extractor import ROIExtractor
|
||||
from plane_fitter import PlaneFitter
|
||||
from visualizer import Visualizer
|
||||
|
||||
|
||||
class PlaneToPlaneDistance(BaseMeasurement):
|
||||
"""测量两个平面之间的距离(单向采样)"""
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行平面到平面距离测量"""
|
||||
try:
|
||||
timing_breakdown = {}
|
||||
|
||||
# 1. 提取源平面ROI
|
||||
start_time = time.time()
|
||||
source_roi = self.config.get('source_roi', {})
|
||||
source_points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, source_roi.get('bbox_min'), source_roi.get('bbox_max'), self.spatial_index)
|
||||
timing_breakdown['提取源ROI'] = time.time() - start_time
|
||||
|
||||
# 记录ROI提取信息到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log_roi_extraction(
|
||||
"源平面ROI",
|
||||
len(source_points),
|
||||
source_roi.get('bbox_min'),
|
||||
source_roi.get('bbox_max')
|
||||
)
|
||||
|
||||
if len(source_points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message="源平面ROI区域内没有点"
|
||||
)
|
||||
|
||||
# 2. 提取目标平面ROI
|
||||
start_time = time.time()
|
||||
target_roi = self.config.get('target_roi', {})
|
||||
target_points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, target_roi.get('bbox_min'), target_roi.get('bbox_max'), self.spatial_index)
|
||||
timing_breakdown['提取目标ROI'] = time.time() - start_time
|
||||
|
||||
# 记录ROI提取信息到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log_roi_extraction(
|
||||
"目标平面ROI",
|
||||
len(target_points),
|
||||
target_roi.get('bbox_min'),
|
||||
target_roi.get('bbox_max')
|
||||
)
|
||||
|
||||
if len(target_points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message="目标平面ROI区域内没有点"
|
||||
)
|
||||
|
||||
# 3. 拟合源平面(优化:点过多时降采样)
|
||||
start_time = time.time()
|
||||
params = self.config.get('params', {})
|
||||
|
||||
# 降采样优化:如果点数过多,先降采样再RANSAC
|
||||
max_points_for_ransac = 100000 # 10万个点足够拟合平面
|
||||
source_points_for_fit = source_points
|
||||
if len(source_points) > max_points_for_ransac:
|
||||
sample_indices = np.random.choice(len(source_points), max_points_for_ransac, replace=False)
|
||||
source_points_for_fit = source_points[sample_indices]
|
||||
print(f" 源平面RANSAC优化: 从 {len(source_points):,} 降采样到 {len(source_points_for_fit):,} 个点")
|
||||
|
||||
source_fitter = PlaneFitter(
|
||||
threshold=params.get('ransac_threshold', 2.0),
|
||||
max_iterations=params.get('ransac_max_iterations', 1000),
|
||||
min_inlier_ratio=params.get('min_inlier_ratio', 0.6)
|
||||
)
|
||||
source_plane = source_fitter.fit(source_points_for_fit)
|
||||
timing_breakdown['源平面RANSAC拟合'] = time.time() - start_time
|
||||
|
||||
if source_plane is None:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"源平面拟合失败(点数: {len(source_points)})"
|
||||
)
|
||||
|
||||
# 记录源平面RANSAC拟合信息到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log_ransac_fitting(
|
||||
"源平面",
|
||||
len(source_points_for_fit),
|
||||
len(source_plane.inliers),
|
||||
source_plane.inlier_ratio,
|
||||
(source_plane.normal[0], source_plane.normal[1], source_plane.normal[2], source_plane.d)
|
||||
)
|
||||
|
||||
# 4. 拟合目标平面(优化:点过多时降采样)
|
||||
start_time = time.time()
|
||||
|
||||
# 降采样优化:如果点数过多,先降采样再RANSAC
|
||||
target_points_for_fit = target_points
|
||||
if len(target_points) > max_points_for_ransac:
|
||||
sample_indices = np.random.choice(len(target_points), max_points_for_ransac, replace=False)
|
||||
target_points_for_fit = target_points[sample_indices]
|
||||
print(f" 目标平面RANSAC优化: 从 {len(target_points):,} 降采样到 {len(target_points_for_fit):,} 个点")
|
||||
|
||||
target_fitter = PlaneFitter(
|
||||
threshold=params.get('ransac_threshold', 2.0),
|
||||
max_iterations=params.get('ransac_max_iterations', 1000),
|
||||
min_inlier_ratio=params.get('min_inlier_ratio', 0.6)
|
||||
)
|
||||
target_plane = target_fitter.fit(target_points_for_fit)
|
||||
timing_breakdown['目标平面RANSAC拟合'] = time.time() - start_time
|
||||
|
||||
if target_plane is None:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"目标平面拟合失败(点数: {len(target_points)})"
|
||||
)
|
||||
|
||||
# 记录目标平面RANSAC拟合信息到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log_ransac_fitting(
|
||||
"目标平面",
|
||||
len(target_points_for_fit),
|
||||
len(target_plane.inliers),
|
||||
target_plane.inlier_ratio,
|
||||
(target_plane.normal[0], target_plane.normal[1], target_plane.normal[2], target_plane.d)
|
||||
)
|
||||
|
||||
# 5. 从源平面采样点
|
||||
start_time = time.time()
|
||||
n_samples = params.get('sample_points', 100)
|
||||
# 修复:inliers索引是相对于source_points_for_fit的,不是source_points
|
||||
source_inlier_points = source_points_for_fit[source_plane.inliers]
|
||||
sampled_points = ROIExtractor.sample_points(source_inlier_points, n_samples)
|
||||
timing_breakdown['采样点选择'] = time.time() - start_time
|
||||
|
||||
# 记录采样信息到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log_sampling(len(sampled_points), "从源平面内点均匀采样")
|
||||
|
||||
# 6. 计算采样点到目标平面的距离
|
||||
start_time = time.time()
|
||||
distances = target_plane.distance_to_points(sampled_points)
|
||||
timing_breakdown['距离计算'] = time.time() - start_time
|
||||
|
||||
# 6.5. 可选:检查不同位置的宽度差值(用于门口类测量)
|
||||
check_width_variation = params.get('check_width_variation', False)
|
||||
width_variation_result = None
|
||||
|
||||
if check_width_variation:
|
||||
# 按Y坐标分成3组:上部、中部、下部
|
||||
y_coords = sampled_points[:, 1]
|
||||
y_min, y_max = y_coords.min(), y_coords.max()
|
||||
y_range = y_max - y_min
|
||||
|
||||
# 分组:上部(Y最大的1/3)、中部(中间1/3)、下部(Y最小的1/3)
|
||||
upper_mask = y_coords >= (y_max - y_range / 3)
|
||||
lower_mask = y_coords <= (y_min + y_range / 3)
|
||||
middle_mask = ~upper_mask & ~lower_mask
|
||||
|
||||
upper_distances = np.abs(distances[upper_mask])
|
||||
middle_distances = np.abs(distances[middle_mask])
|
||||
lower_distances = np.abs(distances[lower_mask])
|
||||
|
||||
upper_width = np.mean(upper_distances)
|
||||
middle_width = np.mean(middle_distances)
|
||||
lower_width = np.mean(lower_distances)
|
||||
|
||||
# 计算差值
|
||||
diff_upper_middle = upper_width - middle_width
|
||||
diff_middle_lower = middle_width - lower_width
|
||||
diff_upper_lower = upper_width - lower_width
|
||||
|
||||
# 检查公差(默认±2mm)
|
||||
width_tolerance = params.get('width_variation_tolerance', 2.0)
|
||||
all_diffs = [diff_upper_middle, diff_middle_lower, diff_upper_lower]
|
||||
width_variation_pass = all(abs(d) <= width_tolerance for d in all_diffs)
|
||||
|
||||
width_variation_result = {
|
||||
'upper_width': float(upper_width),
|
||||
'middle_width': float(middle_width),
|
||||
'lower_width': float(lower_width),
|
||||
'diff_upper_middle': float(diff_upper_middle),
|
||||
'diff_middle_lower': float(diff_middle_lower),
|
||||
'diff_upper_lower': float(diff_upper_lower),
|
||||
'tolerance': float(width_tolerance),
|
||||
'pass': width_variation_pass,
|
||||
'upper_point_count': int(upper_mask.sum()),
|
||||
'middle_point_count': int(middle_mask.sum()),
|
||||
'lower_point_count': int(lower_mask.sum())
|
||||
}
|
||||
|
||||
# 记录到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log("")
|
||||
self.detail_logger.log(" 宽度差值检查:")
|
||||
self.detail_logger.log(f" 上部宽度 (Y:{y_max-y_range/3:.1f}~{y_max:.1f}): {upper_width:.4f} mm ({upper_mask.sum()}点)")
|
||||
self.detail_logger.log(f" 中部宽度 (Y:{y_min+y_range/3:.1f}~{y_max-y_range/3:.1f}): {middle_width:.4f} mm ({middle_mask.sum()}点)")
|
||||
self.detail_logger.log(f" 下部宽度 (Y:{y_min:.1f}~{y_min+y_range/3:.1f}): {lower_width:.4f} mm ({lower_mask.sum()}点)")
|
||||
self.detail_logger.log("")
|
||||
self.detail_logger.log(f" 差值 (上-中): {diff_upper_middle:+.4f} mm")
|
||||
self.detail_logger.log(f" 差值 (中-下): {diff_middle_lower:+.4f} mm")
|
||||
self.detail_logger.log(f" 差值 (上-下): {diff_upper_lower:+.4f} mm")
|
||||
self.detail_logger.log(f" 公差要求: ±{width_tolerance:.1f} mm")
|
||||
status = "✅ 通过" if width_variation_pass else "❌ 超差"
|
||||
self.detail_logger.log(f" 检查结果: {status}")
|
||||
|
||||
# 7. 统计结果
|
||||
mean_distance = np.mean(np.abs(distances))
|
||||
std_distance = np.std(distances)
|
||||
min_distance = np.min(np.abs(distances))
|
||||
max_distance = np.max(np.abs(distances))
|
||||
|
||||
# 8. 生成可视化(如果配置了输出目录)
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 只保存ROI点云文件
|
||||
source_ply_path = vis_dir / f"{self.name}_source_roi.ply"
|
||||
target_ply_path = vis_dir / f"{self.name}_target_roi.ply"
|
||||
Visualizer.save_roi_pointcloud(source_points, str(source_ply_path))
|
||||
Visualizer.save_roi_pointcloud(target_points, str(target_ply_path))
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
result_data = {
|
||||
'mean_distance': float(mean_distance),
|
||||
'std_distance': float(std_distance),
|
||||
'min_distance': float(min_distance),
|
||||
'max_distance': float(max_distance),
|
||||
'sample_count': len(sampled_points),
|
||||
'source_roi_points': len(source_points),
|
||||
'target_roi_points': len(target_points),
|
||||
'source_inlier_ratio': float(source_plane.inlier_ratio),
|
||||
'target_inlier_ratio': float(target_plane.inlier_ratio),
|
||||
'source_plane_normal': source_plane.normal.tolist(),
|
||||
'target_plane_normal': target_plane.normal.tolist(),
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
|
||||
# 添加宽度差值结果(如果有)
|
||||
if width_variation_result:
|
||||
result_data['width_variation'] = width_variation_result
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data=result_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
带有孔中心检测的平面到平面距离测量
|
||||
支持 4_hole_center 方式定义参考平面
|
||||
"""
|
||||
import numpy as np
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from roi_extractor import ROIExtractor
|
||||
from plane_fitter import PlaneFitter
|
||||
from visualizer import Visualizer
|
||||
from hole_center_fitter import HoleCenterFitter
|
||||
|
||||
|
||||
class PlaneToPlaneWithHoles(BaseMeasurement):
|
||||
"""平面到平面距离测量(支持4孔定位参考平面)"""
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行测量"""
|
||||
try:
|
||||
timing_breakdown = {}
|
||||
|
||||
# 获取boxes配置
|
||||
boxes = self.config.get('boxes', [])
|
||||
if len(boxes) < 5:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"需要至少5个box(4个孔+至少1个平面),但只有{len(boxes)}个"
|
||||
)
|
||||
|
||||
# 1. 提取前4个孔的点云并拟合参考平面
|
||||
start_time = time.time()
|
||||
print(f" 提取4个孔的ROI区域...")
|
||||
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log(f" 提取4个定位孔...")
|
||||
|
||||
hole_points_list = []
|
||||
hole_results = []
|
||||
for i in range(4):
|
||||
box = boxes[i]
|
||||
points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, box['min'], box['max'], self.spatial_index)
|
||||
if len(points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"孔 '{box['name']}' ROI区域内没有点"
|
||||
)
|
||||
hole_points_list.append(points)
|
||||
print(f" {box['name']}: {len(points)} 个点")
|
||||
|
||||
# 拟合圆心(用于详细日志)
|
||||
result = HoleCenterFitter.fit_hole_center(points)
|
||||
hole_results.append(result)
|
||||
|
||||
# 拟合4孔参考平面
|
||||
reference_plane_result = HoleCenterFitter.fit_4_holes_reference_plane(hole_points_list)
|
||||
plane_y = reference_plane_result['plane_y']
|
||||
|
||||
# 记录到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
for i, (box, result) in enumerate(zip(boxes[:4], hole_results)):
|
||||
self.detail_logger.log_circle_fitting(
|
||||
f"定位孔 '{box['name']}'",
|
||||
tuple(result['center_3d']),
|
||||
result['radius'],
|
||||
len(hole_points_list[i])
|
||||
)
|
||||
self.detail_logger.log("")
|
||||
self.detail_logger.log(f" 4孔中心点: {reference_plane_result['center_point']}")
|
||||
self.detail_logger.log(f" 参考平面: Y = {plane_y:.4f} mm")
|
||||
self.detail_logger.log("")
|
||||
|
||||
timing_breakdown['4孔拟合参考平面'] = time.time() - start_time
|
||||
|
||||
# 2. 提取并拟合源平面(剩余的boxes)
|
||||
start_time = time.time()
|
||||
params = self.config.get('params', {})
|
||||
max_points_for_ransac = 100000
|
||||
|
||||
source_planes = []
|
||||
source_plane_names = []
|
||||
|
||||
print(f" 提取并拟合源平面...")
|
||||
for i in range(4, len(boxes)):
|
||||
box = boxes[i]
|
||||
points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, box['min'], box['max'], self.spatial_index)
|
||||
|
||||
if len(points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"源平面 '{box['name']}' ROI区域内没有点"
|
||||
)
|
||||
|
||||
print(f" {box['name']}: {len(points)} 个点")
|
||||
|
||||
# 降采样优化
|
||||
points_for_fit = points
|
||||
if len(points) > max_points_for_ransac:
|
||||
sample_indices = np.random.choice(len(points), max_points_for_ransac, replace=False)
|
||||
points_for_fit = points[sample_indices]
|
||||
print(f" RANSAC优化: 降采样到 {len(points_for_fit):,} 个点")
|
||||
|
||||
# RANSAC拟合平面
|
||||
fitter = PlaneFitter(
|
||||
threshold=params.get('ransac_threshold', 0.1),
|
||||
max_iterations=params.get('ransac_max_iterations', 1000),
|
||||
min_inlier_ratio=params.get('min_inlier_ratio', 0.6)
|
||||
)
|
||||
plane = fitter.fit(points_for_fit)
|
||||
|
||||
if plane is None:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"源平面 '{box['name']}' 拟合失败"
|
||||
)
|
||||
|
||||
source_planes.append({
|
||||
'plane': plane,
|
||||
'points': points,
|
||||
'name': box['name']
|
||||
})
|
||||
source_plane_names.append(box['name'])
|
||||
print(f" 拟合成功,内点率: {plane.inlier_ratio:.1%}")
|
||||
|
||||
# 记录源平面RANSAC拟合到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log_ransac_fitting(
|
||||
f"源平面 '{box['name']}'",
|
||||
len(points_for_fit),
|
||||
len(plane.inliers),
|
||||
plane.inlier_ratio,
|
||||
(plane.normal[0], plane.normal[1], plane.normal[2], plane.d)
|
||||
)
|
||||
|
||||
timing_breakdown['源平面RANSAC拟合'] = time.time() - start_time
|
||||
|
||||
# 3. 从每个源平面采样点并计算到参考平面的距离
|
||||
start_time = time.time()
|
||||
n_samples = params.get('sample_points', 100)
|
||||
|
||||
all_distances = []
|
||||
plane_results = []
|
||||
|
||||
print(f" 计算距离...")
|
||||
for plane_info in source_planes:
|
||||
plane = plane_info['plane']
|
||||
points = plane_info['points']
|
||||
name = plane_info['name']
|
||||
|
||||
# 从内点中采样
|
||||
inlier_points = points[plane.inliers]
|
||||
sampled_points = ROIExtractor.sample_points(inlier_points, n_samples)
|
||||
|
||||
# 计算到参考平面的距离(y方向)
|
||||
distances = sampled_points[:, 1] - plane_y
|
||||
|
||||
mean_dist = np.mean(np.abs(distances))
|
||||
print(f" {name}: 平均距离 = {mean_dist:.2f} mm")
|
||||
|
||||
all_distances.extend(distances)
|
||||
plane_results.append({
|
||||
'name': name,
|
||||
'mean_distance': float(mean_dist),
|
||||
'distances': distances.tolist()
|
||||
})
|
||||
|
||||
# 记录到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log("")
|
||||
self.detail_logger.log(f" 平面 '{name}' 到参考平面的距离:")
|
||||
self.detail_logger.log(f" 采样点数: {len(sampled_points)} 个点")
|
||||
self.detail_logger.log(f" 平均距离: {mean_dist:.4f} mm")
|
||||
self.detail_logger.log(f" 距离范围: [{np.min(np.abs(distances)):.4f}, {np.max(np.abs(distances)):.4f}] mm")
|
||||
|
||||
timing_breakdown['距离计算'] = time.time() - start_time
|
||||
|
||||
# 4. 统计所有距离
|
||||
all_distances = np.array(all_distances)
|
||||
mean_distance = np.mean(np.abs(all_distances))
|
||||
std_distance = np.std(all_distances)
|
||||
min_distance = np.min(np.abs(all_distances))
|
||||
max_distance = np.max(np.abs(all_distances))
|
||||
|
||||
# 5. 可视化
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 保存孔点云
|
||||
for i, points in enumerate(hole_points_list):
|
||||
ply_path = vis_dir / f"{self.name}_hole_{i+1}.ply"
|
||||
Visualizer.save_roi_pointcloud(points, str(ply_path))
|
||||
|
||||
# 保存源平面点云
|
||||
for plane_info in source_planes:
|
||||
ply_path = vis_dir / f"{self.name}_{plane_info['name']}.ply"
|
||||
Visualizer.save_roi_pointcloud(plane_info['points'], str(ply_path))
|
||||
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data={
|
||||
'mean_distance': float(mean_distance),
|
||||
'std_distance': float(std_distance),
|
||||
'min_distance': float(min_distance),
|
||||
'max_distance': float(max_distance),
|
||||
'sample_count': len(all_distances),
|
||||
'reference_plane_y': float(plane_y),
|
||||
'reference_plane_centers': reference_plane_result['centers_3d'],
|
||||
'source_plane_names': source_plane_names,
|
||||
'plane_results': plane_results,
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
点到平面距离测量模块
|
||||
|
||||
支持两种模式:
|
||||
1. exact_points: 在点云中查找给定坐标的最近点,计算到平面的距离
|
||||
2. bbox_filtered: 从包围盒提取点云,筛选特定条件的点,计算到平面的距离
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import time
|
||||
import open3d as o3d
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Tuple
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from visualizer import Visualizer
|
||||
|
||||
|
||||
class PointToPlaneDistance(BaseMeasurement):
|
||||
"""点到平面距离测量"""
|
||||
|
||||
def __init__(self, name: str, point_cloud: np.ndarray, config: Dict[str, Any], spatial_index=None):
|
||||
super().__init__(name, point_cloud, config, spatial_index)
|
||||
self.mode = config.get('mode', 'exact_points')
|
||||
|
||||
if self.mode == 'exact_points':
|
||||
self.target_points = np.array(config['target_points'])
|
||||
self.search_radius = config.get('search_radius', 10.0)
|
||||
elif self.mode == 'bbox_filtered':
|
||||
self.bboxes = config['bboxes']
|
||||
self.filter_config = config['filter']
|
||||
else:
|
||||
raise ValueError(f"Unknown mode: {self.mode}")
|
||||
|
||||
# 平面参数 (ax + by + cz + d = 0)
|
||||
plane_config = config['plane']
|
||||
self.plane_normal = np.array([
|
||||
plane_config.get('a', 0),
|
||||
plane_config.get('b', 0),
|
||||
plane_config.get('c', 1)
|
||||
])
|
||||
self.plane_d = plane_config.get('d', 0)
|
||||
|
||||
# 归一化法向量
|
||||
norm = np.linalg.norm(self.plane_normal)
|
||||
if norm > 0:
|
||||
self.plane_normal = self.plane_normal / norm
|
||||
self.plane_d = self.plane_d / norm
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行测量"""
|
||||
try:
|
||||
if self.mode == 'exact_points':
|
||||
return self._measure_exact_points()
|
||||
elif self.mode == 'bbox_filtered':
|
||||
return self._measure_bbox_filtered()
|
||||
except Exception as e:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
def _measure_exact_points(self) -> MeasurementResult:
|
||||
"""精确点模式:查找最近点并计算距离(优化版:使用Grid索引)"""
|
||||
from roi_extractor import ROIExtractor
|
||||
|
||||
timing_breakdown = {}
|
||||
|
||||
# 优化:对每个目标点单独用Grid索引提取小区域
|
||||
start_time = time.time()
|
||||
|
||||
found_points = []
|
||||
distances_to_plane = []
|
||||
|
||||
for target_point in self.target_points:
|
||||
# 定义搜索区域(目标点周围的小包围盒)
|
||||
search_margin = self.search_radius * 2
|
||||
bbox_min = target_point - search_margin
|
||||
bbox_max = target_point + search_margin
|
||||
|
||||
# 用Grid索引快速提取局部点云
|
||||
local_points, local_indices = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud, bbox_min, bbox_max, self.spatial_index
|
||||
)
|
||||
|
||||
if len(local_points) == 0:
|
||||
# 如果没找到点,扩大搜索范围
|
||||
search_margin = self.search_radius * 10
|
||||
bbox_min = target_point - search_margin
|
||||
bbox_max = target_point + search_margin
|
||||
local_points, local_indices = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud, bbox_min, bbox_max, self.spatial_index
|
||||
)
|
||||
|
||||
if len(local_points) == 0:
|
||||
print(f"警告: 目标点 {target_point} 周围 {search_margin}mm 内没有点")
|
||||
continue
|
||||
|
||||
# 在局部点云中暴力搜索最近点(点数少,很快)
|
||||
distances = np.linalg.norm(local_points - target_point, axis=1)
|
||||
min_idx = np.argmin(distances)
|
||||
min_distance = distances[min_idx]
|
||||
nearest_point = local_points[min_idx]
|
||||
|
||||
if min_distance > self.search_radius:
|
||||
print(f"警告: 目标点 {target_point} 的最近点距离 {min_distance:.2f}mm 超过搜索半径 {self.search_radius}mm")
|
||||
|
||||
found_points.append(nearest_point)
|
||||
|
||||
# 计算点到平面的距离
|
||||
distance = self._point_to_plane_distance(nearest_point)
|
||||
distances_to_plane.append(distance)
|
||||
timing_breakdown['查找最近点'] = time.time() - start_time
|
||||
|
||||
start_time = time.time()
|
||||
distances_array = np.array(distances_to_plane)
|
||||
found_points_array = np.array(found_points)
|
||||
timing_breakdown['数据整理'] = time.time() - start_time
|
||||
|
||||
# 保存找到的点为点云文件
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
ply_path = vis_dir / f"{self.name}_found_points.ply"
|
||||
Visualizer.save_roi_pointcloud(found_points_array, str(ply_path))
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data={
|
||||
'mean_distance': float(np.mean(np.abs(distances_array))),
|
||||
'std_distance': float(np.std(np.abs(distances_array))),
|
||||
'min_distance': float(np.min(np.abs(distances_array))),
|
||||
'max_distance': float(np.max(np.abs(distances_array))),
|
||||
'point_count': len(distances_array),
|
||||
'mode': 'exact_points',
|
||||
'target_points': self.target_points.tolist(),
|
||||
'found_points': [p.tolist() for p in found_points],
|
||||
'distances': distances_array.tolist(),
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
)
|
||||
|
||||
def _measure_bbox_filtered(self) -> MeasurementResult:
|
||||
"""包围盒过滤模式:提取点云,筛选特定点,计算距离"""
|
||||
# 导入ROIExtractor用于Grid索引加速
|
||||
from roi_extractor import ROIExtractor
|
||||
|
||||
timing_breakdown = {}
|
||||
all_filtered_points = []
|
||||
all_distances = []
|
||||
|
||||
start_time = time.time()
|
||||
for bbox_config in self.bboxes:
|
||||
bbox_min = np.array(bbox_config['min'])
|
||||
bbox_max = np.array(bbox_config['max'])
|
||||
|
||||
# 使用Grid索引快速提取包围盒内的点(优化后)
|
||||
bbox_points, _ = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud, bbox_min, bbox_max, self.spatial_index
|
||||
)
|
||||
|
||||
if len(bbox_points) == 0:
|
||||
print(f"警告: 包围盒 {bbox_min} 到 {bbox_max} 内没有点")
|
||||
continue
|
||||
|
||||
# 应用过滤条件
|
||||
filtered_points = self._apply_filter(bbox_points)
|
||||
|
||||
if len(filtered_points) == 0:
|
||||
print(f"警告: 包围盒 {bbox_min} 到 {bbox_max} 过滤后没有点")
|
||||
continue
|
||||
|
||||
all_filtered_points.extend(filtered_points)
|
||||
|
||||
# 计算每个点到平面的距离
|
||||
for point in filtered_points:
|
||||
distance = self._point_to_plane_distance(point)
|
||||
all_distances.append(distance)
|
||||
timing_breakdown['提取和过滤点云+距离计算'] = time.time() - start_time
|
||||
|
||||
if len(all_distances) == 0:
|
||||
raise ValueError("没有找到符合条件的点")
|
||||
|
||||
start_time = time.time()
|
||||
distances_array = np.array(all_distances)
|
||||
filtered_points_array = np.array(all_filtered_points)
|
||||
timing_breakdown['数据整理'] = time.time() - start_time
|
||||
|
||||
# 保存筛选后的点为点云文件
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
ply_path = vis_dir / f"{self.name}_filtered_points.ply"
|
||||
Visualizer.save_roi_pointcloud(filtered_points_array, str(ply_path))
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data={
|
||||
'mean_distance': float(np.mean(np.abs(distances_array))),
|
||||
'std_distance': float(np.std(np.abs(distances_array))),
|
||||
'min_distance': float(np.min(np.abs(distances_array))),
|
||||
'max_distance': float(np.max(np.abs(distances_array))),
|
||||
'point_count': len(distances_array),
|
||||
'mode': 'bbox_filtered',
|
||||
'num_bboxes': len(self.bboxes),
|
||||
'filtered_points': all_filtered_points,
|
||||
'distances': distances_array.tolist(),
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
)
|
||||
|
||||
def _apply_filter(self, points: np.ndarray) -> List[np.ndarray]:
|
||||
"""应用过滤条件"""
|
||||
filter_type = self.filter_config['type']
|
||||
|
||||
if filter_type == 'max_abs_coordinate':
|
||||
axis = self.filter_config['axis']
|
||||
top_n = self.filter_config['top_n']
|
||||
|
||||
axis_map = {'x': 0, 'y': 1, 'z': 2}
|
||||
axis_idx = axis_map[axis.lower()]
|
||||
|
||||
# 计算绝对值
|
||||
abs_values = np.abs(points[:, axis_idx])
|
||||
|
||||
# 获取top_n个最大值的索引
|
||||
if len(points) <= top_n:
|
||||
return points.tolist()
|
||||
|
||||
top_indices = np.argpartition(abs_values, -top_n)[-top_n:]
|
||||
return points[top_indices].tolist()
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown filter type: {filter_type}")
|
||||
|
||||
def _point_to_plane_distance(self, point: np.ndarray) -> float:
|
||||
"""计算点到平面的有符号距离"""
|
||||
return np.dot(self.plane_normal, point) + self.plane_d
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
点到平面距离测量(支持孔中心检测)
|
||||
支持 4_hole_center + 1_hole_center 方式
|
||||
"""
|
||||
import numpy as np
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from roi_extractor import ROIExtractor
|
||||
from visualizer import Visualizer
|
||||
from hole_center_fitter import HoleCenterFitter
|
||||
|
||||
|
||||
class PointToPlaneWithHoles(BaseMeasurement):
|
||||
"""点到平面距离测量(支持孔中心定位)"""
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行测量"""
|
||||
try:
|
||||
timing_breakdown = {}
|
||||
|
||||
# 获取boxes配置
|
||||
boxes = self.config.get('boxes', [])
|
||||
if len(boxes) < 5:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"需要至少5个box(4个定位孔+1个辅助孔),但只有{len(boxes)}个"
|
||||
)
|
||||
|
||||
# 1. 提取前4个孔的点云并拟合参考平面
|
||||
start_time = time.time()
|
||||
print(f" 提取4个定位孔的ROI区域...")
|
||||
hole_points_list = []
|
||||
for i in range(4):
|
||||
box = boxes[i]
|
||||
points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, box['min'], box['max'], self.spatial_index)
|
||||
if len(points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"定位孔 '{box['name']}' ROI区域内没有点"
|
||||
)
|
||||
hole_points_list.append(points)
|
||||
print(f" {box['name']}: {len(points)} 个点")
|
||||
|
||||
# 拟合4孔参考平面
|
||||
reference_plane_result = HoleCenterFitter.fit_4_holes_reference_plane(hole_points_list)
|
||||
plane_y = reference_plane_result['plane_y']
|
||||
timing_breakdown['4孔拟合参考平面'] = time.time() - start_time
|
||||
|
||||
# 2. 提取第5个辅助孔的点云并拟合圆心
|
||||
start_time = time.time()
|
||||
auxiliary_box = boxes[4]
|
||||
print(f" 提取辅助孔 '{auxiliary_box['name']}' ROI区域...")
|
||||
auxiliary_points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, auxiliary_box['min'], auxiliary_box['max'], self.spatial_index)
|
||||
|
||||
if len(auxiliary_points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"辅助孔 '{auxiliary_box['name']}' ROI区域内没有点"
|
||||
)
|
||||
|
||||
print(f" {auxiliary_box['name']}: {len(auxiliary_points)} 个点")
|
||||
|
||||
# 拟合辅助孔圆心
|
||||
auxiliary_result = HoleCenterFitter.fit_hole_center(auxiliary_points)
|
||||
auxiliary_center = auxiliary_result['center_3d']
|
||||
print(f" 辅助孔圆心: {auxiliary_center}")
|
||||
|
||||
timing_breakdown['辅助孔拟合'] = time.time() - start_time
|
||||
|
||||
# 3. 计算辅助孔圆心到参考平面的距离
|
||||
start_time = time.time()
|
||||
distance = HoleCenterFitter.compute_distance_to_y_plane(
|
||||
np.array(auxiliary_center),
|
||||
plane_y
|
||||
)
|
||||
timing_breakdown['距离计算'] = time.time() - start_time
|
||||
|
||||
print(f" 辅助孔圆心到参考平面的距离: {abs(distance):.2f} mm")
|
||||
|
||||
# 4. 可视化
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 保存定位孔点云
|
||||
for i, points in enumerate(hole_points_list):
|
||||
ply_path = vis_dir / f"{self.name}_locating_hole_{i+1}.ply"
|
||||
Visualizer.save_roi_pointcloud(points, str(ply_path))
|
||||
|
||||
# 保存辅助孔点云
|
||||
ply_path = vis_dir / f"{self.name}_auxiliary_hole.ply"
|
||||
Visualizer.save_roi_pointcloud(auxiliary_points, str(ply_path))
|
||||
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data={
|
||||
'mean_distance': float(abs(distance)),
|
||||
'std_distance': 0.0, # 单点测量,无标准差
|
||||
'min_distance': float(abs(distance)),
|
||||
'max_distance': float(abs(distance)),
|
||||
'point_count': 1,
|
||||
'distance': float(distance), # 带符号的距离
|
||||
'reference_plane_y': float(plane_y),
|
||||
'reference_plane_centers': reference_plane_result['centers_3d'],
|
||||
'auxiliary_center': auxiliary_center,
|
||||
'auxiliary_radius': float(auxiliary_result['radius']),
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
点到点距离测量模块
|
||||
|
||||
用于测量点云中两个指定点之间的距离
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import time
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Tuple
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
# 添加父目录到路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from visualizer import Visualizer
|
||||
|
||||
|
||||
class PointToPointDistance(BaseMeasurement):
|
||||
"""点到点距离测量"""
|
||||
|
||||
def __init__(self, name: str, point_cloud: np.ndarray, config: Dict[str, Any], spatial_index=None):
|
||||
super().__init__(name, point_cloud, config, spatial_index)
|
||||
|
||||
# 检查测量模式
|
||||
self.mode = config.get('mode', 'exact_points')
|
||||
|
||||
if self.mode == 'exact_points':
|
||||
# 点对列表,每对包含两个点的坐标
|
||||
self.point_pairs = config['point_pairs']
|
||||
self.search_radius = config.get('search_radius', 10.0)
|
||||
elif self.mode == 'bbox_filtered':
|
||||
# bbox过滤模式
|
||||
self.bboxes = config.get('bboxes', [])
|
||||
self.filter_config = config.get('filter', {})
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行测量"""
|
||||
if self.mode == 'bbox_filtered':
|
||||
return self._execute_bbox_filtered()
|
||||
else:
|
||||
return self._execute_exact_points()
|
||||
|
||||
def _execute_bbox_filtered(self) -> MeasurementResult:
|
||||
"""执行bbox过滤模式的测量"""
|
||||
try:
|
||||
from roi_extractor import ROIExtractor
|
||||
|
||||
timing_breakdown = {}
|
||||
|
||||
# 1. 从每个bbox提取点云
|
||||
start_time = time.time()
|
||||
bbox_points_list = []
|
||||
|
||||
for bbox in self.bboxes:
|
||||
points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, bbox['min'], bbox['max'], self.spatial_index)
|
||||
if len(points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"bbox '{bbox['name']}' 区域内没有点"
|
||||
)
|
||||
bbox_points_list.append({
|
||||
'name': bbox['name'],
|
||||
'points': points
|
||||
})
|
||||
print(f" {bbox['name']}: {len(points)} 个点")
|
||||
|
||||
timing_breakdown['提取bbox点云'] = time.time() - start_time
|
||||
|
||||
# 2. 根据filter配置过滤点
|
||||
start_time = time.time()
|
||||
filter_type = self.filter_config.get('type')
|
||||
axis = self.filter_config.get('axis', 'z')
|
||||
top_n = self.filter_config.get('top_n', 10)
|
||||
|
||||
axis_idx = {'x': 0, 'y': 1, 'z': 2}[axis.lower()]
|
||||
|
||||
filtered_points_list = []
|
||||
|
||||
if filter_type == 'max_abs_coordinate':
|
||||
for bbox_info in bbox_points_list:
|
||||
points = bbox_info['points']
|
||||
|
||||
# 按绝对值排序
|
||||
abs_values = np.abs(points[:, axis_idx])
|
||||
top_indices = np.argsort(abs_values)[-top_n:]
|
||||
top_points = points[top_indices]
|
||||
|
||||
filtered_points_list.append({
|
||||
'name': bbox_info['name'],
|
||||
'points': top_points,
|
||||
'values': points[top_indices, axis_idx]
|
||||
})
|
||||
|
||||
print(f" {bbox_info['name']}: 选取 {axis} 绝对值最大的 {len(top_points)} 个点")
|
||||
print(f" {axis} 范围: [{top_points[:, axis_idx].min():.2f}, {top_points[:, axis_idx].max():.2f}]")
|
||||
|
||||
timing_breakdown['过滤点'] = time.time() - start_time
|
||||
|
||||
# 3. 计算距离(两组点的坐标差)
|
||||
start_time = time.time()
|
||||
|
||||
if len(filtered_points_list) != 2:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"需要2个bbox,但有{len(filtered_points_list)}个"
|
||||
)
|
||||
|
||||
# 计算两组点在指定轴上的平均坐标
|
||||
group1_values = filtered_points_list[0]['values']
|
||||
group2_values = filtered_points_list[1]['values']
|
||||
|
||||
mean1 = np.mean(group1_values)
|
||||
mean2 = np.mean(group2_values)
|
||||
|
||||
distance = abs(mean2 - mean1)
|
||||
|
||||
print(f" 组1平均{axis}值: {mean1:.2f}")
|
||||
print(f" 组2平均{axis}值: {mean2:.2f}")
|
||||
print(f" 距离: {distance:.2f} mm")
|
||||
|
||||
timing_breakdown['距离计算'] = time.time() - start_time
|
||||
|
||||
# 4. 可视化
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for filtered_info in filtered_points_list:
|
||||
ply_path = vis_dir / f"{self.name}_{filtered_info['name']}_filtered.ply"
|
||||
Visualizer.save_roi_pointcloud(filtered_info['points'], str(ply_path))
|
||||
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data={
|
||||
'mean_distance': float(distance),
|
||||
'std_distance': 0.0,
|
||||
'min_distance': float(distance),
|
||||
'max_distance': float(distance),
|
||||
'point_count': top_n * 2,
|
||||
'group1_mean': float(mean1),
|
||||
'group2_mean': float(mean2),
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def _execute_exact_points(self) -> MeasurementResult:
|
||||
"""执行精确点模式的测量(优化版:使用Grid索引)"""
|
||||
from roi_extractor import ROIExtractor
|
||||
|
||||
try:
|
||||
timing_breakdown = {}
|
||||
|
||||
# 优化:对每个目标点用Grid索引提取小区域
|
||||
start_time = time.time()
|
||||
|
||||
pair_results = []
|
||||
all_found_points = []
|
||||
|
||||
for i, pair in enumerate(self.point_pairs):
|
||||
point1_target = np.array(pair['point1'])
|
||||
point2_target = np.array(pair['point2'])
|
||||
pair_name = pair.get('name', f'点对{i+1}')
|
||||
|
||||
# 为point1找最近点
|
||||
search_margin = self.search_radius * 2
|
||||
bbox_min = point1_target - search_margin
|
||||
bbox_max = point1_target + search_margin
|
||||
|
||||
local_points, _ = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud, bbox_min, bbox_max, self.spatial_index
|
||||
)
|
||||
|
||||
if len(local_points) == 0:
|
||||
search_margin = self.search_radius * 10
|
||||
bbox_min = point1_target - search_margin
|
||||
bbox_max = point1_target + search_margin
|
||||
local_points, _ = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud, bbox_min, bbox_max, self.spatial_index
|
||||
)
|
||||
|
||||
if len(local_points) > 0:
|
||||
distances_to_p1 = np.linalg.norm(local_points - point1_target, axis=1)
|
||||
local_idx1 = np.argmin(distances_to_p1)
|
||||
dist1 = distances_to_p1[local_idx1]
|
||||
point1_found = local_points[local_idx1]
|
||||
else:
|
||||
print(f"警告: {pair_name} - 点1 {point1_target} 附近没有点")
|
||||
continue
|
||||
|
||||
# 为point2找最近点
|
||||
search_margin = self.search_radius * 2
|
||||
bbox_min = point2_target - search_margin
|
||||
bbox_max = point2_target + search_margin
|
||||
|
||||
local_points, _ = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud, bbox_min, bbox_max, self.spatial_index
|
||||
)
|
||||
|
||||
if len(local_points) == 0:
|
||||
search_margin = self.search_radius * 10
|
||||
bbox_min = point2_target - search_margin
|
||||
bbox_max = point2_target + search_margin
|
||||
local_points, _ = ROIExtractor.extract_bbox_points(
|
||||
self.point_cloud, bbox_min, bbox_max, self.spatial_index
|
||||
)
|
||||
|
||||
if len(local_points) > 0:
|
||||
distances_to_p2 = np.linalg.norm(local_points - point2_target, axis=1)
|
||||
local_idx2 = np.argmin(distances_to_p2)
|
||||
dist2 = distances_to_p2[local_idx2]
|
||||
point2_found = local_points[local_idx2]
|
||||
else:
|
||||
print(f"警告: {pair_name} - 点2 {point2_target} 附近没有点")
|
||||
continue
|
||||
|
||||
if dist1 > self.search_radius:
|
||||
print(f"警告: {pair_name} - 点1 {point1_target} 的最近点距离 {dist1:.2f}mm 超过搜索半径 {self.search_radius}mm")
|
||||
if dist2 > self.search_radius:
|
||||
print(f"警告: {pair_name} - 点2 {point2_target} 的最近点距离 {dist2:.2f}mm 超过搜索半径 {self.search_radius}mm")
|
||||
|
||||
# 计算两点之间的欧氏距离
|
||||
distance = np.linalg.norm(point2_found - point1_found)
|
||||
|
||||
pair_results.append({
|
||||
'name': pair_name,
|
||||
'point1_target': point1_target.tolist(),
|
||||
'point2_target': point2_target.tolist(),
|
||||
'point1_found': point1_found.tolist(),
|
||||
'point2_found': point2_found.tolist(),
|
||||
'point1_search_distance': float(dist1),
|
||||
'point2_search_distance': float(dist2),
|
||||
'distance': float(distance)
|
||||
})
|
||||
|
||||
all_found_points.extend([point1_found, point2_found])
|
||||
timing_breakdown['查找点对+距离计算'] = time.time() - start_time
|
||||
|
||||
# 统计所有距离
|
||||
start_time = time.time()
|
||||
distances = np.array([pr['distance'] for pr in pair_results])
|
||||
mean_distance = float(np.mean(distances))
|
||||
std_distance = float(np.std(distances))
|
||||
min_distance = float(np.min(distances))
|
||||
max_distance = float(np.max(distances))
|
||||
timing_breakdown['数据整理'] = time.time() - start_time
|
||||
|
||||
# 检查是否启用差值比较模式
|
||||
compare_mode = self.config.get('compare_mode')
|
||||
comparison_result = None
|
||||
|
||||
if compare_mode == 'difference':
|
||||
compare_pairs = self.config.get('compare_pairs', [])
|
||||
difference_tolerance = self.config.get('difference_tolerance', 0)
|
||||
|
||||
if len(compare_pairs) != 2:
|
||||
print(f"警告: compare_mode='difference' 需要指定2个点对索引,但得到 {len(compare_pairs)} 个")
|
||||
elif max(compare_pairs) >= len(pair_results):
|
||||
print(f"警告: compare_pairs 索引超出范围(最大索引: {len(pair_results)-1})")
|
||||
else:
|
||||
# 获取两组点对的距离
|
||||
idx1, idx2 = compare_pairs[0], compare_pairs[1]
|
||||
distance1 = pair_results[idx1]['distance']
|
||||
distance2 = pair_results[idx2]['distance']
|
||||
|
||||
# 计算差值
|
||||
difference = abs(distance1 - distance2)
|
||||
|
||||
# 判断是否通过
|
||||
passed = difference <= difference_tolerance
|
||||
|
||||
comparison_result = {
|
||||
'pair1_index': idx1,
|
||||
'pair1_name': pair_results[idx1]['name'],
|
||||
'pair1_distance': distance1,
|
||||
'pair2_index': idx2,
|
||||
'pair2_name': pair_results[idx2]['name'],
|
||||
'pair2_distance': distance2,
|
||||
'difference': float(difference),
|
||||
'tolerance': float(difference_tolerance),
|
||||
'passed': passed
|
||||
}
|
||||
|
||||
print(f"\n 差值比较结果:")
|
||||
print(f" {pair_results[idx1]['name']}: {distance1:.4f} mm")
|
||||
print(f" {pair_results[idx2]['name']}: {distance2:.4f} mm")
|
||||
print(f" 差值: {difference:.4f} mm")
|
||||
print(f" 公差: ≤{difference_tolerance:.2f} mm")
|
||||
status = "✅ 通过" if passed else "❌ 不通过"
|
||||
print(f" 判定: {status}")
|
||||
|
||||
# 保存找到的点为点云文件
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 保存所有找到的点
|
||||
all_points_array = np.array(all_found_points)
|
||||
ply_path = vis_dir / f"{self.name}_found_points.ply"
|
||||
Visualizer.save_roi_pointcloud(all_points_array, str(ply_path))
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
result_data = {
|
||||
'mean_distance': mean_distance,
|
||||
'std_distance': std_distance,
|
||||
'min_distance': min_distance,
|
||||
'max_distance': max_distance,
|
||||
'pair_count': len(pair_results),
|
||||
'pair_results': pair_results,
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
|
||||
# 添加差值比较结果(如果有)
|
||||
if comparison_result:
|
||||
result_data['comparison'] = comparison_result
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data=result_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
点到点距离测量(支持孔中心检测)
|
||||
支持 hole_center 方式计算同侧孔中心的距离
|
||||
"""
|
||||
import numpy as np
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from measurements.base import BaseMeasurement, MeasurementResult
|
||||
from roi_extractor import ROIExtractor
|
||||
from visualizer import Visualizer
|
||||
from hole_center_fitter import HoleCenterFitter
|
||||
|
||||
|
||||
class PointToPointWithHoles(BaseMeasurement):
|
||||
"""点到点距离测量(支持孔中心定位)"""
|
||||
|
||||
def execute(self) -> MeasurementResult:
|
||||
"""执行测量"""
|
||||
try:
|
||||
timing_breakdown = {}
|
||||
|
||||
# 获取boxes配置
|
||||
boxes = self.config.get('boxes', [])
|
||||
if len(boxes) != 4:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"需要4个box(左前、右前、左后、右后),但有{len(boxes)}个"
|
||||
)
|
||||
|
||||
# 1. 提取4个孔的点云并拟合圆心
|
||||
start_time = time.time()
|
||||
print(f" 提取4个孔的ROI区域并拟合圆心...")
|
||||
|
||||
# 记录到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log(f" 提取4个孔的ROI区域并拟合圆心...")
|
||||
|
||||
hole_centers = []
|
||||
hole_names = []
|
||||
hole_results = [] # 保存完整的拟合结果
|
||||
|
||||
for i, box in enumerate(boxes):
|
||||
points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, box['min'], box['max'], self.spatial_index)
|
||||
if len(points) == 0:
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"孔 '{box['name']}' ROI区域内没有点"
|
||||
)
|
||||
|
||||
print(f" {box['name']}: {len(points)} 个点")
|
||||
|
||||
# 拟合圆心
|
||||
result = HoleCenterFitter.fit_hole_center(points)
|
||||
hole_centers.append(result['center_3d'])
|
||||
hole_names.append(box['name'])
|
||||
hole_results.append(result)
|
||||
print(f" 圆心: {result['center_3d']}, 半径: {result['radius']:.2f} mm")
|
||||
|
||||
# 记录圆孔拟合结果到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log_circle_fitting(
|
||||
f"孔 '{box['name']}'",
|
||||
tuple(result['center_3d']),
|
||||
result['radius'],
|
||||
len(points)
|
||||
)
|
||||
|
||||
timing_breakdown['4孔拟合'] = time.time() - start_time
|
||||
|
||||
# 2. 计算同侧孔中心的X差值
|
||||
# 假设顺序:左前、右前、左后、右后
|
||||
# 左侧距离:左前(0) - 左后(2) 的X差值
|
||||
# 右侧距离:右前(1) - 右后(3) 的X差值
|
||||
start_time = time.time()
|
||||
|
||||
left_front = np.array(hole_centers[0])
|
||||
right_front = np.array(hole_centers[1])
|
||||
left_rear = np.array(hole_centers[2])
|
||||
right_rear = np.array(hole_centers[3])
|
||||
|
||||
# 计算X方向距离(取绝对值)
|
||||
left_distance = abs(left_front[0] - left_rear[0])
|
||||
right_distance = abs(right_front[0] - right_rear[0])
|
||||
|
||||
print(f" 左侧距离({hole_names[0]} - {hole_names[2]}): {left_distance:.2f} mm")
|
||||
print(f" 右侧距离({hole_names[1]} - {hole_names[3]}): {right_distance:.2f} mm")
|
||||
|
||||
# 计算平均值
|
||||
mean_distance = (left_distance + right_distance) / 2.0
|
||||
min_distance = min(left_distance, right_distance)
|
||||
max_distance = max(left_distance, right_distance)
|
||||
std_distance = np.std([left_distance, right_distance])
|
||||
|
||||
print(f" 平均距离: {mean_distance:.2f} mm")
|
||||
|
||||
# 记录距离计算结果到详细日志
|
||||
if hasattr(self, 'detail_logger') and self.detail_logger:
|
||||
self.detail_logger.log("")
|
||||
self.detail_logger.log(f" 孔中心距离计算:")
|
||||
self.detail_logger.log(f" {hole_names[0]} X坐标: {left_front[0]:.4f} mm")
|
||||
self.detail_logger.log(f" {hole_names[2]} X坐标: {left_rear[0]:.4f} mm")
|
||||
self.detail_logger.log(f" 左侧距离: {left_distance:.4f} mm")
|
||||
self.detail_logger.log("")
|
||||
self.detail_logger.log(f" {hole_names[1]} X坐标: {right_front[0]:.4f} mm")
|
||||
self.detail_logger.log(f" {hole_names[3]} X坐标: {right_rear[0]:.4f} mm")
|
||||
self.detail_logger.log(f" 右侧距离: {right_distance:.4f} mm")
|
||||
self.detail_logger.log("")
|
||||
self.detail_logger.log_distance_result(mean_distance, std_distance, min_distance, max_distance)
|
||||
|
||||
timing_breakdown['距离计算'] = time.time() - start_time
|
||||
|
||||
# 3. 可视化
|
||||
start_time = time.time()
|
||||
visualization_dir = self.config.get('visualization_dir')
|
||||
if visualization_dir:
|
||||
vis_dir = Path(visualization_dir)
|
||||
vis_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 保存每个孔的点云
|
||||
for i, box in enumerate(boxes):
|
||||
points, _ = ROIExtractor.extract_bbox_points(self.point_cloud, box['min'], box['max'], self.spatial_index)
|
||||
ply_path = vis_dir / f"{self.name}_{box['name']}.ply"
|
||||
Visualizer.save_roi_pointcloud(points, str(ply_path))
|
||||
|
||||
timing_breakdown['可视化保存'] = time.time() - start_time
|
||||
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=True,
|
||||
data={
|
||||
'mean_distance': float(mean_distance),
|
||||
'std_distance': float(std_distance),
|
||||
'min_distance': float(min_distance),
|
||||
'max_distance': float(max_distance),
|
||||
'point_count': 2, # 测量了2个距离值
|
||||
'left_distance': float(left_distance),
|
||||
'right_distance': float(right_distance),
|
||||
'hole_centers': [c.tolist() for c in hole_centers],
|
||||
'hole_names': hole_names,
|
||||
'timing_breakdown': timing_breakdown
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
return MeasurementResult(
|
||||
task_name=self.name,
|
||||
task_type=self.task_type,
|
||||
success=False,
|
||||
error_message=f"执行过程中发生错误: {str(e)}\n{traceback.format_exc()}"
|
||||
)
|
||||
208
alg/train_measurement_system_v3.2_0629/src/performance_logger.py
Normal file
208
alg/train_measurement_system_v3.2_0629/src/performance_logger.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""性能日志记录器 - 用于分析优化"""
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class PerformanceLogger:
|
||||
"""性能日志记录器"""
|
||||
|
||||
def __init__(self, output_dir: str = "results"):
|
||||
"""
|
||||
初始化性能日志记录器
|
||||
|
||||
Args:
|
||||
output_dir: 输出目录路径
|
||||
"""
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成日志文件名
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
self.log_file = self.output_dir / f"performance_{timestamp}.log"
|
||||
|
||||
# 任务性能记录
|
||||
self.task_records = []
|
||||
|
||||
# 初始化阶段性能记录
|
||||
self.init_phases = {
|
||||
'pointcloud_loading': {'time': 0, 'points': 0, 'file': ''},
|
||||
'spatial_index': {'time': 0, 'method': '', 'info': {}}
|
||||
}
|
||||
|
||||
def log_task(self, task_name: str, total_time: float, timing_breakdown: Dict[str, float]):
|
||||
"""
|
||||
记录单个任务的性能数据
|
||||
|
||||
Args:
|
||||
task_name: 任务名称
|
||||
total_time: 总执行时间(秒)
|
||||
timing_breakdown: 各步骤耗时字典 {步骤名: 耗时(秒)}
|
||||
"""
|
||||
record = {
|
||||
'task_name': task_name,
|
||||
'total_time': total_time,
|
||||
'breakdown': timing_breakdown
|
||||
}
|
||||
self.task_records.append(record)
|
||||
|
||||
def log_pointcloud_loading(self, time_seconds: float, point_count: int, filename: str):
|
||||
"""
|
||||
记录点云加载性能
|
||||
|
||||
Args:
|
||||
time_seconds: 加载耗时(秒)
|
||||
point_count: 点云数量
|
||||
filename: 文件名
|
||||
"""
|
||||
self.init_phases['pointcloud_loading'] = {
|
||||
'time': time_seconds,
|
||||
'points': point_count,
|
||||
'file': filename
|
||||
}
|
||||
|
||||
def log_spatial_index(self, time_seconds: float, method: str, info: Dict):
|
||||
"""
|
||||
记录空间索引构建性能
|
||||
|
||||
Args:
|
||||
time_seconds: 构建耗时(秒)
|
||||
method: 索引方法名称
|
||||
info: 索引详细信息
|
||||
"""
|
||||
self.init_phases['spatial_index'] = {
|
||||
'time': time_seconds,
|
||||
'method': method,
|
||||
'info': info
|
||||
}
|
||||
|
||||
def save(self):
|
||||
"""保存性能日志到文件"""
|
||||
with open(self.log_file, 'w', encoding='utf-8') as f:
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write("点云测量系统 - 性能分析报告\n")
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
# 输出初始化阶段性能
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write("初始化阶段性能\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
# 点云加载
|
||||
pc_loading = self.init_phases['pointcloud_loading']
|
||||
if pc_loading['time'] > 0:
|
||||
f.write("阶段1: 点云加载\n")
|
||||
f.write("-" * 80 + "\n")
|
||||
f.write(f" 文件名: {pc_loading['file']}\n")
|
||||
f.write(f" 点云数量: {pc_loading['points']:,} 个点\n")
|
||||
f.write(f" 加载耗时: {pc_loading['time']:.2f} 秒\n")
|
||||
if pc_loading['points'] > 0:
|
||||
f.write(f" 加载速度: {pc_loading['points']/pc_loading['time']/1000000:.2f} M点/秒\n")
|
||||
f.write("\n")
|
||||
|
||||
# 空间索引构建
|
||||
spatial_idx = self.init_phases['spatial_index']
|
||||
if spatial_idx['time'] > 0:
|
||||
f.write("阶段2: 空间索引构建\n")
|
||||
f.write("-" * 80 + "\n")
|
||||
f.write(f" 方法: {spatial_idx['method']}\n")
|
||||
f.write(f" 构建耗时: {spatial_idx['time']:.2f} 秒\n")
|
||||
info = spatial_idx['info']
|
||||
if 'grid_count' in info:
|
||||
f.write(f" 网格数量: {info['grid_count']:,}\n")
|
||||
f.write(f" 平均点/格: {info['avg_points_per_grid']:.1f}\n")
|
||||
elif 'node_count' in info:
|
||||
f.write(f" 节点数量: {info['node_count']:,}\n")
|
||||
f.write("\n")
|
||||
|
||||
# 初始化总耗时
|
||||
init_total = pc_loading['time'] + spatial_idx['time']
|
||||
if init_total > 0:
|
||||
f.write(f"初始化阶段总耗时: {init_total:.2f} 秒\n\n")
|
||||
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
# 任务执行性能
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write("任务执行性能\n")
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write(f"总任务数: {len(self.task_records)}\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
# 计算总时间
|
||||
total_all_tasks = sum(r['total_time'] for r in self.task_records)
|
||||
f.write(f"所有任务总耗时: {total_all_tasks:.2f} 秒\n\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
# 逐个任务输出
|
||||
for i, record in enumerate(self.task_records, 1):
|
||||
task_name = record['task_name']
|
||||
total_time = record['total_time']
|
||||
breakdown = record['breakdown']
|
||||
|
||||
f.write(f"[{i}/{len(self.task_records)}] {task_name}\n")
|
||||
f.write("-" * 80 + "\n")
|
||||
f.write(f"总耗时: {total_time:.2f} 秒 ({total_time/total_all_tasks*100:.1f}% 占比)\n\n")
|
||||
|
||||
if breakdown:
|
||||
f.write("详细时间分布:\n")
|
||||
f.write(f"{'步骤':<40} {'耗时 (秒)':<12} {'占比':<10}\n")
|
||||
f.write("-" * 80 + "\n")
|
||||
|
||||
for step_name, step_time in breakdown.items():
|
||||
percentage = (step_time / total_time * 100) if total_time > 0 else 0
|
||||
f.write(f" {step_name:<38} {step_time:>8.2f} {percentage:>6.1f}%\n")
|
||||
|
||||
f.write("\n" + "=" * 80 + "\n\n")
|
||||
|
||||
# 汇总统计
|
||||
f.write("\n" + "=" * 80 + "\n")
|
||||
f.write("步骤耗时汇总(所有任务平均)\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
# 收集所有步骤的耗时
|
||||
step_totals = {}
|
||||
step_counts = {}
|
||||
|
||||
for record in self.task_records:
|
||||
for step_name, step_time in record['breakdown'].items():
|
||||
if step_name not in step_totals:
|
||||
step_totals[step_name] = 0
|
||||
step_counts[step_name] = 0
|
||||
step_totals[step_name] += step_time
|
||||
step_counts[step_name] += 1
|
||||
|
||||
# 计算平均值并排序
|
||||
step_averages = [(name, step_totals[name] / step_counts[name])
|
||||
for name in step_totals.keys()]
|
||||
step_averages.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
f.write(f"{'步骤':<40} {'平均耗时 (秒)':<15} {'出现次数':<10}\n")
|
||||
f.write("-" * 80 + "\n")
|
||||
|
||||
for step_name, avg_time in step_averages:
|
||||
count = step_counts[step_name]
|
||||
f.write(f"{step_name:<40} {avg_time:>10.2f} {count:>5}\n")
|
||||
|
||||
f.write("\n" + "=" * 80 + "\n")
|
||||
|
||||
# 总体性能摘要
|
||||
grand_total = init_total + total_all_tasks
|
||||
f.write("\n" + "=" * 80 + "\n")
|
||||
f.write("总体性能摘要\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
f.write(f" 初始化阶段: {init_total:.2f} 秒 ({init_total/grand_total*100:.1f}%)\n")
|
||||
f.write(f" - 点云加载: {pc_loading['time']:.2f} 秒\n")
|
||||
f.write(f" - 空间索引: {spatial_idx['time']:.2f} 秒\n")
|
||||
f.write(f" 任务执行阶段: {total_all_tasks:.2f} 秒 ({total_all_tasks/grand_total*100:.1f}%)\n")
|
||||
f.write(f" 总耗时: {grand_total:.2f} 秒\n")
|
||||
|
||||
f.write("\n" + "=" * 80 + "\n")
|
||||
f.write(f"日志已保存到: {self.log_file}\n")
|
||||
f.write("=" * 80 + "\n")
|
||||
|
||||
print(f"\n性能日志已保存到: {self.log_file}")
|
||||
return str(self.log_file)
|
||||
169
alg/train_measurement_system_v3.2_0629/src/plane_fitter.py
Normal file
169
alg/train_measurement_system_v3.2_0629/src/plane_fitter.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""平面拟合模块 - 使用RANSAC算法拟合平面"""
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlaneModel:
|
||||
"""平面模型"""
|
||||
normal: np.ndarray # 法向量 (3,)
|
||||
point: np.ndarray # 平面上的点 (3,)
|
||||
d: float # 平面方程 ax+by+cz+d=0 中的d
|
||||
inliers: np.ndarray # 内点索引
|
||||
inlier_ratio: float # 内点比例
|
||||
|
||||
def distance_to_point(self, point: np.ndarray) -> float:
|
||||
"""计算点到平面的有向距离"""
|
||||
return np.dot(self.normal, point) + self.d
|
||||
|
||||
def distance_to_points(self, points: np.ndarray) -> np.ndarray:
|
||||
"""计算多个点到平面的有向距离"""
|
||||
return np.dot(points, self.normal) + self.d
|
||||
|
||||
|
||||
class PlaneFitter:
|
||||
"""RANSAC平面拟合器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
threshold: float = 2.0,
|
||||
max_iterations: int = 1000,
|
||||
min_inlier_ratio: float = 0.6
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
threshold: 内点距离阈值(mm)
|
||||
max_iterations: 最大迭代次数
|
||||
min_inlier_ratio: 最小内点比例
|
||||
"""
|
||||
self.threshold = threshold
|
||||
self.max_iterations = max_iterations
|
||||
self.min_inlier_ratio = min_inlier_ratio
|
||||
|
||||
def fit(self, points: np.ndarray) -> Optional[PlaneModel]:
|
||||
"""
|
||||
使用RANSAC拟合平面
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
|
||||
Returns:
|
||||
PlaneModel或None(拟合失败)
|
||||
"""
|
||||
if len(points) < 3:
|
||||
return None
|
||||
|
||||
best_inliers = None
|
||||
best_model = None
|
||||
best_inlier_count = 0
|
||||
|
||||
for _ in range(self.max_iterations):
|
||||
# 随机选择3个点
|
||||
sample_indices = np.random.choice(len(points), 3, replace=False)
|
||||
sample_points = points[sample_indices]
|
||||
|
||||
# 计算平面参数
|
||||
model = self._fit_plane_from_points(sample_points)
|
||||
if model is None:
|
||||
continue
|
||||
|
||||
# 计算所有点到平面的距离
|
||||
distances = np.abs(model.distance_to_points(points))
|
||||
|
||||
# 找到内点
|
||||
inliers = np.where(distances < self.threshold)[0]
|
||||
|
||||
if len(inliers) > best_inlier_count:
|
||||
best_inlier_count = len(inliers)
|
||||
best_inliers = inliers
|
||||
best_model = model
|
||||
|
||||
if best_inliers is None or len(best_inliers) < 3:
|
||||
return None
|
||||
|
||||
# 使用所有内点重新拟合平面
|
||||
inlier_points = points[best_inliers]
|
||||
final_model = self._fit_plane_least_squares(inlier_points)
|
||||
|
||||
if final_model is None:
|
||||
return None
|
||||
|
||||
final_model.inliers = best_inliers
|
||||
final_model.inlier_ratio = len(best_inliers) / len(points)
|
||||
|
||||
# 检查内点比例
|
||||
if final_model.inlier_ratio < self.min_inlier_ratio:
|
||||
return None
|
||||
|
||||
return final_model
|
||||
|
||||
def _fit_plane_from_points(self, points: np.ndarray) -> Optional[PlaneModel]:
|
||||
"""从3个点拟合平面"""
|
||||
if len(points) != 3:
|
||||
return None
|
||||
|
||||
# 计算两个向量
|
||||
v1 = points[1] - points[0]
|
||||
v2 = points[2] - points[0]
|
||||
|
||||
# 法向量 = v1 × v2
|
||||
normal = np.cross(v1, v2)
|
||||
norm = np.linalg.norm(normal)
|
||||
|
||||
if norm < 1e-6:
|
||||
return None
|
||||
|
||||
normal = normal / norm
|
||||
|
||||
# 平面方程: n·(p - p0) = 0 => n·p = n·p0 => ax+by+cz+d=0, d=-n·p0
|
||||
d = -np.dot(normal, points[0])
|
||||
|
||||
return PlaneModel(
|
||||
normal=normal,
|
||||
point=points[0],
|
||||
d=d,
|
||||
inliers=np.array([]),
|
||||
inlier_ratio=0.0
|
||||
)
|
||||
|
||||
def _fit_plane_least_squares(self, points: np.ndarray) -> Optional[PlaneModel]:
|
||||
"""使用最小二乘法拟合平面"""
|
||||
if len(points) < 3:
|
||||
return None
|
||||
|
||||
# 如果点数过多,降采样以避免内存爆炸
|
||||
# SVD需要O(n^2)内存,超过10000个点会导致内存问题
|
||||
max_points_for_svd = 10000
|
||||
if len(points) > max_points_for_svd:
|
||||
indices = np.random.choice(len(points), max_points_for_svd, replace=False)
|
||||
points_for_fit = points[indices]
|
||||
else:
|
||||
points_for_fit = points
|
||||
|
||||
# 计算质心
|
||||
centroid = np.mean(points_for_fit, axis=0)
|
||||
|
||||
# 中心化点云
|
||||
centered = points_for_fit - centroid
|
||||
|
||||
# SVD分解
|
||||
try:
|
||||
_, _, vh = np.linalg.svd(centered)
|
||||
normal = vh[2] # 最小奇异值对应的向量
|
||||
|
||||
# 确保法向量指向正方向(可选)
|
||||
if normal[2] < 0:
|
||||
normal = -normal
|
||||
|
||||
d = -np.dot(normal, centroid)
|
||||
|
||||
return PlaneModel(
|
||||
normal=normal,
|
||||
point=centroid,
|
||||
d=d,
|
||||
inliers=np.array([]),
|
||||
inlier_ratio=0.0
|
||||
)
|
||||
except np.linalg.LinAlgError:
|
||||
return None
|
||||
111
alg/train_measurement_system_v3.2_0629/src/pointcloud_loader.py
Normal file
111
alg/train_measurement_system_v3.2_0629/src/pointcloud_loader.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""点云加载模块"""
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
import struct
|
||||
|
||||
|
||||
class PointCloudLoader:
|
||||
"""加载点云文件"""
|
||||
|
||||
@staticmethod
|
||||
def load_ply(file_path: str) -> np.ndarray:
|
||||
"""
|
||||
加载PLY格式点云
|
||||
|
||||
Args:
|
||||
file_path: PLY文件路径
|
||||
|
||||
Returns:
|
||||
np.ndarray: 点云数组 (N, 3)
|
||||
"""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"点云文件不存在: {file_path}")
|
||||
|
||||
print(f"正在加载点云: {file_path}")
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
# 读取头部
|
||||
header_lines = []
|
||||
while True:
|
||||
line = f.readline().decode('ascii').strip()
|
||||
header_lines.append(line)
|
||||
if line == 'end_header':
|
||||
break
|
||||
|
||||
# 解析头部
|
||||
vertex_count = 0
|
||||
format_type = None
|
||||
properties = []
|
||||
|
||||
for line in header_lines:
|
||||
if line.startswith('element vertex'):
|
||||
vertex_count = int(line.split()[-1])
|
||||
elif line.startswith('format'):
|
||||
format_type = line.split()[1]
|
||||
elif line.startswith('property'):
|
||||
parts = line.split()
|
||||
properties.append({
|
||||
'type': parts[1],
|
||||
'name': parts[2]
|
||||
})
|
||||
|
||||
if vertex_count == 0:
|
||||
raise ValueError("PLY文件中没有顶点数据")
|
||||
|
||||
# 读取顶点数据
|
||||
if format_type == 'binary_little_endian':
|
||||
points = PointCloudLoader._read_binary_ply(f, vertex_count, properties)
|
||||
elif format_type == 'ascii':
|
||||
points = PointCloudLoader._read_ascii_ply(f, vertex_count)
|
||||
else:
|
||||
raise ValueError(f"不支持的PLY格式: {format_type}")
|
||||
|
||||
print(f" 加载完成: {len(points)} 个点")
|
||||
return points
|
||||
|
||||
@staticmethod
|
||||
def _read_binary_ply(f, vertex_count: int, properties: list) -> np.ndarray:
|
||||
"""读取二进制PLY数据(优化版:批量读取)"""
|
||||
# 构建numpy dtype
|
||||
type_map = {
|
||||
'float': np.float32,
|
||||
'double': np.float64,
|
||||
'uchar': np.uint8,
|
||||
'uint': np.uint32,
|
||||
'int': np.int32
|
||||
}
|
||||
|
||||
# 构建结构化数组的dtype
|
||||
dtype_list = []
|
||||
for prop in properties:
|
||||
dtype_list.append((prop['name'], type_map.get(prop['type'], np.float32)))
|
||||
|
||||
dtype = np.dtype(dtype_list)
|
||||
|
||||
# 一次性读取所有数据
|
||||
raw_data = np.fromfile(f, dtype=dtype, count=vertex_count)
|
||||
|
||||
# 提取xyz坐标(假设前3个属性是x, y, z)
|
||||
# 如果属性名不是x,y,z,可以通过索引访问
|
||||
if 'x' in raw_data.dtype.names and 'y' in raw_data.dtype.names and 'z' in raw_data.dtype.names:
|
||||
points = np.column_stack([raw_data['x'], raw_data['y'], raw_data['z']])
|
||||
else:
|
||||
# 假设前3个字段是xyz
|
||||
points = np.column_stack([raw_data[raw_data.dtype.names[0]],
|
||||
raw_data[raw_data.dtype.names[1]],
|
||||
raw_data[raw_data.dtype.names[2]]])
|
||||
|
||||
return points.astype(np.float64)
|
||||
|
||||
@staticmethod
|
||||
def _read_ascii_ply(f, vertex_count: int) -> np.ndarray:
|
||||
"""读取ASCII PLY数据(优化版:批量读取)"""
|
||||
# 读取剩余所有行
|
||||
remaining = f.read().decode('ascii')
|
||||
|
||||
# 使用numpy的快速文本解析
|
||||
from io import StringIO
|
||||
points = np.loadtxt(StringIO(remaining), dtype=np.float64, max_rows=vertex_count, usecols=(0, 1, 2))
|
||||
|
||||
return points
|
||||
65
alg/train_measurement_system_v3.2_0629/src/roi_extractor.py
Normal file
65
alg/train_measurement_system_v3.2_0629/src/roi_extractor.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""ROI提取模块 - 从点云中提取包围盒区域的点"""
|
||||
import numpy as np
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
class ROIExtractor:
|
||||
"""从点云中提取指定包围盒区域的点"""
|
||||
|
||||
@staticmethod
|
||||
def extract_bbox_points(
|
||||
points: np.ndarray,
|
||||
bbox_min: list,
|
||||
bbox_max: list,
|
||||
spatial_index=None
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
提取包围盒内的点
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
bbox_min: 包围盒最小坐标 [x_min, y_min, z_min]
|
||||
bbox_max: 包围盒最大坐标 [x_max, y_max, z_max]
|
||||
spatial_index: 空间索引对象(可选)
|
||||
|
||||
Returns:
|
||||
roi_points: 提取的点 (M, 3)
|
||||
indices: 原始点云中的索引 (M,)
|
||||
"""
|
||||
bbox_min = np.array(bbox_min)
|
||||
bbox_max = np.array(bbox_max)
|
||||
|
||||
if spatial_index is not None:
|
||||
# 使用空间索引快速查询
|
||||
indices = spatial_index.query_bbox(bbox_min, bbox_max)
|
||||
else:
|
||||
# 回退到线性扫描
|
||||
mask = np.all((points >= bbox_min) & (points <= bbox_max), axis=1)
|
||||
indices = np.where(mask)[0]
|
||||
|
||||
roi_points = points[indices]
|
||||
return roi_points, indices
|
||||
|
||||
@staticmethod
|
||||
def sample_points(points: np.ndarray, n_samples: int, seed: Optional[int] = 42) -> np.ndarray:
|
||||
"""
|
||||
从点云中随机采样点
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
n_samples: 采样点数
|
||||
seed: 随机种子
|
||||
|
||||
Returns:
|
||||
sampled_points: 采样后的点 (n_samples, 3)
|
||||
"""
|
||||
if len(points) == 0:
|
||||
return np.array([]).reshape(0, 3)
|
||||
|
||||
n_samples = min(n_samples, len(points))
|
||||
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
|
||||
indices = np.random.choice(len(points), size=n_samples, replace=False)
|
||||
return points[indices]
|
||||
@@ -0,0 +1,17 @@
|
||||
"""空间索引模块 - 提供多种ROI提取方法"""
|
||||
|
||||
from .base import SpatialIndex
|
||||
from .linear_scan import LinearScanIndex
|
||||
from .grid_index import GridIndex
|
||||
from .grid_index_optimized import GridIndexOptimized
|
||||
from .octree_index import OctreeIndex
|
||||
from .kdtree_index import KDTreeIndex
|
||||
|
||||
__all__ = [
|
||||
'SpatialIndex',
|
||||
'LinearScanIndex',
|
||||
'GridIndex',
|
||||
'GridIndexOptimized',
|
||||
'OctreeIndex',
|
||||
'KDTreeIndex',
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
"""空间索引基类"""
|
||||
from abc import ABC, abstractmethod
|
||||
import numpy as np
|
||||
|
||||
|
||||
class SpatialIndex(ABC):
|
||||
"""空间索引抽象基类"""
|
||||
|
||||
def __init__(self, points: np.ndarray):
|
||||
"""
|
||||
初始化索引
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
"""
|
||||
self.points = points
|
||||
self.build_time = 0.0 # 构建时间(秒)
|
||||
|
||||
@abstractmethod
|
||||
def query_bbox(self, bbox_min: np.ndarray, bbox_max: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
查询包围盒内的点
|
||||
|
||||
Args:
|
||||
bbox_min: [x_min, y_min, z_min]
|
||||
bbox_max: [x_max, y_max, z_max]
|
||||
|
||||
Returns:
|
||||
包围盒内的点的索引数组
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_info(self) -> dict:
|
||||
"""返回索引信息(用于日志)"""
|
||||
return {
|
||||
'type': self.__class__.__name__,
|
||||
'point_count': len(self.points),
|
||||
'build_time': self.build_time
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Grid/Voxel 网格索引"""
|
||||
import numpy as np
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from .base import SpatialIndex
|
||||
|
||||
|
||||
class GridIndex(SpatialIndex):
|
||||
"""Grid/Voxel 空间索引 - O(N)构建, O(1)查询"""
|
||||
|
||||
def __init__(self, points: np.ndarray, grid_size: float = 100.0):
|
||||
"""
|
||||
构建网格索引
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
grid_size: 网格大小(毫米)
|
||||
"""
|
||||
super().__init__(points)
|
||||
self.grid_size = grid_size
|
||||
|
||||
print(f" 构建Grid索引 (grid_size={grid_size}mm)...")
|
||||
start = time.time()
|
||||
|
||||
# 计算点云边界
|
||||
self.mins = points.min(axis=0)
|
||||
self.maxs = points.max(axis=0)
|
||||
|
||||
# 将每个点映射到网格坐标
|
||||
grid_coords = np.floor((points - self.mins) / grid_size).astype(np.int32)
|
||||
|
||||
# 构建索引:grid_key → point_indices
|
||||
self.grid = defaultdict(list)
|
||||
for idx, (gx, gy, gz) in enumerate(grid_coords):
|
||||
grid_key = (gx, gy, gz)
|
||||
self.grid[grid_key].append(idx)
|
||||
|
||||
# 转换为numpy数组以加速查询
|
||||
for key in self.grid:
|
||||
self.grid[key] = np.array(self.grid[key], dtype=np.int32)
|
||||
|
||||
self.build_time = time.time() - start
|
||||
print(f" 网格数量: {len(self.grid):,}")
|
||||
print(f" 平均点/格: {len(points) / len(self.grid):.1f}")
|
||||
print(f" 构建耗时: {self.build_time:.2f} 秒")
|
||||
|
||||
def query_bbox(self, bbox_min: np.ndarray, bbox_max: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Grid索引查询 - O(1 + K)复杂度
|
||||
|
||||
直接定位到相关网格,只检查这些网格中的点
|
||||
"""
|
||||
# 将bbox转换为网格坐标范围
|
||||
grid_min = np.floor((bbox_min - self.mins) / self.grid_size).astype(np.int32)
|
||||
grid_max = np.floor((bbox_max - self.mins) / self.grid_size).astype(np.int32)
|
||||
|
||||
# 收集相关网格中的所有点索引
|
||||
candidate_indices = []
|
||||
for gx in range(grid_min[0], grid_max[0] + 1):
|
||||
for gy in range(grid_min[1], grid_max[1] + 1):
|
||||
for gz in range(grid_min[2], grid_max[2] + 1):
|
||||
grid_key = (gx, gy, gz)
|
||||
if grid_key in self.grid:
|
||||
candidate_indices.append(self.grid[grid_key])
|
||||
|
||||
if len(candidate_indices) == 0:
|
||||
return np.array([], dtype=np.int32)
|
||||
|
||||
# 合并所有候选点
|
||||
candidate_indices = np.concatenate(candidate_indices)
|
||||
candidate_points = self.points[candidate_indices]
|
||||
|
||||
# 精确过滤(网格是粗略的,需要精确检查)
|
||||
mask = (
|
||||
(candidate_points[:, 0] >= bbox_min[0]) &
|
||||
(candidate_points[:, 0] <= bbox_max[0]) &
|
||||
(candidate_points[:, 1] >= bbox_min[1]) &
|
||||
(candidate_points[:, 1] <= bbox_max[1]) &
|
||||
(candidate_points[:, 2] >= bbox_min[2]) &
|
||||
(candidate_points[:, 2] <= bbox_max[2])
|
||||
)
|
||||
|
||||
return candidate_indices[mask]
|
||||
|
||||
def get_info(self) -> dict:
|
||||
info = super().get_info()
|
||||
info['method'] = 'Grid网格索引'
|
||||
info['complexity'] = 'O(1 + K)'
|
||||
info['grid_size'] = self.grid_size
|
||||
info['grid_count'] = len(self.grid)
|
||||
info['avg_points_per_grid'] = len(self.points) / len(self.grid)
|
||||
return info
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Grid/Voxel 网格索引 - 优化版(numpy向量化)"""
|
||||
import numpy as np
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from .base import SpatialIndex
|
||||
|
||||
|
||||
class GridIndexOptimized(SpatialIndex):
|
||||
"""Grid/Voxel 空间索引 - 优化版,使用numpy向量化避免Python循环"""
|
||||
|
||||
def __init__(self, points: np.ndarray, grid_size: float = 100.0):
|
||||
"""
|
||||
构建网格索引(优化版)
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
grid_size: 网格大小(毫米)
|
||||
"""
|
||||
super().__init__(points)
|
||||
self.grid_size = grid_size
|
||||
|
||||
print(f" 构建Grid索引(优化版 - numpy向量化)(grid_size={grid_size}mm)...")
|
||||
start = time.time()
|
||||
|
||||
# 计算点云边界
|
||||
self.mins = points.min(axis=0)
|
||||
self.maxs = points.max(axis=0)
|
||||
|
||||
# 将每个点映射到网格坐标
|
||||
grid_coords = np.floor((points - self.mins) / grid_size).astype(np.int32)
|
||||
|
||||
# ===== 关键优化:使用numpy向量化替代Python循环 =====
|
||||
|
||||
# 1. 将3D网格坐标编码为1D哈希值(避免tuple开销)
|
||||
# 使用大素数来避免冲突
|
||||
hash_multiplier = np.array([1, 1000003, 1000003**2], dtype=np.int64)
|
||||
grid_hash = (grid_coords * hash_multiplier).sum(axis=1)
|
||||
|
||||
# 2. 使用argsort排序(numpy C实现,非常快)
|
||||
sort_idx = np.argsort(grid_hash)
|
||||
sorted_hash = grid_hash[sort_idx]
|
||||
|
||||
# 3. 找到每个唯一网格的边界
|
||||
# 只需要遍历唯一网格数(约500万),而不是10亿点!
|
||||
unique_mask = np.concatenate(([True], sorted_hash[1:] != sorted_hash[:-1]))
|
||||
unique_indices = np.where(unique_mask)[0]
|
||||
|
||||
# 4. 构建索引字典
|
||||
self.grid = {}
|
||||
self.grid_hash_to_coords = {} # 保存hash到3D坐标的映射
|
||||
|
||||
n_unique = len(unique_indices)
|
||||
for i in range(n_unique):
|
||||
start_idx = unique_indices[i]
|
||||
end_idx = unique_indices[i + 1] if i + 1 < n_unique else len(sorted_hash)
|
||||
|
||||
hash_val = sorted_hash[start_idx]
|
||||
indices = sort_idx[start_idx:end_idx]
|
||||
|
||||
# 使用hash作为key(比tuple快)
|
||||
self.grid[hash_val] = indices
|
||||
|
||||
# 保存hash到原始3D坐标的映射(用于查询)
|
||||
self.grid_hash_to_coords[hash_val] = tuple(grid_coords[indices[0]])
|
||||
|
||||
self.build_time = time.time() - start
|
||||
print(f" 网格数量: {len(self.grid):,}")
|
||||
print(f" 平均点/格: {len(points) / len(self.grid):.1f}")
|
||||
print(f" 构建耗时: {self.build_time:.2f} 秒")
|
||||
|
||||
def query_bbox(self, bbox_min: np.ndarray, bbox_max: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Grid索引查询 - O(1 + K)复杂度
|
||||
"""
|
||||
# 将bbox转换为网格坐标范围
|
||||
grid_min = np.floor((bbox_min - self.mins) / self.grid_size).astype(np.int32)
|
||||
grid_max = np.floor((bbox_max - self.mins) / self.grid_size).astype(np.int32)
|
||||
|
||||
# 收集相关网格中的所有点索引
|
||||
candidate_indices = []
|
||||
|
||||
# 遍历bbox覆盖的网格(通常很少,比如10×10×10=1000个)
|
||||
hash_multiplier = np.array([1, 1000003, 1000003**2], dtype=np.int64)
|
||||
for gx in range(grid_min[0], grid_max[0] + 1):
|
||||
for gy in range(grid_min[1], grid_max[1] + 1):
|
||||
for gz in range(grid_min[2], grid_max[2] + 1):
|
||||
grid_coord = np.array([gx, gy, gz], dtype=np.int32)
|
||||
grid_hash_val = (grid_coord * hash_multiplier).sum()
|
||||
|
||||
if grid_hash_val in self.grid:
|
||||
candidate_indices.append(self.grid[grid_hash_val])
|
||||
|
||||
if len(candidate_indices) == 0:
|
||||
return np.array([], dtype=np.int32)
|
||||
|
||||
# 合并所有候选点
|
||||
candidate_indices = np.concatenate(candidate_indices)
|
||||
candidate_points = self.points[candidate_indices]
|
||||
|
||||
# 精确过滤
|
||||
mask = (
|
||||
(candidate_points[:, 0] >= bbox_min[0]) &
|
||||
(candidate_points[:, 0] <= bbox_max[0]) &
|
||||
(candidate_points[:, 1] >= bbox_min[1]) &
|
||||
(candidate_points[:, 1] <= bbox_max[1]) &
|
||||
(candidate_points[:, 2] >= bbox_min[2]) &
|
||||
(candidate_points[:, 2] <= bbox_max[2])
|
||||
)
|
||||
|
||||
return candidate_indices[mask]
|
||||
|
||||
def get_info(self) -> dict:
|
||||
info = super().get_info()
|
||||
info['method'] = 'Grid网格索引(优化版)'
|
||||
info['complexity'] = 'O(1 + K)'
|
||||
info['grid_size'] = self.grid_size
|
||||
info['grid_count'] = len(self.grid)
|
||||
info['avg_points_per_grid'] = len(self.points) / len(self.grid)
|
||||
info['optimization'] = 'numpy向量化'
|
||||
return info
|
||||
@@ -0,0 +1,66 @@
|
||||
"""KDTree 索引(使用scipy)"""
|
||||
import numpy as np
|
||||
import time
|
||||
from scipy.spatial import cKDTree
|
||||
from .base import SpatialIndex
|
||||
|
||||
|
||||
class KDTreeIndex(SpatialIndex):
|
||||
"""KDTree 空间索引 - O(N log N)构建, O(log N + K)查询"""
|
||||
|
||||
def __init__(self, points: np.ndarray):
|
||||
"""
|
||||
构建KD树索引
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
"""
|
||||
super().__init__(points)
|
||||
|
||||
print(f" 构建KDTree索引...")
|
||||
start = time.time()
|
||||
|
||||
# 使用scipy的cKDTree(C实现,比纯Python快)
|
||||
self.tree = cKDTree(points)
|
||||
|
||||
self.build_time = time.time() - start
|
||||
print(f" 构建耗时: {self.build_time:.2f} 秒")
|
||||
|
||||
def query_bbox(self, bbox_min: np.ndarray, bbox_max: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
KDTree查询 - O(log N + K)复杂度
|
||||
|
||||
使用球形查询近似bbox,然后精确过滤
|
||||
"""
|
||||
# KDTree没有直接的bbox查询,使用球形查询近似
|
||||
center = (bbox_min + bbox_max) / 2
|
||||
# 使用外接球半径,确保包含整个bbox
|
||||
radius = np.linalg.norm(bbox_max - bbox_min) / 2
|
||||
|
||||
# 球形查询
|
||||
candidate_indices = self.tree.query_ball_point(center, radius)
|
||||
|
||||
if len(candidate_indices) == 0:
|
||||
return np.array([], dtype=np.int32)
|
||||
|
||||
candidate_indices = np.array(candidate_indices, dtype=np.int32)
|
||||
candidate_points = self.points[candidate_indices]
|
||||
|
||||
# 精确过滤(因为球形查询是近似的)
|
||||
mask = (
|
||||
(candidate_points[:, 0] >= bbox_min[0]) &
|
||||
(candidate_points[:, 0] <= bbox_max[0]) &
|
||||
(candidate_points[:, 1] >= bbox_min[1]) &
|
||||
(candidate_points[:, 1] <= bbox_max[1]) &
|
||||
(candidate_points[:, 2] >= bbox_min[2]) &
|
||||
(candidate_points[:, 2] <= bbox_max[2])
|
||||
)
|
||||
|
||||
return candidate_indices[mask]
|
||||
|
||||
def get_info(self) -> dict:
|
||||
info = super().get_info()
|
||||
info['method'] = 'KDTree索引(scipy)'
|
||||
info['complexity'] = 'O(log N + K)'
|
||||
info['note'] = '构建慢,适合小数据集'
|
||||
return info
|
||||
@@ -0,0 +1,32 @@
|
||||
"""线性扫描(无索引)- 基准方法"""
|
||||
import numpy as np
|
||||
import time
|
||||
from .base import SpatialIndex
|
||||
|
||||
|
||||
class LinearScanIndex(SpatialIndex):
|
||||
"""线性扫描索引(实际上就是无索引)"""
|
||||
|
||||
def __init__(self, points: np.ndarray):
|
||||
super().__init__(points)
|
||||
# 无需构建索引
|
||||
self.build_time = 0.0
|
||||
|
||||
def query_bbox(self, bbox_min: np.ndarray, bbox_max: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
线性扫描查询 - O(N)复杂度
|
||||
|
||||
遍历所有点,检查是否在bbox内
|
||||
"""
|
||||
mask = (
|
||||
(self.points[:, 0] >= bbox_min[0]) & (self.points[:, 0] <= bbox_max[0]) &
|
||||
(self.points[:, 1] >= bbox_min[1]) & (self.points[:, 1] <= bbox_max[1]) &
|
||||
(self.points[:, 2] >= bbox_min[2]) & (self.points[:, 2] <= bbox_max[2])
|
||||
)
|
||||
return np.where(mask)[0]
|
||||
|
||||
def get_info(self) -> dict:
|
||||
info = super().get_info()
|
||||
info['method'] = '线性扫描(无索引)'
|
||||
info['complexity'] = 'O(N)'
|
||||
return info
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Octree 八叉树索引"""
|
||||
import numpy as np
|
||||
import time
|
||||
from .base import SpatialIndex
|
||||
|
||||
|
||||
class OctreeNode:
|
||||
"""八叉树节点"""
|
||||
|
||||
def __init__(self, center: np.ndarray, size: float):
|
||||
self.center = center
|
||||
self.size = size
|
||||
self.children = [None] * 8 # 8个子节点
|
||||
self.point_indices = [] # 叶节点存储点索引
|
||||
self.is_leaf = True
|
||||
|
||||
|
||||
class OctreeIndex(SpatialIndex):
|
||||
"""Octree 空间索引 - O(N)构建, O(log N + K)查询"""
|
||||
|
||||
def __init__(self, points: np.ndarray, max_depth: int = 8, max_points_per_node: int = 1000):
|
||||
"""
|
||||
构建八叉树索引
|
||||
|
||||
Args:
|
||||
points: 点云数组 (N, 3)
|
||||
max_depth: 最大树深度
|
||||
max_points_per_node: 每个叶节点最多存储的点数
|
||||
"""
|
||||
super().__init__(points)
|
||||
self.max_depth = max_depth
|
||||
self.max_points_per_node = max_points_per_node
|
||||
|
||||
print(f" 构建Octree索引 (max_depth={max_depth}, max_points={max_points_per_node})...")
|
||||
start = time.time()
|
||||
|
||||
# 计算点云边界
|
||||
mins = points.min(axis=0)
|
||||
maxs = points.max(axis=0)
|
||||
center = (mins + maxs) / 2
|
||||
size = np.max(maxs - mins) / 2
|
||||
|
||||
# 构建树
|
||||
all_indices = np.arange(len(points), dtype=np.int32)
|
||||
self.root = self._build_tree(center, size, all_indices, depth=0)
|
||||
|
||||
self.build_time = time.time() - start
|
||||
self.node_count = self._count_nodes(self.root)
|
||||
print(f" 节点数量: {self.node_count:,}")
|
||||
print(f" 构建耗时: {self.build_time:.2f} 秒")
|
||||
|
||||
def _build_tree(self, center: np.ndarray, size: float, indices: np.ndarray, depth: int) -> OctreeNode:
|
||||
"""递归构建八叉树"""
|
||||
node = OctreeNode(center, size)
|
||||
|
||||
# 终止条件:达到最大深度或点数足够少
|
||||
if depth >= self.max_depth or len(indices) <= self.max_points_per_node:
|
||||
node.point_indices = indices
|
||||
node.is_leaf = True
|
||||
return node
|
||||
|
||||
# 分割成8个子空间
|
||||
node.is_leaf = False
|
||||
half_size = size / 2
|
||||
points_subset = self.points[indices]
|
||||
|
||||
for i in range(8):
|
||||
# 计算子节点中心
|
||||
child_center = center.copy()
|
||||
child_center[0] += half_size if (i & 1) else -half_size
|
||||
child_center[1] += half_size if (i & 2) else -half_size
|
||||
child_center[2] += half_size if (i & 4) else -half_size
|
||||
|
||||
# 找到属于这个子空间的点
|
||||
mask = (
|
||||
((points_subset[:, 0] >= child_center[0] - half_size) &
|
||||
(points_subset[:, 0] < child_center[0] + half_size)) &
|
||||
((points_subset[:, 1] >= child_center[1] - half_size) &
|
||||
(points_subset[:, 1] < child_center[1] + half_size)) &
|
||||
((points_subset[:, 2] >= child_center[2] - half_size) &
|
||||
(points_subset[:, 2] < child_center[2] + half_size))
|
||||
)
|
||||
|
||||
child_indices = indices[mask]
|
||||
|
||||
if len(child_indices) > 0:
|
||||
node.children[i] = self._build_tree(child_center, half_size, child_indices, depth + 1)
|
||||
|
||||
return node
|
||||
|
||||
def query_bbox(self, bbox_min: np.ndarray, bbox_max: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Octree查询 - O(log N + K)复杂度
|
||||
|
||||
递归查询与bbox相交的节点
|
||||
"""
|
||||
result = []
|
||||
self._query_recursive(self.root, bbox_min, bbox_max, result)
|
||||
|
||||
if len(result) == 0:
|
||||
return np.array([], dtype=np.int32)
|
||||
|
||||
# 合并所有候选点
|
||||
candidate_indices = np.concatenate(result)
|
||||
candidate_points = self.points[candidate_indices]
|
||||
|
||||
# 精确过滤
|
||||
mask = (
|
||||
(candidate_points[:, 0] >= bbox_min[0]) &
|
||||
(candidate_points[:, 0] <= bbox_max[0]) &
|
||||
(candidate_points[:, 1] >= bbox_min[1]) &
|
||||
(candidate_points[:, 1] <= bbox_max[1]) &
|
||||
(candidate_points[:, 2] >= bbox_min[2]) &
|
||||
(candidate_points[:, 2] <= bbox_max[2])
|
||||
)
|
||||
|
||||
return candidate_indices[mask]
|
||||
|
||||
def _query_recursive(self, node: OctreeNode, bbox_min: np.ndarray, bbox_max: np.ndarray, result: list):
|
||||
"""递归查询节点"""
|
||||
if node is None:
|
||||
return
|
||||
|
||||
# 检查节点边界框是否与查询bbox相交
|
||||
node_min = node.center - node.size
|
||||
node_max = node.center + node.size
|
||||
|
||||
if not self._bbox_intersects(bbox_min, bbox_max, node_min, node_max):
|
||||
return
|
||||
|
||||
if node.is_leaf:
|
||||
# 叶节点:添加所有点索引
|
||||
if len(node.point_indices) > 0:
|
||||
result.append(node.point_indices)
|
||||
else:
|
||||
# 内部节点:递归查询子节点
|
||||
for child in node.children:
|
||||
if child is not None:
|
||||
self._query_recursive(child, bbox_min, bbox_max, result)
|
||||
|
||||
def _bbox_intersects(self, bbox1_min: np.ndarray, bbox1_max: np.ndarray,
|
||||
bbox2_min: np.ndarray, bbox2_max: np.ndarray) -> bool:
|
||||
"""检查两个bbox是否相交"""
|
||||
return not (
|
||||
bbox1_max[0] < bbox2_min[0] or bbox1_min[0] > bbox2_max[0] or
|
||||
bbox1_max[1] < bbox2_min[1] or bbox1_min[1] > bbox2_max[1] or
|
||||
bbox1_max[2] < bbox2_min[2] or bbox1_min[2] > bbox2_max[2]
|
||||
)
|
||||
|
||||
def _count_nodes(self, node: OctreeNode) -> int:
|
||||
"""统计节点数量"""
|
||||
if node is None:
|
||||
return 0
|
||||
count = 1
|
||||
if not node.is_leaf:
|
||||
for child in node.children:
|
||||
count += self._count_nodes(child)
|
||||
return count
|
||||
|
||||
def get_info(self) -> dict:
|
||||
info = super().get_info()
|
||||
info['method'] = 'Octree八叉树索引'
|
||||
info['complexity'] = 'O(log N + K)'
|
||||
info['max_depth'] = self.max_depth
|
||||
info['node_count'] = self.node_count
|
||||
return info
|
||||
510
alg/train_measurement_system_v3.2_0629/src/task_manager.py
Normal file
510
alg/train_measurement_system_v3.2_0629/src/task_manager.py
Normal file
@@ -0,0 +1,510 @@
|
||||
"""任务管理器 - 负责加载配置、调度任务、汇总结果"""
|
||||
import yaml
|
||||
import numpy as np
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from pointcloud_loader import PointCloudLoader
|
||||
from measurements.base import MeasurementResult
|
||||
from measurements.plane_distance import PlaneToPlaneDistance
|
||||
from measurements.point_to_plane import PointToPlaneDistance
|
||||
from measurements.point_to_point import PointToPointDistance
|
||||
from measurements.plane_with_holes import PlaneToPlaneWithHoles
|
||||
from measurements.point_to_plane_with_holes import PointToPlaneWithHoles
|
||||
from measurements.point_to_point_with_holes import PointToPointWithHoles
|
||||
from measurements.line_to_line import LineToLineDistance
|
||||
from measurements.line_to_plane import LineToPlaneDistance
|
||||
from excel_exporter import ExcelExporter
|
||||
from performance_logger import PerformanceLogger
|
||||
from measurement_logger import MeasurementLogger
|
||||
from spatial_index import LinearScanIndex, GridIndex, GridIndexOptimized, OctreeIndex, KDTreeIndex
|
||||
|
||||
|
||||
class TaskManager:
|
||||
"""测量任务管理器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化任务管理器"""
|
||||
self.config = None
|
||||
self.point_cloud = None
|
||||
self.spatial_index = None # 空间索引
|
||||
self.tasks = []
|
||||
self.results = []
|
||||
self.pointcloud_path_override = None
|
||||
self.output_dir_override = None
|
||||
|
||||
def load_config(self, config_path: str) -> bool:
|
||||
"""
|
||||
加载配置文件
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径
|
||||
|
||||
Returns:
|
||||
bool: 是否加载成功
|
||||
"""
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
# 加载点云(支持相对路径)
|
||||
pointcloud_path = self.pointcloud_path_override or self.config.get('pointcloud_file')
|
||||
if not pointcloud_path:
|
||||
print("错误: 配置文件中未指定点云文件路径")
|
||||
return False
|
||||
|
||||
# 处理相对路径
|
||||
pointcloud_path = self._resolve_path(pointcloud_path, config_path)
|
||||
|
||||
# 加载点云
|
||||
print(f"\n{'='*70}")
|
||||
print("阶段1: 加载点云")
|
||||
print("="*70)
|
||||
print(f" 文件: {Path(pointcloud_path).name}")
|
||||
|
||||
load_start = time.time()
|
||||
self.point_cloud = PointCloudLoader.load_ply(pointcloud_path)
|
||||
load_time = time.time() - load_start
|
||||
|
||||
print(f" 点云数量: {len(self.point_cloud):,} 个点")
|
||||
print(f" 加载耗时: {load_time:.2f} 秒")
|
||||
|
||||
# 保存点云加载信息供性能日志使用
|
||||
self.pointcloud_load_time = load_time
|
||||
self.pointcloud_filename = Path(pointcloud_path).name
|
||||
|
||||
# 构建空间索引
|
||||
print(f"\n{'='*70}")
|
||||
print("阶段2: 构建空间索引")
|
||||
print("="*70)
|
||||
index_start = time.time()
|
||||
self._build_spatial_index()
|
||||
self.index_build_time = time.time() - index_start
|
||||
|
||||
# 解析任务
|
||||
print(f"\n{'='*70}")
|
||||
print("阶段3: 解析测量任务")
|
||||
print("="*70)
|
||||
tasks_config = self.config.get('tasks', [])
|
||||
if not tasks_config:
|
||||
print("警告: 配置文件中没有定义任务")
|
||||
return True
|
||||
|
||||
print(f"\n找到 {len(tasks_config)} 个测量任务")
|
||||
|
||||
for task_config in tasks_config:
|
||||
task = self._create_task(task_config)
|
||||
if task:
|
||||
self.tasks.append(task)
|
||||
print(f" - {task.name} ({task.task_type})")
|
||||
|
||||
return True
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"错误: 配置文件不存在: {config_path}")
|
||||
return False
|
||||
except yaml.YAMLError as e:
|
||||
print(f"错误: 配置文件格式错误: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"错误: 加载配置失败: {e}")
|
||||
return False
|
||||
|
||||
def _resolve_path(self, path: str, config_path: str) -> str:
|
||||
"""
|
||||
解析路径(支持相对路径和绝对路径)
|
||||
|
||||
Args:
|
||||
path: 待解析的路径
|
||||
config_path: 配置文件路径(用于相对路径的基准)
|
||||
|
||||
Returns:
|
||||
str: 解析后的绝对路径
|
||||
"""
|
||||
path_obj = Path(path)
|
||||
if path_obj.is_absolute():
|
||||
return str(path_obj)
|
||||
else:
|
||||
# 相对于配置文件所在目录
|
||||
config_dir = Path(config_path).parent
|
||||
return str(config_dir / path_obj)
|
||||
|
||||
def override_pointcloud_path(self, path: str):
|
||||
"""
|
||||
覆盖点云文件路径(用于命令行参数)
|
||||
|
||||
Args:
|
||||
path: 点云文件路径
|
||||
"""
|
||||
self.pointcloud_path_override = path
|
||||
|
||||
def override_output_dir(self, output_dir: str):
|
||||
"""
|
||||
覆盖输出目录(用于命令行参数)
|
||||
|
||||
Args:
|
||||
output_dir: 输出目录路径
|
||||
"""
|
||||
self.output_dir_override = output_dir
|
||||
|
||||
def _create_task(self, task_config: Dict[str, Any]):
|
||||
"""
|
||||
根据配置创建测量任务
|
||||
|
||||
Args:
|
||||
task_config: 任务配置字典
|
||||
|
||||
Returns:
|
||||
BaseMeasurement: 测量任务实例
|
||||
"""
|
||||
task_type = task_config.get('type')
|
||||
task_name = task_config.get('name', 'Unnamed Task')
|
||||
|
||||
# 处理带有孔中心检测的任务类型
|
||||
if '4_hole_center' in task_type:
|
||||
if 'plane_to_plane' in task_type:
|
||||
return PlaneToPlaneWithHoles(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
elif 'point_to_plane' in task_type:
|
||||
return PointToPlaneWithHoles(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
elif 'hole_center' in task_type and 'point_to_point' in task_type:
|
||||
return PointToPointWithHoles(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
elif task_type == 'plane_to_plane':
|
||||
return PlaneToPlaneDistance(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
elif task_type == 'point_to_plane':
|
||||
return PointToPlaneDistance(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
elif task_type == 'point_to_point':
|
||||
return PointToPointDistance(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
elif task_type == 'line_to_line':
|
||||
return LineToLineDistance(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
elif task_type == 'line_to_plane':
|
||||
return LineToPlaneDistance(
|
||||
name=task_name,
|
||||
point_cloud=self.point_cloud,
|
||||
config=task_config,
|
||||
spatial_index=self.spatial_index
|
||||
)
|
||||
else:
|
||||
print(f"警告: 未知的任务类型: {task_type}")
|
||||
return None
|
||||
|
||||
def run_all_tasks(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
执行所有测量任务
|
||||
|
||||
Returns:
|
||||
List[Dict]: 任务结果列表
|
||||
"""
|
||||
import time
|
||||
|
||||
self.results = []
|
||||
|
||||
# 创建性能日志记录器
|
||||
if self.output_dir_override:
|
||||
output_dir = Path(self.output_dir_override)
|
||||
else:
|
||||
output_dir = Path(__file__).parent.parent / "results"
|
||||
|
||||
perf_logger = PerformanceLogger(str(output_dir))
|
||||
|
||||
# 记录初始化阶段性能
|
||||
if hasattr(self, 'pointcloud_load_time'):
|
||||
perf_logger.log_pointcloud_loading(
|
||||
self.pointcloud_load_time,
|
||||
len(self.point_cloud),
|
||||
self.pointcloud_filename
|
||||
)
|
||||
if hasattr(self, 'index_build_time'):
|
||||
index_info = self.spatial_index.get_info() if self.spatial_index else {}
|
||||
index_method = self.config.get('roi_extraction', {}).get('method', 'unknown')
|
||||
perf_logger.log_spatial_index(
|
||||
self.index_build_time,
|
||||
index_method,
|
||||
index_info
|
||||
)
|
||||
|
||||
# 创建详细测量日志记录器
|
||||
detail_logger = MeasurementLogger(str(output_dir))
|
||||
|
||||
for i, task in enumerate(self.tasks, 1):
|
||||
print(f"\n[{i}/{len(self.tasks)}] 执行任务: {task.name}")
|
||||
print("-" * 60)
|
||||
|
||||
# 记录任务开始到详细日志
|
||||
detail_logger.log_task_start(i, len(self.tasks), task.name)
|
||||
|
||||
# 记录开始时间
|
||||
start_time = time.time()
|
||||
|
||||
# 将详细日志记录器传递给任务
|
||||
task.detail_logger = detail_logger
|
||||
|
||||
# 执行任务
|
||||
result = task.execute()
|
||||
|
||||
# 记录结束时间
|
||||
end_time = time.time()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
# 将执行时间添加到结果数据中
|
||||
if result.data is None:
|
||||
result.data = {}
|
||||
result.data['execution_time'] = execution_time
|
||||
|
||||
# 获取详细的时间分布
|
||||
timing_breakdown = result.data.get('timing_breakdown', {})
|
||||
|
||||
# 记录到性能日志
|
||||
perf_logger.log_task(task.name, execution_time, timing_breakdown)
|
||||
|
||||
# 记录结果
|
||||
result_dict = {
|
||||
'name': result.task_name,
|
||||
'type': result.task_type,
|
||||
'status': 'success' if result.success else 'failed',
|
||||
'data': result.data if result.success else None,
|
||||
'error': result.error_message if not result.success else None,
|
||||
'execution_time': execution_time
|
||||
}
|
||||
self.results.append(result_dict)
|
||||
|
||||
# 检查是否有宽度差值检查结果,如果有则生成额外的任务19结果
|
||||
if result.success and result.data and 'width_variation' in result.data:
|
||||
width_var = result.data['width_variation']
|
||||
|
||||
# 创建任务19的结果
|
||||
task19_name = task.name.replace("任务18", "任务19").replace("门口宽度", "门口宽度差值")
|
||||
task19_result_dict = {
|
||||
'name': task19_name,
|
||||
'type': result.task_type,
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'upper_width': width_var['upper_width'],
|
||||
'middle_width': width_var['middle_width'],
|
||||
'lower_width': width_var['lower_width'],
|
||||
'diff_upper_middle': width_var['diff_upper_middle'],
|
||||
'diff_middle_lower': width_var['diff_middle_lower'],
|
||||
'diff_upper_lower': width_var['diff_upper_lower'],
|
||||
'max_diff': max(abs(width_var['diff_upper_middle']),
|
||||
abs(width_var['diff_middle_lower']),
|
||||
abs(width_var['diff_upper_lower'])),
|
||||
'tolerance': width_var['tolerance'],
|
||||
'pass': width_var['pass'],
|
||||
'execution_time': execution_time
|
||||
},
|
||||
'error': None,
|
||||
'execution_time': execution_time
|
||||
}
|
||||
self.results.append(task19_result_dict)
|
||||
|
||||
# 打印任务19结果
|
||||
print(f"\n [自动生成] {task19_name}")
|
||||
print(f" 上部宽度: {width_var['upper_width']:.4f} mm")
|
||||
print(f" 中部宽度: {width_var['middle_width']:.4f} mm")
|
||||
print(f" 下部宽度: {width_var['lower_width']:.4f} mm")
|
||||
print(f" 最大差值: {task19_result_dict['data']['max_diff']:.4f} mm")
|
||||
print(f" 公差要求: ±{width_var['tolerance']:.1f} mm")
|
||||
status = "✅ 通过" if width_var['pass'] else "❌ 超差"
|
||||
print(f" 检查结果: {status}")
|
||||
|
||||
# 打印结果
|
||||
if result.success:
|
||||
print(f"[成功] 任务完成 (用时: {execution_time:.2f} 秒)")
|
||||
if result.data:
|
||||
print(f" 平均距离: {result.data.get('mean_distance', 0):.2f} mm")
|
||||
print(f" 标准差: {result.data.get('std_distance', 0):.2f} mm")
|
||||
print(f" 范围: [{result.data.get('min_distance', 0):.2f}, {result.data.get('max_distance', 0):.2f}] mm")
|
||||
print(f" 测量点数: {result.data.get('point_count', result.data.get('sample_count', 0))}")
|
||||
if 'source_inlier_ratio' in result.data:
|
||||
print(f" 源平面内点率: {result.data.get('source_inlier_ratio', 0):.1%}")
|
||||
if 'target_inlier_ratio' in result.data:
|
||||
print(f" 目标平面内点率: {result.data.get('target_inlier_ratio', 0):.1%}")
|
||||
if 'plane_inlier_ratio' in result.data:
|
||||
print(f" 平面内点率: {result.data.get('plane_inlier_ratio', 0):.1%}")
|
||||
|
||||
# 记录成功到详细日志
|
||||
detail_logger.log_task_success(execution_time)
|
||||
if result.data:
|
||||
# 记录测量结果到详细日志
|
||||
if 'mean_distance' in result.data:
|
||||
detail_logger.log_distance_result(
|
||||
result.data.get('mean_distance', 0),
|
||||
result.data.get('std_distance', 0),
|
||||
result.data.get('min_distance', 0),
|
||||
result.data.get('max_distance', 0)
|
||||
)
|
||||
else:
|
||||
print(f"[失败] {result.error_message} (用时: {execution_time:.2f} 秒)")
|
||||
# 记录失败到详细日志
|
||||
detail_logger.log_task_failure(result.error_message, execution_time)
|
||||
|
||||
# 可视化(如果配置中启用)
|
||||
if result.success and task.config.get('visualization', False):
|
||||
self._visualize_result(task, result)
|
||||
|
||||
# 保存性能日志
|
||||
perf_logger.save()
|
||||
|
||||
# 关闭详细测量日志
|
||||
detail_logger.close()
|
||||
|
||||
# 导出Excel结果
|
||||
self._export_results()
|
||||
|
||||
return self.results
|
||||
|
||||
def _visualize_result(self, task, result: MeasurementResult):
|
||||
"""
|
||||
可视化测量结果
|
||||
|
||||
Args:
|
||||
task: 测量任务
|
||||
result: 测量结果
|
||||
"""
|
||||
try:
|
||||
import open3d as o3d
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
print(" 生成可视化...")
|
||||
|
||||
data = result.data
|
||||
if not data:
|
||||
return
|
||||
|
||||
# 创建可视化窗口
|
||||
vis = o3d.visualization.Visualizer()
|
||||
vis.create_window(window_name=f"测量结果: {task.name}", width=1200, height=800)
|
||||
|
||||
# 显示源平面点云(蓝色)
|
||||
source_pcd = o3d.geometry.PointCloud()
|
||||
source_pcd.points = o3d.utility.Vector3dVector(data['_source_points'])
|
||||
source_pcd.paint_uniform_color([0.2, 0.4, 0.8])
|
||||
vis.add_geometry(source_pcd)
|
||||
|
||||
# 显示目标平面点云(绿色)
|
||||
target_pcd = o3d.geometry.PointCloud()
|
||||
target_pcd.points = o3d.utility.Vector3dVector(data['_target_points'])
|
||||
target_pcd.paint_uniform_color([0.2, 0.8, 0.4])
|
||||
vis.add_geometry(target_pcd)
|
||||
|
||||
# 显示采样点(红色)
|
||||
sampled_pcd = o3d.geometry.PointCloud()
|
||||
sampled_pcd.points = o3d.utility.Vector3dVector(data['_sampled_points'])
|
||||
sampled_pcd.paint_uniform_color([0.9, 0.2, 0.2])
|
||||
vis.add_geometry(sampled_pcd)
|
||||
|
||||
# 运行可视化
|
||||
vis.run()
|
||||
vis.destroy_window()
|
||||
|
||||
except Exception as e:
|
||||
print(f" 可视化失败: {e}")
|
||||
|
||||
def _export_results(self):
|
||||
"""导出结果到Excel"""
|
||||
try:
|
||||
# 转换为MeasurementResult对象列表
|
||||
measurement_results = []
|
||||
for result_dict in self.results:
|
||||
measurement_results.append(MeasurementResult(
|
||||
task_name=result_dict['name'],
|
||||
task_type=result_dict['type'],
|
||||
success=result_dict['status'] == 'success',
|
||||
data=result_dict.get('data'),
|
||||
error_message=result_dict.get('error')
|
||||
))
|
||||
|
||||
# 导出Excel(支持命令行覆盖输出目录)
|
||||
if self.output_dir_override:
|
||||
output_dir = Path(self.output_dir_override)
|
||||
else:
|
||||
output_dir = Path(__file__).parent.parent / "results"
|
||||
|
||||
exporter = ExcelExporter(str(output_dir))
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"measurement_results_{timestamp}"
|
||||
|
||||
# 传递任务配置信息到导出器
|
||||
excel_path = exporter.export(measurement_results, filename, self.config.get('tasks', []))
|
||||
print(f"\n结果已导出到: {excel_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n导出Excel失败: {e}")
|
||||
|
||||
def _build_spatial_index(self):
|
||||
"""根据配置构建空间索引"""
|
||||
index_config = self.config.get('roi_extraction', {})
|
||||
index_method = index_config.get('method', 'linear').lower()
|
||||
|
||||
if index_method == 'linear':
|
||||
print(" 方法: 线性扫描(无索引)")
|
||||
self.spatial_index = LinearScanIndex(self.point_cloud)
|
||||
|
||||
elif index_method == 'grid':
|
||||
grid_size = index_config.get('grid_size', 100.0)
|
||||
use_optimized = index_config.get('use_optimized', True) # 默认使用优化版
|
||||
|
||||
if use_optimized:
|
||||
print(f" 方法: Grid网格索引(优化版 - numpy向量化)")
|
||||
self.spatial_index = GridIndexOptimized(self.point_cloud, grid_size=grid_size)
|
||||
else:
|
||||
print(f" 方法: Grid网格索引(原始版 - Python循环)")
|
||||
self.spatial_index = GridIndex(self.point_cloud, grid_size=grid_size)
|
||||
|
||||
elif index_method == 'octree':
|
||||
max_depth = index_config.get('max_depth', 8)
|
||||
max_points = index_config.get('max_points_per_node', 1000)
|
||||
print(f" 方法: Octree八叉树索引")
|
||||
self.spatial_index = OctreeIndex(self.point_cloud, max_depth=max_depth, max_points_per_node=max_points)
|
||||
|
||||
elif index_method == 'kdtree':
|
||||
print(f" 方法: KDTree索引")
|
||||
self.spatial_index = KDTreeIndex(self.point_cloud)
|
||||
|
||||
else:
|
||||
print(f" 警告: 未知的索引方法 '{index_method}',使用线性扫描")
|
||||
self.spatial_index = LinearScanIndex(self.point_cloud)
|
||||
|
||||
# 显示索引信息
|
||||
info = self.spatial_index.get_info()
|
||||
if 'grid_count' in info:
|
||||
print(f" 详细信息: {info['grid_count']:,} 个网格, 平均 {info['avg_points_per_grid']:.1f} 点/格")
|
||||
elif 'node_count' in info:
|
||||
print(f" 详细信息: {info['node_count']:,} 个节点")
|
||||
195
alg/train_measurement_system_v3.2_0629/src/visualizer.py
Normal file
195
alg/train_measurement_system_v3.2_0629/src/visualizer.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""可视化工具 - 用于显示点云、平面、ROI等"""
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
|
||||
class Visualizer:
|
||||
"""点云和测量结果可视化工具"""
|
||||
|
||||
@staticmethod
|
||||
def visualize_roi_in_context(
|
||||
full_cloud: np.ndarray,
|
||||
roi_cloud: np.ndarray,
|
||||
bbox_min: List[float],
|
||||
bbox_max: List[float],
|
||||
title: str = "ROI in Context",
|
||||
downsample_full: int = 10,
|
||||
save_path: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
在完整点云上下文中可视化ROI区域
|
||||
|
||||
Args:
|
||||
full_cloud: 完整点云 (N, 3)
|
||||
roi_cloud: ROI区域点云 (M, 3)
|
||||
bbox_min: 包围盒最小坐标 [x, y, z]
|
||||
bbox_max: 包围盒最大坐标 [x, y, z]
|
||||
title: 窗口标题
|
||||
downsample_full: 完整点云降采样倍数(每N个点取1个)
|
||||
save_path: 保存截图路径(可选)
|
||||
"""
|
||||
# 降采样完整点云以提高显示性能
|
||||
full_cloud_downsampled = full_cloud[::downsample_full]
|
||||
|
||||
# 创建完整点云对象(灰色)
|
||||
pcd_full = o3d.geometry.PointCloud()
|
||||
pcd_full.points = o3d.utility.Vector3dVector(full_cloud_downsampled)
|
||||
pcd_full.paint_uniform_color([0.7, 0.7, 0.7]) # 灰色
|
||||
|
||||
# 创建ROI点云对象(红色)
|
||||
pcd_roi = o3d.geometry.PointCloud()
|
||||
pcd_roi.points = o3d.utility.Vector3dVector(roi_cloud)
|
||||
pcd_roi.paint_uniform_color([1.0, 0.0, 0.0]) # 红色
|
||||
|
||||
# 创建包围盒线框
|
||||
bbox = Visualizer._create_bbox_lineset(bbox_min, bbox_max, color=[0, 1, 0])
|
||||
|
||||
# 可视化
|
||||
geometries = [pcd_full, pcd_roi, bbox]
|
||||
o3d.visualization.draw_geometries(
|
||||
geometries,
|
||||
window_name=title,
|
||||
width=1200,
|
||||
height=800,
|
||||
point_show_normal=False
|
||||
)
|
||||
|
||||
# 保存截图(如果指定)
|
||||
if save_path:
|
||||
vis = o3d.visualization.Visualizer()
|
||||
vis.create_window(window_name=title, width=1200, height=800, visible=False)
|
||||
for geom in geometries:
|
||||
vis.add_geometry(geom)
|
||||
vis.update_renderer()
|
||||
vis.capture_screen_image(save_path)
|
||||
vis.destroy_window()
|
||||
print(f"截图已保存: {save_path}")
|
||||
|
||||
@staticmethod
|
||||
def visualize_plane_measurement(
|
||||
source_cloud: np.ndarray,
|
||||
target_cloud: np.ndarray,
|
||||
source_plane_normal: np.ndarray,
|
||||
source_plane_point: np.ndarray,
|
||||
target_plane_normal: np.ndarray,
|
||||
target_plane_point: np.ndarray,
|
||||
sample_points: np.ndarray,
|
||||
distances: np.ndarray,
|
||||
title: str = "Plane Measurement",
|
||||
save_path: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
可视化平面到平面的距离测量
|
||||
|
||||
Args:
|
||||
source_cloud: 源平面点云
|
||||
target_cloud: 目标平面点云
|
||||
source_plane_normal: 源平面法向量
|
||||
source_plane_point: 源平面上的点
|
||||
target_plane_normal: 目标平面法向量
|
||||
target_plane_point: 目标平面上的点
|
||||
sample_points: 采样点
|
||||
distances: 每个采样点到目标平面的距离
|
||||
title: 窗口标题
|
||||
save_path: 保存截图路径
|
||||
"""
|
||||
# 创建源平面点云(蓝色)
|
||||
pcd_source = o3d.geometry.PointCloud()
|
||||
pcd_source.points = o3d.utility.Vector3dVector(source_cloud)
|
||||
pcd_source.paint_uniform_color([0.0, 0.5, 1.0]) # 蓝色
|
||||
|
||||
# 创建目标平面点云(绿色)
|
||||
pcd_target = o3d.geometry.PointCloud()
|
||||
pcd_target.points = o3d.utility.Vector3dVector(target_cloud)
|
||||
pcd_target.paint_uniform_color([0.0, 1.0, 0.0]) # 绿色
|
||||
|
||||
# 创建采样点(红色)
|
||||
pcd_samples = o3d.geometry.PointCloud()
|
||||
pcd_samples.points = o3d.utility.Vector3dVector(sample_points)
|
||||
pcd_samples.paint_uniform_color([1.0, 0.0, 0.0]) # 红色
|
||||
|
||||
# 创建距离线(从采样点到目标平面的投影)
|
||||
lines = []
|
||||
for i, (point, dist) in enumerate(zip(sample_points, distances)):
|
||||
# 计算投影点
|
||||
projection = point - dist * target_plane_normal
|
||||
lines.append([point, projection])
|
||||
|
||||
# 创建线集
|
||||
line_points = []
|
||||
line_indices = []
|
||||
for i, (start, end) in enumerate(lines):
|
||||
line_points.append(start)
|
||||
line_points.append(end)
|
||||
line_indices.append([2*i, 2*i+1])
|
||||
|
||||
lineset = o3d.geometry.LineSet()
|
||||
lineset.points = o3d.utility.Vector3dVector(line_points)
|
||||
lineset.lines = o3d.utility.Vector2iVector(line_indices)
|
||||
lineset.paint_uniform_color([1.0, 0.5, 0.0]) # 橙色
|
||||
|
||||
# 可视化
|
||||
geometries = [pcd_source, pcd_target, pcd_samples, lineset]
|
||||
o3d.visualization.draw_geometries(
|
||||
geometries,
|
||||
window_name=title,
|
||||
width=1200,
|
||||
height=800,
|
||||
point_show_normal=False
|
||||
)
|
||||
|
||||
if save_path:
|
||||
vis = o3d.visualization.Visualizer()
|
||||
vis.create_window(window_name=title, width=1200, height=800, visible=False)
|
||||
for geom in geometries:
|
||||
vis.add_geometry(geom)
|
||||
vis.update_renderer()
|
||||
vis.capture_screen_image(save_path)
|
||||
vis.destroy_window()
|
||||
print(f"截图已保存: {save_path}")
|
||||
|
||||
@staticmethod
|
||||
def _create_bbox_lineset(
|
||||
bbox_min: List[float],
|
||||
bbox_max: List[float],
|
||||
color: List[float] = [0, 1, 0]
|
||||
) -> o3d.geometry.LineSet:
|
||||
"""创建包围盒线框"""
|
||||
# 8个顶点
|
||||
points = [
|
||||
[bbox_min[0], bbox_min[1], bbox_min[2]], # 0
|
||||
[bbox_max[0], bbox_min[1], bbox_min[2]], # 1
|
||||
[bbox_max[0], bbox_max[1], bbox_min[2]], # 2
|
||||
[bbox_min[0], bbox_max[1], bbox_min[2]], # 3
|
||||
[bbox_min[0], bbox_min[1], bbox_max[2]], # 4
|
||||
[bbox_max[0], bbox_min[1], bbox_max[2]], # 5
|
||||
[bbox_max[0], bbox_max[1], bbox_max[2]], # 6
|
||||
[bbox_min[0], bbox_max[1], bbox_max[2]], # 7
|
||||
]
|
||||
|
||||
# 12条边
|
||||
lines = [
|
||||
[0, 1], [1, 2], [2, 3], [3, 0], # 底面
|
||||
[4, 5], [5, 6], [6, 7], [7, 4], # 顶面
|
||||
[0, 4], [1, 5], [2, 6], [3, 7], # 竖边
|
||||
]
|
||||
|
||||
lineset = o3d.geometry.LineSet()
|
||||
lineset.points = o3d.utility.Vector3dVector(points)
|
||||
lineset.lines = o3d.utility.Vector2iVector(lines)
|
||||
lineset.paint_uniform_color(color)
|
||||
|
||||
return lineset
|
||||
|
||||
@staticmethod
|
||||
def save_roi_pointcloud(
|
||||
roi_cloud: np.ndarray,
|
||||
save_path: str
|
||||
):
|
||||
"""保存ROI点云为PLY文件"""
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(roi_cloud)
|
||||
o3d.io.write_point_cloud(save_path, pcd)
|
||||
print(f"ROI点云已保存: {save_path}")
|
||||
Reference in New Issue
Block a user