Azaion Suite to the web. First commit. Only rough sketches of future components is done.

This commit is contained in:
Alex Bezdieniezhnykh
2025-03-18 16:28:15 +02:00
commit 2ab732c6b4
32 changed files with 28735 additions and 0 deletions
+38
View File
@@ -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
View File
@@ -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;
+8
View File
@@ -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();
});
+8
View File
@@ -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;
+15
View File
@@ -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;
+20
View File
@@ -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;
+156
View File
@@ -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;
+229
View File
@@ -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;
+85
View File
@@ -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;
+85
View File
@@ -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;
+30
View File
@@ -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;
+41
View File
@@ -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;
+48
View File
@@ -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;
+13
View File
@@ -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;
}
+17
View File
@@ -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();
+1
View File
@@ -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

+9
View File
@@ -0,0 +1,9 @@
class DetectionClass {
constructor(id, name, color) {
this.Id = id;
this.Name = name;
this.Color = color;
}
}
export default DetectionClass;
+13
View File
@@ -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;
+138
View File
@@ -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 };
};
+5
View File
@@ -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';