Qt捕获QVideoFrame并在QVideoWidget显示的异常问题排查
问题1:Format_YUYV格式QVideoFrame转QImage后base64为空
问题出在QImage不直接支持YUYV格式,直接转换会得到无效的QImage,自然导出的base64是空的。手动将YUYV像素数据转成RGB格式即可解决:
- 先把QVideoFrame映射到内存,获取像素指针:
bool QMyAbstractVideoSurface::present(const QVideoFrame &frame) { if (frame.format() != QVideoFrame::Format_YUYV) { return QAbstractVideoSurface::present(frame); } if (!frame.map(QAbstractVideoBuffer::ReadOnly)) { return false; } int width = frame.width(); int height = frame.height(); int bytesPerLine = frame.bytesPerLine(); uchar *yuyvData = frame.bits(); // 创建RGB888格式的QImage,逐个像素转换YUYV到RGB QImage rgbImage(width, height, QImage::Format_RGB888); uchar *rgbData = rgbImage.bits(); // 用BT.601标准公式转换,颜色偏色的话换下面的BT.709公式 for (int y = 0; y < height; ++y) { uchar *yuyvRow = yuyvData + y * bytesPerLine; uchar *rgbRow = rgbData + y * rgbImage.bytesPerLine(); for (int x = 0; x < width; x += 2) { // YUYV结构为Y0 U Y1 V,两个像素共享一组UV分量 uint8_t Y0 = yuyvRow[0]; uint8_t U = yuyvRow[1]; uint8_t Y1 = yuyvRow[2]; uint8_t V = yuyvRow[3]; // 转换第一个像素 int R = Y0 + 1.370705 * (V - 128); int G = Y0 - 0.698001 * (V - 128) - 0.337633 * (U - 128); int B = Y0 + 1.732446 * (U - 128); // 将数值限制在0-255范围内 R = qBound(0, R, 255); G = qBound(0, G, 255); B = qBound(0, B, 255); rgbRow[0] = R; rgbRow[1] = G; rgbRow[2] = B; // 转换第二个像素 R = Y1 + 1.370705 * (V - 128); G = Y1 - 0.698001 * (V - 128) - 0.337633 * (U - 128); B = Y1 + 1.732446 * (U - 128); R = qBound(0, R, 255); G = qBound(0, G, 255); B = qBound(0, B, 255); rgbRow[3] = R; rgbRow[4] = G; rgbRow[5] = B; yuyvRow += 4; rgbRow += 6; } } frame.unmap(); // 现在保存PNG并转base64就正常了 QByteArray pngData; QBuffer buffer(&pngData); buffer.open(QIODevice::WriteOnly); if (rgbImage.save(&buffer, "PNG")) { QString base64 = pngData.toBase64(); // 在这里使用base64进行后续操作 } // 触发显示逻辑(见问题2) emit frameCaptured(rgbImage); return true; }
若转换后颜色偏色,替换为BT.709公式(适配高清视频):
int R = Y0 + 1.5748 * (V - 128); int G = Y0 - 0.1873 * (U - 128) - 0.4681 * (V - 128); int B = Y0 + 1.8556 * (U - 128);
问题2:将捕获的帧显示到QVideoWidget并添加至网格布局
你当前把取景器设为自定义surface,QVideoWidget自身的渲染管道会被覆盖,提供两个可行方案:
方案1:原生预览+帧捕获(推荐,性能最优)
不要将QCamera的取景器设置为自定义surface,改用QMediaCaptureSession同时连接QVideoWidget和自定义QVideoSink,既保留QVideoWidget的硬件加速预览,又能捕获帧:
// 初始化代码 QMediaCaptureSession session; QCamera camera; QVideoWidget videoWidget; QMyVideoSink *videoSink = new QMyVideoSink(this); // 继承QVideoSink session.setCamera(&camera); session.setVideoOutput(&videoWidget); // 让QVideoWidget显示原生预览 session.setVideoSink(videoSink); // 额外绑定sink用于帧捕获 // 将videoWidget添加到网格布局 ui->gridLayout->addWidget(&videoWidget, 0, 0); // 替换为你的实际布局位置 camera.start();
自定义QVideoSink重写setVideoFrame方法:
class QMyVideoSink : public QVideoSink { Q_OBJECT protected: void setVideoFrame(const QVideoFrame &frame) override { QVideoSink::setVideoFrame(frame); // 保证QVideoWidget正常渲染 // 在这里处理帧转换和base64逻辑,和上面present方法中的逻辑一致 if (frame.format() == QVideoFrame::Format_YUYV) { // 转换为QImage并处理base64 } } };
方案2:自定义surface手动绘制到QVideoWidget
若必须使用自定义surface,可在surface中发送信号将转换后的QImage传到UI线程,再手动绘制到QVideoWidget:
- 在自定义surface中添加信号:
class QMyAbstractVideoSurface : public QAbstractVideoSurface { Q_OBJECT signals: void frameReady(const QImage &image); // 其他代码... };
- 在present方法中发送信号:
// 转换得到rgbImage后 emit frameReady(rgbImage);
- 在UI类中连接信号并绘制:
// 初始化时绑定信号槽 connect(mySurface, &QMyAbstractVideoSurface::frameReady, this, [this](const QImage &image) { QPainter painter(&videoWidget); painter.drawImage(videoWidget.rect(), image.scaled(videoWidget.size(), Qt::KeepAspectRatio)); painter.end(); }); // 将videoWidget添加到网格布局 ui->gridLayout->addWidget(&videoWidget, 0, 0);
此方案为软件绘制,性能不如方案1的硬件加速预览,仅在特殊场景使用。
额外提醒
present方法运行在视频线程,更新UI必须切换到UI线程,信号槽是最安全的方式,禁止直接在present中操作UI。- 先打印日志确认帧格式确实为Format_YUYV,部分摄像头可能输出NV12等其他YUV格式。
- 转换后用
rgbImage.isNull()检查QImage有效性,无效则排查转换逻辑。
内容的提问来源于stack exchange,提问作者zabitstack
相关产品推荐
相关产品推荐

