Spaces:
Sleeping
Sleeping
Commit ·
61da00d
1
Parent(s): 7c45e19
fix: add root app.py and HF Space config (Streamlit SDK)
Browse files- README.md +11 -0
- app.py +204 -0
- requirements.txt +2 -0
README.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# 🦠 Bacsense 2.0
|
| 2 |
|
| 3 |

|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: BacSense API
|
| 3 |
+
emoji: 🦠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: "1.32.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
# 🦠 Bacsense 2.0
|
| 13 |
|
| 14 |

|
app.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Ensure bacsense_v2_package is importable from the repo root
|
| 9 |
+
sys.path.insert(0, os.path.abspath(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 |
+
def render_details(item):
|
| 44 |
+
st.image(item["Image"], use_column_width=True)
|
| 45 |
+
st.markdown(f"### Predicted: **{item['Predicted Class']}**")
|
| 46 |
+
|
| 47 |
+
colA, colB = st.columns(2)
|
| 48 |
+
colA.metric("Confidence", f"{item['Confidence (%)']}%")
|
| 49 |
+
colB.metric("Risk Level", item['Risk'])
|
| 50 |
+
|
| 51 |
+
st.markdown("""---""")
|
| 52 |
+
st.markdown("**Bacterial Summary:**")
|
| 53 |
+
st.write(f"- **Gram Stain:** {item['Gram Stain']}")
|
| 54 |
+
st.write(f"- **Shape:** {item['Shape']}")
|
| 55 |
+
|
| 56 |
+
if item['Routed to Specialist']:
|
| 57 |
+
st.info(f"Ambiguous morphology triggered the Specialist SVM. Accepted: {'✅' if item['Specialist Accepted'] else '❌'}")
|
| 58 |
+
|
| 59 |
+
st.markdown("""---""")
|
| 60 |
+
st.markdown("**Precautions:**")
|
| 61 |
+
precaution_text = PRECAUTIONS.get(item['Predicted Class'], "No specific precautions available. Standard water safety protocols suggest boiling before consumption.")
|
| 62 |
+
st.warning(precaution_text)
|
| 63 |
+
|
| 64 |
+
if st.button("Close Summary", key="close_summary_btn"):
|
| 65 |
+
st.session_state.selected_item = None
|
| 66 |
+
st.rerun()
|
| 67 |
+
|
| 68 |
+
# Main UI
|
| 69 |
+
st.title("🦠 BacSense v2 Analytics Dashboard")
|
| 70 |
+
st.markdown("""
|
| 71 |
+
Welcome to the BacSense v2 Dashboard. This cascaded hybrid classifier uses **VGG16 Transfer Learning**
|
| 72 |
+
combined with **Hand-Crafted Feature Engineering** and an **RBF-SVM Specialist** to disambiguate waterborne pathogens.
|
| 73 |
+
You can safely upload **up to 60 images** at once.
|
| 74 |
+
""")
|
| 75 |
+
|
| 76 |
+
# Sidebar for info
|
| 77 |
+
with st.sidebar:
|
| 78 |
+
st.header("Supported Species")
|
| 79 |
+
st.markdown("""
|
| 80 |
+
- *Clostridium perfringens*
|
| 81 |
+
- *Enterococcus faecalis*
|
| 82 |
+
- *Escherichia coli*
|
| 83 |
+
- *Listeria monocytogenes*
|
| 84 |
+
- *Pseudomonas aeruginosa*
|
| 85 |
+
""")
|
| 86 |
+
st.markdown("---")
|
| 87 |
+
st.caption("BacSense v2 Cascaded Model")
|
| 88 |
+
st.caption("Overall Accuracy: 95.65%")
|
| 89 |
+
st.caption("Specialist AUC: 0.9863")
|
| 90 |
+
|
| 91 |
+
st.subheader("Batch Upload (Multiple Images)")
|
| 92 |
+
uploaded_files = st.file_uploader("Upload microscopic bacterial images...", type=["jpg", "jpeg", "png"], accept_multiple_files=True)
|
| 93 |
+
|
| 94 |
+
if "selected_item" not in st.session_state:
|
| 95 |
+
st.session_state.selected_item = None
|
| 96 |
+
|
| 97 |
+
if uploaded_files:
|
| 98 |
+
if st.session_state.selected_item is not None:
|
| 99 |
+
render_details(st.session_state.selected_item)
|
| 100 |
+
else:
|
| 101 |
+
uploaded_files = list(uploaded_files)[:100]
|
| 102 |
+
results = []
|
| 103 |
+
|
| 104 |
+
progress_container = st.container()
|
| 105 |
+
with progress_container:
|
| 106 |
+
st.write(f"Processing {len(uploaded_files)} images...")
|
| 107 |
+
progress_bar = st.progress(0)
|
| 108 |
+
status_text = st.empty()
|
| 109 |
+
|
| 110 |
+
for i, uploaded_file in enumerate(uploaded_files):
|
| 111 |
+
status_text.text(f"Analyzing [{i+1}/{len(uploaded_files)}]: {uploaded_file.name}...")
|
| 112 |
+
|
| 113 |
+
image = Image.open(uploaded_file)
|
| 114 |
+
fd, temp_path = tempfile.mkstemp(suffix=".png")
|
| 115 |
+
if image.mode != 'RGB':
|
| 116 |
+
image = image.convert('RGB')
|
| 117 |
+
|
| 118 |
+
with os.fdopen(fd, 'wb') as f:
|
| 119 |
+
image.save(f, format="PNG")
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
prediction = classifier.predict(temp_path)
|
| 123 |
+
confidence_pct = prediction['confidence'] * 100 if prediction['confidence'] <= 1.0 else prediction['confidence']
|
| 124 |
+
results.append({
|
| 125 |
+
"Filename": uploaded_file.name,
|
| 126 |
+
"Predicted Class": prediction['prediction'],
|
| 127 |
+
"Confidence (%)": round(confidence_pct, 2),
|
| 128 |
+
"Gram Stain": prediction.get('gram', 'Unknown'),
|
| 129 |
+
"Shape": prediction.get('shape', 'Unknown'),
|
| 130 |
+
"Risk": prediction.get('risk', 'Unknown'),
|
| 131 |
+
"Routed to Specialist": prediction.get('routed_to_specialist', False),
|
| 132 |
+
"Specialist Accepted": prediction.get('specialist_accepted', False),
|
| 133 |
+
"Image": image
|
| 134 |
+
})
|
| 135 |
+
except Exception as e:
|
| 136 |
+
st.error(f"Error processing {uploaded_file.name}: {e}")
|
| 137 |
+
finally:
|
| 138 |
+
if os.path.exists(temp_path):
|
| 139 |
+
os.remove(temp_path)
|
| 140 |
+
|
| 141 |
+
progress_bar.progress((i + 1) / len(uploaded_files))
|
| 142 |
+
|
| 143 |
+
status_text.text("Batch Processing Complete!")
|
| 144 |
+
|
| 145 |
+
if results:
|
| 146 |
+
df = pd.DataFrame(results)
|
| 147 |
+
|
| 148 |
+
st.markdown("---")
|
| 149 |
+
st.subheader("📊 Batch Analytics Summary")
|
| 150 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 151 |
+
|
| 152 |
+
total_images = len(results)
|
| 153 |
+
high_risk = len(df[df["Risk"] == "High"])
|
| 154 |
+
routed_spec = len(df[df["Routed to Specialist"] == True])
|
| 155 |
+
avg_confidence = df["Confidence (%)"].mean()
|
| 156 |
+
|
| 157 |
+
col1.metric("Total Images", total_images)
|
| 158 |
+
col2.metric("High Target Risk", high_risk)
|
| 159 |
+
col3.metric("Routed to Specialist", routed_spec, help="Ambiguous cases handled by the 683-dim Specialist SVM")
|
| 160 |
+
col4.metric("Avg Confidence", f"{avg_confidence:.1f}%")
|
| 161 |
+
|
| 162 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 163 |
+
col_chart1, col_chart2 = st.columns(2)
|
| 164 |
+
with col_chart1:
|
| 165 |
+
st.markdown("**Species Distribution**")
|
| 166 |
+
class_counts = df["Predicted Class"].value_counts().reset_index()
|
| 167 |
+
class_counts.columns = ["Species", "Count"]
|
| 168 |
+
st.bar_chart(class_counts.set_index("Species"))
|
| 169 |
+
|
| 170 |
+
with col_chart2:
|
| 171 |
+
st.markdown("**Gram Stain Breakdown**")
|
| 172 |
+
gram_counts = df["Gram Stain"].value_counts()
|
| 173 |
+
st.bar_chart(gram_counts)
|
| 174 |
+
|
| 175 |
+
st.markdown("---")
|
| 176 |
+
st.subheader("📋 Detailed Results Table")
|
| 177 |
+
st.dataframe(df.drop(columns=["Image"]), use_container_width=True)
|
| 178 |
+
|
| 179 |
+
st.markdown("---")
|
| 180 |
+
st.subheader("🖼️ Processed Image Gallery")
|
| 181 |
+
st.caption("Click on 'View Summary' underneath any image to view brief details and precautions for the detected pathogen.")
|
| 182 |
+
|
| 183 |
+
filter_class = st.selectbox("Filter gallery by predicted species:", ["All"] + sorted(df["Predicted Class"].unique().tolist()))
|
| 184 |
+
filtered_results = results if filter_class == "All" else [r for r in results if r["Predicted Class"] == filter_class]
|
| 185 |
+
|
| 186 |
+
cols_per_row = 4
|
| 187 |
+
for i in range(0, len(filtered_results), cols_per_row):
|
| 188 |
+
cols = st.columns(cols_per_row)
|
| 189 |
+
for j in range(cols_per_row):
|
| 190 |
+
if i + j < len(filtered_results):
|
| 191 |
+
item = filtered_results[i + j]
|
| 192 |
+
with cols[j]:
|
| 193 |
+
st.image(item["Image"], use_column_width=True)
|
| 194 |
+
st.markdown(f"**{item['Predicted Class']}**")
|
| 195 |
+
det_col1, det_col2 = st.columns(2)
|
| 196 |
+
det_col1.markdown(f"<small>{item['Confidence (%)']}% Conf</small>", unsafe_allow_html=True)
|
| 197 |
+
if item["Routed to Specialist"]:
|
| 198 |
+
det_col2.markdown(f"<small>Specialist: {'✅' if item['Specialist Accepted'] else '❌'}</small>", unsafe_allow_html=True)
|
| 199 |
+
if st.button("View Summary", key=f"details_btn_{i}_{j}"):
|
| 200 |
+
st.session_state.selected_item = item
|
| 201 |
+
st.rerun()
|
| 202 |
+
|
| 203 |
+
else:
|
| 204 |
+
st.info("Please upload one or more images (up to 60) to start the batch analysis.")
|
requirements.txt
CHANGED
|
@@ -5,6 +5,8 @@ opencv-python-headless>=4.8.0
|
|
| 5 |
numpy>=1.24.0
|
| 6 |
Pillow>=9.5.0
|
| 7 |
scipy>=1.11.0
|
|
|
|
|
|
|
| 8 |
fastapi
|
| 9 |
uvicorn
|
| 10 |
python-multipart
|
|
|
|
| 5 |
numpy>=1.24.0
|
| 6 |
Pillow>=9.5.0
|
| 7 |
scipy>=1.11.0
|
| 8 |
+
streamlit>=1.32.0
|
| 9 |
+
pandas>=2.0.0
|
| 10 |
fastapi
|
| 11 |
uvicorn
|
| 12 |
python-multipart
|