mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 05:16:29 +00:00
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:
@@ -76,6 +76,7 @@
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="28"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="80"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -191,10 +192,20 @@
|
||||
</GridView>
|
||||
</ListView.View>
|
||||
</ListView>
|
||||
|
||||
<controls1:CameraConfigControl
|
||||
x:Name="CameraConfigControl"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4"
|
||||
Camera="{Binding Camera, RelativeSource={RelativeSource AncestorType=Window}, Mode=OneWay}"
|
||||
>
|
||||
|
||||
</controls1:CameraConfigControl>
|
||||
|
||||
<controls1:DetectionClasses
|
||||
x:Name="LvClasses"
|
||||
Grid.Column="0"
|
||||
Grid.Row="4">
|
||||
Grid.Row="5">
|
||||
</controls1:DetectionClasses>
|
||||
|
||||
<GridSplitter
|
||||
@@ -202,7 +213,7 @@
|
||||
ResizeDirection="Columns"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="4"
|
||||
Grid.RowSpan="5"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
@@ -211,7 +222,7 @@
|
||||
<wpf:VideoView
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="4"
|
||||
Grid.RowSpan="5"
|
||||
x:Name="VideoView">
|
||||
<controls1:CanvasEditor x:Name="Editor"
|
||||
Background="#01000000"
|
||||
@@ -224,7 +235,7 @@
|
||||
ResizeDirection="Columns"
|
||||
Grid.Column="3"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="4"
|
||||
Grid.RowSpan="5"
|
||||
ResizeBehavior="PreviousAndNext"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
@@ -234,7 +245,7 @@
|
||||
<DataGrid x:Name="DgAnnotations"
|
||||
Grid.Column="4"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="4"
|
||||
Grid.RowSpan="5"
|
||||
Background="Black"
|
||||
Foreground="White"
|
||||
RowHeaderWidth="0"
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Azaion.Annotator;
|
||||
|
||||
public partial class Annotator
|
||||
{
|
||||
private readonly AppConfig _appConfig;
|
||||
private readonly AppConfig? _appConfig;
|
||||
private readonly LibVLC _libVlc;
|
||||
private readonly MediaPlayer _mediaPlayer;
|
||||
private readonly IMediator _mediator;
|
||||
@@ -41,12 +41,12 @@ public partial class Annotator
|
||||
private readonly IDbFactory _dbFactory;
|
||||
private readonly IInferenceService _inferenceService;
|
||||
private readonly IInferenceClient _inferenceClient;
|
||||
|
||||
|
||||
private bool _suspendLayout;
|
||||
private bool _gpsPanelVisible;
|
||||
|
||||
private readonly CancellationTokenSource _mainCancellationSource = new();
|
||||
public CancellationTokenSource DetectionCancellationSource = new();
|
||||
public CancellationTokenSource DetCancelSource = new();
|
||||
private bool _isInferenceNow;
|
||||
|
||||
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
|
||||
@@ -59,6 +59,8 @@ public partial class Annotator
|
||||
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
|
||||
public string MainTitle { get; set; }
|
||||
|
||||
public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig();
|
||||
|
||||
public Annotator(
|
||||
IConfigUpdater configUpdater,
|
||||
IOptions<AppConfig> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
+74
-66
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -12,6 +12,8 @@ public class DetectionClass : ICloneable
|
||||
|
||||
public Color Color { get; set; }
|
||||
|
||||
public int MaxSizeM { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string UIName
|
||||
{
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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}"
|
||||
|
||||
@@ -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%
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -315,18 +315,24 @@ cdef class Inference:
|
||||
constants_inf.logerror(<str>f'Failed to read image {path}')
|
||||
continue
|
||||
original_media_name = Path(<str> path).stem.replace(" ", "")
|
||||
|
||||
ground_sampling_distance = ai_config.sensor_width * ai_config.altitude / (ai_config.focal_length * img_w)
|
||||
constants_inf.log(<str>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(<str> 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(<str> 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(<str> 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(<str> f'Kept ({m_w} {m_h}) <= {max_size}. class: {constants_inf.annotations_dict[det.cls].name}')
|
||||
else:
|
||||
constants_inf.log(<str> 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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
call ..\Azaion.Inference\build_inference
|
||||
call ..\Azaion.Loader\build_loader
|
||||
call copy_loader_inf
|
||||
call ..\Azaion.Inference\build_inference.cmd
|
||||
call ..\Azaion.Loader\build_loader.cmd
|
||||
call copy_loader_inf.cmd
|
||||
@@ -30,5 +30,10 @@
|
||||
"GenerateAnnotatedImage": true,
|
||||
"SilentDetection": false,
|
||||
"ShowDatasetWithDetectionsOnly": false
|
||||
},
|
||||
"CameraConfig": {
|
||||
"Altitude": 400,
|
||||
"CameraSensorWidth": 23.5,
|
||||
"CameraFocalLength": 24
|
||||
}
|
||||
}
|
||||
@@ -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" ],
|
||||
|
||||
Reference in New Issue
Block a user