Este artigo detalha o processo de calibração automática mão-olho ("eye-in-hand") utilizando OpenCV em C++. O cenário de aplicação envolve um braço robótico de seis eixos que executa tarefas de inserção, onde uma câmera montada na ferramenta (gripper) do robô captura um objeto para alinhá-lo com um alvo.
Princípios da Calibração
1. Sistemas de Coordenadas
Na calibração mão-olho, o objetivo principal é determinar a matriz de rotação e translação entre o sistema de coordenadas da câmera e o sistema de coordenadas da ferramenta (gripper) do robô.
2. Funções Principais do OpenCV
calibrateCamera: Requer as posições dos cantos de um tabuleiro de xadrez (ou centros de círculos) nas imagens capturadas, os pontos 3D correspondentes no espaço do tabuleiro, e as dimensões do tabuleiro. O resultado são os parâmetros intrínsecos da câmera (matriz intrínseca e coeficientes de distorção) e os extrínsecos (vetores de rotação e translação da câmera em relação ao tabuleiro).calibrateHandEye: Utiliza os resultados da calibração câmera-tabuleiro (transformações extrínsecas) e as posições correspondentes do end-effector do robô. É necessário converter as poses do robô (geralmente em formato de 6 graus de liberdade: X, Y, Z, Rx, Ry, Rz) em matrizes de rotação e translação. A ordem e o tipo de rotação (interna/externa) devem ser definidos corretamente para esta conversão. A função então calcula a matriz de transformação entre o sistema de coordenadas da câmera e o sistema de coordenadas da ferramenta.
Processo de Calibração
O processo envolve posicionar um tabuleiro de calibração (por exemplo, em uma superfície nivelada ou vertical). O robô move o conjunto câmera-gripper para múltiplas poses, capturando imagens do tabuleiro em cada pose. As coordenadas do end-effector do robô (relativas à sua base) são registradas simultaneamente para cada imagem capturada.
Implementação Chave
1. Detecção de Pontos e Calibração Câmera-Tabuleiro
A geração dos pontos 3D do tabuleiro é baseada em suas dimensões conhecidas.
// Gera os pontos 3D de um tabuleiro de xadrez
std::vector<:point3f> criarPontosObjeto(int colunas, int linhas, float tamanhoQuadro) {
std::vector<:point3f> pontosObjeto;
pontosObjeto.reserve(colunas * linhas);
for (int i = 0; i < linhas; ++i) {
for (int j = 0; j < colunas; ++j) {
// Define os pontos no plano Z=0 do sistema de coordenadas do tabuleiro
pontosObjeto.emplace_back(j * tamanhoQuadro, i * tamanhoQuadro, 0.0f);
}
}
return pontosObjeto;
}
</:point3f></:point3f>
A detecção dos cantos do tabuleiro em imagens e o refinamento subpíxel são cruciais.
/**
* @brief Detecta os cantos de um tabuleiro de xadrez em uma imagem.
* @param dadosImagem Ponteiro para os dados da imagem (formato BGR).
* @param colunas Número de colunas do tabuleiro.
* @param linhas Número de linhas do tabuleiro.
* @param largura Largura da imagem.
* @param altura Altura da imagem.
* @param pontosX Array para armazenar as coordenadas X dos cantos detectados.
* @param pontosY Array para armazenar as coordenadas Y dos cantos detectados.
* @return 0 se os cantos foram encontrados, -1 caso contrário.
*/
int detectarCantosTabuleiro(uchar* dadosImagem, int colunas, int linhas, int largura, int altura, double*& pontosX, double*& pontosY)
{
try
{
cv::Mat imagem = cv::Mat(cv::Size(largura, altura), CV_8UC3, dadosImagem);
std::vector<:point2f> cantosEncontrados;
cv::Size tamanhoTabuleiro(colunas, linhas);
cv::Mat imagemCinza;
cv::cvtColor(imagem, imagemCinza, cv::COLOR_BGR2GRAY);
// Tenta encontrar os cantos do tabuleiro
bool sucesso = cv::findChessboardCorners(imagemCinza, tamanhoTabuleiro, cantosEncontrados,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
if (sucesso)
{
// Refinamento subpíxel para maior precisão
cv::cornerSubPix(imagemCinza, cantosEncontrados, cv::Size(11, 11), cv::Size(-1, -1),
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1));
// Armazena os resultados
for (size_t i = 0; i < cantosEncontrados.size(); ++i)
{
pontosX[i] = cantosEncontrados[i].x;
pontosY[i] = cantosEncontrados[i].y;
// std::cout << "Canto " << i << ": (" << pontosX[i] << ", " << pontosY[i] << ")" << std::endl;
}
return 0; // Sucesso
}
else
{
// std::cout << "Cantos do tabuleiro não encontrados." << std::endl;
return -1; // Falha
}
}
catch (const std::exception& ex)
{
// Lidar com exceções, se necessário
// std::cerr << "Erro na detecção de cantos: " << ex.what() << std::endl;
return -1; // Falha
}
}
</:point2f>
A calibração da câmera é realizada usando os pontos 2D e 3D, resultando na matriz intrínseca e coeficientes de distorção.
// Assumindo que 'imagensPontos' contém os pontos 2D detectados e 'objetivoPontos' os pontos 3D correspondentes
// e que 'larguraImagem', 'alturaImagem' são as dimensões da imagem.
cv::Mat matrizCamera = cv::Mat::eye(3, 3, CV_64F); // Inicializa matriz intrínseca identidade
cv::Mat coefDistorcao;
std::vector<:mat> rotacoesVetor, translocacoesVetor; // Para armazenar os extrínsecos da câmera
// Calcula a calibração da câmera
double erroRMS = cv::calibrateCamera(objetivoPontos, imagensPontos, cv::Size(larguraImagem, alturaImagem),
matrizCamera, coefDistorcao, rotacoesVetor, translocacoesVetor);
// std::cout << "Erro RMS da Calibração: " << erroRMS << std::endl;
// std::cout << "Matriz Intrínseca da Câmera:\n" << matrizCamera << std::endl;
// std::cout << "Coeficientes de Distorção:\n" << coefDistorcao << std::endl;
</:mat>
2. Calibração Mão-Olho
A conversão das poses do robô para matrizes de rotação e translação é um passo crítico. A função eulerAngleToRotatedMatrix (não totalmente implementada aqui, mas descrita na lógica) é usada para isso, levando em conta a sequência de rotação e se é interna ou externa.
/**
* @brief Converte ângulos de Euler (em graus) para uma matriz de rotação 3x3.
* Assume rotações externas (em torno dos eixos globais).
* @param angulosEuler Matriz 1x3 com os ângulos [rx, ry, rz].
* @param ordemRotacao String que define a ordem das rotações (e.g., "xyz", "zyx").
* @return Matriz de rotação 3x3.
*/
cv::Mat converterEulerParaMatrizRotacaoExterna(const cv::Mat& angulosEuler, const std::string& ordemRotacao)
{
CV_Assert(angulosEuler.rows == 1 && angulosEuler.cols == 3);
cv::Mat angulosRad;
// Converte graus para radianos
cv::convertScaleAbs(angulosEuler, angulosRad, CV_PI / 180.0);
double rx = angulosRad.at<double>(0, 0);
double ry = angulosRad.at<double>(0, 1);
double rz = angulosRad.at<double>(0, 2);
double sin_x = sin(rx), cos_x = cos(rx);
double sin_y = sin(ry), cos_y = cos(ry);
double sin_z = sin(rz), cos_z = cos(rz);
cv::Mat Rx = (cv::Mat_<double>(3, 3) <<
1, 0, 0,
0, cos_x, -sin_x,
0, sin_x, cos_x);
cv::Mat Ry = (cv::Mat_<double>(3, 3) <<
cos_y, 0, sin_y,
0, 1, 0,
-sin_y, 0, cos_y);
cv::Mat Rz = (cv::Mat_<double>(3, 3) <<
cos_z, -sin_z, 0,
sin_z, cos_z, 0,
0, 0, 1);
cv::Mat matrizRotacao;
// Rotações externas são multiplicadas da direita para a esquerda na ordem especificada
if (ordemRotacao == "xyz") matrizRotacao = Rz * Ry * Rx;
else if (ordemRotacao == "zyx") matrizRotacao = Rx * Ry * Rz;
// Adicionar outras combinações conforme necessário
else {
CV_Error(cv::Error::StsBadArg, "Ordem de rotação inválida");
}
// Verificar se é uma matriz de rotação válida (opcional mas recomendado)
// if (!isRotationMatrix(matrizRotacao)) { ... }
return matrizRotacao;
}
// Função auxiliar para converter vetores de pose do robô (e.g., de um arquivo) em matrizes T = [R|t]
cv::Mat converterPoseRoboParaMatriz(const cv::Mat& poseRobo) {
CV_Assert(poseRobo.cols == 6); // Espera [x, y, z, rx, ry, rz]
cv::Mat matrizT = cv::Mat::eye(4, 4, CV_64F);
// Copia a translação
poseRobo(cv::Rect(0, 0, 3, 1)).copyTo(matrizT(cv::Rect(3, 0, 1, 3)));
// Converte a rotação (assumindo ângulos de Euler em graus e rotação externa "xyz")
cv::Mat angulosRotacao = poseRobo(cv::Rect(3, 0, 3, 1));
cv::Mat matrizR = converterEulerParaMatrizRotacaoExterna(angulosRotacao, "xyz");
matrizR.copyTo(matrizT(cv::Rect(0, 0, 3, 3)));
return matrizT;
}
// Dentro do loop de calibração mão-olho:
std::vector<:mat> Rs_gripper2base, Ts_gripper2base;
std::vector<:mat> Rs_target2cam, Ts_target2cam;
// Ler poses do robô de um arquivo, por exemplo
// cv::Mat posesRobo = lerPosesDoArquivo("poses_robo.txt");
// Para cada par de imagem/pose:
{
// cv::Mat poseAtualRobo = posesRobo.row(i);
// cv::Mat T_gripper2base = converterPoseRoboParaMatriz(poseAtualRobo);
// cv::Mat R_gripper2base, t_gripper2base;
// cv::decomposeProjectionMatrix(T_gripper2base, R_gripper2base, t_gripper2base); // ou extrair diretamente R e t
// Rs_gripper2base.push_back(R_gripper2base);
// Ts_gripper2base.push_back(t_gripper2base);
// Obter a rotação e translação da câmera em relação ao tabuleiro (do calibrateCamera)
// cv::Mat R_cam_board, t_cam_board; // Supondo que foram calculados
// Rodrigues(rotacoesVetor[i], R_cam_board);
// t_cam_board = translocacoesVetor[i];
// Para a calibração mão-olho, precisamos da transformação Câmera -> Alvo e Ferramenta -> Base
// Assumindo que a calibração do tabuleiro nos deu extrínsecos da Câmera em relação ao Tabuleiro.
// Precisamos da transformação Alvo -> Câmera, que é o inverso da Câmera -> Alvo.
// E Ferramenta -> Base.
// Exemplo simplificado: assumindo que temos R_cam_board, t_cam_board
// E para simplificar, vamos usar a transformação inversa do robô como T_base_gripper
// Na prática, R_target2cam e t_target2cam viriam da calibração câmera-tabuleiro.
cv::Mat R_cam_tabuleiro_inv = rotacoesVetor[i].t(); // Transposta é a inversa para matrizes de rotação
cv::Mat t_cam_tabuleiro_inv = -R_cam_tabuleiro_inv * translocacoesVetor[i];
Rs_target2cam.push_back(R_cam_tabuleiro_inv); // Representa Target -> Camera
Ts_target2cam.push_back(t_cam_tabuleiro_inv); // Representa Target -> Camera
// Extrair R e t do robô para a pose atual (T_gripper2base)
cv::Mat R_atual_gripper, t_atual_gripper;
// Esta parte depende de como as poses do robô são lidas e convertidas.
// Supondo que tenhamos uma função para extrair R e t de uma pose completa do robô.
// Exemplo: Extrair da matriz T_gripper2base calculada anteriormente.
// cv::decomposeProjectionMatrix(T_gripper2base, R_atual_gripper, t_atual_gripper);
// Rs_gripper2base.push_back(R_atual_gripper);
// Ts_gripper2base.push_back(t_atual_gripper);
}
// Realiza a calibração Mão-Olho
cv::Mat R_cam2gripper, t_cam2gripper;
cv::calibrateHandEye(Rs_gripper2base, Ts_gripper2base, Rs_target2cam, Ts_target2cam,
R_cam2gripper, t_cam2gripper, cv::CALIB_HAND_EYE_TSAI); // Ou outro método
std::cout << "Matriz de Rotação Mão-Olho (Câmera para Gripper):\n" << R_cam2gripper << std::endl;
std::cout << "Vetor de Translação Mão-Olho (Câmera para Gripper):\n" << t_cam2gripper << std::endl;
// Salva os resultados da calibração
// saveCalibrationResults("hand_eye_calibration.yml", matrizCamera, coefDistorcao, R_cam2gripper, t_cam2gripper);
</:mat></:mat></double></double></double></double></double></double>
3. Conversão de Coordenadas Pixel para Base do Robô
Esta etapa transforma um ponto detectado na imagem (em coordenadas de pixel) em coordenadas 3D no sistema de referência da base do robô.
/**
* @brief Converte coordenadas de pixel com profundidade para coordenadas no sistema base do robô.
* @param pixel Ponto em coordenadas de pixel (u, v).
* @param profundidade Profundidade Z no sistema da câmera (mm).
* @param matrizIntriseca Matriz intrínseca da câmera (3x3).
* @param coefDistorcao Coeficientes de distorção da câmera.
* @param R_cam2gripper Matriz de rotação Câmera -> Gripper (mão-olho).
* @param t_cam2gripper Vetor de translação Câmera -> Gripper (mão-olho).
* @param R_gripper2base Matriz de rotação Gripper -> Base (pose do robô no momento da captura).
* @param t_gripper2base Vetor de translação Gripper -> Base (pose do robô no momento da captura).
* @param coordBase Saída: Ponto 3D no sistema base do robô (X, Y, Z).
* @return true se a conversão for bem-sucedida, false caso contrário.
*/
bool pixelParaCoordRoboBase_Homogeneo(const cv::Point2f& pixel, double profundidade,
const cv::Mat& matrizIntriseca, const cv::Mat& coefDistorcao,
const cv::Mat& R_cam2gripper, const cv::Mat& t_cam2gripper,
const cv::Mat& R_gripper2base, const cv::Mat& t_gripper2base,
cv::Point3d& coordBase)
{
// Passo 1: Pixel para Coordenadas da Câmera (considerando distorção)
cv::Point3d pCamCoord;
if (!pixelParaCoordCamera(pixel, matrizIntriseca, coefDistorcao, profundidade, pCamCoord)) {
// std::cerr << "Falha na conversão de pixel para coordenadas da câmera." << std::endl;
return false;
}
// Passo 2: Construir ponto homogêneo na câmera P_cam_h = [X, Y, Z, 1]^T
cv::Mat P_cam_h = (cv::Mat_<double>(4, 1) << pCamCoord.x, pCamCoord.y, pCamCoord.z, 1.0);
// Passo 3: Construir matriz homogênea Câmera -> Gripper (T_cam_gripper)
cv::Mat T_cam_gripper = cv::Mat::eye(4, 4, CV_64F);
R_cam2gripper.copyTo(T_cam_gripper(cv::Rect(0, 0, 3, 3)));
t_cam2gripper.copyTo(T_cam_gripper(cv::Rect(3, 0, 1, 3)));
// Passo 4: Construir matriz homogênea Gripper -> Base (T_gripper_base)
cv::Mat T_gripper_base = cv::Mat::eye(4, 4, CV_64F);
R_gripper2base.copyTo(T_gripper_base(cv::Rect(0, 0, 3, 3)));
t_gripper2base.copyTo(T_gripper_base(cv::Rect(3, 0, 1, 3)));
// Passo 5: Calcular P_base_h = T_gripper_base * T_cam_gripper * P_cam_h
cv::Mat P_base_h = T_gripper_base * T_cam_gripper * P_cam_h;
// Passo 6: Extrair coordenadas X, Y, Z resultantes
coordBase.x = P_base_h.at<double>(0, 0);
coordBase.y = P_base_h.at<double>(1, 0);
coordBase.z = P_base_h.at<double>(2, 0);
return true;
}
/**
* @brief Converte coordenadas de pixel para coordenadas 3D no sistema da câmera.
* @param pixel Ponto em coordenadas de pixel (u, v).
* @param matrizIntriseca Matriz intrínseca da câmera (3x3).
* @param coefDistorcao Coeficientes de distorção da câmera.
* @param profundidade Profundidade Z no sistema da câmera (mm).
* @param coordCamera Saída: Ponto 3D no sistema da câmera (X, Y, Z).
* @return true se a conversão for bem-sucedida, false caso contrário.
*/
bool pixelParaCoordCamera(const cv::Point2f& pixel, const cv::Mat& matrizIntriseca, const cv::Mat& coefDistorcao, double profundidade, cv::Point3d& coordCamera) {
if (matrizIntriseca.empty() || matrizIntriseca.rows != 3 || matrizIntriseca.cols != 3 || matrizIntriseca.type() != CV_64F) {
// Log de erro: Matriz intrínseca inválida
return false;
}
if (profundidade <= 0) {
// Log de erro: Profundidade deve ser positiva
return false;
}
std::vector<:point2f> pontosPixelOriginais = { pixel };
std::vector<:point2f> pontosPixelUndistorted;
// 1. Remover distorção usando os parâmetros da câmera
try {
cv::undistortPoints(pontosPixelOriginais, pontosPixelUndistorted, matrizIntriseca, coefDistorcao, cv::noArray(), matrizIntriseca);
} catch (const cv::Exception& e) {
// Log de erro: Exceção em undistortPoints
return false;
}
if (pontosPixelUndistorted.empty()) {
// Log de erro: Pontos undistorted vazios
return false;
}
cv::Point2f pixelUndistorted = pontosPixelUndistorted[0];
// 2. Extrair parâmetros intrínsecos
double fx = matrizIntriseca.at<double>(0, 0);
double fy = matrizIntriseca.at<double>(1, 1);
double cx = matrizIntriseca.at<double>(0, 2);
double cy = matrizIntriseca.at<double>(1, 2);
if (fx == 0 || fy == 0) {
// Log de erro: fx ou fy são zero
return false;
}
// 3. Converter pixel (undistorted) para coordenadas normalizadas
double x_normalizado = (pixelUndistorted.x - cx) / fx;
double y_normalizado = (pixelUndistorted.y - cy) / fy;
// 4. Converter coordenadas normalizadas para coordenadas da câmera (em mm)
coordCamera.x = x_normalizado * profundidade;
coordCamera.y = y_normalizado * profundidade;
coordCamera.z = profundidade; // A profundidade Z é o próprio valor de profundidade
return true;
}
</double></double></double></double></:point2f></:point2f></double></double></double></double>
4. Correção de Ângulo com QR Code
Em cenários que exigem alinhamento preciso, como inserção, pode ser necessário calcular a orientação do objeto detectado (por exemplo, um QR Code) e corrigir a rotação do gripper.
// Assumindo que 'imagemFinal' é a imagem após processamento e 'dadosQR' é a string decodificada.
// 'bboxQR' contém os 4 cantos do QR Code na imagem.
cv::QRCodeDetector detectorQR;
std::string dadosQR = detectorQR.detectAndDecode(imagemFinal, bboxQR);
if (!bboxQR.empty()) {
// Calcular a orientação do QR Code usando solvePnP
std::vector<:point3f> pontosMundoQR = {
cv::Point3f(0, 0, 0), cv::Point3f(tamanhoQR, 0, 0), // Assumindo que o QR code está no plano Z=0 do seu sistema de referência
cv::Point3f(tamanhoQR, tamanhoQR, 0), cv::Point3f(0, tamanhoQR, 0)
};
cv::Mat rvec_qr, tvec_qr; // Rotação e Translação do QR code em relação à câmera
bool sucessoPnP = cv::solvePnP(pontosMundoQR, bboxQR, matrizCameraCarregada, coefDistorcaoCarregados, rvec_qr, tvec_qr, false, cv::SOLVEPNP_ITERATIVE);
if (sucessoPnP) {
// Calcular a correção de rotação Z
double correcaoAnguloZ = calcularCorrecaoAnguloZ_QRCode(rvec_qr, R_cam2gripper_carregada);
// A variável 'anguloCorrecaoZ' seria então utilizada para ajustar a pose final do robô.
// std::cout << "Correção de Ângulo Z do QR Code: " << correcaoAnguloZ << " graus" << std::endl;
}
}
/**
* @brief Calcula o desvio angular do QR Code em relação à orientação vertical no sistema do gripper.
* @param rvec Vetor de rotação do QR Code em relação à câmera (saída do solvePnP).
* @param R_cam2gripper Matriz de rotação Câmera -> Gripper (resultado da calibração mão-olho).
* @return Ângulo de desvio em graus. Positivo para anti-horário, negativo para horário.
*/
double calcularCorrecaoAnguloZ_QRCode(const cv::Mat& rvec, const cv::Mat& R_cam2gripper)
{
// 1. Converter vetor de rotação para matriz de rotação (QR -> Câmera)
cv::Mat R_qr2cam;
cv::Rodrigues(rvec, R_qr2cam); // Matriz 3x3
// 2. Obter a rotação do QR Code no sistema do Gripper
cv::Mat R_qr2gripper = R_cam2gripper * R_qr2cam;
// 3. Converter para ângulos de Euler (radianos)
cv::Vec3d euler_rad = rotationMatrixToEulerXYZ(R_qr2gripper); // Função auxiliar para conversão
// 4. Converter para graus
cv::Vec3d euler_deg = euler_rad * (180.0 / CV_PI);
double anguloZ_bruto = euler_deg[2]; // Componente Z (yaw)
// 5. Normalizar o ângulo para [-180, 180]
if (anguloZ_bruto > 180.0) anguloZ_bruto -= 360.0;
if (anguloZ_bruto < -180.0) anguloZ_bruto += 360.0;
// 6. Determinar a orientação vertical mais próxima (±90 graus)
double anguloVerticalProximo = (std::abs(anguloZ_bruto - 90.0) < std::abs(anguloZ_bruto + 90.0)) ? 90.0 : -90.0;
// 7. Calcular o ângulo de desvio
double anguloDesvio = anguloZ_bruto - anguloVerticalProximo;
return anguloDesvio;
}
// Função auxiliar (exemplo simplificado) para converter matriz de rotação em Euler XYZ
cv::Vec3d rotationMatrixToEulerXYZ(const cv::Mat& R) {
// Implementação detalhada necessária aqui, que pode variar ligeiramente
// dependendo da biblioteca ou do método preferido (e.g., atan2).
// Para simplificação, assume-se que esta função está disponível.
// Exemplo conceitual (pode precisar de ajustes):
double sy = sqrt(R.at<double>(0,0) * R.at<double>(0,0) + R.at<double>(1,0) * R.at<double>(1,0) );
bool singular = sy < 1e-6;
double x, y, z;
if (!singular) {
x = atan2(R.at<double>(2,1) , R.at<double>(2,2));
y = atan2(-R.at<double>(2,0), sy);
z = atan2(R.at<double>(1,0), R.at<double>(0,0));
} else {
x = atan2(-R.at<double>(1,2), R.at<double>(1,1));
y = atan2(-R.at<double>(2,0), sy);
z = 0;
}
return cv::Vec3d(x, y, z); // Retorna em radianos
}
</double></double></double></double></double></double></double></double></double></double></double></double></:point3f>
Considerações Práticas
- Número de Imagens: Recomenda-se usar entre 20 a 32 imagens para a calibração, sendo 26 uma boa prática.
- Posicionamento do Tabuleiro: Para tarefas de inserção em paredes, calibrar com o tabuleiro na parede pode oferecer maior precisão do que calibrar em uma superfície horizontal e depois interagir com a parede, devido a possíveis desvios de alinhamento.
- Precisão da Profundidade: A profundidade estimada a partir de um QR Code pode ter variações em nível de milímetro. Usá-la diretamente na conversão de pixel para coordenadas do robô pode introduzir erros na ordem de centímetros, especialmente se a pose do robô também tiver incertezas.