继
计算机视觉识别新增物品笔记 一
计算机视觉识别新增物品笔记 二
之后的一段时间里我一直在自己摸索尝试去解决图像中的背景分割和新增物品切割问题.
中间试过各种各样的方法, 感觉总是不理想.
后来又单独尝试用了cv2.Canny 实现图像轮廓提取, 但是无法解决边缘受到强光照的影响造成的物体不连续. 还有阴影造成的物体边界不连续.
后来还尝试用轮廓,找到物品.
凡此种种,在经历过很多次的尝试和失败之后, 我发现了图像识别最重要的几个因素.
我曾经的尝试都是依靠单个算法进行试验. 效果都不是很理想. 因为图像是一个信息高度压缩编码的数据集. 需要解码, 而这个解码过程需要一层层解开… 顺序很重要. 幸得opencv多年的技术积累, 已形成了一套行之有效的基础解码算法库, 我等只需要调用即可.
前期尝试过各种各样的单个算法之后发现效果不理想, 对图像识别的难度也有了比较深刻的认识. 我属于失败学习型, 只有经历过失败,经历过自己的思考才知道它的珍贵. 后来在网上找遍了百度到的所有信息, 始终没有找到合适的算法, 直到我昨天在github上去搜到了一个开源项目, 而这个开源项目公布的算法就是我之前遇到问题的综合性的解决步骤. 一步一步的解决各种问题. 而且解决方案与我所想的竟然是一致的 (虽然效果一般.不是最好,还有更好的) , 但这从正面上证明了我的思考是正确的, 增加了我的对图像识别的信心. 像打了鸡血一样. 经过我一个多月的反复思考和总结尝试, 基本上对基础的图像处理方法有个大概的掌握. 而且对其中的各个方法的优缺点都有深刻的认识, 这在我看来是非常珍贵的. 这也许就是经验的价值吧.
后来又尝试了神经网络 mask R-CNN 对图片进行图像语义分割. 经过实践, 发现神经网络效果还是比较好的. 但是不足之处是需要训练. 对没训练过的物品分割效果就不怎么好. 而我的需求只不过是简单前期预处理一下.
进入正题, 神经网络咱先不说. 咱今天先说一下基于opencv如何做到分割背景,提取新增物品.掌握了里面的各种方法你就掌握了传统的处理图像的利器(工具).
在我目前看来, 各种算法例如去除光照和隐形的方法就是一种工具. 提取轮廓的方法也是一种工具.
而这些工具 各有利弊, 各有其不同的使用场合.
但是光有工具还不够还需要知道如何才能在一步一步的过程中达到自己想要的目的.
如果你有心能够耐心的看完下面每一步的代码, 那么我想你应该对整个图像处理有了大概的了解. 为了方便大家理解, 我加了很多注释.我这里是综合性的步骤而不是网络上那种零散的步骤.
源代码是c++的我翻译成了python的
下面的这段代码是 去除物品背景的算法. 此算法的过程有点类似于PS做图像处理. 所以美工做这个肯定有优势.
# 从 https:#github.com/LowWeiLin/OpenCV_ImageBackgroundRemoval 得到的c++代码 然后翻译成了python代码
# 具体过程如下
# 1.通过转换为HSV并将V设置为固定值来移除阴影
# 2.转换为灰度并规格化
# 3.应用高斯模糊和Canny边缘检测器
# 4.扩张以缩小差距
# 5.从边界整体填充图像
# 6.腐蚀以解释以前的膨胀
# 7.查找最大轮廓
# 8.遮罩原始图像
import cv2
import numpy as np
def main():#{
# #Load source image
sourceImage = cv2.imread("test (2).jpg")
#0. Source Image
cv2.imshow("sourceImage", sourceImage)
#1. Remove Shadows
#Convert to HSV 转换为HSV色彩空间
hsvImg = cv2.cvtColor(sourceImage, cv2.COLOR_BGR2HSV)
channel3 = cv2.split(hsvImg)
# channel3[2] = Mat(hsvImg.rows, hsvImg.cols, CV_8UC1, 200);
# //Set V 这里是把V通道 也就是亮度通道全部改成了200
# 用numpy操作应该是下面这个样子一句话就搞定了,里面的数值就都变成200了.
# 目的是把灰度都调到同一个200的亮度. 也就是去掉了阴影和光照的影响,
# 这一步的目的是为了方便后面的灰度图正确提取轮廓
channel3[2] = channel3[2] * 0 + 200 #np.array(hsvImg.rows, hsvImg.cols, cv2.CV_8UC1, 200) #Set V
#Merge channels
# cv2.merge(channel3, 3, hsvImg) #numpy直接修改了数据 不需要再合并了
# rgbImg as Mat
rgbImg = cv2.cvtColor(hsvImg, cv2.COLOR_HSV2BGR)
cv2.imshow("1. \"Remove Shadows \"", rgbImg) #1.通过转换为HSV并将V设置为固定值来移除阴影
#2. Convert to gray and normalize
# Mat gray(rgbImg.rows, sourceImage.cols, cv2.CV_8UC1)
gray = cv2.cvtColor(rgbImg, cv2.COLOR_BGR2GRAY)
cv2.normalize(gray,gray, 0, 255, cv2.NORM_MINMAX, cv2.CV_8UC1)
cv2.imshow("2. Grayscale", gray)
#3. Edge detector 边界检测
# 3.1 先高斯滤波去掉杂点
gray = cv2.GaussianBlur(gray,(3,3), 0, 0, cv2.BORDER_DEFAULT)
# Mat edges
edges = None
useCanny = True
#这里可以选择两种轮廓检测算法,一个是canny,另外一个是使用sobel算子
if(useCanny):#{
edges = cv2.Canny(gray,threshold1= 250, threshold2=750, apertureSize=5)
#}
else:
#{
#Use Sobel filter and thresholding.
edges = mysobel(gray)
#Automatic thresholding
#threshold(edges, edges, 0, 255, cv2.THRESH_OTSU)
#Manual thresholding
edges = cv2.threshold(edges, thresh=25,maxval=255,type=cv2.THRESH_BINARY)
#}
cv2.imshow("3. Edge Detector", edges)
#4. Dilate 膨胀 目的是扩张小图形的边缘, 尽量保留小物品的信息.例如头发丝.
# Mat
dilateGrad = edges
dilateType = cv2.MORPH_ELLIPSE #椭圆形:MORPH_ELLIPSE, 矩形:MORPH_RECT; 交叉形:MORPH_CROSS;
dilateSize = 3
#Mat
elementDilate = cv2.getStructuringElement(
dilateType,
(2*dilateSize + 1, 2*dilateSize+1),
(dilateSize, dilateSize)
)
# cv2.dilate(edges, dilateGrad, elementDilate)
dilateGrad = cv2.dilate(edges, elementDilate)
cv2.imshow("4. Dilate", dilateGrad)
#5. Floodfill 从边界整体填充图像, 做遮罩层,准备去掉背景的物品.
# Mat
h, w = dilateGrad.shape[:2]
floodFilled = np.zeros(shape=[h+2, w+2], dtype=np.uint8) ##mask必须行和列都加2,且必须为uint8单通道阵列
# 为什么要加2可以这么理解:当从0行0列开始泛洪填充(油漆桶功能)扫描时,mask多出来的2可以保证扫描的边界上所有的像素都会被处理
#cv2.floodFill(dilateGrad,floodFilled, (0, 0), 0, 0, cv2.Scalar(), cv2.Scalar(), 4 + (255 << 8) + cv2.FLOODFILL_MASK_ONLY)
#floodFill 这一步操作的作用是先用魔法棒选定范围再填充颜色
#seedPoint=(0,0) 的意思是从外围开始相当于PS的魔法棒从角上开始点击 ,然后把选中的部分(一般是背景部分)填充为 (0,0,0) 黑色,因为新的两个像素边框是黑色的,背景色被第3步二值化之后也是黑色的
cv2.floodFill(image=dilateGrad,mask=floodFilled,seedPoint=(0,0),newVal=(0, 0, 0), flags= 4 + (255 << 8) + cv2.FLOODFILL_MASK_ONLY)
# cv2.imshow("5.1 Floodfill1", floodFilled)
# c = cv2.scalar(0,0,0)
#反选 ,取反
# floodFilled = cv::Scalar::all(255) - floodFilled
floodFilled = np.abs(255 - floodFilled)
# cv2.imshow("5.2 Floodfill1", floodFilled)
# cv2.waitKey(-1)
# return
# Mat temp
# h, w = dilateGrad.shape[:2]
temp = np.zeros(shape=[h-2, w-2], dtype=np.uint8)
# floodFilled = floodFilled(Rect(1, 1, dilateGrad.cols-2, dilateGrad.rows-2)).copy()
temp = floodFilled[0:w, 0:h] #恢复原图大小
floodFilled = temp
cv2.imshow("5. Floodfill", floodFilled)
#6. Erode 腐蚀缩小以前的膨胀造成的不利影响
erosionType = cv2.MORPH_ELLIPSE
erosionSize = 4
#Met
erosionElement = cv2.getStructuringElement(erosionType,
(2*erosionSize+1, 2*erosionSize+1),
(erosionSize, erosionSize))
floodFilled = cv2.erode(src=floodFilled,kernel= erosionElement)
cv2.imshow("6. Erode", floodFilled)
# cv2.waitKey(-1)
# return
#7. Find largest contour 查找最大轮廓, 也就是主图
largestArea = 0 #int
largestContourIndex = 0 #int
# Rect boundingRectangle
boundingRectangle = None
# largestContour = cv2.largestContour(h, w, cv2.CV_8UC1, (0)) # 定义一个矩阵Mat
#定义最大轮廓的画布
largestContour = np.zeros(shape=[h, w], dtype=np.uint8)
# vector> contours
# vector hierarchy
# findContours mode 参数
# RETR_EXTERNAL - 只提取最外层的轮廓
# RETR_LIST - 提取所有轮廓,并且放置在 list 中
# RETR_CCOMP - 提取所有轮廓,并且将其组织为两层的 hierarchy: 顶层为连通域的外围边界,次层为洞的内层边界。
# RETR_TREE - 提取所有轮廓,并且重构嵌套轮廓的全部 hierarchy
#findContours method 参数
# CV_CHAIN_CODE 以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)。
# CV_CHAIN_APPROX_NONE 将所有的连码点,转换成点。
# CV_CHAIN_APPROX_SIMPLE 压缩水平的、垂直的和斜的部分,也就是函数只保留他们的终点部分。
# CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS:使用the flavors of Teh-Chin chain近似算法的一种。
# CV_LINK_RUNS:通过连接水平段的1,使用完全不同的边缘提取算法。使用CV_RETR_LIST检索模式能使用此方法。
contours, hierarchy = cv2.findContours(image=floodFilled, mode=cv2.RETR_CCOMP,method=cv2.CHAIN_APPROX_SIMPLE)
#查找最大轮廓
# for(int i=0 i < contours.size() i++):#{
for index,contour in enumerate(contours):
a = cv2.contourArea(contour, False) #double
if(a > largestArea):#{
largestArea = a
largestContourIndex = index
boundingRectangle =cv2.boundingRect(contour)
#}
#}
# Scalar color(255, 255, 255)
whiteColor = (255,255,255)
greenColor = (0, 255, 0)
cv2.drawContours(largestContour, contours, largestContourIndex, whiteColor, cv2.FILLED, 8, hierarchy) #Draw the largest contour using previously stored index.
cv2.rectangle(sourceImage, boundingRectangle, greenColor, 1, 8, 0)
cv2.imshow("7. Largest Contour", largestContour)
#8. Mask original image
# Mat maskedSrc
maskedSrc = cv2.copyTo(sourceImage,largestContour)
cv2.imshow("8. Masked Source", maskedSrc)
#Source with largest contour boxed
cv2.imshow("sourceImage boxed", sourceImage)
cv2.waitKey(0)
#}
def mysobel(grayImage):#{
# Mat edges
scale = 1 #int
delta = 0 #int
ddepth = cv2.CV_16S #int
# Mat edges_x, edges_y
# Mat abs_edges_x, abs_edges_y
edges_x = cv2.Sobel(grayImage, ddepth, 1, 0, 3, scale, delta, cv2.BORDER_DEFAULT)
abs_edges_x = cv2.convertScaleAbs( edges_x)
edges_y = cv2.Sobel(grayImage, ddepth, 0, 1, 3, scale, delta, cv2.BORDER_DEFAULT)
abs_edges_y= cv2.convertScaleAbs(edges_y )
edges = cv2.addWeighted(abs_edges_x, 0.5, abs_edges_y, 0.5, 0)
return edges
#}
# if __name__ =="main"
main()
知道了整个过程, 把其中的某几个步骤加到提取新增物品算法上, 就能大大的提高提取新增物品的效果.