iCub-main
Loading...
Searching...
No Matches
FisheyeCalibrationEngine.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <numeric>
6#include <sstream>
7
8namespace
9{
10
11constexpr std::size_t minimumObservations = 30;
12constexpr std::size_t minimumPointsPerObservation = 12;
13
14bool isFinite(const cv::Mat& matrix)
15{
16 return !matrix.empty() && cv::checkRange(matrix, true, nullptr);
17}
18
19bool isFinite(const cv::Point2f& point)
20{
21 return std::isfinite(point.x) && std::isfinite(point.y);
22}
23
24bool isFinite(const cv::Point3f& point)
25{
26 return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.z);
27}
28
29bool isValidCameraResult(const stereo_calib::CameraCalibrationResult& result)
30{
31 return result.imageSize.width > 0 && result.imageSize.height > 0 &&
32 result.K.rows == 3 && result.K.cols == 3 && result.K.type() == CV_64F &&
33 result.D.total() == 4 && result.D.type() == CV_64F &&
34 std::isfinite(result.rms) && result.rms >= 0.0 &&
35 result.K.at<double>(0, 0) > 0.0 && result.K.at<double>(1, 1) > 0.0 &&
36 isFinite(result.K) && isFinite(result.D) &&
37 result.rotationVectors.size() == result.translationVectors.size() &&
38 result.rotationVectors.size() == result.perViewRms.size() &&
39 std::all_of(result.perViewRms.begin(), result.perViewRms.end(),
40 [](double value) { return std::isfinite(value) && value >= 0.0; });
41}
42
43bool isValidStereoResult(const stereo_calib::StereoCalibrationResult& result)
44{
45 if (result.R.rows != 3 || result.R.cols != 3 || result.R.type() != CV_64F ||
46 result.T.total() != 3 || result.T.type() != CV_64F ||
47 !std::isfinite(result.rms) || result.rms < 0.0 ||
48 !isFinite(result.R) || !isFinite(result.T))
49 {
50 return false;
51 }
52
53 return std::abs(cv::determinant(result.R) - 1.0) < 1e-3 && cv::norm(result.T) > 1e-9;
54}
55
56bool isValidRectificationResult(const stereo_calib::RectificationResult& result)
57{
58 return result.R1.rows == 3 && result.R1.cols == 3 && result.R1.type() == CV_64F &&
59 result.R2.rows == 3 && result.R2.cols == 3 && result.R2.type() == CV_64F &&
60 result.P1.rows == 3 && result.P1.cols == 4 && result.P1.type() == CV_64F &&
61 result.P2.rows == 3 && result.P2.cols == 4 && result.P2.type() == CV_64F &&
62 result.Q.rows == 4 && result.Q.cols == 4 && result.Q.type() == CV_64F &&
63 isFinite(result.R1) && isFinite(result.R2) && isFinite(result.P1) &&
64 isFinite(result.P2) && isFinite(result.Q);
65}
66
67bool isValidQuality(const stereo_calib::CalibrationQualityMetrics& quality)
68{
69 return quality.acceptedObservations > 0 &&
70 std::isfinite(quality.baseline) && quality.baseline > 0.0 &&
71 std::isfinite(quality.meanVerticalRectificationErrorPx) && quality.meanVerticalRectificationErrorPx >= 0.0 &&
72 std::isfinite(quality.medianVerticalRectificationErrorPx) && quality.medianVerticalRectificationErrorPx >= 0.0 &&
73 std::isfinite(quality.rmsVerticalRectificationErrorPx) && quality.rmsVerticalRectificationErrorPx >= 0.0 &&
74 std::isfinite(quality.p95VerticalRectificationErrorPx) && quality.p95VerticalRectificationErrorPx >= 0.0 &&
75 std::isfinite(quality.maxVerticalRectificationErrorPx) && quality.maxVerticalRectificationErrorPx >= 0.0;
76}
77
78} // namespace
79
80namespace stereo_calib
81{
82
84 const std::vector<StereoObservation>& observations,
85 const FisheyeCalibrationOptions& options,
86 CalibrationResult& result,
87 std::string& errorMessage) const
88{
89 result = CalibrationResult{};
90 errorMessage.clear();
91
92 CalibrationResult calculated;
93 calculated.mode = options.calibrationMode;
94
95 try
96 {
97 if (!options.isValid())
98 {
99 errorMessage = "Invalid fisheye calibration options.";
100 return false;
101 }
102
103 if (!validateObservations(observations, options.imageSize, errorMessage))
104 {
105 return false;
106 }
107
108 switch (options.calibrationMode)
109 {
111 if (!calibrateMonocular(observations, CameraSide::Left, options,
112 calculated.leftCamera, errorMessage))
113 {
114 return false;
115 }
116 break;
117
119 if (!calibrateMonocular(observations, CameraSide::Right, options,
120 calculated.rightCamera, errorMessage))
121 {
122 return false;
123 }
124 break;
125
127 if (!calibrateMonocular(observations, CameraSide::Left, options,
128 calculated.leftCamera, errorMessage) ||
129 !calibrateMonocular(observations, CameraSide::Right, options,
130 calculated.rightCamera, errorMessage))
131 {
132 return false;
133 }
134 break;
135
137 if (!calibrateMonocular(observations, CameraSide::Left, options,
138 calculated.leftCamera, errorMessage) ||
139 !calibrateMonocular(observations, CameraSide::Right, options,
140 calculated.rightCamera, errorMessage) ||
141 !calibrateStereo(observations, options, calculated.leftCamera,
142 calculated.rightCamera, calculated.stereo, errorMessage) ||
143 !computeRectification(options, calculated.leftCamera, calculated.rightCamera,
144 calculated.stereo, calculated.rectification, errorMessage) ||
145 !evaluateRectification(observations, calculated.leftCamera, calculated.rightCamera,
146 calculated.stereo, calculated.rectification,
147 calculated.quality, errorMessage))
148 {
149 return false;
150 }
151 break;
152
153 default:
154 errorMessage = "Unknown calibration mode.";
155 return false;
156 }
157
158 if (!calculated.isValid() ||
159 (calculated.mode == CalibrationMode::StereoFull && !isValidQuality(calculated.quality)))
160 {
161 errorMessage = "Calibration produced an invalid result.";
162 return false;
163 }
164
165 result = std::move(calculated);
166 return true;
167 }
168 catch (const cv::Exception& exception)
169 {
170 errorMessage = std::string("OpenCV calibration error: ") + exception.what();
171 }
172 catch (const std::exception& exception)
173 {
174 errorMessage = std::string("Calibration error: ") + exception.what();
175 }
176
177 result = CalibrationResult{};
178 return false;
179}
180
181bool FisheyeCalibrationEngine::validateObservations(
182 const std::vector<StereoObservation>& observations,
183 const cv::Size& expectedImageSize,
184 std::string& errorMessage) const
185{
186 if (observations.size() < minimumObservations)
187 {
188 errorMessage = "At least " + std::to_string(minimumObservations) +
189 " stereo observations are required.";
190 return false;
191 }
192
193 std::size_t expectedPointCount = 0;
194 std::vector<cv::Point3f> referenceObjectPoints;
195 for (std::size_t index = 0; index < observations.size(); ++index)
196 {
197 const StereoObservation& observation = observations[index];
198 const std::string prefix = "Observation #" + std::to_string(index + 1) + ": ";
199
200 if (observation.imageSize != expectedImageSize)
201 {
202 std::ostringstream message;
203 message << prefix << "image size is " << observation.imageSize.width << "x"
204 << observation.imageSize.height << ", expected " << expectedImageSize.width
205 << "x" << expectedImageSize.height << ".";
206 errorMessage = message.str();
207 return false;
208 }
209
210 if (!observation.isValid())
211 {
212 errorMessage = prefix + "has inconsistent point lists or an invalid timestamp delta.";
213 return false;
214 }
215
216 if (observation.objectPoints.size() < minimumPointsPerObservation)
217 {
218 errorMessage = prefix + "contains fewer than " +
219 std::to_string(minimumPointsPerObservation) + " calibration points.";
220 return false;
221 }
222
223 if (!std::isfinite(observation.leftTimestampSeconds) ||
224 !std::isfinite(observation.rightTimestampSeconds) ||
225 !std::isfinite(observation.timestampDeltaSeconds))
226 {
227 errorMessage = prefix + "contains a non-finite timestamp.";
228 return false;
229 }
230
231 for (std::size_t pointIndex = 0; pointIndex < observation.objectPoints.size(); ++pointIndex)
232 {
233 if (!isFinite(observation.objectPoints[pointIndex]) ||
234 !isFinite(observation.leftImagePoints[pointIndex]) ||
235 !isFinite(observation.rightImagePoints[pointIndex]))
236 {
237 errorMessage = prefix + "point #" + std::to_string(pointIndex + 1) +
238 " contains a non-finite coordinate.";
239 return false;
240 }
241 }
242
243 if (index == 0)
244 {
245 expectedPointCount = observation.objectPoints.size();
246 referenceObjectPoints = observation.objectPoints;
247 }
248 else if (observation.objectPoints.size() != expectedPointCount)
249 {
250 errorMessage = prefix + "has " + std::to_string(observation.objectPoints.size()) +
251 " object points, expected " + std::to_string(expectedPointCount) + ".";
252 return false;
253 }
254 else
255 {
256 for (std::size_t pointIndex = 0; pointIndex < expectedPointCount; ++pointIndex)
257 {
258 if (cv::norm(observation.objectPoints[pointIndex] - referenceObjectPoints[pointIndex]) > 1e-6)
259 {
260 errorMessage = prefix + "uses object points different from observation #1.";
261 return false;
262 }
263 }
264 }
265 }
266
267 return true;
268}
269
270bool FisheyeCalibrationEngine::calibrateMonocular(
271 const std::vector<StereoObservation>& observations,
272 CameraSide cameraSide,
273 const FisheyeCalibrationOptions& options,
274 CameraCalibrationResult& result,
275 std::string& errorMessage) const
276{
277 std::vector<std::vector<cv::Point3f>> objectPoints;
278 std::vector<std::vector<cv::Point2f>> leftImagePoints;
279 std::vector<std::vector<cv::Point2f>> rightImagePoints;
280 if (!extractCalibrationPoints(observations, objectPoints, leftImagePoints, rightImagePoints, errorMessage))
281 {
282 return false;
283 }
284
285 const std::vector<std::vector<cv::Point2f>>& imagePoints =
286 cameraSide == CameraSide::Left ? leftImagePoints : rightImagePoints;
287 cv::Mat intrinsic = cv::Mat::eye(3, 3, CV_64F);
288 intrinsic.at<double>(0, 0) = options.cameraFocalLengthGuess;
289 intrinsic.at<double>(1, 1) = options.cameraFocalLengthGuess;
290 intrinsic.at<double>(0, 2) = options.imageSize.width * 0.5;
291 intrinsic.at<double>(1, 2) = options.imageSize.height * 0.5;
292 cv::Mat distortion = cv::Mat::zeros(4, 1, CV_64F);
293 std::vector<cv::Mat> rotationVectors;
294 std::vector<cv::Mat> translationVectors;
295
296 const double rms = cv::fisheye::calibrate(objectPoints, imagePoints, options.imageSize,
297 intrinsic, distortion, rotationVectors,
298 translationVectors, options.monocularFlags,
299 options.criteria);
300
301 result = CameraCalibrationResult{};
302 result.imageSize = options.imageSize;
303 result.K = intrinsic.clone();
304 result.D = distortion.reshape(1, 4).clone();
305 result.rotationVectors = std::move(rotationVectors);
306 result.translationVectors = std::move(translationVectors);
307 result.rms = rms;
308
309 if (result.rotationVectors.size() != observations.size() ||
310 result.translationVectors.size() != observations.size())
311 {
312 errorMessage = "OpenCV returned an incomplete set of monocular poses.";
313 return false;
314 }
315
316 result.perViewRms.reserve(observations.size());
317 for (std::size_t index = 0; index < observations.size(); ++index)
318 {
319 std::vector<cv::Point2f> projectedPoints;
320 cv::fisheye::projectPoints(objectPoints[index], projectedPoints,
321 result.rotationVectors[index], result.translationVectors[index],
322 result.K, result.D);
323 if (projectedPoints.size() != imagePoints[index].size())
324 {
325 errorMessage = "OpenCV returned an incomplete projected point set.";
326 return false;
327 }
328 const double viewRms = cv::norm(projectedPoints, imagePoints[index], cv::NORM_L2) /
329 std::sqrt(static_cast<double>(projectedPoints.size()));
330 result.perViewRms.push_back(viewRms);
331 }
332
333 if (!isValidCameraResult(result))
334 {
335 errorMessage = "Monocular calibration produced invalid camera parameters.";
336 return false;
337 }
338 return true;
339}
340
341bool FisheyeCalibrationEngine::calibrateStereo(
342 const std::vector<StereoObservation>& observations,
343 const FisheyeCalibrationOptions& options,
344 const CameraCalibrationResult& leftCamera,
345 const CameraCalibrationResult& rightCamera,
346 StereoCalibrationResult& result,
347 std::string& errorMessage) const
348{
349 std::vector<std::vector<cv::Point3f>> objectPoints;
350 std::vector<std::vector<cv::Point2f>> leftImagePoints;
351 std::vector<std::vector<cv::Point2f>> rightImagePoints;
352 if (!extractCalibrationPoints(observations, objectPoints, leftImagePoints, rightImagePoints, errorMessage))
353 {
354 return false;
355 }
356
357 cv::Mat leftK = leftCamera.K.clone();
358 cv::Mat leftD = leftCamera.D.clone();
359 cv::Mat rightK = rightCamera.K.clone();
360 cv::Mat rightD = rightCamera.D.clone();
361 cv::Mat rotation;
362 cv::Mat translation;
363 const double rms = cv::fisheye::stereoCalibrate(objectPoints, leftImagePoints, rightImagePoints,
364 leftK, leftD, rightK, rightD, options.imageSize,
365 rotation, translation, options.stereoFlags,
366 options.criteria);
367
368 result = StereoCalibrationResult{};
369 result.R = rotation.clone();
370 result.T = translation.reshape(1, 3).clone();
371 result.rms = rms;
372 if (!isValidStereoResult(result))
373 {
374 errorMessage = "Stereo calibration produced invalid extrinsic parameters.";
375 return false;
376 }
377 return true;
378}
379
380bool FisheyeCalibrationEngine::computeRectification(
381 const FisheyeCalibrationOptions& options,
382 const CameraCalibrationResult& leftCamera,
383 const CameraCalibrationResult& rightCamera,
384 const StereoCalibrationResult& stereo,
385 RectificationResult& result,
386 std::string& errorMessage) const
387{
388 result = RectificationResult{};
389 cv::fisheye::stereoRectify(leftCamera.K, leftCamera.D, rightCamera.K, rightCamera.D,
390 options.imageSize, stereo.R, stereo.T, result.R1, result.R2,
391 result.P1, result.P2, result.Q,
392 options.zeroDisparity ? cv::CALIB_ZERO_DISPARITY : 0,
393 options.imageSize, options.rectificationBalance,
394 options.rectificationFovScale);
395 result.outputImageSize = options.imageSize;
396 result.balance = options.rectificationBalance;
397 result.fovScale = options.rectificationFovScale;
398 result.zeroDisparity = options.zeroDisparity;
399
400 if (!isValidRectificationResult(result))
401 {
402 errorMessage = "Stereo rectification produced invalid matrices.";
403 return false;
404 }
405 return true;
406}
407
408bool FisheyeCalibrationEngine::evaluateRectification(
409 const std::vector<StereoObservation>& observations,
410 const CameraCalibrationResult& leftCamera,
411 const CameraCalibrationResult& rightCamera,
412 const StereoCalibrationResult& stereo,
413 const RectificationResult& rectification,
414 CalibrationQualityMetrics& quality,
415 std::string& errorMessage) const
416{
417 std::vector<double> verticalErrors;
418 verticalErrors.reserve(observations.size() * observations.front().objectPoints.size());
419 double timestampDeltaSumMs = 0.0;
420
421 for (const StereoObservation& observation : observations)
422 {
423 std::vector<cv::Point2f> rectifiedLeftPoints;
424 std::vector<cv::Point2f> rectifiedRightPoints;
425 cv::fisheye::undistortPoints(observation.leftImagePoints, rectifiedLeftPoints,
426 leftCamera.K, leftCamera.D, rectification.R1, rectification.P1);
427 cv::fisheye::undistortPoints(observation.rightImagePoints, rectifiedRightPoints,
428 rightCamera.K, rightCamera.D, rectification.R2, rectification.P2);
429 if (rectifiedLeftPoints.size() != rectifiedRightPoints.size() || rectifiedLeftPoints.empty())
430 {
431 errorMessage = "Rectification returned inconsistent point sets.";
432 return false;
433 }
434
435 for (std::size_t pointIndex = 0; pointIndex < rectifiedLeftPoints.size(); ++pointIndex)
436 {
437 const double error = std::abs(static_cast<double>(rectifiedLeftPoints[pointIndex].y) -
438 static_cast<double>(rectifiedRightPoints[pointIndex].y));
439 if (!std::isfinite(error))
440 {
441 errorMessage = "Rectification produced a non-finite vertical error.";
442 return false;
443 }
444 verticalErrors.push_back(error);
445 }
446
447 timestampDeltaSumMs += observation.timestampDeltaSeconds * 1000.0;
448 quality.maxTimestampDeltaMs = std::max(quality.maxTimestampDeltaMs,
449 observation.timestampDeltaSeconds * 1000.0);
450 }
451
452 if (verticalErrors.empty())
453 {
454 errorMessage = "No rectified correspondences are available for quality evaluation.";
455 return false;
456 }
457
458 const double sum = std::accumulate(verticalErrors.begin(), verticalErrors.end(), 0.0);
459 const double sumSquares = std::inner_product(verticalErrors.begin(), verticalErrors.end(),
460 verticalErrors.begin(), 0.0);
461 std::sort(verticalErrors.begin(), verticalErrors.end());
462
463 quality.synchronizedPairs = 0;
464 quality.acceptedObservations = observations.size();
465 quality.rejectedDetections = 0;
466 quality.baseline = cv::norm(stereo.T);
467 quality.meanTimestampDeltaMs = timestampDeltaSumMs / static_cast<double>(observations.size());
468 quality.meanVerticalRectificationErrorPx = sum / static_cast<double>(verticalErrors.size());
469 quality.rmsVerticalRectificationErrorPx = std::sqrt(sumSquares / static_cast<double>(verticalErrors.size()));
470 const std::size_t middle = verticalErrors.size() / 2;
471 quality.medianVerticalRectificationErrorPx = verticalErrors.size() % 2 == 0
472 ? 0.5 * (verticalErrors[middle - 1] + verticalErrors[middle])
473 : verticalErrors[middle];
474 const std::size_t p95Index = static_cast<std::size_t>(std::ceil(0.95 * verticalErrors.size())) - 1;
475 quality.p95VerticalRectificationErrorPx = verticalErrors[p95Index];
476 quality.maxVerticalRectificationErrorPx = verticalErrors.back();
477
478 if (!isValidQuality(quality))
479 {
480 errorMessage = "Rectification quality metrics are invalid.";
481 return false;
482 }
483 return true;
484}
485
486bool FisheyeCalibrationEngine::extractCalibrationPoints(
487 const std::vector<StereoObservation>& observations,
488 std::vector<std::vector<cv::Point3f>>& objectPoints,
489 std::vector<std::vector<cv::Point2f>>& leftImagePoints,
490 std::vector<std::vector<cv::Point2f>>& rightImagePoints,
491 std::string& errorMessage) const
492{
493 objectPoints.clear();
494 leftImagePoints.clear();
495 rightImagePoints.clear();
496 objectPoints.reserve(observations.size());
497 leftImagePoints.reserve(observations.size());
498 rightImagePoints.reserve(observations.size());
499
500 for (const StereoObservation& observation : observations)
501 {
502 objectPoints.push_back(observation.objectPoints);
503 leftImagePoints.push_back(observation.leftImagePoints);
504 rightImagePoints.push_back(observation.rightImagePoints);
505 }
506 if (objectPoints.empty())
507 {
508 errorMessage = "No calibration points are available.";
509 return false;
510 }
511 return true;
512}
513
514} // namespace stereo_calib
bool calibrate(const std::vector< StereoObservation > &observations, const FisheyeCalibrationOptions &options, CalibrationResult &result, std::string &errorMessage) const
bool error
CameraCalibrationResult leftCamera
StereoCalibrationResult stereo
CameraCalibrationResult rightCamera
CalibrationQualityMetrics quality