Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Rotating and cropping an image in WPF

Pete Brown - 24 March 2010

In a sample application I'm working on, Shoebox Scan, I need to be able to work with a rotated crop and save the resulting image off to a file. The image is much larger than what is shown on-screen.

For example, this rotated selection from a much larger image:

Me, posing in San Francisco, about a hundred years ago

Should result in an image that looks something like this:

Me, posing in San Francisco, about a hundred years ago

 

The Problem

There are a couple operations we have to perform on this image. First, we must rotate it opposite the rotation of the adorners. Second, we need to crop to the bounds of the adorner. There's math that does this, and there are great algorithms that keep the image nice and smooth after the rotation. I don't know the math, and can't find the algorithms.

The Solution

Step one is to have the crop area keep track of center point, rotation, width, and height rather than just a set of 4 points. From there, we need to come up with a solution for doing the actual crop and rotate.

I'll be the first to admit that I pay daily for my inattention and minimal effort in higher math classes. So, I needed to find a solution that was either:

  • Canned algorithm someone posted
  • Free third-party code or control with a compatible license
  • Pumping it through the WPF UI layer

Having found nothing from the first two, I decided to go the third route.

DISCLAIMER: Gah! I'm using UI layer elements from back in service code on the client. This is not a best practice by any stretch, and has some limitations, especially around threading. I'm posting it here just because I know you are all cool to the occasional ugly hack, and won't beat me up too much over it. Use it in your own code only if you attribute it to your slow cousin or something.

The Magic

The unicorns and rainbows all come about from the RenderTargetBitmap class in WPF. This class allows you to render to a bitmap anything which could normally be rendered to screen.

In order to use RenderTargetBitmap without actually having the visual on-screen, you need to create a container, put your image (or other visuals in it) and then tell the container to arrange itself and its children. I used a canvas since canvas makes no attempt to scale children.

// more ui cruft. This is just for layout
Canvas container = new Canvas();
container.Children.Add(image);
container.Arrange(new Rect(0, 0, source.PixelWidth, source.PixelHeight));

// render the result to a new bitmap. 
RenderTargetBitmap target = new RenderTargetBitmap(targetWidth, targetHeight, sourceDpiX, sourceDpiY, PixelFormats.Default);
target.Render(container);

You can then take the RenderTargetBitmap and use it in place of a BitmapSource in any control.

An Aside on DPI

The key thing to remember is that you're dealing with a number of DPI conversions. The image from the scanner is typically at 300, 600 or 1200 dpi. WPF (and Silverlight) use device-independent pixels, so you have to convert to those. The formula is:

screen width = scanned image width / scanned image dpi * 96

Whenever you do any manipulation with a transform or other UI element and target your scanned image, you'll need to do this conversion.

 

Test Application

This was a bit of a hairy problem, so I built a little test app that had a pre-scanned image. Debugging while waiting for the scanner each run was not going to be productive.

The application calls into a library shared by this test app, and the main scanner app. The library contains the model, viewmodels, services etc. - everything but the UI. The test app contains the sample image and the code to call the crop and rotate code.

image

The xaml for the test app window is really simple:

<Window x:Class="ShoeBoxScan.VisualTests.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="550" Width="925">
    <Window.Background>
        <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
            <GradientStop Color="#FF000000"
                          Offset="0" />
            <GradientStop Color="#FF503030"
                          Offset="1" />
        </LinearGradientBrush>
    </Window.Background>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>

        <Image Grid.Column="0"
               Stretch="Uniform"
               x:Name="SourceImage" />
        
        <Image Grid.Column="1"
               Stretch="Uniform"
               x:Name="ManipulatedImage" />

        <StackPanel Grid.Row="1"
                    Grid.ColumnSpan="2"
                    Orientation="Horizontal"
                    HorizontalAlignment="Right">
            <Button x:Name="RotateAndCrop"
                    Content="Rotate and Crop"
                    Click="RotateAndCrop_Click"/>
        </StackPanel>

    </Grid>
</Window>

For the test code, I measured the approximate center points of each image using photoshop (Window -> Info or F8), figured out the angle and actual size by doing a rotate and crop in the same. I then plugged those numbers into the test application, as I wanted it isolated from the adorner code.

private void RotateAndCrop_Click(object sender, RoutedEventArgs e)
{
    var service = new ImageManipulationService();

    BitmapSource source = new BitmapImage(new Uri("SampleSourceImage.jpg", UriKind.Relative));
    SourceImage.Source = source;

    // test center point and height/width obtained by looking at the image in photoshop
    BitmapSource cropped = service.GenerateCroppedImage(source, new Point(1278, 908), 1850, 1250, -16);
    //BitmapSource cropped = service.GenerateCroppedImage(source, new Point(1510, 2648), 1830, 1230, 17.3);
    ManipulatedImage.Source = cropped;
}

Here's the test app running with the pre-scanned image.

Hey, it's me posing again!

Me, my son, and evil dudes Christmas 2009

And here's the entire rotate and crop function. Disclaimer above applies, so don't just skip to this code and paste it into your production app :) The code rotates the image around the center of the small image we want to crop out, then moves the whole image up so only the part we want is in the uncropped area - effectively cropping the image.

public BitmapSource GenerateCroppedImage(BitmapSource source, Point centerPoint, double width, double height, double rotationAngle)
{
    // TODO: get bounding box for the rotation, as the total image could be really huge

    double sourceDpiX = source.DpiX;
    double sourceDpiY = source.DpiY;

    double wpfUnitsX = 96;
    double wpfUnitsY = 96;

    double centerPointXInScreenUnits = centerPoint.X / sourceDpiX * wpfUnitsX;
    double centerPointYInScreenUnits = centerPoint.Y / sourceDpiY * wpfUnitsY;

    int targetWidth = (int)Math.Round(width, 0);
    int targetHeight = (int)Math.Round(height, 0);

    double targetWidthInScreenUnits = targetWidth / sourceDpiX * wpfUnitsX;
    double targetHeightInScreenUnits = targetHeight / sourceDpiY * wpfUnitsY;

    double sourceWidthInScreenUnits = source.PixelWidth / sourceDpiX * wpfUnitsX;
    double sourceHeightInScreenUnits = source.PixelHeight / sourceDpiY * wpfUnitsY;
    
    // rotate the master image around the point
    RotateTransform rotateTransform = new RotateTransform();
    rotateTransform.CenterX = centerPointXInScreenUnits;
    rotateTransform.CenterY = centerPointYInScreenUnits;
    rotateTransform.Angle = rotationAngle * -1;

    // move the point up to the top left
    TranslateTransform translateTransform = new TranslateTransform();
    translateTransform.X = -1 * (centerPointXInScreenUnits - targetWidthInScreenUnits / 2);
    translateTransform.Y = -1 * (centerPointYInScreenUnits - targetHeightInScreenUnits / 2);

    TransformGroup transformGroup = new TransformGroup();
    transformGroup.Children.Add(rotateTransform);
    transformGroup.Children.Add(translateTransform);

    // create an image element to do all the manipulation. This is a cheap way of
    // getting around doing the math myself.
    Image image = new Image();
    image.Stretch = Stretch.None;
    image.Source = source;
    image.RenderTransform = transformGroup;

    // more ui cruft. This is just for layout
    Canvas container = new Canvas();
    container.Children.Add(image);
    container.Arrange(new Rect(0, 0, source.PixelWidth, source.PixelHeight));

    // render the result to a new bitmap. 
    RenderTargetBitmap target = new RenderTargetBitmap(targetWidth, targetHeight, sourceDpiX, sourceDpiY, PixelFormats.Default);
    target.Render(container);

    return target;
}

The disadvantage of this approach is it is really tied closely to WPF UI. The advantage is...it's tied closely to WPF UI, so you get smooth rotation and resizing and all that other goodness without having to write that code yourself.

Optimizations

One optimization that I'll probably need to do is to pre-crop the image to the bounding box of the adorner before I go and rotate it. Rotation can be an expensive operation, especially on a 1200+ DPI scan. I'll test actual performance and memory use before doing that, however.

       
posted by Pete Brown on Wednesday, March 24, 2010
filed under:        

3 comments for “Rotating and cropping an image in WPF”

  1. Svicksays:
    I'm using this code and it seems to be leaking memory. After rotating twenty 6 Mpx image, the memory used by the application is about 1 GB and doesn't go down even when I tried calling GC.Collect() specifically. Do you have any idea what could be the problem?

Comment on this Post

Remember me