Download corner_canvas.js from LiangLabUMB/CellposeCellCounter_GroundTruth: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/spaces/LiangLabUMB/CellposeCellCounter_GroundTruth/resolve/main/corner_canvas.js
- Command line
-
hf download hf://spaces/LiangLabUMB/CellposeCellCounter_GroundTruth/corner_canvas.js
-
curl -L -o corner_canvas.js https://huggingface.co/spaces/LiangLabUMB/CellposeCellCounter_GroundTruth/resolve/main/corner_canvas.js
11.8 kB
| // 4-corner crop canvas, one instance per <div class="cp-root" data-key="...">. | |
| // | |
| // All corner work happens in the browser: clicking, dragging, zoom and pan | |
| // never call the server, so Gradio never swaps the image for a progress bar. | |
| // The server is contacted only to load an image and, on request, to run grid | |
| // auto-detection, whose proposal comes back as 4 ordinary draggable corners. | |
| // | |
| // Python <-> canvas: | |
| // CP.load(payloadJSON) image or corner proposal for payload.key | |
| // CP.get(key) JSON [[x,y] x4] in FULL-RES pixels, or "" if unset | |
| // CP.seed(key) JSON [x,y] full-res auto-detect seed, or "" | |
| (function () { | |
| "use strict"; | |
| const CORNER_HIT_PX = 12; // screen px: grab radius for a handle | |
| const DRAG_PX = 4; // movement beyond this turns a click into a pan | |
| const MAX_ZOOM = 40; | |
| const ST = {}; // key -> state; survives a re-mounted element | |
| const PENDING = {}; // key -> payload that arrived before mounting | |
| function state(key) { | |
| if (!ST[key]) { | |
| ST[key] = { key, root: null, cv: null, ctx: null, el: {}, | |
| img: null, w: 0, h: 0, scale: 1, corners: [], | |
| view: { s: 1, tx: 0, ty: 0 }, fitted: false, | |
| armed: false, seed: null, busy: false, auto: false, | |
| down: null, drag: -1, panning: false, seq: null, | |
| dirty: true }; | |
| } | |
| return ST[key]; | |
| } | |
| // ------------------------------------------------------------- mounting | |
| function bind(root) { | |
| const S = state(root.dataset.key); | |
| if (S.root === root) return; | |
| S.root = root; | |
| S.cv = root.querySelector("canvas"); | |
| S.ctx = S.cv.getContext("2d"); | |
| for (const n of ["stage", "info", "auto", "reset", "fit", "empty"]) { | |
| S.el[n] = root.querySelector(".cp-" + n); | |
| } | |
| const cv = S.cv; | |
| cv.addEventListener("pointerdown", (e) => onDown(S, e)); | |
| cv.addEventListener("pointermove", (e) => onMove(S, e)); | |
| cv.addEventListener("pointerup", (e) => onUp(S, e)); | |
| cv.addEventListener("pointercancel", (e) => onUp(S, e)); | |
| cv.addEventListener("wheel", (e) => onWheel(S, e), { passive: false }); | |
| cv.addEventListener("contextmenu", (e) => e.preventDefault()); | |
| S.el.auto.addEventListener("click", () => arm(S)); | |
| S.el.reset.addEventListener("click", () => { | |
| S.corners = []; S.auto = false; S.armed = false; ui(S); dirty(S); | |
| }); | |
| S.el.fit.addEventListener("click", () => { fit(S); dirty(S); }); | |
| new ResizeObserver(() => { size(S); dirty(S); }).observe(S.el.stage); | |
| size(S); | |
| ui(S); | |
| if (PENDING[S.key]) { const p = PENDING[S.key]; delete PENDING[S.key]; load(p); } | |
| } | |
| function scan() { | |
| document.querySelectorAll(".cp-root").forEach(bind); | |
| } | |
| function size(S) { | |
| const dpr = window.devicePixelRatio || 1; | |
| const w = S.el.stage.clientWidth, h = S.el.stage.clientHeight; | |
| if (!w || !h) return; // inside a closed accordion | |
| S.cv.width = Math.round(w * dpr); | |
| S.cv.height = Math.round(h * dpr); | |
| if (!S.fitted && S.img) fit(S); | |
| } | |
| // ------------------------------------------------------------- loading | |
| function loadImage(src) { | |
| return new Promise((res, rej) => { | |
| const im = new Image(); | |
| im.onload = () => res(im); im.onerror = rej; im.src = src; | |
| }); | |
| } | |
| async function load(payload) { | |
| if (!payload) return; | |
| let p; | |
| try { p = typeof payload === "string" ? JSON.parse(payload) : payload; } | |
| catch (e) { return; } | |
| if (!p || !p.key) return; | |
| const S = state(p.key); | |
| if (p.seq && p.seq === S.seq) return; // same payload re-delivered | |
| scan(); | |
| if (!S.root) { PENDING[p.key] = p; return; } | |
| S.seq = p.seq; | |
| if (p.mode === "image") { | |
| S.img = p.img ? await loadImage(p.img) : null; | |
| S.w = p.w || 0; S.h = p.h || 0; S.scale = p.scale || 1; | |
| S.corners = []; S.auto = false; S.armed = false; S.busy = false; | |
| S.fitted = false; fit(S); | |
| } else if (p.mode === "corners") { | |
| S.busy = false; | |
| if (p.corners && p.corners.length === 4) { | |
| S.corners = p.corners.map(([x, y]) => [x * S.scale, y * S.scale]); | |
| S.auto = true; | |
| } | |
| S.msg = p.msg || ""; | |
| } | |
| ui(S); dirty(S); | |
| } | |
| // ------------------------------------------------------------- view | |
| function fit(S) { | |
| const cw = S.el.stage.clientWidth, ch = S.el.stage.clientHeight; | |
| if (!S.img || !cw || !ch) return; | |
| const s = Math.min(cw / S.w, ch / S.h) * 0.97; | |
| S.view = { s, tx: (cw - S.w * s) / 2, ty: (ch - S.h * s) / 2 }; | |
| S.fitted = true; | |
| } | |
| function pos(S, e) { | |
| const r = S.cv.getBoundingClientRect(); | |
| return [e.clientX - r.left, e.clientY - r.top]; | |
| } | |
| function toImg(S, sx, sy) { | |
| return [(sx - S.view.tx) / S.view.s, (sy - S.view.ty) / S.view.s]; | |
| } | |
| function toScr(S, ix, iy) { | |
| return [ix * S.view.s + S.view.tx, iy * S.view.s + S.view.ty]; | |
| } | |
| function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } | |
| function onWheel(S, e) { | |
| if (!S.img) return; | |
| e.preventDefault(); | |
| const [sx, sy] = pos(S, e); | |
| const fitS = Math.min(S.el.stage.clientWidth / S.w, | |
| S.el.stage.clientHeight / S.h); | |
| const s = clamp(S.view.s * Math.pow(1.0015, -e.deltaY), fitS * 0.5, MAX_ZOOM); | |
| const [ix, iy] = toImg(S, sx, sy); | |
| S.view = { s, tx: sx - ix * s, ty: sy - iy * s }; | |
| dirty(S); | |
| } | |
| // ------------------------------------------------------------- pointer | |
| function cornerAt(S, sx, sy) { | |
| let best = -1, bd = CORNER_HIT_PX; | |
| S.corners.forEach(([x, y], k) => { | |
| const [px, py] = toScr(S, x, y); | |
| const d = Math.hypot(px - sx, py - sy); | |
| if (d <= bd) { bd = d; best = k; } | |
| }); | |
| return best; | |
| } | |
| function onDown(S, e) { | |
| if (!S.img) return; | |
| S.cv.setPointerCapture(e.pointerId); | |
| const [sx, sy] = pos(S, e); | |
| S.down = { sx, sy, tx: S.view.tx, ty: S.view.ty, b: e.button }; | |
| S.panning = e.button !== 0; | |
| S.drag = e.button === 0 && !S.armed ? cornerAt(S, sx, sy) : -1; | |
| } | |
| function onMove(S, e) { | |
| const [sx, sy] = pos(S, e); | |
| if (!S.down) { | |
| const over = S.img && !S.armed && cornerAt(S, sx, sy) >= 0; | |
| S.cv.style.cursor = over ? "grab" : "crosshair"; | |
| return; | |
| } | |
| const dx = sx - S.down.sx, dy = sy - S.down.sy; | |
| if (S.drag >= 0) { | |
| const [ix, iy] = toImg(S, sx, sy); | |
| S.corners[S.drag] = [clamp(ix, 0, S.w - 1), clamp(iy, 0, S.h - 1)]; | |
| S.auto = false; S.msg = ""; | |
| S.cv.style.cursor = "grabbing"; | |
| ui(S); dirty(S); | |
| return; | |
| } | |
| if (!S.panning && Math.hypot(dx, dy) > DRAG_PX) S.panning = true; | |
| if (S.panning) { | |
| S.view.tx = S.down.tx + dx; S.view.ty = S.down.ty + dy; dirty(S); | |
| } | |
| } | |
| function onUp(S, e) { | |
| if (!S.down) return; | |
| const [sx, sy] = pos(S, e); | |
| const click = !S.panning && S.drag < 0 && S.down.b === 0 && | |
| e.type !== "pointercancel"; | |
| S.down = null; S.panning = false; S.drag = -1; | |
| if (!click) return; | |
| const [ix, iy] = toImg(S, sx, sy); | |
| if (ix < 0 || iy < 0 || ix >= S.w || iy >= S.h) return; | |
| if (S.armed) { detect(S, ix, iy); return; } | |
| if (S.corners.length >= 4) { | |
| S.msg = "All 4 corners set — drag a handle to adjust, or Reset."; | |
| ui(S); return; | |
| } | |
| S.corners.push([ix, iy]); S.auto = false; S.msg = ""; | |
| ui(S); dirty(S); | |
| } | |
| // ------------------------------------------------------------- auto-detect | |
| function arm(S) { | |
| if (!S.img || S.busy) return; | |
| S.armed = !S.armed; | |
| ui(S); dirty(S); | |
| } | |
| function detect(S, ix, iy) { | |
| S.armed = false; S.busy = true; | |
| S.seed = [ix / S.scale, iy / S.scale]; | |
| S.msg = "Detecting the grid…"; | |
| ui(S); dirty(S); | |
| // Python runs on a hidden Gradio button; its js pre-step reads CP.seed. | |
| let b = document.getElementById("cp-detect-" + S.key); | |
| if (b && b.tagName !== "BUTTON") b = b.querySelector("button"); | |
| if (b) b.click(); | |
| else { S.busy = false; S.msg = "Auto-detect unavailable."; ui(S); } | |
| } | |
| // ------------------------------------------------------------- text | |
| function ui(S) { | |
| if (!S.root) return; | |
| S.el.empty.style.display = S.img ? "none" : ""; | |
| S.el.auto.classList.toggle("cp-on", S.armed); | |
| S.el.auto.disabled = !S.img || S.busy; | |
| S.el.reset.disabled = !S.corners.length; | |
| let t; | |
| if (!S.img) t = "No image"; | |
| else if (S.busy) t = S.msg || "Detecting…"; | |
| else if (S.armed) t = "Click the CENTRE of the 4×4 block to auto-detect"; | |
| else if (S.corners.length < 4) { | |
| t = S.corners.length + " / 4 corners — click corner " + | |
| (S.corners.length + 1) + " (any order), or ⌖ Auto"; | |
| if (S.msg) t = S.msg + " · " + t; | |
| } else { | |
| t = (S.auto ? "✓ Auto-detected" : "✓ 4 corners set") + | |
| " — drag a handle to adjust"; | |
| if (S.msg) t = S.msg; | |
| } | |
| S.el.info.textContent = t; | |
| S.el.info.classList.toggle("cp-ok", !!S.img && S.corners.length === 4 && !S.busy); | |
| } | |
| // ------------------------------------------------------------- draw | |
| function dirty(S) { S.dirty = true; } | |
| function frame() { | |
| requestAnimationFrame(frame); | |
| for (const k in ST) { | |
| const S = ST[k]; | |
| if (S.dirty && S.ctx) { S.dirty = false; draw(S); } | |
| } | |
| } | |
| function ordered(c) { | |
| if (c.length !== 4) return c; | |
| const mx = c.reduce((a, p) => a + p[0], 0) / 4; | |
| const my = c.reduce((a, p) => a + p[1], 0) / 4; | |
| return c.slice().sort((a, b) => Math.atan2(a[1] - my, a[0] - mx) - | |
| Math.atan2(b[1] - my, b[0] - mx)); | |
| } | |
| function draw(S) { | |
| const ctx = S.ctx, dpr = window.devicePixelRatio || 1; | |
| ctx.setTransform(1, 0, 0, 1, 0, 0); | |
| ctx.fillStyle = "#1b1d22"; | |
| ctx.fillRect(0, 0, S.cv.width, S.cv.height); | |
| if (!S.img) return; | |
| const { s, tx, ty } = S.view; | |
| ctx.setTransform(dpr * s, 0, 0, dpr * s, dpr * tx, dpr * ty); | |
| ctx.imageSmoothingEnabled = s < 2; | |
| ctx.drawImage(S.img, 0, 0); | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| const pts = ordered(S.corners).map(([x, y]) => toScr(S, x, y)); | |
| const col = S.auto ? "#3ddc84" : "#4aaaff"; | |
| ctx.lineWidth = 2; ctx.strokeStyle = col; | |
| if (pts.length >= 2) { | |
| ctx.beginPath(); | |
| ctx.moveTo(pts[0][0], pts[0][1]); | |
| for (const p of pts.slice(1)) ctx.lineTo(p[0], p[1]); | |
| if (pts.length === 4) { | |
| ctx.closePath(); | |
| ctx.fillStyle = S.auto ? "rgba(61,220,132,0.12)" : "rgba(74,170,255,0.12)"; | |
| ctx.fill(); | |
| } | |
| ctx.stroke(); | |
| } | |
| for (const [x, y] of S.corners) { | |
| const [px, py] = toScr(S, x, y); | |
| ctx.beginPath(); ctx.arc(px, py, 9, 0, 2 * Math.PI); | |
| ctx.fillStyle = col; ctx.globalAlpha = 0.85; ctx.fill(); ctx.globalAlpha = 1; | |
| ctx.strokeStyle = "#fff"; ctx.stroke(); | |
| ctx.beginPath(); | |
| ctx.moveTo(px - 4, py); ctx.lineTo(px + 4, py); | |
| ctx.moveTo(px, py - 4); ctx.lineTo(px, py + 4); | |
| ctx.stroke(); | |
| } | |
| if (S.seed && S.busy) { | |
| const [px, py] = toScr(S, S.seed[0] * S.scale, S.seed[1] * S.scale); | |
| ctx.strokeStyle = "#ffcc00"; ctx.lineWidth = 2; | |
| ctx.beginPath(); ctx.arc(px, py, 12, 0, 2 * Math.PI); ctx.stroke(); | |
| } | |
| } | |
| // ------------------------------------------------------------- API | |
| function get(key) { | |
| const S = ST[key]; | |
| if (!S || !S.img || S.corners.length !== 4) return ""; | |
| return JSON.stringify(S.corners.map(([x, y]) => [x / S.scale, y / S.scale])); | |
| } | |
| function seed(key) { | |
| const S = ST[key]; | |
| return S && S.seed ? JSON.stringify(S.seed) : ""; | |
| } | |
| function fail(key, msg) { // detection finished without corners | |
| const S = ST[key]; | |
| if (S) { S.busy = false; S.msg = msg || ""; ui(S); dirty(S); } | |
| } | |
| window.CP = { load, get, seed, fail, _st: ST }; | |
| setInterval(scan, 300); | |
| requestAnimationFrame(frame); | |
| })(); | |