episode-visualizer / index.html
qhrong's picture
Update index.html
f0eb7eb verified
raw
history blame
15.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Motion Capture Visualization</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
<!--'Orbitron', sans-serif;Arial, Helvetica, sans-serif-->
<style>
body {
font-family: 'Orbitron', sans-serif;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
background-color: #000;
color: #fff;
}
h1 {
color: gold;
text-shadow: 2px 2px 4px rgba(255, 215, 0, 0.5);
}
.main-container {
display: flex;
width: 100%;
max-width: 1500px;
margin-top: 100px;
}
.episode-list {
width: 150px;
margin-right: 0px;
}
.episode-list label {
display: block;
margin-bottom: 10px;
color: #ddd;
}
.content-container {
display: flex;
justify-content: space-between;
flex-grow: 1;
margin-left: 120px;
}
.video-container {
width: 58%;
}
#plotDiv {
width: 460px;
height: 485px;
}
#episodes-container {
max-height: 500px;
width: 210px;
overflow-y: auto;
overflow-x: hidden;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #222;
margin-left: 10px;
}
#episodes-title {
background-color: #222;
color: #fff;
padding: 10px;
margin: 0;
position: sticky;
top: 0;
z-index: 1;
}
#episodes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 2px;
padding: 10px;
}
.episode-radio {
display: flex;
align-items: center;
}
video {
width: 100%;
height: auto;
}
.controls {
display: flex;
justify-content: center;
margin-top: 2px;
}
· button {
margin: 0 5px;
font-size: 20px;
background-color: #333;
color: #fff;
border: none;
padding: 5px 10px;
cursor: pointer;
}
.checkbox-container label {
display: block;
margin-bottom: 20px;
}
#loadingIndicator {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
color: rgb(255, 255, 255);
padding: 20px;
border-radius: 5px;
z-index: 1000;
}
.checkbox-list {
max-height: 400px;
/* 设置最大高度,超过此高度会出现滚动条 */
overflow-y: auto;
/* 添加垂直滚动条 */
padding-right: 10px;
/* 为滚动条留出空间 */
}
.checkbox-list label {
display: block;
margin-bottom: 0px;
color: #ddd;
}
.checkbox-list input[type="checkbox"] {
margin-right: 10px;
}
/* 自定义滚动条样式(针对WebKit浏览器) */
.checkbox-list::-webkit-scrollbar {
width: 0px;
}
.checkbox-list::-webkit-scrollbar-track {
background: #333;
border-radius: 4px;
}
.checkbox-list::-webkit-scrollbar-thumb {
background: #666;
border-radius: 4px;
}
.checkbox-list::-webkit-scrollbar-thumb:hover {
background: #888;
}
</style>
</head>
<body>
<h1>Motion Capture Visualization</h1>
<div class="main-container">
<!--
<div class="episode-list">
<h3>Episodes</h3>
<label><input type="checkbox"> Episode 1</label>
<label><input type="checkbox"> Episode 2</label>
<label><input type="checkbox"> Episode 29</label>
<label><input type="checkbox"> Episode 30</label>
</div>
-->
<div id="episodes-container">
<h3 id="episodes-title">Episodes</h3>
<div id="episodes-grid"></div>
</div>
<div class="content-container">
<!--
<div class="checkbox-container">
<h3>Tasks</h3>
<label>
<input type="radio" name="videoOption" value="fold_towels" checked> Fold towels
</label>
<label>
<input type="radio" name="videoOption" value="pipette"> Pipette
</label>
<label>
<input type="radio" name="videoOption" value="take_the_item"> Take the item
</label>
<label>
<input type="radio" name="videoOption" value="twist_the_tube"> Twist the tube
</label>
</div>-->
<div class="video-container">
<video id="laptopVideo">
<source src="https://huggingface.co/datasets/cyberorigin/fold_towels/resolve/main/Video/video.mp4"
type="video/mp4">
Your browser does not support the video tag.
</video>
<div class="controls">
<button id="playPauseBtn">▶️</button>
<button id="rewindBtn"></button>
<button id="forwardBtn"></button>
<button id="restartBtn">↩️</button>
</div>
</div>
<div id="plotDiv"></div>
</div>
</div>
<div id="loadingIndicator">Loading...</div>
<script>
let csvFilePath = 'https://huggingface.co/datasets/cyberorigin/fold_towels/resolve/main/MoCap/mocap.csv';
const body_part_names = [
'Left Shoulder', 'Right Upper Arm', 'Left Lower Leg', 'Spine1', 'Right Upper Leg',
'Spine3', 'Right Lower Arm', 'Left Foot', 'Right Lower Leg', 'Right Shoulder',
'Left Hand', 'Left Upper Leg', 'Right Foot', 'Spine', 'Spine2', 'Left Lower Arm',
'Left Toe', 'Neck', 'Right Hand', 'Right Toe', 'Head', 'Left Upper Arm', 'Hips'
];
const laptopVideo = document.getElementById('laptopVideo');
const playPauseBtn = document.getElementById('playPauseBtn');
const rewindBtn = document.getElementById('rewindBtn');
const forwardBtn = document.getElementById('forwardBtn');
const restartBtn = document.getElementById('restartBtn');
const radioButtons = document.querySelectorAll('input[name="videoOption"]');
const episodeContainer = document.getElementById('episodes-container');
const episodesGrid = document.getElementById('episodes-grid');
document.addEventListener('DOMContentLoaded', loadEpisodesCsv);
function loadEpisodesCsv() {
fetch("https://huggingface.co/datasets/cyberorigin/test/resolve/main/episodes.csv")
.then(response => response.text())
.then(data => {
const lines = data.split('\n');
processEpisodes(lines);
})
.catch(error => console.error('Error loading CSV file:', error));
}
function processEpisodes(lines) {
episodesGrid.innerHTML = '';
lines.forEach((line, index) => {
if (line.trim() !== '' && index != 0) {
const id = line.split(',')[0];
const episodeNumber = index;
const radioDiv = document.createElement('div');
radioDiv.className = 'episode-radio';
const radio = document.createElement('input');
radio.type = 'radio';
radio.id = `episode${episodeNumber}`;
radio.name = 'episodeGroup';
radio.addEventListener('change', () => updateVideoAndCSVSource(id));
const label = document.createElement('label');
label.htmlFor = `episode${episodeNumber}`;
label.textContent = `Episode ${episodeNumber}`;
radioDiv.appendChild(radio);
radioDiv.appendChild(label);
episodesGrid.appendChild(radioDiv);
}
});
}
let totalEpisodes = 100; //获取episode的数量
// const container = document.getElementById('episodes-container')
// 循环创建复选框
let animationFrameId;
let isPlaying = false;
function togglePlayPause() {
if (!isPlaying) {
laptopVideo.play();
playPauseBtn.textContent = '⏸️';
isPlaying = true;
animate3DVisualization();
} else {
laptopVideo.pause();
playPauseBtn.textContent = '▶️';
isPlaying = false;
cancelAnimationFrame(animationFrameId);
}
}
function rewind() {
laptopVideo.currentTime -= 5;
update3DVisualization();
}
function forward() {
laptopVideo.currentTime += 5;
update3DVisualization();
}
function restart() {
laptopVideo.currentTime = 0;
update3DVisualization();
}
playPauseBtn.addEventListener('click', togglePlayPause);
rewindBtn.addEventListener('click', rewind);
forwardBtn.addEventListener('click', forward);
restartBtn.addEventListener('click', restart);
function getCoordinates(data, coordinate) {
return body_part_names.map(part => parseFloat(data[`${part}_${coordinate}`]));
}
let frames;
function processData(results) {
console.log("Processing data:", results);
const motion_capture_data = results.data.filter((_, index) => index % 3 === 0);
frames = motion_capture_data.map((row, index) => ({
name: index.toString(),
data: [{
x: getCoordinates(row, 'x'),
y: getCoordinates(row, 'y'),
z: getCoordinates(row, 'z'),
mode: 'markers',
type: 'scatter3d',
marker: { size: 4.8, color: "blue" }
}]
}));
if (frames.length === 0) {
console.error("No frames were created from the data");
return;
}
const initialFrame = frames[0].data[0];
const layout = {
title: {
text: '3D Motion Capture',
font: {
color: 'white',
size: 20 // 设置字体大小
},
x: 0.5, // 设置标题在x轴的位置(0.5表示居中)
y: 1.2
},
paper_bgcolor: 'black',
plot_bgcolor: 'black',
scene: {
xaxis: {
title: 'X',
color: 'white',
gridcolor: 'gray'
},
yaxis: {
title: 'Y',
color: 'white',
gridcolor: 'gray'
},
zaxis: {
title: 'Z',
color: 'white',
gridcolor: 'gray'
},
bgcolor: 'black'
},
font: { color: 'white' },
margin: {
l: 2, // 左边距
r: 2, // 右边距
b: 2, // 底部边距
t: 50, // 顶部边距
pad: 4 // 图表内边距
}
};
Plotly.newPlot('plotDiv', [initialFrame], layout);
}
function update3DVisualization() {
if (!frames) return;
const currentTime = laptopVideo.currentTime;
const totalDuration = laptopVideo.duration;
const frameIndex = Math.floor((currentTime / totalDuration) * frames.length);
const frame = frames[Math.min(frameIndex, frames.length - 1)];
Plotly.animate('plotDiv', frame, {
transition: { duration: 0 },
frame: { duration: 0, redraw: true }
});
}
function animate3DVisualization() {
update3DVisualization();
if (isPlaying) {
animationFrameId = requestAnimationFrame(animate3DVisualization);
}
}
//function updateVideoAndCSVSource() {
//const selectedOption = document.querySelector('input[name="videoOption"]:checked').value;
//const videoUrl = `https://huggingface.co/datasets/cyberorigin/${selectedOption}/resolve/main/Video/video.mp4`;
//if (selectedOption != "twist_the_tube") {
// csvFilePath = `https://huggingface.co/datasets/cyberorigin/${selectedOption}/resolve/main/MoCap/mocap.csv`;
//}
//else {
// csvFilePath = `https://huggingface.co/datasets/cyberorigin/${selectedOption}/resolve/main/MoCap/MoCap.csv`;
//}
function updateVideoAndCSVSource(id) {
const selectedOption = document.querySelector('input[name="episodeGroup"]:checked').value;
const videoUrl = `https://huggingface.co/datasets/cyberorigin/pick_and_place/resolve/main/color/${id}.mp4`;
csvFilePath = `https://huggingface.co/datasets/cyberorigin/pick_and_place/resolve/main/motion_capture/${id}.csv`;
laptopVideo.pause();
laptopVideo.querySelector('source').src = videoUrl;
laptopVideo.load();
isPlaying = false;
playPauseBtn.textContent = '▶️';
// Fetch and process the new CSV data
fetchAndProcessActionCSV();
}
function fetchAndProcessActionCSV() {
fetch(csvFilePath)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(csvString => {
console.log("CSV data loaded successfully");
Papa.parse(csvString, {
header: true,
dynamicTyping: true,
complete: processData
});
})
.catch(error => console.error('Error loading the CSV file:', error));
}
//radioButtons.forEach(radio => {
// radio.addEventListener('change', updateVideoAndCSVSource);
//});
// Initial CSV fetch and processing
fetchAndProcessActionCSV();
</script>
</body>
</html>