ElmahdiJaouali commited on
Commit
182ba72
·
1 Parent(s): 98cb571

Add SVM dashboard + API with model loading from HF repository

Browse files
Files changed (8) hide show
  1. .env.example +5 -0
  2. Dockerfile +22 -0
  3. README.md +25 -6
  4. api/main.py +351 -0
  5. dashboard/favicon.ico +0 -0
  6. dashboard/index.html +288 -0
  7. dashboard/logo.png +0 -0
  8. requirements.txt +31 -0
.env.example ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Hugging Face Model Repository
2
+ HF_MODEL_REPO=enigmaceo/svm-classification-cat-and-dog
3
+
4
+ # Optional: Hugging Face Token (for private models)
5
+ # HF_TOKEN=your_huggingface_token_here
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Copy requirements first for better caching
7
+ COPY requirements.txt .
8
+
9
+ # Install dependencies
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ # Copy application files
13
+ COPY api/ ./api/
14
+ COPY dashboard/ ./dashboard/
15
+ COPY models/ ./models/
16
+ COPY static/ ./static/
17
+
18
+ # Expose port
19
+ EXPOSE 7860
20
+
21
+ # Run the application
22
+ CMD ["python", "api/main.py"]
README.md CHANGED
@@ -1,12 +1,31 @@
1
  ---
2
- title: Example Cat Vs Dog Classification Svm
3
- emoji: 📉
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: 'this is just example create demo of model on '
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Cat vs Dog Classification SVM
3
+ emoji: 🐱🐶
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
+ app_port: 7860
10
  ---
11
 
12
+ # Cat vs Dog Classification with SVM
13
+
14
+ This Hugging Face Space provides an interactive web interface for classifying cat and dog images using Support Vector Machines.
15
+
16
+ ## Features
17
+
18
+ - **Upload & Classify**: Upload cat or dog images for instant classification
19
+ - **Real-time Results**: Get predictions with confidence scores
20
+ - **Feature Visualization**: See extracted features that led to classification
21
+
22
+ ## Model Information
23
+
24
+ This application loads the trained SVM model from a separate Model Repository to demonstrate proper separation of concerns.
25
+
26
+ ## Technical Stack
27
+
28
+ - **Backend**: FastAPI with Python
29
+ - **Frontend**: HTML5 + Tailwind CSS
30
+ - **Machine Learning**: scikit-learn SVM
31
+ - **Deployment**: Docker on Hugging Face Spaces
api/main.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ FastAPI Backend for SVM Iris Classification
4
+ Provides endpoints for image upload and prediction
5
+ """
6
+
7
+ from fastapi import FastAPI, File, UploadFile, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi.responses import HTMLResponse
11
+ import numpy as np
12
+ import joblib
13
+ import json
14
+ import os
15
+ from typing import Dict, Any
16
+ import cv2
17
+ from PIL import Image
18
+ import io
19
+ import base64
20
+ from huggingface_hub import hf_hub_download
21
+
22
+ app = FastAPI(title="SVM Iris Classification API", version="1.0.0")
23
+
24
+ # Enable CORS
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_credentials=True,
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+ # Mount static files
34
+ app.mount("/static", StaticFiles(directory="../static"), name="static")
35
+
36
+ # Global variables for models and artifacts
37
+ model = None
38
+ scaler = None
39
+ label_encoder = None
40
+ metadata = None
41
+
42
+ def load_models():
43
+ """Load trained models and artifacts from Hugging Face Hub"""
44
+ global model, scaler, label_encoder, metadata
45
+
46
+ try:
47
+ # Model repository configuration
48
+ repo_id = os.getenv("HF_MODEL_REPO", "your-username/cat-dog-svm-model")
49
+
50
+ # Download and load best model (compressed)
51
+ model_path = hf_hub_download(repo_id, "svm_best_model.pkl.gz")
52
+ import gzip
53
+ import pickle
54
+ with gzip.open(model_path, 'rb') as f:
55
+ model = pickle.load(f)
56
+
57
+ # Download and load scaler
58
+ scaler_path = hf_hub_download(repo_id, "scaler.pkl")
59
+ scaler = joblib.load(scaler_path)
60
+
61
+ # Download and load label encoder
62
+ encoder_path = hf_hub_download(repo_id, "label_encoder.pkl")
63
+ label_encoder = joblib.load(encoder_path)
64
+
65
+ # Download and load metadata
66
+ metadata_path = hf_hub_download(repo_id, "metadata.json")
67
+ with open(metadata_path, 'r') as f:
68
+ metadata = json.load(f)
69
+
70
+ print(f"Model and artifacts loaded successfully from {repo_id}")
71
+ return True
72
+ except Exception as e:
73
+ print(f"Error loading models from Hugging Face: {e}")
74
+ print("Falling back to local files...")
75
+ return load_local_models()
76
+
77
+ def load_local_models():
78
+ """Fallback: Load models from local files"""
79
+ global model, scaler, label_encoder, metadata
80
+
81
+ try:
82
+ # Load best model (compressed)
83
+ model_path = "../models/svm_best_model.pkl.gz"
84
+ if os.path.exists(model_path):
85
+ import gzip
86
+ import pickle
87
+ with gzip.open(model_path, 'rb') as f:
88
+ model = pickle.load(f)
89
+
90
+ # Load scaler
91
+ scaler_path = "../models/scaler.pkl"
92
+ if os.path.exists(scaler_path):
93
+ scaler = joblib.load(scaler_path)
94
+
95
+ # Load label encoder
96
+ encoder_path = "../models/label_encoder.pkl"
97
+ if os.path.exists(encoder_path):
98
+ label_encoder = joblib.load(encoder_path)
99
+
100
+ # Load metadata
101
+ metadata_path = "../models/metadata.json"
102
+ if os.path.exists(metadata_path):
103
+ with open(metadata_path, 'r') as f:
104
+ metadata = json.load(f)
105
+
106
+ print("Model and artifacts loaded successfully from local files")
107
+ return True
108
+ except Exception as e:
109
+ print(f"Error loading local models: {e}")
110
+ return False
111
+
112
+ def extract_hog_features(image, pixels_per_cell=(8, 8)):
113
+ """Extract HOG features from image"""
114
+ from skimage.feature import hog
115
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
116
+ features, hog_img = hog(
117
+ gray,
118
+ orientations=9,
119
+ pixels_per_cell=pixels_per_cell,
120
+ cells_per_block=(2, 2),
121
+ block_norm='L2-Hys',
122
+ visualize=True,
123
+ transform_sqrt=True
124
+ )
125
+ return features.astype(np.float32)
126
+
127
+ def extract_color_histogram(image, bins=32):
128
+ """Extract color histogram features from HSV image"""
129
+ hsv = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
130
+ hist_h = np.histogram(hsv[:,:,0], bins=bins, range=(0, 180))[0]
131
+ hist_s = np.histogram(hsv[:,:,1], bins=bins, range=(0, 256))[0]
132
+ hist_v = np.histogram(hsv[:,:,2], bins=bins, range=(0, 256))[0]
133
+ return np.concatenate([hist_h, hist_s, hist_v]).astype(np.float32)
134
+
135
+ def extract_lbp_features(image, radius=3, n_points=24):
136
+ """Extract Local Binary Pattern features for texture"""
137
+ from skimage.feature import local_binary_pattern
138
+ gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
139
+ lbp = local_binary_pattern(gray, n_points, radius, method='uniform')
140
+ hist, _ = np.histogram(lbp.ravel(), bins=n_points + 2)
141
+ hist = hist.astype(np.float32)
142
+ hist /= (hist.sum() + 1e-7) # Normalize
143
+ return hist
144
+
145
+ def extract_features_from_image(image_data: bytes) -> np.ndarray:
146
+ """
147
+ Extract HOG, color histogram, and LBP features from uploaded image
148
+ Same feature extraction as used in training
149
+ """
150
+ try:
151
+ # Convert bytes to PIL Image
152
+ image = Image.open(io.BytesIO(image_data))
153
+
154
+ # Convert to numpy array and RGB
155
+ img_array = np.array(image)
156
+ if len(img_array.shape) == 2: # Grayscale
157
+ img_array = cv2.cvtColor(img_array, cv2.COLOR_GRAY2RGB)
158
+ elif img_array.shape[2] == 4: # RGBA
159
+ img_array = cv2.cvtColor(img_array, cv2.COLOR_RGBA2RGB)
160
+
161
+ # Resize to match training size
162
+ img_resized = cv2.resize(img_array, (128, 128))
163
+
164
+ # Extract HOG features
165
+ hog_feat = extract_hog_features(img_resized)
166
+
167
+ # Extract color histogram
168
+ col_feat = extract_color_histogram(img_resized)
169
+
170
+ # Extract LBP features
171
+ lbp_feat = extract_lbp_features(img_resized)
172
+
173
+ # Combine features
174
+ combined_features = np.concatenate([hog_feat, col_feat, lbp_feat])
175
+
176
+ return combined_features.reshape(1, -1)
177
+
178
+ except Exception as e:
179
+ raise HTTPException(status_code=400, detail=f"Error processing image: {str(e)}")
180
+
181
+ @app.on_event("startup")
182
+ async def startup_event():
183
+ """Load models on startup"""
184
+ success = load_models()
185
+ if not success:
186
+ print("Warning: Could not load models. Please run training script first.")
187
+
188
+ @app.get("/", response_class=HTMLResponse)
189
+ async def root():
190
+ """Serve the dashboard"""
191
+ try:
192
+ with open("../dashboard/index.html", "r") as f:
193
+ return HTMLResponse(content=f.read())
194
+ except FileNotFoundError:
195
+ return HTMLResponse(content="<h1>SVM Classification API</h1><p>Dashboard not found. Please check dashboard folder.</p>")
196
+
197
+ @app.get("/api/health")
198
+ async def health_check():
199
+ """Health check endpoint"""
200
+ return {
201
+ "status": "healthy",
202
+ "model_loaded": model is not None,
203
+ "best_kernel": metadata.get('best_kernel') if metadata else None
204
+ }
205
+
206
+ @app.get("/api/models")
207
+ async def get_models():
208
+ """Get available models and their information"""
209
+ if not metadata:
210
+ raise HTTPException(status_code=503, detail="Models not loaded")
211
+
212
+ return {
213
+ "best_kernel": metadata.get('best_kernel'),
214
+ "classes": label_encoder.classes_.tolist() if label_encoder else [],
215
+ "model_info": metadata.get("model_info", {}),
216
+ "results": metadata.get("model_results", {})
217
+ }
218
+
219
+ @app.post("/api/predict")
220
+ async def predict_image(file: UploadFile = File(...)):
221
+ """
222
+ Predict cat or dog from uploaded image
223
+ """
224
+ print("Prediction request received")
225
+
226
+ if not model:
227
+ raise HTTPException(status_code=503, detail="Model not loaded")
228
+
229
+ if not scaler:
230
+ raise HTTPException(status_code=503, detail="Scaler not loaded")
231
+
232
+ if not label_encoder:
233
+ raise HTTPException(status_code=503, detail="Label encoder not loaded")
234
+
235
+ try:
236
+ # Read image data
237
+ image_data = await file.read()
238
+ print(f"Image data size: {len(image_data)} bytes")
239
+
240
+ # Extract features (same pipeline as training)
241
+ features = extract_features_from_image(image_data)
242
+ print(f"Features shape: {features.shape}")
243
+
244
+ # Scale features
245
+ features_scaled = scaler.transform(features)
246
+ print(f"Scaled features shape: {features_scaled.shape}")
247
+
248
+ # Make prediction
249
+ prediction = model.predict(features_scaled)[0]
250
+ probabilities = None
251
+
252
+ # Get decision function values if available
253
+ if hasattr(model, 'decision_function'):
254
+ decision_values = model.decision_function(features_scaled)[0]
255
+ # Convert to probabilities using softmax
256
+ exp_values = np.exp(decision_values - np.max(decision_values))
257
+ probabilities = exp_values / np.sum(exp_values)
258
+
259
+ # Map prediction to class name
260
+ class_id = int(prediction)
261
+ class_name = label_encoder.inverse_transform([class_id])[0]
262
+
263
+ # Get kernel info from metadata
264
+ kernel_used = metadata.get('best_kernel', 'unknown') if metadata else 'unknown'
265
+
266
+ # Compute feature vector sizes for UI display
267
+ image = Image.open(io.BytesIO(image_data))
268
+ img_array = np.array(image)
269
+ if len(img_array.shape) == 2: # Grayscale
270
+ img_array = cv2.cvtColor(img_array, cv2.COLOR_GRAY2RGB)
271
+ elif img_array.shape[2] == 4: # RGBA
272
+ img_array = cv2.cvtColor(img_array, cv2.COLOR_RGBA2RGB)
273
+ img_resized = cv2.resize(img_array, (128, 128))
274
+
275
+ hog_size = int(extract_hog_features(img_resized).shape[0])
276
+ color_size = int(extract_color_histogram(img_resized).shape[0])
277
+ lbp_size = int(extract_lbp_features(img_resized).shape[0])
278
+
279
+ # Prepare response
280
+ response = {
281
+ "prediction": {
282
+ "class_id": class_id,
283
+ "class_name": class_name,
284
+ "kernel": kernel_used,
285
+ "confidence": float(np.max(probabilities)) if probabilities is not None else None
286
+ },
287
+ "probabilities": probabilities.tolist() if probabilities is not None else None,
288
+ "features": {
289
+ "hog_size": hog_size,
290
+ "color_size": color_size,
291
+ "lbp_size": lbp_size
292
+ }
293
+ }
294
+
295
+ return response
296
+
297
+ except Exception as e:
298
+ raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
299
+
300
+ @app.post("/api/predict-batch")
301
+ async def predict_batch(files: list[UploadFile] = File(...), kernel: str = "rbf"):
302
+ """
303
+ Predict multiple images at once
304
+ """
305
+ if kernel not in models:
306
+ raise HTTPException(
307
+ status_code=400,
308
+ detail=f"Kernel '{kernel}' not available. Available: {list(models.keys())}"
309
+ )
310
+
311
+ results = []
312
+
313
+ for file in files:
314
+ try:
315
+ image_data = await file.read()
316
+ features = extract_features_from_image(image_data)
317
+ features_scaled = scaler.transform(features)
318
+
319
+ model = models[kernel]
320
+ prediction = model.predict(features_scaled)[0]
321
+ class_id = int(prediction)
322
+ class_name = label_encoder.inverse_transform([class_id])[0]
323
+
324
+ results.append({
325
+ "filename": file.filename,
326
+ "prediction": {
327
+ "class_id": class_id,
328
+ "class_name": class_name,
329
+ "kernel": kernel
330
+ }
331
+ })
332
+
333
+ except Exception as e:
334
+ results.append({
335
+ "filename": file.filename,
336
+ "error": str(e)
337
+ })
338
+
339
+ return {"results": results}
340
+
341
+ @app.get("/api/performance")
342
+ async def get_performance():
343
+ """Get model performance metrics"""
344
+ if not metadata:
345
+ raise HTTPException(status_code=503, detail="Models not loaded")
346
+
347
+ return {"results": metadata.get("model_results", {})}
348
+
349
+ if __name__ == "__main__":
350
+ import uvicorn
351
+ uvicorn.run(app, host="0.0.0.0", port=7860)
dashboard/favicon.ico ADDED
dashboard/index.html ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SVM Cats vs Dogs Dashboard</title>
7
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8
+ <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
9
+ <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
13
+ <script src="https://cdn.tailwindcss.com"></script>
14
+ <script>
15
+ tailwind.config = {
16
+ theme: {
17
+ extend: {
18
+ fontFamily: {
19
+ sans: ['Inter', 'ui-sans-serif', 'system-ui']
20
+ }
21
+ }
22
+ }
23
+ }
24
+ </script>
25
+ <style>
26
+ body { min-height: 100vh; }
27
+ .loading-spinner {
28
+ border: 3px solid rgba(0, 0, 0, 0.08);
29
+ border-top: 3px solid #2563eb;
30
+ border-radius: 50%;
31
+ width: 40px;
32
+ height: 40px;
33
+ animation: spin 1s linear infinite;
34
+ }
35
+ @keyframes spin {
36
+ 0% { transform: rotate(0deg); }
37
+ 100% { transform: rotate(360deg); }
38
+ }
39
+ .fade-in {
40
+ animation: fadeIn 0.6s ease-in;
41
+ }
42
+ @keyframes fadeIn {
43
+ from { opacity: 0; transform: translateY(20px); }
44
+ to { opacity: 1; transform: translateY(0); }
45
+ }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <div class="min-h-screen bg-gray-50 text-gray-900">
50
+ <header class="border-b bg-white/80 backdrop-blur">
51
+ <div class="max-w-6xl mx-auto px-4 py-6 flex items-center justify-between">
52
+ <div class="flex items-center gap-3">
53
+ <img src="/static/logo.png" alt="SVM Logo" class="h-10 w-10 rounded-xl">
54
+ <div>
55
+ <div class="text-lg font-semibold leading-tight">Cats vs Dogs Classification</div>
56
+ <div class="text-sm text-gray-500">Upload an image and get a prediction with confidence</div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </header>
61
+
62
+ <main class="max-w-4xl mx-auto px-4 py-10">
63
+ <section class="bg-white rounded-2xl shadow-sm border p-6 md:p-8 fade-in">
64
+ <div>
65
+ <h2 class="text-2xl font-semibold tracking-tight">Try a prediction</h2>
66
+ <p class="text-sm text-gray-500 mt-2">Drag and drop a JPG/PNG, or click to browse. Max 10MB.</p>
67
+ </div>
68
+
69
+ <div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
70
+ <div>
71
+ <label id="uploadArea" for="fileInput" class="rounded-2xl border-2 border-dashed border-gray-200 bg-gray-50 p-6 hover:bg-gray-100 transition cursor-pointer block">
72
+ <input type="file" id="fileInput" accept="image/*" class="hidden" />
73
+ <div class="flex items-center gap-4">
74
+ <div class="h-12 w-12 rounded-xl bg-white border flex items-center justify-center">
75
+ <svg class="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
76
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 12V4m0 8l-3-3m3 3l3-3" />
77
+ </svg>
78
+ </div>
79
+ <div class="flex-1">
80
+ <div class="font-semibold">Upload image</div>
81
+ <div id="fileHint" class="text-sm text-gray-500 mt-0.5">No file selected</div>
82
+ </div>
83
+ </div>
84
+ <div class="mt-4 text-xs text-gray-500">Tip: choose a clear photo with the pet centered.</div>
85
+ </label>
86
+
87
+ <div class="mt-5">
88
+ <div id="status" class="hidden text-sm"></div>
89
+ </div>
90
+
91
+ <div id="loading" class="hidden mt-5 rounded-xl border bg-gray-50 p-4">
92
+ <div class="flex items-center gap-3">
93
+ <div class="loading-spinner"></div>
94
+ <div>
95
+ <div class="font-medium">Analyzing image…</div>
96
+ <div class="text-sm text-gray-500">Extracting features and predicting class</div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <div class="rounded-2xl border bg-white overflow-hidden">
103
+ <div class="p-4 border-b bg-gray-50">
104
+ <div class="text-sm font-semibold">Preview</div>
105
+ <div id="previewMeta" class="text-xs text-gray-500">Upload an image to see preview</div>
106
+ </div>
107
+ <div class="p-4">
108
+ <div class="aspect-square rounded-xl bg-gray-100 overflow-hidden flex items-center justify-center">
109
+ <img id="previewImage" alt="Preview" class="hidden h-full w-full object-cover" />
110
+ <div id="previewPlaceholder" class="text-sm text-gray-500">No image</div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <div id="results" class="hidden mt-6 rounded-2xl border bg-white p-6">
117
+ <div class="flex items-start justify-between gap-4">
118
+ <div>
119
+ <div class="text-sm text-gray-500">Prediction</div>
120
+ <div id="prediction" class="text-3xl font-bold">-</div>
121
+ <div id="confidence" class="text-sm text-gray-600 mt-1">Confidence: -</div>
122
+ </div>
123
+ <div class="text-right">
124
+ <div class="text-sm text-gray-500">Kernel</div>
125
+ <div id="kernelUsed" class="text-sm font-semibold">-</div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="mt-5 grid grid-cols-1 md:grid-cols-3 gap-4">
130
+ <div class="rounded-xl border bg-gray-50 p-4">
131
+ <div class="text-xs text-gray-500">HOG features</div>
132
+ <div id="hogFeatures" class="text-lg font-semibold">-</div>
133
+ </div>
134
+ <div class="rounded-xl border bg-gray-50 p-4">
135
+ <div class="text-xs text-gray-500">Color histogram</div>
136
+ <div id="colorFeatures" class="text-lg font-semibold">-</div>
137
+ </div>
138
+ <div class="rounded-xl border bg-gray-50 p-4">
139
+ <div class="text-xs text-gray-500">LBP features</div>
140
+ <div id="lbpFeatures" class="text-lg font-semibold">-</div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </section>
145
+ </main>
146
+ </div>
147
+
148
+ <script>
149
+ let selectedFile = null;
150
+ let isPredicting = false;
151
+
152
+ // File upload handling
153
+ const uploadArea = document.getElementById('uploadArea');
154
+ const fileInput = document.getElementById('fileInput');
155
+ const loading = document.getElementById('loading');
156
+ const results = document.getElementById('results');
157
+ const status = document.getElementById('status');
158
+ const previewImage = document.getElementById('previewImage');
159
+ const previewPlaceholder = document.getElementById('previewPlaceholder');
160
+ const previewMeta = document.getElementById('previewMeta');
161
+ const fileHint = document.getElementById('fileHint');
162
+
163
+
164
+ function setStatus(message, kind) {
165
+ if (!message) {
166
+ status.textContent = '';
167
+ status.className = 'hidden text-sm';
168
+ return;
169
+ }
170
+
171
+ const base = 'text-sm';
172
+ if (kind === 'error') {
173
+ status.className = `${base} text-red-600`;
174
+ } else if (kind === 'success') {
175
+ status.className = `${base} text-green-600`;
176
+ } else {
177
+ status.className = `${base} text-gray-600`;
178
+ }
179
+ status.textContent = message;
180
+ }
181
+
182
+ uploadArea.addEventListener('dragover', (e) => {
183
+ e.preventDefault();
184
+ uploadArea.classList.add('ring-2', 'ring-blue-500');
185
+ });
186
+
187
+ uploadArea.addEventListener('dragleave', () => {
188
+ uploadArea.classList.remove('ring-2', 'ring-blue-500');
189
+ });
190
+
191
+ uploadArea.addEventListener('drop', (e) => {
192
+ e.preventDefault();
193
+ uploadArea.classList.remove('ring-2', 'ring-blue-500');
194
+ const files = e.dataTransfer.files;
195
+ if (files.length > 0) {
196
+ handleFileSelect(files[0]);
197
+ }
198
+ });
199
+
200
+ fileInput.addEventListener('change', (e) => {
201
+ if (e.target.files.length > 0) {
202
+ handleFileSelect(e.target.files[0]);
203
+ }
204
+ });
205
+
206
+ function handleFileSelect(file) {
207
+ if (file.size > 10 * 1024 * 1024) {
208
+ alert('File too large. Please select an image under 10MB.');
209
+ return;
210
+ }
211
+
212
+ if (!file.type.startsWith('image/')) {
213
+ alert('Please select an image file.');
214
+ return;
215
+ }
216
+
217
+ selectedFile = file;
218
+ setStatus('', '');
219
+ fileHint.textContent = `${file.name} • ${(file.size / 1024).toFixed(0)} KB`;
220
+
221
+ const reader = new FileReader();
222
+ reader.onload = () => {
223
+ previewImage.src = reader.result;
224
+ previewImage.classList.remove('hidden');
225
+ previewPlaceholder.classList.add('hidden');
226
+ previewMeta.textContent = `${file.type || 'image'} • ${(file.size / 1024).toFixed(0)} KB`;
227
+ };
228
+ reader.readAsDataURL(file);
229
+
230
+ runPrediction();
231
+ }
232
+
233
+ async function runPrediction() {
234
+ if (!selectedFile) return;
235
+ if (isPredicting) return;
236
+
237
+ isPredicting = true;
238
+ setStatus('Predicting…', 'info');
239
+
240
+ loading.classList.remove('hidden');
241
+ results.classList.add('hidden');
242
+
243
+ try {
244
+ const formData = new FormData();
245
+ formData.append('file', selectedFile);
246
+
247
+ const response = await fetch('/api/predict', {
248
+ method: 'POST',
249
+ body: formData
250
+ });
251
+
252
+ if (!response.ok) {
253
+ throw new Error(`HTTP error! status: ${response.status}`);
254
+ }
255
+
256
+ const result = await response.json();
257
+ displayResults(result);
258
+ setStatus('', '');
259
+ } catch (error) {
260
+ console.error('Prediction failed:', error);
261
+ setStatus('Prediction failed. Try another image.', 'error');
262
+ } finally {
263
+ loading.classList.add('hidden');
264
+ isPredicting = false;
265
+ }
266
+ }
267
+
268
+ function displayResults(result) {
269
+ const prediction = result.prediction;
270
+ const conf = typeof prediction.confidence === 'number' ? (prediction.confidence * 100) : null;
271
+
272
+ document.getElementById('prediction').textContent = prediction.class_name ?? '-';
273
+ document.getElementById('kernelUsed').textContent = prediction.kernel ?? 'Best Model';
274
+ document.getElementById('confidence').textContent = conf === null ? 'Confidence: n/a' : `Confidence: ${conf.toFixed(1)}%`;
275
+
276
+ const features = result.features || {};
277
+ document.getElementById('hogFeatures').textContent = (features.hog_size ?? '-').toString();
278
+ document.getElementById('colorFeatures').textContent = (features.color_size ?? '-').toString();
279
+ document.getElementById('lbpFeatures').textContent = (features.lbp_size ?? '-').toString();
280
+
281
+ results.classList.remove('hidden');
282
+ setStatus('', '');
283
+ }
284
+
285
+
286
+ </script>
287
+ </body>
288
+ </html>
dashboard/logo.png ADDED
requirements.txt ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core ML and Data Science Libraries
2
+ numpy>=1.21.0
3
+ pandas>=1.3.0
4
+ scikit-learn>=1.0.0
5
+ matplotlib>=3.5.0
6
+ seaborn>=0.11.0
7
+ joblib>=1.1.0
8
+ scikit-image>=0.18.0
9
+
10
+ # Web Framework and API
11
+ fastapi>=0.68.0
12
+ uvicorn>=0.15.0
13
+ python-multipart>=0.0.5
14
+
15
+ # Image Processing
16
+ opencv-python>=4.5.0
17
+ Pillow>=8.3.0
18
+
19
+ # Dataset Download (optional)
20
+ kagglehub>=0.2.0
21
+
22
+ # Data Visualization (for dashboard)
23
+ plotly>=5.0.0
24
+
25
+ # Development and Testing
26
+ pytest>=6.2.0
27
+ requests>=2.25.0
28
+
29
+ # Optional: For enhanced performance
30
+ # numpy>=1.21.0
31
+ # scipy>=1.7.0