Pull to refresh

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

Reading time 6 min
Views 5.5K

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


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


Парсинг 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 (и особенно — учить модельки *^*) и занимаюсь им полтора года. Этот пост создан благодаря нашей команде. Мы — начинающие российские стартаперы и хотим делиться с Вами тем, что узнаем сами.



Удачи c:


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


Tags:
Hubs:
0
Comments 1
Comments Comments 1

Articles