patchcraft 0.5.6__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {patchcraft-0.5.6 → patchcraft-0.6.0}/CHANGELOG.md +88 -1
- {patchcraft-0.5.6 → patchcraft-0.6.0}/CITATION.cff +2 -2
- {patchcraft-0.5.6 → patchcraft-0.6.0}/CONTRIBUTING.md +5 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/PKG-INFO +4 -4
- {patchcraft-0.5.6 → patchcraft-0.6.0}/README.md +2 -2
- {patchcraft-0.5.6 → patchcraft-0.6.0}/README.pt-BR.md +2 -2
- {patchcraft-0.5.6 → patchcraft-0.6.0}/README.pypi.md +3 -3
- {patchcraft-0.5.6 → patchcraft-0.6.0}/STATUS.md +22 -5
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/ADR/0004-precision-and-effort-parameters.md +14 -7
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/GUIDE.md +30 -24
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/SCOPE.md +5 -6
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/THEORY.md +13 -9
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/USAGE.md +35 -3
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/_version.py +3 -3
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/extract.py +25 -46
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/reconstruct.py +74 -7
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/resize.py +35 -3
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/stitch.py +20 -3
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft.egg-info/PKG-INFO +4 -4
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft.egg-info/SOURCES.txt +1 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft.egg-info/scm_file_list.json +1 -0
- patchcraft-0.6.0/src/patchcraft.egg-info/scm_version.json +8 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/_rng.py +4 -1
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_extract.py +15 -11
- patchcraft-0.6.0/tests/test_integer.py +191 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_public_api.py +1 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_reconstruct.py +5 -5
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_resize.py +129 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_stitch.py +30 -10
- patchcraft-0.5.6/src/patchcraft.egg-info/scm_version.json +0 -8
- {patchcraft-0.5.6 → patchcraft-0.6.0}/.gitignore +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/.pre-commit-config.yaml +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/.python-version +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/AGENTS.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/CODE_OF_CONDUCT.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/LICENSE +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/MANIFEST.in +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/MAP.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/SECURITY.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/accel/Cargo.lock +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/accel/Cargo.toml +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/accel/README.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/accel/src/kernel.rs +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/accel/src/lib.rs +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/ADR/0001-patch-extraction-api.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/ADR/0002-patchify-transform.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/ADR/0003-reversibility-classes.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/ADR/0005-border-policy.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/AUXILIARY.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/FOCO-1.0.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/PERFORMANCE.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/ROADMAP.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/STUDIES/2026-08-04-patch-techniques.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/STUDIES/2026-09-03-precision-measurements.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/STUDIES/2026-09-03-strata-conformity.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/STUDIES/2026-10-10-border-policy.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/design/2026-09-01-fase1-bugs-performance.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/design/2026-09-01-fase2-acelerador.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/docs/design/2026-09-01-fase3-predicado.md +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/pyproject.toml +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/setup.cfg +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/setup.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/__init__.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/_accel.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/_foldgeom.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/cache.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/geometry.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/metrics.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/pair.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft/py.typed +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft.egg-info/dependency_links.txt +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft.egg-info/requires.txt +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/src/patchcraft.egg-info/top_level.txt +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/__init__.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/_datasets.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/conftest.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_accel.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_cache.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_citation.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_datasets_helper.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_docs_usage.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_exactness.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_geometry.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_import.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_metrics.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_operator.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_pair.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_reference.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_rng.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tests/test_vmap.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tools/benchmark.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tools/check_dist.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tools/compare_border_strategies.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tools/compare_exactness_rules.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/tools/make_outreach_figures.py +0 -0
- {patchcraft-0.5.6 → patchcraft-0.6.0}/uv.lock +0 -0
|
@@ -6,6 +6,91 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-10-11
|
|
10
|
+
|
|
11
|
+
A new `0.y` because `resize` gains a parameter, `antialias`, whose default
|
|
12
|
+
changes no call. With it comes a faster `stitch` under the uniform window,
|
|
13
|
+
bit-identical to the code it replaces. No returned value of an existing call
|
|
14
|
+
changes. The full suite passes on both code paths at the declared floor and
|
|
15
|
+
at the newest releases, and the 126,736-geometry sweep passes on both paths.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`stitch` with the uniform window hands an overlapping grid to
|
|
20
|
+
`reconstruct`.** Every weight is 1, so the weighted fold is the plain fold
|
|
21
|
+
in the same order and the separable denominator is the count map, which is
|
|
22
|
+
`reconstruct`'s arithmetic without the multiply by ones and the two window
|
|
23
|
+
folds. Compared against the previous code through integer views: 996
|
|
24
|
+
geometries of the sampled space, four float dtypes, signed zeros,
|
|
25
|
+
infinities, NaN payloads and subnormals, outputs and gradients, both code
|
|
26
|
+
paths, 0 differences. Against the previous code, interleaved and taking the
|
|
27
|
+
median of 9 to 41 runs: 1.45 and 1.75 times faster on the pure path at
|
|
28
|
+
3x512x512 and 3x1024x1024 and 1.06 at 3x2048x2048, where the fold dominates,
|
|
29
|
+
and 1.31, 1.17 and 1.10 times with the accelerator. A non-overlapping grid
|
|
30
|
+
takes the same path as before. The test that compared the two with
|
|
31
|
+
`allclose` now compares bits, counts of 3 included.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`resize(..., antialias=None)`**, keyword-only, with the name, values and
|
|
36
|
+
meaning of torchvision v2's `resize` and the default ADR 0004 recorded. With
|
|
37
|
+
`True` the torch backend filters before a bilinear or bicubic downsample, as
|
|
38
|
+
PIL always does. On the tests' noise image reduced by 4 it lands within
|
|
39
|
+
1/255 of the PIL backend, where the unfiltered path is more than 20/255
|
|
40
|
+
away, and it passes a third of the noise's standard deviation. For float
|
|
41
|
+
tensors the output equals torchvision v2's `resize(antialias=True)` bit for
|
|
42
|
+
bit; half precision is filtered in `float32` and cast back, as torchvision
|
|
43
|
+
does, because torch has no antialiased half-precision kernel. Integer
|
|
44
|
+
tensors are rounded and clamped as on every other path, where torchvision
|
|
45
|
+
rounds `uint8` in its own fixed-point kernel, so the two can differ there.
|
|
46
|
+
`None` and `False` leave the call to `F.interpolate` exactly as it was, so
|
|
47
|
+
no existing call changes, and the flag is ignored where it means nothing,
|
|
48
|
+
the other torch modes and the PIL backend, as torchvision ignores it. The
|
|
49
|
+
filtered path uses PIL's bicubic kernel (`a = -0.5`) where the plain one
|
|
50
|
+
uses torch's (`a = -0.75`), so it changes bicubic enlargement too, and the
|
|
51
|
+
docs say it is for reducing. With the flag, resizing to the same size
|
|
52
|
+
returns the input bit for bit. Removing the flag from either call site, or
|
|
53
|
+
the half-precision promotion, fails the new tests.
|
|
54
|
+
|
|
55
|
+
## [0.5.7] - 2026-10-10
|
|
56
|
+
|
|
57
|
+
Integer images, end to end. `extract` takes every dtype at every dilation,
|
|
58
|
+
and `reconstruct` takes the five integer dtypes and returns them exactly in
|
|
59
|
+
every legal geometry, since integer sums do not round. Every float result is
|
|
60
|
+
unchanged: `extract`'s new single path is bit-identical to the one it
|
|
61
|
+
replaces. The full suite passes on both code paths at the declared floor and
|
|
62
|
+
at the newest releases, and the 126,736-geometry sweep, now with an integer
|
|
63
|
+
round trip, passes on both paths.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **Integer images through `extract` and `reconstruct`, exact in every legal
|
|
68
|
+
geometry.** These are steps 1 and 2 of ADR 0004, shipped together as the ADR
|
|
69
|
+
requires. `extract` takes every dtype at every dilation, because it is now
|
|
70
|
+
one strided window view and one copy for all of them: bit-identical to the
|
|
71
|
+
`F.unfold` path it replaced for floats, and 3.9 times faster at dilation 2
|
|
72
|
+
on 3x1024x1024. `reconstruct` takes `uint8`, `int8`, `int16`, `int32` and
|
|
73
|
+
`int64` and returns each pixel's exact mean rounded half to even, the
|
|
74
|
+
rounding `resize` already applies to integers. For patches from `extract`
|
|
75
|
+
that mean is the pixel itself in every legal geometry, with no count-map
|
|
76
|
+
rule, since integer sums do not round. Nothing overflows: up to `int32` the
|
|
77
|
+
sum is taken in `int64`, and `int64` values are split by their pixel's count
|
|
78
|
+
into a quotient and a remainder before they are added. `bool` and complex
|
|
79
|
+
patches are refused with a `ValueError`, `stitch` stays float-only, and the
|
|
80
|
+
accelerator leaves integers to torch. `tests/test_integer.py` checks the
|
|
81
|
+
round trip over the sampled geometry space, all 126,736 geometries under
|
|
82
|
+
`PATCHCRAFT_SWEEP_FULL=1`, for `uint8` and `int64`, and the mean of
|
|
83
|
+
disagreeing patches against Python integers in every dtype; deliberately
|
|
84
|
+
breaking the rounding or the accumulator fails 63 and 90 of its cases.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- THEORY §1 and SCOPE §2.1 described `extract` as built on `F.unfold`, which
|
|
89
|
+
had been true only at dilation above 1 since 0.3.0 and is not true at all
|
|
90
|
+
now. Both describe the strided view and the copy. THEORY §9.1 and §9.2,
|
|
91
|
+
GUIDE §3, USAGE §5, the three READMEs and ADR 0004 state the integer
|
|
92
|
+
contract.
|
|
93
|
+
|
|
9
94
|
## [0.5.6] - 2026-10-10
|
|
10
95
|
|
|
11
96
|
A fix release. `torch.vmap` works again over `extract`, `reconstruct` and
|
|
@@ -1168,7 +1253,9 @@ First public release. Public API stable; signatures will only change in 1.x.
|
|
|
1168
1253
|
- [`README.md`](README.md) covers installation, the car-vs-track metaphor,
|
|
1169
1254
|
validation lab.
|
|
1170
1255
|
|
|
1171
|
-
[Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.
|
|
1256
|
+
[Unreleased]: https://github.com/LeoPR/PatchCraft/compare/v0.6.0...HEAD
|
|
1257
|
+
[0.6.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.6.0
|
|
1258
|
+
[0.5.7]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.7
|
|
1172
1259
|
[0.5.6]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.6
|
|
1173
1260
|
[0.5.5]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.5
|
|
1174
1261
|
[0.5.4]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.5.4
|
|
@@ -6,8 +6,8 @@ authors:
|
|
|
6
6
|
- given-names: Leonardo
|
|
7
7
|
family-names: Marques de Souza
|
|
8
8
|
email: leonardo.marques.souza@gmail.com
|
|
9
|
-
version: "0.
|
|
10
|
-
date-released: "2026-10-
|
|
9
|
+
version: "0.6.0"
|
|
10
|
+
date-released: "2026-10-11"
|
|
11
11
|
repository-code: "https://github.com/LeoPR/PatchCraft"
|
|
12
12
|
license: MIT
|
|
13
13
|
abstract: >
|
|
@@ -275,6 +275,11 @@ moved, or whether the implementation stopped violating it**:
|
|
|
275
275
|
| Documentation, tests, CI, packaging or tooling only | `0.y.Z` | Still a release if it is published, still no break |
|
|
276
276
|
| Nothing published | nothing | Accumulate under `[Unreleased]` |
|
|
277
277
|
|
|
278
|
+
A new keyword-only parameter with a default breaks no call, but it moves a
|
|
279
|
+
signature, so it takes the first row and not the third: 0.6.0 is the example,
|
|
280
|
+
where `resize` gained `antialias`. The third row is for new behaviour behind
|
|
281
|
+
an unchanged signature, such as 0.5.7 accepting integer images.
|
|
282
|
+
|
|
278
283
|
The project's own history sorts cleanly under that rule, and does not sort
|
|
279
284
|
under any rule keyed on whether values changed:
|
|
280
285
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchcraft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Image patch extraction, reconstruction, pairing and seam-aware stitching for super-resolution and dataset pipelines.
|
|
5
5
|
Author: Leonardo Marques de Souza
|
|
6
6
|
License-Expression: MIT
|
|
@@ -111,7 +111,7 @@ blended = stitch(edited, image.shape, stride=16, weight="hann")
|
|
|
111
111
|
assert blended.shape == image.shape # seams smoothed, geometry preserved
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
An 8-bit image goes in as it is. `extract` copies, so its patches hold the image's own bytes, and `reconstruct` returns them exactly in every legal geometry, since `k` copies of `v` sum to `k·v` and divide back to `v`; patches that disagree come back as their exact mean rounded half to even. `stitch` stays float-only, because its windows are float weights.
|
|
115
115
|
|
|
116
116
|
## reconstruct or stitch
|
|
117
117
|
|
|
@@ -162,9 +162,9 @@ It helps when you tile one image for an inference pass too large to run in a sin
|
|
|
162
162
|
|
|
163
163
|
## When the round trip is bit for bit
|
|
164
164
|
|
|
165
|
-
The round trip is exact when every value in the count map is a power of two. Reconstruction adds up the patches covering each pixel and divides by how many there were, and in a legal geometry a map of powers of two holds only the counts 1, 2 and 4, where both steps are exact: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4.
|
|
165
|
+
The round trip of a float image is exact when every value in the count map is a power of two. Reconstruction adds up the patches covering each pixel and divides by how many there were, and in a legal geometry a map of powers of two holds only the counts 1, 2 and 4, where both steps are exact: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4.
|
|
166
166
|
|
|
167
|
-
That makes the geometry the deciding axis rather than the dtype, so `float64` is not a safe harbour: outside the rule the per-pixel error is bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count. A wider float buys a smaller miss and never exactness.
|
|
167
|
+
That makes the geometry the deciding axis rather than the dtype, so `float64` is not a safe harbour: outside the rule the per-pixel error is bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count. A wider float buys a smaller miss and never exactness. An integer image comes back exact in every legal geometry, because integer sums do not round.
|
|
168
168
|
|
|
169
169
|
The everyday shorthand is that `stride == patch_size` and `stride == patch_size / 2` always satisfy the rule. Both are sufficient conditions rather than necessary ones, so a geometry outside them can still be exact, and the [guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) carries the sweep that measures it.
|
|
170
170
|
|
|
@@ -83,7 +83,7 @@ The two functions answer two different questions, so choosing between them is th
|
|
|
83
83
|
|
|
84
84
|
| You want | Call | Because |
|
|
85
85
|
|---|---|---|
|
|
86
|
-
| The patches back as an image, untouched | `reconstruct` | Exact inverse of `extract
|
|
86
|
+
| The patches back as an image, untouched | `reconstruct` | Exact inverse of `extract`: always for an integer image, and for a float image when every overlap count is a power of two, which is always true at `stride == patch_size` |
|
|
87
87
|
| A model's output back as an image | `stitch` | Overlapping patches disagree, and the window hides the grid |
|
|
88
88
|
|
|
89
89
|
## Why not unfold and fold directly
|
|
@@ -140,7 +140,7 @@ PatchCraft does not help when you want a batched op over N images, a dataset or
|
|
|
140
140
|
|
|
141
141
|
## When the round trip is bit for bit
|
|
142
142
|
|
|
143
|
-
The round trip is exact when every value in the count map is a power of two, which in a legal geometry means every count is 1, 2 or 4: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4. Outside that rule the per-pixel error is bounded by (k+1)·eps·|v|, with k the pixel's coverage count. A wider float buys a smaller miss and never exactness, so float64 is not a safe harbour
|
|
143
|
+
The round trip of a float image is exact when every value in the count map is a power of two, which in a legal geometry means every count is 1, 2 or 4: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4. Outside that rule the per-pixel error is bounded by (k+1)·eps·|v|, with k the pixel's coverage count. A wider float buys a smaller miss and never exactness, so float64 is not a safe harbour: among floats the axis that decides the answer is the geometry, and [the guide](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) has the sweep that measures it. An integer image comes back exact in every legal geometry, because integer sums do not round.
|
|
144
144
|
|
|
145
145
|
## Status
|
|
146
146
|
|
|
@@ -83,7 +83,7 @@ O `stitch` é para patches que um modelo reescreveu. Nesse caso os patches vizin
|
|
|
83
83
|
|
|
84
84
|
| Você quer | Chame | Porque |
|
|
85
85
|
|---|---|---|
|
|
86
|
-
| Os patches de volta como imagem, intocados | `reconstruct` | Inverso exato do `extract
|
|
86
|
+
| Os patches de volta como imagem, intocados | `reconstruct` | Inverso exato do `extract`: sempre para uma imagem inteira, e para uma imagem float quando toda contagem de sobreposição é potência de dois, o que sempre vale com `stride == patch_size` |
|
|
87
87
|
| A saída de um modelo de volta como imagem | `stitch` | Patches sobrepostos discordam, e a janela esconde a grade |
|
|
88
88
|
|
|
89
89
|
## Por que não usar `unfold` e `fold` direto
|
|
@@ -140,7 +140,7 @@ O PatchCraft não ajuda quando você quer uma operação em lote sobre N imagens
|
|
|
140
140
|
|
|
141
141
|
## Quando o round-trip é bit a bit
|
|
142
142
|
|
|
143
|
-
O round-trip é exato quando todo valor do mapa de cobertura é potência de dois, o que numa geometria legal quer dizer que toda contagem é 1, 2 ou 4: somar duas ou quatro cópias de um valor nunca arredonda, e dividir por 2 ou por 4 também não. Fora dessa regra, o erro por pixel é limitado por (k+1)·eps·|v|, com k a contagem de cobertura do pixel. Um float mais largo compra um erro menor e nunca a exatidão, então o float64 não é um porto seguro
|
|
143
|
+
O round-trip de uma imagem float é exato quando todo valor do mapa de cobertura é potência de dois, o que numa geometria legal quer dizer que toda contagem é 1, 2 ou 4: somar duas ou quatro cópias de um valor nunca arredonda, e dividir por 2 ou por 4 também não. Fora dessa regra, o erro por pixel é limitado por (k+1)·eps·|v|, com k a contagem de cobertura do pixel. Um float mais largo compra um erro menor e nunca a exatidão, então o float64 não é um porto seguro: entre os floats, quem decide a resposta é a geometria, e [o manual](docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) traz a varredura que mede isso. Uma imagem inteira volta exata em toda geometria legal, porque soma de inteiros não arredonda.
|
|
144
144
|
|
|
145
145
|
## Estado
|
|
146
146
|
|
|
@@ -69,7 +69,7 @@ blended = stitch(edited, image.shape, stride=16, weight="hann")
|
|
|
69
69
|
assert blended.shape == image.shape # seams smoothed, geometry preserved
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
An 8-bit image goes in as it is. `extract` copies, so its patches hold the image's own bytes, and `reconstruct` returns them exactly in every legal geometry, since `k` copies of `v` sum to `k·v` and divide back to `v`; patches that disagree come back as their exact mean rounded half to even. `stitch` stays float-only, because its windows are float weights.
|
|
73
73
|
|
|
74
74
|
## reconstruct or stitch
|
|
75
75
|
|
|
@@ -120,9 +120,9 @@ It helps when you tile one image for an inference pass too large to run in a sin
|
|
|
120
120
|
|
|
121
121
|
## When the round trip is bit for bit
|
|
122
122
|
|
|
123
|
-
The round trip is exact when every value in the count map is a power of two. Reconstruction adds up the patches covering each pixel and divides by how many there were, and in a legal geometry a map of powers of two holds only the counts 1, 2 and 4, where both steps are exact: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4.
|
|
123
|
+
The round trip of a float image is exact when every value in the count map is a power of two. Reconstruction adds up the patches covering each pixel and divides by how many there were, and in a legal geometry a map of powers of two holds only the counts 1, 2 and 4, where both steps are exact: adding two or four copies of a value never rounds, and neither does dividing by 2 or 4.
|
|
124
124
|
|
|
125
|
-
That makes the geometry the deciding axis rather than the dtype, so `float64` is not a safe harbour: outside the rule the per-pixel error is bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count. A wider float buys a smaller miss and never exactness.
|
|
125
|
+
That makes the geometry the deciding axis rather than the dtype, so `float64` is not a safe harbour: outside the rule the per-pixel error is bounded by `(k+1)·eps·|v|`, with `k` the pixel's coverage count. A wider float buys a smaller miss and never exactness. An integer image comes back exact in every legal geometry, because integer sums do not round.
|
|
126
126
|
|
|
127
127
|
The everyday shorthand is that `stride == patch_size` and `stride == patch_size / 2` always satisfy the rule. Both are sufficient conditions rather than necessary ones, so a geometry outside them can still be exact, and the [guide](https://github.com/LeoPR/PatchCraft/blob/main/docs/GUIDE.md#4-when-the-round-trip-is-bit-for-bit) carries the sweep that measures it.
|
|
128
128
|
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
## Where the project is
|
|
8
8
|
|
|
9
|
-
**0.
|
|
9
|
+
**0.6.0 is on PyPI**, published through Trusted Publishing on a tag push: one
|
|
10
10
|
sdist, five `cp312-abi3` platform wheels carrying the Rust accelerator, and one
|
|
11
11
|
universal `py3-none-any` wheel. There is no extra to enable and no second
|
|
12
12
|
package.
|
|
13
13
|
|
|
14
|
-
**
|
|
14
|
+
**2024 tests pass** under `pytest -m "not gpu"`, with 32 skipped and 5
|
|
15
15
|
deselected, plus two full-sweep gates behind `PATCHCRAFT_SWEEP_FULL=1`
|
|
16
16
|
that enumerate all 126,736 legal geometries. CI is green on
|
|
17
17
|
{Ubuntu, Windows} x {3.12, 3.13, 3.14} on the pure path, plus a two-OS job that
|
|
@@ -24,7 +24,21 @@ pillow 10.1, Rust 1.82) that runs it on both paths.
|
|
|
24
24
|
|
|
25
25
|
## What is in flight
|
|
26
26
|
|
|
27
|
-
**Nothing unreleased.** 0.
|
|
27
|
+
**Nothing unreleased.** 0.6.0 added `resize(..., antialias=None)`,
|
|
28
|
+
keyword-only with the default that changes no call, and made `stitch` with
|
|
29
|
+
the uniform window hand an overlapping grid to `reconstruct`, bit-identical and
|
|
30
|
+
1.45 to 1.75 times faster on the pure path at 512 and 1024. The new parameter
|
|
31
|
+
is why it is a new `0.y`, by the README's rule and the first row of
|
|
32
|
+
CONTRIBUTING's table.
|
|
33
|
+
|
|
34
|
+
0.5.7 brought integer images: `extract` takes every
|
|
35
|
+
dtype, and `reconstruct` takes the five integer dtypes and returns them exactly
|
|
36
|
+
in every legal geometry, with disagreeing patches rounded half to even and no
|
|
37
|
+
intermediate overflowing. They are ADR 0004's steps 1 and 2, shipped together.
|
|
38
|
+
The full suite and the 126,736-geometry sweep, which now includes an integer
|
|
39
|
+
round trip, pass on both paths.
|
|
40
|
+
|
|
41
|
+
0.5.6, the release before it, was a fix release: `torch.vmap` over `extract`,
|
|
28
42
|
`reconstruct` and `stitch`, which the documentation offers as a way to batch,
|
|
29
43
|
failed on most calls since 0.5.0 and works again, pinned by
|
|
30
44
|
`tests/test_vmap.py`; and `tilings` returns `[]` for an image smaller than the
|
|
@@ -56,8 +70,11 @@ power-of-two ones, and a `float64` accumulator on all 76. The three-letter
|
|
|
56
70
|
vocabulary its first draft proposed is deferred until a transform needs it.
|
|
57
71
|
|
|
58
72
|
**ADR 0004 is `Proposed`, and asks nothing.** It records the precision and
|
|
59
|
-
effort parameters as a design that is not built; the status reflects
|
|
60
|
-
|
|
73
|
+
effort parameters as a design that is mostly not built; the status reflects
|
|
74
|
+
that `accum_dtype`, `rounding_mode` and the preset are not implemented, not
|
|
75
|
+
that a decision is pending. What it records that did ship: its steps 1 and 2,
|
|
76
|
+
integers through `extract` and `reconstruct` without a knob, in 0.5.7, and its
|
|
77
|
+
third parameter, `resize(..., antialias=None)`, in 0.6.0. The one question it
|
|
61
78
|
appeared to hold, whether to add the knobs before the 1.0 freeze or after,
|
|
62
79
|
rested on the premise that adding one afterwards is expensive. It is not: each
|
|
63
80
|
is a keyword-only argument with a default, so adding one is additive and is a
|
|
@@ -39,11 +39,15 @@ a keyword-only argument with a default is additive, so adding one after 1.0 cost
|
|
|
39
39
|
release and nothing else. With the premise checked and gone, YAGNI, the absence of any
|
|
40
40
|
external consumer, and Strata §9's default verdict all point the same way.
|
|
41
41
|
|
|
42
|
-
**So this ADR asks nothing.** It records a design that is not built, the names precedent
|
|
42
|
+
**So this ADR asks nothing.** It records a design that is mostly not built, the names precedent
|
|
43
43
|
determines, the constraint that would make a preset safe, and the order. It stays `Proposed`
|
|
44
|
-
because
|
|
44
|
+
because two of its three parameters, `accum_dtype` and `rounding_mode`, and the preset are not
|
|
45
|
+
implemented, not because a decision is pending.
|
|
45
46
|
|
|
46
|
-
**
|
|
47
|
+
**What is implemented is the integer path without a knob, and `antialias`:** steps 1 and 2
|
|
48
|
+
below shipped together on 2026-10-10, ahead of the 1.0 the table assigned them to, because the
|
|
49
|
+
owner moved them forward, and `antialias` followed the same day, with the name, values and
|
|
50
|
+
default of section 1. `accum_dtype` and `rounding_mode` do not exist.
|
|
47
51
|
|
|
48
52
|
## 1. The names, determined by precedent
|
|
49
53
|
|
|
@@ -146,8 +150,8 @@ this heading, was posed on top of a table that had already answered it.
|
|
|
146
150
|
|---|---|---|---|
|
|
147
151
|
| 0. Record this ADR, and qualify the four copies of "widening the dtype does not help" | 0.5 to 1 day | no | before 1.0, and it is the only item that must be |
|
|
148
152
|
| A. Fix three defects as bugs, no knob | done, 2026-09-03 | no signature change | done before 1.0, as planned |
|
|
149
|
-
| 1. `extract` accepts the five integer dtypes, by adding them to the fast-path set | ~1 day | no |
|
|
150
|
-
| 2. Exact integer `reconstruct`, unrestricted, `int32` accumulator, every legal geometry | 4 to 6 days | no signature change |
|
|
153
|
+
| 1. `extract` accepts the five integer dtypes, by adding them to the fast-path set | ~1 day | no | **done, 2026-10-10**, with step 2: every dtype now takes one strided-view path, at every dilation |
|
|
154
|
+
| 2. Exact integer `reconstruct`, unrestricted, `int32` accumulator, every legal geometry | 4 to 6 days | no signature change | **done, 2026-10-10**, with step 1: an `int64` sum up to `int32` and a quotient-and-remainder split for `int64`, so nothing overflows; disagreeing patches round half to even, as `resize` does |
|
|
151
155
|
| 3. Byte-native `uint8` quotient-and-remainder path | 2 to 3 days | no | do not build yet: 1.7x over an `int32` accumulator, at the cost of a second, different predicate on the doc surface |
|
|
152
156
|
| 4. `accum_dtype=` on `reconstruct` and `stitch` | 1 day, 2 alongside step 2 | yes, signature | after 1.0 |
|
|
153
157
|
| 5. `rounding_mode=` on `stitch` | 1 day | yes, signature | after 1.0, and only if a real caller appears |
|
|
@@ -176,8 +180,9 @@ into a `precise` tier would sell the absence of a bug as a quality level. **Step
|
|
|
176
180
|
2026-09-03**, and the three were the `stitch` denominator floor, the `reconstruct`
|
|
177
181
|
single-patch aliasing and the `resize` inbound clamp on a dtype that cannot hold the bound.
|
|
178
182
|
The remaining items named in the audit, the `bfloat16` justification in THEORY 9.2 and the
|
|
179
|
-
resize antialias gap,
|
|
180
|
-
|
|
183
|
+
resize antialias gap, were not defects of the same kind: the first was a documentation
|
|
184
|
+
correction, closed in 0.5.6, and the second a user-facing choice, which `antialias=None` as the
|
|
185
|
+
default made additive when it landed on 2026-10-10. Document, at zero
|
|
181
186
|
cost, the rounding policy already in force. Freeze 1.0 at 20 names with no new parameters.
|
|
182
187
|
Then 1.1 brings steps 1 and 2 together and unrestricted. Then stop and wait for a user:
|
|
183
188
|
only if one appears do the knobs follow, and the preset last of all.
|
|
@@ -206,5 +211,7 @@ only if one appears do the knobs follow, and the preset last of all.
|
|
|
206
211
|
once: 0.2.1 added the `reconstruct` integer guard, turning a raw `NotImplementedError`
|
|
207
212
|
into a framed `ValueError`.
|
|
208
213
|
4. Integers with `dilation > 1`: a framed `ValueError`, or torch's raw error in that corner?
|
|
214
|
+
*Answered by the implementation, 2026-10-10:* neither. `extract` no longer goes through
|
|
215
|
+
`F.unfold`, so a dilated integer extraction is the same strided copy as any other.
|
|
209
216
|
5. CUDA. Everything here was measured on CPU, and the accelerator refuses non-CPU tensors
|
|
210
217
|
outright. No accept text may say "integers work" without a device qualifier.
|
|
@@ -328,38 +328,44 @@ There are three ways out, depending on what you need. Decide once for the whole
|
|
|
328
328
|
|
|
329
329
|
## 3. Dtypes and files
|
|
330
330
|
|
|
331
|
-
###
|
|
331
|
+
### Integer images
|
|
332
332
|
|
|
333
|
-
`extract`
|
|
333
|
+
`extract` copies, so an 8-bit image goes in as it is and its patches are the image's own bytes. `reconstruct` takes them back in every legal geometry, and for integers there is no count-map rule: `k` copies of `v` sum to `k·v` exactly, and `k·v` divided by `k` is `v`. The example uses patch 3 at stride 1, whose counts of 3, 6 and 9 make the float round trip inexact ([section 4](#4-when-the-round-trip-is-bit-for-bit)).
|
|
334
334
|
|
|
335
335
|
```python
|
|
336
336
|
import torch
|
|
337
|
-
from patchcraft import extract, reconstruct
|
|
337
|
+
from patchcraft import extract, reconstruct, stitch
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
torch.manual_seed(0)
|
|
340
|
+
eight_bit = torch.randint(0, 256, (1, 64, 64), dtype=torch.uint8) # what read_image or PIL gives you
|
|
341
|
+
patches = extract(eight_bit, patch_size=3, stride=1)
|
|
342
|
+
back = reconstruct(patches, eight_bit.shape, stride=1)
|
|
343
|
+
print(tuple(patches.shape), patches.dtype, "| round trip exact:", torch.equal(back, eight_bit))
|
|
344
344
|
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
as_float = eight_bit.float() / 255
|
|
346
|
+
back_float = reconstruct(extract(as_float, patch_size=3, stride=1), as_float.shape, stride=1)
|
|
347
|
+
print("the same geometry in float32, exact:", torch.equal(back_float, as_float))
|
|
348
|
+
|
|
349
|
+
# Two 1x2 patches that disagree on the middle pixel of a 1x3 strip.
|
|
350
|
+
two = torch.tensor([[[[10, 11]]], [[[12, 20]]]], dtype=torch.uint8)
|
|
351
|
+
print("11 and 12 on the middle pixel:", reconstruct(two, (1, 1, 3), stride=1)[0, 0].tolist())
|
|
347
352
|
|
|
348
353
|
try:
|
|
349
|
-
|
|
350
|
-
except
|
|
351
|
-
print("
|
|
354
|
+
stitch(patches, eight_bit.shape, stride=1, weight="hann")
|
|
355
|
+
except ValueError as error:
|
|
356
|
+
print("stitch:", str(error).split(". ")[0])
|
|
352
357
|
```
|
|
353
358
|
|
|
354
359
|
```
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
360
|
+
(3844, 1, 3, 3) torch.uint8 | round trip exact: True
|
|
361
|
+
the same geometry in float32, exact: False
|
|
362
|
+
11 and 12 on the middle pixel: [10, 12, 20]
|
|
363
|
+
stitch: stitch requires floating-point patches, got dtype=torch.uint8
|
|
358
364
|
```
|
|
359
365
|
|
|
360
|
-
|
|
366
|
+
Patches that disagree, which is what a model returns, come back as the exact mean rounded half to even, the rounding `resize` already applies to integers: 11 and 12 give 12, and so would 12 and 13. Nothing overflows on the way, `int64` included, because up to `int32` the sum lives in `int64`, and `int64` values are divided by their pixel's count before they are added.
|
|
361
367
|
|
|
362
|
-
|
|
368
|
+
`stitch` stays float-only, since its windows are float weights and a weighted mean of integers would have to round somewhere you should choose. `reconstruct` refuses `bool`, because a mean of booleans is a vote. The accepted dtypes are contract text in [THEORY.md](THEORY.md) §9.1 and §9.2, measured on CPU.
|
|
363
369
|
|
|
364
370
|
### End to end from a file
|
|
365
371
|
|
|
@@ -662,7 +668,7 @@ The reason is visible in the uniform column, which does not move at all. Uniform
|
|
|
662
668
|
### The three windows, and what each costs
|
|
663
669
|
|
|
664
670
|
- **`"uniform"`** is the default. Every covering patch contributes equally, which is exactly `reconstruct`'s arithmetic, and it puts the whole disagreement on the grid lines.
|
|
665
|
-
- **`"hann"`** is the strong seam suppressor. On an overlapping grid
|
|
671
|
+
- **`"hann"`** is the strong seam suppressor. On an overlapping grid hann and gaussian cost the same, since the fold dominates, while uniform costs what `reconstruct` costs, because it is `reconstruct`'s arithmetic; where `stride == patch_size` uniform skips the fold altogether. It is the interior of a longer symmetric Hann window, `hann_window(n + 2, periodic=False)[1:-1]`, so it is strictly positive on every sample and never zeroes a pixel. A plain symmetric window is exactly zero at both endpoints and would zero every pixel covered only by patch edges; [THEORY.md](THEORY.md) §2.5 measures it.
|
|
666
672
|
- **`"gaussian"`** keeps far more weight at the patch edge than hann does, so it suppresses seams less at the larger patch sizes in the sweep, and it wins at the smallest one. THEORY §2.5 states the tradeoff as weaker seam suppression than Hann in exchange for a flatter window.
|
|
667
673
|
|
|
668
674
|
**Hann costs fidelity, and the cost is already in the numbers above.** Measured against the model's own patches, uniform keeps 29.60 dB mean and hann keeps 27.15 dB. Hann is trading exactness for smoothness on purpose, so if what you want back is the model's output rather than a pleasant image, uniform is the honest choice.
|
|
@@ -908,7 +914,7 @@ The table below is the one place that records what each call allocates, which is
|
|
|
908
914
|
| `extract` | `image`, `patch_size`, `stride`, `dilation` | `(L, C, ph, pw)` | the whole patch stack, `L * C * ph * pw` values |
|
|
909
915
|
| `Patchify` | the same geometry, at construction | a callable returning `(L, C, ph, pw)` | nothing beyond the geometry, no cache and no buffer |
|
|
910
916
|
| `reconstruct` | `patches`, `image_shape`, `stride`, `dilation` | `(C, H, W)` | one image plus one count map |
|
|
911
|
-
| `stitch` | the same, plus keyword-only `weight=`, default `"uniform"` | `(C, H, W)` | one image, one weight map, one `(ph, pw)` kernel, and on the pure-torch path a full weighted copy of the patch stack, which is the largest of
|
|
917
|
+
| `stitch` | the same, plus keyword-only `weight=`, default `"uniform"` | `(C, H, W)` | under `"uniform"`, what `reconstruct` allocates; under `"hann"` and `"gaussian"`, one image, one weight map, one `(ph, pw)` kernel, and on the pure-torch path a full weighted copy of the patch stack, which is the largest of these and which the accelerated path skips |
|
|
912
918
|
| `WeightKind` | nothing, it is a `Literal` type alias | `"uniform"`, `"hann"` or `"gaussian"` today; the set is **open**, so a new member may arrive in a `0.y` / minor release and an exhaustive `match` on it should carry a fallback | nothing |
|
|
913
919
|
| `pair` | `lr_image`, `hr_image`, `lr_patch_size`, `scale_factor`, `stride` | `PatchPair` | both patch stacks plus `L` metadata records |
|
|
914
920
|
| `PatchPair`, `PatchMeta` | frozen dataclasses | patches and grid coordinates | metadata stays on CPU, always |
|
|
@@ -918,7 +924,7 @@ The table below is the one place that records what each call allocates, which is
|
|
|
918
924
|
| `paired_tilings` | LR and HR shapes | `list[PairedTilingSpec]` | one spec per legal pair, no pixels |
|
|
919
925
|
| `patch_metrics` | two same-shape tensors | `{mae, mse, max_abs, psnr_db}` | accumulates in float64, returns Python floats |
|
|
920
926
|
| `per_patch_mse`, `per_patch_psnr` | two `(L, C, h, w)` stacks | `(L,)` in float64 | one value per patch |
|
|
921
|
-
| `resize` | one tensor or `PIL.Image`, `target_size`, `backend` | the type it received | one resized image |
|
|
927
|
+
| `resize` | one tensor or `PIL.Image`, `target_size`, `backend`, `antialias` | the type it received | one resized image |
|
|
922
928
|
| `Cache` | `root`, `namespace`, `version` | a content-addressed store | files on disk, zstd payloads when installed |
|
|
923
929
|
| `accel_available` | nothing | `bool`, true when this install has the native extension | nothing beyond a cached import probe on first call |
|
|
924
930
|
|
|
@@ -957,9 +963,9 @@ The LR and HR pairing symbols, which are `pair`, `paired_tilings` and `scale_fac
|
|
|
957
963
|
|
|
958
964
|
## 8. Where this applies today
|
|
959
965
|
|
|
960
|
-
**Before 1.0, and with no external consumer yet.** A real project consuming the published API is the gate [ROADMAP.md](ROADMAP.md) sets for calling the shape settled. Until then a new `0.y` may still change what comes out
|
|
966
|
+
**Before 1.0, and with no external consumer yet.** A real project consuming the published API is the gate [ROADMAP.md](ROADMAP.md) sets for calling the shape settled. Until then a new `0.y` may still change what comes out, so pin the `0.y` you tested against with `~=0.y.0` to take its fixes only.
|
|
961
967
|
|
|
962
|
-
What is verified is this. The full local run of `pytest -m "not gpu"` passes
|
|
968
|
+
What is verified is this. The full local run of `pytest -m "not gpu"` passes 2024 tests, skips 32 cases, and deselects 5 GPU tests, in under a minute on this machine, so run `pytest` yourself for the number in your environment. Of those skips, 30 are geometries that do not cover exactly and 2 are the full 126,736-geometry sweep, which is a local gate you arm with `PATCHCRAFT_SWEEP_FULL=1`. CI runs the same suite plus `ruff check` and `mypy --strict` on Ubuntu and Windows against Python 3.12, 3.13 and 3.14, and all six cells are green. Those six are forced onto the pure-torch path, so a separate two-cell job on Ubuntu and Windows builds the Rust kernel and runs the whole suite through it. Those eight run the newest torch, and one more job runs the whole suite on both paths at the declared floor: Python 3.12, torch 2.6, numpy 1.26 and pillow 10.1, with the kernel built by Rust 1.82. Releases reach PyPI through Trusted Publishing on a tag push. The package is typed and it ships `py.typed`.
|
|
963
969
|
|
|
964
970
|
Where the evidence is thinner, this is what it means for you.
|
|
965
971
|
|
|
@@ -1012,7 +1018,7 @@ Installing from the sdist compiles the accelerator when a Rust toolchain is pres
|
|
|
1012
1018
|
author = {Marques de Souza, Leonardo},
|
|
1013
1019
|
title = {PatchCraft: image patch extraction, reconstruction, pairing
|
|
1014
1020
|
and seam-aware stitching},
|
|
1015
|
-
version = {0.
|
|
1021
|
+
version = {0.6.0}, % check CHANGELOG.md for the release you used
|
|
1016
1022
|
year = {2026},
|
|
1017
1023
|
url = {https://github.com/LeoPR/PatchCraft}
|
|
1018
1024
|
}
|
|
@@ -57,13 +57,12 @@ inside the primitive.
|
|
|
57
57
|
|
|
58
58
|
### 2.1 Inside `extract`: torch already does it
|
|
59
59
|
|
|
60
|
-
`extract(image, ...)` reduces to
|
|
61
|
-
|
|
60
|
+
`extract(image, ...)` reduces to one strided window view of the image and
|
|
61
|
+
one copy. There is no Python loop over patches. The view costs nothing, and
|
|
62
|
+
the copy is a single tensor operation over the whole `(C, H, W)` image:
|
|
62
63
|
|
|
63
|
-
- **CPU**:
|
|
64
|
-
|
|
65
|
-
- **CUDA**: a single `im2col` kernel launch. Patches are produced in
|
|
66
|
-
parallel across thousands of CUDA threads.
|
|
64
|
+
- **CPU**: one vectorized copy kernel, which torch runs on its own thread pool.
|
|
65
|
+
- **CUDA**: one copy kernel launch, parallel across the device.
|
|
67
66
|
|
|
68
67
|
You get the parallelism for free; PatchCraft does not need to and does
|
|
69
68
|
not try to.
|
|
@@ -28,9 +28,9 @@ Multi-image batching is **out of scope**, not under-specified: callers use a Pyt
|
|
|
28
28
|
|
|
29
29
|
**Boundary conditions.** Truncation, never padding. Pixels in the trailing rows/columns that do not fit a full patch are dropped. PatchCraft does not synthesize data; callers who need full coverage should pad the image themselves before extracting.
|
|
30
30
|
|
|
31
|
-
**Memory layout.** `F.unfold(x.unsqueeze(0), (ph,pw), dilation=d, stride=(sh,sw))` yields shape `(1, C·ph·pw, L)
|
|
31
|
+
**Memory layout.** `F.unfold(x.unsqueeze(0), (ph,pw), dilation=d, stride=(sh,sw))` yields shape `(1, C·ph·pw, L)`, and the layout to get from there to `(L, C, ph, pw)` is `.view(C, ph, pw, L).permute(3, 0, 1, 2).contiguous()`; the intuitive `.view(L, C, ph, pw)` keeps the shape and scrambles the pixels. `extract` does not go through `F.unfold`: it takes the strided window view `x.unfold(1, eh, sh).unfold(2, ew, sw)` over the dilated footprint, keeps every `d`-th pixel of it, and copies once into `(L, C, ph, pw)`. Device and dtype are preserved from the input.
|
|
32
32
|
|
|
33
|
-
**Design decision.** `patchcraft.extract(image, patch_size, stride, dilation=1) -> Tensor[L, C, ph, pw]` is a pure function built on
|
|
33
|
+
**Design decision.** `patchcraft.extract(image, patch_size, stride, dilation=1) -> Tensor[L, C, ph, pw]` is a pure function built on a strided window view of the image and one copy, which has a kernel for every dtype. `image` must be `(C, H, W)`; batching is explicit (call in a loop or `vmap`) and is not part of the v0.1 signature. `patch_size` and `stride` accept `int` (square) or `(int, int)`. Truncation is the only boundary policy. When no patch fits, we return an empty tensor `Tensor[0, C, ph, pw]` rather than raising, so callers decide. No caching inside the function; caching is a separate concern (see §4). Dilation is supported here even though reconstruction rejects it (see §2). See also [ADR 0001](ADR/0001-patch-extraction-api.md) for the API rationale.
|
|
34
34
|
|
|
35
35
|
**Composability with torch transforms (Patchify).** ADR 0002 adds a thin callable companion: `patchcraft.Patchify(patch_size, stride, dilation=1)` is a class with `__call__(image) -> Tensor[L, C, ph, pw]` that delegates to `extract`. It exists to slot into `torchvision.transforms.Compose([..., Patchify(4, 2), ...])` without forcing callers to write a lambda, because lambdas are not repr-friendly and they skip eager validation. `Patchify` validates the geometry in `__init__` (so a bad `patch_size` fails when the pipeline is built, not when the first batch arrives), and carries **only** the geometry ints (`__slots__`, no `__dict__`, no cache, no buffer). The function is the contract; the class is a convenience. Same shape contract, same dtype/device preservation, same truncation policy.
|
|
36
36
|
|
|
@@ -133,7 +133,7 @@ For *unmodified* patches, every `patch_k(i_k, j_k)` equals `img(x, y)` (definiti
|
|
|
133
133
|
|
|
134
134
|
**Window kernels.**
|
|
135
135
|
|
|
136
|
-
- **`uniform`**: `w ≡ 1`. Numerator becomes the same as `reconstruct`'s fold; denominator becomes the count map.
|
|
136
|
+
- **`uniform`**: `w ≡ 1`. Numerator becomes the same as `reconstruct`'s fold; denominator becomes the count map. On an overlapping grid `stitch` hands the call to `reconstruct`, so the two agree bit for bit. On a non-overlapping one `reconstruct` only copies, while `stitch` returns what the fold would: `+0.0` for a `-0.0`, and a NaN that stays a NaN but may change its bits. Provided so the API is one function with a parameter instead of two functions with a hidden choice.
|
|
137
137
|
- **`hann`**: separable Hann (`outer(hann(ph), hann(pw))`). Center weight is 1, edge weight is small. Strong seam suppression.
|
|
138
138
|
|
|
139
139
|
The 1-D window is the **interior of a longer symmetric Hann window**: `hann_window(n + 2, periodic=False)[1:-1]`, which is strictly positive on every sample, instead of the plain symmetric window `0.5·(1 − cos(2πi/(n−1)))`, which is exactly zero at both endpoints. The zero-endpoint variant was the single largest defect found by the 0.2.0 audit (measured 2026-08-03, fixed in 0.2.1): every pixel whose only covering patches placed it on a patch edge had numerator and denominator both ≈ 0, and the `clamp(min=1e-6)` divisor sent it to zero. Measured on the defective version: at `12×12, ph=pw=4, stride=4`, **108 of 144 pixels** came back zero; at `13×13, ph=pw=4, stride=3`, **105 of 169**, including black bands in the image interior; `patch_size=2` degenerated to an all-zero window and returned an all-black image with no error.
|
|
@@ -228,7 +228,7 @@ LR and HR must agree on dtype and device; mismatch is rejected (caller normalize
|
|
|
228
228
|
- **PIL**: canonical dataset generation (reproducibility is paramount, result stays on CPU anyway).
|
|
229
229
|
- **torch**: in-training augmentation where the tensor is already on GPU and the extra filter divergence does not matter.
|
|
230
230
|
|
|
231
|
-
**Design decision.** One function: `patchcraft.resize(image, target_size, backend="pil", resample=None)`. `target_size` is a `(H, W)` tuple, with no size-spec DSL. `image` may be `PIL.Image` or `Tensor[C, H, W]`. Output type matches input (PIL in → PIL out, Tensor in → Tensor out), regardless of backend. `resample` defaults: `LANCZOS` for PIL, `bilinear` for torch (chosen because bicubic divergence is nastiest to debug; bilinear's difference from PIL's BILINEAR is small). Cross-backend conversions (PIL ↔ Tensor) go through a normalized float32 `[0, 1]` intermediate. No auto-selection: callers pick the backend explicitly and own the consequences.
|
|
231
|
+
**Design decision.** One function: `patchcraft.resize(image, target_size, backend="pil", resample=None, *, antialias=None)`. `target_size` is a `(H, W)` tuple, with no size-spec DSL. `image` may be `PIL.Image` or `Tensor[C, H, W]`. Output type matches input (PIL in → PIL out, Tensor in → Tensor out), regardless of backend. `resample` defaults: `LANCZOS` for PIL, `bilinear` for torch (chosen because bicubic divergence is nastiest to debug; bilinear's difference from PIL's BILINEAR is small). Cross-backend conversions (PIL ↔ Tensor) go through a normalized float32 `[0, 1]` intermediate. No auto-selection: callers pick the backend explicitly and own the consequences.
|
|
232
232
|
|
|
233
233
|
## 6. Quantization (optional)
|
|
234
234
|
|
|
@@ -260,9 +260,9 @@ Esta seção consolida, por API, as condições que v0.1 deve **aceitar**, **rej
|
|
|
260
260
|
`Patchify(...)(image)` is the callable form and delegates to `extract` with the same contract, plus eager geometry validation at `__init__`.
|
|
261
261
|
|
|
262
262
|
**Aceita:**
|
|
263
|
-
- `image` é `Tensor` 3D `(C, H, W)` com `C ≥ 1` arbitrário
|
|
263
|
+
- `image` é `Tensor` 3D `(C, H, W)` com `C ≥ 1` arbitrário, de **qualquer dtype**: os float, os cinco inteiros (`uint8`, `int8`, `int16`, `int32`, `int64`), `bool` e os complexos, em qualquer `dilation`. CPU ou CUDA (preservados na saída).
|
|
264
264
|
|
|
265
|
-
|
|
265
|
+
`extract` só copia: cada patch é uma janela com stride sobre a imagem, materializada por uma cópia, então os valores saem bit a bit iguais aos da imagem em qualquer dtype. Até 0.5.6 os inteiros passavam pelo `F.unfold`, que não tem kernel inteiro em CPU, e falhavam com o erro do próprio torch; a view com stride que o substituiu tem kernel para todo dtype. Medido em CPU; nenhum caminho inteiro executou em GPU.
|
|
266
266
|
- `patch_size`, `stride`, `dilation` como `int` (quadrado) ou `(int, int)` com valores positivos.
|
|
267
267
|
- Patch maior que imagem → retorna `Tensor[0, C, ph, pw]` (não levanta).
|
|
268
268
|
- `stride > patch_size` (grid esparso com lacunas, válido para features que não fazem round-trip; ver §9.2 para a contrapartida em `reconstruct`).
|
|
@@ -286,7 +286,8 @@ Esta seção consolida, por API, as condições que v0.1 deve **aceitar**, **rej
|
|
|
286
286
|
**Aceita:**
|
|
287
287
|
- `sh ≤ ph` e `sw ≤ pw` (cobertura total ou overlap).
|
|
288
288
|
- `image_shape` igual a `(C, H, W)` consistente com a geometria implícita pelo `patches.shape[0]`.
|
|
289
|
-
- Dtypes float (`float16/float32/float64/bfloat16`)
|
|
289
|
+
- Dtypes float (`float16/float32/float64/bfloat16`) e os cinco inteiros (`uint8`, `int8`, `int16`, `int32`, `int64`).
|
|
290
|
+
- Sobre os inteiros: o resultado é, em cada pixel, a média exata dos valores que os patches dão a ele, arredondada para o par mais próximo, o mesmo arredondamento que `resize` aplica a inteiros (§9.4). Para patches vindos do `extract` todos os valores de um pixel são o mesmo `v`, a soma é `k·v` e o resultado é `v`: o round trip é exato **em toda geometria legal**, sem a regra do mapa de cobertura, que é uma regra de ponto flutuante. Nenhum intermediário estoura: até `int32` a soma é feita em `int64`, e em `int64` cada valor é dividido pela contagem do seu pixel antes da soma, em quociente e resto, de modo que a soma dos quocientes não passa do maior valor de entrada e a dos restos fica abaixo de `k²`. O acelerador nativo não tem caminho inteiro e deixa esses casos com o torch. Medido em CPU, contra a mesma média calculada em inteiros do Python (`tests/test_integer.py`).
|
|
290
291
|
- Sobre `float16`: o `F.fold` acumula a soma de todos os patches sobrepostos *antes* de dividir pelo count map, e o numerador estoura o máximo finito de fp16 (65504) bem antes de a divisão acontecer. Medido em 0.2.0: imagem fp16 `(1, 16, 16)` constante de valor `10000.0`, `ph=pw=3`, `stride=1`, devolveu `inf` em **144 de 256** pixels. Desde 0.2.1 a acumulação interna (fold, count map, divisão) é feita em `float32`, com cast de volta ao dtype original no retorno; `stitch` faz o mesmo (§9.9).
|
|
291
292
|
- Sobre `bfloat16`: é promovido do mesmo jeito, **mas não pela mesma razão**, e até 0.5.2 este documento dizia que sim. O `bfloat16` carrega o expoente do `float32`, então não estoura: no caso acima o numerador chegou a `9.0112e+04` contra um máximo finito de `3.390e+38`, sem nenhum `inf`. O que a promoção compra nele é exatidão, já que tem 8 bits de mantissa contra os 11 do `float16`: em 1×16×16, `ph = pw = 3`, `stride = 1`, com dado `torch.rand` gerado em `bfloat16`, acumular nativamente erra até `7.812e-03`, dois passos do `bfloat16` perto de 1, e promovendo o round trip devolve a entrada bit a bit, embora o mapa tenha contagens 3, 6 e 9: o arredondamento final para 8 bits absorve o erro do acumulador em `float32`. É o menos preciso dos dois formatos justamente por trocar mantissa por expoente.
|
|
292
293
|
|
|
@@ -296,6 +297,7 @@ Esta seção consolida, por API, as condições que v0.1 deve **aceitar**, **rej
|
|
|
296
297
|
- Grade truncada: `(num_h - 1) * sh + ph != H` ou `(num_w - 1) * sw + pw != W` (o último patch tem que terminar exatamente na borda da imagem; grades truncadas zeravam pixels silenciosamente em 0.2.0, medido `10×10` patch 4 stride 4 → 36 de 100 zerados, `13×13` patch 5 stride 5 → 69 de 169; guarda adicionada em 0.2.1).
|
|
297
298
|
- `image_shape` inconsistente com `patches.shape[0]` / `stride`.
|
|
298
299
|
- `patches.ndim != 4`.
|
|
300
|
+
- `bool` e complexos: a média de booleanos é uma votação, que o ADR 0004 reserva a uma função própria, e os complexos não têm regra de arredondamento aqui.
|
|
299
301
|
|
|
300
302
|
**Fora de escopo v0.1:**
|
|
301
303
|
- Output em PIL.
|
|
@@ -329,7 +331,7 @@ Retorna `PatchPair(lr_patches, hr_patches, metas)` (frozen dataclass com `__slot
|
|
|
329
331
|
- `dilation` customizado (fixo em 1, porque o round-trip exige).
|
|
330
332
|
- Coerção implícita LR→HR de dtype/device (caller normaliza antes).
|
|
331
333
|
|
|
332
|
-
### 9.4 `resize(image, target_size, backend="pil", resample=None)`
|
|
334
|
+
### 9.4 `resize(image, target_size, backend="pil", resample=None, *, antialias=None)`
|
|
333
335
|
|
|
334
336
|
**Aceita:**
|
|
335
337
|
- `image` é `PIL.Image` em qualquer mode suportado pelo PIL.
|
|
@@ -337,6 +339,7 @@ Retorna `PatchPair(lr_patches, hr_patches, metas)` (frozen dataclass com `__slot
|
|
|
337
339
|
- `image` é `Tensor` em CUDA **apenas se** `backend == "torch"`.
|
|
338
340
|
- `target_size = (H, W)` tupla de 2 ints positivos.
|
|
339
341
|
- `resample=None` → default por backend (LANCZOS pra PIL, bilinear pra torch).
|
|
342
|
+
- `antialias`: `None` (default), `False` ou `True`, com o nome e o sentido do `resize` do torchvision v2 (ADR 0004). Com `True`, o backend torch filtra antes de reduzir em `bilinear` e `bicubic`, como o PIL sempre faz: na imagem de ruído dos testes, reduzida 4×, o resultado fica a menos de 1/255 do backend PIL, contra mais de 20/255 sem o filtro (`tests/test_resize.py`). Meia precisão é filtrada em `float32` e convertida de volta, como no torchvision, porque o torch não tem kernel com antialias para ela. `None` e `False` deixam a chamada ao `F.interpolate` exatamente como era, então a saída padrão não muda. Nos outros modos do torch e no backend PIL o flag não tem sentido e é ignorado, como no torchvision. O caminho filtrado usa o bicúbico do PIL (`a = -0,5`) onde o comum usa o do torch (`a = -0,75`), então também muda a ampliação bicúbica; serve para reduzir.
|
|
340
343
|
- Tensor inteiro com `backend == "torch"`: interpolação em float, cast de volta com **round + clamp** ao range do dtype (bicubic ultrapassa o range de entrada legitimamente, e um cast cru wrapava: `-9.0 → 247` em uint8; corrigido em 0.2.1). Vale para **todos os cinco modos** aceitos, não só para os dois que ultrapassam: o `F.interpolate` não implementa nenhum deles para inteiro, e até 0.5.3 a promoção era condicionada a `bilinear`/`bicubic`, então 13 das 25 combinações modo × dtype levantavam um erro cru do torch (`"adaptive_avg_pool2d" not implemented for 'Int'`) contra o que esta cláusula promete. A promoção é para o **float mais estreito que representa a faixa inteira do dtype exatamente**: `float32` para `uint8`, `int8` e `int16`, cujas faixas cabem nos 24 bits de mantissa, e `float64` para `int32`, que não cabe (`2**24 + 1` voltava `2**24`, em silêncio, desde sempre no caminho bilinear). `int64` é a exceção que nenhum float cobre: acima de `2**53` o valor não é exatamente representável em lugar nenhum e não sobrevive à volta.
|
|
341
344
|
- Tensor float com `backend == "pil"`: o hop uint8 pelo PIL **clampa a `[0, 1]`**; valores fora desse intervalo não sobrevivem nesse backend (comportamento documentado, não exceção).
|
|
342
345
|
- Tensor inteiro com `backend == "pil"`: o mesmo hop uint8 **clampa a `[0, 255]`** na ida e satura ao range do dtype na volta. Os dois backends recusam-se a dar a volta (`wrap`), que é a classe de defeito da 0.2.0, mas saturam em ranges diferentes de propósito: o torch preserva 300 e −9 num int32, o PIL não. Até 0.5.1 a ida fazia `clamp(0, 255)` no dtype de entrada, e num `int8` o próprio limite era irrepresentável, então o torch levantava `RuntimeError` cru antes de limitar coisa alguma.
|
|
@@ -346,6 +349,7 @@ Retorna `PatchPair(lr_patches, hr_patches, metas)` (frozen dataclass com `__slot
|
|
|
346
349
|
- `target_size` não é 2-tupla ou contém valor ≤ 0.
|
|
347
350
|
- `backend` não em `{"pil", "torch"}`.
|
|
348
351
|
- `resample` não suportado pelo backend escolhido (mensagem amigável, não trace interno).
|
|
352
|
+
- `antialias` que não seja `bool` nem `None`.
|
|
349
353
|
|
|
350
354
|
**Fora de escopo v0.1:**
|
|
351
355
|
- Auto-seleção de backend.
|
|
@@ -457,7 +461,7 @@ The blending counterpart to `reconstruct`. Same fold geometry, same rejections;
|
|
|
457
461
|
- `patches` is a 4-D `(L, C, ph, pw)` floating-point tensor (`float16`, `bfloat16`, `float32`, `float64`).
|
|
458
462
|
- `image_shape == (C, H, W)` consistent with the patch grid implied by `patches.shape[0]`, `stride`, and `(ph, pw)`.
|
|
459
463
|
- `stride` as `int` or `(int, int)` with `1 ≤ stride ≤ patch_size` on every axis.
|
|
460
|
-
- `weight` ∈ `{"uniform", "hann", "gaussian"}`. `"uniform"`
|
|
464
|
+
- `weight` ∈ `{"uniform", "hann", "gaussian"}`. `"uniform"` returns what `reconstruct` returns, bit for bit on an overlapping grid, and on a non-overlapping one up to the sign of a zero and the bits of a NaN (§2.5).
|
|
461
465
|
- `dtype` and `device` preserved on output.
|
|
462
466
|
|
|
463
467
|
**Sinaliza (não exceção):**
|