rework to Azaion.Suite

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-21 13:41:32 +02:00
parent 2cf69f4e4e
commit 5a592e9dbf
76 changed files with 1739 additions and 882 deletions
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+34
View File
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<Page Update="DatasetExplorer.xaml">
<Generator>MSBuild:Compile</Generator>
<XamlRuntime>Wpf</XamlRuntime>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.46" />
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Drawing.Common">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\8.0.8\System.Drawing.Common.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+19
View File
@@ -0,0 +1,19 @@
using System.IO;
using System.Windows.Media.Imaging;
namespace Azaion.Dataset;
public static class BitmapExtensions
{
public static async Task<BitmapImage> OpenImage(this string imagePath)
{
var image = new BitmapImage();
await using var stream = File.OpenRead(imagePath);
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
return image;
}
}
+149
View File
@@ -0,0 +1,149 @@
<Window x:Class="Azaion.Dataset.DatasetExplorer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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"
mc:Ignorable="d"
Title="Переглядач анотацій" Height="900" Width="1200">
<Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type datasetExplorer:ThumbnailDto}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="32"></RowDefinition>
</Grid.RowDefinitions>
<Image
Grid.Row="0"
Source="{Binding Image}"
Width="480"
Height="270"
Margin="2" />
<TextBlock
Grid.Row="1"
Foreground="LightGray"
Text="{Binding ImageName}" />
</Grid>
</DataTemplate>
</Window.Resources>
<Grid
Name="MainGrid"
ShowGridLines="False"
Background="Black"
HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="32"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:AnnotationClasses
x:Name="LvClasses"
Grid.Column="0"
Grid.Row="0">
</controls:AnnotationClasses>
<TabControl
Name="Switcher"
Grid.Column="2"
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black">
<TabItem Name="AnnotationsTab" Header="Анотації">
<vwp:GridView
Name="ThumbnailsView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Black"
Margin="2,5,2,2"
ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}"
ItemTemplate="{StaticResource ThumbnailTemplate}"
>
</vwp:GridView>
</TabItem>
<TabItem Name="EditorTab"
Header="Редактор"
Visibility="Collapsed">
<controls:CanvasEditor x:Name="ExplorerEditor"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" >
</controls:CanvasEditor>
</TabItem>
<TabItem Name="ClassDistributionTab" Header="Розподіл класів">
<scottPlot:WpfPlot x:Name="ClassDistribution" />
</TabItem>
</TabControl>
<StatusBar
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Background="#252525"
Foreground="White"
>
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
</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>
<StatusBarItem Grid.Column="3" Background="Black">
<ProgressBar x:Name="RefreshThumbBar"
Width="150"
Height="15"
HorizontalAlignment="Stretch"
Background="#252525"
BorderBrush="#252525"
Foreground="LightBlue"
Maximum="100"
Minimum="0"
Value="0">
</ProgressBar>
</StatusBarItem>
<Separator Grid.Column="4"/>
<StatusBarItem Grid.Column="5" Background="Black">
<TextBlock Name="StatusText" Text=""/>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
+347
View File
@@ -0,0 +1,347 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Annotator.Extensions;
using Azaion.Common;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ScottPlot;
using Color = ScottPlot.Color;
namespace Azaion.Dataset;
public partial class DatasetExplorer
{
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 int _tempSelectedClassIdx = 0;
private readonly IGalleryManager _galleryManager;
public bool ThumbnailLoading { get; set; }
public ThumbnailDto? CurrentThumbnail { get; set; }
public DatasetExplorer(
IOptions<DirectoriesConfig> directoriesConfig,
IOptions<AnnotationConfig> annotationConfig,
ILogger<DatasetExplorer> logger,
IGalleryManager galleryManager)
{
_directoriesConfig = directoriesConfig.Value;
_annotationConfig = annotationConfig.Value;
_logger = logger;
_galleryManager = galleryManager;
InitializeComponent();
Loaded += async (_, _) =>
{
_ = Task.Run(async () => await _galleryManager.RefreshThumbnails());
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
new List<AnnotationClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.MouseUp += async (_, _) =>
{
var selectedClass = (AnnotationClass)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;
};
LvClasses.SelectionChanged += (_, _) =>
{
if (Switcher.SelectedIndex != 1)
return;
var selectedClass = (AnnotationClass)LvClasses.SelectedItem;
if (selectedClass == null)
return;
ExplorerEditor.CurrentAnnClass = selectedClass;
foreach (var ann in ExplorerEditor.CurrentAnns.Where(x => x.IsSelected))
ann.AnnotationClass = selectedClass;
};
LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0;
ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem;
await ReloadThumbnails();
LoadClassDistribution();
RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage;
DataContext = this;
};
Closing += (sender, args) =>
{
args.Cancel = true;
Visibility = Visibility.Hidden;
};
ThumbnailsView.KeyDown += async (sender, args) =>
{
switch (args.Key)
{
case Key.Delete:
DeleteAnnotations();
break;
case Key.Enter:
await EditAnnotation();
break;
}
};
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation();
ThumbnailsView.SelectionChanged += (_, _) =>
{
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {ThumbnailsDtos.Count}";
};
ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentThumbnail!.ImagePath);
galleryManager.ThumbnailsUpdate += thumbnailsPercentage =>
{
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
};
}
private void LoadClassDistribution()
{
var data = _galleryManager.LabelsCache
.SelectMany(x => x.Value.Classes)
.GroupBy(x => x)
.Select(x => new
{
x.Key,
_annotationConfig.AnnotationClassesDict[x.Key].Name,
_annotationConfig.AnnotationClassesDict[x.Key].Color,
ClassCount = x.Count()
})
.ToList();
var foregroundColor = Color.FromColor(System.Drawing.Color.Black);
var bars = data.Select(x => new Bar
{
Orientation = Orientation.Horizontal,
Position = -1.5 * x.Key + 1,
Label = x.ClassCount > 200 ? x.ClassCount.ToString() : "",
FillColor = new Color(x.Color.R, x.Color.G, x.Color.B, x.Color.A),
Value = x.ClassCount,
CenterLabel = true,
LabelOffset = 10
}).ToList();
ClassDistribution.Plot.Add.Bars(bars);
foreach (var x in data)
{
var label = ClassDistribution.Plot.Add.Text(x.Name, 50, -1.5 * x.Key + 1.1);
label.LabelFontColor = foregroundColor;
label.LabelFontSize = 18;
}
ClassDistribution.Plot.Axes.AutoScale();
ClassDistribution.Plot.HideAxesAndGrid();
ClassDistribution.Plot.FigureBackground.Color = new("#888888");
ClassDistribution.Refresh();
}
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e)
{
var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?",
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
_galleryManager.ClearThumbnails();
_galleryManager.RefreshThumbnails();
}
private async Task EditAnnotation()
{
try
{
ThumbnailLoading = true;
if (ThumbnailsView.SelectedItem == null)
return;
var dto = (ThumbnailsView.SelectedItem as ThumbnailDto)!;
CurrentThumbnail = dto;
ExplorerEditor.Background = new ImageBrush
{
ImageSource = await dto.ImagePath.OpenImage()
};
SwitchTab(toEditor: true);
var time = Constants.GetTime(dto.ImagePath);
ExplorerEditor.RemoveAllAnns();
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{
var annClass = _annotationConfig.AnnotationClassesDict[ann.ClassNumber];
var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
}
ThumbnailLoading = false;
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
finally
{
ThumbnailLoading = false;
}
}
public void SwitchTab(bool toEditor)
{
if (toEditor)
{
AnnotationsTab.Visibility = Visibility.Collapsed;
EditorTab.Visibility = Visibility.Visible;
_tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _annotationConfig.AnnotationClasses;
Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = Math.Max(0, _tempSelectedClassIdx - 1);
}
else
{
AnnotationsTab.Visibility = Visibility.Visible;
EditorTab.Visibility = Visibility.Collapsed;
LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.SelectedIndex = _tempSelectedClassIdx;
Switcher.SelectedIndex = 0;
}
}
private void DeleteAnnotations()
{
var tempSelected = ThumbnailsView.SelectedIndex;
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
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);
}
ThumbnailsView.SelectedIndex = Math.Min(ThumbnailsDtos.Count, tempSelected);
}
private async Task ReloadThumbnails()
{
LoadingAnnsCaption.Visibility = Visibility.Visible;
LoadingAnnsBar.Visibility = Visibility.Visible;
if (!Directory.Exists(_directoriesConfig.ThumbnailsDirectory))
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;
}
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);
}
}
@@ -0,0 +1,76 @@
using System.IO;
using System.Windows.Input;
using Azaion.Common.DTO;
using MediatR;
using Microsoft.Extensions.Options;
namespace Azaion.Dataset;
public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalleryManager galleryManager, IOptions<DirectoriesConfig> directoriesConfig)
:
INotificationHandler<KeyEvent>,
INotificationHandler<ImageCreatedEvent>
{
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.Escape, PlaybackControlEnum.Close }
};
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
{
if (keyEvent.WindowEnum != WindowEnum.Annotator)
return;
var key = keyEvent.Args.Key;
var keyNumber = (int?)null;
if ((int)key >= (int)Key.D1 && (int)key <= (int)Key.D9) keyNumber = key - Key.D1;
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue)
datasetExplorer.LvClasses.SelectedIndex = keyNumber.Value;
else
{
if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value))
await HandleControl(value);
}
}
private async Task HandleControl(PlaybackControlEnum controlEnum)
{
switch (controlEnum)
{
case PlaybackControlEnum.SaveAnnotations:
if (datasetExplorer.ThumbnailLoading)
return;
var currentAnns = datasetExplorer.ExplorerEditor.CurrentAnns
.Select(x => new YoloLabel(x.Info, datasetExplorer.ExplorerEditor.RenderSize, datasetExplorer.ExplorerEditor.RenderSize))
.ToList();
await YoloLabel.WriteToFile(currentAnns, Path.Combine(directoriesConfig.Value.LabelsDirectory, datasetExplorer.CurrentThumbnail!.LabelPath));
await galleryManager.CreateThumbnail(datasetExplorer.CurrentThumbnail.ImagePath);
await galleryManager.SaveLabelsCache();
datasetExplorer.CurrentThumbnail.UpdateImage();
datasetExplorer.SwitchTab(toEditor: false);
break;
case PlaybackControlEnum.RemoveSelectedAnns:
datasetExplorer.ExplorerEditor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
datasetExplorer.ExplorerEditor.RemoveAllAnns();
break;
case PlaybackControlEnum.Close:
datasetExplorer.SwitchTab(toEditor: false);
break;
}
}
public Task Handle(ImageCreatedEvent notification, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
+252
View File
@@ -0,0 +1,252 @@
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?> 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;
}
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);
return new ThumbnailDto
{
ThumbnailPath = thumbnailName,
ImagePath = imgPath,
LabelPath = labelName,
ImageDate = File.GetCreationTimeUtc(imgPath)
};
}
catch (Exception e)
{
logger.LogError(e, e.Message);
return 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?> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default);
Task RefreshThumbnails();
void ClearThumbnails();
}
+39
View File
@@ -0,0 +1,39 @@
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));
}
}