| """ |
| Gradio UI for satellite image retrieval. |
| |
| Vaporwave/Outrun interface: neon grids, pink-cyan-purple palette, retro-futurism. |
| """ |
|
|
| import gradio as gr |
| import time |
| import traceback |
| import numpy as np |
| from PIL import Image |
| from pathlib import Path |
| from typing import Optional |
|
|
| from ..retrieval.cross_modal_retrieval import CrossModalRetrieval |
| from ..features.extractor import FeatureExtractor |
|
|
|
|
| _retrieval: Optional[CrossModalRetrieval] = None |
| _feature_extractor: Optional[FeatureExtractor] = None |
| _gallery_dir: Optional[Path] = None |
| _gallery_metadata: Optional[list] = None |
|
|
|
|
| def initialize( |
| retrieval: CrossModalRetrieval, |
| feature_extractor: Optional[FeatureExtractor], |
| gallery_dir: Optional[Path] = None, |
| gallery_metadata: Optional[list] = None, |
| ) -> None: |
| global _retrieval, _feature_extractor, _gallery_dir, _gallery_metadata |
| _retrieval = retrieval |
| _feature_extractor = feature_extractor |
| _gallery_dir = Path(gallery_dir) if gallery_dir else None |
| _gallery_metadata = gallery_metadata |
|
|
|
|
| def _gallery_image_path(idx: int, modality: str) -> Optional[str]: |
| if _gallery_metadata is not None and idx < len(_gallery_metadata): |
| entry = _gallery_metadata[idx] |
| path = Path(entry["gallery_path"]).resolve() |
| if path.exists(): |
| return str(path) |
| if _gallery_dir is not None: |
| path = (_gallery_dir / f"{modality}_{idx}.png").resolve() |
| if path.exists(): |
| return str(path) |
| return None |
|
|
|
|
| def _load_image_tensor(path, modality): |
| """Load an image and return (PIL preview, torch tensor with proper channels).""" |
| import torch |
| ext = Path(path).suffix.lower() |
| |
| if ext in (".tif", ".tiff"): |
| try: |
| import tifffile |
| arr = tifffile.imread(str(path)) |
| |
| if arr.ndim == 2: |
| |
| preview = Image.fromarray(arr).convert("RGB") |
| tensor = torch.from_numpy(arr).float().unsqueeze(0) |
| tensor = tensor.unsqueeze(0) |
| return preview, tensor |
| elif arr.ndim == 3: |
| if arr.shape[-1] in (2, 3, 4, 13): |
| |
| tensor = torch.from_numpy(arr).float() |
| tensor = tensor.permute(2, 0, 1).unsqueeze(0) |
| |
| if arr.shape[-1] >= 3: |
| preview = Image.fromarray(arr[:, :, :3].astype(np.uint8)) |
| else: |
| preview = Image.fromarray(arr[:, :, 0].astype(np.uint8)).convert("RGB") |
| return preview, tensor |
| elif arr.shape[0] in (2, 3, 4, 13): |
| |
| tensor = torch.from_numpy(arr).float().unsqueeze(0) |
| |
| if arr.shape[0] >= 3: |
| preview_arr = np.transpose(arr[:3], (1, 2, 0)) |
| else: |
| preview_arr = np.stack([arr[0]] * 3, axis=-1) |
| if arr.dtype == np.uint16: |
| preview_arr = (preview_arr / 65535.0 * 255).astype(np.uint8) |
| preview = Image.fromarray(preview_arr) |
| return preview, tensor |
| except ImportError: |
| pass |
| |
| img = Image.open(path).convert("RGB") |
| return img, None |
|
|
|
|
| def retrieve(image, modality: str, k: int, retrieval_type: str, |
| use_sar_adapter: bool = False, use_multiscale: bool = False, |
| lat: float = None, lon: float = None, radius_km: float = 50.0): |
| if image is None: |
| return [], "", "Please upload an image first." |
| if _retrieval is None: |
| return [], "", "System not initialized. Please restart the app." |
|
|
| start = time.perf_counter() |
|
|
| try: |
| import torch |
|
|
| if isinstance(image, str): |
| pil_img, img_tensor = _load_image_tensor(image, modality) |
| else: |
| pil_img = image |
| img_tensor = None |
|
|
| if _feature_extractor is not None: |
| if img_tensor is not None and img_tensor.shape[1] not in (3,): |
| |
| query_embedding = _feature_extractor.extract_features_from_tensor( |
| img_tensor, modality=modality, normalize=True |
| ) |
| elif use_sar_adapter and modality == "sar": |
| from ..features.sar_adapter import SARAdapter |
| adapter = SARAdapter() |
| adapter.eval() |
| img_t = torch.from_numpy(np.array(pil_img)).permute(2, 0, 1).float() / 255.0 |
| if img_t.shape[0] == 3: |
| img_t = img_t[:2] |
| img_t = img_t.unsqueeze(0) |
| with torch.no_grad(): |
| adapted = adapter(img_t) |
| adapted_pil = Image.fromarray( |
| (adapted.squeeze(0).permute(1, 2, 0).numpy() * 255).astype(np.uint8)) |
| query_embedding = _feature_extractor.extract_features( |
| adapted_pil, modality=modality, normalize=True) |
| else: |
| query_embedding = _feature_extractor.extract_features( |
| pil_img, modality=modality, normalize=True) |
| else: |
| embed_dim = _retrieval.embed_dim |
| query_embedding = torch.randn(embed_dim) |
| query_embedding = torch.nn.functional.normalize(query_embedding, dim=0) |
|
|
| query_np = query_embedding.unsqueeze(0).numpy().astype(np.float32) |
|
|
| if lat is not None and lon is not None: |
| result = _retrieval.search(query_np, modality, k=k, lat=lat, lon=lon, radius_km=radius_km) |
| elif retrieval_type == "same-modal": |
| result = _retrieval.search(query_np, modality, target_modality=modality, k=k) |
| else: |
| result = _retrieval.search(query_np, modality, k=k, strategy="multi") |
|
|
| elapsed_ms = (time.perf_counter() - start) * 1000 |
|
|
| gallery_images = [] |
| for i, (idx, score) in enumerate(zip(result.indices, result.scores)): |
| mod = result.modalities[i] if result.modalities else modality |
| img_path = _gallery_image_path(idx, mod) |
| if img_path: |
| gallery_images.append(Image.open(img_path)) |
|
|
| if not gallery_images: |
| for idx, _ in zip(result.indices, result.scores): |
| np.random.seed(idx) |
| arr = np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8) |
| gallery_images.append(Image.fromarray(arr)) |
|
|
| timing_text = f"{elapsed_ms:.0f}ms" |
| n_results = len(result.indices) |
| mod_str = ", ".join(set(result.modalities)) if result.modalities else modality |
| status_text = f"{n_results} results | {mod_str} | {elapsed_ms:.0f}ms" |
|
|
| return gallery_images, timing_text, status_text |
|
|
| except Exception as exc: |
| tb = traceback.format_exc() |
| return [], "", f"Error: {exc}\n\n{tb}" |
|
|
|
|
| |
| |
| |
|
|
| VAPORWAVE_CSS = """ |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Outfit:wght@300;400;600&display=swap'); |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| :root { |
| --neon-pink: #ff6bcd; |
| --neon-cyan: #00f0ff; |
| --neon-purple: #b300ff; |
| --neon-blue: #0044ff; |
| --dark-bg: #0a0015; |
| --card-bg: #12002a; |
| --card-border: #2a0050; |
| --text-primary: #e0c0ff; |
| --text-secondary: #9a6fb0; |
| --glow-pink: 0 0 20px rgba(255, 107, 205, 0.5); |
| --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.5); |
| --glow-purple: 0 0 20px rgba(179, 0, 255, 0.5); |
| } |
| |
| /* Grid background */ |
| body, .gradio-container { |
| font-family: 'Outfit', sans-serif !important; |
| max-width: 1200px !important; |
| margin: 0 auto !important; |
| background: var(--dark-bg) !important; |
| color: var(--text-primary) !important; |
| position: relative; |
| overflow-x: hidden; |
| } |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| top: 0; left: 0; right: 0; bottom: 0; |
| background: |
| linear-gradient(transparent 0%, rgba(179, 0, 255, 0.03) 50%, transparent 100%), |
| repeating-linear-gradient( |
| 0deg, |
| transparent, |
| transparent 40px, |
| rgba(0, 240, 255, 0.04) 40px, |
| rgba(0, 240, 255, 0.04) 41px |
| ), |
| repeating-linear-gradient( |
| 90deg, |
| transparent, |
| transparent 40px, |
| rgba(255, 107, 205, 0.04) 40px, |
| rgba(255, 107, 205, 0.04) 41px |
| ); |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| /* Scanline overlay */ |
| body::after { |
| content: ''; |
| position: fixed; |
| top: 0; left: 0; right: 0; bottom: 0; |
| background: repeating-linear-gradient( |
| 0deg, |
| transparent, |
| transparent 2px, |
| rgba(0, 0, 0, 0.15) 2px, |
| rgba(0, 0, 0, 0.15) 4px |
| ); |
| pointer-events: none; |
| z-index: 1; |
| } |
| |
| .gradio-container { |
| position: relative; |
| z-index: 2; |
| background: transparent !important; |
| } |
| |
| /* Header */ |
| .vapor-header { |
| text-align: center; |
| padding: 2rem 1rem 1.5rem; |
| margin: -1rem -1rem 0 -1rem; |
| position: relative; |
| background: linear-gradient(180deg, rgba(179, 0, 255, 0.15) 0%, transparent 100%); |
| border-bottom: 2px solid var(--neon-purple); |
| box-shadow: var(--glow-purple); |
| } |
| |
| .vapor-header::after { |
| content: ''; |
| position: absolute; |
| bottom: -2px; |
| left: 10%; right: 10%; |
| height: 1px; |
| background: linear-gradient(90deg, transparent, var(--neon-cyan), transparent); |
| } |
| |
| .vapor-title { |
| font-family: 'Orbitron', monospace; |
| font-size: 1.6rem; |
| font-weight: 900; |
| text-transform: uppercase; |
| letter-spacing: 4px; |
| background: linear-gradient(90deg, var(--neon-cyan), var(--neon-pink), var(--neon-purple)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| text-shadow: none; |
| filter: drop-shadow(0 0 10px rgba(255, 107, 205, 0.3)); |
| } |
| |
| .vapor-subtitle { |
| font-size: 0.85rem; |
| color: var(--text-secondary); |
| letter-spacing: 3px; |
| text-transform: uppercase; |
| margin-top: 0.3rem; |
| } |
| |
| .vapor-sun { |
| display: inline-block; |
| width: 60px; height: 60px; |
| border-radius: 50%; |
| background: linear-gradient(135deg, var(--neon-pink), var(--neon-purple)); |
| box-shadow: 0 0 40px rgba(255, 107, 205, 0.4), 0 0 80px rgba(179, 0, 255, 0.2); |
| margin-bottom: 0.5rem; |
| animation: pulse-glow 3s ease-in-out infinite; |
| } |
| |
| @keyframes pulse-glow { |
| 0%, 100% { box-shadow: 0 0 40px rgba(255, 107, 205, 0.4), 0 0 80px rgba(179, 0, 255, 0.2); } |
| 50% { box-shadow: 0 0 60px rgba(255, 107, 205, 0.6), 0 0 100px rgba(179, 0, 255, 0.3); } |
| } |
| |
| /* Cards */ |
| .vapor-card-left, .vapor-card-right { |
| background: var(--card-bg) !important; |
| border: 1px solid var(--card-border) !important; |
| box-shadow: 0 0 15px rgba(179, 0, 255, 0.1), inset 0 0 30px rgba(0, 0, 0, 0.3) !important; |
| padding: 1rem !important; |
| border-radius: 4px !important; |
| position: relative; |
| backdrop-filter: blur(10px); |
| } |
| |
| .vapor-card-left::before, .vapor-card-right::before { |
| content: ''; |
| position: absolute; |
| top: 0; left: 0; right: 0; |
| height: 1px; |
| background: linear-gradient(90deg, transparent, var(--neon-cyan), transparent); |
| } |
| |
| .vapor-card-left::after, .vapor-card-right::after { |
| content: ''; |
| position: absolute; |
| bottom: 0; left: 0; right: 0; |
| height: 1px; |
| background: linear-gradient(90deg, transparent, var(--neon-pink), transparent); |
| } |
| |
| .section-label { |
| font-family: 'Orbitron', monospace; |
| font-size: 0.7rem; |
| font-weight: 700; |
| text-transform: uppercase; |
| letter-spacing: 3px; |
| color: var(--neon-cyan); |
| display: block; |
| margin-bottom: 0.75rem; |
| text-shadow: var(--glow-cyan); |
| } |
| |
| /* Search button */ |
| .vapor-btn { |
| background: linear-gradient(135deg, var(--neon-purple), var(--neon-pink)) !important; |
| color: #fff !important; |
| border: none !important; |
| border-radius: 4px !important; |
| font-family: 'Orbitron', monospace !important; |
| font-weight: 700 !important; |
| font-size: 0.85rem !important; |
| text-transform: uppercase !important; |
| letter-spacing: 3px !important; |
| padding: 0.8rem 1.2rem !important; |
| width: 100% !important; |
| cursor: pointer !important; |
| box-shadow: 0 0 20px rgba(179, 0, 255, 0.3) !important; |
| transition: all 0.3s ease !important; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .vapor-btn::before { |
| content: ''; |
| position: absolute; |
| top: -50%; left: -50%; |
| width: 200%; height: 200%; |
| background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent); |
| transform: rotate(45deg); |
| transition: all 0.5s ease; |
| } |
| |
| .vapor-btn:hover { |
| box-shadow: 0 0 40px rgba(179, 0, 255, 0.5), 0 0 60px rgba(255, 107, 205, 0.3) !important; |
| transform: translateY(-2px) !important; |
| } |
| |
| .vapor-btn:hover::before { |
| left: 100%; |
| } |
| |
| .vapor-btn:active { |
| transform: translateY(1px) !important; |
| } |
| |
| /* Status */ |
| .vapor-status textarea { |
| background: rgba(0, 0, 0, 0.4) !important; |
| color: var(--neon-cyan) !important; |
| border: 1px solid var(--card-border) !important; |
| font-family: 'Orbitron', monospace !important; |
| font-weight: 400 !important; |
| font-size: 0.75rem !important; |
| letter-spacing: 2px !important; |
| border-radius: 4px !important; |
| box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.3) !important; |
| } |
| |
| /* Gallery */ |
| .vapor-gallery { |
| border: 1px solid var(--card-border) !important; |
| border-radius: 4px !important; |
| background: rgba(0, 0, 0, 0.2) !important; |
| box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3) !important; |
| } |
| |
| .vapor-gallery img { |
| transition: all 0.3s ease !important; |
| border: 1px solid transparent !important; |
| } |
| |
| .vapor-gallery img:hover { |
| transform: scale(1.05) !important; |
| border-color: var(--neon-cyan) !important; |
| box-shadow: 0 0 15px rgba(0, 240, 255, 0.3) !important; |
| z-index: 10; |
| } |
| |
| /* Modality cards */ |
| .modality-cards { |
| display: flex; |
| gap: 2px; |
| margin-top: 0.75rem; |
| } |
| |
| .modality-card { |
| flex: 1; |
| padding: 0.6rem 0.4rem; |
| text-align: center; |
| font-size: 0.65rem; |
| font-weight: 600; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| color: var(--text-secondary); |
| background: rgba(0, 0, 0, 0.3); |
| border: 1px solid var(--card-border); |
| transition: all 0.3s ease; |
| cursor: pointer; |
| } |
| |
| .modality-card strong { |
| display: block; |
| font-size: 0.75rem; |
| margin-bottom: 0.1rem; |
| } |
| |
| .modality-card:nth-child(1) { border-color: rgba(0, 240, 255, 0.3); } |
| .modality-card:nth-child(2) { border-color: rgba(255, 107, 205, 0.3); } |
| .modality-card:nth-child(3) { border-color: rgba(179, 0, 255, 0.3); } |
| |
| .modality-card:nth-child(1):hover { background: rgba(0, 240, 255, 0.1); box-shadow: 0 0 10px rgba(0, 240, 255, 0.2); } |
| .modality-card:nth-child(2):hover { background: rgba(255, 107, 205, 0.1); box-shadow: 0 0 10px rgba(255, 107, 205, 0.2); } |
| .modality-card:nth-child(3):hover { background: rgba(179, 0, 255, 0.1); box-shadow: 0 0 10px rgba(179, 0, 255, 0.2); } |
| |
| /* Tags */ |
| .tag { |
| display: inline-block; |
| padding: 0.2rem 0.5rem; |
| font-size: 0.6rem; |
| font-family: 'Orbitron', monospace; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| margin: 0 0.1rem; |
| transition: all 0.2s ease; |
| } |
| |
| .tag:hover { |
| transform: translateY(-1px); |
| filter: brightness(1.3); |
| } |
| |
| .tag-optical { background: rgba(0, 240, 255, 0.2); color: var(--neon-cyan); border: 1px solid rgba(0, 240, 255, 0.3); } |
| .tag-sar { background: rgba(255, 107, 205, 0.2); color: var(--neon-pink); border: 1px solid rgba(255, 107, 205, 0.3); } |
| .tag-ms { background: rgba(179, 0, 255, 0.2); color: var(--neon-purple); border: 1px solid rgba(179, 0, 255, 0.3); } |
| |
| /* Footer */ |
| .vapor-footer { |
| text-align: center; |
| font-size: 0.7rem; |
| color: var(--text-secondary); |
| padding: 1rem; |
| margin: 1rem -1rem -1rem; |
| border-top: 1px solid var(--card-border); |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| position: relative; |
| } |
| |
| .vapor-footer::before { |
| content: ''; |
| position: absolute; |
| top: -1px; |
| left: 20%; right: 20%; |
| height: 1px; |
| background: linear-gradient(90deg, transparent, var(--neon-pink), transparent); |
| } |
| |
| /* Gradio overrides */ |
| .gradio-container .wrap { border-radius: 0 !important; } |
| |
| .gradio-container input, .gradio-container textarea, .gradio-container select { |
| border-radius: 4px !important; |
| border: 1px solid var(--card-border) !important; |
| background: rgba(0, 0, 0, 0.4) !important; |
| color: var(--text-primary) !important; |
| font-family: 'Outfit', sans-serif !important; |
| transition: all 0.2s ease !important; |
| } |
| |
| .gradio-container input:focus, .gradio-container textarea:focus, .gradio-container select:focus { |
| border-color: var(--neon-cyan) !important; |
| box-shadow: 0 0 10px rgba(0, 240, 255, 0.2) !important; |
| } |
| |
| .gradio-container .slider-container input[type="range"] { |
| accent-color: var(--neon-pink) !important; |
| } |
| |
| /* Labels and dropdowns */ |
| .gradio-container label { |
| color: var(--text-secondary) !important; |
| font-family: 'Outfit', sans-serif !important; |
| font-weight: 400 !important; |
| letter-spacing: 1px !important; |
| text-transform: uppercase !important; |
| font-size: 0.7rem !important; |
| } |
| |
| /* Scrollbar */ |
| ::-webkit-scrollbar { width: 6px; } |
| ::-webkit-scrollbar-track { background: var(--dark-bg); } |
| ::-webkit-scrollbar-thumb { background: var(--neon-purple); border-radius: 3px; } |
| ::-webkit-scrollbar-thumb:hover { background: var(--neon-pink); } |
| |
| /* File upload */ |
| .gradio-container input[type="file"]::file-selector-button { |
| background: linear-gradient(135deg, var(--neon-purple), var(--neon-pink)) !important; |
| color: #fff !important; |
| border: none !important; |
| border-radius: 4px !important; |
| padding: 0.4rem 0.8rem !important; |
| font-family: 'Orbitron', monospace !important; |
| font-size: 0.65rem !important; |
| text-transform: uppercase !important; |
| cursor: pointer !important; |
| transition: all 0.2s ease !important; |
| } |
| |
| .gradio-container input[type="file"]::file-selector-button:hover { |
| box-shadow: 0 0 10px rgba(179, 0, 255, 0.4) !important; |
| } |
| |
| /* Gallery caption text */ |
| .gradio-container .gallery-item p { |
| font-family: 'Outfit', sans-serif !important; |
| font-size: 0.65rem !important; |
| color: var(--text-secondary) !important; |
| } |
| |
| /* Keep the neon terminal vibes */ |
| @keyframes flicker { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.98; } |
| } |
| |
| .vapor-header { |
| animation: flicker 0.15s infinite; |
| } |
| </style> |
| """ |
|
|
|
|
| def _open_image(file): |
| if file is None: |
| return None |
| if isinstance(file, dict): |
| return Image.open(file.get('path') or file.get('url')) |
| if hasattr(file, 'path'): |
| return Image.open(file.path) |
| if hasattr(file, 'name'): |
| return Image.open(file.name) |
| if isinstance(file, str): |
| return Image.open(file) |
| return Image.open(file) |
|
|
|
|
| def create_app() -> gr.Blocks: |
| def on_upload(file): |
| if file is None: |
| return None |
| path = None |
| if isinstance(file, dict): |
| path = file.get('path') or file.get('url') |
| elif hasattr(file, 'path'): |
| path = file.path |
| elif hasattr(file, 'name'): |
| path = file.name |
| elif isinstance(file, str): |
| path = file |
| if path: |
| pil_img, _ = _load_image_tensor(path, "optical") |
| return pil_img |
| return _open_image(file) |
|
|
| def on_retrieve(file, modality, k, retrieval_type): |
| if file is None: |
| return [], "", "Upload an image first." |
| return retrieve(file, modality, int(float(k)), retrieval_type) |
|
|
| with gr.Blocks(title="SATCOM // Cross-Modal Retrieval") as app: |
| gr.HTML(VAPORWAVE_CSS) |
|
|
| gr.HTML(""" |
| <div class="vapor-header"> |
| <div class="vapor-sun"></div> |
| <div class="vapor-title">SATCOM // RETRIEVAL</div> |
| <div class="vapor-subtitle">Cross-Modal Satellite Image Search // Optical · SAR · Multispectral</div> |
| </div> |
| """) |
|
|
| with gr.Row(): |
| with gr.Column(scale=1, elem_classes=["vapor-card-left"]): |
| gr.HTML('<span class="section-label">// INPUT</span>') |
|
|
| file_input = gr.File( |
| label="Upload Satellite Image", |
| file_types=[".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp"], |
| ) |
|
|
| preview = gr.Image( |
| label="Preview", |
| interactive=False, |
| height=160, |
| ) |
|
|
| gr.HTML('<span class="section-label">// SETTINGS</span>') |
|
|
| modality = gr.Dropdown( |
| ["optical", "sar", "multispectral"], |
| value="optical", |
| label="Query Modality", |
| ) |
|
|
| retrieval_type = gr.Radio( |
| ["same-modal", "cross-modal"], |
| value="same-modal", |
| label="Retrieval Type", |
| ) |
|
|
| k_slider = gr.Slider( |
| 1, 10, value=5, step=1, |
| label="Results (K)", |
| ) |
|
|
| btn = gr.Button( |
| "▶ EXECUTE SEARCH", |
| variant="primary", |
| elem_classes=["vapor-btn"], |
| ) |
|
|
| gr.HTML(""" |
| <div class="modality-cards"> |
| <div class="modality-card"> |
| <strong>OPTICAL</strong> |
| RGB · 3ch |
| </div> |
| <div class="modality-card"> |
| <strong>SAR</strong> |
| Radar · 2ch |
| </div> |
| <div class="modality-card"> |
| <strong>MULTI</strong> |
| All · 13ch |
| </div> |
| </div> |
| """) |
|
|
| with gr.Column(scale=2, elem_classes=["vapor-card-right"]): |
| gr.HTML('<span class="section-label">// RESULTS</span>') |
|
|
| status = gr.Textbox( |
| label="Status", |
| interactive=False, |
| lines=1, |
| elem_classes=["vapor-status"], |
| ) |
|
|
| gallery = gr.Gallery( |
| label="Retrieved Images", |
| columns=5, |
| rows=2, |
| height=360, |
| elem_classes=["vapor-gallery"], |
| ) |
|
|
| timing = gr.Textbox( |
| label="Query Time", |
| interactive=False, |
| lines=1, |
| ) |
|
|
| gr.HTML(""" |
| <div class="vapor-footer"> |
| <span class="tag tag-optical">OPTICAL</span> |
| <span class="tag tag-sar">SAR</span> |
| <span class="tag tag-ms">MULTISPECTRAL</span> |
| // |
| SatCLIP + FAISS + Multi-Index |
| // |
| Ayush · Karan · Anurag |
| </div> |
| """) |
|
|
| file_input.change(fn=on_upload, inputs=[file_input], outputs=[preview]) |
| btn.click( |
| fn=on_retrieve, |
| inputs=[file_input, modality, k_slider, retrieval_type], |
| outputs=[gallery, timing, status], |
| ) |
|
|
| return app |
|
|
|
|
| if __name__ == "__main__": |
| app = create_app() |
| print("Gradio app created. Run with: app.launch()") |
|
|