egezort
Initial commit for hackathon app
07219c6
raw
history blame
9.25 kB
import hashlib
import os
import streamlit as st
import requests
from PIL import Image
from io import BytesIO
CACHE_DIR = os.path.join(os.path.dirname(__file__), "image_cache")
os.makedirs(CACHE_DIR, exist_ok=True)
def _cache_path(url: str) -> str:
"""Return the local file path for a cached image URL."""
key = hashlib.md5(url.encode()).hexdigest()
return os.path.join(CACHE_DIR, f"{key}.png")
@st.cache_data(show_spinner=False)
def resolve_image_url(url: str) -> str:
"""If the URL is a Wikimedia Commons File: page, resolve it to the direct
image URL via the MediaWiki API. Otherwise return the URL unchanged.
Cached in-memory so each File: page is only looked up once per session."""
if "commons.wikimedia.org/wiki/File:" in url:
filename = url.split("/wiki/File:")[-1]
api_url = (
"https://commons.wikimedia.org/w/api.php"
f"?action=query&titles=File:{filename}"
"&prop=imageinfo&iiprop=url&format=json"
)
headers = {"User-Agent": "Mozilla/5.0 (compatible; StreamlitApp/1.0; +https://streamlit.io)"}
try:
r = requests.get(api_url, headers=headers, timeout=10)
pages = r.json()["query"]["pages"]
page = next(iter(pages.values()))
return page["imageinfo"][0]["url"]
except Exception:
return url
return url
def load_image(url: str):
"""Load an image from disk cache if available, otherwise download it,
save it to the cache folder, and return a PIL Image.
The cache persists across app restarts — images are only downloaded once."""
path = _cache_path(url)
if os.path.exists(path):
try:
return Image.open(path)
except Exception:
pass # corrupted cache file — re-download below
headers = {
"User-Agent": (
"Mozilla/5.0 (compatible; StreamlitApp/1.0; "
"+https://streamlit.io)"
)
}
try:
direct_url = resolve_image_url(url)
response = requests.get(direct_url, headers=headers, timeout=10)
response.raise_for_status()
img = Image.open(BytesIO(response.content))
img.save(path, format="PNG") # persist to disk
return img
except Exception:
return None
# List of 8 scenarios based on the spreadsheet rules
# Images sourced from Wikimedia Commons (public domain / CC licensed)
# Supports both commons.wikimedia.org/wiki/File: page URLs and direct upload URLs
scenarios = [
{
# Scenario 1: Different pictures of the same person (Messi)
"label": "Different pictures of the same person",
"image1_url": "https://commons.wikimedia.org/wiki/File:Lionel-Messi-Argentina-2022-FIFA-World-Cup_(cropped).jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Lionel_Messi_in_2018.jpg",
"answer": "Yes",
"feedback": "Different photos of the same subject should be marked Yes."
},
{
# Scenario 2: Identical or resized picture (same image, two sizes)
"label": "Identical or resized picture",
"image1_url": "https://commons.wikimedia.org/wiki/File:Kevin_Garnett_2008-01-13.jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Kevin_Garnett_2008-01-13.jpg",
"answer": "Yes",
"feedback": "Identical images (even at different sizes) require a Yes response."
},
{
# Scenario 3: Different pictures of the same landmark (Eiffel Tower)
"label": "Different pictures of the same landmark",
"image1_url": "https://commons.wikimedia.org/wiki/File:Tour_Eiffel_Wikimedia_Commons.jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Tour_eiffel_at_sunrise_from_the_trocadero.jpg",
"answer": "Yes",
"feedback": "Different photos of the same landmark are the same subject."
},
{
# Scenario 4: Subject vs. representation (Eiffel Tower vs. keychain)
"label": "Subject vs. representation (landmark vs. keychain)",
"image1_url": "https://commons.wikimedia.org/wiki/File:Tour_Eiffel_Wikimedia_Commons.jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Eiffel_Tower_Keychain.jpg",
"answer": "No",
"feedback": "A landmark and a keychain are not the same subject."
},
{
# Scenario 5: Person vs. associated item (player vs. jersey)
"label": "Person vs. associated item (player vs. jersey)",
"image1_url": "https://commons.wikimedia.org/wiki/File:Lionel_Messi_in_2018.jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Adidas_Messi_shirt_rear.JPG",
"answer": "No",
"feedback": "A person and an associated item are not the same subject."
},
{
# Scenario 6: Person vs. their signature
"label": "Person vs. their signature",
"image1_url": "https://commons.wikimedia.org/wiki/File:President_Barack_Obama.jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Health_insurance_reform_bill_signature_20100323_(1).jpg",
"answer": "No",
"feedback": "A person and a signature are not the same subject."
},
{
# Scenario 7: Person vs. their tombstone
"label": "Person vs. their tombstone",
"image1_url": "https://commons.wikimedia.org/wiki/File:Oscar_Wilde_by_Napoleon_Sarony._Three-quarter-length_photograph,_seated.jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Oscar_Wilde_%C3%A9_mort_dans_cette_maison.jpg",
"answer": "No",
"feedback": "A person and their tombstone are not the same subject."
},
{
# Scenario 8: Same person at different ages (Einstein young vs old)
"label": "Same person at different ages",
"image1_url": "https://commons.wikimedia.org/wiki/File:JimmyCarterPortrait2.jpg",
"image2_url": "https://commons.wikimedia.org/wiki/File:Jimmy_Carter_and_Rosalynn_Carter_on_Plains_Peanut_Festival_(cropped).jpg",
"answer": "Yes",
"feedback": "The same person at different ages is still the same subject."
}
]
def check_answer(scenario_idx, user_answer):
scenario = scenarios[scenario_idx]
correct = scenario["answer"]
feedback = scenario["feedback"]
if user_answer == correct:
return f"✅ Correct! {feedback}"
else:
return f"❌ Incorrect. The correct answer is **{correct}**. {feedback}"
# --- Session state init ---
if "scenario_idx" not in st.session_state:
st.session_state.scenario_idx = 0
if "feedback" not in st.session_state:
st.session_state.feedback = None
if "answered" not in st.session_state:
st.session_state.answered = False
# Preload all images once at startup
if "images" not in st.session_state:
with st.spinner("Loading all images, please wait…"):
st.session_state.images = [
(
load_image(s["image1_url"]),
load_image(s["image2_url"]),
)
for s in scenarios
]
st.title("Image Subject Comparison Tutorial")
st.markdown(
"This tutorial demonstrates rules for determining if two images depict the same subject. "
"Answer each scenario to advance to the next one."
)
idx = st.session_state.scenario_idx
total = len(scenarios)
if idx >= total:
st.success("🎉 You've completed all scenarios! Well done.")
if st.button("🔄 Restart"):
st.session_state.scenario_idx = 0
st.session_state.feedback = None
st.session_state.answered = False
st.rerun()
else:
scenario = scenarios[idx]
st.markdown(f"### Scenario {idx + 1} of {total}: *{scenario['label']}*")
st.progress(idx / total)
img1, img2 = st.session_state.images[idx]
col1, col2 = st.columns(2)
with col1:
if img1:
st.image(img1, caption="Image 1", use_container_width=True)
else:
st.error("Image 1 could not be loaded.")
with col2:
if img2:
st.image(img2, caption="Image 2", use_container_width=True)
else:
st.error("Image 2 could not be loaded.")
st.markdown("### Are these the same subject?")
if not st.session_state.answered:
btn_col1, btn_col2 = st.columns(2)
with btn_col1:
if st.button("✅ Yes", use_container_width=True):
st.session_state.feedback = check_answer(idx, "Yes")
st.session_state.answered = True
st.rerun()
with btn_col2:
if st.button("❌ No", use_container_width=True):
st.session_state.feedback = check_answer(idx, "No")
st.session_state.answered = True
st.rerun()
else:
st.info(st.session_state.feedback)
if st.button("➡️ Next Scenario", use_container_width=True):
st.session_state.scenario_idx += 1
st.session_state.feedback = None
st.session_state.answered = False
st.rerun()