mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 14:06:31 +00:00
add db WIP 2, 80%
refactor, renames
This commit is contained in:
@@ -19,7 +19,7 @@ public class Constants
|
||||
|
||||
#region AnnotatorConfig
|
||||
|
||||
public static readonly List<AnnotationClass> DefaultAnnotationClasses =
|
||||
public static readonly List<DetectionClass> DefaultAnnotationClasses =
|
||||
[
|
||||
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" },
|
||||
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" },
|
||||
@@ -62,7 +62,6 @@ public class Constants
|
||||
public const int DEFAULT_THUMBNAIL_BORDER = 10;
|
||||
|
||||
public const string THUMBNAIL_PREFIX = "_thumb";
|
||||
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -92,6 +91,14 @@ public class Constants
|
||||
public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
|
||||
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Database
|
||||
|
||||
public const string ANNOTATIONS_TABLENAME = "annotations";
|
||||
public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue";
|
||||
public const string ADMIN_EMAIL = "admin@azaion.com";
|
||||
public const string DETECTIONS_TABLENAME = "detections";
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public class CanvasEditor : Canvas
|
||||
private readonly TextBlock _classNameHint;
|
||||
|
||||
private Rectangle _curRec = new();
|
||||
private AnnotationControl _curAnn = null!;
|
||||
private DetectionControl _curAnn = null!;
|
||||
|
||||
private const int MIN_SIZE = 20;
|
||||
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
||||
@@ -44,8 +44,8 @@ public class CanvasEditor : Canvas
|
||||
set => SetValue(GetTimeFuncProp, value);
|
||||
}
|
||||
|
||||
private AnnotationClass _currentAnnClass = null!;
|
||||
public AnnotationClass CurrentAnnClass
|
||||
private DetectionClass _currentAnnClass = null!;
|
||||
public DetectionClass CurrentAnnClass
|
||||
{
|
||||
get => _currentAnnClass;
|
||||
set
|
||||
@@ -62,7 +62,7 @@ public class CanvasEditor : Canvas
|
||||
}
|
||||
}
|
||||
|
||||
public readonly List<AnnotationControl> CurrentAnns = new();
|
||||
public readonly List<DetectionControl> CurrentDetections = new();
|
||||
|
||||
public CanvasEditor()
|
||||
{
|
||||
@@ -173,7 +173,7 @@ public class CanvasEditor : Canvas
|
||||
SelectionState = SelectionState.AnnResizing;
|
||||
_lastPos = e.GetPosition(this);
|
||||
_curRec = (Rectangle)sender;
|
||||
_curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent;
|
||||
_curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ public class CanvasEditor : Canvas
|
||||
private void AnnotationPositionStart(object sender, MouseEventArgs e)
|
||||
{
|
||||
_lastPos = e.GetPosition(this);
|
||||
_curAnn = (AnnotationControl)sender;
|
||||
_curAnn = (DetectionControl)sender;
|
||||
|
||||
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
|
||||
ClearSelections();
|
||||
@@ -310,9 +310,9 @@ public class CanvasEditor : Canvas
|
||||
});
|
||||
}
|
||||
|
||||
public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
|
||||
public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
|
||||
{
|
||||
var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
|
||||
var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
|
||||
{
|
||||
Width = canvasLabel.Width,
|
||||
Height = canvasLabel.Height
|
||||
@@ -321,40 +321,40 @@ public class CanvasEditor : Canvas
|
||||
SetLeft(annotationControl, canvasLabel.X );
|
||||
SetTop(annotationControl, canvasLabel.Y);
|
||||
Children.Add(annotationControl);
|
||||
CurrentAnns.Add(annotationControl);
|
||||
CurrentDetections.Add(annotationControl);
|
||||
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
|
||||
return annotationControl;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void RemoveAnnotations(IEnumerable<AnnotationControl> listToRemove)
|
||||
private void RemoveAnnotations(IEnumerable<DetectionControl> listToRemove)
|
||||
{
|
||||
foreach (var ann in listToRemove)
|
||||
{
|
||||
Children.Remove(ann);
|
||||
CurrentAnns.Remove(ann);
|
||||
CurrentDetections.Remove(ann);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAllAnns()
|
||||
{
|
||||
foreach (var ann in CurrentAnns)
|
||||
foreach (var ann in CurrentDetections)
|
||||
Children.Remove(ann);
|
||||
CurrentAnns.Clear();
|
||||
CurrentDetections.Clear();
|
||||
}
|
||||
|
||||
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList());
|
||||
public void RemoveSelectedAnns() => RemoveAnnotations(CurrentDetections.Where(x => x.IsSelected).ToList());
|
||||
|
||||
private void ClearSelections()
|
||||
{
|
||||
foreach (var ann in CurrentAnns)
|
||||
foreach (var ann in CurrentDetections)
|
||||
ann.IsSelected = false;
|
||||
}
|
||||
|
||||
public void ClearExpiredAnnotations(TimeSpan time)
|
||||
{
|
||||
var expiredAnns = CurrentAnns.Where(x =>
|
||||
var expiredAnns = CurrentDetections.Where(x =>
|
||||
x.Time.HasValue &&
|
||||
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
|
||||
.ToList();
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<DataGrid x:Class="Azaion.Common.Controls.AnnotationClasses"
|
||||
<DataGrid x:Class="Azaion.Common.Controls.DetectionClasses"
|
||||
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"
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public partial class AnnotationClasses
|
||||
public partial class DetectionClasses
|
||||
{
|
||||
public AnnotationClasses()
|
||||
public DetectionClasses()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
+13
-9
@@ -8,7 +8,7 @@ using Label = System.Windows.Controls.Label;
|
||||
|
||||
namespace Azaion.Common.Controls;
|
||||
|
||||
public class AnnotationControl : Border
|
||||
public class DetectionControl : Border
|
||||
{
|
||||
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
|
||||
private const double RESIZE_RECT_SIZE = 9;
|
||||
@@ -18,16 +18,16 @@ public class AnnotationControl : Border
|
||||
private readonly Label _probabilityLabel;
|
||||
public TimeSpan? Time { get; set; }
|
||||
|
||||
private AnnotationClass _annotationClass = null!;
|
||||
public AnnotationClass AnnotationClass
|
||||
private DetectionClass _detectionClass = null!;
|
||||
public DetectionClass DetectionClass
|
||||
{
|
||||
get => _annotationClass;
|
||||
get => _detectionClass;
|
||||
set
|
||||
{
|
||||
_grid.Background = value.ColorBrush;
|
||||
_probabilityLabel.Background = value.ColorBrush;
|
||||
_classNameLabel.Text = value.Name;
|
||||
_annotationClass = value;
|
||||
_detectionClass = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +44,13 @@ public class AnnotationControl : Border
|
||||
}
|
||||
}
|
||||
|
||||
public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
|
||||
public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
|
||||
{
|
||||
Time = time;
|
||||
_resizeStart = resizeStart;
|
||||
_classNameLabel = new TextBlock
|
||||
{
|
||||
Text = annotationClass.Name,
|
||||
Text = detectionClass.Name,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(0, 15, 0, 0),
|
||||
@@ -97,7 +97,7 @@ public class AnnotationControl : Border
|
||||
_grid.Children.Add(_probabilityLabel);
|
||||
Child = _grid;
|
||||
Cursor = Cursors.SizeAll;
|
||||
AnnotationClass = annotationClass;
|
||||
DetectionClass = detectionClass;
|
||||
}
|
||||
|
||||
//small corners
|
||||
@@ -118,5 +118,9 @@ public class AnnotationControl : Border
|
||||
return rect;
|
||||
}
|
||||
|
||||
public CanvasLabel Info => new(AnnotationClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
||||
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null)
|
||||
{
|
||||
var label = new CanvasLabel(DetectionClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
||||
return new YoloLabel(label, canvasSize, videoSize);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.IO;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.DTO.Queue;
|
||||
using Azaion.Common.Extensions;
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
@@ -9,26 +11,36 @@ public class Annotation
|
||||
{
|
||||
private static string _labelsDir = null!;
|
||||
private static string _imagesDir = null!;
|
||||
private static string _thumbDir = null!;
|
||||
|
||||
public static void InitializeDirs(DirectoriesConfig config)
|
||||
{
|
||||
_labelsDir = config.LabelsDirectory;
|
||||
_imagesDir = config.ImagesDirectory;
|
||||
_thumbDir = config.ThumbnailsDirectory;
|
||||
}
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
public string ImageExtension { get; set; } = null!;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public List<int> Classes { get; set; } = null!;
|
||||
public string CreatedEmail { get; set; } = null!;
|
||||
public RoleEnum CreatedRole { get; set; }
|
||||
public SourceEnum Source { get; set; }
|
||||
public AnnotationStatus AnnotationStatus { get; set; }
|
||||
|
||||
public string ImagePath => Path.Combine(_imagesDir, $"{Name}.jpg");
|
||||
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
||||
public IEnumerable<Detection> Detections { get; set; } = null!;
|
||||
|
||||
public double Lat { get; set; }
|
||||
public double Lon { get; set; }
|
||||
|
||||
public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
|
||||
public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
|
||||
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
||||
public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
|
||||
}
|
||||
|
||||
|
||||
|
||||
public enum AnnotationStatus
|
||||
{
|
||||
None = 0,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class AnnotationCreatedEvent(Annotation annotation) : INotification
|
||||
{
|
||||
public Annotation Annotation { get; } = annotation;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Azaion.Common.Extensions;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged
|
||||
{
|
||||
public Annotation Annotation { get; set; } = annotation;
|
||||
|
||||
private BitmapImage? _thumbnail;
|
||||
public BitmapImage? Thumbnail
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_thumbnail == null)
|
||||
Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage());
|
||||
return _thumbnail;
|
||||
}
|
||||
private set => _thumbnail = value;
|
||||
}
|
||||
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
File.Delete(Annotation.ImagePath);
|
||||
File.Delete(Annotation.LabelPath);
|
||||
File.Delete(Annotation.ThumbPath);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class AnnotationConfig
|
||||
{
|
||||
public List<AnnotationClass> AnnotationClasses { get; set; } = null!;
|
||||
public List<DetectionClass> AnnotationClasses { get; set; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
private Dictionary<int, AnnotationClass>? _annotationClassesDict;
|
||||
private Dictionary<int, DetectionClass>? _detectionClassesDict;
|
||||
[JsonIgnore]
|
||||
public Dictionary<int, AnnotationClass> AnnotationClassesDict => _annotationClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
|
||||
public Dictionary<int, DetectionClass> DetectionClassesDict => _detectionClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
|
||||
|
||||
public int? LastSelectedExplorerClass { get; set; }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class AnnotationClass
|
||||
public class DetectionClass
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class ImageCreatedEvent(string imagePath) : INotification
|
||||
{
|
||||
public string ImagePath { get; } = imagePath;
|
||||
}
|
||||
@@ -41,12 +41,14 @@ public class CanvasLabel : Label
|
||||
Probability = probability;
|
||||
}
|
||||
|
||||
public CanvasLabel(YoloLabel label, Size canvasSize, Size videoSize, double? probability = null)
|
||||
public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double? probability = null)
|
||||
{
|
||||
var cw = canvasSize.Width;
|
||||
var ch = canvasSize.Height;
|
||||
var canvasAr = cw / ch;
|
||||
var videoAr = videoSize.Width / videoSize.Height;
|
||||
var videoAr = videoSize.HasValue
|
||||
? videoSize.Value.Width / videoSize.Value.Height
|
||||
: canvasAr;
|
||||
|
||||
ClassNumber = label.ClassNumber;
|
||||
|
||||
@@ -102,12 +104,14 @@ public class YoloLabel : Label
|
||||
public RectangleF ToRectangle() =>
|
||||
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
|
||||
|
||||
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size videoSize)
|
||||
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? videoSize = null)
|
||||
{
|
||||
var cw = canvasSize.Width;
|
||||
var ch = canvasSize.Height;
|
||||
var canvasAr = cw / ch;
|
||||
var videoAr = videoSize.Width / videoSize.Height;
|
||||
var videoAr = videoSize.HasValue
|
||||
? videoSize.Value.Width / videoSize.Value.Height
|
||||
: canvasAr;
|
||||
|
||||
ClassNumber = canvasLabel.ClassNumber;
|
||||
|
||||
@@ -182,8 +186,15 @@ public class YoloLabel : Label
|
||||
|
||||
public class Detection : YoloLabel
|
||||
{
|
||||
public Detection(YoloLabel label, double? probability = null)
|
||||
public string AnnotationName { get; set; }
|
||||
public double? Probability { get; set; }
|
||||
|
||||
//For db
|
||||
public Detection(){}
|
||||
|
||||
public Detection(string annotationName, YoloLabel label, double? probability = null)
|
||||
{
|
||||
AnnotationName = annotationName;
|
||||
ClassNumber = label.ClassNumber;
|
||||
CenterX = label.CenterX;
|
||||
CenterY = label.CenterY;
|
||||
@@ -191,5 +202,4 @@ public class Detection : YoloLabel
|
||||
Width = label.Width;
|
||||
Probability = probability;
|
||||
}
|
||||
public double? Probability { get; set; }
|
||||
}
|
||||
@@ -6,14 +6,15 @@ using MessagePack;
|
||||
[MessagePackObject]
|
||||
public class AnnotationCreatedMessage
|
||||
{
|
||||
[Key(0)] public DateTime CreatedDate { get; set; }
|
||||
[Key(1)] public string Name { get; set; } = null!;
|
||||
[Key(2)] public string Label { get; set; } = null!;
|
||||
[Key(3)] public byte[] Image { get; set; } = null!;
|
||||
[Key(4)] public RoleEnum CreatedRole { get; set; }
|
||||
[Key(5)] public string CreatedEmail { get; set; } = null!;
|
||||
[Key(6)] public SourceEnum Source { get; set; }
|
||||
[Key(7)] public AnnotationStatus Status { get; set; }
|
||||
[Key(0)] public DateTime CreatedDate { get; set; }
|
||||
[Key(1)] public string Name { get; set; } = null!;
|
||||
[Key(2)] public string ImageExtension { get; set; } = null!;
|
||||
[Key(3)] public string Detections { get; set; } = null!;
|
||||
[Key(4)] public byte[] Image { get; set; } = null!;
|
||||
[Key(5)] public RoleEnum CreatedRole { get; set; }
|
||||
[Key(6)] public string CreatedEmail { get; set; } = null!;
|
||||
[Key(7)] public SourceEnum Source { get; set; }
|
||||
[Key(8)] public AnnotationStatus Status { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
||||
@@ -8,4 +8,5 @@ public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions
|
||||
{
|
||||
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
||||
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
|
||||
public ITable<Detection> Detections => this.GetTable<Detection>();
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.Diagnostics;
|
||||
using System.Data.SQLite;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using LinqToDB;
|
||||
using LinqToDB.DataProvider.SQLite;
|
||||
using LinqToDB.Mapping;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Azaion.Common.Database;
|
||||
@@ -11,42 +15,72 @@ public interface IDbFactory
|
||||
{
|
||||
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
|
||||
Task Run(Func<AnnotationsDb, Task> func);
|
||||
void SaveToDisk();
|
||||
}
|
||||
|
||||
public class DbFactory : IDbFactory
|
||||
{
|
||||
private readonly DataOptions _dataOptions;
|
||||
private readonly AnnotationConfig _annConfig;
|
||||
|
||||
public DbFactory(IOptions<AnnotationConfig> annConfig)
|
||||
private string MemoryConnStr => "Data Source=:memory:";
|
||||
private readonly SQLiteConnection _memoryConnection;
|
||||
private readonly DataOptions _memoryDataOptions;
|
||||
|
||||
private string FileConnStr => $"Data Source={_annConfig.AnnotationsDbFile}";
|
||||
private readonly SQLiteConnection _fileConnection;
|
||||
private readonly DataOptions _fileDataOptions;
|
||||
|
||||
public DbFactory(IOptions<AnnotationConfig> annConfig, ILogger<DbFactory> logger)
|
||||
{
|
||||
_dataOptions = LoadOptions(annConfig.Value.AnnotationsDbFile);
|
||||
}
|
||||
_annConfig = annConfig.Value;
|
||||
|
||||
private DataOptions LoadOptions(string dbFile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dbFile))
|
||||
throw new ArgumentException($"Empty AnnotationsDbFile in config!");
|
||||
|
||||
var dataOptions = new DataOptions()
|
||||
.UseSQLiteOfficial($"Data Source={dbFile}")
|
||||
_memoryConnection = new SQLiteConnection(MemoryConnStr);
|
||||
_memoryConnection.Open();
|
||||
_memoryDataOptions = new DataOptions()
|
||||
.UseDataProvider(SQLiteTools.GetDataProvider())
|
||||
.UseConnection(_memoryConnection)
|
||||
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
||||
_ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
|
||||
|
||||
_ = dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText));
|
||||
return dataOptions;
|
||||
|
||||
_fileConnection = new SQLiteConnection(FileConnStr);
|
||||
_fileDataOptions = new DataOptions()
|
||||
.UseDataProvider(SQLiteTools.GetDataProvider())
|
||||
.UseConnection(_fileConnection)
|
||||
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
||||
_ = _fileDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
|
||||
|
||||
if (!File.Exists(_annConfig.AnnotationsDbFile))
|
||||
CreateDb();
|
||||
_fileConnection.Open();
|
||||
_fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1);
|
||||
}
|
||||
|
||||
private void CreateDb()
|
||||
{
|
||||
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
|
||||
using var db = new AnnotationsDb(_fileDataOptions);
|
||||
db.CreateTable<Annotation>();
|
||||
db.CreateTable<AnnotationName>();
|
||||
db.CreateTable<Detection>();
|
||||
}
|
||||
|
||||
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
||||
{
|
||||
await using var db = new AnnotationsDb(_dataOptions);
|
||||
await using var db = new AnnotationsDb(_memoryDataOptions);
|
||||
return await func(db);
|
||||
}
|
||||
|
||||
public async Task Run(Func<AnnotationsDb, Task> func)
|
||||
{
|
||||
await using var db = new AnnotationsDb(_dataOptions);
|
||||
await using var db = new AnnotationsDb(_memoryDataOptions);
|
||||
await func(db);
|
||||
}
|
||||
|
||||
public void SaveToDisk()
|
||||
{
|
||||
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AnnotationsDbSchemaHolder
|
||||
@@ -58,7 +92,16 @@ public static class AnnotationsDbSchemaHolder
|
||||
MappingSchema = new MappingSchema();
|
||||
var builder = new FluentMappingBuilder(MappingSchema);
|
||||
|
||||
builder.Entity<AnnotationName>().HasTableName("annotations_queue");
|
||||
builder.Entity<Annotation>()
|
||||
.HasTableName(Constants.ANNOTATIONS_TABLENAME)
|
||||
.HasPrimaryKey(x => x.Name)
|
||||
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName);
|
||||
|
||||
builder.Entity<Detection>()
|
||||
.HasTableName(Constants.DETECTIONS_TABLENAME);
|
||||
|
||||
builder.Entity<AnnotationName>()
|
||||
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME);
|
||||
|
||||
builder.Build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.IO;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Azaion.Common.Extensions;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ using Azaion.Common.DTO.Queue;
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
using Azaion.CommonSecurity.Services;
|
||||
using LinqToDB;
|
||||
using MediatR;
|
||||
using MessagePack;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using RabbitMQ.Stream.Client;
|
||||
using RabbitMQ.Stream.Client.Reliable;
|
||||
|
||||
@@ -20,17 +22,23 @@ public class AnnotationService
|
||||
private readonly AzaionApiClient _apiClient;
|
||||
private readonly IDbFactory _dbFactory;
|
||||
private readonly FailsafeAnnotationsProducer _producer;
|
||||
private readonly IGalleryService _galleryService;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly QueueConfig _queueConfig;
|
||||
private Consumer _consumer = null!;
|
||||
|
||||
public AnnotationService(AzaionApiClient apiClient,
|
||||
IDbFactory dbFactory,
|
||||
FailsafeAnnotationsProducer producer,
|
||||
IOptions<QueueConfig> queueConfig)
|
||||
IOptions<QueueConfig> queueConfig,
|
||||
IGalleryService galleryService,
|
||||
IMediator mediator)
|
||||
{
|
||||
_apiClient = apiClient;
|
||||
_dbFactory = dbFactory;
|
||||
_producer = producer;
|
||||
_galleryService = galleryService;
|
||||
_mediator = mediator;
|
||||
_queueConfig = queueConfig.Value;
|
||||
|
||||
Task.Run(async () => await Init()).Wait();
|
||||
@@ -53,8 +61,8 @@ public class AnnotationService
|
||||
}
|
||||
|
||||
//AI / Manual
|
||||
public async Task SaveAnnotation(string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream = null, CancellationToken token = default) =>
|
||||
await SaveAnnotationInner(DateTime.UtcNow, fName, labels, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
|
||||
public async Task SaveAnnotation(string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) =>
|
||||
await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
|
||||
|
||||
//Queue (only from operators)
|
||||
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
|
||||
@@ -65,7 +73,8 @@ public class AnnotationService
|
||||
await SaveAnnotationInner(
|
||||
message.CreatedDate,
|
||||
message.Name,
|
||||
YoloLabel.Deserialize(message.Label),
|
||||
message.ImageExtension,
|
||||
JsonConvert.DeserializeObject<List<Detection>>(message.Detections) ?? [],
|
||||
message.Source,
|
||||
new MemoryStream(message.Image),
|
||||
message.CreatedRole,
|
||||
@@ -73,7 +82,7 @@ public class AnnotationService
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task SaveAnnotationInner(DateTime createdDate, string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream,
|
||||
private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream,
|
||||
RoleEnum createdRole,
|
||||
string createdEmail,
|
||||
CancellationToken token = default)
|
||||
@@ -85,7 +94,7 @@ public class AnnotationService
|
||||
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
|
||||
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json
|
||||
|
||||
var classes = labels?.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
|
||||
var classes = detections.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
|
||||
AnnotationStatus status;
|
||||
|
||||
var annotation = await _dbFactory.Run(async db =>
|
||||
@@ -108,11 +117,12 @@ public class AnnotationService
|
||||
{
|
||||
CreatedDate = createdDate,
|
||||
Name = fName,
|
||||
Classes = classes,
|
||||
ImageExtension = imageExtension,
|
||||
CreatedEmail = createdEmail,
|
||||
CreatedRole = createdRole,
|
||||
AnnotationStatus = status,
|
||||
Source = source
|
||||
Source = source,
|
||||
Detections = detections
|
||||
};
|
||||
await db.InsertAsync(ann, token: token);
|
||||
}
|
||||
@@ -124,9 +134,10 @@ public class AnnotationService
|
||||
var img = System.Drawing.Image.FromStream(stream);
|
||||
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
||||
}
|
||||
if (labels != null)
|
||||
await YoloLabel.WriteToFile(labels, annotation.LabelPath, token);
|
||||
|
||||
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
||||
await _galleryService.CreateThumbnail(annotation, token);
|
||||
await _producer.SendToQueue(annotation, token);
|
||||
|
||||
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using LinqToDB;
|
||||
using MessagePack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using RabbitMQ.Stream.Client;
|
||||
using RabbitMQ.Stream.Client.Reliable;
|
||||
|
||||
@@ -28,14 +29,13 @@ public class FailsafeAnnotationsProducer
|
||||
_logger = logger;
|
||||
_dbFactory = dbFactory;
|
||||
_queueConfig = queueConfig.Value;
|
||||
Task.Run(async () => await ProcessQueue()).Wait();
|
||||
Task.Run(async () => await ProcessQueue());
|
||||
}
|
||||
|
||||
private async Task<StreamSystem> GetProducerQueueConfig()
|
||||
{
|
||||
return await StreamSystem.Create(new StreamSystemConfig
|
||||
{
|
||||
|
||||
Endpoints = new List<EndPoint> { new IPEndPoint(IPAddress.Parse(_queueConfig.Host), _queueConfig.Port) },
|
||||
UserName = _queueConfig.ProducerUsername,
|
||||
Password = _queueConfig.ProducerPassword
|
||||
@@ -45,7 +45,7 @@ public class FailsafeAnnotationsProducer
|
||||
private async Task Init(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE));
|
||||
//_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE));
|
||||
_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE));
|
||||
}
|
||||
|
||||
private async Task ProcessQueue(CancellationToken cancellationToken = default)
|
||||
@@ -98,7 +98,6 @@ public class FailsafeAnnotationsProducer
|
||||
foreach (var annotation in annotations)
|
||||
{
|
||||
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
|
||||
var label = await File.ReadAllTextAsync(annotation.LabelPath, cancellationToken);
|
||||
var annCreateMessage = new AnnotationCreatedMessage
|
||||
{
|
||||
Name = annotation.Name,
|
||||
@@ -108,7 +107,7 @@ public class FailsafeAnnotationsProducer
|
||||
CreatedDate = annotation.CreatedDate,
|
||||
|
||||
Image = image,
|
||||
Label = label,
|
||||
Detections = JsonConvert.SerializeObject(annotation.Detections),
|
||||
Source = annotation.Source
|
||||
};
|
||||
messages.Add(annCreateMessage);
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using Azaion.Annotator.Extensions;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.DTO.Queue;
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Color = System.Drawing.Color;
|
||||
using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions;
|
||||
using Size = System.Windows.Size;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage);
|
||||
|
||||
public class GalleryService(
|
||||
IOptions<DirectoriesConfig> directoriesConfig,
|
||||
IOptions<ThumbnailConfig> thumbnailConfig,
|
||||
IOptions<AnnotationConfig> annotationConfig,
|
||||
ILogger<GalleryService> logger,
|
||||
IDbFactory dbFactory) : IGalleryService
|
||||
{
|
||||
private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
|
||||
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
|
||||
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
|
||||
|
||||
public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
||||
|
||||
|
||||
private readonly SemaphoreSlim _updateLock = new(1);
|
||||
|
||||
public double ProcessedThumbnailsPercentage { get; set; }
|
||||
|
||||
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 async Task ClearThumbnails(CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
|
||||
file.Delete();
|
||||
await dbFactory.Run(async db =>
|
||||
{
|
||||
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
|
||||
await db.Annotations.DeleteAsync(x => true, token: cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task RefreshThumbnails()
|
||||
{
|
||||
await _updateLock.WaitAsync();
|
||||
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
|
||||
await db.Annotations.ToDictionaryAsync(x => x.Name)));
|
||||
var missedAnnotations = new ConcurrentBag<Annotation>();
|
||||
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();
|
||||
|
||||
var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
|
||||
var imagesCount = files.Length;
|
||||
|
||||
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
||||
{
|
||||
var fName = Path.GetFileNameWithoutExtension(file.Name);
|
||||
try
|
||||
{
|
||||
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
|
||||
if (!File.Exists(labelName))
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
|
||||
return;
|
||||
}
|
||||
|
||||
//Read labels file only when it needed
|
||||
if (existingAnnotations.ContainsKey(fName) && thumbnails.Contains(fName))
|
||||
return;
|
||||
|
||||
var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList();
|
||||
var annotation = new Annotation
|
||||
{
|
||||
Name = fName,
|
||||
ImageExtension = Path.GetExtension(file.Name),
|
||||
Detections = detections,
|
||||
CreatedDate = File.GetCreationTimeUtc(file.FullName),
|
||||
Source = SourceEnum.Manual,
|
||||
CreatedRole = RoleEnum.Validator,
|
||||
CreatedEmail = Constants.ADMIN_EMAIL,
|
||||
AnnotationStatus = AnnotationStatus.Validated
|
||||
};
|
||||
|
||||
if (!existingAnnotations.ContainsKey(fName))
|
||||
missedAnnotations.Add(annotation);
|
||||
|
||||
if (!thumbnails.Contains(fName))
|
||||
await CreateThumbnail(annotation, 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}");
|
||||
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
|
||||
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
|
||||
await Task.CompletedTask;
|
||||
},
|
||||
CpuUtilPercent = 100,
|
||||
ProgressUpdateInterval = 200
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
var copyOptions = new BulkCopyOptions
|
||||
{
|
||||
MaxBatchSize = 50
|
||||
};
|
||||
await dbFactory.Run(async db =>
|
||||
{
|
||||
var xx = missedAnnotations.GroupBy(x => x.Name)
|
||||
.Where(gr => gr.Count() > 1)
|
||||
.ToList();
|
||||
foreach (var gr in xx)
|
||||
Console.WriteLine(gr.Key);
|
||||
await db.BulkCopyAsync(copyOptions, missedAnnotations);
|
||||
await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections));
|
||||
});
|
||||
dbFactory.SaveToDisk();
|
||||
_updateLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var width = (int)_thumbnailConfig.Size.Width;
|
||||
var height = (int)_thumbnailConfig.Size.Height;
|
||||
|
||||
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, 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);
|
||||
|
||||
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;
|
||||
|
||||
var labels = annotation.Detections
|
||||
.Select(x => new CanvasLabel(x, size))
|
||||
.ToList();
|
||||
if (annotation.Detections.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.DetectionClassesDict[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);
|
||||
}
|
||||
|
||||
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IGalleryService
|
||||
{
|
||||
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
||||
double ProcessedThumbnailsPercentage { get; set; }
|
||||
Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default);
|
||||
Task RefreshThumbnails();
|
||||
Task ClearThumbnails(CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user