mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 06:46:30 +00:00
big refactoring. get rid of static properties and coupled architecture. prepare system for integration tests
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<Window x:Class="Azaion.Annotator.Annotator"
|
<Window x:Class="Azaion.Annotator.Annotator"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
@@ -256,7 +256,8 @@
|
|||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
CanUserResizeRows="False"
|
CanUserResizeRows="False"
|
||||||
CanUserResizeColumns="False"
|
CanUserResizeColumns="False"
|
||||||
RowStyleSelector="{StaticResource GradientStyleSelector}">
|
RowStyleSelector="{StaticResource GradientStyleSelector}"
|
||||||
|
local:GradientStyleSelector.ClassProvider="{Binding ClassProvider, RelativeSource={RelativeSource AncestorType=local:Annotator}}">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Width="60"
|
Width="60"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -63,6 +63,11 @@ public partial class Annotator
|
|||||||
public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig();
|
public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig();
|
||||||
private static readonly Guid ReloadTaskId = Guid.NewGuid();
|
private static readonly Guid ReloadTaskId = Guid.NewGuid();
|
||||||
|
|
||||||
|
private readonly IAnnotationPathResolver _pathResolver;
|
||||||
|
private readonly IDetectionClassProvider _classProvider;
|
||||||
|
|
||||||
|
public IDetectionClassProvider ClassProvider => _classProvider;
|
||||||
|
|
||||||
public Annotator(
|
public Annotator(
|
||||||
IConfigUpdater configUpdater,
|
IConfigUpdater configUpdater,
|
||||||
IOptions<AppConfig> appConfig,
|
IOptions<AppConfig> appConfig,
|
||||||
@@ -75,8 +80,12 @@ public partial class Annotator
|
|||||||
IDbFactory dbFactory,
|
IDbFactory dbFactory,
|
||||||
IInferenceService inferenceService,
|
IInferenceService inferenceService,
|
||||||
IInferenceClient inferenceClient,
|
IInferenceClient inferenceClient,
|
||||||
IGpsMatcherService gpsMatcherService)
|
IGpsMatcherService gpsMatcherService,
|
||||||
|
IAnnotationPathResolver pathResolver,
|
||||||
|
IDetectionClassProvider classProvider)
|
||||||
{
|
{
|
||||||
|
_pathResolver = pathResolver;
|
||||||
|
_classProvider = classProvider;
|
||||||
// Initialize configuration and services BEFORE InitializeComponent so bindings can see real values
|
// Initialize configuration and services BEFORE InitializeComponent so bindings can see real values
|
||||||
_appConfig = appConfig.Value;
|
_appConfig = appConfig.Value;
|
||||||
_configUpdater = configUpdater;
|
_configUpdater = configUpdater;
|
||||||
@@ -270,9 +279,10 @@ public partial class Annotator
|
|||||||
{
|
{
|
||||||
Dispatcher.Invoke(async () =>
|
Dispatcher.Invoke(async () =>
|
||||||
{
|
{
|
||||||
if (showImage && !annotation.IsSplit && File.Exists(annotation.ImagePath))
|
var imagePath = _pathResolver.GetImagePath(annotation);
|
||||||
|
if (showImage && !annotation.IsSplit && File.Exists(imagePath))
|
||||||
{
|
{
|
||||||
Editor.SetBackground(await annotation.ImagePath.OpenImage());
|
Editor.SetBackground(await imagePath.OpenImage());
|
||||||
_formState.BackgroundTime = annotation.Time;
|
_formState.BackgroundTime = annotation.Time;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,11 +658,30 @@ public partial class Annotator
|
|||||||
|
|
||||||
public class GradientStyleSelector : StyleSelector
|
public class GradientStyleSelector : StyleSelector
|
||||||
{
|
{
|
||||||
|
public static readonly DependencyProperty ClassProviderProperty = DependencyProperty.RegisterAttached(
|
||||||
|
"ClassProvider",
|
||||||
|
typeof(IDetectionClassProvider),
|
||||||
|
typeof(GradientStyleSelector),
|
||||||
|
new PropertyMetadata(null));
|
||||||
|
|
||||||
|
public static void SetClassProvider(DependencyObject element, IDetectionClassProvider value)
|
||||||
|
{
|
||||||
|
element.SetValue(ClassProviderProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IDetectionClassProvider GetClassProvider(DependencyObject element)
|
||||||
|
{
|
||||||
|
return (IDetectionClassProvider)element.GetValue(ClassProviderProperty);
|
||||||
|
}
|
||||||
|
|
||||||
public override Style? SelectStyle(object item, DependencyObject container)
|
public override Style? SelectStyle(object item, DependencyObject container)
|
||||||
{
|
{
|
||||||
if (container is not DataGridRow row || row.DataContext is not Annotation result)
|
if (container is not DataGridRow row || row.DataContext is not Annotation result)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
var dataGrid = FindParent<DataGrid>(row);
|
||||||
|
var classProvider = dataGrid != null ? GetClassProvider(dataGrid) : null;
|
||||||
|
|
||||||
var style = new Style(typeof(DataGridRow));
|
var style = new Style(typeof(DataGridRow));
|
||||||
var brush = new LinearGradientBrush
|
var brush = new LinearGradientBrush
|
||||||
{
|
{
|
||||||
@@ -661,16 +690,17 @@ public class GradientStyleSelector : StyleSelector
|
|||||||
};
|
};
|
||||||
|
|
||||||
var gradients = new List<GradientStop>();
|
var gradients = new List<GradientStop>();
|
||||||
if (result.Colors.Count == 0)
|
var colors = classProvider?.GetColors(result) ?? [];
|
||||||
|
if (colors.Count == 0)
|
||||||
{
|
{
|
||||||
var color = (Color)ColorConverter.ConvertFromString("#40DDDDDD");
|
var color = (Color)ColorConverter.ConvertFromString("#40DDDDDD");
|
||||||
gradients = [new GradientStop(color, 0.99)];
|
gradients = [new GradientStop(color, 0.99)];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var increment = 1.0 / result.Colors.Count;
|
var increment = 1.0 / colors.Count;
|
||||||
var currentStop = increment;
|
var currentStop = increment;
|
||||||
foreach (var c in result.Colors)
|
foreach (var c in colors)
|
||||||
{
|
{
|
||||||
var resultColor = c.Color.ToConfidenceColor(c.Confidence);
|
var resultColor = c.Color.ToConfidenceColor(c.Confidence);
|
||||||
brush.GradientStops.Add(new GradientStop(resultColor, currentStop));
|
brush.GradientStops.Add(new GradientStop(resultColor, currentStop));
|
||||||
@@ -683,4 +713,16 @@ public class GradientStyleSelector : StyleSelector
|
|||||||
style.Setters.Add(new Setter(Control.BackgroundProperty, brush));
|
style.Setters.Add(new Setter(Control.BackgroundProperty, brush));
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static T? FindParent<T>(DependencyObject child) where T : DependencyObject
|
||||||
|
{
|
||||||
|
var parent = VisualTreeHelper.GetParent(child);
|
||||||
|
while (parent != null)
|
||||||
|
{
|
||||||
|
if (parent is T typedParent)
|
||||||
|
return typedParent;
|
||||||
|
parent = VisualTreeHelper.GetParent(parent);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ public class AnnotatorEventHandler(
|
|||||||
IInferenceService inferenceService,
|
IInferenceService inferenceService,
|
||||||
IDbFactory dbFactory,
|
IDbFactory dbFactory,
|
||||||
IAzaionApi api,
|
IAzaionApi api,
|
||||||
FailsafeAnnotationsProducer producer)
|
FailsafeAnnotationsProducer producer,
|
||||||
|
IAnnotationPathResolver pathResolver,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IUICommandDispatcher uiDispatcher)
|
||||||
:
|
:
|
||||||
INotificationHandler<KeyEvent>,
|
INotificationHandler<KeyEvent>,
|
||||||
INotificationHandler<AnnClassSelectedEvent>,
|
INotificationHandler<AnnClassSelectedEvent>,
|
||||||
@@ -318,7 +321,7 @@ public class AnnotatorEventHandler(
|
|||||||
var annotationsResult = new List<Annotation>();
|
var annotationsResult = new List<Annotation>();
|
||||||
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||||
|
|
||||||
if (!File.Exists(imgPath))
|
if (!fileSystem.FileExists(imgPath))
|
||||||
{
|
{
|
||||||
if (mediaSize.FitSizeForAI())
|
if (mediaSize.FitSizeForAI())
|
||||||
await source.SaveImage(imgPath, ct);
|
await source.SaveImage(imgPath, ct);
|
||||||
@@ -339,7 +342,8 @@ public class AnnotatorEventHandler(
|
|||||||
{
|
{
|
||||||
var annotationName = $"{formState.CurrentMedia?.Name ?? ""}{Constants.SPLIT_SUFFIX}{res.Tile.Width}_{res.Tile.Left:0000}_{res.Tile.Top:0000}!".ToTimeName(time);
|
var annotationName = $"{formState.CurrentMedia?.Name ?? ""}{Constants.SPLIT_SUFFIX}{res.Tile.Width}_{res.Tile.Left:0000}_{res.Tile.Top:0000}!".ToTimeName(time);
|
||||||
|
|
||||||
var tileImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{annotationName}{Constants.JPG_EXT}");
|
var tempAnnotation = new Annotation { Name = annotationName, ImageExtension = Constants.JPG_EXT };
|
||||||
|
var tileImgPath = pathResolver.GetImagePath(tempAnnotation);
|
||||||
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.Left, (int)res.Tile.Top, (int)res.Tile.Width, (int)res.Tile.Height));
|
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.Left, (int)res.Tile.Top, (int)res.Tile.Width, (int)res.Tile.Height));
|
||||||
await bitmap.SaveImage(tileImgPath, ct);
|
await bitmap.SaveImage(tileImgPath, ct);
|
||||||
|
|
||||||
@@ -367,7 +371,7 @@ public class AnnotatorEventHandler(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
mainWindow.Dispatcher.Invoke(() =>
|
uiDispatcher.Execute(() =>
|
||||||
{
|
{
|
||||||
var namesSet = notification.AnnotationNames.ToHashSet();
|
var namesSet = notification.AnnotationNames.ToHashSet();
|
||||||
|
|
||||||
@@ -391,10 +395,11 @@ public class AnnotatorEventHandler(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}"));
|
var tempAnnotation = new Annotation { Name = name, ImageExtension = Constants.JPG_EXT };
|
||||||
File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}"));
|
fileSystem.DeleteFile(pathResolver.GetImagePath(tempAnnotation));
|
||||||
File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}"));
|
fileSystem.DeleteFile(pathResolver.GetLabelPath(tempAnnotation));
|
||||||
File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
|
fileSystem.DeleteFile(pathResolver.GetThumbPath(tempAnnotation));
|
||||||
|
fileSystem.DeleteFile(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -416,7 +421,7 @@ public class AnnotatorEventHandler(
|
|||||||
|
|
||||||
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
|
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
|
||||||
{
|
{
|
||||||
mainWindow.Dispatcher.Invoke(() =>
|
uiDispatcher.Execute(() =>
|
||||||
{
|
{
|
||||||
var mediaInfo = (MediaFile)mainWindow.LvFiles.SelectedItem;
|
var mediaInfo = (MediaFile)mainWindow.LvFiles.SelectedItem;
|
||||||
if ((mediaInfo?.Name ?? "") == e.Annotation.OriginalMediaName)
|
if ((mediaInfo?.Name ?? "") == e.Annotation.OriginalMediaName)
|
||||||
@@ -442,7 +447,7 @@ public class AnnotatorEventHandler(
|
|||||||
|
|
||||||
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
|
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
mainWindow.Dispatcher.Invoke(() =>
|
uiDispatcher.Execute(() =>
|
||||||
{
|
{
|
||||||
mainWindow.StatusHelp.Text = e.Text;
|
mainWindow.StatusHelp.Text = e.Text;
|
||||||
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
|
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
|
||||||
@@ -452,7 +457,7 @@ public class AnnotatorEventHandler(
|
|||||||
|
|
||||||
public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken)
|
public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
mainWindow.Dispatcher.Invoke(() =>
|
uiDispatcher.Execute(() =>
|
||||||
{
|
{
|
||||||
var ann = mainWindow.MapMatcherComponent.Annotations[e.Index];
|
var ann = mainWindow.MapMatcherComponent.Annotations[e.Index];
|
||||||
AddMarker(e.GeoPoint, e.Image, Brushes.Blue);
|
AddMarker(e.GeoPoint, e.Image, Brushes.Blue);
|
||||||
@@ -478,7 +483,7 @@ public class AnnotatorEventHandler(
|
|||||||
|
|
||||||
public async Task Handle(AIAvailabilityStatusEvent e, CancellationToken cancellationToken)
|
public async Task Handle(AIAvailabilityStatusEvent e, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
mainWindow.Dispatcher.Invoke(() =>
|
uiDispatcher.Execute(() =>
|
||||||
{
|
{
|
||||||
logger.LogInformation(e.ToString());
|
logger.LogInformation(e.ToString());
|
||||||
mainWindow.AIDetectBtn.IsEnabled = e.Status == AIAvailabilityEnum.Enabled;
|
mainWindow.AIDetectBtn.IsEnabled = e.Status == AIAvailabilityEnum.Enabled;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public class DetectionControl : Border
|
|||||||
_detectionLabelPanel = new DetectionLabelPanel
|
_detectionLabelPanel = new DetectionLabelPanel
|
||||||
{
|
{
|
||||||
Confidence = canvasLabel.Confidence,
|
Confidence = canvasLabel.Confidence,
|
||||||
DetectionClass = Annotation.DetectionClassesDict[canvasLabel.ClassNumber]
|
DetectionClass = detectionClass
|
||||||
};
|
};
|
||||||
|
|
||||||
DetectionLabelContainer.Children.Add(_detectionLabelPanel);
|
DetectionLabelContainer.Children.Add(_detectionLabelPanel);
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ using Azaion.Common.Extensions;
|
|||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged
|
public class AnnotationThumbnail(Annotation annotation, bool isValidator, string imagePath, string thumbPath) : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public Annotation Annotation { get; set; } = annotation;
|
public Annotation Annotation { get; set; } = annotation;
|
||||||
public bool IsValidator { get; set; } = isValidator;
|
public bool IsValidator { get; set; } = isValidator;
|
||||||
|
private readonly string _imagePath = imagePath;
|
||||||
|
private readonly string _thumbPath = thumbPath;
|
||||||
|
|
||||||
private BitmapImage? _thumbnail;
|
private BitmapImage? _thumbnail;
|
||||||
public BitmapImage? Thumbnail
|
public BitmapImage? Thumbnail
|
||||||
@@ -18,7 +20,9 @@ public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INot
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_thumbnail == null)
|
if (_thumbnail == null)
|
||||||
Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage());
|
{
|
||||||
|
Task.Run(async () => Thumbnail = await _thumbPath.OpenImage());
|
||||||
|
}
|
||||||
return _thumbnail;
|
return _thumbnail;
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
@@ -28,7 +32,7 @@ public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
public string ImageName => Path.GetFileName(_imagePath);
|
||||||
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
|
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
|
||||||
public string CreatedEmail => Annotation.CreatedEmail;
|
public string CreatedEmail => Annotation.CreatedEmail;
|
||||||
public bool IsSeed => IsValidator &&
|
public bool IsSeed => IsValidator &&
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
|
using Azaion.Common.Services;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO.Config;
|
namespace Azaion.Common.DTO.Config;
|
||||||
@@ -40,7 +41,16 @@ public interface IConfigUpdater
|
|||||||
|
|
||||||
public class ConfigUpdater : IConfigUpdater
|
public class ConfigUpdater : IConfigUpdater
|
||||||
{
|
{
|
||||||
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
|
private readonly IConfigurationStore _configStore;
|
||||||
|
|
||||||
|
public ConfigUpdater(IConfigurationStore configStore)
|
||||||
|
{
|
||||||
|
_configStore = configStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigUpdater() : this(new FileConfigurationStore(Constants.CONFIG_PATH, new PhysicalFileSystem()))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public void CheckConfig()
|
public void CheckConfig()
|
||||||
{
|
{
|
||||||
@@ -55,20 +65,6 @@ public class ConfigUpdater : IConfigUpdater
|
|||||||
|
|
||||||
public void Save(AppConfig config)
|
public void Save(AppConfig config)
|
||||||
{
|
{
|
||||||
ThrottleExt.Throttle(async () =>
|
_ = _configStore.SaveAsync(config);
|
||||||
{
|
|
||||||
var publicConfig = new
|
|
||||||
{
|
|
||||||
config.LoaderClientConfig,
|
|
||||||
config.InferenceClientConfig,
|
|
||||||
config.GpsDeniedClientConfig,
|
|
||||||
config.DirectoriesConfig,
|
|
||||||
config.UIConfig,
|
|
||||||
config.CameraConfig
|
|
||||||
};
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(Constants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
|
|
||||||
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,6 @@ namespace Azaion.Common.Database;
|
|||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class Annotation
|
public class Annotation
|
||||||
{
|
{
|
||||||
private static string _labelsDir = null!;
|
|
||||||
private static string _imagesDir = null!;
|
|
||||||
private static string _thumbDir = null!;
|
|
||||||
public static Dictionary<int, DetectionClass> DetectionClassesDict = null!;
|
|
||||||
|
|
||||||
public static void Init(DirectoriesConfig config, Dictionary<int, DetectionClass> detectionClassesDict)
|
|
||||||
{
|
|
||||||
_labelsDir = config.LabelsDirectory;
|
|
||||||
_imagesDir = config.ImagesDirectory;
|
|
||||||
_thumbDir = config.ThumbnailsDirectory;
|
|
||||||
DetectionClassesDict = detectionClassesDict;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Key("n")] public string Name { get; set; } = null!;
|
[Key("n")] public string Name { get; set; } = null!;
|
||||||
[Key("hash")] public string MediaHash { get; set; } = null!;
|
[Key("hash")] public string MediaHash { get; set; } = null!;
|
||||||
[Key("mn")] public string OriginalMediaName { get; set; } = null!;
|
[Key("mn")] public string OriginalMediaName { get; set; } = null!;
|
||||||
@@ -44,9 +31,6 @@ public class Annotation
|
|||||||
|
|
||||||
#region Calculated
|
#region Calculated
|
||||||
[IgnoreMember] public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
|
[IgnoreMember] public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
|
||||||
[IgnoreMember] public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
|
|
||||||
[IgnoreMember] public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
|
||||||
[IgnoreMember] public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
|
|
||||||
[IgnoreMember] public bool IsSplit => Name.Contains(Constants.SPLIT_SUFFIX);
|
[IgnoreMember] public bool IsSplit => Name.Contains(Constants.SPLIT_SUFFIX);
|
||||||
|
|
||||||
private CanvasLabel? _splitTile;
|
private CanvasLabel? _splitTile;
|
||||||
@@ -73,30 +57,7 @@ public class Annotation
|
|||||||
}
|
}
|
||||||
|
|
||||||
[IgnoreMember] public string TimeStr => $"{Time:h\\:mm\\:ss}";
|
[IgnoreMember] public string TimeStr => $"{Time:h\\:mm\\:ss}";
|
||||||
|
|
||||||
private List<(Color Color, double Confidence)>? _colors;
|
|
||||||
[IgnoreMember] public List<(Color Color, double Confidence)> Colors => _colors ??= Detections
|
|
||||||
.Select(d => (DetectionClassesDict[d.ClassNumber].Color, d.Confidence))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private string? _className;
|
|
||||||
[IgnoreMember] public string ClassName
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(_className))
|
|
||||||
{
|
|
||||||
var detectionClasses = Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
|
||||||
_className = detectionClasses.Count > 1
|
|
||||||
? string.Join(", ", detectionClasses.Select(x => DetectionClassesDict[x].UIName))
|
|
||||||
: DetectionClassesDict[detectionClasses.FirstOrDefault()].UIName;
|
|
||||||
}
|
|
||||||
return _className;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion Calculated
|
#endregion Calculated
|
||||||
|
|
||||||
public override string ToString() => $"Annotation: {Name}{TimeStr}: {ClassName}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using LinqToDB;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public class AnnotationRepository : IAnnotationRepository
|
||||||
|
{
|
||||||
|
private readonly IDbFactory _dbFactory;
|
||||||
|
|
||||||
|
public AnnotationRepository(IDbFactory dbFactory)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Annotation>> GetByMediaHashAsync(string hash, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbFactory.Run(async db =>
|
||||||
|
await db.GetTable<Annotation>()
|
||||||
|
.Where(x => x.MediaHash == hash)
|
||||||
|
.ToListAsync(ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Annotation?> GetByNameAsync(string name, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbFactory.Run(async db =>
|
||||||
|
await db.GetTable<Annotation>()
|
||||||
|
.FirstOrDefaultAsync(x => x.Name == name, ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Annotation>> GetAllAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await _dbFactory.Run(async db =>
|
||||||
|
await db.GetTable<Annotation>().ToListAsync(ct));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(Annotation annotation, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _dbFactory.RunWrite(async db =>
|
||||||
|
{
|
||||||
|
await db.InsertOrReplaceAsync(annotation, token: ct);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(List<string> names, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await _dbFactory.DeleteAnnotations(names, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -24,11 +24,6 @@ public static class AnnotationsDbSchemaHolder
|
|||||||
annotationBuilder
|
annotationBuilder
|
||||||
.Ignore(x => x.Milliseconds)
|
.Ignore(x => x.Milliseconds)
|
||||||
.Ignore(x => x.Classes)
|
.Ignore(x => x.Classes)
|
||||||
.Ignore(x => x.ImagePath)
|
|
||||||
.Ignore(x => x.LabelPath)
|
|
||||||
.Ignore(x => x.ThumbPath)
|
|
||||||
.Ignore(x => x.ClassName)
|
|
||||||
.Ignore(x => x.Colors)
|
|
||||||
.Ignore(x => x.SplitTile)
|
.Ignore(x => x.SplitTile)
|
||||||
.Ignore(x => x.IsSplit)
|
.Ignore(x => x.IsSplit)
|
||||||
.Ignore(x => x.TimeStr);
|
.Ignore(x => x.TimeStr);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public interface IAnnotationRepository
|
||||||
|
{
|
||||||
|
Task<List<Annotation>> GetByMediaHashAsync(string hash, CancellationToken ct = default);
|
||||||
|
Task<Annotation?> GetByNameAsync(string name, CancellationToken ct = default);
|
||||||
|
Task<List<Annotation>> GetAllAsync(CancellationToken ct = default);
|
||||||
|
Task SaveAsync(Annotation annotation, CancellationToken ct = default);
|
||||||
|
Task DeleteAsync(List<string> names, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Services;
|
||||||
|
using Azaion.Common.Services.Inference;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Infrastructure;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddAzaionInfrastructure(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IFileSystem, PhysicalFileSystem>();
|
||||||
|
services.AddSingleton<IProcessLauncher, ProcessLauncher>();
|
||||||
|
services.AddSingleton<IAnnotationPathResolver>(sp =>
|
||||||
|
new AnnotationPathResolver(sp.GetRequiredService<IOptions<DirectoriesConfig>>().Value));
|
||||||
|
services.AddSingleton<IDetectionClassProvider, DetectionClassProvider>();
|
||||||
|
services.AddSingleton<IAnnotationRepository, AnnotationRepository>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddAzaionConfiguration(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IConfigurationStore>(sp =>
|
||||||
|
new FileConfigurationStore(Constants.CONFIG_PATH, sp.GetRequiredService<IFileSystem>()));
|
||||||
|
services.AddSingleton<IConfigUpdater, ConfigUpdater>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddAzaionServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IDbFactory, DbFactory>();
|
||||||
|
services.AddSingleton<FailsafeAnnotationsProducer>();
|
||||||
|
services.AddSingleton<IAnnotationService, AnnotationService>();
|
||||||
|
services.AddSingleton<IGalleryService, GalleryService>();
|
||||||
|
services.AddSingleton<IInferenceService, InferenceService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class AnnotationPathResolver : IAnnotationPathResolver
|
||||||
|
{
|
||||||
|
private readonly string _labelsDir;
|
||||||
|
private readonly string _imagesDir;
|
||||||
|
private readonly string _thumbDir;
|
||||||
|
|
||||||
|
public AnnotationPathResolver(DirectoriesConfig config)
|
||||||
|
{
|
||||||
|
_labelsDir = config.LabelsDirectory;
|
||||||
|
_imagesDir = config.ImagesDirectory;
|
||||||
|
_thumbDir = config.ThumbnailsDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetImagePath(Annotation annotation) =>
|
||||||
|
Path.Combine(_imagesDir, $"{annotation.Name}{annotation.ImageExtension}");
|
||||||
|
|
||||||
|
public string GetLabelPath(Annotation annotation) =>
|
||||||
|
Path.Combine(_labelsDir, $"{annotation.Name}.txt");
|
||||||
|
|
||||||
|
public string GetThumbPath(Annotation annotation) =>
|
||||||
|
Path.Combine(_thumbDir, $"{annotation.Name}{Constants.THUMBNAIL_PREFIX}.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -33,6 +33,8 @@ public class AnnotationService : IAnnotationService
|
|||||||
private readonly QueueConfig _queueConfig;
|
private readonly QueueConfig _queueConfig;
|
||||||
private Consumer _consumer = null!;
|
private Consumer _consumer = null!;
|
||||||
private readonly UIConfig _uiConfig;
|
private readonly UIConfig _uiConfig;
|
||||||
|
private readonly IAnnotationPathResolver _pathResolver;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1);
|
||||||
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
|
||||||
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
|
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
|
||||||
@@ -46,7 +48,9 @@ public class AnnotationService : IAnnotationService
|
|||||||
IGalleryService galleryService,
|
IGalleryService galleryService,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IAzaionApi api,
|
IAzaionApi api,
|
||||||
ILogger<AnnotationService> logger)
|
ILogger<AnnotationService> logger,
|
||||||
|
IAnnotationPathResolver pathResolver,
|
||||||
|
IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_producer = producer;
|
_producer = producer;
|
||||||
@@ -56,8 +60,13 @@ public class AnnotationService : IAnnotationService
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_queueConfig = queueConfig.Value;
|
_queueConfig = queueConfig.Value;
|
||||||
_uiConfig = uiConfig.Value;
|
_uiConfig = uiConfig.Value;
|
||||||
|
_pathResolver = pathResolver;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
Task.Run(async () => await InitQueueConsumer()).Wait();
|
public async Task StartQueueConsumerAsync(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
await InitQueueConsumer(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitQueueConsumer(CancellationToken token = default)
|
private async Task InitQueueConsumer(CancellationToken token = default)
|
||||||
@@ -221,13 +230,14 @@ public class AnnotationService : IAnnotationService
|
|||||||
Image image = null!;
|
Image image = null!;
|
||||||
if (stream != null)
|
if (stream != null)
|
||||||
{
|
{
|
||||||
|
var imagePath = _pathResolver.GetImagePath(annotation);
|
||||||
image = Image.FromStream(stream);
|
image = Image.FromStream(stream);
|
||||||
if (File.Exists(annotation.ImagePath))
|
if (_fileSystem.FileExists(imagePath))
|
||||||
ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath));
|
ResilienceExt.WithRetry(() => _fileSystem.DeleteFile(imagePath));
|
||||||
image.Save(annotation.ImagePath, ImageFormat.Jpeg);
|
image.Save(imagePath, ImageFormat.Jpeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
await YoloLabel.WriteToFile(detections, _pathResolver.GetLabelPath(annotation), token);
|
||||||
|
|
||||||
await _galleryService.CreateThumbnail(annotation, image, token);
|
await _galleryService.CreateThumbnail(annotation, image, token);
|
||||||
if (_uiConfig.GenerateAnnotatedImage)
|
if (_uiConfig.GenerateAnnotatedImage)
|
||||||
@@ -235,7 +245,7 @@ public class AnnotationService : IAnnotationService
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}");
|
_logger.LogError(e, $"Try to save {_pathResolver.GetImagePath(annotation)}, Error: {e.Message}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -274,6 +284,7 @@ public class AnnotationService : IAnnotationService
|
|||||||
|
|
||||||
public interface IAnnotationService
|
public interface IAnnotationService
|
||||||
{
|
{
|
||||||
|
Task StartQueueConsumerAsync(CancellationToken token = default);
|
||||||
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
|
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
|
||||||
Task<Annotation> SaveAnnotation(string mediaHash, string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
|
Task<Annotation> SaveAnnotation(string mediaHash, string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
|
||||||
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
|
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class DetectionClassProvider : IDetectionClassProvider
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, DetectionClass> _detectionClasses;
|
||||||
|
|
||||||
|
public DetectionClassProvider(IOptions<AnnotationConfig> annotationConfig)
|
||||||
|
{
|
||||||
|
_detectionClasses = annotationConfig.Value.DetectionClassesDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<int, DetectionClass> GetDetectionClasses() => _detectionClasses;
|
||||||
|
|
||||||
|
public List<(Color Color, double Confidence)> GetColors(Annotation annotation)
|
||||||
|
{
|
||||||
|
return annotation.Detections
|
||||||
|
.Select(d => (_detectionClasses[d.ClassNumber].Color, d.Confidence))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetClassName(Annotation annotation)
|
||||||
|
{
|
||||||
|
var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
||||||
|
return detectionClasses.Count > 1
|
||||||
|
? string.Join(", ", detectionClasses.Select(x => _detectionClasses[x].UIName))
|
||||||
|
: _detectionClasses[detectionClasses.FirstOrDefault()].UIName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ public class FailsafeAnnotationsProducer
|
|||||||
private readonly IAzaionApi _azaionApi;
|
private readonly IAzaionApi _azaionApi;
|
||||||
private readonly QueueConfig _queueConfig;
|
private readonly QueueConfig _queueConfig;
|
||||||
private readonly UIConfig _uiConfig;
|
private readonly UIConfig _uiConfig;
|
||||||
|
private readonly IAnnotationPathResolver _pathResolver;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
private Producer _annotationProducer = null!;
|
private Producer _annotationProducer = null!;
|
||||||
|
|
||||||
@@ -31,14 +33,23 @@ public class FailsafeAnnotationsProducer
|
|||||||
IDbFactory dbFactory,
|
IDbFactory dbFactory,
|
||||||
IOptions<QueueConfig> queueConfig,
|
IOptions<QueueConfig> queueConfig,
|
||||||
IOptions<UIConfig> uiConfig,
|
IOptions<UIConfig> uiConfig,
|
||||||
IAzaionApi azaionApi)
|
IAzaionApi azaionApi,
|
||||||
|
IAnnotationPathResolver pathResolver,
|
||||||
|
IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_azaionApi = azaionApi;
|
_azaionApi = azaionApi;
|
||||||
_queueConfig = queueConfig.Value;
|
_queueConfig = queueConfig.Value;
|
||||||
_uiConfig = uiConfig.Value;
|
_uiConfig = uiConfig.Value;
|
||||||
Task.Run(async () => await ProcessQueue());
|
_pathResolver = pathResolver;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () => await ProcessQueue(ct), ct);
|
||||||
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<StreamSystem> GetProducerQueueConfig()
|
private async Task<StreamSystem> GetProducerQueueConfig()
|
||||||
@@ -104,7 +115,7 @@ public class FailsafeAnnotationsProducer
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var image = record.Operation == AnnotationStatus.Created
|
var image = record.Operation == AnnotationStatus.Created
|
||||||
? await File.ReadAllBytesAsync(annotation.ImagePath, ct)
|
? await _fileSystem.ReadAllBytesAsync(_pathResolver.GetImagePath(annotation), ct)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var annMessage = new AnnotationMessage
|
var annMessage = new AnnotationMessage
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class FileConfigurationStore : IConfigurationStore
|
||||||
|
{
|
||||||
|
private readonly string _configPath;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
|
||||||
|
|
||||||
|
public FileConfigurationStore(string configPath, IFileSystem fileSystem)
|
||||||
|
{
|
||||||
|
_configPath = configPath;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppConfig> LoadAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!_fileSystem.FileExists(_configPath))
|
||||||
|
return new AppConfig();
|
||||||
|
|
||||||
|
var json = Encoding.UTF8.GetString(await _fileSystem.ReadAllBytesAsync(_configPath, ct));
|
||||||
|
return JsonConvert.DeserializeObject<AppConfig>(json) ?? new AppConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveAsync(AppConfig config, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ThrottleExt.Throttle(async () =>
|
||||||
|
{
|
||||||
|
var publicConfig = new
|
||||||
|
{
|
||||||
|
config.LoaderClientConfig,
|
||||||
|
config.InferenceClientConfig,
|
||||||
|
config.GpsDeniedClientConfig,
|
||||||
|
config.DirectoriesConfig,
|
||||||
|
config.UIConfig,
|
||||||
|
config.CameraConfig
|
||||||
|
};
|
||||||
|
var json = JsonConvert.SerializeObject(publicConfig, Formatting.Indented);
|
||||||
|
await _fileSystem.WriteAllBytesAsync(_configPath, Encoding.UTF8.GetBytes(json), ct);
|
||||||
|
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -26,11 +26,15 @@ public class GalleryService(
|
|||||||
IOptions<ThumbnailConfig> thumbnailConfig,
|
IOptions<ThumbnailConfig> thumbnailConfig,
|
||||||
IOptions<AnnotationConfig> annotationConfig,
|
IOptions<AnnotationConfig> annotationConfig,
|
||||||
ILogger<GalleryService> logger,
|
ILogger<GalleryService> logger,
|
||||||
IDbFactory dbFactory) : IGalleryService
|
IDbFactory dbFactory,
|
||||||
|
IAnnotationPathResolver pathResolver,
|
||||||
|
IFileSystem fileSystem) : IGalleryService
|
||||||
{
|
{
|
||||||
private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
|
private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
|
||||||
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
|
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
|
||||||
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
|
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
|
||||||
|
private readonly IAnnotationPathResolver _pathResolver = pathResolver;
|
||||||
|
private readonly IFileSystem _fileSystem = fileSystem;
|
||||||
|
|
||||||
public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
||||||
|
|
||||||
@@ -58,8 +62,9 @@ public class GalleryService(
|
|||||||
|
|
||||||
public async Task ClearThumbnails(CancellationToken cancellationToken = default)
|
public async Task ClearThumbnails(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
|
var thumbDir = _fileSystem.GetDirectoryInfo(_dirConfig.ThumbnailsDirectory);
|
||||||
file.Delete();
|
foreach(var file in thumbDir.GetFiles())
|
||||||
|
_fileSystem.DeleteFile(file.FullName);
|
||||||
await dbFactory.RunWrite(async db =>
|
await dbFactory.RunWrite(async db =>
|
||||||
{
|
{
|
||||||
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
|
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
|
||||||
@@ -83,7 +88,8 @@ public class GalleryService(
|
|||||||
.Select(gr => gr.Key)
|
.Select(gr => gr.Key)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
|
var imagesDir = _fileSystem.GetDirectoryInfo(_dirConfig.ImagesDirectory);
|
||||||
|
var files = imagesDir.GetFiles();
|
||||||
var imagesCount = files.Length;
|
var imagesCount = files.Length;
|
||||||
|
|
||||||
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
||||||
@@ -92,9 +98,9 @@ public class GalleryService(
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
|
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
|
||||||
if (!File.Exists(labelName))
|
if (!_fileSystem.FileExists(labelName))
|
||||||
{
|
{
|
||||||
File.Delete(file.FullName);
|
_fileSystem.DeleteFile(file.FullName);
|
||||||
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
|
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
|
||||||
await dbFactory.DeleteAnnotations([fName], cancellationToken);
|
await dbFactory.DeleteAnnotations([fName], cancellationToken);
|
||||||
return;
|
return;
|
||||||
@@ -213,7 +219,7 @@ public class GalleryService(
|
|||||||
var width = (int)_thumbnailConfig.Size.Width;
|
var width = (int)_thumbnailConfig.Size.Width;
|
||||||
var height = (int)_thumbnailConfig.Size.Height;
|
var height = (int)_thumbnailConfig.Size.Height;
|
||||||
|
|
||||||
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
|
originalImage ??= Image.FromStream(new MemoryStream(await _fileSystem.ReadAllBytesAsync(_pathResolver.GetImagePath(annotation), cancellationToken)));
|
||||||
|
|
||||||
var bitmap = new Bitmap(width, height);
|
var bitmap = new Bitmap(width, height);
|
||||||
|
|
||||||
@@ -273,7 +279,7 @@ public class GalleryService(
|
|||||||
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.Left - frameX) / scale), (float)((label.Top - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.Left - frameX) / scale), (float)((label.Top - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
|
bitmap.Save(_pathResolver.GetThumbPath(annotation), ImageFormat.Jpeg);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -282,7 +288,7 @@ public class GalleryService(
|
|||||||
}
|
}
|
||||||
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
|
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
|
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(_pathResolver.GetImagePath(annotation), token)));
|
||||||
|
|
||||||
using var g = Graphics.FromImage(originalImage);
|
using var g = Graphics.FromImage(originalImage);
|
||||||
foreach (var detection in annotation.Detections)
|
foreach (var detection in annotation.Detections)
|
||||||
@@ -297,11 +303,11 @@ public class GalleryService(
|
|||||||
g.DrawTextBox(label, new PointF((float)(det.Left + det.Width / 2.0), (float)(det.Top - 24)), brush, Brushes.Black);
|
g.DrawTextBox(label, new PointF((float)(det.Left + det.Width / 2.0), (float)(det.Top - 24)), brush, Brushes.Black);
|
||||||
}
|
}
|
||||||
|
|
||||||
var imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
|
var resultPath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
|
||||||
if (File.Exists(imagePath))
|
if (_fileSystem.FileExists(resultPath))
|
||||||
ResilienceExt.WithRetry(() => File.Delete(imagePath));
|
ResilienceExt.WithRetry(() => _fileSystem.DeleteFile(resultPath));
|
||||||
|
|
||||||
originalImage.Save(imagePath, ImageFormat.Jpeg);
|
originalImage.Save(resultPath, ImageFormat.Jpeg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,27 +34,24 @@ public class GpsMatcherClient : IGpsMatcherClient
|
|||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ILogger<GpsMatcherClient> _logger;
|
private readonly ILogger<GpsMatcherClient> _logger;
|
||||||
|
private readonly IProcessLauncher _processLauncher;
|
||||||
private readonly string _requestAddress;
|
private readonly string _requestAddress;
|
||||||
private readonly RequestSocket _requestSocket = new();
|
private readonly RequestSocket _requestSocket = new();
|
||||||
private readonly string _subscriberAddress;
|
private readonly string _subscriberAddress;
|
||||||
private readonly SubscriberSocket _subscriberSocket = new();
|
private readonly SubscriberSocket _subscriberSocket = new();
|
||||||
private readonly NetMQPoller _poller = new();
|
private readonly NetMQPoller _poller = new();
|
||||||
|
|
||||||
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger)
|
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger, IProcessLauncher processLauncher)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_processLauncher = processLauncher;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var process = new Process();
|
_processLauncher.Launch(
|
||||||
process.StartInfo = new ProcessStartInfo
|
Constants.ExternalGpsDeniedPath,
|
||||||
{
|
$"zeromq --rep {gpsConfig.Value.ZeroMqPort} --pub {gpsConfig.Value.ZeroMqReceiverPort}",
|
||||||
FileName = Constants.ExternalGpsDeniedPath,
|
Constants.EXTERNAL_GPS_DENIED_FOLDER);
|
||||||
Arguments = $"zeromq --rep {gpsConfig.Value.ZeroMqPort} --pub {gpsConfig.Value.ZeroMqReceiverPort}",
|
|
||||||
WorkingDirectory = Constants.EXTERNAL_GPS_DENIED_FOLDER,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
process.Start();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Azaion.Common.Database;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IAnnotationPathResolver
|
||||||
|
{
|
||||||
|
string GetImagePath(Annotation annotation);
|
||||||
|
string GetLabelPath(Annotation annotation);
|
||||||
|
string GetThumbPath(Annotation annotation);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IConfigurationStore
|
||||||
|
{
|
||||||
|
Task<AppConfig> LoadAsync(CancellationToken ct = default);
|
||||||
|
Task SaveAsync(AppConfig config, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IDetectionClassProvider
|
||||||
|
{
|
||||||
|
Dictionary<int, DetectionClass> GetDetectionClasses();
|
||||||
|
List<(Color Color, double Confidence)> GetColors(Annotation annotation);
|
||||||
|
string GetClassName(Annotation annotation);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IFileSystem
|
||||||
|
{
|
||||||
|
Task<byte[]> ReadAllBytesAsync(string path, CancellationToken ct = default);
|
||||||
|
Task WriteAllBytesAsync(string path, byte[] content, CancellationToken ct = default);
|
||||||
|
bool FileExists(string path);
|
||||||
|
void DeleteFile(string path);
|
||||||
|
IEnumerable<string> GetFiles(string directory, string searchPattern);
|
||||||
|
IEnumerable<FileInfo> GetFileInfos(string directory, string[] searchPatterns);
|
||||||
|
DirectoryInfo GetDirectoryInfo(string path);
|
||||||
|
bool DirectoryExists(string path);
|
||||||
|
void CreateDirectory(string path);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IMediaPlayerService
|
||||||
|
{
|
||||||
|
void Play();
|
||||||
|
void Pause();
|
||||||
|
void Stop();
|
||||||
|
long Time { get; set; }
|
||||||
|
float Position { get; set; }
|
||||||
|
int Volume { get; set; }
|
||||||
|
bool IsPlaying { get; }
|
||||||
|
long Length { get; }
|
||||||
|
void SetMedia(string mediaPath);
|
||||||
|
void TakeSnapshot(uint num, string path, uint width, uint height);
|
||||||
|
|
||||||
|
event EventHandler? Playing;
|
||||||
|
event EventHandler? Paused;
|
||||||
|
event EventHandler? Stopped;
|
||||||
|
event EventHandler? PositionChanged;
|
||||||
|
event EventHandler? LengthChanged;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IProcessLauncher
|
||||||
|
{
|
||||||
|
void Launch(string fileName, string arguments, string? workingDirectory = null);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IUICommandDispatcher
|
||||||
|
{
|
||||||
|
Task ExecuteAsync(Action action);
|
||||||
|
Task<T> ExecuteAsync<T>(Func<T> func);
|
||||||
|
void Execute(Action action);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ public interface IInferenceClient : IDisposable
|
|||||||
public class InferenceClient : IInferenceClient
|
public class InferenceClient : IInferenceClient
|
||||||
{
|
{
|
||||||
private readonly ILogger<InferenceClient> _logger;
|
private readonly ILogger<InferenceClient> _logger;
|
||||||
|
private readonly IProcessLauncher _processLauncher;
|
||||||
|
private readonly IDetectionClassProvider _classProvider;
|
||||||
|
|
||||||
private readonly DealerSocket _dealer = new();
|
private readonly DealerSocket _dealer = new();
|
||||||
private readonly NetMQPoller _poller = new();
|
private readonly NetMQPoller _poller = new();
|
||||||
@@ -30,12 +32,16 @@ public class InferenceClient : IInferenceClient
|
|||||||
|
|
||||||
public InferenceClient(ILogger<InferenceClient> logger, IOptions<InferenceClientConfig> inferenceConfig,
|
public InferenceClient(ILogger<InferenceClient> logger, IOptions<InferenceClientConfig> inferenceConfig,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IOptions<LoaderClientConfig> loaderConfig)
|
IOptions<LoaderClientConfig> loaderConfig,
|
||||||
|
IProcessLauncher processLauncher,
|
||||||
|
IDetectionClassProvider classProvider)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_inferenceClientConfig = inferenceConfig.Value;
|
_inferenceClientConfig = inferenceConfig.Value;
|
||||||
_loaderClientConfig = loaderConfig.Value;
|
_loaderClientConfig = loaderConfig.Value;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_processLauncher = processLauncher;
|
||||||
|
_classProvider = classProvider;
|
||||||
Start();
|
Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,14 +49,9 @@ public class InferenceClient : IInferenceClient
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var process = new Process();
|
_processLauncher.Launch(
|
||||||
process.StartInfo = new ProcessStartInfo
|
Constants.EXTERNAL_INFERENCE_PATH,
|
||||||
{
|
$"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}");
|
||||||
FileName = Constants.EXTERNAL_INFERENCE_PATH,
|
|
||||||
Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}",
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
process.Start();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -77,7 +78,9 @@ public class InferenceClient : IInferenceClient
|
|||||||
{
|
{
|
||||||
case CommandType.InferenceData:
|
case CommandType.InferenceData:
|
||||||
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(remoteCommand.Data, cancellationToken: ct);
|
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(remoteCommand.Data, cancellationToken: ct);
|
||||||
_logger.LogInformation("Received command: {AnnotationImage}", annotationImage.ToString());
|
var className = _classProvider.GetClassName(annotationImage);
|
||||||
|
_logger.LogInformation("Received command: Annotation {Name} {Time} - {ClassName}",
|
||||||
|
annotationImage.Name, annotationImage.TimeStr, className);
|
||||||
await _mediator.Publish(new InferenceDataEvent(annotationImage), ct);
|
await _mediator.Publish(new InferenceDataEvent(annotationImage), ct);
|
||||||
break;
|
break;
|
||||||
case CommandType.InferenceStatus:
|
case CommandType.InferenceStatus:
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Azaion.Common.Services.Inference;
|
|||||||
|
|
||||||
public interface IInferenceService
|
public interface IInferenceService
|
||||||
{
|
{
|
||||||
|
Task StartAsync();
|
||||||
Task RunInference(List<string> mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default);
|
Task RunInference(List<string> mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default);
|
||||||
CancellationTokenSource InferenceCancelTokenSource { get; set; }
|
CancellationTokenSource InferenceCancelTokenSource { get; set; }
|
||||||
CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; }
|
CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; }
|
||||||
@@ -28,12 +29,17 @@ public class InferenceService : IInferenceService
|
|||||||
_client = client;
|
_client = client;
|
||||||
_azaionApi = azaionApi;
|
_azaionApi = azaionApi;
|
||||||
_aiConfigOptions = aiConfigOptions;
|
_aiConfigOptions = aiConfigOptions;
|
||||||
_ = Task.Run(async () => await CheckAIAvailabilityStatus());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CancellationTokenSource InferenceCancelTokenSource { get; set; } = new();
|
public CancellationTokenSource InferenceCancelTokenSource { get; set; } = new();
|
||||||
public CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; } = new();
|
public CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; } = new();
|
||||||
|
|
||||||
|
public async Task StartAsync()
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () => await CheckAIAvailabilityStatus());
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CheckAIAvailabilityStatus()
|
private async Task CheckAIAvailabilityStatus()
|
||||||
{
|
{
|
||||||
CheckAIAvailabilityTokenSource = new CancellationTokenSource();
|
CheckAIAvailabilityTokenSource = new CancellationTokenSource();
|
||||||
|
|||||||
@@ -10,27 +10,34 @@ using Exception = System.Exception;
|
|||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
public class LoaderClient(LoaderClientConfig config, ILogger logger, CancellationToken ct = default) : IDisposable
|
public class LoaderClient : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly LoaderClientConfig _config;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly CancellationToken _ct;
|
||||||
|
private readonly IProcessLauncher _processLauncher;
|
||||||
private readonly DealerSocket _dealer = new();
|
private readonly DealerSocket _dealer = new();
|
||||||
private readonly Guid _clientId = Guid.NewGuid();
|
private readonly Guid _clientId = Guid.NewGuid();
|
||||||
|
|
||||||
|
public LoaderClient(LoaderClientConfig config, ILogger logger, IProcessLauncher processLauncher, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
_processLauncher = processLauncher;
|
||||||
|
_ct = ct;
|
||||||
|
}
|
||||||
|
|
||||||
public void StartClient()
|
public void StartClient()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var process = new Process();
|
_processLauncher.Launch(
|
||||||
process.StartInfo = new ProcessStartInfo
|
Constants.EXTERNAL_LOADER_PATH,
|
||||||
{
|
$"--port {_config.ZeroMqPort} --api {_config.ApiUrl}");
|
||||||
FileName = Constants.EXTERNAL_LOADER_PATH,
|
|
||||||
Arguments = $"--port {config.ZeroMqPort} --api {config.ApiUrl}",
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
process.Start();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.Error(e, e.Message);
|
_logger.Error(e, e.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +45,7 @@ public class LoaderClient(LoaderClientConfig config, ILogger logger, Cancellatio
|
|||||||
public void Connect()
|
public void Connect()
|
||||||
{
|
{
|
||||||
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
|
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
|
||||||
_dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}");
|
_dealer.Connect($"tcp://{_config.ZeroMqHost}:{_config.ZeroMqPort}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Login(ApiCredentials credentials)
|
public void Login(ApiCredentials credentials)
|
||||||
@@ -63,11 +70,11 @@ public class LoaderClient(LoaderClientConfig config, ILogger logger, Cancellatio
|
|||||||
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
|
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
|
||||||
|
|
||||||
var tryNum = 0;
|
var tryNum = 0;
|
||||||
while (!ct.IsCancellationRequested && tryNum++ < retryCount)
|
while (!_ct.IsCancellationRequested && tryNum++ < retryCount)
|
||||||
{
|
{
|
||||||
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes))
|
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes))
|
||||||
continue;
|
continue;
|
||||||
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
|
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: _ct);
|
||||||
if (res.CommandType == CommandType.Error)
|
if (res.CommandType == CommandType.Error)
|
||||||
throw new Exception(res.Message);
|
throw new Exception(res.Message);
|
||||||
return res;
|
return res;
|
||||||
@@ -77,7 +84,7 @@ public class LoaderClient(LoaderClientConfig config, ILogger logger, Cancellatio
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
logger.Error(e, e.Message);
|
_logger.Error(e, e.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class NoOpProcessLauncher : IProcessLauncher
|
||||||
|
{
|
||||||
|
public void Launch(string fileName, string arguments, string? workingDirectory = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class PhysicalFileSystem : IFileSystem
|
||||||
|
{
|
||||||
|
public Task<byte[]> ReadAllBytesAsync(string path, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return File.ReadAllBytesAsync(path, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteAllBytesAsync(string path, byte[] content, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return File.WriteAllBytesAsync(path, content, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FileExists(string path)
|
||||||
|
{
|
||||||
|
return File.Exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(string path)
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetFiles(string directory, string searchPattern)
|
||||||
|
{
|
||||||
|
return Directory.GetFiles(directory, searchPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<FileInfo> GetFileInfos(string directory, string[] searchPatterns)
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(directory);
|
||||||
|
if (!dir.Exists)
|
||||||
|
return Enumerable.Empty<FileInfo>();
|
||||||
|
|
||||||
|
return searchPatterns.SelectMany(pattern => dir.GetFiles(pattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DirectoryInfo GetDirectoryInfo(string path)
|
||||||
|
{
|
||||||
|
return new DirectoryInfo(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DirectoryExists(string path)
|
||||||
|
{
|
||||||
|
return Directory.Exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateDirectory(string path)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class ProcessLauncher : IProcessLauncher
|
||||||
|
{
|
||||||
|
public void Launch(string fileName, string arguments, string? workingDirectory = null)
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = fileName,
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory ?? "",
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class WpfUICommandDispatcher : IUICommandDispatcher
|
||||||
|
{
|
||||||
|
private readonly Dispatcher _dispatcher;
|
||||||
|
|
||||||
|
public WpfUICommandDispatcher(Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(Action action)
|
||||||
|
{
|
||||||
|
return _dispatcher.InvokeAsync(action).Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> ExecuteAsync<T>(Func<T> func)
|
||||||
|
{
|
||||||
|
return _dispatcher.InvokeAsync(func).Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(Action action)
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ public partial class DatasetExplorer
|
|||||||
|
|
||||||
private readonly IAzaionApi _azaionApi;
|
private readonly IAzaionApi _azaionApi;
|
||||||
private readonly IConfigUpdater _configUpdater;
|
private readonly IConfigUpdater _configUpdater;
|
||||||
|
private readonly IAnnotationPathResolver _pathResolver;
|
||||||
|
|
||||||
public bool ThumbnailLoading { get; set; }
|
public bool ThumbnailLoading { get; set; }
|
||||||
public string CurrentFilter { get; set; } = "";
|
public string CurrentFilter { get; set; } = "";
|
||||||
@@ -51,7 +52,8 @@ public partial class DatasetExplorer
|
|||||||
IDbFactory dbFactory,
|
IDbFactory dbFactory,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IAzaionApi azaionApi,
|
IAzaionApi azaionApi,
|
||||||
IConfigUpdater configUpdater)
|
IConfigUpdater configUpdater,
|
||||||
|
IAnnotationPathResolver pathResolver)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_appConfig = appConfig.Value;
|
_appConfig = appConfig.Value;
|
||||||
@@ -61,6 +63,7 @@ public partial class DatasetExplorer
|
|||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_azaionApi = azaionApi;
|
_azaionApi = azaionApi;
|
||||||
_configUpdater = configUpdater;
|
_configUpdater = configUpdater;
|
||||||
|
_pathResolver = pathResolver;
|
||||||
|
|
||||||
ShowWithObjectsOnlyChBox.IsChecked = _appConfig.UIConfig.ShowDatasetWithDetectionsOnly;
|
ShowWithObjectsOnlyChBox.IsChecked = _appConfig.UIConfig.ShowDatasetWithDetectionsOnly;
|
||||||
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
|
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
|
||||||
@@ -196,7 +199,7 @@ public partial class DatasetExplorer
|
|||||||
ThumbnailsView.SelectedIndex = index;
|
ThumbnailsView.SelectedIndex = index;
|
||||||
|
|
||||||
var ann = CurrentAnnotation.Annotation;
|
var ann = CurrentAnnotation.Annotation;
|
||||||
var image = await ann.ImagePath.OpenImage();
|
var image = await _pathResolver.GetImagePath(ann).OpenImage();
|
||||||
ExplorerEditor.SetBackground(image);
|
ExplorerEditor.SetBackground(image);
|
||||||
SelectedAnnotationName.Text = ann.Name;
|
SelectedAnnotationName.Text = ann.Name;
|
||||||
SwitchTab(toEditor: true);
|
SwitchTab(toEditor: true);
|
||||||
@@ -264,7 +267,11 @@ public partial class DatasetExplorer
|
|||||||
var annThumbnails = _annotationsDict[ExplorerEditor.CurrentAnnClass!.YoloId]
|
var annThumbnails = _annotationsDict[ExplorerEditor.CurrentAnnClass!.YoloId]
|
||||||
.WhereIf(withDetectionsOnly, x => x.Value.Detections.Any())
|
.WhereIf(withDetectionsOnly, x => x.Value.Detections.Any())
|
||||||
.WhereIf(!string.IsNullOrEmpty(CurrentFilter), x => x.Key.Contains(CurrentFilter, StringComparison.CurrentCultureIgnoreCase))
|
.WhereIf(!string.IsNullOrEmpty(CurrentFilter), x => x.Key.Contains(CurrentFilter, StringComparison.CurrentCultureIgnoreCase))
|
||||||
.Select(x => new AnnotationThumbnail(x.Value, currentUser.Role.IsValidator()))
|
.Select(x => new AnnotationThumbnail(
|
||||||
|
x.Value,
|
||||||
|
currentUser.Role.IsValidator(),
|
||||||
|
_pathResolver.GetImagePath(x.Value),
|
||||||
|
_pathResolver.GetThumbPath(x.Value)))
|
||||||
.OrderBy(x => !x.IsSeed)
|
.OrderBy(x => !x.IsSeed)
|
||||||
.ThenByDescending(x =>x.Annotation.CreatedDate);
|
.ThenByDescending(x =>x.Annotation.CreatedDate);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ public class DatasetExplorerEventHandler(
|
|||||||
ILogger<DatasetExplorerEventHandler> logger,
|
ILogger<DatasetExplorerEventHandler> logger,
|
||||||
DatasetExplorer datasetExplorer,
|
DatasetExplorer datasetExplorer,
|
||||||
IAnnotationService annotationService,
|
IAnnotationService annotationService,
|
||||||
IAzaionApi azaionApi) :
|
IAzaionApi azaionApi,
|
||||||
|
IUICommandDispatcher uiDispatcher,
|
||||||
|
IAnnotationPathResolver pathResolver) :
|
||||||
INotificationHandler<KeyEvent>,
|
INotificationHandler<KeyEvent>,
|
||||||
INotificationHandler<DatasetExplorerControlEvent>,
|
INotificationHandler<DatasetExplorerControlEvent>,
|
||||||
INotificationHandler<AnnotationCreatedEvent>,
|
INotificationHandler<AnnotationCreatedEvent>,
|
||||||
@@ -121,7 +123,7 @@ public class DatasetExplorerEventHandler(
|
|||||||
|
|
||||||
public async Task Handle(AnnotationCreatedEvent notification, CancellationToken token)
|
public async Task Handle(AnnotationCreatedEvent notification, CancellationToken token)
|
||||||
{
|
{
|
||||||
await datasetExplorer.Dispatcher.Invoke(async () =>
|
await uiDispatcher.ExecuteAsync(async () =>
|
||||||
{
|
{
|
||||||
var annotation = notification.Annotation;
|
var annotation = notification.Annotation;
|
||||||
var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber;
|
var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber;
|
||||||
@@ -132,7 +134,11 @@ public class DatasetExplorerEventHandler(
|
|||||||
|
|
||||||
var index = 0;
|
var index = 0;
|
||||||
var currentUser = await azaionApi.GetCurrentUserAsync();
|
var currentUser = await azaionApi.GetCurrentUserAsync();
|
||||||
var annThumb = new AnnotationThumbnail(annotation, currentUser.Role.IsValidator());
|
var annThumb = new AnnotationThumbnail(
|
||||||
|
annotation,
|
||||||
|
currentUser.Role.IsValidator(),
|
||||||
|
pathResolver.GetImagePath(annotation),
|
||||||
|
pathResolver.GetThumbPath(annotation));
|
||||||
if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name))
|
if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name))
|
||||||
{
|
{
|
||||||
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
|
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
|
||||||
@@ -153,7 +159,7 @@ public class DatasetExplorerEventHandler(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
datasetExplorer.Dispatcher.Invoke(() =>
|
uiDispatcher.Execute(() =>
|
||||||
{
|
{
|
||||||
var annThumbs = datasetExplorer.SelectedAnnotationDict
|
var annThumbs = datasetExplorer.SelectedAnnotationDict
|
||||||
.Where(x => notification.AnnotationNames.Contains(x.Key))
|
.Where(x => notification.AnnotationNames.Contains(x.Key))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Azaion.Common.Extensions;
|
|||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
using Azaion.Common.Services.Inference;
|
using Azaion.Common.Services.Inference;
|
||||||
using Azaion.Dataset;
|
using Azaion.Dataset;
|
||||||
|
using Azaion.Suite.Services;
|
||||||
using CommandLine;
|
using CommandLine;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -98,8 +99,9 @@ public partial class App
|
|||||||
new ConfigUpdater().CheckConfig();
|
new ConfigUpdater().CheckConfig();
|
||||||
var initConfig = Constants.ReadInitConfig(Log.Logger);
|
var initConfig = Constants.ReadInitConfig(Log.Logger);
|
||||||
var apiDir = initConfig.DirectoriesConfig.ApiResourcesDirectory;
|
var apiDir = initConfig.DirectoriesConfig.ApiResourcesDirectory;
|
||||||
|
var processLauncher = new ProcessLauncher();
|
||||||
|
|
||||||
_loaderClient = new LoaderClient(initConfig.LoaderClientConfig, Log.Logger, _mainCTokenSource.Token);
|
_loaderClient = new LoaderClient(initConfig.LoaderClientConfig, Log.Logger, processLauncher, _mainCTokenSource.Token);
|
||||||
_loaderClient.StartClient();
|
_loaderClient.StartClient();
|
||||||
_loaderClient.Connect();
|
_loaderClient.Connect();
|
||||||
_loaderClient.Login(credentials);
|
_loaderClient.Login(credentials);
|
||||||
@@ -145,7 +147,16 @@ public partial class App
|
|||||||
services.AddSingleton<IAzaionApi>(azaionApi);
|
services.AddSingleton<IAzaionApi>(azaionApi);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
services.AddSingleton<IFileSystem, PhysicalFileSystem>();
|
||||||
|
services.AddSingleton<IConfigurationStore>(sp =>
|
||||||
|
new FileConfigurationStore(Constants.CONFIG_PATH, sp.GetRequiredService<IFileSystem>()));
|
||||||
services.AddSingleton<IConfigUpdater, ConfigUpdater>();
|
services.AddSingleton<IConfigUpdater, ConfigUpdater>();
|
||||||
|
services.AddSingleton<IProcessLauncher, ProcessLauncher>();
|
||||||
|
services.AddSingleton<IUICommandDispatcher>(_ =>
|
||||||
|
new WpfUICommandDispatcher(Application.Current.Dispatcher));
|
||||||
|
services.AddSingleton<IAnnotationPathResolver>(sp =>
|
||||||
|
new AnnotationPathResolver(sp.GetRequiredService<IOptions<Azaion.Common.DTO.DirectoriesConfig>>().Value));
|
||||||
|
services.AddSingleton<IDetectionClassProvider, DetectionClassProvider>();
|
||||||
services.AddSingleton<Annotator.Annotator>();
|
services.AddSingleton<Annotator.Annotator>();
|
||||||
services.AddSingleton<DatasetExplorer>();
|
services.AddSingleton<DatasetExplorer>();
|
||||||
services.AddSingleton<HelpWindow>();
|
services.AddSingleton<HelpWindow>();
|
||||||
@@ -155,16 +166,18 @@ public partial class App
|
|||||||
typeof(AnnotationService).Assembly));
|
typeof(AnnotationService).Assembly));
|
||||||
services.AddSingleton<LibVLC>(_ => new LibVLC("--no-osd", "--no-video-title-show", "--no-snapshot-preview"));
|
services.AddSingleton<LibVLC>(_ => new LibVLC("--no-osd", "--no-video-title-show", "--no-snapshot-preview"));
|
||||||
services.AddSingleton<FormState>();
|
services.AddSingleton<FormState>();
|
||||||
services.AddSingleton<MediaPlayer>(sp =>
|
services.AddSingleton<LibVLCSharp.Shared.MediaPlayer>(sp =>
|
||||||
{
|
{
|
||||||
var libVlc = sp.GetRequiredService<LibVLC>();
|
var libVlc = sp.GetRequiredService<LibVLC>();
|
||||||
return new MediaPlayer(libVlc);
|
return new LibVLCSharp.Shared.MediaPlayer(libVlc);
|
||||||
});
|
});
|
||||||
|
services.AddSingleton<IMediaPlayerService>(sp =>
|
||||||
|
new VlcMediaPlayerService(sp.GetRequiredService<LibVLCSharp.Shared.MediaPlayer>(), sp.GetRequiredService<LibVLC>()));
|
||||||
services.AddSingleton<AnnotatorEventHandler>();
|
services.AddSingleton<AnnotatorEventHandler>();
|
||||||
services.AddSingleton<IDbFactory, DbFactory>();
|
services.AddSingleton<IDbFactory, DbFactory>();
|
||||||
|
|
||||||
services.AddSingleton<FailsafeAnnotationsProducer>();
|
services.AddSingleton<FailsafeAnnotationsProducer>();
|
||||||
|
services.AddSingleton<IAnnotationRepository, AnnotationRepository>();
|
||||||
services.AddSingleton<IAnnotationService, AnnotationService>();
|
services.AddSingleton<IAnnotationService, AnnotationService>();
|
||||||
|
|
||||||
services.AddSingleton<DatasetExplorer>();
|
services.AddSingleton<DatasetExplorer>();
|
||||||
@@ -175,9 +188,6 @@ public partial class App
|
|||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
Annotation.Init(_host.Services.GetRequiredService<IOptions<DirectoriesConfig>>().Value,
|
|
||||||
_host.Services.GetRequiredService<IOptions<AnnotationConfig>>().Value.DetectionClassesDict);
|
|
||||||
|
|
||||||
_host.Services.GetRequiredService<DatasetExplorer>();
|
_host.Services.GetRequiredService<DatasetExplorer>();
|
||||||
|
|
||||||
_mediator = _host.Services.GetRequiredService<IMediator>();
|
_mediator = _host.Services.GetRequiredService<IMediator>();
|
||||||
@@ -186,6 +196,14 @@ public partial class App
|
|||||||
DispatcherUnhandledException += OnDispatcherUnhandledException;
|
DispatcherUnhandledException += OnDispatcherUnhandledException;
|
||||||
|
|
||||||
_host.Start();
|
_host.Start();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await _host.Services.GetRequiredService<IAnnotationService>().StartQueueConsumerAsync();
|
||||||
|
await _host.Services.GetRequiredService<FailsafeAnnotationsProducer>().StartAsync();
|
||||||
|
await _host.Services.GetRequiredService<IInferenceService>().StartAsync();
|
||||||
|
});
|
||||||
|
|
||||||
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new RoutedEventHandler(GlobalKeyHandler));
|
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new RoutedEventHandler(GlobalKeyHandler));
|
||||||
_host.Services.GetRequiredService<MainSuite>().Show();
|
_host.Services.GetRequiredService<MainSuite>().Show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Azaion.Common.Services;
|
||||||
|
using LibVLCSharp.Shared;
|
||||||
|
|
||||||
|
namespace Azaion.Suite.Services;
|
||||||
|
|
||||||
|
public class VlcMediaPlayerService : IMediaPlayerService
|
||||||
|
{
|
||||||
|
private readonly MediaPlayer _mediaPlayer;
|
||||||
|
private readonly LibVLC _libVlc;
|
||||||
|
|
||||||
|
public VlcMediaPlayerService(MediaPlayer mediaPlayer, LibVLC libVlc)
|
||||||
|
{
|
||||||
|
_mediaPlayer = mediaPlayer;
|
||||||
|
_libVlc = libVlc;
|
||||||
|
|
||||||
|
_mediaPlayer.Playing += (s, e) => Playing?.Invoke(s, e);
|
||||||
|
_mediaPlayer.Paused += (s, e) => Paused?.Invoke(s, e);
|
||||||
|
_mediaPlayer.Stopped += (s, e) => Stopped?.Invoke(s, e);
|
||||||
|
_mediaPlayer.PositionChanged += (s, e) => PositionChanged?.Invoke(s, e);
|
||||||
|
_mediaPlayer.LengthChanged += (s, e) => LengthChanged?.Invoke(s, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Play() => _mediaPlayer.Play();
|
||||||
|
public void Pause() => _mediaPlayer.Pause();
|
||||||
|
public void Stop() => _mediaPlayer.Stop();
|
||||||
|
|
||||||
|
public long Time
|
||||||
|
{
|
||||||
|
get => _mediaPlayer.Time;
|
||||||
|
set => _mediaPlayer.Time = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Position
|
||||||
|
{
|
||||||
|
get => _mediaPlayer.Position;
|
||||||
|
set => _mediaPlayer.Position = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Volume
|
||||||
|
{
|
||||||
|
get => _mediaPlayer.Volume;
|
||||||
|
set => _mediaPlayer.Volume = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPlaying => _mediaPlayer.IsPlaying;
|
||||||
|
public long Length => _mediaPlayer.Length;
|
||||||
|
|
||||||
|
public void SetMedia(string mediaPath)
|
||||||
|
{
|
||||||
|
using var media = new Media(_libVlc, mediaPath);
|
||||||
|
_mediaPlayer.Media = media;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeSnapshot(uint num, string path, uint width, uint height)
|
||||||
|
{
|
||||||
|
_mediaPlayer.TakeSnapshot(num, path, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? Playing;
|
||||||
|
public event EventHandler? Paused;
|
||||||
|
public event EventHandler? Stopped;
|
||||||
|
public event EventHandler? PositionChanged;
|
||||||
|
public event EventHandler? LengthChanged;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Azaion.Common.Services;
|
||||||
|
|
||||||
|
namespace Azaion.Test;
|
||||||
|
|
||||||
|
public class InMemoryFileSystem : IFileSystem
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, byte[]> _files = new();
|
||||||
|
private readonly HashSet<string> _directories = new();
|
||||||
|
|
||||||
|
public Task<byte[]> ReadAllBytesAsync(string path, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!_files.TryGetValue(path, out var content))
|
||||||
|
throw new FileNotFoundException($"File not found: {path}");
|
||||||
|
return Task.FromResult(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WriteAllBytesAsync(string path, byte[] content, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
_files[path] = content;
|
||||||
|
var directory = Path.GetDirectoryName(path);
|
||||||
|
if (directory != null)
|
||||||
|
_directories.Add(directory);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FileExists(string path)
|
||||||
|
{
|
||||||
|
return _files.ContainsKey(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteFile(string path)
|
||||||
|
{
|
||||||
|
_files.Remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetFiles(string directory, string searchPattern)
|
||||||
|
{
|
||||||
|
var pattern = searchPattern.Replace("*", ".*").Replace("?", ".");
|
||||||
|
var regex = new System.Text.RegularExpressions.Regex(pattern);
|
||||||
|
return _files.Keys.Where(f => Path.GetDirectoryName(f) == directory && regex.IsMatch(Path.GetFileName(f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<FileInfo> GetFileInfos(string directory, string[] searchPatterns)
|
||||||
|
{
|
||||||
|
var files = searchPatterns.SelectMany(pattern => GetFiles(directory, pattern));
|
||||||
|
return files.Select(f => new FileInfo(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DirectoryInfo GetDirectoryInfo(string path)
|
||||||
|
{
|
||||||
|
if (!_directories.Contains(path))
|
||||||
|
_directories.Add(path);
|
||||||
|
return new DirectoryInfo(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DirectoryExists(string path)
|
||||||
|
{
|
||||||
|
return _directories.Contains(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateDirectory(string path)
|
||||||
|
{
|
||||||
|
_directories.Add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Azaion.Common.Services;
|
||||||
|
|
||||||
|
namespace Azaion.Test;
|
||||||
|
|
||||||
|
public class SynchronousUICommandDispatcher : IUICommandDispatcher
|
||||||
|
{
|
||||||
|
public Task ExecuteAsync(Action action)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> ExecuteAsync<T>(Func<T> func)
|
||||||
|
{
|
||||||
|
return Task.FromResult(func());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(Action action)
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user