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

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

Напомню, какие шаги были сделаны на прошлом уроке:

  • Применить медианную фильтрацию к изображению.

  • Провести бинаризацию.

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

class ContourProcessingStep(ImageProcessingStep):
    """Шаг, отвечающий за выделение контуров"""

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

        contours, hierarchy  = cv2.findContours(info.image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

        height, width = info.image.shape[:2]
        contours_image = np.zeros((height, width, 3), dtype=np.uint8)

        # отображаем контуры
        cv2.drawContours(contours_image, contours, -1, (255, 0, 0), 1, cv2.LINE_AA, hierarchy, 1)

        #Заполним данные
        new_info=ImageInfo(contours_image)
        new_info.contours=contours
        new_info.hierarchy=hierarchy

        return new_info

Этот контур надо аппроксимировать, разработаем следующий класс:

class ContourApproximationProcessingStep(ImageProcessingStep):
    """Шаг, отвечающий за апроксимацию контуров"""

    def __init__(self,eps = 0.005, filter=None):
        """Конструктор
        eps - размер элемента контура от размера общей дуги"""

        self.eps=eps
        self.filter=filter

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

        approx_countours=[]
        img_contours = np.uint8(np.zeros((info.image.shape[0], info.image.shape[1])))
        for countour in info.contours:
            arclen = cv2.arcLength(countour, True)
            epsilon = arclen * self.eps
            approx = cv2.approxPolyDP(countour, epsilon, True)
            append=False
            if not(self.filter is None):
                if self.filter(approx):
                    append=True
            else:
                append=True
            if append:
                approx_countours.append(approx)

        cv2.drawContours(img_contours, approx_countours, -1, (255, 255, 255), 1)

        #Заполним данные
        new_info=ImageInfo(img_contours)
        new_info.contours=approx_countours

        return new_info

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

Итак, испытываем, сначала без фильтра:

import cv2

from Libraries.Core import Engine
from Libraries.ImageProcessingSteps import MedianBlurProcessingStep, ThresholdProcessingStep, ContourProcessingStep, \
    ContourApproximationProcessingStep

def my_filter(approx):
    if len(approx)==4:
        return True
    return False

my_photo = cv2.imread('../Photos/car.jpg')
core=Engine()
core.steps.append(MedianBlurProcessingStep(5))
core.steps.append(ThresholdProcessingStep())
core.steps.append(ContourProcessingStep())
core.steps.append(ContourApproximationProcessingStep(0.02))
#core.steps.append(ContourApproximationProcessingStep(0.02,my_filter))
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()

Смотрим, что у нас получилось:

Здесь для наглядности я показал уменьшенное фото машины. Попробуем обработать полноразмерную фотографию:

Итак, мы видим примерно прямоугольник (да, он кривой, но другие фигуры вообще не похожи на прямоугольник).

Теперь встает вопрос: а как нам среди всех этих линий найти наш «прямоугольник»? Для начала, давайте применим фильтр (вот он нам и понадобился), отбросив все фигуры, которые не являются четырехугольниками. Для этого, как вы заметили, в тексте программы есть такая функция:

def my_filter(approx):
    if len(approx)==4:
        return True
    return False

Осталось поменять вот эту строчку кода

core.steps.append(ContourApproximationProcessingStep(0.02))

На эту:

core.steps.append(ContourApproximationProcessingStep(0.02,my_filter))

И вуаля, у нас остались только четырехугольники:

Как видим, объектов осталось значительно меньше, но все еще много мусора. Отфильтруем его, убрав слишком маленькие объекты:

def my_filter(approx):
    if len(approx)==4:
        if abs(approx[2,0,0]-approx[0,0,0])<10:
            return False
        if abs(approx[2,0,1]-approx[0,0,1])<10:
            return False
        return True
    return False

И вот что у нас получилось:

Осталось всего 5 объектов. По идее, конечно, можно применить дополнительную фильтрацию, например, исключив объекты, имеющие неправильные соотношения длины и ширины (номерной знак имеет конкретные размеры по ГОСТ, а значит, соотношение длины и ширины у него тоже конкретные). Можно так же исключить явно «кривые прямоугольники» у которых разница в длинах противоположных сторон значительно выше уровня погрешности. Правда, при этом следует помнить, что в попытке отфильтровать ненужные объекты можно заодно и нужные до кучи выкинуть. Так что тут надо соблюдать осторожность.

Тем не менее, попробуем. Для начала напишем функцию вычисления длины сторон:

def dist(point1,point2):
    d1 = point1[0] - point2[0]
    d2 = point1[1] - point2[1]
    return math.sqrt(d1*d1+d2*d2)

И внесем изменения в фильтр:

def my_filter(approx):
    if len(approx)==4:
        if abs(approx[2,0,0]-approx[0,0,0])<10:
            return False
        if abs(approx[2,0,1]-approx[0,0,1])<10:
            return False
        if abs(dist(approx[0,0],approx[1,0])/dist(approx[2,0],approx[3,0])-1)>0.4:
            return False
        if abs(dist(approx[0,0],approx[3,0])/dist(approx[1,0],approx[2,0])-1)>0.4:
            return False
        return True
    return False

Вот что получилось:

Как видим, осталось только три объекта. Один из них, кстати, можно отфильтровать на расхождение с прямыми углами (если угол сильно отклоняется от 90 градусов). Но мы пока этого делать не будем, положим, что один лишний объект – не критично.

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

x1=res.contours[0][0][0][0]
y1=res.contours[0][0][0][1]
x2=res.contours[0][2][0][0]
y2=res.contours[0][2][0][1]
cv2.rectangle(finish_result,(x1,y1),(x2,y2),(255,0,0),3)

И вот что в итоге будет нарисовано:

Разумеется, так делать не надо. А надо, ну, хотя бы написать функцию, которая бы извлекала эти точки:

def get_rect(countur_item):
    x1 = countur_item[0][0][0]
    y1 = countur_item[0][0][1]
    x2 = countur_item[2][0][0]
    y2 = countur_item[2][0][1]
    return (x1,y1), (x2,y2)

И тогда мы можем нарисовать первую фигуру вот так:

p1,p2=get_rect(res.contours[0])
cv2.rectangle(finish_result,p1,p2,(255,0,0),3)

А все фигуры вот так:

for item in res.contours:
    p1,p2=get_rect(item)
    cv2.rectangle(finish_result,p1,p2,(255,0,0),3)

И вот что получится:

То есть, теперь нам надо проанализировать только эти три области, поискать там буковки и циферки. Но сначала хотелось бы навести порядок в коде. Вот как выглядит у нас запускаемый файл run2.py:

import cv2
import math

from Libraries.Core import Engine
from Libraries.ImageProcessingSteps import MedianBlurProcessingStep, ThresholdProcessingStep, ContourProcessingStep, \
    ContourApproximationProcessingStep

def dist(point1,point2):
    d1 = point1[0] - point2[0]
    d2 = point1[1] - point2[1]
    return math.sqrt(d1*d1+d2*d2)

def my_filter(approx):
    if len(approx)==4:
        if abs(approx[2,0,0]-approx[0,0,0])<10:
            return False
        if abs(approx[2,0,1]-approx[0,0,1])<10:
            return False
        if abs(dist(approx[0,0],approx[1,0])/dist(approx[2,0],approx[3,0])-1)>0.4:
            return False
        if abs(dist(approx[0,0],approx[3,0])/dist(approx[1,0],approx[2,0])-1)>0.4:
            return False
        return True
    return False

def get_rect(countur_item):
    x1 = countur_item[0][0][0]
    y1 = countur_item[0][0][1]
    x2 = countur_item[2][0][0]
    y2 = countur_item[2][0][1]
    return (x1,y1), (x2,y2)

my_photo = cv2.imread('../Photos/6108249.jpg')
#my_photo = cv2.imread('../Photos/car.jpg')
core=Engine()
core.steps.append(MedianBlurProcessingStep(5))
core.steps.append(ThresholdProcessingStep())
core.steps.append(ContourProcessingStep())
#core.steps.append(ContourApproximationProcessingStep(0.02))
core.steps.append(ContourApproximationProcessingStep(0.02,my_filter))
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)

finish_result = history[0].image.copy()

for item in res.contours:
    p1,p2=get_rect(item)
    cv2.rectangle(finish_result,p1,p2,(255,0,0),3)
cv2.imshow('Finish', finish_result)


cv2.waitKey()
cv2.destroyAllWindows()

Не очень красиво, проведем некоторый рефакторинг. Добавим в папку Libraries файл Utils.py и перенесем туда функции get_rect и dist. Импортируем эти функции:

from Libraries.Utils import dist, get_rect

Теперь запускаемый файл выглядит так:

import cv2

from Libraries.Core import Engine
from Libraries.ImageProcessingSteps import MedianBlurProcessingStep, ThresholdProcessingStep, ContourProcessingStep, \
    ContourApproximationProcessingStep
from Libraries.Utils import dist, get_rect, show_history


def my_filter(approx):
    if len(approx)==4:
        if abs(approx[2,0,0]-approx[0,0,0])<10:
            return False
        if abs(approx[2,0,1]-approx[0,0,1])<10:
            return False
        if abs(dist(approx[0,0],approx[1,0])/dist(approx[2,0],approx[3,0])-1)>0.4:
            return False
        if abs(dist(approx[0,0],approx[3,0])/dist(approx[1,0],approx[2,0])-1)>0.4:
            return False
        return True
    return False


my_photo = cv2.imread('../Photos/6108249.jpg')
#my_photo = cv2.imread('../Photos/car.jpg')
core=Engine()
core.steps.append(MedianBlurProcessingStep(5))
core.steps.append(ThresholdProcessingStep())
core.steps.append(ContourProcessingStep())
#core.steps.append(ContourApproximationProcessingStep(0.02))
core.steps.append(ContourApproximationProcessingStep(0.02,my_filter))
res,history=core.process(my_photo)

show_history(res,history)

finish_result = history[0].image.copy()

for item in res.contours:
    p1, p2 = get_rect(item)
    cv2.rectangle(finish_result, p1, p2, (255, 0, 0), 3)
cv2.imshow('Finish', finish_result)


cv2.waitKey()
cv2.destroyAllWindows()

А файл утилит вот так:

import math
import cv2

def get_rect(countur_item):
    x1 = countur_item[0][0][0]
    y1 = countur_item[0][0][1]
    x2 = countur_item[2][0][0]
    y2 = countur_item[2][0][1]
    return (x1,y1), (x2,y2)

def dist(point1,point2):
    d1 = point1[0] - point2[0]
    d2 = point1[1] - point2[1]
    return math.sqrt(d1*d1+d2*d2)

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

Теперь можно подумать о том, как «расшифровать» номер. На этом уроке я расскажу, как при помощи нейросети распознавать цифры, а распознавалку будем писать на следующем уроке.

И так, знакомитесь, его Величество Keras. Чтобы установить его под Windows, cсначала ставим TensorFlow:

pip3 install tensorflow

а затем и сам Keras:

pip3 install Keras

Ну, и собственно, пример кода обучения нейросети на встроенном в керас стандартном датасете minst:

from keras import layers
from keras import models

from keras.datasets import mnist
import tensorflow as tf


model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255
train_labels = tf.keras.utils.to_categorical(train_labels)
test_labels = tf.keras.utils.to_categorical(test_labels)

model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)

test_loss, test_acc = model.evaluate(test_images, test_labels)
print(test_acc)

Здесь используется сверточная нейросеть, на входе которой черно-белая картинка 28 на 28 пикселей, на выходе десятизначный вектор вероятностей, что на картинке та или иная цифра. Модель показывает точность порядка 99% на тестовой выборке.

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

from keras.datasets import mnist
import cv2

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
print(train_labels)

cv2.imshow("Цифра", train_images[0])
cv2.waitKey(0)
cv2.destroyAllWindows()

Для элемента номер нуль мы увидим цифру 5:

И этому элементу действительно соответствует лэйбл 5:

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

model.save('cats_and_dogs_small_1.h5')

Ну а теперь попробуем загрузить картинку с цифрой и дать нейросетке распознать ее:

import cv2
from keras import models

my_photo = cv2.imread('imgs/Digit0.png',cv2.IMREAD_GRAYSCALE) #загрузим изображение

#приведем изображение к формату для нейросети
normal_photo=my_photo/255.0
input=normal_photo.reshape(1,28,28)

#скормим изображение нейросетке и получим результат
model = models.load_model('mnist_model.bin')
result=model.predict(input)

print(result)

Вот картинка:

На выходе:

[[9.9990845e-01 1.4144711e-08 8.4316625e-08 3.7920216e-11 2.4454723e-06

  4.7663391e-08 8.7873021e-05 4.1903621e-07 3.8488349e-08 6.0560058e-07]]

Видно, в первой (то есть нулевой, счет с нуля) ячейке (соответствует цифре 0) вероятность почти 1, в остальных почти 0.

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

[[5.2682775e-08 9.9998152e-01 9.0230742e-07 1.2926430e-09 9.7749239e-07

  6.3665328e-07 5.2730784e-06 1.0716837e-05 2.0985880e-08 1.3917042e-11]]

Как видим, и тут распознал циферку правильно.

На этом все, засовывать нейросеть в проект с «красивым» кодом будем в следующий раз.

Напомню, что примеры можно скачать здесь: megabax/CVContainer: It is my pet computer vision project. (github.com)

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