Вот идея. Мы разбиваем эту проблему на несколько этапов:
Определить среднюю площадь прямоугольного контура. Затем мы пороговые значения, затем находим контуры и фильтруем, используя ограничивающую прямоугольную область контура. Причина, по которой мы это делаем, заключается в том, что любой типичный символ будет таким большим, тогда как большой шум будет охватывать большую прямоугольную область. Затем мы определяем среднюю площадь.
Удалить большие контуры выбросов. Мы повторяем контуры снова и удаляем большие контуры, если они 5x
больше, чем средняя площадь контура, заполняя контур. Вместо использования фиксированной области порога, мы используем этот динамический порог для большей устойчивости.
Расширьте вертикальное ядро, чтобы соединить символы . Идея состоит в том, чтобы воспользоваться наблюдением, что символы выровнены в столбцах. Расширяя вертикальное ядро, мы соединяем текст, чтобы шум не включался в этот комбинированный контур.
Удалить небольшой шум . Теперь, когда сохраняемый текст связан, мы находим контуры и удаляем все контуры, меньшие, чем 4x
средняя площадь контура.
Побитовый - и реконструировать образ . Поскольку у нас есть только желаемые контуры, чтобы сохранить нашу маску, мы побитовые - и сохранить текст и получить наш результат.
Вот визуализация процесса:
У порога Отсу для получения бинарного изображения затем находим контуры, чтобы определить среднюю площадь прямоугольного контура. Отсюда мы удаляем большие контуры выбросов, выделенные зеленым цветом, заполняя контуры
Далее мы строим вертикальное ядро и расширяем, чтобы соединить символы. Этот шаг соединяет весь необходимый текст, чтобы сохранить и изолирует шум в отдельные капли.
Теперь мы находим контуры и фильтруем, используя область контура, чтобы удалить небольшой шум
Здесь все удаленные шумовые частицы выделены зеленым
Результат
Код
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()
Примечание. Традиционная обработка изображений ограничивается установлением порога, морфологическими операциями и фильтрацией контуров (аппроксимация контура, площадь, соотношение сторон или обнаружение больших двоичных объектов). Поскольку входные изображения могут различаться в зависимости от размера символов, найти единственное решение довольно сложно. Возможно, вы захотите изучить свой собственный классификатор с помощью машинного / глубокого обучения для динамического решения.