基于MFC和OpenCV实现人脸识别

基于MFC和OpenCV实现人脸识别

文章目录

  • 基于MFC和OpenCV实现人脸识别
    • 1. 项目说明
    • 1. 创建项目
    • 2. 启动窗口
    • 3. 登录窗口-添加窗口、从启动窗口跳转
    • 4. 启动窗口-美化按钮
    • 5. 登录窗口-美化按钮、雪花视频
    • 6. 注册窗口-美化按钮、雪花视频、从启动窗口跳转
    • 7. 注册窗口-开启摄像头
    • 8. 注册窗口-3秒倒计时拍摄
    • 9. 注册窗口-用户注册
    • 11. 欢迎窗口
    • 12. 登录窗口-用户刷脸登录
    • 13. HOME窗口
    • 14. 注册窗口、登录窗口-添加人脸识别方框

  • 笔记主要参考B站视频“【Cpp语言项目】软件开发:人脸识别”。
  • 可能会用到的资料有如下所示,下载链接见文末:
  1. 《奇牛编程-人脸识别资料》1,但是其中有一些命名错误可能会导致程序调用失败:
  • 雷军图片“neijun.jpg”–应为–>“leijun.jpg”
  • 音频文件“zhuche.mp3”–应为–>“zhuce.mp3”
  1. 《我的人脸识别素材》2

注:工程及代码文件会放在本人的Github仓库。


1. 项目说明

实现效果

图1 窗口跳转示意图及实际图
  • 如上图所示,本项目运行后首先出现“启动窗口”,其包括“登录”和“注册”两个按钮。点击“登录”按钮就跳转到“登录窗口”,点击“注册”按钮就跳转到“注册窗口”。
  • 跳转到“登录窗口”后,右侧暂时显示雪花状视频(16张图片轮换显示)。点击“刷脸”按钮后,右侧变成摄像头画面,3秒后自动拍摄人脸照片,若匹配到库中的人脸则跳转到“HOME窗口”;若未匹配到或库中还没有人脸信息,则给出相应的弹窗提示,关闭弹窗返回“登录窗口”。
  • 跳转到“HOME”窗口,左侧显示用户的注册照和基本信息,右侧是视频播放窗口,窗口左下角的按钮可以控制视频的播放和暂停。关闭窗口后回退到“启动窗口”。
  • 跳转到“注册窗口”后,右侧仍然是显示雪花状视频。先填写用户名,若直接点击“刷脸”按钮会给出“请填写用户名”的弹窗。填写用户名并点击“刷脸”按钮后,右侧开启摄像头捕捉人脸,成功检测到人脸后跳转到“欢迎窗口”,否则给出“未检测到人脸!”弹窗提示,关闭弹窗后返回“注册窗口”。
  • 跳转到“欢迎窗口”后,就显示一张欢迎图片,关闭窗口后回退到“启动窗口”。

所需工具
只需要有简单的C语言或者cpp基础,即可完成本项目。工具如下:

老师使用:VisualStudio2019 + OpenCV2.4.9 + 虹软SDKv3.0 + vlc3.0.12
我的使用:VisualStudio2022 + OpenCV4.8.0 + 虹软SDKv3.0 + vlc3.0.18
我的亮点:课程使用OpenCV2.x时代的老代码,我使用当前(2023年9月)最新的OpenCV4.8.0完成功能。

  1. Visual Studio:使用里面的MFC框架完成窗口的制作。
  2. OpenCV:完成摄像头获取图片等基本的图片操作。
  3. 虹软SDK:根据OpenCV获取的图片数据,完成 离线人脸识别 (仅初次使用需联网激活)。
  4. vlc:多媒体播放器,完成最后的视频播放等功能。

  比较老的游戏或国企项目还在使用MFC进行开发,而现如今更火的是Qt,但本项目还是先采用MFC框架。另外,整个项目开发过程中,我尽量按照课程所述进行,但是有很多素材实在是太抽象了,所以小部分素材我会自行替换。另外,代码这玩意越学越熟,所以一开始的几节笔记都写的很详细,一个窗口能唠好几节,后面可能一个窗口就用一节写完了。

代码说明
  本笔记中会给出一些代码,但要注意的是代码具有迭代性,随着功能的增多会不断加入新的代码,所以想看全部的源代码建议直接到本人的Github仓库下载。下面所提及的每一个代码都包括.h/.cpp两个文件:

外部添加代码

  1. ButtonPNG:用于美化按钮的显示。
  2. faceTool:使用虹软人脸识别SDK完成人脸识别功能。
  3. VideoPlayer:使用vlc的SDK完成视频的播放、暂停、退出等。

剩下的都是窗口的“添加类”:

  1. face_recognition:整个项目的主函数,自动生成。
  2. face_recognitionDlg:“启动窗口”的函数,自动生成。
  3. WinLogin:“登录窗口”的函数,“添加类”生成主体。
  4. WinRegister:“注册窗口”的函数,“添加类”生成主体。
  5. WinWelcome:“欢迎窗口”的函数,“添加类”生成主体。
  6. WinHome:“HOME窗口的函数”,“添加类”生成主体。

其余的代码文件暂时不需要了解太多。

1. 创建项目

本节创建基本的MFC项目,属于是先搭建一个基本的“舞台”:

本节步骤:

  1. 给Visual Studio安装MFC框架(默认不安装)。
  2. 创建MFC项目。

参考文章

  • “【Visual Studio 2019】创建 MFC 桌面程序”
  • “VS新建项目时,名称与解决方案名称的区别”
图2 VisualStudio安装MFC
图3 创建MFC项目

2. 启动窗口

本节配置启动窗口。

本节步骤:

  1. 添加素材。将奇牛编程的图片素材解压,然后粘贴到项目的资源文件夹./face_recognition/res/中。
  2. 通过“图片控件”添加背景图片。
  3. 通过“按钮控件”添加“注册”、“登录”按钮。

注:

  1. 使用代码和窗口拖动都可以更改图片的位置。但为了开发迅速,通常使用代码修改会变化的图片(动态图片),而使用MFC控件设置不会变化的图片(静态图片)。个人体会是能不用窗口就不用窗口,这玩意的大小和显示范围有可能会自己变,一个字,不好使!
  2. 本节还不会使用代码控制控件位置,所以就先拖动。
图4 导入素材
图5 添加背景图片
图6 添加“注册”、“登录”按钮

3. 登录窗口-添加窗口、从启动窗口跳转

本节步骤:

  1. 创建登录窗口。直接复制前面的“启动窗口”。
  2. 添加“登录窗口”的“刷脸”按钮。
  3. 设置“启动窗口”单击“登录”跳转到“登录窗口”。
图7 添加“登录”窗口
图8 添加“登录窗口”的“刷脸”按钮

下图首先生成“登录窗口”的头文件和源文件(前三图),然后进入“启动窗口”的源文件中,设置按钮跳转的代码(后两图)。

图9 设置“启动窗口”单击“登录”跳转到“登录窗口”
// “启动窗口”源文件face_recognitionDlg.cpp
/注意要添加一个头文件//
#include "WinLogin.h"///下面是源文件最后一个函数
// 此函数为单击启动窗口“登录”按钮后的操作
void CfacerecognitionDlg::OnBnClickedButton5Log()
{// 跳转到“登录窗口”WinLogin win_log;  // 定义“登录窗口”变量win_log.DoModal(); // 以模态方式呈现出来,也就是必须在当前窗口进行操作,而无法操作其他窗口
}

4. 启动窗口-美化按钮

  本节美化按钮,如圆角、半透明、按下变颜色。要实现这一系列的功能,就需要代码来定义一个类,使按钮显示为设计好的图片,然后在不同状态下(如鼠标单击)显示不同的图片,这便是资料中“ButtonPNG.h”和“ButtonPNG.cpp”所做的事情,只是顾名思义,该代码只能识别PNG格式的图片。

本节步骤:

  1. 美化按钮。添加ButtonPNG代码,然后右键按钮“添加ButtonPNG变量”,最后在窗口的初始化函数中进行ButtonPNG提供的按钮初始化函数,即可完成按钮的美化。
  2. 同样的方法也将“注册按钮”进行了美化。

注:按钮图片有四个联排:正常状态、鼠标悬停状态、鼠标单击状态、禁止使用状态。
关于“添加变量”的说明:虽然上一节添加了“启动窗口”的“登录按钮”单击后跳转到“登录窗口”的代码,但是这个代码只是从窗口元素控件的角度规定了单击按钮后的动作,并没有创建相应的变量来表示相应的按钮,于是本节需要创建一个变量来表示“启动窗口”的“登录按钮”(该变量会声明在“启动窗口”的头文件中),这个按钮变量被声明为刚才添加的ButtonPNG,于是就可以调用ButtonPNG中的各种方法,包括如何显示按钮。

图10 添加背景、美化“登录”/“注册”按钮
  • 上述将背景图片删除,然后在程序中用代码控制显示。
  • 第一行是将“ButtonPNG.h”和“ButtonPNG.cpp”添加到项目中来,后面的是添加背景、美化“登录”/“注册”按钮。

ButtonPNG.h

#pragma once#include "pch.h"
#include <atlimage.h>#ifndef ULONG_PTR
#define ULONG_PTR ULONG
#endif 
//#include "Includes/GdiPlus.h"
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment(lib,"GdiPlus.lib")//按钮的状态
enum {CTRL_NOFOCUS = 0x01,  //普通CTRL_FOCUS,           //mousemoveCTRL_SELECTED,        //buttondownCTRL_DISABLE,         //无效
};//图片形式
enum {BTN_IMG_1 = 1,  //BTN_IMG_3 = 3,  //3分图(1个图片内有3小图,下同)BTN_IMG_4 = 4,  //4分图
};//按钮类型
enum {BTN_TYPE_NORMAL = 0x10,  //普通BTNBTN_TYPE_MENU,           //菜单类型的BTNBTN_TYPE_STATIC,         //静态类型的BTN
};//从资源里面加载位图
bool LoadImageFromResourse(CImage* pImg, UINT nImgID, LPCTSTR lpImgType);
bool LoadPicture(CImage& bmp, UINT nImgID, LPCTSTR lpImgType = _T("PNG")); //含Alpha通道的图片处理成CImagevoid CreateStretchImage(CImage* pImage, CImage* ResultImage, int StretchWidth, int StretchHeight);#if _MSC_VER > 1000
#pragma once
#endif class ButtonPNG : public CButton {
public:ButtonPNG();virtual ~ButtonPNG();
public:void Init(UINT nImg, int nPartNum, UINT nBtnType=BTN_TYPE_NORMAL);bool ShowImage(CDC* pDC, Image* pImage, UINT nState);Image *ImageFromResource(HINSTANCE hInstance,UINT uImgID,LPCTSTR lpType);void PaintParent();protected:afx_msg BOOL OnEraseBkgnd(CDC* pDC);afx_msg void OnMouseMove(UINT nFlags, CPoint point);afx_msg void OnLButtonDown(UINT nFlags, CPoint point);afx_msg void OnLButtonUp(UINT nFlags, CPoint point);afx_msg LRESULT OnMouseHOver(WPARAM wParam,LPARAM lParam);afx_msg LRESULT OnMouseLeave(WPARAM wParam,LPARAM lParam);afx_msg void OnPaint();DECLARE_MESSAGE_MAP()protected:virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);virtual void PreSubclassWindow();public:bool m_bTracked;UINT m_nState;private:int m_nImgPart;Image* m_pImage;UINT m_nBtnType;BOOL m_bMenuOn;  //BTN类型为BTN_TYPE_MENU时,是否处于按下的状态
};void drawPicOnPait(CImage* img, CWnd* wnd, int x, int y);//#endif

ButtonPNG.cpp

// PngButton.cpp : implementation file
////#include "stdafx.h"
#include "pch.h"
#include "ButtonPNG.h"
#include "resource.h"#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif/
// CPngButton
ButtonPNG::ButtonPNG() {m_bTracked=false;m_bMenuOn = FALSE;m_nImgPart = 0;m_pImage = NULL;m_nState = CTRL_NOFOCUS;m_nBtnType = BTN_TYPE_NORMAL;
}ButtonPNG::~ButtonPNG() {if(m_pImage == NULL) {delete m_pImage;m_pImage = NULL;}
}void ButtonPNG::Init(UINT nImg, int nPartNum, UINT nBtnType) {m_pImage = ImageFromResource(AfxGetResourceHandle(), nImg, L"PNG");m_nBtnType = nBtnType;m_nImgPart = nPartNum;if (m_pImage == NULL)return;CRect rcButton;if (m_nImgPart == BTN_IMG_1)rcButton = CRect(0, 0, m_pImage->GetWidth(), m_pImage->GetHeight());else if(m_nImgPart == BTN_IMG_3)rcButton = CRect(0, 0, m_pImage->GetWidth()/3, m_pImage->GetHeight());else if (m_nImgPart == BTN_IMG_4)rcButton = CRect(0, 0, m_pImage->GetWidth()/4, m_pImage->GetHeight());elsereturn;SetWindowPos(NULL, 0, 0, rcButton.Width(), rcButton.Height(), SWP_NOACTIVATE|SWP_NOMOVE);
}BEGIN_MESSAGE_MAP(ButtonPNG, CButton)//{{AFX_MSG_MAP(CPngButton)ON_WM_ERASEBKGND()ON_WM_MOUSEMOVE()ON_WM_LBUTTONDOWN()ON_WM_LBUTTONUP()ON_WM_PAINT()ON_MESSAGE(WM_MOUSEHOVER,OnMouseHOver)ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave)//}}AFX_MSG_MAP
END_MESSAGE_MAP()/
// CPngButton message handlers
void ButtonPNG::OnPaint() {CButton::OnPaint();
}void ButtonPNG::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) {if (!IsWindowEnabled())m_nState = CTRL_DISABLE;CDC dc;dc.Attach(lpDrawItemStruct->hDC);ShowImage(&dc, m_pImage, m_nState);
}bool ButtonPNG::ShowImage(CDC* pDC, Image* pImage, UINT nState) {bool bSuc = false;if(pImage!=NULL) {CRect rcButton;if (m_nImgPart == BTN_IMG_1)rcButton = CRect(0, 0, m_pImage->GetWidth(), m_pImage->GetHeight());else if(m_nImgPart == BTN_IMG_3) {if (nState == CTRL_NOFOCUS)rcButton = CRect(0, 0, m_pImage->GetWidth()/3, m_pImage->GetHeight());else if(nState == CTRL_FOCUS)rcButton = CRect(m_pImage->GetWidth()/3, 0, m_pImage->GetWidth()/3 * 2, m_pImage->GetHeight());else if (nState == CTRL_SELECTED)rcButton = CRect(m_pImage->GetWidth()/3 * 2, 0, m_pImage->GetWidth(), m_pImage->GetHeight());elsereturn false;}else if (m_nImgPart == BTN_IMG_4) {if (nState == CTRL_NOFOCUS)rcButton = CRect(0, 0, m_pImage->GetWidth()/4, m_pImage->GetHeight());else if(nState == CTRL_FOCUS)rcButton = CRect(m_pImage->GetWidth()/4, 0, m_pImage->GetWidth()/4 * 2, m_pImage->GetHeight());else if (nState == CTRL_SELECTED)rcButton = CRect(m_pImage->GetWidth()/4 * 2, 0, m_pImage->GetWidth()/4 * 3, m_pImage->GetHeight());else if (nState == CTRL_DISABLE)rcButton = CRect(m_pImage->GetWidth()/4 * 3, 0, m_pImage->GetWidth(), m_pImage->GetHeight());elsereturn false;}elsereturn false;Graphics graph(pDC->GetSafeHdc());graph.DrawImage(pImage, 0, 0, rcButton.left, rcButton.top, rcButton.Width(), rcButton.Height(), UnitPixel);graph.ReleaseHDC(pDC->GetSafeHdc());bSuc=true;}return bSuc;
}Image *ButtonPNG::ImageFromResource(HINSTANCE hInstance,UINT uImgID,LPCTSTR lpType) {HRSRC hResInfo=::FindResource(hInstance,MAKEINTRESOURCE(uImgID),lpType);if(hResInfo==NULL)return NULL; //failDWORD dwSize;dwSize=SizeofResource(hInstance,hResInfo); //get resource size(bytes) HGLOBAL hResData;hResData=::LoadResource(hInstance,hResInfo);if(hResData==NULL)return NULL; //failHGLOBAL hMem;hMem=::GlobalAlloc(GMEM_MOVEABLE,dwSize);if(hMem==NULL){::FreeResource(hResData);return NULL;}LPVOID lpResData,lpMem;lpResData=::LockResource(hResData);lpMem=::GlobalLock(hMem);::CopyMemory(lpMem,lpResData,dwSize); //copy memory::GlobalUnlock(hMem);::FreeResource(hResData); //free memoryIStream *pStream;HRESULT hr;hr=::CreateStreamOnHGlobal(hMem,TRUE,&pStream);//create stream objectImage *pImage=NULL;if(SUCCEEDED(hr)){pImage=Image::FromStream(pStream);//get GDI+ pointerpStream->Release();}::GlobalFree(hMem);return pImage;
}void ButtonPNG::PreSubclassWindow() {ModifyStyle(0, BS_OWNERDRAW);if (NULL != GetSafeHwnd()) {if (!(GetButtonStyle() & WS_CLIPSIBLINGS))SetWindowLong(GetSafeHwnd(), GWL_STYLE, GetWindowLong(GetSafeHwnd(),GWL_STYLE) | WS_CLIPSIBLINGS);}CButton::PreSubclassWindow();
}BOOL ButtonPNG::OnEraseBkgnd(CDC* pDC) {return TRUE;
}void ButtonPNG::OnMouseMove(UINT nFlags, CPoint point) {// TODO: Add your message handler code here and/or call defaultif(!m_bTracked){TRACKMOUSEEVENT tme;ZeroMemory(&tme,sizeof(TRACKMOUSEEVENT));tme.cbSize=sizeof(TRACKMOUSEEVENT);tme.dwFlags=TME_HOVER|TME_LEAVE;tme.dwHoverTime=1;tme.hwndTrack=this->GetSafeHwnd();if(::_TrackMouseEvent(&tme))m_bTracked=true;}CButton::OnMouseMove(nFlags, point);
}void ButtonPNG::OnLButtonDown(UINT nFlags, CPoint point) {if (m_nState != CTRL_SELECTED) {m_nState = CTRL_SELECTED;if (!m_bMenuOn)m_bMenuOn = TRUE;PaintParent();}CButton::OnLButtonDown(nFlags, point);
}void ButtonPNG::OnLButtonUp(UINT nFlags, CPoint point) {if (m_nState != CTRL_FOCUS) {m_nState = CTRL_FOCUS;PaintParent();}CButton::OnLButtonUp(nFlags, point);
}LRESULT ButtonPNG::OnMouseHOver(WPARAM wParam,LPARAM lParam) {//鼠标放上去时if (m_nState != CTRL_FOCUS) {m_nState = CTRL_FOCUS;PaintParent();    }return 0;
}LRESULT ButtonPNG::OnMouseLeave(WPARAM wParam,LPARAM lParam) {//鼠标移开时m_bTracked=false;if (m_nBtnType == BTN_TYPE_NORMAL)m_nState = CTRL_NOFOCUS;else if (m_nBtnType == BTN_TYPE_MENU) {if (m_bMenuOn)m_nState = CTRL_SELECTED;elsem_nState = CTRL_NOFOCUS;}PaintParent();return 0;
}void ButtonPNG::PaintParent() {CRect   rect;  GetWindowRect(&rect);  GetParent()-> ScreenToClient(&rect);  GetParent()-> InvalidateRect(&rect); 
}bool LoadImageFromResourse(CImage* pImg, UINT nImgID, LPCTSTR lpImgType) {if (pImg == NULL) {return FALSE;}pImg->Destroy();//查找资源HRSRC hRsrc = ::FindResource(AfxGetResourceHandle(), MAKEINTRESOURCE(nImgID), lpImgType);if (hRsrc == NULL) {return false;}//加载资源HGLOBAL hImgData = ::LoadResource(AfxGetResourceHandle(), hRsrc);if (hImgData == NULL) {::FreeResource(hImgData);return false;}LPVOID lpVoid = ::LockResource(hImgData);                            //锁定内存中指定资源LPSTREAM pStream = NULL;DWORD dwSize = ::SizeofResource(AfxGetResourceHandle(), hRsrc);HGLOBAL hNew = ::GlobalAlloc(GHND, dwSize);LPBYTE lpByte = (LPBYTE)::GlobalLock(hNew);::memcpy(lpByte, lpVoid, dwSize);::GlobalUnlock(hNew);                                               //解除资源锁定HRESULT ht = ::CreateStreamOnHGlobal(hNew, TRUE, &pStream);if (ht != S_OK) {GlobalFree(hNew);}else {//加载图片pImg->Load(pStream);GlobalFree(hNew);}//释放资源::FreeResource(hImgData);return true;
}bool LoadPicture(CImage& bmp, UINT nImgID, LPCTSTR lpImgType)   //含Alpha通道的图片处理成CImage
{LoadImageFromResourse(&bmp, nImgID, lpImgType); //加载图片资源if (bmp.IsNull()) {return false;}if (bmp.GetBPP() == 32) //确认该图片包含Alpha通道{for (int i = 0; i < bmp.GetWidth(); i++) {for (int j = 0; j < bmp.GetHeight(); j++) {byte* pByte = (byte*)bmp.GetPixelAddress(i, j);pByte[0] = pByte[0] * pByte[3] / 255;pByte[1] = pByte[1] * pByte[3] / 255;pByte[2] = pByte[2] * pByte[3] / 255;}}}return true;
}void drawPicOnPait(CImage* img, CWnd* wnd, int x, int y) {CPaintDC dc(wnd);CDC* pDC = &dc;CDC dcMem;dcMem.CreateCompatibleDC(pDC);CRect rcClient;GetClientRect(wnd->m_hWnd, &rcClient);CBitmap memBitmap;memBitmap.CreateCompatibleBitmap(pDC, img->GetWidth(), img->GetHeight());dcMem.SelectObject(memBitmap);dcMem.FillSolidRect(rcClient, RGB(255, 255, 255));    //设置画布颜色if (!img->IsNull()) {//CRect rcImg = CRect(x, y, img->GetWidth(), img->GetHeight());CRect rcImg = CRect(0, 0, img->GetWidth(), img->GetHeight());img->Draw(dcMem.m_hDC, rcImg, rcImg);}pDC->BitBlt(x, y, rcClient.Width(), rcClient.Height(), &dcMem, 0, 0, SRCCOPY);memBitmap.DeleteObject();
}void CreateStretchImage(CImage* pImage, CImage* ResultImage, int StretchWidth, int StretchHeight) {if (pImage->IsDIBSection()) {//取得pImage的DCCDC* pImageDC1 = CDC::FromHandle(pImage->GetDC());//Image因为有自己的DC,所以必须使用FromHandle取得对应的DCCBitmap* bitmap1 = pImageDC1->GetCurrentBitmap();BITMAP bmpInfo;bitmap1->GetBitmap(&bmpInfo);//建立新的CImageResultImage->Create(StretchWidth, StretchHeight, bmpInfo.bmBitsPixel);CDC* ResultImageDC = CDC::FromHandle(ResultImage->GetDC());//当Destination比较小的时候,会根据Destination DC上的Stretch Blt mode决定是否保留删除点的资讯ResultImageDC->SetStretchBltMode(HALFTONE);//使用高品质::SetBrushOrgEx(ResultImageDC->m_hDC, 0, 0, NULL);//调整Brush的起点//把pImage画到ResultImage上面StretchBlt(*ResultImageDC, 0, 0, StretchWidth, StretchHeight, *pImageDC1, 0, 0, pImage->GetWidth(), pImage->GetHeight(), SRCCOPY);pImage->ReleaseDC();ResultImage->ReleaseDC();}
}

5. 登录窗口-美化按钮、雪花视频

本节步骤:

  1. 添加并美化“登录窗口”的“刷脸”按钮。
  2. 使用定时器来实现雪花画面循环。
图11 添加并美化“刷脸”按钮
初始化函数/
BOOL WinLogin::OnInitDialog()
{CDialogEx::OnInitDialog();  // 父类的(同名)初始化方法LoadPicture(m_imgBG, IDB_PNG3); // 背景图片的初始化m_btnCamera.Init(IDB_PNG7, 4, BTN_TYPE_NORMAL); // “刷脸”按钮的初始化return 0;
}绘制函数/
void WinLogin::OnPaint()
{//CPaintDC dc(this); // device context for painting// TODO: 在此处添加消息处理程序代码// 不为绘图消息调用 CDialogEx::OnPaint()drawPicOnPait(&m_imgBG, this, 0, 0); // 在当前窗口(登录窗口)的(0,0)位置绘制IDB_PNG3
}
图12 实现右侧的雪花效果
///头文件//
CStatic m_imgSnow_signal; // 雪花图片显示控件
afx_msg void OnTimer(UINT_PTR nIDEvent); // 定时器函数
HBITMAP m_imgsnows[16]; // 定义雪花图片数组///源文件//
初始化函数/
::MoveWindow(m_btnCamera.m_hWnd, 220-90/2-8, 420, 90, 50, 0); // 调整“刷脸”按钮的位置(背景图片440x610)// 初始化snow图片组
//char filename_snow[256];
CString filename_snows; // 存储文件名(MFC提供的类型)
for (int i = 0; i < 16; i++) {//sprintf(filename_snow, "res/snow/snow_%d.bmp", i);filename_snows.Format(L"res/snow/snow_%d.bmp", i); // 定义每个图片的文件名m_imgsnows[i] = (HBITMAP)LoadImage(0, filename_snows, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 将对应的雪花图片存储到数组中
}// 调整雪花视频显示的位置
::MoveWindow(m_imgSnow_signal.m_hWnd, 440, 0, 640, 609, 1); // 前面两个冒号表示使用全局// 启动定时器来播放雪花状文件
SetTimer(1, 30, NULL); // 设置定时器1,每个30ms执行空函数NULL//中断函数/
static int snowIndex = 0; // 雪花图片的编号索引
if (nIDEvent == 1) {m_imgSnow_signal.SetBitmap(m_imgsnows[snowIndex]); // 更新图片控件的显示snowIndex = (snowIndex + 1) % 16; // 更新雪花图片索引。对16取余是因为16张雪花照片重复。
}

6. 注册窗口-美化按钮、雪花视频、从启动窗口跳转

本节步骤:

  1. 添加并美化“注册窗口”的“刷脸”按钮。
  2. 使用定时器来实现雪花画面循环。
  3. 设置“启动界面”的“注册”按钮跳转到“注册窗口”。

注:由于“注册窗口”和“登录窗口”的功能差不多,所以可以直接复制“登录窗口”的内容,比较轻松。

图13 设置“注册窗口”

WinRegister.h

///开头
#include "ButtonPNG.h"///类的定义
public:BOOL OnInitDialog();                        // 定义初始化函数//CImage m_imgBG;                             // 定义“注册窗口”左侧的背景图片//ButtonPNG m_btnCamera;                      // 定义“刷脸按钮”变量CEdit m_editName;                           // 定义“名字编辑框”变量CStatic m_imgSnow_single;                   // 定义单张雪花图片变量HBITMAP m_imgsnows[16];                     // 定义存储所有雪花图片的数组//afx_msg void OnPaint();                     // 窗口的绘制函数afx_msg void OnTimer(UINT_PTR nIDEvent);    // 定时器函数

WinRegister.cpp

///初始化函数
// 本函数为“注册窗口”的初始化函数
BOOL WinRegister::OnInitDialog()
{// 父类的(同名)初始化方法CDialogEx::OnInitDialog();// 设置注册窗口的大小SetWindowPos(NULL, 0, 0, 1080 + 25, 609 + 70, SWP_NOMOVE);// 初始化注册窗口左侧的背景图片LoadPicture(m_imgBG, IDB_PNG3);// 设置“刷脸按钮”m_btnCamera.Init(IDB_PNG7, 4, BTN_TYPE_NORMAL); // “刷脸”按钮的初始化::MoveWindow(m_btnCamera.m_hWnd, 220 - 90/2 - 8, 420, 90, 50, 0); // 调整“刷脸”按钮的位置(背景图片440x610)和大小(90x50)。// 设置“名字编辑框”CFont font;font.CreatePointFont(150, L"华文行楷", NULL); // 设置字号150、字体为华文行楷m_editName.SetFont(&font); // 设置编辑框的字体::MoveWindow(m_editName.m_hWnd, 220 - 150/2 - 8, 525, 150, 60, 0); // 调整编辑框位置(背景图片440x610)和大小(200x60)。// 设置窗口右侧的雪花视频显示CString filename_snows; // 存储文件名(MFC提供的类型)for (int i = 0; i < 16; i++) {filename_snows.Format(L"res/snow/snow_%d.bmp", i); // 定义每个图片的文件名m_imgsnows[i] = (HBITMAP)LoadImage(0, filename_snows, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 将对应的雪花图片存储到数组中}::MoveWindow(m_imgSnow_single.m_hWnd, 440, 0, 640, 609, 1); // 调整雪花视频显示的位置// 启动定时器来播放雪花状文件SetTimer(1, 30, NULL); // 设置定时器1,每个30ms执行空函数NULLreturn 0;
}///绘制函数
void WinRegister::OnPaint()
{//CPaintDC dc(this); // device context for painting// TODO: 在此处添加消息处理程序代码// 不为绘图消息调用 CDialogEx::OnPaint()drawPicOnPait(&m_imgBG, this, 0, 0); // 在当前窗口(注册窗口)的(0,0)位置绘制IDB_PNG3
}///定时器函数
void WinRegister::OnTimer(UINT_PTR nIDEvent)
{// TODO: 在此添加消息处理程序代码和/或调用默认值static int snowIndex = 0; // 雪花图片的编号索引if (nIDEvent == 1) {m_imgSnow_single.SetBitmap(m_imgsnows[snowIndex]); // 更新图片控件的显示snowIndex = (snowIndex + 1) % 16; // 更新雪花图片索引。对16取余是因为16张雪花照片重复。}CDialogEx::OnTimer(nIDEvent);
}
图14 设置“启动窗口”的“注册”按钮跳转到“注册窗口”

face_recognitionDlg.cpp

///启动窗口的“注册”跳转函数//
void CfacerecognitionDlg::OnBnClickedButtonReg()
{// 跳转到“注册窗口”WinRegister win_reg;  // 定义“注册窗口”变量win_reg.DoModal(); // 以模态方式呈现出来,也就是必须在当前窗口进行操作,而无法操作其他窗口
}

7. 注册窗口-开启摄像头

本节步骤:

  1. 配置opencv4.8.0环境。添加头文件;添加库目录;添加静态库;添加动态库拷贝到可执行文件中。
  2. 开启摄像头。在注册窗口添加一个图片控件,并更改ID、添加变量m_imgCamera,然后再去注册窗口源文件添加摄像头显示相关代码。

注:课程配置2.4.9环境后还需要添加CvvImage.h/.cpp(OpenCV非官方代码)、tools.h/.cpp(Rock自己写的)四个代码文件,都是老代码。其中CvvImage.h/.cpp文件在OpenCV2.2后就已经从OpenCV中移除了。而我配置OpenCV环境不需要添加文件
参考链接:

  • opencv官网:https://opencv.org
  • OpenCV2.4.9的C++环境配置视频:“7-使用摄像头捕捉人像”。
  • “VS2022配置C++ OpenCV4.8.0环境”
  • “图像学习环境搭建”–“三、配置OpenCV库(460 vc15)”
  • “mfc集成opencv,实现监控、拍照、录像、录像播放(保姆级教程)”——直接将opencv窗口放在图片控件中
  • “MFC+Opencv4+vs2017 显示图像 详细小白教程(不使用cvvImage)”——将cv::Mat格式转换成CImage
图15 配置opencv4.8.0环境
图16 开启摄像头

WinRegister.cpp

// WinRegister.cpp: 实现文件
//#include "pch.h"
#include "face_recognition.h"
#include "afxdialogex.h"
#include "WinRegister.h"
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui_c.h> // 用到了cvGetWindowHandle
using namespace cv;
static cv::VideoCapture cap_WinReg; // 定义注册窗口的摄像头(static只能本文件使用)// WinRegister 对话框IMPLEMENT_DYNAMIC(WinRegister, CDialogEx)WinRegister::WinRegister(CWnd* pParent /*=nullptr*/): CDialogEx(IDD_FACE_RECOGNITION_REG, pParent)
{}WinRegister::~WinRegister()
{
}void WinRegister::DoDataExchange(CDataExchange* pDX)
{CDialogEx::DoDataExchange(pDX);DDX_Control(pDX, IDC_BUTTON1, m_btnCamera);DDX_Control(pDX, IDC_EDIT1, m_editName);DDX_Control(pDX, IDC_IMG_SNOWS, m_imgSnow_single);DDX_Control(pDX, IDC_STATIC_CAMERA_REG, m_imgCamera_single);
}// 本函数为“注册窗口”的初始化函数
BOOL WinRegister::OnInitDialog()
{// 父类的(同名)初始化方法CDialogEx::OnInitDialog();// 设置注册窗口的大小SetWindowPos(NULL, 0, 0, 1080 + 25, 609 + 70, SWP_NOMOVE);// 初始化注册窗口左侧的背景图片LoadPicture(m_imgBG, IDB_PNG3);// 设置“刷脸按钮”m_btnCamera.Init(IDB_PNG7, 4, BTN_TYPE_NORMAL); // “刷脸”按钮的初始化::MoveWindow(m_btnCamera.m_hWnd, 220 - 90/2 - 8, 420, 90, 50, 0); // 调整“刷脸”按钮的位置(背景图片440x610)和大小(90x50)。// 设置“名字编辑框”CFont font;font.CreatePointFont(150, L"华文行楷", NULL); // 设置字号150、字体为华文行楷m_editName.SetFont(&font); // 设置编辑框的字体::MoveWindow(m_editName.m_hWnd, 220 - 150/2 - 8, 525, 150, 60, 0); // 调整编辑框位置(背景图片440x610)和大小(200x60)。// 设置窗口右侧的雪花视频显示CString filename_snows; // 存储文件名(MFC提供的类型)for (int i = 0; i < 16; i++) {filename_snows.Format(L"res/snow/snow_%d.bmp", i); // 定义每个图片的文件名m_imgsnows[i] = (HBITMAP)LoadImage(0, filename_snows, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 将对应的雪花图片存储到数组中}::MoveWindow(m_imgSnow_single.m_hWnd, 440, 0, 640, 609, 1); // 调整雪花视频显示的位置// 启动定时器来播放雪花状文件SetTimer(1, 30, NULL); // 设置定时器1,每个30ms执行空函数NULL// 调整摄像头显示的图片控件的位置(440,64)和大小(640x480)::MoveWindow(m_imgCamera_single.m_hWnd, 440, 64, 640, 480, 1);return 0;
}BEGIN_MESSAGE_MAP(WinRegister, CDialogEx)ON_WM_PAINT()ON_WM_TIMER()ON_BN_CLICKED(IDC_BUTTON1, &WinRegister::OnBnClickedButton1)
END_MESSAGE_MAP()// WinRegister 消息处理程序void WinRegister::OnPaint()
{//CPaintDC dc(this); // device context for painting// TODO: 在此处添加消息处理程序代码// 不为绘图消息调用 CDialogEx::OnPaint()drawPicOnPait(&m_imgBG, this, 0, 0); // 在当前窗口(注册窗口)的(0,0)位置绘制IDB_PNG3
}void WinRegister::OnTimer(UINT_PTR nIDEvent)
{// TODO: 在此添加消息处理程序代码和/或调用默认值static int snowIndex = 0; // 雪花图片的编号索引if (nIDEvent == 1) {m_imgSnow_single.SetBitmap(m_imgsnows[snowIndex]); // 更新图片控件的显示snowIndex = (snowIndex + 1) % 16; // 更新雪花图片索引。对16取余是因为16张雪花照片重复。}else if (nIDEvent == 2) {// 获取摄像头拍摄的单帧,并进行显示cv::Mat cam_frame;cap_WinReg >> cam_frame;imshow("m_imgCamera_single", cam_frame);        }CDialogEx::OnTimer(nIDEvent);
}// “注册窗口”中的“刷脸按钮”点击操作
void WinRegister::OnBnClickedButton1()
{// 关闭雪花视频的定时器(MFC框架中自带函数)KillTimer(1);// 将opencv的窗体嵌入到图片控件m_imgCamera_single中cv::namedWindow("m_imgCamera_single", cv::WINDOW_AUTOSIZE);     // 打开一个opencv窗口,注意名称要与图片控件一致// 第二个选项是cv::WindowFlags:https://vovkos.github.io/doxyrest-showcase/opencv/sphinxdoc/enum_cv_WindowFlags.htmlHWND hWnd = (HWND)cvGetWindowHandle("m_imgCamera_single");      // 获取opencv窗口句柄HWND hParent = ::GetParent(hWnd);                               // 获取opencv窗口的父窗口句柄::SetParent(hWnd, GetDlgItem(IDC_STATIC_CAMERA_REG)->m_hWnd);   // 将opencv窗口的句柄设置为图片控件的句柄::ShowWindow(hParent, SW_HIDE);                                 // 隐藏原父窗口// 打开默认摄像头0cap_WinReg.open(0);if (!cap_WinReg.isOpened()) {::MessageBox(NULL, _T("摄像头打开失败!\n请检查摄像头是否正确连接并开启!"), _T("警告"), MB_OK | MB_ICONEXCLAMATION);//c++中MessageBox弹窗的用法大全:https://blog.csdn.net/LCR2025/article/details/129223538return;}// 开启摄像头的定时器SetTimer(2, 20, NULL);
}

8. 注册窗口-3秒倒计时拍摄

本节步骤:

  1. 实现3秒倒计时拍摄。主要思路是对中断次数进行计数,完成3s倒计时拍摄。注意要播放3s倒计时的音频文件,还需要还添加多媒体的头文件。
图17 实现3秒倒计时拍摄

WinRegister.cpp

// WinRegister.cpp: 实现文件
//#include "pch.h"
#include "face_recognition.h"
#include "afxdialogex.h"
#include "WinRegister.h"
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui_c.h> // 用到了cvGetWindowHandle
using namespace cv;
static cv::VideoCapture cap_WinReg; // 定义注册窗口的摄像头(static只能本文件使用)// 使用 Windows 多媒体 API 提供的音频和多媒体功能,包括播放声音、音乐和控制多媒体设备。
#include <mmsystem.h> // 该头文件包含了许多用于音频、视频、音乐和多媒体设备控制的函数和数据类型的声明。
#pragma comment(lib, "winmm.lib") // 将Windows多媒体库文件 winmm.lib 关联到程序中,以便调用多媒体API函数。// WinRegister 对话框IMPLEMENT_DYNAMIC(WinRegister, CDialogEx)WinRegister::WinRegister(CWnd* pParent /*=nullptr*/): CDialogEx(IDD_FACE_RECOGNITION_REG, pParent)
{}WinRegister::~WinRegister()
{
}void WinRegister::DoDataExchange(CDataExchange* pDX)
{CDialogEx::DoDataExchange(pDX);DDX_Control(pDX, IDC_BUTTON1, m_btnCamera);DDX_Control(pDX, IDC_EDIT1, m_editName);DDX_Control(pDX, IDC_IMG_SNOWS, m_imgSnow_single);DDX_Control(pDX, IDC_STATIC_CAMERA_REG, m_imgCamera_single);
}// 本函数为“注册窗口”的初始化函数
BOOL WinRegister::OnInitDialog()
{// 父类的(同名)初始化方法CDialogEx::OnInitDialog();// 设置注册窗口的大小SetWindowPos(NULL, 0, 0, 1080 + 25, 609 + 70, SWP_NOMOVE);// 初始化注册窗口左侧的背景图片LoadPicture(m_imgBG, IDB_PNG3);// 设置“刷脸按钮”m_btnCamera.Init(IDB_PNG7, 4, BTN_TYPE_NORMAL); // “刷脸”按钮的初始化::MoveWindow(m_btnCamera.m_hWnd, 220 - 90/2 - 8, 420, 90, 50, 0); // 调整“刷脸”按钮的位置(背景图片440x610)和大小(90x50)。// 设置“名字编辑框”CFont font;font.CreatePointFont(150, L"华文行楷", NULL); // 设置字号150、字体为华文行楷m_editName.SetFont(&font); // 设置编辑框的字体::MoveWindow(m_editName.m_hWnd, 220 - 150/2 - 8, 525, 150, 60, 0); // 调整编辑框位置(背景图片440x610)和大小(200x60)。// 设置窗口右侧的雪花视频显示CString filename_snows; // 存储文件名(MFC提供的类型)for (int i = 0; i < 16; i++) {filename_snows.Format(L"res/snow/snow_%d.bmp", i); // 定义每个图片的文件名m_imgsnows[i] = (HBITMAP)LoadImage(0, filename_snows, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 将对应的雪花图片存储到数组中}::MoveWindow(m_imgSnow_single.m_hWnd, 440, 0, 640, 609, 1); // 调整雪花视频显示的位置// 启动定时器来播放雪花状文件SetTimer(1, 30, NULL); // 设置定时器1,每个30ms执行空函数NULL// 调整摄像头显示的图片控件的位置(440,64)和大小(640x480)::MoveWindow(m_imgCamera_single.m_hWnd, 440, 64, 640, 480, 1);return 0;
}BEGIN_MESSAGE_MAP(WinRegister, CDialogEx)ON_WM_PAINT()ON_WM_TIMER()ON_BN_CLICKED(IDC_BUTTON1, &WinRegister::OnBnClickedButton1)
END_MESSAGE_MAP()// WinRegister 消息处理程序void WinRegister::OnPaint()
{//CPaintDC dc(this); // device context for painting// TODO: 在此处添加消息处理程序代码// 不为绘图消息调用 CDialogEx::OnPaint()drawPicOnPait(&m_imgBG, this, 0, 0); // 在当前窗口(注册窗口)的(0,0)位置绘制IDB_PNG3
}// 整个注册窗口的中断函数
void WinRegister::OnTimer(UINT_PTR nIDEvent)
{static int snowIndex = 0;   // 雪花图片的编号索引static int shoot_count = 0; // 3秒倒计时拍摄的时间计数// 定时器1用于循环播放16张雪花背景图片if (nIDEvent == 1) {m_imgSnow_single.SetBitmap(m_imgsnows[snowIndex]); // 更新图片控件的显示snowIndex = (snowIndex + 1) % 16; // 更新雪花图片索引。对16取余是因为16张雪花照片重复。}// 定时器2用于摄像头显示,并在3s时拍摄照片else if (nIDEvent == 2) {cv::Mat cam_frame;       // 定义摄像头单帧cap_WinReg >> cam_frame; // 获取摄像头拍摄的单帧// 获取摄像头拍摄的单帧,并进行显示if (shoot_count < 3000/30) { // 3000表示3000ms(3s),30是定时器2的中断间隔时间if (shoot_count == 0) {mciSendString(L"play res/zhuce.mp3", 0, 0, 0); // 播放3秒倒计时音效}shoot_count++;imshow("m_imgCamera_single", cam_frame);}else if (shoot_count == 3000 / 30) {cv::imwrite("tmp.jpg", cam_frame);  // 保存单帧照片shoot_count = 0;                    // 清零计数KillTimer(2);                       // 关闭定时器cap_WinReg.release();               // 关闭摄像头CDialogEx::OnOK();                  // 关闭注册窗口}}CDialogEx::OnTimer(nIDEvent);
}// “注册窗口”中的“刷脸按钮”点击操作
void WinRegister::OnBnClickedButton1()
{// 关闭雪花视频的定时器(MFC框架中自带函数)KillTimer(1);// 将雪花图片更换成墙纸HBITMAP bg_wall = (HBITMAP)LoadImage(NULL, L"res/wall.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);m_imgSnow_single.SetBitmap(bg_wall);// 将opencv的窗体嵌入到图片控件m_imgCamera_single中cv::namedWindow("m_imgCamera_single", cv::WINDOW_AUTOSIZE);     // 打开一个opencv窗口,注意名称要与图片控件一致// 第二个选项是cv::WindowFlags:https://vovkos.github.io/doxyrest-showcase/opencv/sphinxdoc/enum_cv_WindowFlags.htmlHWND hWnd = (HWND)cvGetWindowHandle("m_imgCamera_single");      // 获取opencv窗口句柄HWND hParent = ::GetParent(hWnd);                               // 获取opencv窗口的父窗口句柄::SetParent(hWnd, GetDlgItem(IDC_STATIC_CAMERA_REG)->m_hWnd);   // 将opencv窗口的句柄设置为图片控件的句柄::ShowWindow(hParent, SW_HIDE);                                 // 隐藏原父窗口// 打开默认摄像头0cap_WinReg.open(0);if (!cap_WinReg.isOpened()) {::MessageBox(NULL, _T("摄像头打开失败!\n请检查摄像头是否正确连接并开启!"), _T("警告"), MB_OK | MB_ICONEXCLAMATION);//c++中MessageBox弹窗的用法大全:https://blog.csdn.net/LCR2025/article/details/129223538return;}// 开启摄像头的定时器SetTimer(2, 30, NULL);
}

9. 注册窗口-用户注册

要实现多用户注册,可以用数据库来存储文件信息,但是本工程为求简便,就直接使用重命名注册图片的方式,来存储用户信息。存储图片格式为 职业-用户名-颜值.jpg,其中“颜值”使用用户与“雷军”的人脸识别相似度来计算。

本节步骤:

  1. 配置虹软人脸识别SDK环境。可以实现离线人脸识别。
  2. 完成用户注册功能。由于虹软人脸识别SDK的接口还是偏底层,所以Rock还是自己写了“faceTool.h/.cpp”将其封装成更高层的接口,来方便调用。但是Rock一直采用OpenCV2.x版本的旧代码,已经不适用OpenCV4.x这样的新版本了,所以我对它的代码进行了升级并添加了我认为比较详尽的注释。虹软要求先创建一个人脸识别的模块,整个程序只需创建一个

Rock:“百度云人脸识别SDK效果不是很好、接口也不是很好,所以本项目用虹软人脸识别SDK”。
虹软官网:https://www.arcsoft.com.cn/
SDK说明:奇牛编程素材中的“人脸识别-V3.0.zip”解压后有X86、X64两个版本。或者下面也演示了如何去官网下载。

图18 配置虹软人脸识别SDK环境

不同的SDK调用套路都不太一样,可以查看官方示例:

图19 虹软人脸识别SDK帮助文档

注意前两张图片添加完faceTools后,将其代码改成下面我给出的代码:

图20 完成用户注册功能

faceTool.h

#pragma once#include "arcsoft_face_sdk.h"
#include "amcomdef.h"
#include "asvloffscreen.h"
#include "merror.h"
#include <direct.h>
#include <iostream>  
#include <stdarg.h>
#include <string>
#include <opencv2\opencv.hpp>using namespace std;
#pragma comment(lib, "libarcsoft_face_engine.lib")#define SafeFree(p) { if ((p)) free(p); (p) = NULL; }
#define SafeArrayDelete(p) { if ((p)) delete [] (p); (p) = NULL; } 
#define SafeDelete(p) { if ((p)) delete (p); (p) = NULL; } #define APPID "qAQ7JXMqChSZ5td1RJq1i16Lkew4WgZxXv92vnAWXqs"// 32位
//#define SDKKey "8LJqeAmH6wsjcdBBMt6E1WRjt8aHyaWdsfUg7ELx8KPD"//64位
#define SDKKey  "7kidGLKLxqz39fUgPFkzvQvZADdtYtMEMZX64iACPYZM"// 虹软人脸识别SDK初始化
void faceInit(MHandle* handle);// 人脸对比函数,返回相似度
float faceCompare(MHandle handle, cv::Mat& img1, cv::Mat& img2);// 返回图片中识别到的人脸位置
BOOL faceRegion(MHandle handle, cv::Mat img, cv::Rect& face_rect);

faceTool.cpp

#include "pch.h"
#include "faceTool.h"void faceInit(MHandle* handle) {//激活接口,需联网激活MRESULT res = ASFOnlineActivation((char*)APPID, (char*)SDKKey);if (MOK != res && MERR_ASF_ALREADY_ACTIVATED != res)printf("激活失败\n");//获取激活文件信息ASF_ActiveFileInfo  activeFileInfo;res = ASFGetActiveFileInfo(&activeFileInfo);if (res != MOK)printf("ASFGetActiveFileInfo fail: %d\n", res);//初始化接口MInt32 mask = ASF_FACE_DETECT | ASF_FACERECOGNITION | ASF_AGE | ASF_GENDER | ASF_FACE3DANGLE | ASF_LIVENESS | ASF_IR_LIVENESS;res = ASFInitEngine(ASF_DETECT_MODE_IMAGE, ASF_OP_0_ONLY, 30, 10, mask, handle);if (res != MOK)printf("接口初始化失败\n");
}float faceCompare(MHandle handle, cv::Mat& img1, cv::Mat& img2) {ASF_MultiFaceInfo detectedFaces1{ 0 };          // 定义多人脸信息ASF_SingleFaceInfo SingleDetectedFaces1{ 0 };   // 定义单人脸信息ASF_FaceFeature feature1{ 0 };                  // 定义人脸特征ASF_FaceFeature copyfeature1{ 0 };              // 定义人脸特征的拷贝cv::Rect roiRect1(0, 0, img1.cols - img1.cols % 4, img1.rows); // 定义要裁剪的区域(只是因为要求宽度是4的整数倍)cv::Mat cutImg1 = img1(roiRect1).clone();       // 得到裁剪好的图片// 检测是否存在人脸(注意这里虹软SDK文档要求图片宽度必须是4的倍数)MRESULT res;res = ASFDetectFaces(handle, cutImg1.cols, cutImg1.rows, ASVL_PAF_RGB24_B8G8R8, (MUInt8*)cutImg1.data, &detectedFaces1);if (MOK == res) {// 取出图片中的第一个人脸信息if (detectedFaces1.faceRect != NULL && detectedFaces1.faceOrient != NULL) {// 其实这个判断可以不写,因为只要res==MOK,detectedFaces1中就一定会有内容,所以直接赋值没问题。// 但是我想消除编译器警告,所以才加上这个判断。SingleDetectedFaces1.faceRect.left = detectedFaces1.faceRect[0].left;SingleDetectedFaces1.faceRect.top = detectedFaces1.faceRect[0].top;SingleDetectedFaces1.faceRect.right = detectedFaces1.faceRect[0].right;SingleDetectedFaces1.faceRect.bottom = detectedFaces1.faceRect[0].bottom;SingleDetectedFaces1.faceOrient = detectedFaces1.faceOrient[0];}// 单人脸特征提取res = ASFFaceFeatureExtract(handle, cutImg1.cols, cutImg1.rows, ASVL_PAF_RGB24_B8G8R8, (MUInt8*)cutImg1.data, &SingleDetectedFaces1, &feature1);if (res == MOK) {// 若提取到了人脸特征信息,才将其进行拷贝。// 至于为什么要进行拷贝,暂时还没有搞懂??copyfeature1.featureSize = feature1.featureSize;copyfeature1.feature = (MByte*)malloc(feature1.featureSize);memset(copyfeature1.feature, 0, feature1.featureSize);memcpy(copyfeature1.feature, feature1.feature, feature1.featureSize);}else {printf("ASFFaceFeatureExtract 1 fail: %d\n", res);}}else {printf("ASFDetectFaces 1 fail: %d\n", res);}//第二张人脸处理ASF_MultiFaceInfo detectedFaces2{ 0 };          // 定义多人脸信息ASF_SingleFaceInfo SingleDetectedFaces2{ 0 };   // 定义单人脸信息ASF_FaceFeature feature2 = { 0 };               // 定义人脸特征cv::Rect roiRect2(0, 0, img2.cols - img2.cols % 4, img2.rows); // 定义要裁剪的区域(只是因为要求宽度是4的整数倍)cv::Mat cutImg2 = img2(roiRect2).clone();       // 得到裁剪好的图片// 检测图片中的人脸信息res = ASFDetectFaces(handle, cutImg2.cols, cutImg2.rows, ASVL_PAF_RGB24_B8G8R8, (MUInt8*)cutImg2.data, &detectedFaces2);if (MOK == res) {if (detectedFaces2.faceRect != NULL && detectedFaces2.faceOrient != NULL) {// 其实这个判断可以不写,因为只要res==MOK,detectedFaces1中就一定会有内容,所以直接赋值没问题。// 但是我想消除编译器警告,所以才加上这个判断。SingleDetectedFaces2.faceRect.left = detectedFaces2.faceRect[0].left;SingleDetectedFaces2.faceRect.top = detectedFaces2.faceRect[0].top;SingleDetectedFaces2.faceRect.right = detectedFaces2.faceRect[0].right;SingleDetectedFaces2.faceRect.bottom = detectedFaces2.faceRect[0].bottom;SingleDetectedFaces2.faceOrient = detectedFaces2.faceOrient[0];}// 单人脸特征提取res = ASFFaceFeatureExtract(handle, cutImg2.cols, cutImg2.rows, ASVL_PAF_RGB24_B8G8R8, (MUInt8*)cutImg2.data, &SingleDetectedFaces2, &feature2);if (MOK != res) {printf("ASFFaceFeatureExtract 2 fail: %d\n", res);}}else {printf("ASFDetectFaces 2 fail: %d\n", res);}// 单人脸特征比对MFloat confidenceLevel;res = ASFFaceFeatureCompare(handle, &copyfeature1, &feature2, &confidenceLevel);if (res != MOK) {confidenceLevel = -1;}SafeFree(copyfeature1.feature); //释放内存return confidenceLevel;
}// 返回图片中识别到的人脸位置
BOOL faceRegion(MHandle handle, cv::Mat img, cv::Rect& face_rect) {// 裁剪图片,使其宽度为4的整数倍(ASFDetectFaces要求)cv::Rect roiRect1(0, 0, img.cols - img.cols % 4, img.rows); // 定义要裁剪的区域(只是因为要求宽度是4的整数倍)cv::Mat cutImg1 = img(roiRect1).clone();       // 得到裁剪好的图片// 检测是否存在人脸ASF_MultiFaceInfo detectedFaces1{ 0 };          // 定义多人脸信息MRESULT res = ASFDetectFaces(handle, cutImg1.cols, cutImg1.rows, ASVL_PAF_RGB24_B8G8R8, (MUInt8*)cutImg1.data, &detectedFaces1);// 若存在人脸就返回第一个人脸信息if (MOK == res && detectedFaces1.faceRect != NULL && detectedFaces1.faceNum) {face_rect.x = detectedFaces1.faceRect[0].left;face_rect.y = detectedFaces1.faceRect[0].top;face_rect.width = detectedFaces1.faceRect[0].right - detectedFaces1.faceRect[0].left;face_rect.height = detectedFaces1.faceRect[0].bottom - detectedFaces1.faceRect[0].top;return true;}else {printf("ASFDetectFaces 1 fail: %d\n", res);return false;}
}

11. 欢迎窗口

  1. 点击“刷脸按钮”后检查编辑框,若为空则提示填写信息。
  2. 添加注册成功后的欢迎界面。
图21 完善注册功能

12. 登录窗口-用户刷脸登录

本节步骤:

  1. 实现刷脸登录。从注册窗口的摄像头相关代码中复制,即可轻易实现点击“刷脸按钮”1s后,自动抓拍人脸。

注:登录成功的表示暂时先用一个弹窗替代,后续再设置跳转到“HOME”窗口。

图22 实现刷脸登录

WinLogin.cpp

// WinLogin.cpp: 实现文件
//#include "pch.h"
#include "face_recognition.h"
#include "afxdialogex.h"
#include "WinLogin.h"
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui_c.h> // 用到了cvGetWindowHandle// 使用 Windows 多媒体 API 提供的音频和多媒体功能,包括播放声音、音乐和控制多媒体设备。
#include <mmsystem.h> // 该头文件包含了许多用于音频、视频、音乐和多媒体设备控制的函数和数据类型的声明。
#pragma comment(lib, "winmm.lib") // 将Windows多媒体库文件 winmm.lib 关联到程序中,以便调用多媒体API函数。#include "faceTool.h" // 人脸识别模块#include "WinWelcome.h" // 欢迎界面#include <vector> // 人脸识别函数FaceCheck
#include <string>using namespace cv;
static cv::VideoCapture cap_WinLog; // 定义登录窗口的摄像头(static只能本文件使用)
extern MHandle faceModel; // 人脸识别模块// WinLogin 对话框IMPLEMENT_DYNAMIC(WinLogin, CDialogEx)WinLogin::WinLogin(CWnd* pParent /*=nullptr*/): CDialogEx(IDD_FACE_RECOGNITION_LOG, pParent)
{}WinLogin::~WinLogin()
{
}BOOL WinLogin::OnInitDialog()
{CDialogEx::OnInitDialog();  // 父类的(同名)初始化方法SetWindowPos(NULL, 0, 0, 1080 + 25, 609 + 70, SWP_NOMOVE); // 设置窗口大小LoadPicture(m_imgBG, IDB_PNG3); // 背景图片的初始化m_btnCamera.Init(IDB_PNG7, 4, BTN_TYPE_NORMAL); // “刷脸”按钮的初始化::MoveWindow(m_btnCamera.m_hWnd, 220-90/2-8, 420, 90, 50, 0); // 调整“刷脸”按钮的位置(背景图片440x610)// 初始化snow图片组//char filename_snow[256];CString filename_snows; // 存储文件名(MFC提供的类型)for (int i = 0; i < 16; i++) {//sprintf(filename_snow, "res/snow/snow_%d.bmp", i);filename_snows.Format(L"res/snow/snow_%d.bmp", i); // 定义每个图片的文件名m_imgsnows[i] = (HBITMAP)LoadImage(0, filename_snows, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 将对应的雪花图片存储到数组中}// 调整雪花视频显示的位置::MoveWindow(m_imgSnow_signal.m_hWnd, 440, 0, 640, 609, 1); // 前面两个冒号表示使用全局// 启动定时器来播放雪花状文件SetTimer(1, 30, NULL); // 设置定时器1,每个30ms执行空函数NULL// 调整摄像头显示的图片控件的位置(440,64)和大小(640x480)::MoveWindow(m_imgCamera_single.m_hWnd, 440, 64, 640, 480, 1);return 0;
}void WinLogin::DoDataExchange(CDataExchange* pDX)
{CDialogEx::DoDataExchange(pDX);DDX_Control(pDX, IDC_BUTTON1, m_btnCamera);DDX_Control(pDX, IDC_IMG_SNOWS, m_imgSnow_signal);DDX_Control(pDX, IDC_STATIC_CAMERA_LOG, m_imgCamera_single);
}BEGIN_MESSAGE_MAP(WinLogin, CDialogEx)ON_WM_PAINT()ON_WM_TIMER()ON_BN_CLICKED(IDC_BUTTON1, &WinLogin::OnBnClickedButton1)
END_MESSAGE_MAP()// WinLogin 消息处理程序void WinLogin::OnPaint()
{//CPaintDC dc(this); // device context for painting// TODO: 在此处添加消息处理程序代码// 不为绘图消息调用 CDialogEx::OnPaint()drawPicOnPait(&m_imgBG, this, 0, 0); // 在当前窗口(登录窗口)的(0,0)位置绘制IDB_PNG3
}// 进行人脸识别
// 基本思路:将待识别的人脸 face_check 与user库中的所有图片进行一一对比,
//           然后返回相似度80%以上的图片名称。
BOOL FaceCheck(cv::Mat face_check, char* res_filename) {// 存放所有的文件名std::vector<CString> filename_all;WIN32_FIND_DATA filedata;HANDLE file = FindFirstFile(L"users/*.jpg", &filedata);// 找到users目录下的第一个文件if (file != INVALID_HANDLE_VALUE) {do {filename_all.push_back(filedata.cFileName);} while (FindNextFile(file, &filedata));// 逐个文件进行对比char filepath_single[100]; // 单个图片的库路径for (int i = 0; i < filename_all.size(); i++) {// 将 CString 转换成 char*,获取单个图片的库路径USES_CONVERSION;char* filename_char = T2A(filename_all[i]);sprintf_s(filepath_single, sizeof(filepath_single), "users/%s", filename_char);// 读取users库中的人脸cv::Mat face_USER = cv::imread(filepath_single, 1);// 进行人脸对比,并返回结果if (faceCompare(faceModel, face_check, face_USER) >= 0.80) {// 去掉后缀“.jpg”std::string tmp_str{ filename_char };std::string res_str = tmp_str.substr(0, tmp_str.size() - 4);// 返回识别到的人脸信息//strcpy_s(res_filename, sizeof(res_filename), res_str.c_str());strcpy_s(res_filename, sizeof(res_str)+1, res_str.c_str());return true;}}}else {::MessageBox(NULL, _T("人脸库为空!\n请先进行注册!"), _T("警告"), MB_OK | MB_ICONEXCLAMATION);//c++中MessageBox弹窗的用法大全:https://blog.csdn.net/LCR2025/article/details/129223538}return false;
}// 自动生成定时器的中断函数
void WinLogin::OnTimer(UINT_PTR nIDEvent)
{// TODO: 在此添加消息处理程序代码和/或调用默认值static int snowIndex = 0; // 雪花图片的编号索引static int count_timer = 0; // 初始化定时器计数static char res_filename[100]{ "" }; // 人脸识别结果(图片的名称)if (nIDEvent == 1) {m_imgSnow_signal.SetBitmap(m_imgsnows[snowIndex]); // 更新图片控件的显示snowIndex = (snowIndex + 1) % 16; // 更新雪花图片索引。对16取余是因为16张雪花照片重复。}else if (nIDEvent == 2) {cv::Mat cam_frame;       // 定义摄像头单帧cap_WinLog >> cam_frame; // 获取摄像头拍摄的单帧imshow("m_imgCamera_single", cam_frame); // 显示画面count_timer++;if (count_timer == 1000 / 20) { // 1000意为1000mscount_timer = 0;                        // 清零计数KillTimer(2);                           // 关闭定时器2cap_WinLog.release();                   // 关闭摄像头cv::destroyWindow("m_imgCamera_single");// 关闭摄像头显示窗口// 进行人脸识别if (FaceCheck(cam_frame, res_filename)) {// 播放登录成功提示音mciSendString(L"play res/login.mp3", 0, 0, 0);// 存储用户信息char* context = NULL;strcpy_s(user_job, sizeof(user_job), strtok_s(res_filename, "-", &context));strcpy_s(user_name, sizeof(user_name), strtok_s(NULL, "-", &context));user_yanzhi = atoi(strtok_s(NULL, "-", &context)); // 字符串转整数user_logined = true; // 表明用户成功登录// 正常关闭“登录窗口”::MessageBox(NULL, _T("登录成功!"), _T("提示"), MB_OK | MB_ICONASTERISK);//c++中MessageBox弹窗的用法大全:https://blog.csdn.net/LCR2025/article/details/129223538CDialogEx::OnOK(); // 关闭后就会跳转到“启动窗口”中的“登录按钮”函数中}else {// 登陆失败提示窗口::MessageBox(NULL, _T("登录失败!"), _T("错误"), MB_OK | MB_ICONHAND);//c++中MessageBox弹窗的用法大全:https://blog.csdn.net/LCR2025/article/details/129223538// 重新回到登录窗口user_logined = false;SetTimer(1, 30, NULL); // 启动雪花视频定时器return;}}}CDialogEx::OnTimer(nIDEvent);
}void WinLogin::OnBnClickedButton1()
{// 关闭雪花定时器KillTimer(1);// 将雪花图片更换成墙纸HBITMAP bg_wall = (HBITMAP)LoadImage(NULL, L"res/wall.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);m_imgSnow_signal.SetBitmap(bg_wall);// 将opencv的窗体嵌入到图片控件m_imgCamera_single中cv::namedWindow("m_imgCamera_single", cv::WINDOW_AUTOSIZE);     // 打开一个opencv窗口,注意名称要与图片控件一致// 第二个选项是cv::WindowFlags:https://vovkos.github.io/doxyrest-showcase/opencv/sphinxdoc/enum_cv_WindowFlags.htmlHWND hWnd = (HWND)cvGetWindowHandle("m_imgCamera_single");      // 获取opencv窗口句柄HWND hParent = ::GetParent(hWnd);                               // 获取opencv窗口的父窗口句柄::SetParent(hWnd, GetDlgItem(IDC_STATIC_CAMERA_LOG)->m_hWnd);   // 将opencv窗口的句柄设置为图片控件的句柄::ShowWindow(hParent, SW_HIDE);                                 // 隐藏原父窗口// 打开默认摄像头0cap_WinLog.open(0);if (!cap_WinLog.isOpened()) {::MessageBox(NULL, _T("摄像头打开失败!\n请检查摄像头是否正确连接并开启!"), _T("警告"), MB_OK | MB_ICONEXCLAMATION);//c++中MessageBox弹窗的用法大全:https://blog.csdn.net/LCR2025/article/details/129223538return;}// 开启摄像头的定时器SetTimer(2, 20, NULL);
}

13. HOME窗口

上一节登录成功后会跳回到“启动窗口”,但实际上应该跳转到“HOME窗口”。

本节步骤:

  1. 配置vlc多媒体环境。
  2. 完成HOME窗口功能。首先要添加VideoPlay.h/.cpp文件,然后还需要将HOME窗口所有的控件都更改ID、添加变量。注意右侧的图片控件既充当背景,也充当视频播放窗口。然后就是添加OnPaint()开始写代码。
  • HOME窗口所有控件的ID和变量名:

IDC_HOME_HEAD/m_img_headpic
IDC_HOME_NAME/m_text_name
IDC_HOME_JOB/m_text_job
IDC_HOME_YANZHI/m_text_yanzhi
IDC_HOME_Q_COIN/m_text_q_coin
IDC_HOME_ID/m_text_id
IDC_HOME_PLAY_PAUSE/m_btn_play_pause
IDC_HOME_VIDEO/m_img_video

  • 带水印的照片是两张照片合成,可以参考“OpenCV如何叠加大小不同的图片”。
  • 视频使用VLC多媒体播放器。
    vlc官网:https://www.videolan.org/
    下载vlc-sdk:http://download.videolan.org/pub/videolan/vlc/
图23 配置vlc多媒体环境
图24 完成HOME窗口功能

WinHome.h

#pragma once
#include "afxdialogex.h"
#include "ButtonPNG.h"
#include "VideoPlayer.h"// WinHome 对话框
class WinHome : public CDialogEx {DECLARE_DYNAMIC(WinHome)
public:WinHome(CWnd* pParent = nullptr);   // 标准构造函数virtual ~WinHome();// 对话框数据
#ifdef AFX_DESIGN_TIMEenum { IDD = IDD_HOME };
#endifprotected:virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持DECLARE_MESSAGE_MAP()
public:// HOME窗口的控件CStatic m_img_headpic;CStatic m_text_name;CStatic m_text_job;CStatic m_text_yanzhi;CStatic m_text_q_coin;CStatic m_text_id;ButtonPNG m_btn_play_pause;CStatic m_img_video;CImage cimg_head;     // 左上角的头像显示需要先读取CImage cimg_video_bg; // 视频显示的初始化背景CImage btn_bg;        // 按钮背景,防止按下按钮之后就看不见按钮了// 存储要显示的用户信息char user_name[64]; // HOME窗口要显示的姓名char user_job[64];  // HOME窗口要显示的工作int  user_yanzhi;   // HOME窗口要显示的颜值// HOME窗口的函数BOOL OnInitDialog(); // 定义初始化函数afx_msg void OnPaint();afx_msg void OnBnClickedHomePlayPause();// 存储播放器相关的VideoPlayer m_player; // VideoPlayer.h封装好的播放器类型int status_player{ 0 }; // 播放器状态
};

WinHome.cpp

// WinHome.cpp: 实现文件
// 
#include "pch.h"
#include "face_recognition.h"
#include "afxdialogex.h"
#include "WinHome.h"
#include <opencv2/opencv.hpp>// WinHome 对话框
IMPLEMENT_DYNAMIC(WinHome, CDialogEx)WinHome::WinHome(CWnd* pParent /*=nullptr*/): CDialogEx(IDD_HOME, pParent)
{}WinHome::~WinHome() {}void WinHome::DoDataExchange(CDataExchange* pDX)
{CDialogEx::DoDataExchange(pDX);DDX_Control(pDX, IDC_HOME_HEAD, m_img_headpic);DDX_Control(pDX, IDC_HOME_NAME, m_text_name);DDX_Control(pDX, IDC_HOME_JOB, m_text_job);DDX_Control(pDX, IDC_HOME_YANZHI, m_text_yanzhi);DDX_Control(pDX, IDC_HOME_Q_COIN, m_text_q_coin);DDX_Control(pDX, IDC_HOME_ID, m_text_id);DDX_Control(pDX, IDC_HOME_PLAY_PAUSE, m_btn_play_pause);DDX_Control(pDX, IDC_HOME_VIDEO, m_img_video);
}BEGIN_MESSAGE_MAP(WinHome, CDialogEx)ON_WM_PAINT()ON_BN_CLICKED(IDC_HOME_PLAY_PAUSE, &WinHome::OnBnClickedHomePlayPause)
END_MESSAGE_MAP()// WinHome 消息处理程序BOOL WinHome::OnInitDialog() {// 父类的(同名)初始化方法CDialogEx::OnInitDialog();// 设置窗口大小SetWindowPos(NULL, 0, 0, 1080 + 25, 609 + 70, SWP_NOMOVE);// 设置窗口标题SetWindowText(L"HOME窗口");// 左上角显示头像int face_width{ 200 }, face_height{ 150 };  // 设置头像大小char filepath[256];                         // 定义注册照片(背景)路径sprintf_s(filepath, sizeof(filepath), "users/%s-%s-%d.jpg", user_job, user_name, user_yanzhi);cv::Mat img_bg, img_logo;img_bg = cv::imread(filepath);                                          // 加载注册照img_logo = cv::imread("res/logo.png");                                  // 加载水印cv::Mat imgROI = img_bg(cv::Rect(0, 0, img_logo.cols, img_logo.rows));  // 要合成的区域addWeighted(imgROI, 0.5, img_logo, 0.5, 0, imgROI);                     // 合成图片cv::resize(img_bg, img_bg, cv::Size{ face_width,face_height });         // 缩放图片cv::imwrite("res\\tmp_home_face.jpg", img_bg);                          // 保存合成后的图片::MoveWindow(m_img_headpic.m_hWnd, 20, 20, face_width, face_height, 1); // 调整头像显示窗口的位置(20,20)和大小cimg_head.Load(L"res\\tmp_home_face.jpg");                             // 读取头像m_img_headpic.SetBitmap((HBITMAP)cimg_head);                           // 显示头像// 定义5个标签的显示CFont font_home;font_home.CreatePointFont(500, L"宋体", NULL);     // 5个标签的字体int pos_x{ 40 }, pos_y{ 200 }, pos_interval{ 40 }; // 5个标签的位置参数int text_width{ 150 }, text_height{ 30 };          // 5个标签的大小CString text_tmp;                                  // 临时存储标签的显示内容text_tmp = (CString)"姓名:" + (CString)user_name;                          // 生成内容m_text_name.SetWindowText(text_tmp);                                        // 显示内容m_text_name.SetFont(&font_home);                                            // 使用设置的字体::MoveWindow(m_text_name.m_hWnd, pos_x, pos_y, text_width, text_height, 0); // 调整位置和大小text_tmp = (CString)"工作:" + (CString)user_job;                                       // 生成内容m_text_job.SetWindowText(text_tmp);                                                     // 显示内容m_text_job.SetFont(&font_home);                                                         // 使用设置的字体::MoveWindow(m_text_job.m_hWnd, pos_x, pos_y+pos_interval, text_width, text_height, 0); // 调整位置和大小text_tmp.Format(L"颜值:%d", user_yanzhi);                                                     // 生成内容m_text_yanzhi.SetWindowText(text_tmp);                                                         // 显示内容m_text_yanzhi.SetFont(&font_home);                                                             // 使用设置的字体::MoveWindow(m_text_yanzhi.m_hWnd, pos_x, pos_y + pos_interval*2, text_width, text_height, 0); // 调整位置和大小m_text_q_coin.SetWindowText(L"Q币:99,999,999");                                               // 显示内容m_text_q_coin.SetFont(&font_home);                                                             // 使用设置的字体::MoveWindow(m_text_q_coin.m_hWnd, pos_x, pos_y + pos_interval*3, text_width, text_height, 0); // 调整位置和大小m_text_id.SetWindowText(L"ID:NB0001");                                                    // 显示内容m_text_id.SetFont(&font_home);                                                             // 使用设置的字体::MoveWindow(m_text_id.m_hWnd, pos_x, pos_y + pos_interval*4, text_width, text_height, 0); // 调整位置和大小// 左下角显示按钮m_btn_play_pause.Init(IDB_PNG12, 4, BTN_TYPE_NORMAL);        // 按钮的初始化::MoveWindow(m_btn_play_pause.m_hWnd, 80, 430, 120, 120, 0); // 调整按钮的位置(80,430)和大小(120x120)// 右侧视频播放界面的背景图片的初始化::MoveWindow(m_img_video.m_hWnd, 1080 - 802, 0, 802, 609, 1); // 调整窗口大小cimg_video_bg.Load(L"res/videoBG.png");                       // 读取背景m_img_video.SetBitmap((HBITMAP)cimg_video_bg);                // 显示背景// 初始化视频播放器videoPlayerInit(&m_player);// 按钮背景btn_bg.Load(L"res/boardBg.bmp");return 0;
}// 绘制函数
void WinHome::OnPaint() {drawPicOnPait(&btn_bg, this, 450, 0); // 绘制按钮背景
}// 按钮点击函数
void WinHome::OnBnClickedHomePlayPause() {// 开始播放if (status_player == 0) {m_player.hwnd = GetDlgItem(IDC_HOME_VIDEO)->GetSafeHwnd();//videoPlayerPlay(&m_player, "res\\流浪地球2-太空电梯超燃混剪.mp4");videoPlayerPlay(&m_player, "C:\\Users\\14751\\Desktop\\face_recognition\\res\\LLDQ.mp4");//注意这里必须是完整路径,且不能有中文m_btn_play_pause.Init(IDB_PNG11, 4, BTN_TYPE_NORMAL); // 按钮换皮肤status_player = 1;}// 暂停else if (status_player == 1) {m_btn_play_pause.Init(IDB_PNG12, 4, BTN_TYPE_NORMAL); // 按钮换皮肤videoPlayerPause(&m_player); // 暂停视频status_player = 2;}// 继续播放else if (status_player == 2) {m_btn_play_pause.Init(IDB_PNG11, 4, BTN_TYPE_NORMAL); // 按钮换皮肤videoPlayerPause(&m_player); // 继续播放视频status_player = 1;}
}

14. 注册窗口、登录窗口-添加人脸识别方框

最后这一节是我个人感觉人脸识别应该添加一个人脸方框,要不然我怎么知道是否识别到我的脸了呢?所以:

本节步骤:

  1. 添加人脸识别方框。
图25 添加人脸识别方框

FaceTool中的人脸方框标记函数

// 返回图片中识别到的人脸位置
BOOL faceRegion(MHandle handle, cv::Mat img, cv::Rect& face_rect) {// 裁剪图片,使其宽度为4的整数倍(ASFDetectFaces要求)cv::Rect roiRect1(0, 0, img.cols - img.cols % 4, img.rows); // 定义要裁剪的区域(只是因为要求宽度是4的整数倍)cv::Mat cutImg1 = img(roiRect1).clone();       // 得到裁剪好的图片// 检测是否存在人脸ASF_MultiFaceInfo detectedFaces1{ 0 };          // 定义多人脸信息MRESULT res = ASFDetectFaces(handle, cutImg1.cols, cutImg1.rows, ASVL_PAF_RGB24_B8G8R8, (MUInt8*)cutImg1.data, &detectedFaces1);// 若存在人脸就返回第一个人脸信息if (MOK == res && detectedFaces1.faceRect != NULL && detectedFaces1.faceNum) {face_rect.x = detectedFaces1.faceRect[0].left;face_rect.y = detectedFaces1.faceRect[0].top;face_rect.width = detectedFaces1.faceRect[0].right - detectedFaces1.faceRect[0].left;face_rect.height = detectedFaces1.faceRect[0].bottom - detectedFaces1.faceRect[0].top;return true;}else {printf("ASFDetectFaces 1 fail: %d\n", res);return false;}
}

  1. 奇牛编程-人脸识别资料.rar ↩︎

  2. 我的人脸识别素材 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/147451.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

知识图谱小白入门(1):neo4j的安装与CQL的使用

文章目录 序一、安装neo4j1.1 下载neo4j1.2 安装JDK1.3 BUG&#xff1a;dbms failed to start 二、CQL语法2.1 CQL语法创建节点查询节点创建关系查询关系2.2 习题 习题答案 序 知识图谱&#xff0c;是一种实体间的信息与关系知识的网状结构&#xff0c;借用图论中点与边的概念…

自动驾驶中的感知模型:实现安全与智能驾驶的关键

自动驾驶中的感知模型&#xff1a;实现安全与智能驾驶的关键 文章目录 引言感知模型的作用感知模型的技术安全与挑战结论 2023星火培训【专项营】Apollo开发者社区布道师倾力打造&#xff0c;包含PnC、新感知等的全新专项课程上线了。理论与实践相结合&#xff0c;全新的PnC培训…

力扣练习——链表在线OJ

目录 提示&#xff1a; 一、移除链表元素 题目&#xff1a; 解答&#xff1a; 二、反转链表 题目&#xff1a; 解答&#xff1a; 三、找到链表的中间结点 题目&#xff1a; 解答&#xff1a; 四、合并两个有序链表&#xff08;经典&#xff09; 题目&#xff1a; 解…

opencv实现目标跟踪及视频转存

创建跟踪器 def createTypeTracker(trackerType): 读取视频第一帧&#xff0c;选择跟踪的目标 读第一帧。 ok, frame video.read() 选择边界框 bbox cv2.selectROI(frame, False) 初始化跟踪器 tracker_type ‘MIL’ tracker createTypeTracker(tracker_type) 用第一…

SpringBoot的全局异常拦截

在 Spring Boot 中&#xff0c;可以通过使用 ControllerAdvice 注解和 ExceptionHandler 注解来实现全局异常拦截。 RestControllerAdvice RestControllerAdvice 是 Spring Framework 提供的注解&#xff0c;用于定义全局异常处理类&#xff0c;并且结合 ExceptionHandler 注…

Rabbitmq安装-docker版

1.简介 2.安装消息队列 下载地址https://www.rabbitmq.com/download.html 使用docker方式安装 需要先下载docker&#xff0c;参考文章https://blog.csdn.net/weixin_43917045/article/details/104747341?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22arti…

基于matlab创作简易表白代码

一、程序 以下是一个基于MATLAB的简单表白代码&#xff1a; % 表白代码 clc; % 清除命令行窗口 clear; % 清除所有变量 close all; % 关闭所有图形窗口 % 输入被表白者的名字 name input(请输入被表白者的名字&#xff1a;, s); % 显示表白信息 fprintf(\n); fprintf(亲爱的…

力扣刷题-哈希表-三数之和

15 三数之和 给你一个包含 n 个整数的数组 nums&#xff0c;判断 nums 中是否存在三个元素 a&#xff0c;b&#xff0c;c &#xff0c;使得 a b c 0 &#xff1f;请你找出所有满足条件且不重复的三元组。 注意&#xff1a; 答案中不可以包含重复的三元组。 示例&#xff1a…

由于计算机中丢失msvcp110.dll的解决方法与msvcp110.dll丢失修复方法

相信大家在打开电脑软件或许游戏都有遇到过电脑提示找不到msvcp110.dll文件&#xff0c;导致软件游戏打不开&#xff0c;我们应该怎么办&#xff1f;不用着急&#xff0c;今天小编我分享我找了很久成功解决问题的方法给大家&#xff0c;希望可以帮到各位。 1. 使用DLL修复工具&…

【word】从正文开始设置页码

在写报告的时候&#xff0c;会要求有封面和目录&#xff0c;各占一页。正文从第3页开始&#xff0c;页码从正文开始设置 word是新建的 分出三节&#xff08;封面、目录、正文&#xff09; 布局--->分割符--->分节符--->下一页 这样就能将word分为3节&#xff0c;分…

基于SSM的餐厅点菜管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

京东数据产品:8月大家电市场增长类目市场数据分析

上期我们已经分析了大家电市场及市场中的头部类目&#xff0c;从大家电的市场数据可知&#xff0c;整个行业大盘及多数细分市场都呈下滑走势。不过&#xff0c;仍有部分偏向精致生活的电器呈上升走势&#xff0c;如洗烘套装、内衣清洗机、衣物护理机等&#xff0c;下面我们一起…

案例突破——再探策略模式

再探设计模式 一、背景介绍二、 思路方案三、过程1. 策略模式基本概念2. 策略模式类图3. 策略模式基本代码策略类抽象策略类Context类客户端 4. 策略模式还可以进行优化的地方5. 对策略模式的优化&#xff08;配置文件反射&#xff09; 四、总结五、升华 一、背景介绍 在做项目…

多线程(如何理解pthread库)

上一节&#xff0c;我们主要介绍了pthread库中一些常见函数的用法&#xff0c;这节我们主要分析一下pthread库到底是什么&#xff1f; 什么是库 我们之前提过&#xff0c;在每一个linux平台下&#xff0c;必定会存在对应的pthread库 它存在于/lib64这个路径底下 换句话说&am…

辅助驾驶功能开发-测试篇(2)-真值系统介绍

1 真值系统概述 1.1 真值评测系统核心应用 快速构建有效感知真值,快速完成感知性能评估,快速分析感知性能缺陷。 主要应用场景包括: 1. 感知算法开发验证: 在算法开发周期中,评测结果可以作为测试报告的一部分,体现算法性能的提升。 2. 遴选供应…

我的企业证书是正常的但是下载应用app到手机提示无法安装“app名字”无法安装此app,因为无法验证其完整性解决方案

我的企业证书是正常的但是下载应用app到手机提示无法安装“app名字”无法安装此app&#xff0c;因为无法验证其完整性解决方案 首先&#xff0c;确保您从可信任的来源下载并安装企业开发者签名过的应用程序。如果您不确定应用程序的来源&#xff0c;建议您联系应用程序提供者…

linux下查找文件的相关命令

linux下查找文件的相关命令 运行环境&#xff1a;centos7 参考来源&#xff1a;man、鸟哥入门书籍 一、脚本文件查找&#xff1a;which/type 1. which man手册描述&#xff1a; 返回当前环境可以被执行的文件&#xff08;或链接&#xff09;的路径。搜索PATH变量匹配参数中…

【PostgreSQL】【存储管理】表和元组的组织方式

外存管理负责处理数据库与外存介质(PostgreSQL8.4.1版本中只支持磁盘的管理操作)的交互过程。在PostgreSQL中&#xff0c;外存管理由SMGR(主要代码在smgr.c中)提供了对外存的统一接口。SMGR负责统管各种介质管理器&#xff0c;会根据上层的请求选择一个具体的介质管理器进行操作…

十四天学会C++之第二天(函数和库)

1. 函数的定义和调用 在C中&#xff0c;函数是组织和结构化代码的关键工具之一。它们允许您将一段代码封装成一个可重复使用的模块&#xff0c;这有助于提高代码的可读性和维护性。 为什么使用函数&#xff1f; 函数在编程中的作用不可小觑。它们有以下几个重要用途&#xf…

WEB3 solidity 带着大家编写测试代码 操作订单 创建/取消/填充操作

好 在我们的不懈努力之下 交易所中的三种订单函数已经写出来了 但是 我们只是编译 确认了 代码没什么问题 但还没有实际的测试过 这个测试做起来 其实就比较的麻烦了 首先要有两个账号 且他们都要在交易所中有存入 我们还是先将 ganache 的虚拟环境启动起来 然后 我们在项目…