beiryu
  • Projects
  • Blog
  • About

© 2026 Khanh Dinh. I build software and write about the journey.

  • GitHub
  • LinkedIn
  • X
August 29, 2026 · 0 views

PNG to centerline SVG: recovering the line the artist actually drew

Why most bitmap tracers turn line-art icons into filled outlines, what tools support centerline tracing, and the Go pipeline I built: upscale, threshold, distance transform, Zhang–Suen thinning, graph clean-up, curve fitting and an automatic render-and-diff check.

  • Tutorials
  • Frontend

I had a few icon libraries that only existed as PNG files: thin, uniform line-art icons, the kind you would normally draw with a 1.5px stroke. I needed them as SVG. Not just any SVG, but SVG that behaves like the rest of a modern icon set: stroke="currentColor", a configurable stroke-width, round caps, and paths you can animate with stroke-dasharray.

Every tool I tried produced something that looked right and was wrong underneath. This post explains why, what the alternatives are, and the pipeline I ended up writing in Go.

The problem: outlines vs centerlines#

Most bitmap tracers (Potrace, VTracer, the default modes in Illustrator and Inkscape) do outline tracing. They find the boundary between dark and light pixels and wrap it in curves. For a 2px line, that gives you a long, thin, filled shape with two parallel edges:

Original stroke         Outline tracing                 Centerline tracing
                        (filled shape, 2 edges)         (one path + stroke-width)

  ━━━━━━━━━━            ┌──────────────────┐            ────────────────────
                        └──────────────────┘

Visually identical at the original size. In practice:

  • You cannot change the stroke width. The thickness is baked into the geometry.
  • stroke="currentColor" does nothing, because there is no stroke, only fill.
  • Line-drawing animations trace both edges of the shape, so the effect looks broken.
  • Files are larger: every line is stored twice.
  • Edges inherit every bump of the anti-aliased pixels, so icons look slightly wobbly when scaled up.

What I wanted was the medial axis: the single line through the middle of each stroke, the line the artist actually drew. That is called centerline tracing, and it is a much harder problem.

What already exists#

ToolCenterline?Notes
PotraceNoExcellent outline tracer, the default engine behind many apps
VTracerNoFast, good for colour images, outline only
AutoTraceYes (-centerline)The classic open-source option; results vary with image quality
Inkscape Trace BitmapYes (centerline mode, via AutoTrace)Easy to try manually, hard to batch
Illustrator Image TracePartly (strokes mode)Works on clean art, needs manual tuning per icon
scikit-image / OpenCV thinningSkeleton onlyGives you pixels, not vector paths; you build the rest

AutoTrace and Inkscape got me closest, but on a few hundred icons I kept hitting the same issues: small spurs at corners, broken joins at T-intersections, loops not closed, inconsistent stroke widths. Fixing them by hand defeated the purpose, so I wrote my own pipeline: a small, single-binary Go CLI that I could run over a whole folder.

The pipeline#

PNG ─▶ 1. normalise ─▶ 2. upscale + threshold ─▶ 3. distance transform
                                                         │
SVG ◀─ 8. emit ◀─ 7. fit curves ◀─ 6. simplify ◀─ 5. build graph ◀─ 4. skeletonise

1. Normalise#

Flatten transparency onto white, convert to greyscale, and crop to the icon's bounding box with a small margin. Keep the original canvas size so the SVG viewBox matches the source (for example 0 0 24 24).

2. Upscale and threshold#

Small icons are the hard case: a 24px icon with a 1.5px stroke has almost no pixels to work with, and anti-aliasing is most of the signal. Two things help a lot:

  • Upscale first (4× to 8×, with smooth interpolation) so the stroke is several pixels thick. All later steps work in the upscaled space; coordinates are divided back at the end.
  • Threshold with Otsu's method instead of a fixed 50% cut. It adapts to each image's contrast, which matters when a library mixes darker and lighter exports.

3. Distance transform#

For every foreground pixel, compute its distance to the nearest background pixel. On the centerline, that distance is half the stroke width. This gives you two things for free:

  • The stroke width for the SVG: twice the median distance along the skeleton. Using the median ignores the wider blobs at joins.
  • A way to rank pixels when pruning noise later: real strokes sit on a ridge of the distance map; spurs do not.

4. Skeletonise#

Thinning erodes the shape layer by layer until only a one-pixel-wide line is left. The Zhang–Suen algorithm is simple, fast, and good enough for line art. Each pass checks the 8 neighbours of a pixel (p2 is north, then clockwise to p9) and removes it only if doing so cannot break the line:

// One Zhang–Suen sub-iteration. step is 0 or 1. Returns true if any pixel changed.
func thinPass(img [][]bool, step int) bool {
	h, w := len(img), len(img[0])
	var remove [][2]int
	for y := 1; y < h-1; y++ {
		for x := 1; x < w-1; x++ {
			if !img[y][x] {
				continue
			}
			p := [8]bool{ // p2..p9, clockwise from north
				img[y-1][x], img[y-1][x+1], img[y][x+1], img[y+1][x+1],
				img[y+1][x], img[y+1][x-1], img[y][x-1], img[y-1][x-1],
			}
			b, a := 0, 0
			for i := 0; i < 8; i++ {
				if p[i] {
					b++
				}
				if !p[i] && p[(i+1)%8] {
					a++ // 0 → 1 transitions around the ring
				}
			}
			if b < 2 || b > 6 || a != 1 {
				continue
			}
			p2, p4, p6, p8 := p[0], p[2], p[4], p[6]
			if step == 0 && !(p2 && p4 && p6) && !(p4 && p6 && p8) ||
				step == 1 && !(p2 && p4 && p8) && !(p2 && p6 && p8) {
				remove = append(remove, [2]int{y, x})
			}
		}
	}
	for _, c := range remove {
		img[c[0]][c[1]] = false
	}
	return len(remove) > 0
}

func skeletonise(img [][]bool) {
	for {
		c0 := thinPass(img, 0)
		c1 := thinPass(img, 1)
		if !c0 && !c1 {
			return
		}
	}
}

Both sub-iterations run every round (a plain thinPass(img, 0) || thinPass(img, 1) would skip the second one whenever the first changed something). Stop when neither removes anything.

5. Build a graph#

A skeleton is still pixels. To turn it into paths, classify each skeleton pixel by how many skeleton neighbours it has:

  • 1 neighbour: an endpoint.
  • 2 neighbours: part of a line.
  • 3 or more: a junction (a T, an X, or a corner where strokes meet).

Walk from every endpoint and junction along 2-neighbour pixels until you reach another node. Each walk becomes an edge with an ordered list of points. Loops with no nodes at all (the letter O, a circle icon) need a separate pass: pick any unvisited pixel and walk until you return to it.

Then clean the graph:

  • Prune spurs. Thinning leaves short whiskers at corners and stroke ends. Remove any edge that ends in an endpoint and is shorter than about one stroke width.
  • Merge junction clusters. A single visual junction often becomes two or three junction pixels next to each other. Collapse them into one node at their centre.
  • Join through junctions. At each junction, pair up the incoming edges whose directions continue most smoothly (smallest change in angle) and join them into one path. Fewer, longer paths look better and animate better.

6. Simplify#

Pixel chains are full of staircase steps. Run Ramer–Douglas–Peucker with a tolerance around half a pixel (in original icon units) to keep only the points that define the shape.

7. Fit curves#

Straight segments between simplified points look faceted on curves. Fit cubic Bézier curves through the points (Schneider's algorithm is the standard approach), while keeping corners sharp: if the angle at a point is below a threshold, split the curve there instead of smoothing through it. Snap nearly horizontal and vertical segments to exact axes, and round coordinates to two decimals.

8. Emit SVG#

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
     fill="none" stroke="currentColor" stroke-width="1.5"
     stroke-linecap="round" stroke-linejoin="round">
  <path d="M4 12h16M12 4v16"/>
</svg>

The stroke attributes live on the root, so the icon can be recoloured and resized from CSS like any other line-icon set.

Checking the result#

Tracing is lossy, so I never trusted the output blindly. For each icon, the tool:

  1. Renders the generated SVG back to a bitmap at the source size.
  2. Compares it with the original (pixel difference, after thresholding both).
  3. Flags icons above a difference threshold into a review folder, with a side-by-side preview.

On a clean line-art set, most icons passed untouched. The flagged ones were almost always the same kinds of art: very small details that merged at low resolution, intentional fills (a solid dot or a filled badge) that should stay as shapes, and strokes of mixed width.

Lessons#

  • Know which problem you have. Outline tracing is the right tool for logos, filled glyphs and colour art. Centerline tracing is only for art that really is made of strokes.
  • Resolution beats cleverness. Upscaling before thinning fixed more problems than any tuning of the later steps.
  • The skeleton is the easy part. Thinning is a textbook algorithm. Most of the work, and most of the quality, is in the graph clean-up: spurs, junctions, loops.
  • Mixed art needs a hybrid. Detect solid regions (where the distance transform is much larger than the typical stroke) and trace those with outline tracing, then combine both in one SVG.
  • Automate the check, not just the conversion. Rendering the result back and diffing it turned a few hundred icons from a manual review into a short list.

If your icons really are line art, centerline tracing gives you SVGs that are smaller, recolourable, resizable and animatable. That is worth the extra steps.

Enjoyed this? Get the next one by email.

Keep reading

Jan 13, 2024

The Miracle of Yoga: Health and Balance from Within

Yoga promotes health and wellness by balancing the energy body, which can alleviate physical and psychological ailments. This balance is achieved through simple practices, not necessarily complex yoga poses. Chronic ailments, which account for over 70% of health issues, are self-produced and can be managed by activating the body's internal repair mechanisms. Thus, yoga is a tool that enables individuals to take charge of their health and wellness.

Jan 1, 2024

How to build a Portfolio Tracking like CoinMarketCap from Zero (Part 1)

The MVP will focus on essential features required to track and manage portfolios of various assets, including cryptocurrencies. It will serve as a foundation for future feature enhancements adding later: NFTs, stocks, re