GitHub Copilot commited on
Commit
45cef88
·
1 Parent(s): 04f7887

Feature: Add Image Analyzer tab and HF secrets config

Browse files
Files changed (3) hide show
  1. app.py +51 -0
  2. logos/image_analyzer.py +204 -0
  3. 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'