Créez un simple OMR (lecteur de feuille de marque) avec Python et OpenCV

Je cherchais OMR (Optical Mark Reader) parce que je voulais rendre la saisie de données aussi efficace que possible lors d'une enquête par questionnaire papier, mais je ne trouvais pas ce que je cherchais, alors j'ai moi-même choisi un simple. J'ai décidé de le faire. Il existe peut-être un moyen plus intelligent, mais j'ai créé quelque chose qui fonctionne comme il se doit.

OpenCV est utilisé pour le traitement de reconnaissance d'image, et NumPy est utilisé pour le traitement numérique. Notez que vous devez être un peu prudent sur la façon d'utiliser OpenCV 3 à partir de Python 3 en utilisant Homebrew sur Mac. Il ne peut pas être utilisé à partir de Python 3 sauf si l'option --with-python3 est ajoutée lors de l'exécution de l'installation de brew. De plus, comme il s'agit d'un package keg only, vous devez définir vous-même le chemin de la bibliothèque pour Python 2 et 3. Si vous effectuez une recherche sur le net, vous verrez différentes pages qui présentent la procédure, mais celle qui est facile à comprendre est cette page. Je pense.

Écoulement brutal

Préparation préalable

Créer une feuille de notes

Tout d'abord, créez une feuille de repères car elle ne démarrera pas sans elle. Si vous souhaitez rendre le processus de lecture des repères aussi simple que possible, il y a quelques points à garder à l'esprit lors de la création de la feuille de repères.

Remarque 1: la marque elle-même est aussi fine que possible

Si la marque elle-même est dessinée en noir, il sera difficile de déterminer si elle est remplie. La marque elle-même doit être faite dans une couleur claire pour ne pas être difficile à voir, ou elle doit être faite avec des lignes assez fines.

Les repères des feuilles de repères peuvent être créés à l'aide d'Illustrator, etc., ou ils peuvent être créés à l'aide de figures de base d'un logiciel de traitement de texte tel que MS Word et Pages. La figure ci-dessous est créée à l'aide du rectangle arrondi de Pages. La couleur est 40% de gris, le nombre est de 5 pt de Hiragino Kakugo W1 et la ligne environnante est de 0,3 pt. Sur la feuille de marquage, la taille de la marque est de 3 mm de largeur et 5 mm de hauteur.

mark.png

Remarque 2: les marques doivent être régulièrement espacées avec un espace suffisant entre elles.

Pour simplifier le processus de reconnaissance des marques, séparez un peu les marques et disposez-les de manière à ce qu'elles soient ** régulièrement espacées ** dans chacune des directions verticale et horizontale. Si l'espacement des marques est suffisamment large, il n'y a pas de problème même si les marques s'écartent légèrement de l'espacement régulier.

Remarque 3: Préparez un point caractéristique (marqueur)

Des points caractéristiques (** marqueurs **) sont nécessaires pour extraire la zone de la feuille de repères. Créez une figure qui a une forme relativement simple et ne chevauche pas d'autres caractères ou symboles, et placez 2 à 4 aux quatre coins de la zone utilisée pour le traitement de reconnaissance.

À ce stade, assurez-vous que le ** coin supérieur gauche ** du marqueur correspond au coin de la zone de reconnaissance. Dans l'exemple de feuille de repères, la zone avec une marge de 3 lignes pour la hauteur de la ligne de repère au-dessus de la zone où la marque est placée et 1 ligne en dessous est utilisée comme zone de reconnaissance, et les coins supérieur gauche, supérieur droit et inférieur droit de cette zone sont utilisés. Des marqueurs sont attachés aux trois endroits. Je pense que c'est correct de n'avoir que deux marqueurs, le coin supérieur gauche et le coin inférieur droit, mais je pensais que l'inclinaison de la page pouvait être mal évaluée, alors je l'ai placée en haut à droite au cas où. L'image ci-dessous est un échantillon du papier de feuille de repère créé.

marksheet.png

Étant donné que le fichier image du marqueur est requis pour le traitement de la reconnaissance, enregistrez uniquement le marqueur en tant que fichier image séparément de la feuille de papier marqueur.

marker.jpg ← マーカーだけを画像として保存する

Ceci termine la préparation de la feuille de notes.

Marquer la numérisation de la feuille

La feuille de repères remplie est lue en ** échelle de gris ** à l'aide d'un scanner de documents tel que ScanSnap. Si l'image numérisée est inclinée, elle ne peut pas être correctement reconnue. Il est possible d'effectuer une correction d'inclinaison avec OpenCV, mais le traitement sera compliqué en conséquence, donc ici, activez la ** correction d'inclinaison ** avec le logiciel de numérisation du scanner et lisez. Si l'orientation de la feuille de repère n'est pas alignée, activez également la rotation automatique.

L'image ci-dessous est le résultat de la numérisation en marquant le papier de la feuille de repères d'échantillon de manière appropriée (le format a été réduit).

sample001_small.png

Processus de reconnaissance

À partir de là, il s'agit d'un traitement de reconnaissance d'image utilisant Python et OpenCV. Tout d'abord, importez NumPy et OpenCV.

import numpy as np
import cv2

Recadrage et redimensionnement d'image

Lisez l'image numérisée et utilisez le marqueur pour découper uniquement la zone requise.

Tout d'abord, lisez le fichier image du marqueur (marker.jpg dans ce cas) pour découper la plage de la feuille de repères et effectuez les réglages nécessaires. La correspondance de modèle est utilisée pour identifier la position du marqueur avec OpenCV, mais la taille de l'image du modèle (marqueur) doit être la même que la taille du marqueur dans l'image numérisée. Redimensionnez le marqueur en fonction de la résolution de numérisation afin que la taille de l'image du marqueur corresponde à peu près à la taille de l'image numérisée. Vous pouvez agrandir la taille du marqueur et l'enregistrer à l'avance.

###Paramètres des marqueurs

marker_dpi = 72 #Résolution d'écran(Taille du marqueur)
scan_dpi = 300 #Résolution d'image numérisée

#niveaux de gris(mode = 0)Lisez le fichier avec
marker=cv2.imread('marker.jpg',0) 

#Obtenez la taille du marqueur
w, h = marker.shape[::-1]

#Redimensionner le marqueur
marker = cv2.resize(marker, (int(h*scan_dpi/marker_dpi), int(w*scan_dpi/marker_dpi)))

Ensuite, chargez l'image de la feuille de repères numérisée. On suppose que l'image numérisée est enregistrée sous «sample001.jpg».

###Charger l'image numérisée
img = cv2.imread('sample001.jpg',0)

Les marqueurs sont extraits de cette image numérisée à l'aide de la fonction de correspondance de modèle matchTemplate ().

res = cv2.matchTemplate(img, marker, cv2.TM_CCOEFF_NORMED)

Cette partie cv2.TM_CCOEFF_NORMED spécifie la fonction pour juger de la similitude. S'il n'y a pas de problème particulier, vous pouvez le laisser tel quel.

Le résultat correspondant par matchTemplate () contient une valeur qui indique le degré de similitude (maximum = 1,0) avec l'image modèle pour chaque coordonnée de l'image numérisée. De là, seules les parties présentant un degré de similitude élevé sont extraites.

threshold = 0.7
loc = np.where( res >= threshold)

Ici, seules les coordonnées dont la similitude est égale ou supérieure à 0,7 sont extraites. Ajustez cette valeur si nécessaire. Plus la valeur est élevée, plus le jugement est strict, et plus la valeur est petite, plus le jugement est lâche. Par exemple, si la taille du marqueur utilisé comme modèle est extrêmement différente de la taille du marqueur dans l'image numérisée, la similitude sera faible et elle ne sera pas reconnue correctement à moins que cette valeur ne soit abaissée. De plus, si les critères sont trop lâches, les non-marqueurs seront faussement détectés. Si la taille du marqueur est appropriée, je pense qu'elle devrait être d'environ 0,7.

À partir des coordonnées extraites, recherchez les coordonnées en haut à gauche et en bas à droite de la zone de reconnaissance. La valeur de coordonnée supérieure gauche est la valeur de coordonnée minimale pour «x» et «y», et la valeur de coordonnée inférieure droite est la plus grande valeur de coordonnée pour «x» et «y» parmi les résultats à forte similitude. Notez que les valeurs de coordonnées extraites sont stockées dans le tableau dans l'ordre «y» et «x».

mark_area={}
mark_area['top_x']= min(loc[1])
mark_area['top_y']= min(loc[0])
mark_area['bottom_x']= max(loc[1])
mark_area['bottom_y']= max(loc[0])

Une image numérisée est découpée en fonction de ces coordonnées. Pour recadrer une image, spécifiez simplement les coordonnées de la zone requise pour l'image d'origine. Cependant, il convient de noter que l'ordre est la coordonnée Y et la coordonnée X.

img = img[mark_area['top_y']:mark_area['bottom_y'],mark_area['top_x']:mark_area['bottom_x']]

Notez la zone découpée et vérifiez si elle est correctement découpée.

cv2.imwrite('res.png',img)

clipped_small.png

Il y a un léger blanc sur la gauche, mais ce n'est pas un problème.

Ensuite, afin de faciliter le traitement ultérieur, redimensionnez l'image découpée de sorte qu'elle soit un multiple entier du nombre de colonnes et de lignes de la marque. Ici, le nombre de colonnes et de lignes est de 100 fois. Lorsque vous comptez le nombre de lignes, tenez compte de la marge entre la zone de marque et le marqueur.

