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

在这里插入图片描述

一、前言

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

HaaS100 WiFi Camera.gif

二、方案简介

2.1 方案组成

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

image.png

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格式。

image.png

三、方案实践

3.1 LCD适配

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

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

       image.png                  image.png

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为例进行环境变量设置:

$cd ~/esp/esp-idf
$./install.sh
$chmod . $HOME/esp-who/esp-idf/export.sh
$source . $HOME/esp-who/esp-idf/export.sh

注意:

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

代码编译

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

$cd examples/single_chip/camera_web_server/
$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

image.png

所以camera wifi的IP就是192.168.3.135。

3.2.2 ESP32 EYE网络设置

SoftAP模式

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

image.png

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

image.png

Station模式

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

image.png

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

image.png

获取图像

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

image.png

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

int application_start(int argc, char **argv)
{
    ......
    set_iotx_info();
    netmgr_init();
    ......
    aos_register_event_filter(EV_WIFI, wifi_service_event, NULL);
    ......
 
    return 0;
}

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

static void wifi_service_event(input_event_t *event, void *priv_data)
{
    if (event->type != EV_WIFI) {
        return;
    }

    if (event->code != CODE_WIFI_ON_SNTP_OK)
        return;

    if (!linkkit_started) {
        LOG("start to do ucloud_ai_demo\n");
        if (ucloud_ai_init() < 0)
            return;

        aos_task_new("wificamera_process",wificamera_main, NULL, 1024*10);
        aos_task_new("linkkit", (void (*)(void *))linkkit_main, NULL, 1024 * 10);
        linkkit_started = 1;
    }
}

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

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

int ucloud_ai_main(void *p)
{
    int ret = 0;
    int recv_len = 0;
    char * customer_header = "Accept: */*\r\n";
    FILE *jpegFile = NULL;
    char *url = WIFICAMERA_URL;
    char *upload_url = NULL;

    LOG("start ucloud_ai_main\n");
    /*for wificamera client*/
    ret = httpclient_prepare(&wificamera_client_data, HEAD_SIZE, BODY_SZIE);
    if (ret != HTTP_SUCCESS)
        return -1;

    wificamera_client.is_http = true;
    httpclient_set_custom_header(&wificamera_client, customer_header);
    ret = httpclient_conn(&wificamera_client, (const char *)url);
    if(HTTP_SUCCESS != ret) {
        LOGE(TAG, "http connect failed");
        return -1;
    }

    /*for ai client*/
    ret = httpclient_prepare(&ai_client_data, HEAD_SIZE, BODY_SZIE);
    if (ret != HTTP_SUCCESS)
        return -1;
    ai_client.is_http = true;
    httpclient_set_custom_header(&ai_client, customer_header);
    ......

    while (1) {
        ret = http_get_image(&wificamera_client, &wificamera_client_data, url);
        if (ret <= 0) {
            LOGE(TAG, "http_get_image fail\n");
            continue;
        }
        .......
    }

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

static int32_t http_get_image(httpclient_t *client, httpclient_data_t *client_data, char *url)
{
    int ret;
    int recv_len = 0;

    httpclient_reset(client_data);
    ret = httpclient_send(client, (const char *)url, HTTP_GET, client_data);
    if(HTTP_SUCCESS != ret) {
        LOGE(TAG, "http send request failed");
        return -1;
    }
    do {
        ret = httpclient_recv(client, client_data);
        if (ret < 0)
            break;
        recv_len = client_data->response_content_len;
    } while (ret == HTTP_EAGAIN);
    return recv_len;
}

存储图像数据

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

static int32_t save_captured_image(char *buf, int len, char *path)
{
    FILE *jpegFile;

    if ((jpegFile = fopen(path, "wb")) == NULL) {
        LOGE(TAG, "opening output file fail\n");
        return -1;
    }
    if (fwrite(buf, len, 1, jpegFile) < 1) {
        LOGE(TAG, "write buf fail\n");
        return -1;
    }
    fclose(jpegFile);
    return 0;
}

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

解码JPEG图像

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

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

int graphics_draw_image(const char *file,  int x, int y)
{
    SDL_Rect drect = { x, y, 0, 0 };
    if(strcmp(file, "") == 0) return;
    for(int i = 0; i < image_count; i++) {
        if(strcmp(file, image[i].file) != 0) continue;
        graphics_draw_texture(image[i].texture, x, y);
        return 0;
    }
    graphics_generate_image(file);
    graphics_draw_texture(image[image_count-1].texture, x, y);
    return 0;
}

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

int tjpeg2rgb(unsigned char* jpeg_buffer, int jpeg_size, unsigned char* rgb_buffer, int* size)
{
    int ret = 0;
    tjhandle handle = NULL;
    int width, height, subsample, colorspace;
    int flags = 0;
    int pixelfmt = TJPF_RGB;
    int pitch = 0;
 
    handle = tjInitDecompress();
    if (!handle) {
        LOGE(TAG, "tjInitDecompress fail, ret = %d", ret);
        return -1;
    }
    ret = tjDecompressHeader3(handle, jpeg_buffer, jpeg_size, &width, &height, &subsample, &colorspace);
    if (ret < 0) {
        LOGE(TAG, "tjDecompressHeader3 fail, ret = %d", ret);
        goto finish;
    }
    LOG("width: %d, height: %d", width, height);
    flags |= 0;
    if ((rgb_buffer = (unsigned char *)tjAlloc(width * height *
                                           tjPixelSize[pixelfmt])) == NULL) {
        LOGE(TAG, "allocating uncompressed image buffer");
        goto finish;
    }
    *size = width * height * tjPixelSize[pixelfmt];
    pitch = tjPixelSize[pixelfmt] * width;
    ret = tjDecompress2(handle, jpeg_buffer, jpeg_size, rgb_buffer, width, pitch,
            height, pixelfmt, flags);
     if (ret < 0) {
        LOGE(TAG, "tjDecompress2 fail, ret = %d", ret);
        tjFree(rgb_buffer);
    }

finish:
    tjDestroy(handle);
    return ret;
}

图像格式转换

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

 int  rgb888torgb565(unsigned char* rgb888_buf, int rgb888_size, unsigned short *rgb565_buf, int rgb565_size)
{ 
    int i = 0;
    unsigned char Red = 0;
    unsigned char Green = 0;
    unsigned char Blue = 0;
    int count = 0;
 
    if(rgb888_buf == NULL || rgb888_size <= 0 || rgb565_buf == NULL || \
        rgb565_size <= 0 || (rgb565_size < (rgb888_size/3)*2)) {
        printf("Invail input parameter in %s\n", __FUNCTION__);
        return -1 ;
    }

    for(i = 0; i<rgb888_size; i += 3) {
        Red = rgb888_buf[i] >> 3;
        Green = rgb888_buf[i+1] >> 2;
        Blue = rgb888_buf[i+2] >> 3;
        rgb565_buf[count++] = ((Red<<11)|(Green<<5)|(Blue));
    } 
    return count;
}

显示图像画面

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

A. 使用SDL显示:

void graphics_flip() {
    SDL_RenderPresent(renderer);
    SDL_RenderClear(renderer);
    SDL_DestroyTexture(image_texture);
}

B. 使用LCD hal接口实现:

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

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

3.3.3 代码编译

$aos make distclean
$aos make wificamera_demo@haas100 -c config
$aos make

3.3.4 代码烧录

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

$out/wificamera_demo@haas100/binary/[email protected]

将文件替换到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/

猜你喜欢

转载自blog.csdn.net/HaaSTech/article/details/113253288