Всем привет! Однажды передо мной встала задача сделать фон картинки прозрачным, без мам, пап и бекндов... Задача есть, надо её решать! В этой статье вы узнаете, с чем мне пришлось столкнуться и как я реализовал вырезание заднего фона у фотографий в приложении на iOS.

Зачем вообще вот это вот всё ?

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

Пример обрезания фона с использованием GPUImage
Пример обрезания фона с использованием GPUImage

Как можно заметить, присутствует очень много "снега" и артефактов.
Решено, внедряем машинное обучение !

Ищем способ удаления заднего фона

На просторах интернета есть хорошие туториалы, описывающие удаление фона с использованием модели машинного обучения DeeplabV3, представленной на официальном сайте Аpple. Только есть одно но. Данная моделька распознаёт ограниченный набор натренированных объектов и если скормить ей картинку с кроссовками или сумкой, она просто не распознает объекта на ней.

Продолжительный гуглинг открыл для меня прекрасную модель под названием u2Net. Данная модель очень качественно сегментирует фотографию на объекты, расположенные на ней. Но на страничке гитхаба модельки лежат в неведомом формате .pth, а ведь наш родной CoreML принимает только формат .mlModel, что делать ?
Есть два пути:

  1. Используем официальную python тулзу для конвертации моделей в .mlModel формат

  2. Заходим на страничку гитхаба, где сконвертировали всё за вас

Выбор очевиден ????

Внедряем U2Net в своё приложение

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

Модель машинного обучения среди файлов
Модель машинного обучения среди файлов

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

Расположение названия класса
Расположение названия класса

Наконец-то настало время попрограммировать. Разминаем пальчики !

Давайте, для удобства сделаем функцию, которая будет присутствовать у каждого экземпляра UIImage класса, что бы, если нам вздумается, мы могли вырезать фон где угодно !! И забегая немного вперёд, уточню, что модель машинного обучения принимают изображения строго определённого размера, который описан на страничке модели во вкладке Predictions.

Размеры входного изображения
Размеры входного изображения
import UIKit

extension UIImage {
  func removeBackgroudIfPosible() -> UIImage? {
      let resizedImage: UIImage = resize(size: .init(width: 320, height: 320))
      ...
  }
}
Используемая функция resize
extension UIImage {
    func resize(size: CGSize? = nil, insets: UIEdgeInsets = .zero, fill: UIColor = .white) -> UIImage {
      var size: CGSize = size ?? self.size
      let widthRatio  = size.width / self.size.width
      let heightRatio = size.height / self.size.height
    
      if widthRatio > heightRatio {
        size = CGSize(width: floor(self.size.width * heightRatio), height: floor(self.size.height * heightRatio))
      } else if heightRatio > widthRatio {
        size = CGSize(width: floor(self.size.width * widthRatio), height: floor(self.size.height * widthRatio))
      }
    
      let rect = CGRect(x: 0,
                        y: 0,
                        width: size.width + insets.left + insets.right,
                        height: size.height + insets.top + insets.bottom)
    
      UIGraphicsBeginImageContextWithOptions(rect.size, false, scale)
    
      fill.setFill()
      UIGraphicsGetCurrentContext()?.fill(rect)
    
      draw(in: CGRect(x: insets.left,
                      y: insets.top,
                      width: size.width,
                      height: size.height))
      let newImage = UIGraphicsGetImageFromCurrentImageContext()
    
      UIGraphicsEndImageContext()
    
      return newImage!
    }
}

Так, картинку мы отресайзили под нужные нам размеры, что дальше, спросите вы ?
Дальше - магия машинного обучения и предсказание !!

import UIKit
extension UIImage {
    func removeBackgroudIfPosible() -> UIImage? {
        let resizedImage: UIImage = resize(size: .init(width: 320, height: 320))
        
        guard
            let resizedCGImage = resizedImage.cgImage,
            let originalCGImage = cgImage,
            let mlModel = try? u2netp(),
            let resultMask = try? mlModel.prediction(input: u2netpInput(in_0With: resizedCGImage)).out_p1 else {
            return nil
        }
        ...
    }
}

Давайте я опишу, получившийся, жирный guard и что там происходит.

  • resizedCGImage - У u2netpInput, есть несколько инициализаторов, мы будем работать с тем, что принимает cgImage;

  • originalCGImage - cgImage оригинального изображения, будем использовать далее для наложения маски;

  • mlModel - инстанс модели машинного обучения, инициализированный на основе сгенерированного класса;

  • resultMask - та самая магия! Функция prediction, возвращает нам u2netpOutput, где out_p1 можно использовать в качестве маски нашего изображения!

Если преобразовать получившуюся маску в картинку, то мы сможем увидеть то, как моделька обработала входящие картинки. Так как возвращаемая картинка имеет размер 320x320, не забудьте заресайзить её под исходные размеры.

let originalImage = CIImage(cgImage: originalCGImage)
var maskImage = CIImage(cvPixelBuffer: resultMask)
        
let scaleX = originalImage.extent.width / maskImage.extent.width
let scaleY = originalImage.extent.height / maskImage.extent.height
maskImage = maskImage.transformed(by: .init(scaleX: scaleX, y: scaleY))

return UIImage(ciImage: maskImage)
Маски исходных изображений
Маски исходных изображений

Вау! Выглядит очень аккуратно, нет снега и каки-то сильных вкраплений, давайте накладывать маску на оригинальную картинку !

Применяем получившуюся маску

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

let context = CIContext(options: nil)
        
guard let inputCGImage = context.createCGImage(originalImage, from: originalImage.extent) else {
    return nil
}
        
let blendFilter = CIFilter.blendWithRedMask()
        
blendFilter.inputImage = CIImage(cgImage: inputCGImage)
blendFilter.maskImage = maskImage
        
guard let outputCIImage = blendFilter.outputImage?.oriented(.up),
      let outputCGImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) else {
        return nil
}
        
return UIImage(cgImage: outputCGImage)

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

Используя эти не хитрые манипуляции, мы можем взглянуть на результат!

Хочется сказать только одно - ВАУ! О такой качественном обрезании мы могли только мечтать, а количество кода, которое пришлось написать, просто мизирное!

Финальная функция для обрезания фото у картинки
import UIKit
import CoreML
import CoreImage.CIFilterBuiltins

extension UIImage {
    func removeBackgroudIfPosible() -> UIImage? {
        let resizedImage: UIImage = resize(size: .init(width: 320, height: 320))
        
        guard
            let resizedCGImage = resizedImage.cgImage,
            let originalCGImage = cgImage,
            let mlModel = try? u2netp(),
            let resultMask = try? mlModel.prediction(input: u2netpInput(in_0With: resizedCGImage)).out_p1 else {
            return nil
        }
        
        let originalImage = CIImage(cgImage: originalCGImage)
        var maskImage = CIImage(cvPixelBuffer: resultMask)
        
        let scaleX = originalImage.extent.width / maskImage.extent.width
        let scaleY = originalImage.extent.height / maskImage.extent.height
        maskImage = maskImage.transformed(by: .init(scaleX: scaleX, y: scaleY))
        
        let context = CIContext(options: nil)
        
        guard let inputCGImage = context.createCGImage(originalImage, from: originalImage.extent) else {
            return nil
        }
        
        let blendFilter = CIFilter.blendWithRedMask()
        
        blendFilter.inputImage = CIImage(cgImage: inputCGImage)
        blendFilter.maskImage = maskImage
        
        guard let outputCIImage = blendFilter.outputImage?.oriented(.up),
              let outputCGImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) else {
            return nil
        }
        
        return UIImage(cgImage: outputCGImage)
    }
}

Всё круто, но как ты собираешься хранить такую тяжёлую модель ?

Модель машинного обучения u2Net, распространяется в двух версиях, u2Netp - light версия 4.6 мб и u2Net - full версия 175.9 мб . Разница между ними очень ощутима, если обратить внимание на их вес. Полную версию u2Net даже не получится хранить в гите, так что не советую вам комитить код с полной моделькой, если планируете его пушить на github. На результат работы light модели можно взглянуть ниже.

Обрезание фона с использованием Light версии модели
Обрезание фона с использованием Light версии модели

Мы же приняли решение хранить в приложении light версию модели, а большую, полную версию модели загружать в фоне, что позволило сохранить функциональность редактора, пока модель не загружена. Если вы захотите сделать тоже самое, Apple и тут о нас подумали и написали туториал !

Итог

Теперь мы умеем пользоваться моделями машинного обучения в iOS приложении и применять их на практике. Машинное обучение открывает море возможностей для распознания, сегментации, классификации, оценки глубины, обнаружения объектов и позы человека и это всё только про фото, а ведь можно ещё и работать с текстом !

Помимо сторонних моделей машинного обучения, iOS имеет встроенный фраемворк Vision, который, оперируя алгоритмами машинного зрения, поможет вам обнаружить лицо, кисть, штрихкод или человека!


Спасибо за прочтение! Надеюсь, что эта статья помогла вам решить аналогичную задачу или просто помогла узнать, как работать с ML моделями в iOS.

Используемые материалы

Список ML моделей от Apple

Тулза по конвертации моделей в mlModel

Конвертированные популярные ML модели

Статья Apple по применению сегментированной маски

Статья Apple по загрузки ML модели из сети

Статья по удалению фона с использованием DeeplabV3

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


  1. Alexrook
    16.12.2022 17:35

    О такой качественном обрезании мы могли только мечтать

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

    И вопрос по теме. А какое максимальное разрешение может обрабатываться такими моделями? Скажем 4000х2000 можно обработать? Есть ли зависимость качества обтравки и разрешения изображения?


    1. TheSwitch Автор
      16.12.2022 17:54

      С терминологией промахнулся, согласен :)
      Как и было описано в статье, разные ML модели принимают на вход изображения с разным разрешением, в данном случае - 320x320. Если Вам нужно обработать изображение размером 4000х2000, то разрешение нужно подогнать под требуемое.
      Вот только что попробовал обработать изображение с разрешением 7680  x  4320

      На выходе изображение имеет исходное разрешение

      При скейле картинки в редакторе потери качества не наблюдается


  1. Alexufo
    19.12.2022 03:04

    Отлично. А что потом? Теперь в интернет магазине превьюшки будут на 200 процентов тяжелее, потому что кроссплатформенную прозрачность поддерживает только png24?

    Такое нужно скорее чтобы залить фон на картинках одним цветом.


    1. TheSwitch Автор
      19.12.2022 10:30

      Не совсем понимаю, что Вы хотите услышать... приложение, для которого разрабатывался данный функционал достаточно специфично и распространяется исключительно на узкий круг лиц, где увеличение объёма приложение не является критичным.
      А что дальше ? Я считаю, что Вы сами должны себе ответить. Поговорите со своим беком, вынесите модель и процессинг изображения на сервер, тем самым уменьшив размер вашего клиента.

      А для чего это нужно ? Если вам хочется заливать фон, заливайте, хочется делать коллаж из объектов, которые представлены на фото, делайте. На официальной страничке гитхаба ML модели u2Net представлены способы её применения, среди которых даже есть фото редактор в iOS приложении.
      Хорошего дня !


      1. Alexufo
        19.12.2022 10:33

        Я как товары увидел. про веб и интернет магазины всю статью думал. Потом думал, а че тут на сфите код))Ввел в заблуждение.