【智能驾驶:视觉感知后处理 阅读笔记】Day6: 张正友标定法

书籍

张正友标定法
相机标定到底在做什么?
相机标定,就是给相机做一个“身份认证”:
- 内参矩阵:描述相机本身的光学属性(焦距、主点位置)
- 畸变系数:描述镜头带来的图像变形(径向/切向畸变)
- 外参矩阵:描述相机在世界中的位置和朝向
只有拿到这些参数,我们才能把真实世界的3D点,准确投影到图片的2D像素上,后续的目标检测、SLAM、自动驾驶环视拼接才有意义。
最通用的标定方法:张正友棋盘格标定法
张正友标定法是目前工业界应用最广泛的方法,也叫“棋盘格标定法”,原理超级简单:
1. 核心思路
- 准备棋盘格标定板:黑白相间的格子,每个格子的尺寸是已知的,我们可以算出每个角点在世界坐标系下的3D坐标。
- 拍摄多张标定板图片:从不同角度、不同距离拍摄,获取足够多的角点数据。
- 检测图片中的角点:用算法找到每个角点在像素坐标系下的2D坐标。
- 求解相机参数:用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
);
参数解释
- image
输入图像,必须是灰度图(gray)。 - patternSize
棋盘格内角点尺寸,如Size(9,6)= 宽9个、高6个角点。
✅ 注意:不是格子数,是角点数! - corners
输出检测到的角点坐标(vector)。 - 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
);
参数解释
- image
灰度图 - corners
输入输出角点,输入是整数坐标,输出是亚像素坐标 - winSize
亚像素搜索窗口,一般用Size(11,11)
窗口越大越稳定,但可能模糊 - zeroZone
死区,一般设Size(-1,-1)表示不用 - 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)
);
参数详解
-
objectPoints
棋盘格 3D 世界坐标
手动构造,Z=0,如:
(0,0,0), (1,0,0), (2,0,0)…
单位:米 / 毫米都行,只要统一 -
imagePoints
findChessboardCorners 检测到的 2D 角点 -
imageSize
图像分辨率(w, h) -
K(输出内参)
[ fx 0 cx ]
[ 0 fy cy ]
[ 0 0 1 ]
-
D(输出KB畸变)
k1, k2, k3, k4四个系数 -
rvecs, tvecs
每张图片的外参(旋转、平移) -
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 实现,一张图片就能完成标定和去畸变

更多推荐
所有评论(0)