Java SWT - 在缩放和缩小后从图像中获取实际的x、y坐标

15

我有一张被缩放以适应比例的图片。用户从缩放后的图片中选择一个矩形。

然后,我会根据这个选择重新绘制:

gc.drawImage(imageDisplayed, minX, minY, width, height, imageDisplayed.getBounds().x,  imageDisplayed.getBounds().y, imageDisplayed.getBounds().width, imageDisplayed.getBounds().height );

现在我想要从缩放和放大后的图片中获取原始坐标。这样正确吗?

public Coordinate GetScaledXYCoordinate(int oldX, int oldY, int width, int height, int scaledWidth, int scaledHeight)
{       
    int newX = (int)(oldX * width)/scaledWidth;
    int newY = (int)(oldY * height)/scaledHeight;

    Coordinate retXY = new Coordinate(newX, newY);
    return retXY;
}


public Coordinate GetZoomedXYCoordinate(int oldX, int oldY, int startX, int endX, int startY, int endY,
        int width, int height,int scaledWidth, int scaledHeight)
{       
    // First get x,y after scaling
    Coordinate xy = GetScaledXYCoordinate(oldX, oldY, width, height, scaledWidth, scaledHeight);

    // Now get x.y after zooming
    int minX = Math.min(startX, endX);
    int minY = Math.min(startY, endY);

    int maxX = Math.max(startX, endX);
    int maxY = Math.max(startY, endY);

    int rectWidth = maxX - minX;
    int rectHeight = maxY - minY;
    return GetScaledXYCoordinate(xy.getX(), xy.getY(), width, height, scaledWidth, scaledHeight);
}

注意: 我希望有一个算法适用于多个缩放,而不仅仅是一个缩放。

更新:

理想情况下,我希望有一个函数,它接受屏幕点X,Y,并返回原始图像X,Y。该函数在缩放和缩放后仍将返回正确的X,Y。


仍然在这个问题上卡住了。 - Robben_Ford_Fan_boy
如果您能够发布一个最小化、完整化和可验证化的例子,将会非常有帮助。 - Baz
1
我理解你的意思是想将屏幕/窗口坐标转换为图像坐标,是吗? - Leon
@Leon 是的。经过缩放和“缩放”后。缩放只是取一个矩形部分并放大。 - Robben_Ford_Fan_boy
4个回答

9
方法selectionToOriginal应该返回一个Rectangle,其中包含相对于原始图像的最后缩放选择的位置和尺寸。
它接收以下参数:
  • scaledDimensions:缩放图像的尺寸,即进行缩放选择的地方。
  • levels:连续缩放Rectangle选择的列表;在第一级中,您将放置原始图像的尺寸。
此测试程序展示了如何使用具有尺寸800x600和缩放尺寸400x300的原始图像。对其应用了两个连续的缩放选择。
import java.util.ArrayList;
import java.util.List;

import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;

public class ScaleTest {

    public static void main(String[] args) {

        Point scaledDimensions = new Point(400, 300);

        List<Rectangle> levels = new ArrayList<Rectangle>();

        // first level is the original image dimension
        levels.add(new Rectangle(0, 0, 800, 600));

        // other levels are the zooming selection inside the scaled image
        levels.add(new Rectangle(0, 0, 200, 150));
        levels.add(new Rectangle(200, 150, 200, 150));

        Rectangle selectionToOriginal = selectionToOriginal(scaledDimensions,
            levels);

        System.out.println(selectionToOriginal);
    }

    public static Rectangle selectionToOriginal(Point scaledDimensions,
        List<Rectangle> levels) {

        int numberOfLevels = levels.size();
        double scaledX = 0;
        double scaledY = 0;

        // we will work with the size of the last selection
        double scaledWidth = levels.get(numberOfLevels - 1).width;
        double scaledHeight = levels.get(numberOfLevels - 1).height;

        // start from the last selection to the first 
        for (int currentLevel = numberOfLevels - 1; currentLevel > 0; currentLevel--) {

            // get the width of the level N - 1
            double previousSelectionWidth = levels.get(currentLevel - 1).width;

            // convert the width of 1 unit in level N to its width in level N - 1
            double unitaryWidth = previousSelectionWidth / scaledDimensions.x;
            // convert the X position in level N in its X position in level N - 1
            scaledX = unitaryWidth * (levels.get(currentLevel).x + scaledX);
            // convert the width in level N in its width in level N - 1
            scaledWidth *= unitaryWidth;

            // get the height of the level N - 1
            double previousSelectionHeight = levels.get(currentLevel - 1).height;

            // convert the height of 1 unit in level N to its height in level N - 1
            double unitaryHeight = previousSelectionHeight / scaledDimensions.y;
            // convert the Y position in level N in its Y position in level N - 1
            scaledY = unitaryHeight * (levels.get(currentLevel).y + scaledY);
            // convert the height in level N in its height in level N - 1
            scaledHeight *= unitaryHeight;
        }

        return new Rectangle((int) scaledX, (int) scaledY, (int) scaledWidth,
            (int) scaledHeight);
    }

}

该程序返回一个位置为(200, 150)、大小为(200, 150)的矩形Rectangle,图像显示如下:

Program result

注:
  • in your code you used the class Coordinate which it seem equal to the SWT class Point which I used in my method
  • the casts in the return instruction

    return new Rectangle((int) scaledX, (int) scaledY, (int) scaledWidth,
            (int) scaledHeight);
    

    will truncate the value of the doubles, consider using Math.round instead if you prefer to round the values


谢谢提供这个可视化。这让我们更容易看到正在发生的事情。 - Devon_C_Miller
我不明白为什么这是最受欢迎的答案。它过于复杂。即使看着“可视化效果”,我也不明白为什么selectionToOriginal需要一个点并返回一个矩形。你的实际屏幕坐标在哪里?根据OP,选择应该是一个矩形。我期望将scaledDimensions更改为(800, 600)会使点(300, 400)出现在输出中(图像坐标系中第三个矩形的右下角)。但它返回了Rectangle {50, 37, 50, 37} - code_onkel
在我看来,这个方法非常简单。scaledDimensions 是一个 Point(包含宽度和高度的 2 个整数),因为我们只需要知道选择操作发生的区域的大小,其 x 和 y 坐标被隐含为 0(就像 OP 所做的那样,在他的方法中仅请求 scaledWidthscaledHeight)。我们不是使用屏幕坐标而是使用图像坐标进行操作,所以在屏幕上移动 Shell 不应该对 OP 所要求的结果产生任何影响。如果您提供了您尝试的测试的确切输入参数,我将解释结果。 - Loris Securo
OP的问题是这样的:“我有一张被缩放以适应的图片。用户从缩放后的图片中选择一个矩形。” scaledDimensions(400, 300)意味着原始图片(大小为800x600)在400x300的空间内完整显示。此外,连续的选择都是在这个空间内进行的,因此第一个选择(0, 0, 200, 150)基本上是原始图片的左上角四分之一(见图像)。第二个选择(200, 150, 200, 150)是前一个选择的右下角。 - Loris Securo
@Robben_Ford_Fan_boy 这个方法应该这样做,你理解输入参数了吗?你遇到了什么问题? - Loris Securo
显示剩余5条评论

8

SWT有一个专门的类Transform用于执行坐标转换(我更愿意说是变换,因为在这种情况下“平移”只是一种特殊情况,其他变换包括“缩放”、“旋转”和“剪切”)。AWT有一个更方便的AffineTransform类,它不受图形子系统的限制。

使用其中之一的类可以简化操作。一旦构建了映射坐标的变换对象(例如,将源图像坐标映射到显示坐标),就可以轻松地获得反向变换(从显示坐标返回到源图像坐标)。使用invert()createInverse()方法即可实现此目的(后者仅适用于AffineTransform)。

