1. Objetivo

Este projeto visa conhecermos melhor os histogramas e como podemos utilizá-los para realçar imagens, enxergando uma maior riqueza de detalhes.

2. Histograma

O histograma é um vetor que armazena as ocorrências de cada tom de cinza presentes. Geralmente é mostrado em um gráfico com as frequências de cada tom de cor, como o mostrado abaixo.

imhist brain
Figura 1. Histograma

A equalização de histogramas é uma operação que melhora o contraste, uniformizando o histograma da imagem de forma automática, redistribuindo os níveis de cinza existentes, e mapeando-os para novos níveis. Embora os picos e vales do histograma sejam mantidos, eles deslocados após a equalização. Este procedimento fará com que o número de intensidades na imagem resultante seja igual ou menor que na imagem original.

A equalização de histogramas segue o seguinte algoritmo:

  • Calcular histograma: \$h(r_k),k \epsilon [0,L-1\$]

  • Calcular histograma acumulado: \$ha(r_k) = \sum h(r_j), j = 0,1,...,k\$

  • Normalizar o histograma acumulado na faixa [0,L]: \$ha(r_k) = \frac{ha(r_k)*(L-1)}{ha(r_l)}\$

  • Transformar a imagem: \$s=ha(r_k),k \epsilon [0,L-1\$]

onde \$h(r_k)\$ é o valor do histograma na posição k, e \$r_k\$ é a intensidade. \$L\$ é geralmente a quantidade de bits da imagem, e \$r = 0\$ representa o preto e \$r = L -1\$ representa o branco.

3. O Código

O código é mostrado abaixo, e será explicado logo em seguida.

equalize.cpp
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(int argc, char** argv){
  Mat image, out;
  int width, height;
  bool color;
  vector<Mat> channels;
  cout<<"Voce quer uma imagem colorida?\n";
  cin>>color;
  char key;

  if(argc>1){
    image = imread(argv[1],CV_LOAD_IMAGE_COLOR);
    if(!image.data){
      cout<<"Nao abriu a imagem!\n";
      return -1;
    }

    width  = image.cols;
    height = image.rows;

    cout << "largura = " << width << endl;
    cout << "altura  = " << height << endl;

    if(color){
      cvtColor(image,out,CV_RGB2HSV);
      split(out,channels);
      equalizeHist(channels[2], channels[2]);
      merge(channels,out);
      cvtColor(out,out,CV_HSV2RGB);
    }
    else{
      cvtColor(image,image,CV_RGB2GRAY);
      equalizeHist(image, out );
    }
    namedWindow("source");
    namedWindow("output");
    imshow("souce", image);
    imshow("output",out);
    imwrite("output.png",out);
    waitKey();

  }
  else{
    VideoCapture cap;
    cap.open(0);

    if(!cap.isOpened()){
      cout << "cameras indisponiveis";
      return -1;
    }

    width  = cap.get(CV_CAP_PROP_FRAME_WIDTH);
    height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);

    cout << "largura = " << width << endl;
    cout << "altura  = " << height << endl;

    cout << "Pressione p para printar e ESC para sair!\n";
    while(1){
      cap >> image;
      flip(image,image,1);

      if(color){
        cvtColor(image,out,CV_BGR2HSV);
        split(out,channels);
        equalizeHist(channels[2], channels[2]);
        merge(channels,out);
        cvtColor(out,out,CV_HSV2BGR);
      }
      else{
        cvtColor(image,image,CV_BGR2GRAY);
        equalizeHist(image, out );
      }
      namedWindow("source");
      namedWindow("output");
      imshow("souce", image);
      imshow("output",out);
      key = waitKey(30);
      if(key==27) break;
      if(key==112){
        imwrite("saida.png",out);
        imwrite("entrada.png",image);
      }
    }
  }

  return 0;
}

3.1. As Variáveis Utilizadas

Mat image, out;
  int width, height;
  bool color;
  vector<Mat> channels;
  cout<<"Voce quer uma imagem colorida?\n";
  cin>>color;

Para a execução correta do programa, criamos duas variáveis da classe cv::Mat: uma de entrada e uma de saída (as matrizes image e out, respectivamente). Também temos a variável channels, um vetor de cv::Mat, que servirá para manipularmos os canais de cores da imagem, caso desejado. Por último, temos uma variável definida pelo usuário bool color;, que indica se o usuário deseja manipular as imagens em preto e branco(0) ou utilizando cores (1).

3.2. Trabalhando com Imagens e Vídeos

O programa é capaz de trabalhar com imagens e vídeos. Para isso, o usuário deve passar uma imagem como parâmetro caso deseje manipular uma imagem, e nenhum parâmetro caso deseje manipular um vídeo. O teste é feito com o primeiro if() do programa:

if(argc>1)

Caso opte por trabalhar com vídeo, um objeto da classe cv::VideoCapture será criado. Essa classe é utilizada para trabalharmos com vídeos, capturando-os de arquivos, sequências de imagens e câmeras. A documentação, para referência, pode ser encontrada aqui.

A função VideoCapture.open() faz com que o elemento da classe abra uma câmera. Utilizamos o parâmetro 0 para abrir a câmera padrão do notebook (webcam). Caso não seja possível abrir, o teste (utilizando a função .isOpened() - está aberto? ) deverá retornar falso e a execução do programa será parada. Por último, fazemos a atribuição do quadro do vídeo à variável image, com cap >> image.

VideoCapture cap;
cap.open(0);

if(!cap.isOpened()){
  cout << "cameras indisponiveis";
  return -1;
}
.
.
.
while(1){
  cap >> image;
  flip(image,image,1);
.
.
.
if(waitKey(30) >= 0) break;

A condição while(1) garante que o programa continue sendo executado (captura contínua de vídeo) até que o usurário pressione um botão (if(waitKey(30) >= 0) break;).

Utilizamos a função flip(entrada,saida,parâmetro) para inverter horizontalmente a imagem, devido a estarmos trabalhando com uma câmera frontal (webcam nativa do notebook).

3.3. Trabalhando com As Funções do OpenCV

Iremos detalhar a manipulação como foi feita para imagens coloridas e no modo vídeo, devido aos modos de imagem e em grayscale possuírem pequenas modificações.

if(color){
	cvtColor(image,out,CV_BGR2HSV);
	split(out,channels);
	equalizeHist(channels[2], channels[2]);
	merge(channels,out);
	cvtColor(out,out,CV_HSV2BGR);
}

Trabalhando com imagens coloridas, primeiramente devemos converter a imagem para outro padrão de cores, como o HSV ou YCrCb. Apesar do YCrCb ser mais adequado para trabalhar com as imagens (é um modelo bem mais usual para trabalho com imagens digitais e optimizado para tal) estamos mais habituados ao HSV. Mas por que precisamos converter a imagem de RGB para outro padrão de cores?

Fazemos isso pois a equalização de histograma não é um processo linear, e desse modo não é correto aplicá-la individualmente às componentes R,G e B; ou seja, a equalização não está relacionada às componentes de cor, mas sim à intensidade de cada pixel presente na imagem e, desse modo, o balanço de cores não deve ser alterado. Assim utilizamos o HSV, que nos permite mexer nas intesidades (V - Value) sem alterar o balanço de cores (H - Hue).

Para imagens em grayscale (P/B) só possuímos a componente de intensidade, e desse modo podemos aplicar diretamente a equalização.

Após termos convertido a imagem, utilizando a função cvtColor(entrada,saida,parâmetro), cuja documentação está disponível aqui, separamos as componentes de cor HSV utilizando a função split(entrada,vetor_de_componentes), que divide os canais em 3 elementos do vetor de matrizes channels[]. Em seguida, equalizamos o histograma das intensidades (que é o terceiro canal do HSV - channels[2]) utilizando a função equalizeHist(entrada,saida), cuja documentação também pode ser encontrada no site do OpenCV.

A equalização de histogramas segue o processo descrito acima, e como já temos uma função pronta do OpenCV decidimos por não implementá-la. O OpenCV possui uma base de usuários muito forte e os algoritmos são otimizados. Implementar a função seria somente um meio de expansão do aprendizado.

Após equalizado, juntamos novamente os canais da matriz utilizando a função merge(entrada,saida). A matriz de saída out é então convertida novamente para RGB (para correta exibição) e em seguida a saída é mostrada através da função imshow().

namedWindow("source");
namedWindow("output");
imshow("souce", image);
imshow("output",out);
}

4. Saída do Programa

Utilizamos a função waitKey(), que nos retorna o valor da tabela ascii que foi pressionada no teclado. O valor correspondente à tecla p é 112, conforme a tabela ascii. Caso o usuário pressione a tecla "p", é salvo um print da câmera, utilizando a função imwrite().

char key;
.
.
.
key = waitKey(30);
	if(key==27) break;
	if(key==112){
    	imwrite("saida.png",out);
    	imwrite("entrada.png",image);
    }

Testamos a saída do programa utilizando a webcam do computador em que foi rodado o programa, utilizando uma imagem colorida, e com uma imagem retirada da internet. As duas imagens de entrada e as duas de saída são mostradas a seguir.

entrada
Figura 2. Utilizando a Câmera do Notebook
saida
Figura 3. Saída do Programa
Unequalized Hawkes Bay NZ
Figura 4. Imagem Retirada da Internet
output
Figura 5. Imagem com Contraste Visivelmente Melhorado

É visível a melhoria do contraste na segunda imagem, mas não tanto na primeira, devido à condições de câmera e iluminação.