小言_互联网的博客

HaaS AI 应用实践“老板来了”系列之二 :WiFi摄像头人像采集

438人阅读  评论(0)

一、前言

在物联网的诸多场景中,除了传感器是AIoT设备中的重要组成外,摄像头作为视觉输入的关键设备,在一些AI应用场景或者监控场景也是必不可少的,本文将带大家一起来给HaaS100开发板装上千里眼,并输出到屏幕显示,充分挖掘HaaS100的硬件能力,帮助你实现更多视觉业务场景应用。监控老板,监控xxx等等,充分发挥你的想象力吧!

二、方案简介

2.1 方案组成

整个方案由HaaS100、WiFi摄像头、LCD组成。LCD与HaaS100通过SPI连接,HaaS100通过Http请求获取到JPEG数据最终显示到LCD上。

2.2 WiFi摄像头选型

市面上的WiFi摄像头比较多,在本例中WiFi摄像头采用ESP官方的ESP32-EYE进行适配,ESP32-CAM是ESP32第三方厂商开发的一款低成本方案,应用也比较广泛,开发者也可以选择它作为方案之一,万能的淘宝上有很多卖家,商家也会提供相应的资料,购买链接如下:

ESP32-EYE: https://detail.tmall.com/item.htm?spm=a230r.1.14.1.150d6a6ftZ6h4K&id=611790371635&ns=1&abbucket=3

ESP32-CAM: https://detail.tmall.com/item.htm?spm=a230r.1.14.1.3f543b21XaGDay&id=581256720864&ns=1&abbucket=3

https://item.taobao.com/item.htm?spm=a230r.1.14.33.150d6a6ftZ6h4K&id=586201030146&ns=1&abbucket=3#detail

2.3 数据处理流程

HaaS100通过http请求到JPEG数据后,通过jpeg解码为RGB888数据,因为SPI LCD是RGB565格式,在送显前就需要将RGB888的数据通过格式转换模块转为RGB565格式。

三、方案实践

3.1 LCD适配

LCD的驱动适配请参考《HaaS100复古机来了》。

购买链接https://item.taobao.com/item.htm?spm=a1z09.2.0.0.768d2e8d9D3S7s&id=38842179442&_u=m1tg6s6048c2

                         

3.2 ESP32-EYE开发配置

3.2.1 环境搭建

代码下载

$git clone --recursive https://github.com/espressif/esp-who.git

Python环境创建

这一个步骤不是必须的,不过如果你有多个python环境的需求,也安装过conda可以使用该步骤为esp32的开发创建一个独立的python开发环境,避免不同开发环境的相互影响。

$conda create -n esp32 python=3.8

ESP-IDF安装

不同的操作系统安装的步骤也有所差异,请参考官网文档进行安装:

https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/get-started/index.html#get-started-set-up-env

环境变量设置

这里以Macbook为例进行环境变量设置:


  
  1. $cd ~/esp/esp-idf
  2. $./install.sh
  3. $chmod . $HOME/esp-who/esp-idf/export.sh
  4. $source . $HOME/esp-who/esp-idf/export.sh

注意:

每次重启终端后都需要执行该步骤,否则找不到idf.py命令,或者可以加入到根目录.bashrc中不用每次再输入该命令。

代码编译

ESP32-EYE的代码中提供了多个Demo,使用camera_web_server来建立一个web服务器,该Demo中摄像头采集的数据以mjpeg格式提供,并且提供了以http请求的方式获取mjpeg/jpeg图像数据。编译需要进入到Demo的目录中:


  
  1. $cd examples/single_chip/camera_web_server/
  2. $idf.py build

代码烧录

$idf.py -p [port] flash

例如:

idf.py -p /dev/cu.SLAB_USBtoUART flash

Log监视器

查看串口log,进入到camera_web_server所在目录执行。

$idf.py -p [port] monitor

例如:

idf.py -p /dev/cu.SLAB_USBtoUART monitor

所以camera wifi的IP就是192.168.3.135。

3.2.2 ESP32 EYE网络设置

SoftAP模式

默认启动后ESP32 EYE已经开启了SSID为ESP32-Camera的AP,可以使用电脑连接该AP。

也可以通过修改sdkconfig来改变ssid/password、station连接数量、AP信道、服务器IP等,然后重新进行编译:

Station模式

ESP32也支持station与SoftAP模式共存,比如想让ESP32 EYE接入到SSID为haas_test的局域网中,修改sdkconfig中的ssid/password即可。

3.2.3 电脑访问获取图像

为了确认ESP32-EYE摄像头是否正常,先通过电脑方式查看web界面

http://192.168.4.1/:

直接抓取流http://192.168.4.1:81/stream:

抓取当前画面http://192.168.4.1/capture:

3.3 HaaS100开发

3.3.1 代码下载

参考《HaaS100快速开始》下载AliOS Things代码。

3.3.2 代码流程分析

代码下载后,wificamera_demo的代码路径:

$application/example/wificamera_demo

获取图像

LCD是320*240的屏,为了降低数据传输带宽的占用,将ESP32-EYE的摄像头数据采集分辨率相应的修改为QVGA(320*240),同时当HaaS100接收到图像数据后不需要进行图像的裁剪处理,减少了CPU资源开销:

在app_entry.c中初始化时注册了一个WiFi事件处理函数:


  
  1. int application_start(int argc, char **argv)
  2. {
  3. ......
  4. set_iotx_info();
  5. netmgr_init();
  6. ......
  7. aos_register_event_filter(EV_WIFI, wifi_service_event, NULL);
  8. ......
  9. return 0;
  10. }

当网络连接成功后,会进入到以下函数完成初始化及WiFi摄像头连接并获取图像数据的线程,另外也会进行物联网平台的连接。


  
  1. static void wifi_service_event(input_event_t *event, void *priv_data)
  2. {
  3. if (event->type != EV_WIFI) {
  4. return;
  5. }
  6. if (event->code != CODE_WIFI_ON_SNTP_OK)
  7. return;
  8. if (!linkkit_started) {
  9. LOG("start to do ucloud_ai_demo\n");
  10. if (ucloud_ai_init() < 0)
  11. return;
  12. aos_task_new("wificamera_process",wificamera_main, NULL, 1024*10);
  13. aos_task_new("linkkit", (void (*)(void *))linkkit_main, NULL, 1024 * 10);
  14. linkkit_started = 1;
  15. }
  16. }

网络连接成功后进入到http_stream_process中调用http_get获取JPEG数据:

这里url参数就是WiFi摄像头的Web Server地址,这里通过capture方式获取图片进行显示,例如只需要传入http_stream_process("http://192.168.4.1:80/capture")即可获取JPEG图像。


  
  1. int ucloud_ai_main(void *p)
  2. {
  3. int ret = 0;
  4. int recv_len = 0;
  5. char * customer_header = "Accept: */*\r\n";
  6. FILE *jpegFile = NULL;
  7. char *url = WIFICAMERA_URL;
  8. char *upload_url = NULL;
  9. LOG("start ucloud_ai_main\n");
  10. /*for wificamera client*/
  11. ret = httpclient_prepare(&wificamera_client_data, HEAD_SIZE, BODY_SZIE);
  12. if (ret != HTTP_SUCCESS)
  13. return -1;
  14. wificamera_client.is_http = true;
  15. httpclient_set_custom_header(&wificamera_client, customer_header);
  16. ret = httpclient_conn(&wificamera_client, (const char *)url);
  17. if(HTTP_SUCCESS != ret) {
  18. LOGE(TAG, "http connect failed");
  19. return -1;
  20. }
  21. /*for ai client*/
  22. ret = httpclient_prepare(&ai_client_data, HEAD_SIZE, BODY_SZIE);
  23. if (ret != HTTP_SUCCESS)
  24. return -1;
  25. ai_client.is_http = true;
  26. httpclient_set_custom_header(&ai_client, customer_header);
  27. ......
  28. while (1) {
  29. ret = http_get_image(&wificamera_client, &wificamera_client_data, url);
  30. if (ret <= 0) {
  31. LOGE(TAG, "http_get_image fail\n");
  32. continue;
  33. }
  34. .......
  35. }

获取一张图片的函数具体实现:


  
  1. static int32_t http_get_image(httpclient_t *client, httpclient_data_t *client_data, char *url)
  2. {
  3. int ret;
  4. int recv_len = 0;
  5. httpclient_reset(client_data);
  6. ret = httpclient_send(client, (const char *)url, HTTP_GET, client_data);
  7. if(HTTP_SUCCESS != ret) {
  8. LOGE(TAG, "http send request failed");
  9. return -1;
  10. }
  11. do {
  12. ret = httpclient_recv(client, client_data);
  13. if (ret < 0)
  14. break;
  15. recv_len = client_data->response_content_len;
  16. } while (ret == HTTP_EAGAIN);
  17. return recv_len;
  18. }

存储图像数据

在获取到一张图像后,把该图片保存到/data目录,实现函数如下:


  
  1. static int32_t save_captured_image(char *buf, int len, char *path)
  2. {
  3. FILE *jpegFile;
  4. if ((jpegFile = fopen(path, "wb")) == NULL) {
  5. LOGE(TAG, "opening output file fail\n");
  6. return -1;
  7. }
  8. if (fwrite(buf, len, 1, jpegFile) < 1) {
  9. LOGE(TAG, "write buf fail\n");
  10. return -1;
  11. }
  12. fclose(jpegFile);
  13. return 0;
  14. }

在调试过程中,存储照片到sdcard可以帮助我们确认抓到的图片是否正常,存储到sdcard的路径是/sdcard/capture.jpg。

解码JPEG图像

在本Demo中实现了两种方式解码图片。使用A方式可以直接从文件系统中加载文件,使用B方式直接解码内存中的JPEG Buffer。

A. 直接使用SDL解码图片并显示,参数传入图片路径:


  
  1. int graphics_draw_image(const char *file, int x, int y)
  2. {
  3. SDL_Rect drect = { x, y, 0, 0 };
  4. if(strcmp(file, "") == 0) return;
  5. for(int i = 0; i < image_count; i++) {
  6. if(strcmp(file, image[i].file) != 0) continue;
  7. graphics_draw_texture(image[i].texture, x, y);
  8. return 0;
  9. }
  10. graphics_generate_image(file);
  11. graphics_draw_texture(image[image_count-1].texture, x, y);
  12. return 0;
  13. }

B. 使用libjpeg-turbo解码器,通过封装后将JPEG图片解码为RGB888格式数据输出:


  
  1. int tjpeg2rgb(unsigned char* jpeg_buffer, int jpeg_size, unsigned char* rgb_buffer, int* size)
  2. {
  3. int ret = 0;
  4. tjhandle handle = NULL;
  5. int width, height, subsample, colorspace;
  6. int flags = 0;
  7. int pixelfmt = TJPF_RGB;
  8. int pitch = 0;
  9. handle = tjInitDecompress();
  10. if (!handle) {
  11. LOGE(TAG, "tjInitDecompress fail, ret = %d", ret);
  12. return -1;
  13. }
  14. ret = tjDecompressHeader3(handle, jpeg_buffer, jpeg_size, &width, &height, &subsample, &colorspace);
  15. if (ret < 0) {
  16. LOGE(TAG, "tjDecompressHeader3 fail, ret = %d", ret);
  17. goto finish;
  18. }
  19. LOG("width: %d, height: %d", width, height);
  20. flags |= 0;
  21. if ((rgb_buffer = (unsigned char *)tjAlloc(width * height *
  22. tjPixelSize[pixelfmt])) == NULL) {
  23. LOGE(TAG, "allocating uncompressed image buffer");
  24. goto finish;
  25. }
  26. *size = width * height * tjPixelSize[pixelfmt];
  27. pitch = tjPixelSize[pixelfmt] * width;
  28. ret = tjDecompress2(handle, jpeg_buffer, jpeg_size, rgb_buffer, width, pitch,
  29. height, pixelfmt, flags);
  30. if (ret < 0) {
  31. LOGE(TAG, "tjDecompress2 fail, ret = %d", ret);
  32. tjFree(rgb_buffer);
  33. }
  34. finish:
  35. tjDestroy(handle);
  36. return ret;
  37. }

图像格式转换

因为屏幕是RGB565格式,需要将图像进一步转换为RGB565格式。


  
  1. int rgb888torgb565(unsigned char* rgb888_buf, int rgb888_size, unsigned short *rgb565_buf, int rgb565_size)
  2. {
  3. int i = 0;
  4. unsigned char Red = 0;
  5. unsigned char Green = 0;
  6. unsigned char Blue = 0;
  7. int count = 0;
  8. if(rgb888_buf == NULL || rgb888_size <= 0 || rgb565_buf == NULL || \
  9. rgb565_size <= 0 || (rgb565_size < (rgb888_size/3)*2)) {
  10. printf("Invail input parameter in %s\n", __FUNCTION__);
  11. return -1 ;
  12. }
  13. for(i = 0; i<rgb888_size; i += 3) {
  14. Red = rgb888_buf[i] >> 3;
  15. Green = rgb888_buf[i+1] >> 2;
  16. Blue = rgb888_buf[i+2] >> 3;
  17. rgb565_buf[count++] = ((Red <<11)|(Green<<5)|(Blue));
  18. }
  19. return count;
  20. }

显示图像画面

同样在显示图像画面也有两种方式实现。

A. 使用SDL显示:


  
  1. void graphics_flip() {
  2. SDL_RenderPresent(renderer);
  3. SDL_RenderClear(renderer);
  4. SDL_DestroyTexture(image_texture);
  5. }

B. 使用LCD hal接口实现:


  
  1. /*show picture on lcd screen*/
  2. hal_lcd->lcd_frame_draw(framebuffer);

代码在前面的http_stream_process中实现,使用该方式直接调用lcd hal接口,如果不想通过SDL实现更丰富的UI相关功能,采用该方式的效率更高。

3.3.3 代码编译


  
  1. $aos make distclean
  2. $aos make wificamera_demo@haas100 -c config
  3. $aos make

3.3.4 代码烧录

如果是使用的Window烧录工具参考《HaaS100快速开始》,烧录的文件位于:

$out/wificamera_demo@haas100/binary/wificamera_demo@haas100.bin

将文件替换到write_flash_gui/ota_bin/ota_rtos.bin。

如果使用的是docker环境参考《一步搞定AliOS Things开发环境安装》4.3烧录固件。

3.3.5 网络连接

按照2.3配置ESP32-EYE后,我们可以通过SoftAP方式直接连接ESP32-EYE,或者将ESP32-EYE和HaaS100连接统一个路由器。

 

A. HaaS100连接ESP32 SoftAP:

$netmgr -t wifi -c ESP32-Camera

使用该方法,使用http_stream_process("http://192.168.4.1:80/capture")获取图像数据。

 

B. HaaS100连接路由器:

$netmgr -t wifi -c haas_test 12345678

ssid/password根据自己路由器配置进行修改。连接路由器后,就需要通过ESP32-EYE的串口Log来确认相应的IP是多少,如3.2.1.7看到连接路由器后的IP是192.168.3.135,然后填入到http_stream_process入口参数中。网络连接成功后就可以在LCD屏上看到画面了。

 

四、总结

WiFi摄像头因为网络具有一定的延迟,帧率在5~10帧,对于延迟要求不高的场合是可以满足需求的。后续会加入USB或SPI本地摄像头降低延迟丰富业务场景。喜欢本文的朋友可以点赞收藏,评论区回复交流哦,谢谢。

 

五、开发者技术支持

如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号

更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/


转载:https://blog.csdn.net/HaaSTech/article/details/113253288
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场