C++ opencv形态学、轮廓查找、特征检测和图像分割
- 形态学基本处理方法
- 二值化
- 全局二值化
- 局部二值化
- 腐蚀和膨胀
- 图像形态学运算
- 开运算
- 闭运算
- 顶帽
- 黑帽
- 代码
- 图像轮廓
- 寻找轮廓
- 绘画轮廓
- 轮廓的面积和周长
- 多边形逼近和凸包
- 多边形逼近
- 凸包
- 外接矩形
- 最小外接矩形
- 最大外接矩形
- 案例
- 车辆检测(简易)
- 特征检测
- Harris角点检测:
- Shi-Tomasi角点检测
- SIFT特征点检测
- SIFT关键点
- 关键点描述子
- SURF特征点检测
- SURF关键点和描述子
- 继承cv::xfeatures2d::SURF纯抽象类
- 继承类的使用
- ORB实时特征检测
- ORB的关键点和描述子
- 特征点匹配
- FLANN 最快邻近区特征匹配方法
- 图像查找
- 案例图像拼接
- 获取单应性矩阵
- 图像拼接
- main函数
- 图像分割
- 传统的图像分割方法
- 分水岭法
- GrabCut法
- MeanShift法
- 背景扣除法
- 人脸识别
- Mat 矩阵
- 数据转换
- Vector 和 Mat 互换
- 数据类型转换
- 坐标点类型转换
形态学基本处理方法
基于图像形态进行处理的一些基本方法,这些处理方法基本是对二进制图像进行处理,卷积核决定着图像处理后的效果。
步骤:灰度图像->二值化->形态学运算
二值化
double threshold( InputArray src, OutputArray dst,
double thresh, double maxval, int type );
/* @param src 输入数组(多通道,8位或32位浮点)
@param dst 输出数组的大小和类型和通道数量与SRC相同.
@param thresh 阈值.
@param maxval 与#THRESH_BINARY和#THRESH_BINARY_INV阈值类型一起使用的最大值。
@param type 阈值类型 (see #ThresholdTypes).
@return 如果使用 Otsu's 或 Triangle方法,则为计算的阈值。
@sa 自适应阈值,寻找轮廓,比较,最小,最大*/
阈值类型
THRESH_BINARY = 0, //! 二值化(低于阈值变黑,高于阈值变为最大值)
THRESH_BINARY_INV = 1, //!反二值化(与之相反)
THRESH_TRUNC = 2, //! 大于阈值时变为阈值量,其他情况不变
THRESH_TOZERO = 3, //! 趋于零(仅地于阈值时变0,其他情况不变)
THRESH_TOZERO_INV = 4, //! 反趋于0(仅高于阈值时变0,其他情况不变)
THRESH_MASK = 7,
THRESH_OTSU = 8, //!< 使用Otsu algorithm 选择最优阈值
THRESH_TRIANGLE = 16 //!< 使用Triangle algorithm 选择最优阈值
全局二值化
cv::Mat thresholdImg;
// OTSU算法获取最优阈值
double optimal_threshold = cv::threshold(cartoonGray, thresholdImg, 0, 0, cv::THRESH_OTSU);
// 二值化处理
cv::threshold(cartoonGray, thresholdImg, optimal_threshold, 255, cv::THRESH_BINARY);
cv::imshow("cartoonGray", cartoonGray);
cv::imshow("thresholdImg", thresholdImg);
cv::waitKey(0);
局部二值化
void adaptiveThreshold( InputArray src, OutputArray dst,
double maxValue, int adaptiveMethod,
int thresholdType, int blockSize, double C );
@param src 源8位单通道图像。.
@param dst 目标图像的大小和类型与src相同.
@param maxValue 非零值赋给满足条件的像素
@param adaptiveMethod 要使用的自适应阈值算法,请参见#AdaptiveThresholdTypes。#BORDER_REPLICATE | #BORDER_ISOLATED 用于处理边界。
@param thresholdType 阈值类型,必须是其中之一 #THRESH_BINARY or #THRESH_BINARY_INV, see #ThresholdTypes.
@param blockSize 像素邻域的大小,用于计算像素的阈值:3、5、7等等。
@param C 常数减去平均值或加权平均值(详见下文)。通常,它是正的,但也可能是零或负的。
@sa threshold, blur, GaussianBlur
cv::Mat adaptiveThresholdImg;
// 自适应阈值二级化处理/局部二值化
cv::adaptiveThreshold(cartoonGray, adaptiveThresholdImg, 255,
cv::ADAPTIVE_THRESH_GAUSSIAN_C,
cv::THRESH_BINARY,3,0);
腐蚀和膨胀
腐蚀原理:锚点所在的像素点卷积核形状若没有全部覆盖白色区域,则变0(需要根据卷积核决定)。
膨胀原理:锚点所在的像素点卷积核形状若有一个接触到白色区域,则变1(需要根据卷积核决定)。
以下为腐蚀过程,传入一个5x5的全1卷积核,卷积核的锚点所重叠的像素点的颜色取决于,卷积核的形状覆盖全部白色区域时为1,反之则为0。最里面的虚线矩形为最终腐蚀后的图像。
cv::Mat erode_img, dilate_img;
// 获取卷积核(MORPH_CROSS十字形,RECT矩形,ELLIPSE椭圆形)
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_CROSS, cv::Size(3, 3));
// 腐蚀
cv::erode(adaptiveThresholdImg, erode_img, kernel);
// 膨胀
cv::dilate(adaptiveThresholdImg, dilate_img, kernel);
cv::imshow("erode_img", erode_img);
cv::imshow("dilate_img", dilate_img);
图像形态学运算
void morphologyEx( InputArray src, OutputArray dst,
int op, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
/* @param src 源图像。通道的数量可以是任意的。深度应为CV_8U, CV_16U, CV_16S, CV_32F or CV_64F.
@param dst 目标图像的大小和类型与源图像相同
@param op 形态操作的类型,请参见#MorphTypes
@param kernel 结构元素。可以使用#getStructuringElement.
@param anchor 与内核的锚定位置。负值表示锚点位于核中心
@param iterations 腐蚀和膨胀的次数。
@param borderType 像素外推方法,参见#BorderTypes。#BORDER_WRAP不支持。
@param borderValue 在边界不变的情况下的边界值。缺省值具有特殊含义。
@sa dilate, erode, getStructuringElement
@note 迭代次数是应用侵蚀或膨胀操作的次数。例如,具有两个迭代的打开操作(#MORPH_OPEN)等价于连续应用: erode -> erode -> dilate -> dilate (and not erode -> dilate -> erode -> dilate)。*/
开运算
先腐蚀再膨胀。作用是对背景去噪
闭运算
先膨胀再腐蚀。作用是对物件内部去噪
顶帽
原图 - 开运算。作用是提取背景的噪声
黑帽
原图 - 闭运算。作用是提取物件内部的噪声
代码
cv::Mat open_img, close_img, tophat_img, blackhat_img;
// 开运算
cv::morphologyEx(adaptiveThresholdImg, open_img, cv::MORPH_OPEN, kernel);
// 闭运算
cv::morphologyEx(adaptiveThresholdImg, close_img, cv::MORPH_CLOSE, kernel);
// 顶帽
cv::morphologyEx(adaptiveThresholdImg, tophat_img, cv::MORPH_TOPHAT, kernel);
// 黑帽
cv::morphologyEx(adaptiveThresholdImg, blackhat_img, cv::MORPH_BLACKHAT, kernel);
cv::imshow("srcImg", adaptiveThresholdImg);
cv::imshow("open_img", open_img);
cv::imshow("close_img", close_img);
cv::imshow("tophat_img", tophat_img);
cv::imshow("blackhat_img", blackhat_img);
cv::waitKey(0);
图像轮廓
轮廓:具有相同颜色和强度连续点的线条
作用:图形分析、物体的识别和检测
寻找轮廓
void findContours( InputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode,
int method, Point offset = Point());
/*@param image 一个8位单通道图像。非零像素被视为1。零像素保持为0,因此图像被视为二值。你可以使用#compare,#inRange, #threshold ,#adaptive #Threshold, #Canny,等,以创建灰度或彩色的二值图像。如果mode等于#RETR_CCOMP或#RETR_FLOODFILL,则输入也可以是标签的32位整数图像(CV_32SC1)。
@param contours 检测到的轮廓。每个轮廓都存储为点的向量(e.g.
std::vector<std::vector<cv::Point> >).
@param hierarchy Optional output vector (e.g. std::vector<cv::Vec4i>), 包含映像拓扑信息。它的元素和等高线的数量一样多. 对于每i个轮廓 contours[i], 元素 hierarchy[i][0] , hierarchy[i][1] , hierarchy[i][2] , 和 hierarchy[i][3] 分别设置为同一层次级别下一个和上一个轮廓、第一个子轮廓和父轮廓的轮廓的基于0的指数。如果对于轮廓i没有下一个、上一个、父或嵌套的轮廓,则hierarchy[i] 对应的元素是负的。
@note 在Python中,层次结构嵌套在顶层数组中。使用hierarchy[0][i]访问第i个等高线的层次元素。
@param mode 轮廓检索模式,参见#RetrievalModes
@param method 轮廓逼近法,参见#ContourApproximationModes
@param offset 可选偏移量,每个等高线点通过该偏移量进行偏移。如果从图像ROI中提取轮廓,然后在整个图像上下文中进行分析,那么这是很有用的。 */
#RetrievalModes
/** 只检索极端的外部轮廓。它为所有轮廓设置' hierarchy[i][2]=hierarchy[i][3]=-1 '。 */
RETR_EXTERNAL = 0,
/** 检索所有轮廓,而不建立任何层次关系 */
RETR_LIST = 1,
/** 检索所有轮廓并将它们组织到一个两级层次结构中。在顶层,有组件的外部边界。在第二层,有洞的边界。如果连接组件的孔内有另一个轮廓线,它仍然放在顶层。*/
RETR_CCOMP = 2,
/** 检索所有轮廓并重构嵌套轮廓的完整层次结构。*/
RETR_TREE = 3,
RETR_FLOODFILL = 4 //!<
#ContourApproximationModes
/** 存储了所有的等高线点。也就是说,轮廓线的任意2个后续点(x1,y1)和(x2,y2)要么是水平的,要么是垂直的,要么是对角线,即max(abs(x1-x2),abs(y2-y1))==1。*/
CHAIN_APPROX_NONE = 1,
/** 压缩水平段、垂直段和对角线段,只留下它们的端点。例如,一个由上至右的矩形轮廓用4个点编码。 */
CHAIN_APPROX_SIMPLE = 2,
/** 应用Teh-Chin链近似算法的一种风格@cite TehChin89*/
CHAIN_APPROX_TC89_L1 = 3,
/** 应用Teh-Chin链近似算法的一种风格@cite TehChin89*/
CHAIN_APPROX_TC89_KCOS = 4
绘画轮廓
void drawContours( InputOutputArray image, InputArrayOfArrays contours,
int contourIdx, const Scalar& color,
int thickness = 1, int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX, Point offset = Point() );
/*@param image 目标图像.
@param contours 所有的输入轮廓。每个轮廓被存储为一个点向量。
@param contourIdx 指示要绘制的轮廓的参数。如果它是负的,就画出所有的等高线。
@param color 轮廓的颜色.
@param thickness 绘制等高线的线条粗细。如果为负值(例如,thickness=#FILLED),则绘制内部轮廓。
@param lineType 线连接度. 参见 #LineTypes
@param hierarchy 关于层次结构的可选信息。只有当你只想画一些等高线时才需要 (see maxLevel ).
@param maxLevel 绘制等高线的最大水平。如果为0,则只绘制指定的轮廓.
如果为1,则该函数绘制轮廓线和所有嵌套的轮廓线。如果是2,则表示函数
绘制等高线、所有嵌套等高线、所有嵌套到嵌套等高线,等等。 只有当存在可用的层次结构时,才会考虑此参数。
@param offset 轮廓移位参数。移动所有绘制的等高线的指定 \f$\texttt{offset}=(dx,dy)\f$ .
@note 当thickness=#FILLED时,该函数被设计为正确处理带有孔的连接组件,即使没有提供层次结构数据。 这是通过使用奇偶法则分析所有的轮廓来完成的。如果你有一个单独检索的轮廓的联合集合,这可能会给出不正确的结果。为了解决这个问题,您需要为每个等高线子组分别调用#drawContours,或者使用contourIdx参数遍历集合。*/
cv::Mat img_gray,thresholdImg;
// 转灰度图像
cv::cvtColor(img, img_gray, cv::COLOR_BGR2GRAY);
// 二值化
cv::threshold(img_gray, thresholdImg, 180, 255, cv::THRESH_BINARY);
// 储存轮廓坐标的数组
std::vector<std::vector<cv::Point>> ContoursPointArray;
// 寻找轮廓
// RETR_CCOMP 存储顺序是最里面的为顶层,先到左边顶层由外到内编号,再到左边顶层由外到内编号;接着到第二层,先到左边第二层由外到内编号,再到左边第二层由外到内编号;由此类推。
// RETR_TREE 存储顺序是先右边由外向内,再往左边由外向内,依次编号(较常用)
// RETR_LIST 存储顺序是先找右边顶层的内框,再找左边顶层的内框,接着右外,再到左外;下一层亦是如此。
cv::findContours(thresholdImg, ContoursPointArray, cv::RETR_TREE, cv::CHAIN_APPROX_TC89_KCOS);
//std::cout << ContoursPointArray[0];
// 绘画轮廓
cv::drawContours(img, ContoursPointArray, -1, { 0,0,255 }, 1,16);
cv::imshow("img_gray", img_gray);
cv::imshow("src", img);
cv::imshow("thresholdImg", thresholdImg);
cv::waitKey(0);
RETR_CCOMP 存储顺序是最里面的为顶层,先到左边顶层由外到内编号,再到左边顶层由外到内编号;接着到第二层,先到左边第二层由外到内编号,再到左边第二层由外到内编号;由此类推。
RETR_TREE 存储顺序是先右边由外向内,再往左边由外向内,依次编号(较常用)
RETR_LIST 存储顺序是先找右边顶层的内框,再找左边顶层的内框,接着右外,再到左外;下一层亦是如此。
轮廓的面积和周长
/*@param contour 二维点(轮廓顶点)的输入向量,存储在std::vector或Mat中
@param oriented 定向区域标志。如果为true,函数将返回一个带符号的面积值,该值取决于轮廓方向(顺时针或逆时针)。使用这个特性,您可以通过取一个区域的符号来确定轮廓的方向。缺省情况下,该参数为false,即返回绝对值。*/
double contourArea( InputArray contour, bool oriented = false );
/*@param curve 二维点(轮廓顶点)的输入向量,存储在std::vector或Mat中
@param closed 指示曲线是否关闭的标志。*/
double arcLength( InputArray curve, bool closed );
多边形逼近和凸包
多边形逼近
void approxPolyDP( InputArray curve,
OutputArray approxCurve,
double epsilon, bool closed );
/*@param curve 二维点的输入向量存储在std::vector或Mat中
@param approxCurve 近似的结果。类型应该与输入曲线的类型相匹配.
@param epsilon 指定近似精度的参数。这是原始曲线与其近似值之间的最大距离。
@param closed 如果为真,则近似曲线是闭合的(它的第一个顶点和最后一个顶点是相连的)。否则,它没有关闭。*/
cv::Mat hand = cv::imread("C:\\Users\\10358\\Desktop\\img\\hand.jpg");
cv::Mat hand_gray, handThresholdImg;
// 转灰度图像
cv::cvtColor(hand, hand_gray, cv::COLOR_BGR2GRAY);
// 二值化 (阈值要恰当)
cv::threshold(hand_gray, handThresholdImg, 230, 255, cv::THRESH_BINARY_INV);
std::vector<std::vector<cv::Point>> handContoursPointArray;
std::vector<cv::Point> approxArray;
// 寻找轮廓
cv::findContours(handThresholdImg, handContoursPointArray,cv::RETR_TREE,cv::CHAIN_APPROX_SIMPLE);
// 多边形逼近
cv::approxPolyDP(handContoursPointArray[0], approxArray, 10, true);
// 绘画轮廓
cv::polylines(hand, approxArray, true, { 0,0,255 }, 3);
cv::imshow("handImg", hand);
cv::imshow("handThresholdImg", handThresholdImg);
cv::waitKey(0);
凸包
void convexHull( InputArray points, OutputArray hull,
bool clockwise = false, bool returnPoints = true );
/*@param points 输入2D点集,存储在std::vector或Mat中。
@param hull 输出凸包。它要么是指数的整数向量,要么是点的整数向量。在第一种情况下,包体元素是原始数组中凸包点的基于0的索引(因为凸包点集是原始点集的子集)。第二种情况,hull元素就是hull点本身。
@param clockwise 定位标志。如果为真,则输出凸包为顺时针方向。否则,它是逆时针方向的。假设的坐标系X轴指向右,Y轴指向上。
@param returnPoints 操作标记。对于矩阵,当标志为真时,函数返回凸包点。否则,它返回凸包点的索引。 当输出数组为 std::vector, 该标志被忽略。输出取决于向量的类型:std::vector<int>表示returnPoints=false, std::vector<Point>表示returnPoints=true。
@note ' points '和' hull '应该是不同的数组,不支持就地处理*/
cv::Mat hand = cv::imread("C:\\Users\\10358\\Desktop\\img\\hand.jpg");
cv::Mat hand_gray, handThresholdImg;
// 转灰度图像
cv::cvtColor(hand, hand_gray, cv::COLOR_BGR2GRAY);
// 二值化 (阈值要恰当)
cv::threshold(hand_gray, handThresholdImg, 230, 255, cv::THRESH_BINARY_INV);
std::vector<std::vector<cv::Point>> handContoursPointArray;
std::vector<cv::Point> approxArray, convexHullArray;
// 寻找轮廓
cv::findContours(handThresholdImg, handContoursPointArray,cv::RETR_TREE,cv::CHAIN_APPROX_SIMPLE);
// 多边形逼近
cv::approxPolyDP(handContoursPointArray[0], approxArray, 10, true);
// 绘画多边形逼近轮廓
cv::polylines(hand, approxArray, true, { 0,0,255 }, 3);
// 凸包
cv::convexHull(handContoursPointArray[0], convexHullArray);
// 绘画凸包轮廓
cv::polylines(hand, convexHullArray, true, { 0,0,255 }, 3);
cv::imshow("handImg", hand);
cv::imshow("handThresholdImg", handThresholdImg);
cv::waitKey(0);
外接矩形
最小外接矩形
cv::Mat img_gray,thresholdImg;
// 转灰度图像
cv::cvtColor(img, img_gray, cv::COLOR_BGR2GRAY);
// 二值化
cv::threshold(img_gray, thresholdImg, 180, 255, cv::THRESH_BINARY);
std::vector<std::vector<cv::Point>> ContoursPointArray;
// 寻找轮廓
cv::findContours(thresholdImg, ContoursPointArray, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// 最小外接矩阵
cv::RotatedRect min_rect = cv::minAreaRect(ContoursPointArray[0]);
//cv::boxPoints(min_rect, boxArray);
// 绘制圆心
cv::circle(img, cv::Point(min_rect.center.x, min_rect.center.y), 5, cv::Scalar(0, 255, 255), -1);
cv::Point2f rect[4];
// 存储点集
min_rect.points(rect);
for (int j = 0; j < 4; j++) {
line(img, rect[j], rect[(j + 1) % 4], cv::Scalar(0, 0, 255), 2, 8); //绘制最小外接矩形每条边
}
cv::imshow("src", img);
cv::imshow("thresholdImg", thresholdImg);
最大外接矩形
cv::Mat img_gray,thresholdImg;
// 转灰度图像
cv::cvtColor(img, img_gray, cv::COLOR_BGR2GRAY);
// 二值化
cv::threshold(img_gray, thresholdImg, 180, 255, cv::THRESH_BINARY);
std::vector<std::vector<cv::Point>> ContoursPointArray;
// 寻找轮廓
cv::findContours(thresholdImg, ContoursPointArray, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// 最大外接矩阵
cv::Rect max_rect = cv::boundingRect(ContoursPointArray[0]);
cv::rectangle(img, max_rect, cv::Scalar(0, 255, 255), 1, 16);
cv::imshow("src", img);
cv::imshow("thresholdImg", thresholdImg);
案例
车辆检测(简易)
// 检测线高度
int lineHight = 550;
// 检测偏移量
int offSet = 4;
// 车辆统计
int count = 0;
int ESC = 27;
cv::VideoCapture video = cv::VideoCapture("C:\\Download\\video\\videovideo.mp4");
cv::Mat frame, frameGray, frameThreshold, GausBlur,mask,erode_mask, dilate_mask, close_mask;
// 创建背景
cv::Ptr<cv::BackgroundSubtractorMOG2> bgsubmog2 = cv::createBackgroundSubtractorMOG2();
// 卷积核
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Point(7, 7));
// 存储视频对象
//cv::VideoWriter vw = cv::VideoWriter("C:\\Download\\video\\img.avi", cv::VideoWriter::fourcc('M', 'J', 'P', 'G'), 25, cv::Size(1280, 720));
//cv::VideoWriter vw = cv::VideoWriter("C:\\Download\\video\\car.mp4", cv::VideoWriter::fourcc('m', 'p', '4', 'v'), 25, cv::Size(1280, 720));
std::vector<cv::Point> cars = {};
while (true)
{
video.read(frame);
if (frame.empty()) {
break;
}
// 灰度
cv::cvtColor(frame, frameGray, cv::COLOR_RGB2GRAY);
//double optimal_threshold = cv::threshold(frameGray, frameThreshold, 0, 0, cv::THRESH_OTSU);
//cv::threshold(frameGray, frameThreshold, 80, 255, cv::THRESH_BINARY);
// 去噪
cv::GaussianBlur(frameGray, GausBlur, cv::Size(3, 3), 10);
// 去背景
bgsubmog2->apply(GausBlur, mask);
// 腐蚀
cv::erode(mask, erode_mask, kernel);
// 膨胀
cv::dilate(erode_mask, dilate_mask, kernel, cv::Point(-1, -1),3);
// 闭运算 祛除物体内噪点
cv::morphologyEx(dilate_mask, close_mask, cv::MORPH_CLOSE, kernel);
cv::morphologyEx(close_mask, close_mask, cv::MORPH_CLOSE, kernel);
// 寻找轮廓
std::vector<std::vector<cv::Point>> carCts;
cv::findContours(close_mask, carCts, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// frame[1336 x 754]
cv::line(frame, cv::Point(10, lineHight), cv::Point(1300, lineHight), cv::Scalar(255, 255, 0), 3, 16);
for (size_t i = 0; i < carCts.size(); i++)
{
// 最大外接矩形
cv::Rect max_rect = cv::boundingRect(carCts[i]);
// 绘画目标矩形
if (max_rect.width > 110 && max_rect.height > 110)
{
cv::rectangle(frame, max_rect, cv::Scalar(0, 0, 255), 1, 16);
// 将目标车辆的中心点加入数组
cv::Point cPoint((int)(max_rect.x + max_rect.width / 2), (int)(max_rect.y + max_rect.height / 2));
cars.push_back(cPoint);
}
// 检测通过车辆
for (cv::Point point : cars) {
if (point.y < lineHight + offSet && point.y > lineHight - offSet) {
count += 1;
cars.clear();
}
}
}
cv::putText(frame, "Cars Count: " + std::to_string(count), cv::Point(600, 60), cv::FONT_HERSHEY_SIMPLEX, 2, cv::Scalar(255, 255, 0), 3, 16);
// 写入帧
/*cv::resize(frame, frame, cv::Size(1280, 720));
vw.write(frame);*/
cv::imshow("close_mask", close_mask);
cv::imshow("frame", frame);
if (cv::waitKey(1) == ESC) {
break;
}
}
video.release();
//vw.release();
cv::destroyAllWindows();
特征检测
图像特征就是指有意义的图像区域具有独特性、易于识别性,比如角点、斑点以及高密度区。
角点:在特征中最重要的是角点,灰度梯度的最大值对应的像素,两条线的交点,极值点(一阶导数最大值,但二阶导数为0)
Harris角点检测:
光滑地区,无论向哪里移动,衡量系数不变。
边缘地址,垂直边缘移动时,衡量系统变化具烈。
在交点处,往那个方向移动,衡量系统都变化具烈。
void cornerHarris( InputArray src, OutputArray dst, int blockSize,
int ksize, double k,
int borderType = BORDER_DEFAULT );
/*
@param src 输入单通道8位或浮点图像。
@param dst 存储Harris检测器响应的图像。它的类型为CV_32FC1,大小与src相同。
@param blockSize 邻域大小,即扫描窗口的大小。
@param ksize Sobel算子的孔径参数。
@param k Harris探测器自由参数。参见上面的公式。
@param borderType 像素外推法。看到#BorderTypes。不支持#BORDER_WRAP。
*/
// 灰度图像
cv::cvtColor(img, img_gray, cv::COLOR_BGR2GRAY);
// harris角点检测
cv::cornerHarris(img_gray, harris_img, 2, 3, 0.04);
// 获取mat最大值最小值及其坐标
double minVal, maxVal;
cv::Point minPoint, maxPoint;
cv::minMaxLoc(harris_img, &minVal, &maxVal, &minPoint, &maxPoint);
//std::cout << "minVal" << minVal << "maxVal" << maxVal;
// 找出角点区域(BGR)
for (size_t i = 0; i < img.rows; i++)
{
uchar* tmp = img.ptr<uchar>(i);
float_t* tmp2 = harris_img.ptr<float_t>(i);
for (size_t j = 0, z=0; j < (size_t)img.cols*3; z++,j+=3)
{
if ((float_t)tmp2[z] > maxVal*0.01)
{
//std::cout << "tmp2[z]" << (float_t)tmp2[z] << "maxVal" << maxVal;
//tmp[j] = 255;
//tmp[j+1]= 0;
tmp[j+2] = 255;
}
}
}
//找出角点区域(二值图)
/*double threshold = 0.0001;
cv::threshold(harris_img, harris_img,
threshold, 255, cv::THRESH_BINARY_INV);*/
cv::imshow("img", img);
cv::imshow("img_gray", img_gray);
cv::waitKey(0);
Shi-Tomasi角点检测
Shi-Tomasi是基于Harris角点检测所改进的。Harris 角点检测算的稳定性和k有关,而k是个经验值,不好设定最佳值。
void goodFeaturesToTrack( InputArray image, OutputArray corners,
int maxCorners, double qualityLevel, double minDistance,
InputArray mask = noArray(), int blockSize = 3,
bool useHarrisDetector = false, double k = 0.04 );
/*
@param image 输入8位或浮点32位,单通道图像。
@param corners 检测角的输出向量。
@param maxCorners 返回的最大拐角数。如果找到的角比找到的多,就把其中最强的那个退回去。' maxCorners <= 0 '表示未设置最大值限制,并返回所有检测到的角。
@param qualityLevel 表征图像角的最小可接受质量的参数。参数值乘以最佳角质量度量值,即最小特征值(见#cornerMinEigenVal)或Harris函数响应(见#cornerHarris)。质量指标低于产品质量的拐角被拒收。例如,如果最佳角的质量度量值=1500,而qualityLevel=0.01,那么所有质量度量值小于15的角都将被拒绝。
@param minDistance 返回角之间的最小可能欧氏距离。
@param mask 可选的兴趣区域。如果图像不是空的(它需要有类型CV_8UC1并且与图像大小相同),它指定检测角的区域。
@param blockSize 在每个像素邻域上计算导数协变矩阵的平均块的大小。 See cornerEigenValsAndVecs
@param useHarrisDetector 是否使用Harris探测器的参数(see #cornerHarris)or #cornerMinEigenVal
@param k Harris探测器的自由参数。
*/
// 灰度图像
cv::cvtColor(img, img_gray, cv::COLOR_BGR2GRAY);
// Shi-Tomasi角点检测
std::vector<cv::Point> Tomasi_array;
cv::goodFeaturesToTrack(img_gray, Tomasi_array, 1000, 0.01, 10);
for (cv::Point point: Tomasi_array)
{
cv::circle(img, point, 2, cv::Scalar(0, 0, 255), -1);
}
cv::imshow("img", img);
cv::imshow("img_gray", img_gray);
cv::waitKey(0);
SIFT特征点检测
关键点:位置,大小和方向
关键点描述子:记录了关键点周围对其有贡献的像素点的一组向量值,其不受仿射变换、光照变换等影响
优点:关键点检测准确和描述子十分详细
缺点:检测速度慢
SIFT关键点
// SIFT特征点检测
cv::Ptr<cv::SIFT> sift = cv::SIFT().create(); // 创建SIFT
std::vector<cv::KeyPoint> siftArray;
// 获取关键点
// detect(单通道图像,KeyPoint数组)
sift->detect(img_gray, siftArray);
// 绘画关键点
// drawKeypoints(单通道图像,KeyPoint数组,输出图像)
cv::drawKeypoints(img_gray, siftArray, img);
cv::imshow("img", img);
cv::imshow("img_gray", img_gray);
cv::waitKey(0);
关键点描述子
// SIFT特征点检测
//cv::Ptr<cv::SIFT> sift = cv::SIFT().create(); // 实例后创建SIFT
cv::Ptr<cv::SIFT> sift = cv::SIFT::create(); // 创建SIFT
std::vector<cv::KeyPoint> siftArray;
cv::Mat descriptorsArray;
// 获取关键点和描述子
// detectAndCompute(单通道图像,mask,KeyPoint数组, Mat数组)
sift->detectAndCompute(img_gray, cv::Mat(),siftArray, descriptorsArray);
// 第一个关键点的描述子
std::cout << descriptorsArray(cv::Range(0,1),cv::Range(0,descriptorsArray.cols));
// 绘画关键点
// drawKeypoints(单通道图像,KeyPoint数组,输出图像)
cv::drawKeypoints(img_gray, siftArray, img);
cv::imshow("img", img);
cv::imshow("img_gray", img_gray);
SURF特征点检测
优点:检测速度得到了优化
缺点:准确性没SIFT高
SURF关键点和描述子
// SURF特征点检测
cv::Ptr<cv::xfeatures2d::SURF> surf = cv::xfeatures2d::SURF::create();
std::vector<cv::KeyPoint> surfArray;
cv::Mat descriptorsArray;
// 获取关键点和描述子
// detectAndCompute(单通道图像,mask,KeyPoint数组, Mat数组)
surf->detectAndCompute(img_gray, cv::Mat(), surfArray, descriptorsArray);
// 绘画关键点
// drawKeypoints(单通道图像,KeyPoint数组,输出图像)
cv::drawKeypoints(img_gray, surfArray, img);
cv::imshow("img", img);
cv::imshow("img_gray", img_gray);
cv::waitKey(0);
继承cv::xfeatures2d::SURF纯抽象类
抽象类: 成员函数中有纯虚函数,这种类叫抽象类,抽象类不能实例化 (不能创建对象)。 抽象类必须被继承且纯虚函数被覆盖后,由子类实例化对象。 如果继承抽象类,但没有覆盖纯虚函数,那么子类也将称为抽象类,不能实例化。
纯抽象类: 所有成员函数都是纯虚函数,这种只能被继承的类叫纯抽象类。 这种类一般用来设计接口,这种类在子类被替换后不需要被修改,或少量的修改即可继续使用。
class MySURF: public cv::xfeatures2d::SURF
{
public:
MySURF() {
hessianThreshold = 0;
nOctaves = 0;
nOctaveLayers = 0;
extended = 0;
upright = 0;
};
protected:
virtual void setHessianThreshold(double hessianThreshold) {
this->hessianThreshold = hessianThreshold;
};
virtual double getHessianThreshold() const {
return this->hessianThreshold;
};
virtual void setNOctaves(int nOctaves) {
this->nOctaves = nOctaves;
};
virtual int getNOctaves() const{
return this->nOctaves;
};
virtual void setNOctaveLayers(int nOctaveLayers) {
this->nOctaveLayers = nOctaveLayers;
};
virtual int getNOctaveLayers() const {
return this->nOctaveLayers;
};
virtual void setExtended(bool extended) {
this->extended = extended;
};
virtual bool getExtended() const {
return this->extended;
};
virtual void setUpright(bool upright) {
this->upright = upright;
};
virtual bool getUpright() const {
return upright;
};
private:
double hessianThreshold;
int nOctaves, nOctaveLayers;
bool extended, upright;
};
继承类的使用
// SURF特征点检测
cv::Ptr<cv::xfeatures2d::SURF> surf = MySURF().create();
std::vector<cv::KeyPoint> surfArray;
cv::Mat descriptorsArray;
// 获取关键点和描述子
// detectAndCompute(单通道图像,mask,KeyPoint数组, Mat数组)
surf->detectAndCompute(img_gray, cv::Mat(), surfArray, descriptorsArray);
// 绘画关键点
// drawKeypoints(单通道图像,KeyPoint数组,输出图像)
cv::drawKeypoints(img_gray, surfArray, img);
cv::imshow("img", img);
cv::imshow("img_gray", img_gray);
cv::waitKey(0);
ORB实时特征检测
ORB = Oriented FAST + Rotated BRIEF
优点:可以做到实时检测
缺点:准确性不如SURF和SIFT
ORB的关键点和描述子
// ORB实时特征点检测
cv::Ptr<cv::ORB> orb = cv::ORB::create();
std::vector<cv::KeyPoint> ORBArray;
cv::Mat descriptorsArray;
// 获取关键点和描述子
// detectAndCompute(单通道图像,mask,KeyPoint数组, Mat数组)
orb->detectAndCompute(img_gray, cv::Mat(), ORBArray, descriptorsArray);
// 绘画关键点
// drawKeypoints(单通道图像,KeyPoint数组,输出图像)
cv::drawKeypoints(img_gray, ORBArray, img);
cv::imshow("img", img);
cv::imshow("img_gray", img_gray);
cv::waitKey(0);
特征点匹配
void match( InputArray queryDescriptors, InputArray trainDescriptors,
CV_OUT std::vector<DMatch>& matches, InputArray mask=noArray() ) const;
/* @param queryDescriptors 描述子的查询集。
@param trainDescriptors 描述子的训练集。 这个集合不会被添加到存储在类对象中的描述子的训练集合中.
@param matches 匹配结果集. 如果描述子的查询集在mask中被屏蔽,则不为该描述子添加匹配。因此,匹配结果集大小可能小于描述子的查询集计数。
@param mask 可以指定在输入的查询集和描述子的训练集之间允许的匹配
*/
void drawMatches( InputArray img1, const std::vector<KeyPoint>& keypoints1,
InputArray img2, const std::vector<KeyPoint>& keypoints2,
const std::vector<DMatch>& matches1to2, InputOutputArray outImg,
const Scalar& matchColor=Scalar::all(-1), const Scalar& singlePointColor=Scalar::all(-1),
const std::vector<char>& matchesMask=std::vector<char>(), DrawMatchesFlags flags=DrawMatchesFlags::DEFAULT );
/*
@param img1 第一张源图像.
@param keypoints1 第一个源图像的关键点.
@param img2 第二源图像.
@param keypoints2 第二个源图像的关键点.
@param matches1to2 从第一张图像匹配到第二张图像,这意味着keypoints1[i]在keypoints2[Matches [i]]中有一个对应的点。
@param outImg 输出图像。它的内容取决于定义在输出图像中绘制内容的flags值。请参阅下面可能的flags值。
@param matchColor 匹配的颜色(线和连接的关键点)。 如果matchColor==Scalar::all(-1), 颜色是随机生成的。
@param singlePointColor 单关键点的颜色 (圆圈), 这意味着关键点没有匹配. 如果singlePointColor==Scalar::all(-1) , 颜色是随机生成的。
@param matchesMask 掩码。决定绘制哪些匹配. 如果掩码为空,则绘制所有匹配项。
@param flags 标志设置绘图功能。可能的标志位值由DrawMatchesFlags定义。
这个函数从输出图像中的两个图像中提取关键点的匹配。匹配是连接两个关键点(圆)的一条线。参考cv::DrawMatchesFlags。
*/
cv::Mat orig_gray, search_gray,result_img;
// 灰度化
cv::cvtColor(orig_img, orig_gray, cv::COLOR_BGR2GRAY);
cv::cvtColor(search_img, search_gray, cv::COLOR_BGR2GRAY);
// 创建SIFT
cv::Ptr<cv::SIFT> sift = cv::SIFT::create();
std::vector<cv::KeyPoint> origImgSiftArray, searchImgSiftArray;
cv::Mat origImgDescriptorsArray, searchImgDescriptorsArray;
// 获取两张图的关键点和描述子
sift->detectAndCompute(orig_gray, cv::Mat(), origImgSiftArray, origImgDescriptorsArray);
sift->detectAndCompute(search_gray, cv::Mat(), searchImgSiftArray, searchImgDescriptorsArray);
// BF特征点匹配
cv::BFMatcher bf = cv::BFMatcher();
std::vector<cv::DMatch> matchArray;
// 获取每个描述子最佳的匹配向量
bf.match(origImgDescriptorsArray, searchImgDescriptorsArray, matchArray);
// 绘画匹配集
cv::drawMatches(orig_img, origImgSiftArray, search_img, searchImgSiftArray, matchArray, result_img);
cv::imshow("result_img", result_img);
cv::waitKey(0);
FLANN 最快邻近区特征匹配方法
速度快,精确差
void knnMatch( InputArray queryDescriptors, InputArray trainDescriptors,
CV_OUT std::vector<std::vector<DMatch> >& matches, int k,
InputArray mask=noArray(), bool compactResult=false ) const;
/** @brief 从查询集中为每个描述子查找k个最佳匹配。
@param queryDescriptors 描述子的查询集.
@param trainDescriptors 描述子的训练集. 这个集合不会被添加到存储在类对象中的描述子的训练集合中。
@param mask Mask 可以指定在输入的查询集和描述子的训练集之间允许的匹配
@param matches Matches. 对于同一个查询描述子,每个Matches[i]是k个或更少的匹配。
@param k 每个查询描述子找到的最佳匹配的计数,如果查询描述符的可能匹配总数少于k,则更少。
@param compactResult 当mask(或多个mask)不为空时使用的参数。如果compactResult为false,则匹配向量的大小与queryDescriptors行相同。如果compactResult为true,则匹配向量不包含完全屏蔽查询描述符的匹配。
这些DescriptorMatcher::match方法的扩展变体为每个查询描述符找到几个最佳匹配。匹配按距离递增顺序返回。有关查询和训练描述符的详细信息,请参见DescriptorMatcher::match
*/
cv::Mat orig_gray, search_gray,result_img;
// 灰度化
cv::cvtColor(orig_img, orig_gray, cv::COLOR_BGR2GRAY);
cv::cvtColor(search_img, search_gray, cv::COLOR_BGR2GRAY);
// 创建SIFT
cv::Ptr<cv::SIFT> sift = cv::SIFT::create();
std::vector<cv::KeyPoint> origImgSiftArray, searchImgSiftArray;
cv::Mat origImgDescriptorsArray, searchImgDescriptorsArray;
// 获取两张图的关键点和描述子
sift->detectAndCompute(orig_gray, cv::Mat(), origImgSiftArray, origImgDescriptorsArray);
sift->detectAndCompute(search_gray, cv::Mat(), searchImgSiftArray, searchImgDescriptorsArray);
std::vector<std::vector<cv::DMatch>> matches;
std::vector<cv::DMatch> goodmatch;
cv::FlannBasedMatcher flann = cv::FlannBasedMatcher();
// knnMatch 返回k个最匹配的向量
flann.knnMatch(origImgDescriptorsArray, searchImgDescriptorsArray, matches, 2);
// 过滤,寻找较好的匹配向量
for (size_t i = 0; i < matches.size(); i++)
{
if (matches[i][0].distance < 0.7 * matches[i][1].distance) {
goodmatch.push_back(matches[i][0]);
}
}
// 绘画匹配集
cv::drawMatches(orig_img, origImgSiftArray, search_img, searchImgSiftArray, goodmatch, result_img);
cv::imshow("result_img", result_img);
cv::waitKey(0);
图像查找
图像查找 = 特征匹配 + 单应性矩阵
单应性矩阵的作用:矫正图片, 透视变换的变换矩阵
Mat findHomography( InputArray srcPoints, InputArray dstPoints,
int method = 0, double ransacReprojThreshold = 3,
OutputArray mask=noArray(), const int maxIters = 2000,
const double confidence = 0.995);
/* @brief 查找两个平面之间的透视变换。
@param srcPoints 原始平面中各点的坐标,类型为CV_32FC2 or vector<Point2f>的矩阵。
@param dstPoints 原始平面中各点在目标平面上的坐标, 类型为CV_32FC2 or vector<Point2f>的矩阵。
@param method 用于计算单应矩阵的方法。有以下几种方法:
- **0** - 一种使用所有点的常规方法,即最小二乘法
- @ref RANSAC - RANSAC-基于鲁棒的方法
- @ref LMEDS - Least-中位数鲁棒法
- @ref RHO - PROSAC-基于鲁棒的方法
@param ransacReprojThreshold 将点对视为内嵌函数时允许的最大重投影误差(仅在RANSAC和RHO方法中使用)。
@param mask 可选的输出掩码由鲁棒的方法设置( RANSAC or LMeDS ). 注意,输入掩码值将被忽略
@param maxIters RANSAC迭代的最大次数。
@param confidence 置信度,在0到1之间。
*/
CV_EXPORTS_W void perspectiveTransform(InputArray src, OutputArray dst, InputArray m );
/*
@brief 执行向量的透视矩阵变换
函数cv::perspective tivetransform通过将src的每个元素作为2D或3D向量进行转换,即在搜索图中找到原图的坐标点。
@note 函数转换一个稀疏的2D或3D向量集合。如果你想使用透视转换来转换图像,请使用arpPerspective . 如果你有一个逆问题,也就是说,你想从几对对应的点中计算最可能的透视转换,你可以使用getPerspectiveTransform or findHomography.
@param src 输入双通道或三通道浮点数组;每一个元素是要转换的2D/3D向量。
@param dst 输出数组的大小和类型与src相同。
@param m 3x3或4x4浮点变换矩阵。
@sa transform, warpPerspective, getPerspectiveTransform, findHomography
*/
void warpPerspective( InputArray src, OutputArray dst,
InputArray M, Size dsize,
int flags = INTER_LINEAR,
int borderMode = BORDER_CONSTANT,
const Scalar& borderValue = Scalar());
/*
@brief 将透视转换应用于图像。warpPerspective函数使用指定的矩阵转换源图像:
@param src 输入图像
@param dst 输出图像,大小为dsize,类型与src相同
@param M 变换矩阵
@param dsize 输出图像的大小。
@param flags 插值方法的组合(#INTER_LINEAR或#INTER_NEAREST)和可选标志#WARP_INVERSE_MAP,它将M设置为逆变换
@param borderMode 像素外推法 (#BORDER_CONSTANT or #BORDER_REPLICATE).
@param borderValue 用于固定边框的情况;缺省情况下,它等于0。
@sa warpAffine, resize, remap, getRectSubPix, perspectiveTransform
*/
cv::Mat orig_gray, search_gray,result_img;
// 灰度化
cv::cvtColor(orig_img, orig_gray, cv::COLOR_BGR2GRAY);
cv::cvtColor(search_img, search_gray, cv::COLOR_BGR2GRAY);
// 创建SIFT
cv::Ptr<cv::SIFT> sift = cv::SIFT::create();
std::vector<cv::KeyPoint> origImgSiftArray, searchImgSiftArray;
cv::Mat origImgDescriptorsArray, searchImgDescriptorsArray;
// 获取两张图的关键点和描述子
sift->detectAndCompute(orig_gray, cv::Mat(), origImgSiftArray, origImgDescriptorsArray);
sift->detectAndCompute(search_gray, cv::Mat(), searchImgSiftArray, searchImgDescriptorsArray);
std::vector<std::vector<cv::DMatch>> matches;
std::vector<cv::DMatch> goodmatch;
cv::FlannBasedMatcher flann = cv::FlannBasedMatcher();
// knnMatch 返回k个最匹配的向量
flann.knnMatch(origImgDescriptorsArray, searchImgDescriptorsArray, matches, 2);
// 过滤,寻找较好的匹配向量
for (size_t i = 0; i < matches.size(); i++)
{
if (matches[i][0].distance < 0.7 * matches[i][1].distance) {
goodmatch.push_back(matches[i][0]);
}
}
// 有足够的匹配向量,才需要寻找
if (goodmatch.size() >= 4)
{
std::vector<cv::Point2f> srcPoints, searchPoints;
// 获取匹配向量中原图和搜索图相关的坐标
for (size_t i = 0; i < matches.size(); i++)
{
// 获取匹配向量中原图的坐标
srcPoints.push_back(origImgSiftArray[matches[i][0].queryIdx].pt);
// 获取匹配向量中搜索图的坐标
searchPoints.push_back(searchImgSiftArray[matches[i][0].trainIdx].pt);
}
// 单应性矩阵
// RANSAC 随机抽样算法
cv::Mat homoMat = cv::findHomography(srcPoints, searchPoints, cv::RANSAC, 5.0);
//cv::Mat homoMat = cv::getPerspectiveTransform(srcPoints, dstPoints, cv::RANSAC);
std::vector<cv::Point2f> origBorderP = { {0,0},{0,(float)(orig_img.rows - 1)},{(float)(orig_img.cols - 1),(float)(orig_img.rows - 1)},{(float)(orig_img.cols - 1),0} };
std::vector<cv::Point2f> origOnSearchP_f;
std::vector<cv::Point2i> origOnSearchP_i;
// 透视变换 cv::perspectiveTransform(原图坐标点,原图在搜索图上的坐标点,单应性矩阵) 在搜索图中找到原图的坐标点
cv::perspectiveTransform(origBorderP, origOnSearchP_f, homoMat);
//cv::warpPerspective(orig_img, search_img, homoMat, search_img.size());
/*类型转换 mat to vector
cv::Mat mat;
std::vector<cv::Point2f> origOnSearchP_f = cv::Mat_<cv::Point2f>(mat);
*/
// 数据类型转换 std::vector<cv::Point2f> to std::vector<cv::Point2i>
cv::Mat(origOnSearchP_f).convertTo(origOnSearchP_i, CV_32SC1);
cv::polylines(search_img, origOnSearchP_i, true, cv::Scalar(0, 0, 255), 2, 16);
}
// 绘画匹配集
cv::drawMatches(orig_img, origImgSiftArray, search_img, searchImgSiftArray, goodmatch, result_img);
cv::imshow("result_img", result_img);
cv::waitKey(0);
cv::perspectiveTransform 效果图
cv::warpPerspective 效果图
案例图像拼接
获取单应性矩阵
cv::Mat findHomo(cv::Mat img1, cv::Mat img2, double confineValue=0.8)
cv::Mat findHomo(cv::Mat img1, cv::Mat img2, double confineValue=0.8) {
// 灰度化
cv::Mat img1_gray, img2_gray;
cv::cvtColor(img1, img1_gray, cv::COLOR_BGR2GRAY);
cv::cvtColor(img2, img2_gray, cv::COLOR_BGR2GRAY);
// 特征点检测
cv::Ptr<cv::SIFT> sift = cv::SIFT::create();
// 获取关键点和描述子
std::vector<cv::KeyPoint> kp1, kp2;
cv::Mat dp1, dp2;
sift->detectAndCompute(img1_gray, cv::Mat(), kp1, dp1);
sift->detectAndCompute(img2_gray, cv::Mat(), kp2, dp2);
// 特征匹配
cv::BFMatcher bfM = cv::BFMatcher();
// 获取描述子的k个匹配向量
std::vector< std::vector<cv::DMatch>> matchesArray;
bfM.knnMatch(dp1, dp2, matchesArray, 2);
// 过滤匹配向量
std::vector<cv::DMatch> goodMatches;
for (std::vector<cv::DMatch> m: matchesArray)
{
if (m[0].distance < confineValue * m[1].distance) {
goodMatches.push_back(m[0]);
}
}
// 确保有足够的匹配向量
if (goodMatches.size() >= 8) {
// 根据匹配向量获取相关坐标
std::vector<cv::Point2f> cp1, cp2;
for(cv::DMatch e: goodMatches)
{
cp1.push_back(kp1[e.queryIdx].pt);
cp2.push_back(kp2[e.trainIdx].pt);
}
return cv::findHomography(cp1, cp2, cv::RANSAC, 5.0);
}
else {
std::cout << "There are not enough matching vectors \n";
exit(-1);
}
}
图像拼接
cv::Mat stitchImg(cv::Mat img1, cv::Mat img2, cv::Mat homo)
/// <summary>
/// 图像拼接
/// </summary>
/// <param name="img1">搜索集</param>
/// <param name="img2">训练集</param>
/// <param name="homo1">img1 on img2单应性矩阵</param>
/// <returns></returns>
cv::Mat stitchImg(cv::Mat img1, cv::Mat img2, cv::Mat homo) {
std::vector<cv::Point2f> img1Dims = { {0,0},{0,(float)img1.rows},{(float)img1.cols,(float)img1.rows},{(float)img1.cols,0} };
std::vector<cv::Point2f> img2Dims = { {0,0},{0,(float)img2.rows},{(float)img2.cols,(float)img2.rows},{(float)img2.cols,0} };
std::vector<cv::Point2f> resultImgDims;
cv::perspectiveTransform(img2Dims, resultImgDims, homo);
// 拼接数组
//resultImgDims.insert(resultImgDims.begin(), { {0,0},{0,(float)img1.rows},{(float)img1.cols,(float)img1.rows},{(float)img1.cols,0} });
//for (cv::Point2f point : img1Dims) resultImgDims.push_back(point);
nc::NdArray<cv::Point2f> result = resultImgDims;
nc::NdArray<cv::Point2f> result1 = img1Dims;
nc::NdArray<cv::Point2f> concatenateP = nc::concatenate({ result,result1 });
// 获取最大,最小的x和y
nc::NdArray<float> tempArray = { concatenateP[0].x, concatenateP[0].y };
for (cv::Point2f point : concatenateP) tempArray = nc::append(tempArray, { point.x, point.y }).reshape(-1,2);
int maxP[] = {(int)(tempArray.max(nc::Axis::ROW)[0]+ 0.5),(int)(tempArray.max(nc::Axis::ROW)[1] + 0.5) };
int minP[] = {(int)(tempArray.min(nc::Axis::ROW)[0]- 0.5),(int)(tempArray.min(nc::Axis::ROW)[1] - 0.5) };
std::cout << tempArray;
std::cout << maxP[0] << maxP[1] <<"\n";
std::cout << minP[0] << minP[1] << "\n";
// 平移透视之后的图片,定义平移变换矩阵
cv::Mat transformArray = cv::Mat::eye(3,3,CV_64FC1);
transformArray.at<double>(0, 2) = std::abs((double)minP[0]);
transformArray.at<double>(1, 2) = std::abs((double)minP[1]);
cv::Mat resultImg;
//转换图像,将搜索集放于训练集适当位置
cv::warpPerspective(img1, resultImg, transformArray*homo, cv::Size(maxP[0] + std::abs(minP[0]), maxP[1] + std::abs(minP[1])));
img2.copyTo(resultImg({ std::abs(minP[1]), std::abs(minP[1]) + img2.rows },
{ std::abs(minP[0]), std::abs(minP[0]) + img2.cols }));
return resultImg;
}
main函数
int main(){
cv::Mat img1 = cv::imread(".\\img\\mountain1.jpg");
cv::Mat img2 = cv::imread(".\\img\\mountain2.jpg");
// 两张图像大小需一致
cv::resize(img1, img1, cv::Size(640, 480));
cv::resize(img2, img2, cv::Size(640, 480));
// 获取img1 on img2单应性矩阵
cv::Mat homo1 = findHomo(img1, img2);
// 获取拼接后的图像
cv::Mat resultImg1 = stitchImg(img1, img2, homo1);
//获取img2 on img1单应性矩阵
//cv::Mat homo2 = findHomo(img2, img1);
//cv::Mat resultImg2 = stitchImg(img2, img1, homo2);
cv::imshow("resultImg1", resultImg1);
cv::imshow("img1", img1);
cv::imshow("img2", img2);
cv::waitKey(0);
return 0;
}
图像分割
传统的图像分割方法
分水岭法
void distanceTransform( InputArray src, OutputArray dst,
OutputArray labels, int distanceType, int maskSize,
int labelType = DIST_LABEL_CCOMP );
void distanceTransform( InputArray src, OutputArray dst,
int distanceType, int maskSize, int dstType=CV_32F);
/** @brief 计算源图像中每个像素到最近的零像素的距离。
函数cv::distanceTransform计算二值图像中每个像素到最近的零像素的近似或精确距离。对于零像素的图像,距离显然为零。
当maskSize == #DIST_MASK_PRECISE和distanceType == #DIST_L2时,该函数将运行@cite Felzenszwalb04中描述的算法。该算法利用TBB库实现了并行化。
@param src 8位,单通道(二进制)源图像。
@param dst Output 具有计算距离的图像。它是一个8位或32位浮点的单通道映像,大小与src相同。
@param labels 输出二维标签数组(离散Voronoi图)。它的类型为CV_32SC1,大小与src相同。
@param distanceType 距离类型,请参见#DistanceTypes
@param maskSize 距离转换掩码的大小,参见#DistanceTransformMasks。在#DIST_L1或#DIST_C距离类型的情况下,参数被迫为3,因为3×3掩码给出的结果与5×5或任何更大的掩码相同。
@param dstType 输出图像的类型。请参见#DistanceTransformLabelTypes.它可以是CV_8U或CV_32F。类型CV_8U只能用于函数的第一个变体,并且distanceType == #DIST_L1
*/
int connectedComponents(InputArray image, OutputArray labels,
int connectivity, int ltype, int ccltype);
int connectedComponents(InputArray image, OutputArray labels,
int connectivity = 8, int ltype = CV_32S);
/** @brief 计算布尔图像的标记图像的连接组件
@param image 要标记的8位单通道图像
@param labels 目的地标记图像
@param connectivity 8或4分别为8路或4路连接
@param ltype 输出图像标签类型。目前支持CV_32S和CV_16U。
@param ccltype 连接组件的算法类型(参见#ConnectedComponentsAlgorithmsTypes).
*/
cv::Mat pillThreshold,pillGray,pillOpen,pillBGM, pillDist, pillFG,pillUnknow,pillMask;
// 灰度化
cv::cvtColor(pill, pillGray, cv::COLOR_BGR2GRAY);
// 二值化
cv::threshold(pillGray, pillThreshold, 0, 255, cv::THRESH_BINARY + cv::THRESH_OTSU);
// 获取卷积核(MORPH_CROSS十字形,RECT矩形,ELLIPSE椭圆形)
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
// 开运算()
cv::morphologyEx(pillThreshold, pillOpen, cv::MORPH_OPEN, kernel,cv::Point(-1,-1));
// 获取背景
cv::dilate(pillOpen, pillBGM, kernel);
// 获取前景
cv::distanceTransform(pillOpen, pillDist, cv::DIST_L2, 5);
double distMax;
cv::minMaxLoc(pillDist, 0, &distMax);
cv::threshold(pillDist, pillFG, 0.55 * distMax, 255, cv::THRESH_BINARY);
// 更改type(只允许相同通道)
pillFG.convertTo(pillFG, CV_8UC1,255);
//std::cout << pillBGM.type() << "\n" << pillFG.type();
获取未知区域
cv::subtract(pillBGM, pillFG, pillUnknow);
创建连通域
cv::connectedComponents(pillFG, pillMask);
//pillMask += 1;
for (size_t i = 0; i < pillUnknow.rows; i++)
{
for (size_t j = 0; j < pillUnknow.cols; j++)
{
if (pillUnknow.at<uchar>(i,j) == 255)
{
pillMask.at<std::int32_t>(i, j) = 0;
}
}
}
// 分水岭算法 轮廓标配-1
cv::watershed(pill, pillMask);
for (size_t i = 0; i < pillMask.rows; i++)
{
for (size_t j = 0; j < pillMask.cols; j++)
{
if (pillMask.at<std::int32_t>(i, j) == -1)
{
pill.at<uchar>(i, j*3) = 0;
pill.at<uchar>(i, j*3+1) = 0;
pill.at<uchar>(i, j*3+2) = 255;
}
}
}
cv::imshow("pill", pill);
cv::imshow("pillOpen", pillOpen);
cv::imshow("pillDist", pillDist);
cv::imshow("pillBGM", pillBGM);
cv::imshow("pillFG", pillFG);
cv::imshow("pillUnknow", pillUnknow);
cv::waitKey(0);
GrabCut法
static class GrabCutTest {
private:
cv::Mat imgout, bgMask, fgMask,tem1,tem2;
nc::NdArray<UINT8> ncMask;
public:
static int startX;
static int startY;
static cv::Mat img;
static bool flag_rectangle;
static cv::Rect rect;
static cv::Mat mask;
GrabCutTest() {
imgout = imgout.zeros(img.size(), CV_8UC3);
//mask = mask.zeros(img.size(), CV_8UC1);
tem1.create(img.size(), CV_8UC1);
ncMask = nc::zeros<UINT8>(nc::Shape(img.rows, img.cols));
};
~GrabCutTest() {
NULL;
};
// 静态函数 属于类的方法,不属于对象,可直接调用
static void mouseCallback(int event, int x, int y, int flags, void* userdata = 0) {
std::cout << "onmouse\n";
if (event == cv::EVENT_LBUTTONDOWN) {
//std::cout << "down\n";
startX = x;
startY = y;
flag_rectangle = true;
}
else if (event == cv::EVENT_LBUTTONUP) {
//std::cout << "up\n";
flag_rectangle = false;
cv::rectangle(img, cv::Point(startX, startY), cv::Point(x, y), cv::Scalar(0, 0, 255), 2, 16);
//mask.setTo(cv::Scalar::all(cv::GC_BGD));
rect = {std::min(startX,x),std::min(startY,y), std::abs(x-startX),std::abs(y-startY)};
//mask(rect).setTo(cv::Scalar(cv::GC_PR_FGD));
}
else if (event == cv::EVENT_MOUSEMOVE) {
std::cout << "move\n";
if (flag_rectangle) {
cv::Mat img2;
img.copyTo(img2);
cv::rectangle(img2, cv::Point(startX, startY), cv::Point(x, y), cv::Scalar(0, 255, 255), 2, 16);
cv::imshow("input", img2);
cv::waitKey(100);
}
}
//std::cout << event << " " << x << " " << y << " " << flags << " " << userdata << std::endl;
};
void run() {
std::cout << "run\n";
cv::namedWindow("input");
cv::setMouseCallback("input", mouseCallback);
while (true)
{
cv::imshow("input", img);
cv::imshow("output", imgout);
int key = cv::waitKey(100);
if (key == int('q')) break;
if (key == int('g')) {
// GC_FGD = 1 // 属于前景色的像素
// GC_BGD =0; // 属于背景色的像素
// GC_PR_FGD = 3 // 可能属于前景的像素
// GC_PR_BGD = 2 // 可能属于背景的像素
cv::grabCut(img, mask, rect, bgMask, fgMask, 1,cv::GC_INIT_WITH_RECT);
}
/*// 比较两个Mat矩阵是否相等
if (memcmp(mask.data, tem1.setTo(cv::Scalar::all(cv::GC_FGD)).data, mask.total() * mask.elemSize()) == 0
|| memcmp(mask.data, tem1.setTo(cv::Scalar::all(cv::GC_PR_FGD)).data, mask.total() * mask.elemSize()) == 0) {
}*/
for (size_t i = 0; i < mask.total() * mask.elemSize(); i++)
{
uchar* temp = mask.data;
if (!(*(temp + i) == 1 || *(temp + i) == 3)) {
*(temp + i) = 0;
}
}
cv::bitwise_and(img, img, imgout, mask);
//img.copyTo(imgout, mask);
}
};
};
int GrabCutTest::startX = 0;
int GrabCutTest::startY = 0;
bool GrabCutTest::flag_rectangle = false;
cv::Mat GrabCutTest::img = cv::imread("./img/3.jpg");
cv::Rect GrabCutTest::rect = cv::Rect();
cv::Mat GrabCutTest::mask = mask.zeros(img.size(), CV_8UC1);
MeanShift法
void pyrMeanShiftFiltering( InputArray src, OutputArray dst,
double sp, double sr, int maxLevel = 1,
TermCriteria termcrit=TermCriteria(TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) );
/**
@brief 执行图像的meanshift分割的初始步骤.
该函数实现了meanshift分割的滤波阶段,即函数的输出是经过滤波后的经过颜色渐变和细粒纹理压平的“分色”图像。
在输入图像(或缩小的输入图像,见下图)的每一个像素(X,Y)处,函数执行meanshift迭代,即考虑联合空间-颜色多维空间中的像素(X,Y)邻域:
\f[(x,y): X- \texttt{sp} \le x \le X+ \texttt{sp} , Y- \texttt{sp} \le y \le Y+ \texttt{sp} , ||(R,G,B)-(r,g,b)|| \le \texttt{sr}\f]
其中(R,G,B)和(R,G,B)分别是颜色分量在(X,Y)和(X,Y)处的向量(不过,算法不依赖于所使用的颜色空间,所以可以使用任何3分量的颜色空间)。在邻域上找到平均空间值(X',Y')和平均颜色向量(R',G',B'),并在下一次迭代中充当邻域中心:
\f[(X,Y)~(X',Y'), (R,G,B)~(R',G',B').\f]
迭代结束后,初始像素(即迭代开始的像素)的颜色组件被设置为最终值(最后一次迭代的平均颜色):
\f[I(X,Y) <- (R*,G*,B*)\f]
当maxLevel>0时,构建maxLevel+1层的高斯金字塔,并先在最小的层上运行上述过程。在此之后,结果被传播到较大的层,迭代只在那些层颜色与金字塔的低分辨率层的差异超过sr的像素上再次运行。这使得颜色区域的边界更加清晰。注意,结果实际上与在原始图像上运行meanshift过程得到的结果不同(即当maxLevel==0时)。
@param src 8位3通道图像。
@param dst 与源图像具有相同格式和相同大小的目标图像。
@param sp 空间窗口半径。
@param sr 颜色窗口半径。
@param maxLevel 用于分割的金字塔的最大水平。
@param termcrit 终止标准:何时停止meanshift迭代
*/
void Canny( InputArray image, OutputArray edges,
double threshold1, double threshold2,
int apertureSize = 3, bool L2gradient = false
/*
@param image 8位输入图象.
@param edges 输出边图;单通道8位图像,与图像大小相同。
@param threshold1 迟滞过程的第一个阈值。
@param threshold2 迟滞过程的第二个阈值。
@param apertureSize Sobel算子的孔径大小。
@param L2gradient 一个标志,表示是否有更精确的L_2范数
*/
);
cv::Mat meanshiftImg,cannyImg;
// pyrMeanShiftFiltering(输入图,输出图,空间窗口半径,颜色窗口半径):空间窗口半径和颜色窗口半径都需要调整
// 空间窗口半径越大,轮廓越清晰,不过计算量大,耗时久;颜色窗口半径越大,物体的原有越模糊
cv::pyrMeanShiftFiltering(img, meanshiftImg, 100, 30);
cv::Canny(meanshiftImg, cannyImg, 150, 300);
std::vector<std::vector<cv::Point>> ContoursPointArray;
cv::findContours(cannyImg, ContoursPointArray, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
cv::drawContours(img, ContoursPointArray, -1, cv::Scalar(0, 0, 255), 2, 16);
cv::imshow("src", img);
cv::imshow("meanshiftImg", meanshiftImg);
背景扣除法
cv::VideoCapture video = cv::VideoCapture("./video.mp4");
cv::Ptr<cv::BackgroundSubtractorMOG2> mog = cv::createBackgroundSubtractorMOG2();
/*createBackgroundSubtractorKNN(int history=500, double dist2Threshold=400.0,
bool detectShadows=true);
history是用于构建背景统计模型的帧数。该值越小,模型将越快考虑背景的变化,从而将其视为背景。反之亦然。
dist2Threshold 是一个阈值,用于定义像素是否与背景不同。该值越小,运动检测越灵敏。反之亦然。
detectShadows : 如果设置为 true,阴影将在生成的蒙版上以灰色显示*/
cv::Ptr<cv::BackgroundSubtractorKNN> knn = cv::createBackgroundSubtractorKNN();
while (true)
{
cv::Mat frame,frameMask;
video.read(frame);
knn->apply(frame, frameMask);
cv::imshow("video", frameMask);
if (cv::waitKey(10) == (int)'q')
break;
}
video.release();
cv::destroyAllWindows();
人脸识别
// haar级联分类
cv::CascadeClassifier HaarFaceCascade = cv::CascadeClassifier("./haarcascades/haarcascade_frontalface_alt.xml");
// lbp级联分类
cv::CascadeClassifier lbpFaceCascade = cv::CascadeClassifier("D:/vsOpencv/opencv/sources/data/lbpcascades/lbpcascade_frontalface.xml");
cv::Mat src = cv::imread("./img/face.jpeg");
cv::Mat src2 = cv::imread("./img/face.jpeg");
cv::Mat srcGray;
cv::cvtColor(src, srcGray, cv::COLOR_RGB2GRAY);
cv::equalizeHist(srcGray, srcGray);//直方图均值化,提升对比度,提升图像特征提取的准确率
std::vector<cv::Rect> faces,faces2;
// haar
HaarFaceCascade.detectMultiScale(srcGray, faces);
// lbp
lbpFaceCascade.detectMultiScale(srcGray, faces2);
for (size_t i = 0; i < faces.size(); i++)
{
cv::rectangle(src, faces[i], cv::Scalar(0, 0, 255), 1, 16);
}
for (size_t i = 0; i < faces2.size(); i++)
{
cv::rectangle(src2, faces2[i], cv::Scalar(0, 0, 255), 1, 16);
}
cv::putText(src, "face Count: " + std::to_string(faces.size()), cv::Point(30, 30), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 0), 2, 16);
cv::putText(src2, "face Count: " + std::to_string(faces2.size()), cv::Point(30, 30), cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 0), 2, 16);
cv::imshow("haar", src);
cv::imshow("lbp", src2);
cv::waitKey(0);
Mat 矩阵
数据转换
Vector 和 Mat 互换
// Mat to Vector
std::vector<uchar> arr_u = (std::vector<uchar>)img.reshape(1,1); // reshape(新的通道数,新的row数)
//vector to mat
cv::Mat array2Img(nc::NdArray<uchar> array, int n) {
size_t h = array.numRows();
size_t w = array.numCols();
cv::Mat img(h, (size_t)(w / n), CV_8UC(n)); //保存为RGB
for (size_t i = 0; i < h; i++)
{
uchar* tmp = img.ptr<uchar>(i);
for (size_t j = 0; j < w; j++)
{
tmp[j] = array(i, j);
}
}
return img;
}
数据类型转换
// Mat to Vector
std::vector<uchar> arr_u = (std::vector<uchar>)img.reshape(1,1); // reshape(新的通道数,新的row数)
// vector数据类型转换 1
cv::Mat(arr_u).convertTo(arr_i, CV_32SC1);
// vector数据类型转换 2
// std::vector<int> arr_i = std::vector<int>(arr_u.begin(), arr_u.end());
// vector数据类型转换 3
//for (auto ele : arr_u) arr_i.push_back(static_cast<int>(ele));
nc::NdArray<uchar> nArr_uc = arr_u;
// NdArray数据类型转换
nc::NdArray<nc::int16> nArr_i16 = nArr_uc.astype<nc::int16>();
坐标点类型转换
// 数据类型转换 std::vector<cv::Point2f> to std::vector<cv::Point2i>
std::vector<cv::Point2f> origOnSearchP_f;
std::vector<cv::Point2i> origOnSearchP_i;
cv::Mat(origOnSearchP_f).convertTo(origOnSearchP_i, CV_32SC1);
// 类型转换 mat to vector
cv::Mat mat;
std::vector<cv::Point2f> origOnSearchP_f = cv::Mat_<cv::Point2f>(mat);