patchcraft 0.2.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.
Files changed (46) hide show
  1. patchcraft-0.2.0/.github/workflows/release.yml +111 -0
  2. patchcraft-0.2.0/.github/workflows/test.yml +43 -0
  3. patchcraft-0.2.0/.gitignore +55 -0
  4. patchcraft-0.2.0/.python-version +1 -0
  5. patchcraft-0.2.0/.vscode/settings.json +3 -0
  6. patchcraft-0.2.0/CHANGELOG.md +175 -0
  7. patchcraft-0.2.0/LICENSE +21 -0
  8. patchcraft-0.2.0/PKG-INFO +266 -0
  9. patchcraft-0.2.0/README.md +226 -0
  10. patchcraft-0.2.0/docs/ADR/0001-patch-extraction-api.md +100 -0
  11. patchcraft-0.2.0/docs/ADR/0002-patchify-transform.md +101 -0
  12. patchcraft-0.2.0/docs/AUXILIARY.md +167 -0
  13. patchcraft-0.2.0/docs/ROADMAP.md +94 -0
  14. patchcraft-0.2.0/docs/SCOPE.md +282 -0
  15. patchcraft-0.2.0/docs/THEORY.md +451 -0
  16. patchcraft-0.2.0/docs/USAGE.md +482 -0
  17. patchcraft-0.2.0/lab/.gitignore +3 -0
  18. patchcraft-0.2.0/lab/2026-05-16-roundtrip-mnist.py +80 -0
  19. patchcraft-0.2.0/lab/README.md +34 -0
  20. patchcraft-0.2.0/lab/usage_demo.out +174 -0
  21. patchcraft-0.2.0/lab/usage_demo.py +239 -0
  22. patchcraft-0.2.0/pyproject.toml +89 -0
  23. patchcraft-0.2.0/src/patchcraft/__init__.py +39 -0
  24. patchcraft-0.2.0/src/patchcraft/cache.py +247 -0
  25. patchcraft-0.2.0/src/patchcraft/extract.py +116 -0
  26. patchcraft-0.2.0/src/patchcraft/geometry.py +290 -0
  27. patchcraft-0.2.0/src/patchcraft/metrics.py +151 -0
  28. patchcraft-0.2.0/src/patchcraft/pair.py +161 -0
  29. patchcraft-0.2.0/src/patchcraft/py.typed +0 -0
  30. patchcraft-0.2.0/src/patchcraft/reconstruct.py +118 -0
  31. patchcraft-0.2.0/src/patchcraft/resize.py +189 -0
  32. patchcraft-0.2.0/src/patchcraft/stitch.py +215 -0
  33. patchcraft-0.2.0/tests/__init__.py +0 -0
  34. patchcraft-0.2.0/tests/_datasets.py +117 -0
  35. patchcraft-0.2.0/tests/conftest.py +1 -0
  36. patchcraft-0.2.0/tests/test_cache.py +254 -0
  37. patchcraft-0.2.0/tests/test_datasets_helper.py +64 -0
  38. patchcraft-0.2.0/tests/test_extract.py +250 -0
  39. patchcraft-0.2.0/tests/test_geometry.py +333 -0
  40. patchcraft-0.2.0/tests/test_import.py +7 -0
  41. patchcraft-0.2.0/tests/test_metrics.py +170 -0
  42. patchcraft-0.2.0/tests/test_pair.py +212 -0
  43. patchcraft-0.2.0/tests/test_reconstruct.py +205 -0
  44. patchcraft-0.2.0/tests/test_resize.py +187 -0
  45. patchcraft-0.2.0/tests/test_stitch.py +229 -0
  46. patchcraft-0.2.0/uv.lock +1010 -0
