|
|
import os |
|
|
|
|
|
import streamlit as st |
|
|
from PIL import Image |
|
|
|
|
|
from predictor import predict_image |
|
|
|
|
|
APP_DIR = os.path.dirname(os.path.abspath(__file__)) |
|
|
ASSETS_DIR = os.path.join(APP_DIR, "assets") |
|
|
|
|
|
|
|
|
st.set_page_config(page_title="Image Classifier App", page_icon="π€", layout="centered") |
|
|
st.html(""" |
|
|
<style> |
|
|
.stMainBlockContainer { |
|
|
max-width: 70rem; |
|
|
padding-bottom: 1rem; |
|
|
} |
|
|
</style> |
|
|
""") |
|
|
|
|
|
|
|
|
if "uploaded_image" not in st.session_state: |
|
|
st.session_state["uploaded_image"] = None |
|
|
if "example_selected" not in st.session_state: |
|
|
st.session_state["example_selected"] = False |
|
|
if "prediction_result" not in st.session_state: |
|
|
st.session_state["prediction_result"] = None |
|
|
|
|
|
|
|
|
with st.container(): |
|
|
st.title( |
|
|
body="πΌοΈ Image Classifier with CNN", |
|
|
help="An interactive application to classify images into over 1000 categories.", |
|
|
) |
|
|
st.html("<br>") |
|
|
|
|
|
|
|
|
tab_app, tab_about, tab_architecture = st.tabs( |
|
|
["**App**", "**About**", "**Architecture**"] |
|
|
) |
|
|
|
|
|
|
|
|
with tab_app: |
|
|
|
|
|
col_upload, col_results = st.columns(2, gap="large") |
|
|
|
|
|
|
|
|
with col_upload: |
|
|
st.header("Upload an Image", divider=True) |
|
|
|
|
|
|
|
|
uploaded_file = st.file_uploader( |
|
|
label="Drag and drop an image here or click to browse", |
|
|
type=["jpg", "jpeg", "png", "webp", "avif"], |
|
|
help="Maximum file size is 200MB", |
|
|
key="image_uploader", |
|
|
) |
|
|
|
|
|
st.html("<br>") |
|
|
st.subheader("Or Try an Example", divider=True) |
|
|
|
|
|
|
|
|
selected_example = st.segmented_control( |
|
|
label="Categories", |
|
|
options=["Animal", "Vehicle", "Object", "Building"], |
|
|
default=None, |
|
|
help="Select one of the pre-loaded examples", |
|
|
) |
|
|
|
|
|
st.html("<br>") |
|
|
|
|
|
|
|
|
classify_button = st.button( |
|
|
label="Classify Image", |
|
|
key="classify_btn", |
|
|
type="primary", |
|
|
icon="β¨", |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if uploaded_file or selected_example: |
|
|
st.session_state.prediction_result = None |
|
|
|
|
|
image_to_process = None |
|
|
|
|
|
if uploaded_file: |
|
|
image_to_process = Image.open(uploaded_file) |
|
|
|
|
|
elif selected_example: |
|
|
try: |
|
|
img_path = os.path.join( |
|
|
APP_DIR, "assets", f"{selected_example.lower()}.jpg" |
|
|
) |
|
|
image_to_process = Image.open(img_path) |
|
|
except FileNotFoundError: |
|
|
st.error( |
|
|
f"Error: The example image '{selected_example.lower()}.jpg' was not found." |
|
|
) |
|
|
st.stop() |
|
|
|
|
|
|
|
|
with col_results: |
|
|
st.header("Results", divider=True) |
|
|
|
|
|
|
|
|
if not image_to_process and not st.session_state.prediction_result: |
|
|
st.info("Choose an image or an example to get a prediction.") |
|
|
|
|
|
|
|
|
if image_to_process: |
|
|
st.image(image_to_process, caption="Image to be classified") |
|
|
|
|
|
|
|
|
if classify_button and image_to_process: |
|
|
with st.spinner(text="π§ Analyzing image..."): |
|
|
try: |
|
|
from predictor import predict_image |
|
|
|
|
|
predicted_label, predicted_score = predict_image( |
|
|
image_to_process |
|
|
) |
|
|
st.session_state.prediction_result = { |
|
|
"label": predicted_label.replace("_", " ").title(), |
|
|
"score": predicted_score, |
|
|
} |
|
|
except Exception as e: |
|
|
st.error(f"An error occurred during prediction: {e}") |
|
|
|
|
|
|
|
|
if st.session_state.prediction_result: |
|
|
st.metric( |
|
|
label="Prediction", |
|
|
value=st.session_state.prediction_result["label"], |
|
|
delta=f"{st.session_state.prediction_result['score'] * 100:.2f}%", |
|
|
help="The predicted category and its confidence score.", |
|
|
delta_color="normal", |
|
|
) |
|
|
st.balloons() |
|
|
|
|
|
elif image_to_process: |
|
|
st.info("Click 'Classify Image' to see the prediction.") |
|
|
|
|
|
|
|
|
|
|
|
with tab_about: |
|
|
st.header("About This Project") |
|
|
st.markdown(""" |
|
|
- This project is an **image classification app** powered by a Convolutional Neural Network (CNN). |
|
|
- Simply upload an image, and the app predicts its category from **over 1,000 classes** using a pre-trained **ResNet50** model. |
|
|
- Originally developed as a **multi-service ML system** (FastAPI + Redis + Streamlit), this version has been **adapted into a single Streamlit app** for lightweight, cost-effective deployment on Hugging Face Spaces. |
|
|
|
|
|
### Model & Description |
|
|
- **Model:** ResNet50 (pre-trained on the **ImageNet** dataset with 1,000+ categories). |
|
|
- **Pipeline:** Images are resized, normalized, and passed to the model. |
|
|
- **Output:** The app displays the **Top prediction** with confidence score. |
|
|
|
|
|
[ResNet50](https://www.tensorflow.org/api_docs/python/tf/keras/applications/ResNet50) is widely used in both research and production, making it an excellent showcase of deep learning capabilities and transferable ML skills. |
|
|
""") |
|
|
|
|
|
with tab_architecture: |
|
|
with st.expander("π οΈ View Original System Architecture"): |
|
|
st.image( |
|
|
image="./src/assets/architecture.jpg", |
|
|
caption="Original Microservices Architecture", |
|
|
) |
|
|
|
|
|
st.markdown(""" |
|
|
### Original Architecture |
|
|
- **FastAPI** β REST API for image processing |
|
|
- **Redis** β Message broker for service communication |
|
|
- **Streamlit** β Interactive web UI |
|
|
- **TensorFlow** β Deep learning inference engine |
|
|
- **Locust** β Load testing & benchmarking |
|
|
- **Docker Compose** β Service orchestration |
|
|
|
|
|
### Simplified Version |
|
|
- **Streamlit only** β UI and model combined in a single app |
|
|
- **TensorFlow (ResNet50)** β Core prediction engine |
|
|
- **Docker** β Containerized for Hugging Face Spaces deployment |
|
|
|
|
|
This evolution demonstrates the ability to design a **scalable microservices system** and also **adapt it into a lightweight single-service solution** for cost-effective demos. |
|
|
""") |
|
|
|
|
|
|
|
|
|
|
|
st.divider() |
|
|
st.markdown( |
|
|
""" |
|
|
<div style="text-align: center; margin-bottom: 1.5rem;"> |
|
|
<b>Connect with me:</b> πΌ <a href="https://www.linkedin.com/in/alex-turpo/" target="_blank">LinkedIn</a> β’ |
|
|
π± <a href="https://github.com/iBrokeTheCode" target="_blank">GitHub</a> β’ |
|
|
π€ <a href="https://huggingface.co/iBrokeTheCode" target="_blank">Hugging Face</a> |
|
|
</div> |
|
|
""", |
|
|
unsafe_allow_html=True, |
|
|
) |
|
|
|