Imagen de limpieza para OCR

9

He estado tratando de borrar imágenes para OCR: (las líneas)

ingrese la descripción de la imagen aquí

Necesito eliminar estas líneas para a veces procesar aún más la imagen y me estoy acercando bastante, pero muchas veces el umbral quita demasiado del texto:

    copy = img.copy()
    blur = cv2.GaussianBlur(copy, (9,9), 0)
    thresh = cv2.adaptiveThreshold(blur,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV,11,30)

    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9,9))
    dilate = cv2.dilate(thresh, kernel, iterations=2)

    cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cnts = cnts[0] if len(cnts) == 2 else cnts[1]

    for c in cnts:
        area = cv2.contourArea(c)
        if area > 300:
            x,y,w,h = cv2.boundingRect(c)
            cv2.rectangle(copy, (x, y), (x + w, y + h), (36,255,12), 3)

Editar: Además, el uso de números constantes no funcionará en caso de que cambie la fuente. ¿Hay una forma genérica de hacer esto?

K41F4r
fuente
2
Algunas de estas líneas, o fragmentos de ellas, tienen las mismas características que el texto legal, y será difícil deshacerse de ellas sin estropear el texto válido. Si esto aplica, puede enfocarse en los hechos de que son más largos que los personajes y algo aislados. Entonces, un primer paso podría ser estimar el tamaño y la cercanía de los personajes.
Yves Daoust
@YvesDaoust ¿Cómo se podría encontrar la cercanía de los personajes? (dado que el filtrado puramente por tamaño se mezcla con los personajes muchas veces)
K41F4r
1
Podrías encontrar, para cada gota, la distancia a su vecino más cercano. Luego, mediante el análisis de histograma de las distancias, encontraría un umbral entre "cerrar" y "aparte" (algo así como el modo de distribución), o entre "rodeado" y "aislado".
Yves Daoust
En caso de varias líneas pequeñas cerca una de la otra, ¿su vecino más cercano no sería la otra línea pequeña? ¿Sería demasiado costoso calcular la distancia promedio a todos los demás blobs?
K41F4r
"¿no sería su vecino más cercano la otra pequeña línea?": buena objeción, su señoría. De hecho, un montón de segmentos cortos cercanos no difieren del texto legítimo, aunque en un arreglo completamente improbable. Puede que tenga que reagrupar los fragmentos de líneas discontinuas. No estoy seguro de que la distancia promedio a todos los rescataría.
Yves Daoust

Respuestas:

14

Aquí hay una idea. Dividimos este problema en varios pasos:

  1. Determinar el área de contorno rectangular promedio. Luego umbralamos los contornos y los filtramos usando área del rectángulo delimitador del contorno. La razón por la que hacemos esto es por la observación de que cualquier personaje típico solo será tan grande, mientras que el ruido grande abarcará un área rectangular más grande. Luego determinamos el área promedio.

  2. Eliminar grandes contornos atípicos. Repetimos los contornos nuevamente y eliminamos los contornos grandes si están5x más grandes que el área de contorno promedio rellenando el contorno. En lugar de usar un área de umbral fijo, usamos este umbral dinámico para una mayor solidez.

  3. Dilatar con un núcleo vertical para conectar personajes . La idea es aprovechar la observación de que los personajes están alineados en columnas. Al dilatar con un núcleo vertical conectamos el texto para que el ruido no se incluya en este contorno combinado.

  4. Eliminar pequeños ruidos . Ahora que el texto a guardar está conectado, encontramos contornos y eliminamos los contornos más pequeños que 4xel área de contorno promedio.

  5. A nivel de bits y para reconstruir la imagen . Como solo hemos deseado contornos para mantener nuestra máscara, lo hacemos a nivel de bits y para preservar el texto y obtener nuestro resultado.


Aquí hay una visualización del proceso:

Tenemos el umbral de Otsu para obtener una imagen binaria y luego encontramos contornos para determinar el área de contorno rectangular promedio. Desde aquí eliminamos los grandes contornos atípicos resaltados en verde rellenando contornos

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

A continuación, construimos un núcleo vertical y lo dilatamos para conectar los caracteres. Este paso conecta todo el texto deseado para mantener y aísla el ruido en blobs individuales.

ingrese la descripción de la imagen aquí

Ahora encontramos contornos y filtros usando el área de contorno para eliminar el pequeño ruido

ingrese la descripción de la imagen aquí

Aquí están todas las partículas de ruido eliminadas resaltadas en verde

ingrese la descripción de la imagen aquí

Resultado

ingrese la descripción de la imagen aquí

Código

import cv2

# Load image, grayscale, and Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# Determine average contour area
average_area = [] 
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    area = w * h
    average_area.append(area)

average = sum(average_area) / len(average_area)

# Remove large lines if contour area is 5x bigger then average contour area
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    area = w * h
    if area > average * 5:  
        cv2.drawContours(thresh, [c], -1, (0,0,0), -1)

# Dilate with vertical kernel to connect characters
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,5))
dilate = cv2.dilate(thresh, kernel, iterations=3)

# Remove small noise if contour area is smaller than 4x average
cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
    area = cv2.contourArea(c)
    if area < average * 4:
        cv2.drawContours(dilate, [c], -1, (0,0,0), -1)

# Bitwise mask with input image
result = cv2.bitwise_and(image, image, mask=dilate)
result[dilate==0] = (255,255,255)

cv2.imshow('result', result)
cv2.imshow('dilate', dilate)
cv2.imshow('thresh', thresh)
cv2.waitKey()

Nota: El procesamiento tradicional de imágenes se limita a los umbrales, las operaciones morfológicas y el filtrado de contornos (aproximación de contornos, área, relación de aspecto o detección de manchas). Dado que las imágenes de entrada pueden variar según el tamaño del texto de los caracteres, encontrar una solución singular es bastante difícil. Es posible que desee estudiar la capacitación de su propio clasificador con aprendizaje automático / profundo para una solución dinámica.

nathancy
fuente
1
En caso de una fuente más grande, ¿no eliminaría esto también el texto?
K41F4r
Sí, puede, por lo que tendría que ajustar el valor del área de umbral. Para un enfoque más dinámico, una idea es determinar el área de caracteres promedio y luego usarla como umbral
nathancy
Parece ser demasiado específico para el ejemplo, el uso del área promedio todavía eliminará el texto muchas veces, lo que empeora el resultado para OCR
K41F4r
¿Tiene otro ejemplo de imagen de entrada que podría agregar a la publicación?
nathancy
1
Encontrar una solución que funcione en todas las situaciones utilizando técnicas tradicionales de procesamiento de imágenes es bastante difícil. Es posible que desee estudiar la capacitación de su propio clasificador mediante el aprendizaje profundo. ¡Buena suerte!
nathancy