diff --git a/app/page.tsx b/app/page.tsx index 130a677..65148c7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -28,6 +28,8 @@ export type DetectionData = { first_detected_frame: number first_detected_time: number confidence: number + lat?: number + lng?: number }> signboard_list?: Array<{ signboard_id: number @@ -35,6 +37,8 @@ export type DetectionData = { first_detected_frame: number first_detected_time: number confidence: number + lat?: number + lng?: number }> frames: Array<{ frame_id: number diff --git a/components/upload-section.tsx b/components/upload-section.tsx index af31fa7..733fa68 100644 --- a/components/upload-section.tsx +++ b/components/upload-section.tsx @@ -21,6 +21,7 @@ type UploadSectionProps = { export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: UploadSectionProps) { const [file, setFile] = useState(null) + const [jsonFile, setJsonFile] = useState(null) const [speed, setSpeed] = useState(30) const [detectionType, setDetectionType] = useState("pothole-detection") const [uploading, setUploading] = useState(false) @@ -28,6 +29,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up const [statusMessage, setStatusMessage] = useState("") const [error, setError] = useState(null) const fileInputRef = useRef(null) + const jsonFileInputRef = useRef(null) const wsRef = useRef(null) const handleDetectionTypeChange = (value: DetectionType) => { @@ -133,6 +135,11 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up formData.append("file", file) formData.append("detection_type", detectionType) formData.append("speed_kmh", speed.toString()) + + // Add JSON file if provided + if (jsonFile) { + formData.append("json_file", jsonFile) + } setUploading(true) setProgress(0) @@ -199,7 +206,7 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up -
+
{/* File Input */}
@@ -224,6 +231,30 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up )}
+ {/* JSON File Input */} +
+ +
+ { + setJsonFile(e.target.files?.[0] || null) + setError(null) + }} + disabled={uploading} + className="flex-1" + /> +
+ {jsonFile && ( +

+ Selected: {jsonFile.name} ({(jsonFile.size / 1024).toFixed(2)} KB) +

+ )} +
+ {/* Detection Type Selector */}
@@ -238,13 +269,11 @@ export function UploadSection({ onDetectionComplete, onDetectionTypeChange }: Up
- 🕳️ Pothole Detection
- 🚦 Signboard Detection
diff --git a/components/video-player-section.tsx b/components/video-player-section.tsx index d1ae443..242c901 100644 --- a/components/video-player-section.tsx +++ b/components/video-player-section.tsx @@ -23,8 +23,10 @@ type DetectionLog = { type?: string // For signboards bbox: { x1: number; y1: number; x2: number; y2: number } confidence: number + latitude?: number + longitude?: number }> - timestamp: string + videoTime: string // Video timestamp (MM:SS) } function SummarySection({ data, show, detectionType }: { data: DetectionData; show: boolean; detectionType: DetectionType }) { @@ -120,6 +122,8 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection const [showSummary, setShowSummary] = useState(false) const [hasPlayedOnce, setHasPlayedOnce] = useState(false) const [videoError, setVideoError] = useState(null) + const [lastDetectedLat, setLastDetectedLat] = useState(null) + const [lastDetectedLng, setLastDetectedLng] = useState(null) const frameDetectionMap = useRef>(new Map()) const lastProcessedFrame = useRef(-1) const loggedFrames = useRef>(new Set()) @@ -128,6 +132,45 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection const isPothole = detectionType === "pothole-detection" const isSignboard = detectionType === "sign-board-detection" + // Create GPS coordinate map from signboard_list or pothole_list + const gpsMap = useRef>(new Map()) + + // Build GPS map from signboard_list or pothole_list + useEffect(() => { + const map = new Map() + + if (isPothole && data.pothole_list) { + data.pothole_list.forEach(item => { + if (item.lat !== undefined && item.lng !== undefined) { + map.set(item.pothole_id, { lat: item.lat, lng: item.lng }) + } + }) + } else if (isSignboard && data.signboard_list) { + data.signboard_list.forEach(item => { + if (item.lat !== undefined && item.lng !== undefined) { + map.set(item.signboard_id, { lat: item.lat, lng: item.lng }) + } + }) + } + + gpsMap.current = map + console.log(`[VideoPlayer] GPS map built with ${map.size} entries`) + }, [data, isPothole, isSignboard]) + + // Helper function to format video time from frame number + const formatVideoTime = useCallback((frame: number, fps: number): string => { + const seconds = frame / fps + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const secs = Math.floor(seconds % 60) + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` + } + return `${minutes}:${secs.toString().padStart(2, '0')}` + }, []) + + // Build optimized frame detection map useEffect(() => { const map = new Map() @@ -297,22 +340,40 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection loggedFrames.current.add(frame) + // Update last detected GPS coordinates if available + if (detections.length > 0) { + const detectionId = isPothole ? detections[0].pothole_id : detections[0].signboard_id + const gpsCoords = gpsMap.current.get(detectionId) + + if (gpsCoords) { + setLastDetectedLat(gpsCoords.lat) + setLastDetectedLng(gpsCoords.lng) + } + } + setLogs((prev) => { const newLog: DetectionLog = { frame, - detections: detections.map((det) => ({ - id: isPothole ? det.pothole_id : det.signboard_id, - type: isSignboard ? det.type : undefined, - bbox: det.bbox, - confidence: det.confidence, - })), - timestamp: new Date().toLocaleTimeString(), + detections: detections.map((det) => { + const detectionId = isPothole ? det.pothole_id : det.signboard_id + const gpsCoords = gpsMap.current.get(detectionId) + + return { + id: detectionId, + type: isSignboard ? det.type : undefined, + bbox: det.bbox, + confidence: det.confidence, + latitude: gpsCoords?.lat, + longitude: gpsCoords?.lng, + } + }), + videoTime: formatVideoTime(frame, data.video_info.fps), } const updated = [newLog, ...prev].slice(0, MAX_LOGS) return updated }) - }, [isPothole, isSignboard]) + }, [isPothole, isSignboard, formatVideoTime, data.video_info.fps]) // Track current frame and log detections const updateFrameInfo = useCallback(() => { @@ -479,6 +540,22 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection FPS: {data.video_info.fps.toFixed(1)}
+ {lastDetectedLat !== null && lastDetectedLng !== null && ( + <> +
+ Lat: + + {lastDetectedLat.toFixed(6)} + +
+
+ Lng: + + {lastDetectedLng.toFixed(6)} + +
+ + )}
@@ -506,7 +583,7 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection >
Frame: {log.frame} - {log.timestamp} + {log.videoTime}
{log.detections.length === 0 ? ( @@ -526,6 +603,11 @@ export default function VideoPlayerSection({ data, videoId, videoFile, detection
Coordinates: ({Math.round(det.bbox.x1)}, {Math.round(det.bbox.y1)}) → ({Math.round(det.bbox.x2)}, {Math.round(det.bbox.y2)})
+ {det.latitude !== undefined && det.longitude !== undefined && ( +
+ GPS: {det.latitude.toFixed(6)}, {det.longitude.toFixed(6)} +
+ )} ))}