PyOpenNI + PyGame

Luego de haber instalado todas las bibliotecas correspondientes para nuestra Kinect .. muchos tutoriales ya hay dando vueltas [1] [2][3], Nos encontramos con PyOpenNI y PyGame para demostrar lo sencillo que es interactuar con sensores 3D en menos de 30′

Veremos rápidamente el código por bloques …
Primero que nada tenemos las clases básicas de nuestro juego, la clase Ball, que será el elemento de nuestro simple juego, BallManager que será la encargada de actualizar e instanciar todas las pelotitas deseadas, una clase para la Kinect y otra para el manejo del juego en general (Clase Game).

La clase Ball, se encarga de darle imagen, posición, velocidad y ángulo a nuestra pelota, dandole funciones como move y bounce para calcular hacia donde y cómo debe moverse.

class Ball(pygame.sprite.Sprite):
    """Ball Class for set image, speed and angle """
    def __init__(self, xy, speed, angle):
        pygame.sprite.Sprite.__init__(self)
        self.img_load('images/sphere.png')
        self.rect.centerx, self.rect.centery = xy
        self.speed =  speed
        self.angle = angle
        
    def move(self):
        self.rect.centerx += np.sin(self.angle) * self.speed
        self.rect.centery -= np.cos(self.angle) * self.speed
        
    def bounce(self):
        if self.rect.centerx > SCREEN_WIDTH - self.rect.width:
            self.rect.centerx = 2*(SCREEN_WIDTH - self.rect.width) - self.rect.centerx
            self.angle = -self.angle*2
        elif self.rect.centerx < self.rect.width:
            self.rect.centerx = 2*self.rect.width - self.rect.centerx
            self.angle = -self.angle*2
        if self.rect.centery > SCREEN_HEIGHT - self.rect.height:
            self.rect.centery = 2*(SCREEN_HEIGHT - self.rect.height) - self.rect.centery
            self.angle = np.pi - self.angle*2    
        elif self.rect.centery < self.rect.height:
            self.rect.centery = 2*self.rect.width - self.rect.centery
            self.angle = np.pi - self.angle*2
        
    def img_load(self, filename):
        self.image = pygame.image.load(filename)
        self.image = pygame.transform.scale(self.image, (60,60))  
        self.rect = self.image.get_rect()
        

La clase ballManager se encarga de instanciar y actualizar la posición de todas las pelotas que tenemos en el juego ..

class BallManager(object):
    """ Create and update state of balls"""
    def __init__(self, numballs = 30, balls = []):      
        self.blist = balls
        self.multiple_balls(numballs)

    def update(self):
        for ball in self.blist:
            ball.move()
            ball.bounce()

    def add_ball(self, xy, speed, angle):
        self.blist.append(Ball(xy, speed, angle))

    def multiple_balls(self, numballs):
        for i in range(numballs):
            self.add_ball((np.random.randint(0, SCREEN_WIDTH),
                          np.random.randint(0, SCREEN_HEIGHT)),
                          np.random.randint(4, 20),
                          np.random.uniform(0, np.pi*2))

En la clase Kinect tenemos la creación de contextos y generadores para indicar cuales son los datos que queremos tomar de nuestro sensor, gracias a openni ya contamos con generadores de gestos, por lo que podemos avisar de antemano cual es el gesto que esperamos reconocer (en nuestro caso un saludo agitando la mano).

class Kinect(object):
    """Manage context and generator of the kinect"""
    def __init__(self, game):

        self.context = openni.Context()
        self.context.init()
        self.depth_generator = openni.DepthGenerator()
        self.depth_generator.create(self.context)
        self.depth_generator.set_resolution_preset(openni.RES_VGA)
        self.depth_generator.fps = 30

        self.image_generator = openni.ImageGenerator()
        self.image_generator.create(self.context)
        self.image_generator.set_resolution_preset(openni.RES_VGA)

        self.gesture_generator = openni.GestureGenerator()
        self.gesture_generator.create(self.context)
        self.gesture_generator.add_gesture('Wave')

        self.hands_generator = openni.HandsGenerator()
        self.hands_generator.create(self.context)

        self.gesture_generator.register_gesture_cb(self.gesture_detected, self.gesture_progress)
        self.hands_generator.register_hand_cb(self.create, self.update, self.destroy)

        self.game = game

    def gesture_detected(self, src, gesture, id, end_point):
        print "Detected gesture:", gesture
        self.hands_generator.start_tracking(end_point)

    def gesture_progress(self, src, gesture, point, progress):
        pass

    def destroy(self, src, id, time):
        pass

    def create(self, src, id, pos, time):
        pass

    def update(self, src, id, pos, time):
        if pos != None:
            for ball in self.game.ball_manager.blist:
                new_rect = pygame.Rect(int(ball.rect.x), int(ball.rect.y), 60, 60)
                kinect_rect = pygame.Rect(self.game.size[0]/2 - int(pos[0]), self.game.size[1]/2 - int(pos[1]), 40, 40)
                print "kinect %s and ball %s" %(kinect_rect.center, new_rect.center)
                if new_rect.colliderect(kinect_rect):
                    print "wiii %d" %(len(self.game.ball_manager.blist))
                    ball.kill()
            pygame.draw.circle(self.game.display_surf, (0, 128, 255), (self.game.size[0]/2 - int(pos[0]), self.game.size[1]/2 - int(pos[1])), 10)

    def capture_rgb(self):
        rgb_frame = np.fromstring(self.image_generator.get_raw_image_map_bgr(), dtype=np.uint8).reshape(SCREEN_HEIGHT, SCREEN_WIDTH, 3)
        image = cv.fromarray(rgb_frame)
        cv.Flip(image, None, 1)
        cv.CvtColor(cv.fromarray(rgb_frame), image, cv.CV_BGR2RGB)
        self.game.frame = pygame.image.frombuffer(image.tostring(), cv.GetSize(image), 'RGB')

Vale decir que no es nada limpio en cuestiones objetosas que el update de colisiones se haga dentro de la clase Kinect, pero recuerden que la idea es mostrar que en 20 min de codeo sale andando un jueguito básico.

Y como último nuestra clase Game, que se encarga de las acciones básicas de un main de juego, el inicio, renderizado, el loop general .. etc.

class Game(object):
    """Define screen, sprites and states of the game"""
    def __init__(self):
        self.timer = pygame.time.Clock()
        self.sprites = pygame.sprite.RenderUpdates()
        self._running = True
        self.display_surf = None
        self.background = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
        self.background.fill((0,0,0))
        self.size = (SCREEN_WIDTH, SCREEN_HEIGHT)
        self.frame = None
        self.my_kinect = Kinect(self)
        self.ball_manager = BallManager(30)
        for ball in self.ball_manager.blist:
            self.sprites.add(ball)

    def on_init(self):
        pygame.init()
        self.display_surf = pygame.display.set_mode(self.size, pygame.HWSURFACE | pygame.DOUBLEBUF)
        self.display_surf.blit(self.background, (0,0))
        self._running = True
        self.my_kinect.context.start_generating_all()

    def on_event(self, event):
        if event.type == pygame.QUIT:
            self._running = False

    def on_loop(self):        
        self.my_kinect.context.wait_any_update_all()
        self.my_kinect.capture_rgb()
        self.ball_manager.update()

    def on_render(self):
        self.sprites.clear(self.display_surf, self.background)
        self.display_surf.blit(self.frame, (0,0))
        for sprite in self.sprites:
                sprite.update()
        dirty = self.sprites.draw(self.display_surf)
        pygame.display.update(dirty)
        pygame.display.flip()

    def on_cleanup(self):
        pygame.quit()
 
    def on_execute(self):
        self.on_init()
        while( self._running ):
            for event in pygame.event.get():
                self.on_event(event)
            self.on_loop()
            self.on_render()
        self.on_cleanup()

El código completo se puede encontrar en https://github.com/celiacintas/KinectStuff/blob/master/game_colegios.py

Y un video super acelerado … porque digamos que como juego no es el más entretenido

Log 0 – Manipulación Básica de Imagenes

La suma de valores a cada pixel puede ser utilizado para obtener,
Ajuste de Contraste, sumando un valor a cada pixel de una imagen incrementa su valor, por lo tanto aumenta su brillo.


from PIL import Image

myIm = Image.open("tucan.jpg")

myImResta = myIm.point(lambda x: x-100)
myImSuma = myIm.point(lambda x: x+100)

myImResta.save("contraste-100.png")
myImSuma.save("contraste+100.png")

En esta imagen vimos suma y resta de valores constantes.

Otra operación interesante es el ‘blending‘ .. que consiste en sumar dos imagenes (ponderando una mas que la otra con un alpha constante).

from PIL import Image

fox = Image.open("fox.jpg")
buddy = Image.open("fox2.jpg")

# para realizar blending deben tener el mismo tamano
buddy.resize(fox.size)

out = Image.blend(fox, tucan, 0.40)
# out = image1 * (1.0 - alpha) + image2 * alpha

out.save("myFoxyBlending.jpg")

En cuanto a operaciones lógicas una de las mas simples, como lo es la Negación , podemos inferir facilmente que esta operación invierte la representación de la imagen.
En imagenes binarias, el negro se convierte en blanco y viceversa.
En imagenes de color o escala de grises, el proceso consiste en aplicar a cada pixel $(MAX – p(x,y))$, donde $MAX$ es el valor máximo posible.

from PIL import Image, ImageChops

myImC = Image.open("tucan.jpg") #imagen color

myGreyTucan = myImC.convert("L")
myBWTucan = myImC.convert("1")

out = ImageChops.invert(myImC) #Invertido Color
out2 = ImageChops.invert(myGreyTucan) #Invertido Color
out3 = ImageChops.invert(myBWTucan) #Invertido Blanco y Negro

También podemos jugar con los espacios de color …

La representación de los colores en una imagen digital se logra usando una combinación de uno o más canales.

La representación que utilizamos para almacenar los colores, especificando numero y ‘naturaleza’ del canal es conocido como espacio de color.

 from PIL import Image
 from pylab import subplot,bar,savefig

 myIm = Image.open("tucan.jpg")
 myHis = myIm.histogram()

 subplot(333)
 bar(range(256), myHis[0:256], ec = 'r')
 subplot(336)
 bar(range(256), myHis[256:512], ec = 'g')
 subplot(339)
 bar(range(256), myHis[512:768], ec = 'b')

 savefig("myHIsto.png")
 

RGB es un espacio tridimensional, por lo que podemos imaginarlo como un cubo, con ejes R,G y B, todos los ejes poseen el mismo rango (escalado de 0-255 para 1 byte por canal, asignando 24 bit para la representación de la imagen).

El color negro lo encontramos en el origen del cubo $RGB(0,0,0)$ , el cual corresponde a la ausencia de color y el color blanco en la esquina opuesta $RGB(255,255,255)$ indicando la máxima cantidad de los tres colores.
El espacio de color RGB esta basado sobre la porción del espectro visible de los humanos.

Otro espacio con el que podemos experimentar es HSV,

El espacio de color ‘apreciable’ es una forma alternativa para representar imagenes en su verdadero color de una forma más natural para el humano que el espacio RGB.
En este espacio nos concentramos en el :

  • Matiz, es la longitud de onda dominante del color, (rojo, azul o verde).
  • Saturación, muestra el nivel de “pureza” del color, la cantidad de “luz blanca” mezclada con el color.
  • Valor, es el brillo del color , luminancia.

La representación HSV de una Imagen , al igual que RGB es un arreglo tridimensional, todo pixel de la imagen $f(x,y)$ posee una tupla del tipo $(h,s,v)$.
Podemos ver en el siguiente ejemplo la división de estos tres canales.

import cv
myIm = cv.LoadImage("tucan.jpg")
myHSV = cv.CreateImage(cv.GetSize(myIm),8,3)
cv.CvtColor(myIm, myHSV, cv.CV_RGB2HSV)

cv.SaveImage("myHSV.png", myHSV)

#Con esto tenemos la trasnformacion de RGB a HSV
#Ahora dividimos los canales

h_myHSV = cv.CreateImage(cv.GetSize(myIm), 8, 1)
s_myHSV = cv.CreateImage(cv.GetSize(myIm), 8, 1)
v_myHSV = cv.CreateImage(cv.GetSize(myIm), 8, 1)

cv.Split(myHSV, h_myHSV, s_myHSV, v_myHSV, None)

cv.SaveImage("h.png",h_myHSV)
cv.SaveImage("s.png",h_myHSV)
cv.SaveImage("v.png",h_myHSV)

 y como en el caso anterior podemos realizar un merge y ver que se vuelve a la imagen inicial.
cv.Merge(h_myHSV, s_myHSV, v_myHSV,None, myIm)
cv.SaveImage("myMergeHSV.png",myIm)

Estos y los siguientes logs forman parte de ejemplos básicos  (de un trabajo) para que no queden en /dev/null 🙂