11constexpr std::size_t minimumObservations = 30;
12constexpr std::size_t minimumPointsPerObservation = 12;
14bool isFinite(
const cv::Mat& matrix)
16 return !matrix.empty() && cv::checkRange(matrix,
true,
nullptr);
19bool isFinite(
const cv::Point2f& point)
21 return std::isfinite(point.x) && std::isfinite(point.y);
24bool isFinite(
const cv::Point3f& point)
26 return std::isfinite(point.x) && std::isfinite(point.y) && std::isfinite(point.z);
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) &&
40 [](
double value) { return std::isfinite(value) && value >= 0.0; });
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))
53 return std::abs(cv::determinant(result.
R) - 1.0) < 1
e-3 && cv::norm(result.
T) > 1
e-9;
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);
84 const std::vector<StereoObservation>& observations,
87 std::string& errorMessage)
const
99 errorMessage =
"Invalid fisheye calibration options.";
103 if (!validateObservations(observations, options.
imageSize, errorMessage))
141 !calibrateStereo(observations, options, calculated.
leftCamera,
147 calculated.
quality, errorMessage))
154 errorMessage =
"Unknown calibration mode.";
161 errorMessage =
"Calibration produced an invalid result.";
165 result = std::move(calculated);
168 catch (
const cv::Exception& exception)
170 errorMessage = std::string(
"OpenCV calibration error: ") + exception.what();
172 catch (
const std::exception& exception)
174 errorMessage = std::string(
"Calibration error: ") + exception.what();
181bool FisheyeCalibrationEngine::validateObservations(
182 const std::vector<StereoObservation>& observations,
183 const cv::Size& expectedImageSize,
184 std::string& errorMessage)
const
186 if (observations.size() < minimumObservations)
188 errorMessage =
"At least " + std::to_string(minimumObservations) +
189 " stereo observations are required.";
193 std::size_t expectedPointCount = 0;
194 std::vector<cv::Point3f> referenceObjectPoints;
195 for (std::size_t index = 0; index < observations.size(); ++index)
197 const StereoObservation& observation = observations[index];
198 const std::string prefix =
"Observation #" + std::to_string(index + 1) +
": ";
200 if (observation.imageSize != expectedImageSize)
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();
210 if (!observation.isValid())
212 errorMessage = prefix +
"has inconsistent point lists or an invalid timestamp delta.";
216 if (observation.objectPoints.size() < minimumPointsPerObservation)
218 errorMessage = prefix +
"contains fewer than " +
219 std::to_string(minimumPointsPerObservation) +
" calibration points.";
223 if (!std::isfinite(observation.leftTimestampSeconds) ||
224 !std::isfinite(observation.rightTimestampSeconds) ||
225 !std::isfinite(observation.timestampDeltaSeconds))
227 errorMessage = prefix +
"contains a non-finite timestamp.";
231 for (std::size_t pointIndex = 0; pointIndex < observation.objectPoints.size(); ++pointIndex)
233 if (!isFinite(observation.objectPoints[pointIndex]) ||
234 !isFinite(observation.leftImagePoints[pointIndex]) ||
235 !isFinite(observation.rightImagePoints[pointIndex]))
237 errorMessage = prefix +
"point #" + std::to_string(pointIndex + 1) +
238 " contains a non-finite coordinate.";
245 expectedPointCount = observation.objectPoints.size();
246 referenceObjectPoints = observation.objectPoints;
248 else if (observation.objectPoints.size() != expectedPointCount)
250 errorMessage = prefix +
"has " + std::to_string(observation.objectPoints.size()) +
251 " object points, expected " + std::to_string(expectedPointCount) +
".";
256 for (std::size_t pointIndex = 0; pointIndex < expectedPointCount; ++pointIndex)
258 if (cv::norm(observation.objectPoints[pointIndex] - referenceObjectPoints[pointIndex]) > 1
e-6)
260 errorMessage = prefix +
"uses object points different from observation #1.";
270bool FisheyeCalibrationEngine::calibrateMonocular(
271 const std::vector<StereoObservation>& observations,
273 const FisheyeCalibrationOptions& options,
274 CameraCalibrationResult& result,
275 std::string& errorMessage)
const
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))
285 const std::vector<std::vector<cv::Point2f>>& imagePoints =
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;
296 const double rms = cv::fisheye::calibrate(objectPoints, imagePoints, options.imageSize,
297 intrinsic, distortion, rotationVectors,
298 translationVectors, options.monocularFlags,
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);
309 if (result.rotationVectors.size() != observations.size() ||
310 result.translationVectors.size() != observations.size())
312 errorMessage =
"OpenCV returned an incomplete set of monocular poses.";
316 result.perViewRms.reserve(observations.size());
317 for (std::size_t index = 0; index < observations.size(); ++index)
319 std::vector<cv::Point2f> projectedPoints;
320 cv::fisheye::projectPoints(objectPoints[index], projectedPoints,
321 result.rotationVectors[index], result.translationVectors[index],
323 if (projectedPoints.size() != imagePoints[index].size())
325 errorMessage =
"OpenCV returned an incomplete projected point set.";
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);
333 if (!isValidCameraResult(result))
335 errorMessage =
"Monocular calibration produced invalid camera parameters.";
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
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))
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();
363 const double rms = cv::fisheye::stereoCalibrate(objectPoints, leftImagePoints, rightImagePoints,
364 leftK, leftD, rightK, rightD, options.imageSize,
365 rotation, translation, options.stereoFlags,
368 result = StereoCalibrationResult{};
369 result.R = rotation.clone();
370 result.T = translation.reshape(1, 3).clone();
372 if (!isValidStereoResult(result))
374 errorMessage =
"Stereo calibration produced invalid extrinsic parameters.";
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
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;
400 if (!isValidRectificationResult(result))
402 errorMessage =
"Stereo rectification produced invalid matrices.";
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
417 std::vector<double> verticalErrors;
418 verticalErrors.reserve(observations.size() * observations.front().objectPoints.size());
419 double timestampDeltaSumMs = 0.0;
421 for (
const StereoObservation& observation : observations)
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())
431 errorMessage =
"Rectification returned inconsistent point sets.";
435 for (std::size_t pointIndex = 0; pointIndex < rectifiedLeftPoints.size(); ++pointIndex)
437 const double error = std::abs(
static_cast<double>(rectifiedLeftPoints[pointIndex].
y) -
438 static_cast<double>(rectifiedRightPoints[pointIndex].
y));
439 if (!std::isfinite(
error))
441 errorMessage =
"Rectification produced a non-finite vertical error.";
444 verticalErrors.push_back(
error);
447 timestampDeltaSumMs += observation.timestampDeltaSeconds * 1000.0;
448 quality.maxTimestampDeltaMs = std::max(quality.maxTimestampDeltaMs,
449 observation.timestampDeltaSeconds * 1000.0);
452 if (verticalErrors.empty())
454 errorMessage =
"No rectified correspondences are available for quality evaluation.";
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());
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();
478 if (!isValidQuality(quality))
480 errorMessage =
"Rectification quality metrics are invalid.";
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
493 objectPoints.clear();
494 leftImagePoints.clear();
495 rightImagePoints.clear();
496 objectPoints.reserve(observations.size());
497 leftImagePoints.reserve(observations.size());
498 rightImagePoints.reserve(observations.size());
500 for (
const StereoObservation& observation : observations)
502 objectPoints.push_back(observation.objectPoints);
503 leftImagePoints.push_back(observation.leftImagePoints);
504 rightImagePoints.push_back(observation.rightImagePoints);
506 if (objectPoints.empty())
508 errorMessage =
"No calibration points are available.";
bool calibrate(const std::vector< StereoObservation > &observations, const FisheyeCalibrationOptions &options, CalibrationResult &result, std::string &errorMessage) const
double rmsVerticalRectificationErrorPx
double medianVerticalRectificationErrorPx
std::size_t acceptedObservations
double maxVerticalRectificationErrorPx
double p95VerticalRectificationErrorPx
double meanVerticalRectificationErrorPx
RectificationResult rectification
CameraCalibrationResult leftCamera
StereoCalibrationResult stereo
CameraCalibrationResult rightCamera
CalibrationQualityMetrics quality
std::vector< cv::Mat > rotationVectors
std::vector< double > perViewRms
std::vector< cv::Mat > translationVectors
CalibrationMode calibrationMode