🎯 高炉智变06|双目视觉+三维重建:让料面形状"看得见"
📅 本文目录
一、前言:为什么要看料面? 👁️
1.1 料面的重要性
高炉装料是高炉炼铁的第一道工序,料面形状直接影响煤气分布和冶炼效果:
📊 料面形状与冶炼效果:
理想料面形状:
___________
/ \
/ ⭐ 中心 \ ← 布料均匀,煤气上升稳定
/ \
/_______🔥边缘_______\ ← 焦炭多,透气性好
理想状态:
✅ 煤气流分布合理
✅ 透气性良好
✅ 焦比最优
✅ 产量最高
1.2 传统料面检测方法
📋 传统料面检测方法对比:
┌──────────┬────────┬────────────────┐
│ 方法 │ 精度 │ 缺点 │
├──────────┼────────┼────────────────┤
│ 人工观察 │ ⭐ │ 主观性强,不精确 │
│ 机械探尺 │ ⭐⭐ │ 只能测单点,有限│
│ 雷达探测 │ ⭐⭐⭐ │ 数据不直观 │
│ 红外测温 │ ⭐⭐ │ 只能测温度 │
└──────────┴────────┴────────────────┘
1.3 双目视觉的优势
💡 双目视觉就像人的两只眼睛,通过视差计算深度,实现料面的"立体感知"!
✨ 双目视觉检测优势:
✅ 三维感知: 获取料面完整三维形状
✅ 全局覆盖: 同时观测整个料面
✅ 非接触: 不影响生产过程
✅ 实时性: 毫秒级响应
✅ 智能化: AI自动分析
二、双目视觉基础原理 🧠
2.1 人眼立体视觉原理
👁️ 人眼立体视觉:
左眼看到的图像 右眼看到的图像
┌───┐ ┌───┐
│ A │ │ A │
│ B │ 融合后 │ B │
│ C │ → │ │ ← 感觉C更近
│ │ │ C │
└───┘ └───┘
距离近的物体,视差大
距离远的物体,视差小
2.2 双目视觉模型
📐 双目视觉几何模型:
物理世界中的点 P
│
│ (X, Y, Z)
│
┌────────────┼────────────┐
│ │ │
│ │ │
相机1中心 O_L │ 相机2中心 O_R
│ │ │
│ B │
│ ┌─────┴─────┐ │
│ │ 图像平面 │ │
│ │ │ │
│ │ 像素 │ │
│ │ (u_L) │ │
│ └───────────┘ │
│ │
└───────────────────────┘
基线距离 b
视差 d = |u_L - u_R|
深度 Z = f * b / d
其中:
- f: 焦距
- b: 基线距离
- d: 视差
2.3 核心公式推导
"""
双目视觉深度计算原理:
P(X, Y, Z)
/│
/ │
/ │ Z (深度)
/ │
/ │
/ │
O_L /──────│────── O_R
│ │
│ │
b │
│ │
│ X
│
视差: d = x_L - x_R
由相似三角形:
(Z - f) / Z = (b - d) / b
解得:
Z = f * b / d
所以深度Z与视差d成反比!
"""
def calculate_depth(focal_length, baseline, disparity):
"""
根据视差计算深度
Args:
focal_length: 焦距(像素)
baseline: 基线距离(mm)
disparity: 视差(像素)
Returns:
depth: 深度(mm)
"""
if disparity < 1e-6:
return float('inf') # 无效视差
depth = focal_length * baseline / disparity
return depth
三、相机标定技术 🔧
3.1 为什么需要标定?
📐 相机标定的必要性:
相机实际存在以下畸变:
1. 径向畸变: 桶形畸变、枕形畸变
2. 切向畸变: 镜头与传感器不平行
3. 内参: 焦距、主点
4. 外参: 相机位置、姿态
标定目的:
✅ 消除镜头畸变
✅ 获取相机内参
✅ 获取相机外参
✅ 建立世界坐标系
3.2 张正友标定法
import numpy as np
import cv2
class CameraCalibrator:
"""
张正友标定法相机标定
使用棋盘格标定板进行标定
"""
def __init__(self, chessboard_size=(9, 6), square_size=25):
"""
Args:
chessboard_size: 棋盘格内角点数 (列, 行)
square_size: 每个棋盘格的大小(mm)
"""
self.chessboard_size = chessboard_size
self.square_size = square_size
# 存储标定板角点
self.object_points = [] # 世界坐标系中的点
self.image_points = [] # 图像中的点
# 准备世界坐标系的点
self._prepare_object_points()
def _prepare_object_points(self):
"""准备世界坐标系中的点"""
objp = np.zeros((self.chessboard_size[0] * self.chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:self.chessboard_size[0],
0:self.chessboard_size[1]].T.reshape(-1, 2)
objp *= self.square_size
self.object_points_template = objp
def find_corners(self, image):
"""
在图像中找棋盘格角点
Args:
image: 输入图像
Returns:
corners: 角点坐标
found: 是否找到
"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 找棋盘格角点
found, corners = cv2.findChessboardCorners(gray, self.chessboard_size, None)
if found:
# 亚像素精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
return corners, found
def add_calibration_image(self, image):
"""添加标定图像"""
corners, found = self.find_corners(image)
if found:
self.image_points.append(corners)
self.object_points.append(self.object_points_template)
return True
return False
def calibrate(self, image_size):
"""
执行相机标定
Args:
image_size: 图像尺寸 (width, height)
Returns:
camera_matrix: 内参矩阵
dist_coeffs: 畸变系数
rvecs: 旋转向量
tvecs: 平移向量
"""
ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
self.object_points,
self.image_points,
image_size,
None,
None
)
self.camera_matrix = camera_matrix
self.dist_coeffs = dist_coeffs
self.rvecs = rvecs
self.tvecs = tvecs
return camera_matrix, dist_coeffs, rvecs, tvecs
def undistort(self, image):
"""校正图像畸变"""
return cv2.undistort(
image,
self.camera_matrix,
self.dist_coeffs
)
def get_extrinsic_params(self, idx):
"""获取指定图像的外参"""
rvec = self.rvecs[idx]
tvec = self.tvecs[idx]
# 旋转向量转旋转矩阵
R, _ = cv2.Rodrigues(rvec)
return R, tvec
def print_results(self):
"""打印标定结果"""
print("\n📷 相机标定结果:")
print("="*50)
print("\n📐 内参矩阵:")
print(self.camera_matrix)
print("\n📐 畸变系数:")
print(self.dist_coeffs)
print("\n📏 重投影误差:")
total_error = 0
for i in range(len(self.object_points)):
imgpoints2, _ = cv2.projectPoints(
self.object_points[i],
self.rvecs[i],
self.tvecs[i],
self.camera_matrix,
self.dist_coeffs
)
error = cv2.norm(self.image_points[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
total_error += error
mean_error = total_error / len(self.object_points)
print(f"平均误差: {mean_error:.4f} 像素")
3.3 双目标定
class StereoCalibrator:
"""
双目相机标定
"""
def __init__(self, left_calibrator, right_calibrator):
self.left_calibrator = left_calibrator
self.right_calibrator = right_calibrator
def calibrate_stereo(self, left_images, right_images, image_size):
"""
执行双目标定
Returns:
R: 旋转矩阵
T: 平移向量
E: 本质矩阵
F: 基础矩阵
"""
# 确保左右图像数量一致
assert len(left_images) == len(right_images), "左右图像数量不一致"
# 分别标定左右相机
cameraMatrix1, distCoef1, _, _ = self.left_calibrator.calibrate(image_size)
cameraMatrix2, distCoef2, _, _ = self.right_calibrator.calibrate(image_size)
# 双目标定
criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 100, 1e-5)
ret, cameraMatrix1, distCoef1, cameraMatrix2, distCoef2, R, T, E, F = \
cv2.stereoCalibrate(
self.left_calibrator.object_points,
self.left_calibrator.image_points,
self.right_calibrator.image_points,
cameraMatrix1, distCoef1,
cameraMatrix2, distCoef2,
image_size,
flags=cv2.CALIB_FIX_INTRINSIC,
criteria=criteria
)
self.R = R # 旋转矩阵
self.T = T # 平移向量
self.E = E # 本质矩阵
self.F = F # 基础矩阵
self.cameraMatrix1 = cameraMatrix1
self.cameraMatrix2 = cameraMatrix2
return R, T, E, F
def stereo_rectify(self, image_size):
"""
双目校正
使左右图像对齐到同一平面上
"""
rectify_scale = 1
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
self.cameraMatrix1, self.left_calibrator.dist_coeffs,
self.cameraMatrix2, self.right_calibrator.dist_coeffs,
image_size,
self.R, self.T,
alpha=rectify_scale
)
# 生成校正映射
map1x, map1y = cv2.initUndistortRectifyMap(
self.cameraMatrix1,
self.left_calibrator.dist_coeffs,
R1, P1,
image_size,
cv2.CV_32FC1
)
map2x, map2y = cv2.initUndistortRectifyMap(
self.cameraMatrix2,
self.right_calibrator.dist_coeffs,
R2, P2,
image_size,
cv2.CV_32FC1
)
self.map1x, self.map1y = map1x, map1y
self.map2x, self.map2y = map2x, map2y
self.Q = Q
return map1x, map1y, map2x, map2y, Q
四、立体匹配算法 🔍
4.1 立体匹配原理
📐 立体匹配流程:
1. 预处理: 灰度化、归一化
↓
2. 代价计算: 计算每个像素的匹配代价
↓
3. 代价聚合: 聚合邻域像素的代价
↓
4. 视差计算: 求解最优视差
↓
5. 视差细化: 亚像素精化
↓
6. 后处理: 过滤无效视差
4.2 BM算法(块匹配)
class StereoMatcherBM:
"""
块匹配立体匹配算法 (BM)
简单高效,适合实时应用
"""
def __init__(self):
# 创建BM匹配器
self.matcher = cv2.StereoBM_create(
numDisparities=64, # 视差搜索范围(必须是16的倍数)
blockSize=15 # 匹配块大小(奇数)
)
def compute(self, left_image, right_image):
"""
计算视差图
Args:
left_image: 左图像
right_image: 右图像
Returns:
disparity: 视差图
"""
# 转灰度
if len(left_image.shape) == 3:
left_gray = cv2.cvtColor(left_image, cv2.COLOR_BGR2GRAY)
right_gray = cv2.cvtColor(right_image, cv2.COLOR_BGR2GRAY)
else:
left_gray = left_image
right_gray = right_image
# 计算视差
disparity = self.matcher.compute(left_gray, right_gray)
return disparity
def set_params(self, num_disparities=64, block_size=15):
"""设置参数"""
self.matcher = cv2.StereoBM_create(
numDisparities=num_disparities,
blockSize=block_size
)
4.3 SGBM算法(半全局块匹配)
class StereoMatcherSGBM:
"""
半全局块匹配算法 (SGBM)
精度更高,适合高精度测量
"""
def __init__(self):
# 创建SGBM匹配器
self.matcher = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=128,
blockSize=3,
P1=8 * 3 * 3**2, # 惩罚系数1
P2=32 * 3 * 3**2, # 惩罚系数2
disp12MaxDiff=1,
uniquenessRatio=10,
speckleWindowSize=100,
speckleRange=32
)
def compute(self, left_image, right_image):
"""计算视差图"""
if len(left_image.shape) == 3:
left_gray = cv2.cvtColor(left_image, cv2.COLOR_BGR2GRAY)
right_gray = cv2.cvtColor(right_image, cv2.COLOR_BGR2GRAY)
else:
left_gray = left_image
right_gray = right_image
disparity = self.matcher.compute(left_gray, right_gray)
return disparity
def set_params(self, **kwargs):
"""设置参数"""
self.matcher = cv2.StereoSGBM_create(**kwargs)
4.4 AD-Census算法
class ADCensusStereo:
"""
AD-Census立体匹配算法
基于自适应窗口的代价聚合,精度高
"""
def __init__(self, max_disparity=128):
self.max_disparity = max_disparity
def compute(self, left_image, right_image):
"""计算视差图"""
# 转灰度
if len(left_image.shape) == 3:
left_gray = cv2.cvtColor(left_image, cv2.COLOR_BGR2GRAY)
right_gray = cv2.cvtColor(right_image, cv2.COLOR_BGR2GRAY)
else:
left_gray = left_image
right_gray = right_image
# 转换为float
left = left_gray.astype(np.float32)
right = right_gray.astype(np.float32)
h, w = left.shape
disparity = np.zeros((h, w), np.float32)
# 简化的AD-Census实现
# 实际工程中应使用完整版AD-Census
for y in range(7, h - 7):
for x in range(7, w - 7):
best_cost = float('inf')
best_disp = 0
for d in range(0, min(self.max_disparity, x - 7)):
# AD代价
cost = abs(left[y, x] - right[y, x - d])
# Census变换代价
left_census = self._census_transform(left[y-7:y+8, x-7:x+8], x, 7)
right_census = self._census_transform(right[y-7:y+8, x-7-d:x+8-d], x-d, 7)
cost += self._hamming_distance(left_census, right_census) * 2
if cost < best_cost:
best_cost = cost
best_disp = d
disparity[y, x] = best_disp
return disparity
def _census_transform(self, patch, center_x, offset):
"""Census变换"""
center_val = patch[offset, offset]
census = 0
for i in range(patch.shape[0]):
for j in range(patch.shape[1]):
if i != offset or j != offset:
census = (census << 1) | (1 if patch[i, j] < center_val else 0)
return census
def _hamming_distance(self, a, b):
"""汉明距离"""
xor = a ^ b
return bin(xor).count('1')
五、三维重建技术 📊
5.1 视差图转深度图
def disparity_to_depth(disparity, focal_length, baseline):
"""
将视差图转换为深度图
Args:
disparity: 视差图
focal_length: 焦距
baseline: 基线距离
Returns:
depth: 深度图
"""
# 避免除零
disparity_safe = np.maximum(disparity, 1e-6)
# 计算深度
depth = (focal_length * baseline) / disparity_safe
# 标记无效区域
depth[disparity <= 0] = 0
return depth
def depth_to_point_cloud(depth, camera_matrix):
"""
深度图转点云
Args:
depth: 深度图
camera_matrix: 相机内参
Returns:
points: 点云数据 (N x 3)
"""
h, w = depth.shape
# 相机内参
fx = camera_matrix[0, 0]
fy = camera_matrix[1, 1]
cx = camera_matrix[0, 2]
cy = camera_matrix[1, 2]
# 生成像素坐标
u, v = np.meshgrid(np.arange(w), np.arange(h))
# 转换为相机坐标系
x = (u - cx) * depth / fx
y = (v - cy) * depth / fy
z = depth
# 过滤无效点
valid = depth > 0
points = np.stack([x[valid], y[valid], z[valid]], axis=1)
return points
5.2 料面三维重建
class BurdenSurfaceReconstructor:
"""
高炉料面三维重建器
"""
def __init__(self, camera_matrix, baseline, calibration_params):
self.camera_matrix = camera_matrix
self.baseline = baseline
self.calibration = calibration_params
# 立体匹配器
self.matcher = StereoMatcherSGBM()
def reconstruct(self, left_image, right_image):
"""
重建料面三维形状
Args:
left_image: 左相机图像
right_image: 右相机图像
Returns:
surface: 料面三维数据
"""
# 1. 图像校正
left_rect = cv2.remap(
left_image,
self.calibration['map1x'],
self.calibration['map1y'],
cv2.INTER_LINEAR
)
right_rect = cv2.remap(
right_image,
self.calibration['map2x'],
self.calibration['map2y'],
cv2.INTER_LINEAR
)
# 2. 计算视差
disparity = self.matcher.compute(left_rect, right_rect)
# 3. 视差后处理
disparity = self._post_process_disparity(disparity)
# 4. 视差转深度
depth = disparity_to_depth(
disparity.astype(np.float32),
self.camera_matrix[0, 0],
self.baseline
)
# 5. 生成点云
points = depth_to_point_cloud(depth, self.camera_matrix)
# 6. 料面提取
surface = self._extract_surface(points)
return {
'disparity': disparity,
'depth': depth,
'points': points,
'surface': surface
}
def _post_process_disparity(self, disparity):
"""视差后处理"""
# speckle滤波
speckle_window = 200
speckle_range = 32
disparity = disparity.astype(np.int16)
cv2.filterSpeckles(disparity, 0, speckle_window, speckle_range)
# 中值滤波
disparity = cv2.medianBlur(disparity, 5)
# 转换为float
disparity = disparity.astype(np.float32) / 16.0
return disparity
def _extract_surface(self, points):
"""
提取料面
料面是料面上所有点的集合
"""
if len(points) == 0:
return None
# 计算料面高度分布
z_values = points[:, 2]
# 过滤掉非料面区域(根据Z值范围)
surface_z_min = np.percentile(z_values, 5)
surface_z_max = np.percentile(z_values, 95)
surface_mask = (z_values >= surface_z_min) & (z_values <= surface_z_max)
surface_points = points[surface_mask]
return {
'points': surface_points,
'height_min': surface_z_min,
'height_max': surface_z_max,
'center_height': np.mean(surface_points[:, 2]) if len(surface_points) > 0 else 0
}
def analyze_surface_shape(self, surface):
"""
分析料面形状
Returns:
analysis: 形状分析结果
"""
if surface is None or len(surface['points']) == 0:
return None
points = surface['points']
# 计算中心高度
center_height = np.mean(points[:, 2])
# 计算高度标准差
height_std = np.std(points[:, 2])
# 计算环形分布
radial_profile = self._compute_radial_profile(points, center_height)
return {
'center_height': center_height,
'height_variance': height_std,
'radial_profile': radial_profile,
'total_points': len(points),
'coverage': len(points) / (np.pi * (1.0 ** 2)) # 假设半径1米
}
def _compute_radial_profile(self, points, center_height):
"""计算径向高度分布"""
# 计算每个点到中心的距离
distances = np.sqrt(points[:, 0]**2 + points[:, 1]**2)
# 分桶统计
bins = np.linspace(0, 1.0, 11) # 0-1米,分10个桶
radial_profile = []
for i in range(len(bins) - 1):
mask = (distances >= bins[i]) & (distances < bins[i+1])
if np.sum(mask) > 0:
avg_height = np.mean(points[mask, 2])
radial_profile.append({
'radius_min': bins[i],
'radius_max': bins[i+1],
'avg_height': avg_height
})
else:
radial_profile.append({
'radius_min': bins[i],
'radius_max': bins[i+1],
'avg_height': 0
})
return radial_profile
六、实战代码实现 💻
6.1 完整双目系统
import cv2
import numpy as np
import time
class StereoVisionSystem:
"""
双目视觉系统
完整的高炉料面检测系统
"""
def __init__(self, config):
self.config = config
# 相机参数
self.camera_matrix = np.array(config['camera_matrix'])
self.baseline = config['baseline']
# 初始化组件
self.matcher = StereoMatcherSGBM()
self.calibrator = None
def load_calibration(self, calib_file):
"""加载标定参数"""
data = np.load(calib_file, allow_pickle=True).item()
self.camera_matrix = data['camera_matrix']
self.baseline = data['baseline']
self.calibrator = data
def capture_stereo_pair(self, left_cam, right_cam):
"""采集双目图像对"""
ret_left, left_frame = left_cam.read()
ret_right, right_frame = right_cam.read()
if ret_left and ret_right:
return left_frame, right_frame
return None, None
def process_frame(self, left_image, right_image):
"""
处理双目图像对
Returns:
results: 处理结果
"""
# 1. 立体校正
left_rect = self._rectify(left_image, 'left')
right_rect = self._rectify(right_image, 'right')
# 2. 计算视差
disparity = self.matcher.compute(left_rect, right_rect)
# 3. 计算深度
depth = disparity_to_depth(
disparity.astype(np.float32),
self.camera_matrix[0, 0],
self.baseline
)
# 4. 生成点云
points = depth_to_point_cloud(depth, self.camera_matrix)
# 5. 可视化
vis = self._visualize(left_image, disparity, depth)
return {
'disparity': disparity,
'depth': depth,
'points': points,
'visualization': vis
}
def _rectify(self, image, side='left'):
"""校正图像"""
if self.calibrator is None:
return image
if side == 'left':
return cv2.remap(
image,
self.calibrator['map1x'],
self.calibrator['map1y'],
cv2.INTER_LINEAR
)
else:
return cv2.remap(
image,
self.calibrator['map2x'],
self.calibrator['map2y'],
cv2.INTER_LINEAR
)
def _visualize(self, left_image, disparity, depth):
"""可视化"""
# 视差图着色
disp_vis = cv2.applyColorMap(
cv2.convertScaleAbs(disparity, alpha=255/128),
cv2.COLORMAP_JET
)
# 深度图着色
depth_vis = cv2.applyColorMap(
cv2.convertScaleAbs(depth, alpha=255/5000),
cv2.COLORMAP_JET
)
# 拼接
vis = np.hstack([left_image, disp_vis, depth_vis])
return vis
def run_realtime(self, left_cam, right_cam):
"""实时运行"""
print("📷 启动双目视觉系统...")
while True:
left_image, right_image = self.capture_stereo_pair(left_cam, right_cam)
if left_image is not None:
results = self.process_frame(left_image, right_image)
# 显示
cv2.imshow('Stereo Vision', results['visualization'])
# 打印统计
avg_depth = np.mean(results['depth'][results['depth'] > 0])
print(f"\r深度: {avg_depth:.1f}mm", end='')
# 按q退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
# 使用示例
def main():
# 配置
config = {
'camera_matrix': [
[1000, 0, 640],
[0, 1000, 360],
[0, 0, 1]
],
'baseline': 300, # mm
'calibration_file': 'stereo_calib.npz'
}
# 创建系统
system = StereoVisionSystem(config)
# 加载标定参数
system.load_calibration('stereo_calib.npz')
# 打开相机
left_cam = cv2.VideoCapture(0)
right_cam = cv2.VideoCapture(1)
# 实时运行
system.run_realtime(left_cam, right_cam)
# 释放资源
left_cam.release()
right_cam.release()
if __name__ == '__main__':
main()
七、总结与预告 🎯
7.1 本期要点
📝 本期知识点总结:
✅ 理解了双目视觉原理
✅ 掌握了相机标定技术
✅ 学会了立体匹配算法(BM/SGBM)
✅ 实现了三维重建技术
✅ 完成了料面检测系统
🎯 核心收获:
双目视觉让高炉料面"看得见、摸得着"!
7.2 下期预告
print("""
📢 下期预告:
第7期 | 深度学习异常检测:火眼金睛守护高炉安全
预告内容:
├── 👁️ CNN异常检测原理
├── 🔥 风口挂渣/烧穿检测
├── ⚠️ 实时预警系统
└── 💻 完整代码实现
敬请期待!🔔🔔🔔
""")
🔥 关注我,第一时间获取下一期精彩内容!
标签: #双目视觉 #三维重建 #立体匹配 #相机标定 #高炉炼铁 #点云
相关文章:
👍 如果觉得有帮助,请点赞、收藏、转发!
版权归作者所有,未经许可请勿抄袭,套用,商用(或其它具有利益性行为)。
🔔 关注专栏,不错过后续精彩内容!

178

被折叠的 条评论
为什么被折叠?



