mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 09:56:31 +00:00
add autodetection
This commit is contained in:
@@ -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>();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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(',', '.');
|
||||||
|
|||||||
@@ -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>>;
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user