Releases: WSILabs/opentile-go
v0.60.1 — fix BIF Level.OverlapMode (OverlapStitched on stitched levels)
Fixed
- BIF
Level.OverlapModewasOverlapNoneon stitched levels. v0.60.0 added
theOverlapModefield and wired DZI/SZI to set it, but the BIF reader was not
updated — it set only the (correct)Overlappingbool, leavingOverlapModeat
its zero value. So a stitched BIF level reportedOverlapping=truebut
OverlapMode=OverlapNone, violating the documented invariant
Overlapping == (OverlapMode != OverlapNone)and mis-reporting BIF as
non-overlapping via the new enum. BIF now derives both fields from one source:
overlapping levels reportOverlapStitched, non-overlapping report
OverlapNone. TheOverlappingbool value is unchanged (it was already
correct), so consumers gating on!Overlappingare unaffected; only the
OverlapModeenum value is corrected. NewTestOverlapModeInvariant
(cross-format) and a BIF-specificOverlapStitchedassertion guard it.
v0.60.0 — DZI/SZI Overlap>0 support (+ OverlapMode API, edge-tile ReadRegion fix)
DZI/SZI Overlap > 0 support — clean composited output, new OverlapMode /
TileContentRect API, and a pre-existing bare-DZI/SZI edge-tile ReadRegion fix.
Added
- DZI/SZI
Overlap > 0support. Bare DZI (formats/dzi) and SZI (formats/szi)
pyramids whose manifest declares a tile overlap now read correctly:
ReadRegion/ReadRegionScaled/ScaledStrips/StitchedTilereturn clean,
overlap-free composited pixels, while rawTile()/DecodedTile()return the
on-disk padded tile. Newopentile.OverlapModeenum (OverlapNone/
OverlapBordered/OverlapStitched) onLevel;Level.Overlappingis now the
derived convenienceOverlapMode != OverlapNone(value unchanged for every
previously readable slide). NewLevel.TileContentRect(col,row) (Region, bool)
returns the per-tile content crop within a decoded tile.Overlap = 0reads are
byte-identical. Validated against libvips overlap_0/overlap_1 DZIs of the same
slide (region MAD ≈ JPEG noise) plus synthetic lossless PNG fixtures.
Changed
Level.Overlapping/Level.Grid/Level.TileOverlapfield docs reworded to
distinguish "padded tiles" (OverlapBordered) from "compacted grid"
(OverlapStitched); the "Grid does not tile Size" property now belongs to
OverlapMode == OverlapStitched.
Fixed
- Bare DZI/SZI
ReadRegionon right/bottom edge tiles. Bare DZI/SZI store edge
tiles unpadded at their actual clipped size (unlike TIFF's zero-padded edge
tiles); the region compositor decoded each tile into a fixedTileSizescratch,
which the strict decoder rejected for a sub-TileSizeedge tile
(dst 256x256 != decoded 176x256), soReadRegionfailed on any region touching
a right/bottom edge tile. The compositor now decodes unpadded edge tiles into a
natural-size buffer; byte-identical for padded-tile formats (TIFF/BIF). Pre-existing
since the v0.52 bare-DZI reader.
v0.59.1 — precise subtile crop origin (fixes deep-zoom cross-level drift on legacy BIF)
Precise subtile crop origin — fixes deep-zoom cross-level drift on legacy
iScan BIF's non-square tiles (#68 follow-up).
Fixed
- Deep-level cross-level registration on legacy BIF (non-square tiles). The
reduced-level subtile model (v0.56.0) computed a subtile's crop origin within
its stored tile asq·(Dim>>i), which floors the per-subtile size. A stored
tile packs2ⁱsubtiles per axis across its fullTileW/TileH, so flooring
accumulates a crop-origin error of up to(2ⁱ−1)·fracpx when the dimension
is not a multiple of2ⁱ. Legacy iScan tiles are non-square1024×1360:
1024is a power of two (exact), but1360/32 = 42.5first loses precision
at L5, drifting the vertical crop up to ~15 px at the bottom of each
stored tile — a content shift that appeared as level-to-level "drift" when
zooming out past L4. The crop origin now uses the preciseround(q·Dim/2ⁱ).
Cross-level registration measures ≤ 1 px at every level (was +6…+15 px at
L5–L6); X was always clean (power-of-two width). DP-generation BIF (square
1024×1024tiles) and the other 10 formats are byte-identical.
Notes
- No API,
Level.Size, placement,Grid, or tile-byte changes — only the
pixel content sampled at deep reduced levels of non-square-tile legacy BIF.
No fixture/geometry-pin changes. NewTestReducedDeepLevelRegistration
(local-only) +TestSubtileSourcePreciseCrop(CI-safe synthetic) gate it.
v0.59.0 — cross-axis drift in legacy iScan BIF stitching (closes #68)
Cross-axis drift in legacy iScan BIF stitching — model the per-column vertical
(and per-row horizontal) scanner-stage skew the joints record (closes #68).
Fixed
- Legacy iScan BIF per-column/per-row drift (#68). Adjacent camera frames
are captured at a small cross-axis offset — horizontally-adjacent frames at
a slight vertical shift, vertically-adjacent frames at a slight horizontal
shift — a faint scanner-stage skew the scanner records as the<TileJointInfo>
join's cross-axis overlap. The previous separable layout placed every tile
in a grid row at the same Y (and every tile in a column at the same X),
discarding that component; it accumulated into a visible per-column vertical
shear on zoom (the "slightly wonky tile placement" symptom) and a ~0.05%
height residual.buildLegacyLayoutnow integrates the full 2-D join
displacement vectors —(tw−OverlapX, −OverlapY)horizontally,
(−OverlapX, th−OverlapY)vertically — which separates into in-axis
X[col]/Y[row]plus cross-axisyCol[col]/xRow[row]baselines, placing
tile(c,r)at(X[c]+xRow[r], Y[r]+yCol[c]). The cross-axis sign and
magnitude are pixel-validated against OS-2 cross-correlation and locked by a
new seam-MAD gate (the modeled offset lowers the overlap-band MAD on all four
legacy fixtures — AC1.592 21.2→6.6, 1_19 15.8→6.8). Clean-room: derived from
the file's own joint overlaps, not a translation of openslide/bio-formats.
Notes
- Honoring the skew makes the stitched grid a faint parallelogram, so a legacy
L0's reportedLevel.Size(and the floor-halved reduced levels) is slightly
larger than openslide's nominal de-sheared extent by the integrated drift
span (OS-1 +123×+174 px on a 105936×94125 hull; OS-2 115060×76560). openslide
is the lower bound. Per-tile raw/decoded bytes andGridare unchanged. - DP-generation BIF (which uses the
<Frame>-node layout, not
buildLegacyLayout) and the other 10 formats are untouched. - All legacy fixtures (OS-1/OS-2/AC1.592/1_19/S12-18199-1A) are PHI/local-only;
the cross-axis pixel gate and dims/geometry pins skip cleanly in CI.
v0.58.0 — multi-AOI legacy iScan BIF stitching (closes #67)
Multi-AOI legacy iScan BIF stitching — place every scanned Area of Interest at
its own slide anchor (closes #67).
Fixed
- Legacy iScan BIF multi-AOI placement (#67). A legacy iScan slide may carry
several Areas of Interest (AOIs) — separate scanned tissue regions, each a
sub-grid of the global tile grid placed at its own origin (OS-2 has three
AoiOriginnodes, one unscanned).buildLegacyLayoutpreviously stitched the
whole frame grid as a single AOI, which overlaid the disjoint regions and left
a visible seam through the large AOI on zoom. Following openslide's Ventana
reader, the layout now pairsImageInfo[i]withAoiOrigin[i]by document
order, skipsAOIScanned=falseAOIs, and places each scanned AOI's local
NumCols×NumRowsgrid at its own(Pos-X, Pos-Y)anchor (Pos-Y is measured
from the AOI bottom, so it is Y-flipped into image space). Within each AOI the
existing separable per-gap-average overlap model (#63) is reused. Single-AOI
slides (OS-1: one AOI at origin 0) are the degenerate case and remain
byte-identical. OS-2 L0 now reports the union hull across scanned AOIs
(114951×76389) with the inter-AOI seam removed; reduced levels floor-halve.
Notes
- Per-tile raw/decoded bytes and
Gridare unchanged (raw frame addressing).
Only the stitched placement (Level.Size,ReadRegion/ReadRegionScaled/
ScaledStrips/StitchedTileoutput) changes, and only for multi-AOI legacy
slides. The other 10 formats and DP-generation BIF are untouched. - OS-2.bif is a PHI/local-only fixture (gitignored); its committed
OS-2.bif.jsonsnapshot and theTestBIFGeometry/TestSlideParitypins are
SHA/geometry-only and skip cleanly in CI when the slide is absent.
Caller-chosen display tile size — render square tiles from non-square BIF
Caller-chosen display tile size — render uniform/square display tiles from
formats that store non-square tiles (legacy BIF).
Added
Level.StitchedGridFor(tile Size) Size—StitchedGridfor a caller-chosen
display tile size (ceil(Size/tile)), so a viewer can render uniform/square
display tiles independent of the storedTileSize.Level.StitchedTileIntonow uses the dst's dimensions as the display tile
size on overlapping levels (stitched BIF). The composite is region-based,
so dst may be any size — a viewer can render e.g. 512×512 display tiles even
though legacy BIF stores non-square 1024×1360 tiles (which choke renderers
that assume square). The result is pixel-identical toReadRegionover
[tx*dst.W, ty*dst.H, dst.W, dst.H], and the per-source-tile decode-once frame
cache still applies. Pair it withStitchedGridFor(dstSize). Existing callers
passing aTileSize-sized dst are unchanged.
Notes
- Scoped to overlapping levels (BIF). Non-overlapping formats store square
tiles and keep the existing fast path:StitchedTileIntothere still behaves
likeDecodedTileInto(dst must equalTileSize); useReadRegionfor an
arbitrary rectangle.StitchedTile/StitchedGrid(theTileSize-default
convenience forms) are unchanged.
BIF reduced levels stitched via the openslide subtile model — DP + legacy iScan (#83, #80)
BIF reduced pyramid levels are stitched via the openslide subtile model — DP
and legacy iScan, correctly this time (#83, #80).
Fixed
- BIF reduced levels (L1+) now composite via the openslide Ventana subtile
model (#83 DP + #80 legacy). A stored reduced tile is the downsample of a
2ⁱ×2ⁱ block of L0 camera frames, with the inter-frame overlap baked inside
its pixels. The earlier whole-tile placement (v0.55.0) could only remove
overlap at tile boundaries, so it over-compacted the dense legacy case
("very broken on zoom", reverted in v0.55.1). Reading openslide's
openslide-vendor-ventana.cshowed the correct approach: decompose each
reduced tile into per-L0-frame subtiles and place each subtile at that
frame's own compacted position (L0 origin >> i), sourced from stored tile
(col>>i, row>>i)cropped to its(col%2ⁱ, row%2ⁱ)quadrant. Every frame
lands at its stitched spot, removing all overlap (internal + boundary) →
an exact 2× pyramid that matches L0 downsampled (validated: Ventana-1 L1
content MAD 3.8 vs L0÷2; OS-1 legacy L1 MAD 2.7, vs the broken whole-tile
layout's 33). DP and legacy now share one path.Size= L0 hull
floor-halved;Grid+ tile bytes unchanged. - Legacy iScan reduced-level
Size/Downsampleare stitched again (the
v0.55.1 revert to the raw frame grid is superseded): OS-1 L152909×46962
(= L0 hull ÷ 2, the openslide content extent), inter-level ratio ~2.0.
Added
subtileLayout— an internal optional extension of theregionLayout
capability:UnitSize(level)+SubtileSource(level, col, row). The shared
compositors (ReadRegion/StitchedTileand theScaledStripsiterator)
decode the mapped source tile and blit its cropped quadrant, so all three
pixel-reassembly paths handle subtiles uniformly.
Changed
- BIF reduced levels report
Level.Overlapping = truefor both DP and
legacy iScan (whenever L0 was overlap-compacted). Consumers on the #71
Overlappingcontract route reduced-level reads through the region API and
get the stitch-aligned output. Tile bytes andGridare unchanged.
Revert legacy iScan reduced-level stitching (#80) — mis-registered on zoom; DP (#83) unaffected
Revert legacy iScan reduced-level stitching (#80) — it mis-registered on zoom.
Fixed
- Reverted the legacy-iScan half of #80 (v0.55.0). Compositing legacy reduced
pyramid levels via the downsample-L0 layout mis-registers them ("very broken
on zoom", reported on OS-1/OS-2 in openscope). Root cause: legacy frame overlap is
dense (~11% at every gap), so a reduced tile spans multiple overlapping frames
and the inter-frame overlap is baked into the downsampled tile's pixels (a
~57px duplicate band inside each 1024px L1 tile). Placement-only stitching removes
overlap at tile boundaries but cannot remove the overlap baked inside a tile,
so the composited reduced level is intrinsically stretched. Correct legacy reduced
rendering needs per-tile pixel cropping — out of scope for a placement layout, so
#80 is reopened. Legacy reduced levels revert to naive (Size= raw frame
grid,Overlapping=false); only L0 is stitched for legacy (unchanged since
v0.46).Size/Downsample/Overlappingfor legacy reduced levels return to their
v0.54.x values. - DP-BIF reduced-level stitching (#83) is unaffected. DP overlap is sparse, so
reduced DP tiles rarely contain an internal frame boundary; their downsample-L0
composite stays correct (Ventana-1 renders acceptably). Reduced DP levels keep
Overlapping=trueand the floor-halvedSize.
BIF: reduced pyramid levels stitch-aligned with L0 — DP + legacy iScan (#83, #80)
BIF reduced pyramid levels (DP and legacy iScan) are stitch-aligned with L0
(#83, #80).
Fixed
- BIF reduced levels (L1+) now composite stitch-aligned with L0 (#83 DP, #80
legacy iScan). The scanner stores reduced levels as the raw (un-compacted) frame
grid downsampled, so their pixels carried the frame overlap (DP:~overlap/2ⁱ
residual at the frame-join seams — ~60px at L1 on Ventana-1, halving each deeper
level; legacy iScan: dense ~11%). v0.53.0 corrected only DP's reported
Level.Size; the layout stayed naive (and legacySizestayed the raw frame
grid), so a region /StitchedTileread crossing the L0↔L1 boundary still showed
a content shift. Reduced levels now build their stitch layout by downsampling
the L0 compacted layout (reduced tile(col,row)inherits L0 frame
(col<<i, row<<i)'s compacted origin, scaled by1/2ⁱ) and report
Level.Overlapping = true, so the existingregionLayout/compositeStitchedLoop
path composites them with no compositor change. Legacy reducedSizenow derives
from the L0 stitched hull (floor-halved) too — matching openslide's L1 content
extent within ~0.1% (OS-1 L152909vs openslide~52907), closing the
#78-legacy deferral.Gridand tile bytes are unchanged.
Changed
- BIF reduced levels now report
Level.Overlapping = true(wasfalse) for DP
and legacy iScan slides whose L0 was overlap-compacted. Consumers that gate
per-tile fast paths onOverlapping(the documented #71 contract) automatically
route reduced-level pixel reassembly through the region API and get the
stitch-aligned output. Consumers that ignoredOverlappingand iteratedGridon
reduced levels were already mis-stitching; they should adopt the contract. - Legacy iScan reduced-level
Level.Size/Downsamplechanged to the stitched
(floor-halved-hull) content extent (e.g. OS-1 L159392×51000→52909×46962).
This is the openslide-matching value (within ~0.1%); the prior raw-frame-grid
dimensions overstated the content. Tile bytes andGridare unchanged.
IFE resolution: read the Iris scale-relative convention, no vendor override (#81)
IFE Magnification/MPP read the Iris scale-relative convention — verified against the
Iris-Codec encoder source, with no per-vendor override (#81 follow-up).
Fixed
- IFE
Metadata.Magnification/Metadata.MPPread the Iris resolution convention.
v0.54.0 mis-framed the root cause as "an encoder stamped a downsampled level's value
into the header" and special-casedaperio.AppMag/aperio.MPP. The actual Iris
convention — confirmed against the Iris-Codec encoder source
(Iris-Codec/src/IrisCodecEncoder.cpp,READ_OPENSLIDE_METADATA, which writes
micronsPerPixel = MPP_finest × max_scaleandmagnification = objective / max_scale,
front().downsample == max_scale) — is that theMETADATAheader stores
scale-relative quantities:magnificationis a coefficient andmicronsPerPixel
is anchored at the lowest-resolution layer. opentile-go inverts that:
Magnification = magnification × max_scaleandMPP = micronsPerPixel / max_scale
(max_scale= the finest layer's scale). The raw header values stay in
MagnificationFromHeader/MPPFromHeader.
Removed
- The
aperio.*override is gone. v0.54.0/first-cut-v0.54.1 special-cased the
source scanner'saperio.AppMag/aperio.MPP(orImageDescriptionbanner) to
"correct" a header that disagreed with the convention. That existed solely to prop up
one fixture (cervix_2x_jpeg.iris) whose header is stale — computed for a 4-layer
max_scale = 64pyramid but the file ships a 9-layermax_scale = 256ladder, so the
convention at its realmax_scale = 256would give an impossible160×. The
convention is the spec; a disagreeing header is a bug in the file, so the override
(aperioL0Resolution+ helpers) was removed and the fixture's header is corrected at
the source instead (see Added).aperio.*values remain available via
Properties["iris.aperio.*"].
Added
cmd/ifefixheader— a small utility that rewrites an.irisMETADATA header's
scale-relative resolution fields to be conformant with the file's own pyramid. It
readsmax_scalefromLAYER_EXTENTSand writes
magnification = appmag / max_scale,micronsPerPixel = mpp × max_scalefrom the
supplied true L0 objective / MPP. Used to fix the publiccervix_2x_jpeg.iris
fixture's stale header:go run ./cmd/ifefixheader -appmag 40 -mpp 0.262968 ….
Documentation
docs/formats/ife.md— documents the reversed Iris layer numbering, the
scale-relative header semantics, the× / ÷ max_scalederivation (cited to the
encoder source), the no-override stance +cmd/ifefixheaderworkflow, and corrects an
earlier "cervix is a 2× downsample of a 253,952 × 177,152 original" note (the embedded
Aperio metadata reports native126940×88416@0.262968= the finest layer). The
public425248_JPEG/AVIF.irisreferences carry no resolution metadata at all. The
localsample_files/ife/ife-format-spec-for-opentile-go.mdreference gains a matching
note.