CoolFace
Apppublic

bahaakabbara/computervision

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
app.py1979 linesDownload Raw Back to root
1"""Machine Vision Lab — Harris stereo correspondence + manual Sobel gradients.
2
3Styled to match the Likelihood Lab design system (cream background, ink
4palette, pill-style top tabs, rounded cards, Inter + JetBrains Mono).
5Functionality is unchanged from the original app.py; only the layout,
6structural organisation, and visual presentation have been redesigned.
7"""
8from __future__ import annotations
9
10import os
11
12import cv2
13import numpy as np
14import pandas as pd
15import streamlit as st
16
17
18# ============================================================
19# Page Configuration
20# ============================================================
21
22st.set_page_config(
23    page_title="Machine Vision Lab",
24    layout="wide",
25    initial_sidebar_state="collapsed",
26)
27
28
29# ============================================================
30# Styling — Bashir / Likelihood Lab design system, transposed
31# from Gradio to Streamlit via heavy CSS injection.
32# ============================================================
33
34APP_CSS = """
35<style>
36@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
37
38:root {
39  --bg: #fbfaf7;
40  --ink: #0f172a;
41  --ink-soft: #1f2937;
42  --muted: #6b7280;
43  --subtle: #94a3b8;
44  --line: #e5e7eb;
45  --line-soft: #f1f5f9;
46  --panel: #ffffff;
47  --accent: #0f172a;
48  --ok: #16a34a;
49  --warn: #b45309;
50  --r-sm: 12px;
51  --r-md: 16px;
52  --r-lg: 20px;
53  --r-xl: 24px;
54}
55
56/* ---------- Base ---------- */
57html, body, .stApp {
58  background: var(--bg) !important;
59  font-family: 'Inter', -apple-system, 'Segoe UI', sans-serif !important;
60  color: var(--ink) !important;
61}
62
63#MainMenu, footer, [data-testid="stToolbar"] { visibility: hidden !important; height: 0 !important; }
64header[data-testid="stHeader"] { background: transparent !important; height: 0 !important; min-height: 0 !important; }
65[data-testid="stSidebar"], [data-testid="stSidebarCollapsedControl"] { display: none !important; }
66
67.main .block-container,
68[data-testid="stAppViewContainer"] > section > div.block-container {
69  max-width: 100% !important;
70  padding-top: 14px !important;
71  padding-left: clamp(20px, 2.5vw, 44px) !important;
72  padding-right: clamp(20px, 2.5vw, 44px) !important;
73  padding-bottom: 60px !important;
74}
75
76/* IMPORTANT: scope to .stApp only — don't broadcast to every span/div.
77   Streamlit uses Material Symbols Rounded for icons (caret, X close,
78   cloud-upload, etc.) where the span's text content IS the icon name and
79   the font swaps it for a glyph. Overriding font-family on every span
80   shows the icon's name as literal text. So we set Inter on .stApp and
81   let inheritance do the rest; elements with their own font-family
82   (icon spans, code blocks) keep theirs. */
83.stApp {
84  font-family: 'Inter', -apple-system, 'Segoe UI', sans-serif;
85}
86
87/* Belt-and-suspenders: anything that looks like a Material icon span keeps
88   its icon font, no matter how aggressive a downstream rule gets. */
89.stApp [class*="material-symbols"],
90.stApp [class*="material-icons"],
91.stApp [data-testid*="icon" i] span,
92.stApp [class*="iconify"],
93.stApp i[class*="icon"] {
94  font-family: 'Material Symbols Rounded',
95               'Material Symbols Outlined',
96               'Material Icons',
97               'Material Icons Outlined' !important;
98}
99
100code, .stCode, .stCode pre, pre code {
101  font-family: 'JetBrains Mono', ui-monospace, monospace !important;
102  font-size: 13px !important;
103  background: #f9fafb !important;
104  border-radius: var(--r-sm) !important;
105  color: var(--ink-soft) !important;
106}
107
108/* Kill the default Streamlit blue */
109input[type="radio"], input[type="checkbox"] { accent-color: var(--ink) !important; }
110*:focus-visible { outline: 2px solid var(--ink) !important; outline-offset: 2px; }
111a, .stApp a { color: var(--ink) !important; text-decoration: underline; text-underline-offset: 3px; }
112
113h1, h2, h3, h4, h5, h6,
114.stApp h1, .stApp h2, .stApp h3 {
115  font-family: 'Inter', sans-serif !important;
116  color: var(--ink) !important;
117  letter-spacing: -.035em !important;
118  font-weight: 700 !important;
119}
120
121/* ---------- Hero ---------- */
122.app-hero {
123  padding: 26px 0 14px;
124  border-bottom: 1px solid var(--line);
125  margin-bottom: 18px;
126  animation: fadeIn .35s ease-out;
127}
128.app-hero .sup {
129  font-size: 11px;
130  letter-spacing: .14em;
131  text-transform: uppercase;
132  color: var(--muted);
133  font-weight: 800;
134}
135.app-hero h1 {
136  margin: 10px 0 10px !important;
137  font-size: clamp(34px, 6vw, 70px) !important;
138  line-height: .94 !important;
139  letter-spacing: -0.07em !important;
140  color: var(--ink) !important;
141  font-weight: 800 !important;
142  max-width: 980px;
143}
144.app-hero p.lede {
145  color: #4b5563 !important;
146  font-size: 16px !important;
147  line-height: 1.65 !important;
148  max-width: 780px;
149  margin: 0 !important;
150}
151.metric-strip {
152  display: grid;
153  grid-template-columns: repeat(4, minmax(0, 1fr));
154  gap: 10px;
155  margin-top: 22px;
156}
157.metric-strip > div {
158  background: #fff;
159  border: 1px solid var(--line);
160  border-radius: var(--r-lg);
161  padding: 14px 16px;
162  animation: fadeIn .35s ease-out;
163}
164.metric-strip span {
165  display: block;
166  color: var(--muted);
167  text-transform: uppercase;
168  letter-spacing: .08em;
169  font-weight: 800;
170  font-size: 10px;
171}
172.metric-strip strong {
173  display: block;
174  font-size: 18px;
175  margin: 7px 0 4px;
176  color: var(--ink);
177  letter-spacing: -.025em;
178  font-weight: 700;
179}
180.metric-strip small {
181  color: #6b7280;
182  font-size: 12px;
183  line-height: 1.4;
184}
185
186/* ---------- Section heads (tab intros) ---------- */
187.section-head {
188  margin: 6px 0 18px;
189  max-width: 900px;
190  animation: fadeIn .3s ease-out;
191}
192.section-head .kicker {
193  color: var(--muted);
194  font-size: 11px;
195  text-transform: uppercase;
196  letter-spacing: .14em;
197  font-weight: 800;
198  margin-bottom: 10px;
199}
200.section-head h2 {
201  color: var(--ink) !important;
202  font-size: 26px !important;
203  line-height: 1.1 !important;
204  letter-spacing: -.035em !important;
205  margin: 0 0 8px !important;
206  font-weight: 700 !important;
207}
208.section-head p {
209  color: #4b5563 !important;
210  line-height: 1.65 !important;
211  margin: 0 !important;
212  font-size: 15px;
213}
214
215/* Caption row at top of a card */
216.section-label {
217  display: flex;
218  align-items: center;
219  justify-content: space-between;
220  padding: 2px 0 12px;
221  border-bottom: 1px solid var(--line-soft);
222  margin: -2px 0 14px;
223}
224.section-label .lbl {
225  font-size: 10px;
226  letter-spacing: .14em;
227  text-transform: uppercase;
228  color: var(--muted);
229  font-weight: 800;
230}
231.section-label .meta {
232  font-size: 11px;
233  color: var(--subtle);
234  font-family: 'JetBrains Mono', monospace;
235}
236
237/* Subsection in main body (e.g. "Detected Harris corners") */
238.subsection {
239  margin: 18px 0 14px;
240  max-width: 900px;
241  animation: fadeIn .3s ease-out;
242}
243.subsection .sub-kicker {
244  font-size: 10px;
245  text-transform: uppercase;
246  letter-spacing: .14em;
247  color: var(--muted);
248  font-weight: 800;
249  margin-bottom: 6px;
250}
251.subsection h3 {
252  font-size: 20px !important;
253  margin: 0 0 8px !important;
254  letter-spacing: -.025em !important;
255  font-weight: 700 !important;
256  color: var(--ink) !important;
257  line-height: 1.15;
258}
259.subsection p {
260  color: #4b5563 !important;
261  line-height: 1.6;
262  font-size: 14px;
263  margin: 0 !important;
264}
265
266/* ---------- Tabs (pill style, fully rounded) ---------- */
267.stTabs [data-baseweb="tab-list"] {
268  gap: 4px !important;
269  border-bottom: 1px solid var(--line-soft) !important;
270  padding: 6px 0 8px !important;
271  margin-bottom: 18px !important;
272  background: transparent !important;
273}
274.stTabs [data-baseweb="tab"] {
275  border-radius: 999px !important;
276  padding: 8px 18px !important;
277  background: transparent !important;
278  color: var(--muted) !important;
279  font-weight: 700 !important;
280  font-size: 14px !important;
281  border: 0 !important;
282  margin: 0 !important;
283  transition: all .15s ease;
284}
285.stTabs [data-baseweb="tab"] p {
286  font-weight: 700 !important;
287  font-size: 14px !important;
288  color: inherit !important;
289  margin: 0 !important;
290}
291.stTabs [data-baseweb="tab"]:hover {
292  color: var(--ink) !important;
293  background: rgba(15, 23, 42, 0.04) !important;
294}
295.stTabs [aria-selected="true"] {
296  background: rgba(15, 23, 42, 0.08) !important;
297  color: var(--ink) !important;
298}
299.stTabs [aria-selected="true"] p {
300  color: var(--ink) !important;
301}
302.stTabs [data-baseweb="tab-highlight"],
303.stTabs [data-baseweb="tab-border"] {
304  display: none !important;
305  background: transparent !important;
306  height: 0 !important;
307}
308
309/* ---------- Cards (st.container(border=True)) ---------- */
310[data-testid="stVerticalBlockBorderWrapper"] {
311  background: #fff !important;
312  border: 1px solid var(--line) !important;
313  border-radius: var(--r-lg) !important;
314  padding: 18px 22px 20px !important;
315  box-shadow: 0 1px 0 rgba(15, 23, 42, .02), 0 8px 24px rgba(15, 23, 42, .04) !important;
316  margin-bottom: 14px !important;
317  animation: fadeIn .3s ease-out;
318}
319
320/* Universal rounding for inputs/blocks */
321.stTextInput input,
322.stNumberInput input,
323.stSelectbox > div > div,
324[data-baseweb="select"] > div,
325[data-baseweb="input"] > div {
326  border-radius: var(--r-sm) !important;
327  background: #fff !important;
328}
329
330/* ---------- Buttons ---------- */
331.stButton > button,
332.stDownloadButton > button {
333  background: var(--ink) !important;
334  color: #fff !important;
335  border: 1px solid var(--ink) !important;
336  border-radius: var(--r-sm) !important;
337  font-family: 'Inter', sans-serif !important;
338  font-weight: 700 !important;
339  font-size: 14px !important;
340  padding: 10px 18px !important;
341  box-shadow: none !important;
342  transition: all .15s ease;
343}
344.stButton > button:hover,
345.stDownloadButton > button:hover {
346  background: #1e293b !important;
347  border-color: #1e293b !important;
348  color: #fff !important;
349}
350.stButton > button:focus,
351.stDownloadButton > button:focus {
352  box-shadow: none !important;
353  outline: 2px solid var(--ink) !important;
354  outline-offset: 2px;
355}
356
357/* ---------- Form labels (uppercase kicker style) ---------- */
358.stSlider > label,
359.stRadio > label,
360.stCheckbox label,
361.stSelectbox > label,
362.stFileUploader > label,
363.stTextInput > label,
364.stNumberInput > label {
365  font-size: 11px !important;
366  text-transform: uppercase !important;
367  letter-spacing: .12em !important;
368  color: var(--muted) !important;
369  font-weight: 800 !important;
370}
371.stSlider > label p,
372.stRadio > label p,
373.stCheckbox label p,
374.stSelectbox > label p,
375.stFileUploader > label p {
376  color: var(--muted) !important;
377  font-size: 11px !important;
378  font-weight: 800 !important;
379  letter-spacing: .12em !important;
380  text-transform: uppercase !important;
381}
382
383/* ---------- Slider ---------- */
384.stSlider [data-baseweb="slider"] [role="slider"] {
385  background: var(--ink) !important;
386  border-color: var(--ink) !important;
387  box-shadow: 0 0 0 2px rgba(15, 23, 42, .1) !important;
388}
389.stSlider [data-baseweb="slider"] > div > div > div {
390  background: var(--ink) !important;
391}
392.stSlider [data-testid="stTickBar"] {
393  background: transparent !important;
394}
395.stSlider [data-testid="stTickBarMin"],
396.stSlider [data-testid="stTickBarMax"] {
397  color: var(--subtle) !important;
398  font-size: 11px !important;
399  font-family: 'JetBrains Mono', monospace !important;
400}
401
402/* Slider current-value indicator (the number above the handle) —
403   Streamlit's default is bright red (#ff4b4b); force it to ink. */
404.stSlider [data-baseweb="slider"] [role="slider"],
405.stSlider [data-baseweb="slider"] [role="slider"] *,
406.stSlider [data-testid="stThumbValue"],
407.stSlider [data-testid="stThumbValue"] *,
408.stSlider [data-baseweb="thumb-value"],
409.stSlider [data-baseweb="thumb-value"] *,
410.stSlider [class*="StyledThumbValue"],
411.stSlider [class*="thumbValue" i] {
412  color: var(--ink) !important;
413  font-family: 'JetBrains Mono', monospace !important;
414  font-weight: 600 !important;
415}
416
417/* Belt-and-suspenders for the red value: any element inside the slider
418   that Streamlit / BaseWeb colors red — recolor to ink. Tick-bar text uses
419   var(--subtle) above, which is non-red, so this won't fight that rule. */
420.stSlider [data-baseweb="slider"] [style*="color: rgb(255, 75, 75)"],
421.stSlider [data-baseweb="slider"] [style*="color:#ff4b4b" i],
422.stSlider [data-baseweb="slider"] [style*="color: #ff4b4b" i],
423.stSlider [data-baseweb="slider"] [style*="rgb(255,75,75)"],
424.stSlider [data-baseweb="slider"] div:not([class*="track"]):not([class*="Track"]) {
425  color: var(--ink) !important;
426}
427
428/* Re-restore tick-bar subtle color (in case the broad rule above caught it) */
429.stSlider [data-testid="stTickBarMin"],
430.stSlider [data-testid="stTickBarMax"] {
431  color: var(--subtle) !important;
432}
433
434/* Slider value tooltip */
435.stSlider [data-baseweb="tooltip"] {
436  background: var(--ink) !important;
437  color: #fff !important;
438  font-family: 'JetBrains Mono', monospace !important;
439  border-radius: var(--r-sm) !important;
440}
441
442/* ---------- Radio & checkbox option text ---------- */
443.stRadio [role="radiogroup"] label {
444  text-transform: none !important;
445  letter-spacing: 0 !important;
446  font-weight: 500 !important;
447  font-size: 14px !important;
448  color: var(--ink) !important;
449}
450.stRadio [role="radiogroup"] label p {
451  text-transform: none !important;
452  letter-spacing: 0 !important;
453  font-weight: 500 !important;
454  font-size: 14px !important;
455  color: var(--ink) !important;
456}
457.stCheckbox label p {
458  text-transform: none !important;
459  letter-spacing: 0 !important;
460  font-weight: 500 !important;
461  font-size: 14px !important;
462  color: var(--ink) !important;
463}
464
465/* ---------- File uploader ---------- */
466[data-testid="stFileUploaderDropzone"] {
467  background: #fafafa !important;
468  border: 1.5px dashed #d1d5db !important;
469  border-radius: var(--r-md) !important;
470  padding: 22px !important;
471}
472[data-testid="stFileUploaderDropzone"] button {
473  background: var(--ink) !important;
474  color: #fff !important;
475  border: 1px solid var(--ink) !important;
476  border-radius: var(--r-sm) !important;
477  font-weight: 700 !important;
478}
479[data-testid="stFileUploaderDropzoneInstructions"] span,
480[data-testid="stFileUploaderDropzoneInstructions"] small,
481[data-testid="stFileUploaderDropzoneInstructions"] div {
482  color: var(--muted) !important;
483}
484
485/* ---------- Metrics (st.metric → card like Bashir's strip) ---------- */
486[data-testid="stMetric"] {
487  background: #fff !important;
488  border: 1px solid var(--line) !important;
489  border-radius: var(--r-lg) !important;
490  padding: 14px 16px !important;
491  box-shadow: 0 1px 0 rgba(15, 23, 42, .02), 0 8px 24px rgba(15, 23, 42, .04) !important;
492  animation: fadeIn .3s ease-out;
493}
494[data-testid="stMetricLabel"] {
495  color: var(--muted) !important;
496}
497[data-testid="stMetricLabel"] > div,
498[data-testid="stMetricLabel"] p {
499  text-transform: uppercase !important;
500  letter-spacing: .08em !important;
501  font-weight: 800 !important;
502  font-size: 10px !important;
503  color: var(--muted) !important;
504}
505[data-testid="stMetricValue"] {
506  font-family: 'JetBrains Mono', monospace !important;
507  color: var(--ink) !important;
508  font-size: 30px !important;
509  font-weight: 600 !important;
510  letter-spacing: -.04em !important;
511  line-height: 1 !important;
512  margin-top: 4px !important;
513}
514[data-testid="stMetricValue"] > div {
515  color: var(--ink) !important;
516  font-family: 'JetBrains Mono', monospace !important;
517}
518
519/* ---------- Captions ---------- */
520[data-testid="stCaptionContainer"],
521.stCaptionContainer {
522  color: var(--muted) !important;
523  font-size: 12px !important;
524}
525
526/* ---------- Help / tooltip icon ---------- */
527[data-testid="stTooltipIcon"] svg {
528  fill: var(--subtle) !important;
529}
530[data-testid="stTooltipHoverTarget"] {
531  color: var(--subtle) !important;
532}
533
534/* ---------- Expander ---------- */
535[data-testid="stExpander"] {
536  background: #fff !important;
537  border: 1px solid var(--line) !important;
538  border-radius: var(--r-lg) !important;
539  box-shadow: 0 1px 0 rgba(15, 23, 42, .02), 0 4px 12px rgba(15, 23, 42, .03) !important;
540  overflow: hidden;
541  margin-bottom: 14px !important;
542}
543[data-testid="stExpander"] summary,
544[data-testid="stExpander"] details > summary {
545  padding: 14px 20px !important;
546  font-weight: 700 !important;
547  font-size: 13px !important;
548  color: var(--ink) !important;
549  font-family: 'Inter', sans-serif !important;
550}
551[data-testid="stExpander"] summary p {
552  font-weight: 700 !important;
553  font-size: 13px !important;
554  color: var(--ink) !important;
555  margin: 0 !important;
556}
557[data-testid="stExpander"] summary:hover {
558  background: var(--line-soft) !important;
559}
560[data-testid="stExpander"] details[open] summary {
561  border-bottom: 1px solid var(--line-soft) !important;
562}
563[data-testid="stExpanderDetails"] {
564  padding: 18px 22px 22px !important;
565}
566[data-testid="stExpander"] .stMarkdown h2 {
567  font-size: 18px !important;
568  margin: 14px 0 8px !important;
569  font-weight: 700 !important;
570  letter-spacing: -.025em !important;
571}
572[data-testid="stExpander"] .stMarkdown h2:first-child {
573  margin-top: 0 !important;
574}
575[data-testid="stExpander"] .stMarkdown p,
576[data-testid="stExpander"] .stMarkdown li {
577  color: #4b5563 !important;
578  line-height: 1.65 !important;
579  font-size: 14px !important;
580}
581[data-testid="stExpander"] .stMarkdown strong {
582  color: var(--ink) !important;
583  font-weight: 700 !important;
584}
585[data-testid="stExpander"] .stMarkdown hr {
586  border-color: var(--line-soft) !important;
587  margin: 16px 0 !important;
588}
589
590/* ---------- Divider ---------- */
591[data-testid="stHorizontalDivider"] hr,
592hr {
593  border-color: var(--line-soft) !important;
594  margin: 20px 0 !important;
595}
596
597/* ---------- Images ---------- */
598.stImage img, [data-testid="stImage"] img {
599  border-radius: var(--r-md) !important;
600  border: 1px solid var(--line-soft);
601}
602.stImage figcaption,
603[data-testid="stImageCaption"],
604[data-testid="stImage"] + div p,
605[data-testid="stImage"] [data-testid="caption"] {
606  color: var(--muted) !important;
607  font-size: 11px !important;
608  font-weight: 700 !important;
609  text-transform: uppercase;
610  letter-spacing: .1em;
611  padding-top: 8px !important;
612  font-family: 'Inter', sans-serif !important;
613}
614
615/* ---------- Dataframe ---------- */
616.stDataFrame, [data-testid="stDataFrame"] {
617  border-radius: var(--r-md) !important;
618  overflow: hidden;
619  border: 1px solid var(--line) !important;
620  background: #fff !important;
621}
622[data-testid="stDataFrame"] [role="columnheader"] {
623  background: var(--line-soft) !important;
624  color: var(--ink) !important;
625  font-weight: 700 !important;
626}
627
628/* ---------- Alerts / info / warning / success / error ---------- */
629.stAlert, [data-testid="stAlert"] {
630  border-radius: var(--r-md) !important;
631  border: 1px solid var(--line) !important;
632  background: #fff !important;
633  box-shadow: 0 1px 0 rgba(15, 23, 42, .02), 0 4px 12px rgba(15, 23, 42, .03) !important;
634}
635[data-testid="stAlert"] [data-testid="stMarkdownContainer"] p {
636  color: var(--ink-soft) !important;
637  font-size: 13px !important;
638}
639[data-baseweb="notification"] {
640  background: #fff !important;
641  border-radius: var(--r-md) !important;
642}
643
644/* ---------- Spinner ---------- */
645.stSpinner > div > div {
646  border-color: var(--ink) transparent transparent transparent !important;
647}
648.stSpinner [data-testid="stMarkdownContainer"] p,
649.stSpinner span {
650  color: var(--muted) !important;
651  font-size: 12px !important;
652  text-transform: uppercase !important;
653  letter-spacing: .12em !important;
654  font-weight: 800 !important;
655}
656
657/* ---------- Inline code in markdown ---------- */
658.stMarkdown code:not(pre code) {
659  background: #f1f5f9 !important;
660  color: var(--ink) !important;
661  padding: 1px 6px !important;
662  border-radius: 6px !important;
663  font-size: 12px !important;
664  font-family: 'JetBrains Mono', monospace !important;
665}
666
667/* ---------- Animation ---------- */
668@keyframes fadeIn {
669  from { opacity: 0; transform: translateY(6px); }
670  to   { opacity: 1; transform: translateY(0); }
671}
672
673/* ---------- Responsive ---------- */
674@media (max-width: 900px) {
675  .metric-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
676}
677@media (max-width: 560px) {
678  .metric-strip { grid-template-columns: 1fr; }
679  .app-hero h1 { font-size: 38px !important; }
680}
681</style>
682"""
683
684st.markdown(APP_CSS, unsafe_allow_html=True)
685
686
687# ============================================================
688# Hero
689# ============================================================
690
691st.markdown(
692    """
693    <header class='app-hero'>
694      <div class='sup'>Machine vision · lab</div>
695      <h1>Interactive vision tools for stereo &amp; gradient analysis.</h1>
696      <p class='lede'>Two from-scratch machine-vision pipelines on one canvas: Harris–Stephens corner detection with rectified-stereo correspondence on the left, a manual Sobel gradient explorer on the right. Tune the parameters; the result updates immediately.</p>
697      <div class='metric-strip'>
698        <div><span>Corner detector</span><strong>Harris–Stephens</strong><small>R = det(M) − k · tr(M)²</small></div>
699        <div><span>Patch metric</span><strong>Normalized SSD</strong><small>brightness-invariant matching</small></div>
700        <div><span>Stereo geometry</span><strong>Rectified</strong><small>same-row search constraint</small></div>
701        <div><span>Output</span><strong>Sparse disparity</strong><small>colored depth-style overlay</small></div>
702      </div>
703    </header>
704    """,
705    unsafe_allow_html=True,
706)
707
708
709# ============================================================
710# Utility Functions
711# ============================================================
712
713def bgr_to_rgb(img):
714    return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
715
716
717def load_uploaded_image(uploaded_file):
718    file_bytes = np.asarray(bytearray(uploaded_file.getvalue()), dtype=np.uint8)
719    img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
720    return img
721
722
723def load_default_image(path):
724    if os.path.exists(path):
725        return cv2.imread(path)
726    return None
727
728
729def image_to_jpeg_bytes(img):
730    success, encoded_img = cv2.imencode(".jpg", img)
731    if success:
732        return encoded_img.tobytes()
733    return None
734
735
736# ============================================================
737# Manual Sobel Operator
738# ============================================================
739
740def manual_sobel(gray):
741
742    gray = gray.astype(np.float32)
743
744    sobel_x = np.array([
745        [-1, 0, 1],
746        [-2, 0, 2],
747        [-1, 0, 1]
748    ], dtype=np.float32)
749
750    sobel_y = np.array([
751        [-1, -2, -1],
752        [0, 0, 0],
753        [1, 2, 1]
754    ], dtype=np.float32)
755
756    h, w = gray.shape
757
758    Ix = np.zeros((h, w), dtype=np.float32)
759    Iy = np.zeros((h, w), dtype=np.float32)
760
761    for y in range(1, h - 1):
762        for x in range(1, w - 1):
763
764            region = gray[y - 1:y + 2, x - 1:x + 2]
765
766            gx = np.sum(region * sobel_x)
767            gy = np.sum(region * sobel_y)
768
769            Ix[y, x] = gx
770            Iy[y, x] = gy
771
772    magnitude = np.sqrt(Ix ** 2 + Iy ** 2)
773
774    Ix_display = cv2.normalize(np.abs(Ix), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
775    Iy_display = cv2.normalize(np.abs(Iy), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
776    magnitude_display = cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
777
778    return Ix, Iy, Ix_display, Iy_display, magnitude_display
779
780
781# ============================================================
782# Harris Corner Detection — Manual Implementation
783# ============================================================
784
785def harris_corners_manual(
786    img,
787    threshold_ratio=0.005,
788    k=0.04,
789    gaussian_kernel=(3, 3),
790    gaussian_sigma=1,
791    min_distance=8,
792    max_corners=500
793):
794    """
795    Manual Harris corner detector.
796
797    Returns:
798        corners: list of (x, y)
799        R: Harris response matrix
800    """
801
802    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
803    gray_float = np.float32(gray)
804
805    # Compute image gradients
806    Ix = cv2.Sobel(gray_float, cv2.CV_64F, 1, 0, ksize=3)
807    Iy = cv2.Sobel(gray_float, cv2.CV_64F, 0, 1, ksize=3)
808
809    # Products of derivatives
810    Ix2 = Ix * Ix
811    Iy2 = Iy * Iy
812    Ixy = Ix * Iy
813
814    # Gaussian smoothing
815    Sx2 = cv2.GaussianBlur(Ix2, gaussian_kernel, gaussian_sigma)
816    Sy2 = cv2.GaussianBlur(Iy2, gaussian_kernel, gaussian_sigma)
817    Sxy = cv2.GaussianBlur(Ixy, gaussian_kernel, gaussian_sigma)
818
819    # Harris response
820    detM = (Sx2 * Sy2) - (Sxy ** 2)
821    traceM = Sx2 + Sy2
822    R = detM - k * (traceM ** 2)
823
824    # Threshold
825    threshold = threshold_ratio * R.max()
826
827    # Non-maximum suppression using dilation
828    R_dilated = cv2.dilate(R, None)
829    corner_mask = (R == R_dilated) & (R > threshold)
830
831    # Get corner coordinates
832    y_coords, x_coords = np.where(corner_mask)
833
834    # Store corners with response values
835    candidate_corners = []
836
837    for x, y in zip(x_coords, y_coords):
838        candidate_corners.append((x, y, R[y, x]))
839
840    # Sort strongest corners first
841    candidate_corners = sorted(candidate_corners, key=lambda c: c[2], reverse=True)
842
843    # Keep corners separated by min_distance
844    selected_corners = []
845
846    for x, y, response in candidate_corners:
847        too_close = False
848
849        for x_selected, y_selected in selected_corners:
850            distance = np.sqrt((x - x_selected) ** 2 + (y - y_selected) ** 2)
851
852            if distance < min_distance:
853                too_close = True
854                break
855
856        if not too_close:
857            selected_corners.append((x, y))
858
859        if len(selected_corners) >= max_corners:
860            break
861
862    return selected_corners, R
863
864
865# ============================================================
866# Drawing Functions
867# ============================================================
868
869def draw_corners(img, corners, color=(0, 0, 255)):
870    """Draw circles on detected corners."""
871    result = img.copy()
872
873    for x, y in corners:
874        cv2.circle(result, (x, y), 4, color, 1)
875
876    return result
877
878
879def generate_distinct_colors(n):
880    """Generate n visually distinct colors using HSV color space."""
881    colors = []
882
883    for i in range(n):
884        hue = int(180 * i / max(n, 1))
885        color_hsv = np.uint8([[[hue, 255, 255]]])
886        color_bgr = cv2.cvtColor(color_hsv, cv2.COLOR_HSV2BGR)[0][0]
887
888        colors.append(
889            (
890                int(color_bgr[0]),
891                int(color_bgr[1]),
892                int(color_bgr[2])
893            )
894        )
895
896    return colors
897
898
899def draw_best_matches_colored(
900    img_left,
901    img_right,
902    matches,
903    max_matches_to_draw=20,
904    circle_radius=7,
905    show_labels=True
906):
907    """Draw the best matched corners using the same color in both images."""
908
909    matches_sorted = sorted(matches, key=lambda m: m[2])
910    matches_to_draw = matches_sorted[:max_matches_to_draw]
911
912    hL, wL = img_left.shape[:2]
913    hR, wR = img_right.shape[:2]
914
915    height = max(hL, hR)
916    width = wL + wR
917
918    result = np.zeros((height, width, 3), dtype=np.uint8)
919
920    result[:hL, :wL] = img_left
921    result[:hR, wL:wL + wR] = img_right
922
923    colors = generate_distinct_colors(len(matches_to_draw))
924
925    for i, match in enumerate(matches_to_draw):
926        (xL, yL), (xR, yR), score, disparity = match
927
928        color = colors[i]
929
930        xR_shifted = xR + wL
931
932        cv2.circle(result, (xL, yL), circle_radius + 2, (0, 0, 0), -1)
933        cv2.circle(result, (xR_shifted, yR), circle_radius + 2, (0, 0, 0), -1)
934
935        cv2.circle(result, (xL, yL), circle_radius, color, -1)
936        cv2.circle(result, (xR_shifted, yR), circle_radius, color, -1)
937
938        if show_labels:
939            label = str(i + 1)
940
941            cv2.putText(result, label, (xL + 10, yL - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 3, cv2.LINE_AA)
942            cv2.putText(result, label, (xL + 10, yL - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 1, cv2.LINE_AA)
943            cv2.putText(result, label, (xR_shifted + 10, yR - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 0, 0), 3, cv2.LINE_AA)
944            cv2.putText(result, label, (xR_shifted + 10, yR - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 1, cv2.LINE_AA)
945
946    return result, matches_to_draw
947
948
949# ============================================================
950# Patch and Similarity Functions
951# ============================================================
952
953def extract_patch(gray, x, y, patch_size=11):
954    half = patch_size // 2
955
956    if x - half < 0 or x + half >= gray.shape[1]:
957        return None
958
959    if y - half < 0 or y + half >= gray.shape[0]:
960        return None
961
962    patch = gray[y - half:y + half + 1, x - half:x + half + 1]
963
964    return patch
965
966
967def compute_normalized_ssd(patch1, patch2):
968    """Normalized SSD. Lower is more similar."""
969    patch1 = patch1.astype(np.float32)
970    patch2 = patch2.astype(np.float32)
971
972    patch1 = patch1 - np.mean(patch1)
973    patch2 = patch2 - np.mean(patch2)
974
975    std1 = np.std(patch1)
976    std2 = np.std(patch2)
977
978    if std1 < 1e-6 or std2 < 1e-6:
979        return float("inf")
980
981    patch1 = patch1 / std1
982    patch2 = patch2 / std2
983
984    diff = patch1 - patch2
985
986    return np.sum(diff ** 2)
987
988
989# ============================================================
990# Correspondence Test
991# ============================================================
992
993def match_corners_rectified(
994    img_left,
995    img_right,
996    corners_left,
997    corners_right,
998    patch_size=11,
999    row_tolerance=2,
1000    min_disparity=0,
1001    max_disparity=150,
1002    ratio_threshold=0.8,
1003    max_score=300
1004):
1005    """Match Harris corners between rectified stereo images."""
1006
1007    gray_left = cv2.cvtColor(img_left, cv2.COLOR_BGR2GRAY)
1008    gray_right = cv2.cvtColor(img_right, cv2.COLOR_BGR2GRAY)
1009
1010    matches = []
1011
1012    for xL, yL in corners_left:
1013        patch_left = extract_patch(gray_left, xL, yL, patch_size)
1014
1015        if patch_left is None:
1016            continue
1017
1018        best_score = float("inf")
1019        second_best_score = float("inf")
1020        best_match = None
1021
1022        for xR, yR in corners_right:
1023
1024            if abs(yL - yR) > row_tolerance:
1025                continue
1026
1027            disparity = xL - xR
1028
1029            if disparity < min_disparity or disparity > max_disparity:
1030                continue
1031
1032            patch_right = extract_patch(gray_right, xR, yR, patch_size)
1033
1034            if patch_right is None:
1035                continue
1036
1037            score = compute_normalized_ssd(patch_left, patch_right)
1038
1039            if score < best_score:
1040                second_best_score = best_score
1041                best_score = score
1042                best_match = (xR, yR)
1043
1044            elif score < second_best_score:
1045                second_best_score = score
1046
1047        if best_match is not None:
1048
1049            if second_best_score == float("inf"):
1050                ratio = 0
1051            else:
1052                ratio = best_score / second_best_score
1053
1054            if best_score < max_score and ratio < ratio_threshold:
1055                xR, yR = best_match
1056                disparity = xL - xR
1057                matches.append(((xL, yL), (xR, yR), best_score, disparity))
1058
1059    return matches
1060
1061
1062def left_right_consistency_check(matches_left_to_right, matches_right_to_left, tolerance=2):
1063    """Keep only matches that agree in both directions."""
1064    consistent_matches = []
1065
1066    for (xL, yL), (xR, yR), score, disparity in matches_left_to_right:
1067
1068        for (xR2, yR2), (xL2, yL2), score2, disparity2 in matches_right_to_left:
1069
1070            right_point_agrees = abs(xR - xR2) <= tolerance and abs(yR - yR2) <= tolerance
1071            left_point_agrees = abs(xL - xL2) <= tolerance and abs(yL - yL2) <= tolerance
1072
1073            if right_point_agrees and left_point_agrees:
1074                consistent_matches.append(((xL, yL), (xR, yR), score, disparity))
1075                break
1076
1077    return consistent_matches
1078
1079
1080# ============================================================
1081# Sparse Disparity / Relative Depth Visualization
1082# ============================================================
1083
1084def draw_sparse_disparity_visualization(
1085    img_left,
1086    matches,
1087    max_matches_to_draw=100,
1088    circle_radius=7,
1089    show_values=True
1090):
1091    """Draw matched points on the left image using colors based on disparity."""
1092
1093    result = img_left.copy()
1094
1095    matches_sorted = sorted(matches, key=lambda m: m[2])
1096    matches_to_draw = matches_sorted[:max_matches_to_draw]
1097
1098    if len(matches_to_draw) == 0:
1099        return result
1100
1101    disparities = np.array([m[3] for m in matches_to_draw], dtype=np.float32)
1102
1103    min_disp = float(np.min(disparities))
1104    max_disp = float(np.max(disparities))
1105
1106    if abs(max_disp - min_disp) < 1e-6:
1107        max_disp = min_disp + 1.0
1108
1109    for match in matches_to_draw:
1110        (xL, yL), (xR, yR), score, disparity = match
1111
1112        normalized = int(255 * (disparity - min_disp) / (max_disp - min_disp))
1113        normalized = np.clip(normalized, 0, 255)
1114
1115        color_map_input = np.uint8([[normalized]])
1116        color = cv2.applyColorMap(color_map_input, cv2.COLORMAP_JET)[0][0]
1117        color = (int(color[0]), int(color[1]), int(color[2]))
1118
1119        cv2.circle(result, (xL, yL), circle_radius + 2, (0, 0, 0), -1)
1120        cv2.circle(result, (xL, yL), circle_radius, color, -1)
1121
1122        if show_values:
1123            text = str(int(disparity))
1124
1125            cv2.putText(result, text, (xL + 8, yL - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 0), 3, cv2.LINE_AA)
1126            cv2.putText(result, text, (xL + 8, yL - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 1, cv2.LINE_AA)
1127
1128    return result
1129
1130
1131# ============================================================
1132# Match Table
1133# ============================================================
1134
1135def create_matches_dataframe(matches):
1136    rows = []
1137
1138    matches_sorted = sorted(matches, key=lambda m: m[2])
1139
1140    for i, match in enumerate(matches_sorted):
1141        (xL, yL), (xR, yR), score, disparity = match
1142
1143        rows.append(
1144            {
1145                "Match": i + 1,
1146                "Left x": xL,
1147                "Left y": yL,
1148                "Right x": xR,
1149                "Right y": yR,
1150                "Disparity": disparity,
1151                "Score": round(float(score), 2)
1152            }
1153        )
1154
1155    return pd.DataFrame(rows)
1156
1157
1158# ============================================================
1159# Small HTML helpers (Bashir-style section heads)
1160# ============================================================
1161
1162def section_head(kicker: str, title: str, body: str) -> None:
1163    st.markdown(
1164        f"""
1165        <section class='section-head'>
1166          <div class='kicker'>{kicker}</div>
1167          <h2>{title}</h2>
1168          <p>{body}</p>
1169        </section>
1170        """,
1171        unsafe_allow_html=True,
1172    )
1173
1174
1175def subsection(kicker: str, title: str, body: str | None = None) -> None:
1176    body_html = f"<p>{body}</p>" if body else ""
1177    st.markdown(
1178        f"""
1179        <div class='subsection'>
1180          <div class='sub-kicker'>{kicker}</div>
1181          <h3>{title}</h3>
1182          {body_html}
1183        </div>
1184        """,
1185        unsafe_allow_html=True,
1186    )
1187
1188
1189def section_label(text: str, meta: str | None = None) -> None:
1190    meta_html = f"<span class='meta'>{meta}</span>" if meta else ""
1191    st.markdown(
1192        f"<div class='section-label'><span class='lbl'>{text}</span>{meta_html}</div>",
1193        unsafe_allow_html=True,
1194    )
1195
1196
1197# ============================================================
1198# Tabs
1199# ============================================================
1200

Showing the first 1,200 of 1979 lines. Download the file for the rest.