| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.tools;
|
|---|
| 3 |
|
|---|
| 4 | import java.awt.Color;
|
|---|
| 5 | import java.awt.Dimension;
|
|---|
| 6 | import java.awt.geom.Point2D;
|
|---|
| 7 | import java.awt.geom.Rectangle2D;
|
|---|
| 8 | import java.awt.image.BufferedImage;
|
|---|
| 9 |
|
|---|
| 10 | /**
|
|---|
| 11 | * Image warping algorithm.
|
|---|
| 12 | *
|
|---|
| 13 | * Deforms an image geometrically according to a given transformation formula.
|
|---|
| 14 | * @since 11858
|
|---|
| 15 | */
|
|---|
| 16 | public class ImageWarp {
|
|---|
| 17 |
|
|---|
| 18 | /**
|
|---|
| 19 | * Transformation that translates the pixel coordinates.
|
|---|
| 20 | */
|
|---|
| 21 | public interface PointTransform {
|
|---|
| 22 | Point2D transform(Point2D pt);
|
|---|
| 23 | }
|
|---|
| 24 |
|
|---|
| 25 | /**
|
|---|
| 26 | * Interpolation method.
|
|---|
| 27 | */
|
|---|
| 28 | public enum Interpolation {
|
|---|
| 29 | /**
|
|---|
| 30 | * Nearest neighbor.
|
|---|
| 31 | *
|
|---|
| 32 | * Simplest possible method. Faster, but not very good quality.
|
|---|
| 33 | */
|
|---|
| 34 | NEAREST_NEIGHBOR(1),
|
|---|
| 35 | /**
|
|---|
| 36 | * Bilinear.
|
|---|
| 37 | *
|
|---|
| 38 | * Decent quality.
|
|---|
| 39 | */
|
|---|
| 40 | BILINEAR(2);
|
|---|
| 41 |
|
|---|
| 42 | private final int margin;
|
|---|
| 43 |
|
|---|
| 44 | private Interpolation(int margin) {
|
|---|
| 45 | this.margin = margin;
|
|---|
| 46 | }
|
|---|
| 47 |
|
|---|
| 48 | /**
|
|---|
| 49 | * Number of pixels to scan outside the source image.
|
|---|
| 50 | * Used to get smoother borders.
|
|---|
| 51 | * @return the margin
|
|---|
| 52 | */
|
|---|
| 53 | public int getMargin() {
|
|---|
| 54 | return margin;
|
|---|
| 55 | }
|
|---|
| 56 | }
|
|---|
| 57 |
|
|---|
| 58 | /**
|
|---|
| 59 | * Warp an image.
|
|---|
| 60 | * @param srcImg the original image
|
|---|
| 61 | * @param targetDim dimension of the target image
|
|---|
| 62 | * @param invTransform inverse transformation (translates pixel coordinates
|
|---|
| 63 | * of the target image to pixel coordinates of the original image)
|
|---|
| 64 | * @param interpolation the interpolation method
|
|---|
| 65 | * @return the warped image
|
|---|
| 66 | */
|
|---|
| 67 | public static BufferedImage warp(BufferedImage srcImg, Dimension targetDim, PointTransform invTransform, Interpolation interpolation) {
|
|---|
| 68 | BufferedImage imgTarget = new BufferedImage(targetDim.width, targetDim.height, BufferedImage.TYPE_INT_ARGB);
|
|---|
| 69 | Rectangle2D srcRect = new Rectangle2D.Double(0, 0, srcImg.getWidth(), srcImg.getHeight());
|
|---|
| 70 | for (int j = 0; j < imgTarget.getHeight(); j++) {
|
|---|
| 71 | for (int i = 0; i < imgTarget.getWidth(); i++) {
|
|---|
| 72 | Point2D srcCoord = invTransform.transform(new Point2D.Double(i, j));
|
|---|
| 73 | if (isInside(srcCoord, srcRect, interpolation.getMargin())) {
|
|---|
| 74 | int rgb;
|
|---|
| 75 | switch (interpolation) {
|
|---|
| 76 | case NEAREST_NEIGHBOR:
|
|---|
| 77 | rgb = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg).getRGB();
|
|---|
| 78 | break;
|
|---|
| 79 | case BILINEAR:
|
|---|
| 80 | int x0 = (int) Math.floor(srcCoord.getX());
|
|---|
| 81 | double dx = srcCoord.getX() - x0;
|
|---|
| 82 | int y0 = (int) Math.floor(srcCoord.getY());
|
|---|
| 83 | double dy = srcCoord.getY() - y0;
|
|---|
| 84 | Color c00 = getColor(x0, y0, srcImg);
|
|---|
| 85 | Color c01 = getColor(x0, y0 + 1, srcImg);
|
|---|
| 86 | Color c10 = getColor(x0 + 1, y0, srcImg);
|
|---|
| 87 | Color c11 = getColor(x0 + 1, y0 + 1, srcImg);
|
|---|
| 88 | int red = (int) Math.round(
|
|---|
| 89 | (c00.getRed() * (1-dx) + c10.getRed() * dx) * (1-dy) +
|
|---|
| 90 | (c01.getRed() * (1-dx) + c11.getRed() * dx) * dy);
|
|---|
| 91 | int green = (int) Math.round(
|
|---|
| 92 | (c00.getGreen()* (1-dx) + c10.getGreen() * dx) * (1-dy) +
|
|---|
| 93 | (c01.getGreen() * (1-dx) + c11.getGreen() * dx) * dy);
|
|---|
| 94 | int blue = (int) Math.round(
|
|---|
| 95 | (c00.getBlue()* (1-dx) + c10.getBlue() * dx) * (1-dy) +
|
|---|
| 96 | (c01.getBlue() * (1-dx) + c11.getBlue() * dx) * dy);
|
|---|
| 97 | int alpha = (int) Math.round(
|
|---|
| 98 | (c00.getAlpha()* (1-dx) + c10.getAlpha() * dx) * (1-dy) +
|
|---|
| 99 | (c01.getAlpha() * (1-dx) + c11.getAlpha() * dx) * dy);
|
|---|
| 100 | rgb = new Color(red, green, blue, alpha).getRGB();
|
|---|
| 101 | break;
|
|---|
| 102 | default:
|
|---|
| 103 | throw new AssertionError();
|
|---|
| 104 | }
|
|---|
| 105 | imgTarget.setRGB(i, j, rgb);
|
|---|
| 106 | }
|
|---|
| 107 | }
|
|---|
| 108 | }
|
|---|
| 109 | return imgTarget;
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | private static boolean isInside(Point2D p, Rectangle2D rect, double margin) {
|
|---|
| 113 | return isInside(p.getX(), rect.getMinX(), rect.getMaxX(), margin) &&
|
|---|
| 114 | isInside(p.getY(), rect.getMinY(), rect.getMaxY(), margin);
|
|---|
| 115 | }
|
|---|
| 116 |
|
|---|
| 117 | private static boolean isInside(double x, double xMin, double xMax, double margin) {
|
|---|
| 118 | return x + margin >= xMin && x - margin <= xMax;
|
|---|
| 119 | }
|
|---|
| 120 |
|
|---|
| 121 | private static Color getColor(int x, int y, BufferedImage img) {
|
|---|
| 122 | // border strategy: continue with the color of the outermost pixel,
|
|---|
| 123 | // but change alpha component to fully translucent
|
|---|
| 124 | boolean transparent = false;
|
|---|
| 125 | if (x < 0) {
|
|---|
| 126 | x = 0;
|
|---|
| 127 | transparent = true;
|
|---|
| 128 | } else if (x >= img.getWidth()) {
|
|---|
| 129 | x = img.getWidth() - 1;
|
|---|
| 130 | transparent = true;
|
|---|
| 131 | }
|
|---|
| 132 | if (y < 0) {
|
|---|
| 133 | y = 0;
|
|---|
| 134 | transparent = true;
|
|---|
| 135 | } else if (y >= img.getHeight()) {
|
|---|
| 136 | y = img.getHeight() - 1;
|
|---|
| 137 | transparent = true;
|
|---|
| 138 | }
|
|---|
| 139 | Color clr = new Color(img.getRGB(x, y));
|
|---|
| 140 | if (!transparent)
|
|---|
| 141 | return clr;
|
|---|
| 142 | // keep color components, but set transparency to 0
|
|---|
| 143 | // (the idea is that border fades out and mixes with next tile)
|
|---|
| 144 | return new Color(clr.getRed(), clr.getGreen(), clr.getBlue(), 0);
|
|---|
| 145 | }
|
|---|
| 146 | }
|
|---|