目录
1 概述
本文将从代码层面的角度来剖析印章数据集如何自动生成,以及如何进行训练与测试,如果希望获取直接运行的项目,请参考:
-
🔗项目路径(含教程):印章检测yolov5(完整视频教程+数据集自动生成)
-
📨作者邮箱:mayoukeyuan@163.com
如果大家希望替换数据集,只需要替换掉本文的图片即可,其他训练和预处理操作均一致
1.1 简介
由于未找到印章目标检测的公开数据集及印章数据的敏感性,因为本文的印章数据均为测试使用的自动生成,本任务首先基于随机中文字符生成包含印章的word
文档,然后将word
转换为pdf
,将pdf
转换为图片,并使用labelme
对图片进行标注,最后使用yolov5
对印章进行检测
注意:由于是随机中文字符生成,因此文档内容都是乱编的,本实验的主要目的在于检测出印章的位置,因此文档的内容对此没有影响,所以本文使用随机字符生成数据,另外,如果希望本模型获得更好的泛化性能,需要为其添加自己希望检测的自定义数据集
-
🔗项目路径(含教程):印章检测yolov5(完整视频教程+数据集自动生成)
-
📨作者邮箱:mayoukeyuan@163.com
1.2 演示
这是我们最后希望达到的检测效果:
本实验的主要目的在于检测出印章的位置,因此文档的内容对此没有影响,所以本文使用随机字符生成数据
2 软件安装
-
Anaconda(推荐)
:安装教程:Miniconda/Anaconda最新安装教程-CSDN博客这是python的集成环境管理软件,安装此软件会将各个python环境分开管理,能够避免因为环境冲突而导致的诸多问题,如果电脑已安装则可以忽略
-
poppler(必须)
安装教程:poppler安装教程-CSDN博客这是在利用
pdf
生成印章图像中,所使用到的库pdf2image
的依赖环境 -
pycharm(非必须)
下载链接:PyCharm: the Python IDE for data science and web development编辑器也可以使用
VsCode
,只要有自己顺手的编辑器,此项不是必须
3 数据集
本文首先利用Pillow库生成印章图片,并使用docx将随机字符和印章写入word文件,最后分别使用comtypes和pdf2image将word转换为pdf和png。
3.1 生成随机字符
根据自定义的中文字符随机组合来生成公司名称、标题、子标题、内容以及落款日期
- 这个子函数用于生成指定长度的随机文本。函数接收一个字符列表、最小长度和最大长度作为参数,然后在字符列表中随机选择字符,生成指定长度的字符串
def generate_random_text(char_list, min_length, max_length):
"""
随机生成对应的内容
:param char_list:
:param min_length:
:param max_length:
"""
length = random.randint(min_length, max_length)
return ''.join(random.choice(char_list) for _ in range(length))
- 这个子函数用于生成随机的段落列表。首先,随机生成段落的数量(3到7段),然后为每个段落生成3到5句随机句子,并用句号连接每个句子,最后返回段落列表。
def generate_content():
"""
生成随机段落列表
"""
paragraphs = []
num_paragraphs = random.randint(3, 7) # 生成 3 到 7 段落
for _ in range(num_paragraphs):
num_sentences = random.randint(3, 5) # 每段包含 3 到 5 句
sentences = [generate_random_text(chinese_char, 8, 15) for _ in range(num_sentences)]
paragraph = '。'.join(sentences) + '。' # 用句号分隔句子并在末尾加句号
paragraphs.append(paragraph)
return paragraphs # 用换行分隔段落
- 定义一个包含常用汉字的列表,用于生成随机文本时选择字符
chinese_char = ['的', '一', '是', '在', '不', '了', '有', '和', '人', '这', '中', '大', '为', '上', '个', '国',
'我', '以', '要', '他', '时', '来', '用', '们', '生', '到', '作', '地', '于', '出', '就', '分',
'对', '成', '会', '可', '主', '发', '年', '动', '同', '工', '也', '能', '下', '过', '子', '说',
'产', '种', '面', '而', '方', '后', '多', '定', '行', '学', '法', '所', '民', '得', '经', '十',
'三', '之', '进', '着', '等', '部', '度', '家', '电', '力', '里', '如', '水', '化', '高', '自',
'二', '理', '起', '小', '物', '现', '实', '加', '量', '都', '两', '体', '制', '机', '当', '使',
'点', '从', '业', '本', '去', '把', '性', '好', '应', '开', '它', '合', '还', '因', '由', '其',
'些', '然', '前', '外', '天', '政', '四', '日', '那', '社', '义', '事', '平', '形', '相', '全',
'表', '间', '样', '与', '关', '各', '重新']
- 定义了落款日期的范围,分别为1990年1月1日和2020年12月31日
start_date = datetime(1990, 1, 1)
end_date = datetime(2020, 12, 31)
- 在主函数中,首先调用
generate_random_text()
生成公司名称、标题和子标题。然后调用generate_content()
生成内容段落。最后生成落款日期并格式化输出
name = generate_random_text(chinese_char, 2, 8) + "有限公司"
title = generate_random_text(chinese_char, 5, 10)
subtitle = generate_random_text(chinese_char, 10, 20)
content = generate_content()
inscribe = name + "\n" + random_datetime(start_date, end_date).strftime('%Y年%m月%d日')
- 完整代码:
def generate_info_random_by_char():
"""
随机生成公司名称, 标题, 子标题, 内容, 落款日期
"""
def generate_random_text(char_list, min_length, max_length):
"""
随机生成对应的内容
:param char_list:
:param min_length:
:param max_length:
"""
length = random.randint(min_length, max_length)
return ''.join(random.choice(char_list) for _ in range(length))
def generate_content():
"""
生成随机段落列表
"""
paragraphs = []
num_paragraphs = random.randint(3, 7) # 生成 3 到 7 段落
for _ in range(num_paragraphs):
num_sentences = random.randint(3, 5) # 每段包含 3 到 5 句
sentences = [generate_random_text(chinese_char, 8, 15) for _ in range(num_sentences)]
paragraph = '。'.join(sentences) + '。' # 用句号分隔句子并在末尾加句号
paragraphs.append(paragraph)
return paragraphs # 用换行分隔段落
chinese_char = ['的', '一', '是', '在', '不', '了', '有', '和', '人', '这', '中', '大', '为', '上', '个', '国',
'我', '以', '要', '他', '时', '来', '用', '们', '生', '到', '作', '地', '于', '出', '就', '分',
'对', '成', '会', '可', '主', '发', '年', '动', '同', '工', '也', '能', '下', '过', '子', '说',
'产', '种', '面', '而', '方', '后', '多', '定', '行', '学', '法', '所', '民', '得', '经', '十',
'三', '之', '进', '着', '等', '部', '度', '家', '电', '力', '里', '如', '水', '化', '高', '自',
'二', '理', '起', '小', '物', '现', '实', '加', '量', '都', '两', '体', '制', '机', '当', '使',
'点', '从', '业', '本', '去', '把', '性', '好', '应', '开', '它', '合', '还', '因', '由', '其',
'些', '然', '前', '外', '天', '政', '四', '日', '那', '社', '义', '事', '平', '形', '相', '全',
'表', '间', '样', '与', '关', '各', '重新']
start_date = datetime(1990, 1, 1)
end_date = datetime(2020, 12, 31)
name = generate_random_text(chinese_char, 2, 8) + "有限公司"
title = generate_random_text(chinese_char, 5, 10)
subtitle = generate_random_text(chinese_char, 10, 20)
content = generate_content()
inscribe = name + "\n" + random_datetime(start_date, end_date).strftime('%Y年%m月%d日')
return name, title, subtitle, content, inscribe
3.2 生成印章图片
- 确定参数,即可以修改的变量:
:param save_stamp_path: 将要保存的印章路径
:param center_text: 印章中间的文字
:param circle_text: 印章周围的问题
:param width: 印章的宽度
:param height: 印章的高度
:param background_color: 印章的背景颜色
:param border_color: 印章的外边框颜色
:param fill_color: 印章的填充颜色
:param font_size: 设置印章内部文字大小
:parma angle_gap: 字体之间的间距
- 首先建立空白画面
# 创建一个白色背景的图片
image = Image.new('RGB', (width, height), background_color)
draw = ImageDraw.Draw(image)
- 绘制印章的圆形外边框
# 绘制圆形印章的外边框
draw.ellipse((10, 10, width - 10, height - 10), outline=border_color, width=5)
- 设置印章内部的字体大小
# 设置印章内部文字的字体和大小
font = ImageFont.truetype(font_file, font_size)
- 在印章的中间添加文字
# 在印章中间添加文字
text_bbox = draw.textbbox((0, 0), center_text, font=font)
text_width, text_height = text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1]
text_x = (width - text_width) / 2
text_y = (height - text_height) / 2
draw.text((text_x, text_y), center_text, fill=fill_color, font=font)
- 在印章圆圈周围绘制弯曲的文字:
# 绘制弯曲的文字
circle_radius = 110
circle_center = (width // 2, height // 2)
start_angle = - 90 - (len(circle_text) - 1) / 2 * angle_gap
for i, char in enumerate(circle_text):
angle = np.radians(start_angle + i * angle_gap)
x = circle_center[0] + circle_radius * np.cos(angle)
y = circle_center[1] + circle_radius * np.sin(angle)
char_image = Image.new('RGBA', (font_size * 2, font_size * 2), (0, 0, 0, 0))
image.paste(char_image, (int(x - font_size), int(y - font_size)), char_image)
- 至此,将上述代码组装起来,为其设置印章中的内容,可以得到:
3.3 生成word文件
本部分主要用于生成包含标题、子标题、内容、落款和印章的Word文档
- 本部分所需要的参数:
def generate_report_with_stamp(stamp_path, title, sub_title, content, inscribe, save_word_path):
"""
生成word文档, 其中落款和印章在一起
:param stamp_path: 印章的png路径
:param title: 文档的标题
:param sub_title: 文档的子标题
:param content: 文档的内容
:param inscribe: 文档的落款
:param save_word_path: 文档保存的路径
:return:
"""
- 新建一个空白文档
doc = Document()
- 添加一个段落并设置标题的格式,包括字体大小、加粗、颜色和字体
# 设置标题
title_paragraph = doc.add_paragraph()
title_run = title_paragraph.add_run(title)
title_run.font.size = Pt(24)
title_run.bold = True
title_run.font.color.rgb = RGBColor(255, 0, 0)
title_run.font.name = '宋体'
title_run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
title_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
- 设置子标题的格式,包括字体大小和字体
# 设置子标题
sub_title_paragraph = doc.add_paragraph()
sub_title_run = sub_title_paragraph.add_run(sub_title)
sub_title_run.font.size = Pt(16)
sub_title_run.font.name = '宋体'
sub_title_run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
sub_title_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
- 通过遍历内容列表,逐段落添加内容,并设置每个段落的格式,包括首行缩进、行间距和段后间距
# 设置内容
for tmp_content in content:
content_paragraph = doc.add_paragraph(tmp_content)
content_paragraph.paragraph_format.first_line_indent = Pt(28)
content_paragraph.paragraph_format.line_spacing = Pt(20)
content_paragraph.paragraph_format.space_after = Pt(20)
content_run = content_paragraph.runs[0]
content_run.font.size = Pt(12)
content_run.font.name = '宋体'
content_run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
- 添加落款并设置其格式,使其右对齐
# 添加落款
inscribe_paragraph = doc.add_paragraph()
inscribe_run = inscribe_paragraph.add_run(inscribe)
inscribe_run.font.size = Pt(12)
inscribe_run.font.name = '宋体'
inscribe_run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
inscribe_paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.RIGHT
- 计算印章在文档中的大致位置,并将其插入到文档中。我们假设每行大约有33个字,并估算印章所在的行数
# 添加印章图片
line_number = sum([len(_) // 33 + 1 for _ in content]) + 5 # 根据字数估计印章的大致位置
p = doc.paragraphs[0]
add_float_picture(p, stamp_path, width=Cm(3.0), pos_x=Cm(16), pos_y=Cm(line_number))
- 将文档保存到指定路径
# 保存文档
doc.save(save_word_path)
- 结果如下图所示:
3.4 Word转PDF文件
本部分主要使用了comtypes
库与Microsoft Word进行交互
- 使用
comtypes.client.CreateObject
创建一个Word应用程序对象,并将其设置为不可见
# 初始化Word应用程序
word = comtypes.client.CreateObject('Word.Application')
word.Visible = False
- 使用
word.Documents.Open
方法打开指定路径的Word文档
# 打开Word文档
doc = word.Documents.Open(docx_path)
- 在进行转换操作之前,我们需要确保输出文件夹存在。如果不存在,则创建该文件夹
# 确保输出文件夹存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
- 遍历文档中的每一页,并将其导出为PDF
for i in range(1, doc.ComputeStatistics(2) + 1): # 2 means wdStatisticPages
# 将页面导出为PNG图片
output_path = os.path.join(output_folder, os.path.basename(docx_path).replace('.docx', '.pdf'))
doc.ExportAsFixedFormat(
OutputFileName=output_path,
ExportFormat=17, # 17 means wdExportFormatPNG
Item=0, # 0 means wdExportDocumentContent
CreateBookmarks=0, # 0 means wdExportCreateNoBookmarks
DocStructureTags=True
)
- 结果如下图所示:
3.5 PDF转图像
- 直接使用下述代码即可将PDF转换为图像:
def convert_pdf_to_png(pdf_path, output_image_path):
"""
Converts pdf file to png file
:param pdf_path: The pdf file
:param output_image_path: img output path
:return:
"""
pages = convert_from_path(pdf_path, dpi=300)
for i, page in enumerate(pages):
page.save(output_image_path, 'PNG')
- 结果如下图所示:
4 labelme标记
labelme标记印章只有一个类别,所以我们将其命名为Stamp,另外需要打开后使用打开目录打开含有图片的文件夹,还需要在文件->更改输出路径
中更改输出路径,也就是json保存的位置
使用教程参考这篇文章的第三章:http://t.csdnimg.cn/nCKzM
5 yolov5训练
- 将json转换为yolo格式的txt,可以直接使用下述函数:
def convert_dataset(json_base_dir, output_label_dir):
for name in os.listdir(json_base_dir):
json_path = os.path.join(json_base_dir, name)
with open(json_path, 'r') as f:
data = json.load(f)
imageHeight = data['imageHeight']
imageWidth = data['imageWidth']
yolo_annotations = []
for shape in data['shapes']:
points = shape['points']
x_min = points[0][0]
y_min = points[0][1]
x_max = points[1][0]
y_max = points[1][1]
# 计算中心点、宽和高
x_center = (x_min + x_max) / 2.0
y_center = (y_min + y_max) / 2.0
width = x_max - x_min
height = y_max - y_min
# 归一化
x_center /= imageWidth
y_center /= imageHeight
width /= imageWidth
height /= imageHeight
# YOLO格式: class x_center y_center width height
yolo_annotation = f"0 {x_center} {y_center} {width} {height}\n"
yolo_annotations.append(yolo_annotation)
# 保存到txt文件
txt_path = os.path.join(output_label_dir, name.replace('.json', '.txt'))
with open(txt_path, 'w') as f:
f.writelines(yolo_annotations)
-
下载yolov5项目并解压:https://github.com/ultralytics/yolov5
-
修改其中的配置文件
data/coco.yaml
,把下面的内容粘贴到该配置文件中,注意修改自己数据集的实际路径:
path: ./dataset/stamp # dataset root dir
train: train/images # train images (relative to 'path') 118287 images
val: val/images # val images (relative to 'path') 5000 images
# Classes
names:
0: stamp
-
安装虚拟环境,这里直接给出命令:
conda create -n Stamp python=3.9.19
conda activate Stamp
conda install pytorch==1.11.0 torchvision==0.12.0 torchaudio==0.11.0 cudatoolkit=11.3 -c pytorch
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
-
使用下述命令开启训练:
python train.py --data stamp.yaml --weights yolov5s.pt --img 640 --batch-size 4 --epochs 30
训练完成后会得到:
-
利用训练好的模型进行检测:
python detect.py --weights runs/train/exp/weights/best.pt --source dataset/Stamp/images/6.png
得到的结果为: