python - tutorial - Eliminar los subrayados horizontales
tesseract ocr tutorial (4)
Estoy intentando extraer texto de unos pocos cientos de JPG que contienen información sobre los registros de la pena capital; los JPG son recibidos por el Departamento de Justicia Criminal de Texas (TDCJ). A continuación se muestra un fragmento de ejemplo con información de identificación personal eliminada.
He identificado los subrayados como el impedimento para un OCR adecuado. Si entro, captura una imagen de un sub-snippet y líneas en blanco, el OCR resultante a través de pytesseract es muy bueno. Pero con los subrayados presentes, es extremadamente pobre.
¿Cómo puedo eliminar mejor estas líneas horizontales? Lo que he intentado:
- Comenzó en el tutorial de OpenCV doc: extraiga líneas horizontales y verticales utilizando operaciones morfológicas . Quedé atascado bastante rápido, porque sé cero C ++.
- Se siguió junto con Eliminación de líneas horizontales en la imagen ; terminó con una cadena ilegible.
- Siguiendo con la eliminación de líneas horizontales / verticales largas de la imagen de borde usando OpenCV , no fue capaz de obtener la intuición detrás del tamaño de la matriz de ceros aquí.
Etiquetando esta pregunta con c ++ con la esperanza de que alguien pueda ayudar a traducir el Paso 5 de la documentación a Python. He intentado un lote de transformaciones como Hugh Line Transform, pero me siento en la oscuridad dentro de una biblioteca y un área con la que no tengo experiencia previa.
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
Algunas sugerencias:
- Dado que está comenzando con un JPEG, no componga la pérdida. Guarda tus archivos intermedios como PNGs. Tesseract hace frente a los que están bien.
-
cv2.resize
la imagen 2x (usandocv2.resize
) entregando a Tesseract. - Intenta detectar y eliminar el subrayado negro. ( Esta pregunta puede ayudar). Hacer eso mientras preservar a los descendientes puede ser complicado.
- Explore las opciones de línea de comandos de Tesseract, de las cuales hay muchas (y están horriblemente documentadas, algunas requieren inmersiones en la fuente de C ++ para tratar de entenderlas). Parece que las ligaduras están causando algo de dolor. IIRC (ha pasado un tiempo), hay una configuración o dos que pueden ayudar.
Como la mayoría de las líneas a ser detectadas en su fuente son líneas horizontales, similares a mi otra respuesta, es encontrar un solo color, espacios horizontales en la imagen
Esta es la imagen de origen:
Aquí están mis dos pasos principales para eliminar la línea horizontal larga:
- Haga morph-close con el kernel de línea larga en la imagen gris
kernel = np.ones((1,40), np.uint8)
morphed = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
luego, obtener la imagen transformada contiene las líneas largas:
- Invierta la imagen transformada y agregue a la imagen de origen:
dst = cv2.add(gray, (255-morphed))
luego obtener la imagen con largas líneas eliminadas:
Bastante simple, ¿verdad? Y también existen small line segments
, creo que tiene pocos efectos en OCR. Note, casi todos los caracteres se mantienen originales, excepto g
, j
, p
, q
, y
, Q
, tal vez un poco diferente. Pero las herramientas de OCR más modernas, como Tesseract
(con tecnología LSTM
) tienen la capacidad de lidiar con una confusión tan simple.
0123456789abcdef g hi j klmno pq rstuvwx y zABCDEFGHIJKLMNOP Q RSTUVWXYZ
Código total para guardar la imagen eliminada como 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)
Actualización @ 2018.01.23 13:15:15 CST:
Tesseract es una poderosa herramienta para hacer OCR. Hoy instalo el tesseract-4.0 y pytesseract. Luego hago ocr usando pytesseract
en mi resultado line_removed.png
.
import cv2
import pytesseract
img = cv2.imread("line_removed.png")
print(pytesseract.image_to_string(img, lang="eng"))
Este es el reuslt, bien para mí.
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
Todas las respuestas hasta ahora parecen estar utilizando operaciones morfológicas. Aquí hay algo un poco diferente. Esto debería dar resultados bastante buenos si las líneas son horizontales .
Para esto uso una parte de la imagen de muestra que se muestra a continuación.
Cargue la imagen, conviértala a escala de grises e inviértala.
import cv2
import numpy as np
import matplotlib.pyplot as plt
im = cv2.imread(''sample.jpg'')
gray = 255 - cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
Imagen en escala de grises invertida:
Si escanea una fila en esta imagen invertida, verá que su perfil se ve diferente según la presencia o la ausencia de una línea.
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])
El perfil en verde es una fila donde no hay subrayado, el rojo es para una fila con subrayado. Si tomas el promedio de cada perfil, verás que el rojo tiene un promedio más alto.
Entonces, utilizando este enfoque puedes detectar los subrayados y eliminarlos.
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)
Aquí están los subrayados detectados en rojo, y la imagen limpia.
Salida de tesseract de la imagen limpiada:
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 razón para usar parte de la imagen ya debe estar clara. Dado que la información de identificación personal se ha eliminado en la imagen original, el umbral no habría funcionado. Pero esto no debería ser un problema cuando se aplica para el procesamiento. A veces es posible que tenga que ajustar los umbrales (16, 0.9).
El resultado no se ve muy bien con partes de las letras eliminadas y algunas de las líneas débiles aún permanecen. Se actualizará si puedo mejorarlo un poco más.
ACTUALIZAR:
Dis algunas mejoras; Limpie y vincule las partes faltantes de las letras. He comentado el código, así que creo que el proceso es claro. También puede comprobar las imágenes intermedias resultantes para ver cómo funciona. Los resultados son un poco mejores
Salida de tesseract de la imagen limpiada:
Convicted as th(
shot once in the
she stepped fr<
brother-in-law. ‘
collect on life ix
applied for man
to the scheme i|
Salida de tesseract de la imagen limpiada:
)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.
código 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)
Uno puede intentar esto.
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)
Manifestación
El método se basa en la respuesta de Zaw Lin. Él / ella identificó líneas en la imagen y simplemente hizo una resta para deshacerse de ellas. Sin embargo , no podemos simplemente restar líneas aquí porque también tenemos las letras e , t , E , T , ¡que también contienen líneas! Si solo restamos líneas horizontales de la imagen, e será casi idéntico a c . - se habrá ido ...
P: ¿Cómo encontramos las líneas?
Para encontrar líneas, podemos hacer uso de la función de erode
. Para hacer uso de erode
, necesitamos definir un kernel. (Puedes pensar en un kernel como una ventana / forma en la que funcionan las funciones).
El kernel se desliza a través de la imagen (como en la convolución 2D). Un píxel en la imagen original (1 o 0) se considerará 1 solo si todos los píxeles debajo del kernel son 1, de lo contrario se erosionará (se pondrá a cero). - (Source).
Para extraer líneas, definimos un kernel, kernel_line
como np.ones((1, 5))
, [1, 1, 1, 1, 1]
. Este kernel se deslizará a través de la imagen y erosionará los píxeles que tienen 0 debajo del kernel.
Más específicamente, mientras que el núcleo se aplica a un píxel, capturará los dos píxeles a su izquierda y los dos a su derecha.
[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.
Las líneas horizontales se conservarán bajo este núcleo, mientras que los píxeles que no tienen vecinos horizontales desaparecerán. Así es como capturamos líneas con la siguiente línea.
clean_lines = cv2.erode(cleaned, kernel_line, iterations=6)
P: ¿Cómo evitamos extraer líneas dentro de e, E, t, T y -?
Combinaremos la erosion
y la dilation
con el parámetro de iteración .
clean_lines = cv2.erode(cleaned, kernel_line, iterations=6)
Es posible que hayas notado las iterations=6
parte. El efecto de este parámetro hará que la parte plana en e, E, t, T, - desaparezca. Esto se debe a que, si bien aplicamos la misma operación varias veces, la parte del límite de estas líneas se reduciría. (Aplicando el mismo kernel, solo la parte del límite se encontrará con 0 y se convertirá en 0 como resultado). Usamos este truco para hacer que las líneas en estos caracteres desaparezcan.
Esto, sin embargo, viene con un efecto secundario que la parte de subrayado larga que queremos eliminar también se reduce. ¡Podemos cultivarlo con dilate
!
clean_lines = cv2.dilate(clean_lines, kernel_line, iterations=6)
Contrariamente a la erosión que encoge una imagen, la dilatación hace que la imagen sea más grande. Si bien aún tenemos el mismo kernel, kernel_line
, si alguna parte debajo del kernel es 1, el píxel objetivo será 1. Aplicando esto, el límite volverá a crecer. (La parte en e, E, t, T, - no volverá a crecer si seleccionamos el parámetro con cuidado para que desaparezca en la parte de erosión).
Con este truco adicional, podemos eliminar con éxito las líneas sin dañar e, E, t, T y -.