joytheslothh commited on
Commit
8f0f112
·
1 Parent(s): b67be67

feat: upgrade to BacSense v2 with dynamic batch dashboard and UI improvements

Browse files
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
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
frontend/.gitignore CHANGED
@@ -22,3 +22,4 @@ dist-ssr
22
  *.njsproj
23
  *.sln
24
  *.sw?
 
 
22
  *.njsproj
23
  *.sln
24
  *.sw?
25
+ .vercel
frontend/run_frontend.ps1 ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ npm install
2
+ npm run dev
frontend/src/App.tsx CHANGED
@@ -1,12 +1,16 @@
 
1
  import { Header } from "./components/Header"
2
  import { Hero } from "./components/Hero"
3
  import { Species } from "./components/Species"
4
  import { UploadZone } from "./components/UploadZone"
 
5
  import { Results } from "./components/Results"
6
  import { Footer } from "./components/Footer"
7
  import TargetCursor from "./components/TargetCursor"
8
 
9
  function App() {
 
 
10
  return (
11
  <>
12
  <TargetCursor
@@ -19,8 +23,8 @@ function App() {
19
  <main className="pt-32 pb-20">
20
  <Hero />
21
  <Species />
22
- <UploadZone />
23
- <Results />
24
  </main>
25
  <Footer />
26
  </>
 
1
+ import { useState } from "react"
2
  import { Header } from "./components/Header"
3
  import { Hero } from "./components/Hero"
4
  import { Species } from "./components/Species"
5
  import { UploadZone } from "./components/UploadZone"
6
+ import type { PredictionResult } from "./components/UploadZone"
7
  import { Results } from "./components/Results"
8
  import { Footer } from "./components/Footer"
9
  import TargetCursor from "./components/TargetCursor"
10
 
11
  function App() {
12
+ const [results, setResults] = useState<PredictionResult[] | null>(null);
13
+
14
  return (
15
  <>
16
  <TargetCursor
 
23
  <main className="pt-32 pb-20">
24
  <Hero />
25
  <Species />
26
+ <UploadZone onResultsGenerated={setResults} />
27
+ {results && results.length > 0 && <Results items={results} />}
28
  </main>
29
  <Footer />
30
  </>
frontend/src/components/Results.tsx CHANGED
@@ -1,4 +1,10 @@
1
- export const Results = () => {
 
 
 
 
 
 
2
  return (
3
  <section className="max-w-7xl mx-auto px-6 mb-24">
4
  <h3 className="text-3xl font-display font-bold mb-8">Classification Results</h3>
@@ -15,45 +21,34 @@ export const Results = () => {
15
  </tr>
16
  </thead>
17
  <tbody className="divide-y divide-slate-200 dark:divide-slate-800 text-slate-900 dark:text-slate-100">
18
- <tr className="hover:bg-primary/5 transition-colors">
19
- <td className="px-6 py-4 text-sm font-medium">sample_a102.png</td>
20
- <td className="px-6 py-4 text-sm italic">E. coli</td>
21
- <td className="px-6 py-4">
22
- <span className="bg-green-500/10 text-green-500 text-[11px] font-bold px-2 py-0.5 rounded">98.2%</span>
23
- </td>
24
- <td className="px-6 py-4 text-sm">Negative</td>
25
- <td className="px-6 py-4 text-sm text-slate-500 dark:text-slate-400">Rod</td>
26
- <td className="px-6 py-4">
27
- <span className="bg-red-500/10 text-red-500 text-[11px] font-bold px-2 py-0.5 rounded uppercase">High Risk</span>
28
- </td>
29
- </tr>
30
- <tr className="hover:bg-primary/5 transition-colors">
31
- <td className="px-6 py-4 text-sm font-medium">sample_b405.png</td>
32
- <td className="px-6 py-4 text-sm italic">S. aureus</td>
33
- <td className="px-6 py-4">
34
- <span className="bg-green-500/10 text-green-500 text-[11px] font-bold px-2 py-0.5 rounded">96.7%</span>
35
- </td>
36
- <td className="px-6 py-4 text-sm">Positive</td>
37
- <td className="px-6 py-4 text-sm text-slate-500 dark:text-slate-400">Cocci</td>
38
- <td className="px-6 py-4">
39
- <span className="bg-orange-500/10 text-orange-500 text-[11px] font-bold px-2 py-0.5 rounded uppercase">Moderate</span>
40
- </td>
41
- </tr>
42
- <tr className="hover:bg-primary/5 transition-colors">
43
- <td className="px-6 py-4 text-sm font-medium">sample_z901.png</td>
44
- <td className="px-6 py-4 text-sm italic">B. cereus</td>
45
- <td className="px-6 py-4">
46
- <span className="bg-yellow-500/10 text-yellow-500 text-[11px] font-bold px-2 py-0.5 rounded">84.1%</span>
47
- </td>
48
- <td className="px-6 py-4 text-sm">Positive</td>
49
- <td className="px-6 py-4 text-sm text-slate-500 dark:text-slate-400">Spore-rod</td>
50
- <td className="px-6 py-4">
51
- <span className="bg-blue-500/10 text-blue-500 text-[11px] font-bold px-2 py-0.5 rounded uppercase">Low Risk</span>
52
- </td>
53
- </tr>
54
  </tbody>
55
  </table>
56
  </div>
57
  </section>
58
  );
59
  };
 
 
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>
 
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/UploadZone.tsx CHANGED
@@ -1,51 +1,88 @@
1
  import { useState, useRef } from "react";
2
 
3
- interface PredictionResult {
 
4
  success: boolean;
5
- prediction: string;
6
- confidence: number;
7
- probabilities: { name: string; probability: number }[];
8
- details: {
 
9
  gram_stain: string;
10
  shape: string;
11
  pathogenicity: string;
12
  };
 
13
  }
14
 
15
- export const UploadZone = () => {
16
- const [file, setFile] = useState<File | null>(null);
17
- const [previewUrl, setPreviewUrl] = useState<string | null>(null);
 
 
 
 
 
 
 
 
 
 
 
18
  const [isUploading, setIsUploading] = useState(false);
19
  const [progress, setProgress] = useState(0);
20
- const [result, setResult] = useState<PredictionResult | null>(null);
 
21
  const [error, setError] = useState<string | null>(null);
22
  const fileInputRef = useRef<HTMLInputElement>(null);
23
 
24
- const handleFile = async (selectedFile: File) => {
25
- setFile(selectedFile);
26
- setPreviewUrl(URL.createObjectURL(selectedFile));
27
- setResult(null);
 
 
 
 
 
 
 
 
28
  setError(null);
29
  setIsUploading(true);
30
  setProgress(20);
31
 
32
  const formData = new FormData();
33
- formData.append("file", selectedFile);
 
 
34
 
35
  try {
36
  setProgress(60);
37
- const response = await fetch("http://localhost:5000/predict", {
38
  method: "POST",
39
  body: formData,
40
  });
41
 
42
  if (!response.ok) {
43
- throw new Error("Failed to analyze image");
44
  }
45
 
46
  setProgress(90);
47
  const data = await response.json();
48
- setResult(data);
 
 
 
 
 
 
 
 
 
 
 
 
49
  setProgress(100);
50
  } catch (err: any) {
51
  setError(err.message || "An error occurred");
@@ -56,26 +93,28 @@ export const UploadZone = () => {
56
 
57
  const onFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
58
  e.preventDefault();
59
- const droppedFile = e.dataTransfer.files[0];
60
- if (droppedFile) handleFile(droppedFile);
 
61
  };
62
 
63
  const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
64
- const selectedFile = e.target.files?.[0];
65
- if (selectedFile) handleFile(selectedFile);
 
66
  };
67
 
68
  return (
69
  <section className="max-w-7xl mx-auto px-6 mb-24 relative z-10" id="upload">
70
- <div className={`grid ${result ? 'grid-cols-1 lg:grid-cols-2' : 'grid-cols-1 max-w-2xl mx-auto'} gap-12`}>
71
- <div className="space-y-6">
72
  <h3 className="text-3xl font-display font-bold">Upload Zone</h3>
73
 
74
  <div
75
  onDragOver={(e) => e.preventDefault()}
76
  onDrop={onFileDrop}
77
  onClick={() => fileInputRef.current?.click()}
78
- 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"
79
  >
80
  <input
81
  type="file"
@@ -83,20 +122,21 @@ export const UploadZone = () => {
83
  onChange={onFileChange}
84
  className="hidden"
85
  accept="image/*"
 
86
  />
87
- <div className="w-20 h-20 rounded-full bg-primary/20 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
88
- <span className="material-symbols-outlined text-primary text-4xl">cloud_upload</span>
89
  </div>
90
- <h4 className="text-xl font-bold mb-2 text-slate-900 dark:text-white">Drag and drop images here</h4>
91
- <p className="text-slate-500 dark:text-slate-400 mb-6">Supports JPG, PNG up to 20MB.</p>
92
- <button className="cursor-target bg-primary text-white px-6 py-2 rounded-lg font-bold hover:scale-105 transition-transform">Browse Files</button>
93
  </div>
94
 
95
  {/* Upload Progress */}
96
  {isUploading && (
97
  <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">
98
  <div className="flex items-center justify-between mb-2">
99
- <span className="text-sm font-semibold text-slate-900 dark:text-slate-100">Processing {file?.name}</span>
100
  <span className="text-sm font-bold text-primary">{progress}%</span>
101
  </div>
102
  <div className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
@@ -104,7 +144,7 @@ export const UploadZone = () => {
104
  </div>
105
  <p className="text-xs text-slate-500 mt-3 flex items-center gap-1">
106
  <span className="material-symbols-outlined text-[14px] animate-spin">sync</span>
107
- Analyzing morphological features via VGG16 backbone...
108
  </p>
109
  </div>
110
  )}
@@ -117,51 +157,144 @@ export const UploadZone = () => {
117
  )}
118
  </div>
119
 
120
- {result && (
121
- <div className="space-y-6">
122
- <h3 className="text-3xl font-display font-bold">Detailed Analysis</h3>
123
- <div className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-md p-8 rounded-3xl border border-slate-200 dark:border-slate-700 shadow-xl">
124
- <div className="flex flex-col sm:flex-row gap-6 mb-8">
125
- <div className="w-32 h-32 rounded-2xl overflow-hidden border-2 border-primary/20 flex-shrink-0 bg-black">
126
- {previewUrl && <img className="w-full h-full object-cover" alt="Uploaded preview" src={previewUrl} />}
127
- </div>
128
- <div className="flex-1">
129
- <div className="flex flex-wrap items-center justify-between gap-2 mb-2">
130
- <h4 className="text-2xl font-bold italic text-slate-900 dark:text-slate-100">{result.prediction}</h4>
131
- <span className={`text-xs font-bold px-3 py-1 rounded-full whitespace-nowrap ${result.confidence > 80 ? 'bg-green-500/10 text-green-500' : 'bg-yellow-500/10 text-yellow-500'}`}>
132
- {result.confidence > 80 ? 'High Confidence' : 'Moderate'}
133
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </div>
135
- <div className="flex items-center gap-4 mb-4">
136
- <div className="text-3xl font-display font-bold text-primary">{result.confidence.toFixed(1)}%</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  </div>
 
138
  <div className="grid grid-cols-2 gap-4">
139
- <div className="text-xs text-slate-900 dark:text-slate-100"><span className="text-slate-500">Morphology:</span> <span className="font-semibold">{result.details.shape}</span></div>
140
- <div className="text-xs text-slate-900 dark:text-slate-100"><span className="text-slate-500">Stain:</span> <span className="font-semibold">{result.details.gram_stain}</span></div>
 
 
 
 
 
 
141
  </div>
142
  </div>
143
  </div>
144
 
145
- <div className="space-y-4">
146
- <h5 className="text-xs font-bold uppercase tracking-widest text-slate-500">Probability Distribution</h5>
147
  <div className="space-y-3">
148
- {result.probabilities.slice(0, 3).map((prob, idx) => (
149
- <div key={prob.name}>
150
- <div className="flex justify-between text-xs mb-1 text-slate-900 dark:text-slate-100">
151
- <span>{prob.name}</span>
152
- <span className="font-bold">{prob.probability.toFixed(1)}%</span>
153
- </div>
154
- <div className="w-full h-1.5 bg-slate-100 dark:bg-slate-700 rounded-full">
155
- <div className={`h-full rounded-full ${idx === 0 ? 'bg-primary' : 'bg-slate-400'}`} style={{ width: `${prob.probability}%` }}></div>
156
- </div>
 
 
 
 
 
 
 
157
  </div>
158
- ))}
159
  </div>
160
  </div>
 
 
 
 
 
 
161
  </div>
162
  </div>
163
- )}
164
- </div>
165
  </section>
166
  );
167
  };
 
 
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 response = await fetch("http://localhost:5000/predict_batch", {
63
  method: "POST",
64
  body: formData,
65
  });
66
 
67
  if (!response.ok) {
68
+ throw new Error("Failed to analyze images");
69
  }
70
 
71
  setProgress(90);
72
  const data = await response.json();
73
+
74
+ // Attach preview URLs to results for display
75
+ if (data.results && Array.isArray(data.results)) {
76
+ const resultsWithPreviews = data.results.map((r: any) => {
77
+ const matchIndex = fileArray.findIndex(f => f.name === r.filename);
78
+ return {
79
+ ...r,
80
+ previewUrl: matchIndex >= 0 ? previewUrls[matchIndex] : null
81
+ };
82
+ });
83
+ setLocalResults(resultsWithPreviews);
84
+ if (onResultsGenerated) onResultsGenerated(resultsWithPreviews);
85
+ }
86
  setProgress(100);
87
  } catch (err: any) {
88
  setError(err.message || "An error occurred");
 
93
 
94
  const onFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
95
  e.preventDefault();
96
+ if (e.dataTransfer.files.length > 0) {
97
+ handleFiles(e.dataTransfer.files);
98
+ }
99
  };
100
 
101
  const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
102
+ if (e.target.files && e.target.files.length > 0) {
103
+ handleFiles(e.target.files);
104
+ }
105
  };
106
 
107
  return (
108
  <section className="max-w-7xl mx-auto px-6 mb-24 relative z-10" id="upload">
109
+ <div className={`grid ${localResults ? 'grid-cols-1 lg:grid-cols-3' : 'grid-cols-1 max-w-2xl mx-auto'} gap-8`}>
110
+ <div className="space-y-6 lg:col-span-1">
111
  <h3 className="text-3xl font-display font-bold">Upload Zone</h3>
112
 
113
  <div
114
  onDragOver={(e) => e.preventDefault()}
115
  onDrop={onFileDrop}
116
  onClick={() => fileInputRef.current?.click()}
117
+ 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"
118
  >
119
  <input
120
  type="file"
 
122
  onChange={onFileChange}
123
  className="hidden"
124
  accept="image/*"
125
+ multiple
126
  />
127
+ <div className="w-16 h-16 rounded-full bg-primary/20 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
128
+ <span className="material-symbols-outlined text-primary text-3xl">cloud_upload</span>
129
  </div>
130
+ <h4 className="text-lg font-bold mb-2 text-slate-900 dark:text-white">Drag & drop images</h4>
131
+ <p className="text-sm text-slate-500 dark:text-slate-400 mb-4">Supports JPG, PNG up to 20MB.</p>
132
+ <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>
133
  </div>
134
 
135
  {/* Upload Progress */}
136
  {isUploading && (
137
  <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">
138
  <div className="flex items-center justify-between mb-2">
139
+ <span className="text-sm font-semibold text-slate-900 dark:text-slate-100">Processing {files.length} images</span>
140
  <span className="text-sm font-bold text-primary">{progress}%</span>
141
  </div>
142
  <div className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
 
144
  </div>
145
  <p className="text-xs text-slate-500 mt-3 flex items-center gap-1">
146
  <span className="material-symbols-outlined text-[14px] animate-spin">sync</span>
147
+ Analyzing morphological features...
148
  </p>
149
  </div>
150
  )}
 
157
  )}
158
  </div>
159
 
160
+ {localResults && (
161
+ <div className="space-y-6 lg:col-span-2">
162
+ <div className="flex items-center justify-between">
163
+ <h3 className="text-3xl font-display font-bold">Analysis Results</h3>
164
+ <span className="bg-primary/10 text-primary px-3 py-1 rounded-full text-sm font-bold">{localResults.length} processed</span>
165
+ </div>
166
+
167
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
168
+ {localResults.map((res, index) => (
169
+ <div
170
+ key={index}
171
+ onClick={() => res.success && setSelectedResult(res)}
172
+ 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' : ''}`}
173
+ title={res.success ? "Click for detailed summary" : ""}
174
+ >
175
+ <div className="flex gap-4 mb-4">
176
+ <div className="w-24 h-24 rounded-2xl overflow-hidden border border-primary/20 flex-shrink-0 bg-black">
177
+ {res.previewUrl ? (
178
+ <img className="w-full h-full object-cover" alt={`Upload ${index}`} src={res.previewUrl} />
179
+ ) : (
180
+ <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>
181
+ )}
182
+ </div>
183
+ <div className="flex-1 min-w-0">
184
+ <p className="text-xs text-slate-500 dark:text-slate-400 truncate mb-1" title={res.filename}>{res.filename}</p>
185
+
186
+ {res.success && res.prediction ? (
187
+ <>
188
+ <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>
189
+ <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'}`}>
190
+ <span className="material-symbols-outlined text-[14px]">{(res.confidence || 0) > 80 ? 'verified' : 'warning'}</span>
191
+ {(res.confidence || 0).toFixed(1)}% Conf
192
+ </div>
193
+ </>
194
+ ) : (
195
+ <div className="text-red-500 text-sm font-bold flex items-center gap-1">
196
+ <span className="material-symbols-outlined text-sm">error</span> Failed
197
+ </div>
198
+ )}
199
+ </div>
200
  </div>
201
+
202
+ {res.success && res.details && (
203
+ <div className="flex-1 flex flex-col justify-end">
204
+ <div className="grid grid-cols-2 gap-2 p-3 bg-slate-50 dark:bg-slate-900/50 rounded-xl">
205
+ <div className="text-xs text-slate-900 dark:text-slate-100">
206
+ <span className="text-slate-500 block mb-0.5 mt-0">Morphology</span>
207
+ <span className="font-semibold">{res.details.shape}</span>
208
+ </div>
209
+ <div className="text-xs text-slate-900 dark:text-slate-100">
210
+ <span className="text-slate-500 block mb-0.5 mt-0">Stain</span>
211
+ <span className="font-semibold">{res.details.gram_stain}</span>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ )}
216
+ </div>
217
+ ))}
218
+ </div>
219
+ </div>
220
+ )}
221
+ </div>
222
+
223
+ {/* Modal Dialog Box */}
224
+ {selectedResult && (
225
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" onClick={() => setSelectedResult(null)}>
226
+ <div
227
+ 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"
228
+ onClick={e => e.stopPropagation()}
229
+ >
230
+ <div className="p-6 md:p-8 flex flex-col gap-6">
231
+ <div className="flex items-start justify-between">
232
+ <h3 className="text-2xl font-bold font-display text-slate-900 dark:text-white">Detailed Classification Summary</h3>
233
+ <button onClick={() => setSelectedResult(null)} className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
234
+ <span className="material-symbols-outlined">close</span>
235
+ </button>
236
+ </div>
237
+
238
+ <div className="flex flex-col md:flex-row gap-6">
239
+ <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">
240
+ {selectedResult.previewUrl ? (
241
+ <img src={selectedResult.previewUrl} alt="Analyzed bacteria" className="w-full h-full object-cover" />
242
+ ) : (
243
+ <div className="w-full h-full flex items-center justify-center text-slate-500"><span className="material-symbols-outlined text-4xl">image</span></div>
244
+ )}
245
+ </div>
246
+ <div className="flex-1 space-y-4">
247
+ <div>
248
+ <span className="text-sm font-bold text-slate-500 uppercase tracking-wider">Predicted Species</span>
249
+ <h4 className="text-3xl font-bold italic text-primary mt-1">{selectedResult.prediction}</h4>
250
  </div>
251
+
252
  <div className="grid grid-cols-2 gap-4">
253
+ <div className="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl">
254
+ <span className="text-xs text-slate-500 block mb-1">Confidence</span>
255
+ <span className="text-lg font-bold text-slate-900 dark:text-white">{(selectedResult.confidence || 0).toFixed(1)}%</span>
256
+ </div>
257
+ <div className="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-xl">
258
+ <span className="text-xs text-slate-500 block mb-1">Risk Level</span>
259
+ <span className="text-lg font-bold text-slate-900 dark:text-white">{selectedResult.details?.pathogenicity || 'Unknown'}</span>
260
+ </div>
261
  </div>
262
  </div>
263
  </div>
264
 
265
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-2">
 
266
  <div className="space-y-3">
267
+ <h5 className="font-bold text-slate-900 dark:text-white border-b border-slate-200 dark:border-slate-700 pb-2">Microbiology Specifics</h5>
268
+ <ul className="space-y-2 text-sm text-slate-600 dark:text-slate-300">
269
+ <li className="flex justify-between"><span>Gram Stain:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.gram_stain}</strong></li>
270
+ <li className="flex justify-between"><span>Morphology:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.shape}</strong></li>
271
+ <li className="flex justify-between"><span>Pathogenicity:</span> <strong className="text-slate-900 dark:text-white">{selectedResult.details?.pathogenicity}</strong></li>
272
+ </ul>
273
+ </div>
274
+
275
+ <div className="space-y-3">
276
+ <h5 className="font-bold text-slate-900 dark:text-white border-b border-slate-200 dark:border-slate-700 pb-2">Precautions & Actions</h5>
277
+ <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">
278
+ <div className="flex gap-2 items-start whitespace-pre-line">
279
+ <span className="material-symbols-outlined text-[18px] mt-0.5">warning</span>
280
+ <p className="leading-relaxed m-0">
281
+ {selectedResult.prediction ? (PRECAUTIONS[selectedResult.prediction] || "Standard water safety protocols apply. Heat above 70°C before any consumption.") : "Unknown"}
282
+ </p>
283
  </div>
284
+ </div>
285
  </div>
286
  </div>
287
+
288
+ <div className="mt-4 flex justify-end">
289
+ <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)}>
290
+ Close Details
291
+ </button>
292
+ </div>
293
  </div>
294
  </div>
295
+ </div>
296
+ )}
297
  </section>
298
  );
299
  };
300
+
frontend/vercel.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "rewrites": [
3
+ {
4
+ "source": "/(.*)",
5
+ "destination": "/index.html"
6
+ }
7
+ ]
8
+ }