using System.Text.Json; using View_by_Distance.Distance.Models.Stateless; using View_by_Distance.FaceRecognitionDotNet; using View_by_Distance.Shared.Models; namespace View_by_Distance.Distance.Models; public partial class E_Distance { private readonly List _Moved; private readonly List _Debug; private readonly List _Renamed; private readonly Serilog.ILogger? _Log; private readonly int _FaceConfidencePercent; private readonly bool _DistanceRenameToMatch; private readonly double[] _RangeFaceConfidence; private readonly bool _DistanceMoveUnableToMatch; private readonly List _AllMappedFaceFiles; private readonly double[] _RangeDistanceTolerance; private readonly int _DistancePixelDistanceTolerance; private readonly List _AllMappedFaceFileNames; private readonly List _DuplicateMappedFaceFiles; public E_Distance(bool distanceMoveUnableToMatch, int distancePixelDistanceTolerance, bool distanceRenameToMatch, int faceConfidencePercent, double[] rangeDistanceTolerance, double[] rangeFaceConfidence) { _Debug = new(); _Moved = new(); _Renamed = new(); _AllMappedFaceFiles = new(); _AllMappedFaceFileNames = new(); _DuplicateMappedFaceFiles = new(); _RangeFaceConfidence = rangeFaceConfidence; _Log = Serilog.Log.ForContext(); _DistanceRenameToMatch = distanceRenameToMatch; _FaceConfidencePercent = faceConfidencePercent; _RangeDistanceTolerance = rangeDistanceTolerance; _DistanceMoveUnableToMatch = distanceMoveUnableToMatch; _DistancePixelDistanceTolerance = distancePixelDistanceTolerance; } private void MoveUnableToMatch(string eDistanceContentDirectory, string mappedFaceFile, string mappedFaceFileName) { bool check; string? directoryName = Path.GetDirectoryName(mappedFaceFile); if (mappedFaceFileName is null || directoryName is null) check = false; else { if (string.IsNullOrEmpty(directoryName) || string.IsNullOrEmpty(directoryName) || !directoryName.Contains(eDistanceContentDirectory)) check = false; else { List directoryNames = new(); string? checkDirectoryName = directoryName; for (int i = 0; i < int.MaxValue; i++) { if (string.IsNullOrEmpty(checkDirectoryName)) continue; directoryNames.Add(Path.GetFileName(checkDirectoryName)); checkDirectoryName = Path.GetDirectoryName(checkDirectoryName); if (string.IsNullOrEmpty(checkDirectoryName)) continue; if (checkDirectoryName == eDistanceContentDirectory) break; } if (string.IsNullOrEmpty(checkDirectoryName) || !directoryNames.Any() || !long.TryParse(directoryNames[^1][1..^1], out long directoryTicks)) { check = false; File.Delete(mappedFaceFile); } else { checkDirectoryName = Path.Combine(checkDirectoryName, $"({directoryTicks}_{string.Join('-', _RangeDistanceTolerance)})"); for (int i = directoryNames.Count - 1 - 1; i > -1; i--) checkDirectoryName = Path.Combine(checkDirectoryName, directoryNames[i]); if (!Directory.Exists(checkDirectoryName)) _ = Directory.CreateDirectory(checkDirectoryName); File.Move(mappedFaceFile, Path.Combine(checkDirectoryName, mappedFaceFileName)); check = true; } } } if (check) _Moved.Add(mappedFaceFile); } public static string? GetFaceEncoding(string file) { string? result; List results = new(); const string comment = "Comment: "; if (File.Exists(file)) { IReadOnlyList directories = MetadataExtractor.ImageMetadataReader.ReadMetadata(file); foreach (MetadataExtractor.Directory directory in directories) { if (directory.Name != "PNG-tEXt") continue; foreach (MetadataExtractor.Tag tag in directory.Tags) { if (tag.Name != "Textual Data" || string.IsNullOrEmpty(tag.Description)) continue; if (!tag.Description.StartsWith(comment)) continue; results.Add(tag.Description); } } } result = results.Any() ? results[0][comment.Length..] : null; return result; } public static string? GetFaceLocation(string file) { string? result; List results = new(); const string artist = "Artist: "; if (File.Exists(file)) { IReadOnlyList directories = MetadataExtractor.ImageMetadataReader.ReadMetadata(file); foreach (MetadataExtractor.Directory directory in directories) { if (directory.Name != "PNG-tEXt") continue; foreach (MetadataExtractor.Tag tag in directory.Tags) { if (tag.Name != "Textual Data" || string.IsNullOrEmpty(tag.Description)) continue; if (!tag.Description.StartsWith(artist)) continue; results.Add(tag.Description); } } } result = results.Any() ? results[0][artist.Length..] : null; return result; } private FaceDistanceContainer[] GetFaceDistanceContainers(MappingFromItem mappingFromItem, Face[] filteredFaces) { FaceDistanceContainer[] results; int confidencePercent; FaceDistance faceDistance; int normalizedPixelPercentage; FaceDistanceContainer faceDistanceContainer; List collection = new(); foreach (Face face in filteredFaces) { if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null) throw new NotSupportedException(); confidencePercent = Shared.Models.Stateless.Methods.ILocation.GetConfidencePercent(_FaceConfidencePercent, _RangeFaceConfidence, face.Location.Confidence); normalizedPixelPercentage = Shared.Models.Stateless.Methods.ILocation.GetNormalizedPixelPercentage(face.Location, Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, face.OutputResolution); if (face.FaceDistance?.Encoding is not null && face.FaceDistance.Encoding is FaceRecognitionDotNet.FaceEncoding faceEncoding) faceDistance = new(confidencePercent, faceEncoding, mappingFromItem.Id, mappingFromItem.IsWrongYear, mappingFromItem.MinimumDateTime, normalizedPixelPercentage); else { faceEncoding = FaceRecognition.LoadFaceEncoding(face.FaceEncoding.RawEncoding); faceDistance = new(confidencePercent, faceEncoding, mappingFromItem.Id, mappingFromItem.IsWrongYear, mappingFromItem.MinimumDateTime, normalizedPixelPercentage); lock (filteredFaces) face.SetFaceDistance(faceDistance); } faceDistanceContainer = new(face, faceDistance); collection.Add(faceDistanceContainer); } results = collection.ToArray(); return results; } private static List GetFaceDistanceEncodings(FaceDistanceContainer[] faceDistanceContainers) { List faceDistanceEncodings = new(); foreach (FaceDistanceContainer faceDistanceContainer in faceDistanceContainers) { if (faceDistanceContainer.FaceDistance.Encoding is null) continue; faceDistanceEncodings.Add(faceDistanceContainer.FaceDistance); } return faceDistanceEncodings; } private List<(Face Face, double? Length)> GetValues(MappingFromItem mappingFromItem, Face[] filteredFaces, string json) { List<(Face Face, double? Length)> results = new(); Face face; FaceDistance faceDistanceLength; Shared.Models.FaceEncoding? modelsFaceEncoding = JsonSerializer.Deserialize(json); if (modelsFaceEncoding is null) throw new NotSupportedException(); FaceRecognitionDotNet.FaceEncoding faceRecognitionDotNetFaceEncoding = FaceRecognition.LoadFaceEncoding(modelsFaceEncoding.RawEncoding); FaceDistance faceDistanceEncoding = new(faceRecognitionDotNetFaceEncoding); FaceDistanceContainer[] faceDistanceContainers = GetFaceDistanceContainers(mappingFromItem, filteredFaces); int faceDistanceContainersLength = faceDistanceContainers.Length; if (faceDistanceContainersLength != filteredFaces.Length) throw new NotSupportedException(); List faceDistanceEncodings = GetFaceDistanceEncodings(faceDistanceContainers); if (faceDistanceEncodings.Count != filteredFaces.Length) throw new NotSupportedException(); List faceDistanceLengths = FaceRecognition.FaceDistances(faceDistanceEncodings, faceDistanceEncoding); if (faceDistanceLengths.Count != faceDistanceContainersLength) throw new NotSupportedException(); for (int i = 0; i < filteredFaces.Length; i++) { face = filteredFaces[i]; faceDistanceLength = faceDistanceLengths[i]; if (faceDistanceLength.Length is null) throw new NotSupportedException(); results.Add(new(face, faceDistanceLength.Length.Value)); } return results; } private (Face, double?)[] GetClosestFaceByDistanceIgnoringTolerance(MappingFromItem mappingFromItem, Face[] filteredFaces, string json) { (Face, double?)[] results; List<(Face Face, double? Length)> collection = GetValues(mappingFromItem, filteredFaces, json); results = (from l in collection orderby l.Length select l).Take(1).ToArray(); (Face face, double? length) = results.First(); lock (_Debug) _Debug.Add(length); return results; } static (int?, int?) GetXY(int normalizedPixelPercentage, OutputResolution? outputResolution) { int? x; int? y; if (outputResolution is null) { x = null; y = null; } else { string normalizedPixelPercentagePadded = Shared.Models.Stateless.Methods.ILocation.GetLeftPadded(Shared.Models.Stateless.ILocation.Digits, normalizedPixelPercentage); (x, y) = Shared.Models.Stateless.Methods.ILocation.GetXY(Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, outputResolution.Width, outputResolution.Height, normalizedPixelPercentagePadded); } return new(x, y); } private (Face, double?)[] GetClosestFaceByPixel(Face[] filteredFaces, int? x1, int? y1) { (Face, double?)[] results; int? x2; int? y2; double distance; int normalizedPixelPercentageLoop; string normalizedPixelPercentagePadded; List<(Face Face, double? Order)> collection = new(); if (x1 is null || y1 is null) throw new NotSupportedException(); foreach (Face face in filteredFaces) { if (face.Location is null || face.OutputResolution is null) throw new NotSupportedException(); normalizedPixelPercentageLoop = Shared.Models.Stateless.Methods.ILocation.GetNormalizedPixelPercentage(face.Location, Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, face.OutputResolution); normalizedPixelPercentagePadded = Shared.Models.Stateless.Methods.ILocation.GetLeftPadded(Shared.Models.Stateless.ILocation.Digits, normalizedPixelPercentageLoop); (x2, y2) = Shared.Models.Stateless.Methods.ILocation.GetXY(Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, face.OutputResolution.Width, face.OutputResolution.Height, normalizedPixelPercentagePadded); if (x2 is null || y2 is null) throw new NotSupportedException(); distance = Math.Sqrt(Math.Pow(x1.Value - x2.Value, 2) + Math.Pow(y1.Value - y2.Value, 2)); collection.Add(new(face, distance)); } results = (from l in collection orderby l.Order where l.Order < _DistancePixelDistanceTolerance select l).Take(1).ToArray(); return results; } private (Face, double?)[] GetClosestFaceByPixel(Face[] filteredFaces, int normalizedPixelPercentage) { if (!filteredFaces.Any()) throw new NotSupportedException(); (Face, double?)[] results; const int zero = 0; OutputResolution? outputResolution = filteredFaces[zero].OutputResolution; (int? x1, int? y1) = GetXY(normalizedPixelPercentage, outputResolution); results = GetClosestFaceByPixel(filteredFaces, x1, y1); return results; } private (Face, double?)[] GetClosestFaceByPixel(Face[] filteredFaces, string json) { if (!filteredFaces.Any()) throw new NotSupportedException(); (Face, double?)[] results; const int zero = 0; OutputResolution? outputResolution = filteredFaces[zero].OutputResolution; if (outputResolution is null) throw new NullReferenceException(nameof(outputResolution)); Location? location = JsonSerializer.Deserialize(json); if (location is null) throw new NullReferenceException(nameof(location)); int normalizedPixelPercentage = Shared.Models.Stateless.Methods.ILocation.GetNormalizedPixelPercentage(location, Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, outputResolution); (int? x1, int? y1) = GetXY(normalizedPixelPercentage, outputResolution); results = GetClosestFaceByPixel(filteredFaces, x1, y1); return results; } private static List<(Face, double?)> GetMatchingFacesByFaceEncoding(Face[] filteredFaces, string? json) { List<(Face, double?)> results = new(); string check; foreach (Face face in filteredFaces) { if (json is null || face.FaceEncoding is null) continue; if (!json.Contains(face.FaceEncoding.RawEncoding[0].ToString())) continue; check = JsonSerializer.Serialize(face.FaceEncoding); if (check != json) continue; results.Add(new(face, 0)); } return results; } private static FileInfo? CheckFileThenGetFileInfo(string facesFileNameExtension, MappingFromItem mappingFromItem, string mappedFaceFile, List<(Face, double?)> checkFaces) { FileInfo? result = null; string checkFile; string? mappedFaceDirectory; string deterministicHashCodeKey; foreach ((Face face, _) in checkFaces) { if (checkFaces.Count != 1) break; if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null) throw new NotSupportedException(); mappedFaceDirectory = Path.GetDirectoryName(mappedFaceFile); if (mappedFaceDirectory is null) throw new NotSupportedException(); deterministicHashCodeKey = Shared.Models.Stateless.Methods.IMapping.GetDeterministicHashCodeKey(mappingFromItem.Id, face.Location, Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, face.OutputResolution); checkFile = Path.Combine(mappedFaceDirectory, $"{deterministicHashCodeKey}{mappingFromItem.ImageFileHolder.ExtensionLowered}{facesFileNameExtension}"); if (checkFile == mappedFaceFile) continue; result = new FileInfo(checkFile); } return result; } private void AppendMatchingDuplicates(string mappedFaceFile, string[] matches) { string checkFile; FileInfo fileInfo = new(mappedFaceFile); List<(long Length, string FullName)> collection = new(); if (fileInfo.Exists) collection.Add(new(fileInfo.Length, fileInfo.FullName)); lock (_DuplicateMappedFaceFiles) _DuplicateMappedFaceFiles.Add(mappedFaceFile); foreach (string match in matches) { fileInfo = new(match); if (!fileInfo.Exists) continue; collection.Add(new(fileInfo.Length, fileInfo.FullName)); break; } collection = collection.OrderBy(l => l.Length).ToList(); for (int i = 0; i < collection.Count - 1; i++) { checkFile = string.Concat(collection[i].FullName, ".dup"); if (File.Exists(checkFile)) continue; File.Move(collection[i].FullName, checkFile); } } public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, string? eDistanceContentDirectory, MappingFromItem mappingFromItem, List faces, List<(string MappedFaceFile, int normalizedPixelPercentage)> collection) { string? json; string[] matches; FileInfo? fileInfo; string mappedFaceFileName; List<(Face, double?)> checkFaces = new(); Face[] filteredFaces = (from l in faces where l.FaceEncoding is not null && l.Location is not null && l.OutputResolution is not null select l).ToArray(); if (filteredFaces.Length != faces.Count) checkFaces.Clear(); foreach ((string mappedFaceFile, int normalizedPixelPercentage) in collection) { if (!filteredFaces.Any()) break; mappedFaceFileName = Path.GetFileName(mappedFaceFile); if (_DuplicateMappedFaceFiles.Contains(mappedFaceFileName)) continue; checkFaces.Clear(); json = GetFaceEncoding(mappedFaceFile); if (json is null) { if (!string.IsNullOrEmpty(eDistanceContentDirectory) && _DistanceMoveUnableToMatch) MoveUnableToMatch(eDistanceContentDirectory, mappedFaceFile, mappedFaceFileName); continue; } checkFaces.AddRange(GetMatchingFacesByFaceEncoding(filteredFaces, json)); if (checkFaces.Count == 1) _Debug.Add(0); if (checkFaces.Count != 1 && !string.IsNullOrEmpty(json)) { checkFaces.Clear(); if (json is null) throw new NotSupportedException(); checkFaces.AddRange(GetClosestFaceByDistanceIgnoringTolerance(mappingFromItem, filteredFaces, json)); } if (checkFaces.Count != 1 && _DistancePixelDistanceTolerance > 0) { checkFaces.Clear(); json = GetFaceLocation(mappedFaceFile); if (json is not null) checkFaces.AddRange(GetClosestFaceByPixel(filteredFaces, json)); else checkFaces.AddRange(GetClosestFaceByPixel(filteredFaces, normalizedPixelPercentage)); throw new NotImplementedException("Without a tolerance this should not ever occur!"); } if (!checkFaces.Any()) { if (!string.IsNullOrEmpty(eDistanceContentDirectory) && _DistanceMoveUnableToMatch) MoveUnableToMatch(eDistanceContentDirectory, mappedFaceFile, mappedFaceFileName); continue; } if (checkFaces.Count != 1) { if (!string.IsNullOrEmpty(eDistanceContentDirectory) && _DistanceMoveUnableToMatch) MoveUnableToMatch(eDistanceContentDirectory, mappedFaceFile, mappedFaceFileName); continue; } fileInfo = CheckFileThenGetFileInfo(facesFileNameExtension, mappingFromItem, mappedFaceFile, checkFaces); if (fileInfo is not null) { if (_DistanceRenameToMatch && fileInfo is not null) { if (fileInfo.Exists) File.Delete(fileInfo.FullName); File.Move(mappedFaceFile, fileInfo.FullName); _Renamed.Add(mappedFaceFile); } continue; } if (_AllMappedFaceFileNames.Contains(mappedFaceFileName)) { lock (_AllMappedFaceFiles) matches = (from l in _AllMappedFaceFiles where l != mappedFaceFile && Path.GetFileName(l) == mappedFaceFileName select l).ToArray(); if (matches.Any()) AppendMatchingDuplicates(mappedFaceFile, matches); } lock (_AllMappedFaceFiles) _AllMappedFaceFiles.Add(mappedFaceFile); lock (_AllMappedFaceFileNames) _AllMappedFaceFileNames.Add(mappedFaceFileName); } } public void Clear() { if (_Log is null) throw new NullReferenceException(nameof(_Log)); double?[] debug = (from l in _Debug where l is null or not 0 select l).ToArray(); if (debug.Any()) { string debugMessage = $"{_Debug.Count - debug.Length} - {debug.Min()} - {_Debug.Max()}"; _Log.Info(debugMessage); } if (_Moved.Any() || _Renamed.Any() || _DuplicateMappedFaceFiles.Any()) throw new NotImplementedException("Restart!"); _Debug.Clear(); _Moved.Clear(); _Renamed.Clear(); _AllMappedFaceFiles.Clear(); _AllMappedFaceFileNames.Clear(); _DuplicateMappedFaceFiles.Clear(); } }