aaanthony

aaanthony 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 codingcat.cn 编辑
编辑

虽然还很菜
但也要坚持学习

个人动态

aaanthony 收藏了文章 · 2019-12-25

「圣诞特辑」纯前端实现人脸识别自动佩戴圣诞帽

在线体验地址:hksite.cn/prjs/christmashat

源码地址:https://github.com/hk029/christmas-hat

写在开头

叮叮当,叮叮当,吊儿个郎当,一年一度的圣诞节到咯,我不由的回想起了前两年票圈被圣诞帽支配的恐惧。打开票圈全是各种@官方求帽子的:

Untitled/Untitled.png?imageView&thumbnail=400x400

票圈头像也瞬间被圣诞帽攻陷:

Untitled/Untitled%201.png?imageView&thumbnail=400x400

在那段时间,你没一顶圣诞帽还真不好意思发票圈

Untitled/Untitled%202.png?imageView&thumbnail=400x400

各种戴帽子的软件也如雨后春笋般浮现出来,不管是小程序还是美图软件无一例外的都增加了戴圣诞帽的功能。但是对于懒人的我来说,自己调整一个圣诞帽子佩戴还是太麻烦了。于是我就想了,有没有什么办法能让我的头像自动佩戴上圣诞帽呢?

还真给我想到了,这就是今天的主题,用纯前端的方式给你做一个自动佩戴圣诞帽的网站。

有了这个网站,你就能顺利在票圈装 13 了,不仅如此,你还可能因此邂逅一段完美的爱情!试想一下,当你发了这个网站在票圈后,女神看到了就会为你的技术所折服,然后主动把照片给你,让你帮她给头像戴上圣诞帽,然后你就顺利的得到了和女神搭讪的机会,然后赢取白富美,走向人生巅峰,想想还有点小激动呢。

Untitled/Untitled%203.png?imageView&thumbnail=400x400

给头像戴上圣诞帽需要几步

给头像佩戴上圣诞帽需要几个步骤呢?很简单,跟大象装进冰箱一样,只需要三个步骤:

  • 打开头像
  • 戴上圣诞帽
  • 下载图片

Untitled/Untitled%204.png?imageView&thumbnail=400x400

其中第一步和最后一步看起来好像都不是什么难事,关键是这第二点,如何给头像戴上圣诞帽?

首先你必须要懂的,当我在说:戴上圣诞帽的时候,我在说什么?让我来翻译以下:

将圣诞帽的图片素材绘制在头像图片的合适位置,使之变成一张图片

所以我们今天的重点来了:如何能确定圣诞帽的位置,并将它和头像合成为一张图片。

首先让我们来聊聊如何确定圣诞帽的位置。

确定圣诞帽的位置

通过手动的方式,我们是很容易确定圣诞帽应该放在什么位置的,那机器如何能确定呢?有人可能想到了那不就是人脸识别技术?是的,这就是我们今天需要用到的技术。

早在 2017 年之前,纯前端说想实现人脸识别还有点天方夜谭的感觉,但是 Tensorflow.js 的出现让这一切成为了可能:

Untitled/Untitled%205.png?imageView&thumbnail=400x400

它是 Google 推出的第一个基于 TensorFlow 的前端深度学习框架。它允许你在浏览器上训练模型,或以推断模式运行预训练的模型。TensorFlow.js 不仅可以提供低级的机器学习构建模块,还可以提供高级的类似 Keras 的 API 来构建神经网络。

Tensorflow.js 是我第一个想到的可以应用的库,但是当我打开官方文档,看到如 Tensors (张量)、Layers (图层)、Optimizers (优化器)……各种陌生概念扑面而来,砸的人生疼,现学感觉是来不及了,那有什么办法能在我不了解各种概念的情况下也能快速上手人脸识别呢?

答案当然有,那就是:face-api.js

face-api.js

face-api.js 是大神 Vincent Mühler 的最新力作,他为人所知的开源项目还有 opencv4nodejsface-recognize(NodeJs 的人脸识别包,不过现在 face-api.js 已经支持 Node 端了,他推荐直接使用 face-api)

face-api.js 是一个建立在 Tensorflow.js 内核上的 Javascript 模块,它实现了三种卷积神经网络(CNN)架构,用于完成人脸检测、识别和特征点检测任务。简而言之,借助它,前端也能很轻松的完成人脸识别的工作。

原理简析

想看实现的童鞋请直接略过这一段,直接开始上手操作。

我们知道机器学习有几个基本要素:数据,模型,算法。他们之间的关系如下:

Untitled/Untitled%206.png?imageView&thumbnail=400x400

  • 训练数据: 训练数据就是一系列打过标签的数据,比如一系列人脸和不是人脸的图片数据。
  • 模型(这里我们主要指监督学习模型): 模型你可以简单理解为是一个预测函数(f(x) = y),简单来说就是根据输入的数据,能给出结果。
  • 算法: 算法就是教机器如何获得最优的模型(损失最小)。比如当机器通过当前模型识别到一张训练图片为人脸,但是标签是「非人脸」,此时就需要根据算法对模型进行调整。常见的算法有例如:梯度下降法(Gradient Descent),共轭梯度法(Conjugate Gradient),牛顿法和拟牛顿法,模拟退火法(Simulated Annealing)……

所以,我们可以这么说,只要有了一个训练好的预测模型,我们就可以对未知数据进行分析预测了。

face-api 的原理

首先,为了在图片中识别出人脸,我们需要告诉机器什么样的脸是人脸,因此我们需要大量的人脸照片,并且标明里面的各种脸部特征数据,让机器去学习:

Untitled/Untitled%207.png?imageView&thumbnail=400x400

face-api.js 针对人脸检测工作实现了一个 SSD(Single Shot Multibox Detector)算法,它本质上是一个基于 MobileNetV1 的卷积神经网络(CNN),同时在网络的顶层加入了一些人脸边框预测层。

然后 face-api.js 会通过该算法让机器不断的学习并优化,从而训练出模型,通过该模型可以识别出所有的人脸边界框

光识别出人脸还远远不够,我们的很多应用都需要找到人脸的特征点(眉毛,鼻子,嘴巴这些的)。因此 face-api.js 会从图片中抽取出每个边界框中的人脸居中的图像,接着将其再次作为输入传给人脸识别网络,让其学习。

为了实现特征点识别这个目标,face-api.js 又实现了一个简单的卷积神经网络,它可以返回给定图像的 68 个人脸特征点:

通过该算法,face-api.js 训练了一系列的模型,通过使用这些已经训练好的模型,我们可以快速实现我们想要的功能。

Untitled/Untitled%208.png?imageView&thumbnail=400x400

face-api.js 的使用方法

引入方式

如果你不使用打包工具的话,可以直接导入 face-api.js 的脚本:dist/face-api.js 获得最新的版本,或者从 dist/face-api.min.js 获得缩减版,并且导入脚本:

<script data-original="face-api.js"></script>

如果你使用 npm 包管理工具,可以输入如下指令:

npm i face-api.js

初始化

我们之前说过,face-api 它实现了一系列的卷积神经网络,并针对网络和移动设备进行了优化。所有的神经网络实例在 faceapi.nets中获取到

var nets = {
    ssdMobilenetv1: new SsdMobilenetv1(), // ssdMobilenetv1 目标检测
    tinyFaceDetector: new TinyFaceDetector(),  // 人脸识别(精简版)
    tinyYolov2: new TinyYolov2(),   // Yolov2 目标检测(精简版)
    mtcnn: new Mtcnn(),   // MTCNN
    faceLandmark68Net: new FaceLandmark68Net(),  // 面部 68 点特征识别
    faceLandmark68TinyNet: new FaceLandmark68TinyNet(), // 面部 68 点特征识别(精简版)
    faceRecognitionNet: new FaceRecognitionNet(),  // 面部识别
    faceExpressionNet: new FaceExpressionNet(),  //  表情识别
    ageGenderNet: new AgeGenderNet()  // 年龄识别
};

其中 MobileNetsyolov2 是业内比较有名的目标检测算法,有兴趣的可以点击链接去看论文(我是看不懂),这篇文章 简要介绍了这些算法,大概就是说他们的检测速度和检测效率都不错。这里你可以根据自己的需要选择不同的算法,加载不同的模型。

官方推荐使用ssdMobilenetv1,因为它的识别精度比较高,但是检索速度相对较慢,如果是实时检测的场景,它的检索速度可能会成为问题,因此,今年下半年作者把 MTCNN 算法也引入了,如果想用实时人脸检测的场景,可以试试 MTCNN。(可以看看作者 这篇文章

模型加载

通过之前的介绍我们也可以知道,模型才是重中之重,有了训练好的模型,我们就可以跳过训练的阶段,直接使用来做人脸识别了。

这也就是国外一个机器学习的布道者 Dan Shiffman 在 视频 中一直所强调的:并不是所有的机器学习入门都应该从学习算法入手,毕竟术业有专攻,目前已经有很多人建立了很多成熟的模型(图形检测,文本识别,图像分类……),我们可以站在巨人的肩膀上去做更多有意思的事情。

face-api 本身也提供了一系列的模型数据(/weights),可以开箱即用:

await faceapi.nets.ssdMobilenetv1.load('/weights')

其中 /weights 是放了 manifest.json 和 shard 文件的目录,建议把官方的 weights 目录直接拷贝下来,因为经常你需要几个模型同时使用。

识别

face-api 提供了很多高级的方法可以使用,其中最方便的就是detectAllFaces / detectSingleFace(input, options) , 注意:它默认是使用SSD Mobilenet V1 ,如果要使用Tiny FaceDetector,需要手动指定:

const detections1 = await faceapi.detectAllFaces(input, new faceapi.SsdMobilenetv1Options())
const detections2 = await faceapi.detectAllFaces(input, new faceapi.TinyFaceDetectorOptions())

其中 detect 系方法都支持链式调用,因此你可以这样用:

await faceapi.detectAllFaces(input)
await faceapi.detectAllFaces(input).withFaceExpressions()
await faceapi.detectAllFaces(input).withFaceLandmarks()
await faceapi.detectAllFaces(input).withFaceLandmarks().withFaceExpressions()

获取识别数据

进行识别操作后,返回的数据是什么样的呢?

如果你是进行的全脸识别,那么数据是一个数组,其中 detection 是默认会有的属性,它提供了一个人脸部的盒子信息

[{detection:{
    box: {
        x: 156.22306283064717
        y: 76.60605907440186
        width: 163.41096172182577
        height: 182.21931457519534
        left: 156.22306283064717
        top: 76.60605907440186
        right: 319.63402455247297
        bottom: 258.82537364959717
        area: 29776.633439024576
        topLeft: Point
        topRight: Point
        bottomLeft: Point
        bottomRight: Point
    }
    ……
}]

如果你进行了链式操作,比如 withFaceLandmarks() 那这个对象会增加一个landmarks的属性,以此类推。

[{detection, landmarks, ……}]

其中landmarks提供了获取脸部各种特征点的方法:

const landmarkPositions = landmarks.positions  // 获取全部 68 个点
const jawOutline = landmarks.getJawOutline()  // 下巴轮廓
const nose = landmarks.getNose()  // 鼻子
const mouth = landmarks.getMouth()  // 嘴巴
const leftEye = landmarks.getLeftEye()  // 左眼
const rightEye = landmarks.getRightEye()  // 右眼
const leftEyeBbrow = landmarks.getLeftEyeBrow()  // 左眉毛
const rightEyeBrow = landmarks.getRightEyeBrow()  // 右眉毛

处理识别数据

要知道,你拿到的数据是根据图片的真实数据来处理的,但我们在网页展示的图片通常不会是 1:1 的实际图片,也就是说图片会进行缩放/扩大处理。比如一张图片是 1000x1000 的,图片上的人脸嘴巴可能在(600,500)这个位置,但是我们实际展示的是 600x600 的图片,如果根据(600,500)这个坐标去画,那早就在画布外了。

因此如果我想要在图片上做一点事情,我们需要把当前的数据进行一个转换,让它的数据匹配特定的大小,这里,可以用它提供的 matchDimensions(canvas, displaySize)resizeResults(result, displaySize) 方法:

// 把 canvas 固定到 displaySize 的大小
faceapi.matchDimensions(canvas, displaySize) 
// 把数据根据 displaySize 做转换 
const resizedResults = faceapi.resizeResults(detectionsWithLandmarks, displaySize)

其中 displaySize 是一个拥有{ width, height }的对象,所以你也可以直接传入带 width 和 height 的 DOM 元素,如 <canvas />, <img />

根据数据绘制图形

光拿到数据可没用,我们主要目的是为了绘制图形,在绘制这一块 face-api 也是提供了一系列高级方法,比如:

faceapi.draw.drawDetections(canvas, resizedDetections)  // 直接在识别区域画个框
faceapi.draw.drawFaceLandmarks(canvas, resizedResults)  // 直接画出识别的的特征点

Untitled/Untitled%209.png?imageView&thumbnail=400x400

(以下测试图片均是采用从百度搜「女生头像」搜到的小姐姐,如有侵权,请告知)

当然你还可以在特定位置画个框或文字,具体用法可以参考:DrawBoxDrawTextField

const drawOptions = {
  label: 'Hello I am a box!',
  lineWidth: 2
}
const drawBox = new faceapi.draw.DrawBox(resizedResults[0].detection.box, drawOptions)
drawBox.draw(canvas)

Untitled/Untitled%2010.png?imageView&thumbnail=400x400

圣诞帽的绘制

说了这么多,突然发现还没到我们的主题,画圣诞帽!让我们赶紧回来。

确定圣诞帽的位置

现在假定我现在拥有了所有的面部数据,我应该如何确定圣诞帽的正确位置?首先,我们必须明确一点,圣诞帽应该是要戴在头顶的,应该没有人把圣诞帽戴在眉毛上吧?(好吧当我没说)

Untitled/Untitled%2011.png?imageView&thumbnail=400x400

但是人脸识别的数据中一般是不包含头顶的,这可咋办?还好我小学一年级学过半个学期的素描,在素描中有个很重要的概念叫三庭五眼

Untitled/Untitled%2012.png?imageView&thumbnail=400x400

也是说正常人的发际线到眉骨的距离是眉骨到下颌距离的一半(作为程序猿的我表示,该规则可能已经不适用了)。

因此我们可以通过获取眉毛的坐标和下颌的坐标来计算出头顶的位置:

/**
 * 获取头顶的坐标
 * @param {*} midPos 眉心点坐标
 * @param {*} jawPos 下巴底点坐标
 */
const getHeadPos = (midPos, jawPos) => {
  // 获取线的 k 值
  const k = getK(midPos, jawPos);
  // 获取眉心到下颌的距离
  const distanceOfEye2Jaw = getDistance(midPos, jawPos);
  return getPos(k, distanceOfEye2Jaw / 2, midPos);
};

 在这里让我们复习几个解析几何的公式:

  • 两点之间距离公式:

Untitled/Untitled%2013.png?imageView&thumbnail=400x400

  • 根据两点确定斜率:

    Untitled/Untitled%2014.png?imageView&thumbnail=400x400

  • 点到直线的距离公式:

Untitled/Untitled%2015.png?imageView&thumbnail=400x400

  • 相互垂直的直线,斜率之积为-1

Untitled/Untitled%2016.png?imageView&thumbnail=400x400

要特别注意的是,由于 Canvas 默认的坐标系的结构和我们之前数学课上学的不太一样,我们把它逆时针旋转 90 度可以发现它的 x,y 轴跟我们认识的是反的,因此为了方便,我们通常在代入公式计算的时候把 x,y 进行一下调换。

Untitled/Untitled%2017.png?imageView&thumbnail=400x400

/**
 * 获取 K 值
 * @param {*} a
 * @param {*} b
 */
const getK = (a, b) => (a.x - b.x) / (a.y - b.y)

/**
 * 获取两点之间距离
 * @param {*} a
 * @param {*} b
 */
const getDistance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));

因此目前已知眉心的坐标,下颌坐标,可以计算出这条直线的斜率,然后根据眉心到头顶的距离(眉心到下巴的一半),可以算出头顶的坐标:

/**
 * 已知 K,d, 点,求另一个点
 * @param {*} k 值
 * @param {*} d 距离
 * @param {*} point 一个基础点
 */
const getPos = (k, d, point) => {
  // 取 y 变小的那一边
  let y = -Math.sqrt((d * d) / (1 + k * k)) + point.y;
  let x = k * (y - point.y) + point.x;
  return { x, y };
};

图片合成

当我们已经知道了圣诞帽子的位置了,那接下的问题就是如何把圣诞帽合成到头像上去了,这里我们采用 Canvas 来实现 ,原理很简单:首先把头像绘制到 Canvas 上,然后再继续绘制圣诞帽就行了。

由于图片中可能不止一个面部数据,可能需要绘制多个帽子:

/**
 * 获取图片
 * @param {*} src 图片地址
 * @param {*} callback
 */
function getImg(src, callback) {
  const img = new Image();
  img.setAttribute('crossOrigin', 'anonymous');
  img.src = src;
  img.onload = () => callback(img);
}
/**
 * 绘制主流程
 * @param {*} canvas 
 * @param {*} options 
 */
function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg' } = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上(由于图片中可能不止一个面部数据,可能需要绘制多个帽子)
    for(let i = 0, len=info.length;i < len;i++) {
        const { headPos } = info[i];
        getImg('images/hat.png', img => ctx.drawImage(img, headPos.x, headPos.y, 200, 120));
    }
}

我们可以看到帽子已经绘制上去了,但是位置有点奇怪:

Untitled/Untitled%2018.png?imageView&thumbnail=400x400

这是因为绘制图片的时候是按图片的左上角为原点绘制的,因此我们在实际绘制帽子的时候需要对坐标进行一个偏移:

/**
 * 根据我当前的圣诞帽元素进行一些偏移(我的图片大小是 200*130), 圣诞帽可佩戴部分的中心 (60,60)
 * @param {*} x
 * @param {*} y
 */
const translateHat = (x, y) => {
    return {
        x: x - 60,
        y: y - 60,
    };
};

function drawing(canvas, options) {
    ……
    const {x, y} = translateHat(headPos.x, headPos.y);
    getImg('images/hat.png', img => ctx.drawImage(img, x, y, 200, 120));
    ……
}

Untitled/Untitled%2019.png?imageView&thumbnail=400x400

这么看起来好多了,感觉自己棒棒哒!

Untitled/Untitled%2020.png?imageView&thumbnail=400x400

优化

但是有小伙伴可以会发现,这样的结果还是有点问题:

  • 帽子大小是固定的,但是头像的上的面孔可大可小,大脸放小帽子显然有点不合适。
  • 帽子的朝向是固定的,如果有人的头像是偏着的呢,帽子是不是也应该偏过来?

因此我们还需要继续做优化:

圣诞帽的大小

圣诞帽的大小我们可以根据识别出的脸的大小来确定,通常来说,帽子可戴的宽度等于脸宽就行了(考虑到展示效果可以略大),我这里强调可戴宽度是因为一个圣诞帽的图片中只有一部分是可戴区域

Untitled/Untitled%2021.png?imageView&thumbnail=400x400

// 0.7 为可戴区域占总区域的比重(为了让帽子更大一点,选择 0.6),0.65 是图片的宽高比
const picSize = { width: faceWidth / 0.6, height: (faceWidth * 0.65) / 0.6 };

而面部的大小可以通过 jawOutlinePoints的起始点距离来计算

/**
 * 获取脸的宽度(即帽子宽度)
 * @param {*} outlinePoints
 */
const getFaceWith = outlinePoints => getDistance(outlinePoints[0], outlinePoints[outlinePoints.length - 1])

圣诞帽的角度

圣诞帽的角度该如何计算呢?其实也很简单,知道头的偏转角度就行了,而头的偏转角度,直接用脸中线(眉心到下颌)和 y 轴的夹角就行了(直接用 atan2 算出来的是补角,需要用 180 度减),这里考虑到后续使用方便直接用的弧度。

/**
 * 获取脸的倾斜弧度
 * @param {*} jawPos
 * @param {*} midPointOfEyebrows
 */
const getFaceRadian = (jawPos, midPointOfEyebrows) =>
    Math.PI - Math.atan2(jawPos.x - midPointOfEyebrows.x, jawPos.y - midPointOfEyebrows.y); //弧度  0.9272952180016122

Untitled/Untitled%2022.png?imageView&thumbnail=400x400

Canvas 中图片旋转

注意,在 Canvas 中没办法直接旋转图片,只能旋转画布,而且画布是按照原点旋转的,这点会特别坑。

Untitled/Untitled%2023.png?imageView&thumbnail=400x400

这里,我们只想让图片按照中心旋转怎么办?我们可以先让 Canvas 把原点平移到帽子的位置,然后再通过帽子的内部偏移使得帽子中心刚好在原点,此时再旋转画布把帽子画上就只影响这一个帽子图片了。

Untitled/Untitled%2024.png?imageView&thumbnail=400x400

/**
 * 绘制帽子 
 * @param {*} ctx 画布实例
 * @param {{}} config 配置
 */
function drawHat(ctx, config) {
    const { headPos, angle, faceWidth } = config;
    getImg('images/hat.png?imageView&thumbnail=400x400', img => {
        // 保存画布
        ctx.save();
        // 画布原点移动到画帽子的地方
        ctx.translate(headPos.x, headPos.y);
        // 旋转画布到特定角度
        ctx.rotate(angle);
        // 偏移图片,使帽子中心刚好在原点
        const { x, y, width, height } = translateHat(faceWidth, 0, 0);
        // 我的圣诞帽子实际佩戴部分长度只有 0.75 倍整个图片长度
        ctx.drawImage(img, x, y, width, height);
        // 还原画布
        ctx.restore();
    });
}

这样整个绘制的主流程大概就是这样:

function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg'} = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上
    for (let i = 0, len = info.length; i < len; i++) {
        drawHat(ctx, info[i]);  
    }
}

成品展示

Untitled/Untitled%2025.png?imageView&thumbnail=400x400

Untitled/Untitled%2026.png?imageView&thumbnail=400x400

Untitled/Untitled%2027.png?imageView&thumbnail=400x400

Untitled/Untitled%2028.png?imageView&thumbnail=400x400

image-20191223201537510

是不是感觉已经很完美了?目前已经能实现对各种大小的脸,不同朝向的脸进行适配了。甚至连狗狗的头像也能识别出来,这都得利于 face-api.js 提供的模型中也有狗狗的脸训练数据。

当然,这个例子实际上还是很不完善,因为当你测试的图片多的时候就会发现,对侧脸头像的识别还是有问题:

image-20191223201619850

这是因为脸的大小用之前的方法计算的其实是不准确的,实际上还应该根据双边眼睛的比例知道用户现在是侧脸还是正面,从而继续调整帽子的大小和位置。但是这里就不继续优化了,感兴趣的小伙伴可以自己去琢磨琢磨如何修改。

写在最后

通过上面这个小例子,我们可以发现前端利用机器学习也可以实现很多不可思议的有趣玩意,再结合 VRARwebRTC,我们甚至可以做一个纯前端的互动小游戏。

当然就算在这个例子上,你也可以进行扩展,比如绘制圣诞胡须,化妆……

如果你想继续深入学习机器学习的相关知识,可以去看看 Coursea 的 机器学习入门课程,如果你想深入学习一下 Tensorflow,可以去看看 Tensorflow.js 的 官方文档

虽然之前有吐槽 Tensorflow.js 知识点太多的问题,但是不得不说 Google 的文档写的还是不错的,提供了很多案例,手把手教你如何实现一些简单的功能:手写数字识别,预测,图片分类器……所以对 Tensorflow.js 感兴趣的童鞋不妨去它的官方文档中逛逛。

不过毕竟 Tensorflow.js 还是很底层的库,如果你只是想用机器学习做一些有趣的事情,不妨尝试一下 ml5.js,这里有一套学习视频

最后,祝大家圣诞快乐!

参考

https://github.com/justadudewhohacks/face-api.js

face-api.js — JavaScript API for Face Recognition in the Browser with tensorflow.js

https://tensorflow.google.cn/js/tutorials

https://www.youtube.com/watch?v=jmznx0Q1fP0

https://www.cnblogs.com/suyuanli/p/8279244.html

https://learn.ml5js.org/docs/#/

本文发布自 网易云音乐前端团队,文章未经授权禁止任何形式的转载。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们
查看原文

aaanthony 赞了文章 · 2019-12-25

「圣诞特辑」纯前端实现人脸识别自动佩戴圣诞帽

在线体验地址:hksite.cn/prjs/christmashat

源码地址:https://github.com/hk029/christmas-hat

写在开头

叮叮当,叮叮当,吊儿个郎当,一年一度的圣诞节到咯,我不由的回想起了前两年票圈被圣诞帽支配的恐惧。打开票圈全是各种@官方求帽子的:

Untitled/Untitled.png?imageView&thumbnail=400x400

票圈头像也瞬间被圣诞帽攻陷:

Untitled/Untitled%201.png?imageView&thumbnail=400x400

在那段时间,你没一顶圣诞帽还真不好意思发票圈

Untitled/Untitled%202.png?imageView&thumbnail=400x400

各种戴帽子的软件也如雨后春笋般浮现出来,不管是小程序还是美图软件无一例外的都增加了戴圣诞帽的功能。但是对于懒人的我来说,自己调整一个圣诞帽子佩戴还是太麻烦了。于是我就想了,有没有什么办法能让我的头像自动佩戴上圣诞帽呢?

还真给我想到了,这就是今天的主题,用纯前端的方式给你做一个自动佩戴圣诞帽的网站。

有了这个网站,你就能顺利在票圈装 13 了,不仅如此,你还可能因此邂逅一段完美的爱情!试想一下,当你发了这个网站在票圈后,女神看到了就会为你的技术所折服,然后主动把照片给你,让你帮她给头像戴上圣诞帽,然后你就顺利的得到了和女神搭讪的机会,然后赢取白富美,走向人生巅峰,想想还有点小激动呢。

Untitled/Untitled%203.png?imageView&thumbnail=400x400

给头像戴上圣诞帽需要几步

给头像佩戴上圣诞帽需要几个步骤呢?很简单,跟大象装进冰箱一样,只需要三个步骤:

  • 打开头像
  • 戴上圣诞帽
  • 下载图片

Untitled/Untitled%204.png?imageView&thumbnail=400x400

其中第一步和最后一步看起来好像都不是什么难事,关键是这第二点,如何给头像戴上圣诞帽?

首先你必须要懂的,当我在说:戴上圣诞帽的时候,我在说什么?让我来翻译以下:

将圣诞帽的图片素材绘制在头像图片的合适位置,使之变成一张图片

所以我们今天的重点来了:如何能确定圣诞帽的位置,并将它和头像合成为一张图片。

首先让我们来聊聊如何确定圣诞帽的位置。

确定圣诞帽的位置

通过手动的方式,我们是很容易确定圣诞帽应该放在什么位置的,那机器如何能确定呢?有人可能想到了那不就是人脸识别技术?是的,这就是我们今天需要用到的技术。

早在 2017 年之前,纯前端说想实现人脸识别还有点天方夜谭的感觉,但是 Tensorflow.js 的出现让这一切成为了可能:

Untitled/Untitled%205.png?imageView&thumbnail=400x400

它是 Google 推出的第一个基于 TensorFlow 的前端深度学习框架。它允许你在浏览器上训练模型,或以推断模式运行预训练的模型。TensorFlow.js 不仅可以提供低级的机器学习构建模块,还可以提供高级的类似 Keras 的 API 来构建神经网络。

Tensorflow.js 是我第一个想到的可以应用的库,但是当我打开官方文档,看到如 Tensors (张量)、Layers (图层)、Optimizers (优化器)……各种陌生概念扑面而来,砸的人生疼,现学感觉是来不及了,那有什么办法能在我不了解各种概念的情况下也能快速上手人脸识别呢?

答案当然有,那就是:face-api.js

face-api.js

face-api.js 是大神 Vincent Mühler 的最新力作,他为人所知的开源项目还有 opencv4nodejsface-recognize(NodeJs 的人脸识别包,不过现在 face-api.js 已经支持 Node 端了,他推荐直接使用 face-api)

face-api.js 是一个建立在 Tensorflow.js 内核上的 Javascript 模块,它实现了三种卷积神经网络(CNN)架构,用于完成人脸检测、识别和特征点检测任务。简而言之,借助它,前端也能很轻松的完成人脸识别的工作。

原理简析

想看实现的童鞋请直接略过这一段,直接开始上手操作。

我们知道机器学习有几个基本要素:数据,模型,算法。他们之间的关系如下:

Untitled/Untitled%206.png?imageView&thumbnail=400x400

  • 训练数据: 训练数据就是一系列打过标签的数据,比如一系列人脸和不是人脸的图片数据。
  • 模型(这里我们主要指监督学习模型): 模型你可以简单理解为是一个预测函数(f(x) = y),简单来说就是根据输入的数据,能给出结果。
  • 算法: 算法就是教机器如何获得最优的模型(损失最小)。比如当机器通过当前模型识别到一张训练图片为人脸,但是标签是「非人脸」,此时就需要根据算法对模型进行调整。常见的算法有例如:梯度下降法(Gradient Descent),共轭梯度法(Conjugate Gradient),牛顿法和拟牛顿法,模拟退火法(Simulated Annealing)……

所以,我们可以这么说,只要有了一个训练好的预测模型,我们就可以对未知数据进行分析预测了。

face-api 的原理

首先,为了在图片中识别出人脸,我们需要告诉机器什么样的脸是人脸,因此我们需要大量的人脸照片,并且标明里面的各种脸部特征数据,让机器去学习:

Untitled/Untitled%207.png?imageView&thumbnail=400x400

face-api.js 针对人脸检测工作实现了一个 SSD(Single Shot Multibox Detector)算法,它本质上是一个基于 MobileNetV1 的卷积神经网络(CNN),同时在网络的顶层加入了一些人脸边框预测层。

然后 face-api.js 会通过该算法让机器不断的学习并优化,从而训练出模型,通过该模型可以识别出所有的人脸边界框

光识别出人脸还远远不够,我们的很多应用都需要找到人脸的特征点(眉毛,鼻子,嘴巴这些的)。因此 face-api.js 会从图片中抽取出每个边界框中的人脸居中的图像,接着将其再次作为输入传给人脸识别网络,让其学习。

为了实现特征点识别这个目标,face-api.js 又实现了一个简单的卷积神经网络,它可以返回给定图像的 68 个人脸特征点:

通过该算法,face-api.js 训练了一系列的模型,通过使用这些已经训练好的模型,我们可以快速实现我们想要的功能。

Untitled/Untitled%208.png?imageView&thumbnail=400x400

face-api.js 的使用方法

引入方式

如果你不使用打包工具的话,可以直接导入 face-api.js 的脚本:dist/face-api.js 获得最新的版本,或者从 dist/face-api.min.js 获得缩减版,并且导入脚本:

<script data-original="face-api.js"></script>

如果你使用 npm 包管理工具,可以输入如下指令:

npm i face-api.js

初始化

我们之前说过,face-api 它实现了一系列的卷积神经网络,并针对网络和移动设备进行了优化。所有的神经网络实例在 faceapi.nets中获取到

var nets = {
    ssdMobilenetv1: new SsdMobilenetv1(), // ssdMobilenetv1 目标检测
    tinyFaceDetector: new TinyFaceDetector(),  // 人脸识别(精简版)
    tinyYolov2: new TinyYolov2(),   // Yolov2 目标检测(精简版)
    mtcnn: new Mtcnn(),   // MTCNN
    faceLandmark68Net: new FaceLandmark68Net(),  // 面部 68 点特征识别
    faceLandmark68TinyNet: new FaceLandmark68TinyNet(), // 面部 68 点特征识别(精简版)
    faceRecognitionNet: new FaceRecognitionNet(),  // 面部识别
    faceExpressionNet: new FaceExpressionNet(),  //  表情识别
    ageGenderNet: new AgeGenderNet()  // 年龄识别
};

其中 MobileNetsyolov2 是业内比较有名的目标检测算法,有兴趣的可以点击链接去看论文(我是看不懂),这篇文章 简要介绍了这些算法,大概就是说他们的检测速度和检测效率都不错。这里你可以根据自己的需要选择不同的算法,加载不同的模型。

官方推荐使用ssdMobilenetv1,因为它的识别精度比较高,但是检索速度相对较慢,如果是实时检测的场景,它的检索速度可能会成为问题,因此,今年下半年作者把 MTCNN 算法也引入了,如果想用实时人脸检测的场景,可以试试 MTCNN。(可以看看作者 这篇文章

模型加载

通过之前的介绍我们也可以知道,模型才是重中之重,有了训练好的模型,我们就可以跳过训练的阶段,直接使用来做人脸识别了。

这也就是国外一个机器学习的布道者 Dan Shiffman 在 视频 中一直所强调的:并不是所有的机器学习入门都应该从学习算法入手,毕竟术业有专攻,目前已经有很多人建立了很多成熟的模型(图形检测,文本识别,图像分类……),我们可以站在巨人的肩膀上去做更多有意思的事情。

face-api 本身也提供了一系列的模型数据(/weights),可以开箱即用:

await faceapi.nets.ssdMobilenetv1.load('/weights')

其中 /weights 是放了 manifest.json 和 shard 文件的目录,建议把官方的 weights 目录直接拷贝下来,因为经常你需要几个模型同时使用。

识别

face-api 提供了很多高级的方法可以使用,其中最方便的就是detectAllFaces / detectSingleFace(input, options) , 注意:它默认是使用SSD Mobilenet V1 ,如果要使用Tiny FaceDetector,需要手动指定:

const detections1 = await faceapi.detectAllFaces(input, new faceapi.SsdMobilenetv1Options())
const detections2 = await faceapi.detectAllFaces(input, new faceapi.TinyFaceDetectorOptions())

其中 detect 系方法都支持链式调用,因此你可以这样用:

await faceapi.detectAllFaces(input)
await faceapi.detectAllFaces(input).withFaceExpressions()
await faceapi.detectAllFaces(input).withFaceLandmarks()
await faceapi.detectAllFaces(input).withFaceLandmarks().withFaceExpressions()

获取识别数据

进行识别操作后,返回的数据是什么样的呢?

如果你是进行的全脸识别,那么数据是一个数组,其中 detection 是默认会有的属性,它提供了一个人脸部的盒子信息

[{detection:{
    box: {
        x: 156.22306283064717
        y: 76.60605907440186
        width: 163.41096172182577
        height: 182.21931457519534
        left: 156.22306283064717
        top: 76.60605907440186
        right: 319.63402455247297
        bottom: 258.82537364959717
        area: 29776.633439024576
        topLeft: Point
        topRight: Point
        bottomLeft: Point
        bottomRight: Point
    }
    ……
}]

如果你进行了链式操作,比如 withFaceLandmarks() 那这个对象会增加一个landmarks的属性,以此类推。

[{detection, landmarks, ……}]

其中landmarks提供了获取脸部各种特征点的方法:

const landmarkPositions = landmarks.positions  // 获取全部 68 个点
const jawOutline = landmarks.getJawOutline()  // 下巴轮廓
const nose = landmarks.getNose()  // 鼻子
const mouth = landmarks.getMouth()  // 嘴巴
const leftEye = landmarks.getLeftEye()  // 左眼
const rightEye = landmarks.getRightEye()  // 右眼
const leftEyeBbrow = landmarks.getLeftEyeBrow()  // 左眉毛
const rightEyeBrow = landmarks.getRightEyeBrow()  // 右眉毛

处理识别数据

要知道,你拿到的数据是根据图片的真实数据来处理的,但我们在网页展示的图片通常不会是 1:1 的实际图片,也就是说图片会进行缩放/扩大处理。比如一张图片是 1000x1000 的,图片上的人脸嘴巴可能在(600,500)这个位置,但是我们实际展示的是 600x600 的图片,如果根据(600,500)这个坐标去画,那早就在画布外了。

因此如果我想要在图片上做一点事情,我们需要把当前的数据进行一个转换,让它的数据匹配特定的大小,这里,可以用它提供的 matchDimensions(canvas, displaySize)resizeResults(result, displaySize) 方法:

// 把 canvas 固定到 displaySize 的大小
faceapi.matchDimensions(canvas, displaySize) 
// 把数据根据 displaySize 做转换 
const resizedResults = faceapi.resizeResults(detectionsWithLandmarks, displaySize)

其中 displaySize 是一个拥有{ width, height }的对象,所以你也可以直接传入带 width 和 height 的 DOM 元素,如 <canvas />, <img />

根据数据绘制图形

光拿到数据可没用,我们主要目的是为了绘制图形,在绘制这一块 face-api 也是提供了一系列高级方法,比如:

faceapi.draw.drawDetections(canvas, resizedDetections)  // 直接在识别区域画个框
faceapi.draw.drawFaceLandmarks(canvas, resizedResults)  // 直接画出识别的的特征点

Untitled/Untitled%209.png?imageView&thumbnail=400x400

(以下测试图片均是采用从百度搜「女生头像」搜到的小姐姐,如有侵权,请告知)

当然你还可以在特定位置画个框或文字,具体用法可以参考:DrawBoxDrawTextField

const drawOptions = {
  label: 'Hello I am a box!',
  lineWidth: 2
}
const drawBox = new faceapi.draw.DrawBox(resizedResults[0].detection.box, drawOptions)
drawBox.draw(canvas)

Untitled/Untitled%2010.png?imageView&thumbnail=400x400

圣诞帽的绘制

说了这么多,突然发现还没到我们的主题,画圣诞帽!让我们赶紧回来。

确定圣诞帽的位置

现在假定我现在拥有了所有的面部数据,我应该如何确定圣诞帽的正确位置?首先,我们必须明确一点,圣诞帽应该是要戴在头顶的,应该没有人把圣诞帽戴在眉毛上吧?(好吧当我没说)

Untitled/Untitled%2011.png?imageView&thumbnail=400x400

但是人脸识别的数据中一般是不包含头顶的,这可咋办?还好我小学一年级学过半个学期的素描,在素描中有个很重要的概念叫三庭五眼

Untitled/Untitled%2012.png?imageView&thumbnail=400x400

也是说正常人的发际线到眉骨的距离是眉骨到下颌距离的一半(作为程序猿的我表示,该规则可能已经不适用了)。

因此我们可以通过获取眉毛的坐标和下颌的坐标来计算出头顶的位置:

/**
 * 获取头顶的坐标
 * @param {*} midPos 眉心点坐标
 * @param {*} jawPos 下巴底点坐标
 */
const getHeadPos = (midPos, jawPos) => {
  // 获取线的 k 值
  const k = getK(midPos, jawPos);
  // 获取眉心到下颌的距离
  const distanceOfEye2Jaw = getDistance(midPos, jawPos);
  return getPos(k, distanceOfEye2Jaw / 2, midPos);
};

 在这里让我们复习几个解析几何的公式:

  • 两点之间距离公式:

Untitled/Untitled%2013.png?imageView&thumbnail=400x400

  • 根据两点确定斜率:

    Untitled/Untitled%2014.png?imageView&thumbnail=400x400

  • 点到直线的距离公式:

Untitled/Untitled%2015.png?imageView&thumbnail=400x400

  • 相互垂直的直线,斜率之积为-1

Untitled/Untitled%2016.png?imageView&thumbnail=400x400

要特别注意的是,由于 Canvas 默认的坐标系的结构和我们之前数学课上学的不太一样,我们把它逆时针旋转 90 度可以发现它的 x,y 轴跟我们认识的是反的,因此为了方便,我们通常在代入公式计算的时候把 x,y 进行一下调换。

Untitled/Untitled%2017.png?imageView&thumbnail=400x400

/**
 * 获取 K 值
 * @param {*} a
 * @param {*} b
 */
const getK = (a, b) => (a.x - b.x) / (a.y - b.y)

/**
 * 获取两点之间距离
 * @param {*} a
 * @param {*} b
 */
const getDistance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));

因此目前已知眉心的坐标,下颌坐标,可以计算出这条直线的斜率,然后根据眉心到头顶的距离(眉心到下巴的一半),可以算出头顶的坐标:

/**
 * 已知 K,d, 点,求另一个点
 * @param {*} k 值
 * @param {*} d 距离
 * @param {*} point 一个基础点
 */
const getPos = (k, d, point) => {
  // 取 y 变小的那一边
  let y = -Math.sqrt((d * d) / (1 + k * k)) + point.y;
  let x = k * (y - point.y) + point.x;
  return { x, y };
};

图片合成

当我们已经知道了圣诞帽子的位置了,那接下的问题就是如何把圣诞帽合成到头像上去了,这里我们采用 Canvas 来实现 ,原理很简单:首先把头像绘制到 Canvas 上,然后再继续绘制圣诞帽就行了。

由于图片中可能不止一个面部数据,可能需要绘制多个帽子:

/**
 * 获取图片
 * @param {*} src 图片地址
 * @param {*} callback
 */
function getImg(src, callback) {
  const img = new Image();
  img.setAttribute('crossOrigin', 'anonymous');
  img.src = src;
  img.onload = () => callback(img);
}
/**
 * 绘制主流程
 * @param {*} canvas 
 * @param {*} options 
 */
function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg' } = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上(由于图片中可能不止一个面部数据,可能需要绘制多个帽子)
    for(let i = 0, len=info.length;i < len;i++) {
        const { headPos } = info[i];
        getImg('images/hat.png', img => ctx.drawImage(img, headPos.x, headPos.y, 200, 120));
    }
}

我们可以看到帽子已经绘制上去了,但是位置有点奇怪:

Untitled/Untitled%2018.png?imageView&thumbnail=400x400

这是因为绘制图片的时候是按图片的左上角为原点绘制的,因此我们在实际绘制帽子的时候需要对坐标进行一个偏移:

/**
 * 根据我当前的圣诞帽元素进行一些偏移(我的图片大小是 200*130), 圣诞帽可佩戴部分的中心 (60,60)
 * @param {*} x
 * @param {*} y
 */
const translateHat = (x, y) => {
    return {
        x: x - 60,
        y: y - 60,
    };
};

function drawing(canvas, options) {
    ……
    const {x, y} = translateHat(headPos.x, headPos.y);
    getImg('images/hat.png', img => ctx.drawImage(img, x, y, 200, 120));
    ……
}

Untitled/Untitled%2019.png?imageView&thumbnail=400x400

这么看起来好多了,感觉自己棒棒哒!

Untitled/Untitled%2020.png?imageView&thumbnail=400x400

优化

但是有小伙伴可以会发现,这样的结果还是有点问题:

  • 帽子大小是固定的,但是头像的上的面孔可大可小,大脸放小帽子显然有点不合适。
  • 帽子的朝向是固定的,如果有人的头像是偏着的呢,帽子是不是也应该偏过来?

因此我们还需要继续做优化:

圣诞帽的大小

圣诞帽的大小我们可以根据识别出的脸的大小来确定,通常来说,帽子可戴的宽度等于脸宽就行了(考虑到展示效果可以略大),我这里强调可戴宽度是因为一个圣诞帽的图片中只有一部分是可戴区域

Untitled/Untitled%2021.png?imageView&thumbnail=400x400

// 0.7 为可戴区域占总区域的比重(为了让帽子更大一点,选择 0.6),0.65 是图片的宽高比
const picSize = { width: faceWidth / 0.6, height: (faceWidth * 0.65) / 0.6 };

而面部的大小可以通过 jawOutlinePoints的起始点距离来计算

/**
 * 获取脸的宽度(即帽子宽度)
 * @param {*} outlinePoints
 */
const getFaceWith = outlinePoints => getDistance(outlinePoints[0], outlinePoints[outlinePoints.length - 1])

圣诞帽的角度

圣诞帽的角度该如何计算呢?其实也很简单,知道头的偏转角度就行了,而头的偏转角度,直接用脸中线(眉心到下颌)和 y 轴的夹角就行了(直接用 atan2 算出来的是补角,需要用 180 度减),这里考虑到后续使用方便直接用的弧度。

/**
 * 获取脸的倾斜弧度
 * @param {*} jawPos
 * @param {*} midPointOfEyebrows
 */
const getFaceRadian = (jawPos, midPointOfEyebrows) =>
    Math.PI - Math.atan2(jawPos.x - midPointOfEyebrows.x, jawPos.y - midPointOfEyebrows.y); //弧度  0.9272952180016122

Untitled/Untitled%2022.png?imageView&thumbnail=400x400

Canvas 中图片旋转

注意,在 Canvas 中没办法直接旋转图片,只能旋转画布,而且画布是按照原点旋转的,这点会特别坑。

Untitled/Untitled%2023.png?imageView&thumbnail=400x400

这里,我们只想让图片按照中心旋转怎么办?我们可以先让 Canvas 把原点平移到帽子的位置,然后再通过帽子的内部偏移使得帽子中心刚好在原点,此时再旋转画布把帽子画上就只影响这一个帽子图片了。

Untitled/Untitled%2024.png?imageView&thumbnail=400x400

/**
 * 绘制帽子 
 * @param {*} ctx 画布实例
 * @param {{}} config 配置
 */
function drawHat(ctx, config) {
    const { headPos, angle, faceWidth } = config;
    getImg('images/hat.png?imageView&thumbnail=400x400', img => {
        // 保存画布
        ctx.save();
        // 画布原点移动到画帽子的地方
        ctx.translate(headPos.x, headPos.y);
        // 旋转画布到特定角度
        ctx.rotate(angle);
        // 偏移图片,使帽子中心刚好在原点
        const { x, y, width, height } = translateHat(faceWidth, 0, 0);
        // 我的圣诞帽子实际佩戴部分长度只有 0.75 倍整个图片长度
        ctx.drawImage(img, x, y, width, height);
        // 还原画布
        ctx.restore();
    });
}

这样整个绘制的主流程大概就是这样:

function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg'} = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上
    for (let i = 0, len = info.length; i < len; i++) {
        drawHat(ctx, info[i]);  
    }
}

成品展示

Untitled/Untitled%2025.png?imageView&thumbnail=400x400

Untitled/Untitled%2026.png?imageView&thumbnail=400x400

Untitled/Untitled%2027.png?imageView&thumbnail=400x400

Untitled/Untitled%2028.png?imageView&thumbnail=400x400

image-20191223201537510

是不是感觉已经很完美了?目前已经能实现对各种大小的脸,不同朝向的脸进行适配了。甚至连狗狗的头像也能识别出来,这都得利于 face-api.js 提供的模型中也有狗狗的脸训练数据。

当然,这个例子实际上还是很不完善,因为当你测试的图片多的时候就会发现,对侧脸头像的识别还是有问题:

image-20191223201619850

这是因为脸的大小用之前的方法计算的其实是不准确的,实际上还应该根据双边眼睛的比例知道用户现在是侧脸还是正面,从而继续调整帽子的大小和位置。但是这里就不继续优化了,感兴趣的小伙伴可以自己去琢磨琢磨如何修改。

写在最后

通过上面这个小例子,我们可以发现前端利用机器学习也可以实现很多不可思议的有趣玩意,再结合 VRARwebRTC,我们甚至可以做一个纯前端的互动小游戏。

当然就算在这个例子上,你也可以进行扩展,比如绘制圣诞胡须,化妆……

如果你想继续深入学习机器学习的相关知识,可以去看看 Coursea 的 机器学习入门课程,如果你想深入学习一下 Tensorflow,可以去看看 Tensorflow.js 的 官方文档

虽然之前有吐槽 Tensorflow.js 知识点太多的问题,但是不得不说 Google 的文档写的还是不错的,提供了很多案例,手把手教你如何实现一些简单的功能:手写数字识别,预测,图片分类器……所以对 Tensorflow.js 感兴趣的童鞋不妨去它的官方文档中逛逛。

不过毕竟 Tensorflow.js 还是很底层的库,如果你只是想用机器学习做一些有趣的事情,不妨尝试一下 ml5.js,这里有一套学习视频

最后,祝大家圣诞快乐!

参考

https://github.com/justadudewhohacks/face-api.js

face-api.js — JavaScript API for Face Recognition in the Browser with tensorflow.js

https://tensorflow.google.cn/js/tutorials

https://www.youtube.com/watch?v=jmznx0Q1fP0

https://www.cnblogs.com/suyuanli/p/8279244.html

https://learn.ml5js.org/docs/#/

本文发布自 网易云音乐前端团队,文章未经授权禁止任何形式的转载。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们
查看原文

赞 64 收藏 34 评论 1

aaanthony 赞了回答 · 2019-01-17

解决segmentfault的消息实时通知是怎么做到的?

应该是用的socket.io 长连接的推送 作为服务端推送可以用pusher

关注 7 回答 4

aaanthony 发布了文章 · 2018-05-18

Win10远程桌面 出现 身份验证错误,要求的函数不受支持,这可能是由于CredSSP加密Oracle修正 解决方法

最近安装了win10的更新以后,mstsc远程桌面连接的时候,就出现这个问题。

clipboard.png

根据提示的链接看微软解答,可能是因为自己电脑上安装了更新,提升了策略的安全级别,但是远程端没有更新,所以就阻止连接了。

解决方法有两种:

(1)组策略修改。要求专业版系统才可以。

运行 gpedit.msc

本地组策略:

计算机配置>管理模板>系统>凭据分配>加密Oracle修正

选择启用并选择易受攻击。

(2)修改注册表

很多人的电脑可能是家庭版系统,就不支持组策略,可以通过修改注册表的方法。

打开注册表编辑器(运行 “regedit”)

找文件夹 路径:HKLM(缩写)SoftwareMicrosoftWindowsCurrentVersionPoliciesSystemCredSSPParameters

一般到System就找不到下面的文件夹了,这时候自己创建就行,一直建到Parameters文件夹

在Parameters里,新建一个 DWORD(32)位类型的项目,名称 “AllowEncryptionOracle” ,值 : 2

这时候再mstsc连接,就可以了。不行的话重启试试。

查看原文

赞 0 收藏 0 评论 0

aaanthony 发布了文章 · 2018-05-01

L1、L2正则化的区别

简单读了一些文章,总结一下L1和L2正则化的区别吧,似乎是非常容易被问到的一个问题。

L1,L2正则化

机器学习中, 损失函数后面一般会加上一个额外项,常用的是l1-norm和l2-norm,即l1范数和l2范数。

可以看作是损失函数的惩罚项。正则化项一般是模型复杂度的单调递增函数,模型越复杂,正则化值就越大。

upload successful

上面的目标函数,第一项是模型要最小化的误差,第二项是正则化项,λ>=0调节两者之间关系的系数。

正则化项可以取不同的形式。

L0范数
是指正则化项是 参数矩阵W中非0元素的个数,也就是说希望W的大部分元素都是0,W是稀疏的。

由于L0正则项非连续非凸不可求导,难以找到有效解,转而使用L1范数。

L1范数
正则化项是向量中各个元素的绝对值之和

L0和L1范数可以实现让参数矩阵稀疏,让参数稀疏的好处,可以实现对特征的选择(权重为0表示对应的特征没有作用,被丢掉),也可以增强模型可解释性(例如研究影响疾病的因素,只有少数几个非零元素,就可以知道这些对应的因素和疾病相关)

L1又称Lasso。

L2范数
功效是解决过拟合问题。当模型过于复杂,就会容易出现过拟合问题。

upload successful

L2范数是指向量各个元素的平方,求和,然后再求平方根
使L2范数最小,可以使得W的每个元素都很小,都接近于0,但和L1范数不同,L2不能实现稀疏,不会让值等于0,而是接近于0。一般认为,越小的参数,模型越简单,越简单的模型就不容易产生过拟合现象。

L2又称Ridge,也称岭回归。

小结

公式:

upload successful

区别:
使用L1范数,可以使得参数稀疏化;
使用L2范数,倾向于使参数稠密地接近于0,避免过拟合。

source

文章同步在我的博客

Reference

更多详细的公式,以及解释和分析,可参考

查看原文

赞 1 收藏 0 评论 0

aaanthony 收藏了文章 · 2018-04-28

RNN&Attention机制&LSTM 入门了解

RNN

经典的RNN结构:

在实际应用中,我们还会遇到很多序列形的数据,如:

  • 自然语言处理问题。x1可以看做是第一个单词,x2可以看做是第二个单词,依次类推。
  • 语音处理。此时,x1、x2、x3……是每帧的声音信号。
  • 时间序列问题。例如每天的股票价格等等

序列形的数据就不太好用原始的神经网络处理了。为了建模序列问题,RNN引入了隐状态h(hidden state)的概念,h可以对序列形的数据提取特征,接着再转换为输出。先从h1的计算开始看:

clipboard.png
图示中记号的含义是:

  • 圆圈或方块表示的是向量。
  • 一个箭头就表示对该向量做一次变换。如上图中h0和x1分别有一个箭头连接,就表示对h0和x1各做了一次变换。

在很多论文中也会出现类似的记号,初学的时候很容易搞乱,但只要把握住以上两点,就可以比较轻松地理解图示背后的含义。

h2的计算和h1类似。要注意的是,在计算时,每一步使用的参数U、W、b都是一样的,也就是说每个步骤的参数都是共享的,这是RNN的重要特点,一定要牢记。

clipboard.png
我们这里为了方便起见,只画出序列长度为4的情况,实际上,这个计算过程可以无限地持续下去。
我们目前的RNN还没有输出,得到输出值的方法就是直接通过h进行计算:

clipboard.png
正如之前所说,一个箭头就表示对对应的向量做一次类似于f(Wx+b)的变换,这里的这个箭头就表示对h1进行一次变换,得到输出y1。

剩下的输出类似进行(使用和y1同样的参数V和c):

clipboard.png
这就是最经典的RNN结构,我们像搭积木一样把它搭好了。它的输入是x1, x2, .....xn,输出为y1, y2, ...yn,也就是说,输入和输出序列必须要是等长的。

由于这个限制的存在,经典RNN的适用范围比较小,但也有一些问题适合用经典的RNN结构建模,如:

  • 计算视频中每一帧的分类标签。因为要对每一帧进行计算,因此输入和输出序列等长。
  • 输入为字符,输出为下一个字符的概率。这就是著名的Char RNN,可以用来生成文章,诗歌,甚至是代码,非常有意思。

N VS 1结构:

有的时候,我们要处理的问题输入是一个序列,输出是一个单独的值而不是序列,只在最后一个h上进行输出变换就可以了:

clipboard.png
这种结构通常用来处理序列分类问题。如输入一段文字判别它所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。

1 VS N结构:

输入不是序列而输出为序列的情况,我们可以只在序列开始进行输入计算:

clipboard.png
还有一种结构是把输入信息X作为每个阶段的输入:

clipboard.png
这种1 VS N的结构可以处理的问题有:

  • 从图像生成文字(image caption),此时输入的X就是图像的特征,而输出的y序列就是一段句子
  • 从类别生成语音或音乐等

N vs M结构

下面我们来介绍RNN最重要的一个变种:N vs M。这种结构又叫Encoder-Decoder模型,也可以称之为Seq2Seq模型。

原始的N vs N RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。

为此,Encoder-Decoder结构先将输入数据编码成一个上下文向量c:

clipboard.png
得到c有多种方式,最简单的方法就是把Encoder的最后一个隐状态赋值给c,还可以对最后的隐状态做一个变换得到c,也可以对所有的隐状态做变换。

拿到c之后,就用另一个RNN网络对其进行解码,这部分RNN网络被称为Decoder。具体做法就是将c当做之前的初始状态h0输入到Decoder中:

clipboard.png
还有一种做法是将c当做每一步的输入:

clipboard.png
由于这种Encoder-Decoder结构不限制输入和输出的序列长度,因此应用的范围非常广泛,比如:

  • 机器翻译。Encoder-Decoder的最经典应用,事实上这一结构就是在机器翻译领域最先提出的
  • 文本摘要。输入是一段文本序列,输出是这段文本序列的摘要序列。
  • 阅读理解。将输入的文章和问题分别编码,再对其进行解码得到问题的答案。
  • 语音识别。输入是语音信号序列,输出是文字序列。
  • …………

Attention机制

在Encoder-Decoder结构中,Encoder把所有的输入序列都编码成一个统一的语义特征c再解码,因此, c中必须包含原始序列中的所有信息,它的长度就成了限制模型性能的瓶颈,不论输入长短都将其编码成一个固定长度的向量表示,这使模型对于长输入序列的学习效果很差(解码效果很差),如机器翻译问题,当要翻译的句子较长时,一个c可能存不下那么多信息,就会造成翻译精度的下降。
Attention机制通过在每个时间输入不同的c来解决这个问题,下图是带有Attention机制的Decoder:

clipboard.png
相当于之前将原始输入信息压缩到一个c中,attention后是将信息按照不同特征分别储存到多个c中,并且每一个c会自动去选取与当前所要输出的y最合适的上下文信息。具体来说,我们用 a_{ij} 衡量Encoder中第j阶段的hj和解码时第i阶段的相关性,最终Decoder中第i阶段的输入的上下文信息 c_i 就来自于所有 h_j 对 a_{ij} 的加权和。
我们从输出端,即decoder部分,倒过来一步一步看公式。

(1)clipboard.png

clipboard.png
是指decoder在t时刻的状态输出,clipboard.png
是指decoder在t-1时刻的状态输出,clipboard.png
是t-1时刻的label(注意是label,不是我们输出的y)clipboard.png看下一个公式,clipboard.png是一个RNN。

(2)clipboard.png

clipboard.png
是指第j个输入在encoder里的输出,clipboard.png
是一个权重

(3)clipboard.png

这个公式跟softmax是何其相似,道理是一样的,是为了得到条件概率,这个的意义是当前这一步decoder对齐第j个输入的程度。最后一个公式,
(4)clipboard.png

这个可以用一个小型的神经网络来逼近。好了,把四个公式串起来看,这个attention机制可以总结为一句话,“当前一步输出应该对齐哪一步输入,主要取决于前一步输出和这一步输入的encoder结果”。

LSTM

由于RNN也有梯度消失的问题,因此很难处理长序列的数据,大牛们对RNN做了改进,得到了RNN的特例LSTM(Long Short-Term Memory),它可以避免常规RNN的梯度消失。
在RNN模型里,每个序列索引位置t都有一个隐藏状态h(t),如果我们略去每层都有的o(t),L(t),y(t),则RNN的模型可以简化成如下图的形式:

clipboard.png
中可以很清晰看出在隐藏状态h(t)由x(t)和h(t−1)得到。得到h(t)后一方面用于当前层的模型损失计算,另一方面用于计算下一层的h(t+1)。

    由于RNN梯度消失的问题,大牛们对于序列索引位置t的隐藏结构做了改进,可以说通过一些技巧让隐藏结构复杂了起来,来避免梯度消失的问题,这样的特殊RNN就是我们的LSTM。由于LSTM可以理解为修改隐藏层的RNN,这里我们以最常见的RNN结构为例讲述。LSTM的结构如下图:
    
clipboard.png

LSTM模型结构剖析:

上面我们给出了LSTM的模型结构,下面我们就一点点的剖析LSTM模型在每个序列索引位置t时刻的内部结构。

    从上图中可以看出,在每个序列索引位置t时刻向前传播的除了和RNN一样的隐藏状态h(t),还多了另一个隐藏状态,如图中上面的长横线。这个隐藏状态我们一般称为细胞状态(Cell State),记为C(t)。如下图所示:
    
clipboard.png

除了细胞状态,LSTM图中还有了很多奇怪的结构,这些结构一般称之为门控结构(Gate)。LSTM在在每个序列索引位置t的门一般包括遗忘门,输入门和输出门三种。下面我们就来研究上图中LSTM的遗忘门,输入门和输出门以及细胞状态。

LSTM之遗忘门

遗忘门(forget gate)顾名思义,是控制是否遗忘的,在LSTM中即以一定的概率控制是否遗忘上一层的隐藏细胞状态。遗忘门子结构如下图所示:

clipboard.png

    图中输入的有上一序列的隐藏状态h(t−1)和本序列数据x(t),通过一个激活函数,一般是sigmoid,得到遗忘门的输出f(t)。由于sigmoid的输出f(t)在[0,1]之间,因此这里的输出f^{(t)}代表了遗忘上一层隐藏细胞状态的概率。用数学表达式即为:
    
    f(t)=σ(Wfh(t−1)+Ufx(t)+bf)
    
其中Wf,Uf,bf为线性关系的系数和偏倚,和RNN中的类似。σ为sigmoid激活函数。

LSTM之输入门

输入门(input gate)负责处理当前序列位置的输入,它的子结构如下图:

clipboard.png

    从图中可以看到输入门由两部分组成,第一部分使用了sigmoid激活函数,输出为i(t),第二部分使用了tanh激活函数,输出为a(t), 两者的结果后面会相乘再去更新细胞状态。用数学表达式即为:
    
i(t)=σ(Wih(t−1)+Uix(t)+bi)
a(t)=tanh(Wah(t−1)+Uax(t)+ba)

其中Wi,Ui,bi,Wa,Ua,ba,为线性关系的系数和偏倚,和RNN中的类似。σ为sigmoid激活函数。

LSTM之细胞状态更新

在研究LSTM输出门之前,我们要先看看LSTM之细胞状态。前面的遗忘门和输入门的结果都会作用于细胞状态C(t)。我们来看看从细胞状态C(t−1)如何得到C(t)。如下图所示:

clipboard.png

    细胞状态C(t)由两部分组成,第一部分是C(t−1)和遗忘门输出f(t)的乘积,第二部分是输入门的i(t)和a(t)的乘积,即:
    
C(t)=C(t−1)⊙f(t)+i(t)⊙a(t)

其中,⊙为Hadamard积,在DNN中也用到过。

LSTM之输出门

有了新的隐藏细胞状态C(t),我们就可以来看输出门了,子结构如下:

clipboard.png

    从图中可以看出,隐藏状态h(t)的更新由两部分组成,第一部分是o(t), 它由上一序列的隐藏状态h(t−1)和本序列数据x(t),以及激活函数sigmoid得到,第二部分由隐藏状态C(t)和tanh激活函数组成, 即:
    
o(t)=σ(Woh(t−1)+Uox(t)+bo)
h(t)=o(t)⊙tanh(C(t))

参考资料:
https://zhuanlan.zhihu.com/p/...
http://www.cnblogs.com/pinard...
https://www.zhihu.com/collect...
http://www.cnblogs.com/pinard...

查看原文

aaanthony 关注了问题 · 2018-01-16

解决如何将表单里的音频audio标签进行提交

我想要将表单里的audio标签,进行提交。使用录音插件后生成的audio标签里有音频资源的src,能够点击播放。我要怎么取得音频资源,然后将音频在表单提交时一起提交呢?
这是页面html:

<li>
    <audio controls data-original="blob:http://127.0.0.1:8080/5fbe913f-ab03-4847-9cb2-2b01c106f9db"></audio>
    <a href="blob:http://127.0.0.1:8080/5fbe913f-ab03-4847-9cb2-2b01c106f9db" download="2016-11-11T06:33:47.532Z.wav">2016-11-11T06:33:47.532Z.wav</a>
    <button>删除该条录音</button>
</li>

谢谢了

关注 3 回答 1

aaanthony 赞了问题 · 2018-01-16

解决如何将表单里的音频audio标签进行提交

我想要将表单里的audio标签,进行提交。使用录音插件后生成的audio标签里有音频资源的src,能够点击播放。我要怎么取得音频资源,然后将音频在表单提交时一起提交呢?
这是页面html:

<li>
    <audio controls data-original="blob:http://127.0.0.1:8080/5fbe913f-ab03-4847-9cb2-2b01c106f9db"></audio>
    <a href="blob:http://127.0.0.1:8080/5fbe913f-ab03-4847-9cb2-2b01c106f9db" download="2016-11-11T06:33:47.532Z.wav">2016-11-11T06:33:47.532Z.wav</a>
    <button>删除该条录音</button>
</li>

谢谢了

关注 3 回答 1

aaanthony 发布了文章 · 2017-11-02

ubuntu16.04下安装hadoop+spark单机版

说明
本文为自己配置单机版hadoop和spark的过程记录,仅供参考,因环境和版本问题,方法可能会有不同。
如有错误,欢迎批评指正。

下载

hadoop: http://hadoop.apache.org/releases.html#Download
spark: http://spark.apache.org/downloads.html
scala:  http://www.scala-lang.org/download/all.html
// 注意选择版本2.11及以上,如果spark是2.0之后的话

解压放置到需要的位置。

提示:以下路径有的因电脑而异,请替换成自己电脑中的路径。

Hadoop

java环境变量

ref: http://www.linuxidc.com/Linux...

首先要让系统中有JAVA_HOME环境变量。如果之前没有配置,需要手动进行配置。

sudo gedit /etc/environment

改成这样:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:$JAVA_HOME/bin"
export CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

保存后source一下立即生效:

source /etc/environment

为了避免重启失效,需要配置所有用户的环境变量:

sudo gedit /etc/profile

加入如下内容:

#set Java environment
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH

使之生效:

source /etc/profile

JAVA_HOME配置没有问题以后,可以进入hadoop安装路径的bin中,终端到此路径,执行

./hadoop

如果出现提示,说明已经可以使用。

将hadoop/bin也加入环境变量

这样以后可以直接在任意位置使用hadoop命令进行启动,无需先cd。

sudo gedit ~/.bashrc

在最后加入:

# hadoop
export PATH=${JAVA_HOME}/bin:/home/anthony/hadoop/bin:$PATH

使之生效:

source ~/.bashrc

在任意路径下的终端输入 hadoop, 如果配置ok的话,可以直接出现提示。

运行word count示例

cd到hadoop的share/hadoop/mapreduce目录下,试一试自带的word count。

cd /home/anthony/hadoop/share/hadoop/mapreduce

先在某位置建立一个文件夹input,放一点文本文档在里面,可以放多几个。然后执行以下命令:

hadoop jar hadoop-mapreduce-examples-2.8.2.jar wordcount ~/hadoop/input ~/hadoop/output
// 后两个参数为输入文件位置和输出位置,输出位置似乎不能预先存在此output文件夹。

如果执行成功,会输出一堆提示,最终生成part-r-00000和_SUCCESS等文件。

Scala

解压scala到相应位置,配置一下路径即可。

sudo gedit ~/.bashrc

追加:

# scala
export SCALA_HOME=/home/anthony/scala/
export PATH=${SCALA_HOME}/bin:${JAVA_HOME}/bin:$PATH

source一下:

source ~/.bashrc

在任意位置,命令输入 scala,如果可以进入,说明配置成功。

Spark

解压到指定位置即可,然后设置一下环境变量。


sudo gedit ~/.bashrc

在末尾追加以下内容:

# spark
export SPARK_HOME=/home/anthony/spark/
export PATH=${SPARK+HOME}/bin:$PATH
                                   

source一下使之生效。
在任意目录打开终端,输入命令测试:

spark-shell

如果可以找到命令,而且进入sprk shell,则说明配置是ok的。

Spark shell会输出较多日志,如果不希望看到太多日志,可以调高日志的等级,默认输出INFO及以上级别的日志。1+1

找到spark/conf/, 把目录中的log4j.properties.template 复制一份并删掉后面的.template,然后编辑:

其中有一句

# Set everything to be logged to the console
log4j.rootCategory=INFO, console

改成:

# Set everything to be logged to the console
log4j.rootCategory=WARN, console

即可统一调高级别。

查看原文

赞 1 收藏 0 评论 0

aaanthony 发布了文章 · 2017-08-26

使用haystack实现django全文检索搜索引擎功能

前言

django是python语言的一个web框架,功能强大。配合一些插件可为web网站很方便地添加搜索功能。

搜索引擎使用whoosh,是一个纯python实现的全文搜索引擎,小巧简单。

中文搜索需要进行中文分词,使用jieba

直接在django项目中使用whoosh需要关注一些基础细节问题,而通过haystack这一搜索框架,可以方便地在django中直接添加搜索功能,无需关注索引建立、搜索解析等细节问题。

haystack支持多种搜索引擎,不仅仅是whoosh,使用solr、elastic search等搜索,也可通过haystack,而且直接切换引擎即可,甚至无需修改搜索代码。

配置搜索

1.安装相关包

pip install django-haystack
pip install whoosh
pip install jieba

2.配置django的settings

修改settings.py文件,添加haystack应用:

INSTALLED_APPS = (
    ...
    'haystack', #将haystack放在最后
)

在settings中追加haystack的相关配置:

HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine',
        'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
    }
}

# 添加此项,当数据库改变时,会自动更新索引,非常方便
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

3.添加url

在整个项目的urls.py中,配置搜索功能的url路径:

urlpatterns = [
    ...
    url(r'^search/', include('haystack.urls')),
]

4.在应用目录下,添加一个索引

在子应用的目录下,创建一个名为 search_indexes.py 的文件。

from haystack import indexes
# 修改此处,为你自己的model
from models import GoodsInfo

# 修改此处,类名为模型类的名称+Index,比如模型类为GoodsInfo,则这里类名为GoodsInfoIndex
class GoodsInfoIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)

    def get_model(self):
        # 修改此处,为你自己的model
        return GoodsInfo

    def index_queryset(self, using=None):
        return self.get_model().objects.all()

说明:
1)修改上文中三处注释即可
2)此文件指定如何通过已有数据来建立索引。get_model处,直接将django中的model放过来,便可以直接完成索引啦,无需关注数据库读取、索引建立等细节。
3)text=indexes.CharField一句,指定了将模型类中的哪些字段建立索引,而use_template=True说明后续我们还要指定一个模板文件,告知具体是哪些字段

5.指定索引模板文件

在项目的“templates/search/indexes/应用名称/”下创建“模型类名称_text.txt”文件。

例如,上面的模型类名称为GoodsInfo,则创建goodsinfo_text.txt(全小写即可),此文件指定将模型中的哪些字段建立索引,写入如下内容:(只修改中文,不要改掉object)

{{ object.字段1 }}
{{ object.字段2 }}
{{ object.字段3 }}

6.指定搜索结果页面

在templates/search/下面,建立一个search.html页面。

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
{% if query %}
    <h3>搜索结果如下:</h3>
    {% for result in page.object_list %}
        <a href="/{{ result.object.id }}/">{{ result.object.gName }}</a><br/>
    {% empty %}
        <p>啥也没找到</p>
    {% endfor %}

    {% if page.has_previous or page.has_next %}
        <div>
            {% if page.has_previous %}<a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; 上一页{% if page.has_previous %}</a>{% endif %}
        |
            {% if page.has_next %}<a href="?q={{ query }}&amp;page={{ page.next_page_number }}">{% endif %}下一页 &raquo;{% if page.has_next %}</a>{% endif %}
        </div>
    {% endif %}
{% endif %}
</body>
</html>

7.使用jieba中文分词器

在haystack的安装文件夹下,路径如“/home/python/.virtualenvs/django_py2/lib/python2.7/site-packages/haystack/backends”,建立一个名为ChineseAnalyzer.py的文件,写入如下内容:

import jieba
from whoosh.analysis import Tokenizer, Token


class ChineseTokenizer(Tokenizer):
    def __call__(self, value, positions=False, chars=False,
                 keeporiginal=False, removestops=True,
                 start_pos=0, start_char=0, mode='', **kwargs):
        t = Token(positions, chars, removestops=removestops, mode=mode,
                  **kwargs)
        seglist = jieba.cut(value, cut_all=True)
        for w in seglist:
            t.original = t.text = w
            t.boost = 1.0
            if positions:
                t.pos = start_pos + value.find(w)
            if chars:
                t.startchar = start_char + value.find(w)
                t.endchar = start_char + value.find(w) + len(w)
            yield t


def ChineseAnalyzer():
    return ChineseTokenizer()

8.切换whoosh后端为中文分词

将上面backends目录中的whoosh_backend.py文件,复制一份,名为whoosh_cn_backend.py,然后打开此文件,进行替换:

# 顶部引入刚才添加的中文分词
from .ChineseAnalyzer import ChineseAnalyzer 

# 在整个py文件中,查找
analyzer=StemmingAnalyzer()
全部改为改为
analyzer=ChineseAnalyzer()
总共大概有两三处吧

9.生成索引

手动生成一次索引:

python manage.py rebuild_index

10.实现搜索入口

在网页中加入搜索框:

<form method='get' action="/search/" target="_blank">
    <input type="text" name="q">
    <input type="submit" value="查询">
</form>

丰富的自定义

上面只是快速完成一个基本的搜索引擎,haystack还有更多可自定义,来实现个性化的需求。

参考官方文档:http://django-haystack.readth...

自定义搜索view

上面的配置中,搜索相关的请求被导入到haystack.urls中,如果想自定义搜索的view,实现更多功能,可以修改。

haystack.urls中内容其实很简单,

from django.conf.urls import url  
from haystack.views import SearchView  
  
urlpatterns = [  
    url(r'^$', SearchView(), name='haystack_search'),  
]  

那么,我们写一个view,继承自SearchView,即可将搜索的url导入到自定义view中处理啦。

class MySearchView(SearchView):
# 重写相关的变量或方法
template = 'search_result.html'

查看SearchView的源码或文档,了解每个方法是做什么的,便可有针对性地进行修改。
比如,上面重写了template变量,修改了搜索结果页面模板的位置。

高亮

在搜索结果页的模板中,可以使用highlight标签(需要先load一下)

{% highlight <text_block> with <query> [css_class "class_name"] [html_tag "span"] [max_length 200] %}

text_block即为全部文字,query为高亮关键字,后面可选参数,可以定义高亮关键字的html标签、css类名,以及整个高亮部分的最长长度。

高亮部分的源码位于 haystack/templatetags/lighlight.py 和 haystack/utils/lighlighting.py文件中,可复制进行修改,实现自定义高亮功能。

ref.

  1. http://django-haystack.readth...

  2. http://blog.csdn.net/ac_hell/...

查看原文

赞 5 收藏 9 评论 0

认证与成就

  • 获得 26 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-05
个人主页被 499 人浏览