#!/usr/bin/env python3
"""stereo_calibration.py - 双目标定"""
import cv2
import numpy as np
def stereo_calibrate(left_images, right_images, pattern_size=(9, 6)):
"""双目标定"""
objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
objp *= 25 # 25mm
obj_points = []
img_points_l = []
img_points_r = []
for left_path, right_path in zip(left_images, right_images):
img_l = cv2.imread(left_path)
img_r = cv2.imread(right_path)
gray_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY)
gray_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY)
ret_l, corners_l = cv2.findChessboardCorners(gray_l, pattern_size)
ret_r, corners_r = cv2.findChessboardCorners(gray_r, pattern_size)
if ret_l and ret_r:
corners_l = cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1),
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
corners_r = cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1),
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
obj_points.append(objp)
img_points_l.append(corners_l)
img_points_r.append(corners_r)
# 单目标定
ret_l, mtx_l, dist_l, _, _ = cv2.calibrateCamera(
obj_points, img_points_l, gray_l.shape[::-1], None, None)
ret_r, mtx_r, dist_r, _, _ = cv2.calibrateCamera(
obj_points, img_points_r, gray_r.shape[::-1], None, None)
# 双目标定
flags = cv2.CALIB_FIX_INTRINSIC
ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate(
obj_points, img_points_l, img_points_r,
mtx_l, dist_l, mtx_r, dist_r,
gray_l.shape[::-1], flags=flags
)
# 立体校正
R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(
mtx_l, dist_l, mtx_r, dist_r,
gray_l.shape[::-1], R, T,
alpha=0, newImageSize=gray_l.shape[::-1]
)
# 计算映射表
map1_l, map2_l = cv2.initUndistortRectifyMap(
mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1)
map1_r, map2_r = cv2.initUndistortRectifyMap(
mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1)
print(f"重投影误差: {ret:.4f}")
print(f"基线距离: {np.linalg.norm(T):.2f} mm")
return {
'camera_matrix_l': mtx_l, 'dist_l': dist_l,
'camera_matrix_r': mtx_r, 'dist_r': dist_r,
'R': R, 'T': T, 'R1': R1, 'R2': R2,
'P1': P1, 'P2': P2, 'Q': Q,
'map1_l': map1_l, 'map2_l': map2_l,
'map1_r': map1_r, 'map2_r': map2_r,
}
if __name__ == "__main__":
left_imgs = [f"calib/left/{i}.jpg" for i in range(20)]
right_imgs = [f"calib/right/{i}.jpg" for i in range(20)]
params = stereo_calibrate(left_imgs, right_imgs)
np.savez("stereo_calibration.npz", **params)
#!/usr/bin/env python3
"""stereo_sgbm.py - SGBM 立体匹配"""
import cv2
import numpy as np
class StereoDepth:
"""双目深度估计"""
def __init__(self, calib_params):
self.map1_l = calib_params['map1_l']
self.map2_l = calib_params['map2_l']
self.map1_r = calib_params['map1_r']
self.map2_r = calib_params['map2_r']
self.Q = calib_params['Q']
self.baseline = np.linalg.norm(calib_params['T'])
self.fx = calib_params['P1'][0, 0]
# SGBM 参数
self.stereo = cv2.StereoSGBM_create(
minDisparity=0,
numDisparities=128, # 必须是 16 的倍数
blockSize=5,
P1=8 * 3 * 5**2,
P2=32 * 3 * 5**2,
disp12MaxDiff=1,
uniquenessRatio=10,
speckleWindowSize=100,
speckleRange=32,
preFilterCap=63,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY,
)
def compute_depth(self, left_img, right_img):
"""计算深度图"""
# 校正
rect_l = cv2.remap(left_img, self.map1_l, self.map2_l, cv2.INTER_LINEAR)
rect_r = cv2.remap(right_img, self.map1_r, self.map2_r, cv2.INTER_LINEAR)
# 转灰度
gray_l = cv2.cvtColor(rect_l, cv2.COLOR_BGR2GRAY)
gray_r = cv2.cvtColor(rect_r, cv2.COLOR_BGR2GRAY)
# 计算视差
disparity = self.stereo.compute(gray_l, gray_r).astype(np.float32) / 16.0
# 视差转深度
depth = np.zeros_like(disparity)
valid = disparity > 0
depth[valid] = (self.fx * self.baseline) / disparity[valid]
return disparity, depth
def compute_3d_points(self, disparity):
"""视差图转 3D 点云"""
points_3d = cv2.reprojectImageTo3D(disparity, self.Q)
return points_3d
if __name__ == "__main__":
calib = np.load("stereo_calibration.npz", allow_pickle=True)
stereo = StereoDepth(calib)
left = cv2.imread("left.jpg")
right = cv2.imread("right.jpg")
disparity, depth = stereo.compute_depth(left, right)
# 可视化深度图
depth_vis = cv2.normalize(depth, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
depth_color = cv2.applyColorMap(depth_vis, cv2.COLORMAP_JET)
cv2.imwrite("depth.jpg", depth_color)
# 测量特定点距离
h, w = depth.shape
center_depth = depth[h//2, w//2]
print(f"画面中心距离: {center_depth:.2f} mm")
#!/usr/bin/env python3
"""pointcloud.py - 3D 点云处理"""
import numpy as np
import open3d as o3d
def depth_to_pointcloud(depth, camera_matrix, rgb=None):
"""深度图转 3D 点云"""
fx = camera_matrix[0, 0]
fy = camera_matrix[1, 1]
cx = camera_matrix[0, 2]
cy = camera_matrix[1, 2]
h, w = depth.shape
u, v = np.meshgrid(np.arange(w), np.arange(h))
z = depth
x = (u - cx) * z / fx
y = (v - cy) * z / fy
points = np.stack([x, y, z], axis=-1).reshape(-1, 3)
# 过滤无效点
valid = (z.reshape(-1) > 0) & (z.reshape(-1) < 10000)
points = points[valid]
# 创建点云
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
if rgb is not None:
colors = rgb.reshape(-1, 3)[valid] / 255.0
pcd.colors = o3d.utility.Vector3dVector(colors)
return pcd
def measure_volume(pcd):
"""测量点云体积"""
# 计算边界框
bbox = pcd.get_axis_aligned_bounding_box()
extent = bbox.get_extent()
volume = extent[0] * extent[1] * extent[2]
return {
'width': extent[0],
'height': extent[1],
'depth': extent[2],
'volume': volume,
'bbox': bbox,
}