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 */ 030package org.openimaj.image.analysis.algorithm; 031 032import static java.lang.Math.PI; 033import static java.lang.Math.cos; 034import static java.lang.Math.round; 035import static java.lang.Math.sin; 036import static java.lang.Math.sqrt; 037 038import java.util.ArrayList; 039import java.util.Iterator; 040import java.util.List; 041 042import org.openimaj.image.FImage; 043import org.openimaj.image.analyser.ImageAnalyser; 044import org.openimaj.image.pixel.FValuePixel; 045import org.openimaj.math.geometry.line.Line2d; 046import org.openimaj.math.geometry.point.Point2dImpl; 047 048/** 049 * Implementation of the Hough Transform for lines as an {@link ImageAnalyser}. 050 * The input image should have the lines to detect zeroed in the image (black). 051 * All other values will be ignored. That means you usually need to invert 052 * images created with edge detectors. 053 * <p><pre> 054 * {@code 055 * CannyEdgeDetector2 ced = new CannyEdgeDetector2(); 056 * FImage i = ced.process( ImageUtilities.readF( new File( 'test.jpg' ) ); 057 * 058 * HoughLines hl = new HoughLines(); 059 * i.inverse().analyse( hl ); 060 * double d = hl.calculatePrevailingAngle(); 061 * } 062 * </pre> 063 * <p> 064 * The analyser is iterable over the lines that are found within the 065 * accumulator space. Iterated lines will be returned in strength order. 066 * Once an iterator has been created, the object contains a copy of the 067 * accumulator space until {@link #clearIterator()} is called. 068 * You can use the Java 5 for construct: 069 * <pre> 070 * int maxLines = 20; 071 * for( Line2d line: hl ) { 072 * System.out.println( "Line: "+line ); 073 * if( --maxLines == 0 ) 074 * break; 075 * } 076 * hl.clearIterator(); 077 * </pre> 078 * <p> 079 * To convert a bin into a degree, use bin*360d/{@link #getNumberOfSegments()}. 080 * To convert a degree into a bin, use degree/360d/{@link #getNumberOfSegments()}. 081 * 082 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 083 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 084 * @created 8 Aug 2011 085 */ 086public class HoughLines implements 087 ImageAnalyser<FImage>, 088 Iterable<Line2d>, 089 Iterator<Line2d> 090{ 091 /** The accumulator images */ 092 private FImage accum = null; 093 094 /** The number of segments in the accumulator space */ 095 private int numberOfSegments = 360; 096 097 /** 098 * When the iterator is being used, this contains the accumulator 099 * that is being iterated over. This accumulator will have the lines 100 * that have already been returned via the iterator set to zero. 101 */ 102 private FImage iteratorAccum = null; 103 104 /** The current accumulator pixel (line) in the iterator */ 105 private FValuePixel iteratorCurrentPix = null; 106 107 private float onValue; 108 109 /** 110 * Default constructor that creates an accumulator space for 360 degrees with a "on value" of 0.0f 111 */ 112 public HoughLines() 113 { 114 this( 360 , 0f); 115 } 116 117 /** 118 * Constructor that creates a default accumulator space with 119 * a specified value for pixels considered to be "on" 120 * @param onValue value of pixels considered on 121 */ 122 public HoughLines(float onValue) 123 { 124 this( 360 , onValue); 125 } 126 127 /** 128 * Constructor that creates an accumulator space for the given number 129 * of segments. 130 * 131 * @param nSegments The number of segments. 132 * @param onValue value of pixels considered on 133 */ 134 public HoughLines( int nSegments , float onValue) 135 { 136 this.setNumberOfSegments( nSegments ); 137 this.onValue = onValue; 138 } 139 140 /** 141 * {@inheritDoc} 142 * @see org.openimaj.image.analyser.ImageAnalyser#analyseImage(org.openimaj.image.Image) 143 */ 144 @Override 145 public void analyseImage(FImage image) 146 { 147 int amax = (int) round(sqrt((image.getHeight()*image.getHeight()) + (image.getWidth()*image.getWidth()))); 148 149 if( accum == null || 150 accum.height != amax || 151 accum.width != getNumberOfSegments() ) 152 accum = new FImage( getNumberOfSegments(), amax ); 153 else accum.zero(); 154 155 for( int y = 0; y < image.getHeight(); y++ ) 156 { 157 for( int x = 0; x < image.getWidth(); x++ ) 158 { 159 if( image.getPixel(x,y) == onValue ) 160 { 161 for( int m = 0; m < getNumberOfSegments(); m++ ) 162 { 163// double mm = PI*m*360d/getNumberOfSegments()/180d; 164 double mm = ((double)m / (double)getNumberOfSegments()) * (2 * PI); 165 int a = (int) round( x * cos(mm) + y * sin(mm) ); 166 if( a < amax && a >= 0) 167 accum.pixels[a][m]++; 168 } 169 } 170 } 171 } 172 } 173 174 /** 175 * Returns the accumulator space. 176 * @return The accumulator space {@link FImage} 177 */ 178 public FImage getAccumulator() 179 { 180 return accum; 181 } 182 183 /** 184 * Calculates a projection across the accumulator space. 185 * Returns an image that has width {@link #getNumberOfSegments()} 186 * and height of 1. Effectively sums across the distances from origin 187 * in the space such that you end up with a representation that gives you 188 * the strength of the angles in the image irrespective of where those 189 * lines occur. 190 * 191 * @return A horizontal projection on the accumulator space as an 192 * FImage with dimensions {@link #getNumberOfSegments()} x 1 193 */ 194 public FImage calculateHorizontalProjection() 195 { 196 return calculateHorizontalProjection( accum ); 197 } 198 199 /** 200 * Calculates a projection across the given accumulator space. 201 * Returns an image that has width the same as the input 202 * and height of 1. Effectively sums across the distances from origin 203 * in the space such that you end up with a representation that gives you 204 * the strength of the angles in the image irrespective of where those 205 * lines occur. 206 * 207 * @param accum The accumulator space to project 208 * @return A horizontal projection on the accumulator space as an 209 * FImage with same width as input image but only 1 pixel high 210 */ 211 public FImage calculateHorizontalProjection( FImage accum ) 212 { 213 FImage proj = new FImage( accum.getWidth(), 1 ); 214 215 for( int x = 0; x < accum.getWidth(); x++ ) 216 { 217 float acc = 0; 218 for( int y = 0; y < accum.getHeight(); y++ ) 219 acc += accum.getPixel(x,y)*accum.getPixel(x,y); 220 proj.setPixel(x,0, (float)Math.sqrt(acc) ); 221 } 222 223 return proj; 224 } 225 226 /** 227 * Returns the most frequent angle that occurs within the accumulator 228 * space by calculating a horizontal projection over the accumulator 229 * space and returning the angle with the most votes. The prevailing 230 * angle is given in degrees. If it is less than zero, then no angle 231 * could be extracted (there was no local maxima in the accumulator). 232 * 233 * @return The prevailing angle (degrees) in the accumulator space; or 234 * Double.MIN_VALUE if the value cannot be calculated 235 */ 236 public double calculatePrevailingAngle() 237 { 238 return calculatePrevailingAngle( accum, 0, 360 ); 239 } 240 241 /** 242 * Returns the most frequent angle that occurs within the given accumulator 243 * space by calculating a horizontal projection over the accumulator 244 * space and returning the angle with the most votes. The prevailing 245 * angle is given in degrees. If it is less than zero, then no angle 246 * could be extracted (there was no local maxima in the accumulator). 247 * 248 * @param accum The accumulator space to use 249 * @param offset The offset into the accumulator of the 0 degree bin 250 * @param nDegrees The number of degrees covered by the accumulator space 251 * @return The prevailing angle (degrees) in the accumulator space; or 252 * Double.MIN_VALUE if there is no prevailing angle 253 */ 254 public double calculatePrevailingAngle( FImage accum, int offset, double nDegrees ) 255 { 256 FValuePixel maxpix = calculateHorizontalProjection(accum).maxPixel(); 257 if( maxpix.x == -1 && maxpix.y == -1 ) 258 return Double.MIN_VALUE; 259 return (maxpix.x+offset) * 260 (nDegrees/accum.getWidth()); 261 } 262 263 /** 264 * Returns the most frequent angle that occurs within the given accumulator 265 * space with a given range of angles (specified in degrees) 266 * by calculating a horizontal projection over the given accumulator 267 * space and returning the angle with the most votes. The prevailing 268 * angle is given in degrees. If it is less than zero, then no angle 269 * could be extracted (there was no local maxima in the accumulator). 270 * 271 * @param minTheta The minimum angle (degrees) 272 * @param maxTheta The maximum angle (degrees) 273 * @return The prevailing angle within the given range; or Double.MIN_VALUE 274 * if the value cannot be calculated 275 */ 276 public double calculatePrevailingAngle( float minTheta, float maxTheta ) 277 { 278 // Swap if some numpty puts (50,40) 279 if( minTheta > maxTheta ) 280 { 281 float tmp = minTheta; 282 minTheta = maxTheta; 283 maxTheta = tmp; 284 } 285 286 if( minTheta >= 0 ) 287 { 288 int mt = (int)(minTheta / (360d/getNumberOfSegments())); 289 int xt = (int)(maxTheta / (360d/getNumberOfSegments())); 290 FImage f = accum.extractROI( mt, 0, xt-mt, accum.getHeight() ); 291 return calculatePrevailingAngle( f, mt, (xt-mt)*(360d/getNumberOfSegments()) ); 292 } 293 else 294 { 295 // If minTheta < maxTheta, the assumption is that someone has 296 // entered something like (-10,10) - between -10 and +10 degrees. 297 298 // Create an accumulator space that's shifted left by the right number of bins 299 int mt = (int)(minTheta / (360d/getNumberOfSegments())); 300 int xt = (int)(maxTheta / (360d/getNumberOfSegments())); 301 FImage a = accum.shiftRight( -mt ).extractROI(0,0,(xt-mt),accum.getHeight()); 302 return calculatePrevailingAngle( a, mt, (xt-mt)*(360d/getNumberOfSegments()) ); 303 } 304 } 305 306 /** 307 * Returns the top line in the accumulator space. 308 * The end points of the line will have x coordinates at -2000 and 2000. 309 * 310 * @return The strongest line in the accumulator space 311 */ 312 public Line2d getBestLine() 313 { 314 return getBestLine( accum, 0 ); 315 } 316 317 /** 318 * Returns the top line in the given accumulator space. 319 * The end points of the line will have x coordinates at -2000 and 2000. 320 * 321 * @param accumulatorSpace The accumulator space to look within 322 * @param offset The number of bins offset from zero degrees 323 * @return The strongest line in the accumulator space 324 */ 325 public Line2d getBestLine( FImage accumulatorSpace, int offset ) 326 { 327 FValuePixel p = accumulatorSpace.maxPixel(); 328 329 // Remember accumulator space is r,theta 330 int theta = p.x + offset; 331 int dist = p.y; 332 333 return getLineFromParams( theta, dist, -2000, 2000 ); 334 } 335 336 /** 337 * Returns the best line within a certain angular range. Angles 338 * should be provided in degrees (0..359). If the best line has 339 * the maximum or minimum angles it will be returned. 340 * 341 * @param minTheta Minimum angle of the best line 342 * @param maxTheta Maximum angle of the best line 343 * @return The best line that has an angle within the given range. 344 */ 345 public Line2d getBestLine( float minTheta, float maxTheta ) 346 { 347 // Swap if some numpty puts (50,40) 348 if( minTheta > maxTheta ) 349 { 350 float tmp = minTheta; 351 minTheta = maxTheta; 352 maxTheta = tmp; 353 } 354 355 if( minTheta >= 0) 356 { 357 int mt = (int)(minTheta / (360d/getNumberOfSegments())); 358 int xt = (int)(maxTheta / (360d/getNumberOfSegments())); 359 FImage f = accum.extractROI( mt, 0, xt-mt, accum.getHeight() ); 360 return getBestLine( f, mt ); 361 } 362 else 363 { 364 // If minTheta < maxTheta, the assumption is that someone has 365 // entered something like (-10,10) - between -10 and +10 degrees. 366 367 // Create an accumulator space that's shifted left by the right number of bins 368 int mt = (int)(minTheta / (360d/getNumberOfSegments())); 369 int xt = (int)(maxTheta / (360d/getNumberOfSegments())); 370 FImage a = accum.shiftRight( -mt ).extractROI(0,0,(xt-mt),accum.getHeight()); 371 return getBestLine( a, mt ); 372 } 373 } 374 375 /** 376 * Returns the top n lines from the given accumulator space within the range. 377 * The end points of the lines will have x coordinates at -2000 and 2000. 378 * 379 * @param n The number of lines to return 380 * @param minTheta The minimum angle (degrees) 381 * @param maxTheta The maximum angle (degrees) 382 * @return A list of lines 383 */ 384 public List<Line2d> getBestLines( int n, float minTheta, float maxTheta ) 385 { 386 // Swap if some numpty puts (50,40) 387 if( minTheta > maxTheta ) 388 { 389 float tmp = minTheta; 390 minTheta = maxTheta; 391 maxTheta = tmp; 392 } 393 394 if( minTheta >= 0) 395 { 396 int mt = (int)(minTheta / (360d/getNumberOfSegments())); 397 int xt = (int)(maxTheta / (360d/getNumberOfSegments())); 398 FImage f = accum.extractROI( mt, 0, xt-mt, accum.getHeight() ); 399 return getBestLines( n, f, mt ); 400 } 401 else 402 { 403 // If minTheta < maxTheta, the assumption is that someone has 404 // entered something like (-10,10) - between -10 and +10 degrees. 405 406 // Create an accumulator space that's shifted left by the right number of bins 407 int mt = (int)(minTheta / (360d/getNumberOfSegments())); 408 int xt = (int)(maxTheta / (360d/getNumberOfSegments())); 409 FImage a = accum.shiftRight( -mt ).extractROI(0,0,(xt-mt),accum.getHeight()); 410 return getBestLines( n, a, mt ); 411 } 412 } 413 414 /** 415 * Returns the top n lines from the accumulator space. 416 * The end points of the lines will have x coordinates at -2000 and 2000. 417 * 418 * @param n The number of lines to return 419 * @return A list of lines 420 */ 421 public List<Line2d> getBestLines( int n ) 422 { 423 return getBestLines( n, accum, 0 ); 424 } 425 426 /** 427 * Returns the top n lines from the given accumulator space. 428 * The end points of the lines will have x coordinates at -2000 and 2000. 429 * 430 * @param n The number of lines to return 431 * @param accumulatorSpace The space to look within 432 * @param offset The offset into the accumulator of 0 in this space 433 * @return A list of lines 434 */ 435 public List<Line2d> getBestLines( int n, FImage accumulatorSpace, int offset ) 436 { 437 FImage accum2 = accumulatorSpace.clone(); 438 List<Line2d> lines = new ArrayList<Line2d>(); 439 for( int i = 0; i < n; i++ ) 440 { 441 FValuePixel p = accum2.maxPixel(); 442 lines.add( getLineFromParams( p.x+offset, p.y, -2000, 2000 ) ); 443 accum2.setPixel( p.x, p.y, 0f ); 444 } 445 446 return lines; 447 } 448 449 /** 450 * From a r,theta parameterisation of a line, this returns a {@link Line2d} 451 * with endpoints at the given x coordinates. If theta is 0 this will return 452 * a vertical line between -2000 and 2000 with the x-coordinate the appopriate 453 * distance from the origin. 454 * 455 * @param theta The angle bin in which the line lies (x in the accumulator space) 456 * @param dist The distance bin in which the line lies (y in the accumulator space) 457 * @param x1 The x-coordinate of the start of the line 458 * @param x2 The x-coordinate of the end of the line 459 * @return A {@link Line2d} 460 */ 461 public Line2d getLineFromParams( int theta, int dist, int x1, int x2 ) 462 { 463 if( theta == 0 ) 464 { 465 return new Line2d( 466 new Point2dImpl( dist, -2000 ), 467 new Point2dImpl( dist, 2000 ) 468 ); 469 } 470 471 double t = theta * (360d/getNumberOfSegments()) * Math.PI/180d; 472 return new Line2d( 473 new Point2dImpl( 474 x1, (float)(x1*(-Math.cos(t)/Math.sin(t)) + (dist/Math.sin(t)) ) ), 475 new Point2dImpl( 476 x2, (float)(x2*(-Math.cos(t)/Math.sin(t)) + (dist/Math.sin(t)) ) 477 ) ); 478 } 479 480 /** 481 * {@inheritDoc} 482 * @see java.lang.Iterable#iterator() 483 */ 484 @Override 485 public Iterator<Line2d> iterator() 486 { 487 clearIterator(); 488 checkIteratorSetup(); 489 return this; 490 } 491 492 /** 493 * Used to clone the accumulator space when an iterator 494 * function is used. 495 */ 496 private void checkIteratorSetup() 497 { 498 if( iteratorAccum == null ) 499 iteratorAccum = accum.clone(); 500 } 501 502 /** 503 * {@inheritDoc} 504 * @see java.util.Iterator#hasNext() 505 */ 506 @Override 507 public boolean hasNext() 508 { 509 return iteratorAccum.maxPixel().value > 0f; 510 } 511 512 /** 513 * {@inheritDoc} 514 * @see java.util.Iterator#next() 515 */ 516 @Override 517 public Line2d next() 518 { 519 iteratorCurrentPix = iteratorAccum.maxPixel(); 520 Line2d l = getBestLine( iteratorAccum, 0 ); 521 iteratorAccum.setPixel( iteratorCurrentPix.x, iteratorCurrentPix.y, 0f ); 522 return l; 523 } 524 525 /** 526 * {@inheritDoc} 527 * @see java.util.Iterator#remove() 528 */ 529 @Override 530 public void remove() 531 { 532 iteratorAccum.setPixel( iteratorCurrentPix.x, iteratorCurrentPix.y, 0f ); 533 } 534 535 /** 536 * Remove the temporary objects created during iteration. 537 */ 538 public void clearIterator() 539 { 540 this.iteratorAccum = null; 541 this.iteratorCurrentPix = null; 542 } 543 544 /** 545 * Set the number of segments used in the accumulator space. By default 546 * this value is 360 (one accumulator bin per degree). However, if you 547 * require greater accuracy then this can be changed. It is suggested 548 * that it is a round multiple of 360. 549 * 550 * @param numberOfSegments Set the number of directional bins in 551 * the accumulator space 552 */ 553 public void setNumberOfSegments( int numberOfSegments ) 554 { 555 this.numberOfSegments = numberOfSegments; 556 } 557 558 /** 559 * Get the number of directional accumulator bins. 560 * 561 * @return the number of directional bins in the accumulator space. 562 */ 563 public int getNumberOfSegments() 564 { 565 return numberOfSegments; 566 } 567}