157 votes

Extraire le texte OpenCV

Je cherche à trouver les boîtes englobantes du texte dans une image et j'utilise actuellement cette approche :

// calculer les variances locales de l'image en niveaux de gris
Mat t_mean, t_mean_2;
Mat grayF;
outImg_gray.convertTo(grayF, CV_32F);
int winSize = 35;
blur(grayF, t_mean, cv::Size(winSize,winSize));
blur(grayF.mul(grayF), t_mean_2, cv::Size(winSize,winSize));
Mat varMat = t_mean_2 - t_mean.mul(t_mean);
varMat.convertTo(varMat, CV_8U);

// seuiller les régions à haute variance
Mat varMatRegions = varMat > 100;

Lorsqu'on donne une image comme celle-ci :

entrer la description de l'image ici

Ensuite, lorsque je montre varMatRegions, j'obtiens cette image :

entrer la description de l'image ici

Comme vous pouvez le voir, cela combine en quelque sorte le bloc de texte de gauche avec l'en-tête de la carte, pour la plupart des cartes cette méthode fonctionne très bien mais sur les cartes plus chargées cela peut poser problème.

La raison pour laquelle il est mauvais que ces contours se connectent est que cela rend la boîte englobante du contour presque toute la carte.

Quelqu'un peut-il suggérer une autre façon de trouver le texte pour assurer une détection appropriée du texte ?

200 points à quiconque peut trouver le texte dans la carte au-dessus des deux premières.

entrer la description de l'image icientrer la description de l'image ici

1 votes

La façon la plus simple que je vois ici est d'augmenter le contraste avant d'obtenir les régions...

3 votes

Cool question. Merci de l'avoir posée et d'avoir posté la prime pour garantir ces réponses intéressantes.

0 votes

Nouveau dans la programmation. Est-ce que la même chose peut être faite pour le texte dans des scripts autres que l'anglais comme le sanskrit?

133voto

William Points 2094

Vous pouvez détecter du texte en trouvant des éléments de bord proches (inspiré d'un LPD):

#include "opencv2/opencv.hpp"

std::vector detectLetters(cv::Mat img)
{
    std::vector boundRect;
    cv::Mat img_gray, img_sobel, img_threshold, element;
    cvtColor(img, img_gray, CV_BGR2GRAY);
    cv::Sobel(img_gray, img_sobel, CV_8U, 1, 0, 3, 1, 0, cv::BORDER_DEFAULT);
    cv::threshold(img_sobel, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);
    element = getStructuringElement(cv::MORPH_RECT, cv::Size(17, 3) );
    cv::morphologyEx(img_threshold, img_threshold, CV_MOP_CLOSE, element); //Does the trick
    std::vector< std::vector< cv::Point> > contours;
    cv::findContours(img_threshold, contours, 0, 1); 
    std::vector > contours_poly( contours.size() );
    for( int i = 0; i < contours.size(); i++ )
        if (contours[i].size()>100)
        { 
            cv::approxPolyDP( cv::Mat(contours[i]), contours_poly[i], 3, true );
            cv::Rect appRect( boundingRect( cv::Mat(contours_poly[i]) ));
            if (appRect.width>appRect.height) 
                boundRect.push_back(appRect);
        }
    return boundRect;
}

Utilisation:

int main(int argc,char** argv)
{
    //Lire
    cv::Mat img1=cv::imread("side_1.jpg");
    cv::Mat img2=cv::imread("side_2.jpg");
    //Détecter
    std::vector letterBBoxes1=detectLetters(img1);
    std::vector letterBBoxes2=detectLetters(img2);
    //Afficher
    for(int i=0; i< letterBBoxes1.size(); i++)
        cv::rectangle(img1,letterBBoxes1[i],cv::Scalar(0,255,0),3,8,0);
    cv::imwrite( "imgOut1.jpg", img1);  
    for(int i=0; i< letterBBoxes2.size(); i++)
        cv::rectangle(img2,letterBBoxes2[i],cv::Scalar(0,255,0),3,8,0);
    cv::imwrite( "imgOut2.jpg", img2);  
    return 0;
}

Résultats:

a. element = getStructuringElement(cv::MORPH_RECT, cv::Size(17, 3) ); imgOut1imgOut2

b. element = getStructuringElement(cv::MORPH_RECT, cv::Size(30, 30) ); imgOut1imgOut2

Les résultats sont similaires pour l'autre image mentionnée.

7 votes

Détecteur de plaque d'immatriculation.

2 votes

Pour certaines cartes, la boîte englobante ne contient pas tout le texte, comme si une lettre était coupée en deux. Comme cette carte : i.imgur.com/tX3XrwH.jpg Comment puis-je étendre la hauteur et la largeur de toutes les boîtes englobantes de n? Merci pour la solution, ça fonctionne très bien!

4 votes

Dire cv::Rect a;. Agrandi par n : a.x -= n / 2; a.y -= n / 2; a.width += n; a.height += n;.

131voto

dhanushka Points 1101

J'ai utilisé une méthode basée sur le gradient dans le programme ci-dessous. Ajouté les images résultantes. Veuillez noter que j'utilise une version réduite de l'image pour le traitement.

Version c++

La licence MIT (MIT)

Copyright (c) 2014 Dhanushka Dangampola

La permission est accordée, gratuitement, à toute personne obtenant une copie
de ce logiciel et des fichiers de documentation associés (le "Logiciel"), de traiter
dans le Logiciel sans restriction, y compris, sans limitation, les droits
d'utiliser, copier, modifier, fusionner, publier, distribuer, concéder sous licence et/ou vendre
des copies du Logiciel, et de permettre aux personnes à qui le Logiciel est
fourni de le faire, sous réserve des conditions suivantes :

L'avis de droit d'auteur ci-dessus et cet avis de permission doivent être inclus dans
toutes les copies ou des parties substantielles du Logiciel.

LE LOGICIEL EST FOURNI "TEL QUEL", SANS GARANTIE D'AUCUNE SORTE, EXPRESSE OU
IMPLICITE, Y COMPRIS, MAIS SANS S'Y LIMITER AUX GARANTIES DE COMMERCIALISATION,
D'ADEQUATION A UN USAGE PARTICULIER ET D'ABSENCE DE CONTREFACON. EN AUCUN CAS LES
AUTEURS OU DETENTEURS DU DROIT D'AUTEUR NE PEUVENT ÊTRE TENU RESPONSABLES DE TOUTE RÉCLAMATION, DOMMAGES OU AUTRES
RESPONSABILITÉ, QUE CE SOIT DANS UNE ACTION CONTRACTUELLE, TORT OU AUTRE, DÉCOULANT DE,
HORS DE OU EN LIEN AVEC LE LOGICIEL OU L'UTILISATION OU AUTRES ACCORDS DANS
LE LOGICIEL.

#include "stdafx.h"

#include 
#include 
#include 
#include 

using namespace cv;
using namespace std;

#define INPUT_FILE              "1.jpg"
#define OUTPUT_FOLDER_PATH      string("")

int _tmain(int argc, _TCHAR* argv[])
{
    Mat large = imread(INPUT_FILE);
    Mat rgb;
    // downsample and use it for processing
    pyrDown(large, rgb);
    Mat small;
    cvtColor(rgb, small, CV_BGR2GRAY);
    // morphological gradient
    Mat grad;
    Mat morphKernel = getStructuringElement(MORPH_ELLIPSE, Size(3, 3));
    morphologyEx(small, grad, MORPH_GRADIENT, morphKernel);
    // binarize
    Mat bw;
    threshold(grad, bw, 0.0, 255.0, THRESH_BINARY | THRESH_OTSU);
    // connect horizontally oriented regions
    Mat connected;
    morphKernel = getStructuringElement(MORPH_RECT, Size(9, 1));
    morphologyEx(bw, connected, MORPH_CLOSE, morphKernel);
    // find contours
    Mat mask = Mat::zeros(bw.size(), CV_8UC1);
    vector> contours;
    vector hierarchy;
    findContours(connected, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));
    // filter contours
    for(int idx = 0; idx >= 0; idx = hierarchy[idx][0])
    {
        Rect rect = boundingRect(contours[idx]);
        Mat maskROI(mask, rect);
        maskROI = Scalar(0, 0, 0);
        // fill the contour
        drawContours(mask, contours, idx, Scalar(255, 255, 255), CV_FILLED);
        // ratio of non-zero pixels in the filled region
        double r = (double)countNonZero(maskROI)/(rect.width*rect.height);

        if (r > .45 /* assume at least 45% of the area is filled if it contains text */
            && 
            (rect.height > 8 && rect.width > 8) /* constraints on region size */
            /* these two conditions alone are not very robust. better to use something 
            like the number of significant peaks in a horizontal projection as a third condition */
            )
        {
            rectangle(rgb, rect, Scalar(0, 255, 0), 2);
        }
    }
    imwrite(OUTPUT_FOLDER_PATH + string("rgb.jpg"), rgb);

    return 0;
}

