splitting python complete

This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-08-12 14:48:56 +03:00
parent fc6e5db795
commit ad782bcbaa
31 changed files with 834 additions and 369 deletions
+85 -97
View File
@@ -29,7 +29,7 @@ namespace Azaion.Annotator;
public partial class Annotator public partial class Annotator
{ {
private readonly AppConfig _appConfig; private readonly AppConfig _appConfig;
private readonly LibVLC _libVLC; private readonly LibVLC _libVlc;
private readonly MediaPlayer _mediaPlayer; private readonly MediaPlayer _mediaPlayer;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly FormState _formState; private readonly FormState _formState;
@@ -42,17 +42,17 @@ public partial class Annotator
private readonly IInferenceClient _inferenceClient; private readonly IInferenceClient _inferenceClient;
private bool _suspendLayout; private bool _suspendLayout;
private bool _gpsPanelVisible = false; private bool _gpsPanelVisible;
public readonly CancellationTokenSource MainCancellationSource = new(); private readonly CancellationTokenSource _mainCancellationSource = new();
public CancellationTokenSource DetectionCancellationSource = new(); public CancellationTokenSource DetectionCancellationSource = new();
public bool IsInferenceNow = false; private bool _isInferenceNow;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150);
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new(); public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new(); private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public Dictionary<string, MediaFileInfo> MediaFilesDict = new(); public Dictionary<string, MediaFileInfo> MediaFilesDict = new();
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new(); public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
@@ -61,7 +61,7 @@ public partial class Annotator
public Annotator( public Annotator(
IConfigUpdater configUpdater, IConfigUpdater configUpdater,
IOptions<AppConfig> appConfig, IOptions<AppConfig> appConfig,
LibVLC libVLC, LibVLC libVlc,
MediaPlayer mediaPlayer, MediaPlayer mediaPlayer,
IMediator mediator, IMediator mediator,
FormState formState, FormState formState,
@@ -78,7 +78,7 @@ public partial class Annotator
Title = MainTitle; Title = MainTitle;
_appConfig = appConfig.Value; _appConfig = appConfig.Value;
_configUpdater = configUpdater; _configUpdater = configUpdater;
_libVLC = libVLC; _libVlc = libVlc;
_mediaPlayer = mediaPlayer; _mediaPlayer = mediaPlayer;
_mediator = mediator; _mediator = mediator;
_formState = formState; _formState = formState;
@@ -91,7 +91,7 @@ public partial class Annotator
Loaded += OnLoaded; Loaded += OnLoaded;
Closed += OnFormClosed; Closed += OnFormClosed;
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator; Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
TbFolder.TextChanged += async (sender, args) => TbFolder.TextChanged += async (_, _) =>
{ {
if (!Path.Exists(TbFolder.Text)) if (!Path.Exists(TbFolder.Text))
return; return;
@@ -179,22 +179,8 @@ public partial class Annotator
VideoView.MediaPlayer = _mediaPlayer; VideoView.MediaPlayer = _mediaPlayer;
//On start playing media //On start playing media
_mediaPlayer.Playing += async (sender, args) => _mediaPlayer.Playing += (_, _) =>
{ {
if (_formState.CurrentMrl == _mediaPlayer.Media?.Mrl)
return; //already loaded all the info
await Dispatcher.Invoke(async () => await ReloadAnnotations());
//show image
if (_formState.CurrentMedia?.MediaType == MediaTypes.Image)
{
await Task.Delay(100); //wait to load the frame and set on pause
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time), showImage: true);
return;
}
_formState.CurrentMrl = _mediaPlayer.Media?.Mrl ?? "";
uint vw = 0, vh = 0; uint vw = 0, vh = 0;
_mediaPlayer.Size(0, ref vw, ref vh); _mediaPlayer.Size(0, ref vw, ref vh);
_formState.CurrentMediaSize = new Size(vw, vh); _formState.CurrentMediaSize = new Size(vw, vh);
@@ -211,12 +197,12 @@ public partial class Annotator
var selectedClass = args.DetectionClass; var selectedClass = args.DetectionClass;
Editor.CurrentAnnClass = selectedClass; Editor.CurrentAnnClass = selectedClass;
_mediator.Publish(new AnnClassSelectedEvent(selectedClass)); _mediator.Publish(new AnnClassSelectedEvent(selectedClass));
}; };
_mediaPlayer.PositionChanged += (o, args) => _mediaPlayer.PositionChanged += (_, _) =>
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time)); ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
VideoSlider.ValueChanged += (value, newValue) => VideoSlider.ValueChanged += (_, newValue) =>
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum); _mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
VideoSlider.KeyDown += (sender, args) => VideoSlider.KeyDown += (sender, args) =>
@@ -227,51 +213,49 @@ public partial class Annotator
DgAnnotations.MouseDoubleClick += (sender, args) => DgAnnotations.MouseDoubleClick += (sender, args) =>
{ {
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow; if (ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) is DataGridRow dgRow)
if (dgRow != null) OpenAnnotationResult((Annotation)dgRow.Item);
OpenAnnotationResult((AnnotationResult)dgRow!.Item);
}; };
DgAnnotations.KeyUp += async (sender, args) => DgAnnotations.KeyUp += async (_, args) =>
{ {
switch (args.Key) switch (args.Key)
{ {
case Key.Up:
case Key.Down: //cursor is already moved by system behaviour case Key.Down: //cursor is already moved by system behaviour
OpenAnnotationResult((AnnotationResult)DgAnnotations.SelectedItem); OpenAnnotationResult((Annotation)DgAnnotations.SelectedItem);
break; break;
case Key.Delete: case Key.Delete:
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question); var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result != MessageBoxResult.OK) if (result != MessageBoxResult.OK)
return; return;
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList(); var res = DgAnnotations.SelectedItems.Cast<Annotation>().ToList();
var annotationNames = res.Select(x => x.Annotation.Name).ToList(); var annotationNames = res.Select(x => x.Name).ToList();
await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames)); await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
break; break;
} }
}; };
Editor.Mediator = _mediator;
DgAnnotations.ItemsSource = _formState.AnnotationResults; DgAnnotations.ItemsSource = _formState.AnnotationResults;
} }
public void OpenAnnotationResult(AnnotationResult res) private void OpenAnnotationResult(Annotation ann)
{ {
_mediaPlayer.SetPause(true); _mediaPlayer.SetPause(true);
Editor.RemoveAllAnns(); if (!ann.IsSplit)
_mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds; Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)ann.Time.TotalMilliseconds;
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum; VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
Editor.ClearExpiredAnnotations(res.Annotation.Time); Editor.ClearExpiredAnnotations(ann.Time);
}); });
ShowAnnotation(res.Annotation, showImage: true); ShowAnnotation(ann, showImage: true, openResult: true);
} }
private void SaveUserSettings() private void SaveUserSettings()
{ {
@@ -284,7 +268,7 @@ public partial class Annotator
_configUpdater.Save(_appConfig); _configUpdater.Save(_appConfig);
} }
private void ShowTimeAnnotations(TimeSpan time, bool showImage = false) public void ShowTimeAnnotations(TimeSpan time, bool showImage = false)
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
@@ -292,60 +276,68 @@ public partial class Annotator
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
Editor.ClearExpiredAnnotations(time); Editor.ClearExpiredAnnotations(time);
}); });
var annotation = TimedAnnotations.Query(time).FirstOrDefault(); var annotations = TimedAnnotations.Query(time).ToList();
if (annotation != null) ShowAnnotation(annotation, showImage); if (!annotations.Any())
return;
foreach (var ann in annotations)
ShowAnnotation(ann, showImage);
} }
private void ShowAnnotation(Annotation annotation, bool showImage = false) private void ShowAnnotation(Annotation annotation, bool showImage = false, bool openResult = false)
{ {
Dispatcher.Invoke(async () => Dispatcher.Invoke(async () =>
{ {
if (showImage) if (showImage && !annotation.IsSplit && File.Exists(annotation.ImagePath))
{ {
if (File.Exists(annotation.ImagePath)) Editor.SetBackground(await annotation.ImagePath.OpenImage());
{ _formState.BackgroundTime = annotation.Time;
Editor.SetBackground(await annotation.ImagePath.OpenImage());
_formState.BackgroundTime = annotation.Time;
}
} }
Editor.CreateDetections(annotation.Time, annotation.Detections, _appConfig.AnnotationConfig.DetectionClasses, _formState.CurrentMediaSize);
if (annotation.SplitTile != null && openResult)
{
var canvasTileLocation = new CanvasLabel(new YoloLabel(annotation.SplitTile, _formState.CurrentMediaSize),
RenderSize);
Editor.ZoomTo(new Point(canvasTileLocation.CenterX, canvasTileLocation.CenterY));
}
else
Editor.CreateDetections(annotation, _appConfig.AnnotationConfig.DetectionClasses, _formState.CurrentMediaSize);
}); });
} }
private async Task ReloadAnnotations() public async Task ReloadAnnotations()
{ {
_formState.AnnotationResults.Clear(); await Dispatcher.InvokeAsync(async () =>
TimedAnnotations.Clear();
Editor.RemoveAllAnns();
var annotations = await _dbFactory.Run(async db =>
await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.OriginalMediaName == _formState.MediaName)
.OrderBy(x => x.Time)
.ToListAsync(token: MainCancellationSource.Token));
TimedAnnotations.Clear();
_formState.AnnotationResults.Clear();
foreach (var ann in annotations)
{ {
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann); _formState.AnnotationResults.Clear();
_formState.AnnotationResults.Add(new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, ann)); TimedAnnotations.Clear();
} Editor.RemoveAllAnns();
var annotations = await _dbFactory.Run(async db =>
await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.OriginalMediaName == _formState.MediaName)
.OrderBy(x => x.Time)
.ToListAsync(token: _mainCancellationSource.Token));
TimedAnnotations.Clear();
_formState.AnnotationResults.Clear();
foreach (var ann in annotations)
{
// Duplicate for speed
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
_formState.AnnotationResults.Add(ann);
}
});
} }
//Add manually //Add manually
public void AddAnnotation(Annotation annotation) public void AddAnnotation(Annotation annotation)
{ {
var mediaInfo = (MediaFileInfo)LvFiles.SelectedItem;
if ((mediaInfo?.FName ?? "") != annotation.OriginalMediaName)
return;
var time = annotation.Time; var time = annotation.Time;
var previousAnnotations = TimedAnnotations.Query(time); var previousAnnotations = TimedAnnotations.Query(time);
TimedAnnotations.Remove(previousAnnotations); TimedAnnotations.Remove(previousAnnotations);
TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation); TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Annotation.Time == time); var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
if (existingResult != null) if (existingResult != null)
{ {
try try
@@ -360,16 +352,14 @@ public partial class Annotator
} }
var dict = _formState.AnnotationResults var dict = _formState.AnnotationResults
.Select((x, i) => new { x.Annotation.Time, Index = i }) .Select((x, i) => new { x.Time, Index = i })
.ToDictionary(x => x.Time, x => x.Index); .ToDictionary(x => x.Time, x => x.Index);
var index = dict.Where(x => x.Key < time) var index = dict.Where(x => x.Key < time)
.OrderBy(x => time - x.Key) .OrderBy(x => time - x.Key)
.Select(x => x.Value + 1) .Select(x => x.Value + 1)
.FirstOrDefault(); .FirstOrDefault();
_formState.AnnotationResults.Insert(index, annotation);
var annRes = new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, annotation);
_formState.AnnotationResults.Insert(index, annRes);
} }
private async Task ReloadFiles() private async Task ReloadFiles()
@@ -380,7 +370,7 @@ public partial class Annotator
var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x => var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x =>
{ {
using var media = new Media(_libVLC, x.FullName); var media = new Media(_libVlc, x.FullName);
media.Parse(); media.Parse();
var fInfo = new MediaFileInfo var fInfo = new MediaFileInfo
{ {
@@ -403,14 +393,16 @@ public partial class Annotator
var allFileNames = allFiles.Select(x => x.FName).ToList(); var allFileNames = allFiles.Select(x => x.FName).ToList();
var labelsDict = await _dbFactory.Run(async db => await db.Annotations var labelsDict = await _dbFactory.Run(async db =>
.GroupBy(x => x.Name.Substring(0, x.Name.Length - 7)) await db.Annotations
.GroupBy(x => x.OriginalMediaName)
.Where(x => allFileNames.Contains(x.Key)) .Where(x => allFileNames.Contains(x.Key))
.ToDictionaryAsync(x => x.Key, x => x.Key)); .Select(x => x.Key)
.ToDictionaryAsync(x => x, x => x));
foreach (var mediaFile in allFiles) foreach (var mediaFile in allFiles)
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles); AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name) MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name)
.ToDictionary(gr => gr.Key, gr => gr.First()); .ToDictionary(gr => gr.Key, gr => gr.First());
@@ -420,13 +412,13 @@ public partial class Annotator
private void OnFormClosed(object? sender, EventArgs e) private void OnFormClosed(object? sender, EventArgs e)
{ {
MainCancellationSource.Cancel(); _mainCancellationSource.Cancel();
_inferenceService.StopInference(); _inferenceService.StopInference();
DetectionCancellationSource.Cancel(); DetectionCancellationSource.Cancel();
_mediaPlayer.Stop(); _mediaPlayer.Stop();
_mediaPlayer.Dispose(); _mediaPlayer.Dispose();
_libVLC.Dispose(); _libVlc.Dispose();
} }
private void OpenContainingFolder(object sender, RoutedEventArgs e) private void OpenContainingFolder(object sender, RoutedEventArgs e)
@@ -447,13 +439,10 @@ public partial class Annotator
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
} }
private void SeekTo(TimeSpan time) => private void OpenFolderItemClick(object sender, RoutedEventArgs e) => OpenFolder();
SeekTo((long)time.TotalMilliseconds); private void OpenFolderButtonClick(object sender, RoutedEventArgs e) => OpenFolder();
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder(); private void OpenFolder()
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
private async Task OpenFolder()
{ {
var dlg = new CommonOpenFileDialog var dlg = new CommonOpenFileDialog
{ {
@@ -468,7 +457,6 @@ public partial class Annotator
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
TbFolder.Text = dlg.FileName; TbFolder.Text = dlg.FileName;
await Task.CompletedTask;
} }
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
@@ -525,7 +513,7 @@ public partial class Annotator
public async Task AutoDetect() public async Task AutoDetect()
{ {
if (IsInferenceNow) if (_isInferenceNow)
return; return;
if (LvFiles.Items.IsEmpty) if (LvFiles.Items.IsEmpty)
@@ -535,7 +523,7 @@ public partial class Annotator
Dispatcher.Invoke(() => Editor.SetBackground(null)); Dispatcher.Invoke(() => Editor.SetBackground(null));
IsInferenceNow = true; _isInferenceNow = true;
AIDetectBtn.IsEnabled = false; AIDetectBtn.IsEnabled = false;
DetectionCancellationSource = new CancellationTokenSource(); DetectionCancellationSource = new CancellationTokenSource();
@@ -550,7 +538,7 @@ public partial class Annotator
await _inferenceService.RunInference(files, DetectionCancellationSource.Token); await _inferenceService.RunInference(files, DetectionCancellationSource.Token);
LvFiles.Items.Refresh(); LvFiles.Items.Refresh();
IsInferenceNow = false; _isInferenceNow = false;
StatusHelp.Text = "Розпізнавання зваершено"; StatusHelp.Text = "Розпізнавання зваершено";
AIDetectBtn.IsEnabled = true; AIDetectBtn.IsEnabled = true;
} }
@@ -596,7 +584,7 @@ public class GradientStyleSelector : StyleSelector
{ {
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 AnnotationResult result) if (container is not DataGridRow row || row.DataContext is not Annotation result)
return null; return null;
var style = new Style(typeof(DataGridRow)); var style = new Style(typeof(DataGridRow));
+36 -27
View File
@@ -23,7 +23,7 @@ using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public class AnnotatorEventHandler( public class AnnotatorEventHandler(
LibVLC libVLC, LibVLC libVlc,
MediaPlayer mediaPlayer, MediaPlayer mediaPlayer,
Annotator mainWindow, Annotator mainWindow,
FormState formState, FormState formState,
@@ -47,8 +47,7 @@ public class AnnotatorEventHandler(
{ {
private const int STEP = 20; private const int STEP = 20;
private const int LARGE_STEP = 5000; private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280; private readonly string _tempImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, "___temp___.jpg");
private readonly string tempImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, "___temp___.jpg");
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new() private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{ {
@@ -144,8 +143,8 @@ public class AnnotatorEventHandler(
if (mediaPlayer.IsPlaying) if (mediaPlayer.IsPlaying)
{ {
mediaPlayer.Pause(); mediaPlayer.Pause();
mediaPlayer.TakeSnapshot(0, tempImgPath, 0, 0); mediaPlayer.TakeSnapshot(0, _tempImgPath, 0, 0);
mainWindow.Editor.SetBackground(await tempImgPath.OpenImage()); mainWindow.Editor.SetBackground(await _tempImgPath.OpenImage());
formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time); formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time);
} }
else else
@@ -238,16 +237,21 @@ public class AnnotatorEventHandler(
return; return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem; var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
if (formState.CurrentMedia == mediaInfo)
return; //already loaded
formState.CurrentMedia = mediaInfo; formState.CurrentMedia = mediaInfo;
mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}"; mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}";
await mainWindow.ReloadAnnotations();
if (mediaInfo.MediaType == MediaTypes.Video) if (mediaInfo.MediaType == MediaTypes.Video)
{ {
mainWindow.Editor.SetBackground(null); mainWindow.Editor.SetBackground(null);
//need to wait a bit for correct VLC playback event handling //need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct); await Task.Delay(100, ct);
mediaPlayer.Stop(); mediaPlayer.Stop();
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); mediaPlayer.Play(new Media(libVlc, mediaInfo.Path));
} }
else else
{ {
@@ -256,6 +260,7 @@ public class AnnotatorEventHandler(
formState.CurrentMediaSize = new Size(image.PixelWidth, image.PixelHeight); formState.CurrentMediaSize = new Size(image.PixelWidth, image.PixelHeight);
mainWindow.Editor.SetBackground(image); mainWindow.Editor.SetBackground(image);
mediaPlayer.Stop(); mediaPlayer.Stop();
mainWindow.ShowTimeAnnotations(TimeSpan.Zero, showImage: true);
} }
} }
@@ -282,13 +287,14 @@ public class AnnotatorEventHandler(
// var annGrid = mainWindow.DgAnnotations; // var annGrid = mainWindow.DgAnnotations;
// annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1); // annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
// mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem); // mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
} }
else else
{ {
await NextMedia(ct: cancellationToken); await NextMedia(ct: cancellationToken);
} }
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
mainWindow.LvFiles.Items.Refresh(); mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns(); mainWindow.Editor.RemoveAllAnns();
@@ -301,7 +307,7 @@ public class AnnotatorEventHandler(
if (!File.Exists(imgPath)) if (!File.Exists(imgPath))
{ {
var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!; var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!;
if (source.PixelWidth <= RESULT_WIDTH * 2 && source.PixelHeight <= RESULT_WIDTH * 2) // Allow to be up to 2560*2560 to save to 1280*1280 if (source.PixelWidth <= Constants.AI_TILE_SIZE * 2 && source.PixelHeight <= Constants.AI_TILE_SIZE * 2) // Allow to be up to 2560*2560 to save to 1280*1280
{ {
//Save image //Save image
await using var stream = new FileStream(imgPath, FileMode.Create); await using var stream = new FileStream(imgPath, FileMode.Create);
@@ -314,28 +320,28 @@ public class AnnotatorEventHandler(
{ {
//Tiling //Tiling
//1. Restore original picture coordinates //1. Convert from RenderSize to CurrentMediaSize
var pictureCoordinatesDetections = canvasDetections.Select(x => new CanvasLabel( var detectionCoords = canvasDetections.Select(x => new CanvasLabel(
new YoloLabel(x, mainWindow.Editor.RenderSize, formState.CurrentMediaSize), formState.CurrentMediaSize, null, x.Confidence)) new YoloLabel(x, mainWindow.Editor.RenderSize, formState.CurrentMediaSize), formState.CurrentMediaSize, null, x.Confidence))
.ToList(); .ToList();
//2. Split to 1280*1280 frames //2. Split to frames
var results = TileProcessor.Split(formState.CurrentMediaSize, pictureCoordinatesDetections, cancellationToken); var results = TileProcessor.Split(formState.CurrentMediaSize, detectionCoords, cancellationToken);
//3. Save each frame as a separate annotation //3. Save each frame as a separate annotation
BitmapEncoder tileEncoder = new JpegBitmapEncoder();
foreach (var res in results) foreach (var res in results)
{ {
var mediaName = $"{formState.MediaName}!split!{res.Tile.X}_{res.Tile.Y}!";
var time = TimeSpan.Zero; var time = TimeSpan.Zero;
var annotationName = mediaName.ToTimeName(time); var annotationName = $"{formState.MediaName}{Constants.SPLIT_SUFFIX}{res.Tile.Left:0000}_{res.Tile.Top:0000}!".ToTimeName(time);
var tileImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{annotationName}{Constants.JPG_EXT}"); var tileImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{annotationName}{Constants.JPG_EXT}");
await using var tileStream = new FileStream(tileImgPath, FileMode.Create); await using var tileStream = new FileStream(tileImgPath, FileMode.Create);
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.X, (int)res.Tile.Y, (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));
tileEncoder.Frames.Add(BitmapFrame.Create(bitmap));
var tileEncoder = new JpegBitmapEncoder { Frames = [BitmapFrame.Create(bitmap)] };
tileEncoder.Save(tileStream); tileEncoder.Save(tileStream);
await tileStream.FlushAsync(cancellationToken); await tileStream.FlushAsync(cancellationToken);
tileStream.Close();
var frameSize = new Size(res.Tile.Width, res.Tile.Height); var frameSize = new Size(res.Tile.Width, res.Tile.Height);
var detections = res.Detections var detections = res.Detections
@@ -343,18 +349,18 @@ public class AnnotatorEventHandler(
.Select(x => new Detection(annotationName, new YoloLabel(x, frameSize))) .Select(x => new Detection(annotationName, new YoloLabel(x, frameSize)))
.ToList(); .ToList();
annotationsResult.Add(await annotationService.SaveAnnotation(mediaName, time, detections, token: cancellationToken)); annotationsResult.Add(await annotationService.SaveAnnotation(formState.MediaName, annotationName, time, detections, token: cancellationToken));
} }
return annotationsResult; return annotationsResult;
} }
} }
var timeImg = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time); var timeImg = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var timeName = formState.MediaName.ToTimeName(timeImg); var annName = formState.MediaName.ToTimeName(timeImg);
var currentDetections = canvasDetections.Select(x => var currentDetections = canvasDetections.Select(x =>
new Detection(timeName, new YoloLabel(x, mainWindow.Editor.RenderSize))) new Detection(annName, new YoloLabel(x, mainWindow.Editor.RenderSize)))
.ToList(); .ToList();
var annotation = await annotationService.SaveAnnotation(formState.MediaName, timeImg, currentDetections, token: cancellationToken); var annotation = await annotationService.SaveAnnotation(formState.MediaName, annName, timeImg, currentDetections, token: cancellationToken);
return [annotation]; return [annotation];
} }
@@ -367,15 +373,15 @@ public class AnnotatorEventHandler(
var namesSet = notification.AnnotationNames.ToHashSet(); var namesSet = notification.AnnotationNames.ToHashSet();
var remainAnnotations = formState.AnnotationResults var remainAnnotations = formState.AnnotationResults
.Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList(); .Where(x => !namesSet.Contains(x.Name)).ToList();
formState.AnnotationResults.Clear(); formState.AnnotationResults.Clear();
foreach (var ann in remainAnnotations) foreach (var ann in remainAnnotations)
formState.AnnotationResults.Add(ann); formState.AnnotationResults.Add(ann);
var timedAnnsToRemove = mainWindow.TimedAnnotations var timedAnnotationsToRemove = mainWindow.TimedAnnotations
.Where(x => namesSet.Contains(x.Value.Name)) .Where(x => namesSet.Contains(x.Value.Name))
.Select(x => x.Value).ToList(); .Select(x => x.Value).ToList();
mainWindow.TimedAnnotations.Remove(timedAnnsToRemove); mainWindow.TimedAnnotations.Remove(timedAnnotationsToRemove);
if (formState.AnnotationResults.Count == 0) if (formState.AnnotationResults.Count == 0)
{ {
@@ -420,7 +426,10 @@ public class AnnotatorEventHandler(
{ {
mainWindow.Dispatcher.Invoke(() => mainWindow.Dispatcher.Invoke(() =>
{ {
mainWindow.AddAnnotation(e.Annotation);
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
if ((mediaInfo?.FName ?? "") == e.Annotation.OriginalMediaName)
mainWindow.AddAnnotation(e.Annotation);
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det => var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " + $"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
+1
View File
@@ -4,6 +4,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<LangVersion>12</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+45 -43
View File
@@ -9,13 +9,15 @@ using System.Windows;
namespace Azaion.Common; namespace Azaion.Common;
public class Constants public static class Constants
{ {
public const string CONFIG_PATH = "config.json"; public const string CONFIG_PATH = "config.json";
public const string LOADER_CONFIG_PATH = "loaderconfig.json"; public const string LOADER_CONFIG_PATH = "loaderconfig.json";
public const string DEFAULT_API_URL = "https://api.azaion.com"; public const string DEFAULT_API_URL = "https://api.azaion.com";
public const string AZAION_SUITE_EXE = "Azaion.Suite.exe"; public const string AZAION_SUITE_EXE = "Azaion.Suite.exe";
public const int AI_TILE_SIZE = 1280;
#region ExternalClientsConfig #region ExternalClientsConfig
private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1"; private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1";
@@ -27,11 +29,11 @@ public class Constants
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe"); public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1"; public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227; private const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1"; private const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5255; private const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5255;
public const int DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT = 5256; private const int DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT = 5256;
#endregion ExternalClientsConfig #endregion ExternalClientsConfig
@@ -42,41 +44,33 @@ public class Constants
# endregion # endregion
public const string JPG_EXT = ".jpg"; public const string JPG_EXT = ".jpg";
public const string TXT_EXT = ".txt"; public const string TXT_EXT = ".txt";
#region DirectoriesConfig #region DirectoriesConfig
public const string DEFAULT_VIDEO_DIR = "video"; private const string DEFAULT_VIDEO_DIR = "video";
public const string DEFAULT_LABELS_DIR = "labels"; private const string DEFAULT_LABELS_DIR = "labels";
public const string DEFAULT_IMAGES_DIR = "images"; private const string DEFAULT_IMAGES_DIR = "images";
public const string DEFAULT_RESULTS_DIR = "results"; private const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails"; private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
public const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir"; private const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
public const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir"; private const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
#endregion #endregion
#region AnnotatorConfig #region AnnotatorConfig
public static readonly AnnotationConfig DefaultAnnotationConfig = new()
{
DetectionClasses = DefaultAnnotationClasses!,
VideoFormats = DefaultVideoFormats!,
ImageFormats = DefaultImageFormats!,
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
};
private static readonly List<DetectionClass> DefaultAnnotationClasses = private static readonly List<DetectionClass> DefaultAnnotationClasses =
[ [
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() }, new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() },
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() }, new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() },
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() }, new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() },
new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() }, new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor() },
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() }, new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() },
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() }, new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() },
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() }, new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() },
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() }, new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() },
new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() }, new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor() },
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() }, new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() },
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() }, new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() },
new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() }, new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() },
@@ -86,20 +80,28 @@ public class Constants
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() }, new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() },
new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor() }, new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor() },
]; ];
private static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
private static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"]; private static readonly AnnotationConfig DefaultAnnotationConfig = new()
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; {
DetectionClasses = DefaultAnnotationClasses,
VideoFormats = DefaultVideoFormats,
ImageFormats = DefaultImageFormats,
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
};
private const int DEFAULT_LEFT_PANEL_WIDTH = 250;
private const int DEFAULT_RIGHT_PANEL_WIDTH = 250;
public static int DEFAULT_LEFT_PANEL_WIDTH = 250; private const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
# endregion AnnotatorConfig # endregion AnnotatorConfig
# region AIRecognitionConfig # region AIRecognitionConfig
public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new() private static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
{ {
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS, FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE, TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
@@ -109,18 +111,18 @@ public class Constants
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
}; };
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2; private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15; private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15; private const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8; private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public const int DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT = 20; private const int DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT = 20;
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4; private const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
# endregion AIRecognitionConfig # endregion AIRecognitionConfig
# region GpsDeniedConfig # region GpsDeniedConfig
public static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new() private static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new()
{ {
MinKeyPoints = 11 MinKeyPoints = 11
}; };
@@ -129,15 +131,15 @@ public class Constants
#region Thumbnails #region Thumbnails
public static readonly ThumbnailConfig DefaultThumbnailConfig = new() private static readonly Size DefaultThumbnailSize = new(240, 135);
private static readonly ThumbnailConfig DefaultThumbnailConfig = new()
{ {
Size = DefaultThumbnailSize, Size = DefaultThumbnailSize,
Border = DEFAULT_THUMBNAIL_BORDER Border = DEFAULT_THUMBNAIL_BORDER
}; };
public static readonly Size DefaultThumbnailSize = new(240, 135); private const int DEFAULT_THUMBNAIL_BORDER = 10;
public const int DEFAULT_THUMBNAIL_BORDER = 10;
public const string THUMBNAIL_PREFIX = "_thumb"; public const string THUMBNAIL_PREFIX = "_thumb";
public const string RESULT_PREFIX = "_result"; public const string RESULT_PREFIX = "_result";
@@ -163,10 +165,10 @@ public class Constants
#endregion #endregion
public const string CSV_PATH = "matches.csv"; public const string SPLIT_SUFFIX = "!split!";
public static readonly InitConfig DefaultInitConfig = new() private static readonly InitConfig DefaultInitConfig = new()
{ {
LoaderClientConfig = new LoaderClientConfig LoaderClientConfig = new LoaderClientConfig
{ {
+30 -11
View File
@@ -5,6 +5,7 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Shapes; using System.Windows.Shapes;
using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.Events; using Azaion.Common.Events;
using MediatR; using MediatR;
@@ -39,7 +40,6 @@ public class CanvasEditor : Canvas
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400); private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
public Image BackgroundImage { get; set; } = new() { Stretch = Stretch.Uniform }; public Image BackgroundImage { get; set; } = new() { Stretch = Stretch.Uniform };
public IMediator Mediator { get; set; } = null!;
public static readonly DependencyProperty GetTimeFuncProp = public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register( DependencyProperty.Register(
@@ -191,7 +191,6 @@ public class CanvasEditor : Canvas
private void CanvasMouseMove(object sender, MouseEventArgs e) private void CanvasMouseMove(object sender, MouseEventArgs e)
{ {
var pos = e.GetPosition(this); var pos = e.GetPosition(this);
Mediator.Publish(new SetStatusTextEvent($"Mouse Coordinates: {pos.X}, {pos.Y}"));
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y; _horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
_verticalLine.X1 = _verticalLine.X2 = pos.X; _verticalLine.X1 = _verticalLine.X2 = pos.X;
SetLeft(_classNameHint, pos.X + 10); SetLeft(_classNameHint, pos.X + 10);
@@ -223,7 +222,6 @@ public class CanvasEditor : Canvas
matrix.Translate(delta.X, delta.Y); matrix.Translate(delta.X, delta.Y);
_matrixTransform.Matrix = matrix; _matrixTransform.Matrix = matrix;
Mediator.Publish(new SetStatusTextEvent(_matrixTransform.Matrix.ToString()));
} }
private void CanvasMouseUp(object sender, MouseButtonEventArgs e) private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
@@ -243,8 +241,8 @@ public class CanvasEditor : Canvas
{ {
Width = width, Width = width,
Height = height, Height = height,
X = Math.Min(endPos.X, _newAnnotationStartPos.X), Left = Math.Min(endPos.X, _newAnnotationStartPos.X),
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y), Top = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
Confidence = 1 Confidence = 1
}); });
control.UpdateLayout(); control.UpdateLayout();
@@ -415,13 +413,26 @@ public class CanvasEditor : Canvas
SetTop(_newAnnotationRect, currentPos.Y); SetTop(_newAnnotationRect, currentPos.Y);
} }
public void CreateDetections(TimeSpan time, IEnumerable<Detection> detections, List<DetectionClass> detectionClasses, Size videoSize) public void CreateDetections(Annotation annotation, List<DetectionClass> detectionClasses, Size mediaSize)
{ {
foreach (var detection in detections) var splitTile = annotation.SplitTile;
foreach (var detection in annotation.Detections)
{ {
var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses); var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
var canvasLabel = new CanvasLabel(detection, RenderSize, videoSize, detection.Confidence); CanvasLabel canvasLabel;
CreateDetectionControl(detectionClass, time, canvasLabel); if (splitTile == null)
canvasLabel = new CanvasLabel(detection, RenderSize, mediaSize, detection.Confidence);
else
{
canvasLabel = new CanvasLabel(detection, new Size(Constants.AI_TILE_SIZE, Constants.AI_TILE_SIZE), null, detection.Confidence)
.ReframeFromSmall(splitTile);
//From CurrentMediaSize to Render Size
var yoloLabel = new YoloLabel(canvasLabel, mediaSize);
canvasLabel = new CanvasLabel(yoloLabel, RenderSize, mediaSize, canvasLabel.Confidence);
}
CreateDetectionControl(detectionClass, annotation.Time, canvasLabel);
} }
} }
@@ -429,8 +440,8 @@ public class CanvasEditor : Canvas
{ {
var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel); var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
detectionControl.MouseDown += AnnotationPositionStart; detectionControl.MouseDown += AnnotationPositionStart;
SetLeft(detectionControl, canvasLabel.X ); SetLeft(detectionControl, canvasLabel.Left );
SetTop(detectionControl, canvasLabel.Y); SetTop(detectionControl, canvasLabel.Top);
Children.Add(detectionControl); Children.Add(detectionControl);
CurrentDetections.Add(detectionControl); CurrentDetections.Add(detectionControl);
_newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color); _newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
@@ -472,4 +483,12 @@ public class CanvasEditor : Canvas
} }
public void ResetBackground() => Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); public void ResetBackground() => Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
public void ZoomTo(Point point)
{
SetZoom();
var matrix = _matrixTransform.Matrix;
matrix.ScaleAt(2, 2, point.X, point.Y);
SetZoom(matrix);
}
} }
+2 -2
View File
@@ -30,7 +30,7 @@ public class DetectionControl : Border
{ {
var brush = new SolidColorBrush(value.Color.ToConfidenceColor()); var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
BorderBrush = brush; BorderBrush = brush;
BorderThickness = new Thickness(3); BorderThickness = new Thickness(1);
foreach (var rect in _resizedRectangles) foreach (var rect in _resizedRectangles)
rect.Stroke = brush; rect.Stroke = brush;
@@ -141,7 +141,7 @@ public class DetectionControl : Border
var rect = new Rectangle() // small rectangles at the corners and sides var rect = new Rectangle() // small rectangles at the corners and sides
{ {
ClipToBounds = false, ClipToBounds = false,
Margin = new Thickness(-RESIZE_RECT_SIZE), Margin = new Thickness(-1.1 * RESIZE_RECT_SIZE),
HorizontalAlignment = ha, HorizontalAlignment = ha,
VerticalAlignment = va, VerticalAlignment = va,
Width = RESIZE_RECT_SIZE, Width = RESIZE_RECT_SIZE,
+25 -23
View File
@@ -3,31 +3,33 @@ using Azaion.Common.Database;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationResult // public class AnnotationResult
{ //{
public Annotation Annotation { get; set; } //public Annotation Annotation { get; set; }
public List<(Color Color, double Confidence)> Colors { get; private set; }
public string ImagePath { get; set; } //public string ImagePath { get; set; }
public string TimeStr { get; set; } //public string TimeStr { get; set; }
public string ClassName { get; set; }
//public List<(Color Color, double Confidence)> Colors { get; private set; }
// public string ClassName { get; set; }
public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation) // public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
{ // {
Annotation = annotation; //Annotation = annotation;
TimeStr = $"{annotation.Time:h\\:mm\\:ss}"; //TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
ImagePath = annotation.ImagePath; //ImagePath = annotation.ImagePath;
var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList(); // var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
// ClassName = detectionClasses.Count > 1
Colors = annotation.Detections // ? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
.Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence)) // : allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
.ToList(); //
// Colors = annotation.Detections
ClassName = detectionClasses.Count > 1 // .Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName)) // .ToList();
: allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
} // }
} // }
+3 -3
View File
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Windows; using System.Windows;
using Azaion.Common.Database;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
@@ -7,13 +8,12 @@ public class FormState
{ {
public MediaFileInfo? CurrentMedia { get; set; } public MediaFileInfo? CurrentMedia { get; set; }
public string MediaName => CurrentMedia?.FName ?? ""; public string MediaName => CurrentMedia?.FName ?? "";
public string CurrentMrl { get; set; } = null!;
public Size CurrentMediaSize { get; set; } public Size CurrentMediaSize { get; set; }
public TimeSpan CurrentVideoLength { get; set; } public TimeSpan CurrentVideoLength { get; set; }
public TimeSpan? BackgroundTime { get; set; } public TimeSpan? BackgroundTime { get; set; }
public int CurrentVolume { get; set; } = 100; public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = []; public ObservableCollection<Annotation> AnnotationResults { get; set; } = [];
public WindowEnum ActiveWindow { get; set; } public WindowEnum ActiveWindow { get; set; }
} }
+31 -27
View File
@@ -22,52 +22,56 @@ public abstract class Label
public class CanvasLabel : Label public class CanvasLabel : Label
{ {
public double X { get; set; } //left public double Left { get; set; }
public double Y { get; set; } //top public double Top { get; set; }
public double Width { get; set; } public double Width { get; set; }
public double Height { get; set; } public double Height { get; set; }
public double Confidence { get; set; } public double Confidence { get; set; }
public double Bottom public double Bottom
{ {
get => Y + Height; get => Top + Height;
set => Height = value - Y; set => Height = value - Top;
} }
public double Right public double Right
{ {
get => X + Width; get => Left + Width;
set => Width = value - X; set => Width = value - Left;
} }
public double CenterX => Left + Width / 2.0;
public double CenterY => Top + Height / 2.0;
public Size Size => new(Width, Height);
public CanvasLabel() { } public CanvasLabel() { }
public CanvasLabel(double left, double right, double top, double bottom) public CanvasLabel(double left, double right, double top, double bottom)
{ {
X = left; Left = left;
Y = top; Top = top;
Width = right - left; Width = right - left;
Height = bottom - top; Height = bottom - top;
Confidence = 1; Confidence = 1;
ClassNumber = -1; ClassNumber = -1;
} }
public CanvasLabel(int classNumber, double x, double y, double width, double height, double confidence = 1) : base(classNumber) public CanvasLabel(int classNumber, double left, double top, double width, double height, double confidence = 1) : base(classNumber)
{ {
X = x; Left = left;
Y = y; Top = top;
Width = width; Width = width;
Height = height; Height = height;
Confidence = confidence; Confidence = confidence;
} }
public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double confidence = 1) public CanvasLabel(YoloLabel label, Size canvasSize, Size? mediaSize = null, double confidence = 1)
{ {
var cw = canvasSize.Width; var cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
var canvasAr = cw / ch; var canvasAr = cw / ch;
var videoAr = videoSize.HasValue var videoAr = mediaSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height ? mediaSize.Value.Width / mediaSize.Value.Height
: canvasAr; : canvasAr;
ClassNumber = label.ClassNumber; ClassNumber = label.ClassNumber;
@@ -80,8 +84,8 @@ public class CanvasLabel : Label
var realHeight = cw / videoAr; //real video height in pixels on canvas var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
X = left * cw; Left = left * cw;
Y = top * realHeight + blackStripHeight; Top = top * realHeight + blackStripHeight;
Width = label.Width * cw; Width = label.Width * cw;
Height = label.Height * realHeight; Height = label.Height * realHeight;
} }
@@ -90,8 +94,8 @@ public class CanvasLabel : Label
var realWidth = ch * videoAr; //real video width in pixels on canvas var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
X = left * realWidth + blackStripWidth; Left = left * realWidth + blackStripWidth;
Y = top * ch; Top = top * ch;
Width = label.Width * realWidth; Width = label.Width * realWidth;
Height = label.Height * ch; Height = label.Height * ch;
} }
@@ -99,10 +103,10 @@ public class CanvasLabel : Label
} }
public CanvasLabel ReframeToSmall(CanvasLabel smallTile) => public CanvasLabel ReframeToSmall(CanvasLabel smallTile) =>
new(ClassNumber, X - smallTile.X, Y - smallTile.Y, Width, Height, Confidence); new(ClassNumber, Left - smallTile.Left, Top - smallTile.Top, Width, Height, Confidence);
public CanvasLabel ReframeFromSmall(CanvasLabel smallTile) => public CanvasLabel ReframeFromSmall(CanvasLabel smallTile) =>
new(ClassNumber, X + smallTile.X, Y + smallTile.Y, Width, Height, Confidence); new(ClassNumber, Left + smallTile.Left, Top + smallTile.Top, Width, Height, Confidence);
} }
@@ -132,13 +136,13 @@ public class YoloLabel : Label
public RectangleF ToRectangle() => public RectangleF ToRectangle() =>
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height); new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? videoSize = null) public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? mediaSize = null)
{ {
var cw = canvasSize.Width; var cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
var canvasAr = cw / ch; var canvasAr = cw / ch;
var videoAr = videoSize.HasValue var videoAr = mediaSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height ? mediaSize.Value.Width / mediaSize.Value.Height
: canvasAr; : canvasAr;
ClassNumber = canvasLabel.ClassNumber; ClassNumber = canvasLabel.ClassNumber;
@@ -146,20 +150,20 @@ public class YoloLabel : Label
double left, top; double left, top;
if (videoAr > canvasAr) //100% width if (videoAr > canvasAr) //100% width
{ {
left = canvasLabel.X / cw; left = canvasLabel.Left / cw;
Width = canvasLabel.Width / cw; Width = canvasLabel.Width / cw;
var realHeight = cw / videoAr; //real video height in pixels on canvas var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
top = (canvasLabel.Y - blackStripHeight) / realHeight; top = (canvasLabel.Top - blackStripHeight) / realHeight;
Height = canvasLabel.Height / realHeight; Height = canvasLabel.Height / realHeight;
} }
else //100% height else //100% height
{ {
top = canvasLabel.Y / ch; top = canvasLabel.Top / ch;
Height = canvasLabel.Height / ch; Height = canvasLabel.Height / ch;
var realWidth = ch * videoAr; //real video width in pixels on canvas var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
left = (canvasLabel.X - blackStripWidth) / realWidth; left = (canvasLabel.Left - blackStripWidth) / realWidth;
Width = canvasLabel.Width / realWidth; Width = canvasLabel.Width / realWidth;
} }
+61 -6
View File
@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Windows.Media;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
@@ -12,12 +13,14 @@ public class Annotation
private static string _labelsDir = null!; private static string _labelsDir = null!;
private static string _imagesDir = null!; private static string _imagesDir = null!;
private static string _thumbDir = null!; private static string _thumbDir = null!;
private static Dictionary<int, DetectionClass> _detectionClassesDict;
public static void InitializeDirs(DirectoriesConfig config)
public static void Init(DirectoriesConfig config, Dictionary<int, DetectionClass> detectionClassesDict)
{ {
_labelsDir = config.LabelsDirectory; _labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory; _imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory; _thumbDir = config.ThumbnailsDirectory;
_detectionClassesDict = detectionClassesDict;
} }
[Key("n")] public string Name { get; set; } = null!; [Key("n")] public string Name { get; set; } = null!;
@@ -40,12 +43,64 @@ public class Annotation
[Key("lon")]public double Lon { get; set; } [Key("lon")]public double Lon { get; set; }
#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 ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
[IgnoreMember]public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt"); [IgnoreMember] public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
[IgnoreMember]public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg"); [IgnoreMember] public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
[IgnoreMember] public bool IsSplit => Name.Contains(Constants.SPLIT_SUFFIX);
private CanvasLabel? _splitTile;
[IgnoreMember] public CanvasLabel? SplitTile
{
get
{
if (!IsSplit)
return null;
if (_splitTile != null)
return _splitTile;
var startCoordIndex = Name.IndexOf(Constants.SPLIT_SUFFIX, StringComparison.Ordinal) + Constants.SPLIT_SUFFIX.Length;
var coordsStr = Name.Substring(startCoordIndex, 9).Split('_');
_splitTile = new CanvasLabel
{
Left = double.Parse(coordsStr[0]),
Top = double.Parse(coordsStr[1]),
Width = Constants.AI_TILE_SIZE,
Height = Constants.AI_TILE_SIZE
};
return _splitTile;
}
}
[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
} }
[MessagePackObject] [MessagePackObject]
+2 -1
View File
@@ -1,4 +1,5 @@
using System.Data.SQLite; using System.Data.SQLite;
using System.Diagnostics;
using System.IO; using System.IO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
@@ -48,7 +49,7 @@ public class DbFactory : IDbFactory
.UseDataProvider(SQLiteTools.GetDataProvider()) .UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_memoryConnection) .UseConnection(_memoryConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema) .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema)
;//.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); .UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
_fileConnection = new SQLiteConnection(FileConnStr); _fileConnection = new SQLiteConnection(FileConnStr);
+10 -10
View File
@@ -94,6 +94,7 @@ public class AnnotationService : IAnnotationService
await SaveAnnotationInner( await SaveAnnotationInner(
msg.CreatedDate, msg.CreatedDate,
msg.OriginalMediaName, msg.OriginalMediaName,
msg.Name,
msg.Time, msg.Time,
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [], JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
msg.Source, msg.Source,
@@ -136,16 +137,16 @@ public class AnnotationService : IAnnotationService
public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default) public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default)
{ {
a.Time = TimeSpan.FromMilliseconds(a.Milliseconds); a.Time = TimeSpan.FromMilliseconds(a.Milliseconds);
return await SaveAnnotationInner(DateTime.UtcNow, a.OriginalMediaName, a.Time, a.Detections.ToList(), return await SaveAnnotationInner(DateTime.UtcNow, a.OriginalMediaName, a.Name, a.Time, a.Detections.ToList(),
SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct); SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct);
} }
//Manual //Manual
public async Task<Annotation> SaveAnnotation(string originalMediaName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) => public async Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream, await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, annotationName, time, detections, SourceEnum.Manual, stream,
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token); _api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, string annotationName, TimeSpan time,
List<Detection> detections, SourceEnum source, Stream? stream, List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole, RoleEnum userRole,
string createdEmail, string createdEmail,
@@ -153,21 +154,20 @@ public class AnnotationService : IAnnotationService
CancellationToken token = default) CancellationToken token = default)
{ {
var status = AnnotationStatus.Created; var status = AnnotationStatus.Created;
var fName = originalMediaName.ToTimeName(time);
var annotation = await _dbFactory.RunWrite(async db => var annotation = await _dbFactory.RunWrite(async db =>
{ {
var ann = await db.Annotations var ann = await db.Annotations
.LoadWith(x => x.Detections) .LoadWith(x => x.Detections)
.FirstOrDefaultAsync(x => x.Name == fName, token: token); .FirstOrDefaultAsync(x => x.Name == annotationName, token: token);
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); await db.Detections.DeleteAsync(x => x.AnnotationName == annotationName, token: token);
if (ann != null) //Annotation is already exists if (ann != null) //Annotation is already exists
{ {
status = AnnotationStatus.Edited; status = AnnotationStatus.Edited;
var annotationUpdatable = db.Annotations var annotationUpdatable = db.Annotations
.Where(x => x.Name == fName) .Where(x => x.Name == annotationName)
.Set(x => x.Source, source); .Set(x => x.Source, source);
if (userRole.IsValidator() && source == SourceEnum.Manual) if (userRole.IsValidator() && source == SourceEnum.Manual)
@@ -188,7 +188,7 @@ public class AnnotationService : IAnnotationService
ann = new Annotation ann = new Annotation
{ {
CreatedDate = createdDate, CreatedDate = createdDate,
Name = fName, Name = annotationName,
OriginalMediaName = originalMediaName, OriginalMediaName = originalMediaName,
Time = time, Time = time,
ImageExtension = Constants.JPG_EXT, ImageExtension = Constants.JPG_EXT,
@@ -264,6 +264,6 @@ public class AnnotationService : IAnnotationService
public interface IAnnotationService public interface IAnnotationService
{ {
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default); Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
Task<Annotation> SaveAnnotation(string originalMediaName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default); Task<Annotation> SaveAnnotation(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);
} }
+7 -7
View File
@@ -237,11 +237,11 @@ public class GalleryService(
.ToList(); .ToList();
if (annotation.Detections.Any()) if (annotation.Detections.Any())
{ {
var labelsMinX = labels.Min(x => x.X); var labelsMinX = labels.Min(x => x.Left);
var labelsMaxX = labels.Max(x => x.X + x.Width); var labelsMaxX = labels.Max(x => x.Left + x.Width);
var labelsMinY = labels.Min(x => x.Y); var labelsMinY = labels.Min(x => x.Top);
var labelsMaxY = labels.Max(x => x.Y + x.Height); var labelsMaxY = labels.Max(x => x.Top + x.Height);
var labelsHeight = labelsMaxY - labelsMinY + 2 * border; var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
var labelsWidth = labelsMaxX - labelsMinX + 2 * border; var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
@@ -270,7 +270,7 @@ public class GalleryService(
var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color; var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B)); var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.X - frameX) / scale), (float)((label.Y - 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(annotation.ThumbPath, ImageFormat.Jpeg);
@@ -291,10 +291,10 @@ public class GalleryService(
var color = detClass.Color; var color = detClass.Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B)); var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var det = new CanvasLabel(detection, new Size(originalImage.Width, originalImage.Height)); var det = new CanvasLabel(detection, new Size(originalImage.Width, originalImage.Height));
g.DrawRectangle(new Pen(brush, width: 3), (float)det.X, (float)det.Y, (float)det.Width, (float)det.Height); g.DrawRectangle(new Pen(brush, width: 3), (float)det.Left, (float)det.Top, (float)det.Width, (float)det.Height);
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%"; var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%";
g.DrawTextBox(label, new PointF((float)(det.X + det.Width / 2.0), (float)(det.Y - 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 imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
+1 -1
View File
@@ -49,7 +49,7 @@ public class InferenceClient : IInferenceClient
Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}", Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}",
CreateNoWindow = true CreateNoWindow = true
}; };
process.Start(); //process.Start();
} }
catch (Exception e) catch (Exception e)
{ {
+18 -23
View File
@@ -18,10 +18,8 @@ public class TileResult
public static class TileProcessor public static class TileProcessor
{ {
private const int MaxTileWidth = 1280; public const int BORDER = 10;
private const int MaxTileHeight = 1280;
private const int Border = 10;
public static List<TileResult> Split(Size originalSize, List<CanvasLabel> detections, CancellationToken cancellationToken) public static List<TileResult> Split(Size originalSize, List<CanvasLabel> detections, CancellationToken cancellationToken)
{ {
var results = new List<TileResult>(); var results = new List<TileResult>();
@@ -30,7 +28,7 @@ public static class TileProcessor
while (processingDetectionList.Count > 0 && !cancellationToken.IsCancellationRequested) while (processingDetectionList.Count > 0 && !cancellationToken.IsCancellationRequested)
{ {
var topMostDetection = processingDetectionList var topMostDetection = processingDetectionList
.OrderBy(d => d.Y) .OrderBy(d => d.Top)
.First(); .First();
var result = GetDetectionsInTile(originalSize, topMostDetection, processingDetectionList); var result = GetDetectionsInTile(originalSize, topMostDetection, processingDetectionList);
@@ -42,11 +40,8 @@ public static class TileProcessor
private static TileResult GetDetectionsInTile(Size originalSize, CanvasLabel startDet, List<CanvasLabel> allDetections) private static TileResult GetDetectionsInTile(Size originalSize, CanvasLabel startDet, List<CanvasLabel> allDetections)
{ {
var tile = new CanvasLabel( var tile = new CanvasLabel(startDet.Left, startDet.Right, startDet.Top, startDet.Bottom);
left: Math.Max(startDet.X - Border, 0), var maxSize = new List<double> { startDet.Width + BORDER, startDet.Height + BORDER, Constants.AI_TILE_SIZE }.Max();
right: Math.Min(startDet.Right + Border, originalSize.Width),
top: Math.Max(startDet.Y - Border, 0),
bottom: Math.Min(startDet.Bottom + Border, originalSize.Height));
var selectedDetections = new List<CanvasLabel>{startDet}; var selectedDetections = new List<CanvasLabel>{startDet};
foreach (var det in allDetections) foreach (var det in allDetections)
@@ -55,26 +50,26 @@ public static class TileProcessor
continue; continue;
var commonTile = new CanvasLabel( var commonTile = new CanvasLabel(
left: Math.Max(Math.Min(tile.X, det.X) - Border, 0), left: Math.Min(tile.Left, det.Left),
right: Math.Min(Math.Max(tile.Right, det.Right) + Border, originalSize.Width), right: Math.Max(tile.Right, det.Right),
top: Math.Max(Math.Min(tile.Y, det.Y) - Border, 0), top: Math.Min(tile.Top, det.Top),
bottom: Math.Min(Math.Max(tile.Bottom, det.Bottom) + Border, originalSize.Height) bottom: Math.Max(tile.Bottom, det.Bottom)
); );
if (commonTile.Width > MaxTileWidth || commonTile.Height > MaxTileHeight) if (commonTile.Width + BORDER > maxSize || commonTile.Height + BORDER > maxSize)
continue; continue;
tile = commonTile; tile = commonTile;
selectedDetections.Add(det); selectedDetections.Add(det);
} }
//normalization, width and height should be at least half of 1280px
tile.Width = Math.Max(tile.Width, MaxTileWidth / 2.0);
tile.Height = Math.Max(tile.Height, MaxTileHeight / 2.0);
//boundaries check after normalization // boundary-aware centering
tile.Right = Math.Min(tile.Right, originalSize.Width); var centerX = selectedDetections.Average(x => x.CenterX);
tile.Bottom = Math.Min(tile.Bottom, originalSize.Height); var centerY = selectedDetections.Average(d => d.CenterY);
tile.Width = maxSize;
tile.Height = maxSize;
tile.Left = Math.Max(0, Math.Min(originalSize.Width - maxSize, centerX - tile.Width / 2.0));
tile.Top = Math.Max(0, Math.Min(originalSize.Height - maxSize, centerY - tile.Height / 2.0));
return new TileResult(tile, selectedDetections); return new TileResult(tile, selectedDetections);
} }
+22 -1
View File
@@ -80,7 +80,7 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="35"></RowDefinition> <RowDefinition Height="35"></RowDefinition>
<RowDefinition Height="35"></RowDefinition> <RowDefinition Height="30"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<controls:DetectionClasses <controls:DetectionClasses
x:Name="LvClasses" x:Name="LvClasses"
@@ -93,6 +93,27 @@
Click="ShowWithObjectsOnly_OnClick"> Click="ShowWithObjectsOnly_OnClick">
Показувати лише анотації з об'єктами Показувати лише анотації з об'єктами
</CheckBox> </CheckBox>
<Grid Name="LeftPaneSearch"
ShowGridLines="False"
Background="Black"
HorizontalAlignment="Stretch"
Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label
Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Stretch"
Margin="1"
Foreground="LightGray"
Content="Фільтр: "/>
<TextBox Name="TbSearch" TextChanged="TbSearch_OnTextChanged"
Grid.Column="1"
Foreground="Gray"/>
</Grid>
</Grid> </Grid>
<TabControl <TabControl
Name="Switcher" Name="Switcher"
+10 -2
View File
@@ -40,6 +40,8 @@ public partial class DatasetExplorer
public AnnotationThumbnail? CurrentAnnotation { get; set; } public AnnotationThumbnail? CurrentAnnotation { get; set; }
private static readonly Guid SearchActionId = Guid.NewGuid();
public DatasetExplorer( public DatasetExplorer(
IOptions<AppConfig> appConfig, IOptions<AppConfig> appConfig,
ILogger<DatasetExplorer> logger, ILogger<DatasetExplorer> logger,
@@ -199,9 +201,8 @@ public partial class DatasetExplorer
}; };
SwitchTab(toEditor: true); SwitchTab(toEditor: true);
var time = ann.Time;
ExplorerEditor.RemoveAllAnns(); ExplorerEditor.RemoveAllAnns();
ExplorerEditor.CreateDetections(time, ann.Detections, _appConfig.AnnotationConfig.DetectionClasses, ExplorerEditor.RenderSize); ExplorerEditor.CreateDetections(ann, _appConfig.AnnotationConfig.DetectionClasses, ExplorerEditor.RenderSize);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -261,6 +262,7 @@ public partial class DatasetExplorer
SelectedAnnotationDict.Clear(); SelectedAnnotationDict.Clear();
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(TbSearch.Text.Length > 2, x => x.Key.ToLower().Contains(TbSearch.Text))
.Select(x => new AnnotationThumbnail(x.Value, _azaionApi.CurrentUser.Role.IsValidator())) .Select(x => new AnnotationThumbnail(x.Value, _azaionApi.CurrentUser.Role.IsValidator()))
.OrderBy(x => !x.IsSeed) .OrderBy(x => !x.IsSeed)
.ThenByDescending(x =>x.Annotation.CreatedDate); .ThenByDescending(x =>x.Annotation.CreatedDate);
@@ -295,4 +297,10 @@ public partial class DatasetExplorer
_configUpdater.Save(_appConfig); _configUpdater.Save(_appConfig);
await ReloadThumbnails(); await ReloadThumbnails();
} }
private void TbSearch_OnTextChanged(object sender, TextChangedEventArgs e)
{
TbSearch.Foreground = TbSearch.Text.Length > 2 ? Brushes.Black : Brushes.Gray;
ThrottleExt.Throttle(ReloadThumbnails, SearchActionId, TimeSpan.FromMilliseconds(400));;
}
} }
@@ -70,7 +70,7 @@ public class DatasetExplorerEventHandler(
.Select(x => new Detection(a.Name, x.ToYoloLabel(datasetExplorer.ExplorerEditor.RenderSize))) .Select(x => new Detection(a.Name, x.ToYoloLabel(datasetExplorer.ExplorerEditor.RenderSize)))
.ToList(); .ToList();
var index = datasetExplorer.ThumbnailsView.SelectedIndex; var index = datasetExplorer.ThumbnailsView.SelectedIndex;
var annotation = await annotationService.SaveAnnotation(a.OriginalMediaName, a.Time, detections, token: token); var annotation = await annotationService.SaveAnnotation(a.OriginalMediaName, a.Name, a.Time, detections, token: token);
await ValidateAnnotations([annotation], token); await ValidateAnnotations([annotation], token);
await datasetExplorer.EditAnnotation(index + 1); await datasetExplorer.EditAnnotation(index + 1);
break; break;
-1
View File
@@ -12,5 +12,4 @@ cdef class Annotation:
cdef public list[Detection] detections cdef public list[Detection] detections
cdef public bytes image cdef public bytes image
cdef format_time(self, ms)
cdef bytes serialize(self) cdef bytes serialize(self)
+15 -15
View File
@@ -1,5 +1,5 @@
import msgpack import msgpack
from pathlib import Path cimport constants_inf
cdef class Detection: cdef class Detection:
def __init__(self, double x, double y, double w, double h, int cls, double confidence): def __init__(self, double x, double y, double w, double h, int cls, double confidence):
@@ -14,6 +14,17 @@ cdef class Detection:
def __str__(self): def __str__(self):
return f'{self.cls}: {self.x:.2f} {self.y:.2f} {self.w:.2f} {self.h:.2f}, prob: {(self.confidence*100):.1f}%' return f'{self.cls}: {self.x:.2f} {self.y:.2f} {self.w:.2f} {self.h:.2f}, prob: {(self.confidence*100):.1f}%'
def __eq__(self, other):
if not isinstance(other, Detection):
return False
if max(abs(self.x - other.x),
abs(self.y - other.y),
abs(self.w - other.w),
abs(self.h - other.h)) > constants_inf.TILE_DUPLICATE_CONFIDENCE_THRESHOLD:
return False
return True
cdef overlaps(self, Detection det2, float confidence_threshold): cdef overlaps(self, Detection det2, float confidence_threshold):
cdef double overlap_x = 0.5 * (self.w + det2.w) - abs(self.x - det2.x) cdef double overlap_x = 0.5 * (self.w + det2.w) - abs(self.x - det2.x)
cdef double overlap_y = 0.5 * (self.h + det2.h) - abs(self.y - det2.y) cdef double overlap_y = 0.5 * (self.h + det2.h) - abs(self.y - det2.y)
@@ -23,9 +34,9 @@ cdef class Detection:
return overlap_area / min_area > confidence_threshold return overlap_area / min_area > confidence_threshold
cdef class Annotation: cdef class Annotation:
def __init__(self, str name, long ms, list[Detection] detections): def __init__(self, str name, str original_media_name, long ms, list[Detection] detections):
self.original_media_name = Path(<str>name).stem.replace(" ", "") self.name = name
self.name = f'{self.original_media_name}_{self.format_time(ms)}' self.original_media_name = original_media_name
self.time = ms self.time = ms
self.detections = detections if detections is not None else [] self.detections = detections if detections is not None else []
for d in self.detections: for d in self.detections:
@@ -42,17 +53,6 @@ cdef class Annotation:
) )
return f"{self.name}: {detections_str}" return f"{self.name}: {detections_str}"
cdef format_time(self, ms):
# Calculate hours, minutes, seconds, and hundreds of milliseconds.
h = ms // 3600000 # Total full hours.
ms_remaining = ms % 3600000
m = ms_remaining // 60000 # Full minutes.
ms_remaining %= 60000
s = ms_remaining // 1000 # Full seconds.
f = (ms_remaining % 1000) // 100 # Hundreds of milliseconds.
h = h % 10
return f"{h}{m:02}{s:02}{f}"
cdef bytes serialize(self): cdef bytes serialize(self):
return msgpack.packb({ return msgpack.packb({
"n": self.name, "n": self.name,
+5 -1
View File
@@ -13,5 +13,9 @@ cdef str MODELS_FOLDER
cdef int SMALL_SIZE_KB cdef int SMALL_SIZE_KB
cdef str SPLIT_SUFFIX
cdef int TILE_DUPLICATE_CONFIDENCE_THRESHOLD
cdef log(str log_message) cdef log(str log_message)
cdef logerror(str error) cdef logerror(str error)
cdef format_time(int ms)
+15 -1
View File
@@ -12,6 +12,9 @@ cdef str MODELS_FOLDER = "models"
cdef int SMALL_SIZE_KB = 3 cdef int SMALL_SIZE_KB = 3
cdef str SPLIT_SUFFIX = "!split!"
cdef int TILE_DUPLICATE_CONFIDENCE_THRESHOLD = 5
logger.remove() logger.remove()
log_format = "[{time:HH:mm:ss} {level}] {message}" log_format = "[{time:HH:mm:ss} {level}] {message}"
logger.add( logger.add(
@@ -40,4 +43,15 @@ cdef log(str log_message):
logger.info(log_message) logger.info(log_message)
cdef logerror(str error): cdef logerror(str error):
logger.error(error) logger.error(error)
cdef format_time(int ms):
# Calculate hours, minutes, seconds, and hundreds of milliseconds.
h = ms // 3600000 # Total full hours.
ms_remaining = ms % 3600000
m = ms_remaining // 60000 # Full minutes.
ms_remaining %= 60000
s = ms_remaining // 1000 # Full seconds.
f = (ms_remaining % 1000) // 100 # Hundreds of milliseconds.
h = h % 10
return f"{h}{m:02}{s:02}{f}"
+10 -5
View File
@@ -9,23 +9,26 @@ cdef class Inference:
cdef InferenceEngine engine cdef InferenceEngine engine
cdef object on_annotation cdef object on_annotation
cdef Annotation _previous_annotation cdef Annotation _previous_annotation
cdef dict[str, list(Detection)] _tile_detections
cdef AIRecognitionConfig ai_config cdef AIRecognitionConfig ai_config
cdef bint stop_signal cdef bint stop_signal
cdef str model_input cdef str model_input
cdef int model_width cdef int model_width
cdef int model_height cdef int model_height
cdef int tile_width
cdef int tile_height
cdef build_tensor_engine(self, object updater_callback) cdef build_tensor_engine(self, object updater_callback)
cdef init_ai(self) cpdef init_ai(self)
cdef bint is_building_engine cdef bint is_building_engine
cdef bint is_video(self, str filepath) cdef bint is_video(self, str filepath)
cdef run_inference(self, RemoteCommand cmd) cdef run_inference(self, RemoteCommand cmd)
cdef _process_video(self, RemoteCommand cmd, AIRecognitionConfig ai_config, str video_name) cdef _process_video(self, RemoteCommand cmd, AIRecognitionConfig ai_config, str video_name)
cpdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths) cdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths)
cpdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data) cdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data)
cpdef split_to_tiles(self, frame, path, img_w, img_h, overlap_percent) cpdef split_to_tiles(self, frame, path, overlap_percent)
cdef stop(self) cdef stop(self)
cdef preprocess(self, frames) cdef preprocess(self, frames)
@@ -33,4 +36,6 @@ cdef class Inference:
cdef postprocess(self, output, ai_config) cdef postprocess(self, output, ai_config)
cdef split_list_extend(self, lst, chunk_size) cdef split_list_extend(self, lst, chunk_size)
cdef bint is_valid_annotation(self, Annotation annotation, AIRecognitionConfig ai_config) cdef bint is_valid_video_annotation(self, Annotation annotation, AIRecognitionConfig ai_config)
cdef bint is_valid_image_annotation(self, Annotation annotation)
cdef remove_tiled_duplicates(self, Annotation annotation)
+87 -42
View File
@@ -1,5 +1,7 @@
import mimetypes import mimetypes
import time import time
from pathlib import Path
import cv2 import cv2
import numpy as np import numpy as np
cimport constants_inf cimport constants_inf
@@ -54,6 +56,8 @@ cdef class Inference:
self.model_input = None self.model_input = None
self.model_width = 0 self.model_width = 0
self.model_height = 0 self.model_height = 0
self.tile_width = 0
self.tile_height = 0
self.engine = None self.engine = None
self.is_building_engine = False self.is_building_engine = False
@@ -93,7 +97,7 @@ cdef class Inference:
except Exception as e: except Exception as e:
updater_callback(f'Error. {str(e)}') updater_callback(f'Error. {str(e)}')
cdef init_ai(self): cpdef init_ai(self):
if self.engine is not None: if self.engine is not None:
return return
@@ -114,6 +118,8 @@ cdef class Inference:
self.engine = OnnxEngine(res.data) self.engine = OnnxEngine(res.data)
self.model_height, self.model_width = self.engine.get_input_shape() self.model_height, self.model_width = self.engine.get_input_shape()
self.tile_width = self.model_width
self.tile_height = self.model_height
cdef preprocess(self, frames): cdef preprocess(self, frames):
blobs = [cv2.dnn.blobFromImage(frame, blobs = [cv2.dnn.blobFromImage(frame,
@@ -211,11 +217,11 @@ cdef class Inference:
images.append(m) images.append(m)
# images first, it's faster # images first, it's faster
if len(images) > 0: if len(images) > 0:
constants_inf.log(f'run inference on {" ".join(images)}...') constants_inf.log(<str>f'run inference on {" ".join(images)}...')
self._process_images(cmd, ai_config, images) self._process_images(cmd, ai_config, images)
if len(videos) > 0: if len(videos) > 0:
for v in videos: for v in videos:
constants_inf.log(f'run inference on {v}...') constants_inf.log(<str>f'run inference on {v}...')
self._process_video(cmd, ai_config, v) self._process_video(cmd, ai_config, v)
@@ -223,8 +229,10 @@ cdef class Inference:
cdef int frame_count = 0 cdef int frame_count = 0
cdef list batch_frames = [] cdef list batch_frames = []
cdef list[int] batch_timestamps = [] cdef list[int] batch_timestamps = []
cdef Annotation annotation
self._previous_annotation = None self._previous_annotation = None
v_input = cv2.VideoCapture(<str>video_name) v_input = cv2.VideoCapture(<str>video_name)
while v_input.isOpened() and not self.stop_signal: while v_input.isOpened() and not self.stop_signal:
ret, frame = v_input.read() ret, frame = v_input.read()
@@ -244,8 +252,12 @@ cdef class Inference:
list_detections = self.postprocess(outputs, ai_config) list_detections = self.postprocess(outputs, ai_config)
for i in range(len(list_detections)): for i in range(len(list_detections)):
detections = list_detections[i] detections = list_detections[i]
annotation = Annotation(video_name, batch_timestamps[i], detections)
if self.is_valid_annotation(annotation, ai_config): original_media_name = Path(<str>video_name).stem.replace(" ", "")
name = f'{original_media_name}_{constants_inf.format_time(batch_timestamps[i])}'
annotation = Annotation(name, original_media_name, batch_timestamps[i], detections)
if self.is_valid_video_annotation(annotation, ai_config):
_, image = cv2.imencode('.jpg', batch_frames[i]) _, image = cv2.imencode('.jpg', batch_frames[i])
annotation.image = image.tobytes() annotation.image = image.tobytes()
self._previous_annotation = annotation self._previous_annotation = annotation
@@ -256,71 +268,104 @@ cdef class Inference:
v_input.release() v_input.release()
cpdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths): cdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths):
cdef list frame_data = [] cdef list frame_data
self._tile_detections = {}
for path in image_paths: for path in image_paths:
frame_data = []
frame = cv2.imread(<str>path) frame = cv2.imread(<str>path)
img_h, img_w, _ = frame.shape
if frame is None: if frame is None:
constants_inf.logerror(<str>f'Failed to read image {path}') constants_inf.logerror(<str>f'Failed to read image {path}')
continue continue
img_h, img_w, _ = frame.shape original_media_name = Path(<str> path).stem.replace(" ", "")
if img_h <= 1.5 * self.model_height and img_w <= 1.5 * self.model_width: if img_h <= 1.5 * self.model_height and img_w <= 1.5 * self.model_width:
frame_data.append((frame, path)) frame_data.append((frame, original_media_name, f'{original_media_name}_000000'))
else: else:
(split_frames, split_pats) = self.split_to_tiles(frame, path, img_w, img_h, ai_config.big_image_tile_overlap_percent) res = self.split_to_tiles(frame, path, ai_config.big_image_tile_overlap_percent)
frame_data.extend(zip(split_frames, split_pats)) frame_data.extend(res)
if len(frame_data) > self.engine.get_batch_size():
for chunk in self.split_list_extend(frame_data, self.engine.get_batch_size()):
self._process_images_inner(cmd, ai_config, chunk)
for chunk in self.split_list_extend(frame_data, self.engine.get_batch_size()): for chunk in self.split_list_extend(frame_data, self.engine.get_batch_size()):
self._process_images_inner(cmd, ai_config, chunk) self._process_images_inner(cmd, ai_config, chunk)
cpdef split_to_tiles(self, frame, path, img_w, img_h, overlap_percent): cpdef split_to_tiles(self, frame, path, overlap_percent):
stride_w = self.model_width * (1 - overlap_percent / 100) constants_inf.log(<str>f'splitting image {path} to tiles...')
stride_h = self.model_height * (1 - overlap_percent / 100) img_h, img_w, _ = frame.shape
n_tiles_x = int(np.ceil((img_w - self.model_width) / stride_w)) + 1 stride_w = int(self.tile_width * (1 - overlap_percent / 100))
n_tiles_y = int(np.ceil((img_h - self.model_height) / stride_h)) + 1 stride_h = int(self.tile_height * (1 - overlap_percent / 100))
results = [] results = []
for y_idx in range(n_tiles_y): original_media_name = Path(<str> path).stem.replace(" ", "")
for x_idx in range(n_tiles_x): for y in range(0, img_h, stride_h):
y_start = y_idx * stride_w for x in range(0, img_w, stride_w):
x_start = x_idx * stride_h x_end = min(x + self.tile_width, img_w)
y_end = min(y + self.tile_height, img_h)
# Ensure the tile doesn't go out of bounds # correct x,y for the close-to-border tiles
y_end = min(y_start + self.model_width, img_h) if x_end - x < self.tile_width:
x_end = min(x_start + self.model_height, img_w) if img_w - (x - stride_w) <= self.tile_width:
continue # the previous tile already covered the last gap
x = img_w - self.tile_width
if y_end - y < self.tile_height:
if img_h - (y - stride_h) <= self.tile_height:
continue # the previous tile already covered the last gap
y = img_h - self.tile_height
# We need to re-calculate start if we are at the edge to get a full 1280x1280 tile tile = frame[y:y_end, x:x_end]
if y_end == img_h: name = f'{original_media_name}{constants_inf.SPLIT_SUFFIX}{x:04d}_{y:04d}!_000000'
y_start = img_h - self.model_height results.append((tile, original_media_name, name))
if x_end == img_w:
x_start = img_w - self.model_width
tile = frame[y_start:y_end, x_start:x_end]
name = path.stem + f'.tile_{x_start}_{y_start}' + path.suffix
results.append((tile, name))
return results return results
cpdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data): cdef _process_images_inner(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list frame_data):
frames = [frame for frame, _ in frame_data] cdef list frames, original_media_names, names
cdef Annotation annotation
frames, original_media_names, names = map(list, zip(*frame_data))
input_blob = self.preprocess(frames) input_blob = self.preprocess(frames)
outputs = self.engine.run(input_blob) outputs = self.engine.run(input_blob)
list_detections = self.postprocess(outputs, ai_config) list_detections = self.postprocess(outputs, ai_config)
for i in range(len(list_detections)): for i in range(len(list_detections)):
detections = list_detections[i] annotation = Annotation(names[i], original_media_names[i], 0, list_detections[i])
annotation = Annotation(frame_data[i][1], 0, detections) if self.is_valid_image_annotation(annotation):
_, image = cv2.imencode('.jpg', frames[i]) _, image = cv2.imencode('.jpg', frames[i])
annotation.image = image.tobytes() annotation.image = image.tobytes()
self.on_annotation(cmd, annotation) self.on_annotation(cmd, annotation)
cdef stop(self): cdef stop(self):
self.stop_signal = True self.stop_signal = True
cdef bint is_valid_annotation(self, Annotation annotation, AIRecognitionConfig ai_config): cdef remove_tiled_duplicates(self, Annotation annotation):
# No detections, invalid right = annotation.name.rindex('!')
left = annotation.name.index(constants_inf.SPLIT_SUFFIX) + len(constants_inf.SPLIT_SUFFIX)
x_str, y_str = annotation.name[left:right].split('_')
x = int(x_str)
y = int(y_str)
for det in annotation.detections:
x1 = det.x * self.tile_width
y1 = det.y * self.tile_height
det_abs = Detection(x + x1, y + y1, det.w * self.tile_width, det.h * self.tile_height, det.cls, det.confidence)
detections = self._tile_detections.setdefault(annotation.original_media_name, [])
if det_abs in detections:
annotation.detections.remove(det)
else:
detections.append(det_abs)
cdef bint is_valid_image_annotation(self, Annotation annotation):
if constants_inf.SPLIT_SUFFIX in annotation.name:
self.remove_tiled_duplicates(annotation)
if not annotation.detections:
return False
return True
cdef bint is_valid_video_annotation(self, Annotation annotation, AIRecognitionConfig ai_config):
if constants_inf.SPLIT_SUFFIX in annotation.name:
self.remove_tiled_duplicates(annotation)
if not annotation.detections: if not annotation.detections:
return False return False
+8 -8
View File
@@ -2,15 +2,15 @@ from setuptools import setup, Extension
from Cython.Build import cythonize from Cython.Build import cythonize
import numpy as np import numpy as np
# debug_args = {} debug_args = {}
# trace_line = False trace_line = False
debug_args = { # debug_args = {
'extra_compile_args': ['-O0', '-g'], # 'extra_compile_args': ['-O0', '-g'],
'extra_link_args': ['-g'], # 'extra_link_args': ['-g'],
'define_macros': [('CYTHON_TRACE_NOGIL', '1')] # 'define_macros': [('CYTHON_TRACE_NOGIL', '1')]
} # }
trace_line = True # trace_line = True
extensions = [ extensions = [
Extension('constants_inf', ['constants_inf.pyx'], **debug_args), Extension('constants_inf', ['constants_inf.pyx'], **debug_args),
+26 -4
View File
@@ -1,8 +1,30 @@
import inference import inference
from ai_config import AIRecognitionConfig from ai_config import AIRecognitionConfig
from remote_command_inf import RemoteCommand from unittest.mock import Mock
import numpy as np
from loader_client import LoaderClient
def test_process_images(): def test_split_to_tiles():
inf = inference.Inference(None, None) loader_client = LoaderClient("test", 0)
inf._process_images(RemoteCommand(30), AIRecognitionConfig(4, 2, 15, 0.15, 15, 0.8, 20, b'test', [], 4), ['test_img01.JPG', 'test_img02.jpg']) ai_config = AIRecognitionConfig(
frame_period_recognition=4,
frame_recognition_seconds=2,
probability_threshold=0.2,
tracking_distance_confidence=0.15,
tracking_probability_increase=0.15,
tracking_intersection_threshold=0.6,
big_image_tile_overlap_percent=20,
file_data=None,
paths=[],
model_batch_size=4
)
inf = inference.Inference(loader_client, ai_config)
test_frame = np.zeros((6336, 8448, 3), dtype=np.uint8)
inf.init_ai()
inf.split_to_tiles(test_frame, 'test_image.jpg', ai_config.big_image_tile_overlap_percent)
+7 -1
View File
@@ -2,9 +2,14 @@ import os
import subprocess import subprocess
cimport constants cimport constants
cdef class HardwareService: cdef class HardwareService:
cdef str _CACHED_HW_INFO = None
@staticmethod @staticmethod
cdef str get_hardware_info(): cdef str get_hardware_info():
global _CACHED_HW_INFO
if _CACHED_HW_INFO is not None:
return <str> _CACHED_HW_INFO
if os.name == 'nt': # windows if os.name == 'nt': # windows
os_command = ( os_command = (
"powershell -Command \"" "powershell -Command \""
@@ -34,5 +39,6 @@ cdef class HardwareService:
cdef str drive_serial = lines[len_lines-1] cdef str drive_serial = lines[len_lines-1]
cdef str res = f'CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {drive_serial}' cdef str res = f'CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {drive_serial}'
constants.log(f'Gathered hardware: {res}') constants.log(<str>f'Gathered hardware: {res}')
_CACHED_HW_INFO = res
return res return res
+3 -1
View File
@@ -175,7 +175,9 @@ public partial class App
}) })
.Build(); .Build();
Annotation.InitializeDirs(_host.Services.GetRequiredService<IOptions<DirectoriesConfig>>().Value); 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>();
+4 -4
View File
@@ -17,10 +17,10 @@
"DirectoriesConfig": { "DirectoriesConfig": {
"ApiResourcesDirectory": "stage", "ApiResourcesDirectory": "stage",
"VideosDirectory": "E:\\Azaion6", "VideosDirectory": "E:\\Azaion6",
"LabelsDirectory": "E:\\labels", "LabelsDirectory": "E:\\labels_test",
"ImagesDirectory": "E:\\images", "ImagesDirectory": "E:\\images_test",
"ResultsDirectory": "E:\\results", "ResultsDirectory": "E:\\results_test",
"ThumbnailsDirectory": "E:\\thumbnails", "ThumbnailsDirectory": "E:\\thumbnails_test",
"GpsSatDirectory": "satellitesDir", "GpsSatDirectory": "satellitesDir",
"GpsRouteDirectory": "routeDir" "GpsRouteDirectory": "routeDir"
}, },
+1 -1
View File
@@ -29,7 +29,7 @@
"ProbabilityThreshold": 0.25, "ProbabilityThreshold": 0.25,
"TrackingDistanceConfidence": 0.15, "TrackingDistanceConfidence": 0.15,
"TrackingProbabilityIncrease": 15.0, "TrackingProbabilityIncrease": 0.15,
"TrackingIntersectionThreshold": 0.6, "TrackingIntersectionThreshold": 0.6,
"BigImageTileOverlapPercent": 20, "BigImageTileOverlapPercent": 20,
+263
View File
@@ -0,0 +1,263 @@
using System.Windows;
using Azaion.Common;
using Azaion.Common.DTO;
using Azaion.Common.Services;
using Xunit;
namespace Azaion.Annotator.Test;
public class TileProcessorTest
{
private const int IMAGE_SIZE = 5000;
[Fact]
public void Split_DetectionsNearImageCorners_ShouldCreateFourTiles()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel>
{
new(10, 60, 10, 60), // Top-left corner
new(IMAGE_SIZE - 60, IMAGE_SIZE - 10, 10, 60), // Top-right corner
new(10, 60, IMAGE_SIZE - 60, IMAGE_SIZE - 10), // Bottom-left corner
new(IMAGE_SIZE - 60, IMAGE_SIZE - 10, IMAGE_SIZE - 60, IMAGE_SIZE - 10) // Bottom-right corner
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Equal(4, results.Count);
}
[Fact]
public void Split_DetectionsFarApartButFitInOneTile_ShouldCreateOneTile()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel>
{
new(100, 150, 100, 150),
new(1200, 1250, 1200, 1250)
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Single(results);
Assert.Equal(2, results[0].Detections.Count);
}
[Fact]
public void Split_DetectionsTooFarApart_ShouldCreateMultipleTiles()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel>
{
new(100, 150, 100, 150),
new(2000, 2050, 2000, 2050) // More than Constants.AI_TILE_SIZE away
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Equal(2, results.Count);
Assert.Contains(results, r => r.Detections.Count == 1 && r.Detections.Contains(detections[0]));
Assert.Contains(results, r => r.Detections.Count == 1 && r.Detections.Contains(detections[1]));
}
[Fact]
public void Split_ComplexScenario_ShouldCreateCorrectNumberOfTiles()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel>
{
// Group 1 (should be tiled together)
new(100, 150, 100, 150),
new(200, 250, 200, 250),
new(500, 550, 500, 550),
// Group 2 (far from group 1, should be in a separate tile)
new(3000, 3050, 3000, 3050),
new(3100, 3150, 3100, 3150),
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Equal(2, results.Count);
var group1Tile = results.FirstOrDefault(r => r.Detections.Count == 3);
var group2Tile = results.FirstOrDefault(r => r.Detections.Count == 2);
Assert.NotNull(group1Tile);
Assert.NotNull(group2Tile);
Assert.Contains(detections[0], group1Tile.Detections);
Assert.Contains(detections[1], group1Tile.Detections);
Assert.Contains(detections[2], group1Tile.Detections);
Assert.Contains(detections[3], group2Tile.Detections);
Assert.Contains(detections[4], group2Tile.Detections);
}
[Fact]
public void Split_NoDetections_ShouldReturnEmptyList()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel>();
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Empty(results);
}
[Fact]
public void Split_OneDetection_ShouldCreateOneTile()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel> { new(100, 150, 100, 150) };
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Single(results);
Assert.Single(results[0].Detections);
Assert.Equal(detections[0], results[0].Detections[0]);
}
[Fact]
public void Split_DetectionsOnTileBoundary_ShouldFitInOneTile()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
// Combined width is 1270. 1270 + BORDER (10) is not > Constants.AI_TILE_SIZE (1280), so they fit.
var detections = new List<CanvasLabel>
{
new(0, 50, 0, 50),
new(Constants.AI_TILE_SIZE - TileProcessor.BORDER - 50, Constants.AI_TILE_SIZE - TileProcessor.BORDER, 0, 50)
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Single(results);
Assert.Equal(2, results[0].Detections.Count);
}
[Fact]
public void Split_DetectionsJustOverTileBoundary_ShouldCreateTwoTiles()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
// Combined width is 1271. 1271 + BORDER (10) is > Constants.AI_TILE_SIZE (1280), so they don't fit.
var detections = new List<CanvasLabel>
{
new(0, 50, 1000, 1050), // Top-most
new(Constants.AI_TILE_SIZE - TileProcessor.BORDER - 49, Constants.AI_TILE_SIZE - TileProcessor.BORDER + 1, 0, 50)
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Equal(2, results.Count);
}
[Fact]
public void Split_ResultingTiles_ShouldBeWithinImageBoundaries()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel>
{
new(10, 60, 10, 60), // Top-left corner
new(IMAGE_SIZE - 60, IMAGE_SIZE - 10, IMAGE_SIZE - 60, IMAGE_SIZE - 10) // Bottom-right corner
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Equal(2, results.Count);
foreach (var result in results)
{
var tile = result.Tile;
Assert.True(tile.Left >= 0, $"Tile Left boundary {tile.Left} is out of bounds.");
Assert.True(tile.Top >= 0, $"Tile Top boundary {tile.Top} is out of bounds.");
Assert.True(tile.Right <= originalSize.Width, $"Tile Right boundary {tile.Right} is out of bounds.");
Assert.True(tile.Bottom <= originalSize.Height, $"Tile Bottom boundary {tile.Bottom} is out of bounds.");
}
}
[Fact]
public void Split_ChainedDetections_ShouldCreateOneTile()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var detections = new List<CanvasLabel>
{
new(100, 200, 100, 200), // Detection A
new(600, 700, 600, 700), // Detection B (close to A)
new(1100, 1200, 1100, 1200) // Detection C (close to B, but far from A)
};
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Single(results);
Assert.Equal(3, results[0].Detections.Count);
}
[Fact]
public void Split_SingleDetectionLargerThanTileSize_ShouldCreateOneTile()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var largeDetection = new CanvasLabel(100, 100 + Constants.AI_TILE_SIZE + 100, 100, 200);
var detections = new List<CanvasLabel> { largeDetection };
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Single(results);
var resultTile = results[0];
Assert.Single(resultTile.Detections);
Assert.Equal(largeDetection, resultTile.Detections[0]);
// The tile should be at least as large as the detection it contains.
Assert.True(resultTile.Tile.Width >= largeDetection.Width);
Assert.True(resultTile.Tile.Height >= largeDetection.Height);
}
[Fact]
public void Split_LargeDetectionWithNearbySmallDetection_ShouldCreateOneTile()
{
// Arrange
var originalSize = new Size(IMAGE_SIZE, IMAGE_SIZE);
var largeTallDetection = new CanvasLabel(100, 150, 100, 100 + Constants.AI_TILE_SIZE + 200);
var smallDetectionNearby = new CanvasLabel(largeTallDetection.Right + 15, largeTallDetection.Right + 35, 700, 720);
var detections = new List<CanvasLabel> { largeTallDetection, smallDetectionNearby };
// Act
var results = TileProcessor.Split(originalSize, detections, CancellationToken.None);
// Assert
Assert.Single(results);
Assert.Equal(2, results[0].Detections.Count);
Assert.Contains(largeTallDetection, results[0].Detections);
Assert.Contains(smallDetectionNearby, results[0].Detections);
}
}