Aqui irei mostrar as atividades desenvolvidas durante meu semestre na matéria de Processamento Digital de Imagens, matéria do curso de Engenharia da Computação da Universidade Federal do Rio Grande do Norte. O roteiro com as atividades propostas e explicações mais detalhadas sobre o funcionamento do opencv encontram-se no repositório do professor Agostinho neste link.
Atividade 1: Utilizando o programa pixels.cpp como referência, implemente um programa regions.cpp. Esse programa deverá solicitar ao usuário as coordenadas de dois pontos P1 e P2 localizados dentro dos limites do tamanho da imagem e exibir que lhe for fornecida. Entretanto, a região definida pelo retângulo de vértices opostos definidos pelos pontos P1 e P2 será exibida com o negativo da imagem na região correspondente. O efeito é ilustrado na figura abaixo.
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
int main(int, char**) {
cv::Mat image;
image = cv::imread("../biel.png", cv::IMREAD_COLOR);
if (!image.data) {
std::cout << "nao abriu biel.png" << std::endl;
return -1;
}
cv::namedWindow("janela", cv::WINDOW_AUTOSIZE);
int x1, y1, x2, y2;
std::cout << "Digite as coordenadas do ponto P1(x1, y1):" << std::endl;
std::cin >> x1 >> y1;
std::cout << "Digite as coordenadas do ponto P2(x2, y2):" << std::endl;
std::cin >> x2 >> y2;
int minX = std::min(x1, x2);
int maxX = std::max(x1, x2);
int minY = std::min(y1, y2);
int maxY = std::max(y1, y2);
for (int i = minX; i < maxX; i++) {
for (int j = minY; j < maxY; j++) {
image.at<cv::Vec3b>(i, j)[0] = 255 - image.at<cv::Vec3b>(i, j)[0]; // negativo B
image.at<cv::Vec3b>(i, j)[1] = 255 - image.at<cv::Vec3b>(i, j)[1]; // negativo G
image.at<cv::Vec3b>(i, j)[2] = 255 - image.at<cv::Vec3b>(i, j)[2]; // negativo R
}
}
cv::imwrite("regions.png", image);
cv::imshow("janela", image);
cv::waitKey();
return 0;
}
No início do código é feita a leitura da imagem e logo em seguida solicita ao usuário dois pontos para definir a região da imagem que receberá o negativo. Após isso é feito o cálculo do negativo da região solicitada pelo usuário, para isso foi feito dois loops que percorrem a região definida e realiza o cálculo do negativo (255 - valor atual do pixel). A figura abaixo mostra o resultado do código para um entrada com P1(50,50) e P2(200,200).
Atividade 2: Utilizando o programa pixels.cpp como referência, implemente um programa trocaregioes.cpp. Seu programa deverá trocar os quadrantes em diagonal na imagem. Explore o uso da classe cv::Mat e seus construtores para criar as regiões que serão trocadas. O efeito é ilustrado na figura abaixo. Assuma que a imagem de entrada tem dimensões múltiplas de 2 para facilitar a implementação do processo de troca.
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
int main(int, char**) {
// Carregar a imagem original
cv::Mat image = cv::imread("../biel.png");
if (image.empty()) {
std::cout << "nao abriu biel.png" << std::endl;
return -1;
}
// Obter as dimensões da imagem
int width = image.cols;
int height = image.rows;
// Dividir a imagem em quatro partes iguais
cv::Rect topLeftRect(0, 0, width / 2, height / 2);
cv::Rect topRightRect(width / 2, 0, width / 2, height / 2);
cv::Rect bottomLeftRect(0, height / 2, width / 2, height / 2);
cv::Rect bottomRightRect(width / 2, height / 2, width / 2, height / 2);
cv::Mat topLeft = image(topLeftRect);
cv::Mat topRight = image(topRightRect);
cv::Mat bottomLeft = image(bottomLeftRect);
cv::Mat bottomRight = image(bottomRightRect);
// Juntar as partes em quadrantes diferentes
cv::Mat result(height, width, image.type());
topLeft.copyTo(result(cv::Rect(width / 2, height / 2, width / 2, height / 2)));
topRight.copyTo(result(cv::Rect(0, height / 2, width / 2, height / 2)));
bottomLeft.copyTo(result(cv::Rect(width / 2, 0, width / 2, height / 2)));
bottomRight.copyTo(result(cv::Rect(0, 0, width / 2, height / 2)));
// Mostrar a imagem resultante
cv::imshow("Result", result);
cv::waitKey(0);
return 0;
}
No inicio do código é feita a leitura da imagem, em seguida é criado quatro retângulos baseados nas dimensões da imagem e com eles faz-se 4 imagens referente aos quatro quadrantes da imagem original. É criado em seguida um cv::Mat com as mesmas dimensões da imagem original, e com as 4 imagens dos quadrantes da imagem original, é utilizada a função copyTo para colocar as imagens nos quadrantes trocados na imagem final, gerando por fim a mesma imagem exemplo da atividade.
Atividade: Utilizando o programa-exemplo filestorage.cpp como base, crie um programa que gere uma imagem de dimensões 256x256 pixels contendo uma senóide de 4 períodos com amplitude igual 127 desenhada na horizontal, semelhante àquela apresentada na figura abaixo. Grave a imagem no formato YML e também exporte no formato PNG, como faz o programa-exemplo. Compare os arquivos gerados, extraindo uma linha correspondente de cada imagem gravada e comparando a diferença entre elas. Trace um gráfico da diferença calculada ao longo da linha correspondente extraída nas imagens. O que você observa? Por que isso acontece?
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <sstream>
#include <string>
int SIDE = 256;
int PERIODOS[2] = {8, 4};
int main(int argc, char** argv) {
std::stringstream ss_img1, ss_yml1;
std::stringstream ss_img2, ss_yml2;
cv::Mat image1, image2, result;
ss_yml1 << "senoide1-" << SIDE << ".yml";
image1 = cv::Mat::zeros(SIDE, SIDE, CV_32FC1);
ss_yml2 << "senoide2-" << SIDE << ".yml";
image2 = cv::Mat::zeros(SIDE, SIDE, CV_32FC1);
cv::FileStorage fs1(ss_yml1.str(), cv::FileStorage::WRITE);
cv::FileStorage fs2(ss_yml2.str(), cv::FileStorage::WRITE);
for (int i = 0; i < SIDE; i++) {
for (int j = 0; j < SIDE; j++) {
image1.at<float>(i, j) = 127 * sin(2 * M_PI * PERIODOS[0] * j / SIDE) + 128;
image2.at<float>(i, j) = 127 * sin(2 * M_PI * PERIODOS[1] * j / SIDE) + 128;
}
}
fs1 << "mat" << image1;
fs1.release();
fs2 << "mat" << image2;
fs2.release();
cv::normalize(image1, image1, 0, 255, cv::NORM_MINMAX);
image1.convertTo(image1, CV_8U);
ss_img1 << "senoide1-" << SIDE << ".png";
cv::imwrite(ss_img1.str(), image1);
cv::normalize(image2, image2, 0, 255, cv::NORM_MINMAX);
image2.convertTo(image2, CV_8U);
ss_img2 << "senoide2-" << SIDE << ".png";
cv::imwrite(ss_img2.str(), image2);
fs1.open(ss_yml1.str(), cv::FileStorage::READ);
fs1["mat"] >> image1;
fs2.open(ss_yml2.str(), cv::FileStorage::READ);
fs2["mat"] >> image2;
cv::normalize(image1, image1, 0, 255, cv::NORM_MINMAX);
image1.convertTo(image1, CV_8U);
cv::normalize(image2, image2, 0, 255, cv::NORM_MINMAX);
image2.convertTo(image2, CV_8U);
result = cv::Mat::zeros(SIDE, SIDE, CV_8U);
for (int i = 0; i < SIDE; i++) {
for (int j = 0; j < SIDE; j++) {
if (image1.at<uchar>(i, j) > image2.at<uchar>(i, j)) {
result.at<uchar>(i, j) = image1.at<uchar>(i, j) - image2.at<uchar>(i, j);
} else {
result.at<uchar>(i, j) = image2.at<uchar>(i, j) - image1.at<uchar>(i, j);
}
}
}
cv::imwrite("result.png", result);
cv::imshow("image1", image1);
cv::imshow("image2", image2);
cv::imshow("result", result);
cv::waitKey();
return 0;
}
Para gerar a imagem da senóide com 4 períodos foi basicamente repetir o código existente para a senóide de período 8. Após criar a senóide de 4 períodos, foi feito uma imagem que é resultado da subtração das duas imagens das senóides. Foi identificado na imagem resultante, que nos locais em que os pixels são brancos ou pretos nas duas imagens, o resultado é a cor preta. Nos locais em que uma das imagens tem pixels pretos e a outra tem pixels brancos, o resultado é a cor branca. E nos locais em que há transição das cores, a imagem resultante ficou cinza. Segue abaixo as imagens.
Atividade: Usando o programa bitplanes.cpp como referência para esteganografia, escreva um programa que recupere a imagem codificada de uma imagem resultante de esteganografia. Lembre-se que os bits menos significativos dos pixels da imagem fornecida deverão compor os bits mais significativos dos pixels da imagem recuperada. O programa deve receber como parâmetros de linha de comando o nome da imagem resultante da esteganografia. Teste a sua implementação com a imagem abaixo:
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
int main(int argc, char**argv) {
cv::Mat imagem, imagemEscondida;
cv::Vec3b valImagem, valImagemEscondida;
int nbits = 3;
imagem = cv::imread("../desafio-esteganografia.png", cv::IMREAD_COLOR);
if (imagem.empty()) {
std::cout << "imagem nao carregou corretamente" << std::endl;
return -1;
}
imagemEscondida = cv::Mat::zeros(imagem.rows, imagem.cols, CV_8UC3);
for (int i = 0; i < imagem.rows; i++) {
for (int j = 0; j < imagem.cols; j++) {
valImagem = imagem.at<cv::Vec3b>(i, j);
valImagemEscondida[0] = valImagem[0] << (8 - nbits);
valImagemEscondida[1] = valImagem[1] << (8 - nbits);
valImagemEscondida[2] = valImagem[2] << (8 - nbits);
imagemEscondida.at<cv::Vec3b>(i, j) = valImagemEscondida;
}
}
imwrite("imagem-escondidade.png", imagemEscondida);
return 0;
}
No código acima, primeiro é realizada a leitura da imagem utilizada para realização da atividade que contém uma imagem escondida. Em seguida crio uma imagem com pixels iguais a zero com as mesmas dimensões da imagem da atividade para colocar a imagem escondida nela. Foi criado um loop para percorrer toda a imagem, pegando cada pixel da imagem original, tornando seus 3 últimos bits os mais significativos e salvando no pixel da nova imagem. Com isso, a nova imagem recebe os 3 bits menos significativos da imagem original, porém na nova imagem eles são os bits mais significativos. Com isso, abaixo encontra-se a imagem descoberta que estava escondida na imagem original.
Atividade 1: Observando-se o programa labeling.cpp como exemplo, é possível verificar que caso existam mais de 255 objetos na cena, o processo de rotulação poderá ficar comprometido, visto que o tipo de dado usado para suportar imagens cinzentas permitem armazenar apenas um byte por pixel. Identifique a situação em que isso ocorre e proponha uma solução para este problema.
- Resposta: Ao usar a imagem com tons de cinza, o programa fica limitado a usar apenas 256 tons de cinza (0 a 255), e como o programa está pintando os objetos em cor de cinza se baseando na contagem de objetos na imagem, caso tenha mais de 255 objetos na imagem o programa estará comprometido. Para contornar isso, pode-se realizar a leitura da imagem de forma colorida (RGB) e com isso ter uma gama maior de valores para contagem.
Atividade 2: Aprimore o algoritmo de contagem apresentado para identificar regiões com ou sem buracos internos que existam na cena. Assuma que objetos com mais de um buraco podem existir. Inclua suporte no seu algoritmo para não contar bolhas que tocam as bordas da imagem. Não se pode presumir, a priori, que elas tenham buracos ou não.
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
int main(int argc, char** argv) {
cv::Mat image;
int width, height;
int nBolhasSemBuracos, nBolhasComBuracos;
cv::Point p;
image = cv::imread("../bolhas.png", cv::IMREAD_GRAYSCALE);
if (!image.data) {
std::cout << "imagem nao carregou corretamente\n";
return (-1);
}
width = image.cols;
height = image.rows;
std::cout << width << "x" << height << std::endl;
p.x = 0;
p.y = 0;
// elimina bolhas que tocam bordas esquerda e direita
for (int i = 0; i < height; i++) {
if (image.at<uchar>(i, 0) == 255) {
p.x = 0;
p.y = i;
cv::floodFill(image, p, 0);
}
if (image.at<uchar>(i, width - 1) == 255) {
p.x = width - 1;
p.y = i;
cv::floodFill(image, p, 0);
}
}
// elimina bolhas que tocam bordas superior e inferior
for (int j = 0; j < width; j++) {
if (image.at<uchar>(0, j) == 255) {
p.x = j;
p.y = 0;
cv::floodFill(image, p, 0);
}
if (image.at<uchar>(height - 1, j) == 255) {
p.x = j;
p.y = height - 1;
cv::floodFill(image, p, 0);
}
}
cv::imwrite("bordas-limpas.png", image);
// trocando cor de fundo da imagem
p.x = 0;
p.y = 0;
cv::floodFill(image, p, 10);
//busca bolhas com buracos
nBolhasComBuracos = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
// achou um buraco
if (image.at<uchar>(i, j) == 0) {
// verifica se a bolha já foi contada pegando o pixel anterior
if (image.at<uchar>(i - 1, j - 1) == 255) {
nBolhasComBuracos++;
p.x = j - 1;
p.y = i - 1;
cv::floodFill(image, p, 100);
}
p.x = j;
p.y = i;
cv::floodFill(image, p, 100);
}
}
}
// busca bolhas restantes
nBolhasSemBuracos = 0;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
// achou uma bolha
if (image.at<uchar>(i, j) == 255) {
nBolhasSemBuracos++;
p.x = j;
p.y = i;
cv::floodFill(image, p, 200);
}
}
}
std::cout << "a figura tem " << nBolhasComBuracos + nBolhasSemBuracos << " bolhas\n";
std::cout << nBolhasComBuracos << " bolhas com buracos\n";
std::cout << nBolhasSemBuracos << " bolhas sem buracos\n";
cv::imshow("image", image);
cv::imwrite("labeling.png", image);
cv::waitKey();
return 0;
}
Explicando o código da resolução, primeiro foi realizada a leitura da imagem com as bolhas e realizada inicialmente a eliminação das bolhas que tocam as bordas da imagem, pois não dá pra presumir se elas tem buracos ou não. Em seguida veio a parte do código que mais quebrou cabeça, conseguir contar quais bolhas tem buracos, com a bolha podendo conter mais de um buraco. Para resolver isso, iniciei mudando a cor de fundo da imagem com o floodfill, fazendo com que o fundo da imagem e o buraco das bolhas tenham tons de cinza diferentes. Em seguida criei um loop para percorrer todos os pixels da imagem, procurando por pixels com a cor 0, quando encontrado um pixel dessa cor significa que encontramos um buraco, porém, verifico o pixel anterior também, caso o pixel anterior tenha cor 255 significa que a bolha ainda não foi contada, pois sempre modifico a cor da bolha ao contabilizá-la. Então, ao encontrar uma bolha que o pixel anterior tenha cor 0, contabilizo uma bolha com buraco e transformo a bolha e o buraco para a cor 100 usando floodfill. E nos casos em que o pixel anterior não tenha cor 255, significa que a bolha já foi contabilizada, sendo assim eu apenas modifico a cor do buraco para 100. E com isso dentro do loop contabilizando todas as bolhas com buracos da imagem. Por fim, é apenas necessário contar as bolhas que sobraram, só verificar pixels com a cor 255 e contabilizar as bolhas sem buracos. Segue abaixo a imagem resultado, com as bolhas com buraco pintadas totalmente (até os buracos) em cinza mais escuro e as bolhas sem buracos em um cinza mais claro.
Atividade: Utilizando o programa convolucao.cpp como referência, implemente um programa laplgauss.cpp. O programa deverá acrescentar mais uma funcionalidade ao exemplo fornecido, permitindo que seja calculado o laplaciano do gaussiano das imagens capturadas. Compare o resultado desse filtro com a simples aplicação do filtro laplaciano.
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
#include "camera.hpp"
void printmask(cv::Mat &m) {
for (int i = 0; i < m.size().height; i++) {
for (int j = 0; j < m.size().width; j++) {
std::cout << m.at<float>(i, j) << ",";
}
std::cout << std::endl;
}
}
int main(int, char **) {
cv::VideoCapture cap;
int camera;
float media[] = {0.1111, 0.1111, 0.1111, 0.1111, 0.1111,
0.1111, 0.1111, 0.1111, 0.1111};
float gauss[] = {0.0625, 0.125, 0.0625, 0.125, 0.25,
0.125, 0.0625, 0.125, 0.0625};
float horizontal[] = {-1, 0, 1, -2, 0, 2, -1, 0, 1};
float vertical[] = {-1, -2, -1, 0, 0, 0, 1, 2, 1};
float laplacian[] = {0, -1, 0, -1, 4, -1, 0, -1, 0};
float laplacianGauss[] = {0, 0, 1, 0, 0,
0, 1, 2, 1, 0,
1, 2, -16, 2, 1,
0, 1, 2, 1, 0,
0, 0, 1, 0, 0};
float boost[] = {0, -1, 0, -1, 5.2, -1, 0, -1, 0};
cv::Mat frame, framegray, frame32f, frameFiltered;
cv::Mat mask(3, 3, CV_32F);
cv::Mat result;
double width, height;
int absolut;
char key;
camera = cameraEnumerator();
cap.open(camera);
if (!cap.isOpened())
return -1;
cap.set(cv::CAP_PROP_FRAME_WIDTH, 640);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, 480);
cv::namedWindow("filtroespacial", cv::WINDOW_NORMAL);
cv::namedWindow("original", cv::WINDOW_NORMAL);
mask = cv::Mat(3, 3, CV_32F, media);
absolut = 1; // calcula absoluto da imagem
for (;;) {
cap >> frame; // captura nova imagem da camera
cv::cvtColor(frame, framegray, cv::COLOR_BGR2GRAY);
cv::flip(framegray, framegray, 1);
cv::imshow("original", framegray);
framegray.convertTo(frame32f, CV_32F);
cv::filter2D(frame32f, frameFiltered, frame32f.depth(), mask, cv::Point(1, 1), cv::BORDER_REPLICATE);
if (absolut) {
frameFiltered = cv::abs(frameFiltered);
}
frameFiltered.convertTo(result, CV_8U);
cv::imshow("filtroespacial", result);
key = (char)cv::waitKey(10);
if (key == 27) break; // tecla ESC pressionada!
switch (key) {
case 'a':
absolut = !absolut;
break;
case 'm':
mask = cv::Mat(3, 3, CV_32F, media);
printmask(mask);
break;
case 'g':
mask = cv::Mat(3, 3, CV_32F, gauss);
printmask(mask);
break;
case 'h':
mask = cv::Mat(3, 3, CV_32F, horizontal);
printmask(mask);
break;
case 'v':
mask = cv::Mat(3, 3, CV_32F, vertical);
printmask(mask);
break;
case 'l':
mask = cv::Mat(3, 3, CV_32F, laplacian);
printmask(mask);
break;
case 'k':
mask = cv::Mat(5, 5, CV_32F, laplacianGauss);
printmask(mask);
break;
case 'b':
mask = cv::Mat(3, 3, CV_32F, boost);
break;
default:
break;
}
}
return 0;
}
O código acima contém toda a base do programa convolucao.cpp, sendo apenas adicionado a matriz do filtro laplaciano do gaussiano para ser utilizado nas imagens e também realizar a comparação com o filtro laplaciano. Abaixo estão as imagens obtidas no programa.
Ao analisar as imagens, foi notado que com o filtro laplaciano do gaussiano as bordas estão bem mais evidentes do que quando foi usado o filtro laplaciano.Atividade 1: Utilizando os programas dft.cpp, calcule e apresente o espectro de magnitude da imagem senoide-256.
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>
void swapQuadrants(cv::Mat& image) {
cv::Mat tmp, A, B, C, D;
// se a imagem tiver tamanho impar, recorta a regiao para o maior
// tamanho par possivel (-2 = 1111...1110)
image = image(cv::Rect(0, 0, image.cols & -2, image.rows & -2));
int centerX = image.cols / 2;
int centerY = image.rows / 2;
// rearranja os quadrantes da transformada de Fourier de forma que
// a origem fique no centro da imagem
// A B -> D C
// C D B A
A = image(cv::Rect(0, 0, centerX, centerY));
B = image(cv::Rect(centerX, 0, centerX, centerY));
C = image(cv::Rect(0, centerY, centerX, centerY));
D = image(cv::Rect(centerX, centerY, centerX, centerY));
// swap quadrants (Top-Left with Bottom-Right)
A.copyTo(tmp);
D.copyTo(A);
tmp.copyTo(D);
// swap quadrant (Top-Right with Bottom-Left)
C.copyTo(tmp);
B.copyTo(C);
tmp.copyTo(B);
}
int main(int argc, char** argv) {
cv::Mat image, padded, complexImage;
std::vector<cv::Mat> planos;
image = imread(argv[1], cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cout << "Erro abrindo imagem" << argv[1] << std::endl;
return EXIT_FAILURE;
}
// expande a imagem de entrada para o melhor tamanho no qual a DFT pode ser
// executada, preenchendo com zeros a lateral inferior direita.
int dft_M = cv::getOptimalDFTSize(image.rows);
int dft_N = cv::getOptimalDFTSize(image.cols);
cv::copyMakeBorder(image, padded, 0, dft_M - image.rows, 0,
dft_N - image.cols, cv::BORDER_CONSTANT,
cv::Scalar::all(0));
// prepara a matriz complexa para ser preenchida
// primeiro a parte real, contendo a imagem de entrada
planos.push_back(cv::Mat_<float>(padded));
// depois a parte imaginaria com valores nulos
planos.push_back(cv::Mat::zeros(padded.size(), CV_32F));
// combina os planos em uma unica estrutura de dados complexa
cv::merge(planos, complexImage);
// calcula a DFT
cv::dft(complexImage, complexImage);
swapQuadrants(complexImage);
// planos[0] : Re(DFT(image)
// planos[1] : Im(DFT(image)
cv::split(complexImage, planos);
// calcula o espectro de magnitude e de fase (em radianos)
cv::Mat magn, fase;
cv::cartToPolar(planos[0], planos[1], magn, fase, false);
cv::normalize(fase, fase, 0, 1, cv::NORM_MINMAX);
// caso deseje apenas o espectro de magnitude da DFT, use:
cv::magnitude(planos[0], planos[1], magn);
// some uma constante para evitar log(0)
// log(1 + sqrt(Re(DFT(image))^2 + Im(DFT(image))^2))
magn += cv::Scalar::all(1);
// calcula o logaritmo da magnitude para exibir
// com compressao de faixa dinamica
cv::log(magn, magn);
cv::normalize(magn, magn, 0, 1, cv::NORM_MINMAX);
// exibe as imagens processadas
cv::imshow("Imagem", image);
cv::imshow("Espectro de magnitude", magn);
cv::imshow("Espectro de fase", fase);
cv::waitKey();
return EXIT_SUCCESS;
}
Atividade 2: Usando agora o filestorage.cpp como referência, adapte o programa dft.cpp para ler a imagem em ponto flutuante armazenada no arquivo YAML equivalente (senoide-256.yml).
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>
void swapQuadrants(cv::Mat& image) {
cv::Mat tmp, A, B, C, D;
// se a imagem tiver tamanho impar, recorta a regiao para o maior
// tamanho par possivel (-2 = 1111...1110)
image = image(cv::Rect(0, 0, image.cols & -2, image.rows & -2));
int centerX = image.cols / 2;
int centerY = image.rows / 2;
// rearranja os quadrantes da transformada de Fourier de forma que
// a origem fique no centro da imagem
// A B -> D C
// C D B A
A = image(cv::Rect(0, 0, centerX, centerY));
B = image(cv::Rect(centerX, 0, centerX, centerY));
C = image(cv::Rect(0, centerY, centerX, centerY));
D = image(cv::Rect(centerX, centerY, centerX, centerY));
// swap quadrants (Top-Left with Bottom-Right)
A.copyTo(tmp);
D.copyTo(A);
tmp.copyTo(D);
// swap quadrant (Top-Right with Bottom-Left)
C.copyTo(tmp);
B.copyTo(C);
tmp.copyTo(B);
}
int main(int argc, char** argv) {
cv::Mat image, imageGray, padded, complexImage;
std::vector<cv::Mat> planos;
cv::FileStorage fs;
fs.open(argv[1], cv::FileStorage::READ);
fs["mat"] >> image;
cv::normalize(image, imageGray, 0, 255, cv::NORM_MINMAX);
imageGray.convertTo(imageGray, CV_8U);
if (image.empty()) {
std::cout << "Erro abrindo imagem" << argv[1] << std::endl;
return EXIT_FAILURE;
}
// expande a imagem de entrada para o melhor tamanho no qual a DFT pode ser
// executada, preenchendo com zeros a lateral inferior direita.
int dft_M = cv::getOptimalDFTSize(image.rows);
int dft_N = cv::getOptimalDFTSize(image.cols);
cv::copyMakeBorder(image, padded, 0, dft_M - image.rows, 0,
dft_N - image.cols, cv::BORDER_CONSTANT,
cv::Scalar::all(0));
// prepara a matriz complexa para ser preenchida
// primeiro a parte real, contendo a imagem de entrada
planos.push_back(cv::Mat_<float>(padded));
// depois a parte imaginaria com valores nulos
planos.push_back(cv::Mat::zeros(padded.size(), CV_32F));
// combina os planos em uma unica estrutura de dados complexa
cv::merge(planos, complexImage);
// calcula a DFT
cv::dft(complexImage, complexImage);
swapQuadrants(complexImage);
// planos[0] : Re(DFT(image)
// planos[1] : Im(DFT(image)
cv::split(complexImage, planos);
// calcula o espectro de magnitude e de fase (em radianos)
cv::Mat magn, fase;
cv::cartToPolar(planos[0], planos[1], magn, fase, false);
cv::normalize(fase, fase, 0, 1, cv::NORM_MINMAX);
// caso deseje apenas o espectro de magnitude da DFT, use:
cv::magnitude(planos[0], planos[1], magn);
// some uma constante para evitar log(0)
// log(1 + sqrt(Re(DFT(image))^2 + Im(DFT(image))^2))
magn += cv::Scalar::all(1);
// calcula o logaritmo da magnitude para exibir
// com compressao de faixa dinamica
cv::log(magn, magn);
cv::normalize(magn, magn, 0, 1, cv::NORM_MINMAX);
// exibe as imagens processadas
cv::imshow("Imagem", imageGray);
cv::imshow("Espectro de magnitude", magn);
cv::imshow("Espectro de fase", fase);
cv::waitKey();
return EXIT_SUCCESS;
}
Atividade 3: Compare o novo espectro de magnitude gerado com o valor teórico da transformada de Fourier da senóide. O que mudou para que o espectro de magnitude gerado agora esteja mais próximo do valor teórico? Porque isso aconteceu?
- Resposta: A senoide-256.yml ficou mais próxima do valor teórico por causa das suas casas decimais extras após a vírgula, com isso tendo uma imagem mais próxima do real.
Atividade: Utilizando o programa dftfilter.cpp como referência, implemente o filtro homomórfico para melhorar imagens com iluminação irregular. Crie uma cena mal iluminada e ajuste os parâmetros do filtro homomórfico para corrigir a iluminação da melhor forma possível. Assuma que a imagem fornecida é em tons de cinza.
- Resposta:
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
cv::Mat image, padded, complexImage, filter, result;
std::vector<cv::Mat> planos;
int dft_M, dft_N;
double gh, gl, d, c;
int gl_slider = 20;
int gl_slider_max = 50;
int gh_slider = 2;
int gh_slider_max = 10;
int c_slider = 1;
int c_slider_max = 100;
int d_slider = 5;
int d_slider_max = 200;
void swapQuadrants(cv::Mat& image) {
cv::Mat tmp, A, B, C, D;
// se a imagem tiver tamanho impar, recorta a regiao para o maior
// tamanho par possivel (-2 = 1111...1110)
image = image(cv::Rect(0, 0, image.cols & -2, image.rows & -2));
int centerX = image.cols / 2;
int centerY = image.rows / 2;
// rearranja os quadrantes da transformada de Fourier de forma que
// a origem fique no centro da imagem
// A B -> D C
// C D B A
A = image(cv::Rect(0, 0, centerX, centerY));
B = image(cv::Rect(centerX, 0, centerX, centerY));
C = image(cv::Rect(0, centerY, centerX, centerY));
D = image(cv::Rect(centerX, centerY, centerX, centerY));
// swap quadrants (Top-Left with Bottom-Right)
A.copyTo(tmp);
D.copyTo(A);
tmp.copyTo(D);
// swap quadrant (Top-Right with Bottom-Left)
C.copyTo(tmp);
B.copyTo(C);
tmp.copyTo(B);
}
void makeFilter(const cv::Mat &image, cv::Mat &filter){
cv::Mat_<float> filter2D(image.rows, image.cols);
for (int i = 0; i < image.rows; i++) {
for (int j = 0; j < image.cols; j++) {
filter2D.at<float>(i, j) = (gh - gl)*(1 - exp(-c*(( (i-dft_M/2)*(i-dft_M/2) + (j-dft_N/2)*(j-dft_N/2) ) / (d*d) ))) + gl;
}
}
cv::Mat planes[] = {filter2D, filter2D};
cv::merge(planes, 2, filter);
}
void applyFilter(){
planos.clear();
// prepara a matriz complexa para ser preenchida
// primeiro a parte real, contendo a imagem de entrada
planos.push_back(cv::Mat_<float>(padded));
// depois a parte imaginaria com valores nulos
planos.push_back(cv::Mat::zeros(padded.size(), CV_32F));
// combina os planos em uma unica estrutura de dados complexa
cv::merge(planos, complexImage);
// calcula a DFT
cv::dft(complexImage, complexImage);
swapQuadrants(complexImage);
// cria o filtro ideal e aplica a filtragem de frequencia
makeFilter(complexImage, filter);
cv::mulSpectrums(complexImage, filter, complexImage, 0);
// calcula a DFT inversa
swapQuadrants(complexImage);
cv::idft(complexImage, complexImage);
// planos[0] : Re(DFT(image)
// planos[1] : Im(DFT(image)
cv::split(complexImage, planos);
// recorta a imagem filtrada para o tamanho original
// selecionando a regiao de interesse (roi)
cv::Rect roi(0, 0, image.cols, image.rows);
result = planos[0](roi);
// normaliza a parte real para exibicao
cv::normalize(result, result, 0, 1, cv::NORM_MINMAX);
cv::imshow("Homomorphic", result);
cv::imwrite("dft-filter.png", result * 255);
}
void on_trackbar(int, void*){
gl = (double) gl_slider/10;
gh = (double) gh_slider/10;
c = (double) c_slider;
d = (double) d_slider;
applyFilter();
}
int main(int argc, char** argv) {
image = imread(argv[1], cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cout << "Erro abrindo imagem" << argv[1] << std::endl;
return EXIT_FAILURE;
}
// expande a imagem de entrada para o melhor tamanho no qual a DFT pode ser
// executada, preenchendo com zeros a lateral inferior direita.
dft_M = cv::getOptimalDFTSize(image.rows);
dft_N = cv::getOptimalDFTSize(image.cols);
cv::copyMakeBorder(image, padded, 0, dft_M - image.rows, 0, dft_N - image.cols, cv::BORDER_CONSTANT, cv::Scalar::all(0));
cv::namedWindow("Homomorphic", 1);
char TrackbarName[50];
std::sprintf( TrackbarName, "gl x %d", gl_slider_max );
cv::createTrackbar( TrackbarName, "Homomorphic", &gl_slider, gl_slider_max, on_trackbar);
std::sprintf( TrackbarName, "gh x %d", gh_slider_max );
cv::createTrackbar( TrackbarName, "Homomorphic", &gh_slider, gh_slider_max, on_trackbar);
std::sprintf( TrackbarName, "c x %d", c_slider_max );
cv::createTrackbar( TrackbarName, "Homomorphic", &c_slider, c_slider_max, on_trackbar);
std::sprintf( TrackbarName, "d x %d", d_slider_max );
cv::createTrackbar( TrackbarName, "Homomorphic", &d_slider, d_slider_max, on_trackbar);
applyFilter();
cv::waitKey(0);
return EXIT_SUCCESS;
}
Atividade: Utilizando os programas canny.cpp e pontilhismo.cpp como referência, implemente um programa cannypoints.cpp. A idéia é usar as bordas produzidas pelo algoritmo de Canny para melhorar a qualidade da imagem pontilhista gerada. A forma como a informação de borda será usada é livre. Entretanto, são apresentadas algumas sugestões de técnicas que poderiam ser utilizadas:
- Resposta:
#include <algorithm>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <opencv2/opencv.hpp>
#include <vector>
#include <chrono>
#include <random>
#define STEP 5
#define JITTER 3
cv::Mat image, points, border;
int width, height;
int x, y, gray;
int top_slider_canny = 10;
int top_slider_canny_max = 200;
int top_slider_point = 1;
int top_slider_point_max = 20;
int top_slider_point_border = 1;
int top_slider_point_border_max = 20;
char TrackbarNameCanny[50];
char TrackbarNamePoint[50];
char TrackbarNamePointBorder[50];
void pontilhismo() {
std::srand(std::time(0));
std::vector<int> yrange;
std::vector<int> xrange;
xrange.resize(height / STEP);
yrange.resize(width / STEP);
std::iota(xrange.begin(), xrange.end(), 0);
std::iota(yrange.begin(), yrange.end(), 0);
for(uint i = 0; i < xrange.size(); i++) {
xrange[i] = xrange[i] * STEP + STEP / 2;
}
for(uint i = 0; i < yrange.size(); i++) {
yrange[i] = yrange[i] * STEP + STEP / 2;
}
points = cv::Mat(height, width, CV_8U, cv::Scalar(255));
unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
std::shuffle(xrange.begin(), xrange.end(), std::default_random_engine(seed));
for (auto i : xrange) {
std::shuffle(yrange.begin(), yrange.end(), std::default_random_engine(seed));
for (auto j : yrange) {
x = i + std::rand() % (2 * JITTER) - JITTER + 1;
y = j + std::rand() % (2 * JITTER) - JITTER + 1;
gray = image.at<uchar>(x, y);
cv::circle(
points,
cv::Point(y, x),
top_slider_point,
CV_RGB(gray, gray, gray),
cv::FILLED,
cv::LINE_AA
);
}
}
}
void applyPointInBorders() {
for(x = 0; x < height; x++) {
for(y = 0; y < width; y++) {
if(border.at<uchar>(x, y) == 255) {
gray = image.at<uchar>(x, y);
circle(
points,
cv::Point(y, x),
top_slider_point_border,
CV_RGB(gray, gray, gray),
cv::FILLED,
cv::LINE_AA
);
}
}
}
}
void on_trackbar(int, void*){
pontilhismo();
cv::Canny(image, border, top_slider_canny, 3*top_slider_canny);
applyPointInBorders();
cv::imshow("Cannypoints", points);
}
int main(int argc, char** argv){
image = cv::imread(argv[1], cv::IMREAD_GRAYSCALE);
if (image.empty()) {
std::cout << "Could not open or find the image" << std::endl;
return -1;
}
sprintf(TrackbarNameCanny, "Canny", top_slider_canny_max);
sprintf(TrackbarNamePoint, "Points Radius", top_slider_point_max);
sprintf(TrackbarNamePointBorder, "Border Radius", top_slider_point_border_max);
//altura e largura da imagem
width = image.cols;
height = image.rows;
//prepara a janela para exibição da imagem
cv::namedWindow("Cannypoints", 1);
cv::createTrackbar(
TrackbarNameCanny,
"Cannypoints",
&top_slider_canny,
top_slider_canny_max,
on_trackbar
);
cv::createTrackbar(
TrackbarNamePoint,
"Cannypoints",
&top_slider_point,
top_slider_point_max,
on_trackbar
);
cv::createTrackbar(
TrackbarNamePointBorder,
"Cannypoints",
&top_slider_point_border,
top_slider_point_border_max,
on_trackbar
);
cv::waitKey(0);
return EXIT_SUCCESS;
}
No código acima foi usado os códigos de canny e do pontilhismo para editar a imagem de entrada. Inicialmente foi aplicado o pontilhismo na imagem toda com raio = 1 inicialmente, após isso aplico canny para pegar as bordas e com elas aplicar também o pontilhismo, porém com raios diferentes. Então como é possível ver na imagem abaixo, é possível editar a detecção de bordas com canny e o tamanho do raio dos pontos na imagem toda e também nas bordas de forma separada.
Atividade: Utilizando o programa kmeans.cpp como exemplo prepare um programa exemplo onde a execução do código se dê usando o parâmetro nRodadas=1 e inciar os centros de forma aleatória usando o parâmetro KMEANS_RANDOM_CENTERS ao invés de KMEANS_PP_CENTERS. Realize 10 rodadas diferentes do algoritmo e compare as imagens produzidas. Explique porque elas podem diferir tanto.
- Resposta:
#include <cstdlib>
#include <opencv2/opencv.hpp>
int main(int argc, char** argv) {
int nClusters = 12, nRodadas = 1;
char filename[50];
cv::Mat rotulos, centros;
cv::Mat img = cv::imread(argv[1], cv::IMREAD_COLOR);
cv::Mat samples(img.rows * img.cols, 3, CV_32F);
for (int n = 0; n < 10; n++) {
for (int y = 0; y < img.rows; y++) {
for (int x = 0; x < img.cols; x++) {
for (int z = 0; z < 3; z++) {
samples.at<float>(y + x * img.rows, z) = img.at<cv::Vec3b>(y, x)[z];
}
}
}
cv::kmeans(
samples,
nClusters,
rotulos,
cv::TermCriteria(
cv::TermCriteria::EPS | cv::TermCriteria::COUNT, 10000, 0.0001
),
nRodadas,
cv::KMEANS_RANDOM_CENTERS,
centros
);
cv::Mat rotulada(img.size(), img.type());
for (int y = 0; y < img.rows; y++) {
for (int x = 0; x < img.cols; x++) {
int indice = rotulos.at<int>(y + x * img.rows, 0);
rotulada.at<cv::Vec3b>(y, x)[0] = (uchar)centros.at<float>(indice, 0);
rotulada.at<cv::Vec3b>(y, x)[1] = (uchar)centros.at<float>(indice, 1);
rotulada.at<cv::Vec3b>(y, x)[2] = (uchar)centros.at<float>(indice, 2);
}
}
sprintf(filename, "sushi-%.d.png", n+1);
cv::imwrite(filename, rotulada);
}
cv::waitKey();
}
Imagem original
Imagem 1 com kmeans
Imagem 2 com kmeans
Imagem 3 com kmeans
Imagem 4 com kmeans
Imagem 5 com kmeans
Imagem 6 com kmeans
Imagem 7 com kmeans
Imagem 8 com kmeans
Imagem 9 com kmeans
Imagem 10 com kmeans
Atividade: Utilizando o programa momentos-regioes.cpp como referência utilize as imagens pessoa.jpg e multidao.jpg e descubra em que posição a pessoa da primeira imagem se encontra na segunda imagem. Caso o programa fique lento, verifique se é possível utilizar a função cv::resize() para redimensionar as imagens e tornar o processamento mais rápido. Discuta as dificuldades encontradas na resolução do problema.
- Resposta:
#include <iostream>
#include <vector>
#include <cmath>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
double compareMoments(double m1[], double m2[]) {
double value = 0;
for (int i = 0; i < 7; i++) {
value += abs(m1[i] - m2[i]);
}
return value;
}
int main(int argc, char* argv[]) {
double erro = 999;
double huMomentsPessoa[7];
double huMomentsTrecho[7];
int posicaoPessoa[2];
int size;
cv::Mat trechoImage;
cv::Moments momentsTrecho;
cv::Mat pessoa = cv::imread(argv[1], IMREAD_GRAYSCALE);
cv::Mat multidao = cv::imread(argv[2], IMREAD_GRAYSCALE);
// Verificar se as imagens foram carregadas corretamente
if (pessoa.empty() || multidao.empty()) {
cout << "Erro ao carregar as imagens" << endl;
return -1;
}
cv::resize(pessoa, pessoa, Size(pessoa.cols / 6, pessoa.rows / 6), INTER_LINEAR);
cv::resize(multidao, multidao, Size(multidao.cols / 6, multidao.rows / 6), INTER_LINEAR);
cv::Moments momentsPessoa = cv::moments(pessoa, false);
cv::HuMoments(momentsPessoa, huMomentsPessoa);
for (int i = 0; i < 7; i++) {
huMomentsPessoa[i] = -1 * std::copysign(1.0, huMomentsPessoa[i]) * log10(abs(huMomentsPessoa[i]));
}
size = pessoa.cols;
for (int i = 0; i < (multidao.cols - size); i++) {
for (int j = 0; j < (multidao.rows - size); j++) {
cv::Rect rect(i, j, size, size);
trechoImage = multidao(rect);
momentsTrecho = cv::moments(trechoImage, false);
cv::HuMoments(momentsTrecho, huMomentsTrecho);
for (int k = 0; k < 7; k++) {
huMomentsTrecho[k] = -1 * std::copysign(1.0, huMomentsTrecho[k]) * log10(abs(huMomentsTrecho[k]));
}
double result = compareMoments(huMomentsPessoa, huMomentsTrecho);
if (result < erro) {
erro = result;
posicaoPessoa[0] = i;
posicaoPessoa[1] = j;
}
}
}
for (int i = posicaoPessoa[0]; i < (posicaoPessoa[0] + size); i++) {
for (int j = posicaoPessoa[1]; j < (posicaoPessoa[1] + size); j++) {
if (i == posicaoPessoa[0] || i == posicaoPessoa[0] + size - 1) {
multidao.at<uchar>(i, j) = 0;
}
if (j == posicaoPessoa[1] || j == posicaoPessoa[1] + size - 1) {
multidao.at<uchar>(i, j) = 0;
}
}
}
std::cout << erro << " - x: " << posicaoPessoa[0] << ", y: " << posicaoPessoa[1] << std::endl;
cv::imwrite("image.png", multidao);
cv::imshow("janela", multidao);
cv::waitKey(0);
return 0;
}
Explicando o código acima, foi realizada a leitura das imagens da pessoa.jpg e multidão.jpg e diminui a dimensção das imagens em 6 vezes para melhorar o tempo de processamento. Em seguida, encontrei o momento de Hu para a imagem da pessoa.jpg e criei um cv::Rect com o mesmo tamanho da imagem pessoa.jpg. Esse cv::Rect foi criado para percorrer a imagem multidao.jpg pegando apenas o trecho de tamanho do Rect, que é igual ao da imagem pessoa.jpg, para em seguida calcular seu momento Hu e comparar com o momento Hu da pessoa.jpg. Essa comparação é feita na função compareMoments, que retorna o valor do erro e verifico o local onde ocorreu o menor erro dos momentos e salvo no array posicaoPessoa. Em seguida demarco um quadrado em preto na imagem para localizar o Rect que teve o menor erro de momento. Porém o resultado não foi o esperado, infelizmente o Rect que está sendo encontrado não corresponde a imagem.jpg e não consegui corrigir o erro.
Atividade: Um sistema de captura de imagens precisa realizar o reconhecimento de carateres de um visor de segmentos para uma aplicação industrial. Ocorre que o software de reconhecimento de padrões apresenta dificuldades de reconhecer os dígitos em virtude da separação existente entre os segmentos do visor. Usando o programa morfologia.cpp como referência, crie um programa que resolva o problema da pré-filtragem de forma para reconhecimento dos caracteres usando operações morfológicas. Você poderá usar as imagens digitos-1.png, digitos-2.png, digitos-3.png, digitos-4.png e digitos-5.png para testar seu programa. Cuidado para deixar o ponto decimal separado dos demais dígitos para evitar um reconhecimento errado do número no visor.
- Resposta:
#include <iostream>
#include <opencv2/opencv.hpp>
int main(int argc, char** argv) {
cv::Mat digitos1, dilatacao1, erosao1;
cv::Mat digitos2, dilatacao2, erosao2;
cv::Mat digitos3, dilatacao3, erosao3;
cv::Mat digitos4, dilatacao4, erosao4;
cv::Mat digitos5, dilatacao5, erosao5;
cv::Mat str, image;
digitos1 = cv::imread("../digitos-1.png", cv::IMREAD_UNCHANGED);
digitos2 = cv::imread("../digitos-2.png", cv::IMREAD_UNCHANGED);
digitos3 = cv::imread("../digitos-3.png", cv::IMREAD_UNCHANGED);
digitos4 = cv::imread("../digitos-4.png", cv::IMREAD_UNCHANGED);
digitos5 = cv::imread("../digitos-5.png", cv::IMREAD_UNCHANGED);
if(digitos1.empty() || digitos2.empty() ||
digitos3.empty() || digitos4.empty() ||
digitos4.empty()) {
std::cout << "Erro ao carregar as imagens" << std::endl;
return -1;
}
cv::bitwise_not(digitos1, digitos1);
cv::bitwise_not(digitos2, digitos2);
cv::bitwise_not(digitos3, digitos3);
cv::bitwise_not(digitos4, digitos4);
cv::bitwise_not(digitos5, digitos5);
// elemento estruturante
str = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 15));
cv::dilate(digitos1, dilatacao1, str);
cv::erode(dilatacao1, erosao1, str);
cv::dilate(digitos2, dilatacao2, str);
cv::erode(dilatacao2, erosao2, str);
cv::dilate(digitos3, dilatacao3, str);
cv::erode(dilatacao3, erosao3, str);
cv::dilate(digitos4, dilatacao4, str);
cv::erode(dilatacao4, erosao4, str);
cv::dilate(digitos5, dilatacao5, str);
cv::erode(dilatacao5, erosao5, str);
cv::bitwise_not(erosao1, erosao1);
cv::bitwise_not(erosao2, erosao2);
cv::bitwise_not(erosao3, erosao3);
cv::bitwise_not(erosao4, erosao4);
cv::bitwise_not(erosao5, erosao5);
cv::imwrite("morfologia1.png", erosao1);
cv::imwrite("morfologia2.png", erosao2);
cv::imwrite("morfologia3.png", erosao3);
cv::imwrite("morfologia4.png", erosao4);
cv::imwrite("morfologia5.png", erosao5);
cv::waitKey();
return 0;
}
Digitos-1.png
Resultado Digitos-1.png
Digitos-2.png
Resultado Digitos-2.png
Digitos-3.png
Resultado Digitos-3.png
Digitos-4.png
Resultado Digitos-4.png
Digitos-5.png
Resultado Digitos-5.png
Explicando o código, eu faço a leitura das 5 imagens dos números em formato de display. As imagens são binárias e os números são pretos com o fundo branco, só que a morfologia no opencv funciona em elementos da cor branca e com isso eu faço a inversão para que os números fiquem brancos e o fundo preto para aplicar a morfologia. Após a troca das cores, crio o elemento estruturante de um retângulo com uma altura considerável para que consiga unir os espaços nos números do display. Em seguida é aplicado a dilatação para conectar os elementos dos números que estão separados e logo após a erosão para fazer o elemento voltar ao seu tamanho normal, só que agora todo conectado. Por fim, volto a cor dos números para preto e o fundo para branco.