YOLOv8--绘制中文标签耗时优化

设备:RTX4080
运行环境:Python=3.8(要求>=3.8),torch1.12.0+cu113(要求>=1.8)
问题:ultralytics代码绘制中文标签乱码,以及其他网上中文绘制推理脚本进行中文可视化时,绘制时间较长(甚至远大于推理时间),尤其目标数量100+时,可视化时间可能上百毫秒,对要求实时推理需求很不友好。

本文方法:CPU/GPU上中文绘制耗时几乎忽略不计,接口代码可以集成到其他推理脚本中!

注意1:下面代码可视化有些框没有标签是因为代码进行超出边界过滤,需自行去优化修改!
注意2:自行去测试时,注意参数修改!

1 绘制较慢的原因分析

常用的方法和很多博主都是对框一个个遍历,并从opencv转Image格式进行中文绘制,这种绘制速度极慢!!!
例子如下
在这里插入图片描述

其中,图像格式转换开销占主要:
PIL 与 OpenCV 格式转换:代码中使用了 PIL 库来创建字符图像,然后再将其转换为 OpenCV 能处理的 numpy 数组格式。这个转换过程涉及到颜色通道顺序的调整(从 RGBA 到 BGR)以及数据类型的转换,会消耗一定的时间。特别是在处理大量字符图像时,这种转换的累积开销会变得明显。

char_img_cv = cv2.cvtColor(np.array(char_img), cv2.COLOR_RGBA2BGR)

2 本文中文标签绘制思路

本文中文绘制方法简言之就是 贴图,先利用PIL库创建好所有中文标签并转换Opencv格式生成缓存,然后绘制时只需要拿出来贴在原图上,测试结果绘制100个中文标签耗时几乎忽略不计,具体如下:

2.1 主要实现流程

该代码绘制中文标签的流程主要分为初始化、缓存创建、绘制三个主要阶段,以下是详细步骤:

2.1.1 初始化阶段

类实例化:在主程序中创建 Draw_Chinese_text 类的实例,在实例化时会传入字体大小、字体文件路径等参数。
字体与颜色设置:在 Draw_Chinese_text 类的 init 方法中,根据传入的字体文件路径使用 ImageFont.truetype 加载字体,同时设置文本颜色。

2.1.2 缓存创建阶段

初始化缓存:实例化 Draw_Chinese_text 类时会调用 draw_cached_text_init 方法,该方法会遍历传入的 cached_char_images_dict 字典。
创建字符图像:对于字典中的每个文本标签,创建一个透明的 PIL 图像,使用 ImageDraw 在图像上绘制文本,然后将其转换为 OpenCV 格式的 numpy 数组,并保存到缓存字典中。

2.1.3 绘制阶段

检查缓存:在 draw_txt 方法中,当需要绘制文本时,首先检查该文本是否已经存在于缓存字典中。
处理新文本:如果文本不在缓存中,创建新的字符图像,将其转换为 OpenCV 格式,并保存到缓存字典中。
绘制文本:从缓存中获取对应的字符图像,检查其在目标图像上的绘制位置是否越界,如果不越界,则将字符图像覆盖到目标图像的对应位置。

2.1.4 流程总结

整个绘制中文标签的流程通过初始化字体和颜色、创建并管理字符图像缓存,最终将缓存的字符图像高效地绘制到目标图像上,利用缓存机制减少了重复绘制和字体渲染的开销,提高了绘制效率。

3 实战测试

本文将利用Visdrone2019数据集和模型进行测试。
中文字体为HarmonyOS_Sans_SC_Regular.ttf【华为定制鸿蒙字体,网上免费下载】

3.1 创建脚本Draw_Chinese_text.py,内容如下:

import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont

class Draw_Chinese_text():
    def __init__(self, textSize, thickness, font_path, cached_char_images_dict={'行人':{'text_lenght': 2, 'cache_img': None}}):
        # 指定字体文件路径(注意,这里后续使用cv2时需要确保能正确加载字体,可能和PIL加载方式有区别)
        self.font_path = font_path
        self.font = ImageFont.truetype(self.font_path, textSize)

        # 指定文本颜色(白色示例,可按需修改,opencv中颜色顺序为BGR)
        self.fill = (255, 255, 255)
        self.thickness = thickness
        self.textSize = textSize + self.thickness
        self.cached_char_images_dict = cached_char_images_dict

        self.draw_cached_text_init()

    def draw_cached_text_init(self):
        """
        使用缓存机制初始化绘制文本相关内容-使用PIL来创建字符图像缓存-并转成cv2格式
        """
        for txt_key in self.cached_char_images_dict:
            txt_len = self.cached_char_images_dict[txt_key]['text_lenght']
            # 创建一个临时的透明图像用于绘制单个字符
            char_img = Image.new("RGBA", (self.textSize * int(txt_len), self.textSize), (255, 0, 0, 0))
            char_draw = ImageDraw.Draw(char_img)
            char_draw.text((self.thickness // 2, self.thickness // 2), txt_key, font=self.font, fill=self.fill)
            # 将PIL图像转换为OpenCV能处理的numpy数组格式(注意颜色通道顺序转换等)
            char_img_cv = cv2.cvtColor(np.array(char_img), cv2.COLOR_RGBA2BGR)
            self.cached_char_images_dict[txt_key]['cache_img'] = char_img_cv

    def draw_txt(self, img_cv, point, label):
        """
        在目标图像上绘制文本-将缓存的字符图像绘制到传入的opencv图像上
        :param img_cv: 目标图像, opencv的numpy数组格式表示
        :param point: 绘制文本的起始坐标,格式为 (x, y)
        :param label: 要绘制的文本标签
        """
        # 显示的标签不在默认字典,则创建新键值
        if label not in self.cached_char_images_dict:
            self.cached_char_images_dict[label] = {}
            txt_len = len(label)
            # 创建一个临时的透明图像用于绘制单个字符
            char_img = Image.new("RGBA", (self.textSize * int(txt_len), self.textSize), (255, 0, 0, 0))
            char_draw = ImageDraw.Draw(char_img)
            char_draw.text((self.thickness // 2, self.thickness // 2), label, font=self.font, fill=self.fill)
            # 将PIL图像转换为OpenCV能处理的numpy数组格式(注意颜色通道顺序转换等)
            char_img_cv = cv2.cvtColor(np.array(char_img), cv2.COLOR_RGBA2BGR)

            # 将新显示的值保存起来
            self.cached_char_images_dict[label]['cache_img'] = char_img_cv
            self.cached_char_images_dict[label]['text_lenght'] = txt_len


        char_np_img = self.cached_char_images_dict[label]['cache_img']
        h, w, _ = char_np_img.shape
        x, y = point
        # 判断标签是否越界,如果越界进行相应处理(这里简单示例,可以根据实际情况完善逻辑)
        if x < 0 or y < 0 or x + w > img_cv.shape[1] or y + h > img_cv.shape[0]:
            return img_cv

        # 将缓存的字符图像覆盖到目标图像对应位置上(这里简单的覆盖,可根据需求考虑更复杂的融合等操作)
        img_cv[y:y + h, x:x + w] = char_np_img
        return img_cv

3.2 创建测试脚本

from ultralytics import YOLO
import os
import cv2
import argparse
import time
from Draw_Chinese_text import Draw_Chinese_text

class YOLOv8(object):
    def __init__(self, args):
        self.args = args
        # 加载模型
        self.model = YOLO(model=self.args.model_path)

        # 初始化绘制中文文本的类
        self.draw_txt = Draw_Chinese_text(args.textSize, args.thickness, args.font_path, args.cached_char_images_dict)

    def __call__(self, img_path):
        # 读取图片
        frame = cv2.imread(img_path)
        self.results = self.model(source=frame, imgsz=self.args.imgz, save=False, conf=self.args.conf_thres, iou=self.args.iou_thres)

        # 绘制结果
        self.visualize(self.args, frame)
        cv2.imwrite(os.path.join(args.out_path, os.path.basename(img_path)), frame)

    def visualize(self, args, frame):
        # 绘制结果
        # Visualize the results on the frame
        tmp_data = self.results[0].boxes.data.cpu().numpy()

        start_time = time.time()
        t2 = 0
        for obj in tmp_data:
            x1, y1, x2, y2, conf0, cls = obj
            x1, y1, x2, y2, cls = int(x1), int(y1), int(x2), int(y2), int(cls)

            text = args.classes[cls]
            if text not in args.vis_classes:
                continue
            position = x1, y1 - args.textSize - args.thickness

            t3 = time.time()
            self.draw_txt.draw_txt(frame, position, text)
            t2 += (time.time() - t3)
            # print('绘制中文时间:{}s'.format(time.time() - t3))
            cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), args.thickness)
        print('总绘制中文时间:{}s'.format(t2))
        print('总绘制时间:{}s'.format(time.time() - start_time))
       

if __name__ == '__main__':
    # 命令行参数
    parser = argparse.ArgumentParser(description="gdu ai test")
    parser.add_argument("--model_path", type=str, default=r"E:\Code\YOLO-Chinese\visdrone_8\weights/best.pt", help="模型路径")
    parser.add_argument("--img_dir", type=str, default=r"E:\datasets\visdrone_mini\VisDrone2019-DET-train\images", help="输入路径")
    parser.add_argument("--out_path", type=str, default=r"E:\Code\YOLO-Chinese\res", help="保存路径")

    # 类别与模型参数设置,下面采用visdrone数据集类别名称为:['pedestrian', 'people', 'bicycle', 'car', 'van', 'truck', 'tricycle', 'awning-tricycle', 'bus', 'motor']
    parser.add_argument("--classes", default=['行人', '人', '自行车', '汽车', '面包车', '卡车', '三轮车', '遮阳三轮车', '巴士', '摩托车'], type=list, help="模型中文类别名称")
    parser.add_argument("--vis_classes", default=['行人', '人', '自行车', '汽车', '面包车', '卡车', '三轮车', '遮阳三轮车', '巴士', '摩托车'], type=list, help="需要显示的类别名称")
    
    parser.add_argument("--imgz", default=[640, 640], type=list, help="模型推理输入尺寸")
    parser.add_argument("--conf_thres", default=0.25, type=float, help="目标置信度阈值")
    parser.add_argument("--iou_thres", default=0.6, type=float, help="目标之间iou阈值")

    # 画框设置
    parser.add_argument("--cached_char_images_dict",
                        default={ '行人':{'text_lenght': 2, 'cache_img': None},
                                  '人':{'text_lenght': 1, 'cache_img': None},
                                  '自行车':{'text_lenght': 3, 'cache_img': None},
                                  '汽车':{'text_lenght': 2, 'cache_img': None},
                                  '面包车':{'text_lenght': 3, 'cache_img': None},
                                  '卡车':{'text_lenght': 2, 'cache_img': None},
                                  '三轮车':{'text_lenght': 3, 'cache_img': None},
                                  '遮阳三轮车':{'text_lenght': 5, 'cache_img': None},
                                  '巴士':{'text_lenght': 2, 'cache_img': None},
                                  '摩托车':{'text_lenght': 3, 'cache_img': None}
                                },
                        type=dict, help="中文标签显示设置, 长度与字数对应")
    parser.add_argument("--font_path", default=r"E:\Code\YOLO-Chinese\HarmonyOS_Sans_SC_Regular.ttf", type=str, help="字体路径, 默认鸿蒙字体")
    parser.add_argument("--text_fill", default=True, type=bool, help="字体是否填充底纹,默认填充为红色")
    parser.add_argument("--textSize", default=20, type=int, help="字体大小")
    parser.add_argument("--textColor", default=(255, 255, 255), help="字体颜色RGB")
    parser.add_argument("--thickness", default=2, type=int, help="框的线厚度")

    parser.add_argument("--device", default='0', type=str, help="在哪张卡上推理")
    args = parser.parse_args()
    os.environ['CUDA_VISIBLE_DEVICES'] = args.device

    # run
    demo_yolo = YOLOv8(args)
    for img_path in os.listdir(args.img_dir):
        demo_yolo(os.path.join(args.img_dir, img_path))

3.3 耗时分析

耗时:几乎忽略不计,效果优于其他方法。
在这里插入图片描述

3.4 结果可视化

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值