mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 10:46:34 +00:00
Azaion Suite to the web. First commit. Only rough sketches of future components is done.
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import AnnotationMain from './components/AnnotationMain';
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100vh' }}> {/* Use full viewport height */}
|
||||
<AnnotationMain />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
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;
|
||||
@@ -0,0 +1,15 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnnotationControls;
|
||||
@@ -0,0 +1,20 @@
|
||||
// src/components/AnnotationList.js
|
||||
// No changes
|
||||
import React from 'react';
|
||||
|
||||
function AnnotationList({ annotations, onAnnotationClick }) {
|
||||
return (
|
||||
<div>
|
||||
<h3>Annotations</h3>
|
||||
<ul>
|
||||
{annotations.map((annotation, index) => (
|
||||
<li key={index} onClick={() => onAnnotationClick(annotation.time)}>
|
||||
Frame {index + 1} - {annotation.annotations.length} objects
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnnotationList;
|
||||
@@ -0,0 +1,156 @@
|
||||
import React, { useState, useEffect } 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
|
||||
|
||||
function AnnotationMain() {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [annotations, setAnnotations] = useState({});
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
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();
|
||||
|
||||
useEffect(() => {
|
||||
const initialFiles = [];
|
||||
setFiles(initialFiles);
|
||||
}, []);
|
||||
|
||||
const handleFileSelect = (file) => {
|
||||
console.log("handleFileSelect called with:", file);
|
||||
setSelectedFile(file);
|
||||
setAnnotations({});
|
||||
setDetections([]);
|
||||
setSelectedDetectionIndices([]);
|
||||
setCurrentTime(0);
|
||||
setIsPlaying(false); // Reset playing state
|
||||
};
|
||||
|
||||
const handleDropNewFiles = (newFiles) => {
|
||||
setFiles(prevFiles => [...prevFiles, ...newFiles]);
|
||||
if (!selectedFile) {
|
||||
setSelectedFile(newFiles[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnnotationSave = () => {
|
||||
const containerRef = { current: { offsetWidth: videoRef.current.videoWidth, offsetHeight: videoRef.current.videoHeight } };
|
||||
const annotationSelectedClass = selectedClass;
|
||||
const imageData = AnnotationService.createAnnotationImage(videoRef, detections, null, annotationSelectedClass, containerRef);
|
||||
if (imageData) {
|
||||
setAnnotations(prevAnnotations => ({
|
||||
...prevAnnotations,
|
||||
[currentTime]: { time: currentTime, detections: detections, imageData },
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
const newDetections = detections.filter((_, index) => !selectedDetectionIndices.includes(index));
|
||||
setDetections(newDetections);
|
||||
};
|
||||
|
||||
const handleAnnotationClick = (time) => {
|
||||
setCurrentTime(time);
|
||||
const annotation = annotations[time];
|
||||
if (annotation) {
|
||||
setDetections(annotation.detections);
|
||||
setSelectedDetectionIndices([]);
|
||||
}
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime = time;
|
||||
}
|
||||
setIsPlaying(false); // Pause when clicking an annotation
|
||||
};
|
||||
|
||||
const handleClassSelect = (cls) => {
|
||||
setSelectedClass(cls);
|
||||
};
|
||||
|
||||
const handleDetectionsChange = (newDetections) => {
|
||||
setDetections(newDetections);
|
||||
};
|
||||
|
||||
const handleSelectionChange = (newSelection) => {
|
||||
setSelectedDetectionIndices(newSelection);
|
||||
};
|
||||
|
||||
const handlePlayPause = () => { // Moved from VideoPlayer
|
||||
setIsPlaying(prev => !prev);
|
||||
if (videoRef.current) {
|
||||
if (isPlaying) {
|
||||
videoRef.current.pause();
|
||||
} else {
|
||||
videoRef.current.play();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFrameForward = () => { // Moved from VideoPlayer
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime += 1 / 30;
|
||||
setCurrentTime(videoRef.current.currentTime)
|
||||
}
|
||||
};
|
||||
|
||||
const handleFrameBackward = () => { // Moved from VideoPlayer
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime -= 1 / 30;
|
||||
setCurrentTime(videoRef.current.currentTime)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ width: '15%', paddingRight: '10px', display: 'flex', flexDirection: 'column' }}>
|
||||
<MediaList
|
||||
files={files}
|
||||
selectedFile={selectedFile}
|
||||
onFileSelect={handleFileSelect}
|
||||
onDropNewFiles={handleDropNewFiles}
|
||||
/>
|
||||
<div style={{ marginTop: 'auto' }}>
|
||||
<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}
|
||||
/>
|
||||
</VideoPlayer>
|
||||
<AnnotationControls
|
||||
onFrameBackward={handleFrameBackward}
|
||||
onPlayPause={handlePlayPause}
|
||||
isPlaying={isPlaying}
|
||||
onFrameForward={handleFrameForward}
|
||||
onSaveAnnotation={handleAnnotationSave}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnnotationMain;
|
||||
@@ -0,0 +1,229 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import * as AnnotationService from '../services/AnnotationService';
|
||||
import DetectionContainer from './DetectionContainer';
|
||||
|
||||
function CanvasEditor({
|
||||
width,
|
||||
height,
|
||||
detections,
|
||||
initialCurrentDetection = null,
|
||||
selectedDetectionIndices,
|
||||
onDetectionsChange,
|
||||
onSelectionChange,
|
||||
children,
|
||||
detectionClass
|
||||
}) {
|
||||
const containerRef = useRef(null);
|
||||
const [currentDetection, setCurrentDetection] = useState(initialCurrentDetection);
|
||||
const [mouseDownPos, setMouseDownPos] = useState(null);
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const [resizeData, setResizeData] = useState(null);
|
||||
const [localDetections, setLocalDetections] = useState(detections || []);
|
||||
const [localSelectedIndices, setLocalSelectedIndices] = useState(selectedDetectionIndices || []);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalDetections(detections || []);
|
||||
}, [detections]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalSelectedIndices(selectedDetectionIndices || []);
|
||||
}, [selectedDetectionIndices]);
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const { x: mouseX, y: mouseY } = AnnotationService.calculateRelativeCoordinates(e, containerRef);
|
||||
setMouseDownPos({ mouseX, mouseY });
|
||||
|
||||
let detectionFound = false;
|
||||
for (let i = localDetections.length - 1; i >= 0; i--) {
|
||||
if (AnnotationService.isMouseOverDetection(e.clientX, e.clientY, localDetections[i], containerRef)) {
|
||||
if (e.ctrlKey) {
|
||||
const newSelectedIndices = localSelectedIndices.includes(i)
|
||||
? localSelectedIndices.filter(index => index !== i)
|
||||
: [...localSelectedIndices, i];
|
||||
setLocalSelectedIndices(newSelectedIndices);
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange(newSelectedIndices);
|
||||
}
|
||||
} else {
|
||||
const newSelectedIndices = [i];
|
||||
setLocalSelectedIndices(newSelectedIndices);
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange(newSelectedIndices);
|
||||
}
|
||||
}
|
||||
setDragOffset({
|
||||
x: mouseX - localDetections[i].x1,
|
||||
y: mouseY - localDetections[i].y1,
|
||||
});
|
||||
detectionFound = true;
|
||||
break; // Stop the loop once a detection is found
|
||||
}
|
||||
}
|
||||
|
||||
if (!detectionFound) {
|
||||
if (!e.ctrlKey) {
|
||||
setLocalSelectedIndices([]);
|
||||
if (onSelectionChange) {
|
||||
onSelectionChange([]);
|
||||
}
|
||||
}
|
||||
if (detectionClass) {
|
||||
setCurrentDetection({ x1: mouseX, y1: mouseY, x2: mouseX, y2: mouseY, class: detectionClass });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!containerRef.current) return;
|
||||
const { x: mouseX, y: mouseY } = AnnotationService.calculateRelativeCoordinates(e, containerRef);
|
||||
|
||||
if (localSelectedIndices.length > 0 && mouseDownPos && !resizeData) {
|
||||
// Dragging logic
|
||||
const newDetections = [...localDetections];
|
||||
const firstSelectedIndex = localSelectedIndices[0];
|
||||
|
||||
// Check for valid index before accessing.
|
||||
if (firstSelectedIndex === undefined || !newDetections[firstSelectedIndex]) return;
|
||||
|
||||
const firstSelectedDetection = newDetections[firstSelectedIndex];
|
||||
const { newX1, newY1, newX2, newY2 } = AnnotationService.calculateNewPosition(mouseX, mouseY, dragOffset, firstSelectedDetection, containerRef);
|
||||
const deltaX = newX1 - firstSelectedDetection.x1;
|
||||
const deltaY = newY1 - firstSelectedDetection.y1;
|
||||
|
||||
localSelectedIndices.forEach(index => {
|
||||
// Check for valid index before accessing.
|
||||
if (newDetections[index] === undefined) return;
|
||||
|
||||
const detection = newDetections[index];
|
||||
let updatedX1 = detection.x1 + deltaX;
|
||||
let updatedY1 = detection.y1 + deltaY;
|
||||
let updatedX2 = detection.x2 + deltaX;
|
||||
let updatedY2 = detection.y2 + deltaY;
|
||||
|
||||
const bounds = AnnotationService.calculateNewPosition(updatedX1 + dragOffset.x, updatedY1 + dragOffset.y, dragOffset, { ...detection, x1: updatedX1, y1: updatedY1, x2: updatedX2, y2: updatedY2 }, containerRef);
|
||||
detection.x1 = bounds.newX1;
|
||||
detection.y1 = bounds.newY1;
|
||||
detection.x2 = bounds.newX2;
|
||||
detection.y2 = bounds.newY2;
|
||||
});
|
||||
|
||||
setLocalDetections(newDetections);
|
||||
if (onDetectionsChange) {
|
||||
onDetectionsChange(newDetections); // Notify about changes
|
||||
}
|
||||
} else if (currentDetection && !resizeData) {
|
||||
// Drawing a new detection.
|
||||
setCurrentDetection(prev => ({ ...prev, x2: mouseX, y2: mouseY }));
|
||||
} else if (resizeData) {
|
||||
const { index, position } = resizeData;
|
||||
if (localDetections[index] === undefined) return;
|
||||
|
||||
const newDetections = [...localDetections];
|
||||
const detection = newDetections[index];
|
||||
const updatedDetection = AnnotationService.calculateResizedPosition(mouseX, mouseY, position, detection, containerRef);
|
||||
newDetections[index] = updatedDetection;
|
||||
setLocalDetections(newDetections);
|
||||
if (onDetectionsChange) {
|
||||
onDetectionsChange(newDetections);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
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];
|
||||
setLocalDetections(newDetections);
|
||||
if (onDetectionsChange) {
|
||||
onDetectionsChange(newDetections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentDetection(null);
|
||||
setMouseDownPos(null);
|
||||
setDragOffset({ x: 0, y: 0 });
|
||||
setResizeData(null);
|
||||
};
|
||||
|
||||
const handleDetectionMouseDown = (e, index) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!localSelectedIndices.includes(index)) {
|
||||
if (!e.ctrlKey) {
|
||||
const newSelectedIndices = [index];
|
||||
setLocalSelectedIndices(newSelectedIndices);
|
||||
onSelectionChange && onSelectionChange(newSelectedIndices);
|
||||
} else {
|
||||
const newSelectedIndices = [...localSelectedIndices, index];
|
||||
setLocalSelectedIndices(newSelectedIndices);
|
||||
onSelectionChange && onSelectionChange(newSelectedIndices);
|
||||
}
|
||||
}
|
||||
|
||||
const { x: mouseX, y: mouseY } = AnnotationService.calculateRelativeCoordinates(e, containerRef);
|
||||
setDragOffset({
|
||||
x: mouseX - localDetections[index].x1,
|
||||
y: mouseY - localDetections[index].y1,
|
||||
});
|
||||
setMouseDownPos({x: mouseX, y: mouseY})
|
||||
|
||||
};
|
||||
|
||||
const handleResize = (e, index, position) => {
|
||||
e.stopPropagation();
|
||||
setResizeData({ index, position });
|
||||
if (!localSelectedIndices.includes(index)) {
|
||||
if (!e.ctrlKey) {
|
||||
setLocalSelectedIndices([index]);
|
||||
onSelectionChange && onSelectionChange([index]);
|
||||
}
|
||||
else{
|
||||
const newSelectedIndices = [...localSelectedIndices, index];
|
||||
setLocalSelectedIndices(newSelectedIndices);
|
||||
onSelectionChange && onSelectionChange(newSelectedIndices);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
>
|
||||
<div ref={containerRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
>
|
||||
{children}
|
||||
<DetectionContainer
|
||||
detections={localDetections}
|
||||
selectedDetectionIndices={localSelectedIndices}
|
||||
onDetectionMouseDown={handleDetectionMouseDown}
|
||||
currentDetection={currentDetection}
|
||||
onResize={handleResize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CanvasEditor;
|
||||
@@ -0,0 +1,85 @@
|
||||
// src/components/Detection.js
|
||||
import React from 'react';
|
||||
|
||||
function Detection({ detection, isSelected, onDetectionMouseDown, onResize }) { // Corrected prop name
|
||||
if (!detection || !detection.class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { Color: color } = detection.class;
|
||||
|
||||
if (!color) {
|
||||
console.error("Color is undefined for detection class:", detection.class);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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 resizeHandleSize = 8;
|
||||
|
||||
const resizeHandles = [
|
||||
{ position: 'top-left', cursor: 'nwse-resize', x: -resizeHandleSize, y: -resizeHandleSize, },
|
||||
{ position: 'top-right', cursor: 'nesw-resize', x: detection.x2 - detection.x1 , y: -resizeHandleSize,},
|
||||
{ position: 'bottom-left', cursor: 'nesw-resize', x: -resizeHandleSize, y: detection.y2 - detection.y1, },
|
||||
{ position: 'bottom-right', cursor: 'nwse-resize', x: detection.x2 - detection.x1, y: detection.y2 - detection.y1 , },
|
||||
{ position: 'top-middle', cursor: 'ns-resize', x: (detection.x2 - detection.x1) / 2 - resizeHandleSize / 2, y: -resizeHandleSize },
|
||||
{ position: 'bottom-middle', cursor: 'ns-resize', x: (detection.x2 - detection.x1) / 2 - resizeHandleSize / 2, y: detection.y2 - detection.y1 },
|
||||
{ position: 'left-middle', cursor: 'ew-resize', x: -resizeHandleSize, y: (detection.y2 - detection.y1) / 2 - resizeHandleSize / 2 },
|
||||
{ position: 'right-middle', cursor: 'ew-resize', x: detection.x2 - detection.x1, y: (detection.y2 - detection.y1) / 2 - resizeHandleSize / 2 },
|
||||
];
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
left: `${detection.x1}px`,
|
||||
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
|
||||
boxSizing: 'border-box',
|
||||
cursor: isSelected ? 'move' : 'default',
|
||||
pointerEvents: 'auto',
|
||||
};
|
||||
|
||||
if (isSelected) {
|
||||
style.border = `3px solid black`;
|
||||
style.boxShadow = `0 0 4px 2px ${borderColor}`; // Use calculated border color
|
||||
}
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
e.stopPropagation();
|
||||
onDetectionMouseDown(e); // Corrected prop name
|
||||
};
|
||||
|
||||
const handleResizeMouseDown = (e, position) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onResize(e, position);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={style} onMouseDown={handleMouseDown}>
|
||||
{isSelected && resizeHandles.map((handle) => (
|
||||
<div
|
||||
key={handle.position}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${handle.x}px`,
|
||||
top: `${handle.y}px`,
|
||||
width: `${resizeHandleSize}px`,
|
||||
height: `${resizeHandleSize}px`,
|
||||
backgroundColor: 'black',
|
||||
cursor: handle.cursor,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onMouseDown={(e) => handleResizeMouseDown(e, handle.position)}
|
||||
/>
|
||||
))}
|
||||
<span style={{ color: 'white', fontSize: '12px', position: "absolute", top: "-18px", left: "0px" }}>{detection.class.Name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Detection;
|
||||
@@ -0,0 +1,85 @@
|
||||
// --- START OF FILE DetectionClassList.js --- (No changes needed)
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DetectionClass from '../models/DetectionClass';
|
||||
|
||||
function DetectionClassList({ onClassSelect }) {
|
||||
const [detectionClasses, setDetectionClasses] = useState([]);
|
||||
const [selectedClassId, setSelectedClassId] = useState(null); // Use an ID for selection
|
||||
|
||||
const colors = [ // Define colors *inside* the component
|
||||
"#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#000000",
|
||||
"#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#808080",
|
||||
"#C00000", "#00C000", "#0000C0", "#C0C000", "#C000C0", "#00C0C0", "#C0C0C0",
|
||||
"#400000", "#004000", "#000040", "#404000", "#400040", "#004040", "#404040",
|
||||
"#200000", "#002000", "#000020", "#202000", "#200020", "#002020", "#202020",
|
||||
"#600000", "#006000", "#000060", "#606000", "#600060", "#006060", "#606060",
|
||||
"#A00000", "#00A000", "#0000A0", "#A0A000", "#A000A0", "#00A0A0", "#A0A0A0",
|
||||
"#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);
|
||||
const g = parseInt(hexColor.slice(3, 5), 16);
|
||||
const b = parseInt(hexColor.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('config.json') // Make sure this path is correct
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const classObjects = data.classes.map(cls => {
|
||||
const color = calculateColor(cls.Id, '1'); // Full opacity for border
|
||||
return new DetectionClass(cls.Id, cls.Name, color);
|
||||
});
|
||||
setDetectionClasses(classObjects);
|
||||
|
||||
if (classObjects.length > 0 && onClassSelect) {
|
||||
onClassSelect(classObjects[0]); // Select the first class by default
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error loading detection classes:", error));
|
||||
}, [onClassSelect]);
|
||||
|
||||
const handleClassClick = (cls) => {
|
||||
setSelectedClassId(cls.Id); // Update the selected ID
|
||||
onClassSelect && onClassSelect(cls);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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 = selectedClassId === cls.Id;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={cls.Id}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
border: `1px solid ${isSelected ? '#000' : '#eee'}`, // Use cls.Color for the selected border
|
||||
backgroundColor: isSelected ? darkBg : backgroundColor, // Conditional background
|
||||
fontSize: '14px',
|
||||
marginBottom: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onClick={() => handleClassClick(cls)}
|
||||
>
|
||||
{cls.Name}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetectionClassList;
|
||||
@@ -0,0 +1,30 @@
|
||||
// src/components/DetectionContainer.js
|
||||
import React from 'react';
|
||||
import Detection from './Detection';
|
||||
|
||||
function DetectionContainer({ detections, selectedDetectionIndices, calculateColor, onDetectionMouseDown, currentDetection, onResize }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{detections.map((detection, index) => (
|
||||
<Detection
|
||||
key={index}
|
||||
detection={detection}
|
||||
isSelected={selectedDetectionIndices.includes(index)}
|
||||
onDetectionMouseDown={(e) => onDetectionMouseDown(e, index)}
|
||||
onResize={(e, position) => onResize(e, index, position)}
|
||||
/>
|
||||
))}
|
||||
{currentDetection && (
|
||||
<Detection
|
||||
detection={currentDetection}
|
||||
isSelected={false}
|
||||
onDetectionMouseDown={() => {}} // No-op handler for the current detection
|
||||
onResize={() => {}} // No-op handler for the current detection
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetectionContainer;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
function MediaList({ files, selectedFile, onFileSelect, onDropNewFiles }) {
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop: onDropNewFiles,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ fontSize: '14px' }}>Files</h3>
|
||||
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
||||
{files.map((file) => (
|
||||
<li
|
||||
key={file.name}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '6px',
|
||||
borderBottom: '1px solid #eee',
|
||||
backgroundColor: selectedFile === file ? '#f0f0f0' : 'transparent',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
onClick={() => onFileSelect(file)}
|
||||
>
|
||||
{file.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div {...getRootProps()} style={{ border: '2px dashed #ccc', padding: '8px', textAlign: 'center', marginTop: '10px', cursor: 'pointer' }}>
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<p style={{ fontSize: '12px' }}>Drop here</p>
|
||||
) : (
|
||||
<p style={{ fontSize: '12px' }}>Drag new files</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaList;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, {useEffect } from 'react';
|
||||
|
||||
function VideoPlayer({ children, videoFile, currentTime, videoRef, isPlaying }) {
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useEffect Videoplayer");
|
||||
if (!(videoFile && videoRef.current))
|
||||
return;
|
||||
|
||||
console.log("Setting video source:", videoFile);
|
||||
videoRef.current.src = URL.createObjectURL(videoFile);
|
||||
videoRef.current.onloadedmetadata = () => {
|
||||
|
||||
};
|
||||
}, [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])
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative'}}>
|
||||
{/* Video Element */}
|
||||
<div style={{ width: '100%', height: '100%'}}>
|
||||
{children}
|
||||
<video
|
||||
ref={videoRef}
|
||||
style={{ width: '100%', height: '100%', pointerEvents: isPlaying ? 'none' : 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoPlayer;
|
||||
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,9 @@
|
||||
class DetectionClass {
|
||||
constructor(id, name, color) {
|
||||
this.Id = id;
|
||||
this.Name = name;
|
||||
this.Color = color;
|
||||
}
|
||||
}
|
||||
|
||||
export default DetectionClass;
|
||||
@@ -0,0 +1,13 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -0,0 +1,138 @@
|
||||
export const calculateRelativeCoordinates = (e, containerRef) => {
|
||||
if (!containerRef.current) return { x: 0, y: 0 };
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
return {
|
||||
x: e.clientX - containerRect.left,
|
||||
y: e.clientY - containerRect.top,
|
||||
};
|
||||
};
|
||||
|
||||
export const isMouseOverDetection = (x, y, detection, containerRef) => {
|
||||
if (!containerRef.current) return false;
|
||||
const { x: relativeX, y: relativeY } = calculateRelativeCoordinates({ clientX: x, clientY: y }, 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, currentDetection, selectedClass, containerRef) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
if (!containerRef.current) return null;
|
||||
const container = containerRef.current;
|
||||
|
||||
canvas.width = container.offsetWidth;
|
||||
canvas.height = container.offsetHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
|
||||
const defaultColor = 'rgba(0,0,0,0.4)'
|
||||
|
||||
detections.forEach(detection => {
|
||||
const color = selectedClass && (detection.class.Id === selectedClass.Id) ? selectedClass.Color : detection.class.Color || defaultColor;
|
||||
|
||||
ctx.fillStyle = color.replace('1', '0.4');
|
||||
ctx.fillRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(detection.x1, detection.y1, detection.x2 - detection.x1, detection.y2 - detection.y1);
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText(detection.class.Name, detection.x1, detection.y1 - 5);
|
||||
});
|
||||
|
||||
if (currentDetection) {
|
||||
const color = selectedClass ? selectedClass.Color : defaultColor;
|
||||
ctx.fillStyle = color.replace('1', '0.4');
|
||||
ctx.fillRect(currentDetection.x1, currentDetection.y1, currentDetection.x2 - currentDetection.x1, currentDetection.y2 - currentDetection.y1);
|
||||
ctx.strokeStyle = color; // Full opacity
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(currentDetection.x1, currentDetection.y1, currentDetection.x2 - currentDetection.x1, currentDetection.y2 - currentDetection.y1);
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.fillText(currentDetection.class.Name, currentDetection.x1, currentDetection.y1 - 5);
|
||||
}
|
||||
|
||||
return canvas.toDataURL('image/png');
|
||||
};
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
let containerWidth = containerRef.current.offsetWidth;
|
||||
let containerHeight = containerRef.current.offsetHeight;
|
||||
|
||||
|
||||
if (newX1 < 0) {
|
||||
newX1 = 0;
|
||||
newX2 = detection.x2 - detection.x1;
|
||||
}
|
||||
if (newY1 < 0) {
|
||||
newY1 = 0;
|
||||
newY2 = detection.y2 - detection.y1;
|
||||
}
|
||||
if (newX2 > containerWidth) {
|
||||
newX2 = containerWidth;
|
||||
newX1 = newX2 - (detection.x2 - detection.x1);
|
||||
}
|
||||
if (newY2 > containerHeight) {
|
||||
newY2 = containerHeight;
|
||||
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;
|
||||
switch (position) {
|
||||
case 'top-left':
|
||||
x1 = Math.min(mouseX, detection.x2 - 5);
|
||||
y1 = Math.min(mouseY, detection.y2 - 5);
|
||||
break;
|
||||
case 'top-right':
|
||||
x2 = Math.max(mouseX, detection.x1 + 5);
|
||||
y1 = Math.min(mouseY, detection.y2 - 5);
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x1 = Math.min(mouseX, detection.x2 - 5);
|
||||
y2 = Math.max(mouseY, detection.y1 + 5);
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x2 = Math.max(mouseX, detection.x1 + 5);
|
||||
y2 = Math.max(mouseY, detection.y1 + 5);
|
||||
break;
|
||||
case 'top-middle':
|
||||
y1 = Math.min(mouseY, detection.y2 - 5);
|
||||
break;
|
||||
case 'bottom-middle':
|
||||
y2 = Math.max(mouseY, detection.y1 + 5);
|
||||
break;
|
||||
case 'left-middle':
|
||||
x1 = Math.min(mouseX, detection.x2 - 5);
|
||||
break;
|
||||
case 'right-middle':
|
||||
x2 = Math.max(mouseX, detection.x1 + 5);
|
||||
break;
|
||||
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 };
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user