Matheus Petrovich <matheus.petrovich@gmail.com>
Leandro Campelo <leandrocampelo92@gmail.com>
Lista de Exercício: Unidade I
Exercício 3: Manipulando pixels em uma imagem
1-Criando Região Negativa
O programa a seguir recebe como entrada uma imagem e coordenadas (x0,y0) e (x1,y1) para desenhar uma região dentro da qual as cores serão invertidas. Esse efeito é obtido através da substituição do valor de um pixel por seu complemento para 255 (se anteriormente o valor fosse 10, vira 245, por exemplo). A imagem abaixo foi colocada como entrada, juntamente com as coordenadas (50,250) e (250, 350).

A imagem será varrida somente entre as linhas e colunas definidas na entrada pelos pontos (x0,y0) e (x1,y1), e somente entre essas que os valores serão substituídos pelo complemento. A imagem abaixo reflete a saída do exemplo acima.

2-Trocando Regiões
Este problema deve receber como entrada uma imagem, e ter como saída a imagem com os quatro quadrantes espelhados diagonalmente. O superior esquerdo no inferior direito e vice versa, o superior direito no inferior esquerdo e vice versa. Para esse fim inicialmente declaramos na imagem de entrada 4 regiões de interesse por meio do código abaixo:
(...) //carregamento da imagem e tratamento de erro
int r = image.rows, c = image.cols;
Mat tmp(image,Rect(0,c/2,r/2,c/2));
Mat tmp1(image,Rect(r/2,c/2,r/2,c/2));
Mat tmp2(image,Rect(c/2,0,r/2,c/2));
Mat tmp3(image,Rect(0,0,r/2,c/2));
(...) //troca das regioes
então criamos uma imagem de saída como o clone da imagem de entrada, e colamos as regiões de interesse nas posições corretas, como visto no código abaixo
(...) //definição das regiões de interesse
Mat out = image.clone();
//inf esq
tmp.copyTo(out(Rect(r/2,0,c/2,r/2)));
//sup esq
tmp1.copyTo(out(Rect(0,0,c/2,r/2)));
//sup dir
tmp2.copyTo(out(Rect(0,c/2,c/2,r/2)));
//inf dir
tmp3.copyTo(out(Rect(r/2,c/2,c/2,r/2)));
imwrite("trocado.png",out);
imshow("janela", out);
waitKey();
return 0;
}
Como exemplo usamos a imagem abaixo como entrada:

E obtivemos a seguinte imagem como saída:

Exercício 4: Preenchendo Regiões
1-Contando Acima de 256 Objetos
Considere a figura abaixom, há somente dois tons de cinza (0 e 255) presentes nela, na qual os objetos têm tom de cinza 255 e o fundo tem tom de cinza 0.

Nesta questão fizemos uma busca por objetos percorrendo toda a imagem, da esquerda para direita e de cima para baixo, quando achamos um objeto na imagem executamos o algoritmo floodfill()
e mudamos o tom de cinza dele para um que seja diferente de 0 e 255 (100, por exemplo) e incrementamos o contador. Assim ocorrerá para todos os objetos que tenham tom de cinza igual a 255. Quando terminar a varredura da imagem, o valor do contador será a quantidade de objetos presentes na imagem e os objetos contados ficarão com o tom de cinza 100, mais escuro do que o original. Na figura a seguir podemos ver o resultado do processamento da imagem.

Fixamos a rotulação em apenas um tom de cinza porque só existem 256 possibilidades de tons de cinza que um computador consegue distinguir, se houvesse mais de 255 objetos na imagem e o processo de rotulação continuasse como estava antes (quando achasse um novo objeto acrescentava a ele um tom de cinza a mais) o floodfill()
iria ficar comprometido, pois quando a contagem chegasse em um valor cujo tom de cinza do objeto fosse igualado ou ultrapassasse 255 o mesmo objeto poderia ser contado mais de uma vez, pois valores de tons de cinza maiores do que 255 iriam ser automaticamente normalizados para 255, assim causando mais contagens do que objetos.
As modificações feitas para a realização deste exercício foi:
-
Declarar uma constante para setar o tom de cinza em 10 para fazer com que os objetos ficassem cor um tom de cinza diferente do objeto e do plano de fundo após a realização do
floodfill()
.
const int TomDeCinzaDez = 10; (1)
-
Passar ela como parâmetro.
floodFill(image,p,TomDeCinzaDez); (2)
-
Acrescentar um cout para imprimir no terminal quantos objetos há na imagem.
cout << "Foram encontrados " << nobjects << " objetos nesta imagem." << endl; (3)
Como podemos ver na imagem abaixo:

O código na íntegra pode ser encontrado aqui
2-Contagem de Objetos com e sem Buracos
O projeto a seguir recebe uma imagem binária como entrada, objetos sendo brancos e o fundo preto, e conta quantos desses objetos têm buracos (um ou mais) e quantos não têm. Uma vez que vamos estar contando dois tipos distintos de objetos, trocamos a rotulação por dois contadores. Um exemplo de entrada é a imagem a seguir:

Para retirar os objetos que estão em contato com as bordas, inicialmente o programa desenha uma moldura interna no tom dos objetos, 255, e em seguida executa um floodfill para a cor de fundo a partir do (0,0), resultando na imagem a seguir:

Uma vez que desejamos reconhecer os objetos com buracos, inicialmente diferenciamos os buracos do fundo, levando o fundo para 100. Agora a imagem será varrida em busca de objetos com buracos. Quando um pixel branco tiver sido precedido por um pixel preto, isso indica que acabamos de sair de um buraco. Uma vez que o fundo passou por floodfill para o tom 100, qualquer pixel preto necessáriamente pertence a um buraco. O objeto sofrerá floodfill para a cor do fundo (para nao ser contabilizado mais de uma vez), o contador nobjectsCB é incrementado, e a varredura segue.

Quando o programa chega ao fim da imagem, uma segunda varredura ocorrerá, procurrando por pixels brancos. Quaisquer pixeis brancos remanescentes na imagem pertecem a objetos sem buracos. Quando um pixel com tom 255 for encontrado na varredura ele passará por floodfill para a cor do fundo e o contador nobjectsSB será incrementado. O output do programa para a imagem de exemplo pode ser visto abaixo:
$ ./labelling bolhas.png > sem: 13, com: 8
O código na íntegra pode ser encontrado aqui
Exercício 5: Manipulação de Histograma
1-Equalização
Este exercício tem como objetivo equalizar imagens (frames) capturadas por uma câmera em tempo real e mostrar os seus respectivos histogramas. Para realizar este exercício temos que ter uma noção do que é a equalização de imagens. Equalização é um método que melhora significativamente o contraste da imagem, fazendo com que a visualização de imagens muito escuras ou muito claras se torne melhor. Ela faz a distribuição de ocorrências das cores no histograma, fazendo com que as cores que estão muito concentradas em uma região do histrograma sejas distribuídas por toda a faixa do histograma, fazendo com que certos locais da imagem que antes tinham difícil visualização possam ser destacadas. Temos um exemplo disso logo abaixo:


Para realizar este exercício foi necessário utilizar a função split()
para separar os canais e armazená-los nos vetores ‘planes[]’ (vetores das componentes R, G e B) para equalizá-los de forma separada através da função equalizeHist()
, após a equalização ser feita em cada uma das componentes de cor, é feito o cálculo do histograma para cada uma delas através da função calcHist()
. Logo depois, fazemos a operação reversa através da função merge()
juntando os canais em uma única matriz. Depois disso é feito a normalização e o plot dos histogramas dos 3 canais no canto superior esquerdo do frame.
A modificação no código do tutorial para a realização deste exercício foi:
-
A adição da função
equalizeHist()
.
void equalizeHist(InputArray src, OutputArray dst);
Onde: src: fonte de imagem de 8 bits com único canal de entrada. dst: imagem de destino com mesmo tamanho e tipo do src.
equalizeHist(planes[0], planes[0]); equalizeHist(planes[1], planes[1]); (1) equalizeHist(planes[2], planes[2]);
Faz a equalização em cada canal da imagem separadamente.
-
A adição da função
merge()
.
void merge(InputArrayOfArrays mv, OutputArray dst);
Onde: mv: array de entrada ou vetor de matrizes que foram fundidas; todas as matrizes em mv devem ter o mesmo tamanho e a mesma profundidade. dst: matriz do mesmo tamanho e a mesma profundidade que a saída mv; O número de canais será o número total de canais no conjunto de matriz.
merge(planes, image); (2)
Junta os canais em uma única matriz multi-canal.
O código na íntegra pode ser encontrado aqui
2-Detector de Movimentos
O projeto a seguir utiliza alterações no histograma dos frames de um vídeo para detectar movimentos na cena. Para um detector mais básico, utilizamos apenas uma das componentes de cor da imagem. As entradas para o programa são um valor que indica a sensibilidade do detector, e um intervalo de tempo para regravar a referencia (em milisegundos). Para cada frame, o histograma da componente vermelha é calculado e é então comparado com o histograma do frame de referência. A Diferença entre cada tom é calculada e acumulada em uma variável dif, que é comparada ao limiar. Quando o limiar é ultrapassado, um círculo indicativo vermelho é desenhado no centro da tela.
(...)
for(int i=0; i<nbins; i++){
dif += abs(histV.at<float>(i)-histR.at<float>(i)); //acumula para calcular diferenca media
line(histImgR, Point(i, histh),
Point(i, cvRound(histR.at<float>(i))),
Scalar(0, 0, 255), 1, 8, 0);
}
if(dif > thres) //se a diferenca for acima do limite permitido
circle(image,Point(image.cols/2,image.rows/2),200, Scalar(0,0,255),-200); //ativa alarme
dif = 0; // reseta dif pro proximo frame
(...)
O intervalo de atualização é medido antes do loop infinito do streaming de vídeo e novamente no fim do loop, se o tempo passado for múltiplo do intervalo, a referência é atualizada.
(...)
saida = (int) clock() - (time_1); (1)
saida2 = (double) ((saida) / (double) CLOCKS_PER_SEC); (2)
saida2 *= 1000.0; (3)
if((int)saida2%intervalo<100){ (4)
histV = histR.clone(); (5)
}
}
return 0;
}
1 Calcula a diferença de clocks do começo até agora
2 Transforma os clocks em microsegundos
3 Transforma micro em milisegundos
4 Verifica se tempo é múltiplo do intervalo
5 Atualiza a referência
O código na íntegra pode ser encontrado aqui
Exercício 6: Filtros Espaciais
1-Concatenação de Filtros
O Programa fornecido como base possui uma série de filtros espaciais permitindo ao usuário trocar entre eles em tempo real, os filtros são os seguintes:
-
Média
-
Gauss
-
Bordas Verticais
-
Bordas Horizontais
-
Laplaciano
A esses adicionamos uma opção para a aplicação encadeada dos filtros Gaussiano e Laplaciano, nessa ordem. Para acessar essa nova opção o usuário deve digitar 'x', o caractere digitado serve como input para uma instrução switch, o case x
foi adicionado sobre o case g:
, que trata a seleção do filtro Gaussiano. Para todos os efeitos o programa está executando uma filtragem com a máscara Gaussiana.
Para acompanhar no código qual filtro havia sido selecionado, foi necessária a inclusão da variável char keys, uma vez que no runtime o único registro do filtro selecionado é a máscara em mask. Em cada case
foi inserida uma instrução para registrar qual filtro foi selecionado em keys. Após a operação de filtragem foi inserido um condicional que testa se o filtro escolhido foi o 'x', se tiver sido, a segunda filtragem é executada na imagem de saída da primeira, observe o trexo de código:
(...) //configuração
video >> cap; //imagem é capturada
cvtColor(cap, frame, CV_BGR2GRAY); //convertida para grayscale
flip(frame, frame, 1); //espelhada
imshow("original", frame);
frame.convertTo(frame32f, CV_32F); //convertida para float
filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0);
//filtrada uma vez
if(absolut){
frameFiltered=abs(frameFiltered);
}
if(keys == 'x'){ //se opção x
mask = Mat(3, 3, CV_32F, laplacian); //muda a mascara para o laplaciano
filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0);
// filtra uma segunda vez
mask = Mat(3, 3, CV_32F, gauss); // e volta a mascara pra gaussiana
}
(...) //exibe imagem e testa se usuário mudou o tipo de filtragem
A imagem original é a seguinte:

O resultado do Laplaciano isolado pode ser visto abaixo:

O resultado da aplicação do Laplaciano em uma imagem tratada com o Gaussiano pode ser visto abaixo:

É possível observar nas imagens que naquela tratada inicialmente com o gaussiano, um filtro de borramento, as bordas da imagem são menos evidentes, mais apagadas.
O código na íntegra pode ser encontrado aqui
Exercício 7: Filtros Espaciais II
1-Tiltshift Estático
Tiltshift é um processo de edição de imagem (e de fotografia) que usa o borramento e a angulação da imagem para simular que uma paisagem seja, na verdade, uma miniatura. Esse efeito pode ser obtido pela associação de duas versões de uma mesma imagem, sendo uma com borramento e outra sem. A imagem com borramento é exibida nas margens superior e inferior para simular o efeito de distância, enquanto a imagem em foco aparece no centro, representando justamente o foco da imagem final. Esse programa tem como entrada uma imagem e em tempo de operação pede ao usuário mais 3 inputs. Em sliders que aparecem juntamente com a imagem de saída o usuário pode configurar a posição central da janela de foco, a largura da janela de foco, e a "agressividade" do decaimento entre as duas imagens. A ponderação entre as duas imagens é dada pela função alfa que é função do x, l1, l2 e d, onde x é a linha da imagem, d é o decaimento, e l1 e l2 são as linhas nas quais o alfa é 0.5. l1 pode ser encontrado como a posição da janela menos metade da largura, e l2 a posição da janela mais a metade da largura.
\$alfa(x) = 1/2*(tanh((x-l1)/d) - tanh((x-l2)/d))\$
As imagens abaixo tem o propósito de ilustrar como a alteração dos parâmetros posição e largura modificam a janela:



A imagem desfocada é obtida através da passagem de um filtro suavizador de bordas de 5x5 três vezes sobre a imagem original seguindo exemplo do exercício de filtragem, abaixo segue a seção de código responsável:
(...)
float media[] = {1,1,1,1,1,
1,1,1,1,1,
1,1,1,1,1,
1,1,1,1,1,
1,1,1,1,1};
mask = Mat(5, 5, CV_32F, media);
scaleAdd(mask, 1/25.0, Mat::zeros(5,5,CV_32F), mask1);
swap(mask, mask1);
image2.convertTo(frame32f, CV_32F);
filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0);
frame32f = frameFiltered;
filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0);
frame32f = frameFiltered;
filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0);
frameFiltered.convertTo(image1, CV_8U);
(...)
Quando alguma alteração é feita em algum dos sliders a seguinte função é chamada:
void on_trackbar_config(int, void*){
int tmp1, tmp2;
dst = image1.clone();
float alfa;
tmp1 = pos_slider - jan_slider/2;
tmp2 = pos_slider + jan_slider/2;
//printf("t1 %d t2 %d\n", tmp1,tmp2);
if(tmp1 >= 0 && tmp2 <= 100){
l2 = tmp2;
l1 = tmp1;
l1 = (l1/100.0)*dst.rows;
l2 = (l2/100.0)*dst.rows;
}
for(int i=0; i<dst.rows ;i++){
alfa = 0.5*(tanh((i-l1)/(dec_slider+1))-tanh((i-l2)/(dec_slider+1)));
addWeighted(image2.row(i), alfa, image1.row(i), 1-alfa,0.0,dst.row(i));
}
imshow("Tiltshift",dst);
imwrite("tiltshift.png",dst);
}
Usando a função addWeighted()
podemos sobrepor duas imagens com um fator de ponderação de ponto flutuante, sabendo que em todas as linhas da matriz os elementos receberiam uma ponderação igual, decidimos utilizar a mencionada função de forma iterativa, calculando a matriz de saída uma linha por vez. A seguir podemos ver um exemplo do programa em funcionamento:

Somente a imagem:

O código na íntegra pode ser encontrado aqui
2-Tiltshift em Vídeo
As seguintes alterações foram feitas ao programa acima para a edição de vídeo
VideoCapture vid(argv[1]); (1) int width = static_cast<int>(vid.get(CV_CAP_PROP_FRAME_WIDTH)); int height = static_cast<int>(vid.get(CV_CAP_PROP_FRAME_HEIGHT)); double FPS = vid.get(CV_CAP_PROP_FPS); VideoWriter out("output.mov", CV_FOURCC('m','p', '4', 'v'), 15, cv::Size(width, height)); (2)
1 Ao invés de abrir a câmera, o programa abre o arquivo de vídeo passado como parâmetro
2 Aqui ele cria o arquivo de saída, com base nas configurações do de entrada
A outra alteração foi que a distorção da imagem de entrada agora fica em um loop de leitura do vídeo, e no fim do loop há um comando de escrita da imagem final no arquivo de saída, como pode ser visto abaixo:
while(1){ vid >> image1; image2 = image1.clone(); float media[] = {1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1}; mask = Mat(5, 5, CV_32F, media); scaleAdd(mask, 1/25.0, Mat::zeros(5,5,CV_32F), mask1); swap(mask, mask1); // -------- criar imagem borrada--------------- image2.convertTo(frame32f, CV_32F); filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0); frame32f = frameFiltered; filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0); frame32f = frameFiltered; filter2D(frame32f, frameFiltered, frame32f.depth(), mask, Point(1,1), 0); frameFiltered.convertTo(image1, CV_8U); //---------------------------------------------- on_trackbar_config(jan_slider,0); out << dst; if(cv::waitKey(1000.0/FPS) == 27) break; }
Infelizmente não pudemos finalizar a obtenção do vídeo de saída ainda, pois ocorreram erros no salvamento. Continuaremos a tentar e atualizaremos esta página com o programa funcionando plenamente, mas com o código é possível visualizar o vídeo editado, e alterar as configurações da janela, somente não salva.
O código na íntegra pode ser encontrado aqui
Bibliografia
-
Agostinho Brito Jr. 'Tutorial de Processamento de Imagens'. Disponível em http://agostinhobritojr.github.io/tutoriais/pdi/
-
http://www.cplusplus.com. 'Principal portal de desenvolvimento e referência para programação em C++'.