从零开始写高性能的人脸识别服务器(三)
我们在前两章学习了高性能的服务器高性能在哪里,下面开始着手Coding,其实Coing很简单,主要是学习和理解netty和protobuf比较难。github仓库地址
1 消息格式
消息序列化使用protobuf。在传输过程中为了极大的提高效率,直接传输的图像的像素点。服务器响应的数据格式参考的是HTTP的状态码。但是业务量比较少嘛,也没有啥提现。
1.1 定义Proto
(1)图片的消息格式。ImageProto消息,即客户端给服务端的消息
syntax = "proto3"; // proto3 必须加此注解
option java_package = "xin.marico.facerecogition.entity";
option java_outer_classname = "ImageProto";
message Image {
string personName = 1;
repeated int32 data = 2; //存储图像的像素数据,相当于int数组
int32 cols = 3; //图像的宽度
int32 rows = 4;//图像的高度
}
(2)相应消息格式,即服务端给客户端回复的消息
syntax = "proto3"; // proto3 必须加此注解
option java_package = "xin.marico.facerecogition.entity";
message ResultProto {
int32 status = 1; //消息状态码
string message = 2;//消息体
}
1.2 编译
protoc.exe对消息格式进行编译。
(1)编译成Java类
D:\protobuf-3.11.2\protoc-3.11.2-win64\bin>protoc.exe --java_out=./FaceRecoginition ImageProto.proto
(2)编译成C++类
D:\protobuf-3.11.2\protoc-3.11.2-win64\bin>protoc.exe --cpp_out=./FaceRecoginition ImageProto.proto
2 人脸识别微服务
Python人脸识别程序,不断监听Redis的队列,从Redis队列里面取出数据进行处理。数据格式为imgKey_resultKey,imgKey是图像存在redis中存储的key值,resultKey是Netty监听的Redis的key。
人脸上传的原理是,将首先在图片中检测出人脸的位置,将人脸抠出来,进行编码,将编码存储在Redis数据库。
人脸识别的原理是对要识别的人脸进行编码,然后将得到的编码信息与人脸库中的信息进行比对,返回最符合的人脸信息。下面是具体代码。
# -*- coding: utf-8 -*-
import face_recognition
import sys
import redis
from datetime import datetime
import time
from PIL import Image
import os
import uuid
import numpy as np
from numpy import float64
import json
from multiprocessing import Process
from multiprocessing import Pool
def recognize_face(megQueueStr):
print("开始人脸识别的进程,监听消息队列:",megQueueStr)
redisCli = redis.StrictRedis(host='127.0.0.1', port=6379)
while(True):
queueMsg = redisCli.lpop(megQueueStr)
time.sleep(0.001)
if queueMsg is not None:
print("识别人脸...")
queueMsg = str(queueMsg, encoding='UTF-8')
imgKey, resultKey = queueMsg.split("_")
result_status = 200
result_msg = ""
# 1.加载未知人脸图片
redisCli = redis.Redis(host='127.0.0.1', port=6379)
cols = int(redisCli.hget(imgKey, "cols"))
rows = int(redisCli.hget(imgKey, "rows"))
data = redisCli.hget(imgKey, "data")
unknown_face = np.array(json.loads(data), dtype=np.uint8)
unknown_face = unknown_face.reshape(rows, cols, 3)
redisCli.delete(imgKey)
# 2.对未知人脸图片进行编码
unknown_face_encodings = face_recognition.face_encodings(unknown_face)
if len(unknown_face_encodings) == 0:
redisCli.set(resultKey, str(500) + "_" + "未检测到人脸")
continue
unknown_face_encoding = unknown_face_encodings[0]
# 3.加载所有已知人脸
known_faces_encoding = []
face_infos = redisCli.lrange('face_infos', 0, -1)
for face_info in face_infos:
face_info = eval(str(face_info, encoding='utf-8'))
face_encoding = np.array(json.loads(face_info[1]))
known_faces_encoding.append(face_encoding);
if len(known_faces_encoding)==0:
redisCli.set(resultKey, str(500) + "_" + "人脸库为空,先录入人脸")
continue
# 4.识别人脸
face_distances = face_recognition.face_distance(known_faces_encoding, unknown_face_encoding)
min_index = np.argmin(face_distances)
if face_distances[min_index] <= 0.4:
result_msg = eval(str(face_infos[min_index], encoding='utf-8'))[0]
else:
result_msg = "未知人脸"
# 5.返回结果
redisCli.set(resultKey, str(result_status) + "_" + result_msg)
print("结果:", str(result_status) + "_" + result_msg)
def upload_faceid(megQueueStr):
print("开始上传人脸id的进程,监听消息队列:",megQueueStr)
redisCli = redis.StrictRedis(host='127.0.0.1', port=6379)
while(True):
queueMsg = redisCli.lpop(megQueueStr)
time.sleep(0.001)
if queueMsg is not None:
print("上传人脸...")
queueMsg = str(queueMsg, encoding='UTF-8')
imgKey,resultKey = queueMsg.split("_")
result_status = 200
result_msg = "人脸录入成功"
personName = str(redisCli.hget(imgKey, "personName"), encoding='UTF-8')
cols = int(redisCli.hget(imgKey, "cols"))
rows = int(redisCli.hget(imgKey, "rows"))
data = redisCli.hget(imgKey, "data")
upload_image = np.array(json.loads(data), dtype=np.uint8)
upload_image = upload_image.reshape(rows, cols, 3)
# 将人脸删除
redisCli.delete(imgKey)
# 2.对人脸图片进行编码,只要第一张人脸。
image_face_encodings = face_recognition.face_encodings(upload_image)
if len(image_face_encodings) == 0:
redisCli.set(resultKey, str(500) + "_" + "未检测到人脸")
continue
image_face_encoding = image_face_encodings[0]
# 3.将人脸图片截取保存到文件夹
top, right, bottom, left = face_recognition.face_locations(upload_image)[0]
face_img = upload_image[top:bottom, left:right]
pil_img = Image.fromarray(face_img)
# 3.1 生成随机的文件名,拼接保存路径
save_name = str(uuid.uuid1())
save_path = "/FaceRecognition/pictures_of_people_i_know/" + personName;
if not os.path.exists(save_path):
os.makedirs(save_path)
save_path += "/" + save_name + ".jpg"
# 3.2保存人脸图片
pil_img.save(save_path);
# 4.将人脸的128维编码信息存到redis中
face_info = [personName, str(image_face_encoding.tolist()), "save_path"]
redisCli.rpush("face_infos", str(face_info));
redisCli.set(resultKey,str(result_status)+"_"+result_msg)
print("结果:",str(result_status)+"_"+result_msg)
if __name__ == '__main__':
uploadProcessCount = 1
recognitionProcessCount = 2
processPool = Pool(uploadProcessCount+recognitionProcessCount)
#创建人脸识别的线程池
for i in range(recognitionProcessCount):
processPool.apply_async(recognize_face, ("face_recogintion_queue_"+str(i),))
#创建上传人脸的进程池
for i in range(uploadProcessCount):
processPool.apply_async(upload_faceid, ("upload_faceid_queue_"+str(i),))
# 关闭子进程池
processPool.close()
# 等待所有子进程结束
processPool.join()
3 Netty服务器
人脸识别整完之后,开始开发Netty服务器。其实Netty服务器的开发不难,难在Netty的学习。Netty这一端写了HTTP服务器和TCP的服务器。HTTP用来处理Web端的请求。TCP用来处理Qt客户端的请求。HTTP端口为8888,TCP端口为6666。在这里我不贴全部代码了,全部代码去github下载,如果可以的话,给弟弟个star激励一下。
(1)Main入口
package xin.marico.facerecogition.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import xin.marico.facerecogition.initializer.HttpInitializer;
import xin.marico.facerecogition.initializer.SocketInitializer;
public class FaceRecognitionServer {
private static int SOCKET_POOR = 6666;
private static int HTTP_PORT = 8888;
public static void socketProcess() {
System.out.println("启动监听socket连接----");
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new SocketInitializer());
Channel channel = bootstrap.bind(SOCKET_POOR).sync().channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void httpProcess() {
System.out.println("启动监听http连接----");
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new HttpInitializer());
Channel channel = bootstrap.bind(HTTP_PORT).sync().channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
System.out.println("---------------启动AI服务器-----------------");
Thread httpProcess = new Thread(() -> {
httpProcess();
});
Thread socketProcess = new Thread(() -> {
socketProcess();
});
httpProcess.start();
socketProcess.start();
}
}
(2)SocketInitializer
package xin.marico.facerecogition.initializer;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.timeout.IdleStateHandler;
import xin.marico.facerecogition.vo.ImageProto;
import xin.marico.facerecogition.handler.SocketHandler;
import java.util.concurrent.TimeUnit;
public class SocketInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 下面这一行是重点,Netty和protobuf一起使用的时候,要加上这个解码器
socketChannel.pipeline().addLast( new ProtobufVarint32FrameDecoder());
socketChannel.pipeline().addLast("decoder", new ProtobufDecoder(ImageProto.Image.getDefaultInstance()));
socketChannel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
socketChannel.pipeline().addLast("encoder", new ProtobufEncoder());
socketChannel.pipeline().addLast("heartJump",new IdleStateHandler(12,12,20, TimeUnit.SECONDS));
socketChannel.pipeline().addLast("imageHadler",new SocketHandler());
}
}
(2)HttpInitializer
package xin.marico.facerecogition.initializer;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import xin.marico.facerecogition.handler.HttpHandler;
public class HttpInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("httpcodec",new HttpServerCodec());
//对写大数据流的支持
ch.pipeline().addLast(new ChunkedWriteHandler());
//设置单次请求的文件大小上限
ch.pipeline().addLast(new HttpObjectAggregator(1024*1024*10));
//websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws
//ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
ch.pipeline().addLast("httpHandler",new HttpHandler());
}
}
(3)Http以及Socket的处理器代码比较多,需要的同学,去我的git上下载代码跑一下就可以。github仓库地址