第1章:相机与Camera2 Raw格式——DNG输出、Raw数据读取、Raw转RGB、后处理流程
各位同学,今天我们来聊聊Raw格式。说实话,我刚接触Android相机开发时,也被Raw这个概念搞得有点晕。JPEG不香吗?为什么非要折腾Raw?
嗯,这个问题问得好。JPEG确实方便,但它是经过相机ISP(图像信号处理器)处理后的成品。说白了,就像餐厅做好的菜——味道固定了,你想调整咸淡?没门。而Raw呢,相当于厨房里的生食材,你想怎么烹饪都行。
1.1 什么是DNG格式?
DNG(Digital Negative)是Adobe推出的开放Raw格式标准。Android从5.0开始支持DNG输出。我个人习惯用DNG而不是厂商私有的Raw格式,原因很简单——兼容性好,几乎所有后期软件都认它。
在Camera2 API中,启用DNG输出其实不复杂。关键一步是在创建CaptureRequest时,设置正确的输出格式:
// 创建DNG格式的ImageReader
ImageReader dngReader = ImageReader.newInstance(
width, height,
ImageFormat.RAW_SENSOR, // 注意:不是JPEG
2
);
// 在CaptureRequest中设置输出目标
CaptureRequest.Builder requestBuilder =
cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
requestBuilder.addTarget(dngReader.getSurface());
这里有个坑,我曾经踩过——RAW_SENSOR格式的ImageReader,它的Surface不能直接用于预览。你得单独准备一个PreviewSurface。嗯,这个细节很容易被忽略。
1.2 Raw数据的读取
拿到DNG文件后,怎么读取里面的Raw数据?其实DNG本质上是TIFF格式的变种,我们可以用Android的DngCreator来生成DNG文件,但读取时就需要自己解析了。
我个人推荐用libraw或者直接解析TIFF结构。不过对于入门来说,我们先看看怎么从ImageReader拿到原始的像素数据:
Image image = dngReader.acquireLatestImage();
if (image != null) {
// RAW_SENSOR格式的Image只有一个平面
Image.Plane plane = image.getPlanes()[0];
ByteBuffer buffer = plane.getBuffer();
byte[] rawData = new byte[buffer.remaining()];
buffer.get(rawData);
// 注意:这里的rawData是Bayer排列的原始数据
// 每个像素通常是10位、12位或14位
image.close();
}
你想想看,这个rawData里存的是什么?是每个像素点的原始电压值,没有经过任何处理。所以它看起来就是一片灰蒙蒙的,啥也看不清。
关键点:Raw数据是线性空间的,而人眼感知是非线性的。所以直接显示Raw数据,你会得到一张很暗、偏色的图片。这完全正常。
1.3 Raw转RGB——去马赛克(Demosaic)
为什么Raw数据是灰的?因为每个像素只记录了红、绿、蓝中的一种颜色。这就是Bayer排列。最常见的BGGR模式:
| B | G | B | G |
|---|---|---|---|
| G | R | G | R |
| B | G | B | G |
| G | R | G | R |
去马赛克算法就是根据相邻像素,插值出每个像素缺失的颜色信息。最简单的办法是双线性插值:
// 伪代码:双线性插值去马赛克
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 根据Bayer模式判断当前像素是什么颜色
if (isRed(x, y)) {
rgb[x][y].r = raw[x][y];
rgb[x][y].g = averageGreenNeighbors(x, y);
rgb[x][y].b = averageBlueNeighbors(x, y);
}
// 类似处理Green和Blue像素
}
}
当然,实际项目中我不会用这么简单的方法。效果太差了,边缘会有明显的锯齿和伪色。我建议至少用基于梯度的自适应插值算法,或者直接用OpenCV的demosaicing函数。
小技巧:如果你只是想快速验证Raw数据是否正确,可以用Android的BitmapFactory直接加载DNG文件?不行,它不认识DNG。你得先转成RGB,再生成Bitmap。
1.4 后处理流程
Raw转RGB只是第一步。真正的后处理流程,说白了就是模拟ISP干的事。我整理了一个典型的处理管线:
Raw数据 → 黑电平校正 → 白平衡 → 去马赛克 →
色彩校正矩阵 → Gamma校正 → 色调映射 → 输出RGB
每个环节都有讲究。比如黑电平校正,因为传感器有暗电流,即使完全遮光也有一定的输出值。这个值必须减掉:
// 黑电平校正
int blackLevel = 64; // 不同传感器不同,从DNG metadata获取
for (int i = 0; i < rawData.length; i++) {
int corrected = (rawData[i] & 0xFFFF) - blackLevel;
if (corrected < 0) corrected = 0;
rawData[i] = (short) corrected;
}
白平衡呢?我记得有一次项目,在室内荧光灯下拍照,出来的Raw数据偏绿严重。原因就是白平衡增益没算对。正确的做法是从DNG的metadata里读取AsShotNeutral标签:
// 从DNG metadata读取白平衡增益
float[] asShotNeutral = {0.48f, 1.0f, 0.62f}; // 示例值
float rGain = asShotNeutral[1] / asShotNeutral[0];
float bGain = asShotNeutral[1] / asShotNeutral[2];
// 应用到每个像素
rgb.r *= rGain;
rgb.b *= bGain;
注意:白平衡增益不要超过4.0,否则噪点会爆炸。我曾经遇到过增益到8.0的情况,暗部全是彩色噪点,根本没法看。
1.5 完整的处理管线示例
下面是一个简化的完整处理流程,我把它封装成了一个工具类:
public class RawProcessor {
public Bitmap processRaw(byte[] rawData, int width, int height,
CameraMetadata metadata) {
// 1. 黑电平校正
short[] corrected = blackLevelCorrection(rawData, metadata);
// 2. 白平衡
float[] gains = extractWhiteBalanceGains(metadata);
applyWhiteBalance(corrected, gains, width, height);
// 3. 去马赛克
int[] rgb = demosaic(corrected, width, height);
// 4. 色彩校正矩阵
float[] ccm = getColorCorrectionMatrix(metadata);
applyCCM(rgb, ccm, width, height);
// 5. Gamma校正
applyGamma(rgb, 2.2f);
// 6. 色调映射 + 输出Bitmap
return createBitmap(rgb, width, height);
}
}
这个流程看起来步骤多,但其实每一步都不复杂。关键是参数要对——从DNG metadata里读出来的值,不同厂商、不同传感器都不一样。我建议你写代码时,先把metadata打印出来看看,心里有个底。
1.6 避坑指南
最后分享几个我踩过的坑:
- Raw数据位深:别假设一定是10位。有的传感器是12位,有的是14位。从metadata的bitsPerSample读取。
- Bayer模式:不是所有传感器都是BGGR。RGGB、GRBG、GBRG都有可能。metadata里有colorFilterArrangement。
- 内存管理:Raw数据通常很大。4000x3000的12位Raw,一个帧就18MB。ImageReader的maxImages别设太大,2-3就够了。
- 性能问题:在UI线程做Raw处理?想都别想。必须开子线程,最好用RenderScript或者OpenCL加速。
嗯,关于Raw格式的基础知识就这些。说实话,Raw处理是个深坑,但一旦掌握了,你能做出比手机自带相机好得多的效果。毕竟,JPEG是别人帮你做的菜,Raw是你自己下厨——味道由你掌控。
核心总结:Raw处理的关键在于理解每个步骤的物理意义,以及正确地从metadata中提取参数。不要盲目套用固定值,不同手机、不同传感器的参数天差地别。