] >
Paru dans GNU/Linux Magazine n°193 de mai 2016.
Protégé par la licence CC BY-NC-ND 2.0.
Après les présentations de l’article précédent, on continue notre voyage en lisant l’image du code QR pour la traduire en un tableau de 0 et de 1.
Avant de lire l’image, un petit rappel s’impose. Nous allons étudier le code QR de la figure 1, le même que celui de l'article précédent. J’appelle module un petit carré blanc ou noir, cible les motifs carrés des coins, elles nous serviront pour déterminer la taille en pixels d’un module et l’orientation de l’image.
Fig. 1 Le code QR à décoder.
Le cadre est clair, on peut commencer à lire l’image fournie en entrée.
Nous y voilà, la première méthode charge et stocke les pixels de l’image dans l’attribut
01: def chargeim(self): 02: parser=ArgumentParser(description="Lit et corrige un code QR.") 03: parser.add_argument("-i",required=True,metavar="image",help="Image d’entrée.") 04: parser.add_argument("-c",choices=["1","0"],default="1",help="Corrige ou non les erreurs.") 05: parser.add_argument("-a",choices=["1","0"],default="0",help="On affiche tout ou seulement le message.") 06: arguments=parser.parse_args() 07: self.fichier=arguments.i 08: self.corrige=int(arguments.c) 09: self.tout=int(arguments.a)
Voici ce que donne l’option
> ./qrdecode.py -h usage: qrdecode.py [-h] -i image [-c {1,0}] [-a {1,0}] Lit et corrige un code QR. optional arguments: -h, --help show this help message and exit -i image Image d’entrée. -c {1,0} Corrige ou non les erreurs (peut planter si non). -a {1,0} On affiche tout ou seulement le message.
Et sans option :
> ./qrdecode.py usage: qrdecode.py [-h] -i image [-c {1,0}] [-a {1,0}] qrdecode.py: error: the following arguments are required: -i
La suite du code :
11: try: 12: im=Image.open(self.fichier) 13: except IOError: 14: print("Fichier "+self.fichier+" inexistant.",file=stderr) 15: exit(1)
On essaie d’ouvrir l’image, en cas d’échec on sort en se plaignant sur la sortie d’erreur.
17: im=im.convert('1') 18: pix=im.load() 19: self.image=[]
On convertit l’image en noir et blanc à l’aide de la bibliothèque de traitement d'images
17: for i in range(im.height): 18: self.image.append([0+(pix[j,i]==0) for j in range(im.width)])
On stocke l’image dans l’attribut image qui est un tableau de tableaux puis, à la fin de la méthode
20: while sum(self.image[0])==0: 21: self.image=self.image[1:] 22: while sum(self.image[-1])==0: 23: self.image=self.image[:-1] 24: self.image=[[0]*len(self.image[0])]*2+self.image+[[0]*len(self.image[0])]*2
On retaille le haut puis le bas avant d’ajouter deux lignes blanches en haut puis en bas.
25: while True: 26: s=0 27: for l in self.image: 28: s+=l[0] 29: if s==0: 30: for i in range(len(self.image)): 31: self.image[i]=self.image[i][1:] 32: else: 33: break 34: while True: 35: s=0 36: for l in self.image: 37: s+=l[-1] 38: if s==0: 39: for i in range(len(self.image)): 40: self.image[i]=self.image[i][:-1] 41: else: 42: break 43: for i in range(len(self.image)): 44: self.image[i]=[0,0]+self.image[i]+[0,0]
Et de même avec les colonnes de gauche ligne 31 où on enlève le premier élément blanc de chaque ligne (si la colonne est entièrement blanche) puis de même avec le dernier blanc ligne 40 avant d’ajouter deux colonnes blanches de chaque côté.
On n’utilise pas les petits pointillés bleus d’un module d’épaisseur (voir figure 2 pour rappel), on va reconnaître le motif blanc-noir-blanc à partir du coin supérieur gauche et du coin opposé. En effet, au moins l’un des deux contient une cible. Bien entendu, si l’attribut
Fig. 2 : Les quatre différentes zones d’un code QR.
01: def dims(self): 02: if self.image is None: 03: self.chargeim() 04: i=0 05: etat0,etat1=0,0 06: no0,no1=[],[]
On va reconnaître l’expression rationnelle
Fig. 3 : Notre petite machine qui reconnaît le motif.
08: while not(len(no0)==2 or len(no1)==2): 09: if self.image[i][i]!=etat0: 10: etat0=self.image[i][i] 11: no0.append(i) 12: if self.image[-i-1][-i-1]!=etat1: 13: etat1=self.image[-i-1][-i-1] 14: no1.append(i) 14: i+=1 16: if len(no0)<len(no1): 17: self.module=no1[0]-no1[1] 18: else: 19: self.module=no0[1]-no0[0]
Le premier entre
Ici, un
On peut aussi utiliser un petit parcours de graphe étiqueté :
01: def dims(self): 02: if self.image is None: 03: self.chargeim() 05: graphe=[[None,0,None,None],[None,0,1,None],[None,None,1,0],[None]*4] 06: motif=[self.image[i][i] for i in range(len(self.image))] 07: _,coord1=regraph(graphe,motif,0,3) 08: _,coord2=regraph(graphe,motif[::-1],0,3) 09: coord=min(coord1,coord2) 10: self.module=coord[2]-coord[1]
On reconnaît une matrice de notre graphe à quatre sommets, une ligne correspond à un sommet, la position au sommet où on va et la valeur au caractère reconnu.
Ainsi au sommet 1,
On envoie donc la diagonale descendante (
def regraph(g,l,deb,fin): pos=deb coord=[] for i in range(len(l)): if l[i] in g[pos]: npos=g[pos].index(l[i]) if npos!=pos: coord.append(i) pos=npos elif i!=len(l)-1: return False,coord return pos==fin,coord
On parcourt le motif envoyé (la liste
Dans cette méthode, on se contente de prendre la valeur du pixel de coordonnées
01: def stockeim(self): 02: if self.module is None: 03: self.dims() 04: debut,taille=2,self.module 05: self.qr=[] 06: for i in range(debut,len(self.image)-debut,taille): 07: self.qr.append([]) 08: for j in range(debut,len(self.image[0])-debut,taille): 09: self.qr[-1].append(self.image[i][j])
On stocke le tableau dans l’attribut
Enfin, il reste à vérifier que le coin sans grande cible est bien en bas à droite du tableau. Si ce n’est pas le cas, on le tourne autant de fois que nécessaire.
01: def placeim(self): 02: if self.qr is None: 03: self.stockeim()
Comme d’habituuudeuh.
On a besoin du motif des carrés concentriques caractéristique des grandes cibles :
cible=[[1,0,1,1,1,0,1] for _ in range(7)] cible[1][2:5]=[0]*3 cible[5][2:5]=[0]*3 cible[0]=[1]*7 cible[6]=[1]*7
Ce code est dans qrcodestandard.py.
11: for i in range(7): 12: if self.qr[i][:7]!=cible[i]: 13: coin=2 14: break 15: if self.qr[-i-1][:7]!=cible[i]: 16: coin=1 17: break 18: if self.qr[i][-7:]!=cible[i]: 19: coin=3 20: break 21: if self.qr[-i-1][-7:]!=cible[i]: 22: coin=0 23: break
On teste si, dans un des coins, un segment horizontal de sept modules ne correspond pas au segment de la cible attendu. Le numéro du canard coin doit se plaindre et comme je collectionne des canards (vivants)… plus ils sont contents, plus je le suis.
La disposition des coins est donc la suivante : .
def tourner90(matrice): nl=len(matrice) nc=len(matrice[0]) m=[[0 for _ in range(nl)] for _ in range(nc)] for i in range(nl): for j in range(nc): m[j][i]=matrice[i][-j-1] return m
Cette fonction
25: for _ in range(coin): 26: self.qr=tourner90(self.qr) 27: self.esttourne=True
On tourne
Le tableau de bits (Jamie Mc Cartney n’y est pour rien) est prêt, il reste à l’interpréter dans le dernier article de cette première partie.