JeVoisBase  1.22
JeVois Smart Embedded Machine Vision Toolkit Base Modules
Share this page:
Loading...
Searching...
No Matches
CalibrateCamera.C
Go to the documentation of this file.
1// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2//
3// JeVois Smart Embedded Machine Vision Toolkit - Copyright (C) 2024 by Laurent Itti, the University of Southern
4// California (USC), and iLab at USC. See http://iLab.usc.edu and http://jevois.org for information about this project.
5//
6// This file is part of the JeVois Smart Embedded Machine Vision Toolkit. This program is free software; you can
7// redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software
8// Foundation, version 2. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
9// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
10// License for more details. You should have received a copy of the GNU General Public License along with this program;
11// if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
12//
13// Contact information: Laurent Itti - 3641 Watt Way, HNB-07A - Los Angeles, CA 90089-2520 - USA.
14// Tel: +1 213 740 3527 - itti@pollux.usc.edu - http://iLab.usc.edu - http://jevois.org
15// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
16/*! \file */
17
18// icon by flaticon
19
20#include <jevois/Core/Module.H>
21#include <jevois/Core/Engine.H>
22#include <jevois/Debug/Timer.H>
24#include <jevoisbase/Components/ArUco/ArUco.H> // for dictionary enum
25#include <opencv2/core.hpp>
26#include <opencv2/core/utility.hpp>
27#include <opencv2/imgproc.hpp>
28#include <opencv2/calib3d.hpp>
29#include <opencv2/objdetect/charuco_detector.hpp>
30#include <chrono>
31
32static jevois::ParameterCategory const ParamCateg("Camera Calibration Options");
33
34//! Enum for parameter \relates CalibrateCamera
35JEVOIS_DEFINE_ENUM_CLASS(Pattern, (ChessBoard) (ChArUcoBoard) (CirclesGrid) (AsymmetricCirclesGrid) );
36
37//! Parameter \relates CalibrateCamera
38JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(pattern, Pattern, "Type of calibration board pattern to use",
39 Pattern::ChessBoard, Pattern_Values, ParamCateg);
40
41//! Parameter \relates CalibrateCamera
42JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(dictionary, aruco::Dict, "ArUco dictionary to use",
43 aruco::Dict::D4X4_50, aruco::Dict_Values, ParamCateg);
44
45//! Parameter \relates CalibrateCamera
46JEVOIS_DECLARE_PARAMETER(squareSize, float, "Size of each tile (check) in user-chosen units (e.g., mm, inch, etc). The "
47 "unit used here is the one that will be used once calibrated to report 3D coordinates "
48 "of objects relative to the camera",
49 23.0f, ParamCateg);
50
51//! Parameter \relates CalibrateCamera
52JEVOIS_DECLARE_PARAMETER(markerSize, float, "ChArUco marker size in user-chosen units (e.g., mm, inch, "
53 "etc). The unit used here is the one that will be used once calibrated to report 3D "
54 "coordinates of objects relative to the camera",
55 27.0f, ParamCateg);
56
57//! Parameter \relates CalibrateCamera
58JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(boardSize, cv::Size, "Board size [width height] in number of horizontal and "
59 "vertical tiles/disks. (Note: for asymmetric circle grid, count the number of "
60 "disks on each row, then the number of rows). The product width * height "
61 "should be the total number of tiles/disks on the grid.)",
62 { 11, 7 }, ParamCateg);
63
64//! Parameter \relates CalibrateCamera
65JEVOIS_DECLARE_PARAMETER(aspectRatio, float, "Fixed aspect ratio value to use when non-zero, or auto when 0.0",
66 0.0F, ParamCateg);
67
68//! Parameter \relates CalibrateCamera
69JEVOIS_DECLARE_PARAMETER(zeroTangentDist, bool, "Assume zero tangential distortion coefficients P1, P2 and do "
70 "not try to optimize them, i.e., assume board is exactly planar",
71 true, ParamCateg);
72
73//! Parameter \relates CalibrateCamera
74JEVOIS_DECLARE_PARAMETER(fixPrincipalPoint, bool, "Fix principal point at center, otherwise find its location",
75 true, ParamCateg);
76
77//! Parameter \relates CalibrateCamera
78JEVOIS_DECLARE_PARAMETER(fishEye, bool, "Use fisheye model. Should be true if using a wide-angle lens that "
79 "produces significant barrel distortion, otherwise false",
80 false, ParamCateg);
81
82//! Parameter \relates CalibrateCamera
83JEVOIS_DECLARE_PARAMETER(winSize, unsigned char, "Half of search window size for sub-pixel corner refinement",
84 11, ParamCateg);
85
86//! Parameter \relates CalibrateCamera
87JEVOIS_DECLARE_PARAMETER(fixK1, bool, "Fix (do not try to optimize) K1 radial distortion parameter",
88 false, ParamCateg);
89
90//! Parameter \relates CalibrateCamera
91JEVOIS_DECLARE_PARAMETER(fixK2, bool, "Fix (do not try to optimize) K2 radial distortion parameter",
92 false, ParamCateg);
93
94//! Parameter \relates CalibrateCamera
95JEVOIS_DECLARE_PARAMETER(fixK3, bool, "Fix (do not try to optimize) K3 radial distortion parameter",
96 false, ParamCateg);
97
98//! Parameter \relates CalibrateCamera
99JEVOIS_DECLARE_PARAMETER(fixK4, bool, "Fix (do not try to optimize) K4 radial distortion parameter",
100 false, ParamCateg);
101
102//! Parameter \relates CalibrateCamera
103JEVOIS_DECLARE_PARAMETER(fixK5, bool, "Fix (do not try to optimize) K5 radial distortion parameter (not used "
104 "by fisheye lenses)",
105 false, ParamCateg);
106
107//! Parameter \relates CalibrateCamera
108JEVOIS_DECLARE_PARAMETER(fixK6, bool, "Fix (do not try to optimize) K6 radial distortion parameter (not used "
109 "by fisheye lenses)",
110 false, ParamCateg);
111
112//! Parameter \relates CalibrateCamera
113JEVOIS_DECLARE_PARAMETER(showUndistorted, bool, "Once calibrated, show undistorted image instead of original capture",
114 false, ParamCateg);
115
116//! Parameter \relates CalibrateCamera
117JEVOIS_DECLARE_PARAMETER(grab, bool, "Grab one image, try to detect the calibration pattern, and add to list of "
118 "detected patterns so far. Click this for at least 5 images showing your calibration board "
119 "under various viewpoints. When enough boards have been successfully captured, click "
120 "'calibrate'",
121 false, ParamCateg);
122
123//! Parameter \relates CalibrateCamera
124JEVOIS_DECLARE_PARAMETER(calibrate, bool, "Calibrate using all the grabbed boards so far. You first need to grab "
125 "enough good views of your calibration board before you can calibrate.",
126 false, ParamCateg);
127
128//! Helper module to calibrate a given sensor+lens combo, which allows ArUco and other modules to do 3D pose estimation
129/*! Just follow the on-screen prompts to calibrate your camera. The calibration results will be saved into
130 /jevois[pro]/share/camera/ on microSD and will be automatically loaded when using a machine vision module that uses
131 camera calibration, for example \jvmod{DemoArUco} or \jvmod{FirstVision}.
132
133 The basic workflow is:
134
135 - print a calibration board and affix is to a good planar surface (e.g., an acrylic board, aluminum board, etc). Or
136 buy a board, for example from https://calib.io or similar.
137
138 - decide which sensor, resolution, and lens you want to calibrate. The sensor should be detected at boot time (check
139 \p engine::camerasens system parameter). The lens can be set manually through the \p engine::cameralens
140 parameter. The default lens is called "standard". For the resolution, edit videomappings.cfg and add a mode for
141 the CalibrateCamera module that will use the same resolution as the one you want to use later for machine
142 vision. Several commented-out examples are already in videomappings.cfg, so likely you can just uncomment one of
143 them. Then run the corresponding version of the \jvmod{CalibrateCamera} module.
144
145 - set the parameters to match the board type, board width and height in number of tiles, tile size in the unit that
146 you want to later use to estimate distance to detected objects (e.g., in millimeters, inches, etc), and possibly
147 ChArUco marker size and dictionary.
148
149 - point the camera towards the board so that the full board is in the camera view. Click 'grab'. Change the
150 viewpoint and repeat at least 5 times. Use various 3D viewpoints.
151
152 - click 'calibrate' and the calibration will be computed and saved to microSD for that sensor, resolution, and
153 lens. It will be ready to be loaded by modules that want to use it.
154
155 The default settings are for a 11x7 Chess Board that was created using the online generator at https://calib.io and
156 which you can get from http://jevois.org/data/calib.io_checker_260x200_7x11_23.pdf -
157 When you print the board, make sure you print at 100% scale. You should confirm that the checks in the printout are
158 23mm x 23mm.
159
160 An alternate chess board with fewer checks may be desirable at low resolutions, such as the 7x5 chess board at
161 http://jevois.org/data/calib.io_checker_260x200_5x7_36.pdf - if you use it, set board size to '7 5', and square size
162 to '36' to match it.
163
164 If you are using a wide-angle fish-eye lens with a lot of distortion, turn on the \p fishEye parameter.
165
166 You can also use a ChArUco board, though we have actually obtained worse reprojection errors with these boards. You
167 can print the board at http://jevois.org/data/calib.io_charuco_260x200_5x7_36_27_DICT_4X4.pdf at 100% scale, and you
168 should confirm that the checks in the printout are 36mm x 36mm, and the ArUco patterns within the white checks are
169 27mm x 27mm. If you use it, you need to set the board type, size, marker size, and square size parameters to match
170 it. An alternate board with more ChArUcos is at
171 http://jevois.org/data/calib.io_charuco_260x200_7x11_23_17_DICT_4X4.pdf
172
173
174 @author Laurent Itti
175
176 @displayname Calibrate Camera
177 @videomapping YUYV 320 495 30.0 YUYV 320 240 30.0 JeVois DemoArUco
178 @videomapping YUYV 640 975 20.0 YUYV 640 480 20.0 JeVois DemoArUco
179 @email itti\@usc.edu
180 @address University of Southern California, HNB-07A, 3641 Watt Way, Los Angeles, CA 90089-2520, USA
181 @copyright Copyright (C) 2024 by Laurent Itti, iLab and the University of Southern California
182 @mainurl http://jevois.org
183 @supporturl http://jevois.org/doc
184 @otherurl http://iLab.usc.edu
185 @license GPL v3
186 @distribution Unrestricted
187 @restrictions None
188 \ingroup modules */
190 public jevois::Parameter<pattern, dictionary, squareSize, markerSize, boardSize,
191 aspectRatio, zeroTangentDist, fixPrincipalPoint, winSize,
192 fishEye, fixK1, fixK2, fixK3, fixK4, fixK5, fixK6,
193 showUndistorted, grab, calibrate>
194{
195 public:
196 // ####################################################################################################
197 //! Constructor
198 // ####################################################################################################
200
201 // ####################################################################################################
202 //! Virtual destructor for safe inheritance
203 // ####################################################################################################
205 { }
206
207 // ####################################################################################################
208 //! Process one captured image
209 // ####################################################################################################
210 void process_frame(cv::Mat const & view)
211 {
212 // Add one image, or run the whole calibration if we have enough images:
213 if (grab::get())
214 {
215 grab::set(false);
216
217 // Try to detect the board:
218 std::vector<cv::Point2f> pointBuf;
219 int chessBoardFlags = cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE;
220
221 // Fast check erroneously fails with high distortions like fisheye
222 if (fishEye::get() == false) chessBoardFlags |= cv::CALIB_CB_FAST_CHECK;
223
224 // Find feature points:
225 cv::Size bs = boardSize::get();
226
227 bool found = false;
228 switch (pattern::get())
229 {
230 case Pattern::ChessBoard:
231 found = cv::findChessboardCorners(view, cv::Size(bs.width - 1, bs.height - 1), pointBuf, chessBoardFlags);
232 if (found)
233 {
234 // Improve the found corners' coordinate accuracy for chessboard:
235 cv::Mat viewGray;
236 cv::cvtColor(view, viewGray, cv::COLOR_BGR2GRAY);
237 cv::cornerSubPix(viewGray, pointBuf, cv::Size(winSize::get(), winSize::get()), cv::Size(-1,-1),
238 cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.0001));
239 }
240 break;
241
242 case Pattern::ChArUcoBoard:
243 {
244 // May need to instantiate a detector
245 if (! itsChArUcoDetector)
246 {
247 // Instantiate the dictionary:
248 cv::aruco::Dictionary dico = ArUco::getDictionary(dictionary::get());
249 cv::aruco::CharucoBoard ch_board({boardSize::get().width, boardSize::get().height}, squareSize::get(),
250 markerSize::get(), dico);
251 cv::aruco::DetectorParameters detector_params;
252 detector_params.cornerRefinementMethod = int(cv::aruco::CORNER_REFINE_NONE); // recommended by docs
253
254 cv::aruco::CharucoParameters charuco_params;
255 charuco_params.tryRefineMarkers = true;
256 charuco_params.cameraMatrix = itsCameraMatrix;
257 charuco_params.distCoeffs = itsDistCoeffs;
258 itsChArUcoDetector.reset(new cv::aruco::CharucoDetector(ch_board, charuco_params, detector_params));
259 }
260
261 // Detect the board:
262 std::vector<int> markerIds, charucoIds;
263 std::vector<std::vector<cv::Point2f> > markerCorners;
264 itsChArUcoDetector->detectBoard(view, pointBuf, charucoIds, markerCorners, markerIds);
265 size_t needed_num = (size_t)((bs.height - 1)*(bs.width - 1));
266 LINFO("ChArUco: detected "<< pointBuf.size() << " out of " << needed_num << " corners");
267 found = (pointBuf.size() == needed_num);
268
269 // Special handling of charuco boards:
270 if (found)
271 {
272 LINFO("ChArUco calibration board successfully detected");
273
274 itsImagePoints.push_back(pointBuf);
275
276 // Draw the corners into the image:
277 itsLastGoodView = view.clone();
278 itsLastGoodTime = std::chrono::steady_clock::now();
279 if (markerIds.size() > 0)
280 cv::aruco::drawDetectedMarkers(itsLastGoodView, markerCorners);
281 if (charucoIds.size() > 0)
282 cv::aruco::drawDetectedCornersCharuco(itsLastGoodView, pointBuf, charucoIds, cv::Scalar(255, 0, 0));
283 }
284 break;
285 }
286
287 case Pattern::CirclesGrid:
288 found = findCirclesGrid(view, bs, pointBuf);
289 break;
290
291 case Pattern::AsymmetricCirclesGrid:
292 found = findCirclesGrid(view, bs, pointBuf, cv::CALIB_CB_ASYMMETRIC_GRID);
293 break;
294 }
295
296 // Common handling of non-charuco boards:
297 if (pattern::get() != Pattern::ChArUcoBoard)
298 if (found)
299 {
300 LINFO("Calibration board successfully detected");
301
302 itsImagePoints.push_back(pointBuf);
303
304 // Draw the corners into the image:
305 itsLastGoodView = view.clone();
306 itsLastGoodTime = std::chrono::steady_clock::now();
307 drawChessboardCorners(itsLastGoodView, cv::Size(bs.width - 1, bs.height - 1), cv::Mat(pointBuf), found);
308
309 }
310 else engine()->reportError("Calibration board was not detected. Make sure it is in full view with no "
311 "occlusions, reflections, strong shadows, etc.");
312 }
313
314 // Do we want to calibrate now?
315 if (calibrate::get())
316 {
317 calibrate::set(false);
318
319 if (itsImagePoints.size() < 5)
320 engine()->reportError("Need at least 5 good board views before calibration");
321 else
322 {
324
325 if (itsCalibrated == false)
326 {
327 engine()->reportError("Calibration failed. Let's try again.");
328 restart();
329 }
330 }
331 }
332
333 // If we are calibrated, show either live view or undistorted view:
334 if (itsCalibrated)
335 {
336 if (showUndistorted::get())
337 {
338 itsLastGoodView = cv::Mat();
339 if (fishEye::get())
340 {
341 cv::Mat newCamMat;
342 cv::fisheye::estimateNewCameraMatrixForUndistortRectify(itsCameraMatrix, itsDistCoeffs, itsImageSize,
343 cv::Matx33d::eye(), newCamMat, 1);
344 cv::fisheye::undistortImage(view, itsLastGoodView, itsCameraMatrix, itsDistCoeffs, newCamMat);
345 }
346 else cv::undistort(view, itsLastGoodView, itsCameraMatrix, itsDistCoeffs);
347 }
348 else itsLastGoodView = view;
349 }
350 }
351
352 // ####################################################################################################
354 {
355 itsCalibrated = false;
356
357 // Gather the calibration flags:
358 itsFlag = 0;
359 if (fishEye::get())
360 {
361 itsFlag = cv::fisheye::CALIB_FIX_SKEW | cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC;
362 if (fixK1::get()) itsFlag |= cv::fisheye::CALIB_FIX_K1;
363 if (fixK2::get()) itsFlag |= cv::fisheye::CALIB_FIX_K2;
364 if (fixK3::get()) itsFlag |= cv::fisheye::CALIB_FIX_K3;
365 if (fixK4::get()) itsFlag |= cv::fisheye::CALIB_FIX_K4;
366 if (fixPrincipalPoint::get()) itsFlag |= cv::fisheye::CALIB_FIX_PRINCIPAL_POINT;
367 }
368 else
369 {
370 if (fixPrincipalPoint::get()) itsFlag |= cv::CALIB_FIX_PRINCIPAL_POINT;
371 if (zeroTangentDist::get()) itsFlag |= cv::CALIB_ZERO_TANGENT_DIST;
372 if (aspectRatio::get()) itsFlag |= cv::CALIB_FIX_ASPECT_RATIO;
373 if (fixK1::get()) itsFlag |= cv::CALIB_FIX_K1;
374 if (fixK2::get()) itsFlag |= cv::CALIB_FIX_K2;
375 if (fixK3::get()) itsFlag |= cv::CALIB_FIX_K3;
376 if (fixK4::get()) itsFlag |= cv::CALIB_FIX_K4;
377 if (fixK5::get()) itsFlag |= cv::CALIB_FIX_K5;
378 if (fixK6::get()) itsFlag |= cv::CALIB_FIX_K6;
379 }
380
381 std::vector<cv::Mat> rvecs, tvecs;
382 std::vector<float> reprojErrs;
383 std::vector<cv::Point3f> newObjPoints;
384
385 itsCalibrated = runCalibration(rvecs, tvecs, reprojErrs, itsAvgErr, newObjPoints);
386
387 // If calibrated, save the calibration now:
388 if (itsCalibrated)
389 {
391 calib.sensor = engine()->camerasens::get();
392 calib.lens = engine()->cameralens::get();
393 calib.fisheye = fishEye::get();
394 calib.w = itsImageSize.width;
395 calib.h = itsImageSize.height;
396 calib.camMatrix = itsCameraMatrix;
397 calib.distCoeffs = itsDistCoeffs;
398 calib.avgReprojErr = itsAvgErr;
399
400 engine()->saveCameraCalibration(calib);
401 }
402 }
403
404 // ####################################################################################################
405 double computeReprojectionErrors(std::vector<std::vector<cv::Point3f>> const & objectPoints,
406 std::vector<cv::Mat> const & rvecs, std::vector<cv::Mat> const & tvecs,
407 std::vector<float> & perViewErrors)
408 {
409 std::vector<cv::Point2f> imagePoints2;
410 size_t totalPoints = 0;
411 double totalErr = 0, err;
412 perViewErrors.resize(objectPoints.size());
413
414 for (size_t i = 0; i < objectPoints.size(); ++i )
415 {
416 if (fishEye::get())
417 cv::fisheye::projectPoints(objectPoints[i], imagePoints2, rvecs[i], tvecs[i], itsCameraMatrix, itsDistCoeffs);
418 else
419 cv::projectPoints(objectPoints[i], rvecs[i], tvecs[i], itsCameraMatrix, itsDistCoeffs, imagePoints2);
420
421 err = cv::norm(itsImagePoints[i], imagePoints2, cv::NORM_L2);
422
423 size_t n = objectPoints[i].size();
424 perViewErrors[i] = (float) std::sqrt(err*err/n);
425 totalErr += err*err;
426 totalPoints += n;
427 }
428
429 return std::sqrt(totalErr / totalPoints);
430 }
431
432 // ####################################################################################################
433 void calcBoardCornerPositions(cv::Size boardsiz, float squaresiz, std::vector<cv::Point3f> & corners)
434 {
435 corners.clear();
436
437 switch (pattern::get())
438 {
439 case Pattern::ChessBoard:
440 case Pattern::CirclesGrid:
441 for (int i = 0; i < boardsiz.height - 1; ++i)
442 for (int j = 0; j < boardsiz.width - 1; ++j)
443 corners.push_back(cv::Point3f(j*squaresiz, i*squaresiz, 0));
444 break;
445
446 case Pattern::ChArUcoBoard:
447 for (int i = 0; i < boardsiz.height - 1; ++i)
448 for (int j = 0; j < boardsiz.width - 1; ++j)
449 corners.push_back(cv::Point3f(j*squaresiz, i*squaresiz, 0));
450 break;
451
452 case Pattern::AsymmetricCirclesGrid:
453 for (int i = 0; i < boardsiz.height - 1; i++)
454 for (int j = 0; j < boardsiz.width - 1; j++)
455 corners.push_back(cv::Point3f((2 * j + i % 2)*squaresiz, i*squaresiz, 0));
456 break;
457 }
458 }
459
460 // ####################################################################################################
461 bool runCalibration(std::vector<cv::Mat> & rvecs, std::vector<cv::Mat> & tvecs, std::vector<float> & reprojErrs,
462 double & totalAvgErr, std::vector<cv::Point3f> & newObjPoints)
463 {
464 itsCameraMatrix = cv::Mat::eye(3, 3, CV_64F);
465 if (fishEye::get() == false && itsFlag & cv::CALIB_FIX_ASPECT_RATIO)
466 itsCameraMatrix.at<double>(0,0) = aspectRatio::get();
467 if (fishEye::get()) itsDistCoeffs = cv::Mat::zeros(4, 1, CV_64F);
468 else itsDistCoeffs = cv::Mat::zeros(8, 1, CV_64F);
469
470 std::vector<std::vector<cv::Point3f> > objectPoints(1);
471 calcBoardCornerPositions(boardSize::get(), squareSize::get(), objectPoints[0]);
472 newObjPoints = objectPoints[0];
473
474 objectPoints.resize(itsImagePoints.size(), objectPoints[0]);
475
476 // Find intrinsic and extrinsic camera parameters
477 double rms;
478
479 if (fishEye::get())
480 {
481 cv::Mat _rvecs, _tvecs;
482 rms = cv::fisheye::calibrate(objectPoints, itsImagePoints, itsImageSize, itsCameraMatrix, itsDistCoeffs,
483 _rvecs, _tvecs, itsFlag);
484
485 rvecs.reserve(_rvecs.rows);
486 tvecs.reserve(_tvecs.rows);
487 for (int i = 0; i < int(objectPoints.size()); i++)
488 {
489 rvecs.push_back(_rvecs.row(i));
490 tvecs.push_back(_tvecs.row(i));
491 }
492 }
493 else
494 {
495 int iFixedPoint = -1;
496 rms = cv::calibrateCameraRO(objectPoints, itsImagePoints, itsImageSize, iFixedPoint,
497 itsCameraMatrix, itsDistCoeffs, rvecs, tvecs, newObjPoints,
498 itsFlag | cv::CALIB_USE_LU);
499 }
500
501 LINFO("Re-projection error reported by calibrateCamera: "<< rms);
502
503 bool ok = cv::checkRange(itsCameraMatrix) && cv::checkRange(itsDistCoeffs);
504
505 objectPoints.clear();
506 objectPoints.resize(itsImagePoints.size(), newObjPoints);
507 totalAvgErr = computeReprojectionErrors(objectPoints, rvecs, tvecs, reprojErrs);
508
509 return ok;
510 }
511
512 // ####################################################################################################
513 //! Processing function with video output to USB
514 // ####################################################################################################
515 virtual void process(jevois::InputFrame && inframe, jevois::OutputFrame && outframe) override
516 {
517 static jevois::Timer timer("processing", 100, LOG_DEBUG);
518
519 // Wait for next available camera image:
520 jevois::RawImage const inimg = inframe.get();
521
522 timer.start();
523
524 // We only handle YUYV input frames, calibration is the same for other formats; any resolution ok:
525 unsigned int const w = inimg.width, h = inimg.height;
526 inimg.require("input", w, h, V4L2_PIX_FMT_YUYV);
527
528 // Update image size:
529 itsImageSize.width = w; itsImageSize.height = h;
530
531 // While we process it, start a thread to wait for out frame and paste the input into it:
532 jevois::RawImage outimg;
533 auto paste_fut = jevois::async([&]() {
534 outimg = outframe.get();
535 outimg.require("output", w, 2*h + 15, V4L2_PIX_FMT_YUYV);
536 jevois::rawimage::paste(inimg, outimg, 0, 0);
537 jevois::rawimage::writeText(outimg, "JeVois Camera Calibration", 3, 3, jevois::yuyv::White);
539 });
540
541 // Convert the image to BGR and process:
542 cv::Mat cvimg = jevois::rawimage::convertToCvBGR(inimg);
543
544 // Detect/calibrate, will update itsCalibrated and itsLastGoodView:
545 process_frame(cvimg);
546
547 // Wait for paste to finish up:
548 paste_fut.get();
549
550 // Let camera know we are done processing the input image:
551 inframe.done();
552
553 // Draw and guide the user:
554 int y = 15, y2 = h + 3; // we use y to print on top image, y2 on bottom image
555
556 // Draw our results if any:
557 if (itsLastGoodView.empty() == false)
558 jevois::rawimage::pasteBGRtoYUYV(itsLastGoodView, outimg, 0, h);
559
560 // If we are now calibrated, show live view, possibly undistorted:
561 if (itsCalibrated)
562 {
563 y2 = jevois::rawimage::itext(outimg, "Calibrated - avg err " + std::to_string(itsAvgErr), y2);
564 if (showUndistorted::get())
565 y2 = jevois::rawimage::itext(outimg, "Undistorted view (see showUndistorted parameter)", y2);
566 else
567 y2 = jevois::rawimage::itext(outimg, "Normal view (see showUndistorted parameter)", y2);
568 }
569 else
570 {
571 // Show some guiding messages:
572 y = jevois::rawimage::itext(outimg, "Set parameters then point to board", y);
573 y = jevois::rawimage::itext(outimg, "Click 'grab' param to grab a board", y);
574 y = jevois::rawimage::itext(outimg, "Vary viewpoints between grabs", y);
575 y = jevois::rawimage::itext(outimg, "Good views so far: " + std::to_string(itsImagePoints.size()), y);
576
577 if (itsImagePoints.size() >= 5)
578 {
579 y = jevois::rawimage::itext(outimg, "Ok to calibrate", y);
580 jevois::rawimage::itext(outimg, "Ok to calibrate", outimg.height - 13);
581 }
582 else
583 {
584 y = jevois::rawimage::itext(outimg, "Need to grab 5+ good views", y);
585 jevois::rawimage::itext(outimg, "Need to grab 5+ good views", outimg.height - 13);
586 }
587 y2 = jevois::rawimage::itext(outimg, "Last grabbed board", y2);
588 }
589
590 // Show processing fps:
591 std::string const & fpscpu = timer.stop();
592 jevois::rawimage::writeText(outimg, fpscpu, 3, h - 13, jevois::yuyv::White);
593
594 // Send the output image with our processing results to the host over USB:
595 outframe.send();
596 }
597
598#ifdef JEVOIS_PRO
599 // ####################################################################################################
600 //! Processing function with zero-copy and GUI on JeVois-Pro
601 // ####################################################################################################
602 virtual void process(jevois::InputFrame && inframe, jevois::GUIhelper & helper) override
603 {
604 static jevois::Timer timer("processing", 100, LOG_DEBUG);
605
606 // Start the GUI frame:
607 unsigned short winw, winh;
608 helper.startFrame(winw, winh);
609
610 // Draw the full-resolution camera frame:
611 int x = 0, y = 0; unsigned short iw = 0, ih = 0;
612 helper.drawInputFrame("camera", inframe, x, y, iw, ih);
613 helper.itext("JeVois-Pro Camera Calibration");
614
615 // Wait for next available camera image at processing resolution, get it as RGB:
616 cv::Mat cvimg = inframe.getCvRGBp();
617 itsImageSize.width = cvimg.cols;
618 itsImageSize.height = cvimg.rows;
619 inframe.done();
620
621 timer.start();
622
623 // Detect/calibrate, will update itsCalibrated and itsLastGoodView:
624 process_frame(cvimg); // fixme they want bgr!
625
626 // If we are now calibrated, show live view, possibly undistorted:
627 if (itsCalibrated)
628 {
629 helper.itext("Camera is now calibrated - average reprojection error = " + std::to_string(itsAvgErr));
630 if (showUndistorted::get())
631 helper.itext("Showing undistorted view. Turn off showUndistorted to see normal view");
632 else
633 helper.itext("Showing normal view. Turn on showUndistorted to see undistorted view");
634
635 int vx = 0, vy = 0; unsigned short vw = 0, vh = 0;
636 helper.drawImage("v", itsLastGoodView, true /*rgb*/, vx, vy, vw, vh, false, true);
637 }
638 else
639 {
640 // Show some guiding messages:
641 helper.itext("Good calibration board views so far: " + std::to_string(itsImagePoints.size()));
642 if (itsImagePoints.size() >= 5)
643 helper.itext("Enough good board views acquired for calibration, you may grab more to improve accuracy,"
644 " or calibrate now.");
645 else
646 helper.itext("Need at least 5 good board views for calibration, keep grabbing, use varied viewpoints");
647
648 // Display last good view for a while, otherwise nothing (live video already drawn):
649 std::chrono::duration<float> const elapsed = std::chrono::steady_clock::now() - itsLastGoodTime;
650 if (elapsed.count() < 2.0F && itsLastGoodView.empty() == false)
651 {
652 helper.itext("Calibration board successfully detected");
653 int vx = 0, vy = 0; unsigned short vw = 0, vh = 0;
654 helper.drawImage("v", itsLastGoodView, true /*rgb*/, vx, vy, vw, vh, false, true);
655 }
656 }
657
658 // Draw a user interface:
659
660 // Set window size applied only on first use ever, otherwise from imgui.ini:
661 ImGui::SetNextWindowPos(ImVec2(100, 100), ImGuiCond_FirstUseEver);
662 ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
663
664 // Light blue window background:
665 ImGui::PushStyleColor(ImGuiCol_WindowBg, 0xf0ffe0e0);
666
667 if (ImGui::Begin("Calibrate Camera Controls"))
668 {
669 int wrap = ImGui::GetWindowSize().x - ImGui::GetFontSize() * 2.0f;
670 ImGui::PushTextWrapPos(wrap);
671
672 if (itsCalibrated)
673 {
674 ImGui::TextUnformatted("Camera is now calibrated and calibration was saved to disk.");
675 ImGui::TextUnformatted("");
676 ImGui::Text("Average reprojection error: %f (lower is better)", itsAvgErr);
677 ImGui::TextUnformatted("");
678 ImGui::Separator();
679
680 if (ImGui::Button("Grab more views")) itsCalibrated = false;
681 }
682 else if (itsReady == false)
683 {
684 if (itsImagePoints.empty())
685 {
686 ImGui::TextUnformatted("To get started, switch to the Parameters tab of the main window, "
687 "and set some parameters.");
688 ImGui::TextUnformatted("");
689 ImGui::TextUnformatted("Critical parameters are:");
690 ImGui::TextUnformatted("");
691 ImGui::Bullet();
692 ImGui::Text("pattern - choose a board type (chessboard, ChArUco, etc)");
693 ImGui::Bullet();
694 ImGui::Text("fishEye - whether you are using a fish-eye (wide angle) lens");
695 ImGui::Bullet();
696 ImGui::Text("squareSize - physical size in your chosen units (mm, inch, etc) of one board square");
697 ImGui::Bullet();
698 ImGui::Text("markerSize - if using ChArUco, physical size of markers in your units");
699 ImGui::Bullet();
700 ImGui::Text("boardSize - horizontal and vertical number of tiles");
701 ImGui::Bullet();
702 ImGui::Text("engine::cameralens - lens you are calibrating (in System Parameters)");
703
704 ImGui::TextUnformatted("");
705 ImGui::Separator();
706 }
707 itsReady = ImGui::Button("Ready!");
708 }
709 else
710 {
711 ImGui::Text("Sensor: %s, Lens: %s, Resolution: %dx%d",
712 engine()->camerasens::strget().c_str(), engine()->cameralens::strget().c_str(),
713 itsImageSize.width, itsImageSize.height);
714
715 ImGui::TextUnformatted("Point the camera so that you get a full view of the calibration board, "
716 "then click Grab.");
717 if (itsImagePoints.empty() == false)
718 {
719 ImGui::TextUnformatted("");
720 ImGui::TextUnformatted("Move the camera around to get a variety of viewpoints.");
721 ImGui::TextUnformatted("Need at least 5 valid viewpoints. The more the better.");
722 ImGui::TextUnformatted("");
723 ImGui::Text("Good board views so far: %zu", itsImagePoints.size());
724 }
725 ImGui::TextUnformatted("");
726 ImGui::Separator();
727 if (ImGui::Button("Grab")) grab::set(true);
728
729 if (itsImagePoints.size() >= 5)
730 {
731 ImGui::SameLine(); ImGui::TextUnformatted(" "); ImGui::SameLine();
732 if (ImGui::Button("Calibrate now")) calibrate::set(true);
733 }
734
735 ImGui::SameLine(); ImGui::TextUnformatted(" "); ImGui::SameLine();
736 if (ImGui::Button("Drop last grab") && itsImagePoints.empty() == false) itsImagePoints.pop_back();
737 }
738
739 // Always show a start over button, except at the very beginning:
740 if (itsReady)
741 {
742 ImGui::SameLine(); ImGui::TextUnformatted(" "); ImGui::SameLine();
743 if (ImGui::Button("Start over")) restart();
744 }
745
746 ImGui::PopTextWrapPos();
747 }
748
749 ImGui::End();
750 ImGui::PopStyleColor();
751
752 // Show processing fps:
753 std::string const & fpscpu = timer.stop();
754 helper.iinfo(inframe, fpscpu, winw, winh);
755
756 // Render the image and GUI:
757 helper.endFrame();
758 }
759#endif
760
761 protected:
762 // Restart if some params changed:
763 void restart()
764 {
765 itsChArUcoDetector.reset();
766 itsReady = false;
767 itsAvgErr = 0.0;
768 itsCalibrated = false;
769 itsImagePoints.clear();
770 }
771
772 //! Restart when some critical params are changed:
773 virtual void onParamChange(pattern const & par, Pattern const & val) { restart(); }
774 virtual void onParamChange(dictionary const & par, aruco::Dict const & val) { restart(); }
775 virtual void onParamChange(boardSize const & par, cv::Size const & val) { restart(); }
776
777 private:
778 bool itsReady = false;
779 bool itsCalibrated = false;
780 int itsFlag = 0;
781 cv::Size itsImageSize;
782 std::vector<std::vector<cv::Point2f> > itsImagePoints;
783 cv::Mat itsCameraMatrix = cv::Mat::eye(3, 3, CV_64F);
784 cv::Mat itsDistCoeffs = cv::Mat::zeros(8, 1, CV_64F);
785 cv::Mat itsLastGoodView;
786 std::chrono::time_point<std::chrono::steady_clock> itsLastGoodTime;
787 std::shared_ptr<cv::aruco::CharucoDetector> itsChArUcoDetector;
788 double itsAvgErr = 0.0;
789};
790
791// Allow the module to be loaded as a shared object (.so) file:
JEVOIS_REGISTER_MODULE(ArUcoBlob)
int h
static cv::aruco::Dictionary getDictionary(aruco::Dict d)
Static method to allow anyone to get the ArUco dictionary for a given value of our dictionary paramet...
Definition ArUco.C:28
Helper module to calibrate a given sensor+lens combo, which allows ArUco and other modules to do 3D p...
JEVOIS_DECLARE_PARAMETER(fishEye, bool, "Use fisheye model. Should be true if using a wide-angle lens that " "produces significant barrel distortion, otherwise false", false, ParamCateg)
Parameter.
JEVOIS_DEFINE_ENUM_CLASS(Pattern,(ChessBoard)(ChArUcoBoard)(CirclesGrid)(AsymmetricCirclesGrid))
Enum for parameter.
JEVOIS_DECLARE_PARAMETER(aspectRatio, float, "Fixed aspect ratio value to use when non-zero, or auto when 0.0", 0.0F, ParamCateg)
Parameter.
void process_frame(cv::Mat const &view)
Process one captured image.
void calcBoardCornerPositions(cv::Size boardsiz, float squaresiz, std::vector< cv::Point3f > &corners)
JEVOIS_DECLARE_PARAMETER(fixPrincipalPoint, bool, "Fix principal point at center, otherwise find its location", true, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER(fixK1, bool, "Fix (do not try to optimize) K1 radial distortion parameter", false, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER(winSize, unsigned char, "Half of search window size for sub-pixel corner refinement", 11, ParamCateg)
Parameter.
double computeReprojectionErrors(std::vector< std::vector< cv::Point3f > > const &objectPoints, std::vector< cv::Mat > const &rvecs, std::vector< cv::Mat > const &tvecs, std::vector< float > &perViewErrors)
JEVOIS_DECLARE_PARAMETER(fixK2, bool, "Fix (do not try to optimize) K2 radial distortion parameter", false, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER(fixK3, bool, "Fix (do not try to optimize) K3 radial distortion parameter", false, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(dictionary, aruco::Dict, "ArUco dictionary to use", aruco::Dict::D4X4_50, aruco::Dict_Values, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER(showUndistorted, bool, "Once calibrated, show undistorted image instead of original capture", false, ParamCateg)
Parameter.
virtual void onParamChange(pattern const &par, Pattern const &val)
Restart when some critical params are changed:
virtual void process(jevois::InputFrame &&inframe, jevois::GUIhelper &helper) override
Processing function with zero-copy and GUI on JeVois-Pro.
bool runCalibration(std::vector< cv::Mat > &rvecs, std::vector< cv::Mat > &tvecs, std::vector< float > &reprojErrs, double &totalAvgErr, std::vector< cv::Point3f > &newObjPoints)
JEVOIS_DECLARE_PARAMETER(squareSize, float, "Size of each tile (check) in user-chosen units (e.g., mm, inch, etc). The " "unit used here is the one that will be used once calibrated to report 3D coordinates " "of objects relative to the camera", 23.0f, ParamCateg)
Parameter.
virtual void process(jevois::InputFrame &&inframe, jevois::OutputFrame &&outframe) override
Processing function with video output to USB.
JEVOIS_DECLARE_PARAMETER(markerSize, float, "ChArUco marker size in user-chosen units (e.g., mm, inch, " "etc). The unit used here is the one that will be used once calibrated to report 3D " "coordinates of objects relative to the camera", 27.0f, ParamCateg)
Parameter.
virtual void onParamChange(dictionary const &par, aruco::Dict const &val)
JEVOIS_DECLARE_PARAMETER(fixK5, bool, "Fix (do not try to optimize) K5 radial distortion parameter (not used " "by fisheye lenses)", false, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(boardSize, cv::Size, "Board size [width height] in number of horizontal and " "vertical tiles/disks. (Note: for asymmetric circle grid, count the number of " "disks on each row, then the number of rows). The product width * height " "should be the total number of tiles/disks on the grid.)", { 11, 7 }, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER(calibrate, bool, "Calibrate using all the grabbed boards so far. You first need to grab " "enough good views of your calibration board before you can calibrate.", false, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER(fixK6, bool, "Fix (do not try to optimize) K6 radial distortion parameter (not used " "by fisheye lenses)", false, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(pattern, Pattern, "Type of calibration board pattern to use", Pattern::ChessBoard, Pattern_Values, ParamCateg)
Parameter.
JEVOIS_DECLARE_PARAMETER(grab, bool, "Grab one image, try to detect the calibration pattern, and add to list of " "detected patterns so far. Click this for at least 5 images showing your calibration board " "under various viewpoints. When enough boards have been successfully captured, click " "'calibrate'", false, ParamCateg)
Parameter.
virtual ~CalibrateCamera()
Virtual destructor for safe inheritance.
JEVOIS_DECLARE_PARAMETER(zeroTangentDist, bool, "Assume zero tangential distortion coefficients P1, P2 and do " "not try to optimize them, i.e., assume board is exactly planar", true, ParamCateg)
Parameter.
virtual void onParamChange(boardSize const &par, cv::Size const &val)
JEVOIS_DECLARE_PARAMETER(fixK4, bool, "Fix (do not try to optimize) K4 radial distortion parameter", false, ParamCateg)
Parameter.
jevois::CameraLens lens
jevois::CameraSensor sensor
friend friend class Module
void drawInputFrame(char const *name, InputFrame const &frame, int &x, int &y, unsigned short &w, unsigned short &h, bool noalias=false, bool casync=false)
bool startFrame(unsigned short &w, unsigned short &h)
void iinfo(jevois::InputFrame const &inframe, std::string const &fpscpu, unsigned short winw=0, unsigned short winh=0)
void itext(char const *txt, ImU32 const &col=IM_COL32_BLACK_TRANS, int line=-1)
void drawImage(char const *name, RawImage const &img, int &x, int &y, unsigned short &w, unsigned short &h, bool noalias=false, bool isoverlay=false)
unsigned int width
unsigned int height
void require(char const *info, unsigned int w, unsigned int h, unsigned int f) const
std::string const & stop(double *seconds)
#define LINFO(msg)
void paste(RawImage const &src, RawImage &dest, int dx, int dy)
void writeText(RawImage &img, std::string const &txt, int x, int y, unsigned int col, Font font=Font6x10)
void drawFilledRect(RawImage &img, int x, int y, unsigned int w, unsigned int h, unsigned int col)
cv::Mat convertToCvBGR(RawImage const &src)
int itext(RawImage &img, std::string const &txt, int y=3, unsigned int col=jevois::yuyv::White, Font font=Font6x10)
void pasteBGRtoYUYV(cv::Mat const &src, RawImage &dst, int dx, int dy)
std::future< std::invoke_result_t< std::decay_t< Function >, std::decay_t< Args >... > > async(Function &&f, Args &&... args)
unsigned short constexpr Black
unsigned short constexpr White