Refactor camera intrinsic parameter handling.

Move logic from parser to compiler.

PiperOrigin-RevId: 847689906
Change-Id: I844746081973205e1b33a3845add842e8b8c3c07
This commit is contained in:
Yuval Tassa
2025-12-22 03:06:36 -08:00
committed by Copybara-Service
parent b77977a962
commit 158525269b
4 changed files with 54 additions and 63 deletions
+22 -30
View File
@@ -2976,6 +2976,28 @@ and the +Y axis points up. Thus the frame position and orientation are the key a
for most scenes and should likely be reduced. In either case, the horizontal field of view is computed automatically
given the window size and the vertical field of view.
.. _body-camera-sensorsize:
:at:`sensorsize`: :at-val:`real(2), "0 0"`
Size of the camera sensor in length units. When specified, all intrinsic attributes become active and :at:`fovy`
is ignored. The field-of-view is then computed automatically from the focal length and sensor size. This
also enables frustum visualization when the :ref:`mjVIS_CAMERA<mjtVisFlag>` visualization flag is active.
.. _body-camera-focal:
.. _body-camera-focalpixel:
:at:`focal` / :at:`focalpixel`: :at-val:`real(2), "0 0"`
Focal length in physical length units or in pixels, respectively. If both are specified, the pixel
value is used and the length value is ignored.
.. _body-camera-principal:
.. _body-camera-principalpixel:
:at:`principal` / :at:`principalpixel`: :at-val:`real(2), "0 0"`
Offset of the principal point (optical axis intersection with the image plane) from the image center. If both are
specified, the pixel value is used. At zero offset, the rendered image is centered on the camera's negative Z axis,
as in a standard pinhole camera model.
.. _body-camera-resolution:
:at:`resolution`: :at-val:`int(2), "1 1"`
@@ -2983,36 +3005,6 @@ and the +Y axis points up. Thus the frame position and orientation are the key a
dimensions are determined by the size of the rendering context. This attribute serves as a convenient
location to save the required resolution when creating a context.
.. _body-camera-focal:
:at:`focal`: :at-val:`real(2), "0 0"`
Focal length of the camera in length units. It is mutually exclusive with :ref:`fovy <body-camera-fovy>`.
See :ref:`CCamera` for details.
.. _body-camera-focalpixel:
:at:`focalpixel`: :at-val:`int(2), "0 0"`
Focal length of the camera in pixel units. If both :at:`focal` and :at:`focalpixel` are specified, the former is
ignored.
.. _body-camera-principal:
:at:`principal`: :at-val:`real(2), "0 0"`
Offset of the principal point of the camera with respect to the camera center in length units. It is mutually
exclusive with :ref:`fovy <body-camera-fovy>`.
.. _body-camera-principalpixel:
:at:`principalpixel`: :at-val:`real(2), "0 0"`
Offset of the principal point of the camera with respect to the camera center in pixel units. If both
:at:`principal` and :at:`principalpixel` are specified, the former is ignored.
.. _body-camera-sensorsize:
:at:`sensorsize`: :at-val:`real(2), "0 0"`
Size of the camera sensor in length units. It is mutually exclusive with :ref:`fovy <body-camera-fovy>`. If
specified, :ref:`resolution <body-camera-resolution>` and :ref:`focal <body-camera-focal>` are required.
.. _body-camera-ipd:
:at:`ipd`: :at-val:`real, "0.068"`
+20 -12
View File
@@ -4116,15 +4116,23 @@ void mjCCamera::Compile(void) {
name.c_str(), id, fovy);
}
// check that specs are not duplicated
if ((principal_length[0] && principal_pixel[0]) ||
(principal_length[1] && principal_pixel[1])) {
throw mjCError(this, "principal length duplicated in camera");
// check for advanced camera intrinsic parameters
bool has_intrinsic = focal_length[0] || focal_length[1] ||
focal_pixel[0] || focal_pixel[1] ||
principal_length[0] || principal_length[1] ||
principal_pixel[0] || principal_pixel[1];
bool has_sensorsize = sensor_size[0] > 0 && sensor_size[1] > 0;
// intrinsic params require sensorsize
if (has_intrinsic && !has_sensorsize) {
throw mjCError(this, "focal/principal require sensorsize in camera '%s' (id = %d)",
name.c_str(), id);
}
if ((focal_length[0] && focal_pixel[0]) ||
(focal_length[1] && focal_pixel[1])) {
throw mjCError(this, "focal length duplicated in camera");
// sensorsize requires resolution
if (has_sensorsize && (resolution[0] <= 0 || resolution[1] <= 0)) {
throw mjCError(this, "sensorsize requires positive resolution in camera '%s' (id = %d)",
name.c_str(), id);
}
// compute number of pixels per unit length
@@ -4134,11 +4142,11 @@ void mjCCamera::Compile(void) {
(float)resolution[1] / sensor_size[1],
};
// defaults are zero, so only one term in each sum is nonzero
intrinsic[0] = focal_pixel[0] / pixel_density[0] + focal_length[0];
intrinsic[1] = focal_pixel[1] / pixel_density[1] + focal_length[1];
intrinsic[2] = principal_pixel[0] / pixel_density[0] + principal_length[0];
intrinsic[3] = principal_pixel[1] / pixel_density[1] + principal_length[1];
// pixel values override length values when both are specified
intrinsic[0] = focal_pixel[0] ? focal_pixel[0] / pixel_density[0] : focal_length[0];
intrinsic[1] = focal_pixel[1] ? focal_pixel[1] / pixel_density[1] : focal_length[1];
intrinsic[2] = principal_pixel[0] ? principal_pixel[0] / pixel_density[0] : principal_length[0];
intrinsic[3] = principal_pixel[1] ? principal_pixel[1] / pixel_density[1] : principal_length[1];
// fovy with principal point at (0, 0)
fovy = std::atan2(sensor_size[1]/2, intrinsic[1]) * 360.0 / mjPI;
+9 -18
View File
@@ -1937,24 +1937,15 @@ void mjXReader::OneCamera(XMLElement* elem, mjsCamera* camera) {
camera->proj = (mjtProjection)n;
}
bool has_principal = ReadAttr(elem, "principalpixel", 2, camera->principal_pixel, text) ||
ReadAttr(elem, "principal", 2, camera->principal_length, text);
bool has_focal = ReadAttr(elem, "focalpixel", 2, camera->focal_pixel, text) ||
ReadAttr(elem, "focal", 2, camera->focal_length, text);
bool needs_sensorsize = has_principal || has_focal;
bool has_sensorsize = ReadAttr(elem, "sensorsize", 2, camera->sensor_size, text, needs_sensorsize);
bool has_fovy = ReadAttr(elem, "fovy", 1, &camera->fovy, text);
bool needs_resolution = has_focal || has_sensorsize;
ReadAttr(elem, "resolution", 2, camera->resolution, text, needs_resolution);
if (camera->resolution[0] < 0 || camera->resolution[1] < 0) {
throw mjXError(elem, "camera resolution cannot be negative");
}
if (has_fovy && has_sensorsize) {
throw mjXError(
elem,
"either 'fovy' or 'sensorsize' attribute can be specified, not both");
ReadAttr(elem, "principalpixel", 2, camera->principal_pixel, text);
ReadAttr(elem, "principal", 2, camera->principal_length, text);
ReadAttr(elem, "focalpixel", 2, camera->focal_pixel, text);
ReadAttr(elem, "focal", 2, camera->focal_length, text);
ReadAttr(elem, "resolution", 2, camera->resolution, text);
bool sensorsize = ReadAttr(elem, "sensorsize", 2, camera->sensor_size, text);
bool fovy = ReadAttr(elem, "fovy", 1, &camera->fovy, text);
if (fovy && sensorsize) {
throw mjXError(elem, "either 'fovy' or 'sensorsize' attribute can be specified, not both");
}
// read userdata
+3 -3
View File
@@ -2001,7 +2001,7 @@ TEST_F(XMLReaderTest, CameraPrincipalRequiresSensorsize) {
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("attribute missing: 'sensorsize'"));
EXPECT_THAT(error.data(), HasSubstr("focal/principal require sensorsize"));
EXPECT_THAT(error.data(), HasSubstr("line 6"));
}
@@ -2011,7 +2011,7 @@ TEST_F(XMLReaderTest, CameraSensorsizeRequiresResolution) {
<worldbody>
<body>
<geom size="1"/>
<camera sensorsize="1 1"/>
<camera sensorsize="1 1" resolution="0 0"/>
</body>
</worldbody>
</mujoco>
@@ -2019,7 +2019,7 @@ TEST_F(XMLReaderTest, CameraSensorsizeRequiresResolution) {
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m, IsNull());
EXPECT_THAT(error.data(), HasSubstr("attribute missing: 'resolution'"));
EXPECT_THAT(error.data(), HasSubstr("requires positive resolution"));
EXPECT_THAT(error.data(), HasSubstr("line 6"));
}