.NET做人臉識別并分類的實現示例

 更新時間:2020-01-15 16:59:07   作者:佚名   我要評論(0)

在游樂場、玻璃天橋、滑雪場等娛樂場所,經常能看到有攝影師在拍照片,令這些經營者發愁的一件事就是照片太多了,客戶在成千上萬張照片中找到自己可不是件容易的事。

在游樂場、玻璃天橋、滑雪場等娛樂場所,經常能看到有攝影師在拍照片,令這些經營者發愁的一件事就是照片太多了,客戶在成千上萬張照片中找到自己可不是件容易的事。在一次游玩等活動或家庭聚會也同理,太多了照片導致挑選十分困難。

還好有.NET,只需少量代碼,即可輕松找到人臉并完成分類。

本文將使用Microsoft Azure云提供的認知服務Cognitive ServicesAPI來識別并進行人臉分類,可以免費使用,注冊地址是:https://portal.azure.com。注冊完成后,會得到兩個密鑰,通過這個密鑰即可完成本文中的所有代碼,這個密鑰長這個樣子(非真實密鑰):

fa3a7bfd807ccd6b17cf559ad584cbaa

使用方法

首先安裝NuGetMicrosoft.Azure.CognitiveServices.Vision.Face,目前最新版是2.5.0-preview.1,然后創建一個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,用于后續的識別,用FaceRectangle保存了其人臉的位置信息,可供對其做進一步操作。RecognitionModelFaceLandmarksFaceAttributes是一些額外屬性,包括識別性別年齡表情等信息,默認不識別,如下圖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.

因此,如果圖片太大,必須進行一定的壓縮(當然如果圖片太小,顯然也沒必要進行壓縮了),使用.NETBitmap,并結合C# 8.0switch 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#/.NETLINQ支持,只需加一行.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));

斷點續傳

也如上文所說,有成千上萬張照片,如果一旦網絡傳輸異常,或者打翻了桌子上的咖啡(誰知道呢?)……或者完全一切正常,只是想再做一些其它的分析,所有東西又要重新開始。我們可以加入下載中常說的“斷點續傳”機制。

其實就是一個緩存,記錄每個文件讀取的結果,然后下次運行時先從緩存中讀取即可,緩存到一個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方法一次只能檢測1000FaceId,而上次活動800多張照片中有超過2000FaceId,因此需要做一些必要的分組。

分組最簡單的方法,就是使用System.Interactive包,它提供了Rx.NET那樣方便快捷的API(這些APILINQ中未提供),但又不需要引入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);
}

總結

文中用到的完整代碼,全部上傳了到我的博客數據Github,只要輸入圖片和key,即可直接使用和運行:
https://github.com/sdcb/blog-data/tree/master/2019/20191122-dotnet-face-detection

這個月我參加了上海的.NET Conf,我上述代碼對.NET Conf800多張照片做了分組,識別出了2000多張人臉,我將其中我的照片的前三張找出來,結果如下:




......

總的來說,這個效果還挺不錯,渣渣分辨率的照片的臉都被它找到了😂。

注意,不一定非得用Azure Cognitive Services來做人臉識別,國內還有阿里云等廠商也提供了人臉識別等服務,并提供了.NET接口,無非就是調用API,注意其限制,代碼總體差不多。

另外,如有離線人臉識別需求,Luxand提供了還有離線版人臉識別SDK,名叫Luxand FaceSDK,同樣提供了.NET接口。因為無需網絡調用,其識別更快,匹配速度更是可達每秒5千萬個人臉數據,精度也非常高,親測好用,目前最新版是v7.1.0,授權昂貴(但百度有驚喜)。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

您可能感興趣的文章:

  • opencv 做人臉識別 opencv 人臉匹配分析
  • 基于OpenCV的PHP圖像人臉識別技術
  • Android camera實時預覽 實時處理,人臉識別示例
  • python實現人臉識別經典算法(一) 特征臉法
  • JavaScript人臉識別技術及臉部識別JavaScript類庫Tracking.js
  • android實現人臉識別技術的示例代碼
  • 微信小程序實現人臉識別
  • PHP使用Face++接口開發微信公眾平臺人臉識別系統的方法
  • 人臉識別經典算法一 特征臉方法(Eigenface)
  • python3+dlib實現人臉識別和情緒分析
  • 詳解如何用OpenCV + Python 實現人臉識別
  • Python3結合Dlib實現人臉識別和剪切

相關文章

  • .NET做人臉識別并分類的實現示例

    .NET做人臉識別并分類的實現示例

    在游樂場、玻璃天橋、滑雪場等娛樂場所,經常能看到有攝影師在拍照片,令這些經營者發愁的一件事就是照片太多了,客戶在成千上萬張照片中找到自己可不是件容易的事。
    2020-01-15
  • Go 中 slice 的 In 功能實現探索

    Go 中 slice 的 In 功能實現探索

    之前在知乎看到一個問題:為什么 Golang 沒有像 Python 中 in 一樣的功能?于是,搜了下這個問題,發現還是有不少人有這樣的疑問。 今天來談談這個話題。 in 是一個
    2020-01-15
  • 詳解Go中Set的實現方式

    詳解Go中Set的實現方式

    本篇主要講述如何利用Go語言的語法特性實現Set類型的數據結構。 需求 對于Set類型的數據結構,其實本質上跟List沒什么多大的區別。無非是Set不能含有重復的Item的
    2020-01-15
  • Go中如何使用set的方法示例

    Go中如何使用set的方法示例

    今天來聊一下 Go 如何使用 set,本文將會涉及 set 和 bitset 兩種數據結構。 Go 的數據結構 Go 內置的數據結構并不多。工作中,我們最常用的兩種數據結構分別是
    2020-01-15
  • ./ 和 sh 的使用區別詳解

    ./ 和 sh 的使用區別詳解

    ./ 和 sh的使用區別 1、使用“./”執行腳本,對應的xxx.sh腳本必須要有執行權限; 2、使用“sh” 執行腳本,對應的xxx.sh沒有執行權限,亦可執行; 3
    2020-01-15
  • win10下如何運行.sh文件的實現步驟

    win10下如何運行.sh文件的實現步驟

    確保您使用至少是Windows10的14316版本。 這種方法只適用于64位版本的Windows 10 今天居然驚奇地發現原來win10的功能簡直強大到沒話說,居然在更新后有一個Linux的子
    2020-01-15
  • Shell腳本之Expect免交互的實現

    Shell腳本之Expect免交互的實現

    Expext概述 Expect是建立在tcl基礎上的一個工具,Expect是用來自動化控制和測試的工具。主要解決shell腳本中不可交互的問題。有助于大規模的系統運維工作。在日常
    2020-01-15
  • Shell腳本實戰之DNS主從同步腳本實例

    Shell腳本實戰之DNS主從同步腳本實例

    DNS主從同步腳本實例 PS:兩個服務器起好后最好兩個服務都重啟一下 主服務器配置 #!/bin/bash #DNS主從同步——主服務器 rpm -q bind if [ $&#63; -ne
    2020-01-15
  • shell之分離解析腳本的實現方法

    shell之分離解析腳本的實現方法

    分離解析腳本 在運行腳本之前,需要VM虛擬機,Centos7,兩臺主機一臺win10 -1 作為廣域網的主機, 一臺win10 -2作為區域網的主機。 之前我的博客有教程 #!/bin/ba
    2020-01-15
  • shell之正向解析腳本的實現方法

    shell之正向解析腳本的實現方法

    正向解析腳本 #!/bin/bash yum install bind -y //安裝解析工具包 //修改主配置文件 sed -i '13s/127.0.0.1/192.168.17.156/' /etc/named.conf //把解析主配
    2020-01-15

最新評論

老快3投注技巧 牛壹佰配资 短期理财 pk10最牛稳赚5码计划一期 股票软件鑫东财配资 好运彩app下载 秒速赛车官方开奖记录 亚洲杯女篮决赛录像 股票融资买入的步骤 嘉盛投资 快乐双彩今晚开奖号码 正规在线股票配资平台 贵州快三推荐号码今天 排列5选号绝招759 北京pk10冠军走势图 吉林快3 预测号码 河北11选五软件哪个好