Python:像素操作性能。嵌入式设备的虚拟桌面

Python: pixel manipulation pefrormance. Virtual desktop for embedded device

我正在寻找一种在 python 中进行像素操作的有效方法。 目标是制作一个 python 脚本作为嵌入式系统的虚拟桌面。 我已经有一个可用的版本,但显示单帧需要一秒以上(太长)。

每秒刷新显示 5 次就好了。

工作原理:

  1. 有一个带微控制器和显示器的电子设备(128x64px,黑白像素)。
  2. 有一台 PC 通过 RS-485 连接到它。
  3. 微控制器中有一个数据缓冲区,代表每个像素。让我们称之为 diplay_buffer.
  4. Python PC 上的脚本从微控制器下载 diplay_buffer。
  5. Python 脚本根据来自 diplay_buffer 的数据创建图像。 (我需要优化)

diplay_buffer 是一个 1024 字节的数组。微控制器对其进行准备,然后在实际显示器上显示其内容。我需要使用 python 脚本在 PC 屏幕上显示真实显示器的虚拟副本。

显示方式:

diplay_buffer中的单个位表示单个像素。 显示器有 128x64 像素。 diplay_buffer 中的每个字节代表垂直方向的 8 个像素。前128个字节表示第一行像素(字节中有64px / 8个像素= 8行)。

我使用 python TK 和函数 img.put() 来插入像素。如果位为 1,我插入黑色像素;如果位为 0,我插入白色像素。这是非常无效的。 也许 class 与 PhotoImage 不同,具有更好的像素能力?

我在示例中附加了最少的代码 diplay_buffer。当您 运行 脚本时,您将看到帧和执行时间。

Meybe 会有人帮助尝试优化它吗? 你能告诉我显示像素的更快方法吗?

登德戴尔

Sample frame downloaded from uC

和代码(你可以轻松运行它)


#this script displays value from uC display buffer in a python screen
from tkinter import Tk, Canvas, PhotoImage, mainloop
from math import sin
import time

WIDTH, HEIGHT = 128, 64
ROWS = 8

#some code from tutorial... check what it does:
window = Tk()
canvas = Canvas(window, width=WIDTH, height=HEIGHT, bg="#ffffff")
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image((WIDTH/2, HEIGHT/2), image=img, state="normal")


#this is sample screen from uC. It is normally periodically read from uC on runtime to refresh screen view. 
diplay_buffer =bytes([16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 0, 0, 0, 0, 0, 0, 0, 130, 254, 130, 0, 0, 254, 32, 16, 8, 254, 0, 254, 144, 144, 144, 128, 0, 124, 130, 130, 130, 124, 0, 0, 0, 0, 0, 0, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 16, 16, 16, 16, 16, 0, 0, 0, 18, 42, 42, 42, 36, 0, 28, 34, 34, 34, 28, 0, 0, 16, 126, 144, 64, 0, 32, 32, 252, 34, 36, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 2, 130, 252, 128, 0, 4, 42, 42, 30, 2, 0, 62, 16, 32, 32, 30, 0, 0, 0, 0, 0, 0, 0, 0, 66, 254, 2, 0, 0, 130, 132, 136, 144, 224, 0, 0, 0, 0, 0, 0, 0, 78, 146, 146, 146, 98, 0, 124, 138, 146, 162, 124, 0, 78, 146, 146, 146, 98, 0, 78, 146, 146, 146, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 254, 16, 16, 16, 254, 0, 28, 42, 42, 42, 24, 0, 0, 130, 254, 2, 0, 0, 0, 130, 254, 2, 0, 0, 28, 34, 34, 34, 28, 0, 0, 0, 0, 0, 0, 0, 254, 144, 144, 144, 128, 0, 62, 16, 32, 32, 16, 0, 0, 34, 190, 2, 0, 0, 28, 42, 42, 42, 24, 0, 62, 16, 32, 32, 30, 0, 28, 34, 34, 20, 254, 0, 0, 0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 124, 130, 130, 130, 68, 0, 4, 42, 42, 30, 2, 0, 62, 16, 32, 32, 30, 0, 0, 0, 0, 0, 0, 0, 50, 9, 9, 9, 62, 0, 28, 34, 34, 34, 28, 0, 60, 2, 2, 4, 62, 0, 0, 0, 0, 0, 0, 0, 28, 34, 34, 34, 28, 0, 63, 24, 36, 36, 24, 0, 32, 32, 252, 34, 36, 0, 0, 34, 190, 2, 0, 0, 62, 32, 30, 32, 30, 0, 0, 34, 190, 2, 0, 0, 34, 38, 42, 50, 34, 0, 28, 42, 42, 42, 24, 0, 64, 128, 154, 144, 96, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 254, 146, 146, 146, 108, 0, 4, 42, 42, 30, 2, 0, 28, 34, 34, 34, 20, 0, 254, 8, 20, 34, 0, 0, 0, 0])


def get_normalized_bit(value, bit_index):
    return (value >> bit_index) & 1


time_start = time.time()
#first pixels are drawn invisible (some kind of frame in python) so set an offset:
x_offset = 2 
y_offset = 2
x=x_offset
y=y_offset

#display all uC pixels (single screen frame):
byteIndex=0
for j in range(ROWS): #multiple rows
    for i in range(WIDTH): #row
        for n in range(8): #byte
            if get_normalized_bit(diplay_buffer[byteIndex], 7-n):
                img.put("black", (x,y+n))
            else:
                img.put("white", (x,y+n))
        x+=1
        byteIndex+=1
    x=x_offset
    y+=7
time_stop = time.time()
print("Refresh time: ", str(time_stop - time_start), "seconds")    
    
mainloop()
 

我并没有真正使用 Tkinter,但我读到使用 put() 将单个像素写入图像非常慢。因此,我修改了您的代码以将像素放入 Numpy 数组中,然后使用 PIL 将其转换为 PhotoImage.

将您的字节缓冲区转换为 PhotoImage 在我的 Mac 上大约需要 1 毫秒。如果将三个 for 循环包装到一个 Numba-jitted 函数中,它可能会快 10-100 倍,但它似乎不值得,因为它可能足够快。

#!/usr/bin/env python3

import numpy as np
from tkinter import *
from PIL import Image, ImageTk

# INSERT YOUR variable display_buffer here <<<

# Make a Numpy array of uint8, that will become
# ... our PIL Image that will become... 
# ... a PhotoImage
WIDTH, HEIGHT, ROWS = 128, 64, 8
na = np.zeros((HEIGHT,WIDTH), np.uint8)

idx = 0
x = y = 0
for j in range(ROWS):
   for i in range(WIDTH):
      b = display_buffer[idx]
      for n in range(8):
         na[y+n, x] = (1 - ((b >> (7-n)) & 1)) * 255
      idx += 1
      x   += 1
   x  = 0
   y += 7

# Make Numpy array into PIL Image
PILImage = Image.fromarray(na)

border = 10
root = Tk()  
canvas = Canvas(root, width = 2*border + WIDTH, height = 2*border + HEIGHT)  
canvas.pack()  
# Make PIL Image into PhotoImage
img = ImageTk.PhotoImage(PILImage)
canvas.create_image(border, border, anchor=NW, image=img) 
root.mainloop() 

另外,我不知道你的串口线有多快,但是传输1024字节可能需要一些时间,所以你可以考虑启动第二个线程从你的串口中重复读取1024字节并将它们填充到一个 Queue 用于主进程 get() 它们来自。


此外,您可以完全避免使用 Tkinter,只需像这样使用 OpenCV imshow()

#!/usr/bin/env python3

import numpy as np
import cv2

# INSERT YOUR display_buffer here <<<

# Make a Numpy array of uint8, that will be displayed
WIDTH, HEIGHT, ROWS = 128, 64, 8
na = np.zeros((HEIGHT,WIDTH), np.uint8)

idx = 0
x = y = 0
for j in range(ROWS):
   for i in range(WIDTH):
      b = display_buffer[idx]
      for n in range(8):
         na[y+n, x] = (1 - ((b >> (7-n)) & 1)) * 255
      idx += 1
      x   += 1
   x  = 0
   y += 7


while True:
  # Display image
  cv2.imshow("Virtual Console", na)

  # Wait for user to press "q" to quit
  if cv2.waitKey(1) & 0xFF == ord('q'):
     break

我决定尝试使用 Numba,提取 128x64 帧的时间减少到 68 微秒。请注意 Python 必须第一次编译,所以我做了一个 warm-up 运行 包含编译然后测量第二个 运行:

#!/usr/bin/env python3

import numba as nb
import numpy as np
from tkinter import *
from PIL import Image, ImageTk
import time

# Make a Numpy array of uint8, that will become
# ... our PIL Image that will become... 
# ... a PhotoImage
WIDTH, HEIGHT, ROWS = 128, 64, 8
na = np.zeros((HEIGHT,WIDTH), np.uint8)

@nb.njit()
def extract(na,display_buffer):
   idx = 0
   x = y = 0
   for j in range(ROWS):
      for i in range(WIDTH):
         b = display_buffer[idx]
         for n in range(8):
            na[y+n, x] = (1 - ((b >> (7-n)) & 1)) * 255
         idx += 1
         x   += 1
      x  = 0
      y += 7
   return na

# Following is first run which includes compilation time
warmup = extract(na, display_buffer)

# Only time the second run
start = time.time()
na = extract(na, display_buffer)
# Make Numpy array into PIL Image
PILImage = Image.fromarray(na)
elapsed = (time.time()-start)*1000
print(f'Total time: {elapsed} ms')      # Reports 0.068 ms

border = 10
root = Tk()  
canvas = Canvas(root, width = 2*border + WIDTH, height = 2*border + HEIGHT)  
canvas.pack()  
# Make PIL Image into PhotoImage
img = ImageTk.PhotoImage(PILImage)
canvas.create_image(border, border, anchor=NW, image=img) 
root.mainloop()