Translate Rectangle Position in Zoom Mode Picturebox

Translate Rectangle Position in Zoom Mode Picturebox

You can translate selected rectangle on the picture box to the rectangle on image this way:

public RectangleF GetRectangeOnImage(PictureBox p, Rectangle selectionRect)
{
var method = typeof(PictureBox).GetMethod("ImageRectangleFromSizeMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var imageRect = (Rectangle)method.Invoke(p, new object[] { p.SizeMode });
if (p.Image == null)
return selectionRect;
var cx = (float)p.Image.Width / (float)imageRect.Width;
var cy = (float)p.Image.Height / (float)imageRect.Height;
var r2 = Rectangle.Intersect(imageRect, selectionRect);
r2.Offset(-imageRect.X, -imageRect.Y);
return new RectangleF(r2.X * cx, r2.Y * cy, r2.Width * cx, r2.Height * cy);
}

Note: You can find ImageRectangleFromSizeMode method source code here and use it as write such method as part of your application code.

Example - Crop Image of PictureBox having SizeMode = Zoom

As an example, the following code will crop the given rectangle of the picture box 1 and will set the result as image of picture box 2:

var selectedRectangle = new Rectangle(7, 30, 50, 40);
var result = GetRectangeOnImage(pictureBox1, selectedRectangle);
using (var bm = new Bitmap((int)result.Width, (int)result.Height))
{
using (var g = Graphics.FromImage(bm))
g.DrawImage(pictureBox1.Image, 0, 0, result, GraphicsUnit.Pixel);
pictureBox2.Image = (Image)bm.Clone();
}

Here is the input image:

Sample Image

And this is the result:

Sample Image

Translating Rectangle Position in Zoom Mode Picturebox Results in Negative Y Coordinate

You know:

  • How to calculate the zoom factor (scale) of the zoomed picture box.
  • How to get the image offset (top-left corner) in the zoomed picture box.

Then having a rectangle on the image (with original size), this is how you can calculate the rectangle size and position on the zoomed picture box:

  1. Zoom out the rectangle using the vertical and horizontal scales
  2. Offset the result using the image offset of the zoomed picture box.

Here are the two methods:

  • GetRectangeOnImage: Having the rectangle on a zoomed picture box, it returns a rectangle on the original image
  • GetRectangeOnPictureBox: Having the rectangle on original image, it returns a rectangle on the zoomed picture box.

Code:

public RectangleF GetRectangeOnImage(PictureBox p, Rectangle rectOnPictureBox)
{
var method = typeof(PictureBox).GetMethod("ImageRectangleFromSizeMode",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
var imageRect = (Rectangle)method.Invoke(p, new object[] { p.SizeMode });
if (p.Image == null)
return rectOnPictureBox;
var cx = (float)p.Image.Width / (float)imageRect.Width;
var cy = (float)p.Image.Height / (float)imageRect.Height;
rectOnPictureBox.Offset(-imageRect.X, -imageRect.Y);
return new RectangleF(rectOnPictureBox.X * cx, rectOnPictureBox.Y * cy,
rectOnPictureBox.Width * cx, rectOnPictureBox.Height * cy);
}
public RectangleF GetRectangeOnPictureBox(PictureBox p, Rectangle rectOnImage)
{
var method = typeof(PictureBox).GetMethod("ImageRectangleFromSizeMode",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
var imageRect = (Rectangle)method.Invoke(p, new object[] { p.SizeMode });
if (p.Image == null)
return rectOnImage;
var cx = (float)p.Image.Width / (float)imageRect.Width;
var cy = (float)p.Image.Height / (float)imageRect.Height;
var r2 = new RectangleF(rectOnImage.X / cx, rectOnImage.Y / cy,
rectOnImage.Width / cx, rectOnImage.Height / cy);
r2.Offset(imageRect.X, imageRect.Y);
return r2;
}

Zoom and translate an Image from the mouse location

A few suggestions and a couple of tricks.

Not exactly tricks, just some methods to speed up the calculations when more than one graphic transformation is in place.

  1. Divide and conquer: split the different graphics effects and transformations in different, specialized, methods that do one thing. Then design in a way that makes it possible for these methods to work together when needed.

  2. Keep it simple: when Graphics objects need to accumulate more than a couple of transformations, the order in which Matrices are stacked can cause misunderstandings. It's simpler (and less prone to generate weird outcomes) to calculate some generic transformations (translate and scale, mostly) beforehand, then let GDI+ render already pre-cooked objects and shapes.

    Here, only Matrix.RotateAt and Matrix.Multiply are used.

    Some notes about Matrix transformations here: Flip the GraphicsPath

  3. Use the right tools: for example, a Panel used as canvas is not exactly the best choice. This Control is not double-buffered; this feature can be enabled, but the Panel class is not meant for drawing, while a PictureBox (or a non-System flat Label) supports it on its own.

    Some more notes here: How to apply a fade transition effect to Images

The sample code shows 4 zoom methods, plus generates rotation transformations (which work side-by-side, don't accumulate).

The Zoom modes are selected using an enumerator (private enum ZoomMode):

Zoom modes:

  • ImageLocation: Image scaling is performed in-place, keeping the current Location on the canvas in a fixed position.
  • CenterCanvas: while the Image is scaled, it remains centered on the Canvas.
  • CenterMouse: the Image is scaled and translated to center itself on the current Mouse location on the Canvas.
  • MouseOffset: the Image is scaled and translated to maintain a relative position determined by the initial location of the Mouse pointer on the Image itself.

You can notice that the code simplifies all the calculations, applying translations exclusively relative to the Rectangle that defines the current Image bounds and only in relation to the Location of this shape.

The Rectangle is only scaled when the calculation needs to preemptively determine what the Image size will be after the Mouse Wheel has generated the next Zoom factor.

Visual sample of the implemented functionalities:

GDI+ Zoom and Rotations Samples

Sample code:

  • canvas is the Custom Control, derived from PictureBox (you can find its definition at the bottom). This control is added to the Form in code, here. Modify as needed.
  • trkRotationAngle is the TrackBar used to define the current rotation of the Image. Add this control to the Form in the designer.
  • radZoom_CheckedChanged is the event handler of all the RadioButtons used to set the current Zoom Mode. The value these Controls set is assigned in their Tag property. Add these controls to the Form in the designer.


using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Windows.Forms;

public partial class frmZoomPaint : Form
{
private float rotationAngle = 0.0f;
private float zoomFactor = 1.0f;
private float zoomStep = .05f;

private RectangleF imageRect = RectangleF.Empty;
private PointF imageLocation = PointF.Empty;
private PointF mouseLocation = PointF.Empty;

private Bitmap drawingImage = null;
private PictureBoxEx canvas = null;
private ZoomMode zoomMode = ZoomMode.ImageLocation;

private enum ZoomMode
{
ImageLocation,
CenterCanvas,
CenterMouse,
MouseOffset
}

public frmZoomPaint()
{
InitializeComponent();
string imagePath = [Path of the Image];
drawingImage = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(imagePath)));
imageRect = new RectangleF(Point.Empty, drawingImage.Size);

canvas = new PictureBoxEx(new Size(555, 300));
canvas.Location = new Point(10, 10);
canvas.MouseWheel += canvas_MouseWheel;
canvas.MouseMove += canvas_MouseMove;
canvas.MouseDown += canvas_MouseDown;
canvas.MouseUp += canvas_MouseUp;
canvas.Paint += canvas_Paint;
Controls.Add(canvas);
}

private void canvas_MouseWheel(object sender, MouseEventArgs e)
{
mouseLocation = e.Location;
float zoomCurrent = zoomFactor;
zoomFactor += e.Delta > 0 ? zoomStep : -zoomStep;
if (zoomFactor < .10f) zoomStep = .01f;
if (zoomFactor >= .10f) zoomStep = .05f;
if (zoomFactor < .0f) zoomFactor = zoomStep;

switch (zoomMode) {
case ZoomMode.CenterCanvas:
imageRect = CenterScaledRectangleOnCanvas(imageRect, canvas.ClientRectangle);
break;
case ZoomMode.CenterMouse:
imageRect = CenterScaledRectangleOnMousePosition(imageRect, e.Location);
break;
case ZoomMode.MouseOffset:
imageRect = OffsetScaledRectangleOnMousePosition(imageRect, zoomCurrent, e.Location);
break;
default:
break;
}
canvas.Invalidate();
}

private void canvas_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
mouseLocation = e.Location;
imageLocation = imageRect.Location;
canvas.Cursor = Cursors.NoMove2D;
}

private void canvas_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
imageRect.Location =
new PointF(imageLocation.X + (e.Location.X - mouseLocation.X),
imageLocation.Y + (e.Location.Y - mouseLocation.Y));
canvas.Invalidate();
}

private void canvas_MouseUp(object sender, MouseEventArgs e) =>
canvas.Cursor = Cursors.Default;

private void canvas_Paint(object sender, PaintEventArgs e)
{
var drawingRect = GetDrawingImageRect(imageRect);

using (var mxRotation = new Matrix())
using (var mxTransform = new Matrix()) {

e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
e.Graphics.PixelOffsetMode = PixelOffsetMode.Half;

mxRotation.RotateAt(rotationAngle, GetDrawingImageCenterPoint(drawingRect));
mxTransform.Multiply(mxRotation);

e.Graphics.Transform = mxTransform;
e.Graphics.DrawImage(drawingImage, drawingRect);
}
}

private void trkRotationAngle_ValueChanged(object sender, EventArgs e)
{
rotationAngle = trkAngle.Value;
canvas.Invalidate();
canvas.Focus();
}

private void radZoom_CheckedChanged(object sender, EventArgs e)
{
var rad = sender as RadioButton;
if (rad.Checked) {
zoomMode = (ZoomMode)int.Parse(rad.Tag.ToString());
}
canvas.Focus();
}

#region Drawing Methods

public RectangleF GetScaledRect(RectangleF rect, float scaleFactor) =>
new RectangleF(rect.Location,
new SizeF(rect.Width * scaleFactor, rect.Height * scaleFactor));

public RectangleF GetDrawingImageRect(RectangleF rect) =>
GetScaledRect(rect, zoomFactor);

public PointF GetDrawingImageCenterPoint(RectangleF rect) =>
new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);

public RectangleF CenterScaledRectangleOnCanvas(RectangleF rect, RectangleF canvas)
{
var scaled = GetScaledRect(rect, zoomFactor);
rect.Location = new PointF((canvas.Width - scaled.Width) / 2,
(canvas.Height - scaled.Height) / 2);
return rect;
}

public RectangleF CenterScaledRectangleOnMousePosition(RectangleF rect, PointF mousePosition)
{
var scaled = GetScaledRect(rect, zoomFactor);
rect.Location = new PointF(mousePosition.X - (scaled.Width / 2),
mousePosition.Y - (scaled.Height / 2));
return rect;
}

public RectangleF OffsetScaledRectangleOnMousePosition(RectangleF rect, float currentZoom, PointF mousePosition)
{
var currentRect = GetScaledRect(imageRect, currentZoom);
if (!currentRect.Contains(mousePosition)) return rect;

float scaleRatio = currentRect.Width / GetScaledRect(rect, zoomFactor).Width;

PointF mouseOffset = new PointF(mousePosition.X - rect.X, mousePosition.Y - rect.Y);
PointF scaledOffset = new PointF(mouseOffset.X / scaleRatio, mouseOffset.Y / scaleRatio);
PointF position = new PointF(rect.X - (scaledOffset.X - mouseOffset.X),
rect.Y - (scaledOffset.Y - mouseOffset.Y));
rect.Location = position;
return rect;
}

#endregion
}

The simple PictureBoxEx custom control (modify and extend as needed):

This PictureBox is selectable, so it can be focused, with a Mouse click

using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

[DesignerCategory("Code")]
public class PictureBoxEx : PictureBox
{
public PictureBoxEx() : this (new Size(200, 200)){ }
public PictureBoxEx(Size size) {
SetStyle(ControlStyles.Selectable | ControlStyles.UserMouse, true);
BorderStyle = BorderStyle.FixedSingle;
Size = size;
}
}

How to correct the mouse down position after zooming?

Based on the referred problem, to scale and offset the view to the mouse wheel position:

private float zoom = 1f;
private PointF wheelPoint;

private void picturebox_MouseWheel(object sender, MouseEventArgs e)
{
zoom += e.Delta < 0 ? -.1f : .1f;
zoom = Math.Max(.1f, Math.Min(10, zoom));
wheelPoint = new PointF(e.X * (zoom - 1f), e.Y * (zoom - 1f));
picturebox.Invalidate();
}

Offset and scale before drawing your shapes:

private void picturebox_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;

g.TranslateTransform(-wheelPoint.X, -wheelPoint.Y);
g.ScaleTransform(zoom, zoom);

// Draw....
}

SO73133298


If you also use the mouse inputs to draw your shapes, then you also need to take the offset into account to get the right point.

Revising the first example to mainly add the ScalePoint method to do the required calculations.

private float _zoom = 1f;
private PointF _wheelPoint;
private readonly SizeF _recSize = new Size(60, 60);
private List<RectangleF> _rects = new List<RectangleF>();

private PointF ScalePoint(PointF p) =>
new PointF(
(p.X + _wheelPoint.X) / _zoom - (_recSize.Width / 2),
(p.Y + _wheelPoint.Y) / _zoom - (_recSize.Height / 2));

private void picturebox_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
_rects.Add(new RectangleF(ScalePoint(e.Location), _recSize));
picturebox.Invalidate();
}
}

private void picturebox_MouseWheel(object sender, MouseEventArgs e)
{
_zoom += e.Delta < 0 ? -.1f : .1f;
_zoom = Math.Max(.1f, Math.Min(10, _zoom));
_wheelPoint = new PointF(e.X * (_zoom - 1f), e.Y * (_zoom - 1f));
picturebox.Invalidate();
}

private void picturebox_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;

g.TranslateTransform(-_wheelPoint.X, -_wheelPoint.Y);
g.ScaleTransform(_zoom, _zoom);
g.SmoothingMode = SmoothingMode.AntiAlias;

_rects.ForEach(r => g.FillEllipse(Brushes.Black, r));
}

SO73133298B

Downscaling a Rectangle to Display as it would look in the Large Original Image

I'll use the following methods:

  • TranslatePictureBoxSelectedRectangleToImage
    Translates a selected rectangle on the picture box to coordinates on the image.

  • TranslateImageSelectedRectangleToPictureBox
    Translates a selected rectangle on the image box to coordinates on the picture box.

  • ScaleRectangle
    Scales a rectangle by given scale factor.

TranslatePictureBoxSelectedRectangleToImage

public RectangleF TranslatePictureBoxSelectedRectangleToImage(PictureBox p, 
RectangleF pictureBoxSelectedRectangle)
{
var method = typeof(PictureBox).GetMethod("ImageRectangleFromSizeMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var imageRect = (Rectangle)method.Invoke(p, new object[] { p.SizeMode });
if (p.Image == null)
return pictureBoxSelectedRectangle;
var cx = (float)p.Image.Width / (float)imageRect.Width;
var cy = (float)p.Image.Height / (float)imageRect.Height;
var r2 = pictureBoxSelectedRectangle;
r2.Offset(-imageRect.X, -imageRect.Y);
return new RectangleF(r2.X * cx, r2.Y * cy, r2.Width * cx, r2.Height * cy);
}

TranslateImageSelectedRectangleToPictureBox

public RectangleF TranslateImageSelectedRectangleToPictureBox(PictureBox p, 
RectangleF imageSelectedRectangle)
{
var method = typeof(PictureBox).GetMethod("ImageRectangleFromSizeMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var imageRect = (Rectangle)method.Invoke(p, new object[] { p.SizeMode });
if (p.Image == null)
return imageSelectedRectangle;
var cx = (float)p.Image.Width / (float)imageRect.Width;
var cy = (float)p.Image.Height / (float)imageRect.Height;
var r2 = new RectangleF(imageSelectedRectangle.X / cx, imageSelectedRectangle.Y / cy,
imageSelectedRectangle.Width / cx, imageSelectedRectangle.Height / cy);
r2.Offset(imageRect.X, imageRect.Y);
return r2;
}

ScaleRectangle

public RectangleF ScaleRectangle(RectangleF r, float c)
{
return new RectangleF(r.X * c, r.Y * c, r.Width * c, r.Height * c);
}

Example

Using above methods with following assumptions:

  • You have image1 in original size and image2 which is a programmatically resized version of image1 with zoom factor c. (It means c = (float)image2.Width/(float)image1.Width.)
  • You are showing image2 in picture box in zoom mode.

Question 1 - Having r1 as selected rectangle on picureBox1, what is the rectangle size and location on image1?

The first method shows how you can convert r1 on picture box, to a rectangle on image2. To convert it to the rectangle on image1, since you know the zoom factor which you used to create image2 from image1, it's enough to apply the same zoom factor on result of first method:

//First convert rectangle of pictureBox1 to rectangle of image2
var r2 = TranslatePictureBoxSelectedRectangleToImage(pictureBox1, r1);

//Then convert rectangle of image2 to rectangle of image1
var result = ScaleRectangle(r2, 1f/c);

Question 2 - Having r1 as selected rectangle on image1, what is the rectangle size and location on pictureBox1?

The second method shows how you can convert r1 on image2, to a rectangle on pictureBox1. To convert from a rectangle on image1, since you know the zoom factor which you used to create image2 from image1, it's enough to apply the same zoom factor on r1 to get the rectangle on image2, then use the second method:

//First convert rectangle of the image1 to rectangle of image2
var r2 = ScaleRectangle(r1, c);

//Then convert rectangle of image2 to rectangle of pictureBox1
var result = TranslateImageSelectedRectangleToPictureBox(pictureBox1, r2);

Crop correct part of image while the PictureBox is in 'zoom' mode

You need to calculate the points using the stretch factor and maybe also the offset.

For Zoom there is only one factor as aspect ratio is always the same for Image and PictureBox, but there usually is an offset; for Stretch you need no offset but two factors.

Here is an example that goes all the way using two PictureBoxes two show a zoomed version and the cropped bitmap. It makes use of an all-purpose function ImageArea that determines size and offset.

Sample Image

Two class level variables:

Point pDown = Point.Empty;
Rectangle rect = Rectangle.Empty;

Three mouse events:

private void PictureBox1_MouseDown(object sender, MouseEventArgs e)
{
pDown = e.Location;
pictureBox1.Refresh();
}

private void PictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (!e.Button.HasFlag(MouseButtons.Left)) return;

rect = new Rectangle(pDown, new Size(e.X - pDown.X, e.Y - pDown.Y));
using (Graphics g = pictureBox1.CreateGraphics())
{
pictureBox1.Refresh();
g.DrawRectangle(Pens.Orange, rect);
}
}

private void PictureBox1_MouseUp(object sender, MouseEventArgs e)
{
Rectangle iR = ImageArea(pictureBox2);
rect = new Rectangle(pDown.X - iR.X, pDown.Y - iR.Y,
e.X - pDown.X, e.Y - pDown.Y);
Rectangle rectSrc = Scaled(rect, pictureBox2, true);
Rectangle rectDest = new Rectangle(Point.Empty, rectSrc.Size);

Bitmap bmp = new Bitmap(rectDest.Width, rectDest.Height);
using (Graphics g = Graphics.FromImage(bmp))
{
g.DrawImage(pictureBox2.Image, rectDest, rectSrc, GraphicsUnit.Pixel);
}
pictureBox2.Image = bmp;
}

Here is a useful function that returns the area of the actual image inside a picturebox for any sizemode..:

Rectangle ImageArea(PictureBox pbox)
{
Size si = pbox.Image.Size;
Size sp = pbox.ClientSize;

if (pbox.SizeMode == PictureBoxSizeMode.StretchImage)
return pbox.ClientRectangle;
if (pbox.SizeMode == PictureBoxSizeMode.Normal ||
pbox.SizeMode == PictureBoxSizeMode.AutoSize)
return new Rectangle(Point.Empty, si);
if (pbox.SizeMode == PictureBoxSizeMode.CenterImage)
return new Rectangle(new Point((sp.Width - si.Width) / 2,
(sp.Height - si.Height) / 2), si);

// PictureBoxSizeMode.Zoom
float ri = 1f * si.Width / si.Height;
float rp = 1f * sp.Width / sp.Height;
if (rp > ri)
{
int width = si.Width * sp.Height / si.Height;
int left = (sp.Width - width) / 2;
return new Rectangle(left, 0, width, sp.Height);
}
else
{
int height = si.Height * sp.Width / si.Width;
int top = (sp.Height - height) / 2;
return new Rectangle(0, top, sp.Width, height);
}
}

We only need the offset to determine the rectangle unscaled. We also need to scale it:

Rectangle Scaled(Rectangle rect, PictureBox pbox, bool scale)
{
float factor = GetFactor(pbox);
if (!scale) factor = 1f / factor;
return Rectangle.Round(new RectangleF(rect.X * factor, rect.Y * factor,
rect.Width * factor, rect.Height * factor));
}

For this need to know the scaling factor, which depends on the aspect ratio:

float GetFactor(PictureBox pBox)
{
if (pBox.Image == null) return 0;
Size si = pBox.Image.Size;
Size sp = pBox.ClientSize;
float ri = 1f * si.Width / si.Height;
float rp = 1f * sp.Width / sp.Height;
float factor = 1f * pBox.Image.Width / pBox.ClientSize.Width;
if (rp > ri) factor = 1f * pBox.Image.Height / pBox.ClientSize.Height;
return factor;
}

This solution will also work if the PictureBox is zoomed in or out by placing it inside a AutoScrolling Panel and changing the Pbox.Size.



Related Topics



Leave a reply



Submit