Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/svgsmith/classify.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
BINARY_MIN_EDGE_DENSITY = 0.02 # ...with sharp edges
EDGE_MAGNITUDE_CUTOFF = 40 # grayscale edge strength counted as a "strong" edge

PHOTO_WARNING = "photographic gradients; vectorization may bloat"
PHOTO_WARNING = (
"photographic gradients; vectorization may bloat — try --flatten-shading for a cleaner result"
)


class Classification(NamedTuple):
Expand Down
12 changes: 12 additions & 0 deletions src/svgsmith/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def _convert(args: argparse.Namespace) -> int:
solid_background=args.solid_background,
background=args.background,
transparent_background=args.transparent_background,
flatten_shading=args.flatten_shading,
detail=args.detail,
out=args.out,
)
Expand Down Expand Up @@ -235,6 +236,17 @@ def build_parser() -> argparse.ArgumentParser:
"subject is preserved even where it shares the background color."
),
)
convert.add_argument(
"--flatten-shading",
action="store_true",
default=False,
help=(
"Collapse soft/glossy shading before tracing (color mode). Smooth "
"gradients (e.g. satin sheen) become clean flat regions instead of "
"shattering into tiny 'scratch' facets — trades fine shading for a "
"cleaner graphic look and a much smaller file."
),
)
convert.add_argument(
"--out",
default=None,
Expand Down
6 changes: 6 additions & 0 deletions src/svgsmith/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class ConvertOptions:
solid_background: bool = False # opt-in: isolate subject, repaint background one solid color
background: str | None = None # exact bg color (#RRGGBB/named); "auto" == --solid-background
transparent_background: bool = False # opt-in: drop the background → transparent SVG
flatten_shading: bool = False # opt-in: collapse soft/glossy shading before tracing
detail: str = "normal" # color detail dial: high | normal | clean | poster
out: str | None = None

Expand Down Expand Up @@ -135,6 +136,11 @@ def convert(input_path: str, opts: ConvertOptions | None = None) -> tuple[str, R
pre_opts = replace(pre_opts, flatten_sigma=flatten_sigma, palette_size=palette_size)
if opts.uniform_outline and classification.mode == "color":
pre_opts = replace(pre_opts, uniform_outline=True)
# Flatten soft/glossy shading before tracing so smooth gradients collapse into
# clean flat regions instead of shattering into scratch facets (opt-in; trades
# fine shading for a cleaner graphic look).
if opts.flatten_shading and classification.mode == "color":
pre_opts = replace(pre_opts, flatten=True, flatten_sigma=0.18, flatten_spatial=8)
# --solid-background is the auto case of --background; an explicit color (other
# than "auto") repaints the detected background to that exact color.
if opts.solid_background or opts.background is not None:
Expand Down
9 changes: 6 additions & 3 deletions src/svgsmith/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class PreprocessOptions:

flatten: bool = False # edge-preserving color flattening (bilateral)
flatten_sigma: float = 0.04 # color sigma; higher = flatter regions
flatten_spatial: int = 4 # bilateral spatial sigma; higher = wider smoothing

quantize: bool = True
palette_size: int = 16 # target palette; T3 preset can inform this
Expand Down Expand Up @@ -71,7 +72,7 @@ def denoise(img: Image.Image, median_size: int) -> Image.Image:
return filtered


def flatten_colors(img: Image.Image, sigma_color: float) -> Image.Image:
def flatten_colors(img: Image.Image, sigma_color: float, sigma_spatial: int = 4) -> Image.Image:
"""Edge-preserving bilateral smoothing to flatten color variation.

Softens gradients and texture *within* regions while keeping edges sharp, so
Expand All @@ -82,7 +83,9 @@ def flatten_colors(img: Image.Image, sigma_color: float) -> Image.Image:
from skimage.restoration import denoise_bilateral

rgb = np.asarray(img.convert("RGB"), dtype=np.float64) / 255.0
smoothed = denoise_bilateral(rgb, sigma_color=sigma_color, sigma_spatial=4, channel_axis=2)
smoothed = denoise_bilateral(
rgb, sigma_color=sigma_color, sigma_spatial=sigma_spatial, channel_axis=2
)
out = Image.fromarray((smoothed * 255.0).round().astype(np.uint8), "RGB")
if img.mode == "RGBA":
out = out.convert("RGBA")
Expand Down Expand Up @@ -254,7 +257,7 @@ def preprocess(image: ImageInput, opts: PreprocessOptions | None = None) -> Imag
if opts.denoise:
img = denoise(img, opts.median_size)
if opts.flatten:
img = flatten_colors(img, opts.flatten_sigma)
img = flatten_colors(img, opts.flatten_sigma, opts.flatten_spatial)
if opts.quantize:
img = quantize_colors(img, opts.palette_size)
if opts.uniform_outline:
Expand Down
17 changes: 17 additions & 0 deletions tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ def test_convert_options_rejects_out_of_range_values(kwargs):
ConvertOptions(**kwargs)


def test_flatten_shading_reduces_facets_on_gradients(tmp_path):
"""--flatten-shading collapses smooth shading, so a noisy gradient traces into
fewer paths than the default."""
import numpy as np
from PIL import Image

rng = np.random.default_rng(0)
ramp = np.add.outer(np.linspace(0, 200, 96), np.linspace(0, 40, 96))
base = np.clip(ramp[..., None] + rng.normal(0, 6, (96, 96, 3)), 0, 255)
Image.fromarray(base.astype(np.uint8), "RGB").save(tmp_path / "gradient.png")
src = str(tmp_path / "gradient.png")

plain = convert(src, ConvertOptions(mode="color", max_iters=2))[0]
flat = convert(src, ConvertOptions(mode="color", max_iters=2, flatten_shading=True))[0]
assert flat.count("<path") < plain.count("<path")


def test_transparent_background_removes_bg_keeps_subject(tmp_path):
"""--transparent-background drops the edge-connected background, leaving the
subject on transparency (region-based, so subject detail is preserved)."""
Expand Down
Loading