using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Text.Json; 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.Stateless; using WindowsShortcutFactory; namespace View_by_Distance.Instance.Models; /// // List /// public class D_Face { internal List AngleBracketCollection { get; } private readonly Model _Model; private readonly string _ArgZero; private readonly Serilog.ILogger? _Log; private readonly string _FilenameExtension; private readonly Configuration _Configuration; private readonly ModelParameter _ModelParameter; private readonly PredictorModel _PredictorModel; private readonly ImageCodecInfo _ImageCodecInfo; private readonly EncoderParameters _EncoderParameters; private readonly JsonSerializerOptions _WriteIndentedJsonSerializerOptions; internal D_Face(Configuration configuration, string argZero, Model model, ModelParameter modelParameter, PredictorModel predictorModel, ImageCodecInfo imageCodecInfo, EncoderParameters encoderParameters, string filenameExtension) { _Model = model; _ArgZero = argZero; _Configuration = configuration; _ModelParameter = modelParameter; _PredictorModel = predictorModel; _ImageCodecInfo = imageCodecInfo; _EncoderParameters = encoderParameters; _FilenameExtension = filenameExtension; AngleBracketCollection = new List(); _Log = Serilog.Log.ForContext(); _WriteIndentedJsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true }; } 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 void SaveFaces(FileHolder resizedFileHolder, List<(Face, string)> collection) { int width; int height; Graphics graphics; Location? location; Bitmap preRotated; Rectangle rectangle; using Bitmap source = new(resizedFileHolder.FullName); foreach ((Face face, string fileName) in collection) { if (face.FaceEncoding is null || face?.Location is null) continue; location = Shared.Models.Stateless.Methods.ILocation.GetLocation(face.Location, source.Height, source.Width, collection.Count); if (location is null) continue; 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(fileName, _ImageCodecInfo, _EncoderParameters); } } } private List GetFaces(FileHolder resizedFileHolder, Item item, Shared.Models.Property property, int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation) { List results = new(); if (item.ImageFileHolder is null) throw new NullReferenceException(nameof(item.ImageFileHolder)); FaceRecognitionDotNet.Image? unknownImage; if (!resizedFileHolder.Exists) unknownImage = null; else { try { unknownImage = FaceRecognition.LoadImageFile(resizedFileHolder.FullName); } catch (Exception) { unknownImage = null; } } if (unknownImage is null) results.Add(new(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, i: null, location: null)); else { List<(Location Location, FaceRecognitionDotNet.FaceEncoding? FaceEncoding, Dictionary? FaceParts)> collection; FaceRecognition faceRecognition = new(_Configuration.NumberOfTimesToUpsample, _Configuration.NumberOfJitters, _PredictorModel, _Model, _ModelParameter); collection = faceRecognition.GetCollection(unknownImage, includeFaceEncoding: true, includeFaceParts: true, sortByNormalizedPixelPercentage: true); if (!collection.Any()) results.Add(new(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, i: null, location: null)); else { int i = 0; Face face; double[] rawEncoding; Shared.Models.FaceEncoding convertedFaceEncoding; foreach ((Location location, FaceRecognitionDotNet.FaceEncoding? faceEncoding, Dictionary? faceParts) in collection) { face = new(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, i, location); if (faceEncoding is not null) { rawEncoding = faceEncoding.GetRawEncoding(); convertedFaceEncoding = new(rawEncoding, faceEncoding.Size); face.SetFaceEncoding(convertedFaceEncoding); } if (faceParts is not null) face.SetFaceParts(faceParts); results.Add(face); i += 1; } } unknownImage.Dispose(); faceRecognition.Dispose(); } if (!results.Any()) throw new Exception(); return results; } #pragma warning restore CA1416 internal List GetFaces(string dResultsFullGroupDirectory, List> subFileTuples, List parseExceptions, Item item, Shared.Models.Property property, FileHolder resizedFileHolder, int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation) { List? results; if (item.Property?.Id is null) throw new NullReferenceException(nameof(item.Property.Id)); if (item.ImageFileHolder is null) throw new NullReferenceException(nameof(item.ImageFileHolder)); if (string.IsNullOrEmpty(dResultsFullGroupDirectory)) throw new NullReferenceException(nameof(dResultsFullGroupDirectory)); string json; int?[] normalizedPixelPercentageCollection; int normalizedPixelPercentageDistinctCount; 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 usingRelativePath = Path.Combine(AngleBracketCollection[0].Replace("<>", "[]"), $"{item.ImageFileHolder.NameWithoutExtension}.json"); string dCollectionFile = Path.Combine(dResultsFullGroupDirectory, "[]", Property.Models.Stateless.IResult.AllInOne, $"{item.Property.Id.Value}{item.ImageFileHolder.ExtensionLowered}.json"); FileInfo fileInfo = new(dCollectionFile); if (!fileInfo.Exists) { if (File.Exists(usingRelativePath)) { File.Move(usingRelativePath, fileInfo.FullName); fileInfo.Refresh(); } 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.Move(parentCheck, fileInfo.FullName); fileInfo.Refresh(); } } } if (_Configuration.ForceFaceLastWriteTimeToCreationTime && !fileInfo.Exists && File.Exists(Path.ChangeExtension(fileInfo.FullName, ".delete"))) { File.Move(Path.ChangeExtension(fileInfo.FullName, ".delete"), fileInfo.FullName); fileInfo.Refresh(); } if (_Configuration.ForceFaceLastWriteTimeToCreationTime && fileInfo.Exists && fileInfo.LastWriteTime != fileInfo.CreationTime) { File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime); fileInfo.Refresh(); } if (_Configuration.PropertiesChangedForFaces) 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)); if (!_Configuration.ForceFaceLastWriteTimeToCreationTime) { normalizedPixelPercentageCollection = Shared.Models.Stateless.Methods.IFace.GetInts(results); if (normalizedPixelPercentageCollection.Contains(3)) throw new Exception($"Not allowed! <{fileInfo.FullName}>"); normalizedPixelPercentageDistinctCount = normalizedPixelPercentageCollection.Distinct().Count(); if (normalizedPixelPercentageDistinctCount != normalizedPixelPercentageCollection.Length) throw new Exception($"Not distinct! <{fileInfo.FullName}>"); } subFileTuples.Add(new Tuple(nameof(D_Face), fileInfo.LastWriteTime)); } catch (Exception) { results = null; parseExceptions.Add(nameof(D_Face)); } } if (results is null) { results = GetFaces(resizedFileHolder, item, property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation); json = JsonSerializer.Serialize(results, _WriteIndentedJsonSerializerOptions); bool updateDateWhenMatches = dateTimes.Any() && fileInfo.Exists && dateTimes.Max() > fileInfo.LastWriteTime; DateTime? dateTime = !updateDateWhenMatches ? null : dateTimes.Max(); if (Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches, compareBeforeWrite: true, updateToWhenMatches: dateTime)) subFileTuples.Add(new Tuple(nameof(D_Face), DateTime.Now)); } if (_Configuration.ForceFaceLastWriteTimeToCreationTime) { results = (from l in results select new Face(results.Count, l)).ToList(); normalizedPixelPercentageCollection = Shared.Models.Stateless.Methods.IFace.GetInts(results); normalizedPixelPercentageDistinctCount = normalizedPixelPercentageCollection.Distinct().Count(); if (normalizedPixelPercentageDistinctCount != normalizedPixelPercentageCollection.Length) throw new Exception($"Not distinct! <{fileInfo.FullName}>"); json = JsonSerializer.Serialize(results, _WriteIndentedJsonSerializerOptions); bool updateDateWhenMatches = dateTimes.Any() && fileInfo.Exists && dateTimes.Max() > fileInfo.LastWriteTime; DateTime? dateTime = !updateDateWhenMatches ? null : dateTimes.Max(); if (Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches, compareBeforeWrite: true, updateToWhenMatches: dateTime)) { File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime); fileInfo.Refresh(); subFileTuples.Add(new Tuple(nameof(D_Face), fileInfo.CreationTime)); } } return results; } internal void SaveFaces(string dResultsFullGroupDirectory, List> subFileTuples, List parseExceptions, Item item, List faceCollection) { 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; double deterministicHashCodeKey; List<(Face, string)> collection = 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); foreach (Face face in faceCollection) { if (face.FaceEncoding is null || face.Location?.NormalizedPixelPercentage is null) { collection.Add(new(face, string.Empty)); continue; } deterministicHashCodeKey = Shared.Models.Stateless.Methods.INamed.GetDeterministicHashCodeKey(item, face); fileInfo = new FileInfo(Path.Combine(facesDirectory, $"{deterministicHashCodeKey}{item.ImageFileHolder.ExtensionLowered}{_FilenameExtension}")); 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); } collection.Add(new(face, fileInfo.FullName)); if (_Configuration.OverrideForFaceImages) check = true; else if (!fileInfo.Exists) check = true; else if (dateTimes.Any() && dateTimes.Max() > fileInfo.LastWriteTime) check = true; } if (check) SaveFaces(item.ResizedFileHolder, collection); } internal static void SaveShortcuts(string[] juliePhares, string dResultsFullGroupDirectory, long ticks, Dictionary> peopleCollection, Map.Models.MapLogic mapLogic, List items) { Person person; string fileName; string fullName; DateTime? minimumDateTime; WindowsShortcut windowsShortcut; const string pattern = @"[\\,\/,\:,\*,\?,\"",\<,\>,\|]"; string dFacesContentDirectory = Path.Combine(dResultsFullGroupDirectory, $"({ticks})"); List<(Item, (string, Face?, (string, string, string, string))[])> collections = Map.Models.MapLogic.GetCollection(mapLogic, items, dFacesContentDirectory); foreach ((Item item, (string personKey, Face? _, (string, string, string, string))[] collection) in collections) { if (collection.Length != 1) continue; foreach ((string personKey, Face? _, (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 = Shared.Models.Stateless.Methods.IProperty.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); } } } private static bool HasLeftAndRight(Dictionary faceParts) { bool result = true; if (!faceParts.ContainsKey(FacePart.LeftEye.ToString())) result = false; else if (!faceParts.ContainsKey(FacePart.RightEye.ToString())) result = false; else if (!faceParts.ContainsKey(FacePart.LeftEyebrow.ToString())) result = false; else if (!faceParts.ContainsKey(FacePart.RightEyebrow.ToString())) result = false; return result; } }