python - 在 python、go 或 julia 中快速直接访问像素

标签 python numpy pygame-surface psychopy psychtoolbox

我编写了一个小程序,可以创建随机噪声并将其全屏显示(5K 分辨率)。我用 pygame 来实现。但刷新速度却非常慢。 surfarray.blit_array 和随机生成都需要花费大量时间。有什么办法可以加快这个速度吗?我也可以灵活地使用 julia 或 golang 来代替。或者还有psychopy或octave与psychotoolbox(但是这些似乎不能在linux/wayland下工作)。

这是我写的:


import pygame
import numpy as N
import pygame.surfarray as surfarray
from numpy import int32, uint8, uint

 
def main():
     
    pygame.init()
     
    #flags = pygame.OPENGL | pygame.FULLSCREEN   # OpenGL does not want to work with surfarray
    flags = pygame.FULLSCREEN
    screen = pygame.display.set_mode((0,0), flags=flags, vsync=1)
    w, h = screen.get_width(), screen.get_height()

    clock = pygame.time.Clock()
    font = pygame.font.SysFont("Arial" , 18 , bold = True)
     
    # define a variable to control the main loop
    running = True

    def fps_counter():
        fps = str(int(clock.get_fps()))
        fps_t = font.render(fps , 1, pygame.Color("RED"))
        screen.blit(fps_t,(0,0))

                    
     
    # main loop
    while running:
        # event handling, gets all event from the event queue
        for event in pygame.event.get():
            # only do something if the event is of type QUIT
            if event.type == pygame.QUIT:
                # change the value to False, to exit the main loop
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    pygame.quit()
                    return
        array_img = N.random.randint(0, high=100, size=(w,h,3), dtype=uint)
        surfarray.blit_array(screen, array_img)
        fps_counter()
        pygame.display.flip()
        clock.tick()
        #print(clock.get_fps())
     
# run the main function only if this module is executed as the main script
# (if you import this as a module then nothing is executed)
if __name__=="__main__":
    # call the main function
    main()

我需要至少 30 fps 的刷新率才能发挥作用

最佳答案

更快的随机数生成

生成随机数的成本很高。当随机数生成器 (RNG) 需要统计上准确(即随机数即使在经过一些变换后也需要看起来非常随机)以及按顺序生成数字时尤其如此。

事实上,对于加密用途或某些数学(蒙特卡罗)模拟,目标 RNG 需要足够先进,以便几个后续生成的随机数之间不存在统计相关性。实际上,执行此操作的软件方法非常昂贵,以至于现代主流处理器提供了一种执行此操作的方法 specific instructions 。但并非所有处理器都支持这一点,并且 AFAIK Numpy 不使用它(当然是为了可移植性,因为在多台机器上具有相同种子的随机序列预计会给出相同的结果)。

幸运的是,RNG 在大多数其他用例中通常不需要那么准确。他们只需要看起来很随意。有many methods来做到这一点(例如 Mersenne Twister、Xoshiro、Xorshift、PCG/LCG)。 RNG 的性能、准确性和特化之间通常需要权衡。由于 Numpy 需要提供相对准确的通用 RNG(尽管据我所知并不意味着用于加密用例),因此其性能次优也就不足为奇了。

对许多不同方法进行了有趣的回顾 here (尽管结果应该持保留态度,特别是在性能方面,因为 SIMD 友好等细节对于许多用例中的性能至关重要)。

在纯 Python(使用 CPython)中实现非常快速的随机数生成器是不可能的,但可以使用 Numba (或 Cython)来做到这一点。不过,可能有一些用 native 语言编写的快速现有模块可以做到这一点。最重要的是,我们可以使用多个线程来加速操作。为了简单起见(也因为它相对较快),我选择实现 Xorshift64 RNG。

import numba as nb

@nb.njit('uint64(uint64,)')
def xorshift64_step(seed):
    seed ^= seed << np.uint64(13)
    seed ^= seed >> np.uint64(7)
    seed ^= seed << np.uint64(17)
    return seed

@nb.njit('uint64()')
def init_xorshift64():
    seed = np.uint64(np.random.randint(0x10000000, 0x7FFFFFFF)) # Bootstrap
    return xorshift64_step(seed)

@nb.njit('(uint64, int_)')
def random_pixel(seed, high):
    # Must be a constant for sake of performance and in the range [0;256]
    max_range = np.uint64(high)
    # Generate 3 group of 16 bits from the RNG
    bits1 = seed & np.uint64(0xFFFF)
    bits2 = (seed >> np.uint64(16)) & np.uint64(0xFFFF)
    bits3 = seed >> np.uint64(48)
    # Scale the numbers using a trick to avoid a modulo 
    # (since modulo are inefficient and statistically incorrect here)
    r = np.uint8(np.uint64(bits1 * max_range) >> np.uint64(16))
    g = np.uint8(np.uint64(bits2 * max_range) >> np.uint64(16))
    b = np.uint8(np.uint64(bits3 * max_range) >> np.uint64(16))
    new_seed = xorshift64_step(seed)
    return (r, g, b, new_seed)

@nb.njit('(int_, int_, int_)', parallel=True)
def pseudo_random_image(w, h, high):
    res = np.empty((w, h, 3), dtype=np.uint8)
    for i in nb.prange(w):
        # RNG seed initialization
        seed = init_xorshift64()
        for j in range(h):
            r, g, b, seed = random_pixel(seed, high)
            res[i, j, 0] = r
            res[i, j, 1] = g
            res[i, j, 2] = b
    return res

代码相当大,但在我的 6 核 i5-9600KF CPU 上,它比 Numpy 快约 22 倍。请注意,类似的代码可以在 Julia 中使用,以便获得快速实现(因为 Julia 使用类似于 Numba 的基于 LLVM 的 JIT)。

在我的机器上,这足以达到75 FPS(最大值),而初始代码达到 16 FPS。


更快的操作和渲染

在大多数平台上,生成新的随机数组受到页面错误速度的限制。这会显着减慢计算速度。在 Python 中缓解这种情况的唯一方法是创建一次 Brame 缓冲区并执行就地操作。此外,PyGame 当然会在内部执行复制(也可能执行许多绘制调用),因此使用较低级别的 API 可以显着加快速度。尽管如此,该操作可能会受到内存限制,并且没有什么可以避免的。但此时对您来说可能已经足够快了。

最重要的是,帧在 GPU 上渲染,因此 CPU 需要发送/复制 GPU 上的缓冲区,通常通过独立 GPU 的 PCIe 互连。对于宽屏幕,此操作不是很快。

实际上,您可以使用着色器直接在 GPU 上生成随机图像(或 OpenCL/CUDA 等工具)。这避免了上述开销,并且 GPU 可以比 CPU 更快地完成此任务。

关于python - 在 python、go 或 julia 中快速直接访问像素,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77361304/

相关文章:

python - 苹果 M1 : install psycopg2 package Symbol not found: _PQbackendPID

python - numpy 基于 Mx1 矩阵创建 Mx2 矩阵

python - 如何修复Python中的“'numpy.ndarray'对象不可调用”错误?

python - 我如何在pygame中删除我的 Sprite 表上的黑色背景

python - 我不明白 screen.fill() 是如何工作的

python - csv 文件的两个特定列的列表字典

c++ - 3个长整数的乘法在C++和Python中给出了不同的答案

python - pytest:基于每个模块的选择性日志级别

python - 使用 pandas 查找从特定范围内的单元格中删除 '!' 的代码

python - 如何从 float32 的 numpy 数组创建 pygame 表面?