Compare commits

...
15 Commits
Author SHA1 Message Date
de4dn0te fb651fb147 Merge pull request 'v0.2.0' (#6) from rembg into main
Reviewed-on: #6
2026-07-14 09:42:22 +00:00
de4dn0te cc58bd54d4 Update readme 2026-07-14 09:41:59 +00:00
de4dn0te 7fcbe8d8e7 Cleanup and deterministic output 2026-07-14 09:41:59 +00:00
de4dn0te 3c48ed082b Multiple output formats 2026-07-14 09:41:59 +00:00
de4dn0te bdb69239bc Tiff intermediary 2026-07-14 09:41:59 +00:00
de4dn0te e5601871ac Merge pull request 'Aftermath of CUDA reconfig' (#5) from rembg into main
Reviewed-on: #5
2026-06-29 06:53:09 +00:00
de4dn0te d9ed47aaba Updated Readme 2026-06-29 09:51:27 +03:00
de4dn0te 4a90885527 Cleanup 2026-06-29 09:31:35 +03:00
de4dn0te 931b2af262 EOD 2026-06-26 15:39:39 +03:00
de4dn0te a8f92251a3 Fixed CUDA version mismatch 2026-06-23 14:47:34 +03:00
de4dn0te 1b5f92ccb1 EoD 2026-06-16 15:50:25 +03:00
de4dn0te cba969a113 Change to prores 2026-05-29 15:24:29 +03:00
de4dn0te 855bb4b047 Clean interrupt and cleanup 2026-05-29 13:22:49 +03:00
de4dn0te 9ce219be50 Merge pull request 'Import "Delete .gitmodules" (#4) from main into rembg
Reviewed-on: #4
2026-05-29 07:21:10 +00:00
de4dn0te 355521bac5 Delete .gitmodules 2026-05-29 07:15:39 +00:00
7 changed files with 444 additions and 86 deletions
+4
View File
@@ -3,3 +3,7 @@
/frames
/export
*.mp4
*.webm
*.mov
*.mkv
__pycache__
-6
View File
@@ -1,6 +0,0 @@
[submodule "lib/sys"]
path = lib/sys
url = https://go.googlesource.com/sys
[submodule "lib/fsnotify"]
path = lib/fsnotify
url = https://github.com/fsnotify/fsnotify.git
+1
View File
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2021 Seth Tribbey
Copyright (c) 2026 Uula Ilola
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+6
View File
@@ -0,0 +1,6 @@
{
"venvPath": ".",
"venv": ".venv",
"reportUnusedCallResult": false,
"reportAny": false
}
+56 -24
View File
@@ -1,32 +1,64 @@
# rembg_from_video
# rembg_for_video
Uses [ffmpeg-python](https://github.com/kkroening/ffmpeg-python) and [rembg](https://github.com/danielgatis/rembg) to attempt removal of a background from a video file.
Two directories will be created in the same directory as the script to hold the video frames (before and after rembg is applied).
Based on project [rembg_from_video](https://github.com/seth-tribbey/rembg_from_video) by [seth-tribbey](https://github.com/seth-tribbey)
### Installation:
[Install rembg by following their instructions](https://github.com/danielgatis/rembg)
## Installation:
rembg specifically requires Python 3.9 as of the time of this writing. Note that you must choose the `rembg[gpu]` version and configure onnxruntime accordingly if you wish to use your GPU for the image processing.
Then:
Currently i've gotten rembg to work only with Python **3.12** because of onnxruntime's shenanigans X/
```bash
python3.12 -m venv .venv
pip install -r requirements.txt
```
pip install ffmpeg-python
```
### Usage:
```
python .\rembg_video.py [-h] [-a] [-af AF] [-ab AB] [-ae AE] [--skip-extract] [--skip-process] input
> **Script for fixing the cudnn path on Linux:** <br>
> export LD_LIBRARY_PATH=/path/to/kakisalmi/.venv/lib/python3.12/site-packages/nvidia/cudnn/lib:$LD_LIBRARY_PATH
positional arguments:
input Input video
&nbsp;
optional arguments:
-h, --help show this help message and exit
-a Turns on alpha matting during background removal
-af AF Alpha matting foreground threshold
-ab AB Alpha matting background threshold
-ae AE Alpha matting erode size
--skip-extract Skips ffmpeg frame extraction
--skip-process Skips rembg frame processing
```
Tip: [Alpha matting can be used to refine the results](https://github.com/danielgatis/rembg#advance-usage)
## Usage:
```
python .\rembg_video.py [-h] [--help] [-o] [--model] [--workers] [--smooth] [--smooth-workers] [--buffer-size] [--output-type] input
```
<style>
table {
border-collapse: separate;
border-spacing: 0;
border-radius: 5px;
overflow: hidden;
}
th,
td {
border: 1px solid #a0a0a0;
padding: 8px 10px;
border-radius: 8px;
text-align: center;
}
td:first-child,
th:first-child {
text-align: center;
}
td:nth-child(2),
th:nth-child(2) {
text-align: left;
}
</style>
<div style="display: flex; gap: 20px; align-items: flex-start;">
<table>
<tr><th colspan="2">Optional Arguments:</th></tr>
<tr><td>-o</td><td>Output path </td></tr>
<tr><td>-h --help</td><td>Show help</td></tr>
<tr><td>--model</td><td>Choose model for rembg</td></tr>
<tr><td>--workers</td><td>Number of concurrent process workers</td></tr>
<tr><td>--smooth</td><td>Size of window for temporal smoothing</td></tr>
<tr><td>--smooth-workers</td><td>Number of CPU threads for temporal smoothing</td></tr>
<tr><td>--buffer-size</td><td>Set size of processing buffer</td></tr>
<tr><td>--output-type</td><td>Choose how video is exported
<br>"complete" = Full Color .mov <br>"mask" = Only mask <br>"mask_seq" = Mask image sequence)
</table>
<table>
<tr><th colspan="2">Positional Arguments:</th></tr>
<tr><td>input</td><td>Input Video</td></tr>
</table>
</div>
+326 -47
View File
@@ -1,67 +1,346 @@
import argparse
import io
import os
import ffmpeg
import pathlib
import shutil
from rembg.bg import remove
import threading
import numpy as np
from queue import Queue
from shutil import rmtree, move
from PIL import Image
from rembg import new_session, remove
#Parse args
parser = argparse.ArgumentParser(description='Applies rembg to the frames of a video')
parser.add_argument('input', type=str, help='Input video')
parser.add_argument('-o', type=str, default="export/output.mp4", help="Define output path")
parser.add_argument('-a', action="store_true", help="Turns on alpha matting during background removal")
parser.add_argument('-af', type=int, default=240, help="Alpha matting foreground threshold")
parser.add_argument('-ab', type=int, default=10, help="Alpha matting background threshold")
parser.add_argument('-ae', type=int, default=10, help="Alpha matting erode size")
parser.add_argument('--skip-extract', action="store_true", help='Skips ffmpeg frame extraction')
parser.add_argument('--skip-process', action="store_true", help='Skips rembg frame processing')
os.environ.setdefault(
"PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True,managed_memory:True"
)
# Parse args
parser = argparse.ArgumentParser(
description="Applies rembg background removal to the frames of a video"
)
parser.add_argument("input", type=str, help="Input video")
parser.add_argument("-o", type=str, default="export", help="Define output path")
parser.add_argument(
"--model",
type=str,
default="u2net_human_seg",
help="rembg model to use (default: u2net_human_seg)",
)
parser.add_argument(
"--workers",
type=int,
default=1,
help="Number of concurrent processing workers (default: 1)",
)
parser.add_argument(
"--smooth",
type=int,
default=3,
help="Temporal mask smoothing window size in frames (default: 3, 0 to disable)",
)
parser.add_argument(
"--buffer-size",
type=int,
default=8,
help="Set maximum number of frames in buffer (default: 8)",
)
parser.add_argument(
"--smooth-workers",
type=int,
default=os.cpu_count() or 4,
help="Number of cpu threads to use for temporal mask smoothing (default: cpu count)",
)
parser.add_argument(
"--output-type",
type=str,
choices=["complete", "mask", "mask_seq"],
default="complete",
help="What way to output keyed video. (default: complete)",
)
args = parser.parse_args()
#Extract video info
def is_oom_error(exc):
text = str(exc).lower()
return any(
phrase in text
for phrase in (
"out of memory",
"cuda out of memory",
"failed to allocate",
"oom",
"memory error",
)
)
def image_to_bytes(image):
with io.BytesIO() as buffer:
image.save(buffer, format="TIFF")
return buffer.getvalue()
def remove_with_fallback(image_bytes, session):
try:
rembg_out = remove(image_bytes, session=session)
if args.output_type == "complete":
return rembg_out
elif args.output_type in ("mask", "mask_seq"):
if isinstance(rembg_out, (bytes, bytearray)):
mask_bytes = bytes(rembg_out)
else:
raise TypeError('mask_bytes is not of type "bytes"')
alpha = Image.open(io.BytesIO(mask_bytes)).convert("RGBA").getchannel("A")
return image_to_bytes(alpha)
else:
raise Exception("Unknown output type.")
except Exception as exc:
if is_oom_error(exc):
raise RuntimeError("Insufficient GPU memory!") from exc
else:
raise RuntimeError("Background removal failed unexpectedly.")
# Extract video info
probe = ffmpeg.probe(args.input)
video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
width = int(video_stream['width'])
height = int(video_stream['height'])
whstr = str(width) + 'x' + str(height)
framerate = video_stream['avg_frame_rate']
video_stream = next(
(stream for stream in probe["streams"] if stream["codec_type"] == "video"), None
)
if video_stream is None:
raise ValueError(f"No video stream found in input file: {args.input}")
width = int(video_stream["width"])
height = int(video_stream["height"])
whstr = str(width) + "x" + str(height)
framerate = video_stream["avg_frame_rate"]
#Extract input video frames
if not args.skip_extract:
frames_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "frames")
if not os.path.isdir(frames_dir):
os.mkdir(frames_dir)
# Setup workspace folders
frames_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "frames")
processed_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "processed")
smoothed_dir = processed_dir + "_smoothed"
rmtree(frames_dir, ignore_errors=True)
rmtree(processed_dir, ignore_errors=True)
rmtree(smoothed_dir, ignore_errors=True)
stream = ffmpeg.input(args.input)
stream = ffmpeg.output(stream, os.path.join(frames_dir, "%04d.png"))
ffmpeg.run(stream)
# Extract input video frames
os.mkdir(frames_dir)
stream = ffmpeg.input(args.input)
stream = ffmpeg.output(stream, os.path.join(frames_dir, "%04d.tiff"))
ffmpeg.run(stream)
#Process frames with rembg
if not args.skip_process:
files_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "frames")
processed_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "processed")
_SENTINEL = object()
try:
# Process frames with pipelined reader -> processors -> writer
if not os.path.isdir(processed_dir):
os.mkdir(processed_dir)
files = sorted(os.listdir(files_dir))
files = sorted(os.listdir(frames_dir))
total_files = len(files)
print(f"Loading rembg session (model={args.model})...", flush=True)
session = new_session(
args.model, providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
read_queue = Queue(maxsize=args.buffer_size)
write_queue = Queue(maxsize=args.buffer_size)
errors = []
active_processors = [
args.workers
] # list so processor() can mutate without nonlocal
active_processors_lock = threading.Lock()
def reader():
try:
for idx, file in enumerate(files, 1):
frame_path = os.path.join(frames_dir, file)
with open(frame_path, "rb") as f:
data = f.read()
read_queue.put((idx, file, data))
os.remove(frame_path)
except Exception as e:
errors.append(e)
finally:
# One sentinel per worker so each one knows when to stop
for _ in range(args.workers):
read_queue.put(_SENTINEL)
def processor():
try:
while True:
item = read_queue.get()
if item is _SENTINEL:
break
idx, file, input_data = item
print(f"Processing frame {idx}/{total_files}: {file}", flush=True)
with open(os.path.join(files_dir, file), "rb") as i:
with open(os.path.join(processed_dir, file), "wb") as o:
input = i.read()
output = remove(input, alpha_matting=args.a, alpha_matting_foreground_threshold=args.af, alpha_matting_background_threshold=args.ab, alpha_matting_erode_size=args.ae)
o.write(output)
print(f"Completed frame {idx}/{total_files}", flush=True)
output_data = remove_with_fallback(input_data, session=session)
write_queue.put((idx, file, output_data))
except Exception as e:
errors.append(e)
finally:
# Signal writer only when the last processor finishes
with active_processors_lock:
active_processors[0] -= 1
if active_processors[0] == 0:
write_queue.put(_SENTINEL)
#Output video
output_file = pathlib.Path(args.o)
output_file.parent.mkdir(exist_ok=True, parents=True)
def writer():
try:
while True:
item = write_queue.get()
if item is _SENTINEL:
break
idx, file, output_data = item
with open(os.path.join(processed_dir, file), "wb") as f:
f.write(output_data)
print(f"Written frame {idx}/{total_files}: {file}", flush=True)
except Exception as e:
errors.append(e)
stream = ffmpeg.input(os.path.join(processed_dir, "%04d.png"), r=framerate, f='image2', s=whstr, pix_fmt='yuv420p')
stream = ffmpeg.output(stream, args.o, vcodec='libx264', crf=25)
ffmpeg.run(stream)
reader_thread = threading.Thread(target=reader, daemon=True)
processor_threads = [
threading.Thread(target=processor, daemon=True) for _ in range(args.workers)
]
writer_thread = threading.Thread(target=writer, daemon=True)
#Cleanup
print("Removing temporary files...")
shutil.rmtree(processed_dir)
shutil.rmtree(frames_dir)
reader_thread.start()
for t in processor_threads:
t.start()
writer_thread.start()
reader_thread.join()
for t in processor_threads:
t.join()
writer_thread.join()
if errors:
raise errors[0]
# Temporal mask smoothing
if args.smooth > 0:
files = sorted(os.listdir(processed_dir))
total = len(files)
window = args.smooth
half = window // 2
print(
f"Applying temporal mask smoothing (window={window}, "
f"workers={args.smooth_workers})...",
flush=True,
)
smoothing_errors = []
progress_lock = threading.Lock()
progress_count = [0]
n_workers = max(1, args.smooth_workers)
# Write smoothed frames to a separate directory rather than
# overwriting processed_dir in place. Overlapping windows mean a
# frame can be a *read* dependency for several write_idx tasks;
# writing in place risked one thread reading a file while another
# was mid-save on it (truncated/corrupt img -> shape errors).
if not os.path.isdir(smoothed_dir):
os.mkdir(smoothed_dir)
def get_alpha(idx):
file = files[idx]
img = Image.open(os.path.join(processed_dir, file)).convert("RGBA")
return np.array(img)[:, :, 3].astype(np.float32)
def smooth_frame(write_idx):
try:
start = max(0, write_idx - half)
end = min(total - 1, write_idx + half)
alphas = np.stack([get_alpha(j) for j in range(start, end + 1)])
smoothed_alpha = np.mean(alphas, axis=0).astype(np.uint8)
filename = files[write_idx]
out_img = Image.open(os.path.join(processed_dir, filename)).convert(
"RGBA"
)
out_arr = np.array(out_img)
out_arr[:, :, 3] = smoothed_alpha
Image.fromarray(out_arr).save(os.path.join(smoothed_dir, filename))
with progress_lock:
progress_count[0] += 1
print(
f"Smoothed frame {progress_count[0]}/{total}: {filename}",
flush=True,
)
except Exception as e:
smoothing_errors.append(e)
smooth_queue = Queue()
for write_idx in range(total):
smooth_queue.put(write_idx)
def smoothing_worker():
while True:
try:
write_idx = smooth_queue.get_nowait()
except Exception:
return
if smoothing_errors:
return
smooth_frame(write_idx)
smoothing_threads = [
threading.Thread(target=smoothing_worker, daemon=True)
for _ in range(n_workers)
]
for t in smoothing_threads:
t.start()
for t in smoothing_threads:
t.join()
if smoothing_errors:
rmtree(smoothed_dir, ignore_errors=True)
raise smoothing_errors[0]
# Swap the smoothed frames in as the new processed_dir contents.
rmtree(processed_dir)
os.rename(smoothed_dir, processed_dir)
if args.output_type != "mask_seq":
# Output video
stream = ffmpeg.input(
os.path.join(processed_dir, "%04d.tiff"),
r=framerate,
f="image2",
s=whstr,
)
if args.output_type == "mask":
output_file = pathlib.Path(args.o) / ("output.mp4")
output_file.parent.mkdir(exist_ok=True, parents=True)
stream = ffmpeg.output(
stream,
str(output_file),
vcodec="libx264",
pix_fmt="gray",
crf=0,
preset="veryslow",
tune="animation",
)
else:
output_file = pathlib.Path(args.o) / ("output.mov")
output_file.parent.mkdir(exist_ok=True, parents=True)
stream = ffmpeg.output(
stream, str(output_file), vcodec="prores_ks", **{"profile:v": "4"}
)
ffmpeg.run(stream, overwrite_output=True)
else:
img_seq_out_folder = os.path.join(args.o, "output_img_seq")
rmtree(img_seq_out_folder, ignore_errors=True)
move(processed_dir, img_seq_out_folder)
except KeyboardInterrupt:
print("\nInterrupted by user")
finally:
print("Removing temporary files...")
rmtree(processed_dir, ignore_errors=True)
rmtree(frames_dir, ignore_errors=True)
rmtree(smoothed_dir, ignore_errors=True)
+42
View File
@@ -0,0 +1,42 @@
attrs==26.1.0
certifi==2026.6.17
charset-normalizer==3.4.7
coloredlogs==15.0.1
ffmpeg-python==0.2.0
flatbuffers==25.12.19
future==1.0.0
humanfriendly==10.0
idna==3.18
ImageIO==2.37.3
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
lazy-loader==0.5
llvmlite==0.47.0
mpmath==1.3.0
networkx==3.6.1
numba==0.65.1
numpy==2.4.6
nvidia-cublas-cu12==12.9.2.10
nvidia-cuda-nvrtc-cu12==12.9.86
nvidia-cuda-runtime-cu12==12.9.79
nvidia-cudnn-cu12==9.10.2.21
nvidia-cufft-cu12==11.4.1.4
nvidia-nvjitlink-cu12==12.9.86
onnxruntime-gpu==1.19.2
packaging==26.2
pillow==12.2.0
platformdirs==4.10.0
pooch==1.9.0
protobuf==7.35.1
PyMatting==1.1.15
referencing==0.37.0
rembg==2.0.76
requests==2.34.2
rpds-py==2026.5.1
scikit-image==0.26.0
scipy==1.18.0
sympy==1.14.0
tifffile==2026.6.1
tqdm==4.68.3
typing_extensions==4.15.0
urllib3==2.7.0