# 人脸隐私保护

# 体验 demo

选择克隆或下载代码到小程序端 IDE, 更新项目 project.config.json 中的 appid 字段为您的小程序 ID,在当前环境中部署部署 (opens new window) AI 人脸特征分析与检测扩展能力,即可体验人脸隐私保护小程序。亦可扫码体验线上版本。

# 克隆

  1. 确保您已安装 git 客户端。
  2. 在命令行中通过以下指令,克隆 demo 仓库:
    git clone https://github.com/TencentCloudBase/Cloudbase-Examples.git--branch feature/keylessCall
    
  3. 进入 clone 下来的 tcb-demo-ai 目录可见项目代码
    cd ./tcb-demo-ai
    

# 下载

若无 git 客户端,或其他原因,可以选择手动下载方式:点击下载 (opens new window)项目代码包,解压到您喜欢的目录。

# 开发

在小程序 IDE 中打开项目,注意更新 project.config.json 中的 appid 字段为您的小程序 ID。

# 目录说明

  • /client/pages/index - 首页导航目录
  • /client/pages/face-detect/smooth - 人脸隐私保护页面入口
    • /client/pages/face-detect/smooth/util.js - 对人脸区域进行高斯平滑的工具方法,主要包括获取一维高斯核、横向扫描平滑、纵向扫描平滑的方法。
  • /client/libs/ - 依赖库相关
    • /client/libs/runtime.js - 在某页面引用后,可在该页面使用 async/await 语法,主要用于兼容 iOS 手机
    • /client/libs/weui.wxss - weui 样式文件,在人脸识别组件中有使用,可选。
    • /client/libs/tcb-services-mp-sdk - 小程序中使用的 tcb-services-mp-sdk,封装 tcb 相关公共能力及方法,可选。

# 上传图片

  1. smooth/index.wxml #8 (opens new window) 中上传按钮绑定 tap 事件处理函数 handleUploadTap,点击按钮将触发上传图片操作。

    <view class="button-container">
      <button type="primary" bindtap="handleUploadTap">上传图片</button>
    </view>
    
  2. smooth/index.js #21 (opens new window) 中实现 handleUploadTap 方法。通过调用 wx.chooseImage 选择需要上传的图片,获取到图片对象后,使用云开发存储能力,调用 wx.cloud.uploadFile (opens new window) 将图片上传到云端,在成功回调中获取云端文件对象,拿到 fileID 存储在组件 data 中,同时存储 tempFilePathstemUrl 待用,调用 this.recognize() 进行图片分析(下一步将实现该方法)。

    handleUploadTap() {
      // 通过微信 API,选择上传的图片
      wx.chooseImage({
        success: dRes => {
          wx.showLoading({
            title: "上传中"
          });
    
          const fileName = dRes.tempFilePaths[0];
          const dotPosition = fileName.lastIndexOf(".");
          const extension = fileName.slice(dotPosition);
          const cloudPath = `${Date.now()}-${Math.floor(
            Math.random(0, 1) * 10000
          )}${extension}`;
          // 云开发上传图片到云端
          wx.cloud.uploadFile({
            cloudPath,
            filePath: dRes.tempFilePaths[0],
            // 上传成功回调
            success: res => {
              console.log(res);
              // 将 fileID 存在组件 data 上
              this.setData(
                {
                   temUrl: dRes.tempFilePaths[0]
                   fileID: res.fileID,
                   hasUploaded: true
                },
                () => {
                   // 图像识别方法,将在下一步实现
                   this.recognize();
                   wx.hideLoading();
                }
              );
            },
            // 上传失败回调,可用于捕获错误,进行提示
            fail: () => {
              wx.hideLoading();
              wx.showToast({
                title: "上传失败",
                icon: "none"
              });
            }
          });
        }
      });
    }
    

# 图片分析

在上一步,我们将一张本地图片上传到云端,并且获取到了云端 fileID,接下来我们将调用云开发扩展能力解决方案 - AI 人脸特征分析与检测,进行图片分析。首先请确保在云开发中部署了 AI 人脸特征分析与检测能力,详见 AI 人脸特征分析与检测使用指引 (opens new window)

  1. 首先在 smooth/index.js (opens new window) 文件头引入依赖 /client/libs/tcb-services-mp-sdk/client/libs/runtime.js

    import regeneratorRuntime from "../../../libs/runtime";
    import TcbService from "../../../libs/tcb-service-mp-sdk/index";
    const tcbService = new TcbService();
    

    之后实现 recognize 方法。通过 tcbService.callService 方法,传入之前存储于组件 data 中的 fileID,调用 AI 人脸特征分析与检测能力,获取图片识别结果。this.getFaceRect 对识别结果简单处理,获取原图像宽高、人脸相对于图片的位置大小,传递给 this.smooth 方法对人脸区域进行高斯平滑后叠加回原图像。

    async recognize() {
      wx.showLoading({
        title: "识别中",
        icon: "none"
      });
    
      try {
        let result = await tcbService.callService({
          service: "ai",
          action: "tcbService-ai-detectFace",
          data: {
            FileID: this.data.fileID
          }
        });
        /**
        或可简单的直接调用云函数
        let result = await wx.cloud.callFunction({
          name: 'tcbService-ai-detectFace',
          data: {
            FileID: this.data.fileID
          }
        });
        **/
        wx.hideLoading();
    
        if (!result.code && result.data) {
          // 图像识别成功,结果对象 result.data
          let imgInfo = this.getFaceRect(result.data);
          this.smooth(this.data.temUrl, imgInfo);
        } else {
          // 识别失败,抛出错误进行错误处理
          throw result;
        }
      } catch (e) {
        wx.hideLoading();
        wx.showToast({
          title: "识别失败",
          icon: "none"
        });
        console.log(e);
      }
    },
    getFaceRect(res) {
     const { ImageWidth, ImageHeight, FaceInfos } = res;
     let face = FaceInfos[0];
     return {
       imageWidth: ImageWidth,
       imageHeight: ImageHeight,
       rectX: face.X / ImageWidth,
       rectY: face.Y / ImageHeight,
       rectWidth: face.Width / ImageWidth,
       rectHeight: face.Height / ImageHeight
     };
    }
    

# 图像裁剪

smooth/index.wxml #11 (opens new window) 中添加画布容器,用于显示打码后的图像。

<view class="image-container" style="width: {{containerWidth}}rpx">
  <canvas
    canvas-id="canvas"
    style="width: {{containerWidth}}rpx; height: {{containerHeight}}rpx"
    class="canvas"
  ></canvas>
</view>

smooth/index.js (opens new window) 文件头引入依赖 /client/pages/face-detect/smooth/util.js

import { smoothX, smoothY, kernel } from "./util";

smooth/index.js #104 (opens new window) 实现 smooth 方法,使用 canvas (opens new window),获取图像像素信息进行处理:

  1. 由于上传图像体积不定,直接对大图像进行高斯平滑效率极低且意义不大。首先对原图像进行等比缩放,绘制在宽度 200rpx (containerWidth)的 canvas 中,高度计算获得,并存储为 containerHeight rpx,画布宽高 px 大小,通过系统 windowWidth 可以算出。
  2. 绘制完成后,通过 wx.canvasGetImageData (opens new window) 获取图像的像素数据,每四项表示一个像素点的 rgba。通过人脸的相对位置及大小,在全体像素数据中获取人脸像素,使用高斯核对人脸像素,分别 x,y 方向进行 2 次平滑,完成对整个人脸的高斯平滑处理。
  3. 将人脸数据提取与一维 Uint8ClampedArray smoothData 中,使用 wx.canvasPutImageData (opens new window) 将平滑后的人脸图像(smoothData)叠加回原始图像的人脸位置。最终画布上的图像即为打码处理后的图像。

示例代码

