Translate Rectangle Position in a Picturebox in Zoom Mode
Asked Answered
H

2

5

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

Horsey answered 16/12, 2018 at 7:38 Comment(0)
P
7

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:

enter image description here

And this is the result:

enter image description here

Pension answered 16/12, 2018 at 8:24 Comment(11)
ImageRectangleFromSizeMode wow, this is digging deep into the hidden vault!Barnebas
@Horsey Changed a bit to support Stretch mode as well.Pension
@Barnebas Added the link to ImageRectangleFromSizeMode source code so future readers can use it and write such method as part of their application code.Pension
Ha! ImageRectangleFromSizeMode is something you usually see mentioned in stack traces (when something goes wrong). Well done!Pisgah
@RezaAghaei Actually also need to do the reverse ie: i have the rectangle coordinates on the original image.When this image is displayed in the picturebox,i need to place the movable frame at the correct position by translating its known position in the original image.Horsey
You learned how to calculate image scale. So it's enough to divide the source rectangle coordinate and size with scale. Then offset it with imageRect.X and Y.Pension
@RezaAghaei Could you please add an example on doing the reverse as per my comment above.I can't seem to figure it out.Horsey
Please take a look at this question #68053178Horsey
Can you please see the update on this question.Horsey
Hi @techno, I'm pretty busy at the moment and as you can see, it's more than two months that I haven't posted any answer. I'll try to back on the track as soon as I can and then if you still need an answer, I'll try to help.Pension
@Horsey I believe Jimi's post answer your new question; however for sake of completeness, I created the reverse method as well and posted as an answer for your question.Pension
P
5

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:

PictureBox Zoom Selection

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);
    }
}
Pisgah answered 16/12, 2018 at 12:19 Comment(24)
Thanks a lot :). I will take a look at both of your answers.Horsey
Actually also need to do the reverse ie: i have the rectangle coordinates on the original image.When this image is displayed in the picturebox,i need to place the movable frame at the correct position by translating its known position in the original image.Horsey
@Horsey See the edit. I've added a selection translator from coordinates relative to the original image to the scaled image (and a sample animation too :). I didn't have not much time, so it's not tested properly, but I've made some slections in different conditions and it looks OK. I'll revise it tomorrow.Pisgah
How do i detect if the control was resized or moved in the picturebox? Currently i use picturebox mouse mouse event. Is there a better way?Horsey
What is a mouse mouse event? :) I'll assume it's a MouseMove. I'm not sure how you implemented this Control (TaW's or Reza Aghaei's custom controls previously mentioned, possibly), but of course this custom control 1) can only be moved inside the bounds of the PictureBox (if it's not, see here: Don't move the Labels outside a PictureBox), 2) exposes a property or rises an event that reference its current position inside the PictureBox. When the selection control is moved(...)Pisgah
(...) you just need to re-calculate its bounds, calliing the translation method you implemented. In relation to the code I posted, it would be the TranslateZoomSelection or Reza Aghaei's GetRectangleOnImage(). You need to store these information each time the control is relocated. I've seen your other question about it: the class that stores these references must be updated with the current bounds of the selection, so you just need to add a property for this. The storage class ought to be serializable, so you can easily save its values in a configuration file/database field.Pisgah
Thanks for your reply. I have solved the storage problem using a List that stores the custom class.I need to store the coordinates after the user has moved the selection rectangle.Which event should i hook onto in this case ?Horsey
How would I do it? Have the Selector Control implement INotifyPropertyChange (or similar behaviour). When the MouseUp of the Selector Control is raised, updated a custom property (of the Selector). The custom property, when set, raises a PropertyChanged event that the parent Form can subscribe. The PropertyChanged EventArgs reference the new Bounds value (and other values, if needed). An example here.Pisgah
If you're using a static class for some reason, another implementation here: How can I make the value of a variable track the value of anotherPisgah
But the 'MouseUp' and 'MoverMove' event of the control is never firing.Only Location changed event is firing.Horsey
I have no idea how you implemented this Selector control. But since an event is raised (LocationChanged, as you said), that's enough. Using this event, you'll have the new Location which, along with the Control Size, gives you the Selector Bounds (the Rectangle). Update the related custom property, then raise the PropertyChanged event, updating its EventArgs with new values. Of course, set the sender object to the current Selector control instance, so the Form that receives the PropertyChanged event has all the details needed to update the storage class.Pisgah
Also since the initial position of the selection window is programatically determined and placed.The location changed event should be prevented from firing in this case as the location is not actually changed by the user.Is using a flag a good approach?Horsey
That Custom Control is a skeleton, where only the WM_NCHITTEST is handled. You can customize it the way you want, adding all the event handlers you require. BUT, since, possibly, just the LocationChanged event is really needed, you could just have: Control currentSelector = sender as Control; Rectangle selection = currentSelector.Bounds;. Here, you have the custom control that's been moved and its Bounds, which provide the selection Rectangle. Update the storage class and that's all. Any other customization depends on your implementation. (Btw, watch out for mouse Double Clicks!)Pisgah
The locationchange event also does work as the control can be resized which does not trigger the location change.Also how can i pass the location to the Main Class?Horsey
In your FrameControl class, in the WndProc override, after the last else statement, insert base.OnLocationChanged(new EventArgs());. You Form class just needs to subscribe the LocationChanged event of the FrameControl. The Control.Bounds property will be updated (remember to cast sender to Control or to FrameControl, it's the same).Pisgah
This works but the Event gets triggered just by Moving the Pointer over the picturebox.Which results in wrong flags getting set.Horsey
It all depends on what you want to know and when. If this event is raised too often, then remove base.OnLocationChanged(new EventArgs()); from the FrameControl and simply let the Form container subscribe to the LocationChanged and Move events of the FrameControl. As usual. You have to keep track of the Location and Size of the Frame anyway. These properties change every time you resize and/or move the control.Pisgah
That seems to have fixed the issue :) .ThanksHorsey
btw.. i have upvoted your answer,but accepted the other as it solved my problem and your answer adds more details to it.Horsey
Yes, of course. This is what was meant to be from the start. That's why I wrote Consider this an addition to Reza Aghaei answer :)Pisgah
I encountered a new issue today,where the Y coordinate of the frame is set to negative.Horsey
When the following parameter is supplied to TranslateSelectionToZoomedSel -- Rect {X = 37 Y = 2 Width = 227 Height = 308} -- SizeF(603,423),imagesize - 311,310 --- The returned rectangle is this {X = 71.73955 Y = -85.15273 Width = 440.131836 Height = 597.1833}Horsey
Do you have any idea why this happens?Horsey
i have posted a new question for this #68053178Horsey

© 2022 - 2024 — McMap. All rights reserved.