Highlights:
- minimum mode: add min_true_black switch (default True = unchanged v1.6.1 behaviour).
True : true-black gate + CC denoise (best for black line work).
False : no gate, keeps colored regions, distance-transform thinning
(best for colorful posters).
- skeleton mode: replace hue-specific green-block smoothing with
saturation-based _color_zones; no hue or side-of-image assumptions.
- Move all comments, docstrings and messages to English.
- Remove built-in default paths; examples/demo.py now requires args.
- README: document both minimum variants, resolution guidance, parameters.
- tests: 18 cases covering I/O, both modes, both minimum variants.
597 lines
22 KiB
Markdown
597 lines
22 KiB
Markdown
# lineartization
|
||
|
||
> **Convert color illustrations / posters into clean black-and-white line art.**
|
||
> Pure Python + OpenCV + scikit-image. No deep-learning models required. Runs offline.
|
||
|
||
**Version:** 1.7.0 · **Author:** DVS · **License:** MIT
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
- [Overview](#overview)
|
||
- [Resolution Matters](#resolution-matters)
|
||
- [Features](#features)
|
||
- [Installation](#installation)
|
||
- [Quick Start](#quick-start)
|
||
- [The Two Extraction Modes](#the-two-extraction-modes)
|
||
- [Minimum Mode: True-Black On/Off](#minimum-mode-true-black-onoff)
|
||
- [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, 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.
|
||
|
||
---
|
||
|
||
## Resolution Matters
|
||
|
||
**The higher the input resolution, the better the extraction.** Stroke recovery
|
||
is a purely geometric operation: at higher resolution a stroke covers more
|
||
pixels, survives binarization more reliably, and thins into a cleaner centre
|
||
line. Low-resolution inputs lose stroke detail before the algorithm even runs,
|
||
and no pure-algorithm method can invent it back.
|
||
|
||
Practical guidance:
|
||
|
||
- **High resolution (≥ 1500 px on the long edge)** → use `skeleton`. You get
|
||
thin, uniform, aesthetically pleasing lines.
|
||
- **Medium resolution** → try `skeleton` first; if strokes break up, fall back
|
||
to `minimum`.
|
||
- **Low resolution / phone snapshot / heavy compression** → use `minimum`. It
|
||
does not depend on thinning thin structures, so it degrades far more
|
||
gracefully.
|
||
- If you can, **upscale before extraction** rather than fighting a small input.
|
||
|
||
The CLI prints this hint after every run, and it is also available
|
||
programmatically as `lineartization.RESOLUTION_HINT`.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
| Feature | Description |
|
||
|---------|-------------|
|
||
| 📐 **Uniform stroke width** | Output 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 |
|
||
| 🔀 **Two minimum variants** | Pick per image: with or without the true-black gate |
|
||
| 🧹 **Tunable denoise** | Four levels: `strong` / `normal` / `light` / `none` |
|
||
| 🛡️ **Protected regions** | Keep chosen rectangles (emblems, seals) from being cleaned away |
|
||
| 🧩 **Region-agnostic** | No hue- or side-of-image assumptions; works on any layout |
|
||
| 🐍 **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
|
||
|
||
# Minimum filter without the true-black gate (colorful posters)
|
||
lineartization colorful.jpg lineart.png --method minimum --no-true-black
|
||
```
|
||
|
||
### 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")
|
||
extract_lineart_file("colorful.jpg", "lineart.png",
|
||
method="minimum", min_true_black=False)
|
||
```
|
||
|
||
---
|
||
|
||
## 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`.
|
||
|
||
---
|
||
|
||
## Minimum Mode: True-Black On/Off
|
||
|
||
`minimum` mode runs the classic Photoshop "minimum filter" (color-dodge blend)
|
||
followed by Otsu. The one thing you get to choose is whether the result is then
|
||
gated by the **true-black criterion**.
|
||
|
||
| | `min_true_black=True` (default) | `min_true_black=False` |
|
||
|---|---|---|
|
||
| True-black gate | ✅ intersects with the true-black mask | ❌ not applied |
|
||
| Colored regions | excluded (only dark, low-chroma pixels survive) | **kept as strokes** |
|
||
| Cleanup | median → open → CC filter → median | despeckle → drop short fragments |
|
||
| Thinning | none (original stroke weight kept) | distance transform to a thin even line |
|
||
| Best for | mostly-black line drawings, clean ink work | colorful posters, illustrations |
|
||
|
||
**Which to pick?** It depends on the picture, so try both when unsure:
|
||
|
||
- A **math worksheet / notebook page** — mostly black strokes on light paper —
|
||
looks better with the true-black gate **on**: the gate removes colored
|
||
scribbles and keeps the line work clean.
|
||
- A **colorful festival poster** — large red / gold areas — looks better with
|
||
the gate **off**: with the gate on, almost everything colorful is discarded
|
||
and the drawing comes out nearly empty.
|
||
|
||
```bash
|
||
lineartization in.jpg out.png -m minimum # true-black on
|
||
lineartization in.jpg out.png -m minimum --no-true-black # true-black off
|
||
```
|
||
|
||
```python
|
||
extract_lineart(img, LineArtConfig(method="minimum", min_true_black=True))
|
||
extract_lineart(img, LineArtConfig(method="minimum", min_true_black=False))
|
||
```
|
||
|
||
---
|
||
|
||
## Denoise Levels
|
||
|
||
Available in `minimum` mode when `min_true_black=True`.
|
||
(`min_true_black=False` uses its own despeckle + fragment removal instead.)
|
||
|
||
| 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]
|
||
[--no-true-black] [--min-mean MIN_MEAN]
|
||
[--min-chroma MIN_CHROMA] [--min-ratio MIN_RATIO]
|
||
[--min-kernel MIN_KERNEL]
|
||
[-d {strong,normal,light,none}]
|
||
[--denoise-area DENOISE_AREA]
|
||
[--m2-noise-area M2_NOISE_AREA]
|
||
[--m2-short-area M2_SHORT_AREA]
|
||
[--m2-short-len M2_SHORT_LEN] [--m2-close-k M2_CLOSE_K]
|
||
[--m2-dist-min M2_DIST_MIN] [--no-color-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) |
|
||
| `--no-true-black` | off | `minimum`: disable the true-black gate |
|
||
| `--min-mean` | `180` | True-black criterion: max RGB mean |
|
||
| `--min-chroma` | `60` | True-black criterion: max chroma |
|
||
| `--min-ratio` | `1.5` | Otsu fallback threshold (%) |
|
||
| `--min-kernel` | `2` | Minimum-filter radius (1–3) |
|
||
| `-d, --denoise` | `strong` | Denoise level (true-black on) |
|
||
| `--denoise-area` | `30` | Connected-component removal threshold |
|
||
| `--m2-noise-area` | `20` | No-true-black: despeckle threshold |
|
||
| `--m2-short-area` | `40` | No-true-black: short-fragment area |
|
||
| `--m2-short-len` | `25` | No-true-black: short-fragment length |
|
||
| `--m2-close-k` | `2` | No-true-black: close kernel before thinning |
|
||
| `--m2-dist-min` | `0.5` | No-true-black: distance threshold |
|
||
| `--no-color-smoothing` | off | Skeleton: disable flat-color 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"
|
||
min_true_black=False, # False -> keep colored regions, thin the strokes
|
||
min_kernel=2, # minimum-filter radius
|
||
m2_dist_min=0.5, # thinning strength (no-true-black variant)
|
||
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"
|
||
──────────────────── ────────────────────
|
||
Paper + text region True-black criterion (optional)
|
||
Pattern / text extraction Minimum filter
|
||
Lee skeletonization Otsu binarization
|
||
Denoise + spur pruning ├─ true-black ON : CC denoise
|
||
Uniform width └─ true-black OFF: despeckle,
|
||
│ fragment removal,
|
||
│ distance-transform thinning
|
||
└───────────────┬────────────────┘
|
||
▼
|
||
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
|
||
|
||
```python
|
||
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) )
|
||
```
|
||
|
||
**Flat color regions.** Broad saturated fills are located by saturation alone
|
||
(`S > color_sat_min`, area within `color_area_range`) — deliberately *not* by
|
||
hue, so the step works for any palette rather than one specific image. Those
|
||
regions are re-extracted from a **mean-shift smoothed** copy, where a Canny
|
||
contour supplies the boundary, which suppresses colour banding inside the fill.
|
||
Tune or disable with `enable_color_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
|
||
```
|
||
|
||
This common front end is followed by one of two back ends:
|
||
|
||
### Back end 1 — `min_true_black=True`
|
||
|
||
```
|
||
mask = (line < 128) AND true_black
|
||
if mask_ratio < min_ratio: # Otsu too sparse -> retry
|
||
mask = adaptiveThreshold(...) AND true_black
|
||
mask = denoise(mask, level) # median / open / CC filter / median
|
||
```
|
||
|
||
The `min_ratio` guard matters for white backgrounds with very thin lines, where
|
||
global Otsu can collapse to almost no ink; the adaptive threshold recovers it.
|
||
|
||
### Back end 2 — `min_true_black=False`
|
||
|
||
```
|
||
mask = (line < 128) # no true-black gate
|
||
mask = despeckle(mask, m2_noise_area)
|
||
mask = drop_short(mask, m2_short_area, m2_short_len)
|
||
mask = morph_close(mask, m2_close_k)
|
||
mask = distance_transform(mask) >= m2_dist_min
|
||
```
|
||
|
||
The last step is what makes the output a thin, even line. A distance transform
|
||
is used **instead of skeletonization**: skeletonization collapses a stroke to a
|
||
1-px medial axis, losing glyph detail and branching at thick crossings, whereas
|
||
the distance transform only shaves inward, preserving stroke topology.
|
||
|
||
### 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.
|
||
|
||
Defaults are `min_mean=180`, `min_chroma=60`. This gate is what the
|
||
`min_true_black` switch turns on and off.
|
||
|
||
---
|
||
|
||
## Denoise Algorithm
|
||
|
||
`minimum` mode with `min_true_black=True` 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.
|
||
|
||
There is also a safety net: if denoising removes more than half of the strokes
|
||
(a sign that real lines were deleted), the pre-denoise result is used instead,
|
||
so the output never goes blank.
|
||
|
||
---
|
||
|
||
## Parameter Reference
|
||
|
||
### Shared
|
||
|
||
| Parameter | Default | Meaning |
|
||
|-----------|---------|---------|
|
||
| `method` | `"skeleton"` | `"skeleton"` or `"minimum"` |
|
||
| `line_width` | `2` | Final stroke width (skeleton mode) |
|
||
| `protect_areas` | `[]` | List of `(x1,x2,y1,y2)` rectangles never cleaned |
|
||
|
||
### Skeleton mode
|
||
|
||
| Parameter | Default | Meaning |
|
||
|-----------|---------|---------|
|
||
| `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 |
|
||
| `density_close` / `density_open` | 41 / 61 | Density-blob morphology |
|
||
| `text_pad` | 40 | Padding around the detected text rectangle |
|
||
| `dark_v` | 160 | Dark-pixel threshold |
|
||
| `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 |
|
||
| `enable_color_smoothing` | `True` | Smooth broad flat colour regions |
|
||
| `color_sat_min` | 60 | Saturation floor for "flat colour region" |
|
||
| `color_area_range` | (3000, 25000) | Plausible area range for such a region |
|
||
| `meanshift_sp` / `meanshift_sr` | 30 / 60 | Mean-shift smoothing parameters |
|
||
|
||
### Minimum mode
|
||
|
||
| Parameter | Default | Meaning |
|
||
|-----------|---------|---------|
|
||
| `min_true_black` | `True` | Apply the true-black gate |
|
||
| `min_mean` / `min_chroma` | 180 / 60 | True-black criterion bounds |
|
||
| `min_kernel` | 2 | Minimum-filter radius |
|
||
| `min_otsu` | `True` | Use Otsu instead of a fixed 128 threshold |
|
||
| `min_ratio` | 1.5 | Fallback to adaptive threshold below this ink % |
|
||
| `denoise` | `"strong"` | Denoise level (true-black on) |
|
||
| `denoise_area` | 30 | CC removal area (true-black on) |
|
||
| `m2_noise_area` | 20 | Despeckle area (true-black off) |
|
||
| `m2_short_area` / `m2_short_len` | 40 / 25 | Short-fragment removal |
|
||
| `m2_close_k` | 2 | Close kernel before thinning |
|
||
| `m2_dist_min` | 0.5 | Distance threshold for thinning |
|
||
|
||
---
|
||
|
||
## 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 the true-black gate is a switch, not a constant.** Some images are
|
||
genuinely black line work on light paper, where the gate is a pure win. Others
|
||
are dominated by saturated colours, where the gate discards most of the drawing.
|
||
Neither setting is universally right, so both are exposed and the default
|
||
(`True`) preserves the long-standing behaviour.
|
||
|
||
**Why the flat-color step keys on saturation, not hue.** An earlier revision
|
||
looked for a specific hue range and also assumed the region lay on the left half
|
||
of the image, which only worked for one particular poster. Saturation alone has
|
||
no such assumptions and generalises to any layout or palette.
|
||
|
||
**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. **Raise the resolution** or use `minimum` mode.
|
||
* `skeleton` mode on large images is slow (tens of seconds) because of the
|
||
full-frame mean-shift pass. Set `enable_color_smoothing=False` to skip it.
|
||
* Heavy JPEG artefacts in the source may survive as small debris; raise
|
||
`--denoise-area` (true-black) or `--m2-noise-area` / `--m2-short-area`
|
||
(no-true-black) 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
|
||
```
|
||
|
||
No file in this project contains a built-in absolute path. Every entry point
|
||
takes its input and output paths from the caller.
|
||
|
||
---
|
||
|
||
## 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.
|