The Image View

The window your image lives in is a tool in its own right — it does much more than hold a pixmap. The top-bar glyphs are shortcuts to duplicate the view, link it live to other views, copy astrometric solutions between images, apply any image as a mask (including the image as a mask of itself for an instant lightness mask), create preview regions for testing compute-heavy operations before committing to the full image, replay single steps or whole histories from a preview onto the base, overlay a WCS grid, and probe individual pixels. This page covers everything the view can do and the modifier keys that unlock each mode.

Workflow
Everything in the view is already there — no mode switch, no preference, no menu to arm. Grab the top-left ⧉ to duplicate, link, copy-WCS, or apply-as-mask (modifier key chooses which). Click ⟂ and drag a rectangle to make a preview tab, run compute-heavy tools on it, then click ⟳ to replay the last step on the full image — or use ⟳'s dropdown to replay any specific step from the preview's history. Hold Space and click/drag to probe pixel values with a loupe (RGB/K + RA/Dec if a WCS exists). Right-click anywhere in the view for the full menu: rename, send to shelf, open the layers panel for this view, mask controls, and more.

The signature idea is that every view is also a document connector. The ⧉ drag tab at the top-left looks like a window-manager grab handle, but what you drop it on, and what modifier you hold while dragging, decides what happens: no modifier opens a new window with a duplicate of the image; Alt makes the two views pan and zoom together live; Ctrl copies the astrometric solution from this image to the one you drop on; Shift turns this image into a mask of that one. And the self-mask trick — Shift+drag the handle back onto its own view — is the fastest way to get a lightness mask: no mask-creation dialog, no settings, just an instant mask from the luminance of the image itself. Combine that with the preview tabs (any rectangular ROI becomes its own tab with its own undo stack) and the ⟳ replay button (push any step from the preview onto the full image when you like what you see), and the view stops being a passive container — it's where the whole workflow is coordinated.

Contents
1. Top-bar buttons at a glance
2. The drag tab — four modes
3. Preview tabs
4. The replay button
5. Pixel readout and loupe
6. WCS grid overlay
7. Zoom and pan
8. Autostretch
9. Linked views in detail
10. Masks
11. Right-click context menu
12. Technical details

1. Top-bar buttons at a glance

Across the top of every view, left to right:

ButtonFunction
⧉Drag tab — duplicate / link / copy WCS / apply as mask (see §2)
⟂Create Preview — click, then drag a rectangle on the image (see §3)
↶Undo (this view's active document — Full tab or active preview)
↷Redo (this view's active document)
⟳Replay last preview action on the base image (click), or pick from preview history (dropdown arrow) (see §4)
⌗Toggle WCS grid overlay (see §6)
right sideCurrent zoom % readout; inline view title (when the window is maximised)

The undo/redo buttons are per-view and per-tab: on the Full tab they walk the base document's history; on a preview tab they walk that preview's isolated history. See §3 for how preview history works.

2. The drag tab — four modes

The ⧉ handle at the top-left of every view is a shape-shifting drag source. The modifier key you're holding when the drag starts decides which of four payloads it carries:

ModifierWhat happens when you drop
(none)Duplicate. Dropping onto the MDI background opens a new window with a duplicate of this image — a fully independent copy with its own undo history. Great for keeping an untouched reference while you process a second copy.
AltLink views. Dropping onto another image's view links the two for live pan and zoom sync. You can link any number of views into a group — a third drop joins all three, a fourth makes it four, and so on. Unlink from the right-click menu (see §9).
CtrlCopy astrometric solution. Dropping onto another image's view copies this image's WCS onto that one. Useful if you've plate-solved one frame and want to seed the solution onto a crop, a re-processed version, or a frame that lost its solve.
ShiftApply as mask. Dropping onto another image's view sets this image as that image's active mask. A ■ badge appears in the target's title; Ctrl+K toggles the mask overlay visualisation, Ctrl+Shift+I inverts it.
The self-mask trick. Shift+drag the ⧉ handle back onto its own view. The image becomes a mask of itself — which is exactly a lightness mask. Bright regions are selected, dim regions are protected. Combine with Ctrl+Shift+I to flip it: now the dim regions are selected and the brights are protected. Any subsequent operation (curves, denoise, saturation) will respect the mask. This is the fastest way to a lightness mask in SASpro, with no mask-creation dialog in the way.

3. Preview tabs

Preview tabs let you try out compute-heavy processing on a small part of an image before committing to the full version. Click the ⟂ button, then drag a rectangle on the image. A new tab appears beside Full labelled Preview 1 (W×H), and the view switches to showing just that cropped region at full data resolution.

Create as many previews as you like. Each one gets its own tab, its own isolated undo/redo stack, and its own ROI-synthesized WCS (so pixel readout still gives correct RA/Dec inside the preview). The ↶ and ↷ buttons on the top bar switch automatically between the Full document's history and the active preview's history as you change tabs.

What you do with a preview:

A preview's WCS is derived from the base image's WCS by shifting CRPIX1 and CRPIX2 by the ROI origin, so the preview's own pixel-to-world mapping stays accurate. If the base image was already a crop (SASpro detects this via header hints — SASKIND=ROI-CROP, CROPX/CROPY, or the ROI being plainly outside the parent's NAXIS), the shift is skipped so the preview's WCS doesn't get corrected twice.

4. The replay button

The ⟳ button has two parts:

Replay doesn't just re-run the operation — it replays the exact preset that was used. SASpro keeps a headless-command history in the main window; the replay logic counts command-id occurrences in the preview's own step list to find the Nth matching entry in that history, pulls the preset from that entry, and routes it through the main window's base-image replay path. If a step was “Cosmic Clarity Denoise at strength 0.7, luminance only,” the replay runs with strength 0.7, luminance only — not with defaults.

The button is only enabled when a preview tab is active and that preview has committed steps. On the Full tab, or on a preview that hasn't been edited yet, it's greyed out.

5. Pixel readout and loupe

Hold Space and click or drag anywhere on the image to probe individual pixels. Two things happen:

Drag with Space still held to probe continuously. Release the mouse to stop; the loupe hides. Space can be released without ending the probe — it's the mouse release that ends it.

On reject-count layers after stacking. If the active view is the per-pixel reject-count layer from a stack integration, the readout automatically shows the actual integer N of frames rejected at that pixel (not the internal normalised float that would be meaningless). This is how you check how many frames contributed to any given pixel after a sigma-clipping stack.

Loupe sizing and the readout strip are configurable from Preferences → Pixel Readout (patch size, zoom size, info font point size, show/hide the readout strip). Changes apply to views opened after the setting is changed.

6. WCS grid overlay

If the image has a valid astrometric solution (plate-solved, or inherited via a Ctrl+drag copy), click ⌗ to overlay a celestial coordinate grid directly on the image. RA lines are labelled in HMS at the top edge, Dec lines in DMS at the left edge, with a thin white grid in between.

Grid spacing is automatic by default — the step is chosen to keep roughly eight grid lines across the field of view. You can switch to a fixed step from Preferences → WCS Grid (choose degrees or arcminutes, set the value). The grid is baked into the display pixmap only when the view is settled (not actively zooming or panning), so interaction stays snappy.

The toggle is per-view and resets to off on every new view. If the image has no WCS, clicking the button turns the state on but nothing visible changes (there's nothing to overlay).

7. Zoom and pan

Standard interactions:

The scale range is 2% to 500%. There's no fit-to-window button on the view itself (the main window has Zoom → Fit), but the mouse wheel plus a click to centre is usually faster.

8. Autostretch

Toggle autostretch for the view: Ctrl+Space or Alt+Shift+A. Autostretch is a display-only non-destructive transformation — the stored pixel data is untouched; only what's drawn changes. Turn it off and you're back to looking at the raw linear data.

Two profiles, switchable from the main-window toolbar or Preferences:

Autostretch can be linked across channels (one LUT applied to all three RGB channels equally — preserves colour balance) or unlinked (each channel gets its own LUT — neutralises colour cast but can shift hue). The linked / unlinked choice is per-view and lives in Preferences.

Autostretch does not alter the file, the processing history, or any downstream computation. It only changes what you see on screen. All tools (stacking, calibration, masks, measurements) operate on the raw data regardless of whether autostretch is on.

9. Linked views in detail

Alt+drag the ⧉ handle from one view onto another to link them. Once linked:

Linked views don't need to be the same size. If you link a 400×300 preview window to a 1920×1200 main image, they still track together — each view works out from its own viewport which image coordinate should be centred and nudges its own scrollbars accordingly. The sync is live during the drag, not just at drag end.

To unlink: right-click → Unlink from Linked Views. This removes the current view from the group entirely; the other members stay linked to each other.

10. Masks

Any image can be a mask of any other. The mechanics:

Masks are shape-matched to the target's dimensions at apply time (if the mask image is a different size, it's resampled). Operations run on the target image are modulated by the mask: fully-selected pixels (mask value 1.0) receive the full effect; fully-protected pixels (mask value 0.0) are untouched; intermediate values blend proportionally.

For richer mask creation (shape masks, content masks, star masks, Gaia-calibrated star radii), use the dedicated Mask Creation tool — the shift-drag path here is the quick route for the common case of using an existing image as a mask.

11. Right-click context menu

Right-click anywhere in the view for the full menu. The items (grouped by section):

12. Technical details

Implementation notes, numbered continuing from the usage sections.

13. Drag-tab payloads and MIME types

Each of the drag tab's four modes produces a different MIME type on the drag payload, so drop targets can unambiguously identify what's being offered:

Modifier state is read at the moment the drag threshold is crossed (Manhattan distance > 6px from the press point), not at drag-start, so the user can decide what mode they want after they grab.

14. Resolution-independent view linking

When a view's pan or zoom changes, it emits a viewTransformChanged(scale, h, v) signal. Instead of relaying raw scrollbar values (which would require all linked views to have identical pixmap sizes), the relay computes the image fraction currently under the sender's viewport centre:

lbl = sender.label vp = sender.scroll.viewport() p = lbl.mapFrom(vp, QPoint(vp.width()//2, vp.height()//2)) fx = p.x() / lbl.width() fy = p.y() / lbl.height()

Each linked peer then applies scale and places the fraction (fx, fy) at its own viewport centre, computing its own scrollbar values from its own label dimensions. Windows of different sizes stay in sync without one dictating the other's layout.

The relay is guarded by _suppress_link_emit during the apply so echoes don't bounce back and create infinite recursion. During live panning, the normal 100ms debounce is bypassed and every mouse-move emits immediately, so linked views track the drag in real time rather than catching up after it ends.

15. Preview-to-base replay routing

Each preview tab keeps a list of committed operations on its backing document: doc._preview_commands, each entry a dict with command_id and the step name. The main window keeps a parallel _headless_history that records every command and its full preset.

When the user clicks a step in the ⟳ dropdown, the replay logic:

  1. Counts how many times this step's command_id appears in _preview_commands up to and including the clicked index → occurrence = N.
  2. Walks _headless_history, counting entries whose normalised cid matches, and selects the Nth one → this is the entry that corresponds to the clicked step.
  3. Builds a payload {command_id, preset} from that history entry, swaps it into the main window's _last_headless_command, calls replay_last_action_on_base(target_sw), then restores the previous _last_headless_command so a subsequent Ctrl+Shift+R still means “the actual last action.”

If a matching history entry can't be found, the fallback is to use whatever's in _last_headless_command if its cid matches; otherwise the replay is skipped with a console warning.

16. Space-hold readout mechanics

Holding Space sets _space_down = True. The next left-click press (anywhere on the viewport or label) is intercepted before the normal pan handler: the viewport position is mapped to image coordinates, the pixel is sampled from the source document (not the display buffer, so values are raw and uncorrupted by autostretch or mask overlay), the loupe is positioned to the right / below the cursor (or above / left if that would go off-screen), and the status bar is updated. Mouse-move events while _readout_dragging is set continue this.

Releasing the mouse ends the readout drag and hides the loupe. Releasing Space does not end the drag — the user can start probing with Space held, then release Space while continuing to drag. Only the mouse release ends it. This matches how PixInsight-style readout tools work and avoids the frustration of accidentally releasing the probe key.

17. Loupe window

The loupe is a frameless always-on-top Qt.Tool window with WA_ShowWithoutActivating and WindowTransparentForInput, so it floats above the view without stealing focus or intercepting mouse events. Patch sampling takes the configured number of source pixels around the probe location (default 17, forced to odd so the crosshair bisects the centre pixel), with edge-aware padding when the probe is near the image boundary — the padded canvas stays the full patch size, with the real pixels inset by however much the probe was away from the edge, so the crosshair still points at the correct source pixel.

The patch is scaled to the display size (default 161, also forced odd) with Qt.FastTransformation (nearest-neighbour) so individual source pixels render as crisp uniform-colour blocks rather than a smeared gradient. The optional readout strip below picks the largest monospace point size (down to 7pt) at which all lines fit within the loupe width, so RGB with 4-decimal precision and shortened RA/Dec strings all fit without truncation on any configured size.

18. ROI-synthesized WCS for preview tabs

When a preview tab is active, the view's _get_celestial_wcs() prefers (in order):

  1. The ROI backing document's own WCS, if DocManager has materialised a separate ROI doc for this preview.
  2. A synthesized WCS built from the base image's header plus the preview's ROI, computed by _compute_cropped_wcs(hdr, x, y, w, h): deep-copy the base header, subtract the ROI origin from CRPIX1 / CRPIX2, update NAXIS1 / NAXIS2 to the ROI size, and tag it SASKIND=ROI-CROP.

Before shifting CRPIX, the function checks whether the base header looks like it's already a crop:

Both cases return a copy of the parent header marked as ROI-CROP but without a double CRPIX shift. The synthesized WCS is cached on the base document's metadata keyed by the preview id, so repeated reads don't re-synthesize.

19. Viewport-sized render pipeline

The view never builds a full-resolution 8-bit copy of the source image. The render pipeline rasterises the display buffer at the current zoom scale, capped at 8192 pixels on each side:

When the cap engages, the effective scale the buffer represents is below what the user asked for. The view tracks this honestly as _pm_src_scale, which is used by the present-scaled call to size the label widget correctly and by all coordinate conversions (loupe, pixel readout, view-state drops, linked views) so they stay in sync with what's actually on screen. The zoom-% readout likewise reports the honest effective scale, not the requested scale.

When the user zooms in and the cached buffer becomes under-resolved for the current zoom, a 500ms debounced settle re-rasterises the buffer at the newer scale (_zoom_settle_needs_rebuild detects when the current buffer is below what a rebuild could produce and when a rebuild would improve matters, i.e. the current scale isn't already at the 8192 cap). A small translucent overlay in the viewport's bottom-left reads “Recalculating display stretch…” while the settle runs.

20. Autostretch LUT caching

Autostretch builds a lookup table indexed by input value and reads output through it. The cache key is (is_mono, linked, target_median, sigma, no_black_clip, input_shape); when all six match, the previously-built LUT is reused and only the per-pixel apply runs. When any changes (profile switch, linked/unlinked toggle, no-black-clip change, image replaced), the key mismatches and a fresh LUT is built from a stride-subsampled slice of the input (step chosen so the sample is around 65k pixels — sufficient for the median/sigma statistics, orders of magnitude faster than sampling every pixel).

LUT size is 4096 entries (12-bit) by default, or 16,777,216 entries (24-bit) if the user has enabled 24-bit stretch in Preferences for maximum tonal precision. The apply function auto-detects which size was built from the LUT's length and dispatches to the matching lookup path, so changing the preference after a LUT was built doesn't crash the apply.

21. Per-doc apply with per-view refresh

Edit application always targets the specific document passed in — never “the active document” by position. This matters for the headless and scripted paths, which may apply an edit to a document that isn't currently in focus.

The refresh walks the MDI subwindow list looking for the one whose widget().document is identically the target doc, and calls that view's refresh_pixmap_from_document / refresh_view / update_from_doc hook if any exists, falling back to plain update() on both the view and its subwindow. The currently-active subwindow isn't changed, so the user's focus stays where it was — even if an edit was just applied to a different view.

Original work for SetiAstro Suite Pro. The Image View is the primary interaction surface in SASpro; the little window is where every tool meets the user. Qt (PyQt6) handles window chrome, scrolling, and base rendering; everything that makes the view an active tool rather than a pixmap holder is new work by Franklin Marek. SASpro-exclusive content: the four-modifier drag-tab affordance (⧉ at the top-left) that routes to four different outcomes from one grab — plain drag duplicates the view into a new document, Alt+drag starts a live view-link, Ctrl+drag copies the astrometric solution to another image, Shift+drag applies the image as a mask to another — with modifier state read at the moment the drag threshold is crossed so the user can decide after they grab; the self-mask shortcut, where Shift+dragging the drag tab back onto its own view applies the image as its own mask, giving an instant lightness mask with no mask-creation dialog; the preview-tab system where any rectangular ROI becomes a tab beside Full with its own isolated undo/redo stack, its own ROI-synthesized WCS (CRPIX shifted by the crop origin, with double-crop detection so a BOTTOM-UP already-cropped file doesn't get CRPIX corrected twice), and its own history of preview-committed operations; the replay-to-base button (⟳) that offers a left-click action (replay the last preview step on the full image) and a dropdown menu listing every step committed on the active preview, with a lookup that counts command-id occurrences in the main window's headless history to find the exact preset that was used for each step and restores it in-place for the replay; the resolution-independent linked views system that forwards pan and zoom between any number of windows as a (scale, image-fraction-under-centre) tuple so linked views don't have to share size or zoom-cap state — one window at 400×300 can be linked to another at 1920×1200 and both will stay centred on the same image feature as either is panned; the Space-hold pixel-readout mode that turns the normal left-click pan into a per-pixel probe without changing the cursor or tool state, feeding both the status bar (pixel, RGB/K, RA/Dec in HMS/DMS) and a translucent loupe window (nearest-neighbour-scaled 17-px source neighbourhood scaled up to a 161-px display with crosshair, plus an inline readout strip beneath it); the count-layer-aware readout that detects when the active document is a stacking reject-count layer and reports an actual integer N (frames rejected at this pixel) instead of a meaningless normalised float; the viewport-sized render pipeline that never builds a full-resolution 8-bit copy of a mosaic — it rasterises at display size with an 8192-per-side cap, tracks an honest effective_scale so the zoom-% readout and all coord math stay truthful even when the cap engages, caches the autostretch LUT keyed on (is_mono, linked, target, sigma, no-black-clip, shape), and settles to a full-resolution re-rasterise after the zoom-in gesture stops; the ROWORDER-aware pixel readout that honours the orientation flip done by the mosaic debayer upstream so (x, y) coordinates reported to the user match the file-on-disk coordinates no matter which way the data was stored; the double-drop MIME system (MIME_VIEWSTATE / MIME_LINKVIEW / MIME_MASK / MIME_ASTROMETRY / MIME_CMD) that lets the drag tab produce typed drag payloads and lets any view accept them without ambiguity; the MDI subwindow registry keyed by id() through a WeakValueDictionary so cross-view operations (link-to, copy-wcs-to, apply-as-mask-to) resolve the target view by identity without lifetime leaks; the per-doc apply with view refresh that always targets the specific document and walks the MDI list to repaint only the subwindow bound to it, without stealing the active view's focus; the context-menu mask control cluster that exposes show-overlay / invert / remove inline when a mask is applied, in addition to Ctrl+K to toggle overlay visibility and Ctrl+Shift+I to invert; the MDI-maximised inline title that moves the view's name into the top bar when the subwindow is maximised so the user doesn't lose track of which document is open; and the send-to-shelf action that cleanly hides a view without closing its document, activates a neighbouring view so focus flows naturally, and leaves the entry retrievable from the window shelf.

Credits

The Image View is original SASpro work by Franklin Marek — the four-modifier drag-tab affordance, self-mask shortcut, preview-tab system with isolated history, replay-to-base button with preset-aware history replay, resolution-independent view linking, Space-hold pixel-readout mode with configurable loupe, count-layer-aware readout, viewport-sized render pipeline with honest effective-scale tracking, autostretch LUT caching, ROI-synthesized WCS, MDI subwindow registry, per-doc apply with view refresh, and all the context-menu, keyboard-shortcut, and drag-and-drop wiring that holds it together.

External libraries: