Spaces:
Runtime error
Runtime error
GitHub Copilot
commited on
Commit
·
45cef88
1
Parent(s):
04f7887
Feature: Add Image Analyzer tab and HF secrets config
Browse files- app.py +51 -0
- logos/image_analyzer.py +204 -0
- push_output.txt +12 -0
app.py
CHANGED
|
@@ -18,11 +18,26 @@ if current_dir not in sys.path:
|
|
| 18 |
try:
|
| 19 |
from logos.dsp_bridge import DSPBridge
|
| 20 |
from logos.logos_core import PRIME_MODULO
|
|
|
|
| 21 |
print(f"[LOGOS] Successfully imported DSPBridge and PRIME_MODULO={PRIME_MODULO}")
|
| 22 |
except ImportError as e:
|
| 23 |
print(f"[LOGOS] Error importing components: {e}")
|
| 24 |
DSPBridge = None
|
| 25 |
PRIME_MODULO = 9973
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
# ==========================================
|
|
@@ -243,5 +258,41 @@ with gr.Blocks(theme=gr.themes.Monochrome(), title="LOGOS SPCW Protocol", js="()
|
|
| 243 |
|
| 244 |
btn_proc.click(process_dsp, [input_img, workers], [output_img, output_stats])
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
if __name__ == "__main__":
|
| 247 |
demo.launch()
|
|
|
|
| 18 |
try:
|
| 19 |
from logos.dsp_bridge import DSPBridge
|
| 20 |
from logos.logos_core import PRIME_MODULO
|
| 21 |
+
from logos.image_analyzer import analyze_image, batch_analyze, summarize_analysis
|
| 22 |
print(f"[LOGOS] Successfully imported DSPBridge and PRIME_MODULO={PRIME_MODULO}")
|
| 23 |
except ImportError as e:
|
| 24 |
print(f"[LOGOS] Error importing components: {e}")
|
| 25 |
DSPBridge = None
|
| 26 |
PRIME_MODULO = 9973
|
| 27 |
+
analyze_image = None
|
| 28 |
+
batch_analyze = None
|
| 29 |
+
summarize_analysis = None
|
| 30 |
+
|
| 31 |
+
# ==========================================
|
| 32 |
+
# ENVIRONMENT CONFIGURATION (HF Secrets)
|
| 33 |
+
# ==========================================
|
| 34 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", None)
|
| 35 |
+
NUM_WORKERS_DEFAULT = int(os.environ.get("NUM_WORKERS", 16))
|
| 36 |
+
DEBUG_MODE = os.environ.get("LOGOS_DEBUG", "0") == "1"
|
| 37 |
+
|
| 38 |
+
if DEBUG_MODE:
|
| 39 |
+
print(f"[LOGOS] DEBUG MODE ENABLED")
|
| 40 |
+
print(f"[LOGOS] NUM_WORKERS_DEFAULT={NUM_WORKERS_DEFAULT}")
|
| 41 |
|
| 42 |
|
| 43 |
# ==========================================
|
|
|
|
| 258 |
|
| 259 |
btn_proc.click(process_dsp, [input_img, workers], [output_img, output_stats])
|
| 260 |
|
| 261 |
+
with gr.Tab("Image Analyzer"):
|
| 262 |
+
gr.Markdown("## Blueprint Analysis Pipeline")
|
| 263 |
+
gr.Markdown("Analyze architectural diagrams and UI screenshots.")
|
| 264 |
+
|
| 265 |
+
with gr.Row():
|
| 266 |
+
with gr.Column():
|
| 267 |
+
upload_files = gr.File(label="Upload Images", file_count="multiple", file_types=["image"])
|
| 268 |
+
btn_analyze = gr.Button("Analyze Images", variant="primary")
|
| 269 |
+
with gr.Column():
|
| 270 |
+
gallery = gr.Gallery(label="Analysis Results", columns=4, height="auto")
|
| 271 |
+
analysis_summary = gr.JSON(label="Summary Statistics")
|
| 272 |
+
|
| 273 |
+
def run_analysis(files):
|
| 274 |
+
if files is None or len(files) == 0:
|
| 275 |
+
return [], {"error": "No files uploaded"}
|
| 276 |
+
|
| 277 |
+
if analyze_image is None:
|
| 278 |
+
return [], {"error": "Image analyzer not loaded"}
|
| 279 |
+
|
| 280 |
+
results = []
|
| 281 |
+
thumbnails = []
|
| 282 |
+
|
| 283 |
+
for f in files:
|
| 284 |
+
try:
|
| 285 |
+
analysis = analyze_image(f.name if hasattr(f, 'name') else f)
|
| 286 |
+
results.append(analysis)
|
| 287 |
+
if analysis.thumbnail is not None:
|
| 288 |
+
thumbnails.append((analysis.thumbnail, f"{analysis.classification}: {analysis.filename}"))
|
| 289 |
+
except Exception as e:
|
| 290 |
+
print(f"[ANALYZER] Error: {e}")
|
| 291 |
+
|
| 292 |
+
summary = summarize_analysis(results) if results else {"error": "No results"}
|
| 293 |
+
return thumbnails, summary
|
| 294 |
+
|
| 295 |
+
btn_analyze.click(run_analysis, [upload_files], [gallery, analysis_summary])
|
| 296 |
+
|
| 297 |
if __name__ == "__main__":
|
| 298 |
demo.launch()
|
logos/image_analyzer.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
image_analyzer.py - LOGOS Image Analysis Pipeline
|
| 3 |
+
Batch-process architectural diagrams and UI screenshots.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import cv2
|
| 8 |
+
import numpy as np
|
| 9 |
+
from PIL import Image
|
| 10 |
+
from typing import List, Dict, Tuple, Optional
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
from collections import Counter
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class ImageAnalysis:
|
| 17 |
+
"""Result of analyzing a single image."""
|
| 18 |
+
filename: str
|
| 19 |
+
path: str
|
| 20 |
+
width: int
|
| 21 |
+
height: int
|
| 22 |
+
aspect_ratio: float
|
| 23 |
+
classification: str # "diagram", "ui", "photo", "other"
|
| 24 |
+
dominant_colors: List[Tuple[int, int, int]]
|
| 25 |
+
edge_density: float
|
| 26 |
+
text_region_ratio: float
|
| 27 |
+
thumbnail: Optional[np.ndarray] = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def classify_image(edge_density: float, color_variance: float, aspect: float) -> str:
|
| 31 |
+
"""
|
| 32 |
+
Simple heuristic classification.
|
| 33 |
+
- Diagrams: High edge density, low color variance, often wide aspect.
|
| 34 |
+
- UI: Medium edge density, structured colors, standard aspect.
|
| 35 |
+
- Photos: Low edge density, high color variance.
|
| 36 |
+
"""
|
| 37 |
+
if edge_density > 0.15 and color_variance < 50:
|
| 38 |
+
return "diagram"
|
| 39 |
+
elif 0.05 < edge_density < 0.20 and 0.5 < aspect < 2.0:
|
| 40 |
+
return "ui"
|
| 41 |
+
elif color_variance > 80:
|
| 42 |
+
return "photo"
|
| 43 |
+
else:
|
| 44 |
+
return "other"
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def get_dominant_colors(image: np.ndarray, k: int = 3) -> List[Tuple[int, int, int]]:
|
| 48 |
+
"""Extract k dominant colors using k-means clustering."""
|
| 49 |
+
pixels = image.reshape(-1, 3).astype(np.float32)
|
| 50 |
+
|
| 51 |
+
# Subsample for speed
|
| 52 |
+
if len(pixels) > 10000:
|
| 53 |
+
indices = np.random.choice(len(pixels), 10000, replace=False)
|
| 54 |
+
pixels = pixels[indices]
|
| 55 |
+
|
| 56 |
+
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
|
| 57 |
+
_, labels, centers = cv2.kmeans(pixels, k, None, criteria, 3, cv2.KMEANS_PP_CENTERS)
|
| 58 |
+
|
| 59 |
+
# Sort by frequency
|
| 60 |
+
counts = Counter(labels.flatten())
|
| 61 |
+
sorted_centers = [centers[i] for i, _ in counts.most_common(k)]
|
| 62 |
+
|
| 63 |
+
return [(int(c[2]), int(c[1]), int(c[0])) for c in sorted_centers] # BGR -> RGB
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def calculate_edge_density(gray: np.ndarray) -> float:
|
| 67 |
+
"""Calculate edge density using Canny edge detection."""
|
| 68 |
+
edges = cv2.Canny(gray, 50, 150)
|
| 69 |
+
return np.count_nonzero(edges) / edges.size
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def estimate_text_regions(gray: np.ndarray) -> float:
|
| 73 |
+
"""
|
| 74 |
+
Estimate ratio of image containing text-like regions.
|
| 75 |
+
Uses morphological operations to find text blocks.
|
| 76 |
+
"""
|
| 77 |
+
# Threshold
|
| 78 |
+
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
| 79 |
+
|
| 80 |
+
# Dilate to connect text characters
|
| 81 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 3))
|
| 82 |
+
dilated = cv2.dilate(binary, kernel, iterations=2)
|
| 83 |
+
|
| 84 |
+
# Find contours
|
| 85 |
+
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 86 |
+
|
| 87 |
+
# Filter by aspect ratio (text regions are usually wide)
|
| 88 |
+
text_area = 0
|
| 89 |
+
for cnt in contours:
|
| 90 |
+
x, y, w, h = cv2.boundingRect(cnt)
|
| 91 |
+
if w > h * 2 and w > 20: # Wide and reasonably sized
|
| 92 |
+
text_area += w * h
|
| 93 |
+
|
| 94 |
+
return text_area / (gray.shape[0] * gray.shape[1])
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def analyze_image(path: str, thumbnail_size: int = 128) -> ImageAnalysis:
|
| 98 |
+
"""
|
| 99 |
+
Analyze a single image and return structured metadata.
|
| 100 |
+
"""
|
| 101 |
+
img = cv2.imread(path)
|
| 102 |
+
if img is None:
|
| 103 |
+
raise ValueError(f"Could not load image: {path}")
|
| 104 |
+
|
| 105 |
+
height, width = img.shape[:2]
|
| 106 |
+
aspect = width / height
|
| 107 |
+
|
| 108 |
+
# Convert to RGB for color analysis
|
| 109 |
+
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 110 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 111 |
+
|
| 112 |
+
# Metrics
|
| 113 |
+
dominant_colors = get_dominant_colors(rgb)
|
| 114 |
+
edge_density = calculate_edge_density(gray)
|
| 115 |
+
text_ratio = estimate_text_regions(gray)
|
| 116 |
+
color_variance = np.std(rgb)
|
| 117 |
+
|
| 118 |
+
# Classification
|
| 119 |
+
classification = classify_image(edge_density, color_variance, aspect)
|
| 120 |
+
|
| 121 |
+
# Thumbnail
|
| 122 |
+
scale = thumbnail_size / max(width, height)
|
| 123 |
+
thumb = cv2.resize(rgb, (int(width * scale), int(height * scale)))
|
| 124 |
+
|
| 125 |
+
return ImageAnalysis(
|
| 126 |
+
filename=os.path.basename(path),
|
| 127 |
+
path=path,
|
| 128 |
+
width=width,
|
| 129 |
+
height=height,
|
| 130 |
+
aspect_ratio=round(aspect, 2),
|
| 131 |
+
classification=classification,
|
| 132 |
+
dominant_colors=dominant_colors,
|
| 133 |
+
edge_density=round(edge_density, 4),
|
| 134 |
+
text_region_ratio=round(text_ratio, 4),
|
| 135 |
+
thumbnail=thumb
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def batch_analyze(folder: str, extensions: List[str] = None) -> List[ImageAnalysis]:
|
| 140 |
+
"""
|
| 141 |
+
Analyze all images in a folder.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
folder: Path to folder containing images.
|
| 145 |
+
extensions: List of valid extensions (default: ['.png', '.jpg', '.jpeg']).
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
List of ImageAnalysis results.
|
| 149 |
+
"""
|
| 150 |
+
if extensions is None:
|
| 151 |
+
extensions = ['.png', '.jpg', '.jpeg', '.bmp', '.webp']
|
| 152 |
+
|
| 153 |
+
results = []
|
| 154 |
+
|
| 155 |
+
for filename in os.listdir(folder):
|
| 156 |
+
ext = os.path.splitext(filename)[1].lower()
|
| 157 |
+
if ext in extensions:
|
| 158 |
+
path = os.path.join(folder, filename)
|
| 159 |
+
try:
|
| 160 |
+
analysis = analyze_image(path)
|
| 161 |
+
results.append(analysis)
|
| 162 |
+
except Exception as e:
|
| 163 |
+
print(f"[ANALYZER] Error processing {filename}: {e}")
|
| 164 |
+
|
| 165 |
+
return results
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def summarize_analysis(results: List[ImageAnalysis]) -> Dict:
|
| 169 |
+
"""Generate summary statistics from batch analysis."""
|
| 170 |
+
if not results:
|
| 171 |
+
return {"count": 0}
|
| 172 |
+
|
| 173 |
+
classifications = Counter(r.classification for r in results)
|
| 174 |
+
avg_edge = sum(r.edge_density for r in results) / len(results)
|
| 175 |
+
avg_text = sum(r.text_region_ratio for r in results) / len(results)
|
| 176 |
+
|
| 177 |
+
return {
|
| 178 |
+
"count": len(results),
|
| 179 |
+
"classifications": dict(classifications),
|
| 180 |
+
"avg_edge_density": round(avg_edge, 4),
|
| 181 |
+
"avg_text_ratio": round(avg_text, 4),
|
| 182 |
+
"total_size_mb": round(sum(r.width * r.height * 3 for r in results) / (1024 * 1024), 2)
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# CLI for standalone testing
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
import sys
|
| 189 |
+
|
| 190 |
+
if len(sys.argv) < 2:
|
| 191 |
+
print("Usage: python image_analyzer.py <folder_path>")
|
| 192 |
+
sys.exit(1)
|
| 193 |
+
|
| 194 |
+
folder = sys.argv[1]
|
| 195 |
+
print(f"[ANALYZER] Processing folder: {folder}")
|
| 196 |
+
|
| 197 |
+
results = batch_analyze(folder)
|
| 198 |
+
summary = summarize_analysis(results)
|
| 199 |
+
|
| 200 |
+
print(f"\n[SUMMARY]")
|
| 201 |
+
print(f" Total Images: {summary['count']}")
|
| 202 |
+
print(f" Classifications: {summary['classifications']}")
|
| 203 |
+
print(f" Avg Edge Density: {summary['avg_edge_density']}")
|
| 204 |
+
print(f" Avg Text Ratio: {summary['avg_text_ratio']}")
|
push_output.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
remote: -------------------------------------------------------------------------
|
| 2 |
+
remote: Sorry, your push was rejected during YAML metadata verification:
|
| 3 |
+
remote: - Error: "short_description" length must be less than or equal to 60 characters long
|
| 4 |
+
remote: ^[[31m-------------------------------------------------------------------------
|
| 5 |
+
remote: -------------------------------------------------------------------------
|
| 6 |
+
remote: Please find the documentation at:
|
| 7 |
+
remote: https://huggingface.co/docs/hub/model-cards#model-card-metadata
|
| 8 |
+
remote:
|
| 9 |
+
remote: -------------------------------------------------------------------------
|
| 10 |
+
To https://huggingface.co/spaces/ANXLOG/LOGOS-SPCW-Matroska
|
| 11 |
+
! [remote rejected] main -> main (pre-receive hook declined)
|
| 12 |
+
error: failed to push some refs to 'https://huggingface.co/spaces/ANXLOG/LOGOS-SPCW-Matroska'
|