基於虹軟人臉識別,實現RTMP直播推流追蹤影片中所有人臉資訊(C#)

語言: CN / TW / HK

前言

大家應該都知道幾個很常見的例子,比如在張學友的演唱會,在安檢通道檢票時,通過人像識別系統成功識別捉了好多在逃人員,被稱為逃犯剋星;人行橫道不遵守交通規則闖紅燈的路人被人臉識別系統抓拍放在大屏上以示警告;參加某次活動通過人臉進行簽到來統計實時人流量等等, 我現在也來做一個通過電視直播,追蹤畫面中所有人臉資訊,並捕獲我需要的目標人物。

具體思路及流程

我這裡是使用人臉識別V3.0版本Windows(X86)32位的SDK,下載地址虹軟人臉識別,從官網註冊成為開發者,然後新建應用,就會得到APP_ID和SDK_KEY。

使用虹軟SDK對直播畫面中的每一幀圖片進行檢測,得到圖片中所有人臉資訊。可以新增目標人物的照片,用目標人物的人臉特徵值與直播畫面幀圖片中人臉資訊列表中的每一個特徵值進行比對。如果有匹配到目標人物,把直播畫面抓拍。具體流程如下:

專案結構如下,ArcFaceSharp是對虹軟SDK中方法的一些封裝,相對於我的上篇文章基於虹軟人臉識別,實現身份認證和自助髮卡 ,這次封裝了多人臉相關的實體類。

關鍵性程式碼

首先要啟用並初始化人臉識別引擎

/// <summary>
/// 啟用並初始化引擎
/// </summary>
private void ActiveAndInitEngines()
{
    //讀取配置檔案中的 APP_ID 和 SDKKEY
    AppSettingsReader reader = new AppSettingsReader();
    string appId = (string)reader.GetValue("APP_ID", typeof(string));
    string sdkKey = (string)reader.GetValue("SDKKEY", typeof(string));
    int retCode = 0;
    //啟用引擎    
    try
    {
        retCode = ASFFunctions.ASFActivation(appId, sdkKey);
    }
    catch (Exception ex)
    {
        //異常處理
        return;
    }
    #region 圖片引擎pImageEngine初始化
    //初始化引擎
    uint detectMode = DetectionMode.ASF_DETECT_MODE_IMAGE;
    //檢測臉部的角度優先值
    int detectFaceOrientPriority = ASF_OrientPriority.ASF_OP_0_HIGHER_EXT;
    //人臉在圖片中所佔比例,如果需要調整檢測人臉尺寸請修改此值,有效數值為2-32
    int detectFaceScaleVal = 16;
    //最大需要檢測的人臉個數
    int detectFaceMaxNum = 5;
    //引擎初始化時需要初始化的檢測功能組合
    int combinedMask = FaceEngineMask.ASF_FACE_DETECT | FaceEngineMask.ASF_FACERECOGNITION | FaceEngineMask.ASF_AGE | FaceEngineMask.ASF_GENDER | FaceEngineMask.ASF_FACE3DANGLE;
    //初始化引擎,正常值為0,其他返回值請參考http://ai.arcsoft.com.cn/bbs/forum.php?mod=viewthread&tid=19&_dsign=dbad527e
    retCode = ASFFunctions.ASFInitEngine(detectMode, detectFaceOrientPriority, detectFaceScaleVal, detectFaceMaxNum, combinedMask, ref pImageEngine);
    if (retCode == 0)
        this.Text = ("圖片引擎初始化成功!\n");
    else
        this.Text = (string.Format("圖片引擎初始化失敗!錯誤碼為:{0}\n", retCode));
    #endregion
}


我們可以在網上搜索一下電視直播RTMP地址輸入到播放地址中,即可在程式中可進行播放。

/// <summary>
/// 播放影片
/// </summary>
private void PlayVideo()
{
    videoCapture = new VideoCapture(rtmp);
    if (videoCapture.IsOpened())
    {
        videoInfo.Filename = rtmp;
        videoInfo.Width = (int)videoCapture.FrameWidth;
        videoInfo.Height = (int)videoCapture.FrameHeight;
        videoInfo.Fps = (int)videoCapture.Fps;

        myTimer.Interval = videoInfo.Fps == 0 ? 300 : 1000 / videoInfo.Fps;
        IsStartPlay = true;
        myTimer.Start();
    }
    else
    {
        MessageBox.Show("影片源異常");
    }
}


/// <summary>
/// 定時器觸發事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MyTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    try
    {
        if (IsStartPlay)//如果開始播放
        {
            lock (LockHelper)//加鎖
            {
                var frame = videoCapture.RetrieveMat();//獲取影片幀
                if (frame != null)
                {
                    if (frame.Width == videoInfo.Width && frame.Height == videoInfo.Height)
                        this.SetVideoCapture(frame);//設定影片捕獲
                    else
                        LogHelper.Log($"bad frame");
                }
            }
        }
    }
    catch (Exception ex)
    {
        LogHelper.Log(ex.Message);
    }
}


/// <summary>
/// 設定影片捕獲
/// </summary>
/// <param name="frame"></param>
private void SetVideoCapture(Mat frame)
{
    try
    {
        btm = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame);//把影片幀轉換為Bitmap
        pic_Video.Image = btm;//把Bitmap賦給圖片控制元件渲染
    }
    catch (Exception ex)
    {
        LogHelper.Log(ex.Message);
    }
}


以上這些就是在OpenCv中通過VideoCaptrue類對影片進行讀取操作,然後把影象渲染到PictureBox控制元件上。在PictureBox的Paint事件中進行人臉識別與比對操作。

/// <summary>
/// 比對函式,將每一幀抓拍的照片和目標人物照片進行比對
/// </summary>
/// <param name="bitmap"></param>
/// <param name="e"></param>
/// <returns></returns>
private void CompareImgWithIDImg(Bitmap bitmap, PaintEventArgs e)
{
    if (bitmap != null)
    {
        //保證只檢測一幀,防止頁面卡頓以及出現其他記憶體被佔用情況
        if (isLock == false)
        {
            isLock = true;
            Graphics g = e.Graphics;
            float offsetX = (pic_Video.Width * 1f / bitmap.Width);
            float offsetY = (pic_Video.Height * 1f / bitmap.Height);
            //根據Bitmap 獲取人臉資訊列表
            List<FaceInfoModel> list = FaceUtil.GetFaceInfos(pImageEngine, bitmap);
            foreach (FaceInfoModel sface in list)
            {
                //非同步處理提取特徵值和比對,不然頁面會比較卡
                ThreadPool.QueueUserWorkItem(new WaitCallback(delegate
                {
                    try
                    {
                        //提取人臉特徵
                        float similarity = CompareTwoFeatures(sface.feature, imageTemp);
                        if (similarity > threshold)//如果相似度大於0.8f,則表示匹配成功,抓拍圖片
                        {
                            this.pic_cutImg.Image = bitmap;
                            this.Invoke((Action)(() =>
                            {
                                this.lbl_simiValue.Text = similarity.ToString();
                            }));
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }));


                MRECT rect = sface.faceRect;
                float x = rect.left * offsetX;
                float width = rect.right * offsetX - x;
                float y = rect.top * offsetY;
                float height = rect.bottom * offsetY - y;
                //根據Rect進行畫框
                g.DrawRectangle(pen, x, y, width, height);
                trackUnit.message = "年齡:" + sface.age.ToString() + "\r\n" + "性別:" + (sface.gender == 0 ? "男" : "女");
                g.DrawString(trackUnit.message, font, brush, x, y + 5);
            }
            isLock = false;
        }
    }
}


SDK 在對單張圖片進行識別的時候,有可能識別出包含多張人臉,我們用FaceInfoModel人臉資訊實體類,把人臉資訊放在此類中。

public class FaceInfoModel
{
    /// <summary>
    /// 年齡
    /// </summary>
    public int age { get; set; }
    /// <summary>
    /// 性別
    /// </summary>
    public int gender { get; set; }
    public ASF_Face3DAngle face3dAngle { get; set; }
    /// <summary>
    /// 人臉框
    /// </summary>
    public MRECT faceRect { get; set; }
    /// <summary>
    /// 人臉角度
    /// </summary>
    public int faceOrient { get; set; }
    /// <summary>
    /// 單人臉特徵
    /// </summary>
    public IntPtr feature { get; set; }


多人臉實體類存放單人臉資訊列表,在建構函式MultiFaceModel中,將指標轉換為多人臉列表。

public class MultiFaceModel : IDisposable
{
    /// <summary>
    /// 多人臉資訊
    /// </summary>
    public ASF_MultiFaceInfo MultiFaceInfo { get; private set; }

    /// <summary>
    /// 單人臉資訊List
    /// </summary>
    public List<ASF_SingleFaceInfo> FaceInfoList { get; private set; }

    /// <summary>
    /// 人臉資訊列表
    /// </summary>
    /// <param name="multiFaceInfo"></param>
    public MultiFaceModel(ASF_MultiFaceInfo multiFaceInfo) 
    {
        this.MultiFaceInfo = multiFaceInfo;
        this.FaceInfoList = new List<ASF_SingleFaceInfo>();
        FaceInfoList = PtrToMultiFaceArray(multiFaceInfo.faceRects, multiFaceInfo.faceOrients, multiFaceInfo.faceNum);
    }
    /// <summary>
    /// 指標轉多人臉列表
    /// </summary>
    /// <param name="faceRect"></param>
    /// <param name="faceOrient"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    private List<ASF_SingleFaceInfo> PtrToMultiFaceArray(IntPtr faceRect, IntPtr faceOrient, int length)
    {
        List<ASF_SingleFaceInfo> FaceInfoList = new List<ASF_SingleFaceInfo>();
        var size = Marshal.SizeOf(typeof(int));
        var sizer = Marshal.SizeOf(typeof(MRECT));

        for (var i = 0; i < length; i++)
        {
            ASF_SingleFaceInfo faceInfo = new ASF_SingleFaceInfo();

            MRECT rect = new MRECT();
            var iPtr = new IntPtr(faceRect.ToInt32() + i * sizer);
            rect = (MRECT)Marshal.PtrToStructure(iPtr, typeof(MRECT));
            faceInfo.faceRect = rect;

            int orient = 0;
            iPtr = new IntPtr(faceOrient.ToInt32() + i * size);
            orient = (int)Marshal.PtrToStructure(iPtr, typeof(int));
            faceInfo.faceOrient = orient;
            FaceInfoList.Add(faceInfo);
        }
        return FaceInfoList;
    }

    public void Dispose()
    {
        Marshal.FreeCoTaskMem(MultiFaceInfo.faceRects);
        Marshal.FreeCoTaskMem(MultiFaceInfo.faceOrients);
    }
}


然後獲取所有人臉資訊,放在列表中備用。首先由SDK檢測人臉,得到Rect框,然後根據ASFProcess方法處理人臉資訊,支援初始化時候指定需要檢測的功能,在process時進一步在這個已經指定的功能集中繼續篩選例如初始化的時候指定檢測年齡和性別, 在process的時候可以只檢測年齡, 但是不能檢測除年齡和性別之外的功能。我們把FaceEngineMask.ASF\_AGE| FaceEngineMask.ASF\_GENDER 年齡和行不這兩個檢測的功能條件加上。然後再通過ASFGetAge方法獲取年齡即可。

/// <summary>
/// 獲取人臉資訊列表
/// </summary>
/// <param name="pEngine"></param>
/// <param name="bitmap"></param>
/// <returns></returns>
public static List<FaceInfoModel>GetFaceInfos(IntPtr pEngine,Image bitmap)
{
    List<FaceInfoModel> listRet = new List<FaceInfoModel>();
    try
    {
        List<int> AgeList = new List<int>();
        List<int> GenderList = new List<int>();
        //檢測人臉,得到Rect框
        ASF_MultiFaceInfo multiFaceInfo = FaceUtil.DetectFace(pEngine, bitmap);
        MultiFaceModel multiFaceModel = new MultiFaceModel(multiFaceInfo);
        //人臉資訊處理
        ImageInfo imageInfo = ImageUtil.ReadBMP(bitmap);
        int retCode = ASFFunctions.ASFProcess(pEngine, imageInfo.width, imageInfo.height, imageInfo.format, imageInfo.imgData, ref multiFaceInfo, FaceEngineMask.ASF_AGE| FaceEngineMask.ASF_GENDER);
        //獲取年齡資訊
        ASF_AgeInfo ageInfo = new ASF_AgeInfo();
        retCode = ASFFunctions.ASFGetAge(pEngine, ref ageInfo);
        AgeList = ageInfo.PtrToAgeArray(ageInfo.ageArray, ageInfo.num);
        //獲取性別資訊
        ASF_GenderInfo genderInfo = new ASF_GenderInfo();
        retCode = ASFFunctions.ASFGetGender(pEngine, ref genderInfo);
        GenderList = genderInfo.PtrToGenderArray(genderInfo.genderArray, genderInfo.num);

        for (int i = 0; i < multiFaceInfo.faceNum; i++)
        {
            FaceInfoModel faceInfo = new FaceInfoModel();
            faceInfo.age = AgeList[i];
            faceInfo.gender = GenderList[i];
            faceInfo.faceRect = multiFaceModel.FaceInfoList[i].faceRect;
            faceInfo.feature = ExtractFeature(pEngine, bitmap, multiFaceModel.FaceInfoList[i]);//提取單人臉特徵
            faceInfo.faceOrient = multiFaceModel.FaceInfoList[i].faceOrient;
            listRet.Add(faceInfo);
        }
        return listRet;//返回多人臉資訊
    }
    catch {
        return listRet;
    }
}


從列表中獲取到的多張人臉,在人臉上畫框作出標識,也可以把提取的人臉資訊,年齡、性別作出展示。接下來就是選擇一張目標人物的照片,通過SDK提取目標人物的人臉特徵值作為比較物件,逐一與影片中的人臉特徵進行比較。如果有判斷到相似度匹配的人臉,則把影片幀影象呈現出來。

/// <summary>
/// 比較兩個特徵值的相似度,返回相似度
/// </summary>
/// <param name="feature1"></param>
/// <param name="feature2"></param>
/// <returns></returns>
private float CompareTwoFeatures(IntPtr feature1, IntPtr feature2)
{
    float similarity = 0.0f;
    //呼叫人臉匹配方法,進行匹配
    ASFFunctions.ASFFaceFeatureCompare(pImageEngine, feature1, feature2, ref similarity);
    return similarity;
}


整體效果如下:

之前只實現了從多張人臉中獲取一張最大尺寸的人臉作為比較物件,這樣影片中也就只能對一張人臉進行畫框標記了,現在是把所有提取到的人臉均進行標記,並把各自特徵值存在列表中,以便與目標人臉特徵值進行匹配。

這樣也就粗略的實現了人臉識別追蹤,並對目標人物進行抓拍的功能了。

遇到的問題

在人臉檢測時,如果畫面中出現的人臉是側臉,則有一定的可能把性別判定出錯。

GitHub原始碼已上傳

瞭解更多人臉識別產品相關內容請到虹軟視覺開放平臺