feat(stage-web): new devtools to remove background

This commit is contained in:
Neko Ayaka
2025-04-10 02:06:06 +08:00
parent e2e5adc523
commit f275b96526
4 changed files with 208 additions and 19 deletions
+1
View File
@@ -38,6 +38,7 @@
"@pixiv/three-vrm-core": "^3.3.6",
"@proj-airi/ccc": "workspace:^",
"@proj-airi/drizzle-duckdb-wasm": "catalog:",
"@proj-airi/gpuu": "workspace:^",
"@proj-airi/provider-transformers": "workspace:^",
"@proj-airi/server-sdk": "workspace:^",
"@proj-airi/stage-ui": "workspace:^",
@@ -0,0 +1,183 @@
<script setup lang="ts">
import type { PreTrainedModel, Processor } from '@huggingface/transformers'
import { AutoModel, AutoProcessor, env, RawImage } from '@huggingface/transformers'
import { check } from '@proj-airi/gpuu/webgpu'
import { InputFile } from '@proj-airi/stage-ui/components'
import { computed, onMounted, ref } from 'vue'
const model = ref<PreTrainedModel>()
const processor = ref<Processor>()
const error = ref<unknown>()
const loading = ref()
const processing = ref(false)
const progressPercent = ref(0)
const processedImages = ref<Array<string>>()
const downloadReady = ref<boolean>()
const imageFiles = ref<File[]>([])
const imageFilesURLs = computed(() => imageFiles.value.map(img => URL.createObjectURL(img)))
onMounted(async () => {
try {
if (!((await check()).supported)) {
throw new Error('WebGPU is not supported in this browser.')
}
const model_id = 'Xenova/modnet'
env.backends.onnx.wasm!.proxy = false
model.value ??= await AutoModel.from_pretrained(model_id, {
device: 'webgpu',
})
processor.value ??= await AutoProcessor.from_pretrained(model_id, {})
}
catch (err) {
error.value = err
}
loading.value = false
})
async function processImages() {
if (!model.value)
return
if (!processor.value)
return
processing.value = true
progressPercent.value = 0
processedImages.value = []
const totalImages = imageFilesURLs.value.length
for (let i = 0; i < totalImages; ++i) {
// Load image
const img = await RawImage.fromURL(imageFilesURLs.value[i])
// Pre-process image
const { pixel_values } = await processor.value(img)
// Predict alpha matte
const { output } = await model.value({ input: pixel_values })
const maskData = (await RawImage.fromTensor(output[0].mul(255).to('uint8')).resize(img.width, img.height)).data
// Create new canvas
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
if (!ctx)
return
// Draw original image output to canvas
ctx.drawImage(img.toCanvas(), 0, 0)
// Update alpha channel
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)
processedImages.value.push(canvas.toDataURL('image/png'))
// Update progress
progressPercent.value = Math.round(((i + 1) / totalImages) * 100)
}
processing.value = false
downloadReady.value = true
}
function downloadImage(index: number) {
if (!processedImages.value || index >= processedImages.value.length || !imageFiles.value[index])
return
// Get original filename and create new filename with suffix
const originalFileName = imageFiles.value[index].name
const fileNameWithoutExt = originalFileName.substring(0, originalFileName.lastIndexOf('.')) || originalFileName
const fileExt = originalFileName.substring(originalFileName.lastIndexOf('.')) || '.png'
const newFileName = `${fileNameWithoutExt}-background-removed${fileExt}`
const link = document.createElement('a')
link.href = processedImages.value[index]
link.download = newFileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function downloadAllImages() {
if (!processedImages.value || processedImages.value.length === 0)
return
processedImages.value.forEach((_, index) => {
setTimeout(() => downloadImage(index), index * 100)
})
}
</script>
<template>
<div flex flex-col items-center gap-4>
<button
bg="neutral-100 dark:neutral-800"
w-full cursor-pointer rounded-lg px-3 py-2
:disabled="processing"
@click="processImages"
>
{{ processing ? 'Processing...' : 'Process' }}
</button>
<div h-full w-full flex gap-2>
<div w="[50%]" border="2 solid neutral-200 dark:neutral-800" bg="neutral-50 dark:neutral-900" min-h="120" h="auto" overflow-hidden rounded-lg>
<img v-for="(url, index) in imageFilesURLs" :key="index" :src="url" h-full w-full object-cover>
</div>
<div w="[50%]" border="2 solid neutral-200 dark:neutral-800" bg="neutral-50 dark:neutral-900" min-h="120" h="auto" relative overflow-hidden rounded-lg>
<!-- Processing overlay with progress bar -->
<div v-if="processing" bg="black/50" absolute inset-0 z-10 flex flex-col items-center justify-center>
<div mb-4 text-white font-medium>
{{ progressPercent }}%
</div>
<div bg="gray-200/30" w="70%" h-2 overflow-hidden rounded-full>
<div
bg="emerald-500"
h-full
:style="{ width: `${progressPercent}%` }"
transition-all duration-200
/>
</div>
</div>
<!-- Processed images -->
<div v-for="(url, index) in processedImages" :key="index" relative class="group" h-full w-full>
<img :src="url" h-full w-full object-cover>
<div bg="black/0 group-hover:black/20" absolute inset-0 flex items-center justify-center transition-all duration-200>
<button
bg="emerald-500 hover:emerald-600"
rounded-full p-2 text-white opacity-0 transition-all duration-200 group-hover:opacity-100
@click="downloadImage(index)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
</div>
</div>
<!-- Download all button -->
<div v-if="processedImages && processedImages.length > 1" absolute bottom-2 right-2>
<button
bg="emerald-500 hover:emerald-600"
rounded-full p-2 text-white
@click="downloadAllImages"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
</button>
</div>
</div>
</div>
<InputFile v-model="imageFiles" w-full />
</div>
</template>
@@ -27,6 +27,7 @@ function handleFileChange(e: Event) {
<template>
<label
relative
class="min-h-[120px] flex flex-col cursor-pointer items-center justify-center rounded-xl p-6"
:class="[
isDraggingDebounced ? 'border-primary-400 dark:border-primary-600 hover:border-primary-300 dark:hover:border-primary-700' : 'border-neutral-200 dark:border-neutral-700 hover:border-primary-300 dark:hover:border-primary-700',
@@ -34,7 +35,7 @@ function handleFileChange(e: Event) {
]"
border="dashed 2"
transition="all duration-300"
opacity-95
cursor-pointer opacity-95
hover="scale-100 opacity-100 shadow-md dark:shadow-lg"
@dragover="isDragging = true"
@dragleave="isDragging = false"
@@ -43,6 +44,7 @@ function handleFileChange(e: Event) {
type="file"
:accept="accept"
:multiple="multiple"
cursor-pointer
class="absolute inset-0 h-full w-full opacity-0"
@change="handleFileChange"
>
+21 -18
View File
@@ -458,7 +458,7 @@ importers:
version: 2.3.0
'@intlify/unplugin-vue-i18n':
specifier: ^6.0.5
version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@4.39.0)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@proj-airi/lobe-icons':
specifier: ^1.0.5
version: 1.0.5
@@ -506,7 +506,7 @@ importers:
version: 3.0.0-beta.7(typescript@5.8.3)(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3))
'@vueuse/motion':
specifier: ^3.0.3
version: 3.0.3(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.3))
version: 3.0.3(magicast@0.3.5)(rollup@4.39.0)(vue@3.5.13(typescript@5.8.3))
less:
specifier: ^4.3.0
version: 4.3.0
@@ -518,13 +518,13 @@ importers:
version: 3.2.0(unocss@66.1.0-beta.10(postcss@8.5.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3)))
unplugin-auto-import:
specifier: ^19.1.2
version: 19.1.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))
version: 19.1.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))
unplugin-vue-components:
specifier: ^28.4.1
version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.3))
version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(vue@3.5.13(typescript@5.8.3))
unplugin-vue-macros:
specifier: ^2.14.5
version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3))
version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.25.0)(rollup@4.39.0)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3))
unplugin-vue-markdown:
specifier: ^28.3.1
version: 28.3.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
@@ -533,13 +533,13 @@ importers:
version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
vite-bundle-visualizer:
specifier: ^1.2.1
version: 1.2.1(rollup@2.79.1)
version: 1.2.1(rollup@4.39.0)
vite-plugin-pwa:
specifier: ^1.0.0
version: 1.0.0(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vite-plugin-vue-devtools:
specifier: ^7.7.2
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3))
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(rollup@4.39.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3))
vite-plugin-vue-layouts:
specifier: ^0.11.0
version: 0.11.0(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
@@ -612,6 +612,9 @@ importers:
'@proj-airi/drizzle-duckdb-wasm':
specifier: 'catalog:'
version: 0.4.21(@types/pg@8.11.11)(gel@2.0.0)(pg@8.14.1)(postgres@3.4.5)(web-worker@1.5.0)
'@proj-airi/gpuu':
specifier: workspace:^
version: link:../../packages/gpuu
'@proj-airi/provider-transformers':
specifier: workspace:^
version: link:../../packages/provider-transformers
@@ -804,7 +807,7 @@ importers:
version: 2.3.0
'@intlify/unplugin-vue-i18n':
specifier: ^6.0.5
version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@4.39.0)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
version: 6.0.5(@vue/compiler-dom@3.5.13)(eslint@9.24.0(jiti@2.4.2))(rollup@2.79.1)(typescript@5.8.3)(vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@proj-airi/lobe-icons':
specifier: ^1.0.5
version: 1.0.5
@@ -843,7 +846,7 @@ importers:
version: 3.0.0-beta.7(typescript@5.8.3)(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3))
'@vueuse/motion':
specifier: ^3.0.3
version: 3.0.3(magicast@0.3.5)(rollup@4.39.0)(vue@3.5.13(typescript@5.8.3))
version: 3.0.3(magicast@0.3.5)(rollup@2.79.1)(vue@3.5.13(typescript@5.8.3))
hfup:
specifier: ^0.5.0
version: 0.5.0(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
@@ -855,13 +858,13 @@ importers:
version: 4.0.1
unplugin-auto-import:
specifier: ^19.1.2
version: 19.1.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))
version: 19.1.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))
unplugin-vue-components:
specifier: ^28.4.1
version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(vue@3.5.13(typescript@5.8.3))
version: 28.4.1(@babel/parser@7.26.10)(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(vue@3.5.13(typescript@5.8.3))
unplugin-vue-macros:
specifier: ^2.14.5
version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.25.0)(rollup@4.39.0)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3))
version: 2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3))
unplugin-vue-markdown:
specifier: ^28.3.1
version: 28.3.1(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
@@ -870,13 +873,13 @@ importers:
version: 0.12.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
vite-bundle-visualizer:
specifier: ^1.2.1
version: 1.2.1(rollup@4.39.0)
version: 1.2.1(rollup@2.79.1)
vite-plugin-pwa:
specifier: ^1.0.0
version: 1.0.0(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
vite-plugin-vue-devtools:
specifier: ^7.7.2
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.39.0))(rollup@4.39.0)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3))
version: 7.7.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@2.79.1))(rollup@2.79.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.3))
vite-plugin-vue-layouts:
specifier: ^0.11.0
version: 0.11.0(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
@@ -23664,9 +23667,9 @@ snapshots:
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.39.0)
'@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.8.3))
unplugin-combine@1.2.1(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
unplugin-combine@1.2.1(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)):
optionalDependencies:
esbuild: 0.25.0
esbuild: 0.19.12
rollup: 2.79.1
unplugin: 1.16.1
vite: 6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0)
@@ -23725,7 +23728,7 @@ snapshots:
transitivePeerDependencies:
- vue
unplugin-vue-macros@2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.25.0)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)):
unplugin-vue-macros@2.14.5(@vueuse/core@13.0.0(vue@3.5.13(typescript@5.8.3)))(esbuild@0.19.12)(rollup@2.79.1)(typescript@5.8.3)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3)):
dependencies:
'@vue-macros/better-define': 1.11.4(vue@3.5.13(typescript@5.8.3))
'@vue-macros/boolean-prop': 0.5.5(vue@3.5.13(typescript@5.8.3))
@@ -23757,7 +23760,7 @@ snapshots:
'@vue-macros/short-vmodel': 1.5.5(vue@3.5.13(typescript@5.8.3))
'@vue-macros/volar': 0.30.15(typescript@5.8.3)(vue-tsc@3.0.0-alpha.2(typescript@5.8.3))(vue@3.5.13(typescript@5.8.3))
unplugin: 1.16.1
unplugin-combine: 1.2.1(esbuild@0.25.0)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
unplugin-combine: 1.2.1(esbuild@0.19.12)(rollup@2.79.1)(unplugin@1.16.1)(vite@6.2.5(@types/node@22.14.0)(jiti@2.4.2)(less@4.3.0)(terser@5.17.6)(tsx@4.19.3)(yaml@2.7.0))
unplugin-vue-define-options: 1.5.5(vue@3.5.13(typescript@5.8.3))
vue: 3.5.13(typescript@5.8.3)
transitivePeerDependencies: