OCR на коленке

Когда FineReader и Tesseract не подходят.
2016-02-15

FineReader и Tesseract решают сложную задачу OCR произвольного документа. Конечно, повторять их труд не имеет смысла. При сборе данных мы часто работаем не с произвольными документами, а со сканами определённого вида. Например, есть таблицы размеров от китайских производителей:

Исходное изображение

Нужно с точностью больше 85% извлекать из изображения данные вида:

{
    "m1": {
        "s": 36,
        "m": 37,
        "l": 38,
        "xl": 39,
        "xxl": 40,
    },
    "m2": {
        "s": 82,
        "m": 86,
        "l": 90,
        "xl": 94,
        "xxl": 98,
    },
    "m3": {
        "s": 68,
        "m": 72,
        "l": 76,
        "xl": 80,
        "xxl": 84,
    },

    ...
}

Просто запустить универсальный OCR не получится по трём причинам: иероглифы, таблица, нужно обработать содержание таблицы. Придётся делать специальное решение. С библиотекой OpenCV это не так сложно.

Сначала выделим участки с тексом. Для этого применим оператор Собеля и сделаем замыкание. Параметры аккуратно подбираются один раз, потому что сканы однотипные.

image = cv2.Sobel(
    image, cv2.CV_8U, 1, 0, ksize=3, scale=1, delta=0,
    borderType=cv2.BORDER_DEFAULT
)
_, image = cv2.threshold(
    image, 0, 255,
    cv2.THRESH_BINARY | cv2.THRESH_OTSU
)
element = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))
image = cv2.morphologyEx(image, cv2.MORPH_CLOSE, element)
contours, _ = cv2.findContours(
    image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
)
for contour in contours:
    x, y, width, height = cv2.boundingRect(contour)
    yield Rectangle(x, y, width, height)
Оператор Собеля
Замыкание
Области с текстами

Нашлось много лишнего: платье, логотип и разделители таблицы. Пока не обращаем на это внимание. Внутри областей с текстом ищем отдельные символы.

for rectangle in rectangles:
    x, y, width, height = rectangle
    roi = image[y:y + height, x:x + width]
    _, roi = cv2.threshold(
        roi, 0, 255,
        cv2.THRESH_BINARY | cv2.THRESH_OTSU
    )
    roi = 255 - roi
    contours, _ = cv2.findContours(
        roi, cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_NONE
    )
    for contour in contours:
        x_, y_, width, height = cv2.boundingRect(contour)
        yield Rectangle(x + x_, y + y_, width, height)
Отдельные символы

Соберём примеры нужных символов: цифры, некоторые буквы. Будем прикладывать их по очереди к каждой области с помощью функции cv2.matchTemplate. Мы можем так поступить, потому что шрифт и оформление сканов фиксированы.

Шаблоны
Классифицированные символы

Теперь уложим символы в таблицу. Для этого спроецируем оставшиеся области на оси X и Y. Через минимумы полученных гистограмм проведём перегородки.

Перегородки таблицы

Из-за лишних символов в таблице появились ненужные ячейки. Чтобы от них избавиться переберём все подтаблицы и выберем такие, у которых слева только размеры (S, M, L, XL), сверху только измерения, а внутри только числа. Среди них выберем самую большую:

Оптимальная подтаблица

Код и примеры доступны на Гитхабе.