Alohaq 发表于 2025-1-12 18:48

嵌入式工程师AI挑战营(进阶):活动完成汇报贴

本帖最后由 Alohaq 于 2025-1-12 23:15 编辑

<p><span style="font-size:24px;">一、前言</span></p>

<p><span style="font-size:24px;">&nbsp; &nbsp; </span>先前早就听说Linux入门有多难,AI人脸识别有多神奇,但奈何种种原因,都没有机会接触,恰逢这次嵌入式工程师AI挑战营活动,终于有机会揭开这两者的神秘面纱!由于两者均零基础入门,下述内容有不当之处,请多多包涵!也正因为两者均为零基础,遂本次活动感觉收获满满,真的学到了很多!活动也真的很棒,哈哈,后面多多举办,我也多多参与!</p>

<p><span style="font-size:24px;">二、算法工程化</span></p>

<p><span style="font-size:24px;">&nbsp; &nbsp;&nbsp;</span>由于零基础,加上活动未限制算法模型,遂选择了一个较我而言比较容易的算法模型:Retinaface+Facenet,其中Retinaface是一个高效的单阶段人脸检测模型,使用深度学习方法定位图像中的人脸区域。它可以检测人脸的位置、角度以及人脸关键点,能够保证人脸特征提却的有效性;Facenet是一个人脸识别模型,它使用 Triplet Loss 或<strong> </strong>Softmax Loss 来训练一个深度神经网络,从而将人脸映射到一个128维的向量空间中,两个相似的人脸的向量距离非常近,而不相似的人脸的向量距离较远,即欧几里得距离值越小,两者脸部匹配度越高。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;选定了算法,如何将其应用在RV1106上呢?这就得借助RKNN-Toolkit2工具,其能够简化模型的部署,只需一个onnx即可转为RKNN模型,并提供C代码接口API在Luckfox Pico Max上进行模型推理,有了这个软件栈,用户就可以快速部署AI模型到Rockchip上!其整体框架如下图所示。活动降低了难度,无需自主训练模型,仅需找到合适的onnx将其转为rknn模型,本篇文章采用的Retinaface和Facenet模型分别来自<a href="https://github.com/bubbliiiing/retinaface-pytorch.git" target="_blank">https://github.com/bubbliiiing/retinaface-pytorch.git</a>和<a href="https://github.com/bubbliiiing/facenet-pytorch.git" target="_blank">https://github.com/bubbliiiing/facenet-pytorch.git</a>,均有已训练好的pth权重文件,都置于&lt;model_data&gt;文件夹之下,由于是Pytorch,其提供了非常方便的API来将pth格式转换为ONNX格式,这里不再赘述,默认已有ONNX格式模型。</p>

<p>&nbsp;</p>

<div style="text-align: center;"></div>

<p>&nbsp;</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;下面就说说RKNN-Toolkit2的使用,该工具本文章是在Ubuntu22.04操作系统下使用,需要依赖3.8或3.9版本的Python,以防需要在多个应用场景下灵活切换,本文章将借助版本号为4.6.14的Miniconda创建python的虚拟环境,指定版本安装命令如下:</p>

<pre>
<code class="language-bash">wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-4.6.14-Linux-x86_64.sh</code></pre>

<p>&nbsp;</p>

<p>&nbsp; &nbsp; Conda具体使用方法本文章略过,只提供指定版本安装指令。接下来就是安装RKNN-Toolkit2工具,本文章也采用了较老版本的,具体版本为v1.6.0,其获取指令如下:</p>

<pre>
<code class="language-bash">git clone --branch v1.6.0 https://github.com/rockchip-linux/rknn-toolkit2.git</code></pre>

<p>&nbsp; &nbsp; 随后用conda指定python3.8版本创建RKNN-Toolkit2相关依赖,并打开克隆文件夹,然后继续安装依赖项,下述安装指令仅针对上述版本克隆仓库,其他版本自行更改:</p>

<pre>
<code class="language-bash">pip install tf-estimator-nightly==2.8.0.dev2021122109
pip install -r rknn-toolkit2/packages/requirements_cp38-1.6.0.txt</code></pre>

<p>&nbsp; &nbsp; 接下来就可以安装RKNN-Toolkit2了,安装指令(注意:仍旧针对本文章指定v1.6.0版本仓库)如下:</p>

<pre>
<code class="language-bash">pip install rknn-toolkit2/packages/rknn_toolkit2-1.6.0+81f21f4d-cp38-cp38-linux_x86_64.whl</code></pre>

<p>&nbsp; &nbsp; 安装完成之后,可以进行导入测试。本文章安装时遭遇很多波折,切换了很多版本,python导入语句总是报错,总是提示该错误:</p>

<pre>
<code class="language-bash">ImportError: libGL.so.1: cannot open shared object file: No such file or directory</code></pre>

<p>&nbsp; &nbsp; 最后发现可能与版本无关,只需要安装opencv库即可,由于不需要UI界面,运行下述指令即可:</p>

<pre>
<code class="language-bash">pip install opencv-python-headless</code></pre>

<p>&nbsp; &nbsp; RKNN-Toolkit2安装完成之后,下面就可以转换RKNN模型了,这里可以直接用luckfox配置好的转换脚本,先来获取源码:</p>

<pre>
<code class="language-bash">git clone https://github.com/LuckfoxTECH/luckfox_pico_rknn_example.git</code></pre>

<p>&nbsp; &nbsp; 随后进入文件夹中再打开convert文件夹,使用脚本转换即可,记得正确进入Conda虚拟环境!脚本使用格式如下:</p>

<pre>
<code class="language-bash">python convert.py &lt;onnx模型地址&gt; &lt;训练集地址&gt; &lt;导出模型地址&gt; &lt;模型类型&gt;</code></pre>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;自此,就能获得您选定的AI模型及其对应Luckfox Pico Max可使用的RKNN模型。<br />
<span style="font-size:24px;">三、应用部署</span></p>

<p>&nbsp; &nbsp; 万事俱备,只欠东风。有了RKNN模型,又该如何使用呢?官方给出了一个流程图,如下图所示:</p>

<p>&nbsp;</p>

<div style="text-align: center;">
<div style="text-align: center;">&nbsp;&nbsp;&nbsp;&nbsp;</div>
</div>

<p>&nbsp; &nbsp; 大体就是初始化rknn,本文章使用两个模型,若内存资源紧张可考虑两个模型内存服复用,然后获取输入输出的通道数并为其申请和设置内存,后面就简单了,输入图像后进行模型推理即可。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;本文章使用一个芯片为ST7789VW的1.14寸的LCD小屏幕,摄像头及模型推理结果可直接在该屏幕上显示出来,在luckfox config中启用FBTFT即可,在程序中可通过/dev/fb0控制。摄像头采用的SC3336,程序上使用的opencv-mobile获取输入的图像,并用其将图像数据转换为模型所需要的大小,不过需要注意,因为程序中用到该摄像头,而系统又存在默认rkipc程序会导致冲突,进而无法使用该摄像头,需要关闭rkipc进程,根据rkipc关键词搜索进程ID并关闭即可,也可使用官方提供的RkLunch-sttop.sh进行关闭,该文件下载地址为<a href="https://github.com/LuckfoxTECH/luckfox-pico/blob/40639eccad8325a669604c95cf06ab002161cb97/project/app/rkipc/rkipc/src/rv1106_ipc/RkLunch-stop.sh" target="_blank">https://github.com/LuckfoxTECH/luckfox-pico/blob/40639eccad8325a669604c95cf06ab002161cb97/project/app/rkipc/rkipc/src/rv1106_ipc/RkLunch-stop.sh</a>,文件下载后放在个人指定位置,可在程序中进行调用,也可在程序编译产生的可执行文件运行之前手动运行,本文章是放在程序中执行,该文件放在可执行文件同级目录下即可,代码如下:</p>

