Switched to ThumbHasher over BlurHasher

This commit is contained in:
2023-05-21 23:56:10 -07:00
parent 514637b9c6
commit a0c880c7ba
26 changed files with 803 additions and 121 deletions

View File

@ -56,15 +56,15 @@ public static class Core
{ {
double basis = xCosines[xPixel] * yCosines[yPixel]; double basis = xCosines[xPixel] * yCosines[yPixel];
Pixel pixel = pixels[xPixel, yPixel]; Pixel pixel = pixels[xPixel, yPixel];
r += basis * pixel._Red; r += basis * pixel.Red;
g += basis * pixel._Green; g += basis * pixel.Green;
b += basis * pixel._Blue; b += basis * pixel.Blue;
} }
double scale = normalization / (width * height); double scale = normalization / (width * height);
factors[componentsX * yComponent + xComponent]._Red = r * scale; factors[componentsX * yComponent + xComponent].Red = r * scale;
factors[componentsX * yComponent + xComponent]._Green = g * scale; factors[componentsX * yComponent + xComponent].Green = g * scale;
factors[componentsX * yComponent + xComponent]._Blue = b * scale; factors[componentsX * yComponent + xComponent].Blue = b * scale;
progressCallback?.Report(processedFactors * 100 / factorCount); progressCallback?.Report(processedFactors * 100 / factorCount);
processedFactors++; processedFactors++;
@ -90,9 +90,9 @@ public static class Core
int factorIndex = componentsX * yComponent + xComponent; int factorIndex = componentsX * yComponent + xComponent;
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Red), 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].Green), actualMaximumValue);
actualMaximumValue = Math.Max(Math.Abs(factors[factorIndex]._Blue), 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))); 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'; 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 yComponent = 0; yComponent < componentsY; yComponent++)
for (int xComponent = 0; xComponent < componentsX; xComponent++) for (int xComponent = 0; xComponent < componentsX; xComponent++)
@ -116,7 +116,7 @@ public static class Core
int factorIndex = componentsX * yComponent + xComponent; 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(); return resultBuffer.ToString();
@ -184,9 +184,9 @@ public static class Core
{ {
ref Pixel result = ref pixels[xPixel, yPixel]; ref Pixel result = ref pixels[xPixel, yPixel];
result._Red = 0.0; result.Red = 0.0;
result._Green = 0.0; result.Green = 0.0;
result._Blue = 0.0; result.Blue = 0.0;
} }
double[] xCosines = new double[outputWidth]; double[] xCosines = new double[outputWidth];
@ -215,9 +215,9 @@ public static class Core
double basis = xCosines[xPixel] * yCosines[yPixel]; double basis = xCosines[xPixel] * yCosines[yPixel];
result._Red += coefficient._Red * basis; result.Red += coefficient.Red * basis;
result._Green += coefficient._Green * basis; result.Green += coefficient.Green * basis;
result._Blue += coefficient._Blue * basis; result.Blue += coefficient.Blue * basis;
} }
progressCallback?.Report(componentIndex * 100 / componentCount); progressCallback?.Report(componentIndex * 100 / componentCount);

View File

@ -5,14 +5,14 @@
/// </summary> /// </summary>
public struct Pixel public struct Pixel
{ {
public double _Red; public double Red { get; set; }
public double _Green; public double Green { get; set; }
public double _Blue; public double Blue { get; set; }
public Pixel(double red, double green, double blue) public Pixel(double red, double green, double blue)
{ {
_Red = red; Red = red;
_Green = green; Green = green;
_Blue = blue; Blue = blue;
} }
} }

View File

@ -63,9 +63,9 @@ public static class BlurHasher
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
ref Pixel res = ref result[x, y]; ref Pixel res = ref result[x, y];
res._Blue = MathUtils.SRgbToLinear(rgb[index++]); res.Blue = MathUtils.SRgbToLinear(rgb[index++]);
res._Green = MathUtils.SRgbToLinear(rgb[index++]); res.Green = MathUtils.SRgbToLinear(rgb[index++]);
res._Red = MathUtils.SRgbToLinear(rgb[index++]); res.Red = MathUtils.SRgbToLinear(rgb[index++]);
} }
} }
@ -92,9 +92,9 @@ public static class BlurHasher
{ {
Pixel pixel = pixelData[xPixel, yPixel]; Pixel pixel = pixelData[xPixel, yPixel];
data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Blue); data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Blue);
data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Green); data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Green);
data[index++] = (byte)MathUtils.LinearTosRgb(pixel._Red); data[index++] = (byte)MathUtils.LinearTosRgb(pixel.Red);
data[index++] = 0; data[index++] = 0;
} }

View File

@ -31,8 +31,8 @@ public class BlurHasher : IBlurHasher
string result; string result;
int actualByte; int actualByte;
result = System.Drawing.BlurHash.BlurHasher.Encode(image, x, y); 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); 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 joined = string.Join(string.Empty, blurHashBytes.Select(l => l.ToString("000")));
string fileName = Path.Combine(directory, $"{x}x{y}-{width}x{height}-{joined}.png"); string fileName = Path.Combine(directory, $"{x}x{y}-{width}x{height}-{joined}.png");
if (!File.Exists(fileName)) if (!File.Exists(fileName))

View File

@ -50,9 +50,9 @@
<PackageReference Include="System.Text.Json" Version="7.0.2" /> <PackageReference Include="System.Text.Json" Version="7.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" /> <ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
<ProjectReference Include="..\Property\Property.csproj" /> <ProjectReference Include="..\Property\Property.csproj" />
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="appsettings.json"> <None Include="appsettings.json">

View File

@ -65,8 +65,8 @@ public class DateGroup
throw new Exception(); throw new Exception();
if (propertyConfiguration.PopulatePropertyId && (configuration.ByCreateDateShortcut || configuration.ByHash) && Shared.Models.Stateless.Methods.IProperty.Any(containers)) if (propertyConfiguration.PopulatePropertyId && (configuration.ByCreateDateShortcut || configuration.ByHash) && Shared.Models.Stateless.Methods.IProperty.Any(containers))
{ {
IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher(); IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher();
propertyLogic.SavePropertyParallelWork(ticks, t, containers, blurHasher); propertyLogic.SavePropertyParallelWork(ticks, t, containers, thumbHasher);
if (appSettings.MaxDegreeOfParallelism < 2) if (appSettings.MaxDegreeOfParallelism < 2)
ticks = LogDelta(ticks, nameof(A_Property.SavePropertyParallelWork)); ticks = LogDelta(ticks, nameof(A_Property.SavePropertyParallelWork));
if (propertyLogic.ExceptionsDirectories.Any()) 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) 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 Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)[] results;
Item[] filteredItems;
string? topDirectory; string? topDirectory;
string? checkDirectory; string? checkDirectory;
string destinationDirectory; string destinationDirectory;
Item[] filteredItems;
List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollection = new(); List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollection = new();
List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollectionDirectory; List<(Item Item, long LastWriteTimeTicks, long MinimumDateTimeTicks, string[] Destination)> fileMoveCollectionDirectory;
foreach (Container container in containers) foreach (Container container in containers)

View File

@ -195,7 +195,7 @@ public partial class E_Distance
} }
} }
public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, string eDistanceContentDirectory, MappingFromItem mappingFromItem, List<Face> faces, List<LocationContainer<MetadataExtractor.Directory>> collection) public void LookForMatchFacesAndPossiblyRename(string facesFileNameExtension, MappingFromItem mappingFromItem, List<Face> faces, List<LocationContainer<MetadataExtractor.Directory>> collection)
{ {
string? json; string? json;
string fileName; string fileName;

View File

@ -202,11 +202,11 @@ public partial class DragDropMove : Form
List<(string, int, DateTime)> results = new(); List<(string, int, DateTime)> results = new();
DateTime dateTime; DateTime dateTime;
Shared.Models.Property property; Shared.Models.Property property;
Shared.Models.Methods.IBlurHasher? blurHasher = null; Shared.Models.Methods.IThumbHasher? thumbHasher = null;
string[] files = Directory.GetFiles(checkDirectory, "*", SearchOption.TopDirectoryOnly); string[] files = Directory.GetFiles(checkDirectory, "*", SearchOption.TopDirectoryOnly);
foreach (string file in files) 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) if (property.Id is null || property.DateTimeOriginal is null)
continue; continue;
dateTime = property.DateTimeOriginal.Value.AddTicks(ticks); dateTime = property.DateTimeOriginal.Value.AddTicks(ticks);
@ -240,7 +240,7 @@ public partial class DragDropMove : Form
ticks++; ticks++;
} }
Shared.Models.Methods.IBlurHasher? blurHasher = null; Shared.Models.Methods.IThumbHasher? thumbHasher = null;
List<(string, int, DateTime)> collection = GetCollection(checkDirectory, minimumDateTime, maximumDateTime, ticks); List<(string, int, DateTime)> collection = GetCollection(checkDirectory, minimumDateTime, maximumDateTime, ticks);
ConstructorInfo? constructorInfo = typeof(PropertyItem).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, Array.Empty<Type>(), null) ?? throw new Exception(); ConstructorInfo? constructorInfo = typeof(PropertyItem).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, Array.Empty<Type>(), null) ?? throw new Exception();
foreach ((string file, int id, DateTime dateTime) in collection) foreach ((string file, int id, DateTime dateTime) in collection)
@ -255,7 +255,7 @@ public partial class DragDropMove : Form
bitmap.SetPropertyItem(propertyItem); bitmap.SetPropertyItem(propertyItem);
bitmap.Save(checkFile); bitmap.Save(checkFile);
bitmap.Dispose(); 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) if (property.Id is null || property.Id.Value != id)
throw new Exception(); 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!"); _Logger.Error("bad file(s) or target file(s) or maximum directory doesn't equal 1!");
else 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 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); 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 badProperty = Property.Models.A_Property.GetImageProperty(thumbHasher, badFiles.First());
Shared.Models.Property targetProperty = Property.Models.A_Property.GetImageProperty(blurHasher, targetFiles.First()); Shared.Models.Property targetProperty = Property.Models.A_Property.GetImageProperty(thumbHasher, targetFiles.First());
if (badProperty.DateTimeOriginal is null || targetProperty.DateTimeOriginal is null) if (badProperty.DateTimeOriginal is null || targetProperty.DateTimeOriginal is null)
_Logger.Error("Date is null!"); _Logger.Error("Date is null!");
else else

View File

@ -2,7 +2,6 @@
using Phares.Shared; using Phares.Shared;
using ShellProgressBar; using ShellProgressBar;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Drawing;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -32,8 +31,8 @@ public partial class DlibDotNet
private readonly Serilog.ILogger? _Log; private readonly Serilog.ILogger? _Log;
private readonly D2_FaceParts _FaceParts; private readonly D2_FaceParts _FaceParts;
private readonly AppSettings _AppSettings; private readonly AppSettings _AppSettings;
private readonly IBlurHasher _IBlurHasher;
private readonly List<string> _Exceptions; private readonly List<string> _Exceptions;
private readonly IThumbHasher _IThumbHasher;
private readonly IsEnvironment _IsEnvironment; private readonly IsEnvironment _IsEnvironment;
private readonly bool _PropertyRootExistedBefore; private readonly bool _PropertyRootExistedBefore;
private readonly Models.Configuration _Configuration; private readonly Models.Configuration _Configuration;
@ -59,7 +58,7 @@ public partial class DlibDotNet
long ticks = DateTime.Now.Ticks; long ticks = DateTime.Now.Ticks;
_Exceptions = new List<string>(); _Exceptions = new List<string>();
_Log = Serilog.Log.ForContext<DlibDotNet>(); _Log = Serilog.Log.ForContext<DlibDotNet>();
_IBlurHasher = new BlurHash.Models.BlurHasher(); _IThumbHasher = new ThumbHash.Models.C2_ThumbHasher();
Property.Models.Configuration propertyConfiguration = Property.Models.Binder.Configuration.Get(isEnvironment, configurationRoot); Property.Models.Configuration propertyConfiguration = Property.Models.Binder.Configuration.Get(isEnvironment, configurationRoot);
Models.Configuration configuration = Models.Binder.Configuration.Get(isEnvironment, configurationRoot, propertyConfiguration); Models.Configuration configuration = Models.Binder.Configuration.Get(isEnvironment, configurationRoot, propertyConfiguration);
_Log.Information(propertyConfiguration.RootDirectory); _Log.Information(propertyConfiguration.RootDirectory);
@ -355,9 +354,8 @@ public partial class DlibDotNet
MapLogic mapLogic, MapLogic mapLogic,
string outputResolution, string outputResolution,
string cResultsFullGroupDirectory, string cResultsFullGroupDirectory,
string dResultsDateGroupDirectory, string c2ResultsFullGroupDirectory,
string dResultsFullGroupDirectory, string dResultsFullGroupDirectory,
string eDistanceContentDirectory,
List<Tuple<string, DateTime>> sourceDirectoryChanges, List<Tuple<string, DateTime>> sourceDirectoryChanges,
Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection, Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection,
Container container, Container container,
@ -377,7 +375,8 @@ public partial class DlibDotNet
List<string> parseExceptions = new(); List<string> parseExceptions = new();
List<Tuple<string, DateTime>> subFileTuples = new(); List<Tuple<string, DateTime>> subFileTuples = new();
List<KeyValuePair<string, string>> metadataCollection; List<KeyValuePair<string, string>> 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 aResultsFullGroupDirectory, _) = GetResultsFullGroupDirectories();
string aPropertySingletonDirectory = Path.Combine(aResultsFullGroupDirectory, "{}"); string aPropertySingletonDirectory = Path.Combine(aResultsFullGroupDirectory, "{}");
@ -389,19 +388,60 @@ public partial class DlibDotNet
string find = "\"CreationTime\":"; string find = "\"CreationTime\":";
if (!json.Contains(find)) if (!json.Contains(find))
throw new NotImplementedException(); throw new NotImplementedException();
#pragma warning disable CA1416 byte[]? thumbHashBytes = _IThumbHasher.Encode(resizedFileHolder.FullName);
using Image image = Image.FromFile(item.ImageFileHolder.FullName); string thumbHashJson = JsonSerializer.Serialize(thumbHashBytes);
#pragma warning restore CA1416 json = json.Replace(find, $"\"{nameof(property.ThumbHashBytes)}\": {thumbHashJson}, {find}");
string blurHash = _IBlurHasher.Encode(image);
json = json.Replace(find, $"\"{nameof(item.Property.BlurHash)}\": \"{blurHash}\", {find}");
property = JsonSerializer.Deserialize<Shared.Models.Property>(json); property = JsonSerializer.Deserialize<Shared.Models.Property>(json);
if (property is null || property.BlurHash is null) if (property is null || property.ThumbHashBytes is null)
throw new NullReferenceException(nameof(property)); throw new NullReferenceException(nameof(property));
json = JsonSerializer.Serialize(property, new JsonSerializerOptions { WriteIndented = true }); 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<byte[]>(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.WriteAllText(matchFile, json);
File.SetLastWriteTime(matchFile, item.Property.LastWriteTime); 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()) if (item.Property is not null && item.Property.Id is not null && !item.Any())
{ {
property = item.Property; property = item.Property;
@ -427,7 +467,7 @@ public partial class DlibDotNet
_Log.Information(string.Concat("LastWriteTimeChanged <", item.ImageFileHolder.FullName, '>')); _Log.Information(string.Concat("LastWriteTimeChanged <", item.ImageFileHolder.FullName, '>'));
else if (item.Moved.HasValue && item.Moved.Value) else if (item.Moved.HasValue && item.Moved.Value)
_Log.Information(string.Concat("Moved <", item.ImageFileHolder.FullName, '>')); _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); item.Update(property);
if (propertyHashCode is null) if (propertyHashCode is null)
{ {
@ -442,7 +482,6 @@ public partial class DlibDotNet
} }
if (property is null || item.Property is null) if (property is null || item.Property is null)
throw new NullReferenceException(nameof(property)); throw new NullReferenceException(nameof(property));
FileHolder resizedFileHolder = _Resize.GetResizedFileHolder(item);
item.SetResizedFileHolder(_Resize.FileNameExtension, resizedFileHolder); item.SetResizedFileHolder(_Resize.FileNameExtension, resizedFileHolder);
string facesDirectory = _Configuration.LoadOrCreateThenSaveImageFacesResultsForOutputResolutions.Contains(outputResolution) ? _Faces.GetFacesDirectory(dResultsFullGroupDirectory, item) : string.Empty; 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; 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) if ((_Configuration.DistanceMoveUnableToMatch || _Configuration.DistanceRenameToMatch)
&& _Configuration.LoadOrCreateThenSaveDistanceResultsForOutputResolutions.Contains(outputResolution) && _Configuration.LoadOrCreateThenSaveDistanceResultsForOutputResolutions.Contains(outputResolution)
&& collection is not null && faceCollection.All(l => !l.Saved)) && 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)) if (_Configuration.SaveFaceLandmarkForOutputResolutions.Contains(outputResolution))
{ {
bool saveRotated = false; bool saveRotated = false;
@ -509,10 +548,9 @@ public partial class DlibDotNet
MapLogic mapLogic, MapLogic mapLogic,
string outputResolution, string outputResolution,
string cResultsFullGroupDirectory, string cResultsFullGroupDirectory,
string dResultsDateGroupDirectory, string c2ResultsFullGroupDirectory,
string dResultsFullGroupDirectory, string dResultsFullGroupDirectory,
string d2ResultsFullGroupDirectory, string d2ResultsFullGroupDirectory,
string eDistanceContentDirectory,
List<Tuple<string, DateTime>> sourceDirectoryChanges, List<Tuple<string, DateTime>> sourceDirectoryChanges,
Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection, Dictionary<string, List<MappingFromPhotoPrism>> fileNameToCollection,
Container container, Container container,
@ -540,9 +578,8 @@ public partial class DlibDotNet
mapLogic, mapLogic,
outputResolution, outputResolution,
cResultsFullGroupDirectory, cResultsFullGroupDirectory,
dResultsDateGroupDirectory, c2ResultsFullGroupDirectory,
dResultsFullGroupDirectory, dResultsFullGroupDirectory,
eDistanceContentDirectory,
sourceDirectoryChanges, sourceDirectoryChanges,
fileNameToCollection, fileNameToCollection,
container, container,
@ -620,7 +657,7 @@ public partial class DlibDotNet
return new(aResultsFullGroupDirectory, bResultsFullGroupDirectory); 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( string cResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(
_Configuration.PropertyConfiguration, _Configuration.PropertyConfiguration,
@ -629,6 +666,13 @@ public partial class DlibDotNet
includeResizeGroup: true, includeResizeGroup: true,
includeModel: false, includeModel: false,
includePredictorModel: 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( string dResultsFullGroupDirectory = Property.Models.Stateless.IResult.GetResultsFullGroupDirectory(
_Configuration.PropertyConfiguration, _Configuration.PropertyConfiguration,
nameof(D_Face), nameof(D_Face),
@ -643,10 +687,10 @@ public partial class DlibDotNet
includeResizeGroup: true, includeResizeGroup: true,
includeModel: true, includeModel: true,
includePredictorModel: 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<string, List<MappingFromPhotoPrism>> fileNameToCollection, ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> 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<string, List<MappingFromPhotoPrism>> fileNameToCollection, ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> idToLocationContainers, MapLogic mapLogic)
{ {
if (_Log is null) if (_Log is null)
throw new NullReferenceException(nameof(_Log)); throw new NullReferenceException(nameof(_Log));
@ -659,6 +703,7 @@ public partial class DlibDotNet
bool anyNullOrNoIsUniqueFileName; bool anyNullOrNoIsUniqueFileName;
string cResultsFullGroupDirectory; string cResultsFullGroupDirectory;
string dResultsFullGroupDirectory; string dResultsFullGroupDirectory;
string c2ResultsFullGroupDirectory;
string d2ResultsFullGroupDirectory; string d2ResultsFullGroupDirectory;
int containersLength = containers.Length; int containersLength = containers.Length;
List<Tuple<string, DateTime>> sourceDirectoryChanges = new(); List<Tuple<string, DateTime>> sourceDirectoryChanges = new();
@ -667,7 +712,7 @@ public partial class DlibDotNet
foreach (string outputResolution in _Configuration.OutputResolutions) foreach (string outputResolution in _Configuration.OutputResolutions)
{ {
total = 0; total = 0;
(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); (cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
for (int i = 0; i < containers.Length; i++) for (int i = 0; i < containers.Length; i++)
{ {
container = containers[i]; container = containers[i];
@ -695,10 +740,9 @@ public partial class DlibDotNet
mapLogic, mapLogic,
outputResolution, outputResolution,
cResultsFullGroupDirectory, cResultsFullGroupDirectory,
dResultsDateGroupDirectory, c2ResultsFullGroupDirectory,
dResultsFullGroupDirectory, dResultsFullGroupDirectory,
d2ResultsFullGroupDirectory, d2ResultsFullGroupDirectory,
eDistanceContentDirectory,
sourceDirectoryChanges, sourceDirectoryChanges,
fileNameToCollection, fileNameToCollection,
container, container,
@ -911,13 +955,14 @@ public partial class DlibDotNet
string? directoryName; string? directoryName;
string cResultsFullGroupDirectory; string cResultsFullGroupDirectory;
string dResultsFullGroupDirectory; string dResultsFullGroupDirectory;
string c2ResultsFullGroupDirectory;
string d2ResultsFullGroupDirectory; string d2ResultsFullGroupDirectory;
List<int> distinctFilteredIds = Shared.Models.Stateless.Methods.IContainer.GetFilteredDistinctIds(_Configuration.PropertyConfiguration, containers); List<int> distinctFilteredIds = Shared.Models.Stateless.Methods.IContainer.GetFilteredDistinctIds(_Configuration.PropertyConfiguration, containers);
LookForAbandoned(idToLocationContainers, distinctFilteredIds); LookForAbandoned(idToLocationContainers, distinctFilteredIds);
LookForAbandoned(bResultsFullGroupDirectory, distinctFilteredIds); LookForAbandoned(bResultsFullGroupDirectory, distinctFilteredIds);
foreach (string outputResolution in _Configuration.OutputResolutions) foreach (string outputResolution in _Configuration.OutputResolutions)
{ {
(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); (cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
directories = Directory.GetDirectories(cResultsFullGroupDirectory, "*", SearchOption.TopDirectoryOnly); directories = Directory.GetDirectories(cResultsFullGroupDirectory, "*", SearchOption.TopDirectoryOnly);
foreach (string directory in directories) foreach (string directory in directories)
{ {
@ -1101,6 +1146,7 @@ public partial class DlibDotNet
string bResultsFullGroupDirectory; string bResultsFullGroupDirectory;
string cResultsFullGroupDirectory; string cResultsFullGroupDirectory;
string dResultsFullGroupDirectory; string dResultsFullGroupDirectory;
string c2ResultsFullGroupDirectory;
string d2ResultsFullGroupDirectory; string d2ResultsFullGroupDirectory;
string fPhotoPrismContentDirectory; string fPhotoPrismContentDirectory;
string fPhotoPrismSingletonDirectory; 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); Shared.Models.Stateless.Methods.IGenealogicalDataCommunication.CreateTree(_Configuration.MappingDefaultName, _Configuration.PersonBirthdayFormat, _Configuration.PropertyConfiguration.ResultAllInOne, _PersonContainers, _GenealogicalDataCommunicationHeaderLines, _GenealogicalDataCommunicationFooterLines, ticks, a2PeopleContentDirectory, personKeyToIds);
ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> idToLocationContainers = mapLogic.GetIdToLocationContainers(); ReadOnlyDictionary<int, List<LocationContainer<MetadataExtractor.Directory>>> idToLocationContainers = mapLogic.GetIdToLocationContainers();
fileNameToCollection = !Directory.Exists(fPhotoPrismSingletonDirectory) ? fileNameToCollection = new() : F_PhotoPrism.GetFileNameToCollection(fPhotoPrismSingletonDirectory); 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) if (_Configuration.LookForAbandoned)
LookForAbandoned(bResultsFullGroupDirectory, containers, idToLocationContainers); LookForAbandoned(bResultsFullGroupDirectory, containers, idToLocationContainers);
_Distance.Clear(); _Distance.Clear();
@ -1175,7 +1221,7 @@ public partial class DlibDotNet
mapLogic.SaveShortcutsForOutputResolutionsPreMapLogic(eDistanceContentDirectory, personKeyToIds, distinctFilteredMappingCollection); mapLogic.SaveShortcutsForOutputResolutionsPreMapLogic(eDistanceContentDirectory, personKeyToIds, distinctFilteredMappingCollection);
if (!string.IsNullOrEmpty(a2PeopleContentDirectory) && _Configuration.SaveFilteredOriginalImagesFromJLinksForOutputResolutions.Contains(outputResolution)) if (!string.IsNullOrEmpty(a2PeopleContentDirectory) && _Configuration.SaveFilteredOriginalImagesFromJLinksForOutputResolutions.Contains(outputResolution))
mapLogic.SaveFilteredOriginalImagesFromJLinks(_Configuration.JLinks, _PersonContainers, a2PeopleContentDirectory, personKeyToIds, distinctFilteredMappingCollection, totalNotMapped); mapLogic.SaveFilteredOriginalImagesFromJLinks(_Configuration.JLinks, _PersonContainers, a2PeopleContentDirectory, personKeyToIds, distinctFilteredMappingCollection, totalNotMapped);
(cResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution); (cResultsFullGroupDirectory, c2ResultsFullGroupDirectory, dResultsFullGroupDirectory, d2ResultsFullGroupDirectory) = GetResultsFullGroupDirectories(outputResolution);
if (_ArgZeroIsConfigurationRootDirectory if (_ArgZeroIsConfigurationRootDirectory
&& _Configuration.SaveResizedSubfiles && _Configuration.SaveResizedSubfiles
&& outputResolution == _Configuration.OutputResolutions[0] && outputResolution == _Configuration.OutputResolutions[0]

View File

@ -49,7 +49,6 @@
<PackageReference Include="WindowsShortcutFactory" Version="1.1.0" /> <PackageReference Include="WindowsShortcutFactory" Version="1.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
<ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" /> <ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" />
<ProjectReference Include="..\Distance\Distance.csproj" /> <ProjectReference Include="..\Distance\Distance.csproj" />
<ProjectReference Include="..\Face\Face.csproj" /> <ProjectReference Include="..\Face\Face.csproj" />
@ -61,6 +60,7 @@
<ProjectReference Include="..\Property\Property.csproj" /> <ProjectReference Include="..\Property\Property.csproj" />
<ProjectReference Include="..\Resize\Resize.csproj" /> <ProjectReference Include="..\Resize\Resize.csproj" />
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" /> <ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="appsettings.json"> <None Include="appsettings.json">

View File

@ -102,7 +102,7 @@ public class A_Property
return results; 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; Shared.Models.Property result;
byte[] bytes; byte[] bytes;
@ -114,10 +114,10 @@ public class A_Property
string? model = null; string? model = null;
string dateTimeFormat; string dateTimeFormat;
DateTime checkDateTime; DateTime checkDateTime;
string? blurHash = null;
DateTime? dateTime = null; DateTime? dateTime = null;
PropertyItem? propertyItem; PropertyItem? propertyItem;
string? orientation = null; string? orientation = null;
byte[]? thumbHashBytes = null;
DateTime? gpsDateStamp = null; DateTime? gpsDateStamp = null;
DateTime? dateTimeOriginal = null; DateTime? dateTimeOriginal = null;
DateTime? dateTimeDigitized = null; DateTime? dateTimeDigitized = null;
@ -134,6 +134,8 @@ public class A_Property
try try
{ {
using Image image = Image.FromFile(fileHolder.FullName); using Image image = Image.FromFile(fileHolder.FullName);
width = image.Width;
height = image.Height;
if (populateId && id is null) if (populateId && id is null)
{ {
using Bitmap bitmap = new(image); using Bitmap bitmap = new(image);
@ -151,18 +153,6 @@ public class A_Property
File.WriteAllBytes(Path.ChangeExtension(contentFileInfo.FullName, string.Empty), bytes); 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(); dateTimeFormat = Shared.Models.Stateless.Methods.IProperty.DateTimeFormat();
if (image.PropertyIdList.Contains((int)IExif.Tags.DateTime)) if (image.PropertyIdList.Contains((int)IExif.Tags.DateTime))
{ {
@ -252,16 +242,18 @@ public class A_Property
throw new NullReferenceException(nameof(fileHolder.CreationTime)); throw new NullReferenceException(nameof(fileHolder.CreationTime));
if (fileHolder.LastWriteTime is null && property?.LastWriteTime is null) if (fileHolder.LastWriteTime is null && property?.LastWriteTime is null)
throw new NullReferenceException(nameof(fileHolder.LastWriteTime)); 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) 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) 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 else
throw new NullReferenceException(nameof(property)); throw new NullReferenceException(nameof(property));
return result; 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; int? id = null;
bool populateId = true; bool populateId = true;
@ -273,13 +265,13 @@ public class A_Property
FileHolder fileHolder = new(fileName); FileHolder fileHolder = new(fileName);
bool isValidImageFormatExtension = true; bool isValidImageFormatExtension = true;
Shared.Models.Property? property = null; 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; return result;
} }
#pragma warning restore CA1416 #pragma warning restore CA1416
private Shared.Models.Property GetPropertyOfPrivate(Shared.Models.Methods.IBlurHasher? blurHasher, Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions, bool isIgnoreExtension, bool isValidMetadataExtensions) private Shared.Models.Property GetPropertyOfPrivate(Shared.Models.Methods.IThumbHasher? thumbHasher, Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions, bool isIgnoreExtension, bool isValidMetadataExtensions)
{ {
Shared.Models.Property? result; Shared.Models.Property? result;
int? id = null; int? id = null;
@ -367,19 +359,16 @@ public class A_Property
parseExceptions.Add(nameof(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\":"; string find = "\"CreationTime\":";
if (!json.Contains(find)) if (!json.Contains(find))
throw new NotImplementedException(); throw new NotImplementedException();
string blurHashDirectory = angleBracket.Replace("<>", "()"); byte[] thumbHashBytes = thumbHasher.Encode(item.ImageFileHolder.FullName);
#pragma warning disable CA1416 string thumbHashJson = JsonSerializer.Serialize(thumbHashBytes);
using Image image = Image.FromFile(item.ImageFileHolder.FullName); json = json.Replace(find, $"\"{nameof(result.ThumbHashBytes)}\": {thumbHashJson}, {find}");
#pragma warning restore CA1416
string blurHash = blurHasher.EncodeAndSave(image, blurHashDirectory);
json = json.Replace(find, $"\"{nameof(result.BlurHash)}\": \"{blurHash}\", {find}");
result = JsonSerializer.Deserialize<Shared.Models.Property>(json); result = JsonSerializer.Deserialize<Shared.Models.Property>(json);
if (result is null || result.BlurHash is null) if (result is null || result.ThumbHashBytes is null)
throw new NullReferenceException(nameof(result)); throw new NullReferenceException(nameof(result));
json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions); json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
File.WriteAllText(fileInfo.FullName, json); File.WriteAllText(fileInfo.FullName, json);
@ -388,7 +377,7 @@ public class A_Property
if (result is null) if (result is null)
{ {
id ??= item.ImageFileHolder.Id; 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); json = JsonSerializer.Serialize(result, _WriteIndentedJsonSerializerOptions);
if (populateId && Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches: true, compareBeforeWrite: true)) 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; return result;
} }
private void SavePropertyParallelForWork(Shared.Models.Methods.IBlurHasher? blurHasher, string sourceDirectory, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<Tuple<string, DateTime>> sourceDirectoryChanges, Item item) private void SavePropertyParallelForWork(Shared.Models.Methods.IThumbHasher? thumbHasher, string sourceDirectory, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<Tuple<string, DateTime>> sourceDirectoryChanges, Item item)
{ {
Shared.Models.Property property; Shared.Models.Property property;
List<string> parseExceptions = new(); List<string> parseExceptions = new();
@ -428,7 +417,7 @@ public class A_Property
File.Move(item.ImageFileHolder.FullName, filteredSourceDirectoryFileExtensionLowered); 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) 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) lock (sourceDirectoryChanges)
sourceDirectoryChanges.Add(new Tuple<string, DateTime>(nameof(A_Property), DateTime.Now)); sourceDirectoryChanges.Add(new Tuple<string, DateTime>(nameof(A_Property), DateTime.Now));
lock (item) lock (item)
@ -436,7 +425,7 @@ public class A_Property
} }
} }
private void SavePropertyParallelWork(int maxDegreeOfParallelism, Shared.Models.Methods.IBlurHasher? blurHasher, List<Exception> exceptions, List<Tuple<string, DateTime>> sourceDirectoryChanges, Container container, List<Item> items, string message) private void SavePropertyParallelWork(int maxDegreeOfParallelism, Shared.Models.Methods.IThumbHasher? thumbHasher, List<Exception> exceptions, List<Tuple<string, DateTime>> sourceDirectoryChanges, Container container, List<Item> items, string message)
{ {
List<Tuple<string, DateTime>> sourceDirectoryFileTuples = new(); List<Tuple<string, DateTime>> sourceDirectoryFileTuples = new();
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxDegreeOfParallelism }; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxDegreeOfParallelism };
@ -449,7 +438,7 @@ public class A_Property
long ticks = DateTime.Now.Ticks; long ticks = DateTime.Now.Ticks;
DateTime dateTime = DateTime.Now; DateTime dateTime = DateTime.Now;
List<Tuple<string, DateTime>> collection; List<Tuple<string, DateTime>> collection;
SavePropertyParallelForWork(blurHasher, container.SourceDirectory, sourceDirectoryChanges, sourceDirectoryFileTuples, items[i]); SavePropertyParallelForWork(thumbHasher, container.SourceDirectory, sourceDirectoryChanges, sourceDirectoryFileTuples, items[i]);
if (i == 0 || sourceDirectoryChanges.Any()) if (i == 0 || sourceDirectoryChanges.Any())
progressBar.Tick(); progressBar.Tick();
lock (sourceDirectoryFileTuples) lock (sourceDirectoryFileTuples)
@ -478,9 +467,6 @@ public class A_Property
singletonDescription: "Properties for each image", singletonDescription: "Properties for each image",
collectionDescription: string.Empty, collectionDescription: string.Empty,
converted: false)); converted: false));
string directory = _AngleBracketCollection[0].Replace("<>", "()");
if (!Directory.Exists(directory))
_ = Directory.CreateDirectory(directory);
} }
private void SetAngleBracketCollection(string sourceDirectory, bool anyNullOrNoIsUniqueFileName) private void SetAngleBracketCollection(string sourceDirectory, bool anyNullOrNoIsUniqueFileName)
@ -495,7 +481,7 @@ public class A_Property
SetAngleBracketCollection(aResultsFullGroupDirectory, sourceDirectory, anyNullOrNoIsUniqueFileName); 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) if (_Log is null)
throw new NullReferenceException(nameof(_Log)); throw new NullReferenceException(nameof(_Log));
@ -521,7 +507,7 @@ public class A_Property
SetAngleBracketCollection(container.SourceDirectory, anyNullOrNoIsUniqueFileName); SetAngleBracketCollection(container.SourceDirectory, anyNullOrNoIsUniqueFileName);
totalSeconds = (int)Math.Truncate(new TimeSpan(DateTime.Now.Ticks - ticks).TotalSeconds); 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}"; 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) foreach (Exception exception in exceptions)
_Log.Error(string.Concat(container.SourceDirectory, Environment.NewLine, exception.Message, Environment.NewLine, exception.StackTrace), exception); _Log.Error(string.Concat(container.SourceDirectory, Environment.NewLine, exception.Message, Environment.NewLine, exception.StackTrace), exception);
if (exceptions.Count == container.Items.Count) 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<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions) public Shared.Models.Property GetProperty(Shared.Models.Methods.IThumbHasher? thumbHasher, Item item, List<Tuple<string, DateTime>> sourceDirectoryFileTuples, List<string> parseExceptions)
{ {
Shared.Models.Property result; Shared.Models.Property result;
bool angleBracketCollectionAny = _AngleBracketCollection.Any(); bool angleBracketCollectionAny = _AngleBracketCollection.Any();
@ -554,7 +540,7 @@ public class A_Property
} }
bool isValidMetadataExtensions = _Configuration.ValidMetadataExtensions.Contains(item.ImageFileHolder.ExtensionLowered); bool isValidMetadataExtensions = _Configuration.ValidMetadataExtensions.Contains(item.ImageFileHolder.ExtensionLowered);
bool isIgnoreExtension = item.IsValidImageFormatExtension && _Configuration.IgnoreExtensions.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) if (!angleBracketCollectionAny)
_AngleBracketCollection.Clear(); _AngleBracketCollection.Clear();
return result; return result;

View File

@ -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);
}

View File

@ -3,7 +3,6 @@ namespace View_by_Distance.Shared.Models.Properties;
public interface IProperty public interface IProperty
{ {
public string? BlurHash { init; get; }
public DateTime CreationTime { init; get; } public DateTime CreationTime { init; get; }
public DateTime? DateTime { init; get; } public DateTime? DateTime { init; get; }
public DateTime? DateTimeDigitized { init; get; } public DateTime? DateTimeDigitized { init; get; }
@ -17,6 +16,7 @@ public interface IProperty
public string? Make { init; get; } public string? Make { init; get; }
public string? Model { init; get; } public string? Model { init; get; }
public string? Orientation { init; get; } public string? Orientation { init; get; }
public byte[]? ThumbHashBytes { init; get; }
public int? Width { init; get; } public int? Width { init; get; }
} }

View File

@ -6,7 +6,6 @@ namespace View_by_Distance.Shared.Models;
public class Property : Properties.IProperty public class Property : Properties.IProperty
{ {
public string? BlurHash { init; get; }
public DateTime CreationTime { init; get; } public DateTime CreationTime { init; get; }
public DateTime? DateTime { init; get; } public DateTime? DateTime { init; get; }
public DateTime? DateTimeDigitized { init; get; } public DateTime? DateTimeDigitized { init; get; }
@ -20,12 +19,12 @@ public class Property : Properties.IProperty
public string? Make { init; get; } public string? Make { init; get; }
public string? Model { init; get; } public string? Model { init; get; }
public string? Orientation { init; get; } public string? Orientation { init; get; }
public byte[]? ThumbHashBytes { init; get; }
public int? Width { init; get; } public int? Width { init; get; }
[JsonConstructor] [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; DateTimeFromName = dateTimeFromName;
CreationTime = creationTime; CreationTime = creationTime;
DateTime = dateTime; DateTime = dateTime;
@ -39,6 +38,7 @@ public class Property : Properties.IProperty
Make = make; Make = make;
Model = model; Model = model;
Orientation = orientation; Orientation = orientation;
ThumbHashBytes = thumbHashBytes;
Width = width; Width = width;
} }

View File

@ -41,11 +41,11 @@
<PackageReference Include="Serilog" Version="2.12.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" /> <ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
<ProjectReference Include="..\Property\Property.csproj" /> <ProjectReference Include="..\Property\Property.csproj" />
<ProjectReference Include="..\Metadata\Metadata.csproj" /> <ProjectReference Include="..\Metadata\Metadata.csproj" />
<ProjectReference Include="..\Resize\Resize.csproj" /> <ProjectReference Include="..\Resize\Resize.csproj" />
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\Instance\appsettings.json"> <None Include="..\Instance\appsettings.json">

View File

@ -168,8 +168,8 @@ public class UnitTestResize
Assert.IsNotNull(item.ImageFileHolder); Assert.IsNotNull(item.ImageFileHolder);
if (item.Property is null) if (item.Property is null)
{ {
Shared.Models.Methods.IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher(); Shared.Models.Methods.IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher();
property = propertyLogic.GetProperty(blurHasher, item, subFileTuples, parseExceptions); property = propertyLogic.GetProperty(thumbHasher, item, subFileTuples, parseExceptions);
item.Update(property); item.Update(property);
} }
if (property is null || item.Property is null) if (property is null || item.Property is null)

View File

@ -40,13 +40,13 @@
<PackageReference Include="Serilog" Version="2.12.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\BlurHash\BlurHash.csproj" />
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" /> <ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
<ProjectReference Include="..\Property\Property.csproj" /> <ProjectReference Include="..\Property\Property.csproj" />
<ProjectReference Include="..\Metadata\Metadata.csproj" /> <ProjectReference Include="..\Metadata\Metadata.csproj" />
<ProjectReference Include="..\Resize\Resize.csproj" /> <ProjectReference Include="..\Resize\Resize.csproj" />
<ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" /> <ProjectReference Include="..\FaceRecognitionDotNet\FaceRecognitionDotNet.csproj" />
<ProjectReference Include="..\Property-Compare\Property-Compare.csproj" /> <ProjectReference Include="..\Property-Compare\Property-Compare.csproj" />
<ProjectReference Include="..\ThumbHash\ThumbHash.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\Instance\appsettings.json"> <None Include="..\Instance\appsettings.json">

View File

@ -242,8 +242,8 @@ public class UnitTestFace
Assert.IsNotNull(item.ImageFileHolder); Assert.IsNotNull(item.ImageFileHolder);
if (item.Property is null) if (item.Property is null)
{ {
Shared.Models.Methods.IBlurHasher? blurHasher = new BlurHash.Models.BlurHasher(); Shared.Models.Methods.IThumbHasher? thumbHasher = new ThumbHash.Models.C2_ThumbHasher();
property = propertyLogic.GetProperty(blurHasher, item, subFileTuples, parseExceptions); property = propertyLogic.GetProperty(thumbHasher, item, subFileTuples, parseExceptions);
item.Update(property); item.Update(property);
} }
if (property is null || item.Property is null) if (property is null || item.Property is null)

1
ThumbHash/.vscode/format-report.json vendored Normal file
View File

@ -0,0 +1 @@
[]

View File

@ -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<T>
{
private static ArrayPool<T> DefaultPool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ArrayPool<T>.Shared;
}
private readonly T[] _Buffer;
private readonly int _Length;
public static SpanOwner<T> Empty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(0);
}
public Span<T> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
ref T r0 = ref MemoryMarshal.GetArrayDataReference(_Buffer);
return MemoryMarshal.CreateSpan(ref r0, _Length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SpanOwner<T> 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);
}

View File

@ -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<float> AC { init; get; }
public float Scale { init; get; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Channel(float dc, SpanOwner<float> ac, float scale)
{
DC = dc;
AC = ac;
Scale = scale;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Deconstruct(out float dc, out SpanOwner<float> ac, out float scale)
{
dc = DC;
ac = AC;
scale = Scale;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>(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>(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>(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}').");
/// <summary>
/// Encodes an RGBA image to a ThumbHash.
/// </summary>
/// <param name="width">The width of the input image. Must be ≤100px.</param>
/// <param name="height">The height of the input image. Must be ≤100px.</param>
/// <param name="rgba">The pixels in the input image, row-by-row. RGB should not be premultiplied by A. Must have `w*h*4` elements.</param>
/// <returns>Byte array containing the ThumbHash</returns>
public static byte[] RgbaToThumbHash(int width, int height, ReadOnlySpan<byte> rgba)
{
Span<byte> hash = stackalloc byte[_MaxHash];
int bytesWritten = RgbaToThumbHash(hash, width, height, rgba);
return hash[..bytesWritten].ToArray();
}
/// <summary>
/// Encodes an RGBA image to a ThumbHash.
/// </summary>
/// <param name="hash"></param>
/// <param name="w">The width of the input image. Must be ≤100px.</param>
/// <param name="h">The height of the input image. Must be ≤100px.</param>
/// <param name="rgba_bytes">The pixels in the input image, row-by-row. RGB should not be premultiplied by A. Must have `w*h*4` elements.</param>
/// <returns>Number of bytes written into hash span</returns>
public static int RgbaToThumbHash(Span<byte> hash, int w, int h, ReadOnlySpan<byte> 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> rgba = MemoryMarshal.Cast<byte, RGBA>(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<float> l_owner = new(w * h); // l: luminance
using SpanOwner<float> p_owner = new(w * h); // p: yellow - blue
using SpanOwner<float> q_owner = new(w * h); // q: red - green
using SpanOwner<float> a_owner = new(w * h); // a: alpha
Span<float> l = l_owner.Span;
Span<float> p = p_owner.Span;
Span<float> q = q_owner.Span;
Span<float> 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<float> channel, int nx, int ny)
{
float dc = 0.0f;
SpanOwner<float> ac_owner = new(nx * ny);
float scale = 0.0f;
Span<float> fx = stackalloc float[w];
Span<float> 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<float> l_ac, float l_scale) = encode_channel(l, Math.Max(lx, 3), Math.Max(ly, 3));
(float p_dc, SpanOwner<float> p_ac, float p_scale) = encode_channel(p, 3, 3);
(float q_dc, SpanOwner<float> q_ac, float q_scale) = encode_channel(q, 3, 3);
(float a_dc, SpanOwner<float> a_ac, float a_scale) = has_alpha ? encode_channel(a, 5, 5) : new Channel(1.0f, SpanOwner<float>.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<float> ac, ref bool is_odd, ref int hi, Span<byte> 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;
}
/// <summary>
/// Decodes a ThumbHash to an RGBA image.
/// </summary>
/// <returns>Width, height, and unpremultiplied RGBA8 pixels of the rendered ThumbHash.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the input is too short.</exception>
public static byte[] ThumbHashToRgba(ReadOnlySpan<byte> hash, int w, int h)
{
using SpanOwner<byte> rgba_owner = new(w * h * 4);
Span<byte> rgba = rgba_owner.Span;
ThumbHashToRgba(hash, w, h, rgba);
return rgba[..(w * h * 4)].ToArray();
}
/// <summary>
/// Decodes a ThumbHash to an RGBA image.
/// </summary>
/// <returns>Width, height, and unpremultiplied RGBA8 pixels of the rendered ThumbHash.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the input is too short.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the RGBA span length is less than `w * h * 4` bytes.</exception>
public static void ThumbHashToRgba(ReadOnlySpan<byte> hash, int w, int h, Span<byte> 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<float> decode_channel(ReadOnlySpan<byte> hash, int start, ref int index, int nx, int ny, float scale)
{
SpanOwner<float> ac_owner = new(nx * ny);
Span<float> 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<float> l_ac_owner = decode_channel(hash, ac_start, ref ac_index, lx, ly, l_scale);
using SpanOwner<float> p_ac_owner = decode_channel(hash, ac_start, ref ac_index, 3, 3, p_scale * 1.25f);
using SpanOwner<float> q_ac_owner = decode_channel(hash, ac_start, ref ac_index, 3, 3, q_scale * 1.25f);
using SpanOwner<float> a_ac_owner = has_alpha ? decode_channel(hash, ac_start, ref ac_index, 5, 5, a_scale) : SpanOwner<float>.Empty;
Span<float> l_ac = l_ac_owner.Span;
Span<float> p_ac = p_ac_owner.Span;
Span<float> q_ac = q_ac_owner.Span;
Span<float> a_ac = a_ac_owner.Span;
Span<float> fx = stackalloc float[7];
Span<float> fy = stackalloc float[7];
ref RGBA pixel = ref MemoryMarshal.AsRef<RGBA>(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));
}
}
}
/// <summary>
/// Extracts the average color from a ThumbHash.
/// </summary>
/// <returns>Unpremultiplied RGBA values where each value ranges from 0 to 1. </returns>
/// <exception cref="NotImplementedException">Thrown if the input is too short.</exception>
public static (float r, float g, float b, float a) ThumbHashToAverageRgba(ReadOnlySpan<byte> 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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<OutputType>library</OutputType>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<PackageId>Phares.View.by.Distance.ThumbHash</PackageId>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<Version>7.0.101.1</Version>
<Authors>Mike Phares</Authors>
<Company>Phares</Company>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup>
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
</PropertyGroup>
<PropertyGroup Condition="'$(IsWindows)'=='true'">
<DefineConstants>Windows</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(IsOSX)'=='true'">
<DefineConstants>OSX</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(IsLinux)'=='true'">
<DefineConstants>Linux</DefineConstants>
</PropertyGroup>
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'browser-wasm'">
<SupportedPlatform Include="browser" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="2.12.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shared\View-by-Distance.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestsWithFaceRecognitionDotNet", "TestsWithFaceRecognitionDotNet\TestsWithFaceRecognitionDotNet.csproj", "{A67D73C7-A1A1-4443-B681-776339CFA08A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestsWithFaceRecognitionDotNet", "TestsWithFaceRecognitionDotNet\TestsWithFaceRecognitionDotNet.csproj", "{A67D73C7-A1A1-4443-B681-776339CFA08A}"
EndProject 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "View-by-Distance.Shared", "shared\View-by-Distance.Shared.csproj", "{1D231660-33B4-4763-9C9F-C6ACC8BA600D}"
EndProject EndProject
Global Global
@ -172,5 +174,9 @@ Global
{9689371E-F67C-4392-A636-C398D28C089B}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{9689371E-F67C-4392-A636-C398D28C089B}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal