侧边栏壁纸
博主头像
YOLO大师

行动起来,活在当下

  • 累计撰写 2 篇文章
  • 累计创建 4 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

如何使用OpenCV和Python标记图像数据集

Administrator
2025-05-10 / 0 评论 / 0 点赞 / 262 阅读 / 0 字

前言

在机器学习模型的开发过程中,准确拟合特定数据分布是实现高性能模型的核心目标之一。然而,为了确保模型在未见数据上的泛化能力,构建高质量的训练数据集显得尤为关键。特别是在监督学习任务中,标注数据的精确性在很大程度上决定了模型性能的上限。

即便模型具备复杂的架构、包含数亿参数,或使用了大量的数据增强策略,若输入数据质量低劣,仍然难以获得令人满意的输出结果。因而,高质量数据构建被视为模型成功部署的基础。

在应对特定任务时,理想情形是能够直接利用已有的公开数据集。然而,现实场景往往缺乏完备、适配的公开资源,此时,开发者通常需要自行构建符合任务需求的专属数据集。这一过程中,原始数据往往处于未标注状态,标注过程成为不可或缺的一环。

演示

本文以一个由移动设备拍摄的图像数据集为例,旨在演示如何快速搭建一个轻量级图像分类标注工具,以便高效地将未标注图像按类别分类。本任务聚焦于区分四种常见的 USB 接口类型:USB-A、USB-C、Micro USB 以及 Mini USB。最初,所有图像文件存放于未标注的输入目录中,标注工具的职责是逐一展示这些图像,允许用户通过键盘操作对其进行分类,并将其移动至相应的输出子目录中。

数据加载

标注过程的第一步是批量加载输入目录中的图像文件。借助 pathlib 库中的 glob 方法,程序可高效检索所有 .jpg 格式图像,并使用 sorted 进行排序,以确保处理顺序的一致性:

from pathlib import Path

input_path = Path("input")
input_img_paths = sorted(input_path.glob("*.jpg"))

在标注开始前,还需确保输出目录以及分类子目录的存在,以避免在写入操作时出现路径不存在的错误:

output_path = Path("output")
output_path.mkdir(parents=True, exist_ok=True)

接下来,程序将逐图加载图像并使用 OpenCV 显示在窗口中,等待用户输入分类指令。通过设置 cv2.waitKey(0),程序将阻塞执行,直至检测到按键输入。当检测到用户按下 Q 键时,标注流程优雅地终止,并关闭所有窗口:

import cv2

...

def annotate_images(
    input_img_paths: list[Path],
    output_path: Path,
)-> None:
  
    for img_path in input_img_paths:
        img = cv2.imread(str(img_path))
  
        cv2.imshow("Image", img)
      
        while True:
            key = cv2.waitKey(0)
      
            # Quit Annotation Tool
            if key == ord("q"):
                return
      
        cv2.destroyAllWindows()

为了精确识别按键输入,推荐结合按位操作 & 0xFF 使用 ord() 函数,从而避免特殊按键状态(如 NumLock)对识别结果的干扰。

image-20250510165204789

标注

任务中涉及的标签预先定义为一个字符串列表,与所需分类任务一一对应:

...

annotate_images(
    input_img_paths=input_img_paths,
    output_path=output_path,
    labels=["usb_a", "usb_c", "usb_mini", "usb_micro"],
)

用户可通过按下数字键 0–3,将当前图像分配至对应的标签目录。具体地,waitKey() 返回的整数可与 ord(str(i)) 进行匹配判断,确定输入类别:

  while True:
    ...
    for i in range(len(labels)):
        if key == ord(str(i)):
            label = labels[i]
            print(f"Classified as {label}")

            # TODO: move to correct label folder
            break

图像移动操作通过 pathlib/ 运算符实现路径拼接,配合 rename() 函数完成重定位:

output_img_path = output_path / label / img_path.name
img_path.rename(output_img_path)

所有输出目录需在程序启动时一次性创建完成:

for label in labels:
    label_dir = output_path / label
    label_dir.mkdir(parents=True, exist_ok=True)

为提高标注效率,可通过构建从按键值到标签名的映射字典来加速分类过程:

labels_key_dict = {ord(str(i)): label for i, label in enumerate(labels)}

if key in labels_key_dict:
    label = labels_key_dict[key]
    ...

此外,为用户提供视觉提示,在图像上动态叠加数字键与标签的对应关系,有助于提升交互体验:

for i, label in enumerate(labels):
    cv2.putText(
        img,
        f"{i}: {label}",
        (10, 30 + 30 * i),
        cv2.FONT_HERSHEY_SIMPLEX,
        1,
        (255, 255, 255),
        2,
        cv2.LINE_AA,
    )

结论

本文介绍了一种简洁而高效的图像分类标注工具,适用于数据集构建的初始阶段。尽管功能相对基础,但该工具具有良好的扩展性。例如,后续可集成图像分割功能,实现同时生成掩码标签,拓展至语义分割等任务场景。

在项目早期阶段,特别是进行探索性数据分析或概念验证时,使用轻量化工具往往更具优势。它们能够以最小的开发成本,实现对核心问题的快速验证,为后续更大规模的数据处理与建模打下坚实基础。

完整代码

# 导入标准库 pathlib 中的 Path 类,用于处理文件路径
from pathlib import Path
# 导入 OpenCV 库用于图像处理
import cv2


def annotate_images(
    input_img_paths: list[Path],  # 输入图像路径列表
    output_path: Path,            # 输出目录路径
    labels: list[str],            # 标签列表,例如 ["usb_a", "usb_c", ...]
    max_size_display: int = 640,  # 图像最大显示尺寸(像素)
) -> None:
    """对图像进行分类标注,通过键盘输入将图像归入指定类别。"""

    # 遍历每张待标注的图像路径
    for img_path in input_img_paths:
        # 读取图像为 BGR 格式的 NumPy 数组
        img = cv2.imread(str(img_path))

        # 计算缩放比例,使图像最长边为 max_size_display 像素
        ratio = max_size_display / max(img.shape[:2])
        # 按比例缩放图像
        img = cv2.resize(img, None, fx=ratio, fy=ratio)

        # 在图像上添加每个类别的键位提示文字(例如 "0: usb_a")
        for i, label in enumerate(labels):
            cv2.putText(
                img,
                f"{i}: {label}",            # 显示文本:编号 + 标签名
                (10, 30 + 30 * i),          # 文本左上角坐标,逐行显示
                cv2.FONT_HERSHEY_SIMPLEX,   # 字体样式
                1,                          # 字体大小
                (255, 255, 255),            # 字体颜色:白色
                2,                          # 字体线宽
                cv2.LINE_AA,                # 抗锯齿绘制
            )

        # 构造按键与标签之间的映射字典,例如:{ord("0"): "usb_a"}
        labels_key_dict = {ord(str(i)): label for i, label in enumerate(labels)}

        # 为每个类别预创建输出文件夹(如果不存在则创建)
        for label in labels:
            label_dir = output_path / label
            label_dir.mkdir(parents=True, exist_ok=True)

        # 打开一个图像窗口显示当前图像
        cv2.imshow("Image", img)

        # 等待用户按键输入进行标注
        while True:
            key = cv2.waitKey(0)  # 无限等待,直到有键被按下

            # 如果按下的是 "q",则退出整个标注工具
            if key == ord("q"):
                return

            # 如果按下的是有效的标签键(如 0/1/2/3)
            if key in labels_key_dict:
                label = labels_key_dict[key]  # 获取对应标签名
                print(f"Classified as {label}")

                # 构造输出图像路径:output/标签名/原图名
                output_img_path = output_path / label / img_path.name
                # 将图像移动到对应类别目录中
                img_path.rename(output_img_path)

                break  # 跳出当前图像循环,开始标注下一张图像

    # 关闭所有 OpenCV 打开的图像窗口
    cv2.destroyAllWindows()


def main():
    # 设置输入目录路径为 "input"
    input_path = Path("input")
    # 加载所有 JPG 图像并按文件名排序
    input_img_paths = sorted(input_path.glob("*.jpg"))

    # 设置输出目录路径为 "output"
    output_path = Path("output")
    # 若输出目录不存在则递归创建
    output_path.mkdir(parents=True, exist_ok=True)

    # 调用标注函数,传入图像路径、输出路径和标签列表
    annotate_images(
        input_img_paths=input_img_paths,
        output_path=output_path,
        labels=["usb_a", "usb_c", "usb_mini", "usb_micro"],  # 定义 4 个类别
    )


# 判断当前脚本是否为主程序入口,若是则调用 main 函数
if __name__ == "__main__":
    main()

0

评论区