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

📅 2026/7/9 1:10:35
售货柜实战: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::stringurl){// 1. 打开 RTSP 流AVDictionary*optsNULL;av_dict_set(opts,rtsp_transport,tcp,0);av_dict_set(opts,stimeout,10000000,0);// 10秒超时intretavformat_open_input(fmt_ctx_,url.c_str(),NULL,opts);av_dict_free(opts);if(ret0)returnfalse;// 2. 找流信息avformat_find_stream_info(fmt_ctx_,NULL);// 3. 找视频流video_idx_-1;for(inti0;ifmt_ctx_-nb_streams;i){if(fmt_ctx_-streams[i]-codecpar-codec_typeAVMEDIA_TYPE_VIDEO){video_idx_i;break;}}if(video_idx_-1)returnfalse;AVCodecParameters*codecparfmt_ctx_-streams[video_idx_]-codecpar;// 4. 初始化硬件设备RKMPPav_hwdevice_ctx_create(hw_device_ctx_,AV_HWDEVICE_TYPE_DRM_PRIME,NULL,NULL,0);// 5. 打开解码器constAVCodec*decoderavcodec_find_decoder(codecpar-codec_id);codec_ctx_avcodec_alloc_context3(decoder);avcodec_parameters_to_context(codec_ctx_,codecpar);codec_ctx_-hw_device_ctxav_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){intretav_read_frame(fmt_ctx_,pkt_);if(ret0)returnret;// 出错或结束if(pkt_-stream_indexvideo_idx_){avcodec_send_packet(codec_ctx_,pkt_);retavcodec_receive_frame(codec_ctx_,hw_frame_);if(ret0){// 硬件帧转 CPU 内存帧if(hw_frame_-formatAV_PIX_FMT_DRM_PRIME){sw_frame_-formatAV_PIX_FMT_YUV420P;sw_frame_-widthhw_frame_-width;sw_frame_-heighthw_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;};关键点TCP 传输稳定不花屏超时设置10 秒连不上就退出配合外层重连硬件解码RKMPPCPU 占用低硬件帧转软件帧要处理像素数据必须转四、FrameProcessor图像预处理YOLO 输入需要 640×640 的 RGB 图像摄像头拍的是 1920×1080 的 YUV需要裁剪出货架区域只关心商品部分缩放到 640×640YUV 转 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 缩快很多。五、DetectorYOLO 推理这部分用 RKNN 或 OpenCV DNN和 FFmpeg 关系不大简单示意classDetector{public:boolinit(conststd::stringmodel_path){// 加载 RKNN 模型 / ONNX 模型// ...returntrue;}// 输入 RGB 图像输出检测结果std::vectorDetectResultdetect(constuint8_t*rgb_data,intw,inth){// 1. 预处理归一化、NCHW 排布// 2. 推理// 3. 后处理NMS、坐标还原// ...returnresults;}};六、Pipeline完整流水线多线程 队列classDetectionPipeline{public:boolstart(conststd::stringrtsp_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*frameav_frame_alloc();if(reader_.readFrame(frame)0){frame_queue_.push(frame);// 入队}else{av_frame_free(frame);// 读失败尝试重连reconnect();}}}voidprocessLoop(){while(running_){AVFrame*frameframe_queue_.pop();// 出队if(!frame)continue;// 预处理uint8_t*rgbnewuint8_t[640*640*3];processor_.process(frame,rgb);av_frame_free(frame);rgb_queue_.push(rgb);}}voiddetectLoop(){while(running_){uint8_t*rgbrgb_queue_.pop();if(!rgb)continue;autoresultsdetector_.detect(rgb,640,640);delete[]rgb;// 输出结果回调/队列if(callback_)callback_(results);}}private:StreamReader reader_;FrameProcessor processor_;Detector detector_;ThreadSafeQueueAVFrame*frame_queue_;ThreadSafeQueueuint8_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){// 丢最旧的一帧autooldqueue.front();queue.pop();release(old);}七、性能优化记录第一版全软软解码 sws 转换 CPU 推理速度~8fpsCPU90%问题卡实时性差第二版硬解RKMPP 硬解码 sws 转换 CPU 推理速度~15fpsCPU~60%提升解码压力释放了第三版全硬件硬解码 RGA 硬件缩放 RKNN NPU 推理速度30fps实时CPU~15%提升NPU 接管推理CPU 只做控制第四版按需识别待机状态每秒 1 帧检测是否有人开门状态全帧率识别平时 CPU5%功耗低优化事件驱动不是一直满负荷跑八、踩过的坑坑 1硬解出来的帧不能直接访问像素一开始直接读 hw_frame-data[0]数据全错。硬解帧是 DRM_PRIME 格式数据在硬件内存必须av_hwframe_transfer_data转到 CPU 才能访问。坑 2RTSP 断线不重连FFmpeg 本身不会自动重连必须外层包循环。加上超时检测 3 秒重试稳定很多。坑 3队列越积越多延迟变大处理速度跟不上的时候队列越来越长延迟几秒甚至几十秒。加队列长度限制满了丢旧帧。坑 4YUV 裁剪 UV 分量没对齐YUV420P 的 UV 分量宽高是 Y 的一半裁剪的时候 x 和 y 要除以 2不然颜色错位。坑 5sws_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-7RTSP 拉流录制、HLS/DASH 切片SDK 开发篇8-9核心结构体、解码、编码、滤镜、完整转码进阶篇10-11硬件加速、性能优化实战篇12售货柜完整流水线从命令行到 SDK 开发从基础用法到硬件加速和性能优化覆盖了 FFmpeg 80% 的常用场景。FFmpeg 是个很庞大的库没人能全部掌握但搞懂核心的编解码、滤镜、流媒体这几块日常项目基本就够用了。剩下的遇到具体问题再查文档就行。我是黒漂技术佬这个系列就到这里。下个系列见。