001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030/**
031 * 
032 */
033package org.openimaj.tools.ocr;
034
035import java.awt.Font;
036import java.awt.GraphicsEnvironment;
037import java.util.ArrayList;
038import java.util.List;
039
040import org.openimaj.image.DisplayUtilities;
041import org.openimaj.image.FImage;
042import org.openimaj.image.Image;
043import org.openimaj.image.processing.transform.ProjectionProcessor;
044import org.openimaj.image.renderer.ImageRenderer;
045import org.openimaj.image.typography.general.GeneralFont;
046import org.openimaj.image.typography.general.GeneralFontRenderer;
047import org.openimaj.image.typography.general.GeneralFontStyle;
048import org.openimaj.math.geometry.point.Point2d;
049import org.openimaj.math.geometry.point.Point2dImpl;
050import org.openimaj.math.geometry.shape.Rectangle;
051import org.openimaj.math.geometry.transforms.TransformUtilities;
052import org.openimaj.util.pair.IndependentPair;
053
054import Jama.Matrix;
055
056/**
057 *      Class that will generate images containing a rendering of a String in
058 *      a random font that has been randomly affected - the idea is to simulate
059 *      real-world images in a controlled way for training or testing OCR.
060 *
061 *      @author David Dupplaw (dpd@ecs.soton.ac.uk)
062 *  @created 19 Aug 2011
063 *      
064 *
065 *      @param <Q> The pixel type
066 *      @param <I> The concrete image type
067 */
068public class FontSimulator<Q,I extends Image<Q,I>>
069{
070        /**
071         *      This is an interface for objects that are interested in listening
072         *      for when the {@link FontSimulator} creates an image during one of its
073         *      runs.
074         *
075         *      @author David Dupplaw (dpd@ecs.soton.ac.uk)
076         *  @created 19 Aug 2011
077         *      
078         *  @param <I> @param <I> Type of {@link Image}
079         */
080        public interface FontSimListener<I>
081        {
082                /**
083                 *      Called when an image is created during one of its runs.
084                 *      @param img The image containing the rendering of the text string
085                 */
086                public void imageCreated( I img );
087        }
088
089        /** The text string to draw on each simulation */
090        private String textString = null;
091
092        /** The number of characters being drawn */
093        private int nChars = 0;
094
095        /**
096         * The amount of size jitter to impose on the simulation.
097         * This value gives the number of points either side of the standard
098         * point size by which the size may be altered. i.e. if this value was
099         * 10 and the standard point size 72, the size may randomly alter between
100         * 62 and 82.
101         */
102        private double sizeJitter = 0;
103
104        /**
105         * The amount of angle jitter to impose on the simulation (in degrees).
106         * This value gives the maximum angle that may be applied to a transform
107         * of the font in either direction (clockwise or anti-clockwise). i.e.
108         * if this is given the value 45, the text may be rotated -45 to +45
109         * degrees (around its centre point).
110         */
111        private double angleJitter = 0;
112
113        /**
114         * The amount of shear jitter to impose on the simulation (in degrees)
115         * The value of this is the maximum angle by which the font may be randomly
116         * sheared in either direction. So if this variable is 35 degrees, the shear
117         * may randomly alter between -35 and +35 degrees.
118         */
119        private double shearJitter = 0;
120
121        /**
122         * The amount of perspective jitter to impose on the simulation.
123         * If this value is not zero, perspective is randomly added to the render.
124         * This is done by distorting the rectangle to a parallelogram. The side
125         * which is altered is chosen randomly and is altered randomly up to a
126         * maximum percentage reduction given by this variable. So if this variable
127         * is 0.5 (50%) then one side of the image will end up at a minimum size
128         * of 50% of its original size. The image is always shrunk.
129         */
130        private double perspectiveJitter = 0;
131
132        /** Whether to randomise the type of the font (plain,bold,italic) */
133        private boolean typeJitter = false;
134
135        /** A list of font family names to avoid (perhaps they contain symbols only? */
136        private List<String> fontsToAvoid = new ArrayList<String>();
137
138        /** The font size to draw the text in */
139        private int fontPointSize = 18;
140
141        /** The amount of padding (in pixels) to add around the edge of the render */
142        private int padding = 5;
143
144        /**
145         *      Create a font simulator that will output the given text string.
146         *      @param textString The text string to draw
147         */
148        public FontSimulator( final String textString )
149        {
150                this.setTextString( textString );
151        }
152
153        /**
154         *      Make a number of runs of creating text renders
155         * 
156         *      @param nRuns The number of runs to make
157         *      @param fsl The listener that will receive the images
158         *      @param imgExample An example of the type of image <I>
159         */
160        public void makeRuns( final int nRuns, final FontSimListener<I> fsl, final I imgExample )
161        {
162                for( int i = 0; i < nRuns; i++ )
163                {
164                        fsl.imageCreated( this.generate( imgExample ) );
165                }
166        }
167
168        /**
169         *      Generates a render of the text string in some random font with some
170         *      random transform.  The input image is not affected or used - it is only
171         *      used as a seed for generating new instances of that type of image. A new
172         *      image of the same type is returned.
173         * 
174         *      @param imgExample An example of the image to which to draw the text.
175         *      @return An image containing the rendered text
176         */
177        public I generate( final I imgExample )
178        {
179                // Get a random font from the system
180                GeneralFont font = this.pickRandomFont();
181                font = this.getJitteredFont( font );
182
183                // Create an image of the right size to fit the text into.
184                // To do that we must create an instance of an image renderer
185                // so that we can create a FontStyle. With the style we can create
186                // a FontRenderer which can give us the bounds of the text to draw.
187                // From that we can create an image into which the text will fit.
188                final ImageRenderer<Q, I> r = imgExample.createRenderer();
189
190                GeneralFontStyle<Q> gfs =
191                                new GeneralFontStyle<Q>( font, r, false );
192                gfs = this.getJitteredFontStyle( gfs );
193
194                final GeneralFontRenderer<Q> gfr = new GeneralFontRenderer<Q>();
195                final Rectangle b = gfr.getSize( this.textString, gfs );
196
197                // Create an image into which the text will fit.
198                I img = imgExample.newInstance(
199                                (int)b.width + this.padding*2,
200                                (int)b.height + this.padding*2 );
201
202                // Draw the characters to the image
203                img.drawText( this.textString, this.padding, (int)(this.padding + b.height), gfs );
204
205                // Transform the image
206                final Matrix transform = this.getJitterTransform( img );
207                img = ProjectionProcessor.project( img, transform );
208
209                return img;
210        }
211
212        /**
213         *      Picks a random font from your system and returns it. Checks
214         *      it against the fontsToAvoid list and will not return a font
215         *      that is on that list. If a font cannot be returned, the method
216         *      will return null. The returned font will have plain type and
217         *      a size of 18 point.
218         * 
219         *      @return A random Font from the system.
220         */
221        public GeneralFont pickRandomFont()
222        {
223                final List<String> fontNames = new ArrayList<String>();
224                final GraphicsEnvironment e = GraphicsEnvironment.getLocalGraphicsEnvironment();
225                for( final String font : e.getAvailableFontFamilyNames() )
226                        if( !this.fontsToAvoid.contains( font ) )
227                                fontNames.add( font );
228
229                if( fontNames.size() == 0 )
230                        return null;
231
232                final int r = (int)(Math.random() * fontNames.size());
233                return new GeneralFont( fontNames.get(r),
234                                java.awt.Font.PLAIN );
235        }
236
237        /**
238         *      Returns a font that is somewhat randomised from the
239         *      initial font. Side-affects the incoming font and returns it.
240         * 
241         *      @param font The font to jitter
242         *      @return The incoming font altered by jitter specification
243         */
244        public GeneralFont getJitteredFont( final GeneralFont font )
245        {
246                if( this.isTypeJitter() )
247                {
248                        final double r = Math.random();
249                        if( r > 0.3 )
250                                if( r > 0.6 )
251                                        font.setType( Font.PLAIN );
252                                else    font.setType( Font.BOLD );
253                        else            font.setType( Font.ITALIC );
254                }
255
256                return font;
257        }
258
259        /**
260         *      Returns an affine transform matrix based on the jitter
261         *      specifications.
262         * 
263         *  @param img image to get bounds from
264         * 
265         *      @return An affine transform matrix
266         */
267        public Matrix getJitterTransform( final I img )
268        {
269                Matrix m = Matrix.identity( 3, 3 );
270
271                if( this.angleJitter > 0 )
272                {
273                        final double r = this.angleJitter*(Math.random()*2-1) / 57.2957795;
274                        final Matrix rm = TransformUtilities.centeredRotationMatrix(
275                                        r, img.getWidth(), img.getHeight() );
276                        m = m.times( rm );
277                }
278
279                if( this.shearJitter > 0 )
280                {
281                        final double r = this.shearJitter*(Math.random()*2-1) / 57.2957795;
282                        final Matrix rm = new Matrix( new double[][]{
283                                        {1, Math.tan(r), 0},
284                                        {0,1,0},
285                                        {0,0,1}
286                        });
287                        m = m.times( rm );
288                }
289
290                if( this.perspectiveJitter > 0 )
291                {
292                        // Get the image bounds to distort
293                        final Rectangle rect = img.getBounds();
294
295                        // Start points
296                        final Point2d p1 = new Point2dImpl( rect.x, rect.y );
297                        final Point2d p2 = new Point2dImpl( rect.x, rect.y+rect.height );
298                        final Point2d p3 = new Point2dImpl( rect.x+rect.width, rect.y+rect.height );
299                        final Point2d p4 = new Point2dImpl( rect.x+rect.width, rect.y );
300
301                        // End points
302                        Point2d p1p = p1;
303                        Point2d p2p = p2;
304                        Point2d p3p = p3;
305                        Point2d p4p = p4;
306
307                        final float s = (float)(this.perspectiveJitter/2f);
308
309                        // Randomly choose a side to reduce
310                        switch( (int)(Math.random()*4) )
311                        {
312                        // top
313                        case 0:
314                                p1p = new Point2dImpl( rect.x+rect.width*s, rect.y );
315                                p4p = new Point2dImpl( rect.x+rect.width-rect.width*s, rect.y );
316                                break;
317                                // left
318                        case 1:
319                                p1p = new Point2dImpl( rect.x, rect.y+rect.height*s );
320                                p2p = new Point2dImpl( rect.x, rect.y+rect.height-rect.height*s );
321                                break;
322                                // bottom
323                        case 2:
324                                p2p = new Point2dImpl( rect.x+rect.width*s, rect.y+rect.height );
325                                p3p = new Point2dImpl( rect.x+rect.width-rect.width*s, rect.y+rect.height );
326                                break;
327                                // right
328                        case 3:
329                                p3p = new Point2dImpl( rect.x+rect.width, rect.y+rect.height*s );
330                                p4p = new Point2dImpl( rect.x+rect.width, rect.y+rect.height-rect.height*s );
331                        }
332
333                        final List<IndependentPair<Point2d, Point2d>> d = new
334                                        ArrayList<IndependentPair<Point2d,Point2d>>();
335
336                        d.add( new IndependentPair<Point2d,Point2d>( p1, p1p ) );
337                        d.add( new IndependentPair<Point2d,Point2d>( p2, p2p ) );
338                        d.add( new IndependentPair<Point2d,Point2d>( p3, p3p ) );
339                        d.add( new IndependentPair<Point2d,Point2d>( p4, p4p ) );
340
341                        final Matrix hm = TransformUtilities.homographyMatrixNorm( d );
342                        m = m.times( hm );
343                }
344
345                return m;
346        }
347
348        /**
349         *      Get a jittered font style
350         *      @param gfs The input font style
351         *      @return The jittered font style
352         */
353        public GeneralFontStyle<Q> getJitteredFontStyle( final GeneralFontStyle<Q> gfs )
354        {
355                //              if( colourJitter > 0 )
356                //              {
357                //                      double r = Math.random() * colourJitter;
358                //                      Q col = gfs.getColour();
359                //              }
360
361                if( this.sizeJitter > 0 )
362                {
363                        final double r = Math.random() * this.sizeJitter - this.sizeJitter/2;
364                        gfs.setFontSize( (int)(this.fontPointSize + r) );
365                }
366
367                return gfs;
368        }
369
370        /**
371         *      Set the text string to use. Can be set to null for random
372         *      characters.
373         *      @param textString the text string to use
374         */
375        public void setTextString( final String textString )
376        {
377                this.textString = textString;
378                this.nChars = textString.length();
379        }
380
381        /**
382         *      Get the text string in use.
383         *      @return the text string in use
384         */
385        public String getTextString()
386        {
387                return this.textString;
388        }
389
390        /**
391         *      Set the amount of size jitter to use.
392         *      @param sizeJitter the amount of size jitter to use
393         */
394        public void setSizeJitter( final double sizeJitter )
395        {
396                this.sizeJitter = sizeJitter;
397        }
398
399        /**
400         *      Get the amount of size jitter in use.
401         *      @return the amount of size jitter in use.
402         */
403        public double getSizeJitter()
404        {
405                return this.sizeJitter;
406        }
407
408        /**
409         *      Set the amount of angle jitter to use.
410         *      @param angleJitter the amount of angle jitter to use
411         */
412        public void setAngleJitter( final double angleJitter )
413        {
414                this.angleJitter = angleJitter;
415        }
416
417        /**
418         *      Get the amount of angle jitter in use.
419         *      @return the amount of angle jitter in use.
420         */
421        public double getAngleJitter()
422        {
423                return this.angleJitter;
424        }
425
426        /**
427         *      Set the amount of shear jitter
428         *      @param shearJitter the amount of shear jitter to use
429         */
430        public void setShearJitter( final double shearJitter )
431        {
432                this.shearJitter = shearJitter;
433        }
434
435        /**
436         *      Get the amount of shear jitter in use.
437         *      @return the amount of shear jitter in use
438         */
439        public double getShearJitter()
440        {
441                return this.shearJitter;
442        }
443
444        /**
445         *      Set the amount of perspective jitter to use.
446         *      @param perspectiveJitter the amount of perspective jitter to use
447         */
448        public void setPerspectiveJitter( final double perspectiveJitter )
449        {
450                this.perspectiveJitter = perspectiveJitter;
451        }
452
453        /**
454         *      Get the amount of perspective jitter being simulated
455         *      @return the amount of perspective jitter
456         */
457        public double getPerspectiveJitter()
458        {
459                return this.perspectiveJitter;
460        }
461
462        /**
463         *      Set the list of fonts to avoid.
464         *      @param fontsToAvoid the list of fonts to avoid
465         */
466        public void setFontsToAvoid( final List<String> fontsToAvoid )
467        {
468                this.fontsToAvoid = fontsToAvoid;
469        }
470
471        /**
472         *      Get the list of fonts to avoid.
473         * 
474         *      @return the fonts to avoid
475         */
476        public List<String> getFontsToAvoid()
477        {
478                return this.fontsToAvoid;
479        }
480
481        /**
482         *      Avoid the font with the given name.
483         *      @param f The font to avoid
484         */
485        public void addFontToAvoid( final String f )
486        {
487                this.fontsToAvoid.add( f );
488        }
489
490        /**
491         *      Set the number of characters to randomly generate.
492         *      @param nChars the number of chars to generate
493         */
494        public void setnChars( final int nChars )
495        {
496                this.nChars = nChars;
497        }
498
499        /**
500         *      Get the number of characters being generated.
501         *      @return the number of characters to generate
502         */
503        public int getnChars()
504        {
505                return this.nChars;
506        }
507
508        /**
509         *      Set whether to jitter the font type or not.
510         * 
511         *      @param typeJitter Whether to jitter the font type
512         */
513        public void setTypeJitter( final boolean typeJitter )
514        {
515                this.typeJitter = typeJitter;
516        }
517
518        /**
519         *      Get whether the font type is being randomised.
520         * 
521         *      @return Whether the font type is being randomised.
522         */
523        public boolean isTypeJitter()
524        {
525                return this.typeJitter;
526        }
527
528        /**
529         *      Set the point size of the font to draw
530         *      @param fontPointSize the font point size to use
531         */
532        public void setFontPointSize( final int fontPointSize )
533        {
534                this.fontPointSize = fontPointSize;
535        }
536
537        /**
538         *      Get the size of the font that is being drawn
539         *      @return the size of the font being drawn
540         */
541        public int getFontPointSize()
542        {
543                return this.fontPointSize;
544        }
545
546        /**
547         *      Set the padding around the outside of the image.
548         *      @param padding the padding amount
549         */
550        public void setPadding( final int padding )
551        {
552                this.padding = padding;
553        }
554
555        /**
556         *      Get the amount of padding around the image.
557         *      @return the padding amount
558         */
559        public int getPadding()
560        {
561                return this.padding;
562        }
563
564        /**
565         *      Simple main that runs a simulation of 5 runs with the text "ABC" or
566         *      text supplied on the command line.
567         * 
568         *      @param args
569         */
570        public static void main( final String[] args )
571        {
572                String text = "ABC";
573                if( args.length > 0 )
574                        text = args[0];
575
576                // Create an FImage font simulator
577                final FontSimulator<Float,FImage> fs =
578                                new FontSimulator<Float,FImage>( text );
579                fs.setFontPointSize( 72 );
580
581                fs.setAngleJitter( 10 );
582                fs.setShearJitter( 20 );
583                fs.setPerspectiveJitter( 0.5 );
584
585                // Make 5 runs
586                fs.makeRuns( 5, new FontSimListener<FImage>()
587                                {
588                        @Override
589                        public void imageCreated( final FImage img )
590                        {
591                                // display the result
592                                DisplayUtilities.display( img );
593                        }
594                                }, new FImage(1,1) );
595        }
596}