Introducción a Surfarray¶
- Autor:
Pete Shinners
- Contacto:
- Traducción al español:
Estefanía Pivaral Serrano
Introducción¶
Este tutorial intentará presentar tanto Numpy como el módulo de surfarray de pygame a los usuarios. Para principiantes, el código que utiliza surfarray puede ser bastante intimidante. Pero en realidad, hay sólo unos pocos conceptos que entender y estarás listo para empezar. Con el uso del módulo de surfarray es posible realizar operaciones a nivel de píxeles desde el código Python sencillo. El rendimiento puede llegar a ser bastante cercano al nivel de hacer el código en C.
Puede que solo desees ir directamente a la sección "Examples" para tener una idea de lo que es posible con este módulo, y luego comenzar desde el principio aquí para ir avanzando.
Ahora bien, no voy a engañarte para que pienses que todo va a ser muy sencillo. Lograr efectos avanzados modificando los valores de píxeles puede ser complicado. Solo dominar Numeric Python, NumPy, (el paquete de matrices original de SciPy era Numeric, el predecesor de NumPy) requiere aprendizaje. En este tutorial me centraré en lo básico y utilizaré muchos ejemplos en un intento de sembrar las semillas de la sabiduría. Después de haber terminado el tutorial, deberías tener una comprensión básica de cómo funciona el surfarray.
Numeric Python¶
Si no tenés instalado el paquete NumPy de python, necesitarás hacerlo. Podés descargar el paquete dede la página de descargas de NumPy en NumPy Downloads Page Para asegurarte que Numpy esté funcionando correctamente, deberías obtener algo como esto desde prompt (inteprete) interactivo de Python.:
>>> from numpy import * #importar numeric
>>> a = array((1,2,3,4,5)) #crear un array
>>> a #mostrar array
array([1, 2, 3, 4, 5])
>>> a[2] #index al array
3
>>> a*2 #nuevo array con valores dobles
array([ 2, 4, 6, 8, 10])
Como se puede ver, el módulo NumPy nos proporciona un nuevo tipo de data, el array. Este objeto mantiene un array de tamaño fijo, y todos los valores que contiene en su interior son del mismo tipo. Los arrays (matrices) también pueden ser multidimensionales, que es como las usaremos con imágenes. Hay un poco más de información, pero es suficiente para empezar.
Si mirás al último comando de arriba, verás que las operaciones matemáticas en los array de NumPy se aplican para todos los valores del array. Esto se llama "element-wise operations" (operaciones elemento a elemento). Estos arrays también pueden dividirse en listas normales. La sintaxis de la división es la misma que se usa en objetos Python estándar. (así que estudia si es necesario).
Aquí hay algunos ejemplos más de arrays que funcionan.
>>> len(a) #obtener el tamaño del array
5
>>> a[2:] #elementos a partir del 2
array([3, 4, 5])
>>> a[:-2] #todos excepto los últimos 2
array([1, 2, 3])
>>> a[2:] + a[:-2] #agregar el primero y último
array([4, 6, 8])
>>> array((1,2,3)) + array((3,4)) #agregar arrays de tamaños incorrectos
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (3,) (2,)
Obtenemos un error con el último comando, porque intentamos sumar dos arrays que tienen tamaños diferentes. Para que dos arrays operen entre sí, incluyendo operaciones comparaciones y asignaciones, deben tener las mismas dismensiones. Es muy importante saber que los nuevos arrays creados a partir de cortar el original hacen referencia a los mismos valores. Por lo tanto, cambiar los valores en una porción de la división también cambia los valores originales. Es importante cómo se hace esto.
>>> a #mostrar nuestro array inicial
array([1, 2, 3, 4, 5])
>>> aa = a[1:3] #dividir al medio 2 elementos
>>> aa #mostrar la división
array([2, 3])
>>> aa[1] = 13 #cambiar el valor en la división
>>> a #mostrar cambio en el original
array([ 1, 2, 13, 4, 5])
>>> aaa = array(a) #copiar el array
>>> aaa #mostrar copia
array([ 1, 2, 13, 4, 5])
>>> aaa[1:4] = 0 #configurar los valores medios a 0
>>> aaa #mostrar copia
array([1, 0, 0, 0, 5])
>>> a #mostrar nuevamente el original
array([ 1, 2, 13, 4, 5])
Ahora vamos a ver pequeños arrays con dos dimensiones. No te preocupes demasiado, comenzar es lo mismo que tener una tupla de dos dimensiones (una tupla dentro de otra tupla). Empecemos con los arrays de dos dimensiones.
>>> row1 = (1,2,3) #crear una tupla de valores
>>> row2 = (3,4,5) #otra tupla
>>> (row1,row2) #mostrar como una tupla de dos dimensiones
((1, 2, 3), (3, 4, 5))
>>> b = array((row1, row2)) #crear un array en 2D
>>> b #mostrar el array
array([[1, 2, 3],
[3, 4, 5]])
>>> array(((1,2),(3,4),(5,6))) #mostrar el nuevo array en 2D
array([[1, 2],
[3, 4],
[5, 6]])
Ahora, con estos arrays bidimensionales (de ahora en más "2D") podemos
indexar valores específicos y hacer cortes ambas dimensiones. Simplemente
usando una coma para separar los índices, nos permite buscar/cortar
en múltiple dimensiones. Simplemente usando ":
" como un índex
(o no proporcionando suficiente índices) nos devuelve todos los valores
en esa dimensión. Veamos cómo funciona esto.
>>> b #mostrar nuestro array desde arriba
array([[1, 2, 3],
[3, 4, 5]])
>>> b[0,1] #indexar un único valor
2
>>> b[1,:] #dividir la segunda fila
array([3, 4, 5])
>>> b[1] #dividir la segunda fila (igual que arriba)
array([3, 4, 5])
>>> b[:,2] #dividir la última columna
array([3, 5])
>>> b[:,:2] #dividir en un array de 2x2
array([[1, 2],
[3, 4]])
De acuerdo, mantente conmigo acá, esto es lo más díficil que puede ponerse.
Al usar NumPy hay una característica más para la división. La división de arrays
también permite especificar un incremento de divsión. La sintaxis para una
división con incremento es start_index : end_index : increment
.
>>> c = arange(10) #como el rango, pero crea un array
>>> c #muestra el array
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> c[1:6:2] #divide valores impares desde el 1 al 6
array([1, 3, 5])
>>> c[4::4] #divide cada 4to valor, empezando por el 4
array([4, 8])
>>> c[8:1:-1] #divide 1 al 8, de atrás para adelante /// invertido
array([8, 7, 6, 5, 4, 3, 2])
Bien, eso es todo. Hay suficiente información acá para que puedas empezar a usar Numpy con el módulo surfarray. Ciertamente hay mucho más en NumPy, pero esto es solo una introducción. Además, ¿queremos pasar a cosas divertidas, no?
Importar Surfarray¶
Para usar el módulo surfarray necesitamos importarlo. Dado que ambos, tanto surfarray y NumPy, son componentes opcionales para pygame es bueno asegurarse de que se importen correctamente antes de usarlos. En estos ejemplos voy a importar NumPy en una variable llamada N. Esto permitirá saber qué funciones estoy usando son del paquete de NumPy. (y es mucho más corto que escribir NumPy antes de cada función)
probá:
import numpy as N
import pygame.surfarray as surfarray
except ImportError:
raise ImportError, "NumPy and Surfarray are required."
Introducción a Surfarray¶
Hay dos tipos principales de funciones en surfarray. Un conjunto de funciones para crear un array que es una copia de los datos de píxeles de la superficie (surface). Las otras funciones crean una copia referenciada de los datos de píxeles del array, de modo que los cambios en el array afectan directamente a la surface original. Hay otras funciones que permiten acceder a cualquier valor alfa por pixel, como arrays junto con algunas otras funciones útiles. Veremos estas otras funciones más adelante.
Al trabajar con estos arrays de surface, existen dos formas de representar los valores de píxeles. En primar lugar, pueden representarse como enteros mapeados. Este tipo de array es un array simple en 2D con un solo entero que representa el valor de color mapeado de la superficie. Este tipo de array es últil para mover partes de una imagen al rededor de la pantalla. El otro tipo de array utiliza tres valores RGB para representar el color de cada píxel. Este tipo de array hace que sea extremadamente sencillo realizar efectos que cambian el color de cada píxel. Este tipo de array es también un poco más complicado de manejar, ya que es esencialmente un array numérico 3D. Aún así, una vez que ajustas tu mente en el modo adecuado, no es mucho más difícil que usar un array 2D normal.
El módulo NumPy utiliza los tipos de números naturales de la máquina para representar los valores de los datos, por lo que un array de NumPy puede consistir de enteros de 8-bits, 16-bits y 32-bits. (los array también pueden usar otro tipos como flotantes y dobles, pero para la manipulación de imágenes principalmente necesitamos preocuparnos por los tipos de enteros). Debido a esta limitación de tamaños de los enteros, debes tener un poco más de cuidado para asegurarte de que el tipo de arrays que hacen referencia a los datos de píxeles se pueda mapear correctamente con un tipo adecuado de datos. Las funciones que crean estos arrays a partir de las superficies son:
- surfarray.pixels2d(surface)
Crea una matriz 2D (valores de píxeles enteros) que hace referencia a los datos originales de la superficie. Esto funcionará para todos los formatos de surface excepto el de 24-bit.
- surfarray.array2d(surface)
Crea un array 2D (valores de píxeles enteros) que es copiada desde cualquier tipo de superficie.
- surfarray.pixels3d(surface)
Crea un array 3D (valores de píxeles RGB) que hacen referencia a los datos originales de la superficie. Esto solo funcionará en superficies de 24-bit y 32-bit que tengan el formato RGB o BGR.
- surfarray.array3d(surface)
Crea un array 3D (valores de píxeles RGB) que se copia desde cualquier tipo de surface.
Aquí hay una pequeña tabla que podría ilustrar mejor qué tipos de funciones se deben usar en cada surface. Como se puede observar, ambas funciones de array funcionarán con cualquier tipo de surface.
32-bit |
24-bit |
16-bit |
8-bit(c-map) |
|
---|---|---|---|---|
pixel2d |
yes |
yes |
yes |
|
array2d |
yes |
yes |
yes |
yes |
pixel3d |
yes |
yes |
||
array3d |
yes |
yes |
yes |
yes |
Ejemplos¶
Con esta información, estamos preparados para comenzar a probar cosas con los arrays de surface. A continuación encontrarán pequeñas demostraciones que crean un array de NumPy y los muestran en pygame. Estas diferentes pruebas se encuentran en el ejemplo arraydemo.py. Hay una función simple llamada surfdemo_show que muestra un array en la pantalla.

allblack = N.zeros((128, 128))
surfdemo_show(allblack, 'allblack')
Nuestro primer ejemplo crea un array completamente negro. Siempre
que se necesite crear una nueva matriz numérica de un tamaño específico,
es mejor usar la función zeros
. Aquí creamos un array 2D de todos
ceros y lo mostramos.

striped = N.zeros((128, 128, 3))
striped[:] = (255, 0, 0)
striped[:,::3] = (0, 255, 255)
surfdemo_show(striped, 'striped')
Aquí estamos tratando con un array 3D. Empezamos creando una imagen completamente roja. Luego cortamos cada tercera fila y le asignamos a un color azul/verde. Como pueden ver, podemos tratar los arrays 3D casi exactamente de la misma manera que los arrays 2D, solo asegúrense de asignarles 3 valores en lugar de un único entero mapeado.

imgsurface = pygame.image.load('surfarray.png')
rgbarray = surfarray.array3d(imgsurface)
surfdemo_show(rgbarray, 'rgbarray')
Aquí cargamos una imagen con el módulo de imagen, luego lo convertimos
en un array 3D de elementos de color RGB enteros. Una copia RGB
de una surface siempre tiene los colores dispuestos como a[r,c,0] para
el componente rojo, a[r,c,1] para el componente verde, y a[r,c,2] para
el azul. Esto se puede usar sin importar cómo se configuren los píxeles
del surface original, a diferencia de un array 2D que es una copia de
los píxeles de la surface mapped
(raw).
Usaremos esta imagen en el resto de los ejemplos.

flipped = rgbarray[:,::-1]
surfdemo_show(flipped, 'flipped')
Aquí volteamos la imagen verticalmente. Todo lo que necesitamos para esto es tomar el array de la imagen original y cortarlo usando un incremento negativo.

scaledown = rgbarray[::2,::2]
surfdemo_show(scaledown, 'scaledown')
Basado en el último ejemplo, reducir una imagen escalar es bastante lógico. Simplemente cortamos todos los píxeles usando un incremento de 2 vertical y horizontalmente.

shape = rgbarray.shape
scaleup = N.zeros((shape[0]*2, shape[1]*2, shape[2]))
scaleup[::2,::2,:] = rgbarray
scaleup[1::2,::2,:] = rgbarray
scaleup[:,1::2] = scaleup[:,::2]
surfdemo_show(scaleup, 'scaleup')
Aumentar la escala de la imagen requiere un poco más de trabajo, pero es similar al escalado previo hacia abajo, lo hacemos todo con cortes. Primero, creamos un array que tiene el doble del tamaño de nuestro original. Primero copiamos el array original en cada otro píxel del nuevo array. Luego lo hacemos de nuevo para cada otro píxel, haciendo las columnas impares. En este punto, tenemos la imagen escalada correctamente en sentido horizontal, pero las otras filas son negras, por lo que simplemente debemos copiar cada fila a la que está debajo. Entonces tenemos una imagen duplicada en tamaño.

redimg = N.array(rgbarray)
redimg[:,:,1:] = 0
surfdemo_show(redimg, 'redimg')
Ahora estamos usando arrays 3D para cambiar los colores. Acá establecemos todos los valores en verde y azul en cero. Esto nos deja solo con el canal rojo.

factor = N.array((8,), N.int32)
soften = N.array(rgbarray, N.int32)
soften[1:,:] += rgbarray[:-1,:] * factor
soften[:-1,:] += rgbarray[1:,:] * factor
soften[:,1:] += rgbarray[:,:-1] * factor
soften[:,:-1] += rgbarray[:,1:] * factor
soften //= 33
surfdemo_show(soften, 'soften')
Aquí realizamos un filtro de convulción 3x3 que suavizará nuestra imagen. Parece que hay muchos pasos aquí, pero lo que estamos haciendo es desplazar la imagen 1 píxel en cada dirección y sumarlos todos juntos (con algunas multiplicaciones por ponderación). Luego se promedian todos los valores. No es Gaussiano, pero es rápido. Un punto con los arrays NumPy, la precisión de las operaciones aritméticas está determinada por el array con el tipo de datos más grande. Entonces, si el factor no se declarara como un array de 1 elemento de tipo numpy.int32, las multiplicaciones se realizarían utilizando numpy.int8, el entero de 8 bits de cada elemento rgbarray. Esto causará una truncación de valores. El array de suavizado también debe declararse con un tamaño de entero más grande que rgbarray para evitar la truncación.

src = N.array(rgbarray)
dest = N.zeros(rgbarray.shape)
dest[:] = 20, 50, 100
diff = (dest - src) * 0.50
xfade = src + diff.astype(N.uint)
surfdemo_show(xfade, 'xfade')
Por último, estamos realizando una transición gradual entre la imagen original y una imagen de color azul sólido. No es emocionante, pero la imagen de destino podría ser cualquier cosa, y cambiar el multiplicador 0.50 permitirá elegir cualquier paso en una transición lineal entre dos imágenes.
Con suerte, a estas alturas estás empezando a ver cómo surfarray puede ser utilizado para realizar efectos especiales y transformaciones que sólo son posibles a nivel de píxeles. Como mínimo, se puede utilizar surfarray para realizar muchas operaciones del tipo Surface.set_at() y Surface.set_at() rápidamente. Pero no creas que esto ha terminado, todavía queda mucho por aprender.
Bloqueo de Superficie (Surface)¶
Al igual que el resto de pygame, surfarray bloqueará cualquier Surface que necesite para acceder a los datos de píxeles. Sin embargo, hay un elemento más a tener en cuenta; al crear los array de pixeles, la surface original quedará bloqueada durante la vida útil de ese array de píxeles. Es importante recordarlo. Asegurate de "eliminar" el array de píxeles o de dejarlo fuera del alcance (es decir, cuando la funcion vuelve, etc.).
También hay que tener en cuenta que realmente no querés hacer muchos (si es que alguno) accesos directos a píxeles en la surface del hardware (HWSURFACE). Esto se debe a que los datos de la surface se encuentra en la tarjeta gráfica, y transferir cambios de píxeles a través del bus PCI/AGP no es rápido.
Transparencia¶
El módulo surfarray tiene varios métodos para acceder a los valores alpha/colorclave de una Surface. Ninguna de las funciones alpha se ve afectada por la transparencia general de una Surface, solo por los vaores de los píxeles. Aquí está la lista de esas funciones.
- surfarray.pixels_alpha(surface)
Crea un array 2D (valores enteros de píxeles) que hace referencia a los datos alpha de la surface original. Esto solo funcionará en imágenes de 32-bit con un componente alfa de 8-bit.
- surfarray.array_alpha(surface)
Crea un array 2D (valores enteros de píxeles) que se copia desde cualquier tipo de surface. Si la surface no tiene valores alfa, el array tendrá valores completamten opacos (255).
- surfarray.array_colorkey(surface)
Crea un array 2D (valores enteros de píxeles) que está establecida como transparente (0) donde el color de ese píxel coincide con el color clave de la Surface.
Otras Funciones de Surfarray¶
Solo hay algunas otras funciones disponibles en surfarray. Podés obtener una lista
mejor con mayor documentación en surfarray reference page
.
Sin embargo, hay una función muy útil.
- surfarray.blit_array(surface, array)
Esto transferirá cualquier tipo de array de surface 2D o 3D a una Surface con las mismas dimensiones. Este blit de surfarray generalmente será más rápido que asignar un array a la de pixeles referenciado. Sin embargo, no debería ser tan rápido como el blitting normal de surface, ya que esos están muy optimizados.
NumPy más Avanzado¶
Hay un par más de cosas que deberías saber sobre los arrays Numpy. Cuando se
trata de arrays muy grandes, como los que son de 640x480, hay algunas cosas
adicionales sobre las que debes tener cuidado. Principalmente, mientras que
usar los operadores como + y * en los arrays los hace fáciles de usar, también
es muy costoso en arrays grandes. Estos operadores deben hacer nuevas copias
temporales del array, que luego generalmente se copian en otro array. Esto
puede requerir mucho tiempo. Afortunadamente, todos los operadores de Numpy
vienen con funciones especiales que pueden realizar la operación "in place"
(en su lugar). Por ejemplo, en lugar de usar screen[:] = screen + brightmap
podrías querer usar add(screen, brightmap, screen)
que es más rápido.
De todos modos, debes leer la documentación UFunc de Numpy para obtener más
información sobre esto. Es importante cuando se trata de los arrays.
Otra cosa a tener en cuenta al trabajar con arrays NumPy es el tipo de datos del array. Algunos de los arrays (especialmente el tipo de píxeles mapeado) a menudo devuelven arrays con un valor sin signo en 8-bits. Estos arrays se desbordarán fácilmente si no tienes cuidado. NumPy usará la misma coerción que se encuentra en los programas en C, por lo que mezclar una operación con números de 8 bits y 32 bits dará como resultado números de 32 bits. Puedes convertir el tipo de datos del array, pero definitivamente debes ser consciente de qué tipos de arrays tienes, si NumPy se encuentra en una situación en la que se arruinaría la precisión, lanzará una excepción.
Por último, debes tenér en cuenta que al asignar valores en los arrays 3D, estos deben estar entre 0 y 255, de lo contrario se producirá alguna truncación indefinida.
Graduación¶
Bueno, ahí está. Mi breve introducción a Numeric Python y surfarray. Espero que ahora veas lo que es posible, y aunque nunca los uses por ti mismo, no tengas miedo cuando veas código que los use. Echale un vistazo al ejemplo vgrade para ver más sobre los arrays numéricos. También, hay algunos demos "flame" que usan surfarray para crear un efectp de fuego en tiempo real.
Lo mejor que podés hacer es probar alguna cosas por tu cuenta. Ve despacio al principio y ve construyendo poco a poco, ya he visto algunas cosas geniales con surfarray, como gradientes radiales y más. Buena suerte.
Edit on GitHub