n_col = 7 #Nombre de marques par ligne

n_row = 7 #Nombre de lignes de marque
margin_top = 3 #Nombre de lignes de marge supérieures
margin_bottom = 1 #Nombre de lignes de marge inférieure

n_row = n_row + margin_top + margin_bottom #Nombre de lignes(Marquer la ligne 7 lignes+Marge supérieure 3 lignes+Marge inférieure 1 ligne)

img = cv2.resize(img, (n_col*100, n_row*100))

De plus, après avoir légèrement brouillé l'image découpée, l'image est binarisée en noir et blanc, et le noir et blanc sont inversés. Dans l'exemple ci-dessous, un flou gaussien est appliqué puis binarisé sur la base d'une luminosité de 50. L'inversion noir et blanc soustrait uniquement la valeur d'image de 255.

###Brouiller
img = cv2.GaussianBlur(img,(5,5),0)

###Binar avec 50 comme seuil
res, img = cv2.threshold(img, 50, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)

###Inversion noir et blanc
img = 255 - img

Le résultat de ces opérations est le suivant.

dicho_small.png

Marquer la reconnaissance

Les marques sont reconnues en découpant et en redimensionnant l'image, puis en la divisant en lignes.

Le processus à effectuer pour chaque ligne consiste d'abord à diviser l'image horizontalement en nombre de marques et à calculer le total de chaque valeur d'image. Puisque l'image est inversée en noir et blanc, la partie colorée est blanche («255») et la partie vierge est noire («0»). En d'autres termes, le total des valeurs d'image signifie la zone de la partie colorée (partie marquée).

Ensuite, la ** médiane ** de la surface de la partie marquée est calculée et utilisée comme ** seuil ** pour déterminer si cette médiane est marquée ou non. Étant donné que certaines parties de la feuille de repères sont à l'origine colorées, telles que les lignes et les chiffres imprimés, les parties non marquées sont également colorées dans une certaine mesure. Par conséquent, dans l'exemple suivant, si la zone de la partie colorée est 3 fois ou plus de la valeur médiane calculée, elle est jugée comme «Vrai», et si ce n'est pas le cas, elle est jugée comme «Faux». Ajustez ce multiple si nécessaire. Plus le multiple est grand, plus le jugement est strict, et plus le multiple est petit, plus le jugement est doux.

Cette méthode est basée sur l'hypothèse que chaque ligne sera remplie d'un ou deux points. Puisqu'elle est basée sur la valeur médiane, elle ne fonctionnera pas bien si toutes les marques sont remplies.

###Préparez un tableau pour mettre le résultat
result = []

###Traitement ligne par ligne(Traiter en excluant les lignes de marge)
for row in range(margin_top, n_row - margin_bottom):

    ###Découpez uniquement la ligne à traiter
    tmp_img = img [row*100:(row+1)*100,]
    area_sum = [] #Un tableau pour mettre la valeur totale
    
    ###Traitement de chaque marque
    for col in range(n_col):

        ###Calculez la valeur totale des images dans chaque zone de marque avec NumPy
        area_sum.append(np.sum(tmp_img[:,col*100:(col+1)*100]))

    ###Jugé selon que la valeur totale de la zone d'image est égale ou supérieure à 3 fois la valeur médiane
    result.append(area_sum > np.median(area_sum) * 3)

Sortie de résultat

Sortons le résultat de la reconnaissance. Pour être prudent, assurez-vous qu'il n'y a pas de marques multiples sur la même ligne.

for x in range(len(result)):
    res = np.where(result[x]==True)[0]+1
    if len(res)>1:
        print('Q%d: ' % (x+1) +str(res)+ ' ##Réponses multiples##')
    elif len(res)==1:
        print('Q%d: ' % (x+1) +str(res))
    else:
        print('Q%d: **Sans réponse**' % (x+1))
Q1: [1]
Q2: [2]
Q3: [3]
Q4: [4]
Q5: [5]
Q6: [6]
Q7: [7]

Il a été reconnu correctement. Il n'y a pas d'avertissement multi-réponses ici, mais si vous abaissez un peu le seuil de reconnaissance, la fausse reconnaissance augmentera et vous pourrez voir les avertissements multi-réponses.

Lorsque le seuil de reconnaissance était fixé à deux fois la valeur médiane et exécuté, le résultat était le suivant.

Q1: [1]
Q2: [2]
Q3: [3]
Q4: [4 7] ##Réponses multiples
Q5: [5]
Q6: [6]
Q7: [7]

Lorsque le seuil de reconnaissance était fixé à 50 fois la valeur médiane et exécuté, le résultat était le suivant.

