3D 点云体积测量:货物堆方量检测实战
1. 堆方量概念
堆方量(Bulk Volume):
├── 定义:松散货物堆放的体积
├── 应用:矿石、粮食、煤炭、砂石
├── 测量:传统人工测量 vs 视觉测量
└── 精度要求:±3%(商业结算)
视觉测量优势:
├── 非接触式:不影响堆放
├── 实时性:秒级测量
├── 高精度:±1-2%
└── 自动化:减少人工
2. 深度相机点云采集
#!/usr/bin/env python3
"""pointcloud_capture.py - 点云采集"""
import cv2
import numpy as np
import open3d as o3d
import pyrealsense2 as rs
class PointCloudCapture:
"""点云采集器"""
def __init__(self):
self.pipeline = rs.pipeline()
config = rs.config()
config.enable_stream(rs.stream.depth, 1280, 720, rs.format.z16, 30)
config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30)
self.pipeline.start(config)
align_to = rs.stream.color
self.align = rs.align(align_to)
# 获取内参
profile = self.pipeline.get_active_profile()
self.intrinsics = profile.get_stream(rs.stream.color).as_video_stream_profile().get_intrinsics()
def capture_pointcloud(self):
"""采集一帧点云"""
frames = self.pipeline.wait_for_frames()
aligned = self.align.process(frames)
depth_frame = aligned.get_depth_frame()
color_frame = aligned.get_color_frame()
# 创建点云
pc = rs.pointcloud()
pc.map_to(color_frame)
points = pc.calculate(depth_frame)
# 转 Open3D 格式
vertices = np.asanyarray(points.get_vertices()).view(np.float32).reshape(-1, 3)
colors = np.asanyarray(color_frame.get_data()).reshape(-1, 3) / 255.0
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(vertices)
pcd.colors = o3d.utility.Vector3dVector(colors)
return pcd
def capture_multi_view(self, num_views=4):
"""多视角采集"""
pcds = []
for i in range(num_views):
print(f"采集视角 {i+1}/{num_views}...")
input("调整相机位置后按 Enter...")
pcd = self.capture_pointcloud()
pcds.append(pcd)
# 多视角融合
merged = self._merge_pointclouds(pcds)
return merged
def _merge_pointclouds(self, pcds):
"""多视角点云融合"""
if len(pcds) == 1:
return pcds[0]
# ICP 配准
merged = pcds[0]
for i in range(1, len(pcds)):
reg = o3d.pipelines.registration.registration_icp(
pcds[i], merged, 0.02,
np.eye(4),
o3d.pipelines.registration.TransformationEstimationPointToPoint()
)
pcds[i].transform(reg.transformation)
merged += pcds[i]
# 降采样
merged = merged.voxel_down_sample(voxel_size=0.01)
return merged
if __name__ == "__main__":
capture = PointCloudCapture()
pcd = capture.capture_pointcloud()
# 保存点云
o3d.io.write_point_cloud("stockpile.pcd", pcd)
print(f"点云点数: {len(pcd.points)}")
3. 体积计算算法
#!/usr/bin/env python3
"""volume_calc.py - 点云体积计算"""
import numpy as np
import open3d as o3d
class VolumeCalculator:
"""体积计算器"""
def __init__(self):
pass
def calculate_volume_convex_hull(self, pcd):
"""凸包体积"""
hull, _ = pcd.compute_convex_hull()
volume = hull.get_volume()
return volume
def calculate_volume_alpha_shape(self, pcd, alpha=0.05):
"""Alpha Shape 体积(更精确)"""
mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_alpha_shape(pcd, alpha)
volume = mesh.get_volume()
return volume
def calculate_volume_voxel(self, pcd, voxel_size=0.01):
"""体素化体积"""
# 体素化
voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(pcd, voxel_size)
voxels = voxel_grid.get_voxels()
# 体积 = 体素数 × 体素体积
voxel_volume = voxel_size ** 3
total_volume = len(voxels) * voxel_volume
return total_volume
def calculate_volume_height_map(self, pcd, grid_size=0.05):
"""高度图体积"""
points = np.asarray(pcd.points)
# 创建网格
x_min, y_min = points[:, 0].min(), points[:, 1].min()
x_max, y_max = points[:, 0].max(), points[:, 1].max()
x_bins = np.arange(x_min, x_max, grid_size)
y_bins = np.arange(y_min, y_max, grid_size)
# 计算每个网格的最大高度
height_map = np.zeros((len(y_bins), len(x_bins)))
for point in points:
x_idx = int((point[0] - x_min) / grid_size)
y_idx = int((point[1] - y_min) / grid_size)
if 0 <= x_idx < len(x_bins) and 0 <= y_idx < len(y_bins):
height_map[y_idx, x_idx] = max(height_map[y_idx, x_idx], point[2])
# 计算底面高度(取最小值或已知值)
base_height = points[:, 2].min()
# 体积 = Σ(高度差 × 网格面积)
height_diff = height_map - base_height
volume = np.sum(height_diff[height_diff > 0]) * grid_size ** 2
return volume, height_map
def calculate_stockpile_volume(self, pcd, base_plane=None):
"""计算堆料体积"""
points = np.asarray(pcd.points)
if base_plane is None:
# 自动检测底面(RANSAC 平面拟合)
plane_model, inliers = pcd.segment_plane(
distance_threshold=0.02,
ransac_n=3,
num_iterations=1000
)
base_height = np.mean(points[inliers, 2])
else:
base_height = base_plane
# 过滤底面以上的点
above_base = points[points[:, 2] > base_height + 0.01]
# 计算体积(高度图法)
pcd_above = o3d.geometry.PointCloud()
pcd_above.points = o3d.utility.Vector3dVector(above_base)
volume, height_map = self.calculate_volume_height_map(pcd_above)
return {
'volume': volume,
'base_height': base_height,
'max_height': above_base[:, 2].max() - base_height,
'height_map': height_map,
}
if __name__ == "__main__":
# 加载点云
pcd = o3d.io.read_point_cloud("stockpile.pcd")
calc = VolumeCalculator()
# 方法 1:凸包
vol_convex = calc.calculate_volume_convex_hull(pcd)
print(f"凸包体积: {vol_convex:.2f} m³")
# 方法 2:高度图
result = calc.calculate_stockpile_volume(pcd)
print(f"堆料体积: {result['volume']:.2f} m³")
print(f"最大高度: {result['max_height']:.2f} m")
4. 精度校准
#!/usr/bin/env python3
"""calibration.py - 体积测量精度校准"""
import numpy as np
class VolumeCalibrator:
"""体积测量校准器"""
def __init__(self):
self.scale_factor = 1.0
self.bias = 0.0
def calibrate(self, measured_volumes, reference_volumes):
"""校准"""
measured = np.array(measured_volumes)
reference = np.array(reference_volumes)
# 线性回归: reference = scale * measured + bias
A = np.vstack([measured, np.ones(len(measured))]).T
self.scale_factor, self.bias = np.linalg.lstsq(A, reference, rcond=None)[0]
# 计算校准后误差
calibrated = self.scale_factor * measured + self.bias
errors = np.abs(calibrated - reference) / reference * 100
print(f"校准系数: scale={self.scale_factor:.4f}, bias={self.bias:.4f}")
print(f"校准前平均误差: {np.mean(np.abs(measured - reference) / reference * 100):.2f}%")
print(f"校准后平均误差: {np.mean(errors):.2f}%")
def apply_calibration(self, measured_volume):
"""应用校准"""
return self.scale_factor * measured_volume + self.bias
# 校准示例
calibrator = VolumeCalibrator()
# 已知体积的标定物
calibrator.calibrate(
measured_volumes=[1.02, 2.05, 3.08, 4.12, 5.15],
reference_volumes=[1.00, 2.00, 3.00, 4.00, 5.00]
)
5. 实时堆方量监测
#!/usr/bin/env python3
"""realtime_stockpile.py - 实时堆方量监测"""
import cv2
import numpy as np
import time
class StockpileMonitor:
"""堆方量监测器"""
def __init__(self, camera, calculator):
self.camera = camera
self.calculator = calculator
self.history = []
def measure_once(self):
"""单次测量"""
pcd = self.camera.capture_pointcloud()
result = self.calculator.calculate_stockpile_volume(pcd)
self.history.append({
'time': time.time(),
'volume': result['volume'],
'max_height': result['max_height'],
})
return result
def get_trend(self, window=10):
"""获取趋势"""
if len(self.history) < 2:
return 0
recent = self.history[-window:]
volumes = [h['volume'] for h in recent]
# 计算变化率
if len(volumes) >= 2:
rate = (volumes[-1] - volumes[0]) / len(volumes)
return rate
return 0
def run(self, interval=5):
"""持续监测"""
print("开始堆方量监测...")
while True:
result = self.measure_once()
trend = self.get_trend()
print(f"体积: {result['volume']:.2f} m³ | "
f"最大高度: {result['max_height']:.2f} m | "
f"变化率: {trend:+.2f} m³/次")
time.sleep(interval)
if __name__ == "__main__":
camera = PointCloudCapture()
calculator = VolumeCalculator()
monitor = StockpileMonitor(camera, calculator)
monitor.run()
总结
| 方法 |
精度 |
速度 |
适用场景 |
| 凸包 |
低 |
快 |
凸形物体 |
| Alpha Shape |
中 |
中 |
不规则形状 |
| 高度图 |
高 |
快 |
堆料/仓储 |
| 体素化 |
高 |
慢 |
复杂形状 |