使用InkCanvas中的笔画裁剪BitmapImage

10
我被委托创建一个“Cinemagraph”功能,用户必须使用InkCanvas选择所需区域,以绘制应保留未更改的选定像素,以用于其余动画/视频(或选择应为“活动”的像素)。
例如:From Johan Blomström 我考虑获取InkCanvas中的Stroke集合,并使用它来剪切图像并与未更改的图像合并。
如何操作?我可以轻松地从磁盘加载图像,但是如何根据笔画剪切图像?
更多细节:
在绘制和选择要保留静态的像素后,我有一个Stroke集合。我可以获取每个单独StrokeGeometry,但我可能需要合并所有几何图形。
基于合并的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和brush…但是我不能使用UIElement,我需要直接应用OpacityMask到BitmapSource。


不太清楚,请解释得更详细些。 - AnjumSKhan
@AnjumSKhan 我需要根据一个形状来裁剪给定的图像。 - Nicke Manarin
1个回答

10

我做到了! (您可以在这里查看结果,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);
    }
}

例子:

干净、未经编辑的动画。

Animation

选择应该被动画的像素。

Green is the pixels that should move

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

Clipped image

Cinemagraph完成!

Only the selected pixels move

正如你所看到的,只有选定的像素可以改变,其他的保持静态。


这太棒了!请问您能发布您所使用的依赖关系吗? - CodeArtist
1
@CodeArtist 当然,看一下吧。我刚刚做了那个。 - Nicke Manarin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接