OpenCV C++ 学习笔记
OpenCV 的安装和配置
Windows
OpenCV 官网下载安装 Windows 最新版预编译包(v4.12.0),之后将 build\x64\vc16
目录下的 bin
和 lib
文件夹添加到环境变量。
下载安装 Visual Studio,选择 C++ 桌面项目开发安装。之后创建新项目,选择 C++ 空项目,进入后修改项目属性,将 build\include
和 build\include\opencv2
添加到 VC++ 的包含目录中,然后将 build\x64\vc16\lib
添加到库目录,再将 opencv_world4120d.lib
添加到链接器-输入-附加依赖项。
之后运行程序,检查是否配置成功。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取图片
cv::Mat image = cv::imread("D:\\Documents\\Opencv_study\\task1_1.png");
// 检查图片是否成功加载
if (image.empty()) {
std::cout << "无法加载图片!" << std::endl;
return -1;
}
// 显示图片
cv::imshow("Display Image", image);
// 等待按键
cv::waitKey(0);
return 0;
}
注意:
由于官网下载的 Windows 预编译包是用 VC 编译的,因此只能使用 Visual Studio,不能使用 Vscode+MinGW。
Linux
使用命令安装。
sudo apt-get install libopencv-dev
或者下载源码自行 CMake 编译。
官网下载源码后解压,cd
进入文件夹。
mkdir build
cd build
cmake ..
make -j8
sudo make install
关于 WSL 的 GUI
将 WSL 版本切换为 WSL2,否则在使用 OpenCV 的 cv::imshow
时不会显示窗口。同时,并不需要像网络上搜索得知的那样下载 X Server,WSL2 有官方的 GUI 支持。害我折腾一晚上加一早上……
OpenCV
CMake 编译
-
CMakeLists.txt
:cmake_minimum_required(VERSION 3.10) project(...) set(CMAKE_CXX_STANDARD 17) add_executable(...) find_package(OpenCV REQUIRED) if (NOT OpenCV_FOUND) message(FATAL_ERROR "OpenCV not found!") endif() include_directories(${OpenCV_INCLUDE_DIRS}) target_link_libraries(... ${OpenCV_LIBS})
图像读取(legacy/display_image.cpp
)
使用 cv::Mat
声明矩阵,cv::imread
读取图片,cv::imshow
显示图片。
#include <iostream>
#include <opencv2/opencv.hpp>
int main() {
cv::Mat img = cv::imread("/home/hanx16msgr/robomaster/week2/assets/light.png");
if (img.empty()) {
std::cout << "No such image." << std::endl;
return -1;
}
cv::imshow("img", img);
cv::waitKey(0);
return 0;
}
图像二值化(legacy/binary_image.cpp
)
图像的细节可能很多,而实际处理中不需要使用到这些多余细节,因此可以对图像二值化操作,使得图像信息量减少,方便处理。
利用函数 cv::threshold(src_img, dst_img, value, max_value, type)
来进行对图像的二值化操作。
#include <iostream>
#include <opencv2/opencv.hpp>
int main() {
cv::Mat img = cv::imread("/home/hanx16msgr/robomaster/week2/assets/light.png");
if (img.empty()) {
std::cout << "No such Image" << std::endl;
return -1;
}
cv::Mat binary_img;
const double threshold_value = 128.; // 二值化阈值
const double max_binary_value = 255.; // 最大值
cv::threshold(img, binary_img, threshold_value, max_binary_value, cv::THRESH_BINARY);
cv::imshow("output", binary_img);
cv::waitKey(0);
return 0;
}
轮廓查找(legacy/find_contours.cpp
)
OpenCV 中提供了函数 cv::findContours
来方便的查找一个二值图上的各个轮廓。
cv::findContours(src_img, contours, hierarchy, mode, method):
src_img
:图片源(最好使用二值化后的图像)。contours
:为std::vector<std::vector<cv::Point>>
类型,用于返回各个轮廓和其包括的点。hierarchy
:为std::vector<cv::Vec4i>
类型,四元组中依次代表:同层下一个轮廓索引、同层上一个轮廓索引、下一层第一个子轮廓索引和上层父轮廓索引。mode
:代表检测模式标志,使用RETR_TREE
构建为树形结构。method
:使用CHAIN_APPROX_NONE
保存轮廓上的每一个点。使用CHAIN_APPROX_SIMPLE
压缩同行同列的点存储。
当我们不需要关心轮廓之间的归属关系的时候,可以使用 cv::findContours(src_img, contours, mode, method)
来仅获取轮廓信息,以节约获取轮廓归属关系的开销。
使用 cv::drawContours(img, contours, index, color, thickness)
来绘制轮廓线。
#include <iostream>
#include <opencv2/opencv.hpp>
constexpr double threshold_value = 60.; // 二值化阈值
constexpr double max_binary_value = 255.; // 最大值
constexpr int thickness = 5; // 轮廓线粗细
int main() {
// 读取图像
cv::Mat img = cv::imread("light.png");
if (img.empty()) {
std::cout << "No such Image!" << std::endl;
return -1;
}
// 转换为灰度图
cv::Mat gray;
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
// 二值化
cv::Mat binary_img;
cv::threshold(gray, binary_img, threshold_value, max_binary_value, cv::THRESH_BINARY);
// 查找轮廓
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(binary_img, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// 绘制轮廓
for (size_t i = 0; i < contours.size(); ++i) {
cv::drawContours(img, contours, i, cv::Scalar(0, 255, 0), thickness); // 用绿色标定轮廓线
}
// 显示结果
cv::imshow("Result", img);
cv::waitKey(0);
return 0;
}
相机标定
一个物体之于相机,有 4 个参考系,分别为世界坐标系、相机坐标系、图像坐标系、像素坐标系。现在需要将这个物体的坐标从世界坐标系转换到像素坐标系下。
一步一步来。
坐标系变换
世界坐标系 - 相机坐标系
计世界坐标为 \((X_{PW},Y_{PW},Z_{PW})\),相机坐标为 \((X_{PC},Y_{PC},Z_{PC})\)。那么从世界坐标到相机坐标所作的事情,就是将坐标旋转一个角度后平移。为了方便进行坐标变换,因此使用齐次坐标。
其中 \(\begin{bmatrix}R|t\end{bmatrix}\) 为相机的外参,$R $ 为一个 \(3\times 3\) 的矩阵,用于表示旋转;\(t\) 为 \(1\times 3\) 的矩阵,用于表示平移。
相机坐标系 - 图像坐标系
计图像坐标为 \((X_P,Y_P)\),\(x\) 方向和 \(y\) 方向上的焦距分别为 $f_x,f_y $。那么根据相似,可以有:
图像坐标系 - 像素坐标系
计像素坐标为 \((u_P,v_P)\)。从图像坐标系到像素坐标系,会因为相机的原因出现图像的缩放和平移。并且可能出现 \(x-y\) 轴并不垂直的情况,需要修正。因此有:
综合以上公式,我们有:
对这个式子重写,可以写成这样:
其中:
- \(s=Z_{PC}\) 为尺度参数。
- \(A=\displaystyle \begin{bmatrix} f_xs_x&c&c_x\\ 0&f_ys_y&c_y\\ 0&0&1 \end{bmatrix}\) 为相机内参,\(\begin{bmatrix}R|t\end{bmatrix}\) 为相机外参。
- \(\tilde m\) 为图像的齐次坐标,\(\tilde M\) 为模型点的齐次坐标。
图像畸变
实际情况中,拍摄的图片可能出现畸变,可分为径向畸变和切向畸变。
径向畸变
这种畸变来源于相机镜头的边缘畸变。
其中 \(r^2=x^2+y^2\),一般镜头只需要 \(k_1,k_2\) 即可校准,但是对于超广角镜头则还需要 \(k_3\) 来校准。
切向畸变
这种畸变来源于被拍摄平面与相机平面不平行。
其中 \(r^2=x^2+y^2\)。
所以要想找到一个对应关系,我们需要的就是:相机外参,相机内参,镜头畸变系数。
相机内参和镜头畸变系数的测定
首先需要找到一张棋盘格的图片,然后对其按照不同方向拍照,得到若干张不同角度的棋盘格照片,导入项目文件夹后,编写代码测定参数。
#include <iostream>
#include <opencv2/opencv.hpp>
constexpr int board_width = 8; // 棋盘格大小,表示内部交点的个数
constexpr int board_height = 5;
constexpr double square_size = 1.;
const cv::Size board_size(board_width, board_height);
int main() {
std::vector<std::vector<cv::Point3f>> object_points;
std::vector<std::vector<cv::Point2f>> image_points;
std::vector<cv::Point2f> corners;
cv::Mat image, gray, small_image;
std::vector<cv::String> file_names;
cv::glob("assets/board*.jpg", file_names);
for (cv::String& file_name: file_names) {
image = cv::imread(file_name);
// 按比例缩小图片,防止因为图片尺寸过大导致 cv::findChessboardCorners 运行缓慢
double scale = 1280. / image.cols;
cv::resize(image, small_image, cv::Size(), scale, scale, cv::INTER_LINEAR);
cv::cvtColor(small_image, gray, cv::COLOR_BGR2GRAY);
// 寻找棋盘格的交点
bool found = cv::findChessboardCorners(gray, board_size, corners,
cv::CALIB_CB_ADAPTIVE_THRESH +
cv::CALIB_CB_NORMALIZE_IMAGE +
cv::CALIB_CB_FAST_CHECK);
std::cout << file_name << '\n';
if (!found)
continue;
// 精细化交点坐标
cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1),
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1));
cv::drawChessboardCorners(small_image, board_size, corners, found);
cv::imshow("image", small_image);
cv::waitKey();
std::vector<cv::Point3f> object_corners;
for (int i = 0; i < board_height; ++i) {
for (int j = 0; j < board_width; ++j) {
object_corners.emplace_back(i * square_size, j * square_size, 0);
}
}
object_points.emplace_back(object_corners);
image_points.emplace_back(corners);
}
// 调用 cv::calibrateCamera 测定参数
cv::Mat camera_matrix, dist_coeffs;
std::vector<cv::Mat> rvecs, tvecs;
cv::calibrateCamera(object_points, image_points, board_size, camera_matrix, dist_coeffs, rvecs, tvecs);
std::cout << "Camera matrix:\n" << camera_matrix << std::endl;
std::cout << "Distortion coefficients:\n" << dist_coeffs << std::endl;
return 0;
}