Final working front

This commit is contained in:
Suman991
2026-01-29 14:27:57 +05:30
parent a83545d1db
commit 93c1d763b5
4 changed files with 165 additions and 35 deletions

View File

@@ -65,6 +65,7 @@ export type DetectionData = {
export default function DetectionPage() { export default function DetectionPage() {
const [detectionData, setDetectionData] = useState<DetectionData | null>(null) const [detectionData, setDetectionData] = useState<DetectionData | null>(null)
const [videoId, setVideoId] = useState<string | null>(null) const [videoId, setVideoId] = useState<string | null>(null)
const [videoFile, setVideoFile] = useState<File | null>(null)
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection") const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
const getTitle = () => { const getTitle = () => {
@@ -93,20 +94,22 @@ export default function DetectionPage() {
{/* Upload Section */} {/* Upload Section */}
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100"> <div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
<UploadSection <UploadSection
onDetectionComplete={(data, vId) => { onDetectionComplete={(data, vId, file) => {
setDetectionData(data) setDetectionData(data)
setVideoId(vId) setVideoId(vId)
setVideoFile(file)
}} }}
onDetectionTypeChange={setDetectionType} onDetectionTypeChange={setDetectionType}
/> />
</div> </div>
{/* Video Player Section */} {/* Video Player Section */}
{detectionData && videoId && ( {detectionData && videoId && videoFile && (
<div className="mt-6 animate-in fade-in slide-in-from-bottom duration-700 delay-200"> <div className="mt-6 animate-in fade-in slide-in-from-bottom duration-700 delay-200">
<VideoPlayerSection <VideoPlayerSection
data={detectionData} data={detectionData}
videoId={videoId} videoId={videoId}
videoFile={videoFile}
detectionType={detectionType} detectionType={detectionType}
/> />
</div> </div>

View File

@@ -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 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" const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "ws://127.0.0.1:8000/api/v1"
type UploadSectionProps = { type UploadSectionProps = {
onDetectionComplete: (data: DetectionData, videoId: string) => void onDetectionComplete: (data: DetectionData, videoId: string, file: File) => void
onDetectionTypeChange: (type: DetectionType) => void onDetectionTypeChange: (type: DetectionType) => void
} }
@@ -110,8 +111,10 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
setStatusMessage("✓ Complete!") setStatusMessage("✓ Complete!")
setError(null) setError(null)
// Pass video_id instead of file // Pass detection data, video_id, and the original file
onDetectionComplete(detectionData, videoId) if (file) {
onDetectionComplete(detectionData, videoId, file)
}
} catch (error) { } catch (error) {
console.error("[Upload] Failed to load results:", error) console.error("[Upload] Failed to load results:", error)
setError("Failed to load results. Please try again.") setError("Failed to load results. Please try again.")
@@ -139,6 +142,10 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
try { try {
console.log("[Upload] Uploading to:", `${API_URL}/upload`) console.log("[Upload] Uploading to:", `${API_URL}/upload`)
console.log("[Upload] Detection type:", detectionType) 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`, { const response = await fetch(`${API_URL}/upload`, {
method: "POST", method: "POST",
@@ -159,6 +166,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
const videoId = result.video_id const videoId = result.video_id
console.log("[Upload] Video uploaded successfully, ID:", videoId) console.log("[Upload] Video uploaded successfully, ID:", videoId)
console.log("[Upload] Response:", result)
setStatusMessage("Uploaded! Starting processing...") setStatusMessage("Uploaded! Starting processing...")
setProgress(10) setProgress(10)

View File

@@ -12,6 +12,7 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000/api/v1
type VideoPlayerSectionProps = { type VideoPlayerSectionProps = {
data: DetectionData data: DetectionData
videoId: string videoId: string
videoFile: File
detectionType: DetectionType 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<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [currentFrame, setCurrentFrame] = useState(0) const [currentFrame, setCurrentFrame] = useState(0)
const [detectionsCount, setDetectionsCount] = useState(0) const [detectionsCount, setDetectionsCount] = useState(0)
const [logs, setLogs] = useState<DetectionLog[]>([]) const [logs, setLogs] = useState<DetectionLog[]>([])
@@ -147,23 +150,131 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid
frameDetectionMap.current = map frameDetectionMap.current = map
}, [data, isPothole]) }, [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(() => { useEffect(() => {
if (videoRef.current && videoId) { window.addEventListener('resize', resizeCanvas)
const videoUrl = `${API_URL}/video/${videoId}` return () => window.removeEventListener('resize', resizeCanvas)
console.log(`[VideoPlayer] Loading processed video from: ${videoUrl}`) }, [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 videoRef.current.src = videoUrl
// Handle video load errors // Handle video load errors
const handleError = () => { const handleError = () => {
console.error("[VideoPlayer] Failed to load processed video") console.error("[VideoPlayer] Failed to load video")
setVideoError("Failed to load processed video. Please try refreshing the page.") setVideoError("Failed to load video. Please try refreshing the page.")
} }
const handleLoaded = () => { const handleLoaded = () => {
console.log("[VideoPlayer] Processed video loaded successfully") console.log("[VideoPlayer] Video loaded successfully")
setVideoError(null) setVideoError(null)
resizeCanvas()
} }
videoRef.current.addEventListener("error", handleError) 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("error", handleError)
videoRef.current.removeEventListener("loadeddata", handleLoaded) 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 // Add detection log with deduplication and size limit
const addDetectionLog = useCallback((frame: number, detections: any[]) => { 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) const detections = frameDetectionMap.current.get(frame)
setDetectionsCount(detections?.length || 0) setDetectionsCount(detections?.length || 0)
// Draw bounding boxes for current frame
drawBoundingBoxes(detections || [])
if (detections && detections.length > 0) { if (detections && detections.length > 0) {
addDetectionLog(frame, detections) addDetectionLog(frame, detections)
} }
} }
}, [data.video_info.fps, addDetectionLog]) }, [data.video_info.fps, addDetectionLog, drawBoundingBoxes])
// Video playback monitoring // Video playback monitoring
useEffect(() => { useEffect(() => {
@@ -270,6 +386,9 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid
const detections = frameDetectionMap.current.get(frame) const detections = frameDetectionMap.current.get(frame)
setDetectionsCount(detections?.length || 0) setDetectionsCount(detections?.length || 0)
// Draw bounding boxes for current frame
drawBoundingBoxes(detections || [])
} }
const handleSeeked = () => { const handleSeeked = () => {
@@ -294,7 +413,7 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid
video.removeEventListener("seeked", handleSeeked) video.removeEventListener("seeked", handleSeeked)
video.removeEventListener("play", handlePlay) video.removeEventListener("play", handlePlay)
} }
}, [data.video_info.fps]) }, [data.video_info.fps, drawBoundingBoxes])
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -302,12 +421,12 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid
<CardHeader> <CardHeader>
<CardTitle>Video Playback with Detection</CardTitle> <CardTitle>Video Playback with Detection</CardTitle>
<CardDescription> <CardDescription>
Watch the processed video with {isPothole ? "pothole" : "signboard"} detections already drawn Watch the video with real-time {isPothole ? "pothole" : "signboard"} detection overlays
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Video Player */} {/* Video Player with Canvas Overlay */}
<div className="lg:col-span-2 space-y-4"> <div className="lg:col-span-2 space-y-4">
{videoError && ( {videoError && (
<div className="bg-destructive/10 text-destructive p-4 rounded-lg mb-4"> <div className="bg-destructive/10 text-destructive p-4 rounded-lg mb-4">
@@ -315,17 +434,27 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid
</div> </div>
)} )}
<div className="relative bg-black rounded-lg overflow-hidden"> <div
<video ref={containerRef}
ref={videoRef} className="relative bg-black rounded-lg overflow-hidden"
controls
className="w-full h-auto"
style={{ style={{
aspectRatio: `${data.video_info.width} / ${data.video_info.height}`, aspectRatio: `${data.video_info.width} / ${data.video_info.height}`,
}} }}
>
<video
ref={videoRef}
controls
className="w-full h-full"
> >
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
{/* Canvas overlay for bounding boxes */}
<canvas
ref={canvasRef}
className="absolute top-0 left-0 pointer-events-none"
style={{ width: '100%', height: '100%' }}
/>
</div> </div>
{/* Video Info */} {/* Video Info */}

14
package-lock.json generated
View File

@@ -2480,7 +2480,6 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -2491,7 +2490,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -2593,7 +2591,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -2856,8 +2853,7 @@
"version": "8.5.1", "version": "8.5.1",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==", "integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/embla-carousel-react": { "node_modules/embla-carousel-react": {
"version": "8.5.1", "version": "8.5.1",
@@ -3304,7 +3300,6 @@
"resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz",
"integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@next/env": "16.0.10", "@next/env": "16.0.10",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
@@ -3430,7 +3425,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -3468,7 +3462,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -3499,7 +3492,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -3512,7 +3504,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz",
"integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -3798,8 +3789,7 @@
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tailwindcss-animate": { "node_modules/tailwindcss-animate": {
"version": "1.0.7", "version": "1.0.7",