Problem
Detector orientation (transpose + rotate) logic is duplicated across ~20 sites with no single "raw vs display" boundary. Each site re-derives the transpose/rot math on its own, which is fragile and has already caused bugs.
Concretely:
pva_reader.py:411 pre-transposes reader.image (.T when image_is_transposed), and the viewer transposes again at area_det_viewer.py:1795-1796 → a double-transpose (the live Transpose checkbox is nearly a display no-op in scan/alignment modes).
- ROI coords are hand-swapped for transpose only, not rotation:
area_det_viewer.py:1235-1238, :1535-1542.
- RSM q-maps
.T'd: :1759-1761. Mouse-pixel readout indexes ad-hoc: :1714-1717.
mask_viewer.py re-implements the transpose+rot transform and its inverse (_get_display_mask, _transform_data_for_display, _display_to_native, orientation buttons).
- The mask is stored in the reader/display orientation, not raw — which made the EPICS
NDPluginBadPixel JSON export write swapped [X,Y] (fixed surgically at export; the underlying inconsistency remains).
Target design
Single source of truth = raw for both image and mask; one Orientation value object maps raw↔display; every display/export path derives from raw through it.
@dataclass(frozen=True)
class Orientation:
transposed: bool = False
rot_k: int = 0 # CCW 90° steps; convention: transpose THEN rotate
def apply(self, arr): ... # raw -> display
def invert(self, arr): ... # display -> raw
def display_index_to_raw(self, i, j, raw_shape): ... # clicks / ROIs
def raw_index_to_display(self, r, c, raw_shape): ...
Rules of the road:
- Reader emits raw only (drop the
.T at pva_reader.py:411).
- Viewer holds one
Orientation; Transpose/Rotate controls mutate only it.
- Display derives:
orientation.apply(raw). Masking = apply raw mask to raw image, then apply once.
- ROIs, q-maps, mouse readout all go through
orientation (also fixes ROI-under-rotation, currently unhandled).
- Mask always raw in
mask_manager (detection from raw frames; import/export raw; overlay via orientation.apply).
mask_viewer consumes the same Orientation; delete its duplicate transform code.
Phased rollout
- Phase 1 — add
orientation.py (+ unit tests); make mask_manager.mask always raw (dead-pixel detection from the raw frame; overlay via orientation.apply). Low blast radius; no RSM/HKL-3D impact.
- Phase 2 — reader → raw; route viewer display, ROIs, q-maps, mouse readout, and
mask_viewer through the one Orientation; delete the double-transpose + duplicated code. Needs live RSM/HKL-3D + ROI verification.
Context
Follow-up to the surgical bad-pixel JSON export fix (un-transpose the mask at export so the file is raw [X,Y]=[col,row]). This issue tracks the proper architectural fix so orientation is centralized and raw is authoritative.
Problem
Detector orientation (transpose + rotate) logic is duplicated across ~20 sites with no single "raw vs display" boundary. Each site re-derives the transpose/rot math on its own, which is fragile and has already caused bugs.
Concretely:
pva_reader.py:411pre-transposesreader.image(.Twhenimage_is_transposed), and the viewer transposes again atarea_det_viewer.py:1795-1796→ a double-transpose (the live Transpose checkbox is nearly a display no-op in scan/alignment modes).area_det_viewer.py:1235-1238,:1535-1542..T'd::1759-1761. Mouse-pixel readout indexes ad-hoc::1714-1717.mask_viewer.pyre-implements the transpose+rot transform and its inverse (_get_display_mask,_transform_data_for_display,_display_to_native, orientation buttons).NDPluginBadPixelJSON export write swapped[X,Y](fixed surgically at export; the underlying inconsistency remains).Target design
Single source of truth = raw for both image and mask; one
Orientationvalue object maps raw↔display; every display/export path derives from raw through it.Rules of the road:
.Tatpva_reader.py:411).Orientation; Transpose/Rotate controls mutate only it.orientation.apply(raw). Masking = apply raw mask to raw image, thenapplyonce.orientation(also fixes ROI-under-rotation, currently unhandled).mask_manager(detection from raw frames; import/export raw; overlay viaorientation.apply).mask_viewerconsumes the sameOrientation; delete its duplicate transform code.Phased rollout
orientation.py(+ unit tests); makemask_manager.maskalways raw (dead-pixel detection from the raw frame; overlay viaorientation.apply). Low blast radius; no RSM/HKL-3D impact.mask_viewerthrough the oneOrientation; delete the double-transpose + duplicated code. Needs live RSM/HKL-3D + ROI verification.Context
Follow-up to the surgical bad-pixel JSON export fix (un-transpose the mask at export so the file is raw
[X,Y]=[col,row]). This issue tracks the proper architectural fix so orientation is centralized and raw is authoritative.