Dans ce billet, je propose de traiter le problème suivant. Prenons le cas d'un appareil possédant un afficheur digital tel qu'une balance, un voltmètre, un réveil, un hygromètre... mais ne possédant pas de moyen simple de récupérer les données (absence de port série etc). Si on souhaite collecter les données, deux méthodes s'offrent à nous. La première est celle de l'électronicien qui consiste à mettre un moyen de communication là où il n'y en a pas. L'inconvénient est qu'il faut avoir une compétence (que je n'ai pas) et qu'il faut faire du cas par cas. La seconde technique est de prendre en photo l'afficheur et d'analyser les images. C'est ce qu'on va faire ici.

Pour la démonstration, je vais utiliser python, une balance dont la masse affichée fluctue au cours du temps et les photos de l'afficheur.

Une fois le lot de photos sur le disque (ex: Img0001.png), on va chercher à détecter les chiffres (ou digits). Scikit-image va nous être précieux. Il nous faut :

  • appliquer un seuil à l'image pour enlever les parasites et passer en binaire. Otsu est intéressant car local, ce qui permet de se prémunir de variation de contraste à grande échelle.
  • connecter les morceaux dans le cas d'un afficheur 7 segments ou d'un seuillage trop agressif.
  • remplir les trous pour qu'il ne soit pas détecté comme objet
  • étiqueter les régions et ne considérer que celles supérieures à une taille critique
  • les extraire

Ceci est fait par ce code (qui manque encore de soin)

import os.path
import glob

import numpy as np
from scipy import ndimage
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

import skimage.io
from skimage.filter import threshold_otsu
from skimage.segmentation import clear_border
from skimage.morphology import closing, square
from skimage.measure import regionprops, label
from skimage.color import label2rgb


def segment_digit(image, filename, output_dir, digit_height=100, digit_width=52,
                  border=7, black_on_white=True, closingpx=4):
    """
    Segement each digit of a picture

    :param image: grey scale picture
    :param filename: filename of the picture source
    :param output_dir: path for the output
    :param digit_height: height of a digit
    :param digit_width: width of a digit
    :param border: pixels to shift the border
    :param black_on_white: black digit on clear background
    :param closingpx: number of pixels to close
    """
    # apply threshold
    thresh = threshold_otsu(image)
    if black_on_white:
        bw = closing(image > thresh, square(closingpx))
    else:
        bw = closing(image < thresh, square(closingpx))

    filled = ndimage.binary_fill_holes(bw)
    #plt.imshow(filled)
    #plt.show()

    # remove artifacts connected to image border
    cleared = filled.copy()
    clear_border(cleared)

    # label image regions
    label_image = label(cleared)
    borders = np.logical_xor(filled, cleared)
    label_image[borders] = -1
    image_label_overlay = label2rgb(label_image, image=image)

    fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(6, 6))
    ax.imshow(image_label_overlay)

    regions = regionprops(label_image)

    for item, region in enumerate(regions):
        # skip small elements
        if region['Area'] < 300:
            continue

        # draw rectangle around segmented digits
        minr, minc, maxr, maxc = region['BoundingBox']
        rect = mpatches.Rectangle((minc, minr), maxc - minc, maxr - minr,
                                  fill=False, edgecolor='red', linewidth=2)

        # uniq size
        img = np.zeros((100, 75), 'uint8')
        img[bw[minr:maxr, minc:maxc]!=0] = 255
        newname = os.path.splitext(os.path.basename(filename))[0] + '-' + str(item) + '.png'
        skimage.io.imsave(os.path.join(output_dir, newname), img)
        ax.add_patch(rect)

    plt.show()

Segmentation de chaque digit

On s'est donc fabriqué un lot d'images (Img0001-0.png, Img0001-1.png...) correspondant à chaque digit.

Il nous reste maintenant à les identifier. Une première méthode serait de faire du template matching, mais ça peut vite s'avérer peu robuste. On va donc préférer passer par du machine-learning. Scikit-learn est là pour ça.

