SVDImageCompression / src /streamlit_app.py
Jishan2019's picture
Update src/streamlit_app.py
d627d5a verified
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("""
<style>
/* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
/* Global Styling */
* {
font-family: 'Inter', sans-serif;
}
/* Hide Streamlit default elements */
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
.stDeployButton {display: none;}
/* Main Background with Gradient */
.main {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed;
}
/* Content Container */
.block-container {
padding-top: 2rem;
padding-bottom: 2rem;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
/* Sidebar Styling */
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
[data-testid="stSidebar"] * {
color: white !important;
}
[data-testid="stSidebar"] h2 {
color: white !important;
font-weight: 700;
margin-top: 0;
}
[data-testid="stSidebar"] h4 {
color: #ffd700 !important;
font-weight: 600;
margin-top: 1.5rem;
}
/* Main Title */
h1 {
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
font-size: 3rem;
margin-bottom: 0.5rem;
}
/* Subtitle */
.subtitle {
text-align: center;
color: #666;
font-size: 1.1rem;
margin-bottom: 2rem;
}
/* Section Headers */
h3 {
color: #667eea;
font-weight: 700;
border-left: 4px solid #667eea;
padding-left: 12px;
margin-top: 2rem;
margin-bottom: 1rem;
}
/* File Uploader Styling */
[data-testid="stFileUploader"] {
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border: 2px dashed #667eea;
border-radius: 15px;
padding: 2rem;
transition: all 0.3s ease;
}
[data-testid="stFileUploader"]:hover {
border-color: #764ba2;
background: linear-gradient(135deg, #667eea25 0%, #764ba225 100%);
}
/* Info Box */
.stAlert {
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-left: 4px solid #667eea;
border-radius: 10px;
}
/* Slider Styling */
.stSlider {
padding: 1rem 0;
}
/* Compression Stats Card */
.stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
border-radius: 15px;
color: white;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
margin: 2rem 0;
}
.stats-card h4 {
color: white;
margin-top: 0;
font-size: 1.3rem;
font-weight: 700;
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
padding-bottom: 0.8rem;
margin-bottom: 1.5rem;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-row:last-child {
border-bottom: none;
}
.stat-label {
font-weight: 600;
font-size: 1rem;
}
.stat-value {
font-weight: 700;
font-size: 1.1rem;
background: rgba(255, 255, 255, 0.2);
padding: 0.3rem 1rem;
border-radius: 20px;
}
.compression-highlight {
background: #ffd700;
color: #764ba2;
padding: 0.5rem 1.5rem;
border-radius: 25px;
font-size: 1.3rem;
font-weight: 700;
text-align: center;
margin-top: 1rem;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
}
/* Image Container */
.image-container {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
/* Subheader in columns */
.stColumn h4 {
color: #667eea;
font-weight: 700;
text-align: center;
margin-bottom: 1rem;
}
/* Progress Bar */
.stProgress > div > div {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
/* Footer */
.footer {
text-align: center;
margin-top: 3rem;
padding: 2rem;
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-radius: 15px;
color: #667eea;
font-weight: 600;
}
/* Metric Cards */
[data-testid="stMetricValue"] {
font-size: 2rem;
color: #667eea;
font-weight: 700;
}
/* Buttons */
.stButton > button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
border: none;
border-radius: 10px;
padding: 0.5rem 2rem;
transition: all 0.3s ease;
}
.stButton > button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
</style>
""", 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("<h1>🎨 SVD Image Compression Studio</h1>", unsafe_allow_html=True)
st.markdown("<p class='subtitle'>Explore the power of linear algebra in image compression using Singular Value Decomposition</p>", 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("""
<div style='background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
padding: 2rem; border-radius: 15px; margin: 2rem 0;
border-left: 5px solid #667eea;'>
<h3 style='color: #667eea; margin-top: 0;'>πŸ‘‹ Welcome to SVD Image Compression Studio!</h3>
<p style='font-size: 1.1rem; margin-bottom: 1.5rem;'>
This tool uses <strong>Singular Value Decomposition</strong>, a powerful linear algebra technique,
to compress images while maintaining visual quality.
</p>
<h4 style='color: #764ba2;'>🎯 What You'll Discover:</h4>
<ul style='font-size: 1rem; line-height: 1.8;'>
<li>Real-time image compression using SVD</li>
<li>Visual comparison between original and compressed images</li>
<li>Detailed compression statistics and ratios</li>
<li>Interactive control over compression level</li>
<li>Download capability for compressed images</li>
</ul>
<h4 style='color: #764ba2; margin-top: 1.5rem;'>πŸš€ Getting Started:</h4>
<p style='font-size: 1rem;'>
Simply <strong>upload an image</strong> using the file uploader above to begin exploring!
</p>
</div>
""", unsafe_allow_html=True)
# Show demo information
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("""
<div style='background: white; padding: 1.5rem; border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); text-align: center;'>
<div style='font-size: 3rem; margin-bottom: 0.5rem;'>πŸ”₯</div>
<h4 style='color: #667eea; margin: 0.5rem 0;'>Max Compression</h4>
<p style='font-size: 0.9rem; color: #666; margin: 0;'>
~50:1 ratio<br/>
Heavy compression<br/>
Lower quality
</p>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div style='background: white; padding: 1.5rem; border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); text-align: center;'>
<div style='font-size: 3rem; margin-bottom: 0.5rem;'>βš–οΈ</div>
<h4 style='color: #667eea; margin: 0.5rem 0;'>Balanced</h4>
<p style='font-size: 0.9rem; color: #666; margin: 0;'>
~10:1 ratio<br/>
Good compression<br/>
Balanced quality
</p>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown("""
<div style='background: white; padding: 1.5rem; border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); text-align: center;'>
<div style='font-size: 3rem; margin-bottom: 0.5rem;'>✨</div>
<h4 style='color: #667eea; margin: 0.5rem 0;'>High Quality</h4>
<p style='font-size: 0.9rem; color: #666; margin: 0;'>
~5:1 ratio<br/>
Light compression<br/>
High quality
</p>
</div>
""", 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("""
<div style='background: #f8f9fa; padding: 1rem; border-radius: 10px;
border-left: 4px solid #667eea; margin-bottom: 1.5rem;'>
<p style='margin: 0; color: #666;'>
<strong>πŸ’‘ Tip:</strong> The rank determines how many singular values are used.
Lower rank = higher compression but lower quality. Higher rank = better quality but less compression.
</p>
</div>
""", 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("<p style='text-align: center; font-weight: 600; margin: 0 0 0.5rem 0;'>⚑ Quick Presets:</p>", 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("""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 0.5rem; border-radius: 12px 12px 0 0; text-align: center;'>
<h4 style='color: white; margin: 0; font-size: 1.1rem;'>πŸ“Œ Original Image</h4>
</div>
""", 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("""
<div style='background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
padding: 0.5rem; border-radius: 12px 12px 0 0; text-align: center;'>
<h4 style='color: white; margin: 0; font-size: 1.1rem;'>πŸ”§ Compressed Image (Rank {})</h4>
</div>
""".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("""
<div style='background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
padding: 1rem; border-radius: 10px; margin-bottom: 1rem;'>
<p style='margin: 0; text-align: center; color: #666; font-size: 0.95rem;'>
πŸ“ˆ Key Performance Indicators
</p>
</div>
""", 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("""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.5rem; border-radius: 12px; color: white; margin-bottom: 1rem;'>
<p style='margin: 0; font-size: 0.9rem; opacity: 0.9;'>πŸ“ Image Dimensions</p>
<p style='margin: 0.3rem 0 0 0; font-size: 1.5rem; font-weight: bold;'>{} Γ— {} pixels</p>
</div>
""".format(image_width, image_height), unsafe_allow_html=True)
st.markdown("""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.5rem; border-radius: 12px; color: white; margin-bottom: 1rem;'>
<p style='margin: 0; font-size: 0.9rem; opacity: 0.9;'>πŸ“‚ Uncompressed Size</p>
<p style='margin: 0.3rem 0 0 0; font-size: 1.5rem; font-weight: bold;'>{:,} values</p>
</div>
""".format(uncompressed_size), unsafe_allow_html=True)
st.markdown("""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.5rem; border-radius: 12px; color: white; margin-bottom: 1rem;'>
<p style='margin: 0; font-size: 0.9rem; opacity: 0.9;'>🎯 Singular Values Used</p>
<p style='margin: 0.3rem 0 0 0; font-size: 1.5rem; font-weight: bold;'>{} / {}</p>
</div>
""".format(rank, max_rank), unsafe_allow_html=True)
with stat_col2:
st.markdown("""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.5rem; border-radius: 12px; color: white; margin-bottom: 1rem;'>
<p style='margin: 0; font-size: 0.9rem; opacity: 0.9;'>πŸ”’ Total Pixels</p>
<p style='margin: 0.3rem 0 0 0; font-size: 1.5rem; font-weight: bold;'>{:,}</p>
</div>
""".format(num_pixels), unsafe_allow_html=True)
st.markdown("""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.5rem; border-radius: 12px; color: white; margin-bottom: 1rem;'>
<p style='margin: 0; font-size: 0.9rem; opacity: 0.9;'>πŸ—œοΈ Compressed Size</p>
<p style='margin: 0.3rem 0 0 0; font-size: 1.5rem; font-weight: bold;'>{:,} values</p>
</div>
""".format(compressed_size), unsafe_allow_html=True)
st.markdown("""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.5rem; border-radius: 12px; color: white; margin-bottom: 1rem;'>
<p style='margin: 0; font-size: 0.9rem; opacity: 0.9;'>πŸ’Ύ Space Saved</p>
<p style='margin: 0.3rem 0 0 0; font-size: 1.5rem; font-weight: bold;'>{:.1f}%</p>
</div>
""".format(space_saved), unsafe_allow_html=True)
# Highlight the compression ratio
st.markdown("""
<div style='background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
padding: 1.5rem 2rem; border-radius: 15px;
text-align: center; margin: 1.5rem 0;
box-shadow: 0 8px 25px rgba(255, 215, 0, 0.3);'>
<p style='margin: 0; font-size: 1.1rem; color: #764ba2; font-weight: 600;'>πŸš€ Compression Ratio</p>
<p style='margin: 0.5rem 0 0 0; font-size: 2.5rem; color: #764ba2; font-weight: bold;'>{:.2f}:1</p>
</div>
""".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("""
<div style='background: #f8f9fa; padding: 1rem; border-radius: 10px;
border-left: 4px solid #667eea; margin-bottom: 1rem;'>
<p style='margin: 0; color: #666;'>
<strong>πŸ“₯ Ready to download?</strong> Save your compressed image to your device.
</p>
</div>
""", 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("""
<div class='footer'>
<p style='font-size: 1.1rem; margin-bottom: 0.5rem;'><strong>πŸ‘¨β€πŸ« Developed by Dr. Jishan Ahmed</strong></p>
<p style='margin: 0.3rem 0; font-size: 0.95rem;'>Data Science Assistant Professor</p>
<p style='margin: 0.3rem 0; font-size: 0.95rem;'>Department of Mathematics</p>
<p style='margin: 0.3rem 0 1rem 0; font-size: 0.95rem;'>Weber State University</p>
<p style='margin-top: 1rem; font-size: 0.85rem; opacity: 0.7;'>
Powered by NumPy, Matplotlib, and Streamlit | Linear Algebra in Action πŸš€
</p>
</div>
""", unsafe_allow_html=True)