Уроки компьютерного зрения на Python + OpenCV с самых азов. Часть 10. Мой пэт-проект

от автора

Оглавление: Уроки компьютерного зрения. Оглавление / Хабр (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)


ссылка на оригинал статьи https://habr.com/ru/articles/738698/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *