kdraw 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.
- kdraw-0.1.0/LICENSE +21 -0
- kdraw-0.1.0/PKG-INFO +635 -0
- kdraw-0.1.0/README.md +612 -0
- kdraw-0.1.0/kdraw/__init__.py +11 -0
- kdraw-0.1.0/kdraw/centerline.py +199 -0
- kdraw-0.1.0/kdraw/cli.py +161 -0
- kdraw-0.1.0/kdraw/graph.py +263 -0
- kdraw-0.1.0/kdraw/optimization.py +68 -0
- kdraw-0.1.0/kdraw/pixel_perfect.py +128 -0
- kdraw-0.1.0/kdraw/plotter.py +124 -0
- kdraw-0.1.0/kdraw/smooth.py +34 -0
- kdraw-0.1.0/kdraw/smoothing.py +62 -0
- kdraw-0.1.0/kdraw.egg-info/PKG-INFO +635 -0
- kdraw-0.1.0/kdraw.egg-info/SOURCES.txt +18 -0
- kdraw-0.1.0/kdraw.egg-info/dependency_links.txt +1 -0
- kdraw-0.1.0/kdraw.egg-info/entry_points.txt +2 -0
- kdraw-0.1.0/kdraw.egg-info/requires.txt +7 -0
- kdraw-0.1.0/kdraw.egg-info/top_level.txt +1 -0
- kdraw-0.1.0/pyproject.toml +41 -0
- kdraw-0.1.0/setup.cfg +4 -0
kdraw-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GumokuCat
|
|
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.
|
kdraw-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kdraw
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Topological Centerline SVG Vectorizer for CNC plotters, laser cutters, and CAM software.
|
|
5
|
+
Author: Ervin James P. Regio
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/anonymouschichvy/kdraw
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/anonymouschichvy/kdraw/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.20.0
|
|
17
|
+
Requires-Dist: pillow>=9.0.0
|
|
18
|
+
Requires-Dist: opencv-python>=4.5.0
|
|
19
|
+
Requires-Dist: scikit-image>=0.19.0
|
|
20
|
+
Provides-Extra: smooth
|
|
21
|
+
Requires-Dist: vtracer>=0.6.0; extra == "smooth"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# <p align="center"> <img src="http://kvenjoy.com/images/draw/icon.png" alt="logo" style="max-width:50%; height:300px;" /> </p>
|
|
25
|
+
# <div align="center">KDRAW: Topological Centerline SVG Vectorizer</div>
|
|
26
|
+
<div align="center">
|
|
27
|
+
<strong>KDRAW is a high-precision topological centerline vectorizer that converts raster graphics into optimized, smooth single-stroke SVGs for CNC plotters, laser cutters, and CAM software.</strong>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<br />
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
<img src="https://img.shields.io/badge/Render-Centerline-blueviolet?style=for-the-badge&logo=visual-studio-code" alt="Centerline Mode" />
|
|
34
|
+
<img src="https://img.shields.io/badge/Optimize-TSP%20Greedy-success?style=for-the-badge&logo=python" alt="TSP Optimization" />
|
|
35
|
+
<img src="https://img.shields.io/badge/Smooth-Chaikin%20Subdivision-orange?style=for-the-badge&logo=scipy" alt="Chaikin Smoothing" />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📸 Visual Documentation & Evidence
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### 💻 Running the Vectorizer
|
|
44
|
+
To convert a text image into optimized single-line vectors using custom configuration:
|
|
45
|
+
```bash
|
|
46
|
+
python main.py input.jpg output_centerline.svg --centerline --no-adaptive --morph-close 5 --min-spur 1 --upscale 8 --morph-close 5
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Here is the visual evidence of the conversion from the high-resolution raster image ([input.jpg](https://github.com/anonymouschichvy/kdraw/blob/main/docs/input.jpg)) to the thinned centerline stroke paths ([output_centerline.svg](https://github.com/anonymouschichvy/kdraw/blob/main/docs/output_centerline.svg)).
|
|
50
|
+
|
|
51
|
+
### 1. Full-Page Comparison (Input vs. SVG Output)
|
|
52
|
+
Below is the full-page overview comparison. The left shows the original raster text ([input.jpg](https://github.com/anonymouschichvy/kdraw/blob/main/docs/input.jpg)) and the right shows the generated thinned centerline paths ([output_centerline.svg](https://github.com/anonymouschichvy/kdraw/blob/main/docs/output_centerline.svg)).
|
|
53
|
+
# <p align="center"> <img src="https://raw.githubusercontent.com/anonymouschichvy/kdraw/main/docs/comparison_full.png" alt="Full Page Comparison" style="max-width:100%;"/> </p>
|
|
54
|
+
|
|
55
|
+
### 1.1 Full-Page Comparison Drawing (Input vs. SVG Output)
|
|
56
|
+
Below is the full-page drawing overview comparison. The left shows the original raster drawing ([input_draw.png](https://github.com/anonymouschichvy/kdraw/blob/main/docs/input_draw.png)) and the right shows the generated thinned centerline paths ([output_centerline.svg](https://github.com/anonymouschichvy/kdraw/blob/main/docs/output_centerline.svg)).
|
|
57
|
+
# <p align="center"> <img src="https://raw.githubusercontent.com/anonymouschichvy/kdraw/main/docs/comparison_draw.png" alt="Full Drawing Comparison" style="max-width:90%;"/> </p>
|
|
58
|
+
|
|
59
|
+
### 2. Zoomed-In Details & Loop Preservation
|
|
60
|
+
To prevent plotters from bleeding ink and closing loops, KDRAW's pre-smoothing keeps character loops (`a`, `e`, `o`, `u`) perfectly open. The left shows the input pixels and the right shows the single-line thinned paths.
|
|
61
|
+
|
|
62
|
+
#### Region 0: Title and Introduction Text
|
|
63
|
+
# <p align="center"> <img src="https://raw.githubusercontent.com/anonymouschichvy/kdraw/main/docs/comparison_region0.png" alt="Region 0 Zoom" style="max-width:100%;"/> </p>
|
|
64
|
+
|
|
65
|
+
#### Region 1: Body Details (Dots of `i` & Colons)
|
|
66
|
+
Observe how the dots of the letter `i` and colons are preserved as independent, clean path strokes rather than being merged or pruned:
|
|
67
|
+
# <p align="center"> <img src="https://raw.githubusercontent.com/anonymouschichvy/kdraw/main/docs/comparison_region1.png" alt="Region 1 Zoom" style="max-width:100%;"/> </p>
|
|
68
|
+
|
|
69
|
+
### 2.1 Zoomed-In Details & Loop Preservation
|
|
70
|
+
Observe how the lines are preserved as independent, clean path strokes rather than being merged or pruned:
|
|
71
|
+
# <p align="center"> <img src="https://raw.githubusercontent.com/anonymouschichvy/kdraw/main/docs/zoom_comparison_draw.png" alt="Drawing Zoom" style="max-width:100%;"/> </p>
|
|
72
|
+
|
|
73
|
+
## ⚡ Key Highlights & Core Capabilities
|
|
74
|
+
|
|
75
|
+
* **🧩 Graph-Based Skeleton Tracing**: Represents the skeleton as a topological graph of nodes (junctions/endpoints) and edges. Prevents junction distortion and splits.
|
|
76
|
+
* **🔎 4x Upscaled Anti-Aliasing**: Interpolates and smooths low-resolution input images before skeletonization to eliminate pixel-level wiggles.
|
|
77
|
+
* **🛡️ Isolated Path Safety (i-Dot Preservation)**: Distinguishes between side spurs (noise) and isolated paths, ensuring colons, periods, and the dots of `i` are never pruned.
|
|
78
|
+
* **🌀 Chaikin Curve Fitting**: Corner-cutting curve smoothing that rounds out characters organic-style without coordinate shrinkage.
|
|
79
|
+
* **🏎️ TSP Pen-Travel Optimization**: Solves the Travelling Salesperson Problem (TSP) on the path sequence to save up to **98% of pen-up travel distance**.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 🛠️ The Visual Pipeline
|
|
84
|
+
|
|
85
|
+
```mermaid
|
|
86
|
+
graph TD
|
|
87
|
+
A[Raster Image input.jpg] --> B[4x Cubic Upscaling]
|
|
88
|
+
B --> C[Gaussian Blur 9x9]
|
|
89
|
+
C --> D[Otsu Binarization]
|
|
90
|
+
D --> E[Morphological Closing 5x5]
|
|
91
|
+
E --> F[Skeletonization]
|
|
92
|
+
F --> G[Graph Extraction & Pruning]
|
|
93
|
+
G --> H[Chaikin Path Smoothing]
|
|
94
|
+
H --> I[TSP Sort & Max-Join]
|
|
95
|
+
I --> J[Stroke SVG output.svg]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 📖 Complete Code Logic & Detailed Algorithms
|
|
101
|
+
|
|
102
|
+
Below is the exhaustive pseudocode and logic breakdown of every helper and processing routine in the KDRAW engine (`main.py` and the `kdraw` package).
|
|
103
|
+
|
|
104
|
+
### 1. `get_hex_color(val, has_alpha)`
|
|
105
|
+
|
|
106
|
+
**Input:** Packed 32-bit pixel value `val`, transparency flag `has_alpha`
|
|
107
|
+
**Output:** Hex color string (`#RRGGBB`) or CSS RGBA string (`rgba(...)`)
|
|
108
|
+
|
|
109
|
+
#### Color Channel Extraction
|
|
110
|
+
|
|
111
|
+
Given a packed ARGB pixel:
|
|
112
|
+
|
|
113
|
+
```math
|
|
114
|
+
\text{val}
|
|
115
|
+
=
|
|
116
|
+
(A \ll 24)
|
|
117
|
+
+
|
|
118
|
+
(R \ll 16)
|
|
119
|
+
+
|
|
120
|
+
(G \ll 8)
|
|
121
|
+
+
|
|
122
|
+
B
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Extract each channel using bitwise operations:
|
|
126
|
+
|
|
127
|
+
```math
|
|
128
|
+
A
|
|
129
|
+
=
|
|
130
|
+
(\text{val} \gg 24)
|
|
131
|
+
\;\&\;
|
|
132
|
+
255
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```math
|
|
136
|
+
R
|
|
137
|
+
=
|
|
138
|
+
(\text{val} \gg 16)
|
|
139
|
+
\;\&\;
|
|
140
|
+
255
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```math
|
|
144
|
+
G
|
|
145
|
+
=
|
|
146
|
+
(\text{val} \gg 8)
|
|
147
|
+
\;\&\;
|
|
148
|
+
255
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```math
|
|
152
|
+
B
|
|
153
|
+
=
|
|
154
|
+
\text{val}
|
|
155
|
+
\;\&\;
|
|
156
|
+
255
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
where:
|
|
160
|
+
|
|
161
|
+
```math
|
|
162
|
+
0 \le A,R,G,B \le 255
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Alpha Normalization
|
|
166
|
+
|
|
167
|
+
When transparency is enabled, convert the alpha channel to the CSS opacity range:
|
|
168
|
+
|
|
169
|
+
```math
|
|
170
|
+
\alpha
|
|
171
|
+
=
|
|
172
|
+
\frac{A}{255}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
with:
|
|
176
|
+
|
|
177
|
+
```math
|
|
178
|
+
0 \le \alpha \le 1
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Output Selection
|
|
182
|
+
|
|
183
|
+
If transparency is present:
|
|
184
|
+
|
|
185
|
+
```math
|
|
186
|
+
\text{has\_alpha}
|
|
187
|
+
\land
|
|
188
|
+
A < 255
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
return:
|
|
192
|
+
|
|
193
|
+
```math
|
|
194
|
+
\text{rgba}(R,G,B,\alpha)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Otherwise return:
|
|
198
|
+
|
|
199
|
+
```math
|
|
200
|
+
\#RRGGBB
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
where:
|
|
204
|
+
|
|
205
|
+
```math
|
|
206
|
+
RRGGBB
|
|
207
|
+
=
|
|
208
|
+
\text{hex}(R)
|
|
209
|
+
\;||\;
|
|
210
|
+
\text{hex}(G)
|
|
211
|
+
\;||\;
|
|
212
|
+
\text{hex}(B)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
and \(||\) denotes string concatenation.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 2. `smooth_paths_laplacian(path, iterations, weight)`
|
|
220
|
+
|
|
221
|
+
**Input:** Curve coordinate array `path`, iteration count `iterations`, smoothing weight `w`
|
|
222
|
+
**Output:** Laplacian-smoothed coordinate array
|
|
223
|
+
|
|
224
|
+
#### Laplacian Smoothing Model
|
|
225
|
+
|
|
226
|
+
For each vertex \( \mathbf{p}_i \), compute the local neighborhood average:
|
|
227
|
+
|
|
228
|
+
```math
|
|
229
|
+
\mathbf{m}_i
|
|
230
|
+
=
|
|
231
|
+
\frac{
|
|
232
|
+
\mathbf{p}_{i-1}
|
|
233
|
+
+
|
|
234
|
+
\mathbf{p}_{i+1}
|
|
235
|
+
}{2}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The updated position is a weighted blend between the original point and its neighborhood mean:
|
|
239
|
+
|
|
240
|
+
```math
|
|
241
|
+
\mathbf{p}_i'
|
|
242
|
+
=
|
|
243
|
+
(1-w)\mathbf{p}_i
|
|
244
|
+
+
|
|
245
|
+
w\mathbf{m}_i
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Substituting the neighborhood average:
|
|
249
|
+
|
|
250
|
+
```math
|
|
251
|
+
\mathbf{p}_i'
|
|
252
|
+
=
|
|
253
|
+
(1-w)\mathbf{p}_i
|
|
254
|
+
+
|
|
255
|
+
w
|
|
256
|
+
\left(
|
|
257
|
+
\frac{
|
|
258
|
+
\mathbf{p}_{i-1}
|
|
259
|
+
+
|
|
260
|
+
\mathbf{p}_{i+1}
|
|
261
|
+
}{2}
|
|
262
|
+
\right)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
where:
|
|
266
|
+
|
|
267
|
+
```math
|
|
268
|
+
0 \le w \le 1
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### Interpretation
|
|
272
|
+
|
|
273
|
+
Special cases:
|
|
274
|
+
|
|
275
|
+
```math
|
|
276
|
+
w = 0
|
|
277
|
+
\quad\Rightarrow\quad
|
|
278
|
+
\mathbf{p}_i' = \mathbf{p}_i
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
(No smoothing)
|
|
282
|
+
|
|
283
|
+
```math
|
|
284
|
+
w = 1
|
|
285
|
+
\quad\Rightarrow\quad
|
|
286
|
+
\mathbf{p}_i'
|
|
287
|
+
=
|
|
288
|
+
\frac{
|
|
289
|
+
\mathbf{p}_{i-1}
|
|
290
|
+
+
|
|
291
|
+
\mathbf{p}_{i+1}
|
|
292
|
+
}{2}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
(Complete neighborhood averaging)
|
|
296
|
+
|
|
297
|
+
For intermediate values:
|
|
298
|
+
|
|
299
|
+
```math
|
|
300
|
+
0 < w < 1
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
the vertex moves proportionally toward the average of its neighboring vertices, reducing local curvature and noise while preserving the overall shape.
|
|
304
|
+
* **Logic**:
|
|
305
|
+
1. If path has less than 3 points, return original path.
|
|
306
|
+
### 2. `smooth_paths_laplacian(path, iterations, w)`
|
|
307
|
+
|
|
308
|
+
**Input:** Coordinate array `path`, iteration count `iterations`, smoothing weight `w`
|
|
309
|
+
**Output:** Laplacian-smoothed coordinate array
|
|
310
|
+
|
|
311
|
+
#### Algorithm
|
|
312
|
+
|
|
313
|
+
1. If the path contains fewer than **3 points**, return the original path.
|
|
314
|
+
|
|
315
|
+
2. Determine whether the path is closed:
|
|
316
|
+
|
|
317
|
+
```math
|
|
318
|
+
\text{is\_closed}
|
|
319
|
+
=
|
|
320
|
+
\left\|
|
|
321
|
+
\mathbf{p}_0 - \mathbf{p}_{n-1}
|
|
322
|
+
\right\|
|
|
323
|
+
< 1.0
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
3. Repeat for each smoothing iteration:
|
|
327
|
+
|
|
328
|
+
- Create a temporary copy of the coordinate array.
|
|
329
|
+
- Apply the update rules below.
|
|
330
|
+
|
|
331
|
+
##### Closed Path
|
|
332
|
+
|
|
333
|
+
For each vertex \( \mathbf{p}_i \) (excluding the duplicated endpoint):
|
|
334
|
+
|
|
335
|
+
```math
|
|
336
|
+
\mathbf{p}_i'
|
|
337
|
+
=
|
|
338
|
+
(1-w)\mathbf{p}_i
|
|
339
|
+
+
|
|
340
|
+
w
|
|
341
|
+
\left(
|
|
342
|
+
\frac{\mathbf{p}_{i-1} + \mathbf{p}_{i+1}}{2}
|
|
343
|
+
\right)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
with cyclic indexing:
|
|
347
|
+
|
|
348
|
+
```math
|
|
349
|
+
\mathbf{p}_{i-1}
|
|
350
|
+
=
|
|
351
|
+
\mathbf{p}_{(i-1)\bmod n}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```math
|
|
355
|
+
\mathbf{p}_{i+1}
|
|
356
|
+
=
|
|
357
|
+
\mathbf{p}_{(i+1)\bmod n}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Maintain closure after updating:
|
|
361
|
+
|
|
362
|
+
```math
|
|
363
|
+
\mathbf{p}_{n-1}
|
|
364
|
+
=
|
|
365
|
+
\mathbf{p}_0
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
##### Open Path
|
|
369
|
+
|
|
370
|
+
Keep endpoints fixed and update interior vertices:
|
|
371
|
+
|
|
372
|
+
```math
|
|
373
|
+
i = 1, 2, \ldots, n-2
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
```math
|
|
377
|
+
\mathbf{p}_i'
|
|
378
|
+
=
|
|
379
|
+
(1-w)\mathbf{p}_i
|
|
380
|
+
+
|
|
381
|
+
w
|
|
382
|
+
\left(
|
|
383
|
+
\frac{\mathbf{p}_{i-1} + \mathbf{p}_{i+1}}{2}
|
|
384
|
+
\right)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### 3. `smooth_paths_chaikin(path, iterations)`
|
|
390
|
+
|
|
391
|
+
**Input:** Coordinate array `path`, iteration count `iterations`
|
|
392
|
+
**Output:** Chaikin corner-cut smoothed coordinate array
|
|
393
|
+
|
|
394
|
+
#### Algorithm
|
|
395
|
+
|
|
396
|
+
1. If the path contains fewer than **3 points**, return the original path.
|
|
397
|
+
|
|
398
|
+
2. Repeat for each iteration.
|
|
399
|
+
|
|
400
|
+
##### Closed Path
|
|
401
|
+
|
|
402
|
+
For every segment:
|
|
403
|
+
|
|
404
|
+
```math
|
|
405
|
+
[\mathbf{p}_i,\mathbf{p}_{i+1}]
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Generate:
|
|
409
|
+
|
|
410
|
+
```math
|
|
411
|
+
\mathbf{q}
|
|
412
|
+
=
|
|
413
|
+
0.75\,\mathbf{p}_i
|
|
414
|
+
+
|
|
415
|
+
0.25\,\mathbf{p}_{i+1}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
```math
|
|
419
|
+
\mathbf{r}
|
|
420
|
+
=
|
|
421
|
+
0.25\,\mathbf{p}_i
|
|
422
|
+
+
|
|
423
|
+
0.75\,\mathbf{p}_{i+1}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Append the first generated point to the end of the sequence to preserve closure.
|
|
427
|
+
|
|
428
|
+
##### Open Path
|
|
429
|
+
|
|
430
|
+
Preserve endpoints:
|
|
431
|
+
|
|
432
|
+
```math
|
|
433
|
+
\mathbf{p}_0
|
|
434
|
+
\qquad\text{and}\qquad
|
|
435
|
+
\mathbf{p}_{n-1}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
For each interior segment:
|
|
439
|
+
|
|
440
|
+
```math
|
|
441
|
+
[\mathbf{p}_i,\mathbf{p}_{i+1}]
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Generate:
|
|
445
|
+
|
|
446
|
+
```math
|
|
447
|
+
\mathbf{q}
|
|
448
|
+
=
|
|
449
|
+
0.75\,\mathbf{p}_i
|
|
450
|
+
+
|
|
451
|
+
0.25\,\mathbf{p}_{i+1}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
```math
|
|
455
|
+
\mathbf{r}
|
|
456
|
+
=
|
|
457
|
+
0.25\,\mathbf{p}_i
|
|
458
|
+
+
|
|
459
|
+
0.75\,\mathbf{p}_{i+1}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Resulting point sequence:
|
|
463
|
+
|
|
464
|
+
```math
|
|
465
|
+
[
|
|
466
|
+
\mathbf{p}_0,\,
|
|
467
|
+
\mathbf{q}_1,\,
|
|
468
|
+
\mathbf{r}_1,\,
|
|
469
|
+
\mathbf{q}_2,\,
|
|
470
|
+
\mathbf{r}_2,\,
|
|
471
|
+
\dots,\,
|
|
472
|
+
\mathbf{p}_{n-1}
|
|
473
|
+
]
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### Chaikin Corner-Cutting Rule
|
|
477
|
+
|
|
478
|
+
For a segment connecting points \( \mathbf{A} \) and \( \mathbf{B} \):
|
|
479
|
+
|
|
480
|
+
```math
|
|
481
|
+
\mathbf{Q}
|
|
482
|
+
=
|
|
483
|
+
\frac{3}{4}\mathbf{A}
|
|
484
|
+
+
|
|
485
|
+
\frac{1}{4}\mathbf{B}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
```math
|
|
489
|
+
\mathbf{R}
|
|
490
|
+
=
|
|
491
|
+
\frac{1}{4}\mathbf{A}
|
|
492
|
+
+
|
|
493
|
+
\frac{3}{4}\mathbf{B}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Repeated application progressively removes sharp corners and converges toward a smooth curve.
|
|
497
|
+
|
|
498
|
+
### 4. `optimize_paths(contours, max_join_dist)`
|
|
499
|
+
* **Input**: List of curves `contours`, pen-down merging threshold `max_join_dist`
|
|
500
|
+
* **Output**: Sorted and merged curves list, original travel distance, optimized travel distance
|
|
501
|
+
* **Logic**:
|
|
502
|
+
1. Convert all contours to NumPy float arrays. Calculate baseline sequential pen travel.
|
|
503
|
+
2. Implement a greedy Travelling Salesperson (TSP) heuristic:
|
|
504
|
+
- Pop the first contour as the active path.
|
|
505
|
+
- While remaining contours exist:
|
|
506
|
+
- Find the distances from the active path's endpoint to the start and endpoints of all remaining contours.
|
|
507
|
+
- Identify the closest coordinate point.
|
|
508
|
+
- If the closest point belongs to the end of a contour, reverse that contour.
|
|
509
|
+
- If the distance to the closest contour is \(\le max\_join\_dist\), extend the active path coordinates directly with the closest contour coordinates (merging).
|
|
510
|
+
- Otherwise, append the active path to the optimized list and set the closest contour as the new active path.
|
|
511
|
+
- Append the final active path.
|
|
512
|
+
|
|
513
|
+
### 5. `build_and_prune_graph(skel_bool, min_spur_length, collapse_dist)`
|
|
514
|
+
* **Input**: Binary skeleton image `skel_bool`, spur limit `min_spur_length`, merge radius `collapse_dist`
|
|
515
|
+
* **Output**: List of cleaned, continuous centerline coordinate paths
|
|
516
|
+
* **Logic**:
|
|
517
|
+
1. Retrieve skeleton coordinates: `pixels = set(zip(*np.where(skel_bool)))`.
|
|
518
|
+
2. Compute 8-connected adjacency dictionary: `adj = {p: get_neighbors(p, pixels) for p in pixels}`.
|
|
519
|
+
3. Classify pixels:
|
|
520
|
+
- `endpoints` (neighbors == 1)
|
|
521
|
+
- `junctions` (neighbors >= 3)
|
|
522
|
+
- `regular` (neighbors == 2)
|
|
523
|
+
4. Cluster contiguous junction pixels using BFS. Each connected component of junction pixels forms a singular "super-junction" node.
|
|
524
|
+
5. Assign node IDs to all endpoints and junction clusters. Build `pixel_to_node` map.
|
|
525
|
+
6. Trace edges:
|
|
526
|
+
- For each node:
|
|
527
|
+
- If a neighbor is a regular pixel, trace along regular pixels (BFS) until hitting any node. Create a stroke edge.
|
|
528
|
+
- If a neighbor is directly in another node, create a direct node-to-node edge of length 2 (essential for preserving i-dots).
|
|
529
|
+
7. Locate isolated cycles (loops with no nodes, degree-2 only like in the letter `o`). Convert to closed loop edges.
|
|
530
|
+
8. Perform iterative topology reductions:
|
|
531
|
+
- **Spur Check**: If an edge connects an endpoint (degree 1) to a junction (degree >= 3), and its pixel length is \(< min\_spur\_length\), delete the edge.
|
|
532
|
+
- **Isolated Check**: If an edge connects two endpoints directly (degree 1 to 1), it is an isolated dot. Protect it from spur pruning.
|
|
533
|
+
- **Junction Collapse**: If an edge connects two junction nodes and is shorter than `collapse_dist`, merge the two junction nodes and update all matching edge node IDs.
|
|
534
|
+
9. Clean up: For any node left with degree 2 (exactly two edges), merge the paths of the two edges into a single edge.
|
|
535
|
+
|
|
536
|
+
### 6. `convert_centerline(...)`
|
|
537
|
+
* **Input**: File paths and all tuning thresholds (`upscale_factor`, `blur_size`, etc.)
|
|
538
|
+
* **Output**: Stroke-only SVG file containing centerline paths
|
|
539
|
+
* **Logic**:
|
|
540
|
+
1. Load input image. If `upscale_factor > 1`, upscale using `cv2.resize` with bicubic interpolation.
|
|
541
|
+
2. Apply Gaussian blur of size `blur_size` (only odd dimensions allowed).
|
|
542
|
+
3. Binarize:
|
|
543
|
+
- If `use_adaptive`: Apply local adaptive Gaussian thresholding using `cv2.adaptiveThreshold` with `block_size` and subtraction constant `c_val`.
|
|
544
|
+
- Else: Apply Otsu's thresholding using `cv2.threshold`.
|
|
545
|
+
4. Morphological filters: Apply closing and opening operations using an elliptical structuring element on the binary mask.
|
|
546
|
+
5. Thin binary mask to a single-pixel centerline using morphological `skeletonize` (Zhang-Suen/Lee).
|
|
547
|
+
6. Call `build_and_prune_graph` to trace skeleton pixels into a clean set of coordinate paths.
|
|
548
|
+
7. Downscale coordinate values by `upscale_factor` to match original image dimensions.
|
|
549
|
+
8. For each path:
|
|
550
|
+
- If the path has only 2 points, bypass simplification.
|
|
551
|
+
- Otherwise, simplify coordinates using RDP (`cv2.approxPolyDP`) with tolerance `epsilon`.
|
|
552
|
+
9. Apply smoothing: If `smooth_iters > 0`, call `smooth_paths_chaikin` or `smooth_paths_laplacian` for `smooth_iters` iterations.
|
|
553
|
+
10. Decimate coordinates post-smoothing using RDP with a tight tolerance `smooth_decimate`.
|
|
554
|
+
11. Call `optimize_paths` with `max_join` to minimize travel sequence.
|
|
555
|
+
12. Format and write paths into SVG XML nodes containing `<path d="..." fill="none" stroke="black" ... />`.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## 🚀 Quick Start
|
|
560
|
+
|
|
561
|
+
### 📦 Installation
|
|
562
|
+
Ensure you have the required libraries installed:
|
|
563
|
+
```bash
|
|
564
|
+
pip install opencv-python scikit-image numpy pillow
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### 💻 Running the Vectorizer
|
|
568
|
+
To convert a text image into optimized single-line vectors using the recommended defaults:
|
|
569
|
+
```bash
|
|
570
|
+
python main.py input.jpg output_centerline.svg --centerline --no-adaptive
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## 📊 Parameters & Customization
|
|
576
|
+
|
|
577
|
+
| CLI Argument | Type | Default | Description |
|
|
578
|
+
| :--- | :---: | :---: | :--- |
|
|
579
|
+
| `--centerline` / `-cl` | flag | `False` | Enables single-stroke skeletonization (eliminates bubble outlines). |
|
|
580
|
+
| `--upscale` | `int` | `4` | Upscaling factor to smooth boundaries before tracing. |
|
|
581
|
+
| `--blur` | `int` | `9` | Pre-threshold Gaussian blur size to remove staircase wiggles. |
|
|
582
|
+
| `--no-adaptive` | flag | `False` | Disables adaptive thresholding (uses Otsu global thresholding, preserving loops). |
|
|
583
|
+
| `--morph-close` | `int` | `5` | Fills in tiny gaps on thin stroke contours. |
|
|
584
|
+
| `--min-spur` | `int` | `16` | Minimum pixel length of a branch to not be pruned as a spur. |
|
|
585
|
+
| `--collapse-junc` | `int` | `8` | Merges adjacent junctions to straighten line joints. |
|
|
586
|
+
| `--max-join` | `float` | `2.5` | Binds path ends within this distance to avoid lifting the pen. |
|
|
587
|
+
| `--smooth-iters` | `int` | `3` | Number of Chaikin smoothing iterations. |
|
|
588
|
+
| `--smooth-decimate` | `float` | `0.1` | Post-smoothing RDP decimation to minimize point count. |
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## 📈 Quality & Performance Metrics
|
|
593
|
+
|
|
594
|
+
Running KDRAW with the optimal centerline defaults provides a massive boost in vector quality and plotter throughput:
|
|
595
|
+
|
|
596
|
+
> [!IMPORTANT]
|
|
597
|
+
> **TSP Optimization saves up to 98% of pen-up travel**, reducing wear and tear on plotter belts and servos.
|
|
598
|
+
|
|
599
|
+
| Metric | Raw Skeleton Trace | KDRAW Graph Pipeline | Improvement |
|
|
600
|
+
| :--- | :---: | :---: | :---: |
|
|
601
|
+
| **Path Count (Pen Lifts)** | 2,468 | **2,071** | **16.1% fewer lifts** |
|
|
602
|
+
| **Pen-Up Travel Distance** | 1,684,002 px | **35,425 px** | **97.9% distance saved** |
|
|
603
|
+
| **Average Angle Change** | 49.9° | **17.4°** | **Curves are 2.8x smoother** |
|
|
604
|
+
| **Punctuation & Dots** | Lost / Jagged | **Perfectly Preserved** | Flawless |
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## 💖 Donate & Support
|
|
609
|
+
|
|
610
|
+
If you find this project useful and would like to support the deployment of FishTrack buoys for coastal fishing communities, donations are greatly appreciated!
|
|
611
|
+
|
|
612
|
+
<p align="left">
|
|
613
|
+
<a href="bitcoin:13zWnp2ty3NPzAXX9QxwEeoPSKhN5tPzic">
|
|
614
|
+
<img src="https://img.shields.io/badge/Donate-Bitcoin-F7931A?style=for-the-badge&logo=bitcoin&logoColor=white" alt="Donate Bitcoin" />
|
|
615
|
+
</a>
|
|
616
|
+
</p>
|
|
617
|
+
|
|
618
|
+
**Bitcoin Address:** `13zWnp2ty3NPzAXX9QxwEeoPSKhN5tPzic`
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## 📜 License
|
|
623
|
+
MIT License. Open-source vector engine.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## Star History
|
|
628
|
+
|
|
629
|
+
<a href="https://www.star-history.com/?repos=anonymouschichvy%2FKDRAW&type=date&legend=top-left">
|
|
630
|
+
<picture>
|
|
631
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=anonymouschichvy/KDRAW&type=date&theme=dark&legend=top-left" />
|
|
632
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=anonymouschichvy/KDRAW&type=date&legend=top-left" />
|
|
633
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=anonymouschichvy/KDRAW&type=date&legend=top-left" />
|
|
634
|
+
</picture>
|
|
635
|
+
</a>
|