source: josm/trunk/src/org/openstreetmap/josm/tools/ImageWarp.java@ 11944

Last change on this file since 11944 was 11915, checked in by bastiK, 7 years ago

see #7427 - run consistency test in debug mode

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