Skip to content

feat(stitch): add patch-placement / mosaic stitching module#10

Merged
Garrett Bischof (gwbischof) merged 4 commits into
mainfrom
feat/stitch-module
Jun 5, 2026
Merged

feat(stitch): add patch-placement / mosaic stitching module#10
Garrett Bischof (gwbischof) merged 4 commits into
mainfrom
feat/stitch-module

Conversation

@gwbischof

@gwbischof Garrett Bischof (gwbischof) commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

What

Adds ptychoml/stitch.py — the generic ViT patch-placement / mosaic
stitching algorithms, lifted from holoptycho/mosaic_stitch.py so they
can be shared from the library instead of duplicated in the streaming app.

Three placement strategies (all pure numpy + scipy.fft):

Function Strategy
place_patches_fourier_shift / stitch_batch_into sub-pixel Fourier-shift placement
stitch_batch_livestitch_into nearest-integer + returns the touched (y0,y1,x0,x1) bbox for incremental display
stitch_batch_nearest plain nearest-integer scatter-add, edge-clamped

canvas/counts accumulate in place; callers normalize as
canvas / np.maximum(counts, 1). Depends only on fourier_shift from
preprocess.py.

The executable code is identical to holoptycho's (verified by diffing
each function with docstrings/comments stripped — all six match); only
docstrings/comments were expanded. So once holoptycho re-exports these
(follow-up PR), it's a zero-behavior-change refactor.

Why

First step of breaking NSLS2/holoptycho#37
into focused, reviewable PRs. Stitching is genuinely generic ptychography
code that didn't yet live in ptychoml.

⚠️ Known gotcha (pre-existing behavior, documented not changed)

The three strategies are not pixel-interchangeable: the Fourier and
livestitch paths flip each patch up-down before placement while
stitch_batch_nearest does not, and the three use different
center-rounding (a footprint can shift ~1px between them). Their counts
agree but placed values don't. Harmless in holoptycho (each method has a
distinct role — livestitch for live accumulation, nearest for JIT warm-up)
but callers must pick one strategy per mosaic. Documented in README +
AGENTS.md.

Tests

tests/test_stitch.py — 13 pure-numpy cases: single-patch placement,
overlap accumulation + counts, counts footprint, livestitch bbox,
edge-clamping (no wrap), up-down flip convention, the flip difference
between methods, Fourier/nearest counts-footprint agreement, and per-batch
vs one-shot associativity.

pixi run --environment ci-py312 test   # 141 passed, 5 skipped

Docs

  • README: new Stitching (ptychoml.stitch) section with a batch +
    streaming usage example, the non-interchangeability caveat, and updated
    pipeline-map bullet.
  • AGENTS.md: ptychoml/stitch.py codebase note — in-place accumulation,
    streaming safety, and the not-interchangeable gotcha.

Garrett Bischof (gwbischof) added a commit that referenced this pull request Jun 5, 2026
…eable

Fact-check of PR #10 surfaced a misleading test: the Fourier and nearest
paths only place identically for vertically-symmetric patches. In general
they differ — the Fourier and livestitch paths flip patches up-down while
nearest does not, and all three use different center-rounding (so a
footprint can shift ~1px). This is pre-existing holoptycho behavior,
lifted verbatim; harmless there (distinct roles) but a real gotcha for a
library.

- Split the misleading parity test into an honest counts-footprint test
  plus an explicit flip-difference test (non-symmetric patch).
- Document the non-interchangeability in README + AGENTS.md.
Move the generic ViT patch-placement algorithms out of
holoptycho/mosaic_stitch.py into a new ptychoml.stitch module so they can
be shared instead of duplicated. Three placement strategies:

- place_patches_fourier_shift / stitch_batch_into: sub-pixel Fourier-shift
- stitch_batch_livestitch_into: nearest-integer + touched bounding box
- stitch_batch_nearest: plain nearest-integer, edge-clamped

Pure numpy + scipy.fft; depends only on fourier_shift from preprocess.
Exported via __init__ __all__, covered by tests/test_stitch.py (12 cases:
placement, accumulation, counts footprint, livestitch bbox, edge clamping,
up-down flip, fourier-vs-nearest parity, per-batch associativity), and
documented in README + AGENTS.md.

First step of breaking NSLS2/holoptycho#37 into focused PRs: holoptycho
will re-export these under their original names (zero behavior change).

Co-authored-by: Himanshu Goel <4122621+himanshugoel2797@users.noreply.github.com>
All three strategies are streaming-safe (per-patch placement, no
batch-global reduction, normalization deferred to caller). Flag the two
gotchas: the nearest/livestitch paths are bit-exact across batchings while
the Fourier path is associative only up to FFT round-off, and the Fourier
path may reallocate the canvas on edge straddle so callers must use the
returned arrays.

Co-authored-by: Himanshu Goel <4122621+himanshugoel2797@users.noreply.github.com>
Document the actual call pattern (allocate canvas/counts once, loop per
batch for streaming or one call for offline), the (B, ph, pw) shape
contract (single patch -> (1, ph, pw)), and the must-use-return-value
rule. Adds a usage block to the README and module docstring.

Co-authored-by: Himanshu Goel <4122621+himanshugoel2797@users.noreply.github.com>
…eable

Fact-check of PR #10 surfaced a misleading test: the Fourier and nearest
paths only place identically for vertically-symmetric patches. In general
they differ — the Fourier and livestitch paths flip patches up-down while
nearest does not, and all three use different center-rounding (so a
footprint can shift ~1px). This is pre-existing holoptycho behavior,
lifted verbatim; harmless there (distinct roles) but a real gotcha for a
library.

- Split the misleading parity test into an honest counts-footprint test
  plus an explicit flip-difference test (non-symmetric patch).
- Document the non-interchangeability in README + AGENTS.md.

Co-authored-by: Himanshu Goel <4122621+himanshugoel2797@users.noreply.github.com>
@gwbischof Garrett Bischof (gwbischof) merged commit cfe454a into main Jun 5, 2026
6 checks passed

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 a new ptychoml.stitch module to centralize ViT patch-placement / mosaic stitching utilities (previously duplicated in holoptycho), along with library exports, documentation, and a dedicated test suite.

Changes:

  • Added ptychoml/stitch.py implementing three stitching strategies (Fourier-shift, LiveStitch-style nearest + bbox, and plain nearest).
  • Added tests/test_stitch.py covering placement, accumulation/counts, edge clamping, bbox behavior, and method convention differences (flip/rounding).
  • Updated README.md, AGENTS.md, and ptychoml/__init__.py to document and export the new stitching APIs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
ptychoml/stitch.py New patch placement / mosaic stitching implementation and API docs.
tests/test_stitch.py New unit tests for stitching behavior and conventions.
README.md New public documentation section for ptychoml.stitch with usage guidance.
ptychoml/__init__.py Re-exports new stitch functions at package top-level.
AGENTS.md Codebase notes documenting the new module’s conventions and gotchas.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ptychoml/stitch.py
Comment on lines +62 to +65
* The nearest paths mutate ``canvas``/``counts`` in place, but the
Fourier path **reallocates** the canvas when a patch straddles the edge
(it pads then crops back). Always use the **returned** arrays rather
than relying on in-place mutation.
Comment thread ptychoml/stitch.py
Comment on lines +117 to +120
ph, pw = patch_shape
sys_float = positions[:, 0] - (ph - 1.0) / 2.0
sxs_float = positions[:, 1] - (pw - 1.0) / 2.0

Comment thread ptychoml/stitch.py
Comment on lines +161 to +164
ph, pw = patches.shape[-2:]
sys, sxs, ph_eff, pw_eff, pad_lengths, fractional = _placement_indices(
image.shape, positions, (ph, pw), pad,
)
Comment thread ptychoml/stitch.py
Comment on lines +337 to +340
ph, pw = patches.shape[-2:]
ch, cw = canvas.shape
for i in range(len(patches)):
ry = int(round(positions_px[i, 0]))
Comment thread README.md
Comment on lines +160 to +166
Patch-placement helpers that accumulate a batch of reconstructed ViT
patches into a running `(canvas, counts)` mosaic. Both arrays accumulate
in place; the displayed/written mosaic is `canvas / np.maximum(counts, 1)`
(no normalization happens inside these functions — the caller picks the
min-overlap threshold). `positions_px` is `(N, 2)` in canvas pixel
coordinates `(y, x)` pointing at patch centers.

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.

2 participants