Привет, Хабр! В этой статье я бы хотел рассказать как с помощью только OpenCV распознавать объекты, на примере игральных карт:
Введение
Допустим, у нас имеется следующее изображение с картами:
А также у нас имеются эталонные изображения каждой карты:
И теперь для того, чтобы детектировать каждую карту, нам необходимо написать три ключевые функции, которые:
- находит контуры всех карт;
- находит координаты каждой отдельной карты;
- распознаёт карту с помощью ключевых точек.
Нахождение контуров карт
def find_contours_of_cards(image):
blurred = cv2.GaussianBlur(image, (3, 3), 0)
T, thresh_img = cv2.threshold(blurred, 215, 255,
cv2.THRESH_BINARY)
(_, cnts, _) = cv2.findContours(thresh_img,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
return cnts
Первым аргументом в данную функцию мы передаём изображение в оттенках серого, к которому применяем гауссово размытие, для того, чтобы было легче найти контуры карт. После этого с помощью функции threshold() мы преобразуем наше серое изображение в бинарное. Данная функция принимает первым параметром изображение, вторым — пороговое значение, третьим — это максимальное значение, которое присваивается значениям пикселей, превышающим пороговое значение. Из нашего примера следует, что любое значение пикселя, превышающее 215, устанавливается равным 255, а любое значение, которое меньше 215, устанавливается равным нулю. И последним параметром передаём метод порогового значения. Мы используем THRESH_BINARY(), который указывает на то, что значения пикселей, которые больше 215 устанавливаются в максимальное значение, которое мы передали третьим параметром. Данная функция возвращает два значения, где первое — это значение, которое мы передали в данную функцию вторым аргументом, а второе — чёрно-белое изображение, которое выглядит подобным образом:
Теперь мы можем найти контуры наших карт, где контур — это кривая, соединяющая все непрерывные точки, которые имеют одинаковый цвет. Поиск контуров осуществляется с помощью метода findContours(), где в качестве первого аргумента эта функция принимает изображение, вторым — это тип контуров, который мы хотим извлечь. Я использую cv2.RETR_EXTERNAL для извлечения только внешних контуров. К примеру, для того, чтобы извлечь все контуры используют cv2.RETR_LIST, а последний параметром мы указываем метод аппроксимации контура. Мы используем cv2.CHAIN_APPROX_SIMPLE, указывая на то, что все лишние точки будут удалены, тем самым экономя память. Например, если вы нашли контур прямой линии, то разве вам нужны все точки этой линии, чтобы представить эту линию? Нет, нам нужны только две конечные точки этой линии. Это как раз то, что и делает cv2.CHAIN_APPROX_SIMPLE.
Нахождение координат карт
def find_coordinates_of_cards(cnts, image):
cards_coordinates = {}
for i in range(0, len(cnts)):
x, y, w, h = cv2.boundingRect(cnts[i])
if w > 20 and h > 30:
img_crop = image[y - 15:y + h + 15,
x - 15:x + w + 15]
cards_name = find_features(img_crop)
cards_coordinates[cards_name] = (x - 15,
y - 15, x + w + 15, y + h + 15)
return cards_coordinates
Данная функция принимает контуры, которые мы нашли в предыдущей функции, а также основное изображение в оттенках серого. Первым делом мы создаём словарь, где в роли ключа будет выступать название карты, а в роли значения координаты каждой карты. Далее мы проходимся в цикле по нашим контурам, где с помощью функции boundingRect() находим ограничительные рамки каждого контура: начальные x и y координаты, за которыми следуют ширина и высота рамки. Так получилось, что функция, которая искала контура, нашла аж 31 контур, хотя карт всего 4. Это могут быть незначительные контуры, которые мы дальше сортируем в условии, исходя из размера контура.
Теперь, зная координаты контура, мы можем вырезать по этим координатам каждую карту, что собственно мы и делаем в следующей строчке кода. Далее вырезанное изображение мы передаём в функцию find_features()(о ней мы поговорим ниже), которая опираясь на ключевые точки, возвращает нам название вырезанной карты. После этого добавляем всё в словарь.
Распознавание карт
Как говорилось выше, распознавание карт будет осуществляться на основе ключевых точек. Ключевые точки — это интересные области изображения. Интересными участками называются такие участки, которые неоднородны. Например, это могут быть углы, так как там происходит резкое изменение интенсивности в двух разных направлениях. Если мы посмотрим на изображение ниже, а потом закроем глаза и попытаемся визуализировать этот образ, то мы вряд ли сможем увидеть что-то конкретное и особенное на этом изображении. Причина этого в том, что изображение не содержит никакой интересной информации:
Теперь закройте глаза и попытайтесь представить это изображение:
И вы увидите, что помните много деталей об этом образе. Причина этого в том, что на изображении много интересных областей. Теперь переходим к практике:
def find_features(img1):
correct_matches_dct = {}
directory = 'images/cards/sample/'
for image in os.listdir(directory):
img2 = cv2.imread(directory+image, 0)
orb = cv2.ORB_create()
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
correct_matches = []
for m, n in matches:
if m.distance < 0.75*n.distance:
correct_matches.append([m])
correct_matches_dct[image.split('.')[0]]
= len(correct_matches)
correct_matches_dct =
dict(sorted(correct_matches_dct.items(),
key=lambda item: item[1], reverse=True))
return list(correct_matches_dct.keys())[0]
Для обнаружения ключевых точек я использую ORB, который мы инициализируем с помощью вызова функции ORB_create(), после чего мы находим ключевые точки и дискрипторы(которые кодируют интересную информацию в ряд чисел) для эталонной карты и для карты, которую мы вырезали из главного изображения. Вот, к примеру, как выглядят ключевые точки для короля:
Затем нам необходимо сопоставить(вычислить расстояние) дискрипторы первого изображения с дискрипторами второго и взять ближайший. Для этого мы создаём BFMatcher объект с помощью вызова метода BFMatcher(). Теперь мы можем с помощью функции knnMatch() найти k лучших совпадений для каждого дескриптора, где k в нашем случае равно 2. Далее нам необходимо выбрать только хорошие совпадения, основываясь на расстоянии. Поэтому мы проходимся в цикле по нашим совпадениям и если оно удовлетворяет условию m.distance < 0.75*n.distance, то мы засчитываем это совпадение как хорошее и добавляем в список. Потом считаем количество хороших совпадений(чем больше, тем лучше) и основываясь на этом делаем вывод, что за карта. Вот какие совпадения были найдены для каждой карты с королём:
И вслед за этим рисуем прямоугольник вокруг карты с помощью функции draw_rectangle_aroud_cards():
def draw_rectangle_aroud_cards(cards_coordinates, image):
for key, value in cards_coordinates.items():
rec = cv2.rectangle(image, (value[0], value[1]),
(value[2], value[3]),
(255, 255, 0), 2)
cv2.putText(rec, key, (value[0], value[1] - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5, (36, 255, 12), 1)
cv2.imshow('Image', image)
cv2.waitKey(0)
На этом всё. Надеюсь, было познавательно) Код и картинки можно найти на github. До новых встреч:)
AAbrasha
Было бы очень здорово указать в начале статьи ссылки на предыдущие части для тех, кто хочет прочитать серию статьей с начала.
wadik69 Автор
Раньше указывал, но, думаю, люди сами догадаются, если в заголовке указана Часть 4, к примеру.
AAbrasha
А вообще, спасибо большое за серию статей! Весь вечер вчера не мог оторваться от изучения новых и новых трюков) Keep it up!