不分配内存的cv::cvtColor问题。

3

我正在尝试使用cv::cvtColor将一个320x240的NV21图像转换为RGBA图像,而不需要分配新内存。

因此,我尝试了各种方法来实现这一目标。我知道cv::cvtColor调用cv::Mat.create(..)为输出Mat分配内存,并且我已经看到cv::Mat.create(...)仅在所请求的Mat的大小和类型与其当前值不同时才分配新内存。

鉴于这些情况,以下是:

//(1)
cv::Mat frame(height + height / 2, width, CV_8UC1, pointerToNV21Data);
//(2)
cv::cvtColor(frame, frame, cv::COLOR_YUV2RGBA_NV21);

分配一个新的连续内存区域,大小为320 * 240 * 4通道 = 307200字节(之前是空闲内存),并且放弃之前分配的320 * 360 * 1通道 = 115200字节。

cv::cvtColor内部调用cv::Mat.create(...)时,会分配新的内存块,因为:

  • cv::cvtColor之前,frame是一个尺寸为320x360、具有1个通道的cv::Mat
  • cv::cvtColor要求它是一个尺寸为320x240、具有4个通道的cv::Mat

所以,我最乐观的解决方案是执行以下操作:

//allocate a byte buffer of 307200 bytes
uchar largeBuffer[307200];

//make sure the 115200 NV21 image bytes are written to the buffer
writeNV21DataToBuffer(largeBuffer);

//create cv::Mat that wraps "largeBuffer" as a NV21 image
cv::Mat frameNV21(height + height / 2, width, CV_8UC1, largeBuffer);

//create a second cv::Mat that wraps around the same "largeBuffer", 
//but this time consider it as pointing to a RGBA image
cv::Mat frameRGBA(height, width, CV_8UC4, largeBuffer);

//call cv::cvtColor
cv::cvtColor(frameNV21, frameRGBA, cv::COLOR_YUV2RGBA_NV21);

通过这样做,确实没有分配新的内存。打印frameNV21frameRGBAcv::cvtColor之前和之后的cv::Mat.dataStart & cv::Mat.dataStart,证实了使用相同的内存区域,并且结果没有为新的内存分配空间。
然而...转换本身并没有正确完成,当在屏幕上显示时,结果看起来像这样:
在这一点上,我已经没有主意了。我的最后一个方法本质上是错误的,还是没有办法实现这个目标?

也许你可以在OpenCV中添加一个新的cvtColor函数,然后重新构建。我认为这可能是更简单的方法。 - Elvis Dukaj
虽然这是一个选项,但我更倾向于不修改OpenCV本身...如果必要的话,我很可能会将新的转换方法添加到我的其中一个实用类中。 - cjurjiu
2个回答

2

是的,您可以在不分配新内存的情况下使用cvtColor:只需使用更大的缓冲区:

//allocate a byte buffer of 307200 + 115200 bytes
uchar largeBuffer[307200 + 115200];

//make sure the 115200 NV21 image bytes are written to the buffer
writeNV21DataToBuffer(largeBuffer + 307200);

//create cv::Mat that wraps "largeBuffer + 307200" as a NV21 image
cv::Mat frameNV21(height + height / 2, width, CV_8UC1, largeBuffer + 307200);

//create a second cv::Mat that wraps around the same "largeBuffer", 
//but this time consider it as pointing to a RGBA image
cv::Mat frameRGBA(height, width, CV_8UC4, largeBuffer);

//call cv::cvtColor
cv::cvtColor(frameNV21, frameRGBA, cv::COLOR_YUV2RGBA_NV21);

如果你的问题是:“我能让就地工作吗?”,意思是没有额外的内存来存储源位图,答案是否定的。至少不用。
问题在于,你需要用覆盖的数据。事实上,如果一切都按顺序进行,你可以将图像的UV部分放在RGBA数据之外,并保存字节。 (不)幸运的是,OpenCV是并行执行cvtColor的,所以你会在奇怪的地方看到错误(可能你会有一些正确转换的水平条纹和一些完全错误的条纹)。所以你需要有一个不同的缓冲区。
转换可以就地完成,但速度更慢,因为你需要重新排列你的数据。

我也认为奇怪的输出可能是由于原始数据被转换后覆盖所致,输出本身的重复模式中有这样的提示......但我不确定 cvtColor 的操作方式,因此不确定是否真的是这种情况。 我刚刚测试了你的答案,它非常好用。谢谢! - cjurjiu

1

这更像是一条评论而不是答案,但我无法将其放入评论中。所以我们来试试这个代码: 在OpenCV中我没有NV21编码器,它只能解码,所以我尝试了这段代码:

cv::Mat ssOrg = cv::imread( "/home/user/ss.png");

cv::Mat ssGray;
cv::cvtColor( ssOrg, ssGray, CV_BGR2YUV_I420);
cv::resize( ssGray, ssGray, cv::Size(320, 360));

uchar largeBuffer[307200];
cv::Mat frameNV21( 360, 320, CV_8UC1, largeBuffer);
cv::Mat frameRGBA( 320, 240, CV_8UC4, largeBuffer);

const uchar * nv21Ptr = ssGray.ptr();
for ( int i = 0; i < 360 * 320; ++i){
    largeBuffer[i] = *nv21Ptr++;
}
cv::cvtColor(frameNV21, frameRGBA, CV_YUV2BGRA_I420);
cv::imshow( "rgb", frameRGBA);

cv::waitKey();

这段代码确实能够工作,所以我认为你的方法是正确的。然而,当我将这行代码 cv::cvtColor(frameNV21, frameRGBA, CV_YUV2BGRA_I420); 改为 cv::cvtColor(frameNV21, frameRGBA, CV_YUV2BGRA_NV21); 时,图像上出现了一些伪影。你能否检查一下是否可以从 NV21 格式转换回来?此外,我在 PC 上进行测试,而你在 Android 上进行测试。

你好,感谢您的帮助。我在另一个项目上使用了这段代码,并进行了一些测试。非常感谢您的帮助,但是有一些问题需要解决。首先,cv::Mat frameRGBA( 320, 240,...); 应该改为 cv::Mat frameRGBA( 240, 320,...);(即行和列应该颠倒),因为行代表高度,而列代表宽度。由于顺序错误,在您的示例中调用 cv::cvtColor 时,仍然会执行新的内存分配,因为 OpenCV 看到需要一个 (240, 320) 大小的 Mat,而 frameRGBA 被定义为 (320, 240) 的 Mat。 - cjurjiu
实际上这似乎是问题所在,当写出(240, 320)时输出结果很混乱,所以我认为我记错了尺寸,于是我把它改成了(320, 240)。我目前测试过了,它可以使用我的错误尺寸,并且正确的尺寸结果与你的一样。 - Hakes
1
由于新的分配,这也意味着转换不是在同一个单一的内存位置上执行的,这就是为什么它在你的测试中(以及在我的初始测试中,直到我看到声明错误的大小)能够正常工作的原因。当在PC和Android上从YUVI420转换回RGBA/BGRA时,我得到了相同的结果。这基本上也证实了@CostantinoGrana在他的答案中所说的(即转换不能在同一内存空间中发生)。当从NV21进行转换时,你得到的伪影是因为NV21与YUVI420不完全兼容。 - cjurjiu

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