debjani31 commited on
Commit
1de1413
·
0 Parent(s):

Fresh start with LFS

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.h5 filter=lfs diff=lfs merge=lfs -text
2
+ *.csv filter=lfs diff=lfs merge=lfs -text
3
+ dataset/** filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
Binary file (356 Bytes). View file
 
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ libsm6 \
8
+ libxext6 \
9
+ libxrender-dev \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements and install Python dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy application files
17
+ COPY app.py .
18
+ COPY mnist_cnn_model.h5 .
19
+ COPY website/ ./website/
20
+
21
+ # Expose port
22
+ EXPOSE 5000
23
+
24
+ # Run the Flask app
25
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # DigitVision
2
+ Handwritten Digit Recognition with CNN
app.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ from flask_cors import CORS
3
+ import numpy as np
4
+ import cv2
5
+ import base64
6
+ import io
7
+ from PIL import Image
8
+ import os
9
+
10
+ # Suppress TensorFlow warnings
11
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
12
+ os.environ['TF_USE_LEGACY_KERAS'] = '1' # Force TensorFlow to use legacy Keras
13
+ import warnings
14
+ warnings.filterwarnings('ignore')
15
+
16
+ import tensorflow as tf
17
+ # Use TensorFlow's built-in Keras instead of standalone Keras
18
+ from tensorflow import keras
19
+ load_model = keras.models.load_model
20
+
21
+ app = Flask(__name__, template_folder='website', static_folder='website')
22
+ CORS(app)
23
+
24
+ # Rebuild model architecture from scratch and load weights
25
+ def load_legacy_model(model_path):
26
+ """Rebuild model architecture and load weights from H5 file"""
27
+ try:
28
+ # Rebuild the exact model architecture
29
+ model = keras.Sequential([
30
+ keras.layers.Input(shape=(28, 28, 1)),
31
+ keras.layers.Conv2D(32, kernel_size=(3, 3), activation='relu'),
32
+ keras.layers.Conv2D(32, kernel_size=(3, 3), activation='relu'),
33
+ keras.layers.MaxPooling2D(pool_size=(2, 2)),
34
+ keras.layers.Dropout(0.25),
35
+ keras.layers.Conv2D(64, kernel_size=(3, 3), activation='relu'),
36
+ keras.layers.Conv2D(64, kernel_size=(3, 3), activation='relu'),
37
+ keras.layers.MaxPooling2D(pool_size=(1, 1)),
38
+ keras.layers.Dropout(0.25),
39
+ keras.layers.Flatten(),
40
+ keras.layers.Dense(256, activation='relu'),
41
+ keras.layers.Dropout(0.5),
42
+ keras.layers.Dense(10, activation='softmax')
43
+ ])
44
+
45
+ # Compile model
46
+ model.compile(
47
+ optimizer='adam',
48
+ loss='categorical_crossentropy',
49
+ metrics=['accuracy']
50
+ )
51
+
52
+ # Load weights from H5 file
53
+ model.load_weights(model_path)
54
+ return model
55
+ except Exception as e:
56
+ print(f"Failed to rebuild and load model: {e}")
57
+ return None
58
+
59
+ # Load the trained model
60
+ base_dir = os.path.dirname(__file__)
61
+ candidate_names = [
62
+ 'mnist_cnn_model.h5', # prefer .h5 if the user re-saved the model
63
+ 'mnist_cnn_model.keras' # fallback to .keras
64
+ ]
65
+
66
+ # Pick the first existing model path
67
+ MODEL_PATH = None
68
+ for name in candidate_names:
69
+ path = os.path.join(base_dir, name)
70
+ if os.path.isfile(path):
71
+ MODEL_PATH = path
72
+ break
73
+ if MODEL_PATH is None:
74
+ # Default to .h5 path even if missing, to keep logs consistent
75
+ MODEL_PATH = os.path.join(base_dir, candidate_names[0])
76
+
77
+ print(f"Looking for model. Resolved path: {MODEL_PATH}")
78
+ print(f"Model file exists: {os.path.isfile(MODEL_PATH)}")
79
+
80
+ try:
81
+ model = load_legacy_model(MODEL_PATH)
82
+ if model:
83
+ print(f"✓ Model loaded successfully from {MODEL_PATH}")
84
+ else:
85
+ print("✗ Failed to load model with compatibility loader")
86
+ model = None
87
+ except Exception as e:
88
+ print(f"✗ Error loading model from {MODEL_PATH}")
89
+ print(f"✗ Error details: {type(e).__name__}: {e}")
90
+ import traceback
91
+ traceback.print_exc()
92
+ model = None
93
+
94
+ def preprocess_image(image):
95
+ """
96
+ Preprocess image for MNIST model prediction
97
+ - Handles both uploaded images and canvas drawings
98
+ - Converts to grayscale
99
+ - Resizes to 28x28
100
+ - Normalizes to 0-1
101
+ - Ensures white digit on black background (MNIST format)
102
+ """
103
+ try:
104
+ img = image.copy()
105
+ print(f"DEBUG PREPROCESS: Input shape={img.shape}, dtype={img.dtype}")
106
+
107
+ # Handle RGBA (canvas drawings come as RGBA with transparent background)
108
+ if len(img.shape) == 3 and img.shape[2] == 4:
109
+ # Extract alpha channel to find drawn areas
110
+ alpha = img[:, :, 3]
111
+ # Convert RGB channels to grayscale
112
+ rgb = img[:, :, :3]
113
+ gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
114
+
115
+ # Create mask: where alpha > 0 (drawn) AND pixel is dark (black drawing)
116
+ # Canvas has BLACK digit on TRANSPARENT background
117
+ drawn_mask = alpha > 0
118
+ dark_mask = gray < 128
119
+ digit_mask = drawn_mask & dark_mask
120
+
121
+ # Create output: white (255) where digit is drawn, black (0) elsewhere
122
+ img = np.zeros_like(gray)
123
+ img[digit_mask] = 255
124
+
125
+ print(f"DEBUG PREPROCESS: Drawn pixels found: {np.sum(digit_mask)}")
126
+ else:
127
+ # If RGB -> grayscale
128
+ if len(img.shape) == 3:
129
+ img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
130
+
131
+ # For uploaded images: determine if we need to invert
132
+ # Check if the digit is darker or lighter than background
133
+ # Apply Otsu's thresholding to separate foreground/background
134
+ _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
135
+
136
+ # Count white vs black pixels
137
+ white_pixels = np.sum(binary == 255)
138
+ black_pixels = np.sum(binary == 0)
139
+
140
+ # If more white pixels, the background is white (digit is black) - no inversion needed
141
+ # If more black pixels, the background is black (digit is white) - already good
142
+ # But we want WHITE digit on BLACK background for MNIST
143
+ if white_pixels > black_pixels:
144
+ # Background is white, digit is black - need to invert
145
+ img = 255 - img
146
+
147
+ print(f"DEBUG PREPROCESS: white_pixels={white_pixels}, black_pixels={black_pixels}, inverted={white_pixels > black_pixels}")
148
+
149
+ print(f"DEBUG PREPROCESS: After grayscale - min={img.min()}, max={img.max()}, mean={img.mean()}")
150
+
151
+ # Apply threshold to get clean binary image with automatic threshold
152
+ _, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
153
+
154
+ print(f"DEBUG PREPROCESS: After threshold - min={thresh.min()}, max={thresh.max()}, mean={thresh.mean()}")
155
+
156
+ # Apply morphological closing to remove small noise
157
+ kernel = np.ones((3,3), np.uint8)
158
+ thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
159
+
160
+ # Find the bounding box of the digit (white pixels)
161
+ coords = cv2.findNonZero(thresh)
162
+ if coords is not None and len(coords) > 10:
163
+ x, y, w, h = cv2.boundingRect(coords)
164
+ digit = thresh[y:y+h, x:x+w]
165
+ print(f"DEBUG PREPROCESS: Found digit at ({x},{y}) size {w}x{h}")
166
+
167
+ # If the bounding box is too large (>80% of image), the digit is likely small and lost
168
+ # Try to find a tighter bounding box by using a higher threshold
169
+ img_h, img_w = thresh.shape
170
+ if w > img_w * 0.8 or h > img_h * 0.8:
171
+ print(f"DEBUG PREPROCESS: Bounding box too large ({w}x{h} vs {img_w}x{img_h}), trying adaptive threshold")
172
+ # Use adaptive threshold to better separate small digit from background
173
+ adaptive_thresh = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
174
+ coords2 = cv2.findNonZero(adaptive_thresh)
175
+ if coords2 is not None and len(coords2) > 10:
176
+ x, y, w, h = cv2.boundingRect(coords2)
177
+ digit = adaptive_thresh[y:y+h, x:x+w]
178
+ print(f"DEBUG PREPROCESS: Found tighter bounding box at ({x},{y}) size {w}x{h}")
179
+
180
+ # Apply morphological operations to thicken thin strokes
181
+ kernel = np.ones((2,2), np.uint8)
182
+ digit = cv2.dilate(digit, kernel, iterations=1)
183
+
184
+ # Enhance contrast - stretch to full range [0, 255]
185
+ digit_min = digit.min()
186
+ digit_max = digit.max()
187
+ if digit_max > digit_min:
188
+ digit = ((digit - digit_min) / (digit_max - digit_min) * 255).astype(np.uint8)
189
+ print(f"DEBUG PREPROCESS: Enhanced contrast - old range [{digit_min},{digit_max}], new range [0,255]")
190
+ else:
191
+ print(f"DEBUG PREPROCESS: WARNING - No contrast to enhance (all pixels same value: {digit_min})")
192
+ else:
193
+ print("DEBUG PREPROCESS: No digit found in image")
194
+ # Return empty canvas if no digit found
195
+ canvas = np.zeros((28, 28, 1), dtype=np.float32)
196
+ return canvas
197
+
198
+ # Resize the cropped digit to fit in 20x20 box (with aspect ratio preserved)
199
+ h_d, w_d = digit.shape
200
+ if h_d > w_d:
201
+ new_h = 20
202
+ new_w = max(1, int(round((w_d * 20) / h_d)))
203
+ else:
204
+ new_w = 20
205
+ new_h = max(1, int(round((h_d * 20) / w_d)))
206
+
207
+ digit_resized = cv2.resize(digit, (new_w, new_h), interpolation=cv2.INTER_AREA)
208
+
209
+ # Center the digit in a 28x28 black canvas
210
+ canvas = np.zeros((28, 28), dtype=np.uint8)
211
+ x_offset = (28 - new_w) // 2
212
+ y_offset = (28 - new_h) // 2
213
+ canvas[y_offset:y_offset+new_h, x_offset:x_offset+new_w] = digit_resized
214
+
215
+ # Normalize to [0, 1]
216
+ canvas = canvas.astype("float32") / 255.0
217
+
218
+ # Reshape to (28, 28, 1)
219
+ canvas = canvas.reshape(28, 28, 1)
220
+
221
+ # Save debug image (for visual check)
222
+ cv2.imwrite("debug_preprocessed.png", (canvas * 255).astype(np.uint8))
223
+ print(f"DEBUG PREPROCESS: Final - min={float(canvas.min()):.3f}, max={float(canvas.max()):.3f}, mean={float(canvas.mean()):.3f}")
224
+
225
+ return canvas
226
+
227
+ except Exception as e:
228
+ print(f"Error in preprocess_image: {e}")
229
+ import traceback
230
+ traceback.print_exc()
231
+ canvas = np.zeros((28, 28, 1), dtype=np.float32)
232
+ return canvas
233
+ @app.route('/')
234
+ def index():
235
+ """Serve the layout page"""
236
+ return render_template('layout.html')
237
+
238
+ @app.route('/layout.html')
239
+ def layout():
240
+ """Serve the layout page"""
241
+ return render_template('layout.html')
242
+
243
+ @app.route('/sign.html')
244
+ def sign():
245
+ """Serve the sign in/up page"""
246
+ return render_template('sign.html')
247
+
248
+ @app.route('/home.html')
249
+ def home():
250
+ """Serve the home page"""
251
+ return render_template('home.html')
252
+
253
+ @app.route('/predict_upload', methods=['POST'])
254
+ def predict_upload():
255
+ """
256
+ Handle image upload prediction
257
+ Expects: multipart/form-data with 'image' file
258
+ Returns: JSON with prediction
259
+ """
260
+ if model is None:
261
+ return jsonify({'error': 'Model not loaded'}), 500
262
+
263
+ try:
264
+ # Get the uploaded file
265
+ if 'image' not in request.files:
266
+ return jsonify({'error': 'No image provided'}), 400
267
+
268
+ file = request.files['image']
269
+ if file.filename == '':
270
+ return jsonify({'error': 'No file selected'}), 400
271
+
272
+ # Read image
273
+ image_data = file.read()
274
+ image = Image.open(io.BytesIO(image_data))
275
+ image_array = np.array(image)
276
+
277
+ # Preprocess
278
+ processed_image = preprocess_image(image_array)
279
+
280
+ # Reshape for model (add batch dimension)
281
+ input_data = processed_image.reshape(1, 28, 28, 1)
282
+
283
+ # Predict
284
+ prediction = model.predict(input_data, verbose=0)
285
+ print("DEBUG INPUT SHAPE:", input_data.shape)
286
+ print("DEBUG INPUT STATS: min=", float(input_data.min()), "max=", float(input_data.max()), "mean=", float(input_data.mean()))
287
+ print("DEBUG MODEL OUTPUT:", prediction)
288
+
289
+ predicted_digit = np.argmax(prediction[0])
290
+ confidence = float(np.max(prediction[0])) * 100
291
+
292
+ return jsonify({
293
+ 'prediction': int(predicted_digit),
294
+ 'confidence': round(confidence, 2)
295
+ })
296
+
297
+ except Exception as e:
298
+ return jsonify({'error': str(e)}), 500
299
+
300
+ @app.route('/predict_canvas', methods=['POST'])
301
+ def predict_canvas():
302
+ """
303
+ Handle canvas drawing prediction
304
+ Expects: JSON with 'image' as base64 data URL
305
+ Returns: JSON with prediction
306
+ """
307
+ if model is None:
308
+ return jsonify({'error': 'Model not loaded'}), 500
309
+
310
+ try:
311
+ data = request.get_json()
312
+ if 'image' not in data:
313
+ return jsonify({'error': 'No image data provided'}), 400
314
+
315
+ # Decode base64 image
316
+ image_data = data['image']
317
+ if ',' in image_data:
318
+ image_data = image_data.split(',')[1]
319
+
320
+ # Decode and convert to numpy array
321
+ image_bytes = base64.b64decode(image_data)
322
+ image = Image.open(io.BytesIO(image_bytes))
323
+ image_array = np.array(image)
324
+
325
+ print(f"DEBUG CANVAS: Original shape={image_array.shape}, dtype={image_array.dtype}")
326
+ print(f"DEBUG CANVAS: Image stats - min={image_array.min()}, max={image_array.max()}, mean={image_array.mean()}")
327
+
328
+ # DON'T convert RGBA to RGB - let preprocess_image handle it
329
+ # The preprocessing function needs the alpha channel to detect drawn areas
330
+
331
+ # Preprocess
332
+ processed_image = preprocess_image(image_array)
333
+
334
+ print(f"DEBUG CANVAS: After preprocess - shape={processed_image.shape}")
335
+ print(f"DEBUG CANVAS: After preprocess - min={processed_image.min()}, max={processed_image.max()}, mean={processed_image.mean()}")
336
+
337
+ # Reshape for model (add batch dimension)
338
+ input_data = processed_image.reshape(1, 28, 28, 1)
339
+
340
+ # Predict
341
+ prediction = model.predict(input_data, verbose=0)
342
+ print(f"DEBUG CANVAS: Model prediction={prediction}")
343
+
344
+ predicted_digit = np.argmax(prediction[0])
345
+ confidence = float(np.max(prediction[0])) * 100
346
+
347
+ print(f"DEBUG CANVAS: Final prediction={predicted_digit}, confidence={confidence}")
348
+
349
+ return jsonify({
350
+ 'prediction': int(predicted_digit),
351
+ 'confidence': round(confidence, 2)
352
+ })
353
+
354
+ except Exception as e:
355
+ print(f"ERROR in predict_canvas: {e}")
356
+ import traceback
357
+ traceback.print_exc()
358
+ return jsonify({'error': str(e)}), 500
359
+
360
+ @app.route('/<path:filename>')
361
+ def serve_files(filename):
362
+ """Serve static files (images, css, js, etc.) from website folder"""
363
+ import os
364
+ file_path = os.path.join('website', filename)
365
+ if os.path.isfile(file_path):
366
+ from flask import send_file
367
+ return send_file(file_path)
368
+ return "File not found", 404
369
+
370
+ if __name__ == '__main__':
371
+ if model is None:
372
+ print("⚠ Warning: Model could not be loaded. Predictions will fail.")
373
+ print("🚀 Starting DigitVision server on http://localhost:5000")
374
+ app.run(debug=True, port=5000)
dataset/submission.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75982bff434710f9869edf837d8b3238ba946d251122649d225f639776bb174f
3
+ size 212908
dataset/test.csv/test.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3353d136e718a01b18f4e2097d2f0bbd2676e5887b75b9497e35d34e90820013
3
+ size 51118296
dataset/train.csv/test.csv/test.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3353d136e718a01b18f4e2097d2f0bbd2676e5887b75b9497e35d34e90820013
3
+ size 51118296
dataset/train.csv/train.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b35cc952c291f03d995f48721373e69454f689e299d724bd9a5927bdabddfeed
3
+ size 76775041
mnist_cnn_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:430785ed0dbc985f221a0aecb18057791da29f8e0c4220bc284ac72cc09da431
3
+ size 13455472
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Flask==2.3.3
2
+ Flask-CORS==4.0.0
3
+ tensorflow==2.13.0
4
+ keras==2.13.1
5
+ numpy==1.24.3
6
+ opencv-python==4.8.0.76
7
+ Pillow==10.1.0
8
+ Werkzeug==2.3.7
website/home.html ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>DigitVision - Handwritten Digit Recognition</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;500&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --neon: #00ffff;
12
+ --bg-dark: #0f0c29;
13
+ --bg-gradient: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
14
+ }
15
+
16
+ body {
17
+ font-family: 'Roboto', sans-serif;
18
+ background: var(--bg-gradient);
19
+ color: #fff;
20
+ min-height: 100vh;
21
+ margin: 0;
22
+ overflow-x: hidden;
23
+ position: relative;
24
+ }
25
+
26
+ /* Back Arrow */
27
+ .back-arrow {
28
+ position: absolute;
29
+ top: 20px;
30
+ left: 20px;
31
+ font-size: 2.2rem;
32
+ color: var(--neon);
33
+ text-shadow: 0 0 12px var(--neon);
34
+ cursor: pointer;
35
+ transition: all 0.3s;
36
+ z-index: 1000;
37
+ font-weight: bold;
38
+ }
39
+ .back-arrow:hover {
40
+ color: #fff;
41
+ transform: scale(1.15);
42
+ }
43
+
44
+ /* ---------- PARTICLES ---------- */
45
+ #particles {
46
+ position: fixed;
47
+ top: 0; left: 0; width: 100%; height: 100%;
48
+ z-index: -1;
49
+ opacity: 0.3;
50
+ }
51
+
52
+ /* ---------- NAVBAR ---------- */
53
+ .navbar {
54
+ background: rgba(15, 12, 41, 0.8);
55
+ backdrop-filter: blur(12px);
56
+ border-bottom: 1px solid rgba(0, 255, 255, 0.2);
57
+ box-shadow: 0 4px 15px rgba(0, 255, 255, 0.1);
58
+ padding: 0.8rem 1rem;
59
+ }
60
+
61
+ .navbar-brand img {
62
+ height: 50px;
63
+ transition: transform .3s ease;
64
+ border-radius: 15px;
65
+ object-fit: cover;
66
+ }
67
+ .navbar-brand img:hover { transform: scale(1.1); }
68
+
69
+ .navbar-nav .nav-link {
70
+ color: #fff !important;
71
+ font-weight: 500;
72
+ margin: 0 10px;
73
+ position: relative;
74
+ transition: all .3s;
75
+ }
76
+ .navbar-nav .nav-link::after {
77
+ content: '';
78
+ position: absolute;
79
+ width: 0;
80
+ height: 2px;
81
+ bottom: -5px;
82
+ left: 50%;
83
+ background: var(--neon);
84
+ transition: all .3s;
85
+ transform: translateX(-50%);
86
+ }
87
+ .navbar-nav .nav-link:hover::after,
88
+ .navbar-nav .nav-link.active::after { width: 80%; }
89
+ .navbar-nav .nav-link:hover,
90
+ .navbar-nav .nav-link.active { color: var(--neon) !important; text-shadow: 0 0 8px var(--neon); }
91
+
92
+ /* ---------- MAIN CONTENT ---------- */
93
+ .main-content {
94
+ padding-top: 7rem;
95
+ max-width: 1200px;
96
+ margin: 0 auto;
97
+ z-index: 1;
98
+ }
99
+
100
+ h1 {
101
+ font-family: 'Orbitron', sans-serif;
102
+ font-size: 3.5rem;
103
+ color: var(--neon);
104
+ text-align: center;
105
+ text-shadow: 0 0 20px var(--neon), 0 0 40px var(--neon);
106
+ animation: glow 2s infinite alternate;
107
+ margin-bottom: 1.5rem;
108
+ }
109
+ @keyframes glow {
110
+ from { text-shadow: 0 0 15px var(--neon), 0 0 30px var(--neon); }
111
+ to { text-shadow: 0 0 25px var(--neon), 0 0 50px var(--neon); }
112
+ }
113
+
114
+ .lead { text-align:center; font-size:1.2rem; opacity:.9; margin-bottom:2.5rem; }
115
+
116
+ /* ---------- TABS ---------- */
117
+ .nav-tabs { border:none; justify-content:center; margin-bottom:2rem; }
118
+ .nav-tabs .nav-link {
119
+ background:transparent; border:none; color:#ccc; font-weight:500;
120
+ padding:.8rem 1.5rem; border-radius:50px; margin:0 10px; transition:all .3s;
121
+ }
122
+ .nav-tabs .nav-link.active {
123
+ background:rgba(0,255,255,.15); color:var(--neon)!important;
124
+ border:2px solid var(--neon); box-shadow:0 0 15px rgba(0,255,255,.3);
125
+ }
126
+
127
+ .tab-pane {
128
+ background:rgba(255,255,255,.08); border-radius:20px; padding:2.5rem;
129
+ backdrop-filter:blur(12px); border:1px solid rgba(0,255,255,.2);
130
+ box-shadow:0 8px 32px rgba(0,255,255,.1);
131
+ }
132
+
133
+ /* ---------- CANVAS (PENCIL CURSOR) ---------- */
134
+ #canvas {
135
+ border:3px solid var(--neon);
136
+ border-radius:15px;
137
+ background:#fff;
138
+ cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>') 2 22, crosshair;
139
+ touch-action:none;
140
+ box-shadow:0 0 20px rgba(0,255,255,.3);
141
+ }
142
+ .eraser-mode #canvas {
143
+ cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/><path d="M9 15l6-6"/></svg>') 12 12, crosshair;
144
+ }
145
+
146
+ #preview {
147
+ max-width:100%; border:3px solid var(--neon); border-radius:15px;
148
+ display:none; margin-top:1rem; box-shadow:0 0 20px rgba(0,255,255,.3);
149
+ }
150
+
151
+ /* ---------- BUTTONS ---------- */
152
+ .btn-neon {
153
+ background:transparent; border:2px solid var(--neon); color:var(--neon);
154
+ font-weight:bold; padding:.6rem 1.4rem; border-radius:50px;
155
+ transition:all .3s; box-shadow:0 0 10px rgba(0,255,255,.4); margin:.3rem;
156
+ }
157
+ .btn-neon:hover, .btn-neon.active {
158
+ background:var(--neon); color:#000; box-shadow:0 0 25px var(--neon);
159
+ transform:translateY(-2px);
160
+ }
161
+
162
+ #result-upload, #result-draw {
163
+ font-size:2.2rem; color:#00ff00; text-shadow:0 0 15px #00ff00;
164
+ margin-top:1.5rem; font-weight:bold;
165
+ }
166
+
167
+ @media (max-width:768px){
168
+ .main-content { padding-top:6rem; }
169
+ h1 { font-size:2.5rem; }
170
+ .navbar-brand img { height:40px; }
171
+ .tab-pane { padding:1.5rem; }
172
+ #canvas { width:100%; height:auto; }
173
+ .btn-neon { padding:.5rem 1rem; font-size:.9rem; }
174
+ }
175
+ </style>
176
+ </head>
177
+ <body>
178
+
179
+ <!-- Back Arrow -->
180
+ <div class="back-arrow" onclick="window.location.href='layout.html'" title="Back to Layout">
181
+ &larr;
182
+ </div>
183
+
184
+ <!-- PARTICLES -->
185
+ <div id="particles"></div>
186
+
187
+ <!-- NAVBAR -->
188
+ <nav class="navbar navbar-expand-lg fixed-top">
189
+ <div class="container-fluid">
190
+ <a class="navbar-brand" href="#">
191
+ <img src="./logo.jpg" alt="DigitVision Logo" id="logo">
192
+ </a>
193
+
194
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
195
+ <span class="navbar-toggler-icon" style="filter:invert(1);"></span>
196
+ </button>
197
+
198
+ <div class="collapse navbar-collapse" id="navbarNav">
199
+ <ul class="navbar-nav ms-auto align-items-center">
200
+ <li class="nav-item"><a class="nav-link active" href="#upload">Upload</a></li>
201
+ <li class="nav-item"><a class="nav-link" href="#draw">Draw</a></li>
202
+ <li class="nav-item"><a class="nav-link" href="#">About</a></li>
203
+ <li class="nav-item"><a class="nav-link" href="#">Contact</a></li>
204
+ <li class="nav-item ms-3 d-flex align-items-center gap-2">
205
+ <span id="userName" style="color:#00ffff; font-weight:500;"></span>
206
+ <button id="logoutBtn" class="btn btn-neon btn-sm">Logout</button>
207
+ </li>
208
+ </ul>
209
+ </div>
210
+ </div>
211
+ </nav>
212
+
213
+ <!-- MAIN CONTENT -->
214
+ <div class="main-content">
215
+ <h1>DigitVision</h1>
216
+ <p class="lead">Upload a handwritten digit or draw it live — powered by deep learning.</p>
217
+
218
+ <!-- TABS -->
219
+ <ul class="nav nav-tabs" id="myTab" role="tablist">
220
+ <li class="nav-item" role="presentation">
221
+ <button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload">Upload Image</button>
222
+ </li>
223
+ <li class="nav-item" role="presentation">
224
+ <button class="nav-link" id="draw-tab" data-bs-toggle="tab" data-bs-target="#draw">Draw Digit</button>
225
+ </li>
226
+ </ul>
227
+
228
+ <div class="tab-content mt-4">
229
+ <!-- ==== UPLOAD ==== -->
230
+ <div class="tab-pane fade show active" id="upload" role="tabpanel">
231
+ <div class="text-center">
232
+ <p style="color: #00ffff; margin-bottom: 1rem; font-size: 0.95rem;">
233
+ ℹ️ Upload an image containing <strong>ONE handwritten digit (0-9)</strong>
234
+ </p>
235
+ <input type="file" id="fileInput" accept="image/*" class="form-control mb-4" style="max-width:500px;margin:0 auto;">
236
+ <img id="preview" alt="Image Preview">
237
+ <button id="predictUpload" class="btn btn-neon mt-3">Predict Digit</button>
238
+ <div id="result-upload"></div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- ==== DRAW ==== -->
243
+ <div class="tab-pane fade" id="draw" role="tabpanel">
244
+ <div class="text-center">
245
+ <p style="color: #00ffff; margin-bottom: 1rem; font-size: 0.95rem;">
246
+ ⚠️ <strong>Draw ONE digit only (0-9)</strong> - The model recognizes single digits.
247
+ </p>
248
+ <canvas id="canvas" width="280" height="280"></canvas>
249
+
250
+ <div class="mt-4 d-flex justify-content-center flex-wrap gap-2">
251
+ <button id="toolPencil" class="btn btn-neon active">Pencil</button>
252
+ <button id="toolEraser" class="btn btn-neon">Eraser</button>
253
+ <button id="clearCanvas" class="btn btn-neon">Clear All</button>
254
+ <button id="predictDraw" class="btn btn-neon">Predict Digit</button>
255
+ </div>
256
+
257
+ <div id="result-draw"></div>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </div>
262
+
263
+ <!-- SCRIPTS -->
264
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
265
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/particles.js/2.0.0/particles.min.js"></script>
266
+ <script>
267
+ // ==== PROTECT THIS PAGE (using URL params for file:// compatibility) ====
268
+ const urlParams = new URLSearchParams(window.location.search);
269
+ const loggedIn = urlParams.get('loggedIn');
270
+ const userStr = urlParams.get('user');
271
+
272
+ if (loggedIn !== 'true' || !userStr) {
273
+ window.location.href = 'sign.html';
274
+ }
275
+
276
+ // ==== SHOW USER NAME ====
277
+ const user = JSON.parse(userStr || '{}');
278
+ const userNameEl = document.getElementById('userName');
279
+ if (userNameEl) {
280
+ userNameEl.textContent = user.name || user.email.split('@')[0];
281
+ }
282
+
283
+ // ==== LOGOUT BUTTON ====
284
+ const logoutBtn = document.getElementById('logoutBtn');
285
+ if (logoutBtn) {
286
+ logoutBtn.addEventListener('click', () => {
287
+ window.location.href = 'sign.html';
288
+ });
289
+ }
290
+
291
+ /* ---------- PARTICLES ---------- */
292
+ particlesJS('particles', {
293
+ particles: {
294
+ number: { value: 90, density: { enable: true, value_area: 800 } },
295
+ color: { value: '#00ffff' },
296
+ shape: { type: 'circle' },
297
+ opacity: { value: 0.6, random: true },
298
+ size: { value: 3, random: true },
299
+ line_linked: { enable: true, distance: 140, color: '#00ffff', opacity: 0.3, width: 1 },
300
+ move: { enable: true, speed: 1.5, direction: 'none', random: false, straight: false, out_mode: 'out' }
301
+ },
302
+ interactivity: { detect_on: 'window', events: { onhover: { enable: true, mode: 'repulse' }, onclick: { enable: true, mode: 'push' }, resize: true } },
303
+ retina_detect: true
304
+ });
305
+
306
+ /* ---------- TAB ↔ NAVBAR SYNC ---------- */
307
+ document.querySelectorAll('#myTab button').forEach(t => {
308
+ t.addEventListener('shown.bs.tab', e => {
309
+ document.querySelectorAll('.navbar-nav .nav-link').forEach(l => {
310
+ l.classList.remove('active');
311
+ if (l.getAttribute('href') === '#' + e.target.getAttribute('data-bs-target')) l.classList.add('active');
312
+ });
313
+ });
314
+ });
315
+
316
+ /* ---------- UPLOAD ---------- */
317
+ const fileInput = document.getElementById('fileInput');
318
+ const preview = document.getElementById('preview');
319
+ const predictUpload = document.getElementById('predictUpload');
320
+ const resultUpload = document.getElementById('result-upload');
321
+
322
+ fileInput.addEventListener('change', e => {
323
+ const file = e.target.files[0];
324
+ if (file) {
325
+ const reader = new FileReader();
326
+ reader.onload = ev => { preview.src = ev.target.result; preview.style.display = 'block'; };
327
+ reader.readAsDataURL(file);
328
+ }
329
+ });
330
+
331
+ predictUpload.addEventListener('click', () => {
332
+ if (!fileInput.files[0]) return alert('Please upload an image.');
333
+ resultUpload.textContent = 'Processing...';
334
+ const fd = new FormData(); fd.append('image', fileInput.files[0]);
335
+ fetch('/predict_upload', { method: 'POST', body: fd })
336
+ .then(r => r.json())
337
+ .then(d => {
338
+ if (d.error) {
339
+ resultUpload.textContent = `Error: ${d.error}`;
340
+ } else {
341
+ resultUpload.textContent = `Predicted: ${d.prediction} (Confidence: ${d.confidence}%)`;
342
+ }
343
+ })
344
+ .catch(err => {
345
+ console.error(err);
346
+ resultUpload.textContent = 'Prediction failed. Check console for details.';
347
+ });
348
+ });
349
+
350
+ /* ---------- DRAW (PENCIL + ERASER) ---------- */
351
+ const canvas = document.getElementById('canvas');
352
+ const ctx = canvas.getContext('2d');
353
+ let drawing = false, isEraser = false;
354
+
355
+ const toolPencil = document.getElementById('toolPencil');
356
+ const toolEraser = document.getElementById('toolEraser');
357
+
358
+ function setTool(eraser) {
359
+ isEraser = eraser;
360
+ canvas.classList.toggle('eraser-mode', eraser);
361
+ toolPencil.classList.toggle('active', !eraser);
362
+ toolEraser.classList.toggle('active', eraser);
363
+ }
364
+ toolPencil.onclick = () => setTool(false);
365
+ toolEraser.onclick = () => setTool(true);
366
+
367
+ // Mouse
368
+ canvas.addEventListener('mousedown', e => { drawing = true; draw(e); });
369
+ canvas.addEventListener('mouseup', () => { drawing = false; ctx.beginPath(); });
370
+ canvas.addEventListener('mousemove', draw);
371
+
372
+ // Touch
373
+ canvas.addEventListener('touchstart', e => { e.preventDefault(); drawing = true; draw(e.touches[0]); });
374
+ canvas.addEventListener('touchend', () => { drawing = false; ctx.beginPath(); });
375
+ canvas.addEventListener('touchmove', e => { e.preventDefault(); if (drawing) draw(e.touches[0]); });
376
+
377
+ function draw(e) {
378
+ if (!drawing) return;
379
+ const rect = canvas.getBoundingClientRect();
380
+ const x = e.clientX - rect.left;
381
+ const y = e.clientY - rect.top;
382
+
383
+ ctx.lineWidth = 22;
384
+ ctx.lineCap = 'round';
385
+
386
+ if (isEraser) {
387
+ ctx.globalCompositeOperation = 'destination-out';
388
+ } else {
389
+ ctx.globalCompositeOperation = 'source-over';
390
+ ctx.strokeStyle = '#000';
391
+ }
392
+
393
+ ctx.lineTo(x, y);
394
+ ctx.stroke();
395
+ ctx.beginPath();
396
+ ctx.moveTo(x, y);
397
+ }
398
+
399
+ document.getElementById('clearCanvas').addEventListener('click', () => {
400
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
401
+ document.getElementById('result-draw').textContent = '';
402
+ setTool(false);
403
+ });
404
+
405
+ document.getElementById('predictDraw').addEventListener('click', () => {
406
+ document.getElementById('result-draw').textContent = 'Processing...';
407
+ const imageData = canvas.toDataURL('image/png');
408
+ fetch('/predict_canvas', {
409
+ method: 'POST',
410
+ headers: { 'Content-Type': 'application/json' },
411
+ body: JSON.stringify({ image: imageData })
412
+ })
413
+ .then(r => r.json())
414
+ .then(d => {
415
+ if (d.error) {
416
+ document.getElementById('result-draw').textContent = `Error: ${d.error}`;
417
+ } else {
418
+ document.getElementById('result-draw').textContent = `Predicted: ${d.prediction} (Confidence: ${d.confidence}%)`;
419
+ }
420
+ })
421
+ .catch(err => {
422
+ console.error(err);
423
+ document.getElementById('result-draw').textContent = 'Prediction failed. Check console for details.';
424
+ });
425
+ });
426
+ </script>
427
+ </body>
428
+ </html>
website/layout.html ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>DigitVision - AI Handwritten Digit Recognition</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;500&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --neon: #00ffff;
12
+ --bg-gradient: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
13
+ }
14
+ body {
15
+ font-family: 'Roboto', sans-serif;
16
+ background: var(--bg-gradient);
17
+ color: #fff;
18
+ min-height: 100vh;
19
+ margin: 0;
20
+ overflow-x: hidden;
21
+ position: relative;
22
+ }
23
+ #particles {
24
+ position: fixed;
25
+ top: 0; left: 0; width: 100%; height: 100%;
26
+ z-index: -1;
27
+ opacity: 0.3;
28
+ }
29
+
30
+ /* NAVBAR */
31
+ .navbar {
32
+ background: rgba(15, 12, 41, 0.8);
33
+ backdrop-filter: blur(12px);
34
+ border-bottom: 1px solid rgba(0, 255, 255, 0.2);
35
+ box-shadow: 0 4px 15px rgba(0, 255, 255, 0.1);
36
+ padding: 0.8rem 1rem;
37
+ }
38
+ .navbar-brand img {
39
+ height: 50px;
40
+ transition: transform .3s;
41
+ border-radius: 15px;
42
+ object-fit: cover;
43
+ }
44
+ .navbar-brand img:hover { transform: scale(1.1); }
45
+ .navbar-nav .nav-link {
46
+ color: #fff !important;
47
+ font-weight: 500;
48
+ margin: 0 10px;
49
+ position: relative;
50
+ transition: all .3s;
51
+ }
52
+ .navbar-nav .nav-link::after {
53
+ content: ''; position: absolute; width: 0; height: 2px; bottom: -5px; left: 50%;
54
+ background: var(--neon); transition: all .3s; transform: translateX(-50%);
55
+ }
56
+ .navbar-nav .nav-link:hover::after, .navbar-nav .nav-link.active::after { width: 80%; }
57
+ .navbar-nav .nav-link:hover, .navbar-nav .nav-link.active { color: var(--neon) !important; text-shadow: 0 0 8px var(--neon); }
58
+
59
+ /* HERO */
60
+ .hero {
61
+ padding: 8rem 1rem 4rem;
62
+ text-align: center;
63
+ max-width: 1000px;
64
+ margin: 0 auto;
65
+ }
66
+ .hero h1 {
67
+ font-family: 'Orbitron', sans-serif;
68
+ font-size: 4.5rem;
69
+ color: var(--neon);
70
+ text-shadow: 0 0 25px var(--neon), 0 0 50px var(--neon);
71
+ animation: glow 2s infinite alternate;
72
+ margin-bottom: 1.5rem;
73
+ }
74
+ @keyframes glow {
75
+ from { text-shadow: 0 0 20px var(--neon), 0 0 40px var(--neon); }
76
+ to { text-shadow: 0 0 30px var(--neon), 0 0 60px var(--neon); }
77
+ }
78
+ .hero p {
79
+ font-size: 1.3rem;
80
+ opacity: 0.9;
81
+ margin-bottom: 2.5rem;
82
+ max-width: 700px;
83
+ margin-left: auto;
84
+ margin-right: auto;
85
+ }
86
+ .btn-neon {
87
+ background: transparent;
88
+ border: 2px solid var(--neon);
89
+ color: var(--neon);
90
+ font-weight: bold;
91
+ padding: 0.8rem 2rem;
92
+ border-radius: 50px;
93
+ font-size: 1.1rem;
94
+ transition: all 0.3s;
95
+ box-shadow: 0 0 15px rgba(0, 255, 255, 0.4);
96
+ }
97
+ .btn-neon:hover {
98
+ background: var(--neon);
99
+ color: #000;
100
+ box-shadow: 0 0 30px var(--neon);
101
+ transform: translateY(-3px);
102
+ }
103
+
104
+ /* FEATURES */
105
+ .features {
106
+ padding: 4rem 1rem;
107
+ display: flex;
108
+ justify-content: center;
109
+ gap: 2rem;
110
+ flex-wrap: wrap;
111
+ }
112
+ .feature-card {
113
+ background: rgba(255, 255, 255, 0.08);
114
+ border-radius: 20px;
115
+ padding: 2rem;
116
+ width: 300px;
117
+ text-align: center;
118
+ backdrop-filter: blur(10px);
119
+ border: 1px solid rgba(0, 255, 255, 0.2);
120
+ box-shadow: 0 8px 25px rgba(0, 255, 255, 0.1);
121
+ transition: transform 0.3s;
122
+ }
123
+ .feature-card:hover {
124
+ transform: translateY(-10px);
125
+ }
126
+ .feature-card h3 {
127
+ color: var(--neon);
128
+ margin-bottom: 1rem;
129
+ font-family: 'Orbitron', sans-serif;
130
+ }
131
+
132
+ .footer {
133
+ text-align: center;
134
+ padding: 2rem;
135
+ color: #aaa;
136
+ font-size: 0.9rem;
137
+ }
138
+
139
+ @media (max-width: 768px) {
140
+ .hero h1 { font-size: 3rem; }
141
+ .hero { padding-top: 6rem; }
142
+ .feature-card { width: 100%; max-width: 350px; }
143
+ }
144
+ </style>
145
+ </head>
146
+ <body>
147
+
148
+ <div id="particles"></div>
149
+
150
+ <!-- NAVBAR -->
151
+ <nav class="navbar navbar-expand-lg fixed-top">
152
+ <div class="container-fluid">
153
+ <a class="navbar-brand" href="#">
154
+ <img src="./logo.jpg" alt="DigitVision Logo">
155
+ </a>
156
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
157
+ <span class="navbar-toggler-icon" style="filter:invert(1);"></span>
158
+ </button>
159
+ <div class="collapse navbar-collapse" id="navbarNav">
160
+ <ul class="navbar-nav ms-auto">
161
+ <li class="nav-item"><a class="nav-link active" href="#">Home</a></li>
162
+ <li class="nav-item"><a class="nav-link" href="#features">Features</a></li>
163
+ <li class="nav-item"><a class="nav-link" href="#">About</a></li>
164
+ <li class="nav-item"><a class="nav-link" href="./sign.html">Sign In</a></li>
165
+ </ul>
166
+ </div>
167
+ </div>
168
+ </nav>
169
+
170
+ <!-- HERO SECTION -->
171
+ <section class="hero">
172
+ <h1>DigitVision</h1>
173
+ <p>Upload a handwritten digit or draw it live — powered by deep learning. Accurate, fast, and futuristic.</p>
174
+ <a href="./sign.html" class="btn btn-neon">Get Started – Sign In</a>
175
+ </section>
176
+
177
+ <!-- FEATURES -->
178
+ <section class="features" id="features">
179
+ <div class="feature-card">
180
+ <h3>Upload & Predict</h3>
181
+ <p>Upload any image of a handwritten digit and get instant prediction.</p>
182
+ </div>
183
+ <div class="feature-card">
184
+ <h3>Draw Live</h3>
185
+ <p>Use pencil & eraser to draw digits on a canvas and predict in real-time.</p>
186
+ </div>
187
+ <!-- AI-Powered card REMOVED -->
188
+ </section>
189
+
190
+ <footer class="footer">
191
+ © 2025 DigitVision. All rights reserved.
192
+ </footer>
193
+
194
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/particles.js/2.0.0/particles.min.js"></script>
195
+ <script>
196
+ particlesJS('particles', {
197
+ particles: {
198
+ number: { value: 100, density: { enable: true, value_area: 800 } },
199
+ color: { value: '#00ffff' },
200
+ shape: { type: 'circle' },
201
+ opacity: { value: 0.6, random: true },
202
+ size: { value: 3, random: true },
203
+ line_linked: { enable: true, distance: 150, color: '#00ffff', opacity: 0.3, width: 1 },
204
+ move: { enable: true, speed: 1.5, direction: 'none', random: false, straight: false, out_mode: 'out' }
205
+ },
206
+ interactivity: { detect_on: 'window', events: { onhover: { enable: true, mode: 'repulse' }, onclick: { enable: true, mode: 'push' } } },
207
+ retina_detect: true
208
+ });
209
+ </script>
210
+ </body>
211
+ </html>
website/logo.jpg ADDED
website/sign.html ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Sign In | DigitVision</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@300;500&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --neon: #00ffff;
12
+ --bg-gradient: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
13
+ }
14
+ body {
15
+ font-family: 'Roboto', sans-serif;
16
+ background: var(--bg-gradient);
17
+ color: #fff;
18
+ min-height: 100vh;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ margin: 0;
23
+ overflow: hidden;
24
+ position: relative;
25
+ }
26
+ #particles {
27
+ position: fixed;
28
+ top: 0; left: 0; width: 100%; height: 100%;
29
+ z-index: -1;
30
+ opacity: 0.3;
31
+ }
32
+
33
+ /* Back Arrow */
34
+ .back-arrow {
35
+ position: absolute;
36
+ top: 20px;
37
+ left: 20px;
38
+ font-size: 2.2rem;
39
+ color: var(--neon);
40
+ text-shadow: 0 0 12px var(--neon);
41
+ cursor: pointer;
42
+ transition: all 0.3s;
43
+ z-index: 100;
44
+ font-weight: bold;
45
+ }
46
+ .back-arrow:hover {
47
+ color: #fff;
48
+ transform: scale(1.15);
49
+ }
50
+
51
+ .card {
52
+ background: rgba(255, 255, 255, 0.08);
53
+ border-radius: 20px;
54
+ padding: 2.5rem;
55
+ backdrop-filter: blur(12px);
56
+ border: 1px solid rgba(0, 255, 255, 0.2);
57
+ box-shadow: 0 8px 32px rgba(0, 255, 255, 0.1);
58
+ max-width: 420px;
59
+ width: 100%;
60
+ }
61
+ h2 {
62
+ font-family: 'Orbitron', sans-serif;
63
+ color: var(--neon);
64
+ text-align: center;
65
+ text-shadow: 0 0 15px var(--neon);
66
+ margin-bottom: 1.5rem;
67
+ }
68
+ .form-control {
69
+ background: rgba(255, 255, 255, 0.1);
70
+ border: 1px solid rgba(0, 255, 255, 0.3);
71
+ color: #fff;
72
+ border-radius: 10px;
73
+ padding: 0.75rem;
74
+ }
75
+ .form-control:focus {
76
+ background: rgba(255, 255, 255, 0.15);
77
+ border-color: var(--neon);
78
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
79
+ color: #fff;
80
+ }
81
+ .btn-neon {
82
+ background: transparent;
83
+ border: 2px solid var(--neon);
84
+ color: var(--neon);
85
+ font-weight: bold;
86
+ border-radius: 50px;
87
+ padding: 0.7rem 1.5rem;
88
+ transition: all 0.3s;
89
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.4);
90
+ width: 100%;
91
+ margin-top: 1rem;
92
+ }
93
+ .btn-neon:hover {
94
+ background: var(--neon);
95
+ color: #000;
96
+ box-shadow: 0 0 20px var(--neon);
97
+ }
98
+ .toggle-text {
99
+ text-align: center;
100
+ margin-top: 1.5rem;
101
+ color: #ccc;
102
+ font-size: 0.95rem;
103
+ }
104
+ .toggle-text a {
105
+ color: var(--neon);
106
+ text-decoration: none;
107
+ font-weight: 500;
108
+ }
109
+ .toggle-text a:hover {
110
+ text-decoration: underline;
111
+ }
112
+ .logo {
113
+ display: block;
114
+ margin: 0 auto 1.5rem;
115
+ height: 60px;
116
+ filter: drop-shadow(0 0 10px var(--neon));
117
+ border-radius: 20px;
118
+ object-fit: cover;
119
+ }
120
+ .alert {
121
+ margin-top: 1rem;
122
+ font-size: 0.9rem;
123
+ }
124
+ </style>
125
+ </head>
126
+ <body>
127
+
128
+ <!-- Back Arrow (REAL ARROW SYMBOL) -->
129
+ <div class="back-arrow" onclick="window.location.href='layout.html'" title="Back to Home">
130
+ &larr;
131
+ </div>
132
+
133
+ <div id="particles"></div>
134
+
135
+ <div class="card">
136
+ <img src="./logo.jpg" alt="DigitVision Logo" class="logo">
137
+ <h2 id="formTitle">Sign In</h2>
138
+
139
+ <form id="authForm">
140
+ <div id="signupFields" style="display:none;">
141
+ <div class="mb-3">
142
+ <input type="text" class="form-control" id="name" placeholder="Full Name" required>
143
+ </div>
144
+ </div>
145
+ <div class="mb-3">
146
+ <input type="email" class="form-control" id="email" placeholder="Email Address" required>
147
+ </div>
148
+ <div class="mb-3">
149
+ <input type="password" class="form-control" id="password" placeholder="Password" required>
150
+ </div>
151
+ <button type="submit" class="btn btn-neon" id="submitBtn">Sign In</button>
152
+ </form>
153
+
154
+ <div class="toggle-text" id="toggleMessage">
155
+ Don't have an account? <a href="#" id="toggleLink">Sign Up &larr;</a>
156
+ </div>
157
+
158
+ <div id="alertBox"></div>
159
+ </div>
160
+
161
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/particles.js/2.0.0/particles.min.js"></script>
162
+ <script>
163
+ particlesJS('particles', {
164
+ particles: {
165
+ number: { value: 80, density: { enable: true, value_area: 800 } },
166
+ color: { value: '#00ffff' },
167
+ shape: { type: 'circle' },
168
+ opacity: { value: 0.5, random: true },
169
+ size: { value: 3, random: true },
170
+ line_linked: { enable: true, distance: 150, color: '#00ffff', opacity: 0.4, width: 1 },
171
+ move: { enable: true, speed: 1.5, direction: 'none', random: false, straight: false, out_mode: 'out' }
172
+ },
173
+ interactivity: { detect_on: 'window', events: { onhover: { enable: true, mode: 'repulse' } } },
174
+ retina_detect: true
175
+ });
176
+
177
+ const isSignup = { value: false };
178
+ const formTitle = document.getElementById('formTitle');
179
+ const signupFields = document.getElementById('signupFields');
180
+ const submitBtn = document.getElementById('submitBtn');
181
+ const toggleMessage = document.getElementById('toggleMessage');
182
+ const alertBox = document.getElementById('alertBox');
183
+
184
+ // Toggle between Sign In and Sign Up
185
+ toggleMessage.addEventListener('click', (e) => {
186
+ if (e.target.id !== 'toggleLink') return;
187
+ e.preventDefault();
188
+
189
+ isSignup.value = !isSignup.value;
190
+ formTitle.textContent = isSignup.value ? 'Sign Up' : 'Sign In';
191
+ signupFields.style.display = isSignup.value ? 'block' : 'none';
192
+ submitBtn.textContent = isSignup.value ? 'Create Account' : 'Sign In';
193
+
194
+ // REAL ARROW SYMBOLS
195
+ toggleMessage.innerHTML = isSignup.value
196
+ ? 'Already have an account? <a href="#" id="toggleLink">Sign In &rarr;</a>'
197
+ : 'Don\'t have an account? <a href="#" id="toggleLink">Sign Up &larr;</a>';
198
+
199
+ alertBox.innerHTML = '';
200
+ });
201
+
202
+ // Form Submit → Always go to home.html
203
+ document.getElementById('authForm').addEventListener('submit', (e) => {
204
+ e.preventDefault();
205
+
206
+ const email = document.getElementById('email').value.trim();
207
+ const password = document.getElementById('password').value;
208
+ const nameInput = document.getElementById('name');
209
+
210
+ // Determine name based on signup state
211
+ const name = isSignup.value ? (nameInput ? nameInput.value.trim() : '') : 'User';
212
+
213
+ // Validation
214
+ if (!email || !password) {
215
+ showAlert('Please fill all required fields.', 'danger');
216
+ return;
217
+ }
218
+ if (isSignup.value && !name) {
219
+ showAlert('Please enter your name.', 'danger');
220
+ return;
221
+ }
222
+ if (password.length < 6) {
223
+ showAlert('Password must be at least 6 characters.', 'warning');
224
+ return;
225
+ }
226
+
227
+ // Use URL params instead of localStorage for file:// protocol compatibility
228
+ const user = { name: isSignup.value ? name : (email.split('@')[0] || 'User'), email };
229
+ const params = new URLSearchParams({
230
+ loggedIn: 'true',
231
+ user: JSON.stringify(user)
232
+ });
233
+
234
+ showAlert(`Welcome, ${user.name}! Redirecting...`, 'success');
235
+
236
+ setTimeout(() => {
237
+ window.location.href = 'home.html?' + params.toString();
238
+ }, 1200);
239
+ });
240
+
241
+ function showAlert(msg, type) {
242
+ alertBox.innerHTML = `<div class="alert alert-${type}">${msg}</div>`;
243
+ }
244
+ </script>
245
+ </body>
246
+ </html>