图像的压缩和网络传输

需求

如果想在远程PC上看到树莓派摄像头拍摄的视频,可以将树莓派配置成一个视频流服务器。但要实现精细控制,比如得到每帧图像的时间戳,或用OpenCV处理每帧图像,这种方法就不行。

本文实现如下功能:一帧帧获取树莓派摄像头的图像,压缩后通过网络发送给远程PC,远程PC解压并显示图像。

分析

使用RaspiCam获取树莓派摄像头的图像数据。这是一个C++库,我使用它的OpenCV接口,将图像数据存储到一个cv::Mat对象中,就可以使用OpenCV的函数处理图像。

如果采集彩色图像,尺寸为640x480,帧率为25fps,每秒的数据量将是640×480×3×25字节,大约是22MB,这显然太大了,实测效果也非常卡,因此图像必须经压缩后再传输。压缩和解压缩使用OpenCV的imencodeimdecode函数。每帧原始图像的大小虽然是固定值,但压缩后的大小则不相同,所以发送压缩图像数据之前要先发送数据大小的信息,否则接收端无法正常解压。

实现

服务端

树莓派是服务端,每收到客户端的请求,就采集一帧图像并压缩,将压缩后图像数据的大小以字符的形式发送出去,接着再发送图像数据。完整代码请参考image_send.cpp,本文省略了头文件和socket接口函数调用等代码,只展示如下重要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 设置图像参数,打开摄像头
raspicam::RaspiCam_Cv camera;
camera.set(CV_CAP_PROP_FORMAT, CV_8UC3);
camera.set(CV_CAP_PROP_FRAME_WIDTH, 640);
camera.set(CV_CAP_PROP_FRAME_HEIGHT, 480);
std::cout << "Openning camera...";
if (!camera.open()) {
std::cerr << "Error openning the camera!" << std::endl;
return -1;
}
std::cout << "Successfully!" << endl;

// 配置压缩参数
std::vector<int> param = std::vector<int>(2);
param[0] = CV_IMWRITE_JPEG_QUALITY;
param[1] = 95;

cv::Mat frame; // 存储一帧图像
std::vector<uchar> buff; // 存储压缩后的数据
char data_size_str[20]; // 压缩后数据的大小用字符串保存
for (;;) {
connfd = accept(listenfd, NULL, NULL);
printf("Receive a request!\n");
if (connfd == -1) {
oops("accept");
}

for (;;) {
camera.grab();
camera.retrieve(frame);
cv::imencode(".jpg", frame, buff, param); // 压缩图像
sprintf(data_size_str, "%d", buff.size()); // 将压缩后数据的大小转换成字符
send(connfd, data_size_str, 15, 0); // 发送压缩数据的大小
send(connfd, &buff[0], buff.size(), 0); // 发送压缩数据
}
close(connfd);
}

客户端

远程PC是客户端,不断重复如下操作:接收固定长度的字符串,从中获取压缩后数据的大小,分配相应大小的空间用于接收即将到来的压缩图像数据,接收数据,解压缩并显示。完整代码请参考image_recv.cpp,重要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cv::namedWindow("Display image", cv::CV_WINDOW_AUTOSIZE);

char data_size_str[20];
for (;;) {
recv(sock, data_size_str, 15, 0); // 读取数据的大小
int framelen = atoi(data_size_str); // 文本转化成int
// 接收压缩后的图像数据
std::vector<uchar> buff(framelen);
int bytes = 0;
for (int i = 0; i < framelen ; i += bytes) {
if ((bytes = recv(sock, &buff[0]+i, framelen-i, 0)) == -1) {
oops("Receive");
}
}
// 解压图像
cv::Mat frame = cv::imdecode(cv::Mat(buff), cv::CV_LOAD_IMAGE_COLOR);
cv::imshow("Display image", frame);
cv::waitKey(20);
}
close(sock);