Je travaille actuellement sur un jeu un peu en réalité augmentée. Le contrôleur que le jeu utilise (je parle ici du périphérique d'entrée physique) est un morceau de papier rectangulaire et monochrome. Je dois détecter la position, la rotation et la taille de ce rectangle dans le flux de capture de la caméra. La détection doit être invariante sur l'échelle et invariante sur la rotation le long des axes X et Y.
L'invariance d'échelle est nécessaire dans le cas où l'utilisateur éloigne le papier ou le rapproche de la caméra. Je n'ai pas besoin de connaître la distance du rectangle, donc l'invariance d'échelle se traduit par l'invariance de taille.
L'invariance de rotation est nécessaire dans le cas où l'utilisateur incline le rectangle le long de son axe X et/ou Y local. Une telle rotation change la forme du papier du rectangle au trapèze. Dans ce cas, le cadre de sélection orienté objet peut être utilisé pour mesurer la taille du papier.
Au début, il y a une étape d'étalonnage. Une fenêtre affiche le flux de la caméra et l'utilisateur doit cliquer sur le rectangle. Au clic, la couleur du pixel pointé par la souris est prise comme couleur de référence. Les cadres sont convertis en espace colorimétrique HSV pour améliorer la distinction des couleurs. J'ai 6 curseurs qui ajustent les seuils supérieur et inférieur pour chaque canal. Ces seuils sont utilisés pour binariser l'image (en utilisant la fonction inRange
d'opencv).
Après cela, j'érode et dilate l'image binaire pour supprimer le bruit et unir les morceaux de nerby (en utilisant les fonctions erode
et dilate
d'opencv).
L'étape suivante consiste à trouver les contours (en utilisant la fonction findContours
d'opencv) dans l'image binaire. Ces contours sont utilisés pour détecter les plus petits rectangles orientés (en utilisant la fonction minAreaRect
d'opencv). Comme résultat final, j'utilise le rectangle avec la plus grande surface.
Une courte conclusion de la procédure:
Comme vous l'avez peut-être remarqué, je ne profite pas des connaissances sur la forme réelle du papier, simplement parce que je ne sais pas comment utiliser correctement ces informations.
J'ai également pensé à utiliser les algorithmes de suivi d'opencv. Mais trois raisons m'ont empêché de les utiliser:
Voici une - relativement - bonne prise (image binaire après érode et dilate)
Comment améliorer la détection en général et surtout pour être plus résistant aux changements d'éclairage?
Ici sont quelques images brutes à tester.
Ne pouvez-vous pas simplement utiliser un matériau plus épais?
Oui, je le peux et je le fais déjà (malheureusement, je ne peux pas accéder à ces pièces pour le moment). Cependant, le problème persiste. Même si j'utilise du matériel comme du carton. Il n'est pas plié aussi facilement que le papier, mais on peut toujours le plier.
Comment obtenez-vous la taille, la rotation et la position du rectangle?
La fonction minAreaRect
d'opencv renvoie un objet RotatedRect
. Cet objet contient toutes les données dont j'ai besoin.
Remarque
Parce que le rectangle est monochrome, il n'y a aucune possibilité de distinguer le haut et le bas ou la gauche et la droite. Cela signifie que la rotation est toujours dans la plage [0, 180]
ce qui convient parfaitement à mes besoins. Le rapport des deux côtés du rect est toujours w:h > 2:1
. Si le rectangle était un carré, la plage de rotation changerait en [0, 90]
, mais cela peut être considéré comme hors de propos ici.
Comme suggéré dans les commentaires, je vais essayer l'égalisation de l'histogramme pour réduire les problèmes de luminosité et jeter un œil à ORB, SURF et SIFT.
Je ferai le point sur les progrès.
Je sais que cela fait un moment que je n'ai pas posé la question. J'ai récemment continué sur le sujet et résolu mon problème (mais pas via la détection de rectangle).
cv::equalizeHist
,cv::aruco::detectMarkers
,Il s'est avéré que la détection du marqueur est très robuste aux changements d'éclairage et aux angles de vision différents, ce qui me permet de sauter toutes les étapes de calibrage.
J'ai placé 2 marqueurs sur chaque contrôleur pour augmenter encore plus la robustesse de détection. Les deux marqueurs doivent être détectés une seule fois (pour mesurer leur corrélation). Après cela, il suffit de trouver un seul marqueur par contrôleur car l'autre peut être extrapolé à partir de la corrélation précédemment calculée.
Voici un résultat de détection dans un environnement lumineux:
dans un environnement plus sombre:
et lorsque vous cachez l'un des marqueurs (le point bleu indique la position du marqueur extrapolé):
La détection de forme initiale que j'ai implémentée n'a pas bien fonctionné. Il était très fragile aux changements d'éclairage. De plus, il a nécessité une première étape d'étalonnage.
Après l'approche de détection de forme, j'ai essayé SIFT et ORB en combinaison avec la force brute et l'assembleur tricoté pour extraire et localiser les caractéristiques dans les cadres. Il s'est avéré que les objets monochromes ne fournissent pas beaucoup de points clés (quelle surprise). Les performances de SIFT étaient de toute façon terribles (environ 10 ips @ 540p). J'ai tracé des lignes et d'autres formes sur le contrôleur, ce qui a permis de disposer de plus de points clés. Cependant, cela n'a pas apporté d'énormes améliorations.
Le canal H dans l'espace HSV est la teinte, et il n'est pas sensible au changement de lumière. Gamme rouge dans environ [150,180].
Sur la base des informations mentionnées, je fais les travaux suivants.
PS. Je ne peux pas récupérer l'image que vous téléchargez sur la boîte de dépôt à cause du RÉSEAU. Donc, j'utilise juste crop le côté droit de votre deuxième image comme entrée.
imgname = "src.png"
img = cv2.imread(imgname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
## Split the H channel in HSV, and get the red range
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)
h[h<150]=0
h[h>180]=0
## normalize, do the open-morp-op
normed = cv2.normalize(h, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8UC1)
kernel = cv2.getStructuringElement(shape=cv2.MORPH_ELLIPSE, ksize=(3,3))
opened = cv2.morphologyEx(normed, cv2.MORPH_OPEN, kernel)
res = np.hstack((h, normed, opened))
cv2.imwrite("tmp1.png", res)
Maintenant, nous obtenons le résultat comme ceci (h, normé, ouvert):
Trouvez ensuite les contours et filtrez-les.
contours = cv2.findContours(opened, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))[-2]
bboxes = []
rboxes = []
cnts = []
dst = img.copy()
for cnt in contours:
## Get the stright bounding rect
bbox = cv2.boundingRect(cnt)
x,y,w,h = bbox
if w<30 or h < 30 or w*h < 2000 or w > 500:
continue
## Draw rect
cv2.rectangle(dst, (x,y), (x+w,y+h), (255,0,0), 1, 16)
## Get the rotated rect
rbox = cv2.minAreaRect(cnt)
(cx,cy), (w,h), rot_angle = rbox
print("rot_angle:", rot_angle)
## backup
bboxes.append(bbox)
rboxes.append(rbox)
cnts.append(cnt)
Le résultat est comme ceci:
rot_angle: -2.4540319442749023
rot_angle: -1.8476102352142334
Du fait de la balise rectangulaire bleue dans l'image source, la carte est divisée en deux côtés. Mais une image propre n'aura aucun problème.