
相机标定原理
相机标定是机器视觉的基石,它通过确定相机的内部参数、外部参数和畸变系数,建立三维世界与二维图像之间的精确映射关系。
标定结果可用于三维重建、SLAM、目标测量、增强现实等关键应用。
本文将从针孔相机模型、标定原理到Android NDK工程实现,带你完整掌握基于OpenCV的相机标定技术,并提供可直接发布的博文内容与可运行源码。
相机标定核心原理
1. 针孔相机模型与成像过程
相机的成像过程本质上是三维场景到二维图像的投影过程,这一过程可以通过针孔相机模型来描述:

- 薄透镜公式 :描述焦距
f、物距do和像距di的关系:
1f=1do+1di\frac{1}{f} = \frac{1}{do} + \frac{1}{di}f1=do1+di1 - 相似三角形投影 :物体高度
ho与图像高度hi满足:
hi=f⋅hodohi = f \cdot \frac{ho}{do}hi=f⋅doho - 齐次坐标下的投影矩阵 :将三维世界坐标
(X, Y, Z)转换为图像像素坐标(u, v):
suv1=fx0u00fyv0001RTXYZ1s \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \begin{bmatrix} f_x & 0 & u_0 \\ 0 & f_y & v_0 \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} R & T \end{bmatrix} \begin{bmatrix} X \\ Y \\ Z \\ 1 \end{bmatrix}s uv1 = fx000fy0u0v01 RT XYZ1
其中:f_x, f_y:x、y方向的焦距(像素单位)u_0, v_0:主点(光轴与图像平面的交点)R, T:相机的旋转矩阵和平移向量(外部参数)
2. 镜头畸变模型
真实镜头存在畸变,其中最主要的是径向畸变:
- 桶形畸变:图像边缘向外膨胀,常见于广角镜头;
- 枕形畸变:图像边缘向内收缩,常见于长焦镜头。
OpenCV采用5参数畸变模型:
{xdistorted=x(1+k1r2+k2r4+k3r6)+2p1xy+p2(r2+2x2)ydistorted=y(1+k1r2+k2r4+k3r6)+p1(r2+2y2)+2p2xy\begin{cases} x_{distorted} = x(1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + 2p_1xy + p_2(r^2 + 2x^2) \\ y_{distorted} = y(1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + p_1(r^2 + 2y^2) + 2p_2xy \end{cases}{xdistorted=x(1+k1r2+k2r4+k3r6)+2p1xy+p2(r2+2x2)ydistorted=y(1+k1r2+k2r4+k3r6)+p1(r2+2y2)+2p2xy
其中 r = \sqrt{x^2 + y^2},k1, k2, k3 为径向畸变系数,p1, p2 为切向畸变系数。
3. 棋盘格标定原理
OpenCV相机标定采用棋盘格图案作为标定物,其核心流程如下:
- 采集标定图像:从不同角度拍摄棋盘格图像;
- 检测角点 :使用
findChessboardCorners检测棋盘格内角点; - 亚像素级优化 :使用
cornerSubPix提高角点精度; - 求解标定参数 :通过
calibrateCamera函数求解相机内参、畸变系数和外参; - 畸变校正 :使用
initUndistortRectifyMap和remap对图像进行去畸变处理。
OpenCV核心API解析
1. cv::findChessboardCorners:检测棋盘格角点
cpp
bool findChessboardCorners(
InputArray image, // 输入灰度图像
Size patternSize, // 棋盘格内角点数量(cols, rows)
OutputArray corners, // 输出角点坐标
int flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE
);
patternSize:棋盘格内部交点数,例如6×4的棋盘格应设置为Size(6,4);- 返回值:
true表示成功检测到角点。
2. cv::cornerSubPix:亚像素级角点优化
cpp
void cornerSubPix(
InputArray image, // 输入灰度图像
InputOutputArray corners, // 输入/输出角点坐标
Size winSize, // 搜索窗口大小
Size zeroZone, // 死区大小,(-1,-1)表示无死区
TermCriteria criteria // 迭代终止条件
);
- 用于将角点坐标优化到亚像素级,提升标定精度。
真实世界里棋盘格角点、边缘交点,几乎不会刚好落在像素格子的整数交点上,而是落在两个像素之间 / 像素内部。
只用整数像素坐标:存在 0~1 像素的误差
亚像素:把角点定位到像素内部,把精度从「整像素」提升到「小数级」
- Size(5,5) 窗口大小
以当前角点为中心,取 5×5 范围的像素做计算。
棋盘格场景:(5,5) / (7,7) 最常用;
纹理复杂大图可适当调大。
3. cv::calibrateCamera:相机标定核心函数
cpp
double calibrateCamera(
InputArrayOfArrays objectPoints, // 世界坐标系中的三维点
InputArrayOfArrays imagePoints, // 图像坐标系中的二维点
Size imageSize, // 图像尺寸
InputOutputArray cameraMatrix, // 输出相机内参矩阵
InputOutputArray distCoeffs, // 输出畸变系数
OutputArrayOfArrays rvecs, // 输出旋转向量
OutputArrayOfArrays tvecs, // 输出平移向量
int flags = 0, // 标定标志位
TermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON)
);
-
返回值:重投影误差,误差越小表示标定结果越准确。
-
cameraMatrix 相机内参矩阵
输出 Mat,固定 3×3 矩阵
输出结果示例:
plaintext
[fx, 0, u0]
[ 0, fy, v0]
[ 0, 0, 1]
标定完成后,这组参数可以离线保存,后续直接复用,不用重复标定。
- rvecs / tvecs 外参
rvecs:每张标定图的旋转向量(罗德里格斯向量)
tvecs:每张标定图的平移向量
作用:描述相机相对于棋盘格的姿态,单相机畸变校正基本用不到;双目视觉、三维重建、AR 才会使用。
4. cv::initUndistortRectifyMap:计算畸变校正映射
cpp
void initUndistortRectifyMap(
InputArray cameraMatrix, // 相机内参矩阵
InputArray distCoeffs, // 畸变系数
InputArray R, // 可选矫正项(无)
InputArray newCameraMatrix, // 矫正后的相机矩阵
Size size, // 图像尺寸
int m1type, // 映射图类型(CV_32FC1/CV_32FC2)
OutputArray map1, // 输出x方向映射图
OutputArray map2 // 输出y方向映射图
);
- map1 / map2 两张映射表(核心输出)
map1:存储每个像素对应的 X 方向目标坐标
map2:存储每个像素对应的 Y 方向目标坐标
两张表尺寸和原图一模一样,每个位置存一个坐标值。
理解:遍历原图每一个 (x,y),去 map1/map2 里查「这个像素应该放到新图的哪个位置」
5. cv::remap:图像畸变校正
cpp
void remap(
InputArray src, // 输入图像
OutputArray dst, // 输出校正后图像
InputArray map1, // x方向映射图
InputArray map2, // y方向映射图
int interpolation, // 插值方式
int borderMode = BORDER_CONSTANT, // 边界处理方式
const Scalar& borderValue = Scalar() // 边界值
);
interpolation插值方式(重点)
映射坐标是小数(亚像素),一个位置会对应原图多个像素,需要插值算颜色。常用枚举:
| 插值类型 | 说明 | 适用场景 |
|---|---|---|
INTER_NEAREST 最近邻 |
取最近整数像素,速度最快,图像易出现锯齿 | 实时预览、对画质要求低 |
INTER_LINEAR 双线性插值 |
周围4个像素加权平均,画质平滑,速度均衡 | 默认首选,畸变校正标准用法(你的代码在用) |
INTER_CUBIC 双三次插值 |
周围16个像素计算,画质最好,速度偏慢 | 高清图像、精细处理 |
你的代码:
INTER_LINEAR,兼顾画质与速度,棋盘格/普通图像都适用。
borderMode边界模式
当映射坐标超出原图范围时的处理规则:BORDER_CONSTANT:边界填充固定颜色(默认),校正后图像四周出现黑边就是这个原因BORDER_REPLICATE:复制图像边缘像素向外延伸,无黑边BORDER_WRAP:图像循环平铺,几乎不用在校正场景
Android完整工程实现
本工程基于 Android NDK + OpenCV,实现从棋盘格图像采集、角点检测、相机标定到畸变校正的完整流程。为简化演示,我们使用单张棋盘格图像进行角点检测和畸变校正演示,实际标定需多张图像。
1. 布局文件 activity_main.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f5f5f5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<!-- 原始图片展示 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="原始棋盘格图像"
android:textSize="16sp"
android:textStyle="bold"/>
<ImageView
android:id="@+id/iv_origin"
android:layout_width="match_parent"
android:layout_height="220dp"
android:scaleType="fitCenter"
android:background="#ffffff"/>
</LinearLayout>
<!-- 角点检测结果 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="棋盘格角点检测结果"
android:textSize="16sp"
android:textStyle="bold"/>
<ImageView
android:id="@+id/iv_corners"
android:layout_width="match_parent"
android:layout_height="220dp"
android:scaleType="fitCenter"
android:background="#ffffff"/>
</LinearLayout>
<!-- 畸变校正结果 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="畸变校正结果"
android:textSize="16sp"
android:textStyle="bold"/>
<ImageView
android:id="@+id/iv_undistorted"
android:layout_width="match_parent"
android:layout_height="220dp"
android:scaleType="fitCenter"
android:background="#ffffff"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
2. 上层 Kotlin 代码 MainActivity.kt
kotlin
package com.example.cameracalibration
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
init {
System.loadLibrary("native-lib")
}
}
/**
* JNI原生方法:执行棋盘格角点检测与畸变校正
* @param srcBitmap 输入原图Bitmap
* @param outCorners 输出角点检测结果Bitmap
* @param outUndistorted 输出畸变校正结果Bitmap
*/
private external fun processCameraCalibration(
srcBitmap: Bitmap,
outCorners: Bitmap,
outUndistorted: Bitmap
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 加载2048×2048棋盘格测试图片
val srcBitmap = BitmapFactory.decodeResource(resources, R.drawable.chessboard_image)
// 创建输出位图
val cornersBitmap = Bitmap.createBitmap(srcBitmap.width, srcBitmap.height, Bitmap.Config.ARGB_8888)
val undistortedBitmap = Bitmap.createBitmap(srcBitmap.width, srcBitmap.height, Bitmap.Config.ARGB_8888)
// 调用原生算法
processCameraCalibration(srcBitmap, cornersBitmap, undistortedBitmap)
// 展示结果
findViewById<ImageView>(R.id.iv_origin).setImageBitmap(srcBitmap)
findViewById<ImageView>(R.id.iv_corners).setImageBitmap(cornersBitmap)
findViewById<ImageView>(R.id.iv_undistorted).setImageBitmap(undistortedBitmap)
}
}
3. 底层 C++ JNI 代码 native-lib.cpp
cpp
#include <jni.h>
#include <opencv2/opencv.hpp>
#include <android/bitmap.h>
#include <vector>
using namespace cv;
using namespace std;
/**
* Bitmap 转 OpenCV Mat(RGBA -> BGR)
*/
Mat bitmapToMat(JNIEnv *env, jobject bitmap) {
AndroidBitmapInfo info;
void* pixels = nullptr;
AndroidBitmap_getInfo(env, bitmap, &info);
AndroidBitmap_lockPixels(env, bitmap, &pixels);
Mat rgba(info.height, info.width, CV_8UC4, pixels);
Mat bgr;
cvtColor(rgba, bgr, COLOR_RGBA2BGR);
AndroidBitmap_unlockPixels(env, bitmap);
return bgr;
}
/**
* OpenCV Mat 转 Bitmap(支持灰度图和彩色图)
*/
void matToBitmap(JNIEnv *env, const Mat& srcMat, jobject dstBitmap) {
AndroidBitmapInfo info;
void* pixels = nullptr;
AndroidBitmap_getInfo(env, dstBitmap, &info);
AndroidBitmap_lockPixels(env, dstBitmap, &pixels);
Mat rgba;
if (srcMat.channels() == 1) {
cvtColor(srcMat, rgba, COLOR_GRAY2RGBA);
} else {
cvtColor(srcMat, rgba, COLOR_BGR2RGBA);
}
memcpy(pixels, rgba.data, info.width * info.height * 4);
AndroidBitmap_unlockPixels(env, dstBitmap);
}
/**
* 相机标定核心类
*/
class CameraCalibrator {
public:
vector<vector<Point3f>> objectPoints;
vector<vector<Point2f>> imagePoints;
Mat cameraMatrix;
Mat distCoeffs;
Size imageSize;
Mat map1, map2;
bool mustInitUndistort;
CameraCalibrator() : mustInitUndistort(true) {}
// 添加棋盘格角点对
void addPoints(const vector<Point2f>& imageCorners, const vector<Point3f>& objectCorners) {
imagePoints.push_back(imageCorners);
objectPoints.push_back(objectCorners);
}
// 相机标定
double calibrate(const Size& imageSize) {
this->imageSize = imageSize;
vector<Mat> rvecs, tvecs;
mustInitUndistort = true;
return calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs);
}
// 畸变校正
Mat remap(const Mat& image) {
Mat undistorted;
if (mustInitUndistort) {
initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(), Mat(), image.size(), CV_32FC1, map1, map2);
mustInitUndistort = false;
}
cv::remap(image, undistorted, map1, map2, INTER_LINEAR);
return undistorted;
}
};
/**
* 棋盘格角点检测与标定流程
*/
void cameraCalibrationProcess(const Mat& srcBgr, Mat& outCorners, Mat& outUndistorted) {
// 1. 转灰度图
Mat srcGray;
cvtColor(srcBgr, srcGray, COLOR_BGR2GRAY);
// 2. 棋盘格参数(6×4内角点)
Size boardSize(6, 4);
vector<Point2f> imageCorners;
bool found = findChessboardCorners(srcGray, boardSize, imageCorners);
// 3. 亚像素级角点优化
if (found) {
cornerSubPix(srcGray, imageCorners, Size(5, 5), Size(-1, -1), TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 30, 0.1));
}
// 4. 绘制角点检测结果
outCorners = srcBgr.clone();
drawChessboardCorners(outCorners, boardSize, imageCorners, found);
// 5. 生成棋盘格三维点坐标(Z=0平面)
vector<Point3f> objectCorners;
for (int i = 0; i < boardSize.height; i++) {
for (int j = 0; j < boardSize.width; j++) {
objectCorners.emplace_back(j, i, 0.0f);
}
}
// 6. 相机标定(演示用,实际需多张图像)
CameraCalibrator calibrator;
if (found) {
calibrator.addPoints(imageCorners, objectCorners);
calibrator.calibrate(srcGray.size());
}
// 7. 畸变校正
outUndistorted = calibrator.remap(srcBgr);
}
/**
* JNI入口函数
*/
extern "C" JNIEXPORT void JNICALL
Java_com_example_cameracalibration_MainActivity_processCameraCalibration(
JNIEnv *env, jobject thiz,
jobject srcBitmap,
jobject outCorners,
jobject outUndistorted)
{
// Bitmap 转 Mat
Mat srcBgr = bitmapToMat(env, srcBitmap);
Mat matCorners, matUndistorted;
// 执行相机标定算法
cameraCalibrationProcess(srcBgr, matCorners, matUndistorted);
// 结果回传给上层Bitmap
matToBitmap(env, matCorners, outCorners);
matToBitmap(env, matUndistorted, outUndistorted);
}

拓展应用与进阶方向
1. 三维重建
利用相机标定结果,结合双目相机或单目相机运动,可实现三维场景重建。
2. 增强现实
通过相机内参矩阵和畸变系数,可将虚拟物体精确投影到真实场景中。
3. 目标测量
基于标定后的相机模型,可从二维图像中计算物体的实际尺寸,实现非接触式测量。
总结
- 核心流程:棋盘格图像采集 → 角点检测 → 亚像素优化 → 相机标定 → 畸变校正;
- 关键参数:相机内参矩阵、畸变系数是标定的核心输出,直接影响后续应用精度;
- 工程价值:源码基于Android NDK + OpenCV实现,可直接移植到工业视觉、AR、三维重建等项目中;
- 精度保障:实际应用中需严格按照标定流程采集图像,确保重投影误差满足要求。
