De nos jours, de nombreux appareils électroniques utilisent des afficheurs 7 segments. Ces appareils peuvent être un four à micro-ondes, un thermostat, un réveil, une calculatrice, … Nous nous intéressons aujourd’hui à la détection et reconnaissance de chiffres pour ce type d’afficheur. En particulier, nous nous intéresserons à l’extraction des informations contenues sur un panneau d’affichage de score.

Afficheur score basket 7 segments pour la reconnaissance de chiffre
Image 1: Afficheur score rugby 7 segments (lien)

Fonctionnement d’un afficheur 7 segments

Afin de pouvoir décoder les chiffres présentant sur un afficheur 7 segments, il est nécessaire de connaître comment ce dernier fonctionne. Un afficheur 7 segments est un afficheur permettant de représenter facilement un chiffre en allumant une certaine combinaison de ces 7 segments. Pour un souci de compréhension et d’implémentation, nous utiliserons l’ordre défini dans l’image suivante.

Afficheur 7 segments
Image 2: Afficheur 7 segments

Ainsi, pour afficher un 3, il est nécessaire d’allumer les segments 1,2,3,5 et 6, comme nous pouvons le voir dans l’image suivante.

Afficheur 7 segments, représentation dun 3
Image 3: Représentation du 3

Maintenant que nous connaissons le fonctionnement de ce type d’afficheur, nous pouvons imaginer la marche à suivre pour récupérer automatiquement l’information d’une image d’afficheur. Par exemple, si seulement les segments 2 et 5 sont allumés, alors nous pouvons interpréter cette combinaison comme étant un « 1 ».

Reconnaissance des chiffres

La première étape de notre programme (hormis l’import des bibliothèques) sera donc d’associer la combinaison des segments allumés avec les chiffres qu’ils représentent ; nous créons donc un dictionnaire composé des 10 combinaisons de segments allumés correspondant aux 10 valeurs des chiffres.

import cv2
import numpy as np

chiffres={
    (1,1,1,0,1,1,1):0,
    (0,0,1,0,0,1,0):1,
    (1,0,1,1,1,0,1):2,
    (1,0,1,1,0,1,1):3,
    (0,1,1,1,0,1,0):4,
    (1,1,0,1,0,1,1):5,
    (1,1,0,1,1,1,1):6,
    (1,0,1,0,0,1,0):7,
    (1,1,1,1,1,1,1):8,
    (1,1,1,1,0,1,1):9}

Maintenant que nous avons défini l’association entre segments et chiffres, nous pouvons définir la fonction permettant, à partir d’une image contenant un afficheur, d’en extraire le nombre. Cette fonction prendra donc en entrée l’image à traiter et le dictionnaire et renverra la valeur de l’afficheur.

def reconnaissance_chiffre(image,chiffres):
    (H,W)=image.shape
    (dh,dw)=(int(H*0.15),int(W*0.25))
    
    segments=[
    ((0,0),(w,dh)),                  # segment 0
    ((0,0),(dw,h//2)),               # segment 1
    ((w-dw,0),(w,h//2)),             # segment 2
    ((0,(h//2)-dh//2),(w,(h//2)+dh//2)), # segment 3
    ((0,h//2),(dw,h)),               # segment 4
    ((w-dw,h//2),(w,h)),             # segment 5
    ((0,h-dh),(w,h))                 # segment 6
    ]

    segON= np.zeros(7)
    j=0
    for ((xa,ya),(xb,yb)) in segments:
        segment=image[ya:yb,xa:xb]
        surface=(xb-xa)*(yb-ya)
        somme=cv2.countNonZero(segment)
        if somme/surface >=0.5:
            segON[j]=1
        j+=1
    return chiffres[tuple(segON)]

La première étape de notre fonction est de déterminer les parties de l’image contenant d’éventuels segments. Pour ce faire, il est nécessaire de commencer par définir la taille des segments, ainsi que leurs positions.

Nous récupérons donc la dimension de l’image en entrée. Cette taille nous permet de définir, quelle que soit l’image d’entrée, la taille des zones contenant les segments. Pour ce faire, nous avons décidé de découper l’image en 4 colonnes pour la largeur, comme nous pouvons le voir sur l’image suivante. Ainsi, chaque colonne aura une largeur égale à 1/4 de la largeur de l’image. Il en va de même pour la hauteur : nous avons découpé celle-ci en 7 lignes soit à peu près 0.15 x H.

Définition des zones de l'afficheur
Image 4: Définition des zones de l’afficheur


Maintenant que nous connaissons la taille des zones, nous pouvons définir dans quelles zones nous devons chercher les segments. C’est ce que nous faisons aux lignes 20 à 28. Pour chaque segment, nous définissons une zone du point d’origine, au point de la fin de la zone.
Il ne reste plus qu’à tester si un segment contient des « pixels allumés » ou non. Nous initialisation la variable segON ligne 30, ainsi qu’une variable j à 0, afin de stocker l’état de chaque segment.

Pour chaque ensemble de coordonnées (xa,ya) et (xb,yb) de nos segments, nous extrayons la partie de l’image correspondante et calculons sa surface (l. 33-34). Nous calculons ensuite, grâce à la fonction cv2.countNonZero  le nombre de pixels allumés dans la zone (ligne 35). Enfin, si le ratio de pixels allumés par rapport à la surface est supérieur à 50 %, alors nous mettons 1 à l’état du segment (lignes 36-37).

Finalement, il nous reste à comparer les valeurs de segON avec celles du dictionnaire chiffres afin de renvoyer la valeur de l’afficheur (ligne 39).

Détection des chiffres

Maintenant que notre fonction pour reconnaître les chiffres à partir d’une image d’un afficheur est prête, nous pouvons nous attaquer à la localisation des chiffres. Pour ce faire, nous commençons par charger l’image (l. 42) et afin de faciliter la localisation, nous réalisons un seuillage afin de mettre en évidence les pixels rouges des segments du panneau d’affichage (l. 43). Enfin, nous appliquons un filtrage gaussien afin de faciliter la détection. Nous pouvons voir le résultat à l’image 5.

image=cv2.imread('afficheur.jpg') 
image_rouge=cv2.inRange(image,(0,0,150), (50,50,255))
blur=cv2.GaussianBlur(image_rouge,(5,5),0)
Image 5: Seuillage de l’afficheur

Nous pouvons maintenant utiliser la fonction cv2.findContours afin de détecter tous les contours de l’image seuillée. Puis nous déclarons deux listes, digit et moy_w que nous utiliserons par la suite.

im2, contours, hierarchy=cv2.findContours(blur,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

digit=[]
moy_w=[]
for i in contours:
    x,y,w,h=cv2.boundingRect(i)
    if w>=20 and w<=80 and h>= 100 and h<= 120:
        moy_w.append(w)

Edit: Si vous êtes passé à OpenCV 4, la fonction findContours ne renvoi plus l’image. Il faut donc remplacer la ligne 45 par:
contours,hierarchy =cv2.findContours(blur,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

Pour chaque contour que nous avons détecté, nous calculons le rectangle encadrant ligne 51, afin de pouvoir garder les zones dont la taille correspond avec celles des chiffres (l. 52). Nous utilisons ensuite une première liste, moy_w, afin de pouvoir stocker la largeur des chiffres (l. 53). Nous faisons cela afin de résoudre un problème que nous pouvons voir dans l’image suivante.

Image 6: Detection des chiffres de l’afficheur

Si vous ne l’avez pas remarqué, le rectangle encadrant du 1 est plus étroit que celui des autres chiffres. Le problème de cela est que lorsque l’on appliquera la fonction reconnaissance_chiffre, les segments seront calculés dans ce rectangle restreint. Nous calculons donc la moyenne des chiffres afin de s’assurer que la partie de l’image que nous analyserons sera assez grande pour contenir l’intégralité de l’afficheur 7 segments.

Nous utilisons ensuite la variable key qui nous permettra de s’assurer que les contours et donc les chiffres seront traités de gauche à droite et de haut en bas (lignes 54 et 56). Enfin, nous stockons dans la liste digit les contours et la variable key.

        key=y*10+x
        digit.append((i,key))
(digits, key) = zip(*sorted(digit,key=lambda b: b[1],reverse=False))

moy_w=int(np.mean(moy_w))

info=[]
for i in digits:
    x,y,w,h=cv2.boundingRect(i)
    if w<moy_w : #cas 1
        x=x+w-moy_w
        w=moy_w
    crop=blur[y:y+h,x:x+w]
    val=reconnaissance_chiffre(crop,chiffres)
    info.append(val)

print('Temps : {}{} : {}{} - Score: Locaux: {}{} -- Visiteurs: {}{}'.format(info[0],info[1],info[2],info[3],info[4],info[5],info[6],info[7])    )

Pour finir, nous déclarons la liste info  qui nous permettra de stocker les informations / les chiffres qui seront extraits du tableau d’affichage des scores.

Pour chaque contour (dans l’ordre) (l. 61), nous calculons le rectangle encadrant (l. 62) et dans le cas où la largeur est insuffisante (cas du 1), nous changeons le point d’origine afin de garantir une taille minimum (l. 64-65). Puis, nous récupérons la zone correspondant au rectangle encadrant (l. 66) et nous appelons la fonction de reconnaissance de chiffre (l. 67). Enfin, nous stockons dans la liste info le résultat de la fonction (l. 68).

Il nous reste plus qu’à afficher les infos que nous avons extraites à la ligne 70.

Il n’y a plus qu’à tester :

>> python3 reconnaissance_chiffre.py 
Temps : 40 : 00 - Score: Locaux: 21 -- Visiteurs: 07

Nous avons vu dans cet article, comment reconnaître des chiffres provenant d’un afficheur numérique. Pour continuer dans  la reconnaissance de caractères, je vous conseille de suivre cet article sur la reconnaissance de caractères.

Si le l’afficheur que vous voulez traiter n’est pas pile en face de la caméra, il peut être nécessaire d’améliorer l’image. Nous en parlons dans cet article.


2 commentaires

LETELLIER · 4 février 2019 à 15 h 58 min

Bonjour,
Super article, bien détailler et commenter;
Une fois la petite modification du findContours passer, c’est tout de suite plus claire.
Cependant j’ai une erreur avec le Zip(*Sorted) qui me renvoie « ValueError: need more than 0 values to unpack » et j’avoue ne pas comprendre. Si vous aviez une idée. Merci

    Florent · 4 février 2019 à 21 h 29 min

    Bonjour,
    Merci pour les retours, j’ai mis à jour l’article.
    Concernant votre problème, je pense qu’il s’agit du fait qu’aucun contours ne rentre dans les conditions et donc que la liste digit est vide.
    Affichez les valeurs en w et h renvoyées par la fonction cv2.boundingRect() et ajustez les seuils en conséquences.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.