using System.Drawing; using System.Drawing.Drawing2D; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using View_by_Distance.FaceRecognitionDotNet; using View_by_Distance.Metadata.Models; using View_by_Distance.Property.Models; using View_by_Distance.Resize.Models; using View_by_Distance.Shared.Models; using View_by_Distance.Shared.Models.Methods; using View_by_Distance.Shared.Models.Stateless; using WindowsShortcutFactory; namespace View_by_Distance.Instance.Models; /// // List /// public class D_Face : Shared.Models.Properties.IFace, IFace { internal List AngleBracketCollection { get; } private readonly Model _Model; private readonly string _ArgZero; private readonly Serilog.ILogger? _Log; private readonly Configuration _Configuration; private readonly ModelParameter _ModelParameter; private readonly PredictorModel _PredictorModel; private readonly JsonSerializerOptions _WriteIndentedJsonSerializerOptions; protected double? _Α; protected DateTime _DateTime; protected Shared.Models.FaceEncoding _FaceEncoding; protected Dictionary _FaceLandmarks; protected Location _Location; protected int? _LocationIndex; protected OutputResolution _OutputResolution; protected bool _Populated; protected string _RelativePath; public double? α => _Α; public DateTime DateTime => _DateTime; public Shared.Models.FaceEncoding FaceEncoding => _FaceEncoding; public Dictionary FaceLandmarks => _FaceLandmarks; public OutputResolution OutputResolution => _OutputResolution; public Location Location => _Location; public int? LocationIndex => _LocationIndex; public bool Populated => _Populated; public string RelativePath => _RelativePath; #nullable disable [JsonConstructor] public D_Face(double? α, DateTime dateTime, Shared.Models.FaceEncoding faceEncoding, Dictionary faceLandmarks, Location location, int? locationIndex, OutputResolution outputResolution, bool populated, string relativePath) { _Α = α; _DateTime = dateTime; _FaceEncoding = faceEncoding; _FaceLandmarks = faceLandmarks; _Location = location; _LocationIndex = locationIndex; _OutputResolution = outputResolution; _Populated = populated; _RelativePath = relativePath; } internal D_Face(Configuration configuration, string argZero, Model model, ModelParameter modelParameter, PredictorModel predictorModel) { _Model = model; _ArgZero = argZero; _Configuration = configuration; _ModelParameter = modelParameter; _PredictorModel = predictorModel; AngleBracketCollection = new List(); _Log = Serilog.Log.ForContext(); _WriteIndentedJsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true }; } private D_Face(Location location) { _Α = α; _DateTime = DateTime.MinValue; _FaceEncoding = null; _FaceLandmarks = null; _OutputResolution = null; _Location = location; _LocationIndex = null; _Populated = false; _RelativePath = string.Empty; } private D_Face() { _Α = α; _DateTime = DateTime.MinValue; _FaceEncoding = null; _FaceLandmarks = null; _OutputResolution = null; _Location = null; _LocationIndex = null; _Populated = false; _RelativePath = string.Empty; } private D_Face(A_Property property, int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation, string relativePath, int? i, Location location) { DateTime?[] dateTimes; dateTimes = new DateTime?[] { property.CreationTime, property.LastWriteTime, property.DateTime, property.DateTimeDigitized, property.DateTimeOriginal, property.GPSDateStamp }; _DateTime = (from l in dateTimes where l.HasValue select l.Value).Min(); _FaceLandmarks = new Dictionary(); _OutputResolution = new(outputResolutionHeight, outputResolutionOrientation, outputResolutionWidth); _Location = location; _LocationIndex = i; _Populated = false; _RelativePath = relativePath; } private D_Face(int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation, Shared.Models.Properties.IFace face) { _Α = face.α; _DateTime = face.DateTime; _FaceEncoding = face.FaceEncoding; _FaceLandmarks = face.FaceLandmarks; _OutputResolution = new(outputResolutionHeight, outputResolutionOrientation, outputResolutionWidth); _Location = face.Location; _LocationIndex = face.LocationIndex; _Populated = face.Populated; _RelativePath = face.RelativePath; } #nullable restore private static void GetPointBounds(PointF[] points, out float xMinimum, out float xMaximum, out float yMinimum, out float yMaximum) { xMinimum = points[0].X; xMaximum = xMinimum; yMinimum = points[0].Y; yMaximum = yMinimum; foreach (PointF point in points) { if (xMinimum > point.X) xMinimum = point.X; if (xMaximum < point.X) xMaximum = point.X; if (yMinimum > point.Y) yMinimum = point.Y; if (yMaximum < point.Y) yMaximum = point.Y; } } public override string ToString() { string result = JsonSerializer.Serialize(this, new JsonSerializerOptions() { WriteIndented = true }); return result; } #pragma warning disable CA1416 internal static Bitmap RotateBitmap(Bitmap bitmap, float angle) { Bitmap result; #if Linux throw new Exception("Built on Linux!"); #elif OSX throw new Exception("Built on macOS!"); #elif Windows // Make a Matrix to represent rotation // by this angle. Matrix rotate_at_origin = new(); rotate_at_origin.Rotate(angle); // Rotate the image's corners to see how big // it will be after rotation. PointF[] points = { new PointF(0, 0), new PointF(bitmap.Width, 0), new PointF(bitmap.Width, bitmap.Height), new PointF(0, bitmap.Height), }; rotate_at_origin.TransformPoints(points); float xMinimum, xMaximum, yMinimum, yMaximum; GetPointBounds(points, out xMinimum, out xMaximum, out yMinimum, out yMaximum); // Make a bitmap to hold the rotated result. int wid = (int)Math.Round(xMaximum - xMinimum); int hgt = (int)Math.Round(yMaximum - yMinimum); result = new Bitmap(wid, hgt); // Create the real rotation transformation. Matrix rotate_at_center = new(); rotate_at_center.RotateAt(angle, new PointF(wid / 2f, hgt / 2f)); // Draw the image onto the new bitmap rotated. using (Graphics gr = Graphics.FromImage(result)) { // Use smooth image interpolation. gr.InterpolationMode = InterpolationMode.High; // Clear with the color in the image's upper left corner. gr.Clear(bitmap.GetPixel(0, 0)); // For debugging. (It's easier to see the background.) // gr.Clear(Color.LightBlue); // Set up the transformation to rotate. gr.Transform = rotate_at_center; // Draw the image centered on the bitmap. int x = (wid - bitmap.Width) / 2; int y = (hgt - bitmap.Height) / 2; gr.DrawImage(bitmap, x, y); } #endif // Return the result bitmap. return result; } private static void SaveFaces(List faceCollection, FileHolder resizedFileHolder, List imageFiles) { int width; int height; Graphics graphics; Location location; Bitmap preRotated; Rectangle rectangle; using Bitmap source = new(resizedFileHolder.FullName); for (int i = 0; i < faceCollection.Count; i++) { if (!faceCollection[i].Populated || faceCollection[i]?.Location is null) continue; location = new Location(faceCollection[i].Location.Confidence, faceCollection[i].Location.Bottom, faceCollection[i].Location.Left, faceCollection[i].Location.Right, faceCollection[i].Location.Top, source.Width, source.Height); width = location.Right - location.Left; height = location.Bottom - location.Top; rectangle = new Rectangle(location.Left, location.Top, width, height); using (preRotated = new(width, height)) { using (graphics = Graphics.FromImage(preRotated)) graphics.DrawImage(source, new Rectangle(0, 0, width, height), rectangle, GraphicsUnit.Pixel); preRotated.Save(imageFiles[i], System.Drawing.Imaging.ImageFormat.Png); } } } private List GetFaces(FileHolder resizedFileHolder, Item item, A_Property property, int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation, string facesDirectory) { List results = new(); if (_Configuration.PaddingLoops is null) throw new Exception(); if (item.ImageFileHolder is null) throw new NullReferenceException(nameof(item.ImageFileHolder)); if (_Configuration.NumberOfJitters is null) throw new NullReferenceException(nameof(_Configuration.NumberOfJitters)); if (_Configuration.NumberOfTimesToUpsample is null) throw new NullReferenceException(nameof(_Configuration.NumberOfTimesToUpsample)); List locations; FaceRecognitionDotNet.Image? unknownImage = null; if (resizedFileHolder.Exists) { try { unknownImage = FaceRecognition.LoadImageFile(resizedFileHolder.FullName); } catch (Exception) { } } if (unknownImage is null) results.Add(new D_Face(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, i: null, location: null)); else { FaceRecognition faceRecognition = FaceRecognition.Create(_ModelParameter); locations = faceRecognition.FaceLocations(_Model, unknownImage, _Configuration.NumberOfTimesToUpsample.Value, sortByPixelPercentage: true); if (!locations.Any()) results.Add(new D_Face(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, i: null, location: null)); else { double? α; int width; int height; int padding; int? leftEyeX; int? leftEyeY; int? rightEyeX; int? rightEyeY; Bitmap rotated; string faceFile; Location location; Bitmap preRotated; Graphics graphics; D_Face? face = null; Rectangle rectangle; double[] rawEncoding; Shared.Models.FaceEncoding faceEncoding; FaceRecognitionDotNet.Image? knownImage; FaceRecognitionDotNet.Image? rotatedImage; List<(FacePart, FacePoint[])[]> facesLandmarks; List faceEncodings; using Bitmap source = unknownImage.ToBitmap(); padding = (int)((source.Width + source.Height) / 2 * .01); for (int i = 0; i < locations.Count; i++) { for (int p = 0; p <= _Configuration.PaddingLoops.Value; p++) { location = new(locations[i].Confidence, locations[i].Bottom + (padding * p), locations[i].Left - (padding * p), locations[i].Right + (padding * p), locations[i].Top - (padding * p), source.Width, source.Height); face = new(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, i, location); width = location.Right - location.Left; height = location.Bottom - location.Top; rectangle = new Rectangle(location.Left, location.Top, width, height); using (preRotated = new Bitmap(width, height)) { leftEyeX = null; leftEyeY = null; rightEyeX = null; rightEyeY = null; using (graphics = Graphics.FromImage(preRotated)) graphics.DrawImage(source, new Rectangle(0, 0, width, height), rectangle, GraphicsUnit.Pixel); // source.Save(Path.Combine(_Configuration.RootDirectory, "source.jpg")); // preRotated.Save(Path.Combine(_Configuration.RootDirectory, $"{p} - preRotated.jpg")); using (knownImage = FaceRecognition.LoadImage(preRotated)) { if (knownImage is null || knownImage.IsDisposed) throw new NullReferenceException(nameof(knownImage)); facesLandmarks = faceRecognition.GetFaceLandmarkCollection(knownImage, _Configuration.NumberOfTimesToUpsample.Value, faceLocations: null, _PredictorModel, _Model); } if (facesLandmarks.Count == 0 && p < _Configuration.PaddingLoops.Value) continue; else if (facesLandmarks.Count != 1) continue; foreach ((FacePart facePart, FacePoint[] facePoints) in facesLandmarks[0]) { face.FaceLandmarks.Add(facePart.ToString(), facePoints); if (facePart is not FacePart.LeftEye and not FacePart.RightEye) continue; if (facePart is FacePart.LeftEye) { leftEyeX = (int)(from l in facePoints select l.X).Average(); leftEyeY = (int)(from l in facePoints select l.Y).Average(); } if (facePart is FacePart.RightEye) { rightEyeX = (int)(from l in facePoints select l.X).Average(); rightEyeY = (int)(from l in facePoints select l.Y).Average(); } } if (rightEyeX is null || leftEyeX is null || rightEyeY is null || leftEyeY is null) continue; α = Shared.Models.Stateless.Methods.IFace.Getα(rightEyeX.Value, leftEyeX.Value, rightEyeY.Value, leftEyeY.Value); using (rotated = RotateBitmap(preRotated, (float)α.Value)) { // rotated.Save(Path.Combine(_Configuration.RootDirectory, $"{p} - rotated.jpg")); using (rotatedImage = FaceRecognition.LoadImage(rotated)) { if (rotatedImage is null || rotatedImage.IsDisposed) throw new NullReferenceException(nameof(rotatedImage)); faceEncodings = faceRecognition.FaceEncodings(rotatedImage, _Configuration.NumberOfTimesToUpsample.Value, knownFaceLocation: null, _Configuration.NumberOfJitters.Value, _PredictorModel, _Model); } if (faceEncodings.Count == 0 && p < _Configuration.PaddingLoops.Value) continue; else if (faceEncodings.Count != 1) continue; rawEncoding = faceEncodings[0].GetRawEncoding(); faceEncoding = new(rawEncoding, faceEncodings[0].Size); face.Update(α, faceEncoding, populated: true); } faceFile = Path.Combine(facesDirectory, $"{i} - {item.ImageFileHolder.NameWithoutExtension}.png"); preRotated.Save(faceFile, System.Drawing.Imaging.ImageFormat.Png); results.Add(face); } if (face.Populated) break; } if (face is null || !face.Populated) { location = new(locations[i].Confidence, locations[i].Bottom, locations[i].Left, locations[i].Right, locations[i].Top, source.Width, source.Height); face = new D_Face(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, i, location); results.Add(face); } } } unknownImage.Dispose(); faceRecognition.Dispose(); } if (!results.Any()) throw new Exception(); return results; } #pragma warning restore CA1416 private void Update(double? α, Shared.Models.FaceEncoding faceEncoding, bool populated) { _Α = α; _FaceEncoding = faceEncoding; _Populated = populated; } internal List GetFaces(Property.Models.Configuration configuration, string outputResolution, List> subFileTuples, List parseExceptions, Item item, A_Property property, FileHolder resizedFileHolder, int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation) { List? results; if (_Configuration.PropertiesChangedForFaces is null) throw new Exception(); if (item.ImageFileHolder is null) throw new NullReferenceException(nameof(item.ImageFileHolder)); string json; D_Face face; bool checkForOutputResolutionChange = false; string[] changesFrom = new string[] { nameof(A_Property), nameof(B_Metadata), nameof(C_Resize) }; List dateTimes = (from l in subFileTuples where changesFrom.Contains(l.Item1) select l.Item2).ToList(); string facesDirectory = Path.Combine(AngleBracketCollection[0].Replace("<>", "()"), item.ImageFileHolder.NameWithoutExtension); FileInfo fileInfo = new(Path.Combine(AngleBracketCollection[0].Replace("<>", "[]"), $"{item.ImageFileHolder.NameWithoutExtension}.json")); if (!fileInfo.Exists) { if (fileInfo.Directory?.Parent is null) throw new Exception(); string parentCheck = Path.Combine(fileInfo.Directory.Parent.FullName, fileInfo.Name); if (File.Exists(parentCheck)) File.Delete(parentCheck); } if (!Directory.Exists(facesDirectory)) _ = Directory.CreateDirectory(facesDirectory); if (_Configuration.PropertiesChangedForFaces.Value) results = null; else if (!fileInfo.Exists) results = null; else if (dateTimes.Any() && dateTimes.Max() > fileInfo.LastWriteTime) results = null; else { json = Shared.Models.Stateless.Methods.IFace.GetJson(fileInfo.FullName); try { results = JsonSerializer.Deserialize>(json); if (results is null) throw new NullReferenceException(nameof(results)); for (int i = 0; i < results.Count; i++) { face = results[i]; if (face.OutputResolution is not null) continue; if (!checkForOutputResolutionChange) checkForOutputResolutionChange = true; results[i] = new(outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, face); } subFileTuples.Add(new Tuple(nameof(D_Face), fileInfo.LastWriteTime)); } catch (Exception) { results = null; parseExceptions.Add(nameof(D_Face)); } } if (results is not null && checkForOutputResolutionChange) { json = JsonSerializer.Serialize(results, _WriteIndentedJsonSerializerOptions); bool updateDateWhenMatches = dateTimes.Any() && fileInfo.Exists && dateTimes.Max() > fileInfo.LastWriteTime; DateTime? dateTime = !updateDateWhenMatches ? null : dateTimes.Max(); if (Property.Models.Stateless.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches, compareBeforeWrite: true, updateToWhenMatches: dateTime)) File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime); } else if (results is null) { results = GetFaces(resizedFileHolder, item, property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, facesDirectory); json = JsonSerializer.Serialize(results, _WriteIndentedJsonSerializerOptions); bool updateDateWhenMatches = dateTimes.Any() && fileInfo.Exists && dateTimes.Max() > fileInfo.LastWriteTime; DateTime? dateTime = !updateDateWhenMatches ? null : dateTimes.Max(); if (Property.Models.Stateless.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches, compareBeforeWrite: true, updateToWhenMatches: dateTime)) subFileTuples.Add(new Tuple(nameof(D_Face), DateTime.Now)); } return results; } internal void SaveFaces(Property.Models.Configuration configuration, List> subFileTuples, List parseExceptions, Item item, List faceCollection) { if (_Configuration.OverrideForFaceImages is null) throw new Exception(); if (item.ImageFileHolder is null) throw new NullReferenceException(nameof(item.ImageFileHolder)); if (item.ResizedFileHolder is null) throw new NullReferenceException(nameof(item.ResizedFileHolder)); FileInfo fileInfo; bool check = false; string parentCheck; List imageFiles = new(); string[] changesFrom = new string[] { nameof(A_Property), nameof(B_Metadata), nameof(C_Resize) }; string facesDirectory = Path.Combine(AngleBracketCollection[0].Replace("<>", "()"), item.ImageFileHolder.NameWithoutExtension); List dateTimes = (from l in subFileTuples where changesFrom.Contains(l.Item1) select l.Item2).ToList(); bool facesDirectoryExisted = Directory.Exists(facesDirectory); if (!facesDirectoryExisted) _ = Directory.CreateDirectory(facesDirectory); for (int i = 0; i < faceCollection.Count; i++) { if (!faceCollection[i].Populated || faceCollection[i]?.Location is null) { imageFiles.Add(string.Empty); continue; } fileInfo = new FileInfo(Path.Combine(facesDirectory, $"{i} - {item.ImageFileHolder.NameWithoutExtension}.png")); if (!fileInfo.Exists) { if (fileInfo.Directory?.Parent is null) throw new Exception(); parentCheck = Path.Combine(fileInfo.Directory.Parent.FullName, fileInfo.Name); if (File.Exists(parentCheck)) File.Delete(parentCheck); } imageFiles.Add(fileInfo.FullName); if (_Configuration.OverrideForFaceImages.Value) check = true; else if (!fileInfo.Exists) check = true; else if (dateTimes.Any() && dateTimes.Max() > fileInfo.LastWriteTime) check = true; } if (check) SaveFaces(faceCollection, item.ResizedFileHolder, imageFiles); } internal static void SaveShortcuts(Property.Models.Configuration configuration, Model? model, PredictorModel? predictorModel, string[] juliePhares, long ticks, Dictionary> peopleCollection, PropertyLogic propertyLogic, string outputResolution, Item[] filteredItems) { Person person; string fileName; string fullName; DateTime? minimumDateTime; WindowsShortcut windowsShortcut; const string pattern = @"[\\,\/,\:,\*,\?,\"",\<,\>,\|]"; string dFacesContentDirectory = Path.Combine(Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(configuration, model, predictorModel, nameof(D_Face), outputResolution, includeResizeGroup: true, includeModel: true, includePredictorModel: true), $"({ticks})"); List<(Item, (string, Shared.Models.Properties.IFace?, (string, string, string, string))[])> collections = Item.GetCollection(propertyLogic, filteredItems, dFacesContentDirectory); foreach ((Item item, (string personKey, Shared.Models.Properties.IFace? _, (string, string, string, string))[] collection) in collections) { if (collection.Length != 1) continue; foreach ((string personKey, Shared.Models.Properties.IFace? _, (string directory, string copyDirectory, string copyFileName, string shortcutFileName)) in collection) { if (string.IsNullOrEmpty(personKey)) continue; if (item.Property?.Id is null || item.ImageFileHolder is null || item.ResizedFileHolder is null) continue; minimumDateTime = Property.Models.Stateless.A_Property.GetMinimumDateTime(item.Property); if (minimumDateTime is null) continue; if (!Directory.Exists(directory)) { _ = Directory.CreateDirectory(directory); if (!string.IsNullOrEmpty(personKey) && peopleCollection.ContainsKey(personKey)) { person = peopleCollection[personKey][0]; fullName = string.Concat(Regex.Replace(Shared.Models.Stateless.Methods.IPersonName.GetFullName(person.Name), pattern, string.Empty), ".txt"); File.WriteAllText(Path.Combine(directory, fullName), string.Empty); } } if (juliePhares.Contains(personKey) && !string.IsNullOrEmpty(copyDirectory)) { if (!Directory.Exists(copyDirectory)) _ = Directory.CreateDirectory(copyDirectory); fileName = Path.Combine(copyDirectory, $"{item.Property.Id.Value}{item.ResizedFileHolder.ExtensionLowered}"); if (!File.Exists(fileName)) File.Copy(item.ResizedFileHolder.FullName, fileName); } fileName = Path.Combine(directory, $"{item.Property.Id.Value}.lnk"); if (File.Exists(fileName)) continue; windowsShortcut = new() { Path = item.ImageFileHolder.FullName }; windowsShortcut.Save(fileName); windowsShortcut.Dispose(); if (!File.Exists(fileName)) continue; File.SetLastWriteTime(fileName, minimumDateTime.Value); } } } double Shared.Models.Stateless.Methods.IFace.TestStatic_Getα(int x1, int x2, int y1, int y2) => throw new NotImplementedException(); string Shared.Models.Stateless.Methods.IFace.TestStatic_GetJson(string jsonFileFullName) => throw new NotImplementedException(); Face Shared.Models.Stateless.Methods.IFace.TestStatic_GetFace(string jsonFileFullName) => throw new NotImplementedException(); Face[] Shared.Models.Stateless.Methods.IFace.TestStatic_GetFaces(string jsonFileFullName) => throw new NotImplementedException(); private static bool HasLeftAndRight(Dictionary faceLandmarks) { bool result = true; if (!faceLandmarks.ContainsKey(FacePart.LeftEye.ToString())) result = false; else if (!faceLandmarks.ContainsKey(FacePart.RightEye.ToString())) result = false; else if (!faceLandmarks.ContainsKey(FacePart.LeftEyebrow.ToString())) result = false; else if (!faceLandmarks.ContainsKey(FacePart.RightEyebrow.ToString())) result = false; return result; } }