joytheslothh commited on
Commit
c68c9e6
·
2 Parent(s): f8b517fc851312

Merge branch 'main' of https://github.com/JoyTheSloth/BacSense-2.0

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +10 -0
  2. BacSense_v2_Technical_Documentation.docx +3 -0
  3. BacSense_v2_Technical_Documentation.docx-1.pdf +3 -0
  4. BacSense_v2_Technical_Documentation.docx.txt +628 -0
  5. Dockerfile +31 -0
  6. README.md +77 -8
  7. bacsense_v2_package/class_names.pkl +3 -0
  8. bacsense_v2_package/example_usage.py +48 -0
  9. bacsense_v2_package/inference.py +268 -0
  10. bacsense_v2_package/pca_model.pkl +3 -0
  11. bacsense_v2_package/requirements.txt +7 -0
  12. bacsense_v2_package/specialist_metadata.json +38 -0
  13. bacsense_v2_package/specialist_pca.pkl +3 -0
  14. bacsense_v2_package/specialist_scaler.pkl +3 -0
  15. bacsense_v2_package/specialist_svm.pkl +3 -0
  16. bacsense_v2_package/standard_scaler.pkl +3 -0
  17. bacsense_v2_package/svm_classifier.pkl +3 -0
  18. bacsense_v2_package/vgg16_feature_extractor.keras +3 -0
  19. bacterial-classifier/.gitignore +7 -0
  20. bacterial-classifier/README.txt +62 -0
  21. bacterial-classifier/api.py +80 -0
  22. bacterial-classifier/app.py +226 -0
  23. bacterial-classifier/requirements.txt +7 -0
  24. bacterial-classifier/run_backend.ps1 +4 -0
  25. frontend/.gitignore +25 -0
  26. frontend/README.md +73 -0
  27. frontend/eslint.config.js +23 -0
  28. frontend/index.html +63 -0
  29. frontend/package-lock.json +0 -0
  30. frontend/package.json +35 -0
  31. frontend/public/bacteria-bg.png +3 -0
  32. frontend/public/bacteria-light-bg.png +3 -0
  33. frontend/public/favicon.svg +1 -0
  34. frontend/public/galaxy-bg.png +3 -0
  35. frontend/public/icons.svg +24 -0
  36. frontend/run_frontend.ps1 +2 -0
  37. frontend/src/App.css +1 -0
  38. frontend/src/App.tsx +27 -0
  39. frontend/src/assets/hero.png +3 -0
  40. frontend/src/assets/react.svg +1 -0
  41. frontend/src/assets/vite.svg +1 -0
  42. frontend/src/components/Footer.tsx +45 -0
  43. frontend/src/components/Header.tsx +31 -0
  44. frontend/src/components/Hero.tsx +65 -0
  45. frontend/src/components/Results.tsx +54 -0
  46. frontend/src/components/Species.tsx +36 -0
  47. frontend/src/components/TargetCursor.tsx +321 -0
  48. frontend/src/components/UploadZone.tsx +301 -0
  49. frontend/src/index.css +1 -0
  50. frontend/src/lib/utils.ts +6 -0
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .gemini/
2
+ site/
3
+ .stitch/
4
+ frontend/node_modules/
5
+ frontend/dist/
6
+ bacterial-classifier/venv/
7
+ bacterial-classifier/__pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ .DS_Store
BacSense_v2_Technical_Documentation.docx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98a586556a6a10ef4de5a36a7c870a7065275d817dce50248eaa80e73c7b9777
3
+ size 24791
BacSense_v2_Technical_Documentation.docx-1.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf372800de468169f3e1fff7acd2eb447a4fc9c87398a096a986da11d217ecd3
3
+ size 299570
BacSense_v2_Technical_Documentation.docx.txt ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BacSense v2 | Technical Documentation
2
+
3
+
4
+ BacSense
5
+ Version 2.0
6
+ Technical Architecture & Model Documentation
7
+
8
+
9
+ Cascaded Hybrid Classifier for Waterborne Bacterial Identification
10
+ VGG16 Transfer Learning + Hand-Crafted Feature Engineering + RBF-SVM
11
+
12
+
13
+ Author
14
+ Alapan Sen
15
+ Guide
16
+ Dr. Nilanjana Dutta Roy
17
+ Institution
18
+ Amity University Kolkata
19
+ Deployed
20
+ bacsense.streamlit.app
21
+ Version
22
+ 2.0 — Cascaded Specialist
23
+ Date
24
+ March 2026
25
+ ________________
26
+
27
+
28
+ 1. Executive Summary
29
+ BacSense v2 is a two-stage cascaded hybrid classifier for the identification of five waterborne bacterial species from Gram-stained microscopy images. The system combines deep transfer learning (VGG16) with rich hand-crafted feature engineering and a binary specialist Support Vector Machine to resolve a critical confusion pair that the original single-stage model completely failed on.
30
+
31
+
32
+ Problem: The original BacSense v1 achieved 95.83% overall accuracy but had near-zero F1 scores for Escherichia coli and Pseudomonas aeruginosa — two morphologically similar Gram-negative rods that VGG16 FC features alone could not separate.
33
+
34
+
35
+ Solution: A cascaded architecture that routes ambiguous predictions to a specialist binary SVM trained on a 683-dimensional feature vector combining VGG16 deep features with seven hand-crafted descriptors targeting the discriminative signals invisible to the main model.
36
+
37
+
38
+ Metric
39
+ v1 (Main SVM only)
40
+ v2 (Cascaded)
41
+ Overall Accuracy
42
+ 95.83%
43
+ 95.65%
44
+ E. coli F1
45
+ ~0.00 (failing)
46
+ 0.9568
47
+ P. aeruginosa F1
48
+ ~0.00 (failing)
49
+ 0.9563
50
+ P. aeruginosa Recall
51
+ ~0.00
52
+ 0.9480
53
+ Specialist ROC-AUC
54
+
55
+ 0.9863
56
+ CV F1 (Specialist)
57
+
58
+ 0.9498
59
+
60
+
61
+ ________________
62
+
63
+
64
+ 2. Dataset
65
+ 2.1 Source: DIBaS (Digital Image of Bacteria Species)
66
+ The DIBaS dataset, introduced by Zielinski et al. (2017) in PLOS ONE, is the primary source of microscopy images. It contains 660 images across 33 bacterial species captured at 100x magnification with oil immersion. BacSense uses a 5-class subset of 307 original images corresponding to clinically relevant waterborne pathogens.
67
+
68
+
69
+ Species
70
+ Gram
71
+ Shape
72
+ Original Images
73
+ Risk Level
74
+ Escherichia coli
75
+ Negative
76
+ Rod
77
+ 59
78
+ High
79
+ Pseudomonas aeruginosa
80
+ Negative
81
+ Rod
82
+ 63
83
+ High
84
+ Enterococcus faecalis
85
+ Positive
86
+ Coccus
87
+ 62
88
+ Medium
89
+ Clostridium perfringens
90
+ Positive
91
+ Rod
92
+ 61
93
+ High
94
+ Listeria monocytogenes
95
+ Positive
96
+ Rod
97
+ 62
98
+ High
99
+
100
+
101
+ 2.2 Data Augmentation
102
+ The original 307 images are insufficient for robust deep feature training. An aggressive augmentation pipeline expands the dataset 18.7x to approximately 5,000 images for main model training. For specialist training, each of the two confusion classes is independently augmented to 800 images.
103
+
104
+
105
+ Parameter
106
+ Value
107
+ Rotation range
108
+ 360 degrees (full circular)
109
+ Width / height shift
110
+ 10%
111
+ Horizontal / vertical flip
112
+ Enabled
113
+ Zoom range
114
+ 15%
115
+ Brightness range
116
+ 0.85 – 1.15
117
+ Fill mode
118
+ Reflect
119
+ Target per class (specialist)
120
+ 800 augmented + original PNG
121
+
122
+
123
+ For the specialist, original PNG images (59 E. coli + 63 P. aeruginosa) are combined with augmented JPG images during training to prevent domain shift artifacts between file formats. Total specialist training data: 1,722 images.
124
+ ________________
125
+
126
+
127
+ 3. System Architecture
128
+ 3.1 Overview: Two-Stage Cascaded Design
129
+ BacSense v2 uses a cascaded architecture where a fast 5-class main SVM provides an initial prediction. If that prediction falls within the known confusion pair (E. coli or P. aeruginosa), the image is routed to a dedicated specialist binary SVM that uses a much richer feature representation to make the final call.
130
+
131
+
132
+ Architecture Flow: Input Image → VGG16 Feature Extractor → PCA(94) → Main SVM → [if ambiguous] → 683-dim Feature Extraction → PCA(243) → Specialist SVM → Final Prediction
133
+
134
+
135
+
136
+
137
+ Stage 1: Main SVM
138
+ If Ambiguous
139
+ Stage 2: Specialist
140
+ Final Output
141
+ Input
142
+ Raw image
143
+ Features
144
+ VGG16 (512-dim) → PCA (94-dim)
145
+ Trigger
146
+ E. coli or P. aeruginosa predicted
147
+ Features
148
+ 683-dim rich vector → PCA (243-dim)
149
+ Output
150
+ 5-class or binary species label
151
+
152
+
153
+ 3.2 Stage 1: Main Classifier
154
+ 3.2.1 Feature Extractor — VGG16
155
+ VGG16 is used as a frozen feature extractor with ImageNet pre-trained weights. The top classification layer is removed, and the output of the last global average pooling layer provides a 512-dimensional feature vector for each input image. Images are resized to 128 x 128 pixels and normalized to [0, 1] before passing through the network.
156
+
157
+
158
+ Component
159
+ Detail
160
+ Base architecture
161
+ VGG16 (Simonyan & Zisserman, 2014)
162
+ Pre-training
163
+ ImageNet (1.2M images, 1000 classes)
164
+ Fine-tuning
165
+ None — weights fully frozen
166
+ Input size
167
+ 128 x 128 x 3 (RGB)
168
+ Output
169
+ 512-dimensional feature vector
170
+
171
+
172
+ 3.2.2 Dimensionality Reduction — PCA
173
+ Principal Component Analysis reduces the 512-dimensional VGG16 output to 94 components while retaining 95% of explained variance. This reduces overfitting risk, lowers SVM training time, and removes correlated feature dimensions that introduce noise into the decision boundary.
174
+
175
+
176
+ 3.2.3 Main Classifier — RBF-SVM
177
+ A Radial Basis Function Support Vector Machine is trained on the PCA-reduced features for 5-class classification. The SVM uses a one-vs-one decision strategy with class-balanced weighting to handle slight class imbalance in the original dataset.
178
+
179
+
180
+ Hyperparameter
181
+ Value
182
+ Selection Method
183
+ Kernel
184
+ RBF
185
+ Fixed — standard for high-dim features
186
+ C (regularization)
187
+ 10
188
+ GridSearchCV, 3-fold CV
189
+ Gamma
190
+ 0.01
191
+ GridSearchCV, 3-fold CV
192
+ Class weight
193
+ Balanced
194
+ Fixed — handles class imbalance
195
+ Decision function
196
+ OVO (one-vs-one)
197
+ Default for multi-class SVC
198
+
199
+
200
+ ________________
201
+
202
+
203
+ 4. Stage 2: Specialist Classifier
204
+ 4.1 Motivation — Why a Specialist?
205
+ E. coli and P. aeruginosa are both Gram-negative, rod-shaped bacteria with similar cell dimensions and staining characteristics. The standard VGG16 FC feature vector collapses their representations into overlapping regions in feature space, producing near-zero F1 scores for both species in the main model. Three peer-reviewed papers converge on the same solution: richer feature concatenation targeting color distribution, texture micropatterns, and spatial arrangement.
206
+
207
+
208
+ Paper
209
+ Key Finding
210
+ Feature Applied in BacSense
211
+ Zielinski et al., 2017 (PLOS ONE)
212
+ Color distribution features explicitly recommended for morphologically similar species
213
+ HSV histogram (48-dim)
214
+ Wahid et al. (Inception-v3 + SVM)
215
+ Feature concatenation is the principled fix for intra-species confusion
216
+ Full 683-dim concatenated vector
217
+ Rachmad et al., 2020
218
+ Binary CNN+SVM specialist viable for rod-shaped bacteria
219
+ Cascaded binary specialist SVM
220
+
221
+
222
+ 4.2 Feature Vector — 683 Dimensions
223
+ The specialist uses a 683-dimensional feature vector formed by concatenating VGG16 deep features with seven hand-crafted descriptors. Each descriptor targets a specific visual property that differentiates E. coli from P. aeruginosa in Gram-stained images.
224
+
225
+
226
+ Feature Group
227
+ Dims
228
+ What It Captures
229
+ Target Signal
230
+ VGG16 FC Features
231
+ 512
232
+ Deep semantic representation from ImageNet
233
+ General shape, high-level patterns
234
+ HSV Histogram
235
+ 48
236
+ Color distribution across Hue/Sat/Value bins (4x4x3)
237
+ Stain intensity differences
238
+ Morphological
239
+ 5
240
+ Area, perimeter, circularity, aspect ratio, solidity
241
+ Cell shape and size
242
+ LBP Histogram
243
+ 59
244
+ Local Binary Patterns, P=8, R=1, uniform
245
+ Texture micropatterns, biofilm
246
+ GLCM Descriptors
247
+ 24
248
+ Contrast, dissimilarity, homogeneity, energy, correlation, ASM (4 angles)
249
+ Spatial gray-level co-occurrence
250
+ Channel Stats
251
+ 9
252
+ Mean, std, skewness per H/S/V channel
253
+ Fine-grained color statistics
254
+ Density Grid
255
+ 16
256
+ 4x4 spatial grid of foreground pixel density
257
+ Clustering vs dispersal pattern
258
+ Hu Moments
259
+ 7
260
+ 7 rotation-invariant moment descriptors (log-transformed)
261
+ Global shape invariants
262
+ Curvature
263
+ 3
264
+ Mean, std, max curvature across all contour points
265
+ Rod bending — key P. aeruginosa signal
266
+
267
+
268
+ 4.3 Why Each Feature Matters for This Pair
269
+ LBP (Local Binary Patterns)
270
+ P. aeruginosa is known to form loose biofilm-like clusters under microscopy. LBP captures the local texture microstructure created by these aggregations — a signal completely absent from VGG16 FC features which operate at a semantic rather than textural level.
271
+
272
+
273
+ GLCM (Gray-Level Co-occurrence Matrix)
274
+ GLCM descriptors computed at four angles (0, 45, 90, 135 degrees) capture the spatial relationship between intensity values across the image. P. aeruginosa typically shows higher local contrast and dissimilarity values due to its clustering behavior and slightly different staining depth.
275
+
276
+
277
+ Curvature Statistics
278
+ P. aeruginosa rods tend to exhibit more curvature and bending than E. coli rods, which are typically straighter. Curvature is computed as the cross product of consecutive edge vectors along detected contours, summarized as mean, standard deviation, and maximum curvature values across all contours in the image.
279
+
280
+
281
+ Spatial Density Grid
282
+ A 4x4 grid divides the image into 16 cells and computes foreground pixel density in each cell. This captures the spatial distribution and clustering pattern of bacteria across the slide — P. aeruginosa tends to form denser local aggregations while E. coli distributes more uniformly.
283
+
284
+
285
+ 4.4 Specialist Pipeline
286
+ Step
287
+ Operation
288
+ Output Dims
289
+ 1. VGG16 extraction
290
+ Forward pass through frozen VGG16
291
+ 512
292
+ 2. Hand-craft extraction
293
+ HSV + Morph + LBP + GLCM + ChannelStats + Density + Hu + Curvature
294
+ 171
295
+ 3. Concatenation
296
+ np.concatenate([vgg, rich])
297
+ 683
298
+ 4. Standardization
299
+ StandardScaler (zero mean, unit variance)
300
+ 683
301
+ 5. PCA
302
+ Retain 95% variance
303
+ 243
304
+ 6. SVM prediction
305
+ RBF-SVM, C=100, gamma=0.001, probability=True
306
+ Binary (0=E.coli, 1=P.aeru)
307
+ 7. Confidence gate
308
+ Accept specialist if prob >= 0.90, else fallback to Stage 1
309
+ Final label
310
+
311
+
312
+ 4.5 Confidence Threshold Gate
313
+ A confidence gate prevents the specialist from overriding the main SVM when it is uncertain. If the specialist's maximum class probability is below 0.90, the main SVM's prediction is used as the final answer. This is a practical engineering decision that prevents low-confidence specialist predictions from overriding a correct main SVM result, at the cost of occasionally retaining main SVM errors in the E. coli / P. aeruginosa pair.
314
+
315
+
316
+ Design Note: The 0.90 threshold was chosen based on observed confidence distributions. Future work should calibrate this threshold using a held-out validation set to maximize the F1 score of the gated system.
317
+ ________________
318
+
319
+
320
+ 5. Training Details
321
+ 5.1 Main Model Training
322
+ Component
323
+ Configuration
324
+ Training images
325
+ ~5,000 (307 original × 18.7x augmentation)
326
+ PCA components
327
+ 94 (95% variance retained from 512-dim input)
328
+ SVM kernel
329
+ RBF
330
+ Hyperparameter search
331
+ GridSearchCV, 3-fold cross-validation
332
+ Search space C
333
+ [0.1, 1, 10, 100]
334
+ Search space gamma
335
+ ["scale", 0.1, 0.01, 0.001]
336
+ Best params
337
+ C=10, gamma=0.01
338
+ Scoring metric
339
+ F1 (macro)
340
+ Test accuracy
341
+ 95.83%
342
+
343
+
344
+ 5.2 Specialist Model Training
345
+ Component
346
+ Configuration
347
+ Training images
348
+ 1,722 total (800 aug E.coli + 800 aug P.aeru + 122 original PNG)
349
+ Feature vector
350
+ 683-dim (512 VGG16 + 171 hand-crafted)
351
+ PCA components
352
+ 243 (95% variance retained from 683-dim input)
353
+ SVM kernel
354
+ RBF
355
+ Hyperparameter search
356
+ GridSearchCV, 3-fold cross-validation
357
+ Search space C
358
+ [1, 10, 100]
359
+ Search space gamma
360
+ ["scale", 0.01, 0.001]
361
+ Best params
362
+ C=100, gamma=0.001
363
+ Class weight
364
+ Balanced
365
+ Scoring metric
366
+ F1 (binary)
367
+ CV F1
368
+ 0.9498
369
+ Test accuracy
370
+ 95.65%
371
+
372
+
373
+ 5.3 Domain Shift Handling
374
+ The augmented training images were saved as JPEG files, while the original DIBaS images are PNG. JPEG compression introduces subtle color and texture artifacts that shift the feature distribution, causing the specialist (trained on augmented JPEGs) to misclassify original PNG images at inference. This was resolved by mixing all original PNG images into the specialist training set, forcing the model to learn features robust to both formats.
375
+ ________________
376
+
377
+
378
+ 6. Evaluation Results
379
+ 6.1 Specialist Classifier Performance
380
+ Metric
381
+ E. coli
382
+ P. aeruginosa
383
+ Macro Avg
384
+ Precision
385
+ 0.9486
386
+ 0.9647
387
+ 0.9566
388
+ Recall
389
+ 0.9651
390
+ 0.9480
391
+ 0.9565
392
+ F1-Score
393
+ 0.9568
394
+ 0.9563
395
+ 0.9565
396
+ Support
397
+ 172
398
+ 173
399
+ 345
400
+
401
+
402
+ Overall test accuracy: 95.65% on 345 held-out images. ROC-AUC: 0.9863, indicating strong probability calibration across the decision boundary.
403
+
404
+
405
+ 6.2 Improvement Over v1
406
+ Metric
407
+ v1 Main Model
408
+ v2 Specialist
409
+ Improvement
410
+ E. coli F1
411
+ ~0.00
412
+ 0.9568
413
+ +0.9568
414
+ P. aeruginosa F1
415
+ ~0.00
416
+ 0.9563
417
+ +0.9563
418
+ P. aeruginosa Recall
419
+ ~0.00
420
+ 0.9480
421
+ +0.9480
422
+ CV F1
423
+ N/A
424
+ 0.9498
425
+
426
+ ROC-AUC
427
+ N/A
428
+ 0.9863
429
+
430
+
431
+
432
+ 6.3 Integration Test
433
+ End-to-end testing of the full cascaded pipeline on original held-out images (not seen during training) produced the following results:
434
+
435
+
436
+ Test
437
+ Correct / Total
438
+ Notes
439
+ E. coli (original PNG)
440
+ 5 / 5
441
+ All routed to specialist, correctly classified
442
+ P. aeruginosa (original PNG)
443
+ 5 / 5
444
+ All routed to specialist, correctly classified
445
+ Total
446
+ 10 / 10
447
+ 100% on held-out originals
448
+ ________________
449
+
450
+
451
+ 7. Technical Stack
452
+ Component
453
+ Library / Version
454
+ Purpose
455
+ Deep feature extractor
456
+ TensorFlow / Keras + VGG16
457
+ ImageNet transfer learning
458
+ Dimensionality reduction
459
+ scikit-learn PCA
460
+ Variance-preserving compression
461
+ Main classifier
462
+ scikit-learn SVC (RBF)
463
+ 5-class prediction
464
+ Specialist classifier
465
+ scikit-learn SVC (RBF, probability=True)
466
+ Binary E.coli / P.aeru
467
+ LBP features
468
+ scikit-image local_binary_pattern
469
+ Texture micropatterns
470
+ GLCM features
471
+ scikit-image graycomatrix / graycoprops
472
+ Spatial co-occurrence
473
+ Color / morphology
474
+ OpenCV (cv2)
475
+ HSV histogram, contours, moments
476
+ Statistical features
477
+ scipy.stats.skew
478
+ Channel skewness
479
+ Image I/O
480
+ Pillow (PIL)
481
+ Image loading and resizing
482
+ Deployment
483
+ Streamlit
484
+ Web interface at bacsense.streamlit.app
485
+
486
+
487
+ 8. Saved Artifacts
488
+ All model artifacts are saved to Google Drive and packaged into bacsense_v2_package.zip for local deployment.
489
+
490
+
491
+ File
492
+ Size
493
+ Description
494
+ vgg16_feature_extractor.keras
495
+ ~55 MB
496
+ Frozen VGG16 feature extractor (no top layer)
497
+ pca_model.pkl
498
+ ~2 MB
499
+ PCA model for main pipeline (512 → 94 dims)
500
+ standard_scaler.pkl
501
+ ~0.1 MB
502
+ Scaler for main SVM input
503
+ svm_classifier.pkl
504
+ Variable
505
+ Trained 5-class RBF-SVM
506
+ class_names.pkl
507
+ <1 KB
508
+ Ordered list of 5 species names
509
+ specialist_svm.pkl
510
+ ~712 KB
511
+ Binary specialist RBF-SVM (prob=True)
512
+ specialist_scaler.pkl
513
+ ~13 KB
514
+ Scaler for 683-dim specialist input
515
+ specialist_pca.pkl
516
+ ~1 MB
517
+ PCA for specialist (683 → 243 dims)
518
+ specialist_metadata.json
519
+ <1 KB
520
+ Training config, metrics, timestamps
521
+ inference.py
522
+ ~15 KB
523
+ Standalone BacSense class for deployment
524
+ requirements.txt
525
+ <1 KB
526
+ Python dependency list
527
+ example_usage.py
528
+ ~3 KB
529
+ Quickstart and Streamlit integration code
530
+ ________________
531
+
532
+
533
+ 9. Inference API
534
+ 9.1 Installation
535
+ Install dependencies and point the BacSense class at the unpacked package directory:
536
+
537
+
538
+ Install: pip install tensorflow scikit-learn scikit-image opencv-python scipy Pillow
539
+
540
+
541
+ 9.2 Usage
542
+ Method
543
+ Signature
544
+ Returns
545
+ Constructor
546
+ BacSense(model_dir, specialist_threshold=0.90)
547
+ BacSense instance
548
+ warmup()
549
+ Pre-loads all models into memory
550
+ None
551
+ predict()
552
+ predict(img_path, verbose=False)
553
+ Result dict (see below)
554
+ predict_batch()
555
+ predict_batch(img_paths, verbose=False)
556
+ List of result dicts
557
+
558
+
559
+ 9.3 Result Dictionary
560
+ Key
561
+ Type
562
+ Example Value
563
+ prediction
564
+ str
565
+ Escherichia coli
566
+ confidence
567
+ float
568
+ 0.9621
569
+ routed_to_specialist
570
+ bool
571
+ True
572
+ specialist_accepted
573
+ bool
574
+ True
575
+ main_prediction
576
+ str
577
+ Escherichia coli
578
+ gram
579
+ str
580
+ Negative
581
+ shape
582
+ str
583
+ Rod
584
+ risk
585
+ str
586
+ High
587
+ ________________
588
+
589
+
590
+ 10. Limitations & Future Work
591
+ 10.1 Current Limitations
592
+ * P. aeruginosa recall is 94.80% — approximately 5.2% of P. aeruginosa images are still misclassified.
593
+ * The confidence threshold (0.90) was chosen empirically, not calibrated on a held-out validation set.
594
+ * The dataset is small (307 original images across 5 classes). Models trained on small datasets may not generalize to different microscopes, staining protocols, or magnifications.
595
+ * JPEG augmentation introduces domain shift relative to original PNG images; mitigated by mixing formats in training, but not fully eliminated.
596
+ * Inference speed is slower than single-stage models due to sequential feature extraction for routed images.
597
+
598
+
599
+ 10.2 Future Work
600
+ * Collect 20-30 more original P. aeruginosa images to improve specialist recall beyond 0.95.
601
+ * Add Gabor filter features (5 scales x 8 orientations = 40 dims) to further capture P. aeruginosa biofilm texture.
602
+ * Calibrate confidence threshold using cross-validated F1 optimization on a dedicated validation set.
603
+ * Extend specialist to handle all morphologically similar pairs, not just E. coli / P. aeruginosa.
604
+ * Evaluate on images from a different microscopy system to assess generalization.
605
+ * Replace binary specialist with a soft-label ensemble combining main SVM and specialist votes.
606
+ ________________
607
+
608
+
609
+ 11. References
610
+
611
+
612
+ [1] Zielinski, B., Plichta, A., Misztal, K., Spurek, P., Brzychczy-Wloch, M., & Ochonska, D. (2017). Deep learning approach to bacterial colony classification. PLOS ONE, 12(9), e0184554.
613
+
614
+
615
+ [2] Rachmad, A., et al. (2020). Comparison of CNN-Based methods for tuberculosis bacteria classification using ResNet-101 and Support Vector Machine. Journal of Physics: Conference Series.
616
+
617
+
618
+ [3] Wahid, A., et al. CNN-based bacterial image classification using Inception-v3 with SVM, KNN, and Naive Bayes classifiers. Applied Sciences.
619
+
620
+
621
+ [4] Simonyan, K., & Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. arXiv:1409.1556.
622
+
623
+
624
+ [5] Ojala, T., Pietikainen, M., & Maenpaa, T. (2002). Multiresolution gray-scale and rotation invariant texture classification with local binary patterns. IEEE TPAMI, 24(7), 971-987.
625
+
626
+
627
+ [6] Haralick, R. M., Shanmugam, K., & Dinstein, I. (1973). Textural features for image classification. IEEE Transactions on Systems, Man, and Cybernetics, 3(6), 610-621.
628
+ Amity University Kolkata | Alapan SenPage
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Install system dependencies for OpenCV and scikit-image
4
+ RUN apt-get update && apt-get install -y \
5
+ libgl1-mesa-glx \
6
+ libglib2.0-0 \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Set up a new user named "user" with user ID 1000
10
+ RUN useradd -m -u 1000 user
11
+ USER user
12
+ ENV PATH="/home/user/.local/bin:$PATH"
13
+
14
+ # Set the working directory to the user's home directory
15
+ WORKDIR /home/user/app
16
+
17
+ # Copy the requirements file into the container
18
+ COPY --chown=user requirements.txt .
19
+
20
+ # Install the Python dependencies
21
+ RUN pip install --no-cache-dir --upgrade pip
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ # Copy the rest of the application code into the container
25
+ COPY --chown=user . .
26
+
27
+ # Hugging Face Spaces expects the app to run on port 7860
28
+ EXPOSE 7860
29
+
30
+ # Command to run the FastAPI application
31
+ CMD ["uvicorn", "bacterial-classifier.api:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: BacSense API
3
- emoji: 📚
4
- colorFrom: purple
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🦠 Bacsense 2.0
2
+
3
+ ![Bacsense Banner](https://img.shields.io/badge/Bacsense-2.0-13a4ec?style=for-the-badge)
4
+
5
+ **Bacsense 2.0** is an open-access visual platform for clinical microbiology research. Our mission is to accelerate pathogen identification through advanced hybrid neural networks and machine learning.
6
+
7
+ This project integrates a robust **VGG16 + SVM** hybrid classification architecture with a modern, high-performance web interface to quickly and accurately identify microscopic bacterial species from uploaded culture images.
8
+
9
+ ## ✨ Key Features
10
+
11
+ - **🔬 High-Accuracy Classification:** Leverages a pre-trained VGG16 backbone for deep feature extraction, paired with a Support Vector Machine (SVM) classifier for pinpoint taxa identification.
12
+ - **⚡ Real-time API:** Fast and lightweight inference backend powered by FastAPI.
13
+ - **🌌 Premium Scientific UI:** A stunning, fully responsive dark-theme design featuring highly interactive GSAP spring cursors, meteor shower effects, and beautifully animated petri-dish data components.
14
+ - **📊 Detailed Analysis Metrics:** Get immediate clinical insights on morphological traits, probability distribution thresholds, and gram stains for tested pathogens natively in the browser.
15
+
16
+ ## 🛠️ Tech Stack
17
+
18
+ ### Frontend
19
+ - **Framework:** React + Vite (TypeScript)
20
+ - **Styling:** Tailwind CSS
21
+ - **Animations:** GSAP (GreenSock) & Framer Motion
22
+ - **UI Architecture:** MagicUI
23
+
24
+ ### Backend / ML Engine
25
+ - **REST API Runtime:** FastAPI & Uvicorn
26
+ - **Machine Learning Pipelines:** TensorFlow / Keras (VGG16), Scikit-Learn (SVM, PCA)
27
+ - **Image Processing Computation:** Pillow (PIL), NumPy, SciPy
28
+
29
  ---
 
 
 
 
 
 
 
30
 
31
+ ## 🚀 Getting Started
32
+
33
+ ### Prerequisites
34
+ - [Node.js](https://nodejs.org/) (v16+)
35
+ - [Python](https://python.org/) (3.9+)
36
+
37
+ ### 1. Boot the ML Backend
38
+
39
+ Open a terminal in the project root and navigate to the backend service to spin up the prediction API:
40
+
41
+ ```bash
42
+ cd bacterial-classifier
43
+ python -m venv venv
44
+
45
+ # Windows Activation
46
+ venv\Scripts\activate
47
+ # Mac/Linux Activation
48
+ # source venv/bin/activate
49
+
50
+ pip install -r requirements.txt
51
+ pip install fastapi uvicorn python-multipart
52
+
53
+ # Start the FastAPI uvicorn server
54
+ uvicorn api:app --host 0.0.0.0 --port 5000 --reload
55
+ ```
56
+ The ML API will successfully bind to `http://localhost:5000`.
57
+
58
+ ### 2. Start the React Frontend
59
+
60
+ Open a new terminal tab, navigate to the frontend folder, install dependencies, and launch the Vite dev server:
61
+
62
+ ```bash
63
+ cd frontend
64
+ npm install
65
+ npm run dev
66
+ ```
67
+
68
+ The user interface will be live at `http://localhost:5173`. 🥳 Drag and drop a microscopic image into the Upload Zone to test the prediction model!
69
+
70
+ ## 🔬 Supported Species
71
+ The engine spans multiple common pathogenic datasets and correctly identifies critical bacteria including:
72
+ - *Escherichia coli* (Gram-negative)
73
+ - *Staphylococcus aureus* (Gram-positive)
74
+ - *Clostridium perfringens* (Anaerobic)
75
+ - *Bacillus cereus* (Spore-forming)
76
+ - *Listeria monocytogenes*
77
+
78
+ ---
79
+ *© 2026 Bacsense Scientific Systems. Built for Next-Gen Bioinformatics.*
bacsense_v2_package/class_names.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5acfb42b5a3aa9da5d6a4479ca3ef12d737a42923c4205fa51645dcfdb4bb635
3
+ size 135
bacsense_v2_package/example_usage.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # BacSense v2 — Usage Examples
3
+ # pip install -r requirements.txt
4
+ # =============================================================================
5
+
6
+ from inference import BacSense
7
+
8
+ # ── Basic usage ───────────────────────────────────────────────────
9
+ model = BacSense("bacsense_v2_package")
10
+ model.warmup() # pre-load at startup (optional but recommended)
11
+
12
+ result = model.predict("image.png", verbose=True)
13
+ print(result["prediction"]) # Escherichia coli
14
+ print(result["confidence"]) # 0.9621
15
+ print(result["gram"]) # Negative
16
+ print(result["shape"]) # Rod
17
+ print(result["risk"]) # High
18
+
19
+ # ── Batch prediction ──────────────────────────────────────────────
20
+ results = model.predict_batch(["img1.png", "img2.png", "img3.png"])
21
+ for r in results:
22
+ print(r["prediction"], r["confidence"])
23
+
24
+ # ── Streamlit app ─────────────────────────────────────────────────
25
+ # import streamlit as st
26
+ # from inference import BacSense
27
+ #
28
+ # @st.cache_resource
29
+ # def load_model():
30
+ # m = BacSense("bacsense_v2_package")
31
+ # m.warmup()
32
+ # return m
33
+ #
34
+ # model = load_model()
35
+ # st.title("BacSense v2 — Bacterial Classifier")
36
+ # uploaded = st.file_uploader("Upload microscopy image", type=["png","jpg","jpeg"])
37
+ # if uploaded:
38
+ # with open("temp_input.png", "wb") as f:
39
+ # f.write(uploaded.read())
40
+ # result = model.predict("temp_input.png", verbose=True)
41
+ # st.success(f"Prediction: {result['prediction']}")
42
+ # st.metric("Confidence", f"{result['confidence']:.2%}")
43
+ # col1, col2, col3 = st.columns(3)
44
+ # col1.metric("Gram Stain", result["gram"])
45
+ # col2.metric("Shape", result["shape"])
46
+ # col3.metric("Risk Level", result["risk"])
47
+ # if result["routed_to_specialist"]:
48
+ # st.info("Specialist classifier was used for E.coli / P.aeruginosa disambiguation")
bacsense_v2_package/inference.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # =============================================================================
3
+ # BacSense v2 — Standalone Inference Module
4
+ # =============================================================================
5
+ # Usage:
6
+ # from inference import BacSense
7
+ # model = BacSense("path/to/bacsense_v2_package")
8
+ # result = model.predict("image.png")
9
+ # print(result["prediction"])
10
+ # =============================================================================
11
+
12
+ import pickle, cv2, warnings
13
+ import numpy as np
14
+ from pathlib import Path
15
+ from PIL import Image
16
+
17
+ warnings.filterwarnings("ignore")
18
+
19
+
20
+ def _lazy_imports():
21
+ from scipy.stats import skew
22
+ from skimage.feature import local_binary_pattern, graycomatrix, graycoprops
23
+ from tensorflow.keras.models import load_model
24
+ return skew, local_binary_pattern, graycomatrix, graycoprops, load_model
25
+
26
+
27
+ class BacSense:
28
+ """
29
+ BacSense v2 — Two-stage cascaded bacterial classifier.
30
+
31
+ Stage 1 : VGG16 + PCA(94) + RBF-SVM → 5-class prediction
32
+ Stage 2 : VGG16 + 171-dim handcrafted features + PCA(243) + RBF-SVM
33
+ (only for E. coli / P. aeruginosa confusion pair)
34
+
35
+ Feature vector (Stage 2): 683-dim
36
+ VGG16(512) + HSV histogram(48) + Morphological(5) + LBP(59)
37
+ + GLCM(24) + Channel stats(9) + Density grid(16)
38
+ + Hu moments(7) + Curvature(3)
39
+
40
+ Performance:
41
+ Overall accuracy : 95.65%
42
+ E. coli F1 : 0.9568
43
+ P. aeruginosa F1 : 0.9563
44
+ ROC-AUC : 0.9863
45
+ """
46
+
47
+ SPECIES_INFO = {
48
+ "Escherichia coli": {"gram": "Negative", "shape": "Rod", "risk": "High"},
49
+ "Pseudomonas aeruginosa": {"gram": "Negative", "shape": "Rod", "risk": "High"},
50
+ "Enterococcus faecalis": {"gram": "Positive", "shape": "Coccus", "risk": "Medium"},
51
+ "Clostridium perfringens": {"gram": "Positive", "shape": "Rod", "risk": "High"},
52
+ "Listeria monocytogenes": {"gram": "Positive", "shape": "Rod", "risk": "High"},
53
+ }
54
+
55
+ AMBIGUOUS = ["Escherichia coli", "Pseudomonas aeruginosa"]
56
+
57
+ def __init__(self, model_dir: str, specialist_threshold: float = 0.90):
58
+ """
59
+ Args:
60
+ model_dir : path to folder containing all model files
61
+ specialist_threshold : min specialist confidence to accept (default 0.90)
62
+ """
63
+ self.model_dir = Path(model_dir)
64
+ self.threshold = specialist_threshold
65
+ self._loaded = False
66
+
67
+ def _load(self):
68
+ if self._loaded:
69
+ return
70
+ print("Loading BacSense v2 models...")
71
+ skew, lbp_fn, graycomatrix, graycoprops, load_model = _lazy_imports()
72
+ self._skew = skew
73
+ self._lbp_fn = lbp_fn
74
+ self._graycomatrix = graycomatrix
75
+ self._graycoprops = graycoprops
76
+
77
+ d = self.model_dir
78
+ self.feature_extractor = load_model(str(d / "vgg16_feature_extractor.keras"))
79
+ with open(d / "pca_model.pkl", "rb") as f: self.pca = pickle.load(f)
80
+ with open(d / "standard_scaler.pkl", "rb") as f: self.scaler = pickle.load(f)
81
+ with open(d / "svm_classifier.pkl", "rb") as f: self.main_svm = pickle.load(f)
82
+ with open(d / "class_names.pkl", "rb") as f: self.class_names = pickle.load(f)
83
+ with open(d / "specialist_svm.pkl", "rb") as f: self.spec_svm = pickle.load(f)
84
+ with open(d / "specialist_scaler.pkl", "rb") as f: self.spec_scaler = pickle.load(f)
85
+ with open(d / "specialist_pca.pkl", "rb") as f: self.spec_pca = pickle.load(f)
86
+ self._loaded = True
87
+ print(" All models loaded ✅")
88
+
89
+ # ── Feature extractors ─────────────────────────────────────────
90
+
91
+ def _hsv_histogram(self, img_path, bins=(4, 4, 3)):
92
+ img = cv2.imread(str(img_path))
93
+ hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
94
+ hist = cv2.calcHist([hsv], [0,1,2], None, list(bins), [0,180,0,256,0,256])
95
+ return cv2.normalize(hist, hist).flatten() # 48
96
+
97
+ def _morphological(self, img_path):
98
+ img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
99
+ blur = cv2.GaussianBlur(img, (5,5), 0)
100
+ _,thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)
101
+ contours,_ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
102
+ if not contours: return np.zeros(5)
103
+ c = max(contours, key=cv2.contourArea)
104
+ area = cv2.contourArea(c)
105
+ perim = cv2.arcLength(c, True)
106
+ circ = 4*np.pi*area / (perim**2+1e-6)
107
+ x,y,w,h = cv2.boundingRect(c)
108
+ aspect = w / (h+1e-6)
109
+ solidity = area / (cv2.contourArea(cv2.convexHull(c))+1e-6)
110
+ return np.array([area, perim, circ, aspect, solidity]) # 5
111
+
112
+ def _lbp(self, img_path, P=8, R=1, n_bins=59):
113
+ img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
114
+ img = cv2.resize(img, (128,128))
115
+ lbp = self._lbp_fn(img, P, R, method="uniform")
116
+ hist,_ = np.histogram(lbp.ravel(), bins=n_bins, range=(0,n_bins))
117
+ hist = hist.astype(float); hist /= (hist.sum()+1e-6)
118
+ return hist # 59
119
+
120
+ def _glcm(self, img_path):
121
+ img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
122
+ img = cv2.resize(img, (128,128))
123
+ img = (img//4).astype(np.uint8)
124
+ angles = [0, np.pi/4, np.pi/2, 3*np.pi/4]
125
+ glcm = self._graycomatrix(img, distances=[1], angles=angles,
126
+ levels=64, symmetric=True, normed=True)
127
+ feats = []
128
+ for prop in ["contrast","dissimilarity","homogeneity",
129
+ "energy","correlation","ASM"]:
130
+ feats.extend(self._graycoprops(glcm, prop).flatten())
131
+ return np.array(feats) # 24
132
+
133
+ def _channel_stats(self, img_path):
134
+ img = cv2.imread(str(img_path))
135
+ hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(float)
136
+ feats = []
137
+ for ch in range(3):
138
+ d = hsv[:,:,ch].ravel()
139
+ feats.extend([d.mean(), d.std(), self._skew(d)])
140
+ return np.array(feats) # 9
141
+
142
+ def _density_grid(self, img_path, grid=4):
143
+ img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
144
+ img = cv2.resize(img, (128,128))
145
+ _,thresh = cv2.threshold(
146
+ cv2.GaussianBlur(img,(5,5),0), 0, 255,
147
+ cv2.THRESH_BINARY+cv2.THRESH_OTSU)
148
+ h,w = thresh.shape; gh,gw = h//grid, w//grid
149
+ return np.array([
150
+ thresh[i*gh:(i+1)*gh, j*gw:(j+1)*gw].mean()/255.0
151
+ for i in range(grid) for j in range(grid)
152
+ ]) # 16
153
+
154
+ def _hu_moments(self, img_path):
155
+ img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
156
+ img = cv2.resize(img, (128,128))
157
+ _,thresh = cv2.threshold(
158
+ cv2.GaussianBlur(img,(5,5),0), 0, 255,
159
+ cv2.THRESH_BINARY+cv2.THRESH_OTSU)
160
+ hu = cv2.HuMoments(cv2.moments(thresh)).flatten()
161
+ return -np.sign(hu) * np.log10(np.abs(hu)+1e-10) # 7
162
+
163
+ def _curvature(self, img_path):
164
+ img = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
165
+ _,thresh = cv2.threshold(
166
+ cv2.GaussianBlur(img,(5,5),0), 0, 255,
167
+ cv2.THRESH_BINARY+cv2.THRESH_OTSU)
168
+ contours,_ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
169
+ if not contours: return np.zeros(3)
170
+ curvatures = []
171
+ for c in contours:
172
+ if len(c) < 5: continue
173
+ pts = c[:,0,:].astype(float)
174
+ for i in range(1, len(pts)-1):
175
+ v1 = pts[i]-pts[i-1]; v2 = pts[i+1]-pts[i]
176
+ cross = abs(v1[0]*v2[1]-v1[1]*v2[0])
177
+ curvatures.append(cross/(np.linalg.norm(v1)*np.linalg.norm(v2)+1e-6))
178
+ if not curvatures: return np.zeros(3)
179
+ curv = np.array(curvatures)
180
+ return np.array([curv.mean(), curv.std(), curv.max()]) # 3
181
+
182
+ def _rich_features(self, img_path):
183
+ return np.concatenate([
184
+ self._hsv_histogram(img_path), # 48
185
+ self._morphological(img_path), # 5
186
+ self._lbp(img_path), # 59
187
+ self._glcm(img_path), # 24
188
+ self._channel_stats(img_path), # 9
189
+ self._density_grid(img_path), # 16
190
+ self._hu_moments(img_path), # 7
191
+ self._curvature(img_path), # 3
192
+ ]) # 171 total
193
+
194
+ # ── Public API ─────────────────────────────────────────────────
195
+
196
+ def predict(self, img_path: str, verbose: bool = False) -> dict:
197
+ """
198
+ Classify a bacterial microscopy image.
199
+
200
+ Returns dict:
201
+ prediction : str species name
202
+ confidence : float 0-1
203
+ routed_to_specialist : bool
204
+ specialist_accepted : bool
205
+ main_prediction : str
206
+ gram : str Positive / Negative
207
+ shape : str Rod / Coccus
208
+ risk : str High / Medium / Low
209
+ """
210
+ self._load()
211
+ img_path = str(img_path)
212
+
213
+ # Stage 1 — Main SVM (5-class)
214
+ img = Image.open(img_path).convert("RGB").resize((128,128), Image.LANCZOS)
215
+ arr = np.expand_dims(np.array(img).astype("float32")/255.0, axis=0)
216
+
217
+ vgg = self.feature_extractor.predict(arr, verbose=0)
218
+ scaled = self.scaler.transform(self.pca.transform(vgg))
219
+ main_pred = self.main_svm.predict(scaled)[0]
220
+ main_class = self.class_names[main_pred]
221
+ main_conf = float(np.max(self.main_svm.decision_function(scaled)[0]))
222
+
223
+ if verbose:
224
+ print(f"Stage 1 — Main SVM: {main_class} (score: {main_conf:.4f})")
225
+
226
+ result = {
227
+ "prediction": main_class, "confidence": main_conf,
228
+ "routed_to_specialist": False, "specialist_accepted": False,
229
+ "main_prediction": main_class,
230
+ **self.SPECIES_INFO.get(main_class, {"gram":"Unknown","shape":"Unknown","risk":"Unknown"})
231
+ }
232
+
233
+ # Stage 2 — Specialist (ambiguous pair only)
234
+ if main_class in self.AMBIGUOUS:
235
+ if verbose: print("Routing to specialist...")
236
+
237
+ rich = self._rich_features(img_path).reshape(1,-1)
238
+ combined = np.concatenate([vgg, rich], axis=1)
239
+ pca_spec = self.spec_pca.transform(self.spec_scaler.transform(combined))
240
+ spec_pred = self.spec_svm.predict(pca_spec)[0]
241
+ spec_proba = self.spec_svm.predict_proba(pca_spec)[0]
242
+ spec_conf = spec_proba.max()
243
+ spec_class = "Escherichia coli" if spec_pred == 0 else "Pseudomonas aeruginosa"
244
+
245
+ accepted = spec_conf >= self.threshold
246
+ final = spec_class if accepted else main_class
247
+
248
+ if verbose:
249
+ tag = "✅ accepted" if accepted else f"⚠️ below {self.threshold}, fallback to main"
250
+ print(f"Stage 2 — Specialist: {spec_class} (conf: {spec_conf:.4f}) {tag}")
251
+ print(f"Final: {final}")
252
+
253
+ result.update({
254
+ "prediction": final, "confidence": spec_conf,
255
+ "routed_to_specialist": True, "specialist_accepted": accepted,
256
+ **self.SPECIES_INFO.get(final, {"gram":"Unknown","shape":"Unknown","risk":"Unknown"})
257
+ })
258
+
259
+ return result
260
+
261
+ def predict_batch(self, img_paths: list, verbose: bool = False) -> list:
262
+ """Classify a list of image paths. Returns list of result dicts."""
263
+ return [self.predict(p, verbose=verbose) for p in img_paths]
264
+
265
+ def warmup(self):
266
+ """Pre-load all models into memory (call once at app startup)."""
267
+ self._load()
268
+ print("BacSense v2 ready ✅")
bacsense_v2_package/pca_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36d2993f7e2095ecfe3cf449a5213460c7c8ce780965434ee213d3be170c16e9
3
+ size 196517
bacsense_v2_package/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ tensorflow>=2.12.0
2
+ scikit-learn>=1.3.0
3
+ scikit-image>=0.21.0
4
+ opencv-python>=4.8.0
5
+ numpy>=1.24.0
6
+ Pillow>=9.5.0
7
+ scipy>=1.11.0
bacsense_v2_package/specialist_metadata.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "timestamp": "2026-03-16 07:11:14",
3
+ "architecture": "VGG16(512) + HSV(48) + Morph(5) + LBP(59) + GLCM(24) + ChannelStats(9) + DensityGrid(16) + HuMoments(7) + Curvature(3) \u2192 PCA(243) \u2192 SVM",
4
+ "feature_dims": {
5
+ "vgg16": 512,
6
+ "hsv_histogram": 48,
7
+ "morphological": 5,
8
+ "lbp": 59,
9
+ "glcm": 24,
10
+ "channel_stats": 9,
11
+ "density_grid": 16,
12
+ "hu_moments": 7,
13
+ "curvature": 3,
14
+ "combined": 683,
15
+ "after_pca": 243
16
+ },
17
+ "best_params": {
18
+ "C": 100,
19
+ "gamma": 0.001,
20
+ "kernel": "rbf"
21
+ },
22
+ "cv_f1": 0.9498,
23
+ "test_accuracy": 0.9565,
24
+ "ecoli_f1": 0.9568,
25
+ "ecoli_recall": 0.9651,
26
+ "pseudomonas_f1": 0.9563,
27
+ "pseudomonas_recall": 0.948,
28
+ "training_data": {
29
+ "augmented_jpg": 1600,
30
+ "original_png": 122,
31
+ "total": 1722
32
+ },
33
+ "improvement_over_v1": {
34
+ "accuracy": "0.9536 \u2192 0.9565",
35
+ "pseudomonas_recall": "0.9306 \u2192 0.9480",
36
+ "cv_f1": "0.9262 \u2192 0.9498"
37
+ }
38
+ }
bacsense_v2_package/specialist_pca.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:271f198091fa0baeb308ab96be7daefba2622f82dbb76a9c72e0f0807955f93b
3
+ size 1339847
bacsense_v2_package/specialist_scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2e59b83d5e0fd6cf002571ee70e889741a7d94b0556a5a260aeacd795ab0ae44
3
+ size 16855
bacsense_v2_package/specialist_svm.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8df00d049310dbdead39c7e6e6bf58779f0f14b5fe62627e908d15ab59ba0a57
3
+ size 749525
bacsense_v2_package/standard_scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c133477b15a420b3d00ed5c9ca41d72b3fe5212152227c82f0f43490823d3e0e
3
+ size 2715
bacsense_v2_package/svm_classifier.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55296fc956943a2e5e041256a957cc782eea5f47f1a808dcde71f2076d2fda97
3
+ size 2013194
bacsense_v2_package/vgg16_feature_extractor.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48d0af85e0b48c38950b874e49ed61b46ce35085976c84fb8c595d30534bdefe
3
+ size 58933107
bacterial-classifier/.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ venv/
4
+ temp_*.jpg
5
+ temp_*.png
6
+ temp_*.jpeg
7
+ .DS_Store
bacterial-classifier/README.txt ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ BACTERIAL CLASSIFICATION MODEL
3
+ ================================
4
+
5
+ Author: ALAPAN SEN
6
+ Enrollment: A9100522041
7
+ Institution: Amity University Kolkata
8
+
9
+ PERFORMANCE
10
+ -----------
11
+ Test Accuracy: 95.83%
12
+ Validation Accuracy: 94.80%
13
+
14
+ MODEL ARCHITECTURE
15
+ ------------------
16
+ Type: Hybrid VGG16 + SVM
17
+ PCA Components: 94
18
+ SVM Parameters: C=10, gamma=0.01
19
+
20
+ BACTERIAL SPECIES (5 classes)
21
+ ------------------------------
22
+ 1. Clostridium perfringens
23
+ 2. Enterococcus faecalis
24
+ 3. Escherichia coli
25
+ 4. Listeria monocytogenes
26
+ 5. Pseudomonas aeruginosa
27
+
28
+ USAGE
29
+ -----
30
+ 1. Install dependencies:
31
+ pip install -r requirements.txt
32
+
33
+ 2. Test the model:
34
+ python test_model.py
35
+
36
+ 3. Classify an image:
37
+ python inference_script.py bacteria.jpg
38
+
39
+ 4. Or use in Python:
40
+ from inference_script import BacterialClassifier
41
+
42
+ classifier = BacterialClassifier(models_dir='models')
43
+ result = classifier.classify('bacteria.jpg')
44
+ print(result)
45
+
46
+ PACKAGE CONTENTS
47
+ ----------------
48
+ models/
49
+ - vgg16_feature_extractor.keras
50
+ - pca_model.pkl
51
+ - standard_scaler.pkl
52
+ - svm_classifier.pkl
53
+ - class_names.pkl
54
+ - bacteria_info.pkl
55
+
56
+ inference_script.py - Classifier code
57
+ test_model.py - Model verification
58
+ requirements.txt - Dependencies
59
+ README.txt - This file
60
+ model_metadata.json - Model specifications
61
+
62
+ Created: 2025-12-03 18:59:38
bacterial-classifier/api.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ from typing import List
6
+ from fastapi import FastAPI, UploadFile, File, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+
9
+ # Add the parent directory to sys.path to import bacsense_v2_package
10
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
11
+ from bacsense_v2_package.inference import BacSense
12
+
13
+ app = FastAPI(title="Bacsense 2.0 API")
14
+
15
+ # Setup CORS to allow requests from the React frontend
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"], # Adjust this in production, e.g., ["http://localhost:5173"]
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ # Load the model upon startup
25
+ # The model files are located in the bacsense_v2_package directory
26
+ model_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'bacsense_v2_package'))
27
+ classifier = BacSense(model_dir=model_dir)
28
+ classifier.warmup()
29
+
30
+ @app.post("/predict_batch")
31
+ async def predict_batch(files: List[UploadFile] = File(...)):
32
+ if not files or len(files) == 0:
33
+ raise HTTPException(status_code=400, detail="No files uploaded")
34
+
35
+ results = []
36
+
37
+ for file in files:
38
+ temp_path = None
39
+ try:
40
+ # Read the uploaded file into an IO stream
41
+ contents = await file.read()
42
+
43
+ # BacSense uses cv2.imread and PIL.Image.open with a file path, so we save it to disk temporarily
44
+ fd, temp_path = tempfile.mkstemp(suffix=".png")
45
+ with os.fdopen(fd, 'wb') as f:
46
+ f.write(contents)
47
+
48
+ # Process the image
49
+ result = classifier.predict(temp_path)
50
+
51
+ # Format probabilities for the frontend
52
+ # UI expects 0-100 for confidence
53
+ confidence_pct = result["confidence"] * 100 if result["confidence"] <= 1.0 else result["confidence"]
54
+
55
+ results.append({
56
+ "filename": file.filename,
57
+ "success": True,
58
+ "prediction": result['prediction'],
59
+ "confidence": confidence_pct,
60
+ "probabilities": [
61
+ {"name": result['prediction'], "probability": confidence_pct}
62
+ ],
63
+ "details": {
64
+ "gram_stain": result.get("gram", "Unknown"),
65
+ "shape": result.get("shape", "Unknown"),
66
+ "pathogenicity": result.get("risk", "Unknown")
67
+ }
68
+ })
69
+ except Exception as e:
70
+ results.append({
71
+ "filename": file.filename,
72
+ "success": False,
73
+ "error": str(e)
74
+ })
75
+ finally:
76
+ # Clean up the temporary file
77
+ if temp_path and os.path.exists(temp_path):
78
+ os.remove(temp_path)
79
+
80
+ return {"results": results}
bacterial-classifier/app.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from PIL import Image
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import pandas as pd
7
+
8
+ # Add the parent directory to sys.path to import bacsense_v2_package
9
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10
+ from bacsense_v2_package.inference import BacSense
11
+
12
+ # Precaution dictionary
13
+ PRECAUTIONS = {
14
+ "Escherichia coli": "Indicator of fecal contamination. \n\n**Precautions/Actions:** Boil water immediately before consumption. Source trace to find sewage leaks. Do not use for washing open wounds.",
15
+ "Pseudomonas aeruginosa": "Opportunistic pathogen resistant to many sanitizers. \n\n**Precautions/Actions:** Ensure water chlorination levels are adequate. Can cause severe infections in immunocompromised individuals. Avoid contact with eyes or ears.",
16
+ "Enterococcus faecalis": "Indicates prolonged fecal contamination. Very resilient. \n\n**Precautions/Actions:** Shock chlorinate the water system. Discontinue use for drinking until negative tests are returned.",
17
+ "Clostridium perfringens": "Spore-forming bacteria, highly resistant to standard disinfection. \n\n**Precautions/Actions:** Indicates remote or past fecal contamination. UV filtration or extreme heat treatment may be required.",
18
+ "Listeria monocytogenes": "Dangerous to pregnant women and immunocompromised individuals. \n\n**Precautions/Actions:** Do not use water for food preparation or drinking. Pasteurization/boiling is required."
19
+ }
20
+
21
+ # Set page config
22
+ st.set_page_config(
23
+ page_title="BacSense v2 Dashboard",
24
+ page_icon="🦠",
25
+ layout="wide"
26
+ )
27
+
28
+ # Initialize classifier
29
+ @st.cache_resource
30
+ def get_classifier():
31
+ model_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'bacsense_v2_package'))
32
+ model = BacSense(model_dir=model_dir)
33
+ model.warmup()
34
+ return model
35
+
36
+ try:
37
+ classifier = get_classifier()
38
+ except Exception as e:
39
+ st.error(f"Error loading model: {e}")
40
+ st.stop()
41
+
42
+ # Dialog function for detailed view
43
+ # Fallback for older Streamlit versions that lack st.dialog
44
+ def render_details(item):
45
+ st.image(item["Image"], use_column_width=True)
46
+ st.markdown(f"### Predicted: **{item['Predicted Class']}**")
47
+
48
+ colA, colB = st.columns(2)
49
+ colA.metric("Confidence", f"{item['Confidence (%)']}%")
50
+ colB.metric("Risk Level", item['Risk'])
51
+
52
+ st.markdown("""---""")
53
+ st.markdown("**Bacterial Summary:**")
54
+ st.write(f"- **Gram Stain:** {item['Gram Stain']}")
55
+ st.write(f"- **Shape:** {item['Shape']}")
56
+
57
+ if item['Routed to Specialist']:
58
+ st.info(f"Ambiguous morphology triggered the Specialist SVM. Accepted: {'✅' if item['Specialist Accepted'] else '❌'}")
59
+
60
+ st.markdown("""---""")
61
+ st.markdown("**Precautions:**")
62
+ precaution_text = PRECAUTIONS.get(item['Predicted Class'], "No specific precautions available. Standard water safety protocols suggest boiling before consumption.")
63
+ st.warning(precaution_text)
64
+
65
+ if st.button("Close Summary", key="close_summary_btn"):
66
+ st.session_state.selected_item = None
67
+ st.rerun()
68
+
69
+ # Main UI
70
+ st.title("🦠 BacSense v2 Analytics Dashboard")
71
+ st.markdown("""
72
+ Welcome to the BacSense v2 Dashboard. This cascaded hybrid classifier uses **VGG16 Transfer Learning**
73
+ combined with **Hand-Crafted Feature Engineering** and an **RBF-SVM Specialist** to disambiguate waterborne pathogens.
74
+ You can safely upload **up to 60 images** at once.
75
+ """)
76
+
77
+ # Sidebar for info
78
+ with st.sidebar:
79
+ st.header("Supported Species")
80
+ st.markdown("""
81
+ - *Clostridium perfringens*
82
+ - *Enterococcus faecalis*
83
+ - *Escherichia coli*
84
+ - *Listeria monocytogenes*
85
+ - *Pseudomonas aeruginosa*
86
+ """)
87
+ st.markdown("---")
88
+ st.caption("BacSense v2 Cascaded Model")
89
+ st.caption("Overall Accuracy: 95.65%")
90
+ st.caption("Specialist AUC: 0.9863")
91
+
92
+ st.subheader("Batch Upload (Multiple Images)")
93
+ uploaded_files = st.file_uploader("Upload microscopic bacterial images...", type=["jpg", "jpeg", "png"], accept_multiple_files=True)
94
+
95
+ if "selected_item" not in st.session_state:
96
+ st.session_state.selected_item = None
97
+
98
+ if uploaded_files:
99
+ # Check if we are viewing details
100
+ if st.session_state.selected_item is not None:
101
+ render_details(st.session_state.selected_item)
102
+ else:
103
+ # Filter to 60 images to prevent abuse if needed, or just process however many there are
104
+ uploaded_files = list(uploaded_files)[:100] # Safe upper limit
105
+ results = []
106
+
107
+ # Progress container
108
+ progress_container = st.container()
109
+ with progress_container:
110
+ st.write(f"Processing {len(uploaded_files)} images...")
111
+ progress_bar = st.progress(0)
112
+ status_text = st.empty()
113
+
114
+ for i, uploaded_file in enumerate(uploaded_files):
115
+ status_text.text(f"Analyzing [{i+1}/{len(uploaded_files)}]: {uploaded_file.name}...")
116
+
117
+ # Load image
118
+ image = Image.open(uploaded_file)
119
+
120
+ # Save temp file
121
+ fd, temp_path = tempfile.mkstemp(suffix=".png")
122
+ if image.mode != 'RGB':
123
+ image = image.convert('RGB')
124
+
125
+ with os.fdopen(fd, 'wb') as f:
126
+ image.save(f, format="PNG")
127
+
128
+ try:
129
+ # Classify using BacSense cascaded model
130
+ prediction = classifier.predict(temp_path)
131
+
132
+ confidence_pct = prediction['confidence'] * 100 if prediction['confidence'] <= 1.0 else prediction['confidence']
133
+ results.append({
134
+ "Filename": uploaded_file.name,
135
+ "Predicted Class": prediction['prediction'],
136
+ "Confidence (%)": round(confidence_pct, 2),
137
+ "Gram Stain": prediction.get('gram', 'Unknown'),
138
+ "Shape": prediction.get('shape', 'Unknown'),
139
+ "Risk": prediction.get('risk', 'Unknown'),
140
+ "Routed to Specialist": prediction.get('routed_to_specialist', False),
141
+ "Specialist Accepted": prediction.get('specialist_accepted', False),
142
+ "Image": image
143
+ })
144
+
145
+ except Exception as e:
146
+ st.error(f"Error processing {uploaded_file.name}: {e}")
147
+ finally:
148
+ # Cleanup
149
+ if os.path.exists(temp_path):
150
+ os.remove(temp_path)
151
+
152
+ # Update progress
153
+ progress_bar.progress((i + 1) / len(uploaded_files))
154
+
155
+ status_text.text("Batch Processing Complete!")
156
+
157
+ if results:
158
+ df = pd.DataFrame(results)
159
+
160
+ # 1. Top Level Metrics
161
+ st.markdown("---")
162
+ st.subheader("📊 Batch Analytics Summary")
163
+ col1, col2, col3, col4 = st.columns(4)
164
+
165
+ total_images = len(results)
166
+ high_risk = len(df[df["Risk"] == "High"])
167
+ routed_spec = len(df[df["Routed to Specialist"] == True])
168
+ avg_confidence = df["Confidence (%)"].mean()
169
+
170
+ col1.metric("Total Images", total_images)
171
+ col2.metric("High Target Risk", high_risk)
172
+ col3.metric("Routed to Specialist", routed_spec, help="Ambiguous cases handled by the 683-dim Specialist SVM")
173
+ col4.metric("Avg Confidence", f"{avg_confidence:.1f}%")
174
+
175
+ # 2. Charts
176
+ st.markdown("<br>", unsafe_allow_html=True)
177
+ col_chart1, col_chart2 = st.columns(2)
178
+ with col_chart1:
179
+ st.markdown("**Species Distribution**")
180
+ class_counts = df["Predicted Class"].value_counts().reset_index()
181
+ class_counts.columns = ["Species", "Count"]
182
+ st.bar_chart(class_counts.set_index("Species"))
183
+
184
+ with col_chart2:
185
+ st.markdown("**Gram Stain Breakdown**")
186
+ gram_counts = df["Gram Stain"].value_counts()
187
+ st.bar_chart(gram_counts)
188
+
189
+ # 3. Data Table
190
+ st.markdown("---")
191
+ st.subheader("📋 Detailed Results Table")
192
+ st.dataframe(df.drop(columns=["Image"]), use_container_width=True)
193
+
194
+ # 4. Filterable Image Gallery
195
+ st.markdown("---")
196
+ st.subheader("🖼️ Processed Image Gallery")
197
+ st.caption("Click on 'View Summary' underneath any image to view brief details and precautions for the detected pathogen.")
198
+
199
+ filter_class = st.selectbox("Filter gallery by predicted species:", ["All"] + sorted(df["Predicted Class"].unique().tolist()))
200
+
201
+ filtered_results = results if filter_class == "All" else [r for r in results if r["Predicted Class"] == filter_class]
202
+
203
+ # Display images in a grid
204
+ cols_per_row = 4
205
+ for i in range(0, len(filtered_results), cols_per_row):
206
+ cols = st.columns(cols_per_row)
207
+ for j in range(cols_per_row):
208
+ if i + j < len(filtered_results):
209
+ item = filtered_results[i + j]
210
+ with cols[j]:
211
+ st.image(item["Image"], use_column_width=True)
212
+ st.markdown(f"**{item['Predicted Class']}**")
213
+
214
+ # Add pills for details
215
+ det_col1, det_col2 = st.columns(2)
216
+ det_col1.markdown(f"<small>{item['Confidence (%)']}% Conf</small>", unsafe_allow_html=True)
217
+ if item["Routed to Specialist"]:
218
+ det_col2.markdown(f"<small>Specialist: {'✅' if item['Specialist Accepted'] else '❌'}</small>", unsafe_allow_html=True)
219
+
220
+ if st.button("View Summary", key=f"details_btn_{i}_{j}"):
221
+ st.session_state.selected_item = item
222
+ st.rerun()
223
+
224
+ else:
225
+ st.info("Please upload one or more images (up to 60) to start the batch analysis.")
226
+
bacterial-classifier/requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ tensorflow>=2.10.0
2
+ scikit-learn>=1.0.0
3
+ numpy>=1.21.0
4
+ pillow>=9.0.0
5
+ scipy>=1.7.0
6
+ streamlit>=1.10.0
7
+ pandas>=1.3.0
bacterial-classifier/run_backend.ps1 ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .\venv\Scripts\Activate.ps1
2
+ pip install -r ..\bacsense_v2_package\requirements.txt
3
+ pip install fastapi uvicorn python-multipart
4
+ python -m uvicorn api:app --reload --port 5000
frontend/.gitignore ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
25
+ .vercel
frontend/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
frontend/eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
frontend/index.html ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html class="dark" lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
6
+ <title>Bacsense 2.0 - Bacterial Classification System</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&amp;family=Poppins:wght@300;400;500;600;700&amp;display=swap" rel="stylesheet"/>
9
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
10
+ <script id="tailwind-config">
11
+ tailwind.config = {
12
+ darkMode: "class",
13
+ theme: {
14
+ extend: {
15
+ colors: {
16
+ "primary": "#13a4ec",
17
+ "background-light": "#f0f8ff",
18
+ "background-dark": "#001233",
19
+ },
20
+ fontFamily: {
21
+ "display": ["Poppins", "sans-serif"],
22
+ "sans": ["Poppins", "sans-serif"]
23
+ },
24
+ borderRadius: {
25
+ "DEFAULT": "0.25rem",
26
+ "lg": "0.5rem",
27
+ "xl": "0.75rem",
28
+ "full": "9999px"
29
+ },
30
+ animation: {
31
+ meteor: "meteor 5s linear infinite",
32
+ },
33
+ keyframes: {
34
+ meteor: {
35
+ "0%": { transform: "rotate(215deg) translateX(0)", opacity: 1 },
36
+ "70%": { opacity: 1 },
37
+ "100%": {
38
+ transform: "rotate(215deg) translateX(-500px)",
39
+ opacity: 0,
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ }
46
+ </script>
47
+ <style>
48
+ .glass {
49
+ background: rgba(255, 255, 255, 0.03);
50
+ backdrop-filter: blur(12px);
51
+ -webkit-backdrop-filter: blur(12px);
52
+ border: 1px solid rgba(255, 255, 255, 0.1);
53
+ }
54
+ .glow-primary {
55
+ box-shadow: 0 0 20px rgba(19, 164, 236, 0.3);
56
+ }
57
+ </style>
58
+ </head>
59
+ <body class="bg-background-light dark:bg-background-dark text-slate-900 dark:text-slate-100 font-sans selection:bg-primary/30">
60
+ <div id="root"></div>
61
+ <script type="module" src="/src/main.tsx"></script>
62
+ </body>
63
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "clsx": "^2.1.1",
14
+ "framer-motion": "^12.36.0",
15
+ "gsap": "^3.14.2",
16
+ "react": "^19.2.4",
17
+ "react-dom": "^19.2.4",
18
+ "react-router-dom": "^7.13.2",
19
+ "tailwind-merge": "^3.5.0"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.39.4",
23
+ "@types/node": "^24.12.0",
24
+ "@types/react": "^19.2.14",
25
+ "@types/react-dom": "^19.2.3",
26
+ "@vitejs/plugin-react": "^6.0.0",
27
+ "eslint": "^9.39.4",
28
+ "eslint-plugin-react-hooks": "^7.0.1",
29
+ "eslint-plugin-react-refresh": "^0.5.2",
30
+ "globals": "^17.4.0",
31
+ "typescript": "~5.9.3",
32
+ "typescript-eslint": "^8.56.1",
33
+ "vite": "^8.0.0"
34
+ }
35
+ }
frontend/public/bacteria-bg.png ADDED

Git LFS Details

  • SHA256: d8323048e9557a945b87de43b59ae272f84fd7fd551b21c3dae77847b6152892
  • Pointer size: 131 Bytes
  • Size of remote file: 657 kB
frontend/public/bacteria-light-bg.png ADDED

Git LFS Details

  • SHA256: 8e8f1f04769a1b902d71f016011dffb98c686fcd8d1bca9df3a380ab54e6f067
  • Pointer size: 131 Bytes
  • Size of remote file: 675 kB
frontend/public/favicon.svg ADDED
frontend/public/galaxy-bg.png ADDED

Git LFS Details

  • SHA256: 2d51d2500804d914dc0e8ac723eb6ef335935085b7dd29ea849619218b15be33
  • Pointer size: 131 Bytes
  • Size of remote file: 537 kB
frontend/public/icons.svg ADDED
frontend/run_frontend.ps1 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ npm install
2
+ npm run dev
frontend/src/App.css ADDED
@@ -0,0 +1 @@
 
 
1
+ /* Empty to avoid conflict */
frontend/src/App.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Routes, Route } from "react-router-dom"
2
+ import { Header } from "./components/Header"
3
+ import { Footer } from "./components/Footer"
4
+ import TargetCursor from "./components/TargetCursor"
5
+ import { HomePage } from "./pages/HomePage"
6
+ import { DocsPage } from "./pages/DocsPage"
7
+
8
+ function App() {
9
+ return (
10
+ <>
11
+ <TargetCursor
12
+ spinDuration={4}
13
+ hideDefaultCursor
14
+ parallaxOn
15
+ hoverDuration={0.2}
16
+ />
17
+ <Header />
18
+ <Routes>
19
+ <Route path="/" element={<HomePage />} />
20
+ <Route path="/docs" element={<DocsPage />} />
21
+ </Routes>
22
+ <Footer />
23
+ </>
24
+ )
25
+ }
26
+
27
+ export default App
frontend/src/assets/hero.png ADDED

Git LFS Details

  • SHA256: 72a860570eddf1dd9988f26c7106c67be286bc9f2fd3303c465ce87edb1ae6cd
  • Pointer size: 130 Bytes
  • Size of remote file: 44.9 kB
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/Footer.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const Footer = () => {
2
+ return (
3
+ <footer className="bg-slate-100 dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 py-16">
4
+ <div className="max-w-7xl mx-auto px-6">
5
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
6
+ <div className="col-span-1 md:col-span-2 space-y-6">
7
+ <div className="flex items-center gap-3">
8
+ <span className="material-symbols-outlined text-primary text-2xl">coronavirus</span>
9
+ <h1 className="font-display font-bold text-xl tracking-tight">Bacsense 2.0</h1>
10
+ </div>
11
+ <p className="text-sm text-slate-500 max-w-sm">
12
+ An open-access platform for clinical microbiology research. Our mission is to accelerate pathogen identification through advanced hybrid neural networks and machine learning.
13
+ </p>
14
+ </div>
15
+ <div className="space-y-4">
16
+ <h6 className="font-bold text-sm">Resources</h6>
17
+ <ul className="text-sm text-slate-500 space-y-2">
18
+ <li><a className="hover:text-primary transition-colors" href="#">API Documentation</a></li>
19
+ <li><a className="hover:text-primary transition-colors" href="#">VGG16 Model Papers</a></li>
20
+ <li><a className="hover:text-primary transition-colors" href="#">Dataset Disclosure</a></li>
21
+ <li><a className="hover:text-primary transition-colors" href="#">Benchmarking</a></li>
22
+ </ul>
23
+ </div>
24
+ <div className="space-y-4">
25
+ <h6 className="font-bold text-sm">Institutional</h6>
26
+ <ul className="text-sm text-slate-500 space-y-2">
27
+ <li><a className="hover:text-primary transition-colors" href="#">Research Partners</a></li>
28
+ <li><a className="hover:text-primary transition-colors" href="#">Grant Information</a></li>
29
+ <li><a className="hover:text-primary transition-colors" href="#">Legal &amp; Ethics</a></li>
30
+ <li><a className="hover:text-primary transition-colors" href="#">Contact Lab</a></li>
31
+ </ul>
32
+ </div>
33
+ </div>
34
+ <div className="pt-8 border-t border-slate-200 dark:border-slate-800 flex flex-col md:flex-row items-center justify-between gap-4">
35
+ <p className="text-xs text-slate-500">© 2026 Bacsense Scientific Systems. All rights reserved.</p>
36
+ <div className="flex items-center gap-6">
37
+ <a className="text-slate-400 hover:text-primary transition-colors" href="#"><span className="material-symbols-outlined">hub</span></a>
38
+ <a className="text-slate-400 hover:text-primary transition-colors" href="#"><span className="material-symbols-outlined">science</span></a>
39
+ <a className="text-slate-400 hover:text-primary transition-colors" href="#"><span className="material-symbols-outlined">school</span></a>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </footer>
44
+ );
45
+ };
frontend/src/components/Header.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Link, useLocation } from "react-router-dom";
2
+
3
+ export const Header = () => {
4
+ const location = useLocation();
5
+
6
+ return (
7
+ <header className="fixed top-0 left-0 right-0 z-50 bg-transparent backdrop-blur-md border-b border-primary/10">
8
+ <div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
9
+ <Link to="/" className="flex items-center gap-3">
10
+ <span className="material-symbols-outlined text-primary text-3xl">coronavirus</span>
11
+ <h1 className="font-display font-bold text-2xl tracking-tight">Bacsense 2.0</h1>
12
+ </Link>
13
+ <nav className="hidden md:flex items-center gap-10">
14
+ <Link className={`cursor-target text-sm font-medium transition-colors ${location.pathname === '/' ? 'text-primary' : 'hover:text-primary text-slate-500 dark:text-slate-400'}`} to="/">Home</Link>
15
+ {location.pathname === '/' && (
16
+ <>
17
+ <a className="cursor-target text-sm font-medium hover:text-primary transition-colors text-slate-500 dark:text-slate-400" href="#species">Supported Species</a>
18
+ <a className="cursor-target text-sm font-medium hover:text-primary transition-colors text-slate-500 dark:text-slate-400" href="#upload">Upload</a>
19
+ </>
20
+ )}
21
+ <Link className={`cursor-target text-sm font-medium transition-colors ${location.pathname === '/docs' ? 'text-primary' : 'hover:text-primary text-slate-500 dark:text-slate-400'}`} to="/docs">Documentation</Link>
22
+ </nav>
23
+ <div className="flex items-center gap-4">
24
+ <Link to="/" className="cursor-target bg-primary text-white px-5 py-2 rounded-lg font-semibold text-sm hover:opacity-90 transition-all glow-primary">
25
+ Get Started
26
+ </Link>
27
+ </div>
28
+ </div>
29
+ </header>
30
+ );
31
+ };
frontend/src/components/Hero.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Meteors } from "../registry/magicui/meteors";
2
+ import { DotPattern } from "../registry/magicui/dot-pattern";
3
+ import { cn } from "../lib/utils";
4
+
5
+ export const Hero = () => {
6
+ return (
7
+ <section className="relative overflow-hidden mb-24 pt-32 pb-32">
8
+ {/* Bacteria/Molecular Background */}
9
+ <div
10
+ className="absolute inset-0 z-0 opacity-80"
11
+ style={{
12
+ backgroundImage: "url('/bacteria-bg.png')",
13
+ backgroundSize: "cover",
14
+ backgroundPosition: "center",
15
+ backgroundRepeat: "no-repeat",
16
+ }}
17
+ />
18
+
19
+ {/* Dark gradient overlay for readability */}
20
+ <div className="absolute inset-0 z-0 bg-background-light/70 dark:bg-background-dark/80 bg-gradient-to-b from-transparent to-background-light dark:to-background-dark" />
21
+
22
+ {/* MagicUI Dot Pattern */}
23
+ <DotPattern
24
+ className={cn(
25
+ "[mask-image:radial-gradient(600px_circle_at_center,white,transparent)] z-0 mix-blend-overlay dark:mix-blend-plus-lighter opacity-60",
26
+ )}
27
+ />
28
+
29
+ {/* MagicUI Meteors */}
30
+ <div className="absolute inset-0 z-0 overflow-hidden">
31
+ <Meteors number={30} />
32
+ </div>
33
+
34
+ <div className="max-w-7xl mx-auto px-6 flex flex-col items-center text-center space-y-8 relative z-10">
35
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-widest">
36
+ <span className="relative flex h-2 w-2">
37
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
38
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
39
+ </span>
40
+ Now with VGG16 + SVM Hybrid Architecture
41
+ </div>
42
+ <h2 className="text-6xl md:text-7xl font-display font-bold max-w-4xl leading-[1.1]">
43
+ Bacterial <span className="text-primary">Classification</span> System
44
+ </h2>
45
+ <p className="text-lg text-slate-500 dark:text-slate-400 max-w-2xl leading-relaxed">
46
+ Advanced medical AI for high-accuracy identification of microbial
47
+ species. Leveraging deep learning features and support vector machines
48
+ for clinical-grade scientific research.
49
+ </p>
50
+ <div className="flex flex-wrap justify-center items-center gap-4 pt-4">
51
+ <a
52
+ href="#upload"
53
+ className="cursor-target bg-primary text-white px-8 py-4 rounded-xl font-bold text-lg hover:scale-[1.02] transition-transform glow-primary flex items-center gap-2"
54
+ >
55
+ <span className="material-symbols-outlined">upload_file</span>
56
+ Upload Image
57
+ </a>
58
+ <button className="cursor-target bg-slate-200 dark:bg-slate-800 px-8 py-4 text-slate-900 dark:text-white rounded-xl font-bold text-lg hover:bg-slate-300 dark:hover:bg-slate-700 transition-colors">
59
+ Documentation
60
+ </button>
61
+ </div>
62
+ </div>
63
+ </section>
64
+ );
65
+ };
frontend/src/components/Results.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { PredictionResult } from "./UploadZone";
2
+
3
+ interface ResultsProps {
4
+ items: PredictionResult[];
5
+ }
6
+
7
+ export const Results = ({ items }: ResultsProps) => {
8
+ return (
9
+ <section className="max-w-7xl mx-auto px-6 mb-24">
10
+ <h3 className="text-3xl font-display font-bold mb-8">Classification Results</h3>
11
+ <div className="glass rounded-3xl overflow-x-auto border border-slate-200 dark:border-slate-800 shadow-2xl">
12
+ <table className="w-full text-left border-collapse min-w-[800px]">
13
+ <thead className="bg-slate-50 dark:bg-slate-800/80 border-b border-slate-200 dark:border-slate-700">
14
+ <tr>
15
+ <th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500">Filename</th>
16
+ <th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500">Predicted Class</th>
17
+ <th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500">Confidence</th>
18
+ <th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500">Gram Stain</th>
19
+ <th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500">Shape</th>
20
+ <th className="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500">Pathogenicity</th>
21
+ </tr>
22
+ </thead>
23
+ <tbody className="divide-y divide-slate-200 dark:divide-slate-800 text-slate-900 dark:text-slate-100">
24
+ {items.map((item, index) => {
25
+ if (!item.success) return null;
26
+ const risk = item.details?.pathogenicity || 'Unknown';
27
+ const confColor = (item.confidence || 0) >= 80 ? 'bg-green-500/10 text-green-500' : 'bg-yellow-500/10 text-yellow-500';
28
+
29
+ let riskColor = 'bg-blue-500/10 text-blue-500'; // low/unknown
30
+ if (risk.toLowerCase().includes('high')) riskColor = 'bg-red-500/10 text-red-500';
31
+ else if (risk.toLowerCase().includes('moderate')) riskColor = 'bg-orange-500/10 text-orange-500';
32
+
33
+ return (
34
+ <tr key={index} className="hover:bg-primary/5 transition-colors">
35
+ <td className="px-6 py-4 text-sm font-medium">{item.filename}</td>
36
+ <td className="px-6 py-4 text-sm italic">{item.prediction}</td>
37
+ <td className="px-6 py-4">
38
+ <span className={`${confColor} text-[11px] font-bold px-2 py-0.5 rounded`}>{(item.confidence || 0).toFixed(1)}%</span>
39
+ </td>
40
+ <td className="px-6 py-4 text-sm">{item.details?.gram_stain}</td>
41
+ <td className="px-6 py-4 text-sm text-slate-500 dark:text-slate-400">{item.details?.shape}</td>
42
+ <td className="px-6 py-4">
43
+ <span className={`${riskColor} text-[11px] font-bold px-2 py-0.5 rounded uppercase`}>{risk}</span>
44
+ </td>
45
+ </tr>
46
+ );
47
+ })}
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ </section>
52
+ );
53
+ };
54
+
frontend/src/components/Species.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const Species = () => {
2
+ const speciesList = [
3
+ { name: "C. perfringens", full: "Clostridium perfringens", traits: "Gram-positive, Anaerobic", img: "https://lh3.googleusercontent.com/aida-public/AB6AXuBqX5as8VB98coMPS1nNFsfu9M233xaiSFzXZJnG-iqFTWvjxRyPasTfINhM5Yi8GcCSLRaqgraUgLDc1VbIo_PF3Y3WOoj79rMRmZzKnmtEQiD3imFaD5xrI9wQRiIcXjS3J96bSpaEQYjt8ZDTLXZ96K7tjwU6JXqZ74Nf8ETJ_T-oxvqdyS-3wxc06Mbrvc5QwkEkuY4UOPUJs25mhCQf8QbL2cpG6pC6A2Pg0fpdGAkSe1sQVqLhZJcBpLV8A8z-V2SsgjrCmo" },
4
+ { name: "Escherichia coli", full: "Escherichia coli", traits: "Gram-negative, Rod", img: "https://lh3.googleusercontent.com/aida-public/AB6AXuD3KwFAUK3s5aDqMCX-TqfHdtQlznX0kk5aU6dfWM7b_ZXqjfbvJ2_2NYG4WAcOnkmXzSEAWmKBQDxKqDwltJNttqllKLNDA2AVNsSH9V-BhZV46l9TQuCjurGru4cUKRTiPjUV_yuAx83WD-TwWSec751FfZA0en73GZU8-3u9bGm0YMSel0Tafi2EjicYvKgewBtwPCFfyZqVjLZCgm-fFbinF__m9zMogoIgENOqZIId2rukRZnrZYefyWZv7cbg3HzN9OOXmQ8" },
5
+ { name: "S. aureus", full: "Staphylococcus aureus", traits: "Gram-positive, Cocci", img: "https://lh3.googleusercontent.com/aida-public/AB6AXuBMYEJJkt17J03_UiNaRU-EPoYelpNlqQ4AtoiWoLPy05oNKBToqZlsFxc0SpA8jCkYQqGg8kAQ1FQFILvsv527XzITyUHScVOE-eTotf0wF4YcFykbRR4kvyceOsGVO3bcHvrQ5apT4CpDV7_jXvy88dSqB4sjYxWrTJCTzrmcMsPg0l8jrTOsmgjRbAI8wJ_TfEhNGLL72YNt6-GQK2mXhBGgT3ba1md93NICpB-rCzppXQWFP4pyud_Sx5DFUHkSmsAQzxT_3Gc" },
6
+ { name: "Bacillus cereus", full: "Bacillus cereus", traits: "Gram-positive, Spore", img: "https://lh3.googleusercontent.com/aida-public/AB6AXuCL1k9a185JqakiYjo-2VJ9mw6OOefPvUoZgM2eTmKwJKMIkVlzcuxbS3rFyEEyeh8fO_N1VV0NC1bqH-e4ilRnPvzxhMZmdHcDl7xlrGF5zcZ8Fn2lUqyEUtdvPaUh4PoFMUCunABFPrO7EiviLb-hhMKKr9Qr-5RBQGWa8wl4G1_NusRvel3BwSNv1dmCdJtabnpfLPf36jzvxtQDO3qMh2_TeIDh6LtCCvHOhiD--cNzhp1QSqIannGMyo4b348Mw2HWCmuYb98" },
7
+ { name: "L. monocytogenes", full: "Listeria monocytogenes", traits: "Gram-positive, Rod", img: "https://lh3.googleusercontent.com/aida-public/AB6AXuAMnV4nByP765X_gmBbkyiNF3oqf4kYgD3vsXPdkUAwM8VZb-cTvkGo93H2hraUtpUgsA1Om24vR2gwZDNJUIZ8FdFm2R3YoHUS15My0y8J26dMerah7GqR0w5NhrwDABjT7hKwEfJjdKpmZlmfe3K8YByQrUFJCMSqWCndfFkpIyfkwd89RINHMI2-5odgkZlCRSLeDr-prVaTJmhZK-eSmWrBodqF2lJyyfHneSJk_nqkcDpr94E1rS2DVIFqH5UpagbfbOGPeGY" }
8
+ ];
9
+
10
+ return (
11
+ <section className="max-w-7xl mx-auto px-6 mb-24" id="species">
12
+ <div className="flex items-end justify-between mb-10">
13
+ <div>
14
+ <h3 className="text-3xl font-display font-bold mb-2">Supported Species</h3>
15
+ <p className="text-slate-500 dark:text-slate-400">Our model is optimized for the most common pathogenic bacteria.</p>
16
+ </div>
17
+ <a className="cursor-target text-primary font-semibold flex items-center gap-1 hover:underline cursor-pointer">
18
+ View all 50+ species <span className="material-symbols-outlined text-sm">arrow_forward</span>
19
+ </a>
20
+ </div>
21
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-x-6 gap-y-12">
22
+ {speciesList.map((species, i) => (
23
+ <div key={i} className="group flex flex-col items-center text-center">
24
+ <div className="aspect-square w-full max-w-[140px] md:max-w-[160px] rounded-full overflow-hidden mb-4 relative border-4 border-slate-100 dark:border-slate-800/80 group-hover:border-primary/50 group-hover:shadow-[0_0_30px_rgba(19,164,236,0.4)] transition-all duration-300">
25
+ <img className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700" alt={`Microscopic view of ${species.full}`} src={species.img} />
26
+ {/* Inner ring to give petri-dish feel */}
27
+ <div className="absolute inset-0 rounded-full border border-white/10 dark:border-white/5 pointer-events-none" />
28
+ </div>
29
+ <h4 className="font-bold text-sm mb-1 italic text-slate-900 dark:text-slate-100">{species.name}</h4>
30
+ <p className="text-xs text-slate-500 leading-tight px-2">{species.traits}</p>
31
+ </div>
32
+ ))}
33
+ </div>
34
+ </section>
35
+ );
36
+ };
frontend/src/components/TargetCursor.tsx ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useCallback, useMemo } from 'react';
2
+ import { gsap } from 'gsap';
3
+
4
+ export interface TargetCursorProps {
5
+ targetSelector?: string;
6
+ spinDuration?: number;
7
+ hideDefaultCursor?: boolean;
8
+ hoverDuration?: number;
9
+ parallaxOn?: boolean;
10
+ }
11
+
12
+ const TargetCursor: React.FC<TargetCursorProps> = ({
13
+ targetSelector = '.cursor-target',
14
+ spinDuration = 2,
15
+ hideDefaultCursor = true,
16
+ hoverDuration = 0.2,
17
+ parallaxOn = true
18
+ }) => {
19
+ const cursorRef = useRef<HTMLDivElement>(null);
20
+ const cornersRef = useRef<NodeListOf<HTMLDivElement> | null>(null);
21
+ const spinTl = useRef<gsap.core.Timeline | null>(null);
22
+ const dotRef = useRef<HTMLDivElement>(null);
23
+
24
+ const isActiveRef = useRef(false);
25
+ const targetCornerPositionsRef = useRef<{ x: number; y: number }[] | null>(null);
26
+ const tickerFnRef = useRef<(() => void) | null>(null);
27
+ const activeStrengthRef = useRef({ current: 0 });
28
+
29
+ const isMobile = useMemo(() => {
30
+ const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
31
+ const isSmallScreen = window.innerWidth <= 768;
32
+ const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
33
+ const mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i;
34
+ const isMobileUserAgent = mobileRegex.test(userAgent.toLowerCase());
35
+ return (hasTouchScreen && isSmallScreen) || isMobileUserAgent;
36
+ }, []);
37
+
38
+ const constants = useMemo(() => ({ borderWidth: 3, cornerSize: 12 }), []);
39
+
40
+ const moveCursor = useCallback((x: number, y: number) => {
41
+ if (!cursorRef.current) return;
42
+ gsap.to(cursorRef.current, { x, y, duration: 0.1, ease: 'power3.out' });
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ if (isMobile || !cursorRef.current) return;
47
+
48
+ const originalCursor = document.body.style.cursor;
49
+ if (hideDefaultCursor) {
50
+ document.body.style.cursor = 'none';
51
+ const style = document.createElement('style');
52
+ style.id = 'hide-default-cursor-style';
53
+ style.innerHTML = `
54
+ * { cursor: none !important; }
55
+ `;
56
+ document.head.appendChild(style);
57
+ }
58
+
59
+ const cursor = cursorRef.current;
60
+ cornersRef.current = cursor.querySelectorAll<HTMLDivElement>('.target-cursor-corner');
61
+
62
+ let activeTarget: Element | null = null;
63
+ let currentLeaveHandler: (() => void) | null = null;
64
+ let resumeTimeout: ReturnType<typeof setTimeout> | null = null;
65
+
66
+ const cleanupTarget = (target: Element) => {
67
+ if (currentLeaveHandler) {
68
+ target.removeEventListener('mouseleave', currentLeaveHandler);
69
+ }
70
+ currentLeaveHandler = null;
71
+ };
72
+
73
+ gsap.set(cursor, {
74
+ xPercent: -50,
75
+ yPercent: -50,
76
+ x: window.innerWidth / 2,
77
+ y: window.innerHeight / 2
78
+ });
79
+
80
+ const createSpinTimeline = () => {
81
+ if (spinTl.current) {
82
+ spinTl.current.kill();
83
+ }
84
+ spinTl.current = gsap
85
+ .timeline({ repeat: -1 })
86
+ .to(cursor, { rotation: '+=360', duration: spinDuration, ease: 'none' });
87
+ };
88
+
89
+ createSpinTimeline();
90
+
91
+ const tickerFn = () => {
92
+ if (!targetCornerPositionsRef.current || !cursorRef.current || !cornersRef.current) {
93
+ return;
94
+ }
95
+ const strength = activeStrengthRef.current.current;
96
+ if (strength === 0) return;
97
+ const cursorX = gsap.getProperty(cursorRef.current, 'x') as number;
98
+ const cursorY = gsap.getProperty(cursorRef.current, 'y') as number;
99
+ const corners = Array.from(cornersRef.current);
100
+ corners.forEach((corner, i) => {
101
+ const currentX = gsap.getProperty(corner, 'x') as number;
102
+ const currentY = gsap.getProperty(corner, 'y') as number;
103
+ const targetX = targetCornerPositionsRef.current![i].x - cursorX;
104
+ const targetY = targetCornerPositionsRef.current![i].y - cursorY;
105
+ const finalX = currentX + (targetX - currentX) * strength;
106
+ const finalY = currentY + (targetY - currentY) * strength;
107
+ const duration = strength >= 0.99 ? (parallaxOn ? 0.2 : 0) : 0.05;
108
+ gsap.to(corner, {
109
+ x: finalX,
110
+ y: finalY,
111
+ duration: duration,
112
+ ease: duration === 0 ? 'none' : 'power1.out',
113
+ overwrite: 'auto'
114
+ });
115
+ });
116
+ };
117
+
118
+ tickerFnRef.current = tickerFn;
119
+
120
+ const moveHandler = (e: MouseEvent) => moveCursor(e.clientX, e.clientY);
121
+ window.addEventListener('mousemove', moveHandler);
122
+
123
+ const scrollHandler = () => {
124
+ if (!activeTarget || !cursorRef.current) return;
125
+ const mouseX = gsap.getProperty(cursorRef.current, 'x') as number;
126
+ const mouseY = gsap.getProperty(cursorRef.current, 'y') as number;
127
+ const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
128
+ const isStillOverTarget =
129
+ elementUnderMouse &&
130
+ (elementUnderMouse === activeTarget || elementUnderMouse.closest(targetSelector) === activeTarget);
131
+ if (!isStillOverTarget) {
132
+ currentLeaveHandler?.();
133
+ }
134
+ };
135
+ window.addEventListener('scroll', scrollHandler, { passive: true });
136
+
137
+ const mouseDownHandler = () => {
138
+ if (!dotRef.current) return;
139
+ gsap.to(dotRef.current, { scale: 0.7, duration: 0.3 });
140
+ gsap.to(cursorRef.current, { scale: 0.9, duration: 0.2 });
141
+ };
142
+
143
+ const mouseUpHandler = () => {
144
+ if (!dotRef.current) return;
145
+ gsap.to(dotRef.current, { scale: 1, duration: 0.3 });
146
+ gsap.to(cursorRef.current, { scale: 1, duration: 0.2 });
147
+ };
148
+
149
+ window.addEventListener('mousedown', mouseDownHandler);
150
+ window.addEventListener('mouseup', mouseUpHandler);
151
+
152
+ const enterHandler = (e: MouseEvent) => {
153
+ const directTarget = e.target as Element;
154
+ const allTargets: Element[] = [];
155
+ let current: Element | null = directTarget;
156
+ while (current && current !== document.body) {
157
+ if (current.matches(targetSelector)) {
158
+ allTargets.push(current);
159
+ }
160
+ current = current.parentElement;
161
+ }
162
+ const target = allTargets[0] || null;
163
+ if (!target || !cursorRef.current || !cornersRef.current) return;
164
+ if (activeTarget === target) return;
165
+ if (activeTarget) {
166
+ cleanupTarget(activeTarget);
167
+ }
168
+ if (resumeTimeout) {
169
+ clearTimeout(resumeTimeout);
170
+ resumeTimeout = null;
171
+ }
172
+
173
+ activeTarget = target;
174
+ const corners = Array.from(cornersRef.current);
175
+ corners.forEach(corner => gsap.killTweensOf(corner));
176
+ gsap.killTweensOf(cursorRef.current, 'rotation');
177
+ spinTl.current?.pause();
178
+ gsap.set(cursorRef.current, { rotation: 0 });
179
+
180
+ const rect = target.getBoundingClientRect();
181
+ const { borderWidth, cornerSize } = constants;
182
+ const cursorX = gsap.getProperty(cursorRef.current, 'x') as number;
183
+ const cursorY = gsap.getProperty(cursorRef.current, 'y') as number;
184
+
185
+ targetCornerPositionsRef.current = [
186
+ { x: rect.left - borderWidth, y: rect.top - borderWidth },
187
+ { x: rect.right + borderWidth - cornerSize, y: rect.top - borderWidth },
188
+ { x: rect.right + borderWidth - cornerSize, y: rect.bottom + borderWidth - cornerSize },
189
+ { x: rect.left - borderWidth, y: rect.bottom + borderWidth - cornerSize }
190
+ ];
191
+
192
+ isActiveRef.current = true;
193
+ gsap.ticker.add(tickerFnRef.current!);
194
+
195
+ gsap.to(activeStrengthRef.current, { current: 1, duration: hoverDuration, ease: 'power2.out' });
196
+
197
+ corners.forEach((corner, i) => {
198
+ gsap.to(corner, {
199
+ x: targetCornerPositionsRef.current![i].x - cursorX,
200
+ y: targetCornerPositionsRef.current![i].y - cursorY,
201
+ duration: 0.2,
202
+ ease: 'power2.out'
203
+ });
204
+ });
205
+
206
+ const leaveHandler = () => {
207
+ gsap.ticker.remove(tickerFnRef.current!);
208
+ isActiveRef.current = false;
209
+ targetCornerPositionsRef.current = null;
210
+ gsap.set(activeStrengthRef.current, { current: 0, overwrite: true });
211
+ activeTarget = null;
212
+ if (cornersRef.current) {
213
+ const corners = Array.from(cornersRef.current);
214
+ gsap.killTweensOf(corners);
215
+ const { cornerSize } = constants;
216
+ const positions = [
217
+ { x: -cornerSize * 1.5, y: -cornerSize * 1.5 },
218
+ { x: cornerSize * 0.5, y: -cornerSize * 1.5 },
219
+ { x: cornerSize * 0.5, y: cornerSize * 0.5 },
220
+ { x: -cornerSize * 1.5, y: cornerSize * 0.5 }
221
+ ];
222
+ const tl = gsap.timeline();
223
+ corners.forEach((corner, index) => {
224
+ tl.to(corner, { x: positions[index].x, y: positions[index].y, duration: 0.3, ease: 'power3.out' }, 0);
225
+ });
226
+ }
227
+ resumeTimeout = setTimeout(() => {
228
+ if (!activeTarget && cursorRef.current && spinTl.current) {
229
+ const currentRotation = gsap.getProperty(cursorRef.current, 'rotation') as number;
230
+ const normalizedRotation = currentRotation % 360;
231
+ spinTl.current.kill();
232
+ spinTl.current = gsap
233
+ .timeline({ repeat: -1 })
234
+ .to(cursorRef.current, { rotation: '+=360', duration: spinDuration, ease: 'none' });
235
+ gsap.to(cursorRef.current, {
236
+ rotation: normalizedRotation + 360,
237
+ duration: spinDuration * (1 - normalizedRotation / 360),
238
+ ease: 'none',
239
+ onComplete: () => {
240
+ spinTl.current?.restart();
241
+ }
242
+ });
243
+ }
244
+ resumeTimeout = null;
245
+ }, 50);
246
+ cleanupTarget(target);
247
+ };
248
+ currentLeaveHandler = leaveHandler;
249
+ target.addEventListener('mouseleave', leaveHandler);
250
+ };
251
+
252
+ window.addEventListener('mouseover', enterHandler as EventListener);
253
+
254
+ return () => {
255
+ if (tickerFnRef.current) {
256
+ gsap.ticker.remove(tickerFnRef.current);
257
+ }
258
+ window.removeEventListener('mousemove', moveHandler);
259
+ window.removeEventListener('mouseover', enterHandler as EventListener);
260
+ window.removeEventListener('scroll', scrollHandler);
261
+ window.removeEventListener('mousedown', mouseDownHandler);
262
+ window.removeEventListener('mouseup', mouseUpHandler);
263
+ if (activeTarget) {
264
+ cleanupTarget(activeTarget);
265
+ }
266
+ spinTl.current?.kill();
267
+ document.body.style.cursor = originalCursor;
268
+ const styleTag = document.getElementById('hide-default-cursor-style');
269
+ if (styleTag) styleTag.remove();
270
+ isActiveRef.current = false;
271
+ targetCornerPositionsRef.current = null;
272
+ activeStrengthRef.current.current = 0;
273
+ };
274
+ }, [targetSelector, spinDuration, moveCursor, constants, hideDefaultCursor, isMobile, hoverDuration, parallaxOn]);
275
+
276
+ useEffect(() => {
277
+ if (isMobile || !cursorRef.current || !spinTl.current) return;
278
+ if (spinTl.current.isActive()) {
279
+ spinTl.current.kill();
280
+ spinTl.current = gsap
281
+ .timeline({ repeat: -1 })
282
+ .to(cursorRef.current, { rotation: '+=360', duration: spinDuration, ease: 'none' });
283
+ }
284
+ }, [spinDuration, isMobile]);
285
+
286
+ if (isMobile) {
287
+ return null;
288
+ }
289
+
290
+ return (
291
+ <div
292
+ ref={cursorRef}
293
+ className="fixed top-0 left-0 w-0 h-0 pointer-events-none z-[9999]"
294
+ style={{ willChange: 'transform' }}
295
+ >
296
+ <div
297
+ ref={dotRef}
298
+ className="absolute top-1/2 left-1/2 w-1 h-1 bg-white rounded-full -translate-x-1/2 -translate-y-1/2"
299
+ style={{ willChange: 'transform', mixBlendMode: 'difference' }}
300
+ />
301
+ <div
302
+ className="target-cursor-corner absolute top-1/2 left-1/2 w-3 h-3 border-[3px] border-white -translate-x-[150%] -translate-y-[150%] border-r-0 border-b-0"
303
+ style={{ willChange: 'transform', mixBlendMode: 'difference' }}
304
+ />
305
+ <div
306
+ className="target-cursor-corner absolute top-1/2 left-1/2 w-3 h-3 border-[3px] border-white translate-x-1/2 -translate-y-[150%] border-l-0 border-b-0"
307
+ style={{ willChange: 'transform', mixBlendMode: 'difference' }}
308
+ />
309
+ <div
310
+ className="target-cursor-corner absolute top-1/2 left-1/2 w-3 h-3 border-[3px] border-white translate-x-1/2 translate-y-1/2 border-l-0 border-t-0"
311
+ style={{ willChange: 'transform', mixBlendMode: 'difference' }}
312
+ />
313
+ <div
314
+ className="target-cursor-corner absolute top-1/2 left-1/2 w-3 h-3 border-[3px] border-white -translate-x-[150%] translate-y-1/2 border-r-0 border-t-0"
315
+ style={{ willChange: 'transform', mixBlendMode: 'difference' }}
316
+ />
317
+ </div>
318
+ );
319
+ };
320
+
321
+ export default TargetCursor;
frontend/src/components/UploadZone.tsx ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from "react";
2
+
3
+ export interface PredictionResult {
4
+ filename?: string;
5
+ success: boolean;
6
+ error?: string;
7
+ prediction?: string;
8
+ confidence?: number;
9
+ probabilities?: { name: string; probability: number }[];
10
+ details?: {
11
+ gram_stain: string;
12
+ shape: string;
13
+ pathogenicity: string;
14
+ };
15
+ previewUrl?: string;
16
+ }
17
+
18
+ const PRECAUTIONS: Record<string, string> = {
19
+ "Escherichia coli": "Indicator of fecal contamination. \n\nPrecautions/Actions: Boil water immediately before consumption. Source trace to find sewage leaks. Do not use for washing open wounds.",
20
+ "Pseudomonas aeruginosa": "Opportunistic pathogen resistant to many sanitizers. \n\nPrecautions/Actions: Ensure water chlorination levels are adequate. Can cause severe infections in immunocompromised individuals. Avoid contact with eyes or ears.",
21
+ "Enterococcus faecalis": "Indicates prolonged fecal contamination. Very resilient. \n\nPrecautions/Actions: Shock chlorinate the water system. Discontinue use for drinking until negative tests are returned.",
22
+ "Clostridium perfringens": "Spore-forming bacteria, highly resistant to standard disinfection. \n\nPrecautions/Actions: Indicates remote or past fecal contamination. UV filtration or extreme heat treatment may be required.",
23
+ "Listeria monocytogenes": "Dangerous to pregnant women and immunocompromised individuals. \n\nPrecautions/Actions: Do not use water for food preparation or drinking. Pasteurization/boiling is required."
24
+ };
25
+
26
+ interface UploadZoneProps {
27
+ onResultsGenerated?: (results: PredictionResult[] | null) => void;
28
+ }
29
+
30
+ export const UploadZone = ({ onResultsGenerated }: UploadZoneProps) => {
31
+ const [files, setFiles] = useState<File[]>([]);
32
+ const [isUploading, setIsUploading] = useState(false);
33
+ const [progress, setProgress] = useState(0);
34
+ const [localResults, setLocalResults] = useState<PredictionResult[] | null>(null);
35
+ const [selectedResult, setSelectedResult] = useState<PredictionResult | null>(null);
36
+ const [error, setError] = useState<string | null>(null);
37
+ const fileInputRef = useRef<HTMLInputElement>(null);
38
+
39
+ const handleFiles = async (selectedFiles: FileList | File[]) => {
40
+ const fileArray = Array.from(selectedFiles);
41
+ if (fileArray.length === 0) return;
42
+
43
+ setFiles(fileArray);
44
+
45
+ // Generate preview URLs
46
+ const previewUrls = fileArray.map(f => URL.createObjectURL(f));
47
+
48
+ setLocalResults(null);
49
+ if (onResultsGenerated) onResultsGenerated(null);
50
+ setSelectedResult(null);
51
+ setError(null);
52
+ setIsUploading(true);
53
+ setProgress(20);
54
+
55
+ const formData = new FormData();
56
+ fileArray.forEach(f => {
57
+ formData.append("files", f);
58
+ });
59
+
60
+ try {
61
+ setProgress(60);
62
+ const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:5000";
63
+ const response = await fetch(`${apiUrl}/predict_batch`, {
64
+ method: "POST",
65
+ body: formData,
66
+ });
67
+
68
+ if (!response.ok) {
69
+ throw new Error("Failed to analyze images");
70
+ }
71
+
72
+ setProgress(90);
73
+ const data = await response.json();
74
+
75
+ // Attach preview URLs to results for display
76
+ if (data.results && Array.isArray(data.results)) {
77
+ const resultsWithPreviews = data.results.map((r: any) => {
78
+ const matchIndex = fileArray.findIndex(f => f.name === r.filename);
79
+ return {
80
+ ...r,
81
+ previewUrl: matchIndex >= 0 ? previewUrls[matchIndex] : null
82
+ };
83
+ });
84
+ setLocalResults(resultsWithPreviews);
85
+ if (onResultsGenerated) onResultsGenerated(resultsWithPreviews);
86
+ }
87
+ setProgress(100);
88
+ } catch (err: any) {
89
+ setError(err.message || "An error occurred");
90
+ } finally {
91
+ setIsUploading(false);
92
+ }
93
+ };
94
+
95
+ const onFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
96
+ e.preventDefault();
97
+ if (e.dataTransfer.files.length > 0) {
98
+ handleFiles(e.dataTransfer.files);
99
+ }
100
+ };
101
+
102
+ const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
103
+ if (e.target.files && e.target.files.length > 0) {
104
+ handleFiles(e.target.files);
105
+ }
106
+ };
107
+
108
+ return (
109
+ <section className="max-w-7xl mx-auto px-6 mb-24 relative z-10" id="upload">
110
+ <div className={`grid ${localResults ? 'grid-cols-1 lg:grid-cols-3' : 'grid-cols-1 max-w-2xl mx-auto'} gap-8`}>
111
+ <div className="space-y-6 lg:col-span-1">
112
+ <h3 className="text-3xl font-display font-bold">Upload Zone</h3>
113
+
114
+ <div
115
+ onDragOver={(e) => e.preventDefault()}
116
+ onDrop={onFileDrop}
117
+ onClick={() => fileInputRef.current?.click()}
118
+ className="cursor-target border-2 border-dashed border-primary/30 rounded-3xl p-12 flex flex-col items-center justify-center text-center bg-background-light/50 dark:bg-background-dark/50 backdrop-blur-md hover:bg-primary/10 transition-colors group cursor-pointer h-64"
119
+ >
120
+ <input
121
+ type="file"
122
+ ref={fileInputRef}
123
+ onChange={onFileChange}
124
+ className="hidden"
125
+ accept="image/*"
126
+ multiple
127
+ />
128
+ <div className="w-16 h-16 rounded-full bg-primary/20 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
129
+ <span className="material-symbols-outlined text-primary text-3xl">cloud_upload</span>
130
+ </div>
131
+ <h4 className="text-lg font-bold mb-2 text-slate-900 dark:text-white">Drag & drop images</h4>
132
+ <p className="text-sm text-slate-500 dark:text-slate-400 mb-4">Supports JPG, PNG up to 20MB.</p>
133
+ <button className="cursor-target bg-primary text-white px-5 py-2 rounded-lg font-bold hover:scale-105 transition-transform text-sm">Browse</button>
134
+ </div>
135
+
136
+ {/* Upload Progress */}
137
+ {isUploading && (
138
+ <div className="bg-slate-100 dark:bg-slate-800/80 p-6 rounded-2xl border border-slate-200 dark:border-slate-700 backdrop-blur-md">
139
+ <div className="flex items-center justify-between mb-2">
140
+ <span className="text-sm font-semibold text-slate-900 dark:text-slate-100">Processing {files.length} images</span>
141
+ <span className="text-sm font-bold text-primary">{progress}%</span>
142
+ </div>
143
+ <div className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
144
+ <div className="h-full bg-primary rounded-full transition-all duration-300" style={{ width: `${progress}%` }}></div>
145
+ </div>
146
+ <p className="text-xs text-slate-500 mt-3 flex items-center gap-1">
147
+ <span className="material-symbols-outlined text-[14px] animate-spin">sync</span>
148
+ Analyzing morphological features...
149
+ </p>
150
+ </div>
151
+ )}
152
+
153
+ {error && (
154
+ <div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-xl flex items-center gap-2">
155
+ <span className="material-symbols-outlined">error</span>
156
+ {error}
157
+ </div>
158
+ )}
159
+ </div>
160
+
161
+ {localResults && (
162
+ <div className="space-y-6 lg:col-span-2">
163
+ <div className="flex items-center justify-between">
164
+ <h3 className="text-3xl font-display font-bold">Analysis Results</h3>
165
+ <span className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm font-bold">{localResults.length} processed</span>
166
+ </div>
167
+
168
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
169
+ {localResults.map((res, index) => (
170
+ <div
171
+ key={index}
172
+ onClick={() => res.success && setSelectedResult(res)}
173
+ className={`bg-white/80 dark:bg-slate-800/80 backdrop-blur-md p-6 rounded-3xl border border-slate-200 dark:border-slate-700 shadow-xl flex flex-col ${res.success ? 'cursor-pointer hover:scale-[1.02] hover:border-primary/50 transition-all' : ''}`}
174
+ title={res.success ? "Click for detailed summary" : ""}
175
+ >
176
+ <div className="flex gap-4 mb-4">
177
+ <div className="w-24 h-24 rounded-2xl overflow-hidden border border-primary/20 flex-shrink-0 bg-black">
178
+ {res.previewUrl ? (
179
+ <img className="w-full h-full object-cover" alt={`Upload ${index}`} src={res.previewUrl} />
180
+ ) : (
181
+ <div className="w-full h-full flex items-center justify-center bg-slate-800"><span className="material-symbols-outlined text-slate-500">image</span></div>
182
+ )}
183
+ </div>
184
+ <div className="flex-1 min-w-0">
185
+ <p className="text-xs text-slate-500 dark:text-slate-400 truncate mb-1" title={res.filename}>{res.filename}</p>
186
+
187
+ {res.success && res.prediction ? (
188
+ <>
189
+ <h4 className="text-lg font-bold italic text-slate-900 dark:text-slate-100 leading-tight mb-2 truncate" title={res.prediction}>{res.prediction}</h4>
190
+ <div className={`inline-flex items-center gap-1 text-xs font-bold px-2 py-1 rounded-full whitespace-nowrap ${(res.confidence || 0) > 80 ? 'bg-green-500/10 text-green-500' : 'bg-yellow-500/10 text-yellow-500'}`}>
191
+ <span className="material-symbols-outlined text-[14px]">{(res.confidence || 0) > 80 ? 'verified' : 'warning'}</span>
192
+ {(res.confidence || 0).toFixed(1)}% Conf
193
+ </div>
194
+ </>
195
+ ) : (
196
+ <div className="text-red-500 text-sm font-bold flex items-center gap-1">
197
+ <span className="material-symbols-outlined text-sm">error</span> Failed
198
+ </div>
199
+ )}
200
+ </div>
201
+ </div>
202
+
203
+ {res.success && res.details && (
204
+ <div className="flex-1 flex flex-col justify-end">
205
+ <div className="grid grid-cols-2 gap-2 p-3 bg-slate-50 dark:bg-slate-900/50 rounded-xl">
206
+ <div className="text-xs text-slate-900 dark:text-slate-100">
207
+ <span className="text-slate-500 block mb-0.5 mt-0">Morphology</span>
208
+ <span className="font-semibold">{res.details.shape}</span>
209
+ </div>
210
+ <div className="text-xs text-slate-900 dark:text-slate-100">
211
+ <span className="text-slate-500 block mb-0.5 mt-0">Stain</span>
212
+ <span className="font-semibold">{res.details.gram_stain}</span>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ )}
217
+ </div>
218
+ ))}
219
+ </div>
220
+ </div>
221
+ )}
222
+ </div>
223
+
224
+ {/* Modal Dialog Box */}
225
+ {selectedResult && (
226
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" onClick={() => setSelectedResult(null)}>
227
+ <div
228
+ className="bg-white dark:bg-slate-900 rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl border border-slate-200 dark:border-slate-700"
229
+ onClick={e => e.stopPropagation()}
230
+ >
231
+ <div className="p-6 md:p-8 flex flex-col gap-6">
232
+ <div className="flex items-start justify-between">
233
+ <h3 className="text-2xl font-bold font-display text-slate-900 dark:text-white">Detailed Classification Summary</h3>
234
+ <button onClick={() => setSelectedResult(null)} className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
235
+ <span className="material-symbols-outlined">close</span>
236
+ </button>
237
+ </div>
238
+
239
+ <div className="flex flex-col md:flex-row gap-6">
240
+ <div className="w-full md:w-48 h-48 rounded-2xl overflow-hidden bg-black flex-shrink-0 border-4 border-slate-100 dark:border-slate-800">
241
+ {selectedResult.previewUrl ? (
242
+ <img src={selectedResult.previewUrl} alt="Analyzed bacteria" className="w-full h-full object-cover" />
243
+ ) : (
244
+ <div className="w-full h-full flex items-center justify-center text-slate-500"><span className="material-symbols-outlined text-4xl">image</span></div>
245
+ )}
246
+ </div>
247
+ <div className="flex-1 space-y-4">
248
+ <div>
249
+ <span className="text-sm font-bold text-slate-500 uppercase tracking-wider">Predicted Species</span>
250
+ <h4 className="text-3xl font-bold italic text-primary mt-1">{selectedResult.prediction}</h4>
251
+ </div>
252
+
253
+ <div className="grid grid-cols-2 gap-4">
254
+ <div className="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl">
255
+ <span className="text-xs text-slate-500 block mb-1">Confidence</span>
256
+ <span className="text-lg font-bold text-slate-900 dark:text-white">{(selectedResult.confidence || 0).toFixed(1)}%</span>
257
+ </div>
258
+ <div className="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl">
259
+ <span className="text-xs text-slate-500 block mb-1">Risk Level</span>
260
+ <span className="text-lg font-bold text-slate-900 dark:text-white">{selectedResult.details?.pathogenicity || 'Unknown'}</span>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-2">
267
+ <div className="space-y-3">
268
+ <h5 className="font-bold text-slate-900 dark:text-white border-b border-slate-200 dark:border-slate-700 pb-2">Microbiology Specifics</h5>
269
+ <ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
270
+ <li className="flex justify-between"><span>Gram Stain:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.gram_stain}</strong></li>
271
+ <li className="flex justify-between"><span>Morphology:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.shape}</strong></li>
272
+ <li className="flex justify-between"><span>Pathogenicity:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.pathogenicity}</strong></li>
273
+ </ul>
274
+ </div>
275
+
276
+ <div className="space-y-3">
277
+ <h5 className="font-bold text-slate-900 dark:text-white border-b border-slate-200 dark:border-slate-700 pb-2">Precautions & Actions</h5>
278
+ <div className="bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-200 dark:border-yellow-500/20 rounded-xl p-4 text-sm text-yellow-800 dark:text-yellow-200 shadow-inner">
279
+ <div className="flex gap-2 items-start whitespace-pre-line">
280
+ <span className="material-symbols-outlined text-[18px] mt-0.5">warning</span>
281
+ <p className="leading-relaxed m-0">
282
+ {selectedResult.prediction ? (PRECAUTIONS[selectedResult.prediction] || "Standard water safety protocols apply. Heat above 70°C before any consumption.") : "Unknown"}
283
+ </p>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ </div>
288
+
289
+ <div className="mt-4 flex justify-end">
290
+ <button className="px-6 py-2 bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold rounded-xl hover:scale-105 transition-transform" onClick={() => setSelectedResult(null)}>
291
+ Close Details
292
+ </button>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ </div>
297
+ )}
298
+ </section>
299
+ );
300
+ };
301
+
frontend/src/index.css ADDED
@@ -0,0 +1 @@
 
 
1
+ /* Tailwind CSS is injected via CDN in index.html for rapid prototyping */
frontend/src/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }