一、准备工作
1.1 环境要求
OpenCV 4.5+(需包含
dnn模块,推荐 4.8 或更高版本)C++11 或更高版本的编译器
(可选)CUDA + cuDNN,用于 GPU 加速推理
1.2 模型准备
将训练好的 YOLOv8 PyTorch 模型(.pt 文件)导出为 ONNX 格式
yolo export model=yolov8n.pt imgsz=640 format=onnx opset=12
推荐使用 opset=12 或更高版本,兼容性更好。模型导出后会生成 yolov8n.onnx 文件。
二、核心代码实现
2.1 整体架构
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
using namespace std;
using namespace cv;
using namespace dnn;
class YOLOv8Detector {
public:
YOLOv8Detector(const string& modelPath, const vector<string>& classNames,
float confThreshold = 0.25, float nmsThreshold = 0.45);
vector<Rect> detect(Mat& frame);
void drawResults(Mat& frame, const vector<Rect>& boxes);
private:
Net net;
vector<string> classes;
float confThreshold;
float nmsThreshold;
int inpWidth;
int inpHeight;
Mat preprocess(const Mat& frame);
vector<Rect> postprocess(Mat& frame, const vector<Mat>& outputs);
};2.2 初始化模型
YOLOv8Detector::YOLOv8Detector(const string& modelPath, const vector<string>& classNames,
float confThreshold, float nmsThreshold) {
this->classes = classNames;
this->confThreshold = confThreshold;
this->nmsThreshold = nmsThreshold;
this->inpWidth = 640; // 与导出时 imgsz 保持一致
this->inpHeight = 640;
// 加载 ONNX 模型
net = readNetFromONNX(modelPath);
// 可选:配置后端和硬件加速
net.setPreferableBackend(DNN_BACKEND_OPENCV); // 或 DNN_BACKEND_CUDA
net.setPreferableTarget(DNN_TARGET_CPU); // 或 DNN_TARGET_CUDA
if (net.empty()) {
cerr << "Failed to load model: " << modelPath << endl;
exit(-1);
}
}2.3 图像预处理(Letterbox)
YOLOv8 使用 Letterbox 技术,在保持图像宽高比的同时将图像缩放到目标尺寸,多余部分填充灰色边框
Mat YOLOv8Detector::preprocess(const Mat& frame) {
Mat blob;
// Letterbox 处理
Mat letterbox;
int top, bottom, left, right;
double scale = min((double)inpWidth / frame.cols, (double)inpHeight / frame.rows);
int newWidth = int(frame.cols * scale);
int newHeight = int(frame.rows * scale);
resize(frame, letterbox, Size(newWidth, newHeight));
top = (inpHeight - newHeight) / 2;
bottom = inpHeight - newHeight - top;
left = (inpWidth - newWidth) / 2;
right = inpWidth - newWidth - left;
copyMakeBorder(letterbox, letterbox, top, bottom, left, right, BORDER_CONSTANT, Scalar(114, 114, 114));
// 转换为 blob 格式:归一化到 [0,1],通道顺序 BGR->RGB
blobFromImage(letterbox, blob, 1.0 / 255.0, Size(inpWidth, inpHeight), Scalar(), true, false);
return blob;
}2.4 推理与后处理
YOLOv8 的输出格式与 YOLOv5 不同:输出维度为
[batch, 84, num_detections]每列包含:
[cx, cy, w, h, class1_score, class2_score, ...]需要对每个检测框执行 NMS 去除重叠框
vector<Rect> YOLOv8Detector::postprocess(Mat& frame, const vector<Mat>& outputs) {
vector<Rect> boxes;
vector<float> confidences;
vector<int> classIds;
// outputs[0] 形状: [1, 84, 8400] (8400 = 特征图格子总数)
float* data = (float*)outputs[0].data;
const int numDetections = outputs[0].size[2]; // 8400
const int numClasses = classes.size(); // COCO 为 80
for (int i = 0; i < numDetections; ++i) {
float* detection = data + i * (numClasses + 4);
float* scores = detection + 4;
float maxScore = *max_element(scores, scores + numClasses);
if (maxScore > confThreshold) {
int classId = max_element(scores, scores + numClasses) - scores;
float cx = detection[0];
float cy = detection[1];
float w = detection[2];
float h = detection[3];
// 将坐标还原到原图尺寸
int left = int((cx - w / 2) * frame.cols);
int top = int((cy - h / 2) * frame.rows);
int width = int(w * frame.cols);
int height = int(h * frame.rows);
boxes.push_back(Rect(left, top, width, height));
confidences.push_back(maxScore);
classIds.push_back(classId);
}
}
// NMS 过滤
vector<int> indices;
NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
vector<Rect> finalBoxes;
for (int idx : indices) {
finalBoxes.push_back(boxes[idx]);
// 可选:绘制标签和置信度
rectangle(frame, boxes[idx], Scalar(0, 255, 0), 2);
string label = classes[classIds[idx]] + ": " + to_string(confidences[idx]);
putText(frame, label, Point(boxes[idx].x, boxes[idx].y - 5),
FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0), 2);
}
return finalBoxes;
}2.5 主函数
int main() {
// 加载类别标签(COCO 数据集共 80 类,人形对应第 0 类 "person")
vector<string> classNames = {"person", "bicycle", "car", "motorcycle", "airplane", "bus",
"train", "truck", "boat", "traffic light", "fire hydrant",
/* ... 其余 69 类 ... */};
// 初始化检测器
YOLOv8Detector detector("yolov8n.onnx", classNames, 0.25, 0.45);
// 读取图像或视频
Mat frame = imread("test.jpg");
if (frame.empty()) {
cerr << "Failed to load image" << endl;
return -1;
}
// 执行检测
auto boxes = detector.detect(frame);
// 显示结果
imshow("YOLOv8 + OpenCV Detection", frame);
waitKey(0);
return 0;
}三、编译与运行
3.1 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(YOLOv8_OpenCV)
set(CMAKE_CXX_STANDARD 11)
find_package(OpenCV REQUIRED)
add_executable(yolov8_demo main.cpp)
target_link_libraries(yolov8_demo ${OpenCV_LIBS})3.2 编译
mkdir build && cd build cmake .. make ./yolov8_demo
四、优化建议
| 优化项 | 实现方式 | 效果 |
|---|---|---|
| GPU 加速 | net.setPreferableBackend(DNN_BACKEND_CUDA) + 编译支持 CUDA 的 OpenCV | 推理速度提升 5-10 倍 |
| 多尺度检测 | 在预处理时使用图像金字塔,对不同尺度的图像分别检测 | 提升远处小目标(如远处行人)检出率 |
| 异步流水线 | 视频采集与检测放在不同线程,使用双缓冲或队列 | 提升视频处理的整体帧率 |
| TensorRT 集成 | 将 ONNX 转换为 TensorRT engine 格式 | 推理速度进一步提升(NVIDIA GPU 专属) |
关于远处人形检测:YOLOv8 本身对小目标检测能力较强。如需进一步提升,可以:
适当增大输入尺寸(如将
inpWidth/inpHeight设为 960 或 1280)使用更大的模型(如
yolov8x而非yolov8n)在预处理时对图像中感兴趣区域进行局部放大检测
五、常见问题
Q:OpenCV 不支持 CUDA 怎么办?
可以自行编译 OpenCV 时开启
-DWITH_CUDA=ON选项;或使用 CPU 模式运行(速度较慢,但功能完整)。
Q:模型加载失败?
检查 ONNX 文件路径是否正确,确认导出时的
opset版本(推荐 12),并确保 OpenCV 版本 >= 4.5。
Q:检测框位置偏移?
问题通常出在 Letterbox 坐标还原环节。请确保
scale计算和边框坐标转换时使用的是正确的缩放比例与偏移量。
如果你还需要了解如何结合 DeepSORT 进行多目标跟踪,或者需要在嵌入式平台(如 Jetson) 上部署,我可以进一步展开说明。
为 YOLOv8 创建专属环境
安装完成后,建议为你的 YOLOv8 项目创建一个独立的“干净房间”,避免不同项目的包版本互相打架。
创建新环境
# 创建一个名为 yolov8_env,Python 版本为 3.10 的独立环境 conda create -n yolov8_env python=3.10 -y
激活环境
conda activate yolov8_env
如果你更倾向于使用镜像源,可以配置 Conda 使用国内镜像来完全绕过这个提示:
# 配置清华源(示例) conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2/ conda config --set show_channel_urls yes
安装yolo
pip install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simple
创建demo
from ultralytics import YOLO
# 加载一个官方提供的预训练模型
model = YOLO('yolov8n.pt')
# 执行检测(对图片、视频或摄像头都可以)
results = model('你的图片路径.jpg')
results[0].show() # 显示检测结果测试代码
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <onnxruntime_cxx_api.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <memory>
#include <string>
struct Detection {
cv::Rect box;
float confidence;
int class_id;
};
class YOLOv8ONNX {
public:
YOLOv8ONNX(const std::string& model_path, float conf_threshold = 0.35f, float nms_threshold = 0.45f)
: m_confThreshold(conf_threshold), m_nmsThreshold(nms_threshold), m_originalWidth(0), m_originalHeight(0), m_scale(0), m_xOffset(0), m_yOffset(0) {
try {
// 初始化ONNX Runtime环境 (新版API)
m_env = std::make_unique<Ort::Env>(ORT_LOGGING_LEVEL_WARNING, "yolov8");
// 创建Session选项
m_sessionOptions = std::make_unique<Ort::SessionOptions>();
m_sessionOptions->SetIntraOpNumThreads(1);
m_sessionOptions->SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);
// 参数0表示使用第一块GPU,如有多个GPU可改为其他编号 需要安装CUDA
//Ort::ThrowOnError(Ort::GetApi().SessionOptionsAppendExecutionProvider_CUDA(*m_sessionOptions, 0));
// 加载模型 (使用窄字符路径,Windows需要处理宽字符)
#ifdef _WIN32
// Windows下需要转换路径为宽字符
std::wstring wmodel_path(model_path.begin(), model_path.end());
m_session = std::make_unique<Ort::Session>(*m_env, wmodel_path.c_str(), *m_sessionOptions);
#else
m_session = std::make_unique<Ort::Session>(*m_env, model_path.c_str(), *m_sessionOptions);
#endif
// 在构造函数中获取名称
Ort::AllocatorWithDefaultOptions allocator;
// 获取输入名称
size_t num_input_nodes = m_session->GetInputCount();
m_inputNamesStr.resize(num_input_nodes);
m_inputNames.resize(num_input_nodes);
m_inputShapes.resize(num_input_nodes);
for (size_t i = 0; i < num_input_nodes; i++) {
auto input_name_ptr = m_session->GetInputNameAllocated(i, allocator);
m_inputNamesStr[i] = input_name_ptr.get(); // 复制字符串内容
m_inputNames[i] = m_inputNamesStr[i].c_str(); // 存储指针
std::cout << "输入名称 " << i << ": " << m_inputNames[i] << std::endl;
// 获取 shape 信息
Ort::TypeInfo type_info = m_session->GetInputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
m_inputShapes[i] = tensor_info.GetShape();
}
// 获取输出名称
size_t num_output_nodes = m_session->GetOutputCount();
m_outputNamesStr.resize(num_output_nodes);
m_outputNames.resize(num_output_nodes);
m_outputShapes.resize(num_output_nodes);
for (size_t i = 0; i < num_output_nodes; i++) {
auto output_name_ptr = m_session->GetOutputNameAllocated(i, allocator);
m_outputNamesStr[i] = output_name_ptr.get(); // 复制字符串内容
m_outputNames[i] = m_outputNamesStr[i].c_str();
std::cout << "输出名称 " << i << ": " << m_outputNames[i] << std::endl;
// 获取 shape 信息
Ort::TypeInfo type_info = m_session->GetOutputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
m_outputShapes[i] = tensor_info.GetShape();
}
// 设置输入尺寸
if (!m_inputShapes.empty() && m_inputShapes[0].size() >= 4) {
// YOLOv8 输入格式通常是 [batch, channels, height, width]
m_inputHeight = static_cast<int>(m_inputShapes[0][2]);
m_inputWidth = static_cast<int>(m_inputShapes[0][3]);
}
else {
// 默认值
m_inputWidth = 640;
m_inputHeight = 640;
}
std::cout << "模型加载成功!" << std::endl;
std::cout << "输入尺寸: " << m_inputWidth << "x" << m_inputHeight << std::endl;
std::cout << "输入节点数: " << num_input_nodes << std::endl;
std::cout << "输出节点数: " << num_output_nodes << std::endl;
}
catch (const Ort::Exception& e) {
std::cerr << "ONNX Runtime错误: " << e.what() << std::endl;
throw;
}
}
// 预处理
cv::Mat preprocess(const cv::Mat& image) {
m_originalWidth = image.cols;
m_originalHeight = image.rows;
float scale = std::min(
static_cast<float>(m_inputWidth) / image.cols,
static_cast<float>(m_inputHeight) / image.rows
);
int new_width = static_cast<int>(image.cols * scale);
int new_height = static_cast<int>(image.rows * scale);
cv::Mat resized;
cv::resize(image, resized, cv::Size(new_width, new_height));
m_letterbox = cv::Mat(m_inputHeight, m_inputWidth, CV_8UC3, cv::Scalar(114, 114, 114));
resized.copyTo(m_letterbox(cv::Rect((m_inputWidth - new_width) / 2,
(m_inputHeight - new_height) / 2,
new_width, new_height)));
m_xOffset = (m_inputWidth - new_width) / 2;
m_yOffset = (m_inputHeight - new_height) / 2;
m_scale = scale;
return m_letterbox;
}
// 推理
std::vector<Detection> detect(cv::Mat& frame) {
auto start = std::chrono::high_resolution_clock::now();
cv::Mat preprocessed = preprocess(frame);
// 转换为ONNX Runtime输入格式
cv::Mat blob;
cv::dnn::blobFromImage(preprocessed, blob, 1.0 / 255.0, cv::Size(), cv::Scalar(), true, false);
// 准备输入张量
std::vector<int64_t> input_shape = { 1, 3, m_inputHeight, m_inputWidth };
size_t input_tensor_size = static_cast<size_t>(1 * 3 * m_inputHeight * m_inputWidth);
std::vector<float> input_tensor_values(blob.begin<float>(), blob.end<float>());
Ort::MemoryInfo memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
std::vector<Ort::Value> input_tensors;
input_tensors.push_back(Ort::Value::CreateTensor<float>(
memory_info, input_tensor_values.data(), input_tensor_size,
input_shape.data(), input_shape.size()
));
// 运行推理 - 动态构建指针数组避免悬空指针问题
std::vector<const char*> input_ptrs;
input_ptrs.reserve(m_inputNames.size());
for (const auto& name : m_inputNamesStr) {
input_ptrs.push_back(name.c_str());
}
std::vector<const char*> output_ptrs;
output_ptrs.reserve(m_outputNamesStr.size());
for (const auto& name : m_outputNamesStr) {
output_ptrs.push_back(name.c_str());
}
std::vector<Ort::Value> output_tensors = m_session->Run(
Ort::RunOptions{ nullptr },
input_ptrs.data(), input_tensors.data(), input_ptrs.size(),
output_ptrs.data(), output_ptrs.size()
);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> inference_time = end - start;
std::cout << "推理时间: " << inference_time.count() << " ms" << std::endl;
// 后处理
std::vector<Detection> detections = postprocess(output_tensors[0]);
return detections;
}
// 后处理 - 只检测人(class_id = 0)
std::vector<Detection> postprocess(Ort::Value& output) {
std::vector<Detection> detections;
float* output_data = output.GetTensorMutableData<float>();
std::vector<int64_t> output_shape = output.GetTensorTypeAndShapeInfo().GetShape();
// YOLOv8输出格式: [1, 84, 8400] 或 [1, 8400, 84]
int num_detections;
int num_classes;
bool is_channels_first;
if (output_shape.size() < 3) {
std::cerr << "输出维度不正确" << std::endl;
return detections;
}
if (output_shape[1] == 84 && output_shape[2] > 1000) {
// 格式 [1, 84, 8400]
num_detections = static_cast<int>(output_shape[2]);
num_classes = static_cast<int>(output_shape[1]) - 4;
is_channels_first = true;
}
else if (output_shape[1] > 1000 && output_shape[2] == 84) {
// 格式 [1, 8400, 84]
num_detections = static_cast<int>(output_shape[1]);
num_classes = static_cast<int>(output_shape[2]) - 4;
is_channels_first = false;
}
else {
std::cerr << "未知的输出格式" << std::endl;
return detections;
}
std::vector<cv::Rect> boxes;
std::vector<float> confidences;
std::vector<int> class_ids;
for (int i = 0; i < num_detections; i++) {
float cx, cy, w, h;
// 获取人的置信度(class_id = 0)
float person_conf;
if (is_channels_first) {
// 格式 [1, 84, 8400] 按通道优先读取
cx = output_data[i + 0 * num_detections];
cy = output_data[i + 1 * num_detections];
w = output_data[i + 2 * num_detections];
h = output_data[i + 3 * num_detections];
// 人的置信度在索引 4(因为 person 的 class_id = 0)
person_conf = output_data[i + 4 * num_detections];
}
else {
// 格式 [1, 8400, 84] 按检测框顺序读取
int offset = i * (num_classes + 4);
cx = output_data[offset + 0];
cy = output_data[offset + 1];
w = output_data[offset + 2];
h = output_data[offset + 3];
// 人的置信度在偏移 4 处
person_conf = output_data[offset + 4];
}
// 只保留人的检测(class_id = 0)
if (person_conf < m_confThreshold) {
continue;
}
// 坐标映射回原图
float x1 = (cx - w / 2 - m_xOffset) / m_scale;
float y1 = (cy - h / 2 - m_yOffset) / m_scale;
float x2 = (cx + w / 2 - m_xOffset) / m_scale;
float y2 = (cy + h / 2 - m_yOffset) / m_scale;
x1 = std::max(0.0f, std::min(x1, static_cast<float>(m_originalWidth)));
y1 = std::max(0.0f, std::min(y1, static_cast<float>(m_originalHeight)));
x2 = std::max(0.0f, std::min(x2, static_cast<float>(m_originalWidth)));
y2 = std::max(0.0f, std::min(y2, static_cast<float>(m_originalHeight)));
cv::Rect box(static_cast<int>(x1), static_cast<int>(y1),
static_cast<int>(x2 - x1), static_cast<int>(y2 - y1));
if (box.width > 0 && box.height > 0) {
boxes.push_back(box);
confidences.push_back(person_conf);
class_ids.push_back(0); // 人的 class_id
}
}
// NMS
if (!boxes.empty()) {
std::vector<int> indices;
cv::dnn::NMSBoxes(boxes, confidences, m_confThreshold, m_nmsThreshold, indices);
for (int idx : indices) {
Detection det;
det.box = boxes[idx];
det.confidence = confidences[idx];
det.class_id = 0; // 人的 class_id
detections.push_back(det);
}
}
return detections;
}
// 绘制检测结果
void drawDetections(cv::Mat& frame, const std::vector<Detection>& detections) {
// 为不同类别设置不同颜色,但这里都是人,用固定颜色
cv::Scalar color = cv::Scalar(0, 255, 0); // 绿色
for (const auto& det : detections) {
cv::rectangle(frame, det.box, color, 2);
std::string label = "person: " + std::to_string(det.confidence).substr(0, 4);
int baseLine;
cv::Size labelSize = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
cv::rectangle(frame,
cv::Point(det.box.x, det.box.y - labelSize.height - baseLine),
cv::Point(det.box.x + labelSize.width, det.box.y),
color, cv::FILLED);
cv::putText(frame, label,
cv::Point(det.box.x, det.box.y - baseLine),
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1);
}
}
private:
// ONNX Runtime相关 (使用智能指针管理)
std::unique_ptr<Ort::Env> m_env;
std::unique_ptr<Ort::SessionOptions> m_sessionOptions;
std::unique_ptr<Ort::Session> m_session;
// 存储字符串内容(确保生命周期)
std::vector<std::string> m_inputNamesStr;
std::vector<std::string> m_outputNamesStr;
// 运行时需要的指针数组
std::vector<const char*> m_inputNames;
std::vector<const char*> m_outputNames;
std::vector<std::vector<int64_t>> m_inputShapes;
std::vector<std::vector<int64_t>> m_outputShapes;
float m_confThreshold;
float m_nmsThreshold;
int m_inputWidth;
int m_inputHeight;
int m_originalWidth;
int m_originalHeight;
int m_xOffset;
int m_yOffset;
float m_scale;
cv::Mat m_letterbox;
};
int onnx_demo() {
std::string model_path = "D:\\yolov8n.onnx";
std::string rtmp_url = "http://demo-videos.qnsdk.com/bbk-H265-50fps.mp4"; // 默认RTMP测试地址
std::cout << "模型路径: " << model_path << std::endl;
std::cout << "RTMP地址: " << rtmp_url << std::endl;
try {
// 初始化YOLO模型
YOLOv8ONNX yolo(model_path, 0.5f, 0.45f);
// 打开RTMP视频流
cv::VideoCapture cap;
// 尝试使用 FFMPEG 后端打开 RTMP 流
std::cout << "正在连接RTMP流..." << std::endl;
cap.open(rtmp_url, cv::CAP_FFMPEG);
if (!cap.isOpened()) {
std::cerr << "错误:无法打开RTMP流!请检查:" << std::endl;
std::cerr << " 1. RTMP地址是否正确" << std::endl;
std::cerr << " 2. 网络连接是否正常" << std::endl;
std::cerr << " 3. 视频流是否在线" << std::endl;
return -1;
}
// 获取视频流信息
double fps = cap.get(cv::CAP_PROP_FPS);
int width = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_WIDTH));
int height = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_HEIGHT));
std::cout << "视频流信息:" << std::endl;
std::cout << " 分辨率: " << width << "x" << height << std::endl;
std::cout << " 帧率: " << fps << " fps" << std::endl;
// 创建窗口
cv::namedWindow("YOLOv8 ONNX RTMP Detection", cv::WINDOW_NORMAL);
cv::Mat frame;
int frame_count = 0;
auto last_time = std::chrono::steady_clock::now();
std::cout << "开始实时检测,按 'q' 或 'ESC' 退出..." << std::endl;
while (true) {
// 读取一帧
cap >> frame;
if (frame.empty()) {
std::cerr << "警告:读取帧失败,尝试重新连接..." << std::endl;
// 尝试重新打开流
cap.release();
cap.open(rtmp_url, cv::CAP_FFMPEG);
if (!cap.isOpened()) {
std::cerr << "重连失败,退出程序" << std::endl;
break;
}
continue;
}
frame_count++;
// 计算实际帧率
auto now = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_time).count();
if (duration >= 1000) {
std::cout << "实时帧率: " << frame_count << " fps" << std::endl;
frame_count = 0;
last_time = now;
}
// 进行目标检测
auto detections = yolo.detect(frame);
// 绘制检测结果
yolo.drawDetections(frame, detections);
// 显示帧率和检测数量
std::string info = "Persons: " + std::to_string(detections.size());
cv::putText(frame, info, cv::Point(10, 30),
cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 255, 0), 2);
// 显示结果
cv::imshow("YOLOv8 ONNX RTMP Detection", frame);
// 按键退出
char key = static_cast<char>(cv::waitKey(1));
if (key == 'q' || key == 'Q' || key == 27) { // 27 = ESC
std::cout << "用户退出" << std::endl;
break;
}
}
// 释放资源
cap.release();
cv::destroyAllWindows();
std::cout << "处理完成" << std::endl;
}
catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
return -1;
}
return 0;
}- 文章2325
- 用户1338
- 访客12519590
粽叶裹着米香,也裹着千年的想念。