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

Jimi
  • 29,621
  • 8
  • 43
  • 61
techno
  • 6,100
  • 16
  • 86
  • 192

2 Answers2

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

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • 1
    `ImageRectangleFromSizeMode` wow, this is digging deep into the hidden vault! – TaW Dec 16 '18 at 09:53
  • @techno Changed a bit to support `Stretch` mode as well. – Reza Aghaei Dec 16 '18 at 10:04
  • 1
    @TaW Added the link to `ImageRectangleFromSizeMode` source code so future readers can use it and write such method as part of their application code. – Reza Aghaei Dec 16 '18 at 10:16
  • 1
    Ha! `ImageRectangleFromSizeMode` is something you usually see mentioned in stack traces (when something goes wrong). Well done! – Jimi Dec 16 '18 at 12:21
  • @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. – techno Dec 16 '18 at 17:33
  • 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`. – Reza Aghaei Dec 16 '18 at 17:41
  • @RezaAghaei Could you please add an example on doing the reverse as per my comment above.I can't seem to figure it out. – techno Jun 20 '21 at 04:06
  • Please take a look at this question https://stackoverflow.com/questions/68052678/translating-rectangle-position-in-zoom-mode-picturebox-results-in-negative-y-coo/68053040#68053040 – techno Jun 23 '21 at 15:37
  • Can you please see the update on this question. – techno Jun 24 '21 at 04:10
  • 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. – Reza Aghaei Jun 24 '21 at 14:04
  • @techno 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](https://stackoverflow.com/q/68052678/3110834). – Reza Aghaei Jun 24 '21 at 19:08
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);
    }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Thanks a lot :). I will take a look at both of your answers. – techno Dec 16 '18 at 12:58
  • 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. – techno Dec 16 '18 at 17:34
  • @techno 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. – Jimi Dec 16 '18 at 20:37
  • 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? – techno Dec 18 '18 at 02:27
  • 1
    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](https://stackoverflow.com/questions/53316286/dont-move-the-labels-outside-a-picturebox?answertab=active#tab-top)), 2) exposes a property or rises an event that reference its current position inside the PictureBox. When the selection control is moved(...) – Jimi Dec 18 '18 at 09:34
  • (...) 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. – Jimi Dec 18 '18 at 09:35
  • 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 ? – techno Dec 18 '18 at 10:26
  • 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](https://stackoverflow.com/questions/51396681/translucent-circle-with-text?answertab=active#tab-top). – Jimi Dec 18 '18 at 10:37
  • 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 another](https://stackoverflow.com/questions/52685245/how-can-i-make-the-value-of-a-variable-track-the-value-of-another?answertab=active#tab-top) – Jimi Dec 18 '18 at 10:37
  • But the 'MouseUp' and 'MoverMove' event of the control is never firing.Only Location changed event is firing. – techno Dec 18 '18 at 10:48
  • 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. – Jimi Dec 18 '18 at 10:58
  • 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? – techno Dec 18 '18 at 11:00
  • 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!) – Jimi Dec 18 '18 at 11:33
  • 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? – techno Dec 18 '18 at 17:25
  • 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). – Jimi Dec 18 '18 at 17:37
  • This works but the Event gets triggered just by Moving the Pointer over the picturebox.Which results in wrong flags getting set. – techno Dec 21 '18 at 03:40
  • 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. – Jimi Dec 21 '18 at 03:54
  • That seems to have fixed the issue :) .Thanks – techno Dec 21 '18 at 03:56
  • btw.. i have upvoted your answer,but accepted the other as it solved my problem and your answer adds more details to it. – techno Dec 21 '18 at 03:57
  • 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* :) – Jimi Dec 21 '18 at 03:58
  • I encountered a new issue today,where the Y coordinate of the frame is set to negative. – techno Jun 19 '21 at 05:25
  • 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}` – techno Jun 19 '21 at 05:29
  • Do you have any idea why this happens? – techno Jun 19 '21 at 05:29
  • i have posted a new question for this https://stackoverflow.com/questions/68052678/translating-rectangle-position-in-zoom-mode-picturebox-results-in-negative-y-coo – techno Jun 20 '21 at 04:28