iCub-main
Loading...
Searching...
No Matches
stereoCalibThread.cpp
Go to the documentation of this file.
1#include <algorithm>
2#include <cmath>
3#include <utility>
4#include <chrono>
5#include <thread>
6#include <sstream>
7#include <yarp/cv/Cv.h>
8#include "stereoCalibThread.h"
9
10namespace
11{
12
13std::string formatCalibrationMatrix(const cv::Mat& matrix)
14{
15 std::ostringstream stream;
16 stream << matrix;
17 return stream.str();
18}
19
20void logCameraCalibration(const char* cameraName,
22{
23 yInfo() << "[CAMERA_CALIBRATION_" << cameraName << "]";
24 yInfo() << "w" << camera.imageSize.width << "h" << camera.imageSize.height;
25 yInfo() << "fx" << camera.K.at<double>(0, 0)
26 << "fy" << camera.K.at<double>(1, 1)
27 << "cx" << camera.K.at<double>(0, 2)
28 << "cy" << camera.K.at<double>(1, 2);
29 yInfo() << "D = [k1 k2 k3 k4] =" << formatCalibrationMatrix(camera.D.t());
30 yInfo() << "K =" << formatCalibrationMatrix(camera.K);
31 yInfo() << "monocular RMS =" << camera.rms;
32}
33
34void logCalibrationResult(const stereo_calib::CalibrationResult& result)
35{
36 yInfo() << "========== Fisheye calibration result (not written to disk) ==========";
37
38 if(result.leftCamera.isValid())
39 {
40 logCameraCalibration("LEFT", result.leftCamera);
41 }
42 if(result.rightCamera.isValid())
43 {
44 logCameraCalibration("RIGHT", result.rightCamera);
45 }
46
47 if(result.stereo.isValid())
48 {
49 cv::Mat homogeneousTransform = cv::Mat::eye(4, 4, CV_64F);
50 result.stereo.R.copyTo(homogeneousTransform(cv::Rect(0, 0, 3, 3)));
51 result.stereo.T.reshape(1, 3).copyTo(homogeneousTransform(cv::Rect(3, 0, 1, 3)));
52
53 yInfo() << "[STEREO_DISPARITY]";
54 yInfo() << "Stereo RMS =" << result.stereo.rms
55 << "baseline norm =" << cv::norm(result.stereo.T);
56 yInfo() << "R =" << formatCalibrationMatrix(result.stereo.R);
57 yInfo() << "T =" << formatCalibrationMatrix(result.stereo.T.t());
58 // HN is the same homogeneous transform layout used by outputCalib.ini.
59 yInfo() << "HN =" << formatCalibrationMatrix(homogeneousTransform);
60 }
61
62 if(result.rectification.isValid())
63 {
64 yInfo() << "R1 =" << formatCalibrationMatrix(result.rectification.R1);
65 yInfo() << "R2 =" << formatCalibrationMatrix(result.rectification.R2);
66 yInfo() << "P1 =" << formatCalibrationMatrix(result.rectification.P1);
67 yInfo() << "P2 =" << formatCalibrationMatrix(result.rectification.P2);
68 yInfo() << "Q =" << formatCalibrationMatrix(result.rectification.Q);
69 }
70
72 {
73 yInfo() << "Rectification vertical error [mean median RMS p95 max] ="
79 }
80 yInfo() << "======================================================================";
81}
82
83} // namespace
84
85void StereoPairSynchronizer::configure(double tolerance, std::size_t maxQueueSize)
86{
87 this->toleranceSeconds = tolerance;
88 this->maxQueueSize = maxQueueSize;
89}
90
92{
93 leftQueue.clear();
94 rightQueue.clear();
95
96 std::lock_guard<std::mutex> lock(_mutex);
97 stats = SynchronizerStatistics{};
98}
99
100void StereoPairSynchronizer::pushLeft(const ImageOf<PixelRgb>& leftFrame, const Stamp& timestamp)
101{
102 StampedFrame frame;
103
104 // Must be a deep copy. Do not retain pointer to the input-port buffer
105 frame.image.copy(leftFrame);
106 frame.stamp = timestamp;
107
108 leftQueue.push_back(std::move(frame));
109 trimLeftQueue();
110}
111
112void StereoPairSynchronizer::pushRight(const ImageOf<PixelRgb>& rightFrame, const Stamp& timestamp)
113{
114 StampedFrame frame;
115
116 // Must be a deep copy. Do not retain pointer to the input-port buffer
117 frame.image.copy(rightFrame);
118 frame.stamp = timestamp;
119
120 rightQueue.push_back(std::move(frame));
121 trimRightQueue();
122}
123
124
125void StereoPairSynchronizer::trimLeftQueue()
126{
127 while(leftQueue.size() > maxQueueSize)
128 {
129 leftQueue.pop_front();
130 std::lock_guard<std::mutex> lock(_mutex);
131 ++stats.droppedLeftFrames;
132 }
133}
134
135void StereoPairSynchronizer::trimRightQueue()
136{
137 while(rightQueue.size() > maxQueueSize)
138 {
139 rightQueue.pop_front();
140 std::lock_guard<std::mutex> lock(_mutex);
141 ++stats.droppedRightFrames;
142 }
143}
144
146{
147 while(!leftQueue.empty() && !rightQueue.empty())
148 {
149 const double leftStamp = leftQueue.front().stamp.getTime();
150 const double rightStamp = rightQueue.front().stamp.getTime();
151
152 const double absoluteDelta = std::abs(leftStamp - rightStamp);
153 if(absoluteDelta <= toleranceSeconds)
154 {
155 pair.left = std::move(leftQueue.front().image);
156 pair.right = std::move(rightQueue.front().image);
157 pair.leftStamp = leftQueue.front().stamp;
158 pair.rightStamp = rightQueue.front().stamp;
159
160 pair.timeStampDelta = absoluteDelta;
161
162 leftQueue.pop_front();
163 rightQueue.pop_front();
164
165 std::lock_guard<std::mutex> lock(_mutex);
166 ++stats.pairedFrames;
167 stats.accumulatedTimeStampDelta += absoluteDelta;
168 stats.maxTimeStampDelta = std::max(stats.maxTimeStampDelta, absoluteDelta);
169
170 return true;
171 }
172
173 if(leftStamp < rightStamp)
174 {
175 // the oldest left frame cannot be paired with this or any newer right frame, so drop it
176 leftQueue.pop_front();
177 std::lock_guard<std::mutex> lock(_mutex);
178 ++stats.droppedLeftFrames;
179 }
180 else
181 {
182 rightQueue.pop_front();
183 std::lock_guard<std::mutex> lock(_mutex);
184 ++stats.droppedRightFrames;
185 }
186 }
187 return false;
188}
189
190
191stereoCalibThread::stereoCalibThread(ResourceFinder &rf, Port* commPort, const char *imageDir)
192{
193 moduleName=rf.check("name", Value("stereoCalib"),"module name (string)").asString().c_str();
194 robotName=rf.check("robotName",Value("icub"), "module name (string)").asString().c_str();
195
196 this->inputLeftPortName = "/"+moduleName;
197 this->inputLeftPortName +=rf.check("imgLeft",Value("/cam/left:i"),"Input image port (string)").asString().c_str();
198
199 this->inputRightPortName = "/"+moduleName;
200 this->inputRightPortName += rf.check("imgRight", Value("/cam/right:i"),"Input image port (string)").asString().c_str();
201
202 this->outNameRight = "/"+moduleName;
203 this->outNameRight += rf.check("outRight",Value("/cam/right:o"),"Output image port (string)").asString().c_str();
204
205 this->outNameLeft = "/"+moduleName;
206 this->outNameLeft +=rf.check("outLeft",Value("/cam/left:o"),"Output image port (string)").asString().c_str();
207
208 Bottle stereoCalibOpts=rf.findGroup("STEREO_CALIBRATION_CONFIGURATION");
209 this->boardWidth = stereoCalibOpts.check("boardWidth", Value(8)).asInt32();
210 this->boardHeight= stereoCalibOpts.check("boardHeight", Value(6)).asInt32();
211 this->numOfPairs= stereoCalibOpts.check("numberOfPairs", Value(30)).asInt32();
212 if(this->numOfPairs < 3)
213 {
214 yWarning() << "numberOfPairs must be at least 3; using 3";
215 this->numOfPairs = 3;
216 }
217 this->squareSize= (float)stereoCalibOpts.check("boardSize", Value(0.09241)).asFloat64();
218 this->boardType= stereoCalibOpts.check("boardType", Value("CHESSBOARD")).asString();
219 const double syncToleranceMs = stereoCalibOpts.check("syncToleranceMs", Value(20.0)).asFloat64();
220 _syncToleranceSeconds = syncToleranceMs / 1000.0;
221 const int configuredQueueSize = stereoCalibOpts.check("syncQueueSize", Value(5)).asInt32();
222 if(configuredQueueSize <= 0)
223 {
224 yWarning() << "Invalid syncQueueSize; using 5";
225 _syncQueueSize = 5;
226 }
227 else
228 {
229 _syncQueueSize = static_cast<std::size_t>(configuredQueueSize);
230 }
231
232 if(_syncToleranceSeconds <= 0.0)
233 {
234 yWarning() << "Invalid syncToleranceMs; using 20 ms";
235 _syncToleranceSeconds = 0.020;
236 }
237
238 synchronizer.configure(_syncToleranceSeconds, _syncQueueSize);
239
240 this->minCaptureIntervalSeconds = stereoCalibOpts.check("minCaptureIntervalSeconds", Value(2.0)).asFloat64();
241 this->minimumBoardSpanRatio = stereoCalibOpts.check("minimumBoardSpanRatio", Value(0.15)).asFloat64();
242
243 this->commandPort=commPort;
244 this->imageDir=imageDir;
245 this->collectionResetRequested.store(false);
246 this->calibrationState.store(CalibrationState::Idle);
247 this->currentPathDir=rf.getHomeContextPath().c_str();
248 const bool legacyMonoRequested =
249 stereoCalibOpts.check("MonoCalib", Value(0)).asInt32() != 0;
250 // All new calibration modes use the synchronized-observation pipeline.
251 // In particular, completion must never bypass CalibrationWriter.
252 this->stereo = true;
253 this->camCalibFile=rf.getHomeContextPath().c_str();
254 this->standalone = rf.check("standalone");
255 string fileName= "outputCalib.ini"; //rf.find("from").asString().c_str();
256
257 this->camCalibFile=this->camCalibFile+"/"+fileName.c_str();
258
259 _observationsFile = stereoCalibOpts.check(
260 "observationsFile", Value("calibrationObservations.yml")).asString();
261 if(!_observationsFile.empty() && _observationsFile.front() != '/')
262 {
263 _observationsFile = this->imageDir + "/" + _observationsFile;
264 }
265
266
267 _chessboardConfiguration.cornersX = this->boardWidth;
268 _chessboardConfiguration.cornersY = this->boardHeight;
269
270 _chessboardConfiguration.squareSizeMeters = this->squareSize;
271
272 _saveImages = stereoCalibOpts.check("saveImages", Value(1)).asInt32() != 0;
273 _drawDiagnosticCorners = stereoCalibOpts.check("drawDiagnosticCorners", Value(1)).asInt32() != 0;
274
275 const std::string configuredMode = stereoCalibOpts.check("calibrationMode", Value("")).asString();
276 if(configuredMode == "MonocularLeft")
277 {
279 }
280 else if(configuredMode == "MonocularRight")
281 {
283 }
284 else if(configuredMode == "MonocularBoth")
285 {
287 }
288 else if(configuredMode.empty() && legacyMonoRequested)
289 {
290 yWarning() << "MonoCalib is deprecated; using MonocularLeft with the synchronized observation pipeline.";
292 }
293 else if(configuredMode.empty() || configuredMode == "StereoFull")
294 {
296 }
297 else
298 {
299 yWarning() << "Unknown calibrationMode; using StereoFull:" << configuredMode;
301 }
302
303 if(!_chessboardConfiguration.isValid())
304 {
305 yError() << "Invalid chessboard configuration";
306 }
307}
308
310{
311 if (!imagePortInLeft.open(inputLeftPortName.c_str())) {
312 cout << ": unable to open port " << inputLeftPortName << endl;
313 return false;
314 }
315
316 if (!imagePortInRight.open(inputRightPortName.c_str())) {
317 cout << ": unable to open port " << inputRightPortName << endl;
318 return false;
319 }
320
321 if (!outPortLeft.open(outNameLeft.c_str())) {
322 cout << ": unable to open port " << outNameLeft << endl;
323 return false;
324 }
325
326 if (!outPortRight.open(outNameRight.c_str())) {
327 cout << ": unable to open port " << outNameRight << endl;
328 return false;
329 }
330
331 //mono calibration does not need the joint positions initialised below
332 if(!stereo || standalone) return true;
333
334 Property optHead;
335 optHead.put("device","remote_controlboard");
336 optHead.put("remote",("/"+robotName+"/head").c_str());
337 optHead.put("local","/"+moduleName+"/client/head");
338 if (polyHead.open(optHead))
339 polyHead.view(posHead);
340 else
341 {
342 cout<<"Devices not available"<<endl;
343 return false;
344 }
345
346 Property optTorso;
347 optTorso.put("device","remote_controlboard");
348 optTorso.put("remote",("/"+robotName+"/torso").c_str());
349 optTorso.put("local","/"+moduleName+"/client/torso");
350
351 bool useTorso=true;
352 if (polyTorso.open(optTorso))
353 polyTorso.view(posTorso);
354 else
355 {
356 yWarning("Unable to connect to torso! Continuing without...");
357 useTorso=false;
358 }
359
360 yarp::sig::Vector head_angles(6,0.0);
361 posHead->getEncoders(head_angles.data());
362
363 yarp::sig::Vector torso_angles(3,0.0);
364 if (useTorso)
365 posTorso->getEncoders(torso_angles.data());
366
367 qL.resize(torso_angles.length()+head_angles.length()-1);
368 for(size_t i=0; i<torso_angles.length(); i++)
369 qL[i]=torso_angles[torso_angles.length()-i-1];
370
371 for(size_t i=0; i<head_angles.length()-2; i++)
372 qL[i+torso_angles.length()]=head_angles[i];
373 qL[7]=head_angles[4]+(0.5-(LEFT))*head_angles[5];
375
376 qR.resize(torso_angles.length()+head_angles.length()-1);
377 for(size_t i=0; i<torso_angles.length(); i++)
378 qR[i]=torso_angles[torso_angles.length()-i-1];
379
380 for(size_t i=0; i<head_angles.length()-2; i++)
381 qR[i+torso_angles.length()]=head_angles[i];
382 qR[7]=head_angles[4]+(0.5-(RIGHT))*head_angles[5];
384
385 return true;
386}
388 yInfo("Running synchronized fisheye calibration pipeline... \n");
389 stereoCalibRun();
390}
391
392void stereoCalibThread::processSynchronizedPair(SynchronizedPair& pair, Size boardSize)
393{
394 // Check minimum capture interval
395 const double pairTime = 0.5 * (pair.leftStamp.getTime() + pair.rightStamp.getTime()); //average pair timestamp
396 const double previousProcessedCandidateTime = lastProcessedCandidateTime;
397 if(previousProcessedCandidateTime >= 0.0 && (pairTime - previousProcessedCandidateTime) < minCaptureIntervalSeconds)
398 {
399 yDebug() << "Skipping candidate pair due to minimum capture interval";
400 return;
401 }
402 if(previousProcessedCandidateTime >= 0.0)
403 {
404 yDebug() << "Timestamp delta between processed pairs:" << (pairTime - previousProcessedCandidateTime) << "seconds";
405 }
406 lastProcessedCandidateTime = pairTime;
407
408
409 bool foundL=false;
410 bool foundR=false;
411 const Size leftSize(pair.left.width(), pair.left.height());
412 const Size rightSize(pair.right.width(), pair.right.height());
413
414 if(leftSize != _expectedImageSize || rightSize != _expectedImageSize)
415 {
416 if(_expectedImageSize.empty())
417 {
418 _expectedImageSize = leftSize;
419 }
420 }
421
422 if(leftSize != _expectedImageSize || rightSize != _expectedImageSize)
423 {
424 yError() << "Left and right images have different sizes:" <<
425 "Left:" << leftSize.width << "x" << leftSize.height <<
426 "Right:" << rightSize.width << "x" << rightSize.height;
427 {
428 std::lock_guard<std::mutex> lock(mtx);
429 _calibrationError = "Input images do not match the expected calibration resolution.";
430 _calibrationResults = stereo_calib::CalibrationResult{};
431 }
432 calibrationState.store(CalibrationState::Error);
433
434 return;
435 }
436
437 LeftRgb=yarp::cv::toCvMat(pair.left);
438 RightRgb=yarp::cv::toCvMat(pair.right);
439
440 // Color adjust
441 Mat leftGray, rightGray;
442 cvtColor(LeftRgb,leftGray,CV_RGB2GRAY);
443 cvtColor(RightRgb,rightGray,CV_RGB2GRAY);
444
445 std::vector<Point2f> leftCorners;
446 std::vector<Point2f> rightCorners;
447
448 if(boardType == "CIRCLES_GRID") {
449 foundL = findCirclesGrid(LeftRgb, boardSize, leftCorners, CALIB_CB_SYMMETRIC_GRID | CALIB_CB_CLUSTERING);
450 foundR = findCirclesGrid(RightRgb, boardSize, rightCorners, CALIB_CB_SYMMETRIC_GRID | CALIB_CB_CLUSTERING);
451 } else if(boardType == "ASYMMETRIC_CIRCLES_GRID") {
452 foundL = findCirclesGrid(LeftRgb, boardSize, leftCorners, CALIB_CB_ASYMMETRIC_GRID | CALIB_CB_CLUSTERING);
453 foundR = findCirclesGrid(RightRgb, boardSize, rightCorners, CALIB_CB_ASYMMETRIC_GRID | CALIB_CB_CLUSTERING);
454 } else if(boardType == "CHESSBOARD_SECTOR_BASED") {
455 foundL = findChessboardCornersSB(leftGray, boardSize, leftCorners);
456 foundR = findChessboardCornersSB(rightGray, boardSize, rightCorners);
457 } else {
458 foundL = findChessboardCorners(leftGray, boardSize, leftCorners, CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE | CALIB_CB_FILTER_QUADS);
459 foundR = findChessboardCorners(rightGray, boardSize, rightCorners, CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE | CALIB_CB_FILTER_QUADS);
460 }
461
462 if(foundL && foundR)
463 {
464 yDebug() << "Found chessboard corners in both left and right images";
465
466 const Rect leftBoardBounds = boundingRect(leftCorners);
467 const Rect rightBoardBounds = boundingRect(rightCorners);
468 const bool leftBoardTooSmall =
469 leftBoardBounds.width < leftSize.width * minimumBoardSpanRatio ||
470 leftBoardBounds.height < leftSize.height * minimumBoardSpanRatio;
471 const bool rightBoardTooSmall =
472 rightBoardBounds.width < rightSize.width * minimumBoardSpanRatio ||
473 rightBoardBounds.height < rightSize.height * minimumBoardSpanRatio;
474
475 if(leftBoardTooSmall || rightBoardTooSmall)
476 {
477 yWarning() << "Skipping stereo pair: chessboard is too small."
478 << "Left board:" << leftBoardBounds.width << "x" << leftBoardBounds.height
479 << "of" << leftSize.width << "x" << leftSize.height << ";"
480 << "right board:" << rightBoardBounds.width << "x" << rightBoardBounds.height
481 << "of" << rightSize.width << "x" << rightSize.height << "."
482 << "Each board must span at least" << (minimumBoardSpanRatio * 100.0)
483 << "% of both image dimensions.";
484
485 ++_rejectedDetections;
486 return;
487 }
488
489 TermCriteria criteria = TermCriteria(TermCriteria::EPS+TermCriteria::COUNT, 30, 0.01);
490 cornerSubPix(leftGray, leftCorners, Size(5,5), Size(-1,-1), criteria);
491 cornerSubPix(rightGray, rightCorners, Size(5,5), Size(-1,-1), criteria);
492
493 // Detection and geometry have succeeded. Build and validate the
494 // complete observation before assigning its accepted index.
496 observation.imageSize = Size(pair.left.width(), pair.left.height());
497
498 observation.objectPoints = _chessboardConfiguration.createObjectPoints();
499 observation.leftImagePoints = leftCorners;
500 observation.rightImagePoints = rightCorners;
501
502 observation.leftTimestampSeconds = pair.leftStamp.getTime();
503 observation.rightTimestampSeconds = pair.rightStamp.getTime();
504 observation.timestampDeltaSeconds = pair.timeStampDelta;
505
506 observation.leftSequenceNumber = pair.leftStamp.getCount();
507 observation.rightSequenceNumber = pair.rightStamp.getCount();
508
509 if(!observation.isValid())
510 {
511 yError() << "Generated invalid stereo observation";
512 ++_rejectedDetections;
513 return;
514 }
515
516 std::size_t observationIndex = 0;
517 observationIndex = _observations.size(); //TODO: check this
518
519 // Raw images are saved before drawing any optional diagnostics and
520 // only after the pair has become an accepted observation candidate.
521 if(_saveImages)
522 {
523 std::string imageError;
524 if(!_calibrationWriter.writeImagePair(imageDir, observationIndex,
525 LeftRgb, RightRgb,
526 observation.leftImageFilename,
527 observation.rightImageFilename,
528 imageError))
529 {
530 yError() << "Could not save accepted calibration image pair:" << imageError;
531 {
532 std::lock_guard<std::mutex> lock(mtx);
533 _calibrationError = imageError;
534 _calibrationResults = stereo_calib::CalibrationResult{};
535 }
536 calibrationState.store(CalibrationState::Error);
537 return;
538 }
539 }
540
541 _observations.push_back(std::move(observation));
542
543 // Diagnostic overlays are deliberately the final step: they never
544 // affect the persisted raw dataset or the stored corner coordinates.
545 if(_drawDiagnosticCorners)
546 {
547 drawChessboardCorners(LeftRgb, boardSize, leftCorners, foundL);
548 drawChessboardCorners(RightRgb, boardSize, rightCorners, foundR);
549 }
550
551 ImageOf<PixelRgb>& outimL = outPortLeft.prepare();
552 outimL = pair.left;
553 outPortLeft.setEnvelope(pair.leftStamp);
554 outPortLeft.write();
555
556 ImageOf<PixelRgb>& outimR = outPortRight.prepare();
557 outimR = pair.right;
558 outPortRight.setEnvelope(pair.rightStamp);
559 outPortRight.write();
560
561 }
562 else
563 {
564 ++_rejectedDetections;
565 }
566
567 return;
568}
569
571{
572 StereoCalibStatus result;
573
574 std::lock_guard<std::mutex> lock(mtx);
575
576 switch(calibrationState.load())
577 {
578 case CalibrationState::Idle:
579 result.state = "Idle";
580 break;
581 case CalibrationState::Collecting:
582 result.state = "Collecting";
583 break;
584 case CalibrationState::Calibrating:
585 result.state = "Calibrating";
586 break;
587 case CalibrationState::Completed:
588 result.state = "Completed";
589 break;
590 case CalibrationState::Error:
591 result.state = "Error";
592 break;
593 }
594
595 const auto syncStats = synchronizer.getStatistics();
596
597 result.pairedFrames = syncStats.pairedFrames;
598 result.droppedLeftFrames = syncStats.droppedLeftFrames;
599 result.droppedRightFrames = syncStats.droppedRightFrames;
600
601 if(syncStats.pairedFrames > 0)
602 {
603 result.meanTimestampDeltaMs = 1000.0 * syncStats.accumulatedTimeStampDelta / static_cast<double>(syncStats.pairedFrames);
604 }
605
606 result.maxTimestampDeltaMs = 1000.0 * syncStats.maxTimeStampDelta;
607
608 result.lastCalibrationError = _calibrationError;
609 if(calibrationState.load() == CalibrationState::Completed && _calibrationResults.isValid())
610 {
611 result.calibrationAvailable = true;
612 switch(_calibrationResults.mode)
613 {
615 result.calibrationMode = "MonocularLeft";
616 break;
618 result.calibrationMode = "MonocularRight";
619 break;
621 result.calibrationMode = "MonocularBoth";
622 break;
624 result.calibrationMode = "StereoFull";
625 break;
626 }
627
628 if(_calibrationResults.leftCamera.isValid())
629 {
630 result.leftMonocularRms = _calibrationResults.leftCamera.rms;
631 }
632 if(_calibrationResults.rightCamera.isValid())
633 {
634 result.rightMonocularRms = _calibrationResults.rightCamera.rms;
635 }
636 if(_calibrationResults.stereo.isValid())
637 {
638 result.stereoRms = _calibrationResults.stereo.rms;
639 result.baselineNorm = _calibrationResults.quality.baseline;
640 }
641 if(_calibrationResults.mode == stereo_calib::CalibrationMode::StereoFull)
642 {
646 _calibrationResults.quality.p95VerticalRectificationErrorPx;
648 _calibrationResults.quality.maxVerticalRectificationErrorPx;
649 }
650 }
651
652 return result;
653}
654
655bool stereoCalibThread::shouldQueueFrameForCollection(const Stamp& timestamp) const
656{
657 return lastProcessedCandidateTime < 0.0 ||
658 (timestamp.getTime() - lastProcessedCandidateTime) >= minCaptureIntervalSeconds;
659}
660
661void stereoCalibThread::stereoCalibRun()
662{
663 Size boardSize, imageSize;
664 boardSize.width=this->boardWidth;
665 boardSize.height=this->boardHeight;
666 int count=1;
667
668 while (!isStopping())
669 {
670 // Keep the worker alive for RPC status inspection after a calibration
671 // failure, but stop consuming/processing camera data until a new start
672 // request changes the state back to Collecting.
673 if(calibrationState.load() == CalibrationState::Error)
674 {
675 Time::delay(0.1);
676 continue;
677 }
678
679 if(collectionResetRequested.exchange(false))
680 {
681 synchronizer.reset();
682 lastProcessedCandidateTime = -1.0;
683 _observations.clear();
684 _rejectedDetections = 0;
685 {
686 std::lock_guard<std::mutex> lock(mtx);
687 _calibrationResults = stereo_calib::CalibrationResult{};
688 _calibrationError.clear();
689 }
690 }
691
692 bool areFramesReceived = false;
693 ImageOf<PixelRgb> *tmpL = imagePortInLeft.read(false);
694
695 if(tmpL != nullptr)
696 {
697 areFramesReceived = true;
698
699 Stamp TSLeft;
700 imagePortInLeft.getEnvelope(TSLeft);
701
702 // Always publish preview immediately
703 ImageOf<PixelRgb>& outimL = outPortLeft.prepare();
704 outimL = *tmpL;
705 outPortLeft.setEnvelope(TSLeft);
706 outPortLeft.write();
707
708 // Only enqueue a separate copy while collecting
709 if(calibrationState.load() == CalibrationState::Collecting &&
710 shouldQueueFrameForCollection(TSLeft))
711 {
712 synchronizer.pushLeft(*tmpL, TSLeft); // this is done for not consuming memory bandwidth for data that will never be used by the calibration
713 }
714 }
715
716 ImageOf<PixelRgb> *tmpR = imagePortInRight.read(false);
717 if(tmpR!=nullptr)
718 {
719 areFramesReceived = true;
720
721 Stamp TSRight;
722 imagePortInRight.getEnvelope(TSRight);
723
724 // Always publish preview immediately
725 ImageOf<PixelRgb>& outimR=outPortRight.prepare();
726 outimR = *tmpR;
727 outPortRight.setEnvelope(TSRight);
728 outPortRight.write();
729
730
731 // Only enqueue a separate copy while collecting
732 if(calibrationState.load() == CalibrationState::Collecting &&
733 shouldQueueFrameForCollection(TSRight))
734 {
735 synchronizer.pushRight(*tmpR, TSRight);
736 }
737 }
738
739 std::vector<stereo_calib::StereoObservation> observationSnapshot;
740 {
741 std::lock_guard<std::mutex> lock(mtx);
742 if(calibrationState.load() == CalibrationState::Collecting)
743 {
744 SynchronizedPair pair;
745 while(synchronizer.tryPopPair(pair))
746 {
747 // Process the synchronized pair
748 processSynchronizedPair(pair, boardSize);
749
750 if(_observations.size() >= static_cast<std::size_t>(numOfPairs))
751 {
752 yInfo("Collected %zu valid stereo observations. Stopping collection.", _observations.size());
753 observationSnapshot = _observations;
754 calibrationState.store(CalibrationState::Calibrating);
755 yInfo() << "Observation collection complete";
756 break;
757 }
758 }
759 }
760 }
761
762 if(calibrationState.load() == CalibrationState::Calibrating)
763 {
764 yInfo() << "Starting the calibration process";
765 if(observationSnapshot.empty())
766 {
767 observationSnapshot = _observations;
768 }
769 _calibrationOptions.imageSize = observationSnapshot.front().imageSize;
770 _calibrationOptions.cameraFocalLengthGuess = 625.0;
771
772 stereo_calib::CalibrationResult calibrationResult;
773 std::string calibrationError;
774 const bool success = _calibrationEngine.calibrate(
775 observationSnapshot,
776 _calibrationOptions,
777 calibrationResult,
778 calibrationError);
779 if(!success)
780 {
781 // The engine converts OpenCV exceptions into a diagnostic. Log
782 // it here, where YARP logging is allowed, and stop calibration
783 // processing while keeping the Error state and status available.
784 yError() << "Fisheye calibration failed:" << calibrationError;
785 {
786 std::lock_guard<std::mutex> lock(mtx);
787 _calibrationResults = stereo_calib::CalibrationResult{};
788 _calibrationError = calibrationError.empty()
789 ? "Fisheye calibration failed without an error message."
790 : calibrationError;
791 }
792 calibrationState.store(CalibrationState::Error);
793 continue;
794 }
795
796 calibrationResult.quality.rejectedDetections = _rejectedDetections;
797 calibrationResult.quality.synchronizedPairs = synchronizer.getStatistics().pairedFrames;
798
799 std::string persistenceError;
800 if(!_calibrationWriter.write(camCalibFile, calibrationResult, persistenceError))
801 {
802 yError() << "Could not save calibration results:" << persistenceError;
803 {
804 std::lock_guard<std::mutex> lock(mtx);
805 _calibrationResults = std::move(calibrationResult);
806 _calibrationError = persistenceError;
807 }
808 calibrationState.store(CalibrationState::Error);
809 continue;
810 }
811 if(!_calibrationWriter.writeObservations(_observationsFile, observationSnapshot, persistenceError))
812 {
813 yError() << "Could not save calibration observations:" << persistenceError;
814 {
815 std::lock_guard<std::mutex> lock(mtx);
816 _calibrationResults = std::move(calibrationResult);
817 _calibrationError = persistenceError;
818 }
819 calibrationState.store(CalibrationState::Error);
820 continue;
821 }
822
823 logCalibrationResult(calibrationResult);
824 {
825 std::lock_guard<std::mutex> lock(mtx);
826 _calibrationResults = std::move(calibrationResult);
827 _calibrationError.clear();
828 }
829
830 calibrationState.store(CalibrationState::Completed);
831 yInfo() << "Entire calibration process completed";
832 }
833
834 if(!areFramesReceived)
835 {
836 Time::delay(0.001); // Sleep for a short duration to avoid busy waiting
837 }
838 cout.flush();
839 }
840}
841
842
843
844void stereoCalibThread::monoCalibRun()
845{
846 while(imagePortInLeft.getInputCount()==0 && imagePortInRight.getInputCount()==0)
847 {
848 yInfo("Connect one camera.. \n");
849 Time::delay(1.0);
850
851 if(isStopping())
852 return;
853
854 }
855
856 bool left= imagePortInLeft.getInputCount()>0?true:false;
857
858 string cameraName;
859
860 if(left)
861 cameraName="LEFT";
862 else
863 cameraName="RIGHT";
864
865 yInfo("CALIBRATING %s CAMERA \n",cameraName.c_str());
866
867
868 int count=1;
869 Size boardSize, imageSize;
870 boardSize.width=this->boardWidth;
871 boardSize.height=this->boardHeight;
872
873
874
875 while (!isStopping()) {
876 if(left)
877 imageL = std::move(imagePortInLeft.read(false));
878 else
879 imageL = std::move(imagePortInRight.read(false));
880
881 if(imageL!=NULL){
882 bool foundL=false;
883 mtx.lock();
884 if(calibrationState.load() == CalibrationState::Calibrating) {
885
886 string pathImg=imageDir;
887 preparePath(pathImg.c_str(),pathL,pathR,count);
888 string iml(pathL);
889 LeftRgb=yarp::cv::toCvMat(*imageL);
890 std::vector<Point2f> pointbufL;
891
892 if(boardType == "CIRCLES_GRID") {
893 foundL = findCirclesGrid(LeftRgb, boardSize, pointbufL, CALIB_CB_SYMMETRIC_GRID | CALIB_CB_CLUSTERING);
894 } else if(boardType == "ASYMMETRIC_CIRCLES_GRID") {
895 foundL = findCirclesGrid(LeftRgb, boardSize, pointbufL, CALIB_CB_ASYMMETRIC_GRID | CALIB_CB_CLUSTERING);
896 } else {
897 foundL = findChessboardCorners(LeftRgb, boardSize, pointbufL, CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_NORMALIZE_IMAGE);
898 }
899
900 if(foundL) {
901 cvtColor(LeftRgb,LeftRgb,CV_RGB2BGR);
902 saveImage(pathImg.c_str(),LeftRgb,count);
903 imageListL.push_back(iml);
904 Mat cL(pointbufL);
905 drawChessboardCorners(LeftRgb, boardSize, cL, foundL);
906 count++;
907 }
908
909 if(count>numOfPairs) {
910 yInfo(" Running %s Camera Calibration... \n", cameraName.c_str());
911 monoCalibration(imageListL,this->boardWidth,this->boardHeight,this->Kleft,this->DistL,cameraName.c_str());
912
913 yInfo(" Saving Calibration Results... \n");
914 updateIntrinsics(LeftRgb.cols,LeftRgb.rows,Kleft.at<double>(0,0),Kleft.at<double>(1,1),Kleft.at<double>(0,2),
915 Kleft.at<double>(1,2),DistL.at<double>(0,0),DistL.at<double>(0,1),DistL.at<double>(0,2),
916 DistL.at<double>(0,3),left?"CAMERA_CALIBRATION_LEFT":"CAMERA_CALIBRATION_RIGHT");
917 yInfo("Calibration Results Saved in %s \n", camCalibFile.c_str());
918
919 calibrationState.store(CalibrationState::Completed);
920 count=1;
921 imageListL.clear();
922 }
923 }
924 mtx.unlock();
925 ImageOf<PixelRgb>& outimL=outPortLeft.prepare();
926 outimL=*imageL;
927 outPortLeft.write();
928
929 ImageOf<PixelRgb>& outimR=outPortRight.prepare();
930 outimR=*imageL;
931 outPortRight.write();
932
933 cout.flush();
934
935 }
936 }
937
938
939 }
941{
942 imagePortInRight.close();
943 imagePortInLeft.close();
944 outPortLeft.close();
945 outPortRight.close();
946 commandPort->close();
947
948 if (polyHead.isValid())
949 polyHead.close();
950
951 if (polyTorso.isValid())
952 polyTorso.close();
953}
954
956 // TODO: the following 2 should not ne necessary since we already store their state in stop()
957 calibrationState.store(CalibrationState::Idle);
958 collectionResetRequested.store(false);
959 imagePortInRight.interrupt();
960 imagePortInLeft.interrupt();
961 outPortLeft.interrupt();
962 outPortRight.interrupt();
963 commandPort->interrupt();
964
965}
967
968 std::lock_guard<std::mutex> lock(mtx);
969
970 const CalibrationState currentState = calibrationState.load();
971
972 if(currentState == CalibrationState::Collecting || currentState == CalibrationState::Calibrating)
973 {
974 yWarning() << "Cannot start a new calibration while calibration is already running";
975 return;
976 }
977
978 _calibrationResults = stereo_calib::CalibrationResult{};
979 _calibrationError.clear();
980 collectionResetRequested.store(true);
981 calibrationState.store(CalibrationState::Collecting);
982
983 yInfo() << "Calibration collection started";
984}
985
987 std::lock_guard<std::mutex> lock(mtx);
988
989 if(calibrationState.load() != CalibrationState::Collecting)
990 {
991 yWarning() << "Cannot stop calibration collection when it is not running";
992 return;
993 }
994 calibrationState.store(CalibrationState::Idle);
995 collectionResetRequested.store(true);
996
997 yInfo() << "Calibration collection stopped";
998}
999
1000void stereoCalibThread::printMatrix(Mat &matrix) {
1001 int row = matrix.rows;
1002 int col = matrix.cols;
1003 cout << endl;
1004 for(int i = 0; i < matrix.rows; i++)
1005 {
1006 const double* Mi = matrix.ptr<double>(i);
1007 for(int j = 0; j < matrix.cols; j++)
1008 cout << Mi[j] << " ";
1009 cout << endl;
1010 }
1011 cout << endl;
1012}
1013
1014
1015bool stereoCalibThread::checkTS(double TSLeft, double TSRight, double th) {
1016 double diff = fabs(TSLeft-TSRight);
1017 if(diff <th)
1018 return true;
1019 else return false;
1020
1021}
1022
1023void stereoCalibThread::preparePath(const char * imageDir, char* pathL, char* pathR, int count) {
1024 char num[5];
1025 sprintf(num, "%i", count);
1026
1027
1028 strncpy(pathL,imageDir, strlen(imageDir));
1029 pathL[strlen(imageDir)]='\0';
1030 strcat(pathL,"left");
1031 strcat(pathL,num);
1032 strcat(pathL,".png");
1033
1034 strncpy(pathR,imageDir, strlen(imageDir));
1035 pathR[strlen(imageDir)]='\0';
1036 strcat(pathR,"right");
1037 strcat(pathR,num);
1038 strcat(pathR,".png");
1039
1040}
1041
1042
1043void stereoCalibThread::saveStereoImage(const char * imageDir, const Mat& left, const Mat& right, int num) {
1044 char pathL[256];
1045 char pathR[256];
1046 preparePath(imageDir, pathL,pathR,num);
1047
1048 yInfo("Saving stereo images number %d \n",num);
1049
1050 imwrite(pathL,left);
1051 imwrite(pathR,right);
1052}
1053
1054void stereoCalibThread::saveImage(const char * imageDir, const Mat& left, int num) {
1055 char pathL[256];
1056 preparePath(imageDir, pathL,pathR,num);
1057
1058 yInfo("Saving images number %d \n",num);
1059
1060 imwrite(pathL,left);
1061}
1062
1063bool stereoCalibThread::updateIntrinsics(int width, int height, double fx, double fy,double cx, double cy, double k1, double k2, double k3, double k4, const string& groupname){
1064
1065 std::vector<string> lines;
1066
1067 bool append = false;
1068
1069 ifstream in;
1070 in.open(camCalibFile.c_str()); //camCalibFile.c_str());
1071
1072 if(in.is_open()){
1073 // file exists
1074 string line;
1075 bool sectionFound = false;
1076 bool sectionClosed = false;
1077
1078 // process lines
1079 while(std::getline(in, line)){
1080 // check if we left calibration section
1081 if (sectionFound == true && line.find("[", 0) != string::npos)
1082 sectionClosed = true; // also valid if no groupname specified
1083 // check if we enter calibration section
1084 if (line.find(string("[") + groupname + string("]"), 0) != string::npos)
1085 sectionFound = true;
1086 // if no groupname specified
1087 if (groupname == "")
1088 sectionFound = true;
1089 // if we are in calibration section (or no section/group specified)
1090 if (sectionFound == true && sectionClosed == false){
1091 // replace w line
1092 if (line.find("w",0) ==0){
1093 stringstream ss;
1094 ss << width;
1095 line = "w " + string(ss.str());
1096 }
1097 // replace h line
1098 if (line.find("h",0) ==0){
1099 stringstream ss;
1100 ss << height;
1101 line = "h " + string(ss.str());
1102 }
1103 // replace fx line
1104 if (line.find("fx",0) != string::npos){
1105 stringstream ss;
1106 ss << fx;
1107 line = "fx " + string(ss.str());
1108 }
1109 // replace fy line
1110 if (line.find("fy",0) != string::npos){
1111 stringstream ss;
1112 ss << fy;
1113 line = "fy " + string(ss.str());
1114 }
1115 // replace cx line
1116 if (line.find("cx",0) != string::npos){
1117 stringstream ss;
1118 ss << cx;
1119 line = "cx " + string(ss.str());
1120 }
1121 // replace cy line
1122 if (line.find("cy",0) != string::npos){
1123 stringstream ss;
1124 ss << cy;
1125 line = "cy " + string(ss.str());
1126 }
1127 // replace k1 line
1128 if (line.find("k1",0) != string::npos){
1129 stringstream ss;
1130 ss << k1;
1131 line = "k1 " + string(ss.str());
1132 }
1133 // replace k2 line
1134 if (line.find("k2",0) != string::npos){
1135 stringstream ss;
1136 ss << k2;
1137 line = "k2 " + string(ss.str());
1138 }
1139 // replace k3 line
1140 if (line.find("k3",0) != string::npos){
1141 stringstream ss;
1142 ss << k3;
1143 line = "k3 " + string(ss.str());
1144 }
1145 // replace k4 line
1146 if (line.find("k4",0) != string::npos){
1147 stringstream ss;
1148 ss << k4;
1149 line = "k4 " + string(ss.str());
1150 }
1151 }
1152 // buffer line
1153 lines.push_back(line);
1154 }
1155
1156 in.close();
1157
1158 // rewrite file
1159 if (!sectionFound){
1160 append = true;
1161 cout << "Camera calibration parameter section " + string("[") + groupname + string("]") + " not found in file " << camCalibFile << ". Adding group..." << endl;
1162 }
1163 else{
1164 // rewrite file
1165 ofstream out;
1166 out.open(camCalibFile.c_str(), ios::trunc);
1167 if (out.is_open()){
1168 for (int i = 0; i < (int)lines.size(); i++)
1169 out << lines[i] << endl;
1170 out.close();
1171 }
1172 else
1173 return false;
1174 }
1175
1176 }
1177 else{
1178 append = true;
1179 }
1180
1181 if (append){
1182 // file doesn't exist or section is appended
1183 ofstream out;
1184 out.open(camCalibFile.c_str(), ios::app);
1185 if (out.is_open()){
1186 out << string("[") + groupname + string("]") << endl;
1187 out << endl;
1188 out << "w " << width << endl;
1189 out << "h " << height << endl;
1190 out << "fx " << fx << endl;
1191 out << "fy " << fy << endl;
1192 out << "cx " << cx << endl;
1193 out << "cy " << cy << endl;
1194 out << "k1 " << k1 << endl;
1195 out << "k2 " << k2 << endl;
1196 out << "k3 " << k3 << endl;
1197 out << "k4 " << k4 << endl;
1198 out << endl;
1199 out.close();
1200 }
1201 else
1202 return false;
1203 }
1204
1205 return true;
1206}
1207
1208double stereoCalibThread::monoCalibration(const vector<string>& imageList, int boardWidth, int boardHeight, Mat &K, Mat &Dist, const char* cameraName)
1209{
1210 vector<vector<Point2f> > imagePoints;
1211 Size boardSize, imageSize;
1212 boardSize.width=boardWidth;
1213 boardSize.height=boardHeight;
1214 int flags=0;
1215 int i;
1216
1217 float squareSize = this->squareSize;
1218 float aspectRatio = 1.f;
1219 if (squareSize <= 0.0f)
1220 {
1221 yWarning("Mono calibration: invalid square size %f, using 1.0", squareSize);
1222 squareSize = 1.0f;
1223 }
1224
1225 Mat view, viewGray;
1226
1227 for(i = 0; i<(int)imageList.size();i++)
1228 {
1229 view = cv::imread(imageList[i], IMREAD_COLOR);
1230 if (view.empty())
1231 {
1232 yWarning("Mono calibration: could not read image %s", imageList[i].c_str());
1233 continue;
1234 }
1235
1236 if (imageSize == Size())
1237 imageSize = view.size();
1238 else if (view.size() != imageSize)
1239 {
1240 yWarning("Mono calibration: image %s has size %dx%d while first image had %dx%d",
1241 imageList[i].c_str(), view.cols, view.rows, imageSize.width, imageSize.height);
1242 continue;
1243 }
1244
1245 vector<Point2f> pointbuf;
1246 cvtColor(view, viewGray, CV_BGR2GRAY);
1247
1248 bool found = false;
1249 if(boardType == "CIRCLES_GRID") {
1250 found = findCirclesGrid(view, boardSize, pointbuf, CALIB_CB_SYMMETRIC_GRID | CALIB_CB_CLUSTERING);
1251 } else if(boardType == "ASYMMETRIC_CIRCLES_GRID") {
1252 found = findCirclesGrid(view, boardSize, pointbuf, CALIB_CB_ASYMMETRIC_GRID | CALIB_CB_CLUSTERING);
1253 } else {
1254 found = findChessboardCorners(viewGray, boardSize, pointbuf,
1255 CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_NORMALIZE_IMAGE);
1256 }
1257
1258 if(found)
1259 {
1260 if (pointbuf.size() != static_cast<size_t>(boardWidth * boardHeight))
1261 {
1262 yWarning("Mono calibration: skipping image %s because %zu corners were detected, expected %d",
1263 imageList[i].c_str(), pointbuf.size(), boardWidth * boardHeight);
1264 continue;
1265 }
1266
1267 Rect bbox = boundingRect(pointbuf);
1268 if (bbox.width < view.cols * 0.15 || bbox.height < view.rows * 0.15)
1269 {
1270 yWarning("Mono calibration: skipping image %s because the detected board is too small (bbox %dx%d) with respect to the image size (%dx%d)",
1271 imageList[i].c_str(), bbox.width, bbox.height, view.cols, view.rows);
1272 continue;
1273 }
1274
1275 TermCriteria subpixCriteria = TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 30, 0.001);
1276 cornerSubPix(viewGray, pointbuf, Size(7, 7), Size(-1, -1), subpixCriteria);
1277 drawChessboardCorners(view, boardSize, Mat(pointbuf), found);
1278 imagePoints.push_back(pointbuf);
1279 }
1280 else
1281 {
1282 yWarning("Mono calibration: no chessboard detected in %s", imageList[i].c_str());
1283 }
1284 }
1285
1286 if (imagePoints.size() < 2)
1287 {
1288 yError("Mono calibration: only %zu valid image(s) remain after filtering; aborting", imagePoints.size());
1289 return 0.0;
1290 }
1291
1292 std::vector<Mat> rvecs, tvecs;
1293 std::vector<float> reprojErrs;
1294 double totalAvgErr = 0;
1295
1296 std::vector<std::vector<Point3f> > objectPoints(1);
1297 calcChessboardCorners(boardSize, squareSize, objectPoints[0]);
1298 objectPoints.resize(imagePoints.size(), objectPoints[0]);
1299
1300 K = Mat::eye(3, 3, CV_64F);
1301 Dist = Mat::zeros(4, 1, CV_64F);
1302
1303 bool usedPinholeGuess = false;
1304 try
1305 {
1306 Mat pinholeK = Mat::eye(3, 3, CV_64F);
1307 Mat pinholeDist = Mat::zeros(5, 1, CV_64F);
1308 std::vector<Mat> pinholeRvecs, pinholeTvecs;
1309 int pinholeFlags = cv::CALIB_FIX_K3 | cv::CALIB_FIX_K4 | cv::CALIB_FIX_TANGENT_DIST;
1310 double pinholeRms = cv::calibrateCamera(objectPoints, imagePoints, imageSize, pinholeK, pinholeDist,
1311 pinholeRvecs, pinholeTvecs, pinholeFlags);
1312 yInfo("Pinhole initial guess RMS for %s camera: %g", cameraName, pinholeRms);
1313 if (pinholeK.rows == 3 && pinholeK.cols == 3)
1314 {
1315 K = pinholeK.clone();
1316 usedPinholeGuess = true;
1317 }
1318 }
1319 catch (const cv::Exception& e)
1320 {
1321 yWarning("Pinhole initial guess failed for %s camera: %s", cameraName, e.what());
1322 }
1323
1324 if (!usedPinholeGuess)
1325 {
1326 double focal = std::max(imageSize.width, imageSize.height) * 0.8;
1327 K = Mat::eye(3, 3, CV_64F);
1328 K.at<double>(0,0) = focal;
1329 K.at<double>(1,1) = focal;
1330 K.at<double>(0,2) = imageSize.width * 0.5;
1331 K.at<double>(1,2) = imageSize.height * 0.5;
1332 }
1333 if( flags & CV_CALIB_FIX_ASPECT_RATIO )
1334 K.at<double>(0,0) = aspectRatio;
1335
1336 try
1337 {
1338 int calFlags = fisheye::CALIB_USE_INTRINSIC_GUESS | fisheye::CALIB_RECOMPUTE_EXTRINSIC | fisheye::CALIB_FIX_SKEW;
1339 double rms = fisheye::calibrate(objectPoints, imagePoints, imageSize, K, Dist, rvecs, tvecs,
1340 calFlags,
1341 TermCriteria(TermCriteria::EPS+TermCriteria::MAX_ITER, 100, 1e-5));
1342
1343 yInfo("Mono calibration RMS for %s camera: %g", cameraName, rms);
1344 yInfo("Mono calibration intrinsics for %s camera: fx=%g fy=%g cx=%g cy=%g",
1345 cameraName, K.at<double>(0,0), K.at<double>(1,1), K.at<double>(0,2), K.at<double>(1,2));
1346 yInfo("Mono calibration distortion for %s camera: k1=%g k2=%g k3=%g k4=%g",
1347 cameraName, Dist.at<double>(0,0), Dist.at<double>(1,0), Dist.at<double>(2,0), Dist.at<double>(3,0));
1348 cout.flush();
1349 return rms;
1350 }
1351 catch (const cv::Exception& e)
1352 {
1353 yError("OpenCV mono calibration raised an exception: %s", e.what());
1354 yError("Mono calibration failed with %zu valid images", imagePoints.size());
1355 throw;
1356 }
1357}
1358
1359
1360namespace {
1361void logStereoCalibrationDebugInfo(const std::vector<std::vector<cv::Point2f> >& imagePointsLeft,
1362 const std::vector<std::vector<cv::Point2f> >& imagePointsRight,
1363 const std::vector<std::vector<cv::Point3f> >& objectPoints,
1364 const std::vector<std::string>& imagelist,
1365 const cv::Size& imageSize,
1366 int boardWidth,
1367 int boardHeight)
1368{
1369 yInfo("Stereo calibration debug: %zu image pairs, board %dx%d, image size %dx%d",
1370 imagePointsLeft.size(), boardWidth, boardHeight, imageSize.width, imageSize.height);
1371
1372 for (size_t i = 0; i < imagePointsLeft.size(); ++i)
1373 {
1374 yInfo("pair[%zu]: left corners=%zu right corners=%zu object points=%zu", i,
1375 imagePointsLeft[i].size(), imagePointsRight[i].size(), objectPoints[i].size());
1376 if (imagePointsLeft[i].size() != imagePointsRight[i].size())
1377 {
1378 yError("pair[%zu] has mismatched corner counts: left=%zu right=%zu", i,
1379 imagePointsLeft[i].size(), imagePointsRight[i].size());
1380 }
1381 if (imagePointsLeft[i].size() != objectPoints[i].size())
1382 {
1383 yError("pair[%zu] has mismatched corners/object-points: corners=%zu object=%zu", i,
1384 imagePointsLeft[i].size(), objectPoints[i].size());
1385 }
1386 if (i < imagelist.size() / 2)
1387 {
1388 yInfo("pair[%zu] images: %s / %s", i, imagelist[2 * i].c_str(), imagelist[2 * i + 1].c_str());
1389 }
1390 }
1391}
1392}
1393
1394void stereoCalibThread::stereoCalibration(const vector<string>& imagelist, int boardWidth, int boardHeight,float sqsize)
1395{
1396 Size boardSize;
1397 boardSize.width=boardWidth;
1398 boardSize.height=boardHeight;
1399 if( imagelist.size() % 2 != 0 )
1400 {
1401 cout << "Error: the image list contains odd (non-even) number of elements\n";
1402 return;
1403 }
1404
1405 const int maxScale = 2;
1406 // ARRAY AND VECTOR STORAGE:
1407
1408 std::vector<std::vector<Point2f> > imagePoints[2];
1409 Size imageSize;
1410
1411 int i, j, k, nimages = (int)imagelist.size()/2;
1412
1413 imagePoints[0].resize(nimages);
1414 imagePoints[1].resize(nimages);
1415 std::vector<string> goodImageList;
1416 bool differentSizes = false;
1417
1418 for( i = j = 0; i < nimages; i++ )
1419 {
1420 for( k = 0; k < 2; k++ )
1421 {
1422 const string& filename = imagelist[i*2+k];
1423 Mat img = cv::imread(filename, IMREAD_GRAYSCALE);
1424 if(img.empty())
1425 break;
1426 if( imageSize == Size() )
1427 imageSize = img.size();
1428 else if( img.size() != imageSize )
1429 {
1430 yWarning() <<"The image " << filename << " has the size different from the first image size.\n";
1431 differentSizes = true;
1432 }
1433 bool found = false;
1434 std::vector<Point2f>& corners = imagePoints[k][j];
1435 for( int scale = 1; scale <= maxScale; scale++ )
1436 {
1437 Mat timg;
1438 if( scale == 1 )
1439 timg = img;
1440 else
1441 resize(img, timg, Size(), scale, scale);
1442
1443 if(boardType == "CIRCLES_GRID") {
1444 found = findCirclesGrid(timg, boardSize, corners, CALIB_CB_SYMMETRIC_GRID | CALIB_CB_CLUSTERING);
1445 } else if(boardType == "ASYMMETRIC_CIRCLES_GRID") {
1446 found = findCirclesGrid(timg, boardSize, corners, CALIB_CB_ASYMMETRIC_GRID | CALIB_CB_CLUSTERING);
1447 } else {
1448 found = findChessboardCorners(timg, boardSize, corners,
1449 CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_NORMALIZE_IMAGE);
1450 }
1451
1452 if( found )
1453 {
1454 if( scale > 1 )
1455 {
1456 Mat cornersMat(corners);
1457 cornersMat *= 1./scale;
1458 }
1459 break;
1460 }
1461 }
1462 if( !found )
1463 break;
1464 }
1465 if( k == 2 )
1466 {
1467 goodImageList.push_back(imagelist[i*2]);
1468 goodImageList.push_back(imagelist[i*2+1]);
1469 j++;
1470 }
1471 }
1472 yInfo("%i pairs have been successfully detected.\n",j);
1473 nimages = j;
1474 if( nimages < 2 )
1475 {
1476 yError("Error: too few pairs detected \n");
1477 return;
1478 }
1479
1480 imagePoints[0].resize(nimages);
1481 imagePoints[1].resize(nimages);
1482
1483 std::vector<std::vector<Point3f> > objectPoints(1);
1484 calcChessboardCorners(boardSize, squareSize, objectPoints[0]);
1485 objectPoints.resize(nimages, objectPoints[0]);
1486
1487 yInfo("Running stereo calibration ...\n");
1488
1489 //logStereoCalibrationDebugInfo(imagePoints[0], imagePoints[1], objectPoints, imagelist, imageSize, boardWidth, boardHeight);
1490
1491 Mat cameraMatrix[2], distCoeffs[2];
1492 Mat E, F;
1493 int flags = fisheye::CALIB_FIX_INTRINSIC | fisheye::CALIB_RECOMPUTE_EXTRINSIC | fisheye::CALIB_FIX_SKEW;
1494 TermCriteria criteria = TermCriteria(TermCriteria::MAX_ITER+TermCriteria::EPS, 100, 1e-5);
1495
1496 yInfo("Stereo calibration intrinsics: left empty=%d right empty=%d", this->Kleft.empty(), this->Kright.empty());
1497 yInfo("Stereo calibration distortion: left empty=%d right empty=%d", this->DistL.empty(), this->DistR.empty());
1498
1499 if (this->Kleft.empty() || this->Kright.empty())
1500 {
1501 if (differentSizes){
1502 yError("Images have different sizes. Please make sure to compute intrinsic parameters before running stereo calibration. Quitting...");
1503 exit (-1);
1504 }
1505 yError("Stereo calibration: intrinsics are empty; cannot proceed with fixed-intrinsic stereo solve.");
1506 return;
1507 }
1508
1509 this->R = Mat::eye(3, 3, CV_64F);
1510 this->T = Mat::zeros(3, 1, CV_64F);
1511 this->T.at<double>(0, 0) = 0.06;
1512 yInfo("Stereo calibration initial guess: R=identity, T=(%.4f, %.4f, %.4f)", this->T.at<double>(0,0), this->T.at<double>(1,0), this->T.at<double>(2,0));
1513
1514 // store image size for later saving of rectification matrices
1515 this->lastImageSize = imageSize;
1516
1517 try
1518 {
1519 yInfo("Using precomputed intrinsic parameters with fixed intrinsics and a baseline-based initial translation");
1520 double rms = fisheye::stereoCalibrate(objectPoints, imagePoints[0], imagePoints[1],
1521 this->Kleft, this->DistL,
1522 this->Kright, this->DistR,
1523 imageSize, this->R, this->T,
1524 flags, criteria);
1525 yInfo("done with RMS error= %f\n",rms);
1526 }
1527 catch (const cv::Exception& e)
1528 {
1529 yError("OpenCV stereo calibration raised an exception: %s", e.what());
1530 yError("Stereo calibration failed with %zu left pairs and %zu right pairs", imagePoints[0].size(), imagePoints[1].size());
1531 throw;
1532 }
1533
1534 // Compute the fundamental matrix for the undistorted stereo pair.
1535 cameraMatrix[0] = this->Kleft;
1536 cameraMatrix[1] = this->Kright;
1537 distCoeffs[0] = this->DistL;
1538 distCoeffs[1] = this->DistR;
1539
1540 Mat R, T;
1541 T = this->T;
1542 R = this->R;
1543 Mat Tx = Mat::zeros(3, 3, CV_64F);
1544 Tx.at<double>(0, 1) = -T.at<double>(2, 0);
1545 Tx.at<double>(0, 2) = T.at<double>(1, 0);
1546 Tx.at<double>(1, 0) = T.at<double>(2, 0);
1547 Tx.at<double>(1, 2) = -T.at<double>(0, 0);
1548 Tx.at<double>(2, 0) = -T.at<double>(1, 0);
1549 Tx.at<double>(2, 1) = T.at<double>(0, 0);
1550
1551 F = cameraMatrix[1].inv().t() * Tx * R * cameraMatrix[0].inv();
1552 yInfo("Computed fundamental matrix from stereo extrinsics.");
1553
1554 double err = 0;
1555 int npoints = 0;
1556 std::vector<Vec3f> lines[2];
1557 for( i = 0; i < nimages; i++ )
1558 {
1559 int npt = (int)imagePoints[0][i].size();
1560 Mat imgpt[2];
1561 for( k = 0; k < 2; k++ )
1562 {
1563 imgpt[k] = Mat(imagePoints[k][i]);
1564 fisheye::undistortPoints(imgpt[k], imgpt[k], cameraMatrix[k], distCoeffs[k], Mat(), cameraMatrix[k]);
1565 yDebug() << "Calculated undistorted points for image " << i << " camera " << k;
1566 computeCorrespondEpilines(imgpt[k], k+1, F, lines[k]);
1567 }
1568 for( j = 0; j < npt; j++ )
1569 {
1570 double errij = fabs(imagePoints[0][i][j].x*lines[1][j][0] +
1571 imagePoints[0][i][j].y*lines[1][j][1] + lines[1][j][2]) +
1572 fabs(imagePoints[1][i][j].x*lines[0][j][0] +
1573 imagePoints[1][i][j].y*lines[0][j][1] + lines[0][j][2]);
1574 err += errij;
1575 }
1576 npoints += npt;
1577 }
1578 yInfo("average reprojection err = %f\n",err/npoints);
1579 cout.flush();
1580}
1581
1582
1583void stereoCalibThread::saveCalibration(const string& extrinsicFilePath, const string& intrinsicFilePath){
1584
1585 if( Kleft.empty() || Kright.empty() || DistL.empty() || DistR.empty() || R.empty() || T.empty()) {
1586 cout << "Error: cameras are not calibrated! Run the calibration or set intrinsic and extrinsic parameters \n";
1587 return;
1588 }
1589
1590 FileStorage fs(intrinsicFilePath+".yml", cv::FileStorage::Mode::WRITE);
1591 if( fs.isOpened() )
1592 {
1593 fs << "M1" << Kleft << "D1" << DistL << "M2" << Kright << "D2" << DistR;
1594 fs.release();
1595 }
1596 else
1597 cout << "Error: can not save the intrinsic parameters\n";
1598
1599 fs.open(extrinsicFilePath+".yml", cv::FileStorage::Mode::WRITE);
1600 if( fs.isOpened() )
1601 {
1602 // compute rectification and projection matrices for fisheye model
1603 try {
1604 Mat R1, R2, P1, P2, Qr;
1605 int flags = 0; // consider fisheye::CALIB_ZERO_DISPARITY if desired
1606 fisheye::stereoRectify(Kleft, DistL, Kright, DistR, this->lastImageSize, R, T, R1, R2, P1, P2, Qr, flags, Size());
1607 fs << "R" << R << "T" << T << "R1" << R1 << "R2" << R2 << "P1" << P1 << "P2" << P2 << "Q" << Qr;
1608 }
1609 catch (const cv::Exception &e) {
1610 yError("stereoRectify failed: %s", e.what());
1611 fs << "R" << R << "T" << T << "Q" << Q;
1612 }
1613 fs.release();
1614 }
1615 else
1616 cout << "Error: can not save the intrinsic parameters\n";
1617
1618}
1619
1620void stereoCalibThread::calcChessboardCorners(Size boardSize, float squareSize, vector<Point3f>& corners)
1621{
1622 corners.resize(0);
1623
1624 if(boardType == "ASYMMETRIC_CIRCLES_GRID") {
1625 for( int i = 0; i < boardSize.height; i++ )
1626 for( int j = 0; j < boardSize.width; j++ )
1627 corners.push_back(Point3f(float((2*j + i % 2)*squareSize), float(i*squareSize), 0));
1628 } else {
1629 for( int i = 0; i < boardSize.height; i++ )
1630 for( int j = 0; j < boardSize.width; j++ )
1631 corners.push_back(Point3f(float(j*squareSize),
1632 float(i*squareSize), 0));
1633 }
1634}
1635
1636bool stereoCalibThread::updateExtrinsics(Mat Rot, Mat Tr, const string& groupname)
1637{
1638 std::vector<string> lines;
1639 bool append = false;
1640
1641 ifstream in;
1642 in.open(camCalibFile.c_str()); //camCalibFile.c_str());
1643
1644 if(in.is_open()){
1645 // file exists
1646 string line;
1647 bool sectionFound = false;
1648 bool sectionClosed = false;
1649
1650 // process lines
1651 while(std::getline(in, line)){
1652 // check if we left calibration section
1653 if (sectionFound == true && line.find("[", 0) != string::npos)
1654 sectionClosed = true; // also valid if no groupname specified
1655 // check if we enter calibration section
1656 if (line.find(string("[") + groupname + string("]"), 0) != string::npos)
1657 sectionFound = true;
1658 // if no groupname specified
1659 if (groupname == "")
1660 sectionFound = true;
1661 // if we are in calibration section (or no section/group specified)
1662 if (sectionFound == true && sectionClosed == false){
1663 // replace w line
1664 if (line.find("HN",0) != string::npos){
1665 stringstream ss;
1666 ss << " (" << Rot.at<double>(0,0) << " " << Rot.at<double>(0,1) << " " << Rot.at<double>(0,2) << " " << Tr.at<double>(0,0) << " "
1667 << Rot.at<double>(1,0) << " " << Rot.at<double>(1,1) << " " << Rot.at<double>(1,2) << " " << Tr.at<double>(1,0) << " "
1668 << Rot.at<double>(2,0) << " " << Rot.at<double>(2,1) << " " << Rot.at<double>(2,2) << " " << Tr.at<double>(2,0) << " "
1669 << 0.0 << " " << 0.0 << " " << 0.0 << " " << 1.0 << ")";
1670 line = "HN" + string(ss.str());
1671 }
1672
1673 if (!standalone) {
1674 if (line.find("QL", 0) != string::npos) {
1675 line = "QL (" + string(qL.toString().c_str()) + ")";
1676 }
1677 if (line.find("QR", 0) != string::npos) {
1678 line = "QR (" + string(qR.toString().c_str()) + ")";
1679 }
1680 }
1681 }
1682 // buffer line
1683 lines.push_back(line);
1684 }
1685
1686 in.close();
1687
1688 // rewrite file
1689 if (!sectionFound){
1690 append = true;
1691 cout << "Camera calibration parameter section " + string("[") + groupname + string("]") + " not found in file " << camCalibFile << ". Adding group..." << endl;
1692 }
1693 else{
1694 // rewrite file
1695 ofstream out;
1696 out.open(camCalibFile.c_str(), ios::trunc);
1697 if (out.is_open()){
1698 for (int i = 0; i < (int)lines.size(); i++)
1699 out << lines[i] << endl;
1700 out.close();
1701 }
1702 else
1703 return false;
1704 }
1705
1706 }
1707 else{
1708 append = true;
1709 }
1710
1711 if (append){
1712 // file doesn't exist or section is appended
1713 ofstream out;
1714 out.open(camCalibFile.c_str(), ios::app);
1715 if (out.is_open()){
1716 out << endl;
1717 out << string("[") + groupname + string("]") << endl;
1718 out << "HN (" << Rot.at<double>(0,0) << " " << Rot.at<double>(0,1) << " " << Rot.at<double>(0,2) << " " << Tr.at<double>(0,0) << " "
1719 << Rot.at<double>(1,0) << " " << Rot.at<double>(1,1) << " " << Rot.at<double>(1,2) << " " << Tr.at<double>(1,0) << " "
1720 << Rot.at<double>(2,0) << " " << Rot.at<double>(2,1) << " " << Rot.at<double>(2,2) << " " << Tr.at<double>(2,0) << " "
1721 << 0.0 << " " << 0.0 << " " << 0.0 << " " << 1.0 << ")";
1722 out << endl;
1723 out << "QL (" << qL.toString().c_str() << ")" << endl;
1724 out << "QR (" << qR.toString().c_str() << ")" << endl;
1725 out.close();
1726 }
1727 else
1728 return false;
1729 }
1730
1731 return true;
1732}
constexpr double tolerance
SynchronizerStatistics getStatistics() const
void configure(double tolerance, std::size_t maxQueueSize)
void pushRight(const ImageOf< PixelRgb > &rightFrame, const Stamp &timestamp)
bool tryPopPair(SynchronizedPair &pair)
void pushLeft(const ImageOf< PixelRgb > &leftFrame, const Stamp &timestamp)
stereoCalibThread(ResourceFinder &rf, Port *commPort, const char *imageDir)
StereoCalibStatus getStatus() const
bool writeImagePair(const std::string &outputDirectory, std::size_t observationIndex, const cv::Mat &leftImg, const cv::Mat &rightImg, std::string &leftFile, std::string &rightFile, std::string &errorMessage) const
bool write(const std::string &outputFile, const CalibrationResult &result, std::string &errorMessage) const
bool writeObservations(const std::string &outputFile, const std::vector< StereoObservation > &observations, std::string &errorMessage) const
bool calibrate(const std::vector< StereoObservation > &observations, const FisheyeCalibrationOptions &options, CalibrationResult &result, std::string &errorMessage) const
void append(const size_t index, shared_ptr< Epsilon_neighbours_t > en)
constexpr double CTRL_DEG2RAD
PI/180.
Definition math.h:66
view(3)
out
Definition sine.m:8
#define LEFT
#define RIGHT
ImageOf< PixelRgb > image
std::string lastCalibrationError
double maxVerticalRectificationErrorPx
std::size_t droppedLeftFrames
double p95VerticalRectificationErrorPx
std::string calibrationMode
double medianVerticalRectificationErrorPx
std::size_t droppedRightFrames
ImageOf< PixelRgb > left
ImageOf< PixelRgb > right
CameraCalibrationResult leftCamera
StereoCalibrationResult stereo
CameraCalibrationResult rightCamera
CalibrationQualityMetrics quality
std::vector< cv::Point3f > createObjectPoints() const
std::vector< cv::Point2f > leftImagePoints
std::vector< cv::Point3f > objectPoints
std::vector< cv::Point2f > rightImagePoints