Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lineart: how one line gets decided

✅ verified · needs ComfyUI · step 2 of the layered route · code: appendix B

What this step is for

An artist tracing a picture first decides which lines are real: the outline certainly, how many lines a skirt fold gets, whether to draw the strands of hair at all. A diffusion image has no lines in it – it is areas of colour and gradients, so the lines have to be found in it, and each line has to be one stroke: a smooth curve rather than a string of short segments, painted in a single go.

There are two ways to find them: the part edges segmentation gives, and a model made for extracting lineart. Both get things wrong, and they get different things wrong. This chapter is about putting both into one pool, letting the source decide which survive, and fitting each survivor into a single stroke.

On the layered route the lines in this chapter are the lines on the source. They are not the lines the finished painting draws – once the layers exist, each one extracts its own (ch. 4). These lines do three things: they let a person decide the layers, they serve as walls for segmentation (ch. 1, “lines as walls”), and they are what ch. 4 is compared against.

After this you will be able to

  • say what each of the two sources gets wrong, and why the source of a line cannot decide whether it is right.
  • read the three colours on the review image and write an override.
  • know how a shaky polyline becomes one smooth stroke, and why smoothing comes before fitting.

Concepts

One stroke: a G1-continuous cubic bezier (several segments joined, with matching tangent directions at the joins), painted in a single go. Its width and colour are properties of that stroke.

Source: where a candidate came from. Source one is the segmentation’s contours, the edge of each sub-part (ch. 1’s sub-pixel rings); source two is raster lineart, an image of white lines on black that a model computes from the source, which is thinned and then walked into individual strokes.

Candidate: a line in the pool whose fate is not yet decided. Each carries its source, which line layer it belongs to, and its fitted curve.

Arbitration: scoring every candidate against the source, keeping one line where several overlap, and dropping those that do not score enough. The only judge is the source: there really is an edge under the line, and the line rides that edge.

Source one (brown): every edge is there, but shaky. Source two (blue): smooth, but it invented a line the source does not have. Both go into the pool.

The method, on one page

  1. Source one: each sub-part’s ring is judged arc by arc – is this arc mine, is there an edge under it – and the surviving arcs are fitted into curves (2.1 The segmentation’s contours).
  2. Source two: the raster lineart is thinned into a skeleton, its endpoints and junctions are identified, and the lines are walked, joined, smoothed and fitted (2.2 Extracting raster lineart and 2.3 Skeletonizing).
  3. Arbitration: sample the source along each line’s normal to see whether there is an edge, compute its support and its smoothness, drop what falls short, and where lines overlap let the first one claim its ground while the later one loses only the covered part (2.4 Arbitration).
  4. Overriding: a person looks at the review image and forces a source by box or by line id in the override table, and it is rerun.

Doing it

mkdir -p work/pic/phase1-layers/step02-lineart && cp dtp/templates/lineart/main.py work/pic/phase1-layers/step02-lineart/
uv run python work/pic/phase1-layers/step02-lineart/main.py     # half a minute (with SEG at the top pointing at the segment step)

Look at the review image: over a faded source, blue is a kept raster-lineart line, brown a kept contour, and red dashes a dropped candidate. Three things to judge by:

  • the outline is one line per leg and one per arm, not broken into pieces.
  • the cut between two same-coloured parts has not been drawn as a line.
  • there are no lines inside the hair or the eyes that the source does not have.

Overrides go into overrides.json at the subproject root (the format is below), and it is rerun, which takes a few tens of seconds.

Common problems

symptomcausewhat to change
a real edge was droppedthe contrast there is too faint and the support falls shortname the source by box in the override table
a line was drawn between two same-coloured partsthat edge came from segmentation, its two sides are the same colour, and it should have been droppedfind out why it survived (usually the raster lineart drew a line there) and override it to neither
an edge became a string of short segmentsit was not smoothed before fitting, or the tolerance is too smallraise the fitting tolerance; leave the smoothing sigma alone
the raster lineart’s lines all shattered into a dozen pixels eachskeletonizing took the stairs on a diagonal for junctionsidentify junctions by crossing number, which is what it does now; see 2.3
there are more lines inside the hair than the source hasthe model drew a faint highlight band as a linethe finished painting suppresses some of them by line weight (ch. 6); override the obvious ones
the eye has no linesin the raster lineart the eye’s interior is a solid block, and the lower lashes are one or two pixels at original sizethe eye’s lines come from the shapes ch. 6’s eye method produces; this step leaves them alone
the mouth has no linesin the source the mouth is a small dark patch with no black lineneither source has one, so the finished painting has none; accepted

The override table’s format:

[{"box": [x0, y0, x1, y1], "use": "ai"},  {"id": "ct-12", "use": "none"}]

The first form is by box: full-image coordinates at original size, forcing every candidate inside the box to the raster lineart (ai), the contour (contour) or neither (none). The second is by line id. This table applies to ch. 4 as well.

Knobs

Knobsdefaultwhen to change it
fit_err3.0the tolerance for fitting a contour (px); raise it when the contour arcs come out too broken
line_min_len12arcs shorter than this are dropped (px); raise it when there are many fragments
thr70the threshold for binarizing the raster lineart; lower it when the lineart is too faint to catch
boxoffreport only the candidates inside one box, for debugging an override

Summary

  • Lines have two sources and both get things wrong; right and wrong are decided by the source image, not by which source the line came from.
  • A line is one stroke: smooth first and then least-squares fit, rather than interpolating through every point.
  • Where lines overlap, trim rather than drop the whole thing.
  • The lines in this chapter are the lines on the source; each layer’s own lines are in ch. 4.

Further reading

  • Code: appendix B, under lineart – source one, source two, and the arbitration; the fitting and the thinning are in the geometry layer.
  • Worked example: ch. 19, “The layered route” (how many candidates, how many survived, the median number of curvature flips, and images from all three extractors).
  • References are in the subsections.