Index: src/org/openstreetmap/josm/tools/ImageWarp.java
===================================================================
--- src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 19042)
+++ src/org/openstreetmap/josm/tools/ImageWarp.java	(working copy)
@@ -5,6 +5,7 @@
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -33,6 +34,16 @@
          * @return transformed pixel coordinates
          */
         Point2D transform(Point2D pt);
+
+        /**
+         * Translates pixel coordinates.
+         * @param x The x coordinate
+         * @param y The y coordinate
+         * @return transformed pixel coordinates
+         */
+        default Point2D transform(double x, double y) {
+            return transform(new Point2D.Double(x, y));
+        }
     }
 
     /**
@@ -75,10 +86,15 @@
 
         @Override
         public Point2D transform(Point2D pt) {
-            int xIdx = (int) Math.floor(pt.getX() / stride);
-            int yIdx = (int) Math.floor(pt.getY() / stride);
-            double dx = pt.getX() / stride - xIdx;
-            double dy = pt.getY() / stride - yIdx;
+            return transform(pt.getX(), pt.getY());
+        }
+
+        @Override
+        public Point2D transform(double x, double y) {
+            int xIdx = (int) Math.floor(x / stride);
+            int yIdx = (int) Math.floor(y / stride);
+            double dx = x / stride - xIdx;
+            double dy = y / stride - yIdx;
             Point2D value00 = getValue(xIdx, yIdx);
             Point2D value01 = getValue(xIdx, yIdx + 1);
             Point2D value10 = getValue(xIdx + 1, yIdx);
@@ -91,14 +107,28 @@
         }
 
         private Point2D getValue(int xIdx, int yIdx) {
-            return getRow(yIdx).computeIfAbsent(xIdx, k -> trfm.transform(new Point2D.Double(xIdx * stride, yIdx * stride)));
+            final Map<Integer, Point2D> rowMap = getRow(yIdx);
+            // This *was* computeIfAbsent. Unfortunately, it appears that it generated a ton of memory allocations.
+            // As in, this was ~50 GB memory allocations in a test, and converting to a non-lambda form made it 1.3GB.
+            // The primary culprit was LamdaForm#linkToTargetMethod
+            Point2D current = rowMap.get(xIdx);
+            if (current == null) {
+                current = transform(xIdx, yIdx);
+                rowMap.put(xIdx, current);
+            }
+            return current;
         }
 
+        private Point2D transform(int xIdx, int yIdx) {
+            return trfm.transform(new Point2D.Double(xIdx * stride, yIdx * stride));
+        }
+
         private Map<Integer, Point2D> getRow(int yIdx) {
             cleanUp(yIdx - 3);
             Map<Integer, Point2D> row = cache.get(yIdx);
+            // Note: using computeIfAbsent will drastically increase memory allocations
             if (row == null) {
-                row = new HashMap<>();
+                row = new HashMap<>(256);
                 cache.put(yIdx, row);
                 if (consistencyTest) {
                     // should not create a row that has been deleted before
@@ -152,14 +182,19 @@
     public static BufferedImage warp(BufferedImage srcImg, Dimension targetDim, PointTransform invTransform, Interpolation interpolation) {
         BufferedImage imgTarget = new BufferedImage(targetDim.width, targetDim.height, BufferedImage.TYPE_INT_ARGB);
         Rectangle2D srcRect = new Rectangle2D.Double(0, 0, srcImg.getWidth(), srcImg.getHeight());
+        // These arrays reduce the amount of memory allocations (getRGB and setRGB are
+        // collectively 40% of the memory cost, 78% if LambdaForm#linkToTargetMethod is
+        // ignored). We mostly want to decrease GC pauses here.
+        final int[] pixel = new int[1]; // Yes, this really does decrease memory allocations with TYPE_INT_ARGB.
+        final Object sharedArray = getSharedArray(srcImg);
         for (int j = 0; j < imgTarget.getHeight(); j++) {
             for (int i = 0; i < imgTarget.getWidth(); i++) {
-                Point2D srcCoord = invTransform.transform(new Point2D.Double(i, j));
+                Point2D srcCoord = invTransform.transform(i, j);
                 if (srcRect.contains(srcCoord)) {
                     int rgba;
                     switch (interpolation) {
                         case NEAREST_NEIGHBOR:
-                            rgba = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg);
+                            rgba = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg, sharedArray);
                             break;
                         case BILINEAR:
                             int x0 = (int) Math.floor(srcCoord.getX());
@@ -166,10 +201,10 @@
                             double dx = srcCoord.getX() - x0;
                             int y0 = (int) Math.floor(srcCoord.getY());
                             double dy = srcCoord.getY() - y0;
-                            int c00 = getColor(x0, y0, srcImg);
-                            int c01 = getColor(x0, y0 + 1, srcImg);
-                            int c10 = getColor(x0 + 1, y0, srcImg);
-                            int c11 = getColor(x0 + 1, y0 + 1, srcImg);
+                            int c00 = getColor(x0, y0, srcImg, sharedArray);
+                            int c01 = getColor(x0, y0 + 1, srcImg, sharedArray);
+                            int c10 = getColor(x0 + 1, y0, srcImg, sharedArray);
+                            int c11 = getColor(x0 + 1, y0 + 1, srcImg, sharedArray);
                             rgba = 0;
                             // loop over color components: blue, green, red, alpha
                             for (int ch = 0; ch <= 3; ch++) {
@@ -183,7 +218,7 @@
                         default:
                             throw new AssertionError(Objects.toString(interpolation));
                     }
-                    imgTarget.setRGB(i, j, rgba);
+                    imgTarget.getRaster().setDataElements(i, j, imgTarget.getColorModel().getDataElements(rgba, pixel));
                 }
             }
         }
@@ -190,10 +225,26 @@
         return imgTarget;
     }
 
-    private static int getColor(int x, int y, BufferedImage img) {
+    private static Object getSharedArray(BufferedImage srcImg) {
+        final int numBands = srcImg.getRaster().getNumBands();
+        // Add data types as needed (shown via profiling, look for getRGB).
+        switch (srcImg.getRaster().getDataBuffer().getDataType()) {
+            case DataBuffer.TYPE_BYTE:
+                return new byte[numBands];
+            case DataBuffer.TYPE_INT:
+                return new int[numBands];
+            default:
+                return null;
+        }
+    }
+
+    private static int getColor(int x, int y, BufferedImage img, Object sharedArray) {
         // border strategy: continue with the color of the outermost pixel,
-        return img.getRGB(
-                Utils.clamp(x, 0, img.getWidth() - 1),
-                Utils.clamp(y, 0, img.getHeight() - 1));
+        final int rx = Utils.clamp(x, 0, img.getWidth() - 1);
+        final int ry = Utils.clamp(y, 0, img.getHeight() - 1);
+        if (sharedArray == null) {
+            return img.getRGB(rx, ry);
+        }
+        return img.getColorModel().getRGB(img.getRaster().getDataElements(rx, ry, sharedArray));
     }
 }
