add manual Tile Processor

zoom on video on pause (temp image)
This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-07-28 12:39:52 +03:00
parent fefd054ea0
commit fc6e5db795
34 changed files with 716 additions and 209 deletions
+128 -56
View File
@@ -2,6 +2,7 @@
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Azaion.Annotator.Controls;
using Azaion.Annotator.DTO;
using Azaion.Common;
@@ -47,7 +48,8 @@ public class AnnotatorEventHandler(
private const int STEP = 20;
private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280;
private readonly string tempImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, "___temp___.jpg");
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Space, PlaybackControlEnum.Pause },
@@ -139,12 +141,21 @@ public class AnnotatorEventHandler(
await Play(cancellationToken);
break;
case PlaybackControlEnum.Pause:
mediaPlayer.Pause();
if (formState.BackgroundTime.HasValue)
if (mediaPlayer.IsPlaying)
{
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
mediaPlayer.Pause();
mediaPlayer.TakeSnapshot(0, tempImgPath, 0, 0);
mainWindow.Editor.SetBackground(await tempImgPath.OpenImage());
formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time);
}
else
{
mediaPlayer.Play();
if (formState.BackgroundTime.HasValue)
{
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
}
}
break;
case PlaybackControlEnum.Stop:
@@ -159,7 +170,7 @@ public class AnnotatorEventHandler(
mainWindow.SeekTo(mediaPlayer.Time + step);
break;
case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations(cancellationToken);
await SaveAnnotation(cancellationToken);
break;
case PlaybackControlEnum.RemoveSelectedAnns:
@@ -226,63 +237,125 @@ public class AnnotatorEventHandler(
if (mainWindow.LvFiles.SelectedItem == null)
return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
mainWindow.Editor.SetBackground(null);
formState.CurrentMedia = mediaInfo;
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
//need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct);
mediaPlayer.Stop();
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}";
if (mediaInfo.MediaType == MediaTypes.Video)
{
mainWindow.Editor.SetBackground(null);
//need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct);
mediaPlayer.Stop();
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
}
else
{
formState.BackgroundTime = TimeSpan.Zero;
var image = await mediaInfo.Path.OpenImage();
formState.CurrentMediaSize = new Size(image.PixelWidth, image.PixelHeight);
mainWindow.Editor.SetBackground(image);
mediaPlayer.Stop();
}
}
//SAVE: MANUAL
private async Task SaveAnnotations(CancellationToken cancellationToken = default)
private async Task SaveAnnotation(CancellationToken cancellationToken = default)
{
if (formState.CurrentMedia == null)
return;
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var originalMediaName = formState.VideoName;
var fName = originalMediaName.ToTimeName(time);
var currentDetections = mainWindow.Editor.CurrentDetections
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
.ToList();
formState.CurrentMedia.HasAnnotations = currentDetections.Count != 0;
mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns();
var timeName = formState.MediaName.ToTimeName(time);
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{Constants.JPG_EXT}");
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{timeName}{Constants.JPG_EXT}");
if (formState.BackgroundTime.HasValue)
formState.CurrentMedia.HasAnnotations = mainWindow.Editor.CurrentDetections.Count != 0;
var annotations = await SaveAnnotationInner(imgPath, cancellationToken);
if (isVideo)
{
//no need to save image, it's already there, just remove background
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
//next item
var annGrid = mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
foreach (var annotation in annotations)
mainWindow.AddAnnotation(annotation);
mediaPlayer.Play();
// next item. Probably not needed
// var annGrid = mainWindow.DgAnnotations;
// annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
// mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
if (isVideo)
mediaPlayer.Play();
else
await NextMedia(ct: cancellationToken);
await NextMedia(ct: cancellationToken);
}
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns();
}
var annotation = await annotationService.SaveAnnotation(originalMediaName, time, currentDetections, token: cancellationToken);
if (isVideo)
mainWindow.AddAnnotation(annotation);
private async Task<List<Annotation>> SaveAnnotationInner(string imgPath, CancellationToken cancellationToken = default)
{
var canvasDetections = mainWindow.Editor.CurrentDetections.Select(x => x.ToCanvasLabel()).ToList();
var annotationsResult = new List<Annotation>();
if (!File.Exists(imgPath))
{
var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!;
if (source.PixelWidth <= RESULT_WIDTH * 2 && source.PixelHeight <= RESULT_WIDTH * 2) // Allow to be up to 2560*2560 to save to 1280*1280
{
//Save image
await using var stream = new FileStream(imgPath, FileMode.Create);
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));
encoder.Save(stream);
await stream.FlushAsync(cancellationToken);
}
else
{
//Tiling
//1. Restore original picture coordinates
var pictureCoordinatesDetections = canvasDetections.Select(x => new CanvasLabel(
new YoloLabel(x, mainWindow.Editor.RenderSize, formState.CurrentMediaSize), formState.CurrentMediaSize, null, x.Confidence))
.ToList();
//2. Split to 1280*1280 frames
var results = TileProcessor.Split(formState.CurrentMediaSize, pictureCoordinatesDetections, cancellationToken);
//3. Save each frame as a separate annotation
BitmapEncoder tileEncoder = new JpegBitmapEncoder();
foreach (var res in results)
{
var mediaName = $"{formState.MediaName}!split!{res.Tile.X}_{res.Tile.Y}!";
var time = TimeSpan.Zero;
var annotationName = mediaName.ToTimeName(time);
var tileImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{annotationName}{Constants.JPG_EXT}");
await using var tileStream = new FileStream(tileImgPath, FileMode.Create);
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.X, (int)res.Tile.Y, (int)res.Tile.Width, (int)res.Tile.Height));
tileEncoder.Frames.Add(BitmapFrame.Create(bitmap));
tileEncoder.Save(tileStream);
await tileStream.FlushAsync(cancellationToken);
var frameSize = new Size(res.Tile.Width, res.Tile.Height);
var detections = res.Detections
.Select(det => det.ReframeToSmall(res.Tile))
.Select(x => new Detection(annotationName, new YoloLabel(x, frameSize)))
.ToList();
annotationsResult.Add(await annotationService.SaveAnnotation(mediaName, time, detections, token: cancellationToken));
}
return annotationsResult;
}
}
var timeImg = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var timeName = formState.MediaName.ToTimeName(timeImg);
var currentDetections = canvasDetections.Select(x =>
new Detection(timeName, new YoloLabel(x, mainWindow.Editor.RenderSize)))
.ToList();
var annotation = await annotationService.SaveAnnotation(formState.MediaName, timeImg, currentDetections, token: cancellationToken);
return [annotation];
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
@@ -316,21 +389,20 @@ public class AnnotatorEventHandler(
});
await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct);
try
foreach (var name in notification.AnnotationNames)
{
foreach (var name in notification.AnnotationNames)
try
{
File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
}
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
catch (Exception e)
{
logger.LogError(e, e.Message);
}
}
//Only validators can send Delete to the queue
@@ -403,4 +475,4 @@ public class AnnotatorEventHandler(
map.SatelliteMap.Position = pointLatLon;
map.SatelliteMap.ZoomAndCenterMarkers(null);
}
}
}