Repository navigation
Register the pyramid levels of slideio formats (CZI, VSI) - #166
Merged
maubreville merged 1 commit intoOct 27, 2026
Merged
Conversation
Levels of slideio images were displaced against each other by up to ~0.65 px, which shows as wobbling while the viewer blends zoom levels: - slideio reports 1 / scale (16.0007 for a 16:1 level) but the levels are exact decimation grids; reads at positions that are not multiples of the downsample are displaced by up to a level pixel. SlideIOSlide now reports the exact integer downsample, snaps positions to it and requests size * downsample. - the scale from a slide level to a Deep Zoom level is derived from the downsample (level 0 size / downsample) instead of the stored level size. - CZI pyramids store levels that are displaced against each other (0.2-0.8 px). util.registration measures the displacement once per image (each level against the level below, three textured spots, parabolic refinement, chained to level 0, a level is corrected only if two spots agree) and get_tile applies it as an offset of the fractional sampling box. Level 0 is never shifted. The offsets are stored in the new table images_pyramidregistration (with the file size/mtime and the algorithm version they were measured on). The table is shared with the Rust port of the server, which creates it with the same DDL; the migration uses CREATE TABLE IF NOT EXISTS so either side can create it first. Fixes #165 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Collaborator
Author
|
tested. works |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #165. Stacked on #164 (it changes the same function): the base of this PR is that branch, merge #164 first.
What
Three fixes for slideio-based formats (CZI, VSI, DICOM), all aimed at the displacement between neighbouring zoom levels (see the issue for measurements):
util/slideio.py):level_downsamplesreports the exact integer downsample when the level is within 1 % of one (slideio reports 16.0007 for an exact 16:1 level);read_regionsnaps the position to it and requests the region assize * downsample. slideio snaps a region to its level grid, so unaligned positions were displaced by up to a level pixel.zDeepZoomGenerator.get_tile): the scale from slide level to Deep Zoom level is derived from the downsample (level 0 size / downsamplepixels of extent) instead of the stored size of the level.util/registration.py): some CZI pyramids store levels displaced by 0.2-0.8 px against each other. The displacement is measured once per image and applied as an offset of the fractional sampling box inget_tile:get_tileignores offsets for slide level 0, and a test checks that level-0 tiles are bit-identical even with an absurd offset;needs_registrationis only set onSlideIOSlide) are not measured; VSI measured zero offsets.Database
New model
images.PyramidRegistration(tableimages_pyramidregistration):image(OneToOne),offsets(JSON,[dx, dy]per level in pixels of that level),algorithm,file_size,file_mtime,updated. A row is only used if size and mtime of the file and theALGORITHMversion match; otherwise the image is measured again, so replacing a file or improving the algorithm needs no manual step. Measuring takes 1.6-5 s once per image (first tile/DZI request), afterwards the offsets come from the database.Migration
0041_pyramidregistrationcreates the table withCREATE TABLE IF NOT EXISTSinsideSeparateDatabaseAndState. The table is shared with the Rust port of the server (exact-rust), which creates the same table with the same DDL; whichever side runs first creates it, the other is a no-op, and the columns are identical either way (checked in both orders byscripts/check-schema-sync.shin exact-rust, which builds two databases from a backup and compares the result column by column).makemigrations --checkreports no changes.Deployment:
python manage.py migrate.Measured
Real server, three slides (two CZI, one VSI), two regions each, 7 level pairs per region; average displacement between neighbouring levels: CZI A 0.355 → 0.063 px, CZI B 0.336 → 0.095 px, VSI 0.258 → 0.041 px (max over all pairs 0.65 → 0.18 px), compared with #164 alone. Tiles of the other formats (NDPI, MRXS, TIFF, OME-TIFF, MP4, NIfTI, MKT) still match the previous output.
Tests
util/test_registration.py(no database): the estimator on synthetic pyramids with a known displacement (recovers (-0.6, +0.4) within 0.15 px), a registered pyramid gets no offsets, a single level has nothing to register; and the generator: offsets move level > 0 but leave level-0 tiles bit-identical.exact/images/test_pyramid_registration.py(DjangoTestCase): measured once then read from the database; measured again when mtime or algorithm version change; registered formats are never measured or stored.exact.imagestests: no new failures (ImageRegistrationTestCasealready fails insetUpwith the installed django-passkeys: "request is required for passkeys.backend.PasskeyModelBackend").Notes
slide.level_offsetsis set once per cached slide (slide_for_image, used by the tile and Deep Zoom views andupdate_image_cache); thumbnails andslide_informationdo not need it.depth > 1) are not measured (offsets are per image, files differ per z).🤖 Generated with Claude Code