Да да, в этой статье будет описана попытка научить компьютер детектировать adult изображения.
В качестве инструментов используется python, opencv и scikit-learn.
На выборке из 2500 примеров удалось получить точность около 90%.
Под катом вы найдёте описание подхода c примерами кода.

Описание


Для начала необходимо получить обучающую выборку — набор изображений, которые относятся к порно и набор любых других изображений. Я получил её следующим образом — обычные картинки взял из выдачи google и yandex фото, а так же с flickr, воспользовавшись его api поиска изображений со свободными лицензиями. Порно под свободной лицензией мне найти не удалось поэтому пришлось воспользовать thepiratebay-ем и произведениями неизвестных авторов с неизвестной лицензией. Вначале я собрал разметку из 1000 изображений (по 500 каждой категории), потом увеличил до 2500.

Разметка есть — теперь можно приступать к обучению. Первым шагом — необходимо преобразовать изображения в наборы признаков. Один из вариантов — взять непосредственно значения яркости в каждом пикселе изображения. Но в этом случае обучиться по этим признаком будет тяжело — их количество огромно а полезная информация в каждом из них не велика. Поэтому на практике часто используют другие подходы. На изображении пытаются найти ключевые точки — такие точки, которые представляют собой наибольшей интерес. Это могут быть углы, резкие смены цвета, линии или что-то ещё. После того как точки обнаружены — в их окрестности считаются дескрипторы. Дескриптор — это компактное представление этой точки (а точнее того, что содержится в небольшом радиусе вокруг неё). Основные требования к ключевым точкам и дескрипторам — устойчивость к масштабированию и повороту изображений. Сейчас существуют различные алгоритмы определения ключевых точек и дескрипторов. Мы воспользуемся алгоритмом SIFT, но можно взять и какой-то другой — SIFT является запатенованным и использовать его в США для коммерческих целей может быть затруднительно.

image


На следующем шаге среди дескрипторов, извлеченных из всех изображений в нашей выборке, необходимо найти похожие. Для этого воспользуемся одним из алгоритмов кластеризации, к примеру K-Means. Он позволяет объеденить огромную кучу дескрипторов (в среднем — около 200 c каждого изображения, а всего 2500 изображений — итого 500 000 дескрипторов) в заданное количество групп с похожими дескрипторами (например, 1000). После выполнения этой операции для любого дескриптора мы можем сказать — в какую именно группу он попадёт.

image


Для каждой из картинок мы получаем массив из 1000 чисел. На первом месте — количество встретившихся на картинке дескрипторов, принадлежащих первой группе, на втором — второй, и т. д. Этот массив мы и будем использовать в качестве признаков. В качестве классификатора будем использовать наивный байесовский классификатор — он довольно хорошо работает с большим числом признаков, большая часть из которых нулевые. Его частенько используют для классификации текстов вместе с моделью bag of words. В нашем же случае подход такой же как и с текстами — только вместо слов — дескрипторы. Но применять байесовский классификатор непосредственно к частотам встречаемости — не самая лучшая идея. Одни дескрипторы могут встречаться на всех картинках, в то время как другие могут встречаться намного реже, но зато быть намного полезнее для решаемой задачи. Для того, чтобы правильней оценить важность признака можно воспользовать мерой tf-idf. В ней вес некоторого слова (для нашей задачи — дескриптора) пропорционален количеству употребления этого слова в документе (для нашей задачи — изображении), и обратно пропорционален частоте употребления слова в других документах коллекции.

После обучения модели и проверки точности на тестовой выборке (например, взяв 80% данных для обучения и 20% для теста) получаем точность около 70%. Попробуем улучшить. Дескрипторы SIFT по умолчанию не используют цветовую информацию. Но есть способы всё же извлечь цветовую информацию для SIFT дескрипторов. Один из них — OpponentSIFT. Его суть в следующем. Строится четыре версии картинки — черно-белая и три слоя в opponent цветах (oponnent цвета — один из варинтов представления цвета, аналог rgb и hsv но более близкий человеческом зрению). Ключевые точки по прежнему ищутся в черно-белом изображении. После этого для каждой из точек извлекается три дескриптора — по одному на канал в opponent цветах. Затем эти три дескриптора соединяют в один длинный.
Воспользовавшись OpponentSIFT получившаяся точность будет около 80%.

Пробуем улучшить дальше. Один классификатор хорошо, а серия — лучше, поэтому пробуем бустинг (а точнее — AdaBoost). Бустинг — подход к построению серии однотипных классификаторов, в которых каждый следующий учится на ошибках предыдущего. Применив бустинг получим результат около 85% процентов.

Среди примеров ошибочно определенных изображений много таких, которые можно было бы отсеять по цветовой гамме. Несмотря на то, что мы использовали цвет в дескрипторах — про общую цветовую гамму наш детектор ничего не знает. Поэтому следующим шагом будем пытаться построить другую модель, используя в качестве признаков не дескрипторы, а цветовую гистограмму. Преобразуем изображение в hsv и воспользуемся каналом hue для подсчёта гистограммы (количества встречаемости каждого из 256 цветовых оттенков).
В итоге, мы получим массив из 256 чисел (количеств встречаемости, по одному числу на цевт — аналогично 1000 группам дескрипторов).
Проделываем с ним те-же самые операции — tfidf, байесовский классификатор, бустинг. Качество такого цветового классификатора будет примерно 80%.

Теперь у нас есть два детектора — один по ключевым точкам, с точностью 85% и второй по цветовой гистограмме — с точностью 80%. Склеим из них один, но крутой! Алгоритм следующий. Если оба детектора выдали одинаковое предсказание — верим им и сразу выдаём ответ. Если они выдали разные предсказания — верим тому из них, кто больше уверен в своём предсказании (тому, кто выдал большую вероятность).
Такой общий детектор будет иметь точность около 90%!

На этом пока остановимся, но вообще 90% не предел и можно пытаться улучшать и дальше. Например, добавить классификаторов, которые будут выискивать на картинах конкретные элементы.

Реализация


Для реализации детектора нам понадобится python, opencv и scikit-learn. Я разрабатывался под ubuntu и использовал python2.7, scikit-learn0.16.1 и opencv2.4. opencv пришлось собрать самому т. к. в версии из репозитория отсутствовали sift дескрипторы.

И так — начнём с открытия файла и вычисления дескрипторов:
img = cv2.imread(fileName)

if img.shape[1] > 1000:
  cf = 1000.0 / img.shape[1]
  newSize = (int(cf * img.shape[0]), int(cf * img.shape[1]), img.shape[2])
  img.resize(newSize)

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

s = cv2.SIFT(nfeatures = 400)

d = cv2.DescriptorExtractor_create("OpponentSIFT")
kp = s.detect(gray, None)
kp, des = d.compute(img, kp)

Тут мы открываем файл, ресайзим его (на файлах с большим разрешением дескрипторы считаются долго и едят много оперативки), трансформируем в черно-белое и вычисляем дескрипторы (сами точки находим по черно-белому, а значения дескрипторов вычисляем уже по цветному.

Дальше считаем цветовую гистограмму для второго классификатора:
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
dist = cv2.calcHist([hsv],[0],None,[256],[0,256])

Переводим картинку в hsv и по каналу hue считаем гистограмму.

Для дальнейшего процесса нам понадобится создать все необходимые классификаторы:
kmeans = MiniBatchKMeans(n_clusters = CLUSTERS_NUMBER, random_state = CLUSTER_SEED, verbose = True)
tfidf = TfidfTransformer()
tfidf1 = TfidfTransformer()
clf = AdaBoostClassifier(MultinomialNB(alpha = BAYES_ALPHA), n_estimators = ADA_BOOST_ESTIMATORS)
clf1 = AdaBoostClassifier(MultinomialNB(alpha = BAYES_ALPHA), n_estimators = ADA_BOOST_ESTIMATORS)

kmeans — для кластеризации, TfidfTransformer, MultinomialBN, AdaBoostClassifier — для классификации.

Теперь можно загружать обучающую выборку и приступать к обучению:
positiveSamples = loadSamples(positiveFiles)
negativeSamples = loadSamples(negativeFiles)

totalDescriptors = []
addDescriptors(totalDescriptors, positiveSamples)
addDescriptors(totalDescriptors, negativeSamples)
  
kmeans.fit(totalDescriptors)
clusters = kmeans.predict(totalDescriptors)

totalSamplesNumber = len(negativeSamples) + len(positiveSamples)
counts = lil_matrix((totalSamplesNumber, CLUSTERS_NUMBER))
counts1 = lil_matrix((totalSamplesNumber, 256))
calculteCounts(positiveSamples, counts, counts1, clusters)
calculteCounts(negativeSamples, counts, counts1, clusters)
counts = csr_matrix(counts)
counts1 = csr_matrix(counts1)

_tfidf = tfidf.fit_transform(counts)
_tfidf1 = tfidf1.fit_transform(counts1)
classes = [True] * len(positiveSamples) + [False] * len(negativeSamples)
clf.fit(_tfidf, classes)
clf1.fit(_tfidf1, classes)

Вначале загружаются положительные и отрицательные примеры. Функция addDescriptors просто извлекает из них дексрипторы в список totalDescriptors. totalDescriptors — это просто куча всех дескрипторов с картинок из всей коллекции — они нужны нам для кластеризации (kmeans.fit).
kmeans.predict возвращает нам список кластеров — какому дескриптору какой кластер соответствует.
Дальше создаём две матрицы, и считаем в них частоту встречаемости. В первую — сколько раз встретились дескрипторы из каждого класстера для каждой картинки. Во вторую — сколько раз встретился каждый цвет для каждой картинки.
Дальше преобразовываем наши матрицы используя tfidf (tfidf.fit_transform) и обучаем бустнутый байесовский классификатор (clf.fit). Для разных детекторов (по дескрипторам и по цвету) — обучаем разные классификаторы (clf и clf1).

На этом обучение окончено. Теперь можно сохранить нашу модель в файл чтобы потом применить её для предсказаний. Для этого воспользуемся стандартным python модулем pickle:

data = pickle.dumps((CLUSTERS_NUMBER, kmeans, tfidf, tfidf1, clf, clf1), -1)
data = zlib.compress(data)
open(fileName, 'w').write(data)


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

samples = loadSamples(files)
totalDescriptors = []
addDescriptors(totalDescriptors, samples)

clusters = kmeans.predict(totalDescriptors)
counts = lil_matrix((len(samples), CLUSTERS_NUMBER))
counts1 = lil_matrix((len(samples), 256))
calculteCounts(samples, counts, counts1, clusters)
counts = csr_matrix(counts)
counts1 = csr_matrix(counts1)

_tfidf = tfidf.transform(counts)
_tfidf1 = tfidf1.transform(counts1)

weights = clf.predict_log_proba(_tfidf)
weights1 = clf1.predict_log_proba(_tfidf1)
predictions = []
for i in xrange(0, len(weights)):
  w = weights[i][0] - weights[i][1]
  w1 = weights1[i][0] - weights1[i][1]
  pred = w < 0
  pred1 = w1 < 0
  if pred != pred1:
    pred = w + w1 < 0
  predictions.append(pred)


На этом всё. Полные исходники вы можете посмотреть на гитхабе. Разметку выложить не могу, т. к. не уверен что это не нарушит лицензии. Возможно выложу обученную модель, если кому-то пригодится.

Комментарии (7)


  1. crystalbit
    28.08.2015 22:41
    +1

    Прочитав анонс, представил, как автор вручную собирает 2500к примеров. Опечатка – 2,5к или просто 2500 :)


  1. rPman
    28.08.2015 22:42

    Можете визуально оценить, какие картинки алгоритм посчитал ошибочно 'кое-какими' и наоборот? Есть какие то закономерности?


    1. bak Автор
      28.08.2015 22:51
      +1

      Обычно ошибается на таких картинках, которые редко представлены в наборе. Вначале ошибался на обычных людях (много лиц, дети) — добавил побольше людей — начал ошибаться на котиках. Добавил побольше котиков — теперь с ними проблем нет. Какой-то закономерности увидеть не получилось — просто периодически на, казалось бы, вполне нормальные изображения даёт положительный результат. Например, летящий в небе самолёт выпустивший шасси.
      Думаю на действительно большой разметке за счёт бустинга можно победить большую часть ошибок.


      1. rPman
        28.08.2015 23:11

        кстати, как вам идея, повысить количество кое-каких изображений, извлекая их из видео :)


        1. bak Автор
          28.08.2015 23:14

          А как определить что в конкретный момент на видео — то самое?


          1. rPman
            28.08.2015 23:39

            А стоит ли?
            Фича нейронных сетей — умение обучаться на зашумленных выборках…

            к тому же, представьте что сеть найдет закономерности даже в промежуточных сценах, которые на первый взгляд невинны, но опытный глаз сразу видит подвох :)


  1. gwer
    28.08.2015 23:27
    +1

    Я уж ждал, что в честь пятницы будет где-нибудь под спойлером часть обучающей выборки…