1 背景介绍
目前ESP32模组中自带摄像头、lcd、WIFI模组、PSRAM等硬件资源,并依靠其大量的例程资源,使其在嵌入式开发领域的应用范围不断在扩展,不过目前,ESP32根据本身的硬件资源,只能支持一些分辨率较小的LCD显示,例如128*160分辨率。大量的应用方案是利用ESP32的摄像头模组通过WIFI传输,在电脑上进行实时显示。本文中的方案正好反过来,本ESP32通过WIFI-http-get方式,拉取其他设备上摄像头的视频流并在本地4.3寸LCD屏上显示,同时为了实现较高分辨率较高帧率的效果做了一系列的调试。
2 系统框架
电脑USB接摄像头,提供Mjpg-streamer服务。ESP32通过WIFI连接电脑的热点,以http-get的方式获取Mjpg-streamer-URL地址上的实时视频流,并通过解码实时显示在本地4.3寸屏幕上。
3 功能描述
3.1 Mjpg-streamer与HTTP-GET视频流
Mjpg-streamer可以在配置文件中修改输入的视频流尺寸、视频流类型、http-get的返回报文等参数。
int width = 320, height = 240, fps = -1, format = V4L2_PIX_FMT_MJPEG, i;
http-get的返回报文:状态码、一帧图片类型、大小、时间戳、数据等信息。
output_http/httpd.c
DBG("preparing header\n");
sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
"Access-Control-Allow-Origin: *\r\n" \
STD_HEADER \
"Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
"\r\n" \
"--" BOUNDARY "\r\n");
.
.
.
sprintf(buffer, "Content-Type: image/jpeg\r\n" \
"Content-Length: %08d\r\n" \
"X-Timestamp: %06d.%06d\r\n" \
"\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
DBG("sending intemdiate header\n");
if(write(context_fd->fd, buffer, strlen(buffer)) < 0) break;
DBG("sending frame\n");
if(write(context_fd->fd, frame, frame_size) < 0) break;
DBG("sending boundary\n");
sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
if(write(context_fd->fd, buffer, strlen(buffer)) < 0) break;
ESP32在发送http-get请求后,将对返回的报文中的内容进行解析,并根据一帧图片大小接收图片数据,传至图片解码函数。
HTTP-GET阻塞-解码刷屏示意图
3.2 jpeg解码并显示
jpeg解码中,因为受到ram大小的限制,将一帧图片分段进行解码并显示,循环解码:一个像素RGB3个字节转2字节565形式。
实际在对(640*480)分辨率的调试中,帧率受硬件限制最高只有8帧,无法达到流畅的效果。后来在解码中加入像素插值,用低画质(320*240)数据显示大图片(640*480),才能达到较高的帧率以及相对流畅的效果。
像素插值流程图
O | X | O | X | O | X | O | X | O | X |
X | X | X | X | X | X | X | X | X | X |
O | X | O | X | O | X | O | X | O | X |
X | X | X | X | X | X | X | X | X | X |
像素插值示意图行列放大一倍
jpegd2.c
while (cinfo.output_scanline < cinfo.output_height) {
(void) jpeg_read_scanlines(&cinfo, buffer, 1);
uint8_t *inbuffer = buffer[0];
uint32_t index;
static uint32_t index_last = 0;
uint32_t y = cinfo.output_scanline;
uint16_t *out = (uint16_t *)outbuffer;// + (y * cinfo.output_width);
for (index = 0; index < cinfo.output_width*WIDTH_PLUS; index++) {
uint16_t c = ((*inbuffer) >> 3) << 11 | ((*(inbuffer + 1)) >> 2) << 5 | (*(inbuffer + 2)) >> 3;
out[index_last+index] = c;
#if HIGHT_PLUS == 2 //根据宏定义屏幕宽度的放大倍数进行像素插值
out[index_last+index+CONFIG_LCD_BUF_WIDTH*2] = c;
#endif
#if WIDTH_PLUS == 2
index++;
out[index_last+index] = c;
#endif
#if HIGHT_PLUS == 2
out[index_last+index+CONFIG_LCD_BUF_WIDTH*2] = c;
#endif
inbuffer += 3;
}
index_last += index*HIGHT_PLUS;
if(!(y % CONFIG_LCD_BUF_HIGHT) || index_last >= (CONFIG_LCD_BUF_HIGHT*cinfo.output_width*WIDTH_PLUS*HIGHT_PLUS)){
lcd_cb(0, (y-CONFIG_LCD_BUF_HIGHT)*HIGHT_PLUS, CONFIG_LCD_BUF_WIDTH*WIDTH_PLUS, CONFIG_LCD_BUF_HIGHT*HIGHT_PLUS, outbuffer);
index_last = 0;
}
}
刷屏调用ESP32库函数esp_lcd_panel_draw_bitmap()分段显示。
3.3 双线程加队列优化
以320*240(8KB)一帧的JPEG为例,http-get需要40~50ms,jpeg解码显示需要40~50ms,通过一个线程负责http-get,另一个线程负责jpeg解码显示,中间通过队列传递一帧的数据,使得帧率比单线程顺序执行提高20%,同时需要占用较大空间的ram。
双线程HTTP-get和解码流程图
3.4 LVGL的显示效果
这里引入LVGL是为了测试LVGL驱动的刷屏效果,同时利用屏幕剩余空间增加一些控制按钮。LVGL目前没有接入视频流的库函数,于是将jpeg解码出的RAW数据用LVGL刷新图片的方式一帧帧刷新出来,中间需要利用ESP32较大的PSRAM来当作缓存,实际的帧率为原先的1/2。
图中视频流为640*480 jpeg(通过LVGL驱动显示)
3.5 图像大小-处理时间优化
4.3寸lcd的分辨率为800*480,最大显示实时视频流的分辨率为640*480。然而一帧640*480jpeg数据大小在30k~100k(受摄像头-图像质量以及整体亮度的影响差距较大),分段刷新中一段640*48的图像解码出来大概61k,需要根据内部ram空间对一段的大小进行适当调整。一帧jpeg图片在http-get和解码显示中的耗时与图片的大小成正比,640*480的像素是320*240的4倍,这直接导致在刷新过程中480P帧率只有240P的1/4。
ESP32-S3 240MHZ 512 KB SRAM 双核 4.3寸lcd 8080 16位并口 --JPEG | ||||
Board | 640*480 | 640*480(lvgl) | 320*240 | 320*240(插值放大至640*480) |
图片大小 | 30KB | 30KB | 8KB | 8KB |
帧率 | 8 | 4 | 22 | 22 |
表中数据为不同分辨率与刷新方式下的实时显示帧率
图中视频流为320*240 jpeg
图中视频流为320*240 jpeg(通过插值放大至640*480)
4 总结
本案例充分开发了ESP32-S3的SRAM、PSRAM、双核CPU资源,以及配套4.3寸800*480LCD的刷屏极限,通过双线程加队列方式能够提高20%的帧率,不过由于硬件限制大部分时间消耗在解码过程中,而分辨率越高消耗的时间就越大,目前实际采用320*240的视频流通过插值放大至640*480像素的方案,在4.3寸LCD能够得到平均22帧相对流畅的效果,可以满足一些对分辨率要求不高的视频相关应用开发。