张正友相机标定法确定相机内参

本文采用张正友标定法确认相机的内参,代码环境为Python3.12。

 
流程简介:

1、先用同一套棋盘(内角点尺寸为 5×6、方格边长 25 mm)在相机不同姿态下拍摄多张覆盖视场的平面棋盘图片;
2、对每张图片用 OpenCV 的 findChessboardCorners 检测内角点并用 cornerSubPix 做亚像素精修,按棋盘格的物理坐标(以 0.025 m 为单位)生成对应的三维目标点(所有点都在同一平面上);
3、然后把这些二维角点与对应的三维平面点送入标定算法(OpenCV 的 calibrateCamera 实现了张正友方法的思想:通过每幅图像估计平面对像平面的单应矩阵、由多个单应求解相机内参的线性近似,再用非线性最小二乘对内参、畸变系数及每幅图的外参进行联合优化以最小化重投影误差)。
4、运行结束后你得到了相机内参矩阵(fx, fy, cx, cy)、畸变系数(k1,k2,p1,p2,k3…)、以及每张图的旋转向量和平移向量(tvec 单位为米,因为你用了 0.025 m 的方格边长);并用重投影误差(你得到的平均约 0.0899 px,calibrateCamera 返回 RMS ≈ 0.4966)评估标定精度——误差极小,说明标定质量很好。
最后把结果保存为 camera_calibration_results.npz(并生成 corners_all.csv、可视化图 _corners.jpg / _reproj.jpg),便于后续去畸变、位姿估计(PnP)或把参数导出为 YAML/JSON 在其他程序中复用。

相机拍摄技巧:
用A4纸打印2-4张棋盘 chessboard_10x7_A4.pdf 采用阿里网盘保存的,可自行下载。
 

1、准备阶段: 用需要测量的摄像机拍摄照片,多个角度、方向拍摄。我拍了400多张才有这几张有效。

张正友相机标定法确定相机内参
张正友相机标定法确定相机内参
张正友相机标定法确定相机内参
张正友相机标定法确定相机内参
张正友相机标定法确定相机内参
张正友相机标定法确定相机内参
 

2、做检测前创建一个Python项目,在Python代码根目录创建一个calib_images目录。这个目录就是用来存放你做检测的图片

3、调用下面这段Python,来测试你相机的内参。

点击查看代码
import cv2 import numpy as np import glob import os import csv from datetime import datetime  def calibrate_camera(calib_dir="calib_images",                      square_size_mm=25.0,                      min_good_images=5,                      candidate_cols_range=(4, 10),                      candidate_rows_range=(3, 8),                      save_csv="corners_all.csv"):     """     针对用户场景(同一相机、同一棋盘、square_size 已知)做的标定脚本。     - 先尝试在第一张图片上自动检测棋盘尺寸 (CHECKERBOARD),找到后固定该尺寸用于所有图片。     - 打印并保存每张图片的角点像素坐标,保存合并 CSV 供检查。     """     square_size = float(square_size_mm) / 1000.0  # mm -> m     if not os.path.exists(calib_dir):         print(f"错误:未找到目录 '{calib_dir}',请确认路径。")         return      # 收集图片     image_formats = ["*.jpg", "*.jpeg", "*.png", "*.bmp"]     images = []     for fmt in image_formats:         images.extend(sorted(glob.glob(os.path.join(calib_dir, fmt))))     if not images:         print(f"错误:目录 '{calib_dir}' 中未找到任何图片。")         return      print(f"找到 {len(images)} 张图片({calib_dir})。开始检测 — {datetime.now().isoformat()}n")      # 候选 pattern 列表(内角点数)     candidate_patterns = [(c, r) for c in range(candidate_cols_range[0], candidate_cols_range[1] + 1)                           for r in range(candidate_rows_range[0], candidate_rows_range[1] + 1)]      # 先用第一张图自动识别棋盘尺寸(更稳妥)     first_img = cv2.imread(images[0])     first_gray = cv2.cvtColor(first_img, cv2.COLOR_BGR2GRAY)     found_pattern = None     flags = cv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE | cv2.CALIB_CB_FAST_CHECK      print("尝试在第一张图片上检测棋盘尺寸 (自动识别)...")     for pat in candidate_patterns:         ret, _ = cv2.findChessboardCorners(first_gray, pat, flags)         if ret:             found_pattern = pat             print(f"在第一张图片检测到棋盘内角点尺寸: {found_pattern},将其作为全局 CHECKERBOARD。n")             break      if found_pattern is None:         print("警告:第一张图片未能自动识别棋盘尺寸。将遍历候选尺寸寻找第一个能识别的尺寸...")         for pat in candidate_patterns:             ret, _ = cv2.findChessboardCorners(first_gray, pat, flags)             if ret:                 found_pattern = pat                 print(f"找到尺寸: {found_pattern}n")                 break      if found_pattern is None:         print("注意:未在第一张图片找到可用尺寸。脚本将对每张图片分别尝试候选尺寸(回退策略)。n")      # 准备存放点与 CSV 输出     objpoints = []     imgpoints = []     used_image_names = []     detected_patterns = []      csv_rows = []     csv_header = ["image", "pattern_cols", "pattern_rows", "corner_index", "x", "y"]      # 检测所有图片     for idx, fname in enumerate(images, start=1):         img = cv2.imread(fname)         if img is None:             print(f"[{idx}/{len(images)}] 警告:无法读取图片 {os.path.basename(fname)},跳过")             continue         gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)         success = False         tried_patterns = []          patterns_to_try = [found_pattern] if found_pattern is not None else candidate_patterns         # 若 found_pattern 不为 None,优先尝试它;若失败再回退到所有 candidate_patterns         for pat in patterns_to_try:             if pat is None:                 continue             tried_patterns.append(pat)             ret, corners = cv2.findChessboardCorners(gray, pat, flags)             if ret:                 # 亚像素精修                 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)                 corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)                  cols, rows = pat                 objp = np.zeros((rows * cols, 3), np.float32)                 objp[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2)                 objp *= square_size                  objpoints.append(objp)                 imgpoints.append(corners2)                 used_image_names.append(os.path.basename(fname))                 detected_patterns.append(pat)                  # 保存带角点图                 vis = img.copy()                 cv2.drawChessboardCorners(vis, pat, corners2, ret)                 result_path = os.path.splitext(fname)[0] + "_corners.jpg"                 cv2.imwrite(result_path, vis)                  # 打印并记录坐标                 corners_xy = corners2.reshape(-1, 2)                 print(f"[{idx}/{len(images)}] {os.path.basename(fname)}: 检测成功, pattern={pat}, corners={len(corners_xy)}")                 per_line = 8                 coord_strs = [f"({x:.3f}, {y:.3f})" for x, y in corners_xy]                 for i in range(0, len(coord_strs), per_line):                     print("   " + "  ".join(coord_strs[i:i+per_line]))                 print(f"   结果图已保存: {os.path.basename(result_path)}n")                  # 写入 CSV 行                 for i_pt, (x, y) in enumerate(corners_xy):                     csv_rows.append([os.path.basename(fname), cols, rows, i_pt, f"{x:.6f}", f"{y:.6f}"])                  success = True                 break  # 找到 pattern 则跳出 pattern 尝试          if not success:             # 如果之前只尝试了 found_pattern,而没有成功,回退尝试所有 candidate_patterns             if found_pattern is not None:                 for pat in candidate_patterns:                     if pat in tried_patterns:                         continue                     ret, corners = cv2.findChessboardCorners(gray, pat, flags)                     if ret:                         criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)                         corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)                         cols, rows = pat                         objp = np.zeros((rows * cols, 3), np.float32)                         objp[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2)                         objp *= square_size                         objpoints.append(objp)                         imgpoints.append(corners2)                         used_image_names.append(os.path.basename(fname))                         detected_patterns.append(pat)                          vis = img.copy()                         cv2.drawChessboardCorners(vis, pat, corners2, ret)                         result_path = os.path.splitext(fname)[0] + "_corners.jpg"                         cv2.imwrite(result_path, vis)                          corners_xy = corners2.reshape(-1, 2)                         print(f"[{idx}/{len(images)}] {os.path.basename(fname)}: 检测成功 (回退), pattern={pat}, corners={len(corners_xy)}")                         coord_strs = [f"({x:.3f}, {y:.3f})" for x, y in corners_xy]                         for i in range(0, len(coord_strs), 8):                             print("   " + "  ".join(coord_strs[i:i+8]))                         print(f"   结果图已保存: {os.path.basename(result_path)}n")                          for i_pt, (x, y) in enumerate(corners_xy):                             csv_rows.append([os.path.basename(fname), cols, rows, i_pt, f"{x:.6f}", f"{y:.6f}"])                         success = True                         break          if not success:             print(f"[{idx}/{len(images)}] {os.path.basename(fname)}: 未检测到角点,已跳过n")      # 保存 CSV(所有角点)     if csv_rows:         with open(save_csv, "w", newline='', encoding='utf-8') as f:             writer = csv.writer(f)             writer.writerow(csv_header)             writer.writerows(csv_rows)         print(f"所有角点坐标已合并保存为: {save_csv}n")     else:         print("未检测到任何角点,CSV 未生成。")      good_n = len(objpoints)     print(f"成功检测到 {good_n} 张有效图片(用于标定)。需要至少 {min_good_images} 张。")     if good_n < min_good_images:         print("有效图片不足,无法进行可靠标定。请检查图片质量或拍摄更多不同视角的棋盘照片。")         return      # image_size 使用第一张图片(因为你确认所有图片相同分辨率)     sample_img = cv2.imread(images[0])     image_size = (sample_img.shape[1], sample_img.shape[0])  # (width, height)      # 标定     print("开始标定(calibrateCamera)...")     rms, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, image_size, None, None)      print("n===== 标定结果 =====")     print(f"calibrateCamera 返回 RMS: {rms:.6f}")     print("相机内参 (camera_matrix):")     print(camera_matrix)     print("n畸变系数 (dist_coeffs):")     print(dist_coeffs.ravel())      # 平均重投影误差     total_error = 0.0     for i in range(len(objpoints)):         imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], camera_matrix, dist_coeffs)         error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)         total_error += error     mean_error = total_error / len(objpoints)     print(f"n平均重投影误差: {mean_error:.6f} 像素")      # 详细打印内参与畸变     fx = camera_matrix[0, 0]; fy = camera_matrix[1, 1]; cx = camera_matrix[0, 2]; cy = camera_matrix[1, 2]     print("n相机内参 (详细):")     print(f" fx = {fx:.6f}")     print(f" fy = {fy:.6f}")     print(f" cx = {cx:.6f}")     print(f" cy = {cy:.6f}")     coeffs = dist_coeffs.ravel()     # 可能只有 4 或 5 个畸变参数     coeffs_str = ", ".join([f"{v:.8f}" for v in coeffs])     print(f"n畸变系数: {coeffs_str}")      # 保存结果     np.savez("camera_calibration_results.npz",              camera_matrix=camera_matrix,              dist_coeffs=dist_coeffs,              rvecs=rvecs,              tvecs=tvecs,              mean_reprojection_error=mean_error,              used_image_names=np.array(used_image_names),              detected_patterns=np.array(detected_patterns),              square_size_m=square_size)     print("n标定数据已保存: camera_calibration_results.npz")     print("完成。")  if __name__ == "__main__":     calibrate_camera()  

4、最后会把输出内容:

1). 结果打印出来;

2). 生成每个图片内角点标注的图片文件 图片名_corners.jpg

3). 输出一个 camera_calibration_results.npz 文件;

4). 以及 corners_all.csv文件。

 

5、如有些看不懂,则用下面这段代码转换下。即可分为机器与人都能看懂的文件。代码如下:

点击查看代码
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ print_calib_readable.py 从 camera_calibration_results.npz 读取标定结果, 打印易读报告,并同时生成:  - calibration_readable.txt  (人类可读)  - calibration_summary.json  (机器可读)  - per_image_reprojection.csv (若 corners_all.csv 存在且可计算逐图误差) 修复 ValueError 的关键点:不把 numpy array 当作布尔值直接判断,而是检查 data.files 中是否包含字段。 """  import numpy as np import cv2 import os import csv import json from math import sqrt  NPZ_FILE = "camera_calibration_results.npz" CORNERS_CSV = "corners_all.csv" READABLE_TXT = "calibration_readable.txt" SUMMARY_JSON = "calibration_summary.json" PER_IMAGE_CSV = "per_image_reprojection.csv"  def load_npz(fn):     if not os.path.exists(fn):         raise FileNotFoundError(f"未找到文件: {fn}")     return np.load(fn, allow_pickle=True)  def to_str_list(arr):     """把 numpy array/iterable 转为 str 列表,兼容 bytes"""     if arr is None:         return None     out = []     try:         for v in arr:             if isinstance(v, bytes):                 out.append(v.decode('utf-8', errors='ignore'))             else:                 out.append(str(v))     except Exception:         # fallback: single value         try:             if isinstance(arr, bytes):                 return [arr.decode('utf-8', errors='ignore')]             return [str(arr)]         except Exception:             return None     return out  def read_corners_csv(csv_path):     """读取 corners_all.csv (image, pattern_cols, pattern_rows, corner_index, x, y)"""     if not os.path.exists(csv_path):         return None     d = {}     with open(csv_path, newline='', encoding='utf-8') as f:         reader = csv.DictReader(f)         for row in reader:             name = row['image']             x = float(row['x']); y = float(row['y'])             idx = int(row['corner_index'])             d.setdefault(name, []).append((idx, (x, y)))     # sort by index and convert to np.array     for name in list(d.keys()):         arr = [p for i,p in sorted(d[name], key=lambda it: it[0])]         d[name] = np.array(arr, dtype=np.float32)     return d  def format_mat(mat):     return "n".join(["  [" + "  ".join(f"{v:12.6f}" for v in row) + "]" for row in mat])  def safe_get(data, *keys):     """从 data (npz) 中按优先级取值,返回 None 或 value"""     for k in keys:         if k in data.files:             return data[k]     return None  def ensure_list_of_vectors(arr):     """保证 rvecs/tvecs 转为 python list,元素为 np.array"""     if arr is None:         return None     try:         return [np.array(v) for v in arr]     except Exception:         # 如果是单个 array 扩展为 list         try:             return [np.array(arr)]         except Exception:             return None  def main():     try:         data = load_npz(NPZ_FILE)     except Exception as e:         print("加载 NPZ 失败:", e)         return      # 安全地获取字段(不直接用 data.get(...) 或在 if 中用数组判断)     camera_matrix = safe_get(data, "camera_matrix", "mtx", "camera_mat", "camera_matx")     dist_coeffs = safe_get(data, "dist_coeffs", "dist_coeff", "dist", "dist_coeff")     rvecs = safe_get(data, "rvecs")     tvecs = safe_get(data, "tvecs")     mean_err = safe_get(data, "mean_reprojection_error", "mean_error")     used_images_raw = safe_get(data, "used_image_names")     detected_patterns_raw = safe_get(data, "detected_patterns")     square_size_m_val = safe_get(data, "square_size_m")     rms_val = safe_get(data, "rms", "calibrate_rms")  # optional      # normalize some fields     used_images = to_str_list(used_images_raw) if used_images_raw is not None else None      # detected_patterns may be array-like of tuples or strings; normalize to list of (cols,rows) tuples     detected_patterns = None     if detected_patterns_raw is not None:         try:             # try to convert to list of tuples             tmp = []             for p in detected_patterns_raw:                 # p could be array([c,r]) or b'(c,r)'                 if isinstance(p, (np.ndarray, list, tuple)):                     tmp.append((int(p[0]), int(p[1])))                 else:                     # try parse                     s = str(p)                     s = s.strip("()[] ")                     parts = [int(x) for x in s.replace(",", " ").split() if x.strip().isdigit()]                     if len(parts) >= 2:                         tmp.append((parts[0], parts[1]))             detected_patterns = tmp         except Exception:             detected_patterns = None      # convert rvecs/tvecs to python lists     rvecs_list = ensure_list_of_vectors(rvecs)     tvecs_list = ensure_list_of_vectors(tvecs)      # start composing readable text and summary dict     lines = []     lines.append("===== Camera Calibration (Human-Readable) =====n")      if camera_matrix is None:         lines.append("错误:NPZ 文件中未找到相机内参 (camera_matrix)。n")         # write file and exit         with open(READABLE_TXT, "w", encoding="utf-8") as f:             f.write("n".join(lines))         print("已生成 (部分) 可读文件:", READABLE_TXT)         return      # print camera matrix     lines.append("相机内参 (camera_matrix) — 3x3 矩阵 (单位: 像素):")     lines.append(format_mat(camera_matrix))     fx = float(camera_matrix[0,0]); fy = float(camera_matrix[1,1]); cx = float(camera_matrix[0,2]); cy = float(camera_matrix[1,2])     skew = float(camera_matrix[0,1]) if camera_matrix.shape[1] > 1 else 0.0     lines.append("n标准参数名称与数值:")     lines.append(f" fx  (焦距 x方向)        = {fx:.6f} px")     lines.append(f" fy  (焦距 y方向)        = {fy:.6f} px")     lines.append(f" cx  (主点 x)            = {cx:.6f} px")     lines.append(f" cy  (主点 y)            = {cy:.6f} px")     lines.append(f" skew (相机倾斜/skew)    = {skew:.6f} (通常接近 0)n")      # distortion     if dist_coeffs is None:         lines.append("畸变系数未找到 (dist_coeffs)。n")         dist_list = []     else:         d = np.array(dist_coeffs).ravel()         lines.append("畸变系数 (distortion coefficients):")         labels = ["k1","k2","p1","p2","k3","k4","k5","k6"]         for i,val in enumerate(d):             lab = labels[i] if i < len(labels) else f"d{i+1}"             lines.append(f" {lab:3s} = {float(val):.8f}")         lines.append("(顺序通常为 k1, k2, p1, p2, k3 )n")         dist_list = [float(x) for x in d.tolist()]      # RMS and mean reprojection     if rms_val is not None:         try:             rms_f = float(rms_val)             lines.append(f"calibrateCamera 返回 RMS (函数返回值) = {rms_f:.6f}")         except Exception:             pass     if mean_err is not None:         try:             mean_err_f = float(mean_err)             lines.append(f"mean_reprojection_error (按图片平均计算) = {mean_err_f:.6f} 像素")         except Exception:             pass     lines.append("(推荐关注按图片平均的 mean_reprojection_error,更直观)n")      if square_size_m_val is not None:         try:             ss = float(square_size_m_val)             lines.append(f"棋盘方格实际边长 (square_size_m) = {ss:.6f} m")         except Exception:             pass      # patterns & images     if detected_patterns is not None:         unique = []         for p in detected_patterns:             if p not in unique:                 unique.append(p)         if len(unique) == 1:             lines.append(f"使用的棋盘内角点尺寸 (pattern cols,rows) = {unique[0]} (一致,所有图片使用同一尺寸)")             common_pattern = unique[0]         else:             lines.append("每张图片检测到的棋盘内角点尺寸 (per-image):")             for i, p in enumerate(detected_patterns):                 name = used_images[i] if used_images and i < len(used_images) else f"img#{i}"                 lines.append(f"  {name:20s} -> {p}")             common_pattern = None     else:         common_pattern = None      if used_images is not None:         lines.append(f"n用于标定的图片数量 = {len(used_images)}")         lines.append("图片列表(用于标定,按序):")         for nm in used_images:             lines.append("  - " + nm)     lines.append("")      # write readable txt now (we will append per-image detail later)     with open(READABLE_TXT, "w", encoding="utf-8") as f:         f.write("n".join(lines))     print("已生成可读文本报告:", READABLE_TXT)      # 准备 summary json     summary = {         "fx": fx, "fy": fy, "cx": cx, "cy": cy, "skew": skew,         "distortion": {f"k{i+1}": (dist_list[i] if i < len(dist_list) else None) for i in range(6)},         "distortion_raw": dist_list,         "rms": float(rms_val) if rms_val is not None else None,         "mean_reprojection_error": float(mean_err) if mean_err is not None else None,         "square_size_m": float(square_size_m_val) if square_size_m_val is not None else None,         "pattern_common": common_pattern,         "images_used": used_images if used_images is not None else []     }      # 如果存在 corners_all.csv,则计算逐图重投影误差并写入 CSV 与 JSON     corners_dict = read_corners_csv(CORNERS_CSV)     per_image_stats = []     if corners_dict is None:         print(f"未找到 {CORNERS_CSV}(可选)。若存在该文件,脚本会计算逐图重投影误差及 rvec/tvec。")     else:         # ensure rvecs/tvecs exist         if rvecs_list is None or tvecs_list is None:             print("注意:NPZ 中缺少 rvecs/tvecs,无法计算逐图重投影误差。")         else:             # iterate using used_images order if available             names_order = used_images if used_images is not None else sorted(list(corners_dict.keys()))             # prepare CSV writer             with open(PER_IMAGE_CSV, "w", newline='', encoding='utf-8') as fcsv:                 writer = csv.writer(fcsv)                 writer.writerow(["image", "pattern_cols", "pattern_rows", "corner_count", "rmse_px",                                  "tvec_x_m", "tvec_y_m", "tvec_z_m", "rvec_x", "rvec_y", "rvec_z"])                 for i, name in enumerate(names_order):                     base = os.path.basename(name)                     if base not in corners_dict:                         # try direct name (maybe used_images stores base names already)                         if name in corners_dict:                             base = name                         else:                             print(f"{base}: 在 {CORNERS_CSV} 中未找到对应角点,跳过逐图误差计算。")                             continue                     img_corners = corners_dict[base]                     if i >= len(rvecs_list) or i >= len(tvecs_list):                         print(f"{base}: rvecs/tvecs 不足,跳过。")                         continue                     rvec = rvecs_list[i].reshape(3,1)                     tvec = tvecs_list[i].reshape(3,1)                     # construct objp based on detected_patterns if available else skip                     if detected_patterns is None or i >= len(detected_patterns):                         print(f"{base}: 无法构造 objpoints(未保存 detected_patterns),跳过该图重投影计算。")                         continue                     pat = detected_patterns[i]                     cols, rows = pat                     objp = np.zeros((rows*cols, 3), np.float32)                     objp[:,:2] = np.mgrid[0:cols, 0:rows].T.reshape(-1,2)                     if square_size_m_val is not None:                         objp[:,:2] *= float(square_size_m_val)                     # project                     proj, _ = cv2.projectPoints(objp, rvec, tvec, camera_matrix, dist_coeffs)                     proj = proj.reshape(-1,2)                     if proj.shape != img_corners.shape:                         print(f"{base}: 投影点数量 {proj.shape[0]} 与 CSV 中角点数量 {img_corners.shape[0]} 不匹配,跳过。")                         continue                     err = np.sqrt(np.mean(np.sum((img_corners - proj)**2, axis=1)))                     R, _ = cv2.Rodrigues(rvec)                     writer.writerow([base, cols, rows, int(img_corners.shape[0]), float(err),                                      float(tvec[0,0]), float(tvec[1,0]), float(tvec[2,0]),                                      float(rvec[0,0]), float(rvec[1,0]), float(rvec[2,0])])                     per_image_stats.append({                         "image": base,                         "pattern": [int(cols), int(rows)],                         "corner_count": int(img_corners.shape[0]),                         "rmse_px": float(err),                         "tvec_m": [float(tvec[0,0]), float(tvec[1,0]), float(tvec[2,0])],                         "rvec": [float(rvec[0,0]), float(rvec[1,0]), float(rvec[2,0])]                     })             print("已生成逐图重投影 CSV:", PER_IMAGE_CSV)      summary["per_image"] = per_image_stats      # 写入 summary json     with open(SUMMARY_JSON, "w", encoding='utf-8') as fj:         json.dump(summary, fj, indent=2, ensure_ascii=False)     print("已生成机器可读摘要:", SUMMARY_JSON)      # append per-image human-readable section to readable txt     with open(READABLE_TXT, "a", encoding='utf-8') as f:         f.write("nn===== Per-image Reprojection Summary =====nn")         if not per_image_stats:             f.write("无可用的逐图重投影结果(可能缺少 corners_all.csv 或 rvecs/tvecs/detected_patterns)。n")         else:             for s in per_image_stats:                 f.write(f"Image: {s['image']}n")                 f.write(f"  pattern (cols,rows) = {s['pattern']} ; corner_count = {s['corner_count']}n")                 f.write(f"  reprojection RMSE = {s['rmse_px']:.6f} pxn")                 f.write(f"  tvec (m) = [{s['tvec_m'][0]:.6f}, {s['tvec_m'][1]:.6f}, {s['tvec_m'][2]:.6f}]n")                 f.write(f"  rvec = [{s['rvec'][0]:.6f}, {s['rvec'][1]:.6f}, {s['rvec'][2]:.6f}]nn")     print("已把逐图结果追加到可读报告:", READABLE_TXT)     print("n全部完成。")  if __name__ == "__main__":     main()  
发表评论

评论已关闭。

相关文章