1. Objetivo

Como trabalho que visa o início dos estudos do processamento digital de imagens, o trabalho visa nos fazer entender alguns dos princípios da manipulação de imagens e alguns recursos básicos do opencv, assim como um dos algoritmos mais básicos da manipulação de imagens, o FloodFill.

2. O Código

contandoBolhas.cpp
#include <iostream>
#include <opencv2/opencv.hpp>
#include <stack>

using namespace cv;
using namespace std;

struct coordenada{
    int x,y;
    inline coordenada() : x(0),y(0) {}
};

void sfill(int x,int y, unsigned char cor, Mat &image){
    coordenada C; coordenada D; stack <coordenada> pilha;
    C.x = x; C.y = y; pilha.push(C);
    const unsigned char cor2 = image.at<uchar>(x,y);
   	while(!pilha.empty()){
        x = pilha.top().x; y = pilha.top().y;
        pilha.pop();
	    if(x<image.cols-1 && image.at<uchar>(x+1,y) == cor2){
	        C.x = x+1; C.y = y;
	        pilha.push(C);
	    }
		if(y<image.rows-1 && image.at<uchar>(x,y+1) == cor2){
	        D.x = x; D.y = y+1;
	        pilha.push(D);
		}
	    if(x!=0 && image.at<uchar>(x-1,y) == cor2){
	       	C.x = x-1; C.y = y;
           	pilha.push(C);
        }
		if(y!=0 && image.at<uchar>(x,y-1) == cor2){
		    D.x = x; D.y = y-1;
		    pilha.push(D);
	 	}

	    image.at<uchar>(x,y) = cor;
	}

}

int main(int argc, char *argv[]){
   Mat image; CvPoint p;
   image = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
   int mLinhas = image.rows; int nColunas = image.cols;

   int counter = 32, nBubbles[4] = {0,0,0,0};

   //Remove bubbles that touch the borders
   for(int i=0;i<mLinhas;i++){
   	if(image.at<uchar>(i,0) == 255) sfill(i,0,0,image);
   	if(image.at<uchar>(i,nColunas-1) == 255) sfill(i,nColunas-1,0,image);
   }
   for(int j=0;j<nColunas;j++){
   	if(image.at<uchar>(0,j) == 255) sfill(0,j,0,image);
   	if(image.at<uchar>(mLinhas-1,j) == 255) sfill(mLinhas-1,j,0,image);
   }
   //Search for bubbles
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) == 255){
   			sfill(i,j,counter,image);
   			counter+=5;
   		}
   	}
   }
   imshow("image",image);
   waitKey();
   imwrite("saida.png",image);
   //Counting Bubbles
   image = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
   //Remove bubbles that touch the borders
   for(int i=0;i<mLinhas;i++){
   	if(image.at<uchar>(i,0) == 255) sfill(i,0,0,image);
   	if(image.at<uchar>(i,nColunas-1) == 255) sfill(i,nColunas-1,0,image);
   }
   for(int j=0;j<nColunas;j++){
   	if(image.at<uchar>(0,j) == 255) sfill(0,j,0,image);
   	if(image.at<uchar>(mLinhas-1,j) == 255) sfill(mLinhas-1,j,0,image);
   }
   //Filling background
   sfill(0,0,254,image);
   //finding bubbles and labeling them as 1
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) == 255){
   			sfill(i,j,1,image);
   		}
   	}
   }
   //Finding bubbles with holes and labeling them 1+number of holes
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) == 0 && image.at<uchar>(i,j-1) != 0 && image.at<uchar>(i,j-1) != 254){
   			sfill(i,j-1,image.at<uchar>(i,j-1)+1,image);
   			sfill(i,j,254,image);
   		}
   	}
   }
   //Counting bubbles
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) != 254){
   			nBubbles[(int)image.at<uchar>(i,j)-1]++;
   			sfill(i,j,254,image);
   		}
   	}
   }
   for(int i=0;i<4;i++){
   	cout<<"A imagem tem "<<nBubbles[i] <<" bolhas com "<<i<<" buracos\n";
   }
   return 0;
}

Após observarmos o código, temos que explicar alguns pontos importantes:

  • Como funciona o algoritmo seedFill?

  • O que diabos o código está fazendo?

  • Como o código abre a imagem?

  • Qual o seu retorno?

2.1. O Algoritmo SeedFill

O SeedFill (ou FloodFill) é o conhecido algoritmo que nos permite, nos mais comuns editores de imagem (o famoso balde de tinta no Paint, do Windows, ou o Pinta, mais comum no linux), preencher determinada região com uma cor específica. Para isso, o algoritmo verifica se as regiões próximas ao ponto possuem a mesma cor que esse ponto, e se sim elas são preenchidas com a cor especificada.

Desse modo, escolhendo um ponto \$(x,y)\$ como sendo o ponto inicial

  1. Iniciar a pilha com esse ponto \$(x,y)\$

  2. Retirar o elemento da pilha

  3. Verificar se os 4-vizinhos possuem a mesma cor que esse ponto

  4. Caso possuam, vão para a pilha

  5. Preencher \$(x,y)\$ com a cor que foi passada

  6. Repetir até o esvaziamento da pilha

Esse algoritmo ilustra o mais simples seedFill, e é o que foi implementado no nosso código .cpp:

void sfill(int x,int y, unsigned char cor, Mat &image){
    coordenada C; coordenada D; stack <coordenada> pilha;
    C.x = x; C.y = y; pilha.push(C);
    const unsigned char cor2 = image.at<uchar>(x,y);
   	while(!pilha.empty()){
        x = pilha.top().x; y = pilha.top().y;
        pilha.pop();
	    if(x<image.cols-1 && image.at<uchar>(x+1,y) == cor2){
	        C.x = x+1; C.y = y;
	        pilha.push(C);
	    }
		if(y<image.rows-1 && image.at<uchar>(x,y+1) == cor2){
	        D.x = x; D.y = y+1;
	        pilha.push(D);
		}
	    if(x!=0 && image.at<uchar>(x-1,y) == cor2){
	       	C.x = x-1; C.y = y;
           	pilha.push(C);
        }
		if(y!=0 && image.at<uchar>(x,y-1) == cor2){
		    D.x = x; D.y = y-1;
		    pilha.push(D);
	 	}

	    image.at<uchar>(x,y) = cor;
	}
}

O que é importante notarmos é que temos um tipo que não pertence às bibliotecas iostream ou opencv, que é a classe coordenada. Apesar da biblioteca opencv possuir uma classe semelhante (CvPoint), essa às vezes pode confundir por não usar o sistema destrógiro, e é de grande vantagem sabermos implementar nossa classe para um melhor desempenho do código. A classe está mostrada a seguir:

struct coordenada{
    int x,y;
    inline coordenada() : x(0),y(0) {}
};

A definimos como struct pois queremos que seus valores sejam públicos, e não precisamos de um acesso seguro, precisamos somente de uma rapidez maior no acesso à classe. A segunda linha contém um construtor default.

No algoritmo do seedFill, passamos como parâmetro o x e o y de início, a cor que desejamos e a imagem a ser preenchida. O primeiro teste que fazemos é para a cor do ponto que passamos como parâmetro, que é feita pela linha de código a seguir:

const unsigned char cor2 = image.at<uchar>(x,y);
Acessamos o valor de um ponto de uma imagem com a função at(), que recebe como parâmetro os valores de x e y. O argumento <uchar> indica que a função foi criada usando gabaritos, e que o valor de retorno dela será um unsigned char.

A seguir adicionamos o ponto à pilha, com a função push(), a qual podemos obter mais informações no CppReference, assim como a função top() e pop(). Iniciamos o loop while() testando se a pilha está ou não vazia. Depois retiramos as informações do primeiro elemento da pilha com a função top(), e retiramos esse primeiro elemento da pilha com a função pop(). A seguir temos 4 if()'s, para que possamos testar os valores dos 4-vizinhos do ponto inicial.

O que é importante notarmos nos if()'s são suas condições. para os vizinhos da esquerda e de cima, os valores de x e y não podem ser 0, caso contrário acessaríamos valores de -1, que não existem na matriz da imagem. Para os vizinhos da direita e de baixo, os valores de x e y têm de ser 2 valores menores que o tamanho da imagem, já que o último elemento da matriz é matriz[linhas-1][colunas-1].

A última linha, mostrada a seguir, ajusta o valor do pixel para a cor desejada, passada como parâmetro. O loop continua até a pilha se esvaziar.

image.at<uchar>(x,y) = cor;

2.2. O que o código está fazendo?

Vamos começar a analisar o código a partir de suas bibliotecas.

#include <iostream>
#include <opencv2/opencv.hpp>
#include <stack>

using namespace cv;
using namespace std;

Utilizamos a biblioteca iostream, indispensável quando vamos programar em c++ (contém algumas funções essenciais como o cin>> e o cout<<). Também utilizamos a biblioteca opencv.hpp, que se encontra na pasta opencv2 e que será informada ao compilador através do arquivo Makefile. Por último temos a biblioteca stack, que é parte da STL e que nos proporcionará o uso da classe stack (ou pilha). Temos as funções namespace que facilitam a nossa vida para que não precisemos digitar cv:: toda vez que formos utilizar a classe Mat.

Na função main() temos nossa função principal, e seus argumentos serão explicados numa sessão posterior.

Mat image; CvPoint p;
   image = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
   int mLinhas = image.rows; int nColunas = image.cols;

   int counter = 32, nBubbles[4] = {0,0,0,0};

   //Remove bubbles that touch the borders
   for(int i=0;i<mLinhas;i++){
   	if(image.at<uchar>(i,0) == 255) sfill(i,0,0,image);
   	if(image.at<uchar>(i,nColunas-1) == 255) sfill(i,nColunas-1,0,image);
   }
   for(int j=0;j<nColunas;j++){
   	if(image.at<uchar>(0,j) == 255) sfill(0,j,0,image);
   	if(image.at<uchar>(mLinhas-1,j) == 255) sfill(mLinhas-1,j,0,image);
   }
   //Search for bubbles
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) == 255){
   			sfill(i,j,counter,image);
   			counter+=5;
   		}
   	}
   }
   imshow("image",image);
   waitKey();

Começamos lendo a imagem com a função ìmread(), cuja documentação está disponível online, assim como de outras funções do opencv. Os atributos .rows e .cols da classe Mat retornam a quantidade de linhas e de colunas, respectivamente. Temos o objeto int counter que servirá para rotularmos as bolhas, e o vetor int nBubbles[] será utilizado posteriormente.

Os dois loops for() iniciais servem para varrer as bordas da imagem, e caso achem alguma bolha que toque na borda ela será removida (temos um seedFill com a mesma cor do fundo). Depois fazemos dois loops encadeados, para que possamos varrer todos os elementos da imagem (coluna por coluna de cada linha). Caso o programa encontre alguma bolha (image.at<uchar>(i,j) == 255) ele executará um seedFill com o valor de counter, que é o nosso rótulo. O valor de counter é iniciado em 32 e incrementado de 5 em 5 para podermos visualizar melhor as variações nos tons de cinza. Caso tivéssemos muitas bolhas isso não seria possível.

Por último temos:

imshow("image",image);
waitKey();

Essas linhas de código, respectivamente:

  1. Mostram a imagem em uma janela chamada "image";

  2. Esperam que o usuário tecle alguma coisa para que a janela possa ser fechada;

Na segunda parte do código iremos contar a quantidade de bolhas, separando as que não possuem buracos das que possum 1, 2 ou n buracos.

//Filling background
   sfill(0,0,254,image);
   //finding bubbles and labeling them as 1
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) == 255){
   			sfill(i,j,1,image);
   		}
   	}
   }
   //Finding bubbles with holes and labeling them 1+number of holes
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) == 0 && image.at<uchar>(i,j-1) != 0 && image.at<uchar>(i,j-1) != 254){
   			sfill(i,j-1,image.at<uchar>(i,j-1)+1,image);
   			sfill(i,j,254,image);
   		}
   	}
   }
   //Counting bubbles
   for(int i = 0;i<mLinhas;i++){
   	for(int j = 0;j<nColunas;j++){
   		if(image.at<uchar>(i,j) != 254){
   			nBubbles[(int)image.at<uchar>(i,j)-1]++;
   			sfill(i,j,254,image);
   		}
   	}
   }
   for(int i=0;i<4;i++){
   	cout<<"A imagem tem "<<nBubbles[i] <<" bolhas com "<<i<<" buracos\n";
   }
   return 0;
É importante mencionarmos que um trecho de código foi omitido aqui. Nele reabrimos a imagem e retiramos novamente as bolhas que tocam nas bordas. Reabrimos para termos novamente a imagem para não termos as bolhas rotuladas como foi feito na parte anterior do código.

Começaremos realizando um seedFill no fundo da imagem, com o valor de 254. Não rotulamos com 255 pois não conseguiríamos distinguir das bolhas. Desse modo distinguiremos o fundo do restante dos buracos das bolhas. Depois procuraremos bolhas e as rotularemos como 1. Não podemos rotular como 0 pois seria impossível distinguir dos buracos. O rótulo das bolhas é igual ao número de buracos que ela contém+1.

\$\text{nBuracos(i)} = \text{Rotulo(i)} - 1\$

Onde o índice i representa o número da bolha. O próximo loop procurará por pixels de valor 0, ou seja, buracos. Cada vez que ele acha um buraco, ele o preenche com o valor do fundo (254) e preenche a bolha com o rótulo_da_bolha+1 (mesmo que rotulo_da_bolha++).

Por fim, temos um loop para procurar bolhas na imagem. Uma bolha será qualquer região cujo valor seja diferente de 254. Quando encontrarmos uma bolha, usaremos o valor de seu rótulo para aumentar o valor de nBubbles na posição desejada, em que 0 é sem buracos. nBubbles é um vetor que armazena a quantidade de bolhas na imagem. Após utilizarmos seu valor, eliminamos a bolha, fazendo com que sua cor (ou seu rótulo) seja igual à cor de fundo.

2.3. Como o Código está abrindo a imagem?

A abertura da imagem se dá, no linux, devido à passagem de parâmetros durante a execução do código no terminal. O trecho de código:

int main(int argc, char *argv[])

Os argumentos da função main() são utilizados quando executamos o código no terminal do linux (tal coisa não acontece no Windows). O primeiro parâmetro diz quantos argumentos foram passados durante a chamada da execução do programa, e o segundo parâmetro, que é o vetor, retorna strings que correspondem ao nome do programa e dos parâmetros passados, respectivamente. A chamada do nosso programa é a seguinte (no terminal, após termos executado o Makefile):

$ ./contandoBolhas bolhas.png

Os valores de argc (argument count) e argv (argument value), são, respectivamente:

  • argc = 2

  • argv = {contandoBolhas, bolhas.png}

As primeiras linhas do código, apresentadas abaixo, fazem a leitura dos argumentos passados como parâmetro e consequente abertura do arquivo de imagem, utilizando a função ìmread(), cuja documentação está disponível juntamente da classe cv::Mat, também disponível nesse link.

2.4. Contando n buracos

O código prevê bolhas com até 3 buracos, mas como podemos fazer para que sejam previstos mais buracos? Basta alterarmos a dimensão do vetor nBubbles[] para a quantidade que desejarmos. Isso pode ser pedido ao usuário ou não.

Caso tenhamos uma quantidade bolhas maior que 252 (não é 255 devido aos rótulos que utilizamos no código) podemos criar uma escala de falsos tons de cinza, utilizando RGB e aumentarmos a quantidade possível de buracos (aumentando o tamanho do rótulo da bolha).

2.5. Qual o retorno da função?

A figura original é mostrada abaixo, juntamente com a saída do programa, logo em seguida.

índice
Figura 1. A imagem Bolhas.png
saida
Figura 2. A saída do programa contandoBolhas.cpp

Verificamos o funcionamento do código através da função cout<<, que irá imprimir na tela o resultado da contagem:

A imagem tem 13 bolhas com 0 buracos
A imagem tem 5 bolhas com 1 buracos
A imagem tem 2 bolhas com 2 buracos
A imagem tem 1 bolhas com 3 buracos

A imagem utilizada não é a mesma do tutorial de PDI, pois essa foi editada para adicionarmos alguns buracos a mais com o programa Pinta.