sexta-feira, 26 de maio de 2017

Medindo consumo de memória em Python

O gerenciamento de memória de programas em Python é feito inteiramente pela linguagem. Isto significa que, por vezes, pode ser difícil controlar o uso de memória e não costuma ser claro o quanto cada objeto ocupa na memória nem quando seu espaço é liberado. Descreverei neste post o uso do módulo memory_profiler, que contém ferramentas para medir o consumo de memória de scripts Python.

Antes de tudo, o memory_profiler pode ser instalado via pip usando o seguinte comando. A opção -U pode ser usada para instalar o módulo na área do usuário em sistemas Unix-like.

$ pip install memory_profiler

Para usar este módulo basta decorar a função que desejamos medir o consumo de memória com o decorador @profile e executar o programa usando

$ python -m memory_profiler arquivo.py

Este comando imprime um relatório linha a linha mostrando o consumo de memória. Quando objetos são criados o consumo de memória aumenta, enquanto quando eles saem de contexto o consumo pode diminuir. O ponto fraco deste modo de execução é que ele só mostra o consumo medido "entre" as linhas do programa. Ou seja, se uma função for chamada e, durante sua execução alocar e liberar uma grande quantidade de memória estes valores não são contabilizados.

Também é disponibilizado o comando mprof, que executa um script e mede o consumo total de memória em pequenos intervalos de tempo. mprof, porém, não discrima onde a memória está sendo usada.


Irei exemplificar o uso do mprof com dois exemplos tirados da TRIOSlib, a biblioteca que desenvolvi durante meu doutorado. O objetivo da biblioteca é usar aprendizado de máquina para tratar problemas de processamento de imagens (mais detalhes aqui).

Uma das tarefas que mais consome memória na TRIOSlib é a extração de características. Um padrão é extraído de cada ponto das imagens de treinamento. Conjuntos de treinamento contendo várias centenas de milhares de padrões são comuns, mesmo usando um pequeno número de imagens de treinamento.

A TRIOSlib implementa dois modos para extração de características. No primeiro uma matriz é alocada com uma linha por pixel das imagens de treinamento. Cada padrão observado é armazenado em uma linha e, se houverem repetições, o mesmo padrão é armazenado diversas vezes. Neste caso o consumo depende do número de pixels das imagens de treinamento. Nos referimos a este modo como modo Array.

No segundo modo a matriz é trocada por um dicionário cujas chaves são padrões observados nas imagens. Se houverem repetições não há consumo extra de memória. Neste caso o consumo de memória depende do número de padrões únicos nas imagens. Quanto maior a repetição de padrões menor será o consumo relativo ao primeiro modo. Nos referimos a este tipo de execução como modo Dicionário ou Dict.

Apesar do memory_profiler possuir um modo que mostra o consumo de memória linha a linha, para operações longas como o treinamento de operadores pode ser mais interessante usar o comando mprof. Apesar de não ser possível identificar qual estrutura está consumindo memória, podemos ver o quanto a memória cresce e diminui conforme o programa executa. Também conseguimos, por meio do decorador @profile, marcar o início e o fim da execução de funções no código.

Para executar um script usamos o comando mprof run arquivo.py. Podemos plotar um gráfico com a última execução usando mprof plot. Usaremos o código abaixo como exemplo.

import trios
import trios.feature_extractors
import trios.classifiers

import numpy as np
from sklearn.tree import DecisionTreeClassifier

import sys


@profile
def main(ordered=True):
    imgset =
trios.Imageset.read('/media/igor/Data1/datasets/staffs/level1.set')
    testset =
trios.Imageset.read('/media/igor/Data1/datasets/staffs/test-
small.set')
    win = np.ones((7, 7), np.uint8)

    wop = trios.WOperator(win,
trios.classifiers.SKClassifier(DecisionTreeClassifier(),
ordered=ordered), trios.feature_extractors.RAWFeatureExtractor(win))

    dataset = wop.extractor.extract_dataset(imgset, ordered=ordered)
    if ordered:
        print(dataset[1].shape[0])
    else:
        print(len(dataset))

    tr2 = profile(wop.classifier.train)
    tr2(dataset, {})
    wop.trained = True

    print(wop.eval(testset[:1]))

if __name__ == '__main__':
    main(sys.argv[1] == 'ordered')

O decorator @profile irá marcar no gráfico o início e fim das funções main e WOperator.Classifier.train. (Lembre-se que um decorador é simplesmente uma função que retorna outra função. Fazemos isto na linha 21 para obter uma função de treinamento com @profile.) A escolha do modo de extração de características é feita pelo parâmetro ordered, passado pela linha de comando. ordered=True implica no uso de uma matriz para os padrões. ordered=False usa um dicionário. Veja abaixo o consumo de memória medidos em uma execução usando os dois modos.

Fig 1: Modo Array

Fig 2: Modo Dict

O consumo máximo de memória do modo Array é cerca de 4 vezes maior do que o modo Dict! A principal diferença entre os dois modos de execução é a quantidade de memória usada pelo método train. Enquanto o modo Array executa a extração de características muito mais rápido que o modo Dict, o treinamento é muito mais custoso tanto em termos de tempo quanto em memória. Isto pode ser explicado pelo fato de que o modo Dict condensa todas as observações repetidas em uma só linha ao treinar. Isto resulta em uma matriz de treinamento com 137.453 linhas, contra 5.595.585 linhas quando usamos o modo Array. A redução no número de linhas é possível atribuindo ao peso de cada exemplo de treinamento único o número de repetições observadas nas imagens. Infelizmente a maioria dos classificadores do scikit-learn não oferece suporte a definir pesos para cada instância de treinamento. Classificadores baseados em árvores de decisão (encontrados no pacote sklearn.tree) possuem este suporte e, como podemos ver isto faz uma grande diferença ao trabalhar com dados em que há repetição nos padrões de entrada observados.


Neste texto exemplifiquei a utilização do memory_profiler para medição de consumo de memória em Python e do decorador @profile para marcar o início e o fim da execução de diversas funções. Como pudemos ver, utilizar estruturas adequadas pode diminuir significativamente o consumo de memória e permitir processar uma maior quantidade de dados em memória.

sexta-feira, 19 de maio de 2017

Espaços de cores

Imagens digitais coloridas podem ser codificadas em uma série de formatos, chamados de espaços de cores. Dependendo da aplicação pode ser mais conveniente trabalhar com um espaço de cores em particular. Neste texto iremos explorar dois espaços de cores bastante usados em Processamento de Imagens: RGB e HSV/HSL. Também iremos comentar brevemente sobre outros espaços de cores existentes e suas características.

O espaço de cores RGB

No espaço RGB uma cor é representada por uma tripla \((r, g, b)\) que codifica a quantidade de vermelho (r), verde (g) e azul(b) presente na cor. Este é um espaço de cores conveniente para a criação de telas pois mapeia facilmente para o sistema visual humano, que contém células receptoras para estas três cores. A cor \((0, 0, 0)\) representa o preto, enquanto \((1, 1, 1)\) representa o branco. Imagens em RGB são comumente representadas em computadores usando 1 byte para cada cor. Isto resulta em imagens em que (255, 255, 255) representa a cor branca.

RGB color solid cube.png
Espaço de cores RGB representado como um cubo. Por SharkD [fonte]


Podemos fazer segmentação de cores no espaço RGB de duas maneiras:

  1. Selecionando cores em um intervalo \([(r_1, b_1, g_1), (r_2, b_2, g_2)]\);
  2. Selecionando cores usando uma esfera ou elipse em torno de uma cor modelo.

Um dos problemas de fazer detecção de cores usando o espaço RGB é que a cor, assim como sua intensidade, fica espalhada nos três canais. No caso da opção 1, identificar os intervalos \([(r_1, b_1, g_1), (r_2, b_2, g_2)]\) pode ser uma tarefa trabalhosa. Os cantos do cubo formado pelo intervalo podem ser cores bem diferentes da desejada se optarmos por incluir todas variações da cor de interesse. Por outro lado, se evitarmos falsos positivos perdemos também muitas cores que são bastante similares à cor de interesse.

No caso da opção 2 precisamos definir um raio \(r\) (ou três raios \(r_r, r_b, r_g\) para a elipse) que contenha as cores desejadas. Apesar desta solução ser mais robusta, continua sendo necessário definir valores para estes parâmetros.

O espaço de cores HSV

Os espaços de cores HSV separam uma cor em três componentes: Hue (Matiz, de \(0^o\) a \(360^o\)), representando a cor do pixel; Saturação (\([0,1]\)), representando quanto a cor é pura; e Valor (\([0,1]\)), representando o brilho da cor. O canal H contém a cor vermelha em \(0^o\), passando por roxo, azul, ciano, verde, amarelo, laranja e vermelho novamente (próximo a \(360^o\)). Saturação pequena indica que a cor é próxima de um tom de cinza (incluindo preto e branco), enquanto Saturação \(1\) indica uma cor "forte". Um Valor pequeno representa cores escuras, enquanto um Valor (próximo de \(1\)) representa cores brilhantes.


Espaço de cores HSV representado como um cone. Por SharkD [fonte]


Diferente de imagens em RGB, que são comumente armazenadas em um uint8, cada implementação/biblioteca pode usar intervalos diferentes para cada um dos canais. Não se esqueça de consultar a documentação!

Uma das maiores desvantagens do espaço RGB é a dificuldade de modificar cores. Por exemplo, como podemos "escurescer" um cor? Como podemos escolher um laranja mais avermelhado do que outro? Ambas tarefas são facilmente realizadas no espaço HSV.

A segmentação de cores também é mais simples no espaço HSV. Ela pode ser feita com um simples intervalo no canal H. Para evitar a detecção de variantes muito escuras ou clares também podem ser adotados valores mínimos para Saturação e Valor.

Uma imagem RGB pode ser convertida para HSV usando a seguintes fórmulas. Primeiro definimos as constantes \(M = \max(R, G, B)\), \(m = \min(R, G, B)\) e \(C = M - m\).

A Matiz (Hue) é definida abaixo. Adotamos \(H = 0\) para tons de cinza.

\begin{equation} H' = \begin{cases} 0, & \textrm{if } C = 0 \\ \frac{G-B}{C} \mod 6, & \textrm{if } M = R \\ \frac{B-R}{C} + 2, & \textrm{if } M = G \\ \frac{R-G}{C} + 4, & \textrm{if } M = B \\ \end{cases} \end{equation} \begin{equation} H = 60^o \times H' \end{equation}

O Valor é definido como

\begin{equation} V = M. \end{equation}

A Saturação é definida como

\begin{equation} S = \begin{cases} 0 & \textrm{if } V = 0 \\ \frac{C}{V} & \textrm{otherwise} \end{cases} \end{equation}

Uma variante do HSV é chamada de HSL e troca o Valor pela Luminância (quantidade de luz recebida). Apesar da troca de um canal, a detecção de cores não é afetada significativamente por esta mudança.

Imagens em níveis de cinza

Apesar de não ser exatamente um espaço de cores (já que só temos tons de cinza), mostramos nesta seção como converter imagens em RGB para níveis de cinza (abreviado para GL, do inglês gray-level).

Cada pixel em uma imagem em GL representa a quantidade de luz observada naquela ponto da imagem. Se nenhuma ou muito pouca luz é observada o pixel toma o valor \(0\) (preto). Se muita luz é observada o pixel toma o valor \(1\) (branco).

Uma maneira popular de converter RGB para GL é usando a fórmula abaixo. Ela é inspirada no sistema visual humano e pesa mais fortemente o verde que o vermelho e azul.

\begin{equation} L = 0.587G + 0.299R + 0.114B \end{equation}

Outros espaços de cores (CMIK etc)

RGB é um sistema aditivo, ou seja, cores são criadas pela emissão de luz. O aumento de uma componente de uma cor implica em um aumento na emissão de luz nesta frequência. Em comparação, CMIK é um sistema subtrativo, ou seja, cores são criadas pela absorção da luz. Logo, CMIK é um formato muito presente na indústria de impressão, pois ele permite representar melhor cores em materiais impressos.

SubtractiveColor.svg
Mistura de cores por subtração - Por SharkD, Jacobolus e Dacium [fonte]


O espaço CMIK não é comumente usado em Processamento de Imagens e Visão Computacional. Ele está listado principalmente por curiosidade e completude. É importante saber que existem espaços de cores adequados para diversas aplicações.

Existem diversos outros espaços de cores (veja uma lista aqui) e cada um possui um caso de uso ideal. Apesar de RGB e HSV serem os mais usados em Processamento de Imagens, o conhecimento de outros espaços de cores pode ser um diferencial.

Bibliotecas para processamento de imagens em Python

Por fim, listo aqui algumas bibliotecas para Processamento de Imagens e Visão Computacional em Python.

  • OpenCV - usado principalmente para aplicações de Visão Computacional, com ênfase no processamento de imagens em tempo real. Feito em C++, mas com excelentes wrappers para Python.
  • Scikit-Image - Biblioteca parte do pacote Scipy para computação científica em Python. Integra muito bem com Numpy e tem diversos algoritmos de processamento de imagens já implementados. Mais completa que OpenCV em termos de quantidade de algoritmos implementados.
  • Pillow - Uma biblioteca um pouco mais antiga de processamento de imagens usando Python. Pode ler uma grande quantidade de formatos de imagens.

sexta-feira, 12 de maio de 2017

Detecção de faces usando OpenCV

A detecção de faces é uma tarefa muito comum em diversas programas de visão computacional. O Facebook, por exemplo, detecta faces para sugerir a identificação de pessoas nas fotos adicionadas por seus usuários. Várias câmeras digitais detectam rostos para melhorar a qualidade da fotografia. Esta câmera, por exemplo, tira fotos quando as pessoas sorriem.

O OpenCV é uma biblioteca muito usada em aplicações de visão computacional, especialmente aquelas que requerem interação em tempo real. Python é uma das linguagens com suporte oficial e possui uma interface bastante prática para trabalhar com detecção de faces.

Antes de tudo, vamos diferenciar duas tarefas muito parecidas a primeira vista: detecçãos e reconhecimento/identificação.

  • Detecção de faces consiste em dizer onde existe uma face em uma imagem. Ou seja, a localização da face na imagem é o resultado final. A entrada é uma imagem ``natural'' que pode ou não conter uma face.
  • Reconhecimento/Identificação consiste em dizer quem está retratado na imagem. Ou seja, o nome de uma pessoa específica é o resultafo final. Normalmente a entrada já é uma imagem contendo somente a face que desejamos identificar.

Detecção de faces em imagens

Nós humanos conseguimos reconhecer faces instintivamente. Podemos descrever uma face, listar quais são as partes (olhos, nariz, boca, ect) de uma face e mesmo reconhecer a mesma pessoa em presença de objetos externos (óculos, chapéu, etc). Apesar de existir, aparentemente, uma programação ``natural'' para que possamos identificar rostos, conseguimos fazer o mesmo tipo de reconhecimento com qualquer objeto que observamos. Ou seja, aprendemos a reconhecer objetos baseado em exemplos e conseguimos descrevê-los internamente de modo a reconhecer variações dos objetos.

A primeira etapa de um sistema de detecção de faces consiste em extrair um conjunto de características que possam diferenciar imagens com faces de imagens sem faces. Dois métodos muito populares estão presentes no OpenCV: Haar Cascades e LBP. Como as faces podem ter vários tamanho, é feita uma análise multi-escala da imagem, ou seja, executamos o mesmo algoritmo de detecção de faces em versões redimensionadas da imagem. Desta maneira, mesmo que um objeto seja muito pequeno ou muito grande, ele será detectado em alguma escala.

Para ``aprendermos'' um objeto precisamos observar alguns exemplos positivos (variações do objeto de interesse), e exemplos negativos (outros objetos diferentes, incluindo alguns que possam ser confundidos com o de interesse). No caso da detecção de faces, precisamos então de conjuntos de imagens que contenham faces e de imagens sem faces. Esta etapa, chamada de treinamento, é uma das mais importantes de um sistema de detecção de faces. O resultado final desta etapa é um classificador que diz se uma imagem possui ou não uma face. Este classificador é aplicado em todos os pontos da imagem para podermos dizer onde estão localizadas as faces.

Por fim, carregamos o classificador treinado na etapa anterior e usamos para encontrar faces. Esta etapa é a única que ocorre durante a execução do sistema de reconhecimento em si. O treinamento sempre é feito em um momento anterior, de preferência supervisionado por um especialista.

Resumindo, um detecção de faces ocorre em dois momentos. No primeiro, o treinamento, usamos exemplos de imagens para ``aprender'' o que é uma face. No segundo usamos o modelo aprendido para detectar faces em imagens. Ao invés de usarmos os próprios pixels para representar as faces extraímos um conjunto de características que facilitem decidir se uma imagem é ou não uma face.

Implementação no OpenCV

O treinamento é uma etapa muito custosa, tanto devido à construção do conjunto de imagens exemplo quanto à determinação do melhor classificador e extrator de características. Felizmente o OpenCV já possui alguns classificadores treinados e prontos para uso. A classe CascadeClassifier é usada para detecção de faces. Suas duas principais funções são load, que carrega um modelo treinado, e detectMultiScale, que faz a detecção e devolve uma lista de retângulos que contém faces.

Os modelos treinados podem ser baixados do repositório do OpenCV no Github. Existem dois tipos de modelos pré-treinados,haarcascades e lbpcascades, para diversas situações. Estamos interessados em faces vistas de frente, então usaremos o modelo lbpcascades/lbpcascades_frontalface_improved.xml. Poderíamos usar também o equivalente encontrado na pasta haarcascades, porém nos testes que fiz o detector baseado em LBP funcionou melhor.

O código abaixo carrega o modelo já treinado (salvo com o nome faces_lbp.xml), o aplica em uma imagem carregada do disco e desenha quadrados nas faces de todos os presentes.

import cv2

# Carrega imagem
img = cv2.imread('foto.jpg')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Carrega detector de faces usando LBP.
cls = cv2.CascadeClassifier('faces_lbp.xml')
faces = cls.detectMultiScale(img_gray)

for face in faces:
    x, y, w, h = face
    # Argumentos: imagem, topo_esquerdo, baixo_direito, cor(BGR),
espessura
    cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), 2 )

cv2.imwrite('foto_face.png', img)
Funciona mesmo!

Esta foi uma pequena introdução sobre detecção de faces usando OpenCV. Possivelmente farei novos posts sobre processamento de imagens e visão computacional no futuro, então se gostou compartilhe ou deixe um comentário. Até a próxima.

Mais informações

Os seguintes artigos podem ser ótimas primeiras leituras para maiores detalhes em detecção de faces.

Viola, P., & Jones, M. (2001). "Rapid object detection using a boosted cascade of simple features". In Computer Vision and Pattern Recognition, 2001. CVPR 2001.

Puttemans Steven, Can Ergun and Toon Goedeme, "Improving Open Source Face Detection by Combining an Adapted Cascade Classification Pipeline and Active Learning", VISAPP 2017

Resumo sobre LBP (bom, este não é clássico... mas é uma explicação simples do que é LBP.)