Filter Explorer for Windows Phone 8

The Filter Explorer app demonstrates some of the image editing capabilities and performance of the Lumia Imaging SDK by allowing the user to apply a number of filter layers to existing or newly captured photos.

Getting started

Compatibility

Filter Explorer is compatible with Windows Phone 8 devices. It has been tested on Nokia Lumia 925 and Nokia Lumia 620.

Using the prebuilt installation package

Download the XAP file and install it on your device by using the Application Deployment tool that comes with the Windows Phone 8 SDK.

Building the application

Download the application solution source code and open the ImageProcessingApp.sln file in Visual Studio 2013 Express for Windows Phone 8. Start building and running the application by pressing F5 or selecting Start Debugging from the Debug menu.

Design

Filter Explorer opens up into a mosaic style photo stream of the user's Camera Roll photos. Photos in photo stream are filtered, each photo with one random filter, and the photos rotate in a quick pace revealing the same photo rendered with another randomly selected filter again and again.

Selecting a photo directly from the stream or by using the gallery picker or camera capture button takes the user to the main photo editing page. On this page tapping on the plus sign takes the user to a filter selection page. Selecting a filter takes the user back to the main photo editing page with the newly selected filter applied to the photo. Multiple filters can be stacked on top of each other again by tapping on the plus sign, and the "Applied filters" indicator in the top of the screen displays the currently applied filters.

Dn859588.filterexplorer_stream(en-us,WIN.10).png Dn859588.filterexplorer_original(en-us,WIN.10).png Dn859588.filterexplorer_filters(en-us,WIN.10).png Dn859588.filterexplorer_filtered(en-us,WIN.10).png

Architecture overview

Architecture breakdown

User interface (Pages namespace):

  • StreamPage displays user's Camera Roll photos with random filters applied to them.
  • PhotoPage displays the image that is currently being edited (with pinch zooming).
  • FilterPage displays thumbnails of filters that can be applied to the image being edited.
  • AboutPage contains some information about the application.

Application data model (Models namespace):

  • PhotoModel is the main model for an image session. It holds the image data and the filters that have been applied to the image.
  • FilterModel represents one filter in Filter Explorer. Notice that one Filter Explorer application filter may consist of many Lumia Imaging SDK IFilter items.
  • FiltersModel represents a collection of Filter Explorer application specific filters (of type FilterModel).
  • StreamItemModel represents a single photo in the photo stream.

Controls namespace contains application specific custom UI controls and their viewmodels. Converters and Helpers namespaces contain utility classes.

Initiating editing session

Initiating a session happens by setting an image buffer to PhotoModel.

using Lumia.Imaging;
using Lumia.InteropServices.WindowsRuntime;

// ...

/// <summary>
/// Photo model is the central piece for keeping the state of the image being edited.
/// </summary>
public class PhotoModel : IDisposable
{
    private IBuffer _buffer = null;

    // ...
    /// <summary>
    /// Get and set image data buffer.
    /// </summary>
    public IBuffer Buffer
    {
        get
        {
            using (BufferImageSource source = new BufferImageSource(_buffer))
            using (JpegRenderer renderer = new JpegRenderer(source))
            {
                IBuffer buffer = null;

                Task.Run(async () => { buffer = await renderer.RenderAsync(); }).Wait();

                return buffer;
            }
        }

        set
        {
            if (_buffer !) value)
            {
                _buffer = value;
            }
        }
    }

    // ...
}

Applying filters

Filters are applied to the session by simply storing them in member variables for later use in the rendering operations.

// ...

public class PhotoModel : IDisposable
{
    private List<IFilter> _components = new List<IFilter>();

    // ...

    public List<FilterModel> AppliedFilters { get; set; }

    // ...

    /// <summary>
    /// Check if there are filters applied that can be removed.
    /// </summary>
    public bool CanUndoFilter
    {
        get
        {
            return AppliedFilters.Count > 0;
        }
    }

    // ...

    /// <summary>
    /// Apply filter to image. Notice that FilterModel may consist of many IFilter components.
    /// </summary>
    /// <param name="filter">Filter to apply</param>
    public void ApplyFilter(FilterModel filter)
    {
        AppliedFilters.Add(filter);

        foreach (IFilter f in filter.Components)
        {
            _components.Add(f);
        }
    }

    /// <summary>
    /// Undo last applied filter (if any).
    /// </summary>
    public void UndoFilter()
    {
        if (CanUndoFilter)
        {
            AppliedFilters.RemoveAt(AppliedFilters.Count - 1);

            for (int i = 0; i < filter.Components.Count; i++)
            {
                _components.RemoveAt(_components.Count - 1);
            }
        }
    }

    // ...
}

Rendering image

An image representing the current session with applied filters is rendered with WriteableBitmapRenderer. Similarly, a JPEG buffer can be rendered with JpegRenderer.

// ...

public class PhotoModel : IDisposable
{
    // ...
    /// <summary>
    /// Renders current image with applied filters to the given bitmap.
    /// </summary>
    /// <param name="bitmap">Bitmap to render to</param>
    public async Task RenderBitmapAsync(WriteableBitmap bitmap)
    {
        using (BufferImageSource source = new BufferImageSource(_buffer))
        using (FilterEffect effect = new FilterEffect(source) { Filters = _components })
        using (WriteableBitmapRenderer renderer = new WriteableBitmapRenderer(effect, bitmap))
        {
            await renderer.RenderAsync();

            bitmap.Invalidate();
        }
    }

    /// <summary>
    /// Renders current image with applied filters to a buffer and returns it.
    /// Meant to be used where the filtered image is for example going to be
    /// saved to a file.
    /// </summary>
    /// <returns>Buffer containing the filtered image data</returns>
    public async Task<IBuffer> RenderFullBufferAsync()
    {
        using (BufferImageSource source = new BufferImageSource(_buffer))
        using (FilterEffect effect = new FilterEffect(source) { Filters = _components })
        using (JpegRenderer renderer = new JpegRenderer(effect))
        {
            return await renderer.RenderAsync();
        }
    }

    /// <summary>
    /// Renders a thumbnail of requested size from the center of the current image with
    /// filters applied.
    /// </summary>
    /// <param name="side">Side length of square thumbnail to render</param>
    /// <returns>Rendered thumbnail bitmap</returns>
    public async Task<Bitmap> RenderThumbnailBitmapAsync(int side)
    {
        Windows.Foundation.Size dimensions = await GetImageSizeAsync();

        int minSide = (int)Math.Min(Width, Height);

        Windows.Foundation.Rect rect = new Windows.Foundation.Rect()
        {
            Width = minSide,
            Height = minSide,
            X = (Width - minSide) / 2,
            Y = (Height - minSide) / 2,
        };

        _components.Add(new CropFilter(rect));

        Bitmap bitmap = new Bitmap(new Windows.Foundation.Size(side, side), ColorMode.Ayuv4444);

        using (BufferImageSource source = new BufferImageSource(_buffer))
        using (FilterEffect effect = new FilterEffect(source) { Filters = _components })
        using (BitmapRenderer renderer = new BitmapRenderer(effect, bitmap, OutputOption.Stretch))
        {
            await renderer.RenderAsync();
        }

        _components.RemoveAt(_components.Count - 1);

        return bitmap;
    }

    /// <summary>Gets the size of the current image</summary>
    /// <returns>Size of the current image</returns>
    public async Task<Windows.Foundation.Size> GetImageSizeAsync()
    {
        using (BufferImageSource source = new BufferImageSource(_buffer))
        {
            return (await source.GetInfoAsync()).ImageSize;
        }
    }

    // ...
}

Rendering the photo stream

In Filter Explorer, a specific StreamRenderingHelper class was created to render both asynchronously and also fast a large number of photos to be displayed on the photo stream page.

// ...

/// <summary>
/// View model representing a single photo item in the application's Camera Roll photo stream.
/// </summary>
public class StreamItemViewModel : INotifyPropertyChanged
{
    // ...

    /// <summary>
    /// Photo stream mosaic layout has images of three sizes.
    /// </summary>
    public enum Size
    {
        None,
        Small,
        Medium,
        Large
    };

    /// <summary>
    /// Size of the needed bitmap to represent this photo stream item.
    /// </summary>
    public Size RequestedSize { get; private set; }

    // ...

    /// <summary>
    /// Method to be called when a bitmap has been rendered for this photo stream item.
    /// 
    /// Starts animated transition to the new image.
    /// </summary>
    /// <param name="bitmap">New image bitmap</param>
    public void TransitionToImage(WriteableBitmap bitmap)
    {
        // ...
    }

    // ...
}

/// <summary>
/// Helper class for asynchronously rendering many images in a fast pace. Rendering is done
/// by invoking many single quick rendering operations in the calling thread's dispatcher.
/// </summary>
public class StreamRenderingHelper : INotifyPropertyChanged
{
    private List<StreamItemViewModel> _priorityQueue = new List<StreamItemViewModel>();
    private List<StreamItemViewModel> _standardQueue = new List<StreamItemViewModel>();
    private bool _enabled = false;
    private int _processingNow = 0;
    private int _processingMax = 18;

    public bool Busy { get; set; }

    /// <summary>
    /// Enable or disable processing of rendering queue.
    /// </summary>
    public bool Enabled
    {
        get
        {
            return _enabled;
        }

        set
        {
            if (_enabled != value)
            {
                _enables = value;

                // ...

                if (_enabled)
                {
                    EnsureProcessing();
                }
            }
        }
    }

    /// <summary>
    /// Total amount of photo stream items currently in the rendering queue.
    /// </summary>
    public int Count
    {
        get
        {
            return _priorityQueue.Count + _standardQueue.Count;
        }
    }

    // ...

    /// <summary>
    /// Adds a photo stream item to the rendering queue.
    /// 
    /// Item's void TransitionToImage(WriteableBitmap) will be called when rendering has been completed.
    /// </summary>
    /// <param name="item">Photo stream item to add</param>
    /// <param name="priority">True if rendering this item is high priority, otherwise false</param>
    public void Add(StreamItemViewModel item, bool priority)
    {
        if (priority)
        {
            _priorityQueue.Add(item);
        }
        else
        {
            _standardQueue.Add(item);
        }

        EnsureProcessing();
    }

    // ...

    /// <summary>
    /// Ensures that images are being rendered.
    /// </summary>
    private void EnsureProcessing()
    {
        for (int i = 0; _enabled && _processingNow < _processingMax && _processingNow < Count; i++)
        {
            Process();
        }
    }

    /// <summary>
    /// Begins a photo stream item rendering process loop. Loop is executed asynchronously item by item
    /// until there are no more items in the queue.
    /// </summary>
    private async void Process()
    {
        _processingNow++;

        while (_enabled && Count > 0)
        {
            StreamItemViewModel item;

            if (_priorityQueue.Count > 0)
            {
                // ...

                item = _priorityQueue[0];

                _priorityQueue.RemoveAt(0);
            }
            else
            {
                item = _standardQueue[0];

                _standardQueue.RemoveAt(0);
            }

            try
            {
                WriteableBitmap bitmap = null;
                Stream thumbnailStream = null;

                if (item.RequestedSize == StreamItemViewModel.Size.Large)
                {
                    bitmap = new WriteableBitmap(280, 280);
                    thumbnailStream = item.Model.Picture.GetImage();
                }
                else if (item.RequestedSize == StreamItemViewModel.Size.Medium)
                {
                    bitmap = new WriteableBitmap(140, 140);
                    thumbnailStream = item.Model.Picture.GetThumbnail();
                }
                else
                {
                    bitmap = new WriteableBitmap(70, 70);
                    thumbnailStream = item.Model.Picture.GetThumbnail();
                }

                thumbnailStream.Position = 0;

                using (StreamImageSource source = new StreamImageSource(thumbnailStream))
                using (FilterEffect effect = new FilterEffect(source))
                {
                    List<IFilter> filters = new List<IFilter>();

                    if (item.RequestedSize == StreamItemViewModel.Size.Large)
                    {
                        int width = item.Model.Picture.Width;
                        int height = item.Model.Picture.Height;

                        if (width > height)
                        {
                            filters.Add(new CropFilter(new Windows.Foundation.Rect()
                            {
                                Width = height,
                                Height = height,
                                X = width / 2 - height / 2,
                                Y = 0
                            }));
                        }
                        else
                        {
                            filters.Add(new CropFilter(new Windows.Foundation.Rect()
                            {
                                Width = width,
                                Height = width,
                                X = 0,
                                Y = height / 2 - width / 2
                            }));
                        }
                    }

                    if (item.Model.Filter != null)
                    {
                        foreach (IFilter f in item.Model.Filter.Components)
                        {
                            filters.Add(f);
                        }
                    }

                    effect.Filters = filters;

                    using (WriteableBitmapRenderer renderer = new WriteableBitmapRenderer(effect, bitmap))
                    {
                        await renderer.RenderAsync();
                    }
                }
                    
                item.TransitionToImage(bitmap);
            }
            catch (Exception ex)
            {
                item.TransitionToImage(null);
            }
        }

        _processingNow--;

        // ...
    }
}

Downloads

Filter Explorer project filter-explorer-master.zip

This example application is hosted in GitHub, where you can check the latest activities, report issues, browse source, ask questions, or even contribute to the project yourself.