web-dev-qa-db-fra.com

Suppression des soulignements horizontaux

J'essaie d'extraire du texte de quelques centaines de JPG contenant des informations sur les dossiers de la peine capitale; les JPG sont hébergés par le Texas Department of Criminal Justice (TDCJ). Ci-dessous, un exemple d'extrait avec des informations personnellement identifiables supprimées.

enter image description here

J'ai identifié les soulignements comme étant un obstacle à une bonne reconnaissance optique de caractères - si j'entre, capture d'écran un sous-extrait et des lignes manuellement blanches, l'OCR résultant via pytesseract est très bon. Mais avec les soulignements présents, c'est extrêmement pauvre.

Comment puis-je supprimer au mieux ces lignes horizontales? Ce que j'ai essayé:

Marquer cette question avec c ++ dans l'espoir que quelqu'un puisse aider à traduire l'étape 5 de la procédure pas à pas docs en Python. J'ai essayé un lot de transformations telles que Hugh Line Transform, mais je me sens dans le noir au sein d'une bibliothèque et d'une zone avec laquelle je n'ai aucune expérience préalable.

import cv2

# Inverted grayscale
img = cv2.imread('rsnippet.jpg', cv2.IMREAD_GRAYSCALE)
img = cv2.bitwise_not(img)

# Transform inverted grayscale to binary
th = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
                            cv2.THRESH_BINARY, 15, -2)

# An alternative; Not sure if `th` or `th2` is optimal here
th2 = cv2.threshold(img, 170, 255, cv2.THRESH_BINARY)[1]

# Create corresponding structure element for horizontal lines.
# Start by cloning th/th2.
horiz = th.copy()
r, c = horiz.shape

# Lost after here - not understanding intuition behind sizing/partitioning
22
Brad Solomon

Jusqu'à présent, toutes les réponses semblent utiliser des opérations morphologiques. Voici quelque chose d'un peu différent. Cela devrait donner des résultats assez bons si les lignes sont horizontales .

Pour cela, j'utilise une partie de votre exemple d'image ci-dessous.

sample

Chargez l'image, convertissez-la en niveaux de gris et inversez-la.

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

im = cv2.imread('sample.jpg')
gray = 255 - cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)

Image inversée en niveaux de gris:

inverted-gray

Si vous numérisez une ligne dans cette image inversée, vous verrez que son profil est différent en fonction de la présence ou de l'absence d'une ligne.

plt.figure(1)
plt.plot(gray[18, :] > 16, 'g-')
plt.axis([0, gray.shape[1], 0, 1.1])
plt.figure(2)
plt.plot(gray[36, :] > 16, 'r-')
plt.axis([0, gray.shape[1], 0, 1.1])

Le profil en vert est une ligne où il n'y a pas de soulignement, le rouge est pour une ligne avec soulignement. Si vous prenez la moyenne de chaque profil, vous verrez que le rouge a une moyenne plus élevée.

no-lineline

Ainsi, en utilisant cette approche, vous pouvez détecter les soulignements et les supprimer.

for row in range(gray.shape[0]):
    avg = np.average(gray[row, :] > 16)
    if avg > 0.9:
        cv2.line(im, (0, row), (gray.shape[1]-1, row), (0, 0, 255))
        cv2.line(gray, (0, row), (gray.shape[1]-1, row), (0, 0, 0), 1)

cv2.imshow("gray", 255 - gray)
cv2.imshow("im", im)

Voici les soulignements détectés en rouge et l'image nettoyée.

detectedcleaned

sortie tesseract de l'image nettoyée:

Convthed as th(
shot once in the
she stepped fr<
brother-in-lawii
collect on life in
applied for man
to the scheme i|

La raison de l'utilisation d'une partie de l'image devrait être claire à ce jour. Étant donné que les informations personnellement identifiables ont été supprimées dans l'image d'origine, le seuil n'aurait pas fonctionné. Mais cela ne devrait pas être un problème lorsque vous l'appliquez pour le traitement. Parfois, vous devrez peut-être ajuster les seuils (16, 0,9).

Le résultat ne semble pas très bon avec des parties des lettres supprimées et certaines des lignes pâles restant encore. Mettra à jour si je peux l'améliorer un peu plus.

MISE À JOUR:

Dis quelques améliorations; nettoyer et relier les parties manquantes des lettres. J'ai commenté le code, donc je pense que le processus est clair. Vous pouvez également vérifier les images intermédiaires résultantes pour voir comment cela fonctionne. Les résultats sont un peu meilleurs.

11-clean

sortie tesseract de l'image nettoyée:

Convicted as th(
shot once in the
she stepped fr<
brother-in-law. ‘
collect on life ix
applied for man
to the scheme i|

22-clean

sortie tesseract de l'image nettoyée:

)r-hire of 29-year-old .
revolver in the garage ‘
red that the victim‘s h
{2000 to kill her. mum
250.000. Before the kil
If$| 50.000 each on bin
to police.

code python:

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

im = cv2.imread('sample2.jpg')
gray = 255 - cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
# prepare a mask using Otsu threshold, then copy from original. this removes some noise
__, bw = cv2.threshold(cv2.dilate(gray, None), 128, 255, cv2.THRESH_BINARY or cv2.THRESH_OTSU)
gray = cv2.bitwise_and(gray, bw)
# make copy of the low-noise underlined image
grayu = gray.copy()
imcpy = im.copy()
# scan each row and remove lines
for row in range(gray.shape[0]):
    avg = np.average(gray[row, :] > 16)
    if avg > 0.9:
        cv2.line(im, (0, row), (gray.shape[1]-1, row), (0, 0, 255))
        cv2.line(gray, (0, row), (gray.shape[1]-1, row), (0, 0, 0), 1)

cont = gray.copy()
graycpy = gray.copy()
# after contour processing, the residual will contain small contours
residual = gray.copy()
# find contours
contours, hierarchy = cv2.findContours(cont, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
for i in range(len(contours)):
    # find the boundingbox of the contour
    x, y, w, h = cv2.boundingRect(contours[i])
    if 10 < h:
        cv2.drawContours(im, contours, i, (0, 255, 0), -1)
        # if boundingbox height is higher than threshold, remove the contour from residual image
        cv2.drawContours(residual, contours, i, (0, 0, 0), -1)
    else:
        cv2.drawContours(im, contours, i, (255, 0, 0), -1)
        # if boundingbox height is less than or equal to threshold, remove the contour gray image
        cv2.drawContours(gray, contours, i, (0, 0, 0), -1)

# now the residual only contains small contours. open it to remove thin lines
st = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
residual = cv2.morphologyEx(residual, cv2.MORPH_OPEN, st, iterations=1)
# prepare a mask for residual components
__, residual = cv2.threshold(residual, 0, 255, cv2.THRESH_BINARY)

cv2.imshow("gray", gray)
cv2.imshow("residual", residual)   

# combine the residuals. we still need to link the residuals
combined = cv2.bitwise_or(cv2.bitwise_and(graycpy, residual), gray)
# link the residuals
st = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (1, 7))
linked = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, st, iterations=1)
cv2.imshow("linked", linked)
# prepare a msak from linked image
__, mask = cv2.threshold(linked, 0, 255, cv2.THRESH_BINARY)
# copy region from low-noise underlined image
clean = 255 - cv2.bitwise_and(grayu, mask)
cv2.imshow("clean", clean)
cv2.imshow("im", im)
9
dhanushka

On peut essayer ça.

img = cv2.imread('img_provided_by_op.jpg', 0)
img = cv2.bitwise_not(img)  

# (1) clean up noises
kernel_clean = np.ones((2,2),np.uint8)
cleaned = cv2.erode(img, kernel_clean, iterations=1)

# (2) Extract lines
kernel_line = np.ones((1, 5), np.uint8)  
clean_lines = cv2.erode(cleaned, kernel_line, iterations=6)
clean_lines = cv2.dilate(clean_lines, kernel_line, iterations=6)

# (3) Subtract lines
cleaned_img_without_lines = cleaned - clean_lines
cleaned_img_without_lines = cv2.bitwise_not(cleaned_img_without_lines)

plt.imshow(cleaned_img_without_lines)
plt.show()
cv2.imwrite('img_wanted.jpg', cleaned_img_without_lines)

Démo

enter image description here

La méthode est basée sur le réponse de Zaw Lin. Il/elle a identifié des lignes dans l'image et a juste fait une soustraction pour les éliminer. Cependant , nous ne pouvons pas simplement soustraire des lignes ici parce que nous avons des lettres e , t , [~ # ~] e [~ # ~] , [~ # ~] t [~ # ~] , - contenant lignes aussi! Si nous soustrayons simplement les lignes horizontales de l'image, e sera presque identique à c . - aura disparu ...

Q: Comment trouve-t-on les lignes?

Pour trouver des lignes, nous pouvons utiliser la fonction erode. Pour utiliser erode, nous devons définir un noyau. (Vous pouvez considérer un noyau comme une fenêtre/forme sur laquelle les fonctions fonctionnent.)

Le noyau glisse à travers l'image (comme dans la convolution 2D). Un pixel dans l'image d'origine (1 ou 0) sera considéré comme 1 uniquement si tous les pixels sous le noyau sont 1, sinon il est érodé (mis à zéro). - (Source).

Pour extraire des lignes, nous définissons un noyau, kernel_line Comme np.ones((1, 5)), [1, 1, 1, 1, 1]. Ce noyau glissera à travers l'image et érodera les pixels qui ont 0 sous le noyau.

Plus précisément, alors que le noyau est appliqué à un pixel, il capturera les deux pixels à sa gauche et deux à sa droite.

 [X X Y X X]
      ^
      |
Applied to Y, `kernel_line` captures Y's neighbors. If any of them is not
0, Y will be set to 0.

Les lignes horizontales seront conservées sous ce noyau tandis que les pixels qui n'ont pas de voisins horizontaux disparaîtront. C'est ainsi que nous capturons des lignes avec la ligne suivante.

clean_lines = cv2.erode(cleaned, kernel_line, iterations=6)

Q: Comment éviter d'extraire des lignes dans e, E, t, T et -?

Nous combinerons erosion et dilation avec itération paramètre.

clean_lines = cv2.erode(cleaned, kernel_line, iterations=6)

Vous avez peut-être remarqué la partie iterations=6. L'effet de ce paramètre fera disparaître la partie plate dans e, E, t, T, - . En effet, alors que nous appliquons la même opération plusieurs fois, la partie limite de ces lignes rétrécit. (En appliquant le même noyau, seule la partie frontière rencontrera 0 et deviendra 0 comme résultat.) Nous utilisons cette astuce pour faire disparaître les lignes de ces caractères.

Cependant, cela a un effet secondaire dont la longue partie de soulignement dont nous voulons nous débarrasser se rétrécit également. Nous pouvons le développer avec dilate!

clean_lines = cv2.dilate(clean_lines, kernel_line, iterations=6)

Contrairement à l'érosion qui rétrécit une image, la dilatation agrandit l'image. Alors que nous avons toujours le même noyau, kernel_line, Si une partie sous le noyau est 1, le pixel cible sera 1. En appliquant cela, la frontière repoussera. (La partie dans e, E, t, T, - ne repoussera pas si nous choisissons soigneusement le paramètre de sorte qu'il disparaisse à la partie d'érosion .)

Avec cette astuce supplémentaire, nous pouvons réussir à nous débarrasser des lignes sans blesser e, E, t, T et -.


8
Tai

Quelques suggestions:

  • Étant donné que vous commencez avec un JPEG, ne compliquez pas la perte. Enregistrez vos fichiers intermédiaires au format PNG. Tesseract s'en sort très bien.
  • Redimensionnez l'image 2x (en utilisant cv2.resize) remise à Tesseract.
  • Essayez de détecter et de supprimer le soulignement noir. ( Cette question pourrait aider). Faire cela tout en préservant les descendants peut être difficile.
  • Explorez les options de ligne de commande de Tesseract, dont il existe de nombreuses (et elles sont horriblement documentées, certaines nécessitant des plongées dans la source C++ pour essayer de les comprendre). On dirait que les ligatures causent du chagrin. IIRC (ça fait longtemps), il y a un ou deux paramètres qui pourraient aider.
3
Dave W. Smith

Comme la plupart des lignes à détecter dans votre source sont des lignes longues horizontales, similaires à ma autre réponse, c'est-à-dire Trouver une seule couleur, des espaces horizontaux dans l'image

Voici l'image source:

Voici mes deux étapes principales pour supprimer la longue ligne horizontale:

  1. Faire morph-close avec noyau longue ligne sur l'image grise
kernel = np.ones((1,40), np.uint8)
morphed = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)

puis, obtenez l'image morphée contenant les longues lignes:

enter image description here

  1. Inversez l'image transformée et ajoutez-la à l'image source:
dst = cv2.add(gray, (255-morphed))

puis obtenez l'image avec les longues lignes supprimées:

enter image description here


Assez simple, non? Et il existe aussi small line segments, Je pense que cela a peu d'effets sur l'OCR. Remarquez que presque tous les caractères restent d'origine, sauf g, j, p, q, y, Q , peut-être un peu différent. Mais les outils OCR modernes tels que Tesseract (avec la technologie LSTM) ont la capacité de gérer une confusion aussi simple.

0123456789abcdef g hi j klmno pq rstuvwx y zABCDEFGHIJKLMNOP [~ # ~] q [~ # ~ ] RSTUVWXYZ


Code total pour enregistrer l'image supprimée en tant que line_removed.png:

#!/usr/bin/python3
# 2018.01.21 16:33:42 CST

import cv2
import numpy as np

## Read
img = cv2.imread("img04.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

## (1) Create long line kernel, and do morph-close-op
kernel = np.ones((1,40), np.uint8)
morphed = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
cv2.imwrite("line_detected.png", morphed)


## (2) Invert the morphed image, and add to the source image:
dst = cv2.add(gray, (255-morphed))
cv2.imwrite("line_removed.png", dst)

Mise à jour @ 2018.01.23 13:15:15 CST:

Tesseract est un outil puissant pour effectuer l'OCR. Aujourd'hui, j'installe le tesseract-4.0 et le pytesseract. Ensuite, je fais ocr en utilisant pytesseract sur mon résultat line_removed.png.

line_removed.png

import cv2       
import pytesseract
img = cv2.imread("line_removed.png")
print(pytesseract.image_to_string(img, lang="eng"))

Ceci est le reuslt, très bien pour moi.

Convicted as the triggerman in the murder—for—hire of 29—year—old .

shot once in the head with a 357 Magnum revolver in the garage of her home at ..
she stepped from her car. Police discovered that the victim‘s husband,
brother—in—law, _ ______ paid _ $2,000 to kill her, apparently so .. _
collect on life insurance policies totaling $250,000. Before the killing, .

applied for additional life insurance policies of $150,000 each on himself and his wife
to the scheme in three different statements to police.

was

and
could
had also

. confessed
3
Kinght 金