Spaces:
Sleeping
Sleeping
| import os | |
| import sys | |
| import tempfile | |
| import cv2 | |
| import dotenv | |
| import gradio as gr | |
| # Import the image slider component | |
| try: | |
| from gradio_imageslider import ImageSlider | |
| except ImportError: | |
| raise ImportError("Please install the slider component by running: pip install gradio-imageslider") | |
| # Load environment variables (e.g. GROQ_API_KEY) | |
| dotenv.load_dotenv() | |
| # Add app.py directory and backend/ directory to system path so imports work in all layouts | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.append(current_dir) | |
| sys.path.append(os.path.join(current_dir, "backend")) | |
| # Fallback imports in case backend files aren't found in your environment | |
| try: | |
| from diff_ai import compare_images_ai | |
| from diff import compare_images | |
| from describe import describe_change | |
| except ImportError: | |
| print("Warning: Backend modules not found. Using mock functions.") | |
| def compare_images_ai(*args, **kwargs): return {"ssim_score": 0.95, "severity": "low"} | |
| def compare_images(*args, **kwargs): return {"ssim_score": 0.95, "severity": "low"} | |
| def describe_change(*args, **kwargs): return "Mock description of changes." | |
| def gradio_compare(img1, img2): | |
| if img1 is None or img2 is None: | |
| return None, None, None, None, "Please upload both images.", {} | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| path1 = os.path.join(tmpdir, "img1.jpg") | |
| path2 = os.path.join(tmpdir, "img2.jpg") | |
| output_path = os.path.join(tmpdir, "result.png") | |
| output_box_path = os.path.join(tmpdir, "result_box.png") | |
| heatmap_path = os.path.join(tmpdir, "heatmap.png") | |
| # Convert RGB inputs (Gradio) to BGR (OpenCV) | |
| img1_bgr = cv2.cvtColor(img1, cv2.COLOR_RGB2BGR) | |
| img2_bgr = cv2.cvtColor(img2, cv2.COLOR_RGB2BGR) | |
| # Save images | |
| cv2.imwrite(path1, img1_bgr) | |
| cv2.imwrite(path2, img2_bgr) | |
| # Run comparison pipeline | |
| pipeline_used = "AI Pipeline" | |
| try: | |
| result_data = compare_images_ai(path1, path2, output_path, heatmap_path, output_box_path=output_box_path) | |
| except Exception as e: | |
| print(f"[Gradio] AI pipeline failed ({e}); falling back to classical") | |
| result_data = compare_images(path1, path2, output_path, heatmap_path) | |
| if os.path.exists(output_path): | |
| import shutil | |
| shutil.copy(output_path, output_box_path) | |
| result_data["pipeline"] = "classical" | |
| pipeline_used = "Classical Pipeline" | |
| # Generate description | |
| try: | |
| description = describe_change(path1, path2, result_data) | |
| except Exception as e: | |
| description = f"Error generating description: {e}" | |
| # Load output images (convert BGR back to RGB for Gradio display) | |
| result_img = cv2.cvtColor(cv2.imread(output_path), cv2.COLOR_BGR2RGB) if os.path.exists(output_path) else None | |
| box_img = cv2.cvtColor(cv2.imread(output_box_path), cv2.COLOR_BGR2RGB) if os.path.exists(output_box_path) else None | |
| heatmap_img = cv2.cvtColor(cv2.imread(heatmap_path), cv2.COLOR_BGR2RGB) if os.path.exists(heatmap_path) else None | |
| # Format metrics dictionary | |
| metrics = { | |
| "Pipeline Used": pipeline_used, | |
| "SSIM Score (Similarity)": f"{result_data.get('ssim_score', 0):.4f}" if isinstance(result_data.get('ssim_score'), (int, float)) else "N/A", | |
| "LPIPS Distance (Perceptual)": f"{result_data.get('lpips_max', 0):.4f}" if isinstance(result_data.get('lpips_max'), (int, float)) else "N/A", | |
| "Detected Object Changes": result_data.get("object_changes", "0"), | |
| "Detected Crack Count": result_data.get("crack_count", "0"), | |
| "Change Severity": str(result_data.get("severity", "none")).upper(), | |
| "Alignment Failed": "Yes" if result_data.get("alignment_failed") else "No" | |
| } | |
| # Return slider tuple (img1, img2), plus the other images and texts | |
| return (img1, img2), result_img, box_img, heatmap_img, description, metrics | |
| # --- UI Theme and Layout Design --- | |
| # Switched to Base theme for a more minimal starting point | |
| theme = gr.themes.Base( | |
| primary_hue="indigo", | |
| secondary_hue="slate", | |
| neutral_hue="slate" | |
| ) | |
| css_styling = """ | |
| /* Container limit */ | |
| .gradio-container { max-width: 1600px !important; } | |
| /* Force Tailwind text-sm (14px, line-height 20px) on text elements */ | |
| .gradio-container, | |
| .gradio-container .prose *, | |
| .gradio-container button, | |
| .gradio-container span, | |
| .gradio-container label, | |
| .gradio-container input, | |
| .gradio-container textarea, | |
| .gradio-container p { | |
| font-size: 0.875rem !important; | |
| line-height: 1.25rem !important; | |
| } | |
| /* Ensure headings are also text-sm but bolder to maintain hierarchy */ | |
| .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4 { | |
| font-size: 0.875rem !important; | |
| font-weight: 600 !important; | |
| margin-bottom: 0.5rem !important; | |
| } | |
| /* Sidebar & Dark Panel styling */ | |
| .dark-panel { | |
| padding: 20px; | |
| border-radius: 12px; | |
| background: rgba(0, 0, 0, 0.2) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.1) !important; | |
| } | |
| /* Minimal Process Button */ | |
| .process-btn { | |
| margin-top: 20px; | |
| font-weight: 600 !important; | |
| border-radius: 8px !important; | |
| background: rgba(255, 255, 255, 0.1) !important; | |
| border: 1px solid rgba(255, 255, 255, 0.2) !important; | |
| } | |
| .process-btn:hover { | |
| background: rgba(255, 255, 255, 0.15) !important; | |
| } | |
| /* Remove default Gradio block borders for an ultra-minimal look */ | |
| .gr-block { border: none !important; } | |
| """ | |
| with gr.Blocks(theme=theme, css=css_styling) as demo: | |
| with gr.Row(): | |
| # --- LEFT COLUMN: Input Controls (Scale 1 for fixed-like width) --- | |
| with gr.Column(scale=1, min_width=280, elem_classes=["dark-panel"]): | |
| gr.Markdown("### Input Images") | |
| img1_input = gr.Image(label="BEFORE IMAGE", type="numpy", height=250) | |
| img2_input = gr.Image(label="AFTER IMAGE", type="numpy", height=250) | |
| submit_btn = gr.Button("Process Analysis", variant="primary", elem_classes=["process-btn"]) | |
| # --- RIGHT COLUMN: Unified Comparison Canvas (Scale 5 to consume all remaining space) --- | |
| with gr.Column(scale=5, elem_classes=["dark-panel"]): | |
| gr.Markdown("### Outputs") | |
| # Image Slider component | |
| slider_output = ImageSlider(label="Before / After", type="numpy", show_label=False) | |
| # Visuals Tabs | |
| with gr.Tabs(): | |
| with gr.TabItem("Segment Overlays"): | |
| result_img_output = gr.Image(label="Segment Overlay Highlight") | |
| with gr.TabItem("Bounding Boxes"): | |
| result_box_output = gr.Image(label="Bounding Box Detections") | |
| with gr.TabItem("Difference Heatmap"): | |
| heatmap_img_output = gr.Image(label="Difference Heatmap") | |
| # --- LOWER SECTION: AI Analysis & Metrics (Single Column) --- | |
| gr.Markdown("### AI Analysis & Metrics") | |
| desc_output = gr.Textbox(label="Explanation of Changes", lines=4) | |
| metrics_output = gr.JSON(label="Quantitative Comparison") | |
| # Connect the backend function | |
| submit_btn.click( | |
| fn=gradio_compare, | |
| inputs=[img1_input, img2_input], | |
| outputs=[ | |
| slider_output, | |
| result_img_output, | |
| result_box_output, | |
| heatmap_img_output, | |
| desc_output, | |
| metrics_output | |
| ] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |