patchcraft 0.2.0__py3-none-any.whl
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/__init__.py +39 -0
- patchcraft/cache.py +247 -0
- patchcraft/extract.py +116 -0
- patchcraft/geometry.py +290 -0
- patchcraft/metrics.py +151 -0
- patchcraft/pair.py +161 -0
- patchcraft/py.typed +0 -0
- patchcraft/reconstruct.py +118 -0
- patchcraft/resize.py +189 -0
- patchcraft/stitch.py +215 -0
- patchcraft-0.2.0.dist-info/METADATA +266 -0
- patchcraft-0.2.0.dist-info/RECORD +14 -0
- patchcraft-0.2.0.dist-info/WHEEL +4 -0
- patchcraft-0.2.0.dist-info/licenses/LICENSE +21 -0
patchcraft/stitch.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Stitch patches back into an image with configurable weighting kernels.
|
|
2
|
+
|
|
3
|
+
Where :func:`patchcraft.reconstruct` is a bit-exact inverse of ``extract``,
|
|
4
|
+
``stitch`` is intended for *modified* patches — patches that have been
|
|
5
|
+
denoised, super-resolved, or otherwise altered — where overlap seams are
|
|
6
|
+
visible if patches are averaged uniformly. Weighting by a window kernel
|
|
7
|
+
(Hann, Gaussian) emphasizes patch centers and reduces those seams.
|
|
8
|
+
|
|
9
|
+
With ``weight="uniform"``, ``stitch`` is mathematically equivalent to
|
|
10
|
+
``reconstruct`` (down to floating-point ordering).
|
|
11
|
+
|
|
12
|
+
Contract: docs/THEORY.md §2.5 and §9.9.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from typing import Literal
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
import torch.nn.functional as F # noqa: N812 (torch convention)
|
|
21
|
+
|
|
22
|
+
from patchcraft.extract import _as_pair
|
|
23
|
+
|
|
24
|
+
__all__ = ["stitch"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
WeightKind = Literal["uniform", "hann", "gaussian"]
|
|
28
|
+
_WEIGHT_KINDS: tuple[WeightKind, ...] = ("uniform", "hann", "gaussian")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _hann_1d(n: int, dtype: torch.dtype, device: torch.device) -> torch.Tensor:
|
|
32
|
+
"""Symmetric Hann window in ``[0, 1]``. ``n == 1`` is degenerate → ``[1.0]``."""
|
|
33
|
+
if n == 1:
|
|
34
|
+
return torch.ones(1, dtype=dtype, device=device)
|
|
35
|
+
i = torch.arange(n, dtype=dtype, device=device)
|
|
36
|
+
return 0.5 * (1.0 - torch.cos(2.0 * math.pi * i / (n - 1)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _gaussian_1d(n: int, dtype: torch.dtype, device: torch.device) -> torch.Tensor:
|
|
40
|
+
"""Gaussian centered at ``(n-1)/2`` with ``sigma = max(1, n/4)``."""
|
|
41
|
+
if n == 1:
|
|
42
|
+
return torch.ones(1, dtype=dtype, device=device)
|
|
43
|
+
sigma = max(1.0, n / 4.0)
|
|
44
|
+
center = (n - 1) / 2.0
|
|
45
|
+
i = torch.arange(n, dtype=dtype, device=device)
|
|
46
|
+
return torch.exp(-((i - center) ** 2) / (2.0 * sigma * sigma))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _window_kernel(
|
|
50
|
+
kind: WeightKind,
|
|
51
|
+
ph: int,
|
|
52
|
+
pw: int,
|
|
53
|
+
dtype: torch.dtype,
|
|
54
|
+
device: torch.device,
|
|
55
|
+
) -> torch.Tensor:
|
|
56
|
+
"""Build a ``(ph, pw)`` window as the outer product of two 1-D windows."""
|
|
57
|
+
if kind == "uniform":
|
|
58
|
+
return torch.ones(ph, pw, dtype=dtype, device=device)
|
|
59
|
+
if kind == "hann":
|
|
60
|
+
wh = _hann_1d(ph, dtype, device)
|
|
61
|
+
ww = _hann_1d(pw, dtype, device)
|
|
62
|
+
return wh.unsqueeze(1) * ww.unsqueeze(0)
|
|
63
|
+
if kind == "gaussian":
|
|
64
|
+
wh = _gaussian_1d(ph, dtype, device)
|
|
65
|
+
ww = _gaussian_1d(pw, dtype, device)
|
|
66
|
+
return wh.unsqueeze(1) * ww.unsqueeze(0)
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"weight must be one of {_WEIGHT_KINDS!r}, got {kind!r}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def stitch(
|
|
73
|
+
patches: torch.Tensor,
|
|
74
|
+
image_shape: tuple[int, int, int],
|
|
75
|
+
stride: int | tuple[int, int],
|
|
76
|
+
*,
|
|
77
|
+
weight: WeightKind = "uniform",
|
|
78
|
+
dilation: int | tuple[int, int] = 1,
|
|
79
|
+
) -> torch.Tensor:
|
|
80
|
+
"""Reassemble a ``(C, H, W)`` image from ``(L, C, ph, pw)`` with blendable weights.
|
|
81
|
+
|
|
82
|
+
Use ``stitch`` when patches have been modified (model output, denoised,
|
|
83
|
+
super-resolved). Use :func:`patchcraft.reconstruct` when patches came
|
|
84
|
+
straight from ``extract`` and you want a bit-exact inverse with no
|
|
85
|
+
extra arithmetic.
|
|
86
|
+
|
|
87
|
+
``weight`` controls how overlapping patches are blended:
|
|
88
|
+
|
|
89
|
+
- ``"uniform"`` — each covering patch contributes equally. Mathematically
|
|
90
|
+
equivalent to ``reconstruct`` (no seam attenuation).
|
|
91
|
+
- ``"hann"`` — Hann window: full weight at patch center, zero at patch
|
|
92
|
+
edges. Strong seam suppression. **Caveat:** image-corner pixels that
|
|
93
|
+
are covered only by patches whose edge-weight at that location is zero
|
|
94
|
+
will be zero in the output. Document this for callers.
|
|
95
|
+
- ``"gaussian"`` — Gaussian centered on the patch with
|
|
96
|
+
``sigma = max(1.0, min(ph, pw) / 4)``. Smooth seam suppression without
|
|
97
|
+
the strict zero at the edge (no corner-zero artifact).
|
|
98
|
+
|
|
99
|
+
Internally: each patch is multiplied by the 2-D weight kernel, the
|
|
100
|
+
weighted patches are folded into the numerator, the weight kernel itself
|
|
101
|
+
is folded over the same geometry into the denominator, and
|
|
102
|
+
``numerator / denominator.clamp(min=1e-6)`` gives the output. The clamp
|
|
103
|
+
absorbs float noise on covered pixels; geometry validation guarantees
|
|
104
|
+
no uncovered pixels.
|
|
105
|
+
|
|
106
|
+
Rejects (per §9.9): ``dilation != 1``; ``stride > patch_size`` in any
|
|
107
|
+
axis; ``patches.ndim != 4``; non-floating-point patches (kernel
|
|
108
|
+
multiplication breaks integer semantics for non-uniform weights —
|
|
109
|
+
callers convert to ``float`` first); ``image_shape`` inconsistent with
|
|
110
|
+
the patch grid; unknown ``weight``.
|
|
111
|
+
|
|
112
|
+
Dtype and device of ``patches`` are preserved.
|
|
113
|
+
"""
|
|
114
|
+
if not isinstance(patches, torch.Tensor):
|
|
115
|
+
raise TypeError(
|
|
116
|
+
f"patches must be torch.Tensor, got {type(patches).__name__}"
|
|
117
|
+
)
|
|
118
|
+
if patches.ndim != 4:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"patches must have ndim==4 (L, C, ph, pw), got ndim={patches.ndim}"
|
|
121
|
+
)
|
|
122
|
+
if not patches.is_floating_point():
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"stitch requires floating-point patches, got dtype={patches.dtype}. "
|
|
125
|
+
"Convert with patches.float() — weight kernels are float-valued and "
|
|
126
|
+
"integer semantics would silently quantize the result."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if weight not in _WEIGHT_KINDS:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"weight must be one of {_WEIGHT_KINDS!r}, got {weight!r}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
n_patches, c, ph, pw = patches.shape
|
|
135
|
+
|
|
136
|
+
if not (isinstance(image_shape, tuple) and len(image_shape) == 3):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"image_shape must be a 3-tuple (C, H, W), got {image_shape!r}"
|
|
139
|
+
)
|
|
140
|
+
for axis_name, val in zip(("C", "H", "W"), image_shape, strict=True):
|
|
141
|
+
if not isinstance(val, int) or isinstance(val, bool) or val <= 0:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"image_shape[{axis_name}] must be a positive int, got {val!r}"
|
|
144
|
+
)
|
|
145
|
+
target_c, h, w = image_shape
|
|
146
|
+
if target_c != c:
|
|
147
|
+
raise ValueError(
|
|
148
|
+
f"image_shape channels={target_c} does not match patches channel count {c}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
sh, sw = _as_pair(stride, "stride")
|
|
152
|
+
dh, dw = _as_pair(dilation, "dilation")
|
|
153
|
+
|
|
154
|
+
if dh != 1 or dw != 1:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"stitch requires dilation==1, got dilation=({dh}, {dw}). "
|
|
157
|
+
"Patches extracted with dilation > 1 cannot round-trip — consume them as features."
|
|
158
|
+
)
|
|
159
|
+
if sh > ph or sw > pw:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"stitch forbids stride > patch_size (partial coverage forbidden), "
|
|
162
|
+
f"got stride=({sh}, {sw}) and patch_size=({ph}, {pw})."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
num_h = (h - ph) // sh + 1
|
|
166
|
+
num_w = (w - pw) // sw + 1
|
|
167
|
+
if num_h <= 0 or num_w <= 0:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"image_shape={image_shape} too small for patch_size=({ph}, {pw}) "
|
|
170
|
+
f"and stride=({sh}, {sw})"
|
|
171
|
+
)
|
|
172
|
+
expected_n_patches = num_h * num_w
|
|
173
|
+
if n_patches != expected_n_patches:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"patches.shape[0]={n_patches} inconsistent with grid implied by "
|
|
176
|
+
f"image_shape={image_shape}, patch_size=({ph}, {pw}), "
|
|
177
|
+
f"stride=({sh}, {sw}): expected L={expected_n_patches} "
|
|
178
|
+
f"(num_h={num_h}, num_w={num_w})."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
kernel = _window_kernel(weight, ph, pw, patches.dtype, patches.device)
|
|
182
|
+
|
|
183
|
+
# Weighted patches: broadcast kernel (ph, pw) across (L, C, ph, pw).
|
|
184
|
+
weighted = patches * kernel
|
|
185
|
+
|
|
186
|
+
# Numerator fold: (L, C, ph, pw) -> (1, C*ph*pw, L) for F.fold.
|
|
187
|
+
num_flat = (
|
|
188
|
+
weighted.permute(1, 2, 3, 0)
|
|
189
|
+
.reshape(c * ph * pw, n_patches)
|
|
190
|
+
.unsqueeze(0)
|
|
191
|
+
)
|
|
192
|
+
folded_num = F.fold(
|
|
193
|
+
num_flat,
|
|
194
|
+
output_size=(h, w),
|
|
195
|
+
kernel_size=(ph, pw),
|
|
196
|
+
stride=(sh, sw),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Denominator fold: replicate kernel across L patches; one "channel"
|
|
200
|
+
# broadcasts across image C in the division.
|
|
201
|
+
kernel_flat = (
|
|
202
|
+
kernel.flatten().unsqueeze(1).repeat(1, n_patches).unsqueeze(0)
|
|
203
|
+
)
|
|
204
|
+
folded_den = F.fold(
|
|
205
|
+
kernel_flat,
|
|
206
|
+
output_size=(h, w),
|
|
207
|
+
kernel_size=(ph, pw),
|
|
208
|
+
stride=(sh, sw),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# clamp(min=1e-6): for "uniform" this matches reconstruct's count-map
|
|
212
|
+
# clamp. For "hann", corner pixels covered only by edge-weight-zero
|
|
213
|
+
# positions have ~0 numerator AND ~0 denominator — output is dominated
|
|
214
|
+
# by the clamp (i.e., zero). Documented artifact (§9.9).
|
|
215
|
+
return (folded_num / folded_den.clamp(min=1e-6))[0]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: patchcraft
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Image patch extraction, reconstruction, pairing and seam-aware stitching for super-resolution and dataset pipelines.
|
|
5
|
+
Project-URL: Homepage, https://github.com/LeoPR/PatchCraft
|
|
6
|
+
Project-URL: Repository, https://github.com/LeoPR/PatchCraft
|
|
7
|
+
Project-URL: Issues, https://github.com/LeoPR/PatchCraft/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Documentation, https://github.com/LeoPR/PatchCraft/blob/main/docs/USAGE.md
|
|
10
|
+
Author: Leonardo Marques de Souza
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: dataset,image,patches,super-resolution,torch
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.12
|
|
28
|
+
Requires-Dist: numpy>=1.26
|
|
29
|
+
Requires-Dist: pillow>=10
|
|
30
|
+
Requires-Dist: torch>=2.6
|
|
31
|
+
Provides-Extra: cache
|
|
32
|
+
Requires-Dist: zstandard>=0.22; extra == 'cache'
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
38
|
+
Requires-Dist: torchvision>=0.20; extra == 'dev'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# PatchCraft
|
|
42
|
+
|
|
43
|
+
A small library for **encoding an image into patches and decoding it back**. Built to slot into other people's `torch` pipelines as one transform among many — like a `GaussianBlur` step in a `Compose([...])`.
|
|
44
|
+
|
|
45
|
+
> **Status (2026-05-17):** v0.1.0 released; v0.2.0-track is on `main` (not yet tagged). Public API: `extract`, `Patchify`, `reconstruct`, `stitch`, `pair`, `resize`, `Cache`, plus geometry helpers (`num_patches`, `tilings`, `TilingSpec`, `scale_factor`, `paired_tilings`, `PairedTilingSpec`), pixel metrics (`patch_metrics`, `per_patch_mse`, `per_patch_psnr`), and `PatchPair`/`PatchMeta`.
|
|
46
|
+
|
|
47
|
+
## The lib vs. this repo
|
|
48
|
+
|
|
49
|
+
Think of the lib as a **car** and this repo as the **car plus its test track**.
|
|
50
|
+
|
|
51
|
+
- **The car** — [`src/patchcraft/`](src/patchcraft/) — is what gets installed by `pip install patchcraft`. It is a single library with one job: take one image (`Tensor[C, H, W]`), encode it into patches, decode patches back into the image, optionally pair LR/HR, resize, cache. **One image at a time, every time.** No datasets, no training, no orchestration, no batching across images. Multi-image is the caller's `for` loop, or `torch.vmap`, or a `DataLoader`.
|
|
52
|
+
- **The track** — [`tests/`](tests/), [`lab/`](lab/), [`tests/_datasets.py`](tests/_datasets.py), and the dev extras (`torchvision`, etc.) — is the pit crew, telemetry, driver and stopwatch that **prove the car works** on real images (MNIST today; more later). It downloads datasets, drives the lib through varied geometries, measures correctness. It never ships in the wheel.
|
|
53
|
+
|
|
54
|
+
The car is also **acoplável** — designed to drop into someone else's pipeline:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from patchcraft import Patchify
|
|
58
|
+
from torchvision import transforms
|
|
59
|
+
|
|
60
|
+
transform = transforms.Compose([
|
|
61
|
+
transforms.ToTensor(),
|
|
62
|
+
transforms.GaussianBlur(kernel_size=3),
|
|
63
|
+
Patchify(patch_size=4, stride=2), # ← PatchCraft as one step
|
|
64
|
+
])
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`Patchify` is a callable; chain it inside a `Compose`, let `DataLoader` parallelize over workers. PatchCraft gives you the primitive; the surrounding pipeline stays your code.
|
|
68
|
+
|
|
69
|
+
## Visual cheat sheet
|
|
70
|
+
|
|
71
|
+
The five core operations, one diagram each. Letters mark which patch each cell came from / goes to.
|
|
72
|
+
|
|
73
|
+
### `extract` — image → patch stack
|
|
74
|
+
|
|
75
|
+
`patch_size=4`, `stride=4` (no overlap) on an 8×8 image:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
image (1, 8, 8) patches (4, 1, 4, 4)
|
|
79
|
+
+-----------------+ +-----+ +-----+
|
|
80
|
+
| . . . . | . . . . | | A | | B |
|
|
81
|
+
| . A . . | . B . . | extract +-----+ +-----+
|
|
82
|
+
| . . . . | . . . . | --------> patch0 patch1
|
|
83
|
+
| . . . . | . . . . |
|
|
84
|
+
|---------+---------| +-----+ +-----+
|
|
85
|
+
| . . . . | . . . . | | C | | D |
|
|
86
|
+
| . C . . | . D . . | +-----+ +-----+
|
|
87
|
+
| . . . . | . . . . | patch2 patch3
|
|
88
|
+
| . . . . | . . . . | (row-major order)
|
|
89
|
+
+-----------------+
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `reconstruct` — patch stack → image (bit-exact when `stride == patch_size`)
|
|
93
|
+
|
|
94
|
+
Each output pixel = sum of patch contributions / `count` map (= how many patches covered it). When `stride == patch_size`, `count` is all-ones and the divide is a no-op.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
stride == patch --> count map all 1 --> trivial copy
|
|
98
|
+
stride < patch --> count map > 1 --> weighted average
|
|
99
|
+
|
|
100
|
+
patch=4, stride=2, image cols 0..7:
|
|
101
|
+
col: 0 1 2 3 4 5 6 7
|
|
102
|
+
patch0: x x x x
|
|
103
|
+
patch1: x x x x
|
|
104
|
+
patch2: x x x x
|
|
105
|
+
count: 1 1 2 2 2 2 1 1 <- divide sum by this
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `pair` — LR <-> HR, same image region, different resolution
|
|
109
|
+
|
|
110
|
+
`scale_factor=2`: every k-th LR patch corresponds to the k-th HR patch; HR coords are LR coords times the integer scale.
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
LR (1, 4, 4) HR (1, 8, 8)
|
|
114
|
+
+---------+ +-------------+
|
|
115
|
+
| . . . . | | . . . . . . . . |
|
|
116
|
+
| .[A]. . | k = 1 --> | . .[A A]. . . . |
|
|
117
|
+
| . . . . | | . .[A A]. . . . |
|
|
118
|
+
| . . . . | | . . . . . . . . |
|
|
119
|
+
+---------+ | . . . . . . . . |
|
|
120
|
+
| . . . . . . . . |
|
|
121
|
+
| . . . . . . . . |
|
|
122
|
+
| . . . . . . . . |
|
|
123
|
+
+-------------+
|
|
124
|
+
|
|
125
|
+
LR patch at (row=1, col=1) <--> HR patch at (row=2, col=2)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `stitch` — same fold geometry as `reconstruct`, but each patch weighted by a window kernel
|
|
129
|
+
|
|
130
|
+
Use when patches were modified by a model and uniform averaging shows boundary seams. Window kernels for `patch_size=4`:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
weight="uniform" weight="hann" weight="gaussian"
|
|
134
|
+
(== reconstruct) centers > edges centers >> edges (never 0)
|
|
135
|
+
|
|
136
|
+
+ + + + . . . . . o o .
|
|
137
|
+
+ + + + . X X . o X X o
|
|
138
|
+
+ + + + . X X . o X X o
|
|
139
|
+
+ + + + . . . . . o o .
|
|
140
|
+
|
|
141
|
+
no seam attenuation strong attenuation, smooth attenuation,
|
|
142
|
+
image corners -> 0 corners preserved
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Everything stays one-image-at-a-time
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
for image in images:
|
|
149
|
+
patches = extract(image, ...) # PatchCraft primitive
|
|
150
|
+
result = model(patches) # caller's work
|
|
151
|
+
out = stitch(result, ...) # PatchCraft primitive
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Multi-image parallelism is the caller's pipeline (`torch.vmap`, `DataLoader` workers, etc.) — see [`SCOPE.md`](docs/SCOPE.md) §2.
|
|
155
|
+
|
|
156
|
+
## Scope (what the car does)
|
|
157
|
+
|
|
158
|
+
- **Extract** patches from a single image with configurable size, stride and dilation (`extract`, `Patchify`).
|
|
159
|
+
- **Reconstruct** an image from its patches — exact and weighted-overlap (`reconstruct`).
|
|
160
|
+
- **Stitch** *modified* patches (model output, denoised, super-resolved) back into one image with a window kernel that attenuates boundary seams (`stitch`, with `weight="uniform"|"hann"|"gaussian"`).
|
|
161
|
+
- **Plan** the geometry ahead of time: `num_patches((H, W), ...)` for the count, `tilings((H, W), allow_overlap=...)` for every full-coverage `(patch_size, stride)` combo (no image, no allocation — just arithmetic). For LR↔HR setups: `scale_factor(...)` and `paired_tilings(...)`.
|
|
162
|
+
- **Pair** LR and HR patches with metadata sufficient to reconstruct either (`pair`, `PatchPair`, `PatchMeta`).
|
|
163
|
+
- **Measure** pixel-level error between two patch stacks: `patch_metrics`, `per_patch_mse`, `per_patch_psnr`.
|
|
164
|
+
- **Resize** with pluggable backends — PIL or torch (`resize`).
|
|
165
|
+
- **Cache** results on disk with content-addressed keys, OneDrive-race retry, optional zstd (`Cache`).
|
|
166
|
+
|
|
167
|
+
## Scope (what the car does NOT do)
|
|
168
|
+
|
|
169
|
+
- **Not a dataset manager.** PatchCraft does not load, download, batch, shuffle, or stream datasets. That's the track's job — `tests/_datasets.py` has `mnist_subset(...)` for dev fixtures, and `torchvision` is in the `[dev]` extra (never a runtime dep of the car).
|
|
170
|
+
- **Not a multi-image API.** Every primitive takes one image. Use `vmap` or a Python loop if you need to apply it to many.
|
|
171
|
+
- No SVMs, no kernels, no quantum circuits — those belong to other projects.
|
|
172
|
+
- No neural network training — PatchCraft is infrastructure, not a model.
|
|
173
|
+
|
|
174
|
+
## Install
|
|
175
|
+
|
|
176
|
+
### From PyPI
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
pip install patchcraft # core only
|
|
180
|
+
pip install patchcraft[cache] # adds zstandard for compressed Cache entries
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### From source (development)
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
git clone https://github.com/LeoPR/PatchCraft.git
|
|
187
|
+
cd patchcraft
|
|
188
|
+
pip install -e ".[dev,cache]"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
For GPU support, install a matching torch wheel before PatchCraft
|
|
192
|
+
(e.g. `pip install torch --index-url https://download.pytorch.org/whl/cu124`).
|
|
193
|
+
|
|
194
|
+
## Run tests
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
pytest
|
|
198
|
+
pytest -m "not gpu" # skip GPU-requiring tests
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Layout
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
PatchCraft/
|
|
205
|
+
├── pyproject.toml package metadata, build backend (hatchling)
|
|
206
|
+
├── README.md this file
|
|
207
|
+
├── LICENSE MIT
|
|
208
|
+
├── .python-version 3.13
|
|
209
|
+
├── .gitignore ignores archive/, venvs, caches, outputs
|
|
210
|
+
├── src/patchcraft/ library core — one-image-at-a-time primitives
|
|
211
|
+
│ ├── __init__.py re-exports the full public API
|
|
212
|
+
│ ├── extract.py patches via F.unfold; Patchify wrapper (ADR 0002)
|
|
213
|
+
│ ├── reconstruct.py inverse via F.fold + count map
|
|
214
|
+
│ ├── geometry.py pre-flight: num_patches, tilings, TilingSpec
|
|
215
|
+
│ ├── pair.py LR↔HR pairing; PatchPair, PatchMeta
|
|
216
|
+
│ ├── resize.py resize with PIL or torch backends
|
|
217
|
+
│ └── cache.py content-addressed disk cache
|
|
218
|
+
├── tests/ pytest suite (contract tests for src/)
|
|
219
|
+
│ ├── test_extract.py extract + Patchify
|
|
220
|
+
│ ├── test_reconstruct.py
|
|
221
|
+
│ ├── test_geometry.py num_patches + tilings
|
|
222
|
+
│ ├── test_pair.py
|
|
223
|
+
│ ├── test_resize.py
|
|
224
|
+
│ ├── test_cache.py
|
|
225
|
+
│ ├── test_datasets_helper.py label_subset
|
|
226
|
+
│ ├── test_import.py
|
|
227
|
+
│ └── _datasets.py dev-only fixtures (MNIST, etc) — NOT public API
|
|
228
|
+
├── lab/ ephemeral experiments; see lab/README.md
|
|
229
|
+
│ ├── README.md bench rules (tracked)
|
|
230
|
+
│ └── .gitignore ignores everything else (tracked)
|
|
231
|
+
├── docs/
|
|
232
|
+
│ ├── USAGE.md live REPL walkthrough of every public API
|
|
233
|
+
│ ├── SCOPE.md responsibilities matrix + parallelization analysis
|
|
234
|
+
│ ├── AUXILIARY.md tests/_datasets, lab/, Z:\ conventions (NOT part of the wheel)
|
|
235
|
+
│ ├── THEORY.md distilled design + §9 condition contract; §0 binding scope
|
|
236
|
+
│ ├── ROADMAP.md milestone plan
|
|
237
|
+
│ └── ADR/
|
|
238
|
+
│ ├── 0001-patch-extraction-api.md pure function `extract`
|
|
239
|
+
│ └── 0002-patchify-transform.md callable wrapper for Compose pipelines
|
|
240
|
+
└── archive/ reference-only; gitignored (pruned 2026-05-17 — only HISTORY.md kept)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Validation lab
|
|
244
|
+
|
|
245
|
+
The library is "one image in, one tensor out" by design — but you only know it works once you run it end-to-end on real images. That happens in two places, neither of which is part of the shipped package:
|
|
246
|
+
|
|
247
|
+
- [`tests/`](tests/) — formal pytest suite that defines the contract from [`docs/THEORY.md`](docs/THEORY.md) §9.
|
|
248
|
+
- [`lab/`](lab/) — ephemeral scripts and notebooks for fast hypothesis-checking. See [`lab/README.md`](lab/README.md) for the bench rules; outputs go to `Z:\outputs\patchcraft\` (off-tree).
|
|
249
|
+
|
|
250
|
+
Datasets used by tests/lab are downloaded lazily into `Z:\caches\datasets\<name>\` on first use; they do not ship with the package and are never bundled into the wheel.
|
|
251
|
+
|
|
252
|
+
## Where to read next
|
|
253
|
+
|
|
254
|
+
| If you want… | Open |
|
|
255
|
+
|---|---|
|
|
256
|
+
| A hands-on tour with real REPL outputs for every public API | [`docs/USAGE.md`](docs/USAGE.md) |
|
|
257
|
+
| The line between "PatchCraft's job" and "your pipeline's job", plus the parallelization story | [`docs/SCOPE.md`](docs/SCOPE.md) |
|
|
258
|
+
| The auxiliary test fixtures and lab conventions (not shipped) | [`docs/AUXILIARY.md`](docs/AUXILIARY.md) |
|
|
259
|
+
| Design decisions, math, the per-API contract | [`docs/THEORY.md`](docs/THEORY.md) |
|
|
260
|
+
| Architecture Decision Records | [`docs/ADR/`](docs/ADR/) |
|
|
261
|
+
| Milestone plan | [`docs/ROADMAP.md`](docs/ROADMAP.md) |
|
|
262
|
+
| Per-release changes | [`CHANGELOG.md`](CHANGELOG.md) |
|
|
263
|
+
|
|
264
|
+
## Author
|
|
265
|
+
|
|
266
|
+
Leonardo Marques de Souza
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
patchcraft/__init__.py,sha256=mvvN6gwkDi6ZFhqjoImxxajfHzk7nnsmZmDADZKrQMU,957
|
|
2
|
+
patchcraft/cache.py,sha256=Rs5_HS7nIVEYPvaDnvbgFcHTEx4xnB2Iqx2QCaeVLr4,8596
|
|
3
|
+
patchcraft/extract.py,sha256=yFSd-ytukeVnieuu45OTpvULDFNEminhA97goMR04xs,4123
|
|
4
|
+
patchcraft/geometry.py,sha256=AZZPV0JMKhpy5DVP-6XMJC-JhNipVZYhPvi8yYHd-7c,10501
|
|
5
|
+
patchcraft/metrics.py,sha256=tJ8hPV4Emaw6jBzWTep3_ad7M-DbbRXdPRvlOuh3xZs,5164
|
|
6
|
+
patchcraft/pair.py,sha256=fXzC03RVtlRh62lRdreknI7tAVz9eMPpLfYpj9VHaZ8,5779
|
|
7
|
+
patchcraft/reconstruct.py,sha256=NGYGPbnQJ8o4aYiOV3m_dNc4X4WHekfidRPa1lelspc,4497
|
|
8
|
+
patchcraft/resize.py,sha256=FB43uJcMTW5FPhthBVcluRTH749X6Cu6PDpzqyyws_U,7009
|
|
9
|
+
patchcraft/stitch.py,sha256=CR-TD1LiaORmKoMarQ_pN20zvlC__CirB1z7778SvmE,8461
|
|
10
|
+
patchcraft/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
patchcraft-0.2.0.dist-info/METADATA,sha256=JjLZOEneH7B5cDYAo7qHUbeC5G78wuHpG73jf0nNwT0,13375
|
|
12
|
+
patchcraft-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
+
patchcraft-0.2.0.dist-info/licenses/LICENSE,sha256=mFbXuSyc-xMcSV55A6NVsfgvW7ldtJb6Eky-Rv5UHN8,1082
|
|
14
|
+
patchcraft-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonardo Marques de Souza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|