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));
断点续传
也如上文所说,有成千上万张照片,如果一旦网络传输异常,或者打翻了桌子上的咖啡(谁知道呢?)……或者完全一切正常,只是想再做一些其它的分析,所有东西又要重新开始。我们可以加入下载中常说的“断点续传”机制。
其实就是一个缓存,记录每个文件读取的结果,然后下次运行时先从缓存中读取即可,缓存到一个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中添加一行代码即可: