OpenCV C++ 学习笔记

OpenCV 的安装和配置

Windows

OpenCV 官网下载安装 Windows 最新版预编译包(v4.12.0),之后将 build\x64\vc16目录下的 binlib 文件夹添加到环境变量。

下载安装 Visual Studio,选择 C++ 桌面项目开发安装。之后创建新项目,选择 C++ 空项目,进入后修改项目属性,将 build\includebuild\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} X_{PC}\\ Y_{PC}\\ Z_{PC} \end{bmatrix} =\begin{bmatrix} R|t \end{bmatrix} \begin{bmatrix} X_{PW}\\ Y_{PW}\\ Z_{PW}\\ 1 \end{bmatrix} \]

其中 \(\begin{bmatrix}R|t\end{bmatrix}\) 为相机的外参,$R $ 为一个 \(3\times 3\) 的矩阵,用于表示旋转;\(t\)\(1\times 3\) 的矩阵,用于表示平移。

相机坐标系 - 图像坐标系

计图像坐标为 \((X_P,Y_P)\)\(x\) 方向和 \(y\) 方向上的焦距分别为 $f_x,f_y $。那么根据相似,可以有:

\[\begin{bmatrix} X_P\\ Y_P\\ 1 \end{bmatrix} =\frac{1}{Z_{PC}} \begin{bmatrix} f_x&0&0\\ 0&f_y&0\\ 0&0&1 \end{bmatrix} \begin{bmatrix} X_{PC}\\ Y_{PC}\\ Z_{PC} \end{bmatrix} \]

图像坐标系 - 像素坐标系

计像素坐标为 \((u_P,v_P)\)。从图像坐标系到像素坐标系,会因为相机的原因出现图像的缩放和平移。并且可能出现 \(x-y\) 轴并不垂直的情况,需要修正。因此有:

\[\begin{bmatrix} u_P\\ v_P\\ 1 \end{bmatrix} =\begin{bmatrix} s_x&c&c_x\\ 0&s_y&c_y\\ 0&0&1 \end{bmatrix} \begin{bmatrix} X_P\\ Y_P\\ 1 \end{bmatrix} \]


综合以上公式,我们有:

\[\begin{bmatrix} u_P\\ v_P\\ 1 \end{bmatrix}= \frac{1}{Z_{PC}} \begin{bmatrix} s_x&c&c_x\\ 0&s_y&c_y\\ 0&0&1 \end{bmatrix} \begin{bmatrix} f_x&0&0\\ 0&f_y&0\\ 0&0&1 \end{bmatrix} \begin{bmatrix} R|t \end{bmatrix} \begin{bmatrix} X_{PW}\\ Y_{PW}\\ Z_{PW}\\ 1 \end{bmatrix} \]

对这个式子重写,可以写成这样:

\[s\tilde{m}=A\begin{bmatrix}R|t\end{bmatrix}\tilde{M} \]

其中:

  • \(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\) 为模型点的齐次坐标。

图像畸变

实际情况中,拍摄的图片可能出现畸变,可分为径向畸变切向畸变

径向畸变

这种畸变来源于相机镜头的边缘畸变。

\[x'=x(1+k_1r^2+k_2r^4+k_3r^6)\\ y'=y(1+k_1r^2+k_2r^4+k_3r^6) \]

其中 \(r^2=x^2+y^2\),一般镜头只需要 \(k_1,k_2\) 即可校准,但是对于超广角镜头则还需要 \(k_3\) 来校准。

切向畸变

这种畸变来源于被拍摄平面与相机平面不平行。

\[x'=x+[2p_1xy+p_2(r^2+2x^2)]\\ y'=y+[p_1(r^2+2y^2)+2p_2xy] \]

其中 \(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;
}
posted @ 2025-09-13 16:42  Hanx16Msgr  阅读(11)  评论(1)    收藏  举报