QT多线程下如何修改QImage

3
我正在编写一款软件,需要对许多(可能很大的)图像进行许多图像操作/合成。多线程可以显著提高速度,但QT不允许同时在同一图像上使用多个QPainter。因此,我必须在每个线程中对复制品进行图像操作/合成,然后再将其复制回来,这会大大降低性能(根据用例而定)。
所以我想到了一个看起来可行但感觉极其hacky的方法。我获取目标图像数据(QImage::bits)指针,并将其提供给工作线程。在工作线程中,我从所提供的指针重新创建一个新的QImage。这意味着没有复制和blitting。只要确保每个像素/块在一个线程中被处理且不分离目标图像,它似乎运行得很好。
我的问题是:这样做是否安全,并且是否存在其他可能出现的问题?
示例代码:
QImage source = ...;
QImage target = ...;
QPainter::CompositionMode compositionMode = QPainter::CompositionMode_SourceOver;

// calculate tiles
QList<QRect> tiles;
for(int y = rect.top(); y < rect.top() + rect.height(); y += tileSize){
    for(int x = rect.left(); x < rect.left() + rect.width(); x += tileSize){
        QRect tile(
                    x, y,
                    x + tileSize > rect.left() + rect.width() ? rect.left() + rect.width() - x : tileSize,
                    y + tileSize > rect.top() + rect.height() ? rect.top() + rect.height() - y : tileSize
        );
        tiles.append(tile);
    }
}

// Get target pixel pointer and do threaded operation on each tile
uchar *targetPix = target.bits();
auto target_size = target.size();
auto targetFormat = target.format();
QList<int> lol = QtConcurrent::blockingMapped(tiles, [&target_size, &targetFormat, &source, targetPix, &compositionMode](const QRect &r){
    QImage tile_target(targetPix, target_size.width(), target_size.height(), targetFormat);
    QPainter p(&tile_target);
    p.setCompositionMode(compositionMode);
    // do you image operations here. For now we just do a simple draw
    p.drawImage(r.topLeft(), source, r);
    return 1; // In reallity this would return sensible data ;)
});

(顺便说一句,这个例子在我的测试中将速度提高了约4.6倍。当然,取决于操作和系统。)

1个回答

2

简短回答

这确实很棘手(但当您想要最先进的性能时,通常需要),但对于某些操作,应该可以(使其)线程安全。当然,这取决于您在 tile_target 上执行的操作。

由您决定是否甚至不 访问 分配瓷砖之外的位(即 r 矩形之外的 tile_target 部分)。

一些考虑因素

确保只 访问 分配瓷砖的位

由于 tile_target 引用整个图像,因此您需要确保不访问此目标瓷砖之外的位。一些问题情况:

  • 反锯齿:在执行此类操作时,您可能会更改相邻位的位
  • 过滤:操作(例如模糊)通常会读取相邻位以计算“平均”值。如果另一个线程同时写入相同的位,则读取也不安全。

可能的解决方案?:允许访问和/或写入瓷砖相邻位的一种选项是将图像分成条纹,并在两个步骤中处理图像:

  • 同时处理偶数条纹
  • 然后同时处理奇数条纹

此过程允许您修改相邻条纹的一半(对于反锯齿很有用)或者(如果没有人写入)访问下一个和/或上一个条纹的所有位(用于过滤目的很有用)。

如果创建足够的条纹使所有 CPU 保持繁忙(即通常是 CPU 支持线程数量的两倍),则这不应显着降低效率。

我应该担心分离吗?

这不应该是您当前实现的问题。 QImage::bits 已将图像 (target) 从任何其他可能存在的副本中分离(如果需要)。由于您通过阻止调用线程执行并发操作,因此原始图像 (target) 将至少存在于 tile_target 图像存在的时间内。

更安全的方法

  • 使用专用于多线程图像处理的库,或者至少允许引用子图像。

  • 将瓷砖的副本(参见 QImage::copy)传递给每个线程,并将结果写回原始图像(使用互斥锁或通过在调用线程中执行此操作)。根据图像操作的计算不敏感性,此额外副本可能是可以忽略的。对于 OP,这似乎不是可行的选项。

请注意,安全的方法在抗锯齿或过滤的情况下可能会生成(稍微)不同于单线程结果的结果。通过尽可能大地使用瓷砖(即创建不超过CPU支持的线程数的瓷砖),可以将这些伪影最小化。

使用GPU

使用GPU进行图像处理通常比使用CPU快得多,特别是对于诸如过滤之类的任务。但是, QImage 不支持此功能。

如果您有商业可用的图像处理库的建议,支持多线程和潜在的SIMD支持,请告诉我。我研究了很多,但它们都有问题,其中一个问题是QT首选的Pixelformat通常不受支持(ARGB vs RGBA),这意味着需要进行另一次复制/转换。(Pixman看起来不错,但没有任何文档) - SleepProgger
@SleepProgger 我不确定你所说的复制是什么意思,因为我认为你正在尝试不复制任何内容,而是在所有工作线程中访问原始图像的数据。 - m7913d
关于一个库,我不知道。我没有高性能“Photoshop”操作的经验。大多数情况下,我使用单线程版本就可以了。OpenCV是一个“图像”库,但更多的是关于图像识别。 - m7913d
我不确定我完全理解你的提议。你是在做两次相同的操作吗?一次是针对接缝区域(在单个线程中)?另一次是针对每个瓷砖(在多个线程中)?然而,最重要的是确保你不会同时访问任何可能被其他线程更改的内存。 - m7913d
是的,那是基本的想法,但只有在使用抗锯齿时才有效。考虑到我正在处理高达8k分辨率的图像,这仍然值得一试。 - SleepProgger
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接