找回密码
 立即注册
首页 业界区 业界 张正友相机标定法确定相机内参

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

汤流婉 2025-8-20 13:42:24
本文采用张正友标定法确认相机的内参,代码环境为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多张才有这几张有效。


1.jpeg

2.jpeg

3.jpeg

4.jpeg

5.jpeg

6.jpeg

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


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

点击查看代码
  1. import cv2
  2. import numpy as np
  3. import glob
  4. import os
  5. import csv
  6. from datetime import datetime
  7. def calibrate_camera(calib_dir="calib_images",
  8.                      square_size_mm=25.0,
  9.                      min_good_images=5,
  10.                      candidate_cols_range=(4, 10),
  11.                      candidate_rows_range=(3, 8),
  12.                      save_csv="corners_all.csv"):
  13.     """
  14.     针对用户场景(同一相机、同一棋盘、square_size 已知)做的标定脚本。
  15.     - 先尝试在第一张图片上自动检测棋盘尺寸 (CHECKERBOARD),找到后固定该尺寸用于所有图片。
  16.     - 打印并保存每张图片的角点像素坐标,保存合并 CSV 供检查。
  17.     """
  18.     square_size = float(square_size_mm) / 1000.0  # mm -> m
  19.     if not os.path.exists(calib_dir):
  20.         print(f"错误:未找到目录 '{calib_dir}',请确认路径。")
  21.         return
  22.     # 收集图片
  23.     image_formats = ["*.jpg", "*.jpeg", "*.png", "*.bmp"]
  24.     images = []
  25.     for fmt in image_formats:
  26.         images.extend(sorted(glob.glob(os.path.join(calib_dir, fmt))))
  27.     if not images:
  28.         print(f"错误:目录 '{calib_dir}' 中未找到任何图片。")
  29.         return
  30.     print(f"找到 {len(images)} 张图片({calib_dir})。开始检测 — {datetime.now().isoformat()}\n")
  31.     # 候选 pattern 列表(内角点数)
  32.     candidate_patterns = [(c, r) for c in range(candidate_cols_range[0], candidate_cols_range[1] + 1)
  33.                           for r in range(candidate_rows_range[0], candidate_rows_range[1] + 1)]
  34.     # 先用第一张图自动识别棋盘尺寸(更稳妥)
  35.     first_img = cv2.imread(images[0])
  36.     first_gray = cv2.cvtColor(first_img, cv2.COLOR_BGR2GRAY)
  37.     found_pattern = None
  38.     flags = cv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE | cv2.CALIB_CB_FAST_CHECK
  39.     print("尝试在第一张图片上检测棋盘尺寸 (自动识别)...")
  40.     for pat in candidate_patterns:
  41.         ret, _ = cv2.findChessboardCorners(first_gray, pat, flags)
  42.         if ret:
  43.             found_pattern = pat
  44.             print(f"在第一张图片检测到棋盘内角点尺寸: {found_pattern},将其作为全局 CHECKERBOARD。\n")
  45.             break
  46.     if found_pattern is None:
  47.         print("警告:第一张图片未能自动识别棋盘尺寸。将遍历候选尺寸寻找第一个能识别的尺寸...")
  48.         for pat in candidate_patterns:
  49.             ret, _ = cv2.findChessboardCorners(first_gray, pat, flags)
  50.             if ret:
  51.                 found_pattern = pat
  52.                 print(f"找到尺寸: {found_pattern}\n")
  53.                 break
  54.     if found_pattern is None:
  55.         print("注意:未在第一张图片找到可用尺寸。脚本将对每张图片分别尝试候选尺寸(回退策略)。\n")
  56.     # 准备存放点与 CSV 输出
  57.     objpoints = []
  58.     imgpoints = []
  59.     used_image_names = []
  60.     detected_patterns = []
  61.     csv_rows = []
  62.     csv_header = ["image", "pattern_cols", "pattern_rows", "corner_index", "x", "y"]
  63.     # 检测所有图片
  64.     for idx, fname in enumerate(images, start=1):
  65.         img = cv2.imread(fname)
  66.         if img is None:
  67.             print(f"[{idx}/{len(images)}] 警告:无法读取图片 {os.path.basename(fname)},跳过")
  68.             continue
  69.         gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  70.         success = False
  71.         tried_patterns = []
  72.         patterns_to_try = [found_pattern] if found_pattern is not None else candidate_patterns
  73.         # 若 found_pattern 不为 None,优先尝试它;若失败再回退到所有 candidate_patterns
  74.         for pat in patterns_to_try:
  75.             if pat is None:
  76.                 continue
  77.             tried_patterns.append(pat)
  78.             ret, corners = cv2.findChessboardCorners(gray, pat, flags)
  79.             if ret:
  80.                 # 亚像素精修
  81.                 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
  82.                 corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
  83.                 cols, rows = pat
  84.                 objp = np.zeros((rows * cols, 3), np.float32)
  85.                 objp[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2)
  86.                 objp *= square_size
  87.                 objpoints.append(objp)
  88.                 imgpoints.append(corners2)
  89.                 used_image_names.append(os.path.basename(fname))
  90.                 detected_patterns.append(pat)
  91.                 # 保存带角点图
  92.                 vis = img.copy()
  93.                 cv2.drawChessboardCorners(vis, pat, corners2, ret)
  94.                 result_path = os.path.splitext(fname)[0] + "_corners.jpg"
  95.                 cv2.imwrite(result_path, vis)
  96.                 # 打印并记录坐标
  97.                 corners_xy = corners2.reshape(-1, 2)
  98.                 print(f"[{idx}/{len(images)}] {os.path.basename(fname)}: 检测成功, pattern={pat}, corners={len(corners_xy)}")
  99.                 per_line = 8
  100.                 coord_strs = [f"({x:.3f}, {y:.3f})" for x, y in corners_xy]
  101.                 for i in range(0, len(coord_strs), per_line):
  102.                     print("   " + "  ".join(coord_strs[i:i+per_line]))
  103.                 print(f"   结果图已保存: {os.path.basename(result_path)}\n")
  104.                 # 写入 CSV 行
  105.                 for i_pt, (x, y) in enumerate(corners_xy):
  106.                     csv_rows.append([os.path.basename(fname), cols, rows, i_pt, f"{x:.6f}", f"{y:.6f}"])
  107.                 success = True
  108.                 break  # 找到 pattern 则跳出 pattern 尝试
  109.         if not success:
  110.             # 如果之前只尝试了 found_pattern,而没有成功,回退尝试所有 candidate_patterns
  111.             if found_pattern is not None:
  112.                 for pat in candidate_patterns:
  113.                     if pat in tried_patterns:
  114.                         continue
  115.                     ret, corners = cv2.findChessboardCorners(gray, pat, flags)
  116.                     if ret:
  117.                         criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
  118.                         corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
  119.                         cols, rows = pat
  120.                         objp = np.zeros((rows * cols, 3), np.float32)
  121.                         objp[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2)
  122.                         objp *= square_size
  123.                         objpoints.append(objp)
  124.                         imgpoints.append(corners2)
  125.                         used_image_names.append(os.path.basename(fname))
  126.                         detected_patterns.append(pat)
  127.                         vis = img.copy()
  128.                         cv2.drawChessboardCorners(vis, pat, corners2, ret)
  129.                         result_path = os.path.splitext(fname)[0] + "_corners.jpg"
  130.                         cv2.imwrite(result_path, vis)
  131.                         corners_xy = corners2.reshape(-1, 2)
  132.                         print(f"[{idx}/{len(images)}] {os.path.basename(fname)}: 检测成功 (回退), pattern={pat}, corners={len(corners_xy)}")
  133.                         coord_strs = [f"({x:.3f}, {y:.3f})" for x, y in corners_xy]
  134.                         for i in range(0, len(coord_strs), 8):
  135.                             print("   " + "  ".join(coord_strs[i:i+8]))
  136.                         print(f"   结果图已保存: {os.path.basename(result_path)}\n")
  137.                         for i_pt, (x, y) in enumerate(corners_xy):
  138.                             csv_rows.append([os.path.basename(fname), cols, rows, i_pt, f"{x:.6f}", f"{y:.6f}"])
  139.                         success = True
  140.                         break
  141.         if not success:
  142.             print(f"[{idx}/{len(images)}] {os.path.basename(fname)}: 未检测到角点,已跳过\n")
  143.     # 保存 CSV(所有角点)
  144.     if csv_rows:
  145.         with open(save_csv, "w", newline='', encoding='utf-8') as f:
  146.             writer = csv.writer(f)
  147.             writer.writerow(csv_header)
  148.             writer.writerows(csv_rows)
  149.         print(f"所有角点坐标已合并保存为: {save_csv}\n")
  150.     else:
  151.         print("未检测到任何角点,CSV 未生成。")
  152.     good_n = len(objpoints)
  153.     print(f"成功检测到 {good_n} 张有效图片(用于标定)。需要至少 {min_good_images} 张。")
  154.     if good_n < min_good_images:
  155.         print("有效图片不足,无法进行可靠标定。请检查图片质量或拍摄更多不同视角的棋盘照片。")
  156.         return
  157.     # image_size 使用第一张图片(因为你确认所有图片相同分辨率)
  158.     sample_img = cv2.imread(images[0])
  159.     image_size = (sample_img.shape[1], sample_img.shape[0])  # (width, height)
  160.     # 标定
  161.     print("开始标定(calibrateCamera)...")
  162.     rms, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, image_size, None, None)
  163.     print("\n===== 标定结果 =====")
  164.     print(f"calibrateCamera 返回 RMS: {rms:.6f}")
  165.     print("相机内参 (camera_matrix):")
  166.     print(camera_matrix)
  167.     print("\n畸变系数 (dist_coeffs):")
  168.     print(dist_coeffs.ravel())
  169.     # 平均重投影误差
  170.     total_error = 0.0
  171.     for i in range(len(objpoints)):
  172.         imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], camera_matrix, dist_coeffs)
  173.         error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
  174.         total_error += error
  175.     mean_error = total_error / len(objpoints)
  176.     print(f"\n平均重投影误差: {mean_error:.6f} 像素")
  177.     # 详细打印内参与畸变
  178.     fx = camera_matrix[0, 0]; fy = camera_matrix[1, 1]; cx = camera_matrix[0, 2]; cy = camera_matrix[1, 2]
  179.     print("\n相机内参 (详细):")
  180.     print(f" fx = {fx:.6f}")
  181.     print(f" fy = {fy:.6f}")
  182.     print(f" cx = {cx:.6f}")
  183.     print(f" cy = {cy:.6f}")
  184.     coeffs = dist_coeffs.ravel()
  185.     # 可能只有 4 或 5 个畸变参数
  186.     coeffs_str = ", ".join([f"{v:.8f}" for v in coeffs])
  187.     print(f"\n畸变系数: {coeffs_str}")
  188.     # 保存结果
  189.     np.savez("camera_calibration_results.npz",
  190.              camera_matrix=camera_matrix,
  191.              dist_coeffs=dist_coeffs,
  192.              rvecs=rvecs,
  193.              tvecs=tvecs,
  194.              mean_reprojection_error=mean_error,
  195.              used_image_names=np.array(used_image_names),
  196.              detected_patterns=np.array(detected_patterns),
  197.              square_size_m=square_size)
  198.     print("\n标定数据已保存: camera_calibration_results.npz")
  199.     print("完成。")
  200. if __name__ == "__main__":
  201.     calibrate_camera()
复制代码
4、最后会把输出内容:


1). 结果打印出来;

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

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

4). 以及 corners_all.csv文件。

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

点击查看代码
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. print_calib_readable.py
  5. 从 camera_calibration_results.npz 读取标定结果,
  6. 打印易读报告,并同时生成:
  7. - calibration_readable.txt  (人类可读)
  8. - calibration_summary.json  (机器可读)
  9. - per_image_reprojection.csv (若 corners_all.csv 存在且可计算逐图误差)
  10. 修复 ValueError 的关键点:不把 numpy array 当作布尔值直接判断,而是检查 data.files 中是否包含字段。
  11. """
  12. import numpy as np
  13. import cv2
  14. import os
  15. import csv
  16. import json
  17. from math import sqrt
  18. NPZ_FILE = "camera_calibration_results.npz"
  19. CORNERS_CSV = "corners_all.csv"
  20. READABLE_TXT = "calibration_readable.txt"
  21. SUMMARY_JSON = "calibration_summary.json"
  22. PER_IMAGE_CSV = "per_image_reprojection.csv"
  23. def load_npz(fn):
  24.     if not os.path.exists(fn):
  25.         raise FileNotFoundError(f"未找到文件: {fn}")
  26.     return np.load(fn, allow_pickle=True)
  27. def to_str_list(arr):
  28.     """把 numpy array/iterable 转为 str 列表,兼容 bytes"""
  29.     if arr is None:
  30.         return None
  31.     out = []
  32.     try:
  33.         for v in arr:
  34.             if isinstance(v, bytes):
  35.                 out.append(v.decode('utf-8', errors='ignore'))
  36.             else:
  37.                 out.append(str(v))
  38.     except Exception:
  39.         # fallback: single value
  40.         try:
  41.             if isinstance(arr, bytes):
  42.                 return [arr.decode('utf-8', errors='ignore')]
  43.             return [str(arr)]
  44.         except Exception:
  45.             return None
  46.     return out
  47. def read_corners_csv(csv_path):
  48.     """读取 corners_all.csv (image, pattern_cols, pattern_rows, corner_index, x, y)"""
  49.     if not os.path.exists(csv_path):
  50.         return None
  51.     d = {}
  52.     with open(csv_path, newline='', encoding='utf-8') as f:
  53.         reader = csv.DictReader(f)
  54.         for row in reader:
  55.             name = row['image']
  56.             x = float(row['x']); y = float(row['y'])
  57.             idx = int(row['corner_index'])
  58.             d.setdefault(name, []).append((idx, (x, y)))
  59.     # sort by index and convert to np.array
  60.     for name in list(d.keys()):
  61.         arr = [p for i,p in sorted(d[name], key=lambda it: it[0])]
  62.         d[name] = np.array(arr, dtype=np.float32)
  63.     return d
  64. def format_mat(mat):
  65.     return "\n".join(["  [" + "  ".join(f"{v:12.6f}" for v in row) + "]" for row in mat])
  66. def safe_get(data, *keys):
  67.     """从 data (npz) 中按优先级取值,返回 None 或 value"""
  68.     for k in keys:
  69.         if k in data.files:
  70.             return data[k]
  71.     return None
  72. def ensure_list_of_vectors(arr):
  73.     """保证 rvecs/tvecs 转为 python list,元素为 np.array"""
  74.     if arr is None:
  75.         return None
  76.     try:
  77.         return [np.array(v) for v in arr]
  78.     except Exception:
  79.         # 如果是单个 array 扩展为 list
  80.         try:
  81.             return [np.array(arr)]
  82.         except Exception:
  83.             return None
  84. def main():
  85.     try:
  86.         data = load_npz(NPZ_FILE)
  87.     except Exception as e:
  88.         print("加载 NPZ 失败:", e)
  89.         return
  90.     # 安全地获取字段(不直接用 data.get(...) 或在 if 中用数组判断)
  91.     camera_matrix = safe_get(data, "camera_matrix", "mtx", "camera_mat", "camera_matx")
  92.     dist_coeffs = safe_get(data, "dist_coeffs", "dist_coeff", "dist", "dist_coeff")
  93.     rvecs = safe_get(data, "rvecs")
  94.     tvecs = safe_get(data, "tvecs")
  95.     mean_err = safe_get(data, "mean_reprojection_error", "mean_error")
  96.     used_images_raw = safe_get(data, "used_image_names")
  97.     detected_patterns_raw = safe_get(data, "detected_patterns")
  98.     square_size_m_val = safe_get(data, "square_size_m")
  99.     rms_val = safe_get(data, "rms", "calibrate_rms")  # optional
  100.     # normalize some fields
  101.     used_images = to_str_list(used_images_raw) if used_images_raw is not None else None
  102.     # detected_patterns may be array-like of tuples or strings; normalize to list of (cols,rows) tuples
  103.     detected_patterns = None
  104.     if detected_patterns_raw is not None:
  105.         try:
  106.             # try to convert to list of tuples
  107.             tmp = []
  108.             for p in detected_patterns_raw:
  109.                 # p could be array([c,r]) or b'(c,r)'
  110.                 if isinstance(p, (np.ndarray, list, tuple)):
  111.                     tmp.append((int(p[0]), int(p[1])))
  112.                 else:
  113.                     # try parse
  114.                     s = str(p)
  115.                     s = s.strip("()[] ")
  116.                     parts = [int(x) for x in s.replace(",", " ").split() if x.strip().isdigit()]
  117.                     if len(parts) >= 2:
  118.                         tmp.append((parts[0], parts[1]))
  119.             detected_patterns = tmp
  120.         except Exception:
  121.             detected_patterns = None
  122.     # convert rvecs/tvecs to python lists
  123.     rvecs_list = ensure_list_of_vectors(rvecs)
  124.     tvecs_list = ensure_list_of_vectors(tvecs)
  125.     # start composing readable text and summary dict
  126.     lines = []
  127.     lines.append("===== Camera Calibration (Human-Readable) =====\n")
  128.     if camera_matrix is None:
  129.         lines.append("错误:NPZ 文件中未找到相机内参 (camera_matrix)。\n")
  130.         # write file and exit
  131.         with open(READABLE_TXT, "w", encoding="utf-8") as f:
  132.             f.write("\n".join(lines))
  133.         print("已生成 (部分) 可读文件:", READABLE_TXT)
  134.         return
  135.     # print camera matrix
  136.     lines.append("相机内参 (camera_matrix) — 3x3 矩阵 (单位: 像素):")
  137.     lines.append(format_mat(camera_matrix))
  138.     fx = float(camera_matrix[0,0]); fy = float(camera_matrix[1,1]); cx = float(camera_matrix[0,2]); cy = float(camera_matrix[1,2])
  139.     skew = float(camera_matrix[0,1]) if camera_matrix.shape[1] > 1 else 0.0
  140.     lines.append("\n标准参数名称与数值:")
  141.     lines.append(f" fx  (焦距 x方向)        = {fx:.6f} px")
  142.     lines.append(f" fy  (焦距 y方向)        = {fy:.6f} px")
  143.     lines.append(f" cx  (主点 x)            = {cx:.6f} px")
  144.     lines.append(f" cy  (主点 y)            = {cy:.6f} px")
  145.     lines.append(f" skew (相机倾斜/skew)    = {skew:.6f} (通常接近 0)\n")
  146.     # distortion
  147.     if dist_coeffs is None:
  148.         lines.append("畸变系数未找到 (dist_coeffs)。\n")
  149.         dist_list = []
  150.     else:
  151.         d = np.array(dist_coeffs).ravel()
  152.         lines.append("畸变系数 (distortion coefficients):")
  153.         labels = ["k1","k2","p1","p2","k3","k4","k5","k6"]
  154.         for i,val in enumerate(d):
  155.             lab = labels[i] if i < len(labels) else f"d{i+1}"
  156.             lines.append(f" {lab:3s} = {float(val):.8f}")
  157.         lines.append("(顺序通常为 k1, k2, p1, p2, k3 )\n")
  158.         dist_list = [float(x) for x in d.tolist()]
  159.     # RMS and mean reprojection
  160.     if rms_val is not None:
  161.         try:
  162.             rms_f = float(rms_val)
  163.             lines.append(f"calibrateCamera 返回 RMS (函数返回值) = {rms_f:.6f}")
  164.         except Exception:
  165.             pass
  166.     if mean_err is not None:
  167.         try:
  168.             mean_err_f = float(mean_err)
  169.             lines.append(f"mean_reprojection_error (按图片平均计算) = {mean_err_f:.6f} 像素")
  170.         except Exception:
  171.             pass
  172.     lines.append("(推荐关注按图片平均的 mean_reprojection_error,更直观)\n")
  173.     if square_size_m_val is not None:
  174.         try:
  175.             ss = float(square_size_m_val)
  176.             lines.append(f"棋盘方格实际边长 (square_size_m) = {ss:.6f} m")
  177.         except Exception:
  178.             pass
  179.     # patterns & images
  180.     if detected_patterns is not None:
  181.         unique = []
  182.         for p in detected_patterns:
  183.             if p not in unique:
  184.                 unique.append(p)
  185.         if len(unique) == 1:
  186.             lines.append(f"使用的棋盘内角点尺寸 (pattern cols,rows) = {unique[0]} (一致,所有图片使用同一尺寸)")
  187.             common_pattern = unique[0]
  188.         else:
  189.             lines.append("每张图片检测到的棋盘内角点尺寸 (per-image):")
  190.             for i, p in enumerate(detected_patterns):
  191.                 name = used_images[i] if used_images and i < len(used_images) else f"img#{i}"
  192.                 lines.append(f"  {name:20s} -> {p}")
  193.             common_pattern = None
  194.     else:
  195.         common_pattern = None
  196.     if used_images is not None:
  197.         lines.append(f"\n用于标定的图片数量 = {len(used_images)}")
  198.         lines.append("图片列表(用于标定,按序):")
  199.         for nm in used_images:
  200.             lines.append("  - " + nm)
  201.     lines.append("")
  202.     # write readable txt now (we will append per-image detail later)
  203.     with open(READABLE_TXT, "w", encoding="utf-8") as f:
  204.         f.write("\n".join(lines))
  205.     print("已生成可读文本报告:", READABLE_TXT)
  206.     # 准备 summary json
  207.     summary = {
  208.         "fx": fx, "fy": fy, "cx": cx, "cy": cy, "skew": skew,
  209.         "distortion": {f"k{i+1}": (dist_list[i] if i < len(dist_list) else None) for i in range(6)},
  210.         "distortion_raw": dist_list,
  211.         "rms": float(rms_val) if rms_val is not None else None,
  212.         "mean_reprojection_error": float(mean_err) if mean_err is not None else None,
  213.         "square_size_m": float(square_size_m_val) if square_size_m_val is not None else None,
  214.         "pattern_common": common_pattern,
  215.         "images_used": used_images if used_images is not None else []
  216.     }
  217.     # 如果存在 corners_all.csv,则计算逐图重投影误差并写入 CSV 与 JSON
  218.     corners_dict = read_corners_csv(CORNERS_CSV)
  219.     per_image_stats = []
  220.     if corners_dict is None:
  221.         print(f"未找到 {CORNERS_CSV}(可选)。若存在该文件,脚本会计算逐图重投影误差及 rvec/tvec。")
  222.     else:
  223.         # ensure rvecs/tvecs exist
  224.         if rvecs_list is None or tvecs_list is None:
  225.             print("注意:NPZ 中缺少 rvecs/tvecs,无法计算逐图重投影误差。")
  226.         else:
  227.             # iterate using used_images order if available
  228.             names_order = used_images if used_images is not None else sorted(list(corners_dict.keys()))
  229.             # prepare CSV writer
  230.             with open(PER_IMAGE_CSV, "w", newline='', encoding='utf-8') as fcsv:
  231.                 writer = csv.writer(fcsv)
  232.                 writer.writerow(["image", "pattern_cols", "pattern_rows", "corner_count", "rmse_px",
  233.                                  "tvec_x_m", "tvec_y_m", "tvec_z_m", "rvec_x", "rvec_y", "rvec_z"])
  234.                 for i, name in enumerate(names_order):
  235.                     base = os.path.basename(name)
  236.                     if base not in corners_dict:
  237.                         # try direct name (maybe used_images stores base names already)
  238.                         if name in corners_dict:
  239.                             base = name
  240.                         else:
  241.                             print(f"{base}: 在 {CORNERS_CSV} 中未找到对应角点,跳过逐图误差计算。")
  242.                             continue
  243.                     img_corners = corners_dict[base]
  244.                     if i >= len(rvecs_list) or i >= len(tvecs_list):
  245.                         print(f"{base}: rvecs/tvecs 不足,跳过。")
  246.                         continue
  247.                     rvec = rvecs_list[i].reshape(3,1)
  248.                     tvec = tvecs_list[i].reshape(3,1)
  249.                     # construct objp based on detected_patterns if available else skip
  250.                     if detected_patterns is None or i >= len(detected_patterns):
  251.                         print(f"{base}: 无法构造 objpoints(未保存 detected_patterns),跳过该图重投影计算。")
  252.                         continue
  253.                     pat = detected_patterns[i]
  254.                     cols, rows = pat
  255.                     objp = np.zeros((rows*cols, 3), np.float32)
  256.                     objp[:,:2] = np.mgrid[0:cols, 0:rows].T.reshape(-1,2)
  257.                     if square_size_m_val is not None:
  258.                         objp[:,:2] *= float(square_size_m_val)
  259.                     # project
  260.                     proj, _ = cv2.projectPoints(objp, rvec, tvec, camera_matrix, dist_coeffs)
  261.                     proj = proj.reshape(-1,2)
  262.                     if proj.shape != img_corners.shape:
  263.                         print(f"{base}: 投影点数量 {proj.shape[0]} 与 CSV 中角点数量 {img_corners.shape[0]} 不匹配,跳过。")
  264.                         continue
  265.                     err = np.sqrt(np.mean(np.sum((img_corners - proj)**2, axis=1)))
  266.                     R, _ = cv2.Rodrigues(rvec)
  267.                     writer.writerow([base, cols, rows, int(img_corners.shape[0]), float(err),
  268.                                      float(tvec[0,0]), float(tvec[1,0]), float(tvec[2,0]),
  269.                                      float(rvec[0,0]), float(rvec[1,0]), float(rvec[2,0])])
  270.                     per_image_stats.append({
  271.                         "image": base,
  272.                         "pattern": [int(cols), int(rows)],
  273.                         "corner_count": int(img_corners.shape[0]),
  274.                         "rmse_px": float(err),
  275.                         "tvec_m": [float(tvec[0,0]), float(tvec[1,0]), float(tvec[2,0])],
  276.                         "rvec": [float(rvec[0,0]), float(rvec[1,0]), float(rvec[2,0])]
  277.                     })
  278.             print("已生成逐图重投影 CSV:", PER_IMAGE_CSV)
  279.     summary["per_image"] = per_image_stats
  280.     # 写入 summary json
  281.     with open(SUMMARY_JSON, "w", encoding='utf-8') as fj:
  282.         json.dump(summary, fj, indent=2, ensure_ascii=False)
  283.     print("已生成机器可读摘要:", SUMMARY_JSON)
  284.     # append per-image human-readable section to readable txt
  285.     with open(READABLE_TXT, "a", encoding='utf-8') as f:
  286.         f.write("\n\n===== Per-image Reprojection Summary =====\n\n")
  287.         if not per_image_stats:
  288.             f.write("无可用的逐图重投影结果(可能缺少 corners_all.csv 或 rvecs/tvecs/detected_patterns)。\n")
  289.         else:
  290.             for s in per_image_stats:
  291.                 f.write(f"Image: {s['image']}\n")
  292.                 f.write(f"  pattern (cols,rows) = {s['pattern']} ; corner_count = {s['corner_count']}\n")
  293.                 f.write(f"  reprojection RMSE = {s['rmse_px']:.6f} px\n")
  294.                 f.write(f"  tvec (m) = [{s['tvec_m'][0]:.6f}, {s['tvec_m'][1]:.6f}, {s['tvec_m'][2]:.6f}]\n")
  295.                 f.write(f"  rvec = [{s['rvec'][0]:.6f}, {s['rvec'][1]:.6f}, {s['rvec'][2]:.6f}]\n\n")
  296.     print("已把逐图结果追加到可读报告:", READABLE_TXT)
  297.     print("\n全部完成。")
  298. if __name__ == "__main__":
  299.     main()
复制代码
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册