feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3 适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新 后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动 前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复 文档: 新增5份算法性能分析报告
This commit is contained in:
460
alg/平整度、平面度、轮廓测量v1.5/Readme.md
Normal file
460
alg/平整度、平面度、轮廓测量v1.5/Readme.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# 测量工具
|
||||
|
||||
这是一个面向工业零件点云、网格和 CAD STEP 模型的几何测量工具。项目支持 CAD 转 STL、点云加噪/滤波、点云配准、点云到 Mesh/CAD 的距离评估、点云到 CAD 的仅平移对齐、曲率估计和平面拟合。
|
||||
|
||||
正式代码位于项目根目录,辅助脚本放在 `archive/`,示例数据放在 `datasets/`,打包工具放在 `packaging/`。推荐在项目根目录执行命令。
|
||||
|
||||
面向开发人员的完整使用说明见 [USAGE.md](USAGE.md)。
|
||||
|
||||
## 1. 项目结构
|
||||
|
||||
```text
|
||||
.
|
||||
main.py # 命令行入口
|
||||
pipeline.py # MeasurementPipeline,总流程入口类
|
||||
io_handler.py # 输入检查、点云/Mesh/CAD 读取
|
||||
config_loader.py # YAML 配置加载
|
||||
config.yaml # 默认配置
|
||||
cad_converter.py # STEP -> STL
|
||||
registration.py # RANSAC + ICP 配准
|
||||
evaluation.py # Chamfer、点云到 Mesh 距离、统计指标
|
||||
visualization.py # Open3D 可视化
|
||||
preprocessing/ # 加噪、滤波、曲率、平面拟合
|
||||
datasets/ # 示例 CAD、Mesh、PointCloud、Noise_PointCloud
|
||||
vis/ # 建议保存命令日志、可视化点云和中间结果
|
||||
results/ # translate-align 默认输出目录
|
||||
archive/ # 辅助脚本
|
||||
packaging/ # pip 打包文件夹
|
||||
```
|
||||
|
||||
## 2. 环境安装
|
||||
|
||||
建议使用独立的 Python 3.10 环境。当前项目建议使用 `High-Speed` 环境运行,不要直接安装到 conda 的 `base` 环境。`pythonocc-core` 需要通过 conda 安装,`requirements.txt` 只管理 pip 依赖。
|
||||
|
||||
```bash
|
||||
conda env create -n High-Speed -f environment.yml
|
||||
conda activate High-Speed
|
||||
pip install -r requirements.txt
|
||||
pip install git+https://github.com/AutodeskAILab/occwl
|
||||
```
|
||||
|
||||
如果已经创建过环境,需要补装或更新 CAD 转 Mesh 依赖:
|
||||
|
||||
```bash
|
||||
conda env update -n High-Speed -f environment.yml
|
||||
conda activate High-Speed
|
||||
pip install git+https://github.com/AutodeskAILab/occwl
|
||||
```
|
||||
|
||||
如果当前终端显示 `(base)`,并且 base 环境是 Python 3.12,直接安装 `pythonocc-core=7.5.1` 会失败;该版本只支持 Python 3.7 到 3.10。
|
||||
|
||||
验证环境:
|
||||
|
||||
```bash
|
||||
python -c "import open3d, trimesh, torch, yaml, OCC.Core.STEPControl; print('env ok')"
|
||||
```
|
||||
|
||||
如果日志里出现 CUDA driver 版本过旧的 warning,程序会退回 CPU 计算。当前配准流程本身主要使用 Open3D CPU 管线;点云到 Mesh/CAD 的距离计算使用 torch,理论上可用 GPU,但取决于 `torch.cuda.is_available()`。
|
||||
|
||||
## 3. 数据要求
|
||||
|
||||
默认数据目录结构如下:
|
||||
|
||||
```text
|
||||
datasets/
|
||||
CAD/ # STEP CAD 文件,示例:11.step、40.step
|
||||
Mesh/ # STL/OBJ/PLY Mesh 文件,示例:11.stl、40.stl
|
||||
PointCloud/ # 原始/干净点云 PLY,示例:11.ply、40.ply
|
||||
Noise_PointCloud/ # 加噪或待配准点云 PLY,示例:11_noisy.ply、40_noisy.ply
|
||||
shift_test_output/ # translate-align 测试点云目录,默认读取 shifted_whole_pointcloud.ply
|
||||
```
|
||||
|
||||
当前批量评估示例数据包含编号 `11`、`13`、`17`、`20`、`22`、`23`、`25`、`26`、`27`、`31`、`34`、`35`、`36`、`37`、`38`、`39`、`40`、`41`、`42`、`45`,同一编号通常应同时存在:
|
||||
|
||||
```text
|
||||
datasets/CAD/11.step
|
||||
datasets/Mesh/11.stl
|
||||
datasets/PointCloud/11.ply
|
||||
datasets/Noise_PointCloud/11_noisy.ply
|
||||
```
|
||||
|
||||
同一任务可以包含多个样本,样本名使用下划线后缀区分;例如 `13_1.stl` 和 `13_2.stl` 都归属任务 `13`,并分别匹配 `13_1_noisy.ply`、`13_2_noisy.ply`。
|
||||
|
||||
`translate-align` 默认使用:
|
||||
|
||||
```text
|
||||
datasets/CAD/M02.stp #标准CAD
|
||||
datasets/shift_test_output/shifted_whole_pointcloud.ply #实际采集点云
|
||||
```
|
||||
|
||||
输入格式要求:
|
||||
|
||||
- CAD:推荐 STEP,扩展名通常为 `.step` 或 `.stp`。用于 `cad2mesh` 和 `contour`。
|
||||
- Mesh:支持 `trimesh` 能读取的格式,常用 `.stl`、`.obj`、`.ply`。Mesh 必须包含顶点和三角面。
|
||||
- PointCloud:推荐 ASCII 或 binary `.ply`,必须包含三维点坐标。曲率、配准、距离评估都需要非空点云。
|
||||
- 路径:可以使用相对路径或绝对路径;相对路径按当前运行目录解析,建议在项目根目录执行。
|
||||
- 单位:示例数据按毫米量级处理。CAD、Mesh 和点云必须使用同一坐标系和同一单位,否则距离、配准结果没有可比性。
|
||||
- 对齐关系:`contour` 假设点云已经与 Mesh/CAD 在同一坐标系下;`registration` 会模拟旋转后做配准。
|
||||
|
||||
数据规模注意:
|
||||
|
||||
- 百万级点云可以运行,但 `registration` 和 `contour` 会比较慢。
|
||||
- `evaluation.chunk_size` 控制点云分块大小。
|
||||
- `evaluation.triangle_chunk_size` 控制 Mesh/CAD 三角面分块大小。内存或显存不足时调小,例如 `1024` 或 `2048`。
|
||||
- `evaluation.cad_mesh_deflection` 控制 STEP 三角化精度。越小越精细,但三角面更多、计算更慢。
|
||||
|
||||
## 4. 输出说明
|
||||
|
||||
建议将通用实验结果放到 `vis/`,Excel 结果放到 `excel/`,`translate-align` 默认写入 `results/`:
|
||||
|
||||
```bash
|
||||
mkdir -p vis excel results
|
||||
```
|
||||
|
||||
常见输出包括:
|
||||
|
||||
- `.log`:命令行指标输出。
|
||||
- `.time`:命令耗时记录。
|
||||
- `.stl`:CAD 转 Mesh 结果。
|
||||
- `.ply`:带颜色的点云结果,例如曲率点云或 CAD 距离热力图。
|
||||
- `.xlsx`:`contour --excel-output` 生成的 Excel,默认保存到 `excel/contour.xlsx`;默认按 `excel/测量结果.xlsx` 模板迁移对应任务行,并写入本次测量均值和测量结论。
|
||||
- `results/aligned_pointcloud_translation_only.ply`:`translate-align` 默认输出的对齐后点云。
|
||||
- `results/T_translation_only.txt`:`translate-align` 默认输出的 4x4 平移矩阵。
|
||||
|
||||
本项目不会自动把所有命令结果写入 `vis/`;如果需要保留日志,可以用 shell 重定向:
|
||||
|
||||
```bash
|
||||
python main.py contour --pointcloud datasets/PointCloud/11.ply --cad datasets/CAD/11.step --no-visualize > vis/contour.log 2>&1
|
||||
```
|
||||
|
||||
## 5. 方法输入输出总览
|
||||
|
||||
| 方法 | 功能 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `cad2mesh` | 将 STEP CAD 转成 STL 三角网格 | `--cad`:STEP 文件 | `--mesh`:STL 文件 |
|
||||
| `denoise` | 给干净点云加噪,并运行配置中的滤波方法 | `--pointcloud`:干净 PLY 点云;`--noise-std`:噪声标准差 | 终端输出点数、平均噪声偏移、滤波保留比例、Chamfer Distance;当前不自动保存滤波点云 |
|
||||
| `registration` | 模拟旋转后进行点云到 Mesh 的配准,并评估配准后距离 | `--pointcloud`:待配准 PLY 点云;`--mesh`:参考 STL/OBJ/PLY Mesh | 终端输出 fitness、RMSE、变换矩阵、配准后点云到 Mesh 的距离统计 |
|
||||
| `contour` | 轮廓/平整度/平面度评估,计算点云到 STEP CAD 三角面的距离 | 单任务:`--pointcloud` 和 `--cad`;批量:`--tasks` 或 `--all`,并指定 `--data-root`、`--pointcloud-kind` | 终端输出 Mean、Max、RMSE、Min、Std;可用 `--save` 保存热力图 PLY;可用 `--excel-output` 保存 Excel |
|
||||
| `curvature` | 估计点云局部曲率并着色 | `--pointcloud`:PLY 点云;`--radius`:邻域半径;`--max-nn`:最大邻居数 | 终端输出曲率 mean/max;可用 `--save` 保存彩色曲率 PLY |
|
||||
| `plane` | 使用 RANSAC 拟合平面 | 可选 `--pointcloud`:PLY 点云;不传则生成模拟带噪平面 | 终端输出平面模型、内点比例、法向角度误差、RMSE;可视化拟合结果 |
|
||||
| `full` | 按配置串联运行加噪/滤波、配准和距离评估 | `--config`:YAML 配置文件;默认使用 `config.yaml` | 返回完整流程结果;终端输出各阶段指标;默认不自动执行 CAD 转 Mesh |
|
||||
|
||||
`contour --excel-output` 生成的 Excel 默认读取 `excel/测量结果.xlsx` 作为模板,按 `任务号` 迁移对应行,新表头保持模板一致:`任务号、项点名称、公差要求、理论尺寸、测量结果、误差结果、测量结论`。脚本只写入 `测量结果` 和 `测量结论`:测量结果填本次测量均值,测量成功填 `成功`,否则填 `失败`。`--excel-output` 不带值时保存为 `excel/contour.xlsx`;只传文件名时会自动保存到 `excel/` 文件夹。可用 `--result-template` 指定其它模板。
|
||||
|
||||
## 6. 快速使用
|
||||
|
||||
查看所有命令:
|
||||
|
||||
```bash
|
||||
python main.py --help
|
||||
```
|
||||
|
||||
查看某个命令参数:
|
||||
|
||||
```bash
|
||||
python main.py registration --help
|
||||
```
|
||||
|
||||
## 7. 常用命令
|
||||
轮廓/平整度/平面度评估(全部数据):
|
||||
|
||||
```bash
|
||||
python main.py contour --all --data-root datasets --pointcloud-kind noise --save vis/contour --excel-output all_contour.xlsx --no-visualize
|
||||
```
|
||||
|
||||
批量模式会优先从 `datasets/Mesh/{样本名}.stl` 读取 Mesh,并按样本名匹配点云。`--tasks 13` 会匹配 `13_1.stl`、`13_2.stl` 等同属任务 13 的样本;`--tasks 13_1` 只运行该样本。`--pointcloud-kind noise` 读取 `datasets/Noise_PointCloud/{样本名}_noisy.ply`;`--pointcloud-kind clean` 读取 `datasets/PointCloud/{样本名}.ply`。如果没有找到 Mesh 样本,则退回 `datasets/CAD/{任务号}.step`。批量模式下 `--save vis/contour` 会按样本名保存为 `vis/contour/{样本名}_contour.ply`,并按原始坐标额外保存合并点云 `vis/contour/combined_contour.ply`;未传 `--save` 时合并点云默认保存到 `vis/combined_contour.ply`。合并点云默认会加入完整点云 `datasets/train/train_2_10M.ply`,可用 `--combined-context-pointcloud datasets/train/train_2_100M.ply` 或 `datasets/train/train_2_1T_配准.ply` 切换;保存合并 PLY 时会按块流式读取大背景点云,不再整体加载进内存。Excel 会按任务号把模板中的对应行迁移到新表;批量模式会在终端最后打印总执行时间 `Contour batch total execution time`,该时间包含任务计算、Excel 保存和合并 PLY 保存。未传 `--no-visualize` 时,所有成功热力图会按原始坐标合并到一个 Open3D 窗口中展示。使用 `train_2_1T_配准.ply` 时必须加 `--no-visualize`,只保存合并结果,不打开窗口。
|
||||
|
||||
使用 `datasets/train/train_2_1T_配准.ply` 作为完整背景点云保存合并结果:
|
||||
注意:使用时,利用combined-context-pointcloud指定配准后的点云路径
|
||||
|
||||
```bash
|
||||
conda activate High-Speed
|
||||
python main.py contour --all --data-root datasets --pointcloud-kind noise --save vis/contour --excel-output all_contour.xlsx --combined-context-pointcloud datasets/train/train_2_1T_配准.ply --no-visualize
|
||||
```
|
||||
|
||||
CAD 转 Mesh:
|
||||
|
||||
```bash
|
||||
python main.py cad2mesh --cad datasets/CAD/11.step --mesh vis/11_cad2mesh.stl --precision 1.0
|
||||
```
|
||||
|
||||
加噪与去噪:
|
||||
|
||||
```bash
|
||||
python main.py denoise --pointcloud datasets/PointCloud/11.ply --noise-std 4 --no-visualize
|
||||
```
|
||||
|
||||
点云配准与 Mesh 距离评估:
|
||||
|
||||
```bash
|
||||
python main.py registration --pointcloud datasets/Noise_PointCloud/11_noisy.ply --mesh datasets/Mesh/11.stl --no-visualize
|
||||
```
|
||||
|
||||
轮廓/平整度/平面度评估(快速查看统计结果):
|
||||
|
||||
```bash
|
||||
python main.py contour --pointcloud datasets/PointCloud/11.ply --cad datasets/CAD/11.step --no-visualize
|
||||
```
|
||||
|
||||
轮廓/平整度/平面度评估(保存热力图和 Excel):
|
||||
|
||||
```bash
|
||||
python main.py contour --cad datasets/CAD/11.step --pointcloud datasets/Noise_PointCloud/11_noisy.ply --save vis/11_contour.ply --excel-output --no-visualize
|
||||
```
|
||||
|
||||
`contour` 是轮廓/平整度/平面度评估的统一入口:不加导出参数时适合快速在命令行查看统计结果;添加 `--save` 时会保存距离热力图 PLY;添加 `--excel-output` 时会把统计结果保存到 `excel/contour.xlsx`。也可以写成 `--excel-output 11_contour.xlsx`,结果会保存到 `excel/11_contour.xlsx`。Excel 默认使用 `excel/测量结果.xlsx` 模板,可通过 `--result-template` 替换。
|
||||
|
||||
轮廓/平整度/平面度评估(指定多个数据):
|
||||
|
||||
```bash
|
||||
python main.py contour --tasks 13,31,40 --data-root datasets --pointcloud-kind noise --save vis/contour --excel-output selected_contour.xlsx --no-visualize
|
||||
```
|
||||
|
||||
曲率估计:
|
||||
|
||||
```bash
|
||||
python main.py curvature --pointcloud datasets/PointCloud/11.ply --radius 2 --max-nn 30 --save vis/11_curvature.ply --no-visualize
|
||||
```
|
||||
|
||||
平面拟合:
|
||||
|
||||
```bash
|
||||
python main.py plane --no-visualize
|
||||
```
|
||||
|
||||
完整流程:
|
||||
|
||||
```bash
|
||||
python main.py --config config.yaml full --no-visualize
|
||||
```
|
||||
|
||||
`full` 会按配置串联加噪/滤波、配准和距离评估。默认 `cad.enabled: false`,不会自动执行 CAD 转 Mesh。
|
||||
|
||||
## 8. 打包使用
|
||||
|
||||
生成独立 Python 包源码:
|
||||
|
||||
```bash
|
||||
python packaging/build_package.py
|
||||
```
|
||||
|
||||
本地开发安装:
|
||||
|
||||
```bash
|
||||
pip install -e packaging/build
|
||||
geometry-measure --help
|
||||
```
|
||||
|
||||
构建 wheel:
|
||||
|
||||
```bash
|
||||
pip install build
|
||||
python packaging/build_package.py
|
||||
python -m build packaging/build
|
||||
```
|
||||
|
||||
打包说明见 [packaging/README.md](packaging/README.md)。
|
||||
|
||||
## 9. 输入不同数据
|
||||
|
||||
方式一:命令行覆盖路径。
|
||||
|
||||
点云配准:
|
||||
|
||||
```bash
|
||||
python main.py registration \
|
||||
--pointcloud datasets/Noise_PointCloud/40_noisy.ply \
|
||||
--mesh datasets/Mesh/40.stl \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
单个数据快速评估:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--pointcloud datasets/PointCloud/40.ply \
|
||||
--cad datasets/CAD/40.step \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
单个数据保存热力图和 Excel:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--cad datasets/CAD/40.step \
|
||||
--pointcloud datasets/Noise_PointCloud/40_noisy.ply \
|
||||
--save vis/40_contour.ply \
|
||||
--excel-output 40_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
指定多个数据批量评估:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--tasks 13,31,40 \
|
||||
--data-root datasets \
|
||||
--pointcloud-kind noise \
|
||||
--save vis/contour \
|
||||
--excel-output selected_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
全部数据批量评估:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--all \
|
||||
--data-root datasets \
|
||||
--pointcloud-kind noise \
|
||||
--save vis/contour \
|
||||
--excel-output all_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
方式二:修改 `config.yaml`。
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
cad: datasets/CAD/40.step
|
||||
mesh: datasets/Mesh/40.stl
|
||||
pointcloud: datasets/PointCloud/40.ply
|
||||
noisy_pointcloud: datasets/Noise_PointCloud/40_noisy.ply
|
||||
output_mesh: vis/40_cad2mesh.stl
|
||||
curvature_output: vis/40_curvature.ply
|
||||
```
|
||||
|
||||
然后运行:
|
||||
|
||||
```bash
|
||||
python main.py --config config.yaml full --no-visualize
|
||||
```
|
||||
|
||||
## 10. 参数配置
|
||||
|
||||
常用参数都在 `config.yaml`:
|
||||
|
||||
- `noise.std`:高斯噪声标准差。
|
||||
- `filters.methods`:启用哪些滤波器。
|
||||
- `registration.voxel_size`:配准下采样体素大小。
|
||||
- `registration.roll/pitch/yaw`:模拟旋转角度。
|
||||
- `registration.trans_range`:模拟平移范围。
|
||||
- `evaluation.chunk_size`:点云到 Mesh 距离分块大小。
|
||||
- `evaluation.triangle_chunk_size`:距离计算时三角面分块大小,显存/内存不足时调小。
|
||||
- `evaluation.cad_mesh_deflection`:STEP CAD 三角化精度,越小越精细但越慢。
|
||||
- `visualization.enabled`:是否默认开启可视化。
|
||||
|
||||
命令行参数优先级高于配置文件。
|
||||
|
||||
## 11. 命令说明
|
||||
|
||||
### 11.1 `translate-align`
|
||||
|
||||
- 用途:将 `archive/translate_align_pointcloud_to_cad.py` 接入统一入口,按车头/车尾局部点的质心差估计整体平移,只修正平移,不修正旋转。
|
||||
- 默认输入:`--cad datasets/CAD/M02.stp --pointcloud datasets/shift_test_output/`。当 `--pointcloud` 传目录时,会优先读取目录下的 `shifted_whole_pointcloud.ply`。
|
||||
- 默认输出目录:`results/`。
|
||||
- 常用调参:`--axis` 指定车辆长度方向;`--crop-ratio` 控制头尾截取比例;`--cad-sample-points` 控制从 CAD mesh 采样的点数;`--local-sample-points` 控制头尾局部用于算质心的采样点数;`--scale-pointcloud` 用于 CAD 与点云单位不一致时缩放点云。
|
||||
- 输出文件:
|
||||
|
||||
```text
|
||||
results/aligned_pointcloud_translation_only.ply # 最终配准后的完整点云
|
||||
results/T_translation_only.txt # 4x4 平移矩阵,可复用到其它点云或评估流程
|
||||
results/cad_sampled.ply # 从 CAD mesh 采样得到的 CAD 点云
|
||||
results/cad_front_sample.ply # CAD 车头区域采样点,用于检查质心计算
|
||||
results/cad_tail_sample.ply # CAD 车尾区域采样点,用于检查质心计算
|
||||
results/source_front_sample.ply # 输入点云车头区域采样点,用于检查质心计算
|
||||
results/source_tail_sample.ply # 输入点云车尾区域采样点,用于检查质心计算
|
||||
```
|
||||
|
||||
- 终端输出:前端/后端质心、前端平移量 `t_front`、后端平移量 `t_tail`、二者差值、最终融合平移 `t_final` 和最终矩阵 `T_final`。如果前后端平移差值较大,通常说明点云和 CAD 存在旋转误差、裁剪区域不合适或单位不一致。
|
||||
- 示例:
|
||||
|
||||
```bash
|
||||
python main.py translate-align --cad datasets/CAD/M02.stp --pointcloud datasets/shift_test_output/ --output-dir results --axis x --crop-ratio 0.01 --no-visualize
|
||||
```
|
||||
|
||||
也可以直接运行辅助脚本:
|
||||
|
||||
```bash
|
||||
python archive/translate_align_pointcloud_to_cad.py -c datasets/CAD/M02.stp -p datasets/shift_test_output/ -o results --no_visualize
|
||||
```
|
||||
|
||||
### 11.2 `cad2mesh`
|
||||
|
||||
- 输入:STEP 文件。
|
||||
- 输出:STL 文件。
|
||||
- 依赖:`pythonocc-core`。
|
||||
- 典型用途:将 CAD 转成后续可用于 Mesh 距离评估的三角网格。
|
||||
|
||||
### 11.3 `denoise`
|
||||
|
||||
- 输入:干净点云。
|
||||
- 输出:终端指标;当前不会自动保存滤波后的点云。
|
||||
- 指标:点数、平均噪声偏移、滤波保留比例、Chamfer Distance。
|
||||
|
||||
### 11.4 `registration`
|
||||
|
||||
- 输入:点云和参考 Mesh。
|
||||
- 输出:配准 fitness、RMSE、变换矩阵、配准后点云到 Mesh 距离统计。
|
||||
- 注意:百万级点云可能耗时较长;当前 Open3D 配准部分主要走 CPU。
|
||||
|
||||
### 11.5 `contour`
|
||||
|
||||
- 用途:轮廓/平整度/平面度评估的统一入口。
|
||||
- 输入:单任务使用点云和 STEP CAD;批量任务使用 `--tasks` 或 `--all` 从 `--data-root` 中读取多组 CAD/点云;如果显式传 `--mesh`,会走点云到 Mesh 距离计算。
|
||||
- 输出:命令行打印点云到 CAD 三角面的 Mean、Max、RMSE、Min、Std;可通过 `--save` 保存距离热力图 PLY,可通过 `--excel-output` 保存 Excel。
|
||||
- 注意:这是全量点到三角面的距离计算,数据越大越慢;STEP 三角化精度由 `--mesh-deflection` 控制。
|
||||
- 常用调参:`--mesh-deflection`、`--point-chunk-size`、`--triangle-chunk-size`。
|
||||
- 批量参数:`--tasks 13,31,40` 执行指定任务,其中 `13` 会自动展开 `13_1`、`13_2` 等样本;`--tasks 13_1` 只执行单个样本;`--all` 优先执行 `--data-root/Mesh` 下全部样本,没有 Mesh 时退回 CAD;`--pointcloud-kind noise|clean` 选择加噪点云或干净点云。
|
||||
- 批量热力图:`--save vis/contour` 会为每个任务生成一个 PLY,并保存合并点云 `vis/contour/combined_contour.ply`;也可以用 `--save vis/{task_id}_contour.ply` 自定义文件名模板,合并点云会保存为 `vis/combined_contour.ply`。未传 `--save` 时,合并点云默认保存为 `vis/combined_contour.ply`。合并点云默认包含完整点云 `datasets/train/train_2_10M.ply`,可用 `--combined-context-pointcloud` 切换为 `train_2_100M.ply` 或 `train_2_1T_配准.ply`;保存合并 PLY 时大背景点云会流式写入,输出文件会因为追加 RGB 颜色而比原始背景点云更大。
|
||||
- 批量可视化:未传 `--no-visualize` 时,所有成功执行的热力图会按原始坐标合并到一个窗口展示,不会为了展示改变点云位置。使用 `train_2_1T_配准.ply` 时必须传 `--no-visualize`,不要打开 Open3D 窗口。
|
||||
- Excel 输出:`--excel-output` 不带值时保存为 `excel/contour.xlsx`;只传文件名时自动保存到 `excel/` 文件夹。默认读取 `excel/测量结果.xlsx` 作为模板,按 `任务号` 迁移对应行,表头保持模板一致,只写入 `测量结果` 和 `测量结论`。可用 `--result-template` 指定其它模板。
|
||||
|
||||
### 11.6 `archive/Contour_Flatness.py`
|
||||
|
||||
- 默认批量模式:优先从 `Mesh/{样本名}.stl` 和 `Noise_PointCloud/{样本名}_noisy.ply` 读取样本;例如 `13_1.stl`、`13_2.stl` 都归属任务 `13`,Excel 会按任务号汇总。
|
||||
- 单任务模式:加 `--single` 后使用 `--CAD/-c` 和 `--pointcloud/-p`,可加 `--visualize` 显示距离热力图;批量模式加 `--visualize` 会把所有成功热力图合并到一个窗口展示。使用 `datasets/train/train_2_1T_配准.ply` 作为 `--combined_context_pointcloud` 时不要加 `--visualize`。
|
||||
- 距离计算:默认参考 `archive/test8.py`,先用点到三角形 AABB 的距离筛出每个点附近的候选三角面,再计算精确点到三角形距离;`--candidate_triangle_count 0` 可退回全量精确搜索。
|
||||
- Excel 输出:默认读取 `excel/测量结果.xlsx` 作为模板,按 `任务号` 迁移对应行,新表头保持模板一致:`任务号、项点名称、公差要求、理论尺寸、测量结果、误差结果、测量结论`。脚本只写入 `测量结果` 和 `测量结论`:测量结果填本次测量均值,测量成功填 `成功`,否则填 `失败`。可用 `--result_template` 指定其它模板。
|
||||
- 示例:
|
||||
|
||||
```bash
|
||||
python archive/Contour_Flatness.py --tasks "13,31,40" --data_root datasets --pointcloud_kind noise --excel_output vis/flatness.xlsx --result_template excel/测量结果.xlsx --candidate_triangle_count 32
|
||||
python archive/Contour_Flatness.py --single -c High_Speed_Data/CAD/11.step -p High_Speed_Data/Noise_PointCloud/11_noisy.ply --candidate_triangle_count 32 --visualize
|
||||
```
|
||||
|
||||
### 11.7 `curvature`
|
||||
|
||||
- 输入:点云。
|
||||
- 输出:曲率 mean/max;可通过 `--save` 保存彩色曲率点云。
|
||||
|
||||
### 11.8 `plane`
|
||||
|
||||
- 输入:可选点云;不传点云时会生成一块带噪平面用于测试。
|
||||
- 输出:平面模型、内点比例、法向角度误差、RMSE。
|
||||
|
||||
## 12. 常见问题
|
||||
|
||||
`No module named OCC`:
|
||||
|
||||
说明 CAD 依赖没有装好。执行:
|
||||
|
||||
```bash
|
||||
conda env update -n High-Speed -f environment.yml
|
||||
```
|
||||
|
||||
`pythonocc-core` 无法通过 pip 安装:
|
||||
|
||||
这是正常的,`pythonocc-core` 应通过 conda 安装。不要把它放进 `requirements.txt`。
|
||||
|
||||
`pythonocc-core=7.5.1` 和 Python 3.12 冲突:
|
||||
|
||||
新建 Python 3.10 环境,不要在 base 里安装。
|
||||
|
||||
`registration` 或 `contour` 很慢:
|
||||
|
||||
这是因为示例点云超过百万点,且距离计算要比较大量三角面。可以先换小点云测试,或者调小点数、增大 `registration.voxel_size`、调小 `evaluation.chunk_size`/`triangle_chunk_size` 以降低内存压力。
|
||||
|
||||
CUDA warning:
|
||||
|
||||
如果 torch 提示 NVIDIA driver 太旧,距离计算会退回 CPU。功能不受影响,但速度会慢。
|
||||
720
alg/平整度、平面度、轮廓测量v1.5/USAGE.md
Normal file
720
alg/平整度、平面度、轮廓测量v1.5/USAGE.md
Normal file
@@ -0,0 +1,720 @@
|
||||
# USAGE
|
||||
|
||||
本文档面向后续开发人员,说明本项目的运行环境、数据约定、命令输入输出、常用工作流和排错方法。
|
||||
|
||||
项目根目录下的 `main.py` 是统一命令行入口。除非特别说明,所有命令都建议在项目根目录执行。
|
||||
|
||||
```bash
|
||||
python main.py --help
|
||||
```
|
||||
|
||||
当前公开命令:
|
||||
|
||||
```text
|
||||
translate-align
|
||||
cad2mesh
|
||||
denoise
|
||||
registration
|
||||
contour
|
||||
curvature
|
||||
plane
|
||||
full
|
||||
```
|
||||
|
||||
## 1. 环境准备
|
||||
|
||||
推荐使用独立 conda 环境。当前项目建议使用 `High-Speed` 环境运行;不要直接在 `base` 环境中安装,尤其不要在 Python 3.12 的 base 环境里安装 `pythonocc-core`。
|
||||
|
||||
```bash
|
||||
conda env create -n High-Speed -f environment.yml
|
||||
conda activate High-Speed
|
||||
pip install -r requirements.txt
|
||||
pip install git+https://github.com/AutodeskAILab/occwl
|
||||
```
|
||||
|
||||
如果环境已经存在:
|
||||
|
||||
```bash
|
||||
conda env update -n High-Speed -f environment.yml
|
||||
conda activate High-Speed
|
||||
pip install -r requirements.txt
|
||||
pip install git+https://github.com/AutodeskAILab/occwl
|
||||
```
|
||||
|
||||
验证依赖:
|
||||
|
||||
```bash
|
||||
python -c "import open3d, trimesh, torch, yaml, OCC.Core.STEPControl; print('env ok')"
|
||||
```
|
||||
|
||||
如果 `torch` 提示 CUDA driver 版本过旧,程序会退回 CPU。功能不受影响,但点云到三角面的距离计算会慢。
|
||||
|
||||
## 2. 数据目录约定
|
||||
|
||||
默认示例数据放在 `datasets/`:
|
||||
|
||||
```text
|
||||
datasets/
|
||||
CAD/ # STEP CAD,例如 11.step
|
||||
Mesh/ # STL/OBJ/PLY Mesh,例如 11.stl
|
||||
PointCloud/ # 原始/干净点云 PLY,例如 11.ply
|
||||
Noise_PointCloud/ # 加噪或待配准点云 PLY,例如 11_noisy.ply
|
||||
shift_test_output/ # translate-align 测试输出,默认读取 shifted_whole_pointcloud.ply
|
||||
```
|
||||
|
||||
同一编号的数据通常应成组存在:
|
||||
|
||||
```text
|
||||
datasets/CAD/11.step
|
||||
datasets/Mesh/11.stl
|
||||
datasets/PointCloud/11.ply
|
||||
datasets/Noise_PointCloud/11_noisy.ply
|
||||
```
|
||||
|
||||
`translate-align` 默认使用下面这组输入:
|
||||
|
||||
```text
|
||||
datasets/CAD/M02.stp
|
||||
datasets/shift_test_output/shifted_whole_pointcloud.ply
|
||||
```
|
||||
|
||||
数据要求:
|
||||
|
||||
- CAD 使用 STEP,扩展名通常是 `.step` 或 `.stp`。
|
||||
- Mesh 使用 `trimesh` 可读取的格式,例如 `.stl`、`.obj`、`.ply`。
|
||||
- PointCloud 推荐 PLY,必须包含三维点坐标。
|
||||
- CAD、Mesh、PointCloud 必须处于同一坐标系和同一单位。
|
||||
- 示例数据按毫米量级处理。
|
||||
- `contour` 假设点云已与 CAD/Mesh 对齐。
|
||||
- `registration` 会模拟旋转后做配准。
|
||||
|
||||
## 3. 输出目录约定
|
||||
|
||||
建议保留以下输出目录:
|
||||
|
||||
```text
|
||||
vis/ # 日志、热力图点云、曲率点云、中间结果
|
||||
excel/ # contour 输出的 Excel 表
|
||||
results/ # translate-align 默认输出目录
|
||||
```
|
||||
|
||||
可以手动创建:
|
||||
|
||||
```bash
|
||||
mkdir -p vis excel results
|
||||
```
|
||||
|
||||
常见输出:
|
||||
|
||||
```text
|
||||
*.stl # CAD 转 Mesh 结果
|
||||
*.ply # 彩色点云、热力图点云
|
||||
*.xlsx # 轮廓/平整度/平面度 Excel 汇总
|
||||
*.log # shell 重定向保存的终端输出
|
||||
results/T_translation_only.txt
|
||||
results/aligned_pointcloud_translation_only.ply
|
||||
```
|
||||
|
||||
## 4. 方法输入输出总览
|
||||
|
||||
| 方法 | 用途 | 输入 | 输出 |
|
||||
|------|------|------|------|
|
||||
| `translate-align` | 点云到标准CAD的配准 | 默认 `--cad datasets/CAD/M02.stp --pointcloud datasets/shift_test_output/`;可调头尾截取和采样参数 | 默认写入 `results/`,输出对齐后点云、4x4 平移矩阵和头尾采样检查文件 |
|
||||
| `cad2mesh` | STEP CAD 转 STL Mesh | `--cad` STEP 文件 | `--mesh` STL 文件 |
|
||||
| `denoise` | 给干净点云加噪,并运行滤波方法 | `--pointcloud` 干净 PLY;`--noise-std` 噪声标准差 | 终端输出点数、平均噪声偏移、滤波保留比例、Chamfer Distance |
|
||||
| `registration` | 点云到 Mesh 配准与距离评估 | `--pointcloud` 待配准 PLY;`--mesh` 参考 Mesh | 终端输出 fitness、RMSE、变换矩阵、配准后距离统计 |
|
||||
| `contour` | 轮廓/平整度/平面度评估 | 单任务:`--pointcloud` 和 `--cad`;批量:`--tasks` 或 `--all`,并指定 `--data-root`、`--pointcloud-kind` | 终端输出 Mean、Max、RMSE、Min、Std;可保存热力图 PLY 和 Excel |
|
||||
| `curvature` | 点云曲率估计 | `--pointcloud` PLY;`--radius` 邻域半径;`--max-nn` 最大邻居数 | 终端输出曲率 mean/max;可保存彩色曲率 PLY |
|
||||
| `plane` | RANSAC 平面拟合 | 可选 `--pointcloud` PLY;不传则生成模拟带噪平面 | 终端输出平面模型、内点比例、法向角度误差、RMSE |
|
||||
| `full` | 配置驱动的完整流程 | `--config` YAML 配置;默认 `config.yaml` | 串联输出加噪/滤波、配准和距离评估结果 |
|
||||
|
||||
## 5. 命令详解
|
||||
|
||||
### 5.1 轮廓/平整度/平面度评估:`contour`
|
||||
|
||||
功能:统一的轮廓/平整度/平面度评估入口。默认读取 STEP CAD,三角化后计算点云到 CAD 三角面的最近距离。
|
||||
|
||||
输入:
|
||||
|
||||
- `--pointcloud`:待评估 PLY 点云。
|
||||
- `--cad`:参考 STEP CAD。
|
||||
- `--mesh`:可传参考 STL/OBJ/PLY Mesh。传了 `--mesh` 时会走点云到 Mesh 距离计算。
|
||||
- `--tasks`:批量执行指定任务编号,用英文逗号分隔,例如 `11,31,40`。
|
||||
- `--all`:批量执行 `--data-root` 下 `CAD/` 中的全部 STEP 数据。
|
||||
- `--data-root`:批量数据根目录,默认 `datasets`。
|
||||
- `--pointcloud-kind`:批量点云来源,`noise` 读取 `Noise_PointCloud/{id}_noisy.ply`,`clean` 读取 `PointCloud/{id}.ply`。
|
||||
- `--mesh-deflection`:STEP 三角化精度,越小越精细,但三角面更多、计算更慢。
|
||||
- `--point-chunk-size`:点云分块大小。
|
||||
- `--triangle-chunk-size`:三角面分块大小。
|
||||
- `--save`:保存距离热力图 PLY。
|
||||
- `--excel-output`:保存 Excel。
|
||||
- `--result-template`:Excel 参考模板,默认 `excel/测量结果.xlsx`。
|
||||
- `--combined-context-pointcloud`:合并热力图时一起加入的完整点云,默认 `datasets/train/train_2_10M.ply`。可指定 `datasets/train/train_2_1T_配准.ply`;保存合并 PLY 时会按块流式写入,避免整体加载完整点云。
|
||||
- `--no-visualize`:禁用 Open3D 窗口。使用 `train_2_1T_配准.ply` 时必须加该参数,否则窗口展示仍会尝试加载超大点云。
|
||||
|
||||
输出:
|
||||
|
||||
- 终端距离统计:Mean、Max、RMSE、Min、Std。
|
||||
- 如果传 `--save`:保存彩色热力图 PLY。
|
||||
- 如果传 `--excel-output`:保存 Excel。
|
||||
- 批量模式:所有成功执行的热力图会按原始坐标额外保存成一个合并 PLY;保存阶段支持对 `datasets/train/train_2_1T_配准.ply` 进行流式合并。未传 `--no-visualize` 时也会按原始坐标合并到一个 Open3D 窗口展示,但超大背景点云不适合窗口展示。
|
||||
- 批量模式会在终端最后打印总执行时间 `Contour batch total execution time`,该时间包含任务计算、Excel 保存和合并 PLY 保存。
|
||||
|
||||
Excel 格式:
|
||||
|
||||
```text
|
||||
任务号 | 项点名称 | 公差要求 | 理论尺寸 | 测量结果 | 误差结果 | 测量结论
|
||||
```
|
||||
|
||||
默认读取 `excel/测量结果.xlsx` 作为模板,按 `任务号` 迁移对应行到新 Excel。程序只填本次的 `测量结果` 和 `测量结论`:测量结果为本次测量均值;测量成功填 `成功`,否则填 `失败`。如果需要使用其它模板,传 `--result-template path/to/template.xlsx`。
|
||||
|
||||
任务号从 CAD 文件名提取。例如:
|
||||
|
||||
```text
|
||||
datasets/CAD/11.step -> 11
|
||||
```
|
||||
|
||||
Excel 路径规则:
|
||||
|
||||
- `--excel-output` 不带值:保存到 `excel/contour.xlsx`
|
||||
- `--excel-output 11_contour.xlsx`:保存到 `excel/11_contour.xlsx`
|
||||
- `--excel-output reports/11.xlsx`:保存到 `reports/11.xlsx`
|
||||
- `--result-template` 不传:默认读取 `excel/测量结果.xlsx`
|
||||
|
||||
批量模式路径规则:
|
||||
|
||||
- CAD:`{data_root}/CAD/{任务号}.step`
|
||||
- 加噪点云:`{data_root}/Noise_PointCloud/{任务号}_noisy.ply`
|
||||
- 干净点云:`{data_root}/PointCloud/{任务号}.ply`
|
||||
- 如果 `--save` 是目录,例如 `--save vis/contour`,每个任务会保存为 `vis/contour/{任务号}_contour.ply`
|
||||
- 如果 `--save` 是带 `{task_id}` 的文件模板,例如 `--save vis/{task_id}_contour.ply`,会替换任务号
|
||||
- 如果 `--save` 是普通 PLY 文件,例如 `--save vis/contour.ply`,批量模式会保存为 `vis/contour_{任务号}.ply`
|
||||
- 合并点云保存规则:未传 `--save` 时保存为 `vis/combined_contour.ply`;`--save vis/contour` 时保存为 `vis/contour/combined_contour.ply`;`--save vis/{task_id}_contour.ply` 时保存为 `vis/combined_contour.ply`;`--save vis/contour.ply` 时保存为 `vis/contour_combined.ply`
|
||||
- 合并点云默认会加入 `datasets/train/train_2_10M.ply`;可用 `--combined-context-pointcloud datasets/train/train_2_100M.ply` 或 `datasets/train/train_2_1T_配准.ply` 切换。保存合并 PLY 时,大背景点云按块从输入文件写入输出文件,不再用 Open3D 整体读入内存;输出文件会比原始背景点云更大,因为会额外写入 RGB 颜色。注意:由于 `train_2_100M.ply` 或 `train_2_1T_配准.ply` 太大,通常需要使用者自行导入或修改地址。
|
||||
- 如果没有传 `--no-visualize`,批量模式会把所有成功热力图合并显示在一个窗口里。使用 `train_2_1T_配准.ply` 时不要打开窗口,必须保留 `--no-visualize`。
|
||||
|
||||
快速查看统计结果:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--pointcloud datasets/PointCloud/11.ply \
|
||||
--cad datasets/CAD/11.step \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
保存热力图和 Excel:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--cad datasets/CAD/11.step \
|
||||
--pointcloud datasets/Noise_PointCloud/11_noisy.ply \
|
||||
--save vis/11_contour.ply \
|
||||
--excel-output 11_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
指定多个任务:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--tasks 11,31,40 \
|
||||
--data-root datasets \
|
||||
--pointcloud-kind noise \
|
||||
--save vis/contour \
|
||||
--excel-output selected_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
执行全部任务:
|
||||
|
||||
```bash
|
||||
conda activate High-Speed
|
||||
python main.py contour \
|
||||
--all \
|
||||
--data-root datasets \
|
||||
--pointcloud-kind noise \
|
||||
--save vis/contour \
|
||||
--excel-output all_contour.xlsx \
|
||||
--combined-context-pointcloud datasets/train/train_2_1T_配准.ply \
|
||||
--no-visualize
|
||||
```
|
||||
注意:使用时,利用combined-context-pointcloud指定配准后的点云路径
|
||||
该模式只保存合并 PLY,不打开 Open3D 展示窗口。`train_2_1T_配准.ply` 会被流式读取,适合用于生成最终大文件;不要去掉 `--no-visualize`。
|
||||
|
||||
显存或内存不足时调小分块:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--cad datasets/CAD/11.step \
|
||||
--pointcloud datasets/Noise_PointCloud/11_noisy.ply \
|
||||
--mesh-deflection 0.5 \
|
||||
--point-chunk-size 512 \
|
||||
--triangle-chunk-size 1024 \
|
||||
--excel-output \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
### 5.2 CAD 转 Mesh:`cad2mesh`
|
||||
|
||||
功能:读取 STEP CAD,离散成 STL 三角网格。
|
||||
|
||||
输入:
|
||||
|
||||
- `--cad`:输入 STEP 文件。
|
||||
- `--mesh`:输出 STL 文件。
|
||||
- `--precision`:离散精度,单位为 mm。值越小通常越精细,生成文件也可能越大。
|
||||
|
||||
输出:
|
||||
|
||||
- 一个 STL 文件。
|
||||
- 终端输出 STEP 读取、网格化和 STL 写入进度。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
python main.py cad2mesh \
|
||||
--cad datasets/CAD/11.step \
|
||||
--mesh vis/11_cad2mesh.stl \
|
||||
--precision 1.0
|
||||
```
|
||||
|
||||
### 5.3 加噪与滤波:`denoise`
|
||||
|
||||
功能:读取干净点云,加高斯噪声,并按配置运行滤波方法。
|
||||
|
||||
输入:
|
||||
|
||||
- `--pointcloud`:干净 PLY 点云。
|
||||
- `--noise-std`:高斯噪声标准差。
|
||||
- `--no-visualize`:禁用 Open3D 窗口。
|
||||
|
||||
输出:
|
||||
|
||||
- 终端输出干净点数、加噪点数、平均噪声偏移。
|
||||
- 对每个滤波方法输出保留比例和 Chamfer Distance。
|
||||
- 当前主流程不会自动保存滤波后的点云文件。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
python main.py denoise \
|
||||
--pointcloud datasets/PointCloud/11.ply \
|
||||
--noise-std 4 \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
### 5.4 配准:`registration`
|
||||
|
||||
功能:对输入点云模拟旋转,然后通过 RANSAC + ICP 配准到 Mesh,并计算配准后距离。
|
||||
|
||||
输入:
|
||||
|
||||
- `--pointcloud`:待配准 PLY 点云,通常使用 `datasets/Noise_PointCloud/*_noisy.ply`。
|
||||
- `--mesh`:参考 STL/OBJ/PLY Mesh。
|
||||
- `--voxel-size`:配准下采样体素大小。
|
||||
- `--roll`、`--pitch`、`--yaw`:模拟旋转角度,单位为 degree。
|
||||
- `--trans-range`:随机平移范围。
|
||||
- `--no-visualize`:禁用 Open3D 窗口。
|
||||
|
||||
输出:
|
||||
|
||||
- `Fitness`
|
||||
- `RMSE`
|
||||
- 4x4 transformation matrix
|
||||
- 配准后点云到 Mesh 的距离统计:Mean、Max、RMSE、Min、Std
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
python main.py registration \
|
||||
--pointcloud datasets/Noise_PointCloud/11_noisy.ply \
|
||||
--mesh datasets/Mesh/11.stl \
|
||||
--voxel-size 10 \
|
||||
--roll 10 \
|
||||
--pitch 10 \
|
||||
--yaw 10 \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
|
||||
### 5.5 曲率估计:`curvature`
|
||||
|
||||
功能:计算点云局部曲率,并按曲率着色。
|
||||
|
||||
输入:
|
||||
|
||||
- `--pointcloud`:PLY 点云。
|
||||
- `--radius`:邻域搜索半径。
|
||||
- `--max-nn`:最大邻居数。
|
||||
- `--color-scale`:颜色缩放系数。
|
||||
- `--save`:保存彩色曲率点云。
|
||||
- `--no-visualize`:禁用 Open3D 窗口。
|
||||
|
||||
输出:
|
||||
|
||||
- 终端输出曲率 mean 和 max。
|
||||
- 如果传 `--save`:保存彩色曲率 PLY。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
python main.py curvature \
|
||||
--pointcloud datasets/PointCloud/11.ply \
|
||||
--radius 2 \
|
||||
--max-nn 30 \
|
||||
--save vis/11_curvature.ply \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
### 5.6 平面拟合:`plane`
|
||||
|
||||
功能:使用 RANSAC 拟合平面。
|
||||
|
||||
输入:
|
||||
|
||||
- `--pointcloud`:可选 PLY 点云。不传时会根据 `config.yaml` 生成模拟带噪平面。
|
||||
- `--threshold-mm`:RANSAC 距离阈值,单位 mm。
|
||||
- `--max-iterations`:RANSAC 最大迭代次数。
|
||||
- `--no-visualize`:禁用 Open3D 窗口。
|
||||
|
||||
输出:
|
||||
|
||||
- 平面模型参数。
|
||||
- 内点比例。
|
||||
- 法向角度误差。
|
||||
- 平面 RMSE。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
python main.py plane --no-visualize
|
||||
```
|
||||
|
||||
带输入点云:
|
||||
|
||||
```bash
|
||||
python main.py plane \
|
||||
--pointcloud datasets/PointCloud/11.ply \
|
||||
--threshold-mm 0.5 \
|
||||
--max-iterations 2000 \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
### 5.7 完整流程:`full`
|
||||
|
||||
功能:按 `config.yaml` 串联运行流程。
|
||||
|
||||
当前默认行为:
|
||||
|
||||
- 如果 `cad.enabled: false`,不会自动执行 CAD 转 Mesh。
|
||||
- 如果 `noise.enabled: true`,会先运行 `denoise`,再对加噪点云做配准。
|
||||
- 最后运行 `contour`。
|
||||
|
||||
输入:
|
||||
|
||||
- `--config`:配置文件路径。不传时使用内置默认配置。
|
||||
- `--no-visualize`:禁用 Open3D 窗口。
|
||||
|
||||
输出:
|
||||
|
||||
- 每个阶段的终端指标。
|
||||
- Python 调用时会返回一个结果字典。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
python main.py --config config.yaml full --no-visualize
|
||||
```
|
||||
|
||||
## 6. 配置文件
|
||||
|
||||
默认配置在 `config.yaml`。
|
||||
|
||||
常用字段:
|
||||
|
||||
```yaml
|
||||
paths:
|
||||
cad: datasets/CAD/11.step
|
||||
mesh: datasets/Mesh/11.stl
|
||||
pointcloud: datasets/PointCloud/11.ply
|
||||
noisy_pointcloud: datasets/Noise_PointCloud/11_noisy.ply
|
||||
output_mesh: datasets/Mesh/11.stl
|
||||
curvature_output:
|
||||
|
||||
cad:
|
||||
enabled: false
|
||||
precision: 1.0
|
||||
|
||||
noise:
|
||||
enabled: true
|
||||
std: 4.0
|
||||
|
||||
registration:
|
||||
voxel_size: 10.0
|
||||
roll: 10.0
|
||||
pitch: 10.0
|
||||
yaw: 10.0
|
||||
trans_range: 0.0
|
||||
|
||||
evaluation:
|
||||
chunk_size: 2048
|
||||
triangle_chunk_size: 4096
|
||||
cad_mesh_deflection: 0.1
|
||||
|
||||
visualization:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
命令行参数优先级高于配置文件。例如:
|
||||
|
||||
```bash
|
||||
python main.py --config config.yaml contour \
|
||||
--cad datasets/CAD/40.step \
|
||||
--pointcloud datasets/Noise_PointCloud/40_noisy.ply \
|
||||
--excel-output 40_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
## 7. Python API 用法
|
||||
|
||||
除了命令行,也可以在 Python 中调用:
|
||||
|
||||
```python
|
||||
from pipeline import MeasurementPipeline
|
||||
|
||||
pipeline = MeasurementPipeline(config_path="config.yaml")
|
||||
|
||||
result = pipeline.run_contour(
|
||||
cad_path="datasets/CAD/11.step",
|
||||
pointcloud_path="datasets/Noise_PointCloud/11_noisy.ply",
|
||||
save_path="vis/11_contour.ply",
|
||||
excel_output_path="11_contour.xlsx",
|
||||
result_template_path="excel/测量结果.xlsx",
|
||||
no_visualize=True,
|
||||
)
|
||||
|
||||
print(result["distance_stats"])
|
||||
```
|
||||
|
||||
常用返回字段:
|
||||
|
||||
- `run_contour`:`pointcloud`、`distances`、`distance_stats`、`cad_vertices`、`cad_faces`、`excel_output`
|
||||
- `run_translate_align_pointcloud_to_cad`:`translation`、`T_final`、`aligned_path`、`matrix_path`
|
||||
- `run_registration`:`source`、`registration`、`distance_stats`
|
||||
- `run_curvature`:`pointcloud`、`curvatures`
|
||||
- `run_plane`:`plane_result`
|
||||
|
||||
## 8. 打包使用
|
||||
|
||||
生成可安装包源码:
|
||||
|
||||
```bash
|
||||
python packaging/build_package.py
|
||||
```
|
||||
|
||||
本地开发安装:
|
||||
|
||||
```bash
|
||||
pip install -e packaging/build
|
||||
geometry-measure --help
|
||||
```
|
||||
|
||||
安装后命令与 `main.py` 对应:
|
||||
|
||||
```bash
|
||||
geometry-measure contour \
|
||||
--cad datasets/CAD/11.step \
|
||||
--pointcloud datasets/Noise_PointCloud/11_noisy.ply \
|
||||
--save vis/11_contour.ply \
|
||||
--excel-output 11_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
每次修改根目录源码后,如果需要同步到打包目录,重新运行:
|
||||
|
||||
```bash
|
||||
python packaging/build_package.py
|
||||
```
|
||||
|
||||
## 9. 辅助脚本说明
|
||||
|
||||
辅助脚本放在 `archive/`。常用流程推荐优先使用 `main.py` 统一入口。
|
||||
|
||||
`archive/Contour_Flatness.py` 可用于批量轮廓/平整度/平面度评估:
|
||||
|
||||
```bash
|
||||
python archive/Contour_Flatness.py \
|
||||
--tasks "11,31,40" \
|
||||
--data_root High_Speed_Data \
|
||||
--pointcloud_kind noise \
|
||||
--excel_output vis/flatness.xlsx
|
||||
```
|
||||
|
||||
单任务模式:
|
||||
|
||||
```bash
|
||||
python archive/Contour_Flatness.py \
|
||||
--single \
|
||||
-c High_Speed_Data/CAD/11.step \
|
||||
-p High_Speed_Data/Noise_PointCloud/11_noisy.ply \
|
||||
--visualize
|
||||
```
|
||||
|
||||
`archive/translate_align_pointcloud_to_cad.py` 已接入统一入口,推荐用 `main.py translate-align` 调用;需要单独运行脚本时也可以直接执行:
|
||||
|
||||
```bash
|
||||
python archive/translate_align_pointcloud_to_cad.py \
|
||||
-c datasets/CAD/M02.stp \
|
||||
-p datasets/shift_test_output/ \
|
||||
-o results \
|
||||
--axis x \
|
||||
--crop_ratio 0.01 \
|
||||
--no_visualize
|
||||
```
|
||||
|
||||
## 10. 常见开发任务
|
||||
|
||||
### 添加一个新数据编号
|
||||
|
||||
例如添加编号 `45`:
|
||||
|
||||
```text
|
||||
datasets/CAD/45.step
|
||||
datasets/Mesh/45.stl
|
||||
datasets/PointCloud/45.ply
|
||||
datasets/Noise_PointCloud/45_noisy.ply
|
||||
```
|
||||
|
||||
运行评估:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--tasks 45 \
|
||||
--data-root datasets \
|
||||
--pointcloud-kind noise \
|
||||
--save vis/contour \
|
||||
--excel-output 45_contour.xlsx \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
### 保存命令日志
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--cad datasets/CAD/11.step \
|
||||
--pointcloud datasets/Noise_PointCloud/11_noisy.ply \
|
||||
--excel-output 11_contour.xlsx \
|
||||
--no-visualize > vis/11_contour.log 2>&1
|
||||
```
|
||||
|
||||
### 内存不足或显存不足
|
||||
|
||||
优先调小:
|
||||
|
||||
```bash
|
||||
--point-chunk-size 512
|
||||
--triangle-chunk-size 1024
|
||||
```
|
||||
|
||||
如果 CAD 三角面过多,也可以增大:
|
||||
|
||||
```bash
|
||||
--mesh-deflection 0.5
|
||||
```
|
||||
|
||||
注意:`mesh_deflection` 越大,CAD 三角化越粗,速度更快但几何近似更粗。
|
||||
|
||||
如果内存错误发生在批量合并完整背景点云阶段,并且使用的是 `datasets/train/train_2_1T_配准.ply`,请确认命令包含 `--no-visualize`。保存合并 PLY 已支持流式写入;Open3D 窗口展示仍不适合 10 亿点背景点云。
|
||||
|
||||
## 11. 排错
|
||||
|
||||
### `No module named OCC`
|
||||
|
||||
说明 CAD 依赖未安装或当前 Python 环境不对。
|
||||
|
||||
```bash
|
||||
conda activate High-Speed
|
||||
conda env update -n High-Speed -f environment.yml
|
||||
```
|
||||
|
||||
### `pythonocc-core` 无法通过 pip 安装
|
||||
|
||||
这是正常情况。`pythonocc-core` 应通过 conda 安装,不要放进纯 pip 安装流程里。
|
||||
|
||||
### 找不到文件
|
||||
|
||||
检查当前工作目录是否为项目根目录。相对路径都按当前运行目录解析。
|
||||
|
||||
推荐:
|
||||
|
||||
```bash
|
||||
pwd
|
||||
python main.py --help
|
||||
```
|
||||
|
||||
Windows PowerShell 下可以使用:
|
||||
|
||||
```powershell
|
||||
Get-Location
|
||||
python main.py --help
|
||||
```
|
||||
|
||||
### Open3D 可视化窗口影响自动化运行
|
||||
|
||||
加 `--no-visualize`。
|
||||
|
||||
```bash
|
||||
python main.py contour --cad datasets/CAD/11.step --pointcloud datasets/PointCloud/11.ply --no-visualize
|
||||
```
|
||||
|
||||
### 结果距离明显不合理
|
||||
|
||||
重点检查:
|
||||
|
||||
- CAD 和点云是否同单位。
|
||||
- CAD 和点云是否已经对齐。
|
||||
- 是否把干净点云和加噪点云用错目录。
|
||||
- `mesh_deflection` 是否过大导致 CAD 近似过粗。
|
||||
|
||||
### Excel 没有生成
|
||||
|
||||
`contour` 只有传了 `--excel-output` 才会生成 Excel:
|
||||
|
||||
```bash
|
||||
python main.py contour \
|
||||
--cad datasets/CAD/11.step \
|
||||
--pointcloud datasets/Noise_PointCloud/11_noisy.ply \
|
||||
--excel-output \
|
||||
--no-visualize
|
||||
```
|
||||
|
||||
默认输出:
|
||||
|
||||
```text
|
||||
excel/contour.xlsx
|
||||
```
|
||||
|
||||
默认模板:
|
||||
|
||||
```text
|
||||
excel/测量结果.xlsx
|
||||
```
|
||||
|
||||
输出表头保持模板一致,程序只更新 `测量结果` 和 `测量结论`。
|
||||
|
||||
### `registration` 或 `contour` 很慢
|
||||
|
||||
原因通常是点云超过百万点,且距离计算要遍历大量三角面。可以:
|
||||
|
||||
- 调小点云规模。
|
||||
- 调小 `--point-chunk-size` 和 `--triangle-chunk-size` 以降低内存压力。
|
||||
- 增大 `--mesh-deflection` 减少 CAD 三角面数量。
|
||||
- 对配准增加 `--voxel-size` 做更强下采样。
|
||||
|
||||
## 12. 开发约定
|
||||
|
||||
- 面向用户的新功能优先接入 `main.py` 和 `MeasurementPipeline`。
|
||||
- `contour` 是轮廓/平整度/平面度评估的唯一公开入口,不再新增其它容易混淆的同类命令。
|
||||
- 新增输出文件时优先放入已有输出目录:通用可视化和日志放 `vis/`,表格放 `excel/`,对齐结果放 `results/`。
|
||||
- 修改根目录源码后,如果需要打包版本同步,运行 `python packaging/build_package.py`。
|
||||
- 不要在文档中引入其它同类距离评估命令名,避免和 `contour` 产生歧义。
|
||||
55
alg/平整度、平面度、轮廓测量v1.5/archive/CAD2Mesh.py
Normal file
55
alg/平整度、平面度、轮廓测量v1.5/archive/CAD2Mesh.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.StlAPI import StlAPI_Writer
|
||||
import time
|
||||
import argparse # 命令行参数解析
|
||||
|
||||
def step2lightweight_stl(mesh_precision, input_step, output_stl):
|
||||
"""STEP转轻量化STL,纯后台处理,无显示,体积暴减"""
|
||||
start_total = time.time()
|
||||
# 1. 读取STEP文件
|
||||
print(f"【1/3】读取STEP文件:{input_step}")
|
||||
step_reader = STEPControl_Reader()
|
||||
if step_reader.ReadFile(input_step) != IFSelect_RetDone:
|
||||
raise Exception("STEP文件读取失败!检查路径/文件完整性")
|
||||
step_reader.TransferRoots()
|
||||
shape = step_reader.OneShape()
|
||||
print(f"【1/3】读取完成,耗时:{time.time()-start_total:.2f}秒")
|
||||
|
||||
# 2. 核心:网格离散化
|
||||
print(f"【2/3】网格离散化(精度{mesh_precision}mm),开始轻量化...")
|
||||
mesh = BRepMesh_IncrementalMesh(shape, mesh_precision, False)
|
||||
mesh.Perform()
|
||||
if not mesh.IsDone():
|
||||
raise Exception("网格离散化失败!")
|
||||
print(f"【2/3】轻量化完成,耗时:{time.time()-start_total:.2f}秒")
|
||||
|
||||
# 3. 保存为STL
|
||||
print(f"【3/3】保存轻量化STL:{output_stl}")
|
||||
stl_writer = StlAPI_Writer()
|
||||
stl_writer.Write(shape, output_stl)
|
||||
|
||||
total_time = time.time() - start_total
|
||||
print(f"✅ 转换完成!总耗时:{total_time:.2f}秒")
|
||||
print(f"💡 提示:STL文件可直接用FreeCAD/MeshLab/3D Viewer打开")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# ========== 命令行参数定义 ==========
|
||||
parser = argparse.ArgumentParser(description="STEP 转 轻量化 STL (命令行args版)")
|
||||
parser.add_argument("--precision", "-p", type=float, default=1.0, help="网格精度 MESH_PRECISION (mm),越大文件越小")
|
||||
parser.add_argument("--input", "-i", type=str, default="High_Speed_Data/CAD/11.step", help="输入STEP文件路径 INPUT_STEP_PATH")
|
||||
parser.add_argument("--output", "-o", type=str, default="High_Speed_Data/Mesh/11.stl", help="输出STL文件路径 OUTPUT_STL_PATH")
|
||||
|
||||
# 解析参数
|
||||
args = parser.parse_args()
|
||||
|
||||
# 调用转换函数
|
||||
try:
|
||||
step2lightweight_stl(
|
||||
mesh_precision=args.precision,
|
||||
input_step=args.input,
|
||||
output_stl=args.output
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"❌ 处理失败:{str(e)}")
|
||||
1593
alg/平整度、平面度、轮廓测量v1.5/archive/Contour_Flatness.py
Normal file
1593
alg/平整度、平面度、轮廓测量v1.5/archive/Contour_Flatness.py
Normal file
File diff suppressed because it is too large
Load Diff
127
alg/平整度、平面度、轮廓测量v1.5/archive/Curvature_Estimation.py
Normal file
127
alg/平整度、平面度、轮廓测量v1.5/archive/Curvature_Estimation.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
def compute_curvature_open3d(pcd, radius=0.05, max_nn=30):
|
||||
"""
|
||||
使用 Open3D 计算点云曲率
|
||||
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
radius: 搜索半径
|
||||
max_nn: 最大邻居数
|
||||
|
||||
Returns:
|
||||
curvatures: (N,) 曲率数组
|
||||
"""
|
||||
# 估计法线(同时获取协方差矩阵信息)
|
||||
pcd.estimate_normals(
|
||||
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=max_nn)
|
||||
)
|
||||
|
||||
# 获取点云数据
|
||||
points = np.asarray(pcd.points)
|
||||
n_points = len(points)
|
||||
curvatures = np.zeros(n_points)
|
||||
|
||||
# 构建 KD-Tree
|
||||
tree = o3d.geometry.KDTreeFlann(pcd)
|
||||
|
||||
for i in range(n_points):
|
||||
# 搜索邻域点
|
||||
[k, idx, _] = tree.search_radius_vector_3d(pcd.points[i], radius)
|
||||
|
||||
if k < 3:
|
||||
curvatures[i] = 1.0
|
||||
continue
|
||||
|
||||
# 获取邻域点坐标
|
||||
neighbors = points[idx]
|
||||
|
||||
# 计算协方差矩阵
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
centered = neighbors - centroid
|
||||
cov = np.cov(centered.T)
|
||||
|
||||
# 特征值分解
|
||||
eigenvalues = np.linalg.eigvalsh(cov)
|
||||
eigenvalues = np.sort(eigenvalues)
|
||||
|
||||
# 计算曲率
|
||||
curvatures[i] = eigenvalues[0] / (eigenvalues.sum() + 1e-8)
|
||||
|
||||
return curvatures
|
||||
|
||||
|
||||
def visualize_curvature(pcd, curvatures, color_scale=3.0):
|
||||
"""
|
||||
根据曲率可视化点云
|
||||
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
curvatures: 曲率数组
|
||||
color_scale: 颜色缩放因子
|
||||
"""
|
||||
colors = np.zeros((len(pcd.points), 3))
|
||||
# 红色通道表示曲率(高曲率区域为红色)
|
||||
colors[:, 0] = np.clip(curvatures * color_scale, 0, 1)
|
||||
# 绿色通道为互补色(低曲率区域为绿色)
|
||||
colors[:, 1] = np.clip(1 - curvatures * color_scale, 0, 1)
|
||||
# 蓝色通道设为0
|
||||
colors[:, 2] = 0
|
||||
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='计算点云曲率并可视化')
|
||||
parser.add_argument('--ply_path', type=str,default="./High_Speed_Data/PointCloud/11.ply", help='输入点云文件路径 (.ply)')
|
||||
parser.add_argument('--radius', type=float, default=2, help='搜索半径 (默认: 50)')
|
||||
parser.add_argument('--max_nn', type=int, default=30, help='最大邻居数 (默认: 30)')
|
||||
parser.add_argument('--color_scale', type=float, default=3.0, help='颜色缩放因子 (默认: 3.0)')
|
||||
parser.add_argument('--save', type=str, default=None, help='保存可视化图像路径 (可选)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(args.ply_path):
|
||||
print(f"错误: 文件不存在 - {args.ply_path}")
|
||||
return
|
||||
|
||||
print(f"读取点云: {args.ply_path}")
|
||||
pcd = o3d.io.read_point_cloud(args.ply_path)
|
||||
# points=np.array(pcd.points)
|
||||
if len(pcd.points) == 0:
|
||||
print("错误: 读取点云失败,文件可能为空或格式不正确")
|
||||
return
|
||||
|
||||
print(f"点云点数: {len(pcd.points)}")
|
||||
|
||||
# 计算曲率
|
||||
print("计算曲率中...")
|
||||
curvatures = compute_curvature_open3d(pcd, radius=args.radius, max_nn=args.max_nn)
|
||||
|
||||
# 统计曲率信息
|
||||
print(f"曲率统计:")
|
||||
print(f" 最小值: {curvatures.min():.6f}")
|
||||
print(f" 最大值: {curvatures.max():.6f}")
|
||||
print(f" 平均值: {curvatures.mean():.6f}")
|
||||
print(f" 标准差: {curvatures.std():.6f}")
|
||||
|
||||
# 可视化
|
||||
print("可视化曲率...")
|
||||
visualize_curvature(pcd, curvatures, color_scale=args.color_scale)
|
||||
|
||||
# 保存截图(如果指定)
|
||||
if args.save:
|
||||
o3d.io.write_point_cloud(args.save, pcd)
|
||||
print(f"点云已保存: {args.save}")
|
||||
|
||||
# 显示窗口
|
||||
o3d.visualization.draw_geometries([pcd], window_name=f"Curvature Visualization - {os.path.basename(args.ply_path)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
431
alg/平整度、平面度、轮廓测量v1.5/archive/Denoise.py
Normal file
431
alg/平整度、平面度、轮廓测量v1.5/archive/Denoise.py
Normal file
@@ -0,0 +1,431 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
import copy
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
# ========== 1. Chamfer Distance 计算 ==========
|
||||
def chamfer_distance(pcd1, pcd2, sqrt=True):
|
||||
"""
|
||||
计算两个点云之间的 CD 距离(Chamfer Distance)
|
||||
|
||||
Args:
|
||||
pcd1: Open3D 点云对象1
|
||||
pcd2: Open3D 点云对象2
|
||||
sqrt: 是否对最终结果开平方根
|
||||
|
||||
Returns:
|
||||
cd: Chamfer Distance 值
|
||||
cd1: 点云1 到 点云2 的平均距离
|
||||
cd2: 点云2 到 点云1 的平均距离
|
||||
"""
|
||||
points1 = np.asarray(pcd1.points)
|
||||
points2 = np.asarray(pcd2.points)
|
||||
|
||||
if len(points1) == 0 or len(points2) == 0:
|
||||
return float('inf'), 0, 0
|
||||
|
||||
tree1 = KDTree(points1)
|
||||
tree2 = KDTree(points2)
|
||||
|
||||
dist1, _ = tree2.query(points1, k=1)
|
||||
cd1 = np.mean(dist1 ** 2)
|
||||
|
||||
dist2, _ = tree1.query(points2, k=1)
|
||||
cd2 = np.mean(dist2 ** 2)
|
||||
|
||||
cd = cd1 + cd2
|
||||
|
||||
if sqrt:
|
||||
return np.sqrt(cd), cd1, cd2
|
||||
return cd, cd1, cd2
|
||||
|
||||
|
||||
# ========== 添加高斯噪声 ==========
|
||||
def add_gaussian_noise(pcd, noise_std=4.0):
|
||||
"""
|
||||
给点云添加高斯噪声
|
||||
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
noise_std: 噪声标准差
|
||||
|
||||
Returns:
|
||||
noisy_pcd: 添加噪声后的点云
|
||||
"""
|
||||
noisy_pcd = copy.deepcopy(pcd)
|
||||
points = np.asarray(noisy_pcd.points)
|
||||
|
||||
noise = np.random.normal(0, noise_std, points.shape)
|
||||
noisy_points = points + noise
|
||||
|
||||
noisy_pcd.points = o3d.utility.Vector3dVector(noisy_points)
|
||||
|
||||
return noisy_pcd
|
||||
|
||||
|
||||
# ========== 2. 统计离群点移除 (SOR) ==========
|
||||
def statistical_outlier_removal(pcd, nb_neighbors=20, std_ratio=2.0):
|
||||
"""
|
||||
统计离群点移除
|
||||
"""
|
||||
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=nb_neighbors, std_ratio=std_ratio)
|
||||
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
outlier_cloud = pcd.select_by_index(ind, invert=True)
|
||||
|
||||
metrics = {
|
||||
'original_points': len(pcd.points),
|
||||
'filtered_points': len(inlier_cloud.points),
|
||||
'removed_points': len(outlier_cloud.points),
|
||||
'keep_ratio': len(inlier_cloud.points) / len(pcd.points) * 100 if len(pcd.points) > 0 else 0,
|
||||
'method': 'SOR',
|
||||
'params': {'nb_neighbors': nb_neighbors, 'std_ratio': std_ratio}
|
||||
}
|
||||
|
||||
return inlier_cloud, outlier_cloud, metrics
|
||||
|
||||
|
||||
# ========== 3. 半径离群点移除 (ROR) ==========
|
||||
def radius_outlier_removal(pcd, nb_points=5, radius=20):
|
||||
"""
|
||||
半径离群点移除
|
||||
"""
|
||||
cl, ind = pcd.remove_radius_outlier(nb_points=nb_points, radius=radius)
|
||||
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
outlier_cloud = pcd.select_by_index(ind, invert=True)
|
||||
|
||||
metrics = {
|
||||
'original_points': len(pcd.points),
|
||||
'filtered_points': len(inlier_cloud.points),
|
||||
'removed_points': len(outlier_cloud.points),
|
||||
'keep_ratio': len(inlier_cloud.points) / len(pcd.points) * 100 if len(pcd.points) > 0 else 0,
|
||||
'method': 'ROR',
|
||||
'params': {'nb_points': nb_points, 'radius': radius}
|
||||
}
|
||||
|
||||
return inlier_cloud, outlier_cloud, metrics
|
||||
|
||||
|
||||
# ========== 4. 直通滤波 (PF) ==========
|
||||
def pass_through_filter(pcd, axis='z', min_val=-6000.0, max_val=6000.0):
|
||||
"""
|
||||
直通滤波
|
||||
"""
|
||||
points = np.asarray(pcd.points)
|
||||
axis_map = {'x': 0, 'y': 1, 'z': 2}
|
||||
idx = axis_map[axis]
|
||||
|
||||
mask = (points[:, idx] >= min_val) & (points[:, idx] <= max_val)
|
||||
filtered_points = points[mask]
|
||||
|
||||
filtered_pcd = o3d.geometry.PointCloud()
|
||||
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
|
||||
|
||||
metrics = {
|
||||
'original_points': len(pcd.points),
|
||||
'filtered_points': len(filtered_points),
|
||||
'removed_points': len(pcd.points) - len(filtered_points),
|
||||
'keep_ratio': len(filtered_points) / len(pcd.points) * 100 if len(pcd.points) > 0 else 0,
|
||||
'method': 'PF',
|
||||
'params': {'axis': axis, 'min_val': min_val, 'max_val': max_val}
|
||||
}
|
||||
|
||||
return filtered_pcd, metrics
|
||||
|
||||
|
||||
def compute_keep_ratio_and_density(pcd, filtered_pcd, sample_size=1000):
|
||||
"""计算直通滤波的保留率和密度变化"""
|
||||
n_orig = len(pcd.points)
|
||||
n_filt = len(filtered_pcd.points)
|
||||
|
||||
if n_orig == 0 or n_filt == 0:
|
||||
return 0, 0
|
||||
|
||||
keep_ratio = n_filt / n_orig * 100
|
||||
|
||||
points_orig = np.asarray(pcd.points)
|
||||
points_filt = np.asarray(filtered_pcd.points)
|
||||
|
||||
if len(points_orig) > sample_size:
|
||||
idx_orig = np.random.choice(len(points_orig), sample_size, replace=False)
|
||||
points_orig_sample = points_orig[idx_orig]
|
||||
else:
|
||||
points_orig_sample = points_orig
|
||||
|
||||
if len(points_filt) > sample_size:
|
||||
idx_filt = np.random.choice(len(points_filt), sample_size, replace=False)
|
||||
points_filt_sample = points_filt[idx_filt]
|
||||
else:
|
||||
points_filt_sample = points_filt
|
||||
|
||||
if len(points_orig_sample) > 5:
|
||||
tree_orig = KDTree(points_orig_sample)
|
||||
dist_orig, _ = tree_orig.query(points_orig_sample, k=2)
|
||||
avg_dist_orig = np.mean(dist_orig[:, 1])
|
||||
else:
|
||||
avg_dist_orig = 0
|
||||
|
||||
if len(points_filt_sample) > 5:
|
||||
tree_filt = KDTree(points_filt_sample)
|
||||
dist_filt, _ = tree_filt.query(points_filt_sample, k=2)
|
||||
avg_dist_filt = np.mean(dist_filt[:, 1])
|
||||
else:
|
||||
avg_dist_filt = 0
|
||||
|
||||
if avg_dist_orig > 0:
|
||||
density_change = (avg_dist_filt - avg_dist_orig) / avg_dist_orig * 100
|
||||
else:
|
||||
density_change = 0
|
||||
|
||||
return keep_ratio, density_change
|
||||
|
||||
|
||||
# ========== 5. 体素滤波 (VGF) ==========
|
||||
def voxel_grid_filter(pcd, leaf_size=0.05):
|
||||
"""
|
||||
体素滤波
|
||||
"""
|
||||
filtered_pcd = pcd.voxel_down_sample(leaf_size)
|
||||
|
||||
n_orig = len(pcd.points)
|
||||
n_filt = len(filtered_pcd.points)
|
||||
|
||||
metrics = {
|
||||
'original_points': n_orig,
|
||||
'filtered_points': n_filt,
|
||||
'removed_points': n_orig - n_filt,
|
||||
'keep_ratio': n_filt / n_orig * 100 if n_orig > 0 else 0,
|
||||
'downsample_rate': (n_orig - n_filt) / n_orig * 100 if n_orig > 0 else 0,
|
||||
'compression_ratio': n_orig / n_filt if n_filt > 0 else 0,
|
||||
'method': 'VGF',
|
||||
'params': {'leaf_size': leaf_size}
|
||||
}
|
||||
|
||||
if n_filt > 5:
|
||||
points_sample = np.asarray(filtered_pcd.points)
|
||||
sample_size = min(1000, n_filt)
|
||||
if n_filt > sample_size:
|
||||
idx = np.random.choice(n_filt, sample_size, replace=False)
|
||||
points_sample = points_sample[idx]
|
||||
tree = KDTree(points_sample)
|
||||
dist, _ = tree.query(points_sample, k=2)
|
||||
metrics['avg_point_distance'] = np.mean(dist[:, 1])
|
||||
|
||||
return filtered_pcd, metrics
|
||||
|
||||
|
||||
# ========== 6. 双边滤波 (BF) ==========
|
||||
def bilateral_filter(pcd, sigma_s=0.01, sigma_r=0.1):
|
||||
"""
|
||||
双边滤波
|
||||
"""
|
||||
if len(pcd.points) == 0:
|
||||
return pcd, {'original_points': 0, 'filtered_points': 0, 'removed_points': 0, 'keep_ratio': 0, 'method': 'BF', 'params': {}}
|
||||
|
||||
# 确保有法线
|
||||
if not pcd.has_normals():
|
||||
pcd.estimate_normals()
|
||||
|
||||
points = np.asarray(pcd.points)
|
||||
normals = np.asarray(pcd.normals)
|
||||
|
||||
tree = KDTree(points)
|
||||
filtered_points = np.zeros_like(points)
|
||||
|
||||
for i, p in enumerate(points):
|
||||
indices = tree.query_ball_point(p, r=2 * sigma_s)
|
||||
if len(indices) <= 1:
|
||||
filtered_points[i] = p
|
||||
continue
|
||||
|
||||
neigh_points = points[indices]
|
||||
neigh_normals = normals[indices]
|
||||
|
||||
spatial_dist = np.linalg.norm(neigh_points - p, axis=1)
|
||||
w_s = np.exp(-spatial_dist ** 2 / (2 * sigma_s ** 2))
|
||||
|
||||
normal_diff = np.linalg.norm(neigh_normals - normals[i], axis=1)
|
||||
w_r = np.exp(-normal_diff ** 2 / (2 * sigma_r ** 2))
|
||||
|
||||
w = w_s * w_r
|
||||
if w.sum() > 0:
|
||||
w = w / w.sum()
|
||||
filtered_points[i] = np.sum(neigh_points.T * w, axis=1)
|
||||
|
||||
filtered_pcd = o3d.geometry.PointCloud()
|
||||
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
|
||||
filtered_pcd.normals = o3d.utility.Vector3dVector(normals)
|
||||
|
||||
metrics = {
|
||||
'original_points': len(pcd.points),
|
||||
'filtered_points': len(filtered_points),
|
||||
'removed_points': 0,
|
||||
'keep_ratio': 100,
|
||||
'method': 'BF',
|
||||
'params': {'sigma_s': sigma_s, 'sigma_r': sigma_r}
|
||||
}
|
||||
|
||||
return filtered_pcd, metrics
|
||||
|
||||
|
||||
# ========== 主函数 ==========
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='点云去噪算法对比 (SOR, ROR, PF, VGF, BF)')
|
||||
parser.add_argument('--ply_path', '-p', type=str, default="./High_Speed_Data/PointCloud/11.ply",
|
||||
help='输入点云文件路径 (.ply)')
|
||||
parser.add_argument('--noise_std', '-n', type=float, default=4.0,
|
||||
help='高斯噪声标准差 (默认: 4.0)')
|
||||
parser.add_argument('--voxel_size', '-v', type=float, default=0.05,
|
||||
help='体素滤波大小 (默认: 0.05)')
|
||||
parser.add_argument('--sor_neighbors', type=int, default=20,
|
||||
help='SOR 邻域点数 (默认: 20)')
|
||||
parser.add_argument('--sor_std_ratio', type=float, default=2.0,
|
||||
help='SOR 标准差倍数 (默认: 2.0)')
|
||||
parser.add_argument('--ror_points', type=int, default=5,
|
||||
help='ROR 半径内最小点数 (默认: 5)')
|
||||
parser.add_argument('--ror_radius', type=float, default=20,
|
||||
help='ROR 搜索半径 (默认: 20)')
|
||||
parser.add_argument('--bf_sigma_s', type=float, default=0.01,
|
||||
help='双边滤波空间标准差 (默认: 0.01)')
|
||||
parser.add_argument('--bf_sigma_r', type=float, default=0.1,
|
||||
help='双边滤波特征标准差 (默认: 0.1)')
|
||||
parser.add_argument('--no_visualize', action='store_true',
|
||||
help='不显示可视化窗口')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(args.ply_path):
|
||||
print(f"错误: 点云文件不存在 - {args.ply_path}")
|
||||
return
|
||||
|
||||
# 1. 读取原始点云
|
||||
print(f"读取点云: {args.ply_path}")
|
||||
pcd_clean = o3d.io.read_point_cloud(args.ply_path)
|
||||
|
||||
if len(pcd_clean.points) == 0:
|
||||
print("读取失败,请检查文件路径")
|
||||
return
|
||||
|
||||
print(f"原始无噪点云点数: {len(pcd_clean.points)}")
|
||||
|
||||
# ========== 添加高斯噪声 ==========
|
||||
print("\n" + "=" * 50)
|
||||
print(f"添加高斯噪声 (std={args.noise_std})")
|
||||
print("=" * 50)
|
||||
pcd_noisy = add_gaussian_noise(pcd_clean, args.noise_std)
|
||||
print(f"加噪后点云点数: {len(pcd_noisy.points)}")
|
||||
|
||||
# 计算噪声水平
|
||||
points_clean = np.asarray(pcd_clean.points)
|
||||
points_noisy = np.asarray(pcd_noisy.points)
|
||||
noise_level = np.mean(np.linalg.norm(points_noisy - points_clean, axis=1))
|
||||
print(f"平均噪声偏移量: {noise_level:.4f}")
|
||||
|
||||
# 加噪点云着色
|
||||
pcd_noisy.paint_uniform_color([1.0, 0.7, 0.5])
|
||||
|
||||
# ========== 运行各去噪算法 ==========
|
||||
results = {}
|
||||
|
||||
# 1. SOR
|
||||
print("\n" + "=" * 50)
|
||||
print("统计离群点移除 (SOR)")
|
||||
print("=" * 50)
|
||||
sor_filtered, sor_outlier, sor_metrics = statistical_outlier_removal(
|
||||
pcd_noisy, nb_neighbors=args.sor_neighbors, std_ratio=args.sor_std_ratio)
|
||||
results['SOR'] = sor_metrics
|
||||
print(f"保留率: {sor_metrics['keep_ratio']:.2f}%")
|
||||
print(f"移除: {sor_metrics['removed_points']} 点")
|
||||
|
||||
# 2. ROR
|
||||
print("\n" + "=" * 50)
|
||||
print("半径离群点移除 (ROR)")
|
||||
print("=" * 50)
|
||||
ror_filtered, ror_outlier, ror_metrics = radius_outlier_removal(
|
||||
pcd_noisy, nb_points=args.ror_points, radius=args.ror_radius)
|
||||
results['ROR'] = ror_metrics
|
||||
print(f"保留率: {ror_metrics['keep_ratio']:.2f}%")
|
||||
print(f"移除: {ror_metrics['removed_points']} 点")
|
||||
|
||||
# 3. 直通滤波 (PF)
|
||||
print("\n" + "=" * 50)
|
||||
print("直通滤波 (PF)")
|
||||
print("=" * 50)
|
||||
points = np.asarray(pcd_noisy.points)
|
||||
z_min, z_max = points[:, 2].min(), points[:, 2].max()
|
||||
pf_filtered, pf_metrics = pass_through_filter(pcd_noisy, axis='z', min_val=z_min, max_val=z_max)
|
||||
results['PF'] = pf_metrics
|
||||
print(f"保留率: {pf_metrics['keep_ratio']:.2f}%")
|
||||
keep_ratio, density_change = compute_keep_ratio_and_density(pcd_noisy, pf_filtered)
|
||||
print(f"密度变化: {density_change:+.2f}%")
|
||||
|
||||
# 4. 体素滤波 (VGF)
|
||||
print("\n" + "=" * 50)
|
||||
print("体素滤波 (VGF)")
|
||||
print("=" * 50)
|
||||
vgf_filtered, vgf_metrics = voxel_grid_filter(pcd_noisy, leaf_size=args.voxel_size)
|
||||
results['VGF'] = vgf_metrics
|
||||
print(f"保留率: {vgf_metrics['keep_ratio']:.2f}%")
|
||||
print(f"压缩比: {vgf_metrics['compression_ratio']:.1f}x")
|
||||
|
||||
# 5. 双边滤波 (BF)
|
||||
print("\n" + "=" * 50)
|
||||
print("双边滤波 (BF)")
|
||||
print("=" * 50)
|
||||
bf_filtered, bf_metrics = bilateral_filter(pcd_noisy, sigma_s=args.bf_sigma_s, sigma_r=args.bf_sigma_r)
|
||||
results['BF'] = bf_metrics
|
||||
print(f"保留率: {bf_metrics['keep_ratio']:.2f}%")
|
||||
|
||||
# Chamfer Distance 计算
|
||||
print("\n" + "=" * 50)
|
||||
print("Chamfer Distance 评估 (与干净点云对比)")
|
||||
print("=" * 50)
|
||||
|
||||
for name, metrics in results.items():
|
||||
if name == 'BF':
|
||||
filtered = bf_filtered
|
||||
elif name == 'SOR':
|
||||
filtered = sor_filtered
|
||||
elif name == 'ROR':
|
||||
filtered = ror_filtered
|
||||
elif name == 'PF':
|
||||
filtered = pf_filtered
|
||||
elif name == 'VGF':
|
||||
filtered = vgf_filtered
|
||||
else:
|
||||
continue
|
||||
|
||||
if len(filtered.points) > 0:
|
||||
cd, _, _ = chamfer_distance(pcd_clean, filtered)
|
||||
print(f"{name}: CD = {cd:.6f} m")
|
||||
|
||||
print("-" * 40)
|
||||
cd_noisy, _, _ = chamfer_distance(pcd_clean, pcd_noisy)
|
||||
print(f"加噪点云 (未经滤波): CD = {cd_noisy:.6f} m")
|
||||
|
||||
# 可视化
|
||||
if not args.no_visualize:
|
||||
print("\n可视化对比...")
|
||||
pcd_clean.paint_uniform_color([0, 0, 1]) # 蓝色 - 原始干净点云
|
||||
pcd_noisy.paint_uniform_color([1.0, 0.7, 0.5]) # 浅橙色 - 加噪点云
|
||||
|
||||
sor_filtered.paint_uniform_color([0, 1, 0]) # 绿色 - SOR 结果
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[pcd_clean, pcd_noisy],
|
||||
window_name=f"Clean (Blue, {len(pcd_clean.points)}) vs Noisy (Orange, {len(pcd_noisy.points)})"
|
||||
)
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[pcd_clean, sor_filtered],
|
||||
window_name=f"SOR: Clean (Blue, {len(pcd_clean.points)}) vs Filtered (Green, {len(sor_filtered.points)})"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
380
alg/平整度、平面度、轮廓测量v1.5/archive/Plane_Estimation.py
Normal file
380
alg/平整度、平面度、轮廓测量v1.5/archive/Plane_Estimation.py
Normal file
@@ -0,0 +1,380 @@
|
||||
# import numpy as np
|
||||
# import open3d as o3d
|
||||
# import random
|
||||
#
|
||||
#
|
||||
# def ransac_plane(points, threshold=0.01, max_iterations=1000, min_inliers=100):
|
||||
# """
|
||||
# RANSAC 平面拟合(单个)
|
||||
# """
|
||||
# best_inliers = []
|
||||
# best_plane = None
|
||||
# n_points = len(points)
|
||||
#
|
||||
# if n_points < 3:
|
||||
# return None, []
|
||||
#
|
||||
# for _ in range(max_iterations):
|
||||
# sample_idx = random.sample(range(n_points), 3)
|
||||
# p1, p2, p3 = points[sample_idx]
|
||||
#
|
||||
# v1 = p2 - p1
|
||||
# v2 = p3 - p1
|
||||
# normal = np.cross(v1, v2)
|
||||
# norm = np.linalg.norm(normal)
|
||||
# if norm < 1e-8:
|
||||
# continue
|
||||
# normal = normal / norm
|
||||
# a, b, c = normal
|
||||
# d = -np.dot(normal, p1)
|
||||
#
|
||||
# distances = np.abs(np.dot(points, normal) + d)
|
||||
# inliers = np.where(distances < threshold)[0]
|
||||
#
|
||||
# if len(inliers) > len(best_inliers) and len(inliers) >= min_inliers:
|
||||
# best_inliers = inliers
|
||||
# best_plane = (a, b, c, d)
|
||||
#
|
||||
# if len(best_inliers) > 0.9 * n_points:
|
||||
# break
|
||||
#
|
||||
# # 用内点重新拟合
|
||||
# if best_inliers is not None and len(best_inliers) > 3:
|
||||
# inlier_points = points[best_inliers]
|
||||
# centroid = np.mean(inlier_points, axis=0)
|
||||
# _, _, vh = np.linalg.svd(inlier_points - centroid)
|
||||
# normal = vh[2]
|
||||
# normal = normal / np.linalg.norm(normal)
|
||||
# a, b, c = normal
|
||||
# d = -np.dot(normal, centroid)
|
||||
# best_plane = (a, b, c, d)
|
||||
#
|
||||
# return best_plane, best_inliers
|
||||
#
|
||||
#
|
||||
# def sequential_ransac_planes(points, threshold=0.01, max_iterations=1000, min_inliers=100, max_planes=5):
|
||||
# """
|
||||
# 迭代 RANSAC 拟合多个平面
|
||||
# """
|
||||
# remaining_points = points.copy()
|
||||
# remaining_indices = np.arange(len(points))
|
||||
#
|
||||
# planes = []
|
||||
# inliers_list = []
|
||||
#
|
||||
# for i in range(max_planes):
|
||||
# if len(remaining_points) < min_inliers:
|
||||
# break
|
||||
#
|
||||
# plane, inliers = ransac_plane(remaining_points, threshold, max_iterations, min_inliers)
|
||||
#
|
||||
# if plane is None or len(inliers) < min_inliers:
|
||||
# break
|
||||
#
|
||||
# planes.append(plane)
|
||||
# original_indices = remaining_indices[inliers]
|
||||
# inliers_list.append(original_indices)
|
||||
#
|
||||
# # 移除内点
|
||||
# mask = np.ones(len(remaining_points), dtype=bool)
|
||||
# mask[inliers] = False
|
||||
# remaining_points = remaining_points[mask]
|
||||
# remaining_indices = remaining_indices[mask]
|
||||
#
|
||||
# print(f"平面 {len(planes)}: {len(inliers)} 个内点, 剩余 {len(remaining_points)} 个点")
|
||||
#
|
||||
# return planes, inliers_list
|
||||
#
|
||||
#
|
||||
# def create_plane_mesh(plane, bounds):
|
||||
# """
|
||||
# 创建平面网格用于可视化
|
||||
# """
|
||||
# a, b, c, d = plane
|
||||
# x_min, x_max = bounds[0]
|
||||
# y_min, y_max = bounds[1]
|
||||
#
|
||||
# x = np.linspace(x_min, x_max, 50)
|
||||
# y = np.linspace(y_min, y_max, 50)
|
||||
# X, Y = np.meshgrid(x, y)
|
||||
#
|
||||
# if abs(c) > 1e-8:
|
||||
# Z = -(a * X + b * Y + d) / c
|
||||
# else:
|
||||
# Z = np.zeros_like(X)
|
||||
#
|
||||
# vertices = []
|
||||
# triangles = []
|
||||
# rows, cols = X.shape
|
||||
#
|
||||
# for i in range(rows):
|
||||
# for j in range(cols):
|
||||
# vertices.append([X[i, j], Y[i, j], Z[i, j]])
|
||||
#
|
||||
# for i in range(rows - 1):
|
||||
# for j in range(cols - 1):
|
||||
# idx = i * cols + j
|
||||
# triangles.append([idx, idx + 1, idx + cols])
|
||||
# triangles.append([idx + 1, idx + cols + 1, idx + cols])
|
||||
#
|
||||
# mesh = o3d.geometry.TriangleMesh()
|
||||
# mesh.vertices = o3d.utility.Vector3dVector(np.array(vertices))
|
||||
# mesh.triangles = o3d.utility.Vector3iVector(np.array(triangles))
|
||||
# mesh.compute_vertex_normals()
|
||||
#
|
||||
# return mesh
|
||||
#
|
||||
#
|
||||
# def visualize_multiple_planes(points, planes, inliers_list):
|
||||
# """
|
||||
# 可视化多个平面(点云 + 平面网格)
|
||||
#
|
||||
# Args:
|
||||
# points: (N, 3) 点云坐标
|
||||
# planes: 平面方程列表 [(a,b,c,d), ...]
|
||||
# inliers_list: 每个平面对应的内点索引列表
|
||||
# """
|
||||
# # 颜色列表(每个平面不同颜色)
|
||||
# colors = [
|
||||
# [1, 0, 0], # 红色
|
||||
# [0, 1, 0], # 绿色
|
||||
# [0, 0, 1], # 蓝色
|
||||
# [1, 1, 0], # 黄色
|
||||
# [1, 0, 1], # 品红
|
||||
# [0, 1, 1], # 青色
|
||||
# [1, 0.5, 0], # 橙色
|
||||
# [0.5, 0, 1], # 紫色
|
||||
# ]
|
||||
#
|
||||
# geometries = []
|
||||
#
|
||||
# # 1. 为每个平面的内点着色
|
||||
# point_colors = np.zeros((len(points), 3))
|
||||
# for i, inliers in enumerate(inliers_list):
|
||||
# color = colors[i % len(colors)]
|
||||
# point_colors[inliers] = color
|
||||
#
|
||||
# # 未分配的点(剩余点)设为灰色
|
||||
# unassigned = np.where(np.sum(point_colors, axis=1) == 0)[0]
|
||||
# point_colors[unassigned] = [0.5, 0.5, 0.5]
|
||||
#
|
||||
# colored_pcd = o3d.geometry.PointCloud()
|
||||
# colored_pcd.points = o3d.utility.Vector3dVector(points)
|
||||
# colored_pcd.colors = o3d.utility.Vector3dVector(point_colors)
|
||||
# geometries.append(colored_pcd)
|
||||
#
|
||||
# # 2. 为每个平面创建网格
|
||||
# for i, (plane, inliers) in enumerate(zip(planes, inliers_list)):
|
||||
# # 计算该平面对应点的范围
|
||||
# inlier_points = points[inliers]
|
||||
# if len(inlier_points) > 0:
|
||||
# x_min, x_max = inlier_points[:, 0].min(), inlier_points[:, 0].max()
|
||||
# y_min, y_max = inlier_points[:, 1].min(), inlier_points[:, 1].max()
|
||||
#
|
||||
# # 稍微扩展边界
|
||||
# x_range = x_max - x_min
|
||||
# y_range = y_max - y_min
|
||||
# x_min -= x_range * 0.1
|
||||
# x_max += x_range * 0.1
|
||||
# y_min -= y_range * 0.1
|
||||
# y_max += y_range * 0.1
|
||||
#
|
||||
# mesh = create_plane_mesh(plane, [[x_min, x_max], [y_min, y_max]])
|
||||
# mesh.paint_uniform_color(colors[i % len(colors)])
|
||||
# # 设置半透明
|
||||
# mesh.paint_uniform_color([colors[i % len(colors)][0],
|
||||
# colors[i % len(colors)][1],
|
||||
# colors[i % len(colors)][2]])
|
||||
# geometries.append(mesh)
|
||||
#
|
||||
# # 可视化
|
||||
# o3d.visualization.draw_geometries(
|
||||
# geometries,
|
||||
# window_name="Multiple Planes - RANSAC",
|
||||
# width=1024,
|
||||
# height=768
|
||||
# )
|
||||
#
|
||||
#
|
||||
# def visualize_single_plane(points, plane, inliers):
|
||||
# """
|
||||
# 可视化单个平面(兼容单平面情况)
|
||||
# """
|
||||
# visualize_multiple_planes(points, [plane], [inliers])
|
||||
#
|
||||
#
|
||||
# # ========== 主程序 ==========
|
||||
# if __name__ == "__main__":
|
||||
# # 读取点云
|
||||
# file_path = r"E:\point_distance\data\part_car_head.ply"
|
||||
# pcd = o3d.io.read_point_cloud(file_path)
|
||||
#
|
||||
# if len(pcd.points) == 0:
|
||||
# print("读取失败,请检查文件路径")
|
||||
# exit()
|
||||
#
|
||||
# points = np.asarray(pcd.points)
|
||||
# print(f"原始点云点数: {len(points)}")
|
||||
#
|
||||
# # 计算点云范围
|
||||
# points_range = points.max(axis=0) - points.min(axis=0)
|
||||
# print(f"点云范围: X[{points_range[0]:.3f}], Y[{points_range[1]:.3f}], Z[{points_range[2]:.3f}]")
|
||||
#
|
||||
# # 设置阈值
|
||||
# threshold = points_range[2] * 0.02
|
||||
# print(f"使用阈值: {threshold:.4f}")
|
||||
#
|
||||
# # ========== 单平面拟合 ==========
|
||||
# # print("\n" + "=" * 50)
|
||||
# # print("单平面拟合")
|
||||
# # print("=" * 50)
|
||||
# #
|
||||
# # plane, inliers = ransac_plane(points, threshold=threshold, max_iterations=1, min_inliers=100)
|
||||
# #
|
||||
# # if plane is not None:
|
||||
# # a, b, c, d = plane
|
||||
# # print(f"拟合平面: {a:.4f}x + {b:.4f}y + {c:.4f}z + {d:.4f} = 0")
|
||||
# # print(f"内点数量: {len(inliers)} / {len(points)} ({len(inliers) / len(points) * 100:.1f}%)")
|
||||
# # visualize_single_plane(points, plane, inliers)
|
||||
#
|
||||
# # ========== 多平面拟合 ==========
|
||||
# print("\n" + "=" * 50)
|
||||
# print("多平面拟合")
|
||||
# print("=" * 50)
|
||||
#
|
||||
# planes, inliers_list = sequential_ransac_planes(
|
||||
# points,
|
||||
# threshold=threshold,
|
||||
# max_iterations=1,
|
||||
# min_inliers=100,
|
||||
# max_planes=1
|
||||
# )
|
||||
#
|
||||
# print(f"共提取 {len(planes)} 个平面")
|
||||
# for i, (plane, inliers) in enumerate(zip(planes, inliers_list)):
|
||||
# a, b, c, d = plane
|
||||
# print(f"平面 {i + 1}: {a:.4f}x + {b:.4f}y + {c:.4f}z + {d:.4f} = 0, 内点数: {len(inliers)}")
|
||||
#
|
||||
# if len(planes) > 0:
|
||||
# visualize_multiple_planes(points, planes, inliers_list)
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import random
|
||||
|
||||
|
||||
def create_square_plane_with_noise(size_mm=1000, noise_std_mm=0.5, num_points=10000):
|
||||
"""创建正方形平面点云并添加高斯噪声"""
|
||||
size_m = size_mm / 1000.0
|
||||
noise_std_m = noise_std_mm / 1000.0
|
||||
|
||||
half_size = size_m / 2
|
||||
x = np.random.uniform(-half_size, half_size, num_points)
|
||||
y = np.random.uniform(-half_size, half_size, num_points)
|
||||
z = np.zeros(num_points)
|
||||
|
||||
points = np.column_stack([x, y, z])
|
||||
noise_z = np.random.normal(0, noise_std_m, num_points)
|
||||
points[:, 2] += noise_z
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def estimate_plane_ransac_open3d(points, threshold_mm=0.5, max_iterations=2000):
|
||||
"""使用 Open3D 内置 RANSAC 平面估计"""
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points)
|
||||
|
||||
threshold_m = threshold_mm / 1000.0
|
||||
|
||||
plane_model, inliers = pcd.segment_plane(
|
||||
distance_threshold=threshold_m,
|
||||
ransac_n=3,
|
||||
num_iterations=max_iterations
|
||||
)
|
||||
|
||||
a, b, c, d = plane_model
|
||||
|
||||
# 计算误差
|
||||
ideal_normal = np.array([0, 0, 1])
|
||||
estimated_normal = np.array([a, b, c])
|
||||
cos_angle = np.dot(ideal_normal, estimated_normal)
|
||||
cos_angle = np.clip(cos_angle, -1, 1)
|
||||
angle_error = np.degrees(np.arccos(cos_angle))
|
||||
offset_error = abs(d)
|
||||
|
||||
inlier_points = points[inliers]
|
||||
distances = np.abs(np.dot(inlier_points, estimated_normal) + d)
|
||||
rmse = np.sqrt(np.mean(distances ** 2))
|
||||
|
||||
print(f"\nRANSAC 平面拟合结果 (Open3D):")
|
||||
print(f" 估计平面: {a:.6f}x + {b:.6f}y + {c:.6f}z + {d:.6f} = 0")
|
||||
print(f" 内点数量: {len(inliers)} / {len(points)} ({len(inliers) / len(points) * 100:.1f}%)")
|
||||
print(f" 法线角度误差: {angle_error:.4f} 度")
|
||||
print(f" 偏移误差: {offset_error:.6f} m ({offset_error * 1000:.3f} mm)")
|
||||
print(f" 平面拟合 RMSE: {rmse:.6f} m ({rmse * 1000:.3f} mm)")
|
||||
|
||||
return plane_model, inliers
|
||||
|
||||
|
||||
def visualize_plane_fitting(points, plane):
|
||||
"""可视化原始点云和拟合平面"""
|
||||
# 原始点云
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points)
|
||||
pcd.paint_uniform_color([0.6, 0.8, 1.0])
|
||||
|
||||
# 创建平面点云
|
||||
a, b, c, d = plane
|
||||
x_min, x_max = points[:, 0].min(), points[:, 0].max()
|
||||
y_min, y_max = points[:, 1].min(), points[:, 1].max()
|
||||
|
||||
x = np.linspace(x_min, x_max, 50)
|
||||
y = np.linspace(y_min, y_max, 50)
|
||||
X, Y = np.meshgrid(x, y)
|
||||
|
||||
if abs(c) > 1e-8:
|
||||
Z = -(a * X + b * Y + d) / c
|
||||
else:
|
||||
Z = np.zeros_like(X)
|
||||
|
||||
plane_points = np.column_stack([X.ravel(), Y.ravel(), Z.ravel()])
|
||||
|
||||
pcd_plane = o3d.geometry.PointCloud()
|
||||
pcd_plane.points = o3d.utility.Vector3dVector(plane_points)
|
||||
pcd_plane.paint_uniform_color([1, 0, 0]) # 红色
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[pcd, pcd_plane],
|
||||
window_name="Plane Fitting",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
# ========== 主程序 ==========
|
||||
if __name__ == "__main__":
|
||||
# 参数设置
|
||||
size_mm = 1000
|
||||
noise_std_mm = 0.5
|
||||
num_points = 100000
|
||||
|
||||
print(f"参数:")
|
||||
print(f" 正方形边长: {size_mm} mm")
|
||||
print(f" 噪声标准差: {noise_std_mm} mm")
|
||||
print(f" 点数: {num_points}")
|
||||
|
||||
# 生成点云
|
||||
points = create_square_plane_with_noise(size_mm, noise_std_mm, num_points)
|
||||
|
||||
# 检查数据范围
|
||||
print(f"\n数据范围:")
|
||||
print(f" X: [{points[:, 0].min():.3f}, {points[:, 0].max():.3f}]")
|
||||
print(f" Y: [{points[:, 1].min():.3f}, {points[:, 1].max():.3f}]")
|
||||
print(f" Z: [{points[:, 2].min():.6f}, {points[:, 2].max():.6f}]")
|
||||
|
||||
# 使用 Open3D RANSAC
|
||||
plane, inliers = estimate_plane_ransac_open3d(points, threshold_mm=0.5, max_iterations=2000)
|
||||
|
||||
# 可视化
|
||||
visualize_plane_fitting(points, plane)
|
||||
246
alg/平整度、平面度、轮廓测量v1.5/archive/Point_Fitness.py
Normal file
246
alg/平整度、平面度、轮廓测量v1.5/archive/Point_Fitness.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import copy
|
||||
import argparse
|
||||
import os
|
||||
from scipy.spatial.transform import Rotation as R
|
||||
import trimesh
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import time
|
||||
|
||||
|
||||
def rotation_matrix_scipy(roll_deg=10, pitch_deg=10, yaw_deg=10):
|
||||
"""
|
||||
使用 scipy 生成绕 XYZ 轴旋转的矩阵
|
||||
"""
|
||||
r = R.from_euler('xyz', [roll_deg, pitch_deg, yaw_deg], degrees=True)
|
||||
return r.as_matrix()
|
||||
|
||||
|
||||
def random_rotation_matrix():
|
||||
"""生成随机旋转矩阵"""
|
||||
angle = np.random.uniform(0, 2 * np.pi)
|
||||
axis = np.random.randn(3)
|
||||
axis = axis / np.linalg.norm(axis)
|
||||
|
||||
K = np.array([
|
||||
[0, -axis[2], axis[1]],
|
||||
[axis[2], 0, -axis[0]],
|
||||
[-axis[1], axis[0], 0]
|
||||
])
|
||||
R = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * K @ K
|
||||
return R
|
||||
|
||||
|
||||
def random_translation_vector(range_xyz=0.5):
|
||||
"""生成随机平移向量"""
|
||||
return np.random.uniform(-range_xyz, range_xyz, 3)
|
||||
|
||||
|
||||
def transform_point_cloud(pcd, R, t):
|
||||
"""对点云应用变换"""
|
||||
transformed_pcd = copy.deepcopy(pcd)
|
||||
points = np.asarray(transformed_pcd.points)
|
||||
points_transformed = (R @ points.T).T + t
|
||||
transformed_pcd.points = o3d.utility.Vector3dVector(points_transformed)
|
||||
return transformed_pcd
|
||||
|
||||
|
||||
def registration_open3d(source_pcd, target_pcd, voxel_size=0.005):
|
||||
"""
|
||||
点云配准(RANSAC + ICP)
|
||||
|
||||
Returns:
|
||||
transformation: 4x4 变换矩阵
|
||||
fitness: 配准 fitness 分数
|
||||
rmse: 配准 RMSE
|
||||
"""
|
||||
# 1. 下采样
|
||||
source_down = source_pcd.voxel_down_sample(voxel_size)
|
||||
target_down = target_pcd.voxel_down_sample(voxel_size)
|
||||
|
||||
# 2. 估计法线
|
||||
radius = voxel_size * 2
|
||||
source_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=30))
|
||||
target_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=30))
|
||||
|
||||
# 3. 计算 FPFH 特征
|
||||
radius_feature = voxel_size * 5
|
||||
source_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
source_down, o3d.geometry.KDTreeSearchParamHybrid(radius=radius_feature, max_nn=100))
|
||||
target_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
target_down, o3d.geometry.KDTreeSearchParamHybrid(radius=radius_feature, max_nn=100))
|
||||
|
||||
# 4. RANSAC 粗配准
|
||||
distance_threshold = voxel_size * 1.5
|
||||
result_ransac = o3d.pipelines.registration.registration_ransac_based_on_feature_matching(
|
||||
source_down, target_down, source_fpfh, target_fpfh,
|
||||
mutual_filter=True,
|
||||
max_correspondence_distance=distance_threshold,
|
||||
estimation_method=o3d.pipelines.registration.TransformationEstimationPointToPoint(),
|
||||
ransac_n=3,
|
||||
checkers=[
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnEdgeLength(0.9),
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnDistance(distance_threshold)
|
||||
],
|
||||
criteria=o3d.pipelines.registration.RANSACConvergenceCriteria(100000, 0.999))
|
||||
|
||||
print(f"RANSAC 配准完成")
|
||||
source_pcd.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
target_pcd.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
|
||||
# 5. ICP 精配准
|
||||
result_icp = o3d.pipelines.registration.registration_icp(
|
||||
source_pcd, target_pcd, distance_threshold / 2, result_ransac.transformation,
|
||||
o3d.pipelines.registration.TransformationEstimationPointToPlane(),
|
||||
o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=200))
|
||||
|
||||
print(f"ICP 精配准完成")
|
||||
print(f" Fitness: {result_icp.fitness:.4f}")
|
||||
print(f" RMSE: {result_icp.inlier_rmse:.6f} m")
|
||||
|
||||
return result_icp.transformation, result_icp.fitness, result_icp.inlier_rmse
|
||||
|
||||
|
||||
def visualize_registration_result(original_pcd, transformed_pcd, registered_pcd):
|
||||
"""可视化原始点云、变换后点云和配准后点云"""
|
||||
source_colored = copy.deepcopy(original_pcd)
|
||||
target_colored = copy.deepcopy(transformed_pcd)
|
||||
registered_colored = copy.deepcopy(registered_pcd)
|
||||
|
||||
source_colored.paint_uniform_color([0, 0, 1]) # 蓝色 - 原始点云
|
||||
target_colored.paint_uniform_color([0, 1, 0]) # 绿色 - 目标点云
|
||||
registered_colored.paint_uniform_color([1, 0, 0]) # 红色 - 配准后点云
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[source_colored, target_colored],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024, height=768
|
||||
)
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[registered_colored, target_colored],
|
||||
window_name="After Registration (Red: Registered, Green: Target)",
|
||||
width=1024, height=768
|
||||
)
|
||||
|
||||
|
||||
def visualize_registration_comparison(original_pcd, transformed_pcd, registered_pcd):
|
||||
"""并排可视化配准前后的对比"""
|
||||
source_before = copy.deepcopy(original_pcd)
|
||||
source_before.paint_uniform_color([0, 0, 1]) # 蓝色
|
||||
|
||||
target_before = copy.deepcopy(transformed_pcd)
|
||||
target_before.paint_uniform_color([0, 1, 0]) # 绿色
|
||||
|
||||
registered_after = copy.deepcopy(registered_pcd)
|
||||
registered_after.paint_uniform_color([1, 0, 0]) # 红色
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[source_before, target_before],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024, height=768
|
||||
)
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[registered_after, target_before],
|
||||
window_name="After Registration (Red: Registered, Green: Target)",
|
||||
width=1024, height=768
|
||||
)
|
||||
|
||||
|
||||
def compute_point_to_mesh_distance_trimesh(points, mesh_path):
|
||||
"""使用 trimesh 计算点云到 Mesh 的距离"""
|
||||
mesh_trimesh = trimesh.load(mesh_path)
|
||||
closest_points, distances, triangle_ids = mesh_trimesh.nearest.on_surface(points)
|
||||
return distances
|
||||
|
||||
|
||||
# ========== 主程序 ==========
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='点云配准与 Mesh 距离计算')
|
||||
parser.add_argument('--ply_path', '-p', type=str, default="./High_Speed_Data/Noise_PointCloud/11_noisy.ply",
|
||||
help='输入点云文件路径 (.ply)')
|
||||
parser.add_argument('--mesh_path', '-m', type=str, default="./High_Speed_Data/Mesh/11.stl",
|
||||
help='输入 Mesh 文件路径 (.stl, .ply, .obj 等)')
|
||||
parser.add_argument('--voxel_size', '-v', type=float, default=10,
|
||||
help='体素滤波大小 (默认: 10)')
|
||||
parser.add_argument('--roll', type=float, default=10,
|
||||
help='绕 X 轴旋转角度 (度, 默认: 10)')
|
||||
parser.add_argument('--pitch', type=float, default=10,
|
||||
help='绕 Y 轴旋转角度 (度, 默认: 10)')
|
||||
parser.add_argument('--yaw', type=float, default=10,
|
||||
help='绕 Z 轴旋转角度 (度, 默认: 10)')
|
||||
parser.add_argument('--trans_range', type=float, default=0,
|
||||
help='平移范围 (米, 默认: 0)')
|
||||
parser.add_argument('--no_visualize', action='store_true',
|
||||
help='不显示可视化窗口')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(args.ply_path):
|
||||
print(f"错误: 点云文件不存在 - {args.ply_path}")
|
||||
return
|
||||
|
||||
if not os.path.exists(args.mesh_path):
|
||||
print(f"错误: Mesh 文件不存在 - {args.mesh_path}")
|
||||
return
|
||||
|
||||
# 1. 读取原始点云
|
||||
print(f"读取点云: {args.ply_path}")
|
||||
source_pcd = o3d.io.read_point_cloud(args.ply_path)
|
||||
|
||||
if len(source_pcd.points) == 0:
|
||||
print("错误: 点云读取失败")
|
||||
return
|
||||
|
||||
print(f"原始点云点数: {len(source_pcd.points)}")
|
||||
|
||||
# 2. 生成变换
|
||||
R = rotation_matrix_scipy(args.roll, args.pitch, args.yaw)
|
||||
t = random_translation_vector(range_xyz=args.trans_range)
|
||||
|
||||
print(f"旋转矩阵:\n{R}")
|
||||
print(f"平移向量: {t}")
|
||||
|
||||
# 3. 创建目标点云(变换后的源点云)
|
||||
target_pcd = transform_point_cloud(source_pcd, R, t)
|
||||
|
||||
# 4. 点云配准
|
||||
print("\n开始点云配准...")
|
||||
t0 = time.time()
|
||||
transformation, fitness, rmse = registration_open3d(source_pcd, target_pcd, voxel_size=args.voxel_size)
|
||||
|
||||
# 5. 将目标点云变换回源点云坐标系
|
||||
inv_transformation = np.linalg.inv(transformation)
|
||||
target_recovered = copy.deepcopy(target_pcd)
|
||||
target_recovered.transform(inv_transformation)
|
||||
|
||||
t1 = time.time()
|
||||
print(f"\n配准结果:")
|
||||
print(f" Fitness: {fitness:.4f}")
|
||||
print(f" RMSE: {rmse:.6f} m")
|
||||
print(f" 估计的变换矩阵:\n{transformation}")
|
||||
print(f"配准耗时: {t1 - t0:.4f} 秒")
|
||||
|
||||
# 6. 计算点到 Mesh 的距离
|
||||
print("\n计算点到 Mesh 距离...")
|
||||
t2 = time.time()
|
||||
points = np.asarray(target_recovered.points)
|
||||
distances = compute_point_to_mesh_distance_trimesh(points, args.mesh_path)
|
||||
t3 = time.time()
|
||||
|
||||
print(f"距离统计:")
|
||||
print(f" 平均距离: {np.mean(distances):.6f} mm ({np.mean(distances) * 1000:.3f} mm)")
|
||||
print(f" 最大距离: {np.max(distances):.6f} mm ({np.max(distances) * 1000:.3f} mm)")
|
||||
print(f" RMSE: {np.sqrt(np.mean(distances ** 2)):.6f} m")
|
||||
print(f"距离计算耗时: {t3 - t2:.4f} 秒")
|
||||
|
||||
# 7. 可视化
|
||||
if not args.no_visualize:
|
||||
print("\n开始可视化...")
|
||||
visualize_registration_comparison(source_pcd, target_pcd, target_recovered)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
280
alg/平整度、平面度、轮廓测量v1.5/archive/Readme.md
Normal file
280
alg/平整度、平面度、轮廓测量v1.5/archive/Readme.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# 工业零件几何测量工具使用指南
|
||||
> 支持测量种类:平整度、平面度、轮廓测量、曲率分析、点云去噪、平面拟合、点云配准评估
|
||||
> 支持测量任务编号:11、13、17、20、25、26、27、31、34、35、36、37、38、39、40、41、42、45
|
||||
|
||||
## 一、环境配置
|
||||
### 基础环境要求
|
||||
- Python 3.10.8
|
||||
- CUDA 11.8(建议,无CUDA时自动降级为CPU计算)
|
||||
- PyTorch 2.2
|
||||
|
||||
### 依赖安装
|
||||
```bash
|
||||
conda install -f requirements.txt
|
||||
```
|
||||
> 注:需确保环境中已安装OCC Core(STEP/STL处理)、trimesh、open3d、numpy、scipy等依赖
|
||||
|
||||
---
|
||||
|
||||
## 二、CAD转轻量化Mesh(CAD2Mesh.py)
|
||||
### 功能说明
|
||||
将STEP格式的CAD模型转换为轻量化STL网格文件,通过调整网格精度控制文件体积(精度值越大,文件越小,精度越低),纯后台处理无界面显示,转换效率高。
|
||||
|
||||
### 输入输出
|
||||
- **输入**:STEP格式CAD文件(路径示例:`High_Speed_Data/CAD/11.step`)
|
||||
- **输出**:STL格式轻量化网格文件(路径示例:`High_Speed_Data/Mesh/11.stl`)
|
||||
|
||||
### 命令行参数说明
|
||||
| 参数 | 简写 | 类型 | 默认值 | 说明 |
|
||||
|--------------|------|--------|---------------------------------|----------------------------------------|
|
||||
| --precision | -p | float | 1.0 | 网格精度(单位:mm),值越大文件体积越小 |
|
||||
| --input | -i | string | High_Speed_Data/CAD/11.step | 输入STEP文件路径 |
|
||||
| --output | -o | string | High_Speed_Data/Mesh/11.stl | 输出STL文件路径 |
|
||||
|
||||
### 运行示例
|
||||
```bash
|
||||
# 完整参数形式
|
||||
python CAD2Mesh.py --precision 1 --input "High_Speed_Data/CAD/11.step" --output "High_Speed_Data/Mesh/11.stl"
|
||||
|
||||
# 简写参数形式
|
||||
python CAD2Mesh.py -p 1 -i "High_Speed_Data/CAD/11.step" -o "High_Speed_Data/Mesh/11.stl"
|
||||
```
|
||||
|
||||
### 运行日志说明
|
||||
程序会输出三步处理日志及耗时:
|
||||
1. STEP文件读取阶段
|
||||
2. 网格离散化(轻量化)阶段
|
||||
3. STL文件保存阶段
|
||||
转换完成后会提示总耗时及STL文件打开工具建议(FreeCAD/MeshLab/3D Viewer)。
|
||||
|
||||
---
|
||||
|
||||
## 三、轮廓-平整度测量(Contour_Flatness.py)
|
||||
### 功能说明
|
||||
高精度计算点云到 CAD STEP 模型三角化表面的最近距离,输出平均距离(mean_dist)、最大距离(max_dist)和最小距离(min_dist),用于评估零件的平整度、平面度、轮廓偏差等几何精度指标;支持 CUDA 加速(自动检测),采用 torch.float64 高精度计算。
|
||||
|
||||
脚本现在支持两种模式:
|
||||
- 默认批量模式:按任务编号从 `High_Speed_Data/CAD/` 和点云目录中读取多组数据,并输出 Excel 汇总。
|
||||
- 单任务模式:添加 `--single` 后,使用 `--CAD` 和 `--pointcloud` 计算一组文件,可选 `--visualize` 显示热力图。批量模式会把所有成功执行的热力图和完整点云按原始坐标合并保存为一个 PLY,默认 `vis/combined_contour.ply`;使用 `--visualize` 时还会合并到一个窗口展示。
|
||||
|
||||
批量模式会优先使用 `Mesh/{样本名}.stl`,并按样本名匹配点云。例如 `13_1.stl` 和 `13_2.stl` 都属于任务 `13`,对应点云分别为 `13_1_noisy.ply`、`13_2_noisy.ply`;Excel 会按任务 `13` 汇总并记录样本数。没有匹配到 Mesh 样本时,脚本会退回旧的 `CAD/{任务号}.step` 模式。
|
||||
|
||||
距离计算流程已参考 `test8.py` 更新:默认先用点到三角形 AABB 的距离为每个点筛选候选三角面,再对候选三角面计算精确点到三角形距离。可通过 `--candidate_triangle_count` 控制候选数量;设为 `0` 或负数时退回旧的全量三角面分块精确搜索。
|
||||
|
||||
### 输入输出
|
||||
- **输入**:
|
||||
- CAD STEP 文件:路径示例 `High_Speed_Data/CAD/11.step`
|
||||
- Mesh 文件:路径示例 `High_Speed_Data/Mesh/13_1.stl`
|
||||
- 点云 PLY 文件:批量模式默认读取 `High_Speed_Data/Noise_PointCloud/{样本名}_noisy.ply`,也可用 `--pointcloud_kind clean` 读取 `High_Speed_Data/PointCloud/{样本名}.ply`
|
||||
- **输出**:
|
||||
- 平均距离(mean_dist):点云中所有点到 CAD 三角面的平均最近距离(单位:mm)
|
||||
- 最大距离(max_dist):点云中所有点到 CAD 三角面的最大最近距离(单位:mm)
|
||||
- 最小距离(min_dist):点云中所有点到 CAD 三角面的最小最近距离(单位:mm)
|
||||
- 批量模式会生成 Excel 汇总文件,默认 `平整度.xlsx`
|
||||
- 批量模式会保存合并热力图 PLY,默认 `vis/combined_contour.ply`,并默认加入完整点云 `datasets/train/train_2_10M.ply`
|
||||
|
||||
### 命令行参数说明
|
||||
| 参数 | 简写 | 类型 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| --CAD | -c | string | High_Speed_Data/CAD/11.step | 单任务模式 CAD STEP 文件路径,仅 `--single` 时使用 |
|
||||
| --pointcloud | -p | string | High_Speed_Data/Noise_PointCloud/11_noisy.ply | 单任务模式点云路径,仅 `--single` 时使用 |
|
||||
| --tasks | - | string | 11,13,17,20,25,26,27,31,34,35,36,37,38,39,40,41,42,45 | 批量任务编号或样本名,多个值用英文逗号分隔;`13` 匹配 `13_1/13_2`,`13_1` 只匹配该样本 |
|
||||
| --data_root | - | string | High_Speed_Data | 批量任务数据根目录 |
|
||||
| --pointcloud_kind | - | string | noise | 批量模式点云来源:`noise` 读取 Noise_PointCloud,`clean` 读取 PointCloud |
|
||||
| --excel_output | - | string | 平整度.xlsx | 批量结果 Excel 输出路径 |
|
||||
| --combined_output | - | string | vis/combined_contour.ply | 批量合并热力图 PLY 输出 |
|
||||
| --combined_context_pointcloud | - | string | datasets/train/train_2_10M.ply | 合并热力图时一起加入的完整点云,可切换为 `train_2_100M.ply` 或 `train_2_1T.ply` |
|
||||
| --single | - | flag | false | 启用单任务模式 |
|
||||
| --visualize | - | flag | false | 显示距离热力图;批量模式会合并所有成功结果 |
|
||||
| --mesh_deflection | - | float | 0.1 | CAD 三角化精度,越小越精细但越慢 |
|
||||
| --point_chunk_size | - | int | 2048 | 点云分块大小,显存/内存不足时可调小 |
|
||||
| --triangle_chunk_size | - | int | 4096 | 全量精确搜索模式下的三角面分块大小;`candidate_triangle_count <= 0` 时使用 |
|
||||
| --candidate_triangle_count | - | int | 32 | 每个点按 AABB 预筛的候选三角面数量;设为 0 或负数则使用全量精确搜索 |
|
||||
|
||||
### 运行示例
|
||||
```bash
|
||||
# 默认批量模式,使用 DEFAULT_TASK_IDS 并输出 平整度.xlsx
|
||||
python Contour_Flatness.py
|
||||
|
||||
# 指定批量任务、点云来源、Excel 输出和候选三角面数量
|
||||
python Contour_Flatness.py --tasks "13,31,40" --data_root "High_Speed_Data" --pointcloud_kind noise --excel_output "vis/flatness.xlsx" --candidate_triangle_count 32
|
||||
|
||||
# 单任务模式
|
||||
python Contour_Flatness.py --single --CAD "High_Speed_Data/CAD/11.step" --pointcloud "High_Speed_Data/Noise_PointCloud/11_noisy.ply" --candidate_triangle_count 32
|
||||
|
||||
# 单任务模式并显示热力图
|
||||
python Contour_Flatness.py --single -c "High_Speed_Data/CAD/11.step" -p "High_Speed_Data/PointCloud/11.ply" --candidate_triangle_count 32 --visualize
|
||||
```
|
||||
|
||||
### 计算说明
|
||||
1. CAD 读取:通过 pythonocc-core 读取 STEP,并使用 `mesh_deflection` 三角化。
|
||||
2. 点云读取:优先使用 open3d(若安装),否则使用 trimesh 兼容读取。
|
||||
3. 样本匹配:批量模式优先扫描 `Mesh` 文件夹,按样本名匹配 `PointCloud` 或 `Noise_PointCloud`;任务号取样本名下划线前的部分。
|
||||
4. 距离计算:默认使用 AABB 候选三角面预筛,减少每个点参与精确距离计算的三角面数量;如果 `candidate_triangle_count <= 0`,则使用全量三角面分块精确搜索。
|
||||
5. 批量结果:每个任务会记录成功/失败状态,成功样本会按任务号汇总并缓存在 `task_distance_data` 中;合并热力图会保存到 `--combined_output`,使用 `--visualize` 时会统一合并展示。
|
||||
6. 精度保障:采用 torch.float64 高精度计算,支持 GPU/CPU 自动切换。
|
||||
|
||||
---
|
||||
|
||||
## 四、点云曲率估计(curvature_estimation.py)
|
||||
### 功能说明
|
||||
基于Open3D对工业零件点云进行曲率计算,通过邻域协方差矩阵分解获取曲率值,支持曲率彩色可视化,用于识别零件边缘、棱角、曲面等特征区域,为表面缺陷检测提供依据。
|
||||
|
||||
### 输入输出
|
||||
- **输入**:PLY格式点云文件(路径示例:`High_Speed_Data/PointCloud/11.ply`)
|
||||
- **输出**:曲率统计值、彩色可视化点云、可保存带曲率信息的PLY文件
|
||||
|
||||
### 命令行参数说明
|
||||
| 参数 | 简写 | 类型 | 默认值 | 说明 |
|
||||
|--------------|------|--------|---------------------------------|----------------------------------------|
|
||||
| --ply_path | — | string | ./High_Speed_Data/PointCloud/11.ply | 输入点云路径 |
|
||||
| --radius | — | float | 2.0 | 邻域搜索半径 |
|
||||
| --max_nn | — | int | 30 | 最大邻域点数 |
|
||||
| --color_scale| — | float | 3.0 | 曲率颜色缩放因子 |
|
||||
| --save | — | string | None | 保存着色点云路径(可选)|
|
||||
|
||||
### 运行示例
|
||||
```bash
|
||||
# 基础运行
|
||||
python curvature_estimation.py --ply_path "High_Speed_Data/PointCloud/11.ply"
|
||||
|
||||
# 保存结果
|
||||
python curvature_estimation.py --ply_path "High_Speed_Data/PointCloud/11.ply" --save "High_Speed_Data/Result/11_curvature.ply"
|
||||
```
|
||||
|
||||
### 计算说明
|
||||
1. 自动估计法线并构建KD-Tree加速邻域搜索
|
||||
2. 邻域点不足3个时自动标记为高曲率
|
||||
3. 颜色映射:红色=高曲率,绿色=低曲率
|
||||
|
||||
---
|
||||
|
||||
## 五、点云去噪处理(denoise.py)
|
||||
### 功能说明
|
||||
集成SOR、ROR、PF、VGF、BF五种去噪与滤波算法,自动添加高斯噪声并对比效果,通过Chamfer Distance量化评估去噪质量,用于扫描点云预处理,提升测量精度。
|
||||
|
||||
### 输入输出
|
||||
- **输入**:PLY格式干净点云(路径示例:`High_Speed_Data/PointCloud/11.ply`)
|
||||
- **输出**:各算法去噪指标、保留率、移除点数、CD距离、可视化对比
|
||||
|
||||
### 命令行参数说明
|
||||
| 参数 | 简写 | 类型 | 默认值 | 说明 |
|
||||
|--------------|------|--------|---------------------------------|----------------------------------------|
|
||||
| --ply_path | -p | string | ./High_Speed_Data/PointCloud/11.ply | 输入点云路径 |
|
||||
| --noise_std | -n | float | 4.0 | 高斯噪声标准差 |
|
||||
| --voxel_size | -v | float | 0.05 | 体素滤波大小 |
|
||||
| --no_visualize | — | bool | False | 关闭可视化窗口 |
|
||||
|
||||
### 运行示例
|
||||
```bash
|
||||
# 基础去噪对比
|
||||
python denoise.py -p "High_Speed_Data/PointCloud/11.ply" --noise_std 2.0
|
||||
|
||||
# 无可视化运行
|
||||
python denoise.py -p "High_Speed_Data/PointCloud/11.ply" --no_visualize
|
||||
```
|
||||
|
||||
### 计算说明
|
||||
1. 自动给点云加噪,模拟真实扫描噪声
|
||||
2. 依次运行SOR/ROR/PF/VGF/BF算法
|
||||
3. 输出保留率、移除点数、Chamfer Distance评估结果
|
||||
|
||||
---
|
||||
|
||||
## 六、平面拟合估计(plane_estimation.py)
|
||||
### 功能说明
|
||||
基于Open3D内置RANSAC算法自动拟合点云中平面结构,输出平面方程、内点占比、法线误差、拟合RMSE,用于提取零件基准平面。
|
||||
|
||||
### 输入输出
|
||||
- **输入**:可自定义生成带噪平面点云
|
||||
- **输出**:平面方程、误差统计、拟合可视化
|
||||
|
||||
### 命令行参数说明
|
||||
| 参数 | 简写 | 类型 | 默认值 | 说明 |
|
||||
|----------------|------|--------|--------|--------------------------|
|
||||
| --threshold_mm | -t | float | 0.5 | 平面距离阈值(mm)|
|
||||
| --max_iterations | -i | int | 2000 | RANSAC迭代次数 |
|
||||
|
||||
### 运行示例
|
||||
```bash
|
||||
# 默认运行
|
||||
python plane_estimation.py
|
||||
|
||||
# 自定义参数
|
||||
python plane_estimation.py --threshold_mm 0.3 --max_iterations 1500
|
||||
```
|
||||
|
||||
### 计算说明
|
||||
1. 支持生成带噪声标准平面用于测试
|
||||
2. 输出平面法向量与理想Z轴夹角误差、偏移误差、拟合RMSE
|
||||
|
||||
---
|
||||
|
||||
## 七、点云配准与适应度评估(point-fitness.py)
|
||||
### 功能说明
|
||||
对点云执行旋转/平移变换,通过RANSAC+ICP完成高精度配准,输出配准适应度Fitness、RMSE,并计算配准后点云到参考网格的距离,用于评估配准精度与零件偏差。
|
||||
|
||||
### 输入输出
|
||||
- **输入**:待配准PLY点云、参考STL网格
|
||||
- **输出**:变换矩阵、配准分数、误差、点云到网格距离统计、配准可视化
|
||||
|
||||
### 命令行参数说明
|
||||
| 参数 | 简写 | 类型 | 默认值 | 说明 |
|
||||
|--------------|------|--------|---------------------------------------------|--------------------|
|
||||
| --ply_path | -p | string | ./High_Speed_Data/Noise_PointCloud/11_noisy.ply | 点云路径 |
|
||||
| --mesh_path | -m | string | ./High_Speed_Data/Mesh/11.stl | 网格路径 |
|
||||
| --voxel_size | -v | float | 10 | 配准下采样大小 |
|
||||
| --no_visualize | — | bool | False | 关闭可视化 |
|
||||
|
||||
### 运行示例
|
||||
```bash
|
||||
# 完整配准+距离计算
|
||||
python point-fitness.py -p "High_Speed_Data/Noise_PointCloud/11_noisy.ply" -m "High_Speed_Data/Mesh/11.stl" -v 5
|
||||
|
||||
# 无可视化
|
||||
python point-fitness.py -p "High_Speed_Data/Noise_PointCloud/11_noisy.ply" -m "High_Speed_Data/Mesh/11.stl" --no_visualize
|
||||
```
|
||||
|
||||
### 计算说明
|
||||
1. 支持指定XYZ三轴旋转与随机平移
|
||||
2. RANSAC粗配准 → ICP精配准
|
||||
3. 输出Fitness(越高越好)、RMSE(越低越好)
|
||||
|
||||
---
|
||||
|
||||
## 八、完整工作流程示例(任务11全流程)
|
||||
1. CAD转轻量化网格
|
||||
```bash
|
||||
python CAD2Mesh.py -p 0.5 -i "High_Speed_Data/CAD/11.step" -o "High_Speed_Data/Mesh/11.stl"
|
||||
```
|
||||
2. 点云去噪预处理
|
||||
```bash
|
||||
python denoise.py -p "High_Speed_Data/PointCloud/11.ply" --noise_std 2.0
|
||||
```
|
||||
3. 平面基准拟合
|
||||
```bash
|
||||
python plane_estimation.py
|
||||
```
|
||||
4. 曲率特征分析
|
||||
```bash
|
||||
python curvature_estimation.py --ply_path "High_Speed_Data/PointCloud/11.ply"
|
||||
```
|
||||
5. 轮廓-平整度测量
|
||||
```bash
|
||||
python Contour_Flatness.py --tasks "13,31,40" --data_root "High_Speed_Data" --excel_output "vis/flatness.xlsx" --candidate_triangle_count 32
|
||||
```
|
||||
6. 点云配准评估
|
||||
```bash
|
||||
python point-fitness.py -p "High_Speed_Data/Noise_PointCloud/11_noisy.ply" -m "High_Speed_Data/Mesh/11.stl"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、常见问题
|
||||
1. STEP文件读取失败:检查路径、文件完整性、OCC Core安装
|
||||
2. 点云读取报错:安装open3d(`pip install open3d`)
|
||||
3. CUDA Out of Memory:减小分块大小或降低点云密度
|
||||
4. 去噪效果差:调小SOR标准差、增大ROR半径
|
||||
5. 平面拟合失败:提高RANSAC迭代次数、降低距离阈值
|
||||
6. 配准Fitness低:减小voxel_size,降低旋转/平移幅度
|
||||
7. 可视化无窗口:服务器环境加`--no_visualize`参数
|
||||
@@ -0,0 +1,447 @@
|
||||
import os
|
||||
import argparse
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
|
||||
try:
|
||||
from OCC.Core.TopoDS import topods
|
||||
USE_TOPODS = "topods"
|
||||
except ImportError:
|
||||
from OCC.Core.TopoDS import TopoDS
|
||||
USE_TOPODS = "TopoDS"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# STEP -> Open3D Mesh
|
||||
# ============================================================
|
||||
|
||||
def read_step_file(step_path):
|
||||
reader = STEPControl_Reader()
|
||||
status = reader.ReadFile(step_path)
|
||||
|
||||
if status != IFSelect_RetDone:
|
||||
raise RuntimeError(f"STEP 文件读取失败: {step_path}")
|
||||
|
||||
reader.TransferRoots()
|
||||
shape = reader.OneShape()
|
||||
|
||||
print("[OK] STEP loaded:", step_path)
|
||||
return shape
|
||||
|
||||
|
||||
def occ_shape_to_numpy_mesh(shape):
|
||||
vertices = []
|
||||
triangles = []
|
||||
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
face_count = 0
|
||||
tri_count = 0
|
||||
|
||||
while explorer.More():
|
||||
current_shape = explorer.Current()
|
||||
|
||||
if USE_TOPODS == "topods":
|
||||
face = topods.Face(current_shape)
|
||||
else:
|
||||
face = TopoDS.Face(current_shape)
|
||||
|
||||
location = face.Location()
|
||||
triangulation = BRep_Tool.Triangulation(face, location)
|
||||
|
||||
if triangulation is None:
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
trsf = location.Transformation()
|
||||
base_index = len(vertices)
|
||||
|
||||
for i in range(1, triangulation.NbNodes() + 1):
|
||||
pnt = triangulation.Node(i)
|
||||
pnt.Transform(trsf)
|
||||
|
||||
vertices.append([
|
||||
pnt.X(),
|
||||
pnt.Y(),
|
||||
pnt.Z()
|
||||
])
|
||||
|
||||
for i in range(1, triangulation.NbTriangles() + 1):
|
||||
tri = triangulation.Triangle(i)
|
||||
n1, n2, n3 = tri.Get()
|
||||
|
||||
triangles.append([
|
||||
base_index + n1 - 1,
|
||||
base_index + n2 - 1,
|
||||
base_index + n3 - 1
|
||||
])
|
||||
|
||||
tri_count += 1
|
||||
|
||||
face_count += 1
|
||||
explorer.Next()
|
||||
|
||||
vertices = np.asarray(vertices, dtype=np.float64)
|
||||
triangles = np.asarray(triangles, dtype=np.int32)
|
||||
|
||||
print("[OK] CAD mesh extracted")
|
||||
print(" CAD faces:", face_count)
|
||||
print(" mesh vertices:", vertices.shape)
|
||||
print(" mesh triangles:", triangles.shape)
|
||||
|
||||
if vertices.shape[0] == 0 or triangles.shape[0] == 0:
|
||||
raise RuntimeError("CAD 三角网格提取失败")
|
||||
|
||||
return vertices, triangles
|
||||
|
||||
|
||||
def step_to_open3d_mesh(step_path, mesh_deflection=0.1):
|
||||
shape = read_step_file(step_path)
|
||||
|
||||
print("[INFO] Meshing CAD...")
|
||||
mesh_algo = BRepMesh_IncrementalMesh(shape, mesh_deflection)
|
||||
mesh_algo.Perform()
|
||||
|
||||
vertices, triangles = occ_shape_to_numpy_mesh(shape)
|
||||
|
||||
mesh = o3d.geometry.TriangleMesh()
|
||||
mesh.vertices = o3d.utility.Vector3dVector(vertices)
|
||||
mesh.triangles = o3d.utility.Vector3iVector(triangles)
|
||||
|
||||
mesh.remove_duplicated_vertices()
|
||||
mesh.remove_duplicated_triangles()
|
||||
mesh.remove_degenerate_triangles()
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 点云处理
|
||||
# ============================================================
|
||||
|
||||
def make_translation_matrix(dx, dy, dz):
|
||||
T = np.eye(4, dtype=np.float64)
|
||||
T[0, 3] = dx
|
||||
T[1, 3] = dy
|
||||
T[2, 3] = dz
|
||||
return T
|
||||
|
||||
|
||||
def crop_front_tail_by_x(pcd, ratio=0.08):
|
||||
"""
|
||||
按 X 轴裁剪车头和车尾。
|
||||
|
||||
front: X 最小端
|
||||
tail: X 最大端
|
||||
"""
|
||||
|
||||
points = np.asarray(pcd.points)
|
||||
|
||||
min_bound = points.min(axis=0)
|
||||
max_bound = points.max(axis=0)
|
||||
|
||||
x_min = min_bound[0]
|
||||
x_max = max_bound[0]
|
||||
x_len = x_max - x_min
|
||||
|
||||
front_min = min_bound.copy()
|
||||
front_max = max_bound.copy()
|
||||
front_min[0] = x_min
|
||||
front_max[0] = x_min + ratio * x_len
|
||||
|
||||
tail_min = min_bound.copy()
|
||||
tail_max = max_bound.copy()
|
||||
tail_min[0] = x_max - ratio * x_len
|
||||
tail_max[0] = x_max
|
||||
|
||||
front_box = o3d.geometry.AxisAlignedBoundingBox(
|
||||
min_bound=front_min,
|
||||
max_bound=front_max
|
||||
)
|
||||
|
||||
tail_box = o3d.geometry.AxisAlignedBoundingBox(
|
||||
min_bound=tail_min,
|
||||
max_bound=tail_max
|
||||
)
|
||||
|
||||
front_pcd = pcd.crop(front_box)
|
||||
tail_pcd = pcd.crop(tail_box)
|
||||
|
||||
print("[OK] Crop front/tail by X")
|
||||
print(" X range:", x_min, x_max)
|
||||
print(" front points:", np.asarray(front_pcd.points).shape)
|
||||
print(" tail points:", np.asarray(tail_pcd.points).shape)
|
||||
|
||||
return front_pcd, tail_pcd
|
||||
|
||||
|
||||
def add_gaussian_noise(pcd, noise_std=0.0):
|
||||
"""
|
||||
给点云添加高斯噪声。
|
||||
noise_std=0 表示不加噪声。
|
||||
"""
|
||||
|
||||
if noise_std <= 0:
|
||||
return pcd
|
||||
|
||||
points = np.asarray(pcd.points)
|
||||
noise = np.random.normal(
|
||||
loc=0.0,
|
||||
scale=noise_std,
|
||||
size=points.shape
|
||||
)
|
||||
|
||||
noisy_points = points + noise
|
||||
|
||||
noisy_pcd = o3d.geometry.PointCloud()
|
||||
noisy_pcd.points = o3d.utility.Vector3dVector(noisy_points)
|
||||
|
||||
if pcd.has_colors():
|
||||
noisy_pcd.colors = pcd.colors
|
||||
|
||||
if pcd.has_normals():
|
||||
noisy_pcd.normals = pcd.normals
|
||||
|
||||
return noisy_pcd
|
||||
|
||||
|
||||
def visualize_pair(cad_pcd, shifted_pcd):
|
||||
cad_vis = o3d.geometry.PointCloud(cad_pcd)
|
||||
shifted_vis = o3d.geometry.PointCloud(shifted_pcd)
|
||||
|
||||
cad_vis.paint_uniform_color([0.0, 0.5, 1.0])
|
||||
shifted_vis.paint_uniform_color([1.0, 0.0, 0.0])
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[cad_vis, shifted_vis],
|
||||
window_name="Blue = CAD sampled point cloud, Red = shifted test point cloud"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主函数
|
||||
# ============================================================
|
||||
|
||||
def generate_shifted_train_pointcloud(
|
||||
cad_path,
|
||||
output_dir,
|
||||
sample_points=1000000,
|
||||
mesh_deflection=0.1,
|
||||
crop_ratio=0.08,
|
||||
dx=100.0,
|
||||
dy=20.0,
|
||||
dz=-10.0,
|
||||
noise_std=0.0,
|
||||
visualize=True
|
||||
):
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# 1. CAD -> mesh
|
||||
cad_mesh = step_to_open3d_mesh(
|
||||
cad_path,
|
||||
mesh_deflection=mesh_deflection
|
||||
)
|
||||
|
||||
# 2. mesh -> 点云
|
||||
print(f"[INFO] Sampling CAD surface: {sample_points} points")
|
||||
cad_pcd = cad_mesh.sample_points_uniformly(
|
||||
number_of_points=sample_points
|
||||
)
|
||||
|
||||
cad_pcd_path = os.path.join(output_dir, "cad_sampled_whole.ply")
|
||||
o3d.io.write_point_cloud(cad_pcd_path, cad_pcd)
|
||||
print("[OK] Saved:", cad_pcd_path)
|
||||
|
||||
# 3. 裁剪原始 CAD 点云的车头 / 车尾
|
||||
cad_front, cad_tail = crop_front_tail_by_x(
|
||||
cad_pcd,
|
||||
ratio=crop_ratio
|
||||
)
|
||||
|
||||
o3d.io.write_point_cloud(
|
||||
os.path.join(output_dir, "cad_front.ply"),
|
||||
cad_front
|
||||
)
|
||||
|
||||
o3d.io.write_point_cloud(
|
||||
os.path.join(output_dir, "cad_tail.ply"),
|
||||
cad_tail
|
||||
)
|
||||
|
||||
# 4. 生成固定平移矩阵
|
||||
T = make_translation_matrix(dx, dy, dz)
|
||||
|
||||
print("\n========== Fixed Translation ==========")
|
||||
print("dx:", dx)
|
||||
print("dy:", dy)
|
||||
print("dz:", dz)
|
||||
print("T:")
|
||||
print(T)
|
||||
|
||||
# 5. 对整车点云施加固定平移
|
||||
shifted_pcd = o3d.geometry.PointCloud(cad_pcd)
|
||||
shifted_pcd.transform(T)
|
||||
|
||||
# 6. 可选:加噪声,模拟真实扫描点云
|
||||
shifted_pcd = add_gaussian_noise(
|
||||
shifted_pcd,
|
||||
noise_std=noise_std
|
||||
)
|
||||
|
||||
shifted_path = os.path.join(output_dir, "shifted_whole_pointcloud.ply")
|
||||
o3d.io.write_point_cloud(shifted_path, shifted_pcd)
|
||||
print("[OK] Saved:", shifted_path)
|
||||
|
||||
# 7. 裁剪平移后的车头 / 车尾
|
||||
shifted_front, shifted_tail = crop_front_tail_by_x(
|
||||
shifted_pcd,
|
||||
ratio=crop_ratio
|
||||
)
|
||||
|
||||
shifted_front_path = os.path.join(output_dir, "shifted_front.ply")
|
||||
shifted_tail_path = os.path.join(output_dir, "shifted_tail.ply")
|
||||
|
||||
o3d.io.write_point_cloud(shifted_front_path, shifted_front)
|
||||
o3d.io.write_point_cloud(shifted_tail_path, shifted_tail)
|
||||
|
||||
print("[OK] Saved:", shifted_front_path)
|
||||
print("[OK] Saved:", shifted_tail_path)
|
||||
|
||||
# 8. 保存真实位移矩阵
|
||||
matrix_path = os.path.join(output_dir, "GT_translation_matrix.txt")
|
||||
np.savetxt(matrix_path, T, fmt="%.10f")
|
||||
print("[OK] Saved:", matrix_path)
|
||||
|
||||
# 9. 保存真实位移向量
|
||||
translation_path = os.path.join(output_dir, "GT_translation_vector.txt")
|
||||
np.savetxt(
|
||||
translation_path,
|
||||
np.array([dx, dy, dz], dtype=np.float64),
|
||||
fmt="%.10f"
|
||||
)
|
||||
print("[OK] Saved:", translation_path)
|
||||
|
||||
# 10. 可视化
|
||||
if visualize:
|
||||
# 百万级点云显示会卡,所以这里可视化时降采样
|
||||
cad_vis = cad_pcd.voxel_down_sample(voxel_size=20.0)
|
||||
shifted_vis = shifted_pcd.voxel_down_sample(voxel_size=20.0)
|
||||
|
||||
visualize_pair(cad_vis, shifted_vis)
|
||||
|
||||
return {
|
||||
"cad_sampled_path": cad_pcd_path,
|
||||
"shifted_pointcloud_path": shifted_path,
|
||||
"shifted_front_path": shifted_front_path,
|
||||
"shifted_tail_path": shifted_tail_path,
|
||||
"translation_matrix_path": matrix_path,
|
||||
"translation_vector": np.array([dx, dy, dz], dtype=np.float64)
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 命令行入口
|
||||
# ============================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="从整车 CAD 生成测试点云,并施加固定前后/整体位移"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--cad",
|
||||
default=r"datasets/CAD/M02车.stp",
|
||||
help="整车 CAD STEP/STP 文件路径"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--output_dir",
|
||||
default=r"datasets\shift_test_output",
|
||||
help="输出目录"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--sample_points",
|
||||
type=int,
|
||||
default=1000000,
|
||||
help="从 CAD 表面采样的点数,默认 1000000"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--mesh_deflection",
|
||||
type=float,
|
||||
default=0.1,
|
||||
help="CAD 三角化精度,越小越精细,默认 0.1"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--crop_ratio",
|
||||
type=float,
|
||||
default=0.08,
|
||||
help="车头/车尾裁剪比例,默认 0.08"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dx",
|
||||
type=float,
|
||||
default=100.0,
|
||||
help="X 方向固定平移量"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dy",
|
||||
type=float,
|
||||
default=20.0,
|
||||
help="Y 方向固定平移量"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dz",
|
||||
type=float,
|
||||
default=-10.0,
|
||||
help="Z 方向固定平移量"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--noise_std",
|
||||
type=float,
|
||||
default=0.0,
|
||||
help="高斯噪声标准差,默认 0 表示不加噪声"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--no_visualize",
|
||||
action="store_true",
|
||||
help="不显示可视化窗口"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
result = generate_shifted_train_pointcloud(
|
||||
cad_path=args.cad,
|
||||
output_dir=args.output_dir,
|
||||
sample_points=args.sample_points,
|
||||
mesh_deflection=args.mesh_deflection,
|
||||
crop_ratio=args.crop_ratio,
|
||||
dx=args.dx,
|
||||
dy=args.dy,
|
||||
dz=args.dz,
|
||||
noise_std=args.noise_std,
|
||||
visualize=not args.no_visualize
|
||||
)
|
||||
|
||||
print("\n========== Result ==========")
|
||||
print("CAD sampled point cloud:", result["cad_sampled_path"])
|
||||
print("Shifted whole point cloud:", result["shifted_pointcloud_path"])
|
||||
print("Shifted front point cloud:", result["shifted_front_path"])
|
||||
print("Shifted tail point cloud:", result["shifted_tail_path"])
|
||||
print("GT translation vector:", result["translation_vector"])
|
||||
print("GT matrix:", result["translation_matrix_path"])
|
||||
362
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/3D Douglas-Peucker.py
Normal file
362
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/3D Douglas-Peucker.py
Normal file
@@ -0,0 +1,362 @@
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
import matplotlib.pyplot as plt
|
||||
from mpl_toolkits.mplot3d import Axes3D
|
||||
|
||||
|
||||
def point_to_line_distance(point, line_start, line_end):
|
||||
"""
|
||||
计算点到三维线段的最短距离
|
||||
|
||||
Args:
|
||||
point: 待计算点 (x, y, z)
|
||||
line_start: 线段起点 (x, y, z)
|
||||
line_end: 线段终点 (x, y, z)
|
||||
|
||||
Returns:
|
||||
点到线段的垂直距离
|
||||
"""
|
||||
p = np.array(point)
|
||||
a = np.array(line_start)
|
||||
b = np.array(line_end)
|
||||
|
||||
# 计算向量
|
||||
ab = b - a
|
||||
ap = p - a
|
||||
|
||||
# 投影参数 t
|
||||
t = np.dot(ap, ab) / np.dot(ab, ab)
|
||||
|
||||
# 找到线段上的最近点
|
||||
if t <= 0:
|
||||
closest = a
|
||||
elif t >= 1:
|
||||
closest = b
|
||||
else:
|
||||
closest = a + t * ab
|
||||
|
||||
# 计算距离
|
||||
distance = np.linalg.norm(p - closest)
|
||||
return distance
|
||||
|
||||
|
||||
def point_to_plane_distance(point, plane_point1, plane_point2, plane_point3):
|
||||
"""
|
||||
计算点到三维平面的距离(三点确定一个平面)
|
||||
|
||||
Args:
|
||||
point: 待计算点 (x, y, z)
|
||||
plane_point1, plane_point2, plane_point3: 平面上的三个点
|
||||
|
||||
Returns:
|
||||
点到平面的垂直距离
|
||||
"""
|
||||
p = np.array(point)
|
||||
p1 = np.array(plane_point1)
|
||||
p2 = np.array(plane_point2)
|
||||
p3 = np.array(plane_point3)
|
||||
|
||||
# 计算平面法向量
|
||||
v1 = p2 - p1
|
||||
v2 = p3 - p1
|
||||
normal = np.cross(v1, v2)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
|
||||
# 计算点到平面的距离
|
||||
distance = abs(np.dot(normal, p - p1))
|
||||
return distance
|
||||
|
||||
|
||||
def douglas_peucker_3d(points, epsilon, use_plane=False):
|
||||
"""
|
||||
3D Douglas-Peucker 算法
|
||||
|
||||
Args:
|
||||
points: 三维点列表或numpy数组,形状 (N, 3)
|
||||
epsilon: 容差阈值
|
||||
use_plane: 是否使用平面距离(True)还是线段距离(False)
|
||||
|
||||
Returns:
|
||||
简化后的点云列表
|
||||
"""
|
||||
if len(points) <= 2:
|
||||
return points.tolist() if isinstance(points, np.ndarray) else points
|
||||
|
||||
# 转换为列表格式
|
||||
if isinstance(points, np.ndarray):
|
||||
points = points.tolist()
|
||||
|
||||
# 首尾点
|
||||
start = points[0]
|
||||
end = points[-1]
|
||||
|
||||
# 找到距离最大的点
|
||||
max_dist = 0
|
||||
max_index = 0
|
||||
|
||||
for i in range(1, len(points) - 1):
|
||||
if use_plane and len(points) > 2:
|
||||
# 使用平面距离(需要至少3个点确定平面)
|
||||
if i > 0 and i < len(points) - 1:
|
||||
# 使用当前点、前一个点和后一个点确定平面
|
||||
plane_p1 = points[i - 1]
|
||||
plane_p2 = points[i]
|
||||
plane_p3 = points[i + 1]
|
||||
dist = point_to_plane_distance(points[i], plane_p1, plane_p2, plane_p3)
|
||||
else:
|
||||
dist = point_to_line_distance(points[i], start, end)
|
||||
else:
|
||||
# 使用线段距离
|
||||
dist = point_to_line_distance(points[i], start, end)
|
||||
|
||||
if dist > max_dist:
|
||||
max_dist = dist
|
||||
max_index = i
|
||||
|
||||
# 递归简化
|
||||
if max_dist > epsilon:
|
||||
# 保留该点,递归处理左右两部分
|
||||
left = douglas_peucker_3d(points[:max_index + 1], epsilon, use_plane)
|
||||
right = douglas_peucker_3d(points[max_index:], epsilon, use_plane)
|
||||
|
||||
# 合并结果(避免重复端点)
|
||||
return left[:-1] + right
|
||||
else:
|
||||
# 只保留首尾点
|
||||
return [start, end]
|
||||
|
||||
|
||||
class DouglasPeucker3D:
|
||||
"""
|
||||
3D Douglas-Peucker 算法的面向对象封装
|
||||
支持点云简化、轨迹压缩、轮廓提取等
|
||||
"""
|
||||
|
||||
def __init__(self, epsilon=0.01, use_plane=False):
|
||||
"""
|
||||
Args:
|
||||
epsilon: 容差阈值
|
||||
use_plane: 是否使用平面距离(更适合曲面点云)
|
||||
"""
|
||||
self.epsilon = epsilon
|
||||
self.use_plane = use_plane
|
||||
|
||||
def simplify(self, points):
|
||||
"""
|
||||
简化点云
|
||||
|
||||
Args:
|
||||
points: (N, 3) 的 numpy 数组或列表
|
||||
|
||||
Returns:
|
||||
简化后的点云 (N', 3) numpy 数组
|
||||
"""
|
||||
if isinstance(points, np.ndarray):
|
||||
points = points.tolist()
|
||||
|
||||
simplified = douglas_peucker_3d(points, self.epsilon, self.use_plane)
|
||||
return np.array(simplified)
|
||||
|
||||
def simplify_trajectory(self, trajectory, time_weight=False):
|
||||
"""
|
||||
专门用于轨迹简化(考虑时间维度)
|
||||
|
||||
Args:
|
||||
trajectory: (N, 4) 的数组 [x, y, z, t] 或 [x, y, z]
|
||||
time_weight: 是否加入时间权重
|
||||
|
||||
Returns:
|
||||
简化后的轨迹
|
||||
"""
|
||||
if time_weight and trajectory.shape[1] == 4:
|
||||
# 时间加权:将时间作为第四维参与距离计算
|
||||
# 需要自定义距离函数
|
||||
pass
|
||||
|
||||
return self.simplify(trajectory[:, :3])
|
||||
|
||||
def extract_contour(self, points, return_indices=False):
|
||||
"""
|
||||
提取点云轮廓(结合法线估计)
|
||||
|
||||
Args:
|
||||
points: (N, 3) 点云
|
||||
return_indices: 是否返回原始点索引
|
||||
|
||||
Returns:
|
||||
轮廓点集
|
||||
"""
|
||||
# 先进行粗略简化
|
||||
simplified = self.simplify(points)
|
||||
|
||||
# 这里可以结合法线估计进一步提取边界点
|
||||
# 简化实现:返回简化后的点作为轮廓候选
|
||||
|
||||
if return_indices:
|
||||
# 查找简化点在原始点云中的最近邻索引
|
||||
from scipy.spatial import KDTree
|
||||
tree = KDTree(points)
|
||||
indices = []
|
||||
for p in simplified:
|
||||
_, idx = tree.query(p)
|
||||
indices.append(idx)
|
||||
return simplified, np.array(indices)
|
||||
|
||||
return simplified
|
||||
|
||||
|
||||
# ========== 示例和使用 ==========
|
||||
|
||||
def generate_test_curve(n_points=100, noise=0.02):
|
||||
"""
|
||||
生成测试用的三维曲线(螺旋线 + 噪声)
|
||||
"""
|
||||
t = np.linspace(0, 4 * np.pi, n_points)
|
||||
x = np.sin(t)
|
||||
y = np.cos(t)
|
||||
z = t / (2 * np.pi)
|
||||
|
||||
points = np.column_stack([x, y, z])
|
||||
|
||||
# 添加噪声
|
||||
if noise > 0:
|
||||
points += np.random.randn(n_points, 3) * noise
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def visualize_simplification(original, simplified, title="3D Douglas-Peucker Simplification"):
|
||||
"""
|
||||
可视化原始点云和简化后的点云
|
||||
"""
|
||||
fig = plt.figure(figsize=(12, 6))
|
||||
|
||||
# 原始点云
|
||||
ax1 = fig.add_subplot(121, projection='3d')
|
||||
ax1.scatter(original[:, 0], original[:, 1], original[:, 2],
|
||||
c='blue', s=10, alpha=0.6, label='Original')
|
||||
ax1.plot(original[:, 0], original[:, 1], original[:, 2],
|
||||
'b-', alpha=0.3)
|
||||
ax1.set_title(f'Original: {len(original)} points')
|
||||
ax1.legend()
|
||||
|
||||
# 简化后点云
|
||||
ax2 = fig.add_subplot(122, projection='3d')
|
||||
ax2.scatter(simplified[:, 0], simplified[:, 1], simplified[:, 2],
|
||||
c='red', s=30, alpha=0.8, label='Simplified')
|
||||
ax2.plot(simplified[:, 0], simplified[:, 1], simplified[:, 2],
|
||||
'r-', linewidth=2)
|
||||
ax2.set_title(f'Simplified: {len(simplified)} points (ε={epsilon})')
|
||||
ax2.legend()
|
||||
|
||||
plt.suptitle(title)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
def compare_epsilon(points, epsilons):
|
||||
"""
|
||||
比较不同 epsilon 值的简化效果
|
||||
"""
|
||||
results = {}
|
||||
for eps in epsilons:
|
||||
dp = DouglasPeucker3D(epsilon=eps)
|
||||
simplified = dp.simplify(points)
|
||||
results[eps] = {
|
||||
'points': simplified,
|
||||
'count': len(simplified),
|
||||
'compression_ratio': len(simplified) / len(points)
|
||||
}
|
||||
print(f"ε={eps:.4f}: {len(simplified)} 点 (压缩率: {results[eps]['compression_ratio']:.2%})")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def demo_point_cloud():
|
||||
"""
|
||||
点云简化演示(使用螺旋线模拟)
|
||||
"""
|
||||
print("=" * 50)
|
||||
print("3D Douglas-Peucker 算法演示")
|
||||
print("=" * 50)
|
||||
|
||||
# 生成测试数据
|
||||
points = generate_test_curve(200, noise=0.01)
|
||||
print(f"\n原始点云: {len(points)} 个点")
|
||||
|
||||
# 测试不同 epsilon
|
||||
epsilons = [0.01, 0.03, 0.05, 0.1, 0.2]
|
||||
print("\n不同容差阈值的简化效果:")
|
||||
results = compare_epsilon(points, epsilons)
|
||||
|
||||
# 可视化
|
||||
dp = DouglasPeucker3D(epsilon=0.05)
|
||||
simplified = dp.simplify(points)
|
||||
visualize_simplification(points, simplified,
|
||||
f"3D DP Simplification (ε=0.05, {len(simplified)}/{len(points)} points)")
|
||||
|
||||
|
||||
def demo_trajectory():
|
||||
"""
|
||||
轨迹简化演示(模拟移动轨迹)
|
||||
"""
|
||||
print("\n" + "=" * 50)
|
||||
print("3D 轨迹简化演示")
|
||||
print("=" * 50)
|
||||
|
||||
# 生成测试轨迹(随机路径)
|
||||
np.random.seed(42)
|
||||
n_points = 100
|
||||
t = np.linspace(0, 10, n_points)
|
||||
x = np.sin(t) + np.random.randn(n_points) * 0.02
|
||||
y = np.cos(t) + np.random.randn(n_points) * 0.02
|
||||
z = t / 5 + np.random.randn(n_points) * 0.01
|
||||
|
||||
trajectory = np.column_stack([x, y, z])
|
||||
print(f"原始轨迹: {len(trajectory)} 个点")
|
||||
|
||||
# 简化轨迹
|
||||
dp = DouglasPeucker3D(epsilon=0.08)
|
||||
simplified = dp.simplify_trajectory(trajectory)
|
||||
print(f"简化后轨迹: {len(simplified)} 个点 (压缩率: {len(simplified) / len(trajectory):.2%})")
|
||||
|
||||
# 可视化
|
||||
visualize_simplification(trajectory, simplified, "3D Trajectory Simplification")
|
||||
|
||||
|
||||
def demo_point_cloud_from_file(pcd_file=None):
|
||||
"""
|
||||
从文件加载点云并简化
|
||||
"""
|
||||
try:
|
||||
import open3d as o3d
|
||||
|
||||
if pcd_file is None:
|
||||
# 使用示例:生成随机点云
|
||||
print("\n生成随机点云进行演示...")
|
||||
points = np.random.randn(500, 3) * 0.5
|
||||
else:
|
||||
pcd = o3d.io.read_point_cloud(pcd_file)
|
||||
points = np.asarray(pcd.points)
|
||||
|
||||
print(f"原始点云: {len(points)} 个点")
|
||||
|
||||
# 简化
|
||||
dp = DouglasPeucker3D(epsilon=0.05)
|
||||
|
||||
# 注意:传统 DP 算法需要有序点集,对于无序点云需要先排序或使用其他方法
|
||||
# 这里演示对有序轨迹/轮廓的应用
|
||||
print("\n注意: DP算法适用于有序点集(如轮廓线、轨迹),")
|
||||
print("对于无序点云,建议先进行排序或使用其他简化方法(如体素滤波)")
|
||||
|
||||
except ImportError:
|
||||
print("需要安装 open3d 来读取点云文件: pip install open3d")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 运行演示
|
||||
demo_point_cloud()
|
||||
demo_trajectory()
|
||||
|
||||
# 如果需要处理实际点云文件
|
||||
# demo_point_cloud_from_file("your_pointcloud.pcd")
|
||||
95
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/BF.py
Normal file
95
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/BF.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
def chamfer_distance(pcd1, pcd2):
|
||||
"""
|
||||
计算两个点云之间的 CD 距离(Chamfer Distance)
|
||||
|
||||
Args:
|
||||
pcd1: Open3D 点云对象1
|
||||
pcd2: Open3D 点云对象2
|
||||
|
||||
Returns:
|
||||
cd: Chamfer Distance 值
|
||||
cd1: 点云1 到 点云2 的平均距离
|
||||
cd2: 点云2 到 点云1 的平均距离
|
||||
"""
|
||||
# 获取点坐标
|
||||
points1 = np.asarray(pcd1.points)
|
||||
points2 = np.asarray(pcd2.points)
|
||||
|
||||
# 构建 KD-Tree 加速最近邻搜索
|
||||
tree1 = KDTree(points1)
|
||||
tree2 = KDTree(points2)
|
||||
|
||||
# 计算 points1 中每个点到 points2 的最近距离
|
||||
dist1, _ = tree2.query(points1, k=1)
|
||||
cd1 = np.mean(dist1 ** 2) # 平方距离的平均值
|
||||
|
||||
# 计算 points2 中每个点到 points1 的最近距离
|
||||
dist2, _ = tree1.query(points2, k=1)
|
||||
cd2 = np.mean(dist2 ** 2) # 平方距离的平均值
|
||||
|
||||
# 总 CD 距离(双向平均)
|
||||
cd = cd1 + cd2
|
||||
|
||||
return np.sqrt(cd), cd1, cd2
|
||||
|
||||
def bilateral_filter(pcd, sigma_s=0.01, sigma_r=0.1):
|
||||
"""
|
||||
对点云进行双边滤波
|
||||
Args:
|
||||
pcd: Open3D点云对象
|
||||
sigma_s: 空间标准差
|
||||
sigma_r: 特征(法线)标准差
|
||||
"""
|
||||
points = np.asarray(pcd.points)
|
||||
normals = np.asarray(pcd.normals)
|
||||
|
||||
# 构建KD树
|
||||
tree = KDTree(points)
|
||||
|
||||
filtered_points = np.zeros_like(points)
|
||||
|
||||
for i, p in enumerate(points):
|
||||
# 搜索邻域点
|
||||
indices = tree.query_ball_point(p, r=2 * sigma_s)
|
||||
if len(indices) <= 1:
|
||||
filtered_points[i] = p
|
||||
continue
|
||||
|
||||
neigh_points = points[indices]
|
||||
neigh_normals = normals[indices]
|
||||
|
||||
# 计算空间权重
|
||||
spatial_dist = np.linalg.norm(neigh_points - p, axis=1)
|
||||
w_s = np.exp(-spatial_dist ** 2 / (2 * sigma_s ** 2))
|
||||
|
||||
# 计算特征(法线)权重
|
||||
normal_diff = np.linalg.norm(neigh_normals - normals[i], axis=1)
|
||||
w_r = np.exp(-normal_diff ** 2 / (2 * sigma_r ** 2))
|
||||
|
||||
# 双边权重
|
||||
w = w_s * w_r
|
||||
w = w / w.sum() # 归一化
|
||||
|
||||
# 加权平均
|
||||
filtered_points[i] = np.sum(neigh_points.T * w, axis=1)
|
||||
|
||||
# 更新点云
|
||||
filtered_pcd = o3d.geometry.PointCloud()
|
||||
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
|
||||
filtered_pcd.normals = o3d.utility.Vector3dVector(normals)
|
||||
|
||||
return filtered_pcd
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_head.ply")
|
||||
pcd.estimate_normals()
|
||||
pcd_filtered = bilateral_filter(pcd, sigma_s=10, sigma_r=10)
|
||||
|
||||
cd=chamfer_distance(pcd,pcd_filtered)
|
||||
print(cd)
|
||||
54
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/CE.py
Normal file
54
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/CE.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def compute_curvature_open3d(pcd, radius=0.1, max_nn=30):
|
||||
"""
|
||||
使用 Open3D 计算曲率
|
||||
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
radius: 搜索半径
|
||||
max_nn: 最大邻居数
|
||||
|
||||
Returns:
|
||||
curvature: 曲率数组
|
||||
"""
|
||||
# 估计法线
|
||||
pcd.estimate_normals(
|
||||
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=max_nn)
|
||||
)
|
||||
|
||||
# 计算曲率(Open3D 在估计法线时已计算)
|
||||
# 曲率值存储在 pcd.point.normals 的第三个分量?
|
||||
# 实际上 Open3D 需要单独计算
|
||||
|
||||
# 方法:使用 PCA 重新计算
|
||||
points = np.asarray(pcd.points)
|
||||
n_points = len(points)
|
||||
curvature = np.zeros(n_points)
|
||||
|
||||
tree = o3d.geometry.KDTreeFlann(pcd)
|
||||
|
||||
for i in range(n_points):
|
||||
[k, idx, _] = tree.search_radius_vector_3d(pcd.points[i], radius)
|
||||
|
||||
if k < 3:
|
||||
curvature[i] = 1.0
|
||||
continue
|
||||
|
||||
neighbors = points[idx]
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
centered = neighbors - centroid
|
||||
cov = np.cov(centered.T)
|
||||
eigenvalues = np.linalg.eigvalsh(cov)
|
||||
eigenvalues = np.sort(eigenvalues)
|
||||
|
||||
curvature[i] = eigenvalues[0] / (eigenvalues.sum() + 1e-8)
|
||||
|
||||
return curvature
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("pointcloud.pcd")
|
||||
curvature = compute_curvature_open3d(pcd, radius=0.05)
|
||||
50
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/DBSCAN.py
Normal file
50
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/DBSCAN.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def dbscan_open3d(pcd, eps=0.1, min_points=10):
|
||||
"""
|
||||
使用 Open3D 进行 DBSCAN 聚类
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
eps: 邻域半径
|
||||
min_points: 最小邻域点数
|
||||
Returns:
|
||||
labels: 每个点的簇标签(-1 表示噪声)
|
||||
clusters: 各簇的点索引列表
|
||||
"""
|
||||
# DBSCAN 聚类
|
||||
labels = np.array(pcd.cluster_dbscan(eps=eps, min_points=min_points))
|
||||
|
||||
# 提取各簇索引
|
||||
unique_labels = np.unique(labels)
|
||||
clusters = []
|
||||
for label in unique_labels:
|
||||
if label == -1:
|
||||
continue
|
||||
clusters.append(np.where(labels == label)[0])
|
||||
|
||||
return labels, clusters
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_head.ply")
|
||||
|
||||
# 预处理
|
||||
pcd = pcd.voxel_down_sample(voxel_size=0.05)
|
||||
pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
|
||||
|
||||
# DBSCAN 聚类
|
||||
labels, clusters = dbscan_open3d(pcd, eps=0.1, min_points=50)
|
||||
|
||||
print(f"找到 {len(clusters)} 个聚类")
|
||||
print(f"噪声点: {np.sum(labels == -1)}")
|
||||
|
||||
# 为每个聚类着色
|
||||
colors = np.random.rand(len(clusters), 3)
|
||||
colored_points = np.zeros((len(pcd.points), 3))
|
||||
for i, cluster in enumerate(clusters):
|
||||
colored_points[cluster] = colors[i]
|
||||
pcd.colors = o3d.utility.Vector3dVector(colored_points)
|
||||
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
65
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/EN.py
Normal file
65
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/EN.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def compute_curvature_open3d(pcd, radius=0.05, max_nn=30):
|
||||
"""
|
||||
使用 Open3D 计算点云曲率
|
||||
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
radius: 搜索半径
|
||||
max_nn: 最大邻居数
|
||||
|
||||
Returns:
|
||||
curvatures: (N,) 曲率数组
|
||||
"""
|
||||
# 估计法线(同时获取协方差矩阵信息)
|
||||
pcd.estimate_normals(
|
||||
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=max_nn)
|
||||
)
|
||||
|
||||
# 获取点云数据
|
||||
points = np.asarray(pcd.points)
|
||||
n_points = len(points)
|
||||
curvatures = np.zeros(n_points)
|
||||
|
||||
# 构建 KD-Tree
|
||||
tree = o3d.geometry.KDTreeFlann(pcd)
|
||||
|
||||
for i in range(n_points):
|
||||
# 搜索邻域点
|
||||
[k, idx, _] = tree.search_radius_vector_3d(pcd.points[i], radius)
|
||||
|
||||
if k < 3:
|
||||
curvatures[i] = 1.0
|
||||
continue
|
||||
|
||||
# 获取邻域点坐标
|
||||
neighbors = points[idx]
|
||||
|
||||
# 计算协方差矩阵
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
centered = neighbors - centroid
|
||||
cov = np.cov(centered.T)
|
||||
|
||||
# 特征值分解
|
||||
eigenvalues = np.linalg.eigvalsh(cov)
|
||||
eigenvalues = np.sort(eigenvalues)
|
||||
|
||||
# 计算曲率
|
||||
curvatures[i] = eigenvalues[0] / (eigenvalues.sum() + 1e-8)
|
||||
|
||||
return curvatures
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\PointCloud\\38.ply")
|
||||
curvatures = compute_curvature_open3d(pcd, radius=50, max_nn=30)
|
||||
|
||||
# 将曲率作为颜色可视化
|
||||
colors = np.zeros((len(pcd.points), 3))
|
||||
colors[:, 0] = curvatures*3 # 红色通道表示曲率
|
||||
colors[:, 1] = 1 - curvatures*3 # 绿色通道为互补色
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
47
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/Eucilid.py
Normal file
47
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/Eucilid.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def euclidean_clustering_open3d(pcd, radius=0.1, min_cluster_size=50, max_cluster_size=10000):
|
||||
"""
|
||||
使用 Open3D 进行欧几里得聚类
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
radius: 聚类半径
|
||||
min_cluster_size: 最小簇大小
|
||||
max_cluster_size: 最大簇大小
|
||||
Returns:
|
||||
labels: 每个点的簇标签(-1 表示噪声/无效簇)
|
||||
clusters: 各簇的点索引列表
|
||||
"""
|
||||
# 欧几里得聚类
|
||||
labels = np.array(pcd.cluster_dbscan(eps=radius, min_points=min_cluster_size))
|
||||
|
||||
# 统计簇信息
|
||||
unique_labels = np.unique(labels)
|
||||
clusters = []
|
||||
|
||||
for label in unique_labels:
|
||||
if label == -1: # -1 表示噪声点
|
||||
continue
|
||||
cluster_indices = np.where(labels == label)[0]
|
||||
if min_cluster_size <= len(cluster_indices) <= max_cluster_size:
|
||||
clusters.append(cluster_indices)
|
||||
|
||||
return labels, clusters
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_left_side.ply")
|
||||
labels, clusters = euclidean_clustering_open3d(pcd, radius=4, min_cluster_size=100)
|
||||
|
||||
print(f"找到 {len(clusters)} 个聚类")
|
||||
|
||||
# 为每个聚类着色
|
||||
colors = np.random.rand(len(clusters), 3)
|
||||
colored_points = np.zeros((len(pcd.points), 3))
|
||||
for i, cluster in enumerate(clusters):
|
||||
colored_points[cluster] = colors[i]
|
||||
|
||||
pcd.colors = o3d.utility.Vector3dVector(colored_points)
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
16
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/FPFH.py
Normal file
16
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/FPFH.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
pcd = o3d.io.read_point_cloud("pointcloud.pcd")
|
||||
|
||||
# 1. 计算法线
|
||||
pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamKNN(k=30))
|
||||
|
||||
# 2. 提取FPFH特征
|
||||
fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
pcd,
|
||||
o3d.geometry.KDTreeSearchParamKNN(k=50)
|
||||
)
|
||||
|
||||
# fpfh.data 是 (33, N) 的矩阵,每列是一个点的33维FPFH描述子
|
||||
print(fpfh.data.shape) # (33, N)
|
||||
17
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/NE.py
Normal file
17
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/NE.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
pcd = o3d.io.read_point_cloud("pointcloud.pcd")
|
||||
|
||||
# 估计法线
|
||||
pcd.estimate_normals(
|
||||
search_param=o3d.geometry.KDTreeSearchParamHybrid(
|
||||
radius=0.1, # 搜索半径
|
||||
max_nn=30 # 最大邻居数
|
||||
)
|
||||
)
|
||||
|
||||
# 可视化法线
|
||||
o3d.visualization.draw_geometries([pcd], point_show_normal=True)
|
||||
|
||||
# 获取法线数据
|
||||
normals = np.asarray(pcd.normals)
|
||||
315
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/P2P.py
Normal file
315
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/P2P.py
Normal file
@@ -0,0 +1,315 @@
|
||||
|
||||
import copy
|
||||
from scipy.spatial.transform import Rotation as R
|
||||
import trimesh
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import time
|
||||
def rotation_matrix_scipy(roll_deg=10, pitch_deg=10, yaw_deg=10):
|
||||
"""
|
||||
使用 scipy 生成绕 XYZ 轴旋转的矩阵
|
||||
"""
|
||||
r = R.from_euler('xyz', [roll_deg, pitch_deg, yaw_deg], degrees=True)
|
||||
return r.as_matrix()
|
||||
|
||||
|
||||
|
||||
|
||||
def random_rotation_matrix():
|
||||
"""生成随机旋转矩阵"""
|
||||
angle = np.random.uniform(0, 2 * np.pi)
|
||||
axis = np.random.randn(3)
|
||||
axis = axis / np.linalg.norm(axis)
|
||||
|
||||
K = np.array([
|
||||
[0, -axis[2], axis[1]],
|
||||
[axis[2], 0, -axis[0]],
|
||||
[-axis[1], axis[0], 0]
|
||||
])
|
||||
R = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * K @ K
|
||||
return R
|
||||
|
||||
|
||||
def random_translation_vector(range_xyz=0.5):
|
||||
"""生成随机平移向量"""
|
||||
return np.random.uniform(-range_xyz, range_xyz, 3)
|
||||
|
||||
|
||||
def transform_point_cloud(pcd, R, t):
|
||||
"""对点云应用变换"""
|
||||
transformed_pcd = copy.deepcopy(pcd)
|
||||
points = np.asarray(transformed_pcd.points)
|
||||
points_transformed = (R @ points.T).T + t
|
||||
transformed_pcd.points = o3d.utility.Vector3dVector(points_transformed)
|
||||
return transformed_pcd
|
||||
|
||||
|
||||
def registration_open3d(source_pcd, target_pcd, voxel_size=0.005):
|
||||
"""
|
||||
点云配准(RANSAC + ICP)
|
||||
|
||||
Returns:
|
||||
transformation: 4x4 变换矩阵
|
||||
fitness: 配准 fitness 分数
|
||||
rmse: 配准 RMSE
|
||||
"""
|
||||
# 1. 下采样
|
||||
source_down = source_pcd.voxel_down_sample(voxel_size)
|
||||
target_down = target_pcd.voxel_down_sample(voxel_size)
|
||||
|
||||
# 2. 估计法线
|
||||
radius = voxel_size * 2
|
||||
source_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=30))
|
||||
target_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=30))
|
||||
|
||||
# 3. 计算 FPFH 特征
|
||||
radius_feature = voxel_size * 5
|
||||
source_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
source_down, o3d.geometry.KDTreeSearchParamHybrid(radius=radius_feature, max_nn=100))
|
||||
target_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
target_down, o3d.geometry.KDTreeSearchParamHybrid(radius=radius_feature, max_nn=100))
|
||||
|
||||
# 4. RANSAC 粗配准
|
||||
distance_threshold = voxel_size * 1.5
|
||||
result_ransac = o3d.pipelines.registration.registration_ransac_based_on_feature_matching(
|
||||
source_down, target_down, source_fpfh, target_fpfh,
|
||||
mutual_filter=True,
|
||||
max_correspondence_distance=distance_threshold,
|
||||
estimation_method=o3d.pipelines.registration.TransformationEstimationPointToPoint(),
|
||||
ransac_n=3,
|
||||
checkers=[
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnEdgeLength(0.9),
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnDistance(distance_threshold)
|
||||
],
|
||||
criteria=o3d.pipelines.registration.RANSACConvergenceCriteria(100000, 0.999))
|
||||
|
||||
print(f"RANSAC 配准完成")
|
||||
source_pcd.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
target_pcd.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
# 5. ICP 精配准
|
||||
result_icp = o3d.pipelines.registration.registration_icp(
|
||||
source_pcd, target_pcd, distance_threshold / 2, result_ransac.transformation,
|
||||
o3d.pipelines.registration.TransformationEstimationPointToPlane(),
|
||||
o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=200))
|
||||
|
||||
print(f"ICP 精配准完成")
|
||||
print(f" Fitness: {result_icp.fitness:.4f}")
|
||||
print(f" RMSE: {result_icp.inlier_rmse:.6f} m")
|
||||
|
||||
return result_icp.transformation, result_icp.fitness, result_icp.inlier_rmse
|
||||
|
||||
|
||||
def visualize_registration_result(original_pcd, transformed_pcd, registered_pcd, title="Registration Result"):
|
||||
"""
|
||||
可视化原始点云、变换后点云和配准后点云
|
||||
|
||||
Args:
|
||||
original_pcd: 原始点云(源)
|
||||
transformed_pcd: 应用随机变换后的点云(目标)
|
||||
registered_pcd: 配准后的点云
|
||||
title: 窗口标题
|
||||
"""
|
||||
# 复制并着色
|
||||
source_colored = copy.deepcopy(original_pcd)
|
||||
target_colored = copy.deepcopy(transformed_pcd)
|
||||
registered_colored = copy.deepcopy(registered_pcd)
|
||||
|
||||
# 设置颜色
|
||||
source_colored.paint_uniform_color([0, 0, 1]) # 蓝色 - 原始点云
|
||||
target_colored.paint_uniform_color([0, 1, 0]) # 绿色 - 目标点云
|
||||
registered_colored.paint_uniform_color([1, 0, 0]) # 红色 - 配准后点云
|
||||
|
||||
# 创建坐标系(可选)
|
||||
# coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.5)
|
||||
|
||||
# 可视化
|
||||
o3d.visualization.draw_geometries(
|
||||
[source_colored, target_colored],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[registered_colored, target_colored],
|
||||
window_name="After Registration (Red: Registered, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
def visualize_registration_comparison(original_pcd, transformed_pcd, registered_pcd):
|
||||
"""
|
||||
并排可视化配准前后的对比
|
||||
|
||||
Args:
|
||||
original_pcd: 原始点云
|
||||
transformed_pcd: 变换后的点云(目标)
|
||||
registered_pcd: 配准后的点云
|
||||
"""
|
||||
# 复制并着色
|
||||
source_before = copy.deepcopy(original_pcd)
|
||||
source_before.paint_uniform_color([0, 0, 1]) # 蓝色
|
||||
|
||||
target_before = copy.deepcopy(transformed_pcd)
|
||||
target_before.paint_uniform_color([0, 1, 0]) # 绿色
|
||||
|
||||
registered_after = copy.deepcopy(registered_pcd)
|
||||
registered_after.paint_uniform_color([1, 0, 0]) # 红色
|
||||
|
||||
target_after = copy.deepcopy(transformed_pcd)
|
||||
target_after.paint_uniform_color([0, 1, 0]) # 绿色
|
||||
|
||||
# 创建坐标系
|
||||
coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.5)
|
||||
|
||||
# 方法1:分别显示两个窗口
|
||||
o3d.visualization.draw_geometries(
|
||||
[source_before],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
o3d.visualization.draw_geometries(
|
||||
[target_before],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
o3d.visualization.draw_geometries(
|
||||
[registered_after, source_before],
|
||||
window_name="After Registration (Red: Registered, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def compute_point_to_mesh_distance(point_cloud, mesh):
|
||||
"""
|
||||
计算点云到 Mesh 的距离
|
||||
|
||||
Args:
|
||||
point_cloud: Open3D 点云对象
|
||||
mesh: Open3D 网格对象
|
||||
|
||||
Returns:
|
||||
distances: 每个点到最近三角形面的距离
|
||||
mean_distance: 平均距离
|
||||
rmse: 均方根误差
|
||||
max_distance: 最大距离
|
||||
"""
|
||||
# 计算点到 mesh 的距离
|
||||
distances = mesh.compute_point_cloud_distance(point_cloud)
|
||||
distances = np.asarray(distances)
|
||||
|
||||
mean_dist = np.mean(distances)
|
||||
rmse = np.sqrt(np.mean(distances ** 2))
|
||||
max_dist = np.max(distances)
|
||||
std_dist = np.std(distances)
|
||||
|
||||
return distances, mean_dist, rmse, max_dist, std_dist
|
||||
|
||||
|
||||
# 使用示例
|
||||
def demo(pcd,mesh):
|
||||
# 加载点云和 mesh
|
||||
# 计算距离
|
||||
distances, mean_dist, rmse, max_dist, std_dist = compute_point_to_mesh_distance(pcd, mesh)
|
||||
|
||||
print(f"平均距离: {mean_dist:.6f} m ({mean_dist * 1000:.3f} mm)")
|
||||
print(f"RMSE: {rmse:.6f} m ({rmse * 1000:.3f} mm)")
|
||||
print(f"最大距离: {max_dist:.6f} m ({max_dist * 1000:.3f} mm)")
|
||||
print(f"标准差: {std_dist:.6f} m ({std_dist * 1000:.3f} mm)")
|
||||
|
||||
# 可视化(按距离着色)
|
||||
pcd.colors = o3d.utility.Vector3dVector(get_color_by_distance(distances))
|
||||
o3d.visualization.draw_geometries([pcd, mesh])
|
||||
|
||||
|
||||
def get_color_by_distance(distances, max_dist=None):
|
||||
"""
|
||||
根据距离生成颜色(红:远,蓝:近)
|
||||
"""
|
||||
if max_dist is None:
|
||||
max_dist = np.percentile(distances, 95) # 使用95分位数
|
||||
|
||||
colors = np.zeros((len(distances), 3))
|
||||
norm_dist = np.clip(distances / (max_dist + 1e-8), 0, 1)
|
||||
|
||||
# 红色通道(距离越远越红)
|
||||
colors[:, 0] = norm_dist
|
||||
# 蓝色通道(距离越近越蓝)
|
||||
colors[:, 2] = 1 - norm_dist
|
||||
|
||||
return colors
|
||||
# ========== 主程序 ==========
|
||||
if __name__ == "__main__":
|
||||
# 1. 读取原始点云
|
||||
file_path = "E:\point_distance\data\\40_noisy.ply"
|
||||
|
||||
source_pcd = o3d.io.read_point_cloud(file_path)
|
||||
|
||||
# points=np.asarray(source_pcd.points)
|
||||
|
||||
t0=time.time()
|
||||
if len(source_pcd.points) == 0:
|
||||
print("读取失败,请检查文件路径")
|
||||
exit()
|
||||
|
||||
print(f"原始点云点数: {len(source_pcd.points)}")
|
||||
|
||||
# 2. 生成随机变换
|
||||
# R = random_rotation_matrix()
|
||||
R = rotation_matrix_scipy(10, 10, 10)
|
||||
t = random_translation_vector(range_xyz=0)
|
||||
|
||||
print(f"随机旋转矩阵:\n{R}")
|
||||
print(f"随机平移向量: {t}")
|
||||
|
||||
# 3. 创建目标点云(变换后的源点云)
|
||||
target_pcd = transform_point_cloud(source_pcd, R, t)
|
||||
|
||||
# 4. 点云配准
|
||||
print("\n开始点云配准...")
|
||||
transformation, fitness, rmse = registration_open3d(source_pcd, target_pcd, voxel_size=10)
|
||||
|
||||
# 5. 应用配准变换
|
||||
# registered_pcd = copy.deepcopy(source_pcd)
|
||||
# registered_pcd.transform(transformation)
|
||||
inv_transformation = np.linalg.inv(transformation)
|
||||
# 将目标点云变换回源点云坐标系
|
||||
target_recovered = copy.deepcopy(target_pcd)
|
||||
target_recovered.transform(inv_transformation)
|
||||
|
||||
|
||||
t1 = time.time()
|
||||
print(f"\n配准结果:")
|
||||
print(f" Fitness: {fitness:.4f}")
|
||||
print(f" RMSE: {rmse:.6f} m")
|
||||
print(f" 估计的变换矩阵:\n{transformation}")
|
||||
print(f"配准time:\n{t1-t0}")
|
||||
|
||||
|
||||
t2=time.time()
|
||||
# 1. 读取数据
|
||||
|
||||
points = np.asarray(target_recovered.points)
|
||||
mesh_trimesh = trimesh.load("E:\point_distance\data\\40.stl")
|
||||
# ========== 1. 读取数据 ==========
|
||||
# 2. 使用 trimesh 计算每个点到 Mesh 的最近距离
|
||||
# trimesh 的 nearest 方法非常高效且精确
|
||||
closest_points, distances, triangle_ids = mesh_trimesh.nearest.on_surface(points)
|
||||
t3 = time.time()
|
||||
|
||||
# distances 即为所求的距离数组
|
||||
print(f"平均距离: {np.mean(distances)}")
|
||||
print(f"最大距离: {np.max(distances)}")
|
||||
print(f" 距离计算: {t3 - t2:.4f} 秒")
|
||||
# 如果需要将结果存回 Open3D 点云进行可视化
|
||||
|
||||
|
||||
# 6. 可视化对比
|
||||
print("\n开始可视化...")
|
||||
visualize_registration_comparison(source_pcd, target_pcd,target_recovered)
|
||||
94
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/PF.py
Normal file
94
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/PF.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
|
||||
def compute_keep_ratio_and_density(pcd, filtered_pcd, sample_size=1000):
|
||||
"""
|
||||
计算直通滤波的保留率和密度变化
|
||||
|
||||
Args:
|
||||
pcd: 原始点云
|
||||
filtered_pcd: 滤波后点云
|
||||
sample_size: 密度计算采样点数(加速计算)
|
||||
|
||||
Returns:
|
||||
keep_ratio: 保留率 (%)
|
||||
density_change: 密度变化 (%)
|
||||
"""
|
||||
n_orig = len(pcd.points)
|
||||
n_filt = len(filtered_pcd.points)
|
||||
|
||||
# 1. 保留率
|
||||
keep_ratio = n_filt / n_orig * 100
|
||||
|
||||
# 2. 密度变化(基于平均最近邻距离)
|
||||
points_orig = np.asarray(pcd.points)
|
||||
points_filt = np.asarray(filtered_pcd.points)
|
||||
|
||||
# 采样(点数太多时加速)
|
||||
if len(points_orig) > sample_size:
|
||||
idx_orig = np.random.choice(len(points_orig), sample_size, replace=False)
|
||||
points_orig_sample = points_orig[idx_orig]
|
||||
else:
|
||||
points_orig_sample = points_orig
|
||||
|
||||
if len(points_filt) > sample_size:
|
||||
idx_filt = np.random.choice(len(points_filt), sample_size, replace=False)
|
||||
points_filt_sample = points_filt[idx_filt]
|
||||
else:
|
||||
points_filt_sample = points_filt
|
||||
|
||||
# 计算平均最近邻距离(密度指标)
|
||||
if len(points_orig_sample) > 5:
|
||||
tree_orig = KDTree(points_orig_sample)
|
||||
dist_orig, _ = tree_orig.query(points_orig_sample, k=2) # k=2 排除自身
|
||||
avg_dist_orig = np.mean(dist_orig[:, 1])
|
||||
else:
|
||||
avg_dist_orig = 0
|
||||
|
||||
if len(points_filt_sample) > 5:
|
||||
tree_filt = KDTree(points_filt_sample)
|
||||
dist_filt, _ = tree_filt.query(points_filt_sample, k=2)
|
||||
avg_dist_filt = np.mean(dist_filt[:, 1])
|
||||
else:
|
||||
avg_dist_filt = 0
|
||||
|
||||
# 密度变化(距离增大 = 密度减小)
|
||||
if avg_dist_orig > 0:
|
||||
density_change = (avg_dist_filt - avg_dist_orig) / avg_dist_orig * 100
|
||||
else:
|
||||
density_change = 0
|
||||
|
||||
return keep_ratio, density_change
|
||||
|
||||
|
||||
# ========== 使用示例 ==========
|
||||
if __name__ == "__main__":
|
||||
# 读取点云
|
||||
pcd = o3d.io.read_point_cloud(r"E:\point_distance\data\part_car_head.ply")
|
||||
|
||||
# 直通滤波(Z轴)
|
||||
points = np.asarray(pcd.points)
|
||||
mask = (points[:, 2] >= 0.0) & (points[:, 2] <= 2.0)
|
||||
filtered_points = points[mask]
|
||||
|
||||
filtered_pcd = o3d.geometry.PointCloud()
|
||||
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
|
||||
|
||||
# 计算指标
|
||||
keep_ratio, density_change = compute_keep_ratio_and_density(pcd, filtered_pcd)
|
||||
|
||||
print("=" * 40)
|
||||
print("直通滤波指标")
|
||||
print("=" * 40)
|
||||
print(f"保留率: {keep_ratio:.2f}%")
|
||||
print(f"密度变化: {density_change:+.2f}%")
|
||||
|
||||
# 解释
|
||||
if density_change > 0:
|
||||
print(f" → 点云变稀疏了 {density_change:.2f}%")
|
||||
elif density_change < 0:
|
||||
print(f" → 点云变密集了 {abs(density_change):.2f}%")
|
||||
else:
|
||||
print(f" → 密度基本不变")
|
||||
380
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RANSC.py
Normal file
380
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RANSC.py
Normal file
@@ -0,0 +1,380 @@
|
||||
# import numpy as np
|
||||
# import open3d as o3d
|
||||
# import random
|
||||
#
|
||||
#
|
||||
# def ransac_plane(points, threshold=0.01, max_iterations=1000, min_inliers=100):
|
||||
# """
|
||||
# RANSAC 平面拟合(单个)
|
||||
# """
|
||||
# best_inliers = []
|
||||
# best_plane = None
|
||||
# n_points = len(points)
|
||||
#
|
||||
# if n_points < 3:
|
||||
# return None, []
|
||||
#
|
||||
# for _ in range(max_iterations):
|
||||
# sample_idx = random.sample(range(n_points), 3)
|
||||
# p1, p2, p3 = points[sample_idx]
|
||||
#
|
||||
# v1 = p2 - p1
|
||||
# v2 = p3 - p1
|
||||
# normal = np.cross(v1, v2)
|
||||
# norm = np.linalg.norm(normal)
|
||||
# if norm < 1e-8:
|
||||
# continue
|
||||
# normal = normal / norm
|
||||
# a, b, c = normal
|
||||
# d = -np.dot(normal, p1)
|
||||
#
|
||||
# distances = np.abs(np.dot(points, normal) + d)
|
||||
# inliers = np.where(distances < threshold)[0]
|
||||
#
|
||||
# if len(inliers) > len(best_inliers) and len(inliers) >= min_inliers:
|
||||
# best_inliers = inliers
|
||||
# best_plane = (a, b, c, d)
|
||||
#
|
||||
# if len(best_inliers) > 0.9 * n_points:
|
||||
# break
|
||||
#
|
||||
# # 用内点重新拟合
|
||||
# if best_inliers is not None and len(best_inliers) > 3:
|
||||
# inlier_points = points[best_inliers]
|
||||
# centroid = np.mean(inlier_points, axis=0)
|
||||
# _, _, vh = np.linalg.svd(inlier_points - centroid)
|
||||
# normal = vh[2]
|
||||
# normal = normal / np.linalg.norm(normal)
|
||||
# a, b, c = normal
|
||||
# d = -np.dot(normal, centroid)
|
||||
# best_plane = (a, b, c, d)
|
||||
#
|
||||
# return best_plane, best_inliers
|
||||
#
|
||||
#
|
||||
# def sequential_ransac_planes(points, threshold=0.01, max_iterations=1000, min_inliers=100, max_planes=5):
|
||||
# """
|
||||
# 迭代 RANSAC 拟合多个平面
|
||||
# """
|
||||
# remaining_points = points.copy()
|
||||
# remaining_indices = np.arange(len(points))
|
||||
#
|
||||
# planes = []
|
||||
# inliers_list = []
|
||||
#
|
||||
# for i in range(max_planes):
|
||||
# if len(remaining_points) < min_inliers:
|
||||
# break
|
||||
#
|
||||
# plane, inliers = ransac_plane(remaining_points, threshold, max_iterations, min_inliers)
|
||||
#
|
||||
# if plane is None or len(inliers) < min_inliers:
|
||||
# break
|
||||
#
|
||||
# planes.append(plane)
|
||||
# original_indices = remaining_indices[inliers]
|
||||
# inliers_list.append(original_indices)
|
||||
#
|
||||
# # 移除内点
|
||||
# mask = np.ones(len(remaining_points), dtype=bool)
|
||||
# mask[inliers] = False
|
||||
# remaining_points = remaining_points[mask]
|
||||
# remaining_indices = remaining_indices[mask]
|
||||
#
|
||||
# print(f"平面 {len(planes)}: {len(inliers)} 个内点, 剩余 {len(remaining_points)} 个点")
|
||||
#
|
||||
# return planes, inliers_list
|
||||
#
|
||||
#
|
||||
# def create_plane_mesh(plane, bounds):
|
||||
# """
|
||||
# 创建平面网格用于可视化
|
||||
# """
|
||||
# a, b, c, d = plane
|
||||
# x_min, x_max = bounds[0]
|
||||
# y_min, y_max = bounds[1]
|
||||
#
|
||||
# x = np.linspace(x_min, x_max, 50)
|
||||
# y = np.linspace(y_min, y_max, 50)
|
||||
# X, Y = np.meshgrid(x, y)
|
||||
#
|
||||
# if abs(c) > 1e-8:
|
||||
# Z = -(a * X + b * Y + d) / c
|
||||
# else:
|
||||
# Z = np.zeros_like(X)
|
||||
#
|
||||
# vertices = []
|
||||
# triangles = []
|
||||
# rows, cols = X.shape
|
||||
#
|
||||
# for i in range(rows):
|
||||
# for j in range(cols):
|
||||
# vertices.append([X[i, j], Y[i, j], Z[i, j]])
|
||||
#
|
||||
# for i in range(rows - 1):
|
||||
# for j in range(cols - 1):
|
||||
# idx = i * cols + j
|
||||
# triangles.append([idx, idx + 1, idx + cols])
|
||||
# triangles.append([idx + 1, idx + cols + 1, idx + cols])
|
||||
#
|
||||
# mesh = o3d.geometry.TriangleMesh()
|
||||
# mesh.vertices = o3d.utility.Vector3dVector(np.array(vertices))
|
||||
# mesh.triangles = o3d.utility.Vector3iVector(np.array(triangles))
|
||||
# mesh.compute_vertex_normals()
|
||||
#
|
||||
# return mesh
|
||||
#
|
||||
#
|
||||
# def visualize_multiple_planes(points, planes, inliers_list):
|
||||
# """
|
||||
# 可视化多个平面(点云 + 平面网格)
|
||||
#
|
||||
# Args:
|
||||
# points: (N, 3) 点云坐标
|
||||
# planes: 平面方程列表 [(a,b,c,d), ...]
|
||||
# inliers_list: 每个平面对应的内点索引列表
|
||||
# """
|
||||
# # 颜色列表(每个平面不同颜色)
|
||||
# colors = [
|
||||
# [1, 0, 0], # 红色
|
||||
# [0, 1, 0], # 绿色
|
||||
# [0, 0, 1], # 蓝色
|
||||
# [1, 1, 0], # 黄色
|
||||
# [1, 0, 1], # 品红
|
||||
# [0, 1, 1], # 青色
|
||||
# [1, 0.5, 0], # 橙色
|
||||
# [0.5, 0, 1], # 紫色
|
||||
# ]
|
||||
#
|
||||
# geometries = []
|
||||
#
|
||||
# # 1. 为每个平面的内点着色
|
||||
# point_colors = np.zeros((len(points), 3))
|
||||
# for i, inliers in enumerate(inliers_list):
|
||||
# color = colors[i % len(colors)]
|
||||
# point_colors[inliers] = color
|
||||
#
|
||||
# # 未分配的点(剩余点)设为灰色
|
||||
# unassigned = np.where(np.sum(point_colors, axis=1) == 0)[0]
|
||||
# point_colors[unassigned] = [0.5, 0.5, 0.5]
|
||||
#
|
||||
# colored_pcd = o3d.geometry.PointCloud()
|
||||
# colored_pcd.points = o3d.utility.Vector3dVector(points)
|
||||
# colored_pcd.colors = o3d.utility.Vector3dVector(point_colors)
|
||||
# geometries.append(colored_pcd)
|
||||
#
|
||||
# # 2. 为每个平面创建网格
|
||||
# for i, (plane, inliers) in enumerate(zip(planes, inliers_list)):
|
||||
# # 计算该平面对应点的范围
|
||||
# inlier_points = points[inliers]
|
||||
# if len(inlier_points) > 0:
|
||||
# x_min, x_max = inlier_points[:, 0].min(), inlier_points[:, 0].max()
|
||||
# y_min, y_max = inlier_points[:, 1].min(), inlier_points[:, 1].max()
|
||||
#
|
||||
# # 稍微扩展边界
|
||||
# x_range = x_max - x_min
|
||||
# y_range = y_max - y_min
|
||||
# x_min -= x_range * 0.1
|
||||
# x_max += x_range * 0.1
|
||||
# y_min -= y_range * 0.1
|
||||
# y_max += y_range * 0.1
|
||||
#
|
||||
# mesh = create_plane_mesh(plane, [[x_min, x_max], [y_min, y_max]])
|
||||
# mesh.paint_uniform_color(colors[i % len(colors)])
|
||||
# # 设置半透明
|
||||
# mesh.paint_uniform_color([colors[i % len(colors)][0],
|
||||
# colors[i % len(colors)][1],
|
||||
# colors[i % len(colors)][2]])
|
||||
# geometries.append(mesh)
|
||||
#
|
||||
# # 可视化
|
||||
# o3d.visualization.draw_geometries(
|
||||
# geometries,
|
||||
# window_name="Multiple Planes - RANSAC",
|
||||
# width=1024,
|
||||
# height=768
|
||||
# )
|
||||
#
|
||||
#
|
||||
# def visualize_single_plane(points, plane, inliers):
|
||||
# """
|
||||
# 可视化单个平面(兼容单平面情况)
|
||||
# """
|
||||
# visualize_multiple_planes(points, [plane], [inliers])
|
||||
#
|
||||
#
|
||||
# # ========== 主程序 ==========
|
||||
# if __name__ == "__main__":
|
||||
# # 读取点云
|
||||
# file_path = r"E:\point_distance\data\part_car_head.ply"
|
||||
# pcd = o3d.io.read_point_cloud(file_path)
|
||||
#
|
||||
# if len(pcd.points) == 0:
|
||||
# print("读取失败,请检查文件路径")
|
||||
# exit()
|
||||
#
|
||||
# points = np.asarray(pcd.points)
|
||||
# print(f"原始点云点数: {len(points)}")
|
||||
#
|
||||
# # 计算点云范围
|
||||
# points_range = points.max(axis=0) - points.min(axis=0)
|
||||
# print(f"点云范围: X[{points_range[0]:.3f}], Y[{points_range[1]:.3f}], Z[{points_range[2]:.3f}]")
|
||||
#
|
||||
# # 设置阈值
|
||||
# threshold = points_range[2] * 0.02
|
||||
# print(f"使用阈值: {threshold:.4f}")
|
||||
#
|
||||
# # ========== 单平面拟合 ==========
|
||||
# # print("\n" + "=" * 50)
|
||||
# # print("单平面拟合")
|
||||
# # print("=" * 50)
|
||||
# #
|
||||
# # plane, inliers = ransac_plane(points, threshold=threshold, max_iterations=1, min_inliers=100)
|
||||
# #
|
||||
# # if plane is not None:
|
||||
# # a, b, c, d = plane
|
||||
# # print(f"拟合平面: {a:.4f}x + {b:.4f}y + {c:.4f}z + {d:.4f} = 0")
|
||||
# # print(f"内点数量: {len(inliers)} / {len(points)} ({len(inliers) / len(points) * 100:.1f}%)")
|
||||
# # visualize_single_plane(points, plane, inliers)
|
||||
#
|
||||
# # ========== 多平面拟合 ==========
|
||||
# print("\n" + "=" * 50)
|
||||
# print("多平面拟合")
|
||||
# print("=" * 50)
|
||||
#
|
||||
# planes, inliers_list = sequential_ransac_planes(
|
||||
# points,
|
||||
# threshold=threshold,
|
||||
# max_iterations=1,
|
||||
# min_inliers=100,
|
||||
# max_planes=1
|
||||
# )
|
||||
#
|
||||
# print(f"共提取 {len(planes)} 个平面")
|
||||
# for i, (plane, inliers) in enumerate(zip(planes, inliers_list)):
|
||||
# a, b, c, d = plane
|
||||
# print(f"平面 {i + 1}: {a:.4f}x + {b:.4f}y + {c:.4f}z + {d:.4f} = 0, 内点数: {len(inliers)}")
|
||||
#
|
||||
# if len(planes) > 0:
|
||||
# visualize_multiple_planes(points, planes, inliers_list)
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import random
|
||||
|
||||
|
||||
def create_square_plane_with_noise(size_mm=1000, noise_std_mm=0.5, num_points=10000):
|
||||
"""创建正方形平面点云并添加高斯噪声"""
|
||||
size_m = size_mm / 1000.0
|
||||
noise_std_m = noise_std_mm / 1000.0
|
||||
|
||||
half_size = size_m / 2
|
||||
x = np.random.uniform(-half_size, half_size, num_points)
|
||||
y = np.random.uniform(-half_size, half_size, num_points)
|
||||
z = np.zeros(num_points)
|
||||
|
||||
points = np.column_stack([x, y, z])
|
||||
noise_z = np.random.normal(0, noise_std_m, num_points)
|
||||
points[:, 2] += noise_z
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def estimate_plane_ransac_open3d(points, threshold_mm=0.5, max_iterations=2000):
|
||||
"""使用 Open3D 内置 RANSAC 平面估计"""
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points)
|
||||
|
||||
threshold_m = threshold_mm / 1000.0
|
||||
|
||||
plane_model, inliers = pcd.segment_plane(
|
||||
distance_threshold=threshold_m,
|
||||
ransac_n=3,
|
||||
num_iterations=max_iterations
|
||||
)
|
||||
|
||||
a, b, c, d = plane_model
|
||||
|
||||
# 计算误差
|
||||
ideal_normal = np.array([0, 0, 1])
|
||||
estimated_normal = np.array([a, b, c])
|
||||
cos_angle = np.dot(ideal_normal, estimated_normal)
|
||||
cos_angle = np.clip(cos_angle, -1, 1)
|
||||
angle_error = np.degrees(np.arccos(cos_angle))
|
||||
offset_error = abs(d)
|
||||
|
||||
inlier_points = points[inliers]
|
||||
distances = np.abs(np.dot(inlier_points, estimated_normal) + d)
|
||||
rmse = np.sqrt(np.mean(distances ** 2))
|
||||
|
||||
print(f"\nRANSAC 平面拟合结果 (Open3D):")
|
||||
print(f" 估计平面: {a:.6f}x + {b:.6f}y + {c:.6f}z + {d:.6f} = 0")
|
||||
print(f" 内点数量: {len(inliers)} / {len(points)} ({len(inliers) / len(points) * 100:.1f}%)")
|
||||
print(f" 法线角度误差: {angle_error:.4f} 度")
|
||||
print(f" 偏移误差: {offset_error:.6f} m ({offset_error * 1000:.3f} mm)")
|
||||
print(f" 平面拟合 RMSE: {rmse:.6f} m ({rmse * 1000:.3f} mm)")
|
||||
|
||||
return plane_model, inliers
|
||||
|
||||
|
||||
def visualize_plane_fitting(points, plane):
|
||||
"""可视化原始点云和拟合平面"""
|
||||
# 原始点云
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points)
|
||||
pcd.paint_uniform_color([0.6, 0.8, 1.0])
|
||||
|
||||
# 创建平面点云
|
||||
a, b, c, d = plane
|
||||
x_min, x_max = points[:, 0].min(), points[:, 0].max()
|
||||
y_min, y_max = points[:, 1].min(), points[:, 1].max()
|
||||
|
||||
x = np.linspace(x_min, x_max, 50)
|
||||
y = np.linspace(y_min, y_max, 50)
|
||||
X, Y = np.meshgrid(x, y)
|
||||
|
||||
if abs(c) > 1e-8:
|
||||
Z = -(a * X + b * Y + d) / c
|
||||
else:
|
||||
Z = np.zeros_like(X)
|
||||
|
||||
plane_points = np.column_stack([X.ravel(), Y.ravel(), Z.ravel()])
|
||||
|
||||
pcd_plane = o3d.geometry.PointCloud()
|
||||
pcd_plane.points = o3d.utility.Vector3dVector(plane_points)
|
||||
pcd_plane.paint_uniform_color([1, 0, 0]) # 红色
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[pcd, pcd_plane],
|
||||
window_name="Plane Fitting",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
# ========== 主程序 ==========
|
||||
if __name__ == "__main__":
|
||||
# 参数设置
|
||||
size_mm = 1000
|
||||
noise_std_mm = 0.5
|
||||
num_points = 100000
|
||||
|
||||
print(f"参数:")
|
||||
print(f" 正方形边长: {size_mm} mm")
|
||||
print(f" 噪声标准差: {noise_std_mm} mm")
|
||||
print(f" 点数: {num_points}")
|
||||
|
||||
# 生成点云
|
||||
points = create_square_plane_with_noise(size_mm, noise_std_mm, num_points)
|
||||
|
||||
# 检查数据范围
|
||||
print(f"\n数据范围:")
|
||||
print(f" X: [{points[:, 0].min():.3f}, {points[:, 0].max():.3f}]")
|
||||
print(f" Y: [{points[:, 1].min():.3f}, {points[:, 1].max():.3f}]")
|
||||
print(f" Z: [{points[:, 2].min():.6f}, {points[:, 2].max():.6f}]")
|
||||
|
||||
# 使用 Open3D RANSAC
|
||||
plane, inliers = estimate_plane_ransac_open3d(points, threshold_mm=0.5, max_iterations=2000)
|
||||
|
||||
# 可视化
|
||||
visualize_plane_fitting(points, plane)
|
||||
147
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RG.py
Normal file
147
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RG.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
from sklearn.neighbors import NearestNeighbors
|
||||
|
||||
|
||||
def estimate_normals(points, k=20):
|
||||
"""用 PCA 估计法线"""
|
||||
n_points = len(points)
|
||||
normals = np.zeros_like(points)
|
||||
|
||||
nbrs = NearestNeighbors(n_neighbors=k).fit(points)
|
||||
distances, indices = nbrs.kneighbors(points)
|
||||
|
||||
for i in range(n_points):
|
||||
neighbors = points[indices[i]]
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
cov = np.cov((neighbors - centroid).T)
|
||||
eig_vals, eig_vecs = np.linalg.eig(cov)
|
||||
normal = eig_vecs[:, np.argmin(eig_vals)]
|
||||
# 统一法线方向
|
||||
if normal[2] < 0:
|
||||
normal = -normal
|
||||
normals[i] = normal
|
||||
|
||||
return normals
|
||||
|
||||
|
||||
def compute_curvature(points, normals, k=20):
|
||||
"""计算曲率(特征值比值)"""
|
||||
curvature = np.zeros(len(points))
|
||||
nbrs = NearestNeighbors(n_neighbors=k).fit(points)
|
||||
distances, indices = nbrs.kneighbors(points)
|
||||
|
||||
for i in range(len(points)):
|
||||
neighbors = points[indices[i]]
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
cov = np.cov((neighbors - centroid).T)
|
||||
eig_vals, _ = np.linalg.eig(cov)
|
||||
curvature[i] = np.min(eig_vals) / (np.sum(eig_vals) + 1e-8)
|
||||
|
||||
return curvature
|
||||
|
||||
|
||||
def region_growing(points, normals, curvature,
|
||||
angle_threshold=30.0, # 法线夹角阈值(度)
|
||||
curvature_threshold=0.05, # 曲率阈值
|
||||
min_cluster_size=100):
|
||||
"""
|
||||
区域生长算法
|
||||
Args:
|
||||
points: (N, 3) 点云坐标
|
||||
normals: (N, 3) 法线
|
||||
curvature: (N,) 曲率
|
||||
angle_threshold: 法线夹角阈值(度)
|
||||
curvature_threshold: 曲率阈值
|
||||
min_cluster_size: 最小簇大小
|
||||
Returns:
|
||||
labels: (N,) 每个点的簇标签(-1 表示未分类/噪声)
|
||||
"""
|
||||
n_points = len(points)
|
||||
labels = np.full(n_points, -1, dtype=int)
|
||||
|
||||
# 按曲率升序排序(曲率小的作为种子)
|
||||
sorted_indices = np.argsort(curvature)
|
||||
|
||||
# 构建 KD-Tree 用于快速邻域搜索
|
||||
tree = KDTree(points)
|
||||
|
||||
label_id = 0
|
||||
angle_threshold_rad = np.radians(angle_threshold)
|
||||
cos_threshold = np.cos(angle_threshold_rad)
|
||||
|
||||
for seed_idx in sorted_indices:
|
||||
if labels[seed_idx] != -1:
|
||||
continue
|
||||
|
||||
# 开始一个新区域
|
||||
current_region = []
|
||||
seeds = [seed_idx]
|
||||
labels[seed_idx] = label_id
|
||||
|
||||
while seeds:
|
||||
current_seed = seeds.pop(0)
|
||||
current_region.append(current_seed)
|
||||
|
||||
# 搜索半径内的邻居(动态半径或固定半径)
|
||||
neighbors = tree.query_ball_point(points[current_seed], r=0.2)
|
||||
|
||||
for neighbor in neighbors:
|
||||
if labels[neighbor] != -1:
|
||||
continue
|
||||
|
||||
# 检查法线夹角
|
||||
dot_product = np.abs(np.dot(normals[current_seed], normals[neighbor]))
|
||||
if dot_product > cos_threshold:
|
||||
labels[neighbor] = label_id
|
||||
|
||||
# 如果曲率足够小,作为新种子
|
||||
if curvature[neighbor] < curvature_threshold:
|
||||
seeds.append(neighbor)
|
||||
|
||||
# 检查区域大小
|
||||
region_size = np.sum(labels == label_id)
|
||||
if region_size < min_cluster_size:
|
||||
# 区域太小,标记为噪声
|
||||
labels[labels == label_id] = -1
|
||||
else:
|
||||
label_id += 1
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
import open3d as o3d
|
||||
|
||||
# 读取点云
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\\bell_pepper_6.pcd")
|
||||
points = np.asarray(pcd.points)
|
||||
|
||||
# 预处理:体素下采样 + 去噪
|
||||
pcd = pcd.voxel_down_sample(0.0002)
|
||||
pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
|
||||
points = np.asarray(pcd.points)
|
||||
|
||||
# 计算法线和曲率
|
||||
print("计算法线...")
|
||||
normals = estimate_normals(points, k=30)
|
||||
|
||||
print("计算曲率...")
|
||||
curvature = compute_curvature(points, normals, k=30)
|
||||
|
||||
# 区域生长分割
|
||||
print("区域生长...")
|
||||
labels = region_growing(points, normals, curvature,
|
||||
angle_threshold=25.0,
|
||||
curvature_threshold=0.05,
|
||||
min_cluster_size=100)
|
||||
|
||||
print(f"分割完成,共 {len(np.unique(labels)) - (1 if -1 in labels else 0)} 个区域")
|
||||
|
||||
# 可视化
|
||||
colors = np.random.rand(len(np.unique(labels)), 3)
|
||||
point_colors = np.array([colors[label] if label != -1 else [0.5, 0.5, 0.5]
|
||||
for label in labels])
|
||||
pcd.colors = o3d.utility.Vector3dVector(point_colors)
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
52
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/ROR.py
Normal file
52
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/ROR.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
def chamfer_distance(pcd1, pcd2):
|
||||
"""
|
||||
计算两个点云之间的 CD 距离(Chamfer Distance)
|
||||
|
||||
Args:
|
||||
pcd1: Open3D 点云对象1
|
||||
pcd2: Open3D 点云对象2
|
||||
|
||||
Returns:
|
||||
cd: Chamfer Distance 值
|
||||
cd1: 点云1 到 点云2 的平均距离
|
||||
cd2: 点云2 到 点云1 的平均距离
|
||||
"""
|
||||
# 获取点坐标
|
||||
points1 = np.asarray(pcd1.points)
|
||||
points2 = np.asarray(pcd2.points)
|
||||
|
||||
# 构建 KD-Tree 加速最近邻搜索
|
||||
tree1 = KDTree(points1)
|
||||
tree2 = KDTree(points2)
|
||||
|
||||
# 计算 points1 中每个点到 points2 的最近距离
|
||||
dist1, _ = tree2.query(points1, k=1)
|
||||
cd1 = np.mean(dist1 ** 2) # 平方距离的平均值
|
||||
|
||||
# 计算 points2 中每个点到 points1 的最近距离
|
||||
dist2, _ = tree1.query(points2, k=1)
|
||||
cd2 = np.mean(dist2 ** 2) # 平方距离的平均值
|
||||
|
||||
# 总 CD 距离(双向平均)
|
||||
cd = cd1 + cd2
|
||||
|
||||
return np.sqrt(cd), cd1, cd2
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_left_side.ply")
|
||||
points = np.asarray(pcd.points)
|
||||
colors = np.tile([0.8, 0.7, 1.0], (len(pcd.points), 1))
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
o3d.visualization.draw_geometries([pcd],window_name="1")
|
||||
# 半径离群点移除
|
||||
cl, ind = pcd.remove_radius_outlier(nb_points=5, radius=20)
|
||||
|
||||
# 提取内点
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
outlier_cloud = pcd.select_by_index(ind, invert=True)
|
||||
|
||||
print(f"原始点云: {len(pcd.points)} 点")
|
||||
print(f"滤波后: {len(inlier_cloud.points)} 点")
|
||||
cd,cd1,cd2=chamfer_distance(pcd1=pcd,pcd2=inlier_cloud)
|
||||
print(cd)
|
||||
60
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/SOR.py
Normal file
60
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/SOR.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
from plyfile import PlyData
|
||||
def chamfer_distance(pcd1, pcd2):
|
||||
"""
|
||||
计算两个点云之间的 CD 距离(Chamfer Distance)
|
||||
|
||||
Args:
|
||||
pcd1: Open3D 点云对象1
|
||||
pcd2: Open3D 点云对象2
|
||||
|
||||
Returns:
|
||||
cd: Chamfer Distance 值
|
||||
cd1: 点云1 到 点云2 的平均距离
|
||||
cd2: 点云2 到 点云1 的平均距离
|
||||
"""
|
||||
# 获取点坐标
|
||||
points1 = np.asarray(pcd1.points)
|
||||
points2 = np.asarray(pcd2.points)
|
||||
|
||||
# 构建 KD-Tree 加速最近邻搜索
|
||||
tree1 = KDTree(points1)
|
||||
tree2 = KDTree(points2)
|
||||
|
||||
# 计算 points1 中每个点到 points2 的最近距离
|
||||
dist1, _ = tree2.query(points1, k=1)
|
||||
cd1 = np.mean(dist1 ** 2) # 平方距离的平均值
|
||||
|
||||
# 计算 points2 中每个点到 points1 的最近距离
|
||||
dist2, _ = tree1.query(points2, k=1)
|
||||
cd2 = np.mean(dist2 ** 2) # 平方距离的平均值
|
||||
|
||||
# 总 CD 距离(双向平均)
|
||||
cd = cd1 + cd2
|
||||
|
||||
return cd, cd1, cd2
|
||||
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_head.ply")
|
||||
colors = np.tile([1, 0, 0], (len(pcd.points), 1))
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
# ply_data = PlyData.read(r"E:\point_distance\data\part_car_head.ply")
|
||||
# colors=np.asarray(ply_data.colors)
|
||||
# with open("E:\point_distance\data\part_car_head.ply", 'r',encoding="utf-8") as f:
|
||||
# lines = f.readlines()
|
||||
# for line in lines[:30]: # 打印头部
|
||||
# print(line.strip())
|
||||
# o3d.visualization.draw_geometries([pcd])
|
||||
# points = np.asarray(pcd.points)
|
||||
# colors = np.asarray(pcd.colors)
|
||||
# normals = np.asarray(pcd.normals)
|
||||
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=50.0)
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
colors=np.tile([0,1,0],(len(inlier_cloud.points),1))
|
||||
inlier_cloud.colors=o3d.utility.Vector3dVector(colors)
|
||||
o3d.visualization.draw_geometries([inlier_cloud])
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
o3d.visualization.draw_geometries()
|
||||
cd,cd1,cd2=chamfer_distance(pcd1=pcd,pcd2=inlier_cloud)
|
||||
print(cd)
|
||||
96
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/VGF.py
Normal file
96
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/VGF.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
|
||||
def evaluate_voxel_filter(pcd, leaf_size=0.05):
|
||||
"""
|
||||
评估体素滤波效果,计算各项指标
|
||||
|
||||
Args:
|
||||
pcd: 原始点云
|
||||
leaf_size: 体素边长
|
||||
|
||||
Returns:
|
||||
metrics: 指标字典
|
||||
filtered_pcd: 滤波后点云
|
||||
"""
|
||||
# 执行体素滤波
|
||||
filtered_pcd = pcd.voxel_down_sample(leaf_size)
|
||||
|
||||
n_orig = len(pcd.points)
|
||||
n_filt = len(filtered_pcd.points)
|
||||
|
||||
# 1. 基础统计指标
|
||||
keep_ratio = n_filt / n_orig * 100
|
||||
downsample_rate = (n_orig - n_filt) / n_orig * 100
|
||||
compression_ratio = n_orig / n_filt if n_filt > 0 else 0
|
||||
|
||||
# 2. 密度变化指标
|
||||
points_orig = np.asarray(pcd.points)
|
||||
points_filt = np.asarray(filtered_pcd.points)
|
||||
|
||||
# 计算平均最近邻距离(采样加速)
|
||||
sample_size = min(1000, n_filt)
|
||||
if n_filt > sample_size:
|
||||
idx = np.random.choice(n_filt, sample_size, replace=False)
|
||||
points_sample = points_filt[idx]
|
||||
else:
|
||||
points_sample = points_filt
|
||||
|
||||
if len(points_sample) > 5:
|
||||
tree = KDTree(points_sample)
|
||||
dist, _ = tree.query(points_sample, k=2)
|
||||
avg_distance = np.mean(dist[:, 1])
|
||||
else:
|
||||
avg_distance = 0
|
||||
|
||||
# 3. 输出指标
|
||||
metrics = {
|
||||
'leaf_size': leaf_size,
|
||||
'n_orig': n_orig,
|
||||
'n_filt': n_filt,
|
||||
'keep_ratio': keep_ratio,
|
||||
'downsample_rate': downsample_rate,
|
||||
'compression_ratio': compression_ratio,
|
||||
'avg_point_distance': avg_distance,
|
||||
'estimated_density': 1 / (avg_distance ** 3) if avg_distance > 0 else 0
|
||||
}
|
||||
|
||||
return metrics, filtered_pcd
|
||||
|
||||
|
||||
# ========== 使用示例 ==========
|
||||
if __name__ == "__main__":
|
||||
# 读取点云
|
||||
pcd = o3d.io.read_point_cloud(r"E:\point_distance\data\part_car_head.ply")
|
||||
|
||||
# 测试不同体素大小
|
||||
leaf_sizes = [2, 15, 20, 2]
|
||||
|
||||
print("=" * 60)
|
||||
print("体素滤波指标评估")
|
||||
print("=" * 60)
|
||||
print(f"{'体素边长':<10} {'保留率':<12} {'下采样率':<12} {'压缩比':<10} {'平均间距':<12}")
|
||||
print("-" * 60)
|
||||
|
||||
for leaf_size in leaf_sizes:
|
||||
metrics, filtered = evaluate_voxel_filter(pcd, leaf_size)
|
||||
print(f"{leaf_size:<10} {metrics['keep_ratio']:>8.2f}% "
|
||||
f"{metrics['downsample_rate']:>8.2f}% "
|
||||
f"{metrics['compression_ratio']:>8.1f}x "
|
||||
f"{metrics['avg_point_distance']:>8.4f}m")
|
||||
|
||||
# 可视化对比
|
||||
print("\n可视化对比...")
|
||||
orig_colored = o3d.geometry.PointCloud()
|
||||
orig_colored.points = pcd.points
|
||||
orig_colored.paint_uniform_color([0, 0, 1]) # 蓝色
|
||||
# o3d.visualization.draw_geometries([orig_colored],window_name=f"orignal (blue,{len(pcd.points)}")
|
||||
metrics, filtered = evaluate_voxel_filter(pcd, leaf_size=20)
|
||||
filtered.paint_uniform_color([1, 0, 0]) # 红色
|
||||
# o3d.visualization.draw_geometries([filtered], window_name=f"Voxel (red,{len(filtered.points)}")
|
||||
o3d.visualization.draw_geometries(
|
||||
[orig_colored, filtered],
|
||||
window_name=f"Original (Blue, {len(pcd.points)} pts) vs Voxel (Red, {len(filtered.points)} pts)"
|
||||
)
|
||||
141
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/distance_mesh.py
Normal file
141
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/distance_mesh.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
import copy
|
||||
|
||||
|
||||
def compute_point_to_mesh_distance(point_cloud, mesh):
|
||||
"""
|
||||
计算点云到 Mesh 的距离
|
||||
|
||||
Args:
|
||||
point_cloud: Open3D 点云对象
|
||||
mesh: Open3D 网格对象
|
||||
|
||||
Returns:
|
||||
distances: 每个点到最近三角形面的距离
|
||||
"""
|
||||
# 方法1:使用 Open3D 内置函数(新版本)
|
||||
try:
|
||||
distances = mesh.compute_point_cloud_distance(point_cloud)
|
||||
return np.asarray(distances)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# 方法2:使用 mesh 顶点最近邻(近似)
|
||||
mesh_points = np.asarray(mesh.vertices)
|
||||
points = np.asarray(point_cloud.points)
|
||||
|
||||
tree = KDTree(mesh_points)
|
||||
distances, indices = tree.query(points)
|
||||
|
||||
return distances
|
||||
|
||||
|
||||
def compute_statistics(distances):
|
||||
"""计算距离统计信息"""
|
||||
stats = {
|
||||
'mean': np.mean(distances),
|
||||
'rmse': np.sqrt(np.mean(distances ** 2)),
|
||||
'max': np.max(distances),
|
||||
'min': np.min(distances),
|
||||
'std': np.std(distances),
|
||||
'median': np.median(distances),
|
||||
}
|
||||
|
||||
# 分位数
|
||||
percentiles = [50, 75, 90, 95, 99]
|
||||
for p in percentiles:
|
||||
stats[f'percentile_{p}'] = np.percentile(distances, p)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def visualize_distance(point_cloud, mesh, distances, max_dist=None):
|
||||
"""可视化距离(按距离着色)"""
|
||||
pcd_colored = copy.deepcopy(point_cloud)
|
||||
|
||||
if max_dist is None:
|
||||
max_dist = np.percentile(distances, 95)
|
||||
|
||||
# 生成颜色(红:远,蓝:近)
|
||||
colors = np.zeros((len(distances), 3))
|
||||
norm_dist = np.clip(distances / (max_dist + 1e-8), 0, 1)
|
||||
colors[:, 0] = norm_dist # 红色通道(远)
|
||||
colors[:, 2] = 1 - norm_dist # 蓝色通道(近)
|
||||
|
||||
pcd_colored.colors = o3d.utility.Vector3dVector(colors)
|
||||
|
||||
# Mesh 着色为灰色
|
||||
mesh_colored = copy.deepcopy(mesh)
|
||||
mesh_colored.paint_uniform_color([0.5, 0.5, 0.5])
|
||||
|
||||
# 可视化
|
||||
o3d.visualization.draw_geometries(
|
||||
[mesh_colored, pcd_colored],
|
||||
window_name="Distance to Mesh (Blue=Near, Red=Far)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
def print_statistics(stats):
|
||||
"""打印统计信息"""
|
||||
print("=" * 60)
|
||||
print("点云到 Mesh 距离统计")
|
||||
print("=" * 60)
|
||||
print(f"平均距离: {stats['mean']:.6f} m ({stats['mean'] * 1000:.3f} mm)")
|
||||
print(f"RMSE: {stats['rmse']:.6f} m ({stats['rmse'] * 1000:.3f} mm)")
|
||||
print(f"中位数: {stats['median']:.6f} m ({stats['median'] * 1000:.3f} mm)")
|
||||
print(f"标准差: {stats['std']:.6f} m ({stats['std'] * 1000:.3f} mm)")
|
||||
print(f"最大距离: {stats['max']:.6f} m ({stats['max'] * 1000:.3f} mm)")
|
||||
print(f"最小距离: {stats['min']:.6f} m ({stats['min'] * 1000:.3f} mm)")
|
||||
print("\n距离分位数:")
|
||||
print(f" 50%: {stats['percentile_50']:.6f} m ({stats['percentile_50'] * 1000:.3f} mm)")
|
||||
print(f" 75%: {stats['percentile_75']:.6f} m ({stats['percentile_75'] * 1000:.3f} mm)")
|
||||
print(f" 90%: {stats['percentile_90']:.6f} m ({stats['percentile_90'] * 1000:.3f} mm)")
|
||||
print(f" 95%: {stats['percentile_95']:.6f} m ({stats['percentile_95'] * 1000:.3f} mm)")
|
||||
print(f" 99%: {stats['percentile_99']:.6f} m ({stats['percentile_99'] * 1000:.3f} mm)")
|
||||
|
||||
|
||||
# ========== 主程序 ==========
|
||||
if __name__ == "__main__":
|
||||
# 读取文件(使用原始字符串避免转义问题)
|
||||
file_path = r"E:\point_distance\data\40_noisy.ply"
|
||||
mesh_path = r"E:\point_distance\data\40.stl"
|
||||
|
||||
print("读取点云...")
|
||||
source_pcd = o3d.io.read_point_cloud(file_path)
|
||||
|
||||
print("读取 Mesh...")
|
||||
mesh = o3d.io.read_triangle_mesh(mesh_path)
|
||||
|
||||
# 检查读取是否成功
|
||||
if len(source_pcd.points) == 0:
|
||||
print("错误:点云读取失败,请检查文件路径")
|
||||
exit()
|
||||
|
||||
if len(mesh.vertices) == 0:
|
||||
print("错误:Mesh 读取失败,请检查文件路径")
|
||||
exit()
|
||||
|
||||
print(f"\n点云点数: {len(source_pcd.points)}")
|
||||
print(f"Mesh 顶点数: {len(mesh.vertices)}")
|
||||
print(f"Mesh 面数: {len(mesh.triangles)}")
|
||||
|
||||
# 可选:Mesh 法线计算(用于更好的可视化)
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
# 计算距离
|
||||
print("\n计算点云到 Mesh 的距离...")
|
||||
distances = compute_point_to_mesh_distance(source_pcd, mesh)
|
||||
|
||||
# 计算统计信息
|
||||
stats = compute_statistics(distances)
|
||||
|
||||
# 打印统计信息
|
||||
print_statistics(stats)
|
||||
|
||||
# 可视化
|
||||
print("\n可视化...")
|
||||
visualize_distance(source_pcd, mesh, distances)
|
||||
25
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/test5.py
Normal file
25
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/test5.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import trimesh
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import time
|
||||
# 1. 读取数据
|
||||
mesh_trimesh = trimesh.load("E:\point_distance\data\\40.stl")
|
||||
pcd_o3d = o3d.io.read_point_cloud("E:\point_distance\data\\40_noisy.ply")
|
||||
points = np.asarray(pcd_o3d.points)
|
||||
start_time = time.time()
|
||||
|
||||
# ========== 1. 读取数据 ==========
|
||||
|
||||
t0 = time.time()
|
||||
# 2. 使用 trimesh 计算每个点到 Mesh 的最近距离
|
||||
# trimesh 的 nearest 方法非常高效且精确
|
||||
closest_points, distances, triangle_ids = mesh_trimesh.nearest.on_surface(points)
|
||||
t1=time.time()
|
||||
|
||||
# distances 即为所求的距离数组
|
||||
print(f"平均距离: {np.mean(distances)}")
|
||||
print(f"最大距离: {np.max(distances)}")
|
||||
print(f" 距离计算: {t1 - t0:.4f} 秒")
|
||||
# 如果需要将结果存回 Open3D 点云进行可视化
|
||||
dists_o3d = o3d.utility.DoubleVector(distances.tolist())
|
||||
# 注意:Open3D 点云对象本身不直接存储标量距离字段用于内置可视化着色
|
||||
10
alg/平整度、平面度、轮廓测量v1.5/archive/requirements.txt
Normal file
10
alg/平整度、平面度、轮廓测量v1.5/archive/requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
#python 3.10.8
|
||||
--extra-index-url https://download.pytorch.org/whl/cu118
|
||||
torch
|
||||
torchvision
|
||||
torchaudio
|
||||
trimesh==4.5.3
|
||||
numpy==2.2.2
|
||||
open3d==0.19.0
|
||||
pythonocc-core==7.5.1 # conda install -c conda-forge pythonocc-core=
|
||||
git+https://github.com/AutodeskAILab/occwl #pip install git+https://github.com/AutodeskAILab/occwl
|
||||
474
alg/平整度、平面度、轮廓测量v1.5/archive/test8.py
Normal file
474
alg/平整度、平面度、轮廓测量v1.5/archive/test8.py
Normal file
@@ -0,0 +1,474 @@
|
||||
import gc
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from xml.sax.saxutils import escape as xml_escape
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import torch
|
||||
|
||||
|
||||
MESH_DIR = Path("Mesh")
|
||||
POINT_CLOUD_DIR = Path("CloudPoint_3000000")
|
||||
RESULT_DIR = Path("result")
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
DTYPE = torch.float64
|
||||
QUERY_CHUNK_SIZE = 8192 if DEVICE.type == "cuda" else 4096
|
||||
CANDIDATE_TRIANGLE_COUNT = 32
|
||||
SUMMARY_XLSX_PATH = RESULT_DIR / "task_distance_summary.xlsx"
|
||||
SUMMARY_CSV_PATH = RESULT_DIR / "task_distance_summary.csv"
|
||||
OPEN_VISUALIZATION_WINDOW = False
|
||||
COLOR_MAX_PERCENTILE = 99.0
|
||||
POINT_SIZE = 2.0
|
||||
|
||||
|
||||
def read_triangle_mesh(stl_path):
|
||||
mesh = o3d.io.read_triangle_mesh(stl_path)
|
||||
if mesh.is_empty():
|
||||
raise ValueError(f"Failed to read mesh: {stl_path}")
|
||||
if not mesh.has_triangles():
|
||||
raise ValueError(f"Mesh has no triangles: {stl_path}")
|
||||
return mesh
|
||||
|
||||
|
||||
def read_point_cloud_points(ply_path):
|
||||
pcd = o3d.io.read_point_cloud(ply_path)
|
||||
if pcd.is_empty():
|
||||
raise ValueError(f"Failed to read point cloud: {ply_path}")
|
||||
return np.asarray(pcd.points, dtype=np.float64).copy()
|
||||
|
||||
|
||||
def point_to_triangle_distance_squared(points, triangles):
|
||||
p = points[:, None, :]
|
||||
a = triangles[:, :, 0, :]
|
||||
b = triangles[:, :, 1, :]
|
||||
c = triangles[:, :, 2, :]
|
||||
|
||||
ab = b - a
|
||||
ac = c - a
|
||||
ap = p - a
|
||||
|
||||
d1 = (ab * ap).sum(dim=-1)
|
||||
d2 = (ac * ap).sum(dim=-1)
|
||||
inf = torch.full_like(d1, torch.inf)
|
||||
|
||||
mask = (d1 <= 0.0) & (d2 <= 0.0)
|
||||
dist2 = torch.where(mask, (ap * ap).sum(dim=-1), inf)
|
||||
|
||||
bp = p - b
|
||||
d3 = (ab * bp).sum(dim=-1)
|
||||
d4 = (ac * bp).sum(dim=-1)
|
||||
mask = (d3 >= 0.0) & (d4 <= d3)
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, (bp * bp).sum(dim=-1), inf))
|
||||
|
||||
vc = d1 * d4 - d3 * d2
|
||||
mask = (vc <= 0.0) & (d1 >= 0.0) & (d3 <= 0.0)
|
||||
v = d1 / (d1 - d3).clamp_min(torch.finfo(points.dtype).eps)
|
||||
closest = a + v[..., None] * ab
|
||||
edge_dist2 = ((p - closest) ** 2).sum(dim=-1)
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, edge_dist2, inf))
|
||||
|
||||
cp = p - c
|
||||
d5 = (ab * cp).sum(dim=-1)
|
||||
d6 = (ac * cp).sum(dim=-1)
|
||||
mask = (d6 >= 0.0) & (d5 <= d6)
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, (cp * cp).sum(dim=-1), inf))
|
||||
|
||||
vb = d5 * d2 - d1 * d6
|
||||
mask = (vb <= 0.0) & (d2 >= 0.0) & (d6 <= 0.0)
|
||||
w = d2 / (d2 - d6).clamp_min(torch.finfo(points.dtype).eps)
|
||||
closest = a + w[..., None] * ac
|
||||
edge_dist2 = ((p - closest) ** 2).sum(dim=-1)
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, edge_dist2, inf))
|
||||
|
||||
va = d3 * d6 - d5 * d4
|
||||
mask = (va <= 0.0) & ((d4 - d3) >= 0.0) & ((d5 - d6) >= 0.0)
|
||||
w = (d4 - d3) / ((d4 - d3) + (d5 - d6)).clamp_min(torch.finfo(points.dtype).eps)
|
||||
closest = b + w[..., None] * (c - b)
|
||||
edge_dist2 = ((p - closest) ** 2).sum(dim=-1)
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, edge_dist2, inf))
|
||||
|
||||
denom = (va + vb + vc).clamp_min(torch.finfo(points.dtype).eps)
|
||||
v = vb / denom
|
||||
w = vc / denom
|
||||
closest = a + ab * v[..., None] + ac * w[..., None]
|
||||
face_dist2 = ((p - closest) ** 2).sum(dim=-1)
|
||||
return torch.where(torch.isinf(dist2), face_dist2, dist2)
|
||||
|
||||
|
||||
def nearest_aabb_candidate_indices(points, triangle_mins, triangle_maxs, candidate_count):
|
||||
below = (triangle_mins[None, :, :] - points[:, None, :]).clamp_min(0.0)
|
||||
above = (points[:, None, :] - triangle_maxs[None, :, :]).clamp_min(0.0)
|
||||
aabb_dist2 = ((below + above) ** 2).sum(dim=-1)
|
||||
k = min(candidate_count, triangle_mins.shape[0])
|
||||
return torch.topk(aabb_dist2, k=k, dim=1, largest=False).indices
|
||||
|
||||
|
||||
def compute_mesh_distances(mesh, points_np, chunk_size, candidate_count):
|
||||
vertices = np.asarray(mesh.vertices, dtype=np.float64)
|
||||
triangles_idx = np.asarray(mesh.triangles, dtype=np.int64)
|
||||
points = torch.as_tensor(points_np, dtype=DTYPE, device=DEVICE)
|
||||
vertices = torch.as_tensor(vertices, dtype=DTYPE, device=DEVICE)
|
||||
triangles_idx = torch.as_tensor(triangles_idx, dtype=torch.long, device=DEVICE)
|
||||
triangles = vertices[triangles_idx]
|
||||
triangle_mins = triangles.min(dim=1).values
|
||||
triangle_maxs = triangles.max(dim=1).values
|
||||
|
||||
distances = []
|
||||
with torch.no_grad():
|
||||
for start in range(0, points.shape[0], chunk_size):
|
||||
chunk = points[start : start + chunk_size]
|
||||
candidate_idx = nearest_aabb_candidate_indices(
|
||||
chunk,
|
||||
triangle_mins,
|
||||
triangle_maxs,
|
||||
candidate_count,
|
||||
)
|
||||
candidate_triangles = triangles[candidate_idx]
|
||||
dist2 = point_to_triangle_distance_squared(chunk, candidate_triangles)
|
||||
chunk_distances = torch.sqrt(dist2.min(dim=1).values)
|
||||
distances.append(chunk_distances.cpu())
|
||||
|
||||
if DEVICE.type == "cuda":
|
||||
torch.cuda.synchronize()
|
||||
|
||||
return torch.cat(distances, dim=0).numpy()
|
||||
|
||||
|
||||
def task_id_from_mesh_path(mesh_path):
|
||||
return mesh_path.stem.split("_", 1)[0]
|
||||
|
||||
|
||||
def discover_mesh_tasks(mesh_dir, point_cloud_dir):
|
||||
mesh_paths = sorted(
|
||||
mesh_dir.glob("*.stl"),
|
||||
key=lambda path: (int(task_id_from_mesh_path(path)), path.stem),
|
||||
)
|
||||
if not mesh_paths:
|
||||
raise FileNotFoundError(f"No STL files found in {mesh_dir}")
|
||||
|
||||
tasks = {}
|
||||
for mesh_path in mesh_paths:
|
||||
point_cloud_path = point_cloud_dir / f"{mesh_path.stem}.ply"
|
||||
if not point_cloud_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Missing point cloud for {mesh_path.name}: {point_cloud_path}"
|
||||
)
|
||||
task_id = task_id_from_mesh_path(mesh_path)
|
||||
tasks.setdefault(task_id, []).append((mesh_path, point_cloud_path))
|
||||
return tasks
|
||||
|
||||
|
||||
def distance_colors(distances):
|
||||
finite_mask = np.isfinite(distances)
|
||||
if not finite_mask.any():
|
||||
raise ValueError("No finite distances to visualize.")
|
||||
|
||||
finite_distances = distances[finite_mask]
|
||||
color_min = float(finite_distances.min())
|
||||
if COLOR_MAX_PERCENTILE is None:
|
||||
color_max = float(finite_distances.max())
|
||||
else:
|
||||
color_max = float(np.percentile(finite_distances, COLOR_MAX_PERCENTILE))
|
||||
|
||||
if color_max <= color_min:
|
||||
color_max = float(finite_distances.max())
|
||||
if color_max <= color_min:
|
||||
normalized = np.zeros_like(distances, dtype=np.float64)
|
||||
else:
|
||||
normalized = (distances - color_min) / (color_max - color_min)
|
||||
normalized = np.clip(normalized, 0.0, 1.0)
|
||||
|
||||
palette_positions = np.array([0.0, 0.25, 0.50, 0.75, 1.0], dtype=np.float64)
|
||||
palette_colors = np.array(
|
||||
[
|
||||
[0.05, 0.12, 0.75],
|
||||
[0.00, 0.65, 1.00],
|
||||
[0.20, 0.90, 0.25],
|
||||
[1.00, 0.90, 0.00],
|
||||
[0.90, 0.05, 0.02],
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
colors = np.column_stack(
|
||||
[
|
||||
np.interp(normalized, palette_positions, palette_colors[:, channel])
|
||||
for channel in range(3)
|
||||
]
|
||||
)
|
||||
return colors, color_min, color_max
|
||||
|
||||
|
||||
def write_visualization_stats(path, mesh_path, point_cloud_path, distances, color_min, color_max, runtime):
|
||||
stats_path = Path(path)
|
||||
stats_text = "\n".join(
|
||||
[
|
||||
f"mesh: {mesh_path}",
|
||||
f"point_cloud: {point_cloud_path}",
|
||||
f"point_count: {len(distances)}",
|
||||
f"min_distance: {distances.min():.8f}",
|
||||
f"max_distance: {distances.max():.8f}",
|
||||
f"mean_distance: {distances.mean():.8f}",
|
||||
f"median_distance: {np.median(distances):.8f}",
|
||||
f"p90_distance: {np.percentile(distances, 90):.8f}",
|
||||
f"p95_distance: {np.percentile(distances, 95):.8f}",
|
||||
f"p99_distance: {np.percentile(distances, 99):.8f}",
|
||||
f"color_min: {color_min:.8f}",
|
||||
f"color_max: {color_max:.8f}",
|
||||
f"runtime: {runtime:.4f}",
|
||||
]
|
||||
)
|
||||
stats_path.write_text(stats_text + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def visualize_distances(points_np, distances, output_path):
|
||||
if len(points_np) != len(distances):
|
||||
raise ValueError(
|
||||
f"Point count ({len(points_np)}) and distance count ({len(distances)}) do not match."
|
||||
)
|
||||
|
||||
colors, color_min, color_max = distance_colors(distances)
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points_np)
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not o3d.io.write_point_cloud(str(output_path), pcd):
|
||||
raise RuntimeError(f"Failed to write colored point cloud: {output_path}")
|
||||
|
||||
if OPEN_VISUALIZATION_WINDOW:
|
||||
vis = o3d.visualization.Visualizer()
|
||||
vis.create_window(window_name="Point Distance Visualization")
|
||||
vis.add_geometry(pcd)
|
||||
render_option = vis.get_render_option()
|
||||
render_option.point_size = POINT_SIZE
|
||||
render_option.background_color = np.asarray([1.0, 1.0, 1.0])
|
||||
vis.run()
|
||||
vis.destroy_window()
|
||||
|
||||
return color_min, color_max
|
||||
|
||||
|
||||
def excel_column_name(column_index):
|
||||
name = ""
|
||||
while column_index:
|
||||
column_index, remainder = divmod(column_index - 1, 26)
|
||||
name = chr(65 + remainder) + name
|
||||
return name
|
||||
|
||||
|
||||
def excel_cell(value, row_index, column_index):
|
||||
cell_ref = f"{excel_column_name(column_index)}{row_index}"
|
||||
if value is None:
|
||||
return f'<c r="{cell_ref}"/>'
|
||||
if isinstance(value, (int, float, np.integer, np.floating)):
|
||||
return f'<c r="{cell_ref}"><v>{float(value)}</v></c>'
|
||||
text = xml_escape(str(value))
|
||||
return f'<c r="{cell_ref}" t="inlineStr"><is><t>{text}</t></is></c>'
|
||||
|
||||
|
||||
def write_summary_xlsx(path, rows):
|
||||
headers = [
|
||||
"Task",
|
||||
"Mesh count",
|
||||
"Point count",
|
||||
"Average distance",
|
||||
"Max distance",
|
||||
"Runtime",
|
||||
]
|
||||
table_rows = [headers] + [
|
||||
[
|
||||
row["task_id"],
|
||||
row["mesh_count"],
|
||||
row["point_count"],
|
||||
row["average_distance"],
|
||||
row["max_distance"],
|
||||
row["runtime"],
|
||||
]
|
||||
for row in rows
|
||||
]
|
||||
|
||||
sheet_rows = []
|
||||
for row_index, row_values in enumerate(table_rows, start=1):
|
||||
cells = "".join(
|
||||
excel_cell(value, row_index, column_index)
|
||||
for column_index, value in enumerate(row_values, start=1)
|
||||
)
|
||||
sheet_rows.append(f'<row r="{row_index}">{cells}</row>')
|
||||
|
||||
worksheet_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<cols>
|
||||
<col min="1" max="1" width="12" customWidth="1"/>
|
||||
<col min="2" max="3" width="14" customWidth="1"/>
|
||||
<col min="4" max="6" width="18" customWidth="1"/>
|
||||
</cols>
|
||||
<sheetData>{''.join(sheet_rows)}</sheetData>
|
||||
</worksheet>"""
|
||||
|
||||
workbook_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheets>
|
||||
<sheet name="Summary" sheetId="1" r:id="rId1"/>
|
||||
</sheets>
|
||||
</workbook>"""
|
||||
|
||||
workbook_rels_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
|
||||
Target="worksheets/sheet1.xml"/>
|
||||
</Relationships>"""
|
||||
|
||||
root_rels_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1"
|
||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
||||
Target="xl/workbook.xml"/>
|
||||
</Relationships>"""
|
||||
|
||||
content_types_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/xl/workbook.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||||
<Override PartName="/xl/worksheets/sheet1.xml"
|
||||
ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||||
</Types>"""
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as xlsx:
|
||||
xlsx.writestr("[Content_Types].xml", content_types_xml)
|
||||
xlsx.writestr("_rels/.rels", root_rels_xml)
|
||||
xlsx.writestr("xl/workbook.xml", workbook_xml)
|
||||
xlsx.writestr("xl/_rels/workbook.xml.rels", workbook_rels_xml)
|
||||
xlsx.writestr("xl/worksheets/sheet1.xml", worksheet_xml)
|
||||
|
||||
|
||||
def write_summary_csv(path, rows):
|
||||
headers = [
|
||||
"Task",
|
||||
"Mesh count",
|
||||
"Point count",
|
||||
"Average distance",
|
||||
"Max distance",
|
||||
"Runtime",
|
||||
]
|
||||
lines = [",".join(headers)]
|
||||
for row in rows:
|
||||
lines.append(
|
||||
",".join(
|
||||
[
|
||||
row["task_id"],
|
||||
str(row["mesh_count"]),
|
||||
str(row["point_count"]),
|
||||
f"{row['average_distance']:.8f}",
|
||||
f"{row['max_distance']:.8f}",
|
||||
f"{row['runtime']:.4f}",
|
||||
]
|
||||
)
|
||||
)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def compute_one_mesh(mesh_path, point_cloud_path, task_output_dir):
|
||||
start_time = time.perf_counter()
|
||||
mesh = read_triangle_mesh(str(mesh_path))
|
||||
original_points_np = read_point_cloud_points(str(point_cloud_path))
|
||||
distances = compute_mesh_distances(
|
||||
mesh,
|
||||
original_points_np,
|
||||
QUERY_CHUNK_SIZE,
|
||||
CANDIDATE_TRIANGLE_COUNT,
|
||||
)
|
||||
|
||||
task_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
distance_output_path = task_output_dir / f"{mesh_path.stem}_distances.npy"
|
||||
colored_output_path = task_output_dir / f"{mesh_path.stem}_distance_colored.ply"
|
||||
stats_output_path = task_output_dir / f"{mesh_path.stem}_distance_stats.txt"
|
||||
|
||||
np.save(distance_output_path, distances)
|
||||
color_min, color_max = visualize_distances(
|
||||
original_points_np,
|
||||
distances,
|
||||
colored_output_path,
|
||||
)
|
||||
runtime = time.perf_counter() - start_time
|
||||
write_visualization_stats(
|
||||
stats_output_path,
|
||||
mesh_path,
|
||||
point_cloud_path,
|
||||
distances,
|
||||
color_min,
|
||||
color_max,
|
||||
runtime,
|
||||
)
|
||||
|
||||
return {
|
||||
"mesh_name": mesh_path.name,
|
||||
"point_count": len(distances),
|
||||
"distance_sum": float(distances.sum()),
|
||||
"average_distance": float(distances.mean()),
|
||||
"max_distance": float(distances.max()),
|
||||
"runtime": runtime,
|
||||
"distance_output_path": distance_output_path,
|
||||
"colored_output_path": colored_output_path,
|
||||
"stats_output_path": stats_output_path,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
tasks = discover_mesh_tasks(MESH_DIR, POINT_CLOUD_DIR)
|
||||
summary_rows = []
|
||||
|
||||
for task_id, mesh_pairs in tasks.items():
|
||||
task_output_dir = RESULT_DIR / task_id
|
||||
task_point_count = 0
|
||||
task_distance_sum = 0.0
|
||||
task_max_distance = 0.0
|
||||
task_runtime = 0.0
|
||||
|
||||
for mesh_path, point_cloud_path in mesh_pairs:
|
||||
mesh_result = compute_one_mesh(mesh_path, point_cloud_path, task_output_dir)
|
||||
task_point_count += mesh_result["point_count"]
|
||||
task_distance_sum += mesh_result["distance_sum"]
|
||||
task_max_distance = max(task_max_distance, mesh_result["max_distance"])
|
||||
task_runtime += mesh_result["runtime"]
|
||||
|
||||
del mesh_result
|
||||
gc.collect()
|
||||
if DEVICE.type == "cuda":
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
summary_rows.append(
|
||||
{
|
||||
"task_id": task_id,
|
||||
"mesh_count": len(mesh_pairs),
|
||||
"point_count": task_point_count,
|
||||
"average_distance": task_distance_sum / task_point_count,
|
||||
"max_distance": task_max_distance,
|
||||
"runtime": task_runtime,
|
||||
}
|
||||
)
|
||||
|
||||
summary_rows.sort(key=lambda row: int(row["task_id"]))
|
||||
write_summary_xlsx(SUMMARY_XLSX_PATH, summary_rows)
|
||||
write_summary_csv(SUMMARY_CSV_PATH, summary_rows)
|
||||
|
||||
print("Task\tAverage distance\tMax distance\tRuntime")
|
||||
for row in summary_rows:
|
||||
print(
|
||||
f"{row['task_id']}\t"
|
||||
f"{row['average_distance']:.8f}\t"
|
||||
f"{row['max_distance']:.8f}\t"
|
||||
f"{row['runtime']:.4f}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1350
alg/平整度、平面度、轮廓测量v1.5/archive/translate_align_pointcloud_to_cad.py
Normal file
1350
alg/平整度、平面度、轮廓测量v1.5/archive/translate_align_pointcloud_to_cad.py
Normal file
File diff suppressed because it is too large
Load Diff
118
alg/平整度、平面度、轮廓测量v1.5/cad_converter.py
Normal file
118
alg/平整度、平面度、轮廓测量v1.5/cad_converter.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class CADConverter:
|
||||
"""Convert STEP CAD files to STL meshes."""
|
||||
|
||||
def read_step_shape(self, input_step: str | Path):
|
||||
try:
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.STEPControl import STEPControl_Reader
|
||||
except ModuleNotFoundError as exc:
|
||||
raise RuntimeError(
|
||||
"pythonocc-core is required for CAD conversion. "
|
||||
"Install it with conda-forge before running CAD workflows."
|
||||
) from exc
|
||||
|
||||
input_step = Path(input_step)
|
||||
step_reader = STEPControl_Reader()
|
||||
if step_reader.ReadFile(str(input_step)) != IFSelect_RetDone:
|
||||
raise ValueError(f"Failed to read STEP file: {input_step}")
|
||||
step_reader.TransferRoots()
|
||||
return step_reader.OneShape()
|
||||
|
||||
def step_to_numpy_mesh(
|
||||
self,
|
||||
input_step: str | Path,
|
||||
mesh_deflection: float = 0.1,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
try:
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.TopAbs import TopAbs_FACE
|
||||
from OCC.Core.TopExp import TopExp_Explorer
|
||||
|
||||
try:
|
||||
from OCC.Core.TopoDS import topods
|
||||
except ImportError:
|
||||
from OCC.Core.TopoDS import TopoDS
|
||||
|
||||
topods = None
|
||||
except ModuleNotFoundError as exc:
|
||||
raise RuntimeError(
|
||||
"pythonocc-core is required for CAD mesh extraction. "
|
||||
"Install it with conda-forge before running CAD workflows."
|
||||
) from exc
|
||||
|
||||
shape = self.read_step_shape(input_step)
|
||||
mesh = BRepMesh_IncrementalMesh(shape, mesh_deflection, False)
|
||||
mesh.Perform()
|
||||
if not mesh.IsDone():
|
||||
raise RuntimeError("CAD mesh discretization failed.")
|
||||
|
||||
vertices: list[list[float]] = []
|
||||
faces: list[list[int]] = []
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
|
||||
while explorer.More():
|
||||
current_shape = explorer.Current()
|
||||
face = topods.Face(current_shape) if topods is not None else TopoDS.Face(current_shape)
|
||||
location = face.Location()
|
||||
triangulation = BRep_Tool.Triangulation(face, location)
|
||||
if triangulation is None:
|
||||
explorer.Next()
|
||||
continue
|
||||
|
||||
transform = location.Transformation()
|
||||
base_index = len(vertices)
|
||||
for index in range(1, triangulation.NbNodes() + 1):
|
||||
point = triangulation.Node(index)
|
||||
point.Transform(transform)
|
||||
vertices.append([point.X(), point.Y(), point.Z()])
|
||||
|
||||
for index in range(1, triangulation.NbTriangles() + 1):
|
||||
n1, n2, n3 = triangulation.Triangle(index).Get()
|
||||
faces.append([base_index + n1 - 1, base_index + n2 - 1, base_index + n3 - 1])
|
||||
|
||||
explorer.Next()
|
||||
|
||||
vertices_np = np.asarray(vertices, dtype=np.float64)
|
||||
faces_np = np.asarray(faces, dtype=np.int64)
|
||||
if len(vertices_np) == 0 or len(faces_np) == 0:
|
||||
raise RuntimeError("CAD triangle mesh extraction failed; try a smaller mesh deflection.")
|
||||
return vertices_np, faces_np
|
||||
|
||||
def convert_step_to_stl(self, input_step: str | Path, output_stl: str | Path, precision: float = 1.0) -> Path:
|
||||
try:
|
||||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCC.Core.StlAPI import StlAPI_Writer
|
||||
except ModuleNotFoundError as exc:
|
||||
raise RuntimeError(
|
||||
"pythonocc-core is required for CAD conversion. "
|
||||
"Install it with conda-forge before running cad2mesh."
|
||||
) from exc
|
||||
|
||||
input_step = Path(input_step)
|
||||
output_stl = Path(output_stl)
|
||||
output_stl.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
start = time.time()
|
||||
print(f"[1/3] Reading STEP: {input_step}")
|
||||
shape = self.read_step_shape(input_step)
|
||||
|
||||
print(f"[2/3] Meshing with precision {precision} mm")
|
||||
mesh = BRepMesh_IncrementalMesh(shape, precision, False)
|
||||
mesh.Perform()
|
||||
if not mesh.IsDone():
|
||||
raise RuntimeError("Mesh discretization failed.")
|
||||
|
||||
print(f"[3/3] Writing STL: {output_stl}")
|
||||
writer = StlAPI_Writer()
|
||||
writer.Write(shape, str(output_stl))
|
||||
print(f"CAD conversion finished in {time.time() - start:.2f}s")
|
||||
return output_stl
|
||||
2603
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/11.step
Normal file
2603
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/11.step
Normal file
File diff suppressed because it is too large
Load Diff
216497
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/13_1.step
Normal file
216497
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/13_1.step
Normal file
File diff suppressed because it is too large
Load Diff
215076
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/13_2.step
Normal file
215076
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/13_2.step
Normal file
File diff suppressed because it is too large
Load Diff
25551
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/20_1.step
Normal file
25551
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/20_1.step
Normal file
File diff suppressed because it is too large
Load Diff
25348
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/20_2.step
Normal file
25348
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/20_2.step
Normal file
File diff suppressed because it is too large
Load Diff
61511
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/25.step
Normal file
61511
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/25.step
Normal file
File diff suppressed because it is too large
Load Diff
10425
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/31.step
Normal file
10425
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/31.step
Normal file
File diff suppressed because it is too large
Load Diff
162144
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/34_1.step
Normal file
162144
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/34_1.step
Normal file
File diff suppressed because it is too large
Load Diff
81889
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/34_2.step
Normal file
81889
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/34_2.step
Normal file
File diff suppressed because it is too large
Load Diff
163984
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/35.step
Normal file
163984
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/35.step
Normal file
File diff suppressed because it is too large
Load Diff
32180
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/39_1.step
Normal file
32180
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/39_1.step
Normal file
File diff suppressed because it is too large
Load Diff
27915
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/39_2.step
Normal file
27915
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/39_2.step
Normal file
File diff suppressed because it is too large
Load Diff
275952
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/45.step
Normal file
275952
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/45.step
Normal file
File diff suppressed because it is too large
Load Diff
6
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/bbox.txt
Normal file
6
alg/平整度、平面度、轮廓测量v1.5/car_part_v1/bbox.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
11:
|
||||
13_1:BBox: [(-3210.0, 1287.999159154, -1679.9494693872527), (12310.0, 3630.218176727, -1415.224622514)], Center: (4550.0, 2459.1086679405, -1547.5870459506264)
|
||||
13_2:BBox: [(-3210.0, 1287.999159154, 1415.224622514), (12310.0, 3630.217969482988, 1679.9494693872527)], Center: (4550.0, 2459.108564318494, 1547.5870459506264)
|
||||
20_1:BBox: [(-2701.5, 1180.0, -1682.9628204686855), (-1451.5, 3435.639725861, -1380.0)], Center: (-2076.5, 2307.8198629304998, -1531.4814102343428)
|
||||
20_2:BBox: [(-2701.5, 1179.999999999994, 1379.9999999999998), (-1451.5, 3435.639725861, 1682.9628204686855)], Center: (-2076.5, 2307.819862930497, 1531.4814102343425)
|
||||
25:BBox: [(12400.0, 1180.0, -1479.9677957553995), (12580.0, 3899.9999364265, 1479.9703280536996)], Center: (12490.0, 2539.99996821325, 0.0012661491500693955)
|
||||
59
alg/平整度、平面度、轮廓测量v1.5/config.yaml
Normal file
59
alg/平整度、平面度、轮廓测量v1.5/config.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
paths:
|
||||
cad: datasets/CAD/11.step
|
||||
mesh: datasets/Mesh/11.stl
|
||||
pointcloud: datasets/PointCloud/11.ply
|
||||
noisy_pointcloud: datasets/Noise_PointCloud/11_noisy.ply
|
||||
output_mesh: datasets/Mesh/11.stl
|
||||
curvature_output:
|
||||
|
||||
cad:
|
||||
enabled: false
|
||||
precision: 1.0
|
||||
|
||||
noise:
|
||||
enabled: true
|
||||
std: 4.0
|
||||
seed:
|
||||
|
||||
filters:
|
||||
enabled: true
|
||||
methods:
|
||||
- SOR
|
||||
sor_neighbors: 20
|
||||
sor_std_ratio: 2.0
|
||||
ror_points: 5
|
||||
ror_radius: 20.0
|
||||
voxel_size: 0.05
|
||||
pass_axis: z
|
||||
pass_min:
|
||||
pass_max:
|
||||
bf_sigma_s: 0.01
|
||||
bf_sigma_r: 0.1
|
||||
|
||||
registration:
|
||||
voxel_size: 10.0
|
||||
roll: 10.0
|
||||
pitch: 10.0
|
||||
yaw: 10.0
|
||||
trans_range: 0.0
|
||||
random_seed:
|
||||
|
||||
evaluation:
|
||||
chunk_size: 2048
|
||||
triangle_chunk_size: 4096
|
||||
cad_mesh_deflection: 0.1
|
||||
|
||||
curvature:
|
||||
radius: 2.0
|
||||
max_nn: 30
|
||||
color_scale: 3.0
|
||||
|
||||
plane:
|
||||
size_mm: 1000.0
|
||||
noise_std_mm: 0.5
|
||||
num_points: 10000
|
||||
threshold_mm: 0.5
|
||||
max_iterations: 2000
|
||||
|
||||
visualization:
|
||||
enabled: true
|
||||
104
alg/平整度、平面度、轮廓测量v1.5/config_loader.py
Normal file
104
alg/平整度、平面度、轮廓测量v1.5/config_loader.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
DEFAULT_CONFIG: dict[str, Any] = {
|
||||
"paths": {
|
||||
"cad": "CAD/11.step",
|
||||
"mesh": "Mesh/11.stl",
|
||||
"pointcloud": "PointCloud/11.ply",
|
||||
"noisy_pointcloud": "Noise_PointCloud/11_noisy.ply",
|
||||
"output_mesh": "Mesh/11.stl",
|
||||
"curvature_output": None,
|
||||
},
|
||||
"cad": {
|
||||
"precision": 1.0,
|
||||
"enabled": False,
|
||||
},
|
||||
"noise": {
|
||||
"enabled": True,
|
||||
"std": 4.0,
|
||||
"seed": None,
|
||||
},
|
||||
"filters": {
|
||||
"enabled": True,
|
||||
"methods": ["SOR"],
|
||||
"sor_neighbors": 20,
|
||||
"sor_std_ratio": 2.0,
|
||||
"ror_points": 5,
|
||||
"ror_radius": 20.0,
|
||||
"voxel_size": 0.05,
|
||||
"pass_axis": "z",
|
||||
"pass_min": None,
|
||||
"pass_max": None,
|
||||
"bf_sigma_s": 0.01,
|
||||
"bf_sigma_r": 0.1,
|
||||
},
|
||||
"registration": {
|
||||
"voxel_size": 10.0,
|
||||
"roll": 10.0,
|
||||
"pitch": 10.0,
|
||||
"yaw": 10.0,
|
||||
"trans_range": 0.0,
|
||||
"random_seed": None,
|
||||
},
|
||||
"evaluation": {
|
||||
"chunk_size": 2048,
|
||||
"triangle_chunk_size": 4096,
|
||||
"cad_mesh_deflection": 0.1,
|
||||
},
|
||||
"curvature": {
|
||||
"radius": 2.0,
|
||||
"max_nn": 30,
|
||||
"color_scale": 3.0,
|
||||
},
|
||||
"plane": {
|
||||
"size_mm": 1000.0,
|
||||
"noise_std_mm": 0.5,
|
||||
"num_points": 10000,
|
||||
"threshold_mm": 0.5,
|
||||
"max_iterations": 2000,
|
||||
},
|
||||
"visualization": {
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def deep_update(base: dict[str, Any], updates: dict[str, Any]) -> dict[str, Any]:
|
||||
result = deepcopy(base)
|
||||
for key, value in updates.items():
|
||||
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
||||
result[key] = deep_update(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
"""Load YAML config and merge it with project defaults."""
|
||||
|
||||
def __init__(self, default_config: dict[str, Any] | None = None) -> None:
|
||||
self.default_config = default_config or DEFAULT_CONFIG
|
||||
|
||||
def load(self, config_path: str | None = None) -> dict[str, Any]:
|
||||
config = deepcopy(self.default_config)
|
||||
if not config_path:
|
||||
return config
|
||||
|
||||
path = Path(config_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Config file not found: {path}")
|
||||
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
user_config = yaml.safe_load(f) or {}
|
||||
|
||||
if not isinstance(user_config, dict):
|
||||
raise ValueError("Config file must contain a YAML mapping at the top level.")
|
||||
|
||||
return deep_update(config, user_config)
|
||||
7
alg/平整度、平面度、轮廓测量v1.5/environment.yml
Normal file
7
alg/平整度、平面度、轮廓测量v1.5/environment.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
name: point-porj
|
||||
channels:
|
||||
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge
|
||||
dependencies:
|
||||
- python=3.10
|
||||
- pip
|
||||
- pythonocc-core=7.5.1
|
||||
203
alg/平整度、平面度、轮廓测量v1.5/evaluation.py
Normal file
203
alg/平整度、平面度、轮廓测量v1.5/evaluation.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import torch
|
||||
import trimesh
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
|
||||
@dataclass
|
||||
class DistanceStats:
|
||||
mean: float
|
||||
max: float
|
||||
rmse: float
|
||||
min: float
|
||||
std: float
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""Evaluation metrics for point-cloud workflows."""
|
||||
|
||||
def chamfer_distance(
|
||||
self,
|
||||
pointcloud_a: o3d.geometry.PointCloud,
|
||||
pointcloud_b: o3d.geometry.PointCloud,
|
||||
sqrt: bool = True,
|
||||
) -> tuple[float, float, float]:
|
||||
points_a = np.asarray(pointcloud_a.points)
|
||||
points_b = np.asarray(pointcloud_b.points)
|
||||
if len(points_a) == 0 or len(points_b) == 0:
|
||||
return float("inf"), 0.0, 0.0
|
||||
|
||||
tree_a = KDTree(points_a)
|
||||
tree_b = KDTree(points_b)
|
||||
dist_a, _ = tree_b.query(points_a, k=1)
|
||||
dist_b, _ = tree_a.query(points_b, k=1)
|
||||
cd_a = float(np.mean(dist_a ** 2))
|
||||
cd_b = float(np.mean(dist_b ** 2))
|
||||
cd = cd_a + cd_b
|
||||
return (float(np.sqrt(cd)), cd_a, cd_b) if sqrt else (float(cd), cd_a, cd_b)
|
||||
|
||||
def point_to_mesh_distance_trimesh(self, points: np.ndarray, mesh_path: str) -> tuple[np.ndarray, DistanceStats]:
|
||||
mesh = trimesh.load(mesh_path, force="mesh")
|
||||
_, distances, _ = mesh.nearest.on_surface(points)
|
||||
distances = np.asarray(distances, dtype=np.float64)
|
||||
return distances, self.distance_stats(distances)
|
||||
|
||||
def point_to_mesh_distance_torch(
|
||||
self,
|
||||
points: np.ndarray,
|
||||
mesh: trimesh.Trimesh,
|
||||
chunk_size: int = 2048,
|
||||
triangle_chunk_size: int | None = 4096,
|
||||
) -> tuple[np.ndarray, DistanceStats]:
|
||||
vertices, faces = self._mesh_arrays(mesh)
|
||||
return self.point_to_triangles_distance_torch(
|
||||
points,
|
||||
vertices,
|
||||
faces,
|
||||
point_chunk_size=chunk_size,
|
||||
triangle_chunk_size=triangle_chunk_size,
|
||||
)
|
||||
|
||||
def point_to_triangles_distance_torch(
|
||||
self,
|
||||
points: np.ndarray,
|
||||
vertices: np.ndarray,
|
||||
faces: np.ndarray,
|
||||
point_chunk_size: int = 2048,
|
||||
triangle_chunk_size: int | None = 4096,
|
||||
) -> tuple[np.ndarray, DistanceStats]:
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
dtype = torch.float64
|
||||
vertices_t = torch.as_tensor(np.asarray(vertices, dtype=np.float64), dtype=dtype, device=device)
|
||||
faces_t = torch.as_tensor(np.asarray(faces, dtype=np.int64), dtype=torch.long, device=device)
|
||||
triangles = vertices_t[faces_t]
|
||||
points_t = torch.as_tensor(points, dtype=dtype, device=device)
|
||||
|
||||
distances = []
|
||||
with torch.no_grad():
|
||||
for start in range(0, points_t.shape[0], point_chunk_size):
|
||||
chunk = points_t[start:start + point_chunk_size]
|
||||
if triangle_chunk_size is None or triangle_chunk_size <= 0:
|
||||
dist2 = self._point_to_triangle_distance_squared(chunk, triangles)
|
||||
min_dist2 = dist2.min(dim=1).values
|
||||
else:
|
||||
min_dist2 = torch.full(
|
||||
(chunk.shape[0],),
|
||||
torch.inf,
|
||||
dtype=points_t.dtype,
|
||||
device=device,
|
||||
)
|
||||
for triangle_start in range(0, triangles.shape[0], triangle_chunk_size):
|
||||
triangle_chunk = triangles[triangle_start:triangle_start + triangle_chunk_size]
|
||||
dist2 = self._point_to_triangle_distance_squared(chunk, triangle_chunk)
|
||||
min_dist2 = torch.minimum(min_dist2, dist2.min(dim=1).values)
|
||||
|
||||
distances.append(torch.sqrt(min_dist2).cpu().numpy())
|
||||
|
||||
all_distances = np.concatenate(distances) if distances else np.array([], dtype=np.float64)
|
||||
return all_distances, self.distance_stats(all_distances)
|
||||
|
||||
def colorize_distances(self, points: np.ndarray, distances: np.ndarray) -> o3d.geometry.PointCloud:
|
||||
if len(points) != len(distances):
|
||||
raise ValueError("points and distances must have the same length.")
|
||||
pointcloud = o3d.geometry.PointCloud()
|
||||
pointcloud.points = o3d.utility.Vector3dVector(np.asarray(points, dtype=np.float64))
|
||||
|
||||
if len(distances) == 0:
|
||||
pointcloud.colors = o3d.utility.Vector3dVector(np.empty((0, 3), dtype=np.float64))
|
||||
return pointcloud
|
||||
|
||||
lower = float(np.min(distances))
|
||||
upper = float(np.max(distances))
|
||||
normalized = (distances - lower) / (upper - lower + 1e-12)
|
||||
colors = self._blue_to_red_colormap(normalized)
|
||||
pointcloud.colors = o3d.utility.Vector3dVector(colors)
|
||||
return pointcloud
|
||||
|
||||
def distance_stats(self, distances: np.ndarray) -> DistanceStats:
|
||||
if len(distances) == 0:
|
||||
return DistanceStats(0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
return DistanceStats(
|
||||
mean=float(np.mean(distances)),
|
||||
max=float(np.max(distances)),
|
||||
rmse=float(np.sqrt(np.mean(distances ** 2))),
|
||||
min=float(np.min(distances)),
|
||||
std=float(np.std(distances)),
|
||||
)
|
||||
|
||||
def registration_summary(self, transformation: np.ndarray, fitness: float, rmse: float) -> dict[str, Any]:
|
||||
return {
|
||||
"fitness": fitness,
|
||||
"rmse": rmse,
|
||||
"transformation": transformation,
|
||||
}
|
||||
|
||||
def _mesh_arrays(self, mesh: trimesh.Trimesh) -> tuple[np.ndarray, np.ndarray]:
|
||||
vertices = np.asarray(mesh.vertices, dtype=np.float64)
|
||||
faces = np.asarray(mesh.faces, dtype=np.int64)
|
||||
if len(vertices) == 0 or len(faces) == 0:
|
||||
raise ValueError("Mesh must contain vertices and faces.")
|
||||
return vertices, faces
|
||||
|
||||
def _blue_to_red_colormap(self, values: np.ndarray) -> np.ndarray:
|
||||
values = np.clip(np.asarray(values, dtype=np.float64), 0.0, 1.0)
|
||||
colors = np.zeros((len(values), 3), dtype=np.float64)
|
||||
colors[:, 0] = np.clip(1.8 * values - 0.2, 0.0, 1.0)
|
||||
colors[:, 1] = np.clip(1.0 - np.abs(values - 0.5) * 2.0, 0.0, 1.0)
|
||||
colors[:, 2] = np.clip(1.2 - 1.8 * values, 0.0, 1.0)
|
||||
return colors
|
||||
|
||||
def _point_to_triangle_distance_squared(self, points: torch.Tensor, triangles: torch.Tensor) -> torch.Tensor:
|
||||
p = points[:, None, :]
|
||||
a = triangles[None, :, 0, :]
|
||||
b = triangles[None, :, 1, :]
|
||||
c = triangles[None, :, 2, :]
|
||||
|
||||
ab = b - a
|
||||
ac = c - a
|
||||
ap = p - a
|
||||
|
||||
d1 = (ab * ap).sum(dim=-1)
|
||||
d2 = (ac * ap).sum(dim=-1)
|
||||
inf = torch.full_like(d1, torch.inf)
|
||||
dist2 = torch.where((d1 <= 0.0) & (d2 <= 0.0), (ap * ap).sum(dim=-1), inf)
|
||||
|
||||
bp = p - b
|
||||
d3 = (ab * bp).sum(dim=-1)
|
||||
d4 = (ac * bp).sum(dim=-1)
|
||||
dist2 = torch.minimum(dist2, torch.where((d3 >= 0.0) & (d4 <= d3), (bp * bp).sum(dim=-1), inf))
|
||||
|
||||
vc = d1 * d4 - d3 * d2
|
||||
mask = (vc <= 0.0) & (d1 >= 0.0) & (d3 <= 0.0)
|
||||
v = d1 / (d1 - d3).clamp_min(torch.finfo(points.dtype).eps)
|
||||
closest = a + v[..., None] * ab
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, ((p - closest) ** 2).sum(dim=-1), inf))
|
||||
|
||||
cp = p - c
|
||||
d5 = (ab * cp).sum(dim=-1)
|
||||
d6 = (ac * cp).sum(dim=-1)
|
||||
dist2 = torch.minimum(dist2, torch.where((d6 >= 0.0) & (d5 <= d6), (cp * cp).sum(dim=-1), inf))
|
||||
|
||||
vb = d5 * d2 - d1 * d6
|
||||
mask = (vb <= 0.0) & (d2 >= 0.0) & (d6 <= 0.0)
|
||||
w = d2 / (d2 - d6).clamp_min(torch.finfo(points.dtype).eps)
|
||||
closest = a + w[..., None] * ac
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, ((p - closest) ** 2).sum(dim=-1), inf))
|
||||
|
||||
va = d3 * d6 - d5 * d4
|
||||
mask = (va <= 0.0) & ((d4 - d3) >= 0.0) & ((d5 - d6) >= 0.0)
|
||||
w = (d4 - d3) / ((d4 - d3) + (d5 - d6)).clamp_min(torch.finfo(points.dtype).eps)
|
||||
closest = b + w[..., None] * (c - b)
|
||||
dist2 = torch.minimum(dist2, torch.where(mask, ((p - closest) ** 2).sum(dim=-1), inf))
|
||||
|
||||
denom = (va + vb + vc).clamp_min(torch.finfo(points.dtype).eps)
|
||||
v = vb / denom
|
||||
w = vc / denom
|
||||
closest = a + ab * v[..., None] + ac * w[..., None]
|
||||
return torch.minimum(dist2, ((p - closest) ** 2).sum(dim=-1))
|
||||
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/11_contour.xlsx
Normal file
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/11_contour.xlsx
Normal file
Binary file not shown.
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/all_contour.xlsx
Normal file
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/all_contour.xlsx
Normal file
Binary file not shown.
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/batch_test_contour.xlsx
Normal file
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/batch_test_contour.xlsx
Normal file
Binary file not shown.
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/cad_distance.xlsx
Normal file
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/cad_distance.xlsx
Normal file
Binary file not shown.
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/test_cad_distance.xlsx
Normal file
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/test_cad_distance.xlsx
Normal file
Binary file not shown.
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/test_contour.xlsx
Normal file
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/test_contour.xlsx
Normal file
Binary file not shown.
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/测量结果.xlsx
Normal file
BIN
alg/平整度、平面度、轮廓测量v1.5/excel/测量结果.xlsx
Normal file
Binary file not shown.
69
alg/平整度、平面度、轮廓测量v1.5/io_handler.py
Normal file
69
alg/平整度、平面度、轮廓测量v1.5/io_handler.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import trimesh
|
||||
|
||||
|
||||
class IOHandler:
|
||||
"""Centralized input validation and file loading."""
|
||||
|
||||
def resolve_path(self, path: str | Path) -> Path:
|
||||
return Path(path).expanduser()
|
||||
|
||||
def require_file(self, path: str | Path, label: str = "file") -> Path:
|
||||
resolved = self.resolve_path(path)
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(f"{label} not found: {resolved}")
|
||||
if not resolved.is_file():
|
||||
raise ValueError(f"{label} is not a file: {resolved}")
|
||||
return resolved
|
||||
|
||||
def ensure_parent_dir(self, path: str | Path) -> Path:
|
||||
resolved = self.resolve_path(path)
|
||||
resolved.parent.mkdir(parents=True, exist_ok=True)
|
||||
return resolved
|
||||
|
||||
def load_point_cloud(self, pointcloud_path: str | Path) -> o3d.geometry.PointCloud:
|
||||
path = self.require_file(pointcloud_path, "Point cloud")
|
||||
pcd = o3d.io.read_point_cloud(str(path))
|
||||
if len(pcd.points) == 0:
|
||||
raise ValueError(f"Point cloud is empty or unreadable: {path}")
|
||||
return pcd
|
||||
|
||||
def load_mesh(self, mesh_path: str | Path) -> trimesh.Trimesh:
|
||||
path = self.require_file(mesh_path, "Mesh")
|
||||
mesh = trimesh.load(str(path), force="mesh")
|
||||
if mesh.is_empty:
|
||||
raise ValueError(f"Mesh is empty or unreadable: {path}")
|
||||
return mesh
|
||||
|
||||
def require_cad(self, cad_path: str | Path) -> Path:
|
||||
return self.require_file(cad_path, "CAD")
|
||||
|
||||
def save_point_cloud(self, pointcloud: o3d.geometry.PointCloud, output_path: str | Path) -> Path:
|
||||
path = self.ensure_parent_dir(output_path)
|
||||
ok = o3d.io.write_point_cloud(str(path), pointcloud)
|
||||
if not ok:
|
||||
raise IOError(f"Failed to save point cloud: {path}")
|
||||
return path
|
||||
|
||||
def point_cloud_to_numpy(self, pointcloud: o3d.geometry.PointCloud) -> np.ndarray:
|
||||
points = np.asarray(pointcloud.points, dtype=np.float64)
|
||||
if points.size == 0:
|
||||
raise ValueError("Point cloud has no points.")
|
||||
return points
|
||||
|
||||
def mesh_triangles_to_numpy(self, mesh: trimesh.Trimesh) -> tuple[np.ndarray, np.ndarray]:
|
||||
vertices = np.asarray(mesh.vertices, dtype=np.float64)
|
||||
faces = np.asarray(mesh.faces, dtype=np.int64)
|
||||
if len(vertices) == 0 or len(faces) == 0:
|
||||
raise ValueError("Mesh must contain vertices and faces.")
|
||||
return vertices, faces
|
||||
|
||||
def print_mapping(self, data: dict[str, Any]) -> None:
|
||||
for key, value in data.items():
|
||||
print(f"{key}: {value}")
|
||||
206
alg/平整度、平面度、轮廓测量v1.5/main.py
Normal file
206
alg/平整度、平面度、轮廓测量v1.5/main.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--config", default=argparse.SUPPRESS, help="YAML config path, default uses built-in settings")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Industrial geometry measurement workflow")
|
||||
parser.add_argument("--config", default=None, help="YAML config path, default uses built-in settings")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
cad = subparsers.add_parser("cad2mesh", parents=[common], help="Convert STEP CAD to STL Mesh")
|
||||
cad.add_argument("--cad", required=False, help="Input STEP file")
|
||||
cad.add_argument("--mesh", required=False, help="Output STL file")
|
||||
cad.add_argument("--precision", type=float, default=None, help="Mesh precision in mm")
|
||||
|
||||
denoise = subparsers.add_parser("denoise", parents=[common], help="Add noise and run denoise filters")
|
||||
denoise.add_argument("--pointcloud", "-p", default=None, help="Input clean PLY point cloud")
|
||||
denoise.add_argument("--noise-std", type=float, default=None, help="Gaussian noise std")
|
||||
denoise.add_argument("--no-visualize", action="store_true", help="Disable Open3D windows")
|
||||
|
||||
registration = subparsers.add_parser("registration", parents=[common], help="Run simulated point-cloud registration")
|
||||
registration.add_argument("--pointcloud", "-p", default=None, help="Input PLY point cloud")
|
||||
registration.add_argument("--mesh", "-m", default=None, help="Reference STL/OBJ/PLY mesh")
|
||||
registration.add_argument("--voxel-size", "-v", type=float, default=None, help="Registration voxel size")
|
||||
registration.add_argument("--roll", type=float, default=None, help="Rotation around X axis in degrees")
|
||||
registration.add_argument("--pitch", type=float, default=None, help="Rotation around Y axis in degrees")
|
||||
registration.add_argument("--yaw", type=float, default=None, help="Rotation around Z axis in degrees")
|
||||
registration.add_argument("--trans-range", type=float, default=None, help="Random translation range")
|
||||
registration.add_argument("--no-visualize", action="store_true", help="Disable Open3D windows")
|
||||
|
||||
contour = subparsers.add_parser(
|
||||
"contour",
|
||||
parents=[common],
|
||||
help="Quick contour/flatness/planarity evaluation against STEP CAD",
|
||||
)
|
||||
contour.add_argument("--pointcloud", "-p", default=None, help="Input PLY point cloud")
|
||||
contour.add_argument("--cad", "-c", default=None, help="Reference STEP CAD file")
|
||||
contour.add_argument("--mesh", "-m", default=None, help="Legacy reference STL/OBJ/PLY mesh")
|
||||
contour.add_argument(
|
||||
"--tasks",
|
||||
default=None,
|
||||
help="Comma-separated task ids or sample names. Example: 13 expands 13_1/13_2; 13_1 runs that sample only.",
|
||||
)
|
||||
contour.add_argument("--all", action="store_true", help="Run contour evaluation for all Mesh/CAD ids under data root")
|
||||
contour.add_argument("--data-root", default="datasets", help="Dataset root for --tasks/--all batch mode")
|
||||
contour.add_argument(
|
||||
"--pointcloud-kind",
|
||||
choices=["noise", "clean"],
|
||||
default="noise",
|
||||
help="Point cloud source for batch mode: noise=Noise_PointCloud, clean=PointCloud",
|
||||
)
|
||||
contour.add_argument("--mesh-deflection", type=float, default=None, help="CAD meshing deflection")
|
||||
contour.add_argument("--point-chunk-size", type=int, default=None, help="Point chunk size")
|
||||
contour.add_argument("--triangle-chunk-size", type=int, default=None, help="Triangle chunk size")
|
||||
contour.add_argument("--save", default=None, help="Optional colored point-cloud output path")
|
||||
contour.add_argument(
|
||||
"--excel-output",
|
||||
nargs="?",
|
||||
const="excel/contour.xlsx",
|
||||
default=None,
|
||||
help="Optional Excel output path; defaults to excel/contour.xlsx when used without a value",
|
||||
)
|
||||
contour.add_argument(
|
||||
"--result-template",
|
||||
default="excel/测量结果.xlsx",
|
||||
help="Measurement-result template used by --excel-output",
|
||||
)
|
||||
contour.add_argument(
|
||||
"--combined-context-pointcloud",
|
||||
default="datasets/train/train_2_10M.ply",
|
||||
help="Full point cloud to include in batch combined_contour.ply",
|
||||
)
|
||||
contour.add_argument("--no-visualize", action="store_true", help="Disable Open3D windows")
|
||||
|
||||
curvature = subparsers.add_parser("curvature", parents=[common], help="Estimate and visualize point-cloud curvature")
|
||||
curvature.add_argument("--pointcloud", "-p", default=None, help="Input PLY point cloud")
|
||||
curvature.add_argument("--radius", type=float, default=None, help="Neighborhood radius")
|
||||
curvature.add_argument("--max-nn", type=int, default=None, help="Max neighbor count")
|
||||
curvature.add_argument("--color-scale", type=float, default=None, help="Color scale")
|
||||
curvature.add_argument("--save", default=None, help="Optional colored point-cloud output path")
|
||||
curvature.add_argument("--no-visualize", action="store_true", help="Disable Open3D windows")
|
||||
|
||||
plane = subparsers.add_parser("plane", parents=[common], help="Estimate plane with RANSAC")
|
||||
plane.add_argument("--pointcloud", "-p", default=None, help="Optional input PLY point cloud")
|
||||
plane.add_argument("--threshold-mm", type=float, default=None, help="RANSAC distance threshold in mm")
|
||||
plane.add_argument("--max-iterations", type=int, default=None, help="RANSAC max iterations")
|
||||
plane.add_argument("--no-visualize", action="store_true", help="Disable Open3D windows")
|
||||
|
||||
translate_align = subparsers.add_parser(
|
||||
"translate-align",
|
||||
parents=[common],
|
||||
help="Translate-align point cloud to STEP CAD with front/tail centroids",
|
||||
)
|
||||
translate_align.add_argument("--cad", "-c", default="datasets/CAD/M02.stp", help="Reference STEP CAD file")
|
||||
translate_align.add_argument(
|
||||
"--pointcloud",
|
||||
"-p",
|
||||
default="datasets/shift_test_output/",
|
||||
help="Input PLY point cloud or a directory containing shifted_whole_pointcloud.ply",
|
||||
)
|
||||
translate_align.add_argument("--output-dir", "-o", default="results", help="Output directory")
|
||||
translate_align.add_argument("--axis", choices=["x", "y", "z"], default="x", help="Train length axis")
|
||||
translate_align.add_argument("--crop-ratio", type=float, default=0.01, help="Front/tail crop ratio")
|
||||
translate_align.add_argument("--mesh-deflection", type=float, default=0.1, help="CAD meshing deflection")
|
||||
translate_align.add_argument("--cad-sample-points", type=int, default=30000, help="Points sampled from CAD mesh")
|
||||
translate_align.add_argument("--local-sample-points", type=int, default=1000, help="Points sampled from each end")
|
||||
translate_align.add_argument("--scale-pointcloud", type=float, default=1.0, help="Scale factor applied to point cloud")
|
||||
translate_align.add_argument("--no-visualize", action="store_true", help="Disable Open3D windows")
|
||||
|
||||
full = subparsers.add_parser("full", parents=[common], help="Run configured full workflow")
|
||||
full.add_argument("--no-visualize", action="store_true", help="Disable Open3D windows")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
from pipeline import MeasurementPipeline
|
||||
|
||||
pipeline = MeasurementPipeline(config_path=args.config)
|
||||
|
||||
if args.command == "cad2mesh":
|
||||
pipeline.run_cad2mesh(cad_path=args.cad, mesh_path=args.mesh, precision=args.precision)
|
||||
elif args.command == "denoise":
|
||||
pipeline.run_denoise(pointcloud_path=args.pointcloud, noise_std=args.noise_std, no_visualize=args.no_visualize)
|
||||
elif args.command == "registration":
|
||||
pipeline.run_registration(
|
||||
pointcloud_path=args.pointcloud,
|
||||
mesh_path=args.mesh,
|
||||
voxel_size=args.voxel_size,
|
||||
roll=args.roll,
|
||||
pitch=args.pitch,
|
||||
yaw=args.yaw,
|
||||
trans_range=args.trans_range,
|
||||
no_visualize=args.no_visualize,
|
||||
)
|
||||
elif args.command == "contour":
|
||||
if args.all or args.tasks:
|
||||
pipeline.run_contour_batch(
|
||||
task_ids=args.tasks,
|
||||
all_tasks=args.all,
|
||||
data_root=args.data_root,
|
||||
pointcloud_kind=args.pointcloud_kind,
|
||||
mesh_deflection=args.mesh_deflection,
|
||||
point_chunk_size=args.point_chunk_size,
|
||||
triangle_chunk_size=args.triangle_chunk_size,
|
||||
save_path=args.save,
|
||||
excel_output_path=args.excel_output,
|
||||
result_template_path=args.result_template,
|
||||
combined_context_pointcloud_path=args.combined_context_pointcloud,
|
||||
no_visualize=args.no_visualize,
|
||||
)
|
||||
else:
|
||||
pipeline.run_contour(
|
||||
pointcloud_path=args.pointcloud,
|
||||
cad_path=args.cad,
|
||||
mesh_path=args.mesh,
|
||||
mesh_deflection=args.mesh_deflection,
|
||||
point_chunk_size=args.point_chunk_size,
|
||||
triangle_chunk_size=args.triangle_chunk_size,
|
||||
save_path=args.save,
|
||||
excel_output_path=args.excel_output,
|
||||
result_template_path=args.result_template,
|
||||
no_visualize=args.no_visualize,
|
||||
)
|
||||
elif args.command == "curvature":
|
||||
pipeline.run_curvature(
|
||||
pointcloud_path=args.pointcloud,
|
||||
radius=args.radius,
|
||||
max_nn=args.max_nn,
|
||||
color_scale=args.color_scale,
|
||||
save_path=args.save,
|
||||
no_visualize=args.no_visualize,
|
||||
)
|
||||
elif args.command == "plane":
|
||||
pipeline.run_plane(
|
||||
pointcloud_path=args.pointcloud,
|
||||
threshold_mm=args.threshold_mm,
|
||||
max_iterations=args.max_iterations,
|
||||
no_visualize=args.no_visualize,
|
||||
)
|
||||
elif args.command == "translate-align":
|
||||
pipeline.run_translate_align_pointcloud_to_cad(
|
||||
cad_path=args.cad,
|
||||
pointcloud_path=args.pointcloud,
|
||||
output_dir=args.output_dir,
|
||||
axis=args.axis,
|
||||
crop_ratio=args.crop_ratio,
|
||||
mesh_deflection=args.mesh_deflection,
|
||||
cad_sample_points=args.cad_sample_points,
|
||||
local_sample_points=args.local_sample_points,
|
||||
scale_pointcloud=args.scale_pointcloud,
|
||||
no_visualize=args.no_visualize,
|
||||
)
|
||||
elif args.command == "full":
|
||||
pipeline.run_full(no_visualize=args.no_visualize)
|
||||
else:
|
||||
parser.error(f"Unsupported command: {args.command}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
5
alg/平整度、平面度、轮廓测量v1.5/packaging/.gitignore
vendored
Normal file
5
alg/平整度、平面度、轮廓测量v1.5/packaging/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
6
alg/平整度、平面度、轮廓测量v1.5/packaging/MANIFEST.in
Normal file
6
alg/平整度、平面度、轮廓测量v1.5/packaging/MANIFEST.in
Normal file
@@ -0,0 +1,6 @@
|
||||
include README.md
|
||||
recursive-include geometry_measure *.yaml
|
||||
global-exclude __pycache__/*
|
||||
global-exclude *.py[cod]
|
||||
prune datasets
|
||||
prune archive
|
||||
139
alg/平整度、平面度、轮廓测量v1.5/packaging/README.md
Normal file
139
alg/平整度、平面度、轮廓测量v1.5/packaging/README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# geometry-measure 打包说明
|
||||
|
||||
这个目录只负责打包,不存放数据集;仅包含已接入统一 CLI 的必要归档工作流。
|
||||
|
||||
## 本地开发安装
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
python packaging/build_package.py
|
||||
pip install -e packaging/build
|
||||
geometry-measure --help
|
||||
```
|
||||
|
||||
也可以进入 `packaging/` 后执行:
|
||||
|
||||
```bash
|
||||
python build_package.py
|
||||
pip install -e build
|
||||
geometry-measure --help
|
||||
```
|
||||
|
||||
## 构建 wheel
|
||||
|
||||
需要先安装构建工具:
|
||||
|
||||
```bash
|
||||
pip install build
|
||||
```
|
||||
|
||||
然后执行:
|
||||
|
||||
```bash
|
||||
python packaging/build_package.py
|
||||
python -m build packaging/build
|
||||
```
|
||||
|
||||
构建产物会出现在:
|
||||
|
||||
```text
|
||||
packaging/build/dist/
|
||||
```
|
||||
|
||||
## 运行示例
|
||||
|
||||
从项目根目录运行:
|
||||
|
||||
```bash
|
||||
geometry-measure contour --pointcloud datasets/PointCloud/11.ply --cad datasets/CAD/11.step --no-visualize
|
||||
geometry-measure contour --cad datasets/CAD/11.step --pointcloud datasets/Noise_PointCloud/11_noisy.ply --save vis/11_contour.ply --excel-output --no-visualize
|
||||
geometry-measure registration --pointcloud datasets/Noise_PointCloud/11_noisy.ply --mesh datasets/Mesh/11.stl --no-visualize
|
||||
geometry-measure denoise --pointcloud datasets/PointCloud/11.ply --noise-std 4 --no-visualize
|
||||
```
|
||||
|
||||
`contour` 是轮廓/平整度/平面度评估的统一入口,本质上是点云到 STEP CAD 或 Mesh 的距离计算。需要保存距离热力图时传 `--save`,需要保存 Excel 时传 `--excel-output`。如需单独指定网格,可显式传入 `--mesh datasets/Mesh/11.stl`。
|
||||
`contour --excel-output` 不带值时会保存 `excel/contour.xlsx`;只传文件名时会自动放到 `excel/` 文件夹。Excel 默认读取 `excel/测量结果.xlsx` 模板,按任务号迁移对应行,表头保持模板一致,只写入本次 `测量结果` 和 `测量结论`。可用 `--result-template` 指定其它模板。
|
||||
`contour` 也支持批量运行:`--tasks 13,31,40` 执行指定任务,其中 `13` 会自动匹配 `13_1`、`13_2` 等样本;`--tasks 13_1` 只运行单个样本。`--all` 优先执行 `datasets/Mesh` 下全部样本,没有 Mesh 时退回 `datasets/CAD` 下全部 STEP;批量模式用 `--pointcloud-kind noise|clean` 选择加噪点云或干净点云。所有成功热力图会按原始坐标额外保存成一个合并 PLY:未传 `--save` 时为 `vis/combined_contour.ply`,`--save vis/contour` 时为 `vis/contour/combined_contour.ply`。合并点云默认加入 `datasets/train/train_2_10M.ply`,可用 `--combined-context-pointcloud` 切换为 `train_2_100M.ply` 或 `train_2_1T.ply`。未传 `--no-visualize` 时,合并结果也会按原始坐标在一个 Open3D 窗口展示。
|
||||
|
||||
使用配置文件:
|
||||
|
||||
```bash
|
||||
geometry-measure --config config.yaml full --no-visualize
|
||||
```
|
||||
|
||||
如果使用打包生成的默认配置,构建脚本会把默认路径改为:
|
||||
|
||||
```text
|
||||
datasets/CAD/11.step
|
||||
datasets/Mesh/11.stl
|
||||
datasets/PointCloud/11.ply
|
||||
datasets/Noise_PointCloud/11_noisy.ply
|
||||
```
|
||||
|
||||
## 数据要求
|
||||
|
||||
wheel 不包含 `datasets/`,运行时需要自己提供数据路径。建议保持以下结构:
|
||||
|
||||
```text
|
||||
datasets/
|
||||
CAD/ # STEP CAD,例如 11.step
|
||||
Mesh/ # STL/OBJ/PLY Mesh,例如 11.stl
|
||||
PointCloud/ # 原始点云 PLY,例如 11.ply
|
||||
Noise_PointCloud/ # 加噪或待配准点云 PLY,例如 11_noisy.ply
|
||||
```
|
||||
|
||||
CAD、Mesh 和点云需要处在同一坐标系和同一单位下。示例数据按毫米量级处理。`contour` 默认认为输入点云已经与参考模型对齐。
|
||||
|
||||
大规模点云可能运行较慢。可以通过配置文件调整:
|
||||
|
||||
```yaml
|
||||
evaluation:
|
||||
chunk_size: 2048
|
||||
triangle_chunk_size: 4096
|
||||
cad_mesh_deflection: 0.1
|
||||
```
|
||||
|
||||
`chunk_size` 控制点云分块,`triangle_chunk_size` 控制三角面分块,`cad_mesh_deflection` 控制 STEP 三角化精度。
|
||||
|
||||
## 输出建议
|
||||
|
||||
建议把日志和可视化产物放到项目根目录的 `vis/`:
|
||||
|
||||
```bash
|
||||
mkdir -p vis
|
||||
geometry-measure curvature --pointcloud datasets/PointCloud/11.ply --save vis/11_curvature.ply --no-visualize
|
||||
geometry-measure contour --cad datasets/CAD/11.step --pointcloud datasets/Noise_PointCloud/11_noisy.ply --save vis/11_contour.ply --excel-output 11_contour.xlsx --no-visualize
|
||||
geometry-measure contour --tasks 13,31,40 --data-root datasets --pointcloud-kind noise --save vis/contour --excel-output selected_contour.xlsx --no-visualize
|
||||
```
|
||||
|
||||
如果需要保存命令行指标,可以使用重定向:
|
||||
|
||||
```bash
|
||||
geometry-measure contour --pointcloud datasets/PointCloud/11.ply --cad datasets/CAD/11.step --no-visualize > vis/contour.log 2>&1
|
||||
```
|
||||
|
||||
## CAD 转换依赖
|
||||
|
||||
基础安装不强制安装 CAD 转换依赖。需要使用 `cad2mesh` 时,建议使用项目根目录的 `environment.yml` 创建或更新独立 Python 3.10 环境:
|
||||
|
||||
```bash
|
||||
conda env create -f environment.yml
|
||||
conda activate point-porj
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
如果环境已存在,则执行:
|
||||
|
||||
```bash
|
||||
conda env update -n point-porj -f environment.yml
|
||||
pip install git+https://github.com/AutodeskAILab/occwl
|
||||
```
|
||||
|
||||
`pythonocc-core` 在很多平台无法通过 pip 安装,请使用 conda 方式。`pythonocc-core=7.5.1` 不支持 Python 3.12,如果 base 环境被固定为 Python 3.12,请新建 Python 3.10 环境。
|
||||
|
||||
## 说明
|
||||
|
||||
- `datasets/` 不会打进 wheel,请通过命令行参数或配置文件指定本地数据路径。
|
||||
- 只有 `translate-align` 依赖的归档脚本会打进 wheel,其他 `archive/` 实验脚本不会打包。
|
||||
- 根目录源码保持原样;`packaging/build_package.py` 会生成临时标准包 `geometry_measure`。
|
||||
135
alg/平整度、平面度、轮廓测量v1.5/packaging/build_package.py
Normal file
135
alg/平整度、平面度、轮廓测量v1.5/packaging/build_package.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PACKAGING_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = PACKAGING_DIR.parent
|
||||
BUILD_DIR = PACKAGING_DIR / "build"
|
||||
PACKAGE_DIR = BUILD_DIR / "geometry_measure"
|
||||
|
||||
SOURCE_FILES = [
|
||||
"main.py",
|
||||
"pipeline.py",
|
||||
"io_handler.py",
|
||||
"config_loader.py",
|
||||
"cad_converter.py",
|
||||
"registration.py",
|
||||
"evaluation.py",
|
||||
"visualization.py",
|
||||
"config.yaml",
|
||||
]
|
||||
|
||||
SOURCE_DIRS = [
|
||||
"preprocessing",
|
||||
]
|
||||
|
||||
ARCHIVE_FILES = [
|
||||
"translate_align_pointcloud_to_cad.py",
|
||||
]
|
||||
|
||||
IMPORT_REWRITES = {
|
||||
"from pipeline import MeasurementPipeline": "from geometry_measure.pipeline import MeasurementPipeline",
|
||||
"from cad_converter import CADConverter": "from geometry_measure.cad_converter import CADConverter",
|
||||
"from config_loader import ConfigLoader": "from geometry_measure.config_loader import ConfigLoader",
|
||||
"from evaluation import Evaluator": "from geometry_measure.evaluation import Evaluator",
|
||||
"from io_handler import IOHandler": "from geometry_measure.io_handler import IOHandler",
|
||||
"from preprocessing.curvature import CurvatureEstimator": (
|
||||
"from geometry_measure.preprocessing.curvature import CurvatureEstimator"
|
||||
),
|
||||
"from preprocessing.filters import PreprocessingRunner": (
|
||||
"from geometry_measure.preprocessing.filters import PreprocessingRunner"
|
||||
),
|
||||
"from preprocessing.noise import GaussianNoiseAdder": (
|
||||
"from geometry_measure.preprocessing.noise import GaussianNoiseAdder"
|
||||
),
|
||||
"from preprocessing.plane import PlaneEstimator": "from geometry_measure.preprocessing.plane import PlaneEstimator",
|
||||
"from preprocessing.plane import PlaneResult": "from geometry_measure.preprocessing.plane import PlaneResult",
|
||||
"from registration import PointCloudRegistration": "from geometry_measure.registration import PointCloudRegistration",
|
||||
"from visualization import Visualizer": "from geometry_measure.visualization import Visualizer",
|
||||
}
|
||||
|
||||
PATH_REWRITES = {
|
||||
"CAD/11.step": "datasets/CAD/11.step",
|
||||
"Mesh/11.stl": "datasets/Mesh/11.stl",
|
||||
"PointCloud/11.ply": "datasets/PointCloud/11.ply",
|
||||
"Noise_PointCloud/11_noisy.ply": "datasets/Noise_PointCloud/11_noisy.ply",
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rebuild_dir()
|
||||
copy_packaging_metadata()
|
||||
copy_sources()
|
||||
write_package_markers()
|
||||
rewrite_python_imports()
|
||||
rewrite_default_paths()
|
||||
print(f"Build package prepared at: {BUILD_DIR}")
|
||||
print("Install with: pip install -e packaging/build")
|
||||
|
||||
|
||||
def rebuild_dir() -> None:
|
||||
if BUILD_DIR.exists():
|
||||
shutil.rmtree(BUILD_DIR)
|
||||
PACKAGE_DIR.mkdir(parents=True)
|
||||
|
||||
|
||||
def copy_packaging_metadata() -> None:
|
||||
shutil.copy2(PACKAGING_DIR / "pyproject.toml", BUILD_DIR / "pyproject.toml")
|
||||
shutil.copy2(PACKAGING_DIR / "MANIFEST.in", BUILD_DIR / "MANIFEST.in")
|
||||
shutil.copy2(PACKAGING_DIR / "README.md", BUILD_DIR / "README.md")
|
||||
|
||||
|
||||
def copy_sources() -> None:
|
||||
for relative in SOURCE_FILES:
|
||||
src = PROJECT_ROOT / relative
|
||||
if not src.exists():
|
||||
raise FileNotFoundError(f"Missing source file: {src}")
|
||||
shutil.copy2(src, PACKAGE_DIR / relative)
|
||||
|
||||
for relative in SOURCE_DIRS:
|
||||
src = PROJECT_ROOT / relative
|
||||
if not src.exists():
|
||||
raise FileNotFoundError(f"Missing source directory: {src}")
|
||||
shutil.copytree(src, PACKAGE_DIR / relative)
|
||||
|
||||
archive_dir = PACKAGE_DIR / "archive"
|
||||
archive_dir.mkdir(parents=True, exist_ok=True)
|
||||
for relative in ARCHIVE_FILES:
|
||||
src = PROJECT_ROOT / "archive" / relative
|
||||
if not src.exists():
|
||||
raise FileNotFoundError(f"Missing archive file: {src}")
|
||||
shutil.copy2(src, archive_dir / relative)
|
||||
|
||||
|
||||
def write_package_markers() -> None:
|
||||
(PACKAGE_DIR / "__init__.py").write_text(
|
||||
'"""Industrial geometry measurement toolkit."""\n\n__version__ = "0.1.0"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
preprocessing_init = PACKAGE_DIR / "preprocessing" / "__init__.py"
|
||||
preprocessing_init.write_text('"""Preprocessing tools for point-cloud workflows."""\n', encoding="utf-8")
|
||||
archive_init = PACKAGE_DIR / "archive" / "__init__.py"
|
||||
archive_init.write_text('"""Archived developer workflows exposed by the CLI."""\n', encoding="utf-8")
|
||||
|
||||
|
||||
def rewrite_python_imports() -> None:
|
||||
for path in PACKAGE_DIR.rglob("*.py"):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for old, new in IMPORT_REWRITES.items():
|
||||
text = text.replace(old, new)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def rewrite_default_paths() -> None:
|
||||
targets = [PACKAGE_DIR / "config.yaml", PACKAGE_DIR / "config_loader.py"]
|
||||
for path in targets:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for old, new in PATH_REWRITES.items():
|
||||
text = text.replace(old, new)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
40
alg/平整度、平面度、轮廓测量v1.5/packaging/pyproject.toml
Normal file
40
alg/平整度、平面度、轮廓测量v1.5/packaging/pyproject.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "geometry-measure"
|
||||
version = "0.1.0"
|
||||
description = "Industrial point cloud and mesh geometry measurement toolkit"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"numpy==2.2.2",
|
||||
"scipy",
|
||||
"open3d==0.19.0",
|
||||
"trimesh==4.5.3",
|
||||
"torch",
|
||||
"torchvision",
|
||||
"torchaudio",
|
||||
"PyYAML",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
cad = [
|
||||
"occwl @ git+https://github.com/AutodeskAILab/occwl",
|
||||
]
|
||||
build = [
|
||||
"build",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
geometry-measure = "geometry_measure.main:main"
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["geometry_measure*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
geometry_measure = ["config.yaml"]
|
||||
1269
alg/平整度、平面度、轮廓测量v1.5/pipeline.py
Normal file
1269
alg/平整度、平面度、轮廓测量v1.5/pipeline.py
Normal file
File diff suppressed because it is too large
Load Diff
39
alg/平整度、平面度、轮廓测量v1.5/preprocessing/curvature.py
Normal file
39
alg/平整度、平面度、轮廓测量v1.5/preprocessing/curvature.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
|
||||
|
||||
class CurvatureEstimator:
|
||||
"""Estimate local point-cloud curvature using neighborhood covariance."""
|
||||
|
||||
def compute(self, pointcloud: o3d.geometry.PointCloud, radius: float = 2.0, max_nn: int = 30) -> np.ndarray:
|
||||
pointcloud.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=max_nn))
|
||||
points = np.asarray(pointcloud.points)
|
||||
kdtree = o3d.geometry.KDTreeFlann(pointcloud)
|
||||
curvatures = np.zeros(len(points), dtype=np.float64)
|
||||
|
||||
for i, point in enumerate(points):
|
||||
_, indices, _ = kdtree.search_hybrid_vector_3d(point, radius, max_nn)
|
||||
if len(indices) < 3:
|
||||
curvatures[i] = 1.0
|
||||
continue
|
||||
|
||||
neighbors = points[indices]
|
||||
cov = np.cov((neighbors - neighbors.mean(axis=0)).T)
|
||||
eigenvalues = np.linalg.eigvalsh(cov)
|
||||
denom = eigenvalues.sum()
|
||||
curvatures[i] = float(eigenvalues[0] / denom) if denom > 0 else 0.0
|
||||
|
||||
return curvatures
|
||||
|
||||
def colorize(self, pointcloud: o3d.geometry.PointCloud, curvatures: np.ndarray, color_scale: float = 3.0) -> o3d.geometry.PointCloud:
|
||||
pcd = copy.deepcopy(pointcloud)
|
||||
normalized = curvatures / (np.max(curvatures) + 1e-12)
|
||||
normalized = np.clip(normalized * color_scale, 0, 1)
|
||||
colors = np.zeros((len(normalized), 3))
|
||||
colors[:, 0] = normalized
|
||||
colors[:, 1] = 1 - normalized
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
return pcd
|
||||
190
alg/平整度、平面度、轮廓测量v1.5/preprocessing/filters.py
Normal file
190
alg/平整度、平面度、轮廓测量v1.5/preprocessing/filters.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilterResult:
|
||||
name: str
|
||||
pointcloud: o3d.geometry.PointCloud
|
||||
metrics: dict[str, Any]
|
||||
outliers: o3d.geometry.PointCloud | None = None
|
||||
|
||||
|
||||
def _base_metrics(original_count: int, filtered_count: int, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
removed_count = original_count - filtered_count
|
||||
return {
|
||||
"method": method,
|
||||
"original_points": original_count,
|
||||
"filtered_points": filtered_count,
|
||||
"removed_points": removed_count,
|
||||
"keep_ratio": filtered_count / original_count * 100 if original_count > 0 else 0,
|
||||
"params": params,
|
||||
}
|
||||
|
||||
|
||||
class SORFilter:
|
||||
def apply(self, pointcloud: o3d.geometry.PointCloud, nb_neighbors: int = 20, std_ratio: float = 2.0) -> FilterResult:
|
||||
_, indices = pointcloud.remove_statistical_outlier(nb_neighbors=nb_neighbors, std_ratio=std_ratio)
|
||||
inliers = pointcloud.select_by_index(indices)
|
||||
outliers = pointcloud.select_by_index(indices, invert=True)
|
||||
metrics = _base_metrics(len(pointcloud.points), len(inliers.points), "SOR", {
|
||||
"nb_neighbors": nb_neighbors,
|
||||
"std_ratio": std_ratio,
|
||||
})
|
||||
return FilterResult("SOR", inliers, metrics, outliers)
|
||||
|
||||
|
||||
class RORFilter:
|
||||
def apply(self, pointcloud: o3d.geometry.PointCloud, nb_points: int = 5, radius: float = 20.0) -> FilterResult:
|
||||
_, indices = pointcloud.remove_radius_outlier(nb_points=nb_points, radius=radius)
|
||||
inliers = pointcloud.select_by_index(indices)
|
||||
outliers = pointcloud.select_by_index(indices, invert=True)
|
||||
metrics = _base_metrics(len(pointcloud.points), len(inliers.points), "ROR", {
|
||||
"nb_points": nb_points,
|
||||
"radius": radius,
|
||||
})
|
||||
return FilterResult("ROR", inliers, metrics, outliers)
|
||||
|
||||
|
||||
class PassThroughFilter:
|
||||
def apply(
|
||||
self,
|
||||
pointcloud: o3d.geometry.PointCloud,
|
||||
axis: str = "z",
|
||||
min_val: float | None = None,
|
||||
max_val: float | None = None,
|
||||
) -> FilterResult:
|
||||
axis_map = {"x": 0, "y": 1, "z": 2}
|
||||
if axis not in axis_map:
|
||||
raise ValueError("axis must be one of: x, y, z")
|
||||
|
||||
points = np.asarray(pointcloud.points)
|
||||
idx = axis_map[axis]
|
||||
if min_val is None:
|
||||
min_val = float(points[:, idx].min())
|
||||
if max_val is None:
|
||||
max_val = float(points[:, idx].max())
|
||||
|
||||
mask = (points[:, idx] >= min_val) & (points[:, idx] <= max_val)
|
||||
filtered = o3d.geometry.PointCloud()
|
||||
filtered.points = o3d.utility.Vector3dVector(points[mask])
|
||||
if pointcloud.has_colors():
|
||||
colors = np.asarray(pointcloud.colors)
|
||||
filtered.colors = o3d.utility.Vector3dVector(colors[mask])
|
||||
|
||||
metrics = _base_metrics(len(pointcloud.points), len(filtered.points), "PF", {
|
||||
"axis": axis,
|
||||
"min_val": min_val,
|
||||
"max_val": max_val,
|
||||
})
|
||||
return FilterResult("PF", filtered, metrics)
|
||||
|
||||
|
||||
class VoxelGridFilter:
|
||||
def apply(self, pointcloud: o3d.geometry.PointCloud, leaf_size: float = 0.05) -> FilterResult:
|
||||
filtered = pointcloud.voxel_down_sample(leaf_size)
|
||||
metrics = _base_metrics(len(pointcloud.points), len(filtered.points), "VGF", {"leaf_size": leaf_size})
|
||||
metrics["downsample_rate"] = (
|
||||
(len(pointcloud.points) - len(filtered.points)) / len(pointcloud.points) * 100
|
||||
if len(pointcloud.points) > 0
|
||||
else 0
|
||||
)
|
||||
metrics["compression_ratio"] = len(pointcloud.points) / len(filtered.points) if len(filtered.points) > 0 else 0
|
||||
return FilterResult("VGF", filtered, metrics)
|
||||
|
||||
|
||||
class BilateralFilter:
|
||||
def apply(self, pointcloud: o3d.geometry.PointCloud, sigma_s: float = 0.01, sigma_r: float = 0.1) -> FilterResult:
|
||||
if len(pointcloud.points) == 0:
|
||||
metrics = _base_metrics(0, 0, "BF", {"sigma_s": sigma_s, "sigma_r": sigma_r})
|
||||
return FilterResult("BF", copy.deepcopy(pointcloud), metrics)
|
||||
|
||||
pcd = copy.deepcopy(pointcloud)
|
||||
if not pcd.has_normals():
|
||||
pcd.estimate_normals()
|
||||
|
||||
points = np.asarray(pcd.points)
|
||||
normals = np.asarray(pcd.normals)
|
||||
tree = KDTree(points)
|
||||
filtered_points = np.zeros_like(points)
|
||||
|
||||
for i, point in enumerate(points):
|
||||
indices = tree.query_ball_point(point, r=2 * sigma_s)
|
||||
if len(indices) <= 1:
|
||||
filtered_points[i] = point
|
||||
continue
|
||||
|
||||
neighbor_points = points[indices]
|
||||
neighbor_normals = normals[indices]
|
||||
spatial_dist = np.linalg.norm(neighbor_points - point, axis=1)
|
||||
spatial_weight = np.exp(-(spatial_dist ** 2) / (2 * sigma_s ** 2))
|
||||
normal_dist = np.linalg.norm(neighbor_normals - normals[i], axis=1)
|
||||
range_weight = np.exp(-(normal_dist ** 2) / (2 * sigma_r ** 2))
|
||||
weights = spatial_weight * range_weight
|
||||
if weights.sum() > 0:
|
||||
weights = weights / weights.sum()
|
||||
filtered_points[i] = np.sum(neighbor_points.T * weights, axis=1)
|
||||
|
||||
filtered = o3d.geometry.PointCloud()
|
||||
filtered.points = o3d.utility.Vector3dVector(filtered_points)
|
||||
metrics = _base_metrics(len(pointcloud.points), len(filtered.points), "BF", {
|
||||
"sigma_s": sigma_s,
|
||||
"sigma_r": sigma_r,
|
||||
})
|
||||
return FilterResult("BF", filtered, metrics)
|
||||
|
||||
|
||||
class PreprocessingRunner:
|
||||
"""Run configured filters in sequence or independently."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.filters = {
|
||||
"SOR": SORFilter(),
|
||||
"ROR": RORFilter(),
|
||||
"PF": PassThroughFilter(),
|
||||
"VGF": VoxelGridFilter(),
|
||||
"BF": BilateralFilter(),
|
||||
}
|
||||
|
||||
def run(self, pointcloud: o3d.geometry.PointCloud, config: dict[str, Any]) -> dict[str, FilterResult]:
|
||||
results: dict[str, FilterResult] = {}
|
||||
for method in config.get("methods", ["SOR"]):
|
||||
name = method.upper()
|
||||
if name not in self.filters:
|
||||
raise ValueError(f"Unsupported filter method: {method}")
|
||||
if name == "SOR":
|
||||
result = self.filters[name].apply(
|
||||
pointcloud,
|
||||
nb_neighbors=int(config.get("sor_neighbors", 20)),
|
||||
std_ratio=float(config.get("sor_std_ratio", 2.0)),
|
||||
)
|
||||
elif name == "ROR":
|
||||
result = self.filters[name].apply(
|
||||
pointcloud,
|
||||
nb_points=int(config.get("ror_points", 5)),
|
||||
radius=float(config.get("ror_radius", 20.0)),
|
||||
)
|
||||
elif name == "PF":
|
||||
result = self.filters[name].apply(
|
||||
pointcloud,
|
||||
axis=str(config.get("pass_axis", "z")),
|
||||
min_val=config.get("pass_min"),
|
||||
max_val=config.get("pass_max"),
|
||||
)
|
||||
elif name == "VGF":
|
||||
result = self.filters[name].apply(pointcloud, leaf_size=float(config.get("voxel_size", 0.05)))
|
||||
else:
|
||||
result = self.filters[name].apply(
|
||||
pointcloud,
|
||||
sigma_s=float(config.get("bf_sigma_s", 0.01)),
|
||||
sigma_r=float(config.get("bf_sigma_r", 0.1)),
|
||||
)
|
||||
results[name] = result
|
||||
return results
|
||||
29
alg/平整度、平面度、轮廓测量v1.5/preprocessing/noise.py
Normal file
29
alg/平整度、平面度、轮廓测量v1.5/preprocessing/noise.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
|
||||
|
||||
class GaussianNoiseAdder:
|
||||
"""Add Gaussian noise to an Open3D point cloud."""
|
||||
|
||||
def __init__(self, noise_std: float = 4.0, seed: int | None = None) -> None:
|
||||
self.noise_std = noise_std
|
||||
self.seed = seed
|
||||
|
||||
def apply(self, pointcloud: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud:
|
||||
noisy = copy.deepcopy(pointcloud)
|
||||
points = np.asarray(noisy.points)
|
||||
rng = np.random.default_rng(self.seed)
|
||||
noise = rng.normal(0, self.noise_std, points.shape)
|
||||
noisy.points = o3d.utility.Vector3dVector(points + noise)
|
||||
return noisy
|
||||
|
||||
def noise_level(self, clean: o3d.geometry.PointCloud, noisy: o3d.geometry.PointCloud) -> float:
|
||||
clean_points = np.asarray(clean.points)
|
||||
noisy_points = np.asarray(noisy.points)
|
||||
if clean_points.shape != noisy_points.shape:
|
||||
raise ValueError("Clean and noisy point clouds must have the same point count.")
|
||||
return float(np.mean(np.linalg.norm(noisy_points - clean_points, axis=1)))
|
||||
62
alg/平整度、平面度、轮廓测量v1.5/preprocessing/plane.py
Normal file
62
alg/平整度、平面度、轮廓测量v1.5/preprocessing/plane.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlaneResult:
|
||||
model: tuple[float, float, float, float]
|
||||
inliers: list[int]
|
||||
inlier_ratio: float
|
||||
normal_angle_deg: float
|
||||
rmse: float
|
||||
pointcloud: o3d.geometry.PointCloud
|
||||
|
||||
|
||||
class PlaneEstimator:
|
||||
"""Generate noisy planes and estimate planes using Open3D RANSAC."""
|
||||
|
||||
def create_square_plane_with_noise(
|
||||
self,
|
||||
size_mm: float = 1000.0,
|
||||
noise_std_mm: float = 0.5,
|
||||
num_points: int = 10000,
|
||||
) -> o3d.geometry.PointCloud:
|
||||
half_size_m = size_mm / 2000.0
|
||||
noise_std_m = noise_std_mm / 1000.0
|
||||
xy = np.random.uniform(-half_size_m, half_size_m, size=(num_points, 2))
|
||||
z = np.random.normal(0, noise_std_m, size=(num_points, 1))
|
||||
points = np.hstack([xy, z])
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points)
|
||||
return pcd
|
||||
|
||||
def estimate(
|
||||
self,
|
||||
pointcloud: o3d.geometry.PointCloud,
|
||||
threshold_mm: float = 0.5,
|
||||
max_iterations: int = 2000,
|
||||
) -> PlaneResult:
|
||||
threshold_m = threshold_mm / 1000.0
|
||||
plane_model, inliers = pointcloud.segment_plane(
|
||||
distance_threshold=threshold_m,
|
||||
ransac_n=3,
|
||||
num_iterations=max_iterations,
|
||||
)
|
||||
|
||||
a, b, c, d = plane_model
|
||||
normal = np.array([a, b, c], dtype=np.float64)
|
||||
normal = normal / (np.linalg.norm(normal) + 1e-12)
|
||||
ideal = np.array([0.0, 0.0, 1.0])
|
||||
angle = float(np.degrees(np.arccos(np.clip(abs(np.dot(normal, ideal)), -1.0, 1.0))))
|
||||
|
||||
points = np.asarray(pointcloud.points)
|
||||
inlier_points = points[inliers]
|
||||
distances = np.abs(np.dot(inlier_points, np.array([a, b, c])) + d)
|
||||
rmse = float(np.sqrt(np.mean(distances ** 2))) if len(distances) else 0.0
|
||||
ratio = len(inliers) / len(points) * 100 if len(points) else 0.0
|
||||
|
||||
return PlaneResult(tuple(float(x) for x in plane_model), list(inliers), ratio, angle, rmse, pointcloud)
|
||||
110
alg/平整度、平面度、轮廓测量v1.5/registration.py
Normal file
110
alg/平整度、平面度、轮廓测量v1.5/registration.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegistrationResult:
|
||||
transformation: np.ndarray
|
||||
fitness: float
|
||||
rmse: float
|
||||
target: o3d.geometry.PointCloud
|
||||
recovered: o3d.geometry.PointCloud
|
||||
|
||||
|
||||
class PointCloudRegistration:
|
||||
"""RANSAC + ICP point-cloud registration."""
|
||||
|
||||
def rotation_matrix(self, roll: float = 10.0, pitch: float = 10.0, yaw: float = 10.0) -> np.ndarray:
|
||||
return Rotation.from_euler("xyz", [roll, pitch, yaw], degrees=True).as_matrix()
|
||||
|
||||
def translation_vector(self, trans_range: float = 0.0, seed: int | None = None) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
return rng.uniform(-trans_range, trans_range, 3)
|
||||
|
||||
def transform_point_cloud(
|
||||
self,
|
||||
pointcloud: o3d.geometry.PointCloud,
|
||||
rotation: np.ndarray,
|
||||
translation: np.ndarray,
|
||||
) -> o3d.geometry.PointCloud:
|
||||
transformed = copy.deepcopy(pointcloud)
|
||||
points = np.asarray(transformed.points)
|
||||
transformed.points = o3d.utility.Vector3dVector((rotation @ points.T).T + translation)
|
||||
return transformed
|
||||
|
||||
def register(
|
||||
self,
|
||||
source: o3d.geometry.PointCloud,
|
||||
target: o3d.geometry.PointCloud,
|
||||
voxel_size: float = 10.0,
|
||||
) -> tuple[np.ndarray, float, float]:
|
||||
source_down = source.voxel_down_sample(voxel_size)
|
||||
target_down = target.voxel_down_sample(voxel_size)
|
||||
|
||||
normal_radius = voxel_size * 2
|
||||
source_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=normal_radius, max_nn=30))
|
||||
target_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=normal_radius, max_nn=30))
|
||||
|
||||
feature_radius = voxel_size * 5
|
||||
source_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
source_down,
|
||||
o3d.geometry.KDTreeSearchParamHybrid(radius=feature_radius, max_nn=100),
|
||||
)
|
||||
target_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
target_down,
|
||||
o3d.geometry.KDTreeSearchParamHybrid(radius=feature_radius, max_nn=100),
|
||||
)
|
||||
|
||||
distance_threshold = voxel_size * 1.5
|
||||
result_ransac = o3d.pipelines.registration.registration_ransac_based_on_feature_matching(
|
||||
source_down,
|
||||
target_down,
|
||||
source_fpfh,
|
||||
target_fpfh,
|
||||
mutual_filter=True,
|
||||
max_correspondence_distance=distance_threshold,
|
||||
estimation_method=o3d.pipelines.registration.TransformationEstimationPointToPoint(),
|
||||
ransac_n=3,
|
||||
checkers=[
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnEdgeLength(0.9),
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnDistance(distance_threshold),
|
||||
],
|
||||
criteria=o3d.pipelines.registration.RANSACConvergenceCriteria(100000, 0.999),
|
||||
)
|
||||
|
||||
source.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
target.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
|
||||
result_icp = o3d.pipelines.registration.registration_icp(
|
||||
source,
|
||||
target,
|
||||
distance_threshold / 2,
|
||||
result_ransac.transformation,
|
||||
o3d.pipelines.registration.TransformationEstimationPointToPlane(),
|
||||
o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=200),
|
||||
)
|
||||
return result_icp.transformation, float(result_icp.fitness), float(result_icp.inlier_rmse)
|
||||
|
||||
def run_simulated_registration(
|
||||
self,
|
||||
source: o3d.geometry.PointCloud,
|
||||
voxel_size: float = 10.0,
|
||||
roll: float = 10.0,
|
||||
pitch: float = 10.0,
|
||||
yaw: float = 10.0,
|
||||
trans_range: float = 0.0,
|
||||
random_seed: int | None = None,
|
||||
) -> RegistrationResult:
|
||||
rotation = self.rotation_matrix(roll, pitch, yaw)
|
||||
translation = self.translation_vector(trans_range, random_seed)
|
||||
target = self.transform_point_cloud(source, rotation, translation)
|
||||
transformation, fitness, rmse = self.register(source, target, voxel_size)
|
||||
recovered = copy.deepcopy(target)
|
||||
recovered.transform(np.linalg.inv(transformation))
|
||||
return RegistrationResult(transformation, fitness, rmse, target, recovered)
|
||||
16
alg/平整度、平面度、轮廓测量v1.5/requirements.txt
Normal file
16
alg/平整度、平面度、轮廓测量v1.5/requirements.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
# Python 3.10 is recommended.
|
||||
# Use environment.yml for conda-only dependencies such as pythonocc-core.
|
||||
--extra-index-url https://download.pytorch.org/whl/cu118
|
||||
|
||||
numpy==2.2.2
|
||||
scipy
|
||||
open3d==0.19.0
|
||||
trimesh==4.5.3
|
||||
torch
|
||||
torchvision
|
||||
torchaudio
|
||||
PyYAML
|
||||
|
||||
# CAD STEP/STL conversion:
|
||||
# conda env update -n point-porj -f environment.yml
|
||||
# pip install git+https://github.com/AutodeskAILab/occwl
|
||||
151
alg/平整度、平面度、轮廓测量v1.5/visualization.py
Normal file
151
alg/平整度、平面度、轮廓测量v1.5/visualization.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
|
||||
from preprocessing.plane import PlaneResult
|
||||
|
||||
|
||||
class Visualizer:
|
||||
"""Open3D visualization utilities."""
|
||||
|
||||
def show_registration(
|
||||
self,
|
||||
source: o3d.geometry.PointCloud,
|
||||
target: o3d.geometry.PointCloud,
|
||||
recovered: o3d.geometry.PointCloud,
|
||||
) -> None:
|
||||
source_colored = copy.deepcopy(source)
|
||||
target_colored = copy.deepcopy(target)
|
||||
recovered_colored = copy.deepcopy(recovered)
|
||||
source_colored.paint_uniform_color([0, 0, 1])
|
||||
target_colored.paint_uniform_color([0, 1, 0])
|
||||
recovered_colored.paint_uniform_color([1, 0, 0])
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[source_colored, target_colored],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024,
|
||||
height=768,
|
||||
)
|
||||
o3d.visualization.draw_geometries(
|
||||
[recovered_colored, target_colored],
|
||||
window_name="After Registration (Red: Recovered, Green: Target)",
|
||||
width=1024,
|
||||
height=768,
|
||||
)
|
||||
|
||||
def show_denoise(
|
||||
self,
|
||||
clean: o3d.geometry.PointCloud,
|
||||
noisy: o3d.geometry.PointCloud,
|
||||
filtered: o3d.geometry.PointCloud | None = None,
|
||||
filter_name: str = "Filtered",
|
||||
) -> None:
|
||||
clean_colored = copy.deepcopy(clean)
|
||||
noisy_colored = copy.deepcopy(noisy)
|
||||
clean_colored.paint_uniform_color([0, 0, 1])
|
||||
noisy_colored.paint_uniform_color([1.0, 0.7, 0.5])
|
||||
o3d.visualization.draw_geometries(
|
||||
[clean_colored, noisy_colored],
|
||||
window_name=f"Clean (Blue, {len(clean.points)}) vs Noisy (Orange, {len(noisy.points)})",
|
||||
)
|
||||
|
||||
if filtered is not None:
|
||||
filtered_colored = copy.deepcopy(filtered)
|
||||
filtered_colored.paint_uniform_color([0, 1, 0])
|
||||
o3d.visualization.draw_geometries(
|
||||
[clean_colored, filtered_colored],
|
||||
window_name=f"{filter_name}: Clean (Blue) vs Filtered (Green)",
|
||||
)
|
||||
|
||||
def show_curvature(self, pointcloud: o3d.geometry.PointCloud, window_name: str = "Curvature Visualization") -> None:
|
||||
o3d.visualization.draw_geometries([pointcloud], window_name=window_name)
|
||||
|
||||
def show_combined_pointclouds(
|
||||
self,
|
||||
pointclouds: list[o3d.geometry.PointCloud],
|
||||
labels: list[str] | None = None,
|
||||
window_name: str = "Combined Visualization",
|
||||
arrange_grid: bool = False,
|
||||
) -> None:
|
||||
combined = self.combine_pointclouds(pointclouds, arrange_grid=arrange_grid)
|
||||
if len(combined.points) == 0:
|
||||
return
|
||||
|
||||
title = window_name
|
||||
if labels:
|
||||
title = f"{window_name} ({len(labels)} results)"
|
||||
o3d.visualization.draw_geometries([combined], window_name=title, width=1280, height=900)
|
||||
|
||||
def combine_pointclouds(
|
||||
self,
|
||||
pointclouds: list[o3d.geometry.PointCloud],
|
||||
arrange_grid: bool = False,
|
||||
) -> o3d.geometry.PointCloud:
|
||||
geometries = [
|
||||
copy.deepcopy(pointcloud)
|
||||
for pointcloud in pointclouds
|
||||
if pointcloud is not None and len(pointcloud.points) > 0
|
||||
]
|
||||
if not geometries:
|
||||
return o3d.geometry.PointCloud()
|
||||
|
||||
for geometry in geometries:
|
||||
if not geometry.has_colors():
|
||||
geometry.paint_uniform_color([0.62, 0.62, 0.62])
|
||||
|
||||
if arrange_grid and len(geometries) > 1:
|
||||
extents = [
|
||||
np.asarray(geometry.get_max_bound()) - np.asarray(geometry.get_min_bound())
|
||||
for geometry in geometries
|
||||
]
|
||||
max_extent = max(float(np.max(extent)) for extent in extents)
|
||||
spacing = max(max_extent * 1.35, 1.0)
|
||||
cols = math.ceil(math.sqrt(len(geometries)))
|
||||
|
||||
for index, geometry in enumerate(geometries):
|
||||
row, col = divmod(index, cols)
|
||||
center = (np.asarray(geometry.get_min_bound()) + np.asarray(geometry.get_max_bound())) / 2.0
|
||||
target_center = np.array([col * spacing, -row * spacing, 0.0])
|
||||
geometry.translate(target_center - center)
|
||||
|
||||
combined = o3d.geometry.PointCloud()
|
||||
for geometry in geometries:
|
||||
combined += geometry
|
||||
return combined
|
||||
|
||||
def show_plane(self, plane_result: PlaneResult) -> None:
|
||||
pointcloud = copy.deepcopy(plane_result.pointcloud)
|
||||
pointcloud.paint_uniform_color([0.2, 0.4, 1.0])
|
||||
inlier_cloud = pointcloud.select_by_index(plane_result.inliers)
|
||||
inlier_cloud.paint_uniform_color([1.0, 0.2, 0.2])
|
||||
|
||||
plane_mesh = self._create_plane_mesh(plane_result.model)
|
||||
o3d.visualization.draw_geometries(
|
||||
[pointcloud, inlier_cloud, plane_mesh],
|
||||
window_name="Plane Fitting",
|
||||
width=1024,
|
||||
height=768,
|
||||
)
|
||||
|
||||
def _create_plane_mesh(self, plane_model: tuple[float, float, float, float]) -> o3d.geometry.TriangleMesh:
|
||||
a, b, c, d = plane_model
|
||||
size = 1.0
|
||||
xx, yy = np.meshgrid(np.linspace(-size, size, 2), np.linspace(-size, size, 2))
|
||||
if abs(c) < 1e-12:
|
||||
zz = np.zeros_like(xx)
|
||||
else:
|
||||
zz = -(a * xx + b * yy + d) / c
|
||||
|
||||
vertices = np.column_stack([xx.ravel(), yy.ravel(), zz.ravel()])
|
||||
triangles = np.array([[0, 1, 2], [1, 3, 2]])
|
||||
mesh = o3d.geometry.TriangleMesh()
|
||||
mesh.vertices = o3d.utility.Vector3dVector(vertices)
|
||||
mesh.triangles = o3d.utility.Vector3iVector(triangles)
|
||||
mesh.paint_uniform_color([0.1, 0.9, 0.1])
|
||||
mesh.compute_vertex_normals()
|
||||
return mesh
|
||||
Reference in New Issue
Block a user