[11858] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.tools;
|
---|
| 3 |
|
---|
| 4 | import java.awt.Dimension;
|
---|
| 5 | import java.awt.geom.Point2D;
|
---|
| 6 | import java.awt.geom.Rectangle2D;
|
---|
| 7 | import java.awt.image.BufferedImage;
|
---|
[11897] | 8 | import java.util.HashMap;
|
---|
| 9 | import java.util.HashSet;
|
---|
| 10 | import java.util.Map;
|
---|
| 11 | import java.util.Set;
|
---|
[11858] | 12 |
|
---|
[11915] | 13 | import org.openstreetmap.josm.Main;
|
---|
| 14 |
|
---|
[11858] | 15 | /**
|
---|
| 16 | * Image warping algorithm.
|
---|
| 17 | *
|
---|
| 18 | * Deforms an image geometrically according to a given transformation formula.
|
---|
[11866] | 19 | * @since 11858
|
---|
[11858] | 20 | */
|
---|
| 21 | public class ImageWarp {
|
---|
| 22 |
|
---|
| 23 | /**
|
---|
| 24 | * Transformation that translates the pixel coordinates.
|
---|
| 25 | */
|
---|
[11866] | 26 | public interface PointTransform {
|
---|
[11971] | 27 | /**
|
---|
| 28 | * Translates pixel coordinates.
|
---|
| 29 | * @param pt pixel coordinates
|
---|
| 30 | * @return transformed pixel coordinates
|
---|
| 31 | */
|
---|
[11858] | 32 | Point2D transform(Point2D pt);
|
---|
| 33 | }
|
---|
| 34 |
|
---|
| 35 | /**
|
---|
[11897] | 36 | * Wrapper that optimizes a given {@link ImageWarp.PointTransform}.
|
---|
| 37 | *
|
---|
| 38 | * It does so by spanning a grid with certain step size. It will invoke the
|
---|
| 39 | * potentially expensive master transform only at those grid points and use
|
---|
| 40 | * bilinear interpolation to approximate transformed values in between.
|
---|
| 41 | * <p>
|
---|
| 42 | * For memory optimization, this class assumes that rows are more or less scanned
|
---|
| 43 | * one-by-one as is done in {@link ImageWarp#warp}. I.e. this transform is <em>not</em>
|
---|
| 44 | * random access in the y coordinate.
|
---|
| 45 | */
|
---|
| 46 | public static class GridTransform implements ImageWarp.PointTransform {
|
---|
| 47 |
|
---|
| 48 | private final double stride;
|
---|
| 49 | private final ImageWarp.PointTransform trfm;
|
---|
| 50 |
|
---|
| 51 | private final Map<Integer, Map<Integer, Point2D>> cache;
|
---|
| 52 |
|
---|
[11915] | 53 | private final boolean consistencyTest;
|
---|
[11897] | 54 | private final Set<Integer> deletedRows;
|
---|
| 55 |
|
---|
| 56 | /**
|
---|
| 57 | * Create a new GridTransform.
|
---|
| 58 | * @param trfm the master transform, that needs to be optimized
|
---|
| 59 | * @param stride step size
|
---|
| 60 | */
|
---|
| 61 | public GridTransform(ImageWarp.PointTransform trfm, double stride) {
|
---|
| 62 | this.trfm = trfm;
|
---|
| 63 | this.stride = stride;
|
---|
| 64 | this.cache = new HashMap<>();
|
---|
[11915] | 65 | this.consistencyTest = Main.isDebugEnabled();
|
---|
| 66 | if (consistencyTest) {
|
---|
[11897] | 67 | deletedRows = new HashSet<>();
|
---|
| 68 | } else {
|
---|
| 69 | deletedRows = null;
|
---|
| 70 | }
|
---|
| 71 | }
|
---|
| 72 |
|
---|
| 73 | @Override
|
---|
| 74 | public Point2D transform(Point2D pt) {
|
---|
| 75 | int xIdx = (int) Math.floor(pt.getX() / stride);
|
---|
| 76 | int yIdx = (int) Math.floor(pt.getY() / stride);
|
---|
| 77 | double dx = pt.getX() / stride - xIdx;
|
---|
| 78 | double dy = pt.getY() / stride - yIdx;
|
---|
| 79 | Point2D value00 = getValue(xIdx, yIdx);
|
---|
| 80 | Point2D value01 = getValue(xIdx, yIdx + 1);
|
---|
| 81 | Point2D value10 = getValue(xIdx + 1, yIdx);
|
---|
| 82 | Point2D value11 = getValue(xIdx + 1, yIdx + 1);
|
---|
| 83 | double valueX = (value00.getX() * (1-dx) + value10.getX() * dx) * (1-dy) +
|
---|
| 84 | (value01.getX() * (1-dx) + value11.getX() * dx) * dy;
|
---|
| 85 | double valueY = (value00.getY() * (1-dx) + value10.getY() * dx) * (1-dy) +
|
---|
| 86 | (value01.getY() * (1-dx) + value11.getY() * dx) * dy;
|
---|
| 87 | return new Point2D.Double(valueX, valueY);
|
---|
| 88 | }
|
---|
| 89 |
|
---|
| 90 | private Point2D getValue(int xIdx, int yIdx) {
|
---|
| 91 | Map<Integer, Point2D> row = getRow(yIdx);
|
---|
| 92 | Point2D val = row.get(xIdx);
|
---|
| 93 | if (val == null) {
|
---|
| 94 | val = trfm.transform(new Point2D.Double(xIdx * stride, yIdx * stride));
|
---|
| 95 | row.put(xIdx, val);
|
---|
| 96 | }
|
---|
| 97 | return val;
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | private Map<Integer, Point2D> getRow(int yIdx) {
|
---|
[11915] | 101 | cleanUp(yIdx - 3);
|
---|
[11897] | 102 | Map<Integer, Point2D> row = cache.get(yIdx);
|
---|
| 103 | if (row == null) {
|
---|
| 104 | row = new HashMap<>();
|
---|
| 105 | cache.put(yIdx, row);
|
---|
[11915] | 106 | if (consistencyTest) {
|
---|
[11897] | 107 | // should not create a row that has been deleted before
|
---|
| 108 | if (deletedRows.contains(yIdx)) throw new AssertionError();
|
---|
[11915] | 109 | // only ever cache 3 rows at once
|
---|
| 110 | if (cache.size() > 3) throw new AssertionError();
|
---|
[11897] | 111 | }
|
---|
| 112 | }
|
---|
| 113 | return row;
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | // remove rows from cache that will no longer be used
|
---|
| 117 | private void cleanUp(int yIdx) {
|
---|
| 118 | Map<Integer, Point2D> del = cache.remove(yIdx);
|
---|
[11915] | 119 | if (consistencyTest && del != null) {
|
---|
[11897] | 120 | // should delete each row only once
|
---|
| 121 | if (deletedRows.contains(yIdx)) throw new AssertionError();
|
---|
| 122 | deletedRows.add(yIdx);
|
---|
| 123 | }
|
---|
| 124 | }
|
---|
| 125 | }
|
---|
| 126 |
|
---|
| 127 | /**
|
---|
[11858] | 128 | * Interpolation method.
|
---|
| 129 | */
|
---|
| 130 | public enum Interpolation {
|
---|
| 131 | /**
|
---|
| 132 | * Nearest neighbor.
|
---|
| 133 | *
|
---|
| 134 | * Simplest possible method. Faster, but not very good quality.
|
---|
| 135 | */
|
---|
[11972] | 136 | NEAREST_NEIGHBOR,
|
---|
| 137 |
|
---|
[11858] | 138 | /**
|
---|
| 139 | * Bilinear.
|
---|
| 140 | *
|
---|
| 141 | * Decent quality.
|
---|
| 142 | */
|
---|
[11972] | 143 | BILINEAR;
|
---|
[11858] | 144 | }
|
---|
| 145 |
|
---|
| 146 | /**
|
---|
| 147 | * Warp an image.
|
---|
| 148 | * @param srcImg the original image
|
---|
| 149 | * @param targetDim dimension of the target image
|
---|
| 150 | * @param invTransform inverse transformation (translates pixel coordinates
|
---|
| 151 | * of the target image to pixel coordinates of the original image)
|
---|
| 152 | * @param interpolation the interpolation method
|
---|
| 153 | * @return the warped image
|
---|
| 154 | */
|
---|
| 155 | public static BufferedImage warp(BufferedImage srcImg, Dimension targetDim, PointTransform invTransform, Interpolation interpolation) {
|
---|
| 156 | BufferedImage imgTarget = new BufferedImage(targetDim.width, targetDim.height, BufferedImage.TYPE_INT_ARGB);
|
---|
| 157 | Rectangle2D srcRect = new Rectangle2D.Double(0, 0, srcImg.getWidth(), srcImg.getHeight());
|
---|
| 158 | for (int j = 0; j < imgTarget.getHeight(); j++) {
|
---|
| 159 | for (int i = 0; i < imgTarget.getWidth(); i++) {
|
---|
| 160 | Point2D srcCoord = invTransform.transform(new Point2D.Double(i, j));
|
---|
[11954] | 161 | if (srcRect.contains(srcCoord)) {
|
---|
[11895] | 162 | int rgba;
|
---|
| 163 | switch (interpolation) {
|
---|
| 164 | case NEAREST_NEIGHBOR:
|
---|
| 165 | rgba = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg);
|
---|
| 166 | break;
|
---|
| 167 | case BILINEAR:
|
---|
| 168 | int x0 = (int) Math.floor(srcCoord.getX());
|
---|
| 169 | double dx = srcCoord.getX() - x0;
|
---|
| 170 | int y0 = (int) Math.floor(srcCoord.getY());
|
---|
| 171 | double dy = srcCoord.getY() - y0;
|
---|
| 172 | int c00 = getColor(x0, y0, srcImg);
|
---|
| 173 | int c01 = getColor(x0, y0 + 1, srcImg);
|
---|
| 174 | int c10 = getColor(x0 + 1, y0, srcImg);
|
---|
| 175 | int c11 = getColor(x0 + 1, y0 + 1, srcImg);
|
---|
| 176 | rgba = 0;
|
---|
| 177 | // loop over color components: blue, green, red, alpha
|
---|
| 178 | for (int ch = 0; ch <= 3; ch++) {
|
---|
| 179 | int shift = 8 * ch;
|
---|
| 180 | int chVal = (int) Math.round(
|
---|
| 181 | (((c00 >> shift) & 0xff) * (1-dx) + ((c10 >> shift) & 0xff) * dx) * (1-dy) +
|
---|
| 182 | (((c01 >> shift) & 0xff) * (1-dx) + ((c11 >> shift) & 0xff) * dx) * dy);
|
---|
| 183 | rgba |= chVal << shift;
|
---|
| 184 | }
|
---|
| 185 | break;
|
---|
| 186 | default:
|
---|
| 187 | throw new AssertionError();
|
---|
| 188 | }
|
---|
| 189 | imgTarget.setRGB(i, j, rgba);
|
---|
[11858] | 190 | }
|
---|
| 191 | }
|
---|
| 192 | }
|
---|
| 193 | return imgTarget;
|
---|
| 194 | }
|
---|
| 195 |
|
---|
[11895] | 196 | private static int getColor(int x, int y, BufferedImage img) {
|
---|
[11858] | 197 | // border strategy: continue with the color of the outermost pixel,
|
---|
[11954] | 198 | return img.getRGB(
|
---|
| 199 | Utils.clamp(x, 0, img.getWidth() - 1),
|
---|
| 200 | Utils.clamp(y, 0, img.getHeight() - 1));
|
---|
[11858] | 201 | }
|
---|
| 202 | }
|
---|