using System.Collections.ObjectModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Reflection; using System.Text; using System.Text.Json; using View_by_Distance.Face.Models; using View_by_Distance.Metadata.Models; using View_by_Distance.Metadata.Models.Stateless.Methods; using View_by_Distance.Property.Models; using View_by_Distance.Property.Models.Stateless; using View_by_Distance.Resize.Models; using View_by_Distance.Shared.Models; using View_by_Distance.Shared.Models.Properties; using View_by_Distance.Shared.Models.Stateless; namespace View_by_Distance.FaceParts.Models; /// // *.png /// public class D2_FaceParts { protected readonly string _FileNameExtension; public string FileNameExtension => _FileNameExtension; private readonly ImageCodecInfo _ImageCodecInfo; private readonly bool _CheckDFaceAndUpWriteDates; private readonly ConstructorInfo _ConstructorInfo; private readonly bool _OverrideForFaceLandmarkImages; private readonly EncoderParameters _EncoderParameters; private readonly IPropertyConfiguration _PropertyConfiguration; private readonly Dictionary> _FileGroups; public D2_FaceParts(IPropertyConfiguration propertyConfiguration, ImageCodecInfo imageCodecInfo, EncoderParameters encoderParameters, string filenameExtension, bool checkDFaceAndUpWriteDates, bool overrideForFaceLandmarkImages) { _FileGroups = []; _ImageCodecInfo = imageCodecInfo; _EncoderParameters = encoderParameters; _FileNameExtension = filenameExtension; _PropertyConfiguration = propertyConfiguration; _CheckDFaceAndUpWriteDates = checkDFaceAndUpWriteDates; _OverrideForFaceLandmarkImages = overrideForFaceLandmarkImages; ConstructorInfo? constructorInfo = typeof(PropertyItem).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, [], null) ?? throw new Exception(); _ConstructorInfo = constructorInfo; } public override string ToString() { string result = JsonSerializer.Serialize(this, new JsonSerializerOptions() { WriteIndented = true }); return result; } public void Update(string dResultsFullGroupDirectory) { _FileGroups.Clear(); ReadOnlyDictionary> keyValuePairs = Shared.Models.Stateless.Methods.IPath.GetKeyValuePairs(_PropertyConfiguration, dResultsFullGroupDirectory, [_PropertyConfiguration.ResultContent]); foreach (KeyValuePair> keyValuePair in keyValuePairs) _FileGroups.Add(keyValuePair.Key, keyValuePair.Value); } 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; } } #pragma warning disable CA1416 private 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 save 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(0, 0), new(bitmap.Width, 0), new(bitmap.Width, bitmap.Height), new(0, bitmap.Height), ]; rotate_at_origin.TransformPoints(points); float xMinimum, xMaximum, yMinimum, yMaximum; GetPointBounds(points, out xMinimum, out xMaximum, out yMinimum, out yMaximum); // Make save 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 Bitmap RotateBitmap(Image image, float angle) { Bitmap result; Bitmap bitmap = new(image); result = RotateBitmap(bitmap, angle); bitmap?.Dispose(); return result; } private void SaveRotated(MappingFromItem mappingFromItem, List<(Shared.Models.Face, string, string)> collection) { double? α; Bitmap rotated; foreach ((Shared.Models.Face face, string _, string rotatedFileName) in collection) { if (face.FaceParts is null) continue; (_, α) = Shared.Models.Stateless.Methods.IFace.GetEyeα(face.FaceParts); if (α is null) continue; using Image image = Image.FromFile(mappingFromItem.ResizedFileHolder.FullName); rotated = RotateBitmap(image, (float)α.Value); if (rotated is not null) { rotated.Save(rotatedFileName, _ImageCodecInfo, _EncoderParameters); rotated.Dispose(); } } } private string GetSeasonDirectory(string d2ResultsFullGroupDirectory, MappingFromItem mappingFromItem, bool any) { string result; string minimumDateYear = mappingFromItem.MinimumDateTime.ToString("yyyy"); DateTime dateTime = mappingFromItem.DateTimeOriginal is null ? mappingFromItem.MinimumDateTime : mappingFromItem.DateTimeOriginal.Value; (int season, string seasonName) = Shared.Models.Stateless.Methods.IProperty.GetSeason(dateTime.DayOfYear); string year = mappingFromItem.DateTimeOriginal is null ? $"{minimumDateYear[1..]}{minimumDateYear[0]}" : mappingFromItem.DateTimeOriginal.Value.ToString("yyyy"); string directory = Path.Combine(d2ResultsFullGroupDirectory, $"[{_PropertyConfiguration.ResultContent}]", $"{year}.{season} {seasonName}"); result = any ? Path.Combine(directory, "---") : Path.Combine(directory, "Complete"); if (!Directory.Exists(result)) _ = Directory.CreateDirectory(result); return result; } private void SaveImage(MappingFromItem mappingFromItem, string directory, Image image, List faceFiles) { short type = 2; string faceFileJson; PropertyItem? propertyItem; const int artist = MetadataExtractor.Formats.Exif.ExifDirectoryBase.TagArtist; // 315 string fileName = Path.Combine(directory, $"{mappingFromItem.FilePath.Name}{_FileNameExtension}"); try { foreach (int propertyId in image.PropertyIdList) { if (propertyId == MetadataExtractor.Formats.Exif.ExifDirectoryBase.TagOrientation) continue; image.RemovePropertyItem(propertyId); } faceFileJson = JsonSerializer.Serialize(faceFiles.ToArray(), FaceFileCollectionGenerationContext.Default.FaceFileArray); propertyItem = IProperty.GetPropertyItem(_ConstructorInfo, artist, type, faceFileJson); image.SetPropertyItem(propertyItem); image.Save(fileName, _ImageCodecInfo, _EncoderParameters); } catch (Exception ex) { if (ex is not null && !string.IsNullOrEmpty(fileName) && File.Exists(fileName)) File.Delete(fileName); faceFileJson = JsonSerializer.Serialize(faceFiles.ToArray(), FaceFileCollectionGenerationContext.Default.FaceFileArray); if (!string.IsNullOrEmpty(faceFileJson)) File.WriteAllText($"{fileName}.json", faceFileJson); } } private void SaveAllFaceParts(string d2ResultsFullGroupDirectory, MappingFromItem mappingFromItem, ExifDirectory exifDirectory, List faces) { int x; int y; Brush brush; int pointSize; bool any = false; FaceFile faceFile; bool? isDefaultName; List personKeys = []; List faceFiles = []; StringBuilder stringBuilder = new(); MappingFromPerson? mappingFromPerson; string? maker = IMetadata.GetMaker(exifDirectory); string? model = IMetadata.GetModel(exifDirectory); MetadataExtractor.GeoLocation? geoLocation = IMetadata.GeoLocation(exifDirectory); using Image image = Image.FromFile(mappingFromItem.ResizedFileHolder.FullName); using Graphics graphics = Graphics.FromImage(image); foreach (Shared.Models.Face face in faces) { if (face.Location is null || face.FaceEncoding is null || face.FaceParts is null || face.FaceParts.Count == 0) continue; if (!any && face.Mapping?.MappingFromPerson is null) any = true; mappingFromPerson = face.Mapping?.MappingFromPerson; brush = mappingFromPerson is null ? Brushes.Red : Brushes.GreenYellow; isDefaultName = mappingFromPerson is null ? null : Shared.Models.Stateless.Methods.IPerson.IsDefaultName(mappingFromPerson); if (mappingFromPerson is not null && isDefaultName is not null && !isDefaultName.Value) personKeys.Add(mappingFromPerson.PersonKey); faceFile = new(face.Mapping?.MappingFromLocation?.AreaPermyriad, face.Mapping?.MappingFromLocation?.ConfidencePercent, geoLocation?.ToDmsString(), face.DateTime, face.FaceEncoding, face.FaceParts, face.Location, maker, mappingFromPerson, model, face.OutputResolution); faceFiles.Add(faceFile); pointSize = GetPointSize(face.FaceParts, defaultPointSize: 2); foreach ((FacePart facePart, FacePoint[] facePoints) in face.FaceParts) { foreach (FacePoint facePoint in facePoints) graphics.FillEllipse(brush, facePoint.X - pointSize, facePoint.Y - pointSize, pointSize * 2, pointSize * 2); if (facePart == FacePart.Chin) continue; if (facePoints.Length < 3) continue; x = (int)(from l in facePoints select l.X).Average(); y = (int)(from l in facePoints select l.Y).Average(); graphics.FillEllipse(Brushes.Purple, x - pointSize, y - pointSize, pointSize * 2, pointSize * 2); } } _ = graphics.Save(); string directory = GetSeasonDirectory(d2ResultsFullGroupDirectory, mappingFromItem, any); SaveImage(mappingFromItem, directory, image, faceFiles); } private void SaveImage(string fileName, Image image, FaceFile faceFile) { short type = 2; string faceFileJson; PropertyItem? propertyItem; const int artist = MetadataExtractor.Formats.Exif.ExifDirectoryBase.TagArtist; // 315 try { foreach (int propertyId in image.PropertyIdList) { if (propertyId == MetadataExtractor.Formats.Exif.ExifDirectoryBase.TagOrientation) continue; image.RemovePropertyItem(propertyId); } faceFileJson = JsonSerializer.Serialize(faceFile, FaceFileGenerationContext.Default.FaceFile); propertyItem = IProperty.GetPropertyItem(_ConstructorInfo, artist, type, faceFileJson); image.SetPropertyItem(propertyItem); image.Save(fileName, _ImageCodecInfo, _EncoderParameters); } catch (Exception ex) { if (ex is not null && !string.IsNullOrEmpty(fileName) && File.Exists(fileName)) File.Delete(fileName); faceFileJson = JsonSerializer.Serialize(faceFile, FaceFileGenerationContext.Default.FaceFile); if (!string.IsNullOrEmpty(faceFileJson)) File.WriteAllText($"{fileName}.json", faceFileJson); } } private void SaveFaceParts(MappingFromItem mappingFromItem, ExifDirectory exifDirectory, List<(Shared.Models.Face, string, string)> collection) { int x; int y; Brush brush; int pointSize; FaceFile faceFile; MappingFromPerson? mappingFromPerson; string? maker = IMetadata.GetMaker(exifDirectory); string? model = IMetadata.GetModel(exifDirectory); MetadataExtractor.GeoLocation? geoLocation = IMetadata.GeoLocation(exifDirectory); foreach ((Shared.Models.Face face, string fileName, string _) in collection) { try { if (face.Location is null || face.FaceEncoding is null || face.FaceParts is null || face.FaceParts.Count == 0) continue; using Image image = Image.FromFile(mappingFromItem.ResizedFileHolder.FullName); mappingFromPerson = face.Mapping?.MappingFromPerson; brush = mappingFromPerson is null ? Brushes.Red : Brushes.GreenYellow; faceFile = new(face.Mapping?.MappingFromLocation?.AreaPermyriad, face.Mapping?.MappingFromLocation?.ConfidencePercent, geoLocation?.ToDmsString(), face.DateTime, face.FaceEncoding, face.FaceParts, face.Location, maker, mappingFromPerson, model, face.OutputResolution); using Graphics graphics = Graphics.FromImage(image); pointSize = GetPointSize(face.FaceParts, defaultPointSize: 2); foreach ((FacePart facePart, FacePoint[] facePoints) in face.FaceParts) { foreach (FacePoint facePoint in facePoints) graphics.FillEllipse(brush, facePoint.X - pointSize, facePoint.Y - pointSize, pointSize * 2, pointSize * 2); if (facePart == FacePart.Chin) continue; if (facePoints.Length < 3) continue; x = (int)(from l in facePoints select l.X).Average(); y = (int)(from l in facePoints select l.Y).Average(); graphics.FillEllipse(Brushes.Purple, x - pointSize, y - pointSize, pointSize * 2, pointSize * 2); } _ = graphics.Save(); SaveImage(fileName, image, faceFile); } catch (Exception) { if (File.Exists(fileName)) File.Delete(fileName); } } } private int GetPointSize(Dictionary faceParts, int defaultPointSize) { int result; FacePoint[]? facePoints; if (faceParts.TryGetValue(FacePart.LeftEye, out facePoints)) result = (int)Math.Ceiling((facePoints.Max(l => l.X) - facePoints.Min(l => l.X)) * .05); else { if (faceParts.TryGetValue(FacePart.RightEye, out facePoints)) result = (int)Math.Ceiling((facePoints.Max(l => l.X) - facePoints.Min(l => l.X)) * .05); else result = defaultPointSize; } return result; } #pragma warning restore CA1416 public void SaveFaceLandmarkImages(Configuration configuration, string d2ResultsFullGroupDirectory, FilePath filePath, List> subFileTuples, List parseExceptions, MappingFromItem mappingFromItem, ExifDirectory exifDirectory, List faces, bool saveRotated) { FileInfo fileInfo; bool check = false; FileInfo rotatedFileInfo; DateTime? dateTime = null; long ticks = DateTime.Now.Ticks; string deterministicHashCodeKey; bool updateDateWhenMatches = false; List<(Shared.Models.Face, string, string)> collection = []; string[] changesFrom = [nameof(A_Property), nameof(B_Metadata), nameof(C_Resize), nameof(D_Face)]; List dateTimes = (from l in subFileTuples where changesFrom.Contains(l.Item1) select l.Item2).ToList(); (_, int directoryIndex) = Shared.Models.Stateless.Methods.IPath.GetDirectoryNameAndIndex(_PropertyConfiguration, filePath); string directory = Path.Combine(_FileGroups[_PropertyConfiguration.ResultContent][directoryIndex], mappingFromItem.FilePath.NameWithoutExtension); bool directoryExists = Directory.Exists(directory); foreach (Shared.Models.Face face in faces) { if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null) { collection.Add(new(face, string.Empty, string.Empty)); continue; } deterministicHashCodeKey = Shared.Models.Stateless.Methods.IMapping.GetDeterministicHashCodeKey(filePath, face.Location, Shared.Models.Stateless.ILocation.Digits, face.OutputResolution); fileInfo = new FileInfo(Path.Combine(directory, $"{deterministicHashCodeKey}{mappingFromItem.FilePath.ExtensionLowered}{_FileNameExtension}")); if (string.IsNullOrEmpty(fileInfo.DirectoryName)) continue; rotatedFileInfo = new FileInfo(Path.Combine(fileInfo.DirectoryName, $"{deterministicHashCodeKey} - R{mappingFromItem.FilePath.ExtensionLowered}{_FileNameExtension}")); collection.Add(new(face, fileInfo.FullName, rotatedFileInfo.FullName)); if (check) continue; else if (!directoryExists) check = true; else if (_OverrideForFaceLandmarkImages) check = true; else if (!fileInfo.Exists) check = true; else if (saveRotated && !rotatedFileInfo.Exists) check = true; else if (_CheckDFaceAndUpWriteDates && dateTimes.Count != 0 && dateTimes.Max() > fileInfo.LastWriteTime) check = true; if (check && !updateDateWhenMatches) { updateDateWhenMatches = dateTimes.Count != 0 && fileInfo.Exists && dateTimes.Max() > fileInfo.LastWriteTime; dateTime = !updateDateWhenMatches ? null : dateTimes.Max(); } } if (check) { if (!directoryExists) _ = Directory.CreateDirectory(directory); SaveFaceParts(mappingFromItem, exifDirectory, collection); if (saveRotated) SaveRotated(mappingFromItem, collection); } } private static bool GetNotMapped(string facePartsCollectionDirectory, List<(Shared.Models.Face Face, FileHolder?, string, bool)> faceCollection) { bool results = false; string checkFile; foreach ((Shared.Models.Face face, FileHolder? fileHolder, string _, bool _) in faceCollection) { if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null) continue; if (fileHolder is not null) { checkFile = Path.Combine(facePartsCollectionDirectory, fileHolder.Name); if (face.Mapping?.MappingFromPerson is not null) { if (File.Exists(checkFile)) File.Delete(checkFile); } else if (face.Mapping?.MappingFromFilterPre.InSkipCollection is not null && face.Mapping.MappingFromFilterPre.InSkipCollection.Value) { if (File.Exists(checkFile)) File.Delete(checkFile); } else { if (!results) results = true; if (!File.Exists(checkFile)) File.Copy(fileHolder.FullName, checkFile); } } } return results; } public void SaveFaceLandmarkImages(string d2ResultsFullGroupDirectory, MappingFromItem mappingFromItem, ExifDirectory exifDirectory, List faces) { bool any = false; foreach (Shared.Models.Face face in faces) { if (face.Location is null || face.FaceEncoding is null || face.FaceParts is null || face.FaceParts.Count == 0) continue; if (!any) any = true; } if (any) SaveAllFaceParts(d2ResultsFullGroupDirectory, mappingFromItem, exifDirectory, faces); } }