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 TextBlock _classNameLabel;
public TimeSpan Time { get; set; }
public TimeSpan? Time { get; set; }
private AnnotationClass _annotationClass = null!;
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;
_resizeStart = resizeStart;
+8 -5
View File
@@ -34,13 +34,13 @@ public class CanvasEditor : Canvas
public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register(
nameof(GetTimeFunc),
typeof(Func<TimeSpan>),
typeof(Func<TimeSpan?>),
typeof(CanvasEditor),
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);
}
@@ -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)
{
@@ -354,7 +354,10 @@ public class CanvasEditor : Canvas
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);
}
}
+11 -12
View File
@@ -24,19 +24,18 @@ public class FormState
public TimeSpan? GetTime(string name)
{
var timeStr = name.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr))
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 7)
return null;
try
{
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
return new TimeSpan(
days: 0,
hours: int.Parse(timeStr[0..1]),
minutes: int.Parse(timeStr[1..3]),
seconds: int.Parse(timeStr[3..5]),
milliseconds: int.Parse(timeStr[5..6]) * 100);
}
catch (Exception e) { return null; }
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!int.TryParse(timeStr[0..1], out var hours))
return null;
if (!int.TryParse(timeStr[1..3], out var minutes))
return null;
if (!int.TryParse(timeStr[3..5], out var seconds))
return null;
if (!int.TryParse(timeStr[5..6], out var milliseconds))
return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
}
}
+10 -17
View File
@@ -135,31 +135,24 @@ public class YoloLabel : Label
var strings = s.Replace(',', '.').Split(' ');
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),
CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture),
CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture),
Width = double.Parse(strings[3], CultureInfo.InvariantCulture),
Height = double.Parse(strings[4], CultureInfo.InvariantCulture)
};
return res;
}
catch (Exception)
{
return null;
}
ClassNumber = int.Parse(strings[0], CultureInfo.InvariantCulture),
CenterX = double.Parse(strings[1], CultureInfo.InvariantCulture),
CenterY = double.Parse(strings[2], CultureInfo.InvariantCulture),
Width = double.Parse(strings[3], CultureInfo.InvariantCulture),
Height = double.Parse(strings[4], CultureInfo.InvariantCulture)
};
return res;
}
public static async Task<List<YoloLabel>> ReadFromFile(string filename)
{
var str = await File.ReadAllTextAsync(filename);
return str.Split(Environment.NewLine)
return str.Split('\n')
.Select(Parse)
.Where(ann => ann != null)
.ToList()!;
+2
View File
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows.Media.Imaging;
using Azaion.Annotator.Extensions;
@@ -26,6 +27,7 @@ public class ThumbnailDto : INotifyPropertyChanged
OnPropertyChanged();
}
}
public string ImageName => Path.GetFileName(ImagePath);
public void UpdateImage() => _image = null;
+47 -14
View File
@@ -12,7 +12,22 @@
<Window.Resources>
<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>
</Window.Resources>
@@ -78,6 +93,7 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
@@ -87,23 +103,40 @@
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="0" Background="Black">
<TextBlock>Loading:</TextBlock>
<TextBlock Name="LoadingAnnsCaption">Завантаження:</TextBlock>
</StatusBarItem>
<StatusBarItem Grid.Column="1" Background="Black">
<controls:UpdatableProgressBar x:Name="Volume"
Width="150"
Height="15"
HorizontalAlignment="Stretch"
Background="#252525"
BorderBrush="#252525"
Foreground="LightBlue"
Maximum="100"
Minimum="0">
</controls:UpdatableProgressBar>
<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>
<Separator Grid.Column="2"/>
<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>
</StatusBar>
</Grid>
+66 -35
View File
@@ -1,10 +1,8 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Microsoft.Extensions.Logging;
@@ -35,7 +33,8 @@ public partial class DatasetExplorer
Config config,
ILogger<DatasetExplorer> logger,
IConfigRepository configRepository,
FormState formState)
FormState formState,
IGalleryManager galleryManager)
{
_config = config;
_logger = logger;
@@ -93,6 +92,8 @@ public partial class DatasetExplorer
SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings();
RefreshThumbBar.Value = galleryManager.ThumbnailsPercentage;
};
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()
@@ -161,7 +167,7 @@ public partial class DatasetExplorer
Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = 1;
var time = _formState.GetTime(dto.ImagePath)!.Value;
var time = _formState.GetTime(dto.ImagePath);
ExplorerEditor.RemoveAllAnns();
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{
@@ -215,6 +221,9 @@ public partial class DatasetExplorer
private async Task ReloadThumbnails()
{
LoadingAnnsCaption.Visibility = Visibility.Visible;
LoadingAnnsBar.Visibility = Visibility.Visible;
if (!Directory.Exists(_config.ThumbnailsDirectory))
return;
@@ -227,50 +236,72 @@ public partial class DatasetExplorer
await AddThumbnail(thumbnail);
if (thumbNum % 1000 == 0)
{
await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache));
LoadingAnnsBar.Value = thumbNum * 100.0 / thumbnails.Length;
}
thumbNum++;
}
LoadingAnnsCaption.Visibility = Visibility.Collapsed;
LoadingAnnsBar.Visibility = Visibility.Collapsed;
await File.WriteAllTextAsync(_thumbnailsCacheFile, JsonConvert.SerializeObject(LabelsCache));
}
private async Task AddThumbnail(string thumbnail)
{
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length];
var imageName = Path.Combine(_config.ImagesDirectory, name);
foreach (var f in _config.ImageFormats)
try
{
var curName = $"{imageName}.{f}";
if (File.Exists(curName))
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length];
var imageName = Path.Combine(_config.ImagesDirectory, name);
foreach (var f in _config.ImageFormats)
{
imageName = curName;
break;
}
}
var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt");
if (!LabelsCache.TryGetValue(name, out var classes))
{
if (!File.Exists(labelPath))
{
_logger.LogError($"No label {labelPath} found ! Image {(!File.Exists(imageName) ? "not exists!" : "exists.")}");
return;
var curName = $"{imageName}.{f}";
if (File.Exists(curName))
{
imageName = curName;
break;
}
}
var labels = await YoloLabel.ReadFromFile(labelPath);
classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
LabelsCache.Add(name, classes);
}
var labelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt");
if (classes.Contains(ExplorerEditor.CurrentAnnClass.Id) || ExplorerEditor.CurrentAnnClass.Id == -1)
{
ThumbnailsDtos.Add(new ThumbnailDto
if (!LabelsCache.TryGetValue(name, out var classes))
{
ThumbnailPath = thumbnail,
ImagePath = imageName,
LabelPath = labelPath
});
}
if (!File.Exists(labelPath))
{
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;
}
var labels = await YoloLabel.ReadFromFile(labelPath);
classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
LabelsCache.Add(name, classes);
}
if (classes.Contains(ExplorerEditor.CurrentAnnClass.Id) || ExplorerEditor.CurrentAnnClass.Id == -1)
{
ThumbnailsDtos.Add(new ThumbnailDto
{
ThumbnailPath = thumbnail,
ImagePath = imageName,
LabelPath = labelPath
});
}
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
}
}
+52 -27
View File
@@ -9,10 +9,15 @@ using Size = System.Windows.Size;
namespace Azaion.Annotator;
public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGalleryManager
{
public int ThumbnailsCount { get; set; }
public int ImagesCount { get; set; }
public event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
private const int UPDATE_STEP = 20;
private readonly SemaphoreSlim _updateLock = new(1);
public double ThumbnailsPercentage { get; set; }
private DirectoryInfo? _thumbnailsDirectory;
private DirectoryInfo ThumbnailsDirectory
@@ -30,35 +35,54 @@ 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()
{
var prefixLen = Config.ThumbnailPrefix.Length;
var thumbnails = ThumbnailsDirectory.GetFiles()
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
.GroupBy(x => x)
.Select(gr => gr.Key)
.ToHashSet();
ThumbnailsCount = thumbnails.Count;
var files = new DirectoryInfo(config.ImagesDirectory).GetFiles();
ImagesCount = files.Length;
foreach (var img in files)
await _updateLock.WaitAsync();
try
{
var imgName = Path.GetFileNameWithoutExtension(img.Name);
if (thumbnails.Contains(imgName))
continue;
try
var prefixLen = Config.ThumbnailPrefix.Length;
var thumbnails = ThumbnailsDirectory.GetFiles()
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
.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 CreateThumbnail(img.FullName);
}
catch (Exception e)
{
logger.LogError(e, $"Failed to generate thumbnail for {img.Name}");
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);
if (thumbnails.Contains(imgName))
continue;
try
{
await CreateThumbnail(img.FullName);
thumbnailsCount++;
}
catch (Exception e)
{
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
{
int ThumbnailsCount { get; set; }
int ImagesCount { get; set; }
event ThumbnailsUpdatedEventHandler ThumbnailsUpdate;
double ThumbnailsPercentage { get; set; }
Task CreateThumbnail(string imgPath);
Task RefreshThumbnails();
void ClearThumbnails();
}
+3
View File
@@ -79,6 +79,9 @@
<MenuItem x:Name="OpenDataExplorerItem"
Foreground="Black"
IsEnabled="True" Header="Відкрити браузер анотацій..." Click="OpenDataExplorerItemClick"/>
<MenuItem x:Name="ReloadThumbnailsItem"
Foreground="Black"
IsEnabled="True" Header="Оновити базу іконок" Click="ReloadThumbnailsItemClick"/>
</MenuItem>
<MenuItem Header="Допомога" Foreground="#FFBDBCBC" Margin="0,3,0,0">
<MenuItem x:Name="OpenHelpWindow"
+14 -4
View File
@@ -180,16 +180,16 @@ public partial class MainWindow
};
_mediaPlayer.PositionChanged += (o, args) =>
{
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
};
VideoSlider.ValueChanged += (value, newValue) =>
_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();
LocationChanged += async (_, _) => await SaveUserSettings();
@@ -401,4 +401,14 @@ public partial class MainWindow
}
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();
}
}