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中基于生成器的状态机
Apr 13 Python
Python中处理字符串之endswith()方法的使用简介
May 18 Python
详解Python中的strftime()方法的使用
May 22 Python
Python切片知识解析
Mar 06 Python
python看某个模块的版本方法
Oct 16 Python
Python2.7版os.path.isdir中文路径返回false的解决方法
Jun 21 Python
Python实现Singleton模式的方式详解
Aug 08 Python
python 采用paramiko 远程执行命令及报错解决
Oct 21 Python
Pytorch maxpool的ceil_mode用法
Feb 18 Python
python不到50行代码完成了多张excel合并的实现示例
May 28 Python
python 密码学示例——凯撒密码的实现
Sep 21 Python
整理Python中常用的conda命令操作
Jun 15 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游戏编程25个脚本代码
2011/02/08 PHP
PHP substr 截取字符串出现乱码问题解决方法[utf8与gb2312]
2011/12/16 PHP
memcache一致性hash的php实现方法
2015/03/05 PHP
PHP上传图片时判断上传文件是否为可用图片的方法
2016/10/20 PHP
tp5(thinkPHP5)框架连接数据库的方法示例
2018/12/24 PHP
avascript中的自执行匿名函数应用示例
2014/09/15 Javascript
javascript关于运动的各种问题经典总结
2015/04/27 Javascript
JavaScript中Number.NEGATIVE_INFINITY值的使用详解
2015/06/05 Javascript
javascript动态添加删除tabs标签的方法
2015/07/06 Javascript
angularjs 源码解析之injector
2016/08/22 Javascript
JS常用知识点整理
2017/01/21 Javascript
解决微信内置浏览器返回上一页强制刷新问题方法
2017/02/05 Javascript
微信小程序wepy框架笔记小结
2018/08/08 Javascript
使用react render props实现倒计时的示例代码
2018/12/06 Javascript
Vue.js仿Select下拉框效果
2020/02/18 Javascript
Python闭包之返回函数的函数用法示例
2018/01/27 Python
PyQt5每天必学之事件与信号
2018/04/20 Python
python 列表,数组和矩阵sum的用法及区别介绍
2018/06/28 Python
Python常用模块os.path之文件及路径操作方法
2019/12/03 Python
tensorflow 保存模型和取出中间权重例子
2020/01/24 Python
Python3基本输入与输出操作实例分析
2020/02/14 Python
使用python从三个角度解决josephus问题的方法
2020/03/27 Python
Python bisect模块原理及常见实例
2020/06/17 Python
英国空调、除湿机和通风设备排名第一:Air Con Centre
2019/02/25 全球购物
卡骆驰德国官方网站:Crocs德国
2019/03/29 全球购物
Invicta手表官方商店:百年制表历史的瑞士腕表品牌
2019/09/26 全球购物
WEB控件及HTML服务端控件能否调用客户端方法?如果能,请解释如何调用?
2015/08/25 面试题
学年自我鉴定范文
2013/10/01 职场文书
普师专业个人自荐信范文
2013/11/26 职场文书
庆六一开幕词
2015/01/29 职场文书
个人工作保证书
2015/02/28 职场文书
一波干货,会议主持词开场白范文
2019/05/06 职场文书
MySQL数据迁移相关总结
2021/04/29 MySQL
CSS3新特性详解(五):多列columns column-count和flex布局
2021/04/30 HTML / CSS
详解Go与PHP的语法对比
2021/05/29 PHP
Golang 字符串的常见操作
2022/04/19 Golang