使用OpenCV的cv::Mat类转换Qt的QImage发生错误(解决方法在最后)
在学习Qt和OpencCV时遇到一个严重的BUG,Qt和OpenCV的细节就不多赘述了,网络上MatToQImage主要有两种方法 1.手撸for循环遍历Mat构造QImage 2.使用QImage的构造函数 QImage::QImage(uchar *data, int width, int height, QImage::Format format, QImageCleanupFunction cleanupFunction = nullptr, void *cleanupInfo = nullptr) 出于 偷懒 代码复用和精简的目的使用第二种方法,对于常用的24位深RGB图片使用以下方法 qImg= QImage(img.data,img.cols,img.rows,QImage::Format_BGR888); 其中qImg是QImage类型,img是cv::Mat类型,使用Format_BGR888因为cv::Mat内部使用BGR色彩空间。
出现Bug
在使用过程中使用一度十分快乐,但是其中一个项目使用这个语句出现了严重BUG 可以看出是很棘手的内存访问BUG,从右侧的调用堆栈也看不出所以然,甚至BUG在程序的哪一行都不知道,后经过不停的设置断点和逐行调试锁定了BUG,在一行显示图片的代码上
ui.label->setPixmap(QPixmap::fromImage(qImg));
其他项目使用相同的代码没有出现这个BUG,BUG出现的也很诡异: 1.使用其他图片就没有BUG。 2.有时候调试不出现BUG,但显示的图片是错误的颜色条的雪花。 3.将这行代码删去会在其他地方出现BUG。
原因:Mat不提供4字节对齐
在多天不断调试和查阅资料和修改代码后得出结论:cv::Mat不提供4字节对齐。 在Qt官方文档有如下
QImage::QImage(const uchar *data, int width, int height, QImage::Format format, QImageCleanupFunction cleanupFunction = nullptr, void *cleanupInfo = nullptr) Constructs an image with the given width, height and format, that uses an existing read-only memory buffer, data. The width and height must be specified in pixels, data must be 32-bit aligned, and each scanline of data in the image must also be 32-bit aligned. The buffer must remain valid throughout the life of the QImage and all copies that have not been modified or otherwise detached from the original buffer. The image does not delete the buffer at destruction. You can provide a function pointer cleanupFunction along with an extra pointer cleanupInfo that will be called when the last copy is destroyed. If format is an indexed color format, the image color table is initially empty and must be sufficiently expanded with setColorCount() or setColorTable() before the image is used. Unlike the similar QImage constructor that takes a non-const data buffer, this version will never alter the contents of the buffer. For example, calling QImage::bits() will return a deep copy of the image, rather than the buffer passed to the constructor. This allows for the efficiency of constructing a QImage from raw data, without the possibility of the raw data being changed. 微软翻译: 构建具有给定宽度、高度和格式的图像,该图像使用现有的内存缓冲器、数据。宽度和高度必须以像素表示,数据必须是 32 位对齐,图像中每个数据扫描线也必须是 32 位对齐。 缓冲器必须在 Q 图像的整个生命以及所有未修改或其他副本的整个生命期间保持有效 脱离原来的缓冲。图像不会在销毁时删除缓冲。您可以提供功能指点器清理功能以及额外的指点器清理信息,当最后一个副本被销毁时,这些信息将被调用。 如果格式是索引颜色格式,则图像颜色表最初是空的,在使用图像之前,必须用设置颜色计数 () 或设置彩色表 () 进行充分扩展。
重点来说构建Qimage的数据每一行都要32位即4字节对齐,如何判断Mat是否对齐?可以使用Mat::isContinuous(),查阅资料:如果矩阵元素每行末尾都是连续存储而没有空隙,返回true,否则为false。我使用的图像是1015x745的图片如下图: 由于这是三通道图片,调用Mat::isContinuous(),结果为turn, 显然1015*3字节的行宽而且每一行连续,没有padding,不是4字节对齐的。
错误分析:
代码如下:
QString imgName = R"(C:\Users\ASUS\OneDrive\document\Resources\fruit.jpeg)";
img = cv::imread(imgName.toStdString());
qImg= QImage(img.data,img.cols, img.rows, QImage::Format_BGR888);
imgShow();
为什么会出现内存访问错误?
由于Qimage对于提供的uchar指针要求每一行都字节对齐(一行只是形象的说法,它在内存中是连续的),由于图片宽1015像素,一行一共3045字节,如果要对齐需要每行补充3字节,Qimage将Mat::data提供的指针视为已经字节对齐,每一行读取3048字节,读取到最后超出了Mat申请的内存空间,所以出现内存访问错误,即使后面的空间其他程序没有使用,除了第一行之外的像素行都会不正确,出现颜色条和雪花,
为什么错误在quint24
quint定义如下
struct quint24 {
quint24() = default;
quint24(uint value)
{
data[0] = uchar(value >> 16);
data[1] = uchar(value >> 8);
data[2] = uchar(value);
}
operator uint() const
{
return data[2] | (data[1] << 8) | (data[0] << 16);
}
uchar data[3];
};
quint24的本质是长度为3的uchar数组,推测在QImage内部将每一个像素的指针赋值给quint24中的data成员,然后再逐个访问每个通道的值
为什么其他项目不出现错误?
其他项目使用的图片宽度恰好都是4字节的整数倍,网络上很多图片的尺寸都是规范宽度:1920,1680,1600,1440,1024,800等等,这些都是4的倍数,也是显示器的标准尺寸,4字节对齐的尺寸计算机运行效率更高,这导致这个BUG出现的概率很小
为什么错误不在QImage构造函数上?
根据前面的文档翻译
缓冲器必须在 Qimage的整个生命以及所有未修改或其他副本的整个生命期间保持有效
可知,QImage的这个构造函数使用浅拷贝,不访问数据,而QImage的拷贝构造函数的文档说明
QImage &QImage::operator=(const QImage &image) Assigns a shallow copy of the given image to this image and returns a reference to this image. 将给定图像的浅副本分配给此图像,并返回此图像的参考。
所以错误的QImage图像在构造和拷贝过程中都不会真正的访问指针中的数据,直到真正使用QImage的时候才出现错误,即使把出现错误位置的代码删去也会在其他地方出现错误。
解决方法
找到问题就好解决了:
方法1.
修改图片尺寸使其行宽恰好4的倍数,如cv::resize(img, img, cv::Size(1000,1000/img.cols*img.rows));或者直接修改图像文件本身。
方法2.
使用另一个构造函数QImage::QImage(uchar *data, int width, int height, qsizetype bytesPerLine, QImage::Format format, QImageCleanupFunction cleanupFunction = nullptr, void *cleanupInfo = nullptr)
qImg= QImage(img.data,img.cols, img.rows,img.cols*3, QImage::Format_BGR888);
它需要你提供每一行的字节数而不要求对齐,为什么网上的转换方法不使用这个?也许大家使用的图片大小都是符号规范的。
方法3
手撸一个构造对齐后数组的方法。
int colum4 = (img.cols * 3 + 3) / 4 * 4;
uchar* Array = new uchar[colum4 * img.rows];
for (size_t i = 0; i < img.rows; i++)
{
std::copy(img.data + i * img.cols * 3, img.data + (i + 1) * img.cols * 3, Array + colum4 * i);
}
qImg= QImage(Array,img.cols, img.rows, QImage::Format_BGR888);
重点在于Array需要最后释放,如果不知道什么时候释放可以看Qimage构造函数的后两个参数
QImageCleanupFunction cleanupFunction = nullptr, void *cleanupInfo = nullptr
QImageCleanupFunction 是一个函数指针类型,由文档可知该类型函数接受void*类型返回void,即接受任意类型的指针并且调用。虽然本人没有尝试,但有足够的理由可以猜测,,当所有的Qimage共享内存的副本都析构了QImage将会调用cleanupFunction (cleanupInfo )。可以自己写一个函数来最终释放内存。
方法4
当然是写一个直接从Mat到QImage一个个像素复制的函数,网上有很多就不赘述了。
总结
方法2最为简单,不过方法3可以很好的证明前面的结论,看起来一个非常小的BUG却用了好几天确定问题并论证解决。 以上方法可能只适用于24位三通道的图像,其他格式图像同理易解决,不过推测四通道图像应该不会出现这个问题。 环境debug,Visual studio2019,Qt6.0.2,opencv使用opencv_world453d.lib。
|