Final working front
This commit is contained in:
@@ -65,6 +65,7 @@ export type DetectionData = {
|
||||
export default function DetectionPage() {
|
||||
const [detectionData, setDetectionData] = useState<DetectionData | null>(null)
|
||||
const [videoId, setVideoId] = useState<string | null>(null)
|
||||
const [videoFile, setVideoFile] = useState<File | null>(null)
|
||||
const [detectionType, setDetectionType] = useState<DetectionType>("pothole-detection")
|
||||
|
||||
const getTitle = () => {
|
||||
@@ -93,20 +94,22 @@ export default function DetectionPage() {
|
||||
{/* Upload Section */}
|
||||
<div className="animate-in fade-in slide-in-from-bottom duration-700 delay-100">
|
||||
<UploadSection
|
||||
onDetectionComplete={(data, vId) => {
|
||||
onDetectionComplete={(data, vId, file) => {
|
||||
setDetectionData(data)
|
||||
setVideoId(vId)
|
||||
setVideoFile(file)
|
||||
}}
|
||||
onDetectionTypeChange={setDetectionType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Video Player Section */}
|
||||
{detectionData && videoId && (
|
||||
{detectionData && videoId && videoFile && (
|
||||
<div className="mt-6 animate-in fade-in slide-in-from-bottom duration-700 delay-200">
|
||||
<VideoPlayerSection
|
||||
data={detectionData}
|
||||
videoId={videoId}
|
||||
videoFile={videoFile}
|
||||
detectionType={detectionType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<HTMLVideoElement>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [currentFrame, setCurrentFrame] = useState(0)
|
||||
const [detectionsCount, setDetectionsCount] = useState(0)
|
||||
const [logs, setLogs] = useState<DetectionLog[]>([])
|
||||
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
@@ -302,12 +421,12 @@ export default function VideoPlayerSection({ data, videoId, detectionType }: Vid
|
||||
<CardHeader>
|
||||
<CardTitle>Video Playback with Detection</CardTitle>
|
||||
<CardDescription>
|
||||
Watch the processed video with {isPothole ? "pothole" : "signboard"} detections already drawn
|
||||
Watch the video with real-time {isPothole ? "pothole" : "signboard"} detection overlays
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<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">
|
||||
{videoError && (
|
||||
<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 className="relative bg-black rounded-lg overflow-hidden">
|
||||
<video
|
||||
ref={videoRef}
|
||||
controls
|
||||
className="w-full h-auto"
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative bg-black rounded-lg overflow-hidden"
|
||||
style={{
|
||||
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.
|
||||
</video>
|
||||
|
||||
{/* Canvas overlay for bounding boxes */}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute top-0 left-0 pointer-events-none"
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Video Info */}
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user