uv add nobgFrom source (with uv)
git clone https://github.com/feyninc/nobg.git
cd nobg
uv syncRequires Python ≥ 3.10, torch ≥ 2.0 and torchvision ≥ 0.15. Those two are deliberately
not installed for you — pick the build that matches your hardware (a CPU wheel, a CUDA
one, ROCm) and install it yourself:
uv add torch torchvisionSee pyproject.toml for the full dependency set.
Remove a background in three lines:
from nobg import AutoModel
model = AutoModel.from_pretrained("feyninc/FeyNobg")
model.process("input.jpg").save("output.png")process handles the whole pipeline — load, preprocess, forward under no_grad in eval
mode, post-process, composite — and builds the processor the model's own config implies, so
there is nothing else to load. image takes anything
loadimg accepts: a path, URL, base64 string, numpy
array or PIL image. Pass a list to get a list back, each matte returned at its own original
resolution:
for cut, path in zip(model.process(["a.jpg", "b.jpg"]), ("a.png", "b.png")):
cut.save(path)Useful keywords: batch_size (images per forward pass; defaults to 1 to keep peak memory
flat), return_type="alpha" for the raw (H, W) matte tensors instead of RGBA cutouts, and
any remaining kwargs go to the processor.
predict is the same call with the processor passed in — reach for it when you already have
one, or when a checkpoint's preprocessor_config.json differs from its config.image_size
(process trusts the model config):
from nobg import AutoProcessor
processor = AutoProcessor.from_pretrained("feyninc/FeyNobg")
model.predict(processor, "input.jpg").save("output.png")The processor comes first, then the inputs that vary: predict(processor, image, prompt, boxes), each one optional after image (BiRefNet takes neither prompt nor boxes; SAM3 takes
both). model.default_processor() returns the one process would build, if you want it
without the Hub round-trip.
Or drive the steps yourself when you need the intermediates:
import torch
from loadimg import load_img
image = load_img("input.jpg").convert("RGB")
inputs = processor(image, return_tensors="pt")
with torch.no_grad():
outputs = model(pixel_values=inputs["pixel_values"])
alpha = processor.post_process_alpha_matting(
outputs, target_sizes=[(image.height, image.width)]
)[0]
processor.cutout(image, alpha).save("output.png")Or try it in the browser first: 🤗 FeyNobg Space.
| Model | Repo | Params | Resolution | Task | Notes |
|---|---|---|---|---|---|
| FeyNobg | feyninc/FeyNobg |
0.3 B | 1024 × 1024 | Background removal / matting | Strongest published model, start here |
| SAM3 | facebook/sam3 |
0.84 B | 1008 × 1008 | Background removal + text-promptable segmentation | Load with Sam3.from_origin. Picks the subject well; edges stay softer than FeyNobg. Gated; weights are under Meta's SAM License |
AutoModel reads the repo tags and returns the concrete class. AutoProcessor
reads preprocessor_config.json (or falls back to the model config) and returns
the matching image processor.
from nobg import AutoModel, AutoProcessor
model = AutoModel.from_pretrained("feyninc/FeyNobg")
processor = AutoProcessor.from_pretrained("feyninc/FeyNobg")Concrete classes work too, if you'd rather be explicit:
from nobg import BiRefNet, BiRefNetImageProcessor
model = BiRefNet.from_pretrained("feyninc/FeyNobg")
processor = BiRefNetImageProcessor.from_pretrained("feyninc/FeyNobg")Constructing from scratch (random init) uses the config dataclass:
from nobg import BiRefNet
from nobg.birefnet.modeling_birefnet import BiRefNetConfig
model = BiRefNet(BiRefNetConfig(image_size=512, embed_dim=128))Sam3 wraps transformers' SAM3
and exposes its prompt-conditioned segmentation as a single alpha matte, so it drops into the
same flow as BiRefNet:
from nobg import Sam3
model = Sam3.from_origin("facebook/sam3")
model.process("input.jpg").save("output.png")Because SAM3 is open-vocabulary, you can cut out specific things by passing a prompt as the second argument — this is the capability BiRefNet doesn't have:
model.process("input.jpg", "the dog").save("dog.png")With no prompt, the processor supplies default_prompt ("the main foreground subject"),
which is what makes prompt-free background removal work.
The third argument is boxes — a visual prompt, in the original image's pixel
coordinates. Use it when the thing you want is easier to point at than to name:
model.process("input.jpg", None, [[120, 80, 460, 720]]).save("cutout.png")Unlike prompt, boxes is per-image: pass [[x1, y1, x2, y2], ...] for one image, or
one such list per image for a batch. Boxes and a prompt can be combined; with boxes and no
prompt, SAM3 segments what the boxes point at.
images = ["a.jpg", "b.jpg"]
boxes = [[[10, 10, 200, 300]], [[40, 60, 380, 500], [400, 20, 620, 260]]]
cuts = model.process(images, "the dog", boxes)process builds its processor from the model config: image size and default_prompt come
straight from it, and the CLIP tokenizer is loaded from whatever repo from_origin read the
weights from, falling back to the ungated openai/clip-vit-large-patch14 (SAM3's text tower
is CLIP's). Pass tokenizer= a repo id, a directory or an instance to override that. When
you'd rather hold the processor yourself, predict is the same call with it passed in first:
from nobg import Sam3Processor
processor = Sam3Processor.from_pretrained("facebook/sam3")
model.predict(processor, "input.jpg", "the dog").save("dog.png")By default the matte comes from SAM3's own prompt-conditioned semantic head
(config.aggregate="semantic"). Set aggregate to "max" or "mean" to build it from the
union of per-object instance masks instead — that path respects score_threshold (how many
detected objects land in the matte, falling back to the best-scoring one so the matte is
never empty), but produces a noticeably softer alpha:
model = Sam3.from_origin("facebook/sam3", aggregate="max")
model.predict(processor, "input.jpg", score_threshold=0.5)Measured against FeyNobg on two photos, the semantic head is the clear default: MAE
0.035/0.039 with 19/29 % of pixels at intermediate alpha, versus 0.144/0.150 and 71/79 % for
the instance union. Reach for "max"/"mean" when you specifically want the matte to track
the detected instance set.
The step-by-step form, when you want the instance-level outputs:
import torch
from loadimg import load_img
image = load_img("input.jpg").convert("RGB")
inputs = processor(images=image, text="the dog", return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
alpha = processor.post_process_alpha_matting(
outputs, target_sizes=[(image.height, image.width)]
)[0]
processor.cutout(image, alpha).save("output.png")Because the matte is never empty, a prompt for something that isn't in the image still
returns one. Read presence_logits to tell the difference — SAM3's presence head is a
reliable confidence signal (on a cosplay photo: "the person" → 0.97, "the hat" → 0.85,
"the dog" → 0.001):
confidence = outputs["presence_logits"].sigmoid().item()The per-object outputs come through untouched (pred_masks, pred_boxes, pred_logits,
presence_logits, semantic_seg), so processor.image_processor.post_process_instance_segmentation
still works for instance-level use.
Which model to reach for. SAM3 finds the right subject — on a test photo its matte agrees with FeyNobg at IoU 0.98 — but it's a detector, not a matting model: masks are predicted at a fraction of the input resolution and upsampled, so edges stay softer (19 – 29 % of pixels land at intermediate alpha, versus 3 % for FeyNobg). Use SAM3 when you need to choose what to cut out, and FeyNobg when you need hair-level edges.
Note
nobg ships no SAM weights — from_origin("facebook/sam3") downloads them from Meta's
gated repo, under Meta's SAM License
rather than nobg's Apache-2.0. Accept it on the Hub first. Only the Apache-2.0
transformers implementation is used in code.
Note that pixel_values must be exactly config.image_size square — the vision tower's
rotary embeddings are fixed-size — so always preprocess through Sam3Processor.
predict takes a list and returns one result per input, each at its original resolution.
batch_size sets how many go through each forward pass:
paths = ("a.jpg", "b.jpg", "c.jpg")
for cut, path in zip(model.predict(processor, list(paths), batch_size=4), paths):
cut.save(path.replace(".jpg", ".png"))Or drive it manually — post_process_alpha_matting takes one target size per image, so
mattes come back at each original resolution:
images = [load_img(p).convert("RGB") for p in paths]
inputs = processor(images, return_tensors="pt")
with torch.no_grad():
outputs = model(pixel_values=inputs["pixel_values"])
mattes = processor.post_process_alpha_matting(
outputs, target_sizes=[(im.height, im.width) for im in images]
)
for im, alpha, path in zip(images, mattes, ("a.png", "b.png", "c.png")):
processor.cutout(im, alpha).save(path)The same pattern handles video: decode to frames, batch them, composite back.
A soft matte leaves the old background mixed into every semi-transparent pixel, so
compositing the original pixels onto a new background shows a halo of the old one —
most visible on hair, fur and motion blur. refine_foreground estimates the unmixed
foreground color for those pixels, and cutout(refine=True) applies it in place:
processor.cutout(image, alpha, refine=True).save("output.png")It is pure torch, so it runs wherever its inputs live — keep the tensors on the GPU and the refinement stays there too:
alpha = processor.post_process_alpha_matting(
outputs, target_sizes=[(image.height, image.width)]
)[0]
foreground = processor.refine_foreground(pixel_tensor.cuda(), alpha.cuda())r (default 90) sets how far the estimator reaches for a color to borrow; the cost
grows about linearly with it.
model = AutoModel.from_pretrained("feyninc/FeyNobg").eval().to("cuda")
inputs = processor(image, return_tensors="pt").to("cuda")
with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
outputs = model(pixel_values=inputs["pixel_values"])NoBg provides the model, processor, and loss needed to train BiRefNet on your own image and mask pairs. Because the model plugs into the Hugging Face Trainer, you get its full training loop, checkpointing, and evaluation for free.
from transformers import Trainer, TrainingArguments
from nobg import AutoProcessor, AutoModel
model = AutoModel.from_pretrained("nobg/FeyNobg")
processor = AutoProcessor.from_pretrained("nobg/FeyNobg")
def collate(examples):
batch = processor(
images=[ex["image"] for ex in examples],
segmentation_maps=[ex["mask"].convert("L") for ex in examples],
return_tensors="pt",
)
return {"pixel_values": batch["pixel_values"], "labels": batch["labels"]}
trainer = Trainer(
model=model,
args=TrainingArguments(output_dir="outputs", learning_rate=2e-5),
train_dataset=dataset,
data_collator=collate,
)
trainer.train()Swapping the loss
model.criterion is a plain function attribute, not a submodule, so it never
enters the state dict and you can replace it outright:
from nobg.loss import birefnet_loss, iou_loss, ssim_loss
def my_loss(scaled_preds, gt):
return birefnet_loss(scaled_preds, gt) + 5 * iou_loss(
scaled_preds[-1].sigmoid(), gt
)
model.criterion = my_lossBiRefNet.from_origin builds a new model from an existing one, injecting every
weight whose key and shape still match and freshly initializing the rest. Handy
for changing resolution, growing the decoder, or migrating pre-0.2.0 checkpoints.
from nobg import BiRefNet
model = BiRefNet.from_origin("feyninc/FeyNobg", image_size=2048)origin may be a Hub repo id, a local directory with config.json +
model.safetensors, or a live nn.Module.
model.push_to_hub("your-username/model-name")
processor.push_to_hub("your-username/model-name")A bare name is auto-prefixed with your Hub username, and a model card is generated from the shared template.
Every model has an ONNX counterpart of each of those three calls. They need the extra:
uv add "nobg[onnx]"model.onnx_save_pretrained("onnx-out") # -> onnx-out/model.onnx + config.json + README.md
model.onnx_push_to_hub("your-username/model-name-onnx")Loading gives back an OnnxModel — the graph under onnxruntime, with the same process,
predict, default_processor and config as the torch model, so it drops into the code
above unchanged:
from nobg import BiRefNet
model = BiRefNet.onnx_from_pretrained("your-username/model-name-onnx")
model.process("input.jpg").save("output.png")providers= picks the execution provider (defaults to everything installed, so an
onnxruntime-gpu build uses the GPU); session_options= takes an
onnxruntime.SessionOptions.
A dedicated -onnx repo is one option; subfolder= is the other, and it is the convention
optimum and transformers.js already look in. All three calls take it:
model.onnx_save_pretrained("out", subfolder="onnx") # -> out/onnx/model.onnx + config.json
model.onnx_push_to_hub("your-username/model-name", subfolder="onnx")
model = BiRefNet.onnx_from_pretrained("your-username/model-name", subfolder="onnx")The push is scoped with path_in_repo, so it writes onnx/ and nothing else — the
model.safetensors beside it, the processor config and any eval assets are not part of the
commit. The repo's model card is patched in a second, separate commit rather than
regenerated: it gains the onnx tag and a "how to load the ONNX export" section if they
aren't already there, so re-pushing is idempotent and a hand-written card survives. Pass
update_model_card=False to leave the card alone entirely; onnx_save_pretrained takes
model_card= for the same decision locally, its default being "no README.md inside a
subfolder", since the directory it joins already has one.
Loading with subfolder= narrows the download to that folder plus the root metadata, so
pulling a graph out of a torch repo doesn't drag the safetensors along. config.json is read
from the subfolder if it's there and from the repo root otherwise, since that's where the Hub
convention keeps it.
Two things differ from the torch model. Shapes are fixed at export time, batch size
included — transformers' Swin windowing reshapes with Python ints, which pins the batch no
matter what dynamic_axes claims, so export at the batch size you'll run at and read it back
off model.batch_size:
model.onnx_save_pretrained("onnx-out", batch_size=4)And only the matte is exported: the graph returns logits alone, without BiRefNet's
intermediate_logits or SAM3's instance heads (pred_masks, presence_logits, …). Keep the
torch model for those.
The graph's inputs come from onnx_dummy_inputs() — pixel_values for BiRefNet, plus
input_ids/attention_mask for SAM3, whose text prompt is therefore baked in as shape only:
any prompt Sam3Processor produces (it pads to 32 tokens) runs on the same graph. Pass
dummy_inputs= to trace a variant, e.g. a box-promptable SAM3:
inputs = model.onnx_dummy_inputs()
inputs["input_boxes"] = torch.zeros(1, 1, 4) # one box per image — only the shape is traced
model.onnx_save_pretrained("onnx-out", dummy_inputs=inputs)Anything else goes to torch.onnx.export, opset_version included (the default, 19, is the
floor for BiRefNet's DeformConv).
- BiRefNet by Peng Zheng et al., the architecture and training recipe this library builds on.
- SAM 3
by Nicolas Carion et al. (Meta AI), wrapped here through its Apache-2.0
transformersimplementation — no SAM weights are redistributed. transformersandhuggingface_hubfor the backbone, processor base and Hub integration.
@software{nobg,
title={nobg: Open Source Background Removal Models for Image and Video Matting},
author={Hichri, Hafedh},
year={2026},
url={https://github.com/feyninc/nobg},
license={Apache-2.0},
}

