remove fix, todo: test

This commit is contained in:
Alex Bezdieniezhnykh
2025-01-03 18:32:56 +02:00
parent 9aebfd787b
commit ae2c62350a
19 changed files with 353 additions and 245 deletions
-18
View File
@@ -65,24 +65,6 @@ public class Constants
#endregion
public static TimeSpan? GetTime(string imagePath)
{
var timeStr = imagePath.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
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);
}
#region Queue
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
+15 -16
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);
}
@@ -154,7 +154,7 @@ public class CanvasEditor : Canvas
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{
if (SelectionState == SelectionState.NewAnnCreating)
CreateAnnotation(e.GetPosition(this));
CreateDetectionControl(e.GetPosition(this));
SelectionState = SelectionState.None;
e.Handled = true;
@@ -291,7 +291,7 @@ public class CanvasEditor : Canvas
SetTop(_newAnnotationRect, currentPos.Y);
}
private void CreateAnnotation(Point endPos)
private void CreateDetectionControl(Point endPos)
{
_newAnnotationRect.Width = 0;
_newAnnotationRect.Height = 0;
@@ -301,7 +301,7 @@ public class CanvasEditor : Canvas
return;
var time = GetTimeFunc();
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
{
Width = width,
Height = height,
@@ -310,20 +310,20 @@ public class CanvasEditor : Canvas
});
}
public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
public DetectionControl CreateDetectionControl(DetectionClass annClass, TimeSpan time, CanvasLabel canvasLabel)
{
var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
var detectionControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
{
Width = canvasLabel.Width,
Height = canvasLabel.Height
};
annotationControl.MouseDown += AnnotationPositionStart;
SetLeft(annotationControl, canvasLabel.X );
SetTop(annotationControl, canvasLabel.Y);
Children.Add(annotationControl);
CurrentDetections.Add(annotationControl);
detectionControl.MouseDown += AnnotationPositionStart;
SetLeft(detectionControl, canvasLabel.X );
SetTop(detectionControl, canvasLabel.Y);
Children.Add(detectionControl);
CurrentDetections.Add(detectionControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
return annotationControl;
return detectionControl;
}
#endregion
@@ -355,8 +355,7 @@ public class CanvasEditor : Canvas
public void ClearExpiredAnnotations(TimeSpan time)
{
var expiredAnns = CurrentDetections.Where(x =>
x.Time.HasValue &&
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList();
RemoveAnnotations(expiredAnns);
}
+2 -2
View File
@@ -16,7 +16,7 @@ public class DetectionControl : Border
private readonly Grid _grid;
private readonly TextBlock _classNameLabel;
private readonly Label _probabilityLabel;
public TimeSpan? Time { get; set; }
public TimeSpan Time { get; set; }
private DetectionClass _detectionClass = null!;
public DetectionClass DetectionClass
@@ -44,7 +44,7 @@ public class DetectionControl : Border
}
}
public DetectionControl(DetectionClass detectionClass, 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;
+33 -21
View File
@@ -1,40 +1,52 @@
using System.Windows.Media;
using Azaion.Common.Database;
using Azaion.Common.Extensions;
using Newtonsoft.Json;
namespace Azaion.Common.DTO;
public class AnnotationResult
{
[JsonProperty(PropertyName = "f")]
public string Image { get; set; } = null!;
public Annotation Annotation { get; set; }
[JsonProperty(PropertyName = "t")]
public TimeSpan Time { get; set; }
public string ImagePath { get; set; }
public string TimeStr { get; set; }
public double Lat { get; set; }
public double Lon { get; set; }
public List<Detection> Detections { get; set; } = new();
public string ClassName { get; set; }
#region For XAML Form
public Color ClassColor0 { get; set; }
public Color ClassColor1 { get; set; }
public Color ClassColor2 { get; set; }
public Color ClassColor3 { get; set; }
[JsonIgnore]
public string TimeStr => $"{Time:h\\:mm\\:ss}";
[JsonIgnore]
public string ClassName { get; set; } = null!;
public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
{
Annotation = annotation;
var detections = annotation.Detections.ToList();
[JsonIgnore]
public Color ClassColor0 { get; set; }
Color GetAnnotationClass(List<int> detectionClasses, int colorNumber)
{
if (detections.Count == 0)
return (-1).ToColor();
[JsonIgnore]
public Color ClassColor1 { get; set; }
return colorNumber >= detectionClasses.Count
? allDetectionClasses[detectionClasses.LastOrDefault()].Color
: allDetectionClasses[detectionClasses[colorNumber]].Color;
}
[JsonIgnore]
public Color ClassColor2 { get; set; }
TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
ImagePath = annotation.ImagePath;
[JsonIgnore]
public Color ClassColor3 { get; set; }
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
#endregion
ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].ShortName))
: allDetectionClasses[detectionClasses.FirstOrDefault()].Name;
ClassColor0 = GetAnnotationClass(detectionClasses, 0);
ClassColor1 = GetAnnotationClass(detectionClasses, 1);
ClassColor2 = GetAnnotationClass(detectionClasses, 2);
ClassColor3 = GetAnnotationClass(detectionClasses, 3);
}
}
@@ -7,7 +7,7 @@ using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged
public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
{
public Annotation Annotation { get; set; } = annotation;
@@ -30,13 +30,6 @@ public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged
public string ImageName => Path.GetFileName(Annotation.ImagePath);
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created;
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)
{
+24
View File
@@ -36,6 +36,30 @@ public class Annotation
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");
private TimeSpan? _time;
public TimeSpan Time
{
get
{
if (_time.HasValue)
return _time.Value;
var timeStr = Name.Split("_").LastOrDefault();
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!string.IsNullOrEmpty(timeStr) &&
timeStr.Length == 6 &&
int.TryParse(timeStr[..1], out var hours) &&
int.TryParse(timeStr[1..3], out var minutes) &&
int.TryParse(timeStr[3..5], out var seconds) &&
int.TryParse(timeStr[5..], out var milliseconds))
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
_time = TimeSpan.FromSeconds(0);
return _time.Value;
}
}
}
+13 -1
View File
@@ -17,6 +17,7 @@ public interface IDbFactory
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
Task Run(Func<AnnotationsDb, Task> func);
void SaveToDisk();
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
}
public class DbFactory : IDbFactory
@@ -41,7 +42,7 @@ public class DbFactory : IDbFactory
.UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_memoryConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
_ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
//.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
_fileConnection = new SQLiteConnection(FileConnStr);
@@ -96,6 +97,16 @@ public class DbFactory : IDbFactory
{
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
}
public async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default)
{
var names = annotations.Select(x => x.Name).ToList();
await Run(async db =>
{
await db.Detections.DeleteAsync(x => names.Contains(x.AnnotationName), token: cancellationToken);
await db.Annotations.DeleteAsync(x => names.Contains(x.Name), token: cancellationToken);
});
}
}
public static class AnnotationsDbSchemaHolder
@@ -110,6 +121,7 @@ public static class AnnotationsDbSchemaHolder
builder.Entity<Annotation>()
.HasTableName(Constants.ANNOTATIONS_TABLENAME)
.HasPrimaryKey(x => x.Name)
.Ignore(x => x.Time)
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName);
builder.Entity<Detection>()
@@ -1,7 +1,7 @@
using Azaion.Common.Database;
using MediatR;
namespace Azaion.Common.DTO;
namespace Azaion.Common.Events;
public class AnnotationCreatedEvent(Annotation annotation) : INotification
{
@@ -0,0 +1,9 @@
using Azaion.Common.Database;
using MediatR;
namespace Azaion.Common.Events;
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification
{
public List<Annotation> Annotations { get; set; } = annotations;
}
@@ -1,7 +1,8 @@
using System.Windows.Input;
using Azaion.Common.DTO;
using MediatR;
namespace Azaion.Common.DTO;
namespace Azaion.Common.Events;
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
{
+27 -6
View File
@@ -5,6 +5,7 @@ using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services;
@@ -19,7 +20,7 @@ using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services;
public class AnnotationService
public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
{
private readonly AzaionApiClient _apiClient;
private readonly IDbFactory _dbFactory;
@@ -83,12 +84,13 @@ public class AnnotationService
}
//AI / Manual
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);
public async Task<Annotation> 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, generateThumbnail: true, token);
//Manual
public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, annotation.Name, annotation.ImageExtension, annotation.Detections.ToList(), SourceEnum.Manual, null, _apiClient.User.Role, _apiClient.User.Email, token);
await SaveAnnotationInner(DateTime.UtcNow, annotation.Name, annotation.ImageExtension, annotation.Detections.ToList(), SourceEnum.Manual, null, _apiClient.User.Role, _apiClient.User.Email,
generateThumbnail: false, token);
//Queue (only from operators)
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
@@ -105,12 +107,14 @@ public class AnnotationService
new MemoryStream(message.Image),
message.CreatedRole,
message.CreatedEmail,
generateThumbnail: true,
cancellationToken);
}
private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream,
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole,
string createdEmail,
bool generateThumbnail = false,
CancellationToken token = default)
{
//Flow for roles:
@@ -129,11 +133,14 @@ public class AnnotationService
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
await db.BulkCopyAsync(detections, cancellationToken: token);
if (ann != null)
{
await db.Annotations
.Where(x => x.Name == fName)
.Set(x => x.Source, source)
.Set(x => x.AnnotationStatus, status)
.UpdateAsync(token: token);
ann.Detections = detections;
}
else
{
ann = new Annotation
@@ -158,7 +165,9 @@ public class AnnotationService
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
}
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
await _galleryService.CreateThumbnail(annotation, token);
if (generateThumbnail)
await _galleryService.CreateThumbnail(annotation, token);
await _producer.SendToQueue(annotation, token);
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
@@ -167,5 +176,17 @@ public class AnnotationService
_dbFactory.SaveToDisk();
return Task.CompletedTask;
}, TimeSpan.FromSeconds(5), token);
return annotation;
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{
await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken);
foreach (var annotation in notification.Annotations)
{
File.Delete(annotation.ImagePath);
File.Delete(annotation.LabelPath);
File.Delete(annotation.ThumbPath);
}
}
}