company_banner

Виртуальная примерочная в OpenCV

    Было ли у вас такое, что в интернет-магазине понравилась какая-нибудь вещь, но не хочется покупать ее, не примерив? Конечно, в некоторых магазинах есть возможность примерить одежду после заказа перед оплатой. Однако по статистике каждый год доля онлайн-заказов в интернет-магазинах одежды и обуви растет, но также растет и доля возвратов, она составляет 50–70% — это огромные затраты на логистику, которые можно будет значительно сократить, используя онлайн-примерочную. Представьте, вы загружаете свою фотографию, выбираете одежду и она переносится на ваше изображение. Уже существуют виртуальные примерочные обуви, они работают достаточно успешно. Некоторое время назад нас заинтересовала эта тема, как обстоят дела с одеждой? Такие работы тоже существуют, но успешных гораздо меньше, во многих из них кроме статьи найти ничего не удается, о рабочем примере остается только мечтать. Мы решили исправить это и поддержать одну из сетей в библиотеке OpenCV. Что из этого вышло можете увидеть в virtual_try_on.py sample.



    Результат не идеален, но в данной области считается достаточно хорошим.


    Хотите узнать как работает виртуальная примерочная и с какими сложностями мы столкнулись при интеграции модели в OpenCV — добро пожаловать под кат!


    Мы выбрали нейронную сеть 2019 года CP-VTON с достаточно хорошим визуальным результатом. По сравнению с аналогами CP-VTON интересна тем, что она достаточно точно может передать форму одежды в сгенерированном изображении, а также при передаче не теряются особенности одежды (рисунок, логотип и текстура). Некоторые аналогичные сети используют 3D модели человека, для получения которых требуется 3D-сканер. Этот факт существенно сокращает области применимости модели. Также большим плюсом является наличие кода на github. Но авторы CP-VTON в репозитории приводят только скрипты для тренировки и тестирования, нет никакой демки, чтобы попробовать сеть на своих фотографиях.


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


    Принцип работы


    Модель CP-VTON состоит из двух подмодулей: GMM (Geometric Matching Module) — модуль для деформации одежды в соответствие с позой человека и TOM (Try-On Module) — модуль для переноса деформированной одежды на изображение человека.


    Для тренировки GMM требуется тройка $(I, c, c_t )$, а для TOM — $(I, \hat{c}, I_t)$, $I$ — изображение человека, $c$ — изображение одежды, $\hat{c}$ — выход GMM, $I_t$ — ground truth (человек в одежде $c$), $c_t$ — ground truth (деформированная одежда, получается наложением маски одежды на $I_t$). Но такие тройки достаточно тяжело собрать, обычно есть только $I_t$ и $c$. Решение этой проблемы предлагалось в одной из предыдущих работ VITON, авторы статьи вместо $I$ на вход сети подавали тензор с описанием человека $p$. При создании $p$ преследовалась цель максимального сохранения информации о человеке, включая лицо, волосы, форму тела и позу, но минимизируя влияние старой одежды (цвет, форма, текстура). Этот подход позволяет не показывать сети эталонное изображение человека в желаемой одежде. Таким образом, GMM принимает на вход $(p, c)$ и генерирует деформированную одежду $\hat{c}$, которая во время тренировки сравнивается с $c_t$. TOM принимает на вход $(p, \hat{c})$, а выход сети $I_o$ сравнивается с $I_t$.


    Рассмотрим поподробнее формирование тензора $p$. Описание человека состоит из маски тела человека, ключевых точек позы и изображения головы. Сначала по фотографии выполняется поиск ключевых точек с помощью сети OpenPose. Для получения бинарной маски тела человека используется семантическая сегментация одежды и частей тела. Эта задача решается с помощью модели LIP_JPPNet. В процессе ее поддержки в OpenCV добавился (самостоятельный) sample human_parsing.py.


    Для маски тела берутся все ненулевые пиксели, для маски головы — пиксели, соответствующие классам: лицо, солнцезащитные очки, головной убор и волосы. Затем маска тела размывается, а маска головы накладывается на входное изображение.


    preprocessing


    Рассмотрим работу модулей.


    GMM используется для изменения формы желаемой одежды в соответствие с позой человека. В процессе работы сеть извлекает ключевые признаки из изображения одежды и тензора с описанием человека независимо, нормирует признаки, используя $L_2-норму$. Вычисляется корреляция признаков и выполняется прогнозирование вектора параметров пространственного преобразования. По нему в результате постобработки генерируется сетка деформации исходного изображения одежды. С помощью билинейной интерполяции одежды по узлам сетки получается форма одежды, соответствующая позе человека. Для сравнения полученной деформации одежды $\hat{c}$ с $c_t$ используется $L_1-loss$.


    gmm


    Модуль TOM переносит деформированную одежду на изображение с человеком. Сеть базируется на архитектуре Unet. Данная архитектура была выбрана, потому что при непосредственном переносе деформированной одежды на изображение человека результат выглядит неестественно на стыках и местах соприкосновения человека и одежды. Unet позволяет избавиться от этого, делая переходы более плавными. Однако невозможно идеально выровнять одежду по фигуре, поэтому даже незначительное смещение одежды относительно позы может сделать выход Unet размытым. Сеть представляет собой последовательность сверток и пулингов, которые уменьшают пространственное разрешение картинки. А потом разрешение увеличивается последовательностью Upsample и сверток. Кодировщик (encoder) в сети является предварительно обученным VGG-19. Чтобы выходное изображение не было слишком размытым используется следующий подход. Unet предсказывает образ человека $I_r$ и композиционную маску $M$. Затем маска комбинируется с деформированной одеждой $c$ и накладывается на $I_r$, скомбинированный с обратной маской $(1 - M)$ и получается результирующее изображение $I_o$.


    $I_o=M\odot c + (1 - M)\odot I_r$


    $\odot$ — знак поэлементного матричного умножения.


    tom


    При обучении сети главная цель Try-On модуля — минимизация несоответствия $I_o$ с $I_t$. При попиксельном сравнении результата работы сети и изображения из разметки малое значение функции потерь будет соответствовать размытым изображениям. А похожим с точки зрения человека изображениям, но с небольшими сдвигами, будет соответствовать большое значение функции потерь. Чтобы избежать этих недостатков используют комбинацию $L_1- loss$ с perceptual loss. Картинка из разметки и предсказание сети пропускаются через несколько слоев сети VGG и сравниваются полученные нейросетевые признаки, которые будут инвариантны к небольшому изменению положения в пространстве. Также VGG чувствительна к резким изменениям яркости пикселей — это позволяет устранить размытость в изображении, сделав большим значение функции потерь на таких картинках.


    Реализация в OpenCV


    Для работы сети кроме изображения человека и одежды также требуется изображение с семантической сегментацией частей тела человека и одежды на нем и файл с ключевыми точками тела человека. Авторы статьи использовали json файлы с сохраненными точками тела человека, полученные с помощью сети OpenPose из Caffe, и изображения с сегментацией из разметки датасета LIP.


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


    python3 virtual_try_on.py -i person_img.jpg -c cloth.jpg

    При портировании сети в OpenCV нужно было убрать всю предобработку через PIL. Тут возникли чудеса с сегментацией. В датасете LIP представлены полутоновые изображения, где интенсивность пикселя соответствует классу объекта. А при скачивании датасета для CP-VTON эти же изображения уже цветные. Читается трехканальное изображение, но потом выполняется работа с одноканальными изображениями:


    shape = (segm > 0).astype(np.float32)
    head  = (segm == 1).astype(np.float32) + \
            (segm == 2).astype(np.float32) + \
            (segm == 4).astype(np.float32) + \
            (segm == 13).astype(np.float32)
    cloth = (segm == 5).astype(np.float32) + \
            (segm == 6).astype(np.float32) + \
            (segm == 7).astype(np.float32)

    Оказывается, что PIL может хранить цветное изображение и кодировку, с помощью которой оно было получено. Перевод лейблов в цвета лежал в human_colormap.mat. Как их оттуда прочитать? Matlab ставить не хотелось. К счастью, scipy имеет загрузчик таких файлов. Потратив немного (много) времени на эти махинации удалось извлечь из сегментации маску тела и головы.


    Дальше – больше. Для размытия маски ее сначала уменьшают в 16 раз, затем возвращают исходные размеры.


    mask = mask.resize((width // 16, height // 16), Image.BILINEAR)
    mask = mask.resize((width, height), Image.BILINEAR)

    Получаем маску в оттенках серого. Для этого используют билинейную интерполяцию. Кажется, что может быть проще при переносе в OpenCV. Заменили PIL resize на cv.resize и получили совсем другой результат.



    Слева маска, полученная через PIL resize, справа — cv.resize.


    Маски получились вообще не похожи, а результат работы сети? В общем смотрите сами.


    mask_pil mask_ocv


    Слева результат, полученный при использовании маски с PIL resize, справа — cv.resize.


    Выглядит грустновато, не хотелось бы так уродовать людям руки. Что же это за билинейная интерполяция такая? Изрядно погуглив, так и не удалось ничего найти. Оставался последний, но верный способ – смотреть исходники на С. Там удалось увидеть, что bilinear resize вовсе не похож на bilinear, а скорее area. Размер ядра зависит от scale factor, в нашем случае ядро имело размер 33 = 16 * 2 + 1, в то время как в OpenCV ядро фиксированного размера – 3. Одновременно с этим мы заметили, что значения пикселей отличаются только при уменьшении размеров, а при восстановлении результаты получаются достаточно близки. Но изменение типа интерполяции все равно не помогло добиться нужного результата. Смотрим дальше. Коэффициенты интерполяции считаются очень странным образом, получаются не симметричны. Это нам уже никак не удавалось смоделировать. Поэтому пришлось написать небольшой класс с таким типом интерполяции:


    Реализация билинейной интерполяции из PIL
    class BilinearFilter(object):
        """
        PIL bilinear resize implementation
        image = image.resize((image_width // 16, image_height // 16), Image.BILINEAR)
        """
        def _precompute_coeffs(self, inSize, outSize):
            filterscale = max(1.0, inSize / outSize)
            ksize = int(np.ceil(filterscale)) * 2 + 1
    
            kk = np.zeros(shape=(outSize * ksize, ), dtype=np.float32)
            bounds = np.empty(shape=(outSize * 2, ), dtype=np.int32)
    
            centers = (np.arange(outSize) + 0.5) * filterscale + 0.5
            bounds[::2] = np.where(centers - filterscale < 0, 0, centers - filterscale)
            bounds[1::2] = np.where(centers + filterscale > inSize, inSize, centers + filterscale) - bounds[::2]
            xmins = bounds[::2] - centers + 1
    
            points = np.array([np.arange(row) + xmins[i] for i, row in enumerate(bounds[1::2])]) / filterscale
            for xx in range(0, outSize):
                point = points[xx]
                bilinear = np.where(point < 1.0, 1.0 - abs(point), 0.0)
                ww = np.sum(bilinear)
                kk[xx * ksize : xx * ksize + bilinear.size] = np.where(ww == 0.0, bilinear, bilinear / ww)
            return bounds, kk, ksize
    
        def _resample_horizontal(self, out, img, ksize, bounds, kk):
            for yy in range(0, out.shape[0]):
                for xx in range(0, out.shape[1]):
                    xmin = bounds[xx * 2 + 0]
                    xmax = bounds[xx * 2 + 1]
                    k = kk[xx * ksize : xx * ksize + xmax]
                    out[yy, xx] = np.round(np.sum(img[yy, xmin : xmin + xmax] * k))
    
        def _resample_vertical(self, out, img, ksize, bounds, kk):
            for yy in range(0, out.shape[0]):
                ymin = bounds[yy * 2 + 0]
                ymax = bounds[yy * 2 + 1]
                k = kk[yy * ksize: yy * ksize + ymax]
                out[yy] = np.round(np.sum(img[ymin : ymin + ymax, 0:out.shape[1]] * k[:, np.newaxis], axis=0))
    
        def imaging_resample(self, img, xsize, ysize):
            height, width, *args = img.shape
            bounds_horiz, kk_horiz, ksize_horiz = self._precompute_coeffs(width, xsize)
            bounds_vert, kk_vert, ksize_vert    = self._precompute_coeffs(height, ysize)
    
            out_hor = np.empty((img.shape[0], xsize), dtype=np.uint8)
            self._resample_horizontal(out_hor, img, ksize_horiz, bounds_horiz, kk_horiz)
            out = np.empty((ysize, xsize), dtype=np.uint8)
            self._resample_vertical(out, out_hor, ksize_vert, bounds_vert, kk_vert)
            return out

    Результаты


    В общем получился достаточно интересный пайплайн из 4 сетей, полученных из разных фреймворков, но дружно работающих в OpenCV. При тестировании сети на реальных фотографиях мы заметили несколько особенностей сети. В тренировочном датасете были только изображения девушек, поэтому сеть не всегда хорошо работает с мужчинами. Руки у них становятся более женственные, а переход от головы к телу имеет очень яркую границу. Также для получения качественного результата нужно загружать фото с соотношением сторон пропорциональных 256 на 192, иначе сеть прибавит пару килограммов. Мы добавили кадрирование исходного изображения в sample до получения нужного соотношения между шириной и высотой. Лучше чтобы кроме человека ничего не было, а одежда на изображении должна находиться на белом фоне без каких-либо узоров и надписей, иначе эти части тоже будут восприниматься как одежда.


    P.S. В мае 2020 года OpenCV отмечает свое 20-летие. Мы планируем в этом году писать больше статей и придумывать другие активности. Следите за новостями!

    Intel
    Компания

    Комментарии 15

      +2
      Реализация хорошая, а идея бред. Все равно на человеке оно будет по другому смотреться и толку от такой «примерки» ноль.
        +4
        Навеяло:
        — Мы сделали автоматическую парикмахерскую машину!
        — А как Вы решили проблему — ведь у всех людей головы разные?
        — Ну-у, это только первые пару раз…

        Всерьёз:
        Обратите внимание на историю с глажкой одежды. Раньше всё гладили, женщины старшего поколения до сих пор гладят и трусы, и носки. Но пришли стиральные машины-автоматы, цепочка ручной работы стирка-глажка порвалась, и сейчас миллионы людей ходят в неглаженном. Популярность получают ткани и одежда, внешний вид которых от этого не особо страдает.
        Так будет, вероятно, и с этой технологией: выбор одежды уже уходит, а потом массово уйдёт в инет-магазины, и соответственно (с понятным запаздыванием) одежда изменится так, что мелкие ошибки такого подбора не будут портить жизнь. Ну и, конечно, сами методы станут лучше. Не так давно 3d-принтеры были суперэкзотикой, а сейчас у пацанвы по домам стоит их множество. Потому очень легко представить и 3-d сканер, достаточный для формирования скана фигуры, отсылаемого в инет-примерочную.
        +1
        Реализация хорошая

        Спорный вопрос. Ставлю себя на место простого покупателя, после такой примерки я бы ничего покупать не захотел.
          +1

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

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

            Ну, и: пока что результат, конечно, ужасен, простая аппликация и то лучше выглядит :-)
            Пока не будет распознавания фигуры, на каком-бы то ни было слабом уровне, не будет толку. Но лиха беда начало.
              0
              Пока не будет распознавания фигуры, на каком-бы то ни было слабом уровне, не будет толку.

              А еще достоверной информации по каждому предмету одежды — как он может деформироваться и как деформируется при тех или иных обстоятельствах.
                0
                Вот да. Всякие диснеи уже каждый волосок шевелят, а тут как
                image
                Но, ещё раз скажу: лиха беда начало. Надеюсь, всё получится и желаю успехов команде.
                  0
                  Надеюсь, всё получится и желаю успехов команде.

                  Препятствий к тому чтобы сделать вроде бы нет, но практической пользы, мне кажется, не будет. На каждый предмет одежды нужно будет делать цифровую модель с физикой деформаций, что будет крайне трудозатратно :)
                  Как пример — один сарафан может растягиваться в талии без изменений своего вида (эластичная ткань), а другой в принципе не тянется и расширять его изображение под фигуру «где будем делать талию» совершенно бессмысленно. Третий вариант тянется, но при этом меняет свой вид — распрямляются складки на резинке в поясе, например. И на моделях разной комплекции он будет выглядеть совсем по-разному. И подобных нюансов с разной одеждой, я уверен, просто море :)
                    0
                    Да не требуется моделей молекулярного уровня, а то, что Вы пишете о сарафанах — называется культура, то есть груда всяких мелочей, которые нужны для цельность технологии. А культура — нарабатывается, не возникает сразу на ровном месте. И, если ждать, пока оно будет — опоздаешь, отстанешь ото всех.

                    Собственно, я начал уходить в абстракции, это значит, что ничего нового уже не скажу. Потому спасибо за общение, но мне пора выходить.
          +1

          Почему это называется "примеркой"? Как будто авторы никогда не видели ни людей, ни одежды…
          Люди, вообще-то, трёхмерные, и простой сборкой двумерных коллажей, чтобы показать, как будет "сидеть" одежда, тут не обойтись. Не говоря уже об удобстве.


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

            0
            На этом этапе это поможет клиенту понять: подходит ли цвет одежды под лицо, в целом как сидит подобный формат на тебе.
            На следующем этапе уже будут делать физику материала и загрузку параметров фигуры клиента (новые поля в Личном кабинете :) )
            Нельзя же сразу все сделать офигено. Как дети малые
            0
            Спасибо огромное за статью. Моя дипломная тоже была на эту тему, но только без нейронных сетей.
            Пробовали производительность на мобильных?
              0
              На мобильных не пробовали, но пока и на CPU есть проблемы. Сегментационная сеть LIP_JPPNet показывает очень хорошее качество, но работает крайне медленно. На intel core i5 запуск пайплайна отрабатывал за 22 секунды, из них 21 с работала сегментация. Пробовали запускать на OpenCL FP16 сегментацию, при повторном запуске получали 8 секунд, используя cl_cache.
                0
                Когда же наступит это будущее, когда можно покупать одежду не в «магазине готового платья», а сшитое на заказ, по твоей фигуре, и при этом дёшево?
                «Джентльмену не пристало ходить к мастеру, мастер должен приходить к джентльмену».(с)Конан Дойл.
                  0

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

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

                  Самое читаемое