diff --git a/PhotoToys/Custom UI/SimpleUI.cs b/PhotoToys/Custom UI/SimpleUI.cs index efbcafc..ad6734e 100644 --- a/PhotoToys/Custom UI/SimpleUI.cs +++ b/PhotoToys/Custom UI/SimpleUI.cs @@ -8,6 +8,11 @@ using Windows.ApplicationModel.DataTransfer; using System.IO; using Windows.Storage.Streams; +using Windows.Storage.Pickers; +using Windows.Storage; +using OpenCvSharp; +using Size = Windows.Foundation.Size; +using Rect = Windows.Foundation.Rect; namespace PhotoToys; @@ -18,6 +23,7 @@ public static void ImShow(this OpenCvSharp.Mat M, MatImage MatImage) MatImage.Mat = M; GC.Collect(); } + public static void ImShow(this Mat M, Action Action) => Action.Invoke(M); public static async Task ImShow(this OpenCvSharp.Mat M, string Title, XamlRoot XamlRoot) { await new ContentDialog @@ -133,7 +139,7 @@ public static UIElement Generate(string PageName, string? PageDescription = null VerticalScrollBarVisibility = ScrollBarVisibility.Auto }; } - public static UIElement GenerateLIVE(string PageName, string? PageDescription = null, Action? OnExecute = null, params ParameterFromUI[] Parameters) + public static UIElement GenerateLIVE(string PageName, string? PageDescription = null, Action>? OnExecute = null, params ParameterFromUI[] Parameters) { var verticalstack = new FluentVerticalStack { @@ -168,11 +174,22 @@ public static UIElement GenerateLIVE(string PageName, string? PageDescription = }, Children = { - new TextBlock + new FluentVerticalStack { - Text = "Result", - VerticalAlignment = VerticalAlignment.Center, - }, + Children = + { + new TextBlock + { + Text = "Result", + VerticalAlignment = VerticalAlignment.Center, + }, + new Button + { + Content = "Export Video", + Visibility = Visibility.Collapsed, + }.Assign(out var ExportVideoButton) + } + }.Assign(out var ExportVideoButtonContainer), new MatImage { UIElement = @@ -191,11 +208,92 @@ public static UIElement GenerateLIVE(string PageName, string? PageDescription = { if (Parameters.All(x => x.ResultReady)) { - OnExecute?.Invoke(MatImage); + OnExecute?.Invoke(x => + { + MatImage.Mat = x; + GC.Collect(); + }); } verticalstack.InvalidateArrange(); }; + if (p is ImageParameter imageParameter) + imageParameter.ParameterValueChanged += delegate + { + ExportVideoButton.Visibility = + (from pa in Parameters + where pa is ImageParameter impa && impa.IsVideoMode + select true).Count() == 1 ? Visibility.Visible : Visibility.Collapsed; + ExportVideoButtonContainer.InvalidateArrange(); + }; } + ExportVideoButton.Click += async delegate + { + var para = (from pa in Parameters + where pa is ImageParameter impa && impa.IsVideoMode + select pa).FirstOrDefault(default(ParameterFromUI)); + if (para is ImageParameter video && video.VideoCapture is VideoCapture vidcapture) + { + var picker = new FileSavePicker + { + SuggestedStartLocation = PickerLocationId.VideosLibrary + }; + + WinRT.Interop.InitializeWithWindow.Initialize(picker, App.CurrentWindowHandle); + + picker.FileTypeChoices.Add("MP4", new string[] { ".mp4" }); + picker.FileTypeChoices.Add("WMV", new string[] { ".wmv" }); + picker.FileTypeChoices.Add("MKV", new string[] { ".mkv" }); + + var sf = await picker.PickSaveFileAsync(); + if (sf != null) + { + var selectedframe = video.PosFrames; + var totalFrames = vidcapture.FrameCount; + var dialog = new ContentDialog + { + Content = new ProgressBar + { + //Value = 50, + }.Assign(out var progressRing), + XamlRoot = Result.XamlRoot, + }; + async Task RunLoop() + { + await Task.Run(async delegate + { + using var writer = new VideoWriter(sf.Path, FourCC.MP4V, vidcapture.Fps, + new OpenCvSharp.Size(vidcapture.FrameWidth, vidcapture.FrameHeight) + ); + for (int i = 0; i < totalFrames; i++) + { + video.PosFrames = i; + if (i % 10 == 0) + dialog.DispatcherQueue.TryEnqueue(delegate + { + progressRing.Value = (double)(i+1) / totalFrames * 100; + }); + if (video.Result is null) break; + TaskCompletionSource result = new(); + OnExecute?.Invoke(x => + { + result.SetResult(x); + GC.Collect(); + }); + writer.Write(await result.Task); + } + writer.Release(); + }); + }; + _ = dialog.ShowAsync(); + + await RunLoop(); + dialog.Hide(); + + video.PosFrames = selectedframe; + + } + } + }; verticalstack.Children.Add(Result); return new ScrollViewer diff --git a/PhotoToys/Parameters/CheckboxParameter.cs b/PhotoToys/Parameters/CheckboxParameter.cs index 4ac8748..e0427e2 100644 --- a/PhotoToys/Parameters/CheckboxParameter.cs +++ b/PhotoToys/Parameters/CheckboxParameter.cs @@ -72,14 +72,16 @@ public CheckboxParameter(string Name, bool Default, bool? InvisibleResult = null }; UI.RegisterPropertyChangedCallback(UIElement.VisibilityProperty, delegate { + _Result = UI.Visibility == Visibility.Visible ? (CheckBox.IsChecked ?? false) : this.InvisibleResult; ParameterValueChanged?.Invoke(); }); ParameterReadyChanged?.Invoke(); ParameterValueChanged?.Invoke(); } public override bool ResultReady => true; + public bool _Result; public new bool Result { - get => UI.Visibility == Visibility.Visible ? (CheckBox.IsChecked ?? false) : InvisibleResult; + get => _Result; set => CheckBox.IsChecked = value; } protected override bool GetResult() => Result; diff --git a/PhotoToys/Parameters/DoubleSliderParameter.cs b/PhotoToys/Parameters/DoubleSliderParameter.cs index bf6d8e5..401b542 100644 --- a/PhotoToys/Parameters/DoubleSliderParameter.cs +++ b/PhotoToys/Parameters/DoubleSliderParameter.cs @@ -13,27 +13,6 @@ namespace PhotoToys.Parameters; class DoubleSliderParameter : ParameterFromUI { - public class Converter : IValueConverter - { - double Min, Max; - Func DisplayConverter; - public Converter(double Min, double Max, Func DisplayConverter) - { - this.Min = Min; - this.Max = Max; - this.DisplayConverter = DisplayConverter; - } - public object Convert(object value, Type targetType, object parameter, string language) - { - if (value is not double Value) throw new ArgumentException(); - return DisplayConverter.Invoke(Value + Min); - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - throw new NotImplementedException(); - } - } public override event Action? ParameterReadyChanged, ParameterValueChanged; public DoubleSliderParameter(string Name, double Min, double Max, double StartingValue, double Step = 1, double SliderWidth = 300, Func? DisplayConverter = null) { @@ -83,7 +62,7 @@ public DoubleSliderParameter(string Name, double Min, double Max, double Startin Value = StartingValue - Min, Width = SliderWidth, Margin = new Thickness(0, 0, 10, 0), - ThumbToolTipValueConverter = new Converter(Min, Max, DisplayConverter) + ThumbToolTipValueConverter = new NewSlider.Converter(Min, Max, DisplayConverter) }.Edit(x => { x.ValueChanged += delegate diff --git a/PhotoToys/Parameters/ImageParameter.cs b/PhotoToys/Parameters/ImageParameter.cs index 444b182..6efe10d 100644 --- a/PhotoToys/Parameters/ImageParameter.cs +++ b/PhotoToys/Parameters/ImageParameter.cs @@ -59,9 +59,12 @@ cbi2.Tag is MatColors c2 && ColorModeParam.Items.Insert(0, ColorModeParam.GenerateItem(MatColors.Color)); } } + public bool IsVideoMode => !(VideoCapture is null || ViewAsImageParam.Result); public SelectParameter ColorModeParam { get; } public CheckboxParameter AlphaRestoreParam { get; } + public CheckboxParameter ViewAsImageParam { get; } public CheckboxParameter OneChannelReplacement { get; } + SimpleUI.FluentVerticalStack AdditionalOptionLayout; public ImageParameter(string Name = "Image", bool ColorMode = true, bool ColorChangable = true, bool OneChannelModeEnabled = false, bool AlphaRestore = true, bool AlphaRestoreChangable = true, AlphaModes AlphaMode = AlphaModes.Restore) { this.AlphaMode = AlphaMode; @@ -83,6 +86,10 @@ public ImageParameter(string Name = "Image", bool ColorMode = true, bool ColorCh AlphaRestoreParam.ParameterValueChanged += () => ParameterValueChanged?.Invoke(); OneChannelReplacement.ParameterReadyChanged += () => ParameterReadyChanged?.Invoke(); OneChannelReplacement.ParameterValueChanged += () => ParameterValueChanged?.Invoke(); + + ViewAsImageParam = new CheckboxParameter("Video As Image", false, true); + ViewAsImageParam.ParameterReadyChanged += () => ParameterReadyChanged?.Invoke(); + ViewAsImageParam.ParameterValueChanged += () => ParameterValueChanged?.Invoke(); UI = new Border { CornerRadius = new CornerRadius(16), @@ -98,7 +105,7 @@ public ImageParameter(string Name = "Image", bool ColorMode = true, bool ColorCh }, new Border { - Height = 250, + Height = 300, AllowDrop = true, Padding = new Thickness(16), Style = App.LayeringBackgroundBorderStyle, @@ -177,25 +184,37 @@ public ImageParameter(string Name = "Image", bool ColorMode = true, bool ColorCh HorizontalAlignment = HorizontalAlignment.Center } }.Assign(out var PreviewImage), - new Button + new SimpleUI.FluentVerticalStack { - Margin = new Thickness(0, 10, 0, 0), - HorizontalAlignment = HorizontalAlignment.Center, - Content = "Remove Image" + Children = + { + new NewSlider + { + Visibility = Visibility.Collapsed, + Width = 300 + }.Assign(out var FrameSlider), + new Button + { + HorizontalAlignment = HorizontalAlignment.Center, + Content = "Remove Image" + } + .Assign(out var RemoveImageButton) + } } .Edit(x => Grid.SetRow(x, 1)) - .Assign(out var RemoveImageButton) - + } } .Edit(x => RemoveImageButton.Click += delegate { x.Visibility = Visibility.Collapsed; Grid.SetColumnSpan(UIStack, 2); - _Result?.Dispose(); - _Result = null; + VideoCapture?.Dispose(); + VideoCapture = null; + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = null; ParameterReadyChanged?.Invoke(); - + ParameterValueChanged?.Invoke(); }) .Edit(x => Grid.SetColumn(x, 1)) .Assign(out var PreviewImageStack) @@ -233,15 +252,52 @@ public ImageParameter(string Name = "Image", bool ColorMode = true, bool ColorCh }; async Task ReadFile(StorageFile sf, string action) { - var stream = await sf.OpenStreamForReadAsync(); - var bytes = new byte[stream.Length]; - await stream.ReadAsync(bytes); - _Result?.Dispose(); - _Result = Cv2.ImDecode(bytes, ImreadModes.Unchanged); - await CompleteDrop( - ErrorTitle: "File Error", - ErrorContent: $"There is an error reading the file you {action}. Make sure the file is the image file!" - ); + if (sf.ContentType.Contains("image")) + { + // It's an image! + var stream = await sf.OpenStreamForReadAsync(); + var bytes = new byte[stream.Length]; + await stream.ReadAsync(bytes); + VideoCapture?.Dispose(); + VideoCapture = null; + FrameSlider.Visibility = Visibility.Collapsed; + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = Cv2.ImDecode(bytes, ImreadModes.Unchanged); + await CompleteDrop( + ErrorTitle: "File Error", + ErrorContent: $"There is an error reading the file you {action}. Make sure the file is the image file!" + ); + } + else if (sf.ContentType.Contains("video")) + { + // It's a video! + VideoCapture = VideoCapture.FromFile(sf.Path); + VideoCapture.PosFrames = 0; + + var framecount = VideoCapture.FrameCount; + var fps = VideoCapture.Fps; + FrameSlider.Visibility = Visibility.Visible; + FrameSlider.Minimum = 0; + FrameSlider.Maximum = framecount; + FrameSlider.ThumbToolTipValueConverter = new NewSlider.Converter(0, framecount, + x => $"{TimeSpan.FromSeconds(x / fps):c} (Frame {x})"); + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = new Mat(); + if (!VideoCapture.Read(ImageBeforeProcessed)) + ImageBeforeProcessed = null; + await CompleteDrop( + ErrorTitle: "File Error", + ErrorContent: $"There is an error reading the file you {action}. Make sure the file is the image file!" + ); + } else + { + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = null; + await CompleteDrop( + ErrorTitle: "File Error", + ErrorContent: $"There is an error reading the file you {action}. Make sure the file is the image or video file!" + ); + } } async Task ReadData(DataPackageView DataPackageView, string action) { @@ -259,8 +315,11 @@ async Task ReadData(DataPackageView DataPackageView, string action) var stream = b.AsStream(); var bytes = new byte[stream.Length]; await stream.ReadAsync(bytes); - _Result?.Dispose(); - _Result = Cv2.ImDecode(bytes, ImreadModes.Unchanged); + VideoCapture?.Dispose(); + VideoCapture = null; + FrameSlider.Visibility = Visibility.Collapsed; + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = Cv2.ImDecode(bytes, ImreadModes.Unchanged); await CompleteDrop( ErrorTitle: "Image Error", ErrorContent: "There is an error reading the Image you dropped" @@ -279,7 +338,7 @@ await CompleteDrop( } async Task CompleteDrop(string ErrorTitle, string ErrorContent) { - if (_Result == null) + if (ImageBeforeProcessed == null) { ContentDialog c = new() { @@ -291,10 +350,10 @@ async Task CompleteDrop(string ErrorTitle, string ErrorContent) await c.ShowAsync(); return; } - var oldResult = _Result; - _Result = oldResult.ToBGRA(); + var oldResult = ImageBeforeProcessed; + ImageBeforeProcessed = oldResult.ToBGRA(); oldResult.Dispose(); - PreviewImage.Mat = _Result; + PreviewImage.Mat = ImageBeforeProcessed; PreviewImageStack.Visibility = Visibility.Visible; Grid.SetColumnSpan(UIStack, 1); ParameterReadyChanged?.Invoke(); @@ -329,16 +388,33 @@ async Task CompleteDrop(string ErrorTitle, string ErrorContent) picker.FileTypeFilter.Add(".jpg"); picker.FileTypeFilter.Add(".jpeg"); picker.FileTypeFilter.Add(".png"); + picker.FileTypeFilter.Add(".mp4"); + picker.FileTypeFilter.Add(".wmv"); + picker.FileTypeFilter.Add(".mkv"); var sf = await picker.PickSingleFileAsync(); - if (sf != null) + if (sf is not null) await ReadFile(sf, "selected"); }; FromClipboard.Click += async delegate { await ReadData(Clipboard.GetContent(), "pasted"); }; - + FrameSlider.ValueChangedSettled += async delegate + { + if (VideoCapture is not null) + { + VideoCapture.PosFrames = (int)FrameSlider.Value; + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = new Mat(); + if (!VideoCapture.Read(ImageBeforeProcessed)) + ImageBeforeProcessed = null; + await CompleteDrop( + ErrorTitle: "Image Error", + ErrorContent: "There is an error reading the Image you selected" + ); + } + }; SelectInventory.Click += async delegate { var picker = InventoryPicker.Value; @@ -349,8 +425,11 @@ async Task CompleteDrop(string ErrorTitle, string ErrorContent) var newResult = await picker.PickAsync(SelectInventory); if (newResult != null) { - _Result?.Dispose(); - _Result = newResult; + VideoCapture?.Dispose(); + VideoCapture = null; + FrameSlider.Visibility = Visibility.Collapsed; + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = newResult; await CompleteDrop( ErrorTitle: "Image Error", ErrorContent: "There is an error reading the Image you selected" @@ -373,44 +452,61 @@ await CompleteDrop( } .Edit(x => { + x.Children.Add(ViewAsImageParam.UI.Edit(x => x.Visibility = Visibility.Collapsed)); if (ColorChangable) - x.Children.Add(ColorModeParam.UI.Edit(x => x.Margin = new Thickness { Bottom = 10 }).Edit(x => Grid.SetRow(x, 0))); + x.Children.Add(ColorModeParam.UI); if (AlphaRestoreChangable) - x.Children.Add(AlphaRestoreParam.UI.Edit(x => x.Margin = new Thickness { Bottom = 10 }).Edit(x => Grid.SetRow(x, 1))); + x.Children.Add(AlphaRestoreParam.UI); if (OneChannelModeEnabled) - { - x.Children.Add(OneChannelReplacement.UI.Edit(x => x.Margin = new Thickness { Bottom = 10 }).Edit(x => Grid.SetRow(x, 2))); - //void UpdateOneChannelReplacement() - //{ - // bool e = ColorModeParam.ResultReady && ColorModeParam.Result != MatColors.Color; - // OneChannelReplacement.UI.Visibility = e ? Visibility.Visible : Visibility.Collapsed; - //} - //ColorModeParam.ParameterValueChanged += UpdateOneChannelReplacement; - //UpdateOneChannelReplacement(); - //if (AlphaRestoreChangable) - //{ - // void UpdateAlphaRestore() - // { - // bool e = OneChannelReplacement.UI.Visibility == Visibility.Visible && OneChannelReplacement.ResultReady && OneChannelReplacement.Result; - // AlphaRestoreParam.UI.Visibility = e ? Visibility.Collapsed : Visibility.Visible; - // } - // AlphaRestoreParam.ParameterValueChanged += UpdateOneChannelReplacement; - // UpdateAlphaRestore(); - //} - } + x.Children.Add(OneChannelReplacement.UI); }) + .Assign(out AdditionalOptionLayout) } } }; } - public override bool ResultReady => _Result != null && ColorModeParam.ResultReady; - Mat? _Result = null; + public override bool ResultReady => ImageBeforeProcessed != null && ColorModeParam.ResultReady; + VideoCapture? _VideoCapture; + public int? PosFrames + { + get => VideoCapture?.PosFrames; + set + { + if (VideoCapture != null) + { + VideoCapture.PosFrames = value ?? 0; + ImageBeforeProcessed?.Dispose(); + ImageBeforeProcessed = new Mat(); + if (!VideoCapture.Read(ImageBeforeProcessed)) + ImageBeforeProcessed = null; + } + } + } + public VideoCapture? VideoCapture + { + get => _VideoCapture; + private set + { + _VideoCapture = value; + ViewAsImageParam.UI.Visibility = value is null ? Visibility.Collapsed : Visibility.Visible; + AdditionalOptionLayout.InvalidateArrange(); + } + } + Mat? _ImageBeforeProcessed = null; + Mat? ImageBeforeProcessed { + get => _ImageBeforeProcessed; + set + { + //VideoCapture?.Dispose(); + _ImageBeforeProcessed = value; + } + } public override Mat Result { get { using var tracker = new ResourcesTracker(); - var baseMat = _Result ?? throw new InvalidOperationException(); + var baseMat = ImageBeforeProcessed ?? throw new InvalidOperationException(); Mat outputMat; switch (ColorModeParam.Result) { @@ -446,7 +542,7 @@ public Mat? AlphaResult if (!ResultReady) throw new InvalidOperationException(); if (AlphaRestoreParam.Result) { - var baseMat = _Result ?? throw new InvalidOperationException(); + var baseMat = ImageBeforeProcessed ?? throw new InvalidOperationException(); return baseMat.ExtractChannel(3); } else return null; @@ -455,7 +551,7 @@ public Mat? AlphaResult public Mat PostProcess(Mat m) { using var tracker = new ResourcesTracker(); - var baseMat = _Result ?? throw new InvalidOperationException(); + var baseMat = ImageBeforeProcessed ?? throw new InvalidOperationException(); if (ColorModeParam.Result != MatColors.Color && OneChannelReplacement.Result) { var newmat = new Mat(); @@ -486,3 +582,13 @@ public Mat PostProcess(Mat m) public override FrameworkElement UI { get; } } +class TestStuff +{ + static TestStuff() + { + var vid = VideoCapture.FromFile(@"C:\Users\Get\Videos\click_through.mp4"); + var frameCount = vid.FrameCount; + var fps = vid.Fps; + + } +} \ No newline at end of file diff --git a/PhotoToys/Parameters/IntSliderParameter.cs b/PhotoToys/Parameters/IntSliderParameter.cs index ba2d530..f6f6940 100644 --- a/PhotoToys/Parameters/IntSliderParameter.cs +++ b/PhotoToys/Parameters/IntSliderParameter.cs @@ -7,6 +7,8 @@ using System.Text; using System.Threading.Tasks; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Data; + namespace PhotoToys.Parameters; class IntSliderParameter : DoubleSliderParameter @@ -42,4 +44,25 @@ public NewSlider() { ValueChanged += (_, _) => RunWhenSettled(); } + public class Converter : IValueConverter + { + double Min, Max; + Func DisplayConverter; + public Converter(double Min, double Max, Func DisplayConverter) + { + this.Min = Min; + this.Max = Max; + this.DisplayConverter = DisplayConverter; + } + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is not double Value) throw new ArgumentException(); + return DisplayConverter.Invoke(Value + Min); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } } \ No newline at end of file diff --git a/PhotoToys/PhotoToys.csproj.user b/PhotoToys/PhotoToys.csproj.user index d80cae5..49fc835 100644 --- a/PhotoToys/PhotoToys.csproj.user +++ b/PhotoToys/PhotoToys.csproj.user @@ -5,7 +5,7 @@ SideloadOnly False x86|x64|arm64 - True + False