Generates a labelled finder chart for the current field by fetching a cutout of a published sky survey (DSS2, Pan-STARRS, SDSS, DESI Legacy, 2MASS, Gaia DR3, or SIMG's Northern-Sky Narrowband Survey) through the CDS hips2fits service, overlaying the user's own doc with a WCS-correct perspective warp, and annotating with bright-star names, deep-sky catalogue objects, an N/E compass, a scale bar, and optionally the imaging train's field-of-view rectangle. Two entry points: driven from an open SASpro doc (uses the doc's WCS to compute the chart's centre and FOV), or driven directly from RA/Dec (used by object-search workflows where no image exists yet).
hips2fits HTTP service (see Credits), SASpro's job is to pick the right cutout, blend it with the user's data, and annotate it. SASpro-exclusive content: the multi-survey HiPS dispatcher with a canonical label → HiPS-ID map covering optical broadband (DSS2 color/red/blue/IR, Pan-STARRS DR1, SDSS9 color-alt, DESI Legacy DR10), infrared (2MASS J/H), Gaia DR3 flux-color, and the Northern-Sky Narrowband Survey (NSNS; TC8 true-colour, Hα DR0.1/DR0.2, [OIII] DR0.2, [SII] DR0.2) — the NSNS surveys are not on CDS, so SASpro routes them through hips2fits by passing the simg.de base URL directly as the hips parameter rather than using a CDS catalogue ID; the overfetch-and-crop strategy (fetch at √2 · 1.02 larger FOV and resolution so a doc at any orientation is covered by the cutout, then centre-crop with the WCS adjusted by CRPIX' = CRPIX - (x_0, y_0)); the three-tier cache (HiPS raster cache keyed on survey+resolution+centre+FOV, base-raster cache for the composed HiPS+doc overlay keyed on HiPS-key+opacity+doc-identity, geometry cache for the doc's WCS/corners/centre/FOV); the debounced render pipeline that distinguishes fetch-triggers (survey, FOV, output size — immediate refetch) from render-triggers (grid, star names, deep-sky overlay, compass, scale bar, FOV box — 150 ms debounce, no network); the WCS-correct doc overlay that projects the user's doc through its own WCS to the sky and through the background WCS back to pixels, derives the cv2.getPerspectiveTransform from the four corner correspondences, warps the doc into the background, and alpha-blends on the warped-content mask; the deep-sky catalogue loader that merges a packaged celestial_catalog.csv covering Messier/NGC/IC/Abell/Sharpless2/LBN/LDN/PN-G with multi-encoding tolerance (latin-1 → UTF-8-BOM → UTF-8 → cp1252 fallback chain), BOM-tolerant column lookup, and catalogue code canonicalisation that collapses the five Sharpless variants (SH2-, SH2 , SH-2, SH 2, SH_2) and four PN-G variants (PNG, PN-G, PN G, PN_G) to canonical codes before filtering; the grid-cell declutter algorithm for both star and DSO labels (34 px cells for DSOs, 28 px cells for stars, first-in-cell wins after brightness-first sorting); the in-bounds label placer that places the label, measures its bounding box in display coordinates via get_window_extent(renderer), computes the shift needed to stay inside the plot area, re-measures to confirm, and drops the label if a second measurement still exceeds the bounds rather than letting it clip at the edge; the DSO size-circle renderer that parses the Info field with the "6.0x4.0" arcmin-dimension regex and draws a cyan outline circle at the major-axis diameter in pixel scale; the N/E compass that constructs the North and East direction vectors by stepping a small sky offset from the field centre and projecting both to pixels (with cos(δ) compensation on the East step so it's a real arc-length step at the given declination); the FOV-dependent scale bar with four bar-length tiers (30′ / 20′ / 10′ / 5′) selected on the fetched FOV, pixel scale derived from proj_plane_pixel_scales, and the arcsec/pixel reported alongside the bar label; the imaging-train FOV box with angular half-sizes atan(pitch · N / (2 · f)) computed per axis, four corners constructed in (East, North) offset space, rotated clockwise from north by the user's rotation angle, converted to RA/Dec with cos(δ) compensation on the East offset so the box lands correctly away from the equator, and projected to the chart's pixel coordinates; the live FOV readout that recomputes the width′ × height′ display whenever any of focal length / pitch / sensor dimensions changes; the WCS recovery chain that walks four fallback paths (prebuilt wcs object → nested original_header/fits_header/header/FITS Header/FITS_HEADER → flattened FITS Header.KEY export-style keys → return None) with non-FITS metadata key stripping and SIP-presence CTYPE patching; and the full QSettings-based persistence of every control for next-launch recall.
The signature move is the WCS-correct perspective warp of your own data onto the published survey:
Your doc's four corners are projected through its own WCS to sky coordinates, then those sky coordinates are projected through the background's WCS back to pixels in the finder chart. The four corner correspondences give the homography, OpenCV warps the whole doc into the background frame, and an alpha-blend composite shows your image at any requested opacity. The result: the published survey tile and your own data occupy the same arcseconds of sky at the same pixel resolution, so your target lands exactly where you shot it — useful for publication figures (“here's my image, here's where it sits in the DSS”), for cross-checking a plate solve against a known survey, or for annotating your field with catalogue objects you can navigate.
Finder charts have three main uses in astrophotography:
SASpro's Finder Chart handles all three. It pulls a cutout of a published sky survey centred on your field, overlays your own image onto it with the correct WCS alignment, and annotates with bright-star names, deep-sky objects, compass direction, pixel scale, and (optionally) the FOV rectangle of your imaging train.
Opens with the current SASpro view's WCS as the reference. The chart's centre is the view's centre pixel projected to sky; the chart's FOV is derived from the view's four corners. Scale is a multiple of the doc's own FOV: 2×, 4×, or 8×. The doc image is overlaid onto the background at user-configurable opacity.
Driven by a (name, RA, Dec) tuple instead of a doc. Used by SASpro's object-search workflows (WIMS) where the user is browsing a catalogue and wants to see what a candidate target looks like before loading any data. No doc overlay — just the survey background and overlays. FOV is picked from a dropdown of standard values (0.25° to 10°).
Dropdown with 13 published surveys plus 5 NSNS narrowband bands, grouped by wavelength:
| Group | Survey | Notes |
|---|---|---|
| Optical broadband | DSS2 (Color / Red / Blue / IR) | Second-generation Digitized Sky Survey from STScI, the universal all-sky baseline. Depth ~21 mag, native scale ~1.7′′/px. |
| Pan-STARRS DR1 (Color) | Northern sky, ~0.25′′/px native. Much deeper than DSS2 but limited to δ > -30°. | |
| SDSS9 (Color Alt) | Sloan Digital Sky Survey, high-latitude strips. Great for galaxy work. | |
| DESI Legacy DR10 (Color) | Dark Energy Spectroscopic Instrument imaging surveys. Very deep (~24 mag), broad sky coverage. | |
| Infrared | 2MASS (J) | Two Micron All-Sky Survey, J band (1.25 µm). Useful for dust-penetrating views. |
| 2MASS (H) | H band (1.65 µm). | |
| Gaia | Gaia DR3 (Flux Color) | Not a classical image survey — a synthesised false-colour image from Gaia DR3 BP/RP flux ratios. Perfect for sanity-checking star fields. |
| NSNS narrowband | DR0.1 True Color (TC8) | SIMG's Northern-Sky Narrowband Survey. Visually arresting narrowband at modest resolution. |
| DR0.1 / DR0.2 Hα | Hydrogen-alpha emission — the shape of HII regions, SNRs, planetary nebula halos. | |
| DR0.2 [OIII] | Doubly-ionised oxygen — planetary nebula bodies, hot stellar winds. | |
| DR0.2 [SII] | Singly-ionised sulphur — shock fronts, SNR filaments. |
Changing the survey triggers an immediate refetch (not debounced) because every pixel of the chart changes.
Doc-driven dialog: Size dropdown with three multipliers of the current doc's FOV — 2× (zoom in, context around the frame), 4× (default, generous surround), 8× (wide context).
Coord dialog: FOV dropdown with ten absolute values from 0.25° to 10°. Default 4°.
Changes trigger a refetch because the request to the HiPS service has to be redone at the new angular size.
Output px spinbox (300 to 2400, step 100, default 900): the pixel size of the finished chart. The HiPS service is asked for this many pixels on a side; your doc gets resampled through the perspective warp to match. Higher is sharper but slower to fetch and heavier to save. For saving at publication quality, 1800–2400 px is typical.
(Doc-driven dialog only.) Slider from 0% to 100%, default 35%. Controls how much of your own data is blended onto the background:
Slider movements debounce the render (no refetch) and are additionally gated on sliderReleased for the final high-quality render.
Check Star names to label every bright star in the chart's area. Two parameters:
The source is SASpro's packaged bright-star table (same used by other SASpro tools). Labels are placed with a gold dot marker and the star's common name (Vega, Altair, Betelgeuse, …). Decluttering keeps nearby bright stars from stacking their labels on top of each other.
Check Deep-sky to annotate the catalogued objects in the field. Three parameters:
DSOs get a cyan square marker, a label, and — where the catalogue entry includes a size — a cyan outline circle at the object's major-axis diameter in sky arcminutes converted through the chart's pixel scale. The circle gives a visual sense of how big the object is in your field.
Check Compass to draw N/E direction arrows in the bottom-right corner. The arrows are built from the actual background WCS by stepping a small sky offset and projecting it to pixels, so they correctly point to true north and east on the chart including any rotation or near-polar distortion.
Check Scale bar to draw a labelled scale bar in the bottom-left corner. The bar's physical length is chosen from the FOV (30′ / 20′ / 10′ / 5′), and the label shows that length plus the pixel scale as arcsec/px.
Check Show grid to overlay an RA/Dec grid using Astropy's WCSAxes. The grid uses the background's WCS so it draws correctly curved near the poles and projects straight along the equator; tick labels are RA and Dec in standard decimal format.
Check Show FOV box to draw a labelled rectangle showing where a specific imaging train would frame the field. Five parameters:
As you adjust any parameter, a live FOV readout appears next to the controls showing the resulting width × height in arcminutes. The box itself updates on the chart on debounce (no network required). Useful for pre-session planning: dial in different scope/camera combinations and see which gives you the composition you want.
Writes the current chart as a PNG at the full output resolution. The last-used directory is remembered across launches.
Opens the chart as a brand-new SASpro view, complete with metadata recording how it was generated (survey, scale multiplier, grid state, output resolution, overlay opacity). Useful when you want to further process the chart (annotate manually, crop, export at a different size).
WCS recovery, HiPS fetch and cache, overlay warp, and the overlay-rendering pipeline in order.
SASpro's metadata model stores WCS information in several possible forms depending on how a doc was created, so the Finder Chart walks a four-tier fallback chain:
astropy.wcs.WCS object under meta["wcs"] (fastest path — the Plate Solver stores one here after a successful solve).astropy.io.fits.Header under meta["original_header"], meta["fits_header"], meta["header"], meta["FITS Header"], or meta["FITS_HEADER"]."FITS Header.CTYPE1" = "RA---TAN-SIP" coming from CSV-exported metadata — SASpro rebuilds a Header from these by stripping the prefix.None — the finder chart renders with no WCS, which falls back to a plain pixel grid and no catalogue overlays.When a header is reconstructed, SASpro strips a set of non-FITS metadata keys that are sometimes stored alongside real cards (FILE_PATH, FITS_HEADER, BIT_DEPTH, WCS_HEADER, etc.) so the resulting WCS() construction doesn't blow up on invalid keywords. The celestial subspace of the WCS is used when the full WCS has extra axes (spectral, polarisation).
Given a WCS and image dimensions, the four corner pixels (0.5, 0.5), (W-0.5, 0.5), (W-0.5, H-0.5), (0.5, H-0.5) are projected to sky through pixel_to_world, giving a SkyCoord array of corners and the centre pixel. Angular separations across opposite edges give the width and height:
Width is the average of top-edge and bottom-edge separations; height is the average of left-edge and right-edge separations. The final FOV used for the fetch is max(width, height) × scale_mult, so a highly rectangular doc still gets a square cutout that covers its longer dimension at the chosen scale.
SASpro fetches survey cutouts from CDS's hips2fits HTTP service, which renders HiPS-format sky tiles into a flat FITS cutout at the requested projection, FOV, and resolution. The request URL encodes the HiPS catalogue identifier, the output dimensions, the FOV, the TAN projection, ICRS coordinate system, the centre RA/Dec, and format=fits. Response is a FITS file in bytes; SASpro opens it from memory with astropy.io.fits.open(BytesIO(data)), extracts the first HDU's data and header, and builds a WCS from the header.
Data normalisation: NaN/inf scrubbed, converted to RGB (grey → grey×3, channel-first → channel-last if needed), percentile-stretched to 1st/99.5th percentile and clipped to [0, 1] float32. The stretch is appropriate for display; the normalised data is held as float01 for every subsequent cache and overlay operation.
The Northern-Sky Narrowband Survey (NSNS) tiles live at simg.de/nebulae3/..., not at CDS. CDS's hips2fits service accepts an arbitrary HiPS base URL as the hips parameter, so SASpro routes NSNS fetches by handing hips2fits the simg.de URL directly. The internal survey ID uses an SIMG/ prefix (SIMG/simg.de/P/NSNS/DR0_1/tc8, etc.) that the dispatcher detects, strips, and maps to the corresponding http://www.simg.de/nebulae3/dr0_1/tc8 URL for the hips2fits call. From hips2fits' perspective it's a normal remote HiPS source; from the user's perspective they just picked “NSNS DR0.2 (Hα)” from the survey dropdown and SASpro figured out the plumbing.
A square cutout at exactly the requested FOV would leave the chart's corners unmapped once overlays apply any rotation or when the doc's footprint is rotated against the HiPS tile grid. SASpro overfetches at a factor of √2 plus a 2% safety margin:
and scales both the FOV and the pixel count by this amount:
which preserves the pixel scale (arcsec/pixel) across the overfetch. Then centre-crops back to the requested output size, with the WCS adjusted by the crop origin:
— a plain pixel offset, since the CRPIX axis convention matches between FITS and the Python array slice. The crop happens on the cached full-resolution raster, so overlays that redraw don't trigger a refetch.
Three independent caches, each keyed on exactly the parameters that invalidate it:
| Cache | Holds | Key | Invalidated by |
|---|---|---|---|
| HiPS raster | The fetched (and crop-corrected) background image + WCS + error string | survey, output px, centre RA/Dec, fetch FOV | Survey change, resolution change, FOV change, re-centre |
| Base raster | The composed uint8 RGB array that goes into matplotlib's imshow — either plain background or background + warped doc overlay | HiPS cache key + opacity + doc identity | Any of the above, plus overlay opacity change or doc swap |
| Doc geometry | Doc's WCS, four sky corners, centre, FOV | Image shape, WCS object id, size multiplier | Doc change, size multiplier change |
Why three tiers: a user adjusting the deep-sky magnitude slider should not retrigger an HTTP fetch and a perspective warp of their image — both expensive. The matplotlib layer (markers, labels, compass, scale bar, grid) is cheap. Separating those costs lets interactive adjustments stay snappy. Changing a different parameter down the dependency chain invalidates everything above it; the three-tier structure means the right invariant level is kept whatever the change.
Each UI control is routed to one of two paths:
force_refetch=True, which invalidates the HiPS and base-raster caches.QTimer, no refetch. Multiple events inside the debounce window coalesce into a single re-render.Opacity is a special case: the slider sends many valueChanged events during a drag, so SASpro additionally hooks sliderReleased for the final clean render once the user settles. Status label updates immediately on any debounced trigger so the user sees “Rendering…” feedback even before the timer fires.
When the user wants their data visible on the chart, SASpro overlays it by projecting the doc through its own WCS to sky coordinates, then through the background WCS back to pixels. The four corner pixels of the doc are the source points; their projection to the background's pixel grid gives the destination points. OpenCV's getPerspectiveTransform builds a 3×3 homography:
that maps every doc pixel to a background pixel, and warpPerspective renders the warped doc into the background's shape. Alpha blend using a content-presence mask:
where M is 1 where the warped doc has content and 0 elsewhere (so no artefact in the uncovered regions, and the alpha gives the opacity in the covered ones).
Why a homography and not a simple affine: the two WCS projections introduce a nonlinear plate-scale variation across the doc's footprint if either is far from the tangent point. The homography captures the first-order perspective distortion, which is what WCS projections look like locally over a few-degree FOV.
SASpro ships a single celestial_catalog.csv that merges Messier, NGC, IC, Abell, Sharpless 2, LBN, LDN, and PN-G objects into one table with a Catalog column identifying each row's source. The loader opens the file with a decoding ladder — latin-1 → utf-8-sig → utf-8 → cp1252 — so it works whether the file was saved from Excel on Windows, Numbers on Mac, or exported by a Python script. BOM-tolerant column lookup walks "Name", "NAME", and the UTF-8 BOM-prefixed "\ufeffName" for every expected field, so a BOM at the start of the first column doesn't lose that column.
Parsed fields per row: name, RA/Dec (degrees), magnitude (optional), Info / Diameter (optional, often contains size like “6.0x4.0” arcmin), Type / LongType (optional), Catalog (source), and a canonical catalogue code (see next section).
Users (and catalogue sources) spell catalogue codes inconsistently:
| Catalogue | Variants seen in the wild | Canonical |
|---|---|---|
| Sharpless 2 | SH2-, SH2 , SH-2, SH 2, SH_2, SHARPLESS | SH2 |
| Planetary Nebula Galactic | PNG, PN-G, PN G, PN_G | PN-G |
SASpro's _canon_catalog_code looks at both the Catalog column and the object's Name prefix and reduces every variant to the canonical form. The user's UI filter (dropdown with the clean canonical names) matches against this canonical code, so a mixed-source catalogue CSV filters correctly without hand-cleaning the data. Other catalogues (NGC, IC, M, Abell, LBN, LDN) are already consistent in their standard spelling and pass through unchanged.
For every DSO (and bright star) in the catalogue:
radius + 2° — a cheap pre-filter that avoids computing full angular separations for most objects.σ(obj, centre) and keep if:The 0.75 factor restricts labels to the inner three-quarters of the chart radius, since labels near the exact edge would clip against the chart boundary.
3. Magnitude cut:
Objects with no magnitude in the catalogue get a sentinel score of 99 so they sort to the end and are kept only if the user's label cap allows.
4. Sort by brightness and keep the top 4 × max_labels — a bit extra over the user's cap, so the declutter step has candidates to pick from in dense regions.
After brightness-sort, each candidate's chart-pixel position is reduced to a grid cell:
with c = 34 px for DSOs, c = 28 px for stars. First object in each cell wins (which is the brightest, since the list was pre-sorted). This stops two bright nearby objects from stacking their labels on top of each other — the brighter one keeps the real estate, the dimmer one gets dropped. Simple, deterministic, no optimisation loop.
The grid-cell approach is deliberately coarser than labels can get. In an open star cluster with a dozen 4th-magnitude stars inside a 100-pixel region, labels would otherwise form an unreadable pile; the grid gives one label per 28-px cell maximum, which comes out at a readable spacing on a 900–2400 px chart.
Even post-declutter, a label near the chart boundary can overflow because labels are drawn at a pixel offset from the marker and the text itself has width. SASpro's _place_label_inside:
get_window_extent(renderer).The renderer is obtained from a fig.canvas.draw() call that happens once per chart render, before any label is drawn. Running this check per-label on hundreds of labels is fast because the renderer is already warm.
When the catalogue entry has an Info field with a size string like "6.0x4.0" or "90x40", SASpro parses it as arcminute dimensions with a regex, takes the major axis as the honest diameter (since no PA information is available), converts to pixels at the chart's scale:
and draws a cyan outline circle at that radius centred on the object. The 60-factor converts arcminutes to arcseconds so it divides by s in arcsec/pixel. Circles under 4 px diameter are not drawn (would just look like a marker). The outline colour is a dark cyan (#5145ff) that's visible on bright and dark backgrounds alike.
The bright-star table is SASpro's shared BRIGHT_STARS list (~200 named stars with proper names, RA, Dec, V-magnitude). Same filter pipeline as DSOs: declination fast-cull, angular separation gate at 0.75×FOV, magnitude cut, sort by V-magnitude, cap at max_labels, declutter at 28-px cells, in-bounds label placement. Marker is a small amber dot (#ffb000) with the star name in the same amber.
Default magnitude limit 5.0: catches the eye-visible stars for naked-eye-scale charts and all the named bright stars (Vega, Deneb, Altair, Betelgeuse…). For wider-field charts with less busy backgrounds, raising to 6.0 or 7.0 catches more secondary stars.
The N/E arrows are built from the background WCS by stepping a small sky offset from the chart centre in two directions and projecting both to pixels:
step = max(0.01, min(0.15, FOV × 0.08)) degrees. Scales with the chart's FOV so the compass arrows always have a visible length regardless of zoom.step / cos(dec) in RA. The cos(dec) compensation makes it a real arc-length step on the sky at the current declination (so East looks east regardless of latitude).Why not just “up and right”: the chart's pixel axes don't always align with sky North and East — a HiPS cutout can land rotated, and your doc's axes don't match the HiPS cutout either. Deriving the compass from the actual WCS projection guarantees correctness at any orientation.
Four-tier bar length based on fetch FOV:
| FOV ≥ | Bar length |
|---|---|
| 4° (240′) | 30′ |
| 2° (120′) | 20′ |
| 1° (60′) | 10′ |
| < 1° | 5′ |
Bar length in pixels is bar_arcmin × 60 / s where s is the pixel scale derived from proj_plane_pixel_scales (median of the two axis scales, in degrees per pixel, converted to arcsec):
Drawn as a white horizontal line with end ticks and a label showing the bar's length in arcminutes plus the pixel scale in arcsec/pixel. Example: "30′ (1.69′′/px)" on a 4° DSS2 chart.
The imaging-train FOV rectangle is specified by focal length f (mm), pixel pitch p (µm), sensor dimensions N_W × N_H (pixels), and rotation φ (degrees clockwise from north). Angular half-sizes per axis:
(same for height; the pitch × N gives sensor dimension in micrometres which divided by 2f is the half-angle tangent). The four corners in East-North offset space are (±θ_W/2, ±θ_H/2), then rotated clockwise about the field centre by φ:
and converted to RA/Dec by compensating RA for the local cosine of declination (so East offset is a real arc-length step):
The minus sign on d_E is because +East is -RA in standard conventions. Each corner's RA/Dec is then projected through the background WCS to chart pixels, the four pixel points are connected and closed as a polygon, and the diagonal centre gets a text label showing width′ × height′ arcminutes.
The cos(δ) compensation matters near the celestial poles. Without it, a 30′ East offset at Dec 80° would be plotted as a 30′ shift in RA, which is actually only ~5.2′ of arc at that declination; the FOV box would appear squashed. With compensation the box lands correctly at any declination.
√2 · 1.02; the three-tier cache (HiPS raster, base raster with doc overlay, doc geometry) with per-tier invalidation; the debounced render pipeline distinguishing fetch-triggers from render-triggers; the WCS-correct doc overlay via cv2.getPerspectiveTransform on the four corner correspondences and alpha blend on the warped-content mask; the deep-sky catalogue loader with the multi-encoding fallback chain and BOM-tolerant column lookup; the catalogue code canonicalisation that collapses Sharpless-2 and PN-G variants; the grid-cell declutter algorithm; the in-bounds label placer with get_window_extent-based bbox measurement and automatic drop on failed shift; the DSO size-circle renderer parsing the Info field with the arcminute-dimension regex; the WCS-correct N/E compass construction via sky-step projection and the cos(δ)-compensated East step; the four-tier FOV-dependent scale bar; the imaging-train FOV box with atan(pitch · N / (2 · f)) per-axis angular half-sizes, CW rotation, and cos(δ)-compensated East offset conversion to RA/Dec; the live FOV readout; the four-tier WCS recovery chain with non-FITS metadata key stripping and SIP-presence CTYPE patching; and the QSettings-based persistence of every control.
alasky.cds.unistra.fr (Boch et al. 2020, Astronomical Data Analysis Software and Systems XXIX). The HiPS format itself is Fernique et al. (2015, A&A 578:A114). Survey credits: DSS2 is the Second-Generation Digitized Sky Survey (POSS-II/UKST plates digitised by STScI); Pan-STARRS DR1 is Chambers et al. (2016, arXiv:1612.05560); SDSS DR9 is Ahn et al. (2012, ApJS 203:21); DESI Legacy Imaging Surveys DR10 is Dey et al. (2019, AJ 157:168); 2MASS is Skrutskie et al. (2006, AJ 131:1163); Gaia DR3 is Gaia Collaboration (2023, A&A 674:A1); NSNS (Northern-Sky Narrowband Survey) is published by simg.de.
astropy.wcs.WCS, celestial subspace), coordinates (SkyCoord, degree units, angular separations), pixel-scale derivation (astropy.wcs.utils.proj_plane_pixel_scales), FITS I/O (astropy.io.fits), and the WCSAxes matplotlib projection used for the RA/Dec grid. astroquery's hips2fits module is used as a fallback path for CDS surveys. OpenCV (cv2) provides the perspective transform estimation and warping for the doc overlay. Matplotlib handles rendering, including the Agg backend for off-screen draw and patheffects.Stroke for the dark text outlines that make labels readable on any background. NumPy provides array operations throughout. PyQt6 provides the dialog UI, QTimer-based debouncing, and QSettings persistence.