Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #347

Merged
merged 3 commits into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions Damselfly.Core.ImageProcessing/ImageMagickProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics;
using Damselfly.Core.Interfaces;
using Damselfly.Core.Utils;
using System.Threading.Tasks;
using Damselfly.Core.Models;
using Damselfly.Core.Utils.Images;

namespace Damselfly.Core.ImageProcessing
Expand Down Expand Up @@ -191,5 +185,10 @@ public Task GetCroppedFile(FileInfo source, int x, int y, int width, int height,
{
throw new NotImplementedException();
}

public Task CropImage(FileInfo source, int x, int y, int width, int height, Stream stream)
{
throw new NotImplementedException();
}
}
}
24 changes: 18 additions & 6 deletions Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Threading.Tasks;
using CoenM.ImageHash;
using CoenM.ImageHash.HashAlgorithms;
using Damselfly.Core.Interfaces;
using Damselfly.Core.Models;
using Damselfly.Core.Utils;
using Damselfly.Core.Utils.Constants;
using Damselfly.Core.Utils.Images;
Expand Down Expand Up @@ -217,6 +212,23 @@ public async Task GetCroppedFile( FileInfo source, int x, int y, int width, int
watch.Stop();
}

public async Task CropImage(FileInfo source, int x, int y, int width, int height, Stream stream)
{
Stopwatch watch = new Stopwatch("ImageSharpCrop");

// Image.Load(string path) is a shortcut for our default type.
// Other pixel formats use Image.Load<TPixel>(string path))
using var image = Image.Load<Rgba32>(source.FullName);

var rect = new Rectangle(x, y, width, height);
image.Mutate(x => x.AutoOrient());
image.Mutate(x => x.Crop(rect));

await image.SaveAsJpegAsync(stream);

watch.Stop();
}

/// <summary>
/// Transforms an image to add a watermark.
/// </summary>
Expand Down
24 changes: 20 additions & 4 deletions Damselfly.Core.ImageProcessing/SkiaSharpProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public Task<ImageProcessResult> CreateThumbs(FileInfo source, IDictionary<FileIn
/// <param name="height"></param>
/// <param name="dest"></param>
/// <returns></returns>
public Task GetCroppedFile(FileInfo source, int x, int y, int width, int height, FileInfo dest)
public Task CropImage(FileInfo source, int x, int y, int width, int height, Stream destStream)
{
Stopwatch watch = new Stopwatch("SkiaSharpCrop");

Expand All @@ -162,9 +162,7 @@ public Task GetCroppedFile(FileInfo source, int x, int y, int width, int height,
var cropRect = new SKRectI(x, y, x + width, y + height);
var cropped = Crop(sourceBitmap, cropRect);
using SKData data = cropped.Encode(SKEncodedImageFormat.Jpeg, 90);

using (var stream = new FileStream(dest.FullName, FileMode.Create, FileAccess.Write))
data.SaveTo(stream);
data.SaveTo(destStream);
}
catch (Exception ex)
{
Expand All @@ -179,6 +177,24 @@ public Task GetCroppedFile(FileInfo source, int x, int y, int width, int height,
return Task.CompletedTask;
}

/// <summary>
/// Crops a file, saving it to disk
/// </summary>
/// <param name="source"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="dest"></param>
/// <returns></returns>
public async Task GetCroppedFile(FileInfo source, int x, int y, int width, int height, FileInfo dest)
{
using (var stream = new FileStream(dest.FullName, FileMode.Create, FileAccess.Write))
{
await CropImage(source, x, y, width, height, stream);
}
}

/// <summary>
/// Crop the image to fit within the dimensions specified.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions Damselfly.Core.Interfaces/IImageProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public interface IImageProcessor
{
Task<ImageProcessResult> CreateThumbs(FileInfo source, IDictionary<FileInfo, ThumbConfig> destFiles );
Task GetCroppedFile(FileInfo source, int x, int y, int width, int height, FileInfo destFile);
Task CropImage(FileInfo path, int x, int y, int width, int height, Stream stream);
Task TransformDownloadImage(string input, Stream output, IExportSettings exportConfig);

static ICollection<string> SupportedFileExtensions { get; }
Expand Down
2 changes: 1 addition & 1 deletion Damselfly.Core/Damselfly.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="6.0.5.128" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="6.0.2" />
<PackageReference Include="CoenM.ImageSharp.ImageHash" Version="1.2.30" />
<PackageReference Include="MudBlazor" Version="6.0.6" />
<PackageReference Include="MudBlazor" Version="6.0.7" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\" />
Expand Down
12 changes: 6 additions & 6 deletions Damselfly.Core/Services/ExifService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public class ExifService : IProcessJobFactory
public List<Tag> FavouriteTags { get; private set; } = new List<Tag>();
public event Action OnFavouritesChanged;
public event Action<List<string>> OnUserTagsAdded;
private const int s_exifWriteDelay = 15;

private void NotifyFavouritesChanged()
{
Expand Down Expand Up @@ -153,7 +152,7 @@ public async Task UpdateFaceDataAsync(Image[] images, List<ImageObject> faces, A
}

// Trigger the work service to look for new jobs
_workService.HandleNewJobs(this, s_exifWriteDelay);
_workService.FlagNewJobs(this);
}


Expand Down Expand Up @@ -233,7 +232,7 @@ public async Task UpdateTagsAsync(Image[] images, List<string> addTags, List<str
NotifyUserTagsAdded(addTags);

// Trigger the work service to look for new jobs
_workService.HandleNewJobs(this, s_exifWriteDelay);
_workService.FlagNewJobs(this);
}

/// <summary>
Expand Down Expand Up @@ -278,7 +277,7 @@ public async Task SetExifFieldAsync(Image[] images, ExifOperation.ExifType exifT
}

// Trigger the work service to look for new jobs
_workService.HandleNewJobs(this, s_exifWriteDelay);
_workService.FlagNewJobs(this);
}

/// <summary>
Expand Down Expand Up @@ -671,6 +670,7 @@ public class ExifProcess : IProcessJob
public ExifService Service { get; set; }
public bool CanProcess => true;
public string Description => "Writing Metadata";
public DateTime ProcessSchedule { get; set; }
public JobPriorities Priority => JobPriorities.ExifService;

public async Task Process()
Expand All @@ -686,7 +686,7 @@ public async Task<ICollection<IProcessJob>> GetPendingJobs(int maxCount)
using var db = new ImageContext();

// We skip any operations where the timestamp is more recent than 30s
var timeThreshold = DateTime.UtcNow.AddSeconds(-1 * s_exifWriteDelay);
var timeThreshold = DateTime.UtcNow.AddSeconds(-1 * 30);

// Find all the operations that are pending, and the timestamp is older than the threshold.
var opsToProcess = await db.KeywordOperations.AsQueryable()
Expand All @@ -702,7 +702,7 @@ public async Task<ICollection<IProcessJob>> GetPendingJobs(int maxCount)
{
ImageId = x.Key,
ExifOps = x.Value,
Service = this
Service = this,
}).ToArray();

return jobs;
Expand Down
11 changes: 8 additions & 3 deletions Damselfly.Core/Services/ImageProcessService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
using System.IO;
using System.Linq;
using System.Collections.Generic;
using Damselfly.Core.Interfaces;
using System.Threading.Tasks;
using Damselfly.Core.Models;
using Damselfly.Core.Utils;
using System;
using Damselfly.Core.Utils.Images;

namespace Damselfly.Core.Services
Expand Down Expand Up @@ -109,5 +106,13 @@ public async Task GetCroppedFile(FileInfo source, int x, int y, int width, int h
if (processor != null)
await processor.GetCroppedFile(source, x, y, width, height, destFile);
}

public async Task CropImage(FileInfo path, int x, int y, int width, int height, Stream stream)
{
var processor = _factory.GetProcessor(path.Extension);

if (processor != null)
await processor.CropImage(path, x, y, width, height, stream);
}
}
}
98 changes: 62 additions & 36 deletions Damselfly.Core/Services/ImageRecognitionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ public class ImageRecognitionService : IProcessJobFactory
private readonly WorkService _workService;
private readonly ExifService _exifService;
private readonly ImageCache _imageCache;
private readonly ImageProcessService _imageProcessor;

private IDictionary<string, Person> _peopleCache;
private IDictionary<string, Person> _peopleCache = null;

public static bool EnableImageRecognition { get; set; } = true;

Expand All @@ -45,7 +46,8 @@ public ImageRecognitionService(StatusService statusService, ObjectDetector objec
AccordFaceService accordFace, EmguFaceService emguService,
ThumbnailService thumbs, ConfigService configService,
ImageClassifier imageClassifier, ImageCache imageCache,
WorkService workService, ExifService exifService)
WorkService workService, ExifService exifService,
ImageProcessService imageProcessor)
{
_thumbService = thumbs;
_accordFaceService = accordFace;
Expand All @@ -56,6 +58,7 @@ public ImageRecognitionService(StatusService statusService, ObjectDetector objec
_emguFaceService = emguService;
_configService = configService;
_imageClassifier = imageClassifier;
_imageProcessor = imageProcessor;
_imageCache = imageCache;
_workService = workService;
_exifService = exifService;
Expand All @@ -67,7 +70,9 @@ public ImageRecognitionService()

public List<Person> GetCachedPeople()
{
return _peopleCache.Values.OrderBy(x => x.Name).ToList();
LoadPersonCache();

return _peopleCache.Values.OrderBy(x => x?.Name).ToList();
}

/// <summary>
Expand All @@ -78,22 +83,31 @@ private void LoadPersonCache(bool force = false)
{
try
{
if (_peopleCache == null || force)
{
var watch = new Stopwatch("LoadPersonCache");
if (_peopleCache == null )
_peopleCache = new ConcurrentDictionary<string, Person>();

using var db = new ImageContext();
if (force)
_peopleCache.Clear();

var watch = new Stopwatch("LoadPersonCache");

using var db = new ImageContext();

var dict = db.People.Where(x => !string.IsNullOrEmpty(x.AzurePersonId))
.AsNoTracking()
.Select(p => new { p.AzurePersonId, Person = p } )
.ToList();

// Pre-cache tags from DB.
_peopleCache = new ConcurrentDictionary<string, Person>(db.People
.Where(x => !string.IsNullOrEmpty(x.AzurePersonId))
.AsNoTracking()
.ToDictionary(k => k.AzurePersonId, v => v));
if (_peopleCache.Any())
Logging.LogTrace("Pre-loaded cach with {0} people.", _peopleCache.Count());
if (dict.Any())
{
// Merge the items into the people cache. Note that we use
// the indexer to avoid dupe key issues. TODO: Should the table be unique?
dict.ToList().ForEach(x => _peopleCache[x.AzurePersonId] = x.Person);

watch.Stop();
Logging.LogTrace("Pre-loaded cach with {0} people.", _peopleCache.Count());
}

watch.Stop();
}
catch (Exception ex)
{
Expand Down Expand Up @@ -149,34 +163,46 @@ public async Task UpdateName( Person person, string name )
/// </summary>
/// <param name="personIdsToAdd"></param>
/// <returns></returns>
public async Task<List<Person>> CreateMissingPeople(IEnumerable<string> personIdsToAdd)
public async Task CreateMissingPeople(IEnumerable<string> personIdsToAdd)
{
using ImageContext db = new ImageContext();

// Find the people that aren't already in the cache and add new ones
var newPeople = personIdsToAdd.Where(x => !_peopleCache.ContainsKey(x))
.Select(x => new Person
try
{
if (personIdsToAdd != null && personIdsToAdd.Any())
{
// Find the people that aren't already in the cache and add new ones
// Be careful - filter out empty ones (shouldn't ever happen, but belt
// and braces
var newNames = personIdsToAdd.Select( x => x.Trim() )
.Where( x => !string.IsNullOrEmpty(x) && !_peopleCache.ContainsKey( x ) )
.ToList();

if (newNames.Any())
{
Logging.Log($"Adding {newNames.Count()} person records.");

var newPeople = newNames.Select(x => new Person
{
AzurePersonId = x,
Name = "Unknown",
State = Person.PersonState.Unknown
}
).ToList();
}).ToList();

if (newPeople.Any())
{
await db.BulkInsert(db.People, newPeople);

if (newPeople.Any())
// Add or replace the new people in the cache (this should always add)
newPeople.ForEach(x => _peopleCache[x.AzurePersonId] = x);
}
}
}
}
catch( Exception ex )
{

Logging.LogTrace("Adding {0} people", newPeople.Count());

await db.BulkInsert(db.People, newPeople);

// Add the new items to the cache.
newPeople.ForEach(x => _peopleCache[x.AzurePersonId] = x);
Logging.LogError($"Exception in CreateMissingPeople: {ex.Message}");
}

var allTags = personIdsToAdd.Select(x => _peopleCache[x]).ToList();
return allTags;
}

/// <summary>
Expand Down Expand Up @@ -429,7 +455,7 @@ private async Task DetectObjects(ImageMetaData metadata)
Logging.LogVerbose($"Processing {medThumb.FullName} with Azure Face Service");

// We got predictions or we're scanning everything - so now let's try the image with Azure.
var azureFaces = await _azureFaceService.DetectFaces(bitmap);
var azureFaces = await _azureFaceService.DetectFaces(medThumb.FullName, _imageProcessor);

azurewatch.Stop();

Expand All @@ -441,7 +467,7 @@ private async Task DetectObjects(ImageMetaData metadata)
var peopleIds = azureFaces.Select(x => x.PersonId.ToString());

// Create any new ones, or pull existing ones back from the cache
var people = await CreateMissingPeople(peopleIds);
await CreateMissingPeople(peopleIds);

var newObjects = azureFaces.Select(x => new ImageObject
{
Expand Down Expand Up @@ -625,7 +651,7 @@ public async Task MarkFolderForScan(Folder folder)
int updated = await db.BatchUpdate(queryable, x => new ImageMetaData { AILastUpdated = null });
_statusService.StatusText = $"Folder {folder.Name} ({updated} images) flagged for AI reprocessing.";

_workService.HandleNewJobs(this);
_workService.FlagNewJobs(this);
}

public async Task MarkAllImagesForScan()
Expand All @@ -636,7 +662,7 @@ public async Task MarkAllImagesForScan()

_statusService.StatusText = $"All {updated} images flagged for AI reprocessing.";

_workService.HandleNewJobs(this);
_workService.FlagNewJobs(this);
}

public async Task MarkImagesForScan(ICollection<Image> images)
Expand Down
Loading