python实现简单图片物体标注工具
作者:weixin_34290352 发布时间:2021-09-07 21:31:50
标签:python,标注工具
本文实例为大家分享了python实现简单图片物体标注工具的具体代码,供大家参考,具体内容如下
# coding: utf-8
"""
物体检测标注小工具
基本思路:
对要标注的图像建立一个窗口循环,然后每次循环的时候对图像进行一次复制,
鼠标在画面上画框的操作、画好的框的相关信息在全局变量中保存,
并且在每个循环中根据这些信息,在复制的图像上重新画一遍,然后显示这份复制的图像。
简化的设计过程:
1、输入是一个文件夹的路径,包含了所需标注物体框的图片。
如果图片中标注了物体,则生成一个相同名称加额外后缀_bbox的文件,来保存标注信息。
2、标注的方式:按下鼠标左键选择物体框的左上角,松开鼠标左键选择物体框的右下角,
按下鼠标右键删除上一个标注好的物体框。
所有待标注物体的类别和标注框颜色由用户自定义。
如果没有定义则默认只标注一种物体,定义该物体名称为Object。
3、方向键 ← 和 → 键用来遍历图片, ↑ 和 ↓ 键用来选择当前要标注的物体,
Delete键删除一种脏图片和对应的标注信息。
自定义标注物体和颜色的信息用一个元组表示
第一个元素表示物体名字
第二个元素表示BGR颜色的tuple或者代表标注框坐标的元祖
利用repr()保存和eval()读取
"""
"""
一些说明:
1. 标注相关的物体标签文件即 .labels 结尾的文件,需要与所选文件夹添加到同一个根目录下
一定要注意这一点,否则无法更新标注物体的类型标签,致使从始至终都只有一个默认物体出现
我就是这个原因,拖了两三天才整好,当然也顺便仔细的读了这篇代码。同时也学习了@staticmethod以及相应Python的decorator的知识。
可以说,在曲折中前进才是棒的。
2. .labels文件为预设物体标签文件,其内容具体格式为:
'object1', (B, G, R)
'object2', (B, G, R)
'object3', (B, G, R)……
具体见文后图片。
3. 最后生成的标注文件,在文后会有,到时再进行解释。
"""
import os
import cv2
# tkinter是Python内置的简单GUI库,实现打开文件夹、确认删除等操作十分方便
from tkMessageBox import askyesno
# 定义标注窗口的默认名称
WINDOW_NAME = 'Simple Bounding Box Labeling Tool'
# 定义画面刷新帧率
FPS = 24
# 定义支持的图像格式
SUPPORTED_FORMATS = ['jpg', 'jpeg', 'png']
# 定义默认物体框的名字为Object,颜色为蓝色,当没有用户自定义物体时,使用该物体
DEFAULT_COLOR = {'Object': (255, 0, 0)}
# 定义灰色,用于信息显示的背景和未定义物体框的显示
COLOR_GRAY = (192, 192, 192)
# 在图像下方多处BAR_HEIGHT的区域,用于显示信息
BAR_HEIGHT = 16
# 上下左右,DELETE键对应的cv2.waitKey()函数的返回值
KEY_UP = 2490368
KEY_DOWN = 2621440
KEY_LEFT = 2424832
KEY_RIGHT = 2555904
KEY_DELETE = 3014656
# 空键用于默认循环
KEY_EMPTY = 0
get_bbox_name = '{}.bbox'.format
# 定义物体框标注工具类
class SimpleBBoxLabeling:
def __init__(self, data_dir, fps=FPS, windown_name=WINDOW_NAME):
self._data_dir = data_dir
self.fps = fps
self.window_name = windown_name if windown_name else WINDOW_NAME
# pt0 是正在画的左上角坐标, pt1 是鼠标所在坐标
self._pt0 = None
self._pt1 = None
# 表明当前是否正在画框的状态标记
self._drawing = False
# 当前标注物体的名称
self._cur_label = None
# 当前图像对应的所有已标注框
self._bboxes = []
# 如果有用户自己定义的标注信息则读取,否则使用默认的物体和颜色
label_path = '{}.labels'.format(self._data_dir)
self.label_colors = DEFAULT_COLOR if not os.path.exists(label_path) else self.load_labels(label_path)
# self.label_colors = self.load_labels(label_path)
# 获取已经标注的文件列表和未标注的文件列表
imagefiles = [x for x in os.listdir(self._data_dir) if x[x.rfind('.') + 1:].lower() in SUPPORTED_FORMATS]
labeled = [x for x in imagefiles if os.path.exists(get_bbox_name(x))]
to_be_labeled = [x for x in imagefiles if x not in labeled]
# 每次打开一个文件夹,都自动从还未标注的第一张开始
self._filelist = labeled + to_be_labeled
self._index = len(labeled)
if self._index > len(self._filelist) - 1:
self._index = len(self._filelist) - 1
# 鼠标回调函数
def _mouse_ops(self, event, x, y, flags, param):
# 按下左键,坐标为左上角,同时表示开始画框,改变drawing,标记为True
if event == cv2.EVENT_LBUTTONDOWN:
self._drawing = True
self._pt0 = (x, y)
# 松开左键,表明画框结束,坐标为有效较并保存,同时改变drawing,标记为False
elif event == cv2.EVENT_LBUTTONUP:
self._drawing = False
self._pt1 = (x, y)
self._bboxes.append((self._cur_label, (self._pt0, self._pt1)))
# 实时更新右下角坐标
elif event == cv2.EVENT_MOUSEMOVE:
self._pt1 = (x, y)
# 按下鼠标右键删除最近画好的框
elif event == cv2.EVENT_RBUTTONUP:
if self._bboxes:
self._bboxes.pop()
# 清除所有标注框和当前状态
def _clean_bbox(self):
self._pt0 = None
self._pt1 = None
self._drawing = False
self._bboxes = []
# 画标注框和当前信息的函数
def _draw_bbox(self, img):
# 在图像下方多出BAR_HEIGHT的区域,显示物体信息
h, w = img.shape[:2]
canvas = cv2.copyMakeBorder(img, 0, BAR_HEIGHT, 0, 0, cv2.BORDER_CONSTANT, value=COLOR_GRAY)
# 正在标注的物体信息,如果鼠标左键已经按下,则像是两个点坐标,否则显示当前待标注物体的名
label_msg = '{}: {}, {}'.format(self._cur_label, self._pt0, self._pt1) \
if self._drawing \
else 'Current label: {}'.format(self._cur_label)
# 显示当前文件名,文件个数信息
msg = '{}/{}: {} | {}'.format(self._index + 1, len(self._filelist), self._filelist[self._index], label_msg)
cv2.putText(canvas, msg, (1, h+12), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
# 画出已经标好的框和对应名字
for label, (bpt0, bpt1) in self._bboxes:
label_color = self.label_colors[label] if label in self.label_colors else COLOR_GRAY
cv2.rectangle(canvas, bpt0, bpt1, label_color, thickness=2)
cv2.putText(canvas, label, (bpt0[0]+3, bpt0[1]+15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, label_color, 2)
# 画正在标注的框和对应名字
if self._drawing:
label_color = self.label_colors[self._cur_label] if self._cur_label in self.label_colors else COLOR_GRAY
if (self._pt1[0] >= self._pt0[0]) and (self._pt1[1] >= self._pt1[0]):
cv2.rectangle(canvas, self._pt0, self._pt1, label_color, thickness=2)
cv2.putText(canvas, self._cur_label, (self._pt0[0] + 3, self._pt0[1] + 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, label_color, 2)
return canvas
# 利用repr()函数导出标注框数据到文件
@staticmethod
def export_bbox(filepath, bboxes):
if bboxes:
with open(filepath, 'w') as f:
for bbox in bboxes:
line = repr(bbox) + '\n'
f.write(line)
elif os.path.exists(filepath):
os.remove(filepath)
# 利用eval()函数读取标注框字符串到数据
@staticmethod
def load_bbox(filepath):
bboxes = []
with open(filepath, 'r') as f:
line = f.readline().rstrip()
while line:
bboxes.append(eval(line))
line = f.readline().rstrip()
return bboxes
# 利用eval()函数读取物体及对应颜色信息到数据
@staticmethod
def load_labels(filepath):
label_colors = {}
with open(filepath, 'r') as f:
line = f.readline().rstrip()
while line:
label, color = eval(line)
label_colors[label] = color
line = f.readline().rstrip()
print label_colors
return label_colors
# 读取图像文件和对应标注框信息(如果有的话)
@staticmethod
def load_sample(filepath):
img = cv2.imread(filepath)
bbox_filepath = get_bbox_name(filepath)
bboxes = []
if os.path.exists(bbox_filepath):
bboxes = SimpleBBoxLabeling.load_bbox(bbox_filepath)
return img, bboxes
# 导出当前标注框信息并清空
def _export_n_clean_bbox(self):
bbox_filepath = os.sep.join([self._data_dir, get_bbox_name(self._filelist[self._index])])
self.export_bbox(bbox_filepath, self._bboxes)
self._clean_bbox()
# 删除当前样本和对应的标注框信息
def _delete_current_sample(self):
filename = self._filelist[self._index]
filepath = os.sep.join([self._data_dir, filename])
if os.path.exists(filepath):
os.remove(filepath)
filepath = get_bbox_name(filepath)
if os.path.exists(filepath):
os.remove(filepath)
self._filelist.pop(self._index)
print('{} is deleted!'.format(filename))
# 开始OpenCV窗口循环的方法,程序的主逻辑
def start(self):
# 之前标注的文件名,用于程序判断是否需要执行一次图像读取
last_filename = ''
# 标注物体在列表中的下标
label_index = 0
# 所有标注物体名称的列表
labels = self.label_colors.keys()
# 带标注物体的种类数
n_labels = len(labels)
# 定义窗口和鼠标回调
cv2.namedWindow(self.window_name)
cv2.setMouseCallback(self.window_name, self._mouse_ops)
key = KEY_EMPTY
# 定义每次循环的持续时间
delay = int(1000 / FPS)
# 只要没有按下Delete键,就持续循环
while key != KEY_DELETE:
# 上下方向键选择当前标注物体
if key == KEY_UP:
if label_index == 0:
pass
else:
label_index -= 1
elif key == KEY_DOWN:
if label_index == n_labels - 1:
pass
else:
label_index += 1
# 左右方向键选择标注图片
elif key == KEY_LEFT:
# 已经到了第一张图片的话就不需要清空上一张
if self._index > 0:
self._export_n_clean_bbox()
self._index -= 1
if self._index < 0:
self._index = 0
elif key == KEY_RIGHT:
# 已经到了最后一张图片的就不需要清空上一张
if self._index < len(self._filelist) - 1:
self._export_n_clean_bbox()
self._index += 1
if self._index > len(self._filelist) - 1:
self._index = len(self._filelist) - 1
# 删除当前图片和对应标注的信息
elif key == KEY_DELETE:
if askyesno('Delete Sample', 'Are you sure?'):
self._delete_current_sample()
key = KEY_EMPTY
continue
# 如果键盘操作执行了换图片, 则重新读取, 更新图片
filename = self._filelist[self._index]
if filename != last_filename:
filepath = os.sep.join([self._data_dir, filename])
img, self._bboxes = self.load_sample(filepath)
# 更新当前标注物体名称
self._cur_label = labels[label_index]
# 把标注和相关信息画在图片上并显示指定的时间
canvas = self._draw_bbox(img)
cv2.imshow(self.window_name, canvas)
key = cv2.waitKey(delay)
# 当前文件名就是下次循环的老文件名
last_filename = filename
print 'Finished!'
cv2.destroyAllWindows()
#如果退出程序,需要对当前文件进行保存
self.export_bbox(os.sep.join([self._data_dir, get_bbox_name(filename)]), self._bboxes)
print 'Labels updated!'
以上实现了工具类,当然需要一个入口函数,将工具类保存为SimpleBBoxLabeling.py,新建Run_Detect.py,写以下内容:
# coding:utf-8
# tkinter是Python内置的简单GUI库,实现打开文件夹、确认删除等操作十分方便
from tkFileDialog import askdirectory
# 导入创建的工具类
from SimpleBBoxLabeling import SimpleBBoxLabeling
if __name__ == '__main__':
dir_with_images = askdirectory(title='Where is the images?')
labeling_task = SimpleBBoxLabeling(dir_with_images)
labeling_task.start()
以下是实现后的效果:
需要的文件
.labels文件内容格式
选择文件夹
进行标注
生成相应标签内容
标注结果
标注后的文件格式为:物体,左上角(起点)和右下角(终点)的坐标。
参考资料: 《深度学习与计算机视觉——算法原理、框架应用与代码实现》 叶韵(编著)
来源:https://blog.csdn.net/weixin_34290352/article/details/87630162


猜你喜欢
- MySQL是目前十分流行的一种关系型数据库管理系统。官网推出的安装包有两种格式,分别是:ZIP格式和MSI格式。其中MSI格式的可以直接点击
- 写这篇文章的缘由是我使用 reqeusts 库请求接口的时候, 直接使用请求参数里的 json 字段发送数据, 但是服务器无法识别我发送的数
- 导语🎼嗨,大宝贝们,又到周末啦,今天你放假了嘛?周末的日子总是无所事事,无所事事。一直在想做什么游戏,给大家来点儿新鲜感,这不?玩游戏、找游
- 如下代码,限制某个函数在某个时间段的调用次数,灵感来源:python装饰器-限制函数调用次数的方法(10s调用一次) 欢迎访问原博客中指定的
- jsp登陆验证,网页登陆验证带验证码校验,登录功能之添加验证码part_1:专门用于生成一个验证码图片的类:VerificationCode
- python实现情感分析(Word2Vec)** 前几天跟着老师做了几个项目,老师写的时候劈里啪啦一顿敲,写了个啥咱也布吉岛,线下自己就瞎琢
- 本文实例讲述了PHP实现将MySQL重复ID二维数组重组为三维数组的方法。分享给大家供大家参考,具体如下:应用场景MYSQL在使用关联查询时
- 在我们开始之前,一定要注意这篇文章只针对Windows用户!对于那些使用Windows的人来说,这是一个有趣的想法。如果您想使用python
- selenium关闭窗口有两个方法,close与quit,我们稍作研究便知道这两个方法的区别。1.看源码或API这是close()的说明:C
- vscode安装python库1.已经在vscode中装了python并配置好python运行环境。检查是否正确配置好运行环境,按Windo
- 用pandas计算相关系数计算相关系数用pandas,比如我想知道风速大小与风向紊乱(标准差来衡量)之间的相关系数,下面是代码:import
- 本文实例讲述了Python学习笔记之列表推导式。分享给大家供大家参考,具体如下:列表推导式列表推导式可以快速简练地创建列表之前的复杂写法:c
- SQL查询中什么时候需要使用表别名?今天写MySQL时遇到使用表别名的问题,这里重新总结一下。1、 表名很长时select * from w
- 许多共享主机的服务提供商不允许运行你自己的服务进程,也不允许修改 httpd.conf 文件。 尽管如此,仍然有可能通过Web服务器产生的子
- 基本语法在讲述if-else时已经提到,如果有多个判断条件,Go语言中提供了Switch-Case的方式。如果switch后面不带条件相当于
- 参考官方案例:https://docs.python.org/zh-cn/3.8/howto/logging-cookbook.htmlim
- 数据去重可以使用duplicated()和drop_duplicates()两个方法。DataFrame.duplicated(subset
- 我就废话不多说了,大家还是直接看代码吧~import pandas as pdimport numpy as npcolumns = [[&
- 1. 关于上传图片失败的问题首先导入jar包 commons-fileupload-1.2.2.jar,ueditor.jar然后修改edi
- 需求:因需要将一json文件中大量的信息填入一固定格式的Excel表格,单纯的复制粘贴肯定也能完成,但是想偷懒一下,于是借助Python解决