在本章中,我們將展示兩個獨立的例子,一個用於人臉檢測,另一個用於動態檢測,以及如何快速地將這些功能添加到應用程序中。
在這一章中,我們將討論:
- 面部檢測
- 動態檢測
- 將檢測添加到應用程序中
面部檢測
人臉檢測,是人臉識別的第一部分。如果你不能從屏幕上的所有東西中識別出一個或多個人臉,那么你將永遠無法識別那是誰的臉。
首先讓我們看一張我們的應用程序截圖:
上圖中,通過攝像頭我們已經捕獲到一張圖像,接下來啟用面部跟蹤,看看會發生什么:
物體面部特征正在被追蹤。我們在物體周圍看到的是面部追蹤器(白色線框),它告訴我們我們這里有一張臉;以及我們的角度探測器(紅線),它提供了一些關於我們臉所處水平方向的參考。
當我們移動物體時,面部追蹤器和角度探測器會追蹤他。這一切都很好,但是如果我們在真實的人臉上啟用面部跟蹤會發生什么呢?
如下圖,面部追蹤器和角度探測器正在追蹤人的面部。
當我們把頭從一邊移到另一邊時,面部追蹤器會跟蹤這個動作,可以看到角度探測器會根據它所識別的面部水平角度進行調整。
可以看到,在這里我們的顏色是黑白的,而不是彩色的。因為這是一個直方圖的反向投影,而且它是一個可以更改的選項。
即使我們遠離攝像機,讓其他物體也進入視野中,面部追蹤器也能在諸多噪音中跟蹤我們的臉,如下圖所示。這正是我們在電影中看到的面部識別系統的工作原理,盡管它更為先進。
現在讓我們深入程序內部,看看它到底是如何工作的。
首先,我們需要問自己一個問題,我們想要解決的問題到底是什么。到底是人臉識別還是人臉檢測。這里不得不提到Viola-Jones算法,因為,首先它有很高的檢出率和很低的誤報率,然后它非常擅長對數據的實時處理,最終要的一點是,它非常善於從非人臉中分別出人臉。
要永遠記住,人臉檢測只是人臉識別的第一步!
這個算法要求輸入一個完整的正面,垂直的臉。臉部需要直接指向采集設備,頭部盡量不要歪,不要昂頭或低頭。
這里有必要在強調一次,我們要做的只是在圖像中檢測出人臉即可。
我們的算法需要經過四個步驟來完成這件事:
- Haar 特征選擇
- 創建一個完整的圖像
- AdaBoost算法(通過迭代弱分類器而產生最終的強分類器的算法) 訓練分類器
- 級聯分類器
在正式開始之前,讓我們先捋一捋面部檢測到底是如果工作的。所有的臉,無論是人的,動物的還是其他的,都有一些相似的特征。例如,都有一個鼻子,兩個鼻孔,一張嘴巴,兩個眼睛,兩個耳朵等等。我們的算法通過Haar特征來匹配這些內容,我們可以通過其中任一項找到其他的特征。
但是,我們這里會遇到一個問題。在一個24x24像素的窗口中,一共有162336個可能的特征。如果這個計算結果是正確的,那么計算他們的時間和成本將非常之高。因此,我們將會使用一種被稱為adaptive boosting(自適應提升法)的算法,或者更為常見的AdaBoost算法。如果你研究過機器學習,我相信你聽說過一種叫做boosting(提升)的技術。我們的學習算法將使用AdaBoost來選擇最好的特征並訓練分類器來使用它們。
AdaBoost可以與許多類型的學習算法一起使用,並且被業界認為是許多需要增強的任務的最佳開箱即用算法。通常在切換到另一種算法並對其進行基准測試之前,您不會注意到它有多好和多快。實際上這種區別是非常明顯的。
在繼續之前,我們先來了解一下什么是boosting(提升)技術。
Boosting從其他弱學習算法中獲取輸出,並將其與weighted sum(加權和)結合,加權和是boost分類器的最終輸出。AdaBoost的自適應部分來自於這樣一個事實,即后續的學習者被調整,以支持那些被以前的分類器錯誤分類的實例。
與其他算法相比,該算法更傾向於對數據進行過擬合,所以AdaBoost對噪聲數據和異常值很敏感。因此我們在准備數據的時候,需要格外注意這一點。
現在,讓我們來看看示例中的程序到底是如何工作的。對於這個示例,我們將再次使用Accord框架。
首先創建一個FaceHaarCascade對象。該對象包含一系列 Haarlike 的特征的弱分類階段的集合。每個階段都包含一組分類器樹, 這些分類器樹將在決策過程中使用。FaceHaarCascade自動為我們創建了所有這些階段和樹,而不需要我們去關心具體實現的細節。
首先,需要在底層構建一個決策樹,它將為每個階段提供節點,並為每個特性提供數值。以下是Accord的部分源碼。
List<HaarCascadeStage> stages = new List<HaarCascadeStage>(); List<HaarFeatureNode[]> nodes; HaarCascadeStage stage; stage = new HaarCascadeStage(0.822689414024353); nodes = new List<HaarFeatureNode[]>(); nodes.Add( new[] { new HaarFeatureNode( 0.004014195874333382,0.0337941907346249, 0.8378106951713562, new int[] { 3, 7, 14, 4, -1 }, new int[] { 3, 9, 14, 2, 2 } ) } ); nodes.Add( new[] { new HaarFeatureNode( 0.0151513395830989, 0.1514132022857666, 0.7488812208175659, new int[] { 1, 2, 18, 4, -1 }, new int[] { 7, 2, 6, 4, 3 } ) } ); nodes.Add( new[] { new HaarFeatureNode( 0.004210993181914091, 0.0900492817163467, 0.6374819874763489, new int[] { 1, 7, 15, 9, -1 }, new int[] { 1, 10, 15, 3, 3 } ) } );
一旦構建完成,我們就可以使用cascade對象來創建HaarObjectDetector,這就是我們將用於檢測的對象。
接下來我們需要提供:
- 我們的面部級聯對象
- 搜索對象時使用的最小窗口大小
- 我們的搜索模式,假設我們只搜索一個對象
- 在搜索期間重新縮放搜索窗口時要使用的重新縮放因子
HaarCascade cascade = new FaceHaarCascade(); detector = new HaarObjectDetector(
cascade,
25, ObjectDetectorSearchMode.Single,
1.2f, ObjectDetectorScalingMode.GreaterToSmaller
);
現在,我們需要准備數據,在本示例中,我們將使用筆記本電腦上的攝像頭捕獲所有圖像。然而,Accord.NET framework 使得使用其他源進行數據采集變得很容易。例如 avi文件,jpg文件等等。
接下來,連接攝像頭,選擇分辨率:
// 創建視頻源 VideoCaptureDevice videoSource = new VideoCaptureDevice(form.VideoDevice); // 設置幀的大小 videoSource.VideoResolution = selectResolution(videoSource); /// <summary> /// 獲取幀的大小 /// </summary> /// <param name="videoSource">視頻源</param> /// <returns>幀的大小</returns> private VideoCapabilities selectResolution(VideoCaptureDevice videoSource) { foreach (var cap in videoSource?.VideoCapabilities) { if (cap.FrameSize.Height == 240) return cap; if (cap.FrameSize.Width == 320) return cap; } return videoSource?.VideoCapabilities.Last(); }
在這個演示中,你會注意到檢測物體正對着攝像機,在背景中,還有一些其他的東西,那就是所謂的隨機噪聲。這樣做是為了展示人臉檢測算法是如何區分出臉的。如果我們的探測器不能處理這些,它就會在噪聲中消失,從而無法檢測到臉。
隨着視頻源的加入,我們需要在接收到新的視頻幀時得到通知,以便處理它、應用標記,等等。我們通過頻源播放器的NewFrameReceived事件來實現這一點。\
在我們已經有了一個視頻源和一個視頻,讓我們看看每當我們被通知有一個新的視頻幀可用時發生了什么。
我們需要做的第一件事是對圖像進行采樣,以使它更容易工作:
ResizeNearestNeighbor resize = new ResizeNearestNeighbor(160, 120); UnmanagedImage downsample = resize.Apply(im);
如果我們沒有找到一張臉,我們將保持跟蹤模式,等待一個具有可檢測面部的幀。一旦我們找到了面部區域,我們需要重置跟蹤器,定位臉部,減小它的大小,以盡可能的剔除背景噪聲,然后初始化跟蹤器,並將在圖像上進行標記。代碼如下:
Rectangle[] regions = detector?.ProcessFrame(downsample); if (regions != null && regions.Length > 0) { tracker?.Reset(); // 跟蹤第一張臉 Rectangle face = regions[0]; // 減小人臉檢測的大小,避免跟蹤背景上的其他內容 Rectangle window = new Rectangle(
(int)((regions[0].X + regions[0].Width / 2f) * xscale),
(int)((regions[0].Y + regions[0].Height / 2f) * yscale),
1,
1
); window.Inflate((int)(0.2f * regions[0].Width * xscale), (int)(0.4f * regions[0].Height * yscale)); if (tracker != null) { tracker.SearchWindow = window; tracker.ProcessFrame(im); } marker = new RectanglesMarker(window); marker.ApplyInPlace(im); eventArgs.Frame = im.ToManagedImage(); tracking = true; } else { detecting = true; }
一旦檢測到臉,我們的圖像幀是這樣的:
如果把頭偏向一邊,我們現在的形象應該是這樣的:
動態檢測
可以看到,在上一個例子中,我們不僅實現了面部檢測,還實現了動態檢測。現在,讓我們把目光轉向更大的范圍,檢測任何物體的運動,而不僅僅是面部。我們將繼續使用Accord.NET來實現。
在動態檢測中,我們會用紅色高亮顯示屏幕上的任何運動。移動的數量由任何一個區域的紅色濃度表示。所以,如下圖所示,我們可以看到手指在移動但是其他的都是靜止的。
如下圖所示,可以看到整個手的移動范圍在增加。
如下圖所示,一旦整只手開始移動,你不僅可以看到更多的紅色,而且紅色的總量是在增加的:
如果不希望對整個屏幕區域進行運動處理,可以自定義運動區域;運動檢測只會發生在這些區域。如下圖,可以看到我們已經定義了一個運動區域,這是唯一的一個區域。
現在,如果我們在攝像頭前面做一些運動,可以看到程序只檢測到了來自我們定義區域發生的運動。
現在,我們來做這樣一個測試,在我們自定義的檢測區域范圍內,放置一個物體,然后我們把手放在這個物體后面進行運動,當然手也是在這個自定義的檢測區域范圍內進行運動的。如下圖,可以看到,手的運動被檢測出來了。
現在我們使用另一個選項,網格運動突出顯示。它會使得檢測到的運動區域基於定義的網格在紅色方塊中突出顯示,如下圖所示。
將檢測添加到應用程序中
以下是處理接收到新的幀的代碼:
private void videoSourcePlayer_NewFrame(object sender, NewFrameEventArgs args) {
lock (this) { if (motionDetector != null) { float motionLevel = motionDetector.ProcessFrame(args.Frame); if (motionLevel > motionAlarmLevel) { //快門速度2秒 flash = (int)(2 * (1000 / timer.Interval)); } //檢查對象的數 if (motionDetector.MotionProcessingAlgorithm is BlobCountingObjectsProcessing) { BlobCountingObjectsProcessing countingDetector = (BlobCountingObjectsProcessing)motionDetector.MotionProcessingAlgorithm; detectedObjectsCount = countingDetector.ObjectsCount; } else { detectedObjectsCount = -1; } // 積累的歷史 motionHistory.Add(motionLevel); if (motionHistory.Count > 300) { motionHistory.RemoveAt(0); } if (顯示運動歷史ToolStripMenuItem.Checked) DrawMotionHistory(args.Frame);
}
}
}
這里的關鍵是檢測視頻幀中發生的動量,這是通過以下代碼完成的。對於本例,我們使用的是兩級的運動報警級別,但是你也可以使用任何你喜歡的級別定義。一旦超過這個閾值,就可以實現所需的邏輯,例如發送電子郵件、開始視頻捕獲等等。
float motionLevel = motionDetector.ProcessFrame(args.Frame); if (motionLevel > motionAlarmLevel) { //快門速度2秒 flash = (int)(2 * (1000 / timer.Interval)); }
總結
在這一章中,我們學習了面部和動態檢測,還展示了一些簡單易用的代碼。我們可以輕松的將這些功能添加到自己的程序中。