File size: 6,444 Bytes
1e3ce4c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | """PixelCursor: the spatio-temporal-semantic-identity selector that owns pixel mutation.
Every studio operation that touches pixels routes through a PixelCursor. Artifacts
are immutable from outside this package; the only way to produce a new Artifact is
via cursor.write_* methods.
Op-set (closed):
seek_* β reposition (returns new cursor)
range / at_frame / all_frames / propagate_temporal β temporal traversal
lock_identity β pin subject or motion identity
bind_motion_exemplar β few-shot motion attach (returns new cursor)
read_* β non-mutating queries (returns data)
write_* β mutation (returns new Artifact)
tell β introspect cursor state
"""
from __future__ import annotations
from dataclasses import dataclass, replace, field
from typing import Optional, Sequence, Tuple
import numpy as np
from .artifact import Artifact
from .selectors import Selector, Point, Region, Mask, Token, Layer, Anchor3D
from .scope import (
TemporalScope,
FrameIndex,
FrameRange,
ALL_FRAMES,
DepthScope,
IdentityLock,
OpacityProfile,
)
@dataclass(frozen=True, slots=True)
class PixelCursor:
artifact: Artifact
selector: Selector
temporal: TemporalScope
depth: Optional[DepthScope] = None
identity_lock: Optional[IdentityLock] = None
opacity: OpacityProfile = field(default_factory=lambda: OpacityProfile(kind="hard"))
# ββ seek_* : reposition without mutating the artifact ββ
def seek_point(self, x: float, y: float, z: Optional[float] = None) -> "PixelCursor":
return replace(self, selector=Point(x=x, y=y, z=z))
def seek_region(self, x: int, y: int, w: int, h: int) -> "PixelCursor":
return replace(self, selector=Region(x=x, y=y, w=w, h=h))
def seek_mask(self, bitmap: np.ndarray, is_soft: bool = False) -> "PixelCursor":
return replace(self, selector=Mask(bitmap=bitmap, is_soft=is_soft))
def seek_token(self, text: str, embedding_id: Optional[str] = None) -> "PixelCursor":
return replace(self, selector=Token(text=text, embedding_id=embedding_id))
def seek_layer(self, layer_id: str) -> "PixelCursor":
return replace(self, selector=Layer(layer_id=layer_id))
def seek_3d_anchor(
self, anchor_id: str, bbox: Tuple[float, float, float, float, float, float]
) -> "PixelCursor":
return replace(self, selector=Anchor3D(anchor_id=anchor_id, bbox=bbox))
# ββ temporal ββ
def at_frame(self, index: int) -> "PixelCursor":
return replace(self, temporal=FrameIndex(index=index))
def range(self, start: int, end: int) -> "PixelCursor":
return replace(self, temporal=FrameRange(start=start, end=end))
def all_frames(self) -> "PixelCursor":
return replace(self, temporal=ALL_FRAMES)
def propagate_temporal(self, end_frame: int, mode: str = "forward") -> "PixelCursor":
"""Extend cursor temporal extent. Backend (e.g. CoTracker3, SAM2) does the actual traversal."""
cur = self.temporal
if isinstance(cur, FrameIndex):
if mode == "backward":
new: TemporalScope = FrameRange(start=end_frame, end=cur.index + 1)
else:
new = FrameRange(start=cur.index, end=end_frame)
elif isinstance(cur, FrameRange):
new = FrameRange(start=cur.start, end=max(cur.end, end_frame))
else:
new = cur
return replace(self, temporal=new)
# ββ identity ββ
def lock_identity(self, lock: IdentityLock) -> "PixelCursor":
return replace(self, identity_lock=lock)
def bind_motion_exemplar(
self, exemplar_paths: Sequence[str], backend: str = "auto"
) -> "PixelCursor":
from .ops import _dispatch
return _dispatch("bind_motion_exemplar", backend, self, exemplar_paths)
# ββ read ops (non-mutating) ββ
def read_state(self) -> dict:
return self.tell()
def read_pixels(self) -> np.ndarray:
from .artifact import Image, FrameStack
a = self.artifact
if isinstance(a, Image):
return a.pixels
if isinstance(a, FrameStack):
t = self.temporal
if isinstance(t, FrameIndex):
return a.frames[t.index]
if isinstance(t, FrameRange):
return a.frames[t.start : t.end]
return a.frames
raise TypeError(f"read_pixels: unsupported artifact type {type(a).__name__}")
# ββ write ops (return new Artifact) ββ
def write_motion(self, motion_spec, backend: str = "auto") -> Artifact:
from .ops import _dispatch
return _dispatch("write_motion", backend, self, motion_spec)
def write_displacement(self, target_xy: Tuple[float, float], backend: str = "auto") -> Artifact:
from .ops import _dispatch
return _dispatch("write_displacement", backend, self, target_xy)
def write_inpaint(self, prompt: str, backend: str = "auto") -> Artifact:
from .ops import _dispatch
return _dispatch("write_inpaint", backend, self, prompt)
def write_keyframe(self, frame_index: int, pose_state, backend: str = "auto") -> Artifact:
from .ops import _dispatch
return _dispatch("write_keyframe", backend, self, frame_index, pose_state)
def write_condition(self, control_signal, backend: str = "auto") -> Artifact:
from .ops import _dispatch
return _dispatch("write_condition", backend, self, control_signal)
# ββ introspection ββ
def tell(self) -> dict:
return {
"selector": type(self.selector).__name__,
"selector_value": self.selector,
"temporal": self.temporal,
"depth": self.depth,
"identity_locked": self.identity_lock is not None,
"identity_source": (self.identity_lock.source if self.identity_lock else None),
"opacity_kind": self.opacity.kind,
"artifact": type(self.artifact).__name__,
}
def open_cursor(
artifact: Artifact,
selector: Optional[Selector] = None,
temporal: Optional[TemporalScope] = None,
) -> PixelCursor:
if selector is None:
selector = Token(text="*all*")
if temporal is None:
temporal = ALL_FRAMES
return PixelCursor(artifact=artifact, selector=selector, temporal=temporal)
|