daemon03 commited on
Commit
fd1723f
Β·
verified Β·
1 Parent(s): 7c6501b

UI update

Browse files
Files changed (1) hide show
  1. src/app.py +300 -186
src/app.py CHANGED
@@ -14,93 +14,190 @@ from quadtree_engine import (
14
  )
15
 
16
  st.set_page_config(
17
- page_title="QuadTree Image Engine",
18
- page_icon="🌲",
19
  layout="wide",
20
- initial_sidebar_state="expanded"
21
  )
22
 
23
  # ── CSS ──────────────────────────────────────────────────────────────────────
24
  st.markdown("""
25
  <style>
26
- @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500;700&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
27
 
28
  html, body, [data-testid="stAppViewContainer"], .stApp {
29
- background: #0d0d0d !important;
30
- color: #f0f0f0 !important;
31
- font-family: 'IBM Plex Sans', sans-serif !important;
32
  }
33
 
34
  header[data-testid="stHeader"] {
35
- display: none !important;
36
  }
37
 
38
-
39
- h1, h2, h3, h4, h5, h6, [data-testid="stMarkdownContainer"] h1, [data-testid="stMarkdownContainer"] h2, [data-testid="stMarkdownContainer"] h3 {
40
- font-family: 'DM Mono', monospace !important;
 
 
41
  }
42
 
43
- [data-testid="stSidebar"] {
44
- background: #141414 !important;
45
- border-right: 1px solid #2a2a2a !important;
 
46
  }
47
 
 
48
  .stButton > button {
49
- background-color: #00ff87 !important;
50
- color: #0d0d0d !important;
51
  border: none !important;
52
- border-radius: 4px !important;
53
- font-family: 'DM Mono', monospace !important;
54
- font-weight: 700 !important;
 
 
55
  width: 100%;
56
- margin-top: 1rem;
57
  }
58
 
59
  .stButton > button:hover {
60
- background-color: #00cc6a !important;
61
- color: #000000 !important;
 
62
  }
63
 
 
64
  .stDownloadButton > button {
65
- background-color: transparent !important;
66
- color: #00ff87 !important;
67
- border: 1px solid #00ff87 !important;
68
- border-radius: 4px !important;
69
- font-family: 'DM Mono', monospace !important;
70
- font-weight: 700 !important;
 
 
 
71
  }
72
 
73
  .stDownloadButton > button:hover {
74
- background-color: #00ff87 !important;
75
- color: #0d0d0d !important;
 
 
 
 
 
 
 
 
76
  }
77
 
78
- .accent-text { color: #00ff87; }
79
- .warning-text { color: #ffb700; }
80
- .error-text { color: #ff4444; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
  .step-row {
83
  display: flex;
84
  align-items: center;
85
  margin-bottom: 0.5rem;
86
- background-color: #141414;
87
- border: 1px solid #2a2a2a;
88
  padding: 0.75rem;
 
89
  }
90
  .step-num {
91
- color: #00ff87;
92
- font-family: 'DM Mono', monospace;
93
  font-weight: 700;
94
  margin-right: 1rem;
95
  min-width: 60px;
96
  }
97
 
98
  .code-block-custom {
99
- background-color: #141414;
100
- border: 1px solid #2a2a2a;
101
  padding: 1rem;
102
- font-family: 'DM Mono', monospace;
103
- color: #f0f0f0;
 
 
 
 
 
 
 
 
 
104
  }
105
  </style>
106
  """, unsafe_allow_html=True)
@@ -161,116 +258,122 @@ def count_nodes_and_leaves(node, depth=0):
161
  max_d = max(c[2] for c in counts)
162
  return total, leaves, max_d
163
 
164
- # ── Sidebar ──────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- with st.sidebar:
167
- st.markdown("### ⬆ INPUT")
 
168
 
169
- input_options = ["Upload file"]
170
- has_ppms = os.path.isdir(PPM_DIR) and len(list_ppms()) > 0
171
- if has_ppms:
172
- input_options.append("Use repo PPM")
173
-
174
- input_mode = st.radio("Source", input_options, label_visibility="collapsed")
175
 
176
  arr1 = None
177
  file_id = None
 
178
 
179
- if input_mode == "Upload file":
180
- up1 = st.file_uploader("Primary image", type=["ppm","png","jpg","jpeg"], key="u1")
181
- if up1:
182
- file_id = f"{up1.name}_{up1.size}"
183
- try:
184
- arr1, _ = load_array(uploaded=up1)
185
- except ValueError as e:
186
- st.error(str(e))
187
- else:
188
- sel = st.selectbox("Select PPM", list_ppms())
189
- if sel:
190
- file_id = f"repo_{sel}"
191
- try:
192
- arr1, _ = load_array(ppm_name=sel)
193
- except ValueError as e:
194
- st.error(str(e))
195
-
196
- # Bug 5: Image Size Guard
197
- if arr1 is not None:
198
- MAX_DIM = 2048
199
- h, w = arr1.shape[:2]
200
- if h > MAX_DIM or w > MAX_DIM:
201
- st.markdown(f"<div style='color: #ffb700; font-size: 0.85rem; margin-bottom: 1rem; border: 1px solid #ffb700; padding: 0.5rem;'>⚠️ Image is {w}Γ—{h}. Images over {MAX_DIM}px may be slow or crash. Consider resizing first.</div>", unsafe_allow_html=True)
202
-
203
- # Bug 6: Stale Result Flash (immediately wipe on new file)
204
- if file_id != st.session_state.get("last_file_id"):
205
- st.session_state.pop("result", None)
206
- st.session_state.pop("tree", None)
207
- st.session_state.pop("op_done", None)
208
- st.session_state["last_file_id"] = file_id
209
 
210
- st.markdown("---")
211
- st.markdown("### βš™ OPERATION")
212
-
213
- OP_MAP = {
214
- "Compress Only": "compress_only",
215
- "Grayscale": "grayscale",
216
- "Negative": "negative",
217
- "Sepia": "sepia",
218
- "Brighten": "brighten",
219
- "Mirror (Horizontal)": "mirror",
220
- "Flip (Vertical)": "water",
221
- "Rotate Left 90Β°": "rotate_left",
222
- "Rotate Right 90Β°": "rotate_right",
223
- "Blend / Union": "union",
224
- }
225
-
226
- operation = st.selectbox("Operation", list(OP_MAP.keys()), label_visibility="collapsed")
227
- op_key = OP_MAP[operation]
228
-
229
- arr2 = None
230
- if op_key == "union":
231
- st.markdown("<div style='font-family: \"DM Mono\", monospace; font-size: 0.85rem;'>Second image for blending:</div>", unsafe_allow_html=True)
232
  if input_mode == "Upload file":
233
- up2 = st.file_uploader("Second image", type=["ppm","png","jpg","jpeg"], key="u2")
234
- if up2:
 
235
  try:
236
- arr2, _ = load_array(uploaded=up2)
237
  except ValueError as e:
238
  st.error(str(e))
239
  else:
240
- sel2 = st.selectbox("Second PPM", list_ppms(), key="sel2")
241
- if sel2:
 
242
  try:
243
- arr2, _ = load_array(ppm_name=sel2)
244
  except ValueError as e:
245
  st.error(str(e))
246
-
247
- st.markdown("---")
248
- st.markdown("### β—Ž THRESHOLD")
249
- threshold = st.slider("Quality vs Compression", 1, 500, 30, label_visibility="collapsed")
250
-
251
- if threshold <= 30:
252
- st.markdown("<div style='color: #00ff87; font-family: \"DM Mono\", monospace; font-weight: bold;'>● LOSSLESS</div>", unsafe_allow_html=True)
253
- elif threshold <= 100:
254
- st.markdown("<div style='color: #ffb700; font-family: \"DM Mono\", monospace; font-weight: bold;'>● BALANCED</div>", unsafe_allow_html=True)
255
- else:
256
- st.markdown("<div style='color: #ff4444; font-family: \"DM Mono\", monospace; font-weight: bold;'>● LOSSY</div>", unsafe_allow_html=True)
257
 
258
- run = st.button("β–Ά RUN", disabled=(arr1 is None))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
- st.markdown("<br>", unsafe_allow_html=True)
261
- download_placeholder = st.empty()
262
-
263
-
264
- # ── Main Area ────────────────────────────────────────────────────────────────
265
 
266
- tab1, tab2, tab3 = st.tabs(["IMAGE VIEW", "ALGORITHM EXPLORER", "C SOURCE"])
 
 
 
 
 
267
 
268
- with tab1:
269
  if run and arr1 is not None:
270
  st.session_state.pop("result", None)
271
  st.session_state.pop("tree", None)
272
 
273
- with st.spinner("Processing..."):
274
  try:
275
  if op_key == "compress_only":
276
  padded, oh, ow = pad_to_square_pow2(arr1)
@@ -289,7 +392,6 @@ with tab1:
289
  computed, tree = process_image(arr1, op_key, threshold, return_tree=True)
290
 
291
  if computed is not None:
292
- # Bug 1 Fix: Store result and tree together, avoid rebuilding
293
  st.session_state["result"] = computed
294
  st.session_state["tree"] = tree
295
  st.session_state["op_done"] = operation
@@ -302,65 +404,77 @@ with tab1:
302
  result_arr = st.session_state.get("result")
303
  tree = st.session_state.get("tree")
304
 
 
305
  if arr1 is not None:
306
- with st.container():
307
- c1, c2 = st.columns(2)
308
- with c1:
309
- st.markdown("<div style='font-family: \"DM Mono\", monospace; margin-bottom: 0.5rem; color: #555;'>BEFORE</div>", unsafe_allow_html=True)
310
- st.image(arr_to_pil(arr1), use_container_width=True)
311
- st.markdown(f"<div style='font-family: \"DM Mono\", monospace; color: #555; text-align: center;'>{arr1.shape[1]} Γ— {arr1.shape[0]}</div>", unsafe_allow_html=True)
312
- with c2:
313
- st.markdown("<div style='font-family: \"DM Mono\", monospace; margin-bottom: 0.5rem; color: #555;'>AFTER</div>", unsafe_allow_html=True)
314
- if result_arr is not None:
315
- st.image(arr_to_pil(result_arr), use_container_width=True)
316
- op_str = st.session_state.get("op_done", "")
317
- th_str = st.session_state.get("thresh_done", "")
318
- st.markdown(f"<div style='font-family: \"DM Mono\", monospace; color: #555; text-align: center;'>{op_str} Β· t={th_str}</div>", unsafe_allow_html=True)
319
- else:
320
- st.markdown("<div style='border: 1px solid #2a2a2a; height: 300px; display: flex; align-items: center; justify-content: center; color: #555; font-family: \"DM Mono\", monospace; background-color: #141414;'>[ RESULT IMAGE ]</div>", unsafe_allow_html=True)
321
-
 
 
 
 
 
 
 
 
 
 
 
322
  if result_arr is not None and tree is not None:
323
  total_nodes, leaves, max_d = count_nodes_and_leaves(tree)
324
  total_pixels = arr1.shape[0] * arr1.shape[1]
325
  ratio = total_pixels / total_nodes if total_nodes > 0 else 0
326
 
327
- st.markdown("<div style='font-family: \"DM Mono\", monospace; color: #555; margin-top: 2rem; margin-bottom: 0.5rem;'>QUADTREE STATS</div>", unsafe_allow_html=True)
328
-
329
- st.markdown(f"""
330
- <div style='display: flex; border: 1px solid #2a2a2a; background-color: #141414;'>
331
- <div style='flex: 1; border-right: 1px solid #2a2a2a; padding: 1rem; text-align: center;'>
332
- <div style='color: #555; font-family: "DM Mono", monospace; font-size: 0.8rem; margin-bottom: 0.5rem;'>TOTAL NODES</div>
333
- <div style='color: #00ff87; font-family: "DM Mono", monospace; font-size: 1.5rem; font-weight: 700;'>{total_nodes:,}</div>
334
- <div style='color: #00ff87; font-size: 0.75rem; font-family: "DM Mono", monospace; margin-top: 4px;'>{ratio:.1f}Γ— fewer nodes</div>
335
- </div>
336
- <div style='flex: 1; border-right: 1px solid #2a2a2a; padding: 1rem; text-align: center;'>
337
- <div style='color: #555; font-family: "DM Mono", monospace; font-size: 0.8rem; margin-bottom: 0.5rem;'>LEAF NODES</div>
338
- <div style='color: #00ff87; font-family: "DM Mono", monospace; font-size: 1.5rem; font-weight: 700;'>{leaves:,}</div>
339
  </div>
340
- <div style='flex: 1; border-right: 1px solid #2a2a2a; padding: 1rem; text-align: center;'>
341
- <div style='color: #555; font-family: "DM Mono", monospace; font-size: 0.8rem; margin-bottom: 0.5rem;'>MAX DEPTH</div>
342
- <div style='color: #00ff87; font-family: "DM Mono", monospace; font-size: 1.5rem; font-weight: 700;'>{max_d}</div>
343
  </div>
344
- <div style='flex: 1; border-right: 1px solid #2a2a2a; padding: 1rem; text-align: center;'>
345
- <div style='color: #555; font-family: "DM Mono", monospace; font-size: 0.8rem; margin-bottom: 0.5rem;'>TOTAL PIXELS</div>
346
- <div style='color: #00ff87; font-family: "DM Mono", monospace; font-size: 1.5rem; font-weight: 700;'>{total_pixels:,}</div>
347
  </div>
348
- <div style='flex: 1; padding: 1rem; text-align: center;'>
349
- <div style='color: #555; font-family: "DM Mono", monospace; font-size: 0.8rem; margin-bottom: 0.5rem;'>THRESHOLD</div>
350
- <div style='color: #00ff87; font-family: "DM Mono", monospace; font-size: 1.5rem; font-weight: 700;'>{st.session_state.get('thresh_done')}</div>
351
  </div>
352
  </div>
353
- """, unsafe_allow_html=True)
354
 
355
- with download_placeholder:
 
 
356
  img_pil = arr_to_pil(result_arr)
357
  buf = io.BytesIO()
358
  img_pil.save(buf, format="PNG")
359
- st.download_button("↓ DOWNLOAD RESULT", buf.getvalue(), "result.png", "image/png")
360
 
361
  else:
362
- st.markdown("<div style='border: 1px solid #2a2a2a; padding: 6rem; text-align: center; color: #555; font-family: \"DM Mono\", monospace; background-color: #141414;'>[ AWAITING INPUT ]</div>", unsafe_allow_html=True)
363
-
 
 
 
 
364
 
365
  with tab2:
366
  st.markdown("### 1. What is a Quadtree?")
@@ -387,7 +501,7 @@ with tab2:
387
 
388
  st.markdown("---")
389
  st.markdown("### 2. How Compression Works")
390
- st.markdown("<div style='color: #555; margin-bottom: 1rem;'>See how threshold controls quality vs. compression</div>", unsafe_allow_html=True)
391
 
392
  @st.cache_data
393
  def get_synthetic_image():
@@ -426,29 +540,29 @@ with tab2:
426
  st.markdown("### 4. Color Filters β€” How They Work on the Tree")
427
  fc1, fc2, fc3, fc4 = st.columns(4)
428
  with fc1:
429
- with st.container(border=True):
430
  st.markdown("**Grayscale**")
431
  st.code("L = 0.299R + 0.587G + 0.114B\nR'=L, G'=L, B'=L")
432
- st.caption("Weights green channel most heavily (human eye is most sensitive to green)")
433
  with fc2:
434
- with st.container(border=True):
435
  st.markdown("**Negative**")
436
  st.code("R' = 255 - R\nG' = 255 - G\nB' = 255 - B")
437
- st.caption("Inverts each channel β€” dark becomes light, colors become complementary")
438
  with fc3:
439
- with st.container(border=True):
440
  st.markdown("**Sepia**")
441
- st.code("R' = 0.393R + 0.769G + 0.189B\nG' = ...\nB' = ...")
442
- st.caption("Warm brownish tones by mixing channels β€” mimics aged photographic paper")
443
  with fc4:
444
- with st.container(border=True):
445
  st.markdown("**Brighten**")
446
- st.code("R' = min(255, R*1.3)\nG' = min(255, G*1.3)\nB' = min(255, B*1.3)")
447
- st.caption("Scales all channels up β€” clips at 255 to avoid overflow")
448
 
449
  st.markdown("---")
450
  st.markdown("### 5. Spatial Transforms β€” Pointer Swaps")
451
- st.markdown("<div class='accent-text'><strong>No pixel data is ever copied. Only 4 pointer assignments per node.</strong></div><br>", unsafe_allow_html=True)
452
  tc1, tc2 = st.columns(2)
453
  with tc1:
454
  st.code("""MIRROR (horizontal):
@@ -511,7 +625,7 @@ Averaging formula: `result.R = (t1.R + t2.R) / 2`
511
  | Rotate/Mirror | O(k) | O(1) | Only pointer swaps |
512
  | Union | O(min(k1,k2)) | O(min(k1,k2)) | Bounded by smaller tree |
513
 
514
- <div style="border-left: 4px solid #00ff87; padding-left: 1rem; margin-top: 1rem; color: #f0f0f0;">
515
  <strong>Key Takeaway:</strong> Filters and transforms run on the compressed tree β€” they're O(nodes) not O(pixels). At threshold=100, a 512Γ—512 image (262K pixels) may have fewer than 5,000 nodes.
516
  </div>
517
  """, unsafe_allow_html=True)
@@ -520,10 +634,10 @@ Averaging formula: `result.R = (t1.R + t2.R) / 2`
520
  with tab3:
521
  st.markdown("### C Source vs Python")
522
  st.markdown("""
523
- <div style="background-color: #141414; border: 1px solid #2a2a2a; padding: 1rem; margin-bottom: 1rem;">
524
  πŸ”— View the full C implementation on GitHub:<br>
525
  <a href="https://github.com/Harshwardhan-Deshmukh03/Image-Manipulation-QuadTree.git" target="_blank"
526
- style="color:#00ff87;font-weight:600;text-decoration:none;">
527
  github.com/Harshwardhan-Deshmukh03/Image-Manipulation-QuadTree
528
  </a>
529
  </div>
 
14
  )
15
 
16
  st.set_page_config(
17
+ page_title="Image Manipulation Engine",
18
+ page_icon="✨",
19
  layout="wide",
20
+ initial_sidebar_state="collapsed"
21
  )
22
 
23
  # ── CSS ──────────────────────────────────────────────────────────────────────
24
  st.markdown("""
25
  <style>
26
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
27
 
28
  html, body, [data-testid="stAppViewContainer"], .stApp {
29
+ background: #FAF6F0 !important; /* Claude's warm light orange/beige background */
30
+ color: #2D2D2D !important;
31
+ font-family: 'Inter', sans-serif !important;
32
  }
33
 
34
  header[data-testid="stHeader"] {
35
+ background: transparent !important;
36
  }
37
 
38
+ /* Hide deploy buttons and toolbar */
39
+ header[data-testid="stHeader"] .stAppDeployButton,
40
+ header[data-testid="stHeader"] [data-testid="stToolbar"],
41
+ header[data-testid="stHeader"] [data-testid="stHeaderActionElements"] {
42
+ display: none !important;
43
  }
44
 
45
+ h1, h2, h3, h4, h5, h6, [data-testid="stMarkdownContainer"] h1 {
46
+ font-family: 'Inter', sans-serif !important;
47
+ font-weight: 600 !important;
48
+ color: #1A1A1A !important;
49
  }
50
 
51
+ /* Claude UI Buttons */
52
  .stButton > button {
53
+ background-color: #1A1A1A !important;
54
+ color: #FFFFFF !important;
55
  border: none !important;
56
+ border-radius: 8px !important;
57
+ font-family: 'Inter', sans-serif !important;
58
+ font-weight: 500 !important;
59
+ padding: 0.5rem 1rem !important;
60
+ transition: all 0.2s ease;
61
  width: 100%;
 
62
  }
63
 
64
  .stButton > button:hover {
65
+ background-color: #333333 !important;
66
+ transform: translateY(-1px);
67
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
68
  }
69
 
70
+ /* Claude UI Secondary Button (Download) */
71
  .stDownloadButton > button {
72
+ background-color: #FFFFFF !important;
73
+ color: #1A1A1A !important;
74
+ border: 1px solid #D1D1D1 !important;
75
+ border-radius: 8px !important;
76
+ font-family: 'Inter', sans-serif !important;
77
+ font-weight: 500 !important;
78
+ width: 100%;
79
+ padding: 0.5rem 1rem !important;
80
+ transition: all 0.2s ease;
81
  }
82
 
83
  .stDownloadButton > button:hover {
84
+ background-color: #F8F8F8 !important;
85
+ border-color: #1A1A1A !important;
86
+ }
87
+
88
+ /* Inputs */
89
+ .stSelectbox > div > div {
90
+ background-color: #FFFFFF !important;
91
+ border-radius: 8px !important;
92
+ border: 1px solid #E5E0D8 !important;
93
+ color: #1A1A1A !important;
94
  }
95
 
96
+ .stTextInput > div > div > input {
97
+ background-color: #FFFFFF !important;
98
+ border-radius: 8px !important;
99
+ border: 1px solid #E5E0D8 !important;
100
+ color: #1A1A1A !important;
101
+ }
102
+
103
+ /* Slider Customization */
104
+ .stSlider [data-testid="stTickBar"] {
105
+ background-color: #E5E0D8 !important;
106
+ }
107
+ .stSlider [data-testid="stSliderThumb"] {
108
+ background-color: #D97757 !important; /* Claude's Peach/Orange accent */
109
+ }
110
+
111
+ /* Tabs Styling */
112
+ .stTabs [data-baseweb="tab-list"] {
113
+ gap: 2rem;
114
+ }
115
+ .stTabs [data-baseweb="tab"] {
116
+ font-family: 'Inter', sans-serif !important;
117
+ font-weight: 500 !important;
118
+ padding-bottom: 0.5rem !important;
119
+ color: #777777 !important;
120
+ }
121
+ .stTabs [aria-selected="true"] {
122
+ color: #1A1A1A !important;
123
+ border-bottom-color: #D97757 !important;
124
+ }
125
+
126
+ /* Hide Sidebar */
127
+ [data-testid="stSidebar"] {
128
+ display: none !important;
129
+ }
130
+ [data-testid="collapsedControl"] {
131
+ display: none !important;
132
+ }
133
+
134
+ /* Custom CSS classes for components */
135
+ .claude-stats-container {
136
+ display: flex;
137
+ gap: 1rem;
138
+ margin-top: 1.5rem;
139
+ flex-wrap: wrap;
140
+ }
141
+ .claude-stat-card {
142
+ flex: 1;
143
+ background-color: #FFFFFF;
144
+ border: 1px solid #E5E0D8;
145
+ border-radius: 12px;
146
+ padding: 1.25rem;
147
+ text-align: center;
148
+ min-width: 140px;
149
+ box-shadow: 0 2px 8px rgba(0,0,0,0.02);
150
+ }
151
+ .claude-stat-label {
152
+ font-size: 0.75rem;
153
+ color: #888888;
154
+ text-transform: uppercase;
155
+ letter-spacing: 0.05em;
156
+ margin-bottom: 0.5rem;
157
+ font-weight: 600;
158
+ }
159
+ .claude-stat-value {
160
+ font-size: 1.5rem;
161
+ font-weight: 700;
162
+ color: #1A1A1A;
163
+ }
164
+ .claude-stat-sub {
165
+ font-size: 0.75rem;
166
+ color: #D97757;
167
+ margin-top: 0.25rem;
168
+ }
169
 
170
  .step-row {
171
  display: flex;
172
  align-items: center;
173
  margin-bottom: 0.5rem;
174
+ background-color: #FFFFFF;
175
+ border: 1px solid #E5E0D8;
176
  padding: 0.75rem;
177
+ border-radius: 8px;
178
  }
179
  .step-num {
180
+ color: #D97757;
 
181
  font-weight: 700;
182
  margin-right: 1rem;
183
  min-width: 60px;
184
  }
185
 
186
  .code-block-custom {
187
+ background-color: #F8F9FA;
188
+ border: 1px solid #E5E0D8;
189
  padding: 1rem;
190
+ border-radius: 8px;
191
+ color: #2D2D2D;
192
+ }
193
+
194
+ hr {
195
+ border-color: #E5E0D8 !important;
196
+ }
197
+
198
+ /* Make radio button labels dark */
199
+ [data-testid="stRadio"] label {
200
+ color: #1A1A1A !important;
201
  }
202
  </style>
203
  """, unsafe_allow_html=True)
 
258
  max_d = max(c[2] for c in counts)
259
  return total, leaves, max_d
260
 
261
+ # ── App Header ───────────────────────────────────────────────────────────────
262
+ st.markdown("<h1 style='text-align: center; margin-bottom: 2rem; font-size: 2.5rem; letter-spacing: -0.02em;'>Image Manipulation Engine</h1>", unsafe_allow_html=True)
263
+
264
+ # ── Main Area ────────────────────────────────────────────────────────────────
265
+
266
+ tab1, tab2, tab3 = st.tabs(["IMAGE VIEW", "ALGORITHM EXPLORER", "C SOURCE"])
267
+
268
+ OP_MAP = {
269
+ "Compress Only": "compress_only",
270
+ "Grayscale": "grayscale",
271
+ "Negative": "negative",
272
+ "Sepia": "sepia",
273
+ "Brighten": "brighten",
274
+ "Mirror (Horizontal)": "mirror",
275
+ "Flip (Vertical)": "water",
276
+ "Rotate Left 90Β°": "rotate_left",
277
+ "Rotate Right 90Β°": "rotate_right",
278
+ "Blend / Union": "union",
279
+ }
280
 
281
+ with tab1:
282
+ # --- CONTROL PANEL ---
283
+ st.markdown("<div style='background-color: #FFFFFF; padding: 1.5rem; border-radius: 12px; border: 1px solid #E5E0D8; margin-bottom: 2rem; box-shadow: 0 4px 12px rgba(0,0,0,0.03);'>", unsafe_allow_html=True)
284
 
285
+ col1, col2, col3 = st.columns([1, 1, 1])
 
 
 
 
 
286
 
287
  arr1 = None
288
  file_id = None
289
+ arr2 = None
290
 
291
+ with col1:
292
+ st.markdown("<div style='color: #1A1A1A; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.95rem;'>1. Input Source</div>", unsafe_allow_html=True)
293
+ input_options = ["Upload file"]
294
+ has_ppms = os.path.isdir(PPM_DIR) and len(list_ppms()) > 0
295
+ if has_ppms:
296
+ input_options.append("Use Sample Images")
297
+
298
+ input_mode = st.radio("Source", input_options, label_visibility="collapsed", horizontal=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  if input_mode == "Upload file":
301
+ up1 = st.file_uploader("Primary image", type=["ppm","png","jpg","jpeg"], key="u1", label_visibility="collapsed")
302
+ if up1:
303
+ file_id = f"{up1.name}_{up1.size}"
304
  try:
305
+ arr1, _ = load_array(uploaded=up1)
306
  except ValueError as e:
307
  st.error(str(e))
308
  else:
309
+ sel = st.selectbox("Select PPM", list_ppms(), label_visibility="collapsed")
310
+ if sel:
311
+ file_id = f"repo_{sel}"
312
  try:
313
+ arr1, _ = load_array(ppm_name=sel)
314
  except ValueError as e:
315
  st.error(str(e))
316
+
317
+ with col2:
318
+ st.markdown("<div style='color: #1A1A1A; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.95rem;'>2. Operation</div>", unsafe_allow_html=True)
319
+ # Spacer to align with the radio button in col1
320
+ st.markdown("<div style='height: 38px; margin-bottom: 0px;'></div>", unsafe_allow_html=True)
 
 
 
 
 
 
321
 
322
+ operation = st.selectbox("Operation", list(OP_MAP.keys()), label_visibility="collapsed")
323
+ op_key = OP_MAP[operation]
324
+
325
+ if op_key == "union":
326
+ st.markdown("<div style='font-size: 0.85rem; color: #777; margin-top: 0.5rem; margin-bottom: 0.2rem;'>Second image for blending:</div>", unsafe_allow_html=True)
327
+ if input_mode == "Upload file":
328
+ up2 = st.file_uploader("Second image", type=["ppm","png","jpg","jpeg"], key="u2", label_visibility="collapsed")
329
+ if up2:
330
+ try:
331
+ arr2, _ = load_array(uploaded=up2)
332
+ except ValueError as e:
333
+ st.error(str(e))
334
+ else:
335
+ sel2 = st.selectbox("Second PPM", list_ppms(), key="sel2", label_visibility="collapsed")
336
+ if sel2:
337
+ try:
338
+ arr2, _ = load_array(ppm_name=sel2)
339
+ except ValueError as e:
340
+ st.error(str(e))
341
+
342
+ with col3:
343
+ st.markdown("<div style='color: #1A1A1A; font-weight: 600; margin-bottom: 0.5rem; font-size: 0.95rem;'>3. Configuration</div>", unsafe_allow_html=True)
344
+ # Spacer to align with the radio button in col1
345
+ st.markdown("<div style='height: 38px; margin-bottom: 0px;'></div>", unsafe_allow_html=True)
346
+
347
+ threshold = st.slider("Quality vs Compression", 1, 500, 30, label_visibility="collapsed")
348
+
349
+ if threshold <= 30:
350
+ st.markdown("<div style='color: #777; font-size: 0.8rem; margin-top: -10px;'>Mode: <span style='color: #10B981; font-weight: 600;'>Lossless</span></div>", unsafe_allow_html=True)
351
+ elif threshold <= 100:
352
+ st.markdown("<div style='color: #777; font-size: 0.8rem; margin-top: -10px;'>Mode: <span style='color: #F59E0B; font-weight: 600;'>Balanced</span></div>", unsafe_allow_html=True)
353
+ else:
354
+ st.markdown("<div style='color: #777; font-size: 0.8rem; margin-top: -10px;'>Mode: <span style='color: #EF4444; font-weight: 600;'>Lossy</span></div>", unsafe_allow_html=True)
355
+
356
+ st.markdown("<hr style='margin-top: 1.5rem; margin-bottom: 1.5rem;'>", unsafe_allow_html=True)
357
 
358
+ col_empty, col_btn = st.columns([4, 2])
359
+ with col_btn:
360
+ run = st.button("✨ Generate Result", disabled=(arr1 is None))
361
+
362
+ st.markdown("</div>", unsafe_allow_html=True)
363
 
364
+ # State management
365
+ if file_id != st.session_state.get("last_file_id"):
366
+ st.session_state.pop("result", None)
367
+ st.session_state.pop("tree", None)
368
+ st.session_state.pop("op_done", None)
369
+ st.session_state["last_file_id"] = file_id
370
 
371
+ # --- IMAGE PROCESSING ---
372
  if run and arr1 is not None:
373
  st.session_state.pop("result", None)
374
  st.session_state.pop("tree", None)
375
 
376
+ with st.spinner("Processing image..."):
377
  try:
378
  if op_key == "compress_only":
379
  padded, oh, ow = pad_to_square_pow2(arr1)
 
392
  computed, tree = process_image(arr1, op_key, threshold, return_tree=True)
393
 
394
  if computed is not None:
 
395
  st.session_state["result"] = computed
396
  st.session_state["tree"] = tree
397
  st.session_state["op_done"] = operation
 
404
  result_arr = st.session_state.get("result")
405
  tree = st.session_state.get("tree")
406
 
407
+ # --- RESULTS DISPLAY ---
408
  if arr1 is not None:
409
+ MAX_DIM = 2048
410
+ h, w = arr1.shape[:2]
411
+ if h > MAX_DIM or w > MAX_DIM:
412
+ st.warning(f"Image is {w}Γ—{h}. Images over {MAX_DIM}px may be slow or crash. Consider resizing first.")
413
+
414
+ rc1, rc2 = st.columns(2)
415
+ with rc1:
416
+ st.markdown("<div style='text-align: center; color: #888; margin-bottom: 1rem; font-weight: 600; letter-spacing: 0.05em; font-size: 0.85rem;'>ORIGINAL</div>", unsafe_allow_html=True)
417
+ st.image(arr_to_pil(arr1), use_container_width=True)
418
+ st.markdown(f"<div style='text-align: center; color: #777; font-size: 0.85rem; margin-top: 0.5rem;'>{arr1.shape[1]} Γ— {arr1.shape[0]} px</div>", unsafe_allow_html=True)
419
+
420
+ with rc2:
421
+ st.markdown("<div style='text-align: center; color: #888; margin-bottom: 1rem; font-weight: 600; letter-spacing: 0.05em; font-size: 0.85rem;'>PROCESSED</div>", unsafe_allow_html=True)
422
+ if result_arr is not None:
423
+ st.image(arr_to_pil(result_arr), use_container_width=True)
424
+ op_str = st.session_state.get("op_done", "")
425
+ th_str = st.session_state.get("thresh_done", "")
426
+ st.markdown(f"<div style='text-align: center; color: #777; font-size: 0.85rem; margin-top: 0.5rem;'>{op_str} (Threshold: {th_str})</div>", unsafe_allow_html=True)
427
+ else:
428
+ st.markdown("""
429
+ <div style='background-color: #FFFFFF; border: 1px dashed #D1D1D1; border-radius: 12px; height: 300px; display: flex; align-items: center; justify-content: center; flex-direction: column;'>
430
+ <span style='font-size: 2rem; margin-bottom: 1rem;'>✨</span>
431
+ <span style='color: #888; font-family: "Inter", sans-serif;'>Ready to process</span>
432
+ </div>
433
+ """, unsafe_allow_html=True)
434
+
435
+ # QuadTree Stats
436
  if result_arr is not None and tree is not None:
437
  total_nodes, leaves, max_d = count_nodes_and_leaves(tree)
438
  total_pixels = arr1.shape[0] * arr1.shape[1]
439
  ratio = total_pixels / total_nodes if total_nodes > 0 else 0
440
 
441
+ st.markdown("""
442
+ <div class='claude-stats-container'>
443
+ <div class='claude-stat-card'>
444
+ <div class='claude-stat-label'>Total Nodes</div>
445
+ <div class='claude-stat-value'>{nodes}</div>
446
+ <div class='claude-stat-sub'>{ratio:.1f}x fewer nodes</div>
 
 
 
 
 
 
447
  </div>
448
+ <div class='claude-stat-card'>
449
+ <div class='claude-stat-label'>Leaf Nodes</div>
450
+ <div class='claude-stat-value'>{leaves}</div>
451
  </div>
452
+ <div class='claude-stat-card'>
453
+ <div class='claude-stat-label'>Max Depth</div>
454
+ <div class='claude-stat-value'>{max_d}</div>
455
  </div>
456
+ <div class='claude-stat-card'>
457
+ <div class='claude-stat-label'>Total Pixels</div>
458
+ <div class='claude-stat-value'>{pixels}</div>
459
  </div>
460
  </div>
461
+ """.replace('{nodes}', f"{total_nodes:,}").replace('{ratio}', f"{ratio}").replace('{leaves}', f"{leaves:,}").replace('{max_d}', str(max_d)).replace('{pixels}', f"{total_pixels:,}"), unsafe_allow_html=True)
462
 
463
+ st.markdown("<br>", unsafe_allow_html=True)
464
+ col_empty, col_dl = st.columns([4, 1])
465
+ with col_dl:
466
  img_pil = arr_to_pil(result_arr)
467
  buf = io.BytesIO()
468
  img_pil.save(buf, format="PNG")
469
+ st.download_button("Download Result", buf.getvalue(), "result.png", "image/png")
470
 
471
  else:
472
+ st.markdown("""
473
+ <div style='background-color: #FFFFFF; border: 1px dashed #D1D1D1; border-radius: 12px; padding: 4rem; text-align: center; margin-top: 2rem;'>
474
+ <h3 style='color: #1A1A1A; font-weight: 500; margin-bottom: 0.5rem;'>No Image Selected</h3>
475
+ <p style='color: #666;'>Upload an image or select one from the repository to get started.</p>
476
+ </div>
477
+ """, unsafe_allow_html=True)
478
 
479
  with tab2:
480
  st.markdown("### 1. What is a Quadtree?")
 
501
 
502
  st.markdown("---")
503
  st.markdown("### 2. How Compression Works")
504
+ st.markdown("<div style='color: #777; margin-bottom: 1rem;'>See how threshold controls quality vs. compression</div>", unsafe_allow_html=True)
505
 
506
  @st.cache_data
507
  def get_synthetic_image():
 
540
  st.markdown("### 4. Color Filters β€” How They Work on the Tree")
541
  fc1, fc2, fc3, fc4 = st.columns(4)
542
  with fc1:
543
+ with st.container():
544
  st.markdown("**Grayscale**")
545
  st.code("L = 0.299R + 0.587G + 0.114B\nR'=L, G'=L, B'=L")
546
+ st.caption("Weights green channel most heavily")
547
  with fc2:
548
+ with st.container():
549
  st.markdown("**Negative**")
550
  st.code("R' = 255 - R\nG' = 255 - G\nB' = 255 - B")
551
+ st.caption("Inverts each channel β€” dark becomes light")
552
  with fc3:
553
+ with st.container():
554
  st.markdown("**Sepia**")
555
+ st.code("R' = 0.393R + ...\nG' = ...\nB' = ...")
556
+ st.caption("Warm brownish tones by mixing channels")
557
  with fc4:
558
+ with st.container():
559
  st.markdown("**Brighten**")
560
+ st.code("R' = min(255, R*1.3)\nG' = ...\nB' = ...")
561
+ st.caption("Scales all channels up")
562
 
563
  st.markdown("---")
564
  st.markdown("### 5. Spatial Transforms β€” Pointer Swaps")
565
+ st.markdown("<div style='color: #D97757; margin-bottom: 1rem;'><strong>No pixel data is ever copied. Only 4 pointer assignments per node.</strong></div>", unsafe_allow_html=True)
566
  tc1, tc2 = st.columns(2)
567
  with tc1:
568
  st.code("""MIRROR (horizontal):
 
625
  | Rotate/Mirror | O(k) | O(1) | Only pointer swaps |
626
  | Union | O(min(k1,k2)) | O(min(k1,k2)) | Bounded by smaller tree |
627
 
628
+ <div style="border-left: 4px solid #D97757; padding-left: 1rem; margin-top: 1rem; color: #2D2D2D;">
629
  <strong>Key Takeaway:</strong> Filters and transforms run on the compressed tree β€” they're O(nodes) not O(pixels). At threshold=100, a 512Γ—512 image (262K pixels) may have fewer than 5,000 nodes.
630
  </div>
631
  """, unsafe_allow_html=True)
 
634
  with tab3:
635
  st.markdown("### C Source vs Python")
636
  st.markdown("""
637
+ <div style="background-color: #FFFFFF; border: 1px solid #E5E0D8; padding: 1rem; margin-bottom: 1rem; border-radius: 8px;">
638
  πŸ”— View the full C implementation on GitHub:<br>
639
  <a href="https://github.com/Harshwardhan-Deshmukh03/Image-Manipulation-QuadTree.git" target="_blank"
640
+ style="color:#D97757;font-weight:600;text-decoration:none;">
641
  github.com/Harshwardhan-Deshmukh03/Image-Manipulation-QuadTree
642
  </a>
643
  </div>