Jak automatycznie wycinać tło ze zdjęć za pomocą AI i Pythona

Jak automatycznie wycinać tło ze zdjęć za pomocą AI i Pythona [Poradnik]

Tworzenie spójnej i profesjonalnej galerii zdjęć na stronę internetową — na przykład dla salonu fryzjerskiego czy sklepu e-commerce — bywa czasochłonne. Ręczne wycinanie krawędzi, nakładanie nowego tła i dopasowywanie rozmiarów w programach graficznych potrafi zająć wiele godzin.

Na szczęście rozwój sztucznej inteligencji pozwala całkowicie zautomatyzować ten proces. W tym poradniku pokazujemy, jak za pomocą darmowego modelu AI RMBG 2.0 oraz prostego skryptu w Pythonie stworzyć automatyczny system, który:

  • Precyzyjnie wytnie tło (nawet ze skomplikowanych krawędzi, takich jak włosy).
  • Wycentruje obiekt i ustali spójną skalę dla całej galerii.
  • Podmieni tło na estetyczny wzór z miękkim cieniem.
  • Nałoży logo / znak wodny.
  • Zmieni nazwy plików na uporządkowany ciąg i skompresuje je do lekkiego formatu WebP.

1. Wymagania wstępne i instalacja komponentów

Wszystkie potrzebne narzędzia są całkowicie darmowe i dają pełną kontrolę nad przetwarzaniem plików bezpośrednio na Twoim komputerze.

Krok 1: Instalacja potrzebnych bibliotek

Otwórz terminal (lub wiersz poleceń) i zainstaluj niezbędne pakiety Pythona:

pip install torch torchvision transformers pillow kornia timm huggingface_hub

Za co odpowiadają poszczególne pakiety?

  • torch i torchvision — silnik do obsługi modeli uczenia maszynowego.
  • transformers — biblioteka do łatwego ładowania modeli AI z platformy HuggingFace.
  • pillow — biblioteka do obróbki obrazów (przycinanie, skalowanie, nakładanie logo).
  • timm oraz kornia — pomocnicze biblioteki wymagane przez architekturę modelu.
  • huggingface_hub — narzędzie do autoryzacji i pobierania wag modeli.

2. Jednorazowa autoryzacja na HuggingFace (Model RMBG 2.0)

Model RMBG 2.0 stworzony przez BRIA AI to obecnie jedna z najlepszych open-source’owych sieci neuronowych do detekcji pierwszego planu. Dostęp do wag modelu wymaga jednorazowego zatwierdzenia darmowej licencji.

  1. Wejdź na stronę huggingface.co/briaai/RMBG-2.0 i zaakceptuj warunki (przycisk Agree and access repository).
  2. W ustawieniach swojego profilu (Settings -> Access Tokens) wygeneruj token typu Read.
  3. Zaloguj się w terminalu komendą:
hf auth login

Wklej wygenerowany token i zatwierdź Enterem. Zostanie on zapisany w systemie.


3. Kompletny skrypt w Pythonie

Utwórz plik obrobka_galerii.py w folderze ze swoimi zdjęciami i wklej do niego poniższy kod:

import os
import io
import urllib.request
import torch
import numpy as np
from PIL import Image, ImageFilter, ImageEnhance
from torchvision import transforms
from transformers import AutoModelForImageSegmentation

# --- USTAWIENIA KONFIGURACYJNE ---
TARGET_SIZE = (1080, 1080)              # Rozdzielczość wyjściowa (kwadrat)
MAX_FILE_SIZE_KB = 250                  # Maksymalna waga pliku w KB
OUTPUT_FOLDER = "gotowe_do_galerii"     # Podfolder na przetworzone zdjęcia
BG_IMAGE_PATH = "salon_bg.jpg"          # Plik tła (np. beton, tekstura)
LOGO_URL = "https://questfryzjer.pl/wp-content/uploads/2025/12/quest-lt.png"
LOGO_LOCAL_PATH = "quest-logo.png"

# 1. Inicjalizacja modelu AI
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"--> Używam urządzenia: {device}")
print("--> Ładowanie modelu RMBG 2.0...")

model = AutoModelForImageSegmentation.from_pretrained(
    'briaai/RMBG-2.0', 
    trust_remote_code=True
)
model.to(device)
model.eval()

def get_or_download_logo():
    """Pobiera logo z podanego adresu URL, jeśli plik nie istnieje lokalnie."""
    if not os.path.exists(LOGO_LOCAL_PATH):
        print("--> Pobieranie logo ze strony...")
        try:
            req = urllib.request.Request(LOGO_URL, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req) as response, open(LOGO_LOCAL_PATH, 'wb') as out_file:
                out_file.write(response.read())
        except Exception as e:
            print(f"[OSTRZEŻENIE] Nie udało się pobrać logo: {e}")
            return None
    return Image.open(LOGO_LOCAL_PATH).convert("RGBA")

def prepare_background(size):
    """Przygotowuje tło – wczytuje i rozmywa obrazek lokalny."""
    if not os.path.exists(BG_IMAGE_PATH):
        raise FileNotFoundError(f"Brak pliku '{BG_IMAGE_PATH}' w folderze ze skryptem!")

    bg = Image.open(BG_IMAGE_PATH).convert("RGB")
    
    # Kadrowanie centralne do wymaganego formatu
    bg_ratio = bg.width / bg.height
    target_ratio = size[0] / size[1]
    
    if bg_ratio > target_ratio:
        new_width = int(bg.height * target_ratio)
        left = (bg.width - new_width) // 2
        bg = bg.crop((left, 0, left + new_width, bg.height))
    else:
        new_height = int(bg.width / target_ratio)
        top = (bg.height - new_height) // 2
        bg = bg.crop((0, top, bg.width, top + new_height))
        
    bg = bg.resize(size, Image.Resampling.LANCZOS)
    return bg.filter(ImageFilter.GaussianBlur(radius=8))

def add_logo_watermark(base_image, logo_img, target_width_ratio=0.22, opacity=0.85, margin=40):
    """Nakłada logo w prawym dolnym rogu."""
    if logo_img is None:
        return base_image

    img_w, img_h = base_image.size
    logo_w = int(img_w * target_width_ratio)
    aspect_ratio = logo_img.height / logo_img.width
    logo_h = int(logo_w * aspect_ratio)
    
    resized_logo = logo_img.resize((logo_w, logo_h), Image.Resampling.LANCZOS)
    
    r, g, b, alpha = resized_logo.split()
    alpha = alpha.point(lambda p: int(p * opacity))
    resized_logo.putalpha(alpha)

    pos_x = img_w - logo_w - margin
    pos_y = img_h - logo_h - margin

    combined = base_image.convert("RGBA")
    combined.paste(resized_logo, (pos_x, pos_y), mask=resized_logo)
    return combined.convert("RGB")

def process_photo(input_path, output_path, bg_template, logo_img):
    orig_image = Image.open(input_path).convert("RGB")
    
    transform_image = transforms.Compose([
        transforms.Resize((1024, 1024)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    
    input_tensor = transform_image(orig_image).unsqueeze(0).to(device)

    # Wycięcie tła przez model AI
    with torch.no_grad():
        preds = model(input_tensor)[-1].sigmoid().cpu()
    
    mask = transforms.ToPILImage()(preds[0].squeeze()).resize(orig_image.size)
    no_bg_img = orig_image.copy()
    no_bg_img.putalpha(mask)

    # Korekta ostrości i kontrastu
    no_bg_img = no_bg_img.filter(ImageFilter.UnsharpMask(radius=2, percent=130, threshold=3))
    enhancer = ImageEnhance.Contrast(no_bg_img)
    no_bg_img = enhancer.enhance(1.08)

    # Centrowanie pierwszego planu
    bbox = no_bg_img.getbbox()
    if bbox:
        no_bg_img = no_bg_img.crop(bbox)

    target_h = int(TARGET_SIZE[1] * 0.85)
    aspect_ratio = no_bg_img.width / no_bg_img.height
    target_w = int(target_h * aspect_ratio)

    if target_w > int(TARGET_SIZE[0] * 0.85):
        target_w = int(TARGET_SIZE[0] * 0.85)
        target_h = int(target_w / aspect_ratio)

    no_bg_img = no_bg_img.resize((target_w, target_h), Image.Resampling.LANCZOS)
    mask_resized = no_bg_img.split()[3]

    final_img = bg_template.copy()
    paste_x = (TARGET_SIZE[0] - no_bg_img.width) // 2
    paste_y = (TARGET_SIZE[1] - no_bg_img.height) // 2

    # Generowanie miękkiego cienia
    shadow_mask = mask_resized.filter(ImageFilter.GaussianBlur(radius=20))
    shadow = Image.new("RGBA", TARGET_SIZE, (0, 0, 0, 0))
    shadow_layer = Image.new("RGBA", no_bg_img.size, (0, 0, 0, 80))
    
    shadow.paste(shadow_layer, (paste_x + 5, paste_y + 10), mask=shadow_mask)
    final_img.paste(shadow, (0, 0), mask=shadow)

    # Połączenie warstw
    final_img.paste(no_bg_img, (paste_x, paste_y), mask=mask_resized)
    final_img = add_logo_watermark(final_img, logo_img)

    # Optymalizacja rozmiaru i zapis w formacie WebP
    quality = 92
    while quality > 10:
        buffer = io.BytesIO()
        final_img.save(buffer, format="WEBP", quality=quality, optimize=True)
        size_kb = buffer.tell() / 1024
        
        if size_kb <= MAX_FILE_SIZE_KB:
            break
        quality -= 4

    with open(output_path, "wb") as f:
        f.write(buffer.getvalue())
        
    print(f"[OK] {os.path.basename(input_path)} -> {os.path.basename(output_path)} ({size_kb:.1f} KB)")

# --- PĘTLA GŁÓWNA ---
if __name__ == "__main__":
    os.makedirs(OUTPUT_FOLDER, exist_ok=True)
    
    try:
        bg_template = prepare_background(TARGET_SIZE)
    except FileNotFoundError as e:
        print(f"\n[BŁĄD] {e}")
        exit(1)
        
    logo_img = get_or_download_logo()
    ignored_files = {BG_IMAGE_PATH, LOGO_LOCAL_PATH}
    valid_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
    
    files = sorted([f for f in os.listdir('.') if f.lower().endswith(valid_extensions) and f not in ignored_files])
    
    if not files:
        print("Nie znaleziono zdjęć do przetworzenia!")
    else:
        print(f"Rozpoczynam przetwarzanie {len(files)} plików...\n")
        for idx, file_name in enumerate(files, start=1):
            input_path = file_name
            output_filename = f"questfryzjer_{idx:02d}.webp"
            output_path = os.path.join(OUTPUT_FOLDER, output_filename)
            
            try:
                process_photo(input_path, output_path, bg_template, logo_img)
            except Exception as e:
                print(f"[BŁĄD] Nie udało się przetworzyć {file_name}: {e}")
                
        print(f"\nPrzetwarzanie zakończone! Pliki zapisano w: /{OUTPUT_FOLDER}")

4. Podstawowe komendy do uruchamiania

Gdy pliki graficzne oraz skrypt znajdują się w jednym folderze, wystarczy jedno polecenie w terminalu:

python obrobka_galerii.py

Dlaczego warto wdrożyć to rozwiązanie?

  • Format WebP: Zapewnia znacznie mniejszą wagę plików przy wysokiej jakości, przyspieszając ładowanie witryny.
  • Spójność wizualna: Wszystkie kadry są idealnie wycentrowane i zachowują stałą skalę (85% wysokości kadru).
  • Porządek w plikach: Automatyczna zmiana nazw (np. questfryzjer_01.webp) ułatwia zarządzanie zasobami w bibliotece mediów WordPressa.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *