using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using Azaion.Annotator.DTO; using Azaion.Common; using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.Common.Services; using Azaion.Common.Services.Inference; using LibVLCSharp.Shared; using MediatR; using Microsoft.WindowsAPICodePack.Dialogs; using Size = System.Windows.Size; using IntervalTree; using LinqToDB; using LinqToDB.Data; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MediaPlayer = LibVLCSharp.Shared.MediaPlayer; namespace Azaion.Annotator; public partial class Annotator { private readonly AppConfig? _appConfig; private readonly LibVLC _libVlc; private readonly MediaPlayer _mediaPlayer; private readonly IMediator _mediator; private readonly FormState _formState; private readonly IConfigUpdater _configUpdater; private readonly HelpWindow _helpWindow; private readonly ILogger _logger; private readonly IDbFactory _dbFactory; private readonly IInferenceService _inferenceService; private bool _suspendLayout; private bool _gpsPanelVisible; private readonly CancellationTokenSource _mainCancellationSource = new(); public CancellationTokenSource DetCancelSource = new(); private bool _isInferenceNow; private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50); private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150); public ObservableCollection AllMediaFiles { get; set; } = new(); private ObservableCollection FilteredMediaFiles { get; set; } = new(); public Dictionary MediaFilesDict = new(); public IntervalTree TimedAnnotations { get; set; } = new(); public string MainTitle { get; set; } public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig(); private static readonly Guid ReloadTaskId = Guid.NewGuid(); private readonly IAnnotationPathResolver _pathResolver; private readonly IDetectionClassProvider _classProvider; public IDetectionClassProvider ClassProvider => _classProvider; public Annotator( IConfigUpdater configUpdater, IOptions appConfig, LibVLC libVlc, MediaPlayer mediaPlayer, IMediator mediator, FormState formState, HelpWindow helpWindow, ILogger logger, IDbFactory dbFactory, IInferenceService inferenceService, IInferenceClient inferenceClient, IGpsMatcherService gpsMatcherService, IAnnotationPathResolver pathResolver, IDetectionClassProvider classProvider) { _pathResolver = pathResolver; _classProvider = classProvider; // Initialize configuration and services BEFORE InitializeComponent so bindings can see real values _appConfig = appConfig.Value; _configUpdater = configUpdater; _libVlc = libVlc; _mediaPlayer = mediaPlayer; _mediator = mediator; _formState = formState; _helpWindow = helpWindow; _logger = logger; _dbFactory = dbFactory; _inferenceService = inferenceService; // Ensure bindings (e.g., Camera) resolve immediately DataContext = this; InitializeComponent(); MainTitle = $"Azaion Annotator {Constants.GetLocalVersion()}"; Title = MainTitle; Loaded += OnLoaded; Closed += OnFormClosed; Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator; TbFolder.TextChanged += async (_, _) => { if (!Path.Exists(TbFolder.Text)) return; try { _appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text; await ReloadFiles(); } catch (Exception e) { _logger.LogError(e, e.Message); } }; Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); MapMatcherComponent.Init(_appConfig, gpsMatcherService); // When camera settings change, persist config CameraConfigControl.CameraChanged += (_, _) => { if (_appConfig != null) _configUpdater.Save(_appConfig); }; } private void OnLoaded(object sender, RoutedEventArgs e) { Core.Initialize(); InitControls(); _suspendLayout = true; MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.LeftPanelWidth ?? Constants.DEFAULT_LEFT_PANEL_WIDTH); MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.RightPanelWidth ?? Constants.DEFAULT_RIGHT_PANEL_WIDTH); _suspendLayout = false; TbFolder.Text = _appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR; LvClasses.Init(_appConfig?.AnnotationConfig.DetectionClasses ?? Constants.DefaultAnnotationClasses); } public void BlinkHelp(string helpText, int times = 2) { _ = Task.Run(async () => { for (int i = 0; i < times; i++) { Dispatcher.Invoke(() => StatusHelp.Text = helpText); await Task.Delay(200); Dispatcher.Invoke(() => StatusHelp.Text = ""); await Task.Delay(200); } Dispatcher.Invoke(() => StatusHelp.Text = helpText); }); } private void InitControls() { VideoView.MediaPlayer = _mediaPlayer; //On start playing media _mediaPlayer.Playing += (_, _) => { uint vw = 0, vh = 0; _mediaPlayer.Size(0, ref vw, ref vh); _formState.CurrentMediaSize = new Size(vw, vh); _formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length); }; LvFiles.MouseDoubleClick += async (_, _) => { await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play)); }; LvClasses.DetectionClassChanged += (_, args) => { var selectedClass = args.DetectionClass; Editor.CurrentAnnClass = selectedClass; _mediator.Publish(new AnnClassSelectedEvent(selectedClass)); }; _mediaPlayer.PositionChanged += (_, _) => ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time)); VideoSlider.ValueChanged += (_, newValue) => _mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum); VideoSlider.KeyDown += (sender, args) => _mediator.Publish(new KeyEvent(sender, args, WindowEnum.Annotator)); Volume.ValueChanged += (_, newValue) => _mediator.Publish(new VolumeChangedEvent((int)newValue)); DgAnnotations.MouseDoubleClick += (sender, args) => { if (ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) is DataGridRow dgRow) OpenAnnotationResult((Annotation)dgRow.Item); }; DgAnnotations.KeyUp += async (_, args) => { switch (args.Key) { case Key.Down: //cursor is already moved by system behaviour OpenAnnotationResult((Annotation)DgAnnotations.SelectedItem); break; case Key.Delete: var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question); if (result != MessageBoxResult.OK) return; var res = DgAnnotations.SelectedItems.Cast().ToList(); var annotationNames = res.Select(x => x.Name).ToList(); await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames)); break; } }; DgAnnotations.ItemsSource = _formState.AnnotationResults; } private void OpenAnnotationResult(Annotation ann) { _mediaPlayer.SetPause(true); if (!ann.IsSplit) Editor.RemoveAllAnns(); _mediaPlayer.Time = (long)ann.Time.TotalMilliseconds; Dispatcher.Invoke(() => { VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; Editor.ClearExpiredAnnotations(ann.Time); }); ShowAnnotation(ann, showImage: true, openResult: true); } private void SaveUserSettings() { if (_suspendLayout || _appConfig is null) return; _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; _configUpdater.Save(_appConfig); } public void ShowTimeAnnotations(TimeSpan time, bool showImage = false) { Dispatcher.Invoke(() => { VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; Editor.ClearExpiredAnnotations(time); }); var annotations = TimedAnnotations.Query(time).ToList(); if (!annotations.Any()) return; foreach (var ann in annotations) ShowAnnotation(ann, showImage); } private void ShowAnnotation(Annotation annotation, bool showImage = false, bool openResult = false) { Dispatcher.Invoke(async () => { var imagePath = _pathResolver.GetImagePath(annotation); if (showImage && !annotation.IsSplit && File.Exists(imagePath)) { Editor.SetBackground(await imagePath.OpenImage()); _formState.BackgroundTime = annotation.Time; } 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); }); } public async Task ReloadAnnotations() { await Dispatcher.InvokeAsync(async () => { _formState.AnnotationResults.Clear(); TimedAnnotations.Clear(); Editor.RemoveAllAnns(); var mediaHash = _formState.CurrentMedia?.Hash; var mediaName = _formState.CurrentMedia?.Name; var annotations = await _dbFactory.Run(async db => await db.Annotations.LoadWith(x => x.Detections) .Where(x => (!string.IsNullOrEmpty(mediaHash) && x.MediaHash == mediaHash) || (x.MediaHash == null && x.OriginalMediaName == 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.Add(ann); } }); } //Add manually public void AddAnnotation(Annotation annotation) { var time = annotation.Time; var previousAnnotations = TimedAnnotations.Query(time); TimedAnnotations.Remove(previousAnnotations); TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation); var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time); if (existingResult != null) { try { _formState.AnnotationResults.Remove(existingResult); } catch (Exception e) { _logger.LogError(e, e.Message); throw; } } var dict = _formState.AnnotationResults .Select((x, i) => new { x.Time, Index = i }) .ToDictionary(x => x.Time, x => x.Index); var index = dict.Where(x => x.Key < time) .OrderBy(x => time - x.Key) .Select(x => x.Value + 1) .FirstOrDefault(); _formState.AnnotationResults.Insert(index, annotation); } public void ReloadFilesThrottled() { ThrottleExt.Throttle(async () => { await ReloadFiles(); }, ReloadTaskId, TimeSpan.FromSeconds(4)); } public async Task ReloadFiles() { var dir = new DirectoryInfo(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR); if (!dir.Exists) return; var folderFiles = dir.GetFiles(Constants.DefaultVideoFormats.Concat(Constants.DefaultImageFormats).ToArray()) .Select(x => x.FullName) .Select(x => new MediaFile(x)) .GroupBy(x => x.Hash) .ToDictionary(x => x.Key, v => v.First()); //sync with db var dbFiles = await _dbFactory.Run(async db => await db.MediaFiles .Where(x => folderFiles.ContainsKey(x.Hash)) .ToDictionaryAsync(x => x.Hash)); var newFiles = folderFiles .Where(x => !dbFiles.ContainsKey(x.Key)) .Select(x => x.Value) .ToList(); if (newFiles.Count > 0) await _dbFactory.RunWrite(async db => await db.BulkCopyAsync(newFiles)); var allFiles = dbFiles.Select(x => x.Value) .Concat(newFiles) .ToList(); await SyncAnnotations(allFiles); AllMediaFiles = new ObservableCollection(allFiles); MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name) .ToDictionary(gr => gr.Key, gr => gr.First()); var selectedIndex = LvFiles.SelectedIndex; LvFiles.ItemsSource = AllMediaFiles; LvFiles.SelectedIndex = selectedIndex; DataContext = this; } private async Task SyncAnnotations(List allFiles) { var hashes = allFiles.Select(x => x.Hash).ToList(); var filenames = allFiles.Select(x => x.Name).ToList(); var nameHashMap = allFiles.ToDictionary(x => x.Name.ToFName(), x => x.Hash); await _dbFactory.RunWrite(async db => { var hashedAnnotations = await db.Annotations .Where(a => hashes.Contains(a.MediaHash)) .ToDictionaryAsync(x => x.Name); var fileNameAnnotations = await db.Annotations .Where(a => filenames.Contains(a.OriginalMediaName)) .ToDictionaryAsync(x => x.Name); var toUpdate = fileNameAnnotations .Where(a => !hashedAnnotations.ContainsKey(a.Key)) .Select(a => new { a.Key, MediaHash = nameHashMap.GetValueOrDefault(a.Value.OriginalMediaName) ?? "" }) .ToList(); if (toUpdate.Count > 0) { var caseBuilder = new StringBuilder("UPDATE Annotations SET MediaHash = CASE Name "); var parameters = new List(); for (int i = 0; i < toUpdate.Count; i++) { caseBuilder.Append($"WHEN @name{i} THEN @hash{i} "); parameters.Add(new DataParameter($"@name{i}", toUpdate[i].Key, DataType.NVarChar)); parameters.Add(new DataParameter($"@hash{i}", toUpdate[i].MediaHash, DataType.NVarChar)); } caseBuilder.Append("END WHERE Name IN ("); caseBuilder.Append(string.Join(", ", Enumerable.Range(0, toUpdate.Count).Select(i => $"@name{i}"))); caseBuilder.Append(")"); await db.ExecuteAsync(caseBuilder.ToString(), parameters.ToArray()); } var annotationMediaHashes = hashedAnnotations .GroupBy(x => x.Value.MediaHash) .Select(x => x.Key) .ToList(); var annotationMediaNames = fileNameAnnotations .GroupBy(x => x.Value.OriginalMediaName) .Select(x => x.Key) .ToList(); await db.MediaFiles .Where(m => annotationMediaHashes.Contains(m.Hash) || annotationMediaNames.Contains(m.Name)) .Set(m => m.Status, MediaStatus.Confirmed) .UpdateAsync(); }); } private void OnFormClosed(object? sender, EventArgs e) { _mainCancellationSource.Cancel(); _inferenceService.StopInference(); DetCancelSource.Cancel(); _mediaPlayer.Stop(); _mediaPlayer.Dispose(); _libVlc.Dispose(); } private void OpenContainingFolder(object sender, RoutedEventArgs e) { var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFile; if (mediaFileInfo == null) return; Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.MediaUrl +"\""); } public void SeekTo(long timeMilliseconds, bool setPause = true) { _mediaPlayer.SetPause(setPause); _mediaPlayer.Time = timeMilliseconds; VideoSlider.Value = _mediaPlayer.Position * 100; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; } private void OpenFolderItemClick(object sender, RoutedEventArgs e) => OpenFolder(); private void OpenFolderButtonClick(object sender, RoutedEventArgs e) => OpenFolder(); private void OpenFolder() { var dlg = new CommonOpenFileDialog { Title = "Open Video folder", IsFolderPicker = true, InitialDirectory = Path.GetDirectoryName(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR) }; var dialogResult = dlg.ShowDialog(); if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName)) return; if (_appConfig is not null) _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; TbFolder.Text = dlg.FileName; } private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) { FilteredMediaFiles = new ObservableCollection(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList()); MediaFilesDict = FilteredMediaFiles.ToDictionary(x => x.Name); LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles; } private void PlayClick(object sender, RoutedEventArgs e) { _mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play)); } private void PauseClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Pause)); private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Stop)); private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.PreviousFrame)); private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.NextFrame)); private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.SaveAnnotations)); private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveSelectedAnns)); private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveAllAnns)); private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOffVolume)); private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOnVolume)); private void OpenHelpWindowClick(object sender, RoutedEventArgs e) { _helpWindow.Show(); _helpWindow.Activate(); } private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => SaveUserSettings(); private void LvFilesContextOpening(object sender, ContextMenuEventArgs e) { var listItem = sender as ListViewItem; LvFilesContextMenu.DataContext = listItem!.DataContext; } private async void AIDetectBtn_OnClick(object sender, RoutedEventArgs e) { try { await AutoDetect(); } catch (Exception ex) { _logger.LogError(ex, ex.Message); } } public async Task AutoDetect() { if (_isInferenceNow) return; if (LvFiles.Items.IsEmpty) return; if (LvFiles.SelectedIndex == -1) LvFiles.SelectedIndex = 0; Dispatcher.Invoke(() => Editor.SetBackground(null)); _isInferenceNow = true; AIDetectBtn.IsEnabled = false; DetCancelSource = new CancellationTokenSource(); var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles) .Skip(LvFiles.SelectedIndex) .Select(x => x.MediaUrl) .ToList(); if (files.Count == 0) return; await _inferenceService.RunInference(files, _appConfig?.CameraConfig ?? Constants.DefaultCameraConfig, DetCancelSource.Token); LvFiles.Items.Refresh(); _isInferenceNow = false; StatusHelp.Text = "Розпізнавання завершено"; AIDetectBtn.IsEnabled = true; } private void SwitchGpsPanel(object sender, RoutedEventArgs e) { _gpsPanelVisible = !_gpsPanelVisible; if (_gpsPanelVisible) { GpsSplitterRow.Height = new GridLength(4); GpsSplitter.Visibility = Visibility.Visible; GpsSectionRow.Height = new GridLength(1, GridUnitType.Star); MapMatcherComponent.Visibility = Visibility.Visible; } else { GpsSplitterRow.Height = new GridLength(0); GpsSplitter.Visibility = Visibility.Collapsed; GpsSectionRow.Height = new GridLength(0); MapMatcherComponent.Visibility = Visibility.Collapsed; } } #region Denys Wishes private void SoundDetections(object sender, RoutedEventArgs e) { MessageBox.Show("Функція Аудіоаналіз знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information); } private void RunDroneMaintenance(object sender, RoutedEventArgs e) { MessageBox.Show("Функція Аналіз стану БПЛА знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information); } #endregion private void DeleteMedia(object sender, RoutedEventArgs e) { var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFile; if (mediaFileInfo == null) return; DeleteMedia(mediaFileInfo); } public void DeleteMedia(MediaFile mediaFile) { var obj = mediaFile.MediaType == MediaTypes.Image ? "цю картинку " : "це відео "; var result = MessageBox.Show($"Видалити {obj}?", "Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result != MessageBoxResult.Yes) return; File.Delete(mediaFile.MediaUrl); AllMediaFiles.Remove(mediaFile); } } public class GradientStyleSelector : StyleSelector { public static readonly DependencyProperty ClassProviderProperty = DependencyProperty.RegisterAttached( "ClassProvider", typeof(IDetectionClassProvider), typeof(GradientStyleSelector), new PropertyMetadata(null)); public static void SetClassProvider(DependencyObject element, IDetectionClassProvider value) { element.SetValue(ClassProviderProperty, value); } public static IDetectionClassProvider GetClassProvider(DependencyObject element) { return (IDetectionClassProvider)element.GetValue(ClassProviderProperty); } public override Style? SelectStyle(object item, DependencyObject container) { if (container is not DataGridRow row || row.DataContext is not Annotation result) return null; var dataGrid = FindParent(row); var classProvider = dataGrid != null ? GetClassProvider(dataGrid) : null; var style = new Style(typeof(DataGridRow)); var brush = new LinearGradientBrush { StartPoint = new Point(0, 0), EndPoint = new Point(1, 0) }; var gradients = new List(); var colors = classProvider?.GetColors(result) ?? []; if (colors.Count == 0) { var color = (Color)ColorConverter.ConvertFromString("#40DDDDDD"); gradients = [new GradientStop(color, 0.99)]; } else { var increment = 1.0 / colors.Count; var currentStop = increment; foreach (var c in colors) { var resultColor = c.Color.ToConfidenceColor(c.Confidence); brush.GradientStops.Add(new GradientStop(resultColor, currentStop)); currentStop += increment; } } foreach (var gradientStop in gradients) brush.GradientStops.Add(gradientStop); style.Setters.Add(new Setter(Control.BackgroundProperty, brush)); return style; } private static T? FindParent(DependencyObject child) where T : DependencyObject { var parent = VisualTreeHelper.GetParent(child); while (parent != null) { if (parent is T typedParent) return typedParent; parent = VisualTreeHelper.GetParent(parent); } return null; } }