Qt多线程:安全更新QLabel

1,572 阅读3分钟

背景——在 Qt 应用程序中渲染视频时,一个常见的需求是将每一帧更新到 QLabel 控件中显示。但是,当渲染线程不在主线程时,直接更新UI可能会导致程序崩溃。

解析视频数据

处理原始字节数组

对于RGBA格式的原始字节数组,可以直接创建QImage:

    QImage image(frame, width, height, width * 4, QImage::Format_RGBA8888);
    image = image.scaled(label->width(), label->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);

处理YUV数据

对于YUV格式的数据,需要先获取Y、U、V分量的buffer和pitch(步长):

    uint8_t *i420_buffer[3];
    int pitch[3];
    i420_buffer[0] = frame->GetBufferY();
    i420_buffer[1] = frame->GetBufferU();
    i420_buffer[2] =frame->GetBufferV();
    pitch[0] = frame->GetPitch0();
    pitch[1] = frame->GetPitch1();
    pitch[2] = frame->GetPitch2();

i420_buffer 数组和 pitch 数组分别包含了YUV帧的数据和每行的字节数(也称为步长)。i420_buffer[0],i420_buffer[1] 和i420_buffer[2]分别包含了Y,U和V的数据。pitch[0],pitch[1]和pitch[2]分别是Y,U和V的步长。然后我们就可以使用这些数据和步长来访问YUV帧的每个像素:

  • 访问第y行第x列的Y值:
    uint8_t Y = i420_buffer[0][y * pitch[0] + x];
  • 访问第y行第x列的U和V值:
    uint8_t U = i420_buffer[1][(y/2)* pitch[1]+(x / 2)];
    uint8_t V = i420_buffer[2][(y/2)* pitch[2]+(x / 2)];

(U和V的数据是以2x2的块存储的,所以需要除以2来获取正确的索引。)

YUV到RGB的转换可以通过以下公式进行:

R = Y + 1.402 * (V - 128)
G = Y - 0.344 * (U - 128) - 0.714 * (V - 128)
B = Y + 1.772 * (U - 128)
for(int y = 0; y < height; ++y){
    for(int x = 0; x < width; ++x){
        uint8_t Y = i420 buffer[0][y * pitch[0] + x];
        uint8_t U = i420 buffer[1][(y / 2) * pitch[1] + (x / 2)];
        uint8_t V = i420 buffer[2][(y / 2) * pitch[2] + (x / 2)];
        int R = Y + 1.402 * (V - 128);
        int G = Y - 0,344 * (U - 128) - 0.714 * (V - 128);
        int B = Y + 1.772 * (U -128);
        image.setPixelColor(x,y,QColor(qBound(0, R, 255), qBound(0, G, 255), qBound(0, B, 255)));
    }
}

但是!!!

使用 setPixelColor 效率太低啦,一不小心还会收到 Qt Creator 的小提醒 😂😂:

The cached device pixel ratio value was stale on window expose. Please file a QTBUG which explains how to reproduce.

我们有更好用的工具:libyuv

    libyuv::I420ToARGB(src_y, src_stride_y, src_u, src_stride_u, src_v, stc_stride_v, argb_buffer, width * 4, width, height);

安全地渲染到QLabel

如果渲染视频的线程是主线程,我们可以在得到 QImage 之后直接设置到 QLabel :

    label->setPixmap(QPixmap::fromImage(image));

但是如果我们是在回调函数中解析帧数据并更新(也就是在非主线程中更新UI组件),那就可能会导致崩溃或其他不稳定行为啦。Qt 的 GUI 组件不是线程安全的,这意味着我们只能在主线程中对它们进行操作。

可以使用信号和槽机制将图像传递回主线程:在非主线程中处理图像并通过信号发送图像数据,在主线程中接收信号并更新QLabel。

// 在非主线程中
void  processImage() {
    // 将数据转换为QImage

    emit imageProcessed(image);
}

// 在主线程中
void renderImage(const QImage& image) {
    // ……

    label->setPixmap(QPixmap::fromImage(image));
}

还有一种更巧妙的写法(参考 Qt Forum 讨论 ):

    QTimer::singleShot(0, this, [this, pixmap](){
        ui->label->setPixmap(pixmap);
        ui->label->setScaledContents(true);
        ui->label->show();
    });

这种方法会在主事件循环的下一个时间点执行更新操作,从而确保在主线程中安全地更新UI。

总结

通过以上方法,我们可以在多线程环境下安全高效地将视频帧渲染到QLabel控件中,避免程序崩溃等问题。