@@ -0,0 +1,111 @@
1
+ name: release
2
+
3
+ # Triggered when a version tag is pushed (vX.Y.Z[.aN, .bN, .rcN]).
4
+ # Steps:
5
+ # 1. Re-run the test job that test.yml uses (sanity check on the
6
+ # exact ref being released).
7
+ # 2. Build the wheel + sdist with `uv build`.
8
+ # 3. Publish to PyPI via Trusted Publishing (OIDC) -- no token,
9
+ # no secret. The pypi.org side must have a configured publisher
10
+ # pointing at THIS repo + THIS workflow + the `pypi` environment.
11
+ # 4. Create / update the GitHub Release page and attach the
12
+ # `dist/*.whl` and `dist/*.tar.gz` files.
13
+
14
+ on:
15
+ push:
16
+ tags:
17
+ - "v[0-9]+.[0-9]+.[0-9]+"
18
+ - "v[0-9]+.[0-9]+.[0-9]+.[ab][0-9]+"
19
+ - "v[0-9]+.[0-9]+.[0-9]+.rc[0-9]+"
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ jobs:
25
+ validate:
26
+ name: re-run tests at tag ref
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v3
31
+ with:
32
+ enable-cache: true
33
+ - run: uv python install 3.13
34
+ - run: uv sync --extra cache --extra dev --python 3.13
35
+ - run: uv run ruff check src tests
36
+ - run: uv run mypy --strict src
37
+ - run: uv run pytest -m "not gpu"
38
+
39
+ build:
40
+ name: build wheel + sdist
41
+ runs-on: ubuntu-latest
42
+ needs: validate
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: astral-sh/setup-uv@v3
46
+ - run: uv build
47
+ - name: Inspect dist metadata
48
+ run: |
49
+ uv run --with twine twine check dist/*
50
+ ls -la dist/
51
+ - uses: actions/upload-artifact@v4
52
+ with:
53
+ name: dist
54
+ path: dist/
55
+
56
+ publish-pypi:
57
+ name: publish to PyPI (Trusted Publishing)
58
+ runs-on: ubuntu-latest
59
+ needs: build
60
+ environment:
61
+ name: pypi
62
+ url: https://pypi.org/project/patchcraft/
63
+ permissions:
64
+ id-token: write # required for OIDC token exchange with PyPI
65
+ steps:
66
+ - uses: actions/download-artifact@v4
67
+ with:
68
+ name: dist
69
+ path: dist/
70
+ - name: Publish via Trusted Publishing
71
+ uses: pypa/gh-action-pypi-publish@release/v1
72
+ # No `password` field -- OIDC handles authentication.
73
+ # The pypi.org "Pending publisher" config must match:
74
+ # PyPI project name: patchcraft
75
+ # Owner: LeoPR
76
+ # Repository: PatchCraft
77
+ # Workflow: release.yml
78
+ # Environment: pypi
79
+
80
+ github-release:
81
+ name: create GitHub Release with assets
82
+ runs-on: ubuntu-latest
83
+ needs: publish-pypi
84
+ permissions:
85
+ contents: write # required to create a Release
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ - uses: actions/download-artifact@v4
89
+ with:
90
+ name: dist
91
+ path: dist/
92
+ - name: Extract version from tag
93
+ id: ver
94
+ run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
95
+ - name: Create Release + attach assets
96
+ uses: softprops/action-gh-release@v2
97
+ with:
98
+ name: "PatchCraft ${{ github.ref_name }}"
99
+ body: |
100
+ See [`CHANGELOG.md`](https://github.com/LeoPR/PatchCraft/blob/main/CHANGELOG.md) for the full notes.
101
+
102
+ Install:
103
+ ```
104
+ pip install patchcraft==${{ steps.ver.outputs.version }}
105
+ ```
106
+ files: |
107
+ dist/*.whl
108
+ dist/*.tar.gz
109
+ fail_on_unmatched_files: true
110
+ draft: false
111
+ prerelease: false
@@ -0,0 +1,43 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: test-${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: ${{ matrix.os }} / py${{ matrix.python }}
16
+ runs-on: ${{ matrix.os }}
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os: [ubuntu-latest, windows-latest]
21
+ python: ["3.12", "3.13"]
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v3
27
+ with:
28
+ enable-cache: true
29
+
30
+ - name: Set up Python ${{ matrix.python }}
31
+ run: uv python install ${{ matrix.python }}
32
+
33
+ - name: Install project with dev + cache extras
34
+ run: uv sync --extra cache --extra dev --python ${{ matrix.python }}
35
+
36
+ - name: Ruff
37
+ run: uv run ruff check src tests
38
+
39
+ - name: Mypy (strict, src only)
40
+ run: uv run mypy --strict src
41
+
42
+ - name: Pytest (skip GPU markers)
43
+ run: uv run pytest -m "not gpu"
@@ -0,0 +1,55 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+
7
+ # Distribution / packaging
8
+ build/
9
+ dist/
10
+ *.egg-info/
11
+ .eggs/
12
+
13
+ # Virtual envs (lives on Z:, but guard anyway)
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Test and coverage
19
+ .pytest_cache/
20
+ .coverage
21
+ .coverage.*
22
+ htmlcov/
23
+ .tox/
24
+
25
+ # Type checkers / linters
26
+ .mypy_cache/
27
+ .ruff_cache/
28
+ .pyre/
29
+ .pytype/
30
+
31
+ # IDE
32
+ .vscode/*
33
+ !.vscode/settings.json
34
+ !.vscode/tasks.json
35
+ !.vscode/launch.json
36
+ .idea/
37
+
38
+ # OS
39
+ Thumbs.db
40
+ .DS_Store
41
+ *.lnk
42
+
43
+ # Env vars
44
+ .env
45
+ .env.local
46
+
47
+ # Experiment outputs
48
+ outputs/
49
+ runs/
50
+
51
+ # Archive (reference material, not part of the package)
52
+ /archive/
53
+
54
+ # Datasets and caches live on Z:
55
+ data/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,3 @@
1
+ {
2
+ "python.defaultInterpreterPath": "${workspaceFolder}/.venv/Scripts/python.exe"
3
+ }
@@ -0,0 +1,175 @@
1
+ # Changelog
2
+
3
+ All notable changes to PatchCraft will be documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] — 2026-05-17
10
+
11
+ Second public release. Adds three feature groups motivated by the QPatchSR
12
+ super-resolution consumer plus internal ergonomics. No breaking changes vs
13
+ v0.1.0 — all v0.1.0 imports keep working.
14
+
15
+ ### Added — cross-resolution geometry (THEORY §1.5, §9.7)
16
+
17
+ Motivated by the QPatchSR consumer's question: "given two image shapes
18
+ (LR and HR of the same source), what `(patch_size, stride)` on each
19
+ side yields the same number of patches with corresponding regions?"
20
+ Three new helpers in `patchcraft.geometry`:
21
+
22
+ - **`scale_factor(lr_shape, hr_shape) -> int | None`** — returns the
23
+ integer `k` such that `hr.shape[-2:] == (k * lr.shape[-2], k *
24
+ lr.shape[-1])`, or `None`. Accepts `(H, W)` or `(C, H, W)`. Pre-
25
+ flight check for `pair`.
26
+ - **`paired_tilings(lr_shape, hr_shape, *, allow_overlap=False, ...)`**
27
+ — enumerates every `(lr_spec, hr_spec)` pair where both fully cover
28
+ their respective image and produce identical patch counts. Patch
29
+ `k` on each side covers the same image region.
30
+ Example: `paired_tilings((14, 14), (28, 28))` returns three pairs:
31
+ `(p_lr=2, p_hr=4, total=49)`, `(p_lr=7, p_hr=14, total=4)`,
32
+ `(p_lr=14, p_hr=28, total=1)`.
33
+ - **`PairedTilingSpec(lr, hr, scale_factor)`** — `NamedTuple` carrying
34
+ both sides and the discovered scale factor.
35
+
36
+ ### Added — patch-level pixel metrics (THEORY §1.6, §9.8)
37
+
38
+ Canonical reductions so consumers don't reinvent slightly-divergent
39
+ versions in every project. New module `patchcraft.metrics`:
40
+
41
+ - **`patch_metrics(a, b, *, max_value=1.0) -> dict[str, float]`** —
42
+ scalar `mae`, `mse`, `max_abs`, `psnr_db` over the full tensor
43
+ (any matching shape works). Internal accumulation in `float64`
44
+ for stability; PSNR returns `+inf` for identical inputs.
45
+ - **`per_patch_mse(a, b) -> Tensor[L]`** — one MSE per patch in a
46
+ `(L, C, h, w)` stack.
47
+ - **`per_patch_psnr(a, b, *, max_value=1.0) -> Tensor[L]`** — one
48
+ PSNR per patch. Identical patches yield `+inf` via `torch.where`
49
+ (no clamp tricks).
50
+
51
+ Explicitly **not** included: SSIM, MS-SSIM, LPIPS, FID, any windowed
52
+ or learned metric. Use `pytorch-msssim`, `lpips`, `clean-fid` on the
53
+ caller side ([SCOPE.md](docs/SCOPE.md) §4.3 explains the boundary).
54
+
55
+ ### Added — patch stitching for modified patches (THEORY §2.5, §9.9)
56
+
57
+ `reconstruct` is the bit-exact inverse of `extract`. When patches have
58
+ been modified (model output, denoised, super-resolved), averaging them
59
+ back uniformly shows visible seams at patch boundaries. `stitch` is the
60
+ seam-aware counterpart: it folds patches weighted by a 2-D window
61
+ kernel so each pixel "trusts" patches closer to its center more.
62
+
63
+ - **`stitch(patches, image_shape, stride, *, weight="uniform"|"hann"|"gaussian", dilation=1)`**
64
+ — same `F.fold` geometry and rejections as `reconstruct`; adds a
65
+ weighted-blend numerator over a weighted-sum denominator. With
66
+ `weight="uniform"` it is mathematically equivalent to `reconstruct`
67
+ (covered by a bit-exact equality test on no-overlap and `allclose`
68
+ on overlap). With `"hann"` it strongly suppresses seams at the
69
+ cost of zeroing image corners that fall on Hann's edge-weight-zero
70
+ region (documented artifact). With `"gaussian"`
71
+ (`sigma = max(1, min(ph, pw) / 4)`) it blends smoothly with no
72
+ corner artifact.
73
+
74
+ Floating-point patches only — window kernels are float-valued and we
75
+ refuse to silently quantize or implicitly promote. Caller converts to
76
+ `float` first.
77
+
78
+ ### Changed
79
+
80
+ - Public API surface: 11 → 18 symbols.
81
+ - [`docs/SCOPE.md`](docs/SCOPE.md) gains rows for paired tilings,
82
+ pixel metrics, and stitch; §4.3 discusses why pixel metrics stayed
83
+ core while windowed/learned metrics did not, §4.4 explains why
84
+ `stitch` is a separate function from `reconstruct` rather than a
85
+ kwarg.
86
+ - [`docs/THEORY.md`](docs/THEORY.md) gains §1.5 expansion (cross-
87
+ resolution paragraphs), §1.6 (patch comparison metrics), §2.5
88
+ (stitch — math, kernels, why it is separate), §9.7
89
+ (paired tilings contract), §9.8 (metrics contract), §9.9 (stitch
90
+ contract).
91
+
92
+ ## [0.1.0] — 2026-05-16
93
+
94
+ First public release. Public API stable; signatures will only change in 1.x.
95
+
96
+ ### Added — core (one image at a time)
97
+
98
+ - **`extract(image, patch_size, stride, dilation=1)`** — patches from a
99
+ `(C, H, W)` tensor via `torch.nn.functional.unfold`. Truncation-only
100
+ boundary; returns `Tensor[0, C, ph, pw]` when geometry fits no patch.
101
+ Per [ADR 0001](docs/ADR/0001-patch-extraction-api.md).
102
+ - **`Patchify(patch_size, stride, dilation=1)`** — callable wrapper for
103
+ `torchvision.transforms.Compose([...])`. Eager geometry validation in
104
+ `__init__`; `__slots__`-bound (no state beyond config). Per
105
+ [ADR 0002](docs/ADR/0002-patchify-transform.md).
106
+ - **`reconstruct(patches, image_shape, stride, dilation=1)`** — inverse
107
+ of `extract` via `F.fold` plus a fold-of-ones count map. Bit-exact
108
+ round-trip for `stride == patch_size`; weighted-exact for overlap.
109
+ Rejects `dilation != 1` and `stride > patch_size` (partial coverage
110
+ is forbidden by design — synthesizing pixel values is not PatchCraft's
111
+ job).
112
+ - **`pair(lr_image, hr_image, lr_patch_size, scale_factor, stride, *, image_id=None)`**
113
+ — LR/HR patch correspondences. Returns a frozen `PatchPair`
114
+ dataclass with `lr_patches`, `hr_patches`, `metas`. Integer
115
+ `scale_factor` only. LR and HR must share `C`, dtype, and device.
116
+ - **`PatchPair`**, **`PatchMeta`** — frozen `@dataclass(slots=True)`.
117
+ `PatchMeta` carries `patch_index`, `row`, `col` (LR coords),
118
+ `lr_patch_size`, `hr_patch_size`, `image_id`. CPU-only metadata.
119
+ - **`resize(image, target_size, backend="pil", resample=None)`** —
120
+ single-image resize. Output type matches input
121
+ (PIL → PIL, Tensor → Tensor). Cross-backend conversions go through
122
+ a float32 [0, 1] / uint8 hop (numpy intermediate; no torchvision in
123
+ the core). CUDA tensors accepted only with `backend="torch"`.
124
+ - **`Cache(root, namespace, version=1)`** — content-addressed disk
125
+ cache. `key_for(*parts) → str`, `put(key, bytes)`, `get(key) → bytes | None`.
126
+ Atomic write via `*.tmp` + `os.replace` with retry on transient
127
+ `PermissionError` (5 attempts on put with exponential backoff
128
+ `0.25/0.5/1/2/4` s — handles OneDrive, antivirus, Windows Search
129
+ races). Optional zstandard compression at level 3 (`[cache]` extra);
130
+ uncompressed fallback when not installed. Sidecar JSON carries
131
+ SHA-256 checksum; corruption surfaces as `OSError`.
132
+ - **`num_patches(image_shape, patch_size, stride, dilation=1)`** — the
133
+ patch count formula, exposed as a function. No allocation, no
134
+ tensor. Accepts `(H, W)` or `(C, H, W)`.
135
+ - **`tilings(image_shape, *, allow_overlap=False, min_patch_size=2, max_patch_size=None)`**
136
+ — enumerate every square, full-coverage `(patch_size, stride)`
137
+ geometry. Always emits `dilation=(1, 1)`. With default flags returns
138
+ exact tilings only (`patch_size == stride`, divisibility); with
139
+ `allow_overlap=True` adds clean-edge overlap geometries. Truncated
140
+ geometries are deliberately excluded — the function answers "what is
141
+ sound by construction?", not "what will `extract` accept?".
142
+ - **`TilingSpec`** — `NamedTuple(patch_size, stride, dilation,
143
+ num_patches, total_patches, overlap)`.
144
+
145
+ ### Added — packaging
146
+
147
+ - `py.typed` marker (PEP 561): downstream `mypy` now honors PatchCraft's
148
+ type hints.
149
+ - `[cache]` extra: pulls `zstandard>=0.22` for compressed cache
150
+ entries. Core works without it.
151
+
152
+ ### Out of scope (v0.1.x)
153
+
154
+ - Multi-image batched API — use a `for` loop, `torch.vmap`, or a
155
+ `DataLoader`. See `Patchify` for `transforms.Compose` integration.
156
+ - Dataset orchestration (download, batching, sampling) — the auxiliary
157
+ framework in [`tests/_datasets.py`](tests/_datasets.py) handles this
158
+ for the test suite and `lab/` scripts; it is not shipped in the wheel.
159
+ - Channels-last layout, quantization, `nn.Module` integration —
160
+ documented in [`docs/THEORY.md`](docs/THEORY.md) §6 and §8 (open
161
+ questions).
162
+
163
+ ### Documentation
164
+
165
+ - [`docs/THEORY.md`](docs/THEORY.md) — §0 binding scope, §§1–6 design
166
+ decisions per primitive, §7 resolved questions, §8 open questions,
167
+ §9 the per-API condition contract (Accepts / Rejects / Out of scope)
168
+ that the test suite mirrors.
169
+ - [`docs/ADR/0001-patch-extraction-api.md`](docs/ADR/0001-patch-extraction-api.md)
170
+ and [`docs/ADR/0002-patchify-transform.md`](docs/ADR/0002-patchify-transform.md).
171
+ - [`README.md`](README.md) — installation, the car-vs-track metaphor,
172
+ validation lab.
173
+
174
+ [0.2.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.2.0
175
+ [0.1.0]: https://github.com/LeoPR/PatchCraft/releases/tag/v0.1.0
@@ -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.
@@ -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