Мир будущего: управление устройствами с помощью жестов. computer vision.. computer vision. Data Mining.. computer vision. Data Mining. data science.. computer vision. Data Mining. data science. datasets.. computer vision. Data Mining. data science. datasets. deep learning.. computer vision. Data Mining. data science. datasets. deep learning. detection.. computer vision. Data Mining. data science. datasets. deep learning. detection. device control.. computer vision. Data Mining. data science. datasets. deep learning. detection. device control. gesture recognition.. computer vision. Data Mining. data science. datasets. deep learning. detection. device control. gesture recognition. human-computer-interaction.. computer vision. Data Mining. data science. datasets. deep learning. detection. device control. gesture recognition. human-computer-interaction. neural networks.
Мир будущего: управление устройствами с помощью жестов - 1

Видели в кино, как устройствами управляют с помощью жестов? Сделать такую систему очень просто, а ещё очень дорого. Но всё-таки есть способ сделать её достаточно лёгкой и простой — настолько, чтобы можно было интегрировать в любое устройство с любым процессором, потратив минимальное количество денег.

Это Александр Нагаев, техлид из SberDevices команды R&D компьютерного зрения. Расскажу, как создавать и использовать оптимизированные модели для управления устройствами с помощью жестов.

Как создать такую систему легко, но дорого

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

  • собираем большой датасет;

  • на этом датасете обучаем большой 3D-трансформер;

  • для этого арендуем сервер DGX из 16 V100 видеокарт;

  • получаем на выходе скорость инференса в 200мс на GPU или примерно 1,5с инференса на CPU.

Мир будущего: управление устройствами с помощью жестов - 2

Ура, круто, бежим к тимлиду показывать результаты!

Но есть нюанс: у вас может не быть ни датасета, ни денег на аренду видеокарт, ни времени на обучение таких больших моделей, ни на чём их инферить. Это самое не оптимальное решение, которое точно в прод никогда не пойдёт.

Какие бывают жесты

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

Жесты можно разделить на две категории:

  1. Статичные: play pause, like dislike, stop, palm, peace.

Например, вы слушаете музыку и хотите сохранить песню в подборку — достаточно показать лайк устройству, и песня появится в избранном. С помощью статичного жеста также можно поставить паузу или ответить на звонок.

  1. Динамические: Drag-and-drop, swipes, volume up down, zoom in out.

Динамические жесты позволяют управлять устройством с помощью движения пальцами, например, перетаскивать и перелистывать карточки, уменьшать/увеличивать громкость или размер картинки.

Статичные жесты

В одной из наших работ уже имеется часть статичных жестов — это датасет HaGRID.

Мир будущего: управление устройствами с помощью жестов - 3

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

Динамические жесты

Динамический жест можно описать четырьмя элементами:

  1. положение руки;

  2. определённый жест руки;

  3. движение руки;

  4. начало и конец жеста.

Мир будущего: управление устройствами с помощью жестов - 4

Сначала нужно понять, как расположена рука. Далее определить статичный жест, и движение руки — откуда и в какую сторону она движется.

У динамических жестов есть начало и конец. Например, у свайпов это рука справа/ слева, внизу/вверху, у Drag-and-drop — схватить в одном месте, отпустить — в другом. Для некоторых динамических жестов начало и конец — это один и тот же статичный жест.

Например, мы хотим реализовать свайп вправо. Это значит, что нужно распознать руку, которая показывает влево, а потом — руку, которая показывает вправо, причём строго в такой последовательности. Чтобы это сделать, нужно в первую очередь найти руки, присвоить им класс «распознать жест», и после этого распознать сам переход от одного класса к другому.

Где брать данные и как их использовать

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

  • В первую очередь нужен датасет для решения задачи детекции рук, ведь чтобы распознавать классы рук, нужно их сначала обнаружить.

  • В датасете должны присутствовать статичные жесты, чтобы сразу присвоить определённой руке класс статичного жеста, и потом привязать к нему второй класс для распознавания динамики.

  • Датасет должен быть максимально разнородным. Это нужно, чтобы решение работало хорошо на разных устройствах с разными типами камер с разным разрешением и так далее.

Наш датасет HaGRID в своих разработках использует компания Intel для обучения и запуска инференса моделей с помощью OpenVINO фреймворка и Google MediaPipe. У MediaPipe есть хорошее решение для распознавания ключевых точек кистей рук. Они дообучали модель детекции в одном из решений распознавания ладошек на нашем датасете. HaGRID состоит из более полумиллиона изображений, восемнадцати статичных жестов и имеет максимальную разнородность.

Мир будущего: управление устройствами с помощью жестов - 5

На примерах видно, что яркость, субъекты, размер боксов на изображении и фон— разнородные. Разметка включает в себя координаты bounding box и label жеста.

Мир будущего: управление устройствами с помощью жестов - 6

В некоторых разметках могут присутствовать два обрамляющих прямоугольника, где один присвоен определённому жесту, а второй — так называемый false positive, то есть не жест.

Модели для обработки видео

Возьмём архитектуру 3D модели, подадим ей на вход тензор, например, из 8 и 16 кадров, и она сразу же выведет класс.

Мир будущего: управление устройствами с помощью жестов - 7

Но у 3D моделей есть недостатки:

  • Нуждаются в специфичном датасете, например, видео с размеченным началом и концом динамического жеста, после чего этот датасет нужно дополнительно обрабатывать перед обучением моделей.

  • Требовательны к железу, так как тензор увеличивается на ещё одну размерность, и обучать его на видеокарте с маленьким количеством видеопамяти не получится.

  • Медленно работают на CPU из-за ограничений и вычислительной сложности.

Но есть и преимущества:

  • Очень хорошо учат контекст, потому что захватывают несколько кадров целиком и сразу же их обрабатывают.

  • Демонстрируют очень хорошее качество распознавания.

Можно ли использовать точки

В AR/VR гарнитурах часто используется распознавание жестов с помощью точек. Но распознавание точек не подходит для нашего решения.

Мир будущего: управление устройствами с помощью жестов - 8

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

Преимущества распознавания жестов по точкам:

  • есть вся информация о руке, положении каждого пальца;

  • высокая скорость работы.

Недостатки распознавания жестов по точкам:

  • высокая вычислительная стоимость;

  • требуется дополнительная обработка;

  • склеивание точек;

  • дрожание точек;

  • нужен размеченный датасет.

Решение с распознаванием точек — двухстадийное, поэтому предсказанные точки нужно дополнительно обрабатывать, отсюда высокая вычислительная стоимость. Ещё, если вы находитесь далеко от камеры, точки склеиваются и дрожат, а значит а для обучения детектора точек нужен дорогой размеченный датасет. Размечать 20 точек на кисти руки — дорого, поэтому от данного решения отказались.

Почему выбрали детектор

Мы выбрали детекторы, так как хотели, чтобы решение было максимально быстрым, лёгким и умело находить маленькие объекты. У детекторов есть проблема — одни хорошо находят большие объекты, другие — маленькие. Мы хотели, чтобы на любом адекватном расстоянии человека от камеры его рука распознавалась детектором.

Есть два типа детекторов: двухстадийные и одностадийные.

Двухстадийные: R-CNN, Fast R-CNN, Faster R-CNN, RetinaNet, детекторы на основе трансформеров DETR.

Плюсы:

  • Высокая точность.

  • Большая гибкость и адаптивность — могут адаптироваться практически под любое разрешение, особенно трансформерные архитектуры.

  • Хорошая локализация объектов.

Минусы:

  • Медленная работа, так как двухстадийные детекторы состоят из backbone и детекционной регрессионной головы.

  • Высокая вычислительная стоимость. В зависимости от того, какой backbone вы будете использовать, зависит ваше качество и вычислительная стоимость.

  • Сложность обучения. Хоть вы и учите детектор как единую сеть, это происходит по отдельности: сначала обучается backbone для распознавания карт признаков на изображении, и только потом детектор.

Одностадийные: SSD, YOLO, CenterNet (немного отличается от остальных).

Плюсы:

  • Высокая скорость, так как детекционная голова интегрирована в backbone.

  • Меньшая вычислительная стоимость, поскольку нужно проводить меньше операций.

  • Достаточно хорошая локализация.

Одностадийные детекторы используют так называемые якоря. Это когда на карте признаков ищется изображение, например, вытянутое по вертикали, по горизонтали, квадратное и так далее.

Минусы:

  • Меньшая точность, так как они привязаны к якорям.

  • Сложности с маленькими объектами. Одностадийные детекторы обрабатывают несколько карт признаков, идущих по порядку. Например, на больших картах признаков они могут найти маленькие объекты, а на маленьких картах признаков — большие.

Кажется логичным выбрать детектор YOLO, ведь есть YOLO Tiny, который весит всего 10 мегабайт. Но мы выбрали не его. Есть ещё один детектор, о котором мало кто слышал или использовал. Это RFBNet.

RFB (Receptive Field Block) 

RFBNet — одностадийный детектор, архитектура которого похожа на архитектуру SSD.

Мир будущего: управление устройствами с помощью жестов - 9

Вверху архитектура стандартного SSDNet, внизу — RFBNet. Отличаются они только тем, что SSD обрабатывает карты признаков напрямую, предсказывает с больших карт признаков маленькие объекты, а с маленьких — большие. В RFBNet после получения карт признаков встраивается дополнительный блок.

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

Например, если ваш коллега пришел в новом стиле, вы можете рассмотреть:

  1. Маленький объект.

  2. Средний, включая маленький.

  3. Весь объект целиком, включая маленький и средний.

Авторы интегрировали эти три понятия в архитектуру RFB-блока. Он состоит из трёх параллельных свёрток, расположенных от меньшего размера к большему. Это позволяет нам искать на большой карте признаков объекты разных размеров: маленькие, средние и большие. Затем все три карты признаков объединяем, и на их основе получаем итоговую хит-мапу объекта.

Мир будущего: управление устройствами с помощью жестов - 10

На схеме выше видно, что на большой карте признаков найден маленький объект с большим количеством информации в центре. Если бы это был большой объект, то было бы наоборот — самая большая карта признаков была бы самой яркой, а самая маленькая — более тусклой.

Интеграция такого блока позволила увеличить количество якорей, то есть bounding-боксов, которые можно предсказать.

Характеристики:

Первый conv:

3✕320✕240 → 64✕160✕120

Параметры:

3929924

Количество боксов:

13260

Вычислительная сложность (MFLOPs):

696

Скорость работы:

24 мс

Вес onnx модели:

11 мб

На первом conv-слое три карты признаков превращаем в 64. Кажется, что это много. Но мы решили немного ужать эту сеть и сделать уменьшенную версию. Вот что получилось:

Исходная модель

Уменьшенная версия

Первый conv: 3✕320✕240 →64✕160✕120

16✕160✕120

Параметры: 3929924

272000

Количество боксов: 13260

4420

Вычислительная сложность (MFLOPs): 696 

53.7

Скорость работы: 24мс

4мс

Вес onnx модели: 11мб

1,1мб

Вместо 64 карт-признаков сделали 16. Карт признаков стало меньше, а значит, якорей будем предсказывать тоже меньше. Количество якорей уменьшилось в три раза, а количество параметров — более чем в 10 раз. Также снизилась вычислительная сложность. Скорость работы составила 4 мс на Intel CPU при тесте на Macbook. Вес onnx модели — 1,1 мб. Кажется, это хороший результат.

Препроцессинг данных

Датасет HaGRID состоит из статичных жестов, и в основном все жесты направлены вертикально. Но нужно, чтобы модель выучила разные вариации. Для этого мы переворачивали изображения, искажали их, обрезали, использовали Cutmix и Mixup, mosaic аугментацию из YOLO и другие аугментации. Это было нужно, чтобы модель находила несколько рук на изображении.

Мир будущего: управление устройствами с помощью жестов - 11

Обучали нашу модель три эпохи. Училась модель два дня на одной видеокарте RTX 3080 TI. В результате получили метрику mAP (Mean Average Precision) 65%, что достаточно хорошо. По сравнению с этой моделью, YOLO-Tiny показывает mAP 72%, YOLO-Tiny больше примерно в 3−4 раза.

Мир будущего: управление устройствами с помощью жестов - 12

На демо выше детектор обрабатывает и находит руки, причём достаточно быстро и хорошо.

Классификатор

Обнаружив руки, нам нужно предсказывать, какие жесты они показывают. Кажется, можно предсказывать классы детектором. Но нет — ведь когда вы учите детектор на поиск жестов, он будет находить только жесты, а нам нужно находить руки даже если классификатор не отработает.

При выборе классификатора использовали следующие критерии:

  • легче детектора;

  • работает быстрее детектора;

  • простая архитектура.

Существует много вариаций классификаторов. Например, у ResNet достаточно тяжелая и сложная архитектура для конвертации и интеграции в слабые устройства.

Тогда провели ресёрч и выписали все backbones, которые смогли найти, а их — огромное количество:

Мир будущего: управление устройствами с помощью жестов - 13

Удалим из списка все трансформерные архитектуры, потому что они слишком тяжёлые. Следующим шагом убрали большие вариации архитектур моделей, так как они много весят и вычислительно сложные. После этого убрали все глубокие версии ResNet и VGG моделей. Затем пришлось отказаться и от ResNet, и от VGG, оставив только самые лёгкие — это две версии MobileNet и две версии Mobileone.

Мир будущего: управление устройствами с помощью жестов - 14

Но согласно нашим критериям, вес классификатора должен быть меньше веса детектора. К сожалению, ни одна из оставшихся моделей этому не соответствует.

На этом моменте у меня опустились руки, я отложил задачу и пошёл показывать тимлиду результаты работы детектора.

Мир будущего: управление устройствами с помощью жестов - 15

Оказалось, что детектор работает достаточно хорошо на расстоянии 2−3 метров. После этого мне предложили челлендж попробовать сделать классификатор на 200 килобайт, а значит в нём должно быть минимальное число параметров.

Мир будущего: управление устройствами с помощью жестов - 16

Действительно, наши кропы достаточно маленького размера — примерно в пять раз меньше исходного изображения. Но я был в шоке, когда мне предложили обучить LeNet, придуманный Яном Лекуном еще в 1998 году. Эта архитектура позволяла предсказывать рукописные цифры на датасете MNIST. Думаю, у всех, кто сталкивался с компьютерным зрением, это была самая первая модель. Исходная LeNet предназначалась для работы с одноканальными изображениями, а у нас RGB – 3 канала. Этот факт ломает всю архитектуру, но я задумался и решил дать шанс этой модели.

LeNet

Посмотрим на исходную версию.

Мир будущего: управление устройствами с помощью жестов - 17

Я имплементировал модель на Torch as is, как описал Ян Лекун в статье. Она стоит из двух свёрточных слоев, трёх линейных слоев, функции активации, пулингов. Выглядит очень старой, такие модели сейчас просто не пишут. Поэтому я решил её немного освежить. Вычислительно сложную функцию тангенса заменить привычным relu.

Мир будущего: управление устройствами с помощью жестов - 18

Average pooling размазывают градиенты по всему тензору. Заменим его на max pooling, чтобы давать больше информации о признаках, которые выучивает модель, и давать им больший вес во время обучения.

Мир будущего: управление устройствами с помощью жестов - 19
Мир будущего: управление устройствами с помощью жестов - 20

Кропы

У нас есть кропы рук. Мы знаем, что исходное разрешение LeNet 32 на 32 — достаточно маленькое, поэтому нам нужно было немного увеличить архитектуру. Также мы знали, что в нашем кропе больше 70% — это рука.

Мир будущего: управление устройствами с помощью жестов - 21

LeNet Hand

Давайте перепишем это на LeNet. Первый conv-слой мы заменили. Вместо одного канала приняли три и сделали stride 2, чтобы немного сжать размер входного изображения. После этого добавили два новых conv-слоя, которые принимают 6 и 16 каналов со stride 2 в первом случае. Во втором случае с ядром свёртки 3, чтобы забирать больше информации.

Добавили три линейных слоя, первый из которых принимал flattened-features из conv-слоя, а последующие два линейных — исходные (как было в LeNet).

Полная архитектура модели LeNet Hand выглядит так:

Мир будущего: управление устройствами с помощью жестов - 22

Архитектура работает так: мы взяли 18 статичных жестов из датасета HaGRID и добавили три новых жеста (рука вправо, влево, вниз). Затем обработали кропы батчем. Так как детектор может обнаруживать несколько рук, мы объединяли их в одну группу, превращали в батч и передавали на вход модели LeNet.

Чтобы обучить LeNe, брали кропы и поворачивали их, аугментируя. Далее добавляли искажения, так как предсказание детектора — это вытянутые bounding-боксы, и когда мы их сжимали в квадрат, рука немного искажалась.

Модель обучалась три часа на видеокарте RTX 3080 TI. Обучение продолжалось менее одной эпохи, потому что объём данных был огромный. В результате получили метрику на тестовой выборке 97% по F1, что достаточно хороший результат.

Сравним модели:

RFB Hand Detection

  • Параметры: 272000

  • MFLOPs: 53,7

  • Скорость работы: 4мс

  • Вес onnx модели: 1,1мб

RGB Hand LeNet

  • Параметры: 34942

  • MFLOPs: 1,21

  • Скорость работы: < 1мс

  • Вес onnx модели: 140кб

У RFB-детектора скорость работы 4мс, вес 1,1 мб, а у LeNet скорость работы менее миллисекунды, а вес onnx-модели даже меньше, чем запрашивал тимлид — 140 кб.

Я потратил 5 часов на написание сети и её обучение, показал результаты, а оказалось…

Мир будущего: управление устройствами с помощью жестов - 23

Что это была шутка!

Не делайте так никогда со своими подчинёнными, пожалуйста. Но в результате шутка выстрелила, и в нашем решении используется именно эта архитектура.

Демо работы классификатора выглядит так:

Мир будущего: управление устройствами с помощью жестов - 24

 

На таком расстоянии неплохо распознаются все статичные жесты, которые обрабатываются в батче.

Но есть проблемы:

  • Детектор теряет руки: руки могут показываться в такой форме, которую детектор никогда не видел, и тогда их можно потерять.

  • Мы не знаем, какая рука показывает какой жест.

  • Много рук в кадре и к каждой нужно привязать жест, если он есть.

Подобные проблемы решает трекинг — это задача, которая стоит параллельно с задачей детекции объектов на видео.

Трекинг

В основе всех трекингов лежит Kalman Filter.

Мир будущего: управление устройствами с помощью жестов - 25

Этот алгоритм позволяет сгладить пространство точек и предсказывать будущие точки относительно поведения предыдущих.

Я взял Sort-трекер. У него есть три вариации: Sort, DeepSort и OC-SORT:

  1. Sort быстрый, простой, но, к сожалению, теряет объекты. Когда один объект попадает за второй, он просто исчезает, а когда появляется заново, ему присваивается новый ID.

  2. DeepSort — это небольшая модификация, когда мы используем вместе с простым трекингом ещё и информацию об отлеживаемом объекте. Он тяжелее, чем простой Sort, так как использует нейронную сеть для получения уникальных признаков объекта.

  3. OC-Sort — такой же, как Sort, но решает проблему потери рук. Когда один объект попадает за второй, он не исчезает, не удаляется из очереди, а ему присваивается небольшая вероятность того, что он действительно находится за другим объектом. После того как он появится, ему присваивается тот же самый ID, какой и был.

Плюсы и минусы трекеров:

Sort

  • Быстрый

  • Простой

  • Теряет объекты

DeepSort 

  • Медленный

  • Сложный

  • Нужна нейросеть

OC-SORT

  • Быстрый

  • Сложный

  • Умеет ждать

Мы взяли за основу OC-SORT, так как он лучший, а у нас некоторые объекты могут пересекаться и выходить за рамку изображения.

Демо работы трекера выглядит так:

Мир будущего: управление устройствами с помощью жестов - 26

Когда руки выходят за кадр и возвращаются, им присваивается тот же ID, что и был.

Итоговое решение

Допустим, у нас есть очередь предсказаний на каждую руку. Мы нашли ладошки, присвоили им определённые классы.

Мир будущего: управление устройствами с помощью жестов - 27

None означает, что классификатор не отработал, но мы не потеряли руку.

Теперь валидируем динамический жест. Проверка начинается после нахождения первого класса (если он является частью динамического жеста). Мы встретили класс left, теперь нам нужно понять, был ли до этого right. Нет, не было? Значит, идём дальше.

Мы нашли right? Тогда теперь задаём вопрос, а был ли до этого left? Если был, то ставим счётчик повторений 1.

На следующем предсказании right ставим счётчик повторения 2 и тогда можем сказать, что это был жест свайп вправо, так как до этого рука показывала лево, а теперь — право.

Далее валидируем скорость.

Мир будущего: управление устройствами с помощью жестов - 28

Слишком долго показывать жест нельзя, это плохо, и слишком быстро — тоже. Скорость — это константное значение n кадров от начального и конечного класса.

Валидируем границы.

Мир будущего: управление устройствами с помощью жестов - 29

На изображении выше — плохой свайп. Нам нужно, чтобы он был горизонтальным или вертикальным, а не по диагонали. Поэтому найдём bounding-боксы рук и поставим им ограничение сверху и снизу.

Вот это хороший свайп:

Мир будущего: управление устройствами с помощью жестов - 30

В ограничивающей рамке должен быть начальный и конечный жест.

Итоговое решение я вынес в небольшую блок-схему:

Мир будущего: управление устройствами с помощью жестов - 31

Есть кадр, который мы передаём на вход детектору, получаем bounding-боксы, предсказываем по ним классы, и далее передаём в трекер. Трекер предсказывает ID для каждой руки.

После этого проверяем, а была ли очередь для этой руки. Если очереди не было, мы её создаём. Если очередь уже есть, то добавляем в неё класс с нашим жестом. Далее проверяем сам жест — был ли до этого left или right. Если нет, просто выходим и начинаем искать следующий жест. Если был, то проверяем скорость показа. Если скорость показа не валидна, просто очищаем очередь, потому что следующие проверки будут также невалидны. Далее проверяем границы жестов. Если они невалидны, опять очищаем очередь.

Если всё прошло, мы предсказываем, что это динамический жест — круто, action! — и очищаем очередь. Для данной руки у нас больше классов нет.

Мир будущего: управление устройствами с помощью жестов - 32

Результат

Минусы решения, которое получилось:

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

  • Чёткая инструкция для пользователя.

  • Пользователь должен привыкнуть использовать наше решение.

  • Небольшое количество жестов, так как динамика состоит из начального и конечного жеста. На самом деле можно пофантазировать, но в любом случае есть конечное количество динамических жестов.

Плюсы:

  • Скорость работы. Общее решение обрабатывает один кадр за 4 мс.

  • Мало весит. Общий вес всех моделей и кода 1,5 мб.

  • Можно это решение написать даже на C и интегрировать куда угодно.

  • Отсутствуют false positive, присущие моделям. Невозможно получить false positive, потому что условия — константные, вероятностей здесь нет.

  • Предсказуемая работа.

Что можно улучшить?

  • Добавить больше жестов.

  • Добавить больше эвристик.

  • Предсказывать поворот руки, например, градус наклона руки.

  • Сделать гибрид на основе распознавания точек и самих bounding-боксов.

Мы релизим весь код и все модели нашего решения в OpenSource.

Вы можете подписаться на телеграм-канал нашей команды и следить за новостями. Вот ссылка на репозиторий, где можно скачать решение, попробовать его и поконтрибьютить.

Автор: nagadit

Источник

Рейтинг@Mail.ru
Rambler's Top100