使用transform()方法执行实际的坐标转换。对于SWT.Transform,如果您需要转换单个点,则其签名有点不方便,但可以轻松地将其包装在辅助函数中。

对于您的目的,您只需要使用scale()translate()方法来定义您的坐标转换。最可能,您希望根据源矩形和目标矩形定义您的变换(类似于您对drawImage()方法的使用); this answer展示了如何完成。然后,当您缩放或以其他方式操作图像的显示方式时,必须使变换对象保持最新状态。

更新

@code_onkel提供了使用此方法的示例程序


8
这是一个使用 SWT 缩放图像的完整工作示例,实现了 Leon's answer 的思路。使用 仿射变换 是二维图形中带有个别坐标系的元素的默认绘制方法。
  1. 使用 Transform 在正确的位置和比例上绘制图片。
  2. 使用该 Transform 的反转来获取所选缩放区域的图像坐标。
  3. 计算新的 Transform 以显示缩放区域。
下面的类执行以下操作:
  • Transform 存储在 paintTransform 中。
  • 缩放区域的屏幕坐标存储在 zoomStartzoomEnd 中。
  • 从拖动的缩放矩形中计算所选区域的图像坐标在 setVisibleImageAreaInScreenCoordinates 中完成。
  • 新的 TransformsetVisibleImageAreaInImageCoordinates 中计算。
  • 大部分代码可以视为样板代码。

请注意,图像永远不会被替换为缩放版本。它使用 paintTransform 进行绘制。这意味着图形上下文负责绘制缩放后的图像。实际绘制代码变得非常简单:

ev.gc.setTransform(paintTransform);
ev.gc.drawImage(img, 0, 0);

所有计算都是在处理状态转换期间进行的,这是由鼠标事件触发的,即在mouseUp()处理程序中调用zoom()方法。

import java.io.InputStream;
import java.net.URL;

import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

public class Zoom implements PaintListener, MouseMoveListener, MouseListener {
    private static final int MOUSE_DOWN = 1;
    private static final int DRAGGING = 2;
    private static final int NOT_DRAGGING = 3;

    int dragState = NOT_DRAGGING;
    Point zoomStart;
    Point zoomEnd;

    ImageData imgData;
    Image img;
    Transform paintTransform;
    Shell shell;
    Color rectColor;

    public Zoom(ImageData image, Shell shell) {
        imgData = image;
        img = new Image(shell.getDisplay(), image);
        this.shell = shell;
        rectColor = new Color(shell.getDisplay(), new RGB(255, 255, 255));
    }

    void zoom() {
        int x0 = Math.min(zoomStart.x, zoomEnd.x);
        int x1 = Math.max(zoomStart.x, zoomEnd.x);
        int y0 = Math.min(zoomStart.y, zoomEnd.y);
        int y1 = Math.max(zoomStart.y, zoomEnd.y);

        setVisibleImageAreaInScreenCoordinates(x0, y0, x1, y1);
    }

    void setVisibleImageAreaInImageCoordinates(float x0, float y0,
            float x1, float y1) {
        Point sz = shell.getSize();

        double width = x1 - x0;
        double height = y1 - y0;

        double sx = (double) sz.x / (double) width;
        double sy = (double) sz.y / (double) height;

        float scale = (float) Math.min(sx, sy);

        // compute offset to center selected rectangle in available area
        double ox = 0.5 * (sz.x - scale * width);
        double oy = 0.5 * (sz.y - scale * height);

        paintTransform.identity();
        paintTransform.translate((float) ox, (float) oy);
        paintTransform.scale(scale, scale);
        paintTransform.translate(-x0, -y0);
    }

    void setVisibleImageAreaInScreenCoordinates(int x0, int y0,
            int x1, int y1) {
        Transform inv = invertPaintTransform();
        // points in screen coordinates
        // to be transformed to image coordinates
        // (top-left and bottom-right corner of selection)
        float[] points = { x0, y0, x1, y1 };

        // actually get image coordinates
        // (in-place operation on points array)
        inv.transform(points);
        inv.dispose();

        // extract image coordinates from array
        float ix0 = points[0];
        float iy0 = points[1];
        float ix1 = points[2];
        float iy1 = points[3];

        setVisibleImageAreaInImageCoordinates(ix0, iy0, ix1, iy1);
    }

    Transform invertPaintTransform() {
        // clone paintTransform
        float[] elems = new float[6];
        paintTransform.getElements(elems);
        Transform inv = new Transform(shell.getDisplay());
        inv.setElements(elems[0], elems[1], elems[2],
                        elems[3], elems[4], elems[5]);

        // invert clone
        inv.invert();
        return inv;
    }

    void fitImage() {
        Point sz = shell.getSize();

        double sx = (double) sz.x / (double) imgData.width;
        double sy = (double) sz.y / (double) imgData.height;

        float scale = (float) Math.min(sx, sy);

        paintTransform.identity();
        paintTransform.translate(sz.x * 0.5f, sz.y * 0.5f);
        paintTransform.scale(scale, scale);
        paintTransform.translate(-imgData.width*0.5f, -imgData.height*0.5f);
    }

    @Override
    public void paintControl(PaintEvent ev) {
        if (paintTransform == null) {
            paintTransform = new Transform(shell.getDisplay());
            fitImage();
        }

        ev.gc.setTransform(paintTransform);
        ev.gc.drawImage(img, 0, 0);

        if (dragState == DRAGGING) {
            drawZoomRect(ev.gc);
        }
    }

    void drawZoomRect(GC gc) {
        int x0 = Math.min(zoomStart.x, zoomEnd.x);
        int x1 = Math.max(zoomStart.x, zoomEnd.x);
        int y0 = Math.min(zoomStart.y, zoomEnd.y);
        int y1 = Math.max(zoomStart.y, zoomEnd.y);

        gc.setTransform(null);

        gc.setAlpha(0x80);
        gc.setForeground(rectColor);
        gc.fillRectangle(x0, y0, x1 - x0, y1 - y0);
    }

    public static void main(String[] args) throws Exception {
        URL url = new URL(
                "https://upload.wikimedia.org/wikipedia/commons/thumb/"  +
                "6/62/Billy_Zoom.jpg/800px-Billy_Zoom.jpg");
        InputStream input = url.openStream();
        ImageData img;
        try {
            img = new ImageData(input);
        } finally {
            input.close();
        }

        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setSize(800, 600);

        Zoom zoom = new Zoom(img, shell);

        shell.open();
        shell.addPaintListener(zoom);
        shell.addMouseMoveListener(zoom);
        shell.addMouseListener(zoom);

        while (!shell.isDisposed()) {
            if (!display.readAndDispatch())
                display.sleep();
        }
        display.dispose();
    }

    @Override
    public void mouseDoubleClick(MouseEvent e) {
    }

    @Override
    public void mouseDown(MouseEvent e) {
        if (e.button != 1) {
            return;
        }
        zoomStart = new Point(e.x, e.y);
        dragState = MOUSE_DOWN;
    }

    @Override
    public void mouseUp(MouseEvent e) {
        if (e.button != 1) {
            return;
        }
        if (dragState == DRAGGING) {
            zoomEnd = new Point(e.x, e.y);
        }
        dragState = NOT_DRAGGING;
        zoom();
        shell.redraw();
    }

    @Override
    public void mouseMove(MouseEvent e) {
        if (dragState == NOT_DRAGGING) {
            return;
        }
        if (e.x == zoomStart.x && e.y == zoomStart.y) {
            dragState = MOUSE_DOWN;
        } else {
            dragState = DRAGGING;
            zoomEnd = new Point(e.x, e.y);
        }
        shell.redraw();
    }
}

当窗口大小调整时,变换目前没有改变。这可以像缩放一样实现:使用旧的窗口大小计算先前可见的图像坐标,使用新的窗口大小计算新的变换。

感谢您添加这个示例实现。我在我的答案中引用了您的回答。 - Leon
@code_onkel 我不明白如何将屏幕坐标转换为原始图像坐标。 - Robben_Ford_Fan_boy
@Robben_Ford_Fan_boy 屏幕到图像的转换是 paintTransform 的反转。实际的计算被隐藏在 setVisibleImageAreaInScreenCoordinates() 中的 inv.transform(points) 中。 - code_onkel
@Robben_Ford_Fan_boy,“invertPaintTransform()”方法看起来比实际复杂,因为“Transform.invert()”是一个原地操作,并且没有一行代码可以克隆“Transform”。 - code_onkel

1
这是我的尝试。
private static Point transformPoint(ArrayList<RectF> rectangleLevels, PointF intPoint)
{
    RectF sourceRec = rectangleLevels.get(rectangleLevels.size()-1);
    Point sourcePoint = new Point((int)intPoint.X, (int)intPoint.Y);
    Point retPoint = sourcePoint;
    for (int i = rectangleLevels.size()-2; i >=0; i--) {
        RectF destRec = rectangleLevels.get(i);
        retPoint = transformPoint(sourceRec, destRec, sourcePoint);

        // Current destination point and rec become source for next round
        sourcePoint = retPoint;
        sourceRec = destRec;
    }
    return retPoint;
}


/*
Rectangle 1 has (x1, y1) origin and (w1, h1) for width and height, and
Rectangle 2 has (x2, y2) origin and (w2, h2) for width and height, then

Given point (x, y) in terms of Rectangle 1 co-ords, to convert it to Rectangle 2 co-ords:
    xNew = ((x-x1)/w1)*w2 + x2;
    yNew = ((y-y1)/h1)*h2 + y2;
 */
private static Point transformPoint(RectF source, RectF destination, Point intPoint)
{
    PointF point = new PointF();
    point.X = intPoint.x;
    point.Y = intPoint.y;
    return transformPoint(source, destination, point);
}

private static Point transformPoint(RectF source, RectF destination, PointF point)
{
    return new Point(
    (int) (((point.X - source.X) / source.Width) * destination.Width + destination.X),
    (int) (((point.Y - source.Y) / source.Height) * destination.Height + destination.Y));
}

所以这意味着我只需要跟踪我的缩放和缩放比例,然后传入屏幕X,Y,以获取原始图像中的x,y:
    ArrayList<RectF> rectangleLevels = new ArrayList<RectF>();
    RectF origImage = getRectangle(0,0,320,200);
    RectF scaledImage = getRectangle(0,0,800,800);
    RectF zoomedImage = getRectangle(310,190,10,10);
    RectF scaledZoomedImage = getRectangle(0,0,800,800);
    rectangleLevels.add(origImage);
    rectangleLevels.add(scaledImage);
    rectangleLevels.add(zoomedImage);
    rectangleLevels.add(scaledZoomedImage);
    PointF pointInZoomedImg = getPoint(799, 799);

    Point retPoint = transformPoint(rectangleLevels, pointInZoomedImg);

这个尝试是否有效,还是你仍然需要帮助?您认为zoomedImage相对于(800,800)还是(320,200)?如果它相对于(800,800),那么我的解决方案将涵盖使用scaledDimensions =(800,800)和levels与这些矩形:(0,0,320,200),(310,190,10,10),(799,799,0,0)的情况。它将返回(127,49,0,0),由于您想要一个点,因此您可以只考虑矩形的x和y部分,即(127,49)。 - Loris Securo
@LorisSecuro - 不,由于存在舍入问题,它并不能完全工作。 - Robben_Ford_Fan_boy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接