human_face_detect-1.1

human_face_detect-1.1

这篇博客写了复现esp-dl官方给的算例human_face_detect的过程和一些解决思路。
原文直接参考文档:
https://github.com/espressif/esp-dl/blob/release/v1.1/examples/human_face_detect/README_cn.md
https://docs.espressif.com/projects/esp-dl/zh_CN/release-v1.1/esp32s3/introduction.html#id4

需要补充的一点点shell知识

在windows中的powershell中,和ubuntu中的bash语法稍有不同。例如:
使用 Remove-Item 删除本地 esp-dl 文件夹:
`Remove-Item -Recurse -Force .\esp-d

驱动问题

当设备管理器中看到一个usb serial设备显示黄色感叹号时,证明电脑能读取到一个esp32s3设备但是没法正常通信,因此需要重新打驱动CH340(CH341是和340通用的)
windows:
https://www.wch.cn/downloads/file/65.html?time=2024-11-11%2016:23:41&code=NokOcQndbAiFrUztVEKcf1eZsx3x82vmIITMErTp
linux:
https://www.wch.cn/downloads/file/177.html?time=2024-11-11%2016:25:36&code=Dj3svYfYENNLKkggzMqECTTlDbDCGU8NUgONgvuE
安装即可识别。

对espressif的esp-dl官方教程的解释

我从官方在线pdf版的教程进入,不管选择esp32s3的pre-release的master分支,还是release的release/v1.1分支(在线pdf版本中只有这两个分支的教程),只要从尝试模型库中的模型的人脸检测部分跳转,挑战后的内容实际都是根据release/v1.1写的。而本人也本着稳定使用的原则采用release/v1.1的版本,这两个库中的内容有很大区别,如下图对比。

概述

1. 项目结构的模块化和标准化

  • release/v1.1
    • 项目结构偏向传统的嵌入式开发方式,文件分布比较直接,比如有硬编码的头文件(image.hpp)用于存储图像数据。
    • 头文件、库、代码文件都需要显式地包含和链接。这使得项目更加紧密集成。
  • master
    • 使用 idf_component.yml 来管理项目中的依赖项,使得项目各个部分的开发和复用更加方便。这种方式更符合 ESP-IDF 的现代化组件标准,让项目更适合长期维护和扩展。

2. 文件组织与依赖的简化

  • 代码和头文件的变化
    • release/v1.1 中,使用了 image.hpp 直接嵌入图像数据,并不灵活。
    • master 中,删除了这种硬编码的做法,改为通过组件的方式(比如依赖 esp_jpeg)来处理图像,提供了更高的灵活性和解耦能力。
  • 库链接管理
    • release/v1.1 中,CMakeLists.txt 中显式地对不同芯片类型进行静态库的链接管理,这使得项目对芯片的支持有了更多的控制,但也增加了代码的复杂性。
    • master 版本不再显式链接多个库,而是通过组件管理自动处理。

3. 文件角色的变化

  • 数据存储方式的变化:(这一点尤其重要,相信动手自己试过的朋友肯定能感受出来很大差异)
    • **image.hpp vs human_face.jpg**:
      • release/v1.1 使用 image.hpp 将图像数据直接嵌入代码,这种方式可以让开发者快速测试模型,但不适合大规模使用。
      • master 中,示例图像文件(Mona_Lisa.jpg)被直接作为文件嵌入项目。这表明新的结构倾向于从文件系统加载数据,而不是将数据硬编码。这种方式更通用,并且减少了对代码体积的影响。

总结:从紧耦合到模块化的转变

  • release/v1.1 的设计更关注直接有效的嵌入式实现,适合于快速开发和验证。它的代码和数据紧耦合,所有内容都集中管理,但这也意味着不太灵活,且难以扩展和维护。
  • master 的改进方向明显朝着现代嵌入式开发的最佳实践靠拢,重点在于模块化、复用性和自动化管理。通过使用组件化管理工具(如 idf_component.yml)和外部资源加载,它减少了手动配置的复杂性,使得项目更适合长期维护和功能扩展。

*.esp32s3配置文件替代性分析

  1. 基础配置:如果项目不需要使用特殊的 SPI RAM 模式(如 Octal 模式)或自定义分区表等高级配置,release/v1.1 的文件已经包含了基础的 CPU 频率和数据缓存设置,可以直接使用。

  2. 性能优化和兼容性:如果项目需要更高的性能(如特定的 SPI RAM 模式、闪存设置)或者兼容更高版本的 ESP-IDF(例如 5.4.0),那么可以参考 master 的配置文件。

  3. sdkconfig.defaults 文件只在编译过程中被读取,设置相应的编译选项。它们不需要读取文件夹或其他文件,也不会有相互依赖。

partitions文件变化

存储空间的变化

  • master 分支将 factory 分区的大小从 2MB 增加到 8MB,这可能是为了给更大的应用程序提供足够的存储空间。

位于项目 main 目录中的CMakeLists.txt文件执行差异

Release/v1.1 执行过程

  1. 组件注册
    • idf_component_register 会把所有的源文件(如 app_main.cpp)和包含目录加入到编译过程中。
    • 因为有很多 include 目录,编译器需要处理和查找多个模块的头文件。
  2. 静态库链接
    • 根据目标芯片(如 esp32s3),选择对应的库文件路径(libhuman_face_detect.alibdl.a),并将这些库文件链接到项目中。
    • 这些库文件提供了特定的功能,例如人脸检测算法。
  3. 总结
    • 这种方式适合需要多个模块和特定功能的复杂项目,因为它手动管理和链接了多个资源。

Master 执行过程

  1. 组件注册
    • 只包含当前目录的源文件和头文件,非常简洁。
    • 依赖的组件(如 esp_jpeg)是通过 idf_component_register 自动引用的,ESP-IDF 会在构建时处理这些依赖,减少手动配置的步骤。
  2. 嵌入文件
    • 有个嵌入的文件(human_face.jpg),这意味着这个文件会被直接打包到程序中,便于在程序中直接访问。
  3. 总结
    • 这种方式更模块化和简单,适合对依赖要求较少的项目,编译过程也会更快一些,因为引用的头文件和库更少。

yml文件

idf_component.yml 文件是用于模块化管理项目的组件和依赖,目的是为了使开发更加灵活和自动化。

执行过程

部署部分代码

1、修改代码后先删除原来的build文件夹
2、运行esp-idf的cmd或powershell快捷方式
本人桌面上的powershell快捷方式不知为何打开变成了cmd命令行,因此可以先执行下列代码完成环境配置:
& "D:\language\Espressif\Initialize-Idf.ps1" -IdfId esp-idf-c1b3982edf7b5833f785c865a3465866
若读者也出现类似问题可以尝试找到自己esp-idf安装位置的ps1脚本进行同样的跳转
3、idf.py build创建build文件夹,也可同时选中esp32s3型号:idf.py -D IDF_TARGET=esp32s3 build
4、idf.py flash进行烧录
5、idf.py monitor监视esp32s3的输出;按ctrl+]退出监视终端
本人因为为了实现后续批注图片的编写性,因此这里执行idf.py monitor | .\parse_face_detect.ps1,后文会介绍功能

master部分

master部分可以直接执行成功,但因为我还没有太搞懂在不引用include文件夹中的各种hpp头文件的过程是怎么运行的,并且这是一个pre-release版本后续可能还会有官方改动,因此我没有尝试采用其他的图片。烧录成功后即进行下一部分的release/v1.1部分。

release/v1.1部分

hpp头文件问题:

执行遇到的第一个问题是如下图所示,可以看到是”\esp-dl\include\tool\dl_tool.hpp”出了问题:

因此将对应的hpp头文件中的对应行进行如下修改:
增加包含头文件:#include <inttypes.h>
第399行改为:printf("latency: %" PRIu32 " us\n", this->get_average_period());
第413行改为:printf("%s: %" PRIu32 " us\n", message, this->get_average_period());
第428行改为:printf("%s:%s: %" PRIu32 " us\n", prefix, key, this->get_average_period());
虽然397、411、426行也有对应的语法,但是并未报错因此不做修改,否则会报错。

PSRAM初始化失败

错误信息:

1
2
3
E (351) spiram: SPI RAM enabled but initialization failed. Bailing out.
E (351) cpu_start: Failed to init external RAM!
abort() was called at PC 0x40375451 on core 0

禁用PSRAM步骤:
执行idf.py menuconfig

  • 进入 ESP PSRAM 菜单
    • 选择 ESP PSRAM ---> 选项。
  • 禁用 PSRAM 支持
    • 有一个选项 Enable PSRAM supportSupport for external, SPI-connected RAM (PSRAM)
    • 将这个选项取消勾选,确保它是禁用状态。

第一种因为大小报错的情况:Flash 或分区表问题导致build过程报错

报错图如下:

解决方法:

方法1:修改 Flash 大小配置

idf.py menuconfig
Serial Flasher Config -> Flash size,将其改为更大的值,esp32s3根据型号等不同有两种最大闪存16MB或32MB。
Esc退出,按y保存更改。

方法 2:调整分区表大小

如果按照方法1中,使用的硬件的 Flash 已经调整至最大值,则需要调整分区表以适配 Flash 容量。
partitions.csv中的factory行(应用分区)最后一个值(其总大小)调整至至少比main文件夹大(因为占flash最大的部分是图片hpp文件)。

第二种因为大小报错的情况:能成功烧录但monitor循环报错

这种情况报错如下图所示,其前半部分绿色的参数部分输出是正常的,在该输出坐标的白色位置开始报错,结尾有大段关于build文件夹中路径的黄色报错,并且三部分反复循环,ctrl+]也无法关闭monitor界面,只能直接关闭终端。

这种情况是因为本人一开始本人弄错了图片清晰度尺寸的关系,一开始我以为只要将网上下载的图片处理到和示例蒙娜丽莎的大小40KB左右即可(这个处理实际上是降低清晰度的过程),但总是出现这种情况的报错,后来在观察从image.jpg生成的image.hpp文件大小时,发现需要将图片处理成宽度和高度都在约600px以下的图片才能完成正常识别。
图片处理网站:https://www.iloveimg.com/zh-cn/resize-image#resize-options,pixels
后续会自制离线能运行的cpp代码,方便编入esp32s3。

后续操作步骤简化

提取python命令

原文说打印检测结果的分数值和坐标值为如下时:

1
2
3
4
5
6
7
8
idf.py flash monitor

... ...

[0] score: 0.987580, box: [137, 75, 246, 215]
left eye: (157, 131), right eye: (199, 133)
nose: (170, 163)
mouth left: (158, 177), mouth right: (193, 180)

运行如下标注命令:

1
python display_image.py -i ../human_face_detect/image.jpg -b "(137, 75, 246, 215)" -k "(157, 131, 199, 133, 170, 163, 158, 177, 193, 180)"

但这还需要繁琐的编辑,因此在这里编辑一个powershell脚本,可以实现python命令的自动输出,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 简化版人脸检测结果解析脚本

$input | ForEach-Object {

    $line = $_

    # 匹配人脸检测结果行,修改正则表达式以更准确匹配输出格式

    if ($line -match '^\[(\d+)\] score: ([\d.]+), box: \[([\d, ]+)\]\s*$') {

        # 保存box信息

        $faceIndex = $matches[1]

        $box = $matches[3]

    }

    # 匹配关键点信息

    elseif ($line -match 'left eye: \(([\d, ]+)\), right eye: \(([\d, ]+)\)') {

        $leftEye = $matches[1]

        $rightEye = $matches[2]

    }

    elseif ($line -match 'nose: \(([\d, ]+)\)') {

        $nose = $matches[1]

    }

    elseif ($line -match 'mouth left: \(([\d, ]+)\), mouth right: \(([\d, ]+)\)') {

        $mouthLeft = $matches[1]

        $mouthRight = $matches[2]

        # 当收集到所有信息后,输出命令

        if ($box -and $leftEye -and $rightEye -and $nose -and $mouthLeft -and $mouthRight) {

            $keypoints = "$leftEye, $rightEye, $nose, $mouthLeft, $mouthRight"

            $command = "python display_image.py -i ../human_face_detect/image.jpg -b `"($box)`" -k `"($keypoints)`""

            Write-Host $command

            # 清除变量,为下一个人脸做准备

            $box = $leftEye = $rightEye = $nose = $mouthLeft = $mouthRight = $null

        }

    }

}

该脚本可以实现将任意数量的人脸输出结果中需要的参数提取,并输出下一步需要执行的python命令。如果是多张人脸则逐行输入各自独立的python代码,然后复制粘贴执行即可。

python命令改写

原库中的display图片的python脚本在多张人脸时,生成的第二张图中不会保留第一张图的批注痕迹,因此改为如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import ast

import cv2

import os



def draw_boxes(image_path, boxes, keypoints=None, output_path=None):

    # Load the image from the given path

    image = cv2.imread(image_path)

    # Ensure boxes is a list of tuples, even if only a single tuple is provided

    if isinstance(boxes, tuple):

        boxes = [boxes]

    # Loop through each box coordinate and draw on the image

    for box in boxes:

        x1, y1, x2, y2 = box

        cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)  # Draw green rectangles

    # If keypoints are provided, draw them on the image

    if keypoints:

        if isinstance(keypoints[0], int):

            keypoints = [keypoints]

        for kp in keypoints:

            for i in range(0, len(kp), 2):

                x, y = kp[i], kp[i + 1]

                cv2.circle(image, (x, y), 3, (0, 0, 255), -1)  # Draw red circles for keypoints

    # Save the modified image if output_path is provided

    if output_path:

        cv2.imwrite(output_path, image)

    # Display the image with all boxes

    cv2.imshow('Image with boxes', image)

    cv2.waitKey(0)  # Wait for any key to be pressed

    cv2.destroyAllWindows()  # Close the image window



if __name__ == "__main__":

    import argparse

    # Set up argument parser

    parser = argparse.ArgumentParser()

    parser.add_argument("-b", "--boxes", required=True, help="Bounding boxes for faces in the format of a list of tuples")

    parser.add_argument("-i", "--image", required=True, help="Path to the image file")

    parser.add_argument("-k", "--keypoints", required=False, help="Keypoints for faces in the format of a list of tuples")

    parser.add_argument("-o", "--output", required=False, help="Path to save the updated image file")

    args = parser.parse_args()



    # Determine the path to save intermediate results if not specified

    output_path = args.output if args.output else args.image



    # Convert the input string of boxes to a list of tuples

    boxes = ast.literal_eval(args.boxes)  # Use ast.literal_eval to safely evaluate the string as Python literal

    # Convert the input string of keypoints to a list of tuples if provided

    keypoints = ast.literal_eval(args.keypoints) if args.keypoints else None

    # Call the function to draw the boxes and keypoints on the image

    draw_boxes(output_path, boxes, keypoints, output_path)

该代码可以实现批注痕迹覆盖。

最终结果

最终结果如图所示,可以实现多张人脸的识别:


human_face_detect-1.1
https://blakehansen130.github.io/2024/11/11/human_face_detect-1.1/
发布于
2024年11月11日
许可协议