Визуализация каскадов Хаара

от автора

Интерпретируемое машинное обучение — популярная тема в последние годы. Во многом благодаря использованию этой технологии в медицине, транспорте и других областях, где цена ошибки велика, нужно понимать, как модель устроена и чем "руководствуется" при принятии решений.

Простота объяснения зависит от сложности модели. Куда проще понять, как работает дерево принятия решений, чем извлечь какие-то определенные правила из весов полносвязной нейронки. К счастью, каскады Хаара имеют довольно простую структуру и можно, последовательно применяя их к изображению, узнать, как работает модель.

Парсинг xml-файла

Начнем с начала. OpenCV работает с каскадами, сохраненными в xml. Автор статьи помог разобраться, как этот файл устроен. Давайте посмотрим.

Сперва идет описание каскада. Будем использовать детектор лиц из OpenCV. stageType говорит нам, что каскады являются бустингом. featureType — тип признаков, а height и width — высота и ширина признаков, используемых классификаторами. maxWeakCount — максимальное количество слабых классификаторов на каждом уровне каскада. stageNum — количество уровней.

<opencv_storage> <cascade type_id="opencv-cascade-classifier"><stageType>BOOST</stageType>   <featureType>HAAR</featureType>   <height>24</height>   <width>24</width>   <stageParams>     <maxWeakCount>211</maxWeakCount></stageParams>   <featureParams>     <maxCatCount>0</maxCatCount></featureParams>   <stageNum>25</stageNum>

Что за признаки и классификаторы? Признаки — это небольшие свертки (маски), которые применяются к изображению.


Из пикселей изображения, находящихся под белой областью, вычитаются пиксели, находящиеся под черной областью

Классификаторы — это решающие деревья, которые после получения значений от сверток выдают какие-то чиселки. И в зависимости от суммы этих чиселок каскад решает, есть ли на изображении нужный предмет.

Уровни (stages) — это группы классификаторов. Каждый уровень смотрит на активацию своих классификаторов и говорит, нужно ли уточнить свои показания (перейти на следующий уровень) или пропустить изображение, потому что на нем ничего нет.

<stages>     <_>       <maxWeakCount>9</maxWeakCount>       <stageThreshold>-5.0425500869750977e+00</stageThreshold>       <weakClassifiers>         <_>           <internalNodes>             0 -1 0 -3.1511999666690826e-02</internalNodes>           <leafValues>             2.0875380039215088e+00 -2.2172100543975830e+00</leafValues></_>

stageThreshold — порог, который нужно преодолеть классификаторам для перехода на следующий уровень. Сами же классификаторы хранятся в теге weakClassifiers. internalNodes содержит информацию об узлах дерева. Первое значение 0 — индекс текущей ноды. Второе — -1 — индекс ноды, в которую нужно перейти, переход по листьям завершается, когда индекс становится меньше 0. (На самом деле, там немного более сложная схема переходов, можно посмотреть в исходниках OpenCV.) Затем идут номер признака 0 (описания признаков — дальше в файле) и пороговое значение дерева -3.1511999666690826e-02.

В leafValues хранится информация о листьях дерева. Первое значение 2.0875380039215088e+00 — левый лист, он возвращается, если значение свертки меньше порога дерева, второе значение -2.2172100543975830e+00 — правый лист — возвращается, если значение свертки больше порога.

Теперь признаки:

<features>     <_>       <rects>         <_>           6 4 12 9 -1.</_>         <_>           6 7 12 3 3.</_></rects></_>

В теге rects хранятся прямоугольники, описывающие свертку. Первые 4 числа — x1, y1, x2, y2 — координаты противоположных вершин прямоугольника, пятое число — его "цвет". Если число отрицательное (-1), то пиксели этого прямоугольника вычитаются, если положительное (3) — складываются.

С файлом разобрались, давайте парсить:

# импортируем библиотеки from lxml import etree from multiprocessing import Pool, cpu_count import time  import numpy as np from scipy.signal import convolve2d import matplotlib.pyplot as plt import cv2

cascade_path = "haarcascade_frontalface_default.xml"  with open(cascade_path) as f:     xml = f.read()

root = etree.fromstring(xml)  cascade = root.find("cascade") width = int(cascade.find("width").text) height = int(cascade.find("height").text) features = cascade.find("features").getchildren()

# создаем массив признаков feature_matrices = np.zeros((len(features), height, width)) for i, feature in enumerate(features):     cur_matrix = np.zeros((height, width))     for rect in feature.find("rects").getchildren():         line = rect.text.strip().split(" ")         x1, y1, x2, y2 = map(int, line[:4])         x1, x2 = min(x1, x2), max(x1, x2)         y1, y2 = min(y1, y2), max(y1, y2)         c = float(line[4])          cur_matrix[y1:y2+1, x1:x2+1] = c      feature_matrices[i] = cur_matrix

# выведем первый признак plt.imshow(feature_matrices[0], cmap="gray") >>>

# парсим уровни каскада в новую структуру stages = cascade.find("stages") stages_list = [] for stage in stages.getchildren():     if type(stage) == etree._Element:         threshold = float(stage.find("stageThreshold").text)         clfs = stage.find("weakClassifiers")          classifiers = []         for clf in clfs:             internal_nodes = clf.find("internalNodes").text.strip().split(" ")             feature_num = int(internal_nodes[2])             feature_thresh = float(internal_nodes[3])              leafs = clf.find("leafValues").text.strip().split(" ")             less_leaf = float(leafs[0])             greater_leaf = float(leafs[1])              classifiers.append([feature_num, feature_thresh, less_leaf, greater_leaf])          stages_list.append([threshold, classifiers])

len(stages_list) >>> 25

Отрисовка классификаторов

Итак, у нас 25 уровней. Давайте наложим их на картинку:

image = cv2.imread("photo.jpg", 0) image_height, image_width = image.shape[:2] plt.imshow(image) >>>


Кислотный Шерлок

for stage in stages_list:     # делаем копию картинки, на которой можно будет рисовать     image_copy = image.copy()      for classifier in stage[1]:         feature_num, thresh, less, greater = classifier          # применение свертки         # можно сделать и перемножением в numpy, но получается дольше         activation_map = convolve2d(image, feature_matrices[feature_num], mode="valid")          # в зависимости от значений листов выбираем, какой лист соответствует большей активации         # и если попадаем в этот лист, то считаем, что классификатор активировался         if greater > less:             activation_map[activation_map < thresh] = 0         else:             activation_map[activation_map > thresh] = 0          # выбираем 5 наибольших активаций по картинке         k = 5         flatten_activation_map = activation_map.flatten()         top_indices = np.argpartition(flatten_activation_map, -k)[-k:]          # фильтруем нулевые активации         top_indices = top_indices[flatten_activation_map[top_indices] > 0]          for top_index in top_indices:             i, j = np.unravel_index(top_index, activation_map.shape)              image_part = image[i:i+height, j:j+width].astype(np.uint8)             rectangle = np.ones(image_part.shape, dtype=np.uint8) * 255              # полупрозрачный прямоугольник там, где активировался классификатор             res = cv2.addWeighted(image_part, 0.5, rectangle, 0.5, 1.0)             image_copy[i:i+height, j:j+width] = res      plt.figure()     plt.imshow(image_copy, cmap="gray")     plt.show()

Код выведет 25 картинок, поэтому я прикреплю только последние 2:

В ноутбуке после статьи можно посмотреть активации на разных размерах картинки:

Куда копать дальше?

Я реализовал визуализацию только одноуровневых каскадов (то есть деревьев с одной нодой и двумя листами), но это можно относительно просто исправить. А чтобы посчитать, какое значение свертки принесет наибольшую активацию, можно распарсить дерево и вытащить оттуда промежуток значений [свертки].

Помимо признаков Хаара есть и другие. Например, LBF или обобщенные признаки Хаара. Их также можно визуализировать. Также на картинке можно показывать сами признаки — отображать не белый прямоугольник, а матрицу классификатора (черно-белые области).

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

Немного о нас

Еще раз привет, меня зовут Евгений. Обожаю Data science (и особенно — учить модельки *^*) и занимаюсь им полтора года. Этот пост создан благодаря нашей команде — FARADAY Lab. Мы — начинающие российские стартаперы и хотим делиться с Вами тем, что узнаем сами.

Удачи c:

Полезные ссылки:

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


Комментарии

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

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