-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Camera Path Recorder</title> | ||
<style> | ||
body { | ||
margin: 0; | ||
overflow: hidden; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
background: #333; | ||
} | ||
video { | ||
position: absolute; | ||
transform: scaleX(-1); /* Flip horizontally */ | ||
} | ||
canvas { | ||
position: absolute; | ||
z-index: 2; | ||
} | ||
.controls { | ||
position: absolute; | ||
z-index: 3; | ||
bottom: 20px; | ||
left: 20px; | ||
display: flex; | ||
flex-direction: column; | ||
background: rgba(0, 0, 0, 0.6); | ||
padding: 10px; | ||
border-radius: 5px; | ||
color: white; | ||
font-family: Arial, sans-serif; | ||
} | ||
select, button { | ||
margin-bottom: 10px; | ||
padding: 5px; | ||
font-size: 14px; | ||
} | ||
button { | ||
cursor: pointer; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<video autoplay playsinline></video> | ||
<canvas></canvas> | ||
<div class="controls"> | ||
<select id="cameraSelect"></select> | ||
<select id="cameraModeSelect"></select> | ||
<button id="fullscreenButton">Go Fullscreen</button> | ||
<button id="startStopCaptureButton">Start Capture</button> | ||
<button id="downloadButton" disabled>Download Capture</button> | ||
<button id="regeneratePathButton">Regenerate Path</button> | ||
</div> | ||
<script> | ||
const video = document.querySelector('video'); | ||
const canvas = document.querySelector('canvas'); | ||
const ctx = canvas.getContext('2d'); | ||
const cameraSelect = document.getElementById('cameraSelect'); | ||
const cameraModeSelect = document.getElementById('cameraModeSelect'); | ||
const fullscreenButton = document.getElementById('fullscreenButton'); | ||
const startStopCaptureButton = document.getElementById('startStopCaptureButton'); | ||
const downloadButton = document.getElementById('downloadButton'); | ||
const regeneratePathButton = document.getElementById('regeneratePathButton'); | ||
let mediaStream = null; | ||
let animationFrameId = null; | ||
let isRecording = false; | ||
let recordedPoints = []; | ||
let crosshairIndex = 0; | ||
let path = []; | ||
let recordingStartTime = null; | ||
let selectedCameraId = null; | ||
let selectedCameraMode = null; | ||
|
||
// Resize canvas and regenerate path when window resizes | ||
function resizeCanvas() { | ||
canvas.width = window.innerWidth; | ||
canvas.height = window.innerHeight; | ||
generatePath(); | ||
} | ||
window.addEventListener('resize', resizeCanvas); | ||
|
||
// Generate a random path | ||
function generatePath() { | ||
path = []; | ||
for (let i = 0; i < 100; i++) { | ||
path.push({ | ||
x: Math.random() * canvas.width, | ||
y: Math.random() * canvas.height | ||
}); | ||
} | ||
crosshairIndex = 0; | ||
drawFrame(); | ||
} | ||
|
||
// Draw the video and overlay the path | ||
function drawFrame() { | ||
if (!mediaStream) return; | ||
ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | ||
|
||
// Draw the path | ||
ctx.strokeStyle = 'white'; | ||
ctx.lineWidth = 2; | ||
ctx.beginPath(); | ||
path.forEach((point, index) => { | ||
if (index === 0) ctx.moveTo(point.x, point.y); | ||
else ctx.lineTo(point.x, point.y); | ||
}); | ||
ctx.stroke(); | ||
|
||
// Draw the crosshair | ||
const crosshair = path[crosshairIndex]; | ||
ctx.strokeStyle = 'green'; | ||
ctx.beginPath(); | ||
ctx.moveTo(crosshair.x - 10, crosshair.y); | ||
ctx.lineTo(crosshair.x + 10, crosshair.y); | ||
ctx.moveTo(crosshair.x, crosshair.y - 10); | ||
ctx.lineTo(crosshair.x, crosshair.y + 10); | ||
ctx.stroke(); | ||
|
||
// Record the crosshair position if recording | ||
if (isRecording) { | ||
const frame = video.requestVideoFrameCallback((now, metadata) => { | ||
recordedPoints.push({ | ||
x: crosshair.x, | ||
y: crosshair.y, | ||
frameNumber: metadata.presentedFrames, | ||
timestamp: (Date.now() - recordingStartTime) / 1000 | ||
}); | ||
}); | ||
} | ||
|
||
animationFrameId = requestAnimationFrame(drawFrame); | ||
} | ||
|
||
// Switch to the selected camera | ||
async function switchCamera() { | ||
if (mediaStream) { | ||
mediaStream.getTracks().forEach(track => track.stop()); | ||
} | ||
const constraints = { | ||
video: { | ||
deviceId: selectedCameraId ? { exact: selectedCameraId } : undefined, | ||
width: selectedCameraMode ? selectedCameraMode.width : undefined, | ||
height: selectedCameraMode ? selectedCameraMode.height : undefined, | ||
frameRate: selectedCameraMode ? selectedCameraMode.frameRate : undefined | ||
} | ||
}; | ||
mediaStream = await navigator.mediaDevices.getUserMedia(constraints); | ||
video.srcObject = mediaStream; | ||
drawFrame(); | ||
} | ||
|
||
// Populate camera and mode options | ||
async function populateCameraOptions() { | ||
const devices = await navigator.mediaDevices.enumerateDevices(); | ||
const videoDevices = devices.filter(device => device.kind === 'videoinput'); | ||
cameraSelect.innerHTML = ''; | ||
videoDevices.forEach((device, index) => { | ||
const option = document.createElement('option'); | ||
option.value = device.deviceId; | ||
option.textContent = device.label || `Camera ${index + 1}`; | ||
cameraSelect.appendChild(option); | ||
}); | ||
selectedCameraId = videoDevices[0]?.deviceId; | ||
cameraSelect.addEventListener('change', (e) => { | ||
selectedCameraId = e.target.value; | ||
switchCamera(); | ||
}); | ||
|
||
// Populate camera mode options | ||
// (Hardcoded for now, real mode selection will require advanced APIs like MediaStreamTrack capabilities) | ||
cameraModeSelect.innerHTML = ` | ||
<option value="hd">HD (1280x720, 30fps)</option> | ||
<option value="fullhd">Full HD (1920x1080, 60fps)</option> | ||
`; | ||
cameraModeSelect.addEventListener('change', (e) => { | ||
const value = e.target.value; | ||
selectedCameraMode = value === 'hd' | ||
? { width: 1280, height: 720, frameRate: 30 } | ||
: { width: 1920, height: 1080, frameRate: 60 }; | ||
switchCamera(); | ||
}); | ||
switchCamera(); | ||
} | ||
|
||
// Start or stop recording | ||
function toggleRecording() { | ||
isRecording = !isRecording; | ||
startStopCaptureButton.textContent = isRecording ? 'Stop Capture' : 'Start Capture'; | ||
if (isRecording) { | ||
recordingStartTime = Date.now(); | ||
recordedPoints = []; | ||
} else { | ||
downloadButton.disabled = false; | ||
} | ||
} | ||
|
||
// Download video and points JSON | ||
function downloadFiles() { | ||
const pointsBlob = new Blob([JSON.stringify({ | ||
points: recordedPoints, | ||
camera: selectedCameraId, | ||
mode: selectedCameraMode, | ||
recordingStartedAt: new Date(recordingStartTime).toISOString() | ||
}, null, 2)], { type: 'application/json' }); | ||
const pointsURL = URL.createObjectURL(pointsBlob); | ||
const pointsLink = document.createElement('a'); | ||
pointsLink.href = pointsURL; | ||
pointsLink.download = 'points.json'; | ||
pointsLink.click(); | ||
|
||
// Video download not implemented in this demo | ||
alert('Video download is not implemented in this demo.'); | ||
} | ||
|
||
// Handle key presses for moving the crosshair | ||
document.addEventListener('keydown', (e) => { | ||
if (e.key === 'j') { | ||
crosshairIndex = Math.min(crosshairIndex + 1, path.length - 1); | ||
} else if (e.key === 'k') { | ||
crosshairIndex = Math.max(crosshairIndex - 1, 0); | ||
} | ||
}); | ||
|
||
// Fullscreen toggle | ||
fullscreenButton.addEventListener('click', () => { | ||
if (!document.fullscreenElement) { | ||
document.documentElement.requestFullscreen(); | ||
} else { | ||
document.exitFullscreen(); | ||
} | ||
}); | ||
|
||
// Initialize | ||
regeneratePathButton.addEventListener('click', generatePath); | ||
startStopCaptureButton.addEventListener('click', toggleRecording); | ||
downloadButton.addEventListener('click', downloadFiles); | ||
populateCameraOptions(); | ||
resizeCanvas(); | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
create a single file html+js+css app that has: | ||
- a select for camera selection - should allow selection among available cameras | ||
- a select for camera mode (resolution and framerate) - should allow selection among modes available for selected camera | ||
- a button to go fullscreen | ||
- start/stop capture button - should start a recording into file | ||
- download capture button - should allow to download 2 files recorded video and points json (more about points below) | ||
- a button to regenerate path (more about path later), if the window is resized then the path should be generated on the whole window | ||
- on the background it should show an image from the selected camera (flipped horizontally) - the image should be centered on the screen and scaled to fit available space, gray bars of background on left/right or top/bottom are acceptable if the aspect ratio of the video doesn't match the aspect ratio of the window/screen | ||
Keep in mind that the permissions to use camera should be requested before listing available devices | ||
|
||
When the app is opened it should generate a random path on the screen and draw it above the video. A starting point on the path should be highlighted by a green crosshair. | ||
- j key - goes to next point - smoothly moved the crosshair along the path (1px at a time, make sure the crosshair doesn't just jump to the next point, the position should be interpolated between current and next point) | ||
- k key - goes to the previous point | ||
- space key - starts and stops the recording/capture | ||
- f key - toggles fullscreen mode | ||
|
||
The background (video from camera) and the path should take the whole window. The buttons should be displayed over the background. | ||
When the recording is started for each frame of the video x and y of the crosshair in screen (not window or client area!) coordinates should be recorded. Keep in mind that j and k can move the cursor during the recording. For each point in points json there should be x, y, frame number (starting from 0), timestamp in seconds from the start of the recording (not from when the camera started the translation). Points json also should have info about selected camera, camera mode and the date and time of when the recording was started |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
opencv-python | ||
mediapipe | ||
pynput | ||
numpy | ||
screeninfo | ||
torch | ||
scikit-learn | ||
pyautogui | ||
keyboard |