O objetivo dessa atividade é demonstrar, em código, como seria feito caso quiséssemos encontrar objetos em imagens e identificar quais destes contém buracos.
Introdução
Para o mundo de processamento de imagens e visão artificial, é muito comum e muito importante que haja a possibilidade de identificar que tipo de objeto e quantos objetos estão presentes em uma cena.
Peguemos a imagem a seguir:

De uma forma simplista e prática, diferentemente dos humanos, máquinas não conseguem processar (sem o uso de algoritmos voltados para isso) e diferenciar quais são os tipos de objetos existentes nessa imagem. Para nós pode parecer fácil saber que há uma quantidade "x" de objetos, dos quais, "y" contém furos (ou buracos) em seu interior, porém, para que uma máquina possa processar esse tipo de informação, faz-se necessário o uso de códigos e funções que faça uma varredura em busca do objetivo.
Desenvolvimento da atividade
Nesta atividade, a imagem assume apenas 2 valores de tom de cinza. O fundo com valor "0" (preto) e os objetos com valor "255" (branco). Assim, podemos rotular os objetos presentes na imagem com um valor de tom de cinza que difere cada um.
O papel da função FloodFill é exatamente este. Processar os valores e atribuir um novo valor de tom de cinza ao "aglomerado de pixel" que encontrar - o objeto.
Porém, torna-se falho caso haja um número de objetos superior a 255, uma vez que não haveria mais um valor de tom de cinza para associar a aquele objeto. Assim, faz-se necessário a atualização do algoritmo para uma quantidade maior de objetos (caso haja). Dessa forma, fora implementado um código que reconheça até 16581375 (255x255x255) objetos. Fizemos uso da ideia de que podemos associar em RGB, ou seja, ao invés de apenas 255 tons de cinza, agora temos 255 para o Red, 255 para o Green e 255 para o Blue.
É preciso retirar as "bolhas" que tocam a borda da imagem, uma vez que não podemos identificar se estas realmente são bolhas ou não, se tem buracos ou não. Dessa forma, o programa se baseará no seguinte fluxograma:

Código no OpenCV
O código feito pode ser visto a seguir, com os devidos comentários:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv){
Mat image; //Imagem a ser modificada
int width, height; //Largura e altura da imagem
int i,j; //Variáveis auxiliares para percorrer a imagem
CvPoint p; //Ponto da imagem a ser usado no floodfill
unsigned long int contador_bolhas = 0; //Variável para contagem de bolhas. A contagem é feita pela mudança RGB,
//sendo necessário ao menos 24 bits.
unsigned long int contador_bolhas_com_buracos = 0; //Variável para contagem de bolhas com buracos.
unsigned long int contador_bolhas_sem_buracos = 0; //Variável para contagem de bolhas sem buracos.
Vec3b cor_bolha_com_buraco(255,0,0); //Cor identificadora de bolhas com buracos (Azul)
Vec3b branco(255,255,255); //Cor das bolhas
Vec3b preto (0,0,0); //Cor de fundo da imagem
Vec3b label; //Cor para labelling
//============== Aquisição da Imagem ========================================
image = imread(argv[1],CV_LOAD_IMAGE_COLOR); //Leitura da imagem
if(!image.data){ //Teste de falha na leitura
cout << "Imagem nao carregou corretamente.\n";
return(-1);
}
width=image.size().width; //Largura da imagem
height=image.size().height; //Altura da imagem
//============== Remoção de bolhas que tocam a borda da imagem ==============
//Remoção na lateral esquerda: índice j = 0 para primeira coluna, e índice i varre todas as linhas
//Caso encontre um pixel branco, realiza o floodfill para a cor de fundo (preto)
i = 0; j = 0;
for(i = 0; i < height; i++){
if(image.at<Vec3b>(i,j) == branco){
p.x=j; p.y=i;
floodFill(image,p,preto);
}
}
//Remoção na lateral direita: índice j = largura - 1 para a última coluna, e índice i varre todas as linhas
//Caso encontre um pixel branco, realiza o floodfill para a cor de fundo (preto)
i = 0; j = width - 1;
for(i = 0; i < height; i++){
if(image.at<Vec3b>(i,j) == branco){
p.x=j; p.y=i;
floodFill(image,p,preto);
}
}
//Remoção na linha superior: indice i = 0 para a primeira linha, e índice j varre todas as colunas
//Caso encontre um pixel branco, realiza o floodfill para a cor de fundo (preto)
i = 0; j = 0;
for(j = 0; j < width; j++){
if(image.at<Vec3b>(i,j) == branco){
p.x=j; p.y=i;
floodFill(image,p,preto);
}
}
//Remoção na linha inferior: índice i = altura - 1, e índice j varre todas as colunas
//Caso encontre um pixel branco, realiza o floodfill para a cor de fundo (preto)
i = height - 1; j = 0;
for(j = 0; j < width; j++){
if(image.at<Vec3b>(i,j) == branco){
p.x=j; p.y=i;
floodFill(image,p,preto);
}
}
//================ Alteração da cor de fundo da imagem ==========================
p.x=0; p.y=0; //O ponto 0,0 faz parte do fundo da imagem
floodFill(image,p,Scalar(1,1,1)); //Após essa modificação, apenas os buracos estarão com cor preta (0,0,0)
imwrite("1.png", image);
//================ Identificar bolhas com buracos ===============================
for(int i=0; i<height; i++){
for(int j=0; j<width; j++){
if(image.at<Vec3b>(i,j) == preto){ //Caso seja encontrado um pixel pertencente a um buraco
if(image.at<Vec3b>(i,j-1) == cor_bolha_com_buraco){} //E o pixel anterior mostrar que a bolha já foi identificada,
//Não fazer nada
else if(image.at<Vec3b>(i,j-1) == branco){ //Caso o pixel anterior não tenha a cor identificadora, mas seja branco
p.x=j - 1; p.y=i; //Estamos tratando de uma bolha com buraco
floodFill(image,p,cor_bolha_com_buraco); //Então mudamos a bolha para uma cor identificadora
}
}
}
}
//================ Alteração da cor de fundo da imagem ==========================
p.x=0; p.y=0; //O ponto 0,0 faz parte do fundo da imagem
floodFill(image,p,preto); //Deixa a cor de fundo como preto novamente
imwrite("2.png", image);
//================ Realizar o labelling das bolhas ==============================
for(int i=0; i<height; i++){
for(int j=0; j<width; j++){
p.x=j; p.y=i;
if(image.at<Vec3b>(i,j) == branco){ //Caso seja encontrado uma bolha branca
contador_bolhas++; //Incrementa o contador para labelling
contador_bolhas_sem_buracos++; //Incrementa a contagem de bolhas sem buraco
label[0] = contador_bolhas & 0x0000FF; //Atribui à componente B, os 8 bits menos significativos do contador
label[1] = (contador_bolhas & 0x00FF00) >> 8; //Atribui à componente G, os bits 15-8 do contador
label[2] = (contador_bolhas & 0xFF0000) >> 16; //Atribui à componente R, os bits 23-16 do contador
floodFill(image,p,label); //Realiza o labelling da bolha
}
else if(image.at<Vec3b>(i,j) == cor_bolha_com_buraco){ //Caso seja encontrado uma bolha com a cor identificadora que possui buraco
contador_bolhas++; //Incrementa o contador para labelling
contador_bolhas_com_buracos++; //Incrementa a contagem de bolhas com buraco
label[0] = contador_bolhas & 0x0000FF; //Atribui à componente B, os 8 bits menos significativos do contador
label[1] = (contador_bolhas & 0x00FF00) >> 8; //Atribui à componente G, os bits 15-8 do contador
label[2] = (contador_bolhas & 0xFF0000) >> 16; //Atribui à componente R, os bits 23-16 do contador
floodFill(image,p,label); //Realiza o labelling da bolha
}
}
}
//============= Exibe resultado ===================================================
cout << "Número de bolhas sem buracos: " << contador_bolhas_sem_buracos << endl;
cout << "Número de bolhas com buracos: " << contador_bolhas_com_buracos << endl;
cout << "Número total de bolhas: " << contador_bolhas << endl;
namedWindow("Labelling",CV_WINDOW_KEEPRATIO); //Cria janela
imshow("Labelling", image); //Exibe imagem na janela criada
imwrite("labeling-result.png", image); //Gera arquivo com a imagem resultante
waitKey(); //Espera teclado ser pressionado para finalizar o programa
return 0;
}
Resultados
Foram observados as imagens processadas em diferentes momentos de execução do programa. O primeiro passo, que seria realizar a retirada das bolhas que tocam as bordas, apresentou o seguinte resultado:

Após retirada dos objetos que tocam as bordas, a imagem está preparada para ser tratada, ou seja, contar quantas destas que restaram contém buracos. Mais uma vez, a imagem anterior é modificada, fazendo a identificação de bolhas com buracos através da cor azul:

Por fim, podemos perceber abaixo as imagens com o floodfill, ou seja, com cada objeto e sua "identificação".

O código então retorna o resultado:
Número de bolhas sem buracos: 14
Número de bolhas com buracos: 7
Número total de bolhas: 21