daemon03 commited on
Commit
5c8cc9d
Β·
verified Β·
1 Parent(s): d144870

Added code files

Browse files
Files changed (3) hide show
  1. src/app.py +560 -0
  2. src/quadtree_engine.py +336 -0
  3. src/test_engine.py +27 -0
src/app.py ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ from PIL import Image
4
+ import io
5
+ import sys
6
+ import os
7
+
8
+ # Bug 3 & 4 fixes - Top level imports, recursion limit
9
+ sys.setrecursionlimit(10000)
10
+
11
+ sys.path.insert(0, os.path.dirname(__file__))
12
+ from quadtree_engine import (
13
+ read_ppm_bytes, process_image, arr_to_pil, compress_image, decompress_image, pad_to_square_pow2
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)
107
+
108
+ # ── Helpers ───────────────────────────────────────────────────────────────────
109
+
110
+ PPM_DIR = os.path.join(os.path.dirname(__file__), "c_project_files")
111
+
112
+ def list_ppms():
113
+ result = []
114
+ if not os.path.isdir(PPM_DIR):
115
+ return result
116
+ for f in sorted(os.listdir(PPM_DIR)):
117
+ if not f.endswith(".ppm"):
118
+ continue
119
+ fpath = os.path.join(PPM_DIR, f)
120
+ try:
121
+ with open(fpath, "rb") as fh:
122
+ header = fh.read(3)
123
+ if header.startswith(b'P6'):
124
+ result.append(f)
125
+ except Exception:
126
+ pass
127
+ return result
128
+
129
+ def load_array(uploaded=None, ppm_name=None):
130
+ if uploaded is not None:
131
+ uploaded.seek(0)
132
+ raw = uploaded.read()
133
+ elif ppm_name:
134
+ path = os.path.join(PPM_DIR, ppm_name)
135
+ with open(path, "rb") as f:
136
+ raw = f.read()
137
+ else:
138
+ return None, None
139
+
140
+ try:
141
+ arr = np.array(Image.open(io.BytesIO(raw)).convert("RGB"), dtype=np.uint8)
142
+ return arr, raw
143
+ except Exception:
144
+ pass
145
+
146
+ try:
147
+ arr = read_ppm_bytes(raw)
148
+ return arr, raw
149
+ except Exception as e:
150
+ fname = uploaded.name if uploaded else ppm_name
151
+ raise ValueError(f"Cannot read '{fname}' as an image.\nDetail: {e}")
152
+
153
+ def count_nodes_and_leaves(node, depth=0):
154
+ if node is None:
155
+ return 0, 0, 0 # total, leaves, max_d
156
+ if node.is_leaf():
157
+ return 1, 1, depth
158
+ counts = [count_nodes_and_leaves(c, depth+1) for c in [node.topLeft, node.topRight, node.bottomLeft, node.bottomRight]]
159
+ total = 1 + sum(c[0] for c in counts)
160
+ leaves = sum(c[1] for c in counts)
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)
277
+ size = padded.shape[0]
278
+ tree = compress_image(padded, 0, 0, size, threshold)
279
+ out = np.zeros((size, size, 3), dtype=np.uint8)
280
+ decompress_image(tree, out, 0, 0, size)
281
+ computed = out[:oh, :ow]
282
+ elif op_key == "union":
283
+ if arr2 is None:
284
+ st.error("Please provide a second image for union.")
285
+ computed, tree = None, None
286
+ else:
287
+ computed, tree = process_image(arr1, "union", threshold, arr2, return_tree=True)
288
+ else:
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
296
+ st.session_state["thresh_done"] = threshold
297
+ except RecursionError:
298
+ st.error("RecursionError: image too large for this threshold. Try a higher threshold.")
299
+ except Exception as e:
300
+ st.error(f"Error: {e}")
301
+
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?")
367
+ c1, c2 = st.columns(2)
368
+ with c1:
369
+ st.code("""
370
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
371
+ β”‚ β”‚ β”‚
372
+ β”‚ TL β”‚ TR β”‚
373
+ β”‚ β”‚ β”‚
374
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
375
+ β”‚ β”‚ β”‚
376
+ β”‚ BL β”‚ BR β”‚
377
+ β”‚ β”‚ β”‚
378
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜
379
+ """, language="text")
380
+ with c2:
381
+ st.markdown("""
382
+ - **`red`, `green`, `blue`**: The average color of this region.
383
+ - **`area`**: The total pixels covered by this node.
384
+ - **Children**: If variance > threshold, the region splits into 4 sub-regions (`topLeft`, `topRight`, `bottomLeft`, `bottomRight`).
385
+ - **Leaf Node**: If variance <= threshold, children are null. The region is colored uniformly.
386
+ """)
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():
394
+ y, x = np.mgrid[0:64, 0:64]
395
+ r = (x * 4) % 255
396
+ g = (y * 4) % 255
397
+ b = ((x + y) * 2) % 255
398
+ noise = np.random.randint(0, 50, (64, 64, 3))
399
+ img = np.stack([r, g, b], axis=-1) + noise
400
+ return np.clip(img, 0, 255).astype(np.uint8)
401
+
402
+ synth_img = get_synthetic_image()
403
+ sim_thresh = st.slider("Simulator Threshold", 1, 200, 50, key="sim_t")
404
+
405
+ sc1, sc2 = st.columns(2)
406
+ with sc1:
407
+ st.image(arr_to_pil(synth_img), caption="Original (64x64)", use_container_width=True)
408
+ with sc2:
409
+ sim_tree = compress_image(synth_img, 0, 0, 64, sim_thresh)
410
+ sim_out = np.zeros((64, 64, 3), dtype=np.uint8)
411
+ decompress_image(sim_tree, sim_out, 0, 0, 64)
412
+ total_sim, leaves_sim, _ = count_nodes_and_leaves(sim_tree)
413
+ st.image(arr_to_pil(sim_out), caption=f"Compressed (Nodes: {total_sim})", use_container_width=True)
414
+
415
+ st.markdown("---")
416
+ st.markdown("### 3. Compression Algorithm Step-by-Step")
417
+ st.markdown("""
418
+ <div class="step-row"><span class="step-num">STEP 1</span><span>Divide image into 4 quadrants</span></div>
419
+ <div class="step-row"><span class="step-num">STEP 2</span><span>Compute mean RGB + variance for each quadrant</span></div>
420
+ <div class="step-row"><span class="step-num">STEP 3</span><span>If variance ≀ threshold: <b>LEAF</b> β†’ store avg color, stop subdividing</span></div>
421
+ <div class="step-row"><span class="step-num">STEP 4</span><span>If variance > threshold: <b>RECURSE</b> into each quadrant with size/2</span></div>
422
+ <div class="step-row"><span class="step-num">STEP 5</span><span>Continue until size=1 pixel or variance ≀ threshold</span></div>
423
+ """, unsafe_allow_html=True)
424
+
425
+ st.markdown("---")
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):
455
+ Before: TL | TR
456
+ -------
457
+ BL | BR
458
+
459
+ After: TR | TL
460
+ -------
461
+ BR | BL""", language="text")
462
+ with tc2:
463
+ st.code("""ROTATE LEFT 90Β°:
464
+ Before: TL | TR
465
+ -------
466
+ BL | BR
467
+
468
+ After: TR | BR
469
+ -------
470
+ TL | BL""", language="text")
471
+
472
+ tc3, tc4 = st.columns(2)
473
+ with tc3:
474
+ st.code("""FLIP (vertical):
475
+ Before: TL | TR
476
+ -------
477
+ BL | BR
478
+
479
+ After: BL | BR
480
+ -------
481
+ TL | TR""", language="text")
482
+ with tc4:
483
+ st.code("""ROTATE RIGHT 90Β°:
484
+ Before: TL | TR
485
+ -------
486
+ BL | BR
487
+
488
+ After: BL | TL
489
+ -------
490
+ BR | TR""", language="text")
491
+
492
+ st.markdown("---")
493
+ st.markdown("### 6. Union / Blend β€” Three Cases")
494
+ st.markdown("""
495
+ - **Case 1:** Both nodes are internal β†’ recurse into all 4 child pairs.
496
+ - **Case 2:** t1 is a leaf, t2 has children β†’ blend t1's solid color with each of t2's children.
497
+ - **Case 3:** t2 is a leaf, t1 has children β†’ blend t2's solid color with each of t1's children.
498
+
499
+ Averaging formula: `result.R = (t1.R + t2.R) / 2`
500
+ *Note: This produces a pixel-perfect 50/50 blend without ever decompressing either image to a pixel buffer.*
501
+ """)
502
+
503
+ st.markdown("---")
504
+ st.markdown("### 7. Complexity Analysis")
505
+ st.markdown("""
506
+ | Operation | Time Complexity | Space Complexity | Notes |
507
+ |---|---|---|---|
508
+ | Compress | O(n log n) | O(n) | n = total pixels |
509
+ | Decompress | O(n) | O(n) | Linear tree traversal |
510
+ | Filter | O(k) | O(k) | k = tree nodes, k β‰ͺ n |
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)
518
+
519
+
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>
530
+ """, unsafe_allow_html=True)
531
+ st.markdown("""
532
+ | C File | Python Equivalent |
533
+ |---|---|
534
+ | `compress.c` | `compress_image()` |
535
+ | `filters.c` | `apply_grayscale()`, `apply_negative()`, etc. |
536
+ | `rotate.c` | `rotate_left()`, `get_mirror_image()`, etc. |
537
+ | `union.c` | `union_of_images()` |
538
+ | `decompress.c` | `decompress_image()` |
539
+ """)
540
+
541
+ c_files = {
542
+ "main.c": "CLI entry point β€” parses flags, orchestrates the full pipeline",
543
+ "compress.c": "Quadtree construction from pixel matrix using variance threshold",
544
+ "decompress.c": "Quadtree β†’ pixel matrix reconstruction",
545
+ "filters.c": "Color filters: grayscale, negative, sepia, brighten",
546
+ "rotate.c": "Spatial transforms: mirror, flip, rotate L/R/180",
547
+ "union.c": "Pixel-level blending of two Quadtrees",
548
+ "suppl.c": "PPM I/O, getMean(), tree serialization helpers",
549
+ "suppl.h": "All struct definitions: pixels, qtNode, qtInfo",
550
+ }
551
+
552
+ st.markdown("<br>", unsafe_allow_html=True)
553
+ for fname, desc in c_files.items():
554
+ fpath = os.path.join(PPM_DIR, fname)
555
+ with st.expander(f"`{fname}` β€” {desc}"):
556
+ if os.path.isfile(fpath):
557
+ with open(fpath) as f:
558
+ st.code(f.read(), language="c")
559
+ else:
560
+ st.info(f"File not found at: {fpath}")
src/quadtree_engine.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quadtree Image Engine - Python implementation of the C quadtree image manipulation.
3
+ Mirrors the logic from compress.c, decompress.c, filters.c, rotate.c, union.c
4
+ """
5
+ import numpy as np
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional
8
+ from PIL import Image
9
+ import io
10
+
11
+
12
+ @dataclass
13
+ class QtNode:
14
+ red: int = 0
15
+ green: int = 0
16
+ blue: int = 0
17
+ area: int = 0
18
+ topLeft: Optional['QtNode'] = field(default=None, repr=False)
19
+ topRight: Optional['QtNode'] = field(default=None, repr=False)
20
+ bottomLeft: Optional['QtNode'] = field(default=None, repr=False)
21
+ bottomRight: Optional['QtNode'] = field(default=None, repr=False)
22
+
23
+ def is_leaf(self):
24
+ return (self.topLeft is None and self.topRight is None and
25
+ self.bottomLeft is None and self.bottomRight is None)
26
+
27
+
28
+ def get_mean(matrix: np.ndarray, x: int, y: int, size: int):
29
+ """Compute average color and variance score for a region. Mirrors getMean() in suppl.c"""
30
+ region = matrix[y:y+size, x:x+size]
31
+ red = int(np.mean(region[:, :, 0]))
32
+ green = int(np.mean(region[:, :, 1]))
33
+ blue = int(np.mean(region[:, :, 2]))
34
+ variance = (
35
+ np.mean((region[:, :, 0].astype(int) - red)**2) +
36
+ np.mean((region[:, :, 1].astype(int) - green)**2) +
37
+ np.mean((region[:, :, 2].astype(int) - blue)**2)
38
+ ) / 3
39
+ return red, green, blue, int(variance)
40
+
41
+
42
+ def compress_image(matrix: np.ndarray, x: int, y: int, size: int, threshold: int) -> QtNode:
43
+ """Build quadtree. Mirrors compressImage() in compress.c"""
44
+ red, green, blue, score = get_mean(matrix, x, y, size)
45
+ node = QtNode(red=red, green=green, blue=blue, area=size * size)
46
+ if size > 1 and score > threshold:
47
+ half = size // 2
48
+ node.topLeft = compress_image(matrix, x, y, half, threshold)
49
+ node.topRight = compress_image(matrix, x + half, y, half, threshold)
50
+ node.bottomRight = compress_image(matrix, x + half, y + half, half, threshold)
51
+ node.bottomLeft = compress_image(matrix, x, y + half, half, threshold)
52
+ return node
53
+
54
+
55
+ def decompress_image(node: QtNode, matrix: np.ndarray, x: int, y: int, size: int):
56
+ """Reconstruct pixel matrix from quadtree. Mirrors decompressImage() in decompress.c"""
57
+ if node.is_leaf():
58
+ matrix[y:y+size, x:x+size, 0] = node.red
59
+ matrix[y:y+size, x:x+size, 1] = node.green
60
+ matrix[y:y+size, x:x+size, 2] = node.blue
61
+ else:
62
+ half = size // 2
63
+ decompress_image(node.topLeft, matrix, x, y, half)
64
+ decompress_image(node.topRight, matrix, x + half, y, half)
65
+ decompress_image(node.bottomRight, matrix, x + half, y + half, half)
66
+ decompress_image(node.bottomLeft, matrix, x, y + half, half)
67
+
68
+
69
+ # ── Filters (mirrors filters.c) ──────────────────────────────────────────────
70
+
71
+ def apply_grayscale(node: QtNode) -> QtNode:
72
+ """True luminance-weighted grayscale.
73
+ Compute single luma value and assign to all 3 channels so the
74
+ output is actually grey (not green-tinted).
75
+ luma = 0.299R + 0.587G + 0.114B (BT.601 standard)
76
+ """
77
+ res = QtNode(area=node.area)
78
+ if not node.is_leaf():
79
+ res.topLeft = apply_grayscale(node.topLeft)
80
+ res.topRight = apply_grayscale(node.topRight)
81
+ res.bottomLeft = apply_grayscale(node.bottomLeft)
82
+ res.bottomRight = apply_grayscale(node.bottomRight)
83
+ luma = int(0.299 * node.red + 0.587 * node.green + 0.114 * node.blue)
84
+ res.red = luma
85
+ res.green = luma
86
+ res.blue = luma
87
+ return res
88
+
89
+
90
+ def apply_negative(node: QtNode) -> QtNode:
91
+ """Invert channels: 255 - value. Mirrors negativeImage() in filters.c"""
92
+ res = QtNode(area=node.area)
93
+ if not node.is_leaf():
94
+ res.topLeft = apply_negative(node.topLeft)
95
+ res.topRight = apply_negative(node.topRight)
96
+ res.bottomLeft = apply_negative(node.bottomLeft)
97
+ res.bottomRight = apply_negative(node.bottomRight)
98
+ res.red = 255 - node.red
99
+ res.green = 255 - node.green
100
+ res.blue = 255 - node.blue
101
+ return res
102
+
103
+
104
+ def apply_sepia(node: QtNode) -> QtNode:
105
+ """Cinematic warm sepia tone. Mirrors sepia() in filters.c"""
106
+ res = QtNode(area=node.area)
107
+ if not node.is_leaf():
108
+ res.topLeft = apply_sepia(node.topLeft)
109
+ res.topRight = apply_sepia(node.topRight)
110
+ res.bottomLeft = apply_sepia(node.bottomLeft)
111
+ res.bottomRight = apply_sepia(node.bottomRight)
112
+ r, g, b = node.red, node.green, node.blue
113
+ res.red = min(255, int(0.393 * r + 0.769 * g + 0.189 * b))
114
+ res.green = min(255, int(0.272 * r + 0.534 * g + 0.131 * b))
115
+ res.blue = min(255, int(0.349 * r + 0.686 * g + 0.168 * b))
116
+ return res
117
+
118
+
119
+ def apply_brighten(node: QtNode, factor: float = 1.3) -> QtNode:
120
+ """Brighten by scaling channels. Mirrors brighten() in filters.c"""
121
+ res = QtNode(area=node.area)
122
+ if not node.is_leaf():
123
+ res.topLeft = apply_brighten(node.topLeft, factor)
124
+ res.topRight = apply_brighten(node.topRight, factor)
125
+ res.bottomLeft = apply_brighten(node.bottomLeft, factor)
126
+ res.bottomRight = apply_brighten(node.bottomRight, factor)
127
+ res.red = min(255, int(node.red * factor))
128
+ res.green = min(255, int(node.green * factor))
129
+ res.blue = min(255, int(node.blue * factor))
130
+ return res
131
+
132
+
133
+ # ── Spatial Transforms (mirrors rotate.c) ────────────────────────────────────
134
+
135
+ def get_water_image(node: QtNode):
136
+ """Vertical flip (top-bottom). Mirrors getWaterImage() in rotate.c"""
137
+ if not node.is_leaf():
138
+ get_water_image(node.topLeft)
139
+ get_water_image(node.topRight)
140
+ get_water_image(node.bottomLeft)
141
+ get_water_image(node.bottomRight)
142
+ node.topLeft, node.bottomLeft = node.bottomLeft, node.topLeft
143
+ node.topRight, node.bottomRight = node.bottomRight, node.topRight
144
+
145
+
146
+ def get_mirror_image(node: QtNode):
147
+ """Horizontal mirror (left-right). Mirrors getMirrorImage() in rotate.c"""
148
+ if not node.is_leaf():
149
+ get_mirror_image(node.topLeft)
150
+ get_mirror_image(node.topRight)
151
+ get_mirror_image(node.bottomLeft)
152
+ get_mirror_image(node.bottomRight)
153
+ node.topLeft, node.topRight = node.topRight, node.topLeft
154
+ node.bottomLeft, node.bottomRight = node.bottomRight, node.bottomLeft
155
+
156
+
157
+ def rotate_left(node: QtNode):
158
+ """90Β° counter-clockwise. Mirrors rotateLeft() in rotate.c"""
159
+ if not node.is_leaf():
160
+ rotate_left(node.topLeft)
161
+ rotate_left(node.topRight)
162
+ rotate_left(node.bottomLeft)
163
+ rotate_left(node.bottomRight)
164
+ tl, tr, bl, br = node.topLeft, node.topRight, node.bottomLeft, node.bottomRight
165
+ node.topLeft = tr
166
+ node.topRight = br
167
+ node.bottomLeft = tl
168
+ node.bottomRight = bl
169
+
170
+
171
+ def rotate_right(node: QtNode):
172
+ """90Β° clockwise. Mirrors rotateRight() in rotate.c"""
173
+ if not node.is_leaf():
174
+ rotate_right(node.topLeft)
175
+ rotate_right(node.topRight)
176
+ rotate_right(node.bottomLeft)
177
+ rotate_right(node.bottomRight)
178
+ tl, tr, bl, br = node.topLeft, node.topRight, node.bottomLeft, node.bottomRight
179
+ node.topLeft = bl
180
+ node.topRight = tl
181
+ node.bottomLeft = br
182
+ node.bottomRight = tr
183
+
184
+
185
+ # ── Union / Blend (mirrors union.c) ──────────────────────────────────────────
186
+
187
+ def union_of_images(t1: QtNode, t2: QtNode) -> QtNode:
188
+ """Average-blend two quadtrees. Mirrors unionOfImages() in union.c"""
189
+ res = QtNode(area=min(t1.area, t2.area))
190
+ res.red = (t1.red + t2.red) // 2
191
+ res.green = (t1.green + t2.green) // 2
192
+ res.blue = (t1.blue + t2.blue) // 2
193
+ if not t1.is_leaf() and not t2.is_leaf():
194
+ res.topLeft = union_of_images(t1.topLeft, t2.topLeft)
195
+ res.topRight = union_of_images(t1.topRight, t2.topRight)
196
+ res.bottomLeft = union_of_images(t1.bottomLeft, t2.bottomLeft)
197
+ res.bottomRight = union_of_images(t1.bottomRight, t2.bottomRight)
198
+ elif t1.is_leaf() and not t2.is_leaf():
199
+ res.topLeft = union_of_images(t1, t2.topLeft)
200
+ res.topRight = union_of_images(t1, t2.topRight)
201
+ res.bottomLeft = union_of_images(t1, t2.bottomLeft)
202
+ res.bottomRight = union_of_images(t1, t2.bottomRight)
203
+ elif not t1.is_leaf() and t2.is_leaf():
204
+ res.topLeft = union_of_images(t1.topLeft, t2)
205
+ res.topRight = union_of_images(t1.topRight, t2)
206
+ res.bottomLeft = union_of_images(t1.bottomLeft, t2)
207
+ res.bottomRight = union_of_images(t1.bottomRight, t2)
208
+ return res
209
+
210
+
211
+ # ── I/O Helpers ───────────────────────────────────────────────────────────────
212
+
213
+ def read_ppm_bytes(data: bytes):
214
+ """
215
+ Parse P6 binary PPM data robustly.
216
+ Returns numpy array (H, W, 3) uint8.
217
+ """
218
+ # First try Pillow (handles PNG/JPG/PPM automatically)
219
+ try:
220
+ img = Image.open(io.BytesIO(data)).convert("RGB")
221
+ return np.array(img, dtype=np.uint8)
222
+ except Exception:
223
+ pass
224
+
225
+ # Manual P6 binary PPM parser β€” reads header byte-by-byte
226
+ pos = 0
227
+ def read_token():
228
+ nonlocal pos
229
+ # Skip whitespace and comments
230
+ while pos < len(data):
231
+ c = data[pos:pos+1]
232
+ if c == b'#':
233
+ while pos < len(data) and data[pos:pos+1] != b'\n':
234
+ pos += 1
235
+ elif c in (b' ', b'\t', b'\n', b'\r'):
236
+ pos += 1
237
+ else:
238
+ break
239
+ start = pos
240
+ while pos < len(data) and data[pos:pos+1] not in (b' ', b'\t', b'\n', b'\r'):
241
+ pos += 1
242
+ return data[start:pos].decode('ascii')
243
+
244
+ magic = read_token()
245
+ if magic != 'P6':
246
+ raise ValueError(f"Not a P6 PPM file (got: {magic!r})")
247
+ w = int(read_token())
248
+ h = int(read_token())
249
+ _maxval = int(read_token())
250
+ # skip exactly one whitespace byte after maxval
251
+ pos += 1
252
+ raw = data[pos:pos + h * w * 3]
253
+ arr = np.frombuffer(raw, dtype=np.uint8).reshape(h, w, 3)
254
+ return arr.copy() # make writable
255
+
256
+
257
+ def arr_to_pil(arr: np.ndarray) -> Image.Image:
258
+ return Image.fromarray(arr.astype(np.uint8), 'RGB')
259
+
260
+
261
+ def next_power_of_two(n: int) -> int:
262
+ p = 1
263
+ while p < n:
264
+ p <<= 1
265
+ return p
266
+
267
+
268
+ def pad_to_square_pow2(arr: np.ndarray):
269
+ """Pad image to square power-of-2 as required by the quadtree."""
270
+ h, w = arr.shape[:2]
271
+ size = next_power_of_two(max(h, w))
272
+ padded = np.zeros((size, size, 3), dtype=np.uint8)
273
+ padded[:h, :w] = arr
274
+ return padded, h, w
275
+
276
+
277
+ def process_image(matrix: np.ndarray, operation: str, threshold: int,
278
+ matrix2: Optional[np.ndarray] = None, return_tree: bool = False) -> np.ndarray:
279
+ """
280
+ Full pipeline: pad β†’ compress β†’ transform β†’ decompress β†’ crop.
281
+ Returns the result as a numpy RGB array, and optionally the tree.
282
+ """
283
+ padded, oh, ow = pad_to_square_pow2(matrix)
284
+ size = padded.shape[0]
285
+
286
+ tree = compress_image(padded, 0, 0, size, threshold)
287
+
288
+ if operation == "grayscale":
289
+ tree = apply_grayscale(tree)
290
+ elif operation == "negative":
291
+ tree = apply_negative(tree)
292
+ elif operation == "sepia":
293
+ tree = apply_sepia(tree)
294
+ elif operation == "brighten":
295
+ tree = apply_brighten(tree)
296
+ elif operation == "mirror":
297
+ get_mirror_image(tree)
298
+ elif operation == "water":
299
+ get_water_image(tree)
300
+ elif operation == "rotate_left":
301
+ rotate_left(tree)
302
+ elif operation == "rotate_right":
303
+ rotate_right(tree)
304
+ elif operation == "union" and matrix2 is not None:
305
+ padded2, oh2, ow2 = pad_to_square_pow2(matrix2)
306
+ s2 = padded2.shape[0]
307
+ s_common = max(size, s2)
308
+ p1 = np.zeros((s_common, s_common, 3), dtype=np.uint8)
309
+ p1[:padded.shape[0], :padded.shape[1]] = padded
310
+ p2 = np.zeros((s_common, s_common, 3), dtype=np.uint8)
311
+ p2[:padded2.shape[0], :padded2.shape[1]] = padded2
312
+ t1 = compress_image(p1, 0, 0, s_common, threshold)
313
+ t2 = compress_image(p2, 0, 0, s_common, threshold)
314
+ tree = union_of_images(t1, t2)
315
+ size = s_common
316
+ # For union, we want the bounding box that encompasses both original images
317
+ oh = max(oh, oh2)
318
+ ow = max(ow, ow2)
319
+
320
+ out = np.zeros((size, size, 3), dtype=np.uint8)
321
+ decompress_image(tree, out, 0, 0, size)
322
+
323
+ if operation == "mirror":
324
+ res = out[:oh, size - ow : size]
325
+ elif operation == "water":
326
+ res = out[size - oh : size, :ow]
327
+ elif operation == "rotate_left":
328
+ res = out[size - ow : size, :oh]
329
+ elif operation == "rotate_right":
330
+ res = out[:ow, size - oh : size]
331
+ else:
332
+ res = out[:oh, :ow]
333
+
334
+ if return_tree:
335
+ return res, tree
336
+ return res
src/test_engine.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from quadtree_engine import process_image
3
+
4
+ # Create a mock "Mona Lisa" image: 1000x671
5
+ arr = np.zeros((1000, 671, 3), dtype=np.uint8)
6
+ # Draw a white rectangle to represent the face in the center
7
+ arr[200:400, 200:471] = 255
8
+
9
+ # Apply "water"
10
+ res = process_image(arr, "water", 30)
11
+
12
+ print(f"Original shape: {arr.shape}")
13
+ print(f"Result shape: {res.shape}")
14
+
15
+ # Check where the white pixels are in the result
16
+ white_pixels = np.where(res[:, :, 0] == 255)
17
+ if len(white_pixels[0]) > 0:
18
+ min_y, max_y = np.min(white_pixels[0]), np.max(white_pixels[0])
19
+ min_x, max_x = np.min(white_pixels[1]), np.max(white_pixels[1])
20
+ print(f"Face is at y: {min_y}-{max_y}, x: {min_x}-{max_x}")
21
+ else:
22
+ print("No white pixels found!")
23
+
24
+ # Let's also check if there are non-zero pixels outside the expected region
25
+ non_zero = np.where(res > 0)
26
+ if len(non_zero[0]) > 0:
27
+ print(f"All Non-zero y: {np.min(non_zero[0])}-{np.max(non_zero[0])}, x: {np.min(non_zero[1])}-{np.max(non_zero[1])}")