使用Camera2获取depth图像

2022-07-11 12:07:16 浏览数 (1)

文章翻译自plluke的"在三星S10 5G上使用3D摄像头",想了解更多的小伙伴可以查看英文原文

背景(双关语)

“隐私模式”或背景模糊的概念很好理解。视觉效果类似于bokeh,但商业价值是隐私,防止信息泄露和整体视觉氛围。像这样:

技术的关键点是生成一个遮罩,将要模糊的区域和不模糊的区域分开。直观的说,如果知道图像中每个像素的距离,就可以生成此遮罩,但距离并不是唯一的方法,还可以利用经过训练的神经网络来区分前景和背景,而无需任何距离信息。不过这种就是另一篇文章了

我们利用三星S10 5G这款手机上的3D摄像头(以下简称ToF摄像头)来进行演示,相关代码都已经上传到github

什么是ToF(Time-of-Flight)

ToF技术是指通过跟踪光束到达某一点所需的时间来测量到达该点的距离。光速是恒定的,所以一旦你知道了时间,也就知道了距离。ToF相机就是利用这种原理来跟踪传感器区域距离。

有不同的方法来计算经过的时间(S10 5G使用红外载波相移检测,940nm iirc),但基本理论是保持不变的。这种方法与其它流行方法(例如苹果真深度相机中使用的结构光)相比各有优缺点,但就我们的目的而言,它只是距离数据的另一个来源

ToF相机

三星S10 5G的前置ToF传感器是索尼IMX316,它以分辨率为240x180的DEPTH16图像格式输出帧。它的视场为75°,大致与S10 5G前置摄像头的80°视场相匹配

Note: S10 5G(以及Note10 5G)通过Camera2 API返回两个摄像头。这两个摄像头实际上都来自同一个传感器,6.5MP摄像头只是10MP摄像头的一部分。如果想自己实现遮罩,需要确保使用10MP相机的帧

通过CameraCharacteristics找到支持TOF的摄像头id

代码语言:javascript复制
for (String camera : cameraManager.getCameraIdList()) {
    CameraCharacteristics chars = cameraManager.getCameraCharacteristics(camera);
    final int[] capabilities = chars.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
    boolean facingFront = chars.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT;
    boolean depthCapable = false;
    for (int capability : capabilities) {
        boolean capable = capability == CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT;
        depthCapable = depthCapable || capable;
    }
    if (depthCapable && facingFront) {
        // Note that the sensor size is much larger than the available capture size
        SizeF sensorSize = chars.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
        Log.i(TAG, "Sensor size: "   sensorSize);

        // Since sensor size doesn't actually match capture size and because it is
        // reporting an extremely wide aspect ratio, this FoV is bogus
        float[] focalLengths = chars.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
        if (focalLengths.length > 0) {
            float focalLength = focalLengths[0];
            double fov = 2 * Math.atan(sensorSize.getWidth() / (2 * focalLength));
            Log.i(TAG, "Calculated FoV: "   fov);
        }
        return camera;
    }
}

一旦查找到了具体的摄像头id,就可以像打开其它相机的流程一样打开它。由于DEPTH16不是一种很好的直接预览格式,我们将ImageReader添加到预览会话并从中直接读取帧

提取深度信息

一旦拿到DEPTH16格式的Image图像,我们就可以拿到每个像素的一个范围(距离)和一个置信度

官方关于DEPTH16格式的文档有十分详细的解释

下面是一个基于Image对象生成int[]掩码的示例

代码语言:javascript复制
private int[] getDepthMask(Image image) {
    ShortBuffer shortDepthBuffer = image.getPlanes()[0].getBuffer().asShortBuffer();
    int[] mask = new int[WIDTH * HEIGHT];
    for (int y = 0; y < HEIGHT; y  ) {
        for (int x = 0; x < WIDTH; x  ) {
            int index = y * WIDTH   x;
            short depthSample = shortDepthBuffer.get(index);
            int newValue = extractRange(depthSample, 0.1);
            mask[index] = newValue;
        }
    }
    return mask;
}
代码语言:javascript复制
private int extractRange(short sample, float confidenceFilter) {
    int depthRange = (short) (sample & 0x1FFF);
    int depthConfidence = (short) ((sample >> 13) & 0x7);
    float depthPercentage = depthConfidence == 0 ? 1.f : (depthConfidence - 1) / 7.f;
    return depthPercentage > confidenceFilter ? depthRange : 0;
}

可以尝试过滤掉更高的置信度,但对于隐私模糊功能,我发现最好让所有的置信度值都通过(0除外),然后再进行一些信号处理。将置信度最小值设置得更高会在一定程度上减少总体噪声,但这样会删除太多有用的信息

可视化深度信息

这里的方法是简单的将深度信息归一化到[0, 255]

代码语言:javascript复制
private int normalizeRange(int range) {
     float normalized = (float)range - RANGE_MIN;
     // Clamp to min/max
     normalized = Math.max(RANGE_MIN, normalized);
     normalized = Math.min(RANGE_MAX, normalized);
     // Normalize to 0 to 255
     normalized = normalized - RANGE_MIN;
     normalized = normalized / (RANGE_MAX - RANGE_MIN) * 255;
     return (int)normalized;
}

然后将其分配给ARGB像素的绿色通道

代码语言:javascript复制
private Bitmap convertToRGBBitmap(int[] mask) {
    Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_4444);
    for (int y = 0; y < HEIGHT; y  ) {
        for (int x = 0; x < WIDTH; x  ) {
            int index = y * WIDTH   x;
            bitmap.setPixel(x, y, Color.argb(255, 0, mask[index],0));
        }
    }
    return bitmap;
}

拿到每帧的bitmap后就可以渲染到TextureView,相机输出的帧是横向方向的,因此需要做一定的旋转处理(具体的实现可以参考github提供的demo)

代码语言:javascript复制
Canvas canvas = textureView.lockCanvas();
canvas.drawBitmap(bitmap, transform, null);
textureView.unlockCanvasAndPost(canvas);

一旦你做完上述所有步骤,就可以得到预览了

帧与帧之间的抖动很明显,可以看到在我的头发和脸部两侧看到非常多的绿色像素(表示距离很近)。如果我们用这个来做模糊遮罩,看起来会很糟糕,所以需要做下平滑处理

接下来作者提供了几种平滑处理的方案,这里的过程翻译就省略掉了,感兴趣的同学可以直接看作者提供的代码。演示效果如下:

隐私模式

利用深度遮罩对相机帧应用模糊,将其转换为预览和编码并发送到实时视频会议服务,还需要做一些其它的处理工作

  • 如果要以16:9的纵横比拍摄前置摄像头,需要将深度遮罩也裁剪为16:9
  • 使用遮罩进行选择性模糊(我的方法是将图像缩小到1/2宽 x 1/2高,应用模糊,然后再放大,然后根据遮罩将原始图像的像素复制回模糊图像,同时沿边缘为像素应用混合渐变,以便从模糊到未模糊的过渡看起来不刺耳)
  • 复用byte buffers和使用YUV/RGB格式
  • 协调多个摄像头的启动/关闭,以及在切换到和退出隐私模式时管理变换(通过drawBitmap进行渲染代价非常大,非必要不使用)

最后贴一个演示效果

作者: plluke

Working with the 3D Camera on the Samsung S10 5G

https://medium.com/swlh/working-with-the-3d-camera-on-the-samsung-s10-5g-4782336783c

https://github.com/plluke/tof

~~END~~

0 人点赞