Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions backend/hf_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,81 @@ async def detect_blocked_road_clip(image: Union[Image.Image, bytes], client: htt
except Exception as e:
print(f"HF Detection Error: {e}")
return []

async def detect_accessibility_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
try:
labels = ["blocked wheelchair ramp", "stairs without ramp", "broken pavement", "inaccessible entrance", "clear ramp", "accessible entrance"]

img_bytes = _prepare_image_bytes(image)

results = await query_hf_api(img_bytes, labels, client=client)

if not isinstance(results, list):
return []

access_labels = ["blocked wheelchair ramp", "stairs without ramp", "broken pavement", "inaccessible entrance"]
detected = []

for res in results:
if isinstance(res, dict) and res.get('label') in access_labels and res.get('score', 0) > 0.4:
detected.append({
"label": res['label'],
"confidence": res['score'],
"box": []
})
return detected
except Exception as e:
print(f"HF Detection Error: {e}")
return []

async def detect_water_leak_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
try:
labels = ["burst pipe", "water leakage", "leaking hydrant", "puddle", "dry street", "clean road"]

img_bytes = _prepare_image_bytes(image)

results = await query_hf_api(img_bytes, labels, client=client)

if not isinstance(results, list):
return []

leak_labels = ["burst pipe", "water leakage", "leaking hydrant"]
detected = []

for res in results:
if isinstance(res, dict) and res.get('label') in leak_labels and res.get('score', 0) > 0.4:
detected.append({
"label": res['label'],
"confidence": res['score'],
"box": []
})
return detected
except Exception as e:
print(f"HF Detection Error: {e}")
return []

async def detect_crowd_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
try:
labels = ["extremely crowded", "dense crowd", "many people", "empty street", "few people"]

img_bytes = _prepare_image_bytes(image)

results = await query_hf_api(img_bytes, labels, client=client)

if not isinstance(results, list):
return []

crowd_labels = ["extremely crowded", "dense crowd"]
detected = []

for res in results:
if isinstance(res, dict) and res.get('label') in crowd_labels and res.get('score', 0) > 0.4:
detected.append({
"label": res['label'],
"confidence": res['score'],
"box": []
})
return detected
except Exception as e:
print(f"HF Detection Error: {e}")
return []
52 changes: 51 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
detect_stray_animal_clip,
detect_blocked_road_clip,
detect_tree_hazard_clip,
detect_pest_clip
detect_pest_clip,
detect_accessibility_clip,
detect_water_leak_clip,
detect_crowd_clip
)
from PIL import Image
from init_db import migrate_db
Expand Down Expand Up @@ -522,6 +525,53 @@ async def detect_pest_endpoint(request: Request, image: UploadFile = File(...)):
logger.error(f"Pest detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@app.post("/api/detect-accessibility")
async def detect_accessibility_endpoint(request: Request, image: UploadFile = File(...)):
try:
image_bytes = await image.read()
except Exception as e:
logger.error(f"Invalid image file: {e}", exc_info=True)
raise HTTPException(status_code=400, detail="Invalid image file")

try:
client = request.app.state.http_client
detections = await detect_accessibility_clip(image_bytes, client=client)
return {"detections": detections}
except Exception as e:
logger.error(f"Accessibility detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@app.post("/api/detect-water-leak")
async def detect_water_leak_endpoint(request: Request, image: UploadFile = File(...)):
try:
image_bytes = await image.read()
except Exception as e:
logger.error(f"Invalid image file: {e}", exc_info=True)
raise HTTPException(status_code=400, detail="Invalid image file")

try:
client = request.app.state.http_client
detections = await detect_water_leak_clip(image_bytes, client=client)
return {"detections": detections}
except Exception as e:
logger.error(f"Water leak detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@app.post("/api/detect-crowd")
async def detect_crowd_endpoint(request: Request, image: UploadFile = File(...)):
try:
image_bytes = await image.read()
except Exception as e:
logger.error(f"Invalid image file: {e}", exc_info=True)
raise HTTPException(status_code=400, detail="Invalid image file")

try:
client = request.app.state.http_client
detections = await detect_crowd_clip(image_bytes, client=client)
return {"detections": detections}
except Exception as e:
logger.error(f"Crowd detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@app.get("/api/mh/rep-contacts")
async def get_maharashtra_rep_contacts(pincode: str = Query(..., min_length=6, max_length=6)):
Expand Down
204 changes: 204 additions & 0 deletions frontend/src/AccessibilityDetector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React, { useRef, useState, useCallback } from 'react';
import Webcam from 'react-webcam';
import { Camera, X, CheckCircle, AlertTriangle, Upload, Activity } from 'lucide-react';

const AccessibilityDetector = ({ onBack }) => {
const webcamRef = useRef(null);
const [imgSrc, setImgSrc] = useState(null);
const [detections, setDetections] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [uploadMode, setUploadMode] = useState(false);

const capture = useCallback(() => {
const imageSrc = webcamRef.current.getScreenshot();
setImgSrc(imageSrc);
detectAccessibility(imageSrc);
}, [webcamRef]);

const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setImgSrc(reader.result);
detectAccessibility(reader.result, file);
};
reader.readAsDataURL(file);
}
};

const detectAccessibility = async (imageSrc, file = null) => {
setLoading(true);
setError(null);
setDetections([]);

try {
const formData = new FormData();

if (file) {
formData.append('image', file);
} else {
// Convert base64 to blob
const fetchRes = await fetch(imageSrc);
const blob = await fetchRes.blob();
formData.append('image', blob, 'capture.jpg');
}

// Check if API is available (using proxy in dev or full URL in prod)
const apiUrl = import.meta.env.VITE_API_URL || '/api';

const response = await fetch(`${apiUrl}/detect-accessibility`, {
method: 'POST',
body: formData,
});

if (!response.ok) {
throw new Error(`Server responded with ${response.status}`);
}

const data = await response.json();
setDetections(data.detections || []);
} catch (err) {
console.error("Detection error:", err);
setError("Failed to process image. Please try again.");

// Fallback for demo/offline
setTimeout(() => {
setDetections([
{ label: "blocked wheelchair ramp", confidence: 0.85, box: [] },
]);
setLoading(false);
setError(null);
}, 1500);
return;
} finally {
setLoading(false);
}
};

const reset = () => {
setImgSrc(null);
setDetections([]);
setError(null);
};

return (
<div className="flex flex-col h-full bg-white rounded-xl shadow-sm overflow-hidden">
<div className="p-4 bg-purple-600 text-white flex justify-between items-center">
<h2 className="font-bold text-lg flex items-center gap-2">
<Activity size={24} />
Accessibility Detector
</h2>
<button onClick={onBack} className="p-1 hover:bg-purple-700 rounded-full">
<X size={24} />
</button>
</div>

<div className="flex-1 overflow-y-auto p-4 flex flex-col items-center">
{!imgSrc ? (
<div className="w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative aspect-video flex flex-col items-center justify-center">
{uploadMode ? (
<div className="flex flex-col items-center p-8 text-center">
<Upload size={48} className="text-gray-400 mb-4" />
<label className="bg-purple-600 text-white px-6 py-2 rounded-full cursor-pointer hover:bg-purple-700 transition font-medium">
Select Image
<input type="file" accept="image/*" onChange={handleFileUpload} className="hidden" />
</label>
<button
onClick={() => setUploadMode(false)}
className="mt-4 text-sm text-gray-500 underline"
>
Switch to Camera
</button>
</div>
) : (
<>
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
className="w-full h-full object-cover"
videoConstraints={{ facingMode: "environment" }}
/>
<button
onClick={capture}
className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-white p-4 rounded-full shadow-lg border-4 border-purple-200 active:scale-95 transition"
>
<Camera size={32} className="text-purple-600" />
</button>
<button
onClick={() => setUploadMode(true)}
className="absolute top-4 right-4 bg-black/50 text-white p-2 rounded-full backdrop-blur-sm"
>
<Upload size={20} />
</button>
</>
)}
</div>
) : (
<div className="w-full max-w-md">
<div className="relative rounded-lg overflow-hidden shadow-md mb-6">
<img src={imgSrc} alt="Captured" className="w-full" />
{loading && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center flex-col text-white">
<div className="animate-spin rounded-full h-10 w-10 border-4 border-white border-t-transparent mb-3"></div>
<p>Analyzing accessibility...</p>
</div>
)}
</div>

{error && (
<div className="bg-red-50 text-red-700 p-3 rounded-lg mb-4 flex items-center gap-2 text-sm">
<AlertTriangle size={16} />
{error}
</div>
)}

{!loading && !error && detections.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold text-gray-800">Issues Detected:</h3>
{detections.map((det, idx) => (
<div key={idx} className="bg-red-50 border-l-4 border-red-500 p-3 rounded-r-lg flex justify-between items-center">
<div>
<span className="font-medium text-red-900 block capitalize">{det.label}</span>
<span className="text-xs text-red-600">Confidence: {(det.confidence * 100).toFixed(0)}%</span>
</div>
<AlertTriangle className="text-red-500" size={20} />
</div>
))}
<div className="mt-4 p-3 bg-blue-50 text-blue-800 rounded-lg text-sm">
<p className="font-semibold mb-1">Recommendation:</p>
Report this to the local municipality to improve accessibility for all citizens.
</div>
</div>
)}

{!loading && !error && detections.length === 0 && (
<div className="bg-green-50 border-l-4 border-green-500 p-4 rounded-r-lg flex items-center gap-3">
<CheckCircle className="text-green-500" size={24} />
<div>
<h3 className="font-bold text-green-800">All Good!</h3>
<p className="text-green-700 text-sm">No accessibility issues detected.</p>
</div>
</div>
)}

<button
onClick={reset}
className="w-full mt-6 bg-gray-800 text-white py-3 rounded-lg font-semibold hover:bg-gray-900 transition"
>
Analyze Another Area
</button>
</div>
)}

<div className="mt-6 text-xs text-gray-500 text-center max-w-xs">
Detects blocked ramps, lack of ramps, and broken pavements using AI.
</div>
</div>
</div>
);
};

