- 2025-01-14
-
回复了主题帖:
【 AI挑战营(进阶)】【复现】使用inspirefacesdk的单人实时视频流识别
junan007 发表于 2025-1-14 16:15
代码已经支持多人检测,但检测到多个人脸时绘制OSD后,OSD被快速刷新了,所以看起来不明显(仔细看还是能 ...
大佬厉害:congratulate:
-
回复了主题帖:
【回顾2024,展望2025】新年抢楼活动来啦!
2025年想自己搭建或者实现一套博客呀今年不要再鸽了:congratulate:
-
回复了主题帖:
以拆会友!看看2.3寸“大”电视和32寸的大彩电内里乾坤
拆得真仔细这个2.3寸大电视也是让我大开眼界了,现在基本见不到了:)
-
回复了主题帖:
【泰坦触觉 TITAN Core开发套件】资实收集以及开发环境搭建
这个触觉模块可有意思了:)
期待后续的创意
-
回复了主题帖:
【瓜分2500元红包】票选2024 DigiKey “感知万物,乐享生活”创意大赛人气作品TOP3!
我觉的LED立方体是这里面最有趣的项目:)
- 2025-01-13
-
回复了主题帖:
【 AI挑战营(进阶)】【复现】使用inspirefacesdk的单人实时视频流识别
wangerxian 发表于 2025-1-13 15:25
环境光强度背景之类的对这种是不是影响很大?
可以对视频做处理的,举个例子vivo手机相机逆光也清晰
送给算法判断的脸其实是人脸检测模型裁出来的人脸框,只要能检测到脸基本上都能出结果
-
发表了主题帖:
【 AI挑战营(进阶)】6.模型部署 2 使用windows原生通知实现的人员欢迎功能展示
【AI挑战营(进阶)】在RV1106部署InsightFace算法的多人实时人脸识别实战6 模型部署2 欢迎特定人员应用展示
RKNN RUNTIME 的 C API 提供了两套调用方式,一种是更直观的通用调用流程(rv1106似乎不支持),另一种是性能更好的零拷贝调用流程,通过Rknn-ToolKit的代码生成工具`rknn.codegen`,我们可以快速生成零拷贝的调用流程示例代码,示例代码包含了测试推理性能、保存推理结果、与预推理结果进行余弦相似度比对的功能。
零拷贝调用流程如下:
以mtcnn第三层onet为例,运行示例代码,可以看到推理性能、推理结果保存、余弦相似度比对的结果。
```shell
rknn_app_query_model_info: model input num: 1, output num: 3
INPUTS:
[NORMAL]: index=0, name=input, n_dims=4, dims=[1,48,48,3,], n_elems=6912, size=6912, w_stride=48, size_with_stride=6912, fmt=NHWC, type=INT8, qnt_type=AFFINE, zp=0, scale=0.007813
[NATIVE]: index=0, name=input, n_dims=4, dims=[1,48,48,3,], n_elems=6912, size=6912, w_stride=48, size_with_stride=6912, fmt=NHWC, type=INT8, qnt_type=AFFINE, zp=0, scale=0.007813
OUTPUTS:
[NORMAL]: index=0, name=output1, n_dims=2, dims=[1,10,], n_elems=10, size=10, w_stride=0, size_with_stride=10, fmt=UNDEFINED, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003805
[NATIVE]: index=0, name=output1, n_dims=2, dims=[1,10,], n_elems=10, size=64, w_stride=0, size_with_stride=64, fmt=UNDEFINED, type=INT8, qnt_type=AFFINE, zp=-128, scale=0.003805
[NORMAL]: index=1, name=output2, n_dims=2, dims=[1,4,], n_elems=4, size=4, w_stride=0, size_with_stride=4, fmt=UNDEFINED, type=INT8, qnt_type=AFFINE, zp=-17, scale=0.002566
[NATIVE]: index=1, name=output2, n_dims=2, dims=[1,4,], n_elems=4, size=64, w_stride=0, size_with_stride=64, fmt=UNDEFINED, type=INT8, qnt_type=AFFINE, zp=-17, scale=0.002566
[NORMAL]: index=2, name=output3, n_dims=2, dims=[1,2,], n_elems=2, size=4, w_stride=0, size_with_stride=4, fmt=UNDEFINED, type=FP16, qnt_type=AFFINE, zp=0, scale=0.000000
[NATIVE]: index=2, name=output3, n_dims=2, dims=[1,2,], n_elems=2, size=64, w_stride=0, size_with_stride=64, fmt=UNDEFINED, type=FP16, qnt_type=AFFINE, zp=0, scale=0.000000
LOAD INPUTS
input[0] - IMG: ./data/inputs/onet_dataset.txt_0.jpg
RUNNING RKNN
loop[0] time: 0.972000 ms
Average time: 1.049000 ms
CHECK OUTPUT
check all output to './data/outputs/runtime'
with golden data in './data/outputs/golden'
output[0] - output1:
rknn_layout_convert: src->fmt=UNDEFINED(1,10,), dst->fmt=UNDEFINED(1,10,)
layout unchanged, memcpy directly
rknn_dtype_convert: convert from INT8 to FP32
cosine similarity: 0.999994
output[1] - output2:
rknn_layout_convert: src->fmt=UNDEFINED(1,4,), dst->fmt=UNDEFINED(1,4,)
layout unchanged, memcpy directly
rknn_dtype_convert: convert from INT8 to FP32
cosine similarity: 0.999892
output[2] - output3:
rknn_layout_convert: src->fmt=UNDEFINED(1,2,), dst->fmt=UNDEFINED(1,2,)
layout unchanged, memcpy directly
rknn_dtype_convert: convert from FP16 to FP32
cosine similarity: 1.000000
```
可以看到,推理性能为1.049000 ms,推理结果保存在`./data/outputs/runtime`文件夹中,与预推理结果进行余弦相似度比对的结果为0.999994、0.999892、1.000000。
接下来,我们将`MTCNN`和`ArcFace`模型部署到`Rv1106`开发板上,实现人脸检测和人脸识别功能。
项目需要使用cmake进行编译,所以我们需要在项目根目录下的`CMakeLists.txt`文件,其中包括了交叉编译器的设置、头文件的引入、库文件的引入、源文件的引入、可执行文件的生成等,若要使用IDE配置项目,需要根据确定的交叉编译器和库文件重新进行配置。
> 使用IDE的语法检查和补全功能需要注意工具链的差异,比如rv1106工具链提供的gcc编译器支持的语法和标准可能与IDE提供的gcc编译器支持的语法和标准不同,这时IDE的语法检查和补全功能可能会出现问题。
#### 实现欢迎特定人员
由于我手头没有可以实现音频编解码的dsp模块,所以我无法使用嵌入式惯用的I/O和板间通信等方式去实现欢迎音频播放,所以这里我使用了一个简单的方式,将检测到的特定人员的结果写入到一个文件中,然后使用一个简单的脚本来检测文件中的内容,当检测到特定输出结果时,我的windows电脑会弹出一个欢迎的toast通知。
```shell
#!/bin/bash
# 设置结果文件路径
ResultFilePath="/tmp/status_face.txt"
# 检查是否传入参数
if [ $# -eq 0 ]; then
echo "错误: 未传入任何参数!请传入参数作为文件内容。"
exit 1
fi
# 将参数内容覆盖写入文件
echo "$*" > $ResultFilePath
# 打印成功消息
echo "参数已写入文件:$ResultFilePath"
echo "文件内容:"
cat $ResultFilePath
```
shell文件保存至`/oem/usr/bin/face_status.sh`,并且赋予执行权限,这样我们就可以通过`System("face_status.sh personname")`来将检测到的特定人员的结果写入到文件中。
```ps1
# 加载模块
Import-Module BurntToast
Import-Module Posh-SSH
# 配置 Linux 服务器信息
$LinuxServer = "172.32.0.93" # Linux服务器的IP地址
$Username = "root" # 用户名
$Password = "luckfox" # 密码(如使用密钥,请替换为-KeyFile 参数)
# 创建 SSH 会话
$SSHSession = New-SSHSession -ComputerName $LinuxServer -Credential (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, (ConvertTo-SecureString $Password -AsPlainText -Force))
$ResultFilePath = "/tmp/status_face.txt"
# 检测人脸结果
Function CheckFaceResult{
Param (
[Parameter(Mandatory=$true)]
[Object]$Session,
[string]$FilePath
)
# 检查文件是否存在
$CheckCommand = "if [ -f $FilePath ]; then cat $FilePath; else echo 'NOT_FOUND'; fi"
$Result = (Invoke-SSHCommand -SessionId $Session.SessionId -Command $CheckCommand).Output.Trim()
# 清除文件内容
$ClearCommand = "echo '' > $FilePath"
Invoke-SSHCommand -SessionId $Session.SessionId -Command $ClearCommand
# 返回标记状态
return $Result}
# 欢迎特定人员
Function WelcomeSpecificPerson(
[string]$PersonName
) {
# 发送通知
New-BurntToastNotification -Text "欢迎$PersonName" -Sound 'Alarm2' -SnoozeAndDismiss
}
# 循环监控
while ($true) {
# 打印运行信息
Write-Host "检测人脸结果..."
# 检测人脸结果
$Result = CheckFaceResult -Session $SSHSession -FilePath $ResultFilePath
# 检查是否检测到特定人员
if ($Result -eq "iex123") {
WelcomeSpecificPerson -PersonName "IEX123"
}
Start-Sleep -Seconds 1
}
# 断开 SSH 会话
Remove-SSHSession -SessionId $SSHSession.SessionId
```
在Windows电脑上运行这个脚本,当检测到特定人员时,Windows电脑会弹出一个欢迎的toast通知。
[localvideo]dbc5cf5961525c5e64c11759a1a3fc06[/localvideo]
- 2025-01-12
-
回复了主题帖:
【 AI挑战营(进阶)】【复现】使用inspirefacesdk的单人实时视频流识别
其实这个demo已经可以实现多人的实时识别了,但是似乎sdk调用部分似乎需要一些简单的修改来确保可以传出多个人脸检测的结果到接下来的识别和绘制流程中
-
发表了主题帖:
【 AI挑战营(进阶)】【复现】使用inspirefacesdk的单人实时视频流识别
【复现】展示检测一个人的模型给出的精度得分
`junnan007`大佬展示了使用inspiresdk进行单人脸实时视频识别的demo,鉴于本人水平有限,还不能根据适当的交叉编译工具链使用cmake管理项目,我们读一下他的代码并跑一下实现。
> 代码地址: [https://github.com/junanxia/rkface_insightface](https://github.com/junanxia/rkface_insightface)
# 1.`CMakeLists.txt`
`CMakelists.txt`文件是CMake工具的输入文件,用于描述项目的构建过程,包括编译选项、编译器、链接器等。在这个文件中,我们可以指定项目的名称、版本、源文件、头文件、链接库等信息。
- 项目名称:rkface_insight
- 交叉编译工具链:设置了 ARM 交叉编译工具链路径,指定了 C 和 C++ 编译器
- 包含目录:项目源目录下的多个 include 子目录
- 链接目录:项目源目录下的 lib 子目录
- 可执行文件及源文件:定义了可执行文件 rkface_insight 及其源文件(如 src/main.cc, src/luckfox_mpi.cc, src/font.h)
- 链接库:列出了需要链接的库文件,例如 InspireFace, rockit, rockchip_mpp, rtsp, rkaiq, opencv_imgproc 等等。
# 2. `main.cc`
`main.cc`是项目的主要源文件,包含了项目的主要逻辑。在这个文件中,我们可以看到项目的主要逻辑是:
- 初始化系统和库
- 加载模型和参考人脸特征
- 启动视频输入、编码和 RTSP 服务
- 创建线程处理视频流和人脸识别
- 在主循环中保持运行
```c++
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "rtsp_demo.h"
#include "luckfox_mpi.h"
#define DISP_WIDTH 1280 // 720
#define DISP_HEIGHT 1080 // 480
MPP_CHN_S stSrcChn, stSrcChn1, stvpssChn, stvencChn;
VENC_RECV_PIC_PARAM_S stRecvParam;
rtsp_demo_handle g_rtsplive = NULL;
rtsp_session_handle g_rtsp_session;
std::map g_feature_map;
HFSession g_face_session;
void LoadReferenceFace()
{
printf("LoadReferenceFace\n");
const char* face_path = "./faces";
DIR *dir = opendir(face_path);
if (dir == NULL) {
return;
}
struct dirent *entry;
int face_idx = 0;
char path[256] = {0};
char cstr[100] = {0};
int ret;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(".", entry->d_name) == 0 || strcmp("..", entry->d_name) == 0) {
continue;
}
sprintf(path, "%s/%s", face_path, entry->d_name);
printf("face feature name: %s\n", path);
cv::Mat image = cv::imread(path);
if (image.empty()) {
printf("Face Image is empty: %s\n", entry->d_name);
continue;
}
HFImageData imageData = {0};
imageData.data = image.data; // Pointer to the image data
imageData.format = HF_STREAM_BGR; // Image format (BGR in this case)
imageData.height = image.rows; // Image height
imageData.width = image.cols; // Image width
imageData.rotation = HF_CAMERA_ROTATION_0; // Image rotation
HFImageStream stream;
ret = HFCreateImageStream(&imageData, &stream); // Create an image stream for processing
if (ret != HSUCCEED) {
printf("Create stream error: %d\n", ret);
continue;
}
HFMultipleFaceData multipleFaceData = {0};
ret = HFExecuteFaceTrack(g_face_session, stream, &multipleFaceData); // Track faces in the image
if (ret != HSUCCEED) {
printf("Run face track error: %d\n", ret);
HFReleaseImageStream(stream);
continue;
}
if (multipleFaceData.detectedNum == 0) { // Check if any faces were detected
printf("No face was detected: %s, %d\n", entry->d_name, ret);
HFReleaseImageStream(stream);
continue;
}
HFFaceFeature feature = {0};
ret = HFFaceFeatureExtract(g_face_session, stream, multipleFaceData.tokens[0], &feature); // Extract features
if (ret != HSUCCEED) {
printf("Extract feature error: %d\n", ret);
HFReleaseImageStream(stream);
continue;
}
memcpy(cstr, entry->d_name, strlen(entry->d_name) - 4);
HFFaceFeatureIdentity identity = {0};
identity.feature = &feature; // Assign the extracted feature
//identity.customId = face_idx; // Custom identifier for the face
identity.id = face_idx;
// identity.tag = cstr; // Tag the feature with the name
int64_t result_id = 0;
ret = HFFeatureHubInsertFeature(identity, &result_id); // Insert the feature into the hub
if (ret != HSUCCEED) {
printf("Feature insertion into FeatureHub failed: %d\n", ret);
HFReleaseImageStream(stream);
continue;
}
g_feature_map.insert(std::make_pair(result_id, cstr));
printf("Feature insertion into FeatureHub id: %lld, %s\n", result_id, cstr);
ret = HFReleaseImageStream(stream);
if (ret != HSUCCEED) {
printf("Release image stream error: %lu\n", ret);
}
face_idx++;
}
closedir(dir);
HInt32 count;
ret = HFFeatureHubGetFaceCount(&count);
printf("Inserted data: %d\n", count);
printf("LoadReferenceFace finished, %d faces loaded.\n", count);
}
static void* GetMediaBuffer(void *arg) {
(void)arg;
printf("========%s========\n", __func__);
void *pData = RK_NULL;
int s32Ret;
VENC_STREAM_S stFrame;
stFrame.pstPack = (VENC_PACK_S *) malloc(sizeof(VENC_PACK_S));
while (1) {
s32Ret = RK_MPI_VENC_GetStream(0, &stFrame, -1);
if (s32Ret == RK_SUCCESS) {
if (g_rtsplive && g_rtsp_session) {
pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
rtsp_tx_video(g_rtsp_session, (uint8_t *)pData, stFrame.pstPack->u32Len, stFrame.pstPack->u64PTS);
rtsp_do_event(g_rtsplive);
}
s32Ret = RK_MPI_VENC_ReleaseStream(0, &stFrame);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("RK_MPI_VENC_ReleaseStream fail %x", s32Ret);
}
}
usleep(10 * 1000);
}
printf("\n======exit %s=======\n", __func__);
free(stFrame.pstPack);
return NULL;
}
static void *RetinaProcessBuffer(void *arg) {
(void)arg;
printf("========%s========\n", __func__);
int disp_width = DISP_WIDTH;
int disp_height = DISP_HEIGHT;
char text[16];
int sX,sY,eX,eY;
int s32Ret;
int group_count = 0;
VIDEO_FRAME_INFO_S stViFrame;
while(1)
{
s32Ret = RK_MPI_VI_GetChnFrame(0, 1, &stViFrame, -1);
if(s32Ret == RK_SUCCESS)
{
void *vi_data = RK_MPI_MB_Handle2VirAddr(stViFrame.stVFrame.pMbBlk);
if(vi_data != RK_NULL)
{
cv::Mat yuv420sp(disp_height + disp_height / 2, disp_width, CV_8UC1, vi_data);
cv::Mat bgr(disp_height, disp_width, CV_8UC3);
cv::cvtColor(yuv420sp, bgr, cv::COLOR_YUV420sp2BGR);
HFImageData imageData = {0};
imageData.data = bgr.data;
imageData.format = HF_STREAM_BGR;
imageData.height = bgr.rows;
imageData.width = bgr.cols;
imageData.rotation = HF_CAMERA_ROTATION_0;
HFImageStream stream;
HResult ret = HFCreateImageStream(&imageData, &stream);
if (ret != HSUCCEED) {
printf("Create stream error: %d\n", ret);
continue;
}
HFMultipleFaceData multipleFaceData = {0};
ret = HFExecuteFaceTrack(g_face_session, stream, &multipleFaceData);
if (ret != HSUCCEED) {
printf("Run face track error: %d\n", ret);
HFReleaseImageStream(stream);
continue;
}
for (int i = 0; i < multipleFaceData.detectedNum; i++) {
printf("Face Index: %d, conf: %f\n", i, multipleFaceData.detConfidence);
float face_conf = multipleFaceData.detConfidence;
if (face_conf < 0.5) {
continue;
}
sX = multipleFaceData.rects.x;
sY = multipleFaceData.rects.y;
eX = sX + multipleFaceData.rects.width;
eY = sY + multipleFaceData.rects.height;
sX = sX - (sX % 2);
sY = sY - (sY % 2);
eX = eX - (eX % 2);
eY = eY - (eY % 2);
test_rgn_overlay_line_process(sX, sY, 0, group_count);
test_rgn_overlay_line_process(eX, sY, 1, group_count);
test_rgn_overlay_line_process(eX, eY, 2, group_count);
test_rgn_overlay_line_process(sX, eY, 3, group_count);
// Initialize the feature structure to store extracted face features
HFFaceFeature feature = {0};
// // Extract facial features from the detected face using the first token
ret = HFFaceFeatureExtract(g_face_session, stream, multipleFaceData.tokens, &feature);
if (ret != HSUCCEED) {
printf("Extract feature error: %d\n", ret); // Print error if extraction fails
continue;
}
HFFaceFeatureIdentity searched = {0};
HFloat confidence;
ret = HFFeatureHubFaceSearch(feature, &confidence, &searched);
if (ret != HSUCCEED) {
printf("Search face feature error: %d\n", ret); // Print error if search fails
continue;
}
printf("Searched size: %d\n", searched.id);
if (searched.id != -1) {
sprintf(text, "%s(%.2f)", g_feature_map[searched.id].c_str(), confidence);
test_rgn_overlay_text_process(sX + 4, sY + 4, text, group_count, 1);
printf("Found similar face: id= %lld, tag=%s, confidence=%f\n", searched.id, g_feature_map[searched.id].c_str(), confidence);
// TODO: Open the door, insert record to database.
} else {
printf("Not Found similar\n");
sprintf(text, "%s", "Unregistered.");
test_rgn_overlay_text_process(sX + 4, sY + 4, text, group_count, 0);
}
group_count++;
}
ret = HFReleaseImageStream(stream);
if (ret != HSUCCEED) {
printf("Release stream error: %d\n", ret);
}
}
s32Ret = RK_MPI_VI_ReleaseChnFrame(0, 1, &stViFrame);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("RK_MPI_VI_ReleaseChnFrame fail %x", s32Ret);
}
}
else{
printf("Get viframe error %d !\n", s32Ret);
continue;
}
usleep(500000);
for(int i = 0;i < group_count; i++) {
rgn_overlay_release(i);
}
group_count = 0;
}
return NULL;
}
int main(int argc, char* argv[])
{
system("RkLunch-stop.sh");
RK_S32 s32Ret = 0;
int width = DISP_WIDTH;
int height = DISP_HEIGHT;
const char *model_path = "./model/Gundam_RV1106";
HResult ret = HFLaunchInspireFace(model_path);
if (ret != HSUCCEED) {
printf("Load Model error: %d\n", ret);
return ret;
}
HFFeatureHubConfiguration featureHubConfiguration;
featureHubConfiguration.primaryKeyMode = HF_PK_AUTO_INCREMENT; // featureBlockNum = 10; // Number of feature blocks
featureHubConfiguration.enablePersistence = 0; // Persistence not enabled, use in-memory database
featureHubConfiguration.persistenceDbPath = "feature.db"; // Database path (not used here)
featureHubConfiguration.searchMode = HF_SEARCH_MODE_EAGER; // Search mode configuration
featureHubConfiguration.searchThreshold = 0.48f; // Threshold for search operations
// Enable the global feature database
ret = HFFeatureHubDataEnable(featureHubConfiguration);
if (ret != HSUCCEED) {
printf("An exception occurred while starting FeatureHub: %d\n", ret);
return ret;
}
HOption option = HF_ENABLE_FACE_RECOGNITION;
ret = HFCreateInspireFaceSessionOptional(option, HF_DETECT_MODE_ALWAYS_DETECT, 20, 160, -1, &g_face_session);
if (ret != HSUCCEED) {
printf("Create session error: %d\n", ret);
return ret;
}
HFSessionSetTrackPreviewSize(g_face_session, 160);
HFSessionSetFilterMinimumFacePixelSize(g_face_session, 4);
LoadReferenceFace();
// rkaiq init
RK_BOOL multi_sensor = RK_FALSE;
const char *iq_dir = "/etc/iqfiles";
rk_aiq_working_mode_t hdr_mode = RK_AIQ_WORKING_MODE_NORMAL;
//hdr_mode = RK_AIQ_WORKING_MODE_ISP_HDR2;
SAMPLE_COMM_ISP_Init(0, hdr_mode, multi_sensor, iq_dir);
SAMPLE_COMM_ISP_Run(0);
// rkmpi init
if (RK_MPI_SYS_Init() != RK_SUCCESS) {
RK_LOGE("rk mpi sys init fail!");
return -1;
}
// rtsp init
g_rtsplive = create_rtsp_demo(554);
g_rtsp_session = rtsp_new_session(g_rtsplive, "/live/0");
rtsp_set_video(g_rtsp_session, RTSP_CODEC_ID_VIDEO_H264, NULL, 0);
rtsp_sync_video_ts(g_rtsp_session, rtsp_get_reltime(), rtsp_get_ntptime());
// vi init
vi_dev_init();
vi_chn_init(0, width, height);
vi_chn_init(1, width, height);
// venc init
RK_CODEC_ID_E enCodecType = RK_VIDEO_ID_AVC;
venc_init(0, width, height, enCodecType);
// bind vi to venc
stSrcChn.enModId = RK_ID_VI;
stSrcChn.s32DevId = 0;
stSrcChn.s32ChnId = 0;
stvencChn.enModId = RK_ID_VENC;
stvencChn.s32DevId = 0;
stvencChn.s32ChnId = 0;
printf("====RK_MPI_SYS_Bind vi0 to venc0====\n");
s32Ret = RK_MPI_SYS_Bind(&stSrcChn, &stvencChn);
if (s32Ret != RK_SUCCESS) {
RK_LOGE("bind 1 ch venc failed");
return -1;
}
printf("init success\n");
pthread_t main_thread;
pthread_create(&main_thread, NULL, GetMediaBuffer, NULL);
pthread_t retina_thread;
pthread_create(&retina_thread, NULL, RetinaProcessBuffer, NULL);
while (1) {
usleep(50000);
}
pthread_join(main_thread, NULL);
pthread_join(retina_thread, NULL);
RK_MPI_SYS_UnBind(&stSrcChn, &stvencChn);
RK_MPI_VI_DisableChn(0, 0);
RK_MPI_VI_DisableChn(0, 1);
RK_MPI_VENC_StopRecvFrame(0);
RK_MPI_VENC_DestroyChn(0);
RK_MPI_VI_DisableDev(0);
RK_MPI_SYS_Exit();
// Stop RKAIQ
SAMPLE_COMM_ISP_Stop(0);
// Release session
HFReleaseInspireFaceSession(g_face_session);
HFTerminateInspireFace();
return 0;
}
```
函数主要功能如下:
| 函数名 | 功能 | 介绍
| --- | --- | --- |
| LoadReferenceFace | 加载参考人脸 | 从指定目录加载人脸图像,进行人脸特征提取,并将其存入特征库 |
| GetMediaBuffer | 获取媒体缓冲区 | 从视频编码器获取视频流数据,并通过 RTSP 传输 |
| RetinaProcessBuffer | 人脸识别处理 | 从视频输入设备获取帧数据,进行人脸检测和特征提取,并与参考特征进行匹配 |
| main | 主函数 | 初始化系统、加载模型、配置特征库、创建人脸识别会话、启动 RTSP 服务、初始化视频输入和编码,创建处理线程,并在主线程中保持运行,直到结束 |
整个程序的流程与官方wiki的双线程OSD流程图一致,只是在人脸识别处理部分使用了inspiresdk提供的接口。
# 执行效果
```shell
Found similar face: id= 4, tag=iex123_31, confidence=0.680341
```
余弦相似度基本在0.6-0.7之间,这个值可以作为一个阈值,用于判断是否为同一个人。
[localvideo]e277a4633fe78ba40da54356ba716b59[/localvideo]
-
发表了主题帖:
【 AI挑战营(进阶)】5.模型部署 1 rknn推理测试
本帖最后由 iexplore123 于 2025-1-12 04:26 编辑
【AI挑战营(进阶)】在RV1106部署InsightFace算法的多人实时人脸识别实战5 模型部署1 rknn推理测试
### 3.1 板上环境准备
`RV1106` 是一款专门用于人工智能相关应用的高度集成 IPC 视觉处理器 SoC。它基于`单核` ARM Cortex-A7 `32` 位内核,集成了 NEON 和 FPU,并内置 NPU 支持 `INT4 / INT8 / INT16` 混合运算,RV1106G3计算能力高达 1TOPS。
#### 1. 构建板上系统(`buildroot`)
`buildroot` 是一个简单的、高效的、易于定制的嵌入式 Linux 系统构建框架。`LuckFoxPico-SDK` 提供了快速构建 `buildroot` 系统的命令行界面工具,可以帮助用户快速构建 `buildroot` 系统,构建流程如下:
```shell
luckfox@luckfox:~/luckfox-pico$ ./build.sh lunch
You're building on Linux
Lunch menu...pick the Luckfox Pico hardware version:
选择 Luckfox Pico 硬件版本:
[0] RV1103_Luckfox_Pico
[1] RV1103_Luckfox_Pico_Mini_A
[2] RV1103_Luckfox_Pico_Mini_B
[3] RV1103_Luckfox_Pico_Plus
[4] RV1106_Luckfox_Pico_Pro_Max
[5] RV1106_Luckfox_Pico_Ultra
[6] RV1106_Luckfox_Pico_Ultra_W
[7] custom
Which would you like? [0~7][default:0]: 3
Lunch menu...pick the boot medium:
选择启动媒介:
[0] SD_CARD
[1] SPI_NAND
Which would you like? [0~1][default:0]: 1
Lunch menu...pick the system version:
选择系统版本:
[0] Buildroot(Support Rockchip official features)
Which would you like? [0~1][default:0]: 0
[build.sh:info] Lunching for Default BoardConfig_IPC/BoardConfig-SPI_NAND-Buildroot-RV1103_Luckfox_Pico_Plus-IPC.mk boards...
[build.sh:info] Running build_select_board succeeded.
luckfox@luckfox:~/luckfox-pico$ ./build.sh
```
> 快速克隆仓库:`git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git`
获取`LuckFoxPico-SDK` 仓库后,我们首先需要配置rv1106专属的32位交叉编译器,命令如下:
```shell
cd tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/
source env_install_toolchain.sh
```
> 该交叉编译器在后续的板上应用开发中也会用到。
获取到编译完成的`buildroot` 系统后,就可以根据[官方说明](https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-RV1106/Luckfox-Pico-Pro-Max/Luckfox-Pico-SD-Card-burn-image)利用官方工具进行烧录了。
烧录工具:[SocToolkit](https://files.luckfox.com/wiki/Luckfox-Pico/Software/SocToolKit_v1.98_20240705_01_win.zip)
官方提供的预构建固件:[百度云](https://pan.baidu.com/s/1Mhf5JMpkFuZo_TuaGSxBYg?pwd=2sf8)
> 官方提供的luckfox pico max固件镜像中的平台变量是`rv1103`的,若使用该镜像,连扳推理时需要将`target`参数设置为`rv1103`。
##### 2. 配置系统环境 (`librknnmrt.so`)
由于`rv1106`是单核32位处理器,而官方未提供32位可运行的`rknn-toolkitlit2`,所以我们无法在板上运行`rknn-toolkit2`,也无法在板上运行`rknn`的`python api`,只能使用c++的`rknn`的api进行推理。
在板上系统中,我们确认rknn相关的库版本与rknn-toolkit2的版本一致,然后将`librknnmrt.so`拷贝到`/oem/usr/lib`目录下,将`rknn-server`拷贝到`/oem/usr/bin`目录下,给与可执行权限,然后启动`rknn-server`。具体参考`rknn-toolkit2`的文档。
### 3.2. rknn模型推理测试
`Rknn-Toolkit` 提供了一个`rknn.api.RKNN`类,我们可以使用这个类实现`rknn`模型的转换、推理、性能评估等功能。`RKNN`类的主要功能如下:
1. 模型转换:支持将PyTorch、ONNX、TensorFlow、TensorFlow Lite、Caffe、DarkNet等模型转为RKNN模型。
2. 量化功能:支持将浮点模型量化为定点模型,并支持混合量化。
3. 模型推理:将RKNN模型分发到指定的NPU设备上进行推理并获取推理结果;或在计算机上仿真NPU运行RKNN模型并获取推理结果。
4. 性能和内存评估:将RKNN模型分发到指定NPU设备上运行,以评估模型在实际设备上运行时的性能和内存占用情况。
5. 量化精度分析:该功能将给出模型量化后每一层推理结果与浮点模型推理结果的余弦距离和欧氏距离,以便于分析量化误差是如何出现的,为提高量化模型的精度提供思路。
6. 模型加密功能:使用指定的加密等级将RKNN模型整体加密。
> 以上功能仅供参考,具体需要查看芯片的支持情况。
#### 3.2.1 MTCNN
`MTCNN`是一个三层级联的人脸检测模型,它可以检测出图像中的人脸位置和五个关键点位置。
各网络的输入输出如下:
|网络层级|输入|---|输出|---|
|---|---|---|---|---|
|---|img|offsets|probs|landmarks|
|PNet|(N, 3, H, W)|(N, 4, H, W)|(N, 2, H, W)|/|
|RNet|(N, 3, 24, 24)|(N, 4)|(N, 2)|/|
|ONet|(N, 3, 48, 48)|(N, 4)|(N, 2)|(N, 10)|
> N 为 batch_size,H 为高度,W 为宽度。
> Rv1106的单核芯片大概只支持batch_size为1的推理。
模型转换的流程如下:
导出模型后,我们可以根据需要调用推理接口进行模拟推理或者连板推理并进行模型的性能评估。
```python
import math
import os
import urllib
import traceback
import time
import sys
import numpy as np
import cv2
from rknn.api import RKNN
from onnxruntime import InferenceSession
Pnet_model = '/root/Mtcnn&Arcface/mtcnn/pnet.onnx'
Rnet_model = '/root/Mtcnn&Arcface/mtcnn/rnet.onnx'
Onnx_model = '/root/Mtcnn&Arcface/mtcnn/onet.onnx'
# 输出的RKNN模型路径
P_NET_RKNN = 'pnet.rknn'
R_NET_RKNN = 'rnet.rknn'
O_NET_RKNN = 'onet.rknn'
IMG_PATH = '/root/Mtcnn&Arcface/mtcnn/office2.jpg'
DATASET = './dataset.txt'
DATASET_PREFIX = '/root/Mtcnn&Arcface/mtcnn/dataset/'
GENCODE = True
QUANTIZE_ON = True
# if this value is too low the algorithm will use a lot of memory
min_face_size = 15.0
# for probabilities
thresholds = [0.6, 0.7, 0.8]
# for NMS
nms_thresholds=[0.5, 0.7, 0.7]
def build_image_pyramid(img, min_face_size):
h, w, _ = img.shape
minl = min(h, w)
m = 12.0 / min_face_size
minl *= m
# generate the image pyramid
scale_list = []
factor = 0.707
factor_count = 0
while minl > 12:
scale_list.append(m*factor**factor_count)
minl *= factor
factor_count += 1
return scale_list
def convert_to_rknn(rknn, onnx_model, rknn_model, dataset, input_size_list=None):
# 配置模型的预处理参数
rknn.config(mean_values=[[127.5, 127.5, 127.5]], std_values=[[128.0, 128.0, 128.0]], target_platform='rv1103')
# 加载ONNX模型
print(f'--> Loading model: {onnx_model}')
ret = rknn.load_onnx(model=onnx_model, inputs=['input'], input_size_list=input_size_list)
if ret != 0:
print('Load model failed!')
exit(ret)
print('done')
# 构建RKNN模型(进行量化,如果需要)
print('--> Building model')
ret = rknn.build(do_quantization=QUANTIZE_ON, dataset=dataset)
if ret != 0:
print('Build model failed!')
exit(ret)
print('done')
# 导出RKNN模型
print(f'--> Exporting RKNN model to: {rknn_model}')
ret = rknn.export_rknn(rknn_model)
if ret != 0:
print('Export RKNN model failed!')
exit(ret)
print('done')
def _generate_bboxes(probs, offsets, scale, threshold):
"""Generate bounding boxes at places
where there is probably a face.
Arguments:
probs: a float numpy array of shape [n, m].
offsets: a float numpy array of shape [1, 4, n, m].
scale: a float number,
width and height of the image were scaled by this number.
threshold: a float number.
Returns:
a float numpy array of shape [n_boxes, 9]
"""
stride = 2
cell_size = 12
# indices of boxes where there is probably a face
inds = np.where(probs > threshold)
if inds[0].size == 0:
return np.array([])
# transformations of bounding boxes
tx1, ty1, tx2, ty2 = [offsets[0, i, inds[0], inds[1]] for i in range(4)]
offsets = np.array([tx1, ty1, tx2, ty2])
score = probs[inds[0], inds[1]]
# P-Net is applied to scaled images
# so we need to rescale bounding boxes back
bounding_boxes = np.vstack([
np.round((stride*inds[1] + 1.0)/scale),
np.round((stride*inds[0] + 1.0)/scale),
np.round((stride*inds[1] + 1.0 + cell_size)/scale),
np.round((stride*inds[0] + 1.0 + cell_size)/scale),
score, offsets
])
return bounding_boxes.T
def run_first_stage(image, scale, threshold):
# 将图像调整为网络输入大小
height, width, _ = image.shape
new_height = math.ceil(height * scale)
new_width = math.ceil(width * scale)
img_resized = cv2.resize(image, (new_width, new_height))
img_resized = np.asarray(img_resized, 'float32')
# img_resized = (img_resized - 127.5) / 128.0
img_resized = np.expand_dims(img_resized, 0)
img_resized = img_resized.transpose((0, 3, 1, 2))
# 进行推理
# rknn的模型需要固定的输入尺寸,所以对每个缩放尺寸的图像都需要单独的RKNN模型
Pnet = RKNN()
scale_dataset_path = f'{DATASET_PREFIX}dataset_scale_{scale}.txt'
scale_rknn_model_path = f'pnet_{new_width}x{new_height}.rknn'
scale_input_size_list = [[1, 3, math.ceil(img.shape[0]*scale), math.ceil(img.shape[1]*scale)]]
convert_rknn_and_init(Pnet, Pnet_model, scale_rknn_model_path, scale_dataset_path, scale_input_size_list)
print('--> Running PNet for scale:', scale)
outputs = Pnet.inference(inputs=[img_resized], data_format='nchw')
probs = outputs[1][0, 1, :, :]
offsets = outputs[0]
Pnet.release()
# # 使用onnxruntime进行推理
# session = InferenceSession(Pnet_model)
# outputs = session.run(None, {'input': img_resized})
# probs = outputs[1][0, 1, :, :]
# offsets = outputs[0]
# 生成边界框
boxes = _generate_bboxes(probs, offsets, scale, threshold)
if len(boxes) == 0:
return None
return boxes
def get_image_boxes(bounding_boxes, img, size=24):
"""Cut out boxes from the image.
Arguments:
bounding_boxes: a float numpy array of shape [n, 5].
img: an instance of PIL.Image.
size: an integer, size of cutouts.
Returns:
a float numpy array of shape [n, 3, size, size].
"""
num_boxes = len(bounding_boxes)
height, width, _ = img.shape
[dy, edy, dx, edx, y, ey, x, ex, w, h] = correct_bboxes(bounding_boxes, width, height)
img_boxes = np.zeros((num_boxes, size, size, 3), 'float32')
for i in range(num_boxes):
img_box = np.zeros((h, w, 3), 'uint8')
img_box[dy:(edy + 1), dx:(edx + 1), :] = img[y:(ey + 1), x:(ex + 1), :]
# resize
img_box = cv2.resize(img_box, (size, size))
img_boxes[i, :, :, :] = img_box
return img_boxes
def nms(boxes, overlap_threshold=0.5, mode='union'):
"""Non-maximum suppression.
Arguments:
boxes: a float numpy array of shape [n, 5],
where each row is (xmin, ymin, xmax, ymax, score).
overlap_threshold: a float number.
mode: 'union' or 'min'.
Returns:
list with indices of the selected boxes
"""
if len(boxes) == 0:
return []
pick = []
x1, y1, x2, y2, score = [boxes[:, i] for i in range(5)]
area = (x2 - x1 + 1.0)*(y2 - y1 + 1.0)
ids = np.argsort(score)
while len(ids) > 0:
last = len(ids) - 1
i = ids[last]
pick.append(i)
ix1 = np.maximum(x1, x1[ids[:last]])
iy1 = np.maximum(y1, y1[ids[:last]])
ix2 = np.minimum(x2, x2[ids[:last]])
iy2 = np.minimum(y2, y2[ids[:last]])
w = np.maximum(0.0, ix2 - ix1 + 1.0)
h = np.maximum(0.0, iy2 - iy1 + 1.0)
inter = w * h
if mode == 'min':
overlap = inter/np.minimum(area, area[ids[:last]])
elif mode == 'union':
overlap = inter/(area + area[ids[:last]] - inter)
ids = np.delete(
ids,
np.concatenate([[last], np.where(overlap > overlap_threshold)[0]])
)
return pick
def calibrate_box(bboxes, offsets):
"""Transform bounding boxes to be more like true bounding boxes.
'offsets' is one of the outputs of the nets.
Arguments:
bboxes: a float numpy array of shape [n, 5].
offsets: a float numpy array of shape [n, 4].
Returns:
a float numpy array of shape [n, 5].
"""
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
w = x2 - x1 + 1.0
h = y2 - y1 + 1.0
w = np.expand_dims(w, 1)
h = np.expand_dims(h, 1)
# this is what happening here:
# tx1, ty1, tx2, ty2 = [offsets[:, i] for i in range(4)]
# x1_true = x1 + tx1*w
# y1_true = y1 + ty1*h
# x2_true = x2 + tx2*w
# y2_true = y2 + ty2*h
# below is just more compact form of this
translation = np.hstack([w, h, w, h])*offsets
bboxes[:, 0:4] = bboxes[:, 0:4] + translation
return bboxes
def convert_to_square(bboxes):
"""Convert bounding boxes to a square form.
Arguments:
bboxes: a float numpy array of shape [n, 5].
Returns:
a float numpy array of shape [n, 5],
squared bounding boxes.
"""
square_bboxes = np.zeros_like(bboxes)
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
h = y2 - y1 + 1.0
w = x2 - x1 + 1.0
max_side = np.maximum(h, w)
square_bboxes[:, 0] = x1 + w*0.5 - max_side*0.5
square_bboxes[:, 1] = y1 + h*0.5 - max_side*0.5
square_bboxes[:, 2] = square_bboxes[:, 0] + max_side - 1.0
square_bboxes[:, 3] = square_bboxes[:, 1] + max_side - 1.0
return square_bboxes
def correct_bboxes(bboxes, width, height):
"""Crop boxes that are too big and get coordinates
with respect to cutouts.
Arguments:
bboxes: a float numpy array of shape [n, 5],
where each row is (xmin, ymin, xmax, ymax, score).
width: a float number.
height: a float number.
Returns:
dy, dx, edy, edx: a int numpy arrays of shape [n],
coordinates of the boxes with respect to the cutouts.
y, x, ey, ex: a int numpy arrays of shape [n],
corrected ymin, xmin, ymax, xmax.
h, w: a int numpy arrays of shape [n],
just heights and widths of boxes.
in the following order:
[dy, edy, dx, edx, y, ey, x, ex, w, h].
"""
x1, y1, x2, y2 = [bboxes[:, i] for i in range(4)]
w, h = x2 - x1 + 1.0, y2 - y1 + 1.0
num_boxes = bboxes.shape[0]
x, y, ex, ey = x1, y1, x2, y2
dx, dy = np.zeros((num_boxes,)), np.zeros((num_boxes,))
edx, edy = w.copy() - 1.0, h.copy() - 1.0
ind = np.where(ex > width - 1.0)[0]
edx[ind] = w[ind] + width - 2.0 - ex[ind]
ex[ind] = width - 1.0
ind = np.where(ey > height - 1.0)[0]
edy[ind] = h[ind] + height - 2.0 - ey[ind]
ey[ind] = height - 1.0
ind = np.where(x < 0.0)[0]
dx[ind] = 0.0 - x[ind]
x[ind] = 0.0
ind = np.where(y < 0.0)[0]
dy[ind] = 0.0 - y[ind]
y[ind] = 0.0
return_list = [dy, edy, dx, edx, y, ey, x, ex, w, h]
return_list = [i.astype('int32') for i in return_list]
return return_list
def draw(image, boxes, landmarks):
img_copy = image.copy()
# 绘制边界框和关键点
for box in boxes:
x1, y1, x2, y2 = box[:4].astype(int)
cv2.rectangle(img_copy, (x1, y1), (x2, y2), (255, 0, 0), 2)
for landmark in landmarks:
for i in range(5):
cv2.circle(img_copy, (int(landmark), int(landmark[i + 5])), 2, (0, 255, 0), -1)
img_rgb = cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB)
return img_rgb
def convert_rknn_and_init(rknn, onnx_model, rknn_model, dataset, input_size_list=None,gen_code=GENCODE):
convert_to_rknn(rknn, onnx_model, rknn_model, dataset, input_size_list)
if gen_code:
with open(dataset, 'r') as f:
inputs = f.readlines()
input_list = [i.strip() for i in inputs]
input = input_list[0]
ret = rknn.codegen(output_path=f'./rknn_app_{rknn_model}', inputs = input, overwrite=True)
if ret != 0:
print(f'Init runtime environment for {rknn_model} failed!')
exit(ret)
print(f'Init runtime environment for {rknn_model} done')
ret = rknn.init_runtime()
if ret != 0:
print(f'Init runtime environment for {rknn_model} failed!')
exit(ret)
print(f'Init runtime environment for {rknn_model} done')
def generate_dataset(bounding_boxes, img, size, dataset_filename, dataset_prefix):
dataset_path = dataset_prefix + dataset_filename
with open(dataset_path, 'w') as f:
for i, img_box in enumerate(get_image_boxes(bounding_boxes, img, size=size)):
img_path = f'{dataset_prefix}{dataset_filename}_{i}.jpg'
img_box = cv2.cvtColor(img_box, cv2.COLOR_BGR2RGB)
cv2.imwrite(img_path, img_box)
f.write(f'{img_path}\n')
return dataset_path
def generate_pnet_dataset_for_each_scale(dataset_filename, dataset_prefix, scale_list):
# 生成PNet量化每个缩放尺寸的数据集
with open(dataset_filename, 'r') as dataset_file:
image_paths = dataset_file.readlines()
for scale in scale_list:
scale_dataset_filename = f'{dataset_prefix}dataset_scale_{scale}.txt'
with open(scale_dataset_filename, 'w') as scale_dataset_file:
for image_path in image_paths:
image_path = image_path.strip()
img = cv2.imread(image_path)
if img is None:
continue
h = math.ceil(img.shape[0] * scale)
w = math.ceil(img.shape[1] * scale)
img_resized = cv2.resize(img, (w, h))
resized_image_path = f'{dataset_prefix}img_{w}x{h}.jpg'
cv2.imwrite(resized_image_path, img_resized)
scale_dataset_file.write(f'{resized_image_path}\n')
return True
if __name__ == '__main__':
# 创建RKNN对象
rknn_rnet = RKNN(verbose=False)
rknn_onet = RKNN(verbose=False)
# 加载图像, 构建图像金字塔
print('--> Load image')
img = cv2.imread(IMG_PATH)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
scale_list = build_image_pyramid(img, min_face_size)
print('scales:', ['{:.2f}'.format(s) for s in scale_list])
print('number of different scales:', len(scale_list))
input_size_list_Pnet = [[1, 3, math.ceil(img.shape[0]*s), math.ceil(img.shape[1]*s)] for s in scale_list]
print('input_size_list_Pnet:', input_size_list_Pnet)
# 生成PNet量化数据集
# generate_pnet_dataset_for_each_scale(DATASET, DATASET_PREFIX, scale_list)
for scale in scale_list:
scale_dataset_filename = f'{DATASET_PREFIX}dataset_scale_{scale}.txt'
with open(scale_dataset_filename, 'w') as scale_dataset_file:
h = math.ceil(img.shape[0] * scale)
w = math.ceil(img.shape[1] * scale)
img_resized = cv2.resize(img, (w, h))
resized_image_path = f'{DATASET_PREFIX}img_{w}x{h}.jpg'
cv2.imwrite(resized_image_path, img_resized)
scale_dataset_file.write(f'{resized_image_path}\n')
# PNet阶段
print('--> Running PNet')
bounding_boxes = []
for scale in scale_list:
boxes = run_first_stage(img, scale, thresholds[0])
bounding_boxes.append(boxes)
bounding_boxes = [i for i in bounding_boxes if i is not None]
if len(bounding_boxes) == 0:
print('No bounding boxes found.')
exit(1)
bounding_boxes = np.vstack(bounding_boxes)
print('number of bounding boxes:', len(bounding_boxes))
img_drawn = draw(img, bounding_boxes, [])
cv2.imwrite('result_pnet.jpg', img_drawn)
print('Save results to pnet_result.jpg!')
print('--> NMS and calibrate bounding boxes')
# NMS + 校准
keep = nms(bounding_boxes[:, 0:5], nms_thresholds[0])
bounding_boxes = bounding_boxes[keep]
# 使用PNet预测的偏移量来校准边界框
bounding_boxes = calibrate_box(bounding_boxes[:, 0:5], bounding_boxes[:, 5:])
bounding_boxes = convert_to_square(bounding_boxes)
bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
print('number of bounding boxes:', len(bounding_boxes))
img_drawn = draw(img, bounding_boxes, [])
cv2.imwrite('result_pnet_1.jpg', img_drawn)
# rknn_pnet.release()
# 生成RNet量化数据集
rnet_dataset = generate_dataset(bounding_boxes, img, 24, 'rnet_dataset.txt', DATASET_PREFIX)
# 初始化并转换RNet模型
convert_rknn_and_init(rknn_rnet, Rnet_model, R_NET_RKNN, rnet_dataset, input_size_list=[[1, 3, 24, 24]])
# RNet阶段
print('--> Running RNet')
img_boxes = get_image_boxes(bounding_boxes, img, size=24)
# 将img_boxes拆分为RKNN输入格式(1, 3, 24, 24)
img_boxes_split = [img_boxes[i:i+1] for i in range(len(img_boxes))]
output = [rknn_rnet.inference(inputs=[img_box]) for img_box in img_boxes_split]
offsets = np.vstack([out[0] for out in output])
probs = np.vstack([out[1] for out in output])
keep = np.where(probs[:, 1] > thresholds[1])[0]
bounding_boxes = bounding_boxes[keep]
bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
offsets = offsets[keep]
print('number of bounding boxes:', len(bounding_boxes))
img_drawn = draw(img, bounding_boxes, [])
cv2.imwrite('result_rnet.jpg', img_drawn)
print('Save results to result_rnet.jpg!')
print('--> NMS and calibrate bounding boxes')
# NMS + 校准
keep = nms(bounding_boxes, nms_thresholds[1])
bounding_boxes = bounding_boxes[keep]
bounding_boxes = calibrate_box(bounding_boxes, offsets[keep])
bounding_boxes = convert_to_square(bounding_boxes)
bounding_boxes[:, 0:4] = np.round(bounding_boxes[:, 0:4])
print('number of bounding boxes:', len(bounding_boxes))
img_drawn = draw(img, bounding_boxes, [])
cv2.imwrite('result_rnet_1.jpg', img_drawn)
rknn_rnet.release()
# 生成ONet量化数据集
onet_dataset = generate_dataset(bounding_boxes, img, 48, 'onet_dataset.txt', DATASET_PREFIX)
# 初始化并转换ONet模型
convert_rknn_and_init(rknn_onet, Onnx_model, O_NET_RKNN, onet_dataset, input_size_list=[[1, 3, 48, 48]])
# ONet阶段
print('--> Running ONet')
img_boxes = get_image_boxes(bounding_boxes, img, size=48)
# 将img_boxes拆分为RKNN输入格式(1, 3, 48, 48)
img_boxes_split = [img_boxes[i:i+1] for i in range(len(img_boxes))]
output = [rknn_onet.inference(inputs=[img_box]) for img_box in img_boxes_split]
probs = np.vstack([out[2] for out in output])
offsets = np.vstack([out[1] for out in output])
landmarks = np.vstack([out[0] for out in output])
keep = np.where(probs[:, 1] > thresholds[2])[0]
bounding_boxes = bounding_boxes[keep]
bounding_boxes[:, 4] = probs[keep, 1].reshape((-1,))
offsets = offsets[keep]
landmarks = landmarks[keep]
# 计算关键点
width = bounding_boxes[:, 2] - bounding_boxes[:, 0] + 1.0
height = bounding_boxes[:, 3] - bounding_boxes[:, 1] + 1.0
xmin, ymin = bounding_boxes[:, 0], bounding_boxes[:, 1]
landmarks[:, 0:5] = np.expand_dims(xmin, 1) + np.expand_dims(width, 1) * landmarks[:, 0:5]
landmarks[:, 5:10] = np.expand_dims(ymin, 1) + np.expand_dims(height, 1) * landmarks[:, 5:10]
print('number of bounding boxes:', len(bounding_boxes))
img_drawn = draw(img, bounding_boxes, landmarks)
cv2.imwrite('result_onet.jpg', img_drawn)
print('Save results to result_onet.jpg!')
# NMS + 校准
bounding_boxes = calibrate_box(bounding_boxes, offsets)
keep = nms(bounding_boxes, nms_thresholds[2], mode='min')
bounding_boxes = bounding_boxes[keep]
landmarks = landmarks[keep]
print('number of bounding boxes:', len(bounding_boxes))
# 绘制结果
img_drawn = draw(img, bounding_boxes, landmarks)
cv2.imwrite('result.jpg', img_drawn)
print('Save results to result.jpg!')
rknn_onet.release()
```
代码分为两部分,第一部分是将`MTCNN`模型转换为`RKNN`模型,第二部分是使用`RKNN`模型进行推理。
第一部分转换模型最核心的部分是`rknn.config()`和`rknn.build()`,`rknn.config()`用于配置模型的预处理参数,`rknn.build()`用于构建RKNN模型。
- config已经包含了模型的输入处理的归一化过程,所以在推理时不需要再对输入图像进行归一化处理。
- build函数中的do_quantization参数用于指定是否进行量化,dataset参数用于指定量化数据集,rv1106不支持非量化的模型,所以这里一定需要进行量化。
- 关于量化数据集,我将推理使用的图像以及得到的结果框预处理后保存到了文件中,然后将文件的路径保存到了dataset.txt文件中,并且将这个文件作为量化数据集传入到build函数中。
> 我使用的onnx模型本质上是一个caffe模型,所以输入的图像色彩格式是BGR,后续应用部署需要注意。
第二部分推理首先输入图像,然后将图像进行金字塔缩放,然后使用`PNet`模型进行推理,得到边界框,然后使用`NMS`和校准边界框,然后将校准后的边界框作为`RNet`的输入,得到更准确的边界框,然后再次使用`NMS`和校准边界框,最后将校准后的边界框作为`ONet`的输入,得到最终的边界框和关键点。
> 由于`rv1106`的`rknn`的模型需要固定的输入尺寸,所以对每个缩放尺寸的图像都需要单独的RKNN模型,这里我使用了一个for循环来处理不同尺寸的图像,并且生成了了多个相应尺寸的RKNN模型来处理不同尺寸的图像。
使用的图像如下:
以下是各层级的推理测试结果:
PNet:
NMS并校准后的结果:
RNet:
NMS并校准后的结果:
ONet:
NMS并校准后的结果:
以上是`MTCNN`模型的推理结果,可以看到,`MTCNN`模型可以准确的检测出图像中的人脸位置和五个关键点位置。
#### 3.2.2 ArcFace
`ArcFace`是一个人脸识别模型,它可以将人脸图像映射到一个高维空间,然后计算两个人脸图像在这个高维空间的距离,从而判断两个人脸图像是否是同一个人。
`ArcFace`模型的输入是一个人脸图像,输出是一个512维的向量。
这部分的转换我参考了`zhuxirui`的成果,他将`ArcFace`模型转换为了`RKNN`模型,并且提供了一个`RKNN`模型的推理代码。
> 帖子地址:[https://bbs.eeworld.com.cn/thread-1302820-1-1.html](https://bbs.eeworld.com.cn/thread-1302820-1-1.html)
------------
附件下载:
-
回复了主题帖:
AI挑战营(进阶) 三:获取onnx模型
zhuxirui 发表于 2025-1-12 00:58
一般如果转换和执行都不报算子错误或者版本不支持其实就没问题,这个主要我之前先在rk3588的板子跟着别人 ...
我后面知道了忘了回了,rknn的文档指引太差了
- 2025-01-08
-
回复了主题帖:
【 AI挑战营(进阶)】4.踩坑记录3 rknn转换
hellokitty_bean 发表于 2025-1-8 17:17
RKNN,这个坑踩得好呀。。。。。。
向楼主学习。。。。。。。。。。。。。
刚刚才做完mtcnn的三层网络转换,因为不熟悉rknn-toolkit的api调了两天bug接下来我就得想办法用c api把三个网络的结果串起来,估计还是大坑
-
回复了主题帖:
【 AI挑战营(进阶)】4.踩坑记录3 rknn转换
wangerxian 发表于 2025-1-7 17:01
你这个是用markdown编辑的吗?
对
- 2025-01-06
-
发表了主题帖:
【 AI挑战营(进阶)】4.踩坑记录3 rknn转换
【AI挑战营(进阶)】在RV1106部署InsightFace算法的多人实时人脸识别实战4 踩坑记录3 模型格式转换之转换到rknn
`rknn`是`rockchip`公司推出的一个神经网络推理框架,它可以在`rockchip`的`rk3399`等芯片上运行`onnx`模型。我们可以使用`rknn-toolkit`工具将`onnx`模型转换为`rknn`模型,然后在`rk3399`等芯片上运行`rknn`模型。
目前`rknn-toolkit2`已经更新到了`2.3.0`版本,我们可以在[最新的仓库](https://github.com/airockchip/rknn-toolkit2)里下载`rknn-toolkit2`的whl安装包,然后安装到本地,就可以使用`rknn-toolkit2`工具将`onnx`模型转换为`rknn`模型了。
> 注意点:仓库只提供了`whl`安装包,没有提供源码,所以我们无法直接在`github`上查看源码,这样我们很多东西要去社区询问才能得到答案。
> `rknn-toolkit2`的包只提供linux系统`arm64`和`x86_64`的安装包,所以我们需要在`linux`系统上安装`rknn-toolkit2`工具,然后将`onnx`模型转换为`rknn`模型。
> 同样由于`rknn-toolkit2`的只有64位架构的版本,所以我们无法在32位的`rv1106`上运行`rknn`的`python api`,这是一个很大的遗憾。
#### 2.3.1 安装rknn-toolkit2
1. 查看系统架构和python版本
在终端中输入以下命令,查看系统架构和python版本:
```shell
╭─ ~ ✔ root@debian 00:17:46 ─╮
╰─ neofetch ─╯
_,met$$$$$gg. root@debian
,g$$$$$$$$$$$$$$$P. -----------
,g$$P" """Y$$.". OS: Debian GNU/Linux 12 (bookworm) x86_64
,$$P' `$$$. Host: KVM/QEMU (Standard PC (i440FX + PIIX, 1996) pc-i440fx-9.0)
',$$P ,ggs. `$$b: Kernel: 6.1.0-28-amd64
`d$$' ,$P"' . $$$ Uptime: 14 hours, 38 mins
$$P d$' , $$P Packages: 1309 (dpkg)
$$: $$. - ,d$$' Shell: zsh 5.9
$$; Y$b._ _,d$P' Resolution: 1920x1080
Y$$. `.`"Y$$$$P"' Terminal: /dev/pts/3
`$$b "-.__ CPU: QEMU Virtual version 2.5+ (4) @ 806MHz
`Y$$ GPU: Intel Raptor Lake-S GT1 [UHD Graphics 770]
`Y$$. Memory: 6009MiB / 15992MiB
`$$b.
`Y$$b.
`"Y$b._
`"""
╭─ ~ ✔ 7s root@debian 00:28:53 ─╮
╰─ python -V ─╯
Python 3.11.2
```
2. 根据系统架构和python版本下载rknn-toolkit2安装包并安装
```shell
pip install -r requirements_cp311-2.3.0.txt
pip install rknn_toolkit2-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
```
3. 安装成功后,我们可以在python终端中导入rknn_toolkit2库,检查是否安装成功
```python
from rknn.api import RKNN
```
可以看到RKNN有很多方法,这些方法可以用于将onnx模型转换为rknn模型,然后在rk3399等芯片上运行rknn模型。
> 我这里使用Pycharm的代码提示工具查看RKNN提供的方法,不知道官方是否有介绍该工具文档,只有例程的话对用户太不友好了。
----------
> 补:rknn-toolkit2只提供了pdf格式的文档,只在仓库的doc文件夹下提供。我认为这样的文档不够友好,反正我是半天没找到,至少readme里应该有文档的链接。
#### 2.3.2 转换onnx模型为rknn模型
根据仓库中的例程,我们有两种方法将onnx模型转换为rknn模型,一种是使用rknn_toolkit2库提供的`rknn.api.rknn_convert`脚本,另一种是手动加载模型并使用rknn_toolkit2库的`rknn.api.RKNN`类的`export_onnx`方法,这种方法更加灵活,我们可以在代码中修改init参数,进行连板推理。
> 官方连板推理的优化似乎还有不足,进行特别慢。
官方没有提供`rknn_convert`的说明,不过我们通过查阅rknn_convert的源码,我们发现rknn_convert的核心代码如下:
```python
convert_model(config_path=input_path,
out_path=output_dir,
target_platform=target_platform,
eval_perf_memory=eval_perf_memory,
verbose=verbose,
accuracy_analysis=accuracy_analysis,
device_id=device_id
)
```
convert_model函数包含了rknn初始化、模型加载、模型转换、模型保存等步骤,方便用户快速将各个框架的模型转换为rknn模型。
config.yaml文件的格式如下:
```yaml
models:
name: PNET # 模型名称
platform: onnx # 原始模型使用的框架
model_file_path: ./path/to/model.onnx # 模型文件路径
quantize: true # 是否量化
dataset: ./dataset.txt # 量化dataset文件路径(相对yml路径)
configs:
quantized_dtype: asymmetric_quantized-8 # 量化类型
mean_values: [0, 0, 0] # rknn.config的mean_values参数
std_values: [255, 255, 255] # rknn.config的std_values参数
quant_img_RGB2BGR: false # 不进行RGB2BGR转换
quantized_algorithm: normal # 量化算法
quantized_method: channel # 量化方法
```
> 配置文件的具体参数可以参考rknn_toolkit2的文档`03_Rockchip_RKNPU_API_Reference_RKNN_Toolkit2_V2.3.0_CN.pdf`。
我们可以根据这个格式创建一个`config.yaml`文件,然后使用`rknn_convert`脚本将onnx模型转换为rknn模型。
```shell
python3 -m rknn.api.rknn_convert -t rv1106 -i ./model_config.yml -o ./
```
#### 2.3.3 连板推理
连板推理是`rknn`的一个特色功能,使用`adb shell`与板子上的`rknn-server`进行通信。
##### 连扳推理的准备工作
1. 确保你的`rknn-toolkit2`运行机的环境安装有adb工具,如果没有,使用`sudo apt install adb`等指令安装即可。
2. 确保你的`rknn-toolkit2`运行机和板子可以相互ping通,如果不采用usb连接的方式且不在同一局域网下,可以考虑使用`frp`等内网穿透工具穿透tcp流量。
3. 确保你的板子上的`rknn-server`与`librknnrt.so`与`rknn-toolkit2`的版本一致,且`rknn-server`已经启动。
> 具体参考`rknn-toolkit2/doc/rknn_server_proxy.md`。
##### 连板推理的使用
1. 使用`adb connect`命令连接板子
2. 在`rknn.init_runtime()`中设置`target='rv1106'`即可实现连板推理
> 关于官方提供的`luckfox pico MAX`TF固件,target参数应该设置为`rv1103`,两款芯片搭载的NPU相同应该没有问题,官方编译固件时使用的是`rv1103`的参数。
------------
- 2025-01-05
-
发表了主题帖:
【 AI挑战营(进阶)】3.踩坑记录2 导出onnx
【AI挑战营(进阶)】在RV1106部署InsightFace算法的多人实时人脸识别实战3 踩坑记录2 模型格式转换之onnx导出
#### 2.2.1 MTCNN模型转换
`MTCNN`模型是一个人脸检测模型,它可以检测出图像中的人脸位置和五个关键点位置。`MTCNN`模型的转换主要是将`TensorFlow`模型转换为`ONNX`模型,我们可以在原来的推理代码的基础上简单增加一个函数`convert_model`,用于将`TensorFlow`模型转换为`ONNX`模型。
##### 1. 在旧版本tf里激流勇进
首先,我们需要安装`tf2onnx`库,它是一个用于将`TensorFlow`模型转换为`ONNX`模型的库。我们可以使用以下命令安装`tf2onnx`库:
```shell
pip install tf2onnx==1.10.1
```
这里由于代码的环境问题,我们使用`tf2onnx`库的`1.10.1`版本,`python 3.6`现在来看过于老旧了,许多框架已经不再支持该版本的兼容性。以后要是再遇到这种远古版本的库,我觉得的可以考虑在新版本直接重构代码,因为这种版本的库可能会有很多难以解决的问题。
由于我们代码中的`MTCNN`模型基本是函数定义的形式,所以我们需要使用`tf2onnx`库的`convert`函数的from_function方法,将`MTCNN`模型转换为`ONNX`模型。我们可以在`FaceDetection_mtcnn.py`文件的模型结构创建部分增加相关代码,用于将`MTCNN`模型转换为`ONNX`模型,代码如下:
```python
if export_onnx:
pnet_output = ['pnet/conv4-2/BiasAdd:0', 'pnet/prob1:0']
rnet_output = ['rnet/conv5-2/conv5-2:0', 'rnet/prob1:0']
onet_output = ['onet/conv6-2/conv6-2:0', 'onet/conv6-3/conv6-3:0', 'onet/prob1:0']
tf2onnx.convert.from_function(
lambda img: sess.run(pnet_output, feed_dict={'pnet/input:0':img}),
input_signature=[tf.TensorSpec([None,None,None,3], tf.float32)],
opset= 9,
output_path=os.path.join(model_path, 'pnet.onnx'))
tf2onnx.convert.from_function(
lambda img: sess.run(rnet_output, feed_dict={'rnet/input:0':img}),
input_signature=[tf.TensorSpec([None,24,24,3], tf.float32)],
opset=9,
output_path=os.path.join(model_path, 'rnet.onnx'))
tf2onnx.convert.from_function(
lambda img: sess.run(onet_output, feed_dict={'onet/input:0':img}),
input_signature=[tf.TensorSpec([None,48,48,3], tf.float32)],
opset=9,
output_path=os.path.join(model_path, 'onet.onnx'))
```
运行时我们发现,`tf2onnx`库的`convert`函数的`from_function`方法只支持`tf2.0`以上的版本,而我们的代码是基于`tf1.0`的版本,所以我们需要将代码中的`tf1.0`版本的代码转换为`tf2.0`版本的代码。
代码的升级流程也不复杂,首先先将代码调试到`tf 1.15.x`的最新版本,然后根据[官方教程](https://tensorflow.google.cn/guide/migrate/migrate_tf2?hl=zh-cn)使用新版本库自带的`tf_upgrade_v2`工具进行代码升级,工作量太大了,所以我们就不再进行这个工作了。
##### 2. 站在巨人的肩膀上
经过一番搜索,我们找到了一个基于`pytorch`的`mtcnn`实现,它的代码地址为[https://github.com/TropComplique/mtcnn-pytorch](https://github.com/TropComplique/mtcnn-pytorch)。不过这个代码已经是8年前的了,是基于`pytorch 0.2`的版本,这个版本的`pytorch`还没有对onnx的支持,所以我们需要将这个代码升级到`pytorch 1.0`以上的版本,然后再将其转换为`onnx`模型。
在这个仓库的一众fork中,我们找到了一个基于`pytorch 1.0`的版本,它的代码地址为[https://github.com/khrlimam/mtcnn-pytorch](https://github.com/khrlimam/mtcnn-pytorch)。这个作者将代码升级到了`pytorch 1.0`以上的版本,并且将其作为一个`pip`包发布到了`pypi`上。简单探索后发现,这个仓库的release中有三个网络的`pth`预训练模型,我们可以直接使用这些模型进行转换。
`Pytorch`的模型转换相对简单,我们只需要将`pytorch`的模型加载到内存中,然后将其转换为`onnx`模型即可。以`Onet`为例,我们可以在Python终端中逐步运行以下每步代码将`pytorch`的模型转换为`onnx`模型:
```python
import torch
from mtcnn_pytorch import PNet, RNet, ONet
onet = ONet()
onet.load_state_dict(torch.load('onet.pth'))
onet.eval()
torch.onnx.export(
onet,
torch.randn(1, 3, 48, 48),
'./onet.onnx',
verbose=True,
input_names=['input'],
dynamic_axes={'input': [0]},
opset_version=9
)
```
这样我们就可以将`pytorch`的模型转换为`onnx`模型了。
#### 2.2.2 InsightFace模型转换
在`onnx/tutorials`的`master`分支中,我们可以找到mxnet到onnx的转换教程,地址为[https://github.com/onnx/tutorials/blob/master/tutorials/MXNetONNXExport.ipynb](https://github.com/onnx/tutorials/blob/master/tutorials/MXNetONNXExport.ipynb)。这个教程中详细介绍了如何将`mxnet`的模型转换为`onnx`模型。
> 该版本mxnet的导出需要使用`onnx>=1.2.1`版本,但是实测python3.6支持的最新版本`onnx`不能正常运行,所以我们需要手动安装一个旧版本比如1.2.1,测试1.4.1似乎也能运行。
核心代码如下:
```shell
onnx_mxnet.export_model(sym, params, [input_shape], np.float32, onnx_file)
```
成功导出后我们发现,该onnx模型使用新版本的`onnxruntime-gpu`库会报错,我很幸运的在网上找到了一个解决方案,来源:[知乎](https://zhuanlan.zhihu.com/p/165294876)
>2)[ONNXRuntimeError] : 6 : RUNTIME_EXCEPTION : Non-zero status code returned while running PRelu node. Name:'conv_1_relu'...... Attempting to broadcast an axis by a dimension other than 1. 56 by 64
错误原因:可能是mxnet支持的onnx版本比较旧!
解决办法:修改PRelu层的slope参数的shape,不仅包括type参数,对应的slope值也要修改来和shape对应。
但是这个解决方案我没有尝试,因为我不熟悉修改onnx模型的操作,特别是这个还需要修改模型参数的shape,我觉得这个操作成本太高了,所以我放弃了使用该项目导出的onnx模型,之后我会在`insightface`的官方仓库里尝试寻找预训练好的onnx模型。
至此,我们前面的工作似乎变得毫无意义,因为我们没有成功的将`insightface`的模型转换为`onnx`模型,所以我们无法在`onnxruntime`中使用自己转换的`insightface`的模型,这是一个很大的遗憾。
其他要说的内容:
------------
##### 关于onnxruntime-gpu库
`onnxruntime-gpu`库是`onnxruntime`的一个分支,它支持`CUDA`加速,可以在`GPU`上运行`onnx`模型。
这个工具也有很多版本依赖问题,他和`python`的版本、`cuda`的版本、`cudnn`的版本是强耦合的,所以在使用的时候要注意这些版本的兼容性。
##### 关于onnx编辑器
网络上常用的onnx可视化工具有`Netron`,它是一个开源的模型可视化工具,可以用于查看`onnx`模型的结构。我们可以在[Netron官网](https://netron.app/)下载`Netron`的安装包,然后安装到本地,就可以使用`Netron`查看`onnx`模型了。
但是编辑onnx需要在写代码和可视化工具之间来回切换,这样效率很低,也不直观,对我这样的小白很不友好,所以我们需要一个可以直接在可视化工具中编辑onnx的工具,这样我们就可以直接在可视化工具中修改onnx模型了。
我在github上找到了一个开源的onnx编辑器`onnx-modifier`,[https://github.com/ZhangGe6/onnx-modifier/blob/master/README_zh-CN.md](https://github.com/ZhangGe6/onnx-modifier/blob/master/README_zh-CN.md)有关于这个工具的介绍,这个工具可以直接在可视化工具中修改onnx模型,这样我们就可以直接在可视化工具中进行简单的修改onnx模型操作了。
-
回复了主题帖:
AI挑战营(进阶) 三:获取onnx模型
iexplore123 发表于 2025-1-4 17:57
大佬大佬,我帮你看了一下,Arcface模型里最后一层有Gemm算子,我也遇到了这个问题,期待大佬的后续 ...
这个Gemm算子支持问题其实好像不是问题,是他们文档写得太弯弯绕绕了,其实是支持的,转换后这部分rknn api会调用cpu计算我竟然被这个耽搁这么多天
-
回复了主题帖:
【 AI挑战营(进阶)】2.踩坑记录1 模型验证
申小林 发表于 2025-1-5 11:29
牛逼牛逼,出个更详细的教程让我复刻一下嘛
嗷,我忘了贴仓库地址了Danbinabo/insighrface,这个仓库适合学习整个人脸项目的流程,不过不太适合这次活动的部署任务
- 2025-01-04
-
回复了主题帖:
AI挑战营(进阶) 三:获取onnx模型
大佬大佬,我帮你看了一下,Arcface模型里最后一层有Gemm算子,我也遇到了这个问题,期待大佬的后续实现
关于模型动态输入的问题,为什么很多端侧ai不支持动态输入啊,怎么知道能不能支持的
另外题外话,有没有大佬可以解释一下,Retina模型里的gather算子和unsqueeze算子是啥,rknn转换大概是怎么处理他们的?最后运行在gpu还是cpu?
-
回复了主题帖:
【 AI挑战营(进阶)】1.思路梳理
chuanyun 发表于 2025-1-3 21:10
期待完整分享
后面模型转换的坑实在太大了,不是我这种初学者一时半会把握得住的
我准备把遇到的一堆问题写写然后直接投奔现有的人脸识别sdk了
-
发表了主题帖:
【 AI挑战营(进阶)】2.踩坑记录1 模型验证
本帖最后由 iexplore123 于 2025-1-5 16:10 编辑
【AI挑战营(进阶)】在RV1106部署InsightFace算法的多人实时人脸识别实战2 踩坑记录1 模型验证
### 2.1 人脸模型验证
`Danbinabo`的教程中,人脸检测模型使用`MTCNN`,人脸识别模型使用`InsightFace`的预训练模型。
运行环境:`Python3.6`,`Tensorflow1_gpu==1.13.1`,`mxnet-cu100`,`opencv_python==4.1.0`。
GPU推理相关:`CUDA10.0`,`cudnn7.6`
建议使用conda管理环境,避免环境冲突。
```shell
conda create -n insighrface python=3.6
conda activate insighrface
pip install tensorflow-gpu==1.13.1 mxnet-cu100 opencv-python==4.1.0
```
CUDA和cudnn的版本要与tensorflow和mxnet的版本对应,否则会出现`CUDA_ERROR`等错误。
#### 2.1.1 人脸检测
`MTCNN`(Multi-task Cascaded Convolutional Networks)是一种基于卷积神经网络的人脸检测算法,由三个级联的卷积神经网络(CNN)组成:P-Net(Proposal Network)、R-Net(Refine Network)和O-Net(Output Network),能够同时检测出图像中的人脸位置、人脸关键点和人脸的置信度。
工作原理:
1. 图像预处理
在进行人脸检测之前,MTCNN首先会对输入图像进行预处理。这包括图像的缩放、裁剪等操作,以适应不同网络的输入要求。通过生成图像金字塔,MTCNN能够处理不同尺度的人脸,从而确保检测的全面性。
2. 候选框生成
P-Net是MTCNN的第一个阶段,负责在预处理后的图像上生成可能包含人脸的候选框。P-Net的输入是一个12x12x3的图像块,通过3层卷积网络后,输出该图像块是否为人脸的概率、人脸框的回归参数以及人脸关键点的位置。根据这些输出,MTCNN会过滤掉低置信度的候选框,保留高置信度的候选框作为后续阶段的输入。
3. 候选框筛选与调整
R-Net是MTCNN的第二个阶段,对P-Net生成的候选框进行进一步的筛选和调整。R-Net的输入是固定大小的图像块(通常为24x24x3),这些图像块由P-Net生成的候选框截取得到。R-Net通过更复杂的网络结构,输出候选框的置信度以及更精确的边框回归参数,用于调整候选框的位置和大小。
4. 人脸检测与关键点定位
O-Net是MTCNN的最后一个阶段,负责对R-Net筛选和调整后的人脸框进行精细处理。O-Net的输入同样是固定大小的图像块(通常为48x48x3),但输出的信息更为丰富,包括人脸置信度、边框回归参数以及五官关键点的精确位置。这些输出信息不仅可以用于人脸检测,还可以为后续的人脸识别、表情分析等任务提供有力支持。
`Danbinabo`的教程中,`MTCNN`的推理相关代码在`FaceDetectiom_mtcnn.py`中,该文件的几个主要函数如下:
1. `def create_mtcnn(sess, model_path)`:创建`MTCNN`模型;加载每个网络的预训练权重(npy文件)并定义网络结构;
```python
def create_mtcnn(sess, model_path, export_onnx=False):
if not model_path:
model_path,_ = os.path.split(os.path.realpath(__file__))
with tf.variable_scope('pnet'):
data = tf.placeholder(tf.float32, (None,None,None,3), 'input')
pnet = PNet({'data':data})
pnet.load(os.path.join(model_path, 'det1.npy'), sess)
with tf.variable_scope('rnet'):
data = tf.placeholder(tf.float32, (None,24,24,3), 'input')
rnet = RNet({'data':data})
rnet.load(os.path.join(model_path, 'det2.npy'), sess)
with tf.variable_scope('onet'):
data = tf.placeholder(tf.float32, (None,48,48,3), 'input')
onet = ONet({'data':data})
onet.load(os.path.join(model_path, 'det3.npy'), sess)
pnet_fun = lambda img : sess.run(('pnet/conv4-2/BiasAdd:0', 'pnet/prob1:0'), feed_dict={'pnet/input:0':img})
rnet_fun = lambda img : sess.run(('rnet/conv5-2/conv5-2:0', 'rnet/prob1:0'), feed_dict={'rnet/input:0':img})
onet_fun = lambda img : sess.run(('onet/conv6-2/conv6-2:0', 'onet/conv6-3/conv6-3:0', 'onet/prob1:0'), feed_dict={'onet/input:0':img})
return pnet_fun, rnet_fun, onet_fun
```
2. `def detect_face(img, minsize, pnet, rnet, onet, threshold, factor)`:人脸检测函数;包括图像预处理、候选框生成、候选框筛选与调整、人脸检测与关键点定位四部操作;
```python
def detect_face(img, minsize, pnet, rnet, onet, threshold, factor):
"""Detects faces in an image, and returns bounding boxes and points for them.
img: input image
minsize: minimum faces' size
pnet, rnet, onet: caffemodel
threshold: threshold=[th1, th2, th3], th1-3 are three steps's threshold
factor: the factor used to create a scaling pyramid of face sizes to detect in the image.
"""
factor_count=0
total_boxes=np.empty((0,9))
points=np.empty(0)
h=img.shape[0]
w=img.shape[1]
minl=np.amin([h, w])
m=12.0/minsize
minl=minl*m
# create scale pyramid
scales=[]
while minl>=12:
scales += [m*np.power(factor, factor_count)]
minl = minl*factor
factor_count += 1
# first stage
for scale in scales:
hs=int(np.ceil(h*scale))
ws=int(np.ceil(w*scale))
im_data = imresample(img, (hs, ws))
im_data = (im_data-127.5)*0.0078125
img_x = np.expand_dims(im_data, 0)
img_y = np.transpose(img_x, (0,2,1,3))
out = pnet(img_y)
out0 = np.transpose(out[0], (0,2,1,3))
out1 = np.transpose(out[1], (0,2,1,3))
boxes, _ = generateBoundingBox(out1[0,:,:,1].copy(), out0[0,:,:,:].copy(), scale, threshold[0])
# inter-scale nms
pick = nms(boxes.copy(), 0.5, 'Union')
if boxes.size>0 and pick.size>0:
boxes = boxes[pick,:]
total_boxes = np.append(total_boxes, boxes, axis=0)
numbox = total_boxes.shape[0]
if numbox>0:
pick = nms(total_boxes.copy(), 0.7, 'Union')
total_boxes = total_boxes[pick,:]
regw = total_boxes[:,2]-total_boxes[:,0]
regh = total_boxes[:,3]-total_boxes[:,1]
qq1 = total_boxes[:,0]+total_boxes[:,5]*regw
qq2 = total_boxes[:,1]+total_boxes[:,6]*regh
qq3 = total_boxes[:,2]+total_boxes[:,7]*regw
qq4 = total_boxes[:,3]+total_boxes[:,8]*regh
total_boxes = np.transpose(np.vstack([qq1, qq2, qq3, qq4, total_boxes[:,4]]))
total_boxes = rerec(total_boxes.copy())
total_boxes[:,0:4] = np.fix(total_boxes[:,0:4]).astype(np.int32)
dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph = pad(total_boxes.copy(), w, h)
numbox = total_boxes.shape[0]
if numbox>0:
# second stage
tempimg = np.zeros((24,24,3,numbox))
for k in range(0,numbox):
tmp = np.zeros((int(tmph[k]),int(tmpw[k]),3))
tmp[dy[k]-1:edy[k],dx[k]-1:edx[k],:] = img[y[k]-1:ey[k],x[k]-1:ex[k],:]
if tmp.shape[0]>0 and tmp.shape[1]>0 or tmp.shape[0]==0 and tmp.shape[1]==0:
tempimg[:,:,:,k] = imresample(tmp, (24, 24))
else:
return np.empty()
tempimg = (tempimg-127.5)*0.0078125
tempimg1 = np.transpose(tempimg, (3,1,0,2))
out = rnet(tempimg1)
out0 = np.transpose(out[0])
out1 = np.transpose(out[1])
score = out1[1,:]
ipass = np.where(score>threshold[1])
total_boxes = np.hstack([total_boxes[ipass[0],0:4].copy(), np.expand_dims(score[ipass].copy(),1)])
mv = out0[:,ipass[0]]
if total_boxes.shape[0]>0:
pick = nms(total_boxes, 0.7, 'Union')
total_boxes = total_boxes[pick,:]
total_boxes = bbreg(total_boxes.copy(), np.transpose(mv[:,pick]))
total_boxes = rerec(total_boxes.copy())
numbox = total_boxes.shape[0]
if numbox>0:
# third stage
total_boxes = np.fix(total_boxes).astype(np.int32)
dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph = pad(total_boxes.copy(), w, h)
tempimg = np.zeros((48,48,3,numbox))
for k in range(0,numbox):
tmp = np.zeros((int(tmph[k]),int(tmpw[k]),3))
tmp[dy[k]-1:edy[k],dx[k]-1:edx[k],:] = img[y[k]-1:ey[k],x[k]-1:ex[k],:]
if tmp.shape[0]>0 and tmp.shape[1]>0 or tmp.shape[0]==0 and tmp.shape[1]==0:
tempimg[:,:,:,k] = imresample(tmp, (48, 48))
else:
return np.empty()
tempimg = (tempimg-127.5)*0.0078125
tempimg1 = np.transpose(tempimg, (3,1,0,2))
out = onet(tempimg1)
out0 = np.transpose(out[0])
out1 = np.transpose(out[1])
out2 = np.transpose(out[2])
score = out2[1,:]
points = out1
ipass = np.where(score>threshold[2])
points = points[:,ipass[0]]
total_boxes = np.hstack([total_boxes[ipass[0],0:4].copy(), np.expand_dims(score[ipass].copy(),1)])
mv = out0[:,ipass[0]]
w = total_boxes[:,2]-total_boxes[:,0]+1
h = total_boxes[:,3]-total_boxes[:,1]+1
points[0:5,:] = np.tile(w,(5, 1))*points[0:5,:] + np.tile(total_boxes[:,0],(5, 1))-1
points[5:10,:] = np.tile(h,(5, 1))*points[5:10,:] + np.tile(total_boxes[:,1],(5, 1))-1
if total_boxes.shape[0]>0:
total_boxes = bbreg(total_boxes.copy(), np.transpose(mv))
pick = nms(total_boxes.copy(), 0.7, 'Min')
total_boxes = total_boxes[pick,:]
points = points[:,pick]
return total_boxes, points
```
3. `def nms(boxes, threshold, method)`:非极大值抑制函数;用于去除重叠度高的候选框,detect_face函数在每个网络输出后都会调用该函数;
```python
def nms(boxes, threshold, method):
if boxes.size==0:
return np.empty((0,3))
x1 = boxes[:,0]
y1 = boxes[:,1]
x2 = boxes[:,2]
y2 = boxes[:,3]
s = boxes[:,4]
area = (x2-x1+1) * (y2-y1+1)
I = np.argsort(s)
pick = np.zeros_like(s, dtype=np.int16)
counter = 0
while I.size>0:
i = I[-1]
pick[counter] = i
counter += 1
idx = I[0:-1]
xx1 = np.maximum(x1, x1[idx])
yy1 = np.maximum(y1, y1[idx])
xx2 = np.minimum(x2, x2[idx])
yy2 = np.minimum(y2, y2[idx])
w = np.maximum(0.0, xx2-xx1+1)
h = np.maximum(0.0, yy2-yy1+1)
inter = w * h
if method is 'Min':
o = inter / np.minimum(area, area[idx])
else:
o = inter / (area + area[idx] - inter)
I = I[np.where(o NCHW:N表示batch_size,用于表示一次输入的样本数;C表示通道数,用于表示图像的颜色通道数,如RGB图像的通道数为3;H表示图像的高度;W表示图像的宽度。
```python
def get_feedData(self, image_4d_array):
image_list = []
for image_3d_array in image_4d_array:
height, width, _ = image_3d_array.shape
if height != 112 or width != 112:
image_3d_array = cv2.resize(image_3d_array, (112, 112))
image_list.append(image_3d_array)
image_4d_array_1 = np.array(image_list)
image_4d_array_2 = np.transpose(image_4d_array_1, [0, 3, 1, 2])
image_4D_Array = nd.array(image_4d_array_2)
image_quantity = len(image_list)
label_1D_Array = nd.ones((image_quantity,))
feed_data = mx.io.DataBatch(data=(image_4D_Array,), label=(label_1D_Array,))
return feed_data
```
3. `def get_feature_2d_array(self, image_4d_array)`:获取特征数组;检查图像数据的维度,将输入的图像数据送入模型进行推理,得到特征数组;最后对特征数组进行归一化处理。
```python
def get_feature_2d_array(self, image_4d_array):
if len(image_4d_array.shape) == 3:
image_4d_array = np.expand_dims(image_4d_array, 0)
assert len(image_4d_array.shape) == 4, 'image_ndarray shape length is not 4'
image_quantity = len(image_4d_array)
if image_quantity != self.batch_size or not self.model:
self.batch_size = image_quantity
self.model = load_model(batch_size=self.batch_size)
feed_data = self.get_feedData(image_4d_array)
self.model.forward(feed_data, is_train=False)
outputs = self.model.get_outputs()
output_2D_Array = outputs[0]
output_2d_array = output_2D_Array.asnumpy()
feature_2d_array = preprocessing.normalize(output_2d_array)
return feature_2d_array
```
`FaceRecognizer`类主要功能是加载人脸库,计算最佳匹配阈值,识别人脸并返回识别结果,其主要函数如下:
4. `def load_database(self, face_dirPath='./face_database')`:加载人脸库;遍历人脸库中的所有人脸文件夹,将每个人脸文件夹中的人脸图片转换为特征向量,并将特征向量与人脸文件夹名对应存入`self.database_2d_array`中。
```python
def load_database(self, face_dirPath='./face_database'):
self.personName_list = next(os.walk(face_dirPath))[1]
personId_list = []
for i, personName in enumerate(self.personName_list):
dirPath = os.path.join(face_dirPath, personName)
fileName_list = next(os.walk(dirPath))[2]
for fileName in fileName_list:
fileSuffix = os.path.splitext(fileName)[1][1:]
if fileSuffix in self.fileSuffix_set:
personId_list.append(i)
self.personId_1d_array = np.array(personId_list)
self.bincount_1d_array = np.bincount(self.personId_1d_array)
self.person_quantity = len(self.personName_list)
self.image_quantity = len(personId_list)
print('人脸数据库中总共有%d个人, %d个人脸图像' %(self.person_quantity, self.image_quantity))
startTime = time.time()
batch_size = 1
imageData_list = []
count = 0
self.database_2d_array = np.empty((self.image_quantity, self.feature_dimension))
for personName in self.personName_list:
dirPath = os.path.join(face_dirPath, personName)
fileName_list = next(os.walk(dirPath))[2]
for fileName in fileName_list:
fileSuffix = os.path.splitext(fileName)[1][1:]
if fileSuffix in self.fileSuffix_set:
filePath = os.path.join(dirPath, fileName)
image_3d_array = np.array(Image.open(filePath))
imageData_list.append(image_3d_array)
count += 1
if count % batch_size == 0:
image_4d_array = np.array(imageData_list)
imageData_list.clear()
self.database_2d_array[count-batch_size: count] = self.face_vectorizer.get_feature_2d_array(image_4d_array)
if count % batch_size != 0:
image_4d_array = np.array(imageData_list)
remainder = count % batch_size
self.database_2d_array[count-remainder: count] = self.face_vectorizer.get_feature_2d_array(image_4d_array)
usedTime = time.time() - startTime
print('加载%d张人脸图像,总共用时 %.4f秒' %(self.image_quantity, usedTime))
# 计算得出最佳阈值
self.make_bestThreshold()
```
5. `def make_bestThreshold(self)`: 计算最佳匹配阈值;通过10折交叉验证,其中1折作为测试集,9折作为训练集,计算每个阈值的准确率,最后取平均准确率最高的阈值作为最佳匹配阈值。
```python
def make_bestThreshold(self):
self.make_randomDataSet()
startTime = time.time()
k_fold = KFold(n_splits=10, shuffle=False)
sample_quantity = len(self.distance_1d_array)
index_1d_array = np.arange(sample_quantity)
bestThreshold_list = []
for fold_index, (train_1d_array, test_1d_array) in enumerate(k_fold.split(index_1d_array)):
train_distance_1d_array = self.distance_1d_array[train_1d_array]
train_isSame_1d_array = self.isSame_1d_array[train_1d_array]
test_distance_1d_array = self.distance_1d_array[test_1d_array]
test_isSame_1d_array = self.isSame_1d_array[test_1d_array]
accuracy_list = []
threshold_1d_array = np.arange(0.5, 2.5, 0.01)
for threshold in threshold_1d_array:
train_accuracy = get_accuracy(train_distance_1d_array, train_isSame_1d_array, threshold)
test_accuracy = get_accuracy(test_distance_1d_array, test_isSame_1d_array, threshold)
# 训练集权重0.4,测试集权重0.6
accuracy = 0.4 * train_accuracy + 0.6 * test_accuracy
accuracy_list.append(accuracy)
bestThreshold_index = np.argmax(accuracy_list)
bestThreshold = threshold_1d_array[bestThreshold_index]
max_accuracy = np.max(accuracy_list)
print('第%d次计算,使用判断阈值%.2f获得最大准确率%.4f' %(fold_index+1, bestThreshold, max_accuracy))
bestThreshold_list.append(bestThreshold)
self.bestThreshold = np.mean(bestThreshold_list)
print('经过10折交叉验证,获得最佳判断阈值%.4f' %self.bestThreshold)
usedTime = time.time() - startTime
print('获取最佳判断阈值,用时%.2f秒' %(usedTime))
```
6. `def get_personName(self, imageFilePath)`:通过输入的图像获取人脸识别结果;它调用 get_personName_1 方法进行实际的识别工作。
7. `def get_personName_1(self, image_3d_array)`:通过输入的图像获取人脸识别结果;首先检查输入图像的维度,然后调用 get_feature_2d_array 方法获取特征向量,接着计算特征向量与人脸库中特征向量的欧氏距离,最后根据最佳匹配阈值判断是否为同一个人。
```python
def get_personName_1(self, image_3d_array):
if len(image_3d_array.shape) == 3:
image_4d_array = np.expand_dims(image_3d_array, 0)
elif len(image_3d_array.shape) == 4:
image_4d_array = image_3d_array
else:
raise ValueError('传入图像数据的维度既不是3维也不是4维')
feature_2d_array = self.face_vectorizer.get_feature_2d_array(image_4d_array)
diffValue_2d_array = np.subtract(self.database_2d_array, feature_2d_array)
distance_1d_array = np.sum(np.square(diffValue_2d_array), 1)
isSame_1d_array = np.less(distance_1d_array, self.bestThreshold)
predict_personId_1d_array = self.personId_1d_array[isSame_1d_array]
if len(predict_personId_1d_array) == 0:
personName = '无效人脸'
else:
min_distance_index = np.argmin(distance_1d_array)
similar_personId = self.personId_1d_array[min_distance_index]
predict_bincount_1d_array = np.bincount(predict_personId_1d_array)
similar_percent = predict_bincount_1d_array[similar_personId] / self.bincount_1d_array[similar_personId]
if similar_percent >= 0.5:
personName = self.personName_list[similar_personId]
else:
personName = '无效人脸'
return personName
```
#### 2.1.3 人脸检测与识别
主程序代码如下:
```python
# 导入常用的库
from PIL import Image, ImageDraw, ImageFont
import os
import numpy as np
import cv2
import time
import sys
# 导入深度学习框架库tensorflow
import tensorflow as tf
# 导入代码文件FaceDetection_mtcnn.py
import FaceDetection_mtcnn
# 导入解析传入参数的库argparse
import argparse
from mtcnn_onnx.detector import detect_faces
# 获取显存动态增长的会话对象session
def get_session():
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
session = tf.Session(config=config)
return session
# 获取当前时间表示的字符串,精确到0.1毫秒
def get_timeString():
now_timestamp = time.time()
now_structTime = time.localtime(now_timestamp)
timeString_pattern = '%Y%m%d_%H%M%S'
now_timeString_1 = time.strftime(timeString_pattern, now_structTime)
now_timeString_2 = ('%.4f' %(now_timestamp%1))[2:]
now_timeString = now_timeString_1 + '_' + now_timeString_2
return now_timeString
# 获取增大的新边界框
def get_new_box(box, margin, image_size):
image_width, image_height = image_size
x1, y1, x2, y2 = box
new_x1 = max(0, x1 - margin / 2)
new_y1 = max(0, y1 - margin / 2)
new_x2 = min(image_width, x2 + margin / 2)
new_y2 = min(image_height, y2 + margin / 2)
new_box = new_x1, new_y1, new_x2, new_y2
return new_box
# 定义人脸检测类FaceDetection
class FaceDetector(object):
# 实例化对象后的初始化方法
def __init__(self, model_dirPath='./resources/mtcnn_model'):
self.session = get_session()
with self.session.as_default():
self.pnet, self.rnet, self.onet = FaceDetection_mtcnn.create_mtcnn(
self.session, model_dirPath)
# 从图像中检测出人脸,并返回检测结果信息
def detect_image(self, image_3d_array, margin=8):
min_size = 20
threshold_list = [0.6, 0.7, 0.7]
factor = 0.7
box_2d_array, point_2d_array = FaceDetection_mtcnn.detect_face(
image_3d_array, min_size,
self.pnet, self.rnet, self.onet,
threshold_list, factor)
# 模型得出的边界框
box_2d_array_1 = box_2d_array.reshape(-1, 5)
# 模型预测出box的4个值、box的置信度,共5个值
box_2d_array_2 = box_2d_array_1[:, 0:4]
box_list = []
image_height, image_width, _ = image_3d_array.shape
image_size = image_width, image_height
for box in box_2d_array_2:
new_box = get_new_box(box, margin, image_size)
box_list.append(new_box)
box_2d_array_3 = np.array(box_list).astype('int')
# 模型得出的人脸5个关键点,即10个值
if len(point_2d_array) == 0:
point_2d_array_1 = np.empty((0, 10))
else:
point_2d_array_1 = np.transpose(point_2d_array, [1, 0])
return box_2d_array_3, point_2d_array_1
# 获取仿射变换后的新图像
def get_affine_image_3d_array(original_image_3d_array, box_1d_array, point_1d_array):
# 左眼、右眼、右嘴角这3个关键点在图像宽高的百分比
affine_percent_1d_array = np.array([0.3333, 0.3969, 0.7867, 0.4227, 0.7, 0.7835])
# 获取剪裁图像数据及宽高信息
x1, y1, x2, y2 = box_1d_array
clipped_image_3d_array = original_image_3d_array[y1:y2, x1:x2]
clipped_image_width = x2 - x1
clipped_image_height = y2 - y1
clipped_image_size = np.array([clipped_image_width, clipped_image_height])
# 左眼、右眼、右嘴角这3个关键点在剪裁图中的坐标
old_point_2d_array = np.float32([
[point_1d_array[0] - x1, point_1d_array[5] - y1],
[point_1d_array[1] - x1, point_1d_array[6] - y1],
[point_1d_array[4] - x1, point_1d_array[9] - y1]
])
# 左眼、右眼、右嘴角这3个关键点在仿射变换图中的坐标
new_point_2d_array = (affine_percent_1d_array.reshape(-1, 2)
* clipped_image_size).astype('float32')
affine_matrix = cv2.getAffineTransform(old_point_2d_array, new_point_2d_array)
# 做仿射变换,并resize至112 * 112
new_size = (112, 112)
clipped_image_size = (clipped_image_width, clipped_image_height)
affine_image_3d_array = cv2.warpAffine(clipped_image_3d_array, affine_matrix, clipped_image_size)
affine_image_3d_array_1 = cv2.resize(affine_image_3d_array, new_size)
return affine_image_3d_array
# 获取注册库的人名列表
last_saveTime = time.time()
def get_personName_list(image_3d_array, box_2d_array, point_2d_array):
global last_saveTime
assert box_2d_array.shape[0] == point_2d_array.shape[0]
personName_list = []
for box_1d_array, point_1d_array in zip(box_2d_array, point_2d_array):
affine_image_3d_array = get_affine_image_3d_array(
image_3d_array, box_1d_array, point_1d_array)# 对检测的人脸做人脸对齐
### 将对齐后的人脸经过模型--得出特征向量,和数据库的特征向量做差,找低于前面交叉验证阈值的--》通过索引取人名
personName = face_recognizer.get_personName_1(affine_image_3d_array)
personName_list.append(personName)
# 保存人脸对齐后的图像数据为图片文件,间隔为1秒
interval = 1
if time.time() - last_saveTime >= interval:
last_saveTime = time.time()
dirPath = './resources/affine_faces/' + personName
if not os.path.isdir(dirPath):
os.makedirs(dirPath)
time_string = get_timeString()
fileName = time_string + '.jpg'
filePath = os.path.join(dirPath, fileName)
image = Image.fromarray(affine_image_3d_array)
image.save(filePath)
return personName_list
# 获取绘制人脸检测、人脸识别效果后的图像
import math
def get_drawed_image_3d_array(image_3d_array, box_2d_array, personName_list, point_2d_array,
show_box=True, show_personName=True,
show_keypoints=False, show_personQuantity=True):
assert len(box_2d_array) == len(personName_list), '请检查函数的参数'
# 获取人脸的数量
person_quantity = len(box_2d_array)
image_height, image_width, _ = image_3d_array.shape
if person_quantity != 0:
# 循环遍历每个实例
for index in range(person_quantity):
# 绘制矩形,即检测出的边界框
if show_box:
box = box_2d_array[index]
x1, y1, x2, y2 = box
leftTop_point = x1, y1
rightBottom_point = x2, y2
color = [255, 0, 0]
thickness = math.ceil((image_width + image_height) / 500)
cv2.rectangle(image_3d_array, leftTop_point, rightBottom_point, color, thickness)
# 绘制文字,文字内容为边界框上边界上方的实例种类名称
if show_personName:
text = personName_list[index]
image = Image.fromarray(image_3d_array)
imageDraw = ImageDraw.Draw(image)
fontSize = math.ceil((image_width + image_height) / 35)
# 添加字体
font = ImageFont.truetype('./font/calibril.ttf', fontSize, encoding='utf-8')
textRegionLeftTop = (x1+5, y1)
color = (255, 0, 0)
imageDraw.text(textRegionLeftTop, text, color, font=font)
image_3d_array = np.array(image)
if show_keypoints:
point_1d_array = point_2d_array[index]
for i in range(5):
point = point_1d_array, point_1d_array[i+5]
radius = math.ceil((image_width + image_height) / 300)
color = (0, 255, 0)
thickness = -1
cv2.circle(image_3d_array, point, radius, color, thickness)
# 绘制文字,文字内容为图片中总共检测出多少个实例物体
if show_personQuantity:
text = 'Total number of faces detected: %d' % person_quantity
image = Image.fromarray(image_3d_array)
imageDraw = ImageDraw.Draw(image)
fontSize = math.ceil((image_width + image_height) / 50)
# 字体文件在Windows10系统可以找到此路径,在Ubuntu系统需要更改路径
font = ImageFont.truetype('calibri.ttf', fontSize, encoding='utf-8')
textRegionLeftTop = (20, 20)
imageDraw.text(textRegionLeftTop, text, (34, 139, 34), font=font)
image_3d_array = np.array(image)
return image_3d_array
# 解析代码文件运行时传入的参数,argument中文叫做参数
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument('--show_keypoints', action='store_true', default=False)
parser.add_argument('--show_personQuantity', action='store_true', default=True)
parser.add_argument('--video_aviFilePath', default=None)
parser.add_argument('--margin', type=int, default=8)
argument_namespace = parser.parse_args()
return argument_namespace
if __name__ == '__main__':
# 解析传入的参数
argument_namespace = parse_args()
show_keypoints = argument_namespace.show_keypoints # 是否显示人脸关键点
show_personQuantity = argument_namespace.show_personQuantity # 人脸分数
video_aviFilePath = argument_namespace.video_aviFilePath # video_path
margin = argument_namespace.margin # 图像边缘阈值
# 实例化相机对象
cameraIndex = 0
camera = cv2.VideoCapture(cameraIndex)
windowName = "faceDetection_demo"
# 采集一帧图像
is_successful, image_3d_array = camera.read()
if is_successful:
# 导入代码文件FaceRecognizer.py中的类FaceRecognizer
from FaceRecognizer import FaceRecognizer
print('成功加载人脸识别类FaceRecognizer')
face_detector = FaceDetector() # 人脸检测类
face_recognizer = FaceRecognizer() # 人脸识别类--这里会计算注册库所有样本
print('成功实例化人脸检测对象、人脸识别对象')
else:
print('未成功调用相机,请检查相机是否已连接电脑')
sys.exit()
if video_aviFilePath != None:
fourcc = cv2.VideoWriter_fourcc('M', 'P', '4', '2')
image_height, image_width, _ = image_3d_array.shape
image_size = image_width, image_height
videoWriter = cv2.VideoWriter(video_aviFilePath, fourcc, 6, image_size)
while is_successful:
startTime = time.time()
# 使用cv2库从相机读取的图像数据均为blue、green、red通道顺序,与rgb顺序相反
is_successful, bgr_3d_array = camera.read() # 抓图,图片格式转换
# 摄像头抓拍图像
rgb_3d_array = cv2.cvtColor(bgr_3d_array, cv2.COLOR_BGR2RGB)
# 对抓拍的图像进行 -- 人脸、关键点检测
box_2d_array, point_2d_array = face_detector.detect_image(rgb_3d_array, margin)
# box, point = detect_faces(Image.fromarray(rgb_3d_array))
# box_2d_array = np.array(box).reshape(-1, 4)
usedTime = time.time() - startTime
print('拍摄图像并在图像中检测人脸,用时%.4f秒' %usedTime)
# 根据人脸检测结果,获取人脸识别结果、绘制检测效果图
startTime = time.time()
if box_2d_array.shape[0] != 0:
# 对检测到的每一个人脸和注册库进行比对--》得出人名
personName_list = get_personName_list(rgb_3d_array, box_2d_array, point_2d_array)
# 绘制检测效果图
show_box=True
show_personName=True
drawed_image_3d_array = get_drawed_image_3d_array(rgb_3d_array, box_2d_array,
personName_list, point_2d_array, show_box, show_personName,
show_keypoints, show_personQuantity)
bgr_3d_array = cv2.cvtColor(drawed_image_3d_array, cv2.COLOR_RGB2BGR)
usedTime = time.time() - startTime
print('人脸识别并绘制检测效果图,用时%.4f秒' %usedTime)
cv2.imwrite('test.jpg',bgr_3d_array)
cv2.imshow(windowName, bgr_3d_array)
# 往视频流文件写入图像数据
if video_aviFilePath != None:
videoWriter.write(bgr_3d_array)
# 按Esc键或者q键退出
pressKey = cv2.waitKey(10)
if 27 == pressKey or ord('q') == pressKey:
cv2.destroyAllWindows()
sys.exit()
print('人脸剪裁及对齐程序运行完成!!')
```
主程序的主要函数和功能如下表所示:
|函数名称|主要参数|功能描述|
|:---|:---|:---|
|get_session|无|获取显存动态增长的会话对象session|
|get_timeString|无|获取当前时间表示的字符串,精确到0.1毫秒|
|get_new_box|box, margin, image_size|获取增大的新边界框|
|FaceDetector|model_dirPath='./resources/mtcnn_model'|定义人脸检测类FaceDetection|
|detect_image|image_3d_array, margin=8|从图像中检测出人脸,并返回检测结果信息|
|get_affine_image_3d_array|original_image_3d_array, box_1d_array, point_1d_array|获取仿射变换后的新图像,用于人脸对齐|
|get_personName_list|image_3d_array, box_2d_array, point_2d_array|获取注册库的人名列表|
|get_drawed_image_3d_array|image_3d_array, box_2d_array, personName_list, point_2d_array, show_box=True, show_personName=True, show_keypoints=False, show_personQuantity=True|获取绘制人脸检测、人脸识别效果后的图像|
|parse_args|无|解析代码文件运行时传入的参数,argument中文叫做参数|
|main|无|主程序代码|
程序会解析传入的参数,根据传入的参数是否显示人脸关键点、是否显示人脸数量、是否提供视频文件路径等,然后调用`FaceDetector`类进行人脸检测,调用`FaceRecognizer`类进行人脸识别,最后调用`get_drawed_image_3d_array`函数绘制人脸检测、人脸识别效果后的图像。
主函数代码流程图:
程序的运行效果如下: