V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
GeekHub
exmorning
V2EX  ›  Android

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

  •  1
     
  •   exmorning · 75 天前 · 3907 次点击
    这是一个创建于 75 天前的主题,其中的信息可能已经有所发展或是发生改变。

    什么是人脸匿名( Face Anonymization )

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

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

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

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

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

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

    face landmarks

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

    TengineKit

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

    TengineKit 效果图

    demo1

    实现

    配置 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 demo

    Demo

    demo

    参考

    https://github.com/OAID/TengineKit

    源码

    https://github.com/jiangzhongbo/TengineKit_Demo_Identity_Protection

    知乎

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

    32 条回复    2020-07-24 14:34:46 +08:00
    Jirajine
        1
    Jirajine   75 天前 via Android
    那么如何确保原始人脸数据不被这个 sdk 偷走呢。
    aabbcc112233
        2
    aabbcc112233   75 天前
    还是上次那个小姐姐吗?感觉颜值降低了 1 分。另外她的微信号你还没发给我
    exmorning
        3
    exmorning   75 天前
    @Jirajine 目前就 auth 了一下,不上传任何数据
    exmorning
        4
    exmorning   75 天前
    @aabbcc112233 不是给了抖音号可以发私信嘛,微信号得自己要啊
    meilande
        5
    meilande   75 天前
    用 bitmap 性能不是很好,正在商用得用 OpenGL 写
    exmorning
        6
    exmorning   75 天前
    @meilande 如果想实现下 OpenGL,可以参考 https://github.com/CainKernel/CainCamera
    emeab
        7
    emeab   75 天前   ❤️ 2
    对于某些视频很实用啊
    CoderGeek
        8
    CoderGeek   75 天前
    再整点换脸 QvQ
    exmorning
        9
    exmorning   75 天前
    @emeab 你启发了我!
    exmorning
        10
    exmorning   75 天前
    @CoderGeek 下一篇考虑写一个换脸的 demo
    JackCui
        11
    JackCui   75 天前
    跟 landmark 有什么关系?不就是检测个人脸 bbox,然后加个模糊吗?
    exmorning
        12
    exmorning   75 天前
    @JackCui 用关键点外接框做的,如果用人脸检测框,打码没有这么稳定
    CoderGeek
        13
    CoderGeek   74 天前
    @exmorning 赞~
    exmorning
        14
    exmorning   74 天前
    wangxiaoaer
        15
    wangxiaoaer   70 天前 via iPhone
    拦截了摄像头的视频流然后识别,模糊?
    exmorning
        16
    exmorning   70 天前
    @wangxiaoaer 中间加一步人脸关键点
    wangxiaoaer
        17
    wangxiaoaer   70 天前
    @exmorning #16 正常情况 Android 允许摄像头被拦截? 支付宝、微信等应用都是自己直接调用摄像头还管用吗?

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

    反正挺慢的,希望有硬件加速……
    imn1
        21
    imn1   70 天前
    @JackCui #11
    没去看或者说看不懂代码
    如果 sdk 的步骤分得比较细,可以重写后处理(模糊)的模块,变成伪造数据(不一定是整体换脸),这就很多可以玩的功能了
    luckyrayyy
        22
    luckyrayyy   70 天前
    小姐姐我爱了
    LiuJiang
        23
    LiuJiang   70 天前
    前天做了个变脸小程序,蛮有意思的,依靠腾讯云人脸五官识别(每个月 1 万次额度)或 face-api,完成的。
    exmorning
        25
    exmorning   70 天前
    @imn1 是的,这一步看开发者的想象力了
    yongzhao106
        26
    yongzhao106   69 天前
    哎呦 不错喔
    yongzhao106
        27
    yongzhao106   69 天前
    B 站也有一个相似的人脸识别视频也挺火的:深入解析 Android 人工智能-人脸检测追踪技术
    Pay4Dealer
        28
    Pay4Dealer   69 天前
    nice
    locoz
        29
    locoz   69 天前 via Android
    @wangxiaoaer #17 很简单,结合 xposed 之类的 hook 框架实现对系统 API 的劫持,在摄像头 API 返回前就做处理,微信支付宝再牛逼也没用。
    exmorning
        30
    exmorning   69 天前
    @locoz 好思路
    Liyiw
        31
    Liyiw   68 天前
    这个用目标检测检测出人脸矩形就可以做了吧,为什么要用到关键点?
    exmorning
        32
    exmorning   68 天前
    @Liyiw 关键点外接框更稳定
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1369 人在线   最高记录 5168   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 00:27 · PVG 08:27 · LAX 17:27 · JFK 20:27
    ♥ Do have faith in what you're doing.