Create monorepo from known-good production state

This commit is contained in:
makearmy 2026-07-09 21:07:34 -04:00
commit c034824338
651 changed files with 120469 additions and 0 deletions

81
bgbye/.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

59
bgbye/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

43
bgbye/public/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
bgbye/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

3
bgbye/server/__init__.py Normal file
View file

@ -0,0 +1,3 @@
# server/__init__.py
__all__ = []

70
bgbye/server/ormbg.py Normal file
View 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

View file

@ -0,0 +1,2 @@
from .ormbg import ORMBG
from .ormbg_processor import ORMBGProcessor

484
bgbye/server/ormbg/ormbg.py Normal file
View 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]

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
Improve UI:
add dropdown for download types
- webp/png
- mov/gif/webm
add download video progressbar

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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
View 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

View 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
View 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';