Do produktu na czystym, białym tle nie potrzebujesz Photoshopa ani subskrypcji. Laptop z kartą graficzną NVIDIA, ComfyUI i model AI do usuwania tła robią to w kilka sekund na zdjęcie, a krótki graf załatwia resztę: kadr, margines i proporcje.
Dlaczego przestałem używać narzędzi online
Packshotów potrzebuję często. Produkty fotografuję tam, gdzie akurat leżą: w kartonie, na trawniku, na posadzce warsztatu. Sklep, katalog czy oferta wyglądają lepiej, gdy każda okładka to sam produkt na bieli.
Najpierw próbowałem narzędzi online do usuwania tła, między innymi iloveimg i usuwania tła w Adobe Firefly. To drugie średnio się u mnie sprawdzało. Najlepszy wynik nadal daje ręczne zaznaczenie części w Photoshopie narzędziem Pióro (Pen Tool). To dobra metoda na profesjonalną sesję zdjęciową, a zła na setki miniatur produktów: trwa za długo.
Zrobiłem więc to lokalnie. ComfyUI okazał się dobrym wyborem, bo cała robota to graf, który buduję raz i puszczam na każde zdjęcie.
Czym jest ComfyUI
ComfyUI to darmowy, otwarty program do uruchamiania modeli AI do obrazu na własnym komputerze. Zamiast jednego przycisku „usuń tło” budujesz graf: łączysz węzły, z których każdy robi jedną rzecz (wczytuje zdjęcie, wycina tło, zmienia rozmiar), a cały graf jest Twoim narzędziem. Działa jako lokalny serwer z interfejsem w przeglądarce i z API przez HTTP, więc ten sam graf uruchomisz ręcznie albo ze skryptu.
Czego potrzeba
| Czego używam | |
|---|---|
| Laptop | Lenovo, Intel Core Ultra 9 275HX, 64 GB RAM, Windows 11 |
| Karta graficzna | NVIDIA RTX PRO 3000 Blackwell Laptop GPU, 12 GB VRAM |
| ComfyUI | 0.13.0 (Python 3.13, PyTorch 2.10 z CUDA) |
| Model | BiRefNet-General |
| Własne węzły | ComfyUI_LayerStyle_Advance (uruchamia BiRefNet), ComfyUI_essentials (MaskBoundingBox+) |
Nie sprawdzałem słabszego sprzętu, więc nie obiecuję dla niego żadnych czasów.
Jak działa graf

Numery poniżej zgadzają się z etykietami na zrzucie.
- Zdjęcie źródłowe. Węzeł
LoadImage. To jedyne miejsce, które zmienia się między zdjęciami. - Model BiRefNet. Wczytuje
BiRefNet-General. - Wycięcie części.
BiRefNetUltraV2zwraca maskę: biała tam, gdzie jest część, czarna wszędzie indziej. Do krawędzi używamGuidedFilter, erode 4, dilate 2, black point 0,01 i white point 0,99. Węzeł liczy wewnętrznie na obrazie co najwyżej 4 megapikseli. - Białe płótno, potem część na bieli (4 i 5). Puste białe płótno wielkości zdjęcia i oryginał wklejony na nie przez maskę, więc wszystko poza maską zostaje białe.
- Kadr do obrysu (6).
MaskBoundingBox+przycina obraz do prostokąta wokół maski. Dlatego margines jest równy, niezależnie od kadru oryginału. - Wpasowanie w ostateczną ramkę (7a–7c i 8a–8c).
ResizeAndPadImageskaluje część do 1800×1300, a węzeł kompozycji wstawia ją w punkt x=100, y=100 na białym płótnie 2000×1500, co daje 100 px marginesu z każdej strony. Druga gałąź robi to samo dla 2000×2000.
Z jednego przebiegu robię dwa pliki: 4:3 i kwadrat. Kafelki i galerie często mają różne proporcje, u mnie 3:2 i 4:3, a obraz 4:3 mieści się w obu bez ucinania części. Kwadrat przydaje się wszędzie tam, gdzie zdjęcie ma trafić do kwadratowego pola, na przykład do miniatury. Dwa węzły PreviewImage mają tytuły packshot 4:3 i packshot 1:1. Zostaw te tytuły, jeśli wywołujesz graf ze skryptu, bo po nich skrypt znajduje wyjścia.
Pobierz graf: packshot-birefnet.workflow.json otwiera się w ComfyUI (przeciągnij plik na płótno), a packshot-birefnet.api.json to ten sam graf w formacie API, dla skryptów.
Przezroczyste tło zamiast białego
Z tej samej maski dostaniesz też przezroczyste tło. Odwróć ją węzłem InvertMask, podaj razem z oryginalnym zdjęciem do JoinImageWithAlpha (ten węzeł sam odwraca maskę, więc podwójne odwrócenie zostawia część nieprzezroczystą) i zapisz wynik jako PNG. Tak powstał cover tego artykułu w trybie ciemnym. Samo przycięcie i wyśrodkowanie zrobiłem poza ComfyUI, w ImageMagicku.
Pułapki, na które wpadłem
Wagi BiRefNet-General trzeba pobrać ręcznie. Węzeł sam pobiera tylko inny model:
python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='ZhengPeng7/BiRefNet', local_dir=r'<ComfyUI>\models\BiRefNet\BiRefNet-General', ignore_patterns=['*.md', '*.txt'])"Przy transformers 5 węzeł BiRefNet u mnie się wywalał z błędem
Input type (float) and bias type (c10::Half) should be the same. Pomogło dodaniedtype=torch.float32dofrom_pretrained(...)w plikubirefnet_ultra_v2.pytego węzła. Nowsza wersja węzła może tego nie wymagać, a aktualizacja może poprawkę cofnąć.Nie kadruj przez
CropByMask V2. W trybiemin_bounding_rectbierze tylko największy kontur i ucinał mi zagłówki foteli połączone z oparciem cienkimi prętami.
Jak to wygląda w trakcie generowania

Naciskasz Run i patrzysz. U góry okna rośnie pasek postępu, liczony węzeł jest obrysowany na niebiesko, a kolejka zadań po prawej wypisuje każde skończone zadanie z jego czasem. Tytuł karty pokazuje procent i nazwę bieżącego węzła, co przydaje się, gdy ComfyUI działa w tle.
Na moim laptopie zdjęcie około 2000 px, taki rozmiar zwykle trzymam, zajmuje 2–4 sekundy. Pełne zdjęcie z telefonu, 3800 px, około 5 sekund. Pierwszy przebieg po starcie ComfyUI jest wolniejszy, około 6 sekund, bo najpierw musi wczytać model.
Przykłady
Wszystko to surowy wynik grafu, bez retuszu. Zdjęcie źródłowe jest po lewej.


Ta wyszła szczególnie dobrze. Osłona silnika Audi leżała na folii bąbelkowej na wzorzystej, odbijającej światło powierzchni, a obok miała etykiety przesyłki. Wycięła się sama osłona, z ostrymi kółkami i napisem TFSI. Widzisz tu kwadratowe wyjście; wersja 4:3 powstaje w tym samym przebiegu.



Modelu nie mylą też szara powierzchnia, siatka maty, cienie na trawie ani czerwona opaska. Ostatnia para pokazuje, gdzie się myli:

Filtr stoi na swoim kartonie i karton został w kadrze, bo model nie wie, który przedmiot jest na sprzedaż.
Od jednego zdjęcia do kilkuset
Graf wywołasz z dowolnego języka, bo ComfyUI udostępnia API przez HTTP. Skrypt musi zrobić trzy rzeczy: wysłać zdjęcie i wskazać je węzłowi LoadImage, zakolejkować graf i poczekać na koniec, a potem pobrać oba wyniki. Całość w Pythonie, z wersją API grafu zapisaną obok:
import json
import sys
import time
from pathlib import Path
import requests
COMFY = "http://127.0.0.1:8188"
WORKFLOW = Path("birefnet-2000-white.api.json")
def packshot(photo: Path, out_dir: Path) -> None:
graph = json.loads(WORKFLOW.read_text())
# 1. Upload the photo and point the LoadImage node at it.
with photo.open("rb") as f:
name = requests.post(f"{COMFY}/upload/image", files={"image": f}, data={"overwrite": "true"}).json()["name"]
load = next(k for k, n in graph.items() if n["class_type"] == "LoadImage")
graph[load]["inputs"]["image"] = name
# 2. Queue the graph and wait for it to finish.
prompt_id = requests.post(f"{COMFY}/prompt", json={"prompt": graph}).json()["prompt_id"]
while not (entry := requests.get(f"{COMFY}/history/{prompt_id}").json().get(prompt_id, {})).get("status", {}).get("completed"):
time.sleep(0.5)
# 3. Save both outputs, found by the titles given to the PreviewImage nodes.
for node_id, node in graph.items():
if node["class_type"] == "PreviewImage":
image = entry["outputs"][node_id]["images"][0]
data = requests.get(f"{COMFY}/view", params=image).content
suffix = node["_meta"]["title"].split()[-1].replace(":", "x") # "4x3" or "1x1"
(out_dir / f"{photo.stem}-{suffix}.png").write_bytes(data)
if __name__ == "__main__":
out = Path("packshots")
out.mkdir(exist_ok=True)
for path in sys.argv[1:]:
started = time.time()
packshot(Path(path), out)
print(f"{path}: {time.time() - started:.1f} s")Pobierz packshot.py.txt, zmień nazwę na packshot.py, połóż obok graf w wersji API jako birefnet-2000-white.api.json i uruchom python packshot.py zdjecia/*.jpg. Na moim laptopie wypisuje 2–3 sekundy na zdjęcie i zapisuje nazwa-4x3.png oraz nazwa-1x1.png w folderze packshots.
Gdy strona stoi na serwerze bez karty graficznej
Jeśli strona działa na serwerze bez karty graficznej, nie przenoś tam pracy. Niech komputer z GPU sam pyta serwer o czekające zlecenia, po jednym, dzięki czemu serwer nigdy nie musi się z nim łączyć. W moim kodzie w Laravelu pętla wygląda tak (w skrócie):
$job = $api->claim(1)[0] ?? null; // ask production for a queued render
$packshot = $renderer->render($api->download($job['source_url'])); // run the graph in ComfyUI
$upload = $api->uploadUrl($productId); // presigned URL on the CDN
$api->upload($upload['upload_url'], $packshot->jpeg, $upload['content_type']);
$api->upload($upload['square_upload_url'], $packshot->squareJpeg, $upload['content_type']);
$api->complete($productId, $upload['key'], $packshot->workflowVersion->value);Gdy laptop jest wyłączony, zlecenia czekają w kolejce i nic nie ginie. Plik grafu jest wersjonowany w repozytorium, a każdy packshot zapisuje, jaka wersja grafu go zrobiła, więc po zmianie wiem, które zdjęcia trzeba zrobić od nowa. System wokół tego workera opisuję w studium przypadku systemu magazynowego.
Gdzie model się myli, więc sprawdzam każdy wynik
Model usuwa wszystko, co wygląda na pierwszy plan. W praktyce kilka rodzajów zdjęć daje zły wynik:
- Część zaznaczona ramką albo strzałką na większym zespole. Intercooler zakreślony na pakiecie chłodnic wychodzi jako cały pakiet.
- Część sfotografowana na aucie. Felgi na samochodzie wychodzą jako całe auto.
- Inne przedmioty albo napisy w kadrze. Trafiają do wyniku razem z częścią, jak karton wyżej.
- Otwory. Model sam ocenia, czy to, co widać przez otwór, jest częścią przedmiotu. Osłona z przykładu ma okrągły otwór: na kopii 1200×1600 graf go wyciął, a na oryginale 3028×4038 zostawił w nim widoczną folię. Wynik zmieniał się też z rozmiarem, do jakiego zmniejszyłem zdjęcie, więc otwory trzeba sprawdzać, a ważny otwór dorysować ręcznie.
- Przezroczyste szkła, cienkie przewody i chrom. Model może je ocenić źle, więc żaden packshot nie jest publikowany bez mojej zgody.
Gotowy packshot czeka obok zdjęcia źródłowego i dopiero po moim zatwierdzeniu staje się okładką. Jeśli jest zły, zlecam go ponownie z innego zdjęcia, na którym część leży sama, albo go odrzucam i strona zostaje przy oryginalnym zdjęciu.
Porównanie z narzędziami online
Żeby sprawdzić to na czymś trudnym, puściłem te same zdjęcia przez dwa narzędzia online, iloveimg (darmowa wersja, bez konta) i usuwanie tła w Adobe Firefly (też bez logowania), oraz przez swój graf. Oba zdjęcia poszły przez wszystkie trzy narzędzia.
Osłona silnika na folii bąbelkowej
Pierwsze zdjęcie to osłona silnika Audi sfotografowana z góry. Leży na folii bąbelkowej na wzorzystej, odbijającej światło metalowej powierzchni, a obok ma etykiety przesyłki.
Adobe Firefly zabrał tylko skrajny brzeg sceny i zostawił 51% kadru. Folia bąbelkowa, wzorzysta powierzchnia i etykiety zostały, więc wynik to nadal zdjęcie całej sceny, w której gdzieś jest osłona. iloveimg i graf w ComfyUI wycięły samą osłonę, zostawiając ok. 15% kadru.

Obudowa filtra powietrza na trawie
Drugie zdjęcie to obudowa filtra powietrza na trawie, w ostrym słońcu, z mocnym cieniem, cienką rurką i wtyczką z wystającymi uciętymi przewodami.

Wszystkie trzy narzędzia poradziły sobie z ogólnym kształtem, łącznie z rurką i przewodami. iloveimg i Adobe oddają wycięcie w oryginalnym kadrze, więc część zostaje w nim i trzeba ją jeszcze przyciąć i wyśrodkować. Graf robi to od razu i daje gotowy plik 4:3 z równym marginesem.

Wycięcia z iloveimg i ComfyUI pokrywają się w 99,5% pikseli, a wycięcie z Adobe z każdym z nich w ok. 98%. W powiększeniu różnice dotyczą ostrości: iloveimg ma brzegi odrobinę bardziej miękkie niż graf, a Adobe najbardziej miękkie, z kępkami trawy zostawionymi między przewodami. Adobe zwrócił też obraz 800×600, czyli o połowę mniejszy od zdjęcia, które dostał. Pod względem samego cięcia iloveimg i graf są na równi. Graf daje tu gotowy kadr i automatyzację, a nie lepsze wycięcie, a od iloveimg odróżniają go limity: serwis sam podaje, że nieograniczone przetwarzanie obrazów daje dopiero Premium. Adobe Firefly zawiódł też na pierwszym zdjęciu, ale dwa zdjęcia i po jednej próbie to obserwacja, nie ranking.
Kiedy lepsze jest coś innego
- Photoshop z narzędziem Pióro nadal daje najlepszą krawędź. To dobry wybór na profesjonalną sesję zdjęciową, gdzie liczy się pojedyncze zdjęcie i jest czas na jego dopracowanie.
- Narzędzie online jest szybsze przy pojedynczym zdjęciu, bo nie trzeba niczego instalować. Przy dużej liczbie zdjęć trzeba za to liczyć się z limitami wersji darmowej albo z płatnym planem.
- Własny graf w ComfyUI opłaca się, gdy robisz to często i masz kartę NVIDIA: konfiguracja zajmuje trochę czasu jeden raz, a każde następne zdjęcie to kilka sekund.
Podsumowanie
- ComfyUI z BiRefNet zamienia zdjęcie części w packshot na bieli w 2–4 sekundy na karcie graficznej laptopa. Jest darmowy, a obliczenia odbywają się na Twoim komputerze.
- Graf jest krótki: wycięcie, wklejenie na biel, kadr do obrysu, wpasowanie w ramkę z marginesem. Jeden przebieg daje plik 4:3 i kwadratowy.
- Krótki skrypt w Pythonie puszcza go na cały folder, a mały worker może go karmić ze sklepu lub strony na serwerze bez karty graficznej.
- Model nie wie, który przedmiot jest na sprzedaż, więc każdy wynik trzeba szybko przejrzeć, zanim zostanie opublikowany.
- Przy pojedynczym zdjęciu albo profesjonalnej sesji lepsze jest narzędzie online lub Photoshop.
Tego samego narzędzia używam też do upscale’owania zdjęć. A jak Ty robisz packshoty: ręcznie, w narzędziu online, a może lokalnie? Ciekaw jestem, co działa u Ciebie i czego brakuje w moim grafie.




