001 /* 002 * 003 004 Licensed under the Apache License, Version 2.0 (the "License"); 005 you may not use this file except in compliance with the License. 006 You may obtain a copy of the License at 007 008 http://www.apache.org/licenses/LICENSE-2.0 009 010 Unless required by applicable law or agreed to in writing, software 011 distributed under the License is distributed on an "AS IS" BASIS, 012 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 See the License for the specific language governing permissions and 014 limitations under the License. 015 */ 016 017 package railo.runtime.img.filter;import java.awt.image.BufferedImage; 018 019 import railo.runtime.engine.ThreadLocalPageContext; 020 import railo.runtime.exp.FunctionException; 021 import railo.runtime.exp.PageException; 022 import railo.runtime.img.ImageUtil; 023 import railo.runtime.img.math.FFT; 024 import railo.runtime.type.KeyImpl; 025 import railo.runtime.type.Struct; 026 import railo.runtime.type.util.CollectionUtil; 027 028 /** 029 * A filter which use FFTs to simulate lens blur on an image. 030 */ 031 public class LensBlurFilter extends AbstractBufferedImageOp implements DynFiltering { 032 033 private float radius = 10; 034 private float bloom = 2; 035 private float bloomThreshold = 192; 036 private float angle = 0; 037 private int sides = 5; 038 039 /** 040 * Set the radius of the kernel, and hence the amount of blur. 041 * @param radius the radius of the blur in pixels. 042 * @see #getRadius 043 */ 044 public void setRadius(float radius) { 045 this.radius = radius; 046 } 047 048 /** 049 * Get the radius of the kernel. 050 * @return the radius 051 * @see #setRadius 052 */ 053 public float getRadius() { 054 return radius; 055 } 056 057 /** 058 * Set the number of sides of the aperture. 059 * @param sides the number of sides 060 * @see #getSides 061 */ 062 public void setSides(int sides) { 063 this.sides = sides; 064 } 065 066 /** 067 * Get the number of sides of the aperture. 068 * @return the number of sides 069 * @see #setSides 070 */ 071 public int getSides() { 072 return sides; 073 } 074 075 /** 076 * Set the bloom factor. 077 * @param bloom the bloom factor 078 * @see #getBloom 079 */ 080 public void setBloom(float bloom) { 081 this.bloom = bloom; 082 } 083 084 /** 085 * Get the bloom factor. 086 * @return the bloom factor 087 * @see #setBloom 088 */ 089 public float getBloom() { 090 return bloom; 091 } 092 093 /** 094 * Set the bloom threshold. 095 * @param bloomThreshold the bloom threshold 096 * @see #getBloomThreshold 097 */ 098 public void setBloomThreshold(float bloomThreshold) { 099 this.bloomThreshold = bloomThreshold; 100 } 101 102 /** 103 * Get the bloom threshold. 104 * @return the bloom threshold 105 * @see #setBloomThreshold 106 */ 107 public float getBloomThreshold() { 108 return bloomThreshold; 109 } 110 111 112 public BufferedImage filter( BufferedImage src, BufferedImage dst ) { 113 int width = src.getWidth(); 114 int height = src.getHeight(); 115 int rows = 1, cols = 1; 116 int log2rows = 0, log2cols = 0; 117 int iradius = (int)Math.ceil(radius); 118 int tileWidth = 128; 119 int tileHeight = tileWidth; 120 121 //int adjustedWidth =(width + iradius*2); 122 //int adjustedHeight =(height + iradius*2); 123 124 tileWidth = iradius < 32 ? Math.min(128, width+2*iradius) : Math.min(256, width+2*iradius); 125 tileHeight = iradius < 32 ? Math.min(128, height+2*iradius) : Math.min(256, height+2*iradius); 126 127 if ( dst == null ) 128 dst = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB ); 129 130 while (rows < tileHeight) { 131 rows *= 2; 132 log2rows++; 133 } 134 while (cols < tileWidth) { 135 cols *= 2; 136 log2cols++; 137 } 138 int w = cols; 139 int h = rows; 140 141 tileWidth = w; 142 tileHeight = h;//FIXME-tileWidth, w, and cols are always all the same 143 144 FFT fft = new FFT( Math.max(log2rows, log2cols) ); 145 146 int[] rgb = new int[w*h]; 147 float[][] mask = new float[2][w*h]; 148 float[][] gb = new float[2][w*h]; 149 float[][] ar = new float[2][w*h]; 150 151 // Create the kernel 152 double polyAngle = Math.PI/sides; 153 double polyScale = 1.0f / Math.cos(polyAngle); 154 double r2 = radius*radius; 155 double rangle = Math.toRadians(angle); 156 float total = 0; 157 int i = 0; 158 for ( int y = 0; y < h; y++ ) { 159 for ( int x = 0; x < w; x++ ) { 160 double dx = x-w/2f; 161 double dy = y-h/2f; 162 double r = dx*dx+dy*dy; 163 double f = r < r2 ? 1 : 0; 164 if (f != 0) { 165 r = Math.sqrt(r); 166 if ( sides != 0 ) { 167 double a = Math.atan2(dy, dx)+rangle; 168 a = ImageMath.mod(a, polyAngle*2)-polyAngle; 169 f = Math.cos(a) * polyScale; 170 } else 171 f = 1; 172 f = f*r < radius ? 1 : 0; 173 } 174 total += (float)f; 175 176 mask[0][i] = (float)f; 177 mask[1][i] = 0; 178 i++; 179 } 180 } 181 182 // Normalize the kernel 183 i = 0; 184 for ( int y = 0; y < h; y++ ) { 185 for ( int x = 0; x < w; x++ ) { 186 mask[0][i] /= total; 187 i++; 188 } 189 } 190 191 fft.transform2D( mask[0], mask[1], w, h, true ); 192 193 for ( int tileY = -iradius; tileY < height; tileY += tileHeight-2*iradius ) { 194 for ( int tileX = -iradius; tileX < width; tileX += tileWidth-2*iradius ) { 195 // System.out.println("Tile: "+tileX+" "+tileY+" "+tileWidth+" "+tileHeight); 196 197 // Clip the tile to the image bounds 198 int tx = tileX, ty = tileY, tw = tileWidth, th = tileHeight; 199 int fx = 0, fy = 0; 200 if ( tx < 0 ) { 201 tw += tx; 202 fx -= tx; 203 tx = 0; 204 } 205 if ( ty < 0 ) { 206 th += ty; 207 fy -= ty; 208 ty = 0; 209 } 210 if ( tx+tw > width ) 211 tw = width-tx; 212 if ( ty+th > height ) 213 th = height-ty; 214 src.getRGB( tx, ty, tw, th, rgb, fy*w+fx, w ); 215 216 // Create a float array from the pixels. Any pixels off the edge of the source image get duplicated from the edge. 217 i = 0; 218 for ( int y = 0; y < h; y++ ) { 219 int imageY = y+tileY; 220 int j; 221 if ( imageY < 0 ) 222 j = fy; 223 else if ( imageY > height ) 224 j = fy+th-1; 225 else 226 j = y; 227 j *= w; 228 for ( int x = 0; x < w; x++ ) { 229 int imageX = x+tileX; 230 int k; 231 if ( imageX < 0 ) 232 k = fx; 233 else if ( imageX > width ) 234 k = fx+tw-1; 235 else 236 k = x; 237 k += j; 238 239 ar[0][i] = ((rgb[k] >> 24) & 0xff); 240 float r = ((rgb[k] >> 16) & 0xff); 241 float g = ((rgb[k] >> 8) & 0xff); 242 float b = (rgb[k] & 0xff); 243 244 // Bloom... 245 if ( r > bloomThreshold ) 246 r *= bloom; 247 // r = bloomThreshold + (r-bloomThreshold) * bloom; 248 if ( g > bloomThreshold ) 249 g *= bloom; 250 // g = bloomThreshold + (g-bloomThreshold) * bloom; 251 if ( b > bloomThreshold ) 252 b *= bloom; 253 // b = bloomThreshold + (b-bloomThreshold) * bloom; 254 255 ar[1][i] = r; 256 gb[0][i] = g; 257 gb[1][i] = b; 258 259 i++; 260 k++; 261 } 262 } 263 264 // Transform into frequency space 265 fft.transform2D( ar[0], ar[1], cols, rows, true ); 266 fft.transform2D( gb[0], gb[1], cols, rows, true ); 267 268 // Multiply the transformed pixels by the transformed kernel 269 i = 0; 270 for ( int y = 0; y < h; y++ ) { 271 for ( int x = 0; x < w; x++ ) { 272 float re = ar[0][i]; 273 float im = ar[1][i]; 274 float rem = mask[0][i]; 275 float imm = mask[1][i]; 276 ar[0][i] = re*rem-im*imm; 277 ar[1][i] = re*imm+im*rem; 278 279 re = gb[0][i]; 280 im = gb[1][i]; 281 gb[0][i] = re*rem-im*imm; 282 gb[1][i] = re*imm+im*rem; 283 i++; 284 } 285 } 286 287 // Transform back 288 fft.transform2D( ar[0], ar[1], cols, rows, false ); 289 fft.transform2D( gb[0], gb[1], cols, rows, false ); 290 291 // Convert back to RGB pixels, with quadrant remapping 292 int row_flip = w >> 1; 293 int col_flip = h >> 1; 294 int index = 0; 295 296 //FIXME-don't bother converting pixels off image edges 297 for ( int y = 0; y < w; y++ ) { 298 int ym = y ^ row_flip; 299 int yi = ym*cols; 300 for ( int x = 0; x < w; x++ ) { 301 int xm = yi + (x ^ col_flip); 302 int a = (int)ar[0][xm]; 303 int r = (int)ar[1][xm]; 304 int g = (int)gb[0][xm]; 305 int b = (int)gb[1][xm]; 306 307 // Clamp high pixels due to blooming 308 if ( r > 255 ) 309 r = 255; 310 if ( g > 255 ) 311 g = 255; 312 if ( b > 255 ) 313 b = 255; 314 int argb = (a << 24) | (r << 16) | (g << 8) | b; 315 rgb[index++] = argb; 316 } 317 } 318 319 // Clip to the output image 320 tx = tileX+iradius; 321 ty = tileY+iradius; 322 tw = tileWidth-2*iradius; 323 th = tileHeight-2*iradius; 324 if ( tx+tw > width ) 325 tw = width-tx; 326 if ( ty+th > height ) 327 th = height-ty; 328 dst.setRGB( tx, ty, tw, th, rgb, iradius*w+iradius, w ); 329 } 330 } 331 return dst; 332 } 333 334 public String toString() { 335 return "Blur/Lens Blur..."; 336 } 337 public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src); 338 Object o; 339 if((o=parameters.removeEL(KeyImpl.init("Radius")))!=null)setRadius(ImageFilterUtil.toFloatValue(o,"Radius")); 340 if((o=parameters.removeEL(KeyImpl.init("Sides")))!=null)setSides(ImageFilterUtil.toIntValue(o,"Sides")); 341 if((o=parameters.removeEL(KeyImpl.init("Bloom")))!=null)setBloom(ImageFilterUtil.toFloatValue(o,"Bloom")); 342 if((o=parameters.removeEL(KeyImpl.init("BloomThreshold")))!=null)setBloomThreshold(ImageFilterUtil.toFloatValue(o,"BloomThreshold")); 343 344 // check for arguments not supported 345 if(parameters.size()>0) { 346 throw new FunctionException(ThreadLocalPageContext.get(), "ImageFilter", 3, "parameters", "the parameter"+(parameters.size()>1?"s":"")+" ["+CollectionUtil.getKeyList(parameters,", ")+"] "+(parameters.size()>1?"are":"is")+" not allowed, only the following parameters are supported [Radius, Sides, Bloom, BloomThreshold]"); 347 } 348 349 return filter(src, dst); 350 } 351 }