You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 12:06:03