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
}
相关推荐
科学熊1 小时前
将chm文件格式转为PDF格式文件
人工智能
数据法师1 小时前
告别付费云端转写!Memo AI:一款部署在本地的无限次音视频转文字神器
人工智能·音视频
阿乔外贸日记1 小时前
以色列电商市场现状:规模、机遇与挑战
大数据·人工智能·智能手机·云计算·汽车
-cywen-1 小时前
扩散模型 2
人工智能
沪漂阿龙1 小时前
面试题:集成学习是什么?Boosting、Bagging、AdaBoost、随机森林为什么有效,一文讲透
人工智能·机器学习·集成学习
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月12日
人工智能·python·信息可视化·自然语言处理·ai编程
DTAS尺寸公差分析软件1 小时前
国产三维公差分析软件 DTAS 3D 核心技术解析:AI+FEA 融合,解决柔性零件公差难题
人工智能·尺寸公差分析·三维公差分析·公差仿真软件·公差计算软件
淡漠的蓝精灵1 小时前
mem0aimem0,给AI加记忆层的开源方案
人工智能·其他