import os import re import cv2 import glob import numpy as np from PIL import Image, ImageDraw, ImageFont # 004这个代码 拉框有点卡,显示中文 能正常标注。 # 005 修改004的问题。 这个代码运行不显示图片。需要标注一下。才会显示。 # 006 显示图片了。 但是标注第2个图片,第1个图片的标注还存在。 class SmoothYOLOAnnotator: def __init__(self, image_folder): self.image_folder = image_folder self.images = sorted(glob.glob(os.path.join(image_folder, "*.jpg")) + glob.glob(os.path.join(image_folder, "*.png")), key=lambda x: int(re.search(r'\d+', os.path.basename(x)).group())) # glob.glob() 函数解析 通过通配符匹配文件路径,返回符合条件的文件列表 # * 任意数量字符 *.jpg匹配所有JPG # ? 单个字 pic?.png匹配pic1.png # [] 指定字符范围[a - z].txt匹配a - z开头的txt # sorted 对可迭代对象(列表、元组、字符串等)进行排序 返回一个新的排序后列表,不修改原对象 print(self.images) self.current_index = 0 #从第1个开始标注 0就是从1 1就是从第2个标注 self.annotations = [] self.temp_box = None self.classes = {1: "小明", 2: "小红", 3: "小刚"} self.cache_img = None self.font = self.load_font() self.window_name = "YOLO标注工具" self.crosshair_color = (0, 0, 255) # 红色十字线 self.crosshair_thickness = 1 def load_font(self): try: return ImageFont.truetype("simhei.ttf", 20) except: return ImageFont.load_default() def init_display(self): """初始化显示图片""" # 清空上一张图的标注 self.annotations = [] self.temp_box = None # 从图片列表中读取当前索引对应的图片 self.current_img = cv2.imread(self.images[self.current_index]) if self.current_img is None: print(f"无法加载图片: {self.images[self.current_index]}") return # 更新缓存图片(添加标注框和文字) # 重置缓存并更新显示 self.cache_img = None self.update_cache() # 在指定窗口中显示处理后的图片 cv2.imshow(self.window_name, self.cache_img) cv2.waitKey(1) # 强制刷新显示 def update_cache(self): # 复制当前图片到缓存(避免修改原图) 所有修改都在缓存图片上进行,不破坏原图 self.cache_img = self.current_img.copy() # 遍历所有标注信息(class_id是类别,x1,y1是左上角,x2,y2是右下角) for (class_id, x1, y1, x2, y2) in self.annotations: # 画绿色矩形框(线宽2像素) cv2.rectangle(self.cache_img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 转换到PIL格式(因为OpenCV和PIL处理图片方式不同) pil_img = Image.fromarray(cv2.cvtColor(self.cache_img, cv2.COLOR_BGR2RGB)) # 准备在图片上写字 绿色矩形框(标记物体位置) # 绿色文字标签(说明物体类别) draw = ImageDraw.Draw(pil_img) # 在框上方25像素处写类别名称(绿色文字) draw.text((x1, y1 - 25), self.classes[class_id], font=self.font, fill=(0, 255, 0)) # 转回OpenCV格式 self.cache_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) def mouse_callback(self, event, x, y, flags, param): """增强的鼠标回调函数""" # 十字线绘制逻辑 display_img = self.cache_img.copy() if self.cache_img is not None else self.current_img.copy() cv2.line(display_img, (0, y), (display_img.shape[1], y), self.crosshair_color, self.crosshair_thickness) cv2.line(display_img, (x, 0), (x, display_img.shape[0]), self.crosshair_color, self.crosshair_thickness) # 十字线绘制逻辑 if event == cv2.EVENT_LBUTTONDOWN: self.temp_box = [x, y, x, y] elif event == cv2.EVENT_MOUSEMOVE: if self.temp_box: self.temp_box[2:] = [x, y] display_img = self.cache_img.copy() cv2.rectangle(display_img, (self.temp_box[0], self.temp_box[1]), (self.temp_box[2], self.temp_box[3]), (255, 0, 0), 1) cv2.imshow(self.window_name, display_img) elif event == cv2.EVENT_LBUTTONUP: if self.temp_box: x1, y1 = min(self.temp_box[0], x), min(self.temp_box[1], y) x2, y2 = max(self.temp_box[0], x), max(self.temp_box[1], y) self.select_class(x1, y1, x2, y2) self.temp_box = None self.update_display(display_img) def update_display(self, img=None): """更新显示内容""" if img is None: img = self.current_img.copy() self.update_cache() if self.cache_img is not None: img = self.cache_img.copy() cv2.imshow(self.window_name, img) def select_class(self, x1, y1, x2, y2): # 创建当前图像的副本用于显示选择界面 selection_img = self.current_img.copy() # 将OpenCV格式(BGR)转为PIL格式(RGB) pil_img = Image.fromarray(cv2.cvtColor(selection_img, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(pil_img) # 绘制分类选择提示文字(红色) draw.text((10, 20), "选择分类:", font=self.font, fill=(0, 0, 255)) # 遍历classes字典显示所有可选类别(1.小明 2.小红 3.小刚) for i, (class_id, name) in enumerate(self.classes.items()): draw.text((10, 50 + i * 30), f"{class_id}. {name}", font=self.font, fill=(0, 0, 255)) # 显示选择窗口(转回OpenCV格式) cv2.imshow("选择分类", cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)) # 等待键盘输入(0表示无限等待) key = cv2.waitKey(0) & 0xFF cv2.destroyWindow("选择分类") # 关闭选择窗口 # 如果按了1/2/3键(ASCII码49-51) if 49 <= key <= 51: # 保存标注(key-48将ASCII码转为数字1/2/3) self.annotations.append((key - 48, x1, y1, x2, y2)) self.update_cache()# 更新显示 cv2.imshow(self.window_name, self.cache_img) def run(self): # 创建显示窗口并设置鼠标回调函数 cv2.namedWindow(self.window_name) cv2.setMouseCallback(self.window_name, self.mouse_callback) self.init_display() # 初始化显示第一张图片 # 主循环(遍历所有图片) while self.current_index < len(self.images): key = cv2.waitKey(10) & 0xFF # 10ms等待按键 if key == ord("n"): # 按n键下一张 self.save_annotations() # 保存当前标注 self.current_index += 1 if self.current_index < len(self.images): self.init_display() # 显示新图片 elif key == ord("p"): # 按p键上一张 self.save_annotations() self.current_index = max(0, self.current_index - 1) self.init_display() elif key == ord("d"):# 按d键删除最后一个标注 if self.annotations: self.annotations.pop()# 移除最后标注 self.update_cache() cv2.imshow(self.window_name, self.cache_img) elif key == ord("q"): # 按q键退出 break cv2.destroyAllWindows()# 关闭所有窗口 def save_annotations(self): #标注保存 if not self.annotations: return height, width = self.current_img.shape[:2] txt_path = os.path.splitext(self.images[self.current_index])[0] + ".txt" with open(txt_path, "w") as f: for (class_id, x1, y1, x2, y2) in self.annotations: x_center = ((x1 + x2) / 2) / width y_center = ((y1 + y2) / 2) / height box_width = abs(x2 - x1) / width box_height = abs(y2 - y1) / height f.write(f"{class_id - 1} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}\n") if __name__ == "__main__": image_folder = "E:/123"; # image_folder = input("输入图片文件夹路径: ") annotator = SmoothYOLOAnnotator(image_folder) annotator.run()
输入文件夹路径 。下面的图片名称 必须是 1 到 n 后缀名 jpg png 都可以。
self.classes = {1: "小明", 2: "小红", 3: "小刚"} 这是分类。请自行修改。
self.current_index = 0 #从第1个开始标注 0就是从1 1就是从第2个标注 假如我们想从第5张图片标注 请输入4 0是从1第个图片标注。
针对标注窗口 显示分数乱码,安装了一个东西。
pip install opencv-python pillow numpy 显示分类名称乱码的时候,安装了这个库。
站长微信:xiaomao0055
站长QQ:14496453