Packshoty na białym tle w ComfyUI, bez Photoshopa

· 10 min czytania

Jak lokalnie, w ComfyUI na karcie graficznej laptopa, robię packshoty produktów na białym tle: kilka sekund na zdjęcie, bez Photoshopa i bez subskrypcji.

Jak lokalnie, w ComfyUI na karcie graficznej laptopa, robię packshoty produktów na białym tle: kilka sekund na zdjęcie, bez Photoshopa i bez subskrypcji. Jak lokalnie, w ComfyUI na karcie graficznej laptopa, robię packshoty produktów na białym tle: kilka sekund na zdjęcie, bez Photoshopa i bez subskrypcji.

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
LaptopLenovo, Intel Core Ultra 9 275HX, 64 GB RAM, Windows 11
Karta graficznaNVIDIA RTX PRO 3000 Blackwell Laptop GPU, 12 GB VRAM
ComfyUI0.13.0 (Python 3.13, PyTorch 2.10 z CUDA)
ModelBiRefNet-General
Własne węzłyComfyUI_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

Graf packshotów w ComfyUI: od zdjęcia źródłowego przez BiRefNet i białe płótno do dwóch wyjść, 4:3 i 1:1

Numery poniżej zgadzają się z etykietami na zrzucie.

  1. Zdjęcie źródłowe. Węzeł LoadImage. To jedyne miejsce, które zmienia się między zdjęciami.
  2. Model BiRefNet. Wczytuje BiRefNet-General.
  3. Wycięcie części. BiRefNetUltraV2 zwraca maskę: biała tam, gdzie jest część, czarna wszędzie indziej. Do krawędzi używam GuidedFilter, 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.
  4. 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.
  5. Kadr do obrysu (6). MaskBoundingBox+ przycina obraz do prostokąta wokół maski. Dlatego margines jest równy, niezależnie od kadru oryginału.
  6. Wpasowanie w ostateczną ramkę (7a–7c i 8a–8c). ResizeAndPadImage skaluje 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 dodanie dtype=torch.float32 do from_pretrained(...) w pliku birefnet_ultra_v2.py tego 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 trybie min_bounding_rect bierze tylko największy kontur i ucinał mi zagłówki foteli połączone z oparciem cienkimi prętami.

Jak to wygląda w trakcie generowania

ComfyUI w trakcie przebiegu: pasek postępu u góry, podświetlony aktywny węzeł i kolejka zadań z łącznym postępem

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.

Filtr paliwa leżący na szarej powierzchni i ten sam filtr na białym tle

Osłona silnika leżąca na folii bąbelkowej na wzorzystej, odbijającej światło metalowej powierzchni, z etykietami przesyłki obok, i ta sama osłona wycięta na białe tło w kwadratowej wersji 1:1

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.

Rura ciśnieniowa z czerwoną opaską na trawie i ta sama część na białym tle

Łoże skrzyni biegów na niebieskiej macie z siatką i ta sama część na białym tle

Dwa przednie fotele na trawie i oba fotele na białym tle

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

Filtr paliwa stojący na swoim kartonie i ten sam filtr na białym tle, z kartonem nadal na zdjęciu

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.

Osłona silnika na folii bąbelkowej na wzorzystej metalowej powierzchni oraz wyniki Adobe Firefly (cała scena nadal na zdjęciu), iloveimg (sama osłona, mała w oryginalnym kadrze) i grafu ComfyUI (sama osłona, gotowy kadr 4:3)

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.

Obudowa filtra powietrza na trawie, oraz wyniki Adobe Firefly, iloveimg i grafu ComfyUI obok siebie

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.

Trzy fragmenty w powiększeniu trzykrotnym: cienka rurka, zacieniony dół obudowy i przewody. W kolumnach oryginał, Adobe Firefly, iloveimg i ComfyUI

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.

Następny krok

Takie same packshoty na Twojej stronie lub w sklepie

Mogę wdrożyć to samo rozwiązanie w każdym typie strony i sklepu: graf dopasowany do Twoich zdjęć, skrypt albo worker, który go karmi, i przegląd przed publikacją. Napisz, co sprzedajesz i gdzie leżą zdjęcia.

Napisz do mnie

Najczęściej zadawane pytania

— 01
Czy potrzebuję karty graficznej NVIDIA?
Graf uruchamiam na karcie NVIDIA (węzeł BiRefNet ma ustawione cuda). Nie sprawdzałem procesora ani karty AMD, więc nie podam dla nich czasów.
— 02
Czy to naprawdę nic nie kosztuje?
ComfyUI jest darmowy i otwarty, a wagi BiRefNet pobierasz z Hugging Face. Zanim użyjesz wyników komercyjnie, sprawdź licencję modelu, którego używasz.
— 03
Czy poradzi sobie z setkami zdjęć?
Tak. Skrypt poniżej wysyła jedno zdjęcie naraz, bo pojedyncza karta nic nie zyskuje na paczce: kazałaby tylko kolejnym zdjęciom czekać. Przy 3 sekundach na zdjęcie 300 zdjęć to 15 minut.
— 04
Czy mogę zmienić margines albo rozmiar?
Tak. Liczby są w węzłach 7a–7c i 8a–8c: płótno (2000×1500 i 2000×2000), rozmiar, do którego wpasowuję część (bok 1800 px), i położenie (margines 100 px).
— 05
Czy wycina każdą część poprawnie?
Nie. Model usuwa wszystko, co wygląda na pierwszy plan, i nie wie, która część jest na sprzedaż, więc każdy wynik sprawdzam, zanim zostanie opublikowany. Sekcja o błędach niżej pokazuje, gdzie się myli.
Powrót do bloga