在OpenCV中将Mat转换为数组/向量

66

我是OpenCV的新手。最近,我在寻找将Mat转换为数组的OpenCV函数方面遇到了问题。我使用了OpenCV API中可用的.ptr和.at方法进行研究,但我无法获得正确的数据。如果可以的话,我希望能直接将Mat转换为数组(如果不行再转换为向量)。我需要OpenCV函数,因为代码必须经过Vivado HLS中的高级综合。请帮忙。

10个回答

103

如果Mat mat的内存是连续的(其所有数据都是连续的),则可以直接将其数据获取到一个1D数组中:

std::vector<uchar> array(mat.rows*mat.cols*mat.channels());
if (mat.isContinuous())
    array = mat.data;
否则,您必须逐行获取其数据行,例如转换为一个二维数组:

否则,您必须逐行获取其数据行,例如转换为一个二维数组:

uchar **array = new uchar*[mat.rows];
for (int i=0; i<mat.rows; ++i)
    array[i] = new uchar[mat.cols*mat.channels()];

for (int i=0; i<mat.rows; ++i)
    array[i] = mat.ptr<uchar>(i);

更新: 如果您使用std::vector,将会更加容易,您可以这样做:

std::vector<uchar> array;
if (mat.isContinuous()) {
  // array.assign(mat.datastart, mat.dataend); // <- has problems for sub-matrix like mat = big_mat.row(i)
  array.assign(mat.data, mat.data + mat.total()*mat.channels());
} else {
  for (int i = 0; i < mat.rows; ++i) {
    array.insert(array.end(), mat.ptr<uchar>(i), mat.ptr<uchar>(i)+mat.cols*mat.channels());
  }
}

p.s.: 对于其他类型的cv::Mat,例如CV_32F,您应该像这样做:

std::vector<float> array;
if (mat.isContinuous()) {
  // array.assign((float*)mat.datastart, (float*)mat.dataend); // <- has problems for sub-matrix like mat = big_mat.row(i)
  array.assign((float*)mat.data, (float*)mat.data + mat.total()*mat.channels());
} else {
  for (int i = 0; i < mat.rows; ++i) {
    array.insert(array.end(), mat.ptr<float>(i), mat.ptr<float>(i)+mat.cols*mat.channels());
  }
}

更新2: 对于OpenCV Mat数据的连续性,可以总结如下:

  • imread()clone()或构造函数创建的矩阵将始终是连续的。
  • 矩阵不连续的唯一情况是它借用了现有矩阵中的数据(除非从大矩阵的ROI中创建,其借用的数据在大矩阵中是连续的,例如1.单行;2.多行与完整原始宽度)。

请查看此代码片段进行演示。


3
最好使用std::vector。使用裸指针时,还需要释放内存。 - madduci
2
@blackibiza 很好的观点。已更新答案使用 std::vector。 :-) - herohuyongtao
2
我猜在使用array.insert复制数据时,应该将mat.cols乘以mat.channels。此外,在使用float作为模板参数时,可以省略从uchar*float*的转换:mat.ptr<float> - Johnny Thunderman
2
这行代码存在一个隐藏的错误:array.assign(mat.datastart, mat.dataend);。给定一个随机的2D(2X10)矩阵 m,调用你的 'vec2mat' 函数,vector<T> array1 = mat2vec(mat.row(0)), array2 = mat2vec(mat.row(1)),你会发现 array1array2 的大小都是20,而不是10。 - ouxiaogu
1
@WilderField 单行矩阵保证是连续的,但是当它的数据从另一个大矩阵(由大矩阵的 ROI 创建)中借用时,单列矩阵不一定连续。我已经创建了一个 代码片段 进行更好的演示。 - herohuyongtao
显示剩余10条评论

31

两行搞定 :)

将Mat转换为数组

uchar * arr = image.isContinuous()? image.data: image.clone().data;
uint length = image.total()*image.channels();

矩阵转向量

cv::Mat flat = image.reshape(1, image.total()*image.channels());
std::vector<uchar> vec = image.isContinuous()? flat : flat.clone();

两者均适用于 任何 一般的 cv::Mat

附带一个工作示例的解释

    cv::Mat image;
    image = cv::imread(argv[1], cv::IMREAD_UNCHANGED);   // Read the file
    cv::namedWindow("cvmat", cv::WINDOW_AUTOSIZE );// Create a window for display.
    cv::imshow("cvmat", image );                   // Show our image inside it.

    // flatten the mat.
    uint totalElements = image.total()*image.channels(); // Note: image.total() == rows*cols.
    cv::Mat flat = image.reshape(1, totalElements); // 1xN mat of 1 channel, O(1) operation
    if(!image.isContinuous()) {
        flat = flat.clone(); // O(N),
    }
    // flat.data is your array pointer
    auto * ptr = flat.data; // usually, its uchar*
    // You have your array, its length is flat.total() [rows=1, cols=totalElements]
    // Converting to vector
    std::vector<uchar> vec(flat.data, flat.data + flat.total());
    // Testing by reconstruction of cvMat
    cv::Mat restored = cv::Mat(image.rows, image.cols, image.type(), ptr); // OR vec.data() instead of ptr
    cv::namedWindow("reconstructed", cv::WINDOW_AUTOSIZE);
    cv::imshow("reconstructed", restored);

    cv::waitKey(0);     

扩展说明:

Mat被存储为一块连续的内存块,如果使用其中一个构造函数创建或复制到另一个Mat,则如此。要转换为数组或vector,我们需要其第一个块的地址和数组/向量长度。

指向内存块的指针

Mat::data是其内存的公共uchar指针。
但这个内存可能不是连续的。正如其他答案所解释的那样,我们可以使用mat.isContinous()检查mat.data是否指向连续的内存。除非您需要极致的效率,否则可以在O(N)时间内使用mat.clone()获取mat的连续版本。 (N =所有通道的元素数)。但是,在处理由cv :: imread()读取的图像时,我们很少会遇到非连续的mat。

数组/向量的长度

问:应该是row*cols*channels对吧?
答案:并不总是。它可以是rows*cols*x*y*channels
问:应该等于mat.total()吗?
答案:对于单通道mat是正确的。但对于多通道mat不是这样
由于OpenCV文档质量较差,数组/向量的长度略微棘手。我们有一个公共成员Mat::size,仅存储单个Mat的尺寸没有通道。对于RGB图像,Mat.size = [rows,cols]而不是[rows,cols,channels]。Mat.total()返回mat单个通道中的总元素数,等于mat.size值的乘积。对于RGB图像,total()= rows * cols。因此,对于任何一般的Mat,连续内存块的长度将为mat.total() * mat.channels()

从数组/向量重构Mat

除了数组/向量外,我们还需要原始Mat的mat.size[类似于数组]和mat.type() [int]。然后使用其中一个构造函数来接受数据指针,我们可以获得原始Mat。由于我们的数据指针指向连续的内存,因此不需要可选的步骤参数。我使用这种方法在nodejs和C ++之间传递Mat作为Uint8Array。这避免了使用node-addon-api编写cv :: Mat的C ++绑定。

参考:


1
如果矩阵不连续,那么“矩阵转向量”的建议将行不通,因为reshape会抛出异常。 - Carlos Ost
你尝试过它并且失败了吗?.clone() 确保您获得一个连续的平坦数组,稍后可以重新整形。 - sziraqui
是的,我知道。你关于克隆返回一个连续数组是正确的,但问题是你建议在克隆之前重新定义mat图像的形状,所以如果图像不连续,它将会抛出异常。不管怎样,这帮助我找到了解决方案。谢谢。 - Carlos Ost
在我的情况下,.clone().reshape(...) 解决了“不连续”的异常。 - undefined

15

假设矩阵只有一列,这里是另一个可能的解决方案(您可以通过reshape将原始矩阵转换为一列矩阵):

Mat matrix= Mat::zeros(20, 1, CV_32FC1);
vector<float> vec;
matrix.col(0).copyTo(vec);

为什么这段代码无法正常运行?代码链接 - yode
@yode img的类型是double,而vec的类型是int。此外,在您的情况下,vec不是一维的。 - Burak

5

这里提供的所有示例都不适用于通用情况,即N维矩阵。任何使用“行”来做假设只有列和行,而4维矩阵可能会更多。

以下是一些示例代码,将非连续的N维矩阵复制到连续的内存流中,然后将其转换回Cv :: Mat。

#include <iostream>
#include <cstdint>
#include <cstring>
#include <opencv2/opencv.hpp>

int main(int argc, char**argv)
{
    if ( argc != 2 )
    {
        std::cerr << "Usage: " << argv[0] << " <Image_Path>\n";
        return -1;
    }
    cv::Mat origSource = cv::imread(argv[1],1);

    if (!origSource.data) {
        std::cerr << "Can't read image";
        return -1;
    }

    // this will select a subsection of the original source image - WITHOUT copying the data
    // (the header will point to a region of interest, adjusting data pointers and row step sizes)
    cv::Mat sourceMat = origSource(cv::Range(origSource.size[0]/4,(3*origSource.size[0])/4),cv::Range(origSource.size[1]/4,(3*origSource.size[1])/4));

    // correctly copy the contents of an N dimensional cv::Mat
    // works just as fast as copying a 2D mat, but has much more difficult to read code :)
    // see https://dev59.com/E2Mk5IYBdhLWcg3w5Bzy
    // copy this code in your own cvMat_To_Char_Array() function which really OpenCV should provide somehow...
    // keep in mind that even Mat::clone() aligns each row at a 4 byte boundary, so uneven sized images always have stepgaps
    size_t totalsize = sourceMat.step[sourceMat.dims-1];
    const size_t rowsize = sourceMat.step[sourceMat.dims-1] * sourceMat.size[sourceMat.dims-1];
    size_t coordinates[sourceMat.dims-1] = {0};
    std::cout << "Image dimensions: ";
    for (int t=0;t<sourceMat.dims;t++)
    {
        // calculate total size of multi dimensional matrix by multiplying dimensions
        totalsize*=sourceMat.size[t];
        std::cout << (t>0?" X ":"") << sourceMat.size[t];
    }
    // Allocate destination image buffer
    uint8_t * imagebuffer = new uint8_t[totalsize];
    size_t srcptr=0,dptr=0;
    std::cout << std::endl;
    std::cout << "One pixel in image has " << sourceMat.step[sourceMat.dims-1] << " bytes" <<std::endl;
    std::cout << "Copying data in blocks of " << rowsize << " bytes" << std::endl ;
    std::cout << "Total size is " << totalsize << " bytes" << std::endl;
    while (dptr<totalsize) {
        // we copy entire rows at once, so lowest iterator is always [dims-2]
        // this is legal since OpenCV does not use 1 dimensional matrices internally (a 1D matrix is a 2d matrix with only 1 row)
        std::memcpy(&imagebuffer[dptr],&(((uint8_t*)sourceMat.data)[srcptr]),rowsize);
        // destination matrix has no gaps so rows follow each other directly
        dptr += rowsize;
        // src matrix can have gaps so we need to calculate the address of the start of the next row the hard way
        // see *brief* text in opencv2/core/mat.hpp for address calculation
        coordinates[sourceMat.dims-2]++;
        srcptr = 0;
        for (int t=sourceMat.dims-2;t>=0;t--) {
            if (coordinates[t]>=sourceMat.size[t]) {
                if (t==0) break;
                coordinates[t]=0;
                coordinates[t-1]++;
            }
            srcptr += sourceMat.step[t]*coordinates[t];
        }
   }

   // this constructor assumes that imagebuffer is gap-less (if not, a complete array of step sizes must be given, too)
   cv::Mat destination=cv::Mat(sourceMat.dims, sourceMat.size, sourceMat.type(), (void*)imagebuffer);

   // and just to proof that sourceImage points to the same memory as origSource, we strike it through
   cv::line(sourceMat,cv::Point(0,0),cv::Point(sourceMat.size[1],sourceMat.size[0]),CV_RGB(255,0,0),3);

   cv::imshow("original image",origSource);
   cv::imshow("partial image",sourceMat);
   cv::imshow("copied image",destination);
   while (cv::waitKey(60)!='q');
}

3

不必逐行获取图像,您可以直接将其放入数组中。对于CV_8U类型的图像,您可以使用字节数组,对于其他类型,请在此处查看。

Mat img; // Should be CV_8U for using byte[]
int size = (int)img.total() * img.channels();
byte[] data = new byte[size];
img.get(0, 0, data); // Gets all pixels

3
byte * matToBytes(Mat image)
{
   int size = image.total() * image.elemSize();
   byte * bytes = new byte[size];  //delete[] later
   std::memcpy(bytes,image.data,size * sizeof(byte));
}

2
虽然这段代码可能回答了问题,但是提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。 - ryanyuyu

3

您可以使用迭代器:

Mat matrix = ...;

std::vector<float> vec(matrix.begin<float>(), matrix.end<float>());

0
cv::Mat m;
m.create(10, 10, CV_32FC3);

float *array = (float *)malloc( 3*sizeof(float)*10*10 );
cv::MatConstIterator_<cv::Vec3f> it = m.begin<cv::Vec3f>();
for (unsigned i = 0; it != m.end<cv::Vec3f>(); it++ ) {
    for ( unsigned j = 0; j < 3; j++ ) {
        *(array + i ) = (*it)[j];
        i++;
    }
}

现在你有一个浮点数数组。如果是8位的情况下,只需将float更改为ucharVec3f更改为Vec3bCV_32FC3更改为CV_8UC3即可。

在这种情况下,您可以使用以下代码重新创建OpenCV的Mat对象:A = Mat(10, 10, CV_32FC3, &array); - Dániel Terbe
我不确定,但我认为你可以做到。我稍后可以检查一下。所以,您想通过再次将它们转换为Mat来验证由cv :: Mat m创建的数组的转换吗? - infoclogged
是的,我想我试过(但那是很久以前了,我不确定),而且它成功了! - Dániel Terbe
好的,你第一条评论末尾的问号让人以为你在问问题。 - infoclogged

0
如果您知道您的img是3通道的,那么您可以尝试这段代码。
 Vec3b* dados = new Vec3b[img.rows*img.cols];
    for (int i = 0; i < img.rows; i++)
        for(int j=0;j<img.cols; j++)
            dados[3*i*img.cols+j] =img.at<Vec3b>(i,j);

如果你想要检查 (i,j) vec3b,你可以写成:
std::cout << (Vec3b)img.at<Vec3b>(i,j) << std::endl;
    std::cout << (Vec3b)dados[3*i*img.cols+j] << std::endl;

0

由于上面的答案在评论中提到并不太准确,但其“编辑队列已满”,因此我不得不添加正确的一行代码。

将Mat(uchar,1通道)转换为vector(uchar):

std::vector<uchar> vec = (image.isContinuous() ? image : image.clone()).reshape(1, 1); // data copy here

将任意类型的向量转换为相同类型的Mat:

Mat m(vec, false); // false(by default) -- do not copy data 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接