Q1: [1]
Q2: **Sans réponse**
Q3: **Sans réponse**
Q4: [4]
Q5: **Sans réponse**
Q6: [6]
Q7: **Sans réponse**

Résumé

Si vous fabriquez correctement le papier de la feuille de marquage, vous pouvez créer un lecteur de feuille de marque avec une capacité de reconnaissance décente relativement facilement en l'utilisant avec un scanner de documents. Étant donné que l'échantillon présenté ici n'est qu'un échantillon, il n'est pas traité de manière à pouvoir reconnaître plusieurs images numérisées, mais je pense qu'il n'est pas si difficile de reconnaître tous les fichiers image du dossier.

Si la disposition de la feuille de notes change, il est nécessaire de modifier le script, mais comme la disposition de la feuille de notes est peu susceptible de changer dans mon environnement, il est presque nécessaire de la modifier après avoir d'abord essayé plusieurs fois et ajusté le seuil. il n'y a pas. En fait, j'ai traité des dizaines à des centaines de feuilles par semaine pendant plus d'un an en utilisant cet OMR avec quelques fonctions ajoutées, mais j'ai changé la valeur de réglage plusieurs fois pendant cette période. Il n'y a que.

Recommended Posts

Créez un simple OMR (lecteur de feuille de marque) avec Python et OpenCV
Essayez de créer un jeu simple avec Python 3 et iPhone
Comment créer une caméra de surveillance (caméra de sécurité) avec Opencv et Python
Créer une lecture de feuille de notes avec Python OpenCV (Conseils pour bien lire)
Créez un Slackbot simple avec un bouton interactif en python
Faites une loterie avec Python
Qu'est-ce que Dieu? Créez un chatbot simple avec python
Créez un outil d'analyse vidéo simple avec python wxpython + openCV
Briller la vie avec Python et OpenCV
Réseau neuronal avec OpenCV 3 et Python 3
Faisons une interface graphique avec python.
Créer un système de recommandation avec python
Faisons un graphe avec python! !!
J'ai fait un circuit simple avec Python (AND, OR, NOR, etc.)
Rubyist a essayé de créer une API simple avec Python + bouteille + MySQL
Créez une illusion rayée avec correction gamma pour Python3 et openCV3
Dessinez une illusion d'aquarelle avec détection des contours en Python3 et openCV3
J'ai essayé de faire un processus d'exécution périodique avec Selenium et Python
Créez une application de scraping avec Python + Django + AWS et modifiez les tâches
Faites en sorte que les vidéos floues ressemblent à des caméras à point fixe avec Python et OpenCV
Créer un fichier power simple avec Python
Lecteur RSS simple réalisé avec Django
Créer un lecteur vidéo avec PySimpleGUI + OpenCV
Capturer des images avec Pupil, python et OpenCV
Fractal pour faire et jouer avec Python
Un mémo contenant Python2.7 et Python3 dans CentOS
Faisons la voix lentement avec Python
Faisons un langage simple avec PLY 1
Créez un framework Web avec Python! (1)
Créez une application de bureau avec Python avec Electron
Faisons un bot Twitter avec Python!
Créez un framework Web avec Python! (2)
J'ai fait un simple blackjack avec Python
Créez un convertisseur Ethernet LAN sans fil et un routeur simple avec Raspberry Pi
Créez un arbre de décision à partir de 0 avec Python et comprenez-le (4. Structure des données)
Hello World et détection de visage avec OpenCV 4.3 + Python
Créer un bot Twitter Trend avec heroku + Python
Construire un environnement python avec virtualenv et direnv
Créez un simple générateur d'images par points avec Flask
Je veux faire un jeu avec Python
Installez OpenCV 4.0 et Python 3.7 sur Windows 10 avec Anaconda
Démarrez un serveur Web Python simple avec Docker
Essayez de créer un code de "décryptage" en Python
Rendre OpenCV3 disponible à partir de python3 installé avec pyenv
Lancer un serveur Web avec Python et Flask
Remplaçons UWSC par Python (5) Faisons un robot
Essayez de créer un groupe de dièdre avec Python
Liste de tâches simple créée avec Python + Django
Correspondance des fonctionnalités avec OpenCV 3 et Python 3 (A-KAZE, KNN)
[# 1] Créez Minecraft avec Python. ~ Recherche préliminaire et conception ~
Mettez Docker dans Windows Home et exécutez un serveur Web simple avec Python
J'ai essayé de faire un processus périodique avec CentOS7, Selenium, Python et Chrome
J'ai fait une application d'envoi de courrier simple avec tkinter de Python
Créons un système de réception simple avec le framework sans serveur Python Chalice et Twilio
WEB grattage avec python et essayez de créer un nuage de mots à partir des critiques
Obtenez le cours de l'action d'une entreprise japonaise avec Python et faites un graphique