diff --git a/Azaion.Annotator/Annotator.xaml b/Azaion.Annotator/Annotator.xaml index bff9f5f..4989fa8 100644 --- a/Azaion.Annotator/Annotator.xaml +++ b/Azaion.Annotator/Annotator.xaml @@ -76,6 +76,7 @@ + @@ -191,10 +192,20 @@ + + + + + + Grid.Row="5"> TimedAnnotations { get; set; } = new(); public string MainTitle { get; set; } + public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig(); + public Annotator( IConfigUpdater configUpdater, IOptions appConfig, @@ -73,10 +75,7 @@ public partial class Annotator IInferenceClient inferenceClient, IGpsMatcherService gpsMatcherService) { - InitializeComponent(); - - MainTitle = $"Azaion Annotator {Constants.GetLocalVersion()}"; - Title = MainTitle; + // Initialize configuration and services BEFORE InitializeComponent so bindings can see real values _appConfig = appConfig.Value; _configUpdater = configUpdater; _libVlc = libVlc; @@ -89,6 +88,14 @@ public partial class Annotator _inferenceService = inferenceService; _inferenceClient = inferenceClient; + // 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; @@ -100,7 +107,6 @@ public partial class Annotator { _appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text; await ReloadFiles(); - SaveUserSettings(); } catch (Exception e) { @@ -109,6 +115,13 @@ public partial class Annotator }; 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) @@ -118,13 +131,13 @@ public partial class Annotator _suspendLayout = true; - MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.UIConfig.LeftPanelWidth); - MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.UIConfig.RightPanelWidth); + 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; + TbFolder.Text = _appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR; - LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses); + LvClasses.Init(_appConfig?.AnnotationConfig.DetectionClasses ?? Constants.DefaultAnnotationClasses); } public void BlinkHelp(string helpText, int times = 2) @@ -212,9 +225,9 @@ public partial class Annotator private void OpenAnnotationResult(Annotation ann) { _mediaPlayer.SetPause(true); - if (!ann.IsSplit) + if (!ann.IsSplit) Editor.RemoveAllAnns(); - + _mediaPlayer.Time = (long)ann.Time.TotalMilliseconds; Dispatcher.Invoke(() => @@ -228,7 +241,7 @@ public partial class Annotator } private void SaveUserSettings() { - if (_suspendLayout) + if (_suspendLayout || _appConfig is null) return; _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; @@ -269,7 +282,7 @@ public partial class Annotator Editor.ZoomTo(new Point(canvasTileLocation.CenterX, canvasTileLocation.CenterY)); } else - Editor.CreateDetections(annotation, _appConfig.AnnotationConfig.DetectionClasses, _formState.CurrentMediaSize); + Editor.CreateDetections(annotation, _appConfig?.AnnotationConfig.DetectionClasses ?? [], _formState.CurrentMediaSize); }); } @@ -333,11 +346,12 @@ public partial class Annotator private async Task ReloadFiles() { - var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory); + var dir = new DirectoryInfo(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR); if (!dir.Exists) return; - var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x => + var videoFiles = dir.GetFiles((_appConfig?.AnnotationConfig.VideoFormats ?? Constants.DefaultVideoFormats) + .ToArray()).Select(x => { var media = new Media(_libVlc, x.FullName); media.Parse(); @@ -351,7 +365,7 @@ public partial class Annotator return fInfo; }).ToList(); - var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray()) + var imageFiles = dir.GetFiles((_appConfig?.AnnotationConfig.ImageFormats ?? Constants.DefaultImageFormats).ToArray()) .Select(x => new MediaFileInfo { Name = x.Name, @@ -383,7 +397,7 @@ public partial class Annotator { _mainCancellationSource.Cancel(); _inferenceService.StopInference(); - DetectionCancellationSource.Cancel(); + DetCancelSource.Cancel(); _mediaPlayer.Stop(); _mediaPlayer.Dispose(); @@ -417,14 +431,16 @@ public partial class Annotator { Title = "Open Video folder", IsFolderPicker = true, - InitialDirectory = Path.GetDirectoryName(_appConfig.DirectoriesConfig.VideosDirectory) + InitialDirectory = Path.GetDirectoryName(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR) }; var dialogResult = dlg.ShowDialog(); if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName)) return; - _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; + if (_appConfig is not null) + _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; + TbFolder.Text = dlg.FileName; } @@ -495,7 +511,7 @@ public partial class Annotator _isInferenceNow = true; AIDetectBtn.IsEnabled = false; - DetectionCancellationSource = new CancellationTokenSource(); + DetCancelSource = new CancellationTokenSource(); var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles) .Skip(LvFiles.SelectedIndex) @@ -504,9 +520,7 @@ public partial class Annotator if (files.Count == 0) return; - //TODO: Get Tile Size from UI based on height setup - var tileSize = 550; - await _inferenceService.RunInference(files, tileSize, DetectionCancellationSource.Token); + await _inferenceService.RunInference(files, _appConfig?.CameraConfig ?? Constants.DefaultCameraConfig, DetCancelSource.Token); LvFiles.Items.Refresh(); _isInferenceNow = false; @@ -607,7 +621,7 @@ public class GradientStyleSelector : StyleSelector foreach (var gradientStop in gradients) brush.GradientStops.Add(gradientStop); - style.Setters.Add(new Setter(DataGridRow.BackgroundProperty, brush)); + style.Setters.Add(new Setter(Control.BackgroundProperty, brush)); return style; } } diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 357acdb..aa2ed2a 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -162,7 +162,7 @@ public class AnnotatorEventHandler( break; case PlaybackControlEnum.Stop: inferenceService.StopInference(); - await mainWindow.DetectionCancellationSource.CancelAsync(); + await mainWindow.DetCancelSource.CancelAsync(); mediaPlayer.Stop(); break; case PlaybackControlEnum.PreviousFrame: diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 1f71d25..e90914f 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -22,6 +22,12 @@ public static class Constants private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1"; private const int DEFAULT_ZMQ_LOADER_PORT = 5025; + private static readonly LoaderClientConfig DefaultLoaderClientConfig = new() + { + ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST, + ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT, + ApiUrl = DEFAULT_API_URL + }; public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe"; public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe"; @@ -31,24 +37,32 @@ public static class Constants public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1"; private const int DEFAULT_ZMQ_INFERENCE_PORT = 5227; + private static readonly InferenceClientConfig DefaultInferenceClientConfig = new() + { + ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST, + ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT, + ApiUrl = DEFAULT_API_URL + }; + private const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1"; private const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5255; private const int DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT = 5256; + private static readonly GpsDeniedClientConfig DefaultGpsDeniedClientConfig = new() + { + ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST, + ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT, + ZeroMqReceiverPort = DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT + }; #endregion ExternalClientsConfig - - # region Cache keys public const string CURRENT_USER_CACHE_KEY = "CurrentUser"; - public const string HARDWARE_INFO_KEY = "HardwareInfo"; - - # endregion public const string JPG_EXT = ".jpg"; public const string TXT_EXT = ".txt"; #region DirectoriesConfig - private const string DEFAULT_VIDEO_DIR = "video"; + public const string DEFAULT_VIDEO_DIR = "video"; private const string DEFAULT_LABELS_DIR = "labels"; private const string DEFAULT_IMAGES_DIR = "images"; private const string DEFAULT_RESULTS_DIR = "results"; @@ -58,31 +72,35 @@ public static class Constants #endregion + + #region AnnotatorConfig - private static readonly List DefaultAnnotationClasses = + public static readonly List DefaultAnnotationClasses = [ - new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() }, - new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() }, - new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() }, - new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor() }, - new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() }, - new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() }, - new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() }, - new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() }, - new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor() }, - new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() }, - new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() }, - new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() }, - new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() }, - new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() }, - new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() }, - new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() }, - new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor() }, + new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor(), MaxSizeM = 7 }, + new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor(), MaxSizeM = 8 }, + new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor(), MaxSizeM = 7 }, + new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor(), MaxSizeM = 14 }, + new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor(), MaxSizeM = 9 }, + new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor(), MaxSizeM = 10 }, + new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor(), MaxSizeM = 2 }, + new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor(), MaxSizeM = 5 }, + new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor(), MaxSizeM = 7 }, + new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor(), MaxSizeM = 8 }, + new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor(), MaxSizeM = 12 }, + new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor(), MaxSizeM = 3 }, + new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor(), MaxSizeM = 14 }, + new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor(), MaxSizeM = 8 }, + new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor(), MaxSizeM = 15 }, + new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor(), MaxSizeM = 20 }, + new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor(), MaxSizeM = 10 }, + new() { Id = 17, Name = "Ammo", ShortName = "БК", Color = "#33658a".ToColor(), MaxSizeM = 2 }, + new() { Id = 18, Name = "Protect.Struct", ShortName = "Зуби.драк", Color = "#969647".ToColor(), MaxSizeM = 2 } ]; - private static readonly List DefaultVideoFormats = ["mp4", "mov", "avi"]; - private static readonly List DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; + public static readonly List DefaultVideoFormats = ["mp4", "mov", "avi", "ts"]; + public static readonly List DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; private static readonly AnnotationConfig DefaultAnnotationConfig = new() { @@ -91,9 +109,26 @@ public static class Constants ImageFormats = DefaultImageFormats, AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE }; - - private const int DEFAULT_LEFT_PANEL_WIDTH = 250; - private const int DEFAULT_RIGHT_PANEL_WIDTH = 250; + + #region UIConfig + public const int DEFAULT_LEFT_PANEL_WIDTH = 200; + public const int DEFAULT_RIGHT_PANEL_WIDTH = 200; + #endregion UIConfig + + #region CameraConfig + + public const int DEFAULT_ALTITUDE = 400; + public const decimal DEFAULT_CAMERA_FOCAL_LENGTH = 24m; + public const decimal DEFAULT_CAMERA_SENSOR_WIDTH = 23.5m; + + public static readonly CameraConfig DefaultCameraConfig = new() + { + Altitude = DEFAULT_ALTITUDE, + CameraFocalLength = DEFAULT_CAMERA_FOCAL_LENGTH, + CameraSensorWidth = DEFAULT_CAMERA_SENSOR_WIDTH + }; + + #endregion CameraConfig private const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db"; @@ -154,6 +189,7 @@ public static class Constants public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue"; public const string ADMIN_EMAIL = "admin@azaion.com"; public const string DETECTIONS_TABLENAME = "detections"; + public const string MEDIAFILE_TABLENAME = "mediafiles"; #endregion @@ -170,28 +206,14 @@ public static class Constants private static readonly InitConfig DefaultInitConfig = new() { - LoaderClientConfig = new LoaderClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST, - ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT, - ApiUrl = DEFAULT_API_URL - }, - InferenceClientConfig = new InferenceClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST, - ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT, - ApiUrl = DEFAULT_API_URL - }, - GpsDeniedClientConfig = new GpsDeniedClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST, - ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT, - ZeroMqReceiverPort = DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT - }, + LoaderClientConfig = DefaultLoaderClientConfig, + InferenceClientConfig = DefaultInferenceClientConfig, + GpsDeniedClientConfig = DefaultGpsDeniedClientConfig, DirectoriesConfig = new DirectoriesConfig { ApiResourcesDirectory = "" - } + }, + CameraConfig = DefaultCameraConfig }; public static readonly AppConfig FailsafeAppConfig = new() @@ -220,24 +242,10 @@ public static class Constants AIRecognitionConfig = DefaultAIRecognitionConfig, GpsDeniedConfig = DefaultGpsDeniedConfig, - LoaderClientConfig = new LoaderClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST, - ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT, - ApiUrl = DEFAULT_API_URL - }, - InferenceClientConfig = new InferenceClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST, - ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT, - ApiUrl = DEFAULT_API_URL - }, - GpsDeniedClientConfig = new GpsDeniedClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST, - ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT, - ZeroMqReceiverPort = DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT - } + LoaderClientConfig = DefaultLoaderClientConfig, + InferenceClientConfig = DefaultInferenceClientConfig, + GpsDeniedClientConfig = DefaultGpsDeniedClientConfig, + CameraConfig = DefaultCameraConfig }; public static InitConfig ReadInitConfig(ILogger logger) diff --git a/Azaion.Common/Controls/CameraConfigControl.xaml b/Azaion.Common/Controls/CameraConfigControl.xaml new file mode 100644 index 0000000..311caa4 --- /dev/null +++ b/Azaion.Common/Controls/CameraConfigControl.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Azaion.Common/Controls/CameraConfigControl.xaml.cs b/Azaion.Common/Controls/CameraConfigControl.xaml.cs new file mode 100644 index 0000000..e399b6a --- /dev/null +++ b/Azaion.Common/Controls/CameraConfigControl.xaml.cs @@ -0,0 +1,56 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using Azaion.Common.DTO.Config; + +namespace Azaion.Common.Controls; + +public partial class CameraConfigControl +{ + public static readonly DependencyProperty CameraProperty = DependencyProperty.Register( + nameof(Camera), typeof(CameraConfig), typeof(CameraConfigControl), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + public CameraConfig Camera + { + get => (CameraConfig)GetValue(CameraProperty) ?? new CameraConfig(); + set => SetValue(CameraProperty, value); + } + + // Fires whenever any camera parameter value changes in UI + public event EventHandler? CameraChanged; + + public CameraConfigControl() + { + InitializeComponent(); + DataContext = this; + Loaded += OnLoaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + // Hook up change notifications + if (AltitudeSlider != null) + AltitudeSlider.ValueChanged += (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty); + + SubscribeNud(AltitudeNud); + SubscribeNud(FocalNud); + SubscribeNud(SensorNud); + } + + private void SubscribeNud(UserControl? nud) + { + if (nud is NumericUpDown num) + { + var dpd = DependencyPropertyDescriptor.FromProperty(NumericUpDown.ValueProperty, typeof(NumericUpDown)); + dpd?.AddValueChanged(num, (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty)); + } + } + + // Initializes the control with the provided CameraConfig instance and wires two-way binding via dependency property + public void Init(CameraConfig cameraConfig) + { + Camera = cameraConfig; + } +} diff --git a/Azaion.Common/Controls/NumericUpDown.xaml b/Azaion.Common/Controls/NumericUpDown.xaml new file mode 100644 index 0000000..62bd90a --- /dev/null +++ b/Azaion.Common/Controls/NumericUpDown.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + ^ + ˅ + + diff --git a/Azaion.Common/Controls/NumericUpDown.xaml.cs b/Azaion.Common/Controls/NumericUpDown.xaml.cs new file mode 100644 index 0000000..f0dd6b2 --- /dev/null +++ b/Azaion.Common/Controls/NumericUpDown.xaml.cs @@ -0,0 +1,101 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Controls; + +namespace Azaion.Common.Controls; + +public partial class NumericUpDown : UserControl +{ + public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register( + nameof(MinValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(0m)); + + public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register( + nameof(MaxValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(100m)); + + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( + nameof(Value), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(10m, OnValueChanged)); + + public static readonly DependencyProperty StepProperty = DependencyProperty.Register( + nameof(Step), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(1m)); + + public decimal MinValue + { + get => (decimal)GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + public decimal MaxValue + { + get => (decimal)GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + public decimal Value + { + get => (decimal)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + public decimal Step + { + get => (decimal)GetValue(StepProperty); + set => SetValue(StepProperty, value); + } + + public NumericUpDown() + { + InitializeComponent(); + } + + private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is NumericUpDown control) + { + control.NudTextBox.Text = ((decimal)e.NewValue).ToString(CultureInfo.InvariantCulture); + control.NudTextBox.SelectionStart = control.NudTextBox.Text.Length; + } + } + + private void NUDTextBox_OnTextChanged(object sender, TextChangedEventArgs e) + { + if (string.IsNullOrEmpty(NudTextBox.Text) || !decimal.TryParse(NudTextBox.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + { + NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture); + return; + } + if (number > MaxValue ) + { + Value = MaxValue; + NudTextBox.Text = MaxValue.ToString(CultureInfo.InvariantCulture); + } + else if (number < MinValue) + { + Value = MinValue; + NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture); + } + else + { + Value = number; + } + + NudTextBox.SelectionStart = NudTextBox.Text.Length; + } + + private void NudButtonUp_OnClick(object sender, RoutedEventArgs e) + { + var step = Step <= 0 ? 1m : Step; + var newVal = Math.Min(MaxValue, Value + step); + Value = newVal; + NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture); + NudTextBox.SelectionStart = NudTextBox.Text.Length; + } + + private void NudButtonDown_OnClick(object sender, RoutedEventArgs e) + { + var step = Step <= 0 ? 1m : Step; + var newVal = Math.Max(MinValue, Value - step); + Value = newVal; + NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture); + NudTextBox.SelectionStart = NudTextBox.Text.Length; + } +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/AIRecognitionConfig.cs b/Azaion.Common/DTO/Config/AIRecognitionConfig.cs index 0b14fb9..08b3b84 100644 --- a/Azaion.Common/DTO/Config/AIRecognitionConfig.cs +++ b/Azaion.Common/DTO/Config/AIRecognitionConfig.cs @@ -18,5 +18,8 @@ public class AIRecognitionConfig [Key("m_bs")] public int ModelBatchSize { get; set; } = 4; [Key("ov_p")] public double BigImageTileOverlapPercent { get; set; } - [Key("tile_size")] public int TileSize { get; set; } + + [Key("cam_a")] public double Altitude { get; set; } + [Key("cam_fl")] public double CameraFocalLength { get; set; } + [Key("cam_sw")] public double CameraSensorWidth { get; set; } } \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index e2acca0..b4017a5 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -28,6 +28,8 @@ public class AppConfig public MapConfig MapConfig{ get; set; } = null!; public GpsDeniedConfig GpsDeniedConfig { get; set; } = null!; + + public CameraConfig CameraConfig { get; set; } = null!; } public interface IConfigUpdater @@ -61,7 +63,8 @@ public class ConfigUpdater : IConfigUpdater config.InferenceClientConfig, config.GpsDeniedClientConfig, config.DirectoriesConfig, - config.UIConfig + config.UIConfig, + config.CameraConfig }; await File.WriteAllTextAsync(Constants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8); diff --git a/Azaion.Common/DTO/Config/CameraConfig.cs b/Azaion.Common/DTO/Config/CameraConfig.cs new file mode 100644 index 0000000..b567f2a --- /dev/null +++ b/Azaion.Common/DTO/Config/CameraConfig.cs @@ -0,0 +1,8 @@ +namespace Azaion.Common.DTO.Config; + +public class CameraConfig +{ + public decimal Altitude { get; set; } + public decimal CameraFocalLength { get; set; } + public decimal CameraSensorWidth { get; set; } +} \ No newline at end of file diff --git a/Azaion.Common/DTO/DetectionClass.cs b/Azaion.Common/DTO/DetectionClass.cs index fa79eb3..c57bae5 100644 --- a/Azaion.Common/DTO/DetectionClass.cs +++ b/Azaion.Common/DTO/DetectionClass.cs @@ -12,6 +12,8 @@ public class DetectionClass : ICloneable public Color Color { get; set; } + public int MaxSizeM { get; set; } + [JsonIgnore] public string UIName { diff --git a/Azaion.Common/DTO/InitConfig.cs b/Azaion.Common/DTO/InitConfig.cs index 74fb984..88a3c3f 100644 --- a/Azaion.Common/DTO/InitConfig.cs +++ b/Azaion.Common/DTO/InitConfig.cs @@ -1,4 +1,6 @@ -namespace Azaion.Common.DTO; +using Azaion.Common.DTO.Config; + +namespace Azaion.Common.DTO; public class InitConfig { @@ -6,4 +8,5 @@ public class InitConfig public InferenceClientConfig InferenceClientConfig { get; set; } = null!; public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!; public DirectoriesConfig DirectoriesConfig { get; set; } = null!; + public CameraConfig CameraConfig { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.Common/DTO/Label.cs b/Azaion.Common/DTO/Label.cs index 4e486c5..ae60596 100644 --- a/Azaion.Common/DTO/Label.cs +++ b/Azaion.Common/DTO/Label.cs @@ -218,27 +218,3 @@ public class YoloLabel : Label public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); } - -[MessagePackObject] -public class Detection : YoloLabel -{ - [JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!; - [JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; } - [JsonProperty(PropertyName = "dn")][Key("dn")] public string Description { get; set; } - [JsonProperty(PropertyName = "af")][Key("af")] public AffiliationEnum Affiliation { get; set; } - - //For db & serialization - public Detection(){} - - public Detection(string annotationName, YoloLabel label, string description = "", double confidence = 1) - { - AnnotationName = annotationName; - Description = description; - ClassNumber = label.ClassNumber; - CenterX = label.CenterX; - CenterY = label.CenterY; - Height = label.Height; - Width = label.Width; - Confidence = confidence; - } -} \ No newline at end of file diff --git a/Azaion.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index 172ab04..dd0879b 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -78,7 +78,7 @@ public class Annotation .Select(d => (DetectionClassesDict[d.ClassNumber].Color, d.Confidence)) .ToList(); - private string _className; + private string? _className; [IgnoreMember] public string ClassName { get diff --git a/Azaion.Common/Database/AnnotationsDb.cs b/Azaion.Common/Database/AnnotationsDb.cs index c8434d9..9090420 100644 --- a/Azaion.Common/Database/AnnotationsDb.cs +++ b/Azaion.Common/Database/AnnotationsDb.cs @@ -1,4 +1,5 @@ using Azaion.Common.DTO; +using CsvHelper; using LinqToDB; using LinqToDB.Data; @@ -9,4 +10,5 @@ public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions public ITable Annotations => this.GetTable(); public ITable AnnotationsQueueRecords => this.GetTable(); public ITable Detections => this.GetTable(); + public ITable MediaFiles => this.GetTable(); } \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationsDbSchemaHolder.cs b/Azaion.Common/Database/AnnotationsDbSchemaHolder.cs new file mode 100644 index 0000000..366b53f --- /dev/null +++ b/Azaion.Common/Database/AnnotationsDbSchemaHolder.cs @@ -0,0 +1,45 @@ +using LinqToDB; +using LinqToDB.Mapping; +using Newtonsoft.Json; + +namespace Azaion.Common.Database; + +public static class AnnotationsDbSchemaHolder +{ + public static readonly MappingSchema MappingSchema; + + static AnnotationsDbSchemaHolder() + { + MappingSchema = new MappingSchema(); + var builder = new FluentMappingBuilder(MappingSchema); + + var annotationBuilder = builder.Entity(); + annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME) + .HasPrimaryKey(x => x.Name) + .Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName) + .Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t)); + + annotationBuilder + .Ignore(x => x.Milliseconds) + .Ignore(x => x.Classes) + .Ignore(x => x.Classes) + .Ignore(x => x.ImagePath) + .Ignore(x => x.LabelPath) + .Ignore(x => x.ThumbPath); + + builder.Entity() + .HasTableName(Constants.DETECTIONS_TABLENAME); + + builder.Entity() + .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME) + .HasPrimaryKey(x => x.Id) + .Property(x => x.AnnotationNames) + .HasDataType(DataType.NVarChar) + .HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject>(str) ?? new List()); + + builder.Entity() + .HasTableName(Constants.MEDIAFILE_TABLENAME); + + builder.Build(); + } +} \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index 6103939..92ca53a 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -6,10 +6,8 @@ using Azaion.Common.DTO.Config; using Azaion.Common.Extensions; using LinqToDB; using LinqToDB.DataProvider.SQLite; -using LinqToDB.Mapping; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Serilog; namespace Azaion.Common.Database; @@ -64,7 +62,12 @@ public class DbFactory : IDbFactory _fileConnection.Open(); using var db = new AnnotationsDb(_fileDataOptions); - SchemaMigrator.EnsureSchemaUpdated(db, typeof(Annotation), typeof(Detection)); + var entityTypes = typeof(AnnotationsDb) + .GetProperties() + .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(ITable<>)) + .Select(p => p.PropertyType.GetGenericArguments()[0]) + .ToArray(); + SchemaMigrator.EnsureSchemaUpdated(db, entityTypes); _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); } @@ -145,41 +148,4 @@ public class DbFactory : IDbFactory _logger.LogInformation($"Deleted {detDeleted} detections, {annDeleted} annotations"); }); } -} - -public static class AnnotationsDbSchemaHolder -{ - public static readonly MappingSchema MappingSchema; - - static AnnotationsDbSchemaHolder() - { - MappingSchema = new MappingSchema(); - var builder = new FluentMappingBuilder(MappingSchema); - - var annotationBuilder = builder.Entity(); - annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME) - .HasPrimaryKey(x => x.Name) - .Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName) - .Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t)); - - annotationBuilder - .Ignore(x => x.Milliseconds) - .Ignore(x => x.Classes) - .Ignore(x => x.Classes) - .Ignore(x => x.ImagePath) - .Ignore(x => x.LabelPath) - .Ignore(x => x.ThumbPath); - - builder.Entity() - .HasTableName(Constants.DETECTIONS_TABLENAME); - - builder.Entity() - .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME) - .HasPrimaryKey(x => x.Id) - .Property(x => x.AnnotationNames) - .HasDataType(DataType.NVarChar) - .HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject>(str) ?? new List()); - - builder.Build(); - } -} +} \ No newline at end of file diff --git a/Azaion.Common/Database/Detection.cs b/Azaion.Common/Database/Detection.cs new file mode 100644 index 0000000..e99849f --- /dev/null +++ b/Azaion.Common/Database/Detection.cs @@ -0,0 +1,29 @@ +using Azaion.Common.DTO; +using MessagePack; +using Newtonsoft.Json; + +namespace Azaion.Common.Database; + +[MessagePackObject] +public class Detection : YoloLabel +{ + [JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!; + [JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; } + [JsonProperty(PropertyName = "dn")][Key("dn")] public string Description { get; set; } + [JsonProperty(PropertyName = "af")][Key("af")] public AffiliationEnum Affiliation { get; set; } + + //For db & serialization + public Detection(){} + + public Detection(string annotationName, YoloLabel label, string description = "", double confidence = 1) + { + AnnotationName = annotationName; + Description = description; + ClassNumber = label.ClassNumber; + CenterX = label.CenterX; + CenterY = label.CenterY; + Height = label.Height; + Width = label.Width; + Confidence = confidence; + } +} \ No newline at end of file diff --git a/Azaion.Common/Database/MediaFile.cs b/Azaion.Common/Database/MediaFile.cs new file mode 100644 index 0000000..b35ac91 --- /dev/null +++ b/Azaion.Common/Database/MediaFile.cs @@ -0,0 +1,18 @@ +namespace Azaion.Common.Database; + +public class MediaFile +{ + public string Name { get; set; } = null!; + public string LocalPath { get; set; } = null!; + public DateTime? ProcessedDate { get; set; } + public MediaDetectionStatus MediaDetectionStatus { get; set; } = MediaDetectionStatus.New; +} + +public enum MediaDetectionStatus +{ + None, + New, + Processing, + Processed, + Error +} \ No newline at end of file diff --git a/Azaion.Common/Database/SchemaMigrator.cs b/Azaion.Common/Database/SchemaMigrator.cs index eb043fb..1f5cff5 100644 --- a/Azaion.Common/Database/SchemaMigrator.cs +++ b/Azaion.Common/Database/SchemaMigrator.cs @@ -21,12 +21,18 @@ public static class SchemaMigrator var entityDescriptor = mappingSchema.GetEntityDescriptor(type); var tableName = entityDescriptor.Name.Name; var existingColumns = GetTableColumns(connection, tableName); + if (existingColumns.Count == 0) // table does not exist + { + var columnDefinitions = entityDescriptor.Columns.Select(GetColumnDefinition); + dbConnection.Execute($"CREATE TABLE {tableName} ({string.Join(", ", columnDefinitions)})"); + continue; + } foreach (var column in entityDescriptor.Columns) { - if (existingColumns.Contains(column.ColumnName, StringComparer.OrdinalIgnoreCase)) + if (existingColumns.Contains(column.ColumnName, StringComparer.OrdinalIgnoreCase)) continue; - + var columnDefinition = GetColumnDefinition(column); dbConnection.Execute($"ALTER TABLE {tableName} ADD COLUMN {columnDefinition}"); } @@ -87,7 +93,7 @@ public static class SchemaMigrator return $"NOT NULL DEFAULT {(Convert.ToBoolean(defaultValue) ? 1 : 0)}"; if (underlyingType.IsEnum) - return $"NOT NULL DEFAULT {(int)defaultValue}"; + return $"NOT NULL DEFAULT {(int)(defaultValue ?? 0)}"; if (underlyingType.IsValueType && defaultValue is IFormattable f) return $"NOT NULL DEFAULT {f.ToString(null, System.Globalization.CultureInfo.InvariantCulture)}"; diff --git a/Azaion.Common/Services/Inference/InferenceService.cs b/Azaion.Common/Services/Inference/InferenceService.cs index 57357ac..542aeda 100644 --- a/Azaion.Common/Services/Inference/InferenceService.cs +++ b/Azaion.Common/Services/Inference/InferenceService.cs @@ -7,7 +7,7 @@ namespace Azaion.Common.Services.Inference; public interface IInferenceService { - Task RunInference(List mediaPaths, int tileSize, CancellationToken ct = default); + Task RunInference(List mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default); CancellationTokenSource InferenceCancelTokenSource { get; set; } CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; } void StopInference(); @@ -44,14 +44,16 @@ public class InferenceService : IInferenceService } } - public async Task RunInference(List mediaPaths, int tileSize, CancellationToken ct = default) + public async Task RunInference(List mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default) { InferenceCancelTokenSource = new CancellationTokenSource(); _client.Send(RemoteCommand.Create(CommandType.Login, _azaionApi.Credentials)); var aiConfig = _aiConfigOptions.Value; aiConfig.Paths = mediaPaths; - aiConfig.TileSize = tileSize; + aiConfig.Altitude = (double)cameraConfig.Altitude; + aiConfig.CameraFocalLength = (double)cameraConfig.CameraFocalLength; + aiConfig.CameraSensorWidth = (double)cameraConfig.CameraSensorWidth; _client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig)); using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct, InferenceCancelTokenSource.Token); diff --git a/Azaion.Common/Services/Inference/InferenceServiceEventHandler.cs b/Azaion.Common/Services/Inference/InferenceServiceEventHandler.cs index 5364285..c134828 100644 --- a/Azaion.Common/Services/Inference/InferenceServiceEventHandler.cs +++ b/Azaion.Common/Services/Inference/InferenceServiceEventHandler.cs @@ -7,10 +7,7 @@ using Microsoft.Extensions.Logging; namespace Azaion.Common.Services.Inference; -public class InferenceServiceEventHandler(IInferenceService inferenceService, - IAnnotationService annotationService, - IMediator mediator, - ILogger logger) : +public class InferenceServiceEventHandler(IInferenceService inferenceService, IAnnotationService annotationService, IMediator mediator) : INotificationHandler, INotificationHandler, INotificationHandler diff --git a/Azaion.Common/Services/Inference/InferenceServiceEvents.cs b/Azaion.Common/Services/Inference/InferenceServiceEvents.cs index abec0fc..5afa305 100644 --- a/Azaion.Common/Services/Inference/InferenceServiceEvents.cs +++ b/Azaion.Common/Services/Inference/InferenceServiceEvents.cs @@ -13,7 +13,7 @@ public class InferenceDataEvent(AnnotationImage annotationImage) : INotification public class InferenceStatusEvent : INotification { [Key("mn")] - public string MediaName { get; set; } + public string? MediaName { get; set; } [Key("dc")] public int DetectionsCount { get; set; } diff --git a/Azaion.Inference/ai_config.pxd b/Azaion.Inference/ai_config.pxd index de1e8ea..18a49fb 100644 --- a/Azaion.Inference/ai_config.pxd +++ b/Azaion.Inference/ai_config.pxd @@ -9,11 +9,14 @@ cdef class AIRecognitionConfig: cdef public double tracking_intersection_threshold cdef public int big_image_tile_overlap_percent - cdef public int tile_size cdef public bytes file_data cdef public list[str] paths cdef public int model_batch_size + cdef public double altitude + cdef public double focal_length + cdef public double sensor_width + @staticmethod cdef from_msgpack(bytes data) diff --git a/Azaion.Inference/ai_config.pyx b/Azaion.Inference/ai_config.pyx index 58a1198..54b1440 100644 --- a/Azaion.Inference/ai_config.pyx +++ b/Azaion.Inference/ai_config.pyx @@ -15,7 +15,10 @@ cdef class AIRecognitionConfig: model_batch_size, big_image_tile_overlap_percent, - tile_size + + altitude, + focal_length, + sensor_width ): self.frame_period_recognition = frame_period_recognition self.frame_recognition_seconds = frame_recognition_seconds @@ -30,7 +33,10 @@ cdef class AIRecognitionConfig: self.model_batch_size = model_batch_size self.big_image_tile_overlap_percent = big_image_tile_overlap_percent - self.tile_size = tile_size + + self.altitude = altitude + self.focal_length = focal_length + self.sensor_width = sensor_width def __str__(self): return (f'frame_seconds : {self.frame_recognition_seconds}, distance_confidence : {self.tracking_distance_confidence}, ' @@ -39,7 +45,11 @@ cdef class AIRecognitionConfig: f'frame_period_recognition : {self.frame_period_recognition}, ' f'big_image_tile_overlap_percent: {self.big_image_tile_overlap_percent}, ' f'paths: {self.paths}, ' - f'model_batch_size: {self.model_batch_size}') + f'model_batch_size: {self.model_batch_size}, ' + f'altitude: {self.altitude}, ' + f'focal_length: {self.focal_length}, ' + f'sensor_width: {self.sensor_width}' + ) @staticmethod cdef from_msgpack(bytes data): @@ -58,5 +68,8 @@ cdef class AIRecognitionConfig: unpacked.get("m_bs"), unpacked.get("ov_p", 20), - unpacked.get("tile_size", 550), + + unpacked.get("cam_a", 400), + unpacked.get("cam_fl", 24), + unpacked.get("cam_sw", 23.5) ) \ No newline at end of file diff --git a/Azaion.Inference/annotation.pyx b/Azaion.Inference/annotation.pyx index 485c5cb..ca9a82d 100644 --- a/Azaion.Inference/annotation.pyx +++ b/Azaion.Inference/annotation.pyx @@ -48,7 +48,7 @@ cdef class Annotation: return f"{self.name}: No detections" detections_str = ", ".join( - f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})" + f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f}) ({d.w:.2f}, {d.h:.2f})" for d in self.detections ) return f"{self.name}: {detections_str}" diff --git a/Azaion.Inference/build_inference.cmd b/Azaion.Inference/build_inference.cmd index c4908c9..2a3a73f 100644 --- a/Azaion.Inference/build_inference.cmd +++ b/Azaion.Inference/build_inference.cmd @@ -1,7 +1,7 @@ echo Build Cython app set CURRENT_DIR=%cd% -REM Change to the parent directory of the current location +REM Change to the file's directory cd /d %~dp0 echo remove dist folder: @@ -58,5 +58,6 @@ robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "onnx_engi robocopy "dist\azaion-inference\_internal" "..\dist-dlls\_internal" /E robocopy "dist\azaion-inference" "..\dist-azaion" "azaion-inference.exe" +robocopy "." "..\dist-azaion" "classes.json" cd /d %CURRENT_DIR% diff --git a/Azaion.Inference/classes.json b/Azaion.Inference/classes.json new file mode 100644 index 0000000..4ba2d0a --- /dev/null +++ b/Azaion.Inference/classes.json @@ -0,0 +1,21 @@ +[ + { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#ff0000", "MaxSizeM": 8 }, + { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00ff00", "MaxSizeM": 8 }, + { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000ff", "MaxSizeM": 7 }, + { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#ffff00", "MaxSizeM": 14 }, + { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#ff00ff", "MaxSizeM": 9 }, + { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00ffff", "MaxSizeM": 10 }, + { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021", "MaxSizeM": 2 }, + { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000", "MaxSizeM": 5 }, + { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000", "MaxSizeM": 7 }, + { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080", "MaxSizeM": 8 }, + { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#a52a2a", "MaxSizeM": 12 }, + { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000", "MaxSizeM": 3 }, + { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#87ceeb", "MaxSizeM": 14 }, + { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f", "MaxSizeM": 8 }, + { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff", "MaxSizeM": 15 }, + { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1", "MaxSizeM": 20 }, + { "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500", "MaxSizeM": 10 }, + { "Id": 17, "Name": "Ammo", "ShortName": "БК", "Color": "#33658a", "MaxSizeM": 2 }, + { "Id": 18, "Name": "Protect.Struct", "ShortName": "Зуби.драк", "Color": "#969647", "MaxSizeM": 2 } +] \ No newline at end of file diff --git a/Azaion.Inference/constants_inf.pxd b/Azaion.Inference/constants_inf.pxd index 0dad79f..f5573eb 100644 --- a/Azaion.Inference/constants_inf.pxd +++ b/Azaion.Inference/constants_inf.pxd @@ -14,8 +14,23 @@ cdef str MODELS_FOLDER cdef int SMALL_SIZE_KB cdef str SPLIT_SUFFIX -cdef int TILE_DUPLICATE_CONFIDENCE_THRESHOLD +cdef double TILE_DUPLICATE_CONFIDENCE_THRESHOLD +cdef int METERS_IN_TILE cdef log(str log_message) cdef logerror(str error) -cdef format_time(int ms) \ No newline at end of file +cdef format_time(int ms) + +cdef dict[int, AnnotationClass] annotations_dict + +cdef class AnnotationClass: + cdef public int id + cdef public str name + cdef public str color + cdef public int max_object_size_meters + +cdef enum WeatherMode: + Norm = 0 + Wint = 20 + Night = 40 + diff --git a/Azaion.Inference/constants_inf.pyx b/Azaion.Inference/constants_inf.pyx index 1630bc5..4b515bf 100644 --- a/Azaion.Inference/constants_inf.pyx +++ b/Azaion.Inference/constants_inf.pyx @@ -1,3 +1,4 @@ +import json import sys from loguru import logger @@ -13,7 +14,37 @@ cdef str MODELS_FOLDER = "models" cdef int SMALL_SIZE_KB = 3 cdef str SPLIT_SUFFIX = "!split!" -cdef int TILE_DUPLICATE_CONFIDENCE_THRESHOLD = 5 +cdef double TILE_DUPLICATE_CONFIDENCE_THRESHOLD = 0.01 +cdef int METERS_IN_TILE = 25 + +cdef class AnnotationClass: + def __init__(self, id, name, color, max_object_size_meters): + self.id = id + self.name = name + self.color = color + self.max_object_size_meters = max_object_size_meters + + def __str__(self): + return f'{self.id} {self.name} {self.color} {self.max_object_size_meters}' + +cdef int weather_switcher_increase = 20 + +WEATHER_MODE_NAMES = { + Norm: "Norm", + Wint: "Wint", + Night: "Night" +} + +with open('classes.json', 'r', encoding='utf-8') as f: + j = json.loads(f.read()) + annotations_dict = {} + + for i in range(0, weather_switcher_increase * 3, weather_switcher_increase): + for cl in j: + id = i + cl['Id'] + mode_name = WEATHER_MODE_NAMES.get(i, "Unknown") + name = cl['Name'] if i == 0 else f'{cl["Name"]}({mode_name})' + annotations_dict[id] = AnnotationClass(id, name, cl['Color'], cl['MaxSizeM']) logger.remove() log_format = "[{time:HH:mm:ss} {level}] {message}" diff --git a/Azaion.Inference/inference.pxd b/Azaion.Inference/inference.pxd index bfd4ba8..dfd64a3 100644 --- a/Azaion.Inference/inference.pxd +++ b/Azaion.Inference/inference.pxd @@ -31,7 +31,7 @@ cdef class Inference: cdef run_inference(self, RemoteCommand cmd) cdef _process_video(self, RemoteCommand cmd, AIRecognitionConfig ai_config, str video_name) cdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths) - cdef _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, double ground_sampling_distance) cdef on_annotation(self, RemoteCommand cmd, Annotation annotation) cdef split_to_tiles(self, frame, path, tile_size, overlap_percent) cdef stop(self) @@ -43,5 +43,5 @@ cdef class Inference: cdef split_list_extend(self, lst, chunk_size) cdef bint is_valid_video_annotation(self, Annotation annotation, AIRecognitionConfig ai_config) - cdef bint is_valid_image_annotation(self, Annotation annotation) + cdef bint is_valid_image_annotation(self, Annotation annotation, double ground_sampling_distance, frame_shape) cdef remove_tiled_duplicates(self, Annotation annotation) diff --git a/Azaion.Inference/inference.pyx b/Azaion.Inference/inference.pyx index 3f39b9c..15b9bb9 100644 --- a/Azaion.Inference/inference.pyx +++ b/Azaion.Inference/inference.pyx @@ -315,18 +315,24 @@ cdef class Inference: constants_inf.logerror(f'Failed to read image {path}') continue original_media_name = Path( path).stem.replace(" ", "") + + ground_sampling_distance = ai_config.sensor_width * ai_config.altitude / (ai_config.focal_length * img_w) + constants_inf.log(f'ground sampling distance: {ground_sampling_distance}') + if img_h <= 1.5 * self.model_height and img_w <= 1.5 * self.model_width: frame_data.append((frame, original_media_name, f'{original_media_name}_000000')) else: - res = self.split_to_tiles(frame, path, ai_config.tile_size, ai_config.big_image_tile_overlap_percent) + tile_size = int(constants_inf.METERS_IN_TILE / ground_sampling_distance) + constants_inf.log( f'calc tile size: {tile_size}') + res = self.split_to_tiles(frame, path, tile_size, ai_config.big_image_tile_overlap_percent) 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) + self._process_images_inner(cmd, ai_config, chunk, ground_sampling_distance) self.send_detection_status(cmd.client_id) 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, ground_sampling_distance) self.send_detection_status(cmd.client_id) cdef send_detection_status(self, client_id): @@ -369,7 +375,7 @@ cdef class Inference: results.append((tile, original_media_name, name)) return results - cdef _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, double ground_sampling_distance): cdef list frames, original_media_names, names cdef Annotation annotation cdef int i @@ -381,7 +387,7 @@ cdef class Inference: list_detections = self.postprocess(outputs, ai_config) for i in range(len(list_detections)): annotation = Annotation(names[i], original_media_names[i], 0, list_detections[i]) - if self.is_valid_image_annotation(annotation): + if self.is_valid_image_annotation(annotation, ground_sampling_distance, frames[i].shape): constants_inf.log( f'Detected {annotation}') _, image = cv2.imencode('.jpg', frames[i]) annotation.image = image.tobytes() @@ -391,6 +397,7 @@ cdef class Inference: self.stop_signal = True cdef remove_tiled_duplicates(self, Annotation annotation): + # Parse tile info from the annotation name right = annotation.name.rindex('!') left = annotation.name.index(constants_inf.SPLIT_SUFFIX) + len(constants_inf.SPLIT_SUFFIX) tile_size_str, x_str, y_str = annotation.name[left:right].split('_') @@ -398,19 +405,46 @@ cdef class Inference: x = int(x_str) y = int(y_str) + # This will be our new, filtered list of detections + cdef list[Detection] unique_detections = [] + + existing_abs_detections = self._tile_detections.setdefault(annotation.original_media_name, []) + for det in annotation.detections: + # Calculate the absolute position and size of the detection x1 = det.x * tile_size y1 = det.y * tile_size det_abs = Detection(x + x1, y + y1, det.w * tile_size, det.h * tile_size, 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 it's not a duplicate, keep it and update the cache + if det_abs not in existing_abs_detections: + unique_detections.append(det) + existing_abs_detections.append(det_abs) + + annotation.detections = unique_detections + + cdef bint is_valid_image_annotation(self, Annotation annotation, double ground_sampling_distance, frame_shape): if constants_inf.SPLIT_SUFFIX in annotation.name: self.remove_tiled_duplicates(annotation) + img_h, img_w, _ = frame_shape + if annotation.detections: + constants_inf.log( f'Initial ann: {annotation}') + + cdef list[Detection] valid_detections = [] + for det in annotation.detections: + m_w = det.w * img_w * ground_sampling_distance + m_h = det.h * img_h * ground_sampling_distance + max_size = constants_inf.annotations_dict[det.cls].max_object_size_meters + + if m_w <= max_size and m_h <= max_size: + valid_detections.append(det) + constants_inf.log( f'Kept ({m_w} {m_h}) <= {max_size}. class: {constants_inf.annotations_dict[det.cls].name}') + else: + constants_inf.log( f'Removed ({m_w} {m_h}) > {max_size}. class: {constants_inf.annotations_dict[det.cls].name}') + + # Replace the old list with the new, filtered one + annotation.detections = valid_detections + if not annotation.detections: return False return True diff --git a/Azaion.Suite/build_loader_inf.cmd b/Azaion.Suite/build_loader_inf.cmd index be08122..e975323 100644 --- a/Azaion.Suite/build_loader_inf.cmd +++ b/Azaion.Suite/build_loader_inf.cmd @@ -1,3 +1,3 @@ -call ..\Azaion.Inference\build_inference -call ..\Azaion.Loader\build_loader -call copy_loader_inf \ No newline at end of file +call ..\Azaion.Inference\build_inference.cmd +call ..\Azaion.Loader\build_loader.cmd +call copy_loader_inf.cmd \ No newline at end of file diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index 711fdb7..9fee45b 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -30,5 +30,10 @@ "GenerateAnnotatedImage": true, "SilentDetection": false, "ShowDatasetWithDetectionsOnly": false + }, + "CameraConfig": { + "Altitude": 400, + "CameraSensorWidth": 23.5, + "CameraFocalLength": 24 } } \ No newline at end of file diff --git a/Azaion.Suite/config.system.json b/Azaion.Suite/config.system.json index 7b2dce5..99f9cb7 100644 --- a/Azaion.Suite/config.system.json +++ b/Azaion.Suite/config.system.json @@ -1,23 +1,25 @@ { "AnnotationConfig": { "DetectionClasses": [ - { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#ff0000" }, - { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00ff00" }, - { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000ff" }, - { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#ffff00" }, - { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#ff00ff" }, - { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00ffff" }, - { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021" }, - { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000" }, - { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000" }, - { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, - { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#a52a2a" }, - { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, - { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#87ceeb" }, - { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" }, - { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" }, - { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" }, - { "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500" } + { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#ff0000", "MaxSizeM": 7 }, + { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00ff00", "MaxSizeM": 8 }, + { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000ff", "MaxSizeM": 7 }, + { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#ffff00", "MaxSizeM": 14 }, + { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#ff00ff", "MaxSizeM": 9 }, + { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00ffff", "MaxSizeM": 10 }, + { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021", "MaxSizeM": 2 }, + { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000", "MaxSizeM": 5 }, + { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000", "MaxSizeM": 7 }, + { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080", "MaxSizeM": 8 }, + { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#a52a2a", "MaxSizeM": 12 }, + { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000", "MaxSizeM": 3 }, + { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#87ceeb", "MaxSizeM": 14 }, + { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f", "MaxSizeM": 8 }, + { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff", "MaxSizeM": 15 }, + { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1", "MaxSizeM": 20 }, + { "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500", "MaxSizeM": 10 }, + { "Id": 17, "Name": "Ammo", "ShortName": "БК", "Color": "#33658a", "MaxSizeM": 2 }, + { "Id": 18, "Name": "Protect.Struct", "ShortName": "Зуби.драк", "Color": "#969647", "MaxSizeM": 2 } ], "VideoFormats": [ ".mp4", ".mov", ".avi", ".ts", ".mkv" ], "ImageFormats": [ ".jpg", ".jpeg", ".png", ".bmp" ],