本篇文章為大家展示了使用.NET怎么實現(xiàn)一個人臉識別功能,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
專注于為中小企業(yè)提供網(wǎng)站建設、做網(wǎng)站服務,電腦端+手機端+微信端的三站合一,更高效的管理,為中小企業(yè)壺關免費做網(wǎng)站提供優(yōu)質(zhì)的服務。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動了千余家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設實現(xiàn)規(guī)模擴充和轉(zhuǎn)變。使用方法
首先安裝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"); IListfaces = await fc.Face.DetectWithStreamAsync(file);
其中返回的faces
是一個IList
結(jié)構,很顯然一次可以識別出多個人臉,其中一個示例返回結(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> Groups { get; set; } public IList MessyGroup { get; set; } // ... }
包含了一個Groups
對象和一個MessyGroup
對象,其中Groups
是一個數(shù)據(jù)的數(shù)據(jù),用于存放人臉的分組,MessyGroup
用于保存未能找到分組的FaceId
。
有了這個,就可以通過一小段簡短的代碼,將不同的人臉組,分別復制對應的文件夾中:
void CopyGroup(string outputPath, GroupResult result, Dictionaryfaces) { 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ǎ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
的傳感器,拍出來的照片一般都是橫向的。但偶爾尋求一些構圖的時候,我們也會選擇縱向構圖。雖然現(xiàn)在許多API
都支持正負30
度的側(cè)臉,但豎著的臉API
基本都是不支持的,如下圖(實在找不到可以授權使用照片的模特了?):
還好照片在拍攝后,都會保留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)后,我的照片如下:
這樣豎拍的照片也能識別出來了。
并行速度
前文說過,一個文件夾可能會有成千上萬個文件,一個個上傳識別,速度可能慢了點,它的代碼可能長這個樣子:
Dictionaryfaces = 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()
即可完成:
Dictionaryfaces = 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)絡傳輸異常,或者打翻了桌子上的咖啡(誰知道呢?)……或者完全一切正常,只是想再做一些其它的分析,所有東西又要重新開始。我們可以加入下載中常說的“斷點續(xù)傳”機制。
其實就是一個緩存,記錄每個文件讀取的結(jié)果,然后下次運行時先從緩存中讀取即可,緩存到一個json
文件中:
Dictionaryfaces = 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
關鍵字,是為了保證多線程下載時的線程安全。
使用時,只需只需在Select
中添加一行代碼即可:
var cache = new Cache>(); // 重點 Dictionary
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
那樣重量級的東西,因此使用起來很方便。
這里我使用的是.Buffer(int)
函數(shù),它可以將IEnumerable
按指定的數(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); }
上述內(nèi)容就是使用.NET怎么實現(xiàn)一個人臉識別功能,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注創(chuàng)新互聯(lián)行業(yè)資訊頻道。