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

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

введите описание изображения здесь

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

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

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

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

person Brad Solomon    schedule 18.01.2018    source источник
comment
Посмотрим, поможет ли это? . Он написан на Java, но должен быть легко перенесен на Python.   -  person Tarun Lalwani    schedule 21.01.2018


Ответы (4)


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

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

образец

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

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

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

Инвертированное изображение в оттенках серого:

инвертированный серый

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

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  строка

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

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)

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

обнаружено  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)
person dhanushka    schedule 21.01.2018
comment
Вау, это впечатляет. Незначительный вопрос - разве img = cv2.imread('sample.jpg', cv2.IMREAD_GRAYSCALE) не делает то же самое, что cv2.imread('sample.jpg') + cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) за один шаг? - person Brad Solomon; 21.01.2018
comment
@BradSolomon Да, это так. Я прочитал изображение как RGB, чтобы нарисовать подчеркивание красным, как показано на изображении. - person dhanushka; 21.01.2018

Это можно попробовать.

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)

Демо

введите описание изображения здесь

Этот метод основан на ответе автора Zaw Lin. Он определил линии на изображении и просто выполнил вычитание, чтобы избавиться от них. Однако мы не можем просто вычесть здесь строки, потому что у нас есть буквы e, t, E, T , - также содержащие строки! Если мы просто вычтем из изображения горизонтальные линии, e будет почти идентичным c. - исчезнет ...

В: Как мы находим линии?

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

Ядро скользит по изображению (как при двухмерной свертке). Пиксель в исходном изображении (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's 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 / a> и dilation с параметром итерация.

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

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

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

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

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

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


person Tai    schedule 20.01.2018
comment
Что влияет на определение строк в kernel? (Здесь 5.) То есть, какие факторы влияют на его выбор, а не просто произвольное использование 5? - person Brad Solomon; 21.01.2018
comment
Пиксель в исходном изображении (либо 1, либо 0) будет считаться 1 только в том случае, если все пиксели под ядром равны 1, в противном случае он будет размыт (обнулен). Итак, здесь наше окно представляет собой столбец (по вертикали), любые пиксели, у которых нет вертикальных соседей, равных 1, будут размыты. @BradSolomon Смотрите мое обновление. - person Tai; 21.01.2018

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

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

Вот два моих основных шага по удалению длинной горизонтальной линии:

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

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

введите описание изображения здесь

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

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

введите описание изображения здесь


Достаточно просто, правда? Также существуют small line segments, я думаю, это мало влияет на распознавание текста. Обратите внимание, почти все символы сохраняют оригинал, кроме _4 _, _ 5 _, _ 6 _, _ 7 _, _ 8 _, _ 9_, возможно, немного отличаются. Но современные инструменты OCR, такие как TesseractLSTM технологией), способны справиться с такой простой путаницей.

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 CST:

Tesseract - мощный инструмент для распознавания текста. Сегодня я устанавливаю 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
person Kinght 金    schedule 21.01.2018

Несколько предложений:

  • Учитывая, что вы начинаете с JPEG, не усугубляйте потери. Сохраните промежуточные файлы как PNG. Tesseract прекрасно с этим справляется.
  • Масштабируйте изображение в 2 раза (используя cv2.resize), передавая Tesseract.
  • Попробуйте обнаружить и удалить черную линию подчеркивания. (Этот вопрос может помочь). Сделать это с сохранением спусковых элементов может быть непросто.
  • Изучите параметры командной строки Tesseract, которых много (и они ужасно документированы, некоторые из них требуют погружения в исходный код C ++, чтобы попытаться понять их). Похоже, что лигатуры вызывают какое-то горе. IIRC (это было давно), есть пара настроек, которые могут помочь.
person Dave W. Smith    schedule 18.01.2018
comment
Вкратце, что вы подразумеваете под сохранением нижних конечностей? - person Brad Solomon; 19.01.2018
comment
Я начал свой ответ, пока вы редактировали вопрос. Теперь яснее. Под «децендерами» я подразумеваю такие вещи, как нижняя часть буквы «g». Обратите внимание на подчеркивание под словом «триггер». - person Dave W. Smith; 19.01.2018
comment
... удаление подчеркивания может привести к тому, что 'gg' будет выглядеть как 'oo' (или, что еще хуже, что-то не так в пространстве Unicode). - person Dave W. Smith; 19.01.2018