1. 方案简介
驾驶员行为检测:通过图像识别出这几种行为:打电话、抽烟、疲劳驾驶。
2. 快速上手
2.1 开发环境准备
如果您初次阅读此文档,请阅读《入门指南/开发环境准备/Easy-Eai编译环境准备与更新》,并按照其相关的操作,进行编译环境的部署。
在PC端Ubuntu系统中执行run脚本,进入EASY-EAI编译环境,具体如下所示。
cd ~/develop_environment
./run.sh
2.2 源码下载以及实例编译
在EASY-EAI编译环境下创建存放源码仓库的管理目录:
cd /opt
mkdir EASY-EAI-Toolkit
cd EASY-EAI-Toolkit
通过git工具,在管理目录内克隆远程仓库
git clone https://github.com/EASY-EAI/EASY-EAI-Toolkit-C-Solution.git
注:
* 此处可能会因网络原因造成卡顿,请耐心等待。
* 如果实在要在gitHub网页上下载,也要把整个仓库下载下来,不能单独下载本实例对应的目录。
进入到对应的例程目录执行编译操作,具体命令如下所示:
cd EASY-EAI-Toolkit-C-Solution/solu-dms/
./build.sh
注:
* 由于依赖库部署在板卡上,因此交叉编译过程中必须保持adb连接。
注:
* 若build.sh脚本不带任何参数,则仅会拷贝solution编译出来的可执行文件。
* 若build.sh脚本带有cpres参数,则会把Release/目录下的所有资源都拷贝到开发板上。
* 若build.sh脚本带有clear参数,则会把build/目录和Release/目录删除。
2.3 模型获取
【百度网盘】
链接:百度网盘 请输入提取码
提取码:0k7j
本方案用到四个模型:face_detect.model、face_landmark98.model、phonecall_detect.model、smoke_detect.model
直接把模型下载到本地Windows主机,复制
进入PC端Ubuntu创建存放model目录:
cd /opt
mkdir model
然后把模型从本地Windows主机粘贴到PC端Ubuntu中:
2.4 方案部署
使用下方命令再次回到开发实例目录
cd /opt/EASY-EAI-Toolkit-C-Solution/solu-dms/
然后,将EASY-EAI编译环境的编译结果部署到板卡中(有两种方法)。
方法一:通过执行以下命令手动部署【推荐】
cp Release/* /mnt/userdata/Solu
方法二:在编译时加上编译参数自动部署
./build.sh cpres
最后,将准备好的模型部署到板卡中(注意:模型要放到编译结果的同一目录中),执行命令如下所示。
cp /opt/model/*.model /mnt/userdata/Solu
2.5 示例方案运行
通过按键Ctrl+Shift+T创建一个新窗口,执行adb shell命令,进入板卡运行环境。
adb shell
进入板卡后,定位到例程部署的位置,如下所示:
cd /userdata/Solu
运行例程命令如下所示:
./solu-dms
2.6 运行效果
运行打印:
root@EASY-EAI-NANO:/userdata/Solu# ./solu-dms
media get entity by name: rkcif-lvds-subdev is null
media get entity by name: rkcif-lite-lvds-subdev is null
media get entity by name: rkisp-mpfbc-subdev is null
media get entity by name: rkisp_dmapath is null
media get entity by name: rkisp-mpfbc-subdev is null
media get entity by name: rkisp_dmapath is null
media get entity by name: rockchip-mipi-dphy-rx is null
[15:47:08.490666][CAMHW]:XCAM ERROR CamHwIsp20.cpp:928: No free isp&ispp needed by fake camera!
Rga built version:1.04 94a2c08+2023-04-12 10:23:53
Had init the rga dev ctx = 0x9197d0
Rga built version:1.04 94a2c08+2023-04-12 10:23:53
##RKMEDIA Log level: 2
……
librknn_runtime version 1.7.3 (2e55827 build: 2022-08-25 10:45:32 base: 1131)
librknn_runtime version 1.7.3 (2e55827 build: 2022-08-25 10:45:32 base: 1131)
librknn_runtime version 1.7.3 (2e55827 build: 2022-08-25 10:45:32 base: 1131)
librknn_runtime version 1.7.3 (2e55827 build: 2022-08-25 10:45:32 base: 1131)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
driver state :
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
driver state :
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
driver state : [Is tired]
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
driver state : [Is tired] [Smoking] [Phone Calling]
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
driver state :
如果从摄像头画面中检测到驾驶员有异常行为,后台会打印出被检测到的异常行为标签:
并且会屏幕右侧显示对应的状态。
2.7 开机启动
首先进入板卡环境,执行以下命令,在板卡上创建一个给本例程使用的应用目录:myapp
cd /userdata/apps/
mkdir myapp
然后回到开发环境中,通过使用“2.4方案部署”类似的操作方法,把本例程所需要的全部文件,包含:编译结果,配置文件,模型等。部署到刚刚新建的myapp目录中。
最后在板卡上创建一个run.sh脚本来管控用户所有需要的应用即可,《入门指南/应用程序开机自启动》会详细描述run.sh脚本该如何编写。
3. 代码解析
方案主逻辑代码位于:EASY-EAI-Toolkit-C-Solution/solu-dms/src/main.cpp。代码实现主要通过调用我司的easyeai-api库快速实现驾驶员行为检测功能,代码主体分为主线程和算法分析子线程。
3.1 组件库组成
要实现安全帽检测功能,需要使用到easyeai-api库的以下组件,如下所示。
模组信息如下所示。
组件 | 头文件以及库路径 | 描述 |
系统操作组件 | easyeai-api/common_api/system_opt | 提供线程操作函数 |
摄像头组件 | easyeai-api/peripheral_api/camera | 提供摄像头操作函数 |
显示屏组件 | easyeai-api/peripheral_api/display | 提供显示屏操作函数 |
人脸检测组件 | easyeai-api/algorithm_api/face_detect | 提供人脸检测操作函数 |
人脸98关键点组件 | easyeai-api/algorithm_api/face_landmark98 | 提供人脸98关键点操作函数 |
吸烟检测组件 | easyeai-api/algorithm_api/smoke_detect | 提供吸烟检测操作函数 |
打电话检测组件 | easyeai-api/algorithm_api/phonecall_detect | 提供打电话检测操作函数 |
这些组件通过CMakeLists.txt编译进工程,具体请看后续章节。
3.2 逻辑框图
项目的整体逻辑框图如下所示。
3.3 主线程
主线程处理的业务有:
- 初始化外设;
- 创建算法分析子线程;
- 抓图发送给到子线程;
- 抓图、显示;
本处附上主要的逻辑功能代码,其他辅助的、校验型的代码先忽略。
组件初始化操作如下,本处调用RGB摄像头。
// 1.打开摄像头
ret = rgbcamera_init(CAMERA_WIDTH, CAMERA_HEIGHT, 90);
pbuf = NULL;
pbuf = (char *)malloc(IMAGE_SIZE);
创建线程互斥锁以及线程,如下所示。
// 2.创建识别线程,以及图像互斥锁
pthread_mutex_init(&img_lock, NULL);
pResult = (Result_t *)malloc(sizeof(Result_t));
memset(pResult, 0, sizeof(Result_t));
if(0 != CreateNormalThread(detect_thread_entry, pResult, &mTid)){
free(pResult);
}
初始化显示屏,如下所示。
// 3.显示初始化
#define SCREEN_WIDTH 720
#define SCREEN_HEIGHT 1280
screen.screen_width = SCREEN_WIDTH;
screen.screen_height = SCREEN_HEIGHT;
screen.wins[0].rotation = 270;
screen.wins[0].in_fmt = IMAGE_TYPE_BGR888;
screen.wins[0].enable = 1;
screen.wins[0].in_w = 640;
screen.wins[0].in_h = 720;
screen.wins[0].HorStride = screen.wins[0].in_w;
screen.wins[0].VirStride = screen.wins[0].in_h;
screen.wins[0].crop_x = 0;
screen.wins[0].crop_y = 0;
screen.wins[0].crop_w = 640;
screen.wins[0].crop_h = 720;
screen.wins[0].win_x = 0;
screen.wins[0].win_y = 0;
screen.wins[0].win_h = 640;
screen.wins[0].win_w = 720;
screen.wins[1].rotation = 270;
screen.wins[1].in_fmt = IMAGE_TYPE_BGR888;
screen.wins[1].enable = 1;
screen.wins[1].in_w = CAMERA_WIDTH;
screen.wins[1].in_h = CAMERA_HEIGHT;
screen.wins[1].HorStride = CAMERA_WIDTH;
screen.wins[1].VirStride = CAMERA_HEIGHT;
screen.wins[1].crop_x = 0;
screen.wins[1].crop_y = 0;
screen.wins[1].crop_w = 640;
screen.wins[1].crop_h = 720;
screen.wins[1].win_x = 0;
screen.wins[1].win_y = 640;
screen.wins[1].win_h = 640;
screen.wins[1].win_w = 720;
ret = disp_init_pro(&screen);
if (ret) {
printf("error: %s, %d\n", __func__, __LINE__);
goto exit1;
}
抓取图像,调用clone操作。
// 4.(取流 + 显示)循环
pthread_mutex_lock(&img_lock);
ret = rgbcamera_getframe(pbuf);
algorithm_image = Mat(CAMERA_HEIGHT, CAMERA_WIDTH, CV_8UC3, pbuf);
image = algorithm_image.clone();
pthread_mutex_unlock(&img_lock);
调用显示图像接口,一边显示镜头画面,另一边通过Result将分析的结果显示对应的状态。
// 根据状态贴提示图
if(driverState != Result.drvState){
driverState = Result.drvState;
if(0 == driverState){
tipsImage = cv::imread("normal.jpg",1);
}else if(driverState & (0x01<<SMOKE_FLAG)){
tipsImage = cv::imread("smoking.jpg",1);
}else if(driverState & (0x01<<PHONE_FLAG)){
tipsImage = cv::imread("calling.jpg",1);
}else if(driverState & (0x01<<TIRE_FLAG)){
tipsImage = cv::imread("tired.jpg",1);
}
disp_commit_pro(tipsImage.data, 0, tipsImage.cols*tipsImage.rows*3);
}
disp_commit_pro(image.data, 1, IMAGE_SIZE);
3.4 算法分析子线程
算法分析子线程,主要完成以下操作:
- 延时监测是否图像缓冲区是否为空;
- 不为空时,证明主函数已发送图像数据过来,线程执行图像获取操作;
- 调用吸烟检测分析函数;
- 调用打电话检测分析函数;
- 调用人脸检测分析函数,以及人脸98关键点分析函数
- 记录模型输出的数据,分析数据并输出算法结果,并用于后续显示画面合成操作;
延时监测是否有图像,操作如下所示。
if(algorithm_image.empty()) {
usleep(5);
continue;
}
获取图像操作如下所示。
pthread_mutex_lock(&img_lock);
image = algorithm_image.clone();
pthread_mutex_unlock(&img_lock);
调用吸烟检测函数,算法得到的目标结果记录于pResult内,如下所示。
/* 1.抽烟检测 */
ret = smoke_detect_run(smokeCtx, image, &pResult->smoke_result);
调用打电话检测函数,算法得到的目标结果记录于pResult内,如下所示。
/* 2.打电话检测 */
ret = phonecall_detect_run(pcCtx, image, &pResult->phonecall_result);
调用人脸检测分析函数,以及人脸98关键点分析函数。得到结果后分析是否有闭眼,打哈欠等动作。
// 人脸检测
ret = face_detect_run(faceCtx, image, pResult->face_result);
// 把人脸裁出,并按256x256像素进行图像缩放
cv::Mat roi_img, reize_img;
roi_img = image(cv::Rect(x, y, max,max));
roi_img = roi_img.clone();
resize(roi_img, reize_img, Size(256,256), 0, 0, INTER_AREA);
pResult->ratio = (float)max/256;
// 把256x256的人脸图像送入98关键点函数找出98个关键点
ret = face_landmark98_run(landmarkCtx, &reize_img, &keyPoints);
// 把98个关键点送入【闭眼】,【打哈欠】判断函数
if(eyesClosing(keyPoints)||yawning(keyPoints)){
tired_count++;
}else{
tired_count = 0;
}
// 【闭眼】或【打哈欠】动作连续维持3次以上,则判断为【疲劳】状态
if(3 <= tired_count){
pResult->drvState |= (0x01<<TIRE_FLAG);
}else{
pResult->drvState &=~(0x01<<TIRE_FLAG);
}
// 最后清空这98个点的数据
std::vector<KeyPointType>().swap(keyPoints);
4. 开发指南
4.1 示例文件&目录结构
Solution git仓库会随着产品迭代更新,不断新增解决方案代码,当前截图只作参考。
4.1.1 Solution git仓库目录介绍。
Solution工程构成如下所示,由功能组件easyeai-api和各个解决方案构成。
单个“solu-”开头的目录即为一个解决方案案例,代码内调用“EASY EAI-API”来满足某一实际应用场景的需求。
功能组件的描述如下所示,easyeai-api是经过高度封装的易用性组件接口,便于用户直接调用板卡资源。
功能 | 组件目录 | 组件子目录 | 描述 |
功能组件 | easyeai-api | algorithm_api | 算法组件 |
common_api | 通用组件 | ||
media_api | 多媒体组件 | ||
netProtocol_api | 网络协议组件 | ||
peripheral_api | 外设硬件组件 |
4.1.2 解决方案最基本的目录构成。
每个解决方案就是一个独立的项目,项目内包含部分如下所示,项目使用cmake构建自动编译部署。
具体介绍如下所示。
组成部分 | 描述 |
build.sh | 编译脚本,用于管理生成可执行文件后的部署准备工作,用户可自定义shell命令 |
CMakeLists.txt | 工程管理文件,用于组织整个工程结构,指导cmake生成Makefile |
include | 用于存放第三方应用库、头文件目录等 |
src | 用于存放实现本方案需求的源代码 |
4.1.3 解决方案可拓展的目录构成。
可拓展的目录是指:开发过程中增加某些功能模块,功能代码。增加模式分为两种:
- 增加已编译的第三方库,在include、libs目录内添加头文件和库文件;
- 增加用户自定义的功能模块,推荐在src目录内增加;
具体情况如下所示,第三方模块相关的文件由include/3rd_model/xxx.h、libs/3rd_model/xxx.a。自定义的功能模块为src/mySrcCode、src/mySrcCode2。
4.2 CMakeLists.txt文件解析
4.2.1 编译环境配置部分:
第一部分为配置部分,配置部分如下所示。(获取当前方案目录、配置工具链、提取方案名称):
配置信息如下所示。
配置项 | 描述 |
CMake要求版本 | cmake_minimum_required函数指定,要求的最低版本 |
CMAKE_SYSTEM_NAME | cmake的系统类型,交叉编译必须 |
CMAKE_CROSSCOMPILING | cmake是否启动交叉编译 |
cross.camke | camke_host_system_information获取平台信息,发现不是armv7l就导入当前平台的交叉编译配置。 |
project项目名 | 由project函数指定 |
4.2.2 easyeai-api配置部分
第二部分是引入我司的功能组件库(针对当前方案进行:配置EASY EAI API头文件目录、库文件目录以及配置库链接参数):
配置信息如下所示。
配置项 | 描述 |
api_inc | 最终通过target_include_directories函数指定目标包含的头文件路径 |
link_directories | 由link_directories函数指定easyeai-api库所在路径 |
LINK_LIBRARIES | 由LINK_LIBRARIES函数指定easyeai-api库文件 |
4.2.3 第三方库配置部分
第三部分配置第三方的库(针对当前方案进行:配置第三方头文件目录、库文件目录、配置第三方库链接参数以及配置源码目录):
配置信息如下所示。
配置项 | 描述 |
custom_inc | 自定义变量custom_inc,最终通过target_include_directories函数指定目标包含的头文件路径,在源码include目录下 |
link_directories | 由link_directories函数指定第三方库所在路径 |
custom_libs | 自定义变量custom_libs,最终通过target_link_libraries函数指定目标引用的库链接参数 |
aux_source_directory | 自定义变量dir_srcs,用于添加工程代码以及自定义的个人代码 |
例如添加个人库的目录组成方式如下所示。
aux_source_directory的修改方式为:
aux_source_directory(./src ./src/mySrcCode ./src/mySrcCode2 dir_srcs)
或
aux_source_directory(./src dir_srcs)
aux_source_directory(./src/mySrcCode dir_srcs)
aux_source_directory(./src/mySrcCode2 dir_srcs)
4.2.4 本方案配置部分
第四部分配置项目的编译信息,内容如下所示:
配置项如下所示。
配置项 | 描述 |
add_executable | 编译结果为${CURRENT_FOLDER}指定,即方案目录名; 编译的源文件为${dir_srcs}指定; |
target_include_directories | 指定头文件的名字,由${api_inc}与${custom_inc}指定; |
target_link_libraries | 指定额外的库,例如opencv的库等 |
4.3 build.sh编译脚本:
4.3.1 路径定位部分
第一部分用于提取目录用于编译操作,内容如下所示:(进入build.sh脚本所在目录,并且提取当前目录绝对路径,提取当前目录名称)
4.3.2 清除编译部分
第二部分清除操作,清除目录为build、Release,内容如下所示:(执行build.sh脚本时,带入了参数“clear”,则清空编译输出)
4.3.3 编译操作
第三部分,编译直接调用cmake,内容如下所示:(重新编译,成部署目录,并把资源自动部署进板卡)