mlbench123 commited on
Commit
e2028e7
Β·
verified Β·
1 Parent(s): 8378254

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +718 -0
app.py CHANGED
@@ -0,0 +1,718 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Complete Industrial Warehouse Image Stitching Application
3
+ Ready for Hugging Face Space Deployment with ZIP Upload
4
+
5
+ Author: Your Name
6
+ Date: November 2024
7
+ License: MIT
8
+ """
9
+
10
+ import gradio as gr
11
+ import cv2
12
+ import numpy as np
13
+ from PIL import Image
14
+ import io
15
+ import json
16
+ from datetime import datetime
17
+ from typing import List, Tuple, Optional
18
+ import tempfile
19
+ import zipfile
20
+ from pathlib import Path
21
+
22
+ class WarehouseStitcher:
23
+ """Production-ready warehouse image stitching pipeline"""
24
+
25
+ def __init__(self):
26
+ self.version = "1.0.0"
27
+ self.config = {
28
+ 'feature_extractor': 'SIFT',
29
+ 'matcher': 'BF',
30
+ 'use_clahe': True,
31
+ 'detect_rack_labels': True,
32
+ 'ransac_threshold': 5.0,
33
+ 'min_match_count': 10,
34
+ }
35
+
36
+ def preprocess_image(self, img: np.ndarray) -> np.ndarray:
37
+ """Apply CLAHE and preprocessing"""
38
+ if len(img.shape) == 3:
39
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
40
+ else:
41
+ gray = img
42
+
43
+ if self.config['use_clahe']:
44
+ clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
45
+ gray = clahe.apply(gray)
46
+
47
+ return gray
48
+
49
+ def detect_rack_labels(self, img: np.ndarray) -> List[dict]:
50
+ """Detect warehouse rack labels"""
51
+ labels = []
52
+ gray = self.preprocess_image(img)
53
+
54
+ edges = cv2.Canny(gray, 50, 150)
55
+ contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
56
+
57
+ for contour in contours:
58
+ area = cv2.contourArea(contour)
59
+ if 500 < area < 50000:
60
+ x, y, w, h = cv2.boundingRect(contour)
61
+ aspect_ratio = w / float(h)
62
+ if 2.0 < aspect_ratio < 8.0:
63
+ labels.append({
64
+ 'bbox': (x, y, w, h),
65
+ 'area': area,
66
+ 'center': (x + w//2, y + h//2)
67
+ })
68
+
69
+ return labels
70
+
71
+ def extract_features(self, img: np.ndarray) -> Tuple:
72
+ """Extract features with selected method"""
73
+ gray = self.preprocess_image(img)
74
+
75
+ if self.config['feature_extractor'] == 'SIFT':
76
+ detector = cv2.SIFT_create(nfeatures=2000, contrastThreshold=0.03, edgeThreshold=10)
77
+ elif self.config['feature_extractor'] == 'ORB':
78
+ detector = cv2.ORB_create(nfeatures=2000)
79
+ else:
80
+ detector = cv2.AKAZE_create()
81
+
82
+ keypoints, descriptors = detector.detectAndCompute(gray, None)
83
+
84
+ # Add rack label keypoints
85
+ if self.config['detect_rack_labels']:
86
+ labels = self.detect_rack_labels(gray)
87
+ for label in labels:
88
+ cx, cy = label['center']
89
+ keypoints.append(cv2.KeyPoint(float(cx), float(cy), 10))
90
+
91
+ return keypoints, descriptors, gray
92
+
93
+ def match_features(self, desc1: np.ndarray, desc2: np.ndarray) -> List:
94
+ """Match features with Lowe's ratio test"""
95
+ if desc1 is None or desc2 is None:
96
+ return []
97
+
98
+ if self.config['feature_extractor'] == 'ORB':
99
+ matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
100
+ else:
101
+ matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=False)
102
+
103
+ matches = matcher.knnMatch(desc1, desc2, k=2)
104
+
105
+ good_matches = []
106
+ for match_pair in matches:
107
+ if len(match_pair) == 2:
108
+ m, n = match_pair
109
+ if m.distance < 0.75 * n.distance:
110
+ good_matches.append(m)
111
+
112
+ return good_matches
113
+
114
+ def estimate_homography(self, kp1, kp2, matches):
115
+ """Estimate homography with RANSAC"""
116
+ if len(matches) < self.config['min_match_count']:
117
+ return None, None, 0.0
118
+
119
+ src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
120
+ dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
121
+
122
+ H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC,
123
+ self.config['ransac_threshold'])
124
+
125
+ if H is None:
126
+ return None, None, 0.0
127
+
128
+ inliers = np.sum(mask)
129
+ confidence = inliers / len(matches)
130
+
131
+ return H, mask, confidence
132
+
133
+ def blend_images(self, img1: np.ndarray, img2: np.ndarray, H: np.ndarray) -> np.ndarray:
134
+ """Blend images using homography"""
135
+ h1, w1 = img1.shape[:2]
136
+ h2, w2 = img2.shape[:2]
137
+
138
+ pts2 = np.float32([[0, 0], [0, h2], [w2, h2], [w2, 0]]).reshape(-1, 1, 2)
139
+ pts2_transformed = cv2.perspectiveTransform(pts2, H)
140
+
141
+ pts = np.concatenate((pts2_transformed,
142
+ np.float32([[0, 0], [0, h1], [w1, h1], [w1, 0]]).reshape(-1, 1, 2)),
143
+ axis=0)
144
+
145
+ [xmin, ymin] = np.int32(pts.min(axis=0).ravel() - 0.5)
146
+ [xmax, ymax] = np.int32(pts.max(axis=0).ravel() + 0.5)
147
+
148
+ t = [-xmin, -ymin]
149
+ Ht = np.array([[1, 0, t[0]], [0, 1, t[1]], [0, 0, 1]])
150
+
151
+ result = cv2.warpPerspective(img2, Ht.dot(H), (xmax - xmin, ymax - ymin))
152
+ result[t[1]:h1 + t[1], t[0]:w1 + t[0]] = img1
153
+
154
+ return result
155
+
156
+ def stitch_images(self, images: List, progress=gr.Progress()) -> Tuple:
157
+ """Main stitching pipeline with progress tracking"""
158
+ if not images or len(images) < 2:
159
+ return None, "❌ Error: Please upload at least 2 images", None
160
+
161
+ logs = []
162
+ logs.append("=" * 70)
163
+ logs.append("🏭 INDUSTRIAL WAREHOUSE IMAGE STITCHING PIPELINE")
164
+ logs.append("=" * 70)
165
+ logs.append(f"πŸ“… Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
166
+ logs.append(f"πŸ“Έ Images to process: {len(images)}")
167
+ logs.append("")
168
+
169
+ # Convert images
170
+ cv_images = []
171
+ for i, img in enumerate(images):
172
+ if isinstance(img, str):
173
+ img = cv2.imread(img)
174
+ elif isinstance(img, Image.Image):
175
+ img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
176
+ cv_images.append(img)
177
+ logs.append(f"βœ“ Image {i+1}: {img.shape[1]}x{img.shape[0]} pixels")
178
+
179
+ logs.append("")
180
+
181
+ # Start stitching
182
+ result = cv_images[0]
183
+ total_matches = 0
184
+ total_inliers = 0
185
+
186
+ for i in range(1, len(cv_images)):
187
+ progress((i / len(cv_images), f"Processing image {i+1}/{len(cv_images)}"))
188
+
189
+ logs.append("-" * 70)
190
+ logs.append(f"πŸ”„ PROCESSING IMAGE {i+1}/{len(cv_images)}")
191
+ logs.append("-" * 70)
192
+
193
+ # Extract features
194
+ kp1, desc1, _ = self.extract_features(result)
195
+ kp2, desc2, _ = self.extract_features(cv_images[i])
196
+ logs.append(f"πŸ” Features: {len(kp1)} ↔ {len(kp2)}")
197
+
198
+ # Match features
199
+ matches = self.match_features(desc1, desc2)
200
+ total_matches += len(matches)
201
+ logs.append(f"πŸ”— Matches: {len(matches)} good matches")
202
+
203
+ if len(matches) < self.config['min_match_count']:
204
+ logs.append(f"⚠️ WARNING: Only {len(matches)} matches")
205
+ logs.append(f"⏭️ Skipping image {i+1}")
206
+ continue
207
+
208
+ # Estimate homography
209
+ H, mask, confidence = self.estimate_homography(kp1, kp2, matches)
210
+
211
+ if H is None:
212
+ logs.append(f"❌ ERROR: Failed to compute homography")
213
+ continue
214
+
215
+ inliers = int(np.sum(mask))
216
+ total_inliers += inliers
217
+ logs.append(f"πŸ“ Homography: {inliers}/{len(matches)} inliers ({confidence:.1%})")
218
+
219
+ # Blend
220
+ result = self.blend_images(result, cv_images[i], H)
221
+ logs.append(f"βœ… Success! New size: {result.shape[1]}x{result.shape[0]}")
222
+ logs.append("")
223
+
224
+ # Final summary
225
+ logs.append("=" * 70)
226
+ logs.append("πŸ“Š FINAL STATISTICS")
227
+ logs.append("=" * 70)
228
+ logs.append(f"βœ“ Final Resolution: {result.shape[1]} x {result.shape[0]} pixels")
229
+ logs.append(f"βœ“ Total Matches: {total_matches:,}")
230
+ logs.append(f"βœ“ Total Inliers: {total_inliers:,}")
231
+ logs.append("=" * 70)
232
+
233
+ # Convert result to RGB
234
+ result_rgb = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
235
+ result_pil = Image.fromarray(result_rgb)
236
+
237
+ # Save for download
238
+ buf = io.BytesIO()
239
+ result_pil.save(buf, format='PNG', optimize=True)
240
+ buf.seek(0)
241
+
242
+ return result_pil, "\n".join(logs), buf
243
+
244
+
245
+ class PoseGuidedWarehouseStitcher(WarehouseStitcher):
246
+ """Enhanced version using drone pose metadata for guided stitching"""
247
+
248
+ def load_metadata_from_file(self, json_path: str) -> dict:
249
+ """Load JSON metadata file"""
250
+ with open(json_path, 'r') as f:
251
+ return json.load(f)
252
+
253
+ def calculate_relative_motion(self, pose1: dict, pose2: dict) -> dict:
254
+ """Calculate relative motion between two poses"""
255
+ nav1 = pose1['nav_snapshot']
256
+ nav2 = pose2['nav_snapshot']
257
+
258
+ dx = nav2['x'] - nav1['x']
259
+ dy = nav2['y'] - nav1['y']
260
+ dz = nav2['z'] - nav1['z']
261
+ dyaw = nav2['yaw'] - nav1['yaw']
262
+
263
+ distance = np.sqrt(dx**2 + dy**2 + dz**2)
264
+
265
+ return {
266
+ 'dx': dx, 'dy': dy, 'dz': dz,
267
+ 'dyaw': dyaw,
268
+ 'distance': distance,
269
+ 'avg_height': (abs(nav1['z']) + abs(nav2['z'])) / 2
270
+ }
271
+
272
+ def estimate_homography_from_pose(self, motion: dict, img_width: int, img_height: int) -> np.ndarray:
273
+ """Estimate initial homography from drone pose data"""
274
+ focal_length_px = img_width * 0.8
275
+ scale = abs(motion['avg_height']) if motion['avg_height'] != 0 else 10.0
276
+
277
+ tx = (motion['dx'] / scale) * focal_length_px
278
+ ty = (motion['dy'] / scale) * focal_length_px
279
+
280
+ theta = motion['dyaw']
281
+ cos_theta = np.cos(theta)
282
+ sin_theta = np.sin(theta)
283
+
284
+ H = np.array([
285
+ [cos_theta, -sin_theta, tx],
286
+ [sin_theta, cos_theta, ty],
287
+ [0, 0, 1]
288
+ ], dtype=np.float64)
289
+
290
+ return H
291
+
292
+ def sort_by_capture_sequence(self, image_paths: List[str], metadata_paths: List[str]) -> Tuple[List, List]:
293
+ """Sort images by capture timestamp"""
294
+ pairs = []
295
+
296
+ for img_path, meta_path in zip(image_paths, metadata_paths):
297
+ metadata = self.load_metadata_from_file(meta_path)
298
+ timestamp = metadata['nav_snapshot']['timestamp_usec']
299
+ pairs.append((timestamp, img_path, meta_path, metadata))
300
+
301
+ pairs.sort(key=lambda x: x[0])
302
+
303
+ sorted_imgs = [p[1] for p in pairs]
304
+ sorted_metas = [p[3] for p in pairs]
305
+
306
+ return sorted_imgs, sorted_metas
307
+
308
+ def stitch_with_poses(self, image_paths: List[str], metadata_paths: List[str],
309
+ progress=gr.Progress()) -> Tuple:
310
+ """Main pose-guided stitching pipeline"""
311
+
312
+ if len(image_paths) != len(metadata_paths):
313
+ return None, "❌ Error: Number of images and metadata files must match", None
314
+
315
+ if len(image_paths) < 2:
316
+ return None, "❌ Error: Need at least 2 images", None
317
+
318
+ logs = []
319
+ logs.append("=" * 70)
320
+ logs.append("🚁 POSE-GUIDED DRONE IMAGE STITCHING PIPELINE")
321
+ logs.append("=" * 70)
322
+ logs.append(f"πŸ“… Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
323
+ logs.append(f"πŸ“Έ Image pairs to process: {len(image_paths)}")
324
+ logs.append("")
325
+
326
+ # Sort by timestamp
327
+ logs.append("πŸ”„ Sorting images by capture sequence...")
328
+ sorted_imgs, sorted_metas = self.sort_by_capture_sequence(image_paths, metadata_paths)
329
+ logs.append(f"βœ“ Images sorted chronologically")
330
+ logs.append("")
331
+
332
+ # Load first image
333
+ result = cv2.imread(sorted_imgs[0])
334
+ logs.append(f"πŸ“Έ Image 1: {result.shape[1]}x{result.shape[0]} @ z={sorted_metas[0]['nav_snapshot']['z']:.2f}m")
335
+
336
+ total_matches = 0
337
+ total_inliers = 0
338
+
339
+ # Process each subsequent image
340
+ for i in range(1, len(sorted_imgs)):
341
+ progress((i / len(sorted_imgs), f"Stitching image {i+1}/{len(sorted_imgs)}"))
342
+
343
+ logs.append("-" * 70)
344
+ logs.append(f"πŸ”„ PROCESSING IMAGE PAIR {i}/{len(sorted_imgs)-1}")
345
+ logs.append("-" * 70)
346
+
347
+ # Load current image
348
+ current_img = cv2.imread(sorted_imgs[i])
349
+ logs.append(f"πŸ“Έ Image {i+1}: {current_img.shape[1]}x{current_img.shape[0]} @ z={sorted_metas[i]['nav_snapshot']['z']:.2f}m")
350
+
351
+ # Calculate relative motion
352
+ motion = self.calculate_relative_motion(sorted_metas[i-1], sorted_metas[i])
353
+ logs.append(f"πŸ“ Drone motion: Ξ”x={motion['dx']:.3f}m, Ξ”y={motion['dy']:.3f}m, Ξ”z={motion['dz']:.3f}m")
354
+ logs.append(f"🧭 Yaw change: {np.degrees(motion['dyaw']):.2f}°")
355
+ logs.append(f"πŸ“ Distance: {motion['distance']:.3f}m")
356
+
357
+ # Get initial homography estimate from pose
358
+ H_initial = self.estimate_homography_from_pose(motion, result.shape[1], result.shape[0])
359
+ logs.append(f"🎯 Initial homography estimated from drone pose")
360
+
361
+ # Extract features
362
+ kp1, desc1, _ = self.extract_features(result)
363
+ kp2, desc2, _ = self.extract_features(current_img)
364
+ logs.append(f"πŸ” Features: {len(kp1)} ↔ {len(kp2)}")
365
+
366
+ # Match features
367
+ matches = self.match_features(desc1, desc2)
368
+ total_matches += len(matches)
369
+ logs.append(f"πŸ”— Matches: {len(matches)} good matches")
370
+
371
+ if len(matches) < self.config['min_match_count']:
372
+ logs.append(f"⚠️ WARNING: Insufficient matches, using pose-only homography")
373
+ H_final = H_initial
374
+ else:
375
+ # Refine homography with feature matches
376
+ H_refined, mask, confidence = self.estimate_homography(kp1, kp2, matches)
377
+
378
+ if H_refined is not None:
379
+ inliers = int(np.sum(mask))
380
+ total_inliers += inliers
381
+ logs.append(f"πŸ“ Refined homography: {inliers}/{len(matches)} inliers ({confidence:.1%})")
382
+ H_final = H_refined
383
+ else:
384
+ logs.append(f"⚠️ Feature-based homography failed, using pose estimate")
385
+ H_final = H_initial
386
+
387
+ # Blend images
388
+ result = self.blend_images(result, current_img, H_final)
389
+ logs.append(f"βœ… Blended! New size: {result.shape[1]}x{result.shape[0]}")
390
+ logs.append("")
391
+
392
+ # Final summary
393
+ logs.append("=" * 70)
394
+ logs.append("πŸ“Š FINAL STATISTICS")
395
+ logs.append("=" * 70)
396
+ logs.append(f"βœ“ Final Resolution: {result.shape[1]} x {result.shape[0]} pixels")
397
+ logs.append(f"βœ“ Total Matches: {total_matches:,}")
398
+ logs.append(f"βœ“ Total Inliers: {total_inliers:,}")
399
+ logs.append(f"βœ“ Completed: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
400
+ logs.append("=" * 70)
401
+
402
+ # Convert to RGB
403
+ result_rgb = cv2.cvtColor(result, cv2.COLOR_BGR2RGB)
404
+ result_pil = Image.fromarray(result_rgb)
405
+
406
+ # Save for download
407
+ buf = io.BytesIO()
408
+ result_pil.save(buf, format='PNG', optimize=True)
409
+ buf.seek(0)
410
+
411
+ return result_pil, "\n".join(logs), buf
412
+
413
+
414
+ def create_demo():
415
+ """Create and configure Gradio interface"""
416
+
417
+ # Initialize both stitchers
418
+ basic_stitcher = WarehouseStitcher()
419
+ pose_stitcher = PoseGuidedWarehouseStitcher()
420
+
421
+ def process_images_basic(files, feature_type, matcher_type, use_clahe, detect_labels, ransac_thresh):
422
+ """Process uploaded images (basic mode)"""
423
+ if not files or len(files) < 2:
424
+ return None, "❌ Please upload at least 2 images for stitching", None
425
+
426
+ # Update configuration
427
+ basic_stitcher.config['feature_extractor'] = feature_type
428
+ basic_stitcher.config['matcher'] = matcher_type
429
+ basic_stitcher.config['use_clahe'] = use_clahe
430
+ basic_stitcher.config['detect_rack_labels'] = detect_labels
431
+ basic_stitcher.config['ransac_threshold'] = ransac_thresh
432
+
433
+ # Load images
434
+ images = [Image.open(f.name) for f in files]
435
+
436
+ # Process
437
+ return basic_stitcher.stitch_images(images)
438
+
439
+ def process_zip_with_metadata(zip_file, feature_type, matcher_type, use_clahe, detect_labels, ransac_thresh):
440
+ """Process ZIP file containing images and metadata"""
441
+ if not zip_file:
442
+ return None, "❌ Please upload a ZIP file", None
443
+
444
+ try:
445
+ with tempfile.TemporaryDirectory() as tmpdir:
446
+ tmpdir_path = Path(tmpdir)
447
+
448
+ # Extract ZIP
449
+ with zipfile.ZipFile(zip_file.name, 'r') as zip_ref:
450
+ zip_ref.extractall(tmpdir_path)
451
+
452
+ # Find images and metadata
453
+ image_files = sorted(list(tmpdir_path.rglob('*.jpg')) +
454
+ list(tmpdir_path.rglob('*.png')))
455
+ json_files = sorted(list(tmpdir_path.rglob('*.json')))
456
+
457
+ if len(image_files) < 2:
458
+ return None, f"❌ Found only {len(image_files)} images, need at least 2", None
459
+
460
+ if len(json_files) == 0:
461
+ return None, "❌ No JSON metadata files found in ZIP", None
462
+
463
+ # Match images with metadata by filename
464
+ image_paths = []
465
+ metadata_paths = []
466
+
467
+ for img_file in image_files:
468
+ # Look for matching JSON
469
+ json_name = img_file.stem + '.json'
470
+ json_candidates = [j for j in json_files if j.name == json_name]
471
+
472
+ if json_candidates:
473
+ image_paths.append(str(img_file))
474
+ metadata_paths.append(str(json_candidates[0]))
475
+
476
+ if len(image_paths) < 2:
477
+ return None, f"❌ Only {len(image_paths)} images have matching metadata", None
478
+
479
+ # Update configuration
480
+ pose_stitcher.config['feature_extractor'] = feature_type
481
+ pose_stitcher.config['matcher'] = matcher_type
482
+ pose_stitcher.config['use_clahe'] = use_clahe
483
+ pose_stitcher.config['detect_rack_labels'] = detect_labels
484
+ pose_stitcher.config['ransac_threshold'] = ransac_thresh
485
+
486
+ # Process with pose guidance
487
+ return pose_stitcher.stitch_with_poses(image_paths, metadata_paths)
488
+
489
+ except Exception as e:
490
+ return None, f"❌ Error processing ZIP: {str(e)}", None
491
+
492
+ # Custom CSS
493
+ custom_css = """
494
+ .gradio-container {
495
+ font-family: 'Arial', sans-serif;
496
+ }
497
+ .output-image {
498
+ border: 2px solid #4CAF50;
499
+ border-radius: 8px;
500
+ }
501
+ """
502
+
503
+ # Create interface
504
+ with gr.Blocks(title="Warehouse Image Stitching", theme=gr.themes.Soft(), css=custom_css) as demo:
505
+
506
+ # Header
507
+ gr.Markdown("""
508
+ # 🏭 Industrial Warehouse Image Stitching Pipeline
509
+
510
+ <div style="background-color: #f0f8ff; padding: 20px; border-radius: 10px; margin-bottom: 20px;">
511
+ <h3>🎯 Production-Ready Stitching for Warehouse Environments</h3>
512
+
513
+ **Key Features:**
514
+ - 🚁 **Pose-Guided Stitching**: Uses drone navigation data for intelligent alignment
515
+ - ✨ Handles specular reflections from shrink wrap and metallic surfaces
516
+ - 🏷️ Detects and uses warehouse rack labels as alignment anchors
517
+ - πŸ” CLAHE preprocessing for enhanced contrast
518
+ - 🎯 RANSAC-based robust homography estimation
519
+ </div>
520
+ """)
521
+
522
+ # Mode selection tabs
523
+ with gr.Tabs():
524
+ # TAB 1: Basic Mode
525
+ with gr.TabItem("πŸ“Έ Basic Mode"):
526
+ gr.Markdown("""
527
+ ### Upload images directly (no metadata needed)
528
+ Perfect for general-purpose panoramas and quick stitching.
529
+ """)
530
+
531
+ with gr.Row():
532
+ with gr.Column(scale=1):
533
+ gr.Markdown("## βš™οΈ Configuration")
534
+
535
+ feature_type_basic = gr.Radio(
536
+ choices=['SIFT', 'ORB', 'AKAZE'],
537
+ value='SIFT',
538
+ label="πŸ” Feature Extractor"
539
+ )
540
+
541
+ matcher_type_basic = gr.Radio(
542
+ choices=['BF', 'FLANN'],
543
+ value='BF',
544
+ label="πŸ”— Feature Matcher"
545
+ )
546
+
547
+ use_clahe_basic = gr.Checkbox(
548
+ value=True,
549
+ label="✨ Enable CLAHE Enhancement"
550
+ )
551
+
552
+ detect_labels_basic = gr.Checkbox(
553
+ value=True,
554
+ label="🏷️ Detect Rack Labels"
555
+ )
556
+
557
+ ransac_thresh_basic = gr.Slider(
558
+ minimum=1.0,
559
+ maximum=10.0,
560
+ value=5.0,
561
+ step=0.5,
562
+ label="πŸ“ RANSAC Threshold"
563
+ )
564
+
565
+ with gr.Column(scale=2):
566
+ file_input = gr.File(
567
+ file_count="multiple",
568
+ file_types=["image"],
569
+ label="πŸ“Έ Upload Warehouse Images (minimum 2)",
570
+ type="filepath"
571
+ )
572
+
573
+ process_basic_btn = gr.Button(
574
+ "πŸ”¨ Stitch Images",
575
+ variant="primary",
576
+ size="lg"
577
+ )
578
+
579
+ # TAB 2: Pose-Guided Mode
580
+ with gr.TabItem("🚁 Pose-Guided Mode"):
581
+ gr.Markdown("""
582
+ ### Upload ZIP file with images + JSON metadata
583
+ For drone captures with navigation data - more robust and accurate!
584
+
585
+ <div style="background-color: #e3f2fd; padding: 15px; border-radius: 8px; margin: 10px 0;">
586
+ <strong>πŸ“¦ ZIP Structure Example:</strong>
587
+ <pre style="background: #fff; padding: 10px; border-radius: 5px;">
588
+ dataset.zip
589
+ β”œβ”€β”€ image_001.jpg
590
+ β”œβ”€β”€ image_001.json (with nav_snapshot)
591
+ β”œβ”€β”€ image_002.jpg
592
+ β”œβ”€β”€ image_002.json
593
+ └── ...
594
+ </pre>
595
+ <strong>Or nested folders:</strong>
596
+ <pre style="background: #fff; padding: 10px; border-radius: 5px;">
597
+ dataset.zip
598
+ β”œβ”€β”€ images/
599
+ β”‚ β”œβ”€β”€ img1.jpg
600
+ β”‚ └── img2.jpg
601
+ └── metadata/
602
+ β”œβ”€β”€ img1.json
603
+ └── img2.json
604
+ </pre>
605
+ </div>
606
+ """)
607
+
608
+ with gr.Row():
609
+ with gr.Column(scale=1):
610
+ gr.Markdown("## βš™οΈ Configuration")
611
+
612
+ feature_type_pose = gr.Radio(
613
+ choices=['SIFT', 'ORB', 'AKAZE'],
614
+ value='SIFT',
615
+ label="πŸ” Feature Extractor"
616
+ )
617
+
618
+ matcher_type_pose = gr.Radio(
619
+ choices=['BF', 'FLANN'],
620
+ value='BF',
621
+ label="πŸ”— Feature Matcher"
622
+ )
623
+
624
+ use_clahe_pose = gr.Checkbox(
625
+ value=True,
626
+ label="✨ Enable CLAHE Enhancement"
627
+ )
628
+
629
+ detect_labels_pose = gr.Checkbox(
630
+ value=True,
631
+ label="🏷️ Detect Rack Labels"
632
+ )
633
+
634
+ ransac_thresh_pose = gr.Slider(
635
+ minimum=1.0,
636
+ maximum=10.0,
637
+ value=5.0,
638
+ step=0.5,
639
+ label="πŸ“ RANSAC Threshold"
640
+ )
641
+
642
+ with gr.Column(scale=2):
643
+ zip_input = gr.File(
644
+ file_count="single",
645
+ file_types=[".zip"],
646
+ label="πŸ“¦ Upload ZIP (images + metadata)",
647
+ type="filepath"
648
+ )
649
+
650
+ process_pose_btn = gr.Button(
651
+ "πŸ”¨ Stitch with Pose Guidance",
652
+ variant="primary",
653
+ size="lg"
654
+ )
655
+
656
+ # Results section (shared by both tabs)
657
+ gr.Markdown("## πŸ“Š Results")
658
+
659
+ with gr.Row():
660
+ with gr.Column(scale=2):
661
+ output_image = gr.Image(
662
+ label="πŸ–ΌοΈ Stitched Panorama",
663
+ type="pil",
664
+ height=500,
665
+ elem_classes=["output-image"]
666
+ )
667
+
668
+ download_btn = gr.File(
669
+ label="⬇️ Download High-Resolution Result",
670
+ type="binary"
671
+ )
672
+
673
+ with gr.Column(scale=1):
674
+ logs_output = gr.Textbox(
675
+ label="πŸ“‹ Processing Logs",
676
+ lines=25,
677
+ max_lines=35,
678
+ autoscroll=True,
679
+ show_copy_button=True
680
+ )
681
+
682
+ # Footer
683
+ gr.Markdown("""
684
+ ---
685
+ <div style="text-align: center; color: #666;">
686
+ <p><strong>Industrial Warehouse Image Stitching Pipeline v1.0.0</strong></p>
687
+ <p>Powered by OpenCV β€’ SIFT β€’ RANSAC β€’ Pose-Guided Alignment</p>
688
+ </div>
689
+ """)
690
+
691
+ # Connect events
692
+ process_basic_btn.click(
693
+ fn=process_images_basic,
694
+ inputs=[file_input, feature_type_basic, matcher_type_basic, use_clahe_basic, detect_labels_basic, ransac_thresh_basic],
695
+ outputs=[output_image, logs_output, download_btn],
696
+ api_name="stitch"
697
+ )
698
+
699
+ process_pose_btn.click(
700
+ fn=process_zip_with_metadata,
701
+ inputs=[zip_input, feature_type_pose, matcher_type_pose, use_clahe_pose, detect_labels_pose, ransac_thresh_pose],
702
+ outputs=[output_image, logs_output, download_btn],
703
+ api_name="pose_stitch"
704
+ )
705
+
706
+ return demo
707
+
708
+
709
+ # Main execution
710
+ if __name__ == "__main__":
711
+ demo = create_demo()
712
+ demo.queue(max_size=5) # Enable queuing for multiple users
713
+ demo.launch(
714
+ server_name="0.0.0.0",
715
+ server_port=7860,
716
+ share=False,
717
+ show_error=True
718
+ )