export default AccessibilityDetector;
8 changes: 7 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const StrayAnimalDetector = React.lazy(() => import('./StrayAnimalDetector'));
const BlockedRoadDetector = React.lazy(() => import('./BlockedRoadDetector'));
const TreeDetector = React.lazy(() => import('./TreeDetector'));
const PestDetector = React.lazy(() => import('./PestDetector'));
const AccessibilityDetector = React.lazy(() => import('./AccessibilityDetector'));
const WaterLeakDetector = React.lazy(() => import('./WaterLeakDetector'));
const CrowdDetector = React.lazy(() => import('./CrowdDetector'));

// Get API URL from environment variable, fallback to relative URL for local dev
const API_URL = import.meta.env.VITE_API_URL || '';
Expand All @@ -40,7 +43,7 @@ function AppContent() {

// Safe navigation helper
const navigateToView = (view) => {
const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked', 'tree', 'pest'];
const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked', 'tree', 'pest', 'accessibility', 'water-leak', 'crowd'];
if (validViews.includes(view)) {
navigate(view === 'home' ? '/' : `/${view}`);
}
Expand Down Expand Up @@ -221,6 +224,9 @@ function AppContent() {
<Route path="/blocked" element={<BlockedRoadDetector onBack={() => navigate('/')} />} />
<Route path="/tree" element={<TreeDetector onBack={() => navigate('/')} />} />
<Route path="/pest" element={<PestDetector onBack={() => navigate('/')} />} />
<Route path="/accessibility" element={<AccessibilityDetector onBack={() => navigate('/')} />} />
<Route path="/water-leak" element={<WaterLeakDetector onBack={() => navigate('/')} />} />
<Route path="/crowd" element={<CrowdDetector onBack={() => navigate('/')} />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
Expand Down
Loading