07 May 2009

Cutting images with PresentationCore

In my business cutting images into smaller images is something that happens quite often these days, since tile based mapping systems like Google Maps, Virtual Earth and OpenLayers are becoming ever more popular. For the past few years I have been using GDI+ as my workhorse, but last week I've kissed it goodbye. Over are the days of messing around with Graphics and Bitmap and not forgetting to dispose them. Enter PresentationCore with the System.Windows.Media.Imaging classes! To use this API, you need to make references to both PresentationCore.dll and WindowsBase.dll. I created a small sample cutter class like this:
using System;
using System.Windows;
using System.Windows.Media.Imaging;
using System.IO;

namespace LocalJoost.ImageCutting
{
  public class ImageCutter
  {
    private string _fileName;
    public ImageCutter(string fileName)
    {
      _fileName = fileName;
    }

    public void Cut(int TileSize, int TilesX, int TilesY)
    {
      var img = new BitmapImage();
      img.BeginInit();
      img.UriSource = new Uri(_fileName);
      img.CacheOption = BitmapCacheOption.OnLoad;
      img.EndInit();

      var fInfo = new FileInfo(_fileName);

      for (int x = 0; x < TilesX; x++)
      {
        for (int y = 0; y < TilesY; y++)
        {
          var subImg = new CroppedBitmap(img,
                   new Int32Rect(x * TileSize,
                          y * TileSize,
                          TileSize, TileSize));
          SaveImage(subImg, fInfo.Extension, 
            string.Format( "{0}_{1}{2}", x, y, fInfo.Extension));

        }
      }
    }
 
    private void SaveImage(BitmapSource image, 
                           string extension, string filePath)
    {
      var encoder = ImageUtilities.GetEncoderFromExtension(extension);
      using (var fs = new FileStream(filePath, 
              FileMode.Create, FileAccess.Write))
      {
        encoder.Frames.Add(BitmapFrame.Create(image));
        encoder.Save(fs);
        fs.Flush();
        fs.Close();
      }
    }
  }
}
You construct this class with a full path to an image file as a string, and then call the "Cut" method with tilesize in pixels (tiles are considered to be square) and the number of tiles in horizontal and vertical direction. It then goes on to cut the image into tiles of TileSize x TileSize pixels. Notice a few things:
img.CacheOption = BitmapCacheOption.OnLoad;
makes sure the image is loaded in one go, and does not get locked The trick of cutting the image itself is done by
var subImg = new CroppedBitmap(img,
               new Int32Rect(x * TileSize,
               y * TileSize,
               TileSize, TileSize));
and then it is fed to a simple private method that saves it to a file. Last, I do not check if the image is large enough to cut the number of tiles you want from. This is a sample, eh? The sample uses a small utility class that gets the right imaging encoder from either the file extension or the mime type, whatever you pass on to it
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace LocalJoost.ImageCutting
{
  /// 
  /// Class with Image utilities - duh
  /// 
  public static class ImageUtilities
  {
    public static BitmapEncoder GetEncoderFromMimeType(string mimeType)
    {
      switch (mimeType.ToLower())
      {
        case "image/jpg":
        case "image/jpeg": 
          return new JpegBitmapEncoder();
        case "image/gif": 
          return new GifBitmapEncoder();
        case "image/png":
          return new PngBitmapEncoder();
        case "image/tif":
        case "image/tiff":
          return new TiffBitmapEncoder();
        case "image/bmp": 
          return new BmpBitmapEncoder();
      }
      return null;
    }

    public static BitmapEncoder GetEncoderFromExtension(string extension)
    {
      return GetEncoderFromMimeType( extension.Replace(".", "image/"));
    }
  }
}
Not only are the System.Windows.Media.Imaging classes easier to use, they are also faster: switching from GDI+ to System.Windows.Media.Imaging reduced processing time to 50%, with an apparant significant lower CPU load and memory requirement. A complete example, including a unit test project that contains a test image which performs the completely hypothetical action of cutting a large 2048x2048 map file into 256x256 tiles ;-), is downloadable here. This project contains the following statement
Path.GetDirectoryNameAssembly.GetExecutingAssembly().Location
don't be intimidated by this, that's just a way to determine the full path of the current directory, i.e. the directory in which the unit test is running - you will find the resulting images there.

2 comments:

Anonymous said...

This is a good stuff. I have had a situation like this to contend with but this is brilliant, particularly the speed.

I made a modification to the Cut Method, to make sure the size is within the image, and if not, adjust the the x and y so that the code will still cut the size requested, albeit, with overlap.

public void Cut(int TileSize, int TilesX, int TilesY)
{
var img = new BitmapImage();
img.BeginInit();
img.UriSource = new Uri(_fileName);
img.CacheOption = BitmapCacheOption.OnLoad;
img.EndInit();

var fInfo = new FileInfo(_fileName);
int newX = 0;
int newY = 0;
int imgWidth = img.PixelWidth;
int imgHeight = img.PixelHeight;
for (int x = 0; x < TilesX; x++)
{
for (int y = 0; y < TilesY; y++)
{
newX = x * TileSize > imgWidth - TileSize ? imgWidth - TileSize : x * TileSize;
newY = y * TileSize > imgHeight - TileSize ? imgHeight - TileSize : y * TileSize;
var subImg = new CroppedBitmap(img,
new Int32Rect(newX,
newY,
TileSize, TileSize));
SaveImage(subImg, fInfo.Extension,
string.Format( "{0}_{1}{2}", x, y, fInfo.Extension));

}
}
}

Loc#alJoost said...

Thanks for the addition. I left this out as 'exercise for the reader' and it is good to see that someone actually take the exercise ;-).