Sobre o OpenCV

Para a execução dos códigos citados nesta página foi utilizado o OpenCV (Open Source Computer Vision), que é uma biblioteca open-source muito usada em aplicações de visão computacional, processamento de imagens e vídeos. Na linguagem Python, esta biblioteca possui uma boa integração com o módulo numérico NumPy, o qual oferece suporte para criação de matrizes multi-dimensionais e um amplo leque de funções para manipulação de arrays. Nas versões mais recentes do OpenCV, esses objetos ndarray já são utilizados nativamente como uma forma de otimização.

1ª Unidade

2.2. Exercícios

2.2.1 Região negativa

  • Implementar um programa capaz de solicitar ao usuário as coordenadas de dois pontos P1 e P2 localizados dentro dos limites do tamanho da imagem e exibir que lhe for fornecida. Entretanto, a região definida pelo retângulo de vértices opostos definidos pelos pontos P1 e P2 será exibida com o negativo da imagem na região correspondente.

A imagem utilizada como base está sendo mostrada na Figura 1.

biel
Figure 1. biel.png

Apesar da imagem em questão ser representada em tons de cinza, a solução deste exercício foi pensada para uma imagem genérica e, portanto, optamos por ler a imagem em matrizes de cores utilizando a flag cv2.IMREAD_COLOR.

img_orig = cv2.imread('biel.png', cv2.IMREAD_COLOR)

Extraímos, então, as dimensões da imagem e o número de canais utilizando o atributo shape.

altura, largura, canais = img_orig.shape

Na sequência, solicitamos ao usuário as coordenadas dos pontos de interesse P1 e P2. A partir das coordenadas fornecidas, percorremos os pixels que compõem o retângulo entre esses dois pontos, invertendo os tons de todos os canais.

for i in range(min(p1x, p2x), max(p1x, p2x)+1):
    for j in range(min(p1y, p2y), max(p1y, p2y)+1):
        for k in range(canais):
            img_neg[i,j,k] = 255 - img_neg[i,j,k]

Para exibir o resultado, precisou-se utilizar a função cv2_imshow(), a qual é uma versão compatível com o Google Colab de cv2.imshow(). O resultado final tomando-se a Figura 1 como entrada e considerando os pontos P1=(90, 190) e P2=(220, 120) está exposto na Figura 2.

biel neg
Figure 2. Região negativa em imagem sugerida

Aplicando como entrada uma imagem colorida, obtivemos o seguinte resultado.

beatles neg
Figure 3. Região negativa em imagem colorida

2.2.2 Troca de quadrantes

  • Implementar um programa que troque os quadrantes em diagonal na imagem.

Da mesma forma que o exercício anterior, lemos a imagem e extraímos os seus dados essenciais. Vale ressaltar que para que a troca dos quadrantes seja possível as dimensões da imagem devem ser pares. Adicionamos, portanto, duas condicionais: caso uma das duas dimensões da imagem seja ímpar, descartaremos a última linha e/ou coluna do array.

if altura % 2 != 0:
    altura -= 1
    img_orig = img_orig[:-1,::]
if largura % 2 != 0:
    largura -= 1
    img_orig = img_orig[::,:-1]

Em seguida, calculamos a metade das dimensões para uso posterior e criamos um novo array para armazenar a imagem trocada.

Por fim, invertemos os quadrantes da figura selecionando as regiões da imagem de origem e atribuindo-as às suas novas posições utilizando slicing de arrays, como mostra o trecho de código abaixo.

# quarto quadrante = segundo quadrante
img_trocada[meia_altura:,meia_largura:] = img_orig[:meia_altura,:meia_largura]
# terceiro quadrante = primeiro quadrante
img_trocada[meia_altura:,:meia_largura] = img_orig[:meia_altura,meia_largura:]
# primeiro quadrante = terceiro quadrante
img_trocada[:meia_altura,meia_largura:] = img_orig[meia_altura:,:meia_largura]
# segundo quadrante = quarto quadrante
img_trocada[:meia_altura,:meia_largura] = img_orig[meia_altura:,meia_largura:]

Exibimos, então, o resultado armazenado em img_trocada, obtendo a figura final, como pode ser visto a seguir.

biel trocada
Figure 4. Troca de quadrantes em imagem sugerida

Como a lógica do programa foi pensada para uma imagem genérica, aplicou-se uma figura colorida com dimensões de 814 x 543 pixels, a fim de verificar o seu funcionamento. O resultado é apresentado na Figura 5.

lago trocada
Figure 5. Troca de quadrantes em imagem colorida

O código completo pode ser encontrado em: trocaregioes.ipynb.

3.2. Exercícios

3.2.1 Rotulação de objetos

  • Observando-se o programa labeling.cpp como exemplo, é possível verificar que caso existam mais de 255 objetos na cena, o processo de rotulação poderá ficar comprometido. Identifique a situação em que isso ocorre e proponha uma solução para este problema.

Existe uma limitação de valores em 255 tons de cinza distintos em razão do tipo de variável que está sendo utilizada (unsigned char). A proposta de solução é utilizar uma matriz do tipo int ou float para aumentar as possibilidades de contabilização. Para além disso, poderíamos também adaptar o algoritmo para o sistema RGB, possibilitando a criação de 255³ rótulos, que são diferenciáveis na visão computacional.

3.2.2 Contagem de objetos

  • Aprimore o algoritmo de contagem apresentado para identificar regiões com ou sem buracos internos que existam na cena. Assuma que objetos com mais de um buraco podem existir. Inclua suporte no seu algoritmo para não contar bolhas que tocam as bordas da imagem. Não se pode presumir, a priori, que elas tenham buracos ou não.

Como devemos levar em consideração que podem existir bolhas com mais de um buraco, a imagem a ser utilizada como base para este código é mostrada na Figura 6.

bolhas
Figure 6. Entrada do programa

Inicialmente, lê-se o arquivo e as dimensões da imagem. Logo após, percorremos todos os pixels de borda, aplicando um flood fill da cor do fundo (tom 0) sempre que nos depararmos com um pixel branco no percurso.

for i in range(altura):
    if img_aux[i,0] == 255:
        cv2.floodFill(img_aux, None, (0,i), 0)
        num_bolhas_borda += 1
    if img_aux[i,largura-1] == 255:
        cv2.floodFill(img_aux, None, (largura-1,i), 0)
        num_bolhas_borda += 1
for j in range(largura):
    if img_aux[0,j] == 255:
        cv2.floodFill(img_aux, None, (j,0), 0)
        num_bolhas_borda += 1
    if img_aux[altura-1,j] == 255:
        cv2.floodFill(img_aux, None, (j,altura-1), 0)
        num_bolhas_borda += 1

A variável num_bolhas_borda armazena o número de bolhas que foram excluídas da figura e é incrementada sempre que chamamos a função cv2.floodFill(). A nova imagem, dada por img_aux, é mostrada na Figura 7.

bolhas1
Figure 7. Imagem sem bolhas de borda

Na sequência, faremos o labeling das bolhas presentes na nova imagem. Para isso, percorremos todos os pixels da figura, procurando novamente por pixels brancos. Ao encontrarmos uma nova bolha, incrementamos a variável num_bolhas e aplicamos o flood fill com o tom correspondente ao valor atual do contador.

num_bolhas = 0
for i in range(altura):
    for j in range(largura):
        if img_aux[i,j] == 255:
            num_bolhas += 1
            cv2.floodFill(img_aux, None, (j,i), num_bolhas)

Ao fim deste processo, conheceremos a quantidade total de bolhas e cada uma delas estará sendo representada por um tom de cinza diferente, como exposto na Figura 8.

bolhas2
Figure 8. Labeling das bolhas

Para realizar a contagem das bolhas com buracos, iniciamos pintando o fundo de branco, aplicando um flood fill no ponto (0,0), de modo que apenas os buracos apresentem tom de cinza 0.

bolhas3
Figure 9. Alteração da cor de fundo

Varremos novamente a figura, dessa vez buscando os pixels pretos e aplicando neles um flood fill com a cor do fundo. Verificamos, então, o pixel antecedente: caso este seja diferente de 220, incrementamos a variável num_bolhas_buracos e pintamos a bolha que contém o buraco em questão com o tom de cinza 220; caso contrário, temos que a bolha já foi contabilizada.

num_bolhas_buracos = 0
for i in range(1, altura):
    for j in range(1, largura):
        if img_aux[i,j] == 0:
            cv2.floodFill(img_aux, None, (j,i), 255)
            if img_aux[i,j-1] != 220:
                cv2.floodFill(img_aux, None, (j-1,i), 220)
                num_bolhas_buracos += 1

O trecho de código acima garantirá que nenhuma bolha seja contabilizada mais de uma vez e fará com que as bolhas com buracos fiquem destacadas na figura, como vemos abaixo.

bolhas4
Figure 10. Indentificação das bolhas com buracos

Obtemos, enfim, a seguinte saída: A figura tem 7 bolhas com buracos e 14 bolhas sem buracos. O código completo pode ser encontrado em: labeling.ipynb.

4.2. Exercícios

4.2.1 Equalização de histograma

  • Implementar um programa que deverá, para cada imagem capturada, realizar a equalização do histograma antes de exibir a imagem. Teste sua implementação apontando a câmera para ambientes com iluminações variadas e observando o efeito gerado. Assuma que as imagens processadas serão em tons de cinza.

Para realizar a captura de imagens através de uma câmera (webcam), utilizamos a função cv2.VideoCapture().

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

Em segunda instância, definimos o tamanho da tela de exibição como (512 x 480) e o número de tonalidades como 256, podendo apresentar valores de 0 a 255 (o segundo valor de hist_range é exclusive).

hist_size = 256
hist_w, hist_h = 512, 480
hist_range = (0, 256)

O parâmetro bin_w define a largura ocupada por cada tom de cinza no histograma.

bin_w = int(round(hist_w/hist_size))

Na sequência, convertemos cada frame capturado para escala de cinza e realizamos a equalização com a função cv.equalizeHist(). A partir de agora, os processos ocorrerão em loop para todos os frames coletados em tempo real enquanto não existir uma condição de interrupção do programa.

frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame_eq = cv2.equalizeHist(frame)

Após a equalização dos tons de cinza, faremos o cálculo do histograma por meio da função cv2.calcHist() empregando os parâmetros previamente definidos como argumentos. Depois disso, normalizaremos os histogramas gerados com a função cv2.normalize().

hist = cv2.calcHist([frame], [0], None, [hist_size], hist_range)
cv2.normalize(hist, hist, alpha=0, beta=hist_h, norm_type=cv2.NORM_MINMAX)

hist_eq = cv2.calcHist([frame_eq], [0], None, [hist_size], hist_range)
cv2.normalize(hist_eq, hist_eq, alpha=0, beta=hist_h, norm_type=cv2.NORM_MINMAX)

Com isso, criamos um conjunto de barras verticais (representação de um trem de pulsos), que partem do eixo horizontal e vão até o ponto do valor calculado, a fim de representar a quantidade de pixels de cada tom de cinza do frame.

for i in range(hist_size):
    cv2.line(histImage, (bin_w*i, hist_h - int(hist[i])), (bin_w*i, hist_h), 255, thickness=2)
    cv2.line(histImage_eq, (bin_w*i, hist_h - int(hist_eq[i])), (bin_w*i, hist_h), 255, thickness=2)

Exibimos, então, a captura gerada pela webcam e o seu histograma:

normal
Figure 11. Captura original da webcam

Após a aplicação da equalização com o nosso programa, geramos o seguinte resultado:

equalizado
Figure 12. Captura equalizada

Nota-se que, como a captura foi realizada em um ambiente com boa iluminação, o histograma se encontra concentrado em uma região mais clara. Após equalização, percebemos uma distribuiçao mais uniforme das intensidades dos pixels na imagem gerada e um melhor constraste.

Para apreciar melhor os resultados da equalização, vamos introduzir uma imagem com pouco contraste, sendo a captura realizada em local com baixa iluminação. Note que os pixels estão agrupados, predominantemente, no lado mais a esquerda do histograma.

normal escuro
Figure 13. Captura original da webcam em ambiente de baixa iluminação

Abaixo visualizamos o resultado da equalização e do novo histograma obtido. Verifica-se uma clara redistribuição dos pixels imagem por todo o histograma.

Apesar de conseguirmos visualizar melhor alguns detalhes da imagem equalizada em comparação com a imagem escura, o ruído está muito presente.

equalizado escuro
Figure 14. Captura equalizada em ambiente de baixa iluminação

O código completo pode ser encontrado em: equalize.py.

4.2.2 Detector de movimento

  • Implementar um programa que deverá continuamente calcular o histograma da imagem (apenas uma componente de cor é suficiente) e compará-lo com o último histograma calculado. Ative um alarme quando a diferença entre estes ultrapassar um limiar pré-estabelecido, utilizando a função de comparação que julgar conveniente.

Este programa utiliza as alterações no histograma dos frames do vídeo capturado para detecção de movimentos. Partindo de uma estrutura semelhante ao do algoritmo anterior, calcularemos, para cada frame, o histograma da componente vermelha e o compararemos com o histograma que o antecedeu.

Precisamos, inicialmente, separar os canais da imagem:

bgr = cv2.split(frame)

Assim, calculamos e normalizamos o histograma atual do canal vermelho.

hist_atual = cv2.calcHist(bgr, [2], None, [hist_size], hist_range)
cv2.normalize(hist_atual, hist_atual, alpha=0, beta=hist_h, norm_type=cv2.NORM_MINMAX)

Para a condição inicial em que não temos um frame anterior para comparação, inicializamos uma variável inicio = True e atribuímos a hist_anterior o histograma atual que está sendo gerado. Terminada esta ação, a variável inicio passa a ser False.

if inicio:
    hist_anterior = hist_atual.copy()
    inicio = False

Os histogramas são comparados por meio da função compareHist() e o resultado é inserido na varivável dif, que representa o quanto o histograma do frame atual difere do anterior. Para tal, utilizamos a métrica cv2.HISTCMP_BHATTACHARYYA, a qual retorna valores entre 0.0 e 1.0, em que quanto mais próximo de 0.0, maior é a semelhança entre as imagens.

A partir disso, comparamos o valor encontrado (em %) ao limiar, que foi definido como 3 após alguns testes para alcançar a sensibilidade desejada. Quando este é ultrapassado, uma mensagem de Movimento detectado ! é exibida na imagem.

dif = cv2.compareHist(hist_anterior, hist_atual, cv2.HISTCMP_BHATTACHARYYA)
    if (100 * dif > 3):
        cv2.putText(frame, "Movimento detectado!", (10, 30), cv2.FONT_HERSHEY_TRIPLEX, 1, (0, 0, 255))

O resultado pode ser observado abaixo:

motiondetector
Figure 15. Detecção de movimento

O código completo pode ser encontrado em: motiondetector.py.

5.2. Exercícios

5.2.1 Laplaciano do gaussiano

  • Utilizando o programa filtroespacial.cpp como referência, implementar um programa que acrescente mais uma funcionalidade ao exemplo fornecido, permitindo que seja calculado o laplaciano do gaussiano das imagens capturadas. Compare o resultado desse filtro com a simples aplicação do filtro laplaciano.

Inicialmente, adaptamos o código citado no exercício para a linguagem Python. Nele, o usuário pode escolher o efeito (módulo, média, gaussiano, bordas horizontais, bordas verticais, laplaciano e boost) que deseja aplicar às imagens capturadas em tempo real por meio das teclas do teclado.

key = cv2.waitKey(10)

if key == 27:
    break
elif key == ord('a'):
    absolut = not absolut
  elif key == ord('m'):
    laplacian_gauss = False
    mask = np.float32(media)
  elif key == ord('g'):
    laplacian_gauss = False
    mask = np.float32(gauss)
  elif key == ord('h'):
    laplacian_gauss = False
    mask = np.float32(horizontal)
  elif key == ord('v'):
    laplacian_gauss = False
    mask = np.float32(vertical)
  elif key == ord('l'):
    laplacian_gauss = False
    mask = np.float32(laplacian)
  elif key == ord('b'):
    laplacian_gauss = False
    mask = np.float32(boost)
  elif key == ord('f'):
    laplacian_gauss = True
    mask = np.float32(gauss)
  elif key == ord('s'):
    cv2.imwrite('filtered_img.png', filtro_espacial)
    print("Imagem salva em filtered_img.png")

Nota-se que foram adicionadas duas novas funcionalidades ao programa: o filtro laplaciano do gaussiano e o salvamento em arquivo do frame atual com filtro aplicado.

Para realizar o laplaciano do gaussiano, foi criada uma flag (laplacian_gauss) que retorna True quando o comando (tecla f) é ativado. Dessa forma, primeiramente selecionamos a máscara adequada para aplicar o filtro gaussiano e, em seguida, criamos uma expressão condicional baseada na flag em questão a fim de aplicar o laplaciano em cima da imagem já filtrada previamente.

frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame_gray = cv2.flip(frame_gray, 1)

frame_32f = np.float32(frame_gray)
filtered_frame = cv2.filter2D(frame_32f, -1, mask)

if laplacian_gauss:
    filtered_frame = cv2.filter2D(filtered_frame, -1, laplacian)

Quando outras opções de filtro são escolhidas, a variável laplacian_gauss é setada para False e, portanto, nenhum processo adicional é realizado no frame.

Na sequência, verificamos se os valores mostrados devem ser absolutos ou não e transformamos os dados da matriz resultante que representa a imagem final em valores unsigned de 8 bits (0 a 255), para, só então, exibí-la em uma janela, assim como a imagem original.

if absolut:
    filtered_frame = np.abs(filtered_frame)

filtro_espacial = np.uint8(filtered_frame)

cv2.imshow('imagem original', frame_gray)
cv2.imshow('imagem com efeito', filtro_espacial)

Para comparar os efeitos do filtro laplaciano e laplaciano do gaussiano, utilizamos como entrada do programa a imagem exposta a seguir.

livro
Figure 16. Imagem utilizada no programa

O comparativo entre os dois resultados é mostrado na Figura 17. Percebe-se que, ao aplicarmos o filtro gaussiano, que realiza um borramento, antes do laplaciano, atenuamos os ruídos da imagem tornando as bordas mais precisas na figura resultante, dando a impressão de uma imagem mais escura.

laplgauss livro
Figure 17. Comparativo entre os filtros laplaciano e laplaciano do gaussiano

O código completo pode ser encontrado em: laplgauss.py.

6.1. Exercícios

6.1.1 Tilt-shift em imagem

  • Utilizando o programa addweighted.cpp como referência, implementar um programa no qual três ajustes deverão ser providos na tela da interface:

    • um ajuste para regular a altura da região central que entrará em foco;

    • um ajuste para regular a força de decaimento da região borrada;

    • um ajuste para regular a posição vertical do centro da região que entrará em foco. Finalizado o programa, a imagem produzida deverá ser salva em arquivo.

Para este código, inicialmente lemos a imagem de entrada do programa, armazenando-a na variável img1. Em seguida, a img2 é obtida aplicando-se cinco vezes o filtro da média com máscara 5x5 na imagem original, a fim de alcançar um efeito de borramento com intensidade desejável.

img_32f = np.float32(img2)
for n in range(5):
    img_32f = cv2.filter2D(img_32f, -1, mask)
img2 = np.uint8(img_32f)

Criamos a janela e também as barras de ajuste solicitadas no exercício utilizando a função cv2.createTrackbar(), definindo um slider que inicia em 0 e pode ir até 100. Um dos parâmetros exigidos para esta criação é uma função de callback, a qual será chamada sempre que o usuário interagir com a barra de rolagem.

cv2.namedWindow('Tiltshift')
cv2.createTrackbar('altura', 'Tiltshift', slider_inicial, slider_max, on_trackbar)
cv2.createTrackbar('centro', 'Tiltshift', slider_inicial, slider_max, on_trackbar)
cv2.createTrackbar('decaimento', 'Tiltshift', slider_inicial, slider_max, on_trackbar)

Ao chamar o callback, a função apresentada acima automaticamente envia como argumento o valor atual da barra que foi ajustada. Por esse motivo, a função on_trackbar() possui um argumento (val), mas este não é utilizado em seu interior, uma vez que optamos por usar a mesma função de callback para todas as três barras. Como alternativa, coletamos a posição atual de todos os sliders localmente, utilizando a função cv2.getTrackbarPos() sempre que há alteração.

def on_trackbar(val):
    global img_final

    slider_altura = cv2.getTrackbarPos('altura', 'Tiltshift')  # altura da região central
    slider_centro = cv2.getTrackbarPos('centro', 'Tiltshift')  # posição vertical do centro
    slider_decaimento = cv2.getTrackbarPos('decaimento', 'Tiltshift')

    l1 = slider_centro - int(slider_altura/2)
    l2 = slider_centro + int(slider_altura/2)

    if l1 >= 0 and l2 <= 100:
        l1 = l1*height/100
        l2 = l2*height/100
    else:
        return

    aux = img1.copy()

    x = np.arange(height, dtype=np.float32)

    alpha = 0.5 * (np.tanh((x - l1)/(slider_decaimento+0.001)) - np.tanh((x - l2)/(slider_decaimento+0.001)))

    for i, element in enumerate(alpha):
        aux[i] = cv2.addWeighted(img1[i], element, img2[i], 1 - element, 0.0)

    cv2.imshow('Tiltshift', aux)
    img_final = aux

Para simular o efeito do tilt-shift, fazemos uma soma ponderada entre a imagem e sua versão borrada por meio da função cv2.addWeighted(). Esse processo pode ser modelado por uma função que define a região de desfoque ao longo do eixo vertical da imagem:

\[\alpha(x) = \frac{1}{2}\left(\tanh{\frac{x-l_1}{d}}-\tanh{\frac{x-l_2}{d}} \right)\]

em que \(x\) representa as linhas da imagem, \(l_1\) e \(l_2\) são os limites verticais para a região sem borramento cujo \(\alpha\) assume valor em torno de 0.5 e \(d\) indica a força do decaimento da região normal para a região borrada.

Para evitar o erro de divisão por zero, foi acrescido ao decaimento um valor de 0.001. Além disso, antes de atribuir os novos parâmetros, verificamos se os valores de altura da região sem borramento e o centro determinado são compatíveis para que o tamanho da imagem não seja excedido.

Ao final, exibimos na janela o resultado dessa combinação linear e o armazenamos na variável global img_final, a fim de disponibilizar a imagem atual para ser salva em arquivo pelo comando s.

Para verificar o funcionamento do código, utilizamos a imagem abaixo como entrada do programa.

cidade
Figure 18. Imagem de entrada para o tilt-shift

Ao realizarmos o ajuste das barras (altura: 70; centro: 48; decaimento: 22), chegamos no resultado apresentado na Figura 19.

img tiltshift
Figure 19. Imagem de saída para o tilt-shift

O código completo pode ser encontrado em: tiltshift.py.

6.1.2 Tilt-shift em vídeo

  • Implementar um programa capaz de processar um arquivo de vídeo, produzir o efeito de tilt-shift nos quadros presentes e escrever o resultado em outro arquivo de vídeo. A ideia é criar um efeito de miniaturização de cenas. Descarte quadros em uma taxa que julgar conveniente para evidenciar o efeito de stop motion, comum em vídeos desse tipo.

Este caso é uma aplicação do exemplo acima, em que cada quadro do vídeo é processado como realizado anteriormente e alguns quadros são descartados para criar um efeito de stop motion.

O vídeo utilizado para exemplificação está disposto abaixo.

Vídeo original

O algoritmo usará os parâmetros de ajuste que são fornecidos pelo usuário através da mudança das barras de rolagem para calcular os valores de \(\alpha\), que é um vetor do tamanho da imagem. Esse efeito, que a princípio é gerado no frame inicial, repercutirá por todos os frames restantes compondo o vídeo final.

cap = cv2.VideoCapture(arq_video)

# lê primeiro frame para ajustar parâmetros do tilt-shift
ret, frame1 = cap.read()

Como o mesmo efeito será aplicado para todos os frames do vídeo, uma vez definida a configuração, não será necessário recalcular os parâmetros. Por esse motivo, tratamos o vetor \(\alpha\) como uma variável global que será constantemente atualizada pela função de callback das barras para, enfim, ser utilizada dentro da função tiltshift() repetidas vezes durante a criação do vídeo.

Essa função recebe como argumento o frame original e o frame borrado e retorna a imagem com o efeito do tilt-shift aplicado. O seu algoritmo segue a mesma lógica que o exercício anterior.

def tiltshift(frame1, frame2):
    img = frame1.copy()

    for i, element in enumerate(alpha):
        img[i] = cv2.addWeighted(
            frame1[i], element, frame2[i], 1 - element, 0.0)

    return img

Como requisito do programa, a função salvarvideo() grava o resultado do efeito aplicado no vídeo. O objetivo da função cv2.VideoWriter() é, portanto, salvar o vídeo final em arquivo, definindo o nome (video_tiltshift.mp4), extensão, taxa de frames por segundo e as dimensões do vídeo (iguais ao original).

Para criar o efeito de stop motion, uma condição descarta é imposta e a variável taxa determinará esse descarte. Nesse processo, os frames que são múltiplos do valor armazenado em taxa devem ser mantidos, ou seja, a cada 8 frames do vídeo original, extraímos 1 frame para o vídeo final.

def salvarvideo(video):
    taxa = 8
    descarta = 0

    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter('video_tiltshift.mp4', fourcc, 15, (width, height))

    cap = cv2.VideoCapture(video)

    while True:
        ret, frame1 = cap.read()
        if ret:
            if descarta == 0:
                frame2 = frame1.copy()

                # aplica borramento
                frame_32f = np.float32(frame2)
                for _ in range(5):
                    frame_32f = cv2.filter2D(frame_32f, -1, mask)
                frame2 = np.uint8(frame_32f)

                novo_frame = tiltshift(frame1, frame2)
                out.write(novo_frame)

                descarta += 1
                descarta = descarta % taxa
            else:
                descarta += 1
                descarta = descarta % taxa
        else:
            break

    out.release()
    print("Vídeo salvo como video_tiltshift.mp4")

Quando o usuário concluir a sua configuração, ele pode pressionar a tecla s para salvar o resultado no arquivo de vídeo video_tiltshift.mp4.

k = cv2.waitKey(0)
if k == 27:
    cv2.destroyAllWindows()
elif k == ord('s'):
    salvarvideo(arq_video)
    cv2.destroyAllWindows()

O resultado do efeito tilt-shift no vídeo mostrado previamente pode ser visualizado abaixo.

video tiltshift
Figure 20. Vídeo resultante com tilt-shift

O código completo pode ser encontrado em: tiltshiftvideo.py.

2ª Unidade

7.2. Exercícios

7.2.1 Filtro homomórfico

  • Utilizando o programa dft.cpp como referência, implementar o filtro homomórfico para melhorar imagens com iluminação irregular. Assumindo que a imagem fornecida é em tons de cinza, crie uma cena mal iluminada e ajuste os parâmetros do filtro homomórfico para corrigir a iluminação da melhor forma possível.

Neste exercício, resolveremos problemas de distribuição irregular de iluminação em imagens utilizando o filtro homomórfico.

O filtro homomórfico é uma versão modificada do filtro gaussiano e atua sobre as componentes de reflectância e iluminância da imagem, a partir da seguinte equação matemática:

\[H(u,v)=(\gamma _{H}-\gamma _{L})(1-e^{-c(D^{2}(u,v)/D_{0}^{2})})+\gamma _{L}\]

Os parâmetros do filtro (\(\gamma_{H}\), \(\gamma_{L}\), \(D_{0}\) e \(c\)) são ajustados pelo usuário a fim atingir o resultado desejado: reduzir os efeitos causados pela má iluminação na imagem original. O algoritmo implementado segue os passos descritos a seguir.

Primeiramente, realiza-se o padding da imagem e aplica-se a transformada de Fourier, transferindo a análise para o domínio da frequência.

Em seguida, desloca-se o centro do seu espectro, invertendo os quadrantes da imagem (A↔D, B↔C) por meio da função deslocaDFT().

def deslocaDFT(image):
    # se a imagem tiver tamanho ímpar, recorta a regiao para evitar cópias de tamanho desigual
    image = image[0:(image.shape[0] & -2), 0:(image.shape[1] & -2)]

    cy = int(image.shape[0] / 2)
    cx = int(image.shape[1] / 2)

    # reorganiza os quadrantes da transformada
    # A B   ->  D C
    # C D       B A
    A = image[:cy, :cx].copy()
    B = image[:cy, cx:].copy()
    C = image[cy:, :cx].copy()
    D = image[cy:, cx:].copy()

    # A <-> D
    image[:cy, :cx] = D
    image[cy:, cx:] = A

    # C <-> B
    image[:cy, cx:] = C
    image[cy:, :cx] = B

    return image

Na sequência, multiplica-se a função na frequência pelo filtro (\(G(u,v) = H(u,v)F(u,v)\)) e retoma-se a configuração original dos quadrantes da imagem invertendo nocamente os quadrantes.

Finalmente, calcula-se a transformada inversa do produto e extrai-se a parte real da imagem no domínio espacial.

Detendo-se ao filtro em si, definimos a função filtro_homomorfico() que implementará o equacionamento matemático. Como comentado anteriormente, o algoritmo usará os parâmetros do filtro, os quais são fornecidos pelo usuário através do controle das barras deslizantes, que variam de 0 a 100.

def filtro_homomorfico():
    gammaH = cv2.getTrackbarPos('gama H', 'filtro homomorfico') / 100
    gammaL = cv2.getTrackbarPos('gama L', 'filtro homomorfico') / 100
    D0 = cv2.getTrackbarPos('D0', 'filtro homomorfico')
    c = cv2.getTrackbarPos('c', 'filtro homomorfico') / 10

A variável tmp corresponde à matriz temporária utilizada para criar as componentes real e imaginária do filtro. Com isso, define-se D(u,v) e implemeta-se o filtro, exibindo o resultado em janela. Enfim, cria-se a matriz com as componentes do filtro e combina ambas em uma matriz multicanal complexa.

    tmp = np.zeros((dft_M, dft_N), dtype=np.float32)
    v = np.array(range(dft_N))

    for u in range(dft_M):
        D = np.sqrt((u-dft_M/2)**2 + (v-dft_N/2)**2)
        tmp[u] = (gammaH-gammaL)*(1-np.exp(-c*(D**2)/(0.0001+D0**2))) + gammaL

    cv2.imshow('filtro', tmp)

    comps = [tmp, tmp]
    filtro_h = cv2.merge(comps)

    return filtro_h

Para exemplificar, foi gerado um filtro com os seguintes parâmetros: \(\gamma_{H}=89\), \(\gamma_{L}=35\), \(D_{0}=70\) e \(c=96\), obtendo o resultando abaixo. Verifica-se que este facilita a passagem de componentes de frequências mais altas (reflectância) em detrimento das frequências mais baixas (iluminância).

filtro h
Figure 21. Filtro homomórfico gerado pelos parâmetros de entrada

A imagem original utilizada de teste é apresentada na Figura 22. Em seguida, é mostrada a imagem após a realização da filtragem homomórfica.

entrada homomorfico
Figure 22. Imagem original com problema de iluminação
foto corrigida
Figure 23. Imagem com parâmetros do filtro homomórfico aplicados

O código completo pode ser encontrado em: homomorfico.py.

8.3 Exercícios

8.3.1 Pontilhismo

  • Utilizando os programas canny.cpp e pontilhismo.cpp como referência, implemente um programa cannypoints.cpp. A ideia é usar as bordas produzidas pelo algoritmo de Canny para melhorar a qualidade da imagem pontilhista gerada. A forma como a informação de borda será usada é livre. Entretanto, são apresentadas algumas sugestões de técnicas que poderiam ser utilizadas:

    • Desenhar pontos grandes na imagem pontilhista básica;

    • Usar a posição dos pixels de borda encontrados pelo algoritmo de Canny para desenhar pontos nos respectivos locais na imagem gerada.

    • Experimente ir aumentando os limiares do algoritmo de Canny e, para cada novo par de limiares, desenhar círculos cada vez menores nas posições encontradas. A Figura 19 foi desenvolvida usando essa técnica.

  • Escolha uma imagem de seu gosto e aplique a técnica que você desenvolveu.

  • Descreva no seu relatório detalhes do procedimento usado para criar sua técnica pontilhista.

Neste programa, iremos simular a técnica do pontilhismo desenhando digitalmente pequenos círculos na imagem escolhida. Estes círculos serão separados por pequenos intervalos e deslocados de seu centro de forma randômica.

Inicialmente, definiremos a função pontilhismo() para aplicação da técnica desejada. As variáveis STEP, JITTER e RAIO são determinadas de antemão. Detalhando melhor esses parâmetros, temos que STEP define o passo usado para varrer a imagem de referência, JITTER regula o intevalo de separação (espalhamento) dos elementos e RAIO corresponde a distância entre um ponto de cada círculo gerado e seu centro.

xrange e yrange são arrays de índices que armazenam as coordenadas dos pontos em que vão ser colocados os círculos do pontilhismo, preenchidos com valores sequenciais iniciando em 0, além de receberem um ganho igual a STEP e um deslocamento STEP//2.

def pontilhismo(image):
    STEP = 4
    JITTER = 4
    RAIO = 3

    points = image.copy()
    rows, cols = image.shape[:-1]

    xrange = np.arange(0, rows-STEP, STEP) + STEP//2
    yrange = np.arange(0, cols-STEP, STEP) + STEP//2

    np.random.shuffle(xrange)

    for i in xrange:
        np.random.shuffle(yrange)
        for j in yrange:
            x = i + np.random.randint(2*JITTER) - JITTER + 1
            y = j + np.random.randint(2*JITTER) - JITTER + 1
            color = tuple(map(int, image[x, y]))
            points = cv2.circle(points, (y, x), RAIO, color, -1, lineType=cv2.LINE_AA)
    return

Após embaralhar aleatoriamente os pontos através da função random.shuffle(), os loops mostrados acima fazem com que as variáveis i e j assumam, a cada iteração, os valores dos arrays xrange e yrange de forma consecutiva.

A imagem que será utilizada para aplicação da técnica de pontilhismo é mostrada abaixo:

jardim
Figure 24. Imagem original

Para melhorar a qualidade das imagens que geraremos com a técnica do pontilhismo, podemos utilizar a detecção de bordas de Canny. O algoritmo de Canny é um dos mais rápidos e eficientes algoritmos de detecção de bordas ou descontinuidades. A saída desse algoritmo é uma imagem binária em que todas as bordas são representadas com valor 255 (branco) e os demais pixels com valor 0 (preto).

A trackbar está associada à função on_trackbar_canny(). Nessa função, são gerados círculos a partir dos pixels de borda detectados, de modo a evidenciar os contornos. Com isso, denota-se que o limiar \(T_2\) empregado é determinado pelo usuário a partir da interação com o slider, enquanto o \(T_1\) é obtido considerando a proporção de 3:1.

def on_trackbar_canny(slider):
    global img_final
    img_final = pontos.copy()

    for raio in [4,3,2,1]:
        fator = (5-raio)/2
        bordas = cv2.Canny(img, fator*slider, 3*fator*slider)

        px, py = np.where(bordas == 255)

        for i in range(0, len(px), 2):
            x, y = px[i], py[i]
            color = tuple(map(int, img[x, y]))
            img_final = cv2.circle(img_final, (y, x), raio, color, -1, lineType=cv2.LINE_AA)

    cv2.imshow('canny', img_final)

O processo é repetido quatro vezes e, à medida em que aumentam-se os limiares, são criados círculos menores. Para impedir a repetição excessiva, os círculos são desenhados alternadamente sobre os pontos que compõem as bordas da imagem, evitando os pontos consecutivos.

Após aplicação, os resultados com variação de threshold podem ser observados abaixo:

jardim pontilhismo
Figure 25. Resultado com variação de threshold

A seguir, visualizamos outro teste realizado e a imagem aplicada a técnica de pontilhismo, com threshold de 20.

outono pontilhismo
Figure 26. Entrada e saída do programa utilizando threshold de 20

O código completo pode ser encontrado em: cannypontilhism.py.

9.2 Exercícios

9.2.1 Clusterização com k-means

  • Utilizando o programa kmeans.cpp como exemplo, prepare um programa exemplo onde a execução do código se dê usando o parâmetro nRodadas=1 e inciar os centros de forma aleatória usando o parâmetro KMEANS_RANDOM_CENTERS ao invés de KMEANS_PP_CENTERS. Realize 10 rodadas diferentes do algoritmo e compare as imagens produzidas. Explique porque elas podem diferir tanto.

O k-means é um processo de quantização que visa classificar N observações em K aglomerados. É um processo iterativo em que extrai-se a distância média das amostras em cada aglomerado, redefinindo as posições dos centroides a cada passo. Esse processo acontece até não termos mais mudanças significativas nas posições dos centroides.

No programa em questão, implementaremos o procedimento descrito acima começando com centroides aleatoriamente determinados, devido à escolha do parâmetro KMEANS_RANDOM_CENTERS. Definido na variável criteria, o critério de parada se dá ao atingir o número máximo de 10000 iterações ou o erro \(\epsilon\) de valor 0,0001.

criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_MAX_ITER, 10000, 0.0001)

for n in range(1, 11):
    _, rotulos, centros = cv2.kmeans(samples, nClusters, None, criteria, nRodadas, cv2.KMEANS_RANDOM_CENTERS)

    centros = np.uint8(centros)

    rotulada = np.zeros(img.shape, dtype=img.dtype)
    for y in range(rows):
        for x in range(cols):
            indice = rotulos[y + x*rows, 0]
            for z in range(3):
                rotulada[y, x][z] = centros[indice][z]

Conforme solicitado, o algoritmo é rodado dez vezes, gerando dez imagens distintas. Como os centros dos agrupamentos são escolhidos, inicialmente, de forma aleatória, esses pontos mudarão a cada rodada, promovendo imagens relativamente diferentes. Isto ocorre devido à natureza não determinística do método k-means, a qual pode levar a diferentes valores de convergência de acordo com os pontos de partida ou, em alguns casos, sequer encontrar uma convergência.

As imagens geradas a partir da Figura 24 foram aglomeradas em um arquivo .gif, a fim de compararmos os resultados obtidos em cada iteração.

clustered img
Figure 27. Imagens clusterizadas

O código completo pode ser encontrado em: kmeans.py.

3ª unidade

Projeto final (Scanner inteligente)

Proposição

Sabe-se que os seres humanos possuem a capacidade de compreender o conteúdo de uma imagem apenas pela observação e interpretação de símbolos. Assim, podemos facilmente reconhecer um texto contido em uma imagem e fazer a sua leitura.

No dia a dia, frequentemente necessitamos da digitalização dessas informações, principalmente quando lidamos com documentos, fazendo-se necessário processar, extrair o texto e armazená-lo de forma editável. Nesse contexto, é bastante conveniente e torna-se muito mais fácil e rápido fazer determinados trabalhos, pesquisar uma dada informação ou manipular o conteúdo de um relatório, por exemplo, quando fazemos essa conversão de forma automática.

Assim, o algoritmo do Scanner Inteligente foi idealizado com o intuito de extrair automaticamente textos impressos e dados de documentos digitalizados em tempo real. Esse recurso integra os assuntos abordados na disciplina de Processamento Digital de Imagens e faz o reconhecimento óptico de caracteres (OCR), buscando o melhor enquadramento e disposição do conteúdo do documento de interesse e convertendo o texto detectado nas imagens em arquivo de texto (.txt), usando bibliotecas Python.

Implementação

A descrição detalhada da implementação deste algoritmo, bem como os resultados obtidos, podem ser acessados clicando aqui.

example scanner
Figure 28. Scanner de documentos inteligente

O código completo pode ser encontrado em: scanner.py.