Flutter和OpenCV联动处理图片

1,655 阅读2分钟

背景

自制的图片处理App开始接入OpenCV的一些能力了,比如自动均衡,边缘检测,后期可能利用dnn模块直接接入一些AI模块。所以简单梳理了一下Flutter和OpenCV交互的流程

image.png

获取图片的像素数据

和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并不会为图片像素数据分配新的内存,一旦你将imageDatafree,使用这个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);

这里需要注意两点:

  1. 这里是通过逐行的方式写入CVPixelBuffer的,因为CVPixelBuffer为了内存对齐,每行像素之间的内存地址未必是连续的
  2. 对于output.data的使用必须和output在一个作用域,因为一旦离开output的作用域,output.data就变成野指针了