csabhay commited on
Commit
a1746b1
·
1 Parent(s): 2b21d93
Files changed (1) hide show
  1. app.py +40 -93
app.py CHANGED
@@ -3,120 +3,67 @@ import numpy as np
3
  from sklearn.cluster import KMeans
4
  from PIL import Image
5
  import os
6
- import traceback
7
  from io import BytesIO
8
 
9
- def compress_kmeans(image: np.ndarray, k: int) -> np.ndarray:
10
- """Compress an RGB image to k colours using K‑Means."""
11
  pixels = image.reshape(-1, 3)
12
- model = KMeans(n_clusters=k, n_init="auto", random_state=42)
13
  model.fit(pixels)
14
  centres = model.cluster_centers_.astype(np.uint8)
15
  labels = model.predict(pixels)
16
- return centres[labels].reshape(image.shape)
 
17
 
18
- def compress_and_report(filepath, k):
19
- """Gradio callback: compress, generate report, return outputs."""
20
  if filepath is None:
21
- return None, "<div style='padding: 20px; color: #64748b;'>Upload an image to view the report.</div>"
22
 
23
- try:
24
- # Extract base filename
25
- base = os.path.basename(str(filepath))
26
- orig_filename = os.path.splitext(base)[0]
27
 
28
- # Load original
29
- orig_pil = Image.open(filepath).convert("RGB")
30
- orig_np = np.array(orig_pil)
31
 
32
- # Compress
33
- comp_np = compress_kmeans(orig_np, k)
34
- comp_pil = Image.fromarray(comp_np)
 
35
 
36
- # Compute file sizes (PNG)
37
- buf_orig = BytesIO()
38
- orig_pil.save(buf_orig, format="PNG")
39
- orig_size = len(buf_orig.getvalue())
40
 
41
- buf_comp = BytesIO()
42
- comp_pil.save(buf_comp, format="PNG")
43
- comp_size = len(buf_comp.getvalue())
44
 
45
- # Metrics
46
- mse = np.mean((orig_np.astype(np.float64) - comp_np.astype(np.float64)) ** 2)
47
- psnr = float("inf") if mse == 0 else 20.0 * np.log10(255.0 / np.sqrt(mse))
48
- ratio = orig_size / comp_size
49
- saved_pct = (1.0 - (comp_size / orig_size)) * 100.0
50
- orig_kb = orig_size / 1024
51
- comp_kb = comp_size / 1024
52
 
53
- # Quality badge
54
- if psnr > 35:
55
- badge = '<span style="color:#16a34a;">Excellent</span>'
56
- elif psnr > 28:
57
- badge = '<span style="color:#eab308;">Good</span>'
58
- else:
59
- badge = '<span style="color:#dc2626;">Acceptable</span>'
60
 
61
- # HTML Report
62
- report_html = f"""
63
- <div style="background: #f8f9fa; padding: 16px; border-radius: 8px; border-left: 4px solid #2563eb; font-family: sans-serif;">
64
- <h4 style="margin: 0 0 12px 0; color: #1e293b;">Compression Report</h4>
65
- <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
66
- <tr style="border-bottom: 1px solid #e2e8f0;">
67
- <td style="padding: 6px 0; font-weight: 600; color: #475569;">PSNR</td>
68
- <td style="padding: 6px 0; color: #0f172a;">{psnr:.2f} dB</td>
69
- <td style="padding: 6px 0;">{badge}</td>
70
- </tr>
71
- <tr style="border-bottom: 1px solid #e2e8f0;">
72
- <td style="padding: 6px 0; font-weight: 600; color: #475569;">MSE</td>
73
- <td style="padding: 6px 0; color: #0f172a;" colspan="2">{mse:.2f}</td>
74
- </tr>
75
- <tr style="border-bottom: 1px solid #e2e8f0;">
76
- <td style="padding: 6px 0; font-weight: 600; color: #475569;">Compression Ratio</td>
77
- <td style="padding: 6px 0; color: #0f172a;" colspan="2">{ratio:.2f}x</td>
78
- </tr>
79
- <tr style="border-bottom: 1px solid #e2e8f0;">
80
- <td style="padding: 6px 0; font-weight: 600; color: #475569;">Original Size</td>
81
- <td style="padding: 6px 0; color: #0f172a;" colspan="2">{orig_kb:.2f} KB</td>
82
- </tr>
83
- <tr>
84
- <td style="padding: 6px 0; font-weight: 600; color: #475569;">Compressed Size</td>
85
- <td style="padding: 6px 0; color: #0f172a;">{comp_kb:.2f} KB</td>
86
- <td style="padding: 6px 0; color: #2563eb; font-weight: 600;">↓ {saved_pct:.1f}%</td>
87
- </tr>
88
- </table>
89
- </div>
90
- """
91
 
92
- download_name = f"{orig_filename}_compressed_{int(comp_kb)}KB.png"
93
- return gr.update(value=comp_np, download_name=download_name), report_html
94
-
95
- except Exception as e:
96
- error_trace = traceback.format_exc()
97
- error_html = f"""
98
- <div style="background: #fee2e2; padding: 16px; border-radius: 8px; border-left: 4px solid #dc2626; font-family: monospace; font-size: 13px;">
99
- <strong style="color: #991b1b;">Runtime Error</strong>
100
- <pre style="white-space: pre-wrap; word-wrap: break-word; margin-top: 8px;">{error_trace}</pre>
101
- </div>
102
- """
103
- return None, error_html
104
-
105
- # Gradio Interface
106
  iface = gr.Interface(
107
- fn=compress_and_report,
108
  inputs=[
109
- gr.Image(type="filepath", label="Upload Image"),
110
- gr.Slider(minimum=2, maximum=64, step=2, value=16, label="Number of Colors (k)"),
111
  ],
112
  outputs=[
113
- gr.Image(type="numpy", label="Compressed Image"),
114
- gr.Markdown(label="Metrics Report"),
 
115
  ],
116
- title="Image Compression using K-Means",
117
- description="Upload an image and adjust number of colors (k) to see lossy compression in action.",
118
  )
119
 
120
- if __name__ == "__main__":
121
- # Set share=True to get a public link (valid for 72 hours)
122
- iface.launch(share=True)
 
3
  from sklearn.cluster import KMeans
4
  from PIL import Image
5
  import os
6
+ import tempfile
7
  from io import BytesIO
8
 
9
+ def compress_image(image, k):
 
10
  pixels = image.reshape(-1, 3)
11
+ model = KMeans(n_clusters=k, n_init=10, random_state=42)
12
  model.fit(pixels)
13
  centres = model.cluster_centers_.astype(np.uint8)
14
  labels = model.predict(pixels)
15
+ compressed = centres[labels].reshape(image.shape)
16
+ return compressed
17
 
18
+ def process(filepath, k):
 
19
  if filepath is None:
20
+ return None, "Please upload an image.", None
21
 
22
+ # Load original
23
+ orig = np.array(Image.open(filepath).convert('RGB'))
 
 
24
 
25
+ # Compress
26
+ comp = compress_image(orig, k)
 
27
 
28
+ def get_size(img):
29
+ with BytesIO() as buf:
30
+ Image.fromarray(img).save(buf, format='PNG')
31
+ return len(buf.getvalue())
32
 
33
+ orig_size = get_size(orig)
34
+ comp_size = get_size(comp)
35
+ ratio = orig_size / comp_size
36
+ saved = (1 - comp_size/orig_size) * 100
37
 
38
+ # PSNR
39
+ mse = np.mean((orig.astype(float) - comp.astype(float)) ** 2)
40
+ psnr = 20 * np.log10(255.0 / np.sqrt(mse)) if mse > 0 else float('inf')
41
 
42
+ stats = (f"**Original:** {orig_size/1024:.1f} KB \n"
43
+ f"**Compressed:** {comp_size/1024:.1f} KB \n"
44
+ f"**Compression ratio:** {ratio:.1f}x \n"
45
+ f"**Space saved:** {saved:.1f}% \n"
46
+ f"**PSNR:** {psnr:.1f} dB")
 
 
47
 
48
+ temp = tempfile.mkdtemp()
49
+ out_path = os.path.join(temp, 'compressed.png')
50
+ Image.fromarray(comp).save(out_path)
 
 
 
 
51
 
52
+ return comp, stats, out_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  iface = gr.Interface(
55
+ fn=process,
56
  inputs=[
57
+ gr.Image(type='filepath', label='Upload an Image'),
58
+ gr.Slider(minimum=2, maximum=64, step=2, value=16, label='Number of colours (k)')
59
  ],
60
  outputs=[
61
+ gr.Image(label='Compressed Image'),
62
+ gr.Markdown(label='Compression Statistics'),
63
+ gr.File(label='Download Compressed Image')
64
  ],
65
+ title='Image Compression with K‑Means',
66
+ description='Reduce the number of colours in an image using K-Means clustering.'
67
  )
68
 
69
+ iface.launch()