您好,登錄后才能下訂單哦!
本文實例為大家分享了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文件內容格式
選擇文件夾
進行標注
生成相應標簽內容
標注結果
標注后的文件格式為:物體,左上角(起點)和右下角(終點)的坐標。
參考資料: 《深度學習與計算機視覺——算法原理、框架應用與代碼實現》 葉韻(編著)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。