openvino入门:轻松调用预训练模型进行图像识别(你的第一个目标检测程序!)

大家好!你是否对深度学习模型的部署和加速感兴趣?想知道如何让你的AI应用在CPU或Intel集成显卡上也能跑的飞快吗?那么,Intel的OpenVINO"工具套件就是你的不二之选"!

今天,我们将开启OpenVINO学习之旅的第一步:学习如何使用OpenVINO C++ API来加载一个预训练好的目标检测模型(例如YOLO系列),并用它来识别图片或视频种的物体。这篇博客将侧重于同步推理的基础流程,一步步带你理解从模型加载到结果呈现的全过程。

什么是OpenVINO?

OpenVINO™ (Open Visual Inference & Neural network Optimization) 是一个用于优化和部署深度学习模型的综合工具套件。简单来说,它像一个“翻译官”和“加速器”:

翻译官:它可以读取不同深度学习框架(如TensorFlow,PyTorch,ONNX)训练出来的模型

加速器:它能针对Intel的各种硬件(CPU、集成显卡IGPU、专用视觉处理单元VPU等)进行深度优化,让模型跑的更快、更省电。

我们的目标:构建一个简单的目标检测程序

我们将编写一个C++程序,它可以:

1.加载一个预训练的目标检测模型(比如常见的ONNX格式)

2.读取一张图片

3.对图片进行预处理,使其符合模型的输入要求。

4.使用OpenVINO执行模型推理。

5.对模型的输出进行后处理,提取有用的检测结果(如物体位置、类别、置信度)

6.在原始图片上绘制检测框并显示

核心步骤概览(一步一个脚印):

1.环境准备:确保你已经安装了OpenVINO SDK和OpenCV。

2.初始化OpenVINO Core:这是与OpenVINO运行时交互的起点。

3.读取模型:将模型文件加载到内存中。

4.配置模型输入输出(重要!):告诉OpenVINO你的数据是什么样的,模型期望什么样的数据。

5.编译模型:OpenVINO根据你的目标硬件和配置优化模型。

6.创建推理请求:获取一个用于执行单次推理的对象。

7.准备输入数据

  • 用OpenCV读取图片
  • 预处理:这是关键的一步,包括缩放图像到模型指定尺寸、调整颜色通道顺序、归一化像素值,并将其转换成模型能够理解的“Blob”格式。

8.执行推理(同步):将预处理好的数据送入模型,等待计算结果

9.处理输出数据

  • 解析:模型的原始输出通常是一堆数字(张量),需要按照模型的定义来解读它们。
  • 置信度阈值过滤:只保留“比较确定”的检测结果。
  • 非极大值抑制(NMS):消除对同一物体的重复检测框。

10.可视化结果:将检测框和标签画在图片上。

让我们可是编码!(同步推理详解)

首先,定义一些程序需要的常量和包含必要的头文件:

#include <iostream>
#include <vector>
#include <string>
#include <chrono> //用于计时

#include <opencv2/opencv.hpp> //OpenCV核心功能、图片读写、绘图
#include <opencv2/dnn.hpp>    //OpenCV的DNN模块,我们将用它进行图像预处理(blobFromImage)和NMS

#include <openvino/openvino.hpp> //OpenVINO核心头文件
// 如果使用旧版OpenVINO (2023.2之前),可能需要 #include <inference_engine.hpp>

// --- 基本配置 ---
const std::vector<std::string> CLASS_NAMES = { //你的模型能够识别的类别名称列表
     // 示例: "person", "car", "dog", "cat"... 根据你的模型修改
};
const std::string MODEL_PATH = "D:/path/to/your/model/best.onnx"; //修改为你的模型文件路径(.onnx或.xml(IR模型))
const std::string INPUT_IMAGE_PATH = "D:/path/to/your/image.jpg"; // 修改为你要识别的图片路径

// --- 模型相关参数(非常重要,必须与你的模型一致!) ---
const int MODEL_INPUT_WIDTH = 640; //模型期望的输入图像宽度(像素)
const int MODEL_INPUT_HEIGHT = 640; //模型期望的输入图像高度(像素)

// --- 后处理参数 ---
const float CONFIDENCE_THRESHOLD = 0.5f; //置信度阈值
const float NMS_THRESHOLD = 0.4f; //非极大值抑制(NMS)的IOU阈值

//绘图函数声明(具体实现在main函数后)
void draw_detections(cv::Mat& frame,
		const std::vector<cv::Rect>& boxes,
        const std::vector<float>& confidences,
        const std::vector<int>& class_ids,
        const std::vector<int>& indices,
        const std::vector<std::string>& class_name);

int main(){
    std::cout << "--- OpenVINO Synchronous Inference Demo ---" << std::endl;
    
    //1. 初始化 OpenVINO Core
    //ov::Core 是OpenVINO运行时的主要入口点,用于设备管理和模型操作。
    ov::Core core;
    std::cout << "Step 1: OpenVINO Core initialized." << std::endl;
    
    //2.读取模型
    //ov::Model 对象代表了从磁盘加载的神经网络结构
    std::shared_ptr<ov::Model> model;
    try{
        // core.read_mode() 可以读取OpenVINO IR格式(.xml和.bin) 或ONNX格式(.onnx)
        model = core.read_model(MODEL_PATH);
        std::cout << "Step 2: Model loaded successfully from: " << MODEL_PATH << std::endl;
    }catch(const std::exception& e){
        std::cerr << "Model loading failed: " << e.what() << std::endl;
        return -1;
    }
    
    
    // --- 理解模型输入:MODEL_INPUT_WIDTH 和 MODEL_INPUT_HEIGHT ---
    //这两个值定义了神经网络在训练时接收的图片尺寸。
    //例如,YOLOv11 可以使用640×640 或者 1280×1280 的输入
    //我们输入的任何图像,在送入模型前都必须被缩放(或填充)到这个尺寸。
    //如果尺寸不匹配,推理结果通常时无意义的。
    
    //3.配置模型输入和输出(可选,可以不用这步,只是让你了解一下模型输入的预处理)
    //这一步告诉OpenVINO我们打算如何向模型提供数据,以及期望如何接收数据。
    //ov::preprocess::PrePostProcessor(PPP) 是一个强大的工具,用于定义预处理和后处理步骤。
    ov::preprocess::PrePostProcessor ppp(model);
    
    //配置模型的第一个输入(通常图像模型只有一个图像输入)
    // ppp.input() 获取默认的第一个输入端口的配置器
    ppp.input().tensor()
    //.set_element_type(ov::element::u8):告诉OpenVINO,我们将提供原始的8位无符号整数像素数据(0-255)。
    //如果你的预处理直接输出浮点数,这里可以是 ov::element::f32。
    //u8通常与后续的.convert_element_type(ov::element::f32)和.scale()结合使用。
    .set_element_type(ov::element::u8)
    //.set_layout("NHWC"):指定输入张量数据的布局。
    // N : Batch size (批处理大小,一次处理多少张图片)
    // H : Height (高度)
    // W : Width (宽度)
    // C : Channels (颜色通道数,如RGB是3)
    // OopenCV的cv::Mat默认是HWC布局。当我们创建一个批次(即使是单张图片),就变成了NHWC.
    // 如果你的模型内部需要不同的布局(如NCHW),我们稍后会转换。
    .set_layout("NHWC")
    //.set_shape({1, MODEL_INPUT_HEIGHT, MODEL_INPUT_WIDTH, 3}):明确指定输入张量的形状。
    // {1, ...}: Batch size 为 1,表示一次推理一张图片。
    //MODEL_INPUT_HEIGHT, MODEL_INPUT_WIDTH: 之前定义的模型输入高宽。
    //3: 3个颜色通道 (BGR 或 RGB)。
    .set_shape({1, static_cast<unsigned long>(MODEL_INPUT_HEIGHT), static_cast<unsigned long>(MODEL_INPUT_WIDTH), 3});
    
    //模型内部可能期望不同的数据类型或布局,PPP可以处理转换:
    ppp.input().preprocess() //获取预处理步骤配置器
    //.convert_layout("NCHW"):如果模型期望NCHW布局,在这里转换。
    // NCHW = Batch, Channels, Height,Width。很多PyTorch模型使用此布局。
   	.convert_layout("NCHW")
    // .convert_element_type(ov::element::f32):将u8像素值转换为32位浮点数。
    //深度学习模型通常在内部使用浮点数进行计算.
    .convert_element_type(ov::element::f32)
    // .scale(255.0f): 对像素值进行归一化,通常是除以255,使像素值在0.0到1.0之间。
    // 这是常见的预处理步骤。有些模型可能需要不同的归一化方式(如减均值除标准差)。
     .scale(255.0f); // 等价于 value / 255.0f
    
    //配置模型的第一个输出(目标检测模型可能有多个输出,这里假设最主要的那个)
    // ppp.output() 或 ppp.output(output_port_index_or_name)
    ppp.output(0).tensor() //获取第一个输出端口的配置器
    // .set_element_type(ov::element::f32): 假设模型的输出是32位浮点数。
    
    //这很常见,因为输出通常包含坐标和置信度等连续值。
     .set_element_type(ov::element::f32);
    
    
    //应用这些预处理/后处理配置到模型上
    model = ppp.build();
   	std::cout << "Step 3: Model pre/post processing configured (Input: u8 NHWC -> f32 NCHW, Output: f32)." << std::endl;
    
    // --- 理解数据类型f32和布局NHWC/NCHW ---
    //f32(float32) :指的是32位单精度浮点数。这是神经网络中进行数值计算时常用的数据类型。
    //u8(uint8):指的是8位无符号整数,范围0-255。图像像素通常以此格式存储。
    
    //NHWC (Batch,Height,Width,CHannels):
    // -N: 批次大小。比如你想同时推理4涨图片,N就是4。对于单张图片,N是1。
    // -H: 图像高度。
    // -W: 图像宽度。
    // -C: 颜色通道。对于RGB或BGR图像,C是3。
    // 这是TensorFlow和OpenCV图像中的常见内存布局
    
    // NCHW (Batch, Channels, Height, Width):
    //   - C H W 是交错存储的。例如,所有R通道值在一起,然后所有G通道值,然后所有B通道值。
    //   这是PyTorch和许多Caffe模型常用的布局。
    //
    // 为什么需要转换?模型在训练时是基于特定数据类型和布局的。推理时必须匹配。
    // OpenVINO的PPP可以方便地处理这些转换。
    
    
    //4.编译模型
    // ov::CompiledModel 是针对特定设备(如CPU, GPU)优化和编译后的模型。
    // 只有编译后的模型才能创建推理请求。
    ov::CompiledModel compiled_model;
    try{
        //core.compile_model(model,"DEVICE_NAME",properties);
        //"CPU":指定在CPU上执行。其他可选值有"CPU",“AUTO”,等。
        //ov::hint::Performance_mode: 这是一个属性,用于指导编译过程。
        // -ov::hint::PerformanceMode::LATENCY: 优化单次推理的延迟。
        // 适合需要快速响应的场景,如实时交互应用。同步推理通常用这个。
        
        // -ov::hint::PerformanceMode::THROUGHPUT: 优化单位时间内的处理量。
        
        //适合需要处理大量数据(如视频流)的场景,常与异步推理结合。
        compiled_model = core.compile_model(model,"CPU",ov::hint::performance_mode(ov::hint::PerformanceMode::LATENCY));
        std::cout << "Step 4: Model compiled successfully for CPU (LATENCY mode)." << std::endl;
    }catch(const std::exception& e){
        std::cerr << "Model compilation failed: " << e.what() << std::endl;
        return -1;
    }
    
    //--- 理解LATENCY vs THROUGHPUT 模式 ---
    // - LATENCY(延迟优先)
    // 目标是让单次推理尽可能快地完成。OpenVINO会尝试减少从输入到输出的端到端时间。
    // 这可能意味着它不会并行优化所有可并行的操作,以避免并行带来的额外开销
    // 适合场景:用户点击按钮后立即看到结束、交互式应用。
    
    // - THROUGHPUT(吞吐量优先)
    // 目标是在单位时间内处理尽可能多的推理请求。OpenVINO会更积极地使用并行处理、
    //流聚合等技术来最大化利用硬件资源。单次请求的延迟可能会略有增加,
    //但总体处理能力更强。
    //适合场景:视频分析、批量图像处理、服务器端推理。常与异步API和多推理请求池配合。
    
    
    //5.创建推理请求
    //ov::InferRequest 对象用于执行实际的推理。
    //对于同步模式,通常一个请求对象就足够了。
    ov::InferRequest infer_request = compiled_mode.create_infer_request();
    std::cout << "Step 5: Inference request created." << std::endl;
    
    //6.准备输入数据
    //使用OpenCV读取图像
    cv::Mat frame = cv::imread(INPUT_IMAGE_PATH);
    if (frame.empty()) {
        std::cerr << "Error: Could not read image: " << INPUT_IMAGE_PATH << std::endl;
        return -1;
    }
    cv::Mat original_frame_for_drawing = frame.clone(); //复制一份用于后续绘制结果
    int original_width = frame.cols;
    int original_height = frame.rows;
    std::cout << "Step 6a: Image loaded. Original size: " << original_width << "x" << original_height << std::endl;
    
    
    //预处理核心: 将cv::Mat转换为模型输入张量
    //我们在第3步配置了PPP,它期望NHWC的u8输入。
    //OPENVINO的'infer_request.set_input_tensor()' 可以直接接收一个构造好的'ov::Tensor'。
    //这个 ‘ov::Tensor’ 可以共享‘cv::Mat’的数据内存,避免不必要的拷贝。
    
    //首先,确保图像尺寸符合模型输入(如果需要,先用OpenCV缩放)
    cv::Mat resized_frame;
    if(frame.cols != MODEL_INPUT_WIDTH || frame.rows != MODEL_INPUT_HEIGHT){
        cv::resize(frame, resized_frame, cv::Size(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT));
         std::cout << "Step 6b: Image resized to model input size: " << MODEL_INPUT_WIDTH << "x" << MODEL_INPUT_HEIGHT << std::endl;
    }else{
        resized_frame = frame;
    }
    
    //创建一个OpenVINO Tensor,它将直接使用 resized_frame 的数据。
    //注意:数据必须是连续的。cv::Mat通常是连续的,除非它是从一个更大的Mat中截取的ROI且未clone。
    //{1,H,W,C} for NHWC
    ov::Shape input_shape = {1, static_cast<unsigned long>(MODEL_INPUT_HEIGHT), static_cast<unsigned long>(MODEL_INPUT_WIDTH), 3};
    // resized_frame.data 是指向像素数据的指针 (u8*)
    ov::Tensor input_tensor(ov::element::u8, input_shape, resized_frame.data);
    
    //将准备好的输入张量设置给推理请求的第一个输入端口
    //compiled_model.input() 获取默认(第一个)输入端口
    infer_request.set_input_tensor(input_tensor);
    std::cout << "Step 6c: Input tensor prepared and set to infer_request." << std::endl;
    
    // --- 关于批处理 (Batching) ---
    //在上面的'input_shape'中,第一个维度 ‘1’代表批处理大小(N)
    //这意味着我们一次只向模型发送一张图片进行推理。
    //如果你想一次处理多张图片(例如4张),批处理大小N将是4.
    //‘input_shape’ 会是 ‘{4,H,W,C}’,并且你需要将4张图片的数据连续地
    //存放在 'input_tensor'指向的内存中。
    //批处理可以提高吞吐量,因为它允许硬件更有效地并行计算。
    //对于同步单图推理,批处理大小通常为1.
    
    //7.执行推理(同步)
    std::cout << "Step 7: Starting synchronous inference..." << std::endl;
    auto start_time = std::chrono::high_resolution_clock::now();
    infer_request.infer(); // 这是同步调用,程序会在此阻塞,直到推理完成。
    auto end_time = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> infer_duration_ms = end_time - start_time;
    std::cout << "Step 7: Inference completed in " << infer_duration_ms.count() << " ms." << std::endl;
    
    //8.处理输出数据
    std::cout << "Step 8: Processing output tensor..." << std::endl;
    //获取模型的第一个输出张量(大多数目标检测模型的主要输出)
    //如果模型有多个输出,可以用infer_request.get_output_tensor(output_port_index_or_name)
    const ov::Tensor& output_tensor = infer_request.get_output_tensor();
    ov::Shape output_shape = output_tensor.get_shape(); // 获取输出张量的形状
    float* output_data = output_tensor.data<float>();   // 获取指向输出数据的指针 (假设是f32)
    
    std::cout << "Output tensor shape: [ ";
    for (size_t i = 0; i < output_shape.size(); ++i) {
        std::cout << output_shape[i] << (i < output_shape.size() - 1 ? ", " : "");
    }
    std::cout << " ]" << std::endl;
    
    
     // **解析YOLO输出 (这里的逻辑需要根据你的模型具体输出格式调整!)**
    // 常见的YOLO输出格式之一 (如代码所示的ONNX模型):
    //   Shape: [batch_size, num_classes + 4, num_proposals]
    //   或者 [batch_size, num_proposals, num_classes + 4]
    //   其中 4 代表: cx, cy, w, h (中心点x, 中心点y, 宽度, 高度)
    //   或 x1, y1, x2, y2 (左上角和右下角坐标)
    //   num_proposals 是模型生成的候选框数量。
    //
    // 代码中 `MODEL_PATH` 指向 `best.onnx`,其输出格式为:
    //   `output_shape[0]` = batch_size (应为1)
    //   `output_shape[1]` = 4 (cx, cy, w, h) + num_classes (类别分数)
    //   `output_shape[2]` = num_proposals (检测框的数量)
    //   数据排列方式: output_data[box_info_idx * num_proposals + proposal_idx]
    
    std::vector<cv::Rect> boxes;              // 存储边界框
    std::vector<float> confidences;         // 存储每个框的置信度
    std::vector<int> class_ids;             // 存储每个框的类别ID
    
    int num_output_elements_per_proposal = static_cast<int>(output_shape[1]); // e.g., 4 + 80 classes = 84
    int num_model_classes = num_output_elements_per_proposal - 4; // 减去 cx, cy, w, h
    int num_proposals = static_cast<int>(output_shape[2]);

    if (CLASS_NAMES.size() != num_model_classes) {
        std::cerr << "Warning: Number of CLASS_NAMES (" << CLASS_NAMES.size()
                  << ") does not match number of classes in model output (" << num_model_classes
                  << "). Please check your CLASS_NAMES list and model." << std::endl;
        // 如果类别数不匹配,后续的class_ids可能会出错。
    }
    
    // 缩放因子:将模型输出的坐标(相对于模型输入尺寸)转换回原始图像尺寸
    float scale_x = static_cast<float>(original_width) / MODEL_INPUT_WIDTH;
    float scale_y = static_cast<float>(original_height) / MODEL_INPUT_HEIGHT;
    
        for (int i = 0; i < num_proposals; ++i) {
        // 对于每个候选框 (proposal i)
        // output_data 的组织方式:
        // [cx0,cx1,...,cx_N-1, cy0,cy1,...,cy_N-1, w0,...,h0,...,score_cls0_0,...,score_clsM_N-1]
        // N = num_proposals, M = num_model_classes

        float cx = output_data[0 * num_proposals + i]; // 中心点x
        float cy = output_data[1 * num_proposals + i]; // 中心点y
        float w = output_data[2 * num_proposals + i];  // 宽度
        float h = output_data[3 * num_proposals + i];  // 高度

        // 从第4个元素开始是类别分数
        float max_class_score = 0.0f;
        int class_id = -1;
        for (int j = 0; j < num_model_classes; ++j) {
            float score = output_data[(4 + j) * num_proposals + i];
            if (score > max_class_score) {
                max_class_score = score;
                class_id = j;
            }
        }
        
            
       // --- 理解置信度阈值 (CONFIDENCE_THRESHOLD) ---
        // `max_class_score` 是模型对这个框属于 `class_id` 类别的“信心”程度。
        // `CONFIDENCE_THRESHOLD` (例如0.5) 是一个门槛:
        //   如果 max_class_score > CONFIDENCE_THRESHOLD,我们认为这个检测是有效的。
        //   否则,我们忽略它(认为模型不够确定,可能是背景或噪声)。
        // 调高此阈值会减少误报(假阳性),但可能漏掉一些真目标(假阴性)。
        // 调低则相反。
        if (max_class_score > CONFIDENCE_THRESHOLD) {
            // 将归一化坐标转换为原始图像坐标
            float x1 = (cx - w / 2.0f) * scale_x; // cx,cy是中心点,转换为左上角x
            float y1 = (cy - h / 2.0f) * scale_y; // 左上角y
            float box_width = w * scale_x;
            float box_height = h * scale_y;

            boxes.push_back(cv::Rect(static_cast<int>(x1), static_cast<int>(y1),
                                     static_cast<int>(box_width), static_cast<int>(box_height)));
            confidences.push_back(max_class_score);
            class_ids.push_back(class_id);
        }
    }
    std::cout << "Step 8a: Parsed " << boxes.size() << " detections above confidence threshold." << std::endl;

    // **非极大值抑制 (NMS)**
    std::vector<int> nms_indices; // 存储通过NMS的检测框在boxes向量中的索引
    if (!boxes.empty()) {
        // cv::dnn::NMSBoxes 参数:
        //   boxes: 边界框列表。
        //   confidences: 对应的置信度列表。
        //   CONFIDENCE_THRESHOLD: 这里的NMS函数内部可能还会用一次置信度过滤,但我们外部已经做过了。
        //                      通常这里的score_threshold可以设低一些或与外部一致。
        //   NMS_THRESHOLD: IOU (Intersection over Union) 阈值。
        //   nms_indices: 输出,被保留下来的框的索引。
        cv::dnn::NMSBoxes(boxes, confidences, CONFIDENCE_THRESHOLD, NMS_THRESHOLD, nms_indices);
    }
    std::cout << "Step 8b: Performed NMS. Kept " << nms_indices.size() << " detections." << std::endl;
    
    // --- 理解NMS阈值 (NMS_THRESHOLD) ---
    // 非极大值抑制 (Non-Maximum Suppression) 用于解决一个常见问题:
    // 模型可能对同一个物体检测出多个高度重叠的边界框。
    // NMS的工作流程(简化版):
    //   1. 选择置信度最高的检测框A。
    //   2. 计算A与其他所有框的IOU (交并比)。IOU衡量两个框的重叠程度,值在0到1之间。
    //      IOU = (A和B的交集面积) / (A和B的并集面积)
    //   3. 如果某个框B与A的IOU大于 `NMS_THRESHOLD` (例如0.4),则认为B是A的重复检测,抑制(删除)B。
    //   4. 从剩下未被抑制的框中,重复步骤1-3,直到没有框剩下。
    // `NMS_THRESHOLD` 越小,NMS越“严格”,会抑制掉更多框 (可能导致漏检)。
    // 越大,NMS越“宽松”,可能保留一些重叠框。

    
     // 9. 可视化结果
    std::cout << "Step 9: Drawing detections on original image." << std::endl;
    draw_detections(original_frame_for_drawing, boxes, confidences, class_ids, nms_indices, CLASS_NAMES);

    cv::imshow("OpenVINO Detection - Synchronous Demo", original_frame_for_drawing);
    std::cout << "Press any key to exit..." << std::endl;
    cv::waitKey(0); // 等待用户按键
    cv::destroyAllWindows();

    std::cout << "--- Application finished ---" << std::endl;
    return 0;
}

//绘制检测框函数,记得声明!
void draw_detections(cv::Mat& frame,
    const std::vector<cv::Rect>& boxes,
    const std::vector<float>& confidences,
    const std::vector<int>& class_ids,
    const std::vector<int>& indices, // 这些是NMS后保留的框在原始boxes/confidences/class_ids中的索引
    const std::vector<std::string>& class_names) {
    for (int idx : indices) { //只遍历NMS筛选后的索引
        cv::Rect box = boxes[idx];
        float score = confidences[idx];
        int class_id_val = class_ids[idx]; // 使用不同的变量名以避免歧义

        // 安全检查,确保 class_id_val 在 CLASS_NAMES 的有效范围内
        if (class_id_val < 0 || class_id_val >= class_names.size()) {
            std::cerr << "Warning: Invalid class_id " << class_id_val
                      << " encountered in draw_detections. Max valid index is "
                      << class_names.size() - 1 << "." << std::endl;
            continue; // 跳过绘制这个无效的框
        }

        cv::rectangle(frame, box, cv::Scalar(0, 255, 0), 2); // 绿色框
        std::string label = class_names[class_id_val] + ": " + cv::format("%.2f", score);
        int baseLine;
        cv::Size label_size = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.7, 1, &baseLine);
        cv::rectangle(frame,
            cv::Point(box.x, box.y - label_size.height - baseLine),
            cv::Point(box.x + label_size.width, box.y),
            cv::Scalar(0, 255, 0), // 绿色背景
            cv::FILLED);
        cv::putText(frame, label, cv::Point(box.x, box.y - baseLine),
            cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); // 黑色文字
    }
}

关键点回顾与总结:

  • 模型是核心:所有配置(输入尺寸、数据类型、布局、输出解析)都必须严格围绕你的模型来定义。仔细阅读模型文档或使用Netron等工具查看模型结构至关重要。
  • 预处理不可或缺:原始图像数据几乎总是需要转换才能被模型正确理解。OpenVINO的PrePostProcessor (PPP) 使得这个过程更规范和高效。
  • 数据流向
    1. cv::Mat (原始图像, HWC, u8)
    2. cv::resize (如果需要,调整到模型输入尺寸, HWC, u8)
    3. ov::Tensor (共享cv::Mat数据, NHWC, u8, batch=1)
    4. PPP (在infer_request内部自动完成):
      • u8 -> f32 (类型转换)
      • /255.0f (归一化)
      • NHWC -> NCHW (布局转换)
    5. 模型推理 (infer_request.infer())
    6. ov::Tensor (模型输出, 通常NCHW或特定格式, f32)
    7. 解析输出数据 (提取boxes, scores, class_ids)
    8. NMS
    9. 绘制到原始cv::Mat上
  • 同步 vs 异步:我们这里使用的是同步推理 (infer_request.infer())。它简单直接,但对于高性能场景(如视频流处理)可能不是最优的。

下一步是什么?

恭喜你!你已经成功地使用OpenVINO完成了第一个目标检测程序。你理解了模型加载、预处理、同步推理和后处理的基本流程。

但是,如果我们要处理实时视频流,或者希望最大化硬件的吞吐量,这种一次处理一张、并且等待其完成的同步方式可能会遇到瓶颈。CPU在等待推理硬件完成时会空闲,无法充分利用资源。

在下一篇博客中,我们将学习如何利用OpenVINO的异步推理推理请求池技术,来显著提升我们应用的识别效率和流畅度,让你的AI应用真正“飞”起来!敬请期待!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