Geometria Epipolar: Princípios Fundamentais e Aplicações Práticas

A geometria epipolar é um conceito central na visão computacional que descreve a relação geométrica intrínseca entre duas imagens capturadas de uma mesma cena por diferentes pontos de vista ou câmeras. Essa relação é independente do conteúdo tridimensional da cena, dependendo apenas dos parâmetros intrínsecos das câmeras e da sua pose relativa (parâmetros extrínsecos) entre as duas tomadas.

Considere dois pontos de vista de câmera, representados por seus centros ópticos O e O'. Um ponto tridimensional X no espaço real é projetado nos pontos x e x' nas respectivas imagens. Os pontos e e' são os epipolos, que correspodnem à projeção do centro óptico de uma câmera no plano da imagem da outra. O plano que contém os centros ópticos O e O' e o ponto X é denominado plano epipolar. A interseção desse plano com os planos das imagens define as linhas epipolares l e l', conforme ilustrado na Figura 1-1.

C2=RC1+t (1.1)

Multiplicando ambos os lados da equação (1.1) pelo operador de produto vetorial com o vetor t, representado pela matriz antissimétrica [t]×:

[t]×C2=[t]×RC1+[t]×t (1.2)

A matriz antissimétrica [t]× é definida como:

[t]×=⎡⎣⎢0tz−ty−tz0tx−tytx0⎤⎦⎥ (1.3)

O produto vetorial de um vetor consigo mesmo é zero, ou seja, t × t = [t]×t = 0 (1.4). Assim, o segundo termo do lado direito da equação (1.2) se anula, resultando em:

[t]×C2=[t]×RC1 (1.5)

Multiplicando ambos os lados da equação (1.5) por C2T:

C2T[t]×C2=C2T[t]×RC1 (1.6)

É importante notar que C2T[t]×C2 = 0, pois [t]×C2 é um vetor ortogonal a C2. Portanto, a equação se simplifica para:

C2T[t]×RC1=0 (1.7)

A expressão [t]×R é conhecida como Matriz Essencial (Essential Matrix), frequentemente denotada por E. Substituindo E na equação (1.7), obtemos a restrição epipolar fundamental:

C2TEC1=0 (1.8)

A Matriz Essencial E encapsula a relação geométrica de coplanaridade entre o ponto 3D X e os dois centros ópticos das câmeras O e O'. Essa coplanaridade é a base matemática para a estimativa de profundidade em sistemas visuais e a essência do conceito de geometria epipolar.

Esta matriz decsreve puramente a rotação e translação entre os dois sistemas de coordenadas das câmeras, refletindo a transformação rígida de um ponto 3D. A Matriz Essencial possui uma propriedade de equivalência de escala, o que significa que multiplicar E por qualquer constante não nula ainda satisfaz a restrição epipolar. Isso sublinha a incapacidade de sistemas de câmera monocular de determinar a escala absoluta, recuperando apenas a relação de posição relativa.

Apesar de R e t terem 3 graus de liberdade cada, devido à equivalência de escala, a Matriz Essencial possui apenas 5 graus de liberdade. Esta característica implica que, teoricamente, um mínimo de 5 pares de pontos correspondentes são necessários para determinar unicamente a pose relativa da câmera. Além disso, a Matriz Essencial tem posto 2 e seus valores singulares são da forma [σ, σ, 0]T, refletindo sua estrutura matemática profunda.

A importância da Matriz Essencial reside em sua capacidade de abstrair os parâmetros intrínsecos da câmera, focando diretamente na transformação geométrica entre os sistemas de coordenadas das duas câmeras. Esta transformação é a pedra angular da visão estéreo, reconstrução 3D e estimativa de pose da câmera, servindo como uma ponte crucial para recuperar informações 3D de imagens 2D.

Se considerarmos os pontos 2D p1 e p2 nas imagens, em coordenadas homogêneas, resultantes da projeção de C1 e C2, e assumindo câmeras ideais (sem distorção), o modelo de câmera pinhole nos dá:

Z1p1=K1C1 (1.9)

Z2p2=K2C2 (1.10)

Onde K1 e K2 são as matrizes de parâmetros intrínsecos das câmeras. Invertendo estas equações para C1 e C2:

C1=Z1K1−1p1 (1.11)

C2=Z2K2−1p2 (1.12)

Substituindo (1.11) e (1.12) na equação da restrição epipolar (1.8), obtemos:

Z2p2T(K2−T)EK1−1Z1p1=0

Como Z1 e Z2 são escalares não nulos, podemos reescrever como:

p2T(K2−T)EK1−1p1=0 (1.13)

A expressão (K2−T)EK1−1 é definida como a Matriz Fundamental (Fundamental Matrix), F. Assim, a equação (1.13) simplifica para:

p2TFp1=0 (1.14)

Esta é outra forma da restrição epipolar, que atua diretamente nas coordenadas de pixels da imagem. A Matriz Fundamental mapeia um ponto em uma imagem para sua linha epipolar correspondente na outra imagem.

Aplicações da Geometria Epipolar

Retificação Estéreo em Câmeras Binoculares

Uma aplicação vital da geometria epipolar em sistemas de visão estéreo é a retificação epipolar.

Objetivo

A retificação epipolar transforma um par de imagens estéreo, originalmente capturadas com alinhamento não ideal, para um cenário onde os planos das imagens são estritamente paralelos e os eixos ópticos são colineares ou paralelos. Isso cria uma configuração estéreo "ideal".

Benefícios

  1. Simplificação da Correspondência Estéreo: A busca por um ponto correspondente de uma imagem (x, y) na imagem esquerda torna-se uma busca unidimensional na mesma linha horizontal da imagem direita. Isso otimiza a eficiência e a simplicidade dos algoritmos de casamento de pontos.
  2. Aumento da Velocidade e Precisão: Reduzir a busca de 2D para 1D diminui drasticamente a carga computacional e minimiza a probabilidade de correspondências incorretas. Este é um passo essencial para o cálculo de profundidade em tempo real em sistemas como veículos autônomos e robôs.

Efeito

Após a retificação, as linhas epipolares de ambas as imagens se tornam horizontais e alinhadas. Isso significa que pontos correspondentes nas imagens esquerda e direita terão a mesma coordenada vertical.

A seguir, um exemplo de implementação em C++ usando OpenCV para retificação estéreo, demonstrando a aplicação prática desses conceitos.

stereo_corrector.hpp

#ifndef STEREO_CORRECTOR_HPP
#define STEREO_CORRECTOR_HPP

#include <iostream>
#include <string>
#include <vector>
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/calib3d.hpp>
#lt;opencv2/imgproc.hpp>
#lt;opencv2/highgui.hpp>

// Estrutura para armazenar os parâmetros de calibração de uma câmera estéreo
struct ParametrosCalibracaoStereo
{
    // Intrínsecos da câmera esquerda
    cv::Mat matriz_camera_esq;
    cv::Mat coefs_distorcao_esq;
    
    // Intrínsecos da câmera direita
    cv::Mat matriz_camera_dir;
    cv::Mat coefs_distorcao_dir;
    
    // Rotação e translação entre as câmeras (extrínsicos)
    cv::Mat matriz_rotacao;    // 3x3
    cv::Mat vetor_translacao; // 3x1
    
    // Dimensões da imagem
    cv::Size dimensoes_imagem;
};

// Estrutura para os resultados do processo de retificação estéreo
struct ResultadoRetificacaoStereo
{
    // Matrizes de retificação (R_esq, R_dir)
    cv::Mat matriz_retificacao_esq;   // 3x3
    cv::Mat matriz_retificacao_dir;  // 3x3
    
    // Matrizes de projeção (P_esq, P_dir)
    cv::Mat matriz_projecao_esq;      // 3x4
    cv::Mat matriz_projecao_dir;     // 3x4
    
    // Matriz de re-projeção (Q, 4x4, usada para calcular profundidade a partir da disparidade)
    cv::Mat matriz_reprojecao_disparidade;   // 4x4
    
    // Regiões de interesse válidas nas imagens retificadas
    cv::Rect roi_valida_esq;
    cv::Rect roi_valida_dir;
    
    // Mapas de remapeamento para correção de distorção e retificação
    cv::Mat mapa_x_esq, mapa_y_esq;
    cv::Mat mapa_x_dir, mapa_y_dir;
};

// Classe para gerenciar o processo de retificação e correção estéreo
class ProcessadorStereo
{
public:
    ProcessadorStereo();
    ~ProcessadorStereo();
    
    /**
     * @brief Carrega os parâmetros de calibração estéreo de um arquivo YAML.
     * @param caminho_arquivo_config Caminho para o arquivo de configuração.
     * @return true se o carregamento for bem-sucedido, false caso contrário.
     */
    bool carregarConfiguracao(const std::string& caminho_arquivo_config);
    
    /**
     * @brief Define os parâmetros de calibração estéreo diretamente.
     * @param config A estrutura de parâmetros de calibração.
     */
    void definirConfiguracao(const ParametrosCalibracaoStereo& config);
    
    /**
     * @brief Executa o cálculo da retificação estéreo.
     * @param alpha Parâmetro de escala livre (0-1), 0 para cortar pixels inválidos, 1 para manter todos.
     * @return A estrutura contendo os resultados da retificação.
     */
    ResultadoRetificacaoStereo executarRetificacao(double alpha = 0.0);
    
    /**
     * @brief Aplica as transformações de retificação a um par de imagens.
     * @param imagem_esq Imagem de entrada da câmera esquerda.
     * @param imagem_dir Imagem de entrada da câmera direita.
     * @param imagem_esq_retificada Imagem retificada da câmera esquerda (saída).
     * @param imagem_dir_retificada Imagem retificada da câmera direita (saída).
     * @return true se a aplicação for bem-sucedida, false caso contrário.
     */
    bool aplicarRetificacao(const cv::Mat& imagem_esq, const cv::Mat& imagem_dir,
                           cv::Mat& imagem_esq_retificada, cv::Mat& imagem_dir_retificada);
    
    /**
     * @brief Salva os resultados da retificação em um arquivo.
     * @param resultados Os resultados da retificação a serem salvos.
     * @param nome_arquivo Nome do arquivo para salvar.
     * @return true se o salvamento for bem-sucedido, false caso contrário.
     */
    bool salvarResultadosRetificacao(const ResultadoRetificacaoStereo& resultados, 
                                     const std::string& nome_arquivo);
    
    /**
     * @brief Carrega os resultados da retificação de um arquivo.
     * @param nome_arquivo Nome do arquivo para carregar.
     * @return A estrutura ResultadoRetificacaoStereo carregada.
     */
    ResultadoRetificacaoStereo carregarResultadosRetificacao(const std::string& nome_arquivo);
    
    /**
     * @brief Exibe uma validação visual da retificação aplicando linhas horizontais.
     * @param imagem_original_esq Imagem original da câmera esquerda.
     * @param imagem_original_dir Imagem original da câmera direita.
     * @param resultados Os resultados da retificação.
     */
    void validarRetificacaoVisual(const cv::Mat& imagem_original_esq, const cv::Mat& imagem_original_dir,
                                  const ResultadoRetificacaoStereo& resultados);
    
    /**
     * @brief Calcula o mapa de profundidade a partir de um mapa de disparidade usando a matriz de re-projeção.
     * @param mapa_disparidade O mapa de disparidade.
     * @param resultados Os resultados da retificação que contêm a matriz de re-projeção.
     * @return O mapa de profundidade calculado.
     */
    cv::Mat calcularMapaProfundidade(const cv::Mat& mapa_disparidade, 
                                    const ResultadoRetificacaoStereo& resultados);

private:
    ParametrosCalibracaoStereo config_estereo_;
    bool config_carregada_;
    
    // Cache para os resultados da última retificação executada
    ResultadoRetificacaoStereo ultimo_resultado_retificacao_;

    /**
     * @brief Verifica se os parâmetros de calibração carregados são válidos.
     * @return true se os parâmetros são válidos, false caso contrário.
     */
    bool validarParametros() const;
    
    /**
     * @brief Gera os mapas de remapeamento (map_x, map_y) para a correção de distorção e retificação.
     * @param resultados A estrutura onde os mapas serão armazenados.
     * @param alpha O parâmetro de escala livre.
     */
    void gerarMapasRemapeamento(ResultadoRetificacaoStereo& resultados, double alpha);
};

#endif // STEREO_CORRECTOR_HPP

stereo_corrector.cpp

#include "stereo_corrector.hpp"
#include <fstream>

ProcessadorStereo::ProcessadorStereo() : config_carregada_(false)
{
}

ProcessadorStereo::~ProcessadorStereo()
{
}

bool ProcessadorStereo::carregarConfiguracao(const std::string& caminho_arquivo_config)
{
    try
    {
        cv::FileStorage fs(caminho_arquivo_config, cv::FileStorage::READ);
        if (!fs.isOpened())
        {
            std::cerr << "ERRO: Não foi possível abrir o arquivo de configuração: " << caminho_arquivo_config << std::endl;
            return false;
        }

        fs["matriz_camera_esq"] >> config_estereo_.matriz_camera_esq;
        fs["coefs_distorcao_esq"] >> config_estereo_.coefs_distorcao_esq;

        fs["matriz_camera_dir"] >> config_estereo_.matriz_camera_dir;
        fs["coefs_distorcao_dir"] >> config_estereo_.coefs_distorcao_dir;

        fs["matriz_rotacao"] >> config_estereo_.matriz_rotacao;
        fs["vetor_translacao"] >> config_estereo_.vetor_translacao;

        fs["largura_imagem"] >> config_estereo_.dimensoes_imagem.width;
        fs["altura_imagem"] >> config_estereo_.dimensoes_imagem.height;

        fs.release();

        config_carregada_ = validarParametros();
        return config_carregada_;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO ao carregar configuração: " << e.what() << std::endl;
        return false;
    }
}

void ProcessadorStereo::definirConfiguracao(const ParametrosCalibracaoStereo& config)
{
    config_estereo_ = config;
    config_carregada_ = validarParametros();
}

ResultadoRetificacaoStereo ProcessadorStereo::executarRetificacao(double alpha)
{
    // Limpa resultados anteriores, se houver
    ultimo_resultado_retificacao_ = ResultadoRetificacaoStereo(); 

    if (!config_carregada_)
    {
        std::cerr << "ERRO: Configuração estéreo não carregada. Impossível retificar." << std::endl;
        return ultimo_resultado_retificacao_;
    }

    try
    {
        // Chama a função central do OpenCV para retificação estéreo
        cv::stereoRectify(
            config_estereo_.matriz_camera_esq, config_estereo_.coefs_distorcao_esq,
            config_estereo_.matriz_camera_dir, config_estereo_.coefs_distorcao_dir,
            config_estereo_.dimensoes_imagem,
            config_estereo_.matriz_rotacao, config_estereo_.vetor_translacao,
            ultimo_resultado_retificacao_.matriz_retificacao_esq, 
            ultimo_resultado_retificacao_.matriz_retificacao_dir,
            ultimo_resultado_retificacao_.matriz_projecao_esq, 
            ultimo_resultado_retificacao_.matriz_projecao_dir,
            ultimo_resultado_retificacao_.matriz_reprojecao_disparidade,
            cv::CALIB_ZERO_DISPARITY, alpha,
            config_estereo_.dimensoes_imagem, 
            &ultimo_resultado_retificacao_.roi_valida_esq, 
            &ultimo_resultado_retificacao_.roi_valida_dir
        );

        // Gera os mapas de remapeamento usando os resultados da retificação
        gerarMapasRemapeamento(ultimo_resultado_retificacao_, alpha);

        std::cout << "Retificação estéreo concluída." << std::endl;
        std::cout << "ROI válida câmera esquerda: " << ultimo_resultado_retificacao_.roi_valida_esq << std::endl;
        std::cout << "ROI válida câmera direita: " << ultimo_resultado_retificacao_.roi_valida_dir << std::endl;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO durante a retificação estéreo: " << e.what() << std::endl;
    }

    return ultimo_resultado_retificacao_;
}

bool ProcessadorStereo::aplicarRetificacao(const cv::Mat& imagem_esq, const cv::Mat& imagem_dir,
                                       cv::Mat& imagem_esq_retificada, cv::Mat& imagem_dir_retificada)
{
    if (!config_carregada_)
    {
        std::cerr << "ERRO: Configuração estéreo não carregada. Impossível aplicar retificação." << std::endl;
        return false;
    }
    
    // Garante que os mapas de remapeamento foram gerados (chamando executarRetificacao se necessário)
    if (ultimo_resultado_retificacao_.mapa_x_esq.empty() || ultimo_resultado_retificacao_.mapa_x_dir.empty())
    {
        std::cerr << "AVISO: Mapas de remapeamento vazios. Executando retificação com alpha=0.0." << std::endl;
        executarRetificacao(0.0); // Recalcula com alpha padrão se os mapas não existirem
        if (ultimo_resultado_retificacao_.mapa_x_esq.empty() || ultimo_resultado_retificacao_.mapa_x_dir.empty()) {
            std::cerr << "ERRO: Falha ao gerar mapas de remapeamento." << std::endl;
            return false;
        }
    }

    try
    {
        // Aplica o remapeamento para corrigir a distorção e retificar as imagens
        cv::remap(imagem_esq, imagem_esq_retificada, 
                  ultimo_resultado_retificacao_.mapa_x_esq, 
                  ultimo_resultado_retificacao_.mapa_y_esq, cv::INTER_LINEAR);
        cv::remap(imagem_dir, imagem_dir_retificada, 
                  ultimo_resultado_retificacao_.mapa_x_dir, 
                  ultimo_resultado_retificacao_.mapa_y_dir, cv::INTER_LINEAR);

        return true;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO ao aplicar retificação: " << e.what() << std::endl;
        return false;
    }
}

bool ProcessadorStereo::salvarResultadosRetificacao(const ResultadoRetificacaoStereo& resultados,
                                            const std::string& nome_arquivo)
{
    try
    {
        cv::FileStorage fs(nome_arquivo, cv::FileStorage::WRITE);
        if (!fs.isOpened())
        {
            std::cerr << "ERRO: Não foi possível abrir o arquivo para salvar resultados: " << nome_arquivo << std::endl;
            return false;
        }

        // Salva as matrizes de retificação e projeção
        fs << "matriz_retificacao_esq" << resultados.matriz_retificacao_esq;
        fs << "matriz_retificacao_dir" << resultados.matriz_retificacao_dir;
        fs << "matriz_projecao_esq" << resultados.matriz_projecao_esq;
        fs << "matriz_projecao_dir" << resultados.matriz_projecao_dir;
        fs << "matriz_reprojecao_disparidade" << resultados.matriz_reprojecao_disparidade;

        // Salva as regiões de interesse válidas
        fs << "roi_valida_esq_x" << resultados.roi_valida_esq.x;
        fs << "roi_valida_esq_y" << resultados.roi_valida_esq.y;
        fs << "roi_valida_esq_largura" << resultados.roi_valida_esq.width;
        fs << "roi_valida_esq_altura" << resultados.roi_valida_esq.height;

        fs << "roi_valida_dir_x" << resultados.roi_valida_dir.x;
        fs << "roi_valida_dir_y" << resultados.roi_valida_dir.y;
        fs << "roi_valida_dir_largura" << resultados.roi_valida_dir.width;
        fs << "roi_valida_dir_altura" << resultados.roi_valida_dir.height;

        fs.release();
        std::cout << "Resultados da retificação salvos em: " << nome_arquivo << std::endl;
        return true;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO ao salvar resultados da retificação: " << e.what() << std::endl;
        return false;
    }
}

ResultadoRetificacaoStereo ProcessadorStereo::carregarResultadosRetificacao(const std::string& nome_arquivo)
{
    ResultadoRetificacaoStereo resultados;

    try
    {
        cv::FileStorage fs(nome_arquivo, cv::FileStorage::READ);
        if (!fs.isOpened())
        {
            std::cerr << "ERRO: Não foi possível abrir o arquivo para carregar resultados: " << nome_arquivo << std::endl;
            return resultados;
        }

        fs["matriz_retificacao_esq"] >> resultados.matriz_retificacao_esq;
        fs["matriz_retificacao_dir"] >> resultados.matriz_retificacao_dir;
        fs["matriz_projecao_esq"] >> resultados.matriz_projecao_esq;
        fs["matriz_projecao_dir"] >> resultados.matriz_projecao_dir;
        fs["matriz_reprojecao_disparidade"] >> resultados.matriz_reprojecao_disparidade;

        resultados.roi_valida_esq.x = (int)fs["roi_valida_esq_x"];
        resultados.roi_valida_esq.y = (int)fs["roi_valida_esq_y"];
        resultados.roi_valida_esq.width = (int)fs["roi_valida_esq_largura"];
        resultados.roi_valida_esq.height = (int)fs["roi_valida_esq_altura"];

        resultados.roi_valida_dir.x = (int)fs["roi_valida_dir_x"];
        resultados.roi_valida_dir.y = (int)fs["roi_valida_dir_y"];
        resultados.roi_valida_dir.width = (int)fs["roi_valida_dir_largura"];
        resultados.roi_valida_dir.height = (int)fs["roi_valida_dir_altura"];

        fs.release();

        // Regenera os mapas de remapeamento usando os parâmetros carregados
        // Primeiro, precisamos garantir que a configuração estéreo base esteja carregada ou definida.
        // Se a classe não tem a config_estereo_ preenchida, não podemos gerar os mapas corretamente.
        // Para simplificar, assumimos que se estamos carregando resultados, a config original foi usada.
        // Para uma solução mais robusta, os mapas deveriam ser salvos/carregados diretamente ou 
        // a config_estereo_ seria salva junto com os resultados da retificação.
        if (config_carregada_){
            gerarMapasRemapeamento(resultados, 0.0); // alpha 0.0 é um valor padrão; o ideal seria salvar/carregar alpha
        } else {
             std::cerr << "AVISO: Configuração estéreo não carregada, mapas de remapeamento não gerados ao carregar resultados." << std::endl;
        }

        ultimo_resultado_retificacao_ = resultados; // Atualiza o cache interno
        std::cout << "Resultados da retificação carregados de: " << nome_arquivo << std::endl;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO ao carregar resultados da retificação: " << e.what() << std::endl;
    }

    return resultados;
}

void ProcessadorStereo::validarRetificacaoVisual(const cv::Mat& imagem_original_esq, const cv::Mat& imagem_original_dir,
                                          const ResultadoRetificacaoStereo& resultados)
{
    if (imagem_original_esq.empty() || imagem_original_dir.empty())
    {
        std::cerr << "ERRO: Imagens de entrada vazias, impossível validar retificação." << std::endl;
        return;
    }

    try
    {
        cv::Mat imagem_esq_retificada, imagem_dir_retificada;
        if (!aplicarRetificacao(imagem_original_esq, imagem_original_dir, imagem_esq_retificada, imagem_dir_retificada))
        {
            std::cerr << "ERRO: Falha ao aplicar retificação para validação visual." << std::endl;
            return;
        }

        // Cria uma tela para exibir as imagens retificadas lado a lado
        cv::Size dimensoes_tela = imagem_esq_retificada.size();
        cv::Mat tela_validacao(dimensoes_tela.height, dimensoes_tela.width * 2, imagem_esq_retificada.type());

        // Copia as imagens retificadas para a tela
        imagem_esq_retificada.copyTo(tela_validacao(cv::Rect(0, 0, dimensoes_tela.width, dimensoes_tela.height)));
        imagem_dir_retificada.copyTo(tela_validacao(cv::Rect(dimensoes_tela.width, 0, dimensoes_tela.width, dimensoes_tela.height)));

        // Desenha linhas horizontais para verificar o alinhamento epipolar
        for (int y = 50; y < tela_validacao.rows; y += 50)
        {
            cv::line(tela_validacao, cv::Point(0, y), cv::Point(tela_validacao.cols - 1, y), cv::Scalar(0, 255, 0), 1);
        }

        cv::imshow("Validação da Retificação Estéreo", tela_validacao);
        std::cout << "Pressione qualquer tecla para fechar a janela de validação." << std::endl;
        cv::waitKey(0);

        std::cout << "Validação visual concluída. As linhas verdes horizontais devem alinhar pontos correspondentes." << std::endl;
        std::cout << "Se as linhas epipolares não parecerem paralelas, pode ser necessária uma recalibração da câmera." << std::endl;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO durante a validação visual da retificação: " << e.what() << std::endl;
    }
}

cv::Mat ProcessadorStereo::calcularMapaProfundidade(const cv::Mat& mapa_disparidade,
                                       const ResultadoRetificacaoStereo& resultados)
{
    cv::Mat mapa_profundidade;

    if (mapa_disparidade.empty())
    {
        std::cerr << "ERRO: Mapa de disparidade vazio, impossível calcular mapa de profundidade." << std::endl;
        return mapa_profundidade;
    }

    if (resultados.matriz_reprojecao_disparidade.empty())
    {
        std::cerr << "ERRO: Matriz de re-projeção vazia nos resultados, impossível calcular profundidade." << std::endl;
        return mapa_profundidade;
    }

    try
    {
        // Usa a matriz de re-projeção (Q) para converter disparidade em 3D (X,Y,Z)
        cv::reprojectImageTo3D(mapa_disparidade, mapa_profundidade, resultados.matriz_reprojecao_disparidade, true);
        std::cout << "Cálculo do mapa de profundidade concluído." << std::endl;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO ao calcular mapa de profundidade: " << e.what() << std::endl;
    }

    return mapa_profundidade;
}

bool ProcessadorStereo::validarParametros() const
{
    if (config_estereo_.matriz_camera_esq.empty() || config_estereo_.matriz_camera_dir.empty())
    {
        std::cerr << "ERRO: Matrizes intrínsecas da câmera estão vazias." << std::endl;
        return false;
    }

    if (config_estereo_.matriz_camera_esq.size() != cv::Size(3, 3) ||
        config_estereo_.matriz_camera_dir.size() != cv::Size(3, 3))
    {
        std::cerr << "ERRO: As matrizes intrínsecas da câmera devem ser 3x3." << std::endl;
        return false;
    }

    if (config_estereo_.matriz_rotacao.empty() || config_estereo_.vetor_translacao.empty())
    {
        std::cerr << "ERRO: Matriz de rotação ou vetor de translação estão vazios." << std::endl;
        return false;
    }

    if (config_estereo_.matriz_rotacao.size() != cv::Size(3, 3) ||
        config_estereo_.vetor_translacao.size() != cv::Size(1, 3)) // OpenCV expects a 3x1 matrix for translation
    {
        std::cerr << "ERRO: Dimensões incorretas para a matriz de rotação (3x3) ou vetor de translação (3x1)." << std::endl;
        return false;
    }

    if (config_estereo_.dimensoes_imagem.width <= 0 || config_estereo_.dimensoes_imagem.height <= 0)
    {
        std::cerr << "ERRO: Dimensões da imagem inválidas." << std::endl;
        return false;
    }

    return true;
}

void ProcessadorStereo::gerarMapasRemapeamento(ResultadoRetificacaoStereo& resultados, double alpha)
{
    if (!config_carregada_)
    {
        std::cerr << "ERRO: Configuração não carregada, impossível criar mapas de remapeamento." << std::endl;
        return;
    }

    try
    {
        // Cria os mapas de remapeamento para a câmera esquerda
        cv::initUndistortRectifyMap(
            config_estereo_.matriz_camera_esq, config_estereo_.coefs_distorcao_esq,
            resultados.matriz_retificacao_esq, resultados.matriz_projecao_esq,
            config_estereo_.dimensoes_imagem, CV_32FC1,
            resultados.mapa_x_esq, resultados.mapa_y_esq
        );

        // Cria os mapas de remapeamento para a câmera direita
        cv::initUndistortRectifyMap(
            config_estereo_.matriz_camera_dir, config_estereo_.coefs_distorcao_dir,
            resultados.matriz_retificacao_dir, resultados.matriz_projecao_dir,
            config_estereo_.dimensoes_imagem, CV_32FC1,
            resultados.mapa_x_dir, resultados.mapa_y_dir
        );

        std::cout << "Mapas de remapeamento gerados com sucesso." << std::endl;
    }
    catch (const cv::Exception& e)
    {
        std::cerr << "ERRO ao gerar mapas de remapeamento: " << e.what() << std::endl;
    }
}

stereo_processor_app.cpp

#include "stereo_corrector.hpp" // Inclui o novo cabeçalho
#include <iostream>
#lt;opencv2/opencv.hpp>

/**
 * Exemplo de aplicação para retificação estéreo usando a classe ProcessadorStereo.
 * Demonstra o fluxo de trabalho de carregamento de configuração, retificação e aplicação a imagens.
 */
int main(int argc, char* argv[])
{
    if (argc < 4)
    {
        std::cout << "Uso: " << argv[0] << " <arquivo_configuracao_yaml> <imagem_esquerda.jpg> <imagem_direita.jpg>" << std::endl;
        std::cout << "Exemplo: " << argv[0] << " calibracao_stereo.yaml esquerda.jpg direita.jpg" << std::endl;
        return -1;
    }

    std::string arquivo_config = argv[1];
    std::string caminho_imagem_esq = argv[2];
    std::string caminho_imagem_dir = argv[3];

    try
    {
        // 1. Instanciar o processador estéreo
        ProcessadorStereo meu_processador_estereo;

        // 2. Carregar os parâmetros de calibração da câmera
        std::cout << "Tentando carregar configuração estéreo de: " << arquivo_config << std::endl;
        if (!meu_processador_estereo.carregarConfiguracao(arquivo_config))
        {
            std::cerr << "Falha ao carregar o arquivo de configuração. Verifique o caminho e o formato." << std::endl;
            return -1;
        }
        std::cout << "Configuração estéreo carregada com sucesso." << std::endl;

        // 3. Carregar as imagens originais para processamento
        cv::Mat imagem_esq_original = cv::imread(caminho_imagem_esq, cv::IMREAD_COLOR);
        cv::Mat imagem_dir_original = cv::imread(caminho_imagem_dir, cv::IMREAD_COLOR);

        if (imagem_esq_original.empty() || imagem_dir_original.empty())
        {
            std::cerr << "ERRO: Não foi possível carregar uma ou ambas as imagens de entrada." << std::endl;
            return -1;
        }

        std::cout << "Dimensões da imagem esquerda: " << imagem_esq_original.size() << std::endl;
        std::cout << "Dimensões da imagem direita: " << imagem_dir_original.size() << std::endl;

        // 4. Executar o cálculo de retificação estéreo (isso gera as matrizes e mapas)
        std::cout << "Iniciando o processo de retificação estéreo..." << std::endl;
        ResultadoRetificacaoStereo resultados_retificacao = meu_processador_estereo.executarRetificacao(0.0); // alpha=0 para cortar regiões inválidas

        // 5. Aplicar as transformações de retificação nas imagens
        cv::Mat imagem_esq_corrigida, imagem_dir_corrigida;
        if (!meu_processador_estereo.aplicarRetificacao(imagem_esq_original, imagem_dir_original,
                                                        imagem_esq_corrigida, imagem_dir_corrigida))
        {
            std::cerr << "Falha ao aplicar as transformações de retificação às imagens." << std::endl;
            return -1;
        }
        std::cout << "Retificação aplicada às imagens com sucesso." << std::endl;

        // 6. Preparar e exibir as imagens originais e retificadas para comparação
        cv::Mat tela_original(imagem_esq_original.rows, imagem_esq_original.cols * 2, imagem_esq_original.type());
        imagem_esq_original.copyTo(tela_original(cv::Rect(0, 0, imagem_esq_original.cols, imagem_esq_original.rows)));
        imagem_dir_original.copyTo(tela_original(cv::Rect(imagem_esq_original.cols, 0, imagem_dir_original.cols, imagem_dir_original.rows)));

        cv::Mat tela_retificada(imagem_esq_corrigida.rows, imagem_esq_corrigida.cols * 2, imagem_esq_corrigida.type());
        imagem_esq_corrigida.copyTo(tela_retificada(cv::Rect(0, 0, imagem_esq_corrigida.cols, imagem_esq_corrigida.rows)));
        imagem_dir_corrigida.copyTo(tela_retificada(cv::Rect(imagem_esq_corrigida.cols, 0, imagem_dir_corrigida.cols, imagem_dir_corrigida.rows)));

        // Desenhar linhas horizontais na imagem retificada para verificação visual do alinhamento epipolar
        for (int y = 50; y < tela_retificada.rows; y += 50)
        {
            cv::line(tela_retificada, cv::Point(0, y), cv::Point(tela_retificada.cols - 1, y), cv::Scalar(0, 255, 0), 1);
        }

        cv::imshow("Imagens Originais (Esquerda | Direita)", tela_original);
        cv::imshow("Imagens Retificadas e Alinhadas (Esquerda | Direita)", tela_retificada);
        std::cout << "Pressione qualquer tecla para continuar e salvar resultados..." << std::endl;
        cv::waitKey(0);

        // 7. Salvar as matrizes e ROIs resultantes da retificação
        std::string arquivo_resultados = "resultados_retificacao_estereo.yaml";
        if (meu_processador_estereo.salvarResultadosRetificacao(resultados_retificacao, arquivo_resultados))
        {
            std::cout << "Parâmetros de retificação salvos em: " << arquivo_resultados << std::endl;
        }

        // 8. Salvar as imagens retificadas
        cv::imwrite("imagem_esquerda_retificada.jpg", imagem_esq_corrigida);
        cv::imwrite("imagem_direita_retificada.jpg", imagem_dir_corrigida);
        std::cout << "Imagens retificadas salvas como 'imagem_esquerda_retificada.jpg' e 'imagem_direita_retificada.jpg'." << std::endl;

        // 9. Executar validação visual adicional (abrirá uma nova janela)
        std::cout << "\nIniciando validação visual detalhada da retificação..." << std::endl;
        meu_processador_estereo.validarRetificacaoVisual(imagem_esq_original, imagem_dir_original, resultados_retificacao);
        
        // 10. Exemplo de carregamento dos resultados salvos
        std::cout << "\nTestando o carregamento dos resultados de retificação salvos..." << std::endl;
        ResultadoRetificacaoStereo resultados_carregados = meu_processador_estereo.carregarResultadosRetificacao(arquivo_resultados);
        if (!resultados_carregados.matriz_retificacao_esq.empty())
        {
            std::cout << "Resultados de retificação carregados com sucesso a partir do arquivo." << std::endl;
            // A partir daqui, 'resultados_carregados' pode ser usada para aplicar retificação ou calcular profundidade
        } else {
            std::cerr << "Falha ao carregar resultados de retificação do arquivo." << std::endl;
        }

        std::cout << "\nDemonstração de retificação estéreo concluída!" << std::endl;
        cv::destroyAllWindows();

    }
    catch (const cv::Exception& e)
    {
        std::cerr << "Exceção OpenCV: " << e.what() << std::endl;
        return -1;
    }
    catch (const std::exception& e)
    {
        std::cerr << "Exceção padrão: " << e.what() << std::endl;
        return -1;
    }

    return 0;
}

calibracao_stereo.yaml

%YAML:1.0

# Arquivo de configuração de calibração para sistemas estéreo
# Contém todos os parâmetros necessários para retificação, tipicamente obtidos via processo de calibração.

# Matriz Intrínseca da Câmera Esquerda (3x3)
matriz_camera_esq: !!opencv-matrix
  rows: 3
  cols: 3
  dt: d
  data: [ 615.0, 0.0, 310.0,
          0.0, 615.0, 240.0,
          0.0, 0.0, 1.0 ]

# Coeficientes de Distorção da Câmera Esquerda [k1, k2, p1, p2, k3]
coefs_distorcao_esq: !!opencv-matrix
  rows: 1
  cols: 5
  dt: d
  data: [ -0.1, 0.05, 0.0, 0.0, 0.0 ]

# Matriz Intrínseca da Câmera Direita (3x3)
matriz_camera_dir: !!opencv-matrix
  rows: 3
  cols: 3
  dt: d
  data: [ 615.0, 0.0, 310.0,
          0.0, 615.0, 240.0,
          0.0, 0.0, 1.0 ]

# Coeficientes de Distorção da Câmera Direita [k1, k2, p1, p2, k3]
coefs_distorcao_dir: !!opencv-matrix
  rows: 1
  cols: 5
  dt: d
  data: [ -0.1, 0.05, 0.0, 0.0, 0.0 ]

# Matriz de Rotação (R) entre as câmeras (3x3)
matriz_rotacao: !!opencv-matrix
  rows: 3
  cols: 3
  dt: d
  data: [ 0.9998, -0.0012, 0.0198,
          0.0012, 0.9999, 0.0123,
          -0.0198, -0.0123, 0.9997 ]

# Vetor de Translação (T) entre as câmeras (3x1), unidade em milímetros ou metros
vetor_translacao: !!opencv-matrix
  rows: 3
  cols: 1
  dt: d
  data: [ -120.0, 5.0, 10.0 ]

# Dimensões esperadas das imagens
largura_imagem: 640
altura_imagem: 480

Em Visão SLAM (Simultaneous Localization and Mapping)

A geometria epipolar desempenha um papel crucial na estimativa da pose da câmera em sistemas SLAM.

Processo Básico

  1. Captura e Correspondência: O sistema captura duas frames de imagem consecutivas, tratando-as como vistas de câmeras em diferentes posições (assumindo K1=K2). Pontos de interesse são extraídos e correspondidos entre essas duas frames.
  2. Cálculo da Matriz: Usando os pares de pontos correspondentes, algoritmos como o "Método dos Oito Pontos" são empregados para calcular a Matriz Fundamental F ou a Matriz Essencial E.
  3. Decomposição da Pose: A Matriz Essencial E é então decomposta para recuperar a matriz de rotação R e o vetor de translação t da câmera, que representam o movimento relativo da primeira para a segunda frame. Note que o vetor de translação é recuperado com um fator de escala desconhecido.

Significado

A geometria epipolar fornece as restrições geométricas vitais entre quadros chave em um sistema SLAM. Isso permite a estimativa precisa do movimento da câmera (pose) e, subsequentemente, a triangulação de pontos para construir um mapa 3D do ambiente.

Tags: Geometria Epipolar Visão Computacional Matriz Essencial Matriz Fundamental Retificação Estéreo

Publicado em 8-13 23:27