OpenCV实现相机畸变校准

原图

校准过程

校准后的对比图

源码

main.cpp
cpp 复制代码
/**
 * @file test_camera_calibrator.cpp
 * @brief 相机标定与图像拼接测试程序
 *
 * 功能说明:
 * 1. 相机标定 - 使用棋盘格标定板标定相机内参和畸变系数
 *
 * 标定算法: Brown-Conrady 畸变模型
 *
 * @author Auto Generated
 * @date 2026
 */

#include "camera_calibrator.h"
#include "jpeg_writer.h"
#include "jpeg_reader.h"
#include <iostream>
#include <sys/stat.h>
#include <opencv2/stitching.hpp>
#include <opencv2/features2d.hpp>

/**
 * @brief 保存JPEG图像
 * @param path 保存路径
 * @param img 要保存的图像
 * @param desc 描述信息(用于日志输出)
 *
 * 使用自定义JPEG读写器保存图像,失败时输出错误信息
 */
void saveImage(const std::string& path, const cv::Mat& img, const std::string& desc) {
    if (imwrite_jpeg(path, img)) {
        std::cout << "[保存] " << desc << std::endl;
    } else {
        std::cerr << "[错误] 保存失败: " << path << std::endl;
    }
}

// ============================================================================
// 相机标定与畸变校正测试程序
// ============================================================================

/**
 * @brief main - 相机标定主函数
 *
 * 标定流程:
 * 1. 从标定图像中检测棋盘格角点
 * 2. 亚像素精化角点坐标
 * 3. 调用OpenCV calibrateCamera进行标定
 * 4. 生成畸变校正对比图
 * 5. 保存标定参数到JSON文件
 */
int main(int argc, char** argv) {
    std::cout << "======================================" << std::endl;
    std::cout << "     相机标定与畸变校正测试程序      " << std::endl;
    std::cout << "======================================" << std::endl;

    mkdir("output", 0755);

    // 棋盘格参数:9x6表示9列6行的内角点
    cv::Size boardSize(9, 6);
    // 每个棋盘格方格的物理尺寸(毫米)
    float squareSize = 25.0f;

    std::cout << "\n===== 从真实标定图像进行标定 =====" << std::endl;
    CameraCalibrator calibrator(boardSize, squareSize);

    std::cout << "\n[1] 添加标定图像并保存角点检测结果..." << std::endl;
    int addedCount = 0;

    // 要处理的标定图像编号
    int imgNums[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14};

    // 遍历所有标定图像
    for (int imgIdx = 0; imgIdx < sizeof(imgNums)/sizeof(imgNums[0]); imgIdx++) {
        int i = imgNums[imgIdx];
        char buf[64];
        sprintf(buf, "../calib_data/left%02d.jpg", i);
        std::string filepath(buf);
        std::string filename(buf);

        // 读取JPEG图像
        cv::Mat image = imread_jpeg(filepath);
        if (image.empty()) {
            std::cerr << "警告: 无法加载图像 " << filepath << std::endl;
            continue;
        }

        // 复制原图用于绘制显示
        cv::Mat display;
        if (image.channels() == 1) {
            cv::cvtColor(image, display, cv::COLOR_GRAY2BGR);
        } else {
            display = image.clone();
        }

        // 存储检测到的角点坐标
        std::vector<cv::Point2f> corners;

        /**
         * 检测棋盘格角点
         * cv::CALIB_CB_ADAPTIVE_THRESH - 使用自适应阈值
         * cv::CALIB_CB_NORMALIZE_IMAGE - 归一化图像对比度
         */
        bool found = cv::findChessboardCorners(image, boardSize, corners,
            cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);

        if (found) {
            /**
             * 亚像素精化
             * 提高角点精度,从像素级提升到亚像素级
             * 迭代30次,精度阈值0.001
             */
            cv::cornerSubPix(image, corners, cv::Size(11, 11), cv::Size(-1, -1),
                cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.001));

            // 在图像上绘制检测到的角点
            cv::drawChessboardCorners(display, boardSize, corners, found);

            // 将角点添加到标定器
            if (calibrator.addCalibrationPoints(image)) {
                // 保存角点检测结果图像
                char outName[64];
                sprintf(outName, "../calib_data/%02d_corners.jpg", i);
                saveImage(outName, display, "角点检测");
                addedCount++;
                std::cout << "成功: " << filepath << " (角点数: " << corners.size() << ")" << std::endl;
            }
        } else {
            std::cerr << "警告: 未检测到角点 " << filepath << std::endl;
        }
    }
    std::cout << std::endl;

    // 至少需要3张图像才能进行标定
    if (addedCount < 3) {
        std::cerr << "错误: 至少需要3张标定图像" << std::endl;
        return -1;
    }
    std::cout << "成功添加 " << addedCount << " 张标定图像" << std::endl;

    /**
     * 执行相机标定
     * 内部使用 OpenCV calibrateCamera 函数
     * 计算相机内参矩阵和畸变系数
     */
    std::cout << "\n[2] 执行相机标定..." << std::endl;
    if (!calibrator.calibrate()) {
        std::cerr << "标定失败!" << std::endl;
        return -1;
    }

    // 保存畸变校正对比图
    std::cout << "\n[3] 生成畸变校正对比图..." << std::endl;
    for (int imgIdx = 0; imgIdx < sizeof(imgNums)/sizeof(imgNums[0]); imgIdx++) {
        int i = imgNums[imgIdx];
        char filepath[64], outname[64];
        sprintf(filepath, "../calib_data/left%02d.jpg", i);
        sprintf(outname, "../calib_data/%02d_distortion_compare.jpg", i);

        cv::Mat image = imread_jpeg(filepath);
        if (image.empty()) continue;

        // 使用标定参数进行畸变校正
        cv::Mat undistorted;
        calibrator.undistort(image, undistorted);

        // 将原图和校正后的图横向拼接生成对比图
        cv::Mat canvas;
        cv::hconcat(image, undistorted, canvas);

        // 添加文字标注
        cv::putText(canvas, "Original (Distorted)", cv::Point(10, 30),
            cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 255, 0), 2);
        cv::putText(canvas, "Undistorted", cv::Point(image.cols + 10, 30),
            cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 255, 0), 2);

        saveImage(outname, canvas, "畸变校正对比");
    }

    // 保存标定参数到JSON文件
    std::cout << "\n[4] 保存标定参数..." << std::endl;
    calibrator.saveParametersJSON("../calib_data/camera_calibration_params.json");

    std::cout << "\n======================================" << std::endl;
    std::cout << "           测试完成!                  " << std::endl;
    std::cout << "======================================" << std::endl;
    std::cout << "\n输出文件:" << std::endl;
    std::cout << "  output/*_corners.jpg       - 角点检测结果" << std::endl;
    std::cout << "  output/*_distortion_compare.jpg - 畸变校正对比" << std::endl;
    std::cout << "  camera_calibration_params.xml - 标定参数" << std::endl;

    return 0;
}
camera_calibrator.cpp
cpp 复制代码
/**
 * @file camera_calibrator.cpp
 * @brief 相机标定与畸变校正类实现
 *
 * @see camera_calibrator.h
 */

#include "camera_calibrator.h"
#include <iostream>

/**
 * @brief 构造函数
 *
 * 初始化棋盘格参数和状态标志
 *
 * @param boardSize 棋盘格内角点数量(列数 x 行数)
 * @param squareSize 每个方格的物理尺寸(毫米)
 */
CameraCalibrator::CameraCalibrator(cv::Size boardSize, float squareSize)
    : boardSize_(boardSize), squareSize_(squareSize), isCalibrated_(false), rmsError_(0.0) {}

/**
 * @brief 生成3D世界坐标点
 *
 * 原理:
 * 假设棋盘格位于Z=0的平面上,棋盘格的世界坐标是已知的
 * 每个角点的坐标 = (列号 * 方格尺寸, 行号 * 方格尺寸, 0)
 *
 * 例如:9x6的棋盘格,第0行第0列角点 → (0, 0, 0)
 *                    第0行第1列角点 → (25, 0, 0)
 *                    第1行第0列角点 → (0, 25, 0)
 *                    ...
 *
 * 注意:内角点数量是(列数-1) x (行数-1),
 *       如果棋盘格有10列7行方格,内角点就是9x6
 */
void CameraCalibrator::generateObjectPoints() {
    objectPoints_.clear();

    // 遍历每张标定图像
    for (size_t i = 0; i < imagePoints_.size(); ++i) {
        std::vector<cv::Point3f> points;

        // 生成该图像对应的3D世界坐标
        for (int row = 0; row < boardSize_.height; ++row) {
            for (int col = 0; col < boardSize_.width; ++col) {
                /**
                 * 第(row,col)个角点的世界坐标
                 *
                 * 物理含义:
                 * - col * squareSize: X方向位置(毫米)
                 * - row * squareSize: Y方向位置(毫米)
                 * - 0: Z方向位置(棋盘格平面上)
                 */
                points.emplace_back(col * squareSize_, row * squareSize_, 0.0f);
            }
        }
        objectPoints_.push_back(points);
    }
}

/**
 * @brief 添加标定图像的角点
 *
 * 流程:
 * 1. 彩色图转灰度图
 * 2. 检测棋盘格角点(像素级)
 * 3. 亚像素精化(提高精度到亚像素级)
 * 4. 保存角点坐标
 *
 * @param image 输入的标定图像
 * @return 是否成功检测到角点
 */
bool CameraCalibrator::addCalibrationPoints(const cv::Mat& image) {
    // 转灰度(如果需要)
    cv::Mat gray;
    if (image.channels() == 3) {
        cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
    } else {
        gray = image;
    }

    // 存储检测到的角点坐标
    std::vector<cv::Point2f> corners;

    /**
     * 检测棋盘格角点
     *
     * cv::CALIB_CB_ADAPTIVE_THRESH:
     *   使用自适应阈值二值化,可以更好处理光照不均匀的图像
     *
     * cv::CALIB_CB_NORMALIZE_IMAGE:
     *   先归一化图像对比度,增强检测稳定性
     */
    bool found = cv::findChessboardCorners(gray, boardSize_, corners,
        cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);

    if (found) {
        /**
         * 亚像素精化
         *
         * findChessboardCorners只能检测到像素级精度
         * cornerSubPix使用亚像素级精化,将精度提升到亚像素
         *
         * 参数说明:
         * - cv::Size(11, 11): 搜索窗口大小
         * - cv::Size(-1, -1): 死区大小(-1表示默认值)
         * - TermCriteria: 迭代终止条件
         *   EPS + MAX_ITER: 迭代30次或精度达到0.001
         */
        cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1),
            cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 30, 0.001));

        // 保存角点和图像尺寸
        imagePoints_.push_back(corners);
        if (imageSize_.width == 0) {
            imageSize_ = image.size();
        }
    }
    return found;
}

/**
 * @brief 执行相机标定
 *
 * 核心函数,使用OpenCV calibrateCamera进行标定
 *
 * 算法原理:
 * 1. 使用已知的3D世界坐标(objectPoints_)和检测到的2D图像坐标(imagePoints_)
 * 2. 求解针孔相机模型参数
 * 3. 得到相机内参矩阵和畸变系数
 *
 * 标定选项:
 * - CALIB_FIX_PRINCIPAL_POINT: 固定主点(cx,cy)在图像中心
 * - CALIB_ZERO_TANGENT_DIST: 假设切向畸变(p1=p2=0),简化模型
 *
 * @return 是否标定成功
 */
bool CameraCalibrator::calibrate() {
    // 需要至少添加了一张图像的角点
    if (imagePoints_.empty()) return false;

    // 生成3D世界坐标点
    generateObjectPoints();

    /**
     * 初始化内参矩阵
     *
     * 初始化为单位矩阵,calibrateCamera会迭代优化
     *
     * 内参矩阵结构:
     * [fx  0 cx]
     * [ 0 fy cy]
     * [ 0  0  1]
     *
     * fx, fy: 焦距(像素)
     * cx, cy: 主点(光轴与图像平面的交点)
     */
    cameraMatrix_ = cv::Mat::eye(3, 3, CV_64F);

    /**
     * 初始化畸变系数
     *
     * [k1, k2, p1, p2, k3]
     *
     * k1, k2, k3: 径向畸变系数
     * p1, p2: 切向畸变系数
     */
    distCoeffs_ = cv::Mat::zeros(5, 1, CV_64F);

    /**
     * 执行标定
     *
     * calibrateCamera 是OpenCV的核心标定函数
     *
     * 输入:
     * - objectPoints_: 每张图的3D世界坐标
     * - imagePoints_: 每张图检测到的2D角点坐标
     * - imageSize_: 图像分辨率
     *
     * 输出:
     * - cameraMatrix_: 相机内参矩阵
     * - distCoeffs_: 畸变系数
     * - rvecs_: 每张图的旋转向量
     * - tvecs_: 每张图的平移向量
     *
     * 返回值:
     * - rmsError_: 重投影均方根误差(像素)
     *
     * 重投影误差:标定后,将3D点用标定参数投影到2D,
     *            与实际检测到的2D点的平均距离
     *            误差越小说明标定越准确
     */
    rmsError_ = cv::calibrateCamera(objectPoints_, imagePoints_, imageSize_,
        cameraMatrix_, distCoeffs_, rvecs_, tvecs_,
        cv::CALIB_FIX_PRINCIPAL_POINT | cv::CALIB_ZERO_TANGENT_DIST);

    // 标记已标定
    isCalibrated_ = true;

    // 输出标定结果
    std::cout << "\n========== 标定结果 ==========" << std::endl;
    std::cout << "内参矩阵:\n" << cameraMatrix_ << std::endl;
    std::cout << "\n畸变系数:\n" << distCoeffs_ << std::endl;
    std::cout << "重投影误差: " << rmsError_ << " 像素" << std::endl;
    std::cout << "================================\n" << std::endl;

    return true;
}

/**
 * @brief 畸变校正
 *
 * 使用标定得到的内参和畸变系数对图像进行校正
 *
 * 校正原理:
 * 对于图像中每个畸变点(x_distorted, y_distorted),
 * 反向计算校正后的理想点(x_corrected, y_corrected)
 *
 * 公式(Brown-Conrady模型):
 * x_corrected = (x_distorted - cx) / fx
 * y_corrected = (y_distorted - cy) / fy
 * r² = x_corrected² + y_corrected²
 *
 * x_ideal = x_corrected * (1 + k1*r² + k2*r⁴ + k3*r⁶)
 *           + 2*p1*x_corrected*y_corrected + p2*(r² + 2*x_corrected²)
 *
 * y_ideal = y_corrected * (1 + k1*r² + k2*r⁴ + k3*r⁶)
 *           + 2*p2*x_corrected*y_corrected + p1*(r² + 2*y_corrected²)
 *
 * x_final = x_ideal * fx + cx
 * y_final = y_ideal * fy + cy
 *
 * @param input 输入的畸变图像
 * @param output 输出的校正后图像
 * @param alpha 自由比例因子
 *              -1: 使用OpenCV默认的优化值
 *               0: 裁剪掉校正后超出有效区域的像素
 *               1: 保留所有像素,可能有黑边
 */
void CameraCalibrator::undistort(const cv::Mat& input, cv::Mat& output, double alpha) {
    if (!isCalibrated_) return;

    /**
     * 计算优化后的新相机矩阵
     *
     * getOptimalNewCameraMatrix 根据畸变系数和alpha值,
     * 计算一个优化后的相机内参矩阵
     *
     * 目的:
     * 畸变校正会改变图像的有效区域,
     * 新相机矩阵可以重新计算主点位置,
     * 使输出图像最大化保留有效内容
     */
    cv::Mat newCameraMatrix = cv::getOptimalNewCameraMatrix(cameraMatrix_, distCoeffs_, imageSize_, alpha);

    /**
     * 执行畸变校正
     *
     * undistort 会:
     * 1. 使用内参矩阵和畸变系数
     * 2. 对输入图像每个像素进行反向映射
     * 3. 生成校正后的输出图像
     */
    cv::undistort(input, output, cameraMatrix_, distCoeffs_, newCameraMatrix);
}

/**
 * @brief 保存标定参数到文件(OpenCV XML格式)
 *
 * @param filePath 文件路径
 * @return 是否保存成功
 */
bool CameraCalibrator::saveParameters(const std::string& filePath) const {
    cv::FileStorage fs(filePath, cv::FileStorage::WRITE);
    if (!fs.isOpened()) return false;

    // 使用OpenCV的FileStorage格式保存
    fs << "camera_matrix" << cameraMatrix_;   // 相机内参矩阵
    fs << "dist_coeffs" << distCoeffs_;        // 畸变系数
    fs << "image_width" << imageSize_.width;  // 图像宽度
    fs << "image_height" << imageSize_.height; // 图像高度
    fs << "rms_error" << rmsError_;           // 重投影误差

    fs.release();
    return true;
}

/**
 * @brief 从文件加载标定参数
 *
 * @param filePath 文件路径
 * @return 是否加载成功
 */
bool CameraCalibrator::loadParameters(const std::string& filePath) {
    cv::FileStorage fs(filePath, cv::FileStorage::READ);
    if (!fs.isOpened()) return false;

    // 读取各项参数
    fs["camera_matrix"] >> cameraMatrix_;
    fs["dist_coeffs"] >> distCoeffs_;
    fs["image_width"] >> imageSize_.width;
    fs["image_height"] >> imageSize_.height;
    fs["rms_error"] >> rmsError_;

    fs.release();

    // 标记已标定
    isCalibrated_ = true;
    return true;
}

/**
 * @brief 重置标定器
 *
 * 清除所有数据,用于重新开始新的标定流程
 */
void CameraCalibrator::reset() {
    imagePoints_.clear();
    objectPoints_.clear();
    rvecs_.clear();
    tvecs_.clear();
    isCalibrated_ = false;
    rmsError_ = 0.0;
}

/**
 * @brief 保存标定参数到JSON文件
 *
 * 输出格式便于其他语言(如Python、JavaScript)读取使用
 *
 * JSON结构:
 * @code
 * {
 *   "camera_matrix": {
 *     "fx": ...,
 *     "fy": ...,
 *     "cx": ...,
 *     "cy": ...
 *   },
 *   "distortion_coefficients": {
 *     "k1": ...,
 *     "k2": ...,
 *     "p1": ...,
 *     "p2": ...,
 *     "k3": ...
 *   },
 *   "image_size": {
 *     "width": ...,
 *     "height": ...
 *   },
 *   "reprojection_error": ...
 * }
 * @endcode
 *
 * @param filePath 文件路径
 * @return 是否保存成功
 */
bool CameraCalibrator::saveParametersJSON(const std::string& filePath) const {
    FILE* fp = fopen(filePath.c_str(), "w");
    if (!fp) return false;

    /**
     * 写入JSON格式的标定参数
     *
     * 内参矩阵参数提取:
     * cameraMatrix_.at<double>(0, 0) → fx
     * cameraMatrix_.at<double>(1, 1) → fy
     * cameraMatrix_.at<double>(0, 2) → cx
     * cameraMatrix_.at<double>(1, 2) → cy
     *
     * 畸变系数参数提取:
     * distCoeffs_.at<double>(0, 0) → k1
     * distCoeffs_.at<double>(1, 0) → k2
     * distCoeffs_.at<double>(2, 0) → p1
     * distCoeffs_.at<double>(3, 0) → p2
     * distCoeffs_.at<double>(4, 0) → k3
     */
    fprintf(fp, "{\n");
    fprintf(fp, "  \"camera_matrix\": {\n");
    fprintf(fp, "    \"fx\": %.6f,\n", cameraMatrix_.at<double>(0, 0));
    fprintf(fp, "    \"fy\": %.6f,\n", cameraMatrix_.at<double>(1, 1));
    fprintf(fp, "    \"cx\": %.6f,\n", cameraMatrix_.at<double>(0, 2));
    fprintf(fp, "    \"cy\": %.6f\n", cameraMatrix_.at<double>(1, 2));
    fprintf(fp, "  },\n");
    fprintf(fp, "  \"distortion_coefficients\": {\n");
    fprintf(fp, "    \"k1\": %.6f,\n", distCoeffs_.at<double>(0, 0));
    fprintf(fp, "    \"k2\": %.6f,\n", distCoeffs_.at<double>(1, 0));
    fprintf(fp, "    \"p1\": %.6f,\n", distCoeffs_.at<double>(2, 0));
    fprintf(fp, "    \"p2\": %.6f,\n", distCoeffs_.at<double>(3, 0));
    fprintf(fp, "    \"k3\": %.6f\n", distCoeffs_.at<double>(4, 0));
    fprintf(fp, "  },\n");
    fprintf(fp, "  \"image_size\": {\n");
    fprintf(fp, "    \"width\": %d,\n", imageSize_.width);
    fprintf(fp, "    \"height\": %d\n", imageSize_.height);
    fprintf(fp, "  },\n");
    fprintf(fp, "  \"reprojection_error\": %.6f\n", rmsError_);
    fprintf(fp, "}\n");

    fclose(fp);
    return true;
}
camera_calibrator.h
cpp 复制代码
/**
 * @file camera_calibrator.h
 * @brief 相机标定与畸变校正类声明
 *
 * 功能说明:
 * - 使用棋盘格标定板进行相机标定
 * - 计算相机内参矩阵和畸变系数
 * - 支持畸变校正和参数保存/加载
 *
 * 相机标定原理:
 * 通过已知的3D标定物坐标(棋盘格角点世界坐标)和对应的2D图像坐标,
 * 利用针孔相机模型求解相机内参(焦距、主点)和畸变系数。
 *
 * 针孔相机模型:
 * s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
 *
 * 其中:
 * - K: 相机内参矩阵 (3x3)
 * - [R|t]: 旋转矩阵和平移向量 (3x4)
 * - [X,Y,Z]: 世界坐标
 * - [u,v]: 像素坐标
 * - s: 尺度因子
 *
 * 内参矩阵K:
 * [fx  0 cx]
 * [ 0 fy cy]
 * [ 0  0  1]
 *
 * 畸变模型(Brown-Conrady):
 * x_distorted = x_corrected + x*(k1*r² + k2*r⁴ + k3*r⁶) + 2*p1*x*y + p2*(r² + 2*x²)
 * y_distorted = y_corrected + y*(k1*r² + k2*r⁴ + k3*r⁶) + 2*p2*x*y + p1*(r² + 2*y²)
 *
 * 其中 r² = x² + y²
 *
 * @author Auto Generated
 * @date 2024
 */

#ifndef CAMERA_CALIBRATOR_H
#define CAMERA_CALIBRATOR_H

#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/calib3d.hpp>
#include <vector>
#include <string>

/**
 * @brief 相机标定与畸变校正类
 *
 * 相机标定流程:
 * 1. 构造对象,指定棋盘格参数(行列数、方格尺寸)
 * 2. 添加多张不同角度拍摄的标定图像
 * 3. 调用calibrate()进行标定
 * 4. 使用undistort()进行畸变校正
 * 5. 使用saveParametersJSON()保存标定参数
 *
 * 示例用法:
 * @code
 * CameraCalibrator calibrator(cv::Size(9, 6), 25.0f);  // 9x6棋盘格,方格25mm
 * for (每张标定图) {
 *     calibrator.addCalibrationPoints(image);
 * }
 * calibrator.calibrate();
 * calibrator.saveParametersJSON("params.json");
 * @endcode
 */
class CameraCalibrator {
public:
    /**
     * @brief 构造函数
     * @param boardSize 棋盘格内角点数量,如cv::Size(9,6)表示9列6行
     * @param squareSize 每个棋盘格方格的物理尺寸(毫米)
     */
    CameraCalibrator(cv::Size boardSize, float squareSize = 1.0f);

    /**
     * @brief 添加标定图像的角点
     *
     * 自动检测棋盘格角点并进行亚像素精化
     *
     * @param image 输入的标定图像
     * @return 是否成功检测到角点
     */
    bool addCalibrationPoints(const cv::Mat& image);

    /**
     * @brief 执行相机标定
     *
     * 使用OpenCV calibrateCamera函数计算相机内参和畸变系数
     *
     * 标定选项:
     * - CALIB_FIX_PRINCIPAL_POINT: 固定主点位置
     * - CALIB_ZERO_TANGENT_DIST: 假设切向畸变为零
     *
     * @return 是否标定成功
     */
    bool calibrate();

    /**
     * @brief 畸变校正
     *
     * 使用标定得到的内参和畸变系数对图像进行校正
     *
     * @param input 输入的畸变图像
     * @param output 输出的校正后图像
     * @param alpha 自由比例因子
     *              -1: 使用默认优化
     *               0: 只裁剪有效像素
     *               1: 保留所有像素(可能有黑边)
     */
    void undistort(const cv::Mat& input, cv::Mat& output, double alpha = -1);

    // =========================================================================
    // 参数保存与加载
    // =========================================================================

    /**
     * @brief 从文件加载标定参数(OpenCV XML格式)
     * @param filePath 文件路径
     * @return 是否加载成功
     */
    bool loadParameters(const std::string& filePath);

    /**
     * @brief 保存标定参数到文件(OpenCV XML格式)
     * @param filePath 文件路径
     * @return 是否保存成功
     */
    bool saveParameters(const std::string& filePath) const;

    /**
     * @brief 保存标定参数到JSON文件(便于其他语言读取)
     * @param filePath 文件路径
     * @return 是否保存成功
     */
    bool saveParametersJSON(const std::string& filePath) const;

    // =========================================================================
    // 参数获取
    // =========================================================================

    /** @brief 获取相机内参矩阵 */
    cv::Mat getCameraMatrix() const { return cameraMatrix_; }

    /** @brief 获取畸变系数向量 [k1, k2, p1, p2, k3] */
    cv::Mat getDistCoeffs() const { return distCoeffs_; }

    /** @brief 获取旋转向量列表 */
    std::vector<cv::Mat> getRotationVectors() const { return rvecs_; }

    /** @brief 获取图像尺寸 */
    cv::Size getImageSize() const { return imageSize_; }

    /** @brief 获取重投影误差(像素) */
    double getReprojectionError() const { return rmsError_; }

    /**
     * @brief 重置标定器,清除所有数据
     *
     * 用于重新开始新的标定流程
     */
    void reset();

private:
    /** @brief 棋盘格内角点尺寸(列数 x 行数) */
    cv::Size boardSize_;

    /** @brief 每个方格的物理尺寸(毫米) */
    float squareSize_;

    /** @brief 标定图像的尺寸 */
    cv::Size imageSize_;

    /** @brief 所有标定图像的角点坐标(2D图像坐标) */
    std::vector<std::vector<cv::Point2f>> imagePoints_;

    /** @brief 所有标定图像对应的3D世界坐标 */
    std::vector<std::vector<cv::Point3f>> objectPoints_;

    /** @brief 相机内参矩阵 (3x3) */
    cv::Mat cameraMatrix_;

    /** @brief 畸变系数 (5x1): [k1, k2, p1, p2, k3] */
    cv::Mat distCoeffs_;

    /** @brief 旋转向量列表(每张图一个) */
    std::vector<cv::Mat> rvecs_;

    /** @brief 平移向量列表(每张图一个) */
    std::vector<cv::Mat> tvecs_;

    /** @brief 是否已完成标定 */
    bool isCalibrated_;

    /** @brief 重投影均方根误差(像素) */
    double rmsError_;

    /**
     * @brief 生成3D世界坐标点
     *
     * 假设棋盘格位于Z=0的平面上
     * 第(row,col)个角点的世界坐标为 (col*squareSize, row*squareSize, 0)
     */
    void generateObjectPoints();
};

#endif
jpeg_reader.h
cpp 复制代码
#ifndef JPEG_READER_H
#define JPEG_READER_H
 
#include <opencv2/opencv.hpp>
#include <cstdio>
#include <jpeglib.h>
 
// 使用 libjpeg 读取 JPEG 文件
static cv::Mat imread_jpeg(const std::string& filepath) {
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;
 
    FILE* infile = fopen(filepath.c_str(), "rb");
    if (!infile) {
        return cv::Mat();
    }
 
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_decompress(&cinfo);
    jpeg_stdio_src(&cinfo, infile);
 
    if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
        jpeg_destroy_decompress(&cinfo);
        fclose(infile);
        return cv::Mat();
    }
 
    jpeg_start_decompress(&cinfo);
 
    int width = cinfo.output_width;
    int height = cinfo.output_height;
    int channels = cinfo.output_components;
 
    cv::Mat img(height, width, channels == 3 ? CV_8UC3 : CV_8UC1);
    JSAMPROW row_pointer[1];
 
    while (cinfo.output_scanline < height) {
        row_pointer[0] = img.data + cinfo.output_scanline * width * channels;
        jpeg_read_scanlines(&cinfo, row_pointer, 1);
    }
 
    jpeg_finish_decompress(&cinfo);
    jpeg_destroy_decompress(&cinfo);
    fclose(infile);
 
    // libjpeg 返回 RGB,OpenCV 使用 BGR
    if (channels == 3) {
        cv::cvtColor(img, img, cv::COLOR_RGB2BGR);
    }
    return img;
}
 
#endif // JPEG_READER_H
jpeg_writer.h
cpp 复制代码
#ifndef JPEG_WRITER_H
#define JPEG_WRITER_H
 
#include <opencv2/opencv.hpp>
#include <cstdio>
#include <jpeglib.h>
 
// 使用 libjpeg 保存 JPEG 文件 - 强制保存为彩色
static bool imwrite_jpeg(const std::string& filepath, const cv::Mat& img, int quality = 90) {
    if (img.empty()) return false;
 
    cv::Mat rgb;
 
    // OpenCV 使用 BGR,libjpeg 使用 RGB
    if (img.channels() == 1) {
        cv::cvtColor(img, rgb, cv::COLOR_GRAY2RGB);
    } else if (img.channels() == 3) {
        cv::cvtColor(img, rgb, cv::COLOR_BGR2RGB);
    } else if (img.channels() == 4) {
        cv::cvtColor(img, rgb, cv::COLOR_BGRA2RGBA);
    } else {
        img.copyTo(rgb);
    }
 
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;
    FILE* outfile;
    JSAMPROW row_pointer[1];
 
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);
 
    if ((outfile = fopen(filepath.c_str(), "wb")) == NULL) {
        return false;
    }
 
    jpeg_stdio_dest(&cinfo, outfile);
    cinfo.image_width = rgb.cols;
    cinfo.image_height = rgb.rows;
    cinfo.input_components = 3;
    cinfo.in_color_space = JCS_RGB;
 
    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, quality, TRUE);
    jpeg_start_compress(&cinfo, TRUE);
 
    while (cinfo.next_scanline < cinfo.image_height) {
        row_pointer[0] = rgb.data + cinfo.next_scanline * rgb.cols * 3;
        jpeg_write_scanlines(&cinfo, row_pointer, 1);
    }
 
    jpeg_finish_compress(&cinfo);
    fclose(outfile);
    jpeg_destroy_compress(&cinfo);
 
    return true;
}
 
#endif // JPEG_WRITER_H

测试结果

cpp 复制代码
======================================
     相机标定与畸变校正测试程序      
======================================

===== 从真实标定图像进行标定 =====

[1] 添加标定图像并保存角点检测结果...
[保存] 角点检测
成功: ../calib_data/left01.jpg (角点数: 54)
[保存] 角点检测
成功: ../calib_data/left02.jpg (角点数: 54)
[保存] 角点检测
成功: ../calib_data/left03.jpg (角点数: 54)
[保存] 角点检测
成功: ../calib_data/left04.jpg (角点数: 54)

成功添加 4 张标定图像

[2] 执行相机标定...

========== 标定结果 ==========
内参矩阵:
[539.2031279337825, 0, 319.5;
 0, 539.7183736104245, 239.5;
 0, 0, 1]

畸变系数:
[-0.2889548355664327;
 0.07941285022620277;
 0;
 0;
 0.07560162201900809]
重投影误差: 0.496707 像素
================================


[3] 生成畸变校正对比图...
[保存] 畸变校正对比
[保存] 畸变校正对比
[保存] 畸变校正对比
[保存] 畸变校正对比

[4] 保存标定参数...

======================================
           测试完成!                  
======================================

输出文件:
  output/*_corners.jpg       - 角点检测结果
  output/*_distortion_compare.jpg - 畸变校正对比
  camera_calibration_params.xml - 标定参数
畸变参数

camera_calibration_params.json

bash 复制代码
{
  "camera_matrix": {
    "fx": 539.203128,
    "fy": 539.718374,
    "cx": 319.500000,
    "cy": 239.500000
  },
  "distortion_coefficients": {
    "k1": -0.288955,
    "k2": 0.079413,
    "p1": 0.000000,
    "p2": 0.000000,
    "k3": 0.075602
  },
  "image_size": {
    "width": 640,
    "height": 480
  },
  "reprojection_error": 0.496707
}
相关推荐
楚山之石20 小时前
【Medical Image Analysis】深度学习医学图像合成综述:伪CT、合成MRI与合成PET的架构、性能与临床边界
图像处理·人工智能·学习·mr·mri
blackappleD20 小时前
百度 Unlimited OCR 2 拆解:SR-SWA 如何让百页文档的 KV 缓存不再涨
人工智能·llm
数字新视界20 小时前
国产动环数据库在智能机房环境监控中的应用与优势分析
大数据·人工智能·数据中心·机房管理·模块化机房
holly就是我20 小时前
AI 编程助手为什么跑偏?用开源 ccglass 看清 Prompt、工具调用和 Token 消耗
人工智能·后端
雨中飘荡的记忆21 小时前
额度又见底了?这个开源项目,给 AI 编程助手接上 61 家"加油站"
人工智能
天远Date Lab21 小时前
零信任架构实战:基于天远企业年报信息核验构建自动化商业保理财务与社保穿透评估网关
人工智能·架构·自动化
EnCi Zheng21 小时前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api
loulanyue_21 小时前
给概率大模型套上确定性缰绳:GraphRAG、神经符号与知识超图重构可信AI底座——读陈华钧杨博等CNCC2026第十届知识图谱论坛实录
人工智能·rag
古法老登瞎折腾21 小时前
大数据智能运维实战(九):让 AI 查日志——找到异常,也保留上下文
java·大数据·运维·人工智能·spring ai·mcp
IT_陈寒21 小时前
刚解决一个 Java 并发问题,这坑踩得我怀疑人生
前端·人工智能·后端