1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165 |
- import cv2
- import os
- from tqdm import tqdm
- import numpy as np
- import scipy.io as sio
- import matplotlib.pyplot as plt
- from scipy.interpolate import griddata
- import glob
- from scipy import stats
- from scipy.optimize import minimize
- from collections import defaultdict
- from scipy.signal import savgol_filter
- from scipy.ndimage import uniform_filter, gaussian_filter, median_filter
- import json
- import pickle
- import shutil
- import pandas as pd
- from sklearn.linear_model import LinearRegression
- from scipy.interpolate import Rbf
- from numpy.linalg import lstsq
- from scipy.optimize import curve_fit
- from aprilgrid import Detector
- from src.utils import *
- from src.calibration.get_camera_params import generate_3d_points, show_detect_result
- from scipy.optimize import least_squares
- import random
- from scipy.io import savemat, loadmat
- from src.pcl_postproc import smooth_pcl, align2ref
- def pixel_to_world(points_2d, K, R, T, depth):
- """
- 将像素坐标转换为世界坐标系中的 3D 坐标。
-
- Parameters:
- points_2d: np.ndarray
- 图像中的2D像素坐标 (N, 2)。
- K: np.ndarray
- 相机内参矩阵 (3x3)。
- R: np.ndarray
- 相机旋转矩阵 (3x3)。
- T: np.ndarray
- 相机平移向量 (3, 1)。
- depth: float or np.ndarray
- 点的深度(单位:毫米或其他实际单位)。
-
- Returns:
- points_3d_world: np.ndarray
- 世界坐标系中的 3D 点 (N, 3)。
- """
- # 将像素坐标转换为齐次坐标 (u, v, 1)
- image_points_3d = np.hstack((points_2d, np.ones((points_2d.shape[0], 1))))
- norm_points = np.linalg.inv(K).dot(image_points_3d.reshape(-1, 3).T).T
- # 逆投影到3D空间
- object_points = []
- for point in norm_points:
- # 将归一化坐标转换为3D点
- point_3d = np.zeros((3,), dtype=np.float32)
- point_3d[:2] = point[:2] / point[2] # 归一化坐标转换为3D坐标
- point_3d[2] = 1
- # 使用旋转和平移向量进行逆投影
- # 首先需要将旋转向量转换为旋转矩阵
- #R, _ = cv2.Rodrigues(R.reshape(3, 3))
- object_point = np.dot(R.T, point_3d - T.reshape(3, 1)).flatten()
- object_points.append(object_point)
-
- points_3d_world = np.array(object_points)
- print("Object Points:")
- print(points_3d_world)
- return points_3d_world
- def project_to_world(points_2d, K, R, T, dist_coeffs, depth=1.0):
- """
- 将相机检测到的2D点转换为世界坐标系下的3D点。
-
- Parameters:
- points_2d: np.ndarray
- 图像中的2D点 (N, 2)。
- K: np.ndarray
- 相机内参矩阵。
- R: np.ndarray
- 相机旋转矩阵 (3x3)。
- T: np.ndarray
- 相机平移向量 (3, 1)。
- dist_coeffs: np.ndarray
- 相机的畸变系数。
- depth: float or np.ndarray
- 点的深度(可以是标量或与点的数量匹配的数组)。
-
- Returns:
- points_3d_world: np.ndarray
- 世界坐标系下的3D点 (N, 3)。
- """
- # 去畸变并将2D点转换为相机归一化坐标
- #points_undistorted = cv2.undistortPoints(points_2d, K, dist_coeffs)
- #print('points_undistorted = ', points_undistorted)
- # 将2D点扩展为3D点,相机归一化坐标 (x, y, 1)
- points_3d_camera = np.hstack((points_2d.squeeze(), np.ones((points_2d.shape[0], 1))))
- # 乘以深度,得到实际的相机坐标系中的 3D 点
- points_3d_camera *= depth
- # 将相机坐标系中的点转换为世界坐标系中的点
- points_3d_world = np.dot(R.T, (points_3d_camera.T - T)).T
- return points_3d_world
- def plot_3d_points(points_3d, label, color, ax):
- """
- 绘制3D点云。
-
- Parameters:
- points_3d: np.ndarray
- 3D世界坐标系中的点 (N, 3)。
- label: str
- 相机的标签名称。
- color: str
- 点的颜色。
- ax: Axes3D
- 3D图形对象。
- """
-
- ax.scatter(points_3d[:, 0], points_3d[:, 1], points_3d[:, 2], label=label, color=color)
- # 定义投影函数
- def project_points(points_3d, rvec, tvec, K, dist_coeffs):
- """
- 将 3D 世界坐标点投影到 2D 图像平面。
-
- Parameters:
- points_3d: numpy.ndarray
- 3D 点的坐标 (N, 3)。
- rvec: numpy.ndarray
- 旋转向量 (3,)。
- tvec: numpy.ndarray
- 平移向量 (3,)。
- K: numpy.ndarray
- 相机内参矩阵 (3, 3)。
- dist_coeffs: numpy.ndarray
- 畸变系数。
-
- Returns:
- image_points: numpy.ndarray
- 投影到图像平面的 2D 点 (N, 2)。
- """
- image_points, _ = cv2.projectPoints(points_3d, rvec, tvec, K, dist_coeffs)
- return np.squeeze(image_points)
- # def residuals(params, n_cameras, n_points, camera_indices, point_indices, points_2d, K_matrices, dist_coeffs):
- # """
- # 计算捆绑调整中的残差,即重投影误差。
-
- # Parameters:
- # params: np.ndarray
- # 优化的参数(包括所有相机的外参和所有3D点的坐标)。
- # n_cameras: int
- # 相机的数量。
- # n_points: int
- # 3D 点的数量。
- # camera_indices: np.ndarray
- # 每个观测对应的相机索引。
- # point_indices: np.ndarray
- # 每个观测对应的 3D 点索引。
- # points_2d: np.ndarray
- # 实际观测到的 2D 图像点。
- # K_matrices: list of np.ndarray
- # 每个相机的内参矩阵。
- # dist_coeffs: list of np.ndarray
- # 每个相机的畸变系数。
-
- # Returns:
- # residuals: np.ndarray
- # 重投影误差。
- # """
- # # 提取旋转和平移向量
- # rvecs = params[:n_cameras * 3].reshape((n_cameras, 3)) # 旋转向量
- # tvecs = params[n_cameras * 3:n_cameras * 6].reshape((n_cameras, 3)) # 平移向量
-
- # # 提取3D点
- # points_3d = params[n_cameras * 6:].reshape((n_points, 3)) # 3D 点
-
- # residuals = []
-
- # # 计算每个观测的重投影误差
- # for i in range(len(camera_indices)):
- # cam_idx = camera_indices[i] # 当前观测的相机
- # point_idx = point_indices[i] # 当前观测的 3D 点
- # K = K_matrices[cam_idx] # 相机的内参矩阵
- # dist = dist_coeffs[cam_idx] # 相机的畸变系数
-
- # # 投影3D点到相机平面,使用OpenCV的projectPoints函数
- # projected_points, _ = cv2.projectPoints(
- # points_3d[point_idx].reshape(1, -1), # 当前的3D点
- # rvecs[cam_idx].reshape(-1, 1), # 当前相机的旋转向量 (Rodrigues形式)
- # tvecs[cam_idx].reshape(-1, 1), # 当前相机的平移向量
- # K, # 内参矩阵
- # dist # 畸变系数
- # )
-
- # # 将 projected_points 变成 (N, 2) 的形状
- # projected_points = projected_points.squeeze()
- # # 计算残差(重投影误差)
- # residuals.append(projected_points - points_2d[i])
- # return np.concatenate(residuals)
- def calculate_normal(points):
- # points 是一个 (4, 3) 的数组,表示 4 个 3D 点
- vec1 = points[1] - points[0]
- vec2 = points[2] - points[0]
- normal = np.cross(vec1, vec2) # 计算法向量
- return normal / np.linalg.norm(normal) # 单位化
- def point_to_plane_distance(point, plane_normal, plane_point):
- return np.dot(plane_normal, point - plane_point)
- def residuals(params, n_cameras, n_points, camera_indices, point_indices, points_2d, K_matrices, dist_coeffs, plane_constraints):
- rvecs = params[:n_cameras * 3].reshape((n_cameras, 3))
- tvecs = params[n_cameras * 3:n_cameras * 6].reshape((n_cameras, 3))
- points_3d = params[n_cameras * 6:].reshape((n_points, 3))
- residuals = []
- # 计算重投影误差
- for i in range(len(camera_indices)):
- cam_idx = camera_indices[i]
- point_idx = point_indices[i]
- K = K_matrices[cam_idx]
- dist = dist_coeffs[cam_idx]
- projected_points, _ = cv2.projectPoints(
- points_3d[point_idx].reshape(1, -1),
- rvecs[cam_idx].reshape(3, 1),
- tvecs[cam_idx].reshape(3, 1),
- K, dist
- )
- projected_points = projected_points.squeeze()
- residual = projected_points - points_2d[i]
- residuals.extend(residual.tolist()) # 将重投影误差的两个分量添加到 residuals
- # 计算平面约束残差
- for constraint in plane_constraints:
- points_idx = constraint
- plane_points = points_3d[points_idx[:3]] # 取前三个点计算平面
- plane_normal = calculate_normal(plane_points)
- plane_point = plane_points[0]
- for idx in points_idx:
- distance = point_to_plane_distance(points_3d[idx], plane_normal, plane_point)
- residuals.append(distance) # 添加标量距离值
- return np.array(residuals)
- def extract_params_from_result(result, n_cameras, n_points):
- """
- 从优化结果中提取旋转向量、平移向量和3D点数据。
-
- Parameters:
- result: OptimizeResult
- scipy.optimize.least_squares 返回的结果。
- n_cameras: int
- 相机的数量。
- n_points: int
- 3D 点的数量。
-
- Returns:
- rvecs: list of np.ndarray
- 优化后的旋转向量。
- tvecs: list of np.ndarray
- 优化后的平移向量。
- points_3d: np.ndarray
- 优化后的3D点坐标。
- """
- optimized_params = result.x
-
- # 提取旋转和平移向量
- rvecs = optimized_params[:n_cameras * 3].reshape((n_cameras, 3)) # 旋转向量
- tvecs = optimized_params[n_cameras * 3:n_cameras * 6].reshape((n_cameras, 3)) # 平移向量
-
- # 提取3D点
- #points_3d = optimized_params[n_cameras * 6:].reshape((n_points, 3)) # 3D点
-
- return rvecs, tvecs
- def compare_params(optimized_rvecs, optimized_tvecs, original_cam_params):
- """
- 对比优化后的相机外参和原始标定结果。
-
- Parameters:
- optimized_rvecs: np.ndarray
- 优化后的旋转向量。
- optimized_tvecs: np.ndarray
- 优化后的平移向量。
- original_cam_params: list of dict
- 原始标定结果(包含旋转矩阵和平移向量)。
-
- Returns:
- None
- """
- for cam_idx, original_params in enumerate(original_cam_params):
- # 原始旋转矩阵和转换为旋转向量
- original_rvec = cv2.Rodrigues(original_params['rotation_matrix'])[0].flatten()
- original_tvec = original_params['translation_vector'].flatten()
- # 优化后的旋转向量和平移向量
- optimized_rvec = optimized_rvecs[cam_idx]
- optimized_tvec = optimized_tvecs[cam_idx]
- # 计算旋转向量和平移向量的差异
- rotation_diff = np.linalg.norm(original_rvec - optimized_rvec)
- translation_diff = np.linalg.norm(original_tvec - optimized_tvec)
- print(f"相机 {cam_idx} 差异:")
- print(f"旋转向量差异: {rotation_diff}")
- print(f"平移向量差异: {translation_diff}")
- def calculate_reprojection_error(rvecs, tvecs, points_3d, points_2d, K_matrices, dist_coeffs):
- """
- 计算重投影误差。
-
- Parameters:
- rvecs: np.ndarray
- 相机的旋转向量。
- tvecs: np.ndarray
- 相机的平移向量。
- points_3d: np.ndarray
- 3D点坐标。
- points_2d: np.ndarray
- 2D图像中的对应点。
- K_matrices: list of np.ndarray
- 相机内参矩阵。
- dist_coeffs: list of np.ndarray
- 畸变系数。
-
- Returns:
- float
- 平均重投影误差。
- """
- total_error = 0
- total_points = 0
- for cam_idx in range(len(rvecs)):
- projected_points, _ = cv2.projectPoints(points_3d, rvecs[cam_idx], tvecs[cam_idx], K_matrices[cam_idx], dist_coeffs[cam_idx])
- error = np.linalg.norm(points_2d[cam_idx] - projected_points.squeeze(), axis=1).sum()
- total_error += error
- total_points += len(points_2d[cam_idx])
- return total_error / total_points
- def bundle_adjustment(all_object_points, all_corners, cam_params, plane_constraints):
- """
- 使用捆绑调整优化相机参数和3D点坐标。
-
- Parameters:
- all_object_points: list of list of np.ndarray
- 每个相机中每个图像中的 3D 物体点。
- all_corners: list of list of np.ndarray
- 每个相机中的多个图像中的 2D 图像点(每台相机拍摄了多张图片)。
- cam_params: list of dict
- 每个相机的内参和外参。
-
- Returns:
- result: scipy.optimize.OptimizeResult
- 优化后的参数结果。
- """
- n_cameras = len(cam_params) # 相机的数量
- # 展平所有 3D 物体点
- all_object_points_flat = []
- for object_points_cam in all_object_points:
- all_object_points_flat.extend(object_points_cam) # 将每个相机的所有 3D 物体点展平
- all_object_points_flat = np.concatenate(all_object_points_flat)
- n_points = all_object_points_flat.shape[0]
- # 收集相机内参矩阵和畸变系数
- K_matrices = [params['camera_matrix'] for params in cam_params]
- dist_coeffs = [params['distortion_coefficients'] for params in cam_params]
- # 创建相机索引和点索引,用于优化
- camera_indices = []
- point_indices = []
- points_2d = []
- # 记录全局 3D 点的偏移量,用于正确生成 point_indices
- point_offset = 0
- # 遍历每台相机的所有图像
- for cam_idx, (object_points_cam, corners_cam) in enumerate(zip(all_object_points, all_corners)):
- for img_idx, points_2d_list in enumerate(corners_cam): # 遍历每个相机的每张图像中的2D点
- n_points_in_image = points_2d_list.shape[0]
- # 给每个2D点分配当前相机的相机索引(相机编号 cam_idx)
- camera_indices.extend([cam_idx] * n_points_in_image)
-
- # 给每个2D点分配全局3D点索引
- point_indices.extend(range(point_offset, point_offset + n_points_in_image))
-
- # 增加偏移量
- point_offset += n_points_in_image
- # 收集2D图像点
- points_2d.extend(points_2d_list)
- camera_indices = np.array(camera_indices)
- point_indices = np.array(point_indices)
- points_2d = np.array(points_2d)
- # 初始化外参(旋转和平移向量)和 3D 点
- rvecs = [cv2.Rodrigues(params['rotation_matrix'])[0].flatten() for params in cam_params]
- tvecs = [params['translation_vector'].flatten() for params in cam_params]
- # 将外参和3D点展平为一维数组
- initial_params = np.hstack([
- np.concatenate(rvecs), # 所有相机的旋转向量
- np.concatenate(tvecs), # 所有相机的平移向量
- all_object_points_flat.ravel() # 所有3D点展平
- ])
- # 执行捆绑调整,使用 'trf' 或 'dogbox' 方法
- # result = least_squares(
- # residuals, initial_params, verbose=2, x_scale='jac', ftol=1e-4, method='trf', # 使用 'trf' 方法
- # args=(n_cameras, n_points, camera_indices, point_indices, points_2d, K_matrices, dist_coeffs)
- # )
- result = least_squares(
- residuals, initial_params, verbose=2, x_scale='jac', ftol=1e-4, method='trf',
- args=(n_cameras, n_points, camera_indices, point_indices, points_2d, K_matrices, dist_coeffs, plane_constraints)
- )
- # 优化前的重投影误差
- original_rvecs = [cv2.Rodrigues(params['rotation_matrix'])[0] for params in cam_params]
- original_tvecs = [params['translation_vector'] for params in cam_params]
- # original_error = calculate_reprojection_error(
- # original_rvecs, original_tvecs, original_points_3d, original_points_2d, K_matrices, dist_coeffs
- # )
- # # 优化后的重投影误差
- # optimized_error = calculate_reprojection_error(
- # optimized_rvecs, optimized_tvecs, points_3d, points_2d, K_matrices, dist_coeffs
- # )
- #print(f"优化前的重投影误差: {original_error}")
- #print(f"优化后的重投影误差: {optimized_error}")
- return result
- def visualize_projection(image, points_2d, color='r', title='Projected Points'):
- """
- 在图像上绘制投影点。
-
- Parameters:
- image: numpy.ndarray
- 图像数据。
- points_2d: numpy.ndarray
- 要绘制的 2D 点。
- color: str
- 绘制点的颜色。
- title: str
- 图像标题。
- """
- plt.figure(figsize=(8, 6))
- plt.imshow(image, cmap='gray')
- plt.scatter(points_2d[:, 0], points_2d[:, 1], c=color, s=50, label='Projected Points')
- plt.legend()
- plt.title(title)
- plt.show()
- def compute_reprojection_error(projected_points, observed_points):
- """
- 计算投影点与实际观测点之间的误差。
-
- Parameters:
- projected_points: numpy.ndarray
- 投影得到的2D点,形状为 (N, 2)。
- observed_points: numpy.ndarray
- 实际观测到的2D点,形状为 (N, 2)。
-
- Returns:
- float
- 平均重投影误差。
- """
- error = np.linalg.norm(projected_points - observed_points, axis=1)
- return np.mean(error)
- # 将 3D 点转换到相机坐标系
- def transform_points_to_camera_frame(points, R, T):
- return (R @ points.T + T).T
- def detect_april_tag_points(detector, img_file, object_point_single):
- """
- 检测单张图像中的 AprilTags,并返回图像中的 2D 角点和相应的 3D 物体点。
-
- Parameters:
- detector: AprilTag 检测器对象
- img_file: 图像文件路径
- object_point_single: 标定板的 3D 物体点
-
- Returns:
- all_corners: np.ndarray
- 图像中的 2D 角点 (N, 2)
- all_object_points: np.ndarray
- 对应的 3D 物体点 (N, 3)
- """
- #print('Processing image file:', img_file)
- # 读取并转换为灰度图像
- src = cv2.imread(img_file)
- gray = cv2.imread(img_file, 0)
- if gray is None:
- print(f"Failed to load image: {img_file}")
- return np.array([]), np.array([])
- # 检测 AprilTags
- detections = detector.detect(gray)
- all_corners = []
- all_object_points = []
- tag_id_lst = []
- #show_detect_result(src, detections)
- if detections:
- for detection in detections:
- corners = detection.corners # 检测到的角点 (4, 2)
- tag_id = detection.tag_id # 标签 ID
- tag_id_lst.append(int(tag_id))
- # 将检测到的角点存储为 np.float32 格式
- all_corners.extend(corners.astype(np.float32))
- # 根据标签 ID 提取相应的 3D 坐标
- if tag_id < len(object_point_single):
- all_object_points.extend(object_point_single[tag_id])
- else:
- print(f"Warning: Tag ID {tag_id} is out of range.")
- if all_object_points and all_corners:
- return np.array(all_corners, dtype=np.float32).reshape(-1, 2), \
- np.array(all_object_points, dtype=np.float32).reshape(-1, 3), \
- tag_id_lst, detections
- else:
- print("No valid detections in this image.")
- return np.array([]), np.array([]), [], []
- def single_camera_calibrate_vis():
- config_path = 'D:/code/pmdrecons-python-xtt/config/cfg_3freq_wafer.json'
- para_path = 'D:\\code\\pmdrecons-python-xtt\\config\\world_params.mat'
- world_params = loadmat(para_path)
- camera_matrix = np.array(world_params['camera_matrix'])
- dist_coeffs = np.array(world_params['distortion_coefficients'])
- img_dir = 'D:\\data\\one_cam\\pmd_benchmarks\\set4-20240906\\set4-20240906\\calibration\\'
- img_paths = glob.glob(os.path.join(img_dir, 'world_calibration/*.bmp'))
- debug = 1
- cfg = json.load(open(config_path, 'r'))
- chessboard_size = cfg['world_chessboard_size']
- square_size = cfg['world_square_size']
- # 准备棋盘格的世界坐标
- objp = np.zeros((chessboard_size[0]*chessboard_size[1], 3), np.float32)
- objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
- objp *= square_size
- # 存储每个棋盘格在相机坐标系中的位置
- chessboard_positions = []
- for fname in img_paths:
- print(fname)
- img = cv2.imread(fname)
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
- # 寻找棋盘格角点
- ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
- if ret:
- # 提高角点精度
- criteria = (cv2.TermCriteria_EPS + cv2.TermCriteria_MAX_ITER, 30, 0.001)
- corners_subpix = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
- # 计算棋盘格的姿态(相对于相机)
- success, rvec, tvec = cv2.solvePnP(objp, corners_subpix, camera_matrix, dist_coeffs)
- if success:
- # 计算旋转矩阵
- R, _ = cv2.Rodrigues(rvec)
- # 将棋盘格角点转换到相机坐标系
- # objp_in_camera = R * objp^T + tvec
- objp_in_camera = (R @ objp.T + tvec).T # 转置以获得 N x 3 的矩阵
- # 存储转换后的棋盘格角点
- chessboard_positions.append(objp_in_camera)
- else:
- print("姿态估计失败:", fname)
- else:
- print("无法找到棋盘格角点:", fname)
- cv2.destroyAllWindows()
- # 绘制多个棋盘格在相机坐标系中的位置
- if len(chessboard_positions) > 0:
- # 创建3D绘图
- fig = plt.figure()
- ax = fig.add_subplot(111, projection='3d')
- # 绘制每个棋盘格
- for idx, chessboard in enumerate(chessboard_positions):
- x = chessboard[:, 0]
- y = chessboard[:, 1]
- z = chessboard[:, 2]
- ax.scatter(x, y, z, label=f'Chessboard {idx+1}')
- plt.pause(3)
-
- # 绘制相机位置(原点)
- ax.scatter(0, 0, 0, c='r', marker='^', s=100, label='Camera Position')
- # 设置标签和标题
- ax.set_xlabel('X axis')
- ax.set_ylabel('Y axis')
- ax.set_zlabel('Z axis')
- ax.set_title('Multiple Chessboard Positions Relative to Fixed Camera')
- # 设置等比例显示
- all_x = np.hstack([chessboard[:, 0] for chessboard in chessboard_positions])
- all_y = np.hstack([chessboard[:, 1] for chessboard in chessboard_positions])
- all_z = np.hstack([chessboard[:, 2] for chessboard in chessboard_positions])
- ax.set_box_aspect([np.ptp(a) for a in [all_x, all_y, all_z]])
- # 显示图例
- ax.legend()
-
- plt.show()
- else:
- print("未能计算出有效的棋盘格位置。")
- return
- def draw(img, corner, imgpts):
- corner = tuple(corner.ravel().astype(int))
- imgpts = imgpts.astype(int)
- # 绘制坐标轴
- img = cv2.line(img, corner, tuple(imgpts[1].ravel()), (0, 0, 255), 5) # X轴(红色)
- img = cv2.line(img, corner, tuple(imgpts[2].ravel()), (0, 255, 0), 5) # Y轴(绿色)
- img = cv2.line(img, corner, tuple(imgpts[3].ravel()), (255, 0, 0), 5) # Z轴(蓝色)
- return img
- def verify_coplanarity(points_3d):
- # 使用SVD拟合平面
- centroid = np.mean(points_3d, axis=0)
- centered_points = points_3d - centroid
- U, S, Vt = np.linalg.svd(centered_points)
- normal_vector = Vt[-1, :]
- # 计算每个点到平面的距离
- distances = np.abs(centered_points @ normal_vector)
- mean_distance = np.mean(distances)
- max_distance = np.max(distances)
- print(f"Mean distance to plane: {mean_distance}")
- print(f"Max distance to plane: {max_distance}")
- # 设置一个阈值判断是否共面
- threshold = 1e-3 # 根据需求调整
- if max_distance < threshold:
- print("All points are approximately coplanar.")
- else:
- print("Points are not coplanar.")
- def fit_plane(points):
- """拟合一个平面并返回法向量和质心."""
- centroid = np.mean(points, axis=0) # 计算质心
- centered_points = points - centroid # 将点移到质心
- # 使用 SVD 分解拟合平面法向量
- _, _, vh = np.linalg.svd(centered_points)
- normal_vector = vh[-1] # 法向量是 SVD 的最后一列
- return normal_vector, centroid
- def rotation_matrix_from_vectors(v1, v2):
- """计算将向量 v1 旋转到 v2 的旋转矩阵."""
- v1 = v1 / np.linalg.norm(v1)
- v2 = v2 / np.linalg.norm(v2)
- axis = np.cross(v1, v2) # 旋转轴
- angle = np.arccos(np.dot(v1, v2)) # 旋转角度
- # 构建旋转矩阵(Rodrigues 公式)
- K = np.array([
- [0, -axis[2], axis[1]],
- [axis[2], 0, -axis[0]],
- [-axis[1], axis[0], 0]
- ])
- rotation_matrix = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * (K @ K)
- return rotation_matrix
- def align_points_to_horizontal(points):
- """将给定的 3D 点集旋转到水平面."""
- normal_vector, centroid = fit_plane(points) # 拟合平面
- print('normal_vector = ', normal_vector)
- rotation_to_horizontal = rotation_matrix_from_vectors(normal_vector, np.array([0, 0, 1]))
- #print('rotation_to_horizontal = ', rotation_to_horizontal)
- # 将所有点旋转到水平面,并返回结果
- centered_points = points - centroid # 平移到质心
- rotated_points = (rotation_to_horizontal @ centered_points.T).T + centroid # 旋转并平移回去
- return rotated_points, rotation_to_horizontal
- def camera_calibrate_vis():
- # 加载配置和相机参数
- config_path = 'D:\\code\\pmd-python\\config\\cfg_3freq_wafer.json'
- detector = Detector('t36h11')
- cfg = json.load(open(config_path, 'r'))
- with open(os.path.join('D:\\code\\pmd-python\\config\\', 'cam_params1023.pkl'), 'rb') as pkl_file:
- cam_params = pickle.load(pkl_file)
-
- # 生成 3D 物体点
- world_chessboard_size, world_square_size, world_square_spacing = [10, 6], 35.2, 10.56
- object_point_single = generate_3d_points(world_chessboard_size, world_square_size, world_square_spacing)
-
- # 保存所有相机的观测数据
- all_points_3d_world = []
- all_image_points = []
- all_camera_centers = []
- K_matrices = []
- dist_coeffs = []
- all_corners = []
- all_object_points = []
- optimized_cam_params = []
- all_tag_lst = []
- all_detections = []
- # 定义四个相机的标定图像路径
- calibration_images_path = [
- 'D:\\data\\four_cam\\calibrate\\single_test\\cam0\\',
- 'D:\\data\\four_cam\\calibrate\\single_test\\cam1\\',
- 'D:\\data\\four_cam\\calibrate\\single_test\\cam2\\',
- 'D:\\data\\four_cam\\calibrate\\single_test\\cam3\\'
- ]
- # 遍历每个相机
- for cam_id, params in enumerate(cam_params):
- K_matrices.append(params['camera_matrix'])
- dist_coeffs.append(params['distortion_coefficients'])
-
- camera_corners = [] # 用于当前相机的2D图像点
- camera_object_points = [] # 用于当前相机的3D物体点
-
- # 对每个相机的标定图像提取2D角点
- for img_file in os.listdir(calibration_images_path[cam_id]):
- # 读取图像并检测AprilTags,获取2D角点和对应的3D物体点
- image_points, object_points, tag_id_lst, detections = detect_april_tag_points(
- detector, calibration_images_path[cam_id] + img_file, object_point_single
- )
- # print('image_points = ', image_points)
- # print('object_points =', object_points)
- print('tag_id_lst = ', tag_id_lst)
- all_tag_lst.append(tag_id_lst)
- all_corners.append(image_points)
- all_object_points.append(object_points)
- all_detections.append(detections)
- undistorted_points = cv2.undistortPoints(image_points, params['camera_matrix'], params['distortion_coefficients'])
- success, rotation_vector, translation_vector = cv2.solvePnP(object_points, image_points, params['camera_matrix'],
- params['distortion_coefficients'],
- params['rotation_matrix'],
- params['translation_vector']
- )
- rotation_matrix, _ = cv2.Rodrigues(rotation_vector)
- # 假设的深度值 (s)
- # 你需要根据场景给出一个合理的深度值
- # 相机的中心
-
- camera_center = -rotation_matrix.T @ translation_vector
- #print('camera_center = ', camera_center)
- depth = camera_center[-1]
- print('undistorted_points shape = ', undistorted_points.shape)
- print('image_points shape = ', image_points.shape)
- print('depth = ', depth)
- # 将 2D 图像点转换为齐次坐标
- #print(image_points.shape)
- image_points_homogeneous = np.hstack([image_points, np.ones((image_points.shape[0], 1), dtype=np.float32)])
- # 批量计算相机坐标系中的 3D 点 (N x 3 矩阵)
- camera_coordinates = (np.linalg.inv(params['camera_matrix']) @ image_points_homogeneous.T).T * depth
- # 将相机坐标转换为世界坐标 (N x 3 矩阵)
- world_coordinates = (np.linalg.inv(rotation_matrix) @ (camera_coordinates - translation_vector.T).T).T
- all_points_3d_world.append(world_coordinates)
- print('len 3d = ', len(all_points_3d_world))
- # adjusted_cam_params = []
- # for i, params in enumerate(cam_params):
- # world_coordinates = all_points_3d_world[i] # 获取该相机的 3D 点
- # #print('world_coordinates = ', world_coordinates)
- # # 将点集旋转到水平面,并获取旋转矩阵
- # #aligned_points, rotation_to_horizontal = align_points_to_horizontal(world_coordinates)
- # aligned_points, rotation_to_horizontal = align2ref(world_coordinates)
-
- # print('aligned_points = ', rotation_matrix)
- # print('rotation_to_horizontal = ', rotation_to_horizontal)
- # # 更新相机的外参(旋转矩阵)
- # params['rotation_matrix'] = rotation_to_horizontal @ params['rotation_matrix']
- # adjusted_cam_params.append(params)
- # # 更新校正后的 3D 点
- # all_points_3d_world[i] = aligned_points
-
- # fig = plt.figure()
- # ax = fig.add_subplot(111, projection='3d')
- # for i, points in enumerate(all_points_3d_world):
- # ax.scatter(points[:, 0], points[:, 1], points[:, 2], label=f'Cam {i}')
- # ax.set_xlabel('X')
- # ax.set_ylabel('Y')
- # ax.set_zlabel('Z')
- # plt.legend()
- # plt.show()
- # # 3. 保存调整后的相机参数,以备后续使用
- # with open(os.path.join('D:\\code\\pmd-python\\config\\', 'cam_params1023.pkl'), 'wb') as f:
- # pickle.dump(adjusted_cam_params, f)
-
- fig = plt.figure()
- ax = fig.add_subplot(111, projection='3d')
- colors = ['r', 'g', 'b', 'c'] # 颜色列表
- for i in range(len(all_points_3d_world)):
-
- plot_3d_points(all_points_3d_world[i], label=f'Cam {i}', color=[random.random() for _ in range(3)], ax=ax)
- # 显示图例和更新当前图像
- ax.legend(loc='upper right')
- plt.pause(2) # 暂停 1 秒,逐个显示效果
-
- # for cam_idx, (params, corners) in enumerate(zip(cam_params, all_corners)):
- # #print('all_points_3d_world[cam_idx] = ', all_points_3d_world[cam_idx])
- # plot_3d_points(all_points_3d_world[cam_idx], label=f'Cam {cam_idx}', color=colors[cam_idx], ax=ax)
- ax.set_xlabel('X')
- ax.set_ylabel('Y')
- ax.set_zlabel('Z')
- # ax.set_zlim([0, 500])
- plt.legend()
- plt.show()
- #cam_params[3]['rotation_matrix'] = np.eye(3)
- #cam_params[3]['translation_vector'] = np.zeros((3, 1))
- # stereo_calibration_data_0_3 = stereo_calibrate(all_detections, 0, 3, object_point_single, cam_params)
- # stereo_calibration_data_1_3 = stereo_calibrate(all_detections, 1, 3, object_point_single, cam_params)
- # stereo_calibration_data_2_3 = stereo_calibrate(all_detections, 2, 3, object_point_single, cam_params)
- # stereo_calib_data_list = []
- # stereo_calib_data_list.append(stereo_calibration_data_0_3)
- # stereo_calib_data_list.append(stereo_calibration_data_1_3)
- # stereo_calib_data_list.append(stereo_calibration_data_2_3)
- # # 设置相机 3 的外参不变
- # R_3 = cam_params[3]['rotation_matrix']
- # T_3 = cam_params[3]['translation_vector']
- # for i in [0, 1, 2]:
- # # 获取相机 i 的初始外参
- # R_i = cam_params[i]['rotation_matrix']
- # T_i = cam_params[i]['translation_vector']
-
- # # 计算初始相对位姿
- # R_i3_init = R_3 @ R_i.T
- # T_i3_init = -R_3 @ R_i.T @ T_i + T_3
-
- # # 获取双目标定结果
- # stereo_calib_data = stereo_calib_data_list[i]
- # R_i3_stereo = stereo_calib_data['rotation_matrix']
- # T_i3_stereo = stereo_calib_data['translation_vector']
-
- # # 计算位姿差异
- # delta_R = R_i3_stereo @ R_i3_init.T
- # delta_T = T_i3_stereo - delta_R @ T_i3_init
-
- # # 更新相机外参
- # R_i_new = delta_R @ R_i
- # T_i_new = delta_R @ T_i + delta_T
-
- # # 更新 cam_params
- # cam_params[i]['rotation_matrix'] = R_i_new
- # cam_params[i]['translation_vector'] = T_i_new
-
- # with open('D:\\code\\pmd-python\\config\\stereo_calib2.pkl', 'wb') as pkl_file:
- # pickle.dump(cam_params, pkl_file)
-
- def stereo_calibrate(all_detections, cam_id1, cam_id2, object_point_single, cam_params):
- detections1 = all_detections[cam_id1]
- detections2 = all_detections[cam_id2]
-
- detections_dict1 = {d.tag_id: d for d in detections1}
- detections_dict2 = {d.tag_id: d for d in detections2}
- # 用于保存所有图像的检测到的角点和 ID
- objpoints = [] # 3D 点
- imgpoints1 = [] # 相机1的 2D 点
- imgpoints2 = [] # 相机2的 2D 点
- image_size = None
- # 找到两个相机共同检测到的标签 ID
- common_tags = set(detections_dict1.keys()) & set(detections_dict2.keys())
- print('common_tags = ', common_tags)
- if common_tags:
- image_points1 = []
- image_points2 = []
- object_points = []
- for tag_id in common_tags:
- # 获取相机1的角点
- corners1 = detections_dict1[tag_id].corners.astype(np.float32)
- # 获取相机2的角点
- corners2 = detections_dict2[tag_id].corners.astype(np.float32)
- # 获取对应的 3D 点
- if tag_id < len(object_point_single):
- obj_pts = object_point_single[tag_id]
- else:
- print(f"Warning: Tag ID {tag_id} is out of range.")
- continue
- object_points.append(obj_pts)
- image_points1.append(corners1)
- image_points2.append(corners2)
- if object_points and image_points1 and image_points2:
- objpoints.append(np.array(object_points, dtype=np.float32).reshape(-1, 3))
- imgpoints1.append(np.array(image_points1, dtype=np.float32).reshape(-1, 2))
- imgpoints2.append(np.array(image_points2, dtype=np.float32).reshape(-1, 2))
- else:
- print("No valid common detections in this image pair.")
- else:
- print("No common tags detected in this image pair.")
-
- K1 = cam_params[cam_id1]['camera_matrix']
- dist1 = cam_params[cam_id1]['distortion_coefficients']
- K2 = cam_params[cam_id2]['camera_matrix']
- dist2 = cam_params[cam_id2]['distortion_coefficients']
- # 立体标定
- flags = 0
- flags |= cv2.CALIB_FIX_INTRINSIC # 固定单目内参
- criteria = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 100, 1e-5)
- ret, _, _, _, _, R, T, E, F = cv2.stereoCalibrate(
- objpoints, # 3D 点
- imgpoints1, # 相机1的 2D 点
- imgpoints2, # 相机2的 2D 点
- K1, # 相机1的内参
- dist1, # 相机1的畸变系数
- K2, # 相机2的内参
- dist2, # 相机2的畸变系数
- image_size, # 图像尺寸
- criteria=criteria,
- flags=flags
- )
- print(f"Stereo Calibration between Camera {cam_id1} and Camera {cam_id2} completed.")
- print(f"Rotation Matrix R:\n{R}")
- print(f"Translation Vector T:\n{T}")
- # 将结果保存到字典中
- stereo_calibration_data = {
- 'camera_matrix1': K1,
- 'distortion_coefficients1': dist1,
- 'camera_matrix2': K2,
- 'distortion_coefficients2': dist2,
- 'rotation_matrix': R,
- 'translation_vector': T,
- 'essential_matrix': E,
- 'fundamental_matrix': F,
- 'reprojection_error': ret
- }
- # 返回标定结果
- print('stereo_calibration_data = ', stereo_calibration_data)
- return stereo_calibration_data
- # ######################## bundle_adjustment
- # # 遍历每个相机
- # for cam_id, params in enumerate(cam_params):
- # K_matrices.append(params['camera_matrix'])
- # dist_coeffs.append(params['distortion_coefficients'])
-
- # camera_corners = [] # 用于当前相机的2D图像点
- # camera_object_points = [] # 用于当前相机的3D物体点
- # # 对每个相机的标定图像提取2D角点
- # for img_file in os.listdir(calibration_images_path[cam_id]):
- # # 读取图像并检测AprilTags,获取2D角点和对应的3D物体点
- # image_points, object_points, tag_lst = detect_april_tag_points(
- # detector, calibration_images_path[cam_id] + img_file, object_point_single
- # )
- # # 将检测到的2D图像点和3D物体点保存到当前相机的列表
- # if image_points is not None and object_points is not None:
- # camera_corners.append(image_points)
- # camera_object_points.append(object_points)
-
- # # 保存当前相机的所有图像点到全局列表
- # all_corners.append(camera_corners)
- # all_object_points.append(camera_object_points)
- # # 调用捆绑调整函数,优化所有相机的内参、外参和3D点
- # plane_constraints = [
- # [0, 1, 2, 3], # 第一个平面的点索引
- # [4, 5, 6, 7] # 第二个平面的点索引
- # ]
- # result = bundle_adjustment(all_object_points, all_corners, cam_params, plane_constraints)
-
- # print(result)
- # optimized_rvecs, optimized_tvecs = extract_params_from_result(result, 4, len(all_object_points))
- # original_rvecs = [cv2.Rodrigues(params['rotation_matrix'])[0] for params in cam_params]
- # original_tvecs = [params['translation_vector'] for params in cam_params]
- # print('optimized_rvecs = ',optimized_rvecs)
-
- # for i in range(4):
- # optimized_rotation_matrices = cv2.Rodrigues(optimized_rvecs[i])[0]
- # #print(original_tvecs[i], optimized_tvecs[i].reshape(-1, 1))
- # calibration_data = {
- # 'camera_matrix': cam_params[i]['camera_matrix'],
- # 'distortion_coefficients': cam_params[i]['distortion_coefficients'],
- # 'rotation_matrix': optimized_rotation_matrices,
- # 'translation_vector': optimized_tvecs[i].reshape(-1, 1)
- # }
-
- # optimized_cam_params.append(calibration_data)
- # with open('D:\\code\\pmd-python\\config\\optimized_param1021.pkl', 'wb') as pkl_file:
-
- # pickle.dump(optimized_cam_params, pkl_file)
-
- # # 打印优化后的结果
- # print("Bundle Adjustment 完成!")
- # #绘制相机
- # fig = plt.figure()
- # ax = fig.add_subplot(111, projection='3d')
- # # 绘制每个相机
- # for ii in range(4):
- # plot_camera(ax, cam_params[ii]['rotation_matrix'], cam_params[ii]['translation_vector'], ii, scale=40, color='r')
- # # 绘制物体 (假设是一个简单的平面物体,位于 z = 0 平面上)
- # plane_x, plane_y = np.meshgrid(np.linspace(-500, 500, 10), np.linspace(-500, 500, 10))
- # plane_z = np.zeros_like(plane_x) # z = 0 的平面
- # ax.plot_surface(plane_x, plane_y, plane_z, color='blue', alpha=0.3)
- # # 设置轴的范围和标签
- # ax.set_xlabel('X axis')
- # ax.set_ylabel('Y axis')
- # ax.set_zlabel('Z axis')
- # ax.set_xlim([-50, 300])
- # ax.set_ylim([-50, 300])
- # ax.set_zlim([0, 500])
- # plt.show()
- def plt_gaussion():
- # 1. Create a Gaussian surface
- X = np.linspace(-3, 3, 100)
- Y = np.linspace(-3, 3, 100)
- X, Y = np.meshgrid(X, Y)
- Z = np.exp(-(X**2 + Y**2))
- # 2. Compute gradients
- Zx, Zy = np.gradient(Z)
- # 3. Plotting the Gaussian surface
- fig = plt.figure(figsize=(14, 6))
- # Plot 1: Gaussian Surface
- ax1 = fig.add_subplot(121, projection='3d')
- ax1.plot_surface(X, Y, Z, cmap='viridis', edgecolor='none')
- ax1.set_title('Gaussian Surface')
- ax1.set_xlabel('X')
- ax1.set_ylabel('Y')
- ax1.set_zlabel('Z')
- # Plot 2: Gradient Field Visualization
- ax2 = fig.add_subplot(122)
- ax2.quiver(X, Y, Zx, Zy, color='r')
- ax2.set_title('Gradient Field of Gaussian Surface')
- ax2.set_xlabel('X')
- ax2.set_ylabel('Y')
- plt.tight_layout()
- plt.show()
- if __name__ == '__main__':
- # white_path = 'D:\\data\\four_cam\\1008_storage\\'
- # cam_num = 1
- # #find_notch(white_path, cam_num)
- # for img in os.listdir(white_path):
- # if img.endswith('.bmp'):
- # get_white_mask(white_path + img, bin_thresh=12)
- camera_calibrate_vis()
- #plt_gaussion()
- #single_camera_calibrate_vis()
-
|