1、实战项目(找中线)
目标是从面对这种简单环境用opencv画出中线,下面是过程实现
2、opencv-python基于HSV抠图
这种环境看似简单,但是用灰度处理二值化的效果奇差,最后选择用HSV进行分割。
先导入包:
import cv2import cv2 as cvimport numpy as np
然后把图片用cvtColor转化成HSV格式
img=cv.imread("1.png")img=cv.cvtColor(img,cv.COLOR_BGR2HSV)
关于HSV :
HSV模型中颜色的参数分别是:色调(H),饱和度(S),明度(V)色调H用角度度量,取值范围为0~360,从红色开始按逆时针方向计算,红色为0,绿色为120,蓝色为240,他们的补色是:黄色为60,青色为180,紫色为300如果直接使用opencv中cvtColor函数,并设置参数为CV_BGR2HSV,那么所得的HSV范围分别是【0,180】【0,255】【0,255】
lower=np.array([0,0,137])upper=np.array([120,255,255])mask = cv2.inRange(img,lowerb=lower,upperb=upper)
关于cv2.inRange方法
mask = inRange(hsv,lower_red,upper_red)hsv:原图lower_red指的是图像中低于这个lower_red的值,图像值变为0upper_red指的是图像中高于这个upper_red的值,图像值变为0
阈值的选择
阈值的选择比较烦人,上面的数值是匹配第一张图片分割的。下面提供一个可视化工具,帮助你找到合适的阈值。
import cv2import numpy as np# 滑动条的回调函数,获取滑动条位置处的值def empty(a):h_min = cv2.getTrackbarPos("Hue Min", "TrackBars")h_max = cv2.getTrackbarPos("Hue Max", "TrackBars")s_min = cv2.getTrackbarPos("Sat Min", "TrackBars")s_max = cv2.getTrackbarPos("Sat Max", "TrackBars")v_min = cv2.getTrackbarPos("Val Min", "TrackBars")v_max = cv2.getTrackbarPos("Val Max", "TrackBars")print(h_min, h_max, s_min, s_max, v_min, v_max)return h_min, h_max, s_min, s_max, v_min, v_maxpath = '1.png'# 创建一个窗口,放置6个滑动条cv2.namedWindow("TrackBars")cv2.resizeWindow("TrackBars", 640, 240)cv2.createTrackbar("Hue Min", "TrackBars", 0, 120, empty)cv2.createTrackbar("Hue Max", "TrackBars", 19, 120, empty)cv2.createTrackbar("Sat Min", "TrackBars", 110, 255, empty)cv2.createTrackbar("Sat Max", "TrackBars", 240, 255, empty)cv2.createTrackbar("Val Min", "TrackBars", 153, 255, empty)cv2.createTrackbar("Val Max", "TrackBars", 255, 255, empty)while True:img = cv2.imread(path)imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)# 调用回调函数,获取滑动条的值h_min, h_max, s_min, s_max, v_min, v_max = empty(0)lower = np.array([h_min, s_min, v_min])upper = np.array([h_max, s_max, v_max])# 获得指定颜色范围内的掩码mask = cv2.inRange(imgHSV, lower, upper)# 对原图图像进行按位与的操作,掩码区域保留imgResult = cv2.bitwise_and(img, img, mask=mask)cv2.imshow("Mask", mask)cv2.imshow("Result", imgResult)cv2.waitKey(1)
边界轮廓线拟合 :
contours_,h_ = cv.findContours(mask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)mask = cv2.cvtColor(mask,cv2.COLOR_GRAY2BGR)cv2.drawContours(mask,contours_,-1,(0,255,0),3)
这里拟合出边界轮廓线并且直接画出来,线的厚度可以在drawContours的最后一个参数3的地方修改。这里contours_的数据类型如下,如果有相关操作注意:
最后展示图片
cv2.imshow("result",mask)cv2.waitKey(0)
轮廓拟合效果如下:
附上二值化效果:
3.拟合中线:
在网上找了多种直线拟合方法,如霍夫直线拟合骨架提取,opencv骨架提取,效果都不好,这里尝试用cv2.fitLine和cv2.line函数自己写一个中线提取方法。
middle_s=[]for i in range(0,480):count = 0sum=0for j in range(0,1103):if contours_[1][j][0][1] == i:sum+=contours_[1][j][0][0]count+=1middle=sum/countprint(count)middle_s.append([[middle,i]])contours_middle=np.array(middle_s).astype(np.int32)cv2.drawContours(mask,contours_middle,-1,(0,255,0),2)output=cv2.fitLine(contours_middle, cv2.DIST_L2, 0, 0.01, 0.01)k = output[1] / output[0]b = output[3] - k * output[2]x_0=-b/kx_480=(479-b)/kcv.line(mask,(int(x_0),0),(int(x_480),479),(255,0,0),4)cv.line(img_c,(int(x_0),0),(int(x_480),479),(255,0,0),4)cv.imshow("img_c",img_c)cv.imshow("line",mask)cv.imshow("mask",mask_c)cv.waitKey(0)
代码中的img_c是上面代码copy的原图,mask是经过轮廓拟合的图片
关于fitLine及直线拟合算法:
fitLine的返回值output有四维,前面两个维度表示方向,类似方向向量两个相除就可以得到斜率k,后面两个维度是直线上一个点的横坐标和纵坐标。斜率k=output[1]/output[0]。有了斜率之后可以求得直线与上下边界线的焦点,直接用line()函数画出拟合的直线。
注意:
这里拟合的是直线,如果有拟合曲线中线需求的可以尝试使用两端轮廓线均值的方法,或者其他函数方法。这里的代码是仅仅适用于第一个图那种单直到的直线拟合,如果是复杂场景可以自己调整。
完整代码如下:
#导入包import cv2import cv2 as cvimport numpy as np#导入图片并备份img=cv.imread("1.png")img_c=img.copy()#把图片转换成HSV颜色空间img=cv.cvtColor(img,cv.COLOR_BGR2HSV)#创建二值化阈值的上下界数组lower=np.array([0,0,137])upper=np.array([120,255,255])#进行inRange()二值化mask = cv2.inRange(img,lowerb=lower,upperb=upper)mask_c=mask.copy()#备份图片#拟合轮廓线,这种方法找的轮廓线不太干净,可以使用Canny会好一点,但是对于中线拟合影响不大contours_,h_ = cv.findContours(mask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE)mask = cv2.cvtColor(mask,cv2.COLOR_GRAY2BGR)cv2.drawContours(mask,contours_,-1,(0,255,0),3)mask_line=mask.copy()#备份轮廓线图片#创建中线点集middle_s=[]#利用循环将中线的点填充到点集中for i in range(0,480):count = 0sum=0for j in range(0,1103):if contours_[1][j][0][1] == i:sum+=contours_[1][j][0][0]count+=1middle=sum/countprint(count)middle_s.append([[middle,i]])#转化成int32类型contours_middle=np.array(middle_s).astype(np.int32)#这里可以直接把中线上的点打印出来就可以得到中线,但是可能有些噪点#所以选择用直线拟合,在drawContours()下面直接输出mask就可以看到cv2.drawContours(mask,contours_middle,-1,(0,255,0),2)#下面把点集放大fitLine()中拟合出直线output=cv2.fitLine(contours_middle, cv2.DIST_L2, 0, 0.01, 0.01)k = output[1] / output[0]b = output[3] - k * output[2]x_0=-b/kx_480=(479-b)/kcv.line(mask,(int(x_0),0),(int(x_480),479),(255,0,0),4)cv.line(img_c,(int(x_0),0),(int(x_480),479),(255,0,0),4)#展示图片cv.imshow("mask_line",mask_line)#二值化后拟合轮廓图cv.imshow("img_c",img_c)#原图画上中线cv.imshow("line",mask)#二值化,轮廓线和中线cv.imshow("mask",mask_c)#二值化图像cv.waitKey(0)
结果图片: