Ubuntu / Linux 系统下 USB 工业相机无法被 Cheese 读取的原因分析与工程级解决方案(附Python / C++代码)
一、问题
上一篇博文中,讲解了Ubun/Liunx系统上如何使用茄子(Cheese)相机读取USB网络摄像头的方法,对于部分厂家的USB摄像头任然不能正常读取视频,这是工业相机常见问题。
在 Linux 系统(Ubuntu 20.04)下使用 USB相机进行视频采集时,遇到了如下问题:
Linux 下:
本篇文章将对该问题进行系统性拆解与工程级解决。
二、设备真实情况分析
2.1 安装v4l-utils
安装v4l-utils指令:
sudo apt install v4l-utils
2.2 查看USB摄像头列表
查看USB摄像头列表,执行下面指令:
2.3 查看USB摄像头支持的格式
查看USB摄像头支持的格式,执行下面指令:
v4l2-ctl -d /dev/video0 --list-formats-ext
输出USB摄像头支持的输出格式如下:
从上面的输出参数可以看出,USB设备驱动正常;UVC协议标准;视频流是标准的YUYV422。USB设备是一台完全标准的UVC USB工业相机。
三、问题根因分析:Cheese 为什么会失败?
3.1 Cheese 架构
Cheese 内部技术架构,Cheese 采用的是:
v4l2src → videoconvert → videoscale → autovideosink
3.2 工业红外相机的特殊性
工业相机普遍具有:
3.3 关键验证实验
使用手动 GStreamer pipeline,终端中输入下面指令,如下:
gst-launch-1.0 v4l2src device=/dev/video0 ! \video/x-raw,format=YUY2,width=640,height=512,framerate=30/1 ! \videoconvert ! autovideosink
可以正常显示画面。这说明:GStreamer 本身完全支持该相机。
3.4 cheese失败原因
真正失败的是 Cheese 的自动协商机制,Cheese 不提供:format 指定、分辨率锁定和framerate 约束,caps negotiation 失败 → pipeline 构建失败 → 直接报错。
四、为什么 ffplay / VLC 却能成功?
ffmpeg 采用的解码链路:
v4l2 → libavdevice → libavcodec → swscale
ffmpeg 的核心优势:
FFmpeg 的 swscale 具有极强的 stride 与内存对齐容错能力,可以自动修复各种怪异分辨率问题。这也是:几乎所有工业视觉系统底层最终都会使用 FFmpeg 的根本原因。
五、示例读取USB工业相机
5.1 安装ffmpeg
安装ffmpeg在终端执行指令:
5.2 方案1:ffplay 直接播放
FFmpeg 多媒体框架里的 ffplay 播放器,执行下面指令,指定设备输出格式,输出分辨率和设备ID,如下:
ffplay -f v4l2 -input_format yuyv422 -video_size 640x512 -i /dev/video0
执行上面指令后,打开的实时视频画面如下:
5.3 方案2:mpv 播放(画质更好)
mpv 播放,执行下面指令,指定设备输出格式,输出分辨率和设备ID,如下:
mpv av://v4l2:/dev/video0 --demuxer-lavf-format=v4l2 --demuxer-lavf-o=input_format=yuyv422,video_size=640x512
补:此方法我自己测试发现,视频成像画面有一点延迟。
5.4 代码读取USB工业相机
5.4.1 读取流程
使用代码读取USB工业相机的流程如下:
USB 红外相机↓v4l2↓FFmpeg↓pipe↓Python/ C++↓OpenCV
5.4.2 Python代码
5.4.2.1 FFmpeg方法
完整的Python示例代码如下:
mport subprocess # 用于调用系统命令,启动FFmpeg进程 import numpy as np import cv2 # 定义相机分辨率w, h = 640, 512# 构造FFmpeg命令参数cmd = [ 'ffmpeg', # 要调用的系统命令 '-f', 'v4l2', # 输入格式:v4l2(Linux视频设备标准协议,必选) '-input_format', 'yuyv422', # 相机原始输入格式:yuyv422 '-video_size', f'{w}x{h}', # 采集分辨率:和上面定义的w/h一致 '-i', '/dev/video0', # 输入设备:高德相机的Linux节点(根据实际情况修改) '-f', 'rawvideo', # 输出格式:原始视频流(无压缩,方便Python解析) '-pix_fmt', 'bgr24', # 输出像素格式:bgr24(OpenCV默认支持的格式,必选) '-' # 输出位置:标准输出(stdout),将处理后的流传给Python]# 执行这行代码后,FFmpeg 就会开始从/dev/video0读取相机流,解码并转成 BGR24 格式,通过管道传给 Python。# 启动 FFmpeg 子进程,建立管道通信 。# subprocess.Popen:启动一个独立的 FFmpeg 子进程,不会阻塞 Python 主进程,实现实时流读取# stdout=subprocess.PIPE:将 FFmpeg 的标准输出(处理后的 BGR 流)和 Python 进程的管道(pipe) 绑定,Python 可以通过pipe.stdout读取 FFmpeg 的输出数据;# bufsize=10**8:设置超大缓冲区(100MB),避免因红外相机流传输速度快导致的数据丢失 / 卡顿,保证实时性。pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=10**8)# 循环读取帧并处理显示(主循环)while True: # 1. 从管道读取一帧BGR24格式的原始数据 raw = pipe.stdout.read(w * h * 3) # 校验:一帧BGR24数据的字节数必须是 宽×高×3(每个像素3个字节:B/G/R) if len(raw) != w * h * 3: print("读取失败") break # 2. 将二进制原始流转成OpenCV可处理的numpy数组 frame = np.frombuffer(raw, dtype=np.uint8).reshape((h, w, 3)) # 3. 将彩色BGR帧转成灰度图(红外相机成像通常看灰度图更清晰) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 4. 显示灰度图,窗口名称为"IR"(Infrared红外) cv2.imshow("IR", gray) # 5. 按键退出:按下ESC键(ASCII码27),终止循环 if cv2.waitKey(1) == 27: break
5.4.2.2 GStreamer方法
前面3.3小节中已经验证,也可以用GStreamer抓取视频流,但常规直接使用pip install opencv-python安装的OpenCV包中,默认不启用 GStreamer,因为:GStreamer 依赖复杂,与 conda 虚拟环境冲突多,打包困难, 所以工业级视频采集功能被阉割,如下:
直接使用会提示报错:❌ GStreamer 管线打开失败。如下:
想要开启GStreamer,必须自己去OpenCV官网下载源码进行编译 OpenCV + GStreamer才行,学者自行编译。
使用GStreamer方法的完整示例代码如下:
import cv2import timeprint("等待相机初始化...")gst_pipeline = ( "v4l2src device=/dev/video0 ! " "video/x-raw,format=YUY2,width=640,height=512,framerate=30/1 ! " "videoconvert ! appsink")cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)if not cap.isOpened(): print("❌ GStreamer 管线打开失败") exit(1)print("✅ 相机已成功打开")while True: ret, frame = cap.read() if not ret: print("读取失败") continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) cv2.imshow("IR", gray) if cv2.waitKey(1) == 27: break
5.4.3 C++代码
完整的C++示例代码如下:
#include<iostream>#include<cstdio>#include<vector>#include<opencv2/opencv.hpp>using namespace std;// ------------------- 参数配置 -------------------// 相机分辨率static const int WIDTH = 640;static const int HEIGHT = 512;// 每帧字节数:BGR24 = 3字节 / 像素static const int FRAME_SIZE = WIDTH * HEIGHT * 3;// ------------------------------------------------intmain(){ cout << "启动红外相机采集..." << endl; /* * 构造 FFmpeg 采集命令(与 Python 完全一致) * * -f v4l2 : 使用 Linux V4L2 采集接口 * -input_format yuyv422 : 指定相机原始输出格式(关键参数) * -video_size 640x512 : 指定采集分辨率 * -i /dev/video0 : 采集设备节点 * -f rawvideo : 输出原始视频流(无压缩) * -pix_fmt bgr24 : 转换为 OpenCV 友好的 BGR24 * - : 输出到 stdout(通过管道传给 C++) */ string cmd = "ffmpeg -loglevel quiet " "-f v4l2 " "-input_format yuyv422 " "-video_size 640x512 " "-i /dev/video0 " "-f rawvideo " "-pix_fmt bgr24 " "-"; /* * popen(): * 启动 FFmpeg 进程,并打开其 stdout 管道,供 C++ 实时读取视频流 */ FILE* pipe = popen(cmd.c_str(), "r"); if (!pipe) { cerr << "❌ FFmpeg 启动失败!" << endl; return -1; } cout << "✅ FFmpeg 采集管道已建立,开始读取视频流..." << endl; // 用于存储一帧原始 BGR 数据 vector<unsignedchar> buffer(FRAME_SIZE); // OpenCV Mat,直接包装 raw buffer cv::Mat frame(HEIGHT, WIDTH, CV_8UC3); cv::Mat gray; while (true) { /* * 从 FFmpeg 管道中读取一帧 * 每次必须完整读取 WIDTH * HEIGHT * 3 字节 */ size_t bytes = fread(buffer.data(), 1, FRAME_SIZE, pipe); if (bytes != FRAME_SIZE) { cerr << "❌ 读取失败,数据长度异常:" << bytes << endl; break; } // 将 buffer 映射为 OpenCV Mat memcpy(frame.data, buffer.data(), FRAME_SIZE); // BGR → 灰度(红外图像通常显示灰度) cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); // 显示图像 cv::imshow("IR", gray); // ESC 退出 if (cv::waitKey(1) == 27) break; } // 关闭管道 pclose(pipe); return 0;}
5.4.4 视频输出
使用上面代码实时读取视频流输出如下:
六、总结
本文分析了Linux系统下USB工业相机无法正常读取视频的问题。通过v4l-utils工具检测发现设备驱动正常,视频流为标准YUYV422格式。Cheese相机因自动协商机制失败而报错,而ffplay/VLC通过FFmpeg的强容错能力可稳定播放。文章提供了两种解决方案:使用ffplay或mpv直接播放,以及通过Python / C++代码结合FFmpeg管道读取视频流。关键发现是工业相机的非标准分辨率(640×512)和YUYV灰度流导致自动协商异常,而FFmpeg的swscale组件能有效处理这些特殊格式。