411 lines
20 KiB
C#
411 lines
20 KiB
C#
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Drawing.Imaging;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
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;
|
|
|
|
namespace View_by_Distance.Instance.Models;
|
|
|
|
/// <summary>
|
|
// List<D_Faces>
|
|
/// </summary>
|
|
public class D_Face
|
|
{
|
|
|
|
internal List<string> AngleBracketCollection { get; }
|
|
|
|
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 Configuration _Configuration;
|
|
private readonly ImageCodecInfo _ImageCodecInfo;
|
|
private readonly ModelParameter _ModelParameter;
|
|
private readonly PredictorModel _PredictorModel;
|
|
private readonly EncoderParameters _EncoderParameters;
|
|
private readonly ImageCodecInfo _HiddenImageCodecInfo;
|
|
private readonly EncoderParameters _HiddenEncoderParameters;
|
|
private readonly JsonSerializerOptions _WriteIndentedAndWhenWritingNull;
|
|
|
|
internal D_Face(Configuration configuration, string argZero, Model model, ModelParameter modelParameter, PredictorModel predictorModel, ImageCodecInfo imageCodecInfo, EncoderParameters encoderParameters, string filenameExtension, ImageCodecInfo hiddenImageCodecInfo, EncoderParameters hiddenEncoderParameters, string hiddenFilenameExtension)
|
|
{
|
|
_Model = model;
|
|
_ArgZero = argZero;
|
|
_Configuration = configuration;
|
|
_ImageCodecInfo = imageCodecInfo;
|
|
_ModelParameter = modelParameter;
|
|
_PredictorModel = predictorModel;
|
|
_EncoderParameters = encoderParameters;
|
|
_FilenameExtension = filenameExtension;
|
|
_Log = Serilog.Log.ForContext<D_Face>();
|
|
AngleBracketCollection = new List<string>();
|
|
_HiddenImageCodecInfo = hiddenImageCodecInfo;
|
|
_HiddenEncoderParameters = hiddenEncoderParameters;
|
|
_HiddenFilenameExtension = hiddenFilenameExtension;
|
|
_WriteIndentedAndWhenWritingNull = new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
|
|
}
|
|
|
|
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, FileInfo?, string)> collection)
|
|
{
|
|
int width;
|
|
int height;
|
|
Graphics graphics;
|
|
Location? location;
|
|
Bitmap preRotated;
|
|
Rectangle rectangle;
|
|
using Bitmap source = new(resizedFileHolder.FullName);
|
|
foreach ((Face face, FileInfo? fileInfo, string fileName) in collection)
|
|
{
|
|
if (fileInfo is null)
|
|
continue;
|
|
if (face.FaceEncoding is null || face?.Location is null)
|
|
continue;
|
|
location = Shared.Models.Stateless.Methods.ILocation.GetLocation(face.Location, ILocation.Digits, 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 (preRotated = new(width, height))
|
|
{
|
|
using (graphics = Graphics.FromImage(preRotated))
|
|
graphics.DrawImage(source, new Rectangle(0, 0, width, height), rectangle, GraphicsUnit.Pixel);
|
|
preRotated.Save(fileInfo.FullName, _ImageCodecInfo, _EncoderParameters);
|
|
}
|
|
if (File.Exists(fileName))
|
|
File.Delete(fileName);
|
|
location = Shared.Models.Stateless.Methods.ILocation.GetLocation(IFaceDistance.HiddenImageFactor, face.Location, ILocation.Digits, 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 (preRotated = new(width, height))
|
|
{
|
|
using (graphics = Graphics.FromImage(preRotated))
|
|
graphics.DrawImage(source, new Rectangle(0, 0, width, height), rectangle, GraphicsUnit.Pixel);
|
|
preRotated.Save(fileName, _HiddenImageCodecInfo, _HiddenEncoderParameters);
|
|
}
|
|
File.SetAttributes(fileName, FileAttributes.Hidden);
|
|
}
|
|
}
|
|
|
|
private List<Face> GetFaces(Item item, Shared.Models.Property property, int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation)
|
|
{
|
|
List<Face> results = new();
|
|
if (item.ImageFileHolder is null)
|
|
throw new NullReferenceException(nameof(item.ImageFileHolder));
|
|
if (item.ResizedFileHolder is null)
|
|
throw new NullReferenceException(nameof(item.ResizedFileHolder));
|
|
FaceRecognitionDotNet.Image? unknownImage;
|
|
if (!item.ResizedFileHolder.Exists)
|
|
unknownImage = null;
|
|
else
|
|
{
|
|
try
|
|
{ unknownImage = FaceRecognition.LoadImageFile(item.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<(int, Location Location, FaceRecognitionDotNet.FaceEncoding? FaceEncoding, Dictionary<FacePart, FacePoint[]>? 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
|
|
{
|
|
Face face;
|
|
double[] rawEncoding;
|
|
Shared.Models.FaceEncoding convertedFaceEncoding;
|
|
foreach ((int locationIndex, Location location, FaceRecognitionDotNet.FaceEncoding? faceEncoding, Dictionary<FacePart, FacePoint[]>? faceParts) in collection)
|
|
{
|
|
face = new(property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation, item.RelativePath, locationIndex, 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();
|
|
}
|
|
if (!results.Any())
|
|
throw new Exception();
|
|
return results;
|
|
}
|
|
|
|
#pragma warning restore CA1416
|
|
|
|
internal List<Face> GetFaces(string dResultsFullGroupDirectory, List<Tuple<string, DateTime>> subFileTuples, List<string> parseExceptions, Item item, Shared.Models.Property property, int outputResolutionWidth, int outputResolutionHeight, int outputResolutionOrientation)
|
|
{
|
|
List<Face>? 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<DateTime> 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<List<Face>>(json);
|
|
if (results is null)
|
|
throw new NullReferenceException(nameof(results));
|
|
if (!_Configuration.ForceFaceLastWriteTimeToCreationTime)
|
|
{
|
|
normalizedPixelPercentageCollection = Shared.Models.Stateless.Methods.IFace.GetInts(results);
|
|
normalizedPixelPercentageDistinctCount = normalizedPixelPercentageCollection.Distinct().Count();
|
|
if (normalizedPixelPercentageDistinctCount != normalizedPixelPercentageCollection.Length)
|
|
throw new Exception($"Not distinct! <{fileInfo.FullName}>");
|
|
}
|
|
subFileTuples.Add(new Tuple<string, DateTime>(nameof(D_Face), fileInfo.LastWriteTime));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
results = null;
|
|
parseExceptions.Add(nameof(D_Face));
|
|
}
|
|
}
|
|
if (results is null)
|
|
{
|
|
results = GetFaces(item, property, outputResolutionWidth, outputResolutionHeight, outputResolutionOrientation);
|
|
json = JsonSerializer.Serialize(results, _WriteIndentedAndWhenWritingNull);
|
|
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<string, DateTime>(nameof(D_Face), DateTime.Now));
|
|
}
|
|
if (_Configuration.ForceFaceLastWriteTimeToCreationTime)
|
|
{
|
|
results = (from l in results select new Face(ILocation.Digits, ILocation.Factor, 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, _WriteIndentedAndWhenWritingNull);
|
|
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<string, DateTime>(nameof(D_Face), fileInfo.CreationTime));
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
internal void SaveFaces(string dResultsFullGroupDirectory, List<Tuple<string, DateTime>> subFileTuples, List<string> parseExceptions, Item item, List<Face> 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;
|
|
string deterministicHashCodeKeyDisplay;
|
|
List<(Face, FileInfo?, 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<DateTime> 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 (item.Property?.Id is null || face.FaceEncoding is null || face.Location?.NormalizedPixelPercentage is null)
|
|
{
|
|
collection.Add(new(face, null, string.Empty));
|
|
continue;
|
|
}
|
|
deterministicHashCodeKeyDisplay = Shared.Models.Stateless.Methods.IMapping.GetDeterministicHashCodeKeyDisplay(item.Property.Id.Value, face.Location.NormalizedPixelPercentage.Value);
|
|
fileInfo = new FileInfo(Path.Combine(facesDirectory, $"{deterministicHashCodeKeyDisplay}{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, Path.Combine(facesDirectory, $"{deterministicHashCodeKeyDisplay}{item.ImageFileHolder.ExtensionLowered}{_HiddenFilenameExtension}")));
|
|
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);
|
|
}
|
|
|
|
private static bool HasLeftAndRight(Dictionary<string, FacePoint[]> 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;
|
|
}
|
|
|
|
} |