踩了1天的opencv坑,忍不住吐槽了。。。
正常两个矩阵运算操作不能适用于一系列的 8U 图像,这是最大的坑,比如你想要将两个图像矩阵相加:
Mat im1 = imread("../opencv/samples/data/pic5.png", CV_8UC1); Mat im2 = imread("../opencv/samples/data/pic6.png", CV_8UC1); Mat overlap = im1 + im2;
如果你这么写,得到的图像绝对是错误的!!!你必须做一些特殊转换。
嘛,首先运行后会看到结果是:
这里 im1 和 im2 是这样的(opencv 自带的示例图像):
(im1)
(im2)
诈一看觉得好像是对的,但实际上正确的是这样的(运行于 python opencv):
而 C++ 会出现这样的原因是因为 8U 图像的值范围是 0~255,Mat 在做相加操作的时候大于255的那些就溢出了。。然后那些因为相加而溢出的像素的值就会是 255,起初我写了这个方法来修正:
Mat matr_add(Mat im1, Mat im2) { CV_Assert(im1.size() == im2.size()); Mat uxx = Mat::zeros(im1.size(), CV_8UC1); for (int i = 0; i < uxx.rows; i++) { for (int j = 0; j < uxx.cols; j++) { uxx.at<uchar>(i, j) = (im1.at<uchar>(i, j) + im2.at<uchar>(i, j)) % 256; } } return uxx; }
运行结果:
正确了。
执行矩阵相乘的时候就更明显了(C++ opencv):
而正确的(运行于 python opencv):
同样用取模的方法修正:
Mat multiply_mod(Mat im1, Mat im2) { CV_Assert(im1.size() == im2.size()); Mat uxx = Mat::zeros(im1.size(), CV_8UC1); for (int i = 0; i < uxx.rows; i++) { for (int j = 0; j < uxx.cols; j++) { uxx.at<uchar>(i, j) = (im1.at<uchar>(i, j) * im2.at<uchar>(i, j)) % 256; } } return uxx; }
运行结果:
修正了。
同理,相减也是一样需要修复:
Mat matr_sub(Mat im1, Mat im2) { CV_Assert(im1.size() == im2.size()); Mat uxx = Mat::zeros(im1.size(), CV_8UC1); for (int i = 0; i < uxx.rows; i++) { for (int j = 0; j < uxx.cols; j++) { uxx.ptr<uchar>(i)[j] = (im1.ptr<uchar>(i)[j] - im2.ptr<uchar>(i)[j]) < 0 ? (im1.ptr<uchar>(i)[j] - im2.ptr<uchar>(i)[j]) % 256 : (im1.ptr<uchar>(i)[j] - im2.ptr<uchar>(i)[j]); } } return uxx; }
很美好,看起来似乎解决了所有问题。
但实际上问题没这么简单,还有个问题在于相除的情况怎么才能修复,因为一些情况下对图像做了一些操作后会得到一个含有0(原因也是因为用 8UC1 类型的 Mat 存储像素,所以导致 0.xxx 的值直接按 0 处理)的矩阵,这时我没办法再修复了,但我又想要获得或者设置正确的浮点像素值,这个问题直接导致了我写的图像相似度检测算法没法正常用...
于是,重新冷静考虑一下。
首先要清楚的是,直接使用 CV_32FC1 读取图像是不行的,不出意外会得到很多 Nan 或 inf 值,从而图像也无法正常显示。
以及我们千万别使用下面几种情况的代码访问像素元素(以下皆假设使用 C1 单通道图像):
情况1:假设 im 类型为 CV_8UC1
im.at<int>(row, col); im.at<float>(row, col); //...
不然,你会发现明明你的图像是 600 x 600,却读到小部分,甚至一进循环就报 opencv_assert 的断言中断错误。参见:https://github.com/kinchungwong/kinchungwong.github.io/blob/master/opencv_issues_11370/explain_opencv_non_issue_11370.md
对于 8U 系列,你只能使用这种方式:
im.at<uchar>(row, col) im.ptr<T>(row)[col];
其他情况,对于其他类型的 Mat,最好是用这种方式获取或设置像素元素信息:
im.ptr<T>(row)[col];
绝不能使用 at<T>() 去访问,因为它是靠类型进行内存地址计算的,所以一定会超出范围。程序会立即中断。
回到主题。
实际上,我们想保证正确且方便的进行图像的矩阵运算,要做的其实只有一步,首先,我们使用 CV_8UC1 (以单通道图像为例)读取图像,然后将其矩阵类型转换为 CV_32FC1 就可以了!就是这么简单却搞了我好久:
Mat im1 = imread("../opencv/samples/data/lena.jpg", CV_8UC1); Mat im1f; im1.convertTo(im1f, CV_32FC1); // ...
于是,我的写的 SSIM 算法可以用上了,下一篇我会完整介绍 SSIM 算法~