python實現簡單圖片物體標註工具

這篇文章主要爲大家詳細介紹了python實現簡單圖片物體標註工具,具有一定的參考價值,感興趣的小夥伴們可以參考一下

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

 以下是實現後的效果:

需要的文件


.labels文件內容格式


選擇文件夾


進行標註


生成相應標籤內容


標註結果
標註後的文件格式爲:物體,左上角(起點)和右下角(終點)的座標。

參考資料: 《深度學習與計算機視覺——算法原理、框架應用與代碼實現》 叶韻(編著)

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章