(首先说明,这里介绍的方法只针对计算机视觉,mask标注也同理。但是没有采用弱/无监督学习方式解决问题,而是纯粹的图像处理方式,方法有一定的局限性。如果你希望用深度学习方式解决深度学习的数据标注问题,那本文可能让你失望了,如果你解决了这个问题请给我写个留言,我也很想学习一下。好了,我们开始吧~)
近年来,人工智能发展迅速,通过深度学习解决问题已经变得很常见。计算机学习图像或文字、声音等的特征,分为有监督学习、半监督学习、无监督学习。针对于目标检测问题,有监督学习即需要我们先告诉机器,这个是猫,这只猫在这个问题。计算机学习了大量数据后就学会了图像一些特征,然后就可以预测任何一张图片。这里就需要人工手动标注大量数据。
人工标注的一个问题是数据的来源。深度学习的发展使得数据的价格变得昂贵。一种方式是自己采集数据,一种是某宝上购买数据,或者采用爬虫方式。
还有一个问题是,有了数据之后,还需要手工标注。所以近年来也出现一种岗位就叫人工标注,没有任何门槛。图片标注常用工具包括labelme、labelImg等。
当然还有一种方式采用百度的智能标注(这么拼命打广告百度不给我打钱吗?)
本文介绍的批量自动标注方法解决的问题是病虫害中害虫种类的标注。具有以下优点:
效果如下:
数据采集
首先需要采集一定数量的原始图像。这些原始数据应该尽可能保证数据的多样性。我这里有三千张图像数据。但是三千图像对于深度学习来讲简直杯水车薪。
批量抠图
将这些图像中的目标进行抠图,背景保留为透明。这一步需要一定的时间,也有点难。一种方式是采用AI抠图算法批量抠图(本文主要采取的方法)。也可以采用一些抠图工具。还可以用PS设定抠图动作进行批量抠图。实在不行,可以淘宝买抠图,大概2块钱一张,好点的公司一周就能抠完了(有点贵,所以我们采用前几种方式,用了一两天抠完)。
数据筛选
抠完图的数据还需要筛选一下,删掉一些不符合要求或有问题的图像。
图像批量裁切
图像裁切将一张图像中若干个目标裁切成一个个单独的图像文件,裁切好放到对应的类别文件夹中,方便后面融合时生成labelme的类别信息。裁切时可以利用图像alpha通道信息进行裁切。步骤为滤波 > 二值化 > 查找边缘 > 过滤目标 > 旋转 > 裁切
。请参考我的博文: 图像批量旋转裁切
注意这里的裁切与图像融合时的裁切的区别。两个裁切步骤都是不可以缺少的。
图像融合
我们将若干裁切好的目标图像与背景图进行融合。融合过程中,对目标图像进行旋转 > 裁切 > 缩放 > 融合
。
首先进行旋转。随机一个角度进行旋转。然后再裁切。那么为什么还需要再裁切一次呢?因为旋转过程中目标的位置变了,可能会产生一些边缘。但是这时候裁切利用下面的函数就可以了,无需再找最小边框(最小边框需要再旋转)。
x, y, w, h = cv2.boundingRect(max_contour)
缩放的时候可以设定一个与背景图像的比例,不同背景图像设置不同尺度;对于同一张背景,还可以设置不同类别之间的尺度比例;对于同一类别,还可以设置一张背景下的尺度范围。
缩放后就可以为目标再背景中获取一个随机位置了。这时候需要注意避免多个目标图像重叠。我这里采用的方式是不同尺度的合成图像,生成不同个数目标,绘制不同大小的宫格。比如尺度1的图像中放18个图像,那就画一个3*6的宫格。然后将每个目标往宫格中随机放置。需要注意的是,我的目标比较小,能保证宫格肯定比缩放后的目标大。对于个别宫格大于目标的,缩放目标的最长边=宫格的最长边。
后面将放一些关于融合算法的关键代码。逻辑部分请根据自己的具体情况设计。
生成labelme的json文件
上一步完成后就可以知道了目标在图像中的位置。根据目标文件的文件夹类别和上一步返回的位置信息就可以生成labelme的json文件了。利用labelme打开合成图像的文件夹,可以浏览检测生成的目标检测位置、类别是否正确。还可以进行更改。
labelme的json文件格式如下:
"version": "4.5.6",
"flags": {},
"shapes": [
{
"label": "GHCH",
"points": [
[
112,
305
],
[
388,
625
]
],
"group_id": 5,
"shape_type": "rectangle",
"flags": {}
}
],
"imagePath": "1600227870430_18.png",
"imageData": null,
"imageHeight": 2200,
"imageWidth": 4400
这部分比较简单。只需要根据你生成的文件格式和内容组建对应的list或dict以及他们的嵌套。最后写json文件方法如下:
json.dump(instance, open(save_path, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
逻辑部分代码很简单。只要熟悉python很快就能完成。不熟悉的百度一下也能完成。编程时有几个需要注意的小细节。
注意:
屏幕中的x,y坐标与cv中rows和cols关系。屏幕的x,y从左上角开始,向右为x正,向下为y正。rows为图像的行数(垂直方向累加),cols为图像的列数(水平方向累加)。
cv2.resize(img, (fx, fy))中size(fx,fy)的顺序与img的shape顺序是反着的。img.shape=(fy,fx)。所以写rows,cols=img.shape[:2]时,注意rows、cols与size的关系。
cv2.multiply不是矩阵乘,且不同于numpy,cv的乘法大于255都取255
这里写一下图像融合代码片段(注意是片段,缺少个别变量定义)。
# 得到前景PNG图像的alpha通道,即alpha掩模
b, g, r, a = cv2.split(foreground)
alpha = cv2.merge((a, a, a))
# 前景
fore = cv2.merge((b, g, r))
# 背景图上随机截取前景大小区域,留作融合处理
rows_fore, cols_fore = fore.shape[:2]
# print("row={},col={}".format(rows_fore, cols_fore))
y = np.random.randint(0, sudoku_h - rows_fore + 1) # sudoku_h为宫格高,即rows。注意屏幕中的x,y坐标与rows和cols关系
x = np.random.randint(0, sudoku_w - cols_fore + 1)
roi = back[y:y + rows_fore, x:x + cols_fore] # back为截取的宫格
# print("roi=", roi.shape)
# alpha归一化到0-1
alpha = alpha.astype(float)/ 255.0
beta = 1 - alpha
# 转float保持类型一致
roi = roi.astype(float)
fore = fore.astype(float)
roi = cv2.multiply(beta, roi) # 不是矩阵乘,且不同于numpy,cv的乘法大于255都取255
fore = cv2.multiply(fore, alpha)
# 融合后放在原图上
dst = roi + fore
back[y:y + rows_fore, x:x + cols_fore] = dst
background[back_rows_begin:back_rows_end, back_cols_begin:back_cols_end] = back
points = [[] for i in range(2)]
x0 = x + back_cols_begin
y0 = y + back_rows_begin
x1 = x0 + cols_fore
y1 = y0 + rows_fore
points[0].append(x0)
points[0].append(y0)
points[1].append(x1)
points[1].append(y1)
return points, background
目前设置了生成1W数据。可以写个多线程加速一下。多开两台电脑同时生成。
生成的json文件(与上图不对应)
{
"version": "4.5.6",
"flags": {},
"shapes": [
{
"label": "GHCH",
"points": [
[
954,
683
],
[
1517,
1643
]
],
"group_id": 5,
"shape_type": "rectangle",
"flags": {}
},
{
"label": "LDE",
"points": [
[
3888,
883
],
[
4301,
1363
]
],
"group_id": 14,
"shape_type": "rectangle",
"flags": {}
}
],
"imagePath": "1600326079208_2.png",
"imageData": null,
"imageHeight": 2200,
"imageWidth": 4400
}
【看完了点个赞再走呗~ ( ^ _ ^ ) ~ ,小白需要你的鼓励呢~】