YOLOv5端侧部署代码分析

模型流程

1. NPU初始化
NpuUint npu_uint;
int ret = npu_uint.npu_init();

2.根据传入的模型文件,创建网络
NetworkItem yolov5;
status = yolov5.network_create(model_file, network_id);
    vip_create_network()
    network_create_io_buffer()//创建输入buffer

3. 准备和验证网络的配置,进行运行前的准备
status = yolov5.network_prepare();

4. 获取输入的bufferbuffer在步骤2中创建的的
yolov5.get_network_input_buff_info(0, &input_buffer_ptr, &input_buffer_size);
yolov5_preprocess_no_copy(input_file, input_buffer_ptr, input_buffer_size);
    get_input_data //对输入图像进行预处理,调整合适的尺寸,获取一个输入指针。

5.创建一个输出buffer,实际已经预分配好了,output_data数组中存储的是指针
int output_cnt = yolov5.get_output_cnt();
float **output_data = new float*[output_cnt]();
for (int i = 0; i < output_cnt; i++)
    output_data[i] = new float[yolov5.m_output_data_len[i]];

//模型跑多少次?loop_count
while(count <loop_count) {
    count ++;
    5. 配置网络的输入和输出缓存区
    status = yolov5.network_input_output_set()
        vip_set_input
        vip_set_output
    6. 提交运行网络的任务,开始运行网络
    status = yolov5.network_run();
    7. 获取输出,输出是什么样的格式?
    yolov5.get_output(output_data);
    8.后处理
    yolov5_postprocess(input_file, output_data);
}

模型输入预处理

输入数据预处理

void get_input_data(const char* image_file, unsigned char* input_data, int letterbox_rows, int letterbox_cols)
{
    cv::Mat sample = cv::imread(image_file, 1);
    if (sample.empty()) {
        fprintf(stderr, "cv::imread %s failed\n", image_file);
        return;
    }

    cv::Mat img;
    cv::cvtColor(sample, img, cv::COLOR_BGR2RGB);

    /* letterbox process to support different letterbox size */

    //按比例缩放输入图片
    float scale_letterbox;
    if ((letterbox_rows * 1.0 / img.rows) < (letterbox_cols * 1.0 / img.cols)){
        scale_letterbox = letterbox_rows * 1.0 / img.rows;
    } else {
        scale_letterbox = letterbox_cols * 1.0 / img.cols;
    }
    int resize_cols = int(scale_letterbox * img.cols);
    int resize_rows = int(scale_letterbox * img.rows);
    cv::resize(img, img, cv::Size(resize_cols, resize_rows));

    // 创建Mat,但并不会进行内存拷贝而是创建了一个 头部指针(header)
    //来引用 input_data 指向的内存。
    cv::Mat img_new(letterbox_cols, letterbox_rows, CV_8UC3, input_data);

    //保持原始图像长宽比的同时缩放到目标尺寸,不足部分用灰色(114)填充
    int top = (letterbox_rows - resize_rows) / 2;
    int bot = (letterbox_rows - resize_rows + 1) / 2;
    int left = (letterbox_cols - resize_cols) / 2;
    int right = (letterbox_cols - resize_cols + 1) / 2;
    // Letterbox filling
    cv::copyMakeBorder(img, img_new, top, bot, left, right, cv::BORDER_CONSTANT, cv::Scalar(114, 114, 114));
}

模型后处理

int yolov5_postprocess(const char *imagepath, float **output)
{
    cv::Mat m = cv::imread(imagepath, 1);
    std::vector<Object> objects;
    detect_yolov5_rt(m, objects, output);
    draw_objects(m, objects);
    return 0;
}

static int detect_yolov5_rt(const cv::Mat& bgr, std::vector<Object>& objects, float **output)
{
    std::chrono::steady_clock::time_point Tbegin, Tend;

    1.使用 std::chrono::steady_clock 进行时间计时,计算函数执行时间。
        Tbegin 记录开始时间,Tend 用于记录结束时间。
    Tbegin = std::chrono::steady_clock::now();

    2. output 是一个三维数组,通常包含3个特征图,每个特征图对应不同尺度的输出数据。
        每个特征图中的数据会被逐步解析,以得到目标物体的位置和类别信息。
    const float  *p8_data_ptr = output[0];
    const float *p16_data_ptr = output[1];
    const float *p32_data_ptr = output[2];

    // set default letterbox size
    int letterbox_rows = LETTERBOX_ROWS;
    int letterbox_cols = LETTERBOX_COLS;
    /* postprocess */
    const float prob_threshold = SCORE_THRESHOLD;
    //物体的置信度阈值,低于该值的目标将被丢弃。
    const float nms_threshold = NMS_THRESHOLD;
    //用于非最大抑制的阈值,用于去除重复的检测框,保留具有最高置信度的框。

    std::vector<Object> proposals;
    std::vector<Object> objects8;
    std::vector<Object> objects16;
    std::vector<Object> objects32;

    3.generate_proposals_rt是一个用于从每个尺度的输出特征图中生成候选框proposals
    函数。每个尺度(8x816x16  32x32)都会调用该函数,并将检测到的物体存储在对应
    objects向量中
    generate_proposals_rt(32, p32_data_ptr, prob_threshold, objects32, letterbox_cols, letterbox_rows);
    proposals.insert(proposals.end(), objects32.begin(), objects32.end());
    generate_proposals_rt(16, p16_data_ptr, prob_threshold, objects16, letterbox_cols, letterbox_rows);
    proposals.insert(proposals.end(), objects16.begin(), objects16.end());
    generate_proposals_rt( 8,  p8_data_ptr, prob_threshold, objects8, letterbox_cols, letterbox_rows);
    proposals.insert(proposals.end(), objects8.begin(), objects8.end());

    4.将候选框按照置信度排序,从高到低。
    qsort_descent_inplace(proposals);
    std::vector<int> picked;
    5.进行非最大抑制,去除冗余的候选框,保留置信度较高且没有重叠过多的框。
      picked 向量存储最终选择的框的索引。
    nms_sorted_bboxes(proposals, picked, nms_threshold);

    6. 调整框的位置(从 letterbox 大小到原始图像大小)
    计算输入图像与 letterbox 大小之间的缩放比率。letterbox 是指图像填充到
    固定尺寸时,保持原图宽高比的矩形区域。计算缩放后的图像尺寸,并根据缩放比例
    调整框的位置。
    /* yolov5 draw the result */
    float scale_letterbox;
    int resize_rows;
    int resize_cols;
    if ((letterbox_rows * 1.0 / bgr.rows) < (letterbox_cols * 1.0 / bgr.cols))
    {
        scale_letterbox = letterbox_rows * 1.0 / bgr.rows;
    }
    else
    {
        scale_letterbox = letterbox_cols * 1.0 / bgr.cols;
    }
    resize_cols = int(scale_letterbox * bgr.cols);
    resize_rows = int(scale_letterbox * bgr.rows);

    int tmp_h = (letterbox_rows - resize_rows) / 2;
    int tmp_w = (letterbox_cols - resize_cols) / 2;

    float ratio_x = (float)bgr.rows / resize_rows;
    float ratio_y = (float)bgr.cols / resize_cols;

    7. 修正目标框的位置.picked.size() 表示最终选中的目标框数量。对于每个选
    中的目标框(通过 picked 向量索引),调整其坐标,从 letterbox 坐标系转换回
    原图坐标系。通过计算得到的 ratio_x  ratio_y 来调整坐标,确保目标框与原始
    图像大小一致。
    int count = picked.size();
    objects.resize(count);
    for (int i = 0; i < count; i++)
    {
        objects[i] = proposals[picked[i]];
        float x0 = (objects[i].rect.x);
        float y0 = (objects[i].rect.y);
        float x1 = (objects[i].rect.x + objects[i].rect.width);
        float y1 = (objects[i].rect.y + objects[i].rect.height);

        x0 = (x0 - tmp_w) * ratio_x;
        y0 = (y0 - tmp_h) * ratio_y;
        x1 = (x1 - tmp_w) * ratio_x;
        y1 = (y1 - tmp_h) * ratio_y;

        x0 = std::max(std::min(x0, (float)(bgr.cols - 1)), 0.f);
        y0 = std::max(std::min(y0, (float)(bgr.rows - 1)), 0.f);
        x1 = std::max(std::min(x1, (float)(bgr.cols - 1)), 0.f);
        y1 = std::max(std::min(y1, (float)(bgr.rows - 1)), 0.f);

        objects[i].rect.x = x0;
        objects[i].rect.y = y0;
        objects[i].rect.width = x1 - x0;
        objects[i].rect.height = y1 - y0;
    }
    8. 计算函数执行时间,并输出检测到的目标数目
    Tend = std::chrono::steady_clock::now();
    float f = std::chrono::duration_cast <std::chrono::milliseconds> (Tend - Tbegin).count();
    fprintf(stderr, "detection num: %d\n", count);

    return 0;
}

该函数主要用于处理 YOLOv5 模型的输出结果,通过三个不同尺度的输出特征图来生成候选框,然后使用非最大抑制(NMS)去除冗余框,最后将目标框的坐标从 letterbox 坐标系转换回原始图像的坐标系。该函数的核心任务是目标检测的后处理,包括筛选高置信度框、去除重复框、调整框位置等步骤。

计算先验框矩形坐标

static void generate_proposals_rt(int stride, const float* feat, float prob_threshold, std::vector<Object>& objects,
                               int letterbox_cols, int letterbox_rows)
{
    static float anchors[18] = {10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326};
    //YOLO预定义的anchor尺寸,共9组(每组宽高),18个值
    int anchor_num = 3; //3个先验框
    int feat_w = letterbox_cols / stride; //计算特征图大小,如果是下采样8倍
    int feat_h = letterbox_rows / stride; //那就是80 * 80
    int cls_num = CLASS_NUM; //分类数量,默认是80
    int anchor_group; //对先验框进行分组
    if (stride == 8)
        anchor_group = 1;
    if (stride == 16)
        anchor_group = 2;
    if (stride == 32)
        anchor_group = 3;
    //反Sigmoid函数,将0~1的输入转化为原始值
    float deprob_threshold = desigmoid(prob_threshold);
    //特征图的尺寸大小,如果是8倍下采样就是80*80=6400份
    int feat_size = feat_w * feat_h;
    //计算单个anchor的特征数据总量,用于定位内存中不同anchor的偏移位置
    //通过feat_ptr + feat_size_cls_5 * n可精确访问第n个anchor的预测数据
    //feat_size_cls_5 = 80*80*85 = 544000
    int feat_size_cls_5 = feat_size * (cls_num + 5);
    //YOLOv5的输出特征图按(cls_num+5)*anchor_num*feat_size组织
    float *feat_ptr = (float*)feat;

    //将原始一维特征数组feat_ptr转换为3个二维矩阵anchor_0/1/2,每个矩阵维度为(cls_num+5)×feat_size
    cv::Mat anchor_0 = cv::Mat((cls_num + 5), feat_size, CV_32FC1, feat_ptr);
    cv::Mat anchor_1 = cv::Mat((cls_num + 5), feat_size, CV_32FC1, feat_ptr + feat_size_cls_5);
    cv::Mat anchor_2 = cv::Mat((cls_num + 5), feat_size, CV_32FC1, feat_ptr + feat_size_cls_5 * 2);

     //anchor_0直接引用feat_ptr起始地址,对应第一个anchor的预测数据。
     //anchor_1通过feat_ptr + feat_size_cls_5偏移,定位到第二个anchor的数据块。
     //anchor_2使用feat_size_cls_5*2偏移量访问第三个anchor数据。
    cv::transpose(anchor_0, anchor_0);
    cv::transpose(anchor_1, anchor_1);
    cv::transpose(anchor_2, anchor_2);

    float *anchor_data[3] = {(float*)anchor_0.data, (float*)anchor_1.data, (float*)anchor_2.data};
    //创建float*类型的数组anchor_data[3],存储3个锚点矩阵的数据指针。

    //三重循环分别遍历特征图高度(feat_h)、宽度(feat_w)和锚点数量(anchor_num)
    for (int h = 0; h <= feat_h - 1; h++)
    {
        int h_feat_w_cls_5 = h * feat_w * (cls_num + 5);
        //h_feat_w_cls_5计算当前行在特征图中的内存偏移量,
        for (int w = 0; w <= feat_w - 1; w++)
        {
            int w_cls_5 =  w * (cls_num + 5);
            //w_cls_5计算当前列在行内的偏移量
            for (int a = 0; a <= anchor_num - 1; a++)
            {
                //process cls score
                int class_index = 0;
                float class_score = -FLT_MAX;
                int a_idx = h_feat_w_cls_5 + w_cls_5;
                //定位到当前锚点的类别分数起始位置(+4跳过前4个坐标参数)
                const float *feat_ptr = anchor_data[a] + a_idx + 4;
                //遍历所有类别(cls_num),通过比较*(feat_ptr + s + 1)获
                //遍历类别概率索引,计算类别中概率最高得分,用于计算先验框分数
                for (int s = 0; s <= cls_num - 1; s++)
                {
                    if (/*score*/ *(feat_ptr + s + 1) > class_score)
                    {
                        class_index = s;
                        class_score = *(feat_ptr + s + 1);  //score;
                    }
                }
                //process box score
                float box_score = *feat_ptr;
                //box_score直接读取特征图数据(*feat_ptr),表示当前锚点包含目标的置信度

                float final_score = 0.0f;
                //采用双阈值过滤机制(deprob_threshold),仅当框分数和类别分数均达标时才计算最终分数
                if (box_score >= deprob_threshold && class_score >= deprob_threshold)
                    final_score = sigmoid(box_score) * sigmoid(class_score);
                    //先验框得分 = 置信度值 x 类别概率中最大的值
                //下面是YOLOv5特有的解码格式计算矩形框(x0,y0,x1,y1)
                if (final_score >= prob_threshold)
                {
                    int loc_idx = a_idx;
                    float dx = sigmoid(*(anchor_data[a] + loc_idx + 0));
                    float dy = sigmoid(*(anchor_data[a] + loc_idx + 1));
                    float dw = sigmoid(*(anchor_data[a] + loc_idx + 2));
                    float dh = sigmoid(*(anchor_data[a] + loc_idx + 3));
                    float pred_cx = (dx * 2.0f - 0.5f + w) * stride;
                    float pred_cy = (dy * 2.0f - 0.5f + h) * stride;
                    float anchor_w = anchors[(anchor_group - 1) * 6 + a * 2 + 0];
                    float anchor_h = anchors[(anchor_group - 1) * 6 + a * 2 + 1];
                    float pred_w = dw * dw * 4.0f * anchor_w;
                    float pred_h = dh * dh * 4.0f * anchor_h;
                    float x0 = pred_cx - pred_w * 0.5f;
                    float y0 = pred_cy - pred_h * 0.5f;
                    float x1 = pred_cx + pred_w * 0.5f;
                    float y1 = pred_cy + pred_h * 0.5f;

                    Object obj;
                    obj.rect.x = x0;
                    obj.rect.y = y0;
                    obj.rect.width = x1 - x0;
                    obj.rect.height = y1 - y0;
                    obj.label = class_index;
                    //先验框最后得分 = 置信度值 x 类别概率中最大的值
                    obj.prob = final_score;
                    //预测框被封装为Object结构体,存入objects向量供后续NMS处理
                    objects.push_back(obj);
                }
            }
        }
    }
}

generate_proposals_rt函数的作用是把所有先验框的信息(坐标信息、置信度、分类概率)计算处理放到objects中。一共是25200个先验框数量。

  • 8倍下采样先验框数量: 3 x 80 x 80 = 19200
  • 16倍下采样先验框数量: 3 x 40 x 40 = 4800
  • 32倍下采样先验框数量: 3 x 20 x 20 = 1200

非极大值抑制

static void qsort_descent_inplace(std::vector<Object>& objects, int left, int right)
{
    int i = left;
    int j = right;
    float p = objects[(left + right) / 2].prob;

    while (i <= j)
    {
        while (objects[i].prob > p)
            i++;

        while (objects[j].prob < p)
            j--;

        if (i <= j)
        {
            // swap
            std::swap(objects[i], objects[j]);

            i++;
            j--;
        }
    }

#pragma omp parallel sections
    {
#pragma omp section
        {
            if (left < j) qsort_descent_inplace(objects, left, j);
        }
#pragma omp section
        {
            if (i < right) qsort_descent_inplace(objects, i, right);
        }
    }
}

static void qsort_descent_inplace(std::vector<Object>& objects)
{
    if (objects.empty())
        return;

    qsort_descent_inplace(objects, 0, objects.size() - 1);
}

按照先验框得分使用快排进行排序,先验框得分为先验框得分 = 置信度值 x 类别概率中最大的值,在generate_proposals中计算而来。

static void nms_sorted_bboxes(const std::vector<Object>& objects, std::vector<int>& picked, float nms_threshold, bool agnostic = false)
{
    //清空输出容器picked, 预计算所有检测框面积存入areas数组,避免重复计算。
    picked.clear();
    const int n = objects.size();
    std::vector<float> areas(n);
    //调用OpenCV的area()方法计算每个矩形框面积
    for (int i = 0; i < n; i++) {
        areas[i] = objects[i].rect.area();
    }

    //遍历所有检测框,初始化当前框的保留标志keep
    for (int i = 0; i < n; i++) {
        const Object& a = objects[i];
        int keep = 1;
        //循环遍历已选中的检测框进行IoU计算
        for (int j = 0; j < (int)picked.size(); j++)
        {
            const Object& b = objects[picked[j]];
            // intersection over union
            float inter_area = intersection_area(a, b);
            float union_area = areas[i] + areas[picked[j]] - inter_area;
            //计算交并比:inter_area / union_area,超过阈值则标记当前框为抑制状态
            if (inter_area / union_area > nms_threshold)
                keep = 0;
        }
        //未被抑制的框索引加入结果集
        if (keep)
            picked.push_back(i);
    }
}

绘制目标框

static void draw_objects(const cv::Mat& bgr, const std::vector<Object>& objects)
{
    std::chrono::steady_clock::time_point Tbegin, Tend;
    1.使用 std::chrono::steady_clock 进行时间计时。Tbegin 记录开始时间,Tend
        用于记录结束时间。目的是计算绘制物体框和标签的时间开销。
    Tbegin = std::chrono::steady_clock::now();

    2.将传入的原始图像 bgr 克隆到 image 中。这是因为原始图像不应该被修改,绘制操
        作会在image上进行,而原图bgr保持不变
    cv::Mat image = bgr.clone();

    3.遍历 objects 向量,这个向量包含了所有检测到的物体(每个物体通过 Object 
        型表示)。obj 是当前遍历到的物体。
    for (size_t i = 0; i < objects.size(); i++)
    {
        const Object& obj = objects[i];

        4.打印输出物体的详细信息:物体的标签 (obj.label)、置信度
            (obj.prob * 100)、物体框的坐标(左上角和右下角),以及物体的类别
            名称(g_classes_name[obj.label])。这对于调试非常有用。
        fprintf(stderr, "%2d: %3.0f%%, [%4.0f, %4.0f, %4.0f, %4.0f], %s\n", obj.label, obj.prob * 100, obj.rect.x,
                obj.rect.y, obj.rect.x + obj.rect.width, obj.rect.y + obj.rect.height, g_classes_name[obj.label].c_str());

        5.使用 OpenCV  cv::rectangle 函数在图像上绘制矩形框,表示检测到的物体位置。
        obj.rect 是物体的边界框,cv::Scalar(255, 0, 0) 表示框的颜色为蓝色(BGR格式)。
        cv::rectangle(image, obj.rect, cv::Scalar(255, 0, 0));

        6. 使用 sprintf 函数构造文本,显示物体的类别名称和置信度(格式为:类别名称 + 置信度百分比)。
        g_classes_name[obj.label] 获取物体的类别名称,obj.prob * 100 表示物体的置信度。
        char text[256];
        sprintf(text, "%s %.1f%%", g_classes_name[obj.label].c_str(), obj.prob * 100);

        int baseLine = 0;
        cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);

        int x = obj.rect.x;
        int y = obj.rect.y - label_size.height - baseLine;
        if (y < 0)
            y = 0;
        if (x + label_size.width > image.cols)
            x = image.cols - label_size.width;
        //绘制文本的白色背景矩形框
        cv::rectangle(image, cv::Rect(cv::Point(x, y), cv::Size(label_size.width, label_size.height + baseLine)),
                      cv::Scalar(255, 255, 255), -1);
        //绘制文本信息
        cv::putText(image, text, cv::Point(x, y + label_size.height), cv::FONT_HERSHEY_SIMPLEX, 0.5,
                    cv::Scalar(0, 0, 0));
    }

    cv::imwrite("output_yolov5.png", image);

    Tend = std::chrono::steady_clock::now();
    float f = std::chrono::duration_cast <std::chrono::milliseconds> (Tend - Tbegin).count();

    std::cout << "draw objects time : " << f << " ms" << std::endl;
}

opencv参考函数

void cv::rectangle(
    cv::Mat& img,           // 输入输出图像
    cv::Rect rec,           // 矩形的位置和大小
    const cv::Scalar& color, // 矩形的颜色(BGR格式)
    int thickness = 1,      // 矩形边框的厚度,默认是 1
    int lineType = 8,       // 线条类型,默认为 8(抗锯齿线)
    int shift = 0           // 对坐标进行的位移,默认为 0
);

cv::Mat img = cv::imread("image.jpg");  // 读取图像
cv::Rect rect(50, 50, 200, 100);       // 创建一个矩形,左上角为(50, 50),宽200,高100
cv::Scalar color(0, 255, 0);           // 设置矩形的颜色为绿色(BGR格式)
cv::rectangle(img, rect, color, 2);    // 在图像上绘制绿色的矩形,边框厚度为2
cv::imwrite("output.jpg", img);        // 保存图像
  • img: 输入和输出图像,矩形将被绘制在这张图像上。
  • rec: 这个矩形的坐标和尺寸,用 cv::Rect(x, y, width, height) 来指定矩形的位置(左上角 (x, y))和尺寸(宽度和高度)。
  • color: 矩形边框的颜色,采用 cv::Scalar 类型表示颜色(BGR 格式),例如 cv::Scalar(255, 0, 0) 表示蓝色。
  • thickness: 边框的厚度。如果设置为负值(例如 -1),则矩形将会填充颜色,而不是只绘制边框。
  • lineType: 线条类型,决定了绘制线条的质量,常用的值有 8(8-connected lines)、4(4-connected lines),以及 CV_AA(抗锯齿线)。
  • shift: 表示坐标的位移,通常在进行精度处理时使用。
void cv::putText(
    cv::Mat& img,                   // 输入输出图像
    const std::string& text,         // 要绘制的文本
    cv::Point org,                  // 文本的起始位置(左下角)
    int fontFace,                   // 字体类型
    double fontScale,               // 字体大小的缩放因子
    const cv::Scalar& color,        // 文本颜色(BGR格式)
    int thickness = 1,              // 字体线条的粗细,默认为 1
    int lineType = 8,               // 线条类型,默认为 8
    bool bottomLeftOrigin = false   // 是否以左下角为原点,默认为 false
);

cv::Mat img = cv::imread("image.jpg");  // 读取图像
std::string text = "Hello, OpenCV!";    // 要绘制的文本
cv::Point org(50, 50);                  // 文本的起始位置(左上角)
cv::Scalar color(0, 0, 255);            // 文本颜色为红色(BGR格式)
int fontFace = cv::FONT_HERSHEY_SIMPLEX; // 使用无衬线字体
double fontScale = 1.0;                 // 字体大小
int thickness = 2;                      // 字体粗细

cv::putText(img, text, org, fontFace, fontScale, color, thickness); // 在图像上绘制文本
cv::imwrite("output_text.jpg", img);    // 保存图像
  • img: 输入和输出图像,文本将被绘制在这张图像上。
  • text: 需要绘制的文本内容,使用 std::string 类型传递。
  • org: 文本的起始位置(左下角的坐标),使用 cv::Point(x, y) 形式来指定。
  • fontFace: 字体类型,OpenCV 提供了几种常见的字体类型:

cv::FONT_HERSHEY_SIMPLEX:简单无衬线字体; cv::FONT_HERSHEY_PLAIN:简单的无衬线字体,字形更小; cv::FONT_HERSHEY_COMPLEX:复杂的无衬线字体; cv::FONT_HERSHEY_SCRIPT_SIMPLEX:手写体; cv::FONT_HERSHEY_SCRIPT_COMPLEX:复杂的手写体等。

  • fontScale: 字体的缩放因子,用于调整字体大小。1.0 表示默认大小,2.0 表示更大。 color: 文本颜色,采用 cv::Scalar 类型表示颜色(BGR 格式),例如 cv::Scalar(255, 0, 0) 表示蓝色。
  • thickness: 字体的线条粗细,默认为 1。
  • lineType: 线条类型,决定了绘制文本时的线条质量,常见的有 8(8-connected lines)和 CV_AA(抗锯齿线)。
  • bottomLeftOrigin: 是否以左下角为原点。默认是 false,意味着文本是从左上角开始绘制的。如果设置为 true,则文本从左下角开始绘制

评论