OpenCV 如何转换矩阵元素

标签 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 手动分配所有颜色元素,它似乎可以工作,但为什么???

最佳答案

注: 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 use Mat.at<uchar>(y,x).
  • If matrix is of type CV_8S then use Mat.at<schar>(y,x).
  • If matrix is of type CV_16U then use Mat.at<ushort>(y,x).
  • If matrix is of type CV_16S then use Mat.at<short>(y,x).
  • If matrix is of type CV_32S then use Mat.at<int>(y,x).
  • If matrix is of type CV_32F then use Mat.at<float>(y,x).
  • If matrix is of type CV_64F then use Mat.at<double>(y,x).


它似乎没有提到在多个 channel 的情况下该怎么做 - 在这种情况下,您使用 cv::Vec<...> (或者更确切地说是提供的 typedef 之一)。 cv::Vec<...>基本上是围绕给定类型的 N 个值的固定大小数组的包装器。

在您的情况下,矩阵 avgsCV_8UC3 -- 每个元素由 3 个无符号字节值组成(即总共 3 个字节)。但是,通过使用 avgs.at<Scalar>(i) ,您将每个元素解释为 4 个 double (总共 32 个字节)。这意味着:
  • 您尝试写入的实际元素(如果正确解释)将仅保存第一个 channel (8 字节浮点)平均值的 3 个最高有效字节——即完全垃圾。
  • 你实际上用更多的垃圾覆盖了接下来的 10 个元素(最后一个部分,第三个 channel 毫发无损)。
  • 在某些时候,您必然会溢出缓冲区并可能破坏其他数据结构。这个问题比较严重。

  • 我们可以使用以下简单程序来演示它。

    示例:
    #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 .

    那么两种可能的选择是:
  • 去掉第 4 个元素,然后
    cast结果转换 cv::Matxuint8_t元素类型。
  • Cast cv::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;
    }
    

    注意:你可以去掉显式的临时变量,它们在这里只是为了更容易阅读。

    输出:

    这两个选项都会产生以下输出:
    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/

    相关文章:

    python - 以不同名称循环保存图像

    c++ - OpenCV error-cvtColor require 1-channel image 以及如何修复它?

    opencv - 尝试在 raspbian wheezy 上构建 opencv-2.4.10 时未声明 SIZE_MAX

    c++ - 在 qt 的子目录项目中找不到 header

    python - 我无法使用 opencv 从字节读取图像

    python - VideoCapture()读取多个视频和帧分辨率问题

    c++ - 使用 OpenCV 进行硬币模板匹配

    python - 使用python更改图像中的颜色范围

    python - Gamma 校正,用于背景较浅的图像

    c++ - matlab在opencv中查找函数实现?