To convert a text image into optimized single-line vectors using custom configuration:
python main.py input.jpg output_centerline.svg --centerline --no-adaptive --morph-close 5 --min-spur 1 --upscale 8 --morph-close 5Here is the visual evidence of the conversion from the high-resolution raster image (input.jpg) to the thinned centerline stroke paths (output_centerline.svg).
Below is the full-page overview comparison. The left shows the original raster text (input.jpg) and the right shows the generated thinned centerline paths (output_centerline.svg).
Below is the full-page drawing overview comparison. The left shows the original raster drawing (input_draw.png) and the right shows the generated thinned centerline paths (output_centerline.svg).
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.
Observe how the dots of the letter i and colons are preserved as independent, clean path strokes rather than being merged or pruned:
Observe how the lines are preserved as independent, clean path strokes rather than being merged or pruned:
- π§© Graph-Based Skeleton Tracing: Represents the skeleton as a topological graph of nodes (junctions/endpoints) and edges. Prevents junction distortion and splits.
- π 4x Upscaled Anti-Aliasing: Interpolates and smooths low-resolution input images before skeletonization to eliminate pixel-level wiggles.
- π‘οΈ Isolated Path Safety (i-Dot Preservation): Distinguishes between side spurs (noise) and isolated paths, ensuring colons, periods, and the dots of
iare never pruned. - π Chaikin Curve Fitting: Corner-cutting curve smoothing that rounds out characters organic-style without coordinate shrinkage.
- ποΈ TSP Pen-Travel Optimization: Solves the Travelling Salesperson Problem (TSP) on the path sequence to save up to 98% of pen-up travel distance.
graph TD
A[Raster Image input.jpg] --> B[4x Cubic Upscaling]
B --> C[Gaussian Blur 9x9]
C --> D[Otsu Binarization]
D --> E[Morphological Closing 5x5]
E --> F[Skeletonization]
F --> G[Graph Extraction & Pruning]
G --> H[Chaikin Path Smoothing]
H --> I[TSP Sort & Max-Join]
I --> J[Stroke SVG output.svg]
Below is the exhaustive pseudocode and logic breakdown of every helper and processing routine in the KDRAW engine (main.py and the kdraw package).
Input: Packed 32-bit pixel value val, transparency flag has_alpha
Output: Hex color string (#RRGGBB) or CSS RGBA string (rgba(...))
Given a packed ARGB pixel:
Extract each channel using bitwise operations:
where:
When transparency is enabled, convert the alpha channel to the CSS opacity range:
with:
If transparency is present:
return:
Otherwise return:
where:
and (||) denotes string concatenation.
Input: Curve coordinate array path, iteration count iterations, smoothing weight w
Output: Laplacian-smoothed coordinate array
For each vertex ( \mathbf{p}_i ), compute the local neighborhood average:
The updated position is a weighted blend between the original point and its neighborhood mean:
Substituting the neighborhood average:
where:
Special cases:
(No smoothing)
(Complete neighborhood averaging)
For intermediate values:
the vertex moves proportionally toward the average of its neighboring vertices, reducing local curvature and noise while preserving the overall shape.
- Logic:
- If path has less than 3 points, return original path.
Input: Coordinate array path, iteration count iterations, smoothing weight w
Output: Laplacian-smoothed coordinate array
-
If the path contains fewer than 3 points, return the original path.
-
Determine whether the path is closed:
-
Repeat for each smoothing iteration:
- Create a temporary copy of the coordinate array.
- Apply the update rules below.
For each vertex ( \mathbf{p}_i ) (excluding the duplicated endpoint):
with cyclic indexing:
Maintain closure after updating:
Keep endpoints fixed and update interior vertices:
Input: Coordinate array path, iteration count iterations
Output: Chaikin corner-cut smoothed coordinate array
-
If the path contains fewer than 3 points, return the original path.
-
Repeat for each iteration.
For every segment:
Generate:
Append the first generated point to the end of the sequence to preserve closure.
Preserve endpoints:
For each interior segment:
Generate:
Resulting point sequence:
For a segment connecting points ( \mathbf{A} ) and ( \mathbf{B} ):
Repeated application progressively removes sharp corners and converges toward a smooth curve.
- Input: List of curves
contours, pen-down merging thresholdmax_join_dist - Output: Sorted and merged curves list, original travel distance, optimized travel distance
- Logic:
- Convert all contours to NumPy float arrays. Calculate baseline sequential pen travel.
- Implement a greedy Travelling Salesperson (TSP) heuristic:
- Pop the first contour as the active path.
- While remaining contours exist:
- Find the distances from the active path's endpoint to the start and endpoints of all remaining contours.
- Identify the closest coordinate point.
- If the closest point belongs to the end of a contour, reverse that contour.
- If the distance to the closest contour is (\le max_join_dist), extend the active path coordinates directly with the closest contour coordinates (merging).
- Otherwise, append the active path to the optimized list and set the closest contour as the new active path.
- Append the final active path.
- Input: Binary skeleton image
skel_bool, spur limitmin_spur_length, merge radiuscollapse_dist - Output: List of cleaned, continuous centerline coordinate paths
- Logic:
- Retrieve skeleton coordinates:
pixels = set(zip(*np.where(skel_bool))). - Compute 8-connected adjacency dictionary:
adj = {p: get_neighbors(p, pixels) for p in pixels}. - Classify pixels:
endpoints(neighbors == 1)junctions(neighbors >= 3)regular(neighbors == 2)
- Cluster contiguous junction pixels using BFS. Each connected component of junction pixels forms a singular "super-junction" node.
- Assign node IDs to all endpoints and junction clusters. Build
pixel_to_nodemap. - Trace edges:
- For each node:
- If a neighbor is a regular pixel, trace along regular pixels (BFS) until hitting any node. Create a stroke edge.
- If a neighbor is directly in another node, create a direct node-to-node edge of length 2 (essential for preserving i-dots).
- For each node:
- Locate isolated cycles (loops with no nodes, degree-2 only like in the letter
o). Convert to closed loop edges. - Perform iterative topology reductions:
- 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.
- Isolated Check: If an edge connects two endpoints directly (degree 1 to 1), it is an isolated dot. Protect it from spur pruning.
- 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.
- Clean up: For any node left with degree 2 (exactly two edges), merge the paths of the two edges into a single edge.
- Retrieve skeleton coordinates:
- Input: File paths and all tuning thresholds (
upscale_factor,blur_size, etc.) - Output: Stroke-only SVG file containing centerline paths
- Logic:
- Load input image. If
upscale_factor > 1, upscale usingcv2.resizewith bicubic interpolation. - Apply Gaussian blur of size
blur_size(only odd dimensions allowed). - Binarize:
- If
use_adaptive: Apply local adaptive Gaussian thresholding usingcv2.adaptiveThresholdwithblock_sizeand subtraction constantc_val. - Else: Apply Otsu's thresholding using
cv2.threshold.
- If
- Morphological filters: Apply closing and opening operations using an elliptical structuring element on the binary mask.
- Thin binary mask to a single-pixel centerline using morphological
skeletonize(Zhang-Suen/Lee). - Call
build_and_prune_graphto trace skeleton pixels into a clean set of coordinate paths. - Downscale coordinate values by
upscale_factorto match original image dimensions. - For each path:
- If the path has only 2 points, bypass simplification.
- Otherwise, simplify coordinates using RDP (
cv2.approxPolyDP) with toleranceepsilon.
- Apply smoothing: If
smooth_iters > 0, callsmooth_paths_chaikinorsmooth_paths_laplacianforsmooth_itersiterations. - Decimate coordinates post-smoothing using RDP with a tight tolerance
smooth_decimate. - Call
optimize_pathswithmax_jointo minimize travel sequence. - Format and write paths into SVG XML nodes containing
<path d="..." fill="none" stroke="black" ... />.
- Load input image. If
Ensure you have the required libraries installed:
pip install opencv-python scikit-image numpy pillowTo convert a text image into optimized single-line vectors using the recommended defaults:
python main.py input.jpg output_centerline.svg --centerline --no-adaptive| CLI Argument | Type | Default | Description |
|---|---|---|---|
--centerline / -cl |
flag | False |
Enables single-stroke skeletonization (eliminates bubble outlines). |
--upscale |
int |
4 |
Upscaling factor to smooth boundaries before tracing. |
--blur |
int |
9 |
Pre-threshold Gaussian blur size to remove staircase wiggles. |
--no-adaptive |
flag | False |
Disables adaptive thresholding (uses Otsu global thresholding, preserving loops). |
--morph-close |
int |
5 |
Fills in tiny gaps on thin stroke contours. |
--min-spur |
int |
16 |
Minimum pixel length of a branch to not be pruned as a spur. |
--collapse-junc |
int |
8 |
Merges adjacent junctions to straighten line joints. |
--max-join |
float |
2.5 |
Binds path ends within this distance to avoid lifting the pen. |
--smooth-iters |
int |
3 |
Number of Chaikin smoothing iterations. |
--smooth-decimate |
float |
0.1 |
Post-smoothing RDP decimation to minimize point count. |
Running KDRAW with the optimal centerline defaults provides a massive boost in vector quality and plotter throughput:
Important
TSP Optimization saves up to 98% of pen-up travel, reducing wear and tear on plotter belts and servos.
| Metric | Raw Skeleton Trace | KDRAW Graph Pipeline | Improvement |
|---|---|---|---|
| Path Count (Pen Lifts) | 2,468 | 2,071 | 16.1% fewer lifts |
| Pen-Up Travel Distance | 1,684,002 px | 35,425 px | 97.9% distance saved |
| Average Angle Change | 49.9Β° | 17.4Β° | Curves are 2.8x smoother |
| Punctuation & Dots | Lost / Jagged | Perfectly Preserved | Flawless |
If you find this project useful and would like to support the deployment of FishTrack buoys for coastal fishing communities, donations are greatly appreciated!
Bitcoin Address: 13zWnp2ty3NPzAXX9QxwEeoPSKhN5tPzic
MIT License. Open-source vector engine.




