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.
- patchcraft-0.2.0/.github/workflows/release.yml +111 -0
- patchcraft-0.2.0/.github/workflows/test.yml +43 -0
- patchcraft-0.2.0/.gitignore +55 -0
- patchcraft-0.2.0/.python-version +1 -0
- patchcraft-0.2.0/.vscode/settings.json +3 -0
- patchcraft-0.2.0/CHANGELOG.md +175 -0
- patchcraft-0.2.0/LICENSE +21 -0
- patchcraft-0.2.0/PKG-INFO +266 -0
- patchcraft-0.2.0/README.md +226 -0
- patchcraft-0.2.0/docs/ADR/0001-patch-extraction-api.md +100 -0
- patchcraft-0.2.0/docs/ADR/0002-patchify-transform.md +101 -0
- patchcraft-0.2.0/docs/AUXILIARY.md +167 -0
- patchcraft-0.2.0/docs/ROADMAP.md +94 -0
- patchcraft-0.2.0/docs/SCOPE.md +282 -0
- patchcraft-0.2.0/docs/THEORY.md +451 -0
- patchcraft-0.2.0/docs/USAGE.md +482 -0
- patchcraft-0.2.0/lab/.gitignore +3 -0
- patchcraft-0.2.0/lab/2026-05-16-roundtrip-mnist.py +80 -0
- patchcraft-0.2.0/lab/README.md +34 -0
- patchcraft-0.2.0/lab/usage_demo.out +174 -0
- patchcraft-0.2.0/lab/usage_demo.py +239 -0
- patchcraft-0.2.0/pyproject.toml +89 -0
- patchcraft-0.2.0/src/patchcraft/__init__.py +39 -0
- patchcraft-0.2.0/src/patchcraft/cache.py +247 -0
- patchcraft-0.2.0/src/patchcraft/extract.py +116 -0
- patchcraft-0.2.0/src/patchcraft/geometry.py +290 -0
- patchcraft-0.2.0/src/patchcraft/metrics.py +151 -0
- patchcraft-0.2.0/src/patchcraft/pair.py +161 -0
- patchcraft-0.2.0/src/patchcraft/py.typed +0 -0
- patchcraft-0.2.0/src/patchcraft/reconstruct.py +118 -0
- patchcraft-0.2.0/src/patchcraft/resize.py +189 -0
- patchcraft-0.2.0/src/patchcraft/stitch.py +215 -0
- patchcraft-0.2.0/tests/__init__.py +0 -0
- patchcraft-0.2.0/tests/_datasets.py +117 -0
- patchcraft-0.2.0/tests/conftest.py +1 -0
- patchcraft-0.2.0/tests/test_cache.py +254 -0
- patchcraft-0.2.0/tests/test_datasets_helper.py +64 -0
- patchcraft-0.2.0/tests/test_extract.py +250 -0
- patchcraft-0.2.0/tests/test_geometry.py +333 -0
- patchcraft-0.2.0/tests/test_import.py +7 -0
- patchcraft-0.2.0/tests/test_metrics.py +170 -0
- patchcraft-0.2.0/tests/test_pair.py +212 -0
- patchcraft-0.2.0/tests/test_reconstruct.py +205 -0
- patchcraft-0.2.0/tests/test_resize.py +187 -0
- patchcraft-0.2.0/tests/test_stitch.py +229 -0
- 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,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
|
patchcraft-0.2.0/LICENSE
ADDED
|
@@ -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
|