001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.vision.pipe.impl; 019 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.List; 023import org.apache.commons.lang3.tuple.Pair; 024import org.opencv.calib3d.Calib3d; 025import org.opencv.core.*; 026import org.opencv.imgproc.Imgproc; 027import org.opencv.objdetect.CharucoBoard; 028import org.opencv.objdetect.CharucoDetector; 029import org.opencv.objdetect.Objdetect; 030import org.photonvision.common.logging.LogGroup; 031import org.photonvision.common.logging.Logger; 032import org.photonvision.vision.frame.FrameDivisor; 033import org.photonvision.vision.opencv.Releasable; 034import org.photonvision.vision.pipe.CVPipe; 035import org.photonvision.vision.pipeline.UICalibrationData; 036 037public class FindBoardCornersPipe 038 extends CVPipe< 039 Pair<Mat, Mat>, 040 FindBoardCornersPipe.FindBoardCornersPipeResult, 041 FindBoardCornersPipe.FindCornersPipeParams> { 042 private static final Logger logger = 043 new Logger(FindBoardCornersPipe.class, LogGroup.VisionModule); 044 045 MatOfPoint3f objectPoints = new MatOfPoint3f(); 046 047 Size imageSize; 048 Size patternSize; 049 050 CharucoBoard board; 051 CharucoDetector detector; 052 053 // Configure the optimizations used while using OpenCV's find corners algorithm 054 // Since we return results in real-time, we want to ensure it goes as fast as 055 // possible 056 // and fails as fast as possible. 057 final int findChessboardFlags = 058 Calib3d.CALIB_CB_NORMALIZE_IMAGE 059 | Calib3d.CALIB_CB_ADAPTIVE_THRESH 060 | Calib3d.CALIB_CB_FILTER_QUADS 061 | Calib3d.CALIB_CB_FAST_CHECK; 062 063 private final MatOfPoint2f boardCorners = new MatOfPoint2f(); 064 065 // Intermediate result mat's 066 Mat smallerInFrame = new Mat(); 067 MatOfPoint2f smallerBoardCorners = new MatOfPoint2f(); 068 069 // SubCornerPix params 070 private final Size zeroZone = new Size(-1, -1); 071 private final TermCriteria criteria = new TermCriteria(3, 30, 0.001); 072 073 private FindCornersPipeParams lastParams = null; 074 075 public void createObjectPoints() { 076 if (this.lastParams != null && this.lastParams.equals(this.params)) return; 077 this.lastParams = this.params; 078 079 this.objectPoints.release(); 080 this.objectPoints = null; 081 this.objectPoints = new MatOfPoint3f(); 082 083 /* 084 * If using a chessboard, then the pattern size if the inner corners of the 085 * board. For example, the pattern size of a 9x9 chessboard would be 8x8 086 * If using a dot board, then the pattern size width is the sum of the bottom 2 087 * rows and the height is the left or right most column 088 * For example, a 5x4 dot board would have a pattern size of 11x4 089 * We subtract 1 for chessboard because the UI prompts users for the number of 090 * squares, not the 091 * number of corners. 092 */ 093 this.patternSize = 094 params.type == UICalibrationData.BoardType.CHESSBOARD 095 ? new Size(params.boardWidth - 1, params.boardHeight - 1) 096 : new Size(params.boardWidth, params.boardHeight); 097 098 // Chessboard and dot board have different 3D points to project as a dot board 099 // has alternating 100 // dots per column 101 if (params.type == UICalibrationData.BoardType.CHESSBOARD) { 102 // Here we can create an NxN grid since a chessboard is rectangular 103 for (int heightIdx = 0; heightIdx < patternSize.height; heightIdx++) { 104 for (int widthIdx = 0; widthIdx < patternSize.width; widthIdx++) { 105 double boardYCoord = heightIdx * params.gridSize; 106 double boardXCoord = widthIdx * params.gridSize; 107 objectPoints.push_back(new MatOfPoint3f(new Point3(boardXCoord, boardYCoord, 0.0))); 108 } 109 } 110 } else if (params.type == UICalibrationData.BoardType.CHARUCOBOARD) { 111 board = 112 new CharucoBoard( 113 new Size(params.boardWidth, params.boardHeight), 114 (float) params.gridSize, 115 (float) params.markerSize, 116 Objdetect.getPredefinedDictionary(params.tagFamily.getValue())); 117 board.setLegacyPattern(params.useOldPattern); 118 detector = new CharucoDetector(board); 119 detector.getDetectorParameters().set_adaptiveThreshConstant(10); 120 detector.getDetectorParameters().set_adaptiveThreshWinSizeMin(11); 121 detector.getDetectorParameters().set_adaptiveThreshWinSizeStep(40); 122 detector.getDetectorParameters().set_adaptiveThreshWinSizeMax(91); 123 124 } else { 125 logger.error("Can't create pattern for unknown board type " + params.type); 126 } 127 } 128 129 /** 130 * Finds the corners in a given image and returns them 131 * 132 * @param in Input for pipe processing. Pair of input and output mat 133 * @return All valid Mats for camera calibration 134 */ 135 @Override 136 protected FindBoardCornersPipeResult process(Pair<Mat, Mat> in) { 137 return findBoardCorners(in); 138 } 139 140 /** 141 * Figures out how much a frame or point cloud must be scaled down by to match the desired size at 142 * which to run FindCorners. Should usually be > 1. 143 * 144 * @param inFrame 145 * @return 146 */ 147 private double getFindCornersScaleFactor(Mat inFrame) { 148 return 1.0 / params.divisor.value; 149 } 150 151 /** 152 * Finds the minimum spacing between a set of x/y points Currently only considers points whose 153 * index is next to each other Which, currently, means it traverses one dimension. This is a rough 154 * heuristic approach which could be refined in the future. 155 * 156 * <p>Note that the current implementation can be fooled under the following conditions: (1) The 157 * width of the image is an odd number, and the smallest distance was actually on the between the 158 * last two points in a given row and (2) The smallest distance was actually in the direction 159 * orthogonal to that which was getting traversed by iterating through the MatOfPoint2f in order. 160 * 161 * <p>I've chosen not to handle these for speed's sake, and because, really, you don't need the 162 * exact answer for "min distance". you just need something fairly reasonable. 163 * 164 * @param inPoints point set to analyze. Must be a "tall" matrix. 165 * @return min spacing between neighbors 166 */ 167 private double getApproxMinSpacing(MatOfPoint2f inPoints) { 168 double minSpacing = Double.MAX_VALUE; 169 for (int pointIdx = 0; pointIdx < inPoints.height() - 1; pointIdx += 2) { 170 // +1 idx Neighbor distance 171 double[] startPoint = inPoints.get(pointIdx, 0); 172 double[] endPoint = inPoints.get(pointIdx + 1, 0); 173 double deltaX = startPoint[0] - endPoint[0]; 174 double deltaY = startPoint[1] - endPoint[1]; 175 double distToNext = Math.sqrt(deltaX * deltaX + deltaY * deltaY); 176 177 minSpacing = Math.min(distToNext, minSpacing); 178 } 179 return minSpacing; 180 } 181 182 /** 183 * @param inFrame Full-size mat that is going to get scaled down before passing to 184 * findBoardCorners 185 * @return the size to scale the input mat to 186 */ 187 private Size getFindCornersImgSize(Mat in) { 188 int width = in.cols() / params.divisor.value; 189 int height = in.rows() / params.divisor.value; 190 return new Size(width, height); 191 } 192 193 /** 194 * Given an input frame and a set of points from the "smaller" findChessboardCorner analysis, 195 * re-scale the points back to where they would have been in the input frame 196 * 197 * @param inPoints set of points derived from a call to findChessboardCorner on a shrunken mat. 198 * Must be a "tall" matrix. 199 * @param origFrame Original frame we're rescaling points back to 200 * @param outPoints mat into which the output rescaled points get placed 201 */ 202 private void rescalePointsToOrigFrame( 203 MatOfPoint2f inPoints, Mat origFrame, MatOfPoint2f outPoints) { 204 // Rescale boardCorners back up to the inproc image size 205 Point[] outPointsArr = new Point[inPoints.height()]; 206 double sf = getFindCornersScaleFactor(origFrame); 207 for (int pointIdx = 0; pointIdx < inPoints.height(); pointIdx++) { 208 double[] pointCoords = inPoints.get(pointIdx, 0); 209 double outXCoord = pointCoords[0] / sf; 210 double outYCoord = pointCoords[1] / sf; 211 outPointsArr[pointIdx] = new Point(outXCoord, outYCoord); 212 } 213 outPoints.fromArray(outPointsArr); 214 } 215 216 /** 217 * Picks a window size for doing subpixel optimization based on the board type and spacing 218 * observed between the corners or points in the image 219 * 220 * @param inPoints 221 * @return 222 */ 223 private Size getWindowSize(MatOfPoint2f inPoints) { 224 double windowHalfWidth = 11; // Dot board uses fixed-size window half-width 225 if (params.type == UICalibrationData.BoardType.CHESSBOARD) { 226 // Chessboard uses a dynamic sized window based on how far apart the corners are 227 windowHalfWidth = Math.floor(getApproxMinSpacing(inPoints) * 0.50); 228 windowHalfWidth = Math.max(1, windowHalfWidth); 229 } 230 return new Size(windowHalfWidth, windowHalfWidth); 231 } 232 233 /** 234 * Find chessboard corners given an input mat and output mat to draw on 235 * 236 * @return Frame resolution, object points, board corners 237 */ 238 private FindBoardCornersPipeResult findBoardCorners(Pair<Mat, Mat> in) { 239 createObjectPoints(); 240 241 float[] levels = null; 242 var outLevels = new MatOfFloat(); 243 244 var objPts = new MatOfPoint3f(); 245 var outBoardCorners = new MatOfPoint2f(); 246 247 var inFrame = in.getLeft(); 248 var outFrame = in.getRight(); 249 250 // Convert the inFrame too grayscale to increase contrast 251 Imgproc.cvtColor(inFrame, inFrame, Imgproc.COLOR_BGR2GRAY); 252 boolean boardFound = false; 253 254 // Get the size of the inFrame 255 this.imageSize = new Size(inFrame.width(), inFrame.height()); 256 257 if (params.type == UICalibrationData.BoardType.CHARUCOBOARD) { 258 Mat objPoints = 259 new Mat(); // 3 dimensional currentObjectPoints, the physical target ChArUco Board 260 Mat imgPoints = 261 new Mat(); // 2 dimensional currentImagePoints, the likely distorted board on the flat 262 // camera sensor frame posed relative to the target 263 Mat detectedCorners = new Mat(); // currentCharucoCorners 264 Mat detectedIds = new Mat(); // currentCharucoIds 265 detector.detectBoard(inFrame, detectedCorners, detectedIds); 266 267 // reformat the Mat to a List<Mat> for matchImagePoints 268 final List<Mat> detectedCornersList = new ArrayList<>(); 269 for (int i = 0; i < detectedCorners.total(); i++) { 270 detectedCornersList.add(detectedCorners.row(i)); 271 } 272 273 if (detectedCornersList.size() 274 >= 10) { // We need at least 4 corners to be used for calibration but we force 10 just to 275 // ensure the user cant get away with a garbage calibration. 276 boardFound = true; 277 } 278 279 if (!boardFound) { 280 // If we can't find a board, give up 281 return null; 282 } 283 board.matchImagePoints(detectedCornersList, detectedIds, objPoints, imgPoints); 284 285 // draw the charuco board 286 Objdetect.drawDetectedCornersCharuco( 287 outFrame, detectedCorners, detectedIds, new Scalar(0, 0, 255)); // Red Text 288 289 imgPoints.copyTo(outBoardCorners); 290 objPoints.copyTo(objPts); 291 292 // Since charuco can still detect without the whole board we need to send "fake" (all 293 // values less than zero) points and then tell it to ignore that corner by setting the 294 // corresponding level to -1. Calibrate3dPipe deals with piping this into the correct format 295 // for each backend 296 { 297 Point[] boardCorners = 298 new Point[(this.params.boardHeight - 1) * (this.params.boardWidth - 1)]; 299 Point3[] objectPoints = 300 new Point3[(this.params.boardHeight - 1) * (this.params.boardWidth - 1)]; 301 levels = new float[(this.params.boardHeight - 1) * (this.params.boardWidth - 1)]; 302 303 for (int i = 0; i < detectedIds.total(); i++) { 304 int id = (int) detectedIds.get(i, 0)[0]; 305 boardCorners[id] = outBoardCorners.toList().get(i); 306 objectPoints[id] = objPts.toList().get(i); 307 levels[id] = 1.0f; 308 } 309 for (int i = 0; i < boardCorners.length; i++) { 310 if (boardCorners[i] == null) { 311 boardCorners[i] = new Point(-1, -1); 312 objectPoints[i] = new Point3(-1, -1, -1); 313 levels[i] = -1.0f; 314 } 315 } 316 317 outBoardCorners.fromArray(boardCorners); 318 objPts.fromArray(objectPoints); 319 outLevels.fromArray(levels); 320 } 321 imgPoints.release(); 322 objPoints.release(); 323 detectedCorners.release(); 324 detectedIds.release(); 325 326 } else { // If not Charuco then do chessboard 327 // Reduce the image size to be much more manageable 328 // Note that opencv will copy the frame if no resize is requested; we can skip 329 // this since we 330 // don't need that copy. See: 331 // https://github.com/opencv/opencv/blob/a8ec6586118c3f8e8f48549a85f2da7a5b78bcc9/modules/imgproc/src/resize.cpp#L4185 332 if (params.divisor != FrameDivisor.NONE) { 333 Imgproc.resize(inFrame, smallerInFrame, getFindCornersImgSize(inFrame)); 334 } else { 335 smallerInFrame = inFrame; 336 } 337 338 // Run the chessboard corner finder on the smaller image 339 boardFound = 340 Calib3d.findChessboardCorners( 341 smallerInFrame, patternSize, smallerBoardCorners, findChessboardFlags); 342 343 if (!boardFound) { 344 return null; 345 } 346 347 rescalePointsToOrigFrame(smallerBoardCorners, inFrame, boardCorners); 348 349 boardCorners.copyTo(outBoardCorners); 350 351 objectPoints.copyTo(objPts); 352 353 // Do sub corner pix for drawing chessboard when using OpenCV 354 Imgproc.cornerSubPix( 355 inFrame, outBoardCorners, getWindowSize(outBoardCorners), zeroZone, criteria); 356 357 // draw the chessboard, doesn't have to be different for a dot board since it 358 // just re projects 359 // the corners we found 360 Calib3d.drawChessboardCorners(outFrame, patternSize, outBoardCorners, true); 361 362 levels = new float[(int) objPts.total()]; 363 Arrays.fill(levels, 1.0f); 364 outLevels.fromArray(levels); 365 } 366 if (!boardFound) { 367 // If we can't find a chessboard/dot board, give up 368 return null; 369 } 370 371 return new FindBoardCornersPipeResult(inFrame.size(), objPts, outBoardCorners, outLevels); 372 } 373 374 public static class FindCornersPipeParams { 375 final int boardHeight; 376 final int boardWidth; 377 final UICalibrationData.BoardType type; 378 final double gridSize; 379 final double markerSize; 380 final FrameDivisor divisor; 381 final UICalibrationData.TagFamily tagFamily; 382 final boolean useOldPattern; 383 384 public FindCornersPipeParams( 385 int boardHeight, 386 int boardWidth, 387 UICalibrationData.BoardType type, 388 UICalibrationData.TagFamily tagFamily, 389 double gridSize, 390 double markerSize, 391 FrameDivisor divisor, 392 boolean useOldPattern) { 393 this.boardHeight = boardHeight; 394 this.boardWidth = boardWidth; 395 this.tagFamily = tagFamily; 396 this.type = type; 397 this.gridSize = gridSize; // meter 398 this.markerSize = markerSize; // meter 399 this.divisor = divisor; 400 this.useOldPattern = useOldPattern; 401 } 402 403 @Override 404 public int hashCode() { 405 final int prime = 31; 406 int result = 1; 407 result = prime * result + boardHeight; 408 result = prime * result + boardWidth; 409 result = prime * result + ((type == null) ? 0 : type.hashCode()); 410 long temp; 411 temp = Double.doubleToLongBits(gridSize); 412 result = prime * result + (int) (temp ^ (temp >>> 32)); 413 result = prime * result + ((divisor == null) ? 0 : divisor.hashCode()); 414 return result; 415 } 416 417 @Override 418 public boolean equals(Object obj) { 419 if (this == obj) return true; 420 if (obj == null) return false; 421 if (getClass() != obj.getClass()) return false; 422 FindCornersPipeParams other = (FindCornersPipeParams) obj; 423 if (boardHeight != other.boardHeight) return false; 424 if (boardWidth != other.boardWidth) return false; 425 if (tagFamily != other.tagFamily) return false; 426 if (useOldPattern != other.useOldPattern) return false; 427 if (type != other.type) return false; 428 if (Double.doubleToLongBits(gridSize) != Double.doubleToLongBits(other.gridSize)) 429 return false; 430 return divisor == other.divisor; 431 } 432 } 433 434 public static class FindBoardCornersPipeResult implements Releasable { 435 public Size size; 436 public MatOfPoint3f objectPoints; 437 public MatOfPoint2f imagePoints; 438 public MatOfFloat levels; 439 440 // Set later only if we need it 441 public Mat inputImage = null; 442 443 public FindBoardCornersPipeResult( 444 Size size, MatOfPoint3f objectPoints, MatOfPoint2f imagePoints, MatOfFloat levels) { 445 this.size = size; 446 this.objectPoints = objectPoints; 447 this.imagePoints = imagePoints; 448 this.levels = levels; 449 } 450 451 @Override 452 public void release() { 453 objectPoints.release(); 454 imagePoints.release(); 455 levels.release(); 456 if (inputImage != null) inputImage.release(); 457 } 458 } 459}