Search code examples
c#wpfbitmapcrop

Clip region not working


My WPF application has a custom control I wrote specifically for displaying part of a bitmap. The bitmap is contained in an object transmitted to my program from my company's back end Windows service as a byte array. Included in the data is a rectangle which specifies the part of the image of interest which needs to be displayed.

Further, when the user clicks on the control, it cycles between three different "magnifications" of that region. This setting is called the "zoom state". Essentially, at one setting, the rectangle that is displayed is larger in width by 60 pixels than the region specified by the rectangle transmitted with the data. At the second setting, the rectangle's width is increased by 25%, and at the third, it is increased by 50%. The height is always computed to keep the aspect ratio of the portion of the bitmap displayed the same as the control's.

To date, I've been generating new bitmaps from the data that are cropped to the region specified by the rectangle computations described above. However, given the amount of data that we receive, and the size of the bitmaps, this is using huge amounts of memory. I need to find a way to reduce the memory consumption.

I did a search on cropping images in WPF & found this MSDN article on clipping an image. This seemed ideal, as I'm already computing a rectangle and it seems this would use a lot less memory. So I modified my code this morning so that instead of creating a CroppedBitmap from the original image & an Int32Rect, it creates a RetangleGeometry struct and sets the Image control's Clip property to that rectangle. The result, however, is I'm not seeing anything at all.

I commented out the code that creates the RectangleGeometry and I do see the whole image in the control at that point, so I know that the problem is somewhere in the code that computes the rectangle. I know the computations in the code are right, but I must be missing something when I convert it into a RectangleGeometry.

Here's the template that the custom control uses:

<Style TargetType="{x:Type local:ZoomableImage}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:ZoomableImage}">
                <Border BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}" 
                        ContextMenu="{TemplateBinding ContextMenu}"
                        Cursor="{TemplateBinding Cursor}"
                        Margin="{TemplateBinding Margin}"
                        Name="ImageBorder">
                    <Image BitmapEffect="{TemplateBinding BitmapEffect}" 
                           BitmapEffectInput="{TemplateBinding BitmapEffectInput}"
                           CacheMode="{TemplateBinding CacheMode}"
                           Effect="{TemplateBinding Effect}" 
                           HorizontalAlignment="Stretch"
                           Name="Image" 
                           Source="{TemplateBinding ImageToBeZoomed}"
                           Stretch="{TemplateBinding Stretch}" 
                           VerticalAlignment="Stretch" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And here's the code that computes the clip rectangle:

private void CreateClipRectangle() {
    Rect srcRect = new Rect( PlateRectangle.X, PlateRectangle.Y, PlateRectangle.Width, PlateRectangle.Height );

    // We want to show some pixels outside the plate's rectangle, so add 60 to the PlateRectangle's Width.
    srcRect.Width += 60.0;

    // Adjust the Width property for the ZoomState, which increases the height & width of the rectangle around the license plate
    if ( ZoomState == ZoomStates.ZoomPlus25 ) {
        srcRect.Width = srcRect.Width * 1.25;
    } else if ( ZoomState == ZoomStates.ZoomPlus50 ) {
        srcRect.Width = srcRect.Width * 1.50;
    }

    // Make sure that srcRect.Width is not bigger than the ImageToBeZoomed's PixelWidth!
    if ( srcRect.Width > ImageToBeZoomed.PixelWidth ) srcRect.Width = ImageToBeZoomed.PixelWidth;

    // We need to keep the aspect ratio of the source rectangle the same as the Image's.
    // Compute srcRect.Height so the srcRect will have the correct aspect ratio, but don't let 
    // the rectangle's height get bigger than the original image's height!
    srcRect.Height = Math.Min( ImageToBeZoomed.PixelHeight, Math.Round( srcRect.Width * ImageBorder.ActualHeight / ImageBorder.ActualWidth ) );

    // Adjust srcRect.X & srcRect.Y to center the source image in the output image
    srcRect.X = srcRect.X - ( srcRect.Width  - PlateRectangle.Width  ) / 2.0;
    srcRect.Y = srcRect.Y - ( srcRect.Height - PlateRectangle.Height ) / 2.0;

    // Adjust srcRect to keep the cropped region from going off the image's edges.
    if ( srcRect.X < 0 ) srcRect.X = 0.0;
    if ( srcRect.Y < 0 ) srcRect.Y = 0.0;
    if ( ( srcRect.X + srcRect.Width  ) > ImageToBeZoomed.PixelWidth  ) srcRect.X = ImageToBeZoomed.PixelWidth  - srcRect.Width;
    if ( ( srcRect.Y + srcRect.Height ) > ImageToBeZoomed.PixelHeight ) srcRect.Y = ImageToBeZoomed.PixelHeight - srcRect.Height;

    // Create a new RectangleGeometry object that we will use to clip the ImageToBeZoomed and put it into the Clip property.
    ImageControl.Clip = new RectangleGeometry( srcRect, 0.0, 0.0 );
}

ImageToBeZoomed is a DependencyProperty that is of type BitmapSource. The byte array is converted into a BitmapImage using this code:

public static BitmapImage BitmapFromBytes( byte[] imageBytes ) {
        BitmapImage image  = null;
        if ( imageBytes != null ) {
            image = new BitmapImage();
            try {
                using ( MemoryStream memoryStream = new MemoryStream( imageBytes ) ) {
                    image.BeginInit();
                    image.CacheOption  = BitmapCacheOption.OnLoad;
                    image.StreamSource = memoryStream;
                    image.EndInit();

                    // Freeze the BitmapImage.  This helps plug memory leaks & saves memory.
                    image.Freeze();
                }
            } catch ( Exception ex ) {
                // . . .
            }
        }
        return image;
    }

The values in the PlateRectangle property are ints and they are in pixels. Could the problem have something to do with needing to convert from pixels to device independent units?

EDIT 1

In playing with this, I've found that I see no image if I set the Y coordinate of the RectangleGeometry struct's Rect property to anything other than 0 or negative values. This makes no sense to me. In most cases, the area of concern is in the middle of the image & no where near the top or bottom edge. Does anyone have any ideas why this is so?

Edit 2

Am I the only one who is having problems with the WPF Clip functionality?


Solution

  • It looks like the Rect you're using for RectangleGeometry is not being scaled. There also seem to be a few other scaling issues in there.

    I replaced your CreateClipRectangle method with this (see inline comments for what was changed/added):

       private void CreateClipRectangle()
        {
            Rect srcRect = new Rect(PlateRectangle.X, PlateRectangle.Y, PlateRectangle.Width, PlateRectangle.Height);
    
            // We want to show some pixels outside the plate's rectangle, so add 60 to the PlateRectangle's Width.
            srcRect.Width += 60.0;
    
            // Adjust the Width property for the ZoomState, which increases the height & width of the rectangle around the license plate
            if (ZoomState == ZoomStates.ZoomPlus25)
            {
                srcRect.Width = srcRect.Width * 1.25;
            }
            else if (ZoomState == ZoomStates.ZoomPlus50)
            {
                srcRect.Width = srcRect.Width * 1.50;
            }
    
            // Make sure that srcRect.Width is not bigger than the ImageToBeZoomed's PixelWidth!
            if (srcRect.Width > ImageToBeZoomed.PixelWidth) srcRect.Width = ImageToBeZoomed.PixelWidth;
    
            // We need to keep the aspect ratio of the source rectangle the same as the Image's.
            // Compute srcRect.Height so the srcRect will have the correct aspect ratio, but don't let 
            // the rectangle's height get bigger than the original image's height!
            double aspectRatio = ((double)ImageToBeZoomed.PixelHeight / ImageToBeZoomed.PixelWidth); // <-- ADDED
            srcRect.Height = Math.Min(ImageToBeZoomed.PixelHeight, Math.Round(srcRect.Width * aspectRatio)); // <-- CHANGED
    
            // Adjust srcRect.X & srcRect.Y to center the source image in the output image
            srcRect.X = srcRect.X - srcRect.Width / 2.0; // <-- CHANGED
            srcRect.Y = srcRect.Y - srcRect.Height / 2.0; // <-- CHANGED
    
            // Adjust srcRect to keep the cropped region from going off the image's edges.
            if (srcRect.X < 0) srcRect.X = 0.0;
            if (srcRect.Y < 0) srcRect.Y = 0.0;
            if ((srcRect.X + srcRect.Width) > ImageToBeZoomed.PixelWidth) srcRect.X = ImageToBeZoomed.PixelWidth - srcRect.Width;
            if ((srcRect.Y + srcRect.Height) > ImageToBeZoomed.PixelHeight) srcRect.Y = ImageToBeZoomed.PixelHeight - srcRect.Height;
    
            double scaleX = (ImageControl.ActualWidth / ImageToBeZoomed.PixelWidth); // <-- ADDED
            double scaleY = (ImageControl.ActualHeight / ImageToBeZoomed.PixelHeight); // <-- ADDED
    
            srcRect.X *= scaleX; // <-- ADDED
            srcRect.Y *= scaleY; // <-- ADDED
            srcRect.Width *= scaleX; // <-- ADDED
            srcRect.Height *= scaleY; // <-- ADDED
    
            // Create a new RectangleGeometry object that we will use to clip the ImageToBeZoomed and put it into the Clip property.
            ImageControl.Clip = new RectangleGeometry(srcRect, 0.0, 0.0);
        }
    

    My ImageControl XAML was just

    <Image Name="ImageControl" Stretch="Uniform" />
    

    There may be further issues with the bounds checking, and I removed the border stuff that I wasn't sure about but hopefully this will get you started.