| | |
| | """ |
| | Qwen3.5-0.8B MapTrace - Gradio Demo |
| | Predicts a traversable path between start (green) and end (red) locations on a map image. |
| | """ |
| |
|
| | import re |
| | import ast |
| | import math |
| | import torch |
| | import gradio as gr |
| | import numpy as np |
| | from PIL import Image, ImageDraw, ImageFont |
| | from transformers import AutoModelForImageTextToText, AutoProcessor |
| |
|
| | |
| | MODEL_ID = "TurkishCodeMan/Qwen3.5-0.8B-MapTrace-PartialFT" |
| |
|
| | |
| | print(f"Loading model: {MODEL_ID}") |
| | processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True) |
| | model = AutoModelForImageTextToText.from_pretrained( |
| | MODEL_ID, |
| | torch_dtype=torch.bfloat16, |
| | device_map="auto", |
| | trust_remote_code=True, |
| | ) |
| | model.eval() |
| | print("✅ Model loaded and ready!") |
| |
|
| |
|
| | |
| |
|
| | def parse_coordinates(text: str) -> list[tuple[float, float]]: |
| | """Model çıktısından koordinatları ayıklar.""" |
| | |
| | coords = re.findall(r"\((-?\d+\.?\d*),\s*(-?\d+\.?\d*)\)", text) |
| | if coords: |
| | return [(float(x), float(y)) for x, y in coords] |
| | return [] |
| |
|
| |
|
| | def draw_path_on_image( |
| | image: Image.Image, |
| | path_coords: list[tuple[float, float]], |
| | start_xy: tuple[float, float], |
| | end_xy: tuple[float, float], |
| | ) -> Image.Image: |
| | """ |
| | Harita görselinin üzerine tahmin edilen yolu çizer. |
| | - Yol: Kalın sarı çizgi + turuncu noktalar |
| | - Start: Parlak yeşil daire |
| | - End : Parlak kırmızı daire |
| | """ |
| | img = image.copy().convert("RGBA") |
| | overlay = Image.new("RGBA", img.size, (0, 0, 0, 0)) |
| | draw = ImageDraw.Draw(overlay) |
| |
|
| | W, H = img.size |
| |
|
| | def norm_to_px(x, y): |
| | return int(x * W), int(y * H) |
| |
|
| | |
| | if len(path_coords) >= 2: |
| | pixel_path = [norm_to_px(*p) for p in path_coords] |
| |
|
| | |
| | draw.line(pixel_path, fill=(0, 0, 0, 160), width=7) |
| | |
| | draw.line(pixel_path, fill=(255, 215, 0, 230), width=4) |
| |
|
| | |
| | for px, py in pixel_path[1:-1]: |
| | r = 5 |
| | draw.ellipse([(px - r, py - r), (px + r, py + r)], fill=(255, 140, 0, 255)) |
| |
|
| | |
| | sx, sy = norm_to_px(*start_xy) |
| | draw.ellipse([(sx - 10, sy - 10), (sx + 10, sy + 10)], fill=(0, 200, 80, 255), outline=(255, 255, 255, 255), width=2) |
| |
|
| | |
| | ex, ey = norm_to_px(*end_xy) |
| | draw.ellipse([(ex - 10, ey - 10), (ex + 10, ey + 10)], fill=(220, 40, 40, 255), outline=(255, 255, 255, 255), width=2) |
| |
|
| | |
| | result = Image.alpha_composite(img, overlay).convert("RGB") |
| | return result |
| |
|
| |
|
| | def run_inference(image: Image.Image, start_x: float, start_y: float, end_x: float, end_y: float): |
| | """ |
| | Model çalıştırır, koordinatları parse eder, görsele yolu çizer. |
| | """ |
| | if image is None: |
| | return None, "❌ Lütfen bir harita görseli yükleyin." |
| |
|
| | |
| | prompt_text = ( |
| | f"You are provided an image of a path with a start location denoted in green " |
| | f"and an end location denoted in red. \n" |
| | f"The normalized xy-coordinates of the start location are ({start_x}, {start_y}) " |
| | f"and of the end location ({end_x}, {end_y}). \n" |
| | f"Output a list of normalized coordinates in the form of a list [(x1,y1), (x2,y2)...] " |
| | f"of the path between the start and end location. \n" |
| | f"Ensure that the path follows the traversable locations of the map." |
| | ) |
| |
|
| | messages = [ |
| | { |
| | "role": "user", |
| | "content": [ |
| | {"type": "image"}, |
| | {"type": "text", "text": prompt_text}, |
| | ], |
| | } |
| | ] |
| |
|
| | text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) |
| |
|
| | inputs = processor( |
| | text=[text], |
| | images=[image], |
| | return_tensors="pt", |
| | padding=True, |
| | min_pixels=256 * 28 * 28, |
| | max_pixels=1024 * 768, |
| | ).to(model.device) |
| |
|
| | with torch.no_grad(): |
| | generated_ids = model.generate( |
| | **inputs, |
| | max_new_tokens=512, |
| | do_sample=False, |
| | temperature=0.0, |
| | ) |
| |
|
| | raw_output = processor.decode( |
| | generated_ids[0][inputs.input_ids.shape[1]:], |
| | skip_special_tokens=True |
| | ).strip() |
| |
|
| | |
| | path = parse_coordinates(raw_output) |
| |
|
| | if not path: |
| | return image, f"⚠️ Model geçerli koordinat üretemedi.\n\nHam çıktı:\n{raw_output}" |
| |
|
| | |
| | result_img = draw_path_on_image(image, path, (start_x, start_y), (end_x, end_y)) |
| |
|
| | |
| | path_str = " → ".join([f"({x:.4f}, {y:.4f})" for x, y in path]) |
| | info_text = ( |
| | f"✅ Tahmin edilen yol: **{len(path)} nokta**\n\n" |
| | f"`{path_str}`\n\n" |
| | f"---\n*Ham model çıktısı:*\n```\n{raw_output[:500]}\n```" |
| | ) |
| |
|
| | return result_img, info_text |
| |
|
| |
|
| | |
| |
|
| | DESCRIPTION = """ |
| | # 🗺️ MapTrace Path Planner |
| | **Model:** [Qwen3.5-0.8B-MapTrace-PartialFT](https://huggingface.co/TurkishCodeMan/Qwen3.5-0.8B-MapTrace-PartialFT) |
| | |
| | Upload a map image and enter normalized start/end coordinates. The model will predict a traversable path between the two locations and overlay it on the map. |
| | |
| | **Coordinates:** Normalized (x, y) values in [0, 1] range. Top-left = (0, 0), Bottom-right = (1, 1). |
| | """ |
| |
|
| | EXAMPLES = [ |
| | ["example_map.png", 0.77, 0.47, 0.54, 0.54], |
| | ] |
| |
|
| | with gr.Blocks( |
| | title="MapTrace Path Planner", |
| | theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="slate"), |
| | css=""" |
| | .gradio-container { max-width: 1100px !important; } |
| | .result-image img { border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); } |
| | footer { display: none !important; } |
| | """, |
| | ) as demo: |
| | gr.Markdown(DESCRIPTION) |
| |
|
| | with gr.Row(): |
| | |
| | with gr.Column(scale=1): |
| | gr.Markdown("### 📤 Input") |
| | input_image = gr.Image( |
| | label="Map Image", |
| | type="pil", |
| | sources=["upload"], |
| | height=350, |
| | ) |
| | with gr.Row(): |
| | start_x = gr.Number(label="Start X", value=0.77, minimum=0.0, maximum=1.0, step=0.01, precision=4) |
| | start_y = gr.Number(label="Start Y", value=0.47, minimum=0.0, maximum=1.0, step=0.01, precision=4) |
| | with gr.Row(): |
| | end_x = gr.Number(label="End X", value=0.54, minimum=0.0, maximum=1.0, step=0.01, precision=4) |
| | end_y = gr.Number(label="End Y", value=0.54, minimum=0.0, maximum=1.0, step=0.01, precision=4) |
| |
|
| | run_btn = gr.Button("🚀 Predict Path", variant="primary", size="lg") |
| |
|
| | |
| | with gr.Column(scale=1): |
| | gr.Markdown("### 📍 Result") |
| | output_image = gr.Image( |
| | label="Predicted Path", |
| | type="pil", |
| | interactive=False, |
| | height=350, |
| | elem_classes=["result-image"], |
| | ) |
| | output_info = gr.Markdown(label="Info", value="*Results will appear here after running the model.*") |
| |
|
| | |
| | with gr.Row(): |
| | gr.Markdown(""" |
| | --- |
| | 🟢 **Start** | 🔴 **End** | 🟡 **Predicted Path** |
| | """) |
| |
|
| | |
| | run_btn.click( |
| | fn=run_inference, |
| | inputs=[input_image, start_x, start_y, end_x, end_y], |
| | outputs=[output_image, output_info], |
| | api_name="predict_path", |
| | show_progress="full", |
| | ) |
| |
|
| | |
| | for component in [start_x, start_y, end_x, end_y]: |
| | component.submit( |
| | fn=run_inference, |
| | inputs=[input_image, start_x, start_y, end_x, end_y], |
| | outputs=[output_image, output_info], |
| | ) |
| |
|
| | if __name__ == "__main__": |
| | demo.launch(share=False) |
| |
|