一、换向器缺陷检测

01工作原理

1. 运动

通过多种机构方案将前道工序的产品取至检测设备中,产品在三个检测工位中流转检测,检测完成后,产品摆盘或剔除至不良盒中。

2. 触发、拍照

PLC通过IO输出方式触发相机拍照,检测软件收到相机拍摄产品的图像。

3. 算法检测

检测软件在收到图像后,经过一系列的处理与检测,根据参数的设定对产品进行结果判定。

4. 分拣、摆盘

PLC根据检测软件的结果判定,对产品进行分类摆放和剔除。

02产品特点

工位多光源方案,最大化检测项,最小化检测工位体积

高亮条光、蓝色背光、红色激光结合:

检测表明缺陷的同时,切换不同光源可以检测冲钩废、产品高度。

白色背光、双环光结合:

检测底部电木掉块同时,切换环光可以检测槽部、钩部缺陷。

白色环光、白色背光结合:

测量顶部电木掉块同时,切换白色背光可以测量钩部长度、角度、宽度等缺陷。

传统算法+深度学习双重方法检测,对产线环境和物料有更强的鲁棒性

51c视觉~CV~合集5_视觉

51c视觉~CV~合集5_视觉_02

针对部分缺陷,传统算法无法直观、稳定地检测到特征时,本检测方案会是使用深度学习进行检测或测量,以达到更好的或传统算法无法实现的效果。

自动取料、摆料

51c视觉~CV~合集5_视觉_03

视觉检测设备可以产线连线通讯,摆料。减轻员工工作量,提高工作效率。

一机多测

51c视觉~CV~合集5_视觉_04

视觉检测设备可以与电气检测共同使用。

例如:片轴检测、片间检测,内径检测,实现一机多测的功能。一台设备可以实现电气、外观全方位检测,在有限的设备空间内,最大化检测效果。

ERP、MES、看板集成

51c视觉~CV~合集5_视觉_05

视觉检测软件实时记录产品的检测记录,为后期通过API或数据库等方式与各系统集成提供可能。方便对产品质量及检测效果进行直观地分析。

03检测效果展示

铜排损伤检测

51c视觉~CV~合集5_视觉_06

铜排表面损伤是换向器常见缺陷,但由于其出现位置随机、颜色不一。使用传统算法会因物料和设备等原因,需频繁调整参数。使用深度学习可以在必选反复调参的前提下,实现很好的检测效果。

铜排内陷检测

51c视觉~CV~合集5_视觉_07

铜排内陷多出现于铜排底部,其呈现颜色为亮色或暗色,因此使用传统算法较难检测。使用深度学习可以比较稳定地获取此特征。

铜排缺角检测

51c视觉~CV~合集5_视觉_08

铜排缺角位于铜排底部角落,其难点在与:如何保证检测效果的前提下,如何降低误检。使用数据集增强方法可以提高检测准确率并降低误检。

铜排圆边检测

51c视觉~CV~合集5_视觉_09

铜排圆边缺陷位于铜排一侧,往往伴随铜排铜排肩部电木粉缺陷出现。此种缺陷均使用深度学习的目标检测方法检测。

铜排肩部电木粉检测

51c视觉~CV~合集5_视觉_10

槽内异物检测

51c视觉~CV~合集5_视觉_11

51c视觉~CV~合集5_视觉_12

槽内余料检测使用传统算法和深度学习共同检测,双重检测保证检测效果。

此方法可用于槽内余料、毛刺等异物检测。

冲钩废检测

51c视觉~CV~合集5_视觉_13

冲钩废缺陷在点光源照射下,高度异于正常钩。

底部槽封料检测

51c视觉~CV~合集5_视觉_14

底面在测量是否封料的同时,还检测钩、槽角度信息。

底部电木检测

51c视觉~CV~合集5_视觉_15

底面电木掉块由于缺陷特征与真实电木相差较小、不同批次电木颜色呈现差异,使用传统算法无法检测。使用深度学习可以获得很好的检测效果。

顶部电木检测

51c视觉~CV~合集5_视觉_16

51c视觉~CV~合集5_视觉_17

顶面电木掉块相对于底面电木掉块,掉块特征可以较好地区分,检测难度较低。使用传统算法可以轻松、准确地检测。

顶面工位利用背光,可以检测钩宽、钩钩夹角、钩尖外径等信息。




二、图像主题色提取算法

许多从自然场景中拍摄的图像,其色彩分布上会给人一种和谐、一致的感觉;反过来,在许多界面设计应用中,我们也希望选择的颜色可以达到这样的效果,但对一般人来说却并不那么容易,这属于色彩心理学的范畴(当然不是指某些伪神棍所谓的那种)。从彩色图像中提取其中的主题颜色,不仅可以用于色彩设计(参考网站:Design Seeds),也可用于图像分类、搜索、识别等,本文分别总结并实现图像主题颜色提取的几种算法,包括颜色量化法(Color Quantization)、聚类(Clustering)和颜色建模的方法(颜色建模法仅作总结),源码可见:GitHub: ImageColorTheme。

1. 颜色量化算法

彩色图像一般采用RGB色彩模式,每个像素由RGB三个颜色分量组成。随着硬件的不断升级,彩色图像的存储由最初的8位、16位变成现在的24位、32真彩色。所谓全彩是指每个像素由8位(28=0~255)表示,红绿蓝三原色组合共有1677万(256∗256∗256)万种颜色,如果将RGB看作是三维空间中的三个坐标,可以得到下面这样一张色彩空间图:

51c视觉~CV~合集5_视觉_18

当然,一张图像不可能包含所有颜色,我们将一张彩色图像所包含的像素投射到色彩空间中,可以更直观地感受图像中颜色的分布:

51c视觉~CV~合集5_视觉_19

因此颜色量化问题可以用所有矢量量化(vector quantization, VQ)算法解决。这里采用开源图像处理库 Leptonica 中用到的两种算法:中位切分法、八叉树算法。

1.1. 中位切分法(Median cut)

GitHub: color-theif 项目采用了 Leptonica 中的用到的(调整)中位切分法,Js 代码比 C 要易读得多。中位切分算法的原理很简单直接,将图像颜色看作是色彩空间中的长方体(VBox),从初始整个图像作为一个长方体开始,将RGB中最长的一边从颜色统计的中位数一切为二,使得到的两个长方体所包含的像素数量相同,重复上述步骤,直到最终切分得到长方体的数量等于主题颜色数量为止。

Leptonica 作者在报告 Median-Cut Color Quantization 中总结了这一算法存在的一些问题,其中主要问题是有可能存在某些条件下 VBox 体积很大但只包含少量像素。解决的方法是,每次进行切分时,并不是对上一次切分得到的所有VBox进行切分,而是通过一个优先级队列进行排序,刚开始时这一队列以VBox仅以VBox所包含的像素数作为优先级考量,当切分次数变多之后,将体积*包含像素数作为优先级。

Python 3 中内置了PriorityQueue:

from queue import PriorityQueue as PQueueclass VBox(object):  
  def __init__(self, r1, r2, g1, g2, b1, b2, histo):
    self.vol = calV()
    self.npixs = calN()
    self.priority = self.npixs * -1 # PQueue 是按优先级自小到大排序boxQueue.put((vbox0.priority, vbox0))

vbox.priority *= vbox.vol  
boxQueue.put((vbox0.priority, vbox0))

除此之外,算法中最重要的部分是统计色彩分布直方图。我们需要将三维空间中的任意一点对应到一维坐标中的整数,这样才能以最快地速度定位这一颜色。如果采用全部的24位信息,那么我们用于保存直方图的数组长度至少要是224=16777216,既然是要提取颜色主题(或是颜色量化),我们可以将颜色由RGB各8位压缩至5位,这样数组长度只有215=32768:

def getColorIndex(self, r, g, b):  
    return (r << (2 * self.SIGBITS)) + (g << self.SIGBITS) + bdef getPixHisto(self):  
    pixHisto = np.zeros(1 << (3 * self.SIGBITS))    for y in range(self.h):        for x in range(self.w):
            r = self.pixData[y, x, 0] >> self.rshift
            g = self.pixData[y, x, 1] >> self.rshift
            b = self.pixData[y, x, 2] >> self.rshift

            pixHisto[self.getColorIndex(r, g, b)] += 1
    return pixHisto

分别对4张图片进行切分、提取:

def testMMCQ(pixDatas, maxColor):  
    start  = time.process_time()
    themes = list(map(lambda d: MMCQ(d, maxColor).quantize(), pixDatas))
    print("MMCQ Time cost: {0}".format(time.process_time() - start))    return themes
imgs = map(lambda i: 'imgs/photo%s.jpg' % i, range(1,5))  
pixDatas = list(map(getPixData, imgs))  
maxColor = 7themes = [testMMCQ(pixDatas, maxColor)]  
imgPalette(pixDatas, themes, ["MMCQ Palette"])

51c视觉~CV~合集5_视觉_20

1.2. 八叉树算法(Octree)

八叉树算法的原理可以参考这篇文章:圖片主題色提取算法小結。作者也提供了 Js 实现的代码,虽然与 Leptonica 中 C 实现的方法差别很大,但原理上是一致的。

建立八叉树的原理实际上跟上面提到的统计直方图有些相似,将颜色成分转换成二进制之后,较低位(八叉树中位置较深层)数值将被压缩进较高位(八叉树中较浅层)。八叉树算法应用到主题色提取可能存在的问题是,每次削减掉的叶子数不确定,但是新增加的只有一个,这就导致我们需要的主题色数量并不一定刚好得到满足,例如设定的主题色数量为7,可能上一次叶子时总数还有10个,到了下一次只剩5个了。类似的问题在后面手动实现的KMeans算法中也有出现,为了保证可以得到足够的主题色,不得不强行提高算法中的颜色数量,然后取图像中包含数量较多的作为主题色:

def getColors(self, node):  
      if node.isLeaf:
          [r, g, b] = list(map(lambda n: int(n[0] / n[1]), zip([node.r, node.g, node.b], [node.n]*3)))
          self.theme.append([r,g,b, node.n])      else:          for i in range(8):              if node.children[i] is not None:
                  self.getColors(node.children[i])
self.theme = sorted(self.theme, key=lambda c: -1*c[1])  return list(map(lambda l: l[:-1],self.theme[:self.maxColor]))

对比上面两种算法的结果:

def testOQ(pixDatas, maxColor):  
    start  = time.process_time()
    themes = list(map(lambda d: OQ(d, maxColor).quantize(), pixDatas))
    print("OQ Time cost: {0}".format(time.process_time() - start))    return themes
themes = [testMMCQ(pixDatas, maxColor), testOQ(pixDatas, maxColor)]  
imgPalette(pixDatas, themes, ["MMCQ Palette", "OQ Palette"])

51c视觉~CV~合集5_视觉_21

可见八叉树算法可能更适合用于提取调色板,而且两种算法运行时间差异也很明显:

#MMCQ Time cost: 8.238793#OQ Time cost: 55.173573

除了OQ中采用较多递归以外,未对原图进行抽样处理也是其中原因之一。

2. 聚类

聚类是一种无监督式机器学习算法,我们这里采用K均值算法。虽然说是“机器学习”听起来时髦些,但算法本质上比上面两种更加简单粗暴。

KMeans算法

KMeans算法的原理更加简洁:“物以类聚”。我们目的是将一堆零散的数据(如上面图2)归为k个类别,使得每个类别中的每个数据样本,距离该类别的中心(质心,centroid)距离最小,数学公式为:

51c视觉~CV~合集5_视觉_22

上文提到八叉树算法可能出现结果与主题色数量不一致的情况,在KMeans算法中,初始的k个类别的质心的选择也可能导致类似的问题。当采用随机选择的方法时,有可能出现在迭代过程中,选择的中心点距离所有其它数据太远而最终导致被孤立。这里分别采用手动实现和scikit-learn的方法实现,根据scikit-learn 提供的API,完成主题色的提取大概只需要几行代码:

from sklearn.cluster import KMeans as KM  

import numpy as np

#@pixData      image pixels stored in numpy.ndarray

#@maxColor     theme color number

h, w, d = pixData.shape  

data = np.reshape((h*w, d))  

km = KM(n_clusters=maxColor)  

km.fit(data)  

theme = np.array(km.cluster_centers_, dtype=np.uint8)



imgs = map(lambda i: 'imgs/photo%s.jpg' % i, range(1,5))  
pixDatas = list(map(getPixData, imgs))  

maxColor = 7  

themes = [testKmeans(pixDatas, maxColor), testKmeans(pixDatas, maxColor, useSklearn=False)]  imgPalette(pixDatas, themes, ["KMeans Palette", "KMeans DIY"])

测试比较手动实现和scikit-learn的结果如下:

好吧我承认很惨,耗时方面也是惨不忍睹。

3. 色彩建模

从上面几种算法结果来看,MMCQ和 KMeans在时间和结果上都还算不错,但仍有改进的空间。如果从人类的角度出发,两种算法的策略或者说在解决主题色提取这一问题时采纳的特征(feature)都接近于颜色密度,即相近的颜色凑在一起数量越多,越容易被提取为主题颜色。

最后要提到的算法来自斯坦福可视化组13年的一篇研究:Modeling how people extract color themes from images,实际上比较像一篇心理学研究的套路:建模-找人类被试进行行为实验-调参拟合。文章提取了图像中的79个特征变量并进行多元回归,同时找到普通人类被试和艺术系学生对图像的主题颜色进行选择,结果证明特征+回归能够更好地拟合人类选择的结果。

79个特征的多元回归模型,不知道会不会出现过度拟合?另外虽然比前面算法多了很多特征,但仍旧多物理特征。对人类观察者来说,我们看到的并非一堆无意义的色块,虽然有研究表明颜色信息并非场景识别的必要线索,但反过来场景图像中的语义信息却很有可能影响颜色对观察者的意义,这大概就是心理学研究与计算机科学方向上的差异。

总结

以上算法若要应用还需更多优化,例如先抽样再处理,计算密集的地方用C/C++或并行等。另外需要一个对Python每个函数执行时间进行记录的工具,分析运行时间长的部分。




三、流水线包装箱检测计数

主要介绍基于OpenCV的流水线包装箱检测计数应用

资源下载

    完整代码和视频下载地址:

https://github.com/freedomwebtech/rpi4-conveyor-belt-boxces-counter

51c视觉~CV~合集5_视觉_23

核心代码如下(cboxtest.py):

import cv2
import numpy as np
from tracker import*




cap=cv2.VideoCapture('box.mp4')
lower_range=np.array([0,46,64])
upper_range=np.array([43,115,160])


def RGB(event, x, y, flags, param):
    if event == cv2.EVENT_MOUSEMOVE :  
        point = [x, y]
        print(point)
  
        
tracker=Tracker()
cv2.namedWindow('box-counter')
cv2.setMouseCallback('box-counter', RGB)
area=[(391,244),(370,443),(391,438),(410,237)]
counter=[]
while True:
    ret,frame=cap.read()
    if not ret:
        break
    frame=cv2.resize(frame,(640,480))
    hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)
    mask=cv2.inRange(hsv,lower_range,upper_range)
    _,mask1=cv2.threshold(mask,254,255,cv2.THRESH_BINARY)
    cnts,_=cv2.findContours(mask1,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)
    list=[]
    for c in cnts:
        x=500
        if cv2.contourArea(c)>x:
            x,y,w,h=cv2.boundingRect(c)
            list.append([x,y,w,h])
    bbox_idx=tracker.update(list)
    for bbox in bbox_idx:
        x1,y1,w1,h1,id=bbox
        cx=int(x1+x1+w1)//2
        cy=int(y1+y1+h1)//2
            
        results=cv2.pointPolygonTest(np.array(area,np.int32),((cx,cy)),False)
        if results>=0:
           cv2.circle(frame,(cx,cy),4,(0,0,255),-1)
           cv2.rectangle(frame,(x1,y1),(x1+w1,y1+h1),(0,255,0),2)
           if counter.count(id)==0:
              counter.append(id)
    cv2.polylines(frame,[np.array(area,np.int32)],True,(255,255,255),2)
    c1=(len(counter)) 


    #cvzone.putTextRect(frame,f"{c1}",(50,60),2,2)
    cv2.putText(frame,str(c1), (420, 350), 0, 3, (0, 0, 255), 4)
    cv2.imshow("box-counter",frame)
    if cv2.waitKey(1)&0xFF==27:
        break
cap.release()
cv2.destroyAllWindows()

    下载测试视频box6.mp4(vid.txt中有链接):

51c视觉~CV~合集5_视觉_24

实现步骤

  【1】通过track.py滑动条动态设置HSV范围,保证较好的提取去包装箱的轮廓mask,效果如下:

51c视觉~CV~合集5_视觉_25

    通过调试设置HSV范围如下,然后做HSV轮廓提取,提取纸箱轮廓。

lower_range=np.array([0,46,64])
upper_range=np.array([43,115,160])

 【2】划定多边形区域,当直线轮廓中心点经过时将目标跟踪的id添加到list中:

51c视觉~CV~合集5_视觉_26

51c视觉~CV~合集5_视觉_27

  【3】纸箱计数:计算list中元素个数即可,具体原理可参考上篇文章:

基于OpenCV+YOLOv5实现车辆跟踪与计数(附源码)

51c视觉~CV~合集5_视觉_28

  最终效果如下:

51c视觉~CV~合集5_视觉_29

总 结

    此应用相对基于OpenCV+YOLOv5实现车辆跟踪与计数(附源码) 案例简单一点,计数原理相同。这里直接用HSV范围提取的纸箱目标,没有用深度学习目标检测方法。另外计数时也不一定使用多边形,以直线和点的距离来计算也可以,核心还是避免重复计数。

    另外实际流水线上,这种简单应用还用不到视觉,红外传感器+单片机就可以搞定了,此例仅供参考。




四、修改一行代码,将图像匹配效果提升14%

OpenCV 4.5.1中最令人兴奋的特性之一是BEBLID (Boosted Efficient Binary Local Image Descriptor),一个新的描述符能够提高图像匹配精度,同时减少执行时间!这篇文章将向你展示这个魔法是如何实现的。所有的源代码都在这个GitHub库中:https://github.com/iago-suarez/beblid-opencv-demo/blob/main/demo.ipynb

在这个例子中,我们将匹配这两个视角不一样的图像:

51c视觉~CV~合集5_视觉_30

首先,确保安装了正确的OpenCV版本是很重要的。在你喜欢的环境中,你可以通过以下方式安装并检查OpenCV Contrib版本:

pip install "opencv-contrib-python>=4.5.1"
python
>>> import cv2 as cv
>>> print(f"OpenCV Version: {cv.__version__}")
OpenCV Version: 4.5.1

在Python中加载这两个图像所需的代码是:

import cv2 as cv

# Load grayscale images
img1 = cv.imread("graf1.png", cv.IMREAD_GRAYSCALE)
img2 = cv.imread("graf3.png", cv.IMREAD_GRAYSCALE)

if img1 is None or img2 is None:
    print('Could not open or find the images!')
    exit(0)

为了评估我们的图像匹配程序,我们需要在两幅图像之间进行正确的(即ground truth)几何变换。它是一个称为单应性的3x3矩阵,当我们从第一个图像中乘以一个点(在齐次坐标中)时,它返回第二个图像中这个点的坐标。加载这个矩阵:

# Load homography (geometric transformation between image)
fs = cv.FileStorage("H1to3p.xml", cv.FILE_STORAGE_READ)
homography = fs.getFirstTopLevelNode().mat()
print(f"Homography from img1 to img2:\n{homography}")

下一步是检测图像中容易在其他图像中找到的部分:Local image features。在本例中,我们将使用ORB,一个快速可靠的检测器来检测角点。ORB检测到强角,在不同的尺度上比较它们,并使用FAST或Harris响应来挑选最好的。它还使用局部patch的一阶矩来寻找每个角点的方向。我们检测每个图像中最多10000个角点:

detector = cv.ORB_create(10000)
kpts1 = detector.detect(img1, None)
kpts2 = detector.detect(img2, None)

在下面的图片中,你可以看到500个用绿点标记的检测响应最强的角点特征:

51c视觉~CV~合集5_视觉_31

很好,现在是时候以一种我们可以在另一张图中找到它们的方式来表示这些关键点了。这个步骤被称为description,因为每个角点的局部patch中的纹理表示 为图像上不同操作得到的数字的向量。有很多的描述符可以用,但如果我们想要一些精确的东西,即使在移动电话或低功耗设备上也能实时运行,OpenCV有两个重要的方法:

  • ORB(导向快速和旋转简短):一个经典的方法,有10年的历史,工作相当好。
  • BEBLID (Boosted Efficient Binary Local Image Descriptor):2020年引入的一个新的描述符,已被证明在几个任务中改善了ORB。由于BEBLID适用于多种检测方法,所以必须将ORB关键点的比例设置为0.75~1。
# Comment or uncomment to use ORB or BEBLID
descriptor = cv.xfeatures2d.BEBLID_create(0.75)
# descriptor = cv.ORB_create()
kpts1, desc1 = descriptor.compute(img1, kpts1)
kpts2, desc2 = descriptor.compute(img2, kpts2)

现在可以匹配这两个图像的描述符来建立对应关系了。让我们使用暴力求解算法,它基本上比较了第一张图像中的每个描述符和第二张图像中的所有描述符。当我们处理二进制描述符时,使用汉明距离进行比较,即计算每对描述符之间不同的比特数。

这里还使用了一个叫做比率检验的小技巧。它不仅确保描述符1和2彼此相似,而且确保没有其他像2一样接近1的描述符。

matcher = cv.DescriptorMatcher_create(cv.DescriptorMatcher_BRUTEFORCE_HAMMING)
nn_matches = matcher.knnMatch(desc1, desc2, 2)
matched1 = []
matched2 = []
nn_match_ratio = 0.8  # Nearest neighbor matching ratio
for m, n in nn_matches:
    if m.distance < nn_match_ratio * n.distance:
        matched1.append(kpts1[m.queryIdx])
        matched2.append(kpts2[m.trainIdx])

因为我们知道正确的几何变换,让我们检查有多少匹配是正确的(inliners)。如果图像2中的点和从图像1投射到图像2的点距离小于2.5像素,我们认为匹配是有效的。

inliers1 = []
inliers2 = []
good_matches = []
inlier_threshold = 2.5  # Distance threshold to identify inliers with homography check
for i, m in enumerate(matched1):
    # Create the homogeneous point
    col = np.ones((3, 1), dtype=np.float64)
    col[0:2, 0] = m.pt
    # Project from image 1 to image 2
    col = np.dot(homography, col)
    col /= col[2, 0]
    # Calculate euclidean distance
    dist = sqrt(pow(col[0, 0] - matched2[i].pt[0], 2) + pow(col[1, 0] - matched2[i].pt[1], 2))
    if dist < inlier_threshold:
        good_matches.append(cv.DMatch(len(inliers1), len(inliers2), 0))
        inliers1.append(matched1[i])
        inliers2.append(matched2[i])

现在我们在inliers1和inliers2变量中有了正确的匹配,我们可以使用cv.drawMatches定性地评估结果。每一个对应点可以在更高级别的任务上对我们有帮助,比如homography estimation,Perspective-n-Point, plane tracking, real-time pose estimation 以及 images stitching。

51c视觉~CV~合集5_视觉_32

由于很难定性地比较这种结果,让我们绘制一些定量的评价指标。最能反映描述符可靠程度的指标是inlier的百分比:

51c视觉~CV~合集5_视觉_33

Matching Results (BEBLID)
*******************************
# Keypoints 1:                          9105
# Keypoints 2:                          9927
# Matches:                              660
# Inliers:                              512
# Percentage of Inliers:                77.57%

使用BEBLID描述符获得77.57%的inliers。如果我们在描述符部分注释掉BEBLID并取消注释ORB描述符,结果下降到63.20%

# Comment or uncomment to use ORB or BEBLID
# descriptor = cv.xfeatures2d.BEBLID_create(0.75)
descriptor = cv.ORB_create()
kpts1, desc1 = descriptor.compute(img1, kpts1)
kpts2, desc2 = descriptor.compute(img2, kpts2)
Matching Results (ORB)
*******************************
# Keypoints 1:                          9105
# Keypoints 2:                          9927
# Matches:                              780
# Inliers:                              493
# Percentage of Inliers:                63.20%

总之,只需更改一行代码,将ORB描述符替换为BEBLID ,就可以将这两个图像的匹配结果提高14%。这在需要局部特征匹配的高级任务中会产生很大影响,所以不要犹豫,试试BEBLID

英文原文:https://towardsdatascience.com/improving-your-image-matching-results-by-14-with-one-line-of-code-b72ae9ca2b73




五、实时弯道检测

主要介绍如何使用 Python 和 OpenCV实现一个实时曲线道路检测系统。

背景介绍

    在任何驾驶场景中,车道线都是指示交通流量和车辆应行驶位置的重要组成部分。这也是开发自动驾驶汽车的一个很好的起点!在我之前的车道检测项目的基础上,我实现了一个曲线车道检测系统,该系统工作得更好,并且对具有挑战性的环境更加稳健。车道检测系统是使用 OpenCV 库用 Python 编写的。    

下面是实现步骤:

  • 畸变校正
  • 透视变换
  • Sobel滤波
  • 直方图峰值检测
  • 滑动窗口搜索
  • 曲线拟合
  • 覆盖检测车道
  • 应用于视频
畸变矫正

    相机镜头扭曲入射光以将其聚焦在相机传感器上。尽管这对于我们捕捉环境图像非常有用,但它们最终往往会稍微不准确地扭曲光线。这可能导致计算机视觉应用中的测量不准确。然而,我们可以很容易地纠正这种失真。

    我们可以使用棋盘格来标定相机然后做畸变校正:

51c视觉~CV~合集5_视觉_34

    测试视频中使用的相机用于拍摄棋盘格的 20 张照片,用于生成畸变模型。我们首先将图像转换为灰度,然后应用cv2.findChessboardCorners()函数。我们已经知道这个棋盘是一个只有直线的二维对象,所以我们可以对检测到的角应用一些变换来正确对齐它们。用 cv2.CalibrateCamera() 来获取畸变系数和相机矩阵。相机已校准!

    然后,您可以使用它cv2.undistort()来矫正其余的输入数据。您可以在下面看到棋盘的原始图像和校正后的图像之间的差异:

51c视觉~CV~合集5_视觉_35

实现代码:

def undistort_img():
    # Prepare object points 0,0,0 ... 8,5,0
    obj_pts = np.zeros((6*9,3), np.float32)
    obj_pts[:,:2] = np.mgrid[0:9, 0:6].T.reshape(-1,2)
    # Stores all object points & img points from all images
    objpoints = []
    imgpoints = []
    # Get directory for all calibration images
    images = glob.glob('camera_cal/*.jpg')
    for indx, fname in enumerate(images):
        img = cv2.imread(fname)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        ret, corners = cv2.findChessboardCorners(gray, (9,6), None)
        if ret == True:
            objpoints.append(obj_pts)
            imgpoints.append(corners)
    # Test undistortion on img
    img_size = (img.shape[1], img.shape[0])
    # Calibrate camera
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img_size, None,None)
    dst = cv2.undistort(img, mtx, dist, None, mtx)
    # Save camera calibration for later use
    dist_pickle = {}
    dist_pickle['mtx'] = mtx
    dist_pickle['dist'] = dist
    pickle.dump( dist_pickle, open('camera_cal/cal_pickle.p', 'wb') )
def undistort(img, cal_dir='camera_cal/cal_pickle.p'):
    #cv2.imwrite('camera_cal/test_cal.jpg', dst)
    with open(cal_dir, mode='rb') as f:
        file = pickle.load(f)    mtx = file['mtx']
    dist = file['dist']
    dst = cv2.undistort(img, mtx, dist, None, mtx)
    return dst
undistort_img()
img = cv2.imread('camera_cal/calibration1.jpg')
dst = undistort(img) # Undistorted image

    这是应用于道路图像的失真校正。您可能无法注意到细微的差异,但它会对图像处理产生巨大影响。

51c视觉~CV~合集5_视觉_36

透视变换

    在相机空间中检测弯曲车道并不是很容易。如果我们想鸟瞰车道怎么办?这可以通过对图像应用透视变换来完成。这是它的样子:

51c视觉~CV~合集5_视觉_37

    注意到什么了吗?通过假设车道位于平坦的 2D 表面上,我们可以拟合一个多项式,该多项式可以准确地表示车道空间中的车道!这不是很酷吗?

    您可以使用cv2.getPerspectiveTransform()函数将这些变换应用于任何图像,以获取变换矩阵,并将cv2.warpPerspective()其应用于图像。下面是代码:

def perspective_warp(img,
                     dst_size=(1280,720),
                     src=np.float32([(0.43,0.65),(0.58,0.65),(0.1,1),(1,1)]),
                     dst=np.float32([(0,0), (1, 0), (0,1), (1,1)])):
    img_size = np.float32([(img.shape[1],img.shape[0])])
    src = src* img_size
    # For destination points, I'm arbitrarily choosing some points to be
    # a nice fit for displaying our warped result
    # again, not exact, but close enough for our purposes
    dst = dst * np.float32(dst_size)
    # Given src and dst points, calculate the perspective transform matrix
    M = cv2.getPerspectiveTransform(src, dst)
    # Warp the image using OpenCV warpPerspective()
    warped = cv2.warpPerspective(img, M, dst_size)
    return warped
Sobel滤波

   在之前的版本中,我使用颜色过滤掉了车道线。然而,这并不总是最好的选择。如果道路使用浅色混凝土代替沥青,道路很容易通过彩色滤光片,管道会将其感知为白色车道线,此方法不够稳健。

    相反,我们可以使用类似于边缘检测器的方法,这次过滤掉道路。车道线通常与道路具有高对比度,因此我们可以利用这一点。之前版本 1 中使用的Canny边缘检测器利用Sobel 算子来获取图像函数的梯度。OpenCV 文档对它的工作原理有很好的解释。我们将使用它来检测高对比度区域以过滤车道标记并忽略道路。

    我们仍将再次使用 HLS 色彩空间,这一次是为了检测饱和度和亮度的变化。sobel 算子应用于这两个通道,我们提取相对于 x 轴的梯度,并将通过梯度阈值的像素添加到表示图像中像素的二进制矩阵中。这是它在相机空间和车道空间中的样子:

51c视觉~CV~合集5_视觉_38

51c视觉~CV~合集5_视觉_39

    请注意,远离相机的图像部分不能很好地保持其质量。由于相机的分辨率限制,来自更远物体的数据非常模糊和嘈杂。我们不需要专注于整个图像,所以我们可以只使用它的一部分。这是我们将使用的图像的样子(ROI):

51c视觉~CV~合集5_视觉_40

直方图峰值检测

   我们将应用一种称为滑动窗口算法的特殊算法来检测我们的车道线。但是,在我们应用它之前,我们需要为算法确定一个好的起点。如果它从存在车道像素的位置开始,它会很好地工作,但是我们如何首先检测这些车道像素的位置呢?其实很简单!

    我们将获得图像相对于 X 轴的直方图。下面直方图的每个部分都显示了图像每列中有多少个白色像素。然后我们取图像每一侧的最高峰,每条车道线一个。这是直方图的样子,在二值图像旁边:

51c视觉~CV~合集5_视觉_41

51c视觉~CV~合集5_视觉_42

滑动窗口搜索

   滑动窗口算法将用于区分左右车道边界,以便我们可以拟合代表车道边界的两条不同曲线。

    算法本身非常简单。从初始位置开始,第一个窗口测量有多少像素位于窗口内。如果像素数量达到某个阈值,它将下一个窗口移动到检测到的像素的平均横向位置。如果没有检测到足够的像素,则下一个窗口从相同的横向位置开始。这一直持续到窗口到达图像的另一边缘。

    落在窗口内的像素被赋予一个标记。在下图中,蓝色标记的像素代表右侧车道,红色标记的像素代表左侧:

51c视觉~CV~合集5_视觉_43

曲线拟合

   项目的其余部分非常简单。我们分别使用 对红色和蓝色像素应用多项式回归np.polyfit(),然后检测器就完成了!

    这是曲线的样子:

51c视觉~CV~合集5_视觉_44

绘制检测车道

   这是检测系统的最后一部分,用户界面!我们只需创建一个覆盖层来填充检测到的车道部分,然后我们最终可以将其应用于视频。一旦应用于视频检测,您应该会看到以下输出:

51c视觉~CV~合集5_视觉_45

结论

   就是这样,一个基本的弯曲车道检测器!它比以前的版本好得多,它甚至可以处理弯曲的车道!但是,它仍然会在一定程度上受到阴影和道路纹理剧烈变化的影响。在我的下一个车道检测项目中,我们将使用一些机器学习技术来开发一个非常强大的车道和车辆检测系统,谢谢!

完整代码:

https://github.com/kemfic/Curved-Lane-Lines/blob/master/P4.ipynb

参考链接:

https://www.hackster.io/kemfic/curved-lane-detection-34f771