# 智能裁剪

# 体验 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/clip - 智能裁剪页面入口
  • /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. clip/index.wxml #8 (opens new window) 中上传按钮绑定 tap 事件处理函数 handleUploadTap,点击按钮将触发上传图片操作。

    <view class="button-container">
      <button type="primary" bindtap="handleUploadTap">上传图片</button>
    </view>
    
  2. clip/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. 首先在 clip/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.clip 方法进行裁剪。

    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.clip(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
      };
    }
    

# 图像裁剪

clip/index.wxml #11 (opens new window) 中添加画布容器

<view class="preview-container">
  <canvas
    wx:for="{{clipSizes}}"
    wx:for-item="size"
    wx:key="{{size[0]/size[1]}}"
    style="width: {{size[0]}}rpx; height: {{size[1]}}rpx; background-image: url({{thumb}})"
    canvas-id="canvas-{{size[0]/size[1]}}"
    class="canvas"
  ></canvas>
</view>

clip/index.js #101 (opens new window) 实现 clip 方法,使用 canvas (opens new window),根据识别信息进行裁剪。基本裁剪策略为:

  1. 原图等比缩放至接近目标大小,比较原图与目标大小的宽高比,若原图像宽高比较大,则保持高边对齐,缩放至目标大小,反之保持宽边对齐进行缩放。缩放后裁剪窗口可在宽/高方向上进行滑动,选择合适的裁剪位置。
  2. 根据人脸位置与大小,计算值人脸中心点位置,将人脸中心点在宽/高方向上与裁剪窗口中心对齐,若裁剪窗口超出缩放后图像,则进行平移,直至裁剪窗口刚好落在图像内。
  3. 使用 CanvasContext.drawImage (opens new window) 将裁剪窗口内的图像绘制于画布中,获取裁剪后图像。(此处需要注意,canvas 的绘制单位为 px,无法根据 rpx 绘制,如果画布设置使用了 rpx,需要进行转换。)

示例代码

// 示例采用了 3 种规格的裁剪,
// data.clipSizes: [[100, 100], [300, 200], [160, 90]]
clip( url, { imageWidth, imageHeight, rectX, rectWidth, rectY, rectHeight }) {
  this.data.clipSizes.forEach(([clipWidth, clipHeight]) => {
    let middleWidth = rectX + rectWidth / 2;
    let middleHeight = rectY + rectHeight / 2;
    let clipAspectRatio = clipWidth / clipHeight;
    let imageAspectRatio = imageWidth / imageHeight;
    let top = 0,
      left = 0;
    let right = 1,
      bottom = 1;
    if (imageAspectRatio < clipAspectRatio) {
      // 宽边对齐,上下移动
      let halfHeight = imageAspectRatio / clipAspectRatio / 2;
      top = middleHeight - halfHeight;
      bottom = middleHeight + halfHeight;
      if (top < 0) {
        bottom = halfHeight * 2;
        top = 0;
      } else if (bottom > 1) {
        top = 1 - halfHeight * 2;
        bottom = 1;
      }
    } else {
      // 高边对齐,左右移动
      let halfWidth = clipAspectRatio / imageAspectRatio / 2;
      left = middleWidth - halfWidth;
      right = middleWidth + halfWidth;
      if (left < 0) {
        right += -left;
        left = 0;
      } else if (right > 1) {
        left = 1 - right + left;
        right = 1;
      }
    }
    wx.getSystemInfo({
      success: function(res) {
        // 获取系统屏幕可用宽度,计算缩放大小
        let context = wx.createCanvasContext(`canvas-${clipAspectRatio}`);
        context.drawImage(
          url,
          Math.floor(left * imageWidth),
          Math.floor(top * imageHeight),
          Math.floor((right - left) * imageWidth),
          Math.floor((bottom - top) * imageHeight),
          0,
          0,
          (res.windowWidth / 750) * clipWidth,
          (res.windowWidth / 750) * clipHeight
        );
        context.draw(false);
      }
    });
  });
}

编译项目,可以实现从文件上传,到人脸识别,再到图片裁剪输出的整个流程。