Image Probe¶
This example shows probing a scalar image at a marked data position.
Preview¶
Run And Adapt¶
Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.
| Route | Availability | Command or action |
|---|---|---|
| C | Canonical native source | just example-c features/image_probe (build and run), or rerun ./build/examples/c/features/image_probe |
| Python | Available; direct-engine adaptation | python3 -m examples.python.gallery.features.image_probe |
| Browser | Live WebGPU route | Open live example |
This example is approved as a starting point for user code and coding agents. Keep the object lifetimes and data shapes intact while adapting the data and styling.
What To Look For¶
A synthetic microscopy-like float field is uploaded as an R32 sampled image and mapped through a scalar color scale. The image visual enables pixel-query capability, and segment plus point visuals draw a crosshair and dot at the probe location. Move the probe in the live preview and compare the in-scene value readout with bright and dim image regions; this is useful for inspecting raw scalar values behind a heat map or image without changing the displayed field on desktop and browser hosts.
Source¶
#!/usr/bin/env python3
"""Scalar image probing with a live marker and query readback."""
from __future__ import annotations
import ctypes
import math
import numpy as np
import datoviz as dvz
from examples.python.gallery import common as ex
FIELD_WIDTH = 256
FIELD_HEIGHT = 192
PROBE_X = 0.68
PROBE_Y = 0.56
PROBE_REQUEST_ID = 1
PROBE_RING_SEGMENTS = 28
PROBE_SEGMENTS = PROBE_RING_SEGMENTS + 4
TAU = 2.0 * math.pi
def _clamp01(value: float) -> float:
return min(max(value, 0.0), 1.0)
def _sample_field(x: float, y: float) -> float:
value = 0.11 + 0.05 * math.sin(TAU * (2.3 * x + 0.35 * y))
value += 0.04 * math.cos(TAU * (0.55 * x - 3.6 * y))
filament = math.sin(TAU * (x * 1.15 + 0.22 * math.sin(TAU * y)))
value += 0.18 * math.exp(-18.0 * (filament - 0.18) * (filament - 0.18))
centers = (
(0.16, 0.22, 0.050),
(0.31, 0.71, 0.042),
(0.46, 0.38, 0.035),
(0.58, 0.84, 0.040),
(0.70, 0.56, 0.038),
(0.78, 0.24, 0.046),
(0.86, 0.69, 0.035),
(0.24, 0.50, 0.030),
)
for i, (cx, cy, sigma) in enumerate(centers):
dx = x - cx
dy = y - cy
d2 = (dx * dx + 1.4 * dy * dy) / (2.0 * sigma * sigma)
value += (0.20 + 0.06 * (i % 3)) * math.exp(-d2)
hot_dx = x - 0.69
value += (
0.30
* math.exp(-(hot_dx * hot_dx) / (2.0 * 0.115 * 0.115))
* (0.78 + 0.22 * math.cos(TAU * (y - 0.48)))
)
hot_dy = y - 0.57
value += 0.50 * math.exp(
-(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0 * 0.022 * 0.022)
)
mirror_dy = y - 0.43
value += 0.42 * math.exp(
-(hot_dx * hot_dx + mirror_dy * mirror_dy) / (2.0 * 0.024 * 0.024)
)
return _clamp01(value)
def _probe_field() -> np.ndarray:
x = np.linspace(0.0, 1.0, FIELD_WIDTH, dtype=np.float32)
y = np.linspace(0.0, 1.0, FIELD_HEIGHT, dtype=np.float32)
u, v = np.meshgrid(x, y)
values = 0.11 + 0.05 * np.sin(TAU * (2.3 * u + 0.35 * v))
values += 0.04 * np.cos(TAU * (0.55 * u - 3.6 * v))
filament = np.sin(TAU * (u * 1.15 + 0.22 * np.sin(TAU * v)))
values += 0.18 * np.exp(-18.0 * (filament - 0.18) * (filament - 0.18))
centers = np.array(
[
[0.16, 0.22, 0.050],
[0.31, 0.71, 0.042],
[0.46, 0.38, 0.035],
[0.58, 0.84, 0.040],
[0.70, 0.56, 0.038],
[0.78, 0.24, 0.046],
[0.86, 0.69, 0.035],
[0.24, 0.50, 0.030],
],
dtype=np.float32,
)
for i, (cx, cy, sigma) in enumerate(centers):
dx = u - cx
dy = v - cy
d2 = (dx * dx + 1.4 * dy * dy) / (2.0 * sigma * sigma)
values += (0.20 + 0.06 * (i % 3)) * np.exp(-d2)
hot_dx = u - 0.69
values += (
0.30
* np.exp(-(hot_dx * hot_dx) / (2.0 * 0.115 * 0.115))
* (0.78 + 0.22 * np.cos(TAU * (v - 0.48)))
)
hot_dy = v - 0.57
values += 0.50 * np.exp(
-(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0 * 0.022 * 0.022)
)
mirror_dy = v - 0.43
values += 0.42 * np.exp(
-(hot_dx * hot_dx + mirror_dy * mirror_dy) / (2.0 * 0.024 * 0.024)
)
return np.clip(values, 0.0, 1.0).astype(np.float32)
def _set_probe_domain(panel) -> None:
if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_X, 0.0, 1.0) != 0:
raise RuntimeError("dvz_panel_set_domain(X) failed")
if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_Y, 0.0, 1.0) != 0:
raise RuntimeError("dvz_panel_set_domain(Y) failed")
def _add_probe_image(scene, panel, values: np.ndarray):
image = dvz.dvz_image(scene, 0)
if not image:
raise RuntimeError("dvz_image() failed")
positions = np.array(
[
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
],
dtype=np.float32,
)
texcoords = np.array(
[[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]],
dtype=np.float32,
)
scale = ex.continuous_scale(scene, b"python_image_probe")
field = dvz.dvz_sampled_field_from_array(scene, values)
if dvz.dvz_visual_set_data_many(
image,
{
"position": positions,
"texcoords": texcoords,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(image) failed")
if dvz.dvz_visual_set_scale(image, b"color", scale) != 0:
raise RuntimeError("dvz_visual_set_scale() failed")
if dvz.dvz_visual_set_field(image, b"field", field) != 0:
raise RuntimeError("dvz_visual_set_field() failed")
if dvz.dvz_visual_set_depth_test(image, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(image) failed")
dvz.dvz_visual_set_query_capabilities(image, dvz.DVZ_QUERY_CAPABILITY_PIXEL)
ex.add_visual(panel, image)
return image
def _plot_size(panel) -> tuple[float, float]:
rect = dvz.DvzRect()
if (
dvz.dvz_panel_plot_rect_px(panel, ctypes.byref(rect))
and rect.width > 0.0
and rect.height > 0.0
):
return float(rect.width), float(rect.height)
return float(ex.WIDTH), float(ex.HEIGHT)
def _probe_marker_arrays(panel, x: float, y: float):
plot_width, plot_height = _plot_size(panel)
starts = np.zeros((PROBE_SEGMENTS, 3), dtype=np.float32)
ends = np.zeros((PROBE_SEGMENTS, 3), dtype=np.float32)
colors = np.zeros((PROBE_SEGMENTS, 4), dtype=np.uint8)
widths = np.zeros(PROBE_SEGMENTS, dtype=np.float32)
gap_x = 6.0 / plot_width
gap_y = 6.0 / plot_height
arm_x = 20.0 / plot_width
arm_y = 20.0 / plot_height
starts[:4] = np.array(
[
[x - arm_x, y, 0.02],
[x + gap_x, y, 0.02],
[x, y - arm_y, 0.02],
[x, y + gap_y, 0.02],
],
dtype=np.float32,
)
ends[:4] = np.array(
[
[x - gap_x, y, 0.02],
[x + arm_x, y, 0.02],
[x, y - gap_y, 0.02],
[x, y + arm_y, 0.02],
],
dtype=np.float32,
)
cyan = np.array([ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, 245], dtype=np.uint8)
colors[:4] = cyan
widths[:4] = 1.8
rx = 12.0 / plot_width
ry = 12.0 / plot_height
for i in range(PROBE_RING_SEGMENTS):
k = i + 4
a0 = TAU * i / PROBE_RING_SEGMENTS
a1 = TAU * (i + 1) / PROBE_RING_SEGMENTS
starts[k] = (x + rx * math.cos(a0), y + ry * math.sin(a0), 0.02)
ends[k] = (x + rx * math.cos(a1), y + ry * math.sin(a1), 0.02)
colors[k] = (ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, 225)
widths[k] = 1.7
return starts, ends, colors, widths
def _add_probe_marker(scene, panel):
starts, ends, colors, widths = _probe_marker_arrays(panel, PROBE_X, PROBE_Y)
segments = dvz.dvz_segment(scene, 0)
if not segments:
raise RuntimeError("dvz_segment() failed")
if dvz.dvz_visual_set_data_many(
segments,
{
"position_start": starts,
"position_end": ends,
"color": colors,
"stroke_width_px": widths,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(segments) failed")
if dvz.dvz_segment_set_caps(
segments, dvz.DVZ_SEGMENT_CAP_ROUND, dvz.DVZ_SEGMENT_CAP_ROUND
) != 0:
raise RuntimeError("dvz_segment_set_caps() failed")
if dvz.dvz_visual_set_depth_test(segments, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(segments) failed")
ex.add_visual(panel, segments)
dot = dvz.dvz_point(scene, 0)
if not dot:
raise RuntimeError("dvz_point() failed")
if dvz.dvz_visual_set_data_many(
dot,
{
"position": np.array([[PROBE_X, PROBE_Y, 0.03]], dtype=np.float32),
"color": ex.color_array(ex.CYAN),
"diameter_px": np.array([6.0], dtype=np.float32),
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(dot) failed")
ex.set_filled_point_style(dot)
if dvz.dvz_visual_set_depth_test(dot, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(dot) failed")
ex.add_visual(panel, dot)
return segments, dot
def _update_probe_marker(panel, segments, dot, x: float, y: float) -> None:
starts, ends, _colors, _widths = _probe_marker_arrays(panel, x, y)
if dvz.dvz_visual_set_data_many(
segments,
{
"position_start": starts,
"position_end": ends,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(update segments) failed")
if dvz.dvz_visual_set_data(
dot, b"position", np.array([[x, y, 0.03]], dtype=np.float32)
) != 0:
raise RuntimeError("dvz_visual_set_data(update dot) failed")
def _resolve_probe_value(panel, query) -> float | None:
if query.status != dvz.DVZ_QUERY_STATUS_HIT or not query.hit:
return None
data = ex.panel_px_to_data(panel, query.panel_position[0], query.panel_position[1])
if data is None:
return None
return _sample_field(data[0], data[1])
def main() -> None:
scene, figure, panel = ex.scene_panel()
_set_probe_domain(panel)
values = _probe_field()
_add_probe_image(scene, panel, values)
probe_segments, probe_dot = _add_probe_marker(scene, panel)
state = {
"cursor": None,
"last_hit": None,
"last_value": None,
}
def set_probe_from_data(x: float, y: float) -> None:
_update_probe_marker(panel, probe_segments, probe_dot, x, y)
src = (ctypes.c_double * 2)(float(x), float(y))
dst = (ctypes.c_double * 2)()
ok = dvz.dvz_panel_transform_point(
panel,
dvz.DVZ_PANEL_COORD_DATA,
dvz.DVZ_PANEL_COORD_PANEL_PX,
src,
dst,
)
if ok:
state["cursor"] = (float(dst[0]), float(dst[1]))
def on_pointer(event) -> None:
if event.type not in (
dvz.DVZ_POINTER_EVENT_MOVE,
dvz.DVZ_POINTER_EVENT_PRESS,
dvz.DVZ_POINTER_EVENT_CLICK,
):
return
panel_pos = ex.figure_to_panel_px(panel, event.pos[0], event.pos[1])
if panel_pos is None:
return
data = ex.panel_px_to_data(panel, panel_pos[0], panel_pos[1])
if data is None:
return
x = _clamp01(data[0])
y = _clamp01(data[1])
_update_probe_marker(panel, probe_segments, probe_dot, x, y)
state["cursor"] = panel_pos
def on_frame(_view, frame_index: int, _elapsed: float) -> None:
if frame_index == 0 and state["cursor"] is None:
set_probe_from_data(PROBE_X, PROBE_Y)
cursor = state["cursor"]
if cursor is not None:
ex.queue_panel_query(
panel,
cursor[0],
cursor[1],
PROBE_REQUEST_ID,
dvz.DVZ_SCENE_TARGET_PIXEL,
)
for query in ex.poll_queries(scene):
if query.request_id != PROBE_REQUEST_ID:
continue
value = _resolve_probe_value(panel, query)
hit = value is not None
last_value = state["last_value"]
changed = state["last_hit"] is None or state["last_hit"] != hit
if hit and (last_value is None or abs(value - last_value) >= 1e-3):
changed = True
if changed:
if hit:
print(f"probe value={value:.3f}")
state["last_value"] = value
else:
print("probe miss")
state["last_value"] = None
state["last_hit"] = hit
ex.run_with_input_callbacks(
scene,
figure,
"Image Probe",
on_pointer=on_pointer,
on_frame=on_frame,
)
if __name__ == "__main__":
main()
/*
* Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project root for details.
* SPDX-License-Identifier: MIT
*/
/* image_probe - This example shows probing a scalar image at a marked data position.
*
* Scenario: features_image_probe
* Style: features, graphite_cyan, 1280x720 window target
*
* Build: just example-c features/image_probe
* Run: ./build/examples/c/features/image_probe --live
* Smoke: ./build/examples/c/features/image_probe --png
*
* What to look for: a synthetic microscopy-like float field is uploaded as an R32 sampled image and
* mapped through a scalar color scale. The image visual enables pixel-query capability, and segment
* plus point visuals draw a crosshair and dot at the probe location. Move the probe in the live
* preview and compare the in-scene value readout with bright and dim image regions; this is useful
* for inspecting raw scalar values behind a heat map or image without changing the displayed
* field on desktop and browser hosts.
*/
/*************************************************************************************************/
/* Includes */
/*************************************************************************************************/
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "_alloc.h"
#include "_assertions.h"
#include "_compat.h"
#include "datoviz/scene.h"
#include "example_style.h"
#include "runner/scenario_runner.h"
DvzScenarioSpec dvz_example_image_probe_scenario(void);
/*************************************************************************************************/
/* Constants */
/*************************************************************************************************/
#define WIDTH EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define FIELD_WIDTH 256u
#define FIELD_HEIGHT 192u
#define PROBE_X 0.68f
#define PROBE_Y 0.56f
#define PROBE_REQUEST_ID 1u
#define PROBE_RING_SEGMENTS 28u
#define PROBE_SEGMENTS (PROBE_RING_SEGMENTS + 4u)
static const float TAU = 6.28318530718f;
/*************************************************************************************************/
/* Structs */
/*************************************************************************************************/
typedef struct ImageProbeState ImageProbeState;
struct ImageProbeState
{
DvzScene* scene;
DvzPanel* panel;
DvzVisual* probe_segments;
DvzVisual* probe_dot;
DvzOverlayCard* readout;
float* values;
bool cursor_valid;
double cursor_x;
double cursor_y;
double last_value;
bool last_hit;
bool has_last_result;
};
/*************************************************************************************************/
/* Helpers */
/*************************************************************************************************/
/**
* Clamp a float to the unit interval.
*
* @param value input value
* @return clamped value
*/
static float _clamp01(float value)
{
if (value < 0.0f)
return 0.0f;
if (value > 1.0f)
return 1.0f;
return value;
}
/**
* Return a deterministic synthetic microscopy-like scalar sample.
*
* @param x normalized X coordinate
* @param y normalized Y coordinate
* @return normalized sample value
*/
static float _sample_field(float x, float y)
{
float value = 0.11f + 0.05f * sinf(TAU * (2.3f * x + 0.35f * y));
value += 0.04f * cosf(TAU * (0.55f * x - 3.6f * y));
const float filament = sinf(TAU * (x * 1.15f + 0.22f * sinf(TAU * y)));
value += 0.18f * expf(-18.0f * (filament - 0.18f) * (filament - 0.18f));
const float centers[8][3] = {
{0.16f, 0.22f, 0.050f}, {0.31f, 0.71f, 0.042f}, {0.46f, 0.38f, 0.035f},
{0.58f, 0.84f, 0.040f}, {0.70f, 0.56f, 0.038f}, {0.78f, 0.24f, 0.046f},
{0.86f, 0.69f, 0.035f}, {0.24f, 0.50f, 0.030f},
};
for (uint32_t i = 0; i < 8u; i++)
{
const float dx = x - centers[i][0];
const float dy = y - centers[i][1];
const float sigma = centers[i][2];
const float d2 = (dx * dx + 1.4f * dy * dy) / (2.0f * sigma * sigma);
value += (0.20f + 0.06f * (float)(i % 3u)) * expf(-d2);
}
const float hot_dx = x - 0.69f;
value +=
0.30f * expf(-(hot_dx * hot_dx) / (2.0f * 0.115f * 0.115f)) *
(0.78f + 0.22f * cosf(TAU * (y - 0.48f)));
const float hot_dy = y - 0.57f;
value += 0.50f * expf(-(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0f * 0.022f * 0.022f));
const float mirror_dy = y - 0.43f;
value += 0.42f * expf(-(hot_dx * hot_dx + mirror_dy * mirror_dy) / (2.0f * 0.024f * 0.024f));
return _clamp01(value);
}
/**
* Fill the probe image scalar field.
*
* @param values output normalized scalar field
*/
static void _fill_probe_field(float values[FIELD_WIDTH * FIELD_HEIGHT])
{
ANN(values);
for (uint32_t y = 0; y < FIELD_HEIGHT; y++)
{
for (uint32_t x = 0; x < FIELD_WIDTH; x++)
{
const float u = FIELD_WIDTH > 1u ? (float)x / (float)(FIELD_WIDTH - 1u) : 0.0f;
const float v = FIELD_HEIGHT > 1u ? (float)y / (float)(FIELD_HEIGHT - 1u) : 0.0f;
values[y * FIELD_WIDTH + x] = _sample_field(u, v);
}
}
}
/**
* Set a normalized data domain on the image panel.
*
* @param panel target panel
* @return true when both domain calls succeed
*/
static bool _set_probe_domain(DvzPanel* panel)
{
ANN(panel);
int rc = dvz_panel_set_domain(panel, DVZ_DIM_X, 0.0, 1.0);
if (rc != 0)
return false;
rc = dvz_panel_set_domain(panel, DVZ_DIM_Y, 0.0, 1.0);
return rc == 0;
}
/**
* Add the scalar image visual and enable pixel-query readback.
*
* @param scene scene owning the visual
* @param panel panel receiving the visual
* @param scale color scale bound to the scalar image
* @param values scalar field values
* @return true when the image was added
*/
static bool _add_probe_image(
DvzScene* scene, DvzPanel* panel, DvzScale* scale, float values[FIELD_WIDTH * FIELD_HEIGHT])
{
ANN(scene);
ANN(panel);
ANN(scale);
ANN(values);
vec3 data_positions[4] = {
{0.0f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f},
{1.0f, 0.0f, 0.0f},
{1.0f, 1.0f, 0.0f},
};
vec2 texcoords[4] = {
{0.0f, 0.0f},
{0.0f, 1.0f},
{1.0f, 0.0f},
{1.0f, 1.0f},
};
DvzVisual* image = dvz_image(scene, 0);
if (image == NULL)
return false;
if (dvz_visual_set_data(image, "position", data_positions, 4) != 0)
return false;
if (dvz_visual_set_data(image, "texcoords", texcoords, 4) != 0)
return false;
if (dvz_visual_set_scale(image, "color", scale) != 0)
return false;
DvzSampledField* field = dvz_sampled_field(
scene, &(DvzSampledFieldDesc){DVZ_STRUCT_INIT_FIELDS(DvzSampledFieldDesc),
.dim = DVZ_FIELD_DIM_2D,
.format = DVZ_FIELD_FORMAT_R32_FLOAT,
.semantic = DVZ_FIELD_SEMANTIC_SCALAR,
.width = FIELD_WIDTH,
.height = FIELD_HEIGHT,
.depth = 1,
});
if (field == NULL)
return false;
if (dvz_sampled_field_set_data(
field, &(DvzFieldDataView){DVZ_STRUCT_INIT_FIELDS(DvzFieldDataView),
.data = values,
.bytes_per_row = FIELD_WIDTH * sizeof(float),
.rows_per_image = FIELD_HEIGHT,
}) != DVZ_OK)
{
return false;
}
if (dvz_visual_set_field(image, "field", field) != DVZ_OK)
return false;
if (dvz_visual_set_depth_test(image, false) != 0)
return false;
dvz_visual_set_query_capabilities(image, DVZ_QUERY_CAPABILITY_PIXEL);
return dvz_panel_add_visual(panel, image, NULL) == 0;
}
/**
* Fill data-space crosshair and ring segments around a probe point.
*
* @param panel target panel
* @param x normalized probe X coordinate
* @param y normalized probe Y coordinate
* @param starts output segment starts
* @param ends output segment ends
* @param colors output segment colors
* @param widths output segment widths
* @return true when marker geometry was filled
*/
static bool _fill_probe_marker(
DvzPanel* panel, float x, float y, vec3 starts[PROBE_SEGMENTS], vec3 ends[PROBE_SEGMENTS],
DvzColor colors[PROBE_SEGMENTS], float widths[PROBE_SEGMENTS])
{
ANN(panel);
ANN(starts);
ANN(ends);
ANN(colors);
ANN(widths);
DvzRect plot = {0};
if (!dvz_panel_plot_rect_px(panel, &plot) || plot.width <= 0.0f || plot.height <= 0.0f)
return false;
const DvzColor cyan = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
const float gap_x = 6.0f / plot.width;
const float gap_y = 6.0f / plot.height;
const float arm_x = 20.0f / plot.width;
const float arm_y = 20.0f / plot.height;
const vec3 cross_starts[4] = {
{x - arm_x, y, 0.02f},
{x + gap_x, y, 0.02f},
{x, y - arm_y, 0.02f},
{x, y + gap_y, 0.02f},
};
const vec3 cross_ends[4] = {
{x - gap_x, y, 0.02f},
{x + arm_x, y, 0.02f},
{x, y - gap_y, 0.02f},
{x, y + arm_y, 0.02f},
};
for (uint32_t i = 0; i < 4u; i++)
{
starts[i][0] = cross_starts[i][0];
starts[i][1] = cross_starts[i][1];
starts[i][2] = cross_starts[i][2];
ends[i][0] = cross_ends[i][0];
ends[i][1] = cross_ends[i][1];
ends[i][2] = cross_ends[i][2];
colors[i] = cyan;
colors[i].a = 245u;
widths[i] = 1.8f;
}
const float rx = 12.0f / plot.width;
const float ry = 12.0f / plot.height;
for (uint32_t i = 0; i < PROBE_RING_SEGMENTS; i++)
{
const uint32_t k = i + 4u;
const float a0 = TAU * (float)i / (float)PROBE_RING_SEGMENTS;
const float a1 = TAU * (float)(i + 1u) / (float)PROBE_RING_SEGMENTS;
starts[k][0] = x + rx * cosf(a0);
starts[k][1] = y + ry * sinf(a0);
starts[k][2] = 0.02f;
ends[k][0] = x + rx * cosf(a1);
ends[k][1] = y + ry * sinf(a1);
ends[k][2] = 0.02f;
colors[k] = cyan;
colors[k].a = 225u;
widths[k] = 1.7f;
}
return true;
}
/**
* Update the live probe marker to a normalized data position.
*
* @param state image probe example state
* @param x normalized probe X coordinate
* @param y normalized probe Y coordinate
*/
static void _update_probe_marker(ImageProbeState* state, float x, float y)
{
if (state == NULL || state->panel == NULL || state->probe_segments == NULL ||
state->probe_dot == NULL)
return;
DvzPanel* panel = state->panel;
ANN(panel);
vec3 starts[PROBE_SEGMENTS] = {{0}};
vec3 ends[PROBE_SEGMENTS] = {{0}};
DvzColor colors[PROBE_SEGMENTS] = {{0}};
float widths[PROBE_SEGMENTS] = {0};
if (!_fill_probe_marker(panel, x, y, starts, ends, colors, widths))
return;
DvzVisualDataUpdate segment_updates[] = {
{.attr_name = "position_start", .data = starts, .item_count = PROBE_SEGMENTS},
{.attr_name = "position_end", .data = ends, .item_count = PROBE_SEGMENTS},
};
if (dvz_visual_set_data_many(state->probe_segments, segment_updates, 2) != 0)
return;
vec3 dot_data[1] = {{x, y, 0.03f}};
(void)dvz_visual_set_data(state->probe_dot, "position", dot_data, 1);
}
/**
* Update the live probe marker from a panel-local cursor position.
*
* @param state image probe example state
*/
static void _update_probe_marker_from_cursor(ImageProbeState* state)
{
if (state == NULL || !state->cursor_valid)
return;
double data[2] = {0};
if (!dvz_panel_position_to_data(
state->panel, DVZ_PANEL_COORD_PANEL_PX,
(const double[2]){state->cursor_x, state->cursor_y}, data))
{
return;
}
_update_probe_marker(state, (float)data[0], (float)data[1]);
}
/**
* Add the visible live probe marker.
*
* @param scene scene owning the visual
* @param panel panel receiving the visual
* @param out_segments output segment visual
* @param out_dot output center dot visual
* @return true when the marker was added
*/
static bool _add_probe_marker(
DvzScene* scene, DvzPanel* panel, DvzVisual** out_segments, DvzVisual** out_dot)
{
ANN(scene);
ANN(panel);
ANN(out_segments);
ANN(out_dot);
vec3 starts[PROBE_SEGMENTS] = {{0}};
vec3 ends[PROBE_SEGMENTS] = {{0}};
DvzColor colors[PROBE_SEGMENTS] = {{0}};
float widths[PROBE_SEGMENTS] = {0};
if (!_fill_probe_marker(panel, PROBE_X, PROBE_Y, starts, ends, colors, widths))
return false;
DvzVisual* marker = dvz_segment(scene, 0);
if (marker == NULL)
return false;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position_start", .data = starts, .item_count = PROBE_SEGMENTS},
{.attr_name = "position_end", .data = ends, .item_count = PROBE_SEGMENTS},
{.attr_name = "color", .data = colors, .item_count = PROBE_SEGMENTS},
{.attr_name = "stroke_width_px", .data = widths, .item_count = PROBE_SEGMENTS},
};
if (dvz_visual_set_data_many(marker, updates, 4) != 0)
return false;
if (dvz_segment_set_caps(marker, DVZ_SEGMENT_CAP_ROUND, DVZ_SEGMENT_CAP_ROUND) != 0)
return false;
if (dvz_visual_set_depth_test(marker, false) != 0)
return false;
if (dvz_panel_add_visual(panel, marker, NULL) != 0)
return false;
vec3 dot_data[1] = {{PROBE_X, PROBE_Y, 0.03f}};
DvzVisual* dot = dvz_point(scene, 0);
if (dot == NULL)
return false;
DvzColor dot_color[1] = {example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY)};
dot_color[0].a = 245u;
float dot_diameter[1] = {6.0f};
DvzVisualDataUpdate dot_updates[] = {
{.attr_name = "position", .data = dot_data, .item_count = 1},
{.attr_name = "color", .data = dot_color, .item_count = 1},
{.attr_name = "diameter_px", .data = dot_diameter, .item_count = 1},
};
if (dvz_visual_set_data_many(dot, dot_updates, 3) != 0)
return false;
DvzPointStyleDesc point_style = dvz_point_style_desc();
point_style.aspect = DVZ_SHAPE_ASPECT_FILLED;
point_style.stroke_width_px = 0.0f;
if (dvz_point_set_style(dot, &point_style) != 0)
return false;
if (dvz_visual_set_depth_test(dot, false) != 0)
return false;
if (dvz_panel_add_visual(panel, dot, NULL) != 0)
return false;
*out_segments = marker;
*out_dot = dot;
return true;
}
/**
* Create the shared scalar color scale.
*
* @param scene scene owning scale resources
* @return created scale, or NULL on failure
*/
static DvzScale* _add_probe_scale(DvzScene* scene)
{
ANN(scene);
DvzScale* scale = dvz_scale(
scene, &(DvzScaleDesc){DVZ_STRUCT_INIT_FIELDS(DvzScaleDesc),
.kind = DVZ_SCALE_CONTINUOUS,
.label = "intensity",
});
if (scale == NULL)
return NULL;
dvz_scale_set_format(
scale, &(DvzFormatDesc){DVZ_STRUCT_INIT_FIELDS(DvzFormatDesc),
.precision = 2,
.trim_trailing_zeros = true});
dvz_scale_set_domain(scale, 0.0, 1.0);
dvz_scale_set_view_range(scale, 0.0, 1.0);
DvzColormap* colormap = example_graphite_cyan_colormap(scene);
if (colormap == NULL)
return NULL;
dvz_scale_set_colormap(scale, colormap);
return scale;
}
/**
* Resolve one scalar probe value from a query result.
*
* @param state image probe example state
* @param query query result
* @param out_value output scalar value
* @return true when a scalar value was resolved
*/
static bool
_query_probe_value(ImageProbeState* state, const DvzQueryResult* query, double* out_value)
{
if (state == NULL || query == NULL || out_value == NULL)
return false;
if (query->status != DVZ_QUERY_STATUS_HIT || !query->hit)
return false;
double data[2] = {0};
if (!dvz_panel_position_to_data(
state->panel, DVZ_PANEL_COORD_PANEL_PX, query->panel_position, data))
{
return false;
}
*out_value = (double)_sample_field((float)data[0], (float)data[1]);
return true;
}
/**
* Return whether a query result differs enough from the last printed value.
*
* @param state image query example state
* @param query query result to compare
* @return true when the result should be printed
*/
static bool _query_changed(ImageProbeState* state, const DvzQueryResult* query)
{
if (state == NULL || query == NULL)
return false;
if (!state->has_last_result || state->last_hit != query->hit)
return true;
if (!query->hit)
return false;
double value = 0.0;
if (!_query_probe_value(state, query, &value))
return true;
double delta = value - state->last_value;
if (delta < 0.0)
delta = -delta;
return delta >= 1e-3;
}
/**
* Remember the last printed query result.
*
* @param state image query example state
* @param query query result to store
*/
static void _store_query_result(ImageProbeState* state, const DvzQueryResult* query)
{
if (state == NULL || query == NULL)
return;
state->has_last_result = true;
state->last_hit = query->hit;
double value = 0.0;
if (_query_probe_value(state, query, &value))
state->last_value = value;
}
/**
* Queue one pixel query at the latest probe position.
*
* @param state image query example state
*/
static void _queue_probe(ImageProbeState* state)
{
if (state == NULL || !state->cursor_valid)
return;
DvzQueryRequest request = dvz_query_request();
request.request_id = PROBE_REQUEST_ID;
request.target = DVZ_SCENE_TARGET_PIXEL;
const int rc = dvz_panel_query_px(state->panel, state->cursor_x, state->cursor_y, &request);
if (rc != 0)
dvz_fprintf(stderr, "dvz_panel_query_px() failed\n");
}
/**
* Add the cross-platform scalar probe readout.
*
* @param panel panel owning the overlay card
* @return created card, or NULL on failure
*/
static DvzOverlayCard* _add_probe_readout(DvzPanel* panel)
{
ANN(panel);
DvzOverlay* overlay = dvz_overlay(panel, 0);
if (overlay == NULL)
return NULL;
DvzOverlayCardDesc desc = dvz_overlay_card_desc();
desc.text = "Value: move the pointer over the image";
desc.placement = DVZ_OVERLAY_CARD_PLACEMENT_TOP_RIGHT;
desc.offset_px[0] = 24.0f;
desc.offset_px[1] = 24.0f;
DvzOverlayCard* card = dvz_overlay_card(overlay, &desc);
if (card == NULL)
return NULL;
DvzOverlayCardStyle style = dvz_overlay_card_style();
DvzColor background = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_PANEL_BG);
background.a = 238u;
style.background_color = background;
style.text_color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_TEXT);
style.padding_px[0] = 16.0f;
style.padding_px[1] = 10.0f;
style.min_width_px = 350.0f;
style.height_px = 46.0f;
style.glyph_advance_px = 8.8f;
style.text_size_px = 18.0f;
style.text_renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS;
style.max_text_chars = 96u;
return dvz_overlay_card_set_style(card, &style) == DVZ_OK ? card : NULL;
}
/*************************************************************************************************/
/* Callbacks */
/*************************************************************************************************/
/**
* Record the latest cursor position and move the live probe marker.
*
* @param event portable pointer event
* @param user_data image probe example state
*/
static void _image_probe_pointer(const DvzScenarioPointerEvent* event, void* user_data)
{
ImageProbeState* state = (ImageProbeState*)user_data;
if (state == NULL || event == NULL)
return;
if (event->type != DVZ_SCENARIO_POINTER_MOVE && event->type != DVZ_SCENARIO_POINTER_CLICK)
return;
double data[2] = {0};
double panel_px[2] = {0};
state->cursor_valid =
dvz_panel_transform_point(
state->panel, DVZ_PANEL_COORD_FIGURE_PX, DVZ_PANEL_COORD_PANEL_PX,
(const double[2]){event->x, event->y}, panel_px) &&
dvz_panel_position_to_data(state->panel, DVZ_PANEL_COORD_PANEL_PX, panel_px, data);
if (state->cursor_valid)
{
state->cursor_x = panel_px[0];
state->cursor_y = panel_px[1];
}
_update_probe_marker_from_cursor(state);
}
/**
* Poll image query results and queue the next probe.
*
* @param ctx scenario context
* @param user_data image probe example state
*/
static void _image_probe_post_frame(DvzScenarioContext* ctx, void* user_data)
{
(void)ctx;
ImageProbeState* state = (ImageProbeState*)user_data;
if (state == NULL)
return;
DvzQueryResult query = {0};
while (dvz_scene_poll_query(state->scene, &query))
{
if (!_query_changed(state, &query))
continue;
double value = 0.0;
if (_query_probe_value(state, &query, &value))
{
char readout[128] = {0};
dvz_snprintf(
readout, sizeof(readout), "Value: %.3f Position: %.1f, %.1f px", value,
query.panel_position[0], query.panel_position[1]);
(void)dvz_overlay_card_set_text(state->readout, readout);
dvz_fprintf(
stdout, "probe value=%0.3f panel=(%0.1f,%0.1f)\n", value,
query.panel_position[0], query.panel_position[1]);
}
else
{
char readout[96] = {0};
dvz_snprintf(
readout, sizeof(readout), "Value: outside image Position: %.1f, %.1f px",
query.panel_position[0], query.panel_position[1]);
(void)dvz_overlay_card_set_text(state->readout, readout);
dvz_fprintf(
stdout, "probe miss panel=(%0.1f,%0.1f)\n", query.panel_position[0],
query.panel_position[1]);
}
_store_query_result(state, &query);
}
_queue_probe(state);
}
/**
* Handle portable scenario events.
*
* @param ctx scenario context
* @param event portable event
* @param user scenario state
*/
static void _scenario_event(DvzScenarioContext* ctx, const DvzScenarioEvent* event, void* user)
{
(void)ctx;
if (event == NULL)
return;
if (event->kind == DVZ_SCENARIO_EVENT_POINTER)
_image_probe_pointer(&event->content.pointer, user);
}
/*************************************************************************************************/
/* Scenario callbacks */
/*************************************************************************************************/
/**
* Initialize the scalar image probe feature scenario.
*
* @param ctx scenario context
* @param out_user scenario state output
* @return true on success
*/
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
if (ctx == NULL)
return false;
if (out_user != NULL)
*out_user = NULL;
ImageProbeState* state = (ImageProbeState*)dvz_calloc(1, sizeof(*state));
if (state == NULL)
return false;
if (out_user != NULL)
*out_user = state;
state->values = (float*)dvz_calloc(FIELD_WIDTH * FIELD_HEIGHT, sizeof(*state->values));
if (state->values == NULL)
return false;
_fill_probe_field(state->values);
ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
if (ctx->figure == NULL)
return false;
DvzPanel* panel = dvz_panel_full(ctx->figure);
if (panel == NULL)
return false;
bool ok = _set_probe_domain(panel);
if (!ok)
return false;
example_graphite_cyan_set_panel_background(panel);
DvzScale* scale = _add_probe_scale(ctx->scene);
if (scale == NULL)
return false;
ok = _add_probe_image(ctx->scene, panel, scale, state->values);
if (!ok)
return false;
DvzVisual* probe_segments = NULL;
DvzVisual* probe_dot = NULL;
ok = _add_probe_marker(ctx->scene, panel, &probe_segments, &probe_dot);
if (!ok)
return false;
double initial_probe_px[2] = {0};
if (!dvz_panel_data_to_position(
panel, DVZ_PANEL_COORD_PANEL_PX, (const double[2]){PROBE_X, PROBE_Y},
initial_probe_px))
{
return false;
}
state->scene = ctx->scene;
state->panel = panel;
state->probe_segments = probe_segments;
state->probe_dot = probe_dot;
state->readout = _add_probe_readout(panel);
if (state->readout == NULL)
return false;
state->cursor_valid = true;
state->cursor_x = initial_probe_px[0];
state->cursor_y = initial_probe_px[1];
return true;
}
/**
* Destroy the scalar image probe feature scenario state.
*
* @param ctx scenario context
* @param user scenario state
*/
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
(void)ctx;
ImageProbeState* state = (ImageProbeState*)user;
if (state == NULL)
return;
dvz_free(state->values);
dvz_free(state);
}
/**
* Return the scalar image probe scenario specification.
*
* @return scenario specification
*/
DvzScenarioSpec dvz_example_image_probe_scenario(void)
{
return (DvzScenarioSpec){
.id = "features_image_probe",
.title = "Image Probe",
.width = WIDTH,
.height = HEIGHT,
.fps = 60.0,
.requirements = DVZ_SCENARIO_REQ_IMAGE_VISUAL | DVZ_SCENARIO_REQ_TEXT_VISUAL |
DVZ_SCENARIO_REQ_QUERY_READBACK | DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
.init = _scenario_init,
.event = _scenario_event,
.post_frame = _image_probe_post_frame,
.destroy = _scenario_destroy,
};
}
/*************************************************************************************************/
/* Functions */
/*************************************************************************************************/
/**
* Run the scalar image probe feature example through the native scenario runner.
*
* @param argc command-line argument count
* @param argv command-line argument vector
* @return process exit code
*/
#ifndef DVZ_EXAMPLE_NO_MAIN
int main(int argc, char** argv)
{
DvzScenarioSpec spec = dvz_example_image_probe_scenario();
return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details
- ID:
features_image_probe - Category:
feature - Lane:
features - Status:
supported - Source:
examples/c/features/image_probe.c - Approved adaptation starter:
yes - Python source:
examples/python/gallery/features/image_probe.py - Python adaptation: Available; direct-engine adaptation
- Browser support: Live in browser
- WebGPU live route:
examples/webgpu/live.html?id=features_image_probe - Browser capability tags:
image,query-readback,frame-callbacks,overlay,text - Validation:
smoke+readback+screenshot
Data
| Field | Value |
|---|---|
kind |
synthetic |
