How to retrieve zoom factor of a WinForms PictureBox? - c#

I need the precise position of my mouse pointer over a PictureBox.
I use the MouseMove event of the PictureBox.
On this PictureBox, I use the "zoom" property to show an image.
What is the correct way for getting the position of the mouse on the original (unzoomed) image?
Is there a way to find the scale factor and use it?
I think need to use imageOriginalSize/imageShowedSize to retrieve this scale factor.
I use this function:
float scaleFactorX = mypic.ClientSize.Width / mypic.Image.Size.Width;
float scaleFactorY = mypic.ClientSize.Height / mypic.Image.Size.Height;
Is possible to use this value to get the correct position of the cursor over the image?

I had to solve this same problem today. I wanted it to work for images of any width:height ratio.
Here's my method to find the point 'unscaled_p' on the original full-sized image.
Point p = pictureBox1.PointToClient(Cursor.Position);
Point unscaled_p = new Point();
// image and container dimensions
int w_i = pictureBox1.Image.Width;
int h_i = pictureBox1.Image.Height;
int w_c = pictureBox1.Width;
int h_c = pictureBox1.Height;
The first trick is to determine if the image is a horizontally or vertically larger relative to the container, so you'll know which image dimension fills the container completely.
float imageRatio = w_i / (float)h_i; // image W:H ratio
float containerRatio = w_c / (float)h_c; // container W:H ratio
if (imageRatio >= containerRatio)
{
// horizontal image
float scaleFactor = w_c / (float)w_i;
float scaledHeight = h_i * scaleFactor;
// calculate gap between top of container and top of image
float filler = Math.Abs(h_c - scaledHeight) / 2;
unscaled_p.X = (int)(p.X / scaleFactor);
unscaled_p.Y = (int)((p.Y - filler) / scaleFactor);
}
else
{
// vertical image
float scaleFactor = h_c / (float)h_i;
float scaledWidth = w_i * scaleFactor;
float filler = Math.Abs(w_c - scaledWidth) / 2;
unscaled_p.X = (int)((p.X - filler) / scaleFactor);
unscaled_p.Y = (int)(p.Y / scaleFactor);
}
return unscaled_p;
Note that because Zoom centers the image, the 'filler' length has to be factored in to determine the dimension that is not filled by the image. The result, 'unscaled_p', is the point on the unscaled image that 'p' correlates to.
Hope that helps!

If I have understood you correctly I believe you would want to do something of this nature:
Assumption: the PictureBox fits to the image width/height, there is no space between the border of the PictureBox and the actual image.
ratioX = e.X / pictureBox.ClientSize.Width;
ratioY = e.Y / pictureBox.ClientSize.Height;
imageX = image.Width * ratioX;
imageY = image.Height * ratioY;
this should give you the points ot the pixel in the original image.

Here is a simple function to solve this:
private Point RemapCursorPosOnZoomedImage(PictureBox pictureBox, int x, int y, out bool isInImage)
{
// original size of image in pixel
float imgSizeX = pictureBox.Image.Width;
float imgSizeY = pictureBox.Image.Height;
// current size of picturebox (without border)
float cSizeX = pictureBox.ClientSize.Width;
float cSizeY = pictureBox.ClientSize.Height;
// calculate scale factor for both sides
float facX = (cSizeX / imgSizeX);
float facY = (cSizeY / imgSizeY);
// use smaller one to fit picturebox zoom layout
float factor = Math.Min(facX, facY);
// calculate current size of the displayed image
float rSizeX = imgSizeX * factor;
float rSizeY = imgSizeY * factor;
// calculate offsets because image is centered
float startPosX = (cSizeX - rSizeX) / 2;
float startPosY = (cSizeY - rSizeY) / 2;
float endPosX = startPosX + rSizeX;
float endPosY = startPosY + rSizeY;
// check if cursor hovers image
isInImage = true;
if (x < startPosX || x > endPosX) isInImage = false;
if (y < startPosY || y > endPosY) isInImage = false;
// remap cursor position
float cPosX = ((float)x - startPosX) / factor;
float cPosY = ((float)y - startPosY) / factor;
// create new point with mapped coords
return new Point((int)cPosX, (int)cPosY);
}

Related

How do you translate chart coordinates to device pixels in SkiaSharp?

I'm attempting to write a matrix transform to convert chart points to device pixels in SkiaSharp. I have it functional as long as I use 0,0 as my minimum chart coordinates but if I need to to step up from a negative number, it causes the drawing to shift left and down. That is to say that the X Axis is shifted to the left off the window and the Y Axis is shift down off the window.
This is intended to be a typical line chart (minimum chart point at the lower left while minimum device point at the upper left). I have accounted for that already in the transform.
While stepping through code I can see that the coordinates returned from the Matrix are not what I expect them to be, so I believe the issue to be with my transform but I haven't been able to pinpoint it.
UPDATE: After further examination, I believe I was mistaken, it is not shifted, it's just not scaling properly to the max end of the screen. There is a bigger margin at the top and right side of the chart than there should be, but the bottom and left side are fine. I've been undable to determine why the scaling doesn't fill the canvas.
Below are my matrix methods:
private SKMatrix ChartToDeviceMatrix, DeviceToChartMatrix;
private void ConfigureTransforms(SKPoint ChartMin,
SKPoint ChartMax, SKPoint DeviceMin, SKPoint DeviceMax)
{
this.ChartToDeviceMatrix = SKMatrix.MakeIdentity();
float xScale = (DeviceMax.X - DeviceMin.X) / (ChartMax.X - ChartMin.X);
float yScale = (DeviceMin.Y - DeviceMax.Y) / (ChartMax.Y - ChartMin.Y);
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, DeviceMin.X, DeviceMax.Y);
this.ChartToDeviceMatrix.TryInvert(out this.DeviceToChartMatrix);
}
// Transform a point from chart to device coordinates.
private SKPoint ChartToDevice(SKPoint point)
{
return this.ChartToDeviceMatrix.MapPoint(point);
}
The code invoking this is:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
float strokeWidth = 1;
float margin = 10;
// SKPaint definitions omitted for brevity.
var ChartMin = new SKPoint(-10, -1); // Works fine if I change this to 0,0
var ChartMax = new SKPoint(110, 11);
var DeviceMin = new SKPoint(margin, margin);
var DeviceMax = new SKPoint(info.Width - margin, info.Height - margin);
const float stepX = 10;
const float stepY = 1;
const float tickX = 0.5;
const float tickY = 0.075F;
// Prepare the transformation matrices.
this.ConfigureTransforms(ChartMin, ChartMax, DeviceMin, DeviceMax);
// Draw the X axis.
var lineStart = new SKPoint(ChartMin.X, 0);
var lineEnd = new SKPoint(ChartMax.X, 0);
canvas.DrawLine(this.ChartToDevice(lineStart), this.ChartToDevice(lineEnd), axisPaint);
// X Axis Tick Marks
for (float x = stepX; x <= ChartMax.X - stepX; x += stepX)
{
var tickMin = new SKPoint(x, -tickY);
var tickMax = new SKPoint(x, tickY);
canvas.DrawLine(this.ChartToDevice(tickMin), this.ChartToDevice(tickMax), axisPaint);
}
// Draw the Y axis.
// The inversion of above, basically the same.
I was able to discover my own problem with enough time. I wasn't calculating the offset correct.
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, DeviceMin.X, DeviceMax.X);
Should have been:
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, -ChartMin.X * xScale + DeviceMin.Y, -ChartMin.Y * yScale + DeviceMax.Y);
Final Matrix method was:
private SKMatrix ChartToDeviceMatrix, DeviceToChartMatrix;
private void ConfigureTransforms(SKPoint ChartMin, SKPoint ChartMax, SKPoint DeviceMin, SKPoint DeviceMax)
{
this.ChartToDeviceMatrix = SKMatrix.MakeIdentity();
float xScale = (DeviceMax.X - DeviceMin.X) / (ChartMax.X - ChartMin.X);
float yScale = (DeviceMin.Y - DeviceMax.Y) / (ChartMax.Y - ChartMin.Y);
float xOffset = -ChartMin.X * xScale + DeviceMin.X;
float yOffset = -ChartMin.Y * yScale + DeviceMax.Y;
this.ChartToDeviceMatrix.SetScaleTranslate(xScale, yScale, xOffset, yOffset);
this.ChartToDeviceMatrix.TryInvert(out this.DeviceToChartMatrix);
}

Zooming graphics without scrolling

I have a user control and I'm using ScaleTransform() to implement zoom.
However, in order to keep the center content in the center after the zoom, it is also necessary to scroll. For example, if I zoom in (make things bigger), the X and Y origin should increase so that most of the content does not move down and to the right. (That is, as I zoom in, some of the content should disappear to the left and top.)
Has anyone worked out the calculations of how much to scroll in the X and Y direction in response to a zoom?
For example:
e.Graphics.ScaleTransform(2.0F, 2.0F);
e.Graphics.TranslateTransform(?, ?);
What would be my arguments to TranslateTransform() be so that the center part of the content remains at the center?
Note: I am not displaying an image. I am drawing the graphic content to the surface of my user control.
Or perhaps there's an even easier way?
This should work and I can't imagine any easier way; it assumes you have decided on the center of the zooming. I have chosen to draw centered on the panel:
float zoom = 1f;
private void drawPanel1_Paint(object sender, PaintEventArgs e)
{
Point c = new Point(drawPanel1.ClientSize.Width / 2, drawPanel1.ClientSize.Height / 2);
// a blue sanity check for testing
e.Graphics.FillEllipse(Brushes.DodgerBlue, c.X - 3, c.Y - 3, 6, 6);
// the offsets you were looking for:
float ox = c.X * ( zoom - 1f);
float oy = c.Y * ( zoom - 1f);
// first move and then scale
e.Graphics.TranslateTransform(-ox, -oy);
e.Graphics.ScaleTransform(zoom, zoom);
// now we can draw centered around our point c
Size sz = new Size(300, 400);
int count = 10;
int wx = sz.Width / count;
int wy = sz.Height / count;
for (int i = 0; i < count; i++)
{
Rectangle r = new Rectangle(c.X - i * wx / 2 , c.Y - i * wy / 2, i * wx, i * wy );
e.Graphics.DrawRectangle(Pens.Red, r );
}
}
Note the order of moving and scaling!
I guess you are using some differet interface, but in my case, that's what got the job done (for leaving the mouse in it's original location on the draw after the mouse wheel event):
private void DrawPb_MouseWheel(object sender, MouseEventArgs e)
{
// e contains current mouse location and the wheel direction
int wheelDirection = e.Delta / Math.Abs(e.Delta); // is 'in' or 'out' (1 or -1).
double factor = Math.Exp(wheelDirection * Constants.ZoomFactor); // divide or multiply
double newX = e.X - e.X / factor; // what used to be x is now newX
double newY = e.Y - e.Y / factor; // same for y
Point offset = new Point((int)(-newX), (int)(-newY)); // the offset of the old point to it's new location
Graph.AddOffset(offset); // apply offset
}

Silverlight Rotate & Scale a bitmap image to fit within rectangle without cropping

I need to rotate a WriteableBitmap and scale it down or up before it gets cropped.
My current code will rotate but will crop the edges if the height is larger then the width.
I assume I need to scale?
public WriteableBitmap Rotate(WriteableBitmap Source, double Angle)
{
RotateTransform rt = new RotateTransform();
rt.Angle = Angle;
TransformGroup transform = new TransformGroup();
transform.Children.Add(rt);
Image tempImage2 = new Image();
WriteableBitmap wb;
rt.CenterX = Source.PixelWidth / 2;
rt.CenterY = Source.PixelHeight / 2;
tempImage2.Width = Source.PixelWidth;
tempImage2.Height = Source.PixelHeight;
wb = new WriteableBitmap((int)(Source.PixelWidth), Source.PixelHeight);
tempImage2.Source = Source;
tempImage2.UpdateLayout();
wb.Render(tempImage2, transform);
wb.Invalidate();
return wb;
}
How do I scale down the image so it will not be cropped? Or is there another way?
You need to calculate the scaling based on the rotation of the corners relative to the centre.
If the image is a square only one corner is needed, but for a rectangle you need to check 2 corners in order to see if a vertical or horizontal edge is overlapped. This check is a linear comparison of how much the rectangle's height and width are exceeded.
Click here for the working testbed app created for this answer (image below): (apologies, all my website content was lost thanks to a non-awesome hosting company)
double CalculateConstraintScale(double rotation, int pixelWidth, int pixelHeight)
The pseudo-code is as follows (actual C# code at the end):
Convert rotation angle into Radians
Calculate the "radius" from the rectangle centre to a corner
Convert BR corner position to polar coordinates
Convert BL corner position to polar coordinates
Apply the rotation to both polar coordinates
Convert the new positions back to Cartesian coordinates (ABS value)
Find the largest of the 2 horizontal positions
Find the largest of the 2 vertical positions
Calculate the delta change for horizontal size
Calculate the delta change for vertical size
Return width/2 / x if horizontal change is greater
Return height/2 / y if vertical change is greater
The result is a multiplier that will scale the image down to fit the original rectangle regardless of rotation.
*Note: While it is possible to do much of the maths using matrix operations, there are not enough calculations to warrant that. I also thought it would make a better example from first-principles.
C# Code:
/// <summary>
/// Calculate the scaling required to fit a rectangle into a rotation of that same rectangle
/// </summary>
/// <param name="rotation">Rotation in degrees</param>
/// <param name="pixelWidth">Width in pixels</param>
/// <param name="pixelHeight">Height in pixels</param>
/// <returns>A scaling value between 1 and 0</returns>
/// <remarks>Released to the public domain 2011 - David Johnston (HiTech Magic Ltd)</remarks>
private double CalculateConstraintScale(double rotation, int pixelWidth, int pixelHeight)
{
// Convert angle to radians for the math lib
double rotationRadians = rotation * PiDiv180;
// Centre is half the width and height
double width = pixelWidth / 2.0;
double height = pixelHeight / 2.0;
double radius = Math.Sqrt(width * width + height * height);
// Convert BR corner into polar coordinates
double angle = Math.Atan(height / width);
// Now create the matching BL corner in polar coordinates
double angle2 = Math.Atan(height / -width);
// Apply the rotation to the points
angle += rotationRadians;
angle2 += rotationRadians;
// Convert back to rectangular coordinate
double x = Math.Abs(radius * Math.Cos(angle));
double y = Math.Abs(radius * Math.Sin(angle));
double x2 = Math.Abs(radius * Math.Cos(angle2));
double y2 = Math.Abs(radius * Math.Sin(angle2));
// Find the largest extents in X & Y
x = Math.Max(x, x2);
y = Math.Max(y, y2);
// Find the largest change (pixel, not ratio)
double deltaX = x - width;
double deltaY = y - height;
// Return the ratio that will bring the largest change into the region
return (deltaX > deltaY) ? width / x : height / y;
}
Example of use:
private WriteableBitmap GenerateConstrainedBitmap(BitmapImage sourceImage, int pixelWidth, int pixelHeight, double rotation)
{
double scale = CalculateConstraintScale(rotation, pixelWidth, pixelHeight);
// Create a transform to render the image rotated and scaled
var transform = new TransformGroup();
var rt = new RotateTransform()
{
Angle = rotation,
CenterX = (pixelWidth / 2.0),
CenterY = (pixelHeight / 2.0)
};
transform.Children.Add(rt);
var st = new ScaleTransform()
{
ScaleX = scale,
ScaleY = scale,
CenterX = (pixelWidth / 2.0),
CenterY = (pixelHeight / 2.0)
};
transform.Children.Add(st);
// Resize to specified target size
var tempImage = new Image()
{
Stretch = Stretch.Fill,
Width = pixelWidth,
Height = pixelHeight,
Source = sourceImage,
};
tempImage.UpdateLayout();
// Render to a writeable bitmap
var writeableBitmap = new WriteableBitmap(pixelWidth, pixelHeight);
writeableBitmap.Render(tempImage, transform);
writeableBitmap.Invalidate();
return writeableBitmap;
}
I released a Test-bed of the code on my website so you can try it for real - click to try it (apologies, all my website content was lost thanks to a non-awesome hosting company)

trying to render an equirectangular panorama

I have an equirectangular panorama source image which is 360 degrees of longitude and 120 degrees of latitude.
I want to write a function which can render this, given width and height of the viewport and a rotation in longitude. I want to can my output image so that it's the full 120 degrees in height.
has anyone got any pointers? I can't get my head around the maths on how to transform from target coordinates back to source.
thanks
slip
Here is my code so far:- (create a c# 2.0 console app, add a ref to system.drawing)
static void Main(string[] args)
{
Bitmap src = new Bitmap(#"C:\Users\jon\slippyr4\pt\grid2.jpg");
// constant stuff
double view_width_angle = d2r(150);
double view_height_angle = d2r(120);
double rads_per_pixel = 2.0 * Math.PI / src.Width;
// scale everything off the height
int output_image_height = src.Width;
// compute radius (from chord trig - my output image forms a chord of a circle with angle view_height_angle)
double radius = output_image_height / (2.0 * Math.Sin(view_height_angle / 2.0));
// work out the image width with that radius.
int output_image_width = (int)(radius * 2.0 * Math.Sin(view_width_angle / 2.0));
// source centres for later
int source_centre_x = src.Width / 2;
int source_centre_y = src.Height / 2;
// work out adjacent length
double adj = radius * Math.Cos(view_width_angle / 2.0);
// create output bmp
Bitmap dst = new Bitmap(output_image_width, output_image_height);
// x & y are output pixels offset from output centre
for (int x = output_image_width / -2; x < output_image_width / 2; x++)
{
// map this x to an angle & then a pixel
double x_angle = Math.Atan(x / adj);
double src_x = (x_angle / rads_per_pixel) + source_centre_x;
// work out the hypotenuse of that triangle
double x_hyp = adj / Math.Cos(x_angle);
for (int y = output_image_height / -2; y < output_image_height / 2; y++)
{
// compute the y angle and then it's pixel
double y_angle = Math.Atan(y / x_hyp);
double src_y = (y_angle / rads_per_pixel) + source_centre_y;
Color c = Color.Magenta;
// this handles out of range source pixels. these will end up magenta in the target
if (src_x >= 0 && src_x < src.Width && src_y >= 0 && src_y < src.Height)
{
c = src.GetPixel((int)src_x, (int)src_y);
}
dst.SetPixel(x + (output_image_width / 2), y + (output_image_height / 2), c);
}
}
dst.Save(#"C:\Users\slippyr4\Desktop\pana.jpg");
}
static double d2r(double degrees)
{
return degrees * Math.PI / 180.0;
}
With this code, i get the results i expect when i set my target image width to 120 degrees. I see the right curvature of horizontal lines etc, as below, and when i try it with a real-life equirectangular panorama, it looks like commercial viewers render.
But, when i make the output image wider, it all goes wrong. You start to see the invalid pixels in a parabola top and bottom at the centre, as shown here with the image 150 degrees wide by 120 degrees high:-
What commericial viewers seem to do is sort of zoom in - so the in the centre, the image is 120 degrees high and therefore at the sides, more is clipped. and therfore, there is no magenta (ie, no invalid source pixels).
But i can't get my head around how to do that in the maths.
This isn't homework, it's a hobby project. hence why i am lacking the understanding of what is going on!. Also, please forgive the severe inefficeincy of the code, i will optimise it when i have it working propertly.
thanks again

Scaling an image using the mouse in a WinForms application?

I'm trying to use the position of the mouse to calculate the scaling factor for scaling an image. Basically, the further you get away from the center of the image, the bigger it gets; and the closer to the center you get, the smaller it gets. I have some code so far but it's acting really strange and I have absolutely no more ideas. First I'll let you know, one thing I was trying to do is average out 5 distances to get a more smooth resize animation. Here's my code:
private void pictureBoxScale_MouseMove(object sender, MouseEventArgs e)
{
if (rotateScaleMode && isDraggingToScale)
{
// For Scaling
int sourceWidth = pictureBox1.Image.Width;
int sourceHeight = pictureBox1.Image.Height;
float dCurrCent = 0; // distance between the current mouse pos and the center of the image
float dPrevCent = 0; // distance between the previous mouse pos and the center of the image
System.Drawing.Point imgCenter = new System.Drawing.Point();
imgCenter.X = pictureBox1.Location.X + (sourceWidth / 2);
imgCenter.Y = pictureBox1.Location.Y + (sourceHeight / 2);
// Calculating the distance between the current mouse location and the center of the image
dCurrCent = (float)Math.Sqrt(Math.Pow(e.X - imgCenter.X, 2) + Math.Pow(e.Y - imgCenter.Y, 2));
// Calculating the distance between the previous mouse location and the center of the image
dPrevCent = (float)Math.Sqrt(Math.Pow(prevMouseLoc.X - imgCenter.X, 2) + Math.Pow(prevMouseLoc.Y - imgCenter.Y, 2));
if (smoothScaleCount < 5)
{
dCurrCentSmooth[smoothScaleCount] = dCurrCent;
dPrevCentSmooth[smoothScaleCount] = dPrevCent;
}
if (smoothScaleCount == 4)
{
float currCentSum = 0;
float prevCentSum = 0;
for (int i = 0; i < 4; i++)
{
currCentSum += dCurrCentSmooth[i];
}
for (int i = 0; i < 4; i++)
{
prevCentSum += dPrevCentSmooth[i];
}
float scaleAvg = (currCentSum / 5) / (prevCentSum / 5);
int destWidth = (int)(sourceWidth * scaleAvg);
int destHeight = (int)(sourceHeight * scaleAvg);
// If statement is for limiting the size of the image
if (destWidth > (currentRotatedImage.Width / 2) && destWidth < (currentRotatedImage.Width * 3) && destHeight > (currentRotatedImage.Height / 2) && destWidth < (currentRotatedImage.Width * 3))
{
AForge.Imaging.Filters.ResizeBilinear resizeFilter = new AForge.Imaging.Filters.ResizeBilinear(destWidth, destHeight);
pictureBox1.Image = resizeFilter.Apply((Bitmap)currentRotatedImage);
pictureBox1.Size = pictureBox1.Image.Size;
pictureBox1.Refresh();
}
smoothScaleCount = -1;
}
prevMouseLoc = e.Location;
currentScaledImage = pictureBox1.Image;
smoothScaleCount++;
}
}
EDIT: Thanks to Ben Voigt and Ray everything works well now. The only thing wrong is that with the way I'm doing it the image doesn't keep it's ratio; but I'll fix that later. Here's what I have for those who want to know:
private void pictureBoxScale_MouseMove(object sender, MouseEventArgs e)
{
if (rotateScaleMode && isDraggingToScale)
{
// For Scaling
int sourceWidth = pictureBox1.Image.Width;
int sourceHeight = pictureBox1.Image.Height;
int scale = e.X + p0.X; //p0 is the location of the mouse when the button first came down
int destWidth = (int)(sourceWidth + (scale/10)); //I divide it by 10 to make it slower
int destHeight = (int)(sourceHeight + (scale/10));
if (destWidth > 20 && destWidth < 1000 && destHeight > 20 && destWidth < 1000)
{
AForge.Imaging.Filters.ResizeBilinear resizeFilter = new AForge.Imaging.Filters.ResizeBilinear(destWidth, destHeight);
pictureBox1.Image = resizeFilter.Apply((Bitmap)currentRotatedImage);
pictureBox1.Size = pictureBox1.Image.Size;
pictureBox1.Refresh();
}
currentScaledImage = pictureBox1.Image; // This is only so I can rotate the scaled image in another part of my program
}
}
You're scaling won't be smooth if you use the center of the image. Instead, use the initial mouse down point (call it p0). Also, rather than using the distance from that point to the current drag point (e), just take the difference along one axis (e.g. exp(e.Y - p0.Y)).
It looks to me (from the scaleAvg calculation) like you're rescaling the already-scaled image. This is a really bad idea because scaling is lossy and the errors will accumulate. Instead, keep a copy of the crisp original image and scale the original directly to the current size.
Also, I would suggest using a different norm, perhaps Manhattan distance, instead of the current Cartesian distance which is a two-norm.
If you do continue using the two-norm, consider getting rid of the Math.Pow calls. They are probably such a small part of the overall scaling complexity that it doesn't matter, but multiplying by itself should be much faster than Math.Pow for squaring a number.

Categories

Resources