add autodetection

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-02 13:09:00 +02:00
parent b5b77d9492
commit 418a2116b7
19 changed files with 545 additions and 268 deletions
-54
View File
@@ -1,54 +0,0 @@
using System.Diagnostics;
using System.IO;
using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Compunet.YoloV8;
using LibVLCSharp.Shared;
using MediatR;
namespace Azaion.Annotator;
public class AIDetector(Config config, MediaPlayer mediaPlayer, VLCFrameExtractor frameExtractor)
: IRequestHandler<AIDetectEvent, List<YoloLabel>>
{
public async Task<List<YoloLabel>> Handle(AIDetectEvent request, CancellationToken cancellationToken)
{
using var predictor = new YoloPredictor(config.AIModelPath);
await frameExtractor.Start(async stream =>
{
stream.Seek(0, SeekOrigin.Begin);
var sw = Stopwatch.StartNew();
var result = await predictor.DetectAsync(stream);
sw.Stop();
var log = string.Join("|", result.Select(det =>
$"{det.Name.Id}.{det.Name.Name}: xy=({det.Bounds.X},{det.Bounds.Y}), size=({det.Bounds.Width}, {det.Bounds.Height}), Prob: {det.Confidence*100:F1}%"));
log += $". Inf time: {sw.ElapsedMilliseconds} ms";
Console.WriteLine(log);
});
while (mediaPlayer.IsPlaying)
{
try
{
// using var thumbnail = await mediaPlayer.Media.GenerateThumbnail(time: 200,
// speed: ThumbnailerSeekSpeed.Fast,
// width: 1280,
// height: resultHeight,
// crop: false,
// pictureType: PictureType.Argb)
//
// mediaPlayer.TakeSnapshot(0, TEMP_IMG, 1280, resultHeight);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
//var result = predictor.Detect();
}
return new List<YoloLabel>();
}
}
+1
View File
@@ -38,6 +38,7 @@ public partial class App : Application
services.AddSingleton<HelpWindow>(); services.AddSingleton<HelpWindow>();
services.AddSingleton<DatasetExplorer>(); services.AddSingleton<DatasetExplorer>();
services.AddSingleton<IGalleryManager, GalleryManager>(); services.AddSingleton<IGalleryManager, GalleryManager>();
services.AddSingleton<IAIDetector, YOLODetector>();
services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
services.AddSingleton<LibVLC>(_ => new LibVLC()); services.AddSingleton<LibVLC>(_ => new LibVLC());
services.AddSingleton<FormState>(); services.AddSingleton<FormState>();
+37
View File
@@ -0,0 +1,37 @@
<Window x:Class="Azaion.Annotator.AutodetectDialog"
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"
mc:Ignorable="d"
WindowStyle="SingleBorderWindow"
ResizeMode="NoResize"
Title="AutodetectDialog"
Height="250" Width="420"
Background="LightGray">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="70"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
FontSize="18" TextAlignment="Center" Padding="0 10 0 0">Йде розпізнавання...</TextBlock>
<ScrollViewer Grid.Row="1">
<TextBlock
Name="TextBlockLog"
Padding="10 10 5 5"/>
</ScrollViewer>
<Button Grid.Row="2"
Width="50" Height="50" ToolTip="Видалити всі аннотації. Клавіша: [X]"
Background="LightGray" BorderBrush="LightGray"
Click="ButtonBase_OnClick">
<Path Stretch="Fill" Fill="Gray" Data="M12,2 C17.5228,2 22,6.47715 22,12 C22,17.5228 17.5228,22 12,22 C6.47715,22 2,17.5228 2,12 C2,6.47715 6.47715,2 12,2 Z
M9.87874,8.46443 C9.48821,8.07391 8.85505,8.07391 8.46452,8.46443 C8.10404,8.82491923 8.07631077,9.39214645 8.38133231,9.78443366 L8.46452,9.87864 L10.5858,11.9999
L8.46443,14.1213 C8.07391,14.5118 8.07391,15.145 8.46443,15.5355 C8.82491923,15.8959615 9.39214645,15.9236893 9.78443366,15.6186834 L9.87864,15.5355 L12,13.4141
L14.1214,15.5355 C14.5119,15.926 15.1451,15.926 15.5356,15.5355 C15.8960615,15.1750385 15.9237893,14.6077793 15.6187834,14.2155027 L15.5356,14.1213 L13.4142,11.9999
L15.5355,9.87862 C15.926,9.4881 15.926,8.85493 15.5355,8.46441 C15.1750385,8.10392077 14.6077793,8.07619083 14.2155027,8.38122018 L14.1213,8.46441
L12,10.5857 L9.87874,8.46443Z" />
</Button>
</Grid>
</Window>
+19
View File
@@ -0,0 +1,19 @@
using System.Windows;
namespace Azaion.Annotator;
public partial class AutodetectDialog : Window
{
public AutodetectDialog()
{
InitializeComponent();
}
public void Log(string message) =>
TextBlockLog.Text = TextBlockLog.Text + Environment.NewLine + message;
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Close();
}
}
+1 -1
View File
@@ -39,7 +39,7 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource> </Resource>
<None Update="config.json"> <None Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
+11 -1
View File
@@ -4,6 +4,7 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Label = System.Windows.Controls.Label;
namespace Azaion.Annotator.Controls; namespace Azaion.Annotator.Controls;
@@ -14,7 +15,9 @@ public class AnnotationControl : Border
private readonly Grid _grid; private readonly Grid _grid;
private readonly TextBlock _classNameLabel; private readonly TextBlock _classNameLabel;
private readonly Label _probabilityLabel;
public TimeSpan? Time { get; set; } public TimeSpan? Time { get; set; }
public double? Probability { get; set; }
private AnnotationClass _annotationClass = null!; private AnnotationClass _annotationClass = null!;
public AnnotationClass AnnotationClass public AnnotationClass AnnotationClass
@@ -41,7 +44,7 @@ public class AnnotationControl : Border
} }
} }
public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart) public AnnotationControl(AnnotationClass annotationClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
{ {
Time = time; Time = time;
_resizeStart = resizeStart; _resizeStart = resizeStart;
@@ -54,6 +57,13 @@ public class AnnotationControl : Border
FontSize = 14, FontSize = 14,
Cursor = Cursors.SizeAll Cursor = Cursors.SizeAll
}; };
_probabilityLabel = new Label
{
Content = probability?.ToString("F1") ?? string.Empty,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -15, 0, 0),
};
_selectionFrame = new Rectangle _selectionFrame = new Rectangle
{ {
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
+1 -1
View File
@@ -312,7 +312,7 @@ public class CanvasEditor : Canvas
public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel) public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
{ {
var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart) var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
{ {
Width = canvasLabel.Width, Width = canvasLabel.Width,
Height = canvasLabel.Height Height = canvasLabel.Height
+21 -1
View File
@@ -37,7 +37,15 @@ public class Config
public ThumbnailConfig ThumbnailConfig { get; set; } = null!; public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
public int? LastSelectedExplorerClass { get; set; } public int? LastSelectedExplorerClass { get; set; }
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
}
public class AIRecognitionConfig
{
public string AIModelPath { get; set; } = null!; public string AIModelPath { get; set; } = null!;
public double FrameRecognitionSeconds { get; set; }
public double TrackingDistanceConfidence { get; set; }
public double TrackingProbabilityIncrease { get; set; }
} }
public class WindowConfig public class WindowConfig
@@ -71,6 +79,11 @@ public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfi
private const string DEFAULT_THUMBNAILS_DIR = "thumbnails"; private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
private const string DEFAULT_UNKNOWN_IMG_DIR = "unknown"; private const string DEFAULT_UNKNOWN_IMG_DIR = "unknown";
private const int DEFAULT_THUMBNAIL_BORDER = 10;
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
private const double TRACKING_PROBABILITY_INCREASE = 15;
private static readonly Size DefaultWindowSize = new(1280, 720); private static readonly Size DefaultWindowSize = new(1280, 720);
private static readonly Point DefaultWindowLocation = new(100, 100); private static readonly Point DefaultWindowLocation = new(100, 100);
private static readonly Size DefaultThumbnailSize = new(240, 135); private static readonly Size DefaultThumbnailSize = new(240, 135);
@@ -111,7 +124,14 @@ public class FileConfigRepository(ILogger<FileConfigRepository> logger) : IConfi
ThumbnailConfig = new ThumbnailConfig ThumbnailConfig = new ThumbnailConfig
{ {
Size = DefaultThumbnailSize, Size = DefaultThumbnailSize,
Border = 10 Border = DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
AIModelPath = "azaion.onnx",
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE
} }
}; };
} }
+9 -6
View File
@@ -9,11 +9,11 @@ public abstract class Label
{ {
[JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; } [JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; }
public Label() protected Label()
{ {
} }
public Label(int classNumber) protected Label(int classNumber)
{ {
ClassNumber = classNumber; ClassNumber = classNumber;
} }
@@ -25,20 +25,22 @@ public class CanvasLabel : Label
public double Y { get; set; } public double Y { get; set; }
public double Width { get; set; } public double Width { get; set; }
public double Height { get; set; } public double Height { get; set; }
public double? Probability { get; }
public CanvasLabel() public CanvasLabel()
{ {
} }
public CanvasLabel(int classNumber, double x, double y, double width, double height) : base(classNumber) public CanvasLabel(int classNumber, double x, double y, double width, double height, double? probability = null) : base(classNumber)
{ {
X = x; X = x;
Y = y; Y = y;
Width = width; Width = width;
Height = height; Height = height;
Probability = probability;
} }
public CanvasLabel(YoloLabel label, Size canvasSize, Size videoSize) public CanvasLabel(YoloLabel label, Size canvasSize, Size videoSize, double? probability = null)
{ {
var cw = canvasSize.Width; var cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
@@ -70,6 +72,7 @@ public class CanvasLabel : Label
Width = label.Width * realWidth; Width = label.Width * realWidth;
Height = label.Height * ch; Height = label.Height * ch;
} }
Probability = probability;
} }
} }
@@ -158,10 +161,10 @@ public class YoloLabel : Label
.ToList()!; .ToList()!;
} }
public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename) public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename, CancellationToken cancellationToken = default)
{ {
var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString())); var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
await File.WriteAllTextAsync(filename, labelsStr); await File.WriteAllTextAsync(filename, labelsStr, cancellationToken);
} }
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
-1
View File
@@ -19,4 +19,3 @@ public class VolumeChangedEvent(int volume) : INotification
public int Volume { get; set; } = volume; public int Volume { get; set; } = volume;
} }
public class AIDetectEvent : IRequest<List<YoloLabel>>;
+10 -2
View File
@@ -192,8 +192,8 @@ public partial class DatasetExplorer
foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath)) foreach (var ann in await YoloLabel.ReadFromFile(dto.LabelPath))
{ {
var annClass = _config.AnnotationClassesDict[ann.ClassNumber]; var annClass = _config.AnnotationClassesDict[ann.ClassNumber];
var annInfo = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize); var canvasLabel = new CanvasLabel(ann, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, annInfo); ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
} }
ThumbnailLoading = false; ThumbnailLoading = false;
@@ -343,4 +343,12 @@ public partial class DatasetExplorer
return null; return null;
} }
} }
public void AddThumbnail(ThumbnailDto thumbnailDto, IEnumerable<int> classes)
{
var selectedClass = ((AnnotationClass?)LvClasses.SelectedItem)?.Id;
if (selectedClass != null && (selectedClass == -1 || classes.Any(x => x == selectedClass)))
ThumbnailsDtos.Insert(0, thumbnailDto);
}
} }
@@ -0,0 +1,9 @@
using System.Windows;
namespace Azaion.Annotator.Extensions;
public static class PointExtensions
{
public static double SqrDistance(this Point p1, Point p2) =>
(p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y);
}
@@ -1,31 +1,27 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Azaion.Annotator.DTO;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using SixLabors.ImageSharp.Drawing;
using SkiaSharp; using SkiaSharp;
namespace Azaion.Annotator.Extensions; namespace Azaion.Annotator.Extensions;
public class VLCFrameExtractor(LibVLC libVLC, MainWindow mainWindow) public class VLCFrameExtractor(LibVLC libVLC)
{ {
private const uint RGBA_BYTES = 4; private const uint RGBA_BYTES = 4;
private const int PLAYBACK_RATE = 3; private const int PLAYBACK_RATE = 4;
private const uint DEFAULT_WIDTH = 1280;
private uint _pitch; // Number of bytes per "line", aligned to x32. private uint _pitch; // Number of bytes per "line", aligned to x32.
private uint _lines; // Number of lines in the buffer, aligned to x32. private uint _lines; // Number of lines in the buffer, aligned to x32.
private uint _width; // Thumbnail width private uint _width; // Thumbnail width
private uint _height; // Thumbnail height private uint _height; // Thumbnail height
private uint _videoFPS;
private Func<Stream,Task> _frameProcessFn = null!;
private MediaPlayer _mediaPlayer = null!; private MediaPlayer _mediaPlayer = null!;
private TimeSpan _lastFrameTimestamp;
private long _lastFrame;
private static uint Align32(uint size) private static uint Align32(uint size)
{ {
if (size % 32 == 0) if (size % 32 == 0)
@@ -34,76 +30,66 @@ public class VLCFrameExtractor(LibVLC libVLC, MainWindow mainWindow)
} }
private static SKBitmap? _currentBitmap; private static SKBitmap? _currentBitmap;
private static readonly ConcurrentQueue<SKBitmap?> FilesToProcess = new(); private static readonly ConcurrentQueue<FrameInfo> FramesQueue = new();
private static long _frameCounter; private static long _frameCounter;
public async Task Start(Func<Stream, Task> frameProcessFn) public async IAsyncEnumerable<(TimeSpan Time, Stream Stream)> ExtractFrames(string mediaPath,
[EnumeratorCancellation] CancellationToken manualCancellationToken = default)
{ {
_frameProcessFn = frameProcessFn; var videoFinishedCancellationToken = new CancellationTokenSource();
var processingCancellationTokenSource = new CancellationTokenSource();
_mediaPlayer = new MediaPlayer(libVLC); _mediaPlayer = new MediaPlayer(libVLC);
_mediaPlayer.Stopped += (s, e) => processingCancellationTokenSource.CancelAfter(1); _mediaPlayer.Stopped += (s, e) => videoFinishedCancellationToken.CancelAfter(1);
using var media = new Media(libVLC, ((MediaFileInfo)mainWindow.LvFiles.SelectedItem).Path); using var media = new Media(libVLC, mediaPath);
await media.Parse(cancellationToken: processingCancellationTokenSource.Token); await media.Parse(cancellationToken: videoFinishedCancellationToken.Token);
var videoTrack = media.Tracks.FirstOrDefault(x => x.Data.Video.Width != 0); var videoTrack = media.Tracks.FirstOrDefault(x => x.Data.Video.Width != 0);
_width = videoTrack.Data.Video.Width; _width = videoTrack.Data.Video.Width;
_height = videoTrack.Data.Video.Height; _height = videoTrack.Data.Video.Height;
_videoFPS = videoTrack.Data.Video.FrameRateNum;
//rescaling to DEFAULT_WIDTH //rescaling to DEFAULT_WIDTH
_height = (uint)(DEFAULT_WIDTH * _height / (double)_width); //TODO: probably rescaling is not necessary, should be checked
_width = DEFAULT_WIDTH; //_width = DEFAULT_WIDTH;
//_height = (uint)(DEFAULT_WIDTH * _height / (double)_width);
_pitch = Align32(_width * RGBA_BYTES); _pitch = Align32(_width * RGBA_BYTES);
_lines = Align32(_height); _lines = Align32(_height);
_mediaPlayer.SetRate(PLAYBACK_RATE);
media.AddOption(":no-audio");
_mediaPlayer.SetVideoFormat("RV32", _width, _height, _pitch);
_mediaPlayer.SetVideoCallbacks(Lock, null, Display);
_mediaPlayer.Play(media); _mediaPlayer.Play(media);
_mediaPlayer.SetRate(3);
try
{
media.AddOption(":no-audio");
_mediaPlayer.SetVideoFormat("RV32", _width, _height, _pitch);
_mediaPlayer.SetVideoCallbacks(Lock, null, Display);
await ProcessThumbnailsAsync(processingCancellationTokenSource.Token);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
_mediaPlayer.Stop();
_mediaPlayer.Dispose();
}
}
private async Task ProcessThumbnailsAsync(CancellationToken token)
{
_frameCounter = 0; _frameCounter = 0;
var surface = SKSurface.Create(new SKImageInfo((int) _width, (int) _height)); var surface = SKSurface.Create(new SKImageInfo((int) _width, (int) _height));
while (!token.IsCancellationRequested) var token = videoFinishedCancellationToken.Token;
while (!(FramesQueue.IsEmpty && token.IsCancellationRequested) && !manualCancellationToken.IsCancellationRequested)
{ {
if (FilesToProcess.TryDequeue(out var bitmap)) if (FramesQueue.TryDequeue(out var frameInfo))
{ {
if (bitmap == null) if (frameInfo.Bitmap == null)
continue; continue;
surface.Canvas.DrawBitmap(bitmap, 0, 0); // Effectively crops the original bitmap to get only the visible area surface.Canvas.DrawBitmap(frameInfo.Bitmap, 0, 0); // Effectively crops the original bitmap to get only the visible area
using var outputImage = surface.Snapshot(); using var outputImage = surface.Snapshot();
using var data = outputImage.Encode(SKEncodedImageFormat.Jpeg, 85); using var data = outputImage.Encode(SKEncodedImageFormat.Jpeg, 85);
using var ms = new MemoryStream(); using var ms = new MemoryStream();
data.SaveTo(ms); data.SaveTo(ms);
if (_frameProcessFn != null)
await _frameProcessFn(ms);
Console.WriteLine($"Time: {TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} Queue size: {FilesToProcess.Count}"); yield return (frameInfo.Time, ms);
bitmap.Dispose();
Console.WriteLine($"Queue size: {FramesQueue.Count}");
frameInfo.Bitmap?.Dispose();
} }
else else
{ {
await Task.Delay(TimeSpan.FromSeconds(1), token); await Task.Delay(TimeSpan.FromSeconds(1), token);
} }
} }
_mediaPlayer.Stop();
_mediaPlayer.Dispose(); _mediaPlayer.Dispose();
} }
@@ -116,12 +102,31 @@ public class VLCFrameExtractor(LibVLC libVLC, MainWindow mainWindow)
private void Display(IntPtr opaque, IntPtr picture) private void Display(IntPtr opaque, IntPtr picture)
{ {
if (_frameCounter % (int)(_videoFPS / 3.0) == 0) var playerTime = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
FilesToProcess.Enqueue(_currentBitmap); if (_lastFrameTimestamp != playerTime)
{
_lastFrame = _frameCounter;
_lastFrameTimestamp = playerTime;
}
if (_frameCounter > 20 && _frameCounter % 10 == 0)
{
var msToAdd = (_frameCounter - _lastFrame) * (_lastFrameTimestamp.TotalMilliseconds / _lastFrame);
var time = _lastFrameTimestamp.Add(TimeSpan.FromMilliseconds(msToAdd));
FramesQueue.Enqueue(new FrameInfo(time, _currentBitmap));
}
else else
{
_currentBitmap?.Dispose(); _currentBitmap?.Dispose();
}
_currentBitmap = null; _currentBitmap = null;
_frameCounter++; _frameCounter++;
} }
} }
public class FrameInfo(TimeSpan time, SKBitmap? bitmap)
{
public TimeSpan Time { get; set; } = time;
public SKBitmap? Bitmap { get; set; } = bitmap;
}
+95 -87
View File
@@ -124,96 +124,104 @@ public class GalleryManager : IGalleryManager
public async Task<ThumbnailDto?> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default) public async Task<ThumbnailDto?> CreateThumbnail(string imgPath, CancellationToken cancellationToken = default)
{ {
var width = (int)_config.ThumbnailConfig.Size.Width; try
var height = (int)_config.ThumbnailConfig.Size.Height;
var imgName = Path.GetFileName(imgPath);
var labelName = Path.Combine(_config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt");
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken)));
var bitmap = new Bitmap(width, height);
using var g = Graphics.FromImage(bitmap);
g.CompositingQuality = CompositingQuality.HighSpeed;
g.SmoothingMode = SmoothingMode.HighSpeed;
g.InterpolationMode = InterpolationMode.Default;
var size = new Size(originalImage.Width, originalImage.Height);
if (!File.Exists(labelName))
{ {
File.Move(imgPath, Path.Combine(_config.UnknownImages, imgName)); var width = (int)_config.ThumbnailConfig.Size.Width;
_logger.LogInformation($"No labels found for image {imgName}! Moved image to the {_config.UnknownImages} folder."); var height = (int)_config.ThumbnailConfig.Size.Height;
var imgName = Path.GetFileName(imgPath);
var labelName = Path.Combine(_config.LabelsDirectory, $"{Path.GetFileNameWithoutExtension(imgPath)}.txt");
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(imgPath, cancellationToken)));
var bitmap = new Bitmap(width, height);
using var g = Graphics.FromImage(bitmap);
g.CompositingQuality = CompositingQuality.HighSpeed;
g.SmoothingMode = SmoothingMode.HighSpeed;
g.InterpolationMode = InterpolationMode.Default;
var size = new Size(originalImage.Width, originalImage.Height);
if (!File.Exists(labelName))
{
File.Move(imgPath, Path.Combine(_config.UnknownImages, imgName));
_logger.LogInformation($"No labels found for image {imgName}! Moved image to the {_config.UnknownImages} folder.");
return null;
}
var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken))
.Select(x => new CanvasLabel(x, size, size))
.ToList();
var thumbWhRatio = width / (float)height;
var border = _config.ThumbnailConfig.Border;
var classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
LabelsCache.TryAdd(imgName, new LabelInfo
{
Classes = classes,
ImageDateTime = File.GetCreationTimeUtc(imgPath)
});
var frameX = 0.0;
var frameY = 0.0;
var frameHeight = size.Height;
var frameWidth = size.Width;
if (labels.Any())
{
var labelsMinX = labels.Min(x => x.X);
var labelsMaxX = labels.Max(x => x.X + x.Width);
var labelsMinY = labels.Min(x => x.Y);
var labelsMaxY = labels.Max(x => x.Y + x.Height);
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
if (labelsWidth / labelsHeight > thumbWhRatio)
{
frameWidth = labelsWidth;
frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height);
frameX = Math.Max(0, labelsMinX - border);
frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border);
}
else
{
frameHeight = labelsHeight;
frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width);
frameY = Math.Max(0, labelsMinY - border);
frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border);
}
}
var scale = frameHeight / height;
g.DrawImage(originalImage, new Rectangle(0, 0, width, height), new RectangleF((float)frameX, (float)frameY, (float)frameWidth, (float)frameHeight), GraphicsUnit.Pixel);
foreach (var label in labels)
{
var color = _config.AnnotationClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
g.FillRectangle(brush, rectangle);
}
var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Config.THUMBNAIL_PREFIX}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
return new ThumbnailDto
{
ThumbnailPath = thumbnailName,
ImagePath = imgPath,
LabelPath = labelName,
ImageDate = File.GetCreationTimeUtc(imgPath)
};
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
return null; return null;
} }
var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken))
.Select(x => new CanvasLabel(x, size, size))
.ToList();
var thumbWhRatio = width / (float)height;
var border = _config.ThumbnailConfig.Border;
var classes = labels.Select(x => x.ClassNumber).Distinct().ToList();
LabelsCache.TryAdd(imgName, new LabelInfo
{
Classes = classes,
ImageDateTime = File.GetCreationTimeUtc(imgPath)
});
var frameX = 0.0;
var frameY = 0.0;
var frameHeight = size.Height;
var frameWidth = size.Width;
if (labels.Any())
{
var labelsMinX = labels.Min(x => x.X);
var labelsMaxX = labels.Max(x => x.X + x.Width);
var labelsMinY = labels.Min(x => x.Y);
var labelsMaxY = labels.Max(x => x.Y + x.Height);
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
if (labelsWidth / labelsHeight > thumbWhRatio)
{
frameWidth = labelsWidth;
frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height);
frameX = Math.Max(0, labelsMinX - border);
frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border);
}
else
{
frameHeight = labelsHeight;
frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width);
frameY = Math.Max(0, labelsMinY - border);
frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border);
}
}
var scale = frameHeight / height;
g.DrawImage(originalImage, new Rectangle(0, 0, width, height), new RectangleF((float)frameX, (float)frameY, (float)frameWidth, (float)frameHeight), GraphicsUnit.Pixel);
foreach (var label in labels)
{
var color = _config.AnnotationClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
g.FillRectangle(brush, rectangle);
}
var thumbnailName = Path.Combine(ThumbnailsDirectory.FullName, $"{Path.GetFileNameWithoutExtension(imgPath)}{Config.THUMBNAIL_PREFIX}.jpg");
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
return new ThumbnailDto
{
ThumbnailPath = thumbnailName,
ImagePath = imgPath,
LabelPath = labelName,
ImageDate = File.GetCreationTimeUtc(imgPath)
};
} }
} }
+1 -6
View File
@@ -229,7 +229,7 @@
RowHeaderWidth="0" RowHeaderWidth="0"
Padding="2 0 0 0" Padding="2 0 0 0"
AutoGenerateColumns="False" AutoGenerateColumns="False"
SelectionMode="Single" SelectionMode="Extended"
CellStyle="{DynamicResource DataGridCellStyle1}" CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
@@ -267,11 +267,6 @@
</DataGridTextColumn.CellStyle> </DataGridTextColumn.CellStyle>
</DataGridTextColumn> </DataGridTextColumn>
</DataGrid.Columns> </DataGrid.Columns>
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<EventSetter Event="MouseDoubleClick" Handler="DgAnnotationsRowClick"></EventSetter>
</Style>
</DataGrid.ItemContainerStyle>
</DataGrid> </DataGrid>
</Grid> </Grid>
+216 -36
View File
@@ -1,20 +1,25 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
using Newtonsoft.Json; using Newtonsoft.Json;
using Point = System.Windows.Point;
using Size = System.Windows.Size; using Size = System.Windows.Size;
using IntervalTree; using IntervalTree;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OpenTK.Graphics.OpenGL;
using Serilog;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator; namespace Azaion.Annotator;
@@ -29,7 +34,9 @@ public partial class MainWindow
private readonly HelpWindow _helpWindow; private readonly HelpWindow _helpWindow;
private readonly ILogger<MainWindow> _logger; private readonly ILogger<MainWindow> _logger;
private readonly IGalleryManager _galleryManager; private readonly IGalleryManager _galleryManager;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly VLCFrameExtractor _vlcFrameExtractor;
private readonly IAIDetector _aiDetector;
private CancellationTokenSource _cancellationTokenSource = new();
private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
@@ -43,6 +50,7 @@ public partial class MainWindow
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new(); private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public IntervalTree<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new(); public IntervalTree<TimeSpan, List<YoloLabel>> Annotations { get; set; } = new();
private AutodetectDialog _autoDetectDialog;
public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer, public MainWindow(LibVLC libVLC, MediaPlayer mediaPlayer,
IMediator mediator, IMediator mediator,
@@ -51,7 +59,9 @@ public partial class MainWindow
HelpWindow helpWindow, HelpWindow helpWindow,
DatasetExplorer datasetExplorer, DatasetExplorer datasetExplorer,
ILogger<MainWindow> logger, ILogger<MainWindow> logger,
IGalleryManager galleryManager) IGalleryManager galleryManager,
VLCFrameExtractor vlcFrameExtractor,
IAIDetector aiDetector)
{ {
InitializeComponent(); InitializeComponent();
_libVLC = libVLC; _libVLC = libVLC;
@@ -64,6 +74,8 @@ public partial class MainWindow
_datasetExplorer = datasetExplorer; _datasetExplorer = datasetExplorer;
_logger = logger; _logger = logger;
_galleryManager = galleryManager; _galleryManager = galleryManager;
_vlcFrameExtractor = vlcFrameExtractor;
_aiDetector = aiDetector;
VideoView.Loaded += VideoView_Loaded; VideoView.Loaded += VideoView_Loaded;
Closed += OnFormClosed; Closed += OnFormClosed;
@@ -189,6 +201,39 @@ public partial class MainWindow
LocationChanged += async (_, _) => await SaveUserSettings(); LocationChanged += async (_, _) => await SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings(); StateChanged += async (_, _) => await SaveUserSettings();
DgAnnotations.MouseDoubleClick += (sender, args) =>
{
Editor.RemoveAllAnns();
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
var res = (AnnotationResult)dgRow!.Item;
_mediaPlayer.SetPause(true);
Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)res.Time.TotalMilliseconds;
ShowTimeAnnotations(res.Time);
};
DgAnnotations.KeyUp += (sender, args) =>
{
if (args.Key != Key.Delete)
return;
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result != MessageBoxResult.OK)
return;
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
foreach (var annotationResult in res)
{
var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image);
var thumbnailPath = Path.Combine(_config.ThumbnailsDirectory, $"{imgName}{Config.THUMBNAIL_PREFIX}.jpg");
File.Delete(annotationResult.Image);
File.Delete(Path.Combine(_config.LabelsDirectory, $"{imgName}.txt"));
File.Delete(thumbnailPath);
_formState.AnnotationResults.Remove(annotationResult);
Annotations.Remove(Annotations.Query(annotationResult.Time));
}
};
Editor.FormState = _formState; Editor.FormState = _formState;
Editor.Mediator = _mediator; Editor.Mediator = _mediator;
DgAnnotations.ItemsSource = _formState.AnnotationResults; DgAnnotations.ItemsSource = _formState.AnnotationResults;
@@ -221,14 +266,16 @@ public partial class MainWindow
var annotations = Annotations.Query(time).SelectMany(x => x).ToList(); var annotations = Annotations.Query(time).SelectMany(x => x).ToList();
foreach (var ann in annotations) foreach (var ann in annotations)
{ AddAnnotationToCanvas(time, new CanvasLabel(ann, Editor.RenderSize, _formState.CurrentVideoSize));
var annClass = _config.AnnotationClasses[ann.ClassNumber];
var annInfo = new CanvasLabel(ann, Editor.RenderSize, _formState.CurrentVideoSize);
Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, time, annInfo));
}
} }
public async Task ReloadAnnotations(CancellationToken cancellationToken) private void AddAnnotationToCanvas(TimeSpan? time, CanvasLabel canvasLabel)
{
var annClass = _config.AnnotationClasses[canvasLabel.ClassNumber];
Dispatcher.Invoke(() => Editor.CreateAnnotation(annClass, time, canvasLabel));
}
private async Task ReloadAnnotations(CancellationToken cancellationToken)
{ {
_formState.AnnotationResults.Clear(); _formState.AnnotationResults.Clear();
Annotations.Clear(); Annotations.Clear();
@@ -243,14 +290,12 @@ public partial class MainWindow
{ {
var name = Path.GetFileNameWithoutExtension(file.Name); var name = Path.GetFileNameWithoutExtension(file.Name);
var time = _formState.GetTime(name); var time = _formState.GetTime(name);
await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName, cancellationToken)); await AddAnnotations(time, await YoloLabel.ReadFromFile(file.FullName, cancellationToken));
} }
} }
public async Task AddAnnotation(TimeSpan? time, List<YoloLabel> annotations) public async Task AddAnnotations(TimeSpan? time, List<YoloLabel> annotations)
{ {
var fName = _formState.GetTimeName(time);
var timeValue = time ?? TimeSpan.FromMinutes(0); var timeValue = time ?? TimeSpan.FromMinutes(0);
var previousAnnotations = Annotations.Query(timeValue); var previousAnnotations = Annotations.Query(timeValue);
Annotations.Remove(previousAnnotations); Annotations.Remove(previousAnnotations);
@@ -269,8 +314,8 @@ public partial class MainWindow
.Select(x => x.Value + 1) .Select(x => x.Value + 1)
.FirstOrDefault(); .FirstOrDefault();
_formState.AnnotationResults.Insert(index, new AnnotationResult(timeValue, fName, annotations, _config)); _formState.AnnotationResults.Insert(index, new AnnotationResult(timeValue, _formState.GetTimeName(time), annotations, _config));
await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{fName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults)); await File.WriteAllTextAsync($"{_config.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults));
} }
private void ReloadFiles() private void ReloadFiles()
@@ -339,9 +384,19 @@ public partial class MainWindow
if (mediaFileInfo == null) if (mediaFileInfo == null)
return; return;
Process.Start("explorer.exe", "/select, \"" + mediaFileInfo.Path +"\""); Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\"");
} }
public void SeekTo(long timeMilliseconds)
{
_mediaPlayer.SetPause(true);
_mediaPlayer.Time = timeMilliseconds;
VideoSlider.Value = _mediaPlayer.Position * 100;
}
private void SeekTo(TimeSpan time) =>
SeekTo((long)time.TotalMilliseconds);
// private void AddClassBtnClick(object sender, RoutedEventArgs e) // private void AddClassBtnClick(object sender, RoutedEventArgs e)
// { // {
// LvClasses.IsReadOnly = false; // LvClasses.IsReadOnly = false;
@@ -402,32 +457,12 @@ public partial class MainWindow
private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOffVolume)); private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOffVolume));
private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOnVolume)); private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOnVolume));
private async void AutoDetect(object sender, RoutedEventArgs e)
{
if (LvFiles.SelectedItem == null)
return;
await _mediator.Send(new AIDetectEvent());
}
private void OpenHelpWindowClick(object sender, RoutedEventArgs e) private void OpenHelpWindowClick(object sender, RoutedEventArgs e)
{ {
_helpWindow.Show(); _helpWindow.Show();
_helpWindow.Activate(); _helpWindow.Activate();
} }
private void DgAnnotationsRowClick(object sender, MouseButtonEventArgs e)
{
DgAnnotations.MouseDoubleClick += (sender, args) =>
{
Editor.RemoveAllAnns();
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
var res = (AnnotationResult)dgRow!.Item;
_mediaPlayer.SetPause(true);
_mediaPlayer.Time = (long)res.Time.TotalMilliseconds; // + 250;
ShowTimeAnnotations(res.Time);
};
}
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings(); private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings();
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e) private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e)
@@ -445,4 +480,149 @@ public partial class MainWindow
var listItem = sender as ListViewItem; var listItem = sender as ListViewItem;
LvFilesContextMenu.DataContext = listItem.DataContext; LvFilesContextMenu.DataContext = listItem.DataContext;
} }
private (TimeSpan Time, List<(YoloLabel Label, float Probability)> Detections)? _previousDetection;
public void AutoDetect(object sender, RoutedEventArgs e)
{
if (LvFiles.SelectedItem == null)
return;
_mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play));
var mediaInfo = (MediaFileInfo)LvFiles.SelectedItem;
_formState.CurrentMedia = mediaInfo;
_mediaPlayer.Stop();
var path = mediaInfo.Path;
var manualCancellationSource = new CancellationTokenSource();
var token = manualCancellationSource.Token;
_autoDetectDialog = new AutodetectDialog
{
Topmost = true,
Owner = this
};
_autoDetectDialog.Closing += (_, _) =>
{
manualCancellationSource.Cancel();
_mediaPlayer.Stop();
};
_autoDetectDialog.Top = Height - _autoDetectDialog.Height - 80;
_ = Task.Run(async () =>
{
using var detector = new YOLODetector(_config);
Dispatcher.Invoke(() => _autoDetectDialog.Log("Ініціалізація AI..."));
await foreach (var timeframe in _vlcFrameExtractor.ExtractFrames(path, token))
{
try
{
var detections = _aiDetector.Detect(timeframe.Stream);
if (!IsValidDetection(timeframe.Time, detections))
continue;
await ProcessDetection(timeframe, detections, token);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
await manualCancellationSource.CancelAsync();
}
}
_autoDetectDialog.Close();
}, token);
_autoDetectDialog.ShowDialog();
Dispatcher.Invoke(() => Editor.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)));
}
private bool IsValidDetection(TimeSpan time, List<(YoloLabel Label, float Probability)> detections)
{
// No AI detection, forbid
if (detections.Count == 0)
return false;
// Very first detection, allow
if (!_previousDetection.HasValue)
return true;
var prev = _previousDetection.Value;
// Time between detections is >= than Frame Recognition Seconds, allow
if (time >= prev.Time.Add(TimeSpan.FromSeconds(_config.AIRecognitionConfig.FrameRecognitionSeconds)))
return true;
// Detection is earlier than previous + FrameRecognitionSeconds.
// Look to the detections more in detail
// More detected objects, allow
if (detections.Count > prev.Detections.Count)
return true;
foreach (var det in detections)
{
var point = new Point(det.Label.CenterX, det.Label.CenterY);
var closestObject = prev.Detections
.Select(p => new
{
Point = p,
Distance = point.SqrDistance(new Point(p.Label.CenterX, p.Label.CenterY))
})
.OrderBy(x => x.Distance)
.First();
// Closest object is farther than Tracking distance confidence, hence it's a different object, allow
if (closestObject.Distance > _config.AIRecognitionConfig.TrackingDistanceConfidence)
return true;
// Since closest object within distance confidence, then it is tracking of the same object. Then if recognition probability for the object > increase from previous
if (det.Probability >= closestObject.Point.Probability + _config.AIRecognitionConfig.TrackingProbabilityIncrease)
return true;
}
return false;
}
private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, List<(YoloLabel Label, float Probability)> detections, CancellationToken token = default)
{
_previousDetection = (timeframe.Time, detections);
await Dispatcher.Invoke(async () =>
{
try
{
var time = timeframe.Time;
var labels = detections.Select(x => x.Label).ToList();
var fName = _formState.GetTimeName(timeframe.Time);
var imgPath = Path.Combine(_config.ImagesDirectory, $"{fName}.jpg");
var img = System.Drawing.Image.FromStream(timeframe.Stream);
img.Save(imgPath, ImageFormat.Jpeg);
await YoloLabel.WriteToFile(labels, Path.Combine(_config.LabelsDirectory, $"{fName}.txt"), token);
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
Editor.RemoveAllAnns();
foreach (var (label, probability) in detections)
AddAnnotationToCanvas(time, new CanvasLabel(label, Editor.RenderSize, Editor.RenderSize, probability));
await AddAnnotations(timeframe.Time, labels);
var log = string.Join(Environment.NewLine, detections.Select(det =>
$"{_config.AnnotationClassesDict[det.Label.ClassNumber].Name}: " +
$"xy=({det.Label.CenterX:F2},{det.Label.CenterY:F2}), " +
$"size=({det.Label.Width:F2}, {det.Label.Height:F2}), " +
$"prob: {det.Probability:F1}%"));
Dispatcher.Invoke(() => _autoDetectDialog.Log(log));
var thumbnailDto = await _galleryManager.CreateThumbnail(imgPath, token);
if (thumbnailDto != null)
_datasetExplorer.AddThumbnail(thumbnailDto, labels.Select(x => x.ClassNumber));
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
});
}
} }
+17 -21
View File
@@ -1,5 +1,6 @@
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
@@ -95,7 +96,7 @@ public class MainWindowEventHandler :
await ControlPlayback(value); await ControlPlayback(value);
if (key == Key.A) if (key == Key.A)
await _mediator.Send( new AIDetectEvent(), cancellationToken); _mainWindow.AutoDetect(null!, null!);
await VolumeControl(key); await VolumeControl(key);
} }
@@ -141,7 +142,7 @@ public class MainWindowEventHandler :
switch (controlEnum) switch (controlEnum)
{ {
case PlaybackControlEnum.Play: case PlaybackControlEnum.Play:
await Play(); Play();
break; break;
case PlaybackControlEnum.Pause: case PlaybackControlEnum.Pause:
_mediaPlayer.Pause(); _mediaPlayer.Pause();
@@ -152,19 +153,16 @@ public class MainWindowEventHandler :
_mediaPlayer.Stop(); _mediaPlayer.Stop();
break; break;
case PlaybackControlEnum.PreviousFrame: case PlaybackControlEnum.PreviousFrame:
_mediaPlayer.SetPause(true); _mainWindow.SeekTo(_mediaPlayer.Time - step);
_mediaPlayer.Time -= step;
_mainWindow.VideoSlider.Value = _mediaPlayer.Position * 100;
break; break;
case PlaybackControlEnum.NextFrame: case PlaybackControlEnum.NextFrame:
_mediaPlayer.SetPause(true); _mainWindow.SeekTo(_mediaPlayer.Time + step);
_mediaPlayer.Time += step;
_mainWindow.VideoSlider.Value = _mediaPlayer.Position * 100;
break; break;
case PlaybackControlEnum.SaveAnnotations: case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations(); await SaveAnnotations();
break; break;
case PlaybackControlEnum.RemoveSelectedAnns: case PlaybackControlEnum.RemoveSelectedAnns:
_mainWindow.Editor.RemoveSelectedAnns(); _mainWindow.Editor.RemoveSelectedAnns();
break; break;
case PlaybackControlEnum.RemoveAllAnns: case PlaybackControlEnum.RemoveAllAnns:
@@ -182,10 +180,10 @@ public class MainWindowEventHandler :
_mediaPlayer.Volume = 0; _mediaPlayer.Volume = 0;
break; break;
case PlaybackControlEnum.Previous: case PlaybackControlEnum.Previous:
await NextMedia(isPrevious: true); NextMedia(isPrevious: true);
break; break;
case PlaybackControlEnum.Next: case PlaybackControlEnum.Next:
await NextMedia(); NextMedia();
break; break;
case PlaybackControlEnum.None: case PlaybackControlEnum.None:
break; break;
@@ -195,12 +193,12 @@ public class MainWindowEventHandler :
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); _logger.LogError(e, e.Message);
throw; throw;
} }
} }
private async Task NextMedia(bool isPrevious = false) private void NextMedia(bool isPrevious = false)
{ {
var increment = isPrevious ? -1 : 1; var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : _mainWindow.LvFiles.Items.Count; var check = isPrevious ? -1 : _mainWindow.LvFiles.Items.Count;
@@ -208,7 +206,7 @@ public class MainWindowEventHandler :
return; return;
_mainWindow.LvFiles.SelectedIndex += increment; _mainWindow.LvFiles.SelectedIndex += increment;
await Play(); Play();
} }
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken) public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
@@ -223,7 +221,7 @@ public class MainWindowEventHandler :
_mediaPlayer.Volume = volume; _mediaPlayer.Volume = volume;
} }
private async Task Play() private void Play()
{ {
if (_mainWindow.LvFiles.SelectedItem == null) if (_mainWindow.LvFiles.SelectedItem == null)
return; return;
@@ -242,7 +240,7 @@ public class MainWindowEventHandler :
return; return;
var time = TimeSpan.FromMilliseconds(_mediaPlayer.Time); var time = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
var fName = _formState.GetTimeName(time); var fName = _formState.GetTimeName(time);
var currentAnns = _mainWindow.Editor.CurrentAnns var currentAnns = _mainWindow.Editor.CurrentAnns
.Select(x => new YoloLabel(x.Info, _mainWindow.Editor.RenderSize, _formState.CurrentVideoSize)) .Select(x => new YoloLabel(x.Info, _mainWindow.Editor.RenderSize, _formState.CurrentVideoSize))
@@ -252,7 +250,7 @@ public class MainWindowEventHandler :
var resultHeight = (uint)Math.Round(RESULT_WIDTH / _formState.CurrentVideoSize.Width * _formState.CurrentVideoSize.Height); var resultHeight = (uint)Math.Round(RESULT_WIDTH / _formState.CurrentVideoSize.Width * _formState.CurrentVideoSize.Height);
await _mainWindow.AddAnnotation(time, currentAnns); await _mainWindow.AddAnnotations(time, currentAnns);
_formState.CurrentMedia.HasAnnotations = _mainWindow.Annotations.Count != 0; _formState.CurrentMedia.HasAnnotations = _mainWindow.Annotations.Count != 0;
_mainWindow.LvFiles.Items.Refresh(); _mainWindow.LvFiles.Items.Refresh();
@@ -269,13 +267,11 @@ public class MainWindowEventHandler :
else else
{ {
File.Copy(_formState.CurrentMedia.Path, destinationPath, overwrite: true); File.Copy(_formState.CurrentMedia.Path, destinationPath, overwrite: true);
await NextMedia(); NextMedia();
} }
var thumbnailDto = await _galleryManager.CreateThumbnail(destinationPath); var thumbnailDto = await _galleryManager.CreateThumbnail(destinationPath);
var selectedClass = ((AnnotationClass?)_datasetExplorer.LvClasses.SelectedItem)?.Id; if (thumbnailDto != null)
_datasetExplorer.AddThumbnail(thumbnailDto, currentAnns.Select(x => x.ClassNumber));
if (selectedClass != null && (selectedClass == -1 || currentAnns.Any(x => x.ClassNumber == selectedClass)))
_datasetExplorer.ThumbnailsDtos.Insert(0, thumbnailDto);
} }
} }
+36
View File
@@ -0,0 +1,36 @@
using System.Drawing.Imaging;
using System.IO;
using Azaion.Annotator.DTO;
using Compunet.YoloV8;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
namespace Azaion.Annotator;
public interface IAIDetector
{
List<(YoloLabel Label, float Probability)> Detect(Stream stream);
}
public class YOLODetector(Config config) : IAIDetector, IDisposable
{
private readonly YoloPredictor _predictor = new(config.AIRecognitionConfig.AIModelPath);
public List<(YoloLabel Label, float Probability)> Detect(Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
var image = Image.Load<Rgb24>(stream);
var result = _predictor.Detect(image);
var imageSize = new System.Windows.Size(image.Width, image.Height);
return result.Select(d =>
{
var label = new YoloLabel(new CanvasLabel(d.Name.Id, d.Bounds.X, d.Bounds.Y, d.Bounds.Width, d.Bounds.Height), imageSize, imageSize);
return (label, d.Confidence * 100);
}).ToList();
}
public void Dispose() => _predictor.Dispose();
}
+7 -2
View File
@@ -1,5 +1,5 @@
{ {
"VideosDirectory": "E:\\Azaion3\\Videos", "VideosDirectory": "E:\\Azaion3\\VideosTest",
"LabelsDirectory": "E:\\labels", "LabelsDirectory": "E:\\labels",
"ImagesDirectory": "E:\\images", "ImagesDirectory": "E:\\images",
"ThumbnailsDirectory": "E:\\thumbnails", "ThumbnailsDirectory": "E:\\thumbnails",
@@ -36,5 +36,10 @@
"ShowHelpOnStart": false, "ShowHelpOnStart": false,
"VideoFormats": ["mov", "mp4"], "VideoFormats": ["mov", "mp4"],
"ImageFormats": ["jpg", "jpeg", "png", "bmp", "gif"], "ImageFormats": ["jpg", "jpeg", "png", "bmp", "gif"],
"AIModelPath": "D:\\dev\\azaion\\azaion_2024-09-19.onnx" "AIRecognitionConfig": {
"AIModelPath": "azaion.onnx",
"FrameRecognitionSeconds": 2,
"TrackingDistanceConfidence": 0.15,
"TrackingProbabilityIncrease": 15
}
} }