您好,登錄后才能下訂單哦!
本篇文章為大家展示了使用.NET怎么實現一個人臉識別功能,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
使用方法
首先安裝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
結構,很顯然一次可以識別出多個人臉,其中一個示例返回結果如下(已轉換為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
所示,可以通過各種參數配置,非常好玩,有興趣的可以試試:
最后,通過.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
是一個數據的數據,用于存放人臉的分組,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); } }
然后就能得到運行結果,如圖,我傳入了102
張照片,輸出了15
個分組和一個“未找到隊友”的分組:
還能有什么問題?
就兩個API
調用而已,代碼一把梭,感覺太簡單了?其實不然,還會有很多問題。
圖片太大,需要壓縮
畢竟要把圖片上傳到云服務中,如果上傳網速不佳,流量會挺大,而且現在的手機、單反、微單都能輕松達到好幾千萬像素,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
,并結合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
的傳感器,拍出來的照片一般都是橫向的。但偶爾尋求一些構圖的時候,我們也會選擇縱向構圖。雖然現在許多API
都支持正負30
度的側臉,但豎著的臉API
基本都是不支持的,如下圖(實在找不到可以授權使用照片的模特了?):
還好照片在拍攝后,都會保留exif
信息,只需讀取exif
信息并對照片做相應的旋轉即可:
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); } }
旋轉后,我的照片如下:
這樣豎拍的照片也能識別出來了。
并行速度
前文說過,一個文件夾可能會有成千上萬個文件,一個個上傳識別,速度可能慢了點,它的代碼可能長這個樣子:
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ù)傳
也如上文所說,有成千上萬張照片,如果一旦網絡傳輸異常,或者打翻了桌子上的咖啡(誰知道呢?)……或者完全一切正常,只是想再做一些其它的分析,所有東西又要重新開始。我們可以加入下載中常說的“斷點續(xù)傳”機制。
其實就是一個緩存,記錄每個文件讀取的結果,然后下次運行時先從緩存中讀取即可,緩存到一個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
關鍵字,是為了保證多線程下載時的線程安全。
使用時,只需只需在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));
將人臉框起來
照片太多,如果活動很大,或者合影中有好幾十個人,分出來的組,將長這個樣子:
完全不知道自己的臉在哪,因此需要將檢測到的臉框起來。
注意框起來的過程,也很有技巧,回憶一下,上傳時的照片本來就是壓縮和旋轉過的,因此返回的DetectedFace
對象值,它也是壓縮和旋轉過的,如果不進行壓縮和旋轉,找到的臉的位置會完全不正確,因此需要將之前的計算過程重新演算一次:
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)));
使用我上面的那張照片,檢測結果如下(有點像相機對焦時人臉識別的感覺):
1000個臉的限制
.GroupAsync
方法一次只能檢測1000
個FaceId
,而上次活動800
多張照片中有超過2000
個FaceId
,因此需要做一些必要的分組。
分組最簡單的方法,就是使用System.Interactive
包,它提供了Rx.NET
那樣方便快捷的API
(這些API
在LINQ
中未提供),但又不需要引入Observable<T>
那樣重量級的東西,因此使用起來很方便。
這里我使用的是.Buffer(int)
函數,它可以將IEnumerable<T>
按指定的數量(如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); }
上述內容就是使用.NET怎么實現一個人臉識別功能,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業(yè)資訊頻道。
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。