在最近的一個項目中,需要實現 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 函數得到圖像的寬度,後面會用得上。具體如以下代碼:
DataBufferByte dataBufferByte = new DataBufferByte(new byte[][] {dataBytes}, dataSize); // 這裡假定 dataBytes 保存了本地獲取的圖像數據,dataSize 為圖像數據大小(總是比“w * h * 每像素字節數”計算出來要大一點)
ComponentSampleModel componentSampleModel = new ComponentSampleModel(DataBuffer.TYPE_BYTE, width, height, 4, bytesPerRow, new int[] {1, 2, 3, 0}); // 自定義 BufferedImage 中的圖像格式,還是以字節來存儲每個像素,具體構造函數的說明見 javadoc api
WritableRaster writableRaster = Raster.createWritableRaster(componentSampleModel, dataBufferByte, new Point(0, 0)); // 創建包含具體圖像數據的柵格陣列
ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB); // 創建 RGB 顏色空間
int[] nBits = {8, 8, 8, 8}; // 對應源圖像數據的 ARGB,因為源圖像數據采用 32ARGB,等效於 4 個字節,每個字節按順序分別表示 alpha、red、green、blue
ColorModel colorModel = new ComponentColorModel(colorSpace, nBits, true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); // 創建顏色模式
BufferedImage bufferedImage = new BufferedImage(colorModel, writableRaster, false, null); // 構建自定義像素格式的 BufferedImage
這種方式就不會出現問題了,本地庫捕獲的實時攝像頭圖像能夠正確填充到 Java 的 BufferedImage 中,而且效率非常高。