Spaces:
Sleeping
Sleeping
Commit
ยท
66d71ff
1
Parent(s):
1b881b3
Add interactive HTML UI for animal image classification
Browse filesIntroduces a styled web interface allowing users to upload animal images for classification directly from the browser. Enhances usability by providing image preview, classification feedback, and error handling, making the API more accessible to non-technical users.
app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
from fastapi import FastAPI, File, UploadFile
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
from PIL import Image
|
| 5 |
from tensorflow.keras.models import load_model
|
|
@@ -19,9 +20,199 @@ app.add_middleware(
|
|
| 19 |
|
| 20 |
ANIMALS = ['Cat', 'Dog', 'Panda'] # Animal names here, these represent the labels of the images that we trained our model on.
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
model = load_model("hf://nathansegers/masterclass-2025")
|
|
|
|
| 1 |
from fastapi import FastAPI, File, UploadFile
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from fastapi.responses import HTMLResponse
|
| 4 |
import numpy as np
|
| 5 |
from PIL import Image
|
| 6 |
from tensorflow.keras.models import load_model
|
|
|
|
| 20 |
|
| 21 |
ANIMALS = ['Cat', 'Dog', 'Panda'] # Animal names here, these represent the labels of the images that we trained our model on.
|
| 22 |
|
| 23 |
+
|
| 24 |
+
@app.get("/", response_class=HTMLResponse)
|
| 25 |
+
def test_upload():
|
| 26 |
+
html_content = """
|
| 27 |
+
<!DOCTYPE html>
|
| 28 |
+
<html lang="en">
|
| 29 |
+
<head>
|
| 30 |
+
<meta charset="UTF-8">
|
| 31 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 32 |
+
<title>Animal Image Classifier</title>
|
| 33 |
+
<style>
|
| 34 |
+
body {
|
| 35 |
+
font-family: Arial, sans-serif;
|
| 36 |
+
max-width: 800px;
|
| 37 |
+
margin: 50px auto;
|
| 38 |
+
padding: 20px;
|
| 39 |
+
background-color: #f5f5f5;
|
| 40 |
+
}
|
| 41 |
+
.container {
|
| 42 |
+
background-color: white;
|
| 43 |
+
padding: 30px;
|
| 44 |
+
border-radius: 10px;
|
| 45 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
| 46 |
+
}
|
| 47 |
+
h1 {
|
| 48 |
+
color: #333;
|
| 49 |
+
text-align: center;
|
| 50 |
+
}
|
| 51 |
+
.upload-section {
|
| 52 |
+
margin: 20px 0;
|
| 53 |
+
padding: 20px;
|
| 54 |
+
border: 2px dashed #ccc;
|
| 55 |
+
border-radius: 5px;
|
| 56 |
+
text-align: center;
|
| 57 |
+
}
|
| 58 |
+
input[type="file"] {
|
| 59 |
+
margin: 10px 0;
|
| 60 |
+
}
|
| 61 |
+
button {
|
| 62 |
+
background-color: #4CAF50;
|
| 63 |
+
color: white;
|
| 64 |
+
padding: 10px 20px;
|
| 65 |
+
border: none;
|
| 66 |
+
border-radius: 5px;
|
| 67 |
+
cursor: pointer;
|
| 68 |
+
font-size: 16px;
|
| 69 |
+
}
|
| 70 |
+
button:hover {
|
| 71 |
+
background-color: #45a049;
|
| 72 |
+
}
|
| 73 |
+
button:disabled {
|
| 74 |
+
background-color: #cccccc;
|
| 75 |
+
cursor: not-allowed;
|
| 76 |
+
}
|
| 77 |
+
.preview {
|
| 78 |
+
margin: 20px 0;
|
| 79 |
+
text-align: center;
|
| 80 |
+
}
|
| 81 |
+
.preview img {
|
| 82 |
+
max-width: 300px;
|
| 83 |
+
max-height: 300px;
|
| 84 |
+
border-radius: 5px;
|
| 85 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
| 86 |
+
}
|
| 87 |
+
.result {
|
| 88 |
+
margin-top: 20px;
|
| 89 |
+
padding: 20px;
|
| 90 |
+
background-color: #e8f5e9;
|
| 91 |
+
border-radius: 5px;
|
| 92 |
+
text-align: center;
|
| 93 |
+
font-size: 20px;
|
| 94 |
+
font-weight: bold;
|
| 95 |
+
color: #2e7d32;
|
| 96 |
+
}
|
| 97 |
+
.error {
|
| 98 |
+
background-color: #ffebee;
|
| 99 |
+
color: #c62828;
|
| 100 |
+
}
|
| 101 |
+
.loading {
|
| 102 |
+
display: none;
|
| 103 |
+
margin: 20px 0;
|
| 104 |
+
text-align: center;
|
| 105 |
+
}
|
| 106 |
+
.spinner {
|
| 107 |
+
border: 4px solid #f3f3f3;
|
| 108 |
+
border-top: 4px solid #4CAF50;
|
| 109 |
+
border-radius: 50%;
|
| 110 |
+
width: 40px;
|
| 111 |
+
height: 40px;
|
| 112 |
+
animation: spin 1s linear infinite;
|
| 113 |
+
margin: 0 auto;
|
| 114 |
+
}
|
| 115 |
+
@keyframes spin {
|
| 116 |
+
0% { transform: rotate(0deg); }
|
| 117 |
+
100% { transform: rotate(360deg); }
|
| 118 |
+
}
|
| 119 |
+
</style>
|
| 120 |
+
</head>
|
| 121 |
+
<body>
|
| 122 |
+
<div class="container">
|
| 123 |
+
<h1>๐พ Animal Image Classifier</h1>
|
| 124 |
+
<p style="text-align: center; color: #666;">Upload an image of a Cat, Dog, or Panda to classify it!</p>
|
| 125 |
+
|
| 126 |
+
<div class="upload-section">
|
| 127 |
+
<input type="file" id="imageInput" accept="image/*">
|
| 128 |
+
<br>
|
| 129 |
+
<button onclick="uploadImage()" id="uploadBtn">Classify Image</button>
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
<div class="loading" id="loading">
|
| 133 |
+
<div class="spinner"></div>
|
| 134 |
+
<p>Classifying...</p>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<div class="preview" id="preview"></div>
|
| 138 |
+
|
| 139 |
+
<div id="result"></div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<script>
|
| 143 |
+
let selectedFile = null;
|
| 144 |
+
|
| 145 |
+
document.getElementById('imageInput').addEventListener('change', function(e) {
|
| 146 |
+
const file = e.target.files[0];
|
| 147 |
+
if (file) {
|
| 148 |
+
selectedFile = file;
|
| 149 |
+
// Show preview
|
| 150 |
+
const reader = new FileReader();
|
| 151 |
+
reader.onload = function(e) {
|
| 152 |
+
document.getElementById('preview').innerHTML =
|
| 153 |
+
'<img src="' + e.target.result + '" alt="Preview">';
|
| 154 |
+
}
|
| 155 |
+
reader.readAsDataURL(file);
|
| 156 |
+
document.getElementById('result').innerHTML = '';
|
| 157 |
+
}
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
async function uploadImage() {
|
| 161 |
+
if (!selectedFile) {
|
| 162 |
+
alert('Please select an image first!');
|
| 163 |
+
return;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
const uploadBtn = document.getElementById('uploadBtn');
|
| 167 |
+
const loading = document.getElementById('loading');
|
| 168 |
+
const resultDiv = document.getElementById('result');
|
| 169 |
+
|
| 170 |
+
// Show loading, disable button
|
| 171 |
+
uploadBtn.disabled = true;
|
| 172 |
+
loading.style.display = 'block';
|
| 173 |
+
resultDiv.innerHTML = '';
|
| 174 |
+
|
| 175 |
+
const formData = new FormData();
|
| 176 |
+
formData.append('img', selectedFile);
|
| 177 |
+
|
| 178 |
+
try {
|
| 179 |
+
const response = await fetch('/upload/image', {
|
| 180 |
+
method: 'POST',
|
| 181 |
+
body: formData
|
| 182 |
+
});
|
| 183 |
+
|
| 184 |
+
if (response.ok) {
|
| 185 |
+
const result = await response.text();
|
| 186 |
+
const animal = result.replace(/"/g, ''); // Remove quotes if present
|
| 187 |
+
|
| 188 |
+
// Display result with emoji
|
| 189 |
+
const emojis = {
|
| 190 |
+
'Cat': '๐ฑ',
|
| 191 |
+
'Dog': '๐ถ',
|
| 192 |
+
'Panda': '๐ผ'
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
resultDiv.innerHTML =
|
| 196 |
+
'<div class="result">Prediction: ' +
|
| 197 |
+
(emojis[animal] || '') + ' ' + animal + '</div>';
|
| 198 |
+
} else {
|
| 199 |
+
resultDiv.innerHTML =
|
| 200 |
+
'<div class="result error">Error: ' + response.status + '</div>';
|
| 201 |
+
}
|
| 202 |
+
} catch (error) {
|
| 203 |
+
resultDiv.innerHTML =
|
| 204 |
+
'<div class="result error">Error: ' + error.message + '</div>';
|
| 205 |
+
} finally {
|
| 206 |
+
// Hide loading, enable button
|
| 207 |
+
loading.style.display = 'none';
|
| 208 |
+
uploadBtn.disabled = false;
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
</script>
|
| 212 |
+
</body>
|
| 213 |
+
</html>
|
| 214 |
+
"""
|
| 215 |
+
return HTMLResponse(content=html_content)
|
| 216 |
|
| 217 |
|
| 218 |
model = load_model("hf://nathansegers/masterclass-2025")
|