来自Gimp程序的OpenCV Python脚本 - 草/硬表面边缘检测

Kil*_*One 5 python opencv computer-vision raspberry-pi

我想开发一个Python OpenCV脚本来复制/改进我开发的Gimp程序.该过程的目标是提供遵循草和硬表面之间的分界线的x,y点阵列.这个阵列将允许我完成我的500磅54"宽压力洗涤机器人,它有一个Raspberry Pi Zero(和相机),因此它可以以每秒几英寸的速度跟随该边缘.我将监控和/或者当我在沙发上看电视时,通过其wifi视频流和iPhone应用程序控制机器人.

这是一个原始图像样本(60x80像素):

在此输入图像描述

Gimp程序是:

  1. 将图像转换为索引的2种颜色.基本上是一边的草和另一边的砖或路面.DARN SHADOWS哎呀那是我:)

在此输入图像描述

  1. 在这两种颜色中,使用下面的棒设置在该值的像素上取较低的Hue值和魔杖.23的Hue设置是我如何移除阴影,15的羽毛设置是我如何删除岛屿/锯齿(裂缝中的草:).

在此输入图像描述

  1. 使用以下高级设置值对路径进行高级选择(默认值的更改为黄色).基本上我只想要线段,我的(x,y)点阵列将是黄色路径点.

在此输入图像描述

  1. 接下来,我将路径导出到.xml文件,我可以从中解析并隔离上图中的黄点.这是.xml文件:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
              "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">

<svg xmlns="http://www.w3.org/2000/svg"
     width="0.833333in" height="1.11111in"
     viewBox="0 0 60 80">
  <path id="Selection"
        fill="none" stroke="black" stroke-width="1"
        d="M 60.00,0.00
           C 60.00,0.00 60.00,80.00 60.00,80.00
             60.00,80.00 29.04,80.00 29.04,80.00
             29.04,80.00 29.04,73.00 29.04,73.00
             29.04,73.00 30.00,61.00 30.00,61.00
             30.00,61.00 30.00,41.00 30.00,41.00
             30.00,41.00 29.00,30.85 29.00,30.85
             29.00,30.85 24.00,30.85 24.00,30.85
             24.00,30.85 0.00,39.00 0.00,39.00
             0.00,39.00 0.00,0.00 0.00,0.00
             0.00,0.00 60.00,0.00 60.00,0.00 Z" />
</svg>
Run Code Online (Sandbox Code Playgroud)

我的Pi Zero上的OpenCV程序执行时间的目标是大约1-2秒或更短(目前需要约0.18秒).

我拼凑了一些结果,这些结果与Gimp xml文件中的相同点有关.我完全不确定它是否正在做Gimp关于蒙版色调范围的事情.我还没有弄清楚如何在面具上应用最小半径,我很确定当面具在硬面的边缘上作为面具的一部分获得"草"块时我将需要它.这是迄今为止的所有轮廓点(ptscanvas.bmp):

在此输入图像描述

截至美国东部时间7月6日下午5点08分,这是一个"仍然凌乱"的脚本,有点工作并找到了这些点;

import numpy as np
import time, sys, cv2

img = cv2.imread('2-60.JPG')
cv2.imshow('Original',img)
# get a blank pntscanvas for drawing points on 
pntscanvas = np.zeros(img.shape, np.uint8)

print (sys.version)  
if sys.version_info[0] < 3:
    raise Exception("Python 3 or a more recent version is required.")

def doredo():
    start_time = time.time()

    # Use kmeans to convert to 2 color image
    hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    Z = hsv_img.reshape((-1,3))
    Z = np.float32(Z)
    # define criteria, number of clusters(K) 
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
    K = 2
    ret,label,center=cv2.kmeans(Z,K,None,criteria,10,cv2.KMEANS_RANDOM_CENTERS)

    # Create a mask by selecting a hue range around the lowest hue of the 2 colors
    if center[0,0] < center[1,0]:
        hueofinterest = center[0,0]
    else:
        hueofinterest = center[1,0]
    hsvdelta = 8
    lowv = np.array([hueofinterest - hsvdelta, 0, 0])
    higv = np.array([hueofinterest + hsvdelta, 255, 255])
    mask = cv2.inRange(hsv_img, lowv, higv)

    # Extract contours from the mask
    ret,thresh = cv2.threshold(mask,250,255,cv2.THRESH_BINARY_INV)
    im2,contours,hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
    
    # Find the biggest area contour
    cnt = contours[0]
    max_area = cv2.contourArea(cnt)

    for cont in contours:
        if cv2.contourArea(cont) > max_area:
            cnt = cont
            max_area = cv2.contourArea(cont)

    # Make array of all edge points of the largets contour, named allpnts  
    perimeter = cv2.arcLength(cnt,True)
    epsilon = 0.01*cv2.arcLength(cnt,True) # 0.0125*cv2.arcLength(cnt,True) seems to work better
    allpnts = cv2.approxPolyDP(cnt,epsilon,True)
    
    end_time = time.time()
    print("Elapsed cv2 time was %g seconds" % (end_time - start_time))

    # Convert back into uint8, and make 2 color image for saving and showing
    center = np.uint8(center)
    res = center[label.flatten()]
    res2 = res.reshape((hsv_img.shape))

    # Save, show and print stuff
    cv2.drawContours(pntscanvas, allpnts, -1, (0, 0, 255), 2)
    cv2.imwrite("pntscanvas.bmp", pntscanvas)
    cv2.imshow("pntscanvas.bmp", pntscanvas)
    print('allpnts')
    print(allpnts)
    print("center")
    print(center)
    print('lowv',lowv)
    print('higv',higv)
    cv2.imwrite('mask.bmp',mask)
    cv2.imshow('mask.bmp',mask)
    cv2.imwrite('CvKmeans2Color.bmp',res2)
    cv2.imshow('CvKmeans2Color.bmp',res2)

print ("Waiting for 'Spacebar' to Do/Redo OR 'Esc' to Exit")
while(1):
    ch = cv2.waitKey(50)
    if ch == 27:
        break
    if ch == ord(' '):
        doredo()
        
cv2.destroyAllWindows()
Run Code Online (Sandbox Code Playgroud)

留下来做:

  1. 在非边缘像素上添加蒙版半径以处理像Gimp在蒙版上运行最小半径之前创建的原始蒙版:

在此输入图像描述

1A.编辑:截至2018年7月9日,我一直专注于这个问题,因为这似乎是我最大的问题.我无法让cv2.findcontours平滑掉'边缘草'以及Gimp的魔棒半径特征.在左边,是一个2色'问题'掩码和叠加的结果'红色'点,直接使用cv2.findcontours找到,在右边,Gimp圆角掩模应用于cv2之前的左图像'问题'掩码. findcontours应用于它,产生正确的图像和点:

在此输入图像描述 在此输入图像描述

我曾尝试查看Gimps源代码,但这超出了我的理解范围,我找不到任何可以做到这一点的OpenCV例程.有没有办法在OpenCV中对边缘掩码的"非边缘"像素应用最小半径平滑?通过'非边缘'我的意思是,你可以看到Gimp没有半径这些'角'(在黄色高光内),但似乎只是将半径平滑应用于图像内部的边缘(注意:Gimps半径算法消除了所有掩码中的小岛,这意味着在应用cv2.findcontours获取兴趣点后,您不必找到最大的区域轮廓):

在此输入图像描述

  1. 从图像边缘的allpnts中删除不相关的数组点.
  2. 找出为什么它找到的数组点似乎围绕绿草而不是硬表面,我以为我正在使用硬表面色调.
  3. 弄清楚为什么CvKmeans2Color.bmp中的硬表面颜色在Gimps转换中显示为橙色而不是米色,为什么这与Gimps转换的像素像素不匹配?这是CvKmeans2Color.bmp和Gimps:

在此输入图像描述 在此输入图像描述

编辑:截至美国东部时间2018年7月12日下午5点:我已经使用了我最容易创建代码的语言,VB6,呃,我知道.无论如何,我已经能够在像素级别上进行线/边平滑程序,以完成我想要的最小半径蒙版.它就像一个PacMan沿着边缘的右侧漫游,尽可能靠近它,并在Pac的左侧留下一条面包屑痕迹.不确定我是否可以从该代码创建一个python脚本,但至少我有一个地方可以开始,因为没有人确认有一个OpenCV替代方法来做到这一点.如果有人感兴趣,这里是一个编译的.exe文件,应该在没有安装的大多数Windows系统上运行(我认为).下面是它的截图(蓝色/绿色蓝色像素是未平滑边缘,绿色/绿色蓝色像素是圆角边缘):

在此输入图像描述

您可以通过此VB6例程获取我的过程逻辑的要点:

Sub BeginFollowingEdgePixel()
   Dim lastwasend As Integer
   wasinside = False
   While (1)
      If HitFrontBumper Then
         GoTo Hit
      Else
         Call MoveForward
      End If
      If circr = orgpos(0) And circc = orgpos(1) Then
         orgpixr = -1 'resets Start/Next button to begin at first first found blue edge pixel
         GoTo outnow 'this condition indicates that you have followed all blue edge pixels
      End If
      Call PaintUnderFrontBumperWhite
      Call PaintGreenOutsideLeftBumper
nomove:
      If NoLeftBumperContact Then
         Call MoveLeft
         Call PaintUnderLeftBumperWhite
         Call PaintGreenOutsideLeftBumper
         If NoLeftBumperContact Then
            If BackBumperContact Then
               Call MakeLeftTheNewForward
            End If
         End If
      ElseIf HitFrontBumper Then
Hit:
         Call PaintAheadOfForwardBumperGreen
         Call PaintGreenOutsideLeftSide
         Call MakeRightTheNewForward
         GoTo nomove
      Else
         Call PaintAheadOfForwardBumperGreen
         Call PaintGreenOutsideLeftSide
         Call PaintUnderFrontBumperWhite
      End If
      If (circr = 19 + circrad Or circr = -circrad Or circc = 19 + circrad Or circc = -circrad) Then
         If lastwasend = 0 And wasinside = True Then
            'finished following one edge pixel
            lastwasend = 1
            GoTo outnow
            Call redrawit
         End If
      Else
         If IsCircleInsideImage Then
            wasinside = True
         End If
         lastwasend = 0
      End If
      Pause (pausev) 'seconds between moves - Pressing Esc advances early
   Wend
outnow:
End Sub
Run Code Online (Sandbox Code Playgroud)

Dav*_*vid 1

好吧,我终于有时间看这个了。我将解决您的每一点,然后展示代码中的更改。如果您有任何问题或建议,请告诉我。

  1. 看来你自己能够很好地做到这一点。

    1.a. 这可以通过在对图像进行任何处理之前模糊图像来解决。为了实现此目的,对代码进行了以下更改;

    ...
    start_time = time.time()                                              
    
    blur_img = cv2.GaussianBlur(img,(5,5),0) #here                        
    
    # Use kmeans to convert to 2 color image                              
    hsv_img = cv2.cvtColor(blur_img, cv2.COLOR_BGR2HSV)
    ...
    
    Run Code Online (Sandbox Code Playgroud)
  2. 我更改了代码以删除完全遵循图像侧面的线上的点。草边也与此重合应该基本上是不可能的。

    ...
    allpnts = cv2.approxPolyDP(cnt,epsilon,True)                          
    
    new_allpnts = []                                                      
    
    
    for i in range(len(allpnts)):                                         
        a = (i-1) % len(allpnts)                                          
        b = (i+1) % len(allpnts)                                          
    
        if ((allpnts[i,0,0] == 0 or allpnts[i,0,0] == (img.shape[1]-1)) and (allpnts[i,0,1] == 0 or allpnts[i,0,1] == (img.shape[0]-1))):          
            tmp1 = allpnts[a,0] - allpnts[i,0]                            
            tmp2 = allpnts[b,0] - allpnts[i,0]                                                                                                                     
            if not (0 in tmp1 and 0 in tmp2):                             
                new_allpnts.append(allpnts[i])
        else:
            new_allpnts.append(allpnts[i])
    ...
    cv2.drawContours(pntscanvas, new_allpnts, -1, (0, 0, 255), 2)
    ...
    
    Run Code Online (Sandbox Code Playgroud)
  3. 由于如何在图像中找到轮廓,我们可以简单地翻转阈值函数并找到图像其他部分周围的轮廓。变化如下:

    ...
    #Extract contours from the mask                                      
    ret,thresh = cv2.threshold(mask,250,255,cv2.THRESH_BINARY) #here      
    im2,contours,hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    ...
    
    Run Code Online (Sandbox Code Playgroud)
  4. 至于颜色差异,您已将图像转换为 HSV 格式,并且在保存之前不会将其切换回 BGR。HSV 的这一更改确实会给你带来更好的结果,所以我会保留它,但它是一个不同的调色板。变化如下:

    ...
    cv2.imshow('mask.bmp',mask)                                           
    res2 = cv2.cvtColor(res2, cv2.COLOR_HSV2BGR)                          
    cv2.imwrite('CvKmeans2Color.bmp',res2)                                
    cv2.imshow('CvKmeans2Color.bmp',res2)
    ...
    
    Run Code Online (Sandbox Code Playgroud)

免责声明:这些更改基于上面的 python 代码。对不在提供代码中的 python 代码进行的任何更改都会导致我的更改无效。