Spaces:
Running
Running
File size: 24,641 Bytes
13aa528 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 |
import os
import tempfile
import zipfile
import shutil # For make_archive
import uuid
from PIL import Image, ImageDraw
from psd_tools import PSDImage
from psd_tools.api.layers import PixelLayer
import gradio as gr
import traceback # For printing stack traces
import subprocess
def install(package):
subprocess.check_call([os.sys.executable, "-m", "pip", "install", package])
install("timm")
install("pydantic==2.10.6")
install("dghs-imgutils==0.15.0")
install("onnxruntime >= 1.17.0")
install("psd_tools==1.10.7")
# os.environ["no_proxy"] = "localhost,127.0.0.1,::1"
# --- Attempt to import the actual function (Detector 1) ---
# Let's keep the original import name as requested in the previous version
try:
# Assuming this is the intended "Detector 1"
from src.wise_crop.detect_and_crop import crop_and_mask_characters_gradio
detector_1_available = True
print("Successfully imported 'crop_and_mask_characters_gradio' as Detector 1.")
except ImportError:
detector_1_available = False
print("Warning: Could not import 'crop_and_mask_characters_gradio'. Using dummy function for Detector 1.")
# Define a dummy version for Detector 1 if import fails
def crop_and_mask_characters_gradio(image_pil: Image.Image):
"""Dummy function 1 if import fails."""
print("Using DUMMY Detector 1.")
if image_pil is None: return []
width, height = image_pil.size
boxes = [
(0, (int(width * 0.1), int(height * 0.1), int(width * 0.3), int(height * 0.4))),
(1, (int(width * 0.6), int(height * 0.5), int(width * 0.25), int(height * 0.35))),
]
valid_boxes = []
for i, (x, y, w, h) in boxes:
x1, y1, x2, y2 = max(0, x), max(0, y), min(width, x + w), min(height, y + h)
if x2 - x1 > 0 and y2 - y1 > 0: valid_boxes.append((i, (x1, y1, x2 - x1, y2 - y1)))
return valid_boxes
# from src.oskar_crop.detect_and_crop import process_single_image as detector_2_function
try:
# Assuming this is the intended "Detector 2"
# Note: Renamed the import alias to avoid conflict if both imports succeed.
# The function call inside process_lineart still uses crop_and_mask_characters_gradio_2
from src.oskar_crop.detect_and_crop import process_single_image as detector_2_function
detector_2_available = True
print("Successfully imported 'process_single_image' as Detector 2.")
# Define the function name used in process_lineart
def crop_and_mask_characters_gradio_2(image_pil: Image.Image):
return detector_2_function(image_pil)
except ImportError:
detector_2_available = False
print("Warning: Could not import 'process_single_image'. Using dummy function for Detector 2.")
# Define a dummy version for Detector 2 if import fails
# --- Define the SECOND Dummy Detection Function ---
def crop_and_mask_characters_gradio_2(image_pil: Image.Image):
"""
SECOND Dummy function to simulate detecting objects and returning bounding boxes.
Returns different results than the first function.
"""
print("Using DUMMY Detector 2.")
if image_pil is None:
return []
width, height = image_pil.size
print(f"Dummy detection 2 running on image size: {width}x{height}")
# Define DIFFERENT fixed bounding boxes for demonstration
boxes = [
(0, (int(width * 0.05), int(height * 0.6), int(width * 0.4), int(height * 0.3))), # Bottom-leftish, wider
(1, (int(width * 0.7), int(height * 0.1), int(width * 0.20), int(height * 0.25))), # Top-rightish, smaller
(2, (int(width * 0.4), int(height * 0.4), int(width * 0.15), int(height * 0.15))), # Center-ish, very small
]
# Basic validation
valid_boxes = []
for i, (x, y, w, h) in boxes:
x1 = max(0, x)
y1 = max(0, y)
x2 = min(width, x + w)
y2 = min(height, y + h)
new_w = x2 - x1
new_h = y2 - y1
if new_w > 0 and new_h > 0:
valid_boxes.append((i, (x1, y1, new_w, new_h)))
print(f"Dummy detection 2 found {len(valid_boxes)} boxes.")
return valid_boxes
# --- Helper Function (make_lineart_transparent - unchanged) ---
def make_lineart_transparent(lineart_path, threshold=200):
"""Converts a lineart image file to a transparent RGBA PIL Image."""
try:
# Ensure we handle potential pathlib objects if Gradio passes them
lineart_gray = Image.open(str(lineart_path)).convert('L')
w, h = lineart_gray.size
lineart_rgba = Image.new('RGBA', (w, h), (0, 0, 0, 0))
gray_pixels = lineart_gray.load()
rgba_pixels = lineart_rgba.load()
for y in range(h):
for x in range(w):
gray_val = gray_pixels[x, y]
alpha = 255 - gray_val
if gray_val < threshold :
rgba_pixels[x, y] = (0, 0, 0, alpha)
else:
rgba_pixels[x, y] = (0, 0, 0, 0)
return lineart_rgba
except FileNotFoundError:
print(f"Helper Error: Image file not found at {lineart_path}")
# Return a blank transparent image or None? Returning None is clearer.
return None
except Exception as e:
print(f"Helper Error processing image {lineart_path}: {e}")
return None
# --- Main Processing Function (modified for better error handling with PIL) ---
def process_lineart(input_pil_or_path, detector_choice): # Input can be PIL or path from examples
"""
Processes the input lineart image using the selected detector.
Detects objects (e.g., characters based on head/face), crops them,
provides a gallery of crops, a ZIP file of crops, and a PSD file
with the original lineart (made transparent) and bounding boxes.
"""
# --- Initialize variables ---
input_pil_image = None
temp_input_path = None
using_temp_input_path = False
status_updates = ["Status: Initializing..."]
psd_output_path = None # Initialize to None
zip_output_path = None # Initialize to None
cropped_images_for_gallery = [] # Initialize to empty list
try:
# --- Handle Input ---
if input_pil_or_path is None:
gr.Warning("Please upload a PNG image or select an example.")
return [], None, None, "Status: No image provided."
print(f"Input type: {type(input_pil_or_path)}")
print(f"Input value: {input_pil_or_path}")
# Check if input is already a PIL image (from upload) or a path (from examples)
if isinstance(input_pil_or_path, Image.Image):
input_pil_image = input_pil_or_path
print("Processing PIL image from upload.")
# Create a temporary path for make_lineart_transparent if needed later
temp_input_fd, temp_input_path = tempfile.mkstemp(suffix=".png")
os.close(temp_input_fd)
input_pil_image.save(temp_input_path, "PNG")
using_temp_input_path = True
elif isinstance(input_pil_or_path, str) and os.path.exists(input_pil_or_path):
print(f"Processing image from file path: {input_pil_or_path}")
try:
input_pil_image = Image.open(input_pil_or_path)
# Use the example path directly for make_lineart_transparent
temp_input_path = input_pil_or_path
using_temp_input_path = False # Don't delete the example file later
except Exception as e:
status_updates.append(f"ERROR: Could not open image file from path '{input_pil_or_path}': {e}")
print(status_updates[-1])
return [], None, None, "\n".join(status_updates) # Return error status
else:
status_updates.append(f"ERROR: Invalid input type received: {type(input_pil_or_path)}. Expected PIL image or file path.")
print(status_updates[-1])
return [], None, None, "\n".join(status_updates) # Return error status
# --- Ensure RGBA and get dimensions ---
try:
input_pil_image = input_pil_image.convert("RGBA")
width, height = input_pil_image.size
except Exception as e:
status_updates.append(f"ERROR: Could not process input image (convert/get size): {e}")
print(status_updates[-1])
# Clean up temp file if created before error
if using_temp_input_path and temp_input_path and os.path.exists(temp_input_path):
try: os.remove(temp_input_path)
except Exception as e_rem: print(f"Warning: Could not remove temp input file {temp_input_path}: {e_rem}")
return [], None, None, "\n".join(status_updates) # Return error status
status_updates = [f"Status: Processing started using {detector_choice}."] # Reset status
print("Starting processing...")
# --- 1. Detect Objects (Conditional) ---
print(f"Selected detector: {detector_choice}")
if detector_choice == "Detector 1":
if not detector_1_available:
status_updates.append("Warning: Using DUMMY Detector 1.")
boxes_info = crop_and_mask_characters_gradio(input_pil_image)
elif detector_choice == "Detector 2":
if not detector_2_available:
status_updates.append("Warning: Using DUMMY Detector 2.")
boxes_info = crop_and_mask_characters_gradio_2(input_pil_image)
else:
# This case should ideally not happen with Radio buttons, but good for safety
status_updates.append(f"ERROR: Invalid detector choice received: {detector_choice}")
print(status_updates[-1])
# Clean up temp file if created before error
if using_temp_input_path and temp_input_path and os.path.exists(temp_input_path):
try: os.remove(temp_input_path)
except Exception as e_rem: print(f"Warning: Could not remove temp input file {temp_input_path}: {e_rem}")
return [], None, None, "\n".join(status_updates) # Return error status
if not boxes_info:
gr.Warning("No objects detected.")
status_updates.append("No objects detected.")
# Clean up temp file if created
if using_temp_input_path and temp_input_path and os.path.exists(temp_input_path):
try: os.remove(temp_input_path)
except Exception as e_rem: print(f"Warning: Could not remove temp input file {temp_input_path}: {e_rem}")
return [], None, None, "\n".join(status_updates)
status_updates.append(f"Detected {len(boxes_info)} objects.")
print(f"Detected boxes: {boxes_info}")
# --- Temporary file paths (partially adjusted) ---
temp_dir_for_outputs = tempfile.gettempdir()
unique_id = uuid.uuid4().hex[:8]
zip_base_name = os.path.join(temp_dir_for_outputs, f"cropped_images_{unique_id}")
zip_output_path = f"{zip_base_name}.zip" # Path for the final zip file
psd_output_path = os.path.join(temp_dir_for_outputs, f"lineart_boxes_{unique_id}.psd")
# temp_input_path is already handled above based on input source
# --- 2. Crop Images and Prepare for ZIP ---
with tempfile.TemporaryDirectory() as temp_crop_dir:
print(f"Saving cropped images to temporary directory: {temp_crop_dir}")
for i, (x, y, w, h) in boxes_info:
# Ensure box coordinates are within image bounds
x1, y1 = max(0, x), max(0, y)
x2, y2 = min(width, x + w), min(height, y + h)
box = (x1, y1, x2, y2)
if box[2] > box[0] and box[3] > box[1]: # Check if width and height are positive
try:
cropped_img = input_pil_image.crop(box)
cropped_images_for_gallery.append(cropped_img)
crop_filename = os.path.join(temp_crop_dir, f"cropped_{i}.png")
cropped_img.save(crop_filename, "PNG")
except Exception as e:
print(f"Error cropping or saving box {i} with coords {box}: {e}")
status_updates.append(f"Warning: Error processing crop {i}.")
else:
print(f"Skipping invalid box {i} with coords {box}")
status_updates.append(f"Warning: Skipped invalid crop dimensions for box {i}.")
# --- 3. Create ZIP File ---
# Check if any PNG files were actually created in the temp dir
if any(f.endswith(".png") for f in os.listdir(temp_crop_dir)):
print(f"Creating ZIP file: {zip_output_path} from {temp_crop_dir}")
try:
shutil.make_archive(zip_base_name, 'zip', temp_crop_dir)
status_updates.append("Cropped images ZIP created.")
# zip_output_path is already correctly set
except Exception as e:
print(f"Error creating ZIP file: {e}")
status_updates.append("Error: Failed to create ZIP file.")
zip_output_path = None # Indicate failure
else:
print("No valid cropped images were saved, skipping ZIP creation.")
status_updates.append("Skipping ZIP creation (no valid crops).")
zip_output_path = None # No zip file to provide
# --- 4. Prepare PSD Layers ---
# a) Line Layer (Use the temp_input_path which is either the original example path or a temp copy)
print(f"Using image path for transparent layer: {temp_input_path}")
line_layer_pil = make_lineart_transparent(temp_input_path)
if line_layer_pil is None:
status_updates.append("Error: Failed to create transparent lineart layer.")
print(status_updates[-1])
# Don't create PSD if lineart failed, return current results
# Clean up temp file if created
if using_temp_input_path and temp_input_path and os.path.exists(temp_input_path):
try: os.remove(temp_input_path)
except Exception as e_rem: print(f"Warning: Could not remove temp input file {temp_input_path}: {e_rem}")
return cropped_images_for_gallery, zip_output_path, None, "\n".join(status_updates) # Return None for PSD
status_updates.append("Transparent lineart layer created.")
# b) Box Layer
box_layer_pil = Image.new('RGBA', (width, height), (255, 255, 255, 255)) # White background
draw = ImageDraw.Draw(box_layer_pil)
for i, (x, y, w, h) in boxes_info:
# Use validated coords again, ensure they are within bounds
x1, y1 = max(0, x), max(0, y)
x2, y2 = min(width, x + w), min(height, y + h)
if x2 > x1 and y2 > y1: # Check validity again just in case
rect = [(x1, y1), (x2, y2)]
# Changed to fill for solid boxes, yellow fill, semi-transparent
draw.rectangle(rect, fill=(255, 255, 0, 128))
status_updates.append("Bounding box layer created.")
# --- 5. Create PSD File ---
print(f"Creating PSD file: {psd_output_path}")
# Double check layer sizes before creating PSD object
if line_layer_pil.size != (width, height) or box_layer_pil.size != (width, height):
size_error_msg = (f"Error: Layer size mismatch during PSD creation. "
f"Line: {line_layer_pil.size}, Box: {box_layer_pil.size}, "
f"Expected: {(width, height)}")
status_updates.append(size_error_msg)
print(size_error_msg)
# Clean up temp file if created
if using_temp_input_path and temp_input_path and os.path.exists(temp_input_path):
try: os.remove(temp_input_path)
except Exception as e_rem: print(f"Warning: Could not remove temp input file {temp_input_path}: {e_rem}")
return cropped_images_for_gallery, zip_output_path, None, "\n".join(status_updates) # No PSD
try:
psd = PSDImage.new(mode='RGBA', size=(width, height))
# Add layers (order matters for visibility in PSD viewers)
# Base layer is transparent by default with RGBA
psd.append(PixelLayer.frompil(line_layer_pil, layer_name='line', top=0, left=0))
psd.append(PixelLayer.frompil(box_layer_pil, layer_name='box', top=0, left=0))
psd.save(psd_output_path)
status_updates.append("PSD file created.")
except Exception as e:
print(f"Error saving PSD file: {e}")
traceback.print_exc()
status_updates.append("Error: Failed to save PSD file.")
psd_output_path = None # Indicate failure
print("Processing finished.")
status_updates.append("Success!")
final_status = "\n".join(status_updates)
# Return all paths, even if None (Gradio handles None for File output)
return cropped_images_for_gallery, zip_output_path, psd_output_path, final_status
except Exception as e:
print(f"An unexpected error occurred in process_lineart: {e}")
traceback.print_exc()
status_updates.append(f"FATAL ERROR: {e}")
final_status = "\n".join(status_updates)
# Return empty/None outputs and the error status
# Ensure cleanup happens even on fatal error
if using_temp_input_path and temp_input_path and os.path.exists(temp_input_path):
try:
os.remove(temp_input_path)
print(f"Cleaned up temporary input file due to error: {temp_input_path}")
except Exception as e_rem:
print(f"Warning: Could not remove temp input file {temp_input_path} during error handling: {e_rem}")
return [], None, None, final_status # Return safe defaults
finally:
# --- Final Cleanup (Only removes temp input if created from upload) ---
if using_temp_input_path and temp_input_path and os.path.exists(temp_input_path):
try:
os.remove(temp_input_path)
print(f"Cleaned up temporary input file: {temp_input_path}")
except Exception as e_rem:
# This might happen if the file was already removed in an error block
print(f"Notice: Could not remove temp input file {temp_input_path} in finally block (may already be removed): {e_rem}")
# --- Gradio Interface Definition (modified) ---
css = '''
.custom-gallery {
height: 500px !important;
width: 100%;
margin: 10px auto;
padding: 0px;
overflow-y: auto !important;
}
'''
with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
gr.Markdown("# Webtoon Lineart Cropper with Filtering by Head-or-Face Detection")
gr.Markdown("Upload a PNG lineart image of your webtoon and automatically crop the character's face or head included region. "
"This demo leverages some detectors to precisely detect and isolate characters. "
"The app will display cropped objects, provide a ZIP of cropped PNGs, "
"and a PSD file with transparent lineart and half-transparent yellow-filled box layers. "
"We provide two detectors to choose from, each with different filtering methods. ")
gr.Markdown("- **Detector 1**: Uses [`imgutils.detect`](https://github.com/deepghs/imgutils/tree/main/imgutils/detect) and VLM-based filtering with [`google/gemma-3-12b-it`](https://huggingface.co/google/gemma-3-12b-it)")
gr.Markdown("- **Detector 2**: Uses [`imgutils.detect`](https://github.com/deepghs/imgutils/tree/main/imgutils/detect) and tag-based filtering with [`SmilingWolf/wd-eva02-large-tagger-v3`](https://huggingface.co/SmilingWolf/wd-eva02-large-tagger-v3)")
gr.Markdown("**Note 1:** The app may take a few seconds to process the image, depending on the size and number of characters detected. The example image below is a lineart PNG file created synthetically from images on [Danbooru](https://danbooru.donmai.us/posts?page=1&tags=dragon_ball_z) after [lineart extraction](https://huggingface.co/spaces/carolineec/informativedrawings).")
gr.Markdown("**Note 2:** This demo is developed by [Kakao Entertainment](https://kakaoent.com/)'s AI Lab for research purposes, specifically designed to preprocess webtoon image data and is also not intended for production use. It is a research prototype and may not be suitable for all use cases. Please use it at your own risk.")
with gr.Row():
with gr.Column(scale=1):
# Input type remains 'filepath' to handle examples cleanly.
image_input = gr.Image(type="filepath", label="Upload Lineart PNG or Select Example", image_mode='RGBA', height=400)
detector_choice_radio = gr.Radio(
choices=["Detector 1", "Detector 2"],
label="Choose Detection Function",
value="Detector 1" # Default value
)
process_button = gr.Button("Process Uploaded/Modified Image", variant="primary")
status_output = gr.Textbox(label="Status", interactive=False, lines=8) # Increased lines slightly more
with gr.Column(scale=3):
gr.Markdown("### Cropped Objects")
# Setting height explicitly can sometimes help layout.
gallery_output = gr.Gallery(label="Detected Objects (Cropped)", elem_id="gallery_crops", columns=4, height=500, interactive=False, elem_classes="custom-gallery") # object_fit="contain")
with gr.Row():
zip_output = gr.File(label="Download Cropped Images (ZIP)")
psd_output = gr.File(label="Download PSD (Lineart + Boxes)")
# --- Add Examples ---
# IMPORTANT: Make sure 'sample_img.png' exists in the same directory
# as this script, or provide the correct relative/absolute path.
# Also ensure the image is a valid PNG.
example_image_path = "./sample_img/sample_danbooru_dragonball.png"
if os.path.exists(example_image_path):
gr.Examples(
examples=[
[example_image_path, "Detector 1"],
[example_image_path, "Detector 2"] # Add example for detector 2 as well
],
# Inputs that the examples populate
inputs=[image_input, detector_choice_radio],
# Outputs that are updated when an example is clicked AND run_on_click=True
outputs=[gallery_output, zip_output, psd_output, status_output],
# The function to call when an example is clicked
fn=process_lineart,
# Make clicking an example automatically run the function
run_on_click=True,
label="Click Example to Run Automatically", # Updated label
cache_examples=True, # Disable caching to ensure fresh processing
cache_mode="lazy",
)
else:
gr.Markdown(f"**(Note:** Could not find `{example_image_path}` for examples. Please create it or ensure it's in the correct directory.)")
# --- Button Click Handler (for manual uploads/changes) ---
process_button.click(
fn=process_lineart,
inputs=[image_input, detector_choice_radio],
outputs=[gallery_output, zip_output, psd_output, status_output]
)
# --- Launch the Gradio App ---
if __name__ == "__main__":
# Create a dummy sample image if it doesn't exist for testing
if not os.path.exists("./sample_img/sample_danbooru_dragonball.png"):
print("Creating a dummy 'sample_danbooru_dragonball.png' for demonstration.")
try:
img = Image.new('L', (300, 200), color=255) # White background (grayscale)
draw = ImageDraw.Draw(img)
# Draw some black lines/shapes
draw.line((30, 30, 270, 30), fill=0, width=2)
draw.rectangle((50, 50, 150, 150), outline=0, width=3)
draw.ellipse((180, 70, 250, 130), outline=0, width=3)
img.save("./sample_img/sample_danbooru_dragonball.png", "PNG")
print("Dummy 'sample_danbooru_dragonball.png' created.")
except Exception as e:
print(f"Warning: Failed to create dummy sample image: {e}")
demo.launch()
# ssr_mode=False |