Files
lineartization/README.md
T

444 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://img.shields.io/badge/python-3.8%2B-blue.svg)]()
[![License](https://img.shields.io/badge/license-MIT-green.svg)]()
---
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [The Two Extraction Modes](#the-two-extraction-modes)
- [Denoise Levels](#denoise-levels)
- [Command Line Interface](#command-line-interface)
- [Python API](#python-api)
- [Technical Documentation](#technical-documentation)
- [Pipeline Overview](#pipeline-overview)
- [Mode A: Skeletonization](#mode-a-skeletonization)
- [Mode B: Minimum Filter](#mode-b-minimum-filter)
- [True-Black Criterion](#true-black-criterion)
- [Denoise Algorithm](#denoise-algorithm)
- [Parameter Reference](#parameter-reference)
- [Design Notes & Known Limits](#design-notes--known-limits)
- [Project Structure](#project-structure)
- [Testing](#testing)
- [Contact](#contact)
- [License](#license)
---
## 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
```bash
pip install lineartization
```
From source:
```bash
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
```bash
# Skeletonization (clear / vector-like source)
lineartization poster.jpg lineart.png
# Minimum filter (handwritten / photographed source)
lineartization handwriting.jpg lineart.png --method minimum
```
### Python
```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 |
```bash
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
```python
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
```bash
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](LICENSE) for details.