Processamento Digital de Imagens
Introdução
Esta página tem como objetivo apresentar os resultados dos exercícios da disciplina de Processamento Digital de Imagens lecionada pelo professor Agostinho Brito Júnior no semestre 2016.1. Estes exercícios podem ser encontrados aqui. Para compilar os códigos apresentados neste tópico pode-se utilizar este arquivo.
Esta página estará sendo atualizada conforme a disciplina for sendo encaminhada.
Primeira Unidade
Manipulando pixels em uma imagem
Inicialmente é solicitado a implementação de um programa que exiba o negativo de uma imagem a partir
de dois pontos
lidos P1
e P2
. Os pontos são lidos via terminal na ordem P1.x
,
P1.y
, P2.x
e P2.y
. Para aplicar o efeito
de negativo, basta inverter cada uma das escalas das componentes RGB em cada um dos pixels
da região especificada. Para inverter é só cálcular: 255 - VALOR_ESCALA
.
O código do regions.cpp pode ser visto a seguir:
#include <iostream>
#include <cv.h>
#include <highgui.h>
using namespace cv;
using namespace std;
void usage(int qtdArgs, char** args);
int main(int qtdArg, char** args){
usage(qtdArg, args);
Mat image;
image= imread(args[1], CV_LOAD_IMAGE_COLOR);
int p1x = atoi(args[2]);
int p1y = atoi(args[3]);
int p2x = atoi(args[4]);
int p2y = atoi(args[5]);
if(!image.data)
cout << "nao abriu a imagem" << endl;
namedWindow("janela", WINDOW_AUTOSIZE);
for(int i=p1x;i< p2x;i++) {
for(int j=p1y; j< p2y;j++){
Vec3b valor = image.at<Vec3b>(i,j);
valor[0] = 255 - valor[0];
valor[1] = 255 - valor[1];
valor[2] = 255 - valor[2];
image.at<Vec3b>(i,j) = valor;
}
}
imshow("janela", image);
waitKey();
return 0;
}
void usage(int qtdArgs, char** args) {
if (qtdArgs < 6) {
printf("Uso: ./regions <nome_arquivo> X1 Y1 X2 Y2\n");
exit(0);
}
}
Para executar o programa basta realizar os seguintes comandos no terminal:
make regions
./regions wallpaper.jpg 100 100 350 650
O resultado está apresentado a seguir:
Em seguida, é solicitado que implemente-se um algoritmo em OpenCV que troque regiões de uma imagem
como
um quebra-cabeça, utilizando alguns construtores da classe Mat
.
O código implementado está apresentado a seguir:
#include <iostream>
#include <cv.h>
#include <highgui.h>
using namespace cv;
using namespace std;
void usage(int qtdArgs, char** args);
int main(int qtdArg, char** args) {
usage(qtdArg, args);
Mat image;
image= imread(args[1], CV_LOAD_IMAGE_COLOR);
int width = image.size().width;
int height = image.size().height;
//Este construtor mapeia uma região retangular definido pelo Rect(...) de uma imagem em uma nova instância de Mat
Mat A(image, Rect(0, 0, width/2, height/2));
Mat B(image, Rect(width/2, 0, width/2, height/2));
Mat C(image, Rect(0, height/2, width/2, height/2));
Mat D(image, Rect(width/2, height/2, width/2, height/2));
//Cria uma matrix de zeros de mesmo tamanho da original
Mat saida = Mat::zeros(image.size(), image.type());
Mat aux;
//Utiliza uma variavel auxiliar para mapear a nova região da matriz de saída
aux = saida.colRange(0, width/2).rowRange(0, height/2);
//Copia o conteúdo em D na região mapeada pela variável auxiliar
D.clone().copyTo(aux);
aux = saida.colRange(width/2, width).rowRange(0, height/2);
C.copyTo(aux);
aux = saida.colRange(0, width/2).rowRange(height/2, height);
B.copyTo(aux);
aux = saida.colRange(width/2, width).rowRange(height/2, height);
A.copyTo(aux);
if(!image.data)
cout << "nao abriu a imagem" << endl;
namedWindow("janela", WINDOW_AUTOSIZE);
imshow("janela", saida);
waitKey();
return 0;
}
O programa pou ser executado da seguinte forma: ./trocaregioes <nome_arquivo>
. O
resultado
pode ser visto a seguir:
Preenchendo Regiões com OpenCV
No programa labeling.cpp fornecido, cada objeto
encontrado é rotulado com um valor no tom de cinza que varia entre 0
e 255
.
Portanto, quando há mais do que 255 objetos na cena, a rotulação fica comprometida pois não mais mais
tons de cinza
que possam ser utilizados para rotular o restante dos objetos.
Uma possível solução para este problema seria resetar a contagem da variável nobjects
,
utilizando a função módulo.
O código modificado está apresentado a seguir:
/* ... Código antes */
nobjects=0;
for(int i=0; i<height; i++){
for(int j=0; j<width; j++){
if(image.at<uchar>(i,j) == 255){
// achou um objeto
nobjects++;
p.x=j;
p.y=i;
floodFill(image,p,nobjects % 255);
}
}
}
/* ... Código Depois */
Dessa forma, quando o valor de nobjects
passa a ser maior ou igual a 255, a escala de
cinza passa a ser "zerada".
O próximo passo é implementar uma solução que realize a contagem de objetos que possuem buracos e de objetos não possuem buracos, desconsiderando aqueles elementos que tocam a borda da imagem. Para isso, foi utilizado a seguinte imagem como teste:
O primeiro passo a se fazer é remover todas os elementos que tocam a borda. Para isso, aplica-se o
tom de
cinza branco (255) a todos os pixels da borda e então, realiza-se um floodfill para o tom de
cinza 0
igual ao do fundo da imagem.
Para diferenciar o fundo da imagem de um buraco, aplica-se novamente o floodfill; dessa vez
rotulando com tom de cinza 1
que no código abaixo, foi chamado de "BACKGROUND".
Depois disso, realiza-se uma contagem total de bolhas, procurando por um tom de cinza 255
e rotulando todos os elementos encontrados
inicialmente como "SEM BOLHAS".
Por fim, realiza-se a rotulagem por aqueles elementos que possuem bolhas. Para fazer isso,
percorre-se a matriz da imagem,
e sempre que for encontrado um pixel com rótulo SEM BOLHA ou COM BOLHA, armazena-se as coordenadas x e
y desse elemento na variável p
.
O laço é continuado até encontrar um pixel de tom de cinza 0
(caso em que foi encontrada
uma bolha), e nessa situação aplica-se o floodfill
no último elemento p
encontrado, rotulando este último agora como "COM BOLHA",
aproveita-se também e rotula-se o buraco
encontrado como "COM_BOLHA" (poderia ser outro rótulo) para que esta bolha não seja encontrada
novamente. Se o elemento p
já tiver sido rotulado
como "COM_BOLHA" não é necessário refazer o floodfill.
O código completo em C++
é apresentado abaixo. O código também pode ser baixado por aqui.
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
int main(int argc, char** argv){
Mat image, mask;
int width, height;
int nobjects;
int BRANCO = 255;
int PRETO = 0;
int BACKGROUND = 1;
int COM_BOLHA = 67;
int SEM_BOLHA = 196;
CvPoint p;
p.x = 0;
p.y = 0;
image = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
if(!image.data){
std::cout << "imagem nao carregou corretamente\n";
return(-1);
}
imshow("imageOriginal", image);
width=image.size().width;
height=image.size().height;
//Aplica 255 em todos os pontos na borda da
// imagem e aplica-se o floodfill para remover os elementos que tocam as bordas
for (int i = 0; i < width; i++) {
image.at<uchar>(i, height -1) = BRANCO;
image.at<uchar>(i, 0) = BRANCO;
}
for (int i = 0; i < height; i++) {
image.at<uchar>(0, i) = BRANCO;
image.at<uchar>(width -1, i) = BRANCO;
}
//Aplica-se o floodfill no ponto (0, 0) removendo todos os elementos da borda
floodFill(image, p, PRETO);
//Aplica-se o floodfill em todo o background da imagem
floodFill(image, p, BACKGROUND);
//Procurando por Elementos, julgando inicialmente que nenhum tem bolhas
int qtdTotal = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
if (image.at<uchar>(i, j) == BRANCO) {
p.x = j;
p.y = i;
floodFill(image, p, SEM_BOLHA);
qtdTotal++;
}
}
}
//Procurando por Elementos que possuem bolhas
int qtdComBolhas = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
//Armazene a posição do elemento encontrado, independente se for rotulado com bolha ou sem bolha
if (image.at<uchar>(i,j) == SEM_BOLHA || image.at<uchar>(i,j) == COM_BOLHA) {
p.x = j;
p.y = i;
} else if (image.at<uchar>(i,j) == PRETO) {
//Caso for encontrado um buraco (rotulo PRETO), rotule o ultimo elemento encontrado como "COM_BOLHA",
//se ele não já tiver sido rotulado como um
if (image.at<uchar>(p.y, p.x) == SEM_BOLHA) {
floodFill(image, p, COM_BOLHA);
qtdComBolhas++;
}
//Rotule o buraco encontrado como COM_BOLHA (poderia ser um rótulo diferente também)
p.x = j;
p.y = i;
floodFill(image, p, COM_BOLHA);
}
}
}
imshow("image", image);
imwrite("labeling.png", image);
printf("Quantidade de elementos sem bolhas: %d, com bolhas: %d\n", qtdTotal - qtdComBolhas, qtdComBolhas);
waitKey();
return 0;
}
O resultado está apresentado na imagem abaixo:
Manipulação de Histogramas
O primeiro exercício deste tópico solicita que se implemente um programa chamado
equalize.cpp
que realize
a equalização de histograma utilizando as funções do OpenCV.
O processo de equalização de imagens consiste em mapear uma determinada distribuição de um histograma em um outro, sendo que neste útlimo, a distribuição ficará mais uniformente distribuída. Como resultado, nota-se uma melhor distribuição do nível de intensidade (brilho) das cores do que na imagem original.
Um histograma de uma imagem nada mais é do que um gráfico que diz a quantidade de pixels que pertencem ao mesmo nível de brilho numa faixa de 0 (mais escuro) a 255 (mais claro). A seguir tem-se um exemplo de uma imagem e seu histograma:
É fácil perceber que para fazer a equalização da imagem acima é fácil pois ela possui somente um canal de cor por ser em Grayscale. No entanto, quando se trata de uma imagem colorida esse procedimento não é tão trivial, pois não faz sentido aplicar o processo de equalização diretamente no histograma de uma imagem RGB. A forma encontrada de aplicar a equalização neste caso foi converter a imagem em RGB para o formato YCrCb, já que esse padrão separa a intensidade luminosa dos outros componentes de cor.
O código da solução desse exercício é apresentado abaixo:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv){
Mat image;
VideoCapture cap;
vector<Mat> channels;
image= imread(argv[1], CV_LOAD_IMAGE_COLOR);
imshow("original", image);
Mat imagemYCrCb;
//Converte a imagem RGB para o padrão YCrCb
cvtColor(image, imagemYCrCb, CV_BGR2YCrCb);
split(imagemYCrCb, channels);
//Aplica-se a equalização no primeiro canal Y (intensidade luminosa)
equalizeHist(channels[0], channels[0]);
merge(channels, imagemYCrCb);
Mat result;
cvtColor(imagemYCrCb, result, CV_YCrCb2BGR);
namedWindow("image", WINDOW_AUTOSIZE);
imshow("image", result);
waitKey();
return 0;
}
O código também pode ser baixado aqui. Segue o resultado do algoritmo:
Sem Equalização:
Com Equalização:
A segunda parte desse exercício, solicita que seja implementado o programa
motiondetector.cpp
.
Este programa deve calcular o histograma das imagens captadas pela câmera e quando a diferença entre o
histograma atual e o anterior ultrapassar um determinado limiar, a aplicação deve acionar um aviso.
Nessa implementação, o aviso aparece como um círculo vermelho na parte superior esquerda da tela
quando o limiar do histograma é ultrapassado. O limiar é indentificado no código do programa como
sendo
a variável tolerância
. Também foi adicionado no código a variável COUNT_MAX
que expressa quantas
vezes consecutivas os histogramas captados devem ultrapassar o limiar para que o alarme efetivamente
ocorra.
Para auxiliar a comparação entre dois histogramas, fez-se uso do método compareHist do OpenCV. Além das variáveis de histogramas que se deseja comparar, essa função recebe como parâmetro o método de comparação, que no OpenCV são 4:
- Correlação
- Chi-Square
- Interseção
- Distância de Bhattacharyya
No código foi adotado o método de correlação que retorna um valor entre 0 e 1 (menos correlacionado
para o mais) e
com um valor de tolerância igual a 0.995
. O código implementado pode ser baixado aqui e o resultado
pode ser visto abaixo:
Como resultado, nota-se que o programa dispara o alarme toda vez que há uma alteração no histograma
da imagem captada.
Por exemplo, na imagem acima, toda vez que o livro era movido na frente da câmera, o alarme disparava.
Para alterar a sensibilidade da detecção basta altera os parâmetros de tolerância
e
COUNT_MAX
no código.
Filtragem no domínio espacial I
Esse exercício, solicita que modifique o código fornecido filtroespacial.cpp para que seja adicionado uma nova funcionalidade de aplicar o laplaciano do gaussiano nas imagens capturadas pela webcam.
O filtro gaussiano realiza a suavização das bordas da imagens, tornando a imagem menos nítida. Em
contrapartida,
esse filtro consegue amenizar o efeito do ruído apresentado na imagem. O código do
filtroespacial.cpp
foi modificado e foi adicionado a funcionaliade na tecla b
. O código implementado pode
ser baixado
aqui e o resulta da solução é apresentado a
seguir:
Filtro do Laplaciano:
Filtro do Laplaciano do Gaussiano:
Nota-se que o filtro do gaussiano atenuou o ruído da imagem, facilitando a identificação das bordas com o filtro laplaciano.
Filtragem no domínio espacial II
Este exercício solicita que seja implementado o programa tiltshift.cpp
. Este programa
deve realizar a leitura
de uma imagem para que se aplique o efeito do tiltshift, disponibilizando na interface 3
funcionalidades:
- Ajuste da altura da região central que entrará em foco;
- Ajuste para regular a força de decaimento da região borrada;
- Ajuste para regular a posição vertical que entrará em foco;
A implementação realizada inicialmente carrega a imagem em duas variáveis e aplica-se o filtro da média consecutivas vezes em uma delas:
image1 = imread("tilt/traffic_newyorkcity.jpg");
height = image1.size().height;
image2 = image1.clone();
Mat aux, mask, mask1;
float media[] = {1,1,1,
1,1,1,
1,1,1};
mask = Mat(3, 3, CV_32F, media);
scaleAdd(mask, 1/9.0, Mat::zeros(3,3,CV_32F), mask1);
mask = mask1;
image2.convertTo(aux, CV_32F);
for (int i = 0; i < deepMedia; i++) {
filter2D(aux, aux, aux.depth(), mask, Point(1, 1), 0);
}
aux=abs(aux);
aux.convertTo(image2, CV_8UC3);
Para modelar a região de desfoque ao longo do eixo vertical da imagem, foi utilizado a seguinte função:
$$ \alpha(x) = \frac{1}{2} ( \tanh \frac{x-l1}{d}-tanh\frac{x-l2}{d} ) $$
A função implementada em C++ que monta a matriz alfa está apresentada a seguir:
void calcAlpha() {
int l1 = - tamanho_faixa/2;
int l2 = -l1;
alpha = Mat::zeros(image1.rows, image1.cols, CV_32F);
beta = Mat::zeros(image1.rows, image1.cols, CV_32F);
int i, j;
for (i = 0; i < alpha.rows; i++) {
int x = i - (posicao_vertical + tamanho_faixa/2);
float alphaValue = 0.5f * (tanh((x - l1)/decaimento) - tanh((x - l2)/decaimento));
for (j = 0; j < alpha.cols; j++) {
alpha.at<float>(i, j) = alphaValue;
beta.at<float>(i, j) = 1 - alphaValue;
}
}
Mat auxA[] = {alpha, alpha, alpha};
Mat auxB[] = {beta, beta, beta};
merge(auxA, 3, alpha);
merge(auxB, 3, beta);
updateScene();
}
Onde foi assumido que a variável Mat alpha
irá ponderar a imagem original
e a variável Mat beta
, que é calculada como sendo \(1 - \alpha\),
ponderá a imagem borrada. Essa expressão é recalculada toda vez que há
alterações nos parâmetros da interface pelo usuário.
A imagem resultante com TiltShift é dada pela seguinte expressão:
$$ imgTiltShift = \alpha \times imagemOriginal + \beta \times imagemBorrada $$
A função em C++ criada para representar essa expressão é mostrada a seguir:
void updateScene() {
Mat outputImagemBorrada, outputImagemOriginal;
image1.convertTo(outputImagemOriginal, CV_32FC3);
image2.convertTo(outputImagemBorrada, CV_32FC3);
multiply(outputImagemOriginal, alpha, outputImagemOriginal);
multiply(outputImagemBorrada, beta, outputImagemBorrada);
Mat imageTiltShift;
add(outputImagemOriginal, outputImagemBorrada, imageTiltShift);
imageTiltShift.convertTo(imageTiltShift, CV_8UC3);
imshow("tiltshift", imageTiltShift);
}
Resultado:
O código completo pode ser baixado por aqui. Alguns outros resultados obtidos são apresentados a seguir:
Segunda Unidade
Filtragem no Domínio da Frequência
Neste exercício, é solicitado a implementação do Fitro Homomórfico para melhorar imagens com iluminação regular. Para isso, utilizou-se a imagem a seguir como base para tratamento do filtro homomórfico.
Para auxílio da aplicação das transformadas de Fourier direta e inversa, foram utilizados os métodos de dft e idft já implementados no OpenCV. Os passos utilizados para a aplicação do filtro foram os seguintes:
- Realiza-se a aplicação da Transformada de Fourier sobre a matriz de pixels da imagem, obtendo-se assim Z(x,y)
- Realiza-se a troca de quadrantes da imagem, apresentada no exercício "Manipulando pixels em uma imagem".
- Constrói-se a matriz do filtro H(x,y), utilizando a seguinte fórmula:
$$ H(u, v) = (\gamma_H - \gamma_L)(1 - e^{\frac{-cD^2(u,v)}{D_o^2}}) + \gamma_L $$
- Após isso, realiza-se uma multiplicação ponto a ponto da matriz H(u, v) por Z(x, y), obtendo-se G(x, y);
- Realiza-se então novamente a troca dos quadrantes mas agora para a nova imagem G(x,y);
- E, por fim, realiza-se a transformada inversa de Fourier sobre a iamgem G(x, y) e a imagem resultante é a imagem obtida.
Foi tentado incrementar o filtro homomórfico aplicando a função logarítmica antes da transformada (tratando-se o caso de log(0)), e aplicando a exponencial ao valor de saída como é mostrado nesse link. No entanto, não obteve-se um resultado agradável e por isso não foi colocado aqui.
Parte do código utilizado para a implementação do filtro homomófico é apresetando a seguir:
void calcHomomorphicFilter() {
Mat filter = Mat(padded.size(), CV_32FC2, Scalar(0));
Mat tmp = Mat(dft_M, dft_N, CV_32F);
for (int i = 0; i < dft_M; i++) {
for (int j = 0; j < dft_N; j++) {
float d2 = pow(i - dft_M/2.0, 2) + pow(j - dft_N/2.0, 2);
float exp = - (d2/pow(d0, 2));
float valor = (yh - yl)*(1 - expf(exp) ) + yl;
tmp.at<float> (i,j) = valor;
}
}
Mat comps[] = {tmp, tmp};
merge(comps, 2, filter);
Mat dftClone = imageDft.clone();
mulSpectrums(dftClone,filter,dftClone,0);
deslocaDFT(dftClone);
idft(dftClone, dftClone);
vector<Mat> planos;
split (dftClone, planos);
normalize(planos[0], planos[0], 0, 1, CV_MINMAX);
...
}
O código completo pode ser encontrado aqui.
A imagem tratada através do filtro homomórfico é mostrada a seguir:
Canny e a arte do pontilhismo
Este exercício solicita que seja implementado o programa cannypoints.cpp. A ideia é que seja utilizado as bordas detectadas através do algoritmo de Canny para melhorar a qualidade da imagem pontilhista.
A estratégia aqui adotada foi desenhar círculos nos contornos detectados pela algoritmo através das funções circle() e findContours() do OpenCV. O resultado da imagem é uma figura pontilhista com mais detalhes nas regiões próximas a borda.
A imagem original utilizada é apresentada a seguir:
Com o algoritmo desenvolvido, aplicou-se o efeito na imagem acima e o resultado sem a utilização do algoritmo de Canny é mostrado a seguir:
Uma vez aplicado o efeito, com a estratégia citada acima, o resultado da imagem foi o seguinte:
Foi realizado também um teste com a geração da imagem em formato colorido, o resultado está representado na imagem abaixo:
O algoritmo que gerou estas imagens pode ser baixado por aqui. O código foi implementado usando as trackbars disponibilizados pelo OpenCV e a parte mais significativa do código é mostrado a seguir:
//Referente ao desenho do pontilhismo usando as bordas de canny como referênca.
vector<vector<Point>> contornos;
vector<Vec4i> hierarquia;
findContours(border, contornos, hierarquia, CV_RETR_TREE, CV_CHAIN_APPROX_NONE);
for (int i = 0; i < contornos.size(); i++){
for (int j = 0; j < contornos[i].size(); j++) {
uchar cor = image.at<uchar>(contornos[i][j].y, contornos[i][j].x);
circle(points, cv::Point(contornos[i][j].x, contornos[i][j].y),
1,
CV_RGB(cor, cor, cor),
-1,
CV_AA);
}
}