egezort commited on
Commit
07219c6
·
0 Parent(s):

Initial commit for hackathon app

Browse files
Files changed (4) hide show
  1. .gitignore +14 -0
  2. README.md +65 -0
  3. app.py +224 -0
  4. requirements.txt +3 -0
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ image_cache/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ venv/
6
+ image_cache/
7
+ venv/
8
+ image_cache/
9
+ venv/
10
+ image_cache/
11
+ venv/
12
+ image_cache/
13
+ venv/
14
+ image_cache/
README.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image Subject Comparison Tutorial
2
+
3
+ This is a Streamlit-based web application for a tutorial on determining if two images depict the same subject. It presents 8 scenarios based on specific rules, allowing users to select a scenario, view placeholder images, and choose "Yes" or "No" to receive feedback.
4
+
5
+ ## Features
6
+
7
+ - **Interactive Scenarios**: Select from 8 predefined scenarios.
8
+ - **Image Display**: View two images side-by-side for each scenario.
9
+ - **Feedback System**: Click "Yes" or "No" to get instant feedback on correctness.
10
+ - **Tutorial Rules**: Based on rules for same-subject comparison (e.g., same person vs. associated items).
11
+
12
+ ## Scenarios
13
+
14
+ 1. **Different pictures of the same person** - Yes
15
+ 2. **Identical or resized picture** - Yes
16
+ 3. **Different pictures of the same landmark** - Yes
17
+ 4. **Subject vs. representation (e.g., landmark vs. keychain)** - No
18
+ 5. **Person vs. associated item (e.g., player vs. jersey)** - No
19
+ 6. **Person vs. their signature** - No
20
+ 7. **Person vs. their tombstone** - No
21
+ 8. **Same person at different ages** - Yes
22
+
23
+ ## Installation
24
+
25
+ 1. Clone or download the repository.
26
+ 2. Install dependencies:
27
+ ```
28
+ pip install -r requirements.txt
29
+ ```
30
+
31
+ ## Running Locally
32
+
33
+ 1. Ensure you have Python installed.
34
+ 2. Run the Streamlit app:
35
+ ```
36
+ streamlit run app.py
37
+ ```
38
+ 3. Open the provided local URL in your browser (usually `http://localhost:8501`).
39
+
40
+ ## Hosting on Hugging Face Spaces
41
+
42
+ 1. Go to [Hugging Face Spaces](https://huggingface.co/spaces).
43
+ 2. Create a new Space.
44
+ 3. Select "Streamlit" as the SDK.
45
+ 4. Upload `app.py` and `requirements.txt`.
46
+ 5. The app will be hosted and accessible via the Space's URL.
47
+
48
+ ## Usage
49
+
50
+ - Select a scenario from the dropdown.
51
+ - View the two images.
52
+ - Click "Yes" or "No" to see if your answer is correct and read the feedback.
53
+
54
+ ## Customization
55
+
56
+ - **Images**: Replace placeholder URLs in `app.py` with actual Wikimedia Commons image links.
57
+ - **Scenarios**: Modify the `scenarios` list in `app.py` to add or change scenarios.
58
+
59
+ ## Dependencies
60
+
61
+ - streamlit
62
+
63
+ ## License
64
+
65
+ [Add license if applicable]
app.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import os
3
+ import streamlit as st
4
+ import requests
5
+ from PIL import Image
6
+ from io import BytesIO
7
+
8
+ CACHE_DIR = os.path.join(os.path.dirname(__file__), "image_cache")
9
+ os.makedirs(CACHE_DIR, exist_ok=True)
10
+
11
+
12
+ def _cache_path(url: str) -> str:
13
+ """Return the local file path for a cached image URL."""
14
+ key = hashlib.md5(url.encode()).hexdigest()
15
+ return os.path.join(CACHE_DIR, f"{key}.png")
16
+
17
+
18
+ @st.cache_data(show_spinner=False)
19
+ def resolve_image_url(url: str) -> str:
20
+ """If the URL is a Wikimedia Commons File: page, resolve it to the direct
21
+ image URL via the MediaWiki API. Otherwise return the URL unchanged.
22
+ Cached in-memory so each File: page is only looked up once per session."""
23
+ if "commons.wikimedia.org/wiki/File:" in url:
24
+ filename = url.split("/wiki/File:")[-1]
25
+ api_url = (
26
+ "https://commons.wikimedia.org/w/api.php"
27
+ f"?action=query&titles=File:{filename}"
28
+ "&prop=imageinfo&iiprop=url&format=json"
29
+ )
30
+ headers = {"User-Agent": "Mozilla/5.0 (compatible; StreamlitApp/1.0; +https://streamlit.io)"}
31
+ try:
32
+ r = requests.get(api_url, headers=headers, timeout=10)
33
+ pages = r.json()["query"]["pages"]
34
+ page = next(iter(pages.values()))
35
+ return page["imageinfo"][0]["url"]
36
+ except Exception:
37
+ return url
38
+ return url
39
+
40
+
41
+ def load_image(url: str):
42
+ """Load an image from disk cache if available, otherwise download it,
43
+ save it to the cache folder, and return a PIL Image.
44
+ The cache persists across app restarts — images are only downloaded once."""
45
+ path = _cache_path(url)
46
+ if os.path.exists(path):
47
+ try:
48
+ return Image.open(path)
49
+ except Exception:
50
+ pass # corrupted cache file — re-download below
51
+
52
+ headers = {
53
+ "User-Agent": (
54
+ "Mozilla/5.0 (compatible; StreamlitApp/1.0; "
55
+ "+https://streamlit.io)"
56
+ )
57
+ }
58
+ try:
59
+ direct_url = resolve_image_url(url)
60
+ response = requests.get(direct_url, headers=headers, timeout=10)
61
+ response.raise_for_status()
62
+ img = Image.open(BytesIO(response.content))
63
+ img.save(path, format="PNG") # persist to disk
64
+ return img
65
+ except Exception:
66
+ return None
67
+
68
+
69
+ # List of 8 scenarios based on the spreadsheet rules
70
+ # Images sourced from Wikimedia Commons (public domain / CC licensed)
71
+ # Supports both commons.wikimedia.org/wiki/File: page URLs and direct upload URLs
72
+ scenarios = [
73
+ {
74
+ # Scenario 1: Different pictures of the same person (Messi)
75
+ "label": "Different pictures of the same person",
76
+ "image1_url": "https://commons.wikimedia.org/wiki/File:Lionel-Messi-Argentina-2022-FIFA-World-Cup_(cropped).jpg",
77
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Lionel_Messi_in_2018.jpg",
78
+ "answer": "Yes",
79
+ "feedback": "Different photos of the same subject should be marked Yes."
80
+ },
81
+ {
82
+ # Scenario 2: Identical or resized picture (same image, two sizes)
83
+ "label": "Identical or resized picture",
84
+ "image1_url": "https://commons.wikimedia.org/wiki/File:Kevin_Garnett_2008-01-13.jpg",
85
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Kevin_Garnett_2008-01-13.jpg",
86
+ "answer": "Yes",
87
+ "feedback": "Identical images (even at different sizes) require a Yes response."
88
+ },
89
+ {
90
+ # Scenario 3: Different pictures of the same landmark (Eiffel Tower)
91
+ "label": "Different pictures of the same landmark",
92
+ "image1_url": "https://commons.wikimedia.org/wiki/File:Tour_Eiffel_Wikimedia_Commons.jpg",
93
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Tour_eiffel_at_sunrise_from_the_trocadero.jpg",
94
+ "answer": "Yes",
95
+ "feedback": "Different photos of the same landmark are the same subject."
96
+ },
97
+ {
98
+ # Scenario 4: Subject vs. representation (Eiffel Tower vs. keychain)
99
+ "label": "Subject vs. representation (landmark vs. keychain)",
100
+ "image1_url": "https://commons.wikimedia.org/wiki/File:Tour_Eiffel_Wikimedia_Commons.jpg",
101
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Eiffel_Tower_Keychain.jpg",
102
+ "answer": "No",
103
+ "feedback": "A landmark and a keychain are not the same subject."
104
+ },
105
+ {
106
+ # Scenario 5: Person vs. associated item (player vs. jersey)
107
+ "label": "Person vs. associated item (player vs. jersey)",
108
+ "image1_url": "https://commons.wikimedia.org/wiki/File:Lionel_Messi_in_2018.jpg",
109
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Adidas_Messi_shirt_rear.JPG",
110
+ "answer": "No",
111
+ "feedback": "A person and an associated item are not the same subject."
112
+ },
113
+ {
114
+ # Scenario 6: Person vs. their signature
115
+ "label": "Person vs. their signature",
116
+ "image1_url": "https://commons.wikimedia.org/wiki/File:President_Barack_Obama.jpg",
117
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Health_insurance_reform_bill_signature_20100323_(1).jpg",
118
+ "answer": "No",
119
+ "feedback": "A person and a signature are not the same subject."
120
+ },
121
+ {
122
+ # Scenario 7: Person vs. their tombstone
123
+ "label": "Person vs. their tombstone",
124
+ "image1_url": "https://commons.wikimedia.org/wiki/File:Oscar_Wilde_by_Napoleon_Sarony._Three-quarter-length_photograph,_seated.jpg",
125
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Oscar_Wilde_%C3%A9_mort_dans_cette_maison.jpg",
126
+ "answer": "No",
127
+ "feedback": "A person and their tombstone are not the same subject."
128
+ },
129
+ {
130
+ # Scenario 8: Same person at different ages (Einstein young vs old)
131
+ "label": "Same person at different ages",
132
+ "image1_url": "https://commons.wikimedia.org/wiki/File:JimmyCarterPortrait2.jpg",
133
+ "image2_url": "https://commons.wikimedia.org/wiki/File:Jimmy_Carter_and_Rosalynn_Carter_on_Plains_Peanut_Festival_(cropped).jpg",
134
+ "answer": "Yes",
135
+ "feedback": "The same person at different ages is still the same subject."
136
+ }
137
+ ]
138
+
139
+
140
+ def check_answer(scenario_idx, user_answer):
141
+ scenario = scenarios[scenario_idx]
142
+ correct = scenario["answer"]
143
+ feedback = scenario["feedback"]
144
+ if user_answer == correct:
145
+ return f"✅ Correct! {feedback}"
146
+ else:
147
+ return f"❌ Incorrect. The correct answer is **{correct}**. {feedback}"
148
+
149
+
150
+ # --- Session state init ---
151
+ if "scenario_idx" not in st.session_state:
152
+ st.session_state.scenario_idx = 0
153
+ if "feedback" not in st.session_state:
154
+ st.session_state.feedback = None
155
+ if "answered" not in st.session_state:
156
+ st.session_state.answered = False
157
+
158
+ # Preload all images once at startup
159
+ if "images" not in st.session_state:
160
+ with st.spinner("Loading all images, please wait…"):
161
+ st.session_state.images = [
162
+ (
163
+ load_image(s["image1_url"]),
164
+ load_image(s["image2_url"]),
165
+ )
166
+ for s in scenarios
167
+ ]
168
+
169
+ st.title("Image Subject Comparison Tutorial")
170
+ st.markdown(
171
+ "This tutorial demonstrates rules for determining if two images depict the same subject. "
172
+ "Answer each scenario to advance to the next one."
173
+ )
174
+
175
+ idx = st.session_state.scenario_idx
176
+ total = len(scenarios)
177
+
178
+ if idx >= total:
179
+ st.success("🎉 You've completed all scenarios! Well done.")
180
+ if st.button("🔄 Restart"):
181
+ st.session_state.scenario_idx = 0
182
+ st.session_state.feedback = None
183
+ st.session_state.answered = False
184
+ st.rerun()
185
+ else:
186
+ scenario = scenarios[idx]
187
+
188
+ st.markdown(f"### Scenario {idx + 1} of {total}: *{scenario['label']}*")
189
+ st.progress(idx / total)
190
+
191
+ img1, img2 = st.session_state.images[idx]
192
+ col1, col2 = st.columns(2)
193
+ with col1:
194
+ if img1:
195
+ st.image(img1, caption="Image 1", use_container_width=True)
196
+ else:
197
+ st.error("Image 1 could not be loaded.")
198
+ with col2:
199
+ if img2:
200
+ st.image(img2, caption="Image 2", use_container_width=True)
201
+ else:
202
+ st.error("Image 2 could not be loaded.")
203
+
204
+ st.markdown("### Are these the same subject?")
205
+
206
+ if not st.session_state.answered:
207
+ btn_col1, btn_col2 = st.columns(2)
208
+ with btn_col1:
209
+ if st.button("✅ Yes", use_container_width=True):
210
+ st.session_state.feedback = check_answer(idx, "Yes")
211
+ st.session_state.answered = True
212
+ st.rerun()
213
+ with btn_col2:
214
+ if st.button("❌ No", use_container_width=True):
215
+ st.session_state.feedback = check_answer(idx, "No")
216
+ st.session_state.answered = True
217
+ st.rerun()
218
+ else:
219
+ st.info(st.session_state.feedback)
220
+ if st.button("➡️ Next Scenario", use_container_width=True):
221
+ st.session_state.scenario_idx += 1
222
+ st.session_state.feedback = None
223
+ st.session_state.answered = False
224
+ st.rerun()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ streamlit
2
+ requests
3
+ Pillow