• 基于MFC和OpenCV实现人脸识别


    基于MFC和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项目。

    参考文章

    图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(); // 以模态方式呈现出来,也就是必须在当前窗口进行操作,而无法操作其他窗口
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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 
    
    #ifndef ULONG_PTR
    #define ULONG_PTR ULONG
    #endif 
    //#include "Includes/GdiPlus.h"
    #include 
    using namespace Gdiplus;
    #pragma comment(lib,"GdiPlus.lib")
    
    
    //按钮的状态
    enum {
        CTRL_NOFOCUS = 0x01,  //普通
        CTRL_FOCUS,           //mousemove
        CTRL_SELECTED,        //buttondown
        CTRL_DISABLE,         //无效
    };
    
    //图片形式
    enum {
        BTN_IMG_1 = 1,  //
        BTN_IMG_3 = 3,  //3分图(1个图片内有3小图,下同)
        BTN_IMG_4 = 4,  //4分图
    };
    
    //按钮类型
    enum {
        BTN_TYPE_NORMAL = 0x10,  //普通BTN
        BTN_TYPE_MENU,           //菜单类型的BTN
        BTN_TYPE_STATIC,         //静态类型的BTN
    };
    
    //从资源里面加载位图
    bool LoadImageFromResourse(CImage* pImg, UINT nImgID, LPCTSTR lpImgType);
    bool LoadPicture(CImage& bmp, UINT nImgID, LPCTSTR lpImgType = _T("PNG")); //含Alpha通道的图片处理成CImage
    
    void 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85

    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());
        else
            return;
    
        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());
                else
                    return 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());
                else
                    return false;
            }
            else
                return 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; //fail
        DWORD dwSize;
        dwSize=SizeofResource(hInstance,hResInfo); //get resource size(bytes) 
        HGLOBAL hResData;
        hResData=::LoadResource(hInstance,hResInfo);
        if(hResData==NULL)
            return NULL; //fail
        HGLOBAL 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 memory
        
        IStream *pStream;
        HRESULT hr;
        hr=::CreateStreamOnHGlobal(hMem,TRUE,&pStream);//create stream object
        Image *pImage=NULL;
        if(SUCCEEDED(hr)){
            pImage=Image::FromStream(pStream);//get GDI+ pointer
            pStream->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 default
        if(!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;
            else
                m_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的DC
            CDC* pImageDC1 = CDC::FromHandle(pImage->GetDC());//Image因为有自己的DC,所以必须使用FromHandle取得对应的DC
            CBitmap* bitmap1 = pImageDC1->GetCurrentBitmap();
            BITMAP bmpInfo;
            bitmap1->GetBitmap(&bmpInfo);
            //建立新的CImage
            ResultImage->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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    图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张雪花照片重复。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    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);    // 定时器函数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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执行空函数NULL
    
        return 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    图14 设置“启动窗口”的“注册”按钮跳转到“注册窗口”

    face_recognitionDlg.cpp

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

    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环境不需要添加文件
    参考链接:

    图15 配置opencv4.8.0环境
    图16 开启摄像头

    WinRegister.cpp

    // WinRegister.cpp: 实现文件
    //
    
    #include "pch.h"
    #include "face_recognition.h"
    #include "afxdialogex.h"
    #include "WinRegister.h"
    #include 
    #include  // 用到了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.html
        HWND 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);                                 // 隐藏原父窗口
    
        // 打开默认摄像头0
        cap_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/129223538
            return;
        }
    
        // 开启摄像头的定时器
        SetTimer(2, 20, NULL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139

    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 
    #include  // 用到了cvGetWindowHandle
    using namespace cv;
    static cv::VideoCapture cap_WinReg; // 定义注册窗口的摄像头(static只能本文件使用)
    
    // 使用 Windows 多媒体 API 提供的音频和多媒体功能,包括播放声音、音乐和控制多媒体设备。
    #include  // 该头文件包含了许多用于音频、视频、音乐和多媒体设备控制的函数和数据类型的声明。
    #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.html
        HWND 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);                                 // 隐藏原父窗口
    
        // 打开默认摄像头0
        cap_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/129223538
            return;
        }
    
        // 开启摄像头的定时器
        SetTimer(2, 30, NULL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161

    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 
    #include   
    #include 
    #include 
    #include 
    
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128

    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 
    #include  // 用到了cvGetWindowHandle
    
    // 使用 Windows 多媒体 API 提供的音频和多媒体功能,包括播放声音、音乐和控制多媒体设备。
    #include  // 该头文件包含了许多用于音频、视频、音乐和多媒体设备控制的函数和数据类型的声明。
    #pragma comment(lib, "winmm.lib") // 将Windows多媒体库文件 winmm.lib 关联到程序中,以便调用多媒体API函数。
    
    #include "faceTool.h" // 人脸识别模块
    
    #include "WinWelcome.h" // 欢迎界面
    
    #include  // 人脸识别函数FaceCheck
    #include 
    
    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意为1000ms
                count_timer = 0;                        // 清零计数
                KillTimer(2);                           // 关闭定时器2
                cap_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/129223538
                    CDialogEx::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.html
        HWND 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);                                 // 隐藏原父窗口
    
        // 打开默认摄像头0
        cap_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/129223538
            return;
        }
    
        // 开启摄像头的定时器
        SetTimer(2, 20, NULL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222

    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

    图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_TIME
    	enum { IDD = IDD_HOME };
    #endif
    
    protected:
    	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 }; // 播放器状态
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    WinHome.cpp

    // WinHome.cpp: 实现文件
    // 
    #include "pch.h"
    #include "face_recognition.h"
    #include "afxdialogex.h"
    #include "WinHome.h"
    #include 
    
    // 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    后记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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    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 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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


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


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

  • 相关阅读:
    NNDL:作业3:分别使用numpy和pytorch实现FNN例题
    Sentinel网关限流
    centos更改yum源
    HTML5学习系列之项目实战1
    SpringCloudAlibaba之gateway网关
    6月B站和微博达人涨粉榜单,微博涨粉榜一竟是TA
    Sql 函数传递参数 字符串拼接
    Practical Knowledge: Introduction of X METAVERSE PRO Social Follow-up Mode
    linux下最全抓包命令使用方式学习和拓展
    YOLOv7-PTQ量化部署
  • 原文地址:https://blog.csdn.net/weixin_46258766/article/details/133266502