<pre>
<code class="language-cpp">system("RkLunch-stop.sh");</code></pre>

<p>&nbsp; &nbsp; 活动要求自选至少3个你喜欢的人,提前录入他们的照片,找包含他们的视频或者照片等进行测试的识别情况,故需要在代码中引入至少三个照片进行提取人脸特征操作,本文章使用Facenet模型进行特征提取,并存储在特征向量中,大致代码如下:</p>

<pre>
<code class="language-cpp">// 为每张参考人脸分配特征向量
float* reference_out_fp32_one = (float*)malloc(sizeof(float) * 128);
float* reference_out_fp32_two = (float*)malloc(sizeof(float) * 128);
float* reference_out_fp32_thr = (float*)malloc(sizeof(float) * 128);

// 提取每张参考人脸的特征
letterbox(one, facenet_input);
ret = rknn_run(app_facenet_ctx.rknn_ctx, nullptr);
if (ret &lt; 0) {
    printf("rknn_run fail! ret=%d\n", ret);
    return -1;
}
output = (uint8_t *)(app_facenet_ctx.output_mems-&gt;virt_addr);
output_normalization(&amp;app_facenet_ctx, output, reference_out_fp32_one);

letterbox(two, facenet_input);
ret = rknn_run(app_facenet_ctx.rknn_ctx, nullptr);
if (ret &lt; 0) {
    printf("rknn_run fail! ret=%d\n", ret);
    return -1;
}
output = (uint8_t *)(app_facenet_ctx.output_mems-&gt;virt_addr);
output_normalization(&amp;app_facenet_ctx, output, reference_out_fp32_two);

letterbox(thr, facenet_input);
ret = rknn_run(app_facenet_ctx.rknn_ctx, nullptr);
if (ret &lt; 0) {
    printf("rknn_run fail! ret=%d\n", ret);
    return -1;
}
output = (uint8_t *)(app_facenet_ctx.output_mems-&gt;virt_addr);
output_normalization(&amp;app_facenet_ctx, output, reference_out_fp32_thr);</code></pre>

<p>&nbsp;&nbsp;&nbsp;&nbsp;有了参考人脸的特质,那么就需要对摄像头采集到的人脸进行特征提取,然后与上文提到的三个参考人脸的特征进行一一对比,计算出欧几里得距离,即可判断是否为同一人(前文提到,值越小,匹配度越高)。但是摄像头并不知道传来的图像是不是人脸,也不会主动将采集的图像中的人脸表识出来,这就得借助RetinaFace模型来进行检测,大致代码如下:</p>

<pre>
<code class="language-cpp">// opencv 获取摄像头捕获的图像
      cap &gt;&gt; bgr;
      cv::resize(bgr, retina_input, cv::Size(retina_width,retina_height), 0, 0, cv::INTER_LINEAR);
      // retinaface 检测人脸个数
      ret = inference_retinaface_model(&amp;app_retinaface_ctx, &amp;od_results);
      if (ret != 0)
      {
            printf("init_retinaface_model fail! ret=%d\n", ret);
            return -1;
      }

      for (int i = 0; i &lt; od_results.count; i++)
      {
            // 获取人脸位置
            object_detect_result *det_result = &amp;(od_results.results);
            mapCoordinates(bgr, retina_input, &amp;det_result-&gt;box.left , &amp;det_result-&gt;box.top);
            mapCoordinates(bgr, retina_input, &amp;det_result-&gt;box.right, &amp;det_result-&gt;box.bottom);

            cv::rectangle(bgr,cv::Point(det_result-&gt;box.left ,det_result-&gt;box.top),
                        cv::Point(det_result-&gt;box.right,det_result-&gt;box.bottom),cv::Scalar(0,255,0),3);
            
            // 获取人脸图像
            cv::Rect roi(det_result-&gt;box.left,det_result-&gt;box.top,
                         (det_result-&gt;box.right - det_result-&gt;box.left),
                         (det_result-&gt;box.bottom - det_result-&gt;box.top));
            
            cv::Mat face_img = bgr(roi);
            
            letterbox(face_img, facenet_input);
            output = (uint8_t *)(app_facenet_ctx.output_mems-&gt;virt_addr);
            ret = rknn_run(app_facenet_ctx.rknn_ctx, nullptr);
            if (ret &lt; 0) {
                printf("rknn_run fail! ret=%d\n", ret);
                return -1;
            }
            output_normalization(&amp;app_facenet_ctx, output, out_fp32); // 对输出进行归一化

            // 计算人脸特征向量之间的欧几里得距离
            float norm_one = get_duclidean_distance(reference_out_fp32_one, out_fp32);
            float norm_two = get_duclidean_distance(reference_out_fp32_two, out_fp32);
            float norm_thr = get_duclidean_distance(reference_out_fp32_thr, out_fp32);

            if (norm_one &lt; 1.1)
            {
                sprintf(show_text, "you:\r%.2f", norm_one);
                cv::putText(bgr, show_text, cv::Point(det_result-&gt;box.left, det_result-&gt;box.top - 8),
                                          cv::FONT_HERSHEY_SIMPLEX,0.5,
                                          cv::Scalar(0,255,0),
                                          1);
            }
            else if (norm_two &lt; 1.1)
            {
                sprintf(show_text, "nan:\r%.2f", norm_two);
                cv::putText(bgr, show_text, cv::Point(det_result-&gt;box.left, det_result-&gt;box.top - 8),
                                          cv::FONT_HERSHEY_SIMPLEX,0.5,
                                          cv::Scalar(0,255,0),
                                          1);
            }
            else if (norm_thr &lt; 1.1)
            {
                sprintf(show_text, "xin:\r%.2f", norm_thr);
                cv::putText(bgr, show_text, cv::Point(det_result-&gt;box.left, det_result-&gt;box.top - 8),
                                          cv::FONT_HERSHEY_SIMPLEX,0.5,
                                          cv::Scalar(0,255,0),
                                          1);
            }            
      }</code></pre>

<p>&nbsp; &nbsp; 上述是最后完成的代码片段,实际调试中,发现精度并不是很高,同张图片的欧几里得距离仅为0.8左右,其他人脸模型则为1.4以上,所以代码中采用1.1作为判断依据,对于小于1.1的则认为与参考人脸为同一人,在LCD屏幕上框出该人并将名字显示在框的左上角,名字后面跟着实时的欧几里得距离结果。<br />
&nbsp; &nbsp; 先来展示下具体的精度情况,若图片处理得当,可获得约0.6左右的欧几里得距离,会在这个范围波动,下图为结果抓拍,为0.7,识别为you(悠)即代码中的一号参考人脸:</p>

<p style="text-align: center;"><br />
</p>

<p>&nbsp; &nbsp; 主要代码已经开源,在本文章的附件中。</p>

<p><span style="font-size:24px;">三、应用部署视频演示</span></p>

<p>&nbsp; &nbsp; 在Pico-lcd-1.14上显示三个参考人脸模型的人名代号及欧几里得距离:</p>

<p>dd113988cd51adafa9f2abb172ec00fc<br />
&nbsp;</p>

<p>&nbsp;</p>

Jacktang 发表于 2025-1-18 10:29

<p>同张图片的欧几里得距离仅为0.8左右,其他人脸模型则为1.4以上,所以代码中采用1.1作为判断依据,对于小于1.1的则认为与参考人脸为同一人,,,</p>

<p>明白了</p>
页: [1]
查看完整版本: 嵌入式工程师AI挑战营(进阶):活动完成汇报贴