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的print用法示例
Feb 11 Python
pygame学习笔记(3):运动速率、时间、事件、文字
Apr 15 Python
python验证码识别的实例详解
Sep 09 Python
python使用threading获取线程函数返回值的实现方法
Nov 15 Python
python如何删除文件、目录
Jun 23 Python
基于CentOS搭建Python Django环境过程解析
Aug 24 Python
PyTorch如何搭建一个简单的网络
Aug 24 Python
python 获取谷歌浏览器保存的密码
Jan 06 Python
python实现简单的井字棋游戏(gui界面)
Jan 22 Python
一文带你掌握Pyecharts地理数据可视化的方法
Feb 06 Python
Python集合的基础操作
Nov 01 Python
Python实现猜拳与猜数字游戏的方法详解
Apr 06 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的一些基础知识分享
2011/07/27 PHP
Laravel 框架路由原理与路由访问实例分析
2020/04/14 PHP
javascript中最常用的继承模式 组合继承
2010/08/12 Javascript
ExtJs中简单的登录界面制作方法
2010/08/19 Javascript
window.ActiveXObject使用说明
2010/11/08 Javascript
用dtree实现树形菜单 dtree使用说明
2011/10/17 Javascript
Extjs优化(一)删除冗余代码提高运行速度
2013/04/15 Javascript
jquery 提示信息显示后自动消失的具体实现
2013/12/18 Javascript
给应用部分的js代码设定一个统一的入口
2014/06/15 Javascript
jQuery制作拼图小游戏
2015/01/12 Javascript
jquery编写Tab选项卡滚动导航切换特效
2020/07/17 Javascript
JS简单实现表格排序功能示例
2016/12/20 Javascript
利用Vue v-model实现一个自定义的表单组件
2017/04/27 Javascript
AngularJs用户登录问题处理(交互及验证、阻止FQ处理)
2017/10/26 Javascript
在 Node.js 中使用 async 函数的方法
2017/11/17 Javascript
浅谈在react中如何实现扫码枪输入
2018/07/04 Javascript
使用next.js开发网址缩短服务的方法
2020/06/17 Javascript
vue中实现弹出层动画效果的示例代码
2020/09/25 Javascript
python实现简单聊天应用 python群聊和点对点均实现
2017/09/14 Python
Python实现合并两个列表的方法分析
2018/05/28 Python
python多环境切换及pyenv使用过程详解
2019/09/27 Python
Windows平台Python编程必会模块之pywin32介绍
2019/10/01 Python
python数据处理——对pandas进行数据变频或插值实例
2020/04/22 Python
python读取excel数据绘制简单曲线图的完整步骤记录
2020/10/30 Python
PHP面试题附答案
2015/11/28 面试题
2013年入党人员的自我鉴定
2013/10/25 职场文书
日语专业毕业生求职信
2013/12/04 职场文书
开业庆典邀请函
2014/01/08 职场文书
酒后驾车标语
2014/06/30 职场文书
反四风个人对照检查材料
2014/09/26 职场文书
法定代表人证明书
2014/11/28 职场文书
捐资助学感谢信
2015/01/21 职场文书
幼儿园园长个人总结
2015/03/02 职场文书
观后感的写法
2015/06/19 职场文书
中学教师读书笔记
2015/07/01 职场文书
2019 入党申请书范文
2019/07/10 职场文书