Skip to content

Camera display range is re-normalised every frame, so the image moves while you watch #149

Description

@pskeshu

_frame_payload auto-contrasts every frame independently: a 1st–99th percentile stretch computed off a strided sample of the downsampled frame, then scaled to 8-bit before JPEG encoding (device_layer.py:967-981).

It is a sensible default for a first look, and wrong for everything after that.

Why it fights the operator

The range moves while you watch. Each frame gets its own lo/hi, so brightness changes because the normalisation shifted, not because the sample did. Judging focus by eye means judging a signal that is being renormalised underneath you. The same is true of the focus score's visual counterpart — an operator sweeping the F-drive is comparing frames that were each scaled differently.

Faint or sparse nuclei are clipped. Bright nuclei on a dark field occupy the top fraction of a percent of the histogram, which is precisely what the 99th-percentile ceiling discards. Ryan at 20:19 on the 2026-08-07 walkthrough, on why two-point calibration was hard: "the embryo is a little small, so we'd probably zoom in on this like in Micro-Manager to make it easier to see nuclei". Zoom is now available and does not help with this — the counts are gone before the browser gets the frame. Likely a contributor to #125.

The sample is strided, not random. flat[::step] over a raster is correlated with image structure — a periodic feature aligned with the stride is over- or under-represented. Cheap and usually harmless, but it means lo/hi can jitter between frames that look identical.

Not fixed by the client-side sliders

The contrast/brightness controls added alongside zoom are CSS filters over the already-stretched 8-bit JPEG. They redistribute what survived; they cannot recover what the stretch clipped.

Done when

The operator can freeze the display range, and a frozen range is visibly frozen. Sketch:

  • carry lo/hi (or percentile pair) as live params, the way exposure and galvo already travel through set_lightsheet_live_params
  • an auto/hold control in the Image panel, defaulting to today's behaviour so nothing changes unasked
  • when held, keep the last computed range and say so on the frame — a stale range silently applied is its own trap
  • consider raising the ceiling above the 99th, or making it settable, for sparse bright objects

Worth measuring before building: capture one raw 16-bit frame with visible nuclei and check what fraction of the range they occupy. If they sit above the 99th percentile, that is the whole bug and the fix is a number.


Found while adding client-side contrast controls; see the feat(ui): contrast and brightness on the camera surfaces commit.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghardwareHardware/device relateduiUser interface

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions