Matching basé sur les contours avec OpenCV

Nous allons voir ici comment utiliser la transformée généralisée de Hough avec OpenCV pour faire du matching en se basant sur les contours d’un objet.

Cette méthode consiste à prendre les points du contour d’un objet à l’aide d’un filtre de Canny, puis calculer leur orientation grâce à deux filtres de Sobel, un horizontal et un vertical. Ces données, une fois calculées, sont placées dans une table appelée la Table R.
Cette table va par la suite être utilisée pour rechercher des similarités dans l’image.

Pour tester cela, nous allons nous baser sur le sujet du post sur la création d’images avec Blender pour le dévracage de pièces de tôlerie fine.
https://vivienmetayer.com/simuler-images-blender/

Le code est disponible sur GitHub à cette adresse :
https://github.com/vivienmetayer/Generalized_Hough_OpenCV_sample/blob/master/Geometric_matching.cpp

pièces à détecter

Le code utilise OpenCV 4.3, compilé avec le support de CUDA pour profiter de la version GPU de l’algorithme, qui est bien plus rapide.

#include <iostream>
#include "opencv2/opencv.hpp"
#include "opencv2/cudaimgproc.hpp"
 
using namespace std;
using namespace cv;
 
const bool use_gpu = true;
 
int main()
{
	Mat img_template = imread("template.png", IMREAD_GRAYSCALE);
	Mat img = imread("image.png", IMREAD_GRAYSCALE);

Pour commencer, nous lisons l’image qui contient les pièces à détecter ainsi que le template, qui contient une seule pièce orientée correctement et détourée.

template
Ptr<GeneralizedHoughGuil> guil = use_gpu ? cuda::createGeneralizedHoughGuil() : createGeneralizedHoughGuil();
 
guil->setMinDist(100);
guil->setLevels(360);
guil->setDp(4);
guil->setMaxBufferSize(4000);
 
guil->setMinAngle(0);
guil->setMaxAngle(360);
guil->setAngleStep(1);
guil->setAngleThresh(15000);
 
guil->setMinScale(0.9);
guil->setMaxScale(1.1);
guil->setScaleStep(0.05);
guil->setScaleThresh(1000);
 
guil->setPosThresh(400);
//guil->setCannyHighThresh(230);
//guil->setCannyLowThresh(150);

Nous réglons ensuite les principaux paramètres de l’algorithme.

  • MinDist : distance minimum entre deux objets détectés
  • Levels : taille de la table R
  • Dp : rapport entre la taille de l’image et celle de la grille de recherche
  • MaxBufferSize : taille maximum des buffers internes, si cette valeur est trop basse, toute l’image ne sera pas couverte
  • PosThresh : nombre de votes nécessaires pour valider une détection. Ce nombre est très dépendant de beaucoup d’autre paramètres et sera donc à régler assez régulièrement si on modifie le reste.

Il est aussi possible de régler les paramètres du filtre de Canny intégré, mais dans notre cas, nous allons le faire manuellement pour visualiser et vérifier qu’on obtient bien les contours désirés.

double sobelScale = 0.05;
int sobelKernelSize = 5;
int cannyHigh = 230;
int cannyLow = 150;
 
Mat canny;
Canny(img, canny, cannyHigh, cannyLow);
namedWindow("canny", WINDOW_NORMAL);
imshow("canny", canny);
 
Mat dx;
Sobel(img, dx, CV_32F, 1, 0, sobelKernelSize, sobelScale);
namedWindow("dx", WINDOW_NORMAL);
imshow("dx", dx);
 
Mat dy;
Sobel(img, dy, CV_32F, 0, 1, sobelKernelSize, sobelScale);
namedWindow("dy", WINDOW_NORMAL);
imshow("dy", dy);
 
Mat canny_template;
Canny(img_template, canny_template, cannyHigh, cannyLow);
namedWindow("canny_template", WINDOW_NORMAL);
imshow("canny_template", canny_template);
 
Mat dx_template;
Sobel(img_template, dx_template, CV_32F, 1, 0, sobelKernelSize, sobelScale);
namedWindow("dx_template", WINDOW_NORMAL);
imshow("dx_template", dx_template);
 
Mat dy_template;
Sobel(img_template, dy_template, CV_32F, 0, 1, sobelKernelSize, sobelScale);
namedWindow("dy_template", WINDOW_NORMAL);
imshow("dy_template", dy_template);

Nous réglons les paramètres des filtres appliqués sur l’image et le template et visualisons les résultats.

résultats des filtres de Canny et Sobel
vector<Vec4f> position;
TickMeter tm;
Mat votes;
 
if (use_gpu) {
	cuda::GpuMat d_template(img_template);
	cuda::GpuMat d_edges_template(canny_template);
	cuda::GpuMat d_image(img);
	cuda::GpuMat d_x(dx);
	cuda::GpuMat d_dx_template(dx_template);
	cuda::GpuMat d_y(dy);
	cuda::GpuMat d_dy_template(dy_template);
	cuda::GpuMat d_canny(canny);
	cuda::GpuMat d_position;
	cuda::GpuMat d_votes;
 
	guil->setTemplate(d_edges_template, d_dx_template, d_dy_template);
 
	tm.start();
 
	guil->detect(d_canny, d_x, d_y, d_position, d_votes);
	
	if (d_position.size().height != 0) {
		d_position.download(position);
		d_votes.download(votes);
	}
	
	tm.stop();
}
else {
	guil->setTemplate(canny_template, dx_template, dy_template);
 
	tm.start();
 
	guil->detect(canny, dx, dy, position, votes);
 
	tm.stop();
}
 
cout << "Found : " << position.size() << " objects" << endl;
cout << "Detection time : " << tm.getTimeMilli() << " ms" << endl;

Ensuite, nous donnons à l’algorithme le template et lançons la détection sur l’image. Pour ces deux fonctions, nous utilisons la surcharge qui prend en paramètre les images des contours détectés par Canny et Sobel.
Pour la version GPU, il faut d’abord passer toutes les images en GpuMat et ensuite récupérer les résultats avec la fonction download.

Le tout est mesuré avec un TickMeter afin de vérifier la différence entre la version CPU et GPU. Le résultat confirme l’avantage d’utiliser CUDA pour cet algorithme avec environ une seconde de temps de détection sur le GPU contre presque trente minutes avec le CPU.
(test effectué avec un Ryzen 5 3600 et une GTX 1060 6Go)

Mat out;
cvtColor(img, out, COLOR_GRAY2BGR);
int index = 0;
Mat votes_int = Mat(votes.rows, votes.cols, CV_32SC3, votes.data, votes.step);
for (auto& i : position) {
	Point2f pos(i[0], i[1]);
	float scale = i[2];
	float angle = i[3];
	
	RotatedRect rect;
	rect.center = pos;
	rect.size = Size2f(img_template.cols * scale, img_template.rows * scale);
	rect.angle = angle;
 
	Point2f pts[4];
	rect.points(pts);
 
	line(out, pts[0], pts[1], Scalar(0, 255, 0), 3);
	line(out, pts[1], pts[2], Scalar(0, 255, 0), 3);
	line(out, pts[2], pts[3], Scalar(0, 255, 0), 3);
	line(out, pts[3], pts[0], Scalar(0, 255, 0), 3);
 
	int score = votes_int.at<Vec3i>(index).val[0];
	index++;
	string score_string = to_string(score);
	putText(out, score_string, rect.center, FONT_HERSHEY_SIMPLEX, 1.0, Scalar(0, 0, 255), 2);
}
 
namedWindow("out", WINDOW_NORMAL);
imshow("out", out);

waitKey();

Finalement, nous récupérons les résultats afin de les afficher sur l’image originelle.

La matrice des votes est retournée dans un format erroné (float*4) dans la version GPU de l’algorithme, c’est pour cela qu’il faut la transformer en la copiant dans une matrice d’entiers groupés par trois.

Pour chaque position détectée, nous récupérons les coordonnées et affichons le nombre de votes et un rectangle orienté autour de la pièce. Ce dernier aura la forme de l’image template utilisé.

résultat de la détection par transformée généralisée de Hough