Version python

La licence MIT (MIT)

Copyright (c) 2017 Dhanushka Dangampola

La permission est accordée, gratuitement, à toute personne obtenant une copie
de ce logiciel et des fichiers de documentation associés (le "Logiciel"), de traiter
dans le Logiciel sans restriction, y compris, sans limitation, les droits
d'utiliser, copier, modifier, fusionner, publier, distribuer, concéder sous licence et/ou vendre
des copies du Logiciel, et de permettre aux personnes à qui le Logiciel est
fourni de le faire, sous réserve des conditions suivantes :

L'avis de droit d'auteur ci-dessus et cet avis de permission doivent être inclus dans
toutes les copies ou des parties substantielles du Logiciel.

LE LOGICIEL EST FOURNI "TEL QUEL", SANS GARANTIE D'AUCUNE SORTE, EXPRESSE OU
IMPLICITE, Y COMPRIS, MAIS SANS S'Y LIMITER AUX GARANTIES DE COMMERCIALISATION,
D'ADEQUATION A UN USAGE PARTICULIER ET D'ABSENCE DE CONTREFACON. EN AUCUN CAS LES
AUTEURS OU DETENTEURS DU DROIT D'AUTEUR NE PEUVENT ÊTRE TENU RESPONSABLES DE TOUTE RÉCLAMATION, DOMMAGES OU AUTRES
RESPONSABILITÉ, QUE CE SOIT DANS UNE ACTION CONTRACTUELLE, TORT OU AUTRE, DÉCOULANT DE,
HORS DE OU EN LIEN AVEC LE LOGICIEL OU L'UTILISATION OU AUTRES ACCORDS DANS
LE LOGICIEL.

import cv2
import numpy as np

large = cv2.imread('1.jpg')
rgb = cv2.pyrDown(large)
small = cv2.cvtColor(rgb, cv2.COLOR_BGR2GRAY)

kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
grad = cv2.morphologyEx(small, cv2.MORPH_GRADIENT, kernel)

_, bw = cv2.threshold(grad, 0.0, 255.0, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 1))
connected = cv2.morphologyEx(bw, cv2.MORPH_CLOSE, kernel)
# en utilisant RETR_EXTERNAL au lieu de RETR_CCOMP
contours, hierarchy = cv2.findContours(connected.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
#Pour opencv 3+ commenter la ligne précédente et décommenter la ligne suivante
#_, contours, hierarchy = cv2.findContours(connected.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

mask = np.zeros(bw.shape, dtype=np.uint8)

for idx in range(len(contours)):
    x, y, w, h = cv2.boundingRect(contours[idx])
    mask[y:y+h, x:x+w] = 0
    cv2.drawContours(mask, contours, idx, (255, 255, 255), -1)
    r = float(cv2.countNonZero(mask[y:y+h, x:x+w])) / (w * h)

    if r > 0.45 and w > 8 and h > 8:
        cv2.rectangle(rgb, (x, y), (x+w-1, y+h-1), (0, 255, 0), 2)

cv2.imshow('rects', rgb)

entrez la description de l'image icientrez la description de l'image icientrez la description de l'image ici

0 votes

Y a-t-il une différence significative entre votre solution et celle de William, à part la taille de l'image ? J'essaie de comprendre vos approches mais les explications sont brèves.

3 votes

Je viens de jeter un coup d'œil à son approche. La principale différence que je remarque est qu'il utilise un filtre de Sobel tandis que j'utilise un filtre de gradient morphologique. Je pense que le filtre morphologique et le sous-échantillonnage aplatissent une grande partie des contours pas très marqués. Le filtre de Sobel pourrait capturer plus de bruit.

0 votes

Très bonne solution. Nécessite quelques améliorations pour mon cas mais en général, Merci!

55voto

anana Points 591

Voici une approche alternative que j'ai utilisée pour détecter les blocs de texte :

  1. Converti l'image en nuances de gris
  2. Appliqué un seuillage (seuillage binaire simple, avec une valeur de 150 comme valeur de seuil)
  3. Appliqué une dilatation pour épaissir les lignes de l'image, permettant d'obtenir des objets plus compacts et moins de fragments d'espace blanc. Utilisé une valeur élevée pour le nombre d'itérations, donc la dilatation est très prononcée (13 itérations, également choisi pour des résultats optimaux).
  4. Identifié les contours des objets dans l'image résultante en utilisant la fonction findContours d'opencv.
  5. Tracé une boîte englobante (rectangle) circonscrivant chaque objet contouré - chacun cadre un bloc de texte.
  6. Dans certains cas, exclure les zones qui ne sont probablement pas l'objet que vous recherchez (par exemple, des blocs de texte) en fonction de leur taille, car l'algorithme ci-dessus peut également trouver des objets intersectant ou imbriqués (comme toute la zone supérieure de la première carte) dont certains pourraient être inintéressants pour vos besoins.

Voici le code écrit en python avec pyopencv, facilement portable en C++.

import cv2

image = cv2.imread("card.png")
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) # en nuances de gris
_,thresh = cv2.threshold(gray,150,255,cv2.THRESH_BINARY_INV) # seuillage
kernel = cv2.getStructuringElement(cv2.MORPH_CROSS,(3,3))
dilated = cv2.dilate(thresh,kernel,iterations = 13) # dilatation
_, contours, hierarchy = cv2.findContours(dilated,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE) # obtenir les contours

# pour chaque contour trouvé, dessiner un rectangle autour sur l'image d'origine
for contour in contours:
    # obtenir le rectangle délimitant le contour
    [x,y,w,h] = cv2.boundingRect(contour)

    # exclure les zones trop grandes
    if h>300 and w>300:
        continue

    # exclure les zones trop petites
    if h<40 or w<40:
        continue

    # dessiner un rectangle autour du contour sur l'image d'origine
    cv2.rectangle(image,(x,y),(x+w,y+h),(255,0,255),2)

# écrire l'image d'origine avec les contours ajoutés sur le disque  
cv2.imwrite("contoured.jpg", image) 

L'image d'origine est la première image de votre publication.

Après le prétraitement (nuances de gris, seuillage et dilatation - donc après l'étape 3), l'image ressemblait à ceci :

Image dilatée

Voici l'image résultante ("contoured.jpg" dans la dernière ligne) ; les boîtes englobantes finales pour les objets dans l'image ressemblent à ceci :

description de l'image ici

Vous pouvez voir que le bloc de texte à gauche est détecté comme un bloc séparé, délimité de son environnement.

En utilisant le même script avec les mêmes paramètres (à l'exception du type de seuillage qui a été modifié pour la deuxième image comme décrit ci-dessous), voici les résultats pour les 2 autres cartes :

description de l'image ici

description de l'image ici

Optimisation des paramètres

Les paramètres (valeur du seuil, paramètres de dilatation) ont été optimisés pour cette image et cette tâche (recherche de blocs de texte) et peuvent être ajustés, si nécessaire, pour d'autres images de cartes ou d'autres types d'objets à trouver.

Pour le seuillage (étape 2), j'ai utilisé un seuil noir. Pour les images où le texte est plus clair que l'arrière-plan, comme la deuxième image de votre publication, un seuil blanc doit être utilisé, donc remplacez le type de seuillage par cv2.THRESH_BINARY). Pour la deuxième image, j'ai également utilisé une valeur légèrement plus élevée pour le seuil (180). Variabilité les paramètres de la valeur du seuil et le nombre d'itérations pour la dilatation donnera des degrés différents de sensibilité dans la délimitation des objets dans l'image.

Recherche d'autres types d'objets :

Par exemple, en réduisant la dilatation à 5 itérations dans la première image nous donne une délimitation plus fine des objets dans l'image, trouvant approximativement tous les mots dans l'image (plutôt que des blocs de texte) :

description de l'image ici

Connaissant la taille approximative d'un mot, j'ai exclu les zones qui étaient trop petites (moins de 20 pixels de largeur ou de hauteur) ou trop grandes (plus de 100 pixels de largeur ou de hauteur) pour ignorer les objets qui ne sont probablement pas des mots, pour obtenir les résultats dans l'image ci-dessus.

2 votes

Vous êtes incroyable! Je vais essayer cela demain.

0 votes

J'ai ajouté une autre étape pour éliminer les objets non pertinents; j'ai également ajouté un exemple pour identifier les mots ou d'autres types d'objets (autre que des blocs de texte)

0 votes

Merci pour la réponse détaillée, cependant je reçois une erreur dans cv2.findContours. Il dit ValueError: trop de valeurs à déballer.

29voto

rtkaleta Points 598

L'approche de @dhanushka a montré le plus de promesse, mais je voulais m'amuser avec Python, donc j'ai continué et l'ai traduit pour le plaisir :

import cv2
import numpy as np
from cv2 import boundingRect, countNonZero, cvtColor, drawContours, findContours, getStructuringElement, imread, morphologyEx, pyrDown, rectangle, threshold

large = imread(image_path)
# sous-échantillonnage et utilisation pour le traitement
rgb = pyrDown(large)
# appliquer l'échelle de gris
small = cvtColor(rgb, cv2.COLOR_BGR2GRAY)
# gradient morphologique
morph_kernel = getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
grad = morphologyEx(small, cv2.MORPH_GRADIENT, morph_kernel)
# binariser
_, bw = threshold(src=grad, thresh=0, maxval=255, type=cv2.THRESH_BINARY+cv2.THRESH_OTSU)
morph_kernel = getStructuringElement(cv2.MORPH_RECT, (9, 1))
# connecter les régions orientées horizontalement
connected = morphologyEx(bw, cv2.MORPH_CLOSE, morph_kernel)
mask = np.zeros(bw.shape, np.uint8)
# trouver les contours
im2, contours, hierarchy = findContours(connected, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
# filtrer les contours
for idx in range(0, len(hierarchy[0])):
    rect = x, y, rect_width, rect_height = boundingRect(contours[idx])
    # remplir le contour
    mask = drawContours(mask, contours, idx, (255, 255, 2555), cv2.FILLED)
    # ratio de pixels non nuls dans la région remplie
    r = float(countNonZero(mask)) / (rect_width * rect_height)
    if r > 0.45 and rect_height > 8 and rect_width > 8:
        rgb = rectangle(rgb, (x, y+rect_height), (x+rect_width, y), (0,255,0),3)

Maintenant pour afficher l'image :

from PIL import Image
Image.fromarray(rgb).show()

Le script n'est pas le plus "Pythonique", mais j'ai essayé de le faire ressembler le plus possible au code C++ original pour que les lecteurs puissent suivre.

Cela fonctionne presque aussi bien que l'original. Je serai ravi de lire des suggestions sur la manière dont il pourrait être amélioré/corrigé pour ressembler pleinement aux résultats originaux.

Entrez une description d'image ici

Entrez une description d'image ici

Entrez une description d'image ici

3 votes

Merci d'avoir fourni une version en python. Beaucoup de personnes trouveront cela utile. +1

0 votes

Quelle est la différence entre remplir le contour et le dessiner ? J'ai trouvé un code sans la phase de remplissage ici : stackoverflow.com/a/23556997/6837132

0 votes

@SarahM Je ne sais pas si vous parlez de la différence générique entre le dessin et le remplissage (assez évident je pense?) ou de l'API OpenCV spécifiquement? Si c'est le dernier cas, consultez la documentation pour drawContours qui indique que "La fonction dessine les contours dans l'image si l'épaisseur > 0 ou rempli la zone délimitée par les contours si l'épaisseur < 0." Cela est fait pour que nous puissions vérifier le ratio de pixels non nuls pour décider si la boîte contient vraisemblablement du texte.

15voto

herohuyongtao Points 13552

Vous pouvez essayer cette méthode développée par Chucai Yi et Yingli Tian.

Ils partagent également un logiciel (basé sur Opencv-1.0 et devant fonctionner sous la plate-forme Windows) que vous pouvez utiliser (bien que le code source ne soit pas disponible). Il générera tous les cadres de texte (affichés en ombres de couleur) dans l'image. En appliquant à vos images d'échantillons, vous obtiendrez les résultats suivants :

Remarque : pour rendre le résultat plus robuste, vous pouvez fusionner les cadres adjacents ensemble.


Mise à jour : Si votre objectif ultime est de reconnaître les textes dans l'image, vous pouvez également consulter gttext, qui est un logiciel OCR gratuit et un outil de vérification de la vérité terrain pour les images en couleur avec du texte. Le code source est également disponible.

Avec cela, vous pouvez obtenir des textes reconnus comme :

0 votes

Gttext est pour Windows. Des suggestions pour les utilisateurs Mac/Linux ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X