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