add altitude + camera spec component and calc tile size by this

also restrict detections to be no bigger than in classes.json
This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-09-23 01:48:10 +03:00
parent b0e4b467c1
commit fde9a9f418
36 changed files with 715 additions and 222 deletions
+74 -66
View File
@@ -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<DetectionClass> DefaultAnnotationClasses =
public static readonly List<DetectionClass> 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<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
private static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi", "ts"];
public static readonly List<string> 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)
@@ -0,0 +1,71 @@
<UserControl x:Class="Azaion.Common.Controls.CameraConfigControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cfg="clr-namespace:Azaion.Common.DTO.Config"
xmlns:controls="clr-namespace:Azaion.Common.Controls"
mc:Ignorable="d"
d:DesignHeight="120" d:DesignWidth="360">
<Grid Margin="4" Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="65"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="70"/>
</Grid.ColumnDefinitions>
<!-- Altitude -->
<TextBlock Grid.Row="0" Grid.Column="0"
Foreground="LightGray"
VerticalAlignment="Center" Margin="0,0,8,0" Text="Altitude, m:"/>
<Slider x:Name="AltitudeSlider" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
Minimum="0" Maximum="10000" TickFrequency="100"
IsSnapToTickEnabled="False"
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<controls:NumericUpDown x:Name="AltitudeNud"
Grid.Row="0" Grid.Column="3"
VerticalAlignment="Center"
MinValue="50"
MaxValue="5000"
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl},
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Step="10">
</controls:NumericUpDown>
<!-- Focal length -->
<TextBlock
Foreground="LightGray"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center"
Margin="0,8,8,0" Text="Focal length, mm:"/>
<controls:NumericUpDown x:Name="FocalNud"
Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
MinValue="0.1"
MaxValue="100"
Step="0.05"
VerticalAlignment="Center"
Value="{Binding Camera.CameraFocalLength, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</controls:NumericUpDown>
<!-- Sensor width -->
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center"
Foreground="LightGray"
Margin="0,8,8,0" Text="Sensor width, mm:"/>
<controls:NumericUpDown x:Name="SensorNud"
Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2" Step="0.05"
VerticalAlignment="Center"
MinValue="0.1"
MaxValue="100"
Value="{Binding Camera.CameraSensorWidth, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</controls:NumericUpDown>
</Grid>
</UserControl>
@@ -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;
}
}
+47
View File
@@ -0,0 +1,47 @@
<UserControl x:Class="Azaion.Common.Controls.NumericUpDown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Azaion.Common.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="DimGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="24" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="12" />
<RowDefinition Height="12" />
</Grid.RowDefinitions>
<TextBox Name="NudTextBox"
Background="DimGray"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
TextAlignment="Right"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}}}"
TextChanged="NUDTextBox_OnTextChanged"/>
<RepeatButton
Name="NudButtonUp"
Grid.Column="1"
Grid.Row="0"
FontSize="10"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Click="NudButtonUp_OnClick"
>^</RepeatButton>
<RepeatButton
Name="NudButtonDown"
Grid.Column="1"
Grid.Row="1"
FontSize="10"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Click="NudButtonDown_OnClick"
>˅</RepeatButton>
</Grid>
</UserControl>
@@ -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;
}
}
@@ -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; }
}
+4 -1
View File
@@ -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);
+8
View File
@@ -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; }
}
+2
View File
@@ -12,6 +12,8 @@ public class DetectionClass : ICloneable
public Color Color { get; set; }
public int MaxSizeM { get; set; }
[JsonIgnore]
public string UIName
{
+4 -1
View File
@@ -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!;
}
-24
View File
@@ -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;
}
}
+1 -1
View File
@@ -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
+2
View File
@@ -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<Annotation> Annotations => this.GetTable<Annotation>();
public ITable<AnnotationQueueRecord> AnnotationsQueueRecords => this.GetTable<AnnotationQueueRecord>();
public ITable<Detection> Detections => this.GetTable<Detection>();
public ITable<MediaFile> MediaFiles => this.GetTable<MediaFile>();
}
@@ -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<Annotation>();
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<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationQueueRecord>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME)
.HasPrimaryKey(x => x.Id)
.Property(x => x.AnnotationNames)
.HasDataType(DataType.NVarChar)
.HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject<List<string>>(str) ?? new List<string>());
builder.Entity<MediaFile>()
.HasTableName(Constants.MEDIAFILE_TABLENAME);
builder.Build();
}
}
+7 -41
View File
@@ -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<Annotation>();
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<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationQueueRecord>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME)
.HasPrimaryKey(x => x.Id)
.Property(x => x.AnnotationNames)
.HasDataType(DataType.NVarChar)
.HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject<List<string>>(str) ?? new List<string>());
builder.Build();
}
}
}
+29
View File
@@ -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;
}
}
+18
View File
@@ -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
}
+9 -3
View File
@@ -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)}";
@@ -7,7 +7,7 @@ namespace Azaion.Common.Services.Inference;
public interface IInferenceService
{
Task RunInference(List<string> mediaPaths, int tileSize, CancellationToken ct = default);
Task RunInference(List<string> 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<string> mediaPaths, int tileSize, CancellationToken ct = default)
public async Task RunInference(List<string> 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);
@@ -7,10 +7,7 @@ using Microsoft.Extensions.Logging;
namespace Azaion.Common.Services.Inference;
public class InferenceServiceEventHandler(IInferenceService inferenceService,
IAnnotationService annotationService,
IMediator mediator,
ILogger<InferenceServiceEventHandler> logger) :
public class InferenceServiceEventHandler(IInferenceService inferenceService, IAnnotationService annotationService, IMediator mediator) :
INotificationHandler<InferenceDataEvent>,
INotificationHandler<InferenceStatusEvent>,
INotificationHandler<InferenceDoneEvent>
@@ -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; }