Skip to content

Add WCS-driven orientation so thumbnails are consistent across the network - #14

Merged
sfoale merged 8 commits into
masterfrom
feature/north-is-up
Sep 1, 2026
Merged

Add WCS-driven orientation so thumbnails are consistent across the network#14
sfoale merged 8 commits into
masterfrom
feature/north-is-up

Conversation

@sfoale

@sfoale sfoale commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

fits_to_img, fits_to_jpg, fits_to_tif, fits_to_zoom_slice_jpg and
get_scaled_image take orient='wcs', which puts north up and east left from the
frame's CD matrix, snapped to the nearest 90 degrees. A frame with no usable WCS
falls back to the fixed vertical flip.

The default for now is orient='legacy', so nothing moves until a caller opts in. Output
under the default is identical to 0.4.11.

Tested with a range of real images.

Two things to note:

  • A quarter turn swaps the width and height of a non-square frame, so its thumbnail
    comes out in the other aspect.
  • A colour stack obeys orient='wcs' only when all of its frames resolve to the same
    transform, since the channels are combined pixel for pixel. This should always be true anyway.

Also incliuded:

  • The header of a multi-extension frame inherits the keywords its data HDU lacks from
    the primary HDU. Sinistro carries its WCS and SATURATE only on the primary, so
    quick_scale_image and multi_fits_to_img now see a SATURATE for those frames.
  • Fixes for Pillow 10 (FreeTypeFont.getsize removed, which broke every labelled
    conversion) and numpy 2 (lstsq residuals no longer convert to float implicitly).
  • First tests in the repo, and a CI matrix over Python 3.10-3.13 plus the oldest
    supported dependency set.

sfoale added 2 commits August 21, 2026 15:49
…etwork

fits_to_img, fits_to_jpg, fits_to_tif, fits_to_zoom_slice_jpg and
get_scaled_image take orient='wcs', which puts north up and east left from
the frame's CD matrix. The transform is snapped to the nearest 90 degrees,
so it is a mirror plus quarter turns and no pixel is resampled. A frame with
no usable WCS falls back to the previous fixed vertical flip.
The default stays orient='legacy', so no existing caller changes behaviour
until it opts in.

Also in this commit - needed to get the above under test on
currently deployed dependency versions:

- Fix _add_label crashing on Pillow >= 10, which removed
  FreeTypeFont.getsize.
- Fix least_squares_line_fit under numpy 2, which no longer converts a
  1-element array to a float implicitly. This raised TypeError on every
  auto-scaled conversion. A rank-deficient fit, which returns no residuals
  at all, no longer raises either.
- Widen the label error handler from IOError, which does not catch what
  modern Pillow raises, so a font problem degrades to a warning as intended
  instead of failing the conversion.
- get_reduced_dimensionality_data now fills in keywords the data HDU lacks
  from the primary header, so multi-extension frames expose their WCS and
  SATURATE.
- get_scaled_image reads the frame once rather than twice on the auto-scale
  path, now that it needs the header for orientation anyway.
- Declare python_requires >=3.10 and minimum numpy, astropy and Pillow
  versions. Floors are the oldest combination the suite passes on; there
  are deliberately no upper bounds.
- Add a test suite and CI across Python 3.10-3.13, plus a row pinning the
  oldest supported dependencies.
pixel and a frame that has lost its WCS would otherwise be stacked against
frames that were rotated. Report an unknown orient the way every other bad
argument is reported, rather than raising through fits_to_jpg.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in WCS-derived orientation mode to make generated thumbnails consistent (north up, east left) across instruments/rotations, while preserving the existing default behavior until callers switch over.

Changes:

  • Introduces orient='wcs'|'legacy' plumbing through scaling + conversion entrypoints, including color-stack fallback behavior when channels can’t share one transform.
  • Improves FITS header handling for multi-extension frames by merging missing primary-header keywords into the data HDU’s header.
  • Adds a first test suite and CI/tox configuration covering Python 3.10–3.13 and dependency floor/ceiling behavior (Pillow 10 / NumPy 2 regressions included).

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tox.ini Updates tox to run the new unittest-based test suite across modern Python versions.
tests/test_scaling.py Adds tests for header merging, scaling split, NumPy 2 lstsq behavior, and scaled-image read count/orientation.
tests/test_orientation.py Adds empirical WCS-orientation tests to validate “north up / east left” across rotations and flip combinations.
tests/test_conversions.py Adds end-to-end tests for Pillow 10 label handling and orientation propagation through public conversion APIs.
tests/helpers.py Provides FITS/WCS test fixtures and FITS writers (single-HDU and multi-HDU Sinistro-like).
tests/__init__.py Initializes the tests package.
setup.py Bumps version to 0.5.0, sets python_requires>=3.10, and adds minimum dependency floors; excludes tests from packages.
requirements.pip Removes the previous placeholder requirements file.
README Documents the 0.5.0 WCS-driven orientation behavior and multi-extension header inheritance change.
fits2image/scaling.py Adds orient support to get_scaled_image, splits auto_scale_data, and adds header-only read + header merge logic.
fits2image/orientation.py Introduces WCS/CD-matrix-driven orientation (snapped to nearest 90°) with legacy fallback behavior.
fits2image/conversions.py Threads orient through conversion APIs, adds color-stack orientation selection, and updates label sizing for Pillow 10.
.github/workflows/run-tests.yml Adds CI matrix for Python 3.10–3.13 and oldest-supported dependency pins; runs unittests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread fits2image/scaling.py
Comment thread fits2image/scaling.py
Comment thread fits2image/conversions.py Outdated
Comment thread setup.py Outdated
setup(
name="fits2image",
version="0.4.11",
version="0.5.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could be brave and call it 1.0.0

@markBowman markBowman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried hard to find something to say to show I read it, but it's too damn perfect.

@jnation3406 jnation3406 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the orient='legacy/wcs' param, but if you can explain with what applications we would want to continue using legacy rather than switch to wcs with a legacy fallback that would help me understand.

Also I noticed you did not add the wcs option to the other method of reducing images, the multi_fits_to_img / quick_scale_img. That new method is currently just used by datalab, but we may want to switch other things to using it as it's more efficient and allows greater flexibility in producing colored composites. Is there a reason not to include the wcs stuff there?

Otherwise it all looks reasonable to me

Comment thread fits2image/conversions.py Outdated
Comment thread fits2image/conversions.py Outdated
Comment thread fits2image/conversions.py Outdated
@sfoale

sfoale commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

I'm not sure about the orient='legacy/wcs' param, but if you can explain with what applications we would want to continue using legacy rather than switch to wcs with a legacy fallback that would help me understand.

Also I noticed you did not add the wcs option to the other method of reducing images, the multi_fits_to_img / quick_scale_img. That new method is currently just used by datalab, but we may want to switch other things to using it as it's more efficient and allows greater flexibility in producing colored composites. Is there a reason not to include the wcs stuff there?

Otherwise it all looks reasonable to me
Good points - addressed in the most recent version. I've abandoned the wcs / legacy choice completely. You get wcs if it exists. Also added the missed datalab paths.

Comment thread fits2image/conversions.py Outdated
Comment thread fits2image/conversions.py
Comment thread fits2image/conversions.py Outdated

@jnation3406 jnation3406 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just a few more questions to satiate my own curiosity

Comment thread fits2image/conversions.py
Comment thread fits2image/conversions.py
Comment thread fits2image/conversions.py
Comment thread fits2image/orientation.py Outdated
@sfoale
sfoale merged commit 6137089 into master Sep 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants