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读取oracle函数返回值
Jul 18 Python
Python编程判断这天是这一年第几天的方法示例
Apr 18 Python
深入浅出分析Python装饰器用法
Jul 28 Python
Python cookbook(数据结构与算法)实现对不原生支持比较操作的对象排序算法示例
Mar 15 Python
python实现三维拟合的方法
Dec 29 Python
python+opencv打开摄像头,保存视频、拍照功能的实现方法
Jan 08 Python
Opencv-Python图像透视变换cv2.warpPerspective的示例
Apr 11 Python
Django实现微信小程序的登录验证功能并维护登录态
Jul 04 Python
python2.7实现复制大量文件及文件夹资料
Aug 31 Python
Python3实现二叉树的最大深度
Sep 30 Python
python游戏开发的五个案例分享
Mar 09 Python
python利用opencv实现颜色检测
Feb 23 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
玩转虚拟域名◎+ .
2006/10/09 PHP
PHP入门教程之图像处理技巧分析
2016/09/11 PHP
php监测数据是否成功插入到Mysql数据库的方法
2016/11/25 PHP
ASP.NET中AJAX 调用实例代码
2012/05/03 Javascript
异步动态加载JS并运行(示例代码)
2013/12/13 Javascript
javascript中数组的定义及使用实例
2015/01/21 Javascript
js实现简单的左右两边固定广告效果实例
2015/04/10 Javascript
js实现文本框选中的方法
2015/05/26 Javascript
JavaScript函数的调用以及参数传递
2015/10/21 Javascript
快速使用Bootstrap搭建传送带
2016/05/06 Javascript
微信小程序实现动态改变view标签宽度和高度的方法【附demo源码下载】
2017/12/05 Javascript
JS实现简易换图时钟功能分析
2018/01/04 Javascript
React 无状态组件(Stateless Component) 与高阶组件
2018/08/14 Javascript
Vue时间轴 vue-light-timeline的用法说明
2020/10/29 Javascript
python进程管理工具supervisor使用实例
2014/09/17 Python
Python使用CMD模块更优雅的运行脚本
2015/05/11 Python
Python基于高斯消元法计算线性方程组示例
2018/01/17 Python
Python3实现的简单验证码识别功能示例
2018/05/02 Python
浅谈Python中eval的强大与危害
2019/03/13 Python
python3 图片 4通道转成3通道 1通道转成3通道 图片压缩实例
2019/12/03 Python
Python+OpenCV实现将图像转换为二进制格式
2020/01/09 Python
Keras设定GPU使用内存大小方式(Tensorflow backend)
2020/05/22 Python
经验丰富程序员才知道的8种高级Python技巧
2020/07/27 Python
安装并免费使用Pycharm专业版(学生/教师)
2020/09/24 Python
python 基于Apscheduler实现定时任务
2020/12/15 Python
HTML5如何实现元素拖拽
2016/03/11 HTML / CSS
获取邓白氏信用报告:Dun & Bradstreet
2019/01/22 全球购物
美国最佳在线航班预订网站:LookupFare
2019/03/26 全球购物
财会自我鉴定范文
2013/12/27 职场文书
七年级英语教学反思
2014/01/15 职场文书
超市促销活动总结
2014/07/01 职场文书
北京故宫导游词
2015/01/31 职场文书
司机个人年终总结
2015/03/03 职场文书
宣传部部长竞选稿
2015/11/21 职场文书
《自然之道》读后感3篇
2019/12/17 职场文书
Java处理延时任务的常用几种解决方案
2022/06/01 Java/Android