|
|
import gradio as gr |
|
|
import numpy as np |
|
|
from PIL import Image |
|
|
import cv2 |
|
|
import time |
|
|
|
|
|
|
|
|
def timing_decorator(func): |
|
|
def wrapper(*args, **kwargs): |
|
|
start_time = time.time() |
|
|
result = func(*args, **kwargs) |
|
|
end_time = time.time() |
|
|
print(f"⏱️ {func.__name__} استغرقت: {end_time - start_time:.2f} ثانية") |
|
|
return result |
|
|
return wrapper |
|
|
|
|
|
@timing_decorator |
|
|
def get_mask_bbox(mask: Image.Image): |
|
|
"""نسخة محسنة من الدالة""" |
|
|
try: |
|
|
mask_np = np.array(mask) |
|
|
if len(mask_np.shape) == 3: |
|
|
|
|
|
mask_np = mask_np[..., -1] if mask_np.shape[2] == 4 else mask_np[..., 0] |
|
|
|
|
|
|
|
|
nonzero_indices = np.where(mask_np > 0) |
|
|
if len(nonzero_indices[0]) == 0: |
|
|
return None |
|
|
|
|
|
y_min, y_max = np.min(nonzero_indices[0]), np.max(nonzero_indices[0]) |
|
|
x_min, x_max = np.min(nonzero_indices[1]), np.max(nonzero_indices[1]) |
|
|
|
|
|
return int(x_min), int(y_min), int(x_max), int(y_max) |
|
|
except Exception as e: |
|
|
print(f"خطأ في get_mask_bbox: {e}") |
|
|
return None |
|
|
|
|
|
def mask_to_alpha(mask: Image.Image, smooth_edges=True, morph_level=0): |
|
|
|
|
|
if mask.mode == 'RGBA': |
|
|
mask_np = np.array(mask)[..., -1] |
|
|
else: |
|
|
mask_np = np.array(mask.convert("L")) |
|
|
|
|
|
if not smooth_edges: |
|
|
mask_np = np.where(mask_np > 127, 255, 0).astype(np.uint8) |
|
|
|
|
|
if morph_level > 0: |
|
|
mask_np = cv2.GaussianBlur(mask_np, (0, 0), sigmaX=morph_level, sigmaY=morph_level, borderType=cv2.BORDER_DEFAULT) |
|
|
mask_np = np.clip(mask_np, 0, 255).astype(np.uint8) |
|
|
|
|
|
return Image.fromarray(mask_np) |
|
|
|
|
|
@timing_decorator |
|
|
def crop_masked_object(img: Image.Image, mask: Image.Image, smooth_edges=True, morph_level=0, scale=1.0, rotation=0, transparency=1.0): |
|
|
bbox = get_mask_bbox(mask) |
|
|
if bbox is None: |
|
|
return None, None |
|
|
xmin, ymin, xmax, ymax = bbox |
|
|
|
|
|
|
|
|
cropped_img = img.crop((xmin, ymin, xmax + 1, ymax + 1)).convert("RGBA") |
|
|
cropped_mask = mask.crop((xmin, ymin, xmax + 1, ymax + 1)) |
|
|
|
|
|
|
|
|
alpha_mask = mask_to_alpha(cropped_mask, smooth_edges, morph_level) |
|
|
cropped_img.putalpha(alpha_mask) |
|
|
|
|
|
if scale != 1.0: |
|
|
new_size = (int(cropped_img.size[0] * scale), int(cropped_img.size[1] * scale)) |
|
|
cropped_img = cropped_img.resize(new_size, Image.LANCZOS) |
|
|
|
|
|
if rotation != 0: |
|
|
cropped_img = cropped_img.rotate(rotation, expand=True) |
|
|
|
|
|
if transparency < 1.0: |
|
|
alpha = cropped_img.split()[-1].point(lambda p: int(p * transparency)) |
|
|
cropped_img.putalpha(alpha) |
|
|
|
|
|
return cropped_img, bbox |
|
|
|
|
|
def paste_object_on_mask(base_img, base_mask, obj_img, obj_bbox, fit_mode="size"): |
|
|
base_bbox = get_mask_bbox(base_mask) |
|
|
if base_bbox is None or obj_bbox is None: |
|
|
return None |
|
|
bxmin, bymin, bxmax, bymax = base_bbox |
|
|
bw, bh = bxmax - bxmin + 1, bymax - bymin + 1 |
|
|
ow, oh = obj_img.size |
|
|
|
|
|
if fit_mode == "size": |
|
|
|
|
|
scale_factor = min(bw / ow, bh / oh) |
|
|
new_ow, new_oh = int(ow * scale_factor), int(oh * scale_factor) |
|
|
obj_img = obj_img.resize((new_ow, new_oh), Image.LANCZOS) |
|
|
|
|
|
x_paste = bxmin + (bw - new_ow) // 2 |
|
|
y_paste = bymin + (bh - new_oh) // 2 |
|
|
else: |
|
|
|
|
|
obj_img = obj_img.resize((bw, bh), Image.LANCZOS) |
|
|
x_paste = bxmin |
|
|
y_paste = bymin |
|
|
|
|
|
|
|
|
result = base_img.copy().convert("RGBA") |
|
|
result.paste(obj_img, (x_paste, y_paste), obj_img) |
|
|
return result |
|
|
|
|
|
def extract_mask_from_editor(editor_data): |
|
|
"""استخراج القناع من بيانات ImageMask - نسخة محسنة""" |
|
|
if editor_data is None: |
|
|
return None |
|
|
|
|
|
|
|
|
if isinstance(editor_data, Image.Image): |
|
|
return editor_data.convert("L") |
|
|
|
|
|
|
|
|
if isinstance(editor_data, dict): |
|
|
|
|
|
if "layers" in editor_data and len(editor_data["layers"]) > 0: |
|
|
layer = editor_data["layers"][0] |
|
|
if isinstance(layer, Image.Image): |
|
|
|
|
|
if layer.mode == 'RGBA': |
|
|
|
|
|
return layer.split()[-1] |
|
|
else: |
|
|
return layer.convert("L") |
|
|
|
|
|
|
|
|
elif "background" in editor_data: |
|
|
bg = editor_data["background"] |
|
|
if isinstance(bg, Image.Image): |
|
|
return Image.new("L", bg.size, 0) |
|
|
|
|
|
return None |
|
|
|
|
|
@timing_decorator |
|
|
def process_transfer(image1_editor, mask1_upload, image2_editor, mask2_upload, |
|
|
scale, transparency, rotation, smooth_edges, morph_level, fit_mode): |
|
|
|
|
|
if image1_editor is None or image2_editor is None: |
|
|
return None, None, None, "**❌ يرجى رفع الصورتين وتحديد الأقنعة." |
|
|
|
|
|
|
|
|
if isinstance(image1_editor, dict) and "background" in image1_editor: |
|
|
base_img = image1_editor["background"].convert("RGBA") |
|
|
else: |
|
|
base_img = image1_editor.convert("RGBA") if isinstance(image1_editor, Image.Image) else None |
|
|
|
|
|
|
|
|
if isinstance(image2_editor, dict) and "background" in image2_editor: |
|
|
obj_img = image2_editor["background"].convert("RGBA") |
|
|
else: |
|
|
obj_img = image2_editor.convert("RGBA") if isinstance(image2_editor, Image.Image) else None |
|
|
|
|
|
if base_img is None or obj_img is None: |
|
|
return None, None, None, "**❌ يرجى رفع الصورتين." |
|
|
|
|
|
|
|
|
base_mask = extract_mask_from_editor(mask1_upload) if mask1_upload is not None else extract_mask_from_editor(image1_editor) |
|
|
|
|
|
|
|
|
obj_mask = extract_mask_from_editor(mask2_upload) if mask2_upload is not None else extract_mask_from_editor(image2_editor) |
|
|
|
|
|
if base_mask is None or obj_mask is None: |
|
|
return None, None, None, "**❌ يجب رسم أو رفع قناع على كلتا الصورتين." |
|
|
|
|
|
|
|
|
base_bbox = get_mask_bbox(base_mask) |
|
|
obj_bbox = get_mask_bbox(obj_mask) |
|
|
|
|
|
if base_bbox is None: |
|
|
return None, None, None, "**❌ لم يتم تحديد أي منطقة في الصورة الأولى. يرجى الرسم أو رفع قناع." |
|
|
|
|
|
if obj_bbox is None: |
|
|
return None, None, None, "**❌ لم يتم تحديد أي كائن في الصورة الثانية. يرجى الرسم أو رفع قناع." |
|
|
|
|
|
|
|
|
cropped_obj, obj_bbox = crop_masked_object(obj_img, obj_mask, smooth_edges, morph_level, scale, rotation, transparency) |
|
|
if cropped_obj is None: |
|
|
return None, None, None, "**❌ لم يتم تحديد أي كائن في الصورة الثانية." |
|
|
|
|
|
|
|
|
result_img = paste_object_on_mask(base_img, base_mask, cropped_obj, obj_bbox, fit_mode) |
|
|
if result_img is None: |
|
|
return None, None, None, "**❌ لم يتم تحديد منطقة مناسبة للصق في الصورة الأولى." |
|
|
|
|
|
|
|
|
if result_img.size != base_img.size: |
|
|
result_img = result_img.resize(base_img.size, Image.LANCZOS) |
|
|
|
|
|
return base_mask, obj_mask, result_img, "**✅ تم نقل الكائن بنجاح!" |
|
|
|
|
|
def create_interface(): |
|
|
css = """ |
|
|
.silver-border { |
|
|
border: 3px solid #666666 !important; |
|
|
border-radius: 12px !important; |
|
|
box-shadow: 0 0 8px #b0b0b0; |
|
|
background: #f8f8f8; |
|
|
} |
|
|
.transfer-btn { |
|
|
background: linear-gradient(145deg, #FF8C00, #FF6B00) !important; |
|
|
color: white !important; |
|
|
border: none !important; |
|
|
padding: 12px 24px !important; |
|
|
font-size: 16px !important; |
|
|
font-weight: bold !important; |
|
|
border-radius: 8px !important; |
|
|
transition: all 0.2s ease !important; |
|
|
} |
|
|
.transfer-btn:hover { |
|
|
background: linear-gradient(145deg, #FF6B00, #E55D00) !important; |
|
|
transform: translateY(-2px) !important; |
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; |
|
|
} |
|
|
.transfer-btn:active { |
|
|
background: linear-gradient(145deg, #E55D00, #CC5500) !important; |
|
|
transform: translateY(2px) !important; |
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important; |
|
|
} |
|
|
""" |
|
|
|
|
|
with gr.Blocks(title="نقل كائن من صورة إلى أخرى مع القناع والمحاذاة", css=css) as demo: |
|
|
gr.Markdown(""" |
|
|
# 🖼️ نقل كائن من صورة إلى أخرى مع القناع والمحاذاة |
|
|
يمكنك تعديل إعدادات نقل الكائن من هنا، أو تركها على الإعدادات الافتراضية. |
|
|
|
|
|
**التعليمات:** |
|
|
1. ارفع الصورة الأولى (صورة الخلفية) وارسم أو ارفع قناع على مكان اللصق. |
|
|
2. ارفع الصورة الثانية (الصورة التي بها الكائن) وحدد عليها بالقلم. |
|
|
3. أو ارفع قناع جاهز للكائن في الصورة الثانية (يجب أن يكون القناع أبيض على أسود). |
|
|
4. اضغط زر "نقل الكائن" وستظهر النتيجة! |
|
|
|
|
|
**ملاحظة:** سيتم تكبير أو تصغير الكائن تلقائيًا ليتناسب مع حجم القناع في الصورة الأولى. |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(): |
|
|
image1_editor = gr.ImageMask( |
|
|
label="الصورة الأولى (اختر مكان اللصق بالقلم)", |
|
|
type="pil", |
|
|
height=400, |
|
|
elem_classes=["silver-border"] |
|
|
) |
|
|
mask1_upload = gr.Image( |
|
|
label="أو رفع قناع جاهز للصورة الأولى (اختياري)", |
|
|
type="pil", |
|
|
height=200, |
|
|
elem_classes=["silver-border"] |
|
|
) |
|
|
with gr.Column(): |
|
|
image2_editor = gr.ImageMask( |
|
|
label="الصورة الثانية (ارفع الصورة وحدد عليها بالقلم)", |
|
|
type="pil", |
|
|
height=400, |
|
|
elem_classes=["silver-border"] |
|
|
) |
|
|
mask2_upload = gr.Image( |
|
|
label="أو رفع قناع جاهز للصورة الثانية (بدون تحديد بالقلم)", |
|
|
type="pil", |
|
|
height=200, |
|
|
elem_classes=["silver-border"] |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
scale = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="حجم الكائن (Scale) - قبل التكيف مع القناع", elem_classes=["silver-border"]) |
|
|
transparency = gr.Slider(0.2, 1.0, value=1.0, step=0.05, label="شفافية الكائن (Transparency)", elem_classes=["silver-border"]) |
|
|
rotation = gr.Slider(-180, 180, value=0, step=1, label="تدوير الكائن (Rotation)", elem_classes=["silver-border"]) |
|
|
|
|
|
with gr.Row(): |
|
|
smooth_edges = gr.Checkbox(value=True, label="تفعيل الحواف الناعمة (Smooth Edges/Anti-aliasing)", elem_classes=["silver-border"]) |
|
|
morph_level = gr.Slider(0, 10, value=0, step=1, label="مستوى تنعيم القناع (Morphological Smoothing)", elem_classes=["silver-border"]) |
|
|
fit_mode = gr.Radio( |
|
|
choices=["size", "shape"], |
|
|
value="size", |
|
|
label="وضع المحاذاة", |
|
|
info="size: يحافظ على التناسب | shape: يملأ المساحة (قد يشوه الصورة)", |
|
|
elem_classes=["silver-border"] |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
transfer_btn = gr.Button("🚀 نقل الكائن", elem_classes=["transfer-btn"]) |
|
|
|
|
|
with gr.Row(): |
|
|
base_mask = gr.Image(label="قناع الصورة الأولى المستخدم", type="pil", height=200, elem_classes=["silver-border"]) |
|
|
obj_mask = gr.Image(label="قناع الصورة الثانية المستخدم", type="pil", height=200, elem_classes=["silver-border"]) |
|
|
result_img = gr.Image(label="النتيجة النهائية (بنفس حجم الصورة الأولى)", type="pil", height=400, elem_classes=["silver-border"]) |
|
|
|
|
|
status = gr.Markdown() |
|
|
|
|
|
transfer_btn.click( |
|
|
fn=process_transfer, |
|
|
inputs=[image1_editor, mask1_upload, image2_editor, mask2_upload, scale, transparency, rotation, smooth_edges, morph_level, fit_mode], |
|
|
outputs=[base_mask, obj_mask, result_img, status] |
|
|
) |
|
|
|
|
|
return demo |
|
|
|
|
|
def main(): |
|
|
demo = create_interface() |
|
|
demo.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=True, |
|
|
max_file_size="100MB" |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |