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.camera.calibration; 031 032import gnu.trove.map.hash.TIntIntHashMap; 033import gnu.trove.map.hash.TObjectIntHashMap; 034 035import java.io.IOException; 036import java.net.MalformedURLException; 037import java.util.ArrayDeque; 038import java.util.ArrayList; 039import java.util.Collections; 040import java.util.Deque; 041import java.util.EnumSet; 042import java.util.HashMap; 043import java.util.List; 044import java.util.Map; 045 046import org.apache.log4j.Logger; 047import org.openimaj.algorithm.iterative.MinEpsilonOrMaxIterations; 048import org.openimaj.image.FImage; 049import org.openimaj.image.MBFImage; 050import org.openimaj.image.analyser.ImageAnalyser; 051import org.openimaj.image.colour.RGBColour; 052import org.openimaj.image.contour.Contour; 053import org.openimaj.image.contour.SuzukiContourProcessor; 054import org.openimaj.image.feature.local.interest.SubPixelCorners; 055import org.openimaj.image.processing.algorithm.EqualisationProcessor; 056import org.openimaj.image.processing.algorithm.MaxFilter; 057import org.openimaj.image.processing.algorithm.MeanCenter; 058import org.openimaj.image.processing.threshold.AdaptiveLocalThresholdMean; 059import org.openimaj.math.geometry.point.Point2d; 060import org.openimaj.math.geometry.point.Point2dImpl; 061import org.openimaj.math.geometry.shape.Circle; 062import org.openimaj.math.geometry.shape.PointList; 063import org.openimaj.math.geometry.shape.Polygon; 064import org.openimaj.math.geometry.shape.Rectangle; 065import org.openimaj.util.pair.FloatIntPair; 066import org.openimaj.video.VideoDisplay; 067import org.openimaj.video.VideoDisplayAdapter; 068import org.openimaj.video.capture.VideoCapture; 069 070/** 071 * This is a port/reworking of the OpenCV chessboard corner finder algorithm to 072 * OpenIMAJ. The key image-processing and geometry algorithms use the OpenIMAJ 073 * equivalents, but the code to extract the board and assess the correctness is 074 * purely based on a (slightly sanitised) port of the original OpenCV code. 075 * <p> 076 * This is improved variant of chessboard corner detection algorithm that uses a 077 * graph of connected quads. It is based on the code contributed by Vladimir 078 * Vezhnevets and Philip Gruebele. Here is the copyright notice from the 079 * original Vladimir's code: 080 * <p> 081 * The algorithms developed and implemented by Vezhnevets Vldimir aka Dead Moroz 082 * (vvp@graphics.cs.msu.ru) See <a 083 * href="http://graphics.cs.msu.su/en/research/calibration/opencv.html" 084 * >http://graphics.cs.msu.su/en/research/calibration/opencv.html</a> for 085 * detailed information. 086 * <p> 087 * Reliability additions and modifications made by Philip Gruebele. 088 * <p> 089 * Some further improvements for detection of partially occluded boards at 090 * non-ideal lighting conditions have been made by Alex Bovyrin and Kurt 091 * Kolonige. 092 * 093 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 094 */ 095public class ChessboardCornerFinder implements ImageAnalyser<FImage> { 096 private static final class Corner extends Point2dImpl { 097 int row; // Board row index 098 int count; // Number of neighbor corners 099 Corner[] neighbours = new Corner[4]; 100 101 public Corner(Point2d point2d) { 102 super(point2d); 103 } 104 105 FloatIntPair meanDist() 106 { 107 float sum = 0; 108 int n = 0; 109 for (int i = 0; i < 4; i++) 110 { 111 if (neighbours[i] != null) 112 { 113 final float dx = neighbours[i].x - x; 114 final float dy = neighbours[i].y - y; 115 sum += Math.sqrt(dx * dx + dy * dy); 116 n++; 117 } 118 } 119 return new FloatIntPair(sum / Math.max(n, 1), n); 120 } 121 } 122 123 private static final class Quad { 124 /** 125 * The group that the quad belongs 126 */ 127 int groupIdx = -1; 128 129 /** 130 * The corners 131 */ 132 Corner[] corners = new Corner[4]; 133 134 /** 135 * Minimum edge length in pixels 136 */ 137 float minEdgeLength; 138 139 /** 140 * Actual number of neighbours 141 */ 142 int count; 143 144 /** 145 * The neighbours 146 */ 147 Quad[] neighbours = new Quad[4]; 148 149 /** 150 * has the quad been sorted? 151 */ 152 boolean ordered; 153 /** 154 * The row position 155 */ 156 int row; 157 /** 158 * The column position 159 */ 160 int col; 161 } 162 163 private static final Logger logger = Logger.getLogger(ChessboardCornerFinder.class); 164 165 private static final int MIN_DILATIONS = 0; 166 private static final int MAX_DILATIONS = 7; 167 private static final SubPixelCorners SUB_PIX_CORNERS = new SubPixelCorners(2, 2, 168 new MinEpsilonOrMaxIterations(0.1, 15)); 169 170 /** 171 * Should filtering be enabled 172 */ 173 boolean filterQuads = true; 174 175 /** 176 * Minimum area of quad if filtering 177 */ 178 private double minSize = 25; 179 180 /** 181 * Maximum approximation distance 182 */ 183 private int maxApproxLevel = 7; 184 185 /** 186 * Pre-process with histogram equalisation 187 */ 188 boolean histogramEqualise = false; 189 190 /** 191 * Should mean adaptive thresholding be used? 192 */ 193 boolean adaptiveThreshold = false; 194 195 /** 196 * Set if a fast check for the pattern be performed to bail early 197 */ 198 final FastChessboardDetector fastDetector; 199 200 /** 201 * Number of blocks across the pattern 202 */ 203 private int patternWidth; 204 205 /** 206 * Number of blocks down the pattern 207 */ 208 private int patternHeight; 209 210 /** 211 * Was the complete pattern found? 212 */ 213 boolean found = false; 214 215 /** 216 * The final corners 217 */ 218 private List<Point2dImpl> outCorners = new ArrayList<Point2dImpl>(); 219 220 /** 221 * Options for controlling how the corner finder works 222 * 223 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 224 * 225 */ 226 public static enum Options { 227 /** 228 * Apply filtering to remove unlikely quads from detection 229 */ 230 FILTER_QUADS, 231 /** 232 * Pre-process the image by performing histogram equalisation 233 */ 234 HISTOGRAM_EQUALISE, 235 /** 236 * Perform adaptive (mean local) thresholding, rather than global 237 */ 238 ADAPTIVE_THRESHOLD, 239 /** 240 * Perform the fast check to detect a pattern and bail early 241 */ 242 FAST_CHECK; 243 } 244 245 /** 246 * Construct with the given pattern size and options set. 247 * 248 * @param patternWidth 249 * the pattern width 250 * @param patternHeight 251 * the pattern height 252 * @param opts 253 * the options 254 */ 255 public ChessboardCornerFinder(int patternWidth, int patternHeight, Options... opts) 256 { 257 this.patternWidth = patternWidth; 258 this.patternHeight = patternHeight; 259 260 final EnumSet<Options> es = EnumSet.noneOf(Options.class); 261 for (final Options o : opts) 262 es.add(o); 263 264 this.filterQuads = es.contains(Options.FILTER_QUADS); 265 this.histogramEqualise = es.contains(Options.HISTOGRAM_EQUALISE); 266 this.adaptiveThreshold = es.contains(Options.ADAPTIVE_THRESHOLD); 267 268 if (es.contains(Options.FAST_CHECK)) 269 fastDetector = new FastChessboardDetector(patternWidth, patternHeight); 270 else 271 fastDetector = null; 272 } 273 274 @Override 275 public void analyseImage(FImage image) { 276 // reset state: 277 this.found = false; 278 this.outCorners.clear(); 279 280 if (histogramEqualise) 281 image = image.process(new EqualisationProcessor()); 282 283 int prevSqrSize = 0; 284 285 if (fastDetector != null) { 286 fastDetector.analyseImage(image); 287 288 if (!fastDetector.chessboardDetected()) { 289 return; 290 } 291 } 292 293 for (int k = 0; k < 6; k++) { 294 for (int dilations = MIN_DILATIONS; dilations < MAX_DILATIONS; dilations++) { 295 if (found) 296 break; 297 298 FImage threshImg; 299 if (adaptiveThreshold) { 300 final int blockSize = (int) (Math.round(prevSqrSize == 0 ? 301 Math.min(image.width, image.height) * (k % 2 == 0 ? 0.2 : 0.1) : prevSqrSize * 2) | 1); 302 303 // adaptive mean thresholding 304 threshImg = image.process(new AdaptiveLocalThresholdMean(blockSize, (k / 2) * 5)); 305 if (dilations > 0) { 306 MaxFilter.filter(threshImg, dilations - 1); 307 } 308 } else { 309 threshImg = image.clone().threshold(MeanCenter.patchMean(image.pixels) - 10f / 255f); 310 MaxFilter.filter(threshImg, dilations); 311 } 312 313 // draw a border to allow us to find quads that go off the edge 314 // of the image 315 threshImg.drawShape(new Rectangle(1, 1, threshImg.width - 3, threshImg.height - 3), 3, 1f); 316 317 final List<Quad> quads = extractQuads(threshImg); 318 319 // not found... 320 if (quads.size() <= 0) 321 continue; 322 323 findQuadNeighbours(quads); 324 325 for (int groupIdx = 0;; groupIdx++) 326 { 327 final List<Quad> quadGroup = findConnectedQuads(quads, groupIdx); 328 int count = quadGroup.size(); 329 330 final int icount = count; 331 if (count == 0) 332 break; 333 334 // order the quad corners globally 335 // maybe delete or add some 336 logger.trace("Starting ordering of inner quads"); 337 count = orderFoundConnectedQuads(quadGroup, quads); 338 logger.trace(String.format("Orig count: %d After ordering: %d", icount, count)); 339 340 if (count == 0) 341 continue; // haven't found inner quads 342 343 // If count is more than it should be, this will remove 344 // those quads which cause maximum deviation from a nice 345 // square pattern. 346 count = cleanFoundConnectedQuads(quadGroup); 347 logger.trace(String.format("Connected group: %d orig count: %d cleaned: %d", groupIdx, icount, 348 count)); 349 350 final List<Corner> cornerGroup = new ArrayList<Corner>(); 351 count = checkQuadGroup(quadGroup, cornerGroup); 352 logger.trace(String.format("Connected group: %d count: %d cleaned: %d", groupIdx, icount, count)); 353 354 int n = count > 0 ? patternWidth * patternHeight : -count; 355 n = Math.min(n, patternWidth * patternHeight); 356 float sumDist = 0; 357 int total = 0; 358 359 for (int i = 0; i < n; i++) 360 { 361 final FloatIntPair pair = cornerGroup.get(i).meanDist(); 362 final float avgi = pair.first; 363 final int ni = pair.second; 364 sumDist += avgi * ni; 365 total += ni; 366 } 367 prevSqrSize = Math.round(sumDist / Math.max(total, 1)); 368 369 if (count > 0 || (outCorners.size() > 0 && -count > outCorners.size())) 370 { 371 // copy corners to output array 372 outCorners.clear(); 373 for (int i = 0; i < n; i++) 374 outCorners.add(new Point2dImpl(cornerGroup.get(i))); 375 376 if (count == patternWidth * patternHeight && checkBoardMonotony()) 377 { 378 found = true; 379 break; 380 } 381 } 382 } // grp 383 } // dilations 384 } // k 385 386 if (found) 387 found = checkBoardMonotony(); 388 389 // check that none of the found corners is too close to the image 390 // boundary 391 if (found) 392 { 393 final int BORDER = 8; 394 int k; 395 for (k = 0; k < patternWidth * patternHeight; k++) 396 { 397 if (outCorners.get(k).x <= BORDER || outCorners.get(k).x > image.width - BORDER || 398 outCorners.get(k).y <= BORDER || outCorners.get(k).y > image.height - BORDER) 399 break; 400 } 401 402 found = k == patternWidth * patternHeight; 403 } 404 405 if (found && patternHeight % 2 == 0 && patternWidth % 2 == 0) 406 { 407 final int lastRow = (patternHeight - 1) * patternWidth; 408 final double dy0 = outCorners.get(lastRow).y - outCorners.get(0).y; 409 if (dy0 < 0) 410 { 411 int i; 412 final int n = patternWidth * patternHeight; 413 for (i = 0; i < n / 2; i++) 414 { 415 Collections.swap(outCorners, i, n - i - 1); 416 } 417 } 418 } 419 420 if (found) { 421 outCorners = SUB_PIX_CORNERS.findSubPixCorners(image, outCorners); 422 } 423 } 424 425 /** 426 * Returns corners in clockwise order corners don't necessarily start at 427 * same position on quad (e.g., top left corner) 428 * 429 * @param threshImg 430 * the binary image 431 * @return the extracted Quads 432 */ 433 private List<Quad> extractQuads(final FImage threshImg) { 434 // if filtering is enabled, we try to guess the board contour containing 435 // all the relevant quads 436 final TObjectIntHashMap<Contour> counters = new TObjectIntHashMap<Contour>(); 437 // cornerList is the list of valid quads (prior to 438 // filtering out those with the wrong parent if applicable) 439 final List<Corner[]> cornerList = new ArrayList<Corner[]>(); 440 final Map<Corner[], Contour> parentMapping = new HashMap<Corner[], Contour>(); 441 Contour board = null; 442 443 // extract contours 444 final Contour contours = SuzukiContourProcessor.findContours(threshImg); 445 446 for (final Contour c : contours.contourIterable()) { 447 final Rectangle bounds = c.calculateRegularBoundingBox(); 448 449 // skip small regions 450 if (!(c.isHole() && bounds.width * bounds.height >= minSize)) 451 continue; 452 453 // try and make an approximated polygon with 4 vertices 454 Polygon p = null; 455 for (int approxLevel = 1; approxLevel <= maxApproxLevel; approxLevel++) { 456 p = c.reduceVertices(approxLevel); 457 if (p.nVertices() == 4) 458 break; 459 } 460 461 // test polygon for correctness 462 if (p != null && p.nVertices() == 4 && p.isConvex() && p.hasNoInnerPolygons()) { 463 final double perimeter = p.calculatePerimeter(); 464 final double area = p.calculateArea(); 465 466 final Corner[] pt = new Corner[4]; 467 for (int i = 0; i < 4; i++) 468 pt[i] = new Corner(p.points.get(i)); 469 470 float dx = pt[0].x - pt[2].x; 471 float dy = pt[0].y - pt[2].y; 472 final double d1 = Math.sqrt(dx * dx + dy * dy); 473 474 dx = pt[1].x - pt[3].x; 475 dy = pt[1].y - pt[3].y; 476 final double d2 = Math.sqrt(dx * dx + dy * dy); 477 478 dx = pt[0].x - pt[1].x; 479 dy = pt[0].y - pt[1].y; 480 final double d3 = Math.sqrt(dx * dx + dy * dy); 481 dx = pt[1].x - pt[2].x; 482 dy = pt[1].y - pt[2].y; 483 final double d4 = Math.sqrt(dx * dx + dy * dy); 484 485 if (!filterQuads 486 || 487 (d3 * 4 > d4 && d4 * 4 > d3 && d3 * d4 < area * 1.5 && area > minSize && d1 >= 0.15 * perimeter && d2 >= 0.15 * perimeter)) 488 { 489 final Contour parent = c.parent; 490 counters.adjustOrPutValue(parent, 1, 1); 491 492 // basic idea is that the board should have more holes in it 493 // than anything else in the scene 494 if (board == null || counters.get(board) < counters.get(parent)) 495 board = parent; 496 497 cornerList.add(pt); 498 parentMapping.put(pt, c.parent); 499 } 500 } 501 } 502 503 final List<Quad> quads = new ArrayList<Quad>(); 504 for (int i = 0; i < cornerList.size(); i++) { 505 final Corner[] pts = cornerList.get(i); 506 507 // reject regions that don't belong to the predicted board 508 if (filterQuads && parentMapping.get(pts) != board) 509 continue; 510 511 final Quad q = new Quad(); 512 q.corners = pts; 513 514 q.minEdgeLength = Float.MAX_VALUE; 515 for (int j = 0; j < 4; j++) { 516 final float dx = pts[j].x - pts[(j + 1) & 3].x; 517 final float dy = pts[j].y - pts[(j + 1) & 3].y; 518 final float d = dx * dx + dy * dy; 519 if (q.minEdgeLength > d) 520 q.minEdgeLength = d; 521 } 522 523 quads.add(q); 524 } 525 return quads; 526 } 527 528 /** 529 * Find the neighbouring quads for each quad 530 * 531 * @param quads 532 * the quads 533 */ 534 private void findQuadNeighbours(List<Quad> quads) 535 { 536 final int quadCount = quads.size(); 537 final float threshScale = 1.f; 538 539 // find quad neighbours 540 for (int idx = 0; idx < quads.size(); idx++) 541 { 542 final Quad curQuad = quads.get(idx); 543 544 // choose the points of the current quadrangle that are close to 545 // some points of the other quadrangles 546 // (it can happen for split corners (due to dilation) of the 547 // checker board). Search only in other quadrangles! 548 549 // for each corner of this quadrangle 550 for (int i = 0; i < 4; i++) 551 { 552 float minDist = Float.MAX_VALUE; 553 int closestCornerIdx = -1; 554 Quad closestQuad = null; 555 Corner closestCorner = null; 556 557 if (curQuad.neighbours[i] != null) 558 continue; 559 560 final Corner pt = curQuad.corners[i]; 561 562 float dx; 563 float dy; 564 float dist; 565 // find the closest corner in all other quadrangles 566 for (int k = 0; k < quadCount; k++) 567 { 568 if (k == idx) 569 continue; 570 571 for (int j = 0; j < 4; j++) 572 { 573 if (quads.get(k).neighbours[j] != null) 574 continue; 575 576 dx = pt.x - quads.get(k).corners[j].x; 577 dy = pt.y - quads.get(k).corners[j].y; 578 dist = dx * dx + dy * dy; 579 580 if (dist < minDist && 581 dist <= curQuad.minEdgeLength * threshScale && 582 dist <= quads.get(k).minEdgeLength * threshScale) 583 { 584 // check edge lengths, make sure they're compatible 585 // edges that are different by more than 1:4 are 586 // rejected 587 final float ediff = curQuad.minEdgeLength - quads.get(k).minEdgeLength; 588 if (ediff > 32 * curQuad.minEdgeLength || 589 ediff > 32 * quads.get(k).minEdgeLength) 590 { 591 logger.trace("Incompatible edge lengths"); 592 continue; 593 } 594 closestCornerIdx = j; 595 closestQuad = quads.get(k); 596 minDist = dist; 597 } 598 } 599 } 600 601 // we found a matching corner point? 602 if (closestCornerIdx >= 0 && minDist < Float.MAX_VALUE) 603 { 604 // If another point from our current quad is closer to the 605 // found corner 606 // than the current one, then we don't count this one after 607 // all. 608 // This is necessary to support small squares where 609 // otherwise the wrong 610 // corner will get matched to closest_quad; 611 closestCorner = closestQuad.corners[closestCornerIdx]; 612 613 int j; 614 for (j = 0; j < 4; j++) 615 { 616 if (curQuad.neighbours[j] == closestQuad) 617 break; 618 619 dx = closestCorner.x - curQuad.corners[j].x; 620 dy = closestCorner.y - curQuad.corners[j].y; 621 622 if (dx * dx + dy * dy < minDist) 623 break; 624 } 625 626 if (j < 4 || curQuad.count >= 4 || closestQuad.count >= 4) 627 continue; 628 629 // Check that each corner is a neighbor of different quads 630 for (j = 0; j < closestQuad.count; j++) 631 { 632 if (closestQuad.neighbours[j] == curQuad) 633 break; 634 } 635 if (j < closestQuad.count) 636 continue; 637 638 // check whether the closest corner to closestCorner 639 // is different from curQuad.corners[i].pt 640 int k; 641 for (k = 0; k < quadCount; k++) 642 { 643 final Quad q = quads.get(k); 644 if (k == idx || q == closestQuad) 645 continue; 646 647 for (j = 0; j < 4; j++) 648 if (q.neighbours[j] == null) 649 { 650 dx = closestCorner.x - q.corners[j].x; 651 dy = closestCorner.y - q.corners[j].y; 652 dist = dx * dx + dy * dy; 653 if (dist < minDist) 654 break; 655 } 656 if (j < 4) 657 break; 658 } 659 660 if (k < quadCount) 661 continue; 662 663 closestCorner.x = (pt.x + closestCorner.x) * 0.5f; 664 closestCorner.y = (pt.y + closestCorner.y) * 0.5f; 665 666 // We've found one more corner - remember it 667 curQuad.count++; 668 curQuad.neighbours[i] = closestQuad; 669 curQuad.corners[i] = closestCorner; 670 671 closestQuad.count++; 672 closestQuad.neighbours[closestCornerIdx] = curQuad; 673 } 674 } 675 } 676 } 677 678 /** 679 * Was a chessboard found in the last call to {@link #analyseImage(FImage)}? 680 * 681 * @return true if found; false otherwise 682 */ 683 public boolean isFound() { 684 return found; 685 } 686 687 /** 688 * Get any corners detected by the last call to 689 * {@link #analyseImage(FImage)}. 690 * 691 * @return the corners 692 */ 693 public List<Point2dImpl> getCorners() { 694 return this.outCorners; 695 } 696 697 /** 698 * Find groups of connected quads. This searches for the first un-labelled 699 * quad and then finds the connected ones. 700 * 701 * @param quads 702 * the quads 703 * @param groupIdx 704 * the group index 705 * @return the quads belonging to the group 706 */ 707 private List<Quad> findConnectedQuads(List<Quad> quads, int groupIdx) 708 { 709 final Deque<Quad> stack = new ArrayDeque<Quad>(); 710 int i; 711 final int quadCount = quads.size(); 712 final List<Quad> outGroup = new ArrayList<Quad>(); 713 714 // Scan the array for a first unlabeled quad 715 for (i = 0; i < quadCount; i++) 716 { 717 if (quads.get(i).count > 0 && quads.get(i).groupIdx < 0) 718 break; 719 } 720 721 // Recursively find a group of connected quads starting from the seed 722 // quad[i] 723 if (i < quadCount) 724 { 725 Quad q = quads.get(i); 726 stack.push(q); 727 outGroup.add(q); 728 q.groupIdx = groupIdx; 729 q.ordered = false; 730 731 while (stack.size() > 0) 732 { 733 q = stack.pop(); 734 for (i = 0; i < 4; i++) 735 { 736 final Quad neighbour = q.neighbours[i]; 737 if (neighbour != null && neighbour.count > 0 && neighbour.groupIdx < 0) 738 { 739 stack.push(neighbour); 740 outGroup.add(neighbour); 741 neighbour.groupIdx = groupIdx; 742 neighbour.ordered = false; 743 } 744 } 745 } 746 } 747 748 return outGroup; 749 } 750 751 /** 752 * order a group of connected quads order of corners: 0 is top left 753 * clockwise from there note: "top left" is nominal, depends on initial 754 * ordering of starting quad but all other quads are ordered consistently 755 * 756 * can change the number of quads in the group can add quads, so we need to 757 * have quad/corner arrays passed in 758 */ 759 private int orderFoundConnectedQuads(List<Quad> quads, List<Quad> allQuads) 760 { 761 final Deque<Quad> stack = new ArrayDeque<Quad>(); 762 763 int quadCount = quads.size(); 764 765 // first find an interior quad 766 Quad start = null; 767 for (int i = 0; i < quadCount; i++) 768 { 769 if (quads.get(i).count == 4) 770 { 771 start = quads.get(i); 772 break; 773 } 774 } 775 776 if (start == null) 777 return 0; // no 4-connected quad 778 779 // start with first one, assign rows/cols 780 int rowMin = 0, colMin = 0, rowMax = 0, colMax = 0; 781 782 final TIntIntHashMap colHist = new TIntIntHashMap(); 783 final TIntIntHashMap rowHist = new TIntIntHashMap(); 784 785 stack.push(start); 786 start.row = 0; 787 start.col = 0; 788 start.ordered = true; 789 790 // Recursively order the quads so that all position numbers (e.g., 791 // 0,1,2,3) are in the at the same relative corner (e.g., lower right). 792 793 while (stack.size() > 0) 794 { 795 final Quad q = stack.pop(); 796 int col = q.col; 797 int row = q.row; 798 colHist.adjustOrPutValue(col, 1, 1); 799 rowHist.adjustOrPutValue(row, 1, 1); 800 801 // check min/max 802 if (row > rowMax) 803 rowMax = row; 804 if (row < rowMin) 805 rowMin = row; 806 if (col > colMax) 807 colMax = col; 808 if (col < colMin) 809 colMin = col; 810 811 for (int i = 0; i < 4; i++) 812 { 813 final Quad neighbour = q.neighbours[i]; 814 switch (i) // adjust col, row for this quad 815 { // start at top left, go clockwise 816 case 0: 817 row--; 818 col--; 819 break; 820 case 1: 821 col += 2; 822 break; 823 case 2: 824 row += 2; 825 break; 826 case 3: 827 col -= 2; 828 break; 829 } 830 831 // just do inside quads 832 if (neighbour != null && neighbour.ordered == false && neighbour.count == 4) 833 { 834 orderQuad(neighbour, q.corners[i], (i + 2) % 4); // set in 835 // order 836 neighbour.ordered = true; 837 neighbour.row = row; 838 neighbour.col = col; 839 stack.push(neighbour); 840 } 841 } 842 } 843 844 // analyze inner quad structure 845 int w = patternWidth - 1; 846 int h = patternHeight - 1; 847 final int drow = rowMax - rowMin + 1; 848 final int dcol = colMax - colMin + 1; 849 850 // normalize pattern and found quad indices 851 if ((w > h && dcol < drow) || 852 (w < h && drow < dcol)) 853 { 854 h = patternWidth - 1; 855 w = patternHeight - 1; 856 } 857 858 logger.trace(String.format("Size: %dx%d Pattern: %dx%d", dcol, drow, w, h)); 859 860 // check if there are enough inner quads 861 if (dcol < w || drow < h) // found enough inner quads? 862 { 863 logger.trace("Too few inner quad rows/cols"); 864 return 0; // no, return 865 } 866 867 // check edges of inner quads 868 // if there is an outer quad missing, fill it in 869 // first order all inner quads 870 int found = 0; 871 for (int i = 0; i < quadCount; i++) 872 { 873 if (quads.get(i).count == 4) 874 { // ok, look at neighbours 875 int col = quads.get(i).col; 876 int row = quads.get(i).row; 877 for (int j = 0; j < 4; j++) 878 { 879 switch (j) // adjust col, row for this quad 880 { // start at top left, go clockwise 881 case 0: 882 row--; 883 col--; 884 break; 885 case 1: 886 col += 2; 887 break; 888 case 2: 889 row += 2; 890 break; 891 case 3: 892 col -= 2; 893 break; 894 } 895 final Quad neighbour = quads.get(i).neighbours[j]; 896 if (neighbour != null && !neighbour.ordered && // is it an 897 // inner 898 // quad? 899 col <= colMax && col >= colMin && 900 row <= rowMax && row >= rowMin) 901 { 902 // if so, set in order 903 logger.trace(String.format("Adding inner: col: %d row: %d", col, row)); 904 found++; 905 orderQuad(neighbour, quads.get(i).corners[j], (j + 2) % 4); 906 neighbour.ordered = true; 907 neighbour.row = row; 908 neighbour.col = col; 909 } 910 } 911 } 912 } 913 914 // if we have found inner quads, add corresponding outer quads, 915 // which are missing 916 if (found > 0) 917 { 918 logger.trace(String.format("Found %d inner quads not connected to outer quads, repairing", found)); 919 for (int i = 0; i < quadCount; i++) 920 { 921 if (quads.get(i).count < 4 && quads.get(i).ordered) 922 { 923 final int added = addOuterQuad(quads.get(i), quads, allQuads); 924 quadCount += added; 925 } 926 } 927 } 928 929 // final trimming of outer quads 930 if (dcol == w && drow == h) // found correct inner quads 931 { 932 logger.trace("Inner bounds ok, check outer quads"); 933 int rcount = quadCount; 934 for (int i = quadCount - 1; i >= 0; i--) // eliminate any quad not 935 // connected to 936 // an ordered quad 937 { 938 if (quads.get(i).ordered == false) 939 { 940 boolean outer = false; 941 for (int j = 0; j < 4; j++) // any neighbours that are 942 // ordered? 943 { 944 if (quads.get(i).neighbours[j] != null && quads.get(i).neighbours[j].ordered) 945 outer = true; 946 } 947 if (!outer) // not an outer quad, eliminate 948 { 949 logger.trace(String.format("Removing quad %d", i)); 950 removeQuadFromGroup(quads, quads.get(i)); 951 rcount--; 952 } 953 } 954 955 } 956 return rcount; 957 } 958 959 return 0; 960 } 961 962 /** 963 * put quad into correct order, where <code>corner</code> has value 964 * <code<common</code> 965 * 966 * @param quad 967 * the quad to sort 968 * @param corner 969 * the corner 970 * @param common 971 * the common vertex 972 */ 973 private void orderQuad(Quad quad, Corner corner, int common) 974 { 975 // find the corner 976 int tc; 977 for (tc = 0; tc < 4; tc++) 978 if (quad.corners[tc].x == corner.x && 979 quad.corners[tc].y == corner.y) 980 break; 981 982 // set corner order 983 // shift 984 while (tc != common) 985 { 986 // shift by one 987 final Corner tempc = quad.corners[3]; 988 final Quad tempq = quad.neighbours[3]; 989 for (int i = 3; i > 0; i--) 990 { 991 quad.corners[i] = quad.corners[i - 1]; 992 quad.neighbours[i] = quad.neighbours[i - 1]; 993 } 994 quad.corners[0] = tempc; 995 quad.neighbours[0] = tempq; 996 tc++; 997 tc = tc % 4; 998 } 999 } 1000 1001 /* 1002 * add an outer quad 1003 * 1004 * looks for the neighbor of <code>quad</code> that isn't present, tries to 1005 * add it in. <code>quad</code> is ordered 1006 * 1007 * @param quad the quad 1008 * 1009 * @param quads all quad group 1010 * 1011 * @param allQuads all the quads 1012 * 1013 * @return 1014 */ 1015 private int addOuterQuad(final Quad quad, List<Quad> quads, List<Quad> allQuads) 1016 { 1017 int added = 0; 1018 for (int i = 0; i < 4; i++) // find no-neighbor corners 1019 { 1020 if (quad.neighbours[i] == null) // ok, create and add neighbor 1021 { 1022 final int j = (i + 2) % 4; 1023 logger.trace("Adding quad as neighbor 2"); 1024 final Quad q = new Quad(); 1025 allQuads.add(q); 1026 added++; 1027 quads.add(q); 1028 1029 // set neighbor and group id 1030 quad.neighbours[i] = q; 1031 quad.count += 1; 1032 q.neighbours[j] = quad; 1033 q.groupIdx = quad.groupIdx; 1034 q.count = 1; // number of neighbours 1035 q.ordered = false; 1036 q.minEdgeLength = quad.minEdgeLength; 1037 1038 // make corners of new quad 1039 // same as neighbor quad, but offset 1040 Corner pt = quad.corners[i]; 1041 final float dx = pt.x - quad.corners[j].x; 1042 final float dy = pt.y - quad.corners[j].y; 1043 for (int k = 0; k < 4; k++) 1044 { 1045 final Corner corner = new Corner(pt); 1046 pt = quad.corners[k]; 1047 q.corners[k] = corner; 1048 corner.x += dx; 1049 corner.y += dy; 1050 } 1051 // have to set exact corner 1052 q.corners[j] = quad.corners[i]; 1053 1054 // now find other neighbor and add it, if possible 1055 if (quad.neighbours[(i + 3) % 4] != null && 1056 quad.neighbours[(i + 3) % 4].ordered && 1057 quad.neighbours[(i + 3) % 4].neighbours[i] != null && 1058 quad.neighbours[(i + 3) % 4].neighbours[i].ordered) 1059 { 1060 final Quad qn = quad.neighbours[(i + 3) % 4].neighbours[i]; 1061 q.count = 2; 1062 q.neighbours[(j + 1) % 4] = qn; 1063 qn.neighbours[(i + 1) % 4] = q; 1064 qn.count += 1; 1065 // have to set exact corner 1066 q.corners[(j + 1) % 4] = qn.corners[(i + 1) % 4]; 1067 } 1068 } 1069 } 1070 return added; 1071 } 1072 1073 /** 1074 * remove quad from quad group 1075 */ 1076 private void removeQuadFromGroup(final List<Quad> quads, Quad q0) 1077 { 1078 final int count = quads.size(); 1079 // remove any references to this quad as a neighbor 1080 for (int i = 0; i < count; i++) 1081 { 1082 final Quad q = quads.get(i); 1083 for (int j = 0; j < 4; j++) 1084 { 1085 if (q.neighbours[j] == q0) 1086 { 1087 q.neighbours[j] = null; 1088 q.count--; 1089 for (int k = 0; k < 4; k++) 1090 if (q0.neighbours[k] == q) 1091 { 1092 q0.neighbours[k] = null; 1093 q0.count--; 1094 break; 1095 } 1096 break; 1097 } 1098 } 1099 } 1100 1101 quads.remove(q0); 1102 } 1103 1104 /** 1105 * if we found too many connect quads, remove those which probably do not 1106 * belong. 1107 * 1108 * @param quadGroup 1109 * the group of quads 1110 * @return the new group size 1111 */ 1112 private int cleanFoundConnectedQuads(List<Quad> quadGroup) 1113 { 1114 int quadCount = quadGroup.size(); 1115 final Point2dImpl center = new Point2dImpl(); 1116 1117 // number of quads this pattern should contain 1118 final int count = ((patternWidth + 1) * (patternHeight + 1) + 1) / 2; 1119 1120 // if we have more quadrangles than we should, 1121 // try to eliminate duplicates or ones which don't belong to the pattern 1122 // rectangle... 1123 if (quadCount <= count) 1124 return quadCount; 1125 1126 // create an array of quadrangle centers 1127 final List<Point2dImpl> centers = new ArrayList<Point2dImpl>(quadCount); 1128 1129 for (int i = 0; i < quadCount; i++) 1130 { 1131 final Point2dImpl ci = new Point2dImpl(); 1132 final Quad q = quadGroup.get(i); 1133 1134 for (int j = 0; j < 4; j++) 1135 { 1136 final Point2dImpl pt = q.corners[j]; 1137 ci.x += pt.x; 1138 ci.y += pt.y; 1139 } 1140 1141 ci.x *= 0.25f; 1142 ci.y *= 0.25f; 1143 1144 centers.add(ci); 1145 center.x += ci.x; 1146 center.y += ci.y; 1147 } 1148 center.x /= quadCount; 1149 center.y /= quadCount; 1150 1151 // If we still have more quadrangles than we should, 1152 // we try to eliminate bad ones based on minimizing the bounding box. 1153 // We iteratively remove the point which reduces the size of 1154 // the bounding box of the blobs the most 1155 // (since we want the rectangle to be as small as possible) 1156 // remove the quadrange that causes the biggest reduction 1157 // in pattern size until we have the correct number 1158 for (; quadCount > count; quadCount--) 1159 { 1160 double minBoxArea = Double.MAX_VALUE; 1161 int minBoxAreaIndex = -1; 1162 Quad q0, q; 1163 1164 // For each point, calculate box area without that point 1165 for (int skip = 0; skip < quadCount; skip++) 1166 { 1167 // get bounding rectangle 1168 final Point2dImpl temp = centers.get(skip); // temporarily make 1169 // index 'skip' the 1170 // same as 1171 centers.set(skip, center); // pattern center (so it is not 1172 // counted for convex hull) 1173 final PointList pl = new PointList(centers); 1174 final Polygon hull = pl.calculateConvexHull(); 1175 centers.set(skip, temp); 1176 final double hullArea = hull.calculateArea(); 1177 1178 // remember smallest box area 1179 if (hullArea < minBoxArea) 1180 { 1181 minBoxArea = hullArea; 1182 minBoxAreaIndex = skip; 1183 } 1184 } 1185 1186 q0 = quadGroup.get(minBoxAreaIndex); 1187 1188 // remove any references to this quad as a neighbor 1189 for (int i = 0; i < quadCount; i++) 1190 { 1191 q = quadGroup.get(i); 1192 for (int j = 0; j < 4; j++) 1193 { 1194 if (q.neighbours[j] == q0) 1195 { 1196 q.neighbours[j] = null; 1197 q.count--; 1198 for (int k = 0; k < 4; k++) 1199 if (q0.neighbours[k] == q) 1200 { 1201 q0.neighbours[k] = null; 1202 q0.count--; 1203 break; 1204 } 1205 break; 1206 } 1207 } 1208 } 1209 1210 // remove the quad 1211 quadCount--; 1212 quadGroup.remove(minBoxAreaIndex); 1213 centers.remove(minBoxAreaIndex); 1214 } 1215 1216 return quadCount; 1217 } 1218 1219 /** 1220 * 1221 */ 1222 private int checkQuadGroup(List<Quad> quadGroup, List<Corner> outCorners) 1223 { 1224 final int ROW1 = 1000000; 1225 final int ROW2 = 2000000; 1226 final int ROW_ = 3000000; 1227 int result = 0; 1228 final int quadCount = quadGroup.size(); 1229 int cornerCount = 0; 1230 final Corner[] corners = new Corner[quadCount * 4]; 1231 1232 int width = 0, height = 0; 1233 final int[] hist = { 0, 0, 0, 0, 0 }; 1234 Corner first = null, first2 = null, right, cur, below, c; 1235 1236 try { 1237 // build dual graph, which vertices are internal quad corners 1238 // and two vertices are connected iff they lie on the same quad edge 1239 for (int i = 0; i < quadCount; i++) 1240 { 1241 final Quad q = quadGroup.get(i); 1242 1243 for (int j = 0; j < 4; j++) 1244 { 1245 if (q.neighbours[j] != null) 1246 { 1247 final Corner a = q.corners[j], b = q.corners[(j + 1) & 3]; 1248 // mark internal corners that belong to: 1249 // - a quad with a single neighbor - with ROW1, 1250 // - a quad with two neighbours - with ROW2 1251 // make the rest of internal corners with ROW_ 1252 final int rowFlag = q.count == 1 ? ROW1 : q.count == 2 ? ROW2 : ROW_; 1253 1254 if (a.row == 0) 1255 { 1256 corners[cornerCount++] = a; 1257 a.row = rowFlag; 1258 } 1259 else if (a.row > rowFlag) 1260 a.row = rowFlag; 1261 1262 if (q.neighbours[(j + 1) & 3] != null) 1263 { 1264 if (a.count >= 4 || b.count >= 4) 1265 throw new Exception(); 1266 for (int k = 0; k < 4; k++) 1267 { 1268 if (a.neighbours[k] == b) 1269 throw new Exception(); 1270 if (b.neighbours[k] == a) 1271 throw new Exception(); 1272 } 1273 a.neighbours[a.count++] = b; 1274 b.neighbours[b.count++] = a; 1275 } 1276 } 1277 } 1278 } 1279 1280 if (cornerCount != patternWidth * patternHeight) 1281 throw new Exception(); 1282 1283 for (int i = 0; i < cornerCount; i++) 1284 { 1285 final int n = corners[i].count; 1286 assert (0 <= n && n <= 4); 1287 hist[n]++; 1288 if (first == null && n == 2) 1289 { 1290 if (corners[i].row == ROW1) 1291 first = corners[i]; 1292 else if (first2 == null && corners[i].row == ROW2) 1293 first2 = corners[i]; 1294 } 1295 } 1296 1297 // start with a corner that belongs to a quad with a signle 1298 // neighbor. 1299 // if we do not have such, start with a corner of a quad with two 1300 // neighbours. 1301 if (first == null) 1302 first = first2; 1303 1304 if (first == null || hist[0] != 0 || hist[1] != 0 || hist[2] != 4 || 1305 hist[3] != (patternWidth + patternHeight) * 2 - 8) 1306 throw new Exception(); 1307 1308 cur = first; 1309 right = below = null; 1310 outCorners.add(cur); 1311 1312 for (int k = 0; k < 4; k++) 1313 { 1314 c = cur.neighbours[k]; 1315 if (c != null) 1316 { 1317 if (right == null) 1318 right = c; 1319 else if (below == null) 1320 below = c; 1321 } 1322 } 1323 1324 if (right == null || (right.count != 2 && right.count != 3) || 1325 below == null || (below.count != 2 && below.count != 3)) 1326 throw new Exception(); 1327 1328 cur.row = 0; 1329 first = below; // remember the first corner in the next row 1330 // find and store the first row (or column) 1331 while (true) 1332 { 1333 right.row = 0; 1334 outCorners.add(right); 1335 1336 if (right.count == 2) 1337 break; 1338 if (right.count != 3 || outCorners.size() >= Math.max(patternWidth, patternHeight)) 1339 throw new Exception(); 1340 cur = right; 1341 1342 for (int k = 0; k < 4; k++) 1343 { 1344 c = cur.neighbours[k]; 1345 if (c != null && c.row > 0) 1346 { 1347 int kk; 1348 for (kk = 0; kk < 4; kk++) 1349 { 1350 if (c.neighbours[kk] == below) 1351 break; 1352 } 1353 if (kk < 4) 1354 below = c; 1355 else 1356 right = c; 1357 } 1358 } 1359 } 1360 1361 width = outCorners.size(); 1362 if (width == patternWidth) 1363 height = patternHeight; 1364 else if (width == patternHeight) 1365 height = patternWidth; 1366 else 1367 throw new Exception(); 1368 1369 // find and store all the other rows 1370 for (int i = 1;; i++) 1371 { 1372 if (first == null) 1373 break; 1374 cur = first; 1375 first = null; 1376 int j; 1377 for (j = 0;; j++) 1378 { 1379 cur.row = i; 1380 outCorners.add(cur); 1381 if (cur.count == 2 + (i < height - 1 ? 1 : 0) && j > 0) 1382 break; 1383 1384 right = null; 1385 1386 // find a neighbor that has not been processed yet 1387 // and that has a neighbor from the previous row 1388 for (int k = 0; k < 4; k++) 1389 { 1390 c = cur.neighbours[k]; 1391 if (c != null && c.row > i) 1392 { 1393 int kk; 1394 for (kk = 0; kk < 4; kk++) 1395 { 1396 if (c.neighbours[kk] != null && c.neighbours[kk].row == i - 1) 1397 break; 1398 } 1399 if (kk < 4) 1400 { 1401 right = c; 1402 if (j > 0) 1403 break; 1404 } 1405 else if (j == 0) 1406 first = c; 1407 } 1408 } 1409 if (right == null) 1410 throw new Exception(); 1411 cur = right; 1412 } 1413 1414 if (j != width - 1) 1415 throw new Exception(); 1416 } 1417 1418 if (outCorners.size() != cornerCount) 1419 throw new Exception(); 1420 1421 // check if we need to transpose the board 1422 if (width != patternWidth) 1423 { 1424 final int t = width; 1425 width = height; 1426 height = t; 1427 1428 for (int i = 0; i < cornerCount; i++) 1429 corners[i] = outCorners.get(i); 1430 1431 for (int i = 0; i < height; i++) 1432 for (int j = 0; j < width; j++) 1433 outCorners.set(i * width + j, corners[j * height + i]); 1434 } 1435 1436 // check if we need to revert the order in each row 1437 { 1438 final Point2dImpl p0 = outCorners.get(0), p1 = outCorners.get(patternWidth - 1), p2 = outCorners 1439 .get(patternWidth); 1440 if ((p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x) < 0) 1441 { 1442 if (width % 2 == 0) 1443 { 1444 for (int i = 0; i < height; i++) 1445 for (int j = 0; j < width / 2; j++) 1446 Collections.swap(outCorners, i * width + j, i * width + width - j - 1); 1447 } 1448 else 1449 { 1450 for (int j = 0; j < width; j++) 1451 for (int i = 0; i < height / 2; i++) 1452 Collections.swap(outCorners, i * width + j, (height - i - 1) * width + j); 1453 } 1454 } 1455 } 1456 1457 result = cornerCount; 1458 1459 } catch (final Exception ex) { 1460 // ignore 1461 } 1462 1463 if (result <= 0) 1464 { 1465 cornerCount = Math.min(cornerCount, patternWidth * patternHeight); 1466 outCorners.clear(); 1467 for (int i = 0; i < cornerCount; i++) 1468 outCorners.add(corners[i]); 1469 result = -cornerCount; 1470 1471 if (result == -patternWidth * patternHeight) 1472 result = -result; 1473 } 1474 1475 return result; 1476 } 1477 1478 /** 1479 * Checks that each board row and column is pretty much monotonous curve: 1480 * 1481 * It analyzes each row and each column of the chessboard as following: 1482 * 1483 * for each corner c lying between end points in the same row/column it 1484 * checks that the point projection to the line segment (a,b) is lying 1485 * between projections of the neighbor corners in the same row/column. 1486 * 1487 * This function has been created as temporary workaround for the bug in 1488 * current implementation of cvFindChessboardCornes that produces absolutely 1489 * unordered sets of corners. 1490 * 1491 * @return true if the board is good; false otherwise. 1492 */ 1493 private boolean checkBoardMonotony() 1494 { 1495 int i, j, k; 1496 1497 for (k = 0; k < 2; k++) 1498 { 1499 for (i = 0; i < (k == 0 ? patternHeight : patternWidth); i++) 1500 { 1501 final Point2dImpl a = k == 0 ? outCorners.get(i * patternWidth) : outCorners.get(i); 1502 final Point2dImpl b = k == 0 ? outCorners.get((i + 1) * patternWidth - 1) : 1503 outCorners.get((patternHeight - 1) * patternWidth + i); 1504 float prevt = 0; 1505 final float dx0 = b.x - a.x, dy0 = b.y - a.y; 1506 if (Math.abs(dx0) + Math.abs(dy0) < Float.MIN_VALUE) 1507 return false; 1508 for (j = 1; j < (k == 0 ? patternWidth : patternHeight) - 1; j++) 1509 { 1510 final Point2dImpl c = k == 0 ? outCorners.get(i * patternWidth + j) : 1511 outCorners.get(j * patternWidth + i); 1512 final float t = ((c.x - a.x) * dx0 + (c.y - a.y) * dy0) / (dx0 * dx0 + dy0 * dy0); 1513 if (t < prevt || t > 1) 1514 return false; 1515 prevt = t; 1516 } 1517 } 1518 } 1519 1520 return true; 1521 } 1522 1523 /** 1524 * Draw the predicted chessboard corners from the last call to 1525 * {@link #analyseImage(FImage)} on the given image. 1526 * 1527 * @param image 1528 * the image to draw on 1529 */ 1530 public void drawChessboardCorners(MBFImage image) { 1531 drawChessboardCorners(image, patternWidth, patternHeight, outCorners, found); 1532 } 1533 1534 /** 1535 * Draw the given chessboard corners from on the given image. 1536 * 1537 * @param image 1538 * the image to draw on 1539 * @param patternWidth 1540 * the chessboard pattern width 1541 * @param patternHeight 1542 * the chessboard pattern height 1543 * @param corners 1544 * the corners 1545 * @param found 1546 * true if the corners were found 1547 */ 1548 public static void drawChessboardCorners(MBFImage image, int patternWidth, int patternHeight, 1549 List<? extends Point2d> corners, boolean found) 1550 { 1551 final int radius = 4; 1552 1553 if (!found) { 1554 final Float[] color = RGBColour.RGB(0, 0, 255); 1555 1556 for (int i = 0; i < corners.size(); i++) 1557 { 1558 final Point2d pt = corners.get(i); 1559 image.drawLine(new Point2dImpl(pt.getX() - radius, pt.getY() - radius), 1560 new Point2dImpl(pt.getX() + radius, pt.getY() + radius), 1, color); 1561 image.drawLine(new Point2dImpl(pt.getX() - radius, pt.getY() + radius), 1562 new Point2dImpl(pt.getX() + radius, pt.getY() - radius), 1, color); 1563 image.drawShape(new Circle(pt, radius), 1, color); 1564 } 1565 } else { 1566 Point2d prevPt = new Point2dImpl(); 1567 final Float[][] lineColours = 1568 { 1569 RGBColour.RGB(0, 0, 255), 1570 RGBColour.RGB(0, 128, 255), 1571 RGBColour.RGB(0, 200, 200), 1572 RGBColour.RGB(0, 255, 0), 1573 RGBColour.RGB(200, 200, 0), 1574 RGBColour.RGB(255, 0, 0), 1575 RGBColour.RGB(255, 0, 255) 1576 }; 1577 1578 for (int y = 0, i = 0; y < patternHeight; y++) { 1579 final Float[] color = lineColours[y % lineColours.length]; 1580 1581 for (int x = 0; x < patternWidth; x++, i++) { 1582 final Point2d pt = corners.get(i); 1583 1584 if (i != 0) { 1585 image.drawLine(prevPt, pt, 1, color); 1586 } 1587 1588 image.drawLine(new Point2dImpl(pt.getX() - radius, pt.getY() - radius), 1589 new Point2dImpl(pt.getX() + radius, pt.getY() + radius), 1, color); 1590 image.drawLine(new Point2dImpl(pt.getX() - radius, pt.getY() + radius), 1591 new Point2dImpl(pt.getX() + radius, pt.getY() - radius), 1, color); 1592 image.drawShape(new Circle(pt, radius), 1, color); 1593 1594 prevPt = pt; 1595 } 1596 } 1597 } 1598 } 1599 1600 /** 1601 * Simple test program 1602 * 1603 * @param args 1604 * @throws MalformedURLException 1605 * @throws IOException 1606 */ 1607 public static void main(String[] args) throws IOException { 1608 final ChessboardCornerFinder fcc = new ChessboardCornerFinder(9, 6, 1609 Options.FILTER_QUADS, Options.FAST_CHECK); 1610 VideoDisplay.createVideoDisplay(new VideoCapture(640, 480)).addVideoListener(new VideoDisplayAdapter<MBFImage>() 1611 { 1612 @Override 1613 public void beforeUpdate(MBFImage frame) { 1614 fcc.analyseImage(frame.flatten()); 1615 fcc.drawChessboardCorners(frame); 1616 } 1617 }); 1618 } 1619}