From a0c880c7ba4aba2dd2542c3c76a0ee66087e3feb Mon Sep 17 00:00:00 2001 From: Mike Phares Date: Sun, 21 May 2023 23:56:10 -0700 Subject: [PATCH] Switched to ThumbHasher over BlurHasher --- BlurHash.Core/Core.cs | 34 +- BlurHash.Core/Pixel.cs | 12 +- BlurHash.System.Drawing.Common/BlurHasher.cs | 12 +- BlurHash/Models/BlurHasher.cs | 2 +- Date-Group/Date-Group.csproj | 2 +- Date-Group/DateGroup.cs | 6 +- Distance/Models/_E_Distance.cs | 2 +- Drag-Drop-Move/DragDropMove.cs | 14 +- Instance/DlibDotNet.cs | 102 +++-- Instance/Instance.csproj | 2 +- Property/Models/A_Property.cs | 66 ++- Shared/Models/Methods/IThumbHasher.cs | 10 + Shared/Models/Properties/IProperty.cs | 2 +- Shared/Models/Property.cs | 6 +- Tests/Tests.csproj | 2 +- Tests/UnitTestResize.cs | 4 +- .../TestsWithFaceRecognitionDotNet.csproj | 2 +- .../UnitTestFace.cs | 4 +- ThumbHash/.vscode/format-report.json | 1 + ThumbHash/Models/SpanOwner.cs | 53 +++ ThumbHash/Models/ThumbHash.Channel.cs | 31 ++ ThumbHash/Models/ThumbHash.RGBA.cs | 38 ++ ThumbHash/Models/ThumbHash.cs | 409 ++++++++++++++++++ ThumbHash/Models/ThumbHasher.cs | 57 +++ ThumbHash/ThumbHash.csproj | 45 ++ View-by-Distance-MKLink-Console.sln | 6 + 26 files changed, 803 insertions(+), 121 deletions(-) create mode 100644 Shared/Models/Methods/IThumbHasher.cs create mode 100644 ThumbHash/.vscode/format-report.json create mode 100644 ThumbHash/Models/SpanOwner.cs create mode 100644 ThumbHash/Models/ThumbHash.Channel.cs create mode 100644 ThumbHash/Models/ThumbHash.RGBA.cs create mode 100644 ThumbHash/Models/ThumbHash.cs create mode 100644 ThumbHash/Models/ThumbHasher.cs create mode 100644 ThumbHash/ThumbHash.csproj diff --git a/BlurHash.Core/Core.cs b/BlurHash.Core/Core.cs index 24dceae..40ea353 100644 --- a/BlurHash.Core/Core.cs +++ b/BlurHash.Core/Core.cs @@ -56,15 +56,15 @@ public static class Core { double basis = xCosines[xPixel] * yCosines[yPixel]; Pixel pixel = pixels[xPixel, yPixel]; - r += basis * pixel._Red; - g += basis * pixel._Green; - b += basis * pixel._Blue; + r += basis * pixel.Red; + g += basis * pixel.Green; + b += basis * pixel.Blue; } double scale = normalization / (width * height); - factors[componentsX * yComponent + xComponent]._Red = r * scale; - factors[componentsX * yComponent + xComponent]._Green = g * scale; - factors[componentsX * yComponent + xComponent]._Blue = b * scale; + factors[componentsX * yComponent + xComponent].Red = r * scale; + factors[componentsX * yComponent + xComponent].Green = g * scale; + factors[componentsX * yComponent + xComponent].Blue = b * scale; progressCallback?.Report(processedFactors * 100 / factorCount); processedFactors++; @@ -90,9 +90,9 @@ public static class Core int factorIndex = componentsX * yComponent + xComponent; - actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Red), actualMaximumValue); - actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Green), actualMaximumValue); - actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Blue), actualMaximumValue); + actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Red), actualMaximumValue); + actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Green), actualMaximumValue); + actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex].Blue), actualMaximumValue); } int quantizedMaximumValue = (int)Math.Max(0.0, Math.Min(82.0, Math.Floor(actualMaximumValue * 166 - 0.5))); @@ -105,7 +105,7 @@ public static class Core resultBuffer[1] = '0'; } - EncodeDc(dc._Red, dc._Green, dc._Blue).EncodeBase83(resultBuffer.Slice(2, 4)); + EncodeDc(dc.Red, dc.Green, dc.Blue).EncodeBase83(resultBuffer.Slice(2, 4)); for (int yComponent = 0; yComponent < componentsY; yComponent++) for (int xComponent = 0; xComponent < componentsX; xComponent++) @@ -116,7 +116,7 @@ public static class Core int factorIndex = componentsX * yComponent + xComponent; - EncodeAc(factors[factorIndex]._Red, factors[factorIndex]._Green, factors[factorIndex]._Blue, maximumValue).EncodeBase83(resultBuffer.Slice(6 + (factorIndex - 1) * 2, 2)); + EncodeAc(factors[factorIndex].Red, factors[factorIndex].Green, factors[factorIndex].Blue, maximumValue).EncodeBase83(resultBuffer.Slice(6 + (factorIndex - 1) * 2, 2)); } return resultBuffer.ToString(); @@ -184,9 +184,9 @@ public static class Core { ref Pixel result = ref pixels[xPixel, yPixel]; - result._Red = 0.0; - result._Green = 0.0; - result._Blue = 0.0; + result.Red = 0.0; + result.Green = 0.0; + result.Blue = 0.0; } double[] xCosines = new double[outputWidth]; @@ -215,9 +215,9 @@ public static class Core double basis = xCosines[xPixel] * yCosines[yPixel]; - result._Red += coefficient._Red * basis; - result._Green += coefficient._Green * basis; - result._Blue += coefficient._Blue * basis; + result.Red += coefficient.Red * basis; + result.Green += coefficient.Green * basis; + result.Blue += coefficient.Blue * basis; } progressCallback?.Report(componentIndex * 100 / componentCount); diff --git a/BlurHash.Core/Pixel.cs b/BlurHash.Core/Pixel.cs index ce49ad5..cf9450a 100644 --- a/BlurHash.Core/Pixel.cs +++ b/BlurHash.Core/Pixel.cs @@ -5,14 +5,14 @@ /// public struct Pixel { - public double _Red; - public double _Green; - public double _Blue; + public double Red { get; set; } + public double Green { get; set; } + public double Blue { get; set; } public Pixel(double red, double green, double blue) { - _Red = red; - _Green = green; - _Blue = blue; + Red = red; + Green = green; + Blue = blue; } } \ No newline at end of file diff --git a/BlurHash.System.Drawing.Common/BlurHasher.cs b/BlurHash.System.Drawing.Common/BlurHasher.cs index 6f1efb4..a70dd96 100644 --- a/BlurHash.System.Drawing.Common/BlurHasher.cs +++ b/BlurHash.System.Drawing.Common/BlurHasher.cs @@ -63,9 +63,9 @@ public static class BlurHasher for (int x = 0; x < width; x++) { ref Pixel res = ref result[x, y]; - res._Blue = MathUtils.SRgbToLinear(rgb[index++]); - res._Green = MathUtils.SRgbToLinear(rgb[index++]); - res._Red = MathUtils.SRgbToLinear(rgb[index++]); + res.Blue = MathUtils.SRgbToLinear(rgb[index++]); + res.Green = MathUtils.SRgbToLinear(rgb[index++]); + res.Red = MathUtils.SRgbToLinear(rgb[index++]); } } @@ -92,9 +92,9 @@ public static class BlurHasher { Pixel pixel = pixelData[xPixel, yPixel]; - data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Blue); - data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Green); - data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Red); + data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Blue); + data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Green); + data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Red); data[index++] = 0; } diff --git a/BlurHash/Models/BlurHasher.cs b/BlurHash/Models/BlurHasher.cs index 41b9441..51379ec 100644 --- a/BlurHash/Models/BlurHasher.cs +++ b/BlurHash/Models/BlurHasher.cs @@ -31,8 +31,8 @@ public class BlurHasher : IBlurHasher string result; int actualByte; result = System.Drawing.BlurHash.BlurHasher.Encode(image, x, y); + using Image actualImage = System.Drawing.BlurHash.BlurHasher.Decode(result, width, height); byte[] blurHashBytes = Encoding.UTF8.GetBytes(result); - using Bitmap actualImage = (Bitmap)System.Drawing.BlurHash.BlurHasher.Decode(result, width, height); string joined = string.Join(string.Empty, blurHashBytes.Select(l => l.ToString("000"))); string fileName = Path.Combine(directory, $"{x}x{y}-{width}x{height}-{joined}.png"); if (!File.Exists(fileName)) diff --git a/Date-Group/Date-Group.csproj b/Date-Group/Date-Group.csproj index 7a8ca48..dbdeb3b 100644 --- a/Date-Group/Date-Group.csproj +++ b/Date-Group/Date-Group.csproj @@ -50,9 +50,9 @@ - + diff --git a/Date-Group/DateGroup.cs b/Date-Group/DateGroup.cs index 53b6d48..5420c39 100644 --- a/Date-Group/DateGroup.cs +++ b/Date-Group/DateGroup.cs @@ -65,8 +65,8 @@ public class DateGroup throw new Exception(); if (propertyConfiguration.PopulatePropertyId && (configuration.ByCreateDateShortcut || configuration.ByHash) && Shared.Models.Stateless.Methods.IProperty.Any(containers)) { - IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher(); - propertyLogic.SavePropertyParallelWork(ticks, t, containers, blurHasher); + IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher(); + propertyLogic.SavePropertyParallelWork(ticks, t, containers, thumbHasher); if (appSettings.MaxDegreeOfParallelism < 2) ticks = LogDelta(ticks, nameof(A_Property.SavePropertyParallelWork)); if (propertyLogic.ExceptionsDirectories.Any()) @@ -338,10 +338,10 @@ public class DateGroup private (Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)[] GetFileMoveCollectionAll(Property.Models.Configuration configuration, string destinationRoot, Container[] containers) { (Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)[] results; + Item[] filteredItems; string? topDirectory; string? checkDirectory; string destinationDirectory; - Item[] filteredItems; List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollection = new(); List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollectionDirectory; foreach (Container container in containers) diff --git a/Distance/Models/_E_Distance.cs b/Distance/Models/_E_Distance.cs index 56c86fb..905387f 100644 --- a/Distance/Models/_E_Distance.cs +++ b/Distance/Models/_E_Distance.cs @@ -195,7 +195,7 @@ public partial class E_Distance } } - public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, string eDistanceContentDirectory, MappingFromItem mappingFromItem, List faces, List> collection) + public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, MappingFromItem mappingFromItem, List faces, List> collection) { string? json; string fileName; diff --git a/Drag-Drop-Move/DragDropMove.cs b/Drag-Drop-Move/DragDropMove.cs index 748ac36..e469871 100644 --- a/Drag-Drop-Move/DragDropMove.cs +++ b/Drag-Drop-Move/DragDropMove.cs @@ -202,11 +202,11 @@ public partial class DragDropMove : Form List<(string, int, DateTime)> results = new(); DateTime dateTime; Shared.Models.Property property; - Shared.Models.Methods.IBlurHasher? blurHasher = null; + Shared.Models.Methods.IThumbHasher? thumbHasher = null; string[] files = Directory.GetFiles(checkDirectory, "*", SearchOption.TopDirectoryOnly); foreach (string file in files) { - property = Property.Models.A_Property.GetImageProperty(blurHasher, file); + property = Property.Models.A_Property.GetImageProperty(thumbHasher, file); if (property.Id is null || property.DateTimeOriginal is null) continue; dateTime = property.DateTimeOriginal.Value.AddTicks(ticks); @@ -240,7 +240,7 @@ public partial class DragDropMove : Form ticks++; } - Shared.Models.Methods.IBlurHasher? blurHasher = null; + Shared.Models.Methods.IThumbHasher? thumbHasher = null; List<(string, int, DateTime)> collection = GetCollection(checkDirectory, minimumDateTime, maximumDateTime, ticks); ConstructorInfo? constructorInfo = typeof(PropertyItem).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, Array.Empty(), null) ?? throw new Exception(); foreach ((string file, int id, DateTime dateTime) in collection) @@ -255,7 +255,7 @@ public partial class DragDropMove : Form bitmap.SetPropertyItem(propertyItem); bitmap.Save(checkFile); bitmap.Dispose(); - property = Property.Models.A_Property.GetImageProperty(blurHasher, checkFile); + property = Property.Models.A_Property.GetImageProperty(thumbHasher, checkFile); if (property.Id is null || property.Id.Value != id) throw new Exception(); } @@ -290,11 +290,11 @@ public partial class DragDropMove : Form _Logger.Error("bad file(s) or target file(s) or maximum directory doesn't equal 1!"); else { - Shared.Models.Methods.IBlurHasher? blurHasher = null; + Shared.Models.Methods.IThumbHasher? thumbHasher = null; DateTime minimumDateTime = DateTime.ParseExact(Path.GetFileName(minimumDirectory.First()), format, null, System.Globalization.DateTimeStyles.None); DateTime maximumDateTime = DateTime.ParseExact(Path.GetFileName(maximumDirectory.First()), format, null, System.Globalization.DateTimeStyles.None).AddHours(23); - Shared.Models.Property badProperty = Property.Models.A_Property.GetImageProperty(blurHasher, badFiles.First()); - Shared.Models.Property targetProperty = Property.Models.A_Property.GetImageProperty(blurHasher, targetFiles.First()); + Shared.Models.Property badProperty = Property.Models.A_Property.GetImageProperty(thumbHasher, badFiles.First()); + Shared.Models.Property targetProperty = Property.Models.A_Property.GetImageProperty(thumbHasher, targetFiles.First()); if (badProperty.DateTimeOriginal is null || targetProperty.DateTimeOriginal is null) _Logger.Error("Date is null!"); else diff --git a/Instance/DlibDotNet.cs b/Instance/DlibDotNet.cs index c1fe1b9..d2ad46a 100644 --- a/Instance/DlibDotNet.cs +++ b/Instance/DlibDotNet.cs @@ -2,7 +2,6 @@ using Phares.Shared; using ShellProgressBar; using System.Collections.ObjectModel; -using System.Drawing; using System.Drawing.Imaging; using System.Text.Json; using System.Text.RegularExpressions; @@ -32,8 +31,8 @@ public partial class DlibDotNet private readonly Serilog.ILogger? _Log; private readonly D2_FaceParts _FaceParts; private readonly AppSettings _AppSettings; - private readonly IBlurHasher _IBlurHasher; private readonly List _Exceptions; + private readonly IThumbHasher _IThumbHasher; private readonly IsEnvironment _IsEnvironment; private readonly bool _PropertyRootExistedBefore; private readonly Models.Configuration _Configuration; @@ -59,7 +58,7 @@ public partial class DlibDotNet long ticks = DateTime.Now.Ticks; _Exceptions = new List(); _Log = Serilog.Log.ForContext(); - _IBlurHasher = new BlurHash.Models.BlurHasher(); + _IThumbHasher = new ThumbHash.Models.C2_ThumbHasher(); Property.Models.Configuration propertyConfiguration = Property.Models.Binder.Configuration.Get(isEnvironment, configurationRoot); Models.Configuration configuration = Models.Binder.Configuration.Get(isEnvironment, configurationRoot, propertyConfiguration); _Log.Information(propertyConfiguration.RootDirectory); @@ -355,9 +354,8 @@ public partial class DlibDotNet MapLogic mapLogic, string outputResolution, string cResultsFullGroupDirectory, - string dResultsDateGroupDirectory, + string c2ResultsFullGroupDirectory, string dResultsFullGroupDirectory, - string eDistanceContentDirectory, List> sourceDirectoryChanges, Dictionary> fileNameToCollection, Container container, @@ -377,7 +375,8 @@ public partial class DlibDotNet List parseExceptions = new(); List> subFileTuples = new(); List> metadataCollection; - if (item.Property is not null && item.Property.Id is not null && !item.Any() && item.Property.BlurHash is null) + FileHolder resizedFileHolder = _Resize.GetResizedFileHolder(item); + if (item.Property is not null && item.Property.Id is not null && resizedFileHolder.Exists && item.Property.ThumbHashBytes is null) { (string aResultsFullGroupDirectory, _) = GetResultsFullGroupDirectories(); string aPropertySingletonDirectory = Path.Combine(aResultsFullGroupDirectory, "{}"); @@ -389,19 +388,60 @@ public partial class DlibDotNet string find = "\"CreationTime\":"; if (!json.Contains(find)) throw new NotImplementedException(); -#pragma warning disable CA1416 - using Image image = Image.FromFile(item.ImageFileHolder.FullName); -#pragma warning restore CA1416 - string blurHash = _IBlurHasher.Encode(image); - json = json.Replace(find, $"\"{nameof(item.Property.BlurHash)}\": \"{blurHash}\", {find}"); + byte[]? thumbHashBytes = _IThumbHasher.Encode(resizedFileHolder.FullName); + string thumbHashJson = JsonSerializer.Serialize(thumbHashBytes); + json = json.Replace(find, $"\"{nameof(property.ThumbHashBytes)}\": {thumbHashJson}, {find}"); property = JsonSerializer.Deserialize(json); - if (property is null || property.BlurHash is null) + if (property is null || property.ThumbHashBytes is null) throw new NullReferenceException(nameof(property)); json = JsonSerializer.Serialize(property, new JsonSerializerOptions { WriteIndented = true }); + if (thumbHashBytes is null || thumbHashBytes.Length != property.ThumbHashBytes.Length) + throw new Exception(nameof(property.ThumbHashBytes)); + for (int i = 0; i < thumbHashBytes.Length; i++) + { + if (thumbHashBytes[i] != property.ThumbHashBytes[i]) + throw new Exception(nameof(property.ThumbHashBytes)); + } + thumbHashBytes = JsonSerializer.Deserialize(thumbHashJson); + if (thumbHashBytes is null || thumbHashBytes.Length != property.ThumbHashBytes.Length) + throw new Exception(nameof(property.ThumbHashBytes)); + for (int i = 0; i < thumbHashBytes.Length; i++) + { + if (thumbHashBytes[i] != property.ThumbHashBytes[i]) + throw new Exception(nameof(property.ThumbHashBytes)); + } File.WriteAllText(matchFile, json); File.SetLastWriteTime(matchFile, item.Property.LastWriteTime); } } + if (item.Property is not null && item.Property.Id is not null && resizedFileHolder.Exists && item.Property.Width is not null && item.Property.Height is not null && item.Property.ThumbHashBytes is not null) + { + string fileName; + string c2ThumbHasherContentDirectory = Path.Combine(c2ResultsFullGroupDirectory, "()"); + string c2ThumbHasherSingletonDirectory = Path.Combine(c2ResultsFullGroupDirectory, "{}"); + if (!Directory.Exists(c2ThumbHasherContentDirectory)) + _ = Directory.CreateDirectory(c2ThumbHasherContentDirectory); + if (!Directory.Exists(c2ThumbHasherSingletonDirectory)) + _ = Directory.CreateDirectory(c2ThumbHasherSingletonDirectory); + MemoryStream memoryStream = _IThumbHasher.GetMemoryStream(item.Property.ThumbHashBytes, item.Property.Width.Value, item.Property.Height.Value); + string thumbHashJson = JsonSerializer.Serialize(item.Property.ThumbHashBytes)[1..^1]; + if (!Regex.Matches(thumbHashJson, @"[\\,\/,\:,\*,\?,\"",\<,\>,\|]").Any()) + fileName = Path.Combine(c2ThumbHasherSingletonDirectory, $"{thumbHashJson}.png"); + else + { + // string thumbHash = BitConverter.ToString(item.Property.ThumbHashBytes).Replace("-", string.Empty); + // fileName = Path.Combine(c2ThumbHasherContentDirectory, $"{thumbHash}.png"); + fileName = Path.Combine(c2ThumbHasherContentDirectory, $"{resizedFileHolder.NameWithoutExtension}.png"); + } + if (!File.Exists(fileName)) + { + using FileStream fileStream = new(fileName, FileMode.CreateNew); + memoryStream.WriteTo(fileStream); + memoryStream.Dispose(); + if (resizedFileHolder.LastWriteTime is not null) + File.SetLastWriteTime(fileName, resizedFileHolder.LastWriteTime.Value); + } + } if (item.Property is not null && item.Property.Id is not null && !item.Any()) { property = item.Property; @@ -427,7 +467,7 @@ public partial class DlibDotNet _Log.Information(string.Concat("LastWriteTimeChanged <", item.ImageFileHolder.FullName, '>')); else if (item.Moved.HasValue && item.Moved.Value) _Log.Information(string.Concat("Moved <", item.ImageFileHolder.FullName, '>')); - property = propertyLogic.GetProperty(_IBlurHasher, item, subFileTuples, parseExceptions); + property = propertyLogic.GetProperty(_IThumbHasher, item, subFileTuples, parseExceptions); item.Update(property); if (propertyHashCode is null) { @@ -442,7 +482,6 @@ public partial class DlibDotNet } if (property is null || item.Property is null) throw new NullReferenceException(nameof(property)); - FileHolder resizedFileHolder = _Resize.GetResizedFileHolder(item); item.SetResizedFileHolder(_Resize.FileNameExtension, resizedFileHolder); string facesDirectory = _Configuration.LoadOrCreateThenSaveImageFacesResultsForOutputResolutions.Contains(outputResolution) ? _Faces.GetFacesDirectory(dResultsFullGroupDirectory, item) : string.Empty; string facePartsDirectory = _Configuration.SaveFaceLandmarkForOutputResolutions.Contains(outputResolution) ? _FaceParts.GetFacePartsDirectory(_Configuration.PropertyConfiguration, dResultsFullGroupDirectory, item, includeNameWithoutExtension: true) : string.Empty; @@ -485,7 +524,7 @@ public partial class DlibDotNet if ((_Configuration.DistanceMoveUnableToMatch || _Configuration.DistanceRenameToMatch) && _Configuration.LoadOrCreateThenSaveDistanceResultsForOutputResolutions.Contains(outputResolution) && collection is not null && faceCollection.All(l => !l.Saved)) - _Distance.LookForMatchFacesAndPossiblyRename(_Faces.FileNameExtension, eDistanceContentDirectory, mappingFromItem, faces, collection); + _Distance.LookForMatchFacesAndPossiblyRename(_Faces.FileNameExtension, mappingFromItem, faces, collection); if (_Configuration.SaveFaceLandmarkForOutputResolutions.Contains(outputResolution)) { bool saveRotated = false; @@ -509,10 +548,9 @@ public partial class DlibDotNet MapLogic mapLogic, string outputResolution, string cResultsFullGroupDirectory, - string dResultsDateGroupDirectory, + string c2ResultsFullGroupDirectory, string dResultsFullGroupDirectory, string d2ResultsFullGroupDirectory, - string eDistanceContentDirectory, List> sourceDirectoryChanges, Dictionary> fileNameToCollection, Container container, @@ -540,9 +578,8 @@ public partial class DlibDotNet mapLogic, outputResolution, cResultsFullGroupDirectory, - dResultsDateGroupDirectory, + c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, - eDistanceContentDirectory, sourceDirectoryChanges, fileNameToCollection, container, @@ -620,7 +657,7 @@ public partial class DlibDotNet return new(aResultsFullGroupDirectory, bResultsFullGroupDirectory); } - private (string, string, string) GetResultsFullGroupDirectories(string outputResolution) + private (string, string, string, string) GetResultsFullGroupDirectories(string outputResolution) { string cResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory( _Configuration.PropertyConfiguration, @@ -629,6 +666,13 @@ public partial class DlibDotNet includeResizeGroup: true, includeModel: false, includePredictorModel: false); + string c2ResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory( + _Configuration.PropertyConfiguration, + nameof(ThumbHash.Models.C2_ThumbHasher), + outputResolution, + includeResizeGroup: true, + includeModel: false, + includePredictorModel: false); string dResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory( _Configuration.PropertyConfiguration, nameof(D_Face), @@ -643,10 +687,10 @@ public partial class DlibDotNet includeResizeGroup: true, includeModel: true, includePredictorModel: true); - return new(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory); + return new(cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory); } - private void FullDoWork(string argZero, string propertyRoot, long ticks, string aResultsFullGroupDirectory, string bResultsFullGroupDirectory, int t, Container[] containers, A_Property propertyLogic, B_Metadata metadata, string eDistanceContentDirectory, Dictionary> fileNameToCollection, ReadOnlyDictionary>> idToLocationContainers, MapLogic mapLogic) + private void FullDoWork(string argZero, string propertyRoot, long ticks, string aResultsFullGroupDirectory, string bResultsFullGroupDirectory, int t, Container[] containers, A_Property propertyLogic, B_Metadata metadata, Dictionary> fileNameToCollection, ReadOnlyDictionary>> idToLocationContainers, MapLogic mapLogic) { if (_Log is null) throw new NullReferenceException(nameof(_Log)); @@ -659,6 +703,7 @@ public partial class DlibDotNet bool anyNullOrNoIsUniqueFileName; string cResultsFullGroupDirectory; string dResultsFullGroupDirectory; + string c2ResultsFullGroupDirectory; string d2ResultsFullGroupDirectory; int containersLength = containers.Length; List> sourceDirectoryChanges = new(); @@ -667,7 +712,7 @@ public partial class DlibDotNet foreach (string outputResolution in _Configuration.OutputResolutions) { total = 0; - (cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); + (cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); for (int i = 0; i < containers.Length; i++) { container = containers[i]; @@ -695,10 +740,9 @@ public partial class DlibDotNet mapLogic, outputResolution, cResultsFullGroupDirectory, - dResultsDateGroupDirectory, + c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory, - eDistanceContentDirectory, sourceDirectoryChanges, fileNameToCollection, container, @@ -911,13 +955,14 @@ public partial class DlibDotNet string? directoryName; string cResultsFullGroupDirectory; string dResultsFullGroupDirectory; + string c2ResultsFullGroupDirectory; string d2ResultsFullGroupDirectory; List distinctFilteredIds = Shared.Models.Stateless.Methods.IContainer.GetFilteredDistinctIds(_Configuration.PropertyConfiguration, containers); LookForAbandoned(idToLocationContainers, distinctFilteredIds); LookForAbandoned(bResultsFullGroupDirectory, distinctFilteredIds); foreach (string outputResolution in _Configuration.OutputResolutions) { - (cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); + (cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); directories = Directory.GetDirectories(cResultsFullGroupDirectory, "*", SearchOption.TopDirectoryOnly); foreach (string directory in directories) { @@ -1101,6 +1146,7 @@ public partial class DlibDotNet string bResultsFullGroupDirectory; string cResultsFullGroupDirectory; string dResultsFullGroupDirectory; + string c2ResultsFullGroupDirectory; string d2ResultsFullGroupDirectory; string fPhotoPrismContentDirectory; string fPhotoPrismSingletonDirectory; @@ -1152,7 +1198,7 @@ public partial class DlibDotNet Shared.Models.Stateless.Methods.IGenealogicalDataCommunication.CreateTree(_Configuration.MappingDefaultName, _Configuration.PersonBirthdayFormat, _Configuration.PropertyConfiguration.ResultAllInOne, _PersonContainers, _GenealogicalDataCommunicationHeaderLines, _GenealogicalDataCommunicationFooterLines, ticks, a2PeopleContentDirectory, personKeyToIds); ReadOnlyDictionary>> idToLocationContainers = mapLogic.GetIdToLocationContainers(); fileNameToCollection = !Directory.Exists(fPhotoPrismSingletonDirectory) ? fileNameToCollection = new() : F_PhotoPrism.GetFileNameToCollection(fPhotoPrismSingletonDirectory); - FullDoWork(argZero, propertyRoot, ticks, aResultsFullGroupDirectory, bResultsFullGroupDirectory, t, containers, propertyLogic, metadata, eDistanceContentDirectory, fileNameToCollection, idToLocationContainers, mapLogic); + FullDoWork(argZero, propertyRoot, ticks, aResultsFullGroupDirectory, bResultsFullGroupDirectory, t, containers, propertyLogic, metadata, fileNameToCollection, idToLocationContainers, mapLogic); if (_Configuration.LookForAbandoned) LookForAbandoned(bResultsFullGroupDirectory, containers, idToLocationContainers); _Distance.Clear(); @@ -1175,7 +1221,7 @@ public partial class DlibDotNet mapLogic.SaveShortcutsForOutputResolutionsPreMapLogic(eDistanceContentDirectory, personKeyToIds, distinctFilteredMappingCollection); if (!string.IsNullOrEmpty(a2PeopleContentDirectory) && _Configuration.SaveFilteredOriginalImagesFromJLinksForOutputResolutions.Contains(outputResolution)) mapLogic.SaveFilteredOriginalImagesFromJLinks(_Configuration.JLinks, _PersonContainers, a2PeopleContentDirectory, personKeyToIds, distinctFilteredMappingCollection, totalNotMapped); - (cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); + (cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); if (_ArgZeroIsConfigurationRootDirectory && _Configuration.SaveResizedSubfiles && outputResolution == _Configuration.OutputResolutions[0] diff --git a/Instance/Instance.csproj b/Instance/Instance.csproj index efbd6d1..46cb320 100644 --- a/Instance/Instance.csproj +++ b/Instance/Instance.csproj @@ -49,7 +49,6 @@ - @@ -61,6 +60,7 @@ + diff --git a/Property/Models/A_Property.cs b/Property/Models/A_Property.cs index 5e1aa3b..70ad49d 100644 --- a/Property/Models/A_Property.cs +++ b/Property/Models/A_Property.cs @@ -102,7 +102,7 @@ public class A_Property return results; } - private static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IBlurHasher? blurHasher, FileHolder fileHolder, Shared.Models.Property? property, bool populateId, bool isIgnoreExtension, bool isValidImageFormatExtension, bool isValidMetadataExtensions, int? id, ASCIIEncoding asciiEncoding, bool writeBitmapDataBytes, string? angleBracket) + private static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IThumbHasher? thumbHasher, FileHolder fileHolder, Shared.Models.Property? property, bool populateId, bool isIgnoreExtension, bool isValidImageFormatExtension, bool isValidMetadataExtensions, int? id, ASCIIEncoding asciiEncoding, bool writeBitmapDataBytes, string? angleBracket) { Shared.Models.Property result; byte[] bytes; @@ -114,10 +114,10 @@ public class A_Property string? model = null; string dateTimeFormat; DateTime checkDateTime; - string? blurHash = null; DateTime? dateTime = null; PropertyItem? propertyItem; string? orientation = null; + byte[]? thumbHashBytes = null; DateTime? gpsDateStamp = null; DateTime? dateTimeOriginal = null; DateTime? dateTimeDigitized = null; @@ -134,6 +134,8 @@ public class A_Property try { using Image image = Image.FromFile(fileHolder.FullName); + width = image.Width; + height = image.Height; if (populateId && id is null) { using Bitmap bitmap = new(image); @@ -151,18 +153,6 @@ public class A_Property File.WriteAllBytes(Path.ChangeExtension(contentFileInfo.FullName, string.Empty), bytes); } } - if (blurHasher is not null && property?.BlurHash is null) - { - if (angleBracket is null) - blurHash = blurHasher.Encode(image); - else - { - string blurHashDirectory = angleBracket.Replace("<>", "()"); - blurHash = blurHasher.EncodeAndSave(image, blurHashDirectory); - } - } - width = image.Width; - height = image.Height; dateTimeFormat = Shared.Models.Stateless.Methods.IProperty.DateTimeFormat(); if (image.PropertyIdList.Contains((int)IExif.Tags.DateTime)) { @@ -252,16 +242,18 @@ public class A_Property throw new NullReferenceException(nameof(fileHolder.CreationTime)); if (fileHolder.LastWriteTime is null && property?.LastWriteTime is null) throw new NullReferenceException(nameof(fileHolder.LastWriteTime)); + if (thumbHasher is not null && property?.ThumbHashBytes is null) + thumbHashBytes = thumbHasher.Encode(fileHolder.FullName); if (fileHolder.CreationTime is not null && fileHolder.LastWriteTime is not null) - result = new(blurHash, fileHolder.CreationTime.Value, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, fileHolder.LastWriteTime.Value, make, model, orientation, width); + result = new(fileHolder.CreationTime.Value, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, fileHolder.LastWriteTime.Value, make, model, orientation, thumbHashBytes, width); else if (property is not null) - result = new(blurHash, property.CreationTime, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, property.LastWriteTime, make, model, orientation, width); + result = new(property.CreationTime, dateTime, dateTimeDigitized, dateTimeFromName, dateTimeOriginal, fileLength, gpsDateStamp, height, id, property.LastWriteTime, make, model, orientation, thumbHashBytes, width); else throw new NullReferenceException(nameof(property)); return result; } - public static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IBlurHasher? blurHasher, string fileName) + public static Shared.Models.Property GetImageProperty(Shared.Models.Methods.IThumbHasher? thumbHasher, string fileName) { int? id = null; bool populateId = true; @@ -273,13 +265,13 @@ public class A_Property FileHolder fileHolder = new(fileName); bool isValidImageFormatExtension = true; Shared.Models.Property? property = null; - Shared.Models.Property result = GetImageProperty(blurHasher, fileHolder, property, populateId, isIgnoreExtension, isValidImageFormatExtension, isValidMetadataExtensions, id, asciiEncoding, writeBitmapDataBytes, angleBracket); + Shared.Models.Property result = GetImageProperty(thumbHasher, fileHolder, property, populateId, isIgnoreExtension, isValidImageFormatExtension, isValidMetadataExtensions, id, asciiEncoding, writeBitmapDataBytes, angleBracket); return result; } #pragma warning restore CA1416 - private Shared.Models.Property GetPropertyOfPrivate(Shared.Models.Methods.IBlurHasher? blurHasher, Item item, List> sourceDirectoryFileTuples, List parseExceptions, bool isIgnoreExtension, bool isValidMetadataExtensions) + private Shared.Models.Property GetPropertyOfPrivate(Shared.Models.Methods.IThumbHasher? thumbHasher, Item item, List> sourceDirectoryFileTuples, List parseExceptions, bool isIgnoreExtension, bool isValidMetadataExtensions) { Shared.Models.Property? result; int? id = null; @@ -367,19 +359,16 @@ public class A_Property parseExceptions.Add(nameof(A_Property)); } } - if (!string.IsNullOrEmpty(json) && result is not null && blurHasher is not null && result.BlurHash is null) + if (!string.IsNullOrEmpty(json) && result is not null && thumbHasher is not null && result.ThumbHashBytes is null) { string find = "\"CreationTime\":"; if (!json.Contains(find)) throw new NotImplementedException(); - string blurHashDirectory = angleBracket.Replace("<>", "()"); -#pragma warning disable CA1416 - using Image image = Image.FromFile(item.ImageFileHolder.FullName); -#pragma warning restore CA1416 - string blurHash = blurHasher.EncodeAndSave(image, blurHashDirectory); - json = json.Replace(find, $"\"{nameof(result.BlurHash)}\": \"{blurHash}\", {find}"); + byte[] thumbHashBytes = thumbHasher.Encode(item.ImageFileHolder.FullName); + string thumbHashJson = JsonSerializer.Serialize(thumbHashBytes); + json = json.Replace(find, $"\"{nameof(result.ThumbHashBytes)}\": {thumbHashJson}, {find}"); result = JsonSerializer.Deserialize(json); - if (result is null || result.BlurHash is null) + if (result is null || result.ThumbHashBytes is null) throw new NullReferenceException(nameof(result)); json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions); File.WriteAllText(fileInfo.FullName, json); @@ -388,7 +377,7 @@ public class A_Property if (result is null) { id ??= item.ImageFileHolder.Id; - result = GetImageProperty(blurHasher, item.ImageFileHolder, result, populateId, isIgnoreExtension, item.IsValidImageFormatExtension, isValidMetadataExtensions, id, _ASCIIEncoding, _Configuration.WriteBitmapDataBytes, angleBracket); + result = GetImageProperty(thumbHasher, item.ImageFileHolder, result, populateId, isIgnoreExtension, item.IsValidImageFormatExtension, isValidMetadataExtensions, id, _ASCIIEncoding, _Configuration.WriteBitmapDataBytes, angleBracket); json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions); if (populateId && Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches: true, compareBeforeWrite: true)) { @@ -417,7 +406,7 @@ public class A_Property return result; } - private void SavePropertyParallelForWork(Shared.Models.Methods.IBlurHasher? blurHasher, string sourceDirectory, List> sourceDirectoryFileTuples, List> sourceDirectoryChanges, Item item) + private void SavePropertyParallelForWork(Shared.Models.Methods.IThumbHasher? thumbHasher, string sourceDirectory, List> sourceDirectoryFileTuples, List> sourceDirectoryChanges, Item item) { Shared.Models.Property property; List parseExceptions = new(); @@ -428,7 +417,7 @@ public class A_Property File.Move(item.ImageFileHolder.FullName, filteredSourceDirectoryFileExtensionLowered); if (item.FileSizeChanged is null || item.FileSizeChanged.Value || item.LastWriteTimeChanged is null || item.LastWriteTimeChanged.Value || item.Property is null) { - property = GetPropertyOfPrivate(blurHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions); + property = GetPropertyOfPrivate(thumbHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions); lock (sourceDirectoryChanges) sourceDirectoryChanges.Add(new Tuple(nameof(A_Property), DateTime.Now)); lock (item) @@ -436,7 +425,7 @@ public class A_Property } } - private void SavePropertyParallelWork(int maxDegreeOfParallelism, Shared.Models.Methods.IBlurHasher? blurHasher, List exceptions, List> sourceDirectoryChanges, Container container, List items, string message) + private void SavePropertyParallelWork(int maxDegreeOfParallelism, Shared.Models.Methods.IThumbHasher? thumbHasher, List exceptions, List> sourceDirectoryChanges, Container container, List items, string message) { List> sourceDirectoryFileTuples = new(); ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxDegreeOfParallelism }; @@ -449,7 +438,7 @@ public class A_Property long ticks = DateTime.Now.Ticks; DateTime dateTime = DateTime.Now; List> collection; - SavePropertyParallelForWork(blurHasher, container.SourceDirectory, sourceDirectoryChanges, sourceDirectoryFileTuples, items[i]); + SavePropertyParallelForWork(thumbHasher, container.SourceDirectory, sourceDirectoryChanges, sourceDirectoryFileTuples, items[i]); if (i == 0 || sourceDirectoryChanges.Any()) progressBar.Tick(); lock (sourceDirectoryFileTuples) @@ -478,9 +467,6 @@ public class A_Property singletonDescription: "Properties for each image", collectionDescription: string.Empty, converted: false)); - string directory = _AngleBracketCollection[0].Replace("<>", "()"); - if (!Directory.Exists(directory)) - _ = Directory.CreateDirectory(directory); } private void SetAngleBracketCollection(string sourceDirectory, bool anyNullOrNoIsUniqueFileName) @@ -495,7 +481,7 @@ public class A_Property SetAngleBracketCollection(aResultsFullGroupDirectory, sourceDirectory, anyNullOrNoIsUniqueFileName); } - public void SavePropertyParallelWork(long ticks, int t, Container[] containers, Shared.Models.Methods.IBlurHasher? blurHasher) + public void SavePropertyParallelWork(long ticks, int t, Container[] containers, Shared.Models.Methods.IThumbHasher? thumbHasher) { if (_Log is null) throw new NullReferenceException(nameof(_Log)); @@ -521,7 +507,7 @@ public class A_Property SetAngleBracketCollection(container.SourceDirectory, anyNullOrNoIsUniqueFileName); totalSeconds = (int)Math.Truncate(new TimeSpan(DateTime.Now.Ticks - ticks).TotalSeconds); message = $"{i + 1:000} [{container.Items.Count:000}] / {containersLength:000} - {total} / {t} total - {totalSeconds} total second(s) - {outputResolution} - {container.SourceDirectory}"; - SavePropertyParallelWork(_MaxDegreeOfParallelism, blurHasher, exceptions, sourceDirectoryChanges, container, container.Items, message); + SavePropertyParallelWork(_MaxDegreeOfParallelism, thumbHasher, exceptions, sourceDirectoryChanges, container, container.Items, message); foreach (Exception exception in exceptions) _Log.Error(string.Concat(container.SourceDirectory, Environment.NewLine, exception.Message, Environment.NewLine, exception.StackTrace), exception); if (exceptions.Count == container.Items.Count) @@ -542,7 +528,7 @@ public class A_Property } } - public Shared.Models.Property GetProperty(Shared.Models.Methods.IBlurHasher? blurHasher, Item item, List> sourceDirectoryFileTuples, List parseExceptions) + public Shared.Models.Property GetProperty(Shared.Models.Methods.IThumbHasher? thumbHasher, Item item, List> sourceDirectoryFileTuples, List parseExceptions) { Shared.Models.Property result; bool angleBracketCollectionAny = _AngleBracketCollection.Any(); @@ -554,10 +540,10 @@ public class A_Property } bool isValidMetadataExtensions = _Configuration.ValidMetadataExtensions.Contains(item.ImageFileHolder.ExtensionLowered); bool isIgnoreExtension = item.IsValidImageFormatExtension && _Configuration.IgnoreExtensions.Contains(item.ImageFileHolder.ExtensionLowered); - result = GetPropertyOfPrivate(blurHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions); + result = GetPropertyOfPrivate(thumbHasher, item, sourceDirectoryFileTuples, parseExceptions, isIgnoreExtension, isValidMetadataExtensions); if (!angleBracketCollectionAny) _AngleBracketCollection.Clear(); return result; } -} \ No newline at end of file +} diff --git a/Shared/Models/Methods/IThumbHasher.cs b/Shared/Models/Methods/IThumbHasher.cs new file mode 100644 index 0000000..9465b1e --- /dev/null +++ b/Shared/Models/Methods/IThumbHasher.cs @@ -0,0 +1,10 @@ +namespace View_by_Distance.Shared.Models.Methods; + +public interface IThumbHasher +{ + + byte[] Encode(string path); + (byte[], MemoryStream) EncodeAndSave(string path, int width, int height); + MemoryStream GetMemoryStream(byte[] thumbHashBytes, int width, int height); + +} \ No newline at end of file diff --git a/Shared/Models/Properties/IProperty.cs b/Shared/Models/Properties/IProperty.cs index 079d832..838bcf8 100644 --- a/Shared/Models/Properties/IProperty.cs +++ b/Shared/Models/Properties/IProperty.cs @@ -3,7 +3,6 @@ namespace View_by_Distance.Shared.Models.Properties; public interface IProperty { - public string? BlurHash { init; get; } public DateTime CreationTime { init; get; } public DateTime? DateTime { init; get; } public DateTime? DateTimeDigitized { init; get; } @@ -17,6 +16,7 @@ public interface IProperty public string? Make { init; get; } public string? Model { init; get; } public string? Orientation { init; get; } + public byte[]? ThumbHashBytes { init; get; } public int? Width { init; get; } } \ No newline at end of file diff --git a/Shared/Models/Property.cs b/Shared/Models/Property.cs index db94eb3..3b6c49f 100644 --- a/Shared/Models/Property.cs +++ b/Shared/Models/Property.cs @@ -6,7 +6,6 @@ namespace View_by_Distance.Shared.Models; public class Property : Properties.IProperty { - public string? BlurHash { init; get; } public DateTime CreationTime { init; get; } public DateTime? DateTime { init; get; } public DateTime? DateTimeDigitized { init; get; } @@ -20,12 +19,12 @@ public class Property : Properties.IProperty public string? Make { init; get; } public string? Model { init; get; } public string? Orientation { init; get; } + public byte[]? ThumbHashBytes { init; get; } public int? Width { init; get; } [JsonConstructor] - public Property(string? blurHash, DateTime creationTime, DateTime? dateTime, DateTime? dateTimeDigitized, DateTime? dateTimeFromName, DateTime? dateTimeOriginal, long fileSize, DateTime? gpsDateStamp, int? height, int? id, DateTime lastWriteTime, string? make, string? model, string? orientation, int? width) + public Property(DateTime creationTime, DateTime? dateTime, DateTime? dateTimeDigitized, DateTime? dateTimeFromName, DateTime? dateTimeOriginal, long fileSize, DateTime? gpsDateStamp, int? height, int? id, DateTime lastWriteTime, string? make, string? model, string? orientation, byte[]? thumbHashBytes, int? width) { - BlurHash = blurHash; DateTimeFromName = dateTimeFromName; CreationTime = creationTime; DateTime = dateTime; @@ -39,6 +38,7 @@ public class Property : Properties.IProperty Make = make; Model = model; Orientation = orientation; + ThumbHashBytes = thumbHashBytes; Width = width; } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index ebfb3df..62464e6 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -41,11 +41,11 @@ - + diff --git a/Tests/UnitTestResize.cs b/Tests/UnitTestResize.cs index 0ef38dd..722539a 100644 --- a/Tests/UnitTestResize.cs +++ b/Tests/UnitTestResize.cs @@ -168,8 +168,8 @@ public class UnitTestResize Assert.IsNotNull(item.ImageFileHolder); if (item.Property is null) { - Shared.Models.Methods.IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher(); - property = propertyLogic.GetProperty(blurHasher, item, subFileTuples, parseExceptions); + Shared.Models.Methods.IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher(); + property = propertyLogic.GetProperty(thumbHasher, item, subFileTuples, parseExceptions); item.Update(property); } if (property is null || item.Property is null) diff --git a/TestsWithFaceRecognitionDotNet/TestsWithFaceRecognitionDotNet.csproj b/TestsWithFaceRecognitionDotNet/TestsWithFaceRecognitionDotNet.csproj index 2617c20..96709c7 100644 --- a/TestsWithFaceRecognitionDotNet/TestsWithFaceRecognitionDotNet.csproj +++ b/TestsWithFaceRecognitionDotNet/TestsWithFaceRecognitionDotNet.csproj @@ -40,13 +40,13 @@ - + diff --git a/TestsWithFaceRecognitionDotNet/UnitTestFace.cs b/TestsWithFaceRecognitionDotNet/UnitTestFace.cs index dbbae36..9881281 100644 --- a/TestsWithFaceRecognitionDotNet/UnitTestFace.cs +++ b/TestsWithFaceRecognitionDotNet/UnitTestFace.cs @@ -242,8 +242,8 @@ public class UnitTestFace Assert.IsNotNull(item.ImageFileHolder); if (item.Property is null) { - Shared.Models.Methods.IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher(); - property = propertyLogic.GetProperty(blurHasher, item, subFileTuples, parseExceptions); + Shared.Models.Methods.IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher(); + property = propertyLogic.GetProperty(thumbHasher, item, subFileTuples, parseExceptions); item.Update(property); } if (property is null || item.Property is null) diff --git a/ThumbHash/.vscode/format-report.json b/ThumbHash/.vscode/format-report.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/ThumbHash/.vscode/format-report.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/ThumbHash/Models/SpanOwner.cs b/ThumbHash/Models/SpanOwner.cs new file mode 100644 index 0000000..d228c14 --- /dev/null +++ b/ThumbHash/Models/SpanOwner.cs @@ -0,0 +1,53 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace View_by_Distance.ThumbHash.Models; + +internal readonly ref struct SpanOwner +{ + + private static ArrayPool DefaultPool + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ArrayPool.Shared; + } + + private readonly T[] _Buffer; + private readonly int _Length; + + public static SpanOwner Empty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(0); + } + + public Span Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ref T r0 = ref MemoryMarshal.GetArrayDataReference(_Buffer); + return MemoryMarshal.CreateSpan(ref r0, _Length); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SpanOwner WithLength(int length) => new(length, _Buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SpanOwner(int length) : this(length, DefaultPool.Rent(length)) + { } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private SpanOwner(int length, T[] buffer) + { + _Length = length; + _Buffer = buffer; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() => + DefaultPool.Return(_Buffer); + +} \ No newline at end of file diff --git a/ThumbHash/Models/ThumbHash.Channel.cs b/ThumbHash/Models/ThumbHash.Channel.cs new file mode 100644 index 0000000..9651b01 --- /dev/null +++ b/ThumbHash/Models/ThumbHash.Channel.cs @@ -0,0 +1,31 @@ +using System.Runtime.CompilerServices; + +namespace View_by_Distance.ThumbHash.Models; + +public static partial class ThumbHash +{ + private readonly ref struct Channel + { + + public float DC { init; get; } + public SpanOwner AC { init; get; } + public float Scale { init; get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Channel(float dc, SpanOwner ac, float scale) + { + DC = dc; + AC = ac; + Scale = scale; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Deconstruct(out float dc, out SpanOwner ac, out float scale) + { + dc = DC; + ac = AC; + scale = Scale; + } + + } +} \ No newline at end of file diff --git a/ThumbHash/Models/ThumbHash.RGBA.cs b/ThumbHash/Models/ThumbHash.RGBA.cs new file mode 100644 index 0000000..7924bd7 --- /dev/null +++ b/ThumbHash/Models/ThumbHash.RGBA.cs @@ -0,0 +1,38 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace View_by_Distance.ThumbHash.Models; + +public static partial class ThumbHash +{ + + [StructLayout(LayoutKind.Sequential)] + private readonly struct RGBA + { + + public byte R { init; get; } + public byte G { init; get; } + public byte B { init; get; } + public byte A { init; get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RGBA(byte r, byte g, byte b, byte a) + { + R = r; + G = g; + B = b; + A = a; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Deconstruct(out byte r, out byte g, out byte b, out byte a) + { + r = R; + g = G; + b = B; + a = A; + } + + } + +} \ No newline at end of file diff --git a/ThumbHash/Models/ThumbHash.cs b/ThumbHash/Models/ThumbHash.cs new file mode 100644 index 0000000..371f789 --- /dev/null +++ b/ThumbHash/Models/ThumbHash.cs @@ -0,0 +1,409 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace View_by_Distance.ThumbHash.Models; + +public static partial class ThumbHash +{ + + private const int _MaxHash = 25; + private const int _MinHash = 5; + + [DoesNotReturn] + static void ThrowIfLessThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) => throw new ArgumentOutOfRangeException(paramName, value, $"'{value}' must be greater than or equal to '{other}'."); + + [DoesNotReturn] + static void ThrowIfGreaterThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) => throw new ArgumentOutOfRangeException(paramName, value, $"'{paramName}' must be less than or equal to '{other}'."); + + [DoesNotReturn] + static void ThrowNotEqual(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null, [CallerArgumentExpression(nameof(other))] string? otherName = null) => throw new ArgumentOutOfRangeException(paramName, value, $"'{paramName}' must be equal to '{other}' ('{otherName}')."); + + /// + /// Encodes an RGBA image to a ThumbHash. + /// + /// The width of the input image. Must be ≤100px. + /// The height of the input image. Must be ≤100px. + /// The pixels in the input image, row-by-row. RGB should not be premultiplied by A. Must have `w*h*4` elements. + /// Byte array containing the ThumbHash + public static byte[] RgbaToThumbHash(int width, int height, ReadOnlySpan rgba) + { + Span hash = stackalloc byte[_MaxHash]; + int bytesWritten = RgbaToThumbHash(hash, width, height, rgba); + return hash[..bytesWritten].ToArray(); + } + + /// + /// Encodes an RGBA image to a ThumbHash. + /// + /// + /// The width of the input image. Must be ≤100px. + /// The height of the input image. Must be ≤100px. + /// The pixels in the input image, row-by-row. RGB should not be premultiplied by A. Must have `w*h*4` elements. + /// Number of bytes written into hash span + public static int RgbaToThumbHash(Span hash, int w, int h, ReadOnlySpan rgba_bytes) + { + if (hash.Length < _MinHash) + ThrowIfLessThan(hash.Length, _MinHash); + + // Encoding an image larger than 100x100 is slow with no benefit + if (rgba_bytes.Length != w * h * 4) + ThrowNotEqual(rgba_bytes.Length, w * h * 4); + + // Determine the average color + float avg_r = 0.0f; + float avg_g = 0.0f; + float avg_b = 0.0f; + float avg_a = 0.0f; + + ReadOnlySpan rgba = MemoryMarshal.Cast(rgba_bytes); + foreach (ref readonly RGBA pixel in rgba) + { + float alpha = pixel.A / 255.0f; + avg_b += alpha / 255.0f * pixel.B; + avg_g += alpha / 255.0f * pixel.G; + avg_r += alpha / 255.0f * pixel.R; + avg_a += alpha; + } + + if (avg_a > 0.0f) + { + avg_r /= avg_a; + avg_g /= avg_a; + avg_b /= avg_a; + } + + bool has_alpha = avg_a < (w * h); + int l_limit = has_alpha ? 5 : 7; // Use fewer luminance bits if there's alpha + int lx = Math.Max((int)MathF.Round(l_limit * w / MathF.Max(w, h)), 1); + int ly = Math.Max((int)MathF.Round(l_limit * h / MathF.Max(w, h)), 1); + + using SpanOwner l_owner = new(w * h); // l: luminance + using SpanOwner p_owner = new(w * h); // p: yellow - blue + using SpanOwner q_owner = new(w * h); // q: red - green + using SpanOwner a_owner = new(w * h); // a: alpha + + Span l = l_owner.Span; + Span p = p_owner.Span; + Span q = q_owner.Span; + Span a = a_owner.Span; + + // Convert the image from RGBA to LPQA (composite atop the average color) + int j = 0; + foreach (ref readonly RGBA pixel in rgba) + { + float alpha = pixel.A / 255.0f; + float b = avg_b * (1.0f - alpha) + alpha / 255.0f * pixel.B; + float g = avg_g * (1.0f - alpha) + alpha / 255.0f * pixel.G; + float r = avg_r * (1.0f - alpha) + alpha / 255.0f * pixel.R; + a[j] = alpha; + q[j] = r - g; + p[j] = (r + g) / 2.0f - b; + l[j] = (r + g + b) / 3.0f; + j += 1; + } + + // Encode using the DCT into DC (constant) and normalized AC (varying) terms + Channel encode_channel(ReadOnlySpan channel, int nx, int ny) + { + float dc = 0.0f; + SpanOwner ac_owner = new(nx * ny); + float scale = 0.0f; + + Span fx = stackalloc float[w]; + Span ac = ac_owner.Span; + int n = 0; + for (int cy = 0; cy < ny; cy++) + { + int cx = 0; + while (cx * ny < nx * (ny - cy)) + { + float f = 0.0f; + for (int x = 0; x < w; x++) + { + fx[x] = MathF.Cos(MathF.PI / w * cx * (x + 0.5f)); + } + for (int y = 0; y < h; y++) + { + float fy = MathF.Cos(MathF.PI / h * cy * (y + 0.5f)); + for (int x = 0; x < w; x++) + { + f += channel[x + y * w] * fx[x] * fy; + } + } + f /= w * h; + if (cx > 0 || cy > 0) + { + ac[n++] = f; + scale = MathF.Max(MathF.Abs(f), scale); + } + else + { + dc = f; + } + cx += 1; + } + } + ac_owner = ac_owner.WithLength(n); + ac = ac_owner.Span; + + if (scale > 0.0f) + { + foreach (ref float aci in ac) + { + aci = 0.5f + 0.5f / scale * aci; + } + } + + return new Channel(dc, ac_owner, scale); + }; + + (float l_dc, SpanOwner l_ac, float l_scale) = encode_channel(l, Math.Max(lx, 3), Math.Max(ly, 3)); + (float p_dc, SpanOwner p_ac, float p_scale) = encode_channel(p, 3, 3); + (float q_dc, SpanOwner q_ac, float q_scale) = encode_channel(q, 3, 3); + (float a_dc, SpanOwner a_ac, float a_scale) = has_alpha ? encode_channel(a, 5, 5) : new Channel(1.0f, SpanOwner.Empty, 1.0f); + + // Write the constants + bool is_landscape = w > h; + uint header24 = (uint)MathF.Round(63.0f * l_dc) + | (((uint)MathF.Round(31.5f + 31.5f * p_dc)) << 6) + | (((uint)MathF.Round(31.5f + 31.5f * q_dc)) << 12) + | (((uint)MathF.Round(31.0f * l_scale)) << 18) + | (has_alpha ? 1u << 23 : 0); + int header16 = (ushort)(is_landscape ? ly : lx) + | (((ushort)MathF.Round(63.0f * p_scale)) << 3) + | (((ushort)MathF.Round(63.0f * q_scale)) << 9) + | (is_landscape ? 1 << 15 : 0); + + int hi = 0; + hash[hi++] = (byte)header24; + hash[hi++] = (byte)(header24 >> 8); + hash[hi++] = (byte)(header24 >> 16); + hash[hi++] = (byte)header16; + hash[hi++] = (byte)(header16 >> 8); + if (has_alpha) + { + float fa_dc = MathF.Round(15.0f * a_dc); + float fa_scale = MathF.Round(15.0f * a_scale); + byte ia_dc = (byte)fa_dc; + byte ia_scale = (byte)fa_scale; + hash[hi++] = (byte)(ia_dc | (ia_scale << 4)); + } + + // Write the varying factors + static void WriteFactor(ReadOnlySpan ac, ref bool is_odd, ref int hi, Span hash) + { + for (int i = 0; i < ac.Length; i++) + { + byte u = (byte)MathF.Round(15.0f * ac[i]); + if (is_odd) + { + hash[hi - 1] |= (byte)(u << 4); + } + else + { + hash[hi++] = u; + } + is_odd = !is_odd; + } + } + + using (l_ac) + using (p_ac) + using (q_ac) + using (a_ac) + { + bool is_odd = false; + WriteFactor(l_ac.Span, ref is_odd, ref hi, hash); + WriteFactor(p_ac.Span, ref is_odd, ref hi, hash); + WriteFactor(q_ac.Span, ref is_odd, ref hi, hash); + if (has_alpha) + { + WriteFactor(a_ac.Span, ref is_odd, ref hi, hash); + } + } + + return hi; + } + + /// + /// Decodes a ThumbHash to an RGBA image. + /// + /// Width, height, and unpremultiplied RGBA8 pixels of the rendered ThumbHash. + /// Thrown if the input is too short. + public static byte[] ThumbHashToRgba(ReadOnlySpan hash, int w, int h) + { + using SpanOwner rgba_owner = new(w * h * 4); + Span rgba = rgba_owner.Span; + ThumbHashToRgba(hash, w, h, rgba); + return rgba[..(w * h * 4)].ToArray(); + } + + /// + /// Decodes a ThumbHash to an RGBA image. + /// + /// Width, height, and unpremultiplied RGBA8 pixels of the rendered ThumbHash. + /// Thrown if the input is too short. + /// Thrown if the RGBA span length is less than `w * h * 4` bytes. + public static void ThumbHashToRgba(ReadOnlySpan hash, int w, int h, Span rgba) + { + // Read the constants + uint header24 = hash[0] + | (((uint)hash[1]) << 8) + | (((uint)hash[2]) << 16); + int header16 = hash[3] | (hash[4] << 8); + float l_dc = (header24 & 63) / 63.0f; + float p_dc = ((header24 >> 6) & 63) / 31.5f - 1.0f; + float q_dc = ((header24 >> 12) & 63) / 31.5f - 1.0f; + float l_scale = ((header24 >> 18) & 31) / 31.0f; + bool has_alpha = (header24 >> 23) != 0; + float p_scale = ((header16 >> 3) & 63) / 63.0f; + float q_scale = ((header16 >> 9) & 63) / 63.0f; + bool is_landscape = (header16 >> 15) != 0; + int l_max = has_alpha ? 5 : 7; + int lx = Math.Max(3, is_landscape ? l_max : header16 & 7); + int ly = Math.Max(3, is_landscape ? header16 & 7 : l_max); + (float a_dc, float a_scale) = has_alpha ? ((hash[5] & 15) / 15.0f, (hash[5] >> 4) / 15.0f) : (1.0f, 1.0f); + + // Read the varying factors (boost saturation by 1.25x to compensate for quantization) + static SpanOwner decode_channel(ReadOnlySpan hash, int start, ref int index, int nx, int ny, float scale) + { + SpanOwner ac_owner = new(nx * ny); + Span ac = ac_owner.Span; + int n = 0; + for (int cy = 0; cy < ny; cy++) + { + for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++, n++, index++) + { + int data = hash[start + (index >> 1)] >> ((index & 1) << 2); + ac[n] = ((data & 15) / 7.5f - 1.0f) * scale; + } + } + + return ac_owner.WithLength(n); + }; + + // Decode using the DCT into RGB + if (rgba.Length < w * h * 4) + ThrowIfLessThan(rgba.Length, w * h * 4); + + int ac_start = has_alpha ? 6 : 5; + int ac_index = 0; + + using SpanOwner l_ac_owner = decode_channel(hash, ac_start, ref ac_index, lx, ly, l_scale); + using SpanOwner p_ac_owner = decode_channel(hash, ac_start, ref ac_index, 3, 3, p_scale * 1.25f); + using SpanOwner q_ac_owner = decode_channel(hash, ac_start, ref ac_index, 3, 3, q_scale * 1.25f); + using SpanOwner a_ac_owner = has_alpha ? decode_channel(hash, ac_start, ref ac_index, 5, 5, a_scale) : SpanOwner.Empty; + Span l_ac = l_ac_owner.Span; + Span p_ac = p_ac_owner.Span; + Span q_ac = q_ac_owner.Span; + Span a_ac = a_ac_owner.Span; + + Span fx = stackalloc float[7]; + Span fy = stackalloc float[7]; + + ref RGBA pixel = ref MemoryMarshal.AsRef(rgba); + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++, pixel = ref Unsafe.AddByteOffset(ref pixel, 4)) + { + float l = l_dc; + float p = p_dc; + float q = q_dc; + float a = a_dc; + + // Precompute the coefficients + for (int cx = 0; cx < Math.Max(lx, has_alpha ? 5 : 3); cx++) + { + fx[cx] = MathF.Cos(MathF.PI / w * (x + 0.5f) * cx); + } + for (int cy = 0; cy < Math.Max(ly, has_alpha ? 5 : 3); cy++) + { + fy[cy] = MathF.Cos(MathF.PI / h * (y + 0.5f) * cy); + } + + // Decode L + for (int cy = 0, j = 0; cy < ly; cy++) + { + int cx = cy > 0 ? 0 : 1; + float fy2 = fy[cy] * 2.0f; + while (cx * ly < lx * (ly - cy)) + { + l += l_ac[j] * fx[cx] * fy2; + j += 1; + cx += 1; + } + } + + // Decode P and Q + for (int cy = 0, j = 0; cy < 3; cy++) + { + int cx = cy > 0 ? 0 : 1; + float fy2 = fy[cy] * 2.0f; + while (cx < 3 - cy) + { + float f = fx[cx] * fy2; + p += p_ac[j] * f; + q += q_ac[j] * f; + j += 1; + cx += 1; + } + } + + // Decode A + if (has_alpha) + { + for (int cy = 0, j = 0; cy < 5; cy++) + { + int cx = cy > 0 ? 0 : 1; + float fy2 = fy[cy] * 2.0f; + while (cx < 5 - cy) + { + a += a_ac[j] * fx[cx] * fy2; + j += 1; + cx += 1; + } + } + } + + // Convert to RGB + float b = l - 2.0f / 3.0f * p; + float r = (3.0f * l - b + q) / 2.0f; + float g = r - q; + + pixel = new( + r: (byte)(Math.Clamp(r, 0.0f, 1.0f) * 255.0f), + g: (byte)(Math.Clamp(g, 0.0f, 1.0f) * 255.0f), + b: (byte)(Math.Clamp(b, 0.0f, 1.0f) * 255.0f), + a: (byte)(Math.Clamp(a, 0.0f, 1.0f) * 255.0f)); + } + } + } + + /// + /// Extracts the average color from a ThumbHash. + /// + /// Unpremultiplied RGBA values where each value ranges from 0 to 1. + /// Thrown if the input is too short. + public static (float r, float g, float b, float a) ThumbHashToAverageRgba(ReadOnlySpan hash) + { + if (hash.Length < _MinHash) + ThrowIfLessThan(hash.Length, _MinHash); + + uint header = hash[0] | ((uint)hash[1] << 8) | ((uint)hash[2] << 16); + float l = (header & 63) / 63.0f; + float p = ((header >> 6) & 63) / 31.5f - 1.0f; + float q = ((header >> 12) & 63) / 31.5f - 1.0f; + bool has_alpha = (header >> 23) != 0; + float a = has_alpha ? (hash[5] & 15) / 15.0f : 1.0f; + float b = l - 2.0f / 3.0f * p; + float r = (3.0f * l - b + q) / 2.0f; + float g = r - q; + + return (r: Math.Clamp(r, 0.0f, 1.0f), + g: Math.Clamp(g, 0.0f, 1.0f), + b: Math.Clamp(b, 0.0f, 1.0f), + a); + } + +} \ No newline at end of file diff --git a/ThumbHash/Models/ThumbHasher.cs b/ThumbHash/Models/ThumbHasher.cs new file mode 100644 index 0000000..217959e --- /dev/null +++ b/ThumbHash/Models/ThumbHasher.cs @@ -0,0 +1,57 @@ +using SkiaSharp; +using View_by_Distance.Shared.Models.Methods; + +namespace View_by_Distance.ThumbHash.Models; + +public class C2_ThumbHasher : IThumbHasher +{ + + private static SKBitmap GetBitmap(string path) + { + SKBitmap result; + using SKBitmap skBitmap = SKBitmap.Decode(path); + result = skBitmap.Copy(SKColorType.Rgba8888); + return result; + } + + private static byte[] Encode(SKBitmap skBitmap) => + ThumbHash.RgbaToThumbHash(skBitmap.Width, skBitmap.Height, skBitmap.GetPixelSpan()); + + private static byte[] Encode(string path) + { + byte[] results; + using SKBitmap skBitmap = GetBitmap(path); + results = Encode(skBitmap); + return results; + } + + byte[] IThumbHasher.Encode(string path) => + Encode(path); + + private static MemoryStream GetMemoryStream(byte[] thumbHashBytes, int width, int height) + { + MemoryStream result = new(); + (int w, int h) = (width / 2, height / 2); + using SKManagedWStream skManagedWStream = new(result); + byte[] bytes = ThumbHash.ThumbHashToRgba(thumbHashBytes, w, h); + SKImageInfo skImageInfo = new(w, h, SKColorType.Rgba8888, SKAlphaType.Unpremul); + using SKImage skImage = SKImage.FromPixelCopy(skImageInfo, bytes); + using SKData sdData = skImage.Encode(SKEncodedImageFormat.Png, 100); + sdData.SaveTo(result); + return result; + } + + MemoryStream IThumbHasher.GetMemoryStream(byte[] thumbHashBytes, int width, int height) => + GetMemoryStream(thumbHashBytes, width, height); + + (byte[], MemoryStream) IThumbHasher.EncodeAndSave(string path, int width, int height) + { + byte[] results; + MemoryStream result; + using SKBitmap skBitmap = GetBitmap(path); + results = Encode(skBitmap); + result = GetMemoryStream(results, width, height); + return (results, result); + } + +} \ No newline at end of file diff --git a/ThumbHash/ThumbHash.csproj b/ThumbHash/ThumbHash.csproj new file mode 100644 index 0000000..8debdf8 --- /dev/null +++ b/ThumbHash/ThumbHash.csproj @@ -0,0 +1,45 @@ + + + enable + 10.0 + enable + library + win-x64 + net7.0 + + + Phares.View.by.Distance.ThumbHash + false + 7.0.101.1 + Mike Phares + Phares + true + snupkg + + + true + true + true + + + Windows + + + OSX + + + Linux + + + + + + + + + + + + + + \ No newline at end of file diff --git a/View-by-Distance-MKLink-Console.sln b/View-by-Distance-MKLink-Console.sln index 77953b1..2e4d7eb 100644 --- a/View-by-Distance-MKLink-Console.sln +++ b/View-by-Distance-MKLink-Console.sln @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestsWithFaceRecognitionDotNet", "TestsWithFaceRecognitionDotNet\TestsWithFaceRecognitionDotNet.csproj", "{A67D73C7-A1A1-4443-B681-776339CFA08A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThumbHash", "ThumbHash\ThumbHash.csproj", "{6F146145-F7A4-4AB7-94C4-C822A9FC8269}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "View-by-Distance.Shared", "shared\View-by-Distance.Shared.csproj", "{1D231660-33B4-4763-9C9F-C6ACC8BA600D}" EndProject Global @@ -172,5 +174,9 @@ Global {9689371E-F67C-4392-A636-C398D28C089B}.Debug|Any CPU.Build.0 = Debug|Any CPU {9689371E-F67C-4392-A636-C398D28C089B}.Release|Any CPU.ActiveCfg = Release|Any CPU {9689371E-F67C-4392-A636-C398D28C089B}.Release|Any CPU.Build.0 = Release|Any CPU + {6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F146145-F7A4-4AB7-94C4-C822A9FC8269}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal