Skip to content

Commit 0371b81

Browse files
authored
Merge pull request #90 from petercorke/fix/camera-plot-unbound-call
2 parents 615ccd2 + 7142305 commit 0371b81

2 files changed

Lines changed: 40 additions & 9 deletions

File tree

‎src/machinevisiontoolbox/Camera.py‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,7 @@ def plot(
11521152
"""
11531153
Plot 3D camera icon in world view (base method)
11541154
1155-
:param pose: camera pose
1155+
:param pose: camera pose, required when called unbound (see below)
11561156
:type pose: :class:`~spatialmath.pose3d.SE3`
11571157
:param scale: scale factor, defaults to 1
11581158
:type scale: float
@@ -1170,15 +1170,28 @@ def plot(
11701170
:type projection: str, optional
11711171
:param ax: axes to draw in, defaults to current 3D axes
11721172
:type ax: :class:`~matplotlib.Axes3D`, optional
1173+
:raises ValueError: called unbound (eg. ``CentralCamera.plot(...)``)
1174+
without a ``pose``
11731175
:return: axes drawn into
11741176
:rtype: :class:`~matplotlib.Axes3D`
11751177
11761178
Plot a 3D icon representing the pose of a camera into a 3D Matplotlib
11771179
plot. Two icons are supported: the traditional frustum, and a
11781180
simplistic camera comprising a box and cylinder.
11791181
1180-
.. note:: If ``pose`` is not given it defaults to the pose of the
1181-
instance.
1182+
This method can be called two ways:
1183+
1184+
- **Bound**, ``camera.plot(...)`` -- draws an icon for ``camera``.
1185+
``pose`` defaults to the instance's own pose if not given.
1186+
- **Unbound**, ``CentralCamera.plot(pose=..., ...)`` -- called on
1187+
the class itself, with no camera instance involved (``self`` is
1188+
``None``). Useful for drawing icons at a set of poses that don't
1189+
correspond to any single camera instance -- e.g. plotting the
1190+
calibration frames returned by :meth:`images2C`. ``pose`` is
1191+
required in this form.
1192+
1193+
.. note:: If ``pose`` is not given, it defaults to the pose of
1194+
``self`` -- this only applies to the bound form.
11821195
"""
11831196

11841197
# if (fig is None) and (ax is None):
@@ -1195,6 +1208,11 @@ def plot(
11951208
ax = smb.axes_logic(ax, 3, projection=projection)
11961209

11971210
if pose is None:
1211+
if self is None:
1212+
raise ValueError(
1213+
"pose is required when plot() is called unbound, eg. "
1214+
"CentralCamera.plot(pose=...)"
1215+
)
11981216
pose = self.pose
11991217

12001218
# Matplotlib >=3.10 can raise while autoscaling ragged 3D line
@@ -1216,26 +1234,27 @@ def _add_collection3d_compat(collection, *args, **kwargs):
12161234
try:
12171235
# draw camera-like object:
12181236
if shape == "frustum":
1219-
self._plot_frustum(ax, pose, scale, alpha)
1237+
CameraBase._plot_frustum(ax, pose, scale, alpha)
12201238

12211239
elif shape == "camera":
1222-
self._plot_camera_icon(ax, pose, scale, solid, color, alpha, label)
1240+
CameraBase._plot_camera_icon(ax, pose, scale, solid, color, alpha, label)
12231241

12241242
if frame is True:
1225-
self.pose.plot(
1243+
pose.plot(
12261244
length=scale * 1.5,
12271245
style="line",
12281246
color=color,
12291247
flo=(0.07, 0, -0.01),
12301248
)
12311249
elif frame is not False:
1232-
self.pose.plot(**frame)
1250+
pose.plot(**frame)
12331251
finally:
12341252
ax.add_collection3d = _orig_add_collection3d
12351253

12361254
return ax
12371255

1238-
def _plot_frustum(self, ax: Axes, pose: SE3, scale: float, alpha: float) -> None:
1256+
@staticmethod
1257+
def _plot_frustum(ax: Axes, pose: SE3, scale: float, alpha: float) -> None:
12391258
"""Draw the ``shape="frustum"`` camera icon into ``ax``."""
12401259
# TODO make this kwargs or optional inputs
12411260
# side colors:
@@ -1279,8 +1298,8 @@ def _plot_frustum(self, ax: Axes, pose: SE3, scale: float, alpha: float) -> None
12791298
poly = Poly3DCollection(points, facecolors=["r", "g", "r", "y"], alpha=alpha)
12801299
ax.add_collection3d(poly)
12811300

1301+
@staticmethod
12821302
def _plot_camera_icon(
1283-
self,
12841303
ax: Axes,
12851304
pose: SE3,
12861305
scale: float,

‎tests/test_camera.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,18 @@ def test_plot_frame(self):
429429
self.assertEqual(len(ax.collections), 4)
430430
self.assertEqual(len(ax.lines), 3)
431431

432+
def test_plot_unbound_with_pose(self):
433+
# eg. CentralCamera.plot(pose=frame.pose, ...) as used to plot a
434+
# set of calibration frames with no single camera instance
435+
ax = CentralCamera.plot(pose=SE3.Tz(1), shape="camera")
436+
self.assertIsNotNone(ax)
437+
self.assertEqual(len(ax.collections), 3)
438+
self.assertEqual(len(ax.lines), 0)
439+
440+
def test_plot_unbound_without_pose_raises(self):
441+
with self.assertRaises(ValueError):
442+
CentralCamera.plot(shape="camera")
443+
432444

433445
# ----------------------------------------------------------------------- #
434446
if __name__ == "__main__":

0 commit comments

Comments
 (0)