From 93c1d763b55f13b10ec070c709106ebf5a83c139 Mon Sep 17 00:00:00 2001 From: Suman991 Date: Thu, 29 Jan 2026 14:27:57 +0530 Subject: [PATCH] Final working front --- app/page.tsx | 7 +- components/upload-section.tsx | 14 ++- components/video-player-section.tsx | 165 +++++++++++++++++++++++++--- package-lock.json | 14 +-- 4 files changed, 165 insertions(+), 35 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 11d7373..130a677 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -65,6 +65,7 @@ export type DetectionData = { export default function DetectionPage() { const [detectionData, setDetectionData] = useState(null) const [videoId, setVideoId] = useState(null) + const [videoFile, setVideoFile] = useState(null) const [detectionType, setDetectionType] = useState("pothole-detection") const getTitle = () => { @@ -93,20 +94,22 @@ export default function DetectionPage() { {/* Upload Section */}
{ + onDetectionComplete={(data, vId, file) => { setDetectionData(data) setVideoId(vId) + setVideoFile(file) }} onDetectionTypeChange={setDetectionType} />
{/* Video Player Section */} - {detectionData && videoId && ( + {detectionData && videoId && videoFile && (
diff --git a/components/upload-section.tsx b/components/upload-section.tsx index a12884e..af31fa7 100644 --- a/components/upload-section.tsx +++ b/components/upload-section.tsx @@ -13,8 +13,9 @@ import type { DetectionData, DetectionType } from "@/app/page" const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1" const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "ws://127.0.0.1:8000/api/v1" + type UploadSectionProps = { - onDetectionComplete: (data: DetectionData, videoId: string) => void + onDetectionComplete: (data: DetectionData, videoId: string, file: File) => void onDetectionTypeChange: (type: DetectionType) => void } @@ -110,8 +111,10 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up setStatusMessage("✓ Complete!") setError(null) - // Pass video_id instead of file - onDetectionComplete(detectionData, videoId) + // Pass detection data, video_id, and the original file + if (file) { + onDetectionComplete(detectionData, videoId, file) + } } catch (error) { console.error("[Upload] Failed to load results:", error) setError("Failed to load results. Please try again.") @@ -139,6 +142,10 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up try { console.log("[Upload] Uploading to:", `${API_URL}/upload`) console.log("[Upload] Detection type:", detectionType) + console.log("[Upload] FormData contents:") + console.log(" - file:", file.name) + console.log(" - detection_type:", detectionType) + console.log(" - speed_kmh:", speed) const response = await fetch(`${API_URL}/upload`, { method: "POST", @@ -159,6 +166,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up const videoId = result.video_id console.log("[Upload] Video uploaded successfully, ID:", videoId) + console.log("[Upload] Response:", result) setStatusMessage("Uploaded! Starting processing...") setProgress(10) diff --git a/components/video-player-section.tsx b/components/video-player-section.tsx index 675cd34..d1ae443 100644 --- a/components/video-player-section.tsx +++ b/components/video-player-section.tsx @@ -12,6 +12,7 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1 type VideoPlayerSectionProps = { data: DetectionData videoId: string + videoFile: File detectionType: DetectionType } @@ -109,8 +110,10 @@ function SummarySection({ data, show, detectionType }: { data: DetectionData; sh ) } -export default function VideoPlayerSection({ data, videoId, detectionType }: VideoPlayerSectionProps) { +export default function VideoPlayerSection({ data, videoId, videoFile, detectionType }: VideoPlayerSectionProps) { const videoRef = useRef(null) + const canvasRef = useRef(null) + const containerRef = useRef(null) const [currentFrame, setCurrentFrame] = useState(0) const [detectionsCount, setDetectionsCount] = useState(0) const [logs, setLogs] = useState([]) @@ -147,23 +150,131 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid frameDetectionMap.current = map }, [data, isPothole]) - // Load processed video from backend + // Function to draw bounding boxes on canvas + const drawBoundingBoxes = useCallback((detections: any[]) => { + const canvas = canvasRef.current + const video = videoRef.current + + if (!canvas || !video) return + + const ctx = canvas.getContext('2d') + if (!ctx) return + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height) + + if (!detections || detections.length === 0) return + + // Calculate scale between video natural size and displayed size + const scaleX = canvas.width / data.video_info.width + const scaleY = canvas.height / data.video_info.height + + detections.forEach((detection) => { + const bbox = detection.bbox + if (!bbox) return + + // Scale coordinates to match displayed video size + const x1 = bbox.x1 * scaleX + const y1 = bbox.y1 * scaleY + const x2 = bbox.x2 * scaleX + const y2 = bbox.y2 * scaleY + const width = x2 - x1 + const height = y2 - y1 + + // Set colors based on detection type + const boxColor = isPothole ? '#ef4444' : '#3b82f6' // red for potholes, blue for signboards + const textBgColor = isPothole ? 'rgba(239, 68, 68, 0.9)' : 'rgba(59, 130, 246, 0.9)' + + // Draw bounding box + ctx.strokeStyle = boxColor + ctx.lineWidth = 3 + ctx.strokeRect(x1, y1, width, height) + + // Draw semi-transparent fill + ctx.fillStyle = isPothole ? 'rgba(239, 68, 68, 0.15)' : 'rgba(59, 130, 246, 0.15)' + ctx.fillRect(x1, y1, width, height) + + // Prepare label text + const id = isPothole ? detection.pothole_id : detection.signboard_id + const confidence = (detection.confidence * 100).toFixed(1) + let labelText = isPothole + ? `Pothole #${id}` + : `${detection.type || 'Sign'} #${id}` + labelText += ` ${confidence}%` + + // Draw label background + ctx.font = 'bold 14px system-ui' + const textMetrics = ctx.measureText(labelText) + const textWidth = textMetrics.width + 16 + const textHeight = 26 + + ctx.fillStyle = textBgColor + ctx.fillRect(x1, y1 - textHeight - 2, textWidth, textHeight) + + // Draw label text + ctx.fillStyle = '#ffffff' + ctx.fillText(labelText, x1 + 8, y1 - 8) + }) + }, [data.video_info.width, data.video_info.height, isPothole]) + + // Resize canvas to match video display size + const resizeCanvas = useCallback(() => { + const video = videoRef.current + const canvas = canvasRef.current + const container = containerRef.current + + if (!video || !canvas || !container) return + + // Get the displayed size of the video + const rect = video.getBoundingClientRect() + canvas.width = rect.width + canvas.height = rect.height + + // Redraw current frame's detections + const frame = Math.round(video.currentTime * data.video_info.fps) + const detections = frameDetectionMap.current.get(frame) + if (detections) { + drawBoundingBoxes(detections) + } + }, [data.video_info.fps, drawBoundingBoxes]) + + // Handle window resize useEffect(() => { - if (videoRef.current && videoId) { - const videoUrl = `${API_URL}/video/${videoId}` - console.log(`[VideoPlayer] Loading processed video from: ${videoUrl}`) + window.addEventListener('resize', resizeCanvas) + return () => window.removeEventListener('resize', resizeCanvas) + }, [resizeCanvas]) + + // Initialize canvas size when video loads + useEffect(() => { + const video = videoRef.current + if (!video) return + + const handleLoadedMetadata = () => { + resizeCanvas() + } + + video.addEventListener('loadedmetadata', handleLoadedMetadata) + return () => video.removeEventListener('loadedmetadata', handleLoadedMetadata) + }, [resizeCanvas]) + + // Load video from uploaded file + useEffect(() => { + if (videoRef.current && videoFile) { + const videoUrl = URL.createObjectURL(videoFile) + console.log(`[VideoPlayer] Loading video from uploaded file`) videoRef.current.src = videoUrl // Handle video load errors const handleError = () => { - console.error("[VideoPlayer] Failed to load processed video") - setVideoError("Failed to load processed video. Please try refreshing the page.") + console.error("[VideoPlayer] Failed to load video") + setVideoError("Failed to load video. Please try refreshing the page.") } const handleLoaded = () => { - console.log("[VideoPlayer] Processed video loaded successfully") + console.log("[VideoPlayer] Video loaded successfully") setVideoError(null) + resizeCanvas() } videoRef.current.addEventListener("error", handleError) @@ -174,9 +285,11 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid videoRef.current.removeEventListener("error", handleError) videoRef.current.removeEventListener("loadeddata", handleLoaded) } + // Revoke object URL to free memory + URL.revokeObjectURL(videoUrl) } } - }, [videoId]) + }, [videoFile, resizeCanvas]) // Add detection log with deduplication and size limit const addDetectionLog = useCallback((frame: number, detections: any[]) => { @@ -215,11 +328,14 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid const detections = frameDetectionMap.current.get(frame) setDetectionsCount(detections?.length || 0) + // Draw bounding boxes for current frame + drawBoundingBoxes(detections || []) + if (detections && detections.length > 0) { addDetectionLog(frame, detections) } } - }, [data.video_info.fps, addDetectionLog]) + }, [data.video_info.fps, addDetectionLog, drawBoundingBoxes]) // Video playback monitoring useEffect(() => { @@ -270,6 +386,9 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid const detections = frameDetectionMap.current.get(frame) setDetectionsCount(detections?.length || 0) + + // Draw bounding boxes for current frame + drawBoundingBoxes(detections || []) } const handleSeeked = () => { @@ -294,7 +413,7 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid video.removeEventListener("seeked", handleSeeked) video.removeEventListener("play", handlePlay) } - }, [data.video_info.fps]) + }, [data.video_info.fps, drawBoundingBoxes]) return (
@@ -302,12 +421,12 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid Video Playback with Detection - Watch the processed video with {isPothole ? "pothole" : "signboard"} detections already drawn + Watch the video with real-time {isPothole ? "pothole" : "signboard"} detection overlays
- {/* Video Player */} + {/* Video Player with Canvas Overlay */}
{videoError && (
@@ -315,17 +434,27 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid
)} -
+
+ + {/* Canvas overlay for bounding boxes */} +
{/* Video Info */} diff --git a/package-lock.json b/package-lock.json index a2e396f..a69bee4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2480,7 +2480,6 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2491,7 +2490,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2593,7 +2591,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2856,8 +2853,7 @@ "version": "8.5.1", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-react": { "version": "8.5.1", @@ -3304,7 +3300,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.0.10", "@swc/helpers": "0.5.15", @@ -3430,7 +3425,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3468,7 +3462,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3499,7 +3492,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3512,7 +3504,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -3798,8 +3789,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7",