Shoe 鞋部检测

VisionKit 从基础库 3.2.1 版本 (安卓微信>=8.0.43,iOS微信>=8.0.43) 开始支持。

鞋子检测 能力作为与 其他 VisionKit 能力 平行的能力接口。

该能力一般用于用户进行 AR试鞋 或者 腿部遮挡 等功能的开发。

方法定义

鞋子检测,目前只支持通过 摄像头实时检测

可以通过配置,决定是否每帧获取 用于对鞋子的腿部区域进行蒙版过滤的 腿部遮挡纹理

摄像头实时检测

首先需要创建 VKSession 的配置,然后通过 VKSession.start 启动 VKSession 实例。

运行过程中,算法实时检测相机中的鞋子,通过 VKSession.on 实时输出鞋子矩阵信息(位置旋转缩放)、8个关键点坐标。

最后,实时获取 VKSession 的帧数据 VKFrame,获得 VKCamera。得到 VKCamera.viewMatrixVKCamera.getProjectionMatrix 并结合,鞋子矩阵关键点信息 进行具体渲染。

开启VKSession示例代码:

// VKSession 配置
const session = wx.createVKSession({
    track: {
        shoe: {
            mode: 1 // 1 使用摄像头
        }
    }
})

// 摄像头实时检测模式下,监测到鞋子时,updateAnchors 事件会连续触发 (每帧触发一次)
session.on('updateAnchors', anchors => {
    // 目前版本能识别 1-2 双鞋子,anchors 长度为 0-2
    anchors.forEach(anchor => {
        // shoedirec 鞋子左右,0 为左,1 为右。
        console.log('anchor.shoedirec', anchor.shoedirec)
        // transform 鞋子的矩阵信息(位置旋转缩放),4*4 行主序矩阵,
        console.log('anchor.transform', anchor.transform)
        // points3d 位置,8个关键点坐标
        console.log('anchor.points3d', anchor.points3d)
    })
})

// 摄像头实时检测模式下,当鞋子从相机中丢失时,会不断触发
session.on('removeAnchors', () => {
  console.log('removeAnchors')
})

// 需要调用一次 start 以启动
session.start(errno => {
  if (errno) {
    // 如果失败,将返回 errno
  } else {
    // 否则,返回null,表示成功
  }
})

渲染试鞋示例代码:

// 可以理解每一帧都需要执行的行为

// 1. 通过 VKSession 实例的 `getVKFrame` 方法可以获取到帧对象
const frame = this.session.getVKFrame(canvas.width, canvas.height)

// 2. 获取 VKCamera
const VKCamera = frame.camera

// 3. 获取 VKCamera 的 视图矩阵与投影矩阵
const viewMat = VKCamera.viewMatrix;
const projMat = VKCamera.getProjectionMatrix(near, far);

// 4. 结合 updateAnchors 里面返回 anchor 的 transform、points3d 进行具体渲染。(具体可以参考官方示例)

开启腿部遮挡

摄像头实时模式下,想要开启腿部分割能力。

需要保证在 VKSession.start 接口 调用,开启 VKSession 后。 调用 VKSessionupdateMaskMode 更新是否开启腿部分割纹理的返回。然后通过 VKFramegetLegSegmentBuffer 获取具体的腿部遮挡纹理buffer,然后基于 buffer 创建纹理,进行对应的遮挡剔除。

示例代码:

// 1. 开启腿部遮挡纹理获取开关
// VKSession.start 后,可以通过 updateMaskMode 更新是否能够获取腿部遮挡纹理buffer
this.session.updateMaskMode({
    useMask: true // 开启或关闭腿部遮挡纹理buffer (客户端 8.0.43 默认开启获取,可以手动关闭)
});

// 2. 通过 VKSession 实例的 `getVKFrame` 方法可以获取到帧对象
const frame = this.session.getVKFrame(canvas.width, canvas.height)

// 3. 通过帧对象,获取具体腿部分割纹理Buffer(160*160、单通道)
const legSegmentBuffer = frame.getLegSegmentBuffer();

// 4. 基于腿部分割纹理过滤内容(具体可以参考官方案例)

PS.除了算法分割外,可以手动添加Mesh进行遮挡,可以实现类似的遮挡分割效果。具体可以参考官方案例。

输出说明

anchor 信息

struct anchor
{
  transform,  // 鞋子矩阵信息,位置旋转缩放
  points3d,   // 鞋子关键点数组
  shoedirec,  // 鞋子左右脚,0 为左脚,1 为右脚
}

1. 鞋子矩阵 transform

长度为 16 的数组,表示 行主序 的 4*4 矩阵。

2. 鞋子关键点 points3d

长度为 8 的数组,表示 鞋子识别的 8个关键点:

Array<Point>(8) point3d

每个数组元素结构为:

struct Point { x, y, z }

表示在 鞋子矩阵 作用后,每个点的 三维偏移位置

以下为关键点图示与点位解释:

  • 点位0 位于鞋的底部后部,脚后跟处,靠近地面的位置。
  • 点位1 位于鞋子最前面,在竖直方向上大致处于鞋子上表面和鞋子下底面的中间。
  • 点位2 位于鞋的后部,在点位0的上方,位于靠近脚踝的位置。
  • 点位3、4 位于鞋子侧面,其中3点位于左鞋的左侧,右鞋的右侧。4点位于左鞋子的右侧,右鞋的左侧。上图中的3点处于被遮挡的位置。3,4点均紧靠鞋子下底面。
  • 点位5,6 位于鞋子侧面,相对于3,4更靠后,在鞋子下底面偏上一点的位置。
  • 点位7 大致位于鞋舌的中央。

3.腿部分割纹理Buffer

160*160 的 单通道浮点数颜色 ArrayBuffer,数值 0.0 对应非腿部区域,大于 0.0 代表是腿部区域,越靠近 1.0 越接近是腿部区域。

如何基于返回信息摆放模型

首先,可以将添加一个节点同步鞋子矩阵信息。再往该节点添加模型,基于鞋子关键点,设置对应的偏移,然后可以缩放模型适配比例,使模型贴合鞋子关键点,达到目标效果。

程序示例

摄像头实时检测示例

微信小程序示例 的 接口VisionKit视觉能力实时鞋部检测 - 试鞋案例

开源地址:实时鞋部检测-试鞋案例

识别鞋子,并将鞋子模型放置到识别出的区域。默认并支持动态开启与关闭腿部遮挡,以及显示与隐藏对应的鞋子关键点。

案例效果:

OCR检测

VisionKit从基础库 2.27.0版本开始提供OCR能力。

方法定义

OCR检测有2种使用方法,一种是输入一张静态图片进行检测,另一种是通过摄像头实时检测。

1. 静态图片检测

通过 VKSession.runOCR 接口 输入一张图像,算法检测到图像中的文字,然后通过 VKSession.on 接口 输出获取的文字内容。

示例代码:

const session = wx.createVKSession({
  track: {
    OCR: { mode: 2 } // mode: 1 - 使用摄像头;2 - 手动传入图像
  },
})

// 静态图片检测模式下,每调一次 runOCR 接口就会触发一次 updateAnchors 事件
session.on('updateAnchors', anchors => {
  console.log('anchors.text', "".concat(anchors.map(anchor=>anchor.text)))
})

// 需要调用一次 start 以启动
session.start(errno => {
  if (errno) {
    // 如果失败,将返回 errno
  } else {
    // 否则,返回null,表示成功
    session.runOCR({
      frameBuffer, // 图片 ArrayBuffer 数据。待检测图像的像素点数据,每四项表示一个像素点的 RGBA
      width, // 图像宽度
      height, // 图像高度
    })
  }
})

2. 通过摄像头实时检测

算法实时检测相机中的文字内容,通过 VKSession.on 接口 实时输出文字。

示例代码:

const session = wx.createVKSession({
  track: {
    OCR: { mode: 1 } // mode: 1 - 使用摄像头;2 - 手动传入图像
  },
})

// 摄像头实时检测模式下,监测到文字时,updateAnchors 事件会连续触发 (每帧触发一次)
session.on('updateAnchors', anchors => {
  console.log('anchors.text',"".concat(anchors.map(anchor=>anchor.text)))
})

// 当文字区域从相机中离开时,会触发 removeAnchors 事件
session.on('removeAnchors', () => {
  console.log('removeAnchors')
})

// 需要调用一次 start 以启动
session.start(errno => {
  if (errno) {
    // 如果失败,将返回 errno
  } else {
    // 否则,返回null,表示成功
  }
})

应用场景示例

  1. 文本检测。
  2. 车牌识别。
  3. 证件文本识别。

程序示例

  1. 实时摄像头OCR检测能力使用参考
  2. 静态图像OCR检测能力使用参考

身份证检测

VisionKit 从基础库 3.3.0 版本后开始支持。

身份证检测 能力作为与 其他 VisionKit 能力 平行的能力接口。

该能力,一般用于用户进行 身份证识别 或者 身份证裁剪 等功能的开发。

方法定义

身份证检测,目前只支持通过 视觉模式,即输入一张静态图片进行身份证识别,返回身份证对应信息。

可以通过配置 getAffineImg,决定每次识别是否返回 身份证区域裁剪矩阵

输入身份证图片要求

输入的身份证图片,尽量以 正常的角度拍摄。不同角度带来的 透视效果,会影响识别的 准确度

视觉模式接口

首先需要创建 VKSession 的配置,然后通过 VKSession.start 启动 VKSession 实例。

开启 VKSession,并添加返回对应身份证信息监听事件,示例代码:

// VKSession 配置
const session = wx.createVKSession({
    track: {
        IDCard: {
            mode: 2 // 照片模式
        }
    },
    version: 'v1',
})

// VKSession start
session.start(err => {

  // 静态图片估计模式下,每调一次 detectIDCard 接口就会触发一次 updateAnchors 事件
  session.on('updateAnchors', anchors => {
      // 处理返回的身份证信息
      if (anchors && anchors[0]) {
          // 存在数组,证明存在身份证信息
          const anchor = anchors[0];

          // 识别信息
          const isComplete = anchor.isComplete; // 身份证是否完整
          const label = anchor.label; // 身份证面信息(0 照片面 / 1 国徽面 )
          const orientation = anchor.orientation; // 身份证朝向 (0 朝上 1 朝下 2 朝下 3 朝左)
          const box = anchor.box; // 身份证坐标框点数组 (0 左上点 1 右上点 2 右下点 3 左下点)

          // 裁剪信息,接口 getAffineImg 为 true 时会返回。
          const affineImgWidth = anchor.affineImgWidth;
          const affineImgHeight = anchor.affineImgHeight;
          const affineMat = anchor.affineMat;

          // 存在裁剪信息,可以结合原图获取裁剪后的身份证图片
          if (affineImgWidth && affineImgHeight && affineMat) {
            /*
              * affineMat 3x3仿射变换矩阵,行主序
              *  [0 1 2
              *   3 4 5
              *   6 7 8]
              */
            /*
              * canvas 2d setTransform
              * setTransform(a, b, c, d, e, f)
              *  [a c e
              *   b d f
              *   0 0 1]
              */
             // 可以利用离屏的Canvas2D,结合原图与裁剪矩阵,进行具体的身份证图片裁剪。
          }
      }

  })
  // 图片没有识别到身份证,会触发一次 removeAnchors
  session.on('removeAnchors', anchors => {
      console.log("没有识别到身份证")
  })
});

调用身份证识别,示例代码:


// 调用具体的身份证图片识别接口
session.detectIDCard({
    // 识别身份证图片的 ArrayBuffer,Uint8ClampedArray,RGBA
    // 比如可以通过 canvas(2D)的 context.getImageData 获取
    frameBuffer: imgDataBuffer,
    // 传入识别图片的原始宽度
    width: imgOriginWidth,
    // 传入识别图片的原始高度
    height: imgOriginHeight,
    // 是否获取裁剪图片信息
    getAffineImg: true,
})
// 调用后,识别处理完毕后
// 识别成功会触发 updateAnchors 回调,处理失败会触发 removeAnchors 回调

输出说明

anchor 信息

struct anchor
{
  isComplete,  // 身份证是否完整
  label,       // 身份证面信息(0 照片面 / 1 国徽面 )
  orientation, // 身份证朝向 (0 朝上 1 朝下 2 朝下 3 朝左)
  box,         // 身份证坐标框点数组 (0 左上点 1 右上点 2 右下点 3 左下点)
  /* 身份证裁剪信息
   * getAffineImg 为 true 时返回 */
  affineImgWidth,   // 身份证裁剪宽度
  affineImgHeight,  // 身份证裁剪区域高度
  affineMat,        // 身份证裁剪矩阵
}

身份证坐标框点数组 box

长度为 4 的数组,表示身份证位于原图中,框的坐标点位置。

Array<Point>(8) box

每个数组元素结构为:

struct Point { x, y }

身份证裁剪矩阵 affineMat

长度为 9 的数组,表示行主序的 3×3 仿射变换矩阵。可以结合 Canvas (2D) 以及原图进行具体的身份证图片裁剪。

程序示例

身份证照片识别示例

微信小程序示例的 接口VisionKit 视觉能力照片身份证识别

开源地址:照片身份证识别

深度估计

VisionKit 提供深度估计能力。

深度估计,就是获取图像中场景里的每个点到相机的距离信息,这种距离信息组成的图我们称之为深度图。

方法定义

我们在这里提供了深度估计的两种模式:

视觉模式: (安卓微信>=8.0.37,iOS微信>=8.0.38起) 使用摄像头进行深度估计 。

AR模式: (安卓微信>=8.0.38同时基础库>=2.33.0,iOS微信>=8.0.39同时基础库>=3.0.0) 使用摄像头和IMU进行深度估计,输出由near和far控制的非线性深度图,用于做虚实遮挡,目前支持部分机型,支持机型列表等同于6Dof-水平面AR-V2平面AR接口支持列表

接口类型 精度 机型覆盖率
视觉模式
AR模式

视觉模式接口

深度估计视觉模式接口提供2种使用方法,一种是输入一张静态图片进行深度估计,另一种是通过摄像头进行实时深度估计。

  1. 静态图片估计

通过VKSession.detectDepth接口输入一张图像,然后通过VKSession.on接口监听深度图信息,图中像素的值代表当前的深度值,颜色越黑,代表距离摄像头越近,反之颜色越白,代表距离深度越远。

示例代码:


const session = wx.createVKSession({
  track: {
    depth: {
      mode: 2 // mode: 1 - 使用摄像头;2 - 手动传入图像
    },
  },
  gl: this.gl,
})
// 静态图片估计模式下,每调一次 detectDepth 接口就会触发一次 updateAnchors 事件
session.on('updateAnchors', anchors => {
  anchors.forEach(anchor => {
    console.log('anchor.depthArray', anchor.depthArray) // 深度图 ArrayBuffer 数据
    console.log('anchor.size', anchor.size) // 深度图大小,结果为数组[宽, 高]
  })
})

// 需要调用一次 start 以启动
session.start(errno => {
  if (errno) {
    // 如果失败,将返回 errno
  } else {
    // 否则,返回null,表示成功
    session.detectDepth({
      frameBuffer, // 待检测图片的ArrayBuffer 数据。待检测的深度图像RGBA数据, 
      width, // 图像宽度
      height, // 图像高度
    })
  }
})
  1. 通过摄像头实时估计

算法实时输出当前帧的深度图,每一帧像素的值代表当前的深度值,颜色越黑,代表距离摄像头越近,反之颜色越白,代表距离深度越远。

示例代码:

const session = wx.createVKSession({
  track: {
    depth: {
      mode: 1 // mode: 1 - 使用摄像头;2 - 手动传入图像
    },
  },
})

// 需要调用一次 start 以启动
session.start(errno => {
  if (errno) {
    // 如果失败,将返回 errno
  } else {
    // 获取每一帧的信息
    const frame = session.getVKFrame(canvas.width, canvas.height)
    // 获取每帧的深度图信息
    const depthBufferRes = frame.getDepthBuffer();
    const depthBuffer = new Float32Array(depthBufferRes.DepthAddress)
    //创建渲染逻辑, 将数组值传输到一张纹理上,并渲染到屏幕
    render()
  }
})

AR模式接口

详情见水平面AR中的V2+虚实遮挡

实时估计输出说明

1. 效果展示

2. 深度估计关键点

深度实时关键点输出字段包括

struct anchor
{
  DepthAddress, // 深度图ArrayBuffer的地址, 用法如new Float32Array(depthBufferRes.DepthAddress); 
  width,    // 返回深度图的宽
  height,      // 返回深度图的高
}

应用场景示例

  1. 特效场景。
  2. AR游戏以及应用(如下是AR虚实遮挡的例子)。

planev2_deepocc_gif

程序示例

视觉模式:

  1. 实时深度估计能力使用参考
  2. 静态图像深度估计能力使用参考

AR模式:

  1. V2平面+虚实遮挡

demo体验

视觉模式:

  1. 实时深度估计能力,在微信小程序示例中的接口-VisionKit视觉能力-实时深度图检测中体验。
  2. 照片深度估计能力,在微信小程序示例中的接口-VisionKit视觉能力-照片深度图检测中体验。

AR模式:

  1. 在微信小程序示例中的接口-VisionKit视觉能力-水平面AR-v2-虚实遮挡中体验。

新建一个XR组件

首先创建项目,让我们选择微信小程序工程:

之后先在app.json加上一行配置:"lazyCodeLoading": "requiredComponents"。然后创建好组件文件夹,新建一个组件,然后修改组件的内容:

index.json:

{
  "component": true,
  "renderer": "xr-frame",
  "usingComponents": {}
}

index.wxml:

<xr-scene>
  <xr-camera clear-color="0.4 0.8 0.6 1" />
</xr-scene>

index.json中,我们指定了这个组件的渲染器是xr-frame;在index.wxml中,我们创建了一个场景xr-scene,并在其下添加了一个相机xr-camera

在页面中使用这个组件

创建完组件后,便可以在页面中使用它,让我们进入pages/index,修改它的jsonwxmlts文件:

json中:

{
  "usingComponents": {
    "xr-start": "../../components/xr-start/index"
  },
  "disableScroll": true
}

ts脚本中:

Page({
  data: {
    width: 300,
    height: 300,
    renderWidth: 300,
    renderHeight: 300,
  },
  onLoad() {
    const info = wx.getSystemInfoSync();
    const width = info.windowWidth;
    const height = info.windowHeight;
    const dpi = info.pixelRatio;
    this.setData({
      width, height,
      renderWidth: width * dpi,
      renderHeight: height * dpi
    });
  },
})

wxml中:

<view>
  <xr-start
    disable-scroll
    id="main-frame"
    width="{{renderWidth}}"
    height="{{renderHeight}}"
    style="width:{{width}}px;height:{{height}}px;"
  />
</view>

这里我们在脚本中设置了微信小程序xr-frame组件需要渲染的宽高,然后传入wxml,并在其中使用了json中引用的组件进行渲染,目前效果如下,可见整个画布被xr-camera上设置的清屏颜色清屏了:

添加一个物体

接下来我们给场上添加一个物体,直接使用xr-mesh以及内置的几何数据、材质,创建一个立方体:

<xr-scene>
  <xr-mesh node-id="cube" geometry="cube" />
  <xr-camera clear-color="0.4 0.8 0.6 1" position="0 1 4" target="cube" camera-orbit-control />
</xr-scene>

这里我们给物体指定了一个node-id,作为节点的索引,之后修改xr-camerapositiontarget,让其始终看向这个立方体,最后再给相机加上camera-orbit-control属性,使得我们能对相机进行控制。

至此,一个立方体是渲染了出来,不过…为什么是黑色的?

微信小程序颜色和灯光

物体黑色是因为在我们没有给xr-mesh指定材质时,用的是基于PBR效果的默认材质,需要光照,解决这个问题有两种方法,其一是不需要光照的物体,可以使用simple材质,这里就引入了材质定义:

<xr-asset-material asset-id="simple" effect="simple" uniforms="u_baseColorFactor:0.8 0.4 0.4 1" />
<xr-mesh node-id="cube" geometry="cube" material="simple" />

效果如下:

虽然这可以解决一些问题,但大部分情况下我们还是需要灯光的,就让我们把材质改回去,然后加上一些灯光吧:

<xr-light type="ambient" color="1 1 1" intensity="1" />
<xr-light type="directional" rotation="40 70 0" color="1 1 1" intensity="3" cast-shadow />

<xr-mesh
  node-id="cube" cast-shadow
  geometry="cube" uniforms="u_baseColorFactor:0.8 0.4 0.4 1"
/>
<xr-mesh
  position="0 -1 0" scale="4 1 4" receive-shadow
  geometry="plane" uniforms="u_baseColorFactor:0.4 0.6 0.8 1"
/>

这里我们加入了一个环境光和一个主平行光,调整了亮度和方向,同时加上了一个新的物体,再通过各个组件的caster-shadowreceive-shadow开启了阴影,效果如下:

有点寡淡,加上图像

虽然有了灯光,但只有纯色还是有一些寡淡,接下来我们尝试加入纹理,让场景的色彩更加丰富一些,这里需要用到资源加载器xr-asset-loadxr-assets

<xr-assets bind:progress="handleAssetsProgress" bind:loaded="handleAssetsLoaded">
  <xr-asset-load type="texture" asset-id="waifu" src="https://mmbizwxaminiprogram-1258344707.cos.ap-guangzhou.myqcloud.com/xr-frame/demo/waifu.png" />
</xr-assets>

<xr-mesh
  node-id="cube" cast-shadow
  geometry="cube" uniforms="u_baseColorMap: waifu"
/>

注意到我们在xr-assets上绑定了两个事件progressloaded,这便于开发者监听资源加载进度,然后按需做一些操作,比如资源加载完成后和wx:if协作再显示物体。默认情况下,我们采用渐进式策略,当资源加载完成后会自动应用到物体上:

methods: {
  handleAssetsProgress: function ({detail}) {
    console.log('assets progress', detail.value);
  },
  handleAssetsLoaded: function ({detail}) {
    console.log('assets loaded', detail.value);
  }
}

这次的修改效果如下:

当然,我们还可以用代码动态加载一张纹理,然后将其设置到物体上,这里以获取用户信息的头像为例:

data: {
  avatarTextureId: 'white'
},

methods: {
  handleReady: function ({detail}) {
    this.scene = detail.value;
    // 该接口已废弃,请授权后,采用 getUserInfo 代替。
    wx.getUserProfile({
      desc: '获取头像',
      success: (res) => {
        this.scene.assets.loadAsset({
          type: 'texture', assetId: 'avatar', src: res.userInfo.avatarUrl
        }).then(() => this.setData({avatarTextureId: 'avatar'}));
      }
    })
  }
}

根据 微信小程序用户头像昵称获取规则调整公告 wx.getUserProfile 于 2022 年 10 月 25 日 24 时后,被废弃

注意这里的handleReady,我们可以在xr-scene上绑定bind:ready="handleReady"触发。完成头像获取后,将数据设置为uniforms的来源:

<xr-mesh
  position="0 -1 0" scale="4 1 4" receive-shadow
  geometry="plane" uniforms="u_baseColorMap: {{avatarTextureId}}"
/>

效果如下:

让场景更丰富,环境数据

物体有了纹理,那么背景能不能也有纹理呢?当然可以。我们提供了环境元素xr-env来定义环境信息,配合以相机可以渲染天空盒,这里以微信小程序框架内置的一个环境数据xr-frame-team-workspace-day为例:

<xr-env env-data="xr-frame-team-workspace-day" />

<xr-mesh
  node-id="cube" cast-shadow
  geometry="cube" uniforms="u_baseColorMap: waifu,u_metallicRoughnessValues:1 0.1"
/>

<xr-camera
  position="0 1 4" target="cube" background="skybox"
  clear-color="0.4 0.8 0.6 1" camera-orbit-control
/>

这里我们将xr-camerabackgournd设置为了skybox,同时调整了立方体的金属粗糙度,效果如下:

同时可以看到场景中的物体相机叠加了一层反射,就像是被环境影响了一样,这是因为环境数据里还包括一些IBL的信息,当然这个我们不在这里赘述了,有兴趣的可以详细阅读后面的章节。

天空盒除了图像,还支持视频,我们可以先加载一个视频纹理,然后覆盖掉环境信息中的sky-map

<xr-asset-load type="video-texture" asset-id="office" src="https://mmbizwxaminiprogram-1258344707.cos.ap-guangzhou.myqcloud.com/xr-frame/demo/videos/office-skybox.mp4" options="autoPlay:true,loop:true" />

<xr-env env-data="xr-frame-team-workspace-day" sky-map="video-office" />

效果如下:

同时除了这种天空盒,我们还支持2D背景,这个在做一些商品展示的时候会比较有用:

<xr-asset-load type="texture" asset-id="weakme" src="https://mmbizwxaminiprogram-1258344707.cos.ap-guangzhou.myqcloud.com/xr-frame/demo/weakme.jpg" />

<xr-env env-data="xr-frame-team-workspace-day" sky-map="weakme" is-sky2d />

效果如下: