Files
lineartization/README.md
T

15 KiB
Raw Blame History

lineartization

Convert color illustrations / handwritten posters into clean black-and-white line art. Pure Python + OpenCV + scikit-image. No deep-learning models required. Runs offline.

Version: 1.6.1 · Author: DVS · License: MIT

Python License


Table of Contents


Overview

lineartization turns a color picture (manga-style illustration, school poster, children's drawing) into a black-on-white line drawing suitable for:

  • Coloring books / templates
  • Printing (high-contrast, ink-friendly)
  • Vectorization / further editing
  • OCR preprocessing

Unlike dedicated edge-detection or upscaling tools (which are pure pixel math and produce broken lines, hollow double edges, or heavy blur), this library works in two well-defined strategies depending on the source quality.


Features

Feature Description
🈶 Chinese-text aware Detects the text block and keeps character strokes complete
📐 Uniform stroke width Text and artwork lines unified to a configurable width
🔗 Continuous lines Lee skeletonization (shape-preserving) instead of naive thinning
🎨 True-black criterion Distinguishes black ink from dark colors using RGB + chroma
🧹 Tunable denoise Four levels: strong / normal / light / none
🛡️ Protected regions Keep complex textures (emblems, seals) from being cleaned away
🐍 Zero model dependency No GPU, no ONNX, no downloads — pip install and run

Installation

pip install lineartization

From source:

git clone https://git.dvscloud.net/dvs/lineartization.git
cd lineartization
pip install -e .

Dependencies: numpy, opencv-python, opencv-contrib-python, scikit-image


Quick Start

Command line

# Skeletonization (clear / vector-like source)
lineartization poster.jpg lineart.png

# Minimum filter (handwritten / photographed source)
lineartization handwriting.jpg lineart.png --method minimum

Python

from lineartization import extract_lineart_file

extract_lineart_file("poster.jpg", "lineart.png")                      # skeleton
extract_lineart_file("handwriting.jpg", "lineart.png", method="minimum")

The Two Extraction Modes

Choosing the wrong mode is the most common cause of bad output. Pick the mode that matches how the source image was produced.

method="skeleton" — for clear sources (default)

Use when the original image already has clean, well-separated lines, e.g. a vector illustration, a high-resolution redrawing, or a professionally scanned black-ink drawing.

  • Strong point: thin, smooth, uniform lines — the most aesthetic result.
  • Weak point: skeletonization on thick handwritten strokes produces spurs and web-like artefacts, because thinning a wide non-uniform stroke inevitably branches.

method="minimum" — for handwritten / low-resolution sources

Use when the image is a photo of a hand-drawn poster, a phone snapshot, or anything with thick, irregular, low-resolution strokes.

  • Strong point: preserves the original strokes, no line breakage.
  • Weak point: strokes are a little thick; result is "usable" rather than refined.

Rule of thumb: if the lines in the source are one clean pixel wide → skeleton. If the lines are thick / wobbly / photographed → minimum.


Denoise Levels

Available only in minimum mode (skeleton mode has its own built-in cleanup).

Level Pipeline Note
strong (default) median → open → connected-component filter (<30 px) → final median Standard aggressive cleanup
normal median → connected-component filter → final median Slightly gentler
light median → remove only "tiny square" blobs → final median Line-preserving
none median only Minimal
lineartization in.jpg out.png -m minimum -d light
lineartization in.jpg out.png -m minimum -d strong --denoise-area 40

Command Line Interface

usage: lineartization [-h] [-m {skeleton,minimum}] [-w WIDTH]
                      [--min-mean MIN_MEAN] [--min-chroma MIN_CHROMA]
                      [--min-kernel MIN_KERNEL]
                      [-d {strong,normal,light,none}]
                      [--denoise-area DENOISE_AREA] [--no-green-smoothing]
                      [--protect x1,x2,y1,y2] [-v] [-V]
                      input output
Option Default Description
-m, --method skeleton Extraction mode
-w, --width 2 Stroke width (skeleton mode)
-d, --denoise strong Denoise level (minimum mode)
--denoise-area 30 Connected-component removal threshold
--min-mean 130 True-black criterion: max RGB mean
--min-chroma 45 True-black criterion: max chroma
--min-kernel 2 Minimum-filter radius (1–3)
--no-green-smoothing off Disable green-block smoothing
--protect — Protected rect x1,x2,y1,y2 (repeatable)
-v, --verbose off Print pipeline logs

Python API

import cv2
from lineartization import LineArtConfig, extract_lineart, load_image, save_image

img = load_image("poster.jpg")                     # BGR uint8, RGBA-safe

cfg = LineArtConfig(
    method="minimum",          # "skeleton" | "minimum"
    denoise="strong",          # strong | normal | light | none
    min_mean=130,              # true-black RGB mean threshold
    min_chroma=45,             # true-black chroma threshold
    min_kernel=2,              # minimum-filter radius
    protect_areas=[(120, 220, 940, 1050)],   # x1,x2,y1,y2
)

lineart = extract_lineart(img, cfg, verbose=True)  # 0/255, white bg, black lines
save_image("lineart.png", lineart)

extract_lineart() accepts a BGR image and returns a single-channel uint8 image valued 0/255 (white background, black lines).


Technical Documentation

Pipeline Overview

                       ┌──────────────┐
   input image  ─────► │ load_image() │  RGBA-safe, white-composited, BGR
                       └──────┬───────┘
                              │
              ┌───────────────┴────────────────┐
              ▼                                ▼
    method = "skeleton"              method = "minimum"
    ────────────────────             ────────────────────
    Region analysis                  True-black criterion
    Pattern extraction               Minimum filter
    Lee skeletonization              Otsu binarization
    Denoise + spur pruning           Denoise (tunable)
    Uniform width                    → white bg / black lines
              │                                │
              └───────────────┬────────────────┘
                              ▼
                     0/255 line-art PNG

Mode A: Skeletonization

Goal: reproduce a clear source as thin, uniform, aesthetically pleasing lines.

Step 1 — Region analysis

Two spatial masks are derived from the HSV representation:

  • Paper region (paper) — bright, low-saturation background of the text block.
    paper = (V > paper_v) AND (S < paper_s)
    paper = morph_close(ELLIPSE 21×21, iterations=3)
    paper = erode(ELLIPSE paper_erode×paper_erode)
    
  • Text rectangle (tz) — the largest connected blob of "ink density".
    ink    = (V < ink_v) AND (S < ink_s)
    dense  = morph_close(ink, 41×41)
    dense  = morph_open(dense, 61×61)
    tz     = bounding_box(largest_blob(dense)) + text_pad
    
    Using the largest density blob (rather than a raw colour mask) reliably excludes scattered decorations such as fireworks or small figures.

Step 2 — Line extraction

at_text = adaptiveThreshold(gray, GAUSSIAN, INV, 31, 14)
at_all  = adaptiveThreshold(gray, MEAN,     INV, 25, 19)
dark    = (V < dark_v)
fine    = dark AND NOT morph_open(dark, 13×13)     # drop large dark blocks
pattern = (NOT paper) AND fine AND at_all
text    = paper AND at_text
lines   = skel( morph_close(text OR pattern, 3×3) )

Optionally, the four large green blocks (hills in a poster) are re-extracted from a mean-shift smoothed copy to suppress colour-banding, and merged via a Canny contour (see enable_green_smoothing).

Step 3 — Denoise & spur pruning

  • Isolated noise removal — a connected component is removed when skeleton_length < noise_sk_len and branch_count < noise_branch and area < noise_area.
  • Spur pruning — walk from every skeleton endpoint; if a branch reaches a junction within spur_maxlen px, it is erased (except inside protected areas).

Step 4 — Uniform width

Text and artwork are separately re-skeletonised, then dilated to line_width.


Mode B: Minimum Filter

Goal: faithfully keep the original strokes of a handwritten / low-res source, avoiding the false-positive colour edges that naive thresholding produces.

The pipeline mirrors the classic Photoshop "Minimum filter" line-art recipe, derived mathematically:

L = grayscale(image)              # line = dark, background = light
R = 255 − L                       # line = light, background = dark
M = erode(R, kernel)              # minimum filter: dark background expands
result = L / (255 − M) · 255      # "Color Dodge" blend
line   = Otsu(result)             # pure black / white

Why L / (255 − M) and not the inverse

The Photoshop Color Dodge blend of a base L and a blend layer B is L / (255 − B). Feeding the eroded inverse M as the blend layer gives the result already in white-background / black-line polarity — no extra inversion is required (an extra 255 − result produces an all-black image, a classic pitfall).


True-Black Criterion

A naive luminance threshold classifies dark colours (deep red, navy) as "black", producing spurious blobs. lineartization instead requires a pixel to be both dark and achromatic:

mean   = (R + G + B) / 3
chroma = max(R,G,B) − min(R,G,B)

true_black = (mean < min_mean) AND (chroma < min_chroma)
  • mean < min_mean ⇒ dark enough.
  • chroma < min_chroma ⇒ R, G, B are close ⇒ grey/black, not a saturated colour.

The final mask is intersected with true_black, so coloured fills are never reported as ink.


Denoise Algorithm

minimum mode exposes four levels. All levels end with a median pass to remove salt-and-pepper residue.

strong : median(3) → open(2×2) → remove CC area<30 → median(3)
normal : median(3)             → remove CC area<30 → median(3)
light  : median(3)             → remove blobs (area<10 & fill≥0.8 & elong<1.8) → median(3)
none   : median(3)

strong is the default. Lower levels trade less noise suppression for fewer false deletions of legitimate short strokes.


Parameter Reference

Parameter Default Meaning
method "skeleton" "skeleton" or "minimum"
paper_v / paper_s 140 / 60 Paper-region brightness / saturation bounds
paper_erode 31 Erosion kernel to shrink the paper region
ink_v / ink_s 140 / 60 Ink criterion for text-block detection
text_pad 40 Padding around the detected text rectangle
dark_v 160 Dark-pixel threshold (skeleton mode)
morph_open_k 13 Kernel removing large dark blocks
adaptive_bs / adaptive_c 25 / 19 Artwork adaptive threshold
noise_sk_len / noise_branch / noise_area 25 / 8 / 300 Isolated-noise criterion
spur_maxlen 25 Max spur length pruned
min_mean / min_chroma 130 / 45 True-black criterion
min_kernel 2 Minimum-filter radius
denoise "strong" Denoise level
denoise_area 30 CC removal area for strong/normal
line_width 2 Stroke width (skeleton mode)
protect_areas [] List of (x1,x2,y1,y2) rectangles never cleaned
enable_green_smoothing True Mean-shift smoothing of green hill blocks

Design Notes & Known Limits

Why skeletonization is not always the answer. Morphological thinning peels border pixels from a blob. For a thick, non-uniform handwritten stroke, the remaining medial axis branches into spurs and webs. That is precisely what method="minimum" avoids by keeping the original stroke instead of reducing it to a 1-px skeleton.

Why edge detection is avoided. Classical edge detectors (Sobel, Laplacian, High-pass) respond to gradients; a rasterised line has two edges, so the output is a hollow double line. Closing the gap yields either a thick smear or requires a centre-line step — both inferior to the direct approaches above.

Known limits.

  • Very low-resolution text (character strokes < 2 px) cannot be recovered by any pure-algorithm method; a semantic/AI model is required. This library does not include one by design.
  • Heavy JPEG artefacts in the source may survive as small debris; raise --denoise-area to suppress them.

Project Structure

lineartization/
├── lineartization/
│   ├── __init__.py      # package entry + CLI
│   ├── __main__.py      # `python -m lineartization`
│   ├── core.py          # algorithm (skeleton / minimum)
│   └── py.typed
├── examples/
│   └── demo.py
├── tests/
│   └── test_core.py
├── pyproject.toml
├── MANIFEST.in
├── README.md
└── LICENSE

Testing

pip install pytest
pytest tests/ -v

Contact

Author DVS
Email admin@dvscloud.net
Backup dvs6666@163.com
Repository https://git.dvscloud.net/dvs/lineartization

License

MIT License — see LICENSE for details.