我无法理解OpenCV的内部运作方式。考虑以下代码:
Scalar getAverageColor(Mat img, vector<Rect>& rois) {
int n = static_cast<int>(rois.size());
Mat avgs(1, n, CV_8UC3);
for (int i = 0; i < n; ++i) {
// What is the correct way to assign the color elements in
// the matrix?
avgs.at<Scalar>(i) = mean(Mat(img, rois[i]));
/*
This seems to always work, but there has to be a better way.
avgs.at<Vec3b>(i)[0] = mean(Mat(img, rois[i]))[0];
avgs.at<Vec3b>(i)[1] = mean(Mat(img, rois[i]))[1];
avgs.at<Vec3b>(i)[2] = mean(Mat(img, rois[i]))[2];
*/
}
// If I access the first element it seems to be set correctly.
Scalar first = avgs.at<Scalar>(0);
// However mean returns [0 0 0 0] if I did the assignment above using scalar, why???
Scalar avg = mean(avgs);
return avg;
}
如果我在循环中使用avgs.at<Scalar>(i) = mean(Mat(img, rois[i]))
进行赋值,则第一个元素看起来正确,但是平均值计算始终返回零(即使以为第一个元素看起来正确)。如果我使用Vec3b手动分配所有颜色元素,那似乎可行,但是为什么呢?
答案 0 :(得分:1)
注意::cv::Scalar
是cv::Scalar_<double>
的typedef,它是从cv::Vec<double, 4>
派生的,而cv::Matx<double, 4, 1>
是cv::Vec3b
派生的。
同样,cv::Mat::at
是cv::Vec<uint8_t, 3>
,它源自cv::Matx<uint8_t, 3, 1>
-这意味着我们可以使用cv::Mat::at
中的这3个中的任何一个,并获得相同的(正确的)行为。
请务必注意,基础数据数组上的basically a reinterpret_cast
是saturate_cast
。您需要非常小心,为模板参数使用一种适当的数据类型,该数据类型与您正在调用的cv::Mat
的元素类型(包括通道数)相对应。
文档中提到以下内容:
请记住,不能随意选择at运算符中使用的大小标识符。这取决于您尝试从中检索数据的图像。下表对此有更深入的了解:
- 如果矩阵的类型为
CV_8U
,则使用Mat.at<uchar>(y,x)
。- 如果矩阵的类型为
CV_8S
,则使用Mat.at<schar>(y,x)
。- 如果矩阵的类型为
CV_16U
,则使用Mat.at<ushort>(y,x)
。- 如果矩阵的类型为
CV_16S
,则使用Mat.at<short>(y,x)
。- 如果矩阵的类型为
CV_32S
,则使用Mat.at<int>(y,x)
。- 如果矩阵的类型为
CV_32F
,则使用Mat.at<float>(y,x)
。- 如果矩阵的类型为
CV_64F
,则使用Mat.at<double>(y,x)
。
似乎没有提到在使用多个通道的情况下该怎么做-在这种情况下,您使用cv::Vec<...>
(或者提供的typedef之一)。 cv::Vec<...>
基本上是给定类型的N个值的固定大小数组的包装。
在您的情况下,矩阵avgs
是CV_8UC3
-每个元素由3个无符号字节值组成(即,总共3个字节)。但是,通过使用avgs.at<Scalar>(i)
,您可以将每个元素解释为4个double(总共32个字节)。这意味着:
我们可以使用以下简单程序进行演示。
示例:
#include <opencv2/opencv.hpp>
int main()
{
cv::Mat test_mat(cv::Mat::zeros(1, 12, CV_8UC3)); // 12 * 3 = 36 bytes of data
std::cout << "Before: " << test_mat << "\n";
cv::Scalar test_scalar(cv::Scalar::all(1234.5678));
test_mat.at<cv::Scalar>(0, 0) = test_scalar;
std::cout << "After: " << test_mat << "\n";
return 0;
}
输出:
Before: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
After: [173, 250, 92, 109, 69, 74, 147, 64, 173, 250, 92, 109, 69, 74, 147, 64, 173, 250, 92, 109, 69, 74, 147, 64, 173, 250, 92, 109, 69, 74, 147, 64, 0, 0, 0, 0]
这清楚地表明我们在写作方面比应有的方式更多。
在调试模式下,错误使用at
还会触发一个断言:
OpenCV(3.4.3) Error: Assertion failed (((((sizeof(size_t)<<28)|0x8442211) >> ((traits::Depth<_Tp>::value) & ((1 << 3) - 1))*4) & 15) == elemSize1()) in cv::Mat::at, file D:\code\shit\so07\deps\include\opencv2/core/mat.inl.hpp, line 1102
要允许将cv::mean
(是cv::Scalar
)的结果分配到我们的CV_8UC3
矩阵,我们需要做两件事(不一定按此顺序):>
double
转换为uint8_t
- OpenCV将执行cv::Matx::get_minor
,但是鉴于均值不会超过输入项的最小值/最大值,我们用常规演员表就可以了。要删除第4个元素,我们可以使用implementation(缺少文档,但是看一下cast可以很好地解释它)。结果是cv::Matx
,因此在使用cv::Vec
时我们必须使用它而不是cv::Mat::at
。
那么两个可能的选项是:
示例:
#include <opencv2/opencv.hpp>
typedef cv::Matx<uint8_t, 3, 1> Mat31b; // Convenience, OpenCV only has typedefs for double and float variants
int main()
{
cv::Mat test_mat(1, 12, CV_8UC3); // 12 * 3 = 36 bytes of data
test_mat = cv::Scalar(1, 1, 1); // Set all elements to 1
std::cout << "Before: " << test_mat << "\n";
cv::Scalar test_scalar{ 2,3,4,0 };
cv::Matx31d temp = test_scalar.get_minor<3, 1>(0, 0);
test_mat.at<Mat31b>(0, 0) = static_cast<Mat31b>(temp);
// or
// cv::Scalar_<uint8_t> temp(static_cast<cv::Scalar_<uint8_t>>(test_scalar));
// test_mat.at<Mat31b>(0, 0) = temp.get_minor<3, 1>(0, 0);
std::cout << "After: " << test_mat << "\n";
return 0;
}
NB:您可以摆脱显式临时对象,它们在这里只是为了便于阅读。
输出:
两个选项均产生以下输出:
Before: [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
After: [ 2, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
我们可以看到,只有前3个字节被更改,因此其行为正确。
关于性能的一些想法。
很难猜测这两种方法中哪个更好。首先进行转换意味着您为临时存储分配了较少的内存,但是随后您必须执行4 saturate_cast
而不是3。必须进行一些基准测试(对于读者来说是专门的)。均值的计算将大大超过它,因此可能无关紧要。
鉴于我们实际上并不需要saturate_cast
,也许简单但更冗长的方法(为您工作的事物的优化版本)可能会在紧密循环中表现更好。
cv::Vec3b& current_element(avgs.at<cv::Vec3b>(i));
cv::Scalar current_mean(cv::mean(cv::Mat(img, rois[i])));
for (int n(0); n < 3; ++n) {
current_element[n] = static_cast<uint8_t>(current_mean[n]);
}
更新:
与{{3}}讨论时提出的另一个想法。当给定cv::Mat
时,cv::Scalar
的赋值运算符被矢量化(它为所有元素分配相同的值),并且它忽略了cv::Scalar
相对于目标可能拥有的其他通道值cv::Mat
类型。 (例如,对于3通道Mat
,它会忽略第4个值)。
我们可以采用目标Mat
的1x1 ROI,并为其分配均值Scalar
。必要的类型转换将发生,并且第四个通道将被断开。可能不是最佳的,但是到目前为止,它是最少的代码。
test_mat(cv::Rect(0, 0, 1, 1)) = test_scalar;
结果与以前相同。