【翻译】基于HTML5,javascript,webrtc,websockets,Jetty和OpenCV的人脸识别

原文地址:
[url]http://www.smartjava.org/content/face-detection-using-html5-javascript-webrtc-websockets-jetty-and-javacvopencv[/url]
[align=center][b]人脸识别[/b]
[b]——基于HTML5,javascript,webrtc,websockets,Jetty和OpenCV[/b]
By jos.dirksen on Thu, 04/19/2012 - 15:57
[/align]
对于HTML5和相关标准,每个新版本的现代浏览器提供了越来越多的标准特性。很多人已经听说过websockets,它可以让你很容易地建立到服务端的双向通讯通道,其背后是没有广泛宣传的webrtc规范。
使用webrtc规范可以通过HTML/Javascript很容易地实现实时视频、音频相关的应用。在应用中你可以使用用户的麦克风和摄像头,并将这些内容分享给互联网上其他用户。例如,你可以建立一个不需要插件的视频会议软件,或者一个使用手机查看的儿童监控软件,或者仅仅使网络直播更容易。完全可以使用跨浏览器特性替代插件。
更新:在最新版的webrtc规范中,我们可以访问麦克风资源!参考如下链接:
[url]http://www.smartjava.org/content/record-audio-using-webrtc-chrome-and-speech-recognition-websockets[/url]
HTML5相关的众多规范中,webrtc还没有完成,在浏览器中的支持也很少。然而,你可以使用开发版Opera和最新版Chrome支持的特性实现一些很酷的功能。本文中,我将带领大家使用webrtc和一些HTML5其他标准实现如下功能:
[img]http://dl.iteye.com/upload/picture/pic/119460/b2e55dd4-4cae-33c3-9bd8-fe9546a415ab.png[/img]
为此我们需要做如下操作:
1.通过getUserMedia特性访问用户摄像头
2.使用websockets向服务端发送数据
3.在服务端,分析收到的数据,使用JavaCV/OpenCV检测和标记识别出来的人脸
4.使用websockets将数据从服务端发送到客户端
5.客户端显示从服务端收到的信息
换句话说,我们要创建一个实时的人脸识别系统,前端完全通过“标准的”HTML5/Javascript实现。在文章中你会看到,我们将使用一些小技巧,因为有些HTML5的特性还没有实现。
【[url]up2pu.iteye.com[/url]翻译】
[b]我们使用哪些工具和技术[/b]
让我们看看我们用来实现人脸识别系统的工具和技术。我们从前端技术开始:
1.Webrtc:规范是这么说的:这些API应该使得应用可以在浏览器中运行,不需要额外的下载和插件,允许各方使用音频、视频进行实时通信,不需要使用中介服务器(除非用于防火墙穿透或者提供中间服务)。
参考:[url]http://www.w3.org/2011/04/webrtc-charter.html[/url]
2.Websockets:使得Web应用可以通过服务端处理实现双向通信,规范中介绍了WebSocket接口。
参考:[url]http://dev.w3.org/html5/websockets/[/url]
3.Canvas:元素提供带有解决方案无关位图画布的脚本,用来实现图像渲染,游戏图像或者其他视觉图像。
参考:[url]http://www.w3.org/wiki/HTML/Elements/canvas[/url]

我们在后台使用什么:
1.Jetty:提供websockets实现。
参考:[url]http://www.eclipse.org/jetty/[/url]
2.OpenCV:一个提供各种图像处理算法的库。我们使用他们提供的算法实现人脸识别。
参考:[url]http://opencv.willowgarage.com/wiki/[/url]
参考:[url]http://opencv.willowgarage.com/wiki/FaceDetection[/url]
3.JavaCV:我们在Jetty中使用OpenCV对收到的数据进行图像检测。使用JavaCV我们可以通过Java使用OpenCV的特性。
参考:[url]http://code.google.com/p/javacv/[/url]
【[url]up2pu.iteye.com[/url]翻译】
[b]前端第一步:开启Chrome的mediastream功能并访问摄像头[/b]
我们先访问摄像头。在我的例子中,我使用的是Chrome的最新版本(canay),该版本已经支持我们需要的webrtc规范。在使用前,你需要先开启它。打开“chrome://flags/”URL,然后开启mediastream功能。
[img]http://dl.iteye.com/upload/picture/pic/119462/39fce19d-381a-3208-9bd7-f93e9d671cf2.png[/img]
一旦你开启它,就可以在不使用插件的情况下,通过浏览器使用webrtc的一些功能来访问摄像头。访问摄像头需要做的就是使用下面的html和javascript:
<div>
<video id="live" width="320" height="240" autoplay></video>
</div>


和下面的javascript:
video = document.getElementById("live")
var ctx;
// use the chrome specific GetUserMedia function
navigator.webkitGetUserMedia("video",
function(stream) {
video.src = webkitURL.createObjectURL(stream);
},
function(err) {
console.log("Unable to get video stream!")
}
)

使用这么小一段HTML和javascript代码,我们就可以访问用户的摄像头并在HTML5视频标签中显示出来。首先,我们使用getUserMedia(使用了chrome特有的webkit前缀)函数获得对摄像头的访问权限。在回调函数中,我们访问流对象。这个流对象就是用户摄像头产生的流。为了显示这个流,我们需要把它添加到视频标签上。视频标签的src属性允许我们设置一个URL用于播放。通过HTML5的另一个特性,我们把流转换成URL。该功能通过URL.CreateObjectURL(也是带有前缀的)函数实现。该函数返回值是个URL,我们把它添加到视频标签上。这就是访问用户摄像头视频流的全部过程。
[img]http://dl.iteye.com/upload/picture/pic/119464/b36c42c3-5c49-37e0-8421-8165c766e36d.png[/img]
下面我们要做的是通过websockets向jetty服务端发送视频流。
【[url]up2pu.iteye.com[/url]翻译】
[b]前端第二步:通过websockets向Jetty服务端发送视频流[/b]
这一步,我们想要从视频流获得数据,然后以二进制的方式通过websockets发送到Jetty服务端。理论上听起来很简单。我们已经得到了二进制的视频流,所以我们可以直接访问字节数据,然后通过websockets发送到远程服务端。然而,事实上行不通。通过getUserMedia得到的流对象,我们无法以流的方式访问它的数据。乐观点说,现在还不行。如果看一下规范,你可以发现,你应该能调用record()来访问recorder。这个recorder可以用来访问元数据。但是不幸的是,目前没有浏览器支持该功能。所以我们需要找个替代方案。现在看只有一个选择:
1.对当前视频截图
2.然后绘制到canvas标签上
3.从canvas获得数据,生成图片
4.通过websockets发送图片数据
这些小技巧导致一些客户端额外的处理和大量的数据发送到服务端,但是它可以用。实现这个也不那么难:
    var video = $("#live").get()[0];
var canvas = $("#canvas");
var ctx = canvas.get()[0].getContext('2d');

navigator.webkitGetUserMedia("video",
function(stream) {
video.src = webkitURL.createObjectURL(stream);
},
function(err) {
console.log("Unable to get video stream!")
}
)

timer = setInterval(
function () {
ctx.drawImage(video, 0, 0, 320, 240);
}, 250);


也不比前段的代码复杂多少。我们加了一个定时器和一个canvas。定时器250ms运行一次,把当前视频图像画到canvas上(可以通过下面的截屏看到)。
[img]http://dl.iteye.com/upload/picture/pic/119466/fdcefac3-b610-3427-8c8e-7d8a4f91e238.png[/img]
你可以看到canvas有点延迟。你可以把间隔时间调小,但是这样会消耗更多的资源。
下一步从canvas获取图像,然后转换成二进制,然后通过websocket发送出去。在我们看websocket部分前,先看看数据部分。为了获得数据,我们扩展了定时器函数的代码:
timer = setInterval(
function () {
ctx.drawImage(video, 0, 0, 320, 240);
var data = canvas.get()[0].toDataURL('image/jpeg', 1.0);
newblob = dataURItoBlob(data);
}, 250);
}

toDataURL函数从当前canvas复制数据,然后储存在dataurl中。dataurl是一个包含base64编码的二进制数据。在我们的例子中它看起来像下面这样:
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEB
AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB
AQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA
QEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCADwAUADASIAAhEBAxEB/8QAHwAAAQU
.. snip ..
QxL7FDBd+0sxJYZ3Ma5WOSYxwMEBViFlRvmIUFmwUO7O75Q4OSByS5L57xcoJuVaSTTpyfJ
RSjKfxayklZKpzXc1zVXVpxlGRKo1K8pPlje6bs22oxSau4R9289JNJuLirpqL4p44FcQMkYMjrs+z
vhpNuzDBjlmJVADuwMLzsIy4OTMBvAxuDM+AQW2vsVzIoyQQwG1j8hxt6VELxd7L5caoT5q4kj
uc4rku4QjOPI4tNXxkua01y8uijJtSTS80le0Z6WjJuz5pXaa//2Q==

我们可以以文本的方式发送它,然后在服务端解析,但是websockets允许我们直接发送二进制数据,我们把它转换成二进制数据。我们需要两步,canvas不允许(或者是我不知道)我们直接访问二进制数据。幸运的是,有人在stackoverflow创建了一个很好用的帮助方法(dataURItoBlob),这就是我们需要的(参考:[url]http://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata/5100158[/url])。这时我们已经可以按照指定时间间隔获得包含当前视频截屏的数据数组。下一步,也是最后一步,在客户端通过websockets发送数据即可。
在Javascript中使用websockets非常简单。你只需要指定websockets的url并实现一些回调函数。首先需要打开连接:
    var ws = new WebSocket("ws://127.0.0.1:9999");
ws.onopen = function () {
console.log("Openened connection to websocket");
}

如果一切顺利,我们现在有了一个双向连接。通过这个连接发送数据只需要调用ws.send:
    timer = setInterval(
function () {
ctx.drawImage(video, 0, 0, 320, 240);
var data = canvas.get()[0].toDataURL('image/jpeg', 1.0);
newblob = dataURItoBlob(data);
ws.send(newblob);
}, 250);
}

这就是客户端的代码。如果我们打开这个页面,我们首先获取用户摄像头的访问权限,然后在视屏标签中显示从摄像头获得的数据流,按照指定间隔时间捕获视频数据,通过websockets发送到后台服务进行后续处理。
【[url]up2pu.iteye.com[/url]翻译】
[b]建立后台服务环境[/b]
本例的后台服务使用Jetty的websocket支持(后续文章中,我会看看能否在Play 2.0 websockets支持的情况下运行)。使用Jetty可以很方便的启动一个带有websocket监听的服务端。我经常运行嵌入式Jetty,使用如下简单的Jetty启动器使websockets运行起来。
public class WebsocketServer extends Server {

private final static Logger LOG = Logger.getLogger(WebsocketServer.class);

public WebsocketServer(int port) {
SelectChannelConnector connector = new SelectChannelConnector();
connector.setPort(port);
addConnector(connector);

WebSocketHandler wsHandler = new WebSocketHandler() {
public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
return new FaceDetectWebSocket();
}
};
setHandler(wsHandler);
}

/**
* Simple innerclass that is used to handle websocket connections.
*
* @author jos
*/
private static class FaceDetectWebSocket implements WebSocket,
WebSocket.OnBinaryMessage, WebSocket.OnTextMessage {

private Connection connection;
private FaceDetection faceDetection = new FaceDetection();

public FaceDetectWebSocket() {
super();
}

/**
* On open we set the connection locally, and enable
* binary support
*/
public void onOpen(Connection connection) {
this.connection = connection;
this.connection.setMaxBinaryMessageSize(1024 * 512);
}

/**
* Cleanup if needed. Not used for this example
*/
public void onClose(int code, String message) {}

/**
* When we receive a binary message we assume it is an image. We then run this
* image through our face detection algorithm and send back the response.
*/
public void onMessage(byte[] data, int offset, int length) {

ByteArrayOutputStream bOut = new ByteArrayOutputStream();
bOut.write(data, offset, length);
try {
byte[] result = faceDetection.convert(bOut.toByteArray());
this.connection.sendMessage(result, 0, result.length);
} catch (IOException e) {
LOG.error("Error in facedetection, ignoring message:" + e.getMessage());
}
}


}

/**
* Start the server on port 999
*/
public static void main(String[] args) throws Exception {
WebsocketServer server = new WebsocketServer(9999);
server.start();
server.join();
}
}

很长的一段代码,但是理解起来并不困难。引入的部分用来创建一个处理器支持websocket协议。这里我们创建一个WebSocketHandler,它总是返回同一个WebSocket。在真实的场景中,你需要根据属性和URL决定WebSocket的类型,本例中我们只需要一直返回同一个。
websocket本身没有那么复杂,但是为了正确运行我们需要配置一些东西。在onOpen方法中我们做了如下事情:
		public void onOpen(Connection connection) {
this.connection = connection;
this.connection.setMaxBinaryMessageSize(1024 * 512);
}

该操作开启二进制消息支持。现在我们的WebSocket可以接收不大于512KB的二进制消息,因为我们没有直接把数据以流的方式发送,而是发送了一个canvas渲染的图片,因此消息非常大。512KB对于640*480已经够了。我们的人脸识别程序在320*240下工作很好,这就够了。对收到的二进制图片数据的处理在onMessage方法中实现:
		public void onMessage(byte[] data, int offset, int length) {

ByteArrayOutputStream bOut = new ByteArrayOutputStream();
bOut.write(data, offset, length);
try {
byte[] result = faceDetection.convert(bOut.toByteArray());
this.connection.sendMessage(result, 0, result.length);
} catch (IOException e) {
LOG.error("Error in facedetection, ignoring message:" + e.getMessage());
}
}

代码没有优化,但是目的很明确。我们得到客户端发送的数据,然后把它写到固定大小的byte数组,传递给faceDetection类。faceDetection类做一些神奇的事情然后返回处理的图片。这个处理后的图片和原始的一样,只是增加了黄色矩形表示被识别的人脸。
[img]http://dl.iteye.com/upload/picture/pic/119468/349122a4-e61b-3234-b7fe-55e71d75fdce.png[/img]
处理后的图像通过同一个websocket连接发送到客户端进行后续处理。在看数据怎么用javascript显示前,我们先看看FaceDetection类。FaceDetection类使用JavaCV(OpenCV的Java封装)中的CvHaarClassifierCascade来检测人脸。我不会深入介绍人脸识别是如何实现的,因为它本身就是一个比较大的话题。
public class FaceDetection {

private static final String CASCADE_FILE = "resources/haarcascade_frontalface_alt.xml";

private int minsize = 20;
private int group = 0;
private double scale = 1.1;

/**
* Based on FaceDetection example from JavaCV.
*/
public byte[] convert(byte[] imageData) throws IOException {
// create image from supplied bytearray
IplImage originalImage = cvDecodeImage(cvMat(1, imageData.length,CV_8UC1, new BytePointer(imageData)));

// Convert to grayscale for recognition
IplImage grayImage = IplImage.create(originalImage.width(), originalImage.height(), IPL_DEPTH_8U, 1);
cvCvtColor(originalImage, grayImage, CV_BGR2GRAY);

// storage is needed to store information during detection
CvMemStorage storage = CvMemStorage.create();

// Configuration to use in analysis
CvHaarClassifierCascade cascade = new CvHaarClassifierCascade(cvLoad(CASCADE_FILE));

// We detect the faces.
CvSeq faces = cvHaarDetectObjects(grayImage, cascade, storage, scale, group, minsize);

// We iterate over the discovered faces and draw yellow rectangles around them.
for (int i = 0; ipublic class FaceDetection {

private static final String CASCADE_FILE = "resources/haarcascade_frontalface_alt.xml";

private int minsize = 20;
private int group = 0;
private double scale = 1.1;

/**
* Based on FaceDetection example from JavaCV.
*/
public byte[] convert(byte[] imageData) throws IOException {
// create image from supplied bytearray
IplImage originalImage = cvDecodeImage(cvMat(1, imageData.length,CV_8UC1, new BytePointer(imageData)));

// Convert to grayscale for recognition
IplImage grayImage = IplImage.create(originalImage.width(), originalImage.height(), IPL_DEPTH_8U, 1);
cvCvtColor(originalImage, grayImage, CV_BGR2GRAY);

// storage is needed to store information during detection
CvMemStorage storage = CvMemStorage.create();

// Configuration to use in analysis
CvHaarClassifierCascade cascade = new CvHaarClassifierCascade(cvLoad(CASCADE_FILE));

// We detect the faces.
CvSeq faces = cvHaarDetectObjects(grayImage, cascade, storage, scale, group, minsize);

// We iterate over the discovered faces and draw yellow rectangles around them.
for (int i = 0; i < faces.total(); i++) {
CvRect r = new CvRect(cvGetSeqElem(faces, i));
cvRectangle(originalImage, cvPoint(r.x(), r.y()),
cvPoint(r.x() + r.width(), r.y() + r.height()),
CvScalar.YELLOW, 1, CV_AA, 0);
}

// convert the resulting image back to an array
ByteArrayOutputStream bout = new ByteArrayOutputStream();
BufferedImage imgb = originalImage.getBufferedImage();
ImageIO.write(imgb, "png", bout);
return bout.toByteArray();
}
}

代码至少解释了步骤。想进一步了解它如何生效可以查看OpenCV和JavaCV网站。通过改变关联文件,修改minsize,group,scale等属性,你可以使用它识别眼睛,鼻子,耳朵,瞳孔等。例如人眼识别看起来像下面这样:
[img]http://dl.iteye.com/upload/picture/pic/119470/b85b10b9-c882-3205-8781-3a843b85ba56.png[/img]
【[url]up2pu.iteye.com[/url]翻译】
[b]前端,显示识别出来的人脸[/b]
最后一步是接收Jetty的web应用发送的消息,然后在img标签中渲染。我们通过设置websocket的onmessage函数实现。下面的代码中,我们接收到二进制消息。然后将消息转换成objectURL(可以把它看做一个本地的、临时的URL),然后将它设置为图像的源。一旦图像被加载,我们回收objectURL,因为它已经没用了。
ws.onmessage = function (msg) {
var target = document.getElementById("target");
url=window.webkitURL.createObjectURL(msg.data);

target.onload = function() {
window.webkitURL.revokeObjectURL(url);
};
target.src = url;
}

现在,我们需要更新一下html为如下形式:
<div style="visibility: hidden;  width:0; height:0;">
<canvas width="320" id="canvas" height="240"></canvas>
</div>

<div>
<video id="live" width="320" height="240" autoplay style="display: inline;"></video>
<img id="target" style="display: inline;"/>
</div>


现在我们已经实现了人脸识别:
[img]http://dl.iteye.com/upload/picture/pic/119472/5676dc63-3030-3d02-860a-540db9393052.png[/img]
正如你所看到,使用新的HTML5 API可以实现很多功能。遗憾的是不是所有功能都实现了,并且某些情况下浏览器支持的也不好。但是,它确实给我们提供了一些精彩又强大的特性。我已经在最新版本的Chrome和Safari中测试(在Safari中使用需要去掉webkit前缀)。它应该也能运行在开启了“userMedia”功能的safari手机浏览器中。确定你是在使用高带宽的WIFI,因为代码没有针对带宽做任何优化。几周后我还会再看看这篇文章,如果有时间,我会实现一个基于Play2/Scala的后台服务版本。

[b]参考资料:[/b]
[url]http://www.html5china.com/course/20120528_3742.html[/url]
[url]http://neave.com/webcam/html5/face/[/url](一个demo)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值