将C或numpy数组转换为Tkinter PhotoImage,最少复制次数

11

我知道一种通过Tkinter显示MxNx3 numpy数组作为RGB图像的方法,但我的方法在过程中会制作几份数组副本:

a = np.random.randint(low=255, size=(100, 100, 3), dtype=np.uint8) # Original
ppm_header = b'P6\n%i %i\n255\n'%(a.shape[0], a.shape[1])
a_bytes = a.tobytes() # First copy
ppm_bytes = ppm_header + a_bytes # Second copy https://en.wikipedia.org/wiki/Netpbm_format
root = tk.Tk()
img = tk.PhotoImage(data=ppm_bytes) # Third and fourth copies?
canvas = tk.Canvas(root, width=a.shape[0], height=a.shape[1])
canvas.pack()
canvas.create_image(0, 0, anchor=tk.NW, image=img) # Fifth copy?
root.mainloop()

如何用最少的副本获得相同的结果?
理想情况下,我想创建一个numpy数组,它是与Tkinter的 PhotoImage 对象使用相同字节的视图,有效地给我一个可变像素值的 PhotoImage ,并使其便宜且快速地更新Tkinter显示。我不知道如何从Tkinter中提取这个指针。
也许有一种方法可以通过ctypes实现,就像在这里暗示PhotoImage.put()方法似乎非常慢,但也许我错了,这是一条前进的路?
我尝试创建一个包含ppm头和图像像素值的bytearray(),然后使用numpy.frombuffer()将图像像素值视为numpy数组,但我认为 PhotoImage 构造函数想要一个 bytes() 对象,而不是一个bytearray()对象,另外我认为Tkinter会将其 data 输入的字节复制到内部格式(32位RGBA?)。我猜这比上面的方法节省了一个副本?

1
就此而言,Tk photo image 的底层数据存储是RGBA格式,每个通道每像素8位。最高效的方法肯定涉及从这里开始,但我不知道如何在Tkinter中实际实现它... - Donal Fellows
@donal-fellows 我有一些使用ctypes与C动态链接库通讯的经验,但我对tkinter的实现方式不太了解,因此无从下手。Python是否有类似于tkinter共享库并有文档化API供调用的呢? - Andrew
1
底层库API在这里,你需要的API调用是Tk_PhotoPutBlock()这里)。组装它的参数...嗯,这有点棘手。_handle_可以从Tk_FindPhoto()中获取,但_interp_被埋在Tkinter(Python到Tcl/Tk包装库)中,我不太清楚如何帮助。此外,您必须仅从一个线程使用该代码,否则会遇到严重问题(这是常见的Python/Tkinter问题)。 - Donal Fellows
3个回答

5
我可以使用PIL和Label将其缩减为1(也许2)份副本:
import numpy as np
import tkinter as tk
from PIL import Image, ImageTk

a = np.random.randint(low=255, size=(100, 100, 3), dtype=np.uint8) # Original
root = tk.Tk()
img = ImageTk.PhotoImage(Image.fromarray(a)) # First and maybe second copy.
lbl = tk.Label(root, image=img)
lbl.pack()
root.mainloop()

然而这仍然不是可变的。如果你想实现可变,我认为你需要通过自己在画布上放置像素来重新创造一幅图像。我曾经用这个项目做过这件事,并发现最快速的更新方法是使用matplotlib动画,这对你来说非常好,因为你已经在使用 np数组了。

我的代码使用了tk.CanvasPIL Image(使用putpixel()),以及matplotlib


实际上,与原始代码相比,您只保存了一个副本:a_bytes = a.tobytes(); ppm_bytes = ppm_header + a_bytes -> Image.fromarray(a) - ivan_pozdeev
ImageTk.PhotoImage 包含两个副本:它委托给 tkinter.BitmapImage,而后者又包含两个副本 - ivan_pozdeev

3
  • 您可以消除第一个和第二个副本

使用numpy.frombuffer,您可以在任意数据上获得 numpy.ndarray

shape=(100,100,3)
ppm_header = b'P6\n%i %i\n255\n'%(shape[0], shape[1])
ppm_bytes = ppm_header + b'\0'*(shape[0]*shape[1]*shape[2])
array_image = np.frombuffer(ppm_bytes, dtype=np.uint8, offset=len(ppm_header)).reshape(shape)
  • 第三和第四份副本是不可避免的(见下文),但第三份副本在调用后立即被丢弃。

  • 第五份副本实际上并没有被制作(也见下文)。

  • 绘图阶段通过窗口系统的绘图API将内容复制到屏幕上,这也是不可避免的


Tcl是一种安全的、垃圾回收的语言,就像Python一样,Tcl对象不支持“缓冲区协议”,也不使用它们不拥有的内存来存储数据(虽然对象可以共享)。


1
我认为他想要的是一种将二进制 blob 传递给 C API 调用的方法;numpy 肯定可以生成正确格式的数据。这样做至少可以让复制在底层是一个直接的 memcpy() - Donal Fellows
@DonalFellows Tk C API 在 Tkinter 中不可用。此外,photo create 是使用 tkImgPhoto.c:ImgPhotoCreate 实现的,但该函数甚至没有在 API 中公开。 - ivan_pozdeev
是的,这就是为什么这是一个专家级别的工作需求。但是,如果您可以获取句柄名称(一个小字符串),那么您应该能够使用相当小的一块C代码完成其余部分。它可能会在底层进行一些黑客操作,因为它正在桥接两个完全不同的内存管理方案(Python和Tcl),但它可以被制作成可行并且实际上很快。 - Donal Fellows
@DonalFellows 这不仅仅是一个“hack”,它将非常脆弱。因为Tcl的对象既不支持“缓冲区协议”,也不使用它们不拥有的内存来存储数据。你可以通过解析内存来访问数据缓冲区 - 在特定于Tcl/Tk版本、架构、编译器和编译选项的方式下,伪造该缓冲区的数据 - 以同样的方式 - 即使那个缓冲区随时可能被移动或丢弃。所以作为一名专家,我不能推荐这种方法。 - ivan_pozdeev

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接