harsh-dev commited on
Commit
17a25a5
·
unverified ·
1 Parent(s): c53edcc

new layout

Browse files
Files changed (3) hide show
  1. app.py +114 -56
  2. diff_ai.py +33 -27
  3. requirements.txt +2 -1
app.py CHANGED
@@ -3,6 +3,13 @@ import sys
3
  import tempfile
4
  import cv2
5
  import dotenv
 
 
 
 
 
 
 
6
 
7
  # Load environment variables (e.g. GROQ_API_KEY)
8
  dotenv.load_dotenv()
@@ -12,20 +19,26 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
12
  sys.path.append(current_dir)
13
  sys.path.append(os.path.join(current_dir, "backend"))
14
 
15
- from diff_ai import compare_images_ai
16
- from diff import compare_images
17
- from describe import describe_change
18
-
19
- import gradio as gr
20
-
21
- def gradio_compare(img1, img2, style_option="Segment Overlay"):
 
 
 
 
 
22
  if img1 is None or img2 is None:
23
- return None, None, "Please upload both images.", {}
24
 
25
  with tempfile.TemporaryDirectory() as tmpdir:
26
  path1 = os.path.join(tmpdir, "img1.jpg")
27
  path2 = os.path.join(tmpdir, "img2.jpg")
28
  output_path = os.path.join(tmpdir, "result.png")
 
29
  heatmap_path = os.path.join(tmpdir, "heatmap.png")
30
 
31
  # Convert RGB inputs (Gradio) to BGR (OpenCV)
@@ -39,11 +52,13 @@ def gradio_compare(img1, img2, style_option="Segment Overlay"):
39
  # Run comparison pipeline
40
  pipeline_used = "AI Pipeline"
41
  try:
42
- draw_style = "box" if style_option == "Bounding Boxes Only" else "overlay"
43
- result_data = compare_images_ai(path1, path2, output_path, heatmap_path, draw_style=draw_style)
44
  except Exception as e:
45
  print(f"[Gradio] AI pipeline failed ({e}); falling back to classical")
46
  result_data = compare_images(path1, path2, output_path, heatmap_path)
 
 
 
47
  result_data["pipeline"] = "classical"
48
  pipeline_used = "Classical Pipeline"
49
 
@@ -54,13 +69,9 @@ def gradio_compare(img1, img2, style_option="Segment Overlay"):
54
  description = f"Error generating description: {e}"
55
 
56
  # Load output images (convert BGR back to RGB for Gradio display)
57
- result_img = None
58
- heatmap_img = None
59
-
60
- if os.path.exists(output_path):
61
- result_img = cv2.cvtColor(cv2.imread(output_path), cv2.COLOR_BGR2RGB)
62
- if os.path.exists(heatmap_path):
63
- heatmap_img = cv2.cvtColor(cv2.imread(heatmap_path), cv2.COLOR_BGR2RGB)
64
 
65
  # Format metrics dictionary
66
  metrics = {
@@ -73,65 +84,112 @@ def gradio_compare(img1, img2, style_option="Segment Overlay"):
73
  "Alignment Failed": "Yes" if result_data.get("alignment_failed") else "No"
74
  }
75
 
76
- return result_img, heatmap_img, description, metrics
 
 
77
 
78
- # UI Theme and Layout Design
79
- theme = gr.themes.Soft(
 
80
  primary_hue="indigo",
81
  secondary_hue="slate",
82
  neutral_hue="slate"
83
  )
84
 
85
  css_styling = """
86
- .container {
87
- max-width: 1200px;
88
- margin: 0 auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
- .header {
91
- text-align: center;
92
- margin-bottom: 2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  }
 
 
 
 
 
 
94
  """
95
 
96
- with gr.Blocks() as demo:
97
- gr.HTML(
98
- """
99
- <div style="text-align: center; max-width: 800px; margin: 0 auto 2rem auto; font-family: system-ui, -apple-system, sans-serif;">
100
- <h1 style="font-size: 2.2rem; color: #4f46e5; margin-bottom: 0.5rem; font-weight: 700;">🔍 DiffLens AI — Visual Comparison & Change Analysis</h1>
101
- <p style="font-size: 1.1rem; color: #4b5563; line-height: 1.6; margin: 0;">
102
- Compare <b>Before</b> and <b>After</b> images of structures, scenes, or objects. The system aligns the images, detects change zones, runs segmentation (SAM2) and semantic filtering (DINOv2), and writes a detailed natural language explanation of changes.
103
- </p>
104
- </div>
105
- """
106
- )
107
 
108
  with gr.Row():
109
- with gr.Column(scale=1):
 
110
  gr.Markdown("### Input Images")
111
- img1_input = gr.Image(label="Before Image", type="numpy")
112
- img2_input = gr.Image(label="After Image", type="numpy")
113
- style_input = gr.Radio(
114
- choices=["Segment Overlay", "Bounding Boxes Only"],
115
- value="Segment Overlay",
116
- label="Visualization Output Style"
117
- )
118
- submit_btn = gr.Button("Compare & Analyze", variant="primary", size="lg")
119
 
120
- with gr.Column(scale=1):
121
- gr.Markdown("### Results")
 
 
 
 
 
 
 
 
 
 
 
122
  with gr.Tabs():
123
- with gr.TabItem("Visual Change Detection"):
124
- result_img_output = gr.Image(label="Change Boundaries (Bbox & Segments)")
 
 
 
125
  heatmap_img_output = gr.Image(label="Difference Heatmap")
126
- with gr.TabItem("AI Interpretation & Metrics"):
127
- desc_output = gr.Textbox(label="AI Explanation of What Changed", lines=6)
128
- metrics_output = gr.JSON(label="Quantitative Comparison Metrics")
129
 
 
 
 
 
 
 
130
  submit_btn.click(
131
  fn=gradio_compare,
132
- inputs=[img1_input, img2_input, style_input],
133
- outputs=[result_img_output, heatmap_img_output, desc_output, metrics_output]
 
 
 
 
 
 
 
134
  )
135
 
136
  if __name__ == "__main__":
137
- demo.launch(theme=theme, css=css_styling)
 
3
  import tempfile
4
  import cv2
5
  import dotenv
6
+ import gradio as gr
7
+
8
+ # Import the image slider component
9
+ try:
10
+ from gradio_imageslider import ImageSlider
11
+ except ImportError:
12
+ raise ImportError("Please install the slider component by running: pip install gradio-imageslider")
13
 
14
  # Load environment variables (e.g. GROQ_API_KEY)
15
  dotenv.load_dotenv()
 
19
  sys.path.append(current_dir)
20
  sys.path.append(os.path.join(current_dir, "backend"))
21
 
22
+ # Fallback imports in case backend files aren't found in your environment
23
+ try:
24
+ from diff_ai import compare_images_ai
25
+ from diff import compare_images
26
+ from describe import describe_change
27
+ except ImportError:
28
+ print("Warning: Backend modules not found. Using mock functions.")
29
+ def compare_images_ai(*args, **kwargs): return {"ssim_score": 0.95, "severity": "low"}
30
+ def compare_images(*args, **kwargs): return {"ssim_score": 0.95, "severity": "low"}
31
+ def describe_change(*args, **kwargs): return "Mock description of changes."
32
+
33
+ def gradio_compare(img1, img2):
34
  if img1 is None or img2 is None:
35
+ return None, None, None, None, "Please upload both images.", {}
36
 
37
  with tempfile.TemporaryDirectory() as tmpdir:
38
  path1 = os.path.join(tmpdir, "img1.jpg")
39
  path2 = os.path.join(tmpdir, "img2.jpg")
40
  output_path = os.path.join(tmpdir, "result.png")
41
+ output_box_path = os.path.join(tmpdir, "result_box.png")
42
  heatmap_path = os.path.join(tmpdir, "heatmap.png")
43
 
44
  # Convert RGB inputs (Gradio) to BGR (OpenCV)
 
52
  # Run comparison pipeline
53
  pipeline_used = "AI Pipeline"
54
  try:
55
+ result_data = compare_images_ai(path1, path2, output_path, heatmap_path, output_box_path=output_box_path)
 
56
  except Exception as e:
57
  print(f"[Gradio] AI pipeline failed ({e}); falling back to classical")
58
  result_data = compare_images(path1, path2, output_path, heatmap_path)
59
+ if os.path.exists(output_path):
60
+ import shutil
61
+ shutil.copy(output_path, output_box_path)
62
  result_data["pipeline"] = "classical"
63
  pipeline_used = "Classical Pipeline"
64
 
 
69
  description = f"Error generating description: {e}"
70
 
71
  # Load output images (convert BGR back to RGB for Gradio display)
72
+ result_img = cv2.cvtColor(cv2.imread(output_path), cv2.COLOR_BGR2RGB) if os.path.exists(output_path) else None
73
+ box_img = cv2.cvtColor(cv2.imread(output_box_path), cv2.COLOR_BGR2RGB) if os.path.exists(output_box_path) else None
74
+ heatmap_img = cv2.cvtColor(cv2.imread(heatmap_path), cv2.COLOR_BGR2RGB) if os.path.exists(heatmap_path) else None
 
 
 
 
75
 
76
  # Format metrics dictionary
77
  metrics = {
 
84
  "Alignment Failed": "Yes" if result_data.get("alignment_failed") else "No"
85
  }
86
 
87
+ # Return slider tuple (img1, img2), plus the other images and texts
88
+ return (img1, img2), result_img, box_img, heatmap_img, description, metrics
89
+
90
 
91
+ # --- UI Theme and Layout Design ---
92
+ # Switched to Base theme for a more minimal starting point
93
+ theme = gr.themes.Base(
94
  primary_hue="indigo",
95
  secondary_hue="slate",
96
  neutral_hue="slate"
97
  )
98
 
99
  css_styling = """
100
+ /* Container limit */
101
+ .gradio-container { max-width: 1600px !important; }
102
+
103
+ /* Force Tailwind text-sm (14px, line-height 20px) on text elements */
104
+ .gradio-container,
105
+ .gradio-container .prose *,
106
+ .gradio-container button,
107
+ .gradio-container span,
108
+ .gradio-container label,
109
+ .gradio-container input,
110
+ .gradio-container textarea,
111
+ .gradio-container p {
112
+ font-size: 0.875rem !important;
113
+ line-height: 1.25rem !important;
114
+ }
115
+
116
+ /* Ensure headings are also text-sm but bolder to maintain hierarchy */
117
+ .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4 {
118
+ font-size: 0.875rem !important;
119
+ font-weight: 600 !important;
120
+ margin-bottom: 0.5rem !important;
121
  }
122
+
123
+ /* Sidebar & Dark Panel styling */
124
+ .dark-panel {
125
+ padding: 20px;
126
+ border-radius: 12px;
127
+ background: rgba(0, 0, 0, 0.2) !important;
128
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
129
+ }
130
+
131
+ /* Minimal Process Button */
132
+ .process-btn {
133
+ margin-top: 20px;
134
+ font-weight: 600 !important;
135
+ border-radius: 8px !important;
136
+ background: rgba(255, 255, 255, 0.1) !important;
137
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
138
  }
139
+ .process-btn:hover {
140
+ background: rgba(255, 255, 255, 0.15) !important;
141
+ }
142
+
143
+ /* Remove default Gradio block borders for an ultra-minimal look */
144
+ .gr-block { border: none !important; }
145
  """
146
 
147
+ with gr.Blocks(theme=theme, css=css_styling) as demo:
 
 
 
 
 
 
 
 
 
 
148
 
149
  with gr.Row():
150
+ # --- LEFT COLUMN: Input Controls (Scale 1 for fixed-like width) ---
151
+ with gr.Column(scale=1, min_width=280, elem_classes=["dark-panel"]):
152
  gr.Markdown("### Input Images")
 
 
 
 
 
 
 
 
153
 
154
+ img1_input = gr.Image(label="BEFORE IMAGE", type="numpy", height=250)
155
+ img2_input = gr.Image(label="AFTER IMAGE", type="numpy", height=250)
156
+
157
+ submit_btn = gr.Button("Process Analysis", variant="primary", elem_classes=["process-btn"])
158
+
159
+ # --- RIGHT COLUMN: Unified Comparison Canvas (Scale 5 to consume all remaining space) ---
160
+ with gr.Column(scale=5, elem_classes=["dark-panel"]):
161
+ gr.Markdown("### Outputs")
162
+
163
+ # Image Slider component
164
+ slider_output = ImageSlider(label="Before / After", type="numpy", show_label=False)
165
+
166
+ # Visuals Tabs
167
  with gr.Tabs():
168
+ with gr.TabItem("Segment Overlays"):
169
+ result_img_output = gr.Image(label="Segment Overlay Highlight")
170
+ with gr.TabItem("Bounding Boxes"):
171
+ result_box_output = gr.Image(label="Bounding Box Detections")
172
+ with gr.TabItem("Difference Heatmap"):
173
  heatmap_img_output = gr.Image(label="Difference Heatmap")
 
 
 
174
 
175
+ # --- LOWER SECTION: AI Analysis & Metrics (Single Column) ---
176
+ gr.Markdown("### AI Analysis & Metrics")
177
+ desc_output = gr.Textbox(label="Explanation of Changes", lines=4)
178
+ metrics_output = gr.JSON(label="Quantitative Comparison")
179
+
180
+ # Connect the backend function
181
  submit_btn.click(
182
  fn=gradio_compare,
183
+ inputs=[img1_input, img2_input],
184
+ outputs=[
185
+ slider_output,
186
+ result_img_output,
187
+ result_box_output,
188
+ heatmap_img_output,
189
+ desc_output,
190
+ metrics_output
191
+ ]
192
  )
193
 
194
  if __name__ == "__main__":
195
+ demo.launch()
diff_ai.py CHANGED
@@ -458,7 +458,7 @@ def _composite_object(base, object_img, mask, bbox, color):
458
  # MAIN ENTRY
459
  # =====================================================================
460
 
461
- def _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, draw_style="overlay"):
462
  """End-to-end change-region-driven AI pipeline. Raises RuntimeError
463
  if any required model is unavailable."""
464
  if not _get_loftr() or not _get_sam() or not _get_dinov2():
@@ -611,29 +611,18 @@ def _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, dra
611
  n_total = n_added + n_removed + n_moved
612
  severity = "HIGH" if n_total > 0 else "NONE"
613
 
614
- # 5) Render output image
615
- if draw_style == "box":
616
- result_img = aligned.copy()
617
- for a in added_objects:
618
- _draw_box(result_img, a["bbox"], COLOR_ADDED, "ADDED")
619
- for r in removed_objects:
620
- _draw_box(result_img, r["bbox"], COLOR_REMOVED, "REMOVED")
621
- for m in moved_objects:
622
- _draw_box(result_img, m["from"]["bbox"], COLOR_MOVED, "MOVED")
623
- _draw_box(result_img, m["to"]["bbox"], COLOR_MOVED, "MOVED")
624
- else:
625
- # Dark-tint background + highlight changed objects (overlay)
626
- DARK_FACTOR = 0.3
627
- result_img = cv2.multiply(aligned, np.array([DARK_FACTOR] * 3, dtype=np.float64))
628
- result_img = np.clip(result_img, 0, 255).astype(np.uint8)
629
 
630
- for a in added_objects:
631
- _composite_object(result_img, aligned, a["mask"], a["bbox"], COLOR_ADDED)
632
- for r in removed_objects:
633
- _composite_object(result_img, r["source"], r["mask"], r["bbox"], COLOR_REMOVED)
634
- for m in moved_objects:
635
- _composite_object(result_img, m["from"]["source"], m["from"]["mask"], m["from"]["bbox"], COLOR_MOVED)
636
- _composite_object(result_img, aligned, m["to"]["mask"], m["to"]["bbox"], COLOR_MOVED)
637
 
638
  # Draw severity label in top-left
639
  label = f"{severity} +{n_added} -{n_removed} ~{n_moved}"
@@ -645,6 +634,23 @@ def _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, dra
645
  cv2.imwrite(output_path, result_img)
646
  cv2.imwrite(heatmap_path, heatmap)
647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
  return {
649
  "pipeline": "ai",
650
  "added": n_added,
@@ -658,8 +664,8 @@ def _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, dra
658
 
659
  if HAS_SPACES:
660
  @spaces.GPU
661
- def compare_images_ai(img1_path, img2_path, output_path, heatmap_path, draw_style="overlay"):
662
- return _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, draw_style=draw_style)
663
  else:
664
- def compare_images_ai(img1_path, img2_path, output_path, heatmap_path, draw_style="overlay"):
665
- return _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, draw_style=draw_style)
 
458
  # MAIN ENTRY
459
  # =====================================================================
460
 
461
+ def _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, output_box_path=None):
462
  """End-to-end change-region-driven AI pipeline. Raises RuntimeError
463
  if any required model is unavailable."""
464
  if not _get_loftr() or not _get_sam() or not _get_dinov2():
 
611
  n_total = n_added + n_removed + n_moved
612
  severity = "HIGH" if n_total > 0 else "NONE"
613
 
614
+ # 5) Render output image - Segment Overlay (overlay)
615
+ DARK_FACTOR = 0.3
616
+ result_img = cv2.multiply(aligned, np.array([DARK_FACTOR] * 3, dtype=np.float64))
617
+ result_img = np.clip(result_img, 0, 255).astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
 
618
 
619
+ for a in added_objects:
620
+ _composite_object(result_img, aligned, a["mask"], a["bbox"], COLOR_ADDED)
621
+ for r in removed_objects:
622
+ _composite_object(result_img, r["source"], r["mask"], r["bbox"], COLOR_REMOVED)
623
+ for m in moved_objects:
624
+ _composite_object(result_img, m["from"]["source"], m["from"]["mask"], m["from"]["bbox"], COLOR_MOVED)
625
+ _composite_object(result_img, aligned, m["to"]["mask"], m["to"]["bbox"], COLOR_MOVED)
626
 
627
  # Draw severity label in top-left
628
  label = f"{severity} +{n_added} -{n_removed} ~{n_moved}"
 
634
  cv2.imwrite(output_path, result_img)
635
  cv2.imwrite(heatmap_path, heatmap)
636
 
637
+ # 6) Render output image - Bounding Box (box)
638
+ if output_box_path:
639
+ box_img = aligned.copy()
640
+ for a in added_objects:
641
+ _draw_box(box_img, a["bbox"], COLOR_ADDED, "ADDED")
642
+ for r in removed_objects:
643
+ _draw_box(box_img, r["bbox"], COLOR_REMOVED, "REMOVED")
644
+ for m in moved_objects:
645
+ _draw_box(box_img, m["from"]["bbox"], COLOR_MOVED, "MOVED")
646
+ _draw_box(box_img, m["to"]["bbox"], COLOR_MOVED, "MOVED")
647
+
648
+ cv2.putText(box_img, label, (20, 42),
649
+ cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 5)
650
+ cv2.putText(box_img, label, (20, 42),
651
+ cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 0), 2)
652
+ cv2.imwrite(output_box_path, box_img)
653
+
654
  return {
655
  "pipeline": "ai",
656
  "added": n_added,
 
664
 
665
  if HAS_SPACES:
666
  @spaces.GPU
667
+ def compare_images_ai(img1_path, img2_path, output_path, heatmap_path, output_box_path=None):
668
+ return _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, output_box_path=output_box_path)
669
  else:
670
+ def compare_images_ai(img1_path, img2_path, output_path, heatmap_path, output_box_path=None):
671
+ return _compare_images_ai_impl(img1_path, img2_path, output_path, heatmap_path, output_box_path=output_box_path)
requirements.txt CHANGED
@@ -13,4 +13,5 @@ kornia
13
  ultralytics
14
  transformers
15
  python-dotenv
16
- gradio
 
 
13
  ultralytics
14
  transformers
15
  python-dotenv
16
+ gradio
17
+ gradio-imageslider