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    }