Mark every below-threshold finger sample with an idle dot
The selector now returns candidate locations with an activity flag, so the quiet half of the finger's sampling grid can be drawn at last: one radius-1 dot per candidate, in the idle tint and off the flow ramp, on no sample that also carries an arrow. The dots are `DRAW_IDLE_GRID_DOTS` (on) and `draw_arrow_display(idle_dots=...)`; box and mini keep their own SDK path.
This commit is contained in:
@@ -35,9 +35,13 @@ NO_DEPTH_BGR = (0x8A, 0x8A, 0x8A)
|
||||
#: value and passes it as ``magnitude_range`` to :func:`draw_arrow_display`.
|
||||
FLOW_MAGNITUDE_RANGE: tuple[float, float] = (0.0, 2.0)
|
||||
|
||||
#: Whether an inactive sample is drawn as a quiet dot. Off: the live 2D canvas draws
|
||||
#: arrows and nothing else, so splitting activity out of the selector changed no pixel.
|
||||
DRAW_IDLE_GRID_DOTS = False
|
||||
#: Whether an inactive sample is drawn as a quiet dot. **On**, which is the decided
|
||||
#: display behaviour: a sample below the display threshold is a statement about the
|
||||
#: sensing region ("sampled here, nothing to show"), and it is the one statement the
|
||||
#: finger canvas could not make before — the selector had dropped the location, so an
|
||||
#: idle grid could not be drawn at all. Off hides the dots again and changes nothing
|
||||
#: else; :func:`draw_arrow_display` takes the same choice per call as ``idle_dots``.
|
||||
DRAW_IDLE_GRID_DOTS = True
|
||||
|
||||
#: Machine-readable code for "this frame could not be drawn": the caller gets the base
|
||||
#: image back and must surface the code instead of presenting it as an idle frame. The
|
||||
@@ -284,6 +288,33 @@ def _policy_from_style(
|
||||
).normalised()
|
||||
|
||||
|
||||
#: The pixels ``cv2.circle(..., radius=1, thickness=-1)`` fills, as ``(dy, dx)`` offsets
|
||||
#: from the centre it is given. One marker shape for every sensor type: the SDK's
|
||||
#: ``orisys.util.draw_arrows`` paints a below-threshold sample with exactly that call, so
|
||||
#: a dot on the finger canvas has to be the same plus-shaped 5-pixel blob a box dot is.
|
||||
#: Painting the offsets in one vectorised write, instead of one ``cv2.circle`` per dot,
|
||||
#: is what keeps a full finger grid cheap: on a 276-candidate island the pass costs 47 us
|
||||
#: against 223 us for the call loop, and the whole draw gains only ~35 us.
|
||||
_IDLE_DOT_OFFSETS = ((-1, 0), (0, -1), (0, 0), (0, 1), (1, 0))
|
||||
|
||||
|
||||
def _paint_idle_dots(
|
||||
image: np.ndarray, rows: np.ndarray, cols: np.ndarray, color: tuple[int, int, int]
|
||||
) -> None:
|
||||
"""Paint one radius-1 marker at every sample, clipped at the image edge.
|
||||
|
||||
The footprint is :data:`_IDLE_DOT_OFFSETS`, which is what ``cv2.circle`` fills for
|
||||
``radius=1, thickness=-1``; a dot that would fall outside the image contributes only
|
||||
the pixels that are inside it, exactly as the clipped ``cv2.circle`` call does.
|
||||
"""
|
||||
h, w = image.shape[:2]
|
||||
for dy, dx in _IDLE_DOT_OFFSETS:
|
||||
rr = rows + dy
|
||||
cc = cols + dx
|
||||
keep = (rr >= 0) & (rr < h) & (cc >= 0) & (cc < w)
|
||||
image[rr[keep], cc[keep]] = color
|
||||
|
||||
|
||||
def _paint_shared_flow_arrows(
|
||||
canvas: np.ndarray,
|
||||
flow: np.ndarray,
|
||||
@@ -294,23 +325,38 @@ def _paint_shared_flow_arrows(
|
||||
below_threshold_color: tuple[int, int, int],
|
||||
magnitude_range: tuple[float, float] | None = None,
|
||||
per_frame_colour: bool = False,
|
||||
idle_dots: bool = True,
|
||||
) -> np.ndarray:
|
||||
"""Draw arrows from :func:`select_flow_samples` so 2D matches 3D sample points.
|
||||
|
||||
The selector returns *candidate locations* and an ``active`` flag per sample; the
|
||||
drawing choice is this function's own. Arrows are drawn for active samples, so what
|
||||
reaches the canvas is what reached it before the contract split.
|
||||
drawing choice is this function's own, and it is **one marker per sample**: an
|
||||
active sample is drawn as an arrow and nothing else, an inactive one as a quiet dot
|
||||
and nothing else, so no sample carries both.
|
||||
|
||||
An inactive sample is drawn as a quiet dot in ``below_threshold_color`` only when
|
||||
:data:`DRAW_IDLE_GRID_DOTS` asks for it, and this canvas does not: a live finger frame
|
||||
at 60 Hz would otherwise gain a grid of dots under the arrows. Flipping that constant
|
||||
is what the split made possible — the dots used to depend on a below-threshold branch
|
||||
that the selector's own filtering had already made unreachable.
|
||||
The dot rules, all of them consequences of the selector's own contract:
|
||||
|
||||
* **density** — one dot per candidate location, i.e. the policy's sampling grid
|
||||
(``flow_grid_spacing``, 10 px on the finger's shared policy) rather than one per UV
|
||||
pixel. A realistic finger island holds 28345 pixels and 276 candidates, so the grid
|
||||
is what makes an idle frame readable instead of noise;
|
||||
* **region** — the selector has already clipped the candidates to ``valid_mask`` at
|
||||
the sample *and* at the arrow tip and to ``flow_edge_margin``, so a dot cannot land
|
||||
where the device vouches for nothing or where the tip would fall outside;
|
||||
* **colour** — ``below_threshold_color`` (:data:`FLOW_IDLE_HEX`), deliberately off the
|
||||
flow ramp: colour is a measurement, and a dot states the absence of one;
|
||||
* **surface** — a 1 px dot per 10 px cell leaves the island's own fill visible, so the
|
||||
canvas is not darkened or recoloured to make the dots readable.
|
||||
|
||||
``idle_dots`` is the switch, defaulting to :data:`DRAW_IDLE_GRID_DOTS`. Arrows are
|
||||
painted after the dots, so a dot from a neighbouring grid point can never take a pixel
|
||||
off an arrow's tip.
|
||||
|
||||
``magnitude_range`` is the **fixed** colour range in uv pixels of displacement
|
||||
(:data:`FLOW_MAGNITUDE_RANGE`); ``per_frame_colour`` opts in to the old
|
||||
re-normalisation of every frame against its own maximum, which makes one magnitude
|
||||
a different colour from one frame to the next.
|
||||
a different colour from one frame to the next. Neither touches the dots: they are off
|
||||
the ramp on purpose.
|
||||
"""
|
||||
from orisys_app_shared.scene3d.visual_policy import select_flow_samples
|
||||
|
||||
@@ -319,14 +365,14 @@ def _paint_shared_flow_arrows(
|
||||
if samples.rows.size == 0:
|
||||
return image
|
||||
active = np.asarray(samples.active, dtype=bool)
|
||||
if DRAW_IDLE_GRID_DOTS:
|
||||
for i in np.flatnonzero(~active):
|
||||
cv2.circle(
|
||||
if idle_dots:
|
||||
inactive = np.flatnonzero(~active)
|
||||
if inactive.size:
|
||||
_paint_idle_dots(
|
||||
image,
|
||||
(int(samples.cols[i]), int(samples.rows[i])),
|
||||
radius=1,
|
||||
color=below_threshold_color,
|
||||
thickness=-1,
|
||||
samples.rows[inactive],
|
||||
samples.cols[inactive],
|
||||
tuple(int(v) for v in below_threshold_color),
|
||||
)
|
||||
if not np.any(active):
|
||||
return image
|
||||
@@ -379,6 +425,7 @@ def draw_arrow_display(
|
||||
policy=None,
|
||||
magnitude_range: tuple[float, float] | None = None,
|
||||
per_frame_colour: bool = False,
|
||||
idle_dots: bool | None = None,
|
||||
) -> np.ndarray:
|
||||
"""Draw flow arrows on a solid canvas or over the processed frame.
|
||||
|
||||
@@ -404,6 +451,12 @@ def draw_arrow_display(
|
||||
back in to normalising every frame against its own maximum, which makes one
|
||||
magnitude a different colour from one frame to the next.
|
||||
|
||||
``idle_dots`` shows or hides the below-threshold markers of the **shared finger
|
||||
canvas**; ``None`` means :data:`DRAW_IDLE_GRID_DOTS`, which is on. Box/Mini draw
|
||||
through the SDK's own ``orisys.util.draw_arrows``, which paints its own below-threshold
|
||||
dots (same tint, same radius-1 marker) and has no such switch: this one governs the
|
||||
canvas that draws here, and Box keeps the appearance it has.
|
||||
|
||||
Returns the drawn image, which carries ``frame_diagnostic``: ``None`` when the frame
|
||||
was drawn, and otherwise a :class:`~orisys_app_shared.scene3d.contracts.Diagnostic`
|
||||
whose ``code`` is :data:`ARROW_RENDER_FAILED`. **The failure must be surfaced**: the
|
||||
@@ -472,6 +525,10 @@ def draw_arrow_display(
|
||||
below_threshold_color=tuple(draw_kw["below_threshold_color"]),
|
||||
magnitude_range=magnitude_range,
|
||||
per_frame_colour=per_frame_colour,
|
||||
# Read here rather than as a parameter default: the module constant is
|
||||
# the display decision, and a caller that flips it (a build, a test)
|
||||
# must be obeyed without every call site having to pass it.
|
||||
idle_dots=DRAW_IDLE_GRID_DOTS if idle_dots is None else bool(idle_dots),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user