smooth(
  url,
  { imageWidth, imageHeight, rectX, rectWidth, rectY, rectHeight }
) {
  wx.showLoading({
    title: "绘图中",
    icon: "none"
  });
  wx.getSystemInfo({
    success: res => {
      let pixelRatio = res.pixelRatio;
      let context = wx.createCanvasContext(`canvas`, this);

      // 画布容器高度,单位 rpx
      let containerHeight = Math.floor(
        (imageHeight / imageWidth) * this.data.containerWidth
      );

      // 画布宽高,单位 px
      let canvasWidthPx = Math.floor(
        (res.windowWidth / 750) * this.data.containerWidth
      );
      let canvasHeightPx = Math.floor(
        (res.windowWidth / 750) * containerHeight
      );

      // 人脸的像素位置/大小
      rectX = Math.floor(rectX * canvasWidthPx);
      rectY = Math.floor(rectY * canvasHeightPx);
      rectWidth = Math.floor(rectWidth * canvasWidthPx);
      rectHeight = Math.floor(rectHeight * canvasHeightPx);

      this.setData({ containerHeight }, () => {
        // 绘制原图并缩放充满画布
        context.drawImage(
          url,
          0,
          0,
          imageWidth,
          imageHeight,
          0,
          0,
          canvasWidthPx,
          canvasHeightPx
        );
        context.draw(false, function() {
          // 回调完成后 绘制实际上可能未完成,
          // 导致获取像素不稳定,延时之后再进行下一步
          setTimeout(() => {
            // 获取画布上的像素信息
            wx.canvasGetImageData({
              canvasId: "canvas",
              x: 0,
              y: 0,
              width: canvasWidthPx,
              height: canvasHeightPx,
              success: ({ width, height, data }) => {
                try {
                  // 对人脸的部分进行高斯平滑,并提取人脸的像素
                  let gKernel = kernel(Math.floor(rectWidth / 15));

                  let smoothData = new Uint8ClampedArray(
                    rectWidth * rectHeight * 4
                  );
                  for (let x = rectX; x < rectX + rectWidth; x++) {
                    for (let y = rectY; y < rectY + rectHeight; y++) {
                      let pointIndex = (x + y * width) * 4;
                      smoothX(
                        pointIndex,
                        gKernel,
                        y * width * 4,
                        ((1 + y) * width - 1) * 4,
                        data
                      );
                    }
                  }
                  for (let x = rectX; x < rectX + rectWidth; x++) {
                    for (let y = rectY; y < rectY + rectHeight; y++) {
                      let pointIndex = (x + y * width) * 4;
                      smoothY(pointIndex, gKernel, width, data);
                    }
                  }
                  for (let x = rectX; x < rectX + rectWidth; x++) {
                    for (let y = rectY; y < rectY + rectHeight; y++) {
                      let pointIndex = (x + y * width) * 4;
                      let smoothIndex =
                        (x - rectX + (y - rectY) * rectWidth) * 4;
                      smoothData[smoothIndex] = data[pointIndex];
                      smoothData[smoothIndex + 1] = data[pointIndex + 1];
                      smoothData[smoothIndex + 2] = data[pointIndex + 2];
                      smoothData[smoothIndex + 3] = data[pointIndex + 3];
                    }
                  }

                  // 将平滑后的人脸像素输出到画布
                  wx.canvasPutImageData({
                    canvasId: "canvas",
                    x: rectX,
                    y: rectY,
                    width: rectWidth,
                    height: rectHeight,
                    data: smoothData,
                    fail: e => {
                      console.log(e);
                    }
                  });
                } catch (e) {
                  console.log(e);
                } finally {
                  wx.hideLoading();
                }
              }
            });
          }, 200);
        });
      });
    },
    fail: () => {
      wx.hideLoading();
    }
  });
}

编译项目,可以实现从文件上传,到人脸识别,绘制图像到 canvas 并对人脸部位进行高斯模糊的能力。