Le principe :

  • On identifie un sous lot d'images. Dans mon cas, j'ai identifié trois images pour chaque digit (donc 30) et elles sont nommées sous la forme digit-id.png).
  • On entraine l'algorithme sur ces images identifiées.
  • On demande de deviner les images inconnues.

Pour la partie connue :

import glob
import os

import numpy as np
import pylab as pl

from sklearn import svm, metrics
import skimage.io

def load_knowndata(filenames):
    training = {'images': [], 'targets': [], 'data' : [], 'name' : []}

    for index, filename in enumerate(filenames):
        target = os.path.splitext(os.path.basename(filename))[0]
        target = int(target.split('-')[0])
        image = skimage.io.imread(filename)
        training['targets'].append(target)
        training['images'].append(image)
        training['name'].append(filename)
        training['data'].append(image.flatten().tolist())
        pl.subplot(6, 5, index + 1)
        pl.axis('off')
        pl.imshow(image, cmap=pl.cm.gray_r, interpolation='nearest')
        pl.title('Training: %i' % target)

    ## To apply an classifier on this data, we need to flatten the image, to
    ## turn the data in a (samples, feature) matrix:
    n_samples = len(training['images'])
    training['images'] = np.array(training['images'])
    training['targets'] = np.array(training['targets'])
    training['data'] = np.array(training['data'])
    return training

Pour la partie inconnue :

def load_unknowndata(filenames):
    training = {'images': [], 'targets': [], 'data' : [], 'name' : []}

    for index, filename in enumerate(filenames):
        image = skimage.io.imread(filename)
        training['targets'].append(-1) # Target = -1: unkown
        training['images'].append(image)
        training['name'].append(filename)
        training['data'].append(image.flatten().tolist())

    ## To apply an classifier on this data, we need to flatten the image, to
    ## turn the data in a (samples, feature) matrix:
    n_samples = len(training['images'])
    training['images'] = np.array(training['images'])
    training['targets'] = np.array(training['targets'])
    training['data'] = np.array(training['data'])
    return training

et on écrit un main rapidement pour enchainer tout ça


if __name__ == '__main__':
    filenames = sorted(glob.glob('learn/*-*.png'))
    training = load_knowndata(filenames)
    pl.show()
    pl.close()

    # Create a classifier: a support vector classifier
    classifier = svm.SVC(gamma=1e-8)

    # We learn the digits on the first half of the digits
    classifier.fit(training['data'], training['targets'])

    filenames = sorted(glob.glob('data/*.png'))
    unknown = load_unknowndata(filenames)

    filenames = glob.glob('data/*.png')
    filenames = set([os.path.splitext(os.path.basename(fn))[0].split('-')[0] for fn in filenames])

    for filename in filenames:
        print('----')
        print(filename)
        print('----')
        fn = sorted(glob.glob('data/' + filename  + '*.png'))
        unknown = load_unknowndata(fn)
        # Now predict the value of the digit on the second half:
        #expected = digits.target[n_samples / 2:]
        predicted = classifier.predict(unknown['data'])

        result = ''
        for pred, image, name in zip(predicted, unknown['images'], unknown['name']):
            print(pred)
            print(name)
            result += str(pred)

        # Check
        fn = 'pictures/' + filename  + '.png'
        image = skimage.io.imread(fn)
        pl.imshow(image[:150, 56:], cmap=pl.cm.gray_r, interpolation='nearest')
        pl.title('Predicted: %s' % result)
        pl.show()

Le résultat est que sur des images de 150 par 300 pixels, contenant 5 digits chacune, j'ai eu 100% de succès sur un test de 60 images. L'algo est même robuste à un changement de valeur du dernier chiffre significatif de la balance lors de la prise de la photo qui se traduit par une superposition de deux digits sur la photos. L'algo détecte l'une ou l'autre des valeurs qu'un humain verrait.

detection de nombres avec une fluctuation : 24774

En conclusion, on se retrouve avec un moyen simple de récupérer les valeurs de n'importe quel afficheur et sans effort ni bricolage.