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" ],