resizer / app.py
dramp77's picture
update revisi
8fc4cc4 verified
import os
import zipfile
import shutil
import time
from PIL import Image, ImageDraw
from io import BytesIO
import io
from rembg import remove
import gradio as gr
from concurrent.futures import ThreadPoolExecutor
from transformers import AutoModelForImageSegmentation, pipeline
import numpy as np
import pandas as pd
import json
import requests
from dotenv import load_dotenv
import torch
from torchvision import transforms
from functools import lru_cache
import cv2
import pillow_avif
import threading
from collections import Counter
from transformers.configuration_utils import PretrainedConfig
if not hasattr(PretrainedConfig, "get_text_config"):
PretrainedConfig.get_text_config = lambda self: None
stop_event = threading.Event()
# Load environment variables
load_dotenv()
PHOTOROOM_API_KEY = os.getenv("PHOTOROOM_API_KEY", "e98517e5e68a1a2eee49b130c2bcef05c1faec42")
_birefnet_model = None
_birefnet_transform = None
_birefnet_hr_model = None
_birefnet_hr_transform = None
@lru_cache(maxsize=1)
def get_birefnet_model():
global _birefnet_model, _birefnet_transform
if _birefnet_model is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
_birefnet_model = AutoModelForImageSegmentation.from_pretrained(
'ZhengPeng7/BiRefNet',
trust_remote_code=True,
torch_dtype=torch.float32
).to(device)
if not hasattr(_birefnet_model.config, "get_text_config"):
_birefnet_model.config.get_text_config = lambda: None
_birefnet_model.eval()
_birefnet_transform = transforms.Compose([
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
return _birefnet_model, _birefnet_transform
def get_birefnet_hr_model():
global _birefnet_hr_model, _birefnet_hr_transform
if _birefnet_hr_model is None:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
_birefnet_hr_model = AutoModelForImageSegmentation.from_pretrained(
'ZhengPeng7/BiRefNet_HR',
trust_remote_code=True,
torch_dtype=torch.float32
).to(device)
if not hasattr(_birefnet_hr_model.config, "get_text_config"):
_birefnet_hr_model.config.get_text_config = lambda: None
_birefnet_hr_model.eval()
_birefnet_hr_transform = transforms.Compose([
transforms.Resize((2048, 2048)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
return _birefnet_hr_model, _birefnet_hr_transform
def remove_background_rembg(input_path):
print(f"Removing background using rembg for image: {input_path}")
with open(input_path, 'rb') as f:
input_image = f.read()
out_data = remove(input_image)
return Image.open(io.BytesIO(out_data)).convert("RGBA")
def remove_background_bria(input_path):
print(f"Removing background using bria for image: {input_path}")
device = 0 if torch.cuda.is_available() else -1
pipe = pipeline("image-segmentation", model="briaai/RMBG-1.4", trust_remote_code=True, device=device)
result = pipe(input_path)
if isinstance(result, list) and len(result) > 0 and "mask" in result[0]:
mask = result[0]["mask"]
else:
mask = result
if mask.mode != "RGBA":
mask = mask.convert("RGBA")
return mask
def remove_background_birefnet(input_path):
try:
model, transform_image = get_birefnet_model()
device = next(model.parameters()).device
image = Image.open(input_path).convert("RGB")
input_tensor = transform_image(image).unsqueeze(0).to(device)
with torch.no_grad():
try:
preds = model(input_tensor)[-1].sigmoid()
pred_mask = preds[0].squeeze().cpu()
except RuntimeError as e:
if 'out of memory' in str(e):
if torch.cuda.is_available():
torch.cuda.empty_cache()
input_tensor = input_tensor.cpu()
model = model.cpu()
preds = model(input_tensor)[-1].sigmoid()
pred_mask = preds[0].squeeze()
model = model.to(device)
else:
raise e
mask_pil = transforms.ToPILImage()(pred_mask)
mask_resized = mask_pil.resize(image.size, Image.LANCZOS)
result = image.copy()
result.putalpha(mask_resized)
result_array = np.array(result)
alpha = result_array[:, :, 3]
_, alpha = cv2.threshold(alpha, 248, 255, cv2.THRESH_BINARY)
kernel_small = np.ones((3, 3), np.uint8)
kernel_medium = np.ones((5, 5), np.uint8)
kernel_large = np.ones((9, 9), np.uint8)
alpha = cv2.GaussianBlur(alpha, (5, 5), 0)
alpha = cv2.morphologyEx(alpha, cv2.MORPH_OPEN, kernel_small, iterations=3)
alpha = cv2.morphologyEx(alpha, cv2.MORPH_CLOSE, kernel_medium, iterations=3)
alpha = cv2.morphologyEx(alpha, cv2.MORPH_CLOSE, kernel_large, iterations=2)
alpha = cv2.bilateralFilter(alpha, 9, 100, 100)
alpha = cv2.medianBlur(alpha, 5)
_, alpha = cv2.threshold(alpha, 248, 255, cv2.THRESH_BINARY)
alpha = cv2.morphologyEx(alpha, cv2.MORPH_OPEN, kernel_small, iterations=2)
alpha = cv2.morphologyEx(alpha, cv2.MORPH_CLOSE, kernel_small, iterations=2)
edges = cv2.Canny(alpha, 100, 200)
alpha = cv2.morphologyEx(alpha, cv2.MORPH_CLOSE, kernel_medium, iterations=1)
alpha = cv2.subtract(alpha, edges)
result_array[:, :, 3] = alpha
result = Image.fromarray(result_array)
if torch.cuda.is_available():
torch.cuda.empty_cache()
return result
except Exception as e:
print(f"Error in remove_background_birefnet: {str(e)}")
import traceback
traceback.print_exc()
raise
def remove_background_birefnet_2(input_path):
model, transform_image = get_birefnet_model()
device = next(model.parameters()).device
image = Image.open(input_path).convert("RGB")
input_tensor = transform_image(image).unsqueeze(0).to(device)
with torch.no_grad():
try:
preds = model(input_tensor)[-1].sigmoid()
pred_mask = preds[0].squeeze().cpu()
except RuntimeError as e:
if 'out of memory' in str(e):
if torch.cuda.is_available():
torch.cuda.empty_cache()
input_tensor = input_tensor.cpu()
model = model.cpu()
preds = model(input_tensor)[-1].sigmoid()
pred_mask = preds[0].squeeze()
model = model.to(device)
else:
raise e
mask_pil = transforms.ToPILImage()(pred_mask)
mask_resized = mask_pil.resize(image.size, Image.LANCZOS)
result = image.copy()
result.putalpha(mask_resized)
if torch.cuda.is_available():
torch.cuda.empty_cache()
return result
def remove_background_birefnet_hr(input_path):
try:
model, transform_img = get_birefnet_hr_model()
device = next(model.parameters()).device
img = Image.open(input_path).convert("RGB")
t_in = transform_img(img).unsqueeze(0).to(device)
with torch.no_grad():
preds = model(t_in)[-1].sigmoid()
mask = preds[0].squeeze().cpu()
mask_pil = transforms.ToPILImage()(mask).resize(img.size, Image.LANCZOS)
out = img.copy()
out.putalpha(mask_pil)
return out.convert("RGBA")
except Exception as e:
print(f"remove_background_birefnet_hr: {e}")
return None
def remove_background_photoroom(input_path):
if input_path.lower().endswith('.avif'):
input_path = convert_avif(input_path, input_path.rsplit('.', 1)[0] + '.png', 'PNG')
if not PHOTOROOM_API_KEY:
raise ValueError("Photoroom API key missing.")
url = "https://sdk.photoroom.com/v1/segment"
headers = {"Accept": "image/png, application/json", "x-api-key": PHOTOROOM_API_KEY}
with open(input_path, "rb") as f:
resp = requests.post(url, headers=headers, files={"image_file": f})
if resp.status_code != 200:
raise Exception(f"PhotoRoom API error: {resp.status_code} - {resp.text}")
return Image.open(BytesIO(resp.content)).convert("RGBA")
def remove_background_none(input_path):
print(f"Removing background using none for image: {input_path}")
return Image.open(input_path).convert("RGBA")
def get_dominant_color(image):
tmp = image.convert("RGBA")
tmp.thumbnail((100, 100))
ccount = Counter(tmp.getdata())
return ccount.most_common(1)[0][0]
def convert_avif(input_path, output_path, output_format='PNG'):
with Image.open(input_path) as img:
if output_format == 'JPG':
img.convert("RGB").save(output_path, "JPEG") # Convert to JPG (RGB mode)
else:
img.save(output_path, "PNG") # Convert to PNG
return output_path
def rotate_image(image, rotation, direction):
if not image or rotation == "None":
return image
if rotation == "90 Degrees":
angle = 90 if direction == "Clockwise" else -90
elif rotation == "180 Degrees":
angle = 180
else:
angle = 0
return image.rotate(angle, expand=True)
def flip_image(image):
return image.transpose(Image.FLIP_LEFT_RIGHT)
def get_bounding_box_with_threshold(image, threshold=10):
arr = np.array(image)
alpha = arr[:, :, 3]
rows = np.any(alpha > threshold, axis=1)
cols = np.any(alpha > threshold, axis=0)
r_idx = np.where(rows)[0]
c_idx = np.where(cols)[0]
if r_idx.size == 0 or c_idx.size == 0:
return None
top, bottom = r_idx[0], r_idx[-1]
left, right = c_idx[0], c_idx[-1]
if left < right and top < bottom:
return (left, top, right, bottom)
else:
return None
## === NEW ==
def position_logic_old(image_path, canvas_size, padding_top, padding_right, padding_bottom, padding_left,
use_threshold=True, bg_method=None, is_person=False,
snap_to_top=False, snap_to_bottom=False, snap_to_left=False, snap_to_right=False):
"""
Position and resize an image on a canvas based on snapping, cropped sides, and birefnet logic.
Args:
image_path (str): Path to the input image.
canvas_size (tuple): Target canvas size (width, height).
padding_top, padding_right, padding_bottom, padding_left (int): Padding on each side.
use_threshold (bool): Use threshold-based bounding box detection.
bg_method (str): Background removal method ('birefnet', 'birefnet_2', etc.).
is_person (bool): Treat as a person image (snaps to bottom by default).
snap_to_top, snap_to_bottom, snap_to_left, snap_to_right (bool): Snap to respective sides.
Returns:
tuple: (log, resized_image, x_position, y_position)
"""
# Load and prepare the image
image = Image.open(image_path).convert("RGBA")
log = []
x, y = 0, 0
# Get bounding box and crop
if use_threshold:
bbox = get_bounding_box_with_threshold(image, threshold=10) # Assume this function exists
else:
bbox = image.getbbox()
if bbox:
# Detect cropped sides
width, height = image.size
cropped_sides = []
tolerance = 30
if any(image.getpixel((x, 0))[3] > tolerance for x in range(width)):
cropped_sides.append("top")
if any(image.getpixel((x, height-1))[3] > tolerance for x in range(width)):
cropped_sides.append("bottom")
if any(image.getpixel((0, y))[3] > tolerance for y in range(height)):
cropped_sides.append("left")
if any(image.getpixel((width-1, y))[3] > tolerance for y in range(height)):
cropped_sides.append("right")
if cropped_sides:
log.append({"info": f"The following sides may contain cropped objects: {', '.join(cropped_sides)}"})
else:
log.append({"info": "The image is not cropped."})
image = image.crop(bbox)
log.append({"action": "crop", "bbox": [str(bbox[0]), str(bbox[1]), str(bbox[2]), str(bbox[3])]})
# Setup variables
target_width, target_height = canvas_size
aspect_ratio = image.width / image.height
# Determine active snaps
snaps_active = []
if padding_top == 0 or snap_to_top:
snaps_active.append("top")
if padding_bottom == 0 or snap_to_bottom or is_person:
snaps_active.append("bottom")
if padding_left == 0 or snap_to_left:
snaps_active.append("left")
if padding_right == 0 or snap_to_right:
snaps_active.append("right")
# Snap handling
if snaps_active:
if "top" in snaps_active and "bottom" in snaps_active:
# Dual vertical snap: fill height
new_height = target_height
new_width = int(new_height * aspect_ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
y = 0
if "left" in snaps_active:
x = 0
elif "right" in snaps_active:
x = target_width - new_width
else:
x = (target_width - new_width) // 2
log.append({"action": "resize_snap_vertical", "new_width": str(new_width), "new_height": str(new_height)})
log.append({"action": "position_snap_vertical", "x": str(x), "y": str(y)})
elif "left" in snaps_active and "right" in snaps_active:
# Dual horizontal snap: fill width
new_width = target_width
new_height = int(new_width / aspect_ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
x = 0
if "top" in snaps_active:
y = 0
elif "bottom" in snaps_active:
y = target_height - new_height
else:
y = (target_height - new_height) // 2
log.append({"action": "resize_snap_horizontal", "new_width": str(new_width), "new_height": str(new_height)})
log.append({"action": "position_snap_horizontal", "x": str(x), "y": str(y)})
else:
# Original snap logic
available_width = target_width
available_height = target_height
if "left" not in snaps_active:
available_width -= padding_left
if "right" not in snaps_active:
available_width -= padding_right
if "top" not in snaps_active:
available_height -= padding_top
if "bottom" not in snaps_active:
available_height -= padding_bottom
if aspect_ratio < 1: # Portrait
new_height = available_height
new_width = int(new_height * aspect_ratio)
if new_width > available_width:
new_width = available_width
new_height = int(new_width / aspect_ratio)
else: # Landscape
new_width = available_width
new_height = int(new_width / aspect_ratio)
if new_height > available_height:
new_height = available_height
new_width = int(new_height * aspect_ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
if "left" in snaps_active:
x = 0
elif "right" in snaps_active:
x = target_width - new_width
else:
x = padding_left + (available_width - new_width) // 2
if "top" in snaps_active:
y = 0
elif "bottom" in snaps_active:
y = target_height - new_height
else:
y = padding_top + (available_height - new_height) // 2
log.append({"action": "resize", "new_width": str(new_width), "new_height": str(new_height)})
log.append({"action": "position", "x": str(x), "y": str(y)})
else:
# No snaps: use cropped sides logic
if len(cropped_sides) == 4:
# All sides cropped: center crop to fit
if aspect_ratio > 1:
new_height = target_height
new_width = int(new_height * aspect_ratio)
left = (new_width - target_width) // 2
image = image.resize((new_width, new_height), Image.LANCZOS)
image = image.crop((left, 0, left + target_width, target_height))
else:
new_width = target_width
new_height = int(new_width / aspect_ratio)
top = (new_height - target_height) // 2
image = image.resize((new_width, new_height), Image.LANCZOS)
image = image.crop((0, top, target_width, top + target_height))
x, y = 0, 0
log.append({"action": "center_crop_resize", "new_size": f"{target_width}x{target_height}"})
elif not cropped_sides:
# No cropping: fit within padding
new_height = target_height - padding_top - padding_bottom
new_width = int(new_height * aspect_ratio)
if new_width > target_width - padding_left - padding_right:
new_width = target_width - padding_left - padding_right
new_height = int(new_width / aspect_ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
x = (target_width - new_width) // 2
y = target_height - new_height - padding_bottom
log.append({"action": "resize", "new_width": str(new_width), "new_height": str(new_height)})
log.append({"action": "position", "x": str(x), "y": str(y)})
else:
# Partial cropping: implement specific cases as needed
# For simplicity, assume centering as a fallback
new_width = target_width - padding_left - padding_right
new_height = int(new_width / aspect_ratio)
if new_height > target_height - padding_top - padding_bottom:
new_height = target_height - padding_top - padding_bottom
new_width = int(new_height * aspect_ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
x = (target_width - new_width) // 2
y = (target_height - new_height) // 2
log.append({"action": "resize_partial_crop", "new_width": str(new_width), "new_height": str(new_height)})
log.append({"action": "position_partial_crop", "x": str(x), "y": str(y)})
# Birefnet override
if bg_method in ['birefnet', 'birefnet_2']:
target_width = min(canvas_size[0] // 2, image.width)
target_height = min(canvas_size[1] // 2, image.height)
if aspect_ratio > 1:
new_width = target_width
new_height = int(new_width / aspect_ratio)
else:
new_height = target_height
new_width = int(new_height * aspect_ratio)
image = image.resize((new_width, new_height), Image.LANCZOS)
x = (canvas_size[0] - new_width) // 2
y = (canvas_size[1] - new_height) // 2
log.append({"action": "birefnet_resize", "new_size": f"{new_width}x{new_height}", "position": f"{x},{y}"})
return log, image, x, y
def position_logic_none(image, canvas_size):
target_width, target_height = canvas_size
aspect_ratio = image.width / image.height
# Berikan margin di semua sisi (misalnya 50px dari setiap tepi)
margin = 50
available_width = target_width - (2 * margin)
available_height = target_height - (2 * margin)
# Scale factor untuk memperkecil gambar (85% dari ukuran available space)
scale_factor = 0.85
max_width = int(available_width * scale_factor)
max_height = int(available_height * scale_factor)
# Tentukan ukuran yang tepat dengan mempertahankan aspect ratio
# dan memastikan gambar tidak terlalu besar (diperkecil dulu)
if aspect_ratio > 1: # landscape
new_width = min(max_width, target_width - (2 * margin))
new_height = int(new_width / aspect_ratio)
if new_height > max_height:
new_height = max_height
new_width = int(new_height * aspect_ratio)
else: # portrait
new_height = min(max_height, target_height - (2 * margin))
new_width = int(new_height * aspect_ratio)
if new_width > max_width:
new_width = max_width
new_height = int(new_width / aspect_ratio)
# Resize gambar dengan ukuran baru (lebih kecil)
image = image.resize((new_width, new_height), Image.LANCZOS)
# Posisi tengah canvas
x = (target_width - new_width) // 2
y = (target_height - new_height) // 2
print(f"Image scaled down and centered: original_size={image.size}, new_size={new_width}x{new_height}, position=({x},{y}), margin={margin}px")
log = [{"action": "scale_down_and_center", "new_size": f"{new_width}x{new_height}", "position": f"{x},{y}", "margin": f"{margin}px"}]
return log, image, x, y
# ------------------ Qwen 2.5VL Inference Functions & Model Loading ------------------
import base64
from transformers import AutoModelForCausalLM, AutoProcessor, AutoTokenizer
import tempfile
import os
import base64
def encode_image(image_path):
try:
with open(image_path, "rb") as f:
image_bytes = f.read()
return base64.b64encode(image_bytes).decode('utf-8')
except Exception as e:
print(f"Error in encode_image: {str(e)}")
raise
def classify_image(image_path, unique_items):
try:
image = Image.open(image_path).convert("RGB")
image = image.resize((224, 224), Image.LANCZOS)
print(f"Classifying image: {image_path} (resized to {image.size})")
prompt = (
f"Classify this image into one of these categories: {', '.join(unique_items)}. "
f"Be sensitive to sizes of an object, e.g. 'small' or 'medium' or 'large', especially for bags. "
f"If a hand is detected, only pick classifications that mention 'hand', however if it\'s a human, only pick classifications which mentioned 'human'. "
f"Return only the classification word, nothing else."
)
# Save resized image to a temporary file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file:
image.save(temp_file.name, format='PNG')
temp_image_path = temp_file.name
# Get raw classification from API with retry logic
classification_result = inference_with_api(temp_image_path, prompt)
print(f"Raw API response for {image_path}: '{classification_result}'")
# Clean up temporary file
os.unlink(temp_image_path)
# Parse and match the classification result
classification_result = classification_result.strip().lower()
for item in unique_items:
if item.lower() in classification_result:
print(f"Matched classification for {image_path}: '{item}'")
return item
print(f"No matching classification found in response: '{classification_result}'. Expected one of: {unique_items}")
return None
except Exception as e:
print(f"Error during classification for {image_path}: {str(e)}")
return None
def analyze_image_for_snap_settings(image_path):
"""
Menganalisis gambar menggunakan Qwen untuk menentukan pengaturan snap yang tepat
"""
try:
prompt = (
"Analyze this product/model/person image and determine if it should be flush against any edges of the canvas.\n\n"
"For each edge (top, bottom, left, right), determine if the image should have padding=0 for that edge based on these specific rules:\n\n"
"1. snap_bottom=true: If it's a person/model (almost always), or if the bottom of the product is cropped or should align with bottom edge\n\n"
"2. snap_left=true: If the left side of a HAND or PRODUCT is cut off or flush against the edge, or if the hand or product is shown from side view facing left\n\n"
"3. snap_right=true: If the right side a HAND or PRODUCT is cut off or flush against the edge, or if the hand or product is shown from side view facing right\n\n"
"4. snap_top=true: If it's a person/model (almost always) or if the top of the product is cut off or should align with top edge\n\n"
"Pay special attention to product orientation: side views often need snap_left or snap_right, while front/back views may not.\n\n"
"EXAMPLES:\n"
"- For a swimwear model standing and showing profile view: {\"snap_top\": false, \"snap_right\": false, \"snap_bottom\": true, \"snap_left\": true}\n"
"- For a handbag shown from the side with handle at top: {\"snap_top\": false, \"snap_right\": false, \"snap_bottom\": true, \"snap_left\": true}\n"
"- For a bikini bottom piece shown from front: {\"snap_top\": false, \"snap_right\": false, \"snap_bottom\": false, \"snap_left\": false}\n"
"- For a swimsuit top on a model shown from side: {\"snap_top\": false, \"snap_right\": true, \"snap_bottom\": false, \"snap_left\": false}\n\n"
"Common combinations:\n"
"- For people/models, usually snap_bottom=true, snap_top=true and sometimes snap_left or snap_right depending on pose\n"
"- For bags shown from side, use both snap_bottom=true and either snap_left=true or snap_right=true\n"
"- For footwear shown from side, consider snap_bottom=true and either snap_left=true or snap_right=true\n"
"- For items cropped on multiple sides, set all appropriate snap values to true\n\n"
"Return ONLY a valid JSON in this exact format: {\"snap_top\": true/false, \"snap_right\": true/false, \"snap_bottom\": true/false, \"snap_left\": true/false}"
)
# Save image to a temporary file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file:
image = Image.open(image_path)
image.save(temp_file.name, format='PNG')
temp_image_path = temp_file.name
# Get analysis from API
analysis_result = inference_with_api(temp_image_path, prompt)
print(f"Raw analysis response for {image_path}: '{analysis_result}'")
# Clean up temporary file
os.unlink(temp_image_path)
# Parse JSON from the response
try:
# Coba parse langsung dulu
try:
snap_settings = json.loads(analysis_result)
if all(key in snap_settings for key in ["snap_top", "snap_right", "snap_bottom", "snap_left"]):
print(f"Direct JSON parsing successful for {image_path}: {snap_settings}")
return snap_settings
except:
pass # Lanjut ke regex jika direct parsing gagal
# Mencari JSON dalam respons menggunakan regex
import re
json_match = re.search(r'(\{.*?\})', analysis_result, re.DOTALL)
if json_match:
json_str = json_match.group(1)
snap_settings = json.loads(json_str)
print(f"Parsed snap settings for {image_path}: {snap_settings}")
return snap_settings
else:
print(f"No JSON found in response for {image_path}")
return None
except json.JSONDecodeError as e:
print(f"Failed to parse JSON from response for {image_path}: {e}")
return None
except Exception as e:
print(f"Error during snap setting analysis for {image_path}: {str(e)}")
return None
def analyze_image_pattern(image_path):
"""
Analyzes image patterns to determine snap settings based on cropped sides, whitespace, and content distribution.
"""
try:
# Initialize snap settings
settings = {
'snap_top': False,
'snap_right': False,
'snap_bottom': False,
'snap_left': False
}
# Load and convert image to RGBA
img = Image.open(image_path).convert("RGBA")
img_np = np.array(img)
height, width = img_np.shape[:2]
aspect_ratio = height / width
# Define mask for foreground pixels (alpha > 128)
mask = img_np[:, :, 3] > 128
# **Detect cropped sides** (foreground pixels within 5 pixels of edges)
top_cropped = np.any(mask[:5, :])
bottom_cropped = np.any(mask[-5:, :])
left_cropped = np.any(mask[:, :5])
right_cropped = np.any(mask[:, -5:])
# **Detect big whitespace** (regions with >80% pixels having alpha < 128)
top_whitespace = np.mean(img_np[:height//4, :, 3] < 128) > 0.8
bottom_whitespace = np.mean(img_np[height - height//4:, :, 3] < 128) > 0.8
left_whitespace = np.mean(img_np[:, :width//4, 3] < 128) > 0.8
right_whitespace = np.mean(img_np[:, width - width//4:, 3] < 128) > 0.8
# **Apply user-specified rules**
if top_whitespace and bottom_whitespace and top_cropped and bottom_cropped:
settings['snap_top'] = True
settings['snap_bottom'] = True
if top_whitespace and bottom_whitespace and left_whitespace and top_cropped and bottom_cropped and left_cropped:
settings['snap_top'] = True
settings['snap_bottom'] = True
settings['snap_left'] = True
if top_whitespace and bottom_whitespace and right_whitespace and top_cropped and bottom_cropped and right_cropped:
settings['snap_top'] = True
settings['snap_bottom'] = True
settings['snap_right'] = True
if bottom_whitespace and not top_whitespace and not left_whitespace and not right_whitespace and bottom_cropped and not top_cropped and not left_cropped and not right_cropped:
settings['snap_bottom'] = True
if top_whitespace and not bottom_whitespace and not left_whitespace and not right_whitespace and top_cropped and not bottom_cropped and not left_cropped and not right_cropped:
settings['snap_top'] = True
# **Additional logic from previous code**
# Set snap_bottom for portrait images if not already set
# Analyze vertical distribution for snap_top if not already set
if not settings['snap_bottom']:
bottom_foreground_ratio = np.mean(mask[height - height//4:, :])
if bottom_foreground_ratio > 0.05: # More than 5% foreground pixels in top quarter
settings['snap_bottom'] = True
# Analyze horizontal distribution if left or right snaps are not set
if not (settings['snap_left'] or settings['snap_right']):
horizontal_dist = np.sum(mask, axis=0)
left_sum = np.sum(horizontal_dist[:width//3])
right_sum = np.sum(horizontal_dist[2*width//3:])
if left_sum > 1.5 * right_sum:
settings['snap_left'] = True
elif right_sum > 1.5 * left_sum:
settings['snap_right'] = True
# Analyze vertical distribution for snap_top if not already set
if not settings['snap_top'] and aspect_ratio > 1.5:
settings['snap_top'] = True
return settings
except Exception as e:
print(f"Error in analyze_image_pattern: {e}")
return {
'snap_top': False,
'snap_right': False,
'snap_bottom': False,
'snap_left': False
}
# ------------------ Modified process_single_image ------------------
def process_single_image(
image_path,
output_folder,
bg_method,
canvas_size_name,
output_format,
bg_choice,
custom_color,
watermark_path=None,
twibbon_path=None,
rotation=None,
direction=None,
flip=False,
use_old_position=True,
sheet_data=None, # DataFrame with sheet data (if provided)
use_qwen=False,
snap_to_bottom=False,
snap_to_top=False,
snap_to_left=False,
snap_to_right=False,
auto_snap=False # Tambahan parameter untuk mengaktifkan auto snap
):
filename = os.path.basename(image_path)
base_no_ext, ext = os.path.splitext(filename.lower())
add_padding_line = False
# ================== FULL SET OF CANVAS SIZE IFS ==================
# Handle custom canvas size as tuple
if isinstance(canvas_size_name, tuple):
canvas_size = canvas_size_name
padding_top = 100
padding_right = 100
padding_bottom = 100
padding_left = 100
elif canvas_size_name == 'Rox- Columbia & Keen':
canvas_size = (1080, 1080)
padding_top = 112
padding_right = 126
padding_bottom = 116
padding_left = 126
elif canvas_size_name == 'Jansport- Zalora':
canvas_size = (762, 1100)
padding_top = 108
padding_right = 51
padding_bottom = 202
padding_left = 51
elif canvas_size_name == 'Shopify & Lazada- Herschel':
canvas_size = (1080, 1080)
padding_top = 200
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Zalora- Herschel & Hedgren':
canvas_size = (762, 1100)
padding_top = 51
padding_right = 51
padding_bottom = 202
padding_left = 51
elif canvas_size_name == 'Jansport & Bratpack & Travelon & Hedgren- Lazada':
canvas_size = (1080, 1080)
padding_top = 180
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Jansport-Human- Lazada':
canvas_size = (1080, 1080)
padding_top = 72
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'DC- Shopify':
canvas_size = (1000, 1000)
padding_top = 50
padding_right = 80
padding_bottom = 50
padding_left = 80
elif canvas_size_name == 'DC- S&L':
canvas_size = (1080, 1080)
padding_top = 180
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'ROX- Hydroflask-Shopify':
canvas_size = (1080, 1080)
padding_top = 112
padding_right = 280
padding_bottom = 116
padding_left = 274
elif canvas_size_name == 'Delsey- Lazada & Shopee':
canvas_size = (1080, 1080)
padding_top = 180
padding_right = 72
padding_bottom = 180
padding_left = 72
elif canvas_size_name == 'Grind- Keen- Shopify':
canvas_size = (1124, 1285)
padding_top = 32
padding_right = 127
padding_bottom = 80
padding_left = 132
elif canvas_size_name == 'Bratpack- Gregory & DBTK- Shopify':
canvas_size = (900, 1200)
padding_top = 72
padding_right = 66
padding_bottom = 63
padding_left = 66
elif canvas_size_name == 'Columbia- Lazada':
canvas_size = (1080, 1080)
padding_top = 72
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Topo Design MP- Tiktok':
canvas_size = (1080, 1080)
padding_top = 200
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Columbia- Shopee & Zalora':
canvas_size = (762, 1100)
padding_top = 51
padding_right = 51
padding_bottom = 202
padding_left = 51
elif canvas_size_name == 'RTR- Columbia- Shopify':
canvas_size = (1100, 737)
padding_top = 38
padding_right = 31
padding_bottom = 39
padding_left = 31
elif canvas_size_name == 'columbia.psd':
canvas_size = (730 , 610)
padding_top = 29
padding_right = 105
padding_bottom = 36
padding_left = 105
elif canvas_size_name == 'jansport-dotcom':
canvas_size = (1126, 1307)
padding_top = 50
padding_right = 50
padding_bottom = 55
padding_left = 50
elif canvas_size_name == 'jansport-tiktok':
canvas_size = (1080, 1080)
padding_top = 180
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'quiksilver-lazada':
canvas_size = (1080, 1080)
padding_top = 200
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'quiksilver-shopee':
canvas_size = (1080, 1080)
padding_top = 200
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'grind':
canvas_size = (1124, 1285)
padding_top = 32
padding_right = 127
padding_bottom = 80
padding_left = 132
elif canvas_size_name == 'Allbirds- Shopee & Rockport':
canvas_size = (1080, 1080)
if base_no_ext.endswith(("_05")):
padding_top = 440
else:
padding_top = 180
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Allbirds- Shopify':
canvas_size = (1124, 1285)
if base_no_ext.endswith("_05"):
padding_top = 700
else:
padding_top = 175
padding_right = 127
padding_bottom = 80
padding_left = 132
elif canvas_size_name == 'Billabong- S&L':
canvas_size = (1080, 1080)
padding_top = 72
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Quiksilver- Shopify':
canvas_size = (1000, 1000)
padding_top = 50
padding_right = 80
padding_bottom = 256
padding_left = 80
elif canvas_size_name == 'TTC-Shopify & Tiktok':
canvas_size = (2800, 3201)
padding_top = 392
padding_right = 50
padding_bottom = 50
padding_left = 50
elif canvas_size_name == 'Hydroflask- Shopee':
canvas_size = (1080, 1080)
padding_top = 180
padding_right = 315
padding_bottom = 180
padding_left = 315
elif canvas_size_name == 'Hydroflask- Shopify':
canvas_size = (1000, 1100)
padding_top = 46
padding_right = 348
padding_bottom = 46
padding_left = 348
elif canvas_size_name == 'WT- New- Shopify':
canvas_size = (2917, 3750)
padding_top = 629
padding_right = 608
padding_bottom = 450
padding_left = 600
elif canvas_size_name == 'Roxy-Shopee':
canvas_size = (1080, 1080)
padding_top = 72
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Skechers':
canvas_size = (3000, 3000)
padding_top = 0
padding_right = 0
padding_bottom = 0
padding_left = 0
elif canvas_size_name == 'Grind- Knockaround- Shopify':
canvas_size = (1124, 1285)
if base_no_ext.endswith("_03"):
padding_top = 175
else:
padding_top = 694
if base_no_ext.endswith("_03"):
padding_bottom = 79
else:
padding_bottom = 204
padding_right = 127
padding_left = 132
elif canvas_size_name == 'Sledgers-Lazada':
canvas_size = (1080, 1080)
padding_top = 420
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'Aetrex-Lazada':
canvas_size = (1080, 1080)
padding_top = 180
padding_right = 200
padding_bottom = 180
padding_left = 200
elif canvas_size_name == 'primer-sale.psd':
canvas_size = (700, 800)
padding_top = 13
padding_right = 13
padding_bottom = 100
padding_left = 12
elif canvas_size_name == 'TUMI-Shopify':
canvas_size = (620, 750)
padding_top = 297
padding_right = 30
padding_bottom = 56
padding_left = 30
else:
canvas_size = (1080, 1080)
padding_top = 100
padding_right = 100
padding_bottom = 100
padding_left = 100
# Classification and padding override
classification_result = None
# Logika Auto Snap yang independen dari klasifikasi
if auto_snap:
try:
print(f"Auto snap enabled, analyzing image for optimal snap settings")
# 1. Aplikasikan aturan preset terlebih dahulu (berdasarkan nama file)
preset_settings = preset_snap_rules(filename, image_path)
print(f"Preset snap settings for {filename}: {preset_settings}")
# Jika tidak ada preset khusus yang cocok (semua False), lanjut ke metode lain
if not any(preset_settings.values()):
print(f"No preset rules match for {filename}, proceeding to pattern analysis")
# 2. Analisis pola visual gambar (pendekatan berbasis computer vision)
pattern_settings = analyze_image_pattern(image_path)
print(f"Pattern analysis results for {filename}: {pattern_settings}")
# Jika pattern analysis berhasil mendeteksi setidaknya satu snap
if any(pattern_settings.values()):
# Gunakan hasil pattern analysis
snap_to_top = pattern_settings.get("snap_top", snap_to_top)
snap_to_right = pattern_settings.get("snap_right", snap_to_right)
snap_to_bottom = pattern_settings.get("snap_bottom", snap_to_bottom)
snap_to_left = pattern_settings.get("snap_left", snap_to_left)
print(f"Using pattern analysis results: top={snap_to_top}, right={snap_to_right}, bottom={snap_to_bottom}, left={snap_to_left}")
else:
# 3. Jika pattern analysis tidak memberikan hasil, gunakan AI
print(f"Pattern analysis inconclusive for {filename}, attempting AI analysis")
snap_settings = analyze_image_for_snap_settings(image_path)
if snap_settings:
# Validasi hasil snap settings
valid_snap = True
for key, value in snap_settings.items():
if not isinstance(value, bool):
print(f"Warning: Invalid value for {key}: {value}, expected boolean")
valid_snap = False
# Hanya terapkan jika hasil valid
if valid_snap:
# Override manual snap settings dengan hasil analisis
snap_to_top = snap_settings.get("snap_top", snap_to_top)
snap_to_right = snap_settings.get("snap_right", snap_to_right)
snap_to_bottom = snap_settings.get("snap_bottom", snap_to_bottom)
snap_to_left = snap_settings.get("snap_left", snap_to_left)
print(f"AI snap settings applied: top={snap_to_top}, right={snap_to_right}, bottom={snap_to_bottom}, left={snap_to_left}")
else:
print(f"Invalid AI snap settings detected, using manual settings instead")
else:
print(f"Unable to determine optimal snap settings with AI, using manual settings instead")
else:
# Gunakan preset settings jika ada
snap_to_top = preset_settings.get("snap_top", snap_to_top)
snap_to_right = preset_settings.get("snap_right", snap_to_right)
snap_to_bottom = preset_settings.get("snap_bottom", snap_to_bottom)
snap_to_left = preset_settings.get("snap_left", snap_to_left)
print(f"Using preset snap settings: top={snap_to_top}, right={snap_to_right}, bottom={snap_to_bottom}, left={snap_to_left}")
# Final settings logging
if snap_to_top:
print(f"Auto snap: Setting top padding to 0 for {filename}")
if snap_to_right:
print(f"Auto snap: Setting right padding to 0 for {filename}")
if snap_to_bottom:
print(f"Auto snap: Setting bottom padding to 0 for {filename}")
if snap_to_left:
print(f"Auto snap: Setting left padding to 0 for {filename}")
except Exception as e:
print(f"Error during auto snap analysis for {filename}: {e}")
print(f"Using manual snap settings due to auto snap error in {filename}.")
# Klasifikasi untuk padding (tidak mempengaruhi auto snap)
if use_qwen and sheet_data is not None: # Only perform classification if toggle is on and sheet data exists
try:
unique_items = sheet_data['Classification'].str.strip().str.lower().unique().tolist()
if not unique_items:
print(f"No unique items found in sheet for {filename}. Using default padding.")
else:
print(f"Unique items for classification of {filename}: {unique_items}")
classification_result = classify_image(image_path, unique_items)
if classification_result is not None:
classification = classification_result.strip().lower()
print(f"Final classification for {filename}: '{classification}'")
if any(term in classification.lower() for term in ["human", "person", "model"]):
print(f"Person detected, setting bottom padding to 0 for {filename}")
snap_to_bottom = True
matched_row = sheet_data[sheet_data['Classification'].str.strip().str.lower() == classification]
if not matched_row.empty:
row = matched_row.iloc[0]
padding_top = int(row['padding_top'])
padding_bottom = int(row['padding_bottom'])
padding_left = int(row['padding_left'])
padding_right = int(row['padding_right'])
print(f"Padding overridden for {filename}: top={padding_top}, bottom={padding_bottom}, left={padding_left}, right={padding_right}\n")
else:
print(f"No match found in sheet for classification '{classification}' in {filename}. Using default padding.\n")
else:
print(f"Classification failed for {filename}. Using default padding.")
except Exception as e:
print(f"Error during classification for {filename}: {e}")
print(f"Using default padding due to classification error in {filename}.")
else:
print(f"Qwen classification not used or no sheet data for {filename}. Using default padding.")
padding_used = {
"top": int(padding_top),
"bottom": int(padding_bottom),
"left": int(padding_left),
"right": int(padding_right)
}
# Background removal and positioning (unchanged)
if stop_event.is_set():
print("Stop event triggered, no processing.")
return None, None, None # Return None for classification too
print(f"Processing image: {filename}")
original_img = Image.open(image_path).convert("RGBA")
# Parse custom color to ensure it's in the correct format
custom_color = parse_color(custom_color)
if bg_method == 'rembg':
mask = remove_background_rembg(image_path)
elif bg_method == 'bria':
mask = remove_background_bria(image_path)
elif bg_method == 'photoroom':
mask = remove_background_photoroom(image_path)
elif bg_method == 'birefnet':
mask = remove_background_birefnet(image_path)
if not mask:
return None, None
elif bg_method == 'birefnet_2':
mask = remove_background_birefnet_2(image_path)
if not mask:
return None, None
elif bg_method == 'birefnet_hr':
mask = remove_background_birefnet_hr(image_path)
if not mask:
return None, None
elif bg_method == 'none':
mask = original_img.copy()
final_width, final_height = canvas_size
orig_w, orig_h = mask.size
threshold = 250
rgb_mask = mask.convert('RGB')
np_mask = np.array(rgb_mask)
def is_column_white(col):
return np.all(np_mask[:, col, 0] >= threshold) and np.all(np_mask[:, col, 1] >= threshold) and np.all(np_mask[:, col, 2] >= threshold)
left_crop = 0
while left_crop < orig_w and is_column_white(left_crop):
left_crop += 1
right_crop = orig_w - 1
while right_crop > 0 and is_column_white(right_crop):
right_crop -= 1
if left_crop < right_crop:
mask = mask.crop((left_crop, 0, right_crop + 1, orig_h))
mask_array = np.array(mask)
if bg_method == 'none':
new_image_array = np.array(mask)
else:
new_image_array = np.array(original_img)
new_image_array[:, :, 3] = mask_array[:, :, 3]
image_with_no_bg = Image.fromarray(new_image_array)
temp_image_path = os.path.join(output_folder, f"temp_{filename}")
image_with_no_bg.save(temp_image_path, format='PNG')
# Selalu gunakan position_logic_none untuk centering gambar
# Kode snap masih disimpan untuk kompatibilitas
if snap_to_left:
print(f"Snap to Left active: Forcing padding_left = 0 (original: {padding_left})")
if snap_to_right:
print(f"Snap to Right active: Forcing padding_right = 0 (original: {padding_right})")
if snap_to_top:
print(f"Snap to Top active: Forcing padding_top = 0 (original: {padding_top})")
if snap_to_bottom:
print(f"Snap to Bottom active: Forcing padding_bottom = 0 (original: {padding_bottom})")
# Gunakan position_logic_none untuk memastikan semua gambar diletakkan di tengah
image = Image.open(temp_image_path)
logs, cropped_img, x, y = position_logic_none(image, canvas_size)
if bg_choice == 'white':
canvas = Image.new("RGBA", canvas_size, "WHITE")
elif bg_choice == 'custom':
canvas = Image.new("RGBA", canvas_size, custom_color)
elif bg_choice == 'dominant':
dom_col = get_dominant_color(original_img)
canvas = Image.new("RGBA", canvas_size, dom_col)
else:
canvas = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
canvas.paste(cropped_img, (x, y), cropped_img)
logs.append({"action": "paste", "x": int(x), "y": int(y)})
if flip:
canvas = flip_image(canvas)
logs.append({"action": "flip_horizontal"})
if rotation != "None" and (rotation == "180 Degrees" or direction != "None"):
if rotation == "90 Degrees":
angle = 90 if direction == "Clockwise" else -90
elif rotation == "180 Degrees":
angle = 180
else:
angle = 0
rotated_subject = cropped_img.rotate(angle, expand=True)
if bg_choice == 'white':
new_canvas = Image.new("RGBA", canvas_size, "WHITE")
elif bg_choice == 'custom':
new_canvas = Image.new("RGBA", canvas_size, custom_color)
elif bg_choice == 'dominant':
dom_col = get_dominant_color(original_img)
new_canvas = Image.new("RGBA", canvas_size, dom_col)
else:
new_canvas = Image.new("RGBA", canvas_size, (0, 0, 0, 0))
# Gunakan position_logic_none untuk rotated image juga
_, rotated_sized_img, rotated_x, rotated_y = position_logic_none(rotated_subject, canvas_size)
new_canvas.paste(rotated_sized_img, (rotated_x, rotated_y), rotated_sized_img)
canvas = new_canvas
logs.append({"action": "rotate_final_centered", "rotation": rotation, "direction": direction})
out_ext = "jpg" if output_format == "JPG" else "png"
out_filename = f"{os.path.splitext(filename)[0]}.{out_ext}"
out_path = os.path.join(output_folder, out_filename)
if (base_no_ext.endswith("_01") or base_no_ext.endswith("_1") or base_no_ext.endswith("_001")) and watermark_path:
w_img = Image.open(watermark_path).convert("RGBA")
canvas.paste(w_img, (0, 0), w_img)
logs.append({"action": "add_watermark"})
if twibbon_path:
twb = Image.open(twibbon_path).convert("RGBA")
canvas.paste(twb, (0, 0), twb)
logs.append({"action": "twibbon"})
if output_format == "JPG":
canvas.convert("RGB").save(out_path, "JPEG")
else:
canvas.save(out_path, "PNG")
os.remove(temp_image_path)
print(f"Processed => {out_path}")
return [(out_path, image_path)], logs, classification_result, padding_used
# ------------------ Modified process_images ------------------
def process_images(
input_files,
bg_method='rembg',
watermark_path=None,
twibbon_path=None,
canvas_size='Rox- Columbia & Keen',
output_format='PNG',
bg_choice='transparent',
custom_color="#ffffff",
num_workers=4,
rotation=None,
direction=None,
flip=False,
use_old_position=True,
progress=gr.Progress(),
sheet_file=None,
use_qwen=False,
snap_to_bottom=False,
snap_to_top=False,
snap_to_left=False,
snap_to_right=False,
auto_snap=False
):
stop_event.clear()
start = time.time()
if bg_method in ['birefnet', 'birefnet_2']:
num_workers = 1
out_folder = "processed_images"
if os.path.exists(out_folder):
shutil.rmtree(out_folder)
os.makedirs(out_folder)
procd = []
origs = []
all_logs = []
classifications = {}
# Load sheet file if provided
sheet_data = None
if sheet_file is not None:
try:
file_path = sheet_file.name if hasattr(sheet_file, "name") else sheet_file
print(f"Attempting to load sheet file: {file_path}")
if file_path.lower().endswith(".xlsx"):
sheet_data = pd.read_excel(file_path)
elif file_path.lower().endswith(".csv"):
sheet_data = pd.read_csv(file_path)
else:
print(f"Unsupported file format for sheet: {file_path}")
if sheet_data is not None:
print(f"Sheet data loaded successfully with columns: {sheet_data.columns.tolist()}")
# Validate required columns
required_cols = {'Classification', 'padding_top', 'padding_bottom', 'padding_left', 'padding_right'}
missing_cols = required_cols - set(sheet_data.columns)
if missing_cols:
print(f"Warning: Missing required columns in sheet: {missing_cols}")
except Exception as e:
print(f"Error loading sheet file '{file_path}': {str(e)}")
sheet_data = None
# Input handling (unchanged)
if isinstance(input_files, str) and input_files.lower().endswith(('.zip', '.rar')):
tmp_in = "temp_input"
if os.path.exists(tmp_in):
shutil.rmtree(tmp_in)
os.makedirs(tmp_in)
with zipfile.ZipFile(input_files, 'r') as zf:
zf.extractall(tmp_in)
images = [os.path.join(tmp_in, f) for f in os.listdir(tmp_in) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.webp', '.tif', '.tiff', '.avif'))]
elif isinstance(input_files, list):
images = input_files
else:
images = [input_files]
total = len(images)
with ThreadPoolExecutor(max_workers=num_workers) as exe:
future_map = {
exe.submit(
process_single_image,
path,
out_folder,
bg_method,
canvas_size,
output_format,
bg_choice,
custom_color,
watermark_path,
twibbon_path,
rotation,
direction,
flip,
use_old_position,
sheet_data,
use_qwen,
snap_to_bottom,
snap_to_top,
snap_to_left,
snap_to_right,
auto_snap
): path for path in images
}
for idx, fut in enumerate(future_map):
if stop_event.is_set():
print("Stop event triggered.")
break
try:
result, log, classification, padding_used = fut.result()
if result:
procd.extend(result)
origs.append(future_map[fut])
all_logs.append({os.path.basename(future_map[fut]): log})
classifications[os.path.basename(future_map[fut])] = {
"classification": classification if classification else "N/A",
"padding": padding_used
}
progress((idx + 1) / total, f"{idx + 1}/{total} processed")
except Exception as e:
print(f"Error processing {future_map[fut]}: {str(e)}")
# Save classifications (unchanged)
with open(os.path.join(out_folder, "classifications.json"), "w") as cf:
json.dump(classifications, cf, indent=2)
zip_out = "processed_images.zip"
with zipfile.ZipFile(zip_out, 'w') as zf:
for outf, _ in procd:
zf.write(outf, os.path.basename(outf))
with open(os.path.join(out_folder, "process_log.json"), "w") as lf:
json.dump(all_logs, lf, indent=2)
elapsed = time.time() - start
print(f"Done in {elapsed:.2f}s")
return origs, procd, zip_out, elapsed, classifications
# ------------------ Gradio UI Setup ------------------
import gradio as gr
from concurrent.futures import ThreadPoolExecutor
def gradio_interface(
input_files,
bg_method,
watermark,
twibbon,
canvas_size,
output_format,
bg_choice,
custom_color,
num_workers,
rotation=None,
direction=None,
flip=False,
sheet_file=None,
use_qwen= False, # sheet file input
snap_to_bottom=False,
snap_to_top=False,
snap_to_left=False,
snap_to_right=False,
auto_snap=False
):
if bg_method in ['birefnet', 'birefnet_2', 'birefnet_hr']:
num_workers = min(num_workers, 2)
progress = gr.Progress()
watermark_path = watermark.name if watermark else None
twibbon_path = twibbon.name if twibbon else None
if isinstance(input_files, str) and input_files.lower().endswith(('.zip', '.rar')):
return process_images(
input_files, bg_method, watermark_path, twibbon_path,
canvas_size, output_format, bg_choice, custom_color, num_workers,
rotation, direction, flip, True, progress, sheet_file, use_qwen,
snap_to_bottom, snap_to_top, snap_to_left, snap_to_right, auto_snap
)
elif isinstance(input_files, list):
return process_images(
input_files, bg_method, watermark_path, twibbon_path,
canvas_size, output_format, bg_choice, custom_color, num_workers,
rotation, direction, flip, True, progress, sheet_file, use_qwen,
snap_to_bottom, snap_to_top, snap_to_left, snap_to_right, auto_snap
)
else:
return process_images(
input_files.name, bg_method, watermark_path, twibbon_path,
canvas_size, output_format, bg_choice, custom_color, num_workers,
rotation, direction, flip, True, progress, sheet_file, use_qwen,
snap_to_bottom, snap_to_top, snap_to_left, snap_to_right, auto_snap
)
def show_color_picker(bg_choice):
if bg_choice == 'custom':
return gr.update(visible=True)
return gr.update(visible=False)
def show_custom_canvas(canvas_size):
if canvas_size == 'Custom':
return gr.update(visible=True), gr.update(visible=True)
return gr.update(visible=False), gr.update(visible=False)
def parse_color(color_str):
"""Convert color string to format that PIL can understand"""
if not color_str:
return "#ffffff"
# If it's already a hex color, return as-is
if color_str.startswith('#'):
return color_str
# Handle rgba() format from Gradio ColorPicker
if color_str.startswith('rgba(') or color_str.startswith('rgb('):
import re
# Extract numbers from rgba(r, g, b, a) or rgb(r, g, b)
numbers = re.findall(r'[\d.]+', color_str)
if len(numbers) >= 3:
r = int(float(numbers[0]))
g = int(float(numbers[1]))
b = int(float(numbers[2]))
# Convert to hex
return f"#{r:02x}{g:02x}{b:02x}"
# Default fallback
return "#ffffff"
def update_compare(evt: gr.SelectData, classifications):
if isinstance(evt.value, dict) and 'caption' in evt.value:
in_path = evt.value['caption'].split("Input: ")[-1]
out_path = evt.value['image']['path']
orig = Image.open(in_path)
proc = Image.open(out_path)
ratio_o = f"{orig.width}x{orig.height}"
ratio_p = f"{proc.width}x{proc.height}"
filename = os.path.basename(in_path)
if filename in classifications:
cls = classifications[filename]["classification"]
pad = classifications[filename]["padding"]
selected_info_text = f"Classification: {cls}, Padding - Top: {pad['top']}, Bottom: {pad['bottom']}, Left: {pad['left']}, Right: {pad['right']}"
else:
selected_info_text = "No classification data available"
return (
gr.update(value=in_path),
gr.update(value=out_path),
gr.update(value=ratio_o),
gr.update(value=ratio_p),
gr.update(value=selected_info_text)
)
else:
print("No caption found in selection.")
return (
gr.update(value=None),
gr.update(value=None),
gr.update(value=""),
gr.update(value=""),
gr.update(value="Select an image to see details")
)
def process(
input_files,
bg_method,
watermark,
twibbon,
canvas_size,
output_format,
bg_choice,
custom_color,
num_workers,
rotation=None,
direction=None,
flip=False,
sheet_file=None,
use_qwen_str="Default (No Vision)",
snap_to_bottom=False,
snap_to_top=False,
snap_to_left=False,
snap_to_right=False,
auto_snap=False,
canvas_width=1080,
canvas_height=1080
):
use_qwen = (use_qwen_str == "Utilize Vision Model") # Convert string to boolean
# Handle custom canvas size
if canvas_size == 'Custom':
canvas_size = (canvas_width, canvas_height)
_, procd, zip_out, tt, classifications = gradio_interface(
input_files, bg_method, watermark, twibbon,
canvas_size, output_format, bg_choice, custom_color, num_workers,
rotation, direction, flip, sheet_file, use_qwen, snap_to_bottom, snap_to_top, snap_to_left, snap_to_right, auto_snap
)
if not procd:
return [], None, "No Image Processed.", "No Classification Available", {}
result_g = []
for outf, inf in procd:
if not os.path.exists(outf):
print(f"[ERROR] Missing out: {outf}")
continue
result_g.append((outf, f"Input: {inf}"))
class_text = "\n".join([
f"{img}: Classification - {data['classification']}, Padding - Top: {data['padding']['top']}, Bottom: {data['padding']['bottom']}, Left: {data['padding']['left']}, Right: {data['padding']['right']}"
for img, data in classifications.items()
]) or "No classifications recorded."
return result_g, zip_out, f"{tt:.2f} seconds", class_text, classifications
def stop_processing():
stop_event.set()
def preset_snap_rules(filename, image_path=None):
"""
Menerapkan aturan preset untuk snap settings berdasarkan nama file atau kategori
Returns dict dengan format {'snap_top': bool, 'snap_right': bool, 'snap_bottom': bool, 'snap_left': bool}
"""
filename_lower = filename.lower()
# Default settings
settings = {
'snap_top': False,
'snap_right': False,
'snap_bottom': False,
'snap_left': False
}
# ---- Pola untuk produk berdasarkan urutan gambar ----
# Angka di filename biasanya menunjukkan view produk
view_num = None
for pattern in ['_01', '_02', '_03', '_04', '_05', '_06', '_1.', '_2.', '_3.', '_4.', '_5.', '_6.']:
if pattern in filename_lower:
view_num = int(pattern.strip('_.'))
break
# --- Pola Format Pendek (pakaian renang, baju, pakaian olahraga) ---
# Format: @1000xxxxxx_01.jpg, @1000xxxxxx_02.jpg, dll
if filename_lower.startswith('@10002'):
print(f"Matched special pattern @10002xxxxx for {filename}")
# View pertama biasanya depan, snap_bottom
if view_num == 1:
settings['snap_bottom'] = True
settings['snap_left'] = True
# View kedua biasanya belakang, snap_bottom
elif view_num == 2:
settings['snap_bottom'] = True
settings['snap_right'] = True
# View ketiga biasanya samping, snap_left dan snap_bottom
elif view_num == 3:
settings['snap_bottom'] = True
settings['snap_left'] = True
settings['snap_top'] = True
# View keempat biasanya samping lain, snap_right dan snap_bottom
elif view_num == 4:
settings['snap_bottom'] = True
settings['snap_right'] = True
settings['snap_top'] = True
# --- Pola Bikini/Baju Renang ---
elif any(x in filename_lower for x in ['bikini', 'swimwear', 'swimsuit', 'swim']):
# Untuk bikini tops (hanya bagian atas)
if any(x in filename_lower for x in ['top', 'bra', 'bust']):
if view_num == 1: # Foto produk pertama - biasanya depan
settings['snap_bottom'] = True
elif view_num == 2: # Foto produk kedua - biasanya belakang
settings['snap_bottom'] = True
elif view_num == 3: # Foto produk ketiga - biasanya samping
settings['snap_bottom'] = True
settings['snap_left'] = True
elif view_num == 4: # Foto produk keempat - biasanya samping lain
settings['snap_bottom'] = True
settings['snap_right'] = True
# Untuk bikini bottoms (hanya bagian bawah)
elif any(x in filename_lower for x in ['bottom', 'pant', 'brief']):
if view_num == 1: # Foto produk pertama - biasanya depan
settings['snap_bottom'] = True
elif view_num == 2: # Foto produk kedua - biasanya belakang
settings['snap_bottom'] = True
elif view_num == 3: # Foto produk ketiga - biasanya samping
settings['snap_bottom'] = True
settings['snap_left'] = True
settings['snap_top'] = True
elif view_num == 4: # Foto produk keempat - biasanya samping lain
settings['snap_bottom'] = True
settings['snap_right'] = True
settings['snap_top'] = True
# Untuk one-piece atau bikini sets
else:
if view_num == 1: # Foto produk pertama - biasanya depan
settings['snap_bottom'] = True
elif view_num == 2: # Foto produk kedua - biasanya belakang
settings['snap_bottom'] = True
elif view_num == 3: # Foto produk ketiga - biasanya samping
settings['snap_bottom'] = True
settings['snap_left'] = True
elif view_num == 4: # Foto produk keempat - biasanya samping lain
settings['snap_bottom'] = True
settings['snap_right'] = True
# --- Pola Pakaian Dengan Model ---
elif any(x in filename_lower for x in ['_model_', 'human', 'person']):
settings['snap_bottom'] = True
# Jika terlihat dari samping, tambahkan snap kiri atau kanan
if "_left" in filename_lower or "_samping" in filename_lower:
settings['snap_left'] = True
if "_right" in filename_lower:
settings['snap_right'] = True
# --- Pola untuk Tas ---
elif any(x in filename_lower for x in ['bag', 'backpack', 'tas', 'sling']):
# Format kode file tertentu
if view_num == 1: # View depan
settings['snap_bottom'] = True
elif view_num == 2: # View belakang
settings['snap_bottom'] = True
elif view_num == 3: # View samping
settings['snap_bottom'] = True
settings['snap_left'] = True
elif view_num == 4: # View samping lain
settings['snap_bottom'] = True
settings['snap_right'] = True
# --- Pola untuk Sepatu ---
elif any(x in filename_lower for x in ['shoe', 'footwear', 'sepatu']):
if "_side" in filename_lower or "_samping" in filename_lower:
settings['snap_bottom'] = True
if "_left" in filename_lower:
settings['snap_left'] = True
elif "_right" in filename_lower:
settings['snap_right'] = True
else:
# Default untuk sepatu dari samping (biasanya sepatu kiri)
settings['snap_left'] = True
# --- Kasus khusus berdasarkan nama file persis ---
# Contoh file yang disebutkan user
if "1000218277_01" in filename_lower:
settings['snap_bottom'] = True
settings['snap_left'] = True
elif "1000218265_01" in filename_lower:
settings['snap_top'] = True
settings['snap_bottom'] = True
settings['snap_left'] = True
elif "1000218268_01" in filename_lower:
settings['snap_top'] = True
settings['snap_bottom'] = True
settings['snap_right'] = True
# Kasus khusus untuk pola @1000xxxxxx (seperti yang disebutkan user)
elif filename_lower.startswith('@'):
if '_01' in filename_lower and filename_lower.startswith('@10002'):
settings['snap_bottom'] = True
settings['snap_left'] = True
# Tambahkan lebih banyak pola sesuai kebutuhan
return settings
with gr.Blocks(theme='allenai/gradio-theme') as iface:
gr.Markdown("## Image BG Removal with Rotation, Watermark, Twibbon & Classifications for Padding Override")
with gr.Row():
input_files = gr.File(label="Upload (Image(s)/ZIP/RAR)", file_types=[".zip", ".rar", "image"], interactive=True)
watermark = gr.File(label="Watermark (Optional)", file_types=[".png"])
twibbon = gr.File(label="Twibbon (Optional)", file_types=[".png"])
sheet_file = gr.File(label="Upload Sheet (.xlsx/.csv)", file_types=[".xlsx", ".csv"], interactive=True)
with gr.Row():
bg_method = gr.Radio(["bria", "none"],
label="Background Removal", value="bria")
bg_choice = gr.Radio(["transparent", "white", "custom"], label="BG Choice", value="white")
custom_color = gr.ColorPicker(label="Custom BG", value="#ffffff", visible=False)
output_format = gr.Radio(["PNG", "JPG"], label="Output Format", value="JPG")
num_workers = gr.Slider(1, 16, 1, label="Number of Workers", value=5)
use_qwen = gr.Dropdown(
["Default (No Vision)", "Utilize Vision Model"],
label="Classification",
value="Default (No Vision)" # Default is off
)
with gr.Row():
canvas_size = gr.Radio(
choices=[
"primer-sale.psd", "Custom"
],
label="Canvas Size", value="primer-sale.psd"
)
with gr.Row() as custom_canvas_row:
canvas_width = gr.Number(label="Canvas Width (px)", value=1080, minimum=1, maximum=5000, step=1, visible=False)
canvas_height = gr.Number(label="Canvas Height (px)", value=1080, minimum=1, maximum=5000, step=1, visible=False)
with gr.Row():
rotation = gr.Radio(["None", "90 Degrees", "180 Degrees"], label="Rotation Angle", value="None")
direction = gr.Radio(["None", "Clockwise", "Anticlockwise"], label="Direction", value="None")
flip_option = gr.Checkbox(label="Flip Horizontal", value=False)
auto_snap = gr.Checkbox(label="Auto Snap (Gunakan AI untuk menentukan snap setting)", value=False)
# Kelompokkan semua snap manual di baris yang terpisah
with gr.Row() as manual_snap_row:
gr.Markdown("### Manual Snap Settings (tidak digunakan jika Auto Snap aktif)")
snap_to_bottom = gr.Checkbox(label="Snap to Bottom (Force padding bottom 0)", value=False)
snap_to_top = gr.Checkbox(label="Snap to Top (Force padding top 0)", value=False)
snap_to_left = gr.Checkbox(label="Snap to Left (Force padding left 0)", value=False)
snap_to_right = gr.Checkbox(label="Snap to Right (Force padding right 0)", value=False)
proc_btn = gr.Button("Process Images")
stop_btn = gr.Button("Stop")
with gr.Row():
gallery_processed = gr.Gallery(label="Processed Images")
with gr.Row():
selected_info = gr.Textbox(label="Selected Image Classification and Padding", lines=2, interactive=False)
with gr.Row():
img_orig = gr.Image(label="Original", interactive=False)
img_proc = gr.Image(label="Processed", interactive=False)
with gr.Row():
ratio_orig = gr.Textbox(label="Original Ratio")
ratio_proc = gr.Textbox(label="Processed Ratio")
with gr.Row():
out_zip = gr.File(label="Download as ZIP")
time_box = gr.Textbox(label="Processing Time (seconds)")
classifications_state = gr.State()
with gr.Row():
class_display = gr.Textbox(label="All Classification and Padding Results", lines=5, interactive=False)
bg_choice.change(show_color_picker, inputs=bg_choice, outputs=custom_color)
canvas_size.change(show_custom_canvas, inputs=canvas_size, outputs=[canvas_width, canvas_height])
proc_btn.click(
fn=process,
inputs=[input_files, bg_method, watermark, twibbon, canvas_size, output_format,
bg_choice, custom_color, num_workers, rotation, direction, flip_option,
sheet_file, use_qwen, snap_to_bottom, snap_to_top, snap_to_left, snap_to_right,
auto_snap, canvas_width, canvas_height],
outputs=[gallery_processed, out_zip, time_box, class_display, classifications_state]
)
gallery_processed.select(
update_compare,
inputs=[classifications_state],
outputs=[img_orig, img_proc, ratio_orig, ratio_proc, selected_info]
)
stop_btn.click(fn=stop_processing, outputs=[])
# Add dependency for hiding/showing manual snap options
def update_manual_snap_visibility(auto_snap_active):
return gr.update(visible=not auto_snap_active)
auto_snap.change(
fn=update_manual_snap_visibility,
inputs=[auto_snap],
outputs=[manual_snap_row]
)
iface.launch(share=True)