diff --git a/bfs_shot_loop.py b/bfs_shot_loop.py index 11ec9a5..0fa7406 100644 --- a/bfs_shot_loop.py +++ b/bfs_shot_loop.py @@ -1390,19 +1390,25 @@ def uncrop(result: torch.Tensor, shot: dict, edge_hardness: float = 0.0, match_c return out -def mask_preview(path: str, analysis: dict, start: int, length: int, spec: dict, count: int = 6) -> dict: - """A few frames of the shot with the mask in red and the crop box, as data URLs.""" +def mask_preview(path: str, analysis: dict, start: int, length: int, spec: dict, count: int = 6, + color: str = "#ff283c", opacity: float = 0.55) -> dict: + """A few frames of the shot with the selected region tinted and the crop box, as data URLs.""" import cv2 r = shot_mask(path, analysis, start, length, spec) masks, (w, h), box = r["masks"], r["size"], r["box"] src = _timeline(analysis["n_src"], analysis["fps_src"], float(analysis["fps"])) picks = sorted(set(int(round(i)) for i in np.linspace(0, length - 1, min(count, length)))) frames = _read_frames(path, src[np.clip(np.array(picks) + start, 0, analysis["n"] - 1)], (w, h)) + try: + tint = np.array([int(color[i:i + 2], 16) for i in (1, 3, 5)], dtype=np.float32) + alpha = min(0.8, max(0.05, float(opacity))) + except (TypeError, ValueError, IndexError): + tint, alpha = np.array([255, 40, 60], dtype=np.float32), 0.55 out = [] for i, fr in zip(picks, frames): img = fr.copy() m = masks[min(i, masks.shape[0] - 1)].numpy().astype(bool) - img[m] = (img[m] * 0.45 + np.array([255, 40, 60]) * 0.55).astype(np.uint8) + img[m] = (img[m].astype(np.float32) * (1 - alpha) + tint * alpha).astype(np.uint8) if box: x0, y0, x1, y1 = box_px(box, w, h) cv2.rectangle(img, (x0, y0), (x1 - 1, y1 - 1), (255, 210, 90), 2) @@ -4023,7 +4029,10 @@ try: import asyncio # SAM 3 takes seconds: keep the server responsive _outside_prompt() out = await asyncio.get_running_loop().run_in_executor( - None, lambda: mask_preview(path, a, seg["start"], seg["end"] - seg["start"], spec, int(body.get("count", 6)))) + None, lambda: mask_preview(path, a, seg["start"], seg["end"] - seg["start"], spec, + int(body.get("count", 6)), + seg.get("color_guide_color") if seg.get("color_guide") else "#ff283c", + seg.get("color_guide_opacity") if seg.get("color_guide") else 0.55)) except Exception as exc: # noqa: BLE001 return web.json_response({"error": f"{type(exc).__name__}: {exc}"}, status=500) return web.json_response(out) diff --git a/web/js/shotloop/view_modal.js b/web/js/shotloop/view_modal.js index 5f1f88c..d5ccf1f 100644 --- a/web/js/shotloop/view_modal.js +++ b/web/js/shotloop/view_modal.js @@ -23,7 +23,10 @@ export function pointsModal(c) { h("div", { class: "mbox" }, [ h("div", { class: "row", style: "margin-bottom:6px" }, [h("span", { class: "sect" }, `Shot #${modal.idx + 1} · points`), ms.color_guide ? h("label", { class: "row", style: "gap:6px;align-items:center" }, [h("span", { class: "hint" }, "Colored Mark"), - h("select", { value: ms.color_guide_color || plan.global_color_guide_ref1 || "#00ff00", style: "width:auto", onChange: e => setMeta(modal.idx, "color_guide_color", e.target.value) }, + h("select", { value: ms.color_guide_color || plan.global_color_guide_ref1 || "#00ff00", style: "width:auto", onChange: e => { + setMeta(modal.idx, "color_guide_color", e.target.value); + if (modal.prev) c.previewMask(modal.idx, { points: modal.points, key: modal.key, text: modal.text }, true); + } }, [["#00ff00", "Green"], ["#9b30ff", "Purple"], ["#ff2020", "Red"], ["#168cff", "Blue"], ["#ffe600", "Yellow"], ["#ff00d4", "Magenta"], ["#00e5c0", "Cyan"]].map(([v, n]) => h("option", { value: v }, n)))]) : null, hint("click = keep (green) · right-click or shift+click = exclude (red) · click a point to remove it")]), h("div", { class: "mimg", onContextmenu: e => e.preventDefault(), onPointerdown: e => { diff --git a/web/js/shotloop/view_shots.js b/web/js/shotloop/view_shots.js index 18a5089..15b9da5 100644 --- a/web/js/shotloop/view_shots.js +++ b/web/js/shotloop/view_shots.js @@ -135,7 +135,10 @@ function editor(c, cur) { check(!!cur.color_guide, v => c.setMeta(i, "color_guide", v), "🎨 Colored Mark", "Color the selected region in the source video as a translucent visual marker. It does not create or enable an inpainting mask."), h("label", { class: "row", style: "gap:6px;align-items:center" }, [h("span", { class: "hint" }, "Region color · Ref 1"), - h("select", { value: cur.color_guide_color || plan.global_color_guide_ref1 || "#00ff00", style: "width:auto", onChange: e => c.setMeta(i, "color_guide_color", e.target.value) }, + h("select", { value: cur.color_guide_color || plan.global_color_guide_ref1 || "#00ff00", style: "width:auto", onChange: e => { + c.setMeta(i, "color_guide_color", e.target.value); + if (maskPrev.value[segKey(cur)]) c.previewMask(i); + } }, GUIDE_COLORS.map(([v, label]) => h("option", { value: v }, label)))]), ]), cur.color_guide ? h("details", { class: "note", style: "margin:4px 0;padding:6px 8px", open: !!cur.color_guide_text2 }, [