Skip to content

特别的特征:直线特征

来自本人笔记: https://github.com/masterAllen/LearnOpenCV/blob/main/docs/特别的特征:直线特征

对于直线这个特征,和普通的特征检测一样,分为三个部分:检测、描述、匹配。

直线检测

直线检测,相当于输入一副图片,输出是一个包含各个线段的数组。

OpenCV 直线检测分为三种方法,这三种方法的代码在 test_line.ipynb 中:

霍尔变换找直线

这个写在了另一篇文章中;返回的是一个 ndarray,大小是 (n, 1, 4),即各个线段的起始和终止点。

LSD 直线检测

来自文章 LSD:a Line Segment Detector,这个方法不需要调参,非常舒服。OpenCV 中 line_descritpor 模块的 LSDDetector 类来完成此方法。创建这个类的对象,然后使用 detect 即可完成。

虽然说 LSD 不需要调参,但其实也涉及到金字塔选几层等这个简单的参数,在创建对象时,不传参数就是使用默认参数,否则传入是一个 LSDParam 对象,其本质也就是包含几个参数的数组。detect 同理,如果不传参数就使用对象的参数。

最后得到包含了一堆 KeyLines 对象的数组,每个 KeyLines 就是线段,遍历即可。其实也很直观,getStartPointgetEndPoint 分别返回起始点。KeyLines 中 octave 表示在哪一层金字塔可以找到这条线段,所以遍历的时候通常会判断当前的 octave 是否为 0,以此确保这条线段在原图上可以找到。可以在 test_line.ipynb 查看具体用法。

EDLine 直线检测

方法来自 2011 年的文章 EDLines: A real-time line segment detector with a false detection control,和 LSD 一样,不需要调参。这个方法本质上是从 LSD 方法发展而来的,速度更快。

OpenCV 中 line_descritpor 模块的 BinaryDescritpor 类来完成此方法。创建这个类的对象,然后使用 detect 即可完成。

这里再细节说一下,这里虽然调用的类叫做 BinaryDescriptor,和 EDLine 好像完全没关系,不像上面的 LSD 方法调用的就叫做 LSDDetector。但其实这里源码中使用 detect 最后会用到一个函数:

int BinaryDescriptor::EDLineDetector::EDline( cv::Mat &image, LineChains &lines )

没错,这里的 EDlineDetector 是 BinaryDescriptor 里面的一个子类,被封装起来了,而不是像 LSDDetector 直接暴露给 cv2.line_descriptor,那为什么二者选择了不同的方式呢,其实这就是开源项目不同分工的锅:
1. 有一些人看到有 LSD 直线检测方法,于是写了代码,被放在了 line_descriptor 这个 module 中。但是并没有人去实现 EDLines 这个方法的代码。
2. 有一天有人想实现 LBD 直线描述方法(见下面的那个大章节),这个 LBD 方法论文中用的都是 EDLines 检测的直线,所以他们需要先把 EDLines 实现,也很自然地把 EDLines 放在 LBD 直线描述方法中。最后 LBD 也被放在了 line_descriptor 这个 module 中,而 EDLines 则是 LBD 里面的一个方法。

其实本质还是【LSD 直线检测方法】和【LBD 直线描述方法】这两个不同类型的东西被放在 line_descriptor 当成同级来对待了。似乎用一个叫做 line 的 module,里面有 detector 和 descriptor 两个子 module 更合理一些。

最后结果和 LSD 类似,也是包了一堆 KeyLines 对象的数组。KeyLines 用法如上面 LSD 检测所述。

Fast line Detector

ximgproc 的方法,传入参数建立对象,然后调用方法就行。代码在 test_line.ipynb 第二个 block 中。

原始论文:

Jin Han Lee, Sehyung Lee, Guoxuan Zhang, Jongwoo Lim, Wan Kyun Chung, and Il Hong Suh. Outdoor place recognition in urban environments using straight lines. In 2014 IEEE International Conference on Robotics and Automation (ICRA), pages 5550–5557\. IEEE, 2014.

EdgeDrawing

ximgproc 的方法,不仅可以检测直线,也可以检测边缘。同样也是传入参数建立对象,然后调用方法。代码在 test_line.ipynb 第二个 block 中。

这里再多说一点,上面的 EDLines 直线检测方法一开始会执行 EdgeDrawing 方法,但它调用的是它自己内部实现的方法:

cv2.linedescriptor: int BinaryDescriptor::EDLineDetector::EdgeDrawing( cv::Mat &image, EdgeChains &edgeChains )

而这里说的 EdgeDrawing 调用的是 cv2.ximgproc 模块的方法,所以其实这里相当于重复劳动,即 OpenCV 项目中有冗余。但这也是一个大项目很难规避的事情。

原始论文:

Cihan Topal and Cuneyt Akinlar. Edge drawing: a combined real-time edge and segment detector. Journal of Visual Communication and Image Representation, 23(6):862–872, 2012.

直线描述

直线描述是用到叫做 LBD 方法,这个就是 line_descriptor 介绍页中大段介绍的内容,具体原理这里不谈。

OpenCV 中 line_descritpor 模块的 BinaryDescritpor 类来完成此方法。前面提到这个类可以用 detect 进行 EDLine 直线检测;而用 compute 则可以进行这里所说的直线描述。

直线匹配

直线匹配和特征匹配类似,不过特征匹配是特征点,直线匹配是线段。OpenCV 中 line_descritpor 模块的 BinaryDescriptorMatcher 类来完成此方法。创建这个类的对象,然后使用 match 即可完成。和特征点匹配的 BruteForceMatcher 和 FlannBasedMatcher 一样,有三种 match 方法:matchknnMatchradiusMatch,用法和特征点匹配一样。

代码实现

具体能跑的代码在 test_line.ipynb 中,这里进行选择性摘抄:

'''
完整的 检测直线 -> 描述直线 -> 匹配直线 流程
'''

# 只保留 octave 为 0 的直线
def get_octave_0(kls, dss):
    nkls, ndss = [], []
    for i, kl in enumerate(kls):
        if kl.octave == 0:
            nkls.append(kls[i])
            ndss.append(dss[i])
    return tuple(nkls), np.array(ndss)

detector = cv2.line_descriptor.BinaryDescriptor.createBinaryDescriptor()

keylines1 = detector.detect(src)
keylines1, descriptors1 = detector.compute(src, keylines1)
keylines1, descriptors1 = get_octave_0(keylines1, descriptors1)

# 不想找其他图片了,就重复对一张图片进行匹配吧...
src2 = np.copy(src)
keylines2 = detector.detect(src2)
keylines2, descriptors2 = detector.compute(src2, keylines2)
keylines2, descriptors2 = get_octave_0(keylines2, descriptors2)

# 匹配,有三种方法,但很遗憾 radiusMatch 没有 python 版本..
# BinaryDescriptorMacher,有些方法没有 python 版本,如 add 等,这个类本身继承自 DescriptorMatcher,和之前点的匹配基本差不多
MATCHES_DIST_THRESHOLD = 25
matcher = cv2.line_descriptor.BinaryDescriptorMatcher()
matches = matcher.match(descriptors1, descriptors2)
# matches = matcher.knnMatch(descriptors1, descriptors2, k=2)
# matches = matcher.radiusMatch(descriptors1, descriptors2, maxDistance=25)
matches = [m for m in matches if m.distance < MATCHES_DIST_THRESHOLD]

# 返回的时 DMatch,自行查阅相关方法
print(type(matches)) 

# 画图
matchimg1 = cv2.line_descriptor.drawLineMatches(
                img1=src, keylines1=keylines1,
                img2=src2, keylines2=keylines2,
                matches1to2=matches,
                # opencv python 版本的 bug,必须要添加 matchesMask(which matches must be drawn),否则报 Unkown C++ error
                matchesMask=np.ones(len(matches), dtype=np.uint8)
            )

show_images([('match', matchimg1)])

Comments