3D 点云体积测量:货物堆方量检测实战

3D 点云体积测量:货物堆方量检测实战

1. 堆方量概念

复制代码
堆方量(Bulk Volume):
├── 定义:松散货物堆放的体积
├── 应用:矿石、粮食、煤炭、砂石
├── 测量:传统人工测量 vs 视觉测量
└── 精度要求:±3%(商业结算)

视觉测量优势:
├── 非接触式:不影响堆放
├── 实时性:秒级测量
├── 高精度:±1-2%
└── 自动化:减少人工

2. 深度相机点云采集

python 复制代码
#!/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. 体积计算算法

python 复制代码
#!/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. 精度校准

python 复制代码
#!/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. 实时堆方量监测

python 复制代码
#!/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 不规则形状
高度图 堆料/仓储
体素化 复杂形状