|
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_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") |
|
else: |
|
img.save(output_path, "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 |
|
|
|
|
|
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) |
|
""" |
|
|
|
image = Image.open(image_path).convert("RGBA") |
|
log = [] |
|
x, y = 0, 0 |
|
|
|
|
|
if use_threshold: |
|
bbox = get_bounding_box_with_threshold(image, threshold=10) |
|
else: |
|
bbox = image.getbbox() |
|
|
|
if bbox: |
|
|
|
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])]}) |
|
|
|
|
|
target_width, target_height = canvas_size |
|
aspect_ratio = image.width / image.height |
|
|
|
|
|
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") |
|
|
|
|
|
if snaps_active: |
|
if "top" in snaps_active and "bottom" in snaps_active: |
|
|
|
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: |
|
|
|
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: |
|
|
|
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: |
|
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: |
|
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: |
|
|
|
if len(cropped_sides) == 4: |
|
|
|
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: |
|
|
|
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: |
|
|
|
|
|
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)}) |
|
|
|
|
|
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 |
|
|
|
|
|
margin = 50 |
|
available_width = target_width - (2 * margin) |
|
available_height = target_height - (2 * margin) |
|
|
|
|
|
scale_factor = 0.85 |
|
max_width = int(available_width * scale_factor) |
|
max_height = int(available_height * scale_factor) |
|
|
|
|
|
|
|
if aspect_ratio > 1: |
|
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: |
|
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) |
|
|
|
|
|
image = image.resize((new_width, new_height), Image.LANCZOS) |
|
|
|
|
|
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 |
|
|
|
|
|
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." |
|
) |
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file: |
|
image.save(temp_file.name, format='PNG') |
|
temp_image_path = temp_file.name |
|
|
|
|
|
classification_result = inference_with_api(temp_image_path, prompt) |
|
print(f"Raw API response for {image_path}: '{classification_result}'") |
|
|
|
|
|
os.unlink(temp_image_path) |
|
|
|
|
|
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}" |
|
) |
|
|
|
|
|
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 |
|
|
|
|
|
analysis_result = inference_with_api(temp_image_path, prompt) |
|
print(f"Raw analysis response for {image_path}: '{analysis_result}'") |
|
|
|
|
|
os.unlink(temp_image_path) |
|
|
|
|
|
try: |
|
|
|
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 |
|
|
|
|
|
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: |
|
|
|
settings = { |
|
'snap_top': False, |
|
'snap_right': False, |
|
'snap_bottom': False, |
|
'snap_left': False |
|
} |
|
|
|
|
|
img = Image.open(image_path).convert("RGBA") |
|
img_np = np.array(img) |
|
height, width = img_np.shape[:2] |
|
aspect_ratio = height / width |
|
|
|
|
|
mask = img_np[:, :, 3] > 128 |
|
|
|
|
|
top_cropped = np.any(mask[:5, :]) |
|
bottom_cropped = np.any(mask[-5:, :]) |
|
left_cropped = np.any(mask[:, :5]) |
|
right_cropped = np.any(mask[:, -5:]) |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
if not settings['snap_bottom']: |
|
bottom_foreground_ratio = np.mean(mask[height - height//4:, :]) |
|
if bottom_foreground_ratio > 0.05: |
|
settings['snap_bottom'] = True |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
} |
|
|
|
|
|
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, |
|
use_qwen=False, |
|
snap_to_bottom=False, |
|
snap_to_top=False, |
|
snap_to_left=False, |
|
snap_to_right=False, |
|
auto_snap=False |
|
): |
|
filename = os.path.basename(image_path) |
|
base_no_ext, ext = os.path.splitext(filename.lower()) |
|
add_padding_line = False |
|
|
|
|
|
|
|
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_result = None |
|
|
|
|
|
if auto_snap: |
|
try: |
|
print(f"Auto snap enabled, analyzing image for optimal snap settings") |
|
|
|
|
|
preset_settings = preset_snap_rules(filename, image_path) |
|
print(f"Preset snap settings for {filename}: {preset_settings}") |
|
|
|
|
|
if not any(preset_settings.values()): |
|
print(f"No preset rules match for {filename}, proceeding to pattern analysis") |
|
|
|
|
|
pattern_settings = analyze_image_pattern(image_path) |
|
print(f"Pattern analysis results for {filename}: {pattern_settings}") |
|
|
|
|
|
if any(pattern_settings.values()): |
|
|
|
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: |
|
|
|
print(f"Pattern analysis inconclusive for {filename}, attempting AI analysis") |
|
snap_settings = analyze_image_for_snap_settings(image_path) |
|
|
|
if 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 |
|
|
|
|
|
if valid_snap: |
|
|
|
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: |
|
|
|
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}") |
|
|
|
|
|
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}.") |
|
|
|
|
|
if use_qwen and sheet_data is not None: |
|
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) |
|
} |
|
|
|
|
|
if stop_event.is_set(): |
|
print("Stop event triggered, no processing.") |
|
return None, None, None |
|
|
|
print(f"Processing image: {filename}") |
|
original_img = Image.open(image_path).convert("RGBA") |
|
|
|
|
|
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') |
|
|
|
|
|
|
|
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})") |
|
|
|
|
|
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)) |
|
|
|
|
|
_, 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 |
|
|
|
|
|
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 = {} |
|
|
|
|
|
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()}") |
|
|
|
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 |
|
|
|
|
|
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)}") |
|
|
|
|
|
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 |
|
|
|
|
|
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, |
|
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 color_str.startswith('#'): |
|
return color_str |
|
|
|
|
|
if color_str.startswith('rgba(') or color_str.startswith('rgb('): |
|
import re |
|
|
|
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])) |
|
|
|
return f"#{r:02x}{g:02x}{b:02x}" |
|
|
|
|
|
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") |
|
|
|
|
|
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() |
|
|
|
|
|
settings = { |
|
'snap_top': False, |
|
'snap_right': False, |
|
'snap_bottom': False, |
|
'snap_left': False |
|
} |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
if filename_lower.startswith('@10002'): |
|
print(f"Matched special pattern @10002xxxxx for {filename}") |
|
|
|
if view_num == 1: |
|
settings['snap_bottom'] = True |
|
settings['snap_left'] = True |
|
|
|
elif view_num == 2: |
|
settings['snap_bottom'] = True |
|
settings['snap_right'] = True |
|
|
|
elif view_num == 3: |
|
settings['snap_bottom'] = True |
|
settings['snap_left'] = True |
|
settings['snap_top'] = True |
|
|
|
elif view_num == 4: |
|
settings['snap_bottom'] = True |
|
settings['snap_right'] = True |
|
settings['snap_top'] = True |
|
|
|
|
|
elif any(x in filename_lower for x in ['bikini', 'swimwear', 'swimsuit', 'swim']): |
|
|
|
if any(x in filename_lower for x in ['top', 'bra', 'bust']): |
|
if view_num == 1: |
|
settings['snap_bottom'] = True |
|
elif view_num == 2: |
|
settings['snap_bottom'] = True |
|
elif view_num == 3: |
|
settings['snap_bottom'] = True |
|
settings['snap_left'] = True |
|
elif view_num == 4: |
|
settings['snap_bottom'] = True |
|
settings['snap_right'] = True |
|
|
|
elif any(x in filename_lower for x in ['bottom', 'pant', 'brief']): |
|
if view_num == 1: |
|
settings['snap_bottom'] = True |
|
elif view_num == 2: |
|
settings['snap_bottom'] = True |
|
elif view_num == 3: |
|
settings['snap_bottom'] = True |
|
settings['snap_left'] = True |
|
settings['snap_top'] = True |
|
elif view_num == 4: |
|
settings['snap_bottom'] = True |
|
settings['snap_right'] = True |
|
settings['snap_top'] = True |
|
|
|
else: |
|
if view_num == 1: |
|
settings['snap_bottom'] = True |
|
elif view_num == 2: |
|
settings['snap_bottom'] = True |
|
elif view_num == 3: |
|
settings['snap_bottom'] = True |
|
settings['snap_left'] = True |
|
elif view_num == 4: |
|
settings['snap_bottom'] = True |
|
settings['snap_right'] = True |
|
|
|
|
|
elif any(x in filename_lower for x in ['_model_', 'human', 'person']): |
|
settings['snap_bottom'] = True |
|
|
|
if "_left" in filename_lower or "_samping" in filename_lower: |
|
settings['snap_left'] = True |
|
if "_right" in filename_lower: |
|
settings['snap_right'] = True |
|
|
|
|
|
elif any(x in filename_lower for x in ['bag', 'backpack', 'tas', 'sling']): |
|
|
|
if view_num == 1: |
|
settings['snap_bottom'] = True |
|
elif view_num == 2: |
|
settings['snap_bottom'] = True |
|
elif view_num == 3: |
|
settings['snap_bottom'] = True |
|
settings['snap_left'] = True |
|
elif view_num == 4: |
|
settings['snap_bottom'] = True |
|
settings['snap_right'] = True |
|
|
|
|
|
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: |
|
|
|
settings['snap_left'] = True |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
elif filename_lower.startswith('@'): |
|
if '_01' in filename_lower and filename_lower.startswith('@10002'): |
|
settings['snap_bottom'] = True |
|
settings['snap_left'] = True |
|
|
|
|
|
|
|
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)" |
|
) |
|
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) |
|
|
|
|
|
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=[]) |
|
|
|
|
|
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) |
|
|
|
|