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:
And this is the result:
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:
- Zoom out the rectangle using the vertical and horizontal scales
- 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.
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.
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 GraphicsPathUse 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:
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 theirTag
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....
}
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));
}
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 andimage2
which is a programmatically resized version ofimage1
with zoom factorc
. (It meansc = (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.
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
Refresh Datagridview When Updating Data Source
How to Tell JSON.Net Globally to Apply the Stringenumconverter to All Enums
How to Read User Input from the Console
Read Fixed Width Record from Text File
Writing Recursive Cte Using Entity Framework Fluent Syntax or Inline Syntax
Variable Declaration in a C# Switch Statement
How to Iterate Over the Properties of an Anonymous Object in C#
Use Linq to Group a Sequence of Numbers with No Gaps
Calling a Static Method on a Generic Type Parameter
Setting Generic Type at Runtime
Namespace' But Is Used Like a 'Type'
How to Update a Claim in ASP.NET Identity
Anonymous Method in Invoke Call
Redirecting Unauthorized Controller in ASP.NET MVC
Specified Argument Was Out of the Range of Valid Values. Parameter Name: Site