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