python实现简单图片物体标注工具


Posted in Python onMarch 18, 2019

本文实例为大家分享了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()

 以下是实现后的效果:

python实现简单图片物体标注工具

需要的文件

python实现简单图片物体标注工具

.labels文件内容格式

python实现简单图片物体标注工具

选择文件夹

python实现简单图片物体标注工具

进行标注

python实现简单图片物体标注工具

生成相应标签内容

python实现简单图片物体标注工具

标注结果
标注后的文件格式为:物体,左上角(起点)和右下角(终点)的坐标。

参考资料: 《深度学习与计算机视觉——算法原理、框架应用与代码实现》 叶韵(编著)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
学习python 之编写简单乘法运算题
Feb 27 Python
理解生产者消费者模型及在Python编程中的运用实例
Jun 26 Python
Django返回json数据用法示例
Sep 18 Python
Python将DataFrame的某一列作为index的方法
Apr 08 Python
python 删除非空文件夹的实例
Apr 26 Python
Tensorflow 训练自己的数据集将数据直接导入到内存
Jun 19 Python
python aiohttp的使用详解
Jun 20 Python
Python3操作读写CSV文件使用包过程解析
Apr 10 Python
Python操作Elasticsearch处理timeout超时
Jul 17 Python
Python 中Operator模块的使用
Jan 30 Python
python基础学习之递归函数知识总结
May 26 Python
上帝为你开了一扇窗之Tkinter常用函数详解
Jun 02 Python
Python面向对象程序设计之类的定义与继承简单示例
Mar 18 #Python
Python动态赋值的陷阱知识点总结
Mar 17 #Python
Python将字符串常量转化为变量方法总结
Mar 17 #Python
实例讲解Python中整数的最大值输出
Mar 17 #Python
python3+selenium自动化测试框架详解
Mar 17 #Python
Django 中间键和上下文处理器的使用
Mar 17 #Python
Python时间和字符串转换操作实例分析
Mar 16 #Python
You might like
PHP实现对png图像进行缩放的方法(支持透明背景)
2015/07/15 PHP
PHP Echo字符串的连接格式
2016/03/07 PHP
PHP扩展框架之Yaf框架的安装与使用
2016/05/18 PHP
Laravel框架实现利用中间件进行操作日志记录功能
2018/06/06 PHP
基于jQuery实现点击同时更改两个iframe的网址
2010/07/01 Javascript
js 编码转换 gb2312 和 utf8 互转的2种方法
2013/08/07 Javascript
jquery插件jSignature实现手动签名
2015/05/04 Javascript
php结合imgareaselect实现图片裁剪
2015/07/05 Javascript
jQuery.each使用详解
2015/07/07 Javascript
Node.js巧妙实现Web应用代码热更新
2015/10/22 Javascript
基于JS实现新闻列表无缝向上滚动实例代码
2016/01/22 Javascript
jQuery实现的小图列表,大图展示效果幻灯片示例
2016/10/25 Javascript
使用JS模拟锚点跳转的实例
2018/02/01 Javascript
Vue.extend实现挂载到实例上的方法
2019/05/01 Javascript
React+TypeScript+webpack4多入口配置详解
2019/08/08 Javascript
解决node终端下运行js文件不支持ES6语法
2020/04/04 Javascript
微信小程序订阅消息(java后端实现)开发
2020/06/01 Javascript
微信小程序清空输入框信息与实现屏幕往上滚动的示例代码
2020/06/23 Javascript
从头学Python之编写可执行的.py文件
2017/11/28 Python
django文档学习之applications使用详解
2018/01/29 Python
Tensorflow卷积神经网络实例进阶
2018/05/24 Python
PyTorch的深度学习入门之PyTorch安装和配置
2019/06/27 Python
keras 特征图可视化实例(中间层)
2020/01/24 Python
K最近邻算法(KNN)---sklearn+python实现方式
2020/02/24 Python
python应用Axes3D绘图(批量梯度下降算法)
2020/03/25 Python
Python sql注入 过滤字符串的非法字符实例
2020/04/03 Python
导致python中import错误的原因是什么
2020/07/01 Python
利用纯CSS3实现tab选项卡切换示例代码
2016/09/21 HTML / CSS
使用CSS3实现input多选框自定义样式的方法示例
2019/07/19 HTML / CSS
html5与css3小应用
2013/04/03 HTML / CSS
通过Canvas及File API缩放并上传图片完整示例
2013/08/08 HTML / CSS
2015年母亲节活动策划方案
2015/05/04 职场文书
我的暑假生活作文(五年级)范文
2019/08/07 职场文书
深入详解JS函数的柯里化
2021/06/09 Javascript
MySQL基础快速入门知识总结(附思维导图)
2021/09/25 MySQL
Redis调用Lua脚本及使用场景快速掌握
2022/03/16 Redis