
本文介绍一种基于多线程与 matplotlib 的可靠方案:在主线程中保持 gui 响应,同时在子线程中持续监听 usb midi 控制器消息,并根据音符编号实时更新窗口内显示的图像。
本文介绍一种基于多线程与 matplotlib 的可靠方案:在主线程中保持 gui 响应,同时在子线程中持续监听 usb midi 控制器消息,并根据音符编号实时更新窗口内显示的图像。
实现“通过 MIDI 实时切换图像”的核心难点在于 GUI 阻塞 与 事件循环冲突:Matplotlib 的 plt.show() 默认阻塞主线程,导致后续 MIDI 监听逻辑无法执行;而改用 block=False 又因缺乏主动重绘机制,图像无法渲染或更新。解决方案是——将 MIDI 输入监听逻辑移至后台线程,让 Matplotlib 主线程专注渲染与交互。
✅ 关键修改要点
启用多线程监听 MIDI
使用threading.Thread(target=self.poll_midi, daemon=True)启动守护线程,确保 MIDI 持续接收不阻塞 GUI。daemon=True保证主线程退出时子线程自动终止,避免程序挂起。-
修复图像更新流程
-
update_image()中仅调用self.im.set_array(...)更新数据,不重复创建 imshow; -
display()中完成图像初始化(首次)或复用(后续),并必须调用plt.draw()(或self.fig.canvas.draw_idle())触发重绘; - 移除
plt.pause(0.1)等非必要轮询,交由 Matplotlib 自身事件循环管理。
-
-
修正潜在错误
- 原代码中
auto_start未定义 → 需显式设为True或从参数传入; -
IndexError中引用了未定义变量n→ 改为self.curr; - 图像加载需补充
glob导入及格式过滤(避免加载非图像文件)。
- 原代码中
? 完整可运行示例(精简优化版)
import matplotlib.pyplot as plt
import cv2
import mido
import glob
import os
import threading
def get_images(path: str) -> list:
"""安全加载指定目录下所有常见格式图像(支持递归)"""
extensions = ("*.png", "*.jpg", "*.jpeg", "*.bmp", "*.tiff")
image_files = []
for ext in extensions:
image_files.extend(glob.glob(os.path.join(path, ext)))
image_files.extend(glob.glob(os.path.join(path, ext.upper())))
images = []
for f in image_files:
img = cv2.imread(f)
if img is not None:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转 RGB 适配 matplotlib
images.append(img)
else:
print(f"Warning: failed to load {f}")
print(f"Loaded {len(images)} valid images.")
return images
def get_midi_port():
input_ports = mido.get_input_names()
for p in input_ports:
if "Midi Through" not in p and "RT Midi" not in p: # 排除虚拟/直通端口
print(f"Using MIDI port: {p}")
return p
raise RuntimeError("No usable MIDI input port found.")
class MIDI_Images:
def __init__(self, path="."):
self.path = path
self.loaded_images = get_images(self.path)
if not self.loaded_images:
raise ValueError("No valid images found in the specified directory.")
self.curr = 0
self.fig, self.ax = plt.subplots(figsize=(8, 6))
self.ax.axis('off') # 隐藏坐标轴,聚焦图像
# 首次显示
self.im = self.ax.imshow(self.loaded_images[self.curr])
plt.tight_layout()
# 启动 MIDI 监听线程(守护线程)
threading.Thread(target=self.poll_midi, daemon=True).start()
# 主线程进入阻塞式 GUI 循环(安全且响应式)
plt.show()
def poll_midi(self):
try:
port_name = get_midi_port()
with mido.open_input(port_name) as inport:
print(f"✅ Listening on '{inport.name}'...")
while True:
for msg in inport.iter_pending():
if msg.type == 'note_on' and msg.velocity > 0: # 忽略 note_off 和静音音符
note_index = msg.note - 24 # 假设最低音符为 C2 (MIDI 24)
if 0 <= note_index < len(self.loaded_images):
self.curr = note_index
self.update_image()
else:
print(f"⚠️ Note {msg.note} → index {note_index} out of range [0, {len(self.loaded_images)-1}]")
except Exception as e:
print(f"❌ MIDI error: {e}")
def update_image(self):
try:
self.im.set_array(self.loaded_images[self.curr])
self.fig.canvas.draw_idle() # 推荐:比 plt.draw() 更高效
# 可选:添加状态提示
self.ax.set_title(f"Image #{self.curr} | MIDI Note {self.curr + 24}", fontsize=12)
except IndexError:
print(f"❌ Image index {self.curr} invalid.")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--imagedir', type=str, default='.', help='Directory containing images')
args = parser.parse_args()
try:
MIDI_Images(args.imagedir)
except KeyboardInterrupt:
print("\nBye.")
except Exception as e:
print(f"Fatal error: {e}")⚠️ 注意事项与最佳实践
-
图像预处理:OpenCV 默认读取 BGR 格式,务必用
cv2.cvtColor(..., cv2.COLOR_BGR2RGB)转换,否则颜色异常; -
MIDI 映射健壮性:建议对
msg.note做边界检查(如24 ≤ note ≤ 108),避免越界;也可映射到0–len(images)-1的模运算; -
线程安全:Matplotlib 的 canvas 绘图操作不是线程安全的,但
draw_idle()在主线程事件队列中调度,因此可在子线程中安全调用(Matplotlib ≥ 3.3); -
依赖安装:
pip install matplotlib opencv-python mido python-rtmidi # Linux/macOS # Windows 用户推荐:pip install python-rtmidi # 避免 portmidi 编译问题
-
替代方案提示:若需更高性能或复杂交互(如叠加控件、视频流),可迁移到 PyQt5/PySide6 +
QLabel.setPixmap(),其信号槽机制天然支持跨线程安全更新。
该方案兼顾简洁性与稳定性,无需引入重量级 GUI 框架,即可实现专业级 MIDI 视觉响应系统。

















