基于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,但是其中有一些命名错误可能会导致程序调用失败:
- 雷军图片“neijun.jpg”–应为–>“leijun.jpg”
- 音频文件“zhuche.mp3”–应为–>“zhuce.mp3”
- 《我的人脸识别素材》2
注:工程及代码文件会放在本人的Github仓库。
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完成功能。
- Visual Studio:使用里面的MFC框架完成窗口的制作。
- OpenCV:完成摄像头获取图片等基本的图片操作。
- 虹软SDK:根据OpenCV获取的图片数据,完成 离线人脸识别 (仅初次使用需联网激活)。
- vlc:多媒体播放器,完成最后的视频播放等功能。
比较老的游戏或国企项目还在使用MFC进行开发,而现如今更火的是Qt,但本项目还是先采用MFC框架。另外,整个项目开发过程中,我尽量按照课程所述进行,但是有很多素材实在是太抽象了,所以小部分素材我会自行替换。另外,代码这玩意越学越熟,所以一开始的几节笔记都写的很详细,一个窗口能唠好几节,后面可能一个窗口就用一节写完了。
代码说明
本笔记中会给出一些代码,但要注意的是代码具有迭代性,随着功能的增多会不断加入新的代码,所以想看全部的源代码建议直接到本人的Github仓库下载。下面所提及的每一个代码都包括.h/.cpp两个文件:
外部添加代码
- ButtonPNG:用于美化按钮的显示。
- faceTool:使用虹软人脸识别SDK完成人脸识别功能。
- VideoPlayer:使用vlc的SDK完成视频的播放、暂停、退出等。
剩下的都是窗口的“添加类”:
- face_recognition:整个项目的主函数,自动生成。
- face_recognitionDlg:“启动窗口”的函数,自动生成。
- WinLogin:“登录窗口”的函数,“添加类”生成主体。
- WinRegister:“注册窗口”的函数,“添加类”生成主体。
- WinWelcome:“欢迎窗口”的函数,“添加类”生成主体。
- WinHome:“HOME窗口的函数”,“添加类”生成主体。
其余的代码文件暂时不需要了解太多。
1. 创建项目
本节创建基本的MFC项目,属于是先搭建一个基本的“舞台”:
本节步骤:
- 给Visual Studio安装MFC框架(默认不安装)。
- 创建MFC项目。
参考文章
- “【Visual Studio 2019】创建 MFC 桌面程序”
- “VS新建项目时,名称与解决方案名称的区别”
2. 启动窗口
本节配置启动窗口。
本节步骤:
- 添加素材。将奇牛编程的图片素材解压,然后粘贴到项目的资源文件夹
./face_recognition/res/中。- 通过“图片控件”添加背景图片。
- 通过“按钮控件”添加“注册”、“登录”按钮。
注:
- 使用代码和窗口拖动都可以更改图片的位置。但为了开发迅速,通常使用代码修改会变化的图片(动态图片),而使用MFC控件设置不会变化的图片(静态图片)。个人体会是能不用窗口就不用窗口,这玩意的大小和显示范围有可能会自己变,一个字,不好使!
- 本节还不会使用代码控制控件位置,所以就先拖动。
3. 登录窗口-添加窗口、从启动窗口跳转
本节步骤:
- 创建登录窗口。直接复制前面的“启动窗口”。
- 添加“登录窗口”的“刷脸”按钮。
- 设置“启动窗口”单击“登录”跳转到“登录窗口”。
下图首先生成“登录窗口”的头文件和源文件(前三图),然后进入“启动窗口”的源文件中,设置按钮跳转的代码(后两图)。
// “启动窗口”源文件face_recognitionDlg.cpp
/注意要添加一个头文件//
#include "WinLogin.h"///下面是源文件最后一个函数
// 此函数为单击启动窗口“登录”按钮后的操作
void CfacerecognitionDlg::OnBnClickedButton5Log()
{// 跳转到“登录窗口”WinLogin win_log; // 定义“登录窗口”变量win_log.DoModal(); // 以模态方式呈现出来,也就是必须在当前窗口进行操作,而无法操作其他窗口
}
4. 启动窗口-美化按钮
本节美化按钮,如圆角、半透明、按下变颜色。要实现这一系列的功能,就需要代码来定义一个类,使按钮显示为设计好的图片,然后在不同状态下(如鼠标单击)显示不同的图片,这便是资料中“ButtonPNG.h”和“ButtonPNG.cpp”所做的事情,只是顾名思义,该代码只能识别PNG格式的图片。
本节步骤:
- 美化按钮。添加ButtonPNG代码,然后右键按钮“添加ButtonPNG变量”,最后在窗口的初始化函数中进行ButtonPNG提供的按钮初始化函数,即可完成按钮的美化。
- 同样的方法也将“注册按钮”进行了美化。
注:按钮图片有四个联排:正常状态、鼠标悬停状态、鼠标单击状态、禁止使用状态。
关于“添加变量”的说明:虽然上一节添加了“启动窗口”的“登录按钮”单击后跳转到“登录窗口”的代码,但是这个代码只是从窗口元素控件的角度规定了单击按钮后的动作,并没有创建相应的变量来表示相应的按钮,于是本节需要创建一个变量来表示“启动窗口”的“登录按钮”(该变量会声明在“启动窗口”的头文件中),这个按钮变量被声明为刚才添加的ButtonPNG,于是就可以调用ButtonPNG中的各种方法,包括如何显示按钮。
- 上述将背景图片删除,然后在程序中用代码控制显示。
- 第一行是将“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. 登录窗口-美化按钮、雪花视频
本节步骤:
- 添加并美化“登录窗口”的“刷脸”按钮。
- 使用定时器来实现雪花画面循环。
初始化函数/
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
}
///头文件//
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. 注册窗口-美化按钮、雪花视频、从启动窗口跳转
本节步骤:
- 添加并美化“注册窗口”的“刷脸”按钮。
- 使用定时器来实现雪花画面循环。
- 设置“启动界面”的“注册”按钮跳转到“注册窗口”。
注:由于“注册窗口”和“登录窗口”的功能差不多,所以可以直接复制“登录窗口”的内容,比较轻松。
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);
}
face_recognitionDlg.cpp
///启动窗口的“注册”跳转函数//
void CfacerecognitionDlg::OnBnClickedButtonReg()
{// 跳转到“注册窗口”WinRegister win_reg; // 定义“注册窗口”变量win_reg.DoModal(); // 以模态方式呈现出来,也就是必须在当前窗口进行操作,而无法操作其他窗口
}
7. 注册窗口-开启摄像头
本节步骤:
- 配置opencv4.8.0环境。添加头文件;添加库目录;添加静态库;添加动态库拷贝到可执行文件中。
- 开启摄像头。在注册窗口添加一个图片控件,并更改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
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秒倒计时拍摄
本节步骤:
- 实现3秒倒计时拍摄。主要思路是对中断次数进行计数,完成3s倒计时拍摄。注意要播放3s倒计时的音频文件,还需要还添加多媒体的头文件。
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,其中“颜值”使用用户与“雷军”的人脸识别相似度来计算。
本节步骤:
- 配置虹软人脸识别SDK环境。可以实现离线人脸识别。
- 完成用户注册功能。由于虹软人脸识别SDK的接口还是偏底层,所以Rock还是自己写了“faceTool.h/.cpp”将其封装成更高层的接口,来方便调用。但是Rock一直采用OpenCV2.x版本的旧代码,已经不适用OpenCV4.x这样的新版本了,所以我对它的代码进行了升级并添加了我认为比较详尽的注释。虹软要求先创建一个人脸识别的模块,整个程序只需创建一个。
Rock:“百度云人脸识别SDK效果不是很好、接口也不是很好,所以本项目用虹软人脸识别SDK”。
虹软官网:https://www.arcsoft.com.cn/
SDK说明:奇牛编程素材中的“人脸识别-V3.0.zip”解压后有X86、X64两个版本。或者下面也演示了如何去官网下载。
不同的SDK调用套路都不太一样,可以查看官方示例:
注意前两张图片添加完faceTools后,将其代码改成下面我给出的代码:
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, ©feature1, &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. 欢迎窗口
- 点击“刷脸按钮”后检查编辑框,若为空则提示填写信息。
- 添加注册成功后的欢迎界面。
12. 登录窗口-用户刷脸登录
本节步骤:
- 实现刷脸登录。从注册窗口的摄像头相关代码中复制,即可轻易实现点击“刷脸按钮”1s后,自动抓拍人脸。
注:登录成功的表示暂时先用一个弹窗替代,后续再设置跳转到“HOME”窗口。
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窗口”。
本节步骤:
- 配置vlc多媒体环境。
- 完成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/
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. 注册窗口、登录窗口-添加人脸识别方框
最后这一节是我个人感觉人脸识别应该添加一个人脸方框,要不然我怎么知道是否识别到我的脸了呢?所以:
本节步骤:
- 添加人脸识别方框。
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;}
}
奇牛编程-人脸识别资料.rar ↩︎
我的人脸识别素材 ↩︎
相关文章:
基于MFC和OpenCV实现人脸识别
基于MFC和OpenCV实现人脸识别 文章目录 基于MFC和OpenCV实现人脸识别1. 项目说明1. 创建项目2. 启动窗口3. 登录窗口-添加窗口、从启动窗口跳转4. 启动窗口-美化按钮5. 登录窗口-美化按钮、雪花视频6. 注册窗口-美化按钮、雪花视频、从启动窗口跳转7. 注册窗口-开启摄像头8. 注…...
力扣 -- 377. 组合总和 Ⅳ
解题步骤: 参考代码: class Solution { public:int combinationSum4(vector<int>& nums, int target) {int nnums.size();vector<double> dp(target1);//初始化dp[0]1;//填表for(int i1;i<target;i){for(int j0;j<n;j){//填表if(…...
阿里云新账户什么意思?老用户、产品首购详细说明
阿里云新账户、老账号、产品首购和同人账号什么意思?阿里云账号分为云新账户、老账户、产品首购、同人账号和同一用户,阿里云官方推出的活动很多是限制账号类型的,常见的如阿里云新用户,什么是阿里云新用户?是指从未在…...
C++ YAML使用
C++工程如何使用YAML-cpp 一、前期准备工作 1、已安装minGW、cmake、make等本地工具。 2、下载YAML-cpp第三方开源代码(一定要下载最新的release版本,不然坑很多)。 3、生成YAML-cpp静态库 (1)在yaml-cpp-master下建立build文件夹; (2)在该文件夹下生成MakaFile文…...
十二、Django之模板的继承+用户列表
模板的继承 新建layout.html: {% load static %} <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><link rel"stylesheet" href"{% static plugins…...
wzsc_文件上传(条件竞争)
打开题目链接,很常见的文件上传框 经过尝试,发现上传东西后会调用upload.php,猜测文件被传到upload目录下 随便传了几个类型的文件,访问upload目录 发现.php文件以及.htaccess、.user.ini这种配置文件都没有传上去 但是通过抓包…...
unplugin-vue-components和unplugin-auto-import插件
unplugin-auto-import:自动按需引入 vue\vue-router\pinia 等的 api unplugin-vue-components:自动按需引入 第三方的组件库组件 和 我们自定义的组件 使用此类插件,不需要手动编写import {xxx} from vue这样的代码了,提升开发效…...
docker系列文章目录
docker系列专栏笔记总算完成了,平时下班比较晚,利用晚上的一些时间整理了这一系列的学习笔记。 docker系列教程包含以下几个方面: docker环境篇 介绍docker环境的搭建,已经管理平台工具(portainer)的简单使用。 docker常用命令篇…...
第80步 时间序列建模实战:GRNN回归建模
基于WIN10的64位系统演示 一、写在前面 这一期,我们使用Matlab进行GRNN模型的构建。 使用的数据如下: 采用《PLoS One》2015年一篇题目为《Comparison of Two Hybrid Models for Forecasting the Incidence of Hemorrhagic Fever with Renal Syndrom…...
《C和指针》笔记33:指针数组
除了创建整型数组一样,也可以声明指针数组。 int *api[10];为了弄清这个复杂的声明,我们假定它是一个表达式,并对它进行求值。下标引用的优先级高于间接访问,所以在这个表达式中,首先执行下标引用。因此,a…...
C/C++字符函数和字符串函数详解————内存函数详解与模拟
个人主页:点我进入主页 专栏分类:C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 欢迎大家点赞,评论,收藏。 一起努力,一起奔赴大厂。 目录 1.前言 2 .memcpy函数 3.memmove函…...
CAcUiDockControlBar初始位置 2023/8/19 下午3:51:18
2023/8/19 下午3:51:18 CAcUiDockControlBar初始位置 2023/8/19 下午3:52:00 CAcUiDockControlBar的初始位置是根据其在程序代码中的设置而确定的。通常情况下,它的初始位置可以通过以下几种方式进行设置: 使用Create函数:在创建CAcUiDockControlBar对象时,可以调用Cre…...
CDH6.3.2 的pyspark读取excel表格数据写入hive中的问题汇总
需求:内网通过Excel文件将数据同步到外网的CDH服务器中,将CDH中的文件数据写入hive中。 CDH版本为:6.3.2 spark版本为:2.4 python版本:2.7.5 操作系统:CentOS Linux 7 集群方式:yarn-cluster …...
2120 -- 预警系统题解
Description OiersOiers 国的预警系统是一棵树,树中有 �n 个结点,编号 1∼�1∼n,树中每条边的长度均为 11。预警系统中只有一个预警信号发射站,就是树的根结点 11 号结点,其它 �−1…...
C++入门-day01
一、认识C C融合了三种不同的编程方式 C代表的过程性语言在C基础上添加的类、结构体puls代表的面向对象语言C模板支持泛型编程 C完全兼容C的特性 Tips:侯捷老师提倡的Modren C是指C11、C14、C17和C20这些新标准所引入的一系列新特性和改进。在我们练习的时候也应当去…...
Android开源 Skeleton 骨架屏 V1.3.0
目录 一、简介 二、效果图 三、引用 Skeleton 添加jitpack 仓库 添加依赖: 四、新增 “块”骨架屏 1、bind方法更改和变化: 2、load方法更改和变化: 五、关于上一个版本 一、简介 骨架屏的作用是在网络请求较慢时,提供基础占位&…...
网络资料搬运(2)
(1) Ubuntu 22.04: 为 Ubuntu22.04 系统添加中文输入法 linux解压gz文件的命令 Ubuntu20.04出现Unit ssh.service could not be found 详解使用SSH远程连接Ubuntu服务器系统 Configuring networks(配置网络) (2) Python && OpenCV: …...
SEO搜索引擎
利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名,吸引更多的用户访问网站,提高网站的访问量,提高网站的销售能力和宣传能力,从而提升网站的品牌效应 搜索引擎优化的技术手段 黑帽SEO 通过欺骗技术和滥用搜索算法来推销毫不…...
动态规划-状态机(188. 买卖股票的最佳时机 IV)
状态分类: f[i,j,0]考虑前i只股票,进行了j笔交易,目前未持有股票 所能获得最大利润 f[i,j,1]考虑前i只股票,进行了j笔交易,目前持有股票 所能获得最大利润 状态转移: f[i][j][0] Math.max(f[i-1][j][0],f[…...
银行业务队列简单模拟(队列应用)
设某银行有A、B两个业务窗口,且处理业务的速度不一样,其中A窗口处理速度是B窗口的2倍 —— 即当A窗口每处理完2个顾客时,B窗口处理完1个顾客。给定到达银行的顾客序列,请按业务完成的顺序输出顾客序列。假定不考虑顾客先后到达的时…...
国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...
基于 TAPD 进行项目管理
起因 自己写了个小工具,仓库用的Github。之前在用markdown进行需求管理,现在随着功能的增加,感觉有点难以管理了,所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD,需要提供一个企业名新建一个项目&#…...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
站群服务器的应用场景都有哪些?
站群服务器主要是为了多个网站的托管和管理所设计的,可以通过集中管理和高效资源的分配,来支持多个独立的网站同时运行,让每一个网站都可以分配到独立的IP地址,避免出现IP关联的风险,用户还可以通过控制面板进行管理功…...
从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践
作者:吴岐诗,杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言:融合数据湖与数仓的创新之路 在数字金融时代,数据已成为金融机构的核心竞争力。杭银消费金…...
