import streamlit as st
from PIL import Image, ImageFile
import numpy as np
import matplotlib.pyplot as plt
import io
# ------------------------------------------------------------------------
# Page configuration and custom styling
st.set_page_config(
page_title="SVD Image Compression",
layout="wide",
page_icon="🎨",
initial_sidebar_state="expanded"
)
st.markdown("""
""", unsafe_allow_html=True)
# Allow large image processing
ImageFile.LOAD_TRUNCATED_IMAGES = True
Image.MAX_IMAGE_PIXELS = None
# ------------------------------------------------------------------------
# Sidebar: Information and Instructions
with st.sidebar:
st.markdown("## 📌 About This App")
st.write(
"This application demonstrates **Singular Value Decomposition (SVD)** for image compression. "
"Upload any image and adjust the rank slider to see real-time compression effects."
)
st.divider()
st.markdown("#### 📖 What is SVD?")
st.write(
"SVD decomposes a matrix into three components: **U**, **Σ**, and **V^T**. "
"By retaining only the largest singular values, we can approximate the original image "
"while significantly reducing storage requirements."
)
st.divider()
st.markdown("#### 🎯 How to Use")
st.markdown("""
1. **Upload** an image file (PNG, JPG, JPEG, BMP, or TIFF)
2. **Adjust** the rank slider to control compression level
3. **Compare** original vs. reconstructed images side-by-side
4. **Analyze** compression statistics and ratio
5. **Download** your compressed image
""")
st.divider()
st.markdown("#### 💡 Pro Tips")
st.info("""
• **Lower rank** = higher compression but lower quality
• **Higher rank** = better quality but less compression
• **Sweet spot**: Usually around 25-50% of max rank
• Use **Quick Presets** for common compression levels
""")
st.divider()
st.markdown("#### 🔬 Educational Value")
st.write(
"This tool helps students and professionals understand how linear algebra "
"concepts apply to real-world data compression problems."
)
# ------------------------------------------------------------------------
# App Title and Header
st.markdown("
🎨 SVD Image Compression Studio
", unsafe_allow_html=True)
st.markdown("Explore the power of linear algebra in image compression using Singular Value Decomposition
", unsafe_allow_html=True)
# ------------------------------------------------------------------------
# Image Upload
st.markdown("### 📤 Step 1: Upload Your Image")
uploaded_file = st.file_uploader(
"Choose an image file to compress",
type=["png", "jpg", "jpeg", "bmp", "tiff"],
help="Supported formats: PNG, JPG, JPEG, BMP, TIFF | Max recommended size: 1024×1024 pixels"
)
if uploaded_file is None:
# Welcome screen with instructions
st.markdown("""
👋 Welcome to SVD Image Compression Studio!
This tool uses Singular Value Decomposition, a powerful linear algebra technique,
to compress images while maintaining visual quality.
🎯 What You'll Discover:
- Real-time image compression using SVD
- Visual comparison between original and compressed images
- Detailed compression statistics and ratios
- Interactive control over compression level
- Download capability for compressed images
🚀 Getting Started:
Simply upload an image using the file uploader above to begin exploring!
""", unsafe_allow_html=True)
# Show demo information
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("""
🔥
Max Compression
~50:1 ratio
Heavy compression
Lower quality
""", unsafe_allow_html=True)
with col2:
st.markdown("""
⚖️
Balanced
~10:1 ratio
Good compression
Balanced quality
""", unsafe_allow_html=True)
with col3:
st.markdown("""
✨
High Quality
~5:1 ratio
Light compression
High quality
""", unsafe_allow_html=True)
st.stop()
# Load Image
try:
image = Image.open(uploaded_file)
st.success(f"✅ Image loaded successfully: **{uploaded_file.name}**")
except Exception as e:
st.error(f"❌ Error loading image: {e}")
st.stop()
# Resize image if too large
max_dimensions = (1024, 1024)
if image.width > max_dimensions[0] or image.height > max_dimensions[1]:
st.warning("⚠️ Resizing large image for optimal processing performance...")
try:
resample_filter = Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.LANCZOS
image.thumbnail(max_dimensions, resample_filter)
except Exception as e:
st.error(f"❌ Error resizing image: {e}")
st.stop()
# Convert image to grayscale numpy array
image_np = np.array(image)
gray_image = np.array(image.convert("L"))
# ------------------------------------------------------------------------
# Compute image properties
image_height, image_width = gray_image.shape
num_pixels = image_height * image_width
# Determine max rank
max_rank = min(image_height, image_width)
default_rank = min(50, max_rank)
# ------------------------------------------------------------------------
# Rank Selection with better UX
st.markdown("### 🎚️ Step 2: Adjust Compression Level")
# Info box about rank selection
st.markdown("""
💡 Tip: The rank determines how many singular values are used.
Lower rank = higher compression but lower quality. Higher rank = better quality but less compression.
""", unsafe_allow_html=True)
# Initialize session state for rank if not exists
if 'rank' not in st.session_state:
st.session_state.rank = default_rank
# Quick presets with better styling
st.markdown("⚡ Quick Presets:
", unsafe_allow_html=True)
preset_col1, preset_col2, preset_col3, preset_col4 = st.columns(4)
with preset_col1:
if st.button("🔥 Max Compression", use_container_width=True, help="~5% of max rank"):
st.session_state.rank = max(1, max_rank // 20)
st.rerun()
with preset_col2:
if st.button("⚖️ Balanced", use_container_width=True, help="25% of max rank"):
st.session_state.rank = max_rank // 4
st.rerun()
with preset_col3:
if st.button("✨ High Quality", use_container_width=True, help="50% of max rank"):
st.session_state.rank = max_rank // 2
st.rerun()
with preset_col4:
if st.button("🎯 Ultra HD", use_container_width=True, help="90% of max rank"):
st.session_state.rank = int(max_rank * 0.9)
st.rerun()
# Slider below presets
col1, col2, col3 = st.columns([1, 3, 1])
with col2:
rank = st.slider(
"Select Compression Rank",
min_value=1,
max_value=max_rank,
value=st.session_state.rank,
step=1,
help=f"Lower values = more compression. Range: 1 to {max_rank}"
)
# Update session state when slider changes
st.session_state.rank = rank
# Show percentage
rank_percentage = (rank / max_rank) * 100
st.caption(f"Current: **{rank}** ({rank_percentage:.1f}% of maximum rank)")
# ------------------------------------------------------------------------
# Perform SVD
with st.spinner("🔄 Processing SVD compression..."):
U, S, VT = np.linalg.svd(gray_image, full_matrices=False)
S_diag = np.diag(S[:rank])
Xprox = U[:, :rank] @ S_diag @ VT[:rank, :]
# ------------------------------------------------------------------------
# Compute Compression Statistics (moved here before image display)
uncompressed_size = num_pixels
compressed_size = rank * (image_width + image_height + 1)
compression_ratio = uncompressed_size / compressed_size if compressed_size > 0 else float('inf')
space_saved = ((uncompressed_size - compressed_size) / uncompressed_size) * 100
# ------------------------------------------------------------------------
# Layout: Original and Reconstructed Images
st.markdown("### 🖼️ Step 3: Visual Comparison")
col_original, col_reconstructed = st.columns(2)
with col_original:
st.markdown("""
📌 Original Image
""", unsafe_allow_html=True)
fig1, ax1 = plt.subplots(figsize=(8, 8), facecolor='white')
ax1.imshow(gray_image, cmap="gray")
ax1.axis("off")
plt.tight_layout()
st.pyplot(fig1)
plt.close(fig1)
st.caption(f"Size: {image_width}×{image_height} | Total: {num_pixels:,} pixels")
with col_reconstructed:
st.markdown("""
🔧 Compressed Image (Rank {})
""".format(rank), unsafe_allow_html=True)
fig2, ax2 = plt.subplots(figsize=(8, 8), facecolor='white')
ax2.imshow(Xprox, cmap="gray")
ax2.axis("off")
plt.tight_layout()
st.pyplot(fig2)
plt.close(fig2)
st.caption(f"Compressed Size: {compressed_size:,} values | Ratio: {compression_ratio:.2f}:1")
# ------------------------------------------------------------------------
# Display Metrics
st.markdown("### 📊 Step 4: Compression Analytics")
# Create a nice header for metrics
st.markdown("""
📈 Key Performance Indicators
""", unsafe_allow_html=True)
metric_col1, metric_col2, metric_col3, metric_col4 = st.columns(4)
with metric_col1:
st.metric(
label="🖼️ Image Size",
value=f"{image_width}×{image_height}",
delta=None,
help=f"Total pixels: {num_pixels:,}"
)
with metric_col2:
st.metric(
label="🎚️ Current Rank",
value=f"{rank}",
delta=f"{(rank/max_rank)*100:.1f}%",
help=f"Using {rank} out of {max_rank} possible singular values"
)
with metric_col3:
st.metric(
label="⚡ Compression Ratio",
value=f"{compression_ratio:.2f}:1",
delta="Higher is better",
help="How much the image is compressed"
)
with metric_col4:
st.metric(
label="💾 Space Saved",
value=f"{space_saved:.1f}%",
delta=f"{uncompressed_size - compressed_size:,}",
help="Reduction in storage requirements"
)
# ------------------------------------------------------------------------
# Detailed Statistics Card - Using Streamlit Components
st.markdown("### 📈 Detailed Compression Report")
# Create a nice container for stats
stats_container = st.container()
with stats_container:
# Create columns for better layout
stat_col1, stat_col2 = st.columns(2)
with stat_col1:
st.markdown("""
📏 Image Dimensions
{} × {} pixels
""".format(image_width, image_height), unsafe_allow_html=True)
st.markdown("""
📂 Uncompressed Size
{:,} values
""".format(uncompressed_size), unsafe_allow_html=True)
st.markdown("""
🎯 Singular Values Used
{} / {}
""".format(rank, max_rank), unsafe_allow_html=True)
with stat_col2:
st.markdown("""
""".format(num_pixels), unsafe_allow_html=True)
st.markdown("""
🗜️ Compressed Size
{:,} values
""".format(compressed_size), unsafe_allow_html=True)
st.markdown("""
""".format(space_saved), unsafe_allow_html=True)
# Highlight the compression ratio
st.markdown("""
🚀 Compression Ratio
{:.2f}:1
""".format(compression_ratio), unsafe_allow_html=True)
# ------------------------------------------------------------------------
# Compression Progress Visualization
st.markdown("### 📉 Compression Efficiency")
progress_value = min(1.0, rank / max_rank)
st.progress(progress_value)
st.caption(f"Using {rank} out of {max_rank} available singular values ({(rank/max_rank)*100:.1f}%)")
# ------------------------------------------------------------------------
# Additional Insights
with st.expander("🔍 View Technical Details"):
st.write(f"""
**SVD Decomposition Details:**
- **U matrix shape**: {U.shape}
- **Σ (Singular values) shape**: {S.shape}
- **V^T matrix shape**: {VT.shape}
- **Reconstructed matrix shape**: {Xprox.shape}
- **Data type**: {gray_image.dtype}
- **Rank used**: {rank}
- **Maximum possible rank**: {max_rank}
**Storage Analysis:**
- Original: {uncompressed_size:,} values
- Compressed (U): {rank * image_height:,} values
- Compressed (Σ): {rank:,} values
- Compressed (V^T): {rank * image_width:,} values
- **Total compressed**: {compressed_size:,} values
- **Reduction**: {space_saved:.2f}%
""")
# ------------------------------------------------------------------------
# Download Option
st.markdown("### 💾 Step 5: Export Your Compressed Image")
st.markdown("""
📥 Ready to download? Save your compressed image to your device.
""", unsafe_allow_html=True)
col_d1, col_d2, col_d3 = st.columns([1, 2, 1])
with col_d2:
# Convert reconstructed image to PIL
reconstructed_pil = Image.fromarray(np.uint8(np.clip(Xprox, 0, 255)))
# Save to bytes
buf = io.BytesIO()
reconstructed_pil.save(buf, format='PNG')
byte_im = buf.getvalue()
st.download_button(
label="⬇️ Download Compressed Image",
data=byte_im,
file_name=f"compressed_rank_{rank}_{uploaded_file.name}",
mime="image/png",
help="Download the reconstructed image as PNG",
use_container_width=True
)
st.caption(f"📁 Filename: compressed_rank_{rank}_{uploaded_file.name}")
# ------------------------------------------------------------------------
# Footer
st.markdown("""
""", unsafe_allow_html=True)