OpenCV实战(22)——单应性及其应用
- 0. 前言
- 1. 单应性
- 1.1 单应性基础
- 1.2 计算两个图像之间的单应性
- 1.3 完整代码
- 2. 检测图像中的平面目标
- 2.1 特征匹配
- 2.2 完整代码
- 小结
- 系列链接
0. 前言
我们已经学习了如何从一组匹配项中计算图像对的基本矩阵。在射影几何中,还存在另一个非常有用的数学实体——单应性,可以利用多视图图像计算,它是一个具有特殊属性的矩阵,具有重要用途。
1. 单应性
1.1 单应性基础
我们已经学习了 3D
点与其在相机上的图像之间的投影关系,并且了解了投影方程使用相机的内在属性和相机位置(用旋转和平移分量指定)将 3D
点与其图像相关联。如果我们仔细检查投影方程,就会意识到存在两种有趣的特殊情况。第一种情况是场景的两个视图仅仅是由相机旋转得到的,则外部矩阵的第四列将全为 0
(即平移为空),例如,通过自身旋转拍摄建筑物或风景时,就会出现这种情况,由于相机离被摄主体足够远,因此移动部分可以忽略不计:
S
[
x
y
1
]
=
[
f
0
0
0
f
0
0
0
1
]
[
r
1
r
2
r
3
0
r
4
r
5
r
6
0
r
7
r
8
r
9
0
]
[
X
Y
Z
1
]
S\left[ \begin{array}{ccc} x\\ y\\ 1\\\end{array}\right]=\left[ \begin{array}{ccc} f&0&0\\ 0&f&0\\ 0&0&1\\\end{array}\right]\left[ \begin{array}{ccc} r_1&r_2&r_3&0\\ r_4&r_5&r_6&0\\ r_7&r_8&r_9&0\\\end{array}\right]\left[ \begin{array}{ccc} X\\ Y\\ Z\\ 1\\\end{array}\right]
S
xy1
=
f000f0001
r1r4r7r2r5r8r3r6r9000
XYZ1
这种特殊情况下的投影关系变成了 3x3
矩阵。当被拍摄到的物体是平面时,也会出现类似情况,在这种特定情况下,我们可以假设该平面上的点均位于
Z
=
0
Z=0
Z=0 上,不失一般性,我们可以得到以下等式:
S
[
x
y
1
]
=
[
f
0
0
0
f
0
0
0
1
]
[
r
1
r
2
r
3
t
1
r
4
r
5
r
6
t
2
r
7
r
8
r
9
t
3
]
[
X
Y
0
1
]
S\left[ \begin{array}{ccc} x\\ y\\ 1\\\end{array}\right]=\left[ \begin{array}{ccc} f&0&0\\ 0&f&0\\ 0&0&1\\\end{array}\right]\left[ \begin{array}{ccc} r_1&r_2&r_3&t_1\\ r_4&r_5&r_6&t_2\\ r_7&r_8&r_9&t_3\\\end{array}\right]\left[ \begin{array}{ccc} X\\ Y\\ 0\\ 1\\\end{array}\right]
S
xy1
=
f000f0001
r1r4r7r2r5r8r3r6r9t1t2t3
XY01
场景点的零坐标将导致投影矩阵的第三列为 0
,投影矩阵最终变为 3x3
矩阵。这个特殊的矩阵称为单应性,它意味着在特殊情况下(在本节中指纯旋转或平面对象),一个点与其图像通过以下形式的线性关系相关联:
S
[
x
′
y
′
1
]
=
H
[
x
y
1
]
S\left[ \begin{array}{ccc} x'\\ y'\\ 1\\\end{array}\right]=H\left[ \begin{array}{ccc} x\\ y\\ 1\\\end{array}\right]
S
x′y′1
=H
xy1
其中,
H
H
H 是一个 3x3
矩阵,标量值
S
S
S 表示比例因子。得到矩阵
H
H
H 的估计值后,就可以使用这种关系将一个视图中的点转移到第二个视图。需要注意的是,在单应关系下基本矩阵将并不确定。
1.2 计算两个图像之间的单应性
假设我们有两张仅由旋转相机得到的图像。可以使用选择的特征和 cv::BFMatcher
函数来匹配这两个图像,然后应用随机抽样一致算法 (RANdom SAmple Consensus
, RANSAC
) 步骤,最后使用基于匹配集(包含大量异常值)的单应性估计。
(1) 通过使用 cv::findHomography
函数进行单应性估计,该函数与 cv::findFundamentalMat
函数相似:
// 查找图像 1 和图像 2 间的单应性
std::vector<char> inliers;
cv::Mat homography= cv::findHomography(
points1, points2, // 对应点
inliers, // inliers 匹配
cv::RANSAC, // RANSAC 方法
1.); // 投影点最大距离
(2) 本节使用的两张图像是通过旋转相机得到的,两张图像如下所示,我们还显示了由函数的 inliers
参数标识的正确关键点:
(3) 使用循环在以上图像上绘制符合所发现单应性的最终正确值:
std::vector<cv::Point2f>::const_iterator itPts = points1.begin();
std::vector<cv::Point2f>::const_iterator itIn = inliers.begin();
while (itPtr!=points1.end()) {
if (*itIn) cv::circle(image1, *itPtr, 3, cv::Scalar(255, 255, 255));
++itPts;
++itIn;
}
(4) 单应性是一个 3×3
的可逆矩阵,因此,一旦计算完成,就可以将图像点从一个图像转移到另一个图像。可以对图像的每个像素执行此操作,以完整转移图像。这个过程称为图像拼接,通常用于根据多个图像构建全景图,可以使用 cv::warpPerspective
函数执行此操作:
// 拼接图像 1 和图像 2
cv::Mat result;
cv::warpPerspective(image1, // 输入图像
result, // 结果
homography, // 单应性
cv::Size(2*image1.cols, image1.rows)); // 结果图像尺寸
(5) 由于现在这两个图像来自同一个角度,获得新图像后,可以将其附加到另一张图像以扩展视图:
cv::Mat half(result, cv::Rect(0, 0, image2.cols, image2.rows));
image2.copyTo(half);
结果如下图所示:
当两个视图通过单应性相关时,可以确定一个图像上的给定场景点在另一幅图像上的位置。由于第二个视图显示了在第一个图像中不可见的场景的一部分,因此可以使用单应性通过另一个图像中的值来扩展图像。这就是我们创建一个新图像的核心思想,它扩展了第一张图的可视角度,其在右侧增加了额外的场景像素值。
由 cv::findHomography
计算的单应性是将第一个图像中的点映射到第二个图像中的点。这个单应性最少可以由四个匹配计算,这里同样使用 RANSAC
算法,一旦找到具有最佳支持度的单应性,cv::findHomography
方法将使用所有识别的正确值对其进行细化。
为了将图像 1
中的点转移到图像 2
,我们实际上需要的是逆单应性,可以通过调用cv::warpPerspective
函数完成,它使用作为输入提供的单应性的逆来获得输出图像每个点的颜色值。当一个输出像素被转移到输入图像之外时,分配给这个像素黑色值 (0
)。需要注意的是,如果想在像素传输过程中直接使用单应性而非逆单应性,则可以使用 cv::WARP_INVERSE_MAP
标志作为第 5
个参数调用 cv::warpPerspective
函数。
1.3 完整代码
完整代码 estimateH.cpp
如下所示:
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <opencv2/stitching.hpp>
int main() {
// 读取输入图像
cv::Mat image1= cv::imread("4.png",0);
cv::Mat image2= cv::imread("5.png",0);
if (!image1.data || !image2.data)
return 0;
cv::namedWindow("Image 1");
cv::imshow("Image 1",image1);
cv::namedWindow("Image 2");
cv::imshow("Image 2",image2);
// 关键点和描述符向量
std::vector<cv::KeyPoint> keypoints1;
std::vector<cv::KeyPoint> keypoints2;
cv::Mat descriptors1, descriptors2;
// 1. 构建 SIFT 特征描述符
cv::Ptr<cv::Feature2D> ptrFeature2D = cv::xfeatures2d::SIFT::create(74);
// 2. 探测 SIFT 特征及其描述符
ptrFeature2D->detectAndCompute(image1, cv::noArray(), keypoints1, descriptors1);
ptrFeature2D->detectAndCompute(image2, cv::noArray(), keypoints2, descriptors2);
std::cout << "Number of feature points (1): " << keypoints1.size() << std::endl;
std::cout << "Number of feature points (2): " << keypoints2.size() << std::endl;
// 3. 匹配两种图像描述符
cv::BFMatcher matcher(cv::NORM_L2, true);
std::vector<cv::DMatch> matches;
matcher.match(descriptors1,descriptors2,matches);
// 绘制匹配
cv::Mat imageMatches;
cv::drawMatches(image1,keypoints1, // 第一张图像及其关键点
image2,keypoints2, // 第二张图像及其关键点
matches, // 匹配
imageMatches, // 结果图像
cv::Scalar(255,255,255), // 线条颜色
cv::Scalar(255,255,255)/*,// 关键点颜色
std::vector<char>(),
2*/);
cv::namedWindow("Matches (pure rotation case)");
cv::imshow("Matches (pure rotation case)",imageMatches);
// 将关键点转换为 Point2f
std::vector<cv::Point2f> points1, points2;
for (std::vector<cv::DMatch>::const_iterator it= matches.begin();
it!= matches.end(); ++it) {
// 获取左侧图像关键点
float x= keypoints1[it->queryIdx].pt.x;
float y= keypoints1[it->queryIdx].pt.y;
points1.push_back(cv::Point2f(x,y));
// 获取右侧图像关键点
x= keypoints2[it->trainIdx].pt.x;
y= keypoints2[it->trainIdx].pt.y;
points2.push_back(cv::Point2f(x,y));
}
std::cout << points1.size() << " " << points2.size() << std::endl;
// 查找图像 1 和图像 2 间的单应性
std::vector<char> inliers;
cv::Mat homography= cv::findHomography(
points1, points2, // 对应点
inliers, // inliers 匹配
cv::RANSAC, // RANSAC 方法
1.); // 投影点最大距离
// 绘制 inliers 点
cv::drawMatches(image1, keypoints1, // 第一张图及其关键点
image2, keypoints2, // 第二张图像及其关键点
matches, // 匹配
imageMatches, // 结果图像
cv::Scalar(255, 255, 255), // 线条颜色
cv::Scalar(255, 255, 255)/*,// 关键点颜色
inliers,
2*/);
cv::namedWindow("Homography inlier points");
cv::imshow("Homography inlier points", imageMatches);
// 拼接图像 1 和图像 2
cv::Mat result;
cv::warpPerspective(image1, // 输入图像
result, // 结果
homography, // 单应性
cv::Size(2*image1.cols, image1.rows)); // 结果图像尺寸
cv::Mat half(result, cv::Rect(0, 0, image2.cols, image2.rows));
image2.copyTo(half);
cv::namedWindow("Image mosaic");
cv::imshow("Image mosaic",result);
// 读取输入图像
std::vector<cv::Mat> images;
images.push_back(cv::imread("4.png"));
images.push_back(cv::imread("5.png"));
cv::Mat panorama; // 输出全景图相
// 创建拼接
cv::Ptr<cv::Stitcher> stitcher = cv::Stitcher::create();
// 拼接图像
cv::Stitcher::Status status = stitcher->stitch(images, panorama);
if (status == cv::Stitcher::OK){
cv::namedWindow("Panorama");
cv::imshow("Panorama", panorama);
}
cv::waitKey();
return 0;
}
2. 检测图像中的平面目标
一个平面的两个图像之间也存在单应性,我们可以利用它来识别图像中的平面物体。在上一小节中,我们介绍了如何使用单应性将通过相机旋转拍摄的图像拼接在一起以创建全景图,我们也还了解到平面的不同图像也会产生单应性,接下来,我们将学习如何利用这一原理识别图像中的平面对象。
假设我们要检测图像中的平面对象,例如海报、绘画、标牌、书籍封面等。为了检测平面对象,需要检测该对象上的特征点并尝试将它们与图像中的特征点进行匹配。然后将使用类似于特征匹配中使用的匹配方案来验证这些匹配,不同的是,本节的匹配方案基于单应性。
2.1 特征匹配
(1) 定义一个与 RobustMatcher 类相似的 TargetMatcher
类:
class TargetMatcher {
private:
// 指向特征点检测器对象的指针
cv::Ptr<cv::FeatureDetector> detector;
// 指向特征描述符提取器对象的指针
cv::Ptr<cv::DescriptorExtractor> descriptor;
cv::Mat target; // 目标图像
int normType;
double distance; // 最小重投影误差
public:
TargetMatcher(const cv::Ptr<cv::FeatureDetector> &detector,
const cv::Ptr<cv::DescriptorExtractor> &descriptor = cv::Ptr<cv::DescriptorExtractor>()) :
detector(detector), descriptor(descriptor),
normType(cv::NORM_L2), distance(1.0) {
if (!this->descriptor) {
this->descriptor = this->detector;
}
}
(2) 添加 target
属性,表示要匹配的平面物体的参考图像。匹配方法与 RobustMatcher
类的方法相同,只是在 ransacTest
方法中使用 cv::findHomography
而非 cv::findFundamentalMat
。我们还添加了一个用于目标匹配并找到目标位置的方法:
// 检测图像中的平面目标
cv::Mat detectTarget(const cv::Mat& image,
std::vector<cv::Point2f>& detectedCorners,
std::vector<cv::DMatch>& matches,
std::vector<cv::KeyPoint>& keypoints1,
std::vector<cv::KeyPoint>& keypoints2) {
// 1. 检测图像关键点
cv::Mat homography = match(target, image, matches, keypoints1, keypoints2);
std::vector<cv::Point2f> corners;
corners.push_back(cv::Point2f(0,0));
corners.push_back(cv::Point2f(target.cols-1,0));
corners.push_back(cv::Point2f(target.cols-1,target.rows-1));
corners.push_back(cv::Point2f(0,target.rows-1));
cv::perspectiveTransform(corners, detectedCorners, homography);
return homography;
}
(3) 通过 match
方法得到单应性后,定义目标的四个角(即其参考图像的四个角)。然后使用 cv::perspectiveTransform
函数将它们传递到其他图像,该函数将输入向量中的每个点乘以单应性矩阵可以得到在其他图像中这些点的坐标。然后执行目标匹配:
// 准备匹配器
TargetMatcher tmatcher(cv::FastFeatureDetector::create(10),cv::BRISK::create());
tmatcher.setNormType(cv::NORM_HAMMING);
// 定义输出数据
std::vector<cv::DMatch> matches;
std::vector<cv::KeyPoint> keypoints1, keypoints2;
std::vector<cv::Point2f> corners;
// 设置目标图像
tmatcher.setTarget(target);
// 使用目标匹配图像
tmatcher.detectTarget(image, corners, matches, keypoints1, keypoints2);
// 绘制目标角点
cv::Point pt= cv::Point(corners[0]);
cv::line(image,cv::Point(corners[0]), cv::Point(corners[1]), cv::Scalar(255,255,255),3);
cv::line(image,cv::Point(corners[1]), cv::Point(corners[2]), cv::Scalar(255,255,255),3);
cv::line(image,cv::Point(corners[2]), cv::Point(corners[3]), cv::Scalar(255,255,255),3);
cv::line(image,cv::Point(corners[3]), cv::Point(corners[0]), cv::Scalar(255,255,255),3);
使用 cv::drawMatches
函数,结果如下所示:
我们还可以利用单应性来修改平面对象的透视效果。例如,如果我们有几张从建筑物不同角度拍摄的图片,可以计算这些图像之间的单应性,并通过将图像围在一起组合拼接成立面图像。计算单应性至少需要两个视图之间的四个匹配点,cv::getPerspectiveTransform
函数可以根据四个对应点计算变换公式。
2.2 完整代码
头文件 (targetMatcher.h
) 完整代码如下所示:
#if !defined TMATCHER
#define TMATCHER
#define VERBOSE 1
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
class TargetMatcher {
private:
// 指向特征点检测器对象的指针
cv::Ptr<cv::FeatureDetector> detector;
// 指向特征描述符提取器对象的指针
cv::Ptr<cv::DescriptorExtractor> descriptor;
cv::Mat target; // 目标图像
int normType;
double distance; // 最小重投影误差
int numberOfLevels; // 金字塔尺寸
double scaleFactor; // 缩放因子
// 目标图像金字塔及其关键点
std::vector<cv::Mat> pyramid;
std::vector<std::vector<cv::KeyPoint> > pyrKeypoints;
std::vector<cv::Mat> pyrDescriptors;
// 创建目标图像金字塔
void createPyramid() {
pyramid.clear();
cv::Mat layer(target);
for (int i=0; i<numberOfLevels; i++) {
pyramid.push_back(target.clone());
resize(target, target, cv::Size(), scaleFactor, scaleFactor);
}
pyrKeypoints.clear();
pyrDescriptors.clear();
// 图像金字塔关键点检测和描述符
for (int i=0; i<numberOfLevels; i++) {
// 在第 i 层检测目标关键点
pyrKeypoints.push_back(std::vector<cv::KeyPoint>());
detector->detect(pyramid[i], pyrKeypoints[i]);
if (VERBOSE) {
std::cout << "Interest points: target=" << pyrKeypoints[i].size() << std::endl;
}
// 在第 i 层计算描述符
pyrDescriptors.push_back(cv::Mat());
descriptor->compute(pyramid[i], pyrKeypoints[i], pyrDescriptors[i]);
}
}
public:
TargetMatcher(const cv::Ptr<cv::FeatureDetector> &detector,
const cv::Ptr<cv::DescriptorExtractor> &descriptor = cv::Ptr<cv::DescriptorExtractor>(),
int numberOfLevels=8, double scaleFactor=0.9) :
detector(detector), descriptor(descriptor),
normType(cv::NORM_L2), distance(1.0),
numberOfLevels(numberOfLevels), scaleFactor(scaleFactor) {
if (!this->descriptor) {
this->descriptor = this->detector;
}
}
// 设置匹配时使用的归一化方法
void setNormType(int norm) {
normType = norm;
}
// 设置最小重投影距离
void setReprojectionDistance(double d) {
distance = d;
}
// 设置目标图像
void setTarget(const cv::Mat t) {
if (VERBOSE) cv::imshow("Target", t);
target = t;
createPyramid();
}
// 使用 RANSAC 标示较好匹配
cv::Mat ransacTest(const std::vector<cv::DMatch>& matches,
std::vector<cv::KeyPoint>& keypoints1,
std::vector<cv::KeyPoint>& keypoints2,
std::vector<cv::DMatch>& outMatches) {
// 将关键点转换为 Point2f
std::vector<cv::Point2f> points1, points2;
outMatches.clear();
for (std::vector<cv::DMatch>::const_iterator it= matches.begin();
it!= matches.end(); ++it) {
points1.push_back(keypoints1[it->queryIdx].pt);
points2.push_back(keypoints2[it->trainIdx].pt);
}
// 查找图像 1 和 图像 2 间的单应性
std::vector<uchar> inliers(points1.size(),0);
cv::Mat homography= cv::findHomography(
points1,points2, // 对应点
inliers, // 匹配状态
cv::RHO, // RHO 方法
distance); // 到重投影点的最大距离
std::vector<uchar>::const_iterator itIn= inliers.begin();
std::vector<cv::DMatch>::const_iterator itM= matches.begin();
for ( ;itIn!= inliers.end(); ++itIn, ++itM) {
if (*itIn) { // 有效匹配
outMatches.push_back(*itM);
}
}
return homography;
}
// 检测图像中的平面目标
cv::Mat detectTarget(const cv::Mat& image,
std::vector<cv::Point2f>& detectedCorners) {
// 1. 检测图像关键点
std::vector<cv::KeyPoint> keypoints;
detector->detect(image, keypoints);
if (VERBOSE)
std::cout << "Interest points: image=" << keypoints.size() << std::endl;
// 计算描述符
cv::Mat descriptors;
descriptor->compute(image, keypoints, descriptors);
std::vector<cv::DMatch> matches;
cv::Mat bestHomography;
cv::Size bestSize;
int maxInliers = 0;
cv::Mat homography;
// 构建匹配器
cv::BFMatcher matcher(normType);
// 2. 鲁棒地检测到的每一金字塔层的单应性
for (int i = 0; i < numberOfLevels; i++) {
// 查找目标和图像之间的 RANSAC 单应性
matches.clear();
// 匹配描述符
matcher.match(pyrDescriptors[i], descriptors, matches);
if (VERBOSE)
std::cout << "Number of matches (level " << i << ")=" << matches.size() << std::endl;
// 使用 RANSAC 验证匹配
std::vector<cv::DMatch> inliers;
homography = ransacTest(matches, pyrKeypoints[i], keypoints, inliers);
if (VERBOSE)
std::cout << "Number of inliers=" << inliers.size() << std::endl;
if (inliers.size() > maxInliers) {
maxInliers = inliers.size();
bestHomography = homography;
bestSize = pyramid[i].size();
}
if (VERBOSE) {
cv::Mat imageMatches;
cv::drawMatches(target, pyrKeypoints[i], // 第一张图向及其关键点
image, keypoints, // 第二张图像及其关键点
inliers, // 匹配
imageMatches, // 结果图像
cv::Scalar(255, 255, 255), // 线条颜色
cv::Scalar(255, 255, 255), // 关键点颜色
std::vector<char>(),
cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
cv::imshow("Target matches", imageMatches);
cv::waitKey();
}
}
// 3. 使用最佳单应性找到图像上的角点位置
if (maxInliers > 8) { // 有效评估
// 最佳尺寸的目标角点
std::vector<cv::Point2f> corners;
corners.push_back(cv::Point2f(0, 0));
corners.push_back(cv::Point2f(bestSize.width - 1, 0));
corners.push_back(cv::Point2f(bestSize.width - 1, bestSize.height - 1));
corners.push_back(cv::Point2f(0, bestSize.height - 1));
// 重新投射目标角点
cv::perspectiveTransform(corners, detectedCorners, bestHomography);
}
if (VERBOSE)
std::cout << "Best number of inliers=" << maxInliers << std::endl;
return bestHomography;
}
};
#endif
主函数文件 (fastCorners.cpp
) 完整代码如下所示:
#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/xfeatures2d.hpp>
#include "targetMatcher.h"
int main() {
// 读取输入图像
cv::Mat target= cv::imread("target.png",0);
cv::Mat image= cv::imread("book.png",0);
if (!target.data || !image.data)
return 0;
cv::namedWindow("Target");
cv::imshow("Target",target);
cv::namedWindow("Image");
cv::imshow("Image", image);
// 准备匹配器
TargetMatcher tmatcher(cv::FastFeatureDetector::create(10),cv::BRISK::create());
tmatcher.setNormType(cv::NORM_HAMMING);
// 定义输出数据
std::vector<cv::DMatch> matches;
std::vector<cv::KeyPoint> keypoints1, keypoints2;
std::vector<cv::Point2f> corners;
// 设置目标图像
tmatcher.setTarget(target);
// 使用目标匹配图像
tmatcher.detectTarget(image, corners);
// 绘制目标角点
if (corners.size()==4) {
cv::line(image, cv::Point(corners[0]), cv::Point(corners[1]), cv::Scalar(255, 255, 255), 3);
cv::line(image, cv::Point(corners[1]), cv::Point(corners[2]), cv::Scalar(255, 255, 255), 3);
cv::line(image, cv::Point(corners[2]), cv::Point(corners[3]), cv::Scalar(255, 255, 255), 3);
cv::line(image, cv::Point(corners[3]), cv::Point(corners[0]), cv::Scalar(255, 255, 255), 3);
}
cv::namedWindow("Target detection");
cv::imshow("Target detection",image);
cv::waitKey();
return 0;
}
小结
在计算机视觉中,平面的单应性被定义为从一个平面到另一个平面的投影映射,在多视图计算中,单应性具有重要用途,本节学习了使用 cv::findHomography
函数进行单应性估计,并利用单应性执行图像匹配任务。
系列链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像