mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 07:16: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 }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', position: 'relative', zIndex: 1 }}>
|
||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onFrameBackward}></button>
|
||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onPlayPause}>{isPlaying ? 'Pause' : 'Play'}</button>
|
||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onFrameForward}></button>
|
||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onSaveAnnotation}>Save Annotation</button>
|
||||
<button style={{ padding: '10px 20px', fontSize: '16px', margin: '0 5px' }} onClick={onDelete}>Delete</button>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import VideoPlayer from './VideoPlayer';
|
||||
import AnnotationList from './AnnotationList';
|
||||
import MediaList from './MediaList';
|
||||
import DetectionClassList from './DetectionClassList';
|
||||
import CanvasEditor from './CanvasEditor';
|
||||
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() {
|
||||
const [files, setFiles] = useState([]);
|
||||
@@ -15,8 +16,13 @@ function AnnotationMain() {
|
||||
const [selectedClass, setSelectedClass] = useState(null);
|
||||
const [detections, setDetections] = useState([]);
|
||||
const [selectedDetectionIndices, setSelectedDetectionIndices] = useState([]);
|
||||
const [isPlaying, setIsPlaying] = useState(false); // Add isPlaying state here
|
||||
const videoRef = React.createRef();
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [videoWidth, setVideoWidth] = useState(640);
|
||||
const [videoHeight, setVideoHeight] = useState(480);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const videoRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const initialFiles = [];
|
||||
@@ -24,49 +30,87 @@ function AnnotationMain() {
|
||||
}, []);
|
||||
|
||||
const handleFileSelect = (file) => {
|
||||
console.log("handleFileSelect called with:", file);
|
||||
if (!file) return;
|
||||
|
||||
setSelectedFile(file);
|
||||
setAnnotations({});
|
||||
setDetections([]);
|
||||
setSelectedDetectionIndices([]);
|
||||
setCurrentTime(0);
|
||||
setIsPlaying(false); // Reset playing state
|
||||
setIsPlaying(false);
|
||||
setErrorMessage("");
|
||||
};
|
||||
|
||||
const handleDropNewFiles = (newFiles) => {
|
||||
setFiles(prevFiles => [...prevFiles, ...newFiles]);
|
||||
if (!selectedFile) {
|
||||
setSelectedFile(newFiles[0]);
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
|
||||
const validFiles = [...newFiles];
|
||||
setFiles(prevFiles => [...prevFiles, ...validFiles]);
|
||||
|
||||
if (!selectedFile && validFiles.length > 0) {
|
||||
setSelectedFile(validFiles[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnnotationSave = () => {
|
||||
const containerRef = { current: { offsetWidth: videoRef.current.videoWidth, offsetHeight: videoRef.current.videoHeight } };
|
||||
const imageData = AnnotationService.createAnnotationImage(videoRef, detections, null, selectedClass, containerRef);
|
||||
if (!videoRef.current) return;
|
||||
|
||||
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) {
|
||||
setAnnotations(prevAnnotations => ({
|
||||
...prevAnnotations,
|
||||
[currentTime]: { time: currentTime, detections: detections, imageData },
|
||||
}));
|
||||
setAnnotations(prevAnnotations => {
|
||||
const newAnnotations = {
|
||||
...prevAnnotations,
|
||||
[currentTime]: { time: currentTime, annotations: detections, imageData }
|
||||
};
|
||||
|
||||
saveAnnotation(currentTime, detections, imageData);
|
||||
setErrorMessage("");
|
||||
|
||||
return newAnnotations;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (selectedDetectionIndices.length === 0) {
|
||||
setErrorMessage("Please select a detection to delete");
|
||||
return;
|
||||
}
|
||||
|
||||
const newDetections = detections.filter((_, index) => !selectedDetectionIndices.includes(index));
|
||||
setDetections(newDetections);
|
||||
setSelectedDetectionIndices([]);
|
||||
setErrorMessage("");
|
||||
};
|
||||
|
||||
const handleAnnotationClick = (time) => {
|
||||
setCurrentTime(time);
|
||||
const annotation = annotations[time];
|
||||
if (annotation) {
|
||||
setDetections(annotation.detections);
|
||||
setDetections(annotation.annotations || []);
|
||||
setSelectedDetectionIndices([]);
|
||||
}
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime = time;
|
||||
}
|
||||
setIsPlaying(false); // Pause when clicking an annotation
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
const handleClassSelect = (cls) => {
|
||||
@@ -81,71 +125,144 @@ function AnnotationMain() {
|
||||
setSelectedDetectionIndices(newSelection);
|
||||
};
|
||||
|
||||
const handlePlayPause = () => { // Moved from VideoPlayer
|
||||
const handlePlayPause = () => {
|
||||
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) {
|
||||
videoRef.current.currentTime += 1 / 30;
|
||||
setCurrentTime(videoRef.current.currentTime)
|
||||
setCurrentTime(videoRef.current.currentTime);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFrameBackward = () => { // Moved from VideoPlayer
|
||||
const handleFrameBackward = () => {
|
||||
if (videoRef.current) {
|
||||
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 (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ width: '15%', paddingRight: '10px', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
width: '15%',
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
borderRight: '1px solid #ccc',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<MediaList
|
||||
files={files}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={handleFileSelect}
|
||||
onDropNewFiles={handleDropNewFiles}
|
||||
/>
|
||||
<div style={{ marginTop: 'auto' }}>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<DetectionClassList onClassSelect={handleClassSelect} />
|
||||
</div>
|
||||
<AnnotationList annotations={Object.values(annotations)} onAnnotationClick={handleAnnotationClick} />
|
||||
|
||||
</div>
|
||||
<div style={{ width: '85%' }}>
|
||||
<VideoPlayer
|
||||
videoFile={selectedFile}
|
||||
currentTime={currentTime}
|
||||
videoRef={videoRef}
|
||||
isPlaying = {isPlaying}
|
||||
>
|
||||
<CanvasEditor
|
||||
width={videoRef.current ? videoRef.current.videoWidth : 0}
|
||||
height={videoRef.current ? videoRef.current.videoHeight : 0}
|
||||
detections={detections}
|
||||
selectedDetectionIndices={selectedDetectionIndices}
|
||||
onDetectionsChange={handleDetectionsChange}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
detectionClass={selectedClass}
|
||||
|
||||
<div style={{
|
||||
width: '70%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
{errorMessage && (
|
||||
<div style={{
|
||||
backgroundColor: '#ffdddd',
|
||||
color: '#d8000c',
|
||||
padding: '6px',
|
||||
margin: '6px',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
{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>
|
||||
<AnnotationControls
|
||||
onFrameBackward={handleFrameBackward}
|
||||
onPlayPause={handlePlayPause}
|
||||
isPlaying={isPlaying}
|
||||
onFrameForward={handleFrameForward}
|
||||
onSaveAnnotation={handleAnnotationSave}
|
||||
onDelete={handleDelete}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
width: '15%',
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
borderLeft: '1px solid #ccc'
|
||||
}}>
|
||||
<AnnotationList
|
||||
annotations={Object.values(annotations)}
|
||||
onAnnotationClick={handleAnnotationClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,16 @@ function CanvasEditor({
|
||||
const [resizeData, setResizeData] = useState(null);
|
||||
const [localDetections, setLocalDetections] = useState(detections || []);
|
||||
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(() => {
|
||||
setLocalDetections(detections || []);
|
||||
@@ -58,8 +68,9 @@ function CanvasEditor({
|
||||
x: mouseX - localDetections[i].x1,
|
||||
y: mouseY - localDetections[i].y1,
|
||||
});
|
||||
setIsDragging(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) {
|
||||
// Dragging logic
|
||||
setIsDragging(true);
|
||||
const newDetections = [...localDetections];
|
||||
const firstSelectedIndex = localSelectedIndices[0];
|
||||
|
||||
// Check for valid index before accessing.
|
||||
if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return;
|
||||
|
||||
const firstSelectedDetection = newDetections[firstSelectedIndex];
|
||||
@@ -94,7 +105,6 @@ function CanvasEditor({
|
||||
const deltaY = newY1 - firstSelectedDetection.y1;
|
||||
|
||||
localSelectedIndices.forEach(index => {
|
||||
// Check for valid index before accessing.
|
||||
if (newDetections[index] === undefined) return;
|
||||
|
||||
const detection = newDetections[index];
|
||||
@@ -112,17 +122,17 @@ function CanvasEditor({
|
||||
|
||||
setLocalDetections(newDetections);
|
||||
if (onDetectionsChange) {
|
||||
onDetectionsChange(newDetections); // Notify about changes
|
||||
onDetectionsChange(newDetections);
|
||||
}
|
||||
} else if (currentDetection && !resizeData) {
|
||||
// Drawing a new detection.
|
||||
setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY }));
|
||||
} else if (resizeData) {
|
||||
setIsDragging(true);
|
||||
const { index, position } = resizeData;
|
||||
if (localDetections[index] === undefined) return;
|
||||
|
||||
const newDetections = [...localDetections];
|
||||
const detection = newDetections[index];
|
||||
const detection = { ...newDetections[index] };
|
||||
const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef);
|
||||
newDetections[index] = updatedDetection;
|
||||
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) {
|
||||
const dx = Math.abs(currentDetection.x2 - currentDetection.x1);
|
||||
const dy = Math.abs(currentDetection.y2 - currentDetection.y1);
|
||||
|
||||
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);
|
||||
if (onDetectionsChange) {
|
||||
onDetectionsChange(newDetections);
|
||||
@@ -150,6 +174,7 @@ function CanvasEditor({
|
||||
setMouseDownPos(null);
|
||||
setDragOffset({ x: 0, y: 0 });
|
||||
setResizeData(null);
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDetectionMouseDown = (e, index) => {
|
||||
@@ -172,8 +197,8 @@ function CanvasEditor({
|
||||
x: mouseX - localDetections[index].x1,
|
||||
y: mouseY - localDetections[index].y1,
|
||||
});
|
||||
setMouseDownPos({x: mouseX, y: mouseY})
|
||||
|
||||
setMouseDownPos({x: mouseX, y: mouseY});
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleResize = (e, index, position) => {
|
||||
@@ -190,20 +215,44 @@ function CanvasEditor({
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
>
|
||||
<div ref={containerRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'auto',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// src/components/Detection.js
|
||||
import React from 'react';
|
||||
|
||||
function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) { // Corrected prop name
|
||||
function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
||||
if (!detection || !detection.class) {
|
||||
return null;
|
||||
}
|
||||
@@ -14,8 +13,13 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
||||
}
|
||||
|
||||
// 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 borderColor = color.startsWith('rgba') ? color.replace('0.4', '1') : color; // Ensure full opacity for border
|
||||
const backgroundColor = color.startsWith('rgba')
|
||||
? 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;
|
||||
|
||||
@@ -36,21 +40,22 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
||||
top: `${detection.y1}px`,
|
||||
width: `${detection.x2 - detection.x1}px`,
|
||||
height: `${detection.y2 - detection.y1}px`,
|
||||
backgroundColor: backgroundColor, // Use the calculated backgroundColor
|
||||
border: `2px solid ${borderColor}`, // Use the calculated borderColor
|
||||
backgroundColor: backgroundColor,
|
||||
border: `2px solid ${borderColor}`,
|
||||
boxSizing: 'border-box',
|
||||
cursor: isSelected ? 'move' : 'default',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: isSelected ? 2 : 1,
|
||||
};
|
||||
|
||||
if (isSelected) {
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
onDetectionMouseDown(e); // Corrected prop name
|
||||
onDetectionMouseDown(e);
|
||||
};
|
||||
|
||||
const handleResizeMouseDown = (e, position) => {
|
||||
@@ -73,11 +78,22 @@ function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) {
|
||||
backgroundColor: 'black',
|
||||
cursor: handle.cursor,
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 3,
|
||||
}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// --- START OF FILE DetectionClassList.js --- (No changes needed)
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DetectionClass from '../models/DetectionClass';
|
||||
|
||||
@@ -6,7 +5,7 @@ function DetectionClassList({ onClassSelect }) {
|
||||
const [detectionClasses, setDetectionClasses] = useState([]);
|
||||
const [selectedClass, setSelectedClass] = useState(null);
|
||||
|
||||
const colors = [ // Define colors *inside* the component
|
||||
const colors = [
|
||||
"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000",
|
||||
"#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#808080",
|
||||
"#C00000", "#00C000", "#0000C0", "#C0C000", "#C000C0", "#00C0C0", "#C0C0C0",
|
||||
@@ -17,7 +16,6 @@ function DetectionClassList({ onClassSelect }) {
|
||||
"#E00000", "#00E000", "#0000E0", "#E0E000", "#E000E0", "#00E0E0", "#E0E0E0"
|
||||
];
|
||||
|
||||
// Calculate color with opacity
|
||||
const calculateColor = (id, opacity = '0.2') => {
|
||||
const hexColor = colors[id % colors.length];
|
||||
const r = parseInt(hexColor.slice(1, 3), 16);
|
||||
@@ -27,24 +25,60 @@ function DetectionClassList({ onClassSelect }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('config.json') // Make sure this path is correct
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const detectionClasses = data.classes.map(cls => {
|
||||
const color = calculateColor(cls.Id, '1'); // Full opacity for border
|
||||
return new DetectionClass(cls.Id, cls.Name, color);
|
||||
});
|
||||
setDetectionClasses(detectionClasses);
|
||||
const defaultClasses = [
|
||||
{ Id: 0, Name: "Car" },
|
||||
{ Id: 1, Name: "Person" },
|
||||
{ Id: 2, Name: "Truck" },
|
||||
{ Id: 3, Name: "Bicycle" },
|
||||
{ Id: 4, Name: "Motorcycle" },
|
||||
{ Id: 5, Name: "Bus" }
|
||||
];
|
||||
|
||||
if (detectionClasses.length > 0 && onClassSelect) {
|
||||
onClassSelect(detectionClasses[0]); // Select the first class by default
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error loading detection classes:", error));
|
||||
});
|
||||
try {
|
||||
fetch('config.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
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) => {
|
||||
setSelectedClass(cls); // Update the selected ID
|
||||
setSelectedClass(cls);
|
||||
onClassSelect && onClassSelect(cls);
|
||||
};
|
||||
|
||||
@@ -53,9 +87,9 @@ function DetectionClassList({ onClassSelect }) {
|
||||
<h3 style={{ marginTop: '15px', fontSize: '14px' }}>Classes</h3>
|
||||
<ul style={{ listStyleType: 'none', padding: 0, margin: 0 }}>
|
||||
{detectionClasses.map((cls) => {
|
||||
const backgroundColor = calculateColor(cls.Id); // Calculate background color (0.2 opacity)
|
||||
const darkBg = calculateColor(cls.Id, '0.8'); // Calculate selected background color (0.4 opacity)
|
||||
const isSelected = selectedClass.Id === cls.Id;
|
||||
const backgroundColor = calculateColor(cls.Id);
|
||||
const darkBg = calculateColor(cls.Id, '0.8');
|
||||
const isSelected = selectedClass && selectedClass.Id === cls.Id;
|
||||
|
||||
return (
|
||||
<li
|
||||
@@ -63,8 +97,8 @@ function DetectionClassList({ onClassSelect }) {
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
border: `1px solid ${isSelected ? '#000' : '#eee'}`, // Use cls.Color for the selected border
|
||||
backgroundColor: isSelected ? darkBg : backgroundColor, // Conditional background
|
||||
border: `1px solid ${isSelected ? '#000' : '#eee'}`,
|
||||
backgroundColor: isSelected ? darkBg : backgroundColor,
|
||||
fontSize: '14px',
|
||||
marginBottom: '2px',
|
||||
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(() => {
|
||||
console.log("useEffect Videoplayer");
|
||||
if (!(videoFile && videoRef.current))
|
||||
return;
|
||||
if (!videoFile || !videoRef.current) return;
|
||||
|
||||
console.log("Setting video source:", videoFile);
|
||||
videoRef.current.src = URL.createObjectURL(videoFile);
|
||||
videoRef.current.onloadedmetadata = () => {
|
||||
try {
|
||||
// Clean up previous object URL
|
||||
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]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if(videoRef.current){
|
||||
// videoRef.current.currentTime = currentTime;
|
||||
// }
|
||||
// }, [currentTime, videoRef])
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (videoRef.current) {
|
||||
// if(isPlaying){
|
||||
// videoRef.current.play()
|
||||
// }
|
||||
// else{
|
||||
// videoRef.current.pause()
|
||||
// }
|
||||
// }
|
||||
// }, [isPlaying, videoRef])
|
||||
// Handle metadata loading and size changes
|
||||
useEffect(() => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
const handleMetadata = () => {
|
||||
if (!videoRef.current) return;
|
||||
|
||||
const width = videoRef.current.videoWidth || 640;
|
||||
const height = videoRef.current.videoHeight || 480;
|
||||
|
||||
if (onSizeChanged) {
|
||||
onSizeChanged(width, height);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div style={{ position: 'relative'}}>
|
||||
{/* Video Element */}
|
||||
<div style={{ width: '100%', height: '100%'}}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
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}
|
||||
<video
|
||||
ref={videoRef}
|
||||
style={{ width: '100%', height: '100%', pointerEvents: isPlaying ? 'none' : 'auto' }}
|
||||
/>
|
||||
</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;
|
||||
};
|
||||
|
||||
// Function to create an annotation image
|
||||
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');
|
||||
if (!containerRef.current) return null;
|
||||
const container = containerRef.current;
|
||||
|
||||
canvas.width = container.offsetWidth;
|
||||
canvas.height = container.offsetHeight;
|
||||
canvas.width = container.offsetWidth || 640;
|
||||
canvas.height = container.offsetHeight || 480;
|
||||
|
||||
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);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1);
|
||||
// Ensure proper opacity for background - consistently using 0.4 opacity
|
||||
const bgColor = detection.class.Color?.startsWith('rgba')
|
||||
? 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';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText(detection.class.Name, detection.x1, detection.y1 - 5);
|
||||
});
|
||||
// Ensure full opacity for border
|
||||
const borderColor = detection.class.Color?.startsWith('rgba')
|
||||
? 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');
|
||||
};
|
||||
|
||||
|
||||
export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, containerRef) => {
|
||||
|
||||
export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, containerRef) => {
|
||||
let newX1 = mouseX - dragOffset.x;
|
||||
let newY1 = mouseY - dragOffset.y;
|
||||
let newX2 = newX1 + (detection.x2 - detection.x1);
|
||||
let newY2 = newY1 + (detection.y2 - detection.y1);
|
||||
|
||||
if (!containerRef.current) {
|
||||
return { newX1, newY1, newX2, newY2 }; // Return early with unchanged values
|
||||
return { newX1, newY1, newX2, newY2 };
|
||||
}
|
||||
|
||||
let containerWidth = containerRef.current.offsetWidth;
|
||||
let containerHeight = containerRef.current.offsetHeight;
|
||||
|
||||
|
||||
if (newX1 < 0) {
|
||||
newX1 = 0;
|
||||
newX2 = detection.x2 - detection.x1;
|
||||
@@ -72,13 +100,13 @@ export const calculateNewPosition = (mouseX, mouseY, dragOffset, detection, con
|
||||
newY1 = newY2 - (detection.y2 - detection.y1);
|
||||
}
|
||||
return { newX1, newY1, newX2, newY2 };
|
||||
|
||||
};
|
||||
|
||||
export const calculateResizedPosition = (mouseX, mouseY, position, detection, containerRef) => {
|
||||
let { x1, y1, x2, y2 } = detection;
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const containerHeight = containerRef.current.offsetHeight;
|
||||
const containerWidth = containerRef.current?.offsetWidth || 640;
|
||||
const containerHeight = containerRef.current?.offsetHeight || 480;
|
||||
|
||||
switch (position) {
|
||||
case 'top-left':
|
||||
x1 = Math.min(mouseX, detection.x2 - 5);
|
||||
@@ -111,11 +139,11 @@ export const calculateResizedPosition = (mouseX, mouseY, position, detection, co
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Boundary checks
|
||||
|
||||
x1 = Math.max(0, x1);
|
||||
y1 = Math.max(0, y1);
|
||||
x2 = Math.min(containerWidth, x2);
|
||||
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