基于MFC和OpenCV实现人脸识别

基于MFC和OpenCV实现人脸识别

文章目录

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

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

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


0. 项目说明

实现效果

图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 修改背景图片

  显然,Rock的美工都太抽象了。于是我打算借鉴《流浪地球》的题材更新一下背景图片。于是首先总结一下当前的图片显示方法:

/************方法一:ButtonPNG加载png-从项目资源添加************/
// 1. ButtonPNG加载png-从项目资源添加
// 头文件-窗口类的定义
CImage m_imgBG;
// 初始化函数OnInitDialog()
LoadPicture(m_imgBG, IDB_PNG1, L"PNG",); // 初始化背景图片
// 绘制函数OnPaint()
drawPicOnPait(&m_imgBG, this, 0, 0); // 在当前窗口(启动窗口)绘制背景图片/************方法二:系统方法加载bitmap-从项目资源添加************/
// 2. 系统方法加载bitmap-从项目资源添加
// 头文件-窗口类的定义
CStatic m_imgBG;
// 初始化函数OnInitDialog()
CBitmap bmp_read;
bmp_read.LoadBitmap(IDB_BITMAP13);                // 读取背景
::MoveWindow(m_imgBG.m_hWnd, 0, 0, 1080, 609, 1); // 调整图片控件显示大小
m_imgBG.SetBitmap(bmp_read);                      // 显示背景
bmp_read.Detach();                                // 分离变量。这行代码必须写!/************方法三:系统方法加载任意资源-从文件路径添加************/
// 3. 系统方法加载bitmap-从文件路径添加
// 头文件-窗口类的定义
CStatic m_imgSnow_single;                   // 定义单张雪花图片变量
HBITMAP m_imgsnows[16];                     // 定义存储所有雪花图片的数组//
// 初始化函数OnInitDialog()
CString filename_snows; // 存储文件名(MFC提供的类型)
filename_snows.Format(L"res/snow/snow_%d.bmp", i); // 定义每个图片的文件名
m_imgsnows[i] = (HBITMAP)LoadImage(0, filename_snows, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); // 将对应的雪花图片存储到数组中
// 任意位置-要显示的时候
m_imgSnow_single.SetBitmap(m_imgsnows[snowIndex]); // 更新图片控件的显示/************方法四:系统方法加载任意资源--从文件路径添加************/
// 4. 系统方法加载任意资源--从文件路径添加
// 头文件-窗口类的定义
CStatic m_img_video;  // 控件的变量名(“添加变量”自动生成)
CImage cimg_video_bg; // 这个必须在头文件定义
// 初始化函数OnInitDialog()
::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);                // 显示背景
  1. 外部方法,只能从资源中读取PNG,无需图片控件,不会遮挡按钮。
  2. 系统方法,只能从资源中读取bitmap,需要图片控件,会遮挡按钮。
  3. 系统方法,只能从路径中读取bitmap,需要图片控件,会遮挡按钮。
  4. 系统方法,只能从路径中读取任意图片,需要图片控件,会遮挡按钮。

总结:后三种方法没有本质上的区别凡是充当背景板的图片,都采用ButtonPNG中提供的方法。涉及到动态显示的图片,则采用系统方法
鸣谢:B站UP 60帧小弟 的视频 “【流浪地球2\杜比视界·全景声\4K\60帧】太空电梯超燃混剪!”。

图26 窗口跳转实际图

于是,修改完背景图片之后的效果就如下图所示(素材见1,没有“original”标记的都是可以在项目中使用的)。不过,更换背景都比较简单,在制作新的按钮图片时,由于原图的大小不一定是600x45,所以需要调整图片尺寸,但是不建议使用PS,因为PS会破坏的图片的透明通道(本人PS小白),所以建议还是用OpenCV的代码来直接缩放图片,最后的效果很好:

// 本代码用于缩放带透明通道的图片,需要配置OpenCV4.x环境
#include <opencv2/opencv.hpp>int main() {// 读取带有透明通道的图像cv::Mat img1 = cv::imread("res/btn-shoot-original.png", cv::IMREAD_UNCHANGED);// 指定目标图像的大小int newWidth = 600; // 新的宽度int newHeight = 45; // 新的高度// 创建一个带有透明通道的目标图像cv::Mat resizedImage(newHeight, newWidth, CV_8UC4, cv::Scalar(0, 0, 0, 0));// 缩放图像(保持透明通道信息)cv::resize(img1, resizedImage(cv::Rect(0, 0, newWidth, newHeight)), cv::Size(newWidth, newHeight));//cv::resize(img1, img1, cv::Size{ 600,45 });cv::imshow("窗口", resizedImage);cv::imwrite("res/btn-shoot.png", resizedImage);cv::waitKey(0);return 0;
}

注:项目文件夹中的“VS_kill.bat”,是清除中间编译文件的批处理工具
参考链接:[推荐] Visual Studio项目清理(批处理)


未解决:
最后,这个MFC显示窗口的大小貌似有点玄学,并不完全是跟着图片大小走的。比如“启动窗口”的背景图片大小为1080x609,但若设置窗口大小也为1080x609就会白边??


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

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

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

相关文章

geecg-uniapp 源码下载运行 修改端口号 修改tabBar 修改展示数据

APP体验&#xff1a; http://jeecg.com/appIndex技术官网&#xff1a; http://www.jeecg.com安装文档&#xff1a; 快速开始 JeecgBoot 开发文档 看云视频教程&#xff1a; 零基础入门视频官方支持&#xff1a; http://jeecg.com/doc/help 一&#xff0c;下载安装 源码下载…

Linux防火墙之--SNAT和DNAT

1.SNAT是什么 SNAT又称源地址转换。源地址转换是内网地址向外访问时&#xff0c;发起访问的内网ip地址转换为指定的ip地址&#xff08;可指定具体的服务以及相应的端口或端口范围&#xff09;&#xff0c;这可以使内网中使用保留ip地址的主机访问外部网络&#xff0c;即内网的多…

基于SpringBoot的靓车汽车销售网站

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 车辆展示管理 车辆品牌管理 用户交流管理 购物车 用户交流 我的订单管理 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的…

【Jmeter】性能测试脚本开发——性能测试环境准备、Jmeter脚本编写和执行

文章目录 一、常用的Jmeter元件二、性能测试环境准备三、编写Jmeter脚本四、执行测试脚本 一、常用的Jmeter元件 取样器-HTTP请求 作用&#xff1a;发送HTTP请求配置原件-HTTP请求默认值 作用&#xff1a;设置HTTP请求的默认参数配置原件-用户定义的变量 作用&#xff1a;定义…

PyTorch 深度学习实战

文章目录 前言1. 环境安装1.Anaconda2.pytorch cuda 环境3.测试 前言 1. 环境安装 1.Anaconda 可以参考这里&#xff1a;Anaconda学习 2.pytorch cuda 环境 我是按照下面的博客一步步完成&#xff0c;亲测有效 Pytorch安装教程&#xff08;最全最详细版&#xff09; 我的…

ArcMap:第二届全国大学生GIS技能大赛(广西师范学院)详解-上午题

目录 01 题目 1.1 第一小题 1.2 第二小题 1.3 第三小题 1.4 数据展示 02 思路和实操 2.1 第一问思路 2.2 第一问操作过程 2.2.1 地理配准 2.2.2 镶嵌 2.2.2.1 第一种镶嵌方法 2.2.2.2 第二种镶嵌方法 2.2.3 裁剪 2.2.4 DEM信息提取 2.2.5 分类 2.3 第二问思路 …

等精度频率计verilog,quartus仿真视频,原理图,代码

名称&#xff1a;等精度频率计设计verilog quartus仿真 软件&#xff1a;Quartus 语言&#xff1a;Verilog 要求&#xff1a; A&#xff1a;测量范围信号:方波 频率:100Hz~1MHz; B&#xff1a;测试误差:<0.1%(全量程) C&#xff1a;时钟频率:50kHz D&#xff1a;预闸…

Oracle SQL Developer 中查看表的数据和字段属性、录入数据

在Oracle SQL Developer中&#xff0c;选中一个表时&#xff0c;右侧会列出表的情况&#xff1b;第一个tab是字段的名称、数据类型等属性&#xff1b; 切换到第二个tab&#xff0c;显示表的数据&#xff1b; 这和sql server management studio不一样的&#xff1b; 看一下部门…

计算机专业毕业设计项目推荐14-文档编辑平台(SpringBoot+Vue+Mysql)

文档编辑平台&#xff08;SpringBootVueMysql&#xff09; **介绍****各部分模块实现** 介绍 本系列(后期可能博主会统一为专栏)博文献给即将毕业的计算机专业同学们,因为博主自身本科和硕士也是科班出生,所以也比较了解计算机专业的毕业设计流程以及模式&#xff0c;在编写的…

十二、Django之模板的继承+用户列表

模板的继承 新建layout.html&#xff1a; {% load static %} <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><link rel"stylesheet" href"{% static plugins…

Vue中如何进行图像识别与人脸对比(如百度AI、腾讯AI)

Vue中的图像识别与人脸对比 在现代Web应用程序中&#xff0c;图像识别和人脸对比技术越来越受欢迎。它们可以用于各种用途&#xff0c;如人脸识别门禁系统、图像分类和验证等。百度AI和腾讯AI是两个流行的人工智能平台&#xff0c;它们提供了强大的图像识别和人脸对比API。本文…

凉鞋的 Unity 笔记 106. 第二轮循环场景视图Sprite Renderer

106. 第二轮循环&场景视图&Sprite Renderer 从这一篇开始&#xff0c;我们开始进行第二轮循环。 这次我们至少能够在游戏运行窗口看到一些东西。 首先还是在场景层次窗口进行编辑&#xff0c;先创建一个 Sprite&#xff0c;操作如下: 创建后&#xff0c;会在 Scene …