使用 InkCanvas 中的笔触裁剪 BitmapImage

Clip BitmapImage using Strokes from a InkCanvas

我的任务是创建一个 "Cinemagraph" 特征,用户必须 select 使用 InkCanvas 绘制应该保留的 selected 像素的所需区域animation/video 的其余部分保持不变(或者 select 应该是 "alive" 的像素)。

示例:

我正在考虑从 InkCanvas 获取 Stroke 集合并使用它来剪辑图像并与未修改的图像合并。

我该怎么做?我可以轻松地从磁盘加载图像,但是如何根据笔画裁剪图像?

更多详情:

绘制并 select 应该保持静态的像素后,我有一个 Stroke 集合。我可以获得每个个体的 Geometry Stroke,但我可能需要合并所有几何图形。

基于合并后的 Geometry,我需要反转(Geometry)并用于剪辑我的第一帧,稍后准备好剪辑图像后,我需要与所有其他帧合并.

到目前为止我的代码:

//Gets the BitmapSource from a String path:
var image = ListFrames[0].ImageLocation.SourceFrom();
var rectangle = new RectangleGeometry(new Rect(new System.Windows.Point(0, 0), new System.Windows.Size(image.Width, image.Height)));
Geometry geometry = Geometry.Empty;

foreach(Stroke stroke in CinemagraphInkCanvas.Strokes)
{
    geometry = Geometry.Combine(geometry, stroke.GetGeometry(), GeometryCombineMode.Union, null);
}

//Inverts the geometry, to clip the other unselect pixels of the BitmapImage.
geometry = Geometry.Combine(geometry, rectangle, GeometryCombineMode.Exclude, null);

//This here is UIElement, I can't use this control, I need a way to clip the image without using the UI.
var clippedImage = new System.Windows.Controls.Image();
clippedImage.Source = image;
clippedImage.Clip = geometry;

//I can't get the render of the clippedImage control because I'm not displaying that control.

有什么方法可以不使用 UIElement 来剪辑 BitmapSource 吗?

也许,也许

我正在考虑 OpacityMask 和画笔...但是我不能使用 UIElement,我需要将 OpacityMask 直接应用到 BitmapSource

我成功了! (You can see the result here, ScreenToGif > Editor > Image Tab > Cinemagraph)


代码

SourceFrom()DpiOf()ScaledSize():

/// <summary>
/// Gets the BitmapSource from the source and closes the file usage.
/// </summary>
/// <param name="fileSource">The file to open.</param>
/// <param name="size">The maximum height of the image.</param>
/// <returns>The open BitmapSource.</returns>
public static BitmapSource SourceFrom(this string fileSource, Int32? size = null)
{
    using (var stream = new FileStream(fileSource, FileMode.Open))
    {
        var bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;

        if (size.HasValue)
            bitmapImage.DecodePixelHeight = size.Value;

        //DpiOf() and ScaledSize() uses the same principles of this extension.

        bitmapImage.StreamSource = stream;
        bitmapImage.EndInit();

        //Just in case you want to load the image in another thread.
        bitmapImage.Freeze();             
        return bitmapImage;
    }
}

GetRender():

/// <summary>
/// Gets a render of the current UIElement
/// </summary>
/// <param name="source">UIElement to screenshot</param>
/// <param name="dpi">The DPI of the source.</param>
/// <returns>An ImageSource</returns>
public static RenderTargetBitmap GetRender(this UIElement source, double dpi)
{
    Rect bounds = VisualTreeHelper.GetDescendantBounds(source);

    var scale = dpi / 96.0;
    var width = (bounds.Width + bounds.X) * scale;
    var height = (bounds.Height + bounds.Y) * scale;

    #region If no bounds

    if (bounds.IsEmpty)
    {
        var control = source as Control;

        if (control != null)
        {
            width = control.ActualWidth * scale;
            height = control.ActualHeight * scale;
        }

        bounds = new Rect(new System.Windows.Point(0d, 0d), 
                          new System.Windows.Point(width, height));
    }

    #endregion

    var roundWidth = (int)Math.Round(width, MidpointRounding.AwayFromZero);
    var roundHeight = (int)Math.Round(height, MidpointRounding.AwayFromZero);

    var rtb = new RenderTargetBitmap(roundWidth, roundHeight, dpi, dpi, 
                                     PixelFormats.Pbgra32);

    DrawingVisual dv = new DrawingVisual();
    using (DrawingContext ctx = dv.RenderOpen())
    {
        VisualBrush vb = new VisualBrush(source);

        var locationRect = new System.Windows.Point(bounds.X, bounds.Y);
        var sizeRect = new System.Windows.Size(bounds.Width, bounds.Height);

        ctx.DrawRectangle(vb, null, new Rect(locationRect, sizeRect));
    }

    rtb.Render(dv);
    return (RenderTargetBitmap)rtb.GetAsFrozen();
}

获取 ImageSourceGeometry:

//Custom extensions, that using the path of the image, will provide the
//DPI (of the image) and the scaled size (PixelWidth and PixelHeight).
var dpi = ListFrames[0].ImageLocation.DpiOf();
var scaledSize = ListFrames[0].ImageLocation.ScaledSize();

//Custom extension that loads the first frame.
var image = ListFrames[0].ImageLocation.SourceFrom();

//Rectangle with the same size of the image. Used within the Xor operation.
var rectangle = new RectangleGeometry(new Rect(
    new System.Windows.Point(0, 0), 
    new System.Windows.Size(image.PixelWidth, image.PixelHeight)));
Geometry geometry = Geometry.Empty;

//Each Stroke is transformed into a Geometry and combined with an Union operation.
foreach(Stroke stroke in CinemagraphInkCanvas.Strokes)
{
    geometry = Geometry.Combine(geometry, stroke.GetGeometry(), 
        GeometryCombineMode.Union, null);
}

//The rectangle with the same size of the image is combined with all of 
//the Strokes using the Xor operation, basically it inverts the Geometry.
geometry = Geometry.Combine(geometry, rectangle, GeometryCombineMode.Xor, null);

Geometry 应用于 Image 元素:

//UIElement used to hold the BitmapSource to be clipped.
var clippedImage = new System.Windows.Controls.Image
{
    Height = image.PixelHeight,
    Width = image.PixelWidth,
    Source = image,
    Clip = geometry
};
clippedImage.Measure(scaledSize);
clippedImage.Arrange(new Rect(scaledSize));

//Gets the render of the Image element, already clipped.
var imageRender = clippedImage.GetRender(dpi, scaledSize);

//Merging with all frames:
Overlay(imageRender, dpi, true);   

Overlay(),合并帧:

private void Overlay(RenderTargetBitmap render, double dpi, bool forAll = false)
{
    //Gets the selected frames based on the selection of a ListView, 
    //In this case, every frame should be selected.
    var frameList = forAll ? ListFrames : SelectedFrames();

    int count = 0;
    foreach (FrameInfo frame in frameList)
    {
        var image = frame.ImageLocation.SourceFrom();

        var drawingVisual = new DrawingVisual();
        using (DrawingContext drawingContext = drawingVisual.RenderOpen())
        {
            drawingContext.DrawImage(image, new Rect(0, 0, image.Width, image.Height));
            drawingContext.DrawImage(render, new Rect(0, 0, render.Width, render.Height));
        }

        //Converts the Visual (DrawingVisual) into a BitmapSource
        var bmp = new RenderTargetBitmap(image.PixelWidth, image.PixelHeight, dpi, dpi, PixelFormats.Pbgra32);
        bmp.Render(drawingVisual);

        //Creates a BmpBitmapEncoder and adds the BitmapSource to the frames of the encoder
        var encoder = new BmpBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(bmp));

        //Saves the image into a file using the encoder
        using (Stream stream = File.Create(frame.ImageLocation))
            encoder.Save(stream);
    }
}

示例:

干净、未经编辑的动画。

选定的应设置动画的像素。

图像已被裁剪(黑色是透明的)。

电影摄影完成!

如您所见,只有选定的像素可以更改,其他像素保持不变。