Vue и Transformers.js для удаления фона

В связи с растущей тенденцией к созданию клиентских приложений ML и приложений, сохраняющих конфиденциальность, споры о "лучшем" методе удаления фона с изображений разгораются с новой силой. Для определения наилучшего метода необходимо изучить такие критерии, как скорость, стоимость, конфиденциальность и простота использования. С помощью Transformers.js теперь мы можем создавать инструменты для удаления фона с изображений, соответствующие всем этим критериям, в режиме реального времени, не полагаясь на удаленный сервер.

Это руководство не просто о создании инструмента; оно посвящено изучению ключевых концепций интеграции машинного обучения во внешние приложения. Вы познакомитесь с технологиями машинного обучения, такими как Transformers.js и WebGPU, которые имеют решающее значение для продвинутой обработки изображений.

Что такое Transformers.js и зачем его использовать?

Transformers.js это библиотека JavaScript для запуска моделей машинного обучения на основе Transformer полностью в браузере, позволяющая выполнять мощную обработку изображений без необходимости в серверной инфраструктуре.

Наш проект будет состоять из следующих функций:

  • Перетаскивание или загрузка файлов: пользователи могут перетаскивать изображения или щелкать мышью, чтобы выбрать файлы со своего компьютера. Удаление фона: мы будем использовать "Обнимающее лицо" Transformers.js библиотека с WebGPU для выполнения удаления фона на изображениях с использованием модели MODNet Загрузка обработанных изображений: Пользователи могут загружать отдельные обработанные изображения или загружать все обработанные изображения в виде ZIP-файла Поддержка пакетной обработки: пользователи могут выполнять удаление фона с нескольких изображений одновременно.

Создание нашего проекта

Создайте новый проект Vue.js используя create-vue для создания каркаса проекта на основе Vite:

npm create vue@latest

Сконфигурируйте проект следующим образом:

Project Configuration

А теперь беги:

cd background-remover
npm install
npm run format
npm run dev

Это приведет к созданию проекта Vue.js в каталоге background-remover, с Vite в качестве инструмента сборки.

Вам потребуется установить следующие зависимости:

  • @huggingface/transformers: Предоставляет доступ к предварительно обученным моделям из Hugging Face jszip и file-saver: они помогут нам создавать ZIP-файлы и позволят пользователям загружать их

Выполните следующие команды, чтобы установить эти зависимости:

npm install @huggingface/transformers jszip file-saver

Структура макета

Мы создадим структуру для макета пользовательского интерфейса проекта следующим образом:

//App.vue
<template>
  <div class="min-h-screen bg-gray-100 text-gray-900 p-8">
    <div class="max-w-4xl mx-auto">
      <h1 class="text-4xl font-bold mb-2 text-center text-blue-700">
        In-browser Background Remover Tool
      </h1>
      <h2 class="text-lg font-semibold mb-2 text-center text-gray-600">
        Remove background and download files in real time without relying on a
        remote server, powered by
        <a
          class="underline text-blue-500"
          target="_blank"
          href="https://vuejs.org/"
          >Vue.js</a
        >
        and
        <a
          class="underline text-blue-500"
          target="_blank"
          href="https://github.com/xenova/transformers.js"
          >Transformers.js</a
        >
        with WebGPU support
      </h2>
      <!-- File upload -->
      <!-- Action buttons -->
      <!-- Processed Images -->
    </div>
  </div>
</template>

Обработка загрузки файлов

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

Замените комментарий "Загрузить файл" следующим:

<div
  class="p-8 mb-8 border-2 border-dashed rounded-lg text-center cursor-pointer transition-colors duration-300 ease-in-out"
  :class="{
    'border-green-500 bg-green-100': isDragAccept,
    'border-red-500 bg-red-100': isDragReject,
    'border-blue-500 bg-blue-100': isDragActive,
    'border-gray-400 hover:border-blue-500 hover:bg-gray-200':
      !isDragActive,
  }"
  @dragover.prevent="onDragOver"
  @dragleave="onDragLeave"
  @drop="onDrop"
  @click="triggerFileInput"
>
  <input
    type="file"
    class="hidden"
    ref="fileInput"
    @change="handleFiles"
    accept="image/*"
    multiple
  />
  <p class="text-lg mb-2">
    {{
      isDragActive
        ? 'Drop the images here...'
        : 'Drag and drop some images here'
    }}
  </p>
  <p class="text-sm text-gray-500">or click to select files</p>
</div>
  • Обработчик события @dragover.prevent="OnDragOver" предотвращает поведение по умолчанию при перетаскивании файла по области, позволяя удалить его @dragleave="onDragLeave" запускает метод onDragLeave, когда перетаскиваемый элемент покидает область перетаскивания Обработчик событий @drop="onDrop" вызывает метод onDrop, когда элемент перетаскивается в область перетаскивания @click="triggerFileInput" запускает метод triggerFileInput, открывая диалоговое окно ввода файла, когда пользователь нажимает на область

Создайте методы onDragLeave, triggerFileInput, handleFiles и OnDragOver, используемые в шаблоне, следующим образом:

<script setup>
import { ref, onMounted } from 'vue'
const images = ref([])

const isDragActive = ref(false)
const isDragAccept = ref(false)
const isDragReject = ref(false)

const fileInput = ref(null)

const handleFiles = event => {
  const files = event.target.files
  addImages(files)
}

const addImages = files => {
  for (const file of files) {
    images.value.push(URL.createObjectURL(file))
  }
}

const onDragOver = event => {
  event.preventDefault()
  isDragActive.value = true
}

const onDragLeave = () => {
  isDragActive.value = false
}

const onDrop = event => {
  event.preventDefault()
  const files = event.dataTransfer.files
  addImages(files)
  isDragActive.value = false
}

const triggerFileInput = () => {
  fileInput.value.click()
}
</script>

Эта логика обрабатывает загрузку файлов как с помощью перетаскивания, так и с помощью традиционного ввода файлов. Метод addImages берет список файлов, генерирует временный URL-адрес для каждого из них, используя URL.createObjectURL(файл), и помещает эти URL-адреса в массив reactive images.

Это делает их доступными для отображения в шаблоне. Метод onDrop запускается, когда файлы помещаются в область отображения. Он извлекает файлы из event.dataTransfer.files, передает их в addImages и присваивает isDragActive значение false, чтобы указать, что операция перетаскивания завершена:

In-Browser Background Remover Tool

Обработка изображений с помощью MODNet

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

Замените комментарий <!- Кнопки действий -> на следующий:

<div class="flex flex-col items-center gap-4 mb-8">
  <button
    @click="processImages"
    :disabled="isProcessing || images.length === 0"
    class="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-100 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors duration-200 text-lg font-semibold"
  >
    {{ isProcessing ? 'Processing...' : 'Process' }}
  </button>

  <div class="flex gap-4">
    <button
      @click="downloadAsZip"
      :disabled="!isDownloadReady"
      class="px-3 py-1 bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-gray-100 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors duration-200 text-sm"
    >
      Download as ZIP
    </button>
    <button
      @click="clearAll"
      class="px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-gray-100 transition-colors duration-200 text-sm"
    >
      Clear All
    </button>
  </div>
</div>

В этом коде:

  • @click="processImages" запускает метод processImages при нажатии кнопки, который обрабатывает загруженные изображения @click="downloadAsZip" запускает метод downloadAsZip, позволяющий пользователям загружать изображения в виде ZIP-файла. @click="ClearAll" запускает метод ClearAll, который удаляет все загруженные изображения из состояния

Создайте методы ClearAll и processImages, используемые в шаблоне, следующим образом:

<script setup>
const processedImages = ref([])
const isProcessing = ref(false)
const isDownloadReady = ref(false)

const clearAll = () => {
  images.value = []
  processedImages.value = []
  isDownloadReady.value = false
}
</script>

Метод ClearAll просто сбрасывает состояния processedImages, isProcessing и isDownloadReady к их значениям по умолчанию.

В методе processImages мы запустим предсказания для изображений, чтобы извлечь фоновую маску с помощью AutoModel и сгенерировать новый графический холст с обновленной прозрачностью. Установите для параметра isProcessing значение true, чтобы указать, что обработка изображения продолжается. Затем очистите массив processedImages, убедившись, что в нем не осталось старых изображений, прежде чем запускать новый процесс:

const processImages = async () => {
  isProcessing.value = true;
  processedImages.value = [];
};

Затем обратитесь к modelRef для обработки пиксельных данных и создания маски сегментации. Кроме того, обратитесь к processorRef для предварительной обработки изображения (например, изменения размера, нормализации) перед передачей его в модель.

const processImages = async () => {
  ...
  const model = modelRef.value
  const processor = processorRef.value
}

Затем просмотрите изображения, ранее загруженные пользователем, которые хранятся в виде URL-адресов. Преобразуйте URL-адрес каждого изображения в объект RawImage, формат, совместимый с этапами обработки. Затем обработайте изображения и передайте их пиксельные данные в модель машинного обучения:

const processImages = async () => {
  ...
  for (const image of images.value) {
    const img = await RawImage.fromURL(image);
    const { pixel_values } = await processor(img);
    const { output } = await model({ input: pixel_values });
  }
};

Модель возвращает выходной объект, который содержит маску сегментации в тензорном формате. Эта маска используется для идентификации определенных областей изображения, таких как передний и задний план.

Генерация маски

Чтобы сгенерировать маску, мы масштабируем выходные данные модели на 255 и преобразуем их в 8-разрядный целочисленный формат без знака, который идеально подходит для рендеринга изображений. Затем мы преобразуем выходной тензор модели в объект RawImage и изменим размер маски в соответствии с размерами исходного изображения:

const processImages = async () => {
  ...
  for (const image of images.value) {
    ...
    const maskData = (
      await RawImage.fromTensor(output[0].mul(255).to('uint8')).resize(
        img.width,
        img.height,
      )
    ).data
  }
}

Манипулирование холстом

Теперь, когда маска сгенерирована, нам нужно создать новый HTML-элемент <canvas> с той же шириной и высотой, что и у исходного изображения. Затем мы рисуем исходное изображение на холсте, используя img.Метод toCanvas() для преобразования исходного изображения обратно в формат, который можно отрисовать:

const processImages = async () => {
  ...
  for (const image of images.value) {
    ...
    const canvas = document.createElement('canvas')
    canvas.width = img.width
    canvas.height = img.height
    const ctx = canvas.getContext('2d')
    ctx.drawImage(img.toCanvas(), 0, 0)
  }
}

Нанесение маски на холст

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

const processImages = async () => {
  ...
  for (const image of images.value) {
    ...
    const pixelData = ctx.getImageData(0, 0, img.width, img.height)
    for (let i = 0; i < maskData.length; ++i) {
      pixelData.data[4 * i + 3] = maskData[i]
    }
    ctx.putImageData(pixelData, 0, 0)
  }
}

Функция getImageData() возвращает массив значений RGBA (красный, зеленый, синий, альфа) для каждого пикселя изображения. Значение 4 * i + 3 относится к альфа-каналу (прозрачности) пикселя в массиве pixelData.

Обработанное изображение теперь можно сохранить, преобразовав измененный холст в формат изображения и сохранив URL-адрес данных в массиве processedImages:

const processImages = async () => {
  ...
  for (const image of images.value) {
    ...
    processedImages.value.push(canvas.toDataURL('image/png'))
  }
}

Теперь обработанное изображение готово к отображению или загрузке.

Теперь установите для параметра isProcessing значение false, чтобы указать, что обработка завершена. Затем установите для параметра isDownloadReady значение true, что позволит загружать обработанные изображения в виде ZIP-файла:

const processImages = async () => {
  ...
  isProcessing.value = false
  isDownloadReady.value = true
}

Рендеринг обработанных изображений

Обработанные изображения будут отображаться в браузере без фона. Замените комментарий <!-- Обработанные изображения --> следующим:

<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
  <div v-for="(src, index) in images" :key="index" class="relative group">
    <img
      :src="processedImages[index] || src"
      :alt="'Image ' + (index + 1)"
      class="rounded-lg object-cover w-full h-48"
    />
    <div
      v-if="processedImages[index]"
      class="absolute inset-0 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-lg flex items-center justify-center"
    >
      <button
        @click="copyToClipboard(processedImages[index] || src)"
        class="mx-2 px-3 py-1 bg-white text-gray-900 rounded-md hover:bg-gray-200 transition-colors duration-200 text-sm"
      >
        Copy
      </button>
      <button
        @click="downloadImage(processedImages[index] || src)"
        class="mx-2 px-3 py-1 bg-white text-gray-900 rounded-md hover:bg-gray-200 transition-colors duration-200 text-sm"
      >
        Download
      </button>
    </div>
    <button
      @click="removeImage(index)"
      class="absolute top-2 right-2 bg-black bg-opacity-50 text-white w-6 h-6 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 hover:bg-opacity-70"
    >
      ✕
    </button>
  </div>
</div>

Создайте методы copyToClipboard и removeImage, используемые в шаблоне, следующим образом:

<script setup>
...
const copyToClipboard = async url => {
  const response = await fetch(url)
  const blob = await response.blob()
  const clipboardItem = new ClipboardItem({ [blob.type]: blob })
  await navigator.clipboard.write([clipboardItem])
  console.log('Image copied to clipboard')
}
</script>

Функция copyToClipboard извлекает изображение с заданного URL-адреса, преобразует его в большой двоичный объект и копирует в системный буфер обмена:

const removeImage = index => {
  images.value.splice(index, 1)
  processedImages.value.splice(index, 1)
}

Функция removeImage удаляет как исходное изображение, так и его обработанный аналог из соответствующих массивов.

Загрузка обработанных изображений

В этом разделе речь пойдет об использовании JSZip для объединения обработанных изображений в ZIP-файл и FileSaver для запуска загрузки в браузере.

Загрузите индивидуальное изображение:

const downloadImage = url => {
  const a = document.createElement('a')
  a.href = url
  a.download = 'image.png'
  a.click()
}

Функция downloadImage запускает функцию загрузки в браузере, позволяя пользователю загружать изображение напрямую, не переходя по URL-адресу изображения.

Загрузите несколько изображений в формате ZIP:

Создайте функцию, которая позволит пользователю загружать все изображения (обработанные или оригинальные) в виде одного ZIP-файла:

const downloadAsZip = async () => {
  const zip = new JSZip()
  const promises = images.value.map(
    (image, i) =>
      new Promise(resolve => {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        const img = new Image()
        img.src = processedImages.value[i] || image
        img.onload = () => {
          canvas.width = img.width
          canvas.height = img.height
          ctx.drawImage(img, 0, 0)
          canvas.toBlob(blob => {
            if (blob) {
              zip.file(`image-${i + 1}.png`, blob)
            }
            resolve(null)
          }, 'image/png')
        }
      }),
  )
  await Promise.all(promises)
  const content = await zip.generateAsync({ type: 'blob' })
  saveAs(content, 'images.zip')
}

Функция downloadAsZip использует библиотеку JSZip для создания ZIP-файла. Она просматривает каждое изображение, создавая холст, рисуя изображение и преобразуя его в большой двоичный файл PNG. Затем каждый большой двоичный файл изображения добавляется в ZIP-файл. После обработки всех изображений создается ZIP-архив, который автоматически загружается с помощью функции SaveAs.

Обработка ошибок и обратная связь в режиме реального времени

Мы будем обрабатывать ошибки, например, когда WebGPU не поддерживается или возникает любая другая проблема:

onMounted(async () => {
  try {
    if (!navigator.gpu) {
      throw new Error('WebGPU is not supported in this browser.')
    }
    const model_id = 'Xenova/modnet'
    env.backends.onnx.wasm.proxy = false
    modelRef.value = await AutoModel.from_pretrained(model_id, {
      device: 'webgpu',
    })
    processorRef.value = await AutoProcessor.from_pretrained(model_id)
  } catch (error) {
    console.error(error)
  }
})

Этот подключаемый модуль сначала проверяет, поддерживает ли браузер WebGPU. Если поддерживается, он загружает предварительно подготовленную модель машинного обучения (Xenova/modnet) вместе с процессором, настраивая среду выполнения ONNX для оптимальной производительности WebGPU. Если WebGPU недоступен или возникает проблема во время загрузки модели, ошибка обнаруживается и регистрируется в журнале.

Вот как выглядит наша окончательная сборка:

Final Project

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

Вывод

В этом руководстве мы рассмотрели, как создать инструмент для удаления фона изображения в реальном времени с помощью Vue.js и Transformers.js, используя WebGPU для быстрой обработки на стороне клиента. Мы узнали, как реализовать ключевые функции, такие как загрузка файлов, предварительная обработка изображений и удаление фона, на основе модели MODNet, продемонстрировав, как легко интегрировать машинное обучение во внешние приложения. Кроме того, мы изучили способы улучшения взаимодействия с пользователями за счет обратной связи в режиме реального времени, обработки ошибок и возможности загрузки обработанных изображений с помощью JSZip.