diff options
| -rw-r--r-- | cnn_v3/training/adjust.html | 239 | ||||
| -rw-r--r-- | tools/adjust.html | 419 |
2 files changed, 419 insertions, 239 deletions
diff --git a/cnn_v3/training/adjust.html b/cnn_v3/training/adjust.html deleted file mode 100644 index 219145c..0000000 --- a/cnn_v3/training/adjust.html +++ /dev/null @@ -1,239 +0,0 @@ -<!DOCTYPE html> -<html> -<head> -<meta charset="UTF-8"> -<title>Align & Optimize</title> -<style> -body{font-family:sans-serif;text-align:center;background:#f5f5f5} -canvas{border:1px solid #ccc;margin-top:10px;cursor:grab} -</style> -</head> -<body> - -<h3>Align & Optimize (Arrows=move, Shift+Arrows=scale)</h3> - -<input type="file" id="f1" accept="image/*"> -<input type="file" id="f2" accept="image/*"><br><br> - -Alpha <input type="range" id="alpha" min="0.2" max="1" step="0.01" value="0.5"> -<br><br> - -<button id="crop">Crop</button> -<button id="opt">Optimize</button> -<button id="dl" disabled>Download</button> - -<div>MSE: <span id="score">-</span></div> - -<canvas id="c"></canvas> - -<script> -const c=document.getElementById('c'),x=c.getContext('2d'); -let i1=new Image(),i2=new Image(),l1=0,l2=0; - -let ox=0,oy=0,sx=1,sy=1; -let drag=0,lx,ly,mx=0,my=0,a=document.getElementById('alpha'); - -let out1,out2; - -// finer than before -const PAN_STEP=0.3; -const SCALE_STEP=0.002; // 🔬 very fine zoom -const DRAG_SPEED=0.4; - -// MSE buffers (reused) -let buf1=document.createElement('canvas'); -let buf2=document.createElement('canvas'); -let b1=buf1.getContext('2d'); -let b2=buf2.getContext('2d'); -const SAMPLE=128; - -// load images -f1.onchange=e=>{ - i1=new Image(); - i1.onload=()=>{ - l1=1;c.width=i1.width;c.height=i1.height; - ox=oy=0;sx=sy=1;draw(); - }; - i1.src=URL.createObjectURL(e.target.files[0]); -}; - -f2.onchange=e=>{ - i2=new Image(); - i2.onload=()=>{ - l2=1; - ox=(c.width-i2.width)/2; - oy=(c.height-i2.height)/2; - sx=sy=1;draw(); - }; - i2.src=URL.createObjectURL(e.target.files[0]); -}; - -function draw(){ - if(!l1)return; - x.clearRect(0,0,c.width,c.height); - x.drawImage(i1,0,0); - if(l2){ - x.save(); - x.globalAlpha=a.value; - x.translate(ox,oy); - x.scale(sx,sy); - x.drawImage(i2,0,0); - x.restore(); - } - updateScore(); -} - -// --- FAST MSE --- -function computeMSE(){ - let x2=ox,y2=oy,w2=i2.width*sx,h2=i2.height*sy; - let x0=Math.max(0,x2),y0=Math.max(0,y2); - let x1=Math.min(c.width,x2+w2),y1=Math.min(c.height,y2+h2); - let w=Math.floor(x1-x0),h=Math.floor(y1-y0); - if(w<=0||h<=0)return Infinity; - - let scale=Math.min(1,SAMPLE/Math.max(w,h)); - let sw=Math.max(1,Math.floor(w*scale)); - let sh=Math.max(1,Math.floor(h*scale)); - - buf1.width=buf2.width=sw; - buf1.height=buf2.height=sh; - - b1.drawImage(i1,x0,y0,w,h,0,0,sw,sh); - b2.drawImage(i2,(x0-ox)/sx,(y0-oy)/sy,w/sx,h/sy,0,0,sw,sh); - - let d1=b1.getImageData(0,0,sw,sh).data; - let d2=b2.getImageData(0,0,sw,sh).data; - - let mse=0,n=sw*sh; - - for(let i=0;i<d1.length;i+=4){ - let g1=0.299*d1[i]+0.587*d1[i+1]+0.114*d1[i+2]; - let g2=0.299*d2[i]+0.587*d2[i+1]+0.114*d2[i+2]; - let d=g1-g2; - mse+=d*d; - } - - return mse/n; -} - -function updateScore(){ - if(l1&&l2){ - let s=computeMSE(); - score.textContent=isFinite(s)?s.toFixed(2):"-"; - } -} - -// mouse -c.onmousemove=e=>{ - mx=e.offsetX;my=e.offsetY; - if(!drag)return; - ox+=(e.offsetX-lx)*DRAG_SPEED; - oy+=(e.offsetY-ly)*DRAG_SPEED; - lx=e.offsetX;ly=e.offsetY; - draw(); -}; - -c.onmousedown=e=>{drag=1;lx=e.offsetX;ly=e.offsetY;c.style.cursor="grabbing";} -c.onmouseup=c.onmouseleave=()=>{drag=0;c.style.cursor="grab";}; - -a.oninput=draw; - -// keyboard -document.onkeydown=e=>{ - if(!l2)return; - if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key)) e.preventDefault(); - - if(e.shiftKey){ - // scale around mouse - let ix=(mx-ox)/sx,iy=(my-oy)/sy; - - if(e.key==="ArrowRight") sx+=SCALE_STEP; - if(e.key==="ArrowLeft") sx-=SCALE_STEP; - if(e.key==="ArrowUp") sy+=SCALE_STEP; - if(e.key==="ArrowDown") sy-=SCALE_STEP; - - ox=mx-ix*sx; - oy=my-iy*sy; - - } else { - // move - if(e.key==="ArrowRight") ox+=PAN_STEP; - if(e.key==="ArrowLeft") ox-=PAN_STEP; - if(e.key==="ArrowUp") oy-=PAN_STEP; - if(e.key==="ArrowDown") oy+=PAN_STEP; - } - - draw(); -}; - -// --- OPTIMIZER (safe + fast) --- -opt.onclick=async ()=>{ - if(!l1||!l2)return; - - let best={ox,oy,sx,sy,score:computeMSE()}; - - for(let iter=0;iter<10;iter++){ - - let stepO=0.5/(iter+1); - let stepS=0.005/(iter+1); - - let valuesO=[-stepO,-stepO/2,0,stepO/2,stepO]; - let valuesS=[-stepS,-stepS/2,0,stepS/2,stepS]; - - for(let dx of valuesO) - for(let dy of valuesO) - for(let dsx of valuesS) - for(let dsy of valuesS){ - - let tox=best.ox+dx; - let toy=best.oy+dy; - let tsx=best.sx+dsx; - let tsy=best.sy+dsy; - - ox=tox;oy=toy;sx=tsx;sy=tsy; - - let s=computeMSE(); - if(s<best.score){ - best={ox:tox,oy:toy,sx:tsx,sy:tsy,score:s}; - } - } - - ox=best.ox;oy=best.oy;sx=best.sx;sy=best.sy; - draw(); - - await new Promise(r=>setTimeout(r,0)); // keep UI responsive - } -}; - -// crop + download -crop.onclick=()=>{ - let x2=ox,y2=oy,w2=i2.width*sx,h2=i2.height*sy; - let x0=Math.max(0,x2),y0=Math.max(0,y2); - let x1=Math.min(c.width,x2+w2),y1=Math.min(c.height,y2+h2); - let w=x1-x0,h=y1-y0; - if(w<=0||h<=0)return alert("no overlap"); - - out1=document.createElement('canvas'); - out2=document.createElement('canvas'); - out1.width=out2.width=w; - out1.height=out2.height=h; - - out1.getContext('2d').drawImage(i1,x0,y0,w,h,0,0,w,h); - out2.getContext('2d').drawImage(i2,(x0-ox)/sx,(y0-oy)/sy,w/sx,h/sy,0,0,w,h); - - dl.disabled=0; -}; - -dl.onclick=()=>{ - let d=(cv,n)=>{ - let a=document.createElement('a'); - a.download=n+'.clipped.png'; - a.href=cv.toDataURL(); - a.click(); - }; - d(out1,'image1');d(out2,'image2'); -}; -</script> - -</body> -</html>
\ No newline at end of file diff --git a/tools/adjust.html b/tools/adjust.html new file mode 100644 index 0000000..3b5b243 --- /dev/null +++ b/tools/adjust.html @@ -0,0 +1,419 @@ +<!-- tools for https://colorifyai.art/photo-to-sketch/#playground in 'Ink Sketch' mode --> + +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<title>Align & Optimize</title> +<style> +body{font-family:sans-serif;text-align:center;background:#f5f5f5} +canvas{border:1px solid #ccc;margin-top:10px;cursor:grab} +textarea{width:90%;height:90px;margin:10px;font-family:monospace;font-size:12px} +button{margin:5px} +button:disabled{opacity:0.5} +#status{font-size:12px;color:#666;height:16px} +</style> +</head> +<body> + +<h3>Align & Optimize</h3> + +<textarea id="cmd"></textarea><br> +<button id="copy">Copy commands</button><br> + +<input type="file" id="f1"> +<input type="file" id="f2"><br><br> + +<button id="opt">Optimize</button> +<button id="optZoom">Optimize Zoom</button> +<button id="crop">Crop</button> +<button id="dl" disabled>Download</button> +<br> +Iterations: <input type="range" id="iterSlider" min="2" max="30" value="8" style="vertical-align:middle;width:120px"> +<span id="iterVal">8</span> + +<div>MSE: <span id="score">-</span> Overlap: <span id="overlap">-</span></div> +<div id="status"></div> + +<canvas id="cv"></canvas> + +<script> +const canvas = document.getElementById('cv'); +function ctx() { return canvas.getContext('2d'); } +const scoreEl = document.getElementById('score'); +const overlapEl = document.getElementById('overlap'); +const statusEl = document.getElementById('status'); +const cmdEl = document.getElementById('cmd'); + +let i1 = new Image(), i2 = new Image(), l1 = 0, l2 = 0; +let name1 = "image1.png", name2 = "image2.png"; + +let ox = 0, oy = 0, sx = 1, sy = 1; +let mx = 0, my = 0, drag = 0, lx = 0, ly = 0; + +let out1, out2; +let pyr1 = [], pyr2 = []; +const LEVELS = 4; + +let optimizing = false; + +// --- pyramid --- +// FIX: clamp x+1 within row to avoid cross-row bleed +function buildPyramid(img) { + let levels = []; + let cv = document.createElement('canvas'); + cv.width = img.width; cv.height = img.height; + let x = cv.getContext('2d'); + x.drawImage(img, 0, 0); + + let d = x.getImageData(0, 0, img.width, img.height).data; + let g = new Float32Array(img.width * img.height); + for (let i = 0, j = 0; i < d.length; i += 4, j++) + g[j] = 0.299 * d[i] + 0.587 * d[i+1] + 0.114 * d[i+2]; + levels.push({ w: img.width, h: img.height, data: g }); + + for (let l = 1; l < LEVELS; l++) { + let p = levels[l-1]; + let w2 = Math.floor(p.w / 2), h2 = Math.floor(p.h / 2); + let g2 = new Float32Array(w2 * h2); + for (let y = 0; y < h2; y++) { + for (let xi = 0; xi < w2; xi++) { + // clamp xi+1 to stay within the row (p.w-1) + let x0 = 2 * xi, x1 = Math.min(2 * xi + 1, p.w - 1); + let y0 = 2 * y, y1 = Math.min(2 * y + 1, p.h - 1); + g2[y * w2 + xi] = ( + p.data[y0 * p.w + x0] + p.data[y0 * p.w + x1] + + p.data[y1 * p.w + x0] + p.data[y1 * p.w + x1] + ) * 0.25; + } + } + levels.push({ w: w2, h: h2, data: g2 }); + } + return levels; +} + +// --- bilinear sample with bounds check --- +// FIX: return NaN for out-of-bounds so caller can skip; clamp indices correctly +function sample(img, x, y) { + if (x < 0 || y < 0 || x >= img.w || y >= img.h) return NaN; + let x0 = Math.floor(x), y0 = Math.floor(y); + let x1 = Math.min(x0 + 1, img.w - 1); // FIX: was img.w (off-by-one) + let y1 = Math.min(y0 + 1, img.h - 1); // FIX: was img.h (off-by-one) + let dx = x - x0, dy = y - y0; + let i00 = img.data[y0 * img.w + x0]; + let i10 = img.data[y0 * img.w + x1]; + let i01 = img.data[y1 * img.w + x0]; + let i11 = img.data[y1 * img.w + x1]; + return (i00 * (1-dx) + i10 * dx) * (1-dy) + (i01 * (1-dx) + i11 * dx) * dy; +} + +// --- MSE over overlapping region only --- +// FIX: compute the intersection of A's rect and the mapped B rect in A-space, +// then only accumulate pixels where B is defined. +function computeMSELevel(level) { + if (!pyr1.length || !pyr2.length) return Infinity; + let A = pyr1[level], B = pyr2[level]; + let sF = 1 / (2 ** level); + + // B's bounding box in A-pixel space at this pyramid level + let tox = ox * sF, toy = oy * sF; + let tsx = sx, tsy = sy; + + // B covers [tox, tox + B.w*tsx] x [toy, toy + B.h*tsy] in A-space + let bx0 = tox, bx1 = tox + B.w * tsx; + let by0 = toy, by1 = toy + B.h * tsy; + + // Intersect with A's extent [0, A.w] x [0, A.h] + let ix0 = Math.max(0, Math.ceil(bx0)); + let iy0 = Math.max(0, Math.ceil(by0)); + let ix1 = Math.min(A.w, Math.floor(bx1)); + let iy1 = Math.min(A.h, Math.floor(by1)); + + if (ix1 <= ix0 || iy1 <= iy0) return Infinity; + + let mse = 0, n = 0; + for (let y = iy0; y < iy1; y++) { + for (let x = ix0; x < ix1; x++) { + let bx = (x - tox) / tsx; + let by = (y - toy) / tsy; + let bVal = sample(B, bx, by); + if (isNaN(bVal)) continue; + let d = A.data[y * A.w + x] - bVal; + mse += d * d; + n++; + } + } + + // Update overlap display (only at level 0) + if (level === 0) { + let pct = n / (A.w * A.h) * 100; + overlapEl.textContent = n > 0 ? pct.toFixed(1) + '%' : '0%'; + } + + return n ? mse / n : Infinity; +} + +function computeMSE() { return computeMSELevel(0); } + +// --- draw --- +function draw() { + if (!l1) return; + const c = ctx(); + c.clearRect(0, 0, canvas.width, canvas.height); + c.drawImage(i1, 0, 0); + if (l2) { + c.save(); + c.globalAlpha = 0.5; + c.translate(ox, oy); + c.scale(sx, sy); + c.drawImage(i2, 0, 0); + c.restore(); + } + scoreEl.textContent = computeMSE().toFixed(2); + updateCmd(); +} + +// --- load --- +f1.onchange = e => { + let f = e.target.files[0]; if (!f) return; + name1 = f.name; + i1 = new Image(); + i1.onload = () => { + l1 = 1; + canvas.width = i1.width; canvas.height = i1.height; + pyr1 = buildPyramid(i1); + draw(); + }; + i1.src = URL.createObjectURL(f); +}; + +f2.onchange = e => { + let f = e.target.files[0]; if (!f) return; + name2 = f.name; + i2 = new Image(); + i2.onload = () => { + l2 = 1; + pyr2 = buildPyramid(i2); + ox = (canvas.width - i2.width) / 2; + oy = (canvas.height - i2.height) / 2; + draw(); + }; + i2.src = URL.createObjectURL(f); +}; + +// --- mouse --- +canvas.onmousemove = e => { + mx = e.offsetX; my = e.offsetY; + if (!drag) return; + ox += (e.offsetX - lx); + oy += (e.offsetY - ly); + lx = e.offsetX; ly = e.offsetY; + draw(); +}; +canvas.onmousedown = e => { + drag = 1; + lx = e.offsetX; ly = e.offsetY; + canvas.style.cursor = "grabbing"; +}; +canvas.onmouseup = canvas.onmouseleave = () => { + drag = 0; + canvas.style.cursor = "grab"; +}; + +// --- scroll to zoom (mouse-anchored) --- +canvas.onwheel = e => { + if (!l2) return; + e.preventDefault(); + let factor = e.deltaY < 0 ? 1.01 : 0.99; + let ix = (mx - ox) / sx; + let iy = (my - oy) / sy; + sx *= factor; sy *= factor; + ox = mx - ix * sx; + oy = my - iy * sy; + draw(); +}; + +// --- keyboard --- +document.addEventListener("keydown", e => { + // FIX: don't intercept keys when typing in textarea + if (document.activeElement === cmdEl) return; + + if (!l2) return; + if (e.key.startsWith("Arrow")) e.preventDefault(); + const PAN_STEP = 1; + const SCALE_STEP = 0.002; + if (e.shiftKey) { + let ix = (mx - ox) / sx; + let iy = (my - oy) / sy; + if (e.key === "ArrowRight") sx += SCALE_STEP; + if (e.key === "ArrowLeft") sx -= SCALE_STEP; + if (e.key === "ArrowUp") sy += SCALE_STEP; + if (e.key === "ArrowDown") sy -= SCALE_STEP; + ox = mx - ix * sx; + oy = my - iy * sy; + } else { + if (e.key === "ArrowRight") ox += PAN_STEP; + if (e.key === "ArrowLeft") ox -= PAN_STEP; + if (e.key === "ArrowUp") oy -= PAN_STEP; + if (e.key === "ArrowDown") oy += PAN_STEP; + } + draw(); +}); + +// FIX: space only fires optimize when not in textarea +document.addEventListener("keydown", e => { + if (document.activeElement === cmdEl) return; + if (e.code === "Space") { + e.preventDefault(); + opt.click(); + } +}); + +// --- slider --- +const iterSlider = document.getElementById('iterSlider'); +const iterVal = document.getElementById('iterVal'); +iterSlider.oninput = () => iterVal.textContent = iterSlider.value; +function numIters() { return parseInt(iterSlider.value); } + +// --- set busy state --- +function setBusy(busy) { + optimizing = busy; + opt.disabled = busy; + optZoom.disabled = busy; + statusEl.textContent = busy ? "Optimizing…" : ""; +} + +// --- probe MSE without mutating globals --- +// Temporarily sets ox/oy/sx/sy, evaluates, then restores. +function probeMSE(tox, toy, tsx, tsy, level) { + let pox = ox, poy = oy, psx = sx, psy = sy; + ox = tox; oy = toy; sx = tsx; sy = tsy; + let s = computeMSELevel(level); + ox = pox; oy = poy; sx = psx; sy = psy; + return s; +} + +// --- optimizer (translation + uniform scale) --- +opt.onclick = async () => { + if (!l1 || !l2 || optimizing) return; + setBusy(true); + const ITERS = numIters(); + for (let level = LEVELS - 1; level >= 0; level--) { + let best = { ox, oy, sx, sy, score: computeMSELevel(level) }; + for (let iter = 0; iter < ITERS; iter++) { + let stepO = (2 ** level) / (iter + 1); + let stepS = 0.01 / (iter + 1); + for (let dx of [-1, 0, 1]) + for (let dy of [-1, 0, 1]) + for (let ds of [-1, 0, 1]) { + let tox = best.ox + dx * stepO; + let toy = best.oy + dy * stepO; + let ts = best.sx + ds * stepS; + if (ts <= 0.01) continue; + // FIX: probe without touching globals + let s = probeMSE(tox, toy, ts, ts, level); + if (s < best.score) + best = { ox: tox, oy: toy, sx: ts, sy: ts, score: s }; + } + // commit the best found this iteration + ox = best.ox; oy = best.oy; sx = best.sx; sy = best.sy; + draw(); + await new Promise(r => setTimeout(r, 0)); + } + } + setBusy(false); +}; + +// --- optimize zoom (mouse-anchor invariant) --- +optZoom.onclick = async () => { + if (!l1 || !l2 || optimizing) return; + setBusy(true); + const ITERS = numIters(); + let ax = mx, ay = my; + + for (let level = LEVELS - 1; level >= 0; level--) { + // best stores the full transform, not just sx + let best = { ox, oy, sx, sy, score: computeMSELevel(level) }; + + for (let iter = 0; iter < ITERS; iter++) { + let stepS = 0.02 / (iter + 1); + + for (let ds of [-1, 0, 1]) { + let ts = best.sx + ds * stepS; + if (ts <= 0.01) continue; + // anchor invariant: point (ax,ay) in canvas == point (ix,iy) in image2 + // must satisfy: ax = ox + ix*sx => ix = (ax - ox) / sx + // after scale change to ts: tox = ax - ix*ts + // derive from COMMITTED globals (best.ox/sx), not mid-loop state + let ix = (ax - best.ox) / best.sx; + let iy = (ay - best.oy) / best.sy; + let tox = ax - ix * ts; + let toy = ay - iy * ts; + let s = probeMSE(tox, toy, ts, ts, level); + if (s < best.score) best = { ox: tox, oy: toy, sx: ts, sy: ts, score: s }; + } + + // commit best directly — no re-derivation needed + ox = best.ox; oy = best.oy; sx = best.sx; sy = best.sy; + + draw(); + await new Promise(r => setTimeout(r, 0)); + } + } + setBusy(false); +}; + +// --- crop --- +crop.onclick = () => { + let x2 = ox, y2 = oy, w2 = i2.width * sx, h2 = i2.height * sy; + let x0 = Math.max(0, x2), y0 = Math.max(0, y2); + let x1 = Math.min(canvas.width, x2 + w2), y1 = Math.min(canvas.height, y2 + h2); + let w = x1 - x0, h = y1 - y0; + if (w <= 0 || h <= 0) return alert("no overlap"); + + out1 = document.createElement('canvas'); + out2 = document.createElement('canvas'); + out1.width = out2.width = w; out1.height = out2.height = h; + + out1.getContext('2d').drawImage(i1, x0, y0, w, h, 0, 0, w, h); + out2.getContext('2d').drawImage( + i2, (x0 - ox) / sx, (y0 - oy) / sy, w / sx, h / sy, 0, 0, w, h + ); + + dl.disabled = 0; +}; + +// --- download --- +dl.onclick = () => { + let save = (cv, n) => { + let a = document.createElement('a'); + a.download = n + '.clipped.png'; + a.href = cv.toDataURL(); + a.click(); + }; + save(out1, 'image1'); + save(out2, 'image2'); +}; + +// --- copy --- +copy.onclick = () => navigator.clipboard.writeText(cmdEl.value); + +// --- ImageMagick command --- +function updateCmd() { + if (!l1 || !l2) return; + let x2 = ox, y2 = oy, w2 = i2.width * sx, h2 = i2.height * sy; + let x0 = Math.max(0, x2), y0 = Math.max(0, y2); + let x1 = Math.min(canvas.width, x2 + w2), y1 = Math.min(canvas.height, y2 + h2); + let w = Math.floor(x1 - x0), h = Math.floor(y1 - y0); + if (w <= 0 || h <= 0) return; + let crop = `${w}x${h}+${Math.round(x0)}+${Math.round(y0)}`; + // AffineProjection: a,b,d,e,tx,ty (uniform scale → a=e=sx) + let a = sx, b = 0, d = 0, e = sy, tx = ox, ty = oy; + cmdEl.value = +`magick "${name1}" -crop ${crop} +repage "${name1}.clipped.png" +magick "${name2}" -virtual-pixel black -distort AffineProjection "${a},${b},${d},${e},${tx},${ty}" -crop ${crop} +repage "${name2}.clipped.png"`; +} +</script> + +</body> +</html>
\ No newline at end of file |
