| 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) |
|
|
|
|