mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 21:26:31 +00:00
remove fix, todo: test
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
-1
@@ -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
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user