代码示例 / 计算机视觉 / 使用YOLOV8和KerasCV进行高效目标检测

使用YOLOV8和KerasCV进行高效目标检测

作者: Gitesh Chawda
创建日期: 2023/06/26
最后修改: 2023/06/26
描述: 使用KerasCV训练自定义YOLOV8目标检测模型。

在Colab中查看 GitHub源


介绍

KerasCV是一个用于计算机视觉任务的Keras扩展。在此示例中,我们将看到 如何使用KerasCV训练YOLOV8目标检测模型。

KerasCV包含适用于流行计算机视觉数据集的预训练模型,如 ImageNet、COCO和Pascal VOC,这些模型可用于迁移学习。KerasCV还 提供了一系列可视化工具,以检查模型学习到的中间表示 并可视化目标检测和分割任务的结果。

如果你对使用KerasCV进行目标检测感兴趣,我强烈建议 查看lukewood创建的指南。此资源可在 使用KerasCV进行目标检测中找到, 提供了构建目标检测模型所需的基本概念和技术 的全面概述。

!pip install --upgrade git+https://github.com/keras-team/keras-cv -q
警告: 以“root”用户身份运行pip可能导致权限破坏和与系统包管理器的冲突行为。建议使用虚拟环境: https://pip.pypa.io/warnings/venv

设置

import os
from tqdm.auto import tqdm
import xml.etree.ElementTree as ET

import tensorflow as tf
from tensorflow import keras

import keras_cv
from keras_cv import bounding_box
from keras_cv import visualization
/opt/conda/lib/python3.10/site-packages/tensorflow_io/python/ops/__init__.py:98: UserWarning: unable to load libtensorflow_io_plugins.so: unable to open file: libtensorflow_io_plugins.so, from paths: ['/opt/conda/lib/python3.10/site-packages/tensorflow_io/python/ops/libtensorflow_io_plugins.so']
引发原因: ['/opt/conda/lib/python3.10/site-packages/tensorflow_io/python/ops/libtensorflow_io_plugins.so: undefined symbol: _ZN3tsl6StatusC1EN10tensorflow5error4CodeESt17basic_string_viewIcSt11char_traitsIcEENS_14SourceLocationE']
  warnings.warn(f"unable to load libtensorflow_io_plugins.so: {e}")
/opt/conda/lib/python3.10/site-packages/tensorflow_io/python/ops/__init__.py:104: UserWarning: file system plugins are not loaded: unable to open file: libtensorflow_io.so, from paths: ['/opt/conda/lib/python3.10/site-packages/tensorflow_io/python/ops/libtensorflow_io.so']
引发原因: ['/opt/conda/lib/python3.10/site-packages/tensorflow_io/python/ops/libtensorflow_io.so: undefined symbol: _ZTVN10tensorflow13GcsFileSystemE']
  warnings.warn(f"file system plugins are not loaded: {e}")

加载数据

在本指南中,我们将使用从 roboflow 获得的自动驾驶汽车数据集。为了 使数据集更易于管理,我从原始包含15,000个数据样本的较大数据集中提取了一个子集。 从这个子集中,我选择了7,316个样本进行模型训练。

为了简化任务,集中我们的努力,我们将处理减少的对象类别。 具体而言,我们将考虑五个主要的检测和分类类:汽车、行人、交通信号灯、自行车和卡车。这些 类别代表了在自动驾驶汽车的背景下遇到的一些最常见和重要的对象。

通过将数据集缩小到这些特定类别,我们可以专注于构建一个 能够准确识别和分类这些重要对象的稳健目标检测模型。

TensorFlow Datasets库提供了一种方便的方式来下载和使用各种 数据集,包括目标检测数据集。这对于希望快速开始使用数据而无需手动下载和 预处理的人来说是一个很好的选择。

您可以在这里查看各种目标检测数据集 TensorFlow Datasets

然而,在这个代码示例中,我们将演示如何使用TensorFlow的 tf.data 管道从头开始加载数据集。此方法提供了更多灵活性,并允许 您根据需要自定义预处理步骤。 加载不在 TensorFlow Datasets 库中的自定义数据集是使用 tf.data 管道的主要优势之一。这种方法允许您创建一个定制的数据预处理管道,以满足特定数据集的需求和要求。


超参数

SPLIT_RATIO = 0.2
BATCH_SIZE = 4
LEARNING_RATE = 0.001
EPOCH = 5
GLOBAL_CLIPNORM = 10.0

创建一个字典将每个类别名称映射到唯一的数字标识符。这个映射在目标检测任务的训练和推理过程中用于编码和解码类别标签。

class_ids = [
    "car",
    "pedestrian",
    "trafficLight",
    "biker",
    "truck",
]
class_mapping = dict(zip(range(len(class_ids)), class_ids))

# 图像和注释的路径
path_images = "/kaggle/input/dataset/data/images/"
path_annot = "/kaggle/input/dataset/data/annotations/"

# 获取 path_annot 中所有 XML 文件路径并排序
xml_files = sorted(
    [
        os.path.join(path_annot, file_name)
        for file_name in os.listdir(path_annot)
        if file_name.endswith(".xml")
    ]
)

# 获取 path_images 中所有 JPEG 图像文件路径并排序
jpg_files = sorted(
    [
        os.path.join(path_images, file_name)
        for file_name in os.listdir(path_images)
        if file_name.endswith(".jpg")
    ]
)

下面的函数读取 XML 文件,找到图像名称和路径,然后遍历 XML 文件中的每个对象,以提取每个对象的边界框坐标和类别标签。

该函数返回三个值:图像路径、边界框列表(每个表示为四个浮点数的列表:xmin、ymin、xmax、ymax)和与每个边界框对应的类别 ID 列表(作为整数表示)。类别 ID 通过使用一个名为 class_mapping 的字典将类别标签映射到整数值来获得。

def parse_annotation(xml_file):
    tree = ET.parse(xml_file)
    root = tree.getroot()

    image_name = root.find("filename").text
    image_path = os.path.join(path_images, image_name)

    boxes = []
    classes = []
    for obj in root.iter("object"):
        cls = obj.find("name").text
        classes.append(cls)

        bbox = obj.find("bndbox")
        xmin = float(bbox.find("xmin").text)
        ymin = float(bbox.find("ymin").text)
        xmax = float(bbox.find("xmax").text)
        ymax = float(bbox.find("ymax").text)
        boxes.append([xmin, ymin, xmax, ymax])

    class_ids = [
        list(class_mapping.keys())[list(class_mapping.values()).index(cls)]
        for cls in classes
    ]
    return image_path, boxes, class_ids


image_paths = []
bbox = []
classes = []
for xml_file in tqdm(xml_files):
    image_path, boxes, class_ids = parse_annotation(xml_file)
    image_paths.append(image_path)
    bbox.append(boxes)
    classes.append(class_ids)
  0%|          | 0/7316 [00:00<?, ?it/s]

在这里,我们使用 tf.ragged.constantbboxclasses 列表创建稀疏张量。稀疏张量是一种可以处理一个或多个维度上数据变长的张量。这在处理具有可变长度序列的数据时非常有用,例如文本或时间序列数据。

classes = [
    [8, 8, 8, 8, 8],      # 5 个类别
    [12, 14, 14, 14],     # 4 个类别
    [1],                  # 1 个类别
    [7, 7],               # 2 个类别
 ...]
bbox = [
    [[199.0, 19.0, 390.0, 401.0],
    [217.0, 15.0, 270.0, 157.0],
    [393.0, 18.0, 432.0, 162.0],
    [1.0, 15.0, 226.0, 276.0],
    [19.0, 95.0, 458.0, 443.0]],     #图像 1 有 4 个物体
    [[52.0, 117.0, 109.0, 177.0]],   #图像 2 有 1 个物体
    [[88.0, 87.0, 235.0, 322.0],
    [113.0, 117.0, 218.0, 471.0]],   #图像 3 有 2 个物体
 ...]

在这种情况下,bboxclasses 列表对于每张图片的长度不同,具体取决于图片中的物体数量以及相应的边界框和类别。为了处理这种可变性,使用稀疏张量而不是常规张量。

稍后,这些稀疏张量用于使用 from_tensor_slices 方法创建一个 tf.data.Dataset。该方法通过沿第一个维度对输入张量进行切片来创建数据集。通过使用稀疏张量,数据集可以处理每张图像数据的不同长度,并为进一步处理提供灵活的输入管道。

bbox = tf.ragged.constant(bbox)
classes = tf.ragged.constant(classes)
image_paths = tf.ragged.constant(image_paths)

data = tf.data.Dataset.from_tensor_slices((image_paths, classes, bbox))

将训练数据和验证数据划分

# 确定验证样本的数量
num_val = int(len(xml_files) * SPLIT_RATIO)

# 将数据集分为训练集和验证集
val_data = data.take(num_val)
train_data = data.skip(num_val)

让我们看看数据加载和边界框格式,以便开始。KerasCV中的边界框具有预定格式。为此,您必须将边界框打包到一个符合以下要求的字典中:

bounding_boxes = {
    # num_boxes 可能是一个不规则维度
    'boxes': Tensor(shape=[batch, num_boxes, 4]),
    'classes': Tensor(shape=[batch, num_boxes])
}

字典有两个键,'boxes''classes',每个键都映射到一个TensorFlow RaggedTensor或Tensor对象。'boxes' Tensor的形状为[batch, num_boxes, 4],其中batch是批次中的图像数量,num_boxes是任意图像中边界框的最大数量。4表示定义边界框所需的四个值:xmin、ymin、xmax、ymax。

'classes' Tensor的形状为[batch, num_boxes],其中每个元素表示'boxes' Tensor中相应边界框的类别标签。num_boxes维度可能是不规则的,这意味着批次中图像的框数可能有所不同。

最终的字典应该是:

{"images": images, "bounding_boxes": bounding_boxes}
def load_image(image_path):
    image = tf.io.read_file(image_path)
    image = tf.image.decode_jpeg(image, channels=3)
    return image


def load_dataset(image_path, classes, bbox):
    # 读取图像
    image = load_image(image_path)
    bounding_boxes = {
        "classes": tf.cast(classes, dtype=tf.float32),
        "boxes": bbox,
    }
    return {"images": tf.cast(image, tf.float32), "bounding_boxes": bounding_boxes}

在这里,我们创建了一个将图像调整为640x640像素的层,同时保持原始宽高比。与图像相关联的边界框以xyxy格式指定。如果需要,调整大小的图像将用零填充,以保持原始宽高比。

KerasCV支持的边界框格式: 1. CENTER_XYWH 2. XYWH 3. XYXY 4. REL_XYXY 5. REL_XYWH 6. YXYX 7. REL_YXYX

您可以在文档中了解有关KerasCV边界框格式的更多信息。

此外,可以在任何一对之间进行格式转换:

boxes = keras_cv.bounding_box.convert_format(
        bounding_box,
        images=image,
        source="xyxy",  # 原始格式
        target="xywh",  # 目标格式(我们要转换为的格式)
    )

数据增强

构建目标检测管道时最具挑战性的任务之一是数据增强。它涉及对输入图像应用各种变换,以增加训练数据的多样性并提高模型的泛化能力。然而,当处理目标检测任务时,这变得更加复杂,因为这些变换需要考虑底层的边界框并相应更新它们。

KerasCV原生支持边界框增强。KerasCV提供了专门设计用于处理边界框的大量数据增强层。这些层在图像转换时智能地调整边界框坐标,确保边界框保持准确并与增强后的图像对齐。

通过利用KerasCV的能力,开发人员可以方便地将适用于边界框的数据增强集成到他们的目标检测管道中。在tf.data管道中动态执行增强,该过程变得无缝且高效,从而实现更好的训练和更准确的目标检测结果。

augmenter = keras.Sequential(
    layers=[
        keras_cv.layers.RandomFlip(mode="horizontal", bounding_box_format="xyxy"),
        keras_cv.layers.RandomShear(
            x_factor=0.2, y_factor=0.2, bounding_box_format="xyxy"
        ),
        keras_cv.layers.JitteredResize(
            target_size=(640, 640), scale_factor=(0.75, 1.3), bounding_box_format="xyxy"
        ),
    ]
)

创建训练数据集

train_ds = train_data.map(load_dataset, num_parallel_calls=tf.data.AUTOTUNE)
train_ds = train_ds.shuffle(BATCH_SIZE * 4)
train_ds = train_ds.ragged_batch(BATCH_SIZE, drop_remainder=True)
train_ds = train_ds.map(augmenter, num_parallel_calls=tf.data.AUTOTUNE)

创建验证数据集

resizing = keras_cv.layers.JitteredResize(
    target_size=(640, 640),
    scale_factor=(0.75, 1.3),
    bounding_box_format="xyxy",
)

val_ds = val_data.map(load_dataset, num_parallel_calls=tf.data.AUTOTUNE)
val_ds = val_ds.shuffle(BATCH_SIZE * 4)
val_ds = val_ds.ragged_batch(BATCH_SIZE, drop_remainder=True)
val_ds = val_ds.map(resizing, num_parallel_calls=tf.data.AUTOTUNE)

可视化

def visualize_dataset(inputs, value_range, rows, cols, bounding_box_format):
    inputs = next(iter(inputs.take(1)))
    images, bounding_boxes = inputs["images"], inputs["bounding_boxes"]
    visualization.plot_bounding_box_gallery(
        images,
        value_range=value_range,
        rows=rows,
        cols=cols,
        y_true=bounding_boxes,
        scale=5,
        font_scale=0.7,
        bounding_box_format=bounding_box_format,
        class_mapping=class_mapping,
    )


visualize_dataset(
    train_ds, bounding_box_format="xyxy", value_range=(0, 255), rows=2, cols=2
)

visualize_dataset(
    val_ds, bounding_box_format="xyxy", value_range=(0, 255), rows=2, cols=2
)

png

png

我们需要从预处理字典中提取输入,并准备好将其输入到模型中。

def dict_to_tuple(inputs):
    return inputs["images"], inputs["bounding_boxes"]


train_ds = train_ds.map(dict_to_tuple, num_parallel_calls=tf.data.AUTOTUNE)
train_ds = train_ds.prefetch(tf.data.AUTOTUNE)

val_ds = val_ds.map(dict_to_tuple, num_parallel_calls=tf.data.AUTOTUNE)
val_ds = val_ds.prefetch(tf.data.AUTOTUNE)

创建模型

YOLOv8是一个最先进的YOLO模型,广泛用于各种计算机视觉任务,如目标检测、图像分类和实例分割。Ultralytics是YOLOv5的创建者,他们还开发了YOLOv8,该模型在架构和开发者体验上相较于前身进行了许多改进和变更。YOLOv8是行业内备受推崇的最新顶尖模型。

下表比较了五种不同大小(以像素计)的YOLOv8模型的性能指标:YOLOv8n、YOLOv8s、YOLOv8m、YOLOv8l和YOLOv8x。指标包括在不同IoU阈值下验证数据的平均精度(mAP)值、在CPU上使用ONNX格式和A100 TensorRT的推理速度、参数数量和浮点运算(FLOPs)(分别以百万和十亿计)。随着模型大小的增加,mAP、参数和FLOPs通常会增加,而速度会下降。YOLOv8x拥有最高的mAP、参数和FLOPs,但推理速度也是最慢的,而YOLOv8n则是体积最小、推理速度最快,同时其mAP、参数和FLOPs也是最低的。

| 模型 | 大小
(像素) | mAPval
50-95 | 速度
CPU ONNX
(毫秒) | 速度
A100 TensorRT
(毫秒) | 参数
(M) | FLOPs
(B) | | ------------------------------------------------------------------------------------ | --------------------- | -------------------- | ------------------------------ | ----------------------------------- | ------------------ | ----------------- | | YOLOv8n | 640 | 37.3 | 80.4 | 0.99 | 3.2 | 8.7 | | YOLOv8s | 640 | 44.9 | 128.4 | 1.20 | 11.2 | 28.6 | | YOLOv8m | 640 | 50.2 | 234.7 | 1.83 | 25.9 | 78.9 | | YOLOv8l | 640 | 52.9 | 375.2 | 2.39 | 43.7 | 165.2 | | YOLOv8x | 640 | 53.9 | 479.1 | 3.53 | 68.2 | 257.8 |

您可以在这个RoboFlow Blog上阅读更多关于YOLOV8及其架构的内容。

首先,我们将创建一个骨干网络实例,供我们的YOLOv8检测器类使用。

在KerasCV中可用的YOLOV8骨干:

  1. 无权重:
1.   yolo_v8_xs_backbone
2.   yolo_v8_s_backbone
3.   yolo_v8_m_backbone
4.   yolo_v8_l_backbone
5.   yolo_v8_xl_backbone
  1. 带有预训练的coco权重:
backbone = keras_cv.models.YOLOV8Backbone.from_preset(
    "yolo_v8_s_backbone_coco"  # 我们将使用带有coco权重的小型yolov8骨干
)
1.   yolo_v8_xs_backbone_coco
2.   yolo_v8_s_backbone_coco
2.   yolo_v8_m_backbone_coco
2.   yolo_v8_l_backbone_coco
2.   yolo_v8_xl_backbone_coco

从 https://storage.googleapis.com/keras-cv/models/yolov8/coco/yolov8_s_backbone.h5 下载数据
20596968/20596968 [==============================] - 0s 0us/step

接下来,让我们使用YOLOV8Detector构建一个YOLOV8模型,该模型接受特征提取器作为backbone参数,一个num_classes参数,该参数根据class_mapping列表的大小指定要检测的目标类别数量,bounding_box_format参数告知模型数据集中bbox的格式,最后,特征金字塔网络(FPN)深度由fpn_depth参数指定。

得益于KerasCV,使用上述任一骨干构建YOLOV8非常简单。

yolo = keras_cv.models.YOLOV8Detector(
    num_classes=len(class_mapping),
    bounding_box_format="xyxy",
    backbone=backbone,
    fpn_depth=1,
)

编译模型

YOLOV8使用的损失

  1. 分类损失:该损失函数计算预期类别概率与实际类别概率之间的差异。在这种情况下, binary_crossentropy,这是一个用于二分类问题的显著解决方案,已被利用。我们使用二元交叉熵,因为每个被识别的对象都被归类为属于或不属于某个特定的对象类别(例如人、车等)。

  2. 区域损失:box_loss 是用来测量预测的边界框与真实值之间差异的损失函数。在这种情况下,使用完全IoU(CIoU)指标,这不仅测量预测的边界框与真实边界框之间的重叠,还考虑了长宽比、中心距离和框大小之间的差异。这些损失函数共同帮助优化对象检测模型,通过最小化预测类别概率与真实类别概率以及边界框之间的差异。

optimizer = tf.keras.optimizers.Adam(
    learning_rate=LEARNING_RATE,
    global_clipnorm=GLOBAL_CLIPNORM,
)

yolo.compile(
    optimizer=optimizer, classification_loss="binary_crossentropy", box_loss="ciou"
)

COCO指标回调

我们将使用KerasCV中的BoxCOCOMetrics来评估模型,并计算Mean Average Precision(mAP)得分、召回率和精确度。当mAP得分提高时,我们还会保存我们的模型。

class EvaluateCOCOMetricsCallback(keras.callbacks.Callback):
    def __init__(self, data, save_path):
        super().__init__()
        self.data = data
        self.metrics = keras_cv.metrics.BoxCOCOMetrics(
            bounding_box_format="xyxy",
            evaluate_freq=1e9,
        )

        self.save_path = save_path
        self.best_map = -1.0

    def on_epoch_end(self, epoch, logs):
        self.metrics.reset_state()
        for batch in self.data:
            images, y_true = batch[0], batch[1]
            y_pred = self.model.predict(images, verbose=0)
            self.metrics.update_state(y_true, y_pred)

        metrics = self.metrics.result(force=True)
        logs.update(metrics)

        current_map = metrics["MaP"]
        if current_map > self.best_map:
            self.best_map = current_map
            self.model.save(self.save_path)  # 当mAP改善时保存模型

        return logs

训练模型

yolo.fit(
    train_ds,
    validation_data=val_ds,
    epochs=3,
    callbacks=[EvaluateCOCOMetricsCallback(val_ds, "model.h5")],
)
第 1 轮/3
1463/1463 [==============================] - 633s 390ms/step - loss: 10.1535 - box_loss: 2.5659 - class_loss: 7.5876 - val_loss: 3.9852 - val_box_loss: 3.1973 - val_class_loss: 0.7879 - MaP: 0.0095 - MaP@[IoU=50]: 0.0193 - MaP@[IoU=75]: 0.0074 - MaP@[area=small]: 0.0021 - MaP@[area=medium]: 0.0164 - MaP@[area=large]: 0.0010 - Recall@[max_detections=1]: 0.0096 - Recall@[max_detections=10]: 0.0160 - Recall@[max_detections=100]: 0.0160 - Recall@[area=small]: 0.0034 - Recall@[area=medium]: 0.0283 - Recall@[area=large]: 0.0010
第 2 轮/3
1463/1463 [==============================] - 554s 378ms/step - loss: 2.6961 - box_loss: 2.2861 - class_loss: 0.4100 - val_loss: 3.8292 - val_box_loss: 3.0052 - val_class_loss: 0.8240 - MaP: 0.0077 - MaP@[IoU=50]: 0.0197 - MaP@[IoU=75]: 0.0043 - MaP@[area=small]: 0.0075 - MaP@[area=medium]: 0.0126 - MaP@[area=large]: 0.0050 - Recall@[max_detections=1]: 0.0088 - Recall@[max_detections=10]: 0.0154 - Recall@[max_detections=100]: 0.0154 - Recall@[area=small]: 0.0075 - Recall@[area=medium]: 0.0191 - Recall@[area=large]: 0.0280
第 3 轮/3
1463/1463 [==============================] - 558s 381ms/step - loss: 2.5930 - box_loss: 2.2018 - class_loss: 0.3912 - val_loss: 3.4796 - val_box_loss: 2.8472 - val_class_loss: 0.6323 - MaP: 0.0145 - MaP@[IoU=50]: 0.0398 - MaP@[IoU=75]: 0.0072 - MaP@[area=small]: 0.0077 - MaP@[area=medium]: 0.0227 - MaP@[area=large]: 0.0079 - Recall@[max_detections=1]: 0.0120 - Recall@[max_detections=10]: 0.0257 - Recall@[max_detections=100]: 0.0258 - Recall@[area=small]: 0.0093 - Recall@[area=medium]: 0.0396 - Recall@[area=large]: 0.0226

<keras.callbacks.History at 0x7f3e01ca6d70>

可视化预测

def visualize_detections(model, dataset, bounding_box_format):
    images, y_true = next(iter(dataset.take(1)))
    y_pred = model.predict(images)
    y_pred = bounding_box.to_ragged(y_pred)
    visualization.plot_bounding_box_gallery(
        images,
        value_range=(0, 255),
        bounding_box_format=bounding_box_format,
        y_true=y_true,
        y_pred=y_pred,
        scale=4,
        rows=2,
        cols=2,
        show=True,
        font_scale=0.7,
        class_mapping=class_mapping,
    )


visualize_detections(yolo, dataset=val_ds, bounding_box_format="xyxy")
1/1 [==============================] - 0s 115ms/step

png