Back-End

28 jun, 2016

Criando um weather snapshot em Python

Publicidade

Reusando o código que escrevi pra tirar snapshots durante a PyConSe e publicar automaticamente no Twitter, escrevi um pequeno aplicativo pra Raspberry Pi com Python pra pegar o mesmo tipo de imagem (só que desta vez da minha janela) e ir acompanhando a evolução do tempo ao longo do dia e do ano.

animated-weather-2016

Acho que será legal fazer uma animação das imagens mostrando o sol que brilha até quase 11 da noite, o inverno que escurece às 2 da tarde, e a neve chegando. E tudo postando no Twitter.

As ferramentas são as mais simples possível: um Raspberry Pi conectado com um dongle wifi e uma webcam USB creative (que aliás uso pra participar dos hangouts). E sempre Python pra fazer tudo.

raspberrypi-1

raspberrypi-2

Descobri que o Forecast.IO fornece uma API com JSON pra buscar a previsão do tempo atual e até 10 dias, com permissão de mil queries por dia de forma gratuita. Perfeito pro meu pequeno projeto. O mais difícil foi fazer a conversão da temperatura de Farenheit pra Celsius (meus dias de vestibulando já se foram faz muito tempo), mas pedi ajuda à Internet pra isso. Fiz uma pequena função que retorna os dados que quero em forma de um array.

import requests
import json
import time

"""
Um monte de código por aqui
[...] 
""""
def get_content():
    timestamp = time.strftime("Date: %Y-%m-%d %H:%M", time.localtime())
    msg = []
    msg.append("Stockholm")
    msg.append(timestamp)

    url = "https://api.forecast.io/forecast/%s/%s" % (wth_key, wth_loc)
    req = requests.get(url)
    jdata = json.loads(req.text)

    summary = jdata["currently"]["summary"]
    temp = jdata["currently"]["temperature"]
    temp = Far2Celsius(temp)

    msg.append(u"Temperature: %s°C" % temp)
    msg.append("Summary: %s" %summary)

    return msg

A primeira coisa que precisei alterar foi a adição de textos à imagem. Tendo a informação vinda do Forecast.IO, eu precisava modificar a imagem pra que ela aparecesse. No início, eu usei uma fonte de cor branca, mas logo percebi que preto ficava com um contraste melhor.  Mas quando chegar o inverno, época em que os dias são realmente muito curtos por aqui, vou precisar pensar numa forma pra trocar para branco. Mas no momento usei as bibliotecas do PIL que manipulam imagem em Python.

import Image
import ImageFont, ImageDraw, ImageOps

IMGSIZE = (1280, 720)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

"""
Um monte de código por aqui
[...] 
""""
def WeatherScreenshot():

    msg = get_content()
    if not msg:
        msg = "Just another shot at %s" % \
            time.strftime("%H:%M", time.localtime())
    if msg:
        msg_body = "\n".join(msg[1:])
        im = Image.open(filename)
        # just get truetype fonts on package ttf-mscorefonts-installer
        try:
            f_top = ImageFont.truetype(font="Arial", size=60)
        except TypeError:
            # older versions hasn't font and require full path
            arialpath = "/usr/share/fonts/truetype/msttcorefonts/Arial.ttf"
            f_top = ImageFont.truetype(arialpath, size=60)
        try:
            f_body = ImageFont.truetype(font="Arial", size=20)
        except TypeError:
            # older versions hasn't font and require full path
            arialpath = "/usr/share/fonts/truetype/msttcorefonts/Arial.ttf"
            f_body = ImageFont.truetype(arialpath, size=20)
        txt = Image.new('L', IMGSIZE)
        d = ImageDraw.Draw(txt)
        d.text( (10, 10), msg[0], font=f_top, fill=255)
        position = 80
        for m in msg[1:]:
            d.text( (10, position), m, font=f_body, fill=255)
            position += 20
        w = txt.rotate(0, expand=1)

        im.paste(ImageOps.colorize(w, BLACK, BLACK), (0,0), w)
        im.save(filename)

Descobri que a versão de raspbian que estou usando, baseado em Debian Wheezy, tem uma API um pouco diferente e pode precisar que a fonte com o path completo seja passada no argumento.

Outra alteração foi mudar a chamada pra webcam capturar a imagem que era uma função, mas modifiquei pra uma thread. Assim, o tempo fica consistente. Do contrário, ao invés de mostrar 12:00, apareceria algo como 12:03 (o tempo pra adquirir a imagem).

import threading

def WeatherScreenshot():
    th = threading.Thread(target=GetPhoto)
    th.start()
    msg = get_content()
    th.join()

E já que mencionei a imagem, esse foi o maior problema até agora.  Descobri que não existe uma forma muito confiável de inicializar a webcam. Às vezes, ela adquiri a imagem de forma bonitinha, às vezes fica super exposta, outras vezes sub.

2016-06-05_170057

E não tem nada que dê um feedback sobre a qualidade. Li vários artigos com dicas de uso com pygame, que é a forma que uso, e com opencv também, mas todas com o mesmo princípio. Basicamente fazem um start() no framework da webcam, que inicializa a webcam, adquirem um número de imagens aleatórios (alguns dizem 30) e esperam pelo melhor ao capturar a imagem. Nada que retorne um indicador de qualidade. Nada.

DISCARDFRAMES = 2 * 30

def GetPhoto():
    filename = None
    pygame.init()
    pygame.camera.init()
    elif os.path.exists("/dev/video0"):
        device = "/dev/video0"
    if not device:
        print "Not webcam found.  Aborting..."
        sys.exit(1)
    # you can get your camera resolution by command "uvcdynctrl -f"
    cam = pygame.camera.Camera(device, IMGSIZE)
    cam.start()
    time.sleep(3)
    counter = 10
    while counter:
        if cam.query_image():
            break
        time.sleep(1)
        counter -= 1
    # idea from https://codeplasma.com/2012/12/03/getting-webcam-images-with-python-and-opencv-2-for-real-this-time/
    # get a set of pictures to be discarded and adjust camera
    for x in xrange(DISCARDFRAMES):
        while not cam.query_image():
            time.sleep(1)
        image = cam.get_image()
    image = cam.get_image()

Basicamente um método de tentativa e erro; por isso que iniciei a chamada à webcam como thread. Como as webcams USB têm CPU própria, não existe – até onde pesquisei – uma API confiável pra verificar se o balanço de branco normalizou antes de capturar a imagem. Só retornam a própria imagem. Tosco.

Então resolvi fazer um outro script como módulo, que basicamente mapeia toda a imagem em seu tamanho e cria um dicionário do tipo “COR: quantas vezes”. Descobri que valores RGB (pega o valor de R + G + B, soma e divide por 3 pra ter a média) acima de 235 já indicam super exposição. Não só isso: como eu conto a quantidade que aquele valor RGB aparece, sempre que um valor sobressai acima de 15% do total, já indica uma imagem ruim. Não é um dos melhores métodos científicos, mas tem funcionando bem (verifiquei nas imagens já adquiridas e salvas).  Os tempos de aquisição de imagem mudaram de até 1 minuto pra em torno de 10 minutos. Mas, por enquanto, com qualidade muito melhor.

import Image

def brightness(filename):
    """
    source: http://stackoverflow.com/questions/6442118/python-measuring-pixel-brightness
    """
    img = Image.open(filename)
    #Convert the image te RGB if it is a .gif for example
    img = img.convert ('RGB')
    RANK = {}
    #coordinates of the pixel
    X_i,Y_i = 0,0
    (X_f, Y_f) = img.size
    #Get RGB
    for i in xrange(X_i, X_f):
        for j in xrange(Y_i, Y_f):
            #print "i:", i,",j:", j
            pixelRGB = img.getpixel((i,j))
            R,G,B = pixelRGB
            br = sum([R,G,B])/ 3 ## 0 is dark (black) and 255 is bright (white)
            if RANK.has_key(br):
                RANK[br] += 1
            else:
                RANK[br] = 1

    color_order = []
    pic_size = X_f * Y_f
    print "Picture size:", pic_size
    for k in sorted(RANK, key=RANK.get, reverse=True):
        amount = RANK[k]
        # if low than 15%, ignore
        if amount < (.15 * pic_size):
            continue
        print k, "=>", RANK[k]
        color_order.append(k)
    if color_order:
        print color_order
        return -1
    return 0

O código todo está disponível no meu GitHub.

E provavelmente devo lançar um gif animado posteriormente com o decorrer do clima ao longo do ano.

Tenho alguns problemas como concorrência no caso de tentar adquirir uma imagem ao mesmo tempo que a crontab tentar fazer isso (implementei uma API em REST pra isso, mas não é algo pra publicar). Devo implementar algum tipo de lock usando /tmp, mas algo simples.

E agora no verão, com sol até quase 11 horas da noite, tenho também um pequeno problema de negação de serviço que às vezes acontece.

2016-06-04_200119

raspberrypi-4

Ainda não descobri um módulo em Python pra mitigar isso =/