OpenCV矩阵元素的转换如何工作

时间:2019-05-28 21:04:45

标签: opencv

我无法理解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手动分配所有颜色元素,那似乎可行,但是为什么呢?

1 个答案:

答案 0 :(得分:1)

注意:cv::Scalarcv::Scalar_<double>的typedef,它是从cv::Vec<double, 4>派生的,而cv::Matx<double, 4, 1>cv::Vec3b派生的。 同样,cv::Mat::atcv::Vec<uint8_t, 3>,它源自cv::Matx<uint8_t, 3, 1> -这意味着我们可以使用cv::Mat::at中的这3个中的任何一个,并获得相同的(正确的)行为。


请务必注意,基础数据数组上的basically a reinterpret_castsaturate_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个值的固定大小数组的包装。


在您的情况下,矩阵avgsCV_8UC3 -每个元素由3个无符号字节值组成(即,总共3个字节)。但是,通过使用avgs.at<Scalar>(i),您可以将每个元素解释为4个double(总共32个字节)。这意味着:

  • 您尝试写入的实际元素(如果正确解释)将仅保存第一个通道(8字节浮点数)平均值的3个最高有效字节-即完全垃圾。
  • 实际上,您会用更多的垃圾覆盖接下来的10个元素(最后一个部分,第3个通道会毫发无损)。
  • 在某些时候,您注定会溢出缓冲区并可能破坏其他数据结构。这个问题很严重。

我们可以使用以下简单程序进行演示。

示例:

#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

那么两个可能的选项是:

  • 摆脱第四个元素,然后 Cast的结果将cv::Matx转换为uint8_t元素类型。

  • @alkasmcv::Scalarcv::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;
}

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;

结果与以前相同。