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
+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;