第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模式:

BGBG
GRGR
BGBG
GRGR

去马赛克算法就是根据相邻像素,插值出每个像素缺失的颜色信息。最简单的办法是双线性插值:

// 伪代码:双线性插值去马赛克
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是你自己下厨——味道由你掌控。

Camera2 Raw处理管线流程图 DNG / RAW_SENSOR 黑电平校正 → 白平衡 → 去马赛克 色彩校正矩阵(CCM) → Gamma校正 色调映射 → 输出RGB/Bitmap 关键metadata参数:bitsPerSample, colorFilterArrangement, blackLevel, asShotNeutral, colorMatrix1 每个环节的参数都从DNG metadata中读取,不同传感器差异很大

核心总结:Raw处理的关键在于理解每个步骤的物理意义,以及正确地从metadata中提取参数。不要盲目套用固定值,不同手机、不同传感器的参数天差地别。

公众号:蓝海资料掘金营,微信deep3321