Apresentação:
Olá, sou João Vitor e vou apresentar alguns recursos da plataforma OpenCV como ferramenta de manipulação de imagens. Nesse projeto teremos a resolução de algumas atividades que serão exemplificadas e discutidas diante do processo.
Atividades
Primeira Unidade
-
Manipulação de Pixels
-
Negativo da Imagem
-
Troca Regiões
-
-
Contador de objetos
-
Histograma
-
Equalizador
-
Dectetor de Movimento
-
Segunda Unidade
-
Filtro Homomórfico
-
Canny com pontilhismo
-
Kmeans
1.Manipulação de Pixels
a.Negativo da Imagem
Essa atividade propõe fazer o negativo de um retângulo que será delimitado pelo usuário. A imagem será trabalhada em tons de cinzas. Para fazer o negativo basta pegar valor do pixel desejado e diminuir 255 e guardar novamente no pixel. Esse programa solicitará a imagem a ser processada, o programa indicará o tamanho da imagem para facilitar a escolha do retângulo e é solicitado a área a ser negativizada por meio do ponto superior esquerda e ponto inferior direito. A resposta do programa é a imagem original e a imagem negativizada na tela.
Abaixo temos a imagem antes e depois do procedimento.
O Código fonte para fazer esse procedimento de torna-lá em tons de cinzas e negativizar uma área.
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv){
Mat image;
int P1[2],P2[2];
image= imread(argv[1],0);
if(!image.data)
cout << "nao abriu" << endl;
imshow("Imagem Inicial",image);
cout<<"Tamanho da imagem : "<<image.rows<<"x"<<image.cols<<"\n";
cout<<"Digite o ponto P1(x,y)\n";//Ponto extremo esquerdo
cin>>P1[0]>>P1[1];//x=linha y=coluna
cout<<"Digite o ponto P2(x,y)\n";//Ponto extremo direito
cin>>P2[0]>>P2[1];//x=linha e y=coluna
if(P1[0]>=0 && P1[1]<image.rows && P2[0]>=P1[0] && P2[1]>=P2[1]){
for(int i=P1[0];i<P2[0];i++){
for(int j=P1[1];j<P2[1];j++){
image.at<uchar>(i,j)=255-image.at<uchar>(i,j);
}
}
imshow("janela", image);
imwrite("dota.jpg",image);
waitKey();
}
return 0;
}
Primeiramente foi craido uma variável Mat e dois vetores de inteiros para armazenar a imagem e os pontos a serem negativizados. Posteriormente 'image' recebe a imagem selecionada pelo usuário e verifica se 'image' recebeu os dados, caso não ocorra será emitido uma mensagem dissendo que a imagem nao pode ser aberta. Caso a imagem for aberta, o programa exibirá na tela a imagem em cor de cinza.
Mat image;
int P1[2],P2[2];
image= imread(argv[1],0);
if(!image.data)
cout << "nao abriu" << endl;
imshow("Imagem Inicial",image);
Caso a imagem for aberta, o programa informará o tamanho em pixels e solicitará do usuário o ponto superior esquerdo e inferior direito do retângulo.
cout<<"Tamanho da imagem : "<<image.rows<<"x"<<image.cols<<"\n"; cout<<"Digite o ponto P1(x,y)\n";//Ponto extremo esquerdo cin>>P1[0]>>P1[1];//x=linha y=coluna cout<<"Digite o ponto P2(x,y)\n";//Ponto extremo direito cin>>P2[0]>>P2[1];//x=linha e y=coluna
A partir disso o programa irá verificar se os pontos solicitados estão dentro do range da imagem. Caso esteja,ele irá varrer todos os pixels que estão no quadrado por meio de dois laços modificando os pixels para seu negativo (O negativo de um pixel é simplismente o valor dele menos 255). Feito esse processo, o programa irá mostrar a imagem inicial em tons de cinzas e a imagem com o retângulo negativizado por meio dessas função nativas do OpenCV.
if(P1[0]>=0 && P1[1]<image.rows && P2[0]>=P1[0] && P2[1]>=P2[1]){
for(int i=P1[0];i<P2[0];i++){
for(int j=P1[1];j<P2[1];j++){
image.at<uchar>(i,j)=255-image.at<uchar>(i,j);
}
}
imshow("janela", image);
imwrite("dota.jpg",image);
waitKey();
}
b.Troca de Regiões
Nessa atividade é necessário somente trocar as regiões da imagem, a mesma foi dividida em quatro parte uniformes, em que o ponto semelhantes entre todas as quatros subimagens é o centro da imagem inicial. Abaixo temos a imagem original e a mesma imagem depois de passar pelo programa.
Podemos perceber que a imagem trocada está em tons de cinzas e a ordem das subimagens estão invertidas, nesse caso foi um escolha do programador deixar nesse estilo. Agora iremos apresentar o código fonte e explicar passo a passo sobre o programa.
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv){
Mat image;
int X,Y,i,j;
image= imread(argv[1],0);
if(!image.data) cout << "nao abriu" << endl;
Mat imagef(image.rows,image.cols,0);
X=image.cols/2 ;
Y=image.rows/2;
cout<<X<<" "<<Y<<endl;
for(i=0;i<image.cols;i++){
for(j=0;j<image.rows;j++){
if (i<=X && j<=Y) imagef.at<uchar>(j,i)=image.at<uchar>(Y+j,X+i);
if (i>=X && j<=Y) imagef.at<uchar>(j,i)=image.at<uchar>(Y+j,i-X);
if (i<=X && j>=Y) imagef.at<uchar>(j,i)=image.at<uchar>(j-Y,i+X);
if (i>=X && j>=Y) imagef.at<uchar>(j,i)=image.at<uchar>(j-Y,i-X);
}
}
imshow("ImagemOriginal",image);
waitKey();
imshow("ImagemTrocada", imagef);
imwrite("imgtrocada.png",imagef);
waitKey();
return 0;
}
Nesse trecho inicial percebemos a criação da variável 'Mat' e de quatro inteiros que irão facilitar futuramente. A 'image' recebe a imagem indicada pelo usuário e percebemos que dentro da função tem um '0', esse zero significa que a imagem será transformada para tons de cinzas. No 'if' é somente uma verificação igual a atividade anterior. Logo abaixo percebemos a criação novamente de uma variável 'Mat' com parâmetros, essa 'imagef' será a imagem final a ser apresentrada, esses parâmetros são a largura e altura da imagem. As variáveis 'X' e 'Y' recebem respectivamente os valores das linhas e colunas dividido por 2 da imagem original e emiti na tela o tamanho do mesmo. Esses dois parâmetros serão o ponto semelhante entre as quatro subimagens.
Mat image; int X,Y,i,j; image= imread(argv[1],0); if(!image.data) cout << "nao abriu" << endl; Mat imagef(image.rows,image.cols,0); X=image.cols/2 ; Y=image.rows/2; cout<<X<<" "<<Y<<endl;
Agora irá começar o transporte de informação da imagem original para a imagem trocada, para isso existe dois laços que irá varrer a imagem original. Dentro dos laços existe quatro seletores 'if',caso o pixel esteja antes de 'X' e 'Y' serão os pixels superiores da esquerda, estes serão transportados para os pixels inferiores a direita na imagem trocada e vice-versa. Caso os pixels estejão na parte superior a direita, esses serão transportada para os pixels inferiores a esquerda e vice-versa.
for(i=0;i<image.cols;i++){
for(j=0;j<image.rows;j++){
if (i<=X && j<=Y) imagef.at<uchar>(j,i)=image.at<uchar>(Y+j,X+i);
if (i>=X && j<=Y) imagef.at<uchar>(j,i)=image.at<uchar>(Y+j,i-X);
if (i<=X && j>=Y) imagef.at<uchar>(j,i)=image.at<uchar>(j-Y,i+X);
if (i>=X && j>=Y) imagef.at<uchar>(j,i)=image.at<uchar>(j-Y,i-X);
}
}
Após todo o procedimento, o programa irá exibir na tela a imagem original e a imagem trocada. A imagem trocada será guardada em um arquivo chamado imgtrocada.png.
imshow("ImagemOriginal",image);
waitKey();
imshow("ImagemTrocada", imagef);
imwrite("imgtrocada.png",imagef);
waitKey();
2.Contador de Objetos
Nessa atividade foi desenvolvido um programa que pudesse contar objetos, nesse caso foi bolhas brancas em uma imagem preta mas pode ser ampliada para aplicação de outros objetos. Iremos explicar passo a passo sobre as ferramentas utilizadas. Abaixo temos o resultado do contador para essa imagem.
O contador de bolhas segue algumas regras para facilitar na identificação de bolhas, as bolhas que estão na borda não serão consideras bolhas pois não se sabe se é uma bolha completa ou uma pedaço. Bolhas na qual possuem buraco ou não serão considerados bolhas. O código fonte se encontra logo abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv){
Mat image;
int width, heigth, objetos=0, i, j,comburaco=0,semburaco=0;
CvPoint p(0,0);
image = imread(argv[1],0);
if(!image.data){
cout<<"Nao abriu\n";
return(0);
}
imshow("Contador",image);
waitKey();
width=image.size().width;
heigth=image.size().height;
for(i=0;i<heigth;i++){
if(image.at<uchar>(i,0)==255){
p.x=0;
p.y=i;
floodFill(image,p,0);
}
if(image.at<uchar>(i,width-1)==255){
p.x=width-1;
p.y=i;
floodFill(image,p,0);
}
}
for(j=0;j<width;j++){
if(image.at<uchar>(0,j)==255){
p.x=j;
p.y=0;
floodFill(image,p,0);
}
if(image.at<uchar>(heigth-1,j)==255){
p.x=j;
p.y=heigth-1;
floodFill(image,p,0);
}
}
for(i=0;i<heigth;i++){
for(j=0;j<width;j++){
if(image.at<uchar>(i,j)==255){
objetos++;
p.x=j;
p.y=i;
floodFill(image,p,objetos);
}
}
}
imshow("Contador",image);
imwrite("labeling.png", image);
waitKey();
p.x=0;
p.y=0;
floodFill(image,p,255);
imwrite("FundoBranco.png",image);
for(i=0;i<heigth;i++){
for(j=0;j<width;j++){
if(image.at<uchar>(i,j)==0 && image.at<uchar>(i,j-1)!=255){
comburaco++;
p.x=j;
p.y=i;
floodFill(image,p,255);
p.x=j-1;
floodFill(image,p,255);
}
}
}
imshow("BolhasS",image);
imwrite("labeling2.png",image);
for(i=0;i<heigth;i++){
for(j=0;j<width;j++){
if(image.at<uchar>(i,j)!=255){
semburaco++;
p.x=j;
p.y=i;
floodFill(image,p,255);
}
}
}
cout<<"Quantidade de Bolhas : "<<objetos<<endl;
cout<<"Quantidade de Bolhas com Buracos : "<<comburaco<<endl;
cout<<"Quantidade de Bolhas sem Buracos : "<<semburaco<<endl;
waitKey();
return 0;
}
Primeiramente percebemos a criação de diversar variáveis, no geral elas serão para a imagem,tamanho da imagem, contador de bolhas, contador de bolhas com buracos e sem buracos. Existe uma nova variável chamada p da classe CvPoint, nesse caso ela será útil para armazenar o pixels em 'X' e 'Y'. Iniciando o processamento do programa, temos a abertura da imagem do usuário e juntamente a verificação da mesma. Posteriormente temos a apresentação da imagem na tela.
Mat image;
int width, heigth, objetos=0, i, j,comburaco=0,semburaco=0;
CvPoint p(0,0);
image = imread(argv[1],0);
if(!image.data){
cout<<"Nao abriu\n";
return(0);
}
imshow("Contador",image);
waitKey();
Logo abaixo existe as variáveis 'width' e 'heigth' que serão armazenadas a quantidades de linhas e colunas da imagem.
width=image.size().width; heigth=image.size().height;
Em seguida temos um laço que varre a borda superior e inferior e dentro temos dois seletores que irão verificar se o pixel a ser varrido é '255'(branco), caso seja, presupoe que existe uma bolha na borda. Como bolhas nas bordas não serão contabilizadas, serão excluídas para facilitar o trabalho. Para exclui essa bolha, existe uma função chamada FloodFill que pinta todos os pixels até encontrar um pixels diferente do selecionado. As informações solicitadas pela função é a imagem, o ponto de operação e o valor desejável para pintar. Fica nitído que o primeiro laço so irá verificar se existe bolhas na borda superior e inferior, para compensar terá um novo laço na qual verifica bolhas na borda da esquerda e direita da mesma forma do primeiro laço.
for(i=0;i<heigth;i++){
if(image.at<uchar>(i,0)==255){
p.x=0;
p.y=i;
floodFill(image,p,0);
}
if(image.at<uchar>(i,width-1)==255){
p.x=width-1;
p.y=i;
floodFill(image,p,0);
}
}
for(j=0;j<width;j++){
if(image.at<uchar>(0,j)==255){
p.x=j;
p.y=0;
floodFill(image,p,0);
}
if(image.at<uchar>(heigth-1,j)==255){
p.x=j;
p.y=heigth-1;
floodFill(image,p,0);
}
}
Posteriormente da verificação e exclusão das bolhas nas bordas da imagem, iremos passar para os laços que contam as bolhas dentro da imagem. Para isso foi criado dois laços que irá varrer toda a imagem,dentro do laço possui um seletor, caso o pixel seja igual a '255'(branco) a variável objetos será incrementada. A variável p da classe CvPoint irá armazenar a posição do pixel e seguidamente ocorre um FloodFill na imagem na posição 'p' com o valor do pixel 'objeto', percebemos que o valor do 'objeto' será diferente de '0'. O motivo de pintar os pixels no valor do 'objeto' é que o seletor passará normalmente pela bolha sem contabilizar a bolha novamente, não interferindo no programa.
for(i=0;i<heigth;i++){
for(j=0;j<width;j++){
if(image.at<uchar>(i,j)==255){
objetos++;
p.x=j;
p.y=i;
floodFill(image,p,objetos);
}
}
}
Posteriormente a imagem depois do procedimento é apresentado na tela:
Podemos percebemos que a parte superior ficou com tons de cinzas mais escuro do que na parte inferior, isso se da por conta que o contador 'objetos' aumenta de acordo com a quantidade de bolhas encontradas, próximo ao final da imagem ele tende a ter um valor mais distante de '0', percebendo isso como um gradiente na imagem.
|
Warning
|
O que acontece caso existir mais de 255 objetos? E o que fazer para solucionar. |
Esse procedimento irá ocorrer quando na imagem tiver mais de 255 objetos na imagem, exemplo em foto de sangue,chuva entre outros, uma solução para o problema é aumentar a quantidade bits de 255 para mais,por exemplo colocando para um inteiro aumentando drasticamente o valor. Para colocar na imagem basta fazer uma modulação da variável de 0 a 255. Continuando o processo, iremos adentrar na funcionalidade de contar quantas bolhas com buracos e sem buracos a imagem possui. Para isso utilizamos a imagem já processada do contador de bolhas e aplicamos um 'FloodFill' na imagem no ponto (0,0) no valor '255'(branco), teremos uma imagem com fundo branco igual a essa.
p.x=0;
p.y=0;
floodFill(image,p,255);
imwrite("FundoBranco.png",image);
Esse procedimento irá facilitar na identificação de bolhas com e sem buracos. Posteriormente teremos dois laços e um seletor como mostra a seguir.
for(i=0;i<heigth;i++){
for(j=0;j<width;j++){
if(image.at<uchar>(i,j)==0 && image.at<uchar>(i,j-1)!=255){
comburaco++;
p.x=j;
p.y=i;
floodFill(image,p,255);
p.x=j-1;
floodFill(image,p,255);
}
}
}
Para ficar mais intuitivo elaborei uma imagem que usaremos como exemplo para a explicação dos laços e seletor.
Esses laços varrem toda a imagem e o seletor so deixa passar quando o pixel é igual a 0 e diferente de 255. Oberservando a imagem, os únicos lugares em que existe pixels igual a zero são nas bolhas com buracos. Analisando isso, fica nitído que so entrará no seletor quando o pixel estiver no buranco. Quando entrar no seletor, irá incremetar a variável comburaco e vai deletar a bolha com buraco utilizando FloodFill com valor de '255' no buraco e no pixel anterior que será a borda da bolha. Posteriormente a esse passo, a imagem restante somente terá bolhas sem buracos, como mostra a seguir:
Para verificar quantos buracos existem na imagem, basta utilizar os laços inicialmente apresentados no contador normal, com cuidado que o fundo é '255'(branco) e o as bolhas são '0'(preto). Dentro do seletor incrementamos o contador e apagamos a bolha para nao contar novamente.
for(i=0;i<heigth;i++){
for(j=0;j<width;j++){
if(image.at<uchar>(i,j)!=255){
semburaco++;
p.x=j;
p.y=i;
floodFill(image,p,255);
}
}
}
O programa está funcionando corretamente de acordo com os requisitos, posteriormente temos a exibição de quantas bolhas existem e quantas são com e sem buracos.
Histograma
a.Equalizdor
O Histograma é um mapeamento de todos os pixels da imagem entre os tons de 0 a 255, para nosso caso só vai haver um histograma pois estamos trabalhando com imagens de tons de cinza. Um exemplo de histograma é mostrado abaixo.
O gráfico mostra a quantidade de pixels em que cada tom de cinza possuí, esse histograma é dessa imagem.
A Equalização de Histograma é uma função que serve para melhorar a distribuição dos valores do histograma, reduzindo as diferenças acentuadas e em imagens ajuda a visualizar objetos que teriamos dificuldade caso nao tivesse feito a equalização. Abaixo temos a imagem mostrada anteriormente Equalizada.
O código fonte não será mostrado pois o foco será fazer isso em vídeo, esse momento foi válido para recordar ou aprender os conceitos de histograma e equalização. Abaixo temos o código para fazer a equalização do vídeo.
#include "opencv2/opencv.hpp"
#include <iostream>
#include <fstream>
using namespace std;
using namespace cv;
int main(){
VideoCapture cap("teste5.mp4");
ofstream arquivo;
ofstream arquivo2;
Mat frame;
Mat Equalizado;
Mat Junto;
arquivo.open("histograma1.txt");
arquivo2.open("histograma2.txt");
if(!cap.isOpened()){
cout << "Erro" << endl;
return -1;
}//Verifica se video foi aberto
int width = cap.get(CV_CAP_PROP_FRAME_WIDTH);
int height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
VideoWriter video("Junto.avi",CV_FOURCC('P','I','M','1'),30, Size(2*width, height),0);
while(1){
cap >> frame;
cvtColor(frame,frame,CV_RGB2GRAY);//Transforma para Cinza
equalizeHist( frame , Equalizado );
if (frame.empty()) break; //Acabar video, fecha while
hconcat(frame,Equalizado,Junto);
video.write(Junto);
imshow( "Video", Junto );
char c=(char)waitKey(25);
if(c==27) break; //Esc fecha video
}
cap.release();
video.release();
//Destrui todos frames
destroyAllWindows();
return 0;
}
O programa grava o vídeo apresentado na tela, o vídeo do lado esquerdo é o origial e do outro lado é equalizado.
Agora vamos analisar o código apresentado acima. Vou explicar as partes referentes a histograma e equalização, o trecho de arquivo não será apresentado. Para abrir um video é muito parecido a imagem, com diferença da Classe chamada de VideoCapture, para esse caso colocamos o nome do vídeo quando criamos a variável. Igualmente a imagem, fizemos a verificação para validar que o video possui informação. No próximo passo, retirei a largura e altura do vídeo por meio da função "get".Isso é somente necessário para guardar o vídeo final.
VideoCapture cap("teste2.mp4");
if(!cap.isOpened()){
cout << "Erro" << endl;
return -1;
}//Verifica se video foi aberto
int width = cap.get(CV_CAP_PROP_FRAME_WIDTH);
int height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
Agora entraremos no while infinito, em que será capturado cada imagem do vídeo para uma variável frame criada iniciamente como 'Mat'. Posteriormente transformamos a foto para tons de cinzas, pois estamos trabalhando somente imagens preto e branco. Existe uma função no próprio OpenCV que equaliza a imagem rapidamente, essa função é a "equalizeHist", os parâmetros passados é a imagem e uma imagem para guarda ela equalizada.
cap >> frame; cvtColor(frame,frame,CV_RGB2GRAY);//Transforma para Cinza equalizeHist( frame , Equalizado );
A função faz o seguinte cálculo:
-
Cria um histograma da imagem
-
Calcula quantos pixels existem na imagem (bits)
-
Cria um vetor com a seguinte função (g[i]=g[i-1]+hist[i]), i=256.
-
Imagem Equalizada recebe essa função ( image.at<uchar>(i,j)= (g[(int)image.at<uchar>(i,j)]) * 256 / bits)
A primeira função é um acumulador do histograma. A segunda função pega o valor do pixel da imagem e coloca como entrada no vetor,a saída do vetor é multiplicado por 256 ( maxímo valor assumido por um pixel) e dividido pela quantidade de bits da imagem, esse trecho de multiplicação e divisão é para normalizar o resultado entre 0 e 255. Abaixo temos a função.
cap >> frame; cvtColor(frame,frame,CV_RGB2GRAY);//Transforma para Cinza equalizeHist( frame , Equalizado );
Abaixo temos um seletor que serve para fechar o while quando o video terminar. Logo depois temos a concatenação da imagem original com a imagem equalizada, isso é só para facilitar a observação das diferenças no próprio vídeo. Posteriormente o vídeo concatenado é mostrado na tela. Podemos ver que temos um seletor, ele serve somente para sair do vídeo quando precionarmos a tecla "Esc".
if (frame.empty()) break; //Acabar video, fecha while hconcat(frame,Equalizado,Junto); video.write(Junto); imshow( "Video", Junto ); char c=(char)waitKey(25); if(c==27) break; //Esc fecha video
Fora do while temos um "release", ele serve para liberar a camera e depois o a função destroyAll, que destrói todos as imagens.
cap.release(); video.release(); //Destrui todos frames destroyAllWindows();
b.Detector de Movimento
Como o nome já diz, iremos programar um detector de movimento em tons de cinzas, mas pode ser ampliado para imagens em RGB. Iremos utilizar o histograma para detectar qualquer alteração na imagem, caso apareça ou suma qualquer objeto da imagem o histograma irá modificar. Entam so precisamos ficar comparando o histograma atual com o histograma anterior, caso seja diferente é porque houve alteração da imagem e podemos deduzir que algo se moveu. Para deixar o detector mais fiel a movimento de objetos, colocaremos alguns parametros, pois caso a imagem sofrer qualquer tipo de ruído em um curto espaço de tempo, o histograma irá modificar e dará um resultado inapropriado. Os parâmetros serão os seguintes,o histograma será normalizado para 64 posições,exemplo: Caso o pixel fique entre 0 e 3, no normalizado ele ficará na posição 0 e assim em diante. Esses parâmetros podem ser mudado de acordo com o programador, caso o histograma normalizado tenha 23 pixels diferentes do histograma anterior será considerado movimento. Abaixo temos o vídeo com Dectector de movimento. Observe quando tiver movimento, aparecerá um quadrado preto na parte superior na esquerda.
O código fonte para o programa Detector de Movimento.
#include "opencv2/opencv.hpp"
#include <iostream>
#include <fstream>
using namespace std;
using namespace cv;
int main(){
VideoCapture cap("teste2.mp4");
ofstream arquivo;
ofstream arquivo2;
Mat frame,histAnt,histAtual,frameBase;
Mat Equalizado;
Mat Junto;
Mat hist;
int nbins = 64,cont=0;
float range[] = {0, 256};
const float *histrange = { range };
bool acummulate = false;
bool uniform = true;
//arquivo.open("histograma1.txt");
//arquivo2.open("histograma2.txt");
if(!cap.isOpened()){
cout << "Erro" << endl;
return -1;
}//Verifica se video foi aberto
int width = cap.get(CV_CAP_PROP_FRAME_WIDTH);
int height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
cout<<"width : "<<width<<endl;
cout<<"height : "<<height<<endl;
VideoWriter video("Detector.avi",CV_FOURCC('P','I','M','1'),30, Size(width, height),0);
while(1){
cap>>frameBase;
cvtColor(frameBase,frameBase,CV_RGB2GRAY);//Transforma para Cinza
calcHist(&frameBase, 1, 0, Mat(), histAnt, 1,
&nbins, &histrange,
uniform, acummulate);
cap >> frame;
cvtColor(frame,frame,CV_RGB2GRAY);//Transforma para Cinza
// cap>>frame;
calcHist(&frame, 1, 0, Mat(), histAtual, 1,
&nbins, &histrange,
uniform, acummulate);
cout<<"HistAnt : "<<histAnt.rows<<" "<<histAnt.cols<<endl;
cout<<"HistAtual : "<<histAtual.rows<<" "<<histAtual.cols<<endl;
for(int i=0;i<64;i++){
cout<<(int)histAtual.at<uchar>(0,i)<<" ";
if((int)histAtual.at<uchar>(0,i)!=histAnt.at<uchar>(0,i)){
cont++;
}
}
if(cont>23){
cout<<"\nAlgo Passo"<<endl;
for(int i=0;i<50;i++){
for(int j=0;j<50;j++){
frame.at<uchar>(i,j)=0;
}
}
}
else{
for(int i=0;i<50;i++){
for(int j=0;j<50;j++){
frame.at<uchar>(i,j)=frame.at<uchar>(i,j);
}
}
}
cout<<cont<<endl;
//waitKey();
cont=0;
cout<<endl;
for(int i=0;i<64;i++){
cout<<(int)histAnt.at<uchar>(0,i)<<" ";
}
cout<<endl;
cont=0;
if (frame.empty()) break; //Acabar video, fecha while
//hconcat(frame,Equalizado,Junto);
video.write(frame);
imshow( "Video", frame );
char c=(char)waitKey(25);
if(c==27) break; //Esc fecha video
}
cap.release();
video.release();
//Destrui todos frames
destroyAllWindows();
return 0;
}
Irei explicar as principais funções necessárias para entender o programa, muita coisa é parecida com o programa anterior. Dentro do while, capturamos uma imagem chamada de 'frameBase' e utilizamos uma função 'calcHist' que calcula o histograma da imagem e guarda em um 'Mat' de dimensões 64x1. Posteriormente capturamos uma nova imagem e calculamos o histograma pela mesma função. Diante disso fazemos um laço para varrer umas dos histogramas, caso os histogramas tenham um valor diferente, o contador que foi inicialdo em zero incremente uma unidade. Como ja parametrizamos a quantidade para ser considerado movimento, utilizamos um 'if', caso cont seja maior que 23, será considerado movimento e um quadrado preto irá aparecer na tela. Depois do 'if' zeramos o cont para que no próximo loop ele fique com zero. Caso queira sair, basta pressionar a tecla 'Esc'.
1.Filtro Homomórfico
O Filtro Homomórfico é muito usado para melhorar imagens que possuem iluminação precária, apesar de ter diversor parâmetros dentro da função para conseguir uma imagem plausível. O Filtro é dada por a seguinte função matemática.
O Filtro Homomórfico trabalha no domínio da frequência, por esse motivo temos os parâmetros "u" e "v". Observando o gráfico do filtro percebemos que as frequências mais baixas são atenuadas e as mais elevadas são mantidas. A principal função desse Filtro é melhorar a qualidade da iluminação da imagem, podendo verificar objetos nas imagens que anteriormente não era possível. Esse Filtro é baseado na iluminância e reflectância da imagem, conforme a Figura abaixo.
Antes de levarmos a função para o domínio da frequência, iremos trata-la para que não ocorra erros na transformação. Antes de iniciar o processo de tratamento na função, deve-se ser incrementado uma unidade em todos os pixels da imagem para que não haja incoerência na utilização do tratamento. Primeiramente aplicaremos o logaritmo neperiano para conseguirmos separarmos a iluminância da reflectância. Abaixo temos a propriedade utilizada do Ln e a nova função pós-tratamento.
Agora basta aplicarmos a Transformada de Furrier na função, tendo o seguinte:
Com a imagem no domínio da frequência podemos aplicar o Filtro Homomórfico por meio da função apresentada na Figura 14. Para retornamos a imagem no domínio espacial basta fazermos a Transformada Inversa de Furrier e aplicar uma exponencial a função para equilibrar com o logaritmo neperiano. Agora iremos analisar os parâmetros do Filtro Homomórfico, abaixo temos um esquema para melhor entendimento.
\(H(u,v)=(\gamma_H - \gamma_L)(1-e^{c(\frac{D^2(u,v)}{D^2_0})}+\gamma_L)\)
\(\gamma_L\) : Limiar inferior de corte
\(\gamma_H\) : Limiar superior de corte
\(D(u,v)=\bigg((u-\frac{M}{2})^2+(v-\frac{N}{2})^{2}\bigg)^\frac{1}{2}\)
\(D^2_0\) : Frequência de corte
Abaixo temos uma aplicação do Filtro Homomórfico.
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#define RADIUS 20
using namespace cv;
using namespace std;
// Troca as posições para DFT
void deslocaDFT(Mat& image ){
Mat tmp, A, B, C, D;
// se a imagem tiver tamanho impar, recorta a regiao para
// evitar cópias de tamanho desigual
image = image(Rect(0, 0, image.cols & -2, image.rows & -2));
int cx = image.cols/2;
int cy = image.rows/2;
// reorganiza os quadrantes da transformada
// A B -> D C
// C D B A
A = image(Rect(0, 0, cx, cy));
B = image(Rect(cx, 0, cx, cy));
C = image(Rect(0, cy, cx, cy));
D = image(Rect(cx, cy, cx, cy));
// A <-> D
A.copyTo(tmp); D.copyTo(A); tmp.copyTo(D);
// C <-> B
C.copyTo(tmp); B.copyTo(C); tmp.copyTo(B);
}
int main(int argc, char** argv){
char *window_name = "FiltroHomomorfico";
Mat image,imaginaryInput, complexImage, multsp;
Mat padded, filter, mag;
Mat imagegray, tmp;
Mat_<float> realInput, zeros;
vector<Mat> planos;
namedWindow(window_name, CV_WINDOW_AUTOSIZE);//Abre uma janela
float d; int y_H, y_H_max=25, y_L, y_L_max=25, d0=5, d0_max=30, c, c_max=15;
// Janela para modificar os parâmetros
createTrackbar("Gama H: ",window_name, &y_H, y_H_max);
createTrackbar("Gama L: ",window_name, &y_L, y_L_max);
createTrackbar("D0: ",window_name, &d0, d0_max);
createTrackbar("c: ",window_name, &c, c_max);
// guarda tecla capturada
char key;
// valores ideais dos tamanhos da imagem para calculo da DFT
int dft_M, dft_N;
image= imread(argv[1],CV_LOAD_IMAGE_COLOR);
if(!image.data){
cout<<"Nao abriu"<<endl;
return 0;
}
// identifica os tamanhos otimos para
// calculo do FFT
dft_M = getOptimalDFTSize(image.rows);
dft_N = getOptimalDFTSize(image.cols);
// realiza o padding da imagem
copyMakeBorder(image, padded, 0, dft_M - image.rows, 0, dft_N - image.cols, BORDER_CONSTANT, Scalar::all(0));
// parte imaginaria da matriz complexa (preenchida com zeros)
zeros = Mat_<float>::zeros(padded.size());
// prepara a matriz complexa para ser preenchida
complexImage = Mat(padded.size(), CV_32FC2, Scalar(0));
// a função de transferência (filtro frequencial) deve ter o mesmo tamanho e tipo da matriz complexa
filter = complexImage.clone();
// cria uma matriz temporária para criar as componentes real e imaginaria do filtro ideal
tmp = Mat(dft_M, dft_N, CV_32F);
// cria a matriz com as componentes do filtro e junta ambas em uma matriz multicanal complexa
Mat comps[]= {tmp, tmp};
for(;;){
// Calcula o filtro homomórfico
for(int i=0; i<dft_M; i++){
for(int j=0; j<dft_N; j++){
d = pow(pow(i - dft_M/2, 2) + pow(j - dft_N/2, 2), 0.5);
tmp.at<float>(i,j) = (y_H - y_L)*(1 - exp(-1*c*(pow(d,2)/pow(d0,2)))) + y_L;
}
}
imshow("Filtro",tmp);
// Salva o filtro homomórfico
comps[0] = tmp; comps[1] = comps[0]; merge(comps, 2, filter);
cvtColor(image, imagegray, CV_BGR2GRAY);
imshow("Imagem Original", imagegray);
imwrite("Figura1.jpg",imagegray);
// realiza o padding da imagem
copyMakeBorder(imagegray, padded, 0, dft_M - image.rows, 0, dft_N - image.cols, BORDER_CONSTANT, Scalar::all(0));
// limpa o array de matrizes que vao compor a imagem complexa
planos.clear();
// cria a compoente real
realInput = Mat_<float>(padded);
// insere as duas componentes no array de matrizes
planos.push_back(realInput);
planos.push_back(zeros);
// combina o array de matrizes em uma unica componente complexa
merge(planos, complexImage);
// Aplicação do logaritmo antes da transformada
for (int i = 0; i < dft_M; i++){
for (int j = 0; j < dft_N;j++){
complexImage.at<char>(i,j) = log(complexImage.at<char>(i,j)+1);
}
}
// calcula o dft
dft(complexImage, complexImage);
// realiza a troca de quadrantes
deslocaDFT(complexImage);
// aplica o filtro homomórfico
mulSpectrums(complexImage,filter,complexImage,0);
// limpa o array de planos
planos.clear();
// separa as partes real e imaginaria para modifica-las
split(complexImage, planos);
// recompoe os planos em uma unica matriz complexa
merge(planos, complexImage);
// troca novamente os quadrantes
deslocaDFT(complexImage);
// calcula a DFT inversa
idft(complexImage, complexImage);
// Aplicação da exponencial após a transformada inversa
for (int i = 0; i < dft_M; i++){
for (int j = 0; j < dft_N;j++){
complexImage.at<char>(i,j) = exp((complexImage.at<char>(i,j)));
}
}
// limpa o array de planos
planos.clear();
// separa as partes real e imaginaria da imagem filtrada
split(complexImage, planos);
// normaliza a parte real para exibicao
normalize(planos[0], planos[0], 0, 1, CV_MINMAX);
imshow(window_name, planos[0]);
imshow("imagem",planos[0]);
key = (char) waitKey(10);
if( key == 27 ) break; // esc pressed!
switch(key){
case 'c':
imwrite("Imagem.jpg",planos[0]);
cout<<"Copiado\n";
break;
}
}
return 0;
}
Esse código junta a transformada de Furrier e toda a teorização aplicada anteriormente para desenvolver o Filtro Homomórfico. Para o Filtro, foi utilizada imagens tons de cinza para melhorar o desempenho e não sobrecarregar o processamento pois as realizações matemáticas são feitas no domínio da frequência. Nesse domínio gasta muito processamento em comparação ao dominio espacial pelos motivo das multiplicações serem matriciais e ter que ficar mudando o dominio da imagem para ser possível trabalhar. Apesar desse ponto negativo, esse Filtro é muito bem utilizado quando relacionado com imagens que possuem iluminação precária.
2. Canny com pontilhismo
O Detector de Bordas com o algoritmo de Canny é bem conhecido no mundo das imagens por ser rápido e eficiente no seu objetivo. O algoritmo somente necessita de dois parâmetros para seu funcionamento, na qual são dois limiares para assumir se a borda é forte ou fraca. O algoritmo funciona pegando a imagem e identificando a borda de acordo com os limiares a aplicando um threshold ma imagem. Lebrando que a imagem deve está em tons de cinzas para melhor aproveitamento do algoritmo. Funcionamento da Algortimo de Canny:
-
Primeiramente ocorre a convolução com o fitlro Gaussiano, e posteriormente é cálculado a magnitude e o ângulo.
-
Com os ângulos determinados de cada pixels, os mesmo são classificados da seguinte forma: Horizontal, Vertical,+45° e -45°.
-
É criada uma imagem para indicar as bordas, para isso é verificado semelhança de ângulos entre seus vizinhos.
-
Caso exista, será comparado suas magnitudes na qual se existir algum de seus vizinhos com magnitude maior a imagem receberá zero no seu respectivo pixel, caso contrário receberá 255.
-Os parâmetros indicados pelos úsuarios são os limiares na qual deixa passar pixels que possuam magnitude superior ao menor limiar.
O Pontilismo é uma técnica de pintura criada nos meados de 1880, na qual utiliza pontos para pintar imagens por meio de justaposição.
Nesse projeto iremos associar o algoritmo de Canny juntamente com o Pontilismo.
3. Kmeans
A quantização vetorail com k-means particiona n observações dentre k grupos onde cada um pertence ao grupo mais pŕoximo da média. Esse método é útil para compactar imagens.
Abaixo temos a utilização do Kmeans iniciando os centros de forma aleatória e somente com uma rodada já que o kmeans é preciso algumas rodadas para conseguir se aproximas da exatidão vista pelo algortimo.
Deixando os centros aleatórios o resultando provalvemente nunca será igual, sendo consideração não deteminístico. Esse algoritmo foi utilizado 10 vezes e feito um GIF representando as mudanças que pode ocorrer na utilização do Kmeans.
Abaixo temos o código fonte para representação do Kmeans
#include <opencv2/opencv.hpp>
#include <cstdlib>
#include <string>
using namespace cv;
using namespace std;
int main( int argc, char** argv ){
int nClusters = 6;
Mat rotulos;
int nRodadas = 1;
Mat centros;
Mat img = imread( argv[1], CV_LOAD_IMAGE_COLOR);
Mat samples(img.rows * img.cols, 3, CV_32F);
for(int i=0;i<10;i++){
for( int y = 0; y < img.rows; y++ ){
for( int x = 0; x < img.cols; x++ ){
for( int z = 0; z < 3; z++){
samples.at<float>(y + x*img.rows, z) = img.at<Vec3b>(y,x)[z];
}
}
}
kmeans(samples,
nClusters,
rotulos,
TermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS, 10000, 0.0001),
nRodadas,
KMEANS_RANDOM_CENTERS,
centros );
Mat rotulada( img.size(), img.type() );
for( int y = 0; y < img.rows; y++ ){
for( int x = 0; x < img.cols; x++ ){
int indice = rotulos.at<int>(y + x*img.rows,0);
rotulada.at<Vec3b>(y,x)[0] = (uchar) centros.at<float>(indice, 0);
rotulada.at<Vec3b>(y,x)[1] = (uchar) centros.at<float>(indice, 1);
rotulada.at<Vec3b>(y,x)[2] = (uchar) centros.at<float>(indice, 2);
}
}
imshow( "clustered image", rotulada );
if(i==0){
imwrite("foto0.jpg", rotulada);
}
if(i==1){
imwrite("foto1.jpg", rotulada);
}
if(i==2){
imwrite("foto2.jpg", rotulada);
}
if(i==3){
imwrite("foto3.jpg", rotulada);
}
if(i==4){
imwrite("foto4.jpg", rotulada);
}
if(i==5){
imwrite("foto5.jpg", rotulada);
}
if(i==6){
imwrite("foto6.jpg", rotulada);
}
if(i==7){
imwrite("foto7.jpg", rotulada);
}
if(i==8){
imwrite("foto8.jpg", rotulada);
}
if(i==9){
imwrite("foto9.jpg", rotulada);
}
}
waitKey( 0 );
}
O código salva dez imagens feitas pelo algoritmo Kmeans, posteriormente basta transformar as imagens em GIF.