我们模型的输入是60*36的一张正则化后的眼部图片,所以需要将视频流中的信息处理成输入所需的规则的眼部图片,整个处理过程是"寻找脸部->取得眼睛图像->正则化眼部图像"。
主要工作流程参考了如下这篇博客:
https://cpury.github.io/learning-where-you-are-looking-at/
1.寻找脸部
基本思路就是首先开启摄像头,将摄像头的视频流导入video标签里面,然后利用将video标签作为输入,利用clmtrackr.js来帮助寻找脸部
所需html
<!doctype html>
<html>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.12.0"></script>
<script src="clmtrackr.js"></script>
<script src="main.js"></script>
<video id="webcam" width="400" height="300" autoplay></video>
<style>
</style>
</body>
</html>
对应main.Js
$(document).ready(function() {
const video = $('#webcam')[0]; // 展示脸部的视频流
const ctrack = new clm.tracker(); //新建一个追踪对象
ctrack.init(); //初始化追踪对象
//追踪循环,一直寻找脸部
function trackingLoop(){
requestAnimationFrame(trackingLoop); //在每一帧都调用循环中的逻辑
let currentPosition=ctrack.getCurrentPosition(); //获取当前脸部关键点坐标
}
// 传入视频流,来进行处理
function onStreaming(stream){
ctrack.start(video); //追踪video中的图像
video.srcObject=stream; //设置video对象的输入来源
trackingLoop(); //调用循环
}
navigator.mediaDevices.getUserMedia({ video: true }).then(onStreaming); //请求用户开启摄像头
});
到这里,currentPosition就是用户脸部关键点坐标了,其参考图如下:
2.得到眼睛图像
这里分为左眼和右眼进行处理,由于用户的脸可能是倾斜的,所以应该找出眼睛各边界的最值,同时由于可能存在的误差,应该适当扩展边界。
在合适的范围内,眼睛的X轴方向基本由眼角确定边界,但是Y轴则可能是由两眼角或眼睛上部确定的,所以需要取三点最值来确定边界。
分左眼右眼处理,得到如下处理函数,将其增加到main.js:
//获取眼睛轮廓,传入脸部关键点坐标,和是否是左眼,返回起始点坐标和宽高
function getEyesRectangle(positions,isLeft) {
if(!isLeft){ //如果是右边
const minX = positions[23][0] - 5;
const maxX = positions[25][0] + 5;
const minY = Math.min(positions[23][1],positions[26][1],positions[25][1])-5;
const maxY = Math.max(positions[23][1],positions[24][1],positions[25][1])+5;
const width = maxX - minX;
const height = maxY - minY;
return [minX, minY, width, height];
}else{ //如果是左边
const minX = positions[30][0] - 5;
const maxX = positions[28][0] + 5;
const minY = Math.min(positions[30][1],positions[31][1],positions[28][1])-5;
const maxY = Math.max(positions[30][1],positions[29][1],positions[28][1])+5;
const width = maxX - minX;
const height = maxY - minY;
return [minX, minY, width, height];
}
}
为了展示效果,同时也为了压缩图片尺寸,在html代码的video标签后面增加两个canvas标签,稍后会将眼睛图片绘制到canvas上:
<div class="eyes">
Left Eye:<br>
<canvas id="left" width="60" height="36"></canvas>
</div>
<div class="eyes">
Right Eye:<br>
<canvas id="right" width="60" height="36"></canvas>
</div>
同时为了合理布局,将他们放置到video标签的右侧:
.eyes {
position:relative;
left:500px;
}
然后修改trackingLoop里的逻辑,如果寻找到了脸部坐标,则获取左右眼轮廓,然后则将其绘制到canvas标签上,这里计算调整因子是因为摄像头你内部可能有因素会影响最后的结果,因此需要将其抵消掉。
if (currentPosition) {
const leftRect=getEyesRectangle(currentPosition,false); //获取右眼轮廓
const rightRect=getEyesRectangle(currentPosition,true); //获取左眼轮廓
const leftCanvas=$('#left')[0]; //获取左眼画布
const leftCC=leftCanvas.getContext('2d'); //获取左眼画笔
const rightCanvas=$('#right')[0]; //获取右眼画布
const rightCC=rightCanvas.getContext('2d'); //获取右眼画笔
const resizeFactorX = video.videoWidth / video.width; //获取横向调整因子
const resizeFactorY = video.videoHeight / video.height //获取纵向调整因子
leftCC.drawImage(
video,
leftRect[0]*resizeFactorX,leftRect[1]*resizeFactorY,
leftRect[2]*resizeFactorX,leftRect[3]*resizeFactorX,
0,0,leftCanvas.width,leftCanvas.height
) //绘制左眼图像
rightCC.drawImage(
video,
rightRect[0]*resizeFactorX,rightRect[1]*resizeFactorY,
rightRect[2]*resizeFactorX,rightRect[3]*resizeFactorX,
0,0,rightCanvas.width,rightCanvas.height
) //绘制右眼图像
}
运行的效果如下:
3.正则化眼部图像
正则化的操作是根据模型输入来确定的,具体说就是灰度化和图像对比度增强。
首先需要获取原始的眼部图片像素数据,这两行,写在绘制左右眼图像的代码后面。
var leftDataCon = leftCC.getImageData(0,0,leftCanvas.width,leftCanvas.height); //获取左眼像素数据
var rightDataCon = rightCC.getImageData(0,0,rightCanvas.width,rightCanvas.height); //获取右眼像素数据
这里获取到的像素数据内部有一个data元素,是一个一维数组,包含真正的RGBA值,每四位是一个元素的RGBA,这四位中的从左到右分别是像素点的R、G、B、Alpha,如leftCC.data[0][0]就是左眼像素数据左上角那个像素的R,这样对于我们这个60*36的图片,data数组就有60*36*4个元素。
接下来就可以对这些数据进行进一步的处理了。
3.1.灰度化
Y = 0.2998 ∗ R + 0.587 ∗ G + 0.114 ∗ B Y=0.2998*R+0.587*G+0.114*B Y=0.2998∗R+0.587∗G+0.114∗B
以这个亮度值 Y Y Y表达图像的灰度值。
根据这个公式,我们可以写一个函数来处理每一个像素点,将RGB转换为亮度值。
// 将图片像素点RGB转化为灰度值,传入像素数据
function toGray(dataCon){
for(var i=0,len = dataCon.data.length ;i<len;i+=4){
var gray =parseInt( dataCon.data[i]*0.3 + dataCon.data[i+1] *0.59 + dataCon.data[i+2]*0.11);
dataCon.data[i]=gray;
dataCon.data[i+1]=gray;
dataCon.data[i+2]=gray;
dataCon.data[i+3]=255;
}
return dataCon
}
然后在获取到像素数据的代码后面紧跟着调用灰度值函数,然后再将像素数据放回canvas标签里面:
leftDataCon=toGray(leftDataCon); //左眼灰度值
rightDataCon=toGray(rightDataCon); //右眼灰度值
leftCC.putImageData(leftDataCon,0,0); //将灰度后的像素数据放回左眼canvas
rightCC.putImageData(rightDataCon,0,0);//将灰度后的像素数据放回右眼canvas
到这里,灰度化的操作就完成了,效果图如下:
3.2.对比度增强
这里参考我们另一个成员的博客:
增强对比度是为了在一定程度上去除不同光照的影响,这里采用直方图均衡化法,其步骤如下:
- 统计赢方图中每个灰度级出现的次数ni,i∈[0,L一1],其中L表示的是图像中的最大灰度级数;
- 累计归一化直方图,可以用如下公式表示:
p x ( i ) = n i n i ∈ [ 0 , L − 1 ] c ( i ) = ∑ j = 0 i P x ( j ) p_x(i)=\frac{n_i}{n} \qquad i\in [0,L-1] \\ c(i)=\sum_{j=0}^iP_x(j) px(i)=nnii∈[0,L−1]c(i)=j=0∑iPx(j)
这里n是图像中所有像素数, P x P_x Px是概率函数,对应于图像的直方图,归一化到[0,1],c是累计概率函数,对应于图像的累计归一化直方图。
- 计算新的像素值,也就是将[0,1]域内像素值映射回最初的域,可以用如下公式表示
y i = T ( x i ) = c ( i ) y i ′ = y i ∗ ( m a x − m i n ) + m i n y_i=T(x_i)=c(i)\\ y'_i=y_i*(max-min)+min yi=T(xi)=c(i)yi′=yi∗(max−min)+min
这里 y i = T ( x i ) y_i=T(x_i) yi=T(xi)表示的是原始图像中的每个像素值 x i x_i xi映射到赢方图 y j ∈ [ 0 , 1 ] y_j\in[0,1] yj∈[0,1], y i ′ y_i' yi′表示最初的域, y i ′ ∈ [ m i n , m a x ] y'_i\in[min,max] yi′∈[min,max],min是像素的最小值,max是像素的最大值。
3.2.1.统计灰度级出现次数
首先可以知道,一张图的灰度级别不会超过255,于是我们可以利用一个长度为256的数组,全部初始化为0之后,用于统计灰度级出现次数,最后根据最大值切片数组即可:
// 统计直方图中每个灰度级出现的次数
function getNi(dataCon){
let ni=[]; //新建统计数组
maxNum=0;
for(var i=0;i<256;i++) //将出现次数初始化为0
ni.push(0);
for(var i=0,len = dataCon.data.length ;i<len;i+=4){
if(maxNum<dataCon.data[i]) //找最大值
maxNum=dataCon.data[i];
ni[dataCon.data[i]]++; //增加一次
}
return ni.slice(0,maxNum+1);; //返回切片数组
}
3.2.2.累计归一化直方图
按照上述公式进行实现即可:
//计算累计概率函数
function getCi(dataCon){
let ni=getNi(dataCon);
let n=dataCon.data.length/4;
let ci=[];
ci.push(0)
var curSum;
for(var i=1;i<ni.length;i++){
curSum=0;
for(var j=0;j<i;j++){
curSum+=ni[j]/n;
}
ci.push(curSum)
}
return ci;
}
3.3.3.计算新的像素值
按照公式实现代码,这里max取了255,min取了0:
// 增加像素点对比度
function toContrast(dataCon){
ci=getCi(dataCon);
for(var i=0,len = dataCon.data.length ;i<len;i+=4){
var gray=parseInt(ci[dataCon.data[i]]*255)
dataCon.data[i]=gray;
dataCon.data[i+1]=gray;
dataCon.data[i+2]=gray;
dataCon.data[i+3]=255;
}
return dataCon
}
之后在trackingLoop中的灰度化操作之后调用这个对比度增强的函数:
rightDataCon=toGray(rightDataCon); //右眼灰度值
leftDataCon=toContrast(leftDataCon); //左眼对比度
rightDataCon=toContrast(rightDataCon); //右眼对比度
leftCC.putImageData(leftDataCon,0,0); //将处理后的像素数据放回左眼canvas
rightCC.putImageData(rightDataCon,0,0);//将处理后的像素数据放回右眼canvas
到这里,对比度增强的操作就完成了,最终得到的效果如下: