436 lines
24 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using View_by_Distance.Face.Models.Stateless;
using View_by_Distance.FaceRecognitionDotNet;
using View_by_Distance.Metadata.Models;
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.Face.Models;
/// <summary>
// List<D_Faces>
/// </summary>
public class D_Face
{
protected readonly string _FileNameExtension;
public string FileNameExtension => _FileNameExtension;
protected readonly string _HiddenFileNameExtension;
public string HiddenFileNameExtension => _HiddenFileNameExtension;
private readonly Model _Model;
private readonly string _ArgZero;
private readonly Serilog.ILogger? _Log;
private readonly bool _OverrideForFaceImages;
private readonly bool _LoadPhotoPrismLocations;
private readonly ImageCodecInfo _ImageCodecInfo;
private readonly ModelParameter _ModelParameter;
private readonly PredictorModel _PredictorModel;
private readonly bool _CheckDFaceAndUpWriteDates;
private readonly bool _PropertiesChangedForFaces;
private readonly ConstructorInfo _ConstructorInfo;
private readonly float _RectangleIntersectMinimum;
private readonly int _FaceDistanceHiddenImageFactor;
private readonly EncoderParameters _EncoderParameters;
private readonly ImageCodecInfo _HiddenImageCodecInfo;
private readonly Dictionary<string, string[]> _FileGroups;
private readonly bool _ForceFaceLastWriteTimeToCreationTime;
private readonly EncoderParameters _HiddenEncoderParameters;
private readonly IPropertyConfiguration _PropertyConfiguration;
private readonly JsonSerializerOptions _WriteIndentedAndWhenWritingNull;
public D_Face(
string argZero,
IPropertyConfiguration propertyConfiguration,
bool checkDFaceAndUpWriteDates,
EncoderParameters encoderParameters,
int faceDistanceHiddenImageFactor,
string filenameExtension,
bool forceFaceLastWriteTimeToCreationTime,
EncoderParameters hiddenEncoderParameters,
string hiddenFileNameExtension,
ImageCodecInfo hiddenImageCodecInfo,
ImageCodecInfo imageCodecInfo,
bool loadPhotoPrismLocations,
string modelDirectory,
string modelName,
bool overrideForFaceImages,
string predictorModelName,
bool propertiesChangedForFaces,
float[] rectangleIntersectMinimums)
{
_ArgZero = argZero;
_FileGroups = new();
_ImageCodecInfo = imageCodecInfo;
_EncoderParameters = encoderParameters;
_FileNameExtension = filenameExtension;
_Log = Serilog.Log.ForContext<D_Face>();
_HiddenImageCodecInfo = hiddenImageCodecInfo;
_OverrideForFaceImages = overrideForFaceImages;
_PropertyConfiguration = propertyConfiguration;
_HiddenEncoderParameters = hiddenEncoderParameters;
_HiddenFileNameExtension = hiddenFileNameExtension;
_LoadPhotoPrismLocations = loadPhotoPrismLocations;
_CheckDFaceAndUpWriteDates = checkDFaceAndUpWriteDates;
_PropertiesChangedForFaces = propertiesChangedForFaces;
_RectangleIntersectMinimum = rectangleIntersectMinimums.Min();
_FaceDistanceHiddenImageFactor = faceDistanceHiddenImageFactor;
_ForceFaceLastWriteTimeToCreationTime = forceFaceLastWriteTimeToCreationTime;
(Model model, PredictorModel predictorModel, ModelParameter modelParameter) = GetModel(modelDirectory, modelName, predictorModelName);
_Model = model;
_PredictorModel = predictorModel;
_ModelParameter = modelParameter;
ConstructorInfo? constructorInfo = typeof(PropertyItem).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, Array.Empty<Type>(), null) ?? throw new Exception();
_ConstructorInfo = constructorInfo;
_WriteIndentedAndWhenWritingNull = new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
}
public override string ToString()
{
string result = JsonSerializer.Serialize(this, new JsonSerializerOptions() { WriteIndented = true });
return result;
}
public void Update(string dResultsFullGroupDirectory)
{
_FileGroups.Clear();
Dictionary<string, string[]> keyValuePairs = Shared.Models.Stateless.Methods.IPath.GetKeyValuePairs(_PropertyConfiguration, dResultsFullGroupDirectory, new string[] { _PropertyConfiguration.ResultCollection, _PropertyConfiguration.ResultContent });
foreach (KeyValuePair<string, string[]> keyValuePair in keyValuePairs)
_FileGroups.Add(keyValuePair.Key, keyValuePair.Value);
}
private static (Model model, PredictorModel predictorModel, ModelParameter modelParameter) GetModel(string modelDirectory, string modelName, string predictorModelName)
{
(Model, PredictorModel, ModelParameter) result;
Array array;
Model? model = null;
PredictorModel? predictorModel = null;
array = Enum.GetValues(typeof(Model));
foreach (Model check in array)
{
if (modelName.Contains(check.ToString()))
{
model = check;
break;
}
}
if (model is null)
throw new Exception("Destination directory must have Model name!");
model = model.Value;
array = Enum.GetValues(typeof(PredictorModel));
foreach (PredictorModel check in array)
{
if (predictorModelName.Contains(check.ToString()))
{
predictorModel = check;
break;
}
}
if (predictorModel is null)
throw new Exception("Destination directory must have Predictor Model name!");
predictorModel = predictorModel.Value;
ModelParameter modelParameter = new()
{
CnnFaceDetectorModel = File.ReadAllBytes(Path.Combine(modelDirectory, "mmod_human_face_detector.dat")),
FaceRecognitionModel = File.ReadAllBytes(Path.Combine(modelDirectory, "dlib_face_recognition_resnet_model_v1.dat")),
PosePredictor5FaceLandmarksModel = File.ReadAllBytes(Path.Combine(modelDirectory, "shape_predictor_5_face_landmarks.dat")),
PosePredictor68FaceLandmarksModel = File.ReadAllBytes(Path.Combine(modelDirectory, "shape_predictor_68_face_landmarks.dat"))
};
result = new(model.Value, predictorModel.Value, modelParameter);
return result;
}
#pragma warning disable CA1416
private void SaveFaces(FileHolder resizedFileHolder, List<(Shared.Models.Face, FileInfo?, string, bool)> collection)
{
int width;
int height;
Bitmap bitmap;
short type = 2;
Graphics graphics;
Location? location;
Rectangle rectangle;
string locationJson;
string faceEncodingJson;
PropertyItem? propertyItem;
string outputResolutionJson;
int artist = (int)IExif.Tags.Artist;
int fileSource = (int)IExif.Tags.FileSource;
int userComment = (int)IExif.Tags.UserComment;
using Bitmap source = new(resizedFileHolder.FullName);
foreach ((Shared.Models.Face face, FileInfo? fileInfo, string fileName, bool save) in collection)
{
if (!save)
continue;
if (fileInfo is null)
continue;
if (face.FaceEncoding is null || face?.Location is null || face?.OutputResolution is null)
continue;
location = Shared.Models.Stateless.Methods.ILocation.GetLocation(face.Location, Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, source.Height, source.Width, collection.Count);
if (location is null)
continue;
width = location.Right - location.Left;
height = location.Bottom - location.Top;
locationJson = JsonSerializer.Serialize(face.Location);
faceEncodingJson = JsonSerializer.Serialize(face.FaceEncoding);
outputResolutionJson = JsonSerializer.Serialize(face.OutputResolution);
rectangle = new Rectangle(location.Left, location.Top, width, height);
using (bitmap = new(width, height))
{
using (graphics = Graphics.FromImage(bitmap))
graphics.DrawImage(source, new Rectangle(0, 0, width, height), rectangle, GraphicsUnit.Pixel);
propertyItem = IProperty.GetPropertyItem(_ConstructorInfo, fileSource, type, locationJson);
bitmap.SetPropertyItem(propertyItem);
propertyItem = IProperty.GetPropertyItem(_ConstructorInfo, artist, type, outputResolutionJson);
bitmap.SetPropertyItem(propertyItem);
propertyItem = IProperty.GetPropertyItem(_ConstructorInfo, userComment, type, faceEncodingJson);
bitmap.SetPropertyItem(propertyItem);
bitmap.Save(fileInfo.FullName, _ImageCodecInfo, _EncoderParameters);
}
if (File.Exists(fileName))
File.Delete(fileName);
location = Shared.Models.Stateless.Methods.ILocation.GetLocation(_FaceDistanceHiddenImageFactor, face.Location, Shared.Models.Stateless.ILocation.Digits, Shared.Models.Stateless.ILocation.Factor, 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 (bitmap = new(width, height))
{
using (graphics = Graphics.FromImage(bitmap))
graphics.DrawImage(source, new Rectangle(0, 0, width, height), rectangle, GraphicsUnit.Pixel);
bitmap.Save(fileName, _HiddenImageCodecInfo, _HiddenEncoderParameters);
}
File.SetAttributes(fileName, FileAttributes.Hidden);
}
}
private List<Shared.Models.Face> GetFaces(string outputResolution, Shared.Models.Property property, MappingFromItem mappingFromItem, Dictionary<string, int[]> outputResolutionToResize, List<Location>? locations)
{
if (_Log is null)
throw new NullReferenceException(nameof(_Log));
if (_PropertyConfiguration.NumberOfJitters is null)
throw new NullReferenceException(nameof(_PropertyConfiguration.NumberOfJitters));
if (_PropertyConfiguration.NumberOfTimesToUpsample is null)
throw new NullReferenceException(nameof(_PropertyConfiguration.NumberOfTimesToUpsample));
List<Shared.Models.Face> results = new();
FaceRecognitionDotNet.Image? unknownImage;
try
{ unknownImage = FaceRecognition.LoadImageFile(mappingFromItem.ResizedFileHolder.FullName); }
catch (Exception)
{
unknownImage = null;
_Log.Info(string.Concat(new StackFrame().GetMethod()?.Name, " <", mappingFromItem.ResizedFileHolder.FullName, ">"));
}
if (unknownImage is not null)
{
(int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation) = Resize.Models.Stateless.Methods.IResize.Get(outputResolution, outputResolutionToResize);
List<(Location Location, FaceRecognitionDotNet.FaceEncoding? FaceEncoding, Dictionary<FacePart, FacePoint[]>? FaceParts)> collection;
FaceRecognition faceRecognition = new(_PropertyConfiguration.NumberOfJitters.Value, _PropertyConfiguration.NumberOfTimesToUpsample.Value, _Model, _ModelParameter, _PredictorModel);
collection = faceRecognition.GetCollection(unknownImage, locations, includeFaceEncoding: true, includeFaceParts: true);
if (collection.Count == 0)
results.Add(new(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, location: null));
else
{
double[] rawEncoding;
Shared.Models.Face face;
Shared.Models.FaceEncoding convertedFaceEncoding;
foreach ((Location location, FaceRecognitionDotNet.FaceEncoding? faceEncoding, Dictionary<FacePart, FacePoint[]>? faceParts) in collection)
{
face = new(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, 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);
}
}
unknownImage.Dispose();
faceRecognition.Dispose();
}
return results;
}
#pragma warning restore CA1416
private static List<LocationContainer<MetadataExtractor.Directory>> GetLocationContainers(string outputResolution, ReadOnlyCollection<LocationContainer<MetadataExtractor.Directory>> locationContainers, Dictionary<string, int[]> outputResolutionToResize, List<Shared.Models.Face> faces)
{
List<LocationContainer<MetadataExtractor.Directory>> results = new();
string? json;
Location? location;
Rectangle? rectangle;
List<int> skip = new();
OutputResolution? outputResolutionCheck = null;
(int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation) = Resize.Models.Stateless.Methods.IResize.Get(outputResolution, outputResolutionToResize);
foreach (Shared.Models.Face face in faces)
{
if (face.Location is null || face.OutputResolution is null)
continue;
skip.Add(Shared.Models.Stateless.Methods.ILocation.GetWholePercentages(face.Location, Shared.Models.Stateless.ILocation.Digits, face.OutputResolution));
}
foreach (LocationContainer<MetadataExtractor.Directory> locationContainer in locationContainers)
{
if (locationContainer.Directories is null)
continue;
if (skip.Contains(locationContainer.WholePercentages))
continue;
foreach (Shared.Models.Face face in faces)
{
if (face.Location is not null && face.OutputResolution is not null)
continue;
json = Metadata.Models.Stateless.Methods.IMetadata.GetOutputResolution(locationContainer.Directories);
if (json is null)
continue;
outputResolutionCheck = JsonSerializer.Deserialize<OutputResolution>(json);
if (outputResolutionCheck is null || outputResolutionCheck.Width != outputResolutionWidth || outputResolutionCheck.Height != outputResolutionHeight)
continue;
rectangle = Shared.Models.Stateless.Methods.ILocation.GetRectangle(Shared.Models.Stateless.ILocation.Digits, outputResolutionCheck, locationContainer.WholePercentages);
if (rectangle is null)
continue;
location = Shared.Models.Stateless.Methods.ILocation.GetLocation(outputResolutionHeight, rectangle.Value, outputResolutionWidth);
if (location is null)
continue;
if (!results.Any(l => l.WholePercentages == locationContainer.WholePercentages))
results.Add(new(locationContainer.FromDistanceContent, locationContainer.DirectoryNumber, locationContainer.File, locationContainer.PersonKey, locationContainer.DisplayDirectoryName, locationContainer.Id, locationContainer.WholePercentages, locationContainer.Directories, rectangle.Value, location));
}
}
if (results.Count > 0)
outputResolutionCheck = null;
return results;
}
public List<Shared.Models.Face> GetFaces(string outputResolution, string dResultsFullGroupDirectory, List<Tuple<string, DateTime>> subFileTuples, List<string> parseExceptions, Shared.Models.Property property, MappingFromItem mappingFromItem, Dictionary<string, int[]> outputResolutionToResize, ReadOnlyCollection<LocationContainer<MetadataExtractor.Directory>>? locationContainers, List<MappingFromPhotoPrism>? mappingFromPhotoPrismCollection)
{
List<Shared.Models.Face>? results;
if (string.IsNullOrEmpty(dResultsFullGroupDirectory))
throw new NullReferenceException(nameof(dResultsFullGroupDirectory));
string? json;
List<Location>? locations;
string[] changesFrom = new string[] { nameof(A_Property), nameof(B_Metadata), nameof(C_Resize) };
List<DateTime> dateTimes = (from l in subFileTuples where changesFrom.Contains(l.Item1) select l.Item2).ToList();
(_, int directoryIndex) = Shared.Models.Stateless.Methods.IPath.GetDirectoryNameAndIndex(_PropertyConfiguration.ResultAllInOneSubdirectoryLength, mappingFromItem.ImageFileHolder.Name);
FileInfo fileInfo = new(Path.Combine(_FileGroups[_PropertyConfiguration.ResultCollection][directoryIndex], $"{mappingFromItem.ImageFileHolder.NameWithoutExtension}{mappingFromItem.ImageFileHolder.ExtensionLowered}.json"));
if (_ForceFaceLastWriteTimeToCreationTime && !fileInfo.Exists && File.Exists(Path.ChangeExtension(fileInfo.FullName, ".delete")))
{
File.Move(Path.ChangeExtension(fileInfo.FullName, ".delete"), fileInfo.FullName);
fileInfo.Refresh();
}
if (_ForceFaceLastWriteTimeToCreationTime && fileInfo.Exists && fileInfo.LastWriteTime != fileInfo.CreationTime)
{
File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime);
fileInfo.Refresh();
}
if (_PropertiesChangedForFaces)
results = null;
else if (!fileInfo.Exists)
results = null;
else if (_CheckDFaceAndUpWriteDates && dateTimes.Count > 0 && dateTimes.Max() > fileInfo.LastWriteTime)
results = null;
else
{
json = Shared.Models.Stateless.Methods.IFace.GetJson(fileInfo.FullName);
try
{
results = JsonSerializer.Deserialize<List<Shared.Models.Face>>(json);
if (results is null)
throw new NullReferenceException(nameof(results));
subFileTuples.Add(new Tuple<string, DateTime>(nameof(D_Face), fileInfo.LastWriteTime));
}
catch (Exception)
{
results = null;
parseExceptions.Add(nameof(D_Face));
}
}
List<LocationContainer<MetadataExtractor.Directory>> collection;
if (results is null || locationContainers is null)
collection = new();
else
collection = GetLocationContainers(outputResolution, locationContainers, outputResolutionToResize, results);
if (!_LoadPhotoPrismLocations || mappingFromPhotoPrismCollection is null || results is null)
locations = (from l in collection where l is not null select l.Location).ToList();
else
locations = Shared.Models.Stateless.Methods.ILocation.GetLocations(collection, results, mappingFromPhotoPrismCollection, _RectangleIntersectMinimum);
if (results is null || (locations is not null && locations.Count > 0))
{
results = GetFaces(outputResolution, property, mappingFromItem, outputResolutionToResize, locations);
if (results.Count == 0)
File.Move(mappingFromItem.ResizedFileHolder.FullName, $"{mappingFromItem.ResizedFileHolder.FullName}.err");
else
{
bool updateDateWhenMatches = dateTimes.Count > 0 && fileInfo.Exists && dateTimes.Max() > fileInfo.LastWriteTime;
DateTime? dateTime = !updateDateWhenMatches ? null : dateTimes.Max();
json = JsonSerializer.Serialize(results, _WriteIndentedAndWhenWritingNull);
if (Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches, compareBeforeWrite: true, updateToWhenMatches: dateTime))
{
if (!_ForceFaceLastWriteTimeToCreationTime)
subFileTuples.Add(new Tuple<string, DateTime>(nameof(D_Face), DateTime.Now));
else
{
File.SetLastWriteTime(fileInfo.FullName, fileInfo.CreationTime);
fileInfo.Refresh();
subFileTuples.Add(new Tuple<string, DateTime>(nameof(D_Face), fileInfo.CreationTime));
}
}
}
}
return results;
}
public List<(Shared.Models.Face, FileInfo?, string, bool)> SaveFaces(string f, string dResultsFullGroupDirectory, List<Tuple<string, DateTime>> subFileTuples, List<string> parseExceptions, MappingFromItem mappingFromItem, List<Shared.Models.Face> faces)
{
List<(Shared.Models.Face, FileInfo?, string, bool Save)> results = new();
bool save;
FileInfo fileInfo;
string deterministicHashCodeKey;
string[] changesFrom = new string[] { nameof(A_Property), nameof(B_Metadata), nameof(C_Resize) };
List<DateTime> dateTimes = (from l in subFileTuples where changesFrom.Contains(l.Item1) select l.Item2).ToList();
(_, int directoryIndex) = Shared.Models.Stateless.Methods.IPath.GetDirectoryNameAndIndex(_PropertyConfiguration.ResultAllInOneSubdirectoryLength, mappingFromItem.ImageFileHolder.Name);
string directory = Path.Combine(_FileGroups[_PropertyConfiguration.ResultContent][directoryIndex], mappingFromItem.ImageFileHolder.NameWithoutExtension);
bool directoryExists = Directory.Exists(directory);
foreach (Shared.Models.Face face in faces)
{
save = false;
if (face.FaceEncoding is null || face.Location is null || face.OutputResolution is null)
{
results.Add(new(face, null, string.Empty, save));
continue;
}
deterministicHashCodeKey = Shared.Models.Stateless.Methods.IMapping.GetDeterministicHashCodeKey(mappingFromItem.Id, face.Location, Shared.Models.Stateless.ILocation.Digits, face.OutputResolution);
fileInfo = new FileInfo(Path.Combine(directory, $"{deterministicHashCodeKey}{mappingFromItem.ImageFileHolder.ExtensionLowered}{_FileNameExtension}"));
if (!directoryExists)
save = true;
else if (_OverrideForFaceImages)
save = true;
else if (!fileInfo.Exists)
save = true;
else if (_CheckDFaceAndUpWriteDates && dateTimes.Count > 0 && dateTimes.Max() > fileInfo.LastWriteTime)
save = true;
results.Add(new(face, fileInfo, Path.Combine(directory, $"{deterministicHashCodeKey}{mappingFromItem.ImageFileHolder.ExtensionLowered}{_HiddenFileNameExtension}"), save));
}
if (results.Any(l => l.Save))
{
if (!directoryExists)
_ = Directory.CreateDirectory(directory);
SaveFaces(mappingFromItem.ResizedFileHolder, results);
}
return results;
}
}