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