python生成文字点选验证码→训练yolo目标检测模型→识别文字点选验证码
前言
- 就在去年的这个时候,我写了python生成验证码→处理验证码→建立CNN模型训练→测试模型准确率→识别验证码这篇文章,开启了我的
CNN
学习之旅 - 接着过了半年,我又写了python+DCGAN模型生成验证码+训练CNN模型+测试模型准确率这篇文章,又开启了我的
GAN
学习之旅 - 那么,接下来,即将开启我的
YOLO
学习之旅
一、生成文字点选验证码
作为十几年杰迷的我,那必须得弄点范特西的数据集,于是乎,我想到了,用周杰伦的歌曲名称+专辑封面来生成我的文字点选验证码图片,效果如下:
原图 | 添加随机歌名之后 |
---|---|
具体实现逻辑如下:
- 从专辑图片中随机选取一张作为背景,并将其大小改为520x520
- 从歌曲中随机选取1~5首歌曲的歌名
- 遍历选取到的几首歌曲的歌名
- 从40~50的字号中随机选取一个字号
- 从-90°~90°中随机选取文字旋转的角度
- 从所有颜色中随机选取一种字体颜色
- 以及在背景图片中随机选取一个坐标点xmin,ymin
- 最后将处理后的文字图片放到背景图片中,以及生成每首歌的归一化后label坐标x,y,w,h
代码如下:
from tqdm import tqdm
from PIL import Image, ImageDraw, ImageFont, ImageOps
import shutil,os
import numpy as np
class CreateData:
def __init__(self,create_num):
self.jay_img_paths=['JAY/' + i for i in os.listdir('JAY/')] # 背景图片路径
self.font_path='../simhei.ttf' # 字体路径
self.img_save_path='images/' # 生成训练集图片的路径
self.label_save_path='labels/' # 生成图片对应label的路径
self.test_path='test/' # 生成测试集图片的路径
# 100首周杰伦歌曲名称
self.songs=['可爱女人', '星晴', '黑色幽默', '龙卷风', '屋顶', '爱在西元前',
'简单爱', '开不了口', '上海一九四三', '双截棍', '安静', '蜗牛',
'你比从前快乐', '世界末日', '半岛铁盒', '暗号', '分裂', '爷爷泡的茶',
'回到过去', '最后的战役', '晴天', '三年二班', '东风破', '你听得到',
'她的睫毛', '轨迹', '断了的弦', '七里香', '借口', '搁浅', '园游会',
'夜曲', '发如雪', '黑色毛衣', '枫', '浪漫手机', '麦芽糖', '珊瑚海',
'一路向北', '听妈妈的话', '千里之外', '退后', '心雨', '白色风车',
'千山万水', '不能说的秘密', '牛仔很忙', '彩虹', '青花瓷', '阳光宅男',
'蒲公英的约定', '我不配', '甜甜的', '最长的电影', '周大侠',
'给我一首歌的时间', '花海', '魔术先生', '说好的幸福呢', '時光機',
'乔克叔叔', '稻香', '说了再见', '好久不见', '愛的飛行日記',
'超人不会飞', 'Mine Mine', '公主病', '你好吗', '疗伤烧肉粽',
'水手怕水', '世界未末日', '超跑女神', '明明就', '爱你没差',
'夢想啟動', '大笨钟', '傻笑', '手语', '乌克丽丽', '哪裡都是你',
'算什么男人', '怎么了', '我要夏天', '手写的从前', '听爸爸的话',
'美人魚', '听见下雨的声音', '说走就走', '一点点', '前世情人',
'不该', '告白气球', '愛情廢柴', '等你下课', '不爱我就拉倒',
'说好不哭', '我是如此相信', 'Mojito', '瓦解']
self.song2label={song:i for i,song in enumerate(self.songs)}
self.label2song={i:song for i,song in enumerate(self.songs)}
self.create_num=create_num
self.image_w=520
self.image_h=520
self.max_iou=0.5 # 每首歌名的boxes的iou不能超过0.5
def create_folder(self):
while True:
try:
for path in [self.img_save_path,self.label_save_path,self.test_path]:
shutil.rmtree(path,ignore_errors=True)
os.makedirs(path,exist_ok=True)
break
except:
pass
def bbox_iou(self,box2):
'''两两计算iou'''
for box1 in self.tmp_boxes:
inter_x1=max([box1[0],box2[0]])
inter_y1=max([box1[1],box2[1]])
inter_x2=min([box1[2],box2[2]])
inter_y2=min([box1[3],box2[3]])
inter_area=(inter_x2-inter_x1+1) * (inter_y2-inter_y1+1)
box1_area=(box1[2]-box1[0]+1) * (box1[3]-box1[1]+1)
box2_area=(box2[2]-box2[0]+1) * (box2[3]-box2[1]+1)
iou=inter_area / (box1_area + box2_area - inter_area + 1e-16)
if iou > self.max_iou:
# 只要有一个与之的iou大于阈值则重新来过
return iou
else:
return 0
def draw_text(self,image,image_draw,song):
iou=np.inf
num=0
while iou > self.max_iou:
if num >= 100:
# 为了避免陷进死循环,如果循环100次都没有找到合适的位置,则iou>0.5的阈值失效
break
random_font_size=np.random.randint(40,50) # 随机字号
random_rotate=np.random.randint(-90,90) # 随机旋转角度
random_color=np.random.randint(0,256,3) # 随机字体颜色
random_x,random_y=np.random.randint(1,520,2) # 随机xmin,ymin
font = ImageFont.truetype(self.font_path, random_font_size)
label=self.song2label[song]
size_wh=font.getsize(song)
img = Image.new('L', size_wh)
img_draw = ImageDraw.Draw(img)
img_draw.text((0, 0), song, font=font, fill=255)
img_rotate = img.rotate(random_rotate, resample=2, expand=True)
img_color = ImageOps.colorize(img_rotate, (0,0,0), random_color)
w,h=img_color.size
xmin=random_x
ymin=random_y
# 为了避免超出520x520,修正xmin,ymin
if random_x+w > self.image_w:
xmin=self.image_w - w - 2
if random_y+h > self.image_h:
ymin=self.image_h - h - 2
xmax=xmin+w
ymax=ymin+h
boxes=(xmin,ymin,xmax,ymax)
iou=self.bbox_iou(boxes)
num+=1
image.paste(img_color, box=(xmin,ymin), mask=img_rotate)
# image_draw.rectangle(boxes,outline=tuple(random_color))
return image,boxes,label
def process(self,boxes):
'''
将xmin,ymin,xmax,ymax转为x,y,w,h
以及归一化坐标,生成label
'''
x1,y1,x2,y2=boxes
x=((x1+x2)/2)/self.image_w
y=((y1+y2)/2)/self.image_h
w=(x2-x1)/self.image_w
h=(y2-y1)/self.image_h
return [x,y,w,h]
def main(self):
'''主函数'''
self.create_folder() # 重置所需文件夹
for i in tqdm(range(self.create_num+3)):
random_song_num=np.random.randint(1,5) # 随机1~4首
random_jay_img_path=np.random.choice(self.jay_img_paths) # 随机背景
image=Image.open(random_jay_img_path).convert('RGB').resize((self.image_w,self.image_h))
image_draw=ImageDraw.Draw(image)
boxes_list=[]
label_list=[]
self.tmp_boxes=[] # 用于计算两两boxes的iou
for j in range(random_song_num):
song=np.random.choice(self.songs)
image,boxes,label=self.draw_text(image,image_draw,song)
self.tmp_boxes.append(boxes)
boxes_list.append(self.process(boxes))
label_list.append(label)
# save image and label
image_filename=self.img_save_path+f'image{i}.png' if i < self.create_num else self.test_path+f'test{i}.png'
label_filename=self.label_save_path+f'image{i}.txt' if i < self.create_num else self.test_path+f'test{i}.txt'
image.save(image_filename)
with open(label_filename,'w') as f:
for k in range(len(label_list)):
# label x y w h
f.write(f'{label_list[k]} {boxes_list[k][0]} {boxes_list[k][1]} {boxes_list[k][2]} {boxes_list[k][3]}\n')
if __name__ == '__main__':
creator=CreateData(5000)
creator.main()
二、YOLOv3
代码过多,就不放上来了,直接看结果。
参考了崔庆才崔大写的滑块验证码识别https://github.com/Python3WebSpider/DeepLearningImageCaptcha2,只是修改了dataset相关代码,配置文件信息,以及训练的代码等等。
- dataset的修改:因为我复现崔大的代码时,发现其label文件的格式是:class xmin ymin w h
所以就自己重新改写了一下,修改成:class x y w h - 配置文件即yolov3.cfg的修改
- 三个yolo层的classes参数改为数据集的类别个数,即100
- 三个yolo层的上一个convolutional层的filters参数改为3*(类别个数+5),即315
- 训练策略
- epoch:100
- batch size:16
- img size:416
- 数据集划分:80%即4000张用于训练,20%即1000张用于验证
- gradient accumulations:5(即被5整除的批次,优化器暂停更新梯度信息)
- 优化器:Adam
- GPU内存占用:15G
- 训练耗时:4.5小时
训练结果的如下:
Train Loss | Valid Loss | Valid Metrics |
---|---|---|
---- best_mAP@0.5: 0.29220820871021674
---- best_epoch: 98
小结
知识巩固:
- TP: 将正类预测为正类的数量
- FN: 将正类预测为负类的数量
- FP: 将负类预测为正类的数量
- TN: 将负类预测为负类的数量
Accuracy | Precision | Recall | F1 Score |
---|---|---|---|
a c c = T P + T N T P + T N + F P + F N acc =\frac{TP+TN}{TP+TN+FP+FN} acc=TP+TN+FP+FNTP+TN |
P = T P T P + F P P=\frac{TP}{TP+FP} P=TP+FPTP |
R = T P T P + F N R=\frac{TP}{TP+FN} R=TP+FNTP |
F 1 = 2 × P × R P + R F_{1} =2\times \frac{P\times R}{P + R} F1=2×P+RP×R |
- 从验证集的指标看,recall高,precision低,说明FN比FP小。也就是说,模型将一首歌名识别成多首,且多数情况下其中就有一首是预测正确的
- 训练了100个epoch,然而mAP@0.5的分数也才0.3左右,提升缓慢
- 看趋势,随着训练epoch的增加,各项指标还是会再提升的
三、YOLOv5
代码过多,就不放上来了,直接看结果。
参考了作者的代码https://github.com/ultralytics/yolov5,去掉了我认为是冗余的代码,主要是简化了train.py、val.py和detect.py的代码。
因为yolov5有提供多个模型:yolov5n、yolov5s、yolov5l、yolov5m、yolov5x,于是我就都尝试了一遍。
均采用以下训练策略:
- epoch:100
- learning rate:0.001
- batch size:16
- image size:416
- augment:False
- rect:False
- quad:False
- multi scale:True
- 数据集划分:80%即4000张用于训练,20%即1000张用于验证
- 优化器:Adam
- lr scheduler:LambdaLR
- 配置文件:hyp.scratch.yaml
- 混合精度训练
- 不使用平滑标签
- 评价指标:
fitness
= 0.1 *mAP@0.5
+ 0.9 *mAP@0.5:0.95
1. yolov5n
GPU内存占用:2G
训练耗时:2.2小时
模型大小:7.39MB
---- best_fitness=0.6837750339549021
---- best_epoch=87
训练结果的如下:
Train Loss | Valid Loss | Valid Metrics |
---|---|---|
2. yolov5s
GPU内存占用:4.25G
训练耗时:2.5小时
模型大小:27.9MB
---- best_fitness=0.878297457833696
---- best_epoch=99
训练结果的如下:
Train Loss | Valid Loss | Valid Metrics |
---|---|---|
3. yolov5l
GPU内存占用:12.8G
训练耗时:3.75小时
模型大小:178MB
---- best_fitness=0.9015696258245549
---- best_epoch=93
训练结果的如下:
Train Loss | Valid Loss | Valid Metrics |
---|---|---|
4. yolov5m
GPU内存占用:8.5G
训练耗时:3小时
模型大小:81.4MB
---- best_fitness=0.9197881788592055
---- best_epoch=98
训练结果的如下:
Train Loss | Valid Loss | Valid Metrics |
---|---|---|
5. yolov5x
GPU内存占用:15.5G
训练耗时:6.3小时
模型大小:332MB
---- best_fitness=0.9216454715562004
---- best_epoch=100
训练结果的如下:
Train Loss | Valid Loss | Valid Metrics |
---|---|---|
小结
- yolov5n模型最小,GPU内存占用最小,且训练时长也最短,可惜性能是最差的,但也比yolov3要好,这一点毋容置疑
- 如果要考虑GPU内存、模型大小、训练时长和性能,那么yolov5s是最好的选择,性价比最高
- yolov5l、yolov5m和yolov5x的性能均达到了90%以上,综合考虑的话,我会选择yolov5m。
四、识别效果
最后,yolov3和yolov5分别对开头的那张图片进行识别,来看看实际的效果吧。(过滤条件均设置为0.5)
因为边幅有限,就比较一张吧,可以看得出来:
- yolov3只识别出了
等你下课
和稻香
,且置信度也不是很高 - yolov5n将
稻香
识别成傻笑
了,以及说好不哭
的置信度也不是很高 - 剩下的均成功识别出来了四首歌,且置信度都是非常高的。差别可能也不是很明显,可以多用几张图片进行测试,在我测试多几张后,yolov5x是最好的,毕竟分数最高。
yolov3 | yolov5n | yolov5s |
---|---|---|
yolov5l | yolov5m | yolov5x |
五、参考链接
以上即为本篇全部内容,代码我整理了一遍,且运行后无bug,若需要源代码的可以关注我的微信公众号《Python王者之路》,回复关键词:20211226
,即可获取。
写在最后
在看了崔大写的滑块验证码识别的推文后,我当时就已经想好了要出这一篇文章了
我先是复现了崔大的代码,当然,这其中遇到了很多的坑,毕竟第一次接触目标检测,之前只是会一点图片分类
可谓说历经千辛万苦,终于成功复现了,当时就想着要不直接写一篇:我的复现过程遇到的问题的文章算了
可是呢,看看yolov3的识别效果,如此难以接受
于是,又开始研究最新的yolov5,因为此时对yolo有了初步的认识,这时复现yolov5还算比较轻松
然后,花了一周左右的时间,测试出了最好的训练策略即本篇采用的策略
最后,又花了一周的时间,按我的训练策略来训练yolov5的5个模型,以及写这篇文章
虽然成功运用yolo模型来做目标检测了,但对于yolo具体细节还有待研究呀~
最后,提前祝大家2022年元旦快乐吧!!
版权声明:本文为CSDN博主「七里香还是稻香」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_39629323/article/details/121989609
暂无评论