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}