image-bridge-toolkit 0.1.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 (23) hide show
  1. image_bridge_toolkit-0.1.0/LICENSE.md +38 -0
  2. image_bridge_toolkit-0.1.0/PKG-INFO +234 -0
  3. image_bridge_toolkit-0.1.0/README.md +201 -0
  4. image_bridge_toolkit-0.1.0/pyproject.toml +55 -0
  5. image_bridge_toolkit-0.1.0/setup.cfg +4 -0
  6. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/__init__.py +5 -0
  7. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/_imaging_backend.py +12 -0
  8. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/cache_builder.py +160 -0
  9. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/cache_io.py +49 -0
  10. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/calculators.py +153 -0
  11. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/constants.py +17 -0
  12. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/dataset_loader.py +94 -0
  13. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/logging_utils.py +15 -0
  14. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/matcher.py +240 -0
  15. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/planner.py +33 -0
  16. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/scanner.py +58 -0
  17. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit/similarity.py +81 -0
  18. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit.egg-info/PKG-INFO +234 -0
  19. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit.egg-info/SOURCES.txt +21 -0
  20. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit.egg-info/dependency_links.txt +1 -0
  21. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit.egg-info/entry_points.txt +3 -0
  22. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit.egg-info/requires.txt +12 -0
  23. image_bridge_toolkit-0.1.0/src/image_bridge_toolkit.egg-info/top_level.txt +1 -0
@@ -0,0 +1,38 @@
1
+ ========================================================================
2
+ SOURCE CODE LICENSE (MIT)
3
+ ========================================================================
4
+ The source code within the `src/` directory is licensed under the MIT License.
5
+
6
+ Copyright (c) 2026 Andrew Kingdom
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM OR OTHER LIABILITY, WHETHER
22
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ ========================================================================
26
+ DOCUMENTATION & BRANDING LICENSE (CC BY-NC-ND 4.0 + TRANSLATION WAIVER)
27
+ ========================================================================
28
+ All non-code assets, including README files, documentation, logo designs,
29
+ and diagrams, are Copyright (c) 2026 Andrew Kingdom.
30
+
31
+ Licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0
32
+ International (CC BY-NC-ND 4.0).
33
+
34
+ Additional Permission: Direct language translations of this documentation are
35
+ expressly permitted without prior written authorization, provided that:
36
+ 1. Full attribution to Andrew Kingdom is maintained.
37
+ 2. A direct hyperlink to the original repository is included.
38
+ 3. No non-linguistic modifications or structural alterations are made.
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: image-bridge-toolkit
3
+ Version: 0.1.0
4
+ Summary: Cache-backed perceptual image fingerprinting and cross-dataset bridge matching.
5
+ Author: Andrew Kingdom
6
+ License: MIT for Code / CC BY-NC-ND 4.0 for Documentation
7
+ Project-URL: Homepage, https://github.com/akingdom/image_bridge_toolkit
8
+ Project-URL: Repository, https://github.com/akingdom/image_bridge_toolkit
9
+ Project-URL: Issues, https://github.com/akingdom/image_bridge_toolkit/issues
10
+ Keywords: image-processing,perceptual-hash,phash,deduplication,dct,computer-vision,reverse-image-search
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE.md
23
+ Requires-Dist: numpy>=1.23
24
+ Requires-Dist: Pillow>=9.0
25
+ Provides-Extra: opencv
26
+ Requires-Dist: opencv-python-headless>=4.6; extra == "opencv"
27
+ Provides-Extra: progress
28
+ Requires-Dist: tqdm>=4.65; extra == "progress"
29
+ Provides-Extra: all
30
+ Requires-Dist: opencv-python-headless>=4.6; extra == "all"
31
+ Requires-Dist: tqdm>=4.65; extra == "all"
32
+ Dynamic: license-file
33
+
34
+ <p align="center">
35
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="100%" height="400">
36
+ <defs>
37
+ <linearGradient id="leftNodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
38
+ <stop offset="0%" stop-color="#06B6D4" stop-opacity="0.25"/>
39
+ <stop offset="100%" stop-color="#0F172A" stop-opacity="0.8"/>
40
+ </linearGradient>
41
+
42
+ <linearGradient id="rightNodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
43
+ <stop offset="0%" stop-color="#EC4899" stop-opacity="0.3"/>
44
+ <stop offset="100%" stop-color="#06B6D4" stop-opacity="0.15"/>
45
+ </linearGradient>
46
+
47
+ <linearGradient id="bridgeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
48
+ <stop offset="0%" stop-color="#06B6D4"/>
49
+ <stop offset="50%" stop-color="#FFFFFF"/>
50
+ <stop offset="100%" stop-color="#EC4899"/>
51
+ </linearGradient>
52
+
53
+ <filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
54
+ <feGaussianBlur stdDeviation="8" result="blur"/>
55
+ <feMerge>
56
+ <feMergeNode in="blur"/>
57
+ <feMergeNode in="SourceGraphic"/>
58
+ </feMerge>
59
+ </filter>
60
+
61
+ <filter id="glow-subtle" x="-20%" y="-20%" width="140%" height="140%">
62
+ <feGaussianBlur stdDeviation="4" result="blur"/>
63
+ <feMerge>
64
+ <feMergeNode in="blur"/>
65
+ <feMergeNode in="SourceGraphic"/>
66
+ </feMerge>
67
+ </filter>
68
+
69
+ <pattern id="fingerprintGrid" width="16" height="16" patternUnits="userSpaceOnUse">
70
+ <path d="M 16 0 L 0 0 0 16" fill="none" stroke="#06B6D4" stroke-width="0.8" opacity="0.3"/>
71
+ <circle cx="8" cy="8" r="1.5" fill="#06B6D4" opacity="0.4"/>
72
+ </pattern>
73
+ </defs>
74
+
75
+ <circle cx="280" cy="300" r="160" fill="#06B6D4" opacity="0.05" filter="url(#glow)"/>
76
+ <circle cx="520" cy="280" r="170" fill="#EC4899" opacity="0.05" filter="url(#glow)"/>
77
+
78
+ <path d="M 140 420 L 220 420 L 260 460" stroke="#06B6D4" stroke-width="1.5" fill="none" opacity="0.3" stroke-dasharray="4 4"/>
79
+ <path d="M 660 180 L 580 180 L 540 140" stroke="#EC4899" stroke-width="1.5" fill="none" opacity="0.3" stroke-dasharray="4 4"/>
80
+
81
+ <g transform="translate(180, 190)">
82
+ <rect x="0" y="0" width="200" height="240" rx="16" fill="#000000" opacity="0.4"/>
83
+ <rect x="0" y="0" width="200" height="240" rx="16" fill="url(#leftNodeGrad)" stroke="#06B6D4" stroke-width="2" opacity="0.9"/>
84
+ <rect x="16" y="16" width="168" height="208" rx="8" fill="url(#fingerprintGrid)"/>
85
+ <path d="M 40 60 L 90 60 L 130 100 L 130 160 L 80 180" stroke="#06B6D4" stroke-width="1.5" fill="none" opacity="0.6"/>
86
+ <path d="M 90 60 L 70 120 L 140 120" stroke="#06B6D4" stroke-width="1.5" fill="none" stroke-dasharray="2 2" opacity="0.5"/>
87
+ <circle cx="40" cy="60" r="4" fill="#FFFFFF" filter="url(#glow-subtle)"/>
88
+ <circle cx="90" cy="60" r="3.5" fill="#06B6D4"/>
89
+ <circle cx="130" cy="100" r="3.5" fill="#06B6D4"/>
90
+ <circle cx="70" cy="120" r="3.5" fill="#EC4899"/>
91
+ <circle cx="140" cy="120" r="3.5" fill="#06B6D4"/>
92
+ <circle cx="130" cy="160" r="4" fill="#FFFFFF" filter="url(#glow-subtle)"/>
93
+ <circle cx="80" cy="180" r="3.5" fill="#06B6D4"/>
94
+ <path d="M 24 36 L 24 24 L 36 24" stroke="#06B6D4" stroke-width="2" fill="none"/>
95
+ <path d="M 176 36 L 176 24 L 164 24" stroke="#06B6D4" stroke-width="2" fill="none"/>
96
+ <path d="M 24 184 L 24 196 L 36 196" stroke="#06B6D4" stroke-width="2" fill="none"/>
97
+ <path d="M 176 184 L 176 196 L 164 196" stroke="#06B6D4" stroke-width="2" fill="none"/>
98
+ </g>
99
+
100
+ <path d="M 330 330 C 400 420, 440 180, 500 270" stroke="url(#bridgeGrad)" stroke-width="12" fill="none" opacity="0.3" filter="url(#glow)"/>
101
+ <path d="M 320 340 C 390 440, 430 170, 510 260" stroke="url(#bridgeGrad)" stroke-width="4" fill="none" stroke-dasharray="12 6 4 6" stroke-linecap="round" filter="url(#glow-subtle)"/>
102
+ <path d="M 350 290 C 410 230, 430 350, 480 300" stroke="#FFFFFF" stroke-width="1.5" fill="none" stroke-dasharray="6 6" opacity="0.7"/>
103
+
104
+ <circle cx="375" cy="355" r="3" fill="#06B6D4" filter="url(#glow-subtle)"/>
105
+ <circle cx="415" cy="310" r="4.5" fill="#FFFFFF" filter="url(#glow)"/>
106
+ <circle cx="445" cy="255" r="3" fill="#EC4899" filter="url(#glow-subtle)"/>
107
+ <rect x="470" y="270" width="5" height="5" transform="rotate(45 472.5 272.5)" fill="#06B6D4"/>
108
+
109
+ <g transform="translate(420, 150)">
110
+ <rect x="0" y="0" width="220" height="260" rx="16" fill="#000000" opacity="0.5"/>
111
+ <rect x="0" y="0" width="220" height="260" rx="16" fill="url(#rightNodeGrad)" stroke="#EC4899" stroke-width="2.5" opacity="0.95"/>
112
+ <rect x="14" y="14" width="192" height="232" rx="10" fill="#0F172A" stroke="#FFFFFF" stroke-width="1" stroke-opacity="0.2"/>
113
+ <g opacity="0.9">
114
+ <circle cx="150" cy="75" r="18" fill="#EC4899" filter="url(#glow-subtle)"/>
115
+ <circle cx="150" cy="75" r="10" fill="#FFFFFF"/>
116
+ <polygon points="40,190 100,100 160,190" fill="#06B6D4" opacity="0.4"/>
117
+ <polygon points="80,200 140,120 200,200" fill="#EC4899" opacity="0.6"/>
118
+ <polygon points="140,120 200,200 140,200" fill="#FFFFFF" opacity="0.15"/>
119
+ <line x1="14" y1="160" x2="206" y2="160" stroke="#06B6D4" stroke-width="1" opacity="0.4" stroke-dasharray="4 4"/>
120
+ <line x1="80" y1="14" x2="80" y2="246" stroke="#06B6D4" stroke-width="1" opacity="0.2" stroke-dasharray="4 4"/>
121
+ </g>
122
+ <rect x="-5" y="-5" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
123
+ <rect x="215" y="-5" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
124
+ <rect x="-5" y="255" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
125
+ <rect x="215" y="255" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
126
+ </g>
127
+
128
+ <circle cx="320" cy="340" r="6" fill="#06B6D4" stroke="#FFFFFF" stroke-width="2" filter="url(#glow-subtle)"/>
129
+ <circle cx="510" cy="260" r="6" fill="#EC4899" stroke="#FFFFFF" stroke-width="2" filter="url(#glow-subtle)"/>
130
+ </svg>
131
+ </p>
132
+
133
+ # image-bridge-toolkit
134
+
135
+ [![PyPI version](https://img.shields.io/pypi/v/image-bridge-toolkit.svg)](https://pypi.org/project/image-bridge-toolkit/)
136
+ [![License: MIT](https://img.shields.io/badge/Code_License-MIT-yellow.svg)](LICENSE)
137
+ [![License: CC BY-NC-ND 4.0](https://img.shields.io/badge/Docs_License-CC_BY--NC--ND_4.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-nd/4.0/)
138
+
139
+ `image-bridge-toolkit` provides cache-backed perceptual image fingerprinting and cross-dataset bridge matching designed to handle large scale (50,000+ image) libraries. It finds identical or highly similar images across disparate folders while remaining resilient to scale shifts, aspect ratio changes, heavy compression, and intrusive watermarks.
140
+
141
+ ---
142
+
143
+ ## Key Features
144
+
145
+ - **Per-Directory JSON Caching**: Generates and maintains lightweight `image_cache.json` files within each directory, keyed by filename and modification time (`mtime`).
146
+ - **Scale-Invariant Waveform Approximations**: Uses 16-float 1D Discrete Cosine Transform (DCT) grayscale histogram vectors to ignore local watermark text spikes and compression artifacts.
147
+ - **5-Tier Matching Funnel**:
148
+ 1. **Tier 1 (Bitwise pHash Luma + Color)**: Generous 64-bit Hamming filtering to eliminate obvious non-matches.
149
+ 2. **Tier 2 (Histogram Waveform)**: Euclidean distance on 16 low-frequency DCT coefficients to filter lighting distribution changes.
150
+ 3. **Tier 3 (LAB Perceptual Metrics)**: Vector distance across `lab_value`, `lab_hue`, `lab_chroma`, and `lab_warmth`.
151
+ 4. **Tier 4 (Spatial Layout Tie-Breaker)**: 16-point color spatial coordinates down-weighting image outer margins where signatures and borders reside.
152
+ 5. **Tier 5 (Optional SSIM Direct Comparison)**: Full structural similarity pixel comparison on top candidate matches.
153
+ - **Resumable & High Performance**: Output streams as `.jsonl` objects, allowing runs over huge original sets to resume instantly without re-processing.
154
+ - **Zero-Dependency Fallback**: Runs with `OpenCV` if available for acceleration, or falls back to pure `NumPy` + `Pillow`.
155
+
156
+ ---
157
+
158
+ ## Installation
159
+
160
+ Install via PyPI:
161
+
162
+ ```bash
163
+ # Minimal installation (NumPy + Pillow)
164
+ pip install image-bridge-toolkit
165
+
166
+ # Recommended installation (includes OpenCV acceleration & progress bars)
167
+ pip install "image-bridge-toolkit[all]"
168
+
169
+ ```
170
+
171
+ For development:
172
+
173
+ ```bash
174
+ git clone [https://github.com/akingdom/image_bridge_toolkit.git](https://github.com/akingdom/image_bridge_toolkit.git)
175
+ cd image_bridge_toolkit
176
+ pip install -e ".[all]"
177
+
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Usage
183
+
184
+ ### 1. Build or Update Metadata Caches (`imgcache-build`)
185
+
186
+ Walks a root directory, scanning all subdirectories (skipping any directory containing `.ignore_subdir`), and generates or updates `image_cache.json`.
187
+
188
+ ```bash
189
+ # Build cache for a query dataset (e.g., 1,000 watermarked images)
190
+ imgcache-build /path/to/thumbnails_set
191
+
192
+ # Build cache for a target dataset (e.g., 50,000 original images)
193
+ imgcache-build /path/to/originals_set
194
+
195
+ ```
196
+
197
+ ### 2. Match Datasets (`imgmatch-bridge`)
198
+
199
+ Matches images from the query dataset against the target library, generating a bridge mapping file with confidence scores and top candidate lists.
200
+
201
+ ```bash
202
+ imgmatch-bridge /path/to/thumbnails_set /path/to/originals_set \
203
+ -o bridge_map.jsonl \
204
+ --max-hamming 16 \
205
+ --candidate-threshold 0.03 \
206
+ --direct-compare
207
+
208
+ ```
209
+
210
+ #### Command-Line Options
211
+
212
+ | Flag | Default | Description |
213
+ | --- | --- | --- |
214
+ | `-o`, `--output` | `bridge_map.jsonl` | File path for output streaming results. |
215
+ | `--max-hamming` | `16` | Maximum allowed Tier 1 pHash Hamming distance threshold. |
216
+ | `--candidate-threshold` | `0.03` | Percentage band (e.g., 3%) to include close secondary matches. |
217
+ | `--direct-compare` | `False` | Performs Tier 5 pixel-level SSIM re-ranking on candidates. |
218
+
219
+ ---
220
+
221
+ ## Cache Integrity & Updating
222
+
223
+ * **`mtime` Matching**: A cached entry is preserved as long as the file's modification time matches the record in `image_cache.json`.
224
+ * **Partial Recomputation**: If an existing cache entry is missing a newly added metric type (e.g., `hist_waveform`), only the missing calculation is executed; existing valid metrics are preserved.
225
+ * **Corrupt Cache Handling**: Invalid or unparseable JSON files are automatically caught and rebuilt without interrupting directory traversal.
226
+ * **Subdirectory Exclusion**: Placing a `.ignore_subdir` marker inside any directory causes the builder and matcher to ignore that folder and all nested subdirectories.
227
+
228
+ ---
229
+
230
+ ## License & Copyright
231
+
232
+ * **Source Code (`src/`)**: Licensed under the [MIT License](https://www.google.com/search?q=LICENSE).
233
+ * **Documentation & Branding**: Copyright © 2026 Andrew Kingdom. Licensed under the [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0)](https://creativecommons.org/licenses/by-nc-nd/4.0/).
234
+ * **Translation Permission**: Permission is explicitly granted to translate this documentation into other languages, provided that full attribution to Andrew Kingdom is maintained, a direct link to the original repository is included, and no non-linguistic modifications or structural alterations are made to the content.
@@ -0,0 +1,201 @@
1
+ <p align="center">
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="100%" height="400">
3
+ <defs>
4
+ <linearGradient id="leftNodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
5
+ <stop offset="0%" stop-color="#06B6D4" stop-opacity="0.25"/>
6
+ <stop offset="100%" stop-color="#0F172A" stop-opacity="0.8"/>
7
+ </linearGradient>
8
+
9
+ <linearGradient id="rightNodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
10
+ <stop offset="0%" stop-color="#EC4899" stop-opacity="0.3"/>
11
+ <stop offset="100%" stop-color="#06B6D4" stop-opacity="0.15"/>
12
+ </linearGradient>
13
+
14
+ <linearGradient id="bridgeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
15
+ <stop offset="0%" stop-color="#06B6D4"/>
16
+ <stop offset="50%" stop-color="#FFFFFF"/>
17
+ <stop offset="100%" stop-color="#EC4899"/>
18
+ </linearGradient>
19
+
20
+ <filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
21
+ <feGaussianBlur stdDeviation="8" result="blur"/>
22
+ <feMerge>
23
+ <feMergeNode in="blur"/>
24
+ <feMergeNode in="SourceGraphic"/>
25
+ </feMerge>
26
+ </filter>
27
+
28
+ <filter id="glow-subtle" x="-20%" y="-20%" width="140%" height="140%">
29
+ <feGaussianBlur stdDeviation="4" result="blur"/>
30
+ <feMerge>
31
+ <feMergeNode in="blur"/>
32
+ <feMergeNode in="SourceGraphic"/>
33
+ </feMerge>
34
+ </filter>
35
+
36
+ <pattern id="fingerprintGrid" width="16" height="16" patternUnits="userSpaceOnUse">
37
+ <path d="M 16 0 L 0 0 0 16" fill="none" stroke="#06B6D4" stroke-width="0.8" opacity="0.3"/>
38
+ <circle cx="8" cy="8" r="1.5" fill="#06B6D4" opacity="0.4"/>
39
+ </pattern>
40
+ </defs>
41
+
42
+ <circle cx="280" cy="300" r="160" fill="#06B6D4" opacity="0.05" filter="url(#glow)"/>
43
+ <circle cx="520" cy="280" r="170" fill="#EC4899" opacity="0.05" filter="url(#glow)"/>
44
+
45
+ <path d="M 140 420 L 220 420 L 260 460" stroke="#06B6D4" stroke-width="1.5" fill="none" opacity="0.3" stroke-dasharray="4 4"/>
46
+ <path d="M 660 180 L 580 180 L 540 140" stroke="#EC4899" stroke-width="1.5" fill="none" opacity="0.3" stroke-dasharray="4 4"/>
47
+
48
+ <g transform="translate(180, 190)">
49
+ <rect x="0" y="0" width="200" height="240" rx="16" fill="#000000" opacity="0.4"/>
50
+ <rect x="0" y="0" width="200" height="240" rx="16" fill="url(#leftNodeGrad)" stroke="#06B6D4" stroke-width="2" opacity="0.9"/>
51
+ <rect x="16" y="16" width="168" height="208" rx="8" fill="url(#fingerprintGrid)"/>
52
+ <path d="M 40 60 L 90 60 L 130 100 L 130 160 L 80 180" stroke="#06B6D4" stroke-width="1.5" fill="none" opacity="0.6"/>
53
+ <path d="M 90 60 L 70 120 L 140 120" stroke="#06B6D4" stroke-width="1.5" fill="none" stroke-dasharray="2 2" opacity="0.5"/>
54
+ <circle cx="40" cy="60" r="4" fill="#FFFFFF" filter="url(#glow-subtle)"/>
55
+ <circle cx="90" cy="60" r="3.5" fill="#06B6D4"/>
56
+ <circle cx="130" cy="100" r="3.5" fill="#06B6D4"/>
57
+ <circle cx="70" cy="120" r="3.5" fill="#EC4899"/>
58
+ <circle cx="140" cy="120" r="3.5" fill="#06B6D4"/>
59
+ <circle cx="130" cy="160" r="4" fill="#FFFFFF" filter="url(#glow-subtle)"/>
60
+ <circle cx="80" cy="180" r="3.5" fill="#06B6D4"/>
61
+ <path d="M 24 36 L 24 24 L 36 24" stroke="#06B6D4" stroke-width="2" fill="none"/>
62
+ <path d="M 176 36 L 176 24 L 164 24" stroke="#06B6D4" stroke-width="2" fill="none"/>
63
+ <path d="M 24 184 L 24 196 L 36 196" stroke="#06B6D4" stroke-width="2" fill="none"/>
64
+ <path d="M 176 184 L 176 196 L 164 196" stroke="#06B6D4" stroke-width="2" fill="none"/>
65
+ </g>
66
+
67
+ <path d="M 330 330 C 400 420, 440 180, 500 270" stroke="url(#bridgeGrad)" stroke-width="12" fill="none" opacity="0.3" filter="url(#glow)"/>
68
+ <path d="M 320 340 C 390 440, 430 170, 510 260" stroke="url(#bridgeGrad)" stroke-width="4" fill="none" stroke-dasharray="12 6 4 6" stroke-linecap="round" filter="url(#glow-subtle)"/>
69
+ <path d="M 350 290 C 410 230, 430 350, 480 300" stroke="#FFFFFF" stroke-width="1.5" fill="none" stroke-dasharray="6 6" opacity="0.7"/>
70
+
71
+ <circle cx="375" cy="355" r="3" fill="#06B6D4" filter="url(#glow-subtle)"/>
72
+ <circle cx="415" cy="310" r="4.5" fill="#FFFFFF" filter="url(#glow)"/>
73
+ <circle cx="445" cy="255" r="3" fill="#EC4899" filter="url(#glow-subtle)"/>
74
+ <rect x="470" y="270" width="5" height="5" transform="rotate(45 472.5 272.5)" fill="#06B6D4"/>
75
+
76
+ <g transform="translate(420, 150)">
77
+ <rect x="0" y="0" width="220" height="260" rx="16" fill="#000000" opacity="0.5"/>
78
+ <rect x="0" y="0" width="220" height="260" rx="16" fill="url(#rightNodeGrad)" stroke="#EC4899" stroke-width="2.5" opacity="0.95"/>
79
+ <rect x="14" y="14" width="192" height="232" rx="10" fill="#0F172A" stroke="#FFFFFF" stroke-width="1" stroke-opacity="0.2"/>
80
+ <g opacity="0.9">
81
+ <circle cx="150" cy="75" r="18" fill="#EC4899" filter="url(#glow-subtle)"/>
82
+ <circle cx="150" cy="75" r="10" fill="#FFFFFF"/>
83
+ <polygon points="40,190 100,100 160,190" fill="#06B6D4" opacity="0.4"/>
84
+ <polygon points="80,200 140,120 200,200" fill="#EC4899" opacity="0.6"/>
85
+ <polygon points="140,120 200,200 140,200" fill="#FFFFFF" opacity="0.15"/>
86
+ <line x1="14" y1="160" x2="206" y2="160" stroke="#06B6D4" stroke-width="1" opacity="0.4" stroke-dasharray="4 4"/>
87
+ <line x1="80" y1="14" x2="80" y2="246" stroke="#06B6D4" stroke-width="1" opacity="0.2" stroke-dasharray="4 4"/>
88
+ </g>
89
+ <rect x="-5" y="-5" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
90
+ <rect x="215" y="-5" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
91
+ <rect x="-5" y="255" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
92
+ <rect x="215" y="255" width="10" height="10" fill="#FFFFFF" stroke="#EC4899" stroke-width="2"/>
93
+ </g>
94
+
95
+ <circle cx="320" cy="340" r="6" fill="#06B6D4" stroke="#FFFFFF" stroke-width="2" filter="url(#glow-subtle)"/>
96
+ <circle cx="510" cy="260" r="6" fill="#EC4899" stroke="#FFFFFF" stroke-width="2" filter="url(#glow-subtle)"/>
97
+ </svg>
98
+ </p>
99
+
100
+ # image-bridge-toolkit
101
+
102
+ [![PyPI version](https://img.shields.io/pypi/v/image-bridge-toolkit.svg)](https://pypi.org/project/image-bridge-toolkit/)
103
+ [![License: MIT](https://img.shields.io/badge/Code_License-MIT-yellow.svg)](LICENSE)
104
+ [![License: CC BY-NC-ND 4.0](https://img.shields.io/badge/Docs_License-CC_BY--NC--ND_4.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-nd/4.0/)
105
+
106
+ `image-bridge-toolkit` provides cache-backed perceptual image fingerprinting and cross-dataset bridge matching designed to handle large scale (50,000+ image) libraries. It finds identical or highly similar images across disparate folders while remaining resilient to scale shifts, aspect ratio changes, heavy compression, and intrusive watermarks.
107
+
108
+ ---
109
+
110
+ ## Key Features
111
+
112
+ - **Per-Directory JSON Caching**: Generates and maintains lightweight `image_cache.json` files within each directory, keyed by filename and modification time (`mtime`).
113
+ - **Scale-Invariant Waveform Approximations**: Uses 16-float 1D Discrete Cosine Transform (DCT) grayscale histogram vectors to ignore local watermark text spikes and compression artifacts.
114
+ - **5-Tier Matching Funnel**:
115
+ 1. **Tier 1 (Bitwise pHash Luma + Color)**: Generous 64-bit Hamming filtering to eliminate obvious non-matches.
116
+ 2. **Tier 2 (Histogram Waveform)**: Euclidean distance on 16 low-frequency DCT coefficients to filter lighting distribution changes.
117
+ 3. **Tier 3 (LAB Perceptual Metrics)**: Vector distance across `lab_value`, `lab_hue`, `lab_chroma`, and `lab_warmth`.
118
+ 4. **Tier 4 (Spatial Layout Tie-Breaker)**: 16-point color spatial coordinates down-weighting image outer margins where signatures and borders reside.
119
+ 5. **Tier 5 (Optional SSIM Direct Comparison)**: Full structural similarity pixel comparison on top candidate matches.
120
+ - **Resumable & High Performance**: Output streams as `.jsonl` objects, allowing runs over huge original sets to resume instantly without re-processing.
121
+ - **Zero-Dependency Fallback**: Runs with `OpenCV` if available for acceleration, or falls back to pure `NumPy` + `Pillow`.
122
+
123
+ ---
124
+
125
+ ## Installation
126
+
127
+ Install via PyPI:
128
+
129
+ ```bash
130
+ # Minimal installation (NumPy + Pillow)
131
+ pip install image-bridge-toolkit
132
+
133
+ # Recommended installation (includes OpenCV acceleration & progress bars)
134
+ pip install "image-bridge-toolkit[all]"
135
+
136
+ ```
137
+
138
+ For development:
139
+
140
+ ```bash
141
+ git clone [https://github.com/akingdom/image_bridge_toolkit.git](https://github.com/akingdom/image_bridge_toolkit.git)
142
+ cd image_bridge_toolkit
143
+ pip install -e ".[all]"
144
+
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Usage
150
+
151
+ ### 1. Build or Update Metadata Caches (`imgcache-build`)
152
+
153
+ Walks a root directory, scanning all subdirectories (skipping any directory containing `.ignore_subdir`), and generates or updates `image_cache.json`.
154
+
155
+ ```bash
156
+ # Build cache for a query dataset (e.g., 1,000 watermarked images)
157
+ imgcache-build /path/to/thumbnails_set
158
+
159
+ # Build cache for a target dataset (e.g., 50,000 original images)
160
+ imgcache-build /path/to/originals_set
161
+
162
+ ```
163
+
164
+ ### 2. Match Datasets (`imgmatch-bridge`)
165
+
166
+ Matches images from the query dataset against the target library, generating a bridge mapping file with confidence scores and top candidate lists.
167
+
168
+ ```bash
169
+ imgmatch-bridge /path/to/thumbnails_set /path/to/originals_set \
170
+ -o bridge_map.jsonl \
171
+ --max-hamming 16 \
172
+ --candidate-threshold 0.03 \
173
+ --direct-compare
174
+
175
+ ```
176
+
177
+ #### Command-Line Options
178
+
179
+ | Flag | Default | Description |
180
+ | --- | --- | --- |
181
+ | `-o`, `--output` | `bridge_map.jsonl` | File path for output streaming results. |
182
+ | `--max-hamming` | `16` | Maximum allowed Tier 1 pHash Hamming distance threshold. |
183
+ | `--candidate-threshold` | `0.03` | Percentage band (e.g., 3%) to include close secondary matches. |
184
+ | `--direct-compare` | `False` | Performs Tier 5 pixel-level SSIM re-ranking on candidates. |
185
+
186
+ ---
187
+
188
+ ## Cache Integrity & Updating
189
+
190
+ * **`mtime` Matching**: A cached entry is preserved as long as the file's modification time matches the record in `image_cache.json`.
191
+ * **Partial Recomputation**: If an existing cache entry is missing a newly added metric type (e.g., `hist_waveform`), only the missing calculation is executed; existing valid metrics are preserved.
192
+ * **Corrupt Cache Handling**: Invalid or unparseable JSON files are automatically caught and rebuilt without interrupting directory traversal.
193
+ * **Subdirectory Exclusion**: Placing a `.ignore_subdir` marker inside any directory causes the builder and matcher to ignore that folder and all nested subdirectories.
194
+
195
+ ---
196
+
197
+ ## License & Copyright
198
+
199
+ * **Source Code (`src/`)**: Licensed under the [MIT License](https://www.google.com/search?q=LICENSE).
200
+ * **Documentation & Branding**: Copyright © 2026 Andrew Kingdom. Licensed under the [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0)](https://creativecommons.org/licenses/by-nc-nd/4.0/).
201
+ * **Translation Permission**: Permission is explicitly granted to translate this documentation into other languages, provided that full attribution to Andrew Kingdom is maintained, a direct link to the original repository is included, and no non-linguistic modifications or structural alterations are made to the content.
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "image-bridge-toolkit"
7
+ version = "0.1.0"
8
+ description = "Cache-backed perceptual image fingerprinting and cross-dataset bridge matching."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT for Code / CC BY-NC-ND 4.0 for Documentation" }
12
+ authors = [
13
+ { name = "Andrew Kingdom" }
14
+ ]
15
+ keywords = [
16
+ "image-processing",
17
+ "perceptual-hash",
18
+ "phash",
19
+ "deduplication",
20
+ "dct",
21
+ "computer-vision",
22
+ "reverse-image-search"
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "Topic :: Scientific/Engineering :: Image Processing",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.9",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ ]
35
+ dependencies = [
36
+ "numpy>=1.23",
37
+ "Pillow>=9.0",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/akingdom/image_bridge_toolkit"
42
+ Repository = "https://github.com/akingdom/image_bridge_toolkit"
43
+ Issues = "https://github.com/akingdom/image_bridge_toolkit/issues"
44
+
45
+ [project.optional-dependencies]
46
+ opencv = ["opencv-python-headless>=4.6"]
47
+ progress = ["tqdm>=4.65"]
48
+ all = ["opencv-python-headless>=4.6", "tqdm>=4.65"]
49
+
50
+ [project.scripts]
51
+ imgcache-build = "image_bridge_toolkit.cache_builder:main"
52
+ imgmatch-bridge = "image_bridge_toolkit.matcher:main"
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """image_bridge_toolkit: perceptual image fingerprint cache + cross-dataset bridge matcher."""
2
+ from ._imaging_backend import HAS_OPENCV
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["HAS_OPENCV", "__version__"]
@@ -0,0 +1,12 @@
1
+ """
2
+ Single source of truth for the optional OpenCV backend.
3
+
4
+ Every other module imports HAS_OPENCV / cv2 from here instead of doing its
5
+ own try/except, so the fallback decision is made exactly once per process.
6
+ """
7
+ try:
8
+ import cv2 # noqa: F401
9
+ HAS_OPENCV = True
10
+ except ImportError: # pragma: no cover - exercised only when cv2 absent
11
+ cv2 = None
12
+ HAS_OPENCV = False