.NET做人臉識別并分類的實現(xiàn)示例
在游樂場、玻璃天橋、滑雪場等娛樂場所,經(jīng)常能看到有攝影師在拍照片,令這些經(jīng)營者發(fā)愁的一件事就是照片太多了,客戶在成千上萬張照片中找到自己可不是件容易的事。在一次游玩等活動或家庭聚會也同理,太多了照片導致挑選十分困難。
還好有.NET
,只需少量代碼,即可輕松找到人臉并完成分類。
本文將使用Microsoft Azure
云提供的認知服務(wù)
(Cognitive Services
)API
來識別并進行人臉分類,可以免費使用,注冊地址是:https://portal.azure.com。注冊完成后,會得到兩個密鑰
,通過這個密鑰
即可完成本文中的所有代碼,這個密鑰
長這個樣子(非真實密鑰):
fa3a7bfd807ccd6b17cf559ad584cbaa
使用方法
首先安裝NuGet
包Microsoft.Azure.CognitiveServices.Vision.Face
,目前最新版是2.5.0-preview.1
,然后創(chuàng)建一個FaceClient
:
string key = "fa3a7bfd807ccd6b17cf559ad584cbaa"; // 替換為你的key using var fc = new FaceClient(new ApiKeyServiceClientCredentials(key)) { Endpoint = "https://southeastasia.api.cognitive.microsoft.com", };
然后識別一張照片:
using var file = File.OpenRead(@"C:\Photos\DSC_996ICU.JPG"); IList<DetectedFace> faces = await fc.Face.DetectWithStreamAsync(file);
其中返回的faces
是一個IList
結(jié)構(gòu),很顯然一次可以識別出多個人臉,其中一個示例返回結(jié)果如下(已轉(zhuǎn)換為JSON
):
[ { "FaceId": "9997b64e-6e62-4424-88b5-f4780d3767c6", "RecognitionModel": null, "FaceRectangle": { "Width": 174, "Height": 174, "Left": 62, "Top": 559 }, "FaceLandmarks": null, "FaceAttributes": null }, { "FaceId": "8793b251-8cc8-45c5-ab68-e7c9064c4cfd", "RecognitionModel": null, "FaceRectangle": { "Width": 152, "Height": 152, "Left": 775, "Top": 580 }, "FaceLandmarks": null, "FaceAttributes": null } ]
可見,該照片返回了兩個DetectedFace
對象,它用FaceId
保存了其Id
,用于后續(xù)的識別,用FaceRectangle
保存了其人臉的位置信息,可供對其做進一步操作。RecognitionModel
、FaceLandmarks
、FaceAttributes
是一些額外屬性,包括識別性別
、年齡
、表情
等信息,默認不識別,如下圖API
所示,可以通過各種參數(shù)配置,非常好玩,有興趣的可以試試:
最后,通過.GroupAsync
來將之前識別出的多個faceId
進行分類:
var faceIds = faces.Select(x => x.FaceId.Value).ToList(); GroupResult reslut = await fc.Face.GroupAsync(faceIds);
返回了一個GroupResult
,其對象定義如下:
public class GroupResult { public IList<IList<Guid>> Groups { get; set; } public IList<Guid> MessyGroup { get; set; } // ... }
包含了一個Groups
對象和一個MessyGroup
對象,其中Groups
是一個數(shù)據(jù)的數(shù)據(jù),用于存放人臉的分組,MessyGroup
用于保存未能找到分組的FaceId
。
有了這個,就可以通過一小段簡短的代碼,將不同的人臉組,分別復制對應的文件夾中:
void CopyGroup(string outputPath, GroupResult result, Dictionary<Guid, (string file, DetectedFace face)> faces) { foreach (var item in result.Groups .SelectMany((group, index) => group.Select(v => (faceId: v, index))) .Select(x => (info: faces[x.faceId], i: x.index + 1)).Dump()) { string dir = Path.Combine(outputPath, item.i.ToString()); Directory.CreateDirectory(dir); File.Copy(item.info.file, Path.Combine(dir, Path.GetFileName(item.info.file)), overwrite: true); } string messyFolder = Path.Combine(outputPath, "messy"); Directory.CreateDirectory(messyFolder); foreach (var file in result.MessyGroup.Select(x => faces[x].file).Distinct()) { File.Copy(file, Path.Combine(messyFolder, Path.GetFileName(file)), overwrite: true); } }
然后就能得到運行結(jié)果,如圖,我傳入了102
張照片,輸出了15
個分組和一個“未找到隊友”的分組:
還能有什么問題?
就兩個API
調(diào)用而已,代碼一把梭,感覺太簡單了?其實不然,還會有很多問題。
圖片太大,需要壓縮
畢竟要把圖片上傳到云服務(wù)中,如果上傳網(wǎng)速不佳,流量會挺大,而且現(xiàn)在的手機、單反、微單都能輕松達到好幾千萬像素,jpg
大小輕松上10MB
,如果不壓縮就上傳,一來流量和速度遭不住。
二來……其實Azure
也不支持,文檔(https://docs.microsoft.com/en-us/rest/api/cognitiveservices/face/face/detectwithstream)顯示,最大僅支持6MB
的圖片,且圖片大小應不大于1920x1080
的分辨率:
- JPEG, PNG, GIF (the first frame), and BMP format are supported. The allowed image file size is from 1KB to 6MB.
- The minimum detectable face size is 36x36 pixels in an image no larger than 1920x1080 pixels. Images with dimensions higher than 1920x1080 pixels will need a proportionally larger minimum face size.
因此,如果圖片太大,必須進行一定的壓縮(當然如果圖片太小,顯然也沒必要進行壓縮了),使用.NET
的Bitmap
,并結(jié)合C# 8.0
的switch expression
,這個判斷邏輯以及壓縮代碼可以一氣呵成:
byte[] CompressImage(string image, int edgeLimit = 1920) { using var bmp = Bitmap.FromFile(image); using var resized = (1.0 * Math.Max(bmp.Width, bmp.Height) / edgeLimit) switch { var x when x > 1 => new Bitmap(bmp, new Size((int)(bmp.Size.Width / x), (int)(bmp.Size.Height / x))), _ => bmp, }; using var ms = new MemoryStream(); resized.Save(ms, ImageFormat.Jpeg); return ms.ToArray(); }
豎立的照片
相機一般都是3:2
的傳感器,拍出來的照片一般都是橫向的。但偶爾尋求一些構(gòu)圖的時候,我們也會選擇縱向構(gòu)圖。雖然現(xiàn)在許多API
都支持正負30
度的側(cè)臉,但豎著的臉API
基本都是不支持的,如下圖(實在找不到可以授權(quán)使用照片的模特了😂):
還好照片在拍攝后,都會保留exif
信息,只需讀取exif
信息并對照片做相應的旋轉(zhuǎn)即可:
void HandleOrientation(Image image, PropertyItem[] propertyItems) { const int exifOrientationId = 0x112; PropertyItem orientationProp = propertyItems.FirstOrDefault(i => i.Id == exifOrientationId); if (orientationProp == null) return; int val = BitConverter.ToUInt16(orientationProp.Value, 0); RotateFlipType rotateFlipType = val switch { 2 => RotateFlipType.RotateNoneFlipX, 3 => RotateFlipType.Rotate180FlipNone, 4 => RotateFlipType.Rotate180FlipX, 5 => RotateFlipType.Rotate90FlipX, 6 => RotateFlipType.Rotate90FlipNone, 7 => RotateFlipType.Rotate270FlipX, 8 => RotateFlipType.Rotate270FlipNone, _ => RotateFlipType.RotateNoneFlipNone, }; if (rotateFlipType != RotateFlipType.RotateNoneFlipNone) { image.RotateFlip(rotateFlipType); } }
旋轉(zhuǎn)后,我的照片如下:
這樣豎拍的照片也能識別出來了。
并行速度
前文說過,一個文件夾可能會有成千上萬個文件,一個個上傳識別,速度可能慢了點,它的代碼可能長這個樣子:
Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder) .Select(file => { byte[] bytes = CompressImage(file); var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult()); (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump(); return (file, faces: result.faces.ToList()); }) .SelectMany(x => x.faces.Select(face => (x.file, face))) .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));
要想把速度變化,可以啟用并行上傳,有了C#
/.NET
的LINQ
支持,只需加一行.AsParallel()
即可完成:
Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder) .AsParallel() // 加的就是這行代碼 .Select(file => { byte[] bytes = CompressImage(file); var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult()); (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump(); return (file, faces: result.faces.ToList()); }) .SelectMany(x => x.faces.Select(face => (x.file, face))) .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));
斷點續(xù)傳
也如上文所說,有成千上萬張照片,如果一旦網(wǎng)絡(luò)傳輸異常,或者打翻了桌子上的咖啡(誰知道呢?)……或者完全一切正常,只是想再做一些其它的分析,所有東西又要重新開始。我們可以加入下載中常說的“斷點續(xù)傳”機制。
其實就是一個緩存,記錄每個文件讀取的結(jié)果,然后下次運行時先從緩存中讀取即可,緩存到一個json
文件中:
Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder) .AsParallel() // 加的就是這行代碼 .Select(file => { byte[] bytes = CompressImage(file); var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult()); (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump(); return (file, faces: result.faces.ToList()); }) .SelectMany(x => x.faces.Select(face => (x.file, face))) .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));
注意代碼下方有一個lock
關(guān)鍵字,是為了保證多線程下載時的線程安全。
使用時,只需只需在Select
中添加一行代碼即可:
var cache = new Cache<List<DetectedFace>>(); // 重點 Dictionary<Guid, (string file, DetectedFace face)> faces = GetFiles(inFolder) .AsParallel() .Select(file => (file: file, faces: cache.GetOrCreate(file, () => // 重點 { byte[] bytes = CompressImage(file); var result = (file, faces: fc.Face.DetectWithStreamAsync(new MemoryStream(bytes)).GetAwaiter().GetResult()); (result.faces.Count == 0 ? $"{file} not detect any face!!!" : $"{file} detected {result.faces.Count}.").Dump(); return result.faces.ToList(); }))) .SelectMany(x => x.faces.Select(face => (x.file, face))) .ToDictionary(x => x.face.FaceId.Value, x => (file: x.file, face: x.face));
將人臉框起來
照片太多,如果活動很大,或者合影中有好幾十個人,分出來的組,將長這個樣子:
完全不知道自己的臉在哪,因此需要將檢測到的臉框起來。
注意框起來的過程,也很有技巧,回憶一下,上傳時的照片本來就是壓縮和旋轉(zhuǎn)過的,因此返回的DetectedFace
對象值,它也是壓縮和旋轉(zhuǎn)過的,如果不進行壓縮和旋轉(zhuǎn),找到的臉的位置會完全不正確,因此需要將之前的計算過程重新演算一次:
using var bmp = Bitmap.FromFile(item.info.file); HandleOrientation(bmp, bmp.PropertyItems); using (var g = Graphics.FromImage(bmp)) { using var brush = new SolidBrush(Color.Red); using var pen = new Pen(brush, 5.0f); var rect = item.info.face.FaceRectangle; float scale = Math.Max(1.0f, (float)(1.0 * Math.Max(bmp.Width, bmp.Height) / 1920.0)); g.ScaleTransform(scale, scale); g.DrawRectangle(pen, new Rectangle(rect.Left, rect.Top, rect.Width, rect.Height)); } bmp.Save(Path.Combine(dir, Path.GetFileName(item.info.file)));
使用我上面的那張照片,檢測結(jié)果如下(有點像相機對焦時人臉識別的感覺):
1000個臉的限制
.GroupAsync
方法一次只能檢測1000
個FaceId
,而上次活動800
多張照片中有超過2000
個FaceId
,因此需要做一些必要的分組。
分組最簡單的方法,就是使用System.Interactive
包,它提供了Rx.NET
那樣方便快捷的API
(這些API
在LINQ
中未提供),但又不需要引入Observable<T>
那樣重量級的東西,因此使用起來很方便。
這里我使用的是.Buffer(int)
函數(shù),它可以將IEnumerable<T>
按指定的數(shù)量(如1000
)進行分組,代碼如下:
foreach (var buffer in faces .Buffer(1000) .Select((list, groupId) => (list, groupId)) { GroupResult group = await fc.Face.GroupAsync(buffer.list.Select(x => x.Key).ToList()); var folder = outFolder + @"\gid-" + buffer.groupId; CopyGroup(folder, group, faces); }
總結(jié)
文中用到的完整代碼,全部上傳了到我的博客數(shù)據(jù)Github
,只要輸入圖片和key
,即可直接使用和運行:
https://github.com/sdcb/blog-data/tree/master/2019/20191122-dotnet-face-detection
這個月我參加了上海的.NET Conf
,我上述代碼對.NET Conf
的800
多張照片做了分組,識別出了2000
多張人臉,我將其中我的照片的前三張找出來,結(jié)果如下:
......
總的來說,這個效果還挺不錯,渣渣分辨率的照片的臉都被它找到了😂。
注意,不一定非得用Azure Cognitive Services
來做人臉識別,國內(nèi)還有阿里云等廠商也提供了人臉識別等服務(wù),并提供了.NET
接口,無非就是調(diào)用API
,注意其限制,代碼總體差不多。
另外,如有離線人臉識別需求,Luxand
提供了還有離線版人臉識別SDK
,名叫Luxand FaceSDK
,同樣提供了.NET
接口。因為無需網(wǎng)絡(luò)調(diào)用,其識別更快,匹配速度更是可達每秒5千萬個人臉數(shù)據(jù),精度也非常高,親測好用,目前最新版是v7.1.0
,授權(quán)昂貴(但百度有驚喜)。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- opencv 做人臉識別 opencv 人臉匹配分析
- 基于OpenCV的PHP圖像人臉識別技術(shù)
- Android camera實時預覽 實時處理,人臉識別示例
- python實現(xiàn)人臉識別經(jīng)典算法(一) 特征臉法
- JavaScript人臉識別技術(shù)及臉部識別JavaScript類庫Tracking.js
- android實現(xiàn)人臉識別技術(shù)的示例代碼
- 微信小程序?qū)崿F(xiàn)人臉識別
- PHP使用Face++接口開發(fā)微信公眾平臺人臉識別系統(tǒng)的方法
- 人臉識別經(jīng)典算法一 特征臉方法(Eigenface)
- python3+dlib實現(xiàn)人臉識別和情緒分析
- 詳解如何用OpenCV + Python 實現(xiàn)人臉識別
- Python3結(jié)合Dlib實現(xiàn)人臉識別和剪切
相關(guān)文章
ASP.NET MVC中Controller控制器向View視圖傳值的幾種方式
這篇文章介紹了ASP.NET MVC中Controller控制器向View視圖傳值的幾種方式,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03Asp.net利用一般處理程序?qū)崿F(xiàn)文件下載功能
這篇文章主要介紹了Asp.net利用一般處理程序?qū)崿F(xiàn)文件下載功能,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-07-07asp.net線程批量導入數(shù)據(jù)時通過ajax獲取執(zhí)行狀態(tài)
asp.net線程批量導入數(shù)據(jù)是大家日常工作中常遇到的一個要求,但批量添加時間一般較長,如果能返回執(zhí)行的狀態(tài)就好,那么下面這篇文章主要給大家介紹了asp.net線程批量導入數(shù)據(jù)時通過ajax獲取執(zhí)行狀態(tài)的方法,有需要的朋友可以參考下。2016-12-12asp.net下使用DbProviderFactories的數(shù)據(jù)庫操作類
項目開發(fā)中用到VB.NET開發(fā),參考網(wǎng)上的資料,自己寫了數(shù)據(jù)庫操作類。2010-06-06