| 1 | // Copyright 2005 Huxtable.com. All rights reserved. |
| 2 | // For more information, see <http://www.jhlabs.com/ip/blurring.html>. |
| 3 | package com.jhlabs.image; |
| 4 | |
| 5 | import java.awt.Rectangle; |
| 6 | import java.awt.RenderingHints; |
| 7 | import java.awt.geom.Point2D; |
| 8 | import java.awt.geom.Rectangle2D; |
| 9 | import java.awt.image.BufferedImage; |
| 10 | import java.awt.image.ColorModel; |
| 11 | import java.awt.image.Kernel; |
| 12 | |
| 13 | /** |
| 14 | * A filter which applies a convolution kernel to an image. |
| 15 | * |
| 16 | * @author Jerry Huxtable |
| 17 | */ |
| 18 | public class ConvolveFilter extends AbstractBufferedImageOp |
| 19 | { |
| 20 | public static final int CLAMP_EDGES = 1; |
| 21 | public static final int WRAP_EDGES = 2; |
| 22 | public static final int ZERO_EDGES = 0; |
| 23 | static final long serialVersionUID = 2239251672685254626L; |
| 24 | |
| 25 | private boolean alpha = true; |
| 26 | private int edgeAction = CLAMP_EDGES; |
| 27 | private Kernel kernel; |
| 28 | |
| 29 | /** |
| 30 | * Construct a filter with a null kernel. This is only useful if you're going to change the |
| 31 | * kernel later on. |
| 32 | */ |
| 33 | public ConvolveFilter() { |
| 34 | this(new float[9]); |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * Construct a filter with the given 3x3 kernel. |
| 39 | * |
| 40 | * @param matrix an array of 9 floats containing the kernel |
| 41 | */ |
| 42 | public ConvolveFilter(float[] matrix) { |
| 43 | this(new Kernel(3, 3, matrix)); |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * Construct a filter with the given kernel. |
| 48 | * |
| 49 | * @param rows the number of rows in the kernel |
| 50 | * @param cols the number of columns in the kernel |
| 51 | * @param matrix an array of rows*cols floats containing the kernel |
| 52 | */ |
| 53 | public ConvolveFilter(int rows, int cols, float[] matrix) { |
| 54 | this(new Kernel(cols, rows, matrix)); |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Construct a filter with the given 3x3 kernel. |
| 59 | * |
| 60 | * @param kernel Description of the parameter |
| 61 | */ |
| 62 | public ConvolveFilter(Kernel kernel) { |
| 63 | this.kernel = kernel; |
| 64 | } |
| 65 | |
| 66 | public void setEdgeAction(int edgeAction) { |
| 67 | this.edgeAction = edgeAction; |
| 68 | } |
| 69 | |
| 70 | public void setKernel(Kernel kernel) { |
| 71 | this.kernel = kernel; |
| 72 | } |
| 73 | |
| 74 | public Rectangle2D getBounds2D(BufferedImage src) { |
| 75 | return new Rectangle(0, 0, src.getWidth(), src.getHeight()); |
| 76 | } |
| 77 | |
| 78 | public int getEdgeAction() { |
| 79 | return edgeAction; |
| 80 | } |
| 81 | |
| 82 | public Kernel getKernel() { |
| 83 | return kernel; |
| 84 | } |
| 85 | |
| 86 | public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { |
| 87 | if (dstPt == null) { |
| 88 | dstPt = new Point2D.Double(); |
| 89 | } |
| 90 | dstPt.setLocation(srcPt.getX(), srcPt.getY()); |
| 91 | return dstPt; |
| 92 | } |
| 93 | |
| 94 | public RenderingHints getRenderingHints() { |
| 95 | return null; |
| 96 | } |
| 97 | |
| 98 | public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, int edgeAction) { |
| 99 | convolve(kernel, inPixels, outPixels, width, height, true, edgeAction); |
| 100 | } |
| 101 | |
| 102 | public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, |
| 103 | int edgeAction) { |
| 104 | if (kernel.getHeight() == 1) { |
| 105 | convolveH(kernel, inPixels, outPixels, width, height, alpha, edgeAction); |
| 106 | } else if (kernel.getWidth() == 1) { |
| 107 | convolveV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); |
| 108 | } else { |
| 109 | convolveHV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | /** |
| 114 | * Convolve with a kernel consisting of one row. |
| 115 | */ |
| 116 | public static void convolveH(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, |
| 117 | boolean alpha, int edgeAction) { |
| 118 | int index = 0; |
| 119 | float[] matrix = kernel.getKernelData(null); |
| 120 | int cols = kernel.getWidth(); |
| 121 | int cols2 = cols / 2; |
| 122 | |
| 123 | for (int y = 0; y < height; y++) { |
| 124 | int ioffset = y * width; |
| 125 | |
| 126 | for (int x = 0; x < width; x++) { |
| 127 | float r = 0; |
| 128 | float g = 0; |
| 129 | float b = 0; |
| 130 | float a = 0; |
| 131 | int moffset = cols2; |
| 132 | |
| 133 | for (int col = -cols2; col <= cols2; col++) { |
| 134 | float f = matrix[moffset + col]; |
| 135 | |
| 136 | if (f != 0) { |
| 137 | int ix = x + col; |
| 138 | |
| 139 | if (ix < 0) { |
| 140 | if (edgeAction == CLAMP_EDGES) { |
| 141 | ix = 0; |
| 142 | } else if (edgeAction == WRAP_EDGES) { |
| 143 | ix = (x + width) % width; |
| 144 | } |
| 145 | } else if (ix >= width) { |
| 146 | if (edgeAction == CLAMP_EDGES) { |
| 147 | ix = width - 1; |
| 148 | } else if (edgeAction == WRAP_EDGES) { |
| 149 | ix = (x + width) % width; |
| 150 | } |
| 151 | } |
| 152 | int rgb = inPixels[ioffset + ix]; |
| 153 | |
| 154 | a += f * ((rgb >> 24) & 0xff); |
| 155 | r += f * ((rgb >> 16) & 0xff); |
| 156 | g += f * ((rgb >> 8) & 0xff); |
| 157 | b += f * (rgb & 0xff); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | int ia = alpha ? ImageMath.clamp((int) (a + 0.5)) : 0xff; |
| 162 | int ir = ImageMath.clamp((int) (r + 0.5)); |
| 163 | int ig = ImageMath.clamp((int) (g + 0.5)); |
| 164 | int ib = ImageMath.clamp((int) (b + 0.5)); |
| 165 | |
| 166 | outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Convolve with a 2D kernel. |
| 173 | */ |
| 174 | public static void convolveHV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, |
| 175 | boolean alpha, int edgeAction) { |
| 176 | int index = 0; |
| 177 | float[] matrix = kernel.getKernelData(null); |
| 178 | int rows = kernel.getHeight(); |
| 179 | int cols = kernel.getWidth(); |
| 180 | int rows2 = rows / 2; |
| 181 | int cols2 = cols / 2; |
| 182 | |
| 183 | for (int y = 0; y < height; y++) { |
| 184 | for (int x = 0; x < width; x++) { |
| 185 | float r = 0; |
| 186 | float g = 0; |
| 187 | float b = 0; |
| 188 | float a = 0; |
| 189 | |
| 190 | for (int row = -rows2; row <= rows2; row++) { |
| 191 | int iy = y + row; |
| 192 | int ioffset; |
| 193 | |
| 194 | if (0 <= iy && iy < height) { |
| 195 | ioffset = iy * width; |
| 196 | } else if (edgeAction == CLAMP_EDGES) { |
| 197 | ioffset = y * width; |
| 198 | } else if (edgeAction == WRAP_EDGES) { |
| 199 | ioffset = ((iy + height) % height) * width; |
| 200 | } else { |
| 201 | continue; |
| 202 | } |
| 203 | |
| 204 | int moffset = cols * (row + rows2) + cols2; |
| 205 | |
| 206 | for (int col = -cols2; col <= cols2; col++) { |
| 207 | float f = matrix[moffset + col]; |
| 208 | |
| 209 | if (f != 0) { |
| 210 | int ix = x + col; |
| 211 | |
| 212 | if (!(0 <= ix && ix < width)) { |
| 213 | if (edgeAction == CLAMP_EDGES) { |
| 214 | ix = x; |
| 215 | } else if (edgeAction == WRAP_EDGES) { |
| 216 | ix = (x + width) % width; |
| 217 | } else { |
| 218 | continue; |
| 219 | } |
| 220 | } |
| 221 | int rgb = inPixels[ioffset + ix]; |
| 222 | |
| 223 | a += f * ((rgb >> 24) & 0xff); |
| 224 | r += f * ((rgb >> 16) & 0xff); |
| 225 | g += f * ((rgb >> 8) & 0xff); |
| 226 | b += f * (rgb & 0xff); |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | int ia = alpha ? ImageMath.clamp((int) (a + 0.5)) : 0xff; |
| 232 | int ir = ImageMath.clamp((int) (r + 0.5)); |
| 233 | int ig = ImageMath.clamp((int) (g + 0.5)); |
| 234 | int ib = ImageMath.clamp((int) (b + 0.5)); |
| 235 | |
| 236 | outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; |
| 237 | } |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * Convolve with a kernel consisting of one column. |
| 243 | */ |
| 244 | public static void convolveV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, |
| 245 | boolean alpha, int edgeAction) { |
| 246 | int index = 0; |
| 247 | float[] matrix = kernel.getKernelData(null); |
| 248 | int rows = kernel.getHeight(); |
| 249 | int rows2 = rows / 2; |
| 250 | |
| 251 | for (int y = 0; y < height; y++) { |
| 252 | for (int x = 0; x < width; x++) { |
| 253 | float r = 0; |
| 254 | float g = 0; |
| 255 | float b = 0; |
| 256 | float a = 0; |
| 257 | |
| 258 | for (int row = -rows2; row <= rows2; row++) { |
| 259 | int iy = y + row; |
| 260 | int ioffset; |
| 261 | |
| 262 | if (iy < 0) { |
| 263 | if (edgeAction == CLAMP_EDGES) { |
| 264 | ioffset = 0; |
| 265 | } else if (edgeAction == WRAP_EDGES) { |
| 266 | ioffset = ((y + height) % height) * width; |
| 267 | } else { |
| 268 | ioffset = iy * width; |
| 269 | } |
| 270 | } else if (iy >= height) { |
| 271 | if (edgeAction == CLAMP_EDGES) { |
| 272 | ioffset = (height - 1) * width; |
| 273 | } else if (edgeAction == WRAP_EDGES) { |
| 274 | ioffset = ((y + height) % height) * width; |
| 275 | } else { |
| 276 | ioffset = iy * width; |
| 277 | } |
| 278 | } else { |
| 279 | ioffset = iy * width; |
| 280 | } |
| 281 | |
| 282 | float f = matrix[row + rows2]; |
| 283 | |
| 284 | if (f != 0) { |
| 285 | int rgb = inPixels[ioffset + x]; |
| 286 | |
| 287 | a += f * ((rgb >> 24) & 0xff); |
| 288 | r += f * ((rgb >> 16) & 0xff); |
| 289 | g += f * ((rgb >> 8) & 0xff); |
| 290 | b += f * (rgb & 0xff); |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | int ia = alpha ? ImageMath.clamp((int) (a + 0.5)) : 0xff; |
| 295 | int ir = ImageMath.clamp((int) (r + 0.5)); |
| 296 | int ig = ImageMath.clamp((int) (g + 0.5)); |
| 297 | int ib = ImageMath.clamp((int) (b + 0.5)); |
| 298 | |
| 299 | outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) { |
| 305 | if (dstCM == null) { |
| 306 | dstCM = src.getColorModel(); |
| 307 | } |
| 308 | return new BufferedImage( |
| 309 | dstCM, |
| 310 | dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), |
| 311 | dstCM.isAlphaPremultiplied(), |
| 312 | null); |
| 313 | } |
| 314 | |
| 315 | public BufferedImage filter(BufferedImage src, BufferedImage dst) { |
| 316 | int width = src.getWidth(); |
| 317 | int height = src.getHeight(); |
| 318 | |
| 319 | if (dst == null) { |
| 320 | dst = createCompatibleDestImage(src, null); |
| 321 | } |
| 322 | |
| 323 | int[] inPixels = new int[width * height]; |
| 324 | int[] outPixels = new int[width * height]; |
| 325 | |
| 326 | getRGB(src, 0, 0, width, height, inPixels); |
| 327 | |
| 328 | convolve(kernel, inPixels, outPixels, width, height, alpha, edgeAction); |
| 329 | |
| 330 | setRGB(dst, 0, 0, width, height, outPixels); |
| 331 | return dst; |
| 332 | } |
| 333 | |
| 334 | public boolean hasAlpha() { |
| 335 | return alpha; |
| 336 | } |
| 337 | |
| 338 | public String toString() { |
| 339 | return "Blur/Convolve..."; |
| 340 | } |
| 341 | } |