fix editing non-timed annotations

This commit is contained in:
Alex Bezdieniezhnykh
2024-09-20 09:23:12 +03:00
parent 2236eb7fcb
commit 742f1ffee9
10 changed files with 218 additions and 119 deletions
@@ -14,7 +14,7 @@ public class AnnotationControl : Border
private readonly Grid _grid; private readonly Grid _grid;
private readonly TextBlock _classNameLabel; private readonly TextBlock _classNameLabel;
public TimeSpan Time { get; set; } public TimeSpan? Time { get; set; }
private AnnotationClass _annotationClass = null!; private AnnotationClass _annotationClass = null!;
public AnnotationClass AnnotationClass public AnnotationClass AnnotationClass
@@ -41,7 +41,7 @@ public class AnnotationControl : Border
} }
} }
public AnnotationControl(AnnotationClass annotationClass, TimeSpan time, Action<object, MouseButtonEventArgs> resizeStart) public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart)
{ {
Time = time; Time = time;
_resizeStart = resizeStart; _resizeStart = resizeStart;
+8 -5
View File
@@ -34,13 +34,13 @@ public class CanvasEditor : Canvas
public static readonly DependencyProperty GetTimeFuncProp = public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register( DependencyProperty.Register(
nameof(GetTimeFunc), nameof(GetTimeFunc),
typeof(Func<TimeSpan>), typeof(Func<TimeSpan?>),
typeof(CanvasEditor), typeof(CanvasEditor),
new PropertyMetadata(null)); new PropertyMetadata(null));
public Func<TimeSpan> GetTimeFunc public Func<TimeSpan?> GetTimeFunc
{ {
get => (Func<TimeSpan>)GetValue(GetTimeFuncProp); get => (Func<TimeSpan?>)GetValue(GetTimeFuncProp);
set => SetValue(GetTimeFuncProp, value); set => SetValue(GetTimeFuncProp, value);
} }
@@ -310,7 +310,7 @@ public class CanvasEditor : Canvas
}); });
} }
public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan time, CanvasLabel canvasLabel) public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
{ {
var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart) var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart)
{ {
@@ -354,7 +354,10 @@ public class CanvasEditor : Canvas
public void ClearExpiredAnnotations(TimeSpan time) public void ClearExpiredAnnotations(TimeSpan time)
{ {
var expiredAnns = CurrentAnns.Where(x => Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds).ToList(); var expiredAnns = CurrentAnns.Where(x =>
x.Time.HasValue &&
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList();
RemoveAnnotations(expiredAnns); RemoveAnnotations(expiredAnns);
} }
} }
+10 -11
View File
@@ -24,19 +24,18 @@ public class FormState
public TimeSpan? GetTime(string name) public TimeSpan? GetTime(string name)
{ {
var timeStr = name.Split("_").LastOrDefault(); var timeStr = name.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr)) if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 7)
return null; return null;
try
{
//For some reason, TimeSpan.ParseExact doesn't work on every platform. //For some reason, TimeSpan.ParseExact doesn't work on every platform.
return new TimeSpan( if (!int.TryParse(timeStr[0..1], out var hours))
days: 0, return null;
hours: int.Parse(timeStr[0..1]), if (!int.TryParse(timeStr[1..3], out var minutes))
minutes: int.Parse(timeStr[1..3]), return null;
seconds: int.Parse(timeStr[3..5]), if (!int.TryParse(timeStr[3..5], out var seconds))
milliseconds: int.Parse(timeStr[5..6]) * 100); return null;
} if (!int.TryParse(timeStr[5..6], out var milliseconds))
catch (Exception e) { return null; } return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
} }
} }
+2 -9
View File
@@ -135,10 +135,8 @@ public class YoloLabel : Label
var strings = s.Replace(',', '.').Split(' '); var strings = s.Replace(',', '.').Split(' ');
if (strings.Length != 5) if (strings.Length != 5)
return null; throw new Exception("Wrong labels format!");
try
{
var res = new YoloLabel var res = new YoloLabel
{ {
ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture), ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture),
@@ -149,17 +147,12 @@ public class YoloLabel : Label
}; };
return res; return res;
} }
catch (Exception)
{
return null;
}
}
public static async Task<List<YoloLabel>> ReadFromFile(string filename) public static async Task<List<YoloLabel>> ReadFromFile(string filename)
{ {
var str = await File.ReadAllTextAsync(filename); var str = await File.ReadAllTextAsync(filename);
return str.Split(Environment.NewLine) return str.Split('\n')
.Select(Parse) .Select(Parse)
.Where(ann => ann != null) .Where(ann => ann != null)
.ToList()!; .ToList()!;
+2
View File
@@ -1,4 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
@@ -26,6 +27,7 @@ public class ThumbnailDto : INotifyPropertyChanged
OnPropertyChanged(); OnPropertyChanged();
} }
} }
public string ImageName => Path.GetFileName(ImagePath);
public void UpdateImage() => _image = null; public void UpdateImage() => _image = null;
+40 -7
View File
@@ -12,7 +12,22 @@
<Window.Resources> <Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}"> <DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}">
<Image Source="{Binding Image}" Width="480" Height="270" Margin="5" /> <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> </DataTemplate>
</Window.Resources> </Window.Resources>
@@ -78,6 +93,7 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -87,10 +103,10 @@
</ItemsPanelTemplate> </ItemsPanelTemplate>
</StatusBar.ItemsPanel> </StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="0" Background="Black"> <StatusBarItem Grid.Column="0" Background="Black">
<TextBlock>Loading:</TextBlock> <TextBlock Name="LoadingAnnsCaption">Завантаження:</TextBlock>
</StatusBarItem> </StatusBarItem>
<StatusBarItem Grid.Column="1" Background="Black"> <StatusBarItem Grid.Column="1" Background="Black">
<controls:UpdatableProgressBar x:Name="Volume" <ProgressBar x:Name="LoadingAnnsBar"
Width="150" Width="150"
Height="15" Height="15"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
@@ -98,12 +114,29 @@
BorderBrush="#252525" BorderBrush="#252525"
Foreground="LightBlue" Foreground="LightBlue"
Maximum="100" Maximum="100"
Minimum="0"> Minimum="0"
</controls:UpdatableProgressBar> Value="0">
</ProgressBar>
</StatusBarItem>
<StatusBarItem Grid.Column="2" Background="Black">
<TextBlock Name="RefreshThumbCaption">База іконок:</TextBlock>
</StatusBarItem> </StatusBarItem>
<Separator Grid.Column="2"/>
<StatusBarItem Grid.Column="3" Background="Black"> <StatusBarItem Grid.Column="3" Background="Black">
<TextBlock Name="StatusText" Text="Loading..."/> <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> </StatusBarItem>
</StatusBar> </StatusBar>
</Grid> </Grid>
+38 -7
View File
@@ -1,10 +1,8 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -35,7 +33,8 @@ public partial class DatasetExplorer
Config config, Config config,
ILogger<DatasetExplorer> logger, ILogger<DatasetExplorer> logger,
IConfigRepository configRepository, IConfigRepository configRepository,
FormState formState) FormState formState,
IGalleryManager galleryManager)
{ {
_config = config; _config = config;
_logger = logger; _logger = logger;
@@ -93,6 +92,8 @@ public partial class DatasetExplorer
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;
}; };
Closing += (sender, args) => Closing += (sender, args) =>
@@ -139,7 +140,12 @@ public partial class DatasetExplorer
} }
}; };
ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath)!.Value; ExplorerEditor.GetTimeFunc = () => _formState.GetTime(CurrentThumbnail!.ImagePath);
galleryManager.ThumbnailsUpdate += thumbnailsPercentage =>
{
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
};
} }
private async Task EditAnnotation() private async Task EditAnnotation()
@@ -161,7 +167,7 @@ public partial class DatasetExplorer
Switcher.SelectedIndex = 1; Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = 1; LvClasses.SelectedIndex = 1;
var time = _formState.GetTime(dto.ImagePath)!.Value; var time = _formState.GetTime(dto.ImagePath);
ExplorerEditor.RemoveAllAnns(); ExplorerEditor.RemoveAllAnns();
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{ {
@@ -215,6 +221,9 @@ public partial class DatasetExplorer
private async Task ReloadThumbnails() private async Task ReloadThumbnails()
{ {
LoadingAnnsCaption.Visibility = Visibility.Visible;
LoadingAnnsBar.Visibility = Visibility.Visible;
if (!Directory.Exists(_config.ThumbnailsDirectory)) if (!Directory.Exists(_config.ThumbnailsDirectory))
return; return;
@@ -227,13 +236,21 @@ public partial class DatasetExplorer
await AddThumbnail(thumbnail); await AddThumbnail(thumbnail);
if (thumbNum % 1000 == 0) if (thumbNum % 1000 == 0)
{
await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache)); await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache));
LoadingAnnsBar.Value = thumbNum * 100.0 / thumbnails.Length;
}
thumbNum++; thumbNum++;
} }
LoadingAnnsCaption.Visibility = Visibility.Collapsed;
LoadingAnnsBar.Visibility = Visibility.Collapsed;
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)
{
try
{ {
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length]; var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length];
var imageName = Path.Combine(_config.ImagesDirectory, name); var imageName = Path.Combine(_config.ImagesDirectory, name);
@@ -253,7 +270,17 @@ public partial class DatasetExplorer
{ {
if (!File.Exists(labelPath)) if (!File.Exists(labelPath))
{ {
_logger.LogError($"No label {labelPath} found ! Image {(!File.Exists(imageName) ? "not exists!" : "exists.")}"); var imageExists = File.Exists(imageName);
if (!imageExists)
{
_logger.LogError($"No label {labelPath} found ! Image {imageName} not found, removing thumbnail {thumbnail}");
File.Delete(thumbnail);
}
else
{
_logger.LogError($"No label {labelPath} found! But Image {imageName} exists! Image moved to {_config.UnknownImages} directory!");
File.Move(imageName, Path.Combine(_config.UnknownImages, imageName));
}
return; return;
} }
@@ -271,6 +298,10 @@ public partial class DatasetExplorer
LabelPath = labelPath LabelPath = labelPath
}); });
} }
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
} }
} }
+34 -9
View File
@@ -9,10 +9,15 @@ using Size = System.Windows.Size;
namespace Azaion.Annotator; namespace Azaion.Annotator;
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 int ThumbnailsCount { get; set; } public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
public int ImagesCount { get; set; } private const int UPDATE_STEP = 20;
private readonly SemaphoreSlim _updateLock = new(1);
public double ThumbnailsPercentage { get; set; }
private DirectoryInfo? _thumbnailsDirectory; private DirectoryInfo? _thumbnailsDirectory;
private DirectoryInfo ThumbnailsDirectory private DirectoryInfo ThumbnailsDirectory
@@ -30,7 +35,16 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
} }
} }
public void ClearThumbnails()
{
foreach(var file in new DirectoryInfo(config.ThumbnailsDirectory).GetFiles())
file.Delete();
}
public async Task RefreshThumbnails() public async Task RefreshThumbnails()
{
await _updateLock.WaitAsync();
try
{ {
var prefixLen = Config.ThumbnailPrefix.Length; var prefixLen = Config.ThumbnailPrefix.Length;
@@ -39,26 +53,36 @@ 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();
ThumbnailsCount = thumbnails.Count; var thumbnailsCount = thumbnails.Count;
var files = new DirectoryInfo(config.ImagesDirectory).GetFiles(); var files = new DirectoryInfo(config.ImagesDirectory).GetFiles();
ImagesCount = files.Length; var imagesCount = files.Length;
foreach (var img in files) for (int i = 0; i < files.Length; i++)
{ {
var img = files[i];
ThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, i * 100 / (double)imagesCount);
var imgName = Path.GetFileNameWithoutExtension(img.Name); var imgName = Path.GetFileNameWithoutExtension(img.Name);
if (i % UPDATE_STEP == 0)
ThumbnailsUpdate?.Invoke(ThumbnailsPercentage);
if (thumbnails.Contains(imgName)) if (thumbnails.Contains(imgName))
continue; continue;
try try
{ {
await CreateThumbnail(img.FullName); await CreateThumbnail(img.FullName);
thumbnailsCount++;
} }
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, $"Failed to generate thumbnail for {img.Name}"); logger.LogError(e, $"Failed to generate thumbnail for {img.Name}! Error: {e.Message}");
}
} }
ThumbnailsCount++; await Task.Delay(10000);
}
finally
{
_updateLock.Release();
} }
} }
@@ -152,8 +176,9 @@ public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGa
public interface IGalleryManager public interface IGalleryManager
{ {
int ThumbnailsCount { get; set; } event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
int ImagesCount { get; set; } double ThumbnailsPercentage { get; set; }
Task CreateThumbnail(string imgPath); Task CreateThumbnail(string imgPath);
Task RefreshThumbnails(); Task RefreshThumbnails();
void ClearThumbnails();
} }
+3
View File
@@ -79,6 +79,9 @@
<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"
Foreground="Black"
IsEnabled="True" Header="Оновити базу іконок" Click="ReloadThumbnailsItemClick"/>
</MenuItem> </MenuItem>
<MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0"> <MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0">
<MenuItem x:Name="OpenHelpWindow" <MenuItem x:Name="OpenHelpWindow"
+14 -4
View File
@@ -180,16 +180,16 @@ public partial class MainWindow
}; };
_mediaPlayer.PositionChanged += (o, args) => _mediaPlayer.PositionChanged += (o, args) =>
{
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time)); ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
};
VideoSlider.ValueChanged += (value, newValue) => VideoSlider.ValueChanged += (value, newValue) =>
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum); _mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
VideoSlider.KeyDown += (sender, args) => _mediator.Publish(new KeyEvent(sender, args)); VideoSlider.KeyDown += (sender, args) =>
_mediator.Publish(new KeyEvent(sender, args));
Volume.ValueChanged += (_, newValue) => _mediator.Publish(new VolumeChangedEvent((int)newValue)); Volume.ValueChanged += (_, newValue) =>
_mediator.Publish(new VolumeChangedEvent((int)newValue));
SizeChanged += async (_, _) => await SaveUserSettings(); SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings(); LocationChanged += async (_, _) => await SaveUserSettings();
@@ -401,4 +401,14 @@ public partial class MainWindow
} }
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings(); private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings();
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e)
{
var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_config.ThumbnailsDirectory}?",
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
_galleryManager.ClearThumbnails();
_galleryManager.RefreshThumbnails();
}
} }