Come individuare codici a barre in un’immagine

C

Come individuare codici a barre in un’immagine

In questo articolo mostrerò una tecnica di segmentazione di codici a barre da immagini.

Per segmentazione si intende l’identificazione della posizione di uno o più oggetti di interesse all’interno di un’immagine. Non userò deep learning, bensì solo caro vecchio image processing 😉.

Risultato finale con i codici a barre evidenziati da riquadri verdi

Setup

Per la scrittura del codice, viene molto comodo Jupyter notebook. Con Jupyter potrete interagire con il codice direttamente online.

Il notebook di esempio è a questo link: notebook di esempio

Utilizzerò le seguenti librerie:

  • OpenCv come libreria di image processing.
  • Numpy: viene utilizzata da OpenCv come appoggio per l’elaborazione numerica.
  • Matplotlib per la visualizzazione delle immagini.

La prima istruzione del notebook serve per caricare l’immagine barcodes.jpg usata nell’esempio.

from google.colab import files

uploaded = files.upload()

Per eseguire il notebook, dovrete caricare voi un’immagine. Potete trovare quella usata per gli esempi, a questo link https://github.com/lucapiccinelli/BarcodesTutorial/blob/master/img/barcodes.jpg

Immagine di test
Immagine di test

Tecnica e codice

Iniziamo importando le librerie e caricando l’immagine da disco.

import cv2
import matplotlib.pyplot as plt
import numpy as np


im = cv2.imread(r’barcodes.jpg’, cv2.IMREAD_GRAYSCALE)

plt.imshow(im, cmap=’Greys_r’)

Passiamo poi alle prime vere elaborazioni: per prima cosa è necessario binarizzare l’immagine, possibilmente mettendo in risalto ciò che è più interessante. Per questo viene applicato l’operatore blackhat che mette in maggiore evidenza gli elementi più scuri.

Segue la binarizzazione dell’immagine tramite sogliatura semplice; l’operatore blackhat ci permette di impostare una soglia molto bassa ottenendo comunque una quantità di rumore  gestibile.

#riscalatura dell'immagine
scale = 800.0 / im.shape[1]
im = cv2.resize(im, (int(im.shape[1] * scale), int(im.shape[0] * scale)))

#blackhat
kernel = np.ones((1, 3), np.uint8)
im = cv2.morphologyEx(im, cv2.MORPH_BLACKHAT, kernel, anchor=(1, 0))

#sogliatura
thresh, im = cv2.threshold(im, 10, 255, cv2.THRESH_BINARY)

Per l’operatore blackhat viene utilizzato un kernel che favorisce l’esaltazione degli elementi verticali. Essendo il kernel di dimensione prefissata, l’immagine viene prima riscalata così da velocizzarne l’elaborazione  (e favorire una sorta di normalizzazione delle immagini di input).

Risultato di blackhat + sogliatura
Risultato di blackhat + sogliatura

A questo punto utilizzo gli operatori morfologici, combinandoli in sequenza al fine di ottenere degli elementi connessi nelle posizioni dei codici a barre.

kernel = np.ones((1, 5), np.uint8)
im = cv2.morphologyEx(im, cv2.MORPH_DILATE, kernel, anchor=(2, 0), iterations=2) #dilatazione
im = cv2.morphologyEx(im, cv2.MORPH_CLOSE, kernel, anchor=(2, 0), iterations=2)  #chiusura

Questa combinazione di dilatazione e chiusura funziona molto bene sull’esempio utilizzato in questa guida. Potrebbe però dimostrarsi poco efficace su altre immagini. In tal caso, si può provare a variare l’utilizzo degli operatori, fino ad ottenere un risultato soddisfacente.

risultato di dilatazione + chiusura
risultato di dilatazione + chiusura

Proseguo utilizzando l’operatore di apertura con un kernel volutamente molto grosso, per eliminare gli elementi più piccoli lasciando solo i codici a barre.

kernel = np.ones((21, 35), np.uint8)
im = cv2.morphologyEx(im, cv2.MORPH_OPEN, kernel, iterations=1)

Ed ecco il risultato finale

Applicazione dell’apertura con kernel 35x21
Applicazione dell’apertura con kernel 35×21

L’ultimo passo è quello di utilizzare l’algoritmo di rilevazione degli elementi connessi per ottenere i rettangoli con coordinate e dimensioni dei codici a barre. Come si vede nell’esempio oltre ai codici a barre è rimasto altro rumore; in questo caso è molto semplice filtrarlo imponendo una soglia sulla dimensione dell’ area dei rettangoli ritornati dall’elaborazione precedente.

#rilettura dell'immagine, stavolta a colori
im_out = cv2.imread(r'barcodes.jpg')

#estrazione dei componenti connessi
contours, hierarchy = cv2.findContours(im, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

unscale = 1.0 / scale
if contours != None:
    for contour in contours:
        
        # se l'area non è grande a sufficienza la salto 
        if cv2.contourArea(contour) <= 2000:
            continue
        
        #estraggo il rettangolo di area minima (in formato (centro_x, centro_y), (width, height), angolo)
        rect = cv2.minAreaRect(contour)
        #l'effetto della riscalatura iniziale deve essere eliminato dalle coordinate rilevate
        rect = \
            ((int(rect[0][0] * unscale), int(rect[0][1] * unscale)), \
             (int(rect[1][0] * unscale), int(rect[1][1] * unscale)), \
             rect[2])
        
        #disegno il tutto sull'immagine originale
        box = np.int0(cv2.cv.BoxPoints(rect))
        cv2.drawContours(im_out, [box], 0, (0, 255, 0), thickness = 2)
        
plt.imshow(im_out)
#scrittura dell' immagine finale
cv2.imwrite(r'img\out.png', im_out)

In ultimo, nel codice qui sopra utilizzo i rettangoli estratti per disegnarli in sovrapposizione all’immagine originale.

Risultato finale con i codici a barre evidenziati da riquadri verdi
Risultato finale con i codici a barre evidenziati da riquadri verdi

Conclusioni

La tecnica presentata è molto semplice ed efficace, tuttavia non priva di difetti:

  • è molto sensibile all’angolatura del codice a barre; fino intorno ai 45 gradi se la cava, oltre è necessario rielaborare l’immagine cambiando la direzione dei kernel;
  • vengono rilevati codici a barre solo entro un certo range di dimensione;
  • nonostante il filtraggio imposto sulla dimensione delle aree estratte non è raro che passino elementi che non sono codici a barre;

Mentre i primi due in buona parte dei contesti di applicazione possono non rappresentare un limite, l’ultimo punto è decisamente più insidioso.

Una buona soluzione è quella di istruire una rete neurale (o classificatore analogo) su elementi caratterizzanti del codice a barre (gradiente immagine, trasformata di Fourier) e filtrare così il rumore in un secondo momento.

Materiale

Segue il codice completo di quanto visto fin’ora.

import cv2
import matplotlib.pyplot as plt
import numpy as np

im = cv2.imread(r'barcodes.jpg', cv2.IMREAD_GRAYSCALE)
im_out = cv2.imread(r'barcodes.jpg')

#riscalatura dell'immagine
scale = 800.0 / im.shape[1]
im = cv2.resize(im, (int(im.shape[1] * scale), int(im.shape[0] * scale)))

#blackhat
kernel = np.ones((1, 3), np.uint8)
im = cv2.morphologyEx(im, cv2.MORPH_BLACKHAT, kernel, anchor=(1, 0))

#sogliatura
thresh, im = cv2.threshold(im, 10, 255, cv2.THRESH_BINARY)

#operazioni  morfologiche
kernel = np.ones((1, 5), np.uint8)
im = cv2.morphologyEx(im, cv2.MORPH_DILATE, kernel, anchor=(2, 0), iterations=2) #dilatazione
im = cv2.morphologyEx(im, cv2.MORPH_CLOSE, kernel, anchor=(2, 0), iterations=2)  #chiusura

kernel = np.ones((21, 35), np.uint8)
im = cv2.morphologyEx(im, cv2.MORPH_OPEN, kernel, iterations=1)

#estrazione dei componenti connessi
contours, hierarchy = cv2.findContours(im, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

unscale = 1.0 / scale
if contours != None:
    for contour in contours:
        
        # se l'area non è grande a sufficienza la salto 
        if cv2.contourArea(contour) <= 2000:
            continue
        
        #estraggo il rettangolo di area minima (in formato (centro_x, centro_y), (width, height), angolo)
        rect = cv2.minAreaRect(contour)
        #l'effetto della riscalatura iniziale deve essere eliminato dalle coordinate rilevate
        rect = \
            ((int(rect[0][0] * unscale), int(rect[0][1] * unscale)), \
             (int(rect[1][0] * unscale), int(rect[1][1] * unscale)), \
             rect[2])
        
        
        #disegno il tutto sull'immagine originale
        box = np.int0(cv2.cv.BoxPoints(rect))
        cv2.drawContours(im_out, [box], 0, (0, 255, 0), thickness = 2)
        
plt.imshow(im_out)
#scrittura dell' immagine finale
cv2.imwrite(r'out.png', im_out)

Se si fosse interessati ad una esecuzione e installazione locale dell’ambiente per eseguire questo codice, rimando alla prima edizione di questo articolo. Troverete passo passo, come installare tutto localmente.

Spero di essere riuscito a illustrare la tecnica in maniera chiara.

Sono molto gradite domande, opinioni, commenti e feedback di ogni tipo.

Grazie per la lettura :).

A proposito di me

Luca Piccinelli

I’m a programmer. I love programming, any language, any paradigm

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti