I'm working on an image viewer in C# WPF that can zoom and crop images. I'd like to be able to make the two work together, but I don't know how to calculate that.
To zoom an image, it is done by modifying TranslateTransform and ScaleTransform X & Y values. Inspired by this answer https://stackoverflow.com/a/6782715/2923736
The crop function is done by calculating TopLeftX, TopRightY, BottomRightX, BottomLeftY that has been drawn on screen.
The size and aspect ratio calculation is done like so:
maxWidth = Math.Min(ScreenWidth, imageWidth);
maxHeight = Math.Min(ScreenHeight, imageHeight);
AspectRatio = Math.Min(maxWidth / imageWidth, maxHeight / imageHeight);
ImageWidth = imageWidth * AspectRatio;
ImageHeight = imageHeight * AspectRatio;
I use this code to calculate the cropped area, but don't know how to put TranslateTransform and ScaleTransform X & Y values into the formula.
internal static Int32Rect? GetCrop()
{
// Contains the dimensions and coordinates of cropped area
var cropArea = CropService.GetCroppedArea();
if (cropArea == null) { return null; }
int x, y, width, height;
x = Convert.ToInt32(cropArea.CroppedRectAbsolute.X / AspectRatio);
y = Convert.ToInt32(cropArea.CroppedRectAbsolute.Y / AspectRatio);
switch (Rotateint) // Degress the image has been rotated by
{
case 0:
case 180:
width = Convert.ToInt32(cropArea.CroppedRectAbsolute.Width / AspectRatio);
height = Convert.ToInt32(cropArea.CroppedRectAbsolute.Height / AspectRatio);
break;
default:
width = Convert.ToInt32(cropArea.CroppedRectAbsolute.Height / AspectRatio);
height = Convert.ToInt32(cropArea.CroppedRectAbsolute.Width / AspectRatio);
break;
}
return new Int32Rect(x, y, width, height);
}
Example of image being cropped, where it has been resized to fit the window and zoomed in by 20%.
I need to calculate that, so it can be saved on the user's hard disk.
Related
I'm trying to automatically create a photo collage with Win2D library. I've defined a couple of methods to do that:
ICanvasImage RotateImage(ICanvasImage photo, Size size, float degrees) returns a rotated image. I need to pass the image, its size and the degrees number. This method works for other things so I don't think it's that the problem.
ICanvasImage AddImage(ICanvasImage image, Size size, float x, float y, float degrees) it's the guilty. It draws the image on a CanvasRenderTarget with the specified position and size. It first call RotateImage to get the photo with the correct rotation and then draw it on the canvas.
At the end I use a method to save the content of the canvas as an image but it's not important, it works...
If I draw an horizontal image (no rotation => 0 degrees) I have no problem because the destination rectangle to pass to renderer.drawImage() is perfectly align with the photo. If I specified any angle the the image comes rotated correctly but the destination rectangle keeps horizontal an then the photo get cropped where it's out of the bounds of the rectangle.
This is my code:
//photo => photo to rotate
//size => size of the photo
//degrees => degrees to rotate the photo
ICanvasImage RotateImage(ICanvasImage photo, Size size, float degrees)
{
double height = Math.Sqrt(size.Width * size.Width + size.Height*size.Height);
//convert degreese to radians
float radians = (float)(degrees * Math.PI / 180d);
//get x,y where to place the rotated image
float y = (float)((height - size.Height) / 2.0f);
float x = (float)((height - size.Width) / 2.0f);
Vector2 endpoint = new Vector2((float)size.Width / 2, (float)size.Height / 2);
ICanvasImage image = new Transform2DEffect
{
Source = photo,
TransformMatrix = Matrix3x2.CreateRotation(radians, endpoint)
};
return image;
}
//size => final size that I want to have
//x,y => position of the photo
ICanvasImage AddImage(ICanvasImage image, Size size, float x, float y, float degrees)
{
//get the rotated image
ICanvasImage rotatedImage = RotateImage(image, size, degrees);
//start to draw in the canvas
using (var ds = renderer.CreateDrawingSession())
{
//destination rectangle with the specified position and size
Rect destRect = new Rect(x,y,size.Width,size.Height);
//source rectangle of the photo
Rect sourceRect = new Rect(0,0,rotatedImage.GetBounds(renderer).Width,rotatedImage.GetBounds(renderer).Height);
//draw the image on the canvas
ds.DrawImage(rotatedImage,destRect, sourceRect);
}
//here I call the method to save canvas' content as an image...
//you can simply try drawing directly on a canvasControl
}
I'm determining the rectangular area in an image and showing it to the user in a PictureBox.
Since the image can sometimes be very large, I'm using a PictureBox with its SizeMode set to Zoom.
I'm using the following code to translate the Rectangle (X, Y) coordinates:
public Point TranslateZoomMousePosition(Point coordinates)
{
// test to make sure our image is not null
if (pictureBox5.Image == null) return coordinates;
// Make sure our control width and height are not 0 and our
// image width and height are not 0
if (pictureBox5.Width == 0 || pictureBox5.Height == 0 || pictureBox5.Image.Width == 0 || pictureBox5.Image.Height == 0) return coordinates;
// This is the one that gets a little tricky. Essentially, need to check
// the aspect ratio of the image to the aspect ratio of the control
// to determine how it is being rendered
float imageAspect = (float)pictureBox5.Image.Width / pictureBox5.Image.Height;
float controlAspect = (float)pictureBox5.Width / pictureBox5.Height;
float newX = coordinates.X;
float newY = coordinates.Y;
if (imageAspect > controlAspect)
{
// This means that we are limited by width,
// meaning the image fills up the entire control from left to right
float ratioWidth = (float)pictureBox5.Image.Width / pictureBox5.Width;
newX *= ratioWidth;
float scale = (float)pictureBox5.Width / pictureBox5.Image.Width;
float displayHeight = scale * pictureBox5.Image.Height;
float diffHeight = pictureBox5.Height - displayHeight;
diffHeight /= 2;
newY -= diffHeight;
newY /= scale;
}
else
{
// This means that we are limited by height,
// meaning the image fills up the entire control from top to bottom
float ratioHeight = (float)pictureBox5.Image.Height / pictureBox5.Height;
newY *= ratioHeight;
float scale = (float)pictureBox5.Height / pictureBox5.Image.Height;
float displayWidth = scale * pictureBox5.Image.Width;
float diffWidth = pictureBox5.Width - displayWidth;
diffWidth /= 2;
newX -= diffWidth;
newX /= scale;
}
return new Point((int)newX, (int)newY);
}
Adding a frame control at the determined position:
pictureBox5.Controls.Clear();
var c = new FrameControl();
c.Size = new Size(myrect.Width, myrect.Height);
c.Location=TranslateZoomMousePosition(newPoint(myrect.Location.X,myrect.Location.Y));
pictureBox5.Controls.Add(c);
But the determined frame/rectangle location is not correct.
What am I i doing wrong?
Update:
I'm trying to translate a Rectangle on an image to a Frame Control on a PictureBox using similar code
public Rectangle GetRectangeOnPictureBox(PictureBox p, Rectangle selectionRect,Bitmap bit)
{
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;
int cx = bit.Width / imageRect.Width;
int cy = bit.Height / imageRect.Height;
Rectangle trsRectangle = new Rectangle(selectionRect.X * cx, selectionRect.Y * cy, selectionRect.Width * cx, selectionRect.Height * cy);
trsRectangle.Offset(imageRect.X, imageRect.Y);
return trsRectangle;
}
This produces invalid result.Please advice
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:
A specialized class that provides some helper tools to determine the scaling factor of a selection and translates the selection coordinates to the scaled Bitmap coordinates.
This version is for zoomed images only.
The ZoomFactor class provides these methods:
PointF TranslateZoomPosition(PointF Coordinates, SizeF ContainerSize, SizeF ImageSize):
returns the PointF translated coordinates of a Point location inside a Container to the Point location inside a Bitmap, zoomed in the container.
RectangleF TranslateZoomSelection(RectangleF Selection, SizeF ContainerSize, SizeF ImageSize):
returns a RectangleF representing a selection created inside a Container, translated to the Bitmap coordinates.
RectangleF TranslateSelectionToZoomedSel(RectangleF SelectionRect, SizeF ContainerSize, SizeF ImageSize):
returns a RectangleF representing a pre-selected area of the original Bitmap translated to the zoomed selection Image inside a Container.
PointF GetImageScaledOrigin(SizeF ContainerSize, SizeF ImageSize):
returns the PointF reference of the zoomed Image origin coordinates inside the Container.
SizeF GetImageScaledSize(SizeF ContainerSize, SizeF ImageSize):
returns the SizeF reference of the Image when scaled inside the Container.
Sample usage, showing how to crop a Bitmap using a selection Rectangle created inside a Container control. The TranslateZoomSelection method returns the Bitmap section corresponding to a selection area:
ZoomFactor zoomHelper = new ZoomFactor()
Bitmap originalBitmap;
RectangleF currentSelection = [Current Selection Rectangle];
RectangleF bitmapRect = zoomHelper.TranslateZoomSelection(currentSelection, [Container].Size, originalBitmap.Size);
var croppedBitmap = new Bitmap((int)bitmapRect.Width, (int)bitmapRect.Height, originalBitmap.PixelFormat))
using (var g = Graphics.FromImage(croppedBitmap))
{
g.DrawImage(originalBitmap, new Rectangle(Point.Empty, Size.Round(bitmapRect.Size)),
bitmapRect, GraphicsUnit.Pixel);
[Container].Image = croppedBitmap;
}
A Sample of the behaviour described above:
Note: In the example, the pre-selection of the image in Portrait inverts Width and Height
The ZoomFactor class:
public class ZoomFactor
{
public ZoomFactor() { }
public PointF TranslateZoomPosition(PointF coordinates, SizeF containerSize, SizeF imageSize)
{
PointF imageOrigin = TranslateCoordinatesOrigin(coordinates, containerSize, imageSize);
float scaleFactor = GetScaleFactor(containerSize, imageSize);
return new PointF(imageOrigin.X / scaleFactor, imageOrigin.Y / scaleFactor);
}
public RectangleF TranslateZoomSelection(RectangleF selectionRect, SizeF containerSize, SizeF imageSize)
{
PointF selectionTrueOrigin = TranslateZoomPosition(selectionRect.Location, containerSize, imageSize);
float scaleFactor = GetScaleFactor(containerSize, imageSize);
SizeF selectionTrueSize = new SizeF(selectionRect.Width / scaleFactor, selectionRect.Height / scaleFactor);
return new RectangleF(selectionTrueOrigin, selectionTrueSize);
}
public RectangleF TranslateSelectionToZoomedSel(RectangleF selectionRect, SizeF containerSize, SizeF imageSize)
{
float scaleFactor = GetScaleFactor(containerSize, imageSize);
RectangleF zoomedSelectionRect = new
RectangleF(selectionRect.X * scaleFactor, selectionRect.Y * scaleFactor,
selectionRect.Width * scaleFactor, selectionRect.Height * scaleFactor);
PointF imageScaledOrigin = GetImageScaledOrigin(containerSize, imageSize);
zoomedSelectionRect.Location = new PointF(zoomedSelectionRect.Location.X + imageScaledOrigin.X,
zoomedSelectionRect.Location.Y + imageScaledOrigin.Y);
return zoomedSelectionRect;
}
public PointF TranslateCoordinatesOrigin(PointF coordinates, SizeF containerSize, SizeF imageSize)
{
PointF imageOrigin = GetImageScaledOrigin(containerSize, imageSize);
return new PointF(coordinates.X - imageOrigin.X, coordinates.Y - imageOrigin.Y);
}
public PointF GetImageScaledOrigin(SizeF containerSize, SizeF imageSize)
{
SizeF imageScaleSize = GetImageScaledSize(containerSize, imageSize);
return new PointF((containerSize.Width - imageScaleSize.Width) / 2,
(containerSize.Height - imageScaleSize.Height) / 2);
}
public SizeF GetImageScaledSize(SizeF containerSize, SizeF imageSize)
{
float scaleFactor = GetScaleFactor(containerSize, imageSize);
return new SizeF(imageSize.Width * scaleFactor, imageSize.Height * scaleFactor);
}
internal float GetScaleFactor(SizeF scaled, SizeF original)
{
return (original.Width > original.Height) ? (scaled.Width / original.Width)
: (scaled.Height / original.Height);
}
}
I'm using the following code to Transform a small rectangle coordinates to a larger one ie: A rectangle position on a small image to the same position on the larger resolution of the same image
Rectangle ConvertToLargeRect(Rectangle smallRect, Size largeImageSize, Size smallImageSize)
{
double xScale = (double)largeImageSize.Width / smallImageSize.Width;
double yScale = (double)largeImageSize.Height / smallImageSize.Height;
int x = (int)(smallRect.X * xScale + 0.5);
int y = (int)(smallRect.Y * yScale + 0.5);
int right = (int)(smallRect.Right * xScale + 0.5);
int bottom = (int)(smallRect.Bottom * yScale + 0.5);
return new Rectangle(x, y, right - x, bottom - y);
}
But there seems to be a problem with some images.The transformed rectangle coordinates seems to be off the image.
UPDATE:
img.Draw(rect, new Bgr(232, 3, 3), 2);
Rectangle transret= ConvertToLargeRect(rect, orgbitmap.Size, bit.Size);
target = new Bitmap(transret.Width, transret.Height);
using (Graphics g = Graphics.FromImage(target))
{
g.SmoothingMode = SmoothingMode.HighQuality;
g.DrawImage(orgbitmap, new Rectangle(0, 0, target.Width, target.Height),
transret, GraphicsUnit.Pixel);
}
Rectangle Drawn on small resolution Image
{X=190,Y=2,Width=226,Height=286}
Rectangle Transformed into Orginal Large Resolution Image {X=698,Y=7,Width=830,Height=931}
Original Image
First of all, if you resize the shape it shouldn't move position. That's not what one would expect out of enlarging a shape. This means the X,Y point of the top-left corner shouldn't be transformed.
Second, you shouldn't be adding 0.5 manually to operations, that's not a clean way to proceed. Use the ceiling function as suggested by #RezaAghaei
Third, you should not substract X/Y from the height/width, your calculations should be done as width * scale.
Please correct those mistakes, and if it doesn't work I'll update the answer with extra steps.
Unfortunately the picture box during resize jumps into the lower corner.
I read that I need to set something up with Docker and Anchor but I have no idea how.
Your Location (x, y) refers to the upper-left corner of the control. When you resize the PictureBox, only the lower-right corner will move unless you also change the location.
Are you changing the size in code? If so, you can use a helper method to do this. The image needs to shift left by half of the change in width if it is getting bigger, and needs to shift right by half of the change of width if it is getting smaller (same logic applies to height):
private void ChangePictureBoxSize(int newWidth, int newHeight)
{
// these will be negative if picturebox is getting bigger
int changeInWidth = pictureBox1.Width - newWidth;
int changeInHeight = pictureBox1.Height - newHeight;
// will shift left and up if picturebox is getting bigger
int newX = pictureBox1.Location.X + (changeInWidth / 2);
int newY = pictureBox1.Location.Y + (changeInHeight / 2);
pictureBox1.Location = new Point(newX, newY);
pictureBox1.Size = new Size(newWidth, newHeight);
}
Using System.Drawing.Image.
If an image width or height exceed the maximum, it need to be resized proportionally .
After resized it need to make sure that neither width or height still exceed the limit.
The Width and Height will be resized until it is not exceed to maximum and minimum automatically (biggest size possible) and also maintain the ratio.
Like this?
public static void Test()
{
using (var image = Image.FromFile(#"c:\logo.png"))
using (var newImage = ScaleImage(image, 300, 400))
{
newImage.Save(#"c:\test.png", ImageFormat.Png);
}
}
public static Image ScaleImage(Image image, int maxWidth, int maxHeight)
{
var ratioX = (double)maxWidth / image.Width;
var ratioY = (double)maxHeight / image.Height;
var ratio = Math.Min(ratioX, ratioY);
var newWidth = (int)(image.Width * ratio);
var newHeight = (int)(image.Height * ratio);
var newImage = new Bitmap(newWidth, newHeight);
using (var graphics = Graphics.FromImage(newImage))
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
return newImage;
}
Much longer solution, but accounts for the following scenarios:
Is the image smaller than the bounding box?
Is the Image and the Bounding Box square?
Is the Image square and the bounding box isn't
Is the image wider and taller than the bounding box
Is the image wider than the bounding box
Is the image taller than the bounding box
private Image ResizePhoto(FileInfo sourceImage, int desiredWidth, int desiredHeight)
{
//throw error if bouning box is to small
if (desiredWidth < 4 || desiredHeight < 4)
throw new InvalidOperationException("Bounding Box of Resize Photo must be larger than 4X4 pixels.");
var original = Bitmap.FromFile(sourceImage.FullName);
//store image widths in variable for easier use
var oW = (decimal)original.Width;
var oH = (decimal)original.Height;
var dW = (decimal)desiredWidth;
var dH = (decimal)desiredHeight;
//check if image already fits
if (oW < dW && oH < dH)
return original; //image fits in bounding box, keep size (center with css) If we made it bigger it would stretch the image resulting in loss of quality.
//check for double squares
if (oW == oH && dW == dH)
{
//image and bounding box are square, no need to calculate aspects, just downsize it with the bounding box
Bitmap square = new Bitmap(original, (int)dW, (int)dH);
original.Dispose();
return square;
}
//check original image is square
if (oW == oH)
{
//image is square, bounding box isn't. Get smallest side of bounding box and resize to a square of that center the image vertically and horizontally with Css there will be space on one side.
int smallSide = (int)Math.Min(dW, dH);
Bitmap square = new Bitmap(original, smallSide, smallSide);
original.Dispose();
return square;
}
//not dealing with squares, figure out resizing within aspect ratios
if (oW > dW && oH > dH) //image is wider and taller than bounding box
{
var r = Math.Min(dW, dH) / Math.Min(oW, oH); //two dimensions so figure out which bounding box dimension is the smallest and which original image dimension is the smallest, already know original image is larger than bounding box
var nH = oH * r; //will downscale the original image by an aspect ratio to fit in the bounding box at the maximum size within aspect ratio.
var nW = oW * r;
var resized = new Bitmap(original, (int)nW, (int)nH);
original.Dispose();
return resized;
}
else
{
if (oW > dW) //image is wider than bounding box
{
var r = dW / oW; //one dimension (width) so calculate the aspect ratio between the bounding box width and original image width
var nW = oW * r; //downscale image by r to fit in the bounding box...
var nH = oH * r;
var resized = new Bitmap(original, (int)nW, (int)nH);
original.Dispose();
return resized;
}
else
{
//original image is taller than bounding box
var r = dH / oH;
var nH = oH * r;
var nW = oW * r;
var resized = new Bitmap(original, (int)nW, (int)nH);
original.Dispose();
return resized;
}
}
}
Working Solution :
For Resize image with size lower then 100Kb
WriteableBitmap bitmap = new WriteableBitmap(140,140);
bitmap.SetSource(dlg.File.OpenRead());
image1.Source = bitmap;
Image img = new Image();
img.Source = bitmap;
WriteableBitmap i;
do
{
ScaleTransform st = new ScaleTransform();
st.ScaleX = 0.3;
st.ScaleY = 0.3;
i = new WriteableBitmap(img, st);
img.Source = i;
} while (i.Pixels.Length / 1024 > 100);
More Reference at http://net4attack.blogspot.com/