news 2026/7/9 1:10:28

售货柜实战:IPC 拉流 → 抽帧 → YOLO 识别完整流水线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
售货柜实战:IPC 拉流 → 抽帧 → YOLO 识别完整流水线

售货柜实战:IPC 拉流 → 抽帧 → YOLO 识别完整流水线

最后一篇实战,把前面 11 篇的知识串起来。用 FFmpeg SDK 实现一个完整的售货柜视觉识别流水线:RTSP 拉流 → 硬件解码 → 帧预处理 → YOLO 商品识别 → 结果输出。从架构设计到核心代码,再到性能优化,完整讲一遍。

大家好,我是黒漂技术佬。

这是 FFmpeg 系列的最后一篇,也是实战篇——把前面讲的 RTSP 拉流、硬件解码、像素格式转换、性能优化,全部整合到售货柜的商品识别流水线里。

整个流程:IPC 摄像头 RTSP 流 → FFmpeg 硬解码 → YUV 转 RGB → YOLOv8 推理 → 输出识别结果。


一、整体架构

流水线设计

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ RTSP 拉流 │ → │ 硬件解码 │ → │ 图像预处理 │ → │ YOLO 推理 │ │ (FFmpeg) │ │ (RKMPP) │ │ (缩放/裁剪) │ │ (RKNN) │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ↓ 识别结果输出

四个阶段,用队列串起来,流水线并行处理。

为什么用 FFmpeg 不用 OpenCV VideoCapture?

很多人做视频处理直接用 OpenCV 的 VideoCapture,简单是简单,但问题不少:

  • 不支持硬件解码,CPU 占用高
  • RTSP 不稳定,断线不会自动重连
  • 缓冲不可控,延迟高
  • 滤镜、格式转换能力弱

FFmpeg 虽然代码多一点,但可控性强太多,生产环境必须用 FFmpeg。


二、核心模块设计

模块拆分

模块职责
StreamReaderRTSP 拉流 + 硬解码,输出 YUV 帧
FrameProcessor图像预处理:裁剪、缩放、格式转换
DetectorYOLO 推理,输出检测框
Pipeline串起整个流水线,管理队列和线程

线程模型

三个线程,两个队列:

拉流解码线程 → [帧队列] → 预处理线程 → [推理队列] → 推理线程

流水线并行,三路同时跑,吞吐量最高。


三、StreamReader:拉流 + 硬解码

初始化代码

classStreamReader{public:boolopen(conststd::string&url){// 1. 打开 RTSP 流AVDictionary*opts=NULL;av_dict_set(&opts,"rtsp_transport","tcp",0);av_dict_set(&opts,"stimeout","10000000",0);// 10秒超时intret=avformat_open_input(&fmt_ctx_,url.c_str(),NULL,&opts);av_dict_free(&opts);if(ret<0)returnfalse;// 2. 找流信息avformat_find_stream_info(fmt_ctx_,NULL);// 3. 找视频流video_idx_=-1;for(inti=0;i<fmt_ctx_->nb_streams;i++){if(fmt_ctx_->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO){video_idx_=i;break;}}if(video_idx_==-1)returnfalse;AVCodecParameters*codecpar=fmt_ctx_->streams[video_idx_]->codecpar;// 4. 初始化硬件设备(RKMPP)av_hwdevice_ctx_create(&hw_device_ctx_,AV_HWDEVICE_TYPE_DRM_PRIME,NULL,NULL,0);// 5. 打开解码器constAVCodec*decoder=avcodec_find_decoder(codecpar->codec_id);codec_ctx_=avcodec_alloc_context3(decoder);avcodec_parameters_to_context(codec_ctx_,codecpar);codec_ctx_->hw_device_ctx=av_buffer_ref(hw_device_ctx_);avcodec_open2(codec_ctx_,decoder,NULL);pkt_=av_packet_alloc();hw_frame_=av_frame_alloc();sw_frame_=av_frame_alloc();returntrue;}// 读取一帧,返回 0 成功,<0 失败/结束intreadFrame(AVFrame*out_frame){while(true){intret=av_read_frame(fmt_ctx_,pkt_);if(ret<0)returnret;// 出错或结束if(pkt_->stream_index==video_idx_){avcodec_send_packet(codec_ctx_,pkt_);ret=avcodec_receive_frame(codec_ctx_,hw_frame_);if(ret==0){// 硬件帧转 CPU 内存帧if(hw_frame_->format==AV_PIX_FMT_DRM_PRIME){sw_frame_->format=AV_PIX_FMT_YUV420P;sw_frame_->width=hw_frame_->width;sw_frame_->height=hw_frame_->height;av_frame_get_buffer(sw_frame_,0);av_hwframe_transfer_data(sw_frame_,hw_frame_,0);av_frame_ref(out_frame,sw_frame_);}else{av_frame_ref(out_frame,hw_frame_);}av_packet_unref(pkt_);return0;}}av_packet_unref(pkt_);}}voidclose(){// 释放资源...}private:AVFormatContext*fmt_ctx_=nullptr;AVCodecContext*codec_ctx_=nullptr;AVBufferRef*hw_device_ctx_=nullptr;AVPacket*pkt_=nullptr;AVFrame*hw_frame_=nullptr;AVFrame*sw_frame_=nullptr;intvideo_idx_=-1;};

关键点

  1. TCP 传输:稳定,不花屏
  2. 超时设置:10 秒连不上就退出,配合外层重连
  3. 硬件解码:RKMPP,CPU 占用低
  4. 硬件帧转软件帧:要处理像素数据必须转

四、FrameProcessor:图像预处理

YOLO 输入需要 640×640 的 RGB 图像,摄像头拍的是 1920×1080 的 YUV,需要:

  1. 裁剪出货架区域(只关心商品部分)
  2. 缩放到 640×640
  3. YUV 转 RGB

用 SwsContext 一步完成

classFrameProcessor{public:// 初始化:源 1920x1080 YUV420P,裁剪区域 (x,y,w,h),输出 640x640 RGBboolinit(intsrc_w,intsrc_h,AVPixelFormat src_fmt,intcrop_x,intcrop_y,intcrop_w,intcrop_h,intdst_w,intdst_h,AVPixelFormat dst_fmt){src_w_=src_w;src_h_=src_h;crop_x_=crop_x;crop_y_=crop_y;crop_w_=crop_w;crop_h_=crop_h;dst_w_=dst_w;dst_h_=dst_h;// 先裁剪再缩放,用 sws 处理// 注意:swscale 不直接支持裁剪,需要先手动裁// 简单做法:用 crop 滤镜,或者手动处理 data 指针// 这里用滤镜方案更优雅initFilter(crop_w,crop_h,src_fmt,"crop=...");sws_ctx_=sws_getContext(crop_w,crop_h,src_fmt,dst_w,dst_h,dst_fmt,SWS_BILINEAR,NULL,NULL,NULL);// 分配输出 bufferav_image_alloc(dst_data_,dst_linesize_,dst_w,dst_h,dst_fmt,1);returnsws_ctx_!=NULL;}// 处理一帧intprocess(AVFrame*in_frame,uint8_t*out_rgb){// 1. 裁剪:调整 data 指针和 linesizeuint8_t*cropped_data[4];intcropped_linesize[4];cropped_data[0]=in_frame->data[0]+crop_y_*in_frame->linesize[0]+crop_x_;cropped_linesize[0]=in_frame->linesize[0];// UV 分量也要对应裁剪(YUV420P 的 UV 是 1/2 大小)cropped_data[1]=in_frame->data[1]+(crop_y_/2)*in_frame->linesize[1]+(crop_x_/2);cropped_linesize[1]=in_frame->linesize[1];cropped_data[2]=in_frame->data[2]+(crop_y_/2)*in_frame->linesize[2]+(crop_x_/2);cropped_linesize[2]=in_frame->linesize[2];// 2. 缩放 + 格式转换sws_scale(sws_ctx_,cropped_data,cropped_linesize,0,crop_h_,dst_data_,dst_linesize_);// 3. 拷贝到输出(连续内存,方便推理)memcpy(out_rgb,dst_data_[0],dst_w_*dst_h_*3);return0;}private:structSwsContext*sws_ctx_=nullptr;uint8_t*dst_data_[4];intdst_linesize_[4];intsrc_w_,src_h_;intcrop_x_,crop_y_,crop_w_,crop_h_;intdst_w_,dst_h_;};

为什么先裁剪再缩放?

货架只占画面中间一部分,裁掉多余区域再缩放,处理的像素少,速度快。

1920×1080 → 裁 800×600 → 缩 640×640,比直接从 1920×1080 缩快很多。


五、Detector:YOLO 推理

这部分用 RKNN 或 OpenCV DNN,和 FFmpeg 关系不大,简单示意:

classDetector{public:boolinit(conststd::string&model_path){// 加载 RKNN 模型 / ONNX 模型// ...returntrue;}// 输入 RGB 图像,输出检测结果std::vector<DetectResult>detect(constuint8_t*rgb_data,intw,inth){// 1. 预处理:归一化、NCHW 排布// 2. 推理// 3. 后处理:NMS、坐标还原// ...returnresults;}};

六、Pipeline:完整流水线

多线程 + 队列

classDetectionPipeline{public:boolstart(conststd::string&rtsp_url){// 初始化各模块reader_.open(rtsp_url);processor_.init(1920,1080,AV_PIX_FMT_YUV420P,560,200,800,600,// 裁剪区域640,640,AV_PIX_FMT_RGB24);detector_.init("yolov8n.rknn");// 启动线程running_=true;read_thread_=std::thread(&DetectionPipeline::readLoop,this);process_thread_=std::thread(&DetectionPipeline::processLoop,this);detect_thread_=std::thread(&DetectionPipeline::detectLoop,this);returntrue;}voidreadLoop(){while(running_){AVFrame*frame=av_frame_alloc();if(reader_.readFrame(frame)==0){frame_queue_.push(frame);// 入队}else{av_frame_free(&frame);// 读失败,尝试重连reconnect();}}}voidprocessLoop(){while(running_){AVFrame*frame=frame_queue_.pop();// 出队if(!frame)continue;// 预处理uint8_t*rgb=newuint8_t[640*640*3];processor_.process(frame,rgb);av_frame_free(&frame);rgb_queue_.push(rgb);}}voiddetectLoop(){while(running_){uint8_t*rgb=rgb_queue_.pop();if(!rgb)continue;autoresults=detector_.detect(rgb,640,640);delete[]rgb;// 输出结果(回调/队列)if(callback_)callback_(results);}}private:StreamReader reader_;FrameProcessor processor_;Detector detector_;ThreadSafeQueue<AVFrame*>frame_queue_;ThreadSafeQueue<uint8_t*>rgb_queue_;std::thread read_thread_;std::thread process_thread_;std::thread detect_thread_;boolrunning_=false;// 断线重连voidreconnect(){reader_.close();std::this_thread::sleep_for(std::chrono::seconds(3));reader_.open(rtsp_url_);}};

队列长度控制

队列不能无限长,处理不过来的时候丢旧帧,保证延迟不会累积。

if(queue.size()>MAX_QUEUE_SIZE){// 丢最旧的一帧autoold=queue.front();queue.pop();release(old);}

七、性能优化记录

第一版(全软)

  • 软解码 + sws 转换 + CPU 推理
  • 速度:~8fps
  • CPU:90%+
  • 问题:卡,实时性差

第二版(硬解)

  • RKMPP 硬解码 + sws 转换 + CPU 推理
  • 速度:~15fps
  • CPU:~60%
  • 提升:解码压力释放了

第三版(全硬件)

  • 硬解码 + RGA 硬件缩放 + RKNN NPU 推理
  • 速度:30fps+(实时)
  • CPU:~15%
  • 提升:NPU 接管推理,CPU 只做控制

第四版(按需识别)

  • 待机状态:每秒 1 帧,检测是否有人
  • 开门状态:全帧率识别
  • 平时 CPU:<5%,功耗低
  • 优化:事件驱动,不是一直满负荷跑

八、踩过的坑

坑 1:硬解出来的帧不能直接访问像素

一开始直接读 hw_frame->data[0],数据全错。硬解帧是 DRM_PRIME 格式,数据在硬件内存,必须av_hwframe_transfer_data转到 CPU 才能访问。

坑 2:RTSP 断线不重连

FFmpeg 本身不会自动重连,必须外层包循环。加上超时检测 + 3 秒重试,稳定很多。

坑 3:队列越积越多延迟变大

处理速度跟不上的时候,队列越来越长,延迟几秒甚至几十秒。加队列长度限制,满了丢旧帧。

坑 4:YUV 裁剪 UV 分量没对齐

YUV420P 的 UV 分量宽高是 Y 的一半,裁剪的时候 x 和 y 要除以 2,不然颜色错位。

坑 5:sws_getContext 反复创建

一开始每帧都创建 SwsContext,巨慢。创建一次复用,速度差十倍。


九、完整项目文件结构

cabinet_vision/ ├── src/ │ ├── stream_reader.h/cpp # FFmpeg 拉流解码 │ ├── frame_processor.h/cpp # 图像预处理 │ ├── detector.h/cpp # YOLO 推理 │ ├── pipeline.h/cpp # 流水线调度 │ └── main.cpp ├── model/ │ └── yolov8n.rknn # 模型文件 └── CMakeLists.txt

依赖:FFmpeg(带 RKMPP)、RKNN、pthread。


十、系列总结

12 篇 FFmpeg 系列到此结束,回顾一下我们覆盖的内容:

基础篇(1-2):命令行入门、转码参数详解
处理篇(3-5):视频滤镜、音频处理、抽帧截图
流媒体篇(6-7):RTSP 拉流录制、HLS/DASH 切片
SDK 开发篇(8-9):核心结构体、解码、编码、滤镜、完整转码
进阶篇(10-11):硬件加速、性能优化
实战篇(12):售货柜完整流水线

从命令行到 SDK 开发,从基础用法到硬件加速和性能优化,覆盖了 FFmpeg 80% 的常用场景。

FFmpeg 是个很庞大的库,没人能全部掌握,但搞懂核心的编解码、滤镜、流媒体这几块,日常项目基本就够用了。剩下的遇到具体问题再查文档就行。

我是黒漂技术佬,这个系列就到这里。下个系列见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 1:10:23

m4a格式录音转文字用什么工具高效?实测3类方案对比

日常办公里很多人都会遇到这样的场景&#xff0c;手机录完的m4a格式会议录音&#xff0c;要花一两个小时反复拖动进度条手动敲字整理&#xff0c;中途还容易漏记关键发言&#xff0c;导出的文件还经常出现格式不兼容的问题。不少人试过几款工具后发现&#xff0c;要么识别准确率…

作者头像 李华
网站建设 2026/7/9 1:08:09

Codex和Claude Code哪个好?2026年AI编程工具对比与选型建议

“Codex和Claude Code哪个好”是个高频问题&#xff0c;但答案很少只有一个。一个前端团队、一个后端中台团队、一个外包交付团队&#xff0c;对好用的定义并不一样。有人要的是快速改Bug&#xff0c;有人要的是读懂旧项目&#xff0c;有人关心PR说明和测试记录&#xff0c;有人…

作者头像 李华
网站建设 2026/7/9 1:08:00

AI算力基建动态简报(2026.07.08)

本简报由 英辰朗迪GEO整理 第1条&#xff1a;英伟达否认Kyber NVL144架构延期&#xff0c;产品路线图保持不变 核心信息&#xff1a; 2026年7月6日&#xff0c;针对供应链研究机构SemiAnalysis关于"下一代AI服务器架构Kyber NVL144面临重大延期"的报道&#xff0c;…

作者头像 李华
网站建设 2026/7/9 1:07:01

USB-A 转 Type-C 转接板 DIY:单面 PCB 设计,10分钟完成焊接与功能验证

USB-A 转 Type-C 转接板 DIY&#xff1a;单面 PCB 设计与快速验证指南1. 理解 USB-C 接口的核心特性USB-C 作为现代电子设备的通用接口标准&#xff0c;其24针脚设计相比传统USB-A接口有着显著的技术革新。首先&#xff0c;USB-C 接口采用完全对称的可逆插拔设计&#xff0c;彻…

作者头像 李华
网站建设 2026/7/9 1:06:01

RMQ/稀疏表加速静态区间查询

引言静态区间查询问题的定义与应用场景&#xff08;如数组区间最小值、最大值等&#xff09;。传统暴力查询的局限性&#xff08;时间复杂度高&#xff09;。引入稀疏表&#xff08;Sparse Table&#xff09;作为高效静态RMQ的解决方案。稀疏表的基本原理稀疏表的核心思想&…

作者头像 李华