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