Matplotlib绘图中插入可鼠标拖拽的png/jpg/svg图像的实现方法
解决方案
该需求完全可以实现,你原有的拖拽事件处理逻辑90%以上可以直接复用,仅需要将原有的多边形补丁替换为Matplotlib的OffsetImage+AnnotationBbox组合即可,这类对象同样属于Matplotlib Artist基类的子类,原生支持contains碰撞检测、set_animated动画控制、位置修改等你原有逻辑依赖的所有属性和方法。
核心修改步骤
- 新增依赖导入:需要用到Pillow库加载本地图片,以及Matplotlib的OffsetImage、AnnotationBbox处理图片渲染
- 替换原有的多边形实例:将
plt.Polygon替换为图片对应的AnnotationBbox实例 - 微调位置读写逻辑:将原有读写
self.polygon.xy的部分,替换为self.artist.get_xy()和self.artist.set_xy()即可 - 可选适配旋转/缩放:如果需要和原有逻辑一样支持角度设置、尺寸缩放,可以通过Pillow的图片旋转方法、OffsetImage的
zoom参数实现
完整可运行修改示例
import numpy as np import matplotlib.pyplot as plt from abc import ABCMeta, abstractmethod import logging from PIL import Image from matplotlib.offsetbox import OffsetImage, AnnotationBbox # 日志配置和原有逻辑一致 logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) class DraggableArtist: __metaclass__ = ABCMeta _lock = None # 同一时间仅允许拖拽一个元素 def __init__(self, center, id_, type_, **kwargs): self.center = np.array(center) self.artist = None # 子类赋值为具体的Artist实例 self.press = None self.background = None self.id_ = id_ self.type_ = type_ def connect(self): '绑定所有需要的事件' self.cidpress = self.artist.figure.canvas.mpl_connect('button_press_event', self.on_press) self.cidrelease = self.artist.figure.canvas.mpl_connect('button_release_event', self.on_release) self.cidmotion = self.artist.figure.canvas.mpl_connect('motion_notify_event', self.on_motion) self.cidscroll = self.artist.figure.canvas.mpl_connect('scroll_event', self.on_scroll) def on_press(self, event): if event.inaxes != self.artist.axes or DraggableArtist._lock is not None: return contains, attrd = self.artist.contains(event) if not contains: return # 存储初始位置和点击坐标 self.press = self.artist.get_xy(), event.xdata, event.ydata DraggableArtist._lock = self logger.debug("Pressed %s (%d) at (%f, %f)", self.type_, self.id_, event.xdata, event.ydata) # 原有blit渲染优化逻辑完全复用 canvas = self.artist.figure.canvas axes = self.artist.axes self.artist.set_animated(True) canvas.draw() self.background = canvas.copy_from_bbox(self.artist.axes.bbox) axes.draw_artist(self.artist) canvas.blit(axes.bbox) def on_scroll(self, event): if event.inaxes != self.artist.axes or DraggableArtist._lock is not None: return contains, attrd = self.artist.contains(event) if not contains: return # 可在此处扩展滚动缩放逻辑,和原有多边形版本逻辑一致 def on_motion(self, event): if DraggableArtist._lock is not self or event.inaxes != self.artist.axes: return init_xy, xpress, ypress = self.press dx = event.xdata - xpress dy = event.ydata - ypress # 更新图片位置 new_x = init_xy[0] + dx new_y = init_xy[1] + dy self.artist.set_xy((new_x, new_y)) self.center = np.array([new_x, new_y]) logger.debug("Move %s (%d) to (%f, %f)", self.type_, self.id_, new_x, new_y) # blit渲染逻辑完全复用 canvas = self.artist.figure.canvas axes = self.artist.axes canvas.restore_region(self.background) axes.draw_artist(self.artist) canvas.blit(axes.bbox) def on_release(self, event): if DraggableArtist._lock is not self: return self.press = None DraggableArtist._lock = None logger.debug("Release %s (%d) at (%f, %f)", self.type_, self.id_, self.center[0], self.center[1]) self.artist.set_animated(False) self.background = None self.artist.figure.canvas.draw() def disconnect(self): self.artist.figure.canvas.mpl_disconnect(self.cidpress) self.artist.figure.canvas.mpl_disconnect(self.cidrelease) self.artist.figure.canvas.mpl_disconnect(self.cidmotion) self.artist.figure.canvas.mpl_disconnect(self.cidscroll) class DraggableImage(DraggableArtist): _id = 0 def __init__(self, img_path, center, zoom=1.0, orientation=0, **kwargs): super().__init__(center, DraggableImage._id, "custom_image", **kwargs) DraggableImage._id += 1 # 加载并处理图片 img = Image.open(img_path) # 适配旋转逻辑,单位为角度 if orientation != 0: img = img.rotate(orientation, expand=True) # 创建OffsetImage,zoom控制缩放比例 offset_img = OffsetImage(img, zoom=zoom) # 创建AnnotationBbox,设置中心对齐 self.artist = AnnotationBbox( offset_img, center, frameon=False, # 不显示图片外边框,需要可改为True pad=0.0 # 去掉边框内边距 ) # 测试代码 fig = plt.figure() ax = fig.add_subplot(111) ax.set_xlim(0, 20) ax.set_ylim(0, 20) ax.set_aspect("equal") # 实例化可拖拽图片,替换为你本地的图片路径即可 img_list = [ DraggableImage("test1.png", center=[2,6], zoom=0.1, orientation=30), DraggableImage("test2.jpg", center=[6,3], zoom=0.2, orientation=0), DraggableImage("test3.svg", center=[10,10], zoom=0.5, orientation=60) ] for img_obj in img_list: ax.add_artist(img_obj.artist) img_obj.connect() plt.show()
注意事项
- 依赖安装:如果没有Pillow库,执行
pip install pillow即可安装 - 缩放参数
zoom需要根据你的原图尺寸调整,避免图片过大/过小 - SVG格式图片加载需要Pillow支持,若加载失败可先转成png/jpg格式使用
- 默认点击图片的整个矩形区域都会触发拖拽,如果需要忽略PNG的透明区域,可以重写contains逻辑,获取点击位置对应的图片像素alpha值判断即可
- 原有逻辑的旋转、缩放、事件绑定都可以直接扩展,和多边形版本逻辑完全一致
内容的提问来源于stack exchange,提问作者deb2014
相关产品推荐
相关产品推荐

