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
Сконфигурируйте проект следующим образом:
А теперь беги:
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, чтобы указать, что операция перетаскивания завершена:
Обработка изображений с помощью 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 недоступен или возникает проблема во время загрузки модели, ошибка обнаруживается и регистрируется в журнале.
Вот как выглядит наша окончательная сборка:
Вы можете получить код для окончательной сборки здесь.
Вывод
В этом руководстве мы рассмотрели, как создать инструмент для удаления фона изображения в реальном времени с помощью Vue.js и Transformers.js, используя WebGPU для быстрой обработки на стороне клиента. Мы узнали, как реализовать ключевые функции, такие как загрузка файлов, предварительная обработка изображений и удаление фона, на основе модели MODNet, продемонстрировав, как легко интегрировать машинное обучение во внешние приложения. Кроме того, мы изучили способы улучшения взаимодействия с пользователями за счет обратной связи в режиме реального времени, обработки ошибок и возможности загрузки обработанных изображений с помощью JSZip.