在这里插入图片描述

书籍

在这里插入图片描述

张正友标定法

相机标定到底在做什么?

相机标定,就是给相机做一个“身份认证”:

  1. 内参矩阵:描述相机本身的光学属性(焦距、主点位置)
  2. 畸变系数:描述镜头带来的图像变形(径向/切向畸变)
  3. 外参矩阵:描述相机在世界中的位置和朝向

只有拿到这些参数,我们才能把真实世界的3D点,准确投影到图片的2D像素上,后续的目标检测、SLAM、自动驾驶环视拼接才有意义。


最通用的标定方法:张正友棋盘格标定法

张正友标定法是目前工业界应用最广泛的方法,也叫“棋盘格标定法”,原理超级简单:

1. 核心思路

  1. 准备棋盘格标定板:黑白相间的格子,每个格子的尺寸是已知的,我们可以算出每个角点在世界坐标系下的3D坐标。
  2. 拍摄多张标定板图片:从不同角度、不同距离拍摄,获取足够多的角点数据。
  3. 检测图片中的角点:用算法找到每个角点在像素坐标系下的2D坐标。
  4. 求解相机参数:用3D世界坐标和2D像素坐标的对应关系,反推出相机的内参、畸变系数。

2. 关键特点

  • 只用一张打印的棋盘格就能完成标定,成本极低
  • 精度足够高,满足绝大多数视觉应用的需求
  • 简化了传统标定法的复杂步骤,非常适合新手入门

3. 补充说明

张正友标定法的原始模型只考虑了径向畸变,实际应用中我们会扩展到同时计算切向畸变,来修正镜头安装偏差带来的误差。


工业量产的标定方式:产线EoL标定法

在车载摄像头的生产线上,我们用的是EoL(End of Line)标定法,也就是产线末端标定。

1. 什么是EoL标定?

简单说,就是摄像头装到车上之后,在产线末端用专门的设备,对每个摄像头的外参(安装位置、角度)进行精确标定,保证每个摄像头的误差都符合工艺标准。

2. 标定原理

产线里会放标准标定板,摄像头拍摄标定板后,通过已知的标定板位置,计算出摄像头的实际安装角度和位置,修正到标准值。

3. 为什么必须做EoL标定?

每个摄像头的安装都会有微小误差,比如偏了0.5度,在近距离可能看不出问题,但在高速行驶中,这个误差会被放大,导致车道线识别、障碍物检测出错。EoL标定就是为了消除这些安装误差,保证量产车的一致性。


实战环节

下面我们直接上代码,用一张静态棋盘格图片,在Android平台上用C++调用OpenCV,完成标定并展示去畸变效果。

C++ 核心代码(标定 + 去畸变)

#include <jni.h>
#include <opencv2/opencv.hpp>
#include <vector>
#include <android/bitmap.h>
#include <log/log.h>

using namespace cv;
using namespace std;

extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_cameracalib_CalibActivity_calibrateFromImage(
        JNIEnv* env, jobject,
        jobject bitmap,
        jint boardWidth, jint boardHeight,
        jfloatArray outParams
) {
    // 1. 读取Bitmap
    AndroidBitmapInfo info;
    void* pixels;
    AndroidBitmap_getInfo(env, bitmap, &info);
    AndroidBitmap_lockPixels(env, bitmap, &pixels);
    Mat img(info.height, info.width, CV_8UC4, pixels);
    cvtColor(img, img, COLOR_RGBA2BGR);

    // 2. 棋盘格世界坐标(假设格子尺寸为30mm)
    vector<vector<Point3f>> objPoints;
    vector<vector<Point2f>> imgPoints;
    vector<Point3f> objP;
    for (int i = 0; i < boardHeight; i++) {
        for (int j = 0; j < boardWidth; j++) {
            objP.emplace_back(j * 30.0f, i * 30.0f, 0.0f);
        }
    }

    // 3. 检测棋盘格角点
    Mat gray;
    cvtColor(img, gray, COLOR_BGR2GRAY);
    vector<Point2f> corners;
    bool found = findChessboardCorners(gray, Size(boardWidth, boardHeight), corners);
    if (!found) {
        AndroidBitmap_unlockPixels(env, bitmap, &pixels);
        return false;
    }
    cornerSubPix(gray, corners, Size(11, 11), Size(-1, -1),
                 TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 30, 0.001));
    objPoints.push_back(objP);
    imgPoints.push_back(corners);

    // 4. 张正友标定:求解内参和畸变系数
    Mat cameraMatrix, distCoeffs;
    vector<Mat> rvecs, tvecs;
    calibrateCamera(objPoints, imgPoints, gray.size(), cameraMatrix, distCoeffs, rvecs, tvecs);

    // 5. 输出参数到Java层
    jfloat* params = env->GetFloatArrayElements(outParams, nullptr);
    params[0] = cameraMatrix.at<double>(0, 0); // fx
    params[1] = cameraMatrix.at<double>(1, 1); // fy
    params[2] = cameraMatrix.at<double>(0, 2); // cx
    params[3] = cameraMatrix.at<double>(1, 2); // cy
    params[4] = distCoeffs.at<double>(0, 0);   // k1
    params[5] = distCoeffs.at<double>(0, 1);   // k2
    params[6] = distCoeffs.at<double>(0, 2);   // p1
    params[7] = distCoeffs.at<double>(0, 3);   // p2
    params[8] = distCoeffs.at<double>(0, 4);   // k3
    env->ReleaseFloatArrayElements(outParams, params, 0);

    // 6. 去畸变处理
    Mat undistorted;
    undistort(img, undistorted, cameraMatrix, distCoeffs);
    cvtColor(undistorted, undistorted, COLOR_BGR2RGBA);
    memcpy(pixels, undistorted.data, info.height * info.width * 4);

    AndroidBitmap_unlockPixels(env, bitmap, &pixels);
    return true;
}

Java 调用代码

package com.example.cameracalib;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

public class CalibActivity extends AppCompatActivity {
    static {
        System.loadLibrary("calib_jni");
    }

    public native boolean calibrateFromImage(
            Bitmap bitmap,
            int boardWidth, int boardHeight,
            float[] outParams
    );

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_calib);

        ImageView iv = findViewById(R.id.image);
        TextView tvResult = findViewById(R.id.result);

        // 加载测试棋盘格图片
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.chessboard);
        iv.setImageBitmap(bmp);

        // 棋盘格角点数量(比如9x6)
        int boardW = 9;
        int boardH = 6;

        // 输出参数:fx, fy, cx, cy, k1, k2, p1, p2, k3
        float[] params = new float[9];

        // 调用C++标定
        boolean success = calibrateFromImage(bmp, boardW, boardH, params);

        if (success) {
            String result = String.format(
                    "标定成功!\n内参:fx=%.2f, fy=%.2f, cx=%.2f, cy=%.2f\n畸变:k1=%.4f, k2=%.4f, p1=%.4f, p2=%.4f, k3=%.4f",
                    params[0], params[1], params[2], params[3],
                    params[4], params[5], params[6], params[7], params[8]
            );
            tvResult.setText(result);
            iv.setImageBitmap(bmp); // 显示去畸变后的图片
        } else {
            tvResult.setText("标定失败,未检测到棋盘格角点");
        }
    }
}

布局文件 activity_calib.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:scaleType="centerCrop"/>

    <TextView
        android:id="@+id/result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:textSize="14sp"/>

</LinearLayout>

函数详解

findChessboardCorners

功能

自动检测图像中的棋盘格内角点,是标定的第一步。
没有它,后面所有标定都无法进行。

函数原型(C++)

bool findChessboardCorners(
    InputArray image,
    Size patternSize,
    OutputArray corners,
    int flags = CALIB_CB_ADAPTIVE_THRESH 
    | CALIB_CB_NORMALIZE_IMAGE
    | CALIB_CB_FAST_CHECK
);
参数解释
  1. image
    输入图像,必须是灰度图(gray)。
  2. patternSize
    棋盘格内角点尺寸,如 Size(9,6) = 宽9个、高6个角点。
    ✅ 注意:不是格子数,是角点数!
  3. corners
    输出检测到的角点坐标(vector)。
  4. flags 检测优化标志
    • CALIB_CB_ADAPTIVE_THRESH:自适应阈值,适应不同光照
    • CALIB_CB_NORMALIZE_IMAGE:直方图衡化,增强对比度
    • CALIB_CB_FAST_CHECK:快速检查有没有棋盘格,没有直接返回
    • CALIB_CB_FAST_CHECK 非常重要,能大幅提速
返回值
  • true:检测成功
  • false:检测失败

cornerSubPix

功能

对角点进行亚像素级精细化,把坐标从“整数”精确到“0.01像素”。
角点精度直接决定标定精度!

函数原型
void cornerSubPix(
    InputArray image,
    InputOutputArray corners,
    Size winSize,
    Size zeroZone,
    TermCriteria criteria
);
参数解释
  1. image
    灰度图
  2. corners
    输入输出角点,输入是整数坐标,输出是亚像素坐标
  3. winSize
    亚像素搜索窗口,一般用 Size(11,11)
    窗口越大越稳定,但可能模糊
  4. zeroZone
    死区,一般设 Size(-1,-1) 表示不用
  5. criteria
    停止条件:
    • 最大迭代次数 30
    • 精度 0.001

fisheye::calibrate(最重要!)

功能

OpenCV 官方 KB 鱼眼相机标定函数
输入:多组棋盘格 3D点 ↔ 2D点
输出:相机内参 K + 畸变系数 D(KB模型 4系数)+ 每幅图外参 R、t

函数原型
double fisheye::calibrate(
    InputArrayOfArrays objectPoints,   // 3D世界点
    InputArrayOfArrays imagePoints,    // 2D像素点
    Size imageSize,                    // 图像尺寸
    InputOutputArray K,                // 内参
    InputOutputArray D,                // 畸变系数 k1,k2,k3,k4
    OutputArrayOfArrays rvecs,         // 旋转
    OutputArrayOfArrays tvecs,         // 平移
    int flags = 0,
    TermCriteria criteria = TermCriteria(
        TermCriteria::MAX_ITER + 
        TermCriteria::EPS, 100, DBL_EPSILON)
);
参数详解
  1. objectPoints
    棋盘格 3D 世界坐标
    手动构造,Z=0,如:
    (0,0,0), (1,0,0), (2,0,0)…
    单位:米 / 毫米都行,只要统一

  2. imagePoints
    findChessboardCorners 检测到的 2D 角点

  3. imageSize
    图像分辨率(w, h)

  4. K(输出内参)

[ fx  0  cx ]
[ 0  fy cy ]
[ 0  0   1 ]
  1. D(输出KB畸变)
    k1, k2, k3, k4 四个系数

  2. rvecs, tvecs
    每张图片的外参(旋转、平移)

  3. flags 标定标志(超级重要)
    常用组合:

fisheye::CALIB_RECOMPUTE_EXTRINSIC
| fisheye::CALIB_FIX_SKEW
| fisheye::CALIB_FIX_K4

含义:

  • FIX_SKEW:固定扭曲为0(几乎所有相机都用)
  • RECOMPUTE_EXTRINSIC:优化时重新计算外参
  • FIX_K4:固定k4=0,更稳定

返回值

重投影误差(越小越好)

  • < 0.5 像素:优秀
  • 0.5~1.0:合格
  • 1.5:标定失败


标准标定流程

采集图片
→ 转灰度图
→ findChessboardCorners()
→ cornerSubPix()
→ 存储所有3D/2D点
→ fisheye::calibrate()
→ 得到 K + D

总结

  • 张正友标定法:入门首选,成本低、精度高,适合所有相机标定场景
  • EoL产线标定:车载量产必备,消除安装误差,保证摄像头一致性
  • 实战代码:Android + C++ + OpenCV 实现,一张图片就能完成标定和去畸变

在这里插入图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