mcpField
I
image-to-svg

image-to-svg

github.com/oaustegard创意与媒体157125 次安装1 天前更新
代码仓库

关于 image-to-svg

一个 Python 图像处理流程,通过基于像素的颜色量化和轮廓提取,将栅格图像复现为 SVG。它提供填充与线稿组合流程、调色板预设及批量生成,适用于创作者和开发者。

主要功能

  1. 01将栅格图像转换为 SVG
  2. 02按图像类型选择处理模式
  3. 03将线稿提取为 SVG 描边
  4. 04使用调色板预设重新映射颜色
  5. 05批量生成多个变体

适用场景

  1. 01矢量化徽标和图标
  2. 02将绘画或照片复现为 SVG
  3. 03创建波普艺术调色板变体
  4. 04从一张图像批量处理多种风格

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/oaustegard/claude-skills --skill image-to-svg

安装后在 Claude Code 中输入/image-to-svg确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

CHANGELOG.md
scripts/
lines.py
nn_assign.c
pipeline.py

详细介绍

将图像复现为 SVG

使用数据驱动的颜色量化和轮廓提取,将栅格图像转换为逼真的 SVG 复现图。绝不要根据视觉解读手绘形状——始终从实际像素数据中提取几何信息。

核心原则

相信数据,而不是想象。 Claude 对图像的视觉解读在精确匹配颜色、定位形状和判断空间关系方面并不可靠。每个形状、颜色和位置都必须通过对源像素的计算分析得出。

快速开始

pip install opencv-python-headless scikit-image scipy scikit-learn --break-system-packages -q
apt-get install -y librsvg2-bin -qq
import sys
sys.path.insert(0, '/mnt/skills/user/image-to-svg/scripts')
from pipeline import image_to_svg

svg, flow = image_to_svg("source.jpg", mode="painting")

with open("output.svg", "w") as f:
    f.write(svg)

flow.summary()  # timing + status per step

模式选择

观察图像并思考:“它是否有平滑渐变或硬边缘?”渐变 → 更高的 K 值。硬边缘 → 更低的 K 值。

模式K最适合深色形状筛选
"graphic"28Logo、图标、康定斯基风格作品、扁平设计宽松(保留细线)
"illustration"40漫画、编辑插画、数字艺术适中
"painting"56文艺复兴风格、印象派、水彩画标准
"photo"64肖像、风景、静物标准(防止产生木刻伪影)

默认值为 "painting"。不确定时,从这里开始。

权衡:K=64 会生成约 2300 个形状(约 1.2MB SVG),而 K=28 会生成约 1000 个形状(约 550KB)。这些数据是在 v1.9.0 的紧凑输出格式推出前测得的;新格式体积约缩小至原来的 2.6 倍(参见输出格式)。K 值增加时,处理时间大约翻倍。对于照片,色调渐变质量的提升非常显著;对于图形艺术,这种提升则毫无用处。

所有模式默认值(K、dark_lum、compactness_min 等)都可以通过 **kwargs 覆盖:

svg, flow = image_to_svg("source.jpg", mode="graphic", K=12, min_area=20)

合成式处理管线(线稿)

对于以线条、笔画和几何形状为主的图像(康定斯基风格作品、建筑图纸、技术插画、漫画墨线作品),标准的纯填充处理管线会生成锯齿状的填充多边形,而不是干净的笔画。合成式处理管线通过两个阶段解决这个问题:

阶段 1 — 线条提取:通过形态学腐蚀分离细小特征 → 骨架化为 1px 中心线 → 使用霍夫直线检测 → 合并共线线段 → 测量笔画宽度 → 采样颜色。输出带有 stroke-width 的 SVG <line> 元素。

阶段 2 — 填充提取:从图像中抑制线条区域(使用中值模糊生成的局部背景估计替换)→ 对清理后的图像运行标准 K 均值量化 → 提取轮廓 → 使用 <path> 填充。

合成:填充绘制在笔画下方,并置于分层的 <g> 组中。

# Auto-detect: classifies input and routes automatically
svg, flow = image_to_svg("kandinsky.jpg", mode="graphic")

# Force compositional pipeline
svg, flow = image_to_svg("technical_drawing.png", mode="graphic", pipeline="compositional")

# Force fill-only (previous default behavior)
svg, flow = image_to_svg("photo.jpg", mode="painting", pipeline="fill")

处理管线选择(pipeline 参数):

值行为
"auto"(默认)根据边缘密度、亮度双峰性和霍夫直线数量对输入进行分类。图形艺术使用合成式处理管线,照片使用纯填充处理管线。
"fill"强制使用纯填充处理管线。适用于照片、绘画,或合成式处理产生不理想结果的情况。
"compositional"强制使用两阶段处理管线。适用于线稿、技术图纸或已知包含线条的墨线作品。

自动分类启发式规则:边缘密度较高(>5% 的像素为边缘像素)且亮度分布呈双峰(>0.35 的双峰系数),或直线密度较高(每 10k 像素有 >3 条霍夫直线)时,图像会被分类为图形。

SVG 输出结构(合成式处理管线):

<svg ...>
  <rect ... />        
  <g id="fills" stroke-width="1" stroke-linejoin="round">  
    <path class="c3" d="m12 40l5-2..z"/>
  </g>
  <g id="strokes">    
    <line x1="..." y1="..." x2="..." y2="..." stroke="#000" stroke-width="2.5" stroke-linecap="round"/>
  </g>
</svg>

笔画宽度控制:垂直于检测到的每条直线进行测量,然后缩放至 0.65 倍,并限制在 4.5 个 SVG 单位以内。这可以防止较粗的特征渲染成臃肿的笔画,同时保持细线清晰。

当前限制——仅支持直线:霍夫变换可检测直线线段。弧线、螺旋线等曲线笔画目前不会被提取为笔画,而会进入填充阶段。后续工作:在骨架分支上使用 cv2.fitEllipse 或样条拟合。

调色板重映射(Warhol 效果)

将结构与颜色分离:K-means 用于查找区域,调色板重映射用于分配鲜明的颜色。由此可生成丝网印刷/波普艺术效果。

# Named preset
svg, flow = image_to_svg("photo.jpg", mode="graphic", K=4, palette="pop")

# Custom hex list (darkest → lightest mapping order)
svg, flow = image_to_svg("photo.jpg", mode="graphic", K=8,
    palette=["#000", "#dc143c", "#ff69b4", "#ffd700", "#32cd32", "#00bfff", "#ff8c00", "#f5f5f5"])

# Override background separately
svg, flow = image_to_svg("photo.jpg", mode="graphic", K=4, palette="ocean", bg_color="#000000")

内置预设:bw、mono3、mono4、pop、pop2、neon、warhol4、warhol6、warhol8、sunset、ocean

工作原理:按亮度对不同的形状颜色排序。按比例映射调色板条目:palette[0] 替换最暗的簇,palette[-1] 替换最亮的簇。除非设置了 bg_color,否则背景默认为调色板中最亮的条目。调色板长度无需与 K 完全一致;颜色会按比例分箱。

人像:即使使用鲜明的调色板,也应将 K 设为 16–24。面部特征(眼镜、胡须、眉毛)需要低 K 会消除的色调范围。经验法则:调色板长度 ≈ K/3,以实现干净的亮度分箱。K=8 且使用 4 色调色板时,面部会变成一团无法区分的色块。

对比度预处理警告:外部增强对比度(contrast-stretch、sigmoidal-contrast)可能会干扰背景检测。该管线的边缘接触启发式假设亮度分布未经改动——激进的色调映射会将主体色调推入与背景相邻的分箱,导致误分类(例如,将深色夹克区域分类为背景,并映射为调色板中最亮的颜色)。如果看到主体区域被替换成背景色,请先尝试不进行预处理。管线自带的双边模糊和可选的 kuwahara/oilpaint 会处理色调分离。

背景检测覆盖(bg_clusters)

控制哪些簇被视为背景:

# Auto-detect (default) — edge-contact heuristic
svg, flow = image_to_svg("photo.jpg", mode="illustration", K=20, palette="warhol6")

# Disable — no clusters removed, no background rect color override
svg, flow = image_to_svg("photo.jpg", mode="illustration", K=20, palette="warhol6", bg_clusters=0)

# Force specific cluster indices (from quantize step's sorted_clusters output)
svg, flow = image_to_svg("photo.jpg", mode="illustration", K=20, palette="warhol6", bg_clusters=[2, 5])

当调色板重映射已明确控制所有颜色,且背景检测造成干扰时,请使用 bg_clusters=0。当你知道哪些簇属于背景,但启发式识别错误时,请使用 bg_clusters=[list]。

人像波普艺术配方(Warhol 风格)
# Key: enough K for facial features, palette length ~K/3, modest smoothing
# Do NOT apply contrast preprocessing — it breaks background detection.
results = image_to_svg_batch("portrait.jpg", [
    {"name": "hot",   "mode": "illustration", "K": 20, "smooth": "kuwahara:6",
     "palette": ["#000", "#D4145A", "#FF6B9D", "#FF85C0", "#FFD700", "#FFEF82", "#FFF8DC"]},
    {"name": "cool",  "mode": "illustration", "K": 20, "smooth": "kuwahara:6",
     "palette": ["#0D0035", "#4A00E0", "#7B68EE", "#00D4FF", "#7FFFD4", "#B0FFE0", "#E0FFFF"]},
    {"name": "earth", "mode": "illustration", "K": 20, "smooth": "kuwahara:6",
     "palette": ["#1a0a00", "#8B4513", "#CD853F", "#DEB887", "#F5DEB3", "#FAEBD7", "#FFF8DC"]},
    {"name": "neon",  "mode": "illustration", "K": 20, "smooth": "kuwahara:6",
     "palette": ["#0d0d0d", "#ff00ff", "#00ff00", "#ffff00", "#00ffff", "#ff69b4", "#f5f5f5"]},
], svg_width=700)

此配方有效的原因:K=20 能保留足够多的色调簇来呈现面部结构(眼镜、胡须、眉脊)。7 色调色板可提供约 K/3 个亮度分箱,足以区分面部特征,又不会显得浑浊。kuwahara:6 可以平滑纹理而不抹除边缘(:12 会抹掉眼镜)。使用原始图像作为输入,只进行管线内的平滑处理;不进行外部对比度调整。

ImageMagick 预处理(smooth)

在量化前使用 ImageMagick 保留边缘的滤镜,可减少 20–45% 的形状数量和 SVG 文件大小。需要在 PATH 中安装 ImageMagick(Claude.ai 容器已预装)。

# Oilpaint: bold, painterly smoothing (default strength=8)
svg, flow = image_to_svg("photo.jpg", mode="photo", smooth="oilpaint")

# Stronger smoothing = fewer shapes, more stylized
svg, flow = image_to_svg("photo.jpg", mode="illustration", K=32, smooth="oilpaint:12")

# Kuwahara: subtler, preserves more structure (default strength=5)
svg, flow = image_to_svg("photo.jpg", mode="painting", smooth="kuwahara:7")

# Works with batch API too
results = image_to_svg_batch("photo.jpg", [
    {"name": "raw",      "mode": "photo"},
    {"name": "smooth",   "mode": "photo", "smooth": "oilpaint"},
    {"name": "stylized", "mode": "illustration", "K": 32, "smooth": "oilpaint:12", "palette": "pop"},
])

可用滤镜:oilpaint(ImageMagick -paint)、kuwahara(ImageMagick -kuwahara)。追加 :N 可设置自定义强度。

工作原理:IM 滤镜在管线的双边模糊和高斯模糊之前运行。二者是在不同尺度上保留边缘的平滑器:IM 处理粗糙纹理,双边模糊处理精细细节。结果是 K-means 区域更加干净,碎片化形状更少。

测量结果(1206×1597 照片,K=32):

smooth形状数SVG 大小减少幅度
none33811868KB—
oilpaint (8)23851329KB-29%
oilpaint:1218421065KB-43%
kuwahara (5)27191453KB-22%
kuwahara:720001152KB-38%

管线架构

使用 flowing DAG 运行器。输入相互独立的步骤会并行运行。

仅填充管线(pipeline="fill")
preprocess → quantize → ┬─ detect_background ─┬─ extract_contours → assemble_svg
                        └─ edge_map           ─┘
组合式流水线(pipeline="compositional")
classify_input ──→ extract_lines ──→ suppress_line_regions ──→ [fill pipeline on cleaned image]
                        │                                              │
                        └──────────── lines ───────────────────→ assemble_compositional ←── fills

步骤(仅填充):

  1. preprocess — 双边滤波 + 高斯模糊(保留边缘地去除纹理)
  2. quantize — 按选定的 K 值进行 K-means 颜色量化
  3. detect_background — 根据边缘接触情况识别背景聚类(与 edge_map 并行)
  4. edge_map — 使用 cv2.Sobel 进行 Sobel 边缘检测(与 detect_background 并行)
  5. extract_contours — 按聚类提取轮廓,感知深色区域并防止木刻效果(膨胀 d=1;描边负责填补间隙)
  6. assemble_svg — 使用按 Z 顺序排列的画家算法进行组装,并通过描边覆盖填充间隙

附加步骤(组合式):

  1. classify_input — 分析边缘密度、双峰性和 Hough 直线数量
  2. extract_lines — 形态学分离细特征 → 骨架化 → Hough 检测 → 合并共线特征 → 测量描边宽度 → 采样颜色
  3. suppress_line_regions — 用中值模糊估算的背景替换线条像素
  4. assemble_compositional — 在描边后方放置填充图层,并在 SVG 中分组
失败时恢复
svg, flow = image_to_svg("source.jpg", mode="photo")
# If extract_contours failed:
flow.override(extract_contours, corrected_shapes)
flow.resume()  # quantize, detect_background, edge_map stay cached

批处理 API

从一张图像生成多个变体,并在 K 值相同的运行之间共享计算:

from pipeline import image_to_svg_batch

results = image_to_svg_batch("photo.jpg", [
    {"name": "photo",   "mode": "photo"},
    {"name": "warhol",  "mode": "graphic", "K": 12, "palette": "warhol4"},
    {"name": "neon",    "mode": "graphic", "K": 12, "palette": "neon"},
    {"name": "sunset",  "mode": "graphic", "K": 12, "palette": "sunset"},
    {"name": "bw",      "mode": "graphic", "K": 16, "palette": "bw"},
], svg_width=1400)

for name, svg in results.items():
    with open(f"{name}.svg", "w") as f:
        f.write(svg)

K 值相同的变体只运行一次流水线(preprocess → quantize → edge_map → extract_contours),然后在组装阶段分支处理调色板映射。这样可确保不同调色板变体的结构一致(形状和路径相同),并为每组共享 K 值节省约 20–60 秒。

批处理模式仍需进行验证。 批处理带来的便利容易让人跳过并排比较——不要跳过。每组 K 值至少渲染一个变体,并在交付前验证。背景检测失败和调色板映射问题如果不渲染就无法发现。

验证流程

每次运行后,都要渲染并进行并排目视比较。 这是必须遵守的要求。

import subprocess
from PIL import Image

subprocess.run(['rsvg-convert', '-w', '1400', 'output.svg', '-o', 'output.png'])

orig = Image.open('source.jpg')
rendered = Image.open('output.png')
target_h = 800
orig_r = orig.resize((int(orig.width * target_h / orig.height), target_h))
rend_r = rendered.resize((int(rendered.width * target_h / rendered.height), target_h))
gap = 20
comp = Image.new('RGB', (orig_r.width + rend_r.width + gap, target_h), (255,255,255))
comp.paste(orig_r, (0, 0))
comp.paste(rend_r, (orig_r.width + gap, 0))
comp.save('comparison.png')
# LOOK AT comparison.png BEFORE claiming success

手动后处理

处理细微的颜色差异

当两个区域的亮度相近、但色相/饱和度不同时,RGB 空间中的 K-means 会将它们合并。请使用HSV 多光谱分析:

hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
h_ch, s_ch, v_ch = hsv[:,:,0], hsv[:,:,1], hsv[:,:,2]

# Separate gray (low saturation) from red (high saturation) at similar brightness
red_mask = ((h_ch < 12) | (h_ch > 168)) & (s_ch > 120) & (v_ch > 80)
gray_mask = (s_ch < 80) & (v_ch > 40) & (v_ch < 120) & spatial_constraint

对于在灰度图中看起来相似、但视觉上有明显区别的颜色,饱和度是关键判别因素。

定位叠加图形

添加未被量化捕获的形状时,请根据 SVG 渲染结果推导坐标,而不是根据源图像。提取流水线会因轮廓简化而改变位置。

# WRONG: extract from source, insert into SVG (coordinate mismatch)
# RIGHT: render SVG → detect gap in render → create shape in render coords → insert
svg_render = cv2.imread('rendered_svg.png')

输出格式

从 v1.9.0 起,填充内容会以紧凑格式写入。在《星夜》中,绘画输出从 3.64 MB 减少到 1.35 MB(gzip 压缩后从 825 KB 减少到 293 KB),在 Chromium 和 librsvg 中的渲染效果一致:

<svg xmlns="http://www.w3.org/2000/svg" id="s1a2b3c" viewBox="0 0 1000 625">
  <style>#s1a2b3c .c0{fill:#3e85bb;stroke:#3e85bb}#s1a2b3c .c1{...}</style>
  <rect width="1000" height="625" fill="#16151e"/>
  <g stroke-width="1" stroke-linejoin="round">
    <path class="c0" d="m793 0l-10.8 2.9-1 14.7..z"/>
  </g>
</svg>
  • 每种调色板颜色使用一个 CSS 类,并限定在每个文档各自的 id 下,因此可以在同一页面内联多个 SVG,而不会发生类名冲突。
  • 连续的同色形状会合并到一个 <path> 中(保持绘制顺序;evenodd 形状仍单独保留),因此元素数量少于形状数量。
  • 路径数据使用相对 m/l 命令,坐标以 10^-precision 为单位的网格表示。precision=1(默认值)与轮廓网格一致,不会造成精度损失;precision=0 会将坐标四舍五入到整数 viewBox 单位,体积约减少 20%,平均像素差异约为 1%。
  • 形状在内部保留绝对的 "M x,y L x,y Z" 路径字符串(flow.value(extract_contours)["shapes"]);只有输出的 SVG 会采用紧凑格式。
  • 若要为颜色添加动画,请指定其类:@keyframes k{50%{fill:#f0f;stroke:#f0f}} #s1a2b3c .c0{animation:k 4s infinite}。

间隙覆盖:stroke=fill

每个填充 <path> 都会使用自身的填充色描边,stroke-width="{gap_stroke}",并采用圆角连接(在 fills 组上设置一次)。这会让每个形状向外扩展自身的填充色,用局部正确的颜色覆盖簇间隙。

自动缩放:gap_stroke 的计算方式为 max(1.0, round(svg_width / source_width))。svg_width=1000 时,500px 的源图得到 gap_stroke=2;1000px 的源图得到 gap_stroke=1。这可以防止“雪花”伪影:形状数量较多的 SVG 在缩小显示时,因描边扩展过度而出现明显的浅色光晕。可显式覆盖:image_to_svg("img.jpg", gap_stroke=3)。

为什么描边比膨胀更适合填补间隙:膨胀作用于轮廓简化之前的二值掩码——会模糊细节。描边作用于 approxPolyDP 之后的最终多边形——能填补所有间隙,包括简化引入的间隙。完全基于矢量;从 v1.9.0 起,描边的开销仅为每种颜色的类中增加一条 stroke 声明。

背景回退:当 detect_background 找不到任何簇时,背景矩形使用 #000000(黑色)而非白色。黑色看起来像阴影;白色看起来像缺失。

膨胀缩减为 iterations=1——仅用于形态学降噪。间隙覆盖完全由描边处理。

反模式

  1. 切勿根据视觉判断手绘形状。应使用 CV 提取。
  2. 切勿在未渲染并比较的情况下声称修复有效。渲染后的比较是唯一的验证方式。
  3. 切勿使用几何图元(圆形、矩形)近似提取出的轮廓。
  4. 切勿从源图像提取坐标并插入 SVG,除非已验证对齐情况。
  5. 切勿全局提升饱和度。应根据测得的 ΔE 针对单种颜色进行调整。
  6. 切勿大幅合并接近背景色的颜色。仅合并与背景色 RGB 距离小于 10 且边缘接触严重的颜色。
  7. 除非收到请求,否则不要使用贝塞尔平滑。简单的 L 形多边形能生成更小的 SVG。
  8. 不要使用大于 3×3 的膨胀核。在 3×3 核上使用 iterations=1——stroke=fill 会在矢量空间处理间隙覆盖,因此膨胀只需封闭噪声孔洞。

已知限制

  • 细线条(仅填充):用于防止照片出现木刻伪影的深色形状筛选,可能会过滤图形艺术中有意绘制的细线。“graphic” 模式会放宽筛选,但非常细的交叉排线仍可能退化。对于线稿输入,请使用 pipeline="compositional"——它会将细小特征提取为 SVG 描边。
  • 曲线(compositional):霍夫变换只能检测直线线段。弧线、螺旋线、手绘曲线等曲线笔画会落入填充阶段,并渲染为填充多边形。后续工作:在骨架分支上使用 cv2.fitEllipse 或样条拟合。
  • 环形/弧形结构:大型深色圆环(如 Kandinsky 的外圆)会分散到多个 K-means 簇中。每个簇的轮廓彼此独立,因此圆环无法形成一个平滑的整体形状。增加深色簇合并步骤会有所帮助。
  • 渐变过渡:无论 K 为何,平滑渐变都会产生阶梯状色带。增大 K 可以减少这种现象,但无法彻底消除。
  • 平行线组(compositional):如果相邻线条之间的垂直距离低于合并阈值(6px),密集排线或网格线可能会被错误合并。目前的合并步骤无法将平行但偏移的线条识别为独立笔画。

依赖项

pip install opencv-python-headless scikit-image scipy scikit-learn --break-system-packages
apt-get install -y librsvg2-bin  # for rsvg-convert

编译加速:如果存在 gcc,首次使用时会自动编译 nn_assign.c(标签分配速度提升 27 倍)。如果不可用,则回退到 numpy。

跨技能依赖(由 pipeline.py 自动解析):

  • flowing — DAG 工作流运行器

评论

支持 Markdown