make thumbnail generating multithread

fix the bug with open video
add class distribution chart
This commit is contained in:
Alex Bezdieniezhnykh
2024-09-25 21:46:07 +03:00
parent 742f1ffee9
commit 22d4493d86
11 changed files with 215 additions and 50 deletions
+46
View File
@@ -0,0 +1,46 @@
using System.Diagnostics;
using Azaion.Annotator.Extensions;
using FluentAssertions;
using Xunit;
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
namespace Azaion.Annotator.Test;
public class ParallelExtTest
{
[Fact]
public async Task ParallelExtWorksOkTest()
{
var list = Enumerable.Range(0, 10).ToList();
var sw = Stopwatch.StartNew();
await ParallelExt.ForEachAsync(list, async (i, cancellationToken) =>
{
await Task.Delay(TimeSpan.FromSeconds(i), cancellationToken);
}, new ParallelOptions
{
CpuUtilPercent = 100,
ProgressUpdateInterval = 1
});
var elapsed = sw.Elapsed;
elapsed.Should().BeLessThan(TimeSpan.FromSeconds(11));
}
[Fact]
public async Task ParallelLibWorksOkTest()
{
var list = Enumerable.Range(0, 10).ToList();
var sw = Stopwatch.StartNew();
await Parallel.ForEachAsync(list, new System.Threading.Tasks.ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount,
}, async (i, cancellationToken) =>
{
await Task.Delay(TimeSpan.FromSeconds(i), cancellationToken);
});
var elapsed = sw.Elapsed;
elapsed.Should().BeLessThan(TimeSpan.FromSeconds(11));
}
}
+1
View File
@@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RangeTree" Version="3.0.1" /> <PackageReference Include="RangeTree" Version="3.0.1" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.39" />
<PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
+2 -2
View File
@@ -19,12 +19,12 @@ public class FormState
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = []; public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
public WindowsEnum ActiveWindow { get; set; } public WindowsEnum ActiveWindow { get; set; }
public string GetTimeName(TimeSpan ts) => $"{VideoName}_{ts:hmmssf}"; public string GetTimeName(TimeSpan? ts) => $"{VideoName}_{ts:hmmssf}";
public TimeSpan? GetTime(string name) public TimeSpan? GetTime(string name)
{ {
var timeStr = name.Split("_").LastOrDefault(); var timeStr = name.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 7) if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
return null; return null;
//For some reason, TimeSpan.ParseExact doesn't work on every platform. //For some reason, TimeSpan.ParseExact doesn't work on every platform.
+2 -2
View File
@@ -148,9 +148,9 @@ public class YoloLabel : Label
return res; return res;
} }
public static async Task<List<YoloLabel>> ReadFromFile(string filename) public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken = default)
{ {
var str = await File.ReadAllTextAsync(filename); var str = await File.ReadAllTextAsync(filename, cancellationToken);
return str.Split('\n') return str.Split('\n')
.Select(Parse) .Select(Parse)
+7 -4
View File
@@ -4,11 +4,11 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
xmlns:local="clr-namespace:Azaion.Annotator"
xmlns:dto="clr-namespace:Azaion.Annotator.DTO" xmlns:dto="clr-namespace:Azaion.Annotator.DTO"
xmlns:controls="clr-namespace:Azaion.Annotator.Controls" xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
xmlns:ScottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
mc:Ignorable="d" mc:Ignorable="d"
Title="Браузер анотацій" Height="900" Width="1200"> Title="Переглядач анотацій" Height="900" Width="1200">
<Window.Resources> <Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}"> <DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}">
@@ -58,7 +58,7 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Background="Black"> Background="Black">
<TabItem Header="Браузер"> <TabItem Header="Анотації">
<vwp:GridView <vwp:GridView
Name="ThumbnailsView" Name="ThumbnailsView"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@@ -70,12 +70,15 @@
> >
</vwp:GridView> </vwp:GridView>
</TabItem> </TabItem>
<TabItem Header="Перегляд"> <TabItem Header="Редактор">
<controls:CanvasEditor x:Name="ExplorerEditor" <controls:CanvasEditor x:Name="ExplorerEditor"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" > HorizontalAlignment="Stretch" >
</controls:CanvasEditor> </controls:CanvasEditor>
</TabItem> </TabItem>
<TabItem Header="Розподіл класів">
<ScottPlot:WpfPlot x:Name="ClassDistribution" />
</TabItem>
</TabControl> </TabControl>
<StatusBar <StatusBar
Grid.Row="1" Grid.Row="1"
+49 -12
View File
@@ -7,6 +7,8 @@ using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using ScottPlot;
using Color = ScottPlot.Color;
using MessageBox = System.Windows.MessageBox; using MessageBox = System.Windows.MessageBox;
namespace Azaion.Annotator; namespace Azaion.Annotator;
@@ -48,7 +50,6 @@ public partial class DatasetExplorer
} }
InitializeComponent(); InitializeComponent();
DataContext = this;
Loaded += async (_, _) => Loaded += async (_, _) =>
{ {
AllAnnotationClasses = new ObservableCollection<AnnotationClass>( AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
@@ -88,12 +89,14 @@ public partial class DatasetExplorer
LvClasses.SelectedIndex = config.LastSelectedExplorerClass ?? 0; LvClasses.SelectedIndex = config.LastSelectedExplorerClass ?? 0;
ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem; ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem;
await ReloadThumbnails(); await ReloadThumbnails();
LoadClassDistribution();
SizeChanged += async (_, _) => await SaveUserSettings(); SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings(); LocationChanged += async (_, _) => await SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings(); StateChanged += async (_, _) => await SaveUserSettings();
RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage; RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage;
DataContext = this;
}; };
Closing += (sender, args) => Closing += (sender, args) =>
@@ -125,18 +128,17 @@ public partial class DatasetExplorer
Switcher.SelectionChanged += (sender, args) => Switcher.SelectionChanged += (sender, args) =>
{ {
if (Switcher.SelectedIndex == 1) switch (Switcher.SelectedIndex)
{ {
//Editor case 0: //ListView
_tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _config.AnnotationClasses;
}
else
{
//Explorer
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.SelectedIndex = _tempSelectedClassIdx; LvClasses.SelectedIndex = _tempSelectedClassIdx;
ExplorerEditor.Background = null; ExplorerEditor.Background = null;
break;
case 1: //Editor
_tempSelectedClassIdx = LvClasses.SelectedIndex;
LvClasses.ItemsSource = _config.AnnotationClasses;
break;
} }
}; };
@@ -148,6 +150,41 @@ public partial class DatasetExplorer
} }
private void LoadClassDistribution()
{
var data = LabelsCache.SelectMany(x => x.Value)
.GroupBy(x => x)
.Select(x => new
{
x.Key,
_config.AnnotationClassesDict[x.Key].Name,
_config.AnnotationClassesDict[x.Key].Color,
ClassCount = x.Count()
})
.ToList();
var plot = ClassDistribution.Plot;
plot.Add.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
}));
foreach (var x in data)
{
var label = plot.Add.Text(x.Name, 50, -1.5 * x.Key + 1.1);
label.LabelFontSize = 16;
}
plot.Axes.AutoScale();
//plot.Axes.SetLimits(-200, data.Max(x => x.ClassCount + 3000), -2 * data.Count + 5, 5);
ClassDistribution.Background = new SolidColorBrush(System.Windows.Media.Colors.Black);
ClassDistribution.Refresh();
}
private async Task EditAnnotation() private async Task EditAnnotation()
{ {
try try
@@ -171,7 +208,7 @@ public partial class DatasetExplorer
ExplorerEditor.RemoveAllAnns(); ExplorerEditor.RemoveAllAnns();
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{ {
var annClass = _config.AnnotationClasses[ann.ClassNumber]; var annClass = _config.AnnotationClassesDict[ann.ClassNumber];
var annInfo = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); var annInfo = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, annInfo); ExplorerEditor.CreateAnnotation(annClass, time, annInfo);
} }
@@ -248,7 +285,7 @@ public partial class DatasetExplorer
await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache)); await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache));
} }
private async Task AddThumbnail(string thumbnail) private async Task AddThumbnail(string thumbnail, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -284,7 +321,7 @@ public partial class DatasetExplorer
return; return;
} }
var labels = await YoloLabel.ReadFromFile(labelPath); var labels = await YoloLabel.ReadFromFile(labelPath, cancellationToken);
classes = labels.Select(x => x.ClassNumber).Distinct().ToList(); classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
LabelsCache.Add(name, classes); LabelsCache.Add(name, classes);
} }
@@ -0,0 +1,67 @@
namespace Azaion.Annotator.Extensions;
public class ParallelOptions
{
public int ProgressUpdateInterval { get; set; } = 100;
public Func<int, Task> ProgressFn { get; set; } = null!;
public double CpuUtilPercent { get; set; } = 100;
}
public class ParallelExt
{
public static async Task ForEachAsync<T>(ICollection<T> source,
Func<T, CancellationToken, ValueTask> processFn,
ParallelOptions? parallelOptions = null,
CancellationToken cancellationToken = default)
{
parallelOptions ??= new ParallelOptions
{
CpuUtilPercent = 100,
ProgressUpdateInterval = 100,
ProgressFn = i =>
{
Console.WriteLine($"Processed {i} item by Task {Task.CurrentId}%");
return Task.CompletedTask;
}
};
var threadsCount = (int)(Environment.ProcessorCount * parallelOptions.CpuUtilPercent / 100.0);
var processedCount = 0;
var chunkSize = Math.Max(1, (int)(source.Count / (decimal)threadsCount));
var chunks = source.Chunk(chunkSize).ToList();
if (chunks.Count > threadsCount)
{
chunks[^2] = chunks[^2].Concat(chunks.Last()).ToArray();
chunks.RemoveAt(chunks.Count - 1);
}
var progressUpdateLock = new SemaphoreSlim(1);
var tasks = new List<Task>();
foreach (var chunk in chunks)
{
tasks.Add(await Task.Factory.StartNew(async () =>
{
foreach (var item in chunk)
{
await processFn(item, cancellationToken);
Interlocked.Increment(ref processedCount);
if (processedCount % parallelOptions.ProgressUpdateInterval == 0 && parallelOptions.ProgressFn != null)
_ = Task.Run(async () =>
{
await progressUpdateLock.WaitAsync(cancellationToken);
try
{
await parallelOptions.ProgressFn(processedCount);
}
finally
{
progressUpdateLock.Release();
}
}, cancellationToken);
}
}, TaskCreationOptions.LongRunning));
}
await Task.WhenAll(tasks);
}
}
+22 -17
View File
@@ -3,8 +3,10 @@ using System.Drawing.Drawing2D;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.IO; using System.IO;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Color = System.Drawing.Color; using Color = System.Drawing.Color;
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
using Size = System.Windows.Size; using Size = System.Windows.Size;
namespace Azaion.Annotator; namespace Azaion.Annotator;
@@ -14,7 +16,7 @@ public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGalleryManager public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGalleryManager
{ {
public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate; public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
private const int UPDATE_STEP = 20;
private readonly SemaphoreSlim _updateLock = new(1); private readonly SemaphoreSlim _updateLock = new(1);
public double ThumbnailsPercentage { get; set; } public double ThumbnailsPercentage { get; set; }
@@ -53,32 +55,35 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
.GroupBy(x => x) .GroupBy(x => x)
.Select(gr => gr.Key) .Select(gr => gr.Key)
.ToHashSet(); .ToHashSet();
var thumbnailsCount = thumbnails.Count;
var files = new DirectoryInfo(config.ImagesDirectory).GetFiles(); var files = new DirectoryInfo(config.ImagesDirectory).GetFiles();
var imagesCount = files.Length; var imagesCount = files.Length;
for (int i = 0; i < files.Length; i++) await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
{ {
var img = files[i]; var imgName = Path.GetFileNameWithoutExtension(file.Name);
ThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, i * 100 / (double)imagesCount);
var imgName = Path.GetFileNameWithoutExtension(img.Name);
if (i % UPDATE_STEP == 0)
ThumbnailsUpdate?.Invoke(ThumbnailsPercentage);
if (thumbnails.Contains(imgName)) if (thumbnails.Contains(imgName))
continue; return;
try try
{ {
await CreateThumbnail(img.FullName); await CreateThumbnail(file.FullName, cancellationToken);
thumbnailsCount++;
} }
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, $"Failed to generate thumbnail for {img.Name}! Error: {e.Message}"); logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}");
} }
} }, new ParallelOptions
{
await Task.Delay(10000); 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 finally
{ {
@@ -86,7 +91,7 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
} }
} }
public async Task CreateThumbnail(string imgPath) public async Task CreateThumbnail(string imgPath, CancellationToken cancellationToken = default)
{ {
var bitmap = await GenerateThumbnail(imgPath); var bitmap = await GenerateThumbnail(imgPath);
if (bitmap != null) if (bitmap != null)
@@ -178,7 +183,7 @@ public interface IGalleryManager
{ {
event ThumbnailsUpdatedEventHandler ThumbnailsUpdate; event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
double ThumbnailsPercentage { get; set; } double ThumbnailsPercentage { get; set; }
Task CreateThumbnail(string imgPath); Task CreateThumbnail(string imgPath, CancellationToken cancellationToken = default);
Task RefreshThumbnails(); Task RefreshThumbnails();
void ClearThumbnails(); void ClearThumbnails();
} }
+1 -1
View File
@@ -78,7 +78,7 @@
IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/> IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/>
<MenuItem x:Name="OpenDataExplorerItem" <MenuItem x:Name="OpenDataExplorerItem"
Foreground="Black" Foreground="Black"
IsEnabled="True" Header="Відкрити браузер анотацій..." Click="OpenDataExplorerItemClick"/> IsEnabled="True" Header="Відкрити переглядач анотацій..." Click="OpenDataExplorerItemClick"/>
<MenuItem x:Name="ReloadThumbnailsItem" <MenuItem x:Name="ReloadThumbnailsItem"
Foreground="Black" Foreground="Black"
IsEnabled="True" Header="Оновити базу іконок" Click="ReloadThumbnailsItemClick"/> IsEnabled="True" Header="Оновити базу іконок" Click="ReloadThumbnailsItemClick"/>
+13 -7
View File
@@ -248,25 +248,25 @@ public partial class MainWindow
foreach (var file in labelFiles) foreach (var file in labelFiles)
{ {
var name = Path.GetFileNameWithoutExtension(file.Name); var name = Path.GetFileNameWithoutExtension(file.Name);
var time = _formState.GetTime(name)!.Value; var time = _formState.GetTime(name);
await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName)); await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName));
} }
} }
public async Task AddAnnotation(TimeSpan time, List<YoloLabel> annotations) public async Task AddAnnotation(TimeSpan? time, List<YoloLabel> annotations)
{ {
var fName = _formState.GetTimeName(time); var fName = _formState.GetTimeName(time);
var previousAnnotations = Annotations.Query(time); var timeValue = time ?? TimeSpan.FromMinutes(0);
var previousAnnotations = Annotations.Query(timeValue);
Annotations.Remove(previousAnnotations); Annotations.Remove(previousAnnotations);
Annotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotations); Annotations.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), annotations);
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time); var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
if (existingResult != null) if (existingResult != null)
_formState.AnnotationResults.Remove(existingResult); _formState.AnnotationResults.Remove(existingResult);
_formState.AnnotationResults.Add(new AnnotationResult(time, fName, annotations, _config)); _formState.AnnotationResults.Add(new AnnotationResult(timeValue, fName, annotations, _config));
await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{fName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults)); await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{fName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults));
} }
@@ -277,7 +277,13 @@ public partial class MainWindow
return; return;
var labelNames = new DirectoryInfo(_config.LabelsDirectory).GetFiles() var labelNames = new DirectoryInfo(_config.LabelsDirectory).GetFiles()
.Select(x => x.Name[..^11]) .Select(x =>
{
var name = Path.GetFileNameWithoutExtension(x.Name);
return name.Length > 8
? name[..^7]
: name;
})
.GroupBy(x => x) .GroupBy(x => x)
.Select(gr => gr.Key) .Select(gr => gr.Key)
.ToDictionary(x => x); .ToDictionary(x => x);
+1 -1
View File
@@ -2,9 +2,9 @@
"VideosDirectory": "E:\\Azaion3\\Videos", "VideosDirectory": "E:\\Azaion3\\Videos",
"LabelsDirectory": "E:\\labels", "LabelsDirectory": "E:\\labels",
"ImagesDirectory": "E:\\images", "ImagesDirectory": "E:\\images",
"ThumbnailsDirectory": "E:\\thumbnails",
"ResultsDirectory": "E:\\results", "ResultsDirectory": "E:\\results",
"UnknownImages": "E:\\unknown", "UnknownImages": "E:\\unknown",
"ThumbnailsDirectory": "E:\\thumbnails",
"AnnotationClasses": [ "AnnotationClasses": [
{ "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" }, { "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" },
{ "Id": 1, "Name": "Вантажівка", "Color": "#4000FF00" }, { "Id": 1, "Name": "Вантажівка", "Color": "#4000FF00" },