mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 09:56:31 +00:00
make thumbnail generating multithread
fix the bug with open video add class distribution chart
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<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.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
|
||||
@@ -19,12 +19,12 @@ public class FormState
|
||||
public ObservableCollection<AnnotationResult> AnnotationResults { 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)
|
||||
{
|
||||
var timeStr = name.Split("_").LastOrDefault();
|
||||
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 7)
|
||||
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
|
||||
return null;
|
||||
|
||||
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
|
||||
|
||||
@@ -148,9 +148,9 @@ public class YoloLabel : Label
|
||||
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')
|
||||
.Select(Parse)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
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:local="clr-namespace:Azaion.Annotator"
|
||||
xmlns:dto="clr-namespace:Azaion.Annotator.DTO"
|
||||
xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
|
||||
xmlns:ScottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF"
|
||||
mc:Ignorable="d"
|
||||
Title="Браузер анотацій" Height="900" Width="1200">
|
||||
Title="Переглядач анотацій" Height="900" Width="1200">
|
||||
|
||||
<Window.Resources>
|
||||
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}">
|
||||
@@ -58,7 +58,7 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Black">
|
||||
<TabItem Header="Браузер">
|
||||
<TabItem Header="Анотації">
|
||||
<vwp:GridView
|
||||
Name="ThumbnailsView"
|
||||
HorizontalAlignment="Stretch"
|
||||
@@ -70,12 +70,15 @@
|
||||
>
|
||||
</vwp:GridView>
|
||||
</TabItem>
|
||||
<TabItem Header="Перегляд">
|
||||
<TabItem Header="Редактор">
|
||||
<controls:CanvasEditor x:Name="ExplorerEditor"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" >
|
||||
</controls:CanvasEditor>
|
||||
</TabItem>
|
||||
<TabItem Header="Розподіл класів">
|
||||
<ScottPlot:WpfPlot x:Name="ClassDistribution" />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
<StatusBar
|
||||
Grid.Row="1"
|
||||
|
||||
@@ -7,6 +7,8 @@ using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using ScottPlot;
|
||||
using Color = ScottPlot.Color;
|
||||
using MessageBox = System.Windows.MessageBox;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
@@ -48,7 +50,6 @@ public partial class DatasetExplorer
|
||||
}
|
||||
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
Loaded += async (_, _) =>
|
||||
{
|
||||
AllAnnotationClasses = new ObservableCollection<AnnotationClass>(
|
||||
@@ -88,12 +89,14 @@ public partial class DatasetExplorer
|
||||
LvClasses.SelectedIndex = config.LastSelectedExplorerClass ?? 0;
|
||||
ExplorerEditor.CurrentAnnClass = (AnnotationClass)LvClasses.SelectedItem;
|
||||
await ReloadThumbnails();
|
||||
LoadClassDistribution();
|
||||
|
||||
SizeChanged += async (_, _) => await SaveUserSettings();
|
||||
LocationChanged += async (_, _) => await SaveUserSettings();
|
||||
StateChanged += async (_, _) => await SaveUserSettings();
|
||||
|
||||
RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage;
|
||||
DataContext = this;
|
||||
};
|
||||
|
||||
Closing += (sender, args) =>
|
||||
@@ -125,18 +128,17 @@ public partial class DatasetExplorer
|
||||
|
||||
Switcher.SelectionChanged += (sender, args) =>
|
||||
{
|
||||
if (Switcher.SelectedIndex == 1)
|
||||
switch (Switcher.SelectedIndex)
|
||||
{
|
||||
//Editor
|
||||
_tempSelectedClassIdx = LvClasses.SelectedIndex;
|
||||
LvClasses.ItemsSource = _config.AnnotationClasses;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Explorer
|
||||
case 0: //ListView
|
||||
LvClasses.ItemsSource = AllAnnotationClasses;
|
||||
LvClasses.SelectedIndex = _tempSelectedClassIdx;
|
||||
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()
|
||||
{
|
||||
try
|
||||
@@ -171,7 +208,7 @@ public partial class DatasetExplorer
|
||||
ExplorerEditor.RemoveAllAnns();
|
||||
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);
|
||||
ExplorerEditor.CreateAnnotation(annClass, time, annInfo);
|
||||
}
|
||||
@@ -248,7 +285,7 @@ public partial class DatasetExplorer
|
||||
await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache));
|
||||
}
|
||||
|
||||
private async Task AddThumbnail(string thumbnail)
|
||||
private async Task AddThumbnail(string thumbnail, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -284,7 +321,7 @@ public partial class DatasetExplorer
|
||||
return;
|
||||
}
|
||||
|
||||
var labels = await YoloLabel.ReadFromFile(labelPath);
|
||||
var labels = await YoloLabel.ReadFromFile(labelPath, cancellationToken);
|
||||
classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using Azaion.Annotator.DTO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Color = System.Drawing.Color;
|
||||
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
|
||||
using Size = System.Windows.Size;
|
||||
|
||||
namespace Azaion.Annotator;
|
||||
@@ -14,7 +16,7 @@ public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
|
||||
public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGalleryManager
|
||||
{
|
||||
public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
|
||||
private const int UPDATE_STEP = 20;
|
||||
|
||||
private readonly SemaphoreSlim _updateLock = new(1);
|
||||
|
||||
public double ThumbnailsPercentage { get; set; }
|
||||
@@ -53,32 +55,35 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
|
||||
.GroupBy(x => x)
|
||||
.Select(gr => gr.Key)
|
||||
.ToHashSet();
|
||||
var thumbnailsCount = thumbnails.Count;
|
||||
|
||||
var files = new DirectoryInfo(config.ImagesDirectory).GetFiles();
|
||||
var imagesCount = files.Length;
|
||||
|
||||
for (int i = 0; i < files.Length; i++)
|
||||
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
||||
{
|
||||
var img = files[i];
|
||||
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);
|
||||
var imgName = Path.GetFileNameWithoutExtension(file.Name);
|
||||
if (thumbnails.Contains(imgName))
|
||||
continue;
|
||||
return;
|
||||
try
|
||||
{
|
||||
await CreateThumbnail(img.FullName);
|
||||
thumbnailsCount++;
|
||||
await CreateThumbnail(file.FullName, cancellationToken);
|
||||
}
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(10000);
|
||||
}, 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
|
||||
{
|
||||
@@ -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);
|
||||
if (bitmap != null)
|
||||
@@ -178,7 +183,7 @@ public interface IGalleryManager
|
||||
{
|
||||
event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
|
||||
double ThumbnailsPercentage { get; set; }
|
||||
Task CreateThumbnail(string imgPath);
|
||||
Task CreateThumbnail(string imgPath, CancellationToken cancellationToken = default);
|
||||
Task RefreshThumbnails();
|
||||
void ClearThumbnails();
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
IsEnabled="True" Header="Відкрити папку..." Click="OpenFolderItemClick"/>
|
||||
<MenuItem x:Name="OpenDataExplorerItem"
|
||||
Foreground="Black"
|
||||
IsEnabled="True" Header="Відкрити браузер анотацій..." Click="OpenDataExplorerItemClick"/>
|
||||
IsEnabled="True" Header="Відкрити переглядач анотацій..." Click="OpenDataExplorerItemClick"/>
|
||||
<MenuItem x:Name="ReloadThumbnailsItem"
|
||||
Foreground="Black"
|
||||
IsEnabled="True" Header="Оновити базу іконок" Click="ReloadThumbnailsItemClick"/>
|
||||
|
||||
@@ -248,25 +248,25 @@ public partial class MainWindow
|
||||
foreach (var file in labelFiles)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddAnnotation(TimeSpan time, List<YoloLabel> annotations)
|
||||
public async Task AddAnnotation(TimeSpan? time, List<YoloLabel> annotations)
|
||||
{
|
||||
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.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);
|
||||
if (existingResult != null)
|
||||
_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));
|
||||
}
|
||||
|
||||
@@ -277,7 +277,13 @@ public partial class MainWindow
|
||||
return;
|
||||
|
||||
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)
|
||||
.Select(gr => gr.Key)
|
||||
.ToDictionary(x => x);
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"VideosDirectory": "E:\\Azaion3\\Videos",
|
||||
"LabelsDirectory": "E:\\labels",
|
||||
"ImagesDirectory": "E:\\images",
|
||||
"ThumbnailsDirectory": "E:\\thumbnails",
|
||||
"ResultsDirectory": "E:\\results",
|
||||
"UnknownImages": "E:\\unknown",
|
||||
"ThumbnailsDirectory": "E:\\thumbnails",
|
||||
"AnnotationClasses": [
|
||||
{ "Id": 0, "Name": "Броньована техніка", "Color": "#40FF0000" },
|
||||
{ "Id": 1, "Name": "Вантажівка", "Color": "#4000FF00" },
|
||||
|
||||
Reference in New Issue
Block a user