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.

Georges Seurat
Figura 1. Tarde de Domingo na Ilha de Grande Jatte

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.

heresjohnny
Figura 2. Imagem Original - Here’s Johnny (The Shining(1990))
heresjohnny2
Figura 3. Imagem Feita Com Pontilhismo

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:

  1. Convolução com o filtro Gaussiano (suavização de ruído), cálculo da magnitude e ângulo do gradiente (detecção de máximos).

  2. Supressão de Não-Máximos

    1. Para aplicarmos a supressão de não máximos, devemos afinar as cristas largas do gradiente.

      1. Os ângulos calculados anteriormente são divididos em intervalos de 45° para a classificação (vertical, horizontal, etc…​)

      2. Para os vizinhos na mesma orientação do pixel, comparar seus gradientes

    2. Caso o gradiente deste pixel seja maior que seus dois vizinhos, seu valor é mantido. Caso contrário, seu valor é igual a zero.

  3. Limiarização Com Histerese

    1. Dois limiares, \$T_1\$ e \$T_2\$, com \$T_1>T_2\$, são utilizados

    2. Se um pixel \$p(x,y) be T_1\$, logo é um ponto de borda forte

    3. Se um pixel \$T_2 le p(x,y) le T_1\$, logo é um ponto de borda fraco

    4. Se um pixel \$p(x,y) le T_2\$ ele é suprimido

    5. Todos os pontos de borda forte são parte da fronteira

    6. 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.

joker
Figura 4. Imagem Original - The Dark Knight (2008)
joker2
Figura 5. Bordas da Imagem com Threshold Inferior = 45

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.

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:

  1. Threshold inferior: Limite inferior do algoritmo de Canny

  2. Raio: Raio dos círculos do pontilhismo

  3. Jitter: Regula o espalhamento (elemento colocado em um ponto aleatório dentro de um limite) dos elementos do pontilhismo

  4. 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.

lord
Figura 6. Interface do Programa
alex
Figura 7. Alex DeLarge - A ClockWork Orange
alex2
Figura 8. Cannylhismo com Threshold = 20, step = 4, Jitter = 2 e Raio = 3
heresjohnny3
Figura 9. Comparação com a [Figura_3]

1. O autor tomou a liberdade de chamar o algoritmo aplicado, carinhosamente, de Cannylhismo