使用InkCanvas中的Strokes剪辑BitmapImage

时间:2016-02-06 19:04:38

标签: c# wpf canvas clip

我的任务是创建一个“Cinemagraph”功能,用户必须使用InkCanvas选择一个所需的区域来绘制所选的像素,这些像素对于动画/视频的其余部分应保持不变(或者,选择应该“活着”的像素。)

示例:From Johan Blomström

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

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

更多详情:

绘制并选择应保持静态的像素后,我有一个Stroke集合。我可以获得每个Geometry的{​​{1}},但我可能需要合并所有几何。

基于合并的Stroke,我需要反转(Geometry)并使用剪辑我的第一帧,稍后剪辑的图像就绪,我需要与所有其他帧合并。 / p>

到目前为止我的代码:

Geometry

有没有办法在不使用UIElement的情况下剪辑BitmapSource?

也许,也许

我正在考虑//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,我需要将OpacityMask直接应用于OpacityMask

1 个答案:

答案 0 :(得分:9)

我做到了! (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);
    }
}

实施例

清洁,未经编辑的动画。

Animation

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

Green is the pixels that should move

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

Clipped image

Cinemagraph完成了!

Only the selected pixels move

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