在最近的一个项目中,需要实现 Mac OS X 环境下的摄像头图像实时捕获并转换为 Java 中的 BufferedImage 对象。首先通过开发一个本地库实现 Mac OS X 的摄像头图像捕获,采用的是 Apple 推荐的新的 AVFoundation 框架,摄像头图像格式设置为 kCVPixelFormatType_32ARGB(设置成其他的测试了无法得到图像,系统不支持),通过 delegate 方式得到 CMSampleBufferRef 类型的 sample buffer 后,需要通过 CMSampleBufferGetImageBuffer 函数将其转换为 CVImageBufferRef 类型的图像缓冲(因为这里捕获的是图像数据,并不是采样数据,所以不能用 CMSampleBufferGetDataBuffer)。然后通过 CVPixelBufferGetBaseAddress 函数得到图像缓冲的首地址,用 CVPixelBufferGetDataSize 函数得到图像缓冲区数据大小,但这里千万要注意,不要以为这时获取的图像缓冲区数据就可以通过Java 的 Raster 和 DataBuffer 等方式来直接填充 Java 中的 BufferedImage(这里假定 BufferedImage 采用 TYPE_INT_ARGB,因为想着对应 kCVPixelFormatType_32ARGB)。因为这么做了会发现图像颜色完全是错乱的,事实上,我们通过计算就可以知道,对应高、宽下的 TYPE_INT_ARGB 格式的 BufferedImage 图像数据大小为 width × height × 4 字节(因为这时一个像素为 int 类型,4 字节大小),但 CVPixelBufferGetDataSize 得到的图像数据大小总比前者要多 4 个字节,可能是保存了其他的一些信息。所以这里直接创建 TYPE_INT_ARGB 格式的 BufferedImage 然后用 Raster 以及 DataBuffer 等方式进行填充是行不通的。
这里就需要采取另外的方式,用 DataBufferByte、ComponentSampleModel、WritableRaster、ColorSpace、ColorModel 来构建 BufferedImage,其实也是使用了 BufferedImage 的另一种不常用的、但效率非常高的构造函数模式。当然,前提是还需要通过 CVPixelBufferGetBytesPerRow 函数得到图像中每个扫描行的字节数,通过 CVPixelBufferGetHeight 函数得到图像的高度,通过 CVPixelBufferGetWidth 函数得到图像的宽度,后面会用得上。具体如以下代码:
1 DataBufferByte dataBufferByte = new DataBufferByte(new byte[][] {dataBytes}, dataSize); // 这里假定 dataBytes 保存了本地获取的图像数据,dataSize 为图像数据大小(总是比“w * h * 每像素字节数”计算出来要大一点) 2 ComponentSampleModel componentSampleModel = new ComponentSampleModel(DataBuffer.TYPE_BYTE, width, height, 4, bytesPerRow, new int[] {1, 2, 3, 0}); // 自定义 BufferedImage 中的图像格式,还是以字节来存储每个像素,具体构造函数的说明见 javadoc api 3 WritableRaster writableRaster = Raster.createWritableRaster(componentSampleModel, dataBufferByte, new Point(0, 0)); // 创建包含具体图像数据的栅格阵列 4 ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB); // 创建 RGB 颜色空间 5 int[] nBits = {8, 8, 8, 8}; // 对应源图像数据的 ARGB,因为源图像数据采用 32ARGB,等效于 4 个字节,每个字节按顺序分别表示 alpha、red、green、blue 6 ColorModel colorModel = new ComponentColorModel(colorSpace, nBits, true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); // 创建颜色模式 7 BufferedImage bufferedImage = new BufferedImage(colorModel, writableRaster, false, null); // 构建自定义像素格式的 BufferedImage
这种方式就不会出现问题了,本地库捕获的实时摄像头图像能够正确填充到 Java 的 BufferedImage 中,而且效率非常高。