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 /frames
/export /export
*.mp4 *.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 MIT License
Copyright (c) 2021 Seth Tribbey Copyright (c) 2021 Seth Tribbey
Copyright (c) 2026 Uula Ilola
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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. 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: ## Installation:
[Install rembg by following their instructions](https://github.com/danielgatis/rembg)
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. Currently i've gotten rembg to work only with Python **3.12** because of onnxruntime's shenanigans X/
```bash
Then: python3.12 -m venv .venv
pip install -r requirements.txt
``` ```
pip install ffmpeg-python > **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
### Usage:
```
python .\rembg_video.py [-h] [-a] [-af AF] [-ab AB] [-ae AE] [--skip-extract] [--skip-process] input
positional arguments: &nbsp;
input Input video
optional arguments: ## Usage:
-h, --help show this help message and exit ```
-a Turns on alpha matting during background removal python .\rembg_video.py [-h] [--help] [-o] [--model] [--workers] [--smooth] [--smooth-workers] [--buffer-size] [--output-type] input
-af AF Alpha matting foreground threshold ```
-ab AB Alpha matting background threshold <style>
-ae AE Alpha matting erode size table {
--skip-extract Skips ffmpeg frame extraction border-collapse: separate;
--skip-process Skips rembg frame processing border-spacing: 0;
``` border-radius: 5px;
Tip: [Alpha matting can be used to refine the results](https://github.com/danielgatis/rembg#advance-usage) 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 argparse
import io
import os import os
import ffmpeg import ffmpeg
import pathlib import pathlib
import shutil import threading
from rembg.bg import remove 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 os.environ.setdefault(
parser = argparse.ArgumentParser(description='Applies rembg to the frames of a video') "PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True,managed_memory:True"
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") # Parse args
parser.add_argument('-af', type=int, default=240, help="Alpha matting foreground threshold") parser = argparse.ArgumentParser(
parser.add_argument('-ab', type=int, default=10, help="Alpha matting background threshold") description="Applies rembg background removal to the frames of a video"
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("input", type=str, help="Input video")
parser.add_argument('--skip-process', action="store_true", help='Skips rembg frame processing') 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() 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) probe = ffmpeg.probe(args.input)
video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None) video_stream = next(
width = int(video_stream['width']) (stream for stream in probe["streams"] if stream["codec_type"] == "video"), None
height = int(video_stream['height']) )
whstr = str(width) + 'x' + str(height) if video_stream is None:
framerate = video_stream['avg_frame_rate'] 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 # Setup workspace folders
if not args.skip_extract: frames_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "frames")
frames_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "frames") processed_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "processed")
if not os.path.isdir(frames_dir): smoothed_dir = processed_dir + "_smoothed"
os.mkdir(frames_dir) rmtree(frames_dir, ignore_errors=True)
rmtree(processed_dir, ignore_errors=True)
rmtree(smoothed_dir, ignore_errors=True)
stream = ffmpeg.input(args.input) # Extract input video frames
stream = ffmpeg.output(stream, os.path.join(frames_dir, "%04d.png")) os.mkdir(frames_dir)
ffmpeg.run(stream) stream = ffmpeg.input(args.input)
stream = ffmpeg.output(stream, os.path.join(frames_dir, "%04d.tiff"))
ffmpeg.run(stream)
#Process frames with rembg _SENTINEL = object()
if not args.skip_process:
files_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "frames") try:
processed_dir = os.path.join(str(pathlib.Path(__file__).parent.absolute()), "processed") # Process frames with pipelined reader -> processors -> writer
if not os.path.isdir(processed_dir): if not os.path.isdir(processed_dir):
os.mkdir(processed_dir) os.mkdir(processed_dir)
files = sorted(os.listdir(files_dir)) files = sorted(os.listdir(frames_dir))
total_files = len(files) 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): 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) print(f"Processing frame {idx}/{total_files}: {file}", flush=True)
with open(os.path.join(files_dir, file), "rb") as i: output_data = remove_with_fallback(input_data, session=session)
with open(os.path.join(processed_dir, file), "wb") as o: write_queue.put((idx, file, output_data))
input = i.read() except Exception as e:
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) errors.append(e)
o.write(output) finally:
print(f"Completed frame {idx}/{total_files}", flush=True) # 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 def writer():
output_file = pathlib.Path(args.o) try:
output_file.parent.mkdir(exist_ok=True, parents=True) 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') reader_thread = threading.Thread(target=reader, daemon=True)
stream = ffmpeg.output(stream, args.o, vcodec='libx264', crf=25) processor_threads = [
ffmpeg.run(stream) threading.Thread(target=processor, daemon=True) for _ in range(args.workers)
]
writer_thread = threading.Thread(target=writer, daemon=True)
#Cleanup reader_thread.start()
print("Removing temporary files...") for t in processor_threads:
shutil.rmtree(processed_dir) t.start()
shutil.rmtree(frames_dir) 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