决胜圣诞,女神心境不必猜!

《决胜圣诞,女神心境不必猜!》

本文将连系挪动装备摄像才能与 TensorFlow.js,在浏览器里完成一个及时的人脸心境分类器。鉴于文章的故事背景较长,对完成自身更有兴致的同砚可直接跳转至手艺计划概述

DEMO 试玩

媒介

看遍了 25 载的雪月没风花,本旺早已不悲不喜。万万没想到,在圣诞节前夜,女神竟然准许了鄙人的约会要求。但是面临这么个大好机会,本前端工程狮竟倏忽慌张起来。想鄙人正如在坐的一些看官一样,虽玉树临风、风流倜傥,却总因猜不透女孩的心机,一不留神就落得个母胎只身。如今已是 8102 年,像我等这么优异的少年若再不脱单,党和人民那都是一万个差别意!痛定思痛,鄙人这就要发挥本身的手艺优势,将察「颜」观色的妙技树点满,做一个洞悉女神喜怒哀愁的优异少年,决胜圣诞之巅!

《决胜圣诞,女神心境不必猜!》

正题最先

需求剖析

我们前端工程师终究在 2018 年迎来了 TensorFlow.js,这就意味着,就算算法学的再弱鸡,又不会 py 生意业务,我们也能靠着 js 随着算法的同砚们学上个一招半式。假如我们能够在约会时期,经由历程正规渠道取得女神的照片,是不是是就能用算法剖析剖析女神看到鄙人的时刻,是高兴照样…不,一定是高兴的!

《决胜圣诞,女神心境不必猜!》

但是,约会的疆场瞬息万变,我们总不能拍了照就放手机里,约完会回到静悄悄的家,再跑代码剖析吧,那可就 「too young too late」 了!时候就是性命,假如不能就地晓得女神的心境,我们还不如给本身 -1s!

因而,我们的目的就是能够在手机上,及时看到如许的效果(嘛,有些大略,不过本文将专注于功用完成,哈哈):

《决胜圣诞,女神心境不必猜!》

手艺计划概述

很简朴,我们须要的就两点,图象收集 & 模子运用,至于效果怎样展现,嗨呀,作为一个前端工程师,render 就是粗茶淡饭呀。关于前端的同砚来讲,唯一能够不熟悉的也就是算法模子怎样用;关于算法的同砚来讲,唯一能够不熟悉的也就是挪动装备怎样运用摄像头。

我们的流程即以下图所示(下文会针对盘算速度的题目举行优化):

《决胜圣诞,女神心境不必猜!》

下面,我们就依据这个流程图来梳理下怎样完成吧!

中间一:图象收集与展现

图象收集

我们怎样运用挪动装备举行图象或许视频流的收集呢?这就须要借助 WebRTC 了。WebRTC,即网页立即通讯(Web Real-Time Communication),是一个支撑网页浏览器举行及时语音对话或视频对话的 API。它于 2011 年 6 月 1 日开源,并在 Google、Mozilla、Opera 支撑下被归入万维网同盟的 W3C 引荐规范。

拉起摄像头并猎取收集到的视频流,这正是我们须要运用到的由 WebRTC 供应的才能,而中间的 API 就是 navigator.mediaDevices.getUserMedia

该要领的兼容性以下,能够看到,关于罕见的手机来讲,照样能够较好支撑的。不过,差别手机、体系品种与版本、浏览器品种与版本能够照样存在一些差别。假如想要更好的做兼容的话,能够斟酌运用 Adapter.js 来做 shim,它能够让我们的 App 与 Api 的差别相断绝。另外,在这里能够看到一些风趣的例子。详细 Adapter.js 的完成能够自行查阅。

《决胜圣诞,女神心境不必猜!》

那末这个要领是怎样运用的呢?我们能够经由历程 MDN 来查阅一下。MediaDevices getUserMedia() 会向用户请求权限,运用媒体输入,取得具有指定范例的 MediaStream(如音频流、视频流),而且会 resolve 一个 MediaStream 对象,假如没有权限或没有婚配的媒体,会报出响应异常:

navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
  /* use the stream */
})
.catch(function(err) {
  /* handle the error */
});

因而,我们能够在进口文件一致如许做:

class App extends Component {
  constructor(props) {
    super(props);
    // ...
    this.stream = null;
    this.video = null;
    // ...
  }

  componentDidMount() {
    // ...
    this.startMedia();
  }

  startMedia = () => {
    const constraints = {
      audio: false,
      video: true,
    };
    navigator.mediaDevices.getUserMedia(constraints)
      .then(this.handleSuccess)
      .catch(this.handleError);
  }

  handleSuccess = (stream) => {
    this.stream = stream; // 猎取视频流
    this.video.srcObject = stream; // 传给 video
  }
  
  handleError = (error) => {
    console.log('navigator.getUserMedia error: ', error);
  }
  // ...
}

及时展现

为何须要 this.video 呢,我们不仅要展现拍摄到的视频流,还要能直观的将女神的脸部神色标记出来,因而须要经由历程 canvas 来同时完成展现视频流和绘制基础图形这两点,而衔接这两点的要领以下:

canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);

固然,我们并不须要在视图中真的供应一个 video DOM,而是由 App 保护在实例内部即可。canvas.width 和 canvas.height 须要斟酌挪动端装备的尺寸,这里略去不表。

而绘制矩形框与笔墨信息则异常简朴,我们只须要拿到算法模子盘算出的位置信息即可:

export const drawBox = ({ ctx, x, y, w, h, emoji }) => {
  ctx.strokeStyle = EmojiToColor[emoji];
  ctx.lineWidth = '4';
  ctx.strokeRect(x, y, w, h);
}

export const drawText = ({ ctx, x, y, text }) => {
  const padding = 4
  ctx.fillStyle = '#ff6347'
  ctx.font = '16px'
  ctx.textBaseline = 'top'
  ctx.fillText(text, x + padding, y + padding)
}

中间二:模子展望

在这里,我们须要将题目举行拆解。鉴于本文所说的「辨认女神脸色背地的心境」属于图象分类题目,那末这个题目就须要我们完成两件事:

  • 从图象中提掏出人脸部份的图象;
  • 将提掏出的图象块作为输入交给模子举行分类盘算。

下面我们来缭绕这两点逐步议论。

人脸提取

我们将借助 face-api.js 来处置惩罚。face-api.js 是基于 tensorflow.js 中间 API (@tensorflow/tfjs-core) 来完成的在浏览器环境中运用的脸部检测与辨认库,自身就供应了
SSD Mobilenet V1、Tiny Yolo V2、MTCNN 这三种异常轻量的、合适挪动装备运用的模子。很好明白的是效果天然是打了不少折扣,这些模子都从模子大小、盘算庞杂度、机械功耗等多方面做了精简,只管有些特地用来盘算的挪动装备照样能够驾御完全模子的,但我们平常的手机是一定没有卡的,天然只能运用 Mobile 版的模子。

这里我们将运用 MTCNN。我们能够小瞄一眼模子的设想,以下图所示。能够看到,我们的图象帧会被转换成差别 size 的张量传入差别的 net,并做了一堆 Max-pooling,末了同时完成人脸分类、bb box 的回归与 landmark 的定位。大抵就是说,输入一张图象,我们能够获得图象中所有人脸的种别、检测框的位置信息和比方眼睛、鼻子、嘴唇的更细节的位置信息。

《决胜圣诞,女神心境不必猜!》

固然,当我们运用了 face-api.js 时就不须要太细致的去斟酌这些,它做了较多的笼统与封装,以至异常横暴的对前端同砚屏障了张量的观点,你只须要取到一个 img DOM,是的,一个已加载好 src 的 img DOM 作为封装要领的输入(加载 img 就是一个 Promise 咯),其内部会本身转换成须要的张量。经由历程下面的代码,我们能够将视频帧中的人脸提掏出来。

export class FaceExtractor {
  constructor(path = MODEL_PATH, params = PARAMS) {
    this.path = path;
    this.params = params;
  }

  async load() {
    this.model = new faceapi.Mtcnn();
    await this.model.load(this.path);
  }

  async findAndExtractFaces(img) {
    // ...一些基础判空保证在加载好后运用
    const input = await faceapi.toNetInput(img, false, true);
    const results = await this.model.forward(input, this.params);
    const detections = results.map(r => r.faceDetection);
    const faces = await faceapi.extractFaces(input.inputs[0], detections);
    return { detections, faces };
  }
}

心境分类

好了,终究到了中间功用了!一个「好」习气是,扒一扒 GitHub 看看有无开源代码能够参考一下,假如你是大佬请当我没说。这里我们将运用一个及时脸部检测和心境分类模子来完成我们的中间功用,这个模子能够辨别高兴、生机、惆怅、恶心、没脸色等。

关于在浏览器中运用 TensorFlow.js 而言,许多时刻我们更多的是运用现有模子,经由历程 tfjs-converter 来将已有的 TensorFlow 的模子、Keras 的模子转换成 tfjs 能够运用的模子。值得一提的是,手机自身集成了许多的传感器,能够收集到许多的数据,置信将来一定有 tfjs 发挥的空间。详细转换要领可参考文档,我们继承往下讲。

《决胜圣诞,女神心境不必猜!》

那末我们能够像运用 face-api.js 一样将 img DOM 传入模子吗?不可,事实上,我们运用的模子的输入并非随便的图象,而是须要转换到指定大小、并只保存灰度图的张量。因而在继承之前,我们须要对原图象举行一些预处置惩罚。

哈哈,躲得了初一躲不过十五,我们照样来相识下什么是张量吧!TensorFlow 的官网是这么诠释的:

张量是对矢量和矩阵向潜伏的更高维度的泛化。TensorFlow 在内部将张量表示为基础数据范例的 n 维数组。

《决胜圣诞,女神心境不必猜!》

算了没紧要,我们画个图来明白张量是什么样的:

《决胜圣诞,女神心境不必猜!》

因而,我们可将其简朴明白为更高维的矩阵,而且存储的时刻就是个数组套数组。固然,我们一般运用的 RGB 图象有三个通道,那是不是是就是说我们的图象数据就是三维张量(宽、高、通道)了呢?也不是,在 TensorFlow 里,第一维一般是 n,详细来讲就是图象个数(更准确的说法是 batch),因而一个图象张量的 shape 平常是 [n, height, width, channel],也即四维张量。

那末我们要怎样对图象举行预处置惩罚呢?起首我们将散布在 [0, 255] 的像素值中间化到 [-127.5, 127.5],然后规范化到 [-1, 1]即可。

const NORM_OFFSET = tf.scalar(127.5);

export const normImg = (img, size) => {
  // 转换成张量
  const imgTensor = tf.fromPixels(img);

  // 从 [0, 255] 规范化到 [-1, 1].
  const normalized = imgTensor
    .toFloat()
    .sub(NORM_OFFSET) // 中间化
    .div(NORM_OFFSET); // 规范化

  const { shape } = imgTensor;
  if (shape[0] === size && shape[1] === size) {
    return normalized;
  }

  // 根据指定大小调解
  const alignCorners = true;
  return tf.image.resizeBilinear(normalized, [size, size], alignCorners);
}

然后将图象转成灰度图:

export const rgbToGray = async imgTensor => {
  const minTensor = imgTensor.min()
  const maxTensor = imgTensor.max()
  const min = (await minTensor.data())[0]
  const max = (await maxTensor.data())[0]
  minTensor.dispose()
  maxTensor.dispose()

  // 灰度图则须要规范化到 [0, 1],根据像素值的区间来规范化
  const normalized = imgTensor.sub(tf.scalar(min)).div(tf.scalar(max - min))

  // 灰度值取 RGB 的平均值
  let grayscale = normalized.mean(2)

  // 扩大通道维度来猎取准确的张量外形 (h, w, 1)
  return grayscale.expandDims(2)
}

如许一来,我们的输入就从 3 通道的彩色图片变成了只要 1 个通道的是非图。

《决胜圣诞,女神心境不必猜!》

注重,我们这里所做的预处置惩罚比较简朴,一方面我们在避免除明白一些细节题目,另一方面也是由于我们是在运用已练习好的模子,不须要做一些庞杂的预处置惩罚来改良练习的效果。

预备好图象后,我们须要最先预备模子了!我们的模子重要须要暴露加载模子的要领 load 和对图象举行分类的 classify 这两个要领。加载模子异常简朴,只须要挪用 tf.loadModel 即可,须要注重的是,加载模子是一个异步历程。我们运用 create-react-app 构建的项目,封装的 Webpack 设置已支撑了 async-await 的要领。

class Model {
  constructor({ path, imageSize, classes, isGrayscale = false }) {
    this.path = path
    this.imageSize = imageSize
    this.classes = classes
    this.isGrayscale = isGrayscale
  }

  async load() {
    this.model = await tf.loadModel(this.path)

    // 预热一下
    const inShape = this.model.inputs[0].shape.slice(1)
    const result = tf.tidy(() => this.model.predict(tf.zeros([1, ...inShape])))
    await result.data()
    result.dispose()
  }

  async imgToInputs(img) {
    // 转换成张量并 resize
    let norm = await prepImg(img, this.imageSize)
    // 转换成灰度图输入
    norm = await rgbToGrayscale(norm)
    // 这就是所说的设置 batch 为 1
    return norm.reshape([1, ...norm.shape])
  }

  async classify(img, topK = 10) {
    const inputs = await this.imgToInputs(img)
    const logits = this.model.predict(inputs)
    const classes = await this.getTopKClasses(logits, topK)
    return classes
  }

  async getTopKClasses(logits, topK = 10) {
    const values = await logits.data()
    let predictionList = []

    for (let i = 0; i < values.length; i++) {
      predictionList.push({ value: values[i], index: i })
    }

    predictionList = predictionList
      .sort((a, b) => b.value - a.value)
      .slice(0, topK)

    return predictionList.map(x => {
      return { label: this.classes[x.index], value: x.value }
    })
  }
}

export default Model

我们能够看到,我们的模子返回的是一个叫 logits 的量,而为了晓得分类的效果,我们又做了 getTopKClasses 的操纵。这能够会使得较少相识这块的同砚有些疑心。实际上,关于一个分类模子而言,我们返回的效果并非一个特定的类,而是对各个 class 的几率散布,举个例子:

// 表示用
const classifyResult = [0.1, 0.2, 0.25, 0.15, 0.3];

也就是说,我们分类的效果实在并非说图象中的东西「一定是人或许狗」,而是「多是人或许多是狗」。以上面的表示代码为例,假如我们的 label 对应的是 [‘女人’, ‘男子’, ‘大狗子’, ‘小狗子’, ‘二哈’],那末上述的效果实在应当明白为:图象中的物体 25% 的能够性为大狗子,20% 的能够性为一个男子。

《决胜圣诞,女神心境不必猜!》

因而,我们须要做 getTopKClasses,依据我们的场景我们只体贴最能够的心境,那末我们也就会取 top1 的几率散布值,从而晓得最能够的展望效果。

怎样,tfjs 封装后的高等要领是不是是在语义上较为清楚呢?

终究我们将上文提到的人脸提取功用与心境分类模子整合到一同,并加上一些基础的 canvas 绘制:

  // 略有调解
  analyzeFaces = async (img) => {
    // ...
    const faceResults = await this.faceExtractor.findAndExtractFaces(img);
    const { detections, faces } = faceResults;

    // 对提取到的每个人脸举行分类
    let emotions = await Promise.all(
      faces.map(async face => await this.emotionModel.classify(face))
    );
    // ...
  }

  drawDetections = () => {
    const { detections, emotions } = this.state;
    if (!detections.length) return;

    const { width, height } = this.canvas;
    const ctx = this.canvas.getContext('2d');
    const detectionsResized = detections.map(d => d.forSize(width, height));
    detectionsResized.forEach((det, i) => {
      const { x, y } = det.box
      const { emoji, name } = emotions[i][0].label;
      drawBox({ ctx, ...det.box, emoji });
      drawText({ ctx, x, y, text: emoji, name });
    });
  }

功德圆满!

及时性优化

事实上,我们还应当斟酌的一个题目是及时性。事实上,我们的盘算历程用到了两个模子,即使已是针对挪动装备做了优化的精简模子,但仍然会存在机能题目。假如我们在构造代码的时刻以壅塞的体式格局举行展望,那末就会涌现一帧一帧的卡顿,女神的笑颜也会变得发抖和生硬。

《决胜圣诞,女神心境不必猜!》

因而,我们要斟酌做一些优化,来更好地画出效果。

笔者这里应用一个 flag 来标记当前是不是有正在举行的模子盘算,假如有,则进入下一个事宜轮回,不然则进入模子盘算的异步操纵。同时,每个事宜轮回都邑实行 canvas 操纵,从而保证标记框总是会展现出来,且每次展现的实在都是缓存在 state 中的前一次模子盘算效果。这类操纵是具有合理性的,由于人脸的挪动一般是一连的(假如不一连这个天下能够要从新审阅一下),这类处置惩罚要领能较好的对效果举行展现,且不会由于模子盘算而壅塞,致使卡顿,本质上是一种离散化采样的技能吧。

  handleSnapshot = async () => {
    // ... 一些 canvas 预备操纵
    canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
    this.drawDetections(); // 绘制 state 中保护的效果
    
    // 应用 flag 推断是不是有正在举行的模子展望
    if (!this.isForwarding) {
      this.isForwarding = true;
      const imgSrc = await getImg(canvas.toDataURL('image/png'));
      this.analyzeFaces(imgSrc);
    }

    const that = this;
    setTimeout(() => {
      that.handleSnapshot();
    }, 10);
  }

  analyzeFaces = async (img) => {
    // ...其他操纵
    const faceResults = await this.models.face.findAndExtractFaces(img);
    const { detections, faces } = faceResults;

    let emotions = await Promise.all(
      faces.map(async face => await this.models.emotion.classify(face))
    );
    this.setState(
      { loading: false, detections, faces, emotions },
      () => {
        // 猎取到新的展望值后,将 flag 置为 false,以再次举行展望
        this.isForwarding = false;
      }
    );
  }

效果展现

我们来在女神这实验下效果看看:

《决胜圣诞,女神心境不必猜!》

嗯,因陋就简吧!虽然有时刻照样会把笑颜辨认成没什么脸色,咦,是不是是 Gakki 演技照样有点…好了好了,时候紧急,赶忙带上兵器预备赴约吧。穿上一身帅气格子衫,打扮成程序员样子容貌~

末端

约会当晚,吃着火锅唱着歌,鄙人与女神相谈甚欢。合理氛围逐步暗昧,话题最先深切到情绪方面时,我天然的问起女神的抱负型。万万没想到,女神倏忽说了如许的话:

《决胜圣诞,女神心境不必猜!》

那一刻我想起了 Eason 的歌:

Lonely Lonely christmas

Merry Merry christmas

写了卡片能寄给谁

心碎的像街上的纸屑

参考

文章可随便转载,但请保存此
原文链接

异常迎接有热情的你到场 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。

    原文作者:alisecued
    原文地址: https://segmentfault.com/a/1190000017498742
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