Robotics, Vision & Control 3e: for Python (Springer, 2023) was finished in 2022. Its prime dependencies — RTB, MVTB, bdsim, spatialmath-python — have all evolved since. Where a notebook's code has to change to keep working, that's a divergence from what's printed on the page, so it's recorded here.
Scope: only entries for changes a reader actually has to make to get a
notebook cell running as intended. Bugs that were fixed upstream (in RTB,
MVTB, bdsim, or spatialmath-python) with no notebook-side change
don't belong here — the printed book code still matches the notebook in
those cases, once the dependency is updated. Keep this list lean; it's not
a running log of every toolbox bug found while testing, only the ones that
actually changed a line the reader would see. (For the fuller
investigation log — including issues that turned out not to need a
notebook change — see claude-notes/notebook-testing-issues.md.)
One ## entry per changed cell/line group, in reading order. Section
numbers come from the nearest preceding section-heading markdown cell in
the notebook itself (e.g. # 2.3.1.1 3D Rotation Matrix).
## §<section number> — <short description of what changed and why>
**Notebook:** `chapN.ipynb`
**Reason:** <one line — which dependency changed and how, or what broke
and why>
**As printed:**
```python
<code exactly as it appears in the book>
```
**Current toolbox syntax:**
```python
<code needed today>
```Notebook: chap7.ipynb
Reason: RTB deprecated ET.eta since 1.4.0 in favour of .param.
As printed:
e[1].eta
e[1].A()Current toolbox syntax:
e[1].param
e[1].A()Notebook: chap7.ipynb
Reason: RTB deprecated and removed models.list(); the replacement
is models.catalog(), and the type-filter kwarg was renamed from type
to mtype (type shadows the Python builtin, which was the reason for
the rename).
As printed:
models.list(type="ETS")
models.list(type="URDF")
models.list(type="DH")Current toolbox syntax:
models.catalog(mtype="ETS")
models.catalog(mtype="URDF")
models.catalog(mtype="DH")§10.4.2, §11.1, §11.5.1.2, §12.1.1.3, §12.1.4, §12.3.2, §12.4, §14.3.1, §14.4.2.4, §14.4.2.8 — Image.image renamed to Image.array
Notebooks: chap10.ipynb, chap11.ipynb, chap12.ipynb, chap14.ipynb
Reason: MVTB 2.0.0 renamed the Image class's raw-pixel-data property
from .image to .array. Same one-line change everywhere it appears —
listed once here rather than per occurrence (roughly 15 individual cells
across these four notebooks).
As printed:
s = shadow_invariant(im.image, 0.7)Current toolbox syntax:
s = shadow_invariant(im.array, 0.7)Notebook: chap13.ipynb
Reason: Same MVTB 2.0.0 rename as .image→.array above, but from
the older .A alias rather than .image.
As printed:
facecolors=spherical.colorize().A, cstride=1, rstride=1Current toolbox syntax:
facecolors=spherical.colorize().array, cstride=1, rstride=1Notebook: chap13.ipynb
Reason: Not a toolbox API change -- a transcription error. rho=1.4e-6
is in metres, so f must be too; f=3045 (3045 metres) makes fx = f/rho
absurdly large (~2.18e9 pixels), which makes every cv2.solvePnP call
inside scene.fiducial() silently fail. The figure-generation script that
produced the book's own printed figure (figures/code/chapter13/ fig13_31.py) uses the correct value, f=4.25e-3.
As printed:
camera = CentralCamera(f=3045, imagesize=scene.shape,
pp=(2016, 1512), rho=1.4e-6);Current toolbox syntax:
camera = CentralCamera(f=4.25e-3, imagesize=scene.shape,
pp=(2016, 1512), rho=1.4e-6);Notebook: chap13.ipynb
Reason: Not a toolbox API change -- a plain (non-raw) Python string
containing \p and \t isn't a recognized escape sequence, so this raises
SyntaxWarning: invalid escape sequence '\p' (and would for \t too, but
\t happens to be a legitimate escape -- tab -- so only \phi warns; both
should be raw strings for correctness).
As printed:
spherical.disp(axes=("$\phi$", "$\theta$"));Current toolbox syntax:
spherical.disp(axes=(r"$\phi$", r"$\theta$"));Notebook: chap14.ipynb
Reason: MVTB's Image class moved to an immutability model (commit
9afa287a7, "Image immutability"): paste() used to modify the receiver
in place (when called with its old default copy=False) and this is what
the book's own figure-generation script relied on
(figures/code/chapter14/fig14_49.py, which predates this change). It now
always returns a new Image and leaves the original untouched, matching
every other Image method's current functional style -- the copy=
parameter is retained on the signature but ignored. Calls that don't
capture the return value are now silent no-ops: composite never
accumulates the pasted tiles, so every downstream cell in this section
(SIFT on an all-zero canvas, homography estimation, the second paste())
fails or produces nonsense.
As printed:
composite.paste(images[0], (0, 0));
composite.disp();
...
composite.paste(tile, topleft, method="blend");
composite.disp();Current toolbox syntax:
composite = composite.paste(images[0], (0, 0));
composite.disp();
...
composite = composite.paste(tile, topleft, method="blend");
composite.disp();Notebook: chap14.ipynb
Reason: PointCloud.Read() already prepends the mvtbdata package's
data subfolder internally, so the documented convention is a bare
filename. The book's own data/bunny.ply form (also used in the printed
figure-generation scripts) double-joined to a nonexistent
data/data/bunny.ply, even though bunny.ply is genuinely present in the
package. Fixed upstream in MVTB (accepts both forms now,
fix/pointcloud-read-data-prefix), but the bare form is the documented
one, so the notebook uses it directly rather than relying on the
backward-compat path.
As printed:
bunny_pcd = PointCloud.Read('data/bunny.ply')Current toolbox syntax:
bunny_pcd = PointCloud.Read('bunny.ply')Notebook: chap11.ipynb
Reason: A real CDF is normalized by definition, so MVTB simplified the
name -- 'cdf' is what 'ncdf' (normalized CDF) always meant. 'ncdf'
remains accepted as a deprecated alias.
As printed:
h.plot("ncdf", color="blue")Current toolbox syntax:
h.plot("cdf", color="blue")Notebook: chap12.ipynb
Reason: torchvision deprecated the boolean pretrained= argument on
model constructors in 0.13, in favour of an explicit weights= enum
naming the exact pretrained-weights set. Passing weights=<enum>.DEFAULT
would track torchvision's current recommendation, but that can change
between torchvision releases; using the specific enum member that
pretrained=True used to select keeps the notebook's results reproducible.
As printed:
model = tv.models.segmentation.fcn_resnet50(pretrained=True).eval();model = tv.models.detection.fasterrcnn_resnet50_fpn(pretrained=True).eval();Current toolbox syntax:
model = tv.models.segmentation.fcn_resnet50(
weights=tv.models.segmentation.FCN_ResNet50_Weights.COCO_WITH_VOC_LABELS_V1
).eval();model = tv.models.detection.fasterrcnn_resnet50_fpn(
weights=tv.models.detection.FasterRCNN_ResNet50_FPN_Weights.COCO_V1
).eval();Notebook: chap12.ipynb
Reason: NumPy 2.0 made np.linalg.eig() always return complex-dtype
arrays, even when every eigenvalue's imaginary part is exactly zero
(NumPy 1.x returned real dtype in that case). J here is a real
symmetric matrix (built from image moments), so its eigenvalues are
always real -- np.linalg.eigh() is both the numerically correct choice
for a symmetric matrix and sidesteps this NumPy 2.0 change, since it
guarantees real output.
As printed:
lmbda, x = np.linalg.eig(J)Current toolbox syntax:
lmbda, x = np.linalg.eigh(J)Notebook: app.ipynb
Reason: Same NumPy 2.0 change as §12.1.3.3: np.linalg.eig() now
always returns complex-dtype arrays, even when every eigenvalue's
imaginary part is exactly zero (NumPy 1.x returned real dtype in that
case). E here is a real symmetric matrix (the ellipse's defining
matrix), so its eigenvalues are always real -- np.linalg.eigh() is
both the numerically correct choice for a symmetric matrix and
sidesteps this NumPy 2.0 change. The complex dtype leaking downstream
into v/r is what broke the following cells' plot_arrow() (a bare
TypeError: ufunc 'hypot' not supported for the input types) and
np.arctan2() calls.
As printed:
e, v = np.linalg.eig(E)Current toolbox syntax:
e, v = np.linalg.eigh(E)Notebook: app.ipynb
Reason: pgraph (a dependency of RTB's BundleAdjust/pose-graph code)
renamed this method; .adjacent() no longer exists. Both .neighbours()
and .neighbors() are defined (not aliases of each other, but identical
in behaviour) -- used the British spelling for consistency with the rest
of the book's voice.
As printed:
g[1].adjacent()Current toolbox syntax:
g[1].neighbours()Notebook: chap11.ipynb
Reason: MVTB 2.0.0 renamed this method. Same change at 4 call sites in this section.
As printed:
mx = mona.rank(rank=0, h=2)Current toolbox syntax:
mx = mona.rankfilter(rank=0, h=2)Notebooks: chap11.ipynb, chap12.ipynb, chap13.ipynb, chap14.ipynb
Reason: MVTB 2.0.0 renamed this class.
As printed:
images = ImageCollection("seq/*.png")Current toolbox syntax:
images = FileCollection("seq/*.png")Notebooks: chap11.ipynb, chap14.ipynb
Reason: MVTB 2.0.0 renamed this class.
As printed:
images = ZipArchive("bridge-l.zip", "*.pgm")Current toolbox syntax:
images = FileArchive("bridge-l.zip", "*.pgm")Notebook: chap11.ipynb
Reason: MVTB 2.0.0 moved camera frame-grabbing onto Python's iterator protocol.
As printed:
camera = VideoCamera(0)
image = camera.grab()
camera.release()
image.disp()Current toolbox syntax:
camera = VideoCamera(0)
image = next(camera)
camera.release()
image.disp()Notebook: chap11.ipynb
Reason: Same change as VideoCamera.grab() above, applied to WebCam.
As printed:
porjus= WebCam("http://uk.jokkmokk.jp/photo/nr4/latest.jpg");
porjus.grab().disp();Current toolbox syntax:
porjus= WebCam("http://uk.jokkmokk.jp/photo/nr4/latest.jpg");
next(porjus).disp();Notebook: chap11.ipynb
Reason: MVTB 2.0.0 split what used to be a single callable stats()
into two: .stats, a dict-valued property (doesn't print), and
.printstats(), a method that prints the same summary the book's
stats() call used to. Same change at 4 call sites.
As printed:
street.stats()Current toolbox syntax:
street.printstats()Notebooks: chap11.ipynb, chap14.ipynb
Reason: MVTB 2.0.0 changed Image.Zeros()/Image.Constant()/etc. to
take image dimensions as a keyword-only size= tuple rather than
positional width/height arguments.
As printed:
canvas = Image.Zeros(1000, 1000, dtype="uint8")
K = Image.Constant(21, 21, value=1/21**2)Current toolbox syntax:
canvas = Image.Zeros(size=(1000, 1000), dtype="uint8")
K = Image.Constant(value=1/21**2, size=(21, 21))Notebook: chap7.ipynb
Reason: RTB's June 2026 xacro/URDF rework removed URDF_read() as a
static method on Robot/ERobot entirely (no deprecation shim -- this
was an architectural change, not a rename) and replaced it with a
module-level function in roboticstoolbox.models.URDF.URDFRobot. The
loading mechanism changed too: RTB no longer bundles raw xacro package
trees like ur_description/ directly, so a full relative xacro path no
longer resolves. Bare robot names ("ur5") now resolve via the
robot_descriptions package instead, matching how the packaged
models.URDF.UR5() class itself loads its data.
As printed:
urdf, *_ = ERobot.URDF_read("ur_description/urdf/ur5_joint_limited_robot.urdf.xacro")
urdfCurrent toolbox syntax:
from roboticstoolbox.models.URDF.URDFRobot import URDF_read
urdf, *_ = URDF_read("ur5")
urdf