最近我发现,在很多特定问题上传统的分割方法挺方便的,比如分割打印字体文件,网站爬下来的表格图像,pdf中的特定格式文件等。在实战中,我总结了几点记录一下。主要采用opencv-python来应用这些算法。
大体来分,传统的分割算法可分为三类:
基于阈值的分割方法
基于区域的分割方法
基于边缘的分割方法以及基于特定理论的分割方法
从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像索赋予相同的编号。
图像分割的目标是将图像中像素根据一定的规则分为若干个(N)个cluster集合,I每个集合包含一类像素。
根据算法分为监督学习算法和无监督学习算法,传统的图像分割算法多数都是无监督学习算法,基于深度学习的分割算法则多是有监督学习算法.
根据我在实际场景中的应用状况,分割出来的结果最好同时可以确定目标的位置,比如提取表格图像中的表格的时候,用霍夫直线检测比用多边形拟合矩形要好。因为直线检测可以定位每一条线从而定位到每一个表格中的格子,而多边形拟合想要达到同样的效果就需要设置很多阈值。(还有一点:由于直线检测对噪声很敏感,直接做多边形拟合很可能找不到,而直线检测就要灵活很多了。)
目前最常用的方法:1.预处理 2.直线检测 3.多边形拟合 4.水平投影
1.预处理方法:这步非常重要,对最终结果有极大影响。
由于色彩变化通常不稳定,一般采用转化成灰度图,然后基于灰度值变化去做分割。
1.1转化成灰度图有两种方法:
1.gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
2.im=cv2.imread("./677849385.jpg",0)#读图像的时候讲最后一个参数置0
1.2灰度图之后用canny算子做边缘检测
这里如果噪点较多,可以先用各种去噪的算法,比如高斯滤波(cv2.GaussianBlur(gray, (3, 3),0)),但有一个缺点,会将图像变模糊,不利于边缘检测。依情况选择使用。
同样为了减少噪声,还有一种选择是采用阈值过滤(cv2.threshold(gray,245,255,cv2.THRESH_BINARY)),这个函数很容易让人误解为高于阈值的就设为0,低于阈值的就设为255这样的二值化,其实做的比这复杂(参考下面的表)。参考:https://cloud.tencent.com/developer/article/1348925 所以如果是仅仅想达据阈值分开是需要自己写的,遍历图像即可.
回到canny算子,Canny(输入的灰度图, 最小阈值, 最大阈值),直觉上看这个函数就是把边缘提取出来,为后面的步骤做准备。原理上看就是寻找局部区域的极大值,因为这个原因找到的边缘可能是不连续的,一段一段的小线段,所以下一步可以采用腐蚀和膨胀的方法来链接边缘。
1.3腐蚀和膨胀
腐蚀和膨胀也是预处理比较重要的方法,二者本质上都是用一个卷积核在图像上滑动做卷积操作。直观上看膨胀是把边缘变粗。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3, 3))
dilation = cv2.dilate(canny,kernel,iterations = 1)#膨胀一下,来连接边缘
我通常用这个办法来连接canny之后不连续的线段,但要根据情况而定,因为连接的同时可能会把不是同一条线段的地方也连在一起,导致不正确的粘连。这时候就需要用一下腐蚀操作。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3, 3))
eroded = cv2.erode(RedThresh,kernel) #腐蚀图像
2.接下来就是分割的方法,分割的方法有很多,我这里记录三种常用的,容易使用的方法。
2.1 多边形拟合
这种方法的意思是,在处理好的边缘检测之后的图像上找对应的图形,可以是长方形,圆形等各种图形。然后用找到的图形边界来筛选出的自己需要的大致区域。由于对噪声太敏感了,一般只用于确定大致位置,粗略提取,把更加细致的活交给直线检测。
有一个例子是身份证位置识别可以参考一下:
2.2直线拟合
上面的多边形拟合的方法有个问题:对噪声比较敏感。比如说有一个四边形,如果有一条边上出现了缺口,导致整个图形没有闭合,那么这个四边形很有可能识别不出来。这个时候去拟合多边形还不如直接拟合直线更有鲁棒性。有一个重要的技巧就是拟合横线的时侯把y坐标设置成最大值,拟合竖线的时侯把x坐标设置成最大值。opencv有两个函数完成这个事情,cv2.HoughLines和cv2.HoughLinesP,我建议用后一个,前一个涉及到角度比较麻烦。
给出后一个的参数详解:
lines = cv2.HoughLinesP(image,rho,theta,threshold[, lines[, minLineLength[, maxLineGap]]])
这个方法中前面四个参数跟cv2.HoughLines方法中的用法相同,详细记录一下minLineLength和maxLineGap。
* lines返回值是以(x1,y1,x2,y2)4个元素的向量为元素的列表。(x1,y1)和(x2,y2)表示一条线段的起点和终点。
* minLineLength指最小的线段长度,小于该参数的直线被舍弃掉,认为不合格。
* maxLineGap指同一条线上的最大间断值。
调用范例:
lines_0 = cv2.HoughLinesP(canny, 1, np.pi / 180, 5, 2, 3)
for line in lines_0:
x1, y1, x2, y2 = line[0]
p1 = (x1, 0)
p2 = (x2, box_m.shape[0])
p3 = (0, y1)
p4 = (box_m.shape[1], y2)
if abs(int((x2 - x1))) < 3:#这样是检测竖线
linesum_1.append((p1, p2))
if abs(int(y1 - y2)) < 3 and abs(int((x2 - x1))) > 50:#这样检测横线
linesum_0.append((p3, p4))
补充一份代码:将检测出来的直线用不同颜色画出来
for i in range(len(point)-1):
cv2.line(im, tuple(point[i][0]), tuple(point[i+1][0]), (0,255,0), 1)
cv2.line(im, tuple(point[0][0]), tuple(point[3][0]), (0,255,0), 1)
cv2.imshow("a",im)
2.3水平分割(重点)
这个方法是统计的方法,我感觉是最好用的方法。方法为统计每一个横或者纵坐标上的灰色像素个数,得到统计像素图,根据统计值来寻找分割的特征。这种方法在很多复杂场景下效果很不错。但是统计的过程中如果图像像素质量较高,遍历图像统计灰色像素的时候就会很慢,我一般会采用gpu来加速处理,一般都能满足实时性要求。具体可以参看:
举个例子:
比如下面有图,我想分开图像和文字,但是图像中没有任何表格骨架来让我提取。这时候,我们统计一下灰度值,通过图像的灰度值一定比文字的灰度值要大,就可以很好的将二者区分开来了。(右图中绿色的为分割线。)
示例代码:
统计灰度直方图部分的代码(gpu加速之后):
from numba import jit
@jit
def shuiping_gpu_1(thresh):
(h,w)=thresh.shape #返回高和宽
a = [0 for z in range(0, h)]
for j in range(0,h):
for i in range(0,w):
if thresh[j,i]==0:
a[j]+=1
thresh[j,i]=255
return thresh,a
@jit
def shuzhi_gpu_1(thresh):
(h,w)=thresh.shape #返回高和宽
a = [0 for z in range(0,w)]
for i in range(0,w):
for j in range(0,h):
if thresh[j,i]==0:
a[i]+=1
thresh[j,i]=255
return thresh,a
调用:
(h,w)=img.shape
img = np.asarray(img)
img_shuiping,a=shuiping_gpu_1(img)
for j in range(1,h-2):
for i in range(0,a[j]):
img_shuiping[j,i]=0
cv2.imshow("o",img_shuiping)
cv2.waitKey(0)
总结:
分割问题是图像处理的基础问题,目标检测,图像语义识别算法均需要在分割的基础上做。
传统方法与深度学习的方法有着千丝万缕的关系,比如传统方法中想尽办法进行边缘提取,统计直方图特征,用ORB,SURF找特征点,本质上跟深度学习用cnns提取特征是一样的目的。
具体应用来说基于深度学习的分割方法更适用于分割应用场景复杂(光照复杂,分割物体本身需要较强的泛化能力,场景杂乱)的情况,这是优点。而缺点也很明显
1.需要标注数据,操作复杂
2.最终结果有不确定性(最好不要对神经网络的泛化能力过高期待)
传统方法正好截然相反,灵活性很欠缺,但只要用对了地方,相比深度学习有成本低,结果可预见,可以精细化调整等极大地优点。