Оглавление: Уроки компьютерного зрения. Оглавление / Хабр (habr.com)

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

  • Предобработка изображения.

  • Промежуточная фильтрация.

  • Выявления специальных признаков (фич).

  • Высокоуровневый анализ.

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

Естественно, делать конвейер ради самого конвейера как-то бессмысленно. Надо, чтобы моя программа делала хоть что-то условно полезное. Сначала я хотел написать пэт-проект, который бы анализировал фотографии со спутников и БЛА и превращал их в граф (это перекликается с темой моей магистерской диссертации). Правда, это слишком уж амбициозная задумка для пэт-проекта. Надо что-то по- проще. В комментариях к одному из уроков мне посоветовали добавить в финале пару глав про выделение отдельных символов и распознавание их при помощи общедоступных нейронок. И вот я и подумал, может, начать пэт-проект именно с этой задачи? Распознавание текстов? Это гораздо проще.

Итак, для начала я создал пустой проект и добавил туда две папки: Exec и Libraries. В первой у меня будет запускаемый файл/файлы, во втором всякие библиотечные файлы. В качестве первого библиотечного файла создал Core.py:

class Engine:
    """"Движок"""
    pass

class ImageProcessingStep:
    """Шаг обработки изображений"""
    pass

class ImageInfo:
    """Содержимое картинки, включая результаты обработки"""
    pass

Может, не совсем удачно разместить эти в Core.py, но пока нет видимости, какова будет окончательная структура, а потом просто сделаю рефакторинг.

Итак, начнем программирование:

class Engine:
    """"Движок"""

    def __init__(self):
        """Конструктор"""

        self.steps=[]

    def process(self, image):
        """Выполнить обработку.
        image - изображение"""

        current_info=ImageInfo(image)
        for step in self.steps:
            current_info=step.process(current_info)
        return current_info

Из этого фрагмента кода видно, что нужно создать конструктор класса ImageInfo, а у класса ImageProcessingStep надо предусмотреть метод process. И, разумеется, ImageProcessingStep должен быть абстрактным классом, а реализовывать методы будет уже каждый конкретный класс. Стоить заметить, что в Python абстрактных классов нет как таковых (в отличии от C#), они эмулируются при помощи декораторов. Но я этого делать не буду, так как использование декораторов приводит к излишнему расходу процессорного времени. Просто представлю, что раз «нет» значит «нет».

Абстрактный класс я сделаю путем добавления псевдоабстрактных методов, помеченных pass:

class ImageProcessingStep:
    """Шаг обработки изображений"""

    def process(self,info):
        """Выполнить обработку"""
        pass

Теперь займемся классом ImageInfo. По идее, это тоже должны быть псевдоабстрактный класс, в зависимости от шага, должен содержать, кроме картинки, еще какие-то дополнительные данные. А пока реализую конструктор:

class ImageInfo:
    """Содержимое картинки, включая результаты обработки"""

    def __init__(self,image):
        """Конструктор"""

        self.image=image

Ну а теперь попробую «натянуть» на эту схему какое-нибудь простейшее действие, ну, например, фильтрацию. Но для начала небольшой рефакторинг, в частности, перенесем класс ImageProcessingStep в файл перенес в ImageProcessingSteps.

Создадим класс, отвечающий за медианную фильтрацию:

class MedianBlurProcessingStep(ImageProcessingStep):
    """Шаг, отвечающий за предобработку типа Медианная фильтрация"""

    def __init__(self,ksize):
        """Конструктор
        ksize - размер ядра фильтра"""

        self.ksize=ksize

    def process(self,info):
        """Выполнить обработку"""

        median_image = cv2.medianBlur(info.image, self.ksize)
        info.filtered_image=median_image
        return info

Далее, создаем папку Exec, а в ней файл run.py. Вот так теперь будет выглядеть структура папок:

Ну и само содержимое файла run.py:

import cv2

from Libraries.Core import Engine
from Libraries.ImageProcessingSteps import MedianBlurProcessingStep

my_photo = cv2.imread('../Photos/MyPhoto1.jpg')
core=Engine()
core.steps.append(MedianBlurProcessingStep(5))
info=core.process(my_photo)

cv2.imshow('origin', info.image) # выводим исходное изображение в окно
cv2.imshow('res', info.filtered_image) # выводим итоговое изображение в окно

cv2.waitKey()
cv2.destroyAllWindows()

Запускаем программу и видим две картинки, одну с царапинами, другую после медианной фильтрации. Да, это уже было на уроке 3, только на этот раз все сделано в соответствии с парадигмой ООП. Теперь пора идти дальше и реализовывать идею распознаванием текста. Для начала попробуем стандартным способом, через tesseract. Прежде всего, я расскажу, как вообще работать с tesseract. И так, ставим библиотеку:

pip install pytesseract

Заходим по этой ссылке: Home · UB-Mannheim/tesseract Wiki (github.com) и качаем оттуда саму tesseract. Если внезапно окажется, что ссылка не актуальная (мало ли, все течет, все меняется), тогда просто погуглите.

После того, как скачали tesseract просто устанавливаем его. В программе на Python нужно будет указать путь к программе, примерно вот так вот это будет выглядеть:

import cv2
import pytesseract

pytesseract.pytesseract.tesseract_cmd = "D:\\Program Files\\Tesseract-OCR\\tesseract.exe"

img = cv2.imread('image.jpg')
print(pytesseract.image_to_string(img, lang = 'rus'))

А вот демонстрация результатов работы программы вместе с исходным изображением:

Как видим, идеальный текст (скриншот ворда) распознался отлично. Теперь надо будет исследовать, как библиотека будет работать на реальных текстах (может, и не надо никакого своего распознавателя символов изобретать).

Попробуем распознать сфотографированную страницу книги:

Вот что мы получим на выходе:

Как видим, где-то слова распознались хорошо, где-то довольно криво.

И еще один, немаловажный нюанс. На выходе сплошной текст. Но в реальности может потребоваться делить текст на какие-то блоки. Например, я однажды писал программу, которая позволяет распознавать текст на чертежах, чтобы потом этот чертеж оцифровать и занести в базу данных. В этом случае требовалось отдельно найти и распознать штампик чертежа, отдельно надписи на выносках, чтобы все эти надписи занести в нужные поля базы данных. Как данная задача решалась практически? При помощи алгоритмов компьютерного зрения изображение делилось на зоны, и эти зоны уже отдельно скармливались tesseract.

Но в моем пэт-проекте я попробую распознавать автомобильные номера. Знаю, это уже вообще баянестый баян, но, тем не менее, студенты до сих пор пишут лабораторные на эту тему. И даже целые курсовые. Да и вообще, надо же с чего-то начинать. А задача это довольно простая для того, чтобы создать некую основу для более сложного проекта.

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

[Python] Изучение OpenCV | Распознавание автомобильных номеров смотреть онлайн видео от Boris Bochkarev | BeTry в хорошем качестве. (rutube.ru)

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

Автор рассказывает все по шагам. Приведу эти шаги. Сначала он предлагает преобразовать текст в оттенки серого и произвести бинаризацию:

import cv2

img = cv2.imread('cars/6108249.jpg')
height,width,_=img.shape
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray,0,255,cv2.THRESH_OTSU)[1]
cv2.imshow('MyPhoto', thresh)
cv2.waitKey(0)

cv2.destroyAllWindows()

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

Результат бинаризации:

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

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

В принципе, неплохо, хотя местами есть помехи:

А в последнем случае номерной знак теряется на белом фоне. Хотя, если приглядеться внимательно, вокруг него есть черный контур:

Так что, возможно, алгоритм и найдет прямоугольник, посмотрим, что автор предложит дальше, подтвердиться ли моя догадка.

Оказалось, парень предложил просто выделить контуры, и исследовать каждый прямоугольник контура с достаточно большой площадью. В общем, что-то наподобие того:

import cv2
from imutils import contours
import pytesseract

pytesseract.pytesseract.tesseract_cmd = "D:\\Program Files\\Tesseract-OCR\\tesseract.exe"

img = cv2.imread('cars/6108249.jpg')
height,width,_=img.shape
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray,0,255,cv2.THRESH_OTSU)[1]
cnts=cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cnts,_=contours.sort_contours(cnts[0])

for c in cnts:
    area=cv2.contourArea(c)
    x,y,w,h=cv2.boundingRect(c)
    if area>20:
        img_area=img[y:y+h,x:x+h]
        result=pytesseract.image_to_string(img_area,lang="rus+eng")
        if len(result)>3:
            print(result)

У него, правда, были немного другие пороги. И все работало, он прямо показал в ролике, все работает. На моих фотографиях это не сработало, поэтому я и заменил пороги. И получил странный вывод:

Можно добавить просмотр картинок, чтобы увидеть, где tesseract находит эти надписи:

…
print(result)
cv2.imshow('MyPhoto', img_area)
cv2.waitKey(0)
…

Смотрим фрагменты картинки:

В общем, цифры tesseract почему то не хочет идентифицировать. А он вообще, понимает цифры? Давайте проверим. Создадим в paint-е картинку из цифр:

Скармливаем tesseract. И видим, что вывод пуст.  Погуглил данную тему. Как оказалось, tesseract работает с цифрами через одно место. Можно поискать какие-то другие версия, но это не вариант. Лучше написать какую-то свою распознавалку, например, с использованием нейросетей.

Но, даже в этом случае встает вопрос, а каким же образом найти местоположение автомобильного номера? Может быть, искать белый прямоугольник на бинарной карте, как я предположил вначале? В любом случае, необходимо сначала вставить наработки в уже написанный ООП-контейнер, чтобы потом анализировать и исследовать.

И тут я понял, что вот такой вот код:

def process(self, image):
    """Выполнить обработку.
    image - изображение"""

    current_info=ImageInfo(image)
    for step in self.steps:
        current_info=step.process(current_info)
    return current_info

...
...
  
def process(self,info):
    """Выполнить обработку"""

    median_image = cv2.medianBlur(info.image, self.ksize)
    info.filtered_image=median_image
    return info

Это плохая идея. Почему? Допустим, я решил сделать бинаризацию картинки. Я могу сделать бинаризацию исходной картинки, а могу после фильтрации. Но после фильтрации она называется не image, а filtered_image. И спрашивается, откуда на шаге бинаризации можно знать, надо обрабатывать image или filtered_image? Поэтому, они должны называется одинаково. А вот историю обработки надо где-то сохранять, чтобы потом восстановить цепочку обработок и проанализировать, как работает вся система.

Для начала исправим метод process класса Engine:

def process(self, image):
    """Выполнить обработку.
    image - изображение"""

    current_info=ImageInfo(image)
    history=[]
    for step in self.steps:
        history.append(current_info)
        current_info=step.process(current_info)
    return current_info, history

Затем файл: ImageProcessingSteps.py

import cv2
from Libraries.Core import ImageInfo


class ImageProcessingStep:
    """Шаг обработки изображений"""

    def process(self,info):
        """Выполнить обработку"""
        pass

class MedianBlurProcessingStep(ImageProcessingStep):
    """Шаг, отвечающий за предобработку типа Медианная фильтрация"""

    def __init__(self,ksize):
        """Конструктор
        ksize - размер ядра фильтра"""

        self.ksize=ksize

    def process(self,info):
        """Выполнить обработку"""

        median_image = cv2.medianBlur(info.image, self.ksize)
        return ImageInfo(median_image)

Ну, и наконец run.py:

import cv2

from Libraries.Core import Engine
from Libraries.ImageProcessingSteps import MedianBlurProcessingStep

my_photo = cv2.imread('../Photos/MyPhoto1.jpg')
core=Engine()
core.steps.append(MedianBlurProcessingStep(5))
res,history=core.process(my_photo)

i=1
for info in history:
    cv2.imshow('image'+str(i), info.image) # выводим изображение в окно
    i=i+1
cv2.imshow('res', res.image)



cv2.waitKey()
cv2.destroyAllWindows()

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

class ThresholdProcessingStep(ImageProcessingStep):
    """Шаг, отвечающий за бинаризацию"""

    def process(self,info):
        """Выполнить обработку"""

        gray = cv2.cvtColor(info.image, cv2.COLOR_BGR2GRAY)
        image = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU)[1]
        return ImageInfo(image)

И новый запускаемый файл run1.py:

import cv2

from Libraries.Core import Engine
from Libraries.ImageProcessingSteps import MedianBlurProcessingStep, ThresholdProcessingStep

my_photo = cv2.imread('../Photos/car.jpg')
core=Engine()
core.steps.append(MedianBlurProcessingStep(5))
core.steps.append(ThresholdProcessingStep())
res,history=core.process(my_photo)

i=1
for info in history:
    cv2.imshow('image'+str(i), info.image) # выводим изображение в окно
    i=i+1
cv2.imshow('res', res.image)

cv2.waitKey()
cv2.destroyAllWindows()

При запуске мы увидим вот такую картинку:

Код этого примера можно взять отсюда: https://github.com/megabax/CVContainer

А теперь подытожим: Мы создали ООП-контейнер для обработки изображений, в будущих уроках прикрутим к нему находилку номера и распознавалку цифр на нейросетях.

Комментарии (0)