尝试 AI 人脸关键点算法实现一下 Android 人脸匿名功能

2020-07-17 11:51:48 +08:00
 exmorning

什么是人脸匿名( Face Anonymization )

随着人脸识别技术的普及,人脸数据的隐私问题也得到越来越多关注,针对隐私保护的研究也陆续出现。目前大致有下面几个方向

  1. 篡改输入人脸识别系统的图像。
  2. 生成式对抗网络(GAN)来匿名某人的照片或视频。
  3. 直接模糊人脸识别到的人脸

本文主要讲第 3 点,讲讲怎么使用移动端人脸关键点算法实现人脸匿名功能。这种方法对设备要求低,代码简单易懂,修改后就可直接落地。

下图就是最终想实现的功能

什么是人脸关键点算法( Face Landmarks )

人脸关键点检测是人脸相关算法中的关键一环,它是人脸识别、表情分析、3D 人脸重建,表情驱动 3D 动画等一系列人脸相关问题的前提。

我们将使用 TengineKit 来实现人脸匿名功能

TengineKit

免费移动端实时人脸 212 关键点 SDK 。是一个易于集成的人脸检测和人脸关键点 SDK 。它可以在各种手机上以非常低的延迟运行。
https://github.com/OAID/TengineKit

TengineKit 效果图

实现

配置 Gradle

Project 中的 build.gradle 添加

    repositories {
        ...
        mavenCentral()
        ...
    }

    allprojects {
        repositories {
            ...
            mavenCentral()
            ...
        }
    }

主 Module 中的 build.gradle 添加

    dependencies {
        ...
        implementation 'com.tengine.android:tenginekit:1.0.3'
        ...
    }

配置 manifests

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET"/>

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.FLASHLIGHT" />

    <uses-feature android:name = "android.hardware.camera" android:required="true"/>
    <uses-feature android:name = "android.hardware.camera.autofocus" />

初始化 Android Camera

为 App 创建自定义摄像头界面的步骤如下:

  1. 检测和访问 Camera
  2. 创建预览 TextureView
  3. 构建预览 TextureView 布局
  4. 将 Camera 和 TextureView 绑定
  5. 启动预览

我们先 new 一个 TextureView.SurfaceTextureListener,在里面完成 camera 的初始配置,当 TextureView 可用的时候,onSurfaceTextureAvailable 中的代码将被调用

   private final TextureView.SurfaceTextureListener surfaceTextureListener = new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(final SurfaceTexture texture, final int width, final int height) {
            int index = getCameraId();
            camera = Camera.open(index);

            try {
                Camera.Parameters parameters = camera.getParameters();
                List<String> focusModes = parameters.getSupportedFocusModes();
                if (focusModes != null && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
                    parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
                }
                List<Camera.Size> cameraSizes = parameters.getSupportedPreviewSizes();
                Size[] sizes = new Size[cameraSizes.size()];
                int i = 0;
                for (Camera.Size size : cameraSizes) {
                    sizes[i++] = new Size(size.width, size.height);
                }
                Size previewSize = CameraConnectionFragment.chooseOptimalSize(sizes, desiredSize.getWidth(), desiredSize.getHeight());
                parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight());
                camera.setDisplayOrientation(90);
                camera.setParameters(parameters);
                camera.setPreviewTexture(texture);
            } catch (IOException exception) {
                camera.release();
            }

            camera.setPreviewCallbackWithBuffer(imageListener);
            Camera.Size s = camera.getParameters().getPreviewSize();
            camera.addCallbackBuffer(new byte[ImageUtils.getYUVByteSize(s.height, s.width)]);

            textureView.setAspectRatio(s.height, s.width);
            camera.startPreview();
        }

        @Override
        public void onSurfaceTextureSizeChanged(final SurfaceTexture texture, final int width, final int height) {
        }

        @Override
        public boolean onSurfaceTextureDestroyed(final SurfaceTexture texture) {
            return true;
        }

        @Override
        public void onSurfaceTextureUpdated(final SurfaceTexture texture) {
        }
    };

此处将 textureView 和 camera 联系起来

    textureView.setSurfaceTextureListener(surfaceTextureListener);

当 camera 启动预览,textureView 得到真实的 size 后。我们得到了 camera 的输出视频流的宽高和预览 textureView,将其保存起来,后续有用到。

    textureView.setRealSizeListener(new AutoFitTextureView.RealSizeListener() {
        @Override
        public void onRealSizeMeasure(int w, int h) {
            if(!isReady){
                isReady = true;
                Camera.Size s = camera.getParameters().getPreviewSize();
                cameraReadyListener.onCameraReady(
                        s.width, s.height,w, h
                );
            }
        }
    });

处理 Camera 传过来的视频流

首先我们先初始化 TengineKit:

  1. 选用 camera 处理模式
  2. 打开人脸检测和人脸关键点功能
  3. 设置视频流格式为 YUV_NV21 ( Android camera 默认格式)
  4. 设置输入视频流的宽高,此处为 camera 的预览宽高
  5. 设置输出视频流的宽高,此处为 textrureView 的宽高
  6. 设置输入视频流来自前置摄像头
    com.tenginekit.Face.init(getBaseContext(),
            AndroidConfig.create()
                    .setCameraMode()
                    .openFunc(AndroidConfig.Func.Detect)
                    .openFunc(AndroidConfig.Func.Landmark)
                    .setInputImageFormat(AndroidConfig.ImageFormat.YUV_NV21)
                    .setInputImageSize(previewWidth, previewHeight)
                    .setOutputImageSize(outputWidth, outputHeight)
    );
    com.tenginekit.Face.Camera.switchCamera(false);

处理数据

  1. 得到手机旋转角度,将其设置到 TengineKit
  2. 开始检测,当检测到人脸数目大于 0 的时候,调用 faceDetect.landmark2d(),得到人脸关键点链表
    int degree = CameraEngine.getInstance().getCameraOrientation(sensorEventUtil.orientation);

    com.tenginekit.Face.Camera.setRotation(degree - 90, false,
            outputWidth, outputHeight);

    com.tenginekit.Face.FaceDetect faceDetect = Face.detect(data);
    faceLandmarks = null;
    if(faceDetect.getFaceCount() > 0){
        faceLandmarks = faceDetect.landmark2d();
    }

高斯模糊和绘制

这里使用 Android 的 bitmap 来实现功能,这种做法比较粗糙,性能差,但是简单易懂,如果读者有兴趣可以使用 OpenGLES 来实现此功能。

  1. 将从摄像头中得到的 yuv 数据通过 TengineKit 的图片帮助函数转化为 Bitmap
  2. 通过人脸关键点的外接框,裁剪 bitmap 得到人脸的 bitmap 数组
  3. 将得到的人脸 bitmap 进行高斯模糊
    if(testBitmap != null){
        testBitmap.recycle();
    }
    testBitmap = Face.Image.convertCameraYUVData(
            data,
            previewWidth, previewHeight,
            outputWidth, outputHeight,
            - 90,
            true);


    for(Bitmap bitmap : testFaceBitmaps){
        bitmap.recycle();
    }
    testFaceBitmaps.clear();
    if(testBitmap != null && faceDetect.getFaceCount() > 0){
        if(faceLandmarks != null){
            for (int i = 0; i < faceLandmarks.size(); i++) {
                    Bitmap face = BitmapUtils.getDstArea(testBitmap, faceLandmarks.get(i).getBoundingBox());
                    face = BitmapUtils.blurByGauss(face, 50);
                    testFaceBitmaps.add(face);
            }
        }
    }

    runInBackground(new Runnable() {
        @Override
        public void run() {
            trackingOverlay.postInvalidate();
        }
    });

trackingOverlay 为定制的 view,将 canvas 暴露出来用于画 bitmap

    trackingOverlay.addCallback(new OverlayView.DrawCallback() {
        @Override
        public void drawCallback(final Canvas canvas) {
            if(testBitmap != null){
                canvas.drawBitmap(testBitmap, 0,0, circlePaint);
            }
            if(faceLandmarks != null){
                for (int i = 0; i < faceLandmarks.size(); i++) {
                    Rect r = faceLandmarks.get(i).getBoundingBox();
                    canvas.drawRect(r, circlePaint);
                    canvas.drawBitmap(testFaceBitmaps.get(i), r.left, r.top, circlePaint);
                }
            }
        }
    });

效果

Demo

参考

https://github.com/OAID/TengineKit

源码

https://github.com/jiangzhongbo/TengineKit_Demo_Identity_Protection

知乎

https://zhuanlan.zhihu.com/p/161038093

13577 次点击
所在节点    Android
32 条回复
Jirajine
2020-07-17 12:20:55 +08:00
那么如何确保原始人脸数据不被这个 sdk 偷走呢。
aabbcc112233
2020-07-17 12:45:29 +08:00
还是上次那个小姐姐吗?感觉颜值降低了 1 分。另外她的微信号你还没发给我
exmorning
2020-07-17 13:07:25 +08:00
@Jirajine 目前就 auth 了一下,不上传任何数据
exmorning
2020-07-17 13:12:03 +08:00
@aabbcc112233 不是给了抖音号可以发私信嘛,微信号得自己要啊
meilande
2020-07-17 17:01:37 +08:00
用 bitmap 性能不是很好,正在商用得用 OpenGL 写
exmorning
2020-07-17 17:26:30 +08:00
@meilande 如果想实现下 OpenGL,可以参考 https://github.com/CainKernel/CainCamera
2kCS5c0b0ITXE5k2
2020-07-17 18:03:26 +08:00
对于某些视频很实用啊
CoderGeek
2020-07-17 18:37:49 +08:00
再整点换脸 QvQ
exmorning
2020-07-17 20:53:11 +08:00
@emeab 你启发了我!
exmorning
2020-07-17 20:53:45 +08:00
@CoderGeek 下一篇考虑写一个换脸的 demo
JackCui
2020-07-17 21:05:53 +08:00
跟 landmark 有什么关系?不就是检测个人脸 bbox,然后加个模糊吗?
exmorning
2020-07-17 21:13:42 +08:00
@JackCui 用关键点外接框做的,如果用人脸检测框,打码没有这么稳定
CoderGeek
2020-07-18 12:25:30 +08:00
@exmorning 赞~
exmorning
2020-07-18 19:59:35 +08:00
wangxiaoaer
2020-07-22 12:31:00 +08:00
拦截了摄像头的视频流然后识别,模糊?
exmorning
2020-07-22 12:36:58 +08:00
@wangxiaoaer 中间加一步人脸关键点
wangxiaoaer
2020-07-22 14:21:33 +08:00
@exmorning #16 正常情况 Android 允许摄像头被拦截? 支付宝、微信等应用都是自己直接调用摄像头还管用吗?

另外你第一张图(最终想实现的功能)的视频源不是用户自己的设备,而是商家的设备,这种情况除了自己带个面具外我想不出来任何匿名的可能性。
exmorning
2020-07-22 14:34:55 +08:00
@wangxiaoaer 这是一个例子,阐述 SDK 落地的一种可能性
xmoiduts
2020-07-22 15:05:43 +08:00
最近也在做抹脸内容作为毕设的一部分,迫于场景内人脸 /人头挺小的,决定选用方案:hog+svm 扫头 /(塑料级) cnn 二次判定 /tracker 跟踪-在一段时间内弥补漏检。

反正挺慢的,希望有硬件加速……
exmorning
2020-07-22 15:09:05 +08:00

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/690842

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX