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 <sstream>
5#include <yarp/cv/Cv.h>
6#include <yarp/os/LogStream.h>
7#include "stereoCalibThread.h"
8
9
10YARP_LOG_COMPONENT(STEREOCALIBRATIONTHREAD, "yarp.tools.stereoCalibThread")
11
12namespace
13{
14
15 std::string formatCalibrationMatrix(const cv::Mat& matrix)
16 {
17 std::ostringstream stream;
18 stream << matrix;
19 return stream.str();
20 }
21
22 void logCameraCalibration(const char* cameraName,
24 {
25 yCInfo(STEREOCALIBRATIONTHREAD) << "[CAMERA_CALIBRATION_" << cameraName << "]";
26 yCInfo(STEREOCALIBRATIONTHREAD) << "w" << camera.imageSize.width << "h" << camera.imageSize.height;
27 yCInfo(STEREOCALIBRATIONTHREAD) << "fx" << camera.K.at<double>(0, 0)
28 << "fy" << camera.K.at<double>(1, 1)
29 << "cx" << camera.K.at<double>(0, 2)
30 << "cy" << camera.K.at<double>(1, 2);
32 {
33 yCInfo(STEREOCALIBRATIONTHREAD) << "k1" << camera.D.at<double>(0, 0)
34 << "k2" << camera.D.at<double>(1, 0)
35 << "k3" << camera.D.at<double>(2, 0)
36 << "k4" << camera.D.at<double>(3, 0);
37 }
39 {
40 yCInfo(STEREOCALIBRATIONTHREAD) << "k1" << camera.D.at<double>(0, 0)
41 << "k2" << camera.D.at<double>(1, 0)
42 << "p1" << camera.D.at<double>(2, 0)
43 << "p2" << camera.D.at<double>(3, 0);
44 }
45 yCInfo(STEREOCALIBRATIONTHREAD) << "K =" << formatCalibrationMatrix(camera.K);
46 yCInfo(STEREOCALIBRATIONTHREAD) << "monocular RMS =" << camera.rms;
47 }
48
49 void logCalibrationResult(const stereo_calib::CalibrationResult& result)
50 {
51 yCInfo(STEREOCALIBRATIONTHREAD) << "========== Calibration result (not written to disk) ==========";
52
53 if(result.leftCamera.isValid())
54 {
55 logCameraCalibration("LEFT", result.leftCamera);
56 }
57 if(result.rightCamera.isValid())
58 {
59 logCameraCalibration("RIGHT", result.rightCamera);
60 }
61
62 if(result.stereo.isValid())
63 {
64 cv::Mat homogeneousTransform = cv::Mat::eye(4, 4, CV_64F);
65 result.stereo.R.copyTo(homogeneousTransform(cv::Rect(0, 0, 3, 3)));
66 result.stereo.T.reshape(1, 3).copyTo(homogeneousTransform(cv::Rect(3, 0, 1, 3)));
67
68 yCInfo(STEREOCALIBRATIONTHREAD) << "[STEREO_DISPARITY]";
69 yCInfo(STEREOCALIBRATIONTHREAD) << "Stereo RMS =" << result.stereo.rms
70 << "baseline norm =" << cv::norm(result.stereo.T);
71 yCInfo(STEREOCALIBRATIONTHREAD) << "R =" << formatCalibrationMatrix(result.stereo.R);
72 yCInfo(STEREOCALIBRATIONTHREAD) << "T =" << formatCalibrationMatrix(result.stereo.T.t());
73 // HN is the same homogeneous transform layout used by outputCalib.ini.
74 yCInfo(STEREOCALIBRATIONTHREAD) << "HN =" << formatCalibrationMatrix(homogeneousTransform);
75 }
76 yCInfo(STEREOCALIBRATIONTHREAD) << "======================================================================";
77 }
78
79 stereo_calib::CameraModel parseCameraModel(const std::string& modelString)
80 {
81 if(modelString == "pinhole")
82 {
84 }
85 else if(modelString == "fisheye")
86 {
88 }
89 else
90 {
91 yCError(STEREOCALIBRATIONTHREAD) << "Invalid camera model string:" << modelString
92 << ". Setting to Invalid.";
94 }
95 }
96
97 stereo_calib::CalibrationMode parseCalibrationMode(const std::string& modeString)
98 {
99 if(modeString == "MonocularLeft")
100 {
102 }
103 else if(modeString == "MonocularRight")
104 {
106 }
107 else if(modeString == "MonocularBoth")
108 {
110 }
111 else if(modeString == "StereoFull")
112 {
114 }
115 else
116 {
117 yCError(STEREOCALIBRATIONTHREAD) << "Invalid calibration mode string:" << modeString
118 << ". Using default StereoFull.";
120 }
121 }
122} // namespace
123
124void StereoPairSynchronizer::configure(double tolerance, std::size_t maxQueueSize)
125{
126 this->toleranceSeconds = tolerance;
127 this->maxQueueSize = maxQueueSize;
128}
129
131{
132 leftQueue.clear();
133 rightQueue.clear();
134
135 std::lock_guard<std::mutex> lock(_mutex);
136 stats = SynchronizerStatistics{};
137}
138
139void StereoPairSynchronizer::pushLeft(const ImageOf<PixelRgb>& leftFrame, const Stamp& timestamp)
140{
141 StampedFrame frame;
142
143 // Must be a deep copy. Do not retain pointer to the input-port buffer
144 frame.image.copy(leftFrame);
145 frame.stamp = timestamp;
146
147 leftQueue.push_back(std::move(frame));
148 trimLeftQueue();
149}
150
151void StereoPairSynchronizer::pushRight(const ImageOf<PixelRgb>& rightFrame, const Stamp& timestamp)
152{
153 StampedFrame frame;
154
155 // Must be a deep copy. Do not retain pointer to the input-port buffer
156 frame.image.copy(rightFrame);
157 frame.stamp = timestamp;
158
159 rightQueue.push_back(std::move(frame));
160 trimRightQueue();
161}
162
163
164void StereoPairSynchronizer::trimLeftQueue()
165{
166 while(leftQueue.size() > maxQueueSize)
167 {
168 leftQueue.pop_front();
169 std::lock_guard<std::mutex> lock(_mutex);
170 ++stats.droppedLeftFrames;
171 }
172}
173
174void StereoPairSynchronizer::trimRightQueue()
175{
176 while(rightQueue.size() > maxQueueSize)
177 {
178 rightQueue.pop_front();
179 std::lock_guard<std::mutex> lock(_mutex);
180 ++stats.droppedRightFrames;
181 }
182}
183
185{
186 while(!leftQueue.empty() && !rightQueue.empty())
187 {
188 const double leftStamp = leftQueue.front().stamp.getTime();
189 const double rightStamp = rightQueue.front().stamp.getTime();
190
191 const double absoluteDelta = std::abs(leftStamp - rightStamp);
192 if(absoluteDelta <= toleranceSeconds)
193 {
194 pair.left = std::move(leftQueue.front().image);
195 pair.right = std::move(rightQueue.front().image);
196 pair.leftStamp = leftQueue.front().stamp;
197 pair.rightStamp = rightQueue.front().stamp;
198
199 pair.timeStampDelta = absoluteDelta;
200
201 leftQueue.pop_front();
202 rightQueue.pop_front();
203
204 std::lock_guard<std::mutex> lock(_mutex);
205 ++stats.pairedFrames;
206 stats.accumulatedTimeStampDelta += absoluteDelta;
207 stats.maxTimeStampDelta = std::max(stats.maxTimeStampDelta, absoluteDelta);
208
209 return true;
210 }
211
212 if(leftStamp < rightStamp)
213 {
214 // the oldest left frame cannot be paired with this or any newer right frame, so drop it
215 leftQueue.pop_front();
216 std::lock_guard<std::mutex> lock(_mutex);
217 ++stats.droppedLeftFrames;
218 }
219 else
220 {
221 rightQueue.pop_front();
222 std::lock_guard<std::mutex> lock(_mutex);
223 ++stats.droppedRightFrames;
224 }
225 }
226 return false;
227}
228
229
230stereoCalibThread::stereoCalibThread(ResourceFinder &rf, Port* commPort, const char *imageDir)
231{
232 moduleName=rf.check("name", Value("stereoCalib"),"module name (string)").asString().c_str();
233 robotName=rf.check("robotName",Value("icub"), "module name (string)").asString().c_str();
234
235 this->inputLeftPortName = "/"+moduleName;
236 this->inputLeftPortName +=rf.check("imgLeft",Value("/cam/left:i"),"Input image port (string)").asString().c_str();
237
238 this->inputRightPortName = "/"+moduleName;
239 this->inputRightPortName += rf.check("imgRight", Value("/cam/right:i"),"Input image port (string)").asString().c_str();
240
241 this->outNameRight = "/"+moduleName;
242 this->outNameRight += rf.check("outRight",Value("/cam/right:o"),"Output image port (string)").asString().c_str();
243
244 this->outNameLeft = "/"+moduleName;
245 this->outNameLeft +=rf.check("outLeft",Value("/cam/left:o"),"Output image port (string)").asString().c_str();
246
247 Bottle stereoCalibOpts=rf.findGroup("STEREO_CALIBRATION_CONFIGURATION");
248 this->boardWidth = stereoCalibOpts.check("boardWidth", Value(8)).asInt32();
249 this->boardHeight = stereoCalibOpts.check("boardHeight", Value(6)).asInt32();
250 this->numOfPairs = stereoCalibOpts.check("numberOfPairs", Value(30)).asInt32();
251 if(this->numOfPairs < 30)
252 {
253 yCWarning(STEREOCALIBRATIONTHREAD) << "numberOfPairs must be at least 30; using 30";
254 this->numOfPairs = 30;
255 }
256 this->squareSize = (float)stereoCalibOpts.check("boardSize", Value(0.09241)).asFloat64();
257 this->boardType = stereoCalibOpts.check("boardType", Value("CHESSBOARD")).asString();
258 const double syncToleranceMs = stereoCalibOpts.check("syncToleranceMs", Value(20.0)).asFloat64();
259 _syncToleranceSeconds = syncToleranceMs / 1000.0;
260 const int configuredQueueSize = stereoCalibOpts.check("syncQueueSize", Value(5)).asInt32();
261 if(configuredQueueSize <= 0)
262 {
263 yCWarning(STEREOCALIBRATIONTHREAD) << "Invalid syncQueueSize; using 5";
264 _syncQueueSize = 5;
265 }
266 else
267 {
268 _syncQueueSize = static_cast<std::size_t>(configuredQueueSize);
269 }
270
271 if(_syncToleranceSeconds <= 0.0)
272 {
273 yCWarning(STEREOCALIBRATIONTHREAD) << "Invalid syncToleranceMs; using 20 ms";
274 _syncToleranceSeconds = 0.020;
275 }
276
277 synchronizer.configure(_syncToleranceSeconds, _syncQueueSize);
278
279 this->minCaptureIntervalSeconds = stereoCalibOpts.check("minCaptureIntervalSeconds", Value(2.0)).asFloat64();
280 this->minimumBoardSpanRatio = stereoCalibOpts.check("minimumBoardSpanRatio", Value(0.15)).asFloat64();
281 this->commandPort=commPort;
282 this->imageDir=imageDir;
283 this->collectionResetRequested.store(false);
284 this->calibrationState.store(CalibrationState::Idle);
285 // All new calibration modes use the synchronized-observation pipeline.
286 // In particular, completion must never bypass CalibrationWriter.
287 this->camCalibFile=rf.getHomeContextPath().c_str();
288 this->standalone = rf.check("standalone", Value(false)).asBool();
289 string fileName= "outputCalib.ini";
290
291 this->camCalibFile=this->camCalibFile+"/"+fileName.c_str();
292
293 _observationsFile = stereoCalibOpts.check(
294 "observationsFile", Value("calibrationObservations.yml")).asString();
295 if(!_observationsFile.empty() && _observationsFile.front() != '/')
296 {
297 _observationsFile = this->imageDir + "/" + _observationsFile;
298 }
299
300 // TODO: remove. This is a duplication. Enough to set: _chessboardConfiguration.cornersX stereoCalibOpts.check("boardWidth", Value(8)).asInt32();
301 _chessboardConfiguration.cornersX = this->boardWidth;
302 _chessboardConfiguration.cornersY = this->boardHeight;
303
304 // TODO: remove as well --> duplication
305 _chessboardConfiguration.squareSizeMeters = this->squareSize;
306
307 _saveImages = stereoCalibOpts.check("saveImages", Value(1)).asInt32() != 0;
308 _drawDiagnosticCorners = stereoCalibOpts.check("drawDiagnosticCorners", Value(1)).asInt32() != 0;
309 _cameraModel = parseCameraModel(stereoCalibOpts.check("cameraModel", Value("pinhole")).asString());
310 _calibrationMode = parseCalibrationMode(stereoCalibOpts.check("calibrationMode", Value("StereoFull")).asString());
311
312 if(!_chessboardConfiguration.isValid())
313 {
314 yCError(STEREOCALIBRATIONTHREAD) << "Invalid chessboard configuration";
315 }
316}
317
319{
320 if (!imagePortInLeft.open(inputLeftPortName.c_str())) {
321 cout << ": unable to open port " << inputLeftPortName << endl;
322 return false;
323 }
324
325 if (!imagePortInRight.open(inputRightPortName.c_str())) {
326 cout << ": unable to open port " << inputRightPortName << endl;
327 return false;
328 }
329
330 if (!outPortLeft.open(outNameLeft.c_str())) {
331 cout << ": unable to open port " << outNameLeft << endl;
332 return false;
333 }
334
335 if (!outPortRight.open(outNameRight.c_str())) {
336 cout << ": unable to open port " << outNameRight << endl;
337 return false;
338 }
339
340 //when in standalone mode we won't open the remote control board devices
341 if(standalone) return true;
342
343 //TODO: develop what to do with the control boards for head and torso
344 // in the legacy implementation the kinematic chain was calculated and joint position added to the output file
345 // now why we need that? is it uselful? do we need to change it to a check on steadiness for the calibration procedure
346 Property optHead;
347 optHead.put("device","remote_controlboard");
348 optHead.put("remote",("/"+robotName+"/head").c_str());
349 optHead.put("local","/"+moduleName+"/client/head");
350 if (!polyHead.open(optHead) ||
351 !polyHead.view(posHead) ||
352 posHead == nullptr)
353 {
354 yCError(STEREOCALIBRATIONTHREAD) << "Unable to acquire the head encoder interface";
355 return false;
356 }
357
358 Property optTorso;
359 optTorso.put("device","remote_controlboard");
360 optTorso.put("remote",("/"+robotName+"/torso").c_str());
361 optTorso.put("local","/"+moduleName+"/client/torso");
362
363 bool useTorso=true;
364 if (!polyTorso.open(optTorso) ||
365 !polyTorso.view(posTorso) ||
366 posTorso == nullptr)
367 {
368 yCWarning(STEREOCALIBRATIONTHREAD, "Unable to connect to torso! Continuing without...");
369 useTorso=false;
370 }
371
372 yarp::sig::Vector head_angles(6,0.0);
373 posHead->getEncoders(head_angles.data());
374
375 yarp::sig::Vector torso_angles(3,0.0);
376 if (useTorso)
377 posTorso->getEncoders(torso_angles.data());
378
379 qL.resize(torso_angles.length()+head_angles.length()-1);
380 for(size_t i=0; i<torso_angles.length(); i++)
381 qL[i]=torso_angles[torso_angles.length()-i-1];
382
383 for(size_t i=0; i<head_angles.length()-2; i++)
384 qL[i+torso_angles.length()]=head_angles[i];
385 qL[7]=head_angles[4]+(0.5-(LEFT))*head_angles[5];
387
388 qR.resize(torso_angles.length()+head_angles.length()-1);
389 for(size_t i=0; i<torso_angles.length(); i++)
390 qR[i]=torso_angles[torso_angles.length()-i-1];
391
392 for(size_t i=0; i<head_angles.length()-2; i++)
393 qR[i+torso_angles.length()]=head_angles[i];
394 qR[7]=head_angles[4]+(0.5-(RIGHT))*head_angles[5];
396
397 return true;
398}
400 yCInfo(STEREOCALIBRATIONTHREAD, "Running synchronized fisheye calibration pipeline... \n");
401 stereoCalibRun();
402}
403
404void stereoCalibThread::processSynchronizedPair(SynchronizedPair& pair, Size boardSize)
405{
406 // Check minimum capture interval
407 const double pairTime = 0.5 * (pair.leftStamp.getTime() + pair.rightStamp.getTime()); //average pair timestamp
408 const double previousProcessedCandidateTime = lastProcessedCandidateTime;
409 if(previousProcessedCandidateTime >= 0.0 && (pairTime - previousProcessedCandidateTime) < minCaptureIntervalSeconds)
410 {
411 yCDebug(STEREOCALIBRATIONTHREAD) << "Skipping candidate pair due to minimum capture interval";
412 return;
413 }
414 if(previousProcessedCandidateTime >= 0.0)
415 {
416 yCDebug(STEREOCALIBRATIONTHREAD) << "Timestamp delta between processed pairs:" << (pairTime - previousProcessedCandidateTime) << "seconds";
417 }
418 lastProcessedCandidateTime = pairTime;
419
420
421 bool foundL=false;
422 bool foundR=false;
423 const Size leftSize(pair.left.width(), pair.left.height());
424 const Size rightSize(pair.right.width(), pair.right.height());
425
426 if(leftSize != _expectedImageSize || rightSize != _expectedImageSize)
427 {
428 if(_expectedImageSize.empty())
429 {
430 _expectedImageSize = leftSize;
431 }
432 }
433
434 if(leftSize != _expectedImageSize || rightSize != _expectedImageSize)
435 {
436 yCError(STEREOCALIBRATIONTHREAD) << "Left and right images have different sizes:" <<
437 "Left:" << leftSize.width << "x" << leftSize.height <<
438 "Right:" << rightSize.width << "x" << rightSize.height;
439 {
440 std::lock_guard<std::mutex> lock(mtx);
441 _calibrationError = "Input images do not match the expected calibration resolution.";
442 _calibrationResults = stereo_calib::CalibrationResult{};
443 }
444 calibrationState.store(CalibrationState::Error);
445
446 return;
447 }
448
449 LeftRgb=yarp::cv::toCvMat(pair.left);
450 RightRgb=yarp::cv::toCvMat(pair.right);
451
452 // Color adjust
453 Mat leftGray, rightGray;
454 cvtColor(LeftRgb,leftGray,CV_RGB2GRAY);
455 cvtColor(RightRgb,rightGray,CV_RGB2GRAY);
456
457 std::vector<Point2f> leftCorners;
458 std::vector<Point2f> rightCorners;
459
460 //TODO: for now we are only using CHESSBOARD GRID. For circle grid and so on we will add an update in the future.
461 if(boardType == "CIRCLES_GRID") {
462 foundL = findCirclesGrid(LeftRgb, boardSize, leftCorners, CALIB_CB_SYMMETRIC_GRID | CALIB_CB_CLUSTERING);
463 foundR = findCirclesGrid(RightRgb, boardSize, rightCorners, CALIB_CB_SYMMETRIC_GRID | CALIB_CB_CLUSTERING);
464 yCWarning(STEREOCALIBRATIONTHREAD) << "Board type:" << boardType << "not yet implemented.";
465 } else if(boardType == "ASYMMETRIC_CIRCLES_GRID") {
466 foundL = findCirclesGrid(LeftRgb, boardSize, leftCorners, CALIB_CB_ASYMMETRIC_GRID | CALIB_CB_CLUSTERING);
467 foundR = findCirclesGrid(RightRgb, boardSize, rightCorners, CALIB_CB_ASYMMETRIC_GRID | CALIB_CB_CLUSTERING);
468 yCWarning(STEREOCALIBRATIONTHREAD) << "Board type:" << boardType << "not yet implemented.";
469 } else if(boardType == "CHESSBOARD_SECTOR_BASED") {
470 foundL = findChessboardCornersSB(leftGray, boardSize, leftCorners);
471 foundR = findChessboardCornersSB(rightGray, boardSize, rightCorners);
472 yCWarning(STEREOCALIBRATIONTHREAD) << "Board type:" << boardType << "not yet implemented.";
473 } else {
474 foundL = findChessboardCorners(leftGray, boardSize, leftCorners, CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE | CALIB_CB_FILTER_QUADS);
475 foundR = findChessboardCorners(rightGray, boardSize, rightCorners, CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE | CALIB_CB_FILTER_QUADS);
476 }
477
478 if(foundL && foundR)
479 {
480 yCDebug(STEREOCALIBRATIONTHREAD) << "Found calibration board corners in both left and right images";
481
482 const Rect leftBoardBounds = boundingRect(leftCorners);
483 const Rect rightBoardBounds = boundingRect(rightCorners);
484 const bool leftBoardTooSmall =
485 leftBoardBounds.width < leftSize.width * minimumBoardSpanRatio ||
486 leftBoardBounds.height < leftSize.height * minimumBoardSpanRatio;
487 const bool rightBoardTooSmall =
488 rightBoardBounds.width < rightSize.width * minimumBoardSpanRatio ||
489 rightBoardBounds.height < rightSize.height * minimumBoardSpanRatio;
490
491 if(leftBoardTooSmall || rightBoardTooSmall)
492 {
493 yCWarning(STEREOCALIBRATIONTHREAD) << "Skipping stereo pair: chessboard is too small."
494 << "Left board:" << leftBoardBounds.width << "x" << leftBoardBounds.height
495 << "of" << leftSize.width << "x" << leftSize.height << ";"
496 << "right board:" << rightBoardBounds.width << "x" << rightBoardBounds.height
497 << "of" << rightSize.width << "x" << rightSize.height << "."
498 << "Each board must span at least" << (minimumBoardSpanRatio * 100.0)
499 << "% of both image dimensions.";
500
501 ++_rejectedDetections;
502 return;
503 }
504
505 TermCriteria criteria = TermCriteria(TermCriteria::EPS+TermCriteria::COUNT, 30, 0.01);
506 cornerSubPix(leftGray, leftCorners, Size(5,5), Size(-1,-1), criteria);
507 cornerSubPix(rightGray, rightCorners, Size(5,5), Size(-1,-1), criteria);
508
509 // Detection and geometry have succeeded. Build and validate the
510 // complete observation before assigning its accepted index.
512 observation.imageSize = Size(pair.left.width(), pair.left.height());
513
514 observation.objectPoints = _chessboardConfiguration.createObjectPoints();
515 observation.leftImagePoints = leftCorners;
516 observation.rightImagePoints = rightCorners;
517
518 observation.leftTimestampSeconds = pair.leftStamp.getTime();
519 observation.rightTimestampSeconds = pair.rightStamp.getTime();
520 observation.timestampDeltaSeconds = pair.timeStampDelta;
521
522 observation.leftSequenceNumber = pair.leftStamp.getCount();
523 observation.rightSequenceNumber = pair.rightStamp.getCount();
524
525 if(!observation.isValid())
526 {
527 yCError(STEREOCALIBRATIONTHREAD) << "Generated invalid stereo observation";
528 ++_rejectedDetections;
529 return;
530 }
531
532 std::size_t observationIndex = 0;
533 observationIndex = _observations.size();
534
535 // Raw images are saved before drawing any optional diagnostics and
536 // only after the pair has become an accepted observation candidate.
537 if(_saveImages)
538 {
539 std::string imageError;
540 if(!_calibrationWriter.writeImagePair(imageDir, observationIndex,
541 LeftRgb, RightRgb,
542 observation.leftImageFilename,
543 observation.rightImageFilename,
544 imageError))
545 {
546 yCError(STEREOCALIBRATIONTHREAD) << "Could not save accepted calibration image pair:" << imageError;
547 {
548 std::lock_guard<std::mutex> lock(mtx);
549 if (calibrationState.load() != CalibrationState::Collecting)
550 {
551 return;
552 }
553
554 _calibrationError = imageError;
555 _calibrationResults = stereo_calib::CalibrationResult{};
556 calibrationState.store(CalibrationState::Error);
557 }
558 return;
559 }
560 }
561
562 // Detection and validation have completed.
563 {
564 std::lock_guard<std::mutex> lock(mtx);
565 if (calibrationState.load() != CalibrationState::Collecting)
566 {
567 return;
568 }
569 _observations.push_back(std::move(observation));
570 }
571
572 // Diagnostic overlays are deliberately the final step: they never
573 // affect the persisted raw dataset or the stored corner coordinates.
574 if(_drawDiagnosticCorners)
575 {
576 drawChessboardCorners(LeftRgb, boardSize, leftCorners, foundL);
577 drawChessboardCorners(RightRgb, boardSize, rightCorners, foundR);
578 }
579
580 ImageOf<PixelRgb>& outimL = outPortLeft.prepare();
581 outimL = pair.left;
582 outPortLeft.setEnvelope(pair.leftStamp);
583 outPortLeft.write();
584
585 ImageOf<PixelRgb>& outimR = outPortRight.prepare();
586 outimR = pair.right;
587 outPortRight.setEnvelope(pair.rightStamp);
588 outPortRight.write();
589
590 }
591 else
592 {
593 ++_rejectedDetections;
594 }
595
596 return;
597}
598
600{
601 StereoCalibStatus result;
602
603 std::lock_guard<std::mutex> lock(mtx);
604
605 switch(calibrationState.load())
606 {
607 case CalibrationState::Idle:
608 result.state = "Idle";
609 break;
610 case CalibrationState::Collecting:
611 result.state = "Collecting";
612 break;
613 case CalibrationState::Calibrating:
614 result.state = "Calibrating";
615 break;
616 case CalibrationState::Completed:
617 result.state = "Completed";
618 break;
619 case CalibrationState::Error:
620 result.state = "Error";
621 break;
622 }
623
624 const auto syncStats = synchronizer.getStatistics();
625
626 result.pairedFrames = syncStats.pairedFrames;
627 result.droppedLeftFrames = syncStats.droppedLeftFrames;
628 result.droppedRightFrames = syncStats.droppedRightFrames;
629
630 if(syncStats.pairedFrames > 0)
631 {
632 result.meanTimestampDeltaMs = 1000.0 * syncStats.accumulatedTimeStampDelta / static_cast<double>(syncStats.pairedFrames);
633 }
634
635 result.maxTimestampDeltaMs = 1000.0 * syncStats.maxTimeStampDelta;
636
637 result.lastCalibrationError = _calibrationError;
638 if(calibrationState.load() == CalibrationState::Completed && _calibrationResults.isValid())
639 {
640 result.calibrationAvailable = true;
641 switch(_calibrationResults.mode)
642 {
644 result.calibrationMode = "MonocularLeft";
645 break;
647 result.calibrationMode = "MonocularRight";
648 break;
650 result.calibrationMode = "MonocularBoth";
651 break;
653 result.calibrationMode = "StereoFull";
654 break;
655 }
656
657 if(_calibrationResults.leftCamera.isValid())
658 {
659 result.leftMonocularRms = _calibrationResults.leftCamera.rms;
660 }
661 if(_calibrationResults.rightCamera.isValid())
662 {
663 result.rightMonocularRms = _calibrationResults.rightCamera.rms;
664 }
665 if(_calibrationResults.stereo.isValid())
666 {
667 result.stereoRms = _calibrationResults.stereo.rms;
668 result.baselineNorm = _calibrationResults.quality.baseline;
669 }
670 }
671
672 return result;
673}
674
675bool stereoCalibThread::shouldQueueFrameForCollection(const Stamp& timestamp) const
676{
677 return lastProcessedCandidateTime < 0.0 ||
678 (timestamp.getTime() - lastProcessedCandidateTime) >= minCaptureIntervalSeconds;
679}
680
681void stereoCalibThread::stereoCalibRun()
682{
683 Size boardSize;
684 boardSize.width=this->boardWidth;
685 boardSize.height=this->boardHeight;
686
687 while (!isStopping())
688 {
689 // Keep the worker alive for RPC status inspection after a calibration
690 // failure, but stop consuming/processing camera data until a new start
691 // request changes the state back to Collecting.
692 if(calibrationState.load() == CalibrationState::Error)
693 {
694 Time::delay(0.1);
695 continue;
696 }
697
698 if(collectionResetRequested.exchange(false))
699 {
700 synchronizer.reset();
701 lastProcessedCandidateTime = -1.0;
702 _expectedImageSize = {};
703 _observations.clear();
704 _rejectedDetections = 0;
705 {
706 std::lock_guard<std::mutex> lock(mtx);
707 _calibrationResults = stereo_calib::CalibrationResult{};
708 _calibrationError.clear();
709 }
710 }
711
712 bool areFramesReceived = false;
713 ImageOf<PixelRgb> *tmpL = imagePortInLeft.read(false);
714
715 if(tmpL != nullptr)
716 {
717 areFramesReceived = true;
718
719 Stamp TSLeft;
720 imagePortInLeft.getEnvelope(TSLeft);
721
722 // Always publish preview immediately
723 ImageOf<PixelRgb>& outimL = outPortLeft.prepare();
724 outimL = *tmpL;
725 outPortLeft.setEnvelope(TSLeft);
726 outPortLeft.write();
727
728 // Only enqueue a separate copy while collecting
729 if(calibrationState.load() == CalibrationState::Collecting &&
730 shouldQueueFrameForCollection(TSLeft))
731 {
732 synchronizer.pushLeft(*tmpL, TSLeft); // this is done for not consuming memory bandwidth for data that will never be used by the calibration
733 }
734 }
735
736 ImageOf<PixelRgb> *tmpR = imagePortInRight.read(false);
737 if(tmpR!=nullptr)
738 {
739 areFramesReceived = true;
740
741 Stamp TSRight;
742 imagePortInRight.getEnvelope(TSRight);
743
744 // Always publish preview immediately
745 ImageOf<PixelRgb>& outimR=outPortRight.prepare();
746 outimR = *tmpR;
747 outPortRight.setEnvelope(TSRight);
748 outPortRight.write();
749
750
751 // Only enqueue a separate copy while collecting
752 if(calibrationState.load() == CalibrationState::Collecting &&
753 shouldQueueFrameForCollection(TSRight))
754 {
755 synchronizer.pushRight(*tmpR, TSRight);
756 }
757 }
758
759 std::vector<stereo_calib::StereoObservation> observationSnapshot;
760
761 if(calibrationState.load() == CalibrationState::Collecting)
762 {
763 SynchronizedPair pair;
764 while(calibrationState.load() == CalibrationState::Collecting &&
765 synchronizer.tryPopPair(pair))
766 {
767 // Process the synchronized pair
768 processSynchronizedPair(pair, boardSize);
769
770 if(calibrationState.load() != CalibrationState::Collecting)
771 {
772 break;
773 }
774 if(_observations.size() >= static_cast<std::size_t>(numOfPairs))
775 {
776 yCInfo(STEREOCALIBRATIONTHREAD, "Collected %zu valid stereo observations. Stopping collection.", _observations.size());
777 observationSnapshot = _observations;
778 calibrationState.store(CalibrationState::Calibrating);
779 yCInfo(STEREOCALIBRATIONTHREAD) << "Observation collection complete";
780 break;
781 }
782 }
783 }
784
785 if(calibrationState.load() == CalibrationState::Calibrating)
786 {
787 yCInfo(STEREOCALIBRATIONTHREAD) << "Starting the calibration process";
788 if(observationSnapshot.empty())
789 {
790 observationSnapshot = _observations;
791 }
792
793 stereo_calib::CalibrationResult calibrationResult;
794 std::string calibrationError;
795 bool success = false;
796 if (_cameraModel == stereo_calib::CameraModel::Pinhole)
797 {
798 stereo_calib::PinholeCalibrationOptions options = _pinholeCalibrationOptions;
799 options.common.calibrationMode = _calibrationMode;
800 options.common.imageSize = observationSnapshot.front().imageSize;
801 success = _pinholeCalibrationEngine.calibrate(
802 observationSnapshot,
803 options,
804 calibrationResult,
805 calibrationError
806 );
807 }
808 else if(_cameraModel == stereo_calib::CameraModel::Fisheye)
809 {
810 stereo_calib::FisheyeCalibrationOptions options = _fisheyeCalibrationOptions;
811 options.common.calibrationMode = _calibrationMode;
812 options.common.imageSize = observationSnapshot.front().imageSize;
813 options.cameraFocalLengthGuess = 625.0;
814 success = _fisheyeCalibrationEngine.calibrate(
815 observationSnapshot,
816 options,
817 calibrationResult,
818 calibrationError
819 );
820 }
821 else
822 {
823 calibrationError = "Unsupported camera model.";
824 success = false;
825 }
826
827 // success and writer should be common between fisheye and pinhole engines, so this block can be shared
828 if(!success)
829 {
830 // The engine converts OpenCV exceptions into a diagnostic. Log
831 // it here, where YARP logging is allowed, and stop calibration
832 // processing while keeping the Error state and status available.
833 yCError(STEREOCALIBRATIONTHREAD) << "Calibration failed:" << calibrationError;
834 {
835 std::lock_guard<std::mutex> lock(mtx);
836 _calibrationResults = stereo_calib::CalibrationResult{};
837 _calibrationError = calibrationError.empty()
838 ? "Calibration failed without an error message."
839 : calibrationError;
840 }
841 calibrationState.store(CalibrationState::Error);
842 continue;
843 }
844
845 const auto syncStats = synchronizer.getStatistics();
846 calibrationResult.quality.rejectedDetections = _rejectedDetections;
847 calibrationResult.quality.acceptedObservations = observationSnapshot.size();
848 calibrationResult.quality.synchronizedPairs = syncStats.pairedFrames;
849 if(syncStats.pairedFrames > 0)
850 {
851 calibrationResult.quality.meanTimestampDeltaMs = 1000.0 * syncStats.accumulatedTimeStampDelta / static_cast<double>(syncStats.pairedFrames);
852 }
853 calibrationResult.quality.maxTimestampDeltaMs = syncStats.maxTimeStampDelta;
854 calibrationResult.quality.baseline = cv::norm(calibrationResult.stereo.T);
855
856 std::string persistenceError;
857 if(!_calibrationWriter.write(camCalibFile, calibrationResult, persistenceError))
858 {
859 yCError(STEREOCALIBRATIONTHREAD) << "Could not save calibration results:" << persistenceError;
860 {
861 std::lock_guard<std::mutex> lock(mtx);
862 _calibrationResults = std::move(calibrationResult);
863 _calibrationError = persistenceError;
864 }
865 calibrationState.store(CalibrationState::Error);
866 continue;
867 }
868 if(!_calibrationWriter.writeObservations(_observationsFile, observationSnapshot, persistenceError))
869 {
870 yCError(STEREOCALIBRATIONTHREAD) << "Could not save calibration observations:" << persistenceError;
871 {
872 std::lock_guard<std::mutex> lock(mtx);
873 _calibrationResults = std::move(calibrationResult);
874 _calibrationError = persistenceError;
875 }
876 calibrationState.store(CalibrationState::Error);
877 continue;
878 }
879
880 logCalibrationResult(calibrationResult);
881 {
882 std::lock_guard<std::mutex> lock(mtx);
883 _calibrationResults = std::move(calibrationResult);
884 _calibrationError.clear();
885 }
886
887 calibrationState.store(CalibrationState::Completed);
888 yCInfo(STEREOCALIBRATIONTHREAD) << "Entire calibration process completed";
889 }
890
891 if(!areFramesReceived)
892 {
893 Time::delay(0.001); // Sleep for a short duration to avoid busy waiting
894 }
895 cout.flush();
896 }
897}
898
900{
901 imagePortInRight.close();
902 imagePortInLeft.close();
903 outPortLeft.close();
904 outPortRight.close();
905 commandPort->close();
906
907 if (polyHead.isValid())
908 polyHead.close();
909
910 if (polyTorso.isValid())
911 polyTorso.close();
912}
913
915 // TODO: the following 2 should not ne necessary since we already store their state in stop()
916 calibrationState.store(CalibrationState::Idle);
917 collectionResetRequested.store(false);
918 imagePortInRight.interrupt();
919 imagePortInLeft.interrupt();
920 outPortLeft.interrupt();
921 outPortRight.interrupt();
922 commandPort->interrupt();
923
924}
926
927 std::lock_guard<std::mutex> lock(mtx);
928
929 const CalibrationState currentState = calibrationState.load();
930
931 if(currentState == CalibrationState::Collecting || currentState == CalibrationState::Calibrating)
932 {
933 yCWarning(STEREOCALIBRATIONTHREAD) << "Cannot start a new calibration while calibration is already running";
934 return;
935 }
936
937 _calibrationResults = stereo_calib::CalibrationResult{};
938 _calibrationError.clear();
939 calibrationState.store(CalibrationState::Collecting);
940 collectionResetRequested.store(true);
941
942 yCInfo(STEREOCALIBRATIONTHREAD) << "Calibration collection started";
943}
944
946 std::lock_guard<std::mutex> lock(mtx);
947
948 if(calibrationState.load() != CalibrationState::Collecting)
949 {
950 yCWarning(STEREOCALIBRATIONTHREAD) << "Cannot stop calibration collection when it is not running";
951 return;
952 }
953 calibrationState.store(CalibrationState::Idle);
954 collectionResetRequested.store(true);
955
956 yCInfo(STEREOCALIBRATIONTHREAD) << "Calibration collection stopped";
957}
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
bool calibrate(const std::vector< StereoObservation > &observations, const PinholeCalibrationOptions &options, CalibrationResult &result, std::string &errorMessage) const
constexpr double CTRL_DEG2RAD
PI/180.
Definition math.h:66
#define LEFT
#define RIGHT
ImageOf< PixelRgb > image
std::string lastCalibrationError
std::size_t droppedLeftFrames
std::string calibrationMode
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