Programas/atividades desenvolvidas para a disciplina DIM0141 - VISÃO COMPUTACIONAL, da Universidade Federal do Rio grande do Norte UFRN
Prefácio
C++
utilizando-se da biblioteca OpenCV
e em ambiente Linux. Para compilar qualquer programa presente neste documento, pode-se fazer uso deste Makefile,
coloca o Makefile na mesma pasta do código fonte, extensão .cpp, e execute via terminal o comando
make <nome_do_programa>
. Todos os códigos encontram-se no Repositório do github.
1. Projetos e Tarefas da Primeira Unidade
1.1. Tarefa 1
-
Descreva a distribuição de células fotorreceptoras na retina e seu impacto na percepção visual
Possuimos dois tipos de células fotorreceptoras, são elas os cones e os bastonetes. Resumidamente os cones são responsaveis pela percepção das cores e os bastonetes pela intensidade luminosa, os cones predominantemente se localizam em um ponto na retina, chamado de fovea, já os bastonetes ficam distribuidos na periferia dessa região, essa distribuição faz com que os seres humanos possuam um ponto focal no centro do seu campo de visão, essa região central é a que possui maior nitidez, e devido a distribuição dos bastonetes, e sua sensibilidade a luminosidade, eles são responsaveis pela visão noturna e nossa visão periférica.
-
Qual a diferença entre os cones S, M e L? Quais deles são mais ativados quando uma luz amarela é projetada na retina?
A imagem a seguir resume bem o papel de cada tipo de cone na nossa perceção de cor, o tipo S é responsavel pela perceção da faixa mais proxima ao azul, M reage principalmente as frequencias proximas do verde e o L ao vermelho. A luz amarela possui comprimento de onda entre 565 e 590 nm, portando podemos observar pelo gráfico que é uma combinação, entre a reação do cone M com o cone L, provoca a reação que nos faz perceber a cor amarela.
-
Escreva um programa para
-
abrir uma imagem e exibir na tela os 3 canais separadamente
-
Compilando e Executando.
$ make q3a $ ./q3a <caminho_para_a_imagem>
Exemplo de funcionamento
Download do código completo: q3a.cpp
Clique aqui pra ver o código completo
//Programa que separa os três canais (RGB) e exibi uma imagem de cada canal
#include <opencv2/opencv.hpp>
using namespace cv;
int main(int argc, char**argv)
{
Mat img;
Mat ch[3];
img = imread(argv[1],CV_LOAD_IMAGE_COLOR);
if(!img.data){
std::cout << "imagem nao carregou corretamente\n";
return(-1);
}
imshow("Original", img);
ch[0] = Mat(img.size(), CV_8UC3);
ch[1] = Mat(img.size(), CV_8UC3);
ch[2] = Mat(img.size(), CV_8UC3);
for(int y = 0; y < img.rows; y++)
for(int x = 0; x < img.cols; x++)
{
ch[0].at<Vec3b>(y,x)[0] = img.at<Vec3b>(y,x)[0];//B
ch[1].at<Vec3b>(y,x)[1] = img.at<Vec3b>(y,x)[1];//G
ch[2].at<Vec3b>(y,x)[2] = img.at<Vec3b>(y,x)[2];//R
}
imshow("Canal B", ch[0]);
imshow("Canal G", ch[1]);
imshow("Canal R", ch[2]);
imwrite("canal_B.png", ch[0]);
imwrite("canal_G.png", ch[1]);
imwrite("canal_R.png", ch[2]);
imwrite("entrada_q3a.png", img);
waitKey(0);
return 0;
}
-
abrir uma imagem e exibir na tela a imagem invertida horizontalmente
Compilando e Executando.
$ make q3b $ ./q3b <caminho_para_a_imagem>
Exemplo de funcionamento
Download do código completo: q3b.cpp
Clique aqui pra ver o código completo
//Este programa recebe uma imagem como entrada e inverte a mesma, espelha na vertical,
//exibi e salva o resultado
#include <opencv2/opencv.hpp>
using namespace cv;
int main(int argc, char**argv)
{
Mat img;
Mat imgFlip;
img = imread(argv[1],CV_LOAD_IMAGE_COLOR);
if(!img.data){
std::cout << "imagem nao carregou corretamente\n";
return(-1);
}
imshow("Original", img);
imgFlip = img.clone();
for(int y = 0; y < img.rows; y++)
for(int x = 0; x < img.cols; x++)
{
imgFlip.at<Vec3b>(y,img.cols - 1 - x) = img.at<Vec3b>(y,x);
}
imshow("Imagem Invertida Horizontalmente", imgFlip);
imwrite("entrada_q3b.png", img);
imwrite("flipHorizontal.png", imgFlip);
waitKey(0);
return 0;
}
-
abrir duas imagens (a e b) de mesmo tamanho e exibir na tela uma nova imagem (c) com o blending entre ambas, usando uma combinação linear entre elas
Compilando e Executando.
$ make q3c $ ./q3c <caminho_para_a_imagem A> <caminho_para_a_imagem B>
Exemplo de funcionamento
Download do código completo: q3c.cpp
Clique aqui pra ver o código completo
#include <opencv2/opencv.hpp>
using namespace cv;
const char* mainWindow = "Blending";
Mat imgA, imgB;
Mat imgC;
void blending(int, void*);
int main(int argc, char**argv)
{
if(argc != 3)
{
std::cout << "Voce deve informar o caminho de duas imagens de mesmo tamanho\n";
return(-1);
}
imgA = imread(argv[1],CV_LOAD_IMAGE_COLOR);
imgB = imread(argv[2],CV_LOAD_IMAGE_COLOR);
//Teste se as imagens foram carregadas corretamente
if(!imgA.data){
std::cout << "imagem "<< argv[1] <<"nao carregou corretamente\n";
return(-1);
}
if(!imgB.data){
std::cout << "imagem "<< argv[2] <<"nao carregou corretamente\n";
return(-1);
}
//Testa se as imagens possuem o mesmo tamanho
if(imgA.size() != imgB.size())
{
std::cout << "As imagens devem possuir o mesmo tamanho\n";
return(-1);
}
imgC = imgB.clone();
//Cria o slider, para ajustar a variavel alpha do calculo do blending
namedWindow(mainWindow, WINDOW_AUTOSIZE);
createTrackbar( "Alpha [0-100%]", mainWindow,
NULL,
100,
blending);
int key;
while(true)
{
imshow(mainWindow, imgC);
key = waitKey(10);
if(key == 27)//esq
break;
}
return 0;
}
void blending(int pos, void*)
{
float alpha = pos/100.0;
for(int y = 0; y < imgA.rows; y++)
for(int x = 0; x < imgA.cols; x++)
{
imgC.at<Vec3b>(y,x) = imgA.at<Vec3b>(y,x)*alpha + imgB.at<Vec3b>(y,x)*(1.0 - alpha);
}
}
-
salvar uma nova imagem com o seguinte gradiente vertical
Compilando e Executando.
$ make q3d $ ./q3d <caminho_para_a_imagem>
Exemplo de funcionamento
Download do código completo: q3d.cpp
Clique aqui pra ver o código completo
#include <opencv2/opencv.hpp>
using namespace cv;
int main(int argc, char**argv)
{
Mat img;
Mat result;
Mat gradient;
img = imread(argv[1],CV_LOAD_IMAGE_GRAYSCALE);
if(!img.data){
std::cout << "imagem nao carregou corretamente\n";
return(-1);
}
imshow("Original", img);
result = Mat(img.size(), CV_32FC1);
gradient = img.clone();
for(int y = 0; y < img.rows; y++)
for(int x = 0; x < img.cols; x++)
{
result.at<float>(y,x) = (img.at<uint8_t>(y,x)/255.0)*y*(1.0/img.rows);
gradient.at<uint8_t>(y,x) = y*(255.0/img.rows);
}
//converte para um formato que eh possivel salvar, neste caso equivalente ao grayscale
//https://docs.opencv.org/2.4/modules/core/doc/basic_structures.html#void%20Mat::convertTo(OutputArray%20m,%20int%20rtype,%20double%20alpha,%20double%20beta)%20const
result.convertTo(result, CV_8UC1, 255);
imshow("Resultado", result);
imshow("Gradiente", gradient);
imwrite("gradiente.png", gradient);
imwrite("imagem_com_gradiente.png", result);
imwrite("entrada_q3d.png", img);
waitKey(0);
return 0;
}
-
Considere o formato de imagem NetPBM
- Qual a diferença entre os números mágicos P1, P2, P3, P4, P5 e P6?
-
P1: BitMap, extenção .pbm, valores: 0-1 (Preto e Branco), ASCII
P2: GrayMap, extenção .pgm, valores:0-255, ASCII
P3: PixMap, extenção .ppm, valores: 3 canais(RGB) 0-255 cada canal, ASCII
P4: BitMap, extenção .pbm, valores: 0-1 (Preto e Branco), Binário
P5: GrayMap, extenção .pgm, valores:0-255, Binário
P6: PixMap, extenção .ppm, valores: 3 canais(RGB) 0-255 cada canal, Binário
- Converta uma imagem jpg para PBM (ASCII) utilizando convert e display para exibir
$ convert -compress none cafe.jpg cafe_ascii.pbm $ display cafe_ascii.pbm
Imagem convertida: cafe.pbm
- Converta a mesma imagem para PBM (binário) e para PPM (binário). Compare o tamanho dos 4 arquivos de imagem
Arquivo |
Tamanho |
cafe.jpg |
98.9Kb |
cafe_ascii.pbm |
600.5Kb |
cafe_binary.pbm |
37.5Kb |
cafe_ascii.ppm |
3.2Mb |
cafe_binary.ppm |
900Kb |
- Por que o formato binário ocupa menos espaço que o formato ASCII?
-
Cada caracter ASCII ocupa 8 bits, já no binário cada bit representa um valor de pixel, no PBM já no PPM, cada conjunto de 3 caracteres asciis represantam um pixel(RGB).
- Por que o formato PPM binário ocupa mais espaço que o formato PBM binário?
-
O formato PPM é um PixMap já o PBM é um BitMap, ou seja, cada unidade do PPM é é formado por um conjunto de 3 bytes, para representar as componentes RGB do pixel, já no padrão PBM, cada unidade representa um unico valor, 0 ou 1, ou seja 1 bit por unidade (pixel de 1 bit).
- Quais desses formatos são vetoriais e quais são bitmaps? BMP, SVG, JPG, EPS, PNG
- Formatos bitmaps
-
BPM, JPEG, PNG.
- Formatos vetorais
-
SVG, EPS.
- Imagens de algumas aplicações possuem um nível de ruído considerável, principalmente aquelas que captam em níveis baixos de iluminação, como na captura de imagens astronômicas. Uma das formas de atenuar esse tipo de ruído é através da média de inúmeras imagens. Utilizando as 9 imagens disponibilizadas, crie um programa que gere uma nova imagem com o ruído atenuado.
-
Compilando e Executando.
$ make q6 $ ./q6
Exemplo de funcionamento
Download do código completo: q6.cpp
Entrada utilizada: imagensComRuido
Clique aqui pra ver o código completo
//Programa que tira a media aritmetica de N imagens
//Este programa carrega todas as imagens de um arquivo, cujo caminho (path)
//esta indicado pela variavel path, e calcula a media de todas essas imagens
#include <opencv2/opencv.hpp>
using namespace cv;
int main(int argc, char**argv)
{
String path("./imagensComRuido/*.jpg");
std::vector<String> fn;
std::vector<Mat> imagens;
Mat media;
glob(path, fn, true);
//carrega as N imagens
for(size_t i = 0; i < fn.size(); i++)
{
Mat im = imread(fn[i], CV_LOAD_IMAGE_GRAYSCALE);
if(!im.data)continue;//caso falhe na leitura, vai para o proximo arquivo
imagens.push_back(im);
}
if(imagens.empty())
{
std::cerr << "Erro ao carrega as imagens de " << path << "\n";
return -1;
}
media = Mat::zeros(imagens[0].size(), CV_32FC1);
//calcula da media das N imagens
for(size_t i = 0; i < imagens.size(); i++)
{
for(int y = 0; y < imagens[0].rows; y++)
for(int x = 0; x < imagens[0].cols; x++)
{
media.at<float>(y,x) += imagens[i].at<uint8_t>(y,x)/(float)imagens.size();
}
}
media.convertTo(media, CV_8UC1, 1.0, 0);
imshow("Media", media);
imwrite("Media.png",media);
waitKey(0);
return 0;
}
1.2. Tarefa 2
1.2.1. Aumentar brilho, pintar faixas
Compilando e executando.
$ make efeito $ ./efeito <caminho_para_a_imagem>
Download do código completo: efeito.cpp
Clique aqui pra ver o código completo
#include <cmath>
#include <opencv2/opencv.hpp>
using namespace cv;
#define LINE_WIDTH 5//px
void gammaCorrection(const Mat &img, Mat &out,double gamma)
{
static Mat lookupTable(1, 256, CV_8U);
static bool first = true;
out = img.clone();
for(int i = 0; i < 256 && first; i++)
lookupTable.ptr()[i] = saturate_cast<uint8_t>(pow(i/255.0, gamma)*255);
first = false;
cv::LUT(img, lookupTable, out);
}
int main(int argc, char** argv){
Mat img;
Mat result;
img = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
if(!img.data)
{
std::cerr << "Erro ao carregar imagem!\n";
return -1;
}
gammaCorrection(img, result, 0.05);
imshow("Saida", img);
waitKey(0);
imshow("Saida", result);
waitKey(0);
//pinta faixas pretas com largura de LINE_WIDTH pixels
//espaçadas de LINE_WIDTH
for(int y = 0; y < img.rows; y++)
for(int x = 0; x < img.cols; x++)
{
result.at<uint8_t>(y,x) = 0;
if((x+1)%LINE_WIDTH == 0)x += LINE_WIDTH;
}
imshow("Saida", result);
waitKey(0);
imwrite("entradaEfeito.png", img);
imwrite("resultadoEfeito.png", result);
return 0;
}
1.2.2. Suavização da imagem anterior
Este programa recebe como parametro o caminho para uma imagem e aplica um filtro de suavização convolucional (filtro de borramento). O programa aplica o filtro toda vez que o usuário pressiona qualquer tecla(com excesão do ESQ), aplicando assim uma suavização em cascata, o programa se encerra ao ser pressionado a tecla ESQ.
Neste exemplo o programa foi utilizado para suavizar a imagem resultando do programa anterior.
Compilando e executando.
$ make suavizacao $ ./suavizacao <caminho_para_a_imagem>
Download do código completo: suavizacao.cpp
Clique aqui pra ver o código completo
#include <string>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main(int argc, char** argv)
{
Mat img;
Mat res;
Mat mask;
float kernel_media[] = {1, 1, 1,
1, 1, 1,
1, 1, 1};
img = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
if(!img.data)
{
std::cerr << "Erro ao carregar imagem\n";
return -1;
}
mask = Mat(3,3, CV_32F, kernel_media)/9.0;
//Aplica filtro de suavização, filtro de Media
filter2D(img, res, -1, mask);
imshow("Entrada", img);
int key;
string fileName;
int count = 0;
while(true)
{
//Salva cada imagem resultante para gerar o gif, com o comando convert -delay 150 tmp/* animation.gif
fileName = string("tmp/a") + to_string(count++) + ".png";
imwrite(fileName, res);
filter2D(res, res, -1, mask);
imshow("Saida", res);
key = waitKey(0);
if(key == 27)
break;
}
imwrite("entradaSuavizacao.png",img);
imwrite("resultadoSuavizacao.png",res);
return 0;
}
1.2.3. Filtro de suavização seletiva
Compilando e executando.
$ make suavizacao2 $ ./suavizacao2 <caminho_para_a_imagem>
Download do código completo: suavizacao2.cpp
Clique aqui pra ver o código completo
#include <string>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
#define MEDIA_SIZE 10.0
void convFilter(const Mat &src,Mat &dest ,const Mat &mask);
int main(int argc, char** argv)
{
Mat img, result, tmp;
Mat blurImg, contourImg;
Mat mask;
float kernel_media[(int)(MEDIA_SIZE*MEDIA_SIZE)];
float kernel_gaussian[] = {1, 2, 1,
2, 4, 2,
1, 2, 1};
float kernel_laplaciano[] = {1, 1, 1,
1, -8, 1,
1, 1, 1};
for(uint i = 0; i < (MEDIA_SIZE*MEDIA_SIZE); i++)
kernel_media[i] = 1.0/(MEDIA_SIZE*MEDIA_SIZE);
img = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
if(!img.data)
{
std::cerr << "Erro ao carregar imagem\n";
return -1;
}
//suavizacao gaussiana
mask = Mat(3,3, CV_32F,kernel_gaussian)/16.0;
convFilter(img, contourImg, mask);
//realçar contornos
mask = Mat(3,3, CV_32F,kernel_laplaciano);
convFilter(contourImg, contourImg, mask);
contourImg.convertTo(tmp, CV_8U, 255.0/2040.0, 127.0);
/*Exibir e salvar imagem*/
imshow("Contornos", tmp);
imwrite("contorno.png", tmp);
//Borramento
mask = Mat(MEDIA_SIZE, MEDIA_SIZE, CV_32F,kernel_media);
convFilter(img, blurImg, mask);
blurImg.convertTo(tmp, CV_8U);
/*Exibir e salvar imagem*/
imshow("Borrada", tmp);
imwrite("borrada.png", tmp);
result = blurImg - contourImg;
result.convertTo(result, CV_8U);
//trecho apenas para exibir e salvar as imagens
imshow("Entrada", img);
imshow("Saida", result);
imwrite("entradaSuavizacao2.png", img);
imwrite("saidaSuavizacao2.png", result);
waitKey(0);
return 0;
}
void convFilter(const Mat &src,Mat &dest ,const Mat &mask)
{
float sum;
Mat src_f;
src.convertTo(src_f, CV_32F);
dest = Mat::zeros(src.size(), CV_32F);
for(int y = mask.rows/2; y < (src.rows - mask.rows/2); y++)
for(int x = mask.cols/2; x < (src.cols - mask.cols/2); x++)
{
sum = 0;
for(int v = 0; v < mask.rows; v++)
for(int u = 0; u < mask.cols; u++)
{
sum+= src_f.at<float>(y - mask.rows/2 + v,x - mask.cols/2 + u)*mask.at<float>(v,u);
}
dest.at<float>(y,x) = sum;
}
}
1.3. Tarefa 4
1.3.1. Análise de alguns tipos de ruídos em uma imagem e calculo do PSNR
PSNR: peak signal to noise ratio, é uma forma de medir a qualidade de uma imagem, quanto maior esse valor, melhor a imagem.
Compilando e executando.
$ make analise_ruido1 $ ./analise_ruido1
Download do código completo: analise_ruido1.cpp
Clique aqui pra ver o código completo
#include <opencv2/opencv.hpp>
#include <string>
#include <cmath>
using namespace cv;
using namespace std;
#define I_MAX 255
float calcPSNR(const Mat &ref, const Mat &img);
void calcHistograma(const Mat &src, uint32_t histograma[]);
//Esta função cria uma imagem, contendo o plot do histograma
//com a dimensão de 256x200 px
void drawHistograma(const uint32_t histograma[], Mat &imgH);
int main(int argc, char** argv){
Mat orig, imgH;
String path("./primeiroConjunto/*.jpg");
std::vector<String> fn;
std::vector<Mat> imagens;
uint32_t histograma[I_MAX+1];
glob(path, fn, true);
orig = imread(fn[0], CV_LOAD_IMAGE_GRAYSCALE);
if(!orig.data)
{
std::cerr << "Erro ao carregar a imagem original" << '\n';
return-1;
}
// carrega as N imagens
for(size_t i = 0; i < fn.size(); i++)
{
Mat im = imread(fn[i], CV_LOAD_IMAGE_GRAYSCALE);
if(!im.data)continue;//caso falhe na leitura, vai para o proximo arquivo
imagens.push_back(im);
calcHistograma(im, histograma);
drawHistograma(histograma, imgH);
imshow("imagem", im);
imshow("Histograma", imgH);
imwrite(string("imagem") + to_string(i) + string(".png"), im);
imwrite(string("histograma") + to_string(i) + string(".png"), imgH);
std::cout << "PSNR:"<< calcPSNR(imagens[0], im) << '\n';
waitKey(0);
}
if(imagens.empty())
{
std::cerr << "Erro ao carrega as imagens de " << path << "\n";
return -1;
}
return 0;
}
void calcHistograma(const Mat &src, uint32_t histograma[])
{
memset(histograma, 0, (I_MAX+1)*sizeof(uint32_t) ); //zera todas as posições do vetor de freq.
for(int y = 0; y < src.rows; y++)
for(int x = 0; x < src.cols; x++)
histograma[src.at<uint8_t>(y,x)]++;
}
uint32_t maxValue(const uint32_t v[], size_t size)
{
uint32_t max = 0;
for(size_t i = 0; i < size; i++)
max = (v[i] > max)?v[i]:max;
return max;
}
void drawHistograma(const uint32_t histograma[], Mat &imgH)
{
imgH = Mat::zeros(200, 256, CV_8U);
uint32_t freqMax = maxValue(histograma, I_MAX+1);
for(uint32_t i = 0; i < I_MAX+1; i++)
{
line(imgH, Point(i, 200), Point(i, 200-histograma[i]*(200.0/freqMax)), 255);
}
}
float calcPSNR(const Mat &ref, const Mat &img)
{
float mse = 0;
for(int y = 0; y < ref.rows; y++)
for(int x = 0; x < ref.cols; x++)
{
mse += pow(img.at<uint8_t>(y,x) - ref.at<uint8_t>(y,x), 2.0);
}
mse = mse/(ref.cols*ref.rows);
return 10.0*log10((float)(I_MAX*I_MAX)/mse);
}
Nome | PSNR | Análise |
---|---|---|
Image 0 |
inf |
Imagem original(sem ruído). Como a imagem 0 foi utilizada como imagem de referencia, por essa razão o PSNR deu inf. |
Image 1 |
34.5399 |
Olhando apenas para a imagem, não conseguimos notar muita diferença, mas se olharmos para o histograma podemos observar um padrão no ruído do tipo gaussiano. |
Image 2 |
15.5066 |
Conseguimos observar, facilmente pela imagem, que o ruído é do tipo sal e pimenta, mas observando também o histograma dessa imagem, podemos notar que o comportamento do ruído se aproxima de uma distribuição normal. |
Image 3 |
31.4324 |
A maior diferença nesta imagem é o histograma, nele podemos notar que o ruído possui uma distribução aproximadamente exponencial. |
Image 4 |
18.2371 |
Na imagem 4 nota-se facilmente o padrão típico de um ruído tipo sal. |
Image 5 |
9.82257 |
Podemos notar na imagem 5 o padrão de ruído do tipo sal e pimenta. |
Image 6 |
18.5184 |
Ruído do tipo pimenta. |
Image 7 |
37.3253 |
Aqui só conseguimos notar a diferença no histograma, por meio dele podemos observar um ruído com comoprtamento gaussiano novamente. |
1.3.2. Filtros para suavização de ruídos
2. Bibliografia
-
Rafael Gonzalez. 'Processamento Digital de Imagens'. Addison-Wesley. 1990. 2 ed.