Aqui está uma ideia. Dividimos esse problema em várias etapas:
Determine a área retangular média do contorno. Limitamos, então, os contornos e filtramos usando a área retangular do contorno. A razão pela qual fazemos isso é por causa da observação de que qualquer caractere típico será apenas tão grande, enquanto um ruído grande abrangerá uma área retangular maior. Em seguida, determinamos a área média.
Remova contornos grandes e extremos. Repetimos os contornos e removemos os contornos grandes, se forem 5x
maiores que a área média do contorno, preenchendo o contorno. Em vez de usar uma área de limite fixo, usamos esse limite dinâmico para obter mais robustez.
Dilate com um kernel vertical para conectar caracteres . A idéia é aproveitar a observação de que os caracteres estão alinhados em colunas. Ao dilatar com um núcleo vertical, conectamos o texto para que o ruído não seja incluído nesse contorno combinado.
Remova pequenos ruídos . Agora que o texto a ser mantido está conectado, encontramos contornos e removemos os contornos menores que 4x
a área média do contorno.
Bit a bit - e para reconstruir a imagem . Como nós apenas desejamos contornos para manter nossa máscara, nós bit a bit - e para preservar o texto e obter nosso resultado.
Aqui está uma visualização do processo:
Nós limiar de Otsu para obter uma imagem binária, em seguida, encontrar contornos para determinar a área retangular média do contorno. A partir daqui, removemos os grandes contornos extremos destacados em verde preenchendo os contornos
Em seguida, construímos um núcleo vertical e dilatamos para conectar os caracteres. Esta etapa conecta todo o texto desejado para manter e isolar o ruído em blobs individuais.
Agora encontramos contornos e filtramos usando a área de contorno para remover o pequeno ruído
Aqui estão todas as partículas de ruído removidas destacadas em verde
Resultado
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: O processamento tradicional da imagem é limitado a operações de limiar, morfológicas e filtragem de contorno (aproximação de contorno, área, proporção ou detecção de blob). Como as imagens de entrada podem variar com base no tamanho do texto dos caracteres, é bastante difícil encontrar uma solução singular. Você pode querer treinar seu próprio classificador com aprendizado profundo / máquina para obter uma solução dinâmica.