背景
自制的图片处理App开始接入OpenCV的一些能力了,比如自动均衡,边缘检测,后期可能利用dnn模块直接接入一些AI模块。所以简单梳理了一下Flutter和OpenCV交互的流程
获取图片的像素数据
和OpenCV交互的关键在于对像素格式和内存管理的理解,在App中我通过下面的代码解码图片,得到原始像素数据
NSImage *image = [NSImage.alloc initWithContentsOfFile:imagePath];
self.width = image.size.width;
self.height = image.size.height;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
void *imageData = malloc( self.height * self.width * 4 );
CGContextRef context = CGBitmapContextCreate( imageData, self.width, self.height, 8, 4 * self.width, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big );
CGColorSpaceRelease( colorSpace );
CGContextClearRect( context, CGRectMake( 0, 0, self.width, self.height ) );
CGRect bounds=CGRectMake( 0, 0, self.width, self.height );
NSGraphicsContext* gctx = [NSGraphicsContext graphicsContextWithCGContext:context flipped:NO];
[NSGraphicsContext setCurrentContext:gctx];
[image drawInRect:NSRectFromCGRect(bounds)];
这里是在macos平台,iOS其实也类似
通过CoreGraphics
我们得到了原始像素数据imageData
,他的格式是RGBA,每个像素由4个字节表示
OpenCV处理图片
cv::Mat的初始化
使用上面的imageData初始化一个cv::Mat
对象
cv::Mat cvImage(inputTexture.height, inputTexture.width, CV_8UC4, imageData);
注意,这里的cv::Mat
并不会为图片像素数据分配新的内存,一旦你将imageData
free,使用这个cv::Mat
的代码会出现严重的内存访问问题。你可以通过cvImage.clone()
创建新的cv::Mat
对象,从而摆脱对imageData
的依赖。还有一点,这里初始化出来的图片是RGBA格式的,如果你的算法需要使用BGR格式,可以通过下面的代码转换
cv::Mat bgrImage;
cv::cvtColor(cvImage, bgrImage, cv::COLOR_RGBA2BGR);
处理后的数据回传到Flutter
比如我们使用边缘检测,处理完图片
cv::Mat output;
cv::Canny(cvImage, output, 32, 128);
cv::cvtColor(output, output, cv::COLOR_GRAY2RGBA);
需要将图片格式再转换成RGBA,因为我们需要将数据回写到Flutter的CVPixelBuffer
中
CVPixelBufferLockBaseAddress(pixelBuffer, 0);
size_t lines = CVPixelBufferGetHeight(pixelBuffer);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer);
int srcBytesPerRow = imageWidth * 4;
uint8_t *addr = CVPixelBufferGetBaseAddress(pixelBuffer);
for (int line = 0; line < lines; ++line) {
memcpy(addr + bytesPerRow * line, output.data + srcBytesPerRow * line, srcBytesPerRow);
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
这里需要注意两点:
- 这里是通过逐行的方式写入
CVPixelBuffer
的,因为CVPixelBuffer
为了内存对齐,每行像素之间的内存地址未必是连续的 - 对于
output.data
的使用必须和output
在一个作用域,因为一旦离开output
的作用域,output.data
就变成野指针了