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

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

see #7427 - optimize warp transformaion

performance problems should be resolved now

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