Visitar URL original
Matplotlib v3.9 compatibility by C-Achard · Pull Request #3394 · DeepLabCut/DeepLabCut · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9cc1d18
Add Matplotlib colormap compatibility shim
C-Achard Jul 13, 2026
9518b9d
Allow custom warning stacklevel in deprecated
C-Achard Jul 13, 2026
bd7fdbb
Use shared get_colormap helper everywhere
C-Achard Jul 13, 2026
4bf4921
Merge branch 'main' into cy/init-mpl-version-change
C-Achard Jul 16, 2026
a5d5221
Merge remote-tracking branch 'upstream/main' into cy/init-mpl-version…
deruyter92 Aug 9, 2026
d5945cc
Fix get_colormap_names() legacy fallback to call _legacy_get_colormap…
deruyter92 Aug 9, 2026
d77d4f8
update version for "deprecated since .." warning
deruyter92 Aug 9, 2026
5e739e2
Add name override to deprecated() for legacy shim warnings
deruyter92 Aug 9, 2026
e6cfac6
Rename matplotlib_future_mode.py → matplotlib_compat.py
deruyter92 Aug 9, 2026
d9288fc
add tests for matplotlib compat layer
deruyter92 Aug 9, 2026
350e209
Import matplotlib.cm explicitly in the compat layer
C-Achard Sep 4, 2026
54ac2ac
Prevent duplicate legacy colormap registration
C-Achard Sep 4, 2026
b1c97fb
Route all plt.get_cmap calls throught the compat layer
C-Achard Sep 4, 2026
c1cf140
Use compat helper to clear matplotlib artists
C-Achard Sep 4, 2026
0e4a691
Patch matplotlib's Axes logger by name
C-Achard Sep 4, 2026
d3472c3
Test the name and stacklevel options of @deprecated
C-Achard Sep 4, 2026
0be8837
Expand matplotlib compat test coverage
C-Achard Sep 4, 2026
fe9d488
Remove unused compat helpers
C-Achard Sep 4, 2026
304bcc6
Remove unecessary tests
C-Achard Sep 4, 2026
aa007f6
Rename matplotlib compat module to utils
C-Achard Sep 4, 2026
0584d53
Replace matplotlib_compat colormap helpers
C-Achard Sep 4, 2026
1c15225
Simplify Matplotlib axes logger helper
C-Achard Sep 4, 2026
8a0ea71
Fix Matplotlib artist iteration
C-Achard Sep 4, 2026
426943f
Merge branch 'dev' into cy/init-mpl-version-change
deruyter92 Sep 8, 2026
31380eb
Merge branch 'dev' into cy/init-mpl-version-change
deruyter92 Sep 8, 2026
de29151
Merge branch 'dev' into cy/init-mpl-version-change
deruyter92 Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions deeplabcut/core/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def deprecated(
replacement: str | None = None,
since: str | None = None,
removed_in: str | None = None,
stacklevel: int = 2,
name: str | None = None,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Mark a function as deprecated.

Expand All @@ -93,12 +95,16 @@ def deprecated(
``"deeplabcut.utils.auxfun_videos.list_videos_in_folder"``.
since: Version in which the function was deprecated.
removed_in: Version in which the function will be removed.
stacklevel: Stack level for the warning. Default is 2, which points to the
caller of the deprecated function. Increase this if you wrap calls in
an extra legacy wrapper function that you don't want to be blamed for the deprecation.
name: Name to report in the warning message. Defaults to ``__qualname__``.
"""

def decorator(fn: Callable[P, R]) -> Callable[P, R]:
info = DeprecationInfo(
kind="callable",
target=fn.__qualname__,
target=name or fn.__qualname__,
replacement=replacement,
since=since,
removed_in=removed_in,
Expand All @@ -107,7 +113,7 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]:

@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
warnings.warn(message, DLCDeprecationWarning, stacklevel=2)
warnings.warn(message, DLCDeprecationWarning, stacklevel=stacklevel)
return fn(*args, **kwargs)

wrapper.__doc__ = f"Deprecated. {message}\n\n" + (fn.__doc__ or "")
Expand Down
4 changes: 3 additions & 1 deletion deeplabcut/core/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import matplotlib.pyplot as plt
import numpy as np

from deeplabcut.utils.matplotlib_compat import get_colormap


def form_figure(nx, ny) -> tuple[plt.Figure, plt.Axes]:
"""Forms a figure on which to plot images."""
Expand Down Expand Up @@ -222,7 +224,7 @@ def _filename(map_name) -> str:
if paf_all_in_one:
inds = [elem[0] for elem in edge_list]
n_inds = len(inds)
cmap = plt.cm.get_cmap(paf_colormap, n_inds)
cmap = get_colormap(paf_colormap, n_inds)
colors = cmap(range(n_inds))
fig3, _ = visualize_paf(image, paf[:, :, inds], colors=colors)
fig3.savefig(output_folder / _filename("paf"))
Expand Down
7 changes: 4 additions & 3 deletions deeplabcut/gui/tracklet_toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from deeplabcut.refine_training_dataset.tracklets import TrackletManager
from deeplabcut.utils.auxfun_videos import VideoReader
from deeplabcut.utils.auxiliaryfunctions import attempt_to_make_folder
from deeplabcut.utils.matplotlib_compat import get_colormap, remove_artists


class DraggablePoint:
Expand Down Expand Up @@ -289,7 +290,7 @@ def reconnect(self):
class TrackletVisualizer:
def __init__(self, manager, videoname, trail_len=50):
self.manager = manager
self.cmap = plt.cm.get_cmap(manager.cfg["colormap"], len(set(manager.tracklet2id)))
self.cmap = get_colormap(manager.cfg["colormap"], len(set(manager.tracklet2id)))
self.videoname = videoname
self.video = VideoReader(videoname)
self.nframes = len(self.video)
Expand Down Expand Up @@ -744,8 +745,8 @@ def on_click(self, event):
self.clean_collections()

def clean_collections(self):
for coll in self.ax2.collections + self.ax3.collections + self.ax_slider.collections:
coll.remove()
for ax in (self.ax2, self.ax3, self.ax_slider):
remove_artists(ax, "collections")

def display_points(self, val):
data = self.manager.xy[:, val]
Expand Down
5 changes: 2 additions & 3 deletions deeplabcut/modelzoo/generalized_data_converter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
SingleDLC_config,
)
from deeplabcut.utils import auxiliaryfunctions
from deeplabcut.utils.matplotlib_compat import get_colormap


def threshold_kpts(config_path, h5path, threshold_mean=0.9, threshold_min=0.1):
Expand Down Expand Up @@ -257,9 +258,7 @@ def customized_colormap(config_path):
bodyparts = cfg["multianimalbodyparts"]
n_bodyparts = len(cfg["multianimalbodyparts"])

import matplotlib.pyplot as plt

cmap = plt.cm.get_cmap("rainbow", n_bodyparts)
cmap = get_colormap("rainbow", n_bodyparts)

colors = [cmap(i) for i in range(n_bodyparts)]

Expand Down
4 changes: 2 additions & 2 deletions deeplabcut/pose_estimation_3d/camera_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes._axes import _log as matplotlib_axes_logger

from deeplabcut.utils import auxiliaryfunctions, auxiliaryfunctions_3d
from deeplabcut.utils.matplotlib_compat import silence_axes_logger

matplotlib_axes_logger.setLevel("ERROR")
silence_axes_logger()


def calibrate_cameras(
Expand Down
8 changes: 4 additions & 4 deletions deeplabcut/pose_estimation_3d/plotting3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import pandas as pd
from matplotlib import gridspec
from matplotlib.animation import FFMpegWriter
from matplotlib.axes._axes import _log as matplotlib_axes_logger
from matplotlib.collections import LineCollection
from mpl_toolkits.mplot3d.art3d import Line3DCollection
from tqdm import tqdm
Expand All @@ -27,8 +26,9 @@
make_labeled_video,
)
from deeplabcut.utils.auxfun_videos import VideoReader
from deeplabcut.utils.matplotlib_compat import get_colormap, silence_axes_logger

matplotlib_axes_logger.setLevel("ERROR")
silence_axes_logger()


def set_up_grid(figsize, xlim, ylim, zlim, view):
Expand Down Expand Up @@ -234,11 +234,11 @@ def create_labeled_video_3d(
ind_links = tuple(zip(*links, strict=False))

if color_by == "bodypart":
color = plt.cm.get_cmap(cmap, len(bodyparts2plot))
color = get_colormap(cmap, len(bodyparts2plot))
colors_ = color(range(len(bodyparts2plot)))
colors = np.tile(colors_, (num_animals, 1))
elif color_by == "individual":
color = plt.cm.get_cmap(cmap, num_animals)
color = get_colormap(cmap, num_animals)
colors_ = color(range(num_animals))
colors = np.repeat(colors_, len(bodyparts2plot), axis=0)

Expand Down
3 changes: 2 additions & 1 deletion deeplabcut/pose_estimation_pytorch/apis/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from deeplabcut.core.engine import Engine
from deeplabcut.pose_estimation_pytorch.task import Task
from deeplabcut.utils import auxiliaryfunctions
from deeplabcut.utils.matplotlib_compat import get_colormap


def create_labeled_images(
Expand Down Expand Up @@ -79,7 +80,7 @@ def create_labeled_images(

color_by_individual = mode == "individual"
if isinstance(cmap, str):
cmap = plt.cm.get_cmap(cmap)
cmap = get_colormap(cmap)

for image_path, image_predictions in predictions.items():
# Load frame
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
from abc import ABC, abstractmethod

import cv2
import matplotlib.pyplot as plt
import numpy as np

from deeplabcut.pose_estimation_pytorch.data.utils import out_of_bounds_keypoints
from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg
from deeplabcut.utils.matplotlib_compat import get_colormap

KEYPOINT_ENCODERS = Registry("kpt_encoders", build_func=build_from_cfg)

Expand Down Expand Up @@ -234,7 +234,7 @@ def _get_condition_matrix_optim(zero_matrix, kpts):
return condition

def get_colors_from_cmap(self, cmap_name, num_colors):
cmap = plt.get_cmap(cmap_name)
cmap = get_colormap(cmap_name)
colors_float = [cmap(i) for i in np.linspace(0, 256, num_colors, dtype=int)]
colors = [(int(r * 255), int(g * 255), int(b * 255)) for r, g, b, _ in colors_float]
return colors
3 changes: 2 additions & 1 deletion deeplabcut/pose_estimation_tensorflow/visualizemaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
visualize_paf,
visualize_scoremaps,
)
from deeplabcut.utils.matplotlib_compat import get_colormap


def extract_maps(
Expand Down Expand Up @@ -400,7 +401,7 @@ def extract_save_all_maps(
else:
inds = [elem[0] for elem in list_of_inds]
n_inds = len(inds)
cmap = plt.cm.get_cmap(cfg["colormap"], n_inds)
cmap = get_colormap(cfg["colormap"], n_inds)
colors = cmap(range(n_inds))
fig3, _ = visualize_paf(image, paf[:, :, inds], colors=colors)
temp = dest_path.format(
Expand Down
7 changes: 4 additions & 3 deletions deeplabcut/refine_training_dataset/stitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
)
from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions
from deeplabcut.utils.auxfun_videos import VideoWriter, collect_video_paths
from deeplabcut.utils.matplotlib_compat import get_colormap


class Tracklet:
Expand Down Expand Up @@ -901,7 +902,7 @@ def plot_paths(self, colormap="Set2"):
spine.set_visible(False)
for path in self.paths:
length = len(path)
colors = plt.get_cmap(colormap, length)(range(length))
colors = get_colormap(colormap, length)(range(length))
for tracklet, color in zip(path, colors, strict=False):
tracklet.plot(color=color, ax=ax)

Expand All @@ -914,7 +915,7 @@ def plot_tracks(self, colormap="viridis"):
for loc, spine in ax.spines.items():
if loc != "bottom":
spine.set_visible(False)
colors = plt.get_cmap(colormap, self.n_tracks)(range(self.n_tracks))
colors = get_colormap(colormap, self.n_tracks)(range(self.n_tracks))
for track, color in zip(self.tracks, colors, strict=False):
track.plot(color=color, ax=ax)

Expand All @@ -926,7 +927,7 @@ def plot_tracklets(self, colormap="Paired"):
spine.set_visible(False)
axes[1].axis("off")

cmap = plt.get_cmap(colormap)
cmap = get_colormap(colormap)
colors = cycle(cmap.colors)
line2tracklet = dict()
tracklet2lines = dict()
Expand Down
4 changes: 3 additions & 1 deletion deeplabcut/utils/auxiliaryfunctions_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import numpy as np
import pandas as pd

from deeplabcut.utils.matplotlib_compat import get_colormap


def Foldernames3Dproject(cfg_3d):
"""Definitions of subfolders in 3D projects."""
Expand Down Expand Up @@ -78,7 +80,7 @@ def compute_triangulation_calibration_images(
P1 = stereo_matrix["P1"]
P2 = stereo_matrix["P2"]
cmap = cfg_3d["colormap"]
colormap = plt.get_cmap(cmap)
colormap = get_colormap(cmap)
markerSize = cfg_3d["dotsize"]
markerType = cfg_3d["markerType"]

Expand Down
5 changes: 3 additions & 2 deletions deeplabcut/utils/make_labeled_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from deeplabcut.pose_estimation_pytorch.config import PoseConfig
from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions, visualization
from deeplabcut.utils.auxfun_videos import VideoWriter, collect_video_paths
from deeplabcut.utils.matplotlib_compat import get_colormap
from deeplabcut.utils.video_processor import (
VideoProcessorCV as vp,
) # used to CreateVideo
Expand Down Expand Up @@ -1021,14 +1022,14 @@ def create_video_with_keypoints_only(

if color_by == "bodypart":
map_ = bodyparts.map(dict(zip(bodypart_names, range(n_bodyparts), strict=False)))
cmap = plt.get_cmap(colormap, n_bodyparts)
cmap = get_colormap(colormap, n_bodyparts)
elif color_by == "individual":
try:
individuals = df.columns.get_level_values("individuals")[::3]
individual_names = individuals.unique().to_list()
n_individuals = len(individual_names)
map_ = individuals.map(dict(zip(individual_names, range(n_individuals), strict=False)))
cmap = plt.get_cmap(colormap, n_individuals)
cmap = get_colormap(colormap, n_individuals)
except KeyError as e:
raise Exception("Coloring by individuals is only valid for multi-animal data") from e
else:
Expand Down
67 changes: 67 additions & 0 deletions deeplabcut/utils/matplotlib_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Small compatibility layer for Matplotlib API migrations.

A single choke point for the Matplotlib APIs DeepLabCut calls from many places,
so that adapting to an upstream change is one edit rather than one per call
site. Each helper wraps an API that Matplotlib has already moved, or
whose current spelling is deprecated.
"""

from __future__ import annotations

import logging

import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from matplotlib.colors import Colormap

# Deliberately not every sublist an ``Axes`` exposes
# ``texts``, ``tables`` and ``containers`` are omitted
# because no DeepLabCut caller used them, and these five reproduce the
# behaviour of the removal loops this helper replaced
ARTIST_KINDS = ("lines", "collections", "artists", "patches", "images")

# Matplotlib builds this logger with ``logging.getLogger(__name__)`` in
# ``matplotlib/axes/_axes.py``, so the name is enough to reach it.
AXES_LOGGER_NAME = "matplotlib.axes._axes"


def get_colormap(
name: str | Colormap | None = None,
lut: int | None = None,
) -> Colormap:
"""Return a colormap, optionally resampled to ``lut`` colors."""
return plt.get_cmap(name, lut)


def remove_artists(ax: Axes, *kinds: str) -> None:
"""Remove artists from an ``Axes``, one children sublist at a time.

Matplotlib 3.7 made the ``Axes`` children sublists (``ax.lines``,
``ax.collections``, ...) immutable ``ArtistList`` views and deprecated
concatenating them with ``+``. Iterating a concatenation also happened to
be the only thing making the removal loops safe, since ``+`` returned a
plain list and therefore a snapshot; iterating a live ``ArtistList`` while
calling ``Artist.remove()`` skips artists. This helper snapshots each
sublist explicitly instead.

Args:
ax: The axes to remove artists from.
*kinds: Names of the children sublists to clear, e.g. ``"collections"``.
Defaults to every kind in ``ARTIST_KINDS``.
"""
for kind in kinds or ARTIST_KINDS:
for artist in list(getattr(ax, kind)):
artist.remove()


def silence_axes_logger(level: int | str = "ERROR") -> None:
"""Raise the log level of Matplotlib's ``Axes`` logger.

Suppresses the chatty per-artist messages Matplotlib emits while drawing,
most notably the "Unable to determine Axes to steal space for Colorbar"
and invalid-color warnings raised during 3D plotting.

Args:
level: Any level accepted by ``logging.Logger.setLevel``.
"""
logging.getLogger(AXES_LOGGER_NAME).setLevel(level)
3 changes: 2 additions & 1 deletion deeplabcut/utils/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from deeplabcut.core.deprecation import renamed_parameter
from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions, visualization
from deeplabcut.utils.auxfun_videos import collect_video_paths
from deeplabcut.utils.matplotlib_compat import get_colormap


def Histogram(vector, color, bins, ax=None, linewidth=1.0):
Expand Down Expand Up @@ -139,7 +140,7 @@ def PlottingResults(
Histogram(temp_y, colors(bpindex), bins, ax4, linewidth=linewidth)

sm = plt.cm.ScalarMappable(
cmap=plt.get_cmap(cfg["colormap"]),
cmap=get_colormap(cfg["colormap"]),
norm=plt.Normalize(vmin=0, vmax=len(bodyparts2plot) - 1),
)
sm._A = []
Expand Down
6 changes: 3 additions & 3 deletions deeplabcut/utils/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from tqdm import trange

from deeplabcut.utils import auxfun_videos, auxiliaryfunctions
from deeplabcut.utils.matplotlib_compat import get_colormap, remove_artists


def get_cmap(n: int, name: str = "hsv") -> Colormap:
Expand All @@ -46,7 +47,7 @@ def get_cmap(n: int, name: str = "hsv") -> Colormap:
A function that maps each index in 0, 1, ..., n-1 to a distinct
RGB color; the keyword argument name must be a standard mpl colormap name.
"""
return plt.cm.get_cmap(name, n)
return get_colormap(name, n)


def make_labeled_image(
Expand Down Expand Up @@ -285,8 +286,7 @@ def create_minimal_figure(dpi=100):


def erase_artists(ax):
for artist in ax.lines + ax.collections + ax.artists + ax.patches + ax.images:
artist.remove()
remove_artists(ax)
ax.figure.canvas.draw_idle()


Expand Down
Loading
Loading