目录
前言
在树莓派部署yolov5lite的基础上,搭建http服务器,添加网页监控功能
一、HTTP协议
http(hyper text transfer prtocol)即超文本传输协议,利用该协议能够实现浏览器加载本地文件的功能,本文在http1.0版本的基础上实现功能。
1.客户端请求
客户端请求的格式一般由:请求行、请求头、空行和请求数据组成,具体格式如下:
请求行: 请求方法|空格|URL|空格|协议版本|回车符|换行符|
请求头: 字段名|:|值|回车符|换行符|
........
字段名|:|值|回车符|换行符|
空行: 回车符|换行符|
请求数据: xxxxxxxx
2.服务器响应
服务器响应的格式一般由:状态行、消息头、空行和响应正文组成,具体格式如下:
状态行: 协议版本|空格|响应代号|空格|代号描述|回车符|换行符|
消息头: 字段名|:|值|回车符|换行符|
........
字段名|:|值|回车符|换行符|
空行: 回车符|换行符|
响应正文: xxxxxxxx
3.常见响应代号和对应描述
响应代号 | 代号描述 | |
服务器上存在请求内容并可以响应给客户端 | 200 | OK |
客户端请求异常,方法有问题 | 501 | METHOD NOT IMPLEMENTED |
请求内容不存在 | 404 | NOT FOUND |
客户端发送的请求格式有问题 | 400 | BAD REQUEST |
二、服务器搭建
1.思路
http协议工作模式为客户端发送请求,服务器响应请求,客户端为浏览器,因此只需要解析客户端发来的请求并响应即可,由于yolov5在c++环境下部署,因此服务器采用c/c++语言。
2.获取请求头部
简历socket连接,read客户端发来的消息并解析
/*
作用:获取请求行
形参:套接字id,读取内容缓存buff
返回值:-1表示出错,0表示读取一个空行, >0表示成功读取一行
*/
int HTTP::getline(int socketid, string &buff)
{
while(true)//遇到换行说明一行结束
{
char ch = '\0';
int len = read(socketid, &ch, 1);
if(len == 1)//正常读取
{
if(ch == '\r') continue;
else if (ch == '\n') break;
buff += ch;//string添加
}
else if (len == -1)//读取出错
{
perror("read error");
return -1;
}
else if (len == 0)//无数据
{
std::cerr<<"关闭客户端"<<endl;
return 0;
}
}
return buff.size();
}
/*
作用:获取请求头,遇到两次回车换行代表请求头结束
形参:套接字id, 头部内容缓存
返回值:0表示成功, -1表示错误
*/
int HTTP::gethead(int socketid, string &header)
{
int len = 0;
do
{
header.clear();
len = getline(socketid, header);
cout<<"head: "<<header<<endl;
}while(len > 0);
cout<<endl;
return len;
}
3.解析头部获取URL和请求
根据请求头格式,先读取一行请求行,利用空格划分字符串并获取URL和请求,如果URL格式不正确,比如url=index.htm?xxxxxxxxx,只取?前的内容
*
作用:解析请求行内容
形参:请求行内容buff,请求方法缓存request,url路径缓存
返回值:解析成功返回true,否则false
*/
bool HTTP::get_request_url(string buff, string &request, string &url)
{
if(buff == "")
return false;
std::vector<string> strlist;
strlist.clear();
buff += " ";
size_t pos = buff.find(' ');
while(pos != buff.npos)
{
strlist.push_back(buff.substr(0, pos));
buff = buff.substr(pos+1, buff.size());
pos = buff.find(' ');
}
request = strlist[0];
url = strlist[1];
cout<<"request: "<<request<<"\turl:"<<url<<endl;
return true;
}
/*
作用:获取真正的url并定位到本地文件
参数: url缓存
返回值:返回url文件大小
*/
long int HTTP::get_realurl(string &url, int &status)
{
url = url.substr(0, url.find('?'));
struct stat info;
if(stat((work_dir+url).c_str(), &info) == -1)//文件不存在或者出错,响应404
{
cerr<<"stat "<<(work_dir+url)<<" failed, reason:"<<strerror(errno)<<endl;
}
else
{
if(S_ISDIR(info.st_mode))//如果是目录
{
url = work_dir + "/200.html";
}
else
{
url = work_dir + url;
}
status = 200;
return info.st_size;
}
url = work_dir + "/404.html";
stat(url.c_str(), &info);
status = 404;
return info.st_size;
}
4.服务器响应
根据请求方法和URL进行响应,由于本文只用到了GET方法,故只需要解决GET和非GET
/*
作用:响应200 and 404
参数:套接字id, url路径, 响应代号status
*/
void HTTP::do_response(int socketid, std::string url, int status)
{
cout<<"==============================do_response========================"<<endl;
int size = get_realurl(url, status);
cout<<"status: "<<status<<endl;
cout<<"url: "<<url.c_str()<<endl;
cout<<"size: "<<size<<endl;
string header;
gethead(socketid, header);
if(url.find("jpg") != url.npos || (url.find("png")) != url.npos || (url.find("ico")) != url.npos)
header = img_header;
else
header = text_header;
if(status == 200 )//url文件存在
{
header = status_200 + header + to_string(size) + "\r\n\r\n";
}
else if(status == 404)//文件不存在
{
header = status_404 + header + to_string(size) + "\r\n\r\n";;
}
cout<<"header: "<<header<<endl;
//1.发送头部
int len = write(socketid, header.c_str(), header.size());
if(len < 0)
{
cerr<<"socket write error"<<endl;
return;
}
//2.发送html文件
FILE* urlfd = fopen(url.c_str(), "rb");
do
{
char buff[1024] = {0};
len = fread(buff, 1, 1024, urlfd);
cout<<buff;
len = write(socketid, buff, len);
if(len < 0)
{
cerr<<"socket write error"<<endl;
return;
}
}while(!feof(urlfd));
fclose(urlfd);
}
/*
作用:响应400 and 501
参数:套接字id, 响应代号status
*/
void HTTP::do_response(int socketid, int status)
{
cout<<"==============================do_response========================"<<endl;
struct stat info;
string header, url;
gethead(socketid, header);
if(status == 400 )//url文件存在
{
url = work_dir + "/400.html";
stat(url.c_str(), &info);
header = status_400 + text_header + to_string(info.st_size) + "\r\n\r\n";
}
else if(status == 501)//文件不存在
{
url = work_dir + "/501.html";
stat(url.c_str(), &info);
header = status_501 + text_header + to_string(info.st_size) + "\r\n\r\n";;
}
cout<<"header: "<<header<<endl;
//1.发送头部
int len = write(socketid, header.c_str(), header.size());
if(len < 0)
{
cerr<<"socket write error"<<endl;
return;
}
//2.发送html文件
FILE* urlfd = fopen(url.c_str(), "rb");
do
{
char buff[1024] = {0};
len = fread(buff, 1, 1024, urlfd);
cout<<buff;
len = write(socketid, buff, len);
if(len < 0)
{
cerr<<"socket write error"<<endl;
return;
}
}while(!feof(urlfd));
fclose(urlfd);
}
/*
作用:处理客户端请求
参数:套接字id
*/
void HTTP::do_request(int socketid)
{
string buff, request, url;
//1.读取请求行并解析方法
int len = getline(socketid, buff);
if(len > 0)//读取正常
{
if(!get_request_url(buff, request, url))//解析错误
return;
if(request == "GET")
{
int status = 0;
do_response(socketid, url, status);
}
else//非GET请求,读取http头部,并响应客户端501 Mehtod Not Implemented
{
do_response(socketid, 501);
}
}
else//读取异常
{
do_response(socketid, 400);
}
}
5.http.h
#ifndef __HTTP_H
#define __HTTP_H
#include <iostream>
#include "tcp.h"
#include "http.h"
#include "sys/stat.h"
#include <errno.h>
#include <string>
#include <vector>
class HTTP
{
private:
const std::string status_200 = "HTTP/1.0 200 OK\r\n";
const std::string status_501 = "HTTP/1.0 501 METHOD NOT IMPLEMENTED\r\n";
const std::string status_404 = "HTTP/1.0 404 NOT FOUND\r\n";
const std::string status_400 = "HTTP/1.0 400 BAD REQUEST\r\n";
const std::string text_header = "Server: Run Server\r\nContent-Type: text/html\r\nConnection: close\r\nContent-Length: ";
const std::string img_header = "Server: Run Server\r\nContent-Type: image/jpeg\r\nConnection: close\r\nContent-Length: ";
const std::string work_dir = "/home/sy/HTTPCPP/html_docs";
public:
int getline(int socketid, std::string &buff);
int gethead(int socketid, std::string &header);
bool get_request_url(std::string buff, std::string &request,std::string &url);
long int get_realurl(std::string &url, int &status);
void do_request(int socketid);
void do_response(int socketid, std::string url, int status);
void do_response(int socketid, int status);
};
#endif
6.tcp.cpp 和tcp.h
#include "tcp.h"
//domain 指定使用何种的地址类型
//PF_INET/AF_INET Ipv4 网络协议
//PF_INET6/AF_INET6 Ipv6 网络协议
int TCP::Socket(int domain,int type,int protocol)
{
int socketid = socket(domain,type,protocol);
if(socketid == -1)
{
perror("socket");
exit(1);
}
return socketid;
}
int TCP::Bind(int sockfd,struct sockaddr * my_addr,int addrlen)
{
int val = bind(sockfd,my_addr,addrlen);
if(val == -1)
{
perror("bind");
exit(0);
}
return val;
}
int TCP::Listen(int s,int backlog)
{
int val = listen(s,backlog);
if(val == -1)
{
perror("listen");
exit(0);
}
return val;
}
int TCP::Accept(int s,struct sockaddr * addr,socklen_t * addrlen)
{
int newfd = accept(s,addr,addrlen);
if(newfd < 0)
{
perror("accept");
exit(0);
}
return newfd;
}
int TCP::Connect(int s,struct sockaddr * addr,socklen_t addrlen)
{
int newfd = connect(s, addr, addrlen);
if(newfd < 0)
{
perror("connet");
exit(0);
}
return newfd;
}
#ifndef TCP_H
#define TCP_H
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdarg.h>
#include <sys/stat.h>
#include <fcntl.h>
class TCP{
public:
int Socket(int domain,int type,int protocol);
int Bind(int sockfd,struct sockaddr * my_addr,int addrlen);
int Listen(int s,int backlog);
int Accept(int s,struct sockaddr * addr,socklen_t * addrlen);
int Connect(int s,struct sockaddr * addr,socklen_t addrlen);
};
#endif
三、主程序的实现
树莓派搭建服务器采用多线程的方式,主程序负责人脸识别并保存jpg格式图片,创建线程用来实现图片和html文件传输,设置全局变量标志位,当图像文件写入完成后,线程才能发送图片,而发送图片时不能写入。
#include <iostream>
#include <string>
#include <ctime>
#include <MNN/MNNDefine.h>
#include <MNN/MNNForwardType.h>
#include <MNN/Interpreter.hpp>
#include <opencv2/opencv.hpp>
#include "Yolo.h"
#include "base64.h"
#include "tcp.h"
#include "http.h"
#include <pthread.h>
bool sendok = false;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void show_shape(std::vector<int> shape)
{
std::cout<<shape[0]<<" "<<shape[1]<<" "<<shape[2]<<" "<<shape[3]<<" "<<shape[4]<<" "<<std::endl;
}
void scale_coords(std::vector<BoxInfo> &boxes, int w_from, int h_from, int w_to, int h_to)
{
float w_ratio = float(w_to)/float(w_from);
float h_ratio = float(h_to)/float(h_from);
for(auto &box: boxes)
{
box.x1 *= w_ratio;
box.x2 *= w_ratio;
box.y1 *= h_ratio;
box.y2 *= h_ratio;
}
return ;
}
cv::Mat draw_box(cv::Mat & cv_mat, std::vector<BoxInfo> &boxes)
{
int CNUM = 80;
static const char* class_names[] = {
"face", "face_mask"
};
cv::RNG rng(0xFFFFFFFF);
cv::Scalar_<int> randColor[CNUM];
for (int i = 0; i < CNUM; i++)
rng.fill(randColor[i], cv::RNG::UNIFORM, 0, 256);
for(auto box : boxes)
{
int width = box.x2-box.x1;
int height = box.y2-box.y1;
char text[256];
cv::Point p = cv::Point(box.x1, box.y1-5);
cv::Rect rect = cv::Rect(box.x1, box.y1, width, height);
cv::rectangle(cv_mat, rect, cv::Scalar(0, 0, 255), 2);
sprintf(text, "%s %.1f%%", class_names[box.label], box.score * 100);
cv::putText(cv_mat, text, p, cv::FONT_HERSHEY_SIMPLEX, 1, cv::Scalar(0, 0, 255), 2);
}
return cv_mat;
}
void* pthread_listen(void* arg)
{
TCP tcp;
HTTP http;
int socketid = tcp.Socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in httpserver;
httpserver.sin_family = AF_INET;
httpserver.sin_port = htons(80);
httpserver.sin_addr.s_addr = htonl(INADDR_ANY);
tcp.Bind(socketid, (struct sockaddr *)&httpserver, sizeof(struct sockaddr));
tcp.Listen(socketid, 100);
while (true)
{
struct sockaddr_in client;
socklen_t clientlen = sizeof(client);
char client_ip[64] = {0};
int client_sockfd = tcp.Accept(socketid, (struct sockaddr *)&client, &clientlen);
cout << "========================================================" << endl;
cout << "client ip: " << inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip)) << "\tport : " << ntohs(client.sin_port) << endl;
while(!sendok);
http.do_request(client_sockfd);
sendok = false;
close(client_sockfd);
}
close(socketid);
pthread_exit(NULL);
}
int main(int argc, char* argv[])
{
Base64 base64;
std::string model_name = "weights/yolov5s.mnn";
int num_classes=2;
std::vector<YoloLayerData> yolov5s_layers{
{"461", 32, {
{116, 90}, {156, 198}, {373, 326}}},
{"403", 16, {
{30, 61}, {62, 45}, {59, 119}}},
{"345", 8, {
{10, 13}, {16, 30}, {33, 23}}},
};
std::vector<YoloLayerData> & layers = yolov5s_layers;
int net_size =640;
// get output data
std::string output_tensor_name0 = layers[2].name ;
std::string output_tensor_name1 = layers[1].name ;
std::string output_tensor_name2 = layers[0].name ;
std::shared_ptr<MNN::Interpreter> net = std::shared_ptr<MNN::Interpreter>(MNN::Interpreter::createFromFile(model_name.c_str()));
if (nullptr == net) {
return 0;
}
MNN::ScheduleConfig config;
config.numThread = 4;
config.type = static_cast<MNNForwardType>(MNN_FORWARD_CPU);
MNN::BackendConfig backendConfig;
backendConfig.precision = (MNN::BackendConfig::PrecisionMode)2;
// backendConfig.precision = MNN::PrecisionMode Precision_Normal; // static_cast<PrecisionMode>(Precision_Normal);
config.backendConfig = &backendConfig;
std::vector<std::string> saveNamesVector;
saveNamesVector.push_back(output_tensor_name0);
saveNamesVector.push_back(output_tensor_name1);
saveNamesVector.push_back(output_tensor_name2);
config.saveTensors = saveNamesVector;
MNN::Session *session = net->createSession(config);
cv::VideoCapture capture;
capture.open(0); //修改这个参数可以选择打开想要用的摄像头
cv::Mat frame;
int INPUT_SIZE = 320;
std::string img;
pthread_mutex_init(&lock, NULL);
pthread_t id;
pthread_create(&id, NULL, pthread_listen, NULL);
while(1)
{
capture >> frame;
cv::Mat image;
cv::flip(frame, image, 1);
cv::Mat raw_image = image;
cv::resize(raw_image, image, cv::Size(INPUT_SIZE, INPUT_SIZE));
// preprocessing
image.convertTo(image, CV_32FC3);
// image = (image * 2 / 255.0f) - 1;
image = image /255.0f;
// wrapping input tensor, convert nhwc to nchw
std::vector<int> dims{1, INPUT_SIZE, INPUT_SIZE, 3};
auto nhwc_Tensor = MNN::Tensor::create<float>(dims, NULL, MNN::Tensor::TENSORFLOW);
auto nhwc_data = nhwc_Tensor->host<float>();
auto nhwc_size = nhwc_Tensor->size();
std::memcpy(nhwc_data, image.data, nhwc_size);
auto inputTensor = net->getSessionInput(session, nullptr);
inputTensor->copyFromHostTensor(nhwc_Tensor);
// run network
clock_t startTime,endTime;
startTime = clock();//计时开始
net->runSession(session);
MNN::Tensor *tensor_scores = net->getSessionOutput(session, output_tensor_name0.c_str());
MNN::Tensor *tensor_boxes = net->getSessionOutput(session, output_tensor_name1.c_str());
MNN::Tensor *tensor_anchors = net->getSessionOutput(session, output_tensor_name2.c_str());
MNN::Tensor tensor_scores_host(tensor_scores, tensor_scores->getDimensionType());
MNN::Tensor tensor_boxes_host(tensor_boxes, tensor_boxes->getDimensionType());
MNN::Tensor tensor_anchors_host(tensor_anchors, tensor_anchors->getDimensionType());
tensor_scores->copyToHostTensor(&tensor_scores_host);
tensor_boxes->copyToHostTensor(&tensor_boxes_host);
tensor_anchors->copyToHostTensor(&tensor_anchors_host);
std::vector<BoxInfo> result;
std::vector<BoxInfo> boxes;
yolocv::YoloSize yolosize = yolocv::YoloSize{INPUT_SIZE,INPUT_SIZE};
float threshold = 0.45;
float nms_threshold = 0.5;
// show_shape(tensor_scores_host.shape());
// show_shape(tensor_boxes_host.shape());
// show_shape(tensor_anchors_host.shape());
boxes = decode_infer(tensor_scores_host, layers[2].stride, yolosize, net_size, num_classes, layers[2].anchors, threshold);
result.insert(result.begin(), boxes.begin(), boxes.end());
boxes = decode_infer(tensor_boxes_host, layers[1].stride, yolosize, net_size, num_classes, layers[1].anchors, threshold);
result.insert(result.begin(), boxes.begin(), boxes.end());
boxes = decode_infer(tensor_anchors_host, layers[0].stride, yolosize, net_size, num_classes, layers[0].anchors, threshold);
result.insert(result.begin(), boxes.begin(), boxes.end());
nms(result, nms_threshold);
std::cout<<result.size()<<std::endl;
endTime = clock();//计时结束
cout << "The forward time is: " <<(double)(endTime - startTime) / 1000.0 << "ms" << endl;
//cout << "The fps is: " <<1000 / (double)(endTime - startTime)<< "frame/s" << endl;
scale_coords(result, INPUT_SIZE, INPUT_SIZE, raw_image.cols, raw_image.rows);
cv::Mat frame_show = draw_box(raw_image, result);
std::vector<unsigned char> vecImg;
std::vector<int> quality;
quality.push_back(CV_IMWRITE_JPEG_QUALITY);
quality.push_back(30);
cv::imencode(".jpg",frame_show, vecImg, quality);
if(!sendok)
cv::imwrite("html_docs/output.jpg", frame_show);
sendok = true;
//cv::imshow("outpt", frame_show);
//if (cv::waitKey(30) >= 0)
// break;
}
return 0;
}
四、本地html文件
接收到客户端请求后,会将本地的html文件进行传输,从而实现网页,利用刷新机制,设置图片为200ms刷新一次,从而实现视频效果。
- 响应代码为200的html
<html lang="zh-CN" refresh=6>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Raspi</title>
</head>
<script>
function refreshimg(){
document.getElementById("myimg").src="output.jpg?time="+(new Date()).getTime();
}
setInterval("refreshimg()",200);
</script>
<body>
<div align="center">
<h1>Raspi Camera</h1>
<img class="myimg"id="myimg"src="output.jpg"vertical-align="middle"width="640px"height="480px">
</div>
</body>
</html>
- 响应代码为400的html
<html lang="zh-CN">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>400 BAD REQUEST</title>
</head>
<body>
<div align="center">
<h1>400 BAD REQUEST</h1>
<h1>请求格式有问题!</h1>
</div>
</body>
</html>
- 响应代码为404的html
<html lang="zh-CN">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>404 NOT FOUND</title>
</head>
<body>
<div align="center">
<h1>404 NOT FOUND</h1>
<h1>File is not existed!</h1>
</div>
</body>
</html>
- 响应代码为501的html
<html lang="zh-CN">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>501 METHOD NOT IMPLEMENTED</title>
</head>
<body>
<div align="center">
<h1>501 METHOD NOT IMPLEMENTED</h1>
<h1>请求方法异常!</h1>
</div>
</body>
</html>
总结
最终实现效果如图