iCub-main
Loading...
Searching...
No Matches
FisheyeCalibTool.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Istituto Italiano di Tecnologia (IIT)
2// All rights reserved.
3//
4// This software may be modified and distributed under the terms of the
5// BSD-3-Clause license. See the accompanying LICENSE file for details.
6
7#include <opencv2/calib3d.hpp>
8#include <opencv2/imgproc.hpp>
9
10#include <algorithm>
11#include <cmath>
12
13#include <yarp/os/Bottle.h>
14#include <yarp/os/LogStream.h>
15#include <yarp/os/Value.h>
16
18
19using yarp::os::Bottle;
20using yarp::os::Searchable;
21using yarp::sig::ImageOf;
22using yarp::sig::PixelRgb;
23
25 : previousImageSize(-1, -1)
26 , groupName()
27 , needInitialization(true)
28 , balance(0.0)
29 , fovScale(1.0)
30 , drawEpipolars(false)
31 , epipolarLineStep(48)
32{
33}
34
35bool FisheyeCalibTool::open(Searchable& config)
36{
37 return configure(config);
38}
39
41{
42 leftMap1.release();
43 leftMap2.release();
44 rightMap1.release();
45 rightMap2.release();
46 return true;
47}
48
49bool FisheyeCalibTool::loadCameraCalibration(const Searchable& group,
50 CameraCalibration& camera,
51 const std::string& groupName)
52{
53 const char* required[] = {"w", "h", "fx", "fy", "cx", "cy", "k1", "k2", "k3", "k4"};
54 for (const char* key : required)
55 {
56 if (!group.check(key))
57 {
58 yError() << "Stereo rectification: missing" << key << "in" << groupName;
59 return false;
60 }
61 }
62
63 camera.calibrationSize = cv::Size(group.find("w").asInt32(), group.find("h").asInt32());
64 if (camera.calibrationSize.width <= 0 || camera.calibrationSize.height <= 0)
65 {
66 yError() << "Stereo rectification: invalid calibration size in" << groupName;
67 return false;
68 }
69
70 camera.K = cv::Mat::eye(3, 3, CV_64F);
71 camera.K.at<double>(0, 0) = group.find("fx").asFloat64();
72 camera.K.at<double>(1, 1) = group.find("fy").asFloat64();
73 camera.K.at<double>(0, 2) = group.find("cx").asFloat64();
74 camera.K.at<double>(1, 2) = group.find("cy").asFloat64();
75
76 camera.D = cv::Mat::zeros(4, 1, CV_64F);
77 camera.D.at<double>(0, 0) = group.find("k1").asFloat64();
78 camera.D.at<double>(1, 0) = group.find("k2").asFloat64();
79 camera.D.at<double>(2, 0) = group.find("k3").asFloat64();
80 camera.D.at<double>(3, 0) = group.find("k4").asFloat64();
81 return true;
82}
83
84bool FisheyeCalibTool::loadStereoCalibration(const Searchable& config)
85{
86 if (!config.check("STEREO_DISPARITY"))
87 {
88 yError() << "Stereo rectification: [STEREO_DISPARITY] group not found";
89 return false;
90 }
91
92 const Bottle& stereo = config.findGroup("STEREO_DISPARITY");
93 if (!stereo.check("HN"))
94 {
95 yError() << "Stereo rectification: HN is missing from [STEREO_DISPARITY]";
96 return false;
97 }
98
99 const Bottle* homogeneous = stereo.find("HN").asList();
100 if (homogeneous == nullptr || homogeneous->size() != 16)
101 {
102 yError() << "Stereo rectification: HN must contain a 4x4 homogeneous transform";
103 return false;
104 }
105
106 rotation = cv::Mat::eye(3, 3, CV_64F);
107 translation = cv::Mat::zeros(3, 1, CV_64F);
108 for (int row = 0; row < 3; ++row)
109 {
110 for (int col = 0; col < 3; ++col)
111 {
112 rotation.at<double>(row, col) = homogeneous->get(row * 4 + col).asFloat64();
113 }
114 translation.at<double>(row, 0) = homogeneous->get(row * 4 + 3).asFloat64();
115 }
116
117 if (cv::norm(translation) <= 1e-12)
118 {
119 yError() << "Stereo rectification: translation from HN is zero";
120 return false;
121 }
122 return true;
123}
124
125bool FisheyeCalibTool::configure(Searchable& config)
126{
127 groupName = config.check("group", yarp::os::Value("")).asString();
128 if (groupName != "CAMERA_CALIBRATION_LEFT" && groupName != "CAMERA_CALIBRATION_RIGHT")
129 {
130 yError() << "Stereo rectification requires --group CAMERA_CALIBRATION_LEFT or CAMERA_CALIBRATION_RIGHT";
131 return false;
132 }
133 if (!config.check("CAMERA_CALIBRATION_LEFT") || !config.check("CAMERA_CALIBRATION_RIGHT"))
134 {
135 yError() << "Stereo rectification requires both camera calibration groups";
136 return false;
137 }
138
139 const Bottle& leftGroup = config.findGroup("CAMERA_CALIBRATION_LEFT");
140 const Bottle& rightGroup = config.findGroup("CAMERA_CALIBRATION_RIGHT");
141 if (!loadCameraCalibration(leftGroup, leftCamera, "CAMERA_CALIBRATION_LEFT")
142 || !loadCameraCalibration(rightGroup, rightCamera, "CAMERA_CALIBRATION_RIGHT")
143 || !loadStereoCalibration(config))
144 {
145 return false;
146 }
147
148 const Bottle& selectedGroup = (groupName == "CAMERA_CALIBRATION_LEFT") ? leftGroup : rightGroup;
149 const yarp::os::Value defaultBalance = config.check("rectifyAlpha", yarp::os::Value(0.0));
150 balance = config.check("balance", selectedGroup.check("balance", defaultBalance)).asFloat64();
151 balance = std::max(0.0, std::min(1.0, balance));
152 fovScale = config.check("fovScale", selectedGroup.check("fovScale", yarp::os::Value(1.0))).asFloat64();
153 if (fovScale <= 0.0)
154 {
155 yError() << "Fisheye rectification: fovScale must be greater than zero";
156 return false;
157 }
158 drawEpipolars = config.check("drawEpipolars", yarp::os::Value(false)).asBool();
159 epipolarLineStep = config.check("epipolarLineStep", yarp::os::Value(48)).asInt32();
160 epipolarLineStep = epipolarLineStep > 0 ? epipolarLineStep : 1;
161 previousImageSize = cv::Size(-1, -1);
162 needInitialization = true;
163 yDebug() << "FisheyeCalibTool configured with balance:" << balance << "fovScale:" << fovScale
164 << "drawEpipolars:" << drawEpipolars << "epipolarLineStep:" << epipolarLineStep;
165 return true;
166}
167
168cv::Mat FisheyeCalibTool::scaledCameraMatrix(const CameraCalibration& camera,
169 const cv::Size& imageSize) const
170{
171 const double scaleX = static_cast<double>(imageSize.width) / camera.calibrationSize.width;
172 const double scaleY = static_cast<double>(imageSize.height) / camera.calibrationSize.height;
173 cv::Mat scaled = camera.K.clone();
174 scaled.at<double>(0, 0) *= scaleX;
175 scaled.at<double>(0, 2) *= scaleX;
176 scaled.at<double>(1, 1) *= scaleY;
177 scaled.at<double>(1, 2) *= scaleY;
178 return scaled;
179}
180
181bool FisheyeCalibTool::initializeMaps(const cv::Size& imageSize)
182{
183 const cv::Mat leftK = scaledCameraMatrix(leftCamera, imageSize);
184 const cv::Mat rightK = scaledCameraMatrix(rightCamera, imageSize);
185 cv::Mat leftR;
186 cv::Mat rightR;
187 cv::Mat leftP;
188 cv::Mat rightP;
189 cv::Mat disparityToDepth;
190 cv::fisheye::stereoRectify(leftK, leftCamera.D, rightK, rightCamera.D, imageSize,
191 rotation, translation, leftR, rightR, leftP, rightP,
192 disparityToDepth, cv::fisheye::CALIB_ZERO_DISPARITY,
193 imageSize, balance, fovScale);
194
195 if (leftR.empty() || rightR.empty() || leftP.empty() || rightP.empty())
196 {
197 yError() << "Fisheye rectification: stereoRectify returned empty matrices";
198 return false;
199 }
200
201 const double leftFocal = leftP.at<double>(0, 0);
202 const double rightFocal = rightP.at<double>(0, 0);
203 if (!std::isfinite(leftFocal) || !std::isfinite(rightFocal)
204 || leftFocal < 1.0 || rightFocal < 1.0)
205 {
206 yWarning() << "Fisheye rectification: stereoRectify returned an invalid focal length"
207 << leftFocal << rightFocal
208 << "- using a projection matrix derived from the original intrinsics";
209 cv::Mat commonProjection = cv::Mat::eye(3, 3, CV_64F);
210 commonProjection.at<double>(0, 0) = 0.5 * (leftK.at<double>(0, 0) + rightK.at<double>(0, 0));
211 commonProjection.at<double>(1, 1) = 0.5 * (leftK.at<double>(1, 1) + rightK.at<double>(1, 1));
212 commonProjection.at<double>(0, 2) = 0.5 * (leftK.at<double>(0, 2) + rightK.at<double>(0, 2));
213 commonProjection.at<double>(1, 2) = 0.5 * (leftK.at<double>(1, 2) + rightK.at<double>(1, 2));
214 leftP = commonProjection;
215 rightP = commonProjection;
216 }
217
218 if(groupName == "CAMERA_CALIBRATION_LEFT")
219 {
220 cv::fisheye::initUndistortRectifyMap(leftK, leftCamera.D, leftR, leftP,
221 imageSize, CV_16SC2, leftMap1, leftMap2);
222 }
223 else if(groupName == "CAMERA_CALIBRATION_RIGHT")
224 {
225 cv::fisheye::initUndistortRectifyMap(rightK, rightCamera.D, rightR, rightP,
226 imageSize, CV_16SC2, rightMap1, rightMap2);
227 }
228
229 previousImageSize = imageSize;
230 needInitialization = false;
231 return (!leftMap1.empty() && !leftMap2.empty()) || (!rightMap1.empty() && !rightMap2.empty());
232}
233
234void FisheyeCalibTool::drawEpipolarLines(cv::Mat& image) const
235{
236 const cv::Scalar lineColor(0, 255, 0);
237 for (int y = 0; y < image.rows; y += epipolarLineStep)
238 {
239 cv::line(image, cv::Point(0, y), cv::Point(image.cols - 1, y), lineColor, 1, cv::LINE_AA);
240 }
241}
242
243void FisheyeCalibTool::apply(const ImageOf<PixelRgb>& in, ImageOf<PixelRgb>& out)
244{
245 const cv::Size imageSize(in.width(), in.height());
246 if (needInitialization || imageSize != previousImageSize)
247 {
248 if (!initializeMaps(imageSize))
249 {
250 yError() << "Fisheye calibration: unable to initialize maps";
251 out = in;
252 return;
253 }
254 }
255
256 out.resize(in.width(), in.height());
257 cv::Mat input(in.height(), in.width(), CV_8UC3, in.getRawImage(), in.getRowSize());
258 cv::Mat output(out.height(), out.width(), CV_8UC3, out.getRawImage(), out.getRowSize());
259 const cv::Mat& activeMap1 = (groupName == "CAMERA_CALIBRATION_LEFT") ? leftMap1 : rightMap1;
260 const cv::Mat& activeMap2 = (groupName == "CAMERA_CALIBRATION_LEFT") ? leftMap2 : rightMap2;
261 cv::remap(input, output, activeMap1, activeMap2, cv::INTER_LINEAR, cv::BORDER_CONSTANT);
262 if (drawEpipolars)
263 {
264 drawEpipolarLines(output);
265 }
266}
267
bool configure(yarp::os::Searchable &config) override
bool open(yarp::os::Searchable &config) override
bool close() override
void apply(const yarp::sig::ImageOf< yarp::sig::PixelRgb > &in, yarp::sig::ImageOf< yarp::sig::PixelRgb > &out) override
out
Definition sine.m:8