mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 10:16:30 +00:00
add db WIP 2, 80%
refactor, renames
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="ScottPlot.WPF" Version="5.0.46" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
|
||||
xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
|
||||
xmlns:datasetExplorer="clr-namespace:Azaion.Dataset"
|
||||
xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
|
||||
xmlns:controls1="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
|
||||
xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common"
|
||||
mc:Ignorable="d"
|
||||
Title="Переглядач анотацій" Height="900" Width="1200"
|
||||
WindowState="Maximized">
|
||||
|
||||
<Window.Resources>
|
||||
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type datasetExplorer:ThumbnailDto}">
|
||||
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationImageView}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
@@ -21,7 +20,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
Grid.Row="0"
|
||||
Source="{Binding Image}"
|
||||
Source="{Binding Thumbnail}"
|
||||
Width="480"
|
||||
Height="270"
|
||||
Margin="2" />
|
||||
@@ -47,11 +46,11 @@
|
||||
<ColumnDefinition Width="4"/>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<controls:AnnotationClasses
|
||||
<controls:DetectionClasses
|
||||
x:Name="LvClasses"
|
||||
Grid.Column="0"
|
||||
Grid.Row="0">
|
||||
</controls:AnnotationClasses>
|
||||
</controls:DetectionClasses>
|
||||
|
||||
<TabControl
|
||||
Name="Switcher"
|
||||
@@ -67,7 +66,7 @@
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Black"
|
||||
Margin="2,5,2,2"
|
||||
ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}"
|
||||
ItemsSource="{Binding SelectedAnnotations, Mode=OneWay}"
|
||||
ItemTemplate="{StaticResource ThumbnailTemplate}"
|
||||
>
|
||||
</vwp:GridView>
|
||||
@@ -109,22 +108,6 @@
|
||||
</Grid>
|
||||
</ItemsPanelTemplate>
|
||||
</StatusBar.ItemsPanel>
|
||||
<StatusBarItem Grid.Column="0" Background="Black">
|
||||
<TextBlock Name="LoadingAnnsCaption">Завантаження:</TextBlock>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem Grid.Column="1" Background="Black">
|
||||
<ProgressBar x:Name="LoadingAnnsBar"
|
||||
Width="150"
|
||||
Height="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="#252525"
|
||||
BorderBrush="#252525"
|
||||
Foreground="LightBlue"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Value="0">
|
||||
</ProgressBar>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem Grid.Column="2" Background="Black">
|
||||
<TextBlock Name="RefreshThumbCaption">База іконок:</TextBlock>
|
||||
</StatusBarItem>
|
||||
|
||||
@@ -4,8 +4,12 @@ using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.Services;
|
||||
using LinqToDB;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScottPlot;
|
||||
@@ -13,33 +17,40 @@ using Color = ScottPlot.Color;
|
||||
|
||||
namespace Azaion.Dataset;
|
||||
|
||||
public partial class DatasetExplorer
|
||||
public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEvent>
|
||||
{
|
||||
private readonly ILogger<DatasetExplorer> _logger;
|
||||
private readonly AnnotationConfig _annotationConfig;
|
||||
private readonly DirectoriesConfig _directoriesConfig;
|
||||
|
||||
public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new();
|
||||
private ObservableCollection<AnnotationClass> AllAnnotationClasses { get; set; } = new();
|
||||
private Dictionary<int, List<Annotation>> _annotationsDict;
|
||||
|
||||
public ObservableCollection<AnnotationImageView> SelectedAnnotations { get; set; } = new();
|
||||
private ObservableCollection<DetectionClass> AllAnnotationClasses { get; set; } = new();
|
||||
|
||||
public Dictionary<string, LabelInfo> LabelsCache { get; set; } = new();
|
||||
|
||||
private int _tempSelectedClassIdx = 0;
|
||||
private readonly IGalleryManager _galleryManager;
|
||||
private readonly IGalleryService _galleryService;
|
||||
private readonly IDbFactory _dbFactory;
|
||||
|
||||
public bool ThumbnailLoading { get; set; }
|
||||
|
||||
public ThumbnailDto? CurrentThumbnail { get; set; }
|
||||
public AnnotationImageView? CurrentAnnotation { get; set; }
|
||||
|
||||
public DatasetExplorer(
|
||||
IOptions<DirectoriesConfig> directoriesConfig,
|
||||
IOptions<AnnotationConfig> annotationConfig,
|
||||
ILogger<DatasetExplorer> logger,
|
||||
IGalleryManager galleryManager,
|
||||
FormState formState)
|
||||
IGalleryService galleryService,
|
||||
FormState formState,
|
||||
IDbFactory dbFactory)
|
||||
{
|
||||
_directoriesConfig = directoriesConfig.Value;
|
||||
_annotationConfig = annotationConfig.Value;
|
||||
_logger = logger;
|
||||
_galleryManager = galleryManager;
|
||||
_galleryService = galleryService;
|
||||
_dbFactory = dbFactory;
|
||||
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
@@ -61,11 +72,11 @@ public partial class DatasetExplorer
|
||||
|
||||
ThumbnailsView.SelectionChanged += (_, _) =>
|
||||
{
|
||||
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {ThumbnailsDtos.Count}";
|
||||
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}";
|
||||
};
|
||||
|
||||
ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentThumbnail!.ImagePath);
|
||||
galleryManager.ThumbnailsUpdate += thumbnailsPercentage =>
|
||||
ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentAnnotation!.Annotation.ImagePath);
|
||||
galleryService.ThumbnailsUpdate += thumbnailsPercentage =>
|
||||
{
|
||||
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
|
||||
};
|
||||
@@ -74,24 +85,22 @@ public partial class DatasetExplorer
|
||||
|
||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = Task.Run(async () => await _galleryManager.RefreshThumbnails());
|
||||
|
||||
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
|
||||
new List<AnnotationClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
|
||||
AllAnnotationClasses = new ObservableCollection<DetectionClass>(
|
||||
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
|
||||
.Concat(_annotationConfig.AnnotationClasses));
|
||||
LvClasses.ItemsSource = AllAnnotationClasses;
|
||||
|
||||
LvClasses.MouseUp += async (_, _) =>
|
||||
{
|
||||
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
|
||||
var selectedClass = (DetectionClass)LvClasses.SelectedItem;
|
||||
ExplorerEditor.CurrentAnnClass = selectedClass;
|
||||
_annotationConfig.LastSelectedExplorerClass = selectedClass.Id;
|
||||
|
||||
if (Switcher.SelectedIndex == 0)
|
||||
await ReloadThumbnails();
|
||||
else
|
||||
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
|
||||
ann.AnnotationClass = selectedClass;
|
||||
foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
|
||||
ann.DetectionClass = selectedClass;
|
||||
};
|
||||
|
||||
LvClasses.SelectionChanged += (_, _) =>
|
||||
@@ -99,35 +108,54 @@ public partial class DatasetExplorer
|
||||
if (Switcher.SelectedIndex != 1)
|
||||
return;
|
||||
|
||||
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
|
||||
var selectedClass = (DetectionClass)LvClasses.SelectedItem;
|
||||
if (selectedClass == null)
|
||||
return;
|
||||
|
||||
ExplorerEditor.CurrentAnnClass = selectedClass;
|
||||
|
||||
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
|
||||
ann.AnnotationClass = selectedClass;
|
||||
foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
|
||||
ann.DetectionClass = selectedClass;
|
||||
};
|
||||
|
||||
LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0;
|
||||
ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem;
|
||||
await ReloadThumbnails();
|
||||
LoadClassDistribution();
|
||||
ExplorerEditor.CurrentAnnClass = (DetectionClass)LvClasses.SelectedItem;
|
||||
|
||||
RefreshThumbBar.Value = _galleryManager.ThumbnailsPercentage;
|
||||
await _dbFactory.Run(async db =>
|
||||
{
|
||||
var allAnnotations = await db.Annotations
|
||||
.LoadWith(x => x.Detections)
|
||||
.OrderByDescending(x => x.CreatedDate)
|
||||
.ToListAsync();
|
||||
_annotationsDict = AllAnnotationClasses.ToDictionary(x => x.Id, _ => new List<Annotation>());
|
||||
|
||||
foreach (var annotation in allAnnotations)
|
||||
AddAnnotationToDict(annotation);
|
||||
});
|
||||
await ReloadThumbnails();
|
||||
await LoadClassDistribution();
|
||||
|
||||
RefreshThumbBar.Value = _galleryService.ProcessedThumbnailsPercentage;
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
private void LoadClassDistribution()
|
||||
private void AddAnnotationToDict(Annotation annotation)
|
||||
{
|
||||
var data = _galleryManager.LabelsCache
|
||||
foreach (var c in annotation.Classes)
|
||||
_annotationsDict[c].Add(annotation);
|
||||
_annotationsDict[-1].Add(annotation);
|
||||
}
|
||||
|
||||
private async Task LoadClassDistribution()
|
||||
{
|
||||
var data = LabelsCache
|
||||
.SelectMany(x => x.Value.Classes)
|
||||
.GroupBy(x => x)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Key,
|
||||
_annotationConfig.AnnotationClassesDict[x.Key].Name,
|
||||
_annotationConfig.AnnotationClassesDict[x.Key].Color,
|
||||
_annotationConfig.DetectionClassesDict[x.Key].Name,
|
||||
_annotationConfig.DetectionClassesDict[x.Key].Color,
|
||||
ClassCount = x.Count()
|
||||
})
|
||||
.ToList();
|
||||
@@ -167,8 +195,8 @@ public partial class DatasetExplorer
|
||||
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes)
|
||||
return;
|
||||
_galleryManager.ClearThumbnails();
|
||||
_galleryManager.RefreshThumbnails();
|
||||
_galleryService.ClearThumbnails();
|
||||
_galleryService.RefreshThumbnails();
|
||||
}
|
||||
|
||||
private async Task EditAnnotation()
|
||||
@@ -180,20 +208,20 @@ public partial class DatasetExplorer
|
||||
if (ThumbnailsView.SelectedItem == null)
|
||||
return;
|
||||
|
||||
var dto = (ThumbnailsView.SelectedItem as ThumbnailDto)!;
|
||||
CurrentThumbnail = dto;
|
||||
CurrentAnnotation = (ThumbnailsView.SelectedItem as AnnotationImageView)!;
|
||||
var ann = CurrentAnnotation.Annotation;
|
||||
ExplorerEditor.Background = new ImageBrush
|
||||
{
|
||||
ImageSource = await dto.ImagePath.OpenImage()
|
||||
ImageSource = await ann.ImagePath.OpenImage()
|
||||
};
|
||||
SwitchTab(toEditor: true);
|
||||
|
||||
var time = Constants.GetTime(dto.ImagePath);
|
||||
var time = Constants.GetTime(ann.ImagePath);
|
||||
ExplorerEditor.RemoveAllAnns();
|
||||
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
|
||||
foreach (var deetection in ann.Detections)
|
||||
{
|
||||
var annClass = _annotationConfig.AnnotationClassesDict[ann.ClassNumber];
|
||||
var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
|
||||
var annClass = _annotationConfig.DetectionClassesDict[deetection.ClassNumber];
|
||||
var canvasLabel = new CanvasLabel(deetection, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
|
||||
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
|
||||
}
|
||||
|
||||
@@ -243,101 +271,32 @@ public partial class DatasetExplorer
|
||||
var selected = ThumbnailsView.SelectedItems.Count;
|
||||
for (var i = 0; i < selected; i++)
|
||||
{
|
||||
var dto = (ThumbnailsView.SelectedItems[0] as ThumbnailDto)!;
|
||||
File.Delete(dto.ImagePath);
|
||||
File.Delete(dto.LabelPath);
|
||||
File.Delete(dto.ThumbnailPath);
|
||||
ThumbnailsDtos.Remove(dto);
|
||||
var dto = (ThumbnailsView.SelectedItems[0] as AnnotationImageView)!;
|
||||
dto.Delete();
|
||||
SelectedAnnotations.Remove(dto);
|
||||
}
|
||||
ThumbnailsView.SelectedIndex = Math.Min(ThumbnailsDtos.Count, tempSelected);
|
||||
ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
|
||||
}
|
||||
|
||||
private async Task ReloadThumbnails()
|
||||
{
|
||||
LoadingAnnsCaption.Visibility = Visibility.Visible;
|
||||
LoadingAnnsBar.Visibility = Visibility.Visible;
|
||||
SelectedAnnotations.Clear();
|
||||
foreach (var ann in _annotationsDict[ExplorerEditor.CurrentAnnClass.Id])
|
||||
SelectedAnnotations.Add(new AnnotationImageView(ann));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(_directoriesConfig.ThumbnailsDirectory))
|
||||
|
||||
private void AddThumbnail(Annotation annotation)
|
||||
{
|
||||
var selectedClass = ((DetectionClass?)LvClasses.SelectedItem)?.Id;
|
||||
if (selectedClass == null)
|
||||
return;
|
||||
|
||||
var thumbnails = Directory.GetFiles(_directoriesConfig.ThumbnailsDirectory, "*.jpg");
|
||||
var thumbnailDtos = new List<ThumbnailDto>();
|
||||
for (int i = 0; i < thumbnails.Length; i++)
|
||||
{
|
||||
var thumbnailDto = await GetThumbnail(thumbnails[i]);
|
||||
if (thumbnailDto != null)
|
||||
thumbnailDtos.Add(thumbnailDto);
|
||||
|
||||
if (i % 1000 == 0)
|
||||
LoadingAnnsBar.Value = i * 100.0 / thumbnails.Length;
|
||||
}
|
||||
|
||||
ThumbnailsDtos.Clear();
|
||||
foreach (var th in thumbnailDtos.OrderByDescending(x => x.ImageDate))
|
||||
ThumbnailsDtos.Add(th);
|
||||
|
||||
LoadingAnnsCaption.Visibility = Visibility.Collapsed;
|
||||
LoadingAnnsBar.Visibility = Visibility.Collapsed;
|
||||
AddAnnotationToDict(annotation);
|
||||
if (annotation.Classes.Contains(selectedClass.Value))
|
||||
SelectedAnnotations.Add(new AnnotationImageView(annotation));
|
||||
}
|
||||
|
||||
private async Task<ThumbnailDto?> GetThumbnail(string thumbnail)
|
||||
{
|
||||
try
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Constants.THUMBNAIL_PREFIX.Length];
|
||||
var imagePath = Path.Combine(_directoriesConfig.ImagesDirectory, name);
|
||||
var labelPath = Path.Combine(_directoriesConfig.LabelsDirectory, $"{name}.txt");
|
||||
|
||||
foreach (var f in _annotationConfig.ImageFormats)
|
||||
{
|
||||
var curName = $"{imagePath}.{f}";
|
||||
if (File.Exists(curName))
|
||||
{
|
||||
imagePath = curName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_galleryManager.LabelsCache.TryGetValue(Path.GetFileName(imagePath), out var info))
|
||||
{
|
||||
if (!File.Exists(imagePath) || !File.Exists(labelPath))
|
||||
{
|
||||
File.Delete(thumbnail);
|
||||
_logger.LogError($"No label {labelPath} found ! Image {imagePath} not found, thumbnail {thumbnail} was removed");
|
||||
return null;
|
||||
}
|
||||
|
||||
var classes = (await YoloLabel.ReadFromFile(labelPath))
|
||||
.Select(x => x.ClassNumber)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
info = _galleryManager.AddToCache(imagePath, classes);
|
||||
}
|
||||
|
||||
if (!info.Classes.Contains(ExplorerEditor.CurrentAnnClass.Id) && ExplorerEditor.CurrentAnnClass.Id != -1)
|
||||
return null;
|
||||
|
||||
return new ThumbnailDto
|
||||
{
|
||||
ThumbnailPath = thumbnail,
|
||||
ImagePath = imagePath,
|
||||
LabelPath = labelPath,
|
||||
ImageDate = info.ImageDateTime
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, e.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddThumbnail(ThumbnailDto thumbnailDto, IEnumerable<int> classes)
|
||||
{
|
||||
var selectedClass = ((AnnotationClass?)LvClasses.SelectedItem)?.Id;
|
||||
|
||||
if (selectedClass != null && (selectedClass == -1 || classes.Any(x => x == selectedClass)))
|
||||
ThumbnailsDtos.Insert(0, thumbnailDto);
|
||||
}
|
||||
public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) =>
|
||||
AddThumbnail(notification.Annotation);
|
||||
}
|
||||
@@ -2,15 +2,16 @@
|
||||
using System.Windows.Input;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.DTO.Queue;
|
||||
using Azaion.Common.Services;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Azaion.Dataset;
|
||||
|
||||
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalleryManager galleryManager, IOptions<DirectoriesConfig> directoriesConfig)
|
||||
:
|
||||
INotificationHandler<KeyEvent>,
|
||||
INotificationHandler<ImageCreatedEvent>
|
||||
public class DatasetExplorerEventHandler(
|
||||
DatasetExplorer datasetExplorer,
|
||||
AnnotationService annotationService) : INotificationHandler<KeyEvent>
|
||||
{
|
||||
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
|
||||
{
|
||||
@@ -48,14 +49,13 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle
|
||||
if (datasetExplorer.ThumbnailLoading)
|
||||
return;
|
||||
|
||||
var currentAnns = datasetExplorer.ExplorerEditor.CurrentAnns
|
||||
.Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize))
|
||||
.ToList();
|
||||
var fName = Path.GetFileNameWithoutExtension(datasetExplorer.CurrentAnnotation!.Annotation.ImagePath);
|
||||
var extension = Path.GetExtension(fName);
|
||||
|
||||
await YoloLabel.WriteToFile(currentAnns, Path.Combine(directoriesConfig.Value.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath));
|
||||
await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath);
|
||||
await galleryManager.SaveLabelsCache();
|
||||
datasetExplorer.CurrentThumbnail.UpdateImage();
|
||||
var detections = datasetExplorer.ExplorerEditor.CurrentDetections
|
||||
.Select(x => new Detection(fName, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize)))
|
||||
.ToList();
|
||||
await annotationService.SaveAnnotation(fName, extension, detections, SourceEnum.Manual);
|
||||
datasetExplorer.SwitchTab(toEditor: false);
|
||||
break;
|
||||
case PlaybackControlEnum.RemoveSelectedAnns:
|
||||
@@ -69,12 +69,4 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Handle(ImageCreatedEvent imageCreatedEvent, CancellationToken cancellationToken)
|
||||
{
|
||||
var (thumbnailDto, detections) = await galleryManager.CreateThumbnail(imageCreatedEvent.ImagePath, cancellationToken);
|
||||
if (thumbnailDto != null && detections != null)
|
||||
datasetExplorer.AddThumbnail(thumbnailDto, detections);
|
||||
await galleryManager.SaveLabelsCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Color = System.Drawing.Color;
|
||||
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
|
||||
using Size = System.Windows.Size;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Drawing.Drawing2D;
|
||||
using Azaion.Common.DTO.Config;
|
||||
|
||||
namespace Azaion.Dataset;
|
||||
|
||||
public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
|
||||
|
||||
public class GalleryManager(
|
||||
IOptions<DirectoriesConfig> directoriesConfig,
|
||||
IOptions<ThumbnailConfig> thumbnailConfig,
|
||||
IOptions<AnnotationConfig> annotationConfig,
|
||||
ILogger<GalleryManager> logger) : IGalleryManager
|
||||
{
|
||||
private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
|
||||
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
|
||||
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
|
||||
|
||||
private readonly string _thumbnailsCacheFile = Path.Combine(directoriesConfig.Value.ThumbnailsDirectory, Constants.THUMBNAILS_CACHE_FILE);
|
||||
public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
||||
|
||||
|
||||
private readonly SemaphoreSlim _updateLock = new(1);
|
||||
|
||||
public double ThumbnailsPercentage { get; set; }
|
||||
public ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; } = new();
|
||||
|
||||
private DirectoryInfo? _thumbnailsDirectory;
|
||||
|
||||
private DirectoryInfo ThumbnailsDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_thumbnailsDirectory != null)
|
||||
return _thumbnailsDirectory;
|
||||
|
||||
var dir = new DirectoryInfo(_dirConfig.ThumbnailsDirectory);
|
||||
if (!dir.Exists)
|
||||
Directory.CreateDirectory(_dirConfig.ThumbnailsDirectory);
|
||||
_thumbnailsDirectory = new DirectoryInfo(_dirConfig.ThumbnailsDirectory);
|
||||
return _thumbnailsDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearThumbnails()
|
||||
{
|
||||
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
|
||||
file.Delete();
|
||||
}
|
||||
|
||||
public async Task RefreshThumbnails()
|
||||
{
|
||||
await _updateLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
|
||||
|
||||
var thumbnails = ThumbnailsDirectory.GetFiles()
|
||||
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
|
||||
.GroupBy(x => x)
|
||||
.Select(gr => gr.Key)
|
||||
.ToHashSet();
|
||||
|
||||
if (File.Exists(_thumbnailsCacheFile))
|
||||
{
|
||||
var cache = JsonConvert.DeserializeObject<ConcurrentDictionary<string, LabelInfo>>(
|
||||
await File.ReadAllTextAsync(_thumbnailsCacheFile), new DenseDateTimeConverter());
|
||||
LabelsCache = cache ?? new ConcurrentDictionary<string, LabelInfo>();
|
||||
}
|
||||
else
|
||||
LabelsCache = new ConcurrentDictionary<string, LabelInfo>();
|
||||
|
||||
var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
|
||||
var imagesCount = files.Length;
|
||||
|
||||
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
||||
{
|
||||
var imgName = Path.GetFileNameWithoutExtension(file.Name);
|
||||
if (thumbnails.Contains(imgName))
|
||||
return;
|
||||
try
|
||||
{
|
||||
await CreateThumbnail(file.FullName, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}");
|
||||
}
|
||||
}, new ParallelOptions
|
||||
{
|
||||
ProgressFn = async num =>
|
||||
{
|
||||
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
|
||||
ThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
|
||||
ThumbnailsUpdate?.Invoke(ThumbnailsPercentage);
|
||||
await Task.CompletedTask;
|
||||
},
|
||||
CpuUtilPercent = 100,
|
||||
ProgressUpdateInterval = 200
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
await SaveLabelsCache();
|
||||
_updateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveLabelsCache()
|
||||
{
|
||||
var labelsCacheStr = JsonConvert.SerializeObject(LabelsCache, new DenseDateTimeConverter());
|
||||
await File.WriteAllTextAsync(_thumbnailsCacheFile, labelsCacheStr);
|
||||
}
|
||||
|
||||
public async Task<(ThumbnailDto? thumbnailDto, List<int>? classes)> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var width = (int)_thumbnailConfig.Size.Width;
|
||||
var height = (int)_thumbnailConfig.Size.Height;
|
||||
|
||||
var imgName = Path.GetFileName(imgPath);
|
||||
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt");
|
||||
|
||||
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken)));
|
||||
|
||||
var bitmap = new Bitmap(width, height);
|
||||
|
||||
using var g = Graphics.FromImage(bitmap);
|
||||
g.CompositingQuality = CompositingQuality.HighSpeed;
|
||||
g.SmoothingMode = SmoothingMode.HighSpeed;
|
||||
g.InterpolationMode = InterpolationMode.Default;
|
||||
|
||||
var size = new Size(originalImage.Width, originalImage.Height);
|
||||
if (!File.Exists(labelName))
|
||||
{
|
||||
File.Delete(imgPath);
|
||||
logger.LogInformation($"No labels found for image {imgName}! Image deleted!");
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken))
|
||||
.Select(x => new CanvasLabel(x, size, size))
|
||||
.ToList();
|
||||
var classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
|
||||
|
||||
AddToCache(imgPath, classes);
|
||||
|
||||
var thumbWhRatio = width / (float)height;
|
||||
var border = _thumbnailConfig.Border;
|
||||
|
||||
var frameX = 0.0;
|
||||
var frameY = 0.0;
|
||||
var frameHeight = size.Height;
|
||||
var frameWidth = size.Width;
|
||||
|
||||
if (labels.Any())
|
||||
{
|
||||
var labelsMinX = labels.Min(x => x.X);
|
||||
var labelsMaxX = labels.Max(x => x.X + x.Width);
|
||||
|
||||
var labelsMinY = labels.Min(x => x.Y);
|
||||
var labelsMaxY = labels.Max(x => x.Y + x.Height);
|
||||
|
||||
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
|
||||
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
|
||||
|
||||
if (labelsWidth / labelsHeight > thumbWhRatio)
|
||||
{
|
||||
frameWidth = labelsWidth;
|
||||
frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height);
|
||||
frameX = Math.Max(0, labelsMinX - border);
|
||||
frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border);
|
||||
}
|
||||
else
|
||||
{
|
||||
frameHeight = labelsHeight;
|
||||
frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width);
|
||||
frameY = Math.Max(0, labelsMinY - border);
|
||||
frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border);
|
||||
}
|
||||
}
|
||||
|
||||
var scale = frameHeight / height;
|
||||
g.DrawImage(originalImage, new Rectangle(0, 0, width, height), new RectangleF((float)frameX, (float)frameY, (float)frameWidth, (float)frameHeight), GraphicsUnit.Pixel);
|
||||
|
||||
foreach (var label in labels)
|
||||
{
|
||||
var color = _annotationConfig.AnnotationClassesDict[label.ClassNumber].Color;
|
||||
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
||||
|
||||
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
||||
g.FillRectangle(brush, rectangle);
|
||||
}
|
||||
|
||||
var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Constants.THUMBNAIL_PREFIX}.jpg");
|
||||
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
|
||||
|
||||
var thumbnailDto = new ThumbnailDto
|
||||
{
|
||||
ThumbnailPath = thumbnailName,
|
||||
ImagePath = imgPath,
|
||||
LabelPath = labelName,
|
||||
ImageDate = File.GetCreationTimeUtc(imgPath)
|
||||
};
|
||||
return (thumbnailDto, classes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public LabelInfo AddToCache(string imgPath, List<int> classes)
|
||||
{
|
||||
var labelInfo = new LabelInfo
|
||||
{
|
||||
Classes = classes,
|
||||
ImageDateTime = File.GetCreationTimeUtc(imgPath)
|
||||
};
|
||||
LabelsCache.TryAdd(Path.GetFileName(imgPath), labelInfo);
|
||||
|
||||
//Save to file only each 2 seconds
|
||||
_ = ThrottleExt.Throttle(async () => await SaveLabelsCache(), TimeSpan.FromSeconds(2));
|
||||
return labelInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IGalleryManager
|
||||
{
|
||||
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
||||
double ThumbnailsPercentage { get; set; }
|
||||
Task SaveLabelsCache();
|
||||
LabelInfo AddToCache(string imgPath, List<int> classes);
|
||||
ConcurrentDictionary<string, LabelInfo> LabelsCache { get; set; }
|
||||
Task<(ThumbnailDto? thumbnailDto, List<int>? classes)> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default);
|
||||
Task RefreshThumbnails();
|
||||
void ClearThumbnails();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Azaion.Dataset;
|
||||
|
||||
public class ThumbnailDto : INotifyPropertyChanged
|
||||
{
|
||||
public string ThumbnailPath { get; set; } = null!;
|
||||
public string ImagePath { get; set; } = null!;
|
||||
public string LabelPath { get; set; } = null!;
|
||||
public DateTime ImageDate { get; set; }
|
||||
|
||||
private BitmapImage? _image;
|
||||
public BitmapImage? Image
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_image == null)
|
||||
Task.Run(async () => Image = await ThumbnailPath.OpenImage());
|
||||
return _image;
|
||||
}
|
||||
set
|
||||
{
|
||||
_image = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public string ImageName => Path.GetFileName(ImagePath);
|
||||
|
||||
public void UpdateImage() => _image = null;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user