Create monorepo from known-good production state
This commit is contained in:
commit
c034824338
651 changed files with 120469 additions and 0 deletions
81
bgbye/.gitignore
vendored
Normal file
81
bgbye/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Bytecode
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Python virtual environment
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# wrangler files
|
||||
.wrangler
|
||||
.dev.vars
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# PyCharm and VSCode settings
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Video file extensions
|
||||
*.mp4
|
||||
*.mov
|
||||
*.avi
|
||||
*.mkv
|
||||
*.flv
|
||||
*.wmv
|
||||
*.webm
|
||||
*.m4v
|
||||
*.mpeg
|
||||
*.mpg
|
||||
*.3gp
|
||||
*.3g2
|
||||
*.gif
|
||||
|
||||
#video-related temporary files
|
||||
*.vob
|
||||
*.ogv
|
||||
*.ogg
|
||||
*.gifv
|
||||
|
||||
# Machine Learning model files
|
||||
*.pkl
|
||||
*.pickle
|
||||
*.h5
|
||||
*.hdf5
|
||||
*.joblib
|
||||
*.onnx
|
||||
*.tflite
|
||||
*.pb
|
||||
*.ckpt
|
||||
*.weights
|
||||
*.pt
|
||||
*.pth
|
||||
*.torch
|
||||
*.bin
|
||||
|
||||
63
bgbye/Dockerfile.gpu
Normal file
63
bgbye/Dockerfile.gpu
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# System deps (incl. GL + X libs so OpenCV works headless)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 python3-venv python3-pip wget ca-certificates \
|
||||
libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Upgrade pip first
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
|
||||
# Core Python deps
|
||||
RUN python3 -m pip install --no-cache-dir \
|
||||
onnxruntime-gpu==1.22.0 \
|
||||
rembg==2.0.67 \
|
||||
fastapi==0.116.1 \
|
||||
uvicorn[standard]==0.35.0 \
|
||||
pillow==11.3.0 \
|
||||
numpy==2.2.6 \
|
||||
pydantic==2.11.7 \
|
||||
opencv-python-headless==4.12.0.88
|
||||
|
||||
# PyTorch CUDA 12.4
|
||||
RUN python3 -m pip install --no-cache-dir \
|
||||
--index-url https://download.pytorch.org/whl/cu124 \
|
||||
torch==2.4.1 \
|
||||
torchvision==0.19.1
|
||||
|
||||
# ML + HuggingFace stack (pinned to avoid BRIA RMBG compatibility bug)
|
||||
RUN python3 -m pip install --no-cache-dir \
|
||||
transparent-background \
|
||||
carvekit \
|
||||
timm \
|
||||
transformers==4.44.2 \
|
||||
tokenizers==0.19.1 \
|
||||
huggingface-hub==0.36.2 \
|
||||
accelerate
|
||||
|
||||
# Your server code
|
||||
COPY server /app/server
|
||||
|
||||
# Remove sudo usage if present (containers already run as root)
|
||||
RUN if [ -f /app/server/setup.sh ]; then \
|
||||
sed -i 's/^[[:space:]]*sudo[[:space:]]\+//g' /app/server/setup.sh; \
|
||||
fi
|
||||
|
||||
# Make sure scripts are executable
|
||||
RUN chmod +x /app/server/setup.sh /app/server/run.sh || true
|
||||
|
||||
EXPOSE 7001
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=5s --retries=5 \
|
||||
CMD wget -qO- http://localhost:7001/health || exit 1
|
||||
|
||||
# Run API
|
||||
CMD ["bash", "/app/server/run.sh"]
|
||||
58
bgbye/README.md
Normal file
58
bgbye/README.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# BGBye
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Latest Node.js and npm
|
||||
- python3 and python3-pip
|
||||
- Windows users must use **Windows Subsystem for Linux (WSL)**
|
||||
- Concurrently (optional): for running both server and client at once.
|
||||
`npm i concurrently -g`
|
||||
|
||||
### Steps
|
||||
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone https://github.com/MangoLion/bgbye
|
||||
cd bgbye
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Set up the server:
|
||||
```
|
||||
npm run setup-server
|
||||
```
|
||||
**Make sure to restart your terminal after!**
|
||||
|
||||
### AMD GPU
|
||||
Goto `./server/setp.sh` and replace the index url with
|
||||
`--index-url https://download.pytorch.org/whl/rocm6.0`
|
||||
|
||||
Do this **before** calling `npm run setup-server`
|
||||
|
||||
### Diagnosis
|
||||
- NPM takes forever to install? (WSL)
|
||||
- You probably didn't install npm on WSL, so its using npm on windows. Install npm on WSL first.
|
||||
- Internal server error 500 whenever submit an image/video
|
||||
- Could be CUDA issues, make sure your cuda GPU is visible (nvidia-smi)?
|
||||
- Internal server error 500 whenever submit a video, but image works
|
||||
- Check if ffmpeg is installed
|
||||
|
||||
## Running the App
|
||||
|
||||
To start both the web app and server, run:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
## Moar notes
|
||||
|
||||
I'm so sorry this is using the long dead create-react-app template! Was setting up Cloudflare Pages for the first time and their tutorial installed CRA 💀
|
||||
|
||||
I'll move this stuff to Vite later
|
||||
16
bgbye/UPSTREAM.md
Normal file
16
bgbye/UPSTREAM.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# BGBye provenance
|
||||
|
||||
This directory began as a customized copy of the BGBye project.
|
||||
|
||||
- Original repository: `https://github.com/MangoLion/bgbye.git`
|
||||
- Imported commit: `5255025e2360cc33599c7c96ed904f1bc49e9ae8`
|
||||
- Original branch: `main`
|
||||
- Original commit date: `2024-07-12`
|
||||
- Original author: Mangolion
|
||||
|
||||
The code has since been modified for integration with the Laser Everything
|
||||
database application. It is maintained here as part of this monorepo and is
|
||||
not expected to receive automatic upstream updates.
|
||||
|
||||
No license file was present in the imported source tree. This provenance
|
||||
record documents the source but does not add or replace a software license.
|
||||
20208
bgbye/package-lock.json
generated
Normal file
20208
bgbye/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
59
bgbye/package.json
Normal file
59
bgbye/package.json
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "bgbye",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@img-comparison-slider/react": "^8.0.2",
|
||||
"@mui/icons-material": "^5.15.21",
|
||||
"@mui/material": "^5.15.21",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"img-comparison-slider": "^8.0.6",
|
||||
"p-limit": "^6.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-best-gradient-color-picker": "^3.0.8",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react18-image-magnifier": "^4.0.2",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "concurrently \"PORT=9877 react-scripts start\" \"cd server && ./run.sh\"",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"deploy": "npm run build && wrangler pages deploy ./build",
|
||||
"preview": "npm run build && wrangler pages dev ./build",
|
||||
"start-server": "cd server && ./run.sh",
|
||||
"setup-server": "cd server && chmod +x setup.sh && chmod +x run.sh && ./setup.sh",
|
||||
"start-client": "PORT=9877 react-scripts start"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
"wrangler": "^3.63.1"
|
||||
}
|
||||
}
|
||||
BIN
bgbye/public/favicon.ico
Normal file
BIN
bgbye/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
43
bgbye/public/index.html
Normal file
43
bgbye/public/index.html
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico?v=2" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="free remove background from images and videos"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>BG Bye</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
bgbye/public/logo192.png
Normal file
BIN
bgbye/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
bgbye/public/logo512.png
Normal file
BIN
bgbye/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
25
bgbye/public/manifest.json
Normal file
25
bgbye/public/manifest.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "BGBye",
|
||||
"name": "BGBye",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
bgbye/public/robots.txt
Normal file
3
bgbye/public/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
3
bgbye/server/__init__.py
Normal file
3
bgbye/server/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# server/__init__.py
|
||||
__all__ = []
|
||||
|
||||
70
bgbye/server/ormbg.py
Normal file
70
bgbye/server/ormbg.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# /app/server/ormbg.py
|
||||
from __future__ import annotations
|
||||
import io
|
||||
import os
|
||||
from typing import Optional, Union
|
||||
|
||||
from rembg import remove as rembg_remove, new_session
|
||||
from PIL import Image
|
||||
import onnxruntime as ort
|
||||
|
||||
# Choose GPU if available, else CPU
|
||||
def _default_providers() -> list[str]:
|
||||
providers = ort.get_available_providers()
|
||||
return ["CUDAExecutionProvider", "CPUExecutionProvider"] if "CUDAExecutionProvider" in providers else ["CPUExecutionProvider"]
|
||||
|
||||
class ORMBGProcessor:
|
||||
"""
|
||||
Minimal adapter used by server.py.
|
||||
Provides:
|
||||
- .warmup()
|
||||
- .process_bytes(input_bytes) -> bytes (PNG with alpha)
|
||||
- .process_pil(img: PIL.Image.Image) -> PIL.Image.Image
|
||||
- .close() (no-op; for API symmetry)
|
||||
"""
|
||||
def __init__(self, model_name: str = "isnet-general-use", providers: Optional[list[str]] = None):
|
||||
# Some rembg internals pick providers automatically; onnxruntime-gpu presence
|
||||
# will make CUDA provider available. We still compute a list so we can log/verify.
|
||||
self.providers = providers or _default_providers()
|
||||
# rembg's API chooses the best available EP under the hood if ORT-GPU is present.
|
||||
# Just ensure the model name exists; common: isnet-general-use, u2net, u2netp, u2net_human_seg
|
||||
self.model_name = model_name
|
||||
self.session = new_session(self.model_name)
|
||||
|
||||
def warmup(self) -> None:
|
||||
# Tiny 1x1 transparent PNG to trigger model load
|
||||
img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
||||
_ = self.process_pil(img)
|
||||
|
||||
def process_bytes(self, data: bytes, return_png: bool = True) -> bytes:
|
||||
"""
|
||||
Accept raw image bytes (jpg/png/etc), return PNG bytes with alpha.
|
||||
"""
|
||||
out = rembg_remove(data, session=self.session)
|
||||
if return_png:
|
||||
# rembg already returns PNG bytes by default; if it's bytes, just pass through
|
||||
if isinstance(out, (bytes, bytearray)):
|
||||
return bytes(out)
|
||||
# if it's a PIL image, convert to PNG bytes
|
||||
if isinstance(out, Image.Image):
|
||||
buf = io.BytesIO()
|
||||
out.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
# fallback: ensure bytes
|
||||
if isinstance(out, Image.Image):
|
||||
buf = io.BytesIO()
|
||||
out.save(buf, format="PNG")
|
||||
return buf.getvalue()
|
||||
return bytes(out)
|
||||
|
||||
def process_pil(self, img: Image.Image) -> Image.Image:
|
||||
out = rembg_remove(img, session=self.session)
|
||||
if isinstance(out, Image.Image):
|
||||
return out
|
||||
# if rembg returned bytes, decode back to PIL
|
||||
return Image.open(io.BytesIO(out)).convert("RGBA")
|
||||
|
||||
def close(self) -> None:
|
||||
# Nothing to dispose explicitly; keep for API compatibility
|
||||
pass
|
||||
|
||||
2
bgbye/server/ormbg/__init__.py
Normal file
2
bgbye/server/ormbg/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .ormbg import ORMBG
|
||||
from .ormbg_processor import ORMBGProcessor
|
||||
484
bgbye/server/ormbg/ormbg.py
Normal file
484
bgbye/server/ormbg/ormbg.py
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
# https://github.com/xuebinqin/DIS/blob/main/IS-Net/models/isnet.py
|
||||
|
||||
|
||||
class REBNCONV(nn.Module):
|
||||
def __init__(self, in_ch=3, out_ch=3, dirate=1, stride=1):
|
||||
super(REBNCONV, self).__init__()
|
||||
|
||||
self.conv_s1 = nn.Conv2d(
|
||||
in_ch, out_ch, 3, padding=1 * dirate, dilation=1 * dirate, stride=stride
|
||||
)
|
||||
self.bn_s1 = nn.BatchNorm2d(out_ch)
|
||||
self.relu_s1 = nn.ReLU(inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
hx = x
|
||||
xout = self.relu_s1(self.bn_s1(self.conv_s1(hx)))
|
||||
|
||||
return xout
|
||||
|
||||
|
||||
## upsample tensor 'src' to have the same spatial size with tensor 'tar'
|
||||
def _upsample_like(src, tar):
|
||||
|
||||
src = F.interpolate(src, size=tar.shape[2:], mode="bilinear")
|
||||
|
||||
return src
|
||||
|
||||
|
||||
### RSU-7 ###
|
||||
class RSU7(nn.Module):
|
||||
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3, img_size=512):
|
||||
super(RSU7, self).__init__()
|
||||
|
||||
self.in_ch = in_ch
|
||||
self.mid_ch = mid_ch
|
||||
self.out_ch = out_ch
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1) ## 1 -> 1/2
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool3 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool4 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv5 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool5 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv6 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv7 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv6d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv5d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv4d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
b, c, h, w = x.shape
|
||||
|
||||
hx = x
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
hx = self.pool3(hx3)
|
||||
|
||||
hx4 = self.rebnconv4(hx)
|
||||
hx = self.pool4(hx4)
|
||||
|
||||
hx5 = self.rebnconv5(hx)
|
||||
hx = self.pool5(hx5)
|
||||
|
||||
hx6 = self.rebnconv6(hx)
|
||||
|
||||
hx7 = self.rebnconv7(hx6)
|
||||
|
||||
hx6d = self.rebnconv6d(torch.cat((hx7, hx6), 1))
|
||||
hx6dup = _upsample_like(hx6d, hx5)
|
||||
|
||||
hx5d = self.rebnconv5d(torch.cat((hx6dup, hx5), 1))
|
||||
hx5dup = _upsample_like(hx5d, hx4)
|
||||
|
||||
hx4d = self.rebnconv4d(torch.cat((hx5dup, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-6 ###
|
||||
class RSU6(nn.Module):
|
||||
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU6, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool3 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool4 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv5 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv6 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv5d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv4d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
hx = self.pool3(hx3)
|
||||
|
||||
hx4 = self.rebnconv4(hx)
|
||||
hx = self.pool4(hx4)
|
||||
|
||||
hx5 = self.rebnconv5(hx)
|
||||
|
||||
hx6 = self.rebnconv6(hx5)
|
||||
|
||||
hx5d = self.rebnconv5d(torch.cat((hx6, hx5), 1))
|
||||
hx5dup = _upsample_like(hx5d, hx4)
|
||||
|
||||
hx4d = self.rebnconv4d(torch.cat((hx5dup, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-5 ###
|
||||
class RSU5(nn.Module):
|
||||
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU5, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool3 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv5 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv4d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
hx = self.pool3(hx3)
|
||||
|
||||
hx4 = self.rebnconv4(hx)
|
||||
|
||||
hx5 = self.rebnconv5(hx4)
|
||||
|
||||
hx4d = self.rebnconv4d(torch.cat((hx5, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-4 ###
|
||||
class RSU4(nn.Module):
|
||||
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU4, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.pool1 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
self.pool2 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=1)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=1)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx = self.pool1(hx1)
|
||||
|
||||
hx2 = self.rebnconv2(hx)
|
||||
hx = self.pool2(hx2)
|
||||
|
||||
hx3 = self.rebnconv3(hx)
|
||||
|
||||
hx4 = self.rebnconv4(hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
### RSU-4F ###
|
||||
class RSU4F(nn.Module):
|
||||
|
||||
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
|
||||
super(RSU4F, self).__init__()
|
||||
|
||||
self.rebnconvin = REBNCONV(in_ch, out_ch, dirate=1)
|
||||
|
||||
self.rebnconv1 = REBNCONV(out_ch, mid_ch, dirate=1)
|
||||
self.rebnconv2 = REBNCONV(mid_ch, mid_ch, dirate=2)
|
||||
self.rebnconv3 = REBNCONV(mid_ch, mid_ch, dirate=4)
|
||||
|
||||
self.rebnconv4 = REBNCONV(mid_ch, mid_ch, dirate=8)
|
||||
|
||||
self.rebnconv3d = REBNCONV(mid_ch * 2, mid_ch, dirate=4)
|
||||
self.rebnconv2d = REBNCONV(mid_ch * 2, mid_ch, dirate=2)
|
||||
self.rebnconv1d = REBNCONV(mid_ch * 2, out_ch, dirate=1)
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
hx = x
|
||||
|
||||
hxin = self.rebnconvin(hx)
|
||||
|
||||
hx1 = self.rebnconv1(hxin)
|
||||
hx2 = self.rebnconv2(hx1)
|
||||
hx3 = self.rebnconv3(hx2)
|
||||
|
||||
hx4 = self.rebnconv4(hx3)
|
||||
|
||||
hx3d = self.rebnconv3d(torch.cat((hx4, hx3), 1))
|
||||
hx2d = self.rebnconv2d(torch.cat((hx3d, hx2), 1))
|
||||
hx1d = self.rebnconv1d(torch.cat((hx2d, hx1), 1))
|
||||
|
||||
return hx1d + hxin
|
||||
|
||||
|
||||
class myrebnconv(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_ch=3,
|
||||
out_ch=1,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
padding=1,
|
||||
dilation=1,
|
||||
groups=1,
|
||||
):
|
||||
super(myrebnconv, self).__init__()
|
||||
|
||||
self.conv = nn.Conv2d(
|
||||
in_ch,
|
||||
out_ch,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
groups=groups,
|
||||
)
|
||||
self.bn = nn.BatchNorm2d(out_ch)
|
||||
self.rl = nn.ReLU(inplace=True)
|
||||
|
||||
def forward(self, x):
|
||||
return self.rl(self.bn(self.conv(x)))
|
||||
|
||||
|
||||
bce_loss = nn.BCELoss(size_average=True)
|
||||
|
||||
|
||||
class ORMBG(nn.Module):
|
||||
|
||||
def __init__(self, in_ch=3, out_ch=1):
|
||||
super(ORMBG, self).__init__()
|
||||
|
||||
self.conv_in = nn.Conv2d(in_ch, 64, 3, stride=2, padding=1)
|
||||
self.pool_in = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage1 = RSU7(64, 32, 64)
|
||||
self.pool12 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage2 = RSU6(64, 32, 128)
|
||||
self.pool23 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage3 = RSU5(128, 64, 256)
|
||||
self.pool34 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage4 = RSU4(256, 128, 512)
|
||||
self.pool45 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage5 = RSU4F(512, 256, 512)
|
||||
self.pool56 = nn.MaxPool2d(2, stride=2, ceil_mode=True)
|
||||
|
||||
self.stage6 = RSU4F(512, 256, 512)
|
||||
|
||||
# decoder
|
||||
self.stage5d = RSU4F(1024, 256, 512)
|
||||
self.stage4d = RSU4(1024, 128, 256)
|
||||
self.stage3d = RSU5(512, 64, 128)
|
||||
self.stage2d = RSU6(256, 32, 64)
|
||||
self.stage1d = RSU7(128, 16, 64)
|
||||
|
||||
self.side1 = nn.Conv2d(64, out_ch, 3, padding=1)
|
||||
self.side2 = nn.Conv2d(64, out_ch, 3, padding=1)
|
||||
self.side3 = nn.Conv2d(128, out_ch, 3, padding=1)
|
||||
self.side4 = nn.Conv2d(256, out_ch, 3, padding=1)
|
||||
self.side5 = nn.Conv2d(512, out_ch, 3, padding=1)
|
||||
self.side6 = nn.Conv2d(512, out_ch, 3, padding=1)
|
||||
|
||||
# self.outconv = nn.Conv2d(6*out_ch,out_ch,1)
|
||||
|
||||
def compute_loss(self, predictions, ground_truth):
|
||||
loss0, loss = 0.0, 0.0
|
||||
for i in range(0, len(predictions)):
|
||||
loss = loss + bce_loss(predictions[i], ground_truth)
|
||||
if i == 0:
|
||||
loss0 = loss
|
||||
return loss0, loss
|
||||
|
||||
def forward(self, x):
|
||||
|
||||
hx = x
|
||||
|
||||
hxin = self.conv_in(hx)
|
||||
# hx = self.pool_in(hxin)
|
||||
|
||||
# stage 1
|
||||
hx1 = self.stage1(hxin)
|
||||
hx = self.pool12(hx1)
|
||||
|
||||
# stage 2
|
||||
hx2 = self.stage2(hx)
|
||||
hx = self.pool23(hx2)
|
||||
|
||||
# stage 3
|
||||
hx3 = self.stage3(hx)
|
||||
hx = self.pool34(hx3)
|
||||
|
||||
# stage 4
|
||||
hx4 = self.stage4(hx)
|
||||
hx = self.pool45(hx4)
|
||||
|
||||
# stage 5
|
||||
hx5 = self.stage5(hx)
|
||||
hx = self.pool56(hx5)
|
||||
|
||||
# stage 6
|
||||
hx6 = self.stage6(hx)
|
||||
hx6up = _upsample_like(hx6, hx5)
|
||||
|
||||
# -------------------- decoder --------------------
|
||||
hx5d = self.stage5d(torch.cat((hx6up, hx5), 1))
|
||||
hx5dup = _upsample_like(hx5d, hx4)
|
||||
|
||||
hx4d = self.stage4d(torch.cat((hx5dup, hx4), 1))
|
||||
hx4dup = _upsample_like(hx4d, hx3)
|
||||
|
||||
hx3d = self.stage3d(torch.cat((hx4dup, hx3), 1))
|
||||
hx3dup = _upsample_like(hx3d, hx2)
|
||||
|
||||
hx2d = self.stage2d(torch.cat((hx3dup, hx2), 1))
|
||||
hx2dup = _upsample_like(hx2d, hx1)
|
||||
|
||||
hx1d = self.stage1d(torch.cat((hx2dup, hx1), 1))
|
||||
|
||||
# side output
|
||||
d1 = self.side1(hx1d)
|
||||
d1 = _upsample_like(d1, x)
|
||||
|
||||
d2 = self.side2(hx2d)
|
||||
d2 = _upsample_like(d2, x)
|
||||
|
||||
d3 = self.side3(hx3d)
|
||||
d3 = _upsample_like(d3, x)
|
||||
|
||||
d4 = self.side4(hx4d)
|
||||
d4 = _upsample_like(d4, x)
|
||||
|
||||
d5 = self.side5(hx5d)
|
||||
d5 = _upsample_like(d5, x)
|
||||
|
||||
d6 = self.side6(hx6)
|
||||
d6 = _upsample_like(d6, x)
|
||||
|
||||
return [
|
||||
F.sigmoid(d1),
|
||||
F.sigmoid(d2),
|
||||
F.sigmoid(d3),
|
||||
F.sigmoid(d4),
|
||||
F.sigmoid(d5),
|
||||
F.sigmoid(d6),
|
||||
], [hx1d, hx2d, hx3d, hx4d, hx5d, hx6]
|
||||
45
bgbye/server/ormbg/ormbg_processor.py
Normal file
45
bgbye/server/ormbg/ormbg_processor.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import torch
|
||||
import torch.nn.functional as F
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from .ormbg import ORMBG
|
||||
|
||||
class ORMBGProcessor:
|
||||
def __init__(self, model_path):
|
||||
self.device = torch.device("cpu")
|
||||
self.net = ORMBG()
|
||||
self.net.load_state_dict(torch.load(model_path, map_location="cpu"))
|
||||
self.net.eval()
|
||||
|
||||
def to(self, device):
|
||||
self.device = torch.device(device)
|
||||
self.net.to(self.device)
|
||||
|
||||
def process_image(self, image):
|
||||
# Ensure image is in RGB mode
|
||||
if image.mode != "RGB":
|
||||
image = image.convert("RGB")
|
||||
|
||||
# Preprocess the image
|
||||
w, h = image.size
|
||||
image = image.resize((1024, 1024), Image.BILINEAR)
|
||||
im_np = np.array(image)
|
||||
im_tensor = torch.tensor(im_np, dtype=torch.float32).permute(2, 0, 1).unsqueeze(0)
|
||||
im_tensor = torch.divide(im_tensor, 255.0).to(self.device)
|
||||
|
||||
# Inference
|
||||
with torch.no_grad():
|
||||
result = self.net(im_tensor)
|
||||
|
||||
# Post-process
|
||||
result = result[0][0] # Take the first element of the output list and the first channel
|
||||
result = F.interpolate(result, size=(h, w), mode="bilinear")
|
||||
result = result.squeeze()
|
||||
result = (result - result.min()) / (result.max() - result.min())
|
||||
|
||||
# Create mask and apply to original image
|
||||
mask = Image.fromarray((result.cpu().numpy() * 255).astype(np.uint8))
|
||||
new_im = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||
new_im.paste(image.resize((w, h), Image.BILINEAR), mask=mask)
|
||||
|
||||
return new_im
|
||||
20
bgbye/server/run.sh
Executable file
20
bgbye/server/run.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MODEL_PATH="${ORMBG_MODEL_PATH:-/root/.ormbg/ormbg.pth}"
|
||||
MODEL_DIR="$(dirname "$MODEL_PATH")"
|
||||
|
||||
if [ ! -f "$MODEL_PATH" ]; then
|
||||
echo "[bgbye] ORMBG model not found at $MODEL_PATH - running setup.sh to fetch it..."
|
||||
mkdir -p "$MODEL_DIR"
|
||||
if [ -x /app/server/setup.sh ]; then
|
||||
bash /app/server/setup.sh
|
||||
else
|
||||
echo "[bgbye] ERROR: /app/server/setup.sh not found or not executable" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[bgbye] Starting API..."
|
||||
exec uvicorn server.server:app --host 0.0.0.0 --port 7001 --no-server-header
|
||||
|
||||
513
bgbye/server/server.py
Normal file
513
bgbye/server/server.py
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
from fastapi import FastAPI, UploadFile, File, Response, Form, HTTPException, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from PIL import Image
|
||||
import io
|
||||
import shutil
|
||||
from rembg import remove as rembg_remove, new_session
|
||||
import time
|
||||
import numpy as np
|
||||
import tempfile
|
||||
import uuid
|
||||
import os
|
||||
import subprocess
|
||||
from transformers import pipeline
|
||||
from transparent_background import Remover
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import torch
|
||||
from .ormbg import ORMBGProcessor
|
||||
from typing import Dict
|
||||
from contextlib import contextmanager
|
||||
|
||||
from carvekit.ml.files.models_loc import download_all
|
||||
|
||||
download_all()
|
||||
|
||||
from carvekit.ml.wrap.u2net import U2NET
|
||||
from carvekit.ml.wrap.basnet import BASNET
|
||||
from carvekit.ml.wrap.fba_matting import FBAMatting
|
||||
from carvekit.ml.wrap.deeplab_v3 import DeepLabV3
|
||||
from carvekit.ml.wrap.tracer_b7 import TracerUniversalB7
|
||||
from carvekit.api.interface import Interface
|
||||
from carvekit.pipelines.postprocessing import MattingMethod
|
||||
from carvekit.pipelines.preprocessing import PreprocessingStub
|
||||
from carvekit.trimap.generator import TrimapGenerator
|
||||
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Add ORMBG model initialization
|
||||
ormbg_model_path = os.path.expanduser("~/.ormbg/ormbg.pth")
|
||||
try:
|
||||
ormbg_processor = ORMBGProcessor(ormbg_model_path)
|
||||
if torch.cuda.is_available():
|
||||
ormbg_processor.to("cuda")
|
||||
else:
|
||||
ormbg_processor.to("cpu")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"ORMBG model file not found: {ormbg_model_path}")
|
||||
print("Error: ORMBG model file not found. Please run 'npm run setup-server' to download it.")
|
||||
exit(1)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Create temp_videos folder if it doesn't exist
|
||||
TEMP_VIDEOS_DIR = "temp_videos"
|
||||
os.makedirs(TEMP_VIDEOS_DIR, exist_ok=True)
|
||||
|
||||
# Create a frames directory within temp_videos
|
||||
FRAMES_DIR = os.path.join(TEMP_VIDEOS_DIR, "frames")
|
||||
os.makedirs(FRAMES_DIR, exist_ok=True)
|
||||
|
||||
# Add a dictionary to store processing status
|
||||
processing_status = {}
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
async def cleanup_old_videos():
|
||||
while True:
|
||||
current_time = datetime.now()
|
||||
for item in os.listdir(TEMP_VIDEOS_DIR):
|
||||
item_path = os.path.join(TEMP_VIDEOS_DIR, item)
|
||||
item_modified = datetime.fromtimestamp(os.path.getmtime(item_path))
|
||||
if current_time - item_modified > timedelta(minutes=10):
|
||||
if os.path.isfile(item_path):
|
||||
os.remove(item_path)
|
||||
logger.info(f"Removed old file: {item_path}")
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
logger.info(f"Removed old directory: {item_path}")
|
||||
await asyncio.sleep(600) # Run every 10 minutes
|
||||
|
||||
# Pre-load all models
|
||||
bria_model = pipeline("image-segmentation", model="briaai/RMBG-1.4", trust_remote_code=True, device="cpu")
|
||||
inspyrenet_model = Remover()
|
||||
inspyrenet_model.model.cpu()
|
||||
rembg_models = {
|
||||
'u2net': new_session('u2net'),
|
||||
'u2net_human_seg': new_session('u2net_human_seg'),
|
||||
'isnet-general-use': new_session('isnet-general-use'),
|
||||
'isnet-anime': new_session('isnet-anime')
|
||||
}
|
||||
|
||||
# Initialize Carvekit models
|
||||
def initialize_carvekit_model(seg_pipe_class, device='cuda'):
|
||||
model = Interface(
|
||||
pre_pipe=PreprocessingStub(),
|
||||
post_pipe=MattingMethod(
|
||||
matting_module=FBAMatting(device=device, input_tensor_size=2048, batch_size=1),
|
||||
trimap_generator=TrimapGenerator(),
|
||||
device=device
|
||||
),
|
||||
seg_pipe=seg_pipe_class(device=device, batch_size=1)
|
||||
)
|
||||
model.segmentation_pipeline.to('cpu')
|
||||
return model
|
||||
|
||||
carvekit_models = {
|
||||
'u2net': initialize_carvekit_model(U2NET),
|
||||
'tracer': initialize_carvekit_model(TracerUniversalB7),
|
||||
'basnet': initialize_carvekit_model(BASNET),
|
||||
'deeplab': initialize_carvekit_model(DeepLabV3)
|
||||
}
|
||||
|
||||
# ---- Method registry + aliases (NEW) -----------------------------------------
|
||||
CARVEKIT = list(carvekit_models.keys()) # ['u2net', 'tracer', 'basnet', 'deeplab']
|
||||
REMBG = ['u2net_human_seg', 'isnet-general-use', 'isnet-anime']
|
||||
EXTRA = ['ormbg', 'bria', 'inspyrenet']
|
||||
ALLOWED_METHODS = sorted(set(CARVEKIT + REMBG + EXTRA))
|
||||
|
||||
ALIASES = {
|
||||
'deeplabv3': 'deeplab',
|
||||
'tracer-b7': 'tracer',
|
||||
'tracer_b7': 'tracer',
|
||||
'u2net_human': 'u2net_human_seg',
|
||||
'isnet_general_use': 'isnet-general-use',
|
||||
'bria14': 'bria',
|
||||
# (intentionally no alias for 'rembg'/'open-rmbg')
|
||||
}
|
||||
|
||||
@app.get("/methods")
|
||||
def list_methods():
|
||||
return {"methods": ALLOWED_METHODS}
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return JSONResponse({"ok": True})
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Ensure GPU memory is cleared after initialization
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def process_with_bria(image):
|
||||
result = bria_model(image, return_mask=True)
|
||||
mask = result
|
||||
if not isinstance(mask, Image.Image):
|
||||
mask = Image.fromarray((mask * 255).astype('uint8'))
|
||||
no_bg_image = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
||||
no_bg_image.paste(image, mask=mask)
|
||||
return no_bg_image
|
||||
|
||||
def process_with_ormbg(image):
|
||||
result = ormbg_processor.process_image(image)
|
||||
return result
|
||||
|
||||
def process_with_inspyrenet(image):
|
||||
return inspyrenet_model.process(image, type='rgba')
|
||||
|
||||
def process_with_rembg(image, model='u2net'):
|
||||
return rembg_remove(image, session=rembg_models[model])
|
||||
|
||||
def process_with_carvekit(image, model='u2net'):
|
||||
# Initialize segmentation network based on model input
|
||||
if model == 'u2net':
|
||||
seg_net = U2NET(device='cuda', batch_size=1)
|
||||
elif model == 'tracer':
|
||||
seg_net = TracerUniversalB7(device='cuda', batch_size=1)
|
||||
elif model == 'basnet':
|
||||
seg_net = BASNET(device='cuda', batch_size=1)
|
||||
elif model == 'deeplab':
|
||||
seg_net = DeepLabV3(device='cuda', batch_size=1)
|
||||
else:
|
||||
raise ValueError("Unsupported model type")
|
||||
|
||||
# Setup the post-processing components
|
||||
fba = FBAMatting(device='cuda', input_tensor_size=2048, batch_size=1)
|
||||
trimap = TrimapGenerator()
|
||||
preprocessing = PreprocessingStub()
|
||||
postprocessing = MattingMethod(matting_module=fba, trimap_generator=trimap, device='cuda')
|
||||
|
||||
interface = Interface(pre_pipe=preprocessing, post_pipe=postprocessing, seg_pipe=seg_net)
|
||||
processed_image = interface([image])[0]
|
||||
|
||||
return processed_image
|
||||
|
||||
@contextmanager
|
||||
def inspyrenet_video_model_context():
|
||||
try:
|
||||
model = Remover()
|
||||
model.model.cuda()
|
||||
yield model
|
||||
finally:
|
||||
model.model.cpu()
|
||||
del model
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
@contextmanager
|
||||
def carvekit_video_model_context(model_name):
|
||||
try:
|
||||
if model_name == 'u2net':
|
||||
seg_net = U2NET(device='cuda', batch_size=1)
|
||||
elif model_name == 'tracer':
|
||||
seg_net = TracerUniversalB7(device='cuda', batch_size=1)
|
||||
elif model_name == 'basnet':
|
||||
seg_net = BASNET(device='cuda', batch_size=1)
|
||||
elif model_name == 'deeplab':
|
||||
seg_net = DeepLabV3(device='cuda', batch_size=1)
|
||||
else:
|
||||
raise ValueError("Unsupported model type")
|
||||
|
||||
fba = FBAMatting(device='cuda', input_tensor_size=2048, batch_size=1)
|
||||
trimap = TrimapGenerator()
|
||||
preprocessing = PreprocessingStub()
|
||||
postprocessing = MattingMethod(matting_module=fba, trimap_generator=trimap, device='cuda')
|
||||
|
||||
interface = Interface(pre_pipe=preprocessing, post_pipe=postprocessing, seg_pipe=seg_net)
|
||||
yield interface
|
||||
finally:
|
||||
del seg_net, fba, trimap, preprocessing, postprocessing, interface
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
# Create a global lock for GPU operations
|
||||
gpu_lock = asyncio.Lock()
|
||||
|
||||
@app.post("/remove_background/")
|
||||
async def remove_background(file: UploadFile = File(...), method: str = Form(...)):
|
||||
try:
|
||||
# normalize aliases
|
||||
method = ALIASES.get(method, method)
|
||||
|
||||
# early reject unknown methods with 400, not 500
|
||||
if method not in ALLOWED_METHODS:
|
||||
raise HTTPException(status_code=400, detail="Invalid method")
|
||||
|
||||
image_data = await file.read()
|
||||
image = Image.open(io.BytesIO(image_data)).convert('RGB')
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
async def process_image():
|
||||
if method == 'bria':
|
||||
return await asyncio.to_thread(process_with_bria, image)
|
||||
elif method == 'inspyrenet':
|
||||
async with gpu_lock:
|
||||
try:
|
||||
inspyrenet_model.model.to('cuda')
|
||||
result = await asyncio.to_thread(inspyrenet_model.process, image, type='rgba')
|
||||
finally:
|
||||
inspyrenet_model.model.to('cpu')
|
||||
return result
|
||||
elif method in ['u2net_human_seg', 'isnet-general-use', 'isnet-anime']:
|
||||
return await asyncio.to_thread(process_with_rembg, image, model=method)
|
||||
elif method == 'ormbg':
|
||||
return await asyncio.to_thread(process_with_ormbg, image)
|
||||
elif method in ['u2net', 'tracer', 'basnet', 'deeplab']:
|
||||
async with gpu_lock:
|
||||
try:
|
||||
carvekit_models[method].segmentation_pipeline.to('cuda')
|
||||
result = await asyncio.to_thread(carvekit_models[method], [image])
|
||||
finally:
|
||||
carvekit_models[method].segmentation_pipeline.to('cpu')
|
||||
return result[0]
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid method")
|
||||
|
||||
no_bg_image = await process_image()
|
||||
|
||||
process_time = time.time() - start_time
|
||||
print(f"Background removal time ({method}): {process_time:.2f} seconds")
|
||||
|
||||
async with gpu_lock:
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
with io.BytesIO() as output:
|
||||
no_bg_image.save(output, format="PNG")
|
||||
content = output.getvalue()
|
||||
|
||||
return Response(content=content, media_type="image/png")
|
||||
|
||||
except HTTPException as he:
|
||||
# let 4xx bubble as-is
|
||||
raise he
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
async def process_frame(frame_path, method):
|
||||
img = Image.open(frame_path).convert('RGB')
|
||||
|
||||
if method == 'bria':
|
||||
processed_frame = await asyncio.to_thread(process_with_bria, img)
|
||||
elif method in ['u2net_human_seg', 'isnet-general-use', 'isnet-anime']:
|
||||
processed_frame = await asyncio.to_thread(process_with_rembg, img, model=method)
|
||||
elif method == 'ormbg':
|
||||
processed_frame = await asyncio.to_thread(process_with_ormbg, img)
|
||||
else:
|
||||
raise ValueError("Invalid method")
|
||||
|
||||
return processed_frame
|
||||
|
||||
async def process_video(video_path, method, video_id):
|
||||
try:
|
||||
processing_status[video_id] = {'status': 'processing', 'progress': 0, 'message': 'Initializing'}
|
||||
|
||||
logger.info(f"Starting video processing: {video_path}")
|
||||
logger.info(f"Method: {method}")
|
||||
logger.info(f"Video ID: {video_id}")
|
||||
|
||||
|
||||
# Check video frame count
|
||||
frame_count_command = ['ffmpeg.ffprobe', '-v', 'error', '-select_streams', 'v:0', '-count_packets',
|
||||
'-show_entries', 'stream=nb_read_packets', '-of', 'csv=p=0', video_path]
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*frame_count_command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.error(f"Error counting frames: {stderr.decode()}")
|
||||
processing_status[video_id] = {'status': 'error', 'message': 'Error counting frames'}
|
||||
return
|
||||
|
||||
frame_count = int(stdout.decode().strip())
|
||||
logger.info(f"Video frame count: {frame_count}")
|
||||
|
||||
#DISABLED VIDEO LENGTH LIMIT
|
||||
#if frame_count > 250:
|
||||
# logger.warning(f"Video too long: {frame_count} frames")
|
||||
# processing_status[video_id] = {'status': 'error', 'message': 'Video too long (max 250 frames)'}
|
||||
# return
|
||||
|
||||
# Create a unique directory for this video's frames
|
||||
frames_dir = os.path.join(FRAMES_DIR, video_id)
|
||||
os.makedirs(frames_dir, exist_ok=True)
|
||||
logger.info(f"Created frames directory: {frames_dir}")
|
||||
|
||||
# Extract frames from video
|
||||
processing_status[video_id] = {'status': 'processing', 'progress': 0, 'message': 'Extracting frames'}
|
||||
extract_command = ['ffmpeg', '-i', video_path, f'{frames_dir}/frame_%05d.png']
|
||||
logger.info(f"Executing frame extraction command: {' '.join(extract_command)}")
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*extract_command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.error(f"Error extracting frames: {stderr.decode()}")
|
||||
processing_status[video_id] = {'status': 'error', 'message': 'Error extracting frames'}
|
||||
return
|
||||
|
||||
# Process frames
|
||||
processing_status[video_id] = {'status': 'processing', 'progress': 0, 'message': 'Removing background'}
|
||||
frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])
|
||||
total_frames = len(frame_files)
|
||||
logger.info(f"Number of extracted frames: {total_frames}")
|
||||
|
||||
if total_frames == 0:
|
||||
logger.error("No frames were extracted from the video")
|
||||
processing_status[video_id] = {'status': 'error', 'message': 'No frames were extracted from the video'}
|
||||
return
|
||||
|
||||
# Initialize the model once, outside the batch processing loop
|
||||
if method == 'inspyrenet':
|
||||
print("start init")
|
||||
model_context = inspyrenet_video_model_context()
|
||||
print("start enter")
|
||||
model = model_context.__enter__()
|
||||
print("finish enter")
|
||||
elif method in ['u2net', 'tracer', 'basnet', 'deeplab']:
|
||||
model_context = carvekit_video_model_context(method)
|
||||
model = model_context.__enter__()
|
||||
else:
|
||||
model = None # For other methods that don't require a specific model
|
||||
|
||||
try:
|
||||
async def process_frame_batch(start_idx, end_idx):
|
||||
for i in range(start_idx, min(end_idx, total_frames)):
|
||||
frame_file = frame_files[i]
|
||||
frame_path = os.path.join(frames_dir, frame_file)
|
||||
img = Image.open(frame_path).convert('RGB')
|
||||
|
||||
if method == 'inspyrenet':
|
||||
processed_frame = model.process(img, type='rgba')
|
||||
elif method in ['u2net', 'tracer', 'basnet', 'deeplab']:
|
||||
processed_frame = model([img])[0]
|
||||
else:
|
||||
processed_frame = await process_frame(frame_path, method)
|
||||
|
||||
processed_frame.save(frame_path, format='PNG')
|
||||
progress = (i + 1) / total_frames * 100
|
||||
processing_status[video_id] = {'status': 'processing', 'progress': progress}
|
||||
|
||||
batch_size = 3
|
||||
for i in range(0, total_frames, batch_size):
|
||||
await process_frame_batch(i, i + batch_size)
|
||||
await asyncio.sleep(0) # Allow other tasks to run
|
||||
|
||||
finally:
|
||||
# Ensure we clean up the model context
|
||||
if method in ['inspyrenet', 'u2net', 'tracer', 'basnet', 'deeplab']:
|
||||
model_context.__exit__(None, None, None)
|
||||
|
||||
# Create output video
|
||||
processing_status[video_id] = {'status': 'processing', 'progress': 100, 'message': 'Encoding video'}
|
||||
output_path = os.path.join(TEMP_VIDEOS_DIR, f"output_{video_id}.webm")
|
||||
create_video_command = [
|
||||
'ffmpeg',
|
||||
'-framerate', '24',
|
||||
'-i', f'{frames_dir}/frame_%05d.png',
|
||||
'-c:v', 'libvpx-vp9',
|
||||
'-pix_fmt', 'yuva420p',
|
||||
'-lossless', '1',
|
||||
output_path
|
||||
]
|
||||
logger.info(f"Executing video creation command: {' '.join(create_video_command)}")
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*create_video_command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.error(f"Error creating output video: {stderr.decode()}")
|
||||
processing_status[video_id] = {'status': 'error', 'message': 'Error creating output video'}
|
||||
return
|
||||
|
||||
logger.info(f"Video processing completed. Output path: {output_path}")
|
||||
processing_status[video_id] = {'status': 'completed', 'output_path': output_path}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error in video processing")
|
||||
processing_status[video_id] = {'status': 'error', 'message': str(e)}
|
||||
finally:
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# Clean up frames directory
|
||||
for file in os.listdir(frames_dir):
|
||||
os.remove(os.path.join(frames_dir, file))
|
||||
os.rmdir(frames_dir)
|
||||
logger.info(f"Cleaned up frames directory: {frames_dir}")
|
||||
|
||||
@app.post("/remove_background_video/")
|
||||
async def remove_background_video(background_tasks: BackgroundTasks, file: UploadFile = File(...), method: str = Form(...)):
|
||||
try:
|
||||
logger.info(f"Starting video background removal with method: {method}")
|
||||
|
||||
# Generate a unique filename for the uploaded video
|
||||
video_id = str(uuid.uuid4())
|
||||
filename = f"input_{video_id}.mp4"
|
||||
file_path = os.path.join(TEMP_VIDEOS_DIR, filename)
|
||||
|
||||
# Save uploaded video to the temp_videos folder
|
||||
with open(file_path, "wb") as buffer:
|
||||
content = await file.read()
|
||||
buffer.write(content)
|
||||
|
||||
logger.info(f"Video file saved: {file_path}")
|
||||
logger.info(f"File exists: {os.path.exists(file_path)}")
|
||||
logger.info(f"File size: {os.path.getsize(file_path)} bytes")
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create video file: {file_path}")
|
||||
|
||||
# Start processing in the background
|
||||
background_tasks.add_task(process_video, file_path, method, video_id)
|
||||
|
||||
return {"video_id": video_id}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in video processing: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Error in video processing: {str(e)}")
|
||||
|
||||
@app.get("/status/{video_id}")
|
||||
async def get_status(video_id: str):
|
||||
if video_id not in processing_status:
|
||||
raise HTTPException(status_code=404, detail="Video ID not found")
|
||||
|
||||
status = processing_status[video_id]
|
||||
|
||||
if status['status'] == 'completed':
|
||||
output_path = status['output_path']
|
||||
if not os.path.exists(output_path):
|
||||
raise HTTPException(status_code=404, detail="Processed video file not found")
|
||||
|
||||
return FileResponse(output_path, media_type="video/webm", filename=f"processed_video_{video_id}.webm")
|
||||
|
||||
return status
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
asyncio.create_task(cleanup_old_videos())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=9876)
|
||||
|
||||
31
bgbye/server/setup.sh
Executable file
31
bgbye/server/setup.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Update package list
|
||||
sudo apt update
|
||||
|
||||
# Install system dependencies
|
||||
sudo apt install -y python3-pip python3-venv pngcrush libjpeg-turbo-progs jpegoptim \
|
||||
libgl1-mesa-glx libglib2.0-0 libsm6 libxext6 libxrender-dev ffmpeg
|
||||
|
||||
# Create and activate a virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Install Python dependencies
|
||||
pip install numpy==1.26.4 #WEIRD IMPORT ERROR WORKAROUND FOR REMBG
|
||||
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
||||
pip install fastapi uvicorn transformers pillow scikit-image transparent-background rembg opencv-python-headless python-multipart requests
|
||||
pip install carvekit #--extra-index-url https://download.pytorch.org/whl/cu121
|
||||
|
||||
# Ensure stuff is in the PATH
|
||||
echo 'export PATH=$PATH:$HOME/.local/bin' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# Install additional requirements if there's a requirements.txt file
|
||||
if [ -f requirements.txt ]; then
|
||||
pip install -r requirements.txt
|
||||
fi
|
||||
|
||||
mkdir -p ~/.ormbg && echo "Downloading the ORMBG model..." && wget -O ~/.ormbg/ormbg.pth https://huggingface.co/schirrmacher/ormbg/resolve/main/models/ormbg.pth
|
||||
|
||||
echo "Setup completed successfully!"
|
||||
39
bgbye/src/App.css
Normal file
39
bgbye/src/App.css
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
93
bgbye/src/App.js
Normal file
93
bgbye/src/App.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, ThemeProvider, createTheme, Paper, FormGroup, FormControlLabel, Checkbox, Divider, Typography } from '@mui/material';
|
||||
import { Toaster, toast } from 'react-hot-toast';
|
||||
import ImageUpload from './components/ImageUpload';
|
||||
import ResponsiveAppBar from './components/ResponsiveAppBar';
|
||||
import MethodSelector from './components/MethodSelector';
|
||||
import ModelsInfo from './components/ModelsInfo';
|
||||
|
||||
// Utility function to show error toast
|
||||
const showErrorToast = (message) => {
|
||||
toast.error(message, {
|
||||
duration: 4000,
|
||||
position: 'bottom-center',
|
||||
style: {
|
||||
background: '#FF4136',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const APP_ID = 'bgbye';
|
||||
|
||||
function App() {
|
||||
const [processedPanels, setProcessedPanels] = useState(0);
|
||||
const [darkMode, setDarkMode] = useState(() => {
|
||||
const storedTheme = localStorage.getItem(APP_ID + '_theme');
|
||||
return storedTheme !== null ? storedTheme === 'true' : false;
|
||||
});
|
||||
const [selectedModels, setSelectedModels] = useState(
|
||||
Object.keys(ModelsInfo).reduce((acc, key) => {
|
||||
acc[key] = true;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const handleProcessed = () => {
|
||||
setProcessedPanels((prevCount) => prevCount + 1);
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
setDarkMode((prevMode) => !prevMode);
|
||||
localStorage.setItem(APP_ID + '_theme', !darkMode);
|
||||
};
|
||||
|
||||
const handleModelChange = (event) => {
|
||||
setSelectedModels({
|
||||
...selectedModels,
|
||||
[event.target.name]: event.target.checked
|
||||
});
|
||||
};
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: darkMode ? 'dark' : 'light',
|
||||
background: {
|
||||
default: darkMode
|
||||
? 'linear-gradient(180deg, #10364a 30%, #0d171c 90%)'
|
||||
: 'linear-gradient(180deg, #cbdbf2 30%, #b0ccff 90%)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
background: theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<ResponsiveAppBar toggleTheme={toggleTheme} darkMode={darkMode}/>
|
||||
{Array.from({ length: processedPanels + 1 }).map((_, index) => (
|
||||
<Box key={index} sx={{ mt: 4 }}>
|
||||
<ImageUpload
|
||||
onProcessed={handleProcessed}
|
||||
theme={theme}
|
||||
fileID={index}
|
||||
selectedModels={selectedModels}
|
||||
showErrorToast={showErrorToast}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
<MethodSelector selectedModels={selectedModels} handleModelChange={handleModelChange} />
|
||||
</Box>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
8
bgbye/src/App.test.js
Normal file
8
bgbye/src/App.test.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
7
bgbye/src/TODO.txt
Normal file
7
bgbye/src/TODO.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Improve UI:
|
||||
|
||||
add dropdown for download types
|
||||
- webp/png
|
||||
- mov/gif/webm
|
||||
|
||||
add download video progressbar
|
||||
48
bgbye/src/components/GradientPickerPopout.js
Normal file
48
bgbye/src/components/GradientPickerPopout.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import ColorPicker from 'react-best-gradient-color-picker';
|
||||
import ColorizeIcon from '@mui/icons-material/Colorize';
|
||||
|
||||
function GradientPickerPopout({ buttonLabel, color, onChange }) {
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleChange = (newColor) => {
|
||||
if (onChange) {
|
||||
onChange(newColor);
|
||||
}
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'simple-popover' : undefined;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button aria-describedby={id} variant="contained" onClick={handleClick} endIcon={<ColorizeIcon/>} fullWidth>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<ColorPicker value={color} onChange={handleChange} />
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GradientPickerPopout;
|
||||
630
bgbye/src/components/ImageUpload.js
Normal file
630
bgbye/src/components/ImageUpload.js
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
useTheme,
|
||||
CircularProgress,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import axios from 'axios';
|
||||
import { ImgComparisonSlider } from '@img-comparison-slider/react';
|
||||
import pLimit from 'p-limit';
|
||||
import GradientPickerPopout from './GradientPickerPopout';
|
||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||
import Magnifier from 'react18-image-magnifier'
|
||||
import ModelsInfo from './ModelsInfo';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||
import GradientIcon from '@mui/icons-material/Gradient';
|
||||
|
||||
const ImageUpload = ({ onProcessed, fileID, selectedModels, showErrorToast }) => {
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [originalFilename, setOriginalFilename] = useState('');
|
||||
const [processedFiles, setProcessedFiles] = useState({});
|
||||
const [activeMethod, setActiveMethod] = useState(null);
|
||||
const [processing, setProcessing] = useState({});
|
||||
const [localSelectedModels, setLocalSelectedModels] = useState(null);
|
||||
const [fileType, setFileType] = useState(null);
|
||||
const [videoMethod, setVideoMethod] = useState('');
|
||||
const [videoId, setVideoId] = useState(null);
|
||||
const [videoProgress, setVideoProgress] = useState(0);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
|
||||
const [doZoom, setDoZoom] = useState(false);
|
||||
|
||||
const [transparent, setTransparent] = useState(true);
|
||||
const [colorBG, setColorBG] = useState('radial-gradient(circle, #fcdfa4 0%, #ffd83b 100%)'); //useState('radial-gradient(circle, #87CEFA 0%, #1E90FF 100%)');
|
||||
|
||||
const [imageWidth, setImageWidth] = useState('500px'); // Default width
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFile) {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
setImageWidth(`${image.width}px`);
|
||||
};
|
||||
image.src = selectedFile;
|
||||
}
|
||||
}, [selectedFile]);
|
||||
|
||||
|
||||
const theme = useTheme();
|
||||
const isPortrait = useMediaQuery('(orientation: portrait)');
|
||||
|
||||
const fileInputID = "fileInput" + fileID.toString();
|
||||
|
||||
const getModelAPIURL = (method)=>{
|
||||
console.log(method, ModelsInfo[method].apiUrlVar, process.env[ModelsInfo[method].apiUrlVar]);
|
||||
|
||||
return process.env[ModelsInfo[method].apiUrlVar];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!localSelectedModels) {
|
||||
setLocalSelectedModels(selectedModels);
|
||||
}
|
||||
}, [selectedModels, localSelectedModels]);
|
||||
|
||||
const dropZoneRef = useRef(null);
|
||||
|
||||
|
||||
|
||||
const processFile = useCallback(async (file, method) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('method', method);
|
||||
const isVideo = file.type.startsWith('video');
|
||||
const endpoint = isVideo ? 'remove_background_video' : 'remove_background';
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${getModelAPIURL(method)}/${endpoint}/`, formData, {
|
||||
responseType: 'blob',
|
||||
withCredentials: false,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
const fileUrl = URL.createObjectURL(response.data);
|
||||
return fileUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${isVideo ? 'video' : 'image'} with ${method}:`, error);
|
||||
showErrorToast(`Error processing ${isVideo ? 'video' : 'image'} with ${method}`);
|
||||
}
|
||||
return null;
|
||||
}, [showErrorToast]);
|
||||
|
||||
const handleFileUpload = useCallback(async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const isVideo = file.type.startsWith('video');
|
||||
setFileType(isVideo ? 'video' : 'image');
|
||||
setSelectedFile(URL.createObjectURL(file));
|
||||
setOriginalFilename(file.name);
|
||||
setProcessedFiles({});
|
||||
setActiveMethod(null);
|
||||
|
||||
const currentSelectedModels = {...selectedModels};
|
||||
setLocalSelectedModels(currentSelectedModels);
|
||||
|
||||
if (!isVideo) {
|
||||
const initialProcessing = Object.fromEntries(
|
||||
Object.entries(currentSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) => [method, true])
|
||||
);
|
||||
setProcessing(initialProcessing);
|
||||
|
||||
// Create a limit function that allows only 6 concurrent operations
|
||||
const limit = pLimit(6);
|
||||
|
||||
// Create an array of promises
|
||||
const promises = Object.entries(currentSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) =>
|
||||
limit(() => processFile(file, method).then(result => {
|
||||
setProcessedFiles(prev => ({...prev, [method]: result}));
|
||||
setProcessing(prev => ({...prev, [method]: false}));
|
||||
if (!activeMethod) {
|
||||
setActiveMethod(method);
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
// Wait for all promises to resolve
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
onProcessed();
|
||||
}, [processFile, onProcessed, selectedModels, activeMethod]);
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
setDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (event) => {
|
||||
event.preventDefault();
|
||||
setDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
setDragOver(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file) {
|
||||
const fakeEvent = { target: { files: [file] } };
|
||||
handleFileUpload(fakeEvent);
|
||||
}
|
||||
}, [handleFileUpload]);
|
||||
|
||||
const handlePaste = useCallback((event) => {
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
const blob = items[i].getAsFile();
|
||||
const file = new File([blob], "pasted-image.png", { type: blob.type });
|
||||
const fakeEvent = { target: { files: [file] } };
|
||||
handleFileUpload(fakeEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [handleFileUpload]);
|
||||
|
||||
useEffect(() => {
|
||||
const dropZone = dropZoneRef.current;
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('paste', handlePaste);
|
||||
return () => {
|
||||
dropZone.removeEventListener('paste', handlePaste);
|
||||
};
|
||||
}
|
||||
}, [handlePaste]);
|
||||
|
||||
const handleMethodChange = (event, newMethod) => {
|
||||
if (newMethod !== null) {
|
||||
setActiveMethod(newMethod);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoMethodChange = (event) => {
|
||||
setVideoMethod(event.target.value);
|
||||
};
|
||||
|
||||
const pollVideoStatus = useCallback(async (id, url) => {
|
||||
try {
|
||||
const response = await axios.get(`${url}/status/${id}`, {
|
||||
responseType: 'blob',
|
||||
withCredentials:false
|
||||
});
|
||||
|
||||
// Check if the response is JSON (status update) or blob (completed video)
|
||||
const contentType = response.headers['content-type'];
|
||||
if (contentType && contentType.indexOf('application/json') !== -1) {
|
||||
// It's a JSON response (status update)
|
||||
const data = await response.data.text().then(JSON.parse);
|
||||
if (data.status === 'processing') {
|
||||
setVideoProgress(data.progress);
|
||||
setStatusMessage(data.message);
|
||||
setTimeout(() => pollVideoStatus(id, url), 4000); // Poll every second
|
||||
} else if (data.status === 'error') {
|
||||
showErrorToast('Error processing video: ' + data.message);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
setStatusMessage('Error: ' + data.message);
|
||||
}
|
||||
} else {
|
||||
// It's a blob response (completed video)
|
||||
setVideoProgress(100);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
setActiveMethod(videoMethod);
|
||||
setStatusMessage('Processing complete');
|
||||
|
||||
const url = URL.createObjectURL(response.data);
|
||||
setProcessedFiles({ [videoMethod]: url });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error polling video status:', error);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
setStatusMessage('Error: Failed to get status update');
|
||||
showErrorToast('Error: Failed to get status update');
|
||||
}
|
||||
}, [videoMethod, showErrorToast]);
|
||||
|
||||
const getVideoDuration = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
|
||||
video.onloadedmetadata = function() {
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
resolve(video.duration);
|
||||
}
|
||||
|
||||
video.onerror = function() {
|
||||
reject("Invalid video. Please select another video file.");
|
||||
}
|
||||
|
||||
video.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const handleProcessVideo = async () => {
|
||||
if (!selectedFile || !videoMethod) return;
|
||||
|
||||
setProcessing({ [videoMethod]: true });
|
||||
setVideoProgress(0);
|
||||
|
||||
try {
|
||||
const file = await fetch(selectedFile).then(r => r.blob());
|
||||
|
||||
// Estimate frame count
|
||||
const duration = await getVideoDuration(file);
|
||||
const estimatedFrameCount = Math.ceil(duration * 24); // Assuming 24 fps
|
||||
|
||||
//DISABLED VIDEO LENGTH LIMIT
|
||||
//if (estimatedFrameCount > 250) {
|
||||
// if (duration > 10){
|
||||
// showErrorToast(`Video too long (${estimatedFrameCount} estimated frames). Maximum allowed: 10 seconds.`);
|
||||
// setProcessing({ [videoMethod]: false });
|
||||
// return;
|
||||
// }
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('method', videoMethod);
|
||||
|
||||
const response = await axios.post(`${getModelAPIURL(videoMethod)}/remove_background_video/`, formData, {
|
||||
withCredentials: false,
|
||||
});
|
||||
|
||||
|
||||
setVideoId(response.data.video_id);
|
||||
pollVideoStatus(response.data.video_id, getModelAPIURL(videoMethod));
|
||||
} catch (error) {
|
||||
console.error('Error processing video:', error);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
showErrorToast('Error processing video: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (fileType === 'video') {
|
||||
// Directly download the video file as a .webm
|
||||
const newFilename = `${originalFilename.split('.')[0]}_${activeMethod}.webm`;
|
||||
const link = document.createElement('a');
|
||||
link.href = processedFiles[activeMethod];
|
||||
link.download = newFilename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
// Handle image downloading with canvas manipulations
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const image = new Image();
|
||||
image.src = processedFiles[activeMethod];
|
||||
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
|
||||
if (fileType === 'image' && transparent === false) {
|
||||
if (colorBG.includes("gradient")) {
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.style.display = 'none'; // Hide the div while it's appended
|
||||
tempDiv.style.background = colorBG;
|
||||
document.body.appendChild(tempDiv);
|
||||
const computedStyle = window.getComputedStyle(tempDiv);
|
||||
const bgImage = computedStyle.backgroundImage;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
if (bgImage.startsWith('linear-gradient')) {
|
||||
parseLinearGradient(ctx, bgImage, canvas.width, canvas.height);
|
||||
} else if (bgImage.startsWith('radial-gradient')) {
|
||||
parseRadialGradient(ctx, bgImage, canvas.width, canvas.height);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = colorBG;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
const fileExtension = 'png';
|
||||
const newFilename = `${originalFilename.split('.')[0]}_${activeMethod}.${fileExtension}`;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = canvas.toDataURL(`image/${fileExtension}`);
|
||||
link.download = newFilename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function parseLinearGradient(ctx, bgImage, width, height) {
|
||||
const colors = bgImage.match(/rgba?\([^)]+\)/g);
|
||||
const linearGradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||
colors.forEach((color, index) => {
|
||||
const position = index / (colors.length - 1);
|
||||
linearGradient.addColorStop(position, color);
|
||||
});
|
||||
ctx.fillStyle = linearGradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
function parseRadialGradient(ctx, bgImage, width, height) {
|
||||
const colors = bgImage.match(/rgba?\([^)]+\)/g);
|
||||
const radialGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height) / 2);
|
||||
colors.forEach((color, index) => {
|
||||
const position = index / (colors.length - 1);
|
||||
radialGradient.addColorStop(position, color);
|
||||
});
|
||||
ctx.fillStyle = radialGradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={dropZoneRef}
|
||||
tabIndex="0" // Make the box focusable
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
|
||||
// This allows the paste event to fire when the box is focused
|
||||
// and the user presses Ctrl+V or Cmd+V
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
border: (!selectedFile) ? '2px dashed' : 'none',
|
||||
borderColor: dragOver ? theme.palette.primary.main : theme.palette.text.disabled,
|
||||
borderRadius: 1,
|
||||
p: isPortrait ? 0 : 2,
|
||||
mt: 2,
|
||||
textAlign: 'center',
|
||||
cursor: !selectedFile && !processing ? 'pointer' : 'default',
|
||||
position: 'relative',
|
||||
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'transparent',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
onClick={() => !selectedFile && !Object.values(processing).some(Boolean) && document.getElementById(fileInputID).click()}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{!selectedFile && (
|
||||
<input
|
||||
type="file"
|
||||
id={fileInputID}
|
||||
accept="image/*,video/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedFile ? (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: isPortrait ? 'column' : 'row',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
maxWidth: '1024px',
|
||||
position: 'relative',
|
||||
}}>
|
||||
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
maxWidth: '1280px',
|
||||
mr: isPortrait ? 0 : 2,
|
||||
mb: isPortrait ? 2 : 0,
|
||||
width: '100%',
|
||||
}}>
|
||||
{fileType === 'image' ? (
|
||||
processedFiles[activeMethod] ? (
|
||||
<div
|
||||
className={transparent ? "checkerboard" : ""}
|
||||
style={!transparent ? { background: colorBG } : {}}
|
||||
>
|
||||
<ToggleButton
|
||||
value="zoom"
|
||||
selected={doZoom}
|
||||
onChange={() => setDoZoom(!doZoom)}
|
||||
aria-label="zoom in"
|
||||
size='small'
|
||||
color='primary'
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '3em',
|
||||
left: '3em',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<ZoomInIcon color='primary'/>
|
||||
</ToggleButton>
|
||||
|
||||
{doZoom && <Magnifier src={processedFiles[activeMethod]} width={'100%'}/>}
|
||||
|
||||
{!doZoom && <ImgComparisonSlider class="slider-example-focus">
|
||||
<img slot="first" src={selectedFile} alt="Original" style={{ width: '100%' }} />
|
||||
<img slot="second" src={processedFiles[activeMethod]} alt="Processed" style={{ width: '100%' }} />
|
||||
{false && <svg slot="handle" xmlns="http://www.w3.org/2000/svg" width="100" viewBox="-8 -3 16 6">
|
||||
<path stroke="#549ef7" d="M -5 -2 L -7 0 L -5 2 M -5 -2 L -5 2 M 5 -2 L 7 0 L 5 2 M 5 -2 L 5 2" strokeWidth="1" fill="#549ef7" vectorEffect="non-scaling-stroke"></path>
|
||||
</svg>}
|
||||
</ImgComparisonSlider>}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<CircularProgress style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} />
|
||||
<img src={selectedFile} alt="Uploaded" style={{ width: '100%', display: "block", boxShadow: '0px 0px 10px 5px #6464647a' }} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
) : (
|
||||
<video src={processedFiles[activeMethod] || selectedFile} controls style={{ width: '100%' }}>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: isPortrait ? 'absolute' : 'static',
|
||||
top: isPortrait ? '1em' : 'auto',
|
||||
right: isPortrait ? '1em' : 'auto',
|
||||
zIndex: isPortrait ? 1000 : 'auto',
|
||||
}}>
|
||||
{(!isPortrait || fileType !== 'video') && <Paper sx={{
|
||||
backgroundColor: isPortrait ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0)',
|
||||
padding: isPortrait ? 1 : 0,
|
||||
}} elevation={2}>
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
Methods
|
||||
</Typography>
|
||||
{selectedFile && localSelectedModels && fileType === 'image' && (
|
||||
<ToggleButtonGroup
|
||||
orientation="vertical"
|
||||
fullWidth
|
||||
value={activeMethod}
|
||||
exclusive
|
||||
color="warning"
|
||||
onChange={handleMethodChange}
|
||||
aria-label="background removal method"
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{Object.entries(localSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) => (
|
||||
<ToggleButton
|
||||
size="small"
|
||||
|
||||
key={method}
|
||||
value={method}
|
||||
aria-label={`${method} method`}
|
||||
disabled={processing[method]}
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="flex-start" width="100%">
|
||||
{isPortrait ? ModelsInfo[method].shortName : ModelsInfo[method].displayName}
|
||||
{processing[method] && <CircularProgress size={16} sx={{ ml: 1 }} />}
|
||||
</Box>
|
||||
</ToggleButton>
|
||||
))
|
||||
}
|
||||
</ToggleButtonGroup>
|
||||
)}
|
||||
</Paper>}
|
||||
{selectedFile && fileType === 'video' && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="video-method-label" >Method</InputLabel>
|
||||
<Select
|
||||
disabled={processing[videoMethod] || Object.keys(processedFiles).length > 0}
|
||||
labelId="video-method-label"
|
||||
value={videoMethod}
|
||||
label="Method"
|
||||
sx={{backgroundColor:isPortrait? theme.palette.info.contrastText :''}}
|
||||
onChange={handleVideoMethodChange}
|
||||
>
|
||||
{Object.entries(localSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) => (
|
||||
<MenuItem key={method} value={method}>{ModelsInfo[method].displayName}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{Object.keys(processedFiles).length === 0 && <Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleProcessVideo}
|
||||
disabled={!videoMethod || Object.values(processing).some(Boolean)}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Process Video
|
||||
{processing[videoMethod] && <CircularProgress size={16} sx={{ ml: 1 }} />}
|
||||
</Button>}
|
||||
{processing[videoMethod] && (
|
||||
<Box sx={{ width: '100%', mt: 2 }}>
|
||||
<LinearProgress
|
||||
variant={videoProgress === 100 ? "indeterminate" : "determinate"}
|
||||
value={videoProgress}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
{statusMessage} {videoProgress < 100 && `(${Math.round(videoProgress)}%)`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{processedFiles[activeMethod] && (
|
||||
<>
|
||||
|
||||
|
||||
{!isPortrait && fileType=='image' && <FormControlLabel
|
||||
control={<Checkbox checked={transparent} onChange={(e)=>setTransparent(e.target.checked)} />}
|
||||
label="Transparent"
|
||||
sx={{color:theme.palette.text.primary}}
|
||||
/>}
|
||||
|
||||
{isPortrait && fileType=='image' && <ToggleButton sx={{backgroundColor:theme.palette.divider, p:0}} value="transparent" selected={!transparent} onChange={()=>{setTransparent(!transparent)}}><GradientIcon fontSize='large' color='primary'/></ToggleButton>}
|
||||
|
||||
{!transparent && fileType=='image' && <GradientPickerPopout
|
||||
buttonLabel={!isPortrait ? "Background" : ""}
|
||||
|
||||
color={colorBG}
|
||||
onChange={newColor => setColorBG(newColor)}
|
||||
/>}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleDownload}
|
||||
endIcon={<DownloadIcon />}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{!isPortrait && "Download"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="h6" sx={{ color: theme.palette.text.primary }}>
|
||||
{dragOver ? "Click, drag and drop, or paste (ctrl-v) to upload an image or video" : "Click, drag and drop, or paste (ctrl-v) to upload an image or video"}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
80
bgbye/src/components/MethodSelector.js
Normal file
80
bgbye/src/components/MethodSelector.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Popover, FormGroup, FormControlLabel, Checkbox, Typography, Divider, IconButton } from '@mui/material';
|
||||
import ChecklistIcon from '@mui/icons-material/Checklist';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||
import ModelsInfo from './ModelsInfo';
|
||||
|
||||
const MethodSelector = ({ selectedModels, handleModelChange }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'method-selector-popover' : undefined;
|
||||
|
||||
const handleInfoClick = (apiUrl) => {
|
||||
window.open(apiUrl, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" onClick={handleClick} endIcon={<ChecklistIcon/>} size='small' sx={{mt:1}}>
|
||||
Select Methods
|
||||
</Button>
|
||||
<Popover
|
||||
elevation={2}
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<FormGroup sx={{ p: 1 }}>
|
||||
{Object.keys(selectedModels).map((model, index) => (
|
||||
<React.Fragment key={model}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={selectedModels[model]}
|
||||
onChange={handleModelChange}
|
||||
name={model}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{ModelsInfo[model].displayName}</span>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleInfoClick(ModelsInfo[model].sourceUrl)}
|
||||
style={{ marginLeft: '8px' }}
|
||||
>
|
||||
<HelpOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{index < Object.keys(selectedModels).length - 1 && (
|
||||
<Divider orientation="horizontal" flexItem />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MethodSelector;
|
||||
65
bgbye/src/components/ModelsInfo.js
Normal file
65
bgbye/src/components/ModelsInfo.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// modelsInfo.js
|
||||
const ModelsInfo = {
|
||||
bria: {
|
||||
displayName: 'Bria RMBG1.4',
|
||||
shortName: "Bria",
|
||||
sourceUrl: 'https://huggingface.co/briaai/RMBG-1.4',
|
||||
apiUrlVar: 'REACT_APP_BRIA_URL'
|
||||
},
|
||||
inspyrenet: {
|
||||
displayName: 'InSPyReNet',
|
||||
shortName: "InSPyRe",
|
||||
sourceUrl: 'https://github.com/plemeri/transparent-background/tree/main',
|
||||
apiUrlVar: 'REACT_APP_INSPYRENET_URL'
|
||||
},
|
||||
u2net: {
|
||||
displayName: 'U2Net',
|
||||
shortName: "U2Net",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_U2NET_URL'
|
||||
},
|
||||
tracer: {
|
||||
displayName: 'Tracer-B7',
|
||||
shortName: "Tracer",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_TRACER_URL'
|
||||
},
|
||||
basnet: {
|
||||
displayName: 'BASNet',
|
||||
shortName: "BASNet",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_BASNET_URL'
|
||||
},
|
||||
deeplab: {
|
||||
displayName: 'DeepLabV3',
|
||||
shortName: "DeepLab",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_DEEPLAB_URL'
|
||||
},
|
||||
u2net_human_seg: {
|
||||
displayName: 'U2Net Human',
|
||||
shortName: "U2Net🧍",
|
||||
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
|
||||
apiUrlVar: 'REACT_APP_U2NET_HUMAN_SEG_URL'
|
||||
},
|
||||
ormbg: {
|
||||
displayName: 'Open RMBG',
|
||||
shortName: "ORMBG",
|
||||
sourceUrl: 'https://huggingface.co/schirrmacher/ormbg',
|
||||
apiUrlVar: 'REACT_APP_ORMBG_URL'
|
||||
},
|
||||
'isnet-general-use': {
|
||||
displayName: 'ISNET-DIS',
|
||||
shortName: "DIS",
|
||||
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
|
||||
apiUrlVar: 'REACT_APP_ISNET_GENERAL_URL'
|
||||
},
|
||||
'isnet-anime': {
|
||||
displayName: 'ISNET-Anime',
|
||||
shortName: "Anime",
|
||||
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
|
||||
apiUrlVar: 'REACT_APP_ISNET_ANIME_URL'
|
||||
}
|
||||
};
|
||||
|
||||
export default ModelsInfo;
|
||||
191
bgbye/src/components/ResponsiveAppBar.js
Normal file
191
bgbye/src/components/ResponsiveAppBar.js
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import * as React from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import Container from '@mui/material/Container';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import AdbIcon from '@mui/icons-material/Adb';
|
||||
import { Deblur } from '@mui/icons-material';
|
||||
|
||||
import ModeNightIcon from '@mui/icons-material/ModeNight';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import Brightness3Icon from '@mui/icons-material/Brightness3';
|
||||
//const pages = ['Products', 'Pricing', 'Blog'];
|
||||
const pages = ['About', 'Help'];
|
||||
|
||||
const pageUrls = {
|
||||
About: 'https://fyrean.itch.io/',
|
||||
//Help: '#',
|
||||
};
|
||||
|
||||
const settings = ['Profile', 'Account', 'Dashboard', 'Logout'];
|
||||
|
||||
function ResponsiveAppBar({toggleTheme, darkMode}) {
|
||||
const [anchorElNav, setAnchorElNav] = React.useState(null);
|
||||
const [anchorElUser, setAnchorElUser] = React.useState(null);
|
||||
|
||||
const handleOpenNavMenu = (event) => {
|
||||
setAnchorElNav(event.currentTarget);
|
||||
};
|
||||
const handleOpenUserMenu = (event) => {
|
||||
setAnchorElUser(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseNavMenu = () => {
|
||||
setAnchorElNav(null);
|
||||
};
|
||||
|
||||
const handleCloseUserMenu = () => {
|
||||
setAnchorElUser(null);
|
||||
};
|
||||
|
||||
const handlePageClick = (page) => {
|
||||
if (!pageUrls[page])
|
||||
return;
|
||||
window.open(pageUrls[page], '_blank');
|
||||
handleCloseNavMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="fixed" sx={{}} >
|
||||
<Container maxWidth="xl" >
|
||||
<Toolbar disableGutters variant="dense" sx={{
|
||||
maxHeight:'20px',
|
||||
minHeight:0
|
||||
}}>
|
||||
<Deblur sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
component="a"
|
||||
href="https://bgbye.fyrean.com"
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '.3rem',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
BGBye
|
||||
</Typography>
|
||||
|
||||
{false&&<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none'} }}>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleOpenNavMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElNav}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={Boolean(anchorElNav)}
|
||||
onClose={handleCloseNavMenu}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none'}
|
||||
}}
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<MenuItem key={page} onClick={() => handlePageClick(page)}>
|
||||
<Typography textAlign="center" variant="subtitle2">{page}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem key='theme' onClick={toggleTheme} variant="contained">
|
||||
{!darkMode ? <LightModeIcon /> : <ModeNightIcon />}
|
||||
{!darkMode ? ' Light' : ' Dark'}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>}
|
||||
<Deblur sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
noWrap
|
||||
component="a"
|
||||
href="#app-bar-with-responsive-menu"
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: 'flex', md: 'none' },
|
||||
flexGrow: 0,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '.3rem',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Matxinh
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'flex' } }}>
|
||||
{pages.map((page) => (
|
||||
<Button
|
||||
key={page}
|
||||
onClick={() => handlePageClick(page)}
|
||||
sx={{ my: 2, color: 'white', display: 'block' }}
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
))}
|
||||
<IconButton onClick={toggleTheme} color={darkMode?'primary':'warning'} size="large">
|
||||
{!darkMode ? <LightModeIcon fontSize="inherit" /> : <Brightness3Icon fontSize="inherit" />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{false&&<Box sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title="Open settings">
|
||||
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
||||
<Avatar alt="Remy Sharp" src="/static/images/avatar/2.jpg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
sx={{ mt: '45px' }}
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu}
|
||||
>
|
||||
{settings.map((setting) => (
|
||||
<MenuItem key={setting} onClick={handleCloseUserMenu}>
|
||||
<Typography textAlign="center">{setting}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
</Menu>
|
||||
</Box>}
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
export default ResponsiveAppBar;
|
||||
50
bgbye/src/index.css
Normal file
50
bgbye/src/index.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.slider-example-focus {
|
||||
/* #549ef7 #ffa658
|
||||
The style below isn't necessary.
|
||||
It has been added for the sake of smoothness.
|
||||
|
||||
transition: box-shadow 200ms ease-in-out;*/
|
||||
box-shadow: 0px 0px 10px 5px #6464647a;
|
||||
display: block;
|
||||
|
||||
--default-handle-shadow: 0px 0px 5px rgba(0, 0, 0, 1);
|
||||
--divider-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
|
||||
--divider-width:2;
|
||||
--default-handle-width: 100px;
|
||||
--divider-color: #549ef7;
|
||||
--default-handle-color: #549ef7;
|
||||
stroke-width: 3px;
|
||||
;
|
||||
}
|
||||
|
||||
.slider-example-focus:focus {
|
||||
outline: none;
|
||||
box-shadow: 0px 0px 10px 5px #6464647a;
|
||||
}
|
||||
|
||||
.checkerboard {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="0" y="0" width="10" height="10" fill="%23ccc"/><rect x="10" y="10" width="10" height="10" fill="%23ccc"/><rect x="10" y="0" width="10" height="10" fill="%23fff"/><rect x="0" y="10" width="10" height="10" fill="%23fff"/></svg>');
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.magnifying-glass {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="0" y="0" width="10" height="10" fill="%23ccc"/><rect x="10" y="10" width="10" height="10" fill="%23ccc"/><rect x="10" y="0" width="10" height="10" fill="%23fff"/><rect x="0" y="10" width="10" height="10" fill="%23fff"/></svg>');
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
17
bgbye/src/index.js
Normal file
17
bgbye/src/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
//import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
//reportWebVitals();
|
||||
1
bgbye/src/logo.svg
Normal file
1
bgbye/src/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
13
bgbye/src/reportWebVitals.js
Normal file
13
bgbye/src/reportWebVitals.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
5
bgbye/src/setupTests.js
Normal file
5
bgbye/src/setupTests.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
Loading…
Add table
Add a link
Reference in a new issue