AI 框架
本章主要介绍爱智 AI 框架。
概述
爱智具备强大的边缘 AI 处理能力,提供 AI 组件库和便捷的 JS API,方便开发者接入和使用 AI。同时提供多种灵活的 AI 模型安装机制,让更多的模型运行在爱智上,适配不同的用户场景,EdgerOS AI 包含以下框架:
- NCNN 是一个针对嵌入式平台优化的高性能神经网络计算框架, 详情请参考 AI Engine/NCNN。
- FaceNN 模块提供人脸检测和识别功能,详情请参考 AI Engine/FaceNN。
- HandNN 模块提供手部检测识别功能,详情请参考 AI Engine/HandNN。
- ThingNN 模块提供物体检测和识别功能,详情请参考 AI Engine/ThingNN。
- LicPlateNN 模块提供车牌检测和识别功能,详情请参考 AI Engine/LicPlateNN。
应用示例
爱智提供以下 AI 应用示例,供开发者参考:
功能介绍
人脸识别
用户信息记录与人脸识别等 AI 功能由 faceai (facelock/face.js) 模块提供。
const faceai = require("./face");
前后端使用 WebSyncTable 技术进行交互,参考以下示例,应用初始化时首先创建 WebSyncTable 对象。
const SyncTable = require("synctable"); const WebSyncTable = require("websynctable"); // ... var main = new SyncTable("main"); var server = new WebSyncTable(app, main);
参考以下示例,在前端引入 @edgeros/web-synctable 模块。
<script src="static/js/synctable.min.js"></script>
参考以下示例,在前端创建 WebSyncTable 对象。
var link = null; app.security.init(function() { if (link == null) { var proto = location.protocol === "http:" ? "ws:" : "wss:"; var server = `${proto}//${window.location.host}`; link = new SyncTable(server, "main", app.security); // ... } });
数据采集与解锁通过 ESP32 SDDC 设备实现,ESP32 SDDC 设备与 EdgerOS 之间通过 SDDC 协议传送数据以及收发指令。参考以下示例,在应用初始化时创建一个设备对象。
const Device = require("device"); const dev = new Device();
人脸解锁
ESP32 SDDC 设备从人体传感器获取一路输入信号,当用户经过人体传感器时,触发一次解锁过程,设备向应用发送 recv 消息,参考以下示例,实现应用中设备对象监听消息(main.js)。
dev.on("message", function(msg) {
if (msg && msg.cmd === "recv" && msg.size > 0) {
recv(false, msg.size)
.then((chunk) => {
lastpic = chunk;
var name = undefined;
var info = faceai.detect(lastpic);
if (info) {
for (var face of info.faces) {
var feature = faceai.feature(face, info.bitmap);
if (feature) {
name = faceai.best(feature);
if (name) {
dev.send({ cmd: "unlock", timeout: 5000 });
}
break;
}
}
} else {
console.log("No face detect!");
}
server.reverse("recv", { file: RECV_FILE, name });
})
.catch((error) => console.error(error));
}
});
人脸解锁过程:
recv()
方法从设备获取图像并更新到缓存。faceai.detect()
从缓存侦测人脸信息。faceai.feature()
提取人脸特征值。faceai.best()
从数据库中比对人脸特征值并返回用户识别结果。- 识别到合法用户时,
dev.send()
向设备发起解锁命令,其中选项timeout
表示开锁持续时间。 server.reverse()
向前端反馈结果,前端更新识别的图像。
设备发现
参考以下示例,在 devreq()
方法中首先获取设备列表,从列表中筛选我们需要的设备,然后将设备对象与有效的设备信息关联起来。
async function devreq() {
var list = await devlist();
for (var device of list) {
var info = await devinfo(device.devid);
if (info && info.report.name === "IoT Camera") {
request(device.devid);
break;
}
}
}
设备发现步骤:
- 在
devlist()
方法中调用Device.list()
获取设备列表。 - 获取设备列表后,用
devinfo()
方法遍历每个设备,devinfo()
方法调用Device.info()
获取每个设备信息。 - 筛选出所需的 AI 设备。
用户注册
注册用户后才能通过设备采集用户人脸数据,参考以下示例,由前端发起请求。
recv: async function() { try { var reply = await link.fetch('recv', { cipher: bindings.data.cipher }, true, 4000); bindings.data.response = reply.res; if (reply.res === 'ok') { bindings.data.picture = reply.file + `?t=${Date.now()}`; } // ... } // ... }
后端接收到请求,调用
recv()
过程获取用户图像,并返回图像信息。server.on("recv", (msg, client, reply) => { // ... recv(msg.cipher).then((chunk) => { lastpic = chunk; reply({ res: "ok", file: RECV_FILE, size: chunk.length }); }); // ... });
参考以下示例,配置
recv()
实现。async function recv(cipher, size) { return new Promise((resolve, rejects) => { var chunks = []; var connector = new Device.Connector(dev, cipher); dev.send({ cmd: "recv", connector, size }); // ... connector.on("data", function(data) { chunks.push(data); }); // ... connector.on("close", function() { if (chunks.length) { var chunk = Buffer.concat(chunks); if (!size || chunk.length === size) { return resolve(chunk); } } rejects("Recv error!"); }); }); }
前端获取图像地址后,预览图像,当采集到合适的人脸图像后,参考以下示例,前端发出保存请求。
save: async function() { // ... try { var reply = await link.fetch('save', { name: bindings.data.name }, true, 4000); // ... } // ... }
参考以下示例,后端接收保存请求。
server.on("save", (msg, client, reply) => { // ... var info = faceai.detect(lastpic); if (info == undefined) { reply({ res: "error", info: "No face detect!" }); } else if (info.faces.length > 1) { reply({ res: "error", info: "Too many face detect!" }); } else { var feature = faceai.feature(info.faces[0], info.bitmap); if (feature) { faceai.save(msg.name, feature); reply({ res: "ok" }); } else { reply({ res: "error", info: "Facial features are not obvious!" }); } } });
补充说明
faceai 模块介绍
在以上用户注册与人脸解锁过程中都使用了 faceai 模块,faceai 模块实现了一个 FaceDatabase 类,用来保存用户注册信息,然后提供了两组接口,一组是数据库接口,另一组是 AI 接口。
FaceDatabase 类封装了一个 LightKV
对象和一个 Map
对象。
class FaceDatabase {
constructor() {
this.lkv = new LightKV("./faces.lkv", "c+", LightKV.OBJECT);
this.map = this.lkv.toMap();
}
// ...
}
faceai 主要包括以下接口:
faceai.detect()
接口
function detect(picture) {
try {
var bitmap = imagecodec.decode(picture, {
components: imagecodec.COMPONENTS_RGB,
});
if (bitmap == undefined) {
throw new Error("Unknown picture format!");
}
var faces = facenn.detect(
bitmap.buffer,
{
width: bitmap.width,
height: bitmap.height,
pixelFormat: facenn.PIX_FMT_RGB24,
},
true
);
if (faces) {
for (var i = 0; i < faces.length; i++) {
if (faces[i].score < DETECT_CONFIDENCE_RATE) {
faces.splice(i, 1);
i--;
}
}
if (faces.length) {
console.log("Detect face num:", faces.length);
return { faces, bitmap };
}
}
} catch (error) {
console.error("Image decode error", error);
}
return undefined;
}
faceai.detect()
实现图像数据接收,侦测图像中人脸,并返回侦测结果,操作步骤如下:
imagecodec.decode()
对图片进行解码,imagecodec 模块请参考 imagecodec。facenn.detect()
侦测图像中人脸,返回侦测到的一组人脸信息,facenn 模块请参考 facenn。- 在一个循环中遍历侦测到的人脸信息,
faces[i].score < DETECT_CONFIDENCE_RATE
实现按默认阈值过滤掉辨识度低的人脸信息。 - 返回已识别的人脸信息。
faceai.feature()
接口
function feature(face, bitmap) {
var feature = facenn.feature(
bitmap.buffer,
{
width: bitmap.width,
height: bitmap.height,
pixelFormat: facenn.PIX_FMT_RGB24,
},
face,
{ live: true }
);
if (feature.live < LIVING_BODY_CONFIDENCE_RATE) {
console.log("No live:", feature.live);
return undefined;
} else {
return feature.keys;
}
}
faceai.feature()
接口用于提取人脸特征值,它接收从 faceai.detect()
返回的人脸数据及解码的图像数据,操作步骤如下:
faceai.feature()
使用facenn.feature()
提取人脸特征信息。- 提供特征值之后,
feature.live < LIVING_BODY_CONFIDENCE_RATE
按默认阈值判断活体对象。 - 返回满足活体检测的人脸特征值。
faceai.best()
接口
function best(feature) {
var iter = facedb.map[Symbol.iterator]();
var name = undefined;
var near = 0;
for (var item of iter) {
var same = facenn.compare(feature, item[1]);
if (same > near) {
near = same;
name = item[0];
}
}
if (name) {
console.info("Best mach with:", name, "prob:", near);
}
return near >= RECONGNITION_CONFIDENCE_RATE ? name : undefined;
}
faceai.best()
用于查询用户,接口接收一个人脸特征值,从数据库中检索出与特征值最匹配的用户。操作步骤如下:
facedb.map[Symbol.iterator]()
从数据库缓存中获取迭代器,检索每个用户记录。facenn.compare(feature, item[1])
将待匹配的特征值与每个用户数据比对,最后获得与特征值最相似的用户。最后还要进行阈值判断,如果匹配成功返回用户名。
扩展 CameraSource
在 CameraSource 接口
start()
中,新创建的 MediaDecoder 需监听 video 事件获取视频帧数据。start() { var netcam = new MediaDecoder(); // ... new Promise((resolve, reject) => { netcam.open(url, { proto: 'tcp', name: name }, 10000, (err) => { // ... netcam.on('video', self.onVideo.bind(self)); resolve(netcam); }); }); // ... }
参考以下示例,为了准确定位人脸在视频中的相对位置,需要将原始视频分辨率信息反馈给前端。
start() { // ... super.start.call(self); netcam.start(); var info = netcam.srcVideoFormat(); self.mediaInfo = { width: info.width, height: info.height, fps: Math.round(info.fps) }; self.sendDataHeader({ type: 'media' }, self.mediaInfo); /* {width, height, fps} */ // ... }
参考以下示例,为了识别人脸信息,引入 facenn 模块。
var facenn = require("facenn");
参考以下示例,在注册的事件中接收并处理视频数据。
onVideo(frame) { var buf = new Buffer(frame.arrayBuffer); const view = DEF_DETEC_VIEW; var faceInfo = facenn.detect(buf, { width: view.width, height: view.height, pixelFormat: FACENN_PIXEL_FORMAT }); var ret = []; /* Empty array - clear. */ for (var i = 0; i < faceInfo.length; i++) { var info = {}; info.x0 = Math.max(faceInfo[i].x0 - 10, 0); info.x1 = Math.min(faceInfo[i].x1 + 10, view.width - 1); info.y0 = Math.max(faceInfo[i].y0 - 10, 0); info.y1 = Math.min(faceInfo[i].y1, view.height - 1); info.x0 = Math.round(info.x0 * this.mediaInfo.width / view.width); info.x1 = Math.round(info.x1 * this.mediaInfo.width / view.width); info.y0 = Math.round(info.y0 * this.mediaInfo.height / view.height); info.y1 = Math.round(info.y1 * this.mediaInfo.height / view.height); ret.push(info); } var cliMgr = this.getCliMgr(); cliMgr.iter(function (cli) { cli.sendData({ type: 'face' }, ret); }); }
通过 cliMgr 遍历每个客户端,将识别的人脸信息分发给每个已连接的客户端。
cli.sendData()
设计为利用数据通道向客户端推送结构化数据的接口。
前端扩展
本节示例前端基于 智能摄像头 示例前端工程 app-demo-camera-base/web 扩展实现, 工程位于 app-demo-camera-ai/web 目录下。本节使用 @edgeros/web-mediaclient 数据通道接口,接收流媒体服务器的人脸识别信息。
为了便于处理,继承 MediaClient 实现了一个扩展的类 ClientWrap,代码位于 app-demo-camera-ai/web/src/lib/mediaclient.js。
export default function createMediaClient( ClientType, origin, canvas, opts, shakeHandle ) { class ClientWrap extends ClientType { constructor(origin, canvas, opts, shakeHandle) { super(origin, shakeHandle, opts); // ... } } // ... return new ClientWrap(origin, canvas, opts, shakeHandle); }
参考以下示例,在 Player.vue 中创建 mediaClient 对象。
import createMediaClient from "@/lib/mediaclient"; // ... const { w, h } = this.getPageSize(); this.np = new NodePlayer(); // ... var canvas = document.getElementById("layout"); var proto = location.protocol === "http:" ? "ws:" : "wss:"; var host = `${proto}//${window.location.host}`; var mediaClient = createMediaClient( MediaClient, host, canvas, { canvaw: w, canvah: h, path: this.dev.path, }, (client, path) => { this.np.start(host + path); } );
将 layout 画布置于 video 视频画布之上与之重合,实现人脸识别与视频同步。
处理 ClientWrap 内部实现,参考以下示例。通过监听 MediaClient 的 data 事件,接收流媒体服务器数据通道推送数据。
constructor(origin, canvas, opts, shakeHandle) { super(origin, shakeHandle, opts); // ... this.on('data', this.onData.bind(this)); }
参考以下示例,onData 接收并处理 data 事件。
onData(self, opts, data) { var type = opts && opts.type ? opts.type : null; if (type === 'media') { this.videow = data.width; this.videoh = data.height; this.rw = this.canvaw / this.videow; this.rh = this.canvah / this.videoh; } else if (type === 'face') { this.draw(data); } else { console.error('Data invalid.'); } }