Spaces:
Sleeping
Sleeping
File size: 15,291 Bytes
630fb49 932db80 630fb49 932db80 630fb49 932db80 467753c 932db80 467753c 932db80 467753c 932db80 467753c 932db80 467753c 932db80 630fb49 932db80 630fb49 932db80 630fb49 932db80 630fb49 932db80 630fb49 932db80 ea95d12 467753c ea95d12 932db80 ea95d12 932db80 ea95d12 467753c ea95d12 932db80 ea95d12 932db80 ea95d12 932db80 ea95d12 467753c 932db80 ea95d12 467753c ea95d12 630fb49 467753c 630fb49 ea95d12 630fb49 932db80 467753c 932db80 630fb49 932db80 630fb49 932db80 630fb49 932db80 630fb49 932db80 630fb49 932db80 630fb49 932db80 630fb49 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 |
import gradio as gr
import os
from PIL import Image, ImageDraw, ImageFont
import io
import base64
from openai import OpenAI
import re
import json
def encode_image(image):
"""Convert PIL Image to base64 string for API"""
buffered = io.BytesIO()
image.save(buffered, format="JPEG", quality=95)
return base64.b64encode(buffered.getvalue()).decode('utf-8')
def draw_annotations(image, annotations):
"""
Draw numbered annotations on the image
Args:
image: PIL Image object
annotations: List of dicts with 'x', 'y', 'label' keys (coordinates are 0-1 normalized)
Returns:
PIL Image with annotations drawn
"""
# Create a copy to avoid modifying original
img_copy = image.copy()
draw = ImageDraw.Draw(img_copy)
# Get image dimensions
width, height = img_copy.size
# Try to load a better font, fall back to default if not available
try:
font_size = max(32, min(width, height) // 25)
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
except:
font = ImageFont.load_default()
# Draw each annotation
for i, ann in enumerate(annotations, 1):
# Convert normalized coordinates to pixel coordinates
x = int(ann['x'] * width)
y = int(ann['y'] * height)
# Circle radius based on image size (larger for better visibility)
radius = max(25, min(width, height) // 50)
# Draw outer circle (white border) - thicker for better visibility
draw.ellipse(
[(x - radius - 4, y - radius - 4), (x + radius + 4, y + radius + 4)],
fill='white',
outline='white'
)
# Draw inner circle (red)
draw.ellipse(
[(x - radius, y - radius), (x + radius, y + radius)],
fill='red',
outline='white',
width=3
)
# Draw number
number_text = str(i)
# Get text bounding box for centering
bbox = draw.textbbox((0, 0), number_text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Draw text centered in circle
text_x = x - text_width // 2
text_y = y - text_height // 2
draw.text((text_x, text_y), number_text, fill='white', font=font)
return img_copy
def analyze_satellite_image(image, geolocation, brief, analysis_mode, api_key):
"""
Analyze satellite imagery using Meta Llama Vision via OpenRouter
Args:
image: PIL Image object
geolocation: String with coordinates in decimal notation
brief: User's analysis requirements and context
analysis_mode: "text_only" or "annotated"
api_key: OpenRouter API key
"""
if not api_key:
return "Please provide your OpenRouter API key to proceed.", None
if not image:
return "Please upload a satellite image.", None
try:
# Use OpenRouter for Meta Llama 3.2 Vision
client = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=api_key
)
# Base system prompt for SATINT analyst role
system_prompt = """You are a seasoned satellite imagery intelligence (SATINT) analyst with decades of experience in analyzing overhead reconnaissance imagery. Your role is to provide objective, professional analysis of satellite imagery.
Your analysis should be:
- Non-emotional and factual
- Precise in describing observable features
- Professional in tone and terminology
- Comprehensive yet concise
- Based solely on what can be observed in the imagery
When geolocation is provided, incorporate geographical and contextual knowledge to enhance your analysis. Consider terrain, climate, regional characteristics, and typical infrastructure patterns for that location.
When a brief is provided, tailor your analysis to address the specific requirements while maintaining professional objectivity."""
# Prepare the user message based on analysis mode
user_message_parts = []
# Add geolocation context if provided
location_context = ""
if geolocation and geolocation.strip():
location_context = f"\n\nGEOLOCATION: {geolocation} (decimal notation)"
# Add brief context if provided
brief_context = ""
if brief and brief.strip():
brief_context = f"\n\nANALYSIS BRIEF: {brief}"
if analysis_mode == "text_only":
instruction = f"""Analyze this satellite image and provide a professional intelligence assessment.{location_context}{brief_context}
Provide your analysis in a structured format covering:
1. Overview and general observations
2. Key features and infrastructure identified
3. Notable patterns or anomalies
4. Assessment and implications (if relevant to the brief)"""
else: # annotated mode
instruction = f"""Analyze this satellite image and provide a professional intelligence assessment with annotations.{location_context}{brief_context}
You MUST format your response in TWO sections:
SECTION 1 - ANNOTATIONS (JSON):
Provide a JSON array of annotation points. Each point should have:
- "x": horizontal position (0.0 to 1.0, where 0.0 is left edge, 1.0 is right edge)
- "y": vertical position (0.0 to 1.0, where 0.0 is top edge, 1.0 is bottom edge)
- "label": brief description of the feature
Start this section with exactly: ANNOTATIONS:
Then provide valid JSON on the next line.
Example format:
ANNOTATIONS:
[
{{"x": 0.25, "y": 0.35, "label": "Military installation"}},
{{"x": 0.75, "y": 0.60, "label": "Vehicle staging area"}}
]
SECTION 2 - ANALYSIS:
Provide your detailed analysis referencing the numbered annotations (1, 2, 3, etc.) that will be drawn on the image:
1. Key features identified (reference annotation numbers)
2. Overview and general observations
3. Notable patterns or anomalies
4. Assessment and implications (if relevant to the brief)
Remember: The annotations will be numbered automatically in the order you list them."""
# Encode image
image_data = encode_image(image)
# Make API call to Llama 3.2 Vision via OpenRouter
response = client.chat.completions.create(
model="meta-llama/llama-3.2-90b-vision-instruct",
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": [
{
"type": "text",
"text": instruction
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_data}"
}
}
]
}
],
max_tokens=4096,
temperature=0.7
)
analysis_text = response.choices[0].message.content
# For annotated mode, parse annotations and draw on image
if analysis_mode == "annotated":
try:
# Extract annotations JSON from response
annotations = []
annotated_image = image
# Look for ANNOTATIONS: section
if "ANNOTATIONS:" not in analysis_text:
# Model didn't provide annotations section
error_msg = """
**ANNOTATION MODE ERROR**
The AI model did not provide an ANNOTATIONS: section in its response.
This means it didn't follow the annotation format instructions.
Try again, or use Text Only mode for standard analysis.
---
**AI Response:**
"""
return error_msg + analysis_text, image
if "ANNOTATIONS:" in analysis_text:
# Extract the JSON part
parts = analysis_text.split("ANNOTATIONS:")
if len(parts) > 1:
json_part = parts[1].split("SECTION 2")[0].strip()
# Also try splitting by "ANALYSIS:" if SECTION 2 not found
if "ANALYSIS:" in json_part:
json_part = json_part.split("ANALYSIS:")[0].strip()
# Try to extract JSON array
json_match = re.search(r'\[.*?\]', json_part, re.DOTALL)
if json_match:
json_str = json_match.group(0)
annotations = json.loads(json_str)
# Draw annotations on image
if annotations and len(annotations) > 0:
annotated_image = draw_annotations(image, annotations)
# Add annotation count to the analysis
annotation_count_msg = f"\n\n**{len(annotations)} annotation(s) marked on image**\n\n"
# Clean up the analysis text to remove JSON section
# Keep only the analysis part
if "ANALYSIS:" in analysis_text:
analysis_text = annotation_count_msg + "ANALYSIS:\n" + analysis_text.split("ANALYSIS:")[1].strip()
elif "SECTION 2" in analysis_text:
analysis_text = annotation_count_msg + analysis_text.split("SECTION 2")[1].strip()
if analysis_text.startswith("- ANALYSIS:"):
analysis_text = analysis_text[12:].strip()
else:
# Annotations array was empty
analysis_text = "\n\n**WARNING: No annotations provided by AI model**\n\n" + analysis_text
return analysis_text, annotated_image
except Exception as e:
# If annotation parsing fails, return original image with a detailed note
error_msg = f"""
**ANNOTATION MODE ERROR**
The AI model's response could not be parsed for annotations. This usually happens when:
- The model doesn't return properly formatted JSON
- The ANNOTATIONS: section is missing or malformed
- The coordinate values are invalid
Error details: {str(e)}
---
**Original AI Response:**
{analysis_text}
"""
return error_msg, image
else:
return analysis_text, None
except Exception as e:
if "authentication" in str(e).lower() or "unauthorized" in str(e).lower():
return "Authentication failed. Please check your OpenRouter API key.", None
return f"Error during analysis: {str(e)}", None
# Create Gradio interface
with gr.Blocks(title="SATINT Analyst - Satellite Imagery Analysis", theme=gr.themes.Soft()) as demo:
gr.Markdown("""
# SATINT Analyst
### Professional Satellite Imagery Intelligence Analysis
Upload satellite imagery and receive professional intelligence analysis from an AI-powered SATINT analyst.
Powered by **Meta Llama 3.2 Vision (90B)** for uncensored, objective analysis.
**Note:** This application requires your own OpenRouter API key (BYOK - Bring Your Own Key).
Get your API key at [openrouter.ai](https://openrouter.ai/keys)
""")
with gr.Row():
with gr.Column(scale=1):
api_key_input = gr.Textbox(
label="OpenRouter API Key",
placeholder="sk-or-v1-...",
type="password",
info="Your API key is only used for this session and is not stored."
)
image_input = gr.Image(
label="Upload Satellite Image",
type="pil",
height=400
)
geolocation_input = gr.Textbox(
label="Geolocation (Optional)",
placeholder="e.g., 38.8977, -77.0365 (decimal notation: latitude, longitude)",
info="Provide coordinates in decimal format for enhanced contextual analysis"
)
brief_input = gr.Textbox(
label="Analysis Brief",
placeholder="Describe what you want analyzed (e.g., 'Identify infrastructure changes', 'Assess military installations', 'Evaluate agricultural land use')",
lines=3,
info="Provide context and specific requirements for the analysis"
)
analysis_mode = gr.Radio(
choices=["text_only", "annotated"],
value="text_only",
label="Analysis Mode",
info="Text Only: Written analysis only | Annotated: Analysis with numbered markers drawn on the image"
)
analyze_btn = gr.Button("Analyze Imagery", variant="primary", size="lg")
with gr.Column(scale=1):
gr.Markdown("### Intelligence Analysis")
with gr.Row():
copy_btn = gr.Button("Copy to Clipboard", size="sm", scale=0)
analysis_output = gr.Markdown(
value="*Analysis will appear here...*",
height=600,
elem_classes="analysis-box"
)
# Hidden textbox to hold raw text for copying
analysis_text_raw = gr.Textbox(visible=False)
annotated_output = gr.Image(
label="Annotated Image",
visible=True
)
gr.Markdown("""
---
### Usage Tips
- **Geolocation**: Use decimal notation (e.g., 38.8977, -77.0365) for latitude and longitude
- **Brief**: Provide specific questions or focus areas for more targeted analysis
- **Text Only Mode**: Receive a detailed written analysis with markdown formatting
- **Annotated Mode**: Receive analysis with numbered annotations drawn on the image referencing key features
- **Copy Button**: Click the clipboard button to copy the analysis text
### Privacy & Model
- **Model**: Meta Llama 3.2 Vision 90B (via OpenRouter)
- Your API key is used only for this session and is not stored
- Images are processed through OpenRouter's API
- Get your OpenRouter API key at [openrouter.ai/keys](https://openrouter.ai/keys)
""")
# Set up the analyze button
def process_analysis(image, geolocation, brief, analysis_mode, api_key):
"""Wrapper to return results for both markdown and raw text"""
text, img = analyze_satellite_image(image, geolocation, brief, analysis_mode, api_key)
return text, text, img # markdown display, raw text for copying, image
analyze_btn.click(
fn=process_analysis,
inputs=[image_input, geolocation_input, brief_input, analysis_mode, api_key_input],
outputs=[analysis_output, analysis_text_raw, annotated_output]
)
# Set up copy button to copy from hidden textbox
copy_btn.click(
fn=lambda x: x,
inputs=[analysis_text_raw],
outputs=[],
js="(text) => {navigator.clipboard.writeText(text); return text;}"
)
if __name__ == "__main__":
demo.launch()
|