Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/dls_tiled/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ def _finish(total, weight, coverage):


def render_cells(values, x, y, x_edges, y_edges, viewport: Viewport):
"""The scan's grid cells (mean of the points in each), sampled at each pixel."""
"""The scan's grid cells (mean of the points in each), sampled at each pixel.
Points without a finite value are left out."""
values = numpy.asarray(values, dtype=float)
finite = numpy.isfinite(values)
values, x, y = values[finite], numpy.asarray(x)[finite], numpy.asarray(y)[finite]
counts, _, _ = numpy.histogram2d(x, y, bins=[x_edges, y_edges])
sums, _, _ = numpy.histogram2d(x, y, bins=[x_edges, y_edges], weights=values)
xs, ys = viewport.centres()
Expand Down Expand Up @@ -251,7 +254,14 @@ def to_rows(image) -> list[list[float | None]]:
return out.tolist()


CHANNEL_COLOUR = {"RedTotal": 0, "GreenTotal": 1, "BlueTotal": 2}
CHANNEL_COLOUR = {
"RedTotal": 0,
"GreenTotal": 1,
"BlueTotal": 2,
"DiodesRed": 0,
"DiodesGreen": 1,
"DiodesBlue": 2,
}


def to_png(image, coverage, colour=0, vmin=None, vmax=None, outline=None) -> bytes:
Expand Down
49 changes: 39 additions & 10 deletions src/dls_tiled/visr.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,7 +962,8 @@ def current_lengths() -> dict[str, int]:
),
)
readbacks = readbacks[:, positioned]
data = {channel: d[positioned] for channel, d in data.items()}
# an optional channel can be shorter than the positions (behind them)
data = {channel: d[positioned[: len(d)]] for channel, d in data.items()}

assert isinstance(red_total, (H5Dataset, numpy.ndarray))
logger.debug(
Expand Down Expand Up @@ -1138,7 +1139,10 @@ async def render( # type: ignore
extent) at ``size`` pixels along its longer side.

``format=json`` returns every channel as rows (row 0 at ``y0``, ``null``
where nothing is drawn) plus a ``coverage`` layer (0 to 1). ``format=png``
where nothing is drawn) plus the camera's ``coverage`` layer (0 to 1). The
diode channels (``DiodesRed``/``DiodesGreen``/``DiodesBlue``) are ``null``
for a run without diodes, and ``null`` where they are behind the camera.
``format=png``
returns one ``channel`` as a tinted RGBA image, transparent where nothing is
drawn, scaled to ``vmin``/``vmax`` if given (else to its own range). For
``ovals``, JSON adds an ``outline`` layer (opacity of the white lines) and
Expand Down Expand Up @@ -1171,10 +1175,31 @@ async def render( # type: ignore

async def compute():
points = await load_points(
root, uid, 0, 1, setpoints, slice_dim, await get_scan_grid(root, uid)
root,
uid,
0,
1,
setpoints,
slice_dim,
await get_scan_grid(root, uid),
optional_channels=DIODE_CHANNELS,
)
# the grid the points were placed on, which may have x and y swapped
x, y, offset, grid = points.x, points.y, points.offset, points.grid
if format == "png" and channel not in points.data:
raise HTTPException(
status_code=404, detail=f"run {uid} has no {channel} channel"
)

def values_of(name):
"""A channel's values, one per point: a diode channel behind the
camera is padded with NaN, which draws nothing."""
values = numpy.asarray(points.data[name], dtype=float)
if len(values) < len(x):
values = numpy.concatenate(
[values, numpy.full(len(x) - len(values), numpy.nan)]
)
return values

step = _nominal_step(grid) if grid is not None else None
if grid is not None:
Expand Down Expand Up @@ -1264,7 +1289,7 @@ def produce() -> tuple[bytes, str]:
"""Draw and encode: the CPU-bound part, run off the event loop."""
outline = outline_of()
if format == "png":
image, coverage = draw(points.data[channel])
image, coverage = draw(values_of(channel))
png = rendering.to_png(
image,
coverage,
Expand All @@ -1275,11 +1300,15 @@ def produce() -> tuple[bytes, str]:
)
return png, "image/png"
else:
images = {
name: draw(points.data[name])
for name in ("RedTotal", "GreenTotal", "BlueTotal")
# each channel's (image, coverage), None for a diode it lacks
images: dict[str, Any] = {
name: draw(values_of(name)) for name in CAMERA_CHANNELS
}
coverage = next(iter(images.values()))[1]
coverage = images["RedTotal"][1]
for name in DIODE_CHANNELS:
images[name] = (
draw(values_of(name)) if name in points.data else None
)
output = {
"kind": kind,
"viewport": {
Expand All @@ -1291,8 +1320,8 @@ def produce() -> tuple[bytes, str]:
"height": viewport.height,
},
"channels": {
name: rendering.to_rows(img)
for name, (img, _) in images.items()
name: None if drawn is None else rendering.to_rows(drawn[0])
for name, drawn in images.items()
},
"coverage": numpy.round(coverage, 3).tolist(),
**(
Expand Down
83 changes: 83 additions & 0 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,89 @@ def test_render_ovals_returns_an_outline_layer(client):
assert png.headers["content-type"] == "image/png"


# the RGB diodes


def write_diodes(client, uid, values):
primary = client[uid]["primary"]
for colour in ("red", "green", "blue"):
primary.write_array(
numpy.asarray(values, dtype=float), key=f"spectroscopy_diodes-{colour}"
)


def test_render_has_null_diodes_for_a_run_without_them(client):
write_scan(client, "camera-only")

channels = get(client, "camera-only", size=3).json()["channels"]

assert channels["RedTotal"][0] == [0.0, 1.0, 2.0]
assert channels["DiodesRed"] is None
assert channels["DiodesGreen"] is None
assert channels["DiodesBlue"] is None


@pytest.mark.parametrize(
"params",
[
{"kind": "cells"},
{"kind": "rectangles"},
{"kind": "gaussian"},
{"kind": "ovals", "beam_x": 1.3, "beam_y": 1.3},
],
)
def test_render_draws_the_diodes_like_the_camera(client, params):
write_scan(client, "diodes")
write_diodes(client, "diodes", numpy.arange(9.0))

channels = get(client, "diodes", size=30, **params).json()["channels"]

assert channels["DiodesRed"] == channels["RedTotal"]
assert channels["DiodesGreen"] == channels["GreenTotal"]
assert channels["DiodesBlue"] == channels["BlueTotal"]


@pytest.mark.parametrize(
"params",
[{"kind": "cells"}, {"kind": "ovals", "beam_x": 0.9, "beam_y": 0.9}],
)
def test_render_draws_nothing_where_the_diodes_are_behind_the_camera(client, params):
write_scan(client, "lagging")
write_diodes(client, "lagging", numpy.arange(7.0))

channels = get(client, "lagging", size=3, **params).json()["channels"]

assert channels["RedTotal"][2] == [6.0, 7.0, 8.0]
assert channels["DiodesRed"][2] == [6.0, None, None]
assert channels["DiodesRed"][:2] == channels["RedTotal"][:2]


def test_render_png_of_a_diode_channel(client):
write_scan(client, "diode-png")
write_diodes(client, "diode-png", numpy.arange(9.0))
write_scan(client, "no-diode-png")

found = get(client, "diode-png", format="png", channel="DiodesBlue", size=8)
missing = get(client, "no-diode-png", format="png", channel="DiodesBlue")

assert found.headers["content-type"] == "image/png"
assert missing.status_code == 404


def test_render_cells_leave_out_points_without_a_value():
image, coverage = rendering.render_cells(
[1.0, numpy.nan, 3.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 0.0],
numpy.array([-0.5, 0.5, 1.5]),
numpy.array([-0.5, 0.5]),
Viewport.fit(-0.5, 1.5, -0.5, 0.5, 2),
)

assert image.tolist() == [[1.0, 3.0]] # not NaN where a NaN shares the cell
assert coverage.tolist() == [[1.0, 1.0]]


@pytest.mark.parametrize(
"params",
[
Expand Down
20 changes: 20 additions & 0 deletions tests/test_visr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1275,3 +1275,23 @@ def test_binned_diodes_follow_the_scans_grid(client):

assert numpy.shape(body["DiodesRed"]) == numpy.shape(body["RedTotal"])
assert body["DiodesRed"] == body["RedTotal"]


def test_binned_leaves_out_points_without_a_position_when_diodes_are_behind(client):
# positions for 4 points, the third without one; diodes for only 3
write_points(client, "gap", [0.0, 1.0, numpy.nan, 1.0], [0.0, 0.0, 1.0, 1.0])
primary = client["gap"]["primary"]
for colour, values in DIODES.items():
primary.write_array(
numpy.array(values[:3]), key=f"spectroscopy_diodes-{colour}"
)

response = client.context.http_client.get(
"/api/v1/binned/gap", params={"width": 2, "height": 2}
)

assert response.status_code == 200, response.text
body = response.json()
assert body["n_points"] == 3
assert body["RedTotal"] == [[0.0, 1.0], [None, 3.0]]
assert body["DiodesRed"] == [[10.0, 11.0], [None, None]]
Loading