Визуализация каскадов Хаара
- четверг, 25 июня 2020 г. в 00:28:37
Интерпретируемое машинное обучение — популярная тема в последние годы. Во многом благодаря использованию этой технологии в медицине, транспорте и других областях, где цена ошибки велика, нужно понимать, как модель устроена и чем "руководствуется" при принятии решений.
Простота объяснения зависит от сложности модели. Куда проще понять, как работает дерево принятия решений, чем извлечь какие-то определенные правила из весов полносвязной нейронки. К счастью, каскады Хаара имеют довольно простую структуру и можно, последовательно применяя их к изображению, узнать, как работает модель.
Начнем с начала. 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 (и особенно — учить модельки *^*) и занимаюсь им полтора года. Этот пост создан благодаря нашей команде. Мы — начинающие российские стартаперы и хотим делиться с Вами тем, что узнаем сами.
Удачи c: