1. Objetivo
No último trabalho da disciplina, nos aprofundaremos nos conceitos de detecção de bordas e os utilizaremos para criar uma figura pontilhista.[1]
2. O Pontilhismo
O pontilhismo começou no século XIX, com Georges Seurat, e é uma técnica de pintura saída do movimento impressionista.
Essa técnica consiste em pintar pequenas manchas, ou pontos, que provocam, por justposição, uma mistura óptica nos olhos do observador, formando a imagem.
A técnica de utilização de pontos coloridos justapostos também pode ser considerada o culminar do desprezo dos impressionistas pela linha, uma vez que esta é somente uma abstração do Homem para representar a natureza.
A figura a seguir mostra um dos mais famosos quadros impressionistas, de Georges Seurat.

Digitalmente, iremos simular a técnica desenhando pequenos círculos na imagem, separados por pequenos intervalos e que vão ser deslocados de seu centro de forma aleatória.
As imagens serão formadas por círculos grandes em relação à da imagem 1, devido à imagem ser discreta. Isso, entretanto, nos permitirá a melhor visualização da técnica. |
2.1. Aplicado o Pontilhismo Via Código
Um exemplo de código de pontilhismo é o do professor Agostinho Brito Jr.. A saída do programa, que é um exemplo da técnica digital do pontilhismo, é mostrada abaixo.

Podemos notar que, além de em preto e branco, a imagem apresenta uma certa falta de qualidade, no que se diz respeito à sua forma (separação de regiões - bordas).
3. Detecção de Bordas Com o Algoritmo 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), isto é, uma imagem binária.
Ele pode ser descrito pelo seguinte algoritmo:
-
Convolução com o filtro Gaussiano (suavização de ruído), cálculo da magnitude e ângulo do gradiente (detecção de máximos).
-
Supressão de Não-Máximos
-
Para aplicarmos a supressão de não máximos, devemos afinar as cristas largas do gradiente.
-
Os ângulos calculados anteriormente são divididos em intervalos de 45° para a classificação (vertical, horizontal, etc…)
-
Para os vizinhos na mesma orientação do pixel, comparar seus gradientes
-
-
Caso o gradiente deste pixel seja maior que seus dois vizinhos, seu valor é mantido. Caso contrário, seu valor é igual a zero.
-
-
Limiarização Com Histerese
-
Dois limiares, \$T_1\$ e \$T_2\$, com \$T_1>T_2\$, são utilizados
-
Se um pixel \$p(x,y) be T_1\$, logo é um ponto de borda forte
-
Se um pixel \$T_2 le p(x,y) le T_1\$, logo é um ponto de borda fraco
-
Se um pixel \$p(x,y) le T_2\$ ele é suprimido
-
Todos os pontos de borda forte são parte da fronteira
-
Para todos os vizinhos dos pontos de borda fraco, procurar nos seus 8-vizinhos se há algum ponto de borda forte. Caso haja, este é marcado como parte da fronteira.
-
Sugestão de Canny: \$T_1/T_2 = 3\text{ ou }T_1/T_2 = 2\$ |
3.1. Exemplo da Detecção de Bordas de Canny
Os exemplos utilizaram o algoritmo de Canny implementado pelo professor Agostinho, disponível aqui.


4. O Projeto: Unindo os dois Algoritmos
Para melhorar a qualidade de imagens que utilizam a técnica do pontilhismo, podemos utilizar a detecção de bordas de Canny. Para isso, desenharemos círculos de raio menor nas bordas, deixando a imagem com uma separação melhor de regiões. O algoritmo é mostrado à seguir, na listagem cannypoints.cpp.
#include <iostream>
#include <opencv2/opencv.hpp>
#include <fstream>
#include <iomanip>
#include <vector>
#include <algorithm>
#include <numeric>
#include <ctime>
#include <cstdlib>
using namespace std;
using namespace cv;
int step_slider = 5;
int step_slider_max = 20;
int jitter_slider = 3;
int jitter_slider_max = 10;
int raio_slider = 3;
int raio_slider_max = 10;
int top_slider = 10;
int top_slider_max = 200;
char TrackbarName[50];
Mat image, border, points;
vector<int> yrange;
vector<int> xrange;
int width, height, r,g,b;
int x, y;
void on_trackbar_canny(int, void*){
Canny(image, border, top_slider, 3*top_slider);
xrange.resize(height/step_slider);
yrange.resize(width/step_slider);
iota(xrange.begin(), xrange.end(), 0);
iota(yrange.begin(), yrange.end(), 0);
for(uint i=0; i<xrange.size(); i++){
xrange[i]= xrange[i]*step_slider+step_slider/2;
}
for(uint i=0; i<yrange.size(); i++){
yrange[i]= yrange[i]*step_slider+step_slider/2;
}
points = Mat(height, width, CV_8UC3, CV_RGB(255,255,255));
random_shuffle(xrange.begin(), xrange.end());
for(auto i : xrange){
random_shuffle(yrange.begin(), yrange.end());
for(auto j : yrange){
if(jitter_slider) x = i+rand()%(2*jitter_slider)-jitter_slider+1;
else x = i;
if(jitter_slider) y = j+rand()%(2*jitter_slider)-jitter_slider+1;
else y = j;
b = image.at<Vec3b>(x,y)[0];
g = image.at<Vec3b>(x,y)[1];
r = image.at<Vec3b>(x,y)[2];
circle(points,cv::Point(y,x),raio_slider,CV_RGB(r,g,b),-1,CV_AA);
}
}
for(int i = 0;i<height;i++){
for(int j = 0;j<width;j++){
// int border_radius = border.at<uchar>(i,j)*(raio_slider-2)/255;
//border_radius = (border_radius>0 ? border_radius : 1);
int border_radius = border.at<uchar>(i,j)*(top_slider/40 + 1)/255;
b = image.at<Vec3b>(i,j)[0];
g = image.at<Vec3b>(i,j)[1];
r = image.at<Vec3b>(i,j)[2];
circle(points,cv::Point(j,i),border_radius,CV_RGB(r,g,b),-1,CV_AA);
}
}
imshow("cannypoints",points);
}
int main(int argc, char** argv){
image= imread(argv[1],CV_LOAD_IMAGE_COLOR);
if(!image.data){
cout << "nao abriu" << argv[1] << endl;
cout << argv[0] << " imagem.jpg";
exit(0);
}
namedWindow("cannypoints",WINDOW_NORMAL);
imshow("cannypoints",image);
srand(time(0));
width=image.size().width;
height=image.size().height;
sprintf( TrackbarName, "Threshold inferior x %d", top_slider_max );
createTrackbar( TrackbarName, "cannypoints", &top_slider, top_slider_max, on_trackbar_canny );
sprintf( TrackbarName, "step x %d", step_slider_max );
createTrackbar( TrackbarName, "cannypoints", &step_slider, step_slider_max, on_trackbar_canny );
sprintf( TrackbarName, "Jitter x %d", jitter_slider_max );
createTrackbar( TrackbarName, "cannypoints", &jitter_slider, jitter_slider_max, on_trackbar_canny );
sprintf( TrackbarName, "Raio x %d", top_slider_max );
createTrackbar( TrackbarName, "cannypoints", &raio_slider, raio_slider_max, on_trackbar_canny );
waitKey();
return 0;
}
4.1. Criando As Trackbars e Explicando as Variáveis
A criação de trackbars e suas variáveis associadas foi explicada em códigos anteriores, e esse assunto não será mais abordado. Nos limitaremos somente à dizer quais variáveis são controladas. Lista de trackbars:
-
Threshold inferior: Limite inferior do algoritmo de Canny
-
Raio: Raio dos círculos do pontilhismo
-
Jitter: Regula o espalhamento (elemento colocado em um ponto aleatório dentro de um limite) dos elementos do pontilhismo
-
Step: A cada quantos pixels aplicaremos o pontilhismo
Todas as trackbars estão associadas à função on_trackbar_canny()
, que será explicada logo adiante.
4.2. Aplicando o Algoritmo à Imagem
A aplicação é feita utilizando a função on_trackbar_canny()
. Primeiramente, a linha de código Canny(image, border, top_slider, 3*top_slider);
calcula as bordas da imagem image
e as armazena na matriz border
.
Utilizamos \$T_1/T_2 = 3\$ devidos aos argumentos 3 e 4 da função. |
As variáveis xrange
e yrange
, ambas vetores, armazenam as coordenadas dos pontos em que vão ser colocados os círculos do pontilhismo. Isso é feito através das seguintes linhas:
xrange.resize(height/step_slider); //Armazena a quantidade de pontos, de acordo com step
yrange.resize(width/step_slider); // se a largura for 240 e o step for 5, teremos 48 pontos
//1 ponto a cada 5 é preenchido
iota(xrange.begin(), xrange.end(), 0); //preenche as posições com valores incrementais
iota(yrange.begin(), yrange.end(), 0); //começando do 0
for(uint i=0; i<xrange.size(); i++){
xrange[i]= xrange[i]*step_slider+step_slider/2; //posições dos pontos
}
for(uint i=0; i<yrange.size(); i++){
yrange[i]= yrange[i]*step_slider+step_slider/2;
}
O seguinte trecho, utilizando as funções random_shuffle
, para colocar os pontos em ordem aleatória, e rand(), para que o espalhamento do ponto, em função de jitter, seja aleatório, coloca os pontos na imagem, através da função cv::circle()
.
A matriz points é criada de forma que seja preenchida inicialmente com valores de 255, para que dê a impressão de ser uma tela branca atrás da imagem pintada.
points = Mat(height, width, CV_8UC3, CV_RGB(255,255,255));
random_shuffle(xrange.begin(), xrange.end());
for(auto i : xrange){
random_shuffle(yrange.begin(), yrange.end());
for(auto j : yrange){
if(jitter_slider) x = i+rand()%(2*jitter_slider)-jitter_slider+1;
else x = i;
if(jitter_slider) y = j+rand()%(2*jitter_slider)-jitter_slider+1;
else y = j;
b = image.at<Vec3b>(x,y)[0];
g = image.at<Vec3b>(x,y)[1];
r = image.at<Vec3b>(x,y)[2];
circle(points,cv::Point(y,x),raio_slider,CV_RGB(r,g,b),-1,CV_AA);
}
}
4.3. Realçando as Bordas
Para realçar as bordas, desenhamos, sem espalhamento, círculos de tamanho menor em relação aos círculos já desenhados. Isso é feito através das seguintes linhas:
int border_radius = border.at<uchar>(i,j)*(top_slider/40 + 1)/255;
circle(points,cv::Point(j,i),border_radius,CV_RGB(r,g,b),-1,CV_AA);
Essas linhas comparam para ver se um ponto é de borda ou não: border.at<uchar>(i,j)
ou é 0 ou é 255. Caso ele seja 0, a multiplicação deixará o círculo com raio(border_radius
) nulo, então nada será desenhado. Caso seja 255, iremos dividir esse valor por 255 (pois não queremos um círculo gigante) e multiplicá-lo por um valor que depende do Threshold.
Como o valor máximo do Threshold é 200, dividimos o valor por 40, logo o resultado varia entre 0 e 5. Somamos 1 para garantir que sempre desenharemos círculos nas bordas.
Uma segunda forma de implementar, é manter o raio da borda variante com o raio das outras figuras, por exemplo sendo 2 pixels menor. Isso é mostrado na linha comentada
int border_radius = border.at<uchar>(i,j)*(raio_slider-2)/255;
border_radius = (border_radius>0 ? border_radius : 1);
A segunda linha garante que o raio das bordas é sempre maior que 0.
A cor da borda é dada de acordo com a imagem original. |
5. O Programa
A tela de controle do programa é mostrada na figura 6. Resultados são mostrados nas riguras respectivas.



