opencv学习笔记(三)

OpenCV读书笔记(3)

  1. 提取直线、轮廓和区域(第七章),要进行基于内容得图像分析,就必须从构成图像得像素集中提取出有意义的特征。轮廓,直线、斑点就是基本的图像图元,可以用来描述图像包含的元素。

  2. Canny算子检测图像轮廓,简单的二值分布图有两个主要缺点:第一,检测到的边缘过厚,这加大了识别物体边界的难度;第二,也是更重要的,通常不可能找到既低到足以检测到图像中所有重要边缘,又搞到足以避免产生太多无关紧要边缘的阈值。Canny算法试图解决这个问题。

    // 应用Canny算法
    cv::Mat contours;
    cv::Canny(image,	// 灰度图像
    			contours,	// 输出轮廓
    			125,	// 低阈值
    			350);	// 高阈值
    
  3. Canny算子的核心理念是用两个不同的阈值来判断哪个点属于轮廓,一个低阈值,一个高阈值。选择低阈值时,要保证它能包含所有属于重要图像轮廓的边缘像素。第二个阈值的作用就是界定重要轮廓的边缘,排除掉异常的边缘。Canny算法将结合这两种边缘分布图,生成最优的轮廓分布图。

  4. 具体做法是在低阈值边缘分布图上只保留具有连续路径的边缘点,同时把那些边缘点连接到属于高阈值边缘分布图的边缘上。这样一来,高阈值分布图上的所有边缘点都被保留下来,而低阈值分布图上边缘点的孤立链全部被移除。只要指定适当的阈值,就能获得高质量的轮廓。这种基于两个阈值获得二值分布图的策略被称为滞后阈值化,可用于任何需要用阈值化获得二值分布图的场景。但是计算复杂度较高。另外,在进行之后阈值化之前,如果梯度幅值不是梯度方向上的最大值,那么对应的边缘点都会被移除。因此,这个方向上梯度的局部最大值对应着轮廓最大强度的位置。这是一个细化轮廓的运算,他创建的轮廓宽度只有一个像素值。这也解释了为什么Canny轮廓分布图的边缘比较薄。

  5. 霍夫变换检测直线,OpenCV提供了两种实现方法,基础版是cv::HoughLines。它的输入是一个二值分布图,其中包含一批图像像素点(用非零像素表示),一些对齐的点构成了直线。它通常是一个已经生成的边缘分布图,例如Canny算子生成的分布图。

    // 应用Canny算法
    cv::Mat contours;
    cv::Canny(image, contours, 125, 350);
    // 用霍夫变换检测直线
    std::vector<cv::Vec2f> lines;
    cv::HoughLines(test, lines, 
    				1, // 半径步长为1,表示函数将搜索所有可能的半径
    				PI/180,  // 步长;角度步长,表示函数将搜索所有可能的角度。
                    60);	// 最小投票数z
    

    这个算法检测的图像中的直线而不是线段,它不会给出直线的端点。因此,绘制的直线将穿透整幅图像。对于垂直方向的直线,计算它于图像水平边界(第一行和最后一行)的交叉点,然后再这两个交叉点之间画线。即使点的坐标超出了图像范围,这个函数也能正确运行,因此没有必要检查交叉点是否图像内部。

  6. 为了解决霍夫变换可能产生的检测结果(多条参数相近的直线穿过同一个像素对齐区域,而导致检测出重复的结果)。提出了霍夫变换的改进版,即概率霍夫变换,用cv::HoughLinesP函数实现。

  7. 霍夫变换的目的是在二值图像中找出全部直线,并且这些直线必须穿过足够多的像素点。处理方法,检查输入的二值分布图像中每个独立的像素点,识别穿过该像素点的所有可能直线。如果同一条直线穿过很多像素点,就说明这条直线明显到足以被认定。

  8. 点集的直线拟合是一个经典数学问题。OpenCV的实现方法是使每个点到直线的距离之和最小化。在众多用于计算距离的函数中,欧几里得距离的计算速度最快,所用参数为cv::DIST_L2。这一选项对应了标准的最小二乘法直线拟合。如果点集中包含了孤立点(即不属于直线的点),可用选用其他距离函数,以减少远距离的点带来的影响。最小化计算的基础是M估算法技术,它采用迭代方式解决加权最小二乘法问题,其中权重与点到直线的距离成反比。也可用这个函数在三维点集上拟合直线。

  9. cv::fitEllipse函数在二维点集上拟合一个椭圆。它返回一个旋转的矩形(一个cv::RotataedRect实例),矩形中有一个内切的椭圆。

    cv::RotatedRect rrect = cv::fitEllipse(cv::Mat(points));
    cv::ellipse(image, rrect, cv::Scalar(0));// 画椭圆时会用到的函数之一。
    
  10. 提取连续区域,图像通常包含各种物体,图像分析的目的之一就说识别和提取这些物体。在物体检测和识别程序中,第一步通常就是生成二值图像,找到感兴趣物体所处的位置。不管用什么方式获得二值图像,下一个步骤都是从由1和0组成的像素中提取出物体。下一步骤都是从由1和0组成的像素集合中提取物体。

  11. OpenCV提供了一个简单的函数,可用提取出图像中连续区域的轮廓,这个函数就说cv::findContours:

    //
    std::vector<std::vector<cv::Point>> contours;
    cv::findContours(image,
    				contours,	// 存储轮廓的向量
    				cv::RETR_EXTERNAL,  // 检索外部轮廓
    				cv::CHAIN_APPROX_NONE); // 每个轮廓的全部像素	         第三个参数的选项:
    cv::CHAIN_APPROX_SIMPLE, 只会列出包含水平、垂直或对角线轮廓的端点。
    cv::CHAIN_APPROX_NONE, 向量将列出轮廓的全部点
    用其他选项可得到逼近轮廓的更复杂的链,对轮廓的表示将更紧凑。用`contours.size()`查看轮廓的数量。
    
  12. 在图像(白色图像)上画出那些区域的轮廓:

    cv::Mat result(image.size(), CV_8U, cv::Scalar(255));
    cv::drawContours(result, contours,
    				-1,	// 画出全部轮廓
    				0,	// 用黑色画
    				2);	// 宽度为2
    
  13. 计算区域的形状描述子,连续区域通常代表着场景中的某个物体。为了识别该物体,或将它与其他图像元素做比较,需要对此区域进行测量,以提取部分特征。

  14. 多边形绘制函数cv::polylines与其他画图函数很相似。第三个布尔型参数表示该轮廓是否闭合(如果闭合,最后一个点将与第一个点相连)。

  15. 凸包是另一种形式的多边形逼近(位于左侧第二个区域);

  16. 计算轮廓矩是另一种功能强大的描述子(在所有区域内部画出重心);

  17. 在表示和定位图像中区域的方法中,边界框可能是最简洁的。其定义是:能完整包含该形状的最小垂直矩形。比较边界框的高度和宽度,可用获得物体在垂直或水平方向的特征。(例如可以通过计算高度与宽度的比例,分辨出一幅图像是汽车还是行人)。最小覆盖圆通常只需要区域尺寸和位置的近似值时使用。

  18. 若要紧凑地表示区域地形状,可采用多边形逼近。在创建时要制定精确度参数,表示形状与对应地简化多边形之间能接受的最大距离。它是cv::approxPolyDP函数的第四个参数。返回的结果是cv::Point类型的向量,表示多边形的顶点个数。在画这个多边形时,要迭代遍历整个像个向量,并在顶点之间画直线,把他们逐个连接起来。

  19. 形状的凸包(或凸包络)是包含该形状的最小凸多边形。可以把它看作一条在区域周围的橡皮筋。可以看出,在形状轮廓中凹进去的位置,凸包轮廓会与原始轮廓发生偏离。OpenCV中有一个专门用于识别凸包缺陷的函数

    std::vector<cv::Vec4i> defects;
    cv::convexityDefects(contour, hull, defects);
    

    参数contour和Hull分别表示原始轮廓和凸包轮廓,函数输出的是一个向量,它的每个元素由四个整数组成:前两个整数是顶点在轮廓中是索引,用来界定该缺陷;第三个整数表示凹陷内部最远的点;最后的整数表示最远点与凸包之间的距离。

  20. 轮廓矩是形状结构分析中常用的数学模型。OpenCV 定义了一个数据结构,封装了形状中计算得到的所有轮廓矩。它是函数 cv::moments 的返回值。这些轮廓矩共同表示物体形状的紧凑程度,常用于特征识别。我们只是用该结构获得每个区域的重心,这里用前面三个空间轮廓矩计算得到。

  21. cv::minAreaRect函数:计算最小覆盖自由矩形
    cv::contourArea函数:估算轮廓的面积(内部的像素数量)
    cv::pointPolygonTest函数:判断一个点在轮廓的内部还是外部
    cv::matchShapes函数:度量两个轮廓之间的相似度。s
    

    所有这些度量属性的方法都可以有效结合起来,用于更高级的结构分析。

检测兴趣点

  1. 在计算集视觉领域,兴趣点(也称关键点特征点)的概念已经得到了广泛的应用,包括目标识别、图像配准、视觉跟踪、三维重建等。这个概念的原理是,从图像中选取某些特征点并对图像进行局部分析(即提取局部特征),而非观察整幅图像(即提取全局特征)。只要图像中有足够多可检测的兴趣点,并且这些兴趣点各不相同且特征稳定、能被精确地定位,上述方法就十分有效。

  2. 视觉不变性是图像分析中一个非常重要的属性。

  3. 在图像中搜索有价值的特征点时,使用角点是一种不错的方法。角点的价值在于它是两条边缘线的结合点,是一种二维特征,可以被精确地检测(即使是亚像素级精度)。

  4. Harri特征检测方法在假定的兴趣点周围放置了一个小窗口,并观察窗口内某个方向上强度值的平均变化。在所有方向上计算平均强度变化值,如果不止一个方向的变化值很高,就认为这个点是角点。Harri测试的步骤为:首先获得平均强度值变化最大的方向,然后检查垂直方向上的平均强度变化值,看它是否也很大;如果是,就说明这时一个角点。

  5. 从数学角度看,用泰勒展开式近似地计算Harri特征。写成矩阵形式,这是一个协方差矩阵,表示在所有方向上强度值变化的速率。这个定义包括了图像的一阶导数,通常用Sobel算子计算。这个协方差矩阵的两个特征值分别表示最大平均强度值变化和垂直方向的平均强度值变化。如果这两个特征值都很小,就说明是在相对同质的区域;如果一个特征值很大,另一个很小,那肯定是在边缘上;如果两者都很大,那么就是在角点上。因此,判断一个点为角点的条件是它的协方差矩阵的最小特征值要大于指定的阈值。

  6. Harri角点算法的原始定义用到了特征分解理论的一些属性:

    • 矩阵的特征值之积等于它的行列式值;
    • 矩阵的特征值之和等于它的对角元素之和(也就是矩阵的迹)。
  7. 膨胀运算会在邻域中把每个像素值替换成最大值,因此只有局部最大值的像素是不变的。用下面的相等测试可以验证这一点:

    cv::compare(cornerStrength, dilated, localMax, cv::CMP_EQ);
    

    因此矩阵localMax只有在局部最大值的位置才为真(即非零)。然后将它用于getCornerMap方法中,排除所有非最大值的特征(用cv::bitwise函数)。

    • 通过显式地计算特征值来检测Harri角点。这种修改原则上不会明显地影响检测结果,但是可以避免使用随意地k参数。

    • 第二项改进是针对特征点聚集的问题。事实上,尽管引入了局部最大值这个条件,兴趣点仍不会再图像中均匀分布,而是聚集再高度纹理化的位置。解决该问题的一种方案,就说限制两个兴趣点之间的最短距离。从Harri值最强的点开始(即具有最大的最低特征值),只允许一定距离之外的点成为兴趣点。在OpenCV中用good-features-to-track(GFTT)实现这个算法。这个算法得名于它检测的特征非常适合作为视觉跟踪程序的起始集合,它的使用方法如下所示:

      // 计算适合跟踪的特征
      std::vector<cv::KeyPoint> keypoints;
      // GFTT检测器
      cv::Ptr<cv::GFTTDetector> ptrGFTT = 
      	cv::GFTTDetector::create(
      						500, // 关键点的最大数量(按强度排序的)
      						0.01,  // 质量等级
      						10);	// 角点之间允许的最短距离
      // 检测GFTT
      ptrGFTT->detect(image, keypoints);
      
  8. OpenCV特征检测的公共接口定义了一个虚拟类cv::Feature2D,提供从包含图像的容器中检测兴趣点。计算特征描述符的方法,从文件中读取和写入检测到的兴趣点的方法。

  9. Harris算子因为需要计算图像的导数,而计算导数是非常耗时的。而且,检测兴趣点通常只是更复杂的算法中的第一步。

  10. FAST(Feature from Accelerated Segment Test,加速分割测试获得特征)。这种算子专门用来快速检测兴趣点—只需要对比几个像素,就可以判断它是否为关键点。

    // 关键点的向量
    std::vector<cv::KeyPoint> keypoints;
    // FAST 特征检测器,阈值为40
    cv::Ptr<cv::FastFeatureDetector> ptrFAST = cv::FastFeatureDetector::create(40);
    // 检测关键点
    ptrFAST->detect(image, keypoints);
    // 画出关键点
    cv::drawKeypoints(image,	// 原始图像
    				keypoints,	// 关键点得向量
    				image,	//	输出图像
    				cv::Scalar(255,255,255), 	// 关键点的颜色
    				cv::DrawMatchesFlags::DRAW_OVER_OUTIMG); // 画图标志
    
  11. 跟Harris检测器有的情况一样,FAST特征算法源于“什么构成了角点”的定义。FAST对角点的定义基于候选特征点周围的图像的强度值。以某个点为中心做一个圆,根据圆上的像素值判断该点是否为关键点。如果存在这样一段圆弧,它的连续长度超过周长的3/4,并且它上面所有像素的强度值都与圆心的强度值明显不同(全部更暗或更亮),那么就认定这是一个关键点。

  12. 如果我们测试圆周上相隔90度的四个点(例如取上、下、左、右四个位置),就很容易证明:为了满足前面的条件,其中必须有三个点都比圆心更亮或比圆心更暗。如果不满足该条件,就可以立即排除这个点,不需要检查圆周上的其他点。这种方法非常高效,因为在实际应用中,图像中大部分像素都可以用这个“四点比较法”排除。从概念上讲,用于检查像素的圆的半径应该作为方法的一个参数。但是根据经验,半径为3时可以得到好的结果和较高的计算效率。因此需要在圆周上检查16个像素。

  13. 一个点与圆心强度值得差距必须达到一个指定的值,才能被认为是明显更暗或更暗;这个值就是创建检测实例时指定的阈值参数。这个阈值越大,检测到的角点数量就越少。

  14. 至于Harris特征,通常最好在发现的角点上执行非最大值抑制。因此,需要定义一个角点强度的衡量方法。有多种衡量方法可供选择,实际选用的方法—-计算中心点像素与认定的连续圆弧上的像素的差值,然后将这些差值的绝对值累加,就能得到角点强度。科研从cv::KeyPoint实例的response属性获取角点强度。

  15. 若事先明确兴趣点数量的情况下,科研对检测过程进行动态适配。简单的做法是采用范围较大的阈值检测出很多兴趣点,然后从中提取n个强度最大的。为此可使用如下标志C++函数:

    if(numberOfPoints < keypoints.size()){
    	std::nth_element(keypoints.begin(), // keypoints的类型是std::vector,表示检测到的兴趣点
    	keypoints.begin() + numberOfPoints, // nunberOfPoints是需要的兴趣点数量
    	keypoints.end(),
    	[](cv::KeyPoint& a, cv::KeyPoint& b){
    		return a.response > b.response;
    	}); // lambda比较器,用于提取最佳的兴趣点
    }
    如果检测到的兴趣点太少(少于需要的数量),那就要采用更小的阈值,但是阈值太宽松又会加大计算量,所以需要权衡利弊,选取最佳的阈值。
    
  16. 检测图像特征点时还会遇到一种情况,就是兴趣点的分布很不均匀。keypoints通常会聚集在纹理较多的区域,例如教堂图像中的100个兴趣点。大部分特征点都集中在教堂的顶部和底部。对此有一种常用的处理方法,就是把图像分割成网格状,对每个小图像进行单独检测。这里的关键在于,利用ROI对每个网格的小图像进行关键点检测,这样得到的关键点分布较为均匀。

尺度不变特征的检测

  1. 特征检测的视觉不变性是一个非常重要的概念。特征检测器已经可以较好地解决方向不变性问题,即图像旋转后仍能检测到相同的特征点。

  2. 理想情况下,对于两幅图像中不同尺度的同一物体点,计算得到的两个尺度因子之间的比率应该等于图像尺度的比率。

  3. SURF特征,它的全称为加速稳健特征(Speeed Up Robust Feature),他们不仅是尺度不变特征,而且是具有较高计算效率的特征。

  4. SURF特征检测属于opencv_contrib库:

    // 创建SURF特征检测器对象
    cv::Ptr<cv::xfeatures2d::SurfFeatureDetector> ptrSURF = 
    			cv::xfeatures2d::SurfFeatureDetector::create(2000.0);
    // 检测关键点
    ptrSURF->detect(image, keypoints);
    // 画出关键点,包括尺度和方向信息
    cv::drawKeypoints(image,	// 原始图像
    				keypoints,		// 关键点的向量
    				featureImage,	// 结果图像
    				cv::Scalar(255,255,255),	// 点的颜色
    				cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS); // 得到了关键点的圆,并且圆的尺寸与每个特征计算的到的尺度成正比。为了使特征具有旋转不变性,SURF还让每个特征关联了一个方向,由每个圆内的辐射线表示。
    
  5. 对于以不同尺度拍摄的两幅图像的不同物体,对应的两个$\delta$值得比率等于拍摄两幅图像的尺度的比率。这是尺度不变特征提取的核心。也就是说,为了检测尺度不变特征,需要在图像空间(图像中)和尺度空间(通过不同尺度下应用导数滤波器得到)分别计算局部最大值。

  6. Hessian矩阵衡量了一个函数的局部曲率。

  7. 在用直方图统计像素时采用积分图像,是为了确保只用三个加法运算就可以计算每个滤波器分支的累加值,与滤波器尺寸无关。

  8. 多尺度FAST特征的检测BRISK不仅是一个特征检测器,它还包含了描述每个被检测关键点的领域的过程。金字塔的各个图层具有不同的分辨率。为了精确定位每个关键点ian,算法需要在尺度和空间两个方面进行插值。插值基于FAST关键点评分。在空间方面,在3x3的领域上进行插值;在尺度方面,计算要符合一个一维抛物线,该抛物线在尺度坐标轴上,穿过当前点和上下两层的两个相邻的局部关键点,这个关键点在尺度上的位置见前面的图片。这样做的结果是,即使在不连续的图像尺度上执行FAST关键点加测,最后检测到的每个关键点的对应尺度也还是连续的值。

  9. cv::BRISK检测器有两个主要参数,第一个参数是判断FAST关键点的阈值,第二个参数是图像中金字塔生成的八度的数量。

  10. ORB特征检测算法,ORB代表定向FAST和旋转BRIEG。这个缩写的第一层意思表示关键点检测,第二层意思表示ORB算法提供的描述子。

Table of Contents