mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 07:06:35 +00:00
annotation save fixed
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
const saveAnnotation = async (time, annotations, imageData) => {
|
|
||||||
// Implement save logic here, including YOLO format conversion
|
|
||||||
console.log("Saving annotation", time, annotations, imageData);
|
|
||||||
//Save the image data to a folder.
|
|
||||||
//Save the annotations to a .txt file in YOLO format.
|
|
||||||
};
|
|
||||||
|
|
||||||
export default saveAnnotation;
|
|
||||||
@@ -2,12 +2,88 @@ import React from 'react';
|
|||||||
|
|
||||||
function AnnotationControls({ onFrameBackward, onPlayPause, isPlaying, onFrameForward, onSaveAnnotation, onDelete }) {
|
function AnnotationControls({ onFrameBackward, onPlayPause, isPlaying, onFrameForward, onSaveAnnotation, onDelete }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', position: 'relative', zIndex: 1 }}>
|
<div style={{
|
||||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onFrameBackward}></button>
|
display: 'flex',
|
||||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onPlayPause}>{isPlaying ? 'Pause' : 'Play'}</button>
|
justifyContent: 'center',
|
||||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onFrameForward}></button>
|
position: 'relative',
|
||||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onSaveAnnotation}>Save Annotation</button>
|
zIndex: 1,
|
||||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onDelete}>Delete</button>
|
padding: '10px',
|
||||||
|
background: '#f5f5f5',
|
||||||
|
borderRadius: '4px',
|
||||||
|
marginTop: '10px'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: '16px',
|
||||||
|
margin: '0 5px',
|
||||||
|
background: '#e0e0e0',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={onFrameBackward}
|
||||||
|
title="Previous Frame"
|
||||||
|
>
|
||||||
|
⏮️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: '16px',
|
||||||
|
margin: '0 5px',
|
||||||
|
background: isPlaying ? '#ff8a8a' : '#8aff8a',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={onPlayPause}
|
||||||
|
>
|
||||||
|
{isPlaying ? '⏸️ Pause' : '▶️ Play'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: '16px',
|
||||||
|
margin: '0 5px',
|
||||||
|
background: '#e0e0e0',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={onFrameForward}
|
||||||
|
title="Next Frame"
|
||||||
|
>
|
||||||
|
⏭️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: '16px',
|
||||||
|
margin: '0 5px',
|
||||||
|
background: '#8ad4ff',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={onSaveAnnotation}
|
||||||
|
>
|
||||||
|
💾 Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: '16px',
|
||||||
|
margin: '0 5px',
|
||||||
|
background: '#ffb38a',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import VideoPlayer from './VideoPlayer';
|
import VideoPlayer from './VideoPlayer';
|
||||||
import AnnotationList from './AnnotationList';
|
import AnnotationList from './AnnotationList';
|
||||||
import MediaList from './MediaList';
|
import MediaList from './MediaList';
|
||||||
import DetectionClassList from './DetectionClassList';
|
import DetectionClassList from './DetectionClassList';
|
||||||
import CanvasEditor from './CanvasEditor';
|
import CanvasEditor from './CanvasEditor';
|
||||||
import * as AnnotationService from '../services/AnnotationService';
|
import * as AnnotationService from '../services/AnnotationService';
|
||||||
import AnnotationControls from './AnnotationControls'; // Import the new component
|
import AnnotationControls from './AnnotationControls';
|
||||||
|
import saveAnnotation from '../services/DataHandler';
|
||||||
|
|
||||||
function AnnotationMain() {
|
function AnnotationMain() {
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
@@ -15,8 +16,13 @@ function AnnotationMain() {
|
|||||||
const [selectedClass, setSelectedClass] = useState(null);
|
const [selectedClass, setSelectedClass] = useState(null);
|
||||||
const [detections, setDetections] = useState([]);
|
const [detections, setDetections] = useState([]);
|
||||||
const [selectedDetectionIndices, setSelectedDetectionIndices] = useState([]);
|
const [selectedDetectionIndices, setSelectedDetectionIndices] = useState([]);
|
||||||
const [isPlaying, setIsPlaying] = useState(false); // Add isPlaying state here
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const videoRef = React.createRef();
|
const [videoWidth, setVideoWidth] = useState(640);
|
||||||
|
const [videoHeight, setVideoHeight] = useState(480);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
|
const videoRef = useRef(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialFiles = [];
|
const initialFiles = [];
|
||||||
@@ -24,49 +30,87 @@ function AnnotationMain() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFileSelect = (file) => {
|
const handleFileSelect = (file) => {
|
||||||
console.log("handleFileSelect called with:", file);
|
if (!file) return;
|
||||||
|
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
setAnnotations({});
|
setAnnotations({});
|
||||||
setDetections([]);
|
setDetections([]);
|
||||||
setSelectedDetectionIndices([]);
|
setSelectedDetectionIndices([]);
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
setIsPlaying(false); // Reset playing state
|
setIsPlaying(false);
|
||||||
|
setErrorMessage("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropNewFiles = (newFiles) => {
|
const handleDropNewFiles = (newFiles) => {
|
||||||
setFiles(prevFiles => [...prevFiles, ...newFiles]);
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
if (!selectedFile) {
|
|
||||||
setSelectedFile(newFiles[0]);
|
const validFiles = [...newFiles];
|
||||||
|
setFiles(prevFiles => [...prevFiles, ...validFiles]);
|
||||||
|
|
||||||
|
if (!selectedFile && validFiles.length > 0) {
|
||||||
|
setSelectedFile(validFiles[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAnnotationSave = () => {
|
const handleAnnotationSave = () => {
|
||||||
const containerRef = { current: { offsetWidth: videoRef.current.videoWidth, offsetHeight: videoRef.current.videoHeight } };
|
if (!videoRef.current) return;
|
||||||
const imageData = AnnotationService.createAnnotationImage(videoRef, detections, null, selectedClass, containerRef);
|
|
||||||
|
if (!detections || detections.length === 0) {
|
||||||
|
setErrorMessage("Please create at least one detection before saving");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeContainerRef = {
|
||||||
|
current: {
|
||||||
|
offsetWidth: videoWidth,
|
||||||
|
offsetHeight: videoHeight
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageData = AnnotationService.createAnnotationImage(
|
||||||
|
videoRef,
|
||||||
|
detections,
|
||||||
|
safeContainerRef
|
||||||
|
);
|
||||||
|
|
||||||
if (imageData) {
|
if (imageData) {
|
||||||
setAnnotations(prevAnnotations => ({
|
setAnnotations(prevAnnotations => {
|
||||||
...prevAnnotations,
|
const newAnnotations = {
|
||||||
[currentTime]: { time: currentTime, detections: detections, imageData },
|
...prevAnnotations,
|
||||||
}));
|
[currentTime]: { time: currentTime, annotations: detections, imageData }
|
||||||
|
};
|
||||||
|
|
||||||
|
saveAnnotation(currentTime, detections, imageData);
|
||||||
|
setErrorMessage("");
|
||||||
|
|
||||||
|
return newAnnotations;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
|
if (selectedDetectionIndices.length === 0) {
|
||||||
|
setErrorMessage("Please select a detection to delete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newDetections = detections.filter((_, index) => !selectedDetectionIndices.includes(index));
|
const newDetections = detections.filter((_, index) => !selectedDetectionIndices.includes(index));
|
||||||
setDetections(newDetections);
|
setDetections(newDetections);
|
||||||
|
setSelectedDetectionIndices([]);
|
||||||
|
setErrorMessage("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAnnotationClick = (time) => {
|
const handleAnnotationClick = (time) => {
|
||||||
setCurrentTime(time);
|
setCurrentTime(time);
|
||||||
const annotation = annotations[time];
|
const annotation = annotations[time];
|
||||||
if (annotation) {
|
if (annotation) {
|
||||||
setDetections(annotation.detections);
|
setDetections(annotation.annotations || []);
|
||||||
setSelectedDetectionIndices([]);
|
setSelectedDetectionIndices([]);
|
||||||
}
|
}
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.currentTime = time;
|
videoRef.current.currentTime = time;
|
||||||
}
|
}
|
||||||
setIsPlaying(false); // Pause when clicking an annotation
|
setIsPlaying(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClassSelect = (cls) => {
|
const handleClassSelect = (cls) => {
|
||||||
@@ -81,71 +125,144 @@ function AnnotationMain() {
|
|||||||
setSelectedDetectionIndices(newSelection);
|
setSelectedDetectionIndices(newSelection);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlayPause = () => { // Moved from VideoPlayer
|
const handlePlayPause = () => {
|
||||||
setIsPlaying(prev => !prev);
|
setIsPlaying(prev => !prev);
|
||||||
if (videoRef.current) {
|
|
||||||
if (isPlaying) {
|
|
||||||
videoRef.current.pause();
|
|
||||||
} else {
|
|
||||||
videoRef.current.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFrameForward = () => { // Moved from VideoPlayer
|
const handleFrameForward = () => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.currentTime += 1 / 30;
|
videoRef.current.currentTime += 1 / 30;
|
||||||
setCurrentTime(videoRef.current.currentTime)
|
setCurrentTime(videoRef.current.currentTime);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFrameBackward = () => { // Moved from VideoPlayer
|
const handleFrameBackward = () => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.currentTime -= 1 / 30;
|
videoRef.current.currentTime -= 1 / 30;
|
||||||
setCurrentTime(videoRef.current.currentTime)
|
setCurrentTime(videoRef.current.currentTime);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSizeChanged = (width, height) => {
|
||||||
|
setVideoWidth(width);
|
||||||
|
setVideoHeight(height);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetCurrentTime = (time) => {
|
||||||
|
setCurrentTime(time);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle debug mode with Ctrl+D
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Space':
|
||||||
|
|
||||||
|
}
|
||||||
|
if (e.ctrlKey && e.key === 'd') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
|
||||||
<div style={{ width: '15%', paddingRight: '10px', display: 'flex', flexDirection: 'column' }}>
|
<div style={{
|
||||||
|
width: '15%',
|
||||||
|
height: '100%',
|
||||||
|
overflowY: 'auto',
|
||||||
|
borderRight: '1px solid #ccc',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}>
|
||||||
<MediaList
|
<MediaList
|
||||||
files={files}
|
files={files}
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
onDropNewFiles={handleDropNewFiles}
|
onDropNewFiles={handleDropNewFiles}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: 'auto' }}>
|
<div style={{ flexGrow: 1 }}>
|
||||||
<DetectionClassList onClassSelect={handleClassSelect} />
|
<DetectionClassList onClassSelect={handleClassSelect} />
|
||||||
</div>
|
</div>
|
||||||
<AnnotationList annotations={Object.values(annotations)} onAnnotationClick={handleAnnotationClick} />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '85%' }}>
|
|
||||||
<VideoPlayer
|
<div style={{
|
||||||
videoFile={selectedFile}
|
width: '70%',
|
||||||
currentTime={currentTime}
|
height: '100%',
|
||||||
videoRef={videoRef}
|
display: 'flex',
|
||||||
isPlaying = {isPlaying}
|
flexDirection: 'column'
|
||||||
>
|
}}>
|
||||||
<CanvasEditor
|
{errorMessage && (
|
||||||
width={videoRef.current ? videoRef.current.videoWidth : 0}
|
<div style={{
|
||||||
height={videoRef.current ? videoRef.current.videoHeight : 0}
|
backgroundColor: '#ffdddd',
|
||||||
detections={detections}
|
color: '#d8000c',
|
||||||
selectedDetectionIndices={selectedDetectionIndices}
|
padding: '6px',
|
||||||
onDetectionsChange={handleDetectionsChange}
|
margin: '6px',
|
||||||
onSelectionChange={handleSelectionChange}
|
borderRadius: '4px'
|
||||||
detectionClass={selectedClass}
|
}}>
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: 0
|
||||||
|
}} ref={containerRef}>
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}>
|
||||||
|
<VideoPlayer
|
||||||
|
videoFile={selectedFile}
|
||||||
|
currentTime={currentTime}
|
||||||
|
videoRef={videoRef}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onSizeChanged={handleSizeChanged}
|
||||||
|
onSetCurrentTime={handleSetCurrentTime}
|
||||||
|
>
|
||||||
|
<CanvasEditor
|
||||||
|
width={videoWidth}
|
||||||
|
height={videoHeight}
|
||||||
|
detections={detections}
|
||||||
|
selectedDetectionIndices={selectedDetectionIndices}
|
||||||
|
onDetectionsChange={handleDetectionsChange}
|
||||||
|
onSelectionChange={handleSelectionChange}
|
||||||
|
detectionClass={selectedClass}
|
||||||
|
/>
|
||||||
|
</VideoPlayer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnnotationControls
|
||||||
|
onFrameBackward={handleFrameBackward}
|
||||||
|
onPlayPause={handlePlayPause}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onFrameForward={handleFrameForward}
|
||||||
|
onSaveAnnotation={handleAnnotationSave}
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
</VideoPlayer>
|
</div>
|
||||||
<AnnotationControls
|
</div>
|
||||||
onFrameBackward={handleFrameBackward}
|
|
||||||
onPlayPause={handlePlayPause}
|
<div style={{
|
||||||
isPlaying={isPlaying}
|
width: '15%',
|
||||||
onFrameForward={handleFrameForward}
|
height: '100%',
|
||||||
onSaveAnnotation={handleAnnotationSave}
|
overflowY: 'auto',
|
||||||
onDelete={handleDelete}
|
borderLeft: '1px solid #ccc'
|
||||||
|
}}>
|
||||||
|
<AnnotationList
|
||||||
|
annotations={Object.values(annotations)}
|
||||||
|
onAnnotationClick={handleAnnotationClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ function CanvasEditor({
|
|||||||
const [resizeData, setResizeData] = useState(null);
|
const [resizeData, setResizeData] = useState(null);
|
||||||
const [localDetections, setLocalDetections] = useState(detections || []);
|
const [localDetections, setLocalDetections] = useState(detections || []);
|
||||||
const [localSelectedIndices, setLocalSelectedIndices] = useState(selectedDetectionIndices || []);
|
const [localSelectedIndices, setLocalSelectedIndices] = useState(selectedDetectionIndices || []);
|
||||||
|
const [dimensions, setDimensions] = useState({ width: width || 640, height: height || 480 });
|
||||||
|
|
||||||
|
// Track if we're in a dragging operation
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (width && height) {
|
||||||
|
setDimensions({ width, height });
|
||||||
|
}
|
||||||
|
}, [width, height]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalDetections(detections || []);
|
setLocalDetections(detections || []);
|
||||||
@@ -58,8 +68,9 @@ function CanvasEditor({
|
|||||||
x: mouseX - localDetections[i].x1,
|
x: mouseX - localDetections[i].x1,
|
||||||
y: mouseY - localDetections[i].y1,
|
y: mouseY - localDetections[i].y1,
|
||||||
});
|
});
|
||||||
|
setIsDragging(true);
|
||||||
detectionFound = true;
|
detectionFound = true;
|
||||||
break; // Stop the loop once a detection is found
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,10 +93,10 @@ function CanvasEditor({
|
|||||||
|
|
||||||
if (localSelectedIndices.length > 0 && mouseDownPos && !resizeData) {
|
if (localSelectedIndices.length > 0 && mouseDownPos && !resizeData) {
|
||||||
// Dragging logic
|
// Dragging logic
|
||||||
|
setIsDragging(true);
|
||||||
const newDetections = [...localDetections];
|
const newDetections = [...localDetections];
|
||||||
const firstSelectedIndex = localSelectedIndices[0];
|
const firstSelectedIndex = localSelectedIndices[0];
|
||||||
|
|
||||||
// Check for valid index before accessing.
|
|
||||||
if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return;
|
if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return;
|
||||||
|
|
||||||
const firstSelectedDetection = newDetections[firstSelectedIndex];
|
const firstSelectedDetection = newDetections[firstSelectedIndex];
|
||||||
@@ -94,7 +105,6 @@ function CanvasEditor({
|
|||||||
const deltaY = newY1 - firstSelectedDetection.y1;
|
const deltaY = newY1 - firstSelectedDetection.y1;
|
||||||
|
|
||||||
localSelectedIndices.forEach(index => {
|
localSelectedIndices.forEach(index => {
|
||||||
// Check for valid index before accessing.
|
|
||||||
if (newDetections[index] === undefined) return;
|
if (newDetections[index] === undefined) return;
|
||||||
|
|
||||||
const detection = newDetections[index];
|
const detection = newDetections[index];
|
||||||
@@ -112,17 +122,17 @@ function CanvasEditor({
|
|||||||
|
|
||||||
setLocalDetections(newDetections);
|
setLocalDetections(newDetections);
|
||||||
if (onDetectionsChange) {
|
if (onDetectionsChange) {
|
||||||
onDetectionsChange(newDetections); // Notify about changes
|
onDetectionsChange(newDetections);
|
||||||
}
|
}
|
||||||
} else if (currentDetection && !resizeData) {
|
} else if (currentDetection && !resizeData) {
|
||||||
// Drawing a new detection.
|
|
||||||
setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY }));
|
setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY }));
|
||||||
} else if (resizeData) {
|
} else if (resizeData) {
|
||||||
|
setIsDragging(true);
|
||||||
const { index, position } = resizeData;
|
const { index, position } = resizeData;
|
||||||
if (localDetections[index] === undefined) return;
|
if (localDetections[index] === undefined) return;
|
||||||
|
|
||||||
const newDetections = [...localDetections];
|
const newDetections = [...localDetections];
|
||||||
const detection = newDetections[index];
|
const detection = { ...newDetections[index] };
|
||||||
const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef);
|
const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef);
|
||||||
newDetections[index] = updatedDetection;
|
newDetections[index] = updatedDetection;
|
||||||
setLocalDetections(newDetections);
|
setLocalDetections(newDetections);
|
||||||
@@ -132,13 +142,27 @@ function CanvasEditor({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = (e) => {
|
||||||
|
// If we're dragging (or resizing), stop propagation to prevent other elements from reacting
|
||||||
|
if (isDragging || resizeData) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
if (currentDetection && mouseDownPos) {
|
if (currentDetection && mouseDownPos) {
|
||||||
const dx = Math.abs(currentDetection.x2 - currentDetection.x1);
|
const dx = Math.abs(currentDetection.x2 - currentDetection.x1);
|
||||||
const dy = Math.abs(currentDetection.y2 - currentDetection.y1);
|
const dy = Math.abs(currentDetection.y2 - currentDetection.y1);
|
||||||
|
|
||||||
if (dx > 5 && dy > 5) {
|
if (dx > 5 && dy > 5) {
|
||||||
const newDetections = [...localDetections, currentDetection];
|
// Normalize coordinates so x1,y1 is always top-left and x2,y2 is bottom-right
|
||||||
|
const normalizedDetection = {
|
||||||
|
...currentDetection,
|
||||||
|
x1: Math.min(currentDetection.x1, currentDetection.x2),
|
||||||
|
y1: Math.min(currentDetection.y1, currentDetection.y2),
|
||||||
|
x2: Math.max(currentDetection.x1, currentDetection.x2),
|
||||||
|
y2: Math.max(currentDetection.y1, currentDetection.y2),
|
||||||
|
};
|
||||||
|
|
||||||
|
const newDetections = [...localDetections, normalizedDetection];
|
||||||
setLocalDetections(newDetections);
|
setLocalDetections(newDetections);
|
||||||
if (onDetectionsChange) {
|
if (onDetectionsChange) {
|
||||||
onDetectionsChange(newDetections);
|
onDetectionsChange(newDetections);
|
||||||
@@ -150,6 +174,7 @@ function CanvasEditor({
|
|||||||
setMouseDownPos(null);
|
setMouseDownPos(null);
|
||||||
setDragOffset({ x: 0, y: 0 });
|
setDragOffset({ x: 0, y: 0 });
|
||||||
setResizeData(null);
|
setResizeData(null);
|
||||||
|
setIsDragging(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDetectionMouseDown = (e, index) => {
|
const handleDetectionMouseDown = (e, index) => {
|
||||||
@@ -172,8 +197,8 @@ function CanvasEditor({
|
|||||||
x: mouseX - localDetections[index].x1,
|
x: mouseX - localDetections[index].x1,
|
||||||
y: mouseY - localDetections[index].y1,
|
y: mouseY - localDetections[index].y1,
|
||||||
});
|
});
|
||||||
setMouseDownPos({x: mouseX, y: mouseY})
|
setMouseDownPos({x: mouseX, y: mouseY});
|
||||||
|
setIsDragging(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = (e, index, position) => {
|
const handleResize = (e, index, position) => {
|
||||||
@@ -190,20 +215,44 @@ function CanvasEditor({
|
|||||||
onSelectionChange && onSelectionChange(newSelectedIndices);
|
onSelectionChange && onSelectionChange(newSelectedIndices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setIsDragging(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add a document-level mouse move and up handler for dragging outside container
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging || resizeData) {
|
||||||
|
const handleDocumentMouseMove = (e) => {
|
||||||
|
handleMouseMove(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDocumentMouseUp = (e) => {
|
||||||
|
handleMouseUp(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleDocumentMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleDocumentMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleDocumentMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleDocumentMouseUp);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isDragging, resizeData, mouseDownPos]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'absolute',
|
||||||
width: `${width}px`,
|
width: '100%',
|
||||||
height: `${height}px`,
|
height: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={containerRef}
|
<div ref={containerRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// src/components/Detection.js
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) { // Corrected prop name
|
function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
||||||
if (!detection || !detection.class) {
|
if (!detection || !detection.class) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -14,8 +13,13 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use startsWith to correctly handle RGBA and hex colors
|
// Use startsWith to correctly handle RGBA and hex colors
|
||||||
const backgroundColor = color.startsWith('rgba') ? color : color.replace('1', '0.4'); // Ensure opacity for background
|
const backgroundColor = color.startsWith('rgba')
|
||||||
const borderColor = color.startsWith('rgba') ? color.replace('0.4', '1') : color; // Ensure full opacity for border
|
? color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 0.4)')
|
||||||
|
: color.replace(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/, 'rgba($1, $2, $3, 0.4)');
|
||||||
|
|
||||||
|
const borderColor = color.startsWith('rgba')
|
||||||
|
? color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)')
|
||||||
|
: color;
|
||||||
|
|
||||||
const resizeHandleSize = 8;
|
const resizeHandleSize = 8;
|
||||||
|
|
||||||
@@ -36,21 +40,22 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
|||||||
top: `${detection.y1}px`,
|
top: `${detection.y1}px`,
|
||||||
width: `${detection.x2 - detection.x1}px`,
|
width: `${detection.x2 - detection.x1}px`,
|
||||||
height: `${detection.y2 - detection.y1}px`,
|
height: `${detection.y2 - detection.y1}px`,
|
||||||
backgroundColor: backgroundColor, // Use the calculated backgroundColor
|
backgroundColor: backgroundColor,
|
||||||
border: `2px solid ${borderColor}`, // Use the calculated borderColor
|
border: `2px solid ${borderColor}`,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
cursor: isSelected ? 'move' : 'default',
|
cursor: isSelected ? 'move' : 'default',
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
|
zIndex: isSelected ? 2 : 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
style.border = `3px solid black`;
|
style.border = `3px solid black`;
|
||||||
style.boxShadow = `0 0 4px 2px ${borderColor}`; // Use calculated border color
|
style.boxShadow = `0 0 4px 2px ${borderColor}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onDetectionMouseDown(e); // Corrected prop name
|
onDetectionMouseDown(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResizeMouseDown = (e, position) => {
|
const handleResizeMouseDown = (e, position) => {
|
||||||
@@ -73,11 +78,22 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
|||||||
backgroundColor: 'black',
|
backgroundColor: 'black',
|
||||||
cursor: handle.cursor,
|
cursor: handle.cursor,
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
|
zIndex: 3,
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => handleResizeMouseDown(e, handle.position)}
|
onMouseDown={(e) => handleResizeMouseDown(e, handle.position)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<span style={{ color: 'white', fontSize: '12px', position: "absolute", top: "-18px", left: "0px" }}>{detection.class.Name}</span>
|
<span style={{
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '12px',
|
||||||
|
position: "absolute",
|
||||||
|
top: "-18px",
|
||||||
|
left: "0px",
|
||||||
|
textShadow: '1px 1px 2px black',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}>
|
||||||
|
{detection.class.Name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// --- START OF FILE DetectionClassList.js --- (No changes needed)
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import DetectionClass from '../models/DetectionClass';
|
import DetectionClass from '../models/DetectionClass';
|
||||||
|
|
||||||
@@ -6,7 +5,7 @@ function DetectionClassList({ onClassSelect }) {
|
|||||||
const [detectionClasses, setDetectionClasses] = useState([]);
|
const [detectionClasses, setDetectionClasses] = useState([]);
|
||||||
const [selectedClass, setSelectedClass] = useState(null);
|
const [selectedClass, setSelectedClass] = useState(null);
|
||||||
|
|
||||||
const colors = [ // Define colors *inside* the component
|
const colors = [
|
||||||
"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000",
|
"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000",
|
||||||
"#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#808080",
|
"#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#808080",
|
||||||
"#C00000", "#00C000", "#0000C0", "#C0C000", "#C000C0", "#00C0C0", "#C0C0C0",
|
"#C00000", "#00C000", "#0000C0", "#C0C000", "#C000C0", "#00C0C0", "#C0C0C0",
|
||||||
@@ -17,7 +16,6 @@ function DetectionClassList({ onClassSelect }) {
|
|||||||
"#E00000", "#00E000", "#0000E0", "#E0E000", "#E000E0", "#00E0E0", "#E0E0E0"
|
"#E00000", "#00E000", "#0000E0", "#E0E000", "#E000E0", "#00E0E0", "#E0E0E0"
|
||||||
];
|
];
|
||||||
|
|
||||||
// Calculate color with opacity
|
|
||||||
const calculateColor = (id, opacity = '0.2') => {
|
const calculateColor = (id, opacity = '0.2') => {
|
||||||
const hexColor = colors[id % colors.length];
|
const hexColor = colors[id % colors.length];
|
||||||
const r = parseInt(hexColor.slice(1, 3), 16);
|
const r = parseInt(hexColor.slice(1, 3), 16);
|
||||||
@@ -27,24 +25,60 @@ function DetectionClassList({ onClassSelect }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('config.json') // Make sure this path is correct
|
const defaultClasses = [
|
||||||
.then(response => response.json())
|
{ Id: 0, Name: "Car" },
|
||||||
.then(data => {
|
{ Id: 1, Name: "Person" },
|
||||||
const detectionClasses = data.classes.map(cls => {
|
{ Id: 2, Name: "Truck" },
|
||||||
const color = calculateColor(cls.Id, '1'); // Full opacity for border
|
{ Id: 3, Name: "Bicycle" },
|
||||||
return new DetectionClass(cls.Id, cls.Name, color);
|
{ Id: 4, Name: "Motorcycle" },
|
||||||
});
|
{ Id: 5, Name: "Bus" }
|
||||||
setDetectionClasses(detectionClasses);
|
];
|
||||||
|
|
||||||
if (detectionClasses.length > 0 && onClassSelect) {
|
try {
|
||||||
onClassSelect(detectionClasses[0]); // Select the first class by default
|
fetch('config.json')
|
||||||
}
|
.then(response => response.json())
|
||||||
})
|
.then(data => {
|
||||||
.catch(error => console.error("Error loading detection classes:", error));
|
const classes = data.classes.map(cls => {
|
||||||
});
|
const color = calculateColor(cls.Id, '1');
|
||||||
|
return new DetectionClass(cls.Id, cls.Name, color);
|
||||||
|
});
|
||||||
|
setDetectionClasses(classes);
|
||||||
|
|
||||||
|
if (classes.length > 0 && onClassSelect && !selectedClass) {
|
||||||
|
setSelectedClass(classes[0]);
|
||||||
|
onClassSelect(classes[0]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.warn("Using default classes");
|
||||||
|
const classes = defaultClasses.map(cls => {
|
||||||
|
const color = calculateColor(cls.Id, '1');
|
||||||
|
return new DetectionClass(cls.Id, cls.Name, color);
|
||||||
|
});
|
||||||
|
setDetectionClasses(classes);
|
||||||
|
|
||||||
|
if (classes.length > 0 && onClassSelect && !selectedClass) {
|
||||||
|
setSelectedClass(classes[0]);
|
||||||
|
onClassSelect(classes[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Using default classes");
|
||||||
|
const classes = defaultClasses.map(cls => {
|
||||||
|
const color = calculateColor(cls.Id, '1');
|
||||||
|
return new DetectionClass(cls.Id, cls.Name, color);
|
||||||
|
});
|
||||||
|
setDetectionClasses(classes);
|
||||||
|
|
||||||
|
if (classes.length > 0 && onClassSelect && !selectedClass) {
|
||||||
|
setSelectedClass(classes[0]);
|
||||||
|
onClassSelect(classes[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleClassClick = (cls) => {
|
const handleClassClick = (cls) => {
|
||||||
setSelectedClass(cls); // Update the selected ID
|
setSelectedClass(cls);
|
||||||
onClassSelect && onClassSelect(cls);
|
onClassSelect && onClassSelect(cls);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,9 +87,9 @@ function DetectionClassList({ onClassSelect }) {
|
|||||||
<h3 style={{ marginTop: '15px', fontSize: '14px' }}>Classes</h3>
|
<h3 style={{ marginTop: '15px', fontSize: '14px' }}>Classes</h3>
|
||||||
<ul style={{ listStyleType: 'none', padding: 0, margin: 0 }}>
|
<ul style={{ listStyleType: 'none', padding: 0, margin: 0 }}>
|
||||||
{detectionClasses.map((cls) => {
|
{detectionClasses.map((cls) => {
|
||||||
const backgroundColor = calculateColor(cls.Id); // Calculate background color (0.2 opacity)
|
const backgroundColor = calculateColor(cls.Id);
|
||||||
const darkBg = calculateColor(cls.Id, '0.8'); // Calculate selected background color (0.4 opacity)
|
const darkBg = calculateColor(cls.Id, '0.8');
|
||||||
const isSelected = selectedClass.Id === cls.Id;
|
const isSelected = selectedClass && selectedClass.Id === cls.Id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@@ -63,8 +97,8 @@ function DetectionClassList({ onClassSelect }) {
|
|||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
border: `1px solid ${isSelected ? '#000' : '#eee'}`, // Use cls.Color for the selected border
|
border: `1px solid ${isSelected ? '#000' : '#eee'}`,
|
||||||
backgroundColor: isSelected ? darkBg : backgroundColor, // Conditional background
|
backgroundColor: isSelected ? darkBg : backgroundColor,
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
marginBottom: '2px',
|
marginBottom: '2px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
+176
-32
@@ -1,45 +1,189 @@
|
|||||||
import React, {useEffect } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
function VideoPlayer({ children, videoFile, currentTime, videoRef, isPlaying }) {
|
function VideoPlayer({ children, videoFile, currentTime, videoRef, isPlaying, onSizeChanged, onSetCurrentTime }) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const [playbackError, setPlaybackError] = useState(null);
|
||||||
|
const objectUrlRef = useRef(null);
|
||||||
|
// Flag to track if time update is coming from natural playback
|
||||||
|
const isPlaybackUpdateRef = useRef(false);
|
||||||
|
|
||||||
|
// Set up the video file when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("useEffect Videoplayer");
|
if (!videoFile || !videoRef.current) return;
|
||||||
if (!(videoFile && videoRef.current))
|
|
||||||
return;
|
|
||||||
|
|
||||||
console.log("Setting video source:", videoFile);
|
try {
|
||||||
videoRef.current.src = URL.createObjectURL(videoFile);
|
// Clean up previous object URL
|
||||||
videoRef.current.onloadedmetadata = () => {
|
if (objectUrlRef.current) {
|
||||||
|
URL.revokeObjectURL(objectUrlRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
// Create new object URL and set it directly
|
||||||
|
const objectUrl = URL.createObjectURL(videoFile);
|
||||||
|
objectUrlRef.current = objectUrl;
|
||||||
|
|
||||||
|
// Reset video and set new source
|
||||||
|
videoRef.current.pause();
|
||||||
|
videoRef.current.src = objectUrl;
|
||||||
|
videoRef.current.load();
|
||||||
|
setPlaybackError(null);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
objectUrlRef.current = null;
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading video:", err);
|
||||||
|
setPlaybackError(`Error loading video: ${err.message}`);
|
||||||
|
}
|
||||||
}, [videoFile]);
|
}, [videoFile]);
|
||||||
|
|
||||||
// useEffect(() => {
|
// Handle metadata loading and size changes
|
||||||
// if(videoRef.current){
|
useEffect(() => {
|
||||||
// videoRef.current.currentTime = currentTime;
|
if (!videoRef.current) return;
|
||||||
// }
|
|
||||||
// }, [currentTime, videoRef])
|
const handleMetadata = () => {
|
||||||
//
|
if (!videoRef.current) return;
|
||||||
// useEffect(() => {
|
|
||||||
// if (videoRef.current) {
|
const width = videoRef.current.videoWidth || 640;
|
||||||
// if(isPlaying){
|
const height = videoRef.current.videoHeight || 480;
|
||||||
// videoRef.current.play()
|
|
||||||
// }
|
if (onSizeChanged) {
|
||||||
// else{
|
onSizeChanged(width, height);
|
||||||
// videoRef.current.pause()
|
}
|
||||||
// }
|
};
|
||||||
// }
|
|
||||||
// }, [isPlaying, videoRef])
|
videoRef.current.addEventListener('loadedmetadata', handleMetadata);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.removeEventListener('loadedmetadata', handleMetadata);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [onSizeChanged]);
|
||||||
|
|
||||||
|
// Handle play/pause state
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoRef.current) return;
|
||||||
|
|
||||||
|
const attemptPlay = async () => {
|
||||||
|
try {
|
||||||
|
if (isPlaying) {
|
||||||
|
isPlaybackUpdateRef.current = true;
|
||||||
|
await videoRef.current.play();
|
||||||
|
setPlaybackError(null);
|
||||||
|
} else {
|
||||||
|
videoRef.current.pause();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Playback error:", err);
|
||||||
|
setPlaybackError(`Playback error: ${err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
attemptPlay();
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
|
// Handle current time changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoRef.current) return;
|
||||||
|
|
||||||
|
// Only update the video's time if it's not coming from natural playback
|
||||||
|
if (!isPlaybackUpdateRef.current) {
|
||||||
|
try {
|
||||||
|
if (videoRef.current.readyState > 0) {
|
||||||
|
videoRef.current.currentTime = currentTime;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Error setting time:", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset the flag after receiving the update
|
||||||
|
isPlaybackUpdateRef.current = false;
|
||||||
|
}
|
||||||
|
}, [currentTime]);
|
||||||
|
|
||||||
|
// Set up time update events
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoRef.current) return;
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (videoRef.current && onSetCurrentTime && isPlaying) {
|
||||||
|
isPlaybackUpdateRef.current = true;
|
||||||
|
onSetCurrentTime(videoRef.current.currentTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeeked = () => {
|
||||||
|
if (videoRef.current && onSetCurrentTime) {
|
||||||
|
onSetCurrentTime(videoRef.current.currentTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
videoRef.current.addEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
videoRef.current.addEventListener('seeked', handleSeeked);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.removeEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
videoRef.current.removeEventListener('seeked', handleSeeked);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [onSetCurrentTime, isPlaying]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative'}}>
|
<div
|
||||||
{/* Video Element */}
|
ref={containerRef}
|
||||||
<div style={{ width: '100%', height: '100%'}}>
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
maxHeight: '100%',
|
||||||
|
display: 'block',
|
||||||
|
objectFit: 'contain',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
preload="auto"
|
||||||
|
playsInline
|
||||||
|
muted
|
||||||
|
/>
|
||||||
|
{playbackError && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10px',
|
||||||
|
left: '10px',
|
||||||
|
background: 'rgba(255,0,0,0.7)',
|
||||||
|
color: 'white',
|
||||||
|
padding: '5px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
fontSize: '12px',
|
||||||
|
zIndex: 10
|
||||||
|
}}>
|
||||||
|
{playbackError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
pointerEvents: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
style={{ width: '100%', height: '100%', pointerEvents: isPlaying ? 'none' : 'auto' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,48 +13,76 @@ export const isMouseOverDetection = (x, y, detection, containerRef) => {
|
|||||||
return relativeX >= detection.x1 && relativeX <= detection.x2 && relativeY >= detection.y1 && relativeY <= detection.y2;
|
return relativeX >= detection.x1 && relativeX <= detection.x2 && relativeY >= detection.y1 && relativeY <= detection.y2;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to create an annotation image
|
|
||||||
export const createAnnotationImage = (videoRef, detections, containerRef) => {
|
export const createAnnotationImage = (videoRef, detections, containerRef) => {
|
||||||
|
if (!videoRef?.current || !containerRef?.current) {
|
||||||
|
console.warn("Missing video or container reference");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
if (!containerRef.current) return null;
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
|
|
||||||
canvas.width = container.offsetWidth;
|
canvas.width = container.offsetWidth || 640;
|
||||||
canvas.height = container.offsetHeight;
|
canvas.height = container.offsetHeight || 480;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return null;
|
||||||
|
|
||||||
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
|
try {
|
||||||
|
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error drawing video to canvas:", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
detections.forEach(detection => {
|
if (detections && detections.length > 0) {
|
||||||
|
detections.forEach(detection => {
|
||||||
|
if (!detection?.class) return;
|
||||||
|
|
||||||
ctx.fillRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1);
|
// Ensure proper opacity for background - consistently using 0.4 opacity
|
||||||
ctx.lineWidth = 2;
|
const bgColor = detection.class.Color?.startsWith('rgba')
|
||||||
ctx.strokeRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1);
|
? detection.class.Color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 0.4)')
|
||||||
|
: detection.class.Color?.replace(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/, 'rgba($1, $2, $3, 0.4)') || 'rgba(255, 0, 0, 0.4)';
|
||||||
|
|
||||||
ctx.fillStyle = 'white';
|
// Ensure full opacity for border
|
||||||
ctx.font = '12px Arial';
|
const borderColor = detection.class.Color?.startsWith('rgba')
|
||||||
ctx.fillText(detection.class.Name, detection.x1, detection.y1 - 5);
|
? detection.class.Color.replace(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/, 'rgba($1, $2, $3, 1)')
|
||||||
});
|
: detection.class.Color || 'rgba(255, 0, 0, 1)';
|
||||||
|
|
||||||
|
ctx.fillStyle = bgColor;
|
||||||
|
ctx.strokeStyle = borderColor;
|
||||||
|
|
||||||
|
const x = Math.max(0, detection.x1 || 0);
|
||||||
|
const y = Math.max(0, detection.y1 || 0);
|
||||||
|
const width = Math.max(1, (detection.x2 || 0) - (detection.x1 || 0));
|
||||||
|
const height = Math.max(1, (detection.y2 || 0) - (detection.y1 || 0));
|
||||||
|
|
||||||
|
ctx.fillRect(x, y, width, height);
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(x, y, width, height);
|
||||||
|
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.font = '12px Arial';
|
||||||
|
ctx.fillText(detection.class.Name || 'Unknown', x, y - 5);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return canvas.toDataURL('image/png');
|
return canvas.toDataURL('image/png');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, containerRef) => {
|
||||||
export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, containerRef) => {
|
|
||||||
|
|
||||||
let newX1 = mouseX - dragOffset.x;
|
let newX1 = mouseX - dragOffset.x;
|
||||||
let newY1 = mouseY - dragOffset.y;
|
let newY1 = mouseY - dragOffset.y;
|
||||||
let newX2 = newX1 + (detection.x2 - detection.x1);
|
let newX2 = newX1 + (detection.x2 - detection.x1);
|
||||||
let newY2 = newY1 + (detection.y2 - detection.y1);
|
let newY2 = newY1 + (detection.y2 - detection.y1);
|
||||||
|
|
||||||
if (!containerRef.current) {
|
if (!containerRef.current) {
|
||||||
return { newX1, newY1, newX2, newY2 }; // Return early with unchanged values
|
return { newX1, newY1, newX2, newY2 };
|
||||||
}
|
}
|
||||||
|
|
||||||
let containerWidth = containerRef.current.offsetWidth;
|
let containerWidth = containerRef.current.offsetWidth;
|
||||||
let containerHeight = containerRef.current.offsetHeight;
|
let containerHeight = containerRef.current.offsetHeight;
|
||||||
|
|
||||||
|
|
||||||
if (newX1 < 0) {
|
if (newX1 < 0) {
|
||||||
newX1 = 0;
|
newX1 = 0;
|
||||||
newX2 = detection.x2 - detection.x1;
|
newX2 = detection.x2 - detection.x1;
|
||||||
@@ -72,13 +100,13 @@ export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, con
|
|||||||
newY1 = newY2 - (detection.y2 - detection.y1);
|
newY1 = newY2 - (detection.y2 - detection.y1);
|
||||||
}
|
}
|
||||||
return { newX1, newY1, newX2, newY2 };
|
return { newX1, newY1, newX2, newY2 };
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateResizedPosition = (mouseX, mouseY, position, detection, containerRef) => {
|
export const calculateResizedPosition = (mouseX, mouseY, position, detection, containerRef) => {
|
||||||
let { x1, y1, x2, y2 } = detection;
|
let { x1, y1, x2, y2 } = detection;
|
||||||
const containerWidth = containerRef.current.offsetWidth;
|
const containerWidth = containerRef.current?.offsetWidth || 640;
|
||||||
const containerHeight = containerRef.current.offsetHeight;
|
const containerHeight = containerRef.current?.offsetHeight || 480;
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'top-left':
|
case 'top-left':
|
||||||
x1 = Math.min(mouseX, detection.x2 - 5);
|
x1 = Math.min(mouseX, detection.x2 - 5);
|
||||||
@@ -111,11 +139,11 @@ export const calculateResizedPosition = (mouseX, mouseY, position, detection, co
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Boundary checks
|
|
||||||
x1 = Math.max(0, x1);
|
x1 = Math.max(0, x1);
|
||||||
y1 = Math.max(0, y1);
|
y1 = Math.max(0, y1);
|
||||||
x2 = Math.min(containerWidth, x2);
|
x2 = Math.min(containerWidth, x2);
|
||||||
y2 = Math.min(containerHeight, y2);
|
y2 = Math.min(containerHeight, y2);
|
||||||
|
|
||||||
return { x1, y1, x2, y2 };
|
return { ...detection, x1, y1, x2, y2 };
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const convertToYoloFormat = (detection, imageWidth, imageHeight) => {
|
||||||
|
const width = (detection.x2 - detection.x1) / imageWidth;
|
||||||
|
const height = (detection.y2 - detection.y1) / imageHeight;
|
||||||
|
const centerX = (detection.x1 / imageWidth) + (width / 2);
|
||||||
|
const centerY = (detection.y1 / imageHeight) + (height / 2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
classId: detection.class.Id,
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAnnotation = (time, detections, imageData) => {
|
||||||
|
if (!detections || !detections.length || !imageData) {
|
||||||
|
console.warn("Nothing to save: missing detections or image data");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const imageFilename = `annotation_${timestamp}.png`;
|
||||||
|
const annotationFilename = `annotation_${timestamp}.txt`;
|
||||||
|
|
||||||
|
const imageWidth = detections[0]?.x2 > 0 ?
|
||||||
|
Math.max(...detections.map(d => d.x2)) : 640;
|
||||||
|
const imageHeight = detections[0]?.y2 > 0 ?
|
||||||
|
Math.max(...detections.map(d => d.y2)) : 480;
|
||||||
|
|
||||||
|
const yoloAnnotations = detections.map(detection => {
|
||||||
|
const yolo = convertToYoloFormat(detection, imageWidth, imageHeight);
|
||||||
|
return `${yolo.classId} ${yolo.centerX.toFixed(6)} ${yolo.centerY.toFixed(6)} ${yolo.width.toFixed(6)} ${yolo.height.toFixed(6)}`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
console.log(`Saving image to: ${imageFilename}`);
|
||||||
|
console.log(`Saving annotations to: ${annotationFilename}`);
|
||||||
|
|
||||||
|
const imageLink = document.createElement('a');
|
||||||
|
imageLink.href = imageData;
|
||||||
|
imageLink.download = imageFilename;
|
||||||
|
imageLink.click();
|
||||||
|
|
||||||
|
const annotationBlob = new Blob([yoloAnnotations], { type: 'text/plain' });
|
||||||
|
const annotationLink = document.createElement('a');
|
||||||
|
annotationLink.href = URL.createObjectURL(annotationBlob);
|
||||||
|
annotationLink.download = annotationFilename;
|
||||||
|
annotationLink.click();
|
||||||
|
|
||||||
|
return { imageFilename, annotationFilename };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving annotation:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default saveAnnotation;
|
||||||
Reference in New Issue
Block a user