我无法理解 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 手动分配所有颜色元素,它似乎可以工作,但为什么???
最佳答案
注: cv::Scalar
是 cv::Scalar_<double>
的 typedef ,源自 cv::Vec<double, 4>
,源自 cv::Matx<double, 4, 1>
.
同样, cv::Vec3b
是 cv::Vec<uint8_t, 3>
源自 cv::Matx<uint8_t, 3, 1>
-- 这意味着我们可以在 cv::Mat::at
中使用这三个中的任何一个并获得相同(正确)的行为。
重要的是要注意 cv::Mat::at
是 basically a reinterpret_cast
在底层数据数组上。您需要非常小心地为模板参数使用适当的数据类型,该类型对应于 cv::Mat
的元素类型(包括 channel 数)。你正在调用它。
该文档提到了以下内容:
Keep in mind that the size identifier used in the at operator cannot be chosen at random. It depends on the image from which you are trying to retrieve the data. The table below gives a better insight in this:
- If matrix is of type
CV_8U
then useMat.at<uchar>(y,x)
.- If matrix is of type
CV_8S
then useMat.at<schar>(y,x)
.- If matrix is of type
CV_16U
then useMat.at<ushort>(y,x)
.- If matrix is of type
CV_16S
then useMat.at<short>(y,x)
.- If matrix is of type
CV_32S
then useMat.at<int>(y,x)
.- If matrix is of type
CV_32F
then useMat.at<float>(y,x)
.- If matrix is of type
CV_64F
then useMat.at<double>(y,x)
.
它似乎没有提到在多个 channel 的情况下该怎么做 - 在这种情况下,您使用
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]
这清楚地表明我们写的比我们应该写的要多。
在Debug模式下,
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 会做一个 saturate_cast
,但考虑到平均值不会超过输入项的最小值/最大值,我们可以使用常规转换。 要删除第四个元素,我们可以使用
cv::Matx::get_minor
(文档有点缺乏,但查看 implementation 可以很好地解释它)。结果是 cv::Matx
,所以我们必须使用它而不是 cv::Vec
使用时 cv::Mat::at
.那么两种可能的选择是:
cast结果转换
cv::Matx
至 uint8_t
元素类型。 cv::Scalar
至 cv::Scalar_<uint8_t>
首先,然后去掉第 4 个元素。 示例:
#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;
}
注意:你可以去掉显式的临时变量,它们在这里只是为了更容易阅读。
输出:
这两个选项都会产生以下输出:
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
s 而不是 3。必须进行一些基准测试(读者练习)。平均值的计算将大大超过它,因此它可能无关紧要。鉴于我们并不真正需要
saturate_cast
s,也许简单但更冗长的方法(对你有用的东西的优化版本)可能在紧密循环中表现得更好。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]);
}
更新:
在与 @alkasm 讨论中提出的另一个想法.
cv::Mat
的赋值运算符当给定 cv::Scalar
时被向量化(它为所有元素分配相同的值),并忽略 cv::Scalar
的附加 channel 值可持有相对于目标cv::Mat
类型。 (例如,对于 3 channel Mat
,它会忽略第四个值)。我们可以采用目标
Mat
的 1x1 ROI ,并将其赋值为 Scalar
.必要的类型转换将发生,第 4 个 channel 将被丢弃。可能不是最优的,但它是迄今为止最少的代码。test_mat(cv::Rect(0, 0, 1, 1)) = test_scalar;
结果和之前一样。
关于OpenCV 如何转换矩阵元素,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56350033/