Удаление горизонтальных подчеркиваний

Я пытаюсь вытащить текст из нескольких сотен JPG, содержащих информацию о записи о смертной казни; JPG организуются Департаментом уголовного правосудия штата Техас (TDCJ). Ниже приведен фрагмент примера с удаленной лично идентифицируемой информацией.

enter image description here

Я определил подчеркивания как препятствие для правильного OCR --if. Я вхожу, скриншот суб-фрагмента и ручных белых линий, получающееся OCR через pytesseract очень хорошее. Но с подчеркиваниями, он крайне беден.

Как я могу лучше всего удалить эти горизонтальные линии? То, что я пробовал:

Пометка этого вопроса с помощью в надежде, что кто-то сможет помочь перевести Шаг 5 в пошаговое руководство к Python. Я пробовал пакет преобразований, таких как Hugh Line Transform, но я чувствую себя в темноте в библиотеке и области, где у меня есть нулевой опыт.

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

Ответ 1

Кажется, что все ответы до сих пор используют морфологические операции. Здесь что-то другое. Это должно дать неплохие результаты, если линии горизонтальны.

Для этого я использую часть вашего образца изображения, показанного ниже.

sample

Загрузите изображение, преобразуйте его в серое и инвертируйте.

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

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

Изображение с инвертированной серой шкалой:

inverted-gray

Если вы сканируете строку в этом обратном изображении, вы увидите, что ее профиль выглядит по-разному в зависимости от наличия или отсутствия строки.

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])

Профиль в зеленом - это строка, в которой нет подчеркивания, красный для строки с подчеркиванием. Если вы возьмете среднее значение для каждого профиля, вы увидите, что красный цвет имеет более высокий средний показатель.

no-line line

Таким образом, используя этот подход, вы можете обнаружить подчеркивания и удалить их.

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)

Вот обнаруженные подчеркивания в красном и очищенное изображение.

detected cleaned

tesseract выход очищенного изображения:

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

Причиной использования части изображения должно быть ясно. Поскольку личная идентифицируемая информация была удалена в исходном изображении, порог не сработал бы. Но это не должно быть проблемой, когда вы применяете его для обработки. Иногда вам придется корректировать пороговые значения (16, 0,9).

Результат не очень хорош с удалением частей букв, а некоторые из слабых линий остаются. Будет обновление, если я могу улучшить его немного больше.

ОБНОВИТЬ:

Выполните некоторые улучшения; очистить и связать недостающие части букв. Я прокомментировал код, поэтому считаю, что процесс ясен. Вы также можете проверить полученные промежуточные изображения, чтобы увидеть, как это работает. Результаты немного лучше.

1 1-clean

tesseract выход очищенного изображения:

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

2 2-clean

tesseract выход очищенного изображения:

)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.

код 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)

Ответ 2

Можно попробовать это.

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)

демонстрация

enter image description here

Метод основан на ответе Зау Лин. Он/она идентифицировал линии на изображении и просто сделал вычитание, чтобы избавиться от них. Однако мы не можем просто вычитать строки здесь, потому что у нас есть буквы e, t, E, T, - содержащие строки! Если мы просто вычтем горизонтальные линии из изображения, e будет почти идентичным c. - уйдет...

В: Как найти строки?

Чтобы найти строки, мы можем использовать функцию erode. Чтобы использовать erode, нам нужно определить ядро. (Вы можете думать о ядре как о окне/форме, функции которого работают.)

Ядро скользит по изображению (как в 2D свертке). Пиксель в исходном изображении (1 или 0) будет считаться 1, только если все пиксели под ядром равны 1, в противном случае он будет разрушен (сделан до нуля). - (Источник).

Чтобы извлечь строки, мы определяем ядро, kernel_line как np.ones((1, 5)), [1, 1, 1, 1, 1]. Это ядро будет скользить по изображению и стирать пиксели с 0 под ядром.

Более конкретно, в то время как ядро применяется к одному пикселю, оно захватывает два пикселя слева и два справа.

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

Горизонтальные линии будут сохранены под этим ядром, в то время как пиксель, у которого нет горизонтальных соседей, исчезнет. Так мы фиксируем линии со следующей строкой.

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

В: Как избежать выделения строк внутри e, E, t, T и -?

Мы будем сочетать erosion и dilation с параметром итерации.

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

Возможно, вы заметили iterations=6 часть. Эффект этого параметра сделает плоскую часть в e, E, t, T, - исчезнет. Это связано с тем, что, хотя мы применяем одну и ту же операцию несколько раз, граничная часть этих строк будет уменьшаться. (Применяя одно и то же ядро, только граничная часть встретится с 0 и станет 0 в результате.) Мы используем этот трюк, чтобы линии в этих символах исчезли.

Это, однако, связано с побочным эффектом, который сокращает длинную часть подчеркивания, которую мы хотим избавиться. Мы можем вырастить его с dilate !

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

В отличие от эрозии, которая сжимает изображение, дилатация делает изображение более крупным. Хотя у нас все еще есть одно ядро kernel_line, если какая-либо часть под ядром равна 1, целевой пиксель будет равен 1. Применяя это, граница будет расти. (Часть в e, E, t, T, - не будет расти, если мы тщательно выберем параметр, чтобы он исчез в эрозионной части.)

С помощью этого дополнительного трюка мы можем успешно избавиться от линий, не повредив e, E, t, T и -.


Ответ 3

Несколько советов:

  • Учитывая, что вы начинаете с JPEG, не суммируйте потери. Сохраните промежуточные файлы в виде PNG. Tesseract справляется с ними просто отлично.
  • cv2.resize изображение 2x (используя cv2.resize), передав Tesseract.
  • Попробуйте обнаружить и удалить черное подчеркивание. (Этот вопрос может помочь). Выполнение этого при сохранении descenders может быть сложным.
  • Исследуйте параметры командной строки Tesseract, которых много (и они ужасно документированы, некоторые требуют погружений в источник C++, чтобы попытаться понять их). Это похоже на лигатуры, вызывая некоторое горе. IIRC (это было время), там могут быть настройки или два, которые могут помочь.

Ответ 4

Поскольку большинство линий, которые будут обнаружены в вашем источнике, являются горизонтальными длинными линиями, аналогичными моему другому ответу, то есть поиск одиночного цвета, горизонтальные пробелы в изображении

Это исходное изображение:

Вот мои два основных шага, чтобы удалить длинную горизонтальную линию:

  1. Сделайте морфинг с ядром длинной строки на сером изображении
kernel = np.ones((1,40), np.uint8)
morphed = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)

то получится, что морфированное изображение содержит длинные строки:

enter image description here

  1. Инвертируйте измененное изображение и добавьте исходное изображение:
dst = cv2.add(gray, (255-morphed))

затем получить изображение с удаленными длинными линиями:

enter image description here


Достаточно просто, не так ли? А также есть small line segments, я думаю, что это мало влияет на OCR. Обратите внимание, что почти все символы сохраняются в оригинале, кроме g, j, p, q, y, Q, возможно, немного различны. Но такие инструменты OCR, как Tesseract (с технологией LSTM), обладают способностью справляться с такой простой путаницей.

0123456789abcdef g hi j klmno pq rstuvwx y zABCDEFGHIJKLMNOP Q RSTUVWXYZ


Общий код для сохранения удалённого изображения как 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)

Обновление @2018.01.23 13:15:15 КНТ:

Tesseract - мощный инструмент для создания OCR. Сегодня я устанавливаю tesseract-4.0 и pytesseract. Затем я делаю ocr, используя pytesseract по моему результату line_removed.png.

line_removed.png

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

Это повторение, отлично для меня.

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