形态学基本处理方法

基于图像形态进行处理的一些基本方法,这些处理方法基本是对二进制图像进行处理,卷积核决定着图像处理后的效果。
步骤:灰度图像->二值化->形态学运算

二值化

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 选择最优阈值

C++ opencv形态学、轮廓查找、特征检测和图像分割

全局二值化

	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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

局部二值化

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

腐蚀和膨胀

腐蚀原理:锚点所在的像素点卷积核形状若没有全部覆盖白色区域,则变0(需要根据卷积核决定)。
膨胀原理:锚点所在的像素点卷积核形状若有一个接触到白色区域,则变1(需要根据卷积核决定)。
以下为腐蚀过程,传入一个5×5的全1卷积核,卷积核的锚点所重叠的像素点的颜色取决于,卷积核的形状覆盖全部白色区域时为1,反之则为0。最里面的虚线矩形为最终腐蚀后的图像。
C++ opencv形态学、轮廓查找、特征检测和图像分割

	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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

图像形态学运算

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割
C++ opencv形态学、轮廓查找、特征检测和图像分割

图像轮廓

轮廓:具有相同颜色和强度连续点的线条
作用:图形分析、物体的识别和检测

寻找轮廓

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 存储顺序是最里面的为顶层,先到左边顶层由外到内编号,再到左边顶层由外到内编号;接着到第二层,先到左边第二层由外到内编号,再到左边第二层由外到内编号;由此类推。
C++ opencv形态学、轮廓查找、特征检测和图像分割
RETR_TREE 存储顺序是先右边由外向内,再往左边由外向内,依次编号(较常用)
C++ opencv形态学、轮廓查找、特征检测和图像分割
RETR_LIST 存储顺序是先找右边顶层的内框,再找左边顶层的内框,接着右外,再到左外;下一层亦是如此。
C++ opencv形态学、轮廓查找、特征检测和图像分割

轮廓的面积和周长

/*@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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

凸包

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

外接矩形

最小外接矩形

	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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

最大外接矩形

	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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

案例

车辆检测(简易)

	// 检测线高度
	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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

关键点描述子

// 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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

继承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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

特征点匹配

	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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

图像查找

图像查找 = 特征匹配 + 单应性矩阵
单应性矩阵的作用:矫正图片, 透视变换的变换矩阵

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 效果图
C++ opencv形态学、轮廓查找、特征检测和图像分割
cv::warpPerspective 效果图
C++ opencv形态学、轮廓查找、特征检测和图像分割
C++ opencv形态学、轮廓查找、特征检测和图像分割

案例图像拼接

获取单应性矩阵

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;

C++ opencv形态学、轮廓查找、特征检测和图像分割

图像分割

传统的图像分割方法

分水岭法

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

背景扣除法

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);

C++ opencv形态学、轮廓查找、特征检测和图像分割

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);
        解决OSError: Error no file named pytorch_model.bin, tf_model.h5 found in directory    
    2023年4月19日
        像素坐标到世界坐标的转换    
    2023年2月24日
    2023年5月26日