Skip to content

Releases: WSILabs/opentile-go

v0.60.1 — fix BIF Level.OverlapMode (OverlapStitched on stitched levels)

27 Jun 15:48
4eae6bc

Choose a tag to compare

Fixed

  • BIF Level.OverlapMode was OverlapNone on stitched levels. v0.60.0 added
    the OverlapMode field and wired DZI/SZI to set it, but the BIF reader was not
    updated — it set only the (correct) Overlapping bool, leaving OverlapMode at
    its zero value. So a stitched BIF level reported Overlapping=true but
    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 report OverlapStitched, non-overlapping report
    OverlapNone. The Overlapping bool value is unchanged (it was already
    correct), so consumers gating on !Overlapping are unaffected; only the
    OverlapMode enum value is corrected. New TestOverlapModeInvariant
    (cross-format) and a BIF-specific OverlapStitched assertion guard it.

v0.60.0 — DZI/SZI Overlap>0 support (+ OverlapMode API, edge-tile ReadRegion fix)

27 Jun 02:57
d9d90e7

Choose a tag to compare

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 > 0 support. Bare DZI (formats/dzi) and SZI (formats/szi)
    pyramids whose manifest declares a tile overlap now read correctly:
    ReadRegion / ReadRegionScaled / ScaledStrips / StitchedTile return clean,
    overlap-free composited pixels, while raw Tile() / DecodedTile() return the
    on-disk padded tile. New opentile.OverlapMode enum (OverlapNone /
    OverlapBordered / OverlapStitched) on Level; Level.Overlapping is now the
    derived convenience OverlapMode != OverlapNone (value unchanged for every
    previously readable slide). New Level.TileContentRect(col,row) (Region, bool)
    returns the per-tile content crop within a decoded tile. Overlap = 0 reads 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.TileOverlap field 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 ReadRegion on 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 fixed TileSize scratch,
    which the strict decoder rejected for a sub-TileSize edge tile
    (dst 256x256 != decoded 176x256), so ReadRegion failed 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)

26 Jun 17:19
9043658

Choose a tag to compare

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 as q·(Dim>>i), which floors the per-subtile size. A stored
    tile packs 2ⁱ subtiles per axis across its full TileW/TileH, so flooring
    accumulates a crop-origin error of up to (2ⁱ−1)·frac px when the dimension
    is not a multiple of 2ⁱ. Legacy iScan tiles are non-square 1024×1360:
    1024 is a power of two (exact), but 1360/32 = 42.5 first 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 precise round(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×1024 tiles) 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. New TestReducedDeepLevelRegistration
    (local-only) + TestSubtileSourcePreciseCrop (CI-safe synthetic) gate it.

v0.59.0 — cross-axis drift in legacy iScan BIF stitching (closes #68)

25 Jun 19:26
f9149f0

Choose a tag to compare

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. buildLegacyLayout now 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-axis yCol[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 reported Level.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 and Grid are 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)

25 Jun 18:09
d5e123e

Choose a tag to compare

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
    AoiOrigin nodes, one unscanned). buildLegacyLayout previously 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 pairs ImageInfo[i] with AoiOrigin[i] by document
    order, skips AOIScanned=false AOIs, and places each scanned AOI's local
    NumCols×NumRows grid 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 Grid are unchanged (raw frame addressing).
    Only the stitched placement (Level.Size, ReadRegion/ReadRegionScaled/
    ScaledStrips/StitchedTile output) 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.json snapshot and the TestBIFGeometry/TestSlideParity pins 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

25 Jun 16:28
8675fdb

Choose a tag to compare

Caller-chosen display tile size — render uniform/square display tiles from
formats that store non-square tiles (legacy BIF).

Added

  • Level.StitchedGridFor(tile Size) SizeStitchedGrid for a caller-chosen
    display tile size (ceil(Size/tile)), so a viewer can render uniform/square
    display tiles independent of the stored TileSize.
  • Level.StitchedTileInto now 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 to ReadRegion over
    [tx*dst.W, ty*dst.H, dst.W, dst.H], and the per-source-tile decode-once frame
    cache still applies. Pair it with StitchedGridFor(dstSize). Existing callers
    passing a TileSize-sized dst are unchanged.

Notes

  • Scoped to overlapping levels (BIF). Non-overlapping formats store square
    tiles and keep the existing fast path: StitchedTileInto there still behaves
    like DecodedTileInto (dst must equal TileSize); use ReadRegion for an
    arbitrary rectangle. StitchedTile / StitchedGrid (the TileSize-default
    convenience forms) are unchanged.

BIF reduced levels stitched via the openslide subtile model — DP + legacy iScan (#83, #80)

25 Jun 15:15
82eee50

Choose a tag to compare

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.c showed 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 / Downsample are stitched again (the
    v0.55.1 revert to the raw frame grid is superseded): OS-1 L1 52909×46962
    (= L0 hull ÷ 2, the openslide content extent), inter-level ratio ~2.0.

Added

  • subtileLayout — an internal optional extension of the regionLayout
    capability: UnitSize(level) + SubtileSource(level, col, row). The shared
    compositors (ReadRegion / StitchedTile and the ScaledStrips iterator)
    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 = true for both DP and
    legacy iScan (whenever L0 was overlap-compacted). Consumers on the #71
    Overlapping contract route reduced-level reads through the region API and
    get the stitch-aligned output. Tile bytes and Grid are unchanged.

Revert legacy iScan reduced-level stitching (#80) — mis-registered on zoom; DP (#83) unaffected

25 Jun 03:36
c0be656

Choose a tag to compare

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/Overlapping for 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=true and the floor-halved Size.

BIF: reduced pyramid levels stitch-aligned with L0 — DP + legacy iScan (#83, #80)

25 Jun 03:02
9f4a4ea

Choose a tag to compare

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 legacy Size stayed the raw frame
    grid), so a region / StitchedTile read 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 by 1/2ⁱ) and report
    Level.Overlapping = true, so the existing regionLayout / compositeStitchedLoop
    path composites them with no compositor change. Legacy reduced Size now derives
    from the L0 stitched hull (floor-halved) too — matching openslide's L1 content
    extent within ~0.1% (OS-1 L1 52909 vs openslide ~52907), closing the
    #78-legacy deferral. Grid and tile bytes are unchanged.

Changed

  • BIF reduced levels now report Level.Overlapping = true (was false) for DP
    and legacy iScan slides whose L0 was overlap-compacted. Consumers that gate
    per-tile fast paths on Overlapping (the documented #71 contract) automatically
    route reduced-level pixel reassembly through the region API and get the
    stitch-aligned output. Consumers that ignored Overlapping and iterated Grid on
    reduced levels were already mis-stitching; they should adopt the contract.
  • Legacy iScan reduced-level Level.Size / Downsample changed to the stitched
    (floor-halved-hull) content extent (e.g. OS-1 L1 59392×5100052909×46962).
    This is the openslide-matching value (within ~0.1%); the prior raw-frame-grid
    dimensions overstated the content. Tile bytes and Grid are unchanged.

IFE resolution: read the Iris scale-relative convention, no vendor override (#81)

25 Jun 03:02
a9092d3

Choose a tag to compare

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.MPP read 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-cased aperio.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_scale and magnification = objective / max_scale,
    front().downsample == max_scale) — is that the METADATA header stores
    scale-relative quantities: magnification is a coefficient and micronsPerPixel
    is anchored at the lowest-resolution layer. opentile-go inverts that:
    Magnification = magnification × max_scale and MPP = 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's aperio.AppMag / aperio.MPP (or ImageDescription banner) 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 = 64 pyramid but the file ships a 9-layer max_scale = 256 ladder, so the
    convention at its real max_scale = 256 would give an impossible 160×. 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 .iris METADATA header's
    scale-relative resolution fields to be conformant with the file's own pyramid. It
    reads max_scale from LAYER_EXTENTS and writes
    magnification = appmag / max_scale, micronsPerPixel = mpp × max_scale from the
    supplied true L0 objective / MPP. Used to fix the public cervix_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_scale derivation (cited to the
    encoder source), the no-override stance + cmd/ifefixheader workflow, and corrects an
    earlier "cervix is a 2× downsample of a 253,952 × 177,152 original" note (the embedded
    Aperio metadata reports native 126940×88416 @ 0.262968 = the finest layer). The
    public 425248_JPEG/AVIF.iris references carry no resolution metadata at all. The
    local sample_files/ife/ife-format-spec-for-opentile-go.md reference gains a matching
    note.