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使用pil生成缩略图的方法
Mar 26 Python
Python缩进和冒号详解
Jun 01 Python
Python 专题四 文件基础知识
Mar 20 Python
新手如何快速入门Python(菜鸟必看篇)
Jun 10 Python
Python中偏函数用法示例
Jun 07 Python
对python 匹配字符串开头和结尾的方法详解
Oct 27 Python
Python这样操作能存储100多万行的xlsx文件
Apr 16 Python
python中的&amp;&amp;及||的实现示例
Aug 07 Python
给Python学习者的文件读写指南(含基础与进阶)
Jan 29 Python
利用Python脚本批量生成SQL语句
Mar 04 Python
Python爬虫爬取博客实现可视化过程解析
Jun 29 Python
Python 如何实现访问者模式
Jul 28 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
phpmailer在服务器上不能正常发送邮件的解决办法
2014/07/08 PHP
PHP分页类集锦
2014/11/18 PHP
php和C#的yield迭代器实现方法对比分析
2019/07/17 PHP
PHP7生产环境队列Beanstalkd用法详解
2020/05/19 PHP
setTimeout 不断吐食CPU的问题分析
2009/04/01 Javascript
jQuery的控件及事件(输入控件及回车事件)使用示例
2013/07/25 Javascript
js中settimeout方法加参数
2014/02/28 Javascript
移动设备web开发首选框架:zeptojs介绍
2015/01/29 Javascript
JS解析XML实例分析
2015/01/30 Javascript
js实现a标签超链接提交form表单的方法
2015/06/24 Javascript
JS实现超精简响应鼠标显示二级菜单代码
2015/09/12 Javascript
深入浅析JavaScript中的Function类型
2016/07/09 Javascript
jQuery实现radio第一次点击选中第二次点击取消功能
2017/05/15 jQuery
Nodejs中使用captchapng模块生成图片验证码
2017/05/18 NodeJs
React通过父组件传递类名给子组件的实现方法
2017/11/13 Javascript
nodejs基于WS模块实现WebSocket聊天功能的方法
2018/01/12 NodeJs
深入浅析Node环境和浏览器的区别
2018/08/14 Javascript
深入理解JS中Number(),parseInt(),parseFloat()三者比较
2018/08/24 Javascript
AngularJs中$cookies简单用法分析
2019/05/30 Javascript
使用微信SDK自定义分享的方法
2019/07/03 Javascript
原生JS实现无缝轮播图片
2020/06/24 Javascript
vue 监听窗口变化对页面部分元素重新渲染操作
2020/07/28 Javascript
解决vue+elementui项目打包后样式变化问题
2020/08/03 Javascript
[04:16]DOTA2英雄梦之声_第09期_斧王
2014/06/21 DOTA
Python 执行字符串表达式函数(eval exec execfile)
2014/08/11 Python
Python中subprocess模块用法实例详解
2015/05/20 Python
简单实现python聊天程序
2018/04/01 Python
在Python中关于使用os模块遍历目录的实现方法
2019/01/03 Python
opencv实现静态手势识别 opencv实现剪刀石头布游戏
2019/01/22 Python
HTML5实现多张图片上传功能
2016/03/11 HTML / CSS
俄罗斯在线服装店:STOLNIK
2021/03/07 全球购物
计算机软件个人的自荐信范文
2013/12/01 职场文书
暑假家长评语大全
2014/04/17 职场文书
学院党委班子四风问题自查报告及整改措施
2014/10/25 职场文书
应聘教师自荐信
2015/03/26 职场文书
中秋节作文(五年级)之关于月亮
2019/09/11 职场文书