"""
Product Category Prediction System — Computer Vision
Run with: streamlit run app.py
"""
import streamlit as st
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
import os
import warnings
from pathlib import Path
from PIL import Image
warnings.filterwarnings("ignore")
# ─── Page Config ───
st.set_page_config(
page_title="Product Category Classifier",
page_icon="🖼️",
layout="wide",
initial_sidebar_state="expanded"
)
st.markdown("""
""", unsafe_allow_html=True)
# ─── Constants ───
APP_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
MODEL_PATH = APP_DIR / "mobilenetv2_local.h5"
IMG_SIZE = (224, 224)
CLASS_NAMES = [
"BABY_PRODUCTS",
"BEAUTY_HEALTH",
"CLOTHING_ACCESSORIES_JEWELLERY",
"ELECTRONICS",
"GROCERY",
"HOBBY_ARTS_STATIONERY",
"HOME_KITCHEN_TOOLS",
"PET_SUPPLIES",
"SPORTS_OUTDOOR",
]
CATEGORY_ICONS = {
"BABY_PRODUCTS": "👶",
"BEAUTY_HEALTH": "💄",
"CLOTHING_ACCESSORIES_JEWELLERY": "👗",
"ELECTRONICS": "📱",
"GROCERY": "🛒",
"HOBBY_ARTS_STATIONERY": "🎨",
"HOME_KITCHEN_TOOLS": "🏠",
"PET_SUPPLIES": "🐾",
"SPORTS_OUTDOOR": "⚽",
}
CATEGORY_COLORS = {
"BABY_PRODUCTS": "#f472b6",
"BEAUTY_HEALTH": "#fb923c",
"CLOTHING_ACCESSORIES_JEWELLERY": "#a78bfa",
"ELECTRONICS": "#60a5fa",
"GROCERY": "#34d399",
"HOBBY_ARTS_STATIONERY": "#fbbf24",
"HOME_KITCHEN_TOOLS": "#f87171",
"PET_SUPPLIES": "#2dd4bf",
"SPORTS_OUTDOOR": "#818cf8",
}
# ─── Sidebar ───
with st.sidebar:
st.markdown("### ⚙️ Settings")
st.markdown("**Model Status**")
if MODEL_PATH.exists():
st.markdown('✅ Model loaded', unsafe_allow_html=True)
else:
st.markdown('❌ Model not found', unsafe_allow_html=True)
st.warning(f"Copy `mobilenetv2_local.h5` to:\n```\n{APP_DIR}\n```")
st.markdown("---")
st.markdown("**⚙️ Prediction Settings**")
top_k = st.slider("Top-K results:", 3, 9, 5)
confidence_threshold = st.slider("Min confidence threshold (%):", 10, 90, 50)
st.markdown("---")
st.markdown("**📦 Categories**")
for name in CLASS_NAMES:
icon = CATEGORY_ICONS.get(name, "📦")
color = CATEGORY_COLORS.get(name, "#8b5cf6")
display = name.replace("_", " ").title()
st.markdown(
f'
'
f'{icon} {display}
',
unsafe_allow_html=True
)
st.markdown(
''
'CV Classifier · MobileNetV2 · 9 Categories'
'
',
unsafe_allow_html=True,
)
# ─── Header ───
st.markdown("""
🖼️ Product Image Category Classifier
Upload product images and get instant category predictions powered by MobileNetV2 Transfer Learning
""", unsafe_allow_html=True)
# ─── Model Loading ───
@st.cache_resource
def load_model():
if not MODEL_PATH.exists():
return None
import tensorflow as tf
return tf.keras.models.load_model(str(MODEL_PATH))
def preprocess_image(img: Image.Image):
img_resized = img.resize(IMG_SIZE)
img_array = np.array(img_resized).astype("float32") / 255.0
if len(img_array.shape) == 2:
img_array = np.stack([img_array] * 3, axis=-1)
elif img_array.shape[2] == 4:
img_array = img_array[:, :, :3]
return np.expand_dims(img_array, axis=0)
def predict_category(model, img: Image.Image):
processed = preprocess_image(img)
preds = model.predict(processed, verbose=0).flatten()
sorted_idx = np.argsort(preds)[::-1]
return [
{
"Category": CLASS_NAMES[idx],
"Probability": float(preds[idx]),
"Icon": CATEGORY_ICONS.get(CLASS_NAMES[idx], "📦"),
}
for idx in sorted_idx
]
# ─── Tabs ───
tab1, tab2, tab3, tab4 = st.tabs([
"🖼️ Single Prediction", "📁 Batch Prediction",
"📊 Model Info", "📋 Project Summary",
])
# ═══════════════════════════════════════════
# TAB 1 — Single Prediction
# ═══════════════════════════════════════════
with tab1:
st.markdown("#### Upload a product image to get an instant category prediction")
model = load_model()
uploaded_file = st.file_uploader(
"Upload product image",
type=["jpg", "jpeg", "png", "webp"],
key="single"
)
if uploaded_file:
img = Image.open(uploaded_file).convert("RGB")
col_img, col_result = st.columns([1, 1])
with col_img:
st.image(img, caption="Uploaded Image", use_container_width=True)
with col_result:
if model is not None:
with st.spinner("🔮 Predicting..."):
results = predict_category(model, img)
top = results[0]
icon = top["Icon"]
name = top["Category"].replace("_", " ").title()
prob = top["Probability"]
color = CATEGORY_COLORS.get(top["Category"], "#8b5cf6")
# Main result card
confidence_ok = prob >= confidence_threshold / 100
st.markdown(f"""
{icon}
{name}
{prob:.1%}
{"✅ High confidence" if confidence_ok else "⚠️ Low confidence"}
""", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
st.markdown(f"**Top {top_k} Predictions:**")
top_results = results[:top_k]
fig = go.Figure(go.Bar(
x=[r["Probability"] for r in top_results],
y=[f"{r['Icon']} {r['Category'].replace('_', ' ')}" for r in top_results],
orientation='h',
marker_color=[CATEGORY_COLORS.get(r["Category"], "#8b5cf6") for r in top_results],
text=[f"{r['Probability']:.1%}" for r in top_results],
textposition='inside',
))
fig.update_layout(
template="plotly_dark", height=280,
xaxis=dict(range=[0, 1], tickformat=".0%"),
yaxis=dict(autorange="reversed"),
margin=dict(l=10, r=10, t=10, b=10),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
)
st.plotly_chart(fig, use_container_width=True)
else:
st.error("❌ Model not loaded. Copy `mobilenetv2_local.h5` to the app folder.")
else:
# Category showcase
st.markdown("### 📦 Supported Categories")
cols = st.columns(3)
for i, name in enumerate(CLASS_NAMES):
with cols[i % 3]:
icon = CATEGORY_ICONS.get(name, "📦")
display = name.replace("_", " ").title()
color = CATEGORY_COLORS.get(name, "#8b5cf6")
st.markdown(f"""
""", unsafe_allow_html=True)
# ═══════════════════════════════════════════
# TAB 2 — Batch Prediction
# ═══════════════════════════════════════════
with tab2:
st.markdown("#### Upload multiple product images for bulk category prediction")
uploaded_files = st.file_uploader(
"Upload product images (multiple)",
type=["jpg", "jpeg", "png", "webp"],
accept_multiple_files=True,
key="batch"
)
if uploaded_files:
model = load_model()
if model is None:
st.error("❌ Model not loaded.")
else:
if st.button("🚀 Run Batch Prediction", type="primary", use_container_width=True):
all_results = []
progress = st.progress(0)
status = st.empty()
for idx, file in enumerate(uploaded_files):
status.text(f"Processing {file.name} ({idx+1}/{len(uploaded_files)})...")
img = Image.open(file).convert("RGB")
results = predict_category(model, img)
top = results[0]
all_results.append({
"File": file.name,
"Prediction": top["Category"].replace("_", " ").title(),
"Category Code": top["Category"],
"Confidence": top["Probability"],
"Icon": top["Icon"],
})
progress.progress((idx + 1) / len(uploaded_files))
status.empty()
results_df = pd.DataFrame(all_results)
# KPIs
c1, c2, c3 = st.columns(3)
high_conf = (results_df["Confidence"] >= confidence_threshold / 100).sum()
with c1:
st.markdown(f'{len(results_df)}
Total Images
', unsafe_allow_html=True)
with c2:
st.markdown(f'{results_df["Confidence"].mean():.1%}
Avg Confidence
', unsafe_allow_html=True)
with c3:
st.markdown(f'{high_conf}
High Confidence (≥{confidence_threshold}%)
', unsafe_allow_html=True)
st.markdown("---")
col1, col2 = st.columns(2)
with col1:
cat_counts = results_df["Prediction"].value_counts()
fig_dist = go.Figure(data=[go.Pie(
labels=cat_counts.index, values=cat_counts.values,
marker=dict(colors=[CATEGORY_COLORS.get(k.upper().replace(" ", "_"), "#8b5cf6") for k in cat_counts.index]),
hole=0.4,
)])
fig_dist.update_layout(title="Category Distribution", height=380, template="plotly_dark", paper_bgcolor="rgba(0,0,0,0)")
st.plotly_chart(fig_dist, use_container_width=True)
with col2:
fig_conf = go.Figure()
fig_conf.add_trace(go.Histogram(x=results_df["Confidence"], nbinsx=20, marker_color="#8b5cf6"))
fig_conf.add_vline(x=confidence_threshold / 100, line_dash="dash", line_color="#f87171",
annotation_text=f"Threshold: {confidence_threshold}%")
fig_conf.update_layout(title="Confidence Score Distribution", template="plotly_dark",
height=380, xaxis=dict(tickformat=".0%"),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)")
st.plotly_chart(fig_conf, use_container_width=True)
# Image gallery
st.markdown("### 🖼️ Results Gallery")
gallery_cols = st.columns(4)
for idx, (_, row) in enumerate(results_df.iterrows()):
with gallery_cols[idx % 4]:
img = Image.open(uploaded_files[idx]).convert("RGB")
st.image(img, use_container_width=True)
ok = row["Confidence"] >= confidence_threshold / 100
c = "#4ade80" if ok else "#fb923c"
st.markdown(f'{row["Icon"]} {row["Prediction"]}
', unsafe_allow_html=True)
st.markdown(f'{row["Confidence"]:.1%}
', unsafe_allow_html=True)
with st.expander("📋 Full Results Table"):
display_df = results_df[["File", "Prediction", "Confidence"]].copy()
display_df["Confidence"] = display_df["Confidence"].apply(lambda x: f"{x:.1%}")
st.dataframe(display_df, use_container_width=True, hide_index=True)
else:
st.info("👆 Upload multiple product images to run bulk prediction.")
# ═══════════════════════════════════════════
# TAB 3 — Model Info
# ═══════════════════════════════════════════
with tab3:
st.markdown("#### MobileNetV2 Transfer Learning — Architecture & Training Details")
col1, col2 = st.columns(2)
with col1:
st.markdown("""
### 🤖 Model Architecture
| Property | Value |
|----------|-------|
| **Base Model** | MobileNetV2 (ImageNet) |
| **Input Size** | 224 × 224 × 3 |
| **Output Classes** | 9 |
| **Top Layers** | Flatten → Dense(512) → BN → Dropout(0.5) → Dense(256) → BN → Dropout(0.5) → Softmax |
| **Optimizer** | Adam (lr = 1e-4) |
| **Loss Function** | Categorical Crossentropy |
| **Augmentation** | Horizontal Flip, Zoom(0.2), Shear(0.2) |
| **Callbacks** | EarlyStopping (patience=5), ReduceLROnPlateau |
| **Max Epochs** | 20 |
""")
with col2:
st.markdown("### 📦 Supported Categories")
for name in CLASS_NAMES:
icon = CATEGORY_ICONS.get(name, "📦")
display = name.replace("_", " ").title()
color = CATEGORY_COLORS.get(name, "#8b5cf6")
st.markdown(f"""
{icon}
{display}
""", unsafe_allow_html=True)
st.markdown("---")
st.markdown("### 🏗️ Model Pipeline")
steps = ["Input\nImage", "Resize\n224×224", "Normalize\n÷255", "MobileNetV2\n(Frozen)",
"Flatten", "Dense\n512+BN", "Dense\n256+BN", "Softmax\n9 Classes"]
colors_p = ["#3b82f6","#6366f1","#8b5cf6","#a855f7","#c084fc","#d8b4fe","#818cf8","#10b981"]
fig_pipeline = go.Figure()
for i, (step, color) in enumerate(zip(steps, colors_p)):
fig_pipeline.add_trace(go.Scatter(
x=[i], y=[0], mode='markers+text',
marker=dict(size=64, color=color, line=dict(width=2, color='white')),
text=step, textposition='middle center',
textfont=dict(size=9, color='white'),
showlegend=False,
))
if i < len(steps) - 1:
fig_pipeline.add_annotation(
x=i + 0.5, y=0, text="→", showarrow=False,
font=dict(size=22, color='#8b5cf6'),
)
fig_pipeline.update_layout(
template="plotly_dark", height=160,
xaxis=dict(visible=False, range=[-0.5, len(steps) - 0.5]),
yaxis=dict(visible=False, range=[-0.6, 0.6]),
margin=dict(l=10, r=10, t=10, b=10),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
)
st.plotly_chart(fig_pipeline, use_container_width=True)
# ═══════════════════════════════════════════
# TAB 4 — Project Summary
# ═══════════════════════════════════════════
with tab4:
st.markdown("#### Project Overview & Business Value")
st.markdown("""
### 🎯 Product Image Category Classification (Computer Vision)
**Objective:** Automatically classify Amazon product images into the correct category to
speed up listing workflows and reduce manual categorization errors.
---
#### 📌 Approach: Transfer Learning with MobileNetV2
**Why MobileNetV2?**
- Lightweight and fast (mobile-friendly architecture)
- Pre-trained on ImageNet (1.4M images, 1,000 classes) — strong visual feature extractor
- Depthwise separable convolutions for high efficiency with low parameter count
- Well-suited for fine-tuning on domain-specific datasets
---
#### 📊 Pipeline
1. **Data Preparation** → Train / Val / Check folders (9 categories)
2. **Augmentation** → Horizontal flip, zoom (0.2), shear (0.2) to improve generalization
3. **Transfer Learning** → MobileNetV2 base (frozen weights) + custom top layers
4. **Training** → Up to 20 epochs, EarlyStopping, ReduceLROnPlateau
5. **Evaluation** → Confusion matrix, classification report, per-class accuracy
6. **Deployment** → Real-time prediction via Streamlit (single & batch modes)
---
#### 📦 9 Product Categories
""")
cols = st.columns(3)
for i, name in enumerate(CLASS_NAMES):
with cols[i % 3]:
icon = CATEGORY_ICONS.get(name, "📦")
color = CATEGORY_COLORS.get(name, "#8b5cf6")
display = name.replace("_", " ").title()
st.markdown(f"""
{icon} {display}
""", unsafe_allow_html=True)
st.markdown("""
---
#### 💡 Business Value
| Benefit | Impact |
|---------|--------|
| **Automatic Categorization** | Instantly classify new products without manual review |
| **Faster Listing** | Reduce time sellers spend on product categorization |
| **Error Reduction** | Detect and flag misclassified products |
| **Scalability** | Classify thousands of products in seconds with batch mode |
| **Consistency** | Eliminate human variability in category assignment |
---
""")
st.caption("🔧 Deployment: Streamlit | Model: MobileNetV2 Transfer Learning | 9 Product Categories")