zxymimi23451's picture
Upload 258 files
78360e7 verified
<!-- Copyright (c) 2024-2025 Bytedance Ltd. and/or its affiliates
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Track Point Editor</title>
<style>
.btn-row {
display: flex;
align-items: center;
margin: 8px 0;
}
.btn-row > * { margin-right: 12px; }
body { font-family: sans-serif; margin: 16px; }
#topControls, #bottomControls { margin-bottom: 12px; }
button, input, select, label { margin: 4px; }
#canvas { border:1px solid #ccc; display: block; margin: auto; }
#canvas { cursor: crosshair; }
#trajProgress { width: 200px; height: 16px; margin-left:12px; }
</style>
</head>
<body>
<h2>Track Point Editor</h2>
<!-- Top controls -->
<div id="topControls" class="btn-row">
<input type="file" id="fileInput" accept="image/*">
<button id="storeBtn">Store Tracks</button>
</div>
<!-- Main drawing canvas -->
<canvas id="canvas"></canvas>
<!-- Track controls -->
<div id="bottomControls">
<div class="btn-row">
<button id="addTrackBtn">Add Freehand Track</button>
<button id="deleteLastBtn">Delete Last Track</button>
<progress id="trajProgress" max="121" value="0" style="display:none;"></progress>
</div>
<div class="btn-row">
<button id="placeCircleBtn">Place Circle</button>
<button id="addCirclePointBtn">Add Circle Point</button>
<label>Radius:
<input type="range" id="radiusSlider" min="10" max="800" value="50" style="display:none;">
</label>
</div>
<div class="btn-row">
<button id="addStaticBtn">Add Static Point</button>
<label>Static Frames:
<input type="number" id="staticFramesInput" value="121" min="1" style="width:60px">
</label>
</div>
<div class="btn-row">
<select id="trackSelect" style="min-width:160px;"></select>
<div id="colorIndicator"
style="
width:16px;
height:16px;
border:1px solid #444;
display:inline-block;
vertical-align:middle;
margin-left:8px;
pointer-events:none;
visibility:hidden;
">
</div>
<button id="deleteTrackBtn">Delete Selected</button>
<button id="editTrackBtn">Edit Track</button>
<button id="duplicateTrackBtn">Duplicate Track</button>
</div>
<!-- Global motion offset -->
<div class="btn-row">
<label>Motion X (px/frame):
<input type="number" id="motionXInput" value="0" style="width:60px">
</label>
<label>Motion Y (px/frame):
<input type="number" id="motionYInput" value="0" style="width:60px">
</label>
<button id="applySelectedMotionBtn">Add to Selected</button>
<button id="applyAllMotionBtn">Add to All</button>
</div>
</div>
<script>
// ——— DOM refs —————————————————————————————————————————
const canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
fileIn = document.getElementById('fileInput'),
storeBtn = document.getElementById('storeBtn'),
addTrackBtn = document.getElementById('addTrackBtn'),
deleteLastBtn = document.getElementById('deleteLastBtn'),
placeCircleBtn = document.getElementById('placeCircleBtn'),
addCirclePointBtn = document.getElementById('addCirclePointBtn'),
addStaticBtn = document.getElementById('addStaticBtn'),
staticFramesInput = document.getElementById('staticFramesInput'),
radiusSlider = document.getElementById('radiusSlider'),
trackSelect = document.getElementById('trackSelect'),
deleteTrackBtn = document.getElementById('deleteTrackBtn'),
editTrackBtn = document.getElementById('editTrackBtn'),
duplicateTrackBtn = document.getElementById('duplicateTrackBtn'),
trajProg = document.getElementById('trajProgress'),
colorIndicator = document.getElementById('colorIndicator'),
motionXInput = document.getElementById('motionXInput'),
motionYInput = document.getElementById('motionYInput'),
applySelectedMotionBtn = document.getElementById('applySelectedMotionBtn'),
applyAllMotionBtn = document.getElementById('applyAllMotionBtn');
let img, image_id, ext, origW, origH,
scaleX=1, scaleY=1;
// track data
let free_tracks = [], current_track = [], drawing=false, motionCounter=0;
let circle=null, static_trajs=[];
let mode='', selectedTrack=null, editMode=false, editInfo=null, duplicateBuffer=null;
const COLORS=['red','green','blue','cyan','magenta','yellow','black'],
FIXED_LENGTH=121,
editSigma = 5/Math.sqrt(2*Math.log(2));
// ——— Upload & scale image ————————————————————————————
fileIn.addEventListener('change', async e => {
const f = e.target.files[0]; if (!f) return;
const fd = new FormData(); fd.append('image',f);
const res = await fetch('/upload_image',{method:'POST',body:fd});
const js = await res.json();
image_id=js.image_id; ext=js.ext;
origW=js.orig_width; origH=js.orig_height;
if(origW>=origH){
canvas.width=800; canvas.height=Math.round(origH*800/origW);
} else {
canvas.height=800; canvas.width=Math.round(origW*800/origH);
}
scaleX=origW/canvas.width; scaleY=origH/canvas.height;
img=new Image(); img.src=js.image_url;
img.onload=()=>{
free_tracks=[]; current_track=[];
circle=null; static_trajs=[];
mode=selectedTrack=''; editMode=false; editInfo=null; duplicateBuffer=null;
trajProg.style.display='none';
radiusSlider.style.display='none';
trackSelect.innerHTML='';
redraw();
};
});
// ——— Store tracks + depth —————————————————————————
storeBtn.onclick = async () => {
if(!image_id) return alert('Load an image first');
const fh = free_tracks.map(tr=>tr.map(p=>({x:p.x*scaleX,y:p.y*scaleY}))),
ct = (circle?.trajectories||[]).map(tr=>tr.map(p=>({x:p.x*scaleX,y:p.y*scaleY}))),
st = static_trajs.map(tr=>tr.map(p=>({x:p.x*scaleX,y:p.y*scaleY})));
const payload = {
image_id, ext,
tracks: fh,
circle_trajectories: ct.concat(st)
};
const res = await fetch('/store_tracks',{
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
const js = await res.json();
img.src=js.overlay_url;
img.onload=()=>ctx.drawImage(img,0,0,canvas.width,canvas.height);
// reset UI
free_tracks=[]; circle=null; static_trajs=[];
mode=selectedTrack=''; editMode=false; editInfo=null; duplicateBuffer=null;
trajProg.style.display='none';
radiusSlider.style.display='none';
trackSelect.innerHTML='';
redraw();
};
// ——— Control buttons —————————————————————————————
addTrackBtn.onclick = ()=>{
mode='free'; drawing=true; current_track=[]; motionCounter=0;
trajProg.max=FIXED_LENGTH; trajProg.value=0;
trajProg.style.display='inline-block';
};
deleteLastBtn.onclick = ()=>{
if(drawing){
drawing=false; current_track=[]; trajProg.style.display='none';
} else if(free_tracks.length){
free_tracks.pop(); updateTrackSelect(); redraw();
}
updateColorIndicator();
};
placeCircleBtn.onclick = ()=>{ mode='placeCircle'; drawing=false; };
addCirclePointBtn.onclick = ()=>{ if(!circle) alert('Place circle first'); else mode='addCirclePt'; };
addStaticBtn.onclick = ()=>{ mode='placeStatic'; };
duplicateTrackBtn.onclick = ()=>{
if(!selectedTrack) return alert('Select a track first');
const arr = selectedTrack.type==='free'
? free_tracks[selectedTrack.idx]
: selectedTrack.type==='circle'
? circle.trajectories[selectedTrack.idx]
: static_trajs[selectedTrack.idx];
duplicateBuffer = arr.map(p=>({x:p.x,y:p.y}));
mode='duplicate'; canvas.style.cursor='copy';
};
radiusSlider.oninput = ()=>{
if(!circle) return;
circle.radius = +radiusSlider.value;
circle.trajectories.forEach((traj,i)=>{
const θ = circle.angles[i];
traj.push({
x: circle.cx + Math.cos(θ)*circle.radius,
y: circle.cy + Math.sin(θ)*circle.radius
});
});
if(selectedTrack?.type==='circle')
trajProg.value = circle.trajectories[selectedTrack.idx].length;
redraw();
};
deleteTrackBtn.onclick = ()=>{
if(!selectedTrack) return;
const {type,idx} = selectedTrack;
if(type==='free') free_tracks.splice(idx,1);
else if(type==='circle'){
circle.trajectories.splice(idx,1);
circle.angles.splice(idx,1);
} else {
static_trajs.splice(idx,1);
}
selectedTrack=null;
trajProg.style.display='none';
updateTrackSelect();
redraw();
updateColorIndicator();
};
editTrackBtn.onclick = ()=>{
if(!selectedTrack) return alert('Select a track first');
editMode=!editMode;
editTrackBtn.textContent = editMode?'Stop Editing':'Edit Track';
};
// ——— Track select & depth init —————————————————————
function updateTrackSelect(){
trackSelect.innerHTML='';
free_tracks.forEach((_,i)=>{
const o=document.createElement('option');
o.value=JSON.stringify({type:'free',idx:i});
o.textContent=`Point ${i+1}`;
trackSelect.appendChild(o);
});
if(circle){
circle.trajectories.forEach((_,i)=>{
const o=document.createElement('option');
o.value=JSON.stringify({type:'circle',idx:i});
o.textContent=`CirclePt ${i+1}`;
trackSelect.appendChild(o);
});
}
static_trajs.forEach((_,i)=>{
const o=document.createElement('option');
o.value=JSON.stringify({type:'static',idx:i});
o.textContent=`StaticPt ${i+1}`;
trackSelect.appendChild(o);
});
if(trackSelect.options.length){
trackSelect.selectedIndex=0;
trackSelect.onchange();
}
updateColorIndicator();
}
function applyMotionToTrajectory(traj, dx, dy) {
traj.forEach((pt, frameIdx) => {
pt.x += dx * frameIdx;
pt.y += dy * frameIdx;
});
}
applySelectedMotionBtn.onclick = () => {
if (!selectedTrack) {
return alert('Please select a track first');
}
const dx = parseFloat(motionXInput.value) || 0;
const dy = parseFloat(motionYInput.value) || 0;
// pick the underlying array
let arr = null;
if (selectedTrack.type === 'free') {
arr = free_tracks[selectedTrack.idx];
} else if (selectedTrack.type === 'circle') {
arr = circle.trajectories[selectedTrack.idx];
} else { // 'static'
arr = static_trajs[selectedTrack.idx];
}
applyMotionToTrajectory(arr, dx, dy);
redraw();
};
// 2) Add motion to every track on the canvas
applyAllMotionBtn.onclick = () => {
const dx = parseFloat(motionXInput.value) || 0;
const dy = parseFloat(motionYInput.value) || 0;
// freehand tracks
free_tracks.forEach(tr => applyMotionToTrajectory(tr, dx, dy));
// circle‑based tracks
if (circle) {
circle.trajectories.forEach(tr => applyMotionToTrajectory(tr, dx, dy));
}
// static points (now will move over frames)
static_trajs.forEach(tr => applyMotionToTrajectory(tr, dx, dy));
redraw();
};
trackSelect.onchange = ()=>{
if(!trackSelect.value){
selectedTrack=null;
trajProg.style.display='none';
return;
}
selectedTrack = JSON.parse(trackSelect.value);
if(selectedTrack.type==='circle'){
trajProg.style.display='inline-block';
trajProg.max=FIXED_LENGTH;
trajProg.value=circle.trajectories[selectedTrack.idx].length;
} else if(selectedTrack.type==='free'){
trajProg.style.display='inline-block';
trajProg.max=FIXED_LENGTH;
trajProg.value=free_tracks[selectedTrack.idx].length;
} else {
trajProg.style.display='none';
}
updateColorIndicator();
};
// ——— Canvas drawing ————————————————————————————————
canvas.addEventListener('mousedown', e=>{
const r=canvas.getBoundingClientRect(),
x=e.clientX-r.left, y=e.clientY-r.top;
// place circle
if(mode==='placeCircle'){
circle={cx:x,cy:y,radius:50,angles:[],trajectories:[]};
radiusSlider.max=Math.min(canvas.width,canvas.height)|0;
radiusSlider.value=50; radiusSlider.style.display='inline';
mode=''; updateTrackSelect(); redraw(); return;
}
// add circle point
if(mode==='addCirclePt'){
const dx=x-circle.cx, dy=y-circle.cy;
const θ=Math.atan2(dy,dx);
const px=circle.cx+Math.cos(θ)*circle.radius;
const py=circle.cy+Math.sin(θ)*circle.radius;
circle.angles.push(θ);
circle.trajectories.push([{x:px,y:py}]);
mode=''; updateTrackSelect(); redraw(); return;
}
// add static
if (mode === 'placeStatic') {
// how many frames to “hold” the point
const len = parseInt(staticFramesInput.value, 10) || FIXED_LENGTH;
// duplicate the click‐point len times
const traj = Array.from({ length: len }, () => ({ x, y }));
// push into free_tracks so it's drawn & edited just like any freehand curve
free_tracks.push(traj);
// reset state
mode = '';
updateTrackSelect();
redraw();
return;
}
// duplicate
if(mode==='duplicate' && duplicateBuffer){
const orig = duplicateBuffer;
// click defines translation by first point
const dx = x - orig[0].x, dy = y - orig[0].y;
const newTr = orig.map(p=>({x:p.x+dx, y:p.y+dy}));
free_tracks.push(newTr);
mode=''; duplicateBuffer=null; canvas.style.cursor='crosshair';
updateTrackSelect(); redraw(); return;
}
// editing
if(editMode && selectedTrack){
const arr = selectedTrack.type==='free'
? free_tracks[selectedTrack.idx]
: selectedTrack.type==='circle'
? circle.trajectories[selectedTrack.idx]
: static_trajs[selectedTrack.idx];
let best=0,bd=Infinity;
arr.forEach((p,i)=>{
const d=(p.x-x)**2+(p.y-y)**2;
if(d<bd){ bd=d; best=i; }
});
editInfo={ trackType:selectedTrack.type,
trackIdx:selectedTrack.idx,
ptIdx:best,
startX:x, startY:y };
return;
}
// freehand start
if(mode==='free'){
drawing=true; motionCounter=0;
current_track=[{x,y}];
redraw();
}
});
canvas.addEventListener('mousemove', e=>{
const r=canvas.getBoundingClientRect(),
x=e.clientX-r.left, y=e.clientY-r.top;
// edit mode
if(editMode && editInfo){
const dx=x-editInfo.startX,
dy=y-editInfo.startY;
const {trackType,trackIdx,ptIdx} = editInfo;
const arr = trackType==='free'
? free_tracks[trackIdx]
: trackType==='circle'
? circle.trajectories[trackIdx]
: static_trajs[trackIdx];
arr.forEach((p,i)=>{
const d=i-ptIdx;
const w=Math.exp(-0.5*(d*d)/(editSigma*editSigma));
p.x+=dx*w; p.y+=dy*w;
});
editInfo.startX=x; editInfo.startY=y;
if(selectedTrack?.type==='circle')
trajProg.value=circle.trajectories[selectedTrack.idx].length;
redraw(); return;
}
// freehand draw
if(drawing && (e.buttons&1)){
motionCounter++;
if(motionCounter%2===0){
current_track.push({x,y});
trajProg.value = Math.min(current_track.length, trajProg.max);
redraw();
}
}
});
canvas.addEventListener('mouseup', ()=>{
if(editMode && editInfo){ editInfo=null; return; }
if(drawing){
free_tracks.push(current_track.slice());
drawing=false; current_track=[];
updateTrackSelect(); redraw();
}
});
function updateColorIndicator() {
const idx = trackSelect.selectedIndex;
if (idx < 0) {
colorIndicator.style.visibility = 'hidden';
return;
}
// Pick the color by index
const col = COLORS[idx % COLORS.length];
colorIndicator.style.backgroundColor = col;
colorIndicator.style.visibility = 'visible';
}
// ——— redraw ———
function redraw(){
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (img.complete) ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// set a fatter line for all strokes
ctx.lineWidth = 2;
// — freehand (and static‑turned‑freehand) tracks —
free_tracks.forEach((tr, i) => {
const col = COLORS[i % COLORS.length];
ctx.strokeStyle = col;
ctx.fillStyle = col;
if (tr.length === 0) return;
// check if every point equals the first
const allSame = tr.every(p => p.x === tr[0].x && p.y === tr[0].y);
if (allSame) {
// draw a filled circle for a “static” dot
ctx.beginPath();
ctx.arc(tr[0].x, tr[0].y, 4, 0, 2 * Math.PI);
ctx.fill();
} else {
// normal polyline
ctx.beginPath();
tr.forEach((p, j) =>
j ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)
);
ctx.stroke();
}
});
if(drawing && current_track.length){
ctx.strokeStyle='black';
ctx.beginPath();
current_track.forEach((p,j)=>
j? ctx.lineTo(p.x,p.y): ctx.moveTo(p.x,p.y));
ctx.stroke();
}
// — circle trajectories —
if (circle) {
// circle outline
ctx.strokeStyle = 'white';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(circle.cx, circle.cy, circle.radius, 0, 2 * Math.PI);
ctx.stroke();
circle.trajectories.forEach((tr, i) => {
const col = COLORS[(free_tracks.length + i) % COLORS.length];
ctx.strokeStyle = col;
ctx.fillStyle = col;
ctx.lineWidth = 2;
if (tr.length <= 1) {
// single‑point circle trajectory → dot
ctx.beginPath();
ctx.arc(tr[0].x, tr[0].y, 4, 0, 2 * Math.PI);
ctx.fill();
} else {
// normal circle track
ctx.beginPath();
tr.forEach((p, j) =>
j ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)
);
ctx.stroke();
// white handle at last point
const lp = tr[tr.length - 1];
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(lp.x, lp.y, 4, 0, 2 * Math.PI);
ctx.fill();
}
});
}
// — static_trajs (if you still use them separately) —
static_trajs.forEach((tr, i) => {
const p = tr[0];
ctx.fillStyle = 'orange';
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fill();
});
}
</script>
</body>
</html>