可能是 SOTA 的像素图修复开源方案

GitHub:xprite-pixelate

待修复的像素图

背景

上一期分享的图片转像素图的文章受到了很多关注,这周继续搞研究。

最近在像素图漫漫入门路上,遇到了两个痛点场景。

像素图临摹

看到合适的像素图想要临摹练习,截屏或者下载的图片没法直接导入软件:

  1. 图片上的一个格子会对应多个像素点,不是真正 1:1 的
  2. 社交平台上的图片往往经过了平台压缩,像素格的边缘会出现伪影
在视频教程上看到的像素图
在视频教程上看到的像素图
截图导入后
截图导入后

AI 生图修复

下面是我用相同提示词在不同生图模型下生成的嘎子,其中除了 Seeddream 是不是看上去都像模像样的。

Nano Banana
Nano Banana
GPT Image
GPT Image
Seeddream
Seeddream
Grok
Grok

然而这些主流生图模型其实都无法生成真·像素图,放大看的话就能发现很多问题:

Nano Banana

GPT Image

Seedream

Grok

背景

❌ 伪透明棋盘格

真透明

❌ 伪透明棋盘格

❌ 伪透明棋盘格

半透明像素

无

❌ 有

无

无

留白

少

少

中

❌ 多

格宽漂移
(像素有大有小)

❌ 13%

7%

❌ 19%

❌ 14%

格内杂色
(涂抹感)

❌ 3.6%

1.4%

❌ 3.2%

2.9%

格式
(JPEG 压缩伪影)

❌ JPEG

PNG

PNG

❌ JPEG

目标与大致思路

于是我想实现一个算法,输入是一些伪像素图,输出是真像素图。

而我之前遇到的问题大致可以分为两类:

  • 网格问题:像素格子的尺寸和偏移不对齐
  • 颜色问题:色彩杂乱,通常的像素图色盘往往只有少数几个颜色。

因此可以一个管线表示:

像素图修复流程

第一步:网格还原

调研

让我们看看现在主流的一些开源解决方案是怎么做的。先给个完整图片:

文章配图

proper-pixel-art

proper-pixel-art 是 Kenneth Allen 做的开源库(MIT):核心思想是用 Canny + 霍夫线恢复网格,再落到真分辨率。

文章配图

在网格恢复上,proper-pixel-art 经过了以下的流程:

  1. Canny 找边:先用 Canny 边缘检测加一次形态学闭运算把边连起来
  2. 概率霍夫线:再用概率霍夫变换找直线段,只留下接近水平或接近竖直的那些(偏差 15° 以内),当作格子线的初稿。
  3. 吸附投影峰:把每条已经找到的线微移到附近最高的投影峰上。这一步主要修改线的位置,不增减格线。
  4. 补齐网格:用间距中位数当格宽,在稀疏处插入缺线。

流程看起来合理,但在部分场景上仍有问题:

  • 多斜边场景初稿稀疏:霍夫要求线段至少 50 像素长、投票数过阈值。对于边缘是斜线或曲线的场景,能抽出来的长直线有限,会导致初稿非常稀疏,指导作用有限。
  • 格宽被取成整数:由于使用的数据类型是 int,即便先放大 2 倍再找线,折回原图只能是 0.5 的倍数。

在格里菲斯图场景下:

  • 格宽取整成 17 px(真实约 18.5 px),画出部分被切成 37×40 格,比实际多出 4 列、5 行左右。
  • 它在每两条初稿线之间各自等分,有些地方切出 8 px 的窄格。同一个画出的像素会被切成一窄一宽两格,输出就出现双像素(比如眼睛部分)。

pixel-art-fixer

pixel-art-fixer 由 Retro Diffusion 维护(开源,MIT):三路与相位无关的格宽检测先投票,意见不一时用证据栈仲裁,再把「放置」和「上色」分开重建。有 Rust 实现,很快。

文章配图
  • 候选算法
    • 自相关(autocorr):看图在平移多少像素后最像自己,找重复间距
    • 游程梳(runlengths):统计同色连续段长度,找重复间距
    • 自相似(selfsim):比局部块在候选格宽下是否重复出现
  • 仲裁因子
    • 融合频谱证据:看频域里哪个格宽峰更稳
    • 格内方差对比:格内越平、格间差越大越好
    • 倍频清理:排除半格、两倍格等假峰

fixer 的格宽找得很准,但它的问题在于:

  • 整张图只用一套等宽的格子:从左边缘按同一宽度等分,没有相位,也不对单条切割做吸附。当切割线穿过轮廓像素格中间的时候,就容易丢失细节,比如图中左眼边的轮廓线就消失了。

Perfect Pixel

Perfect Pixel 是 theAmusing 做的开源库(MIT):用 FFT 估格数,Sobel 把格线吸到边上,再按格取样。

文章配图
  1. FFT 估格数:对灰度图做 FFT,在行列投影上找峰,得到整数格数;失败时退到梯度峰间距的中位数。
  2. Sobel 精修:每条格线在 ±25% 格宽内吸到最强的边上。
  3. 按格取样:默认取格子中心像素。

问题也集中在这里:

  • 整数格:和 proper 一样,小数格宽会被拧歪。
  • 无透明通道:透明底被当成白色,不仅如此,透明图的轮廓附近也会留下灰白杂点。比如单手剑边缘附近就出现了很多杂色像素。

我的方案

既然每个方案或多或少都有点问题,我决定站在巨人肩膀上,再取长补短,缝合出一个更好的方案来。

取长:

  • 多路候选 + 裁决结构(Fixer)

补短

  • 需要被决策的不能只是格宽,最好是整个网格(Fixer)
  • 格宽需要支持浮点数(Proper、Perfect)
  • 需要对半透明像素进行二值化处理(Perfect)

然后我就把这个思路扔给了 AI(前后转手了 grok、codex、claude 并与它们大战了两周),历经坎坷地得到了下面的一套解决方案:

Xprite Auto 网格还原流程

看着很复杂,不过通俗点讲的话,可以理解为一个擂台赛。

  1. 海选
  2. Round #1:淘汰过细的网格
  3. 用晋级选手的格宽派出几位跟着边走的外卡
  4. Round #2:然后所有选手和擂主「原图」同场打分
  5. Round #3:复活赛,让多方投票支持的尺寸有机会翻盘

比如在格里菲斯这个图上,就是基于 Pitch 的边缘路径外卡选手取得了最终的胜利✌️:

文章配图

简单介绍下原始参赛选手和三轮的评委:

选手

怎么提出网格

Uniform

把颜色边按列、按行叠加,找一套等宽格

Edge

把每条切割线钉在清晰的颜色边上,格宽可以不等

Frame

假设整张图是 n 格均匀放大,把每个 n 都试一遍

Pitch

搜格宽和第一条线的位置,先粗搜再细搜

Comb

找颜色变化的重复周期

Boundary

数图片边缘同色连续段的长度

Distance

量同一行里相邻两条颜色边的距离

评委

评价

#1 边缘一致性

衡量颜色边落在切割线上的比例。如果存在格宽至少大 1.5 倍、一致性又明显更高的网格,较细的网格就被淘汰

#2 描述长度

得分 = 编码信息量 − 相位证据 + 倍频惩罚,越低越好
• 编码信息量:用这套网格重建整张图所需的信息量,包括格内颜色、残差和网格参数
• 相位证据:所有切割平移半格后,信息量增加多少。真网格增加得多
• 倍频惩罚:只平移部分切割时信息量几乎不变,说明它是真网格的细分

#3 投票

冠军只由一路提出,而相差一格的尺寸有多路支持、得分也接近时,改用后者。
AC+RL 共识的格数和冠军不一致时,改选该尺寸下得分最低的网格。
透明留白多的图,只有相差两格以上才改选。

第二步:颜色选取

网格定好以后,下一件事是给每一格定颜色。无论是截图还是 AI 生图,格子里通常都有杂色。一格里往往有一百多个源像素。颜色选取就是从这些源像素里定出一个代表色,写进输出图的那一格。

截图
截图
AI 图
AI 图

取色方式

文章配图
  • 平均(mean):全部像素取平均。软边上最容易发灰,过渡色会混进来。格里菲斯上输出 486 种颜色,有 135 格和格子主色相差明显(ΔE > 10)。
  • 中位数(median):红、绿、蓝各自取中位数。对零星杂点不敏感,但三个通道分开取,拼出来的颜色可能格子里根本没有。
  • 众数(mode):取格子里出现最多的颜色。结果一定是格子里真实出现过的颜色。
  • 中心点(nearest):只取格子正中心的一个像素。最快,但中心正好落在过渡色上时就错了,剑刃上能看到几格灰白。
  • 两阶段(two-stage):先把整图量化成十几到几十种颜色,每格按中心加权投票决定属于哪一类;再只用原图里属于这一类的像素求平均,作为这一格的颜色。

根据 benchmark(后面会讲)的反馈,最终我使用了两阶段进行取色。

颜色合并

在 CIELAB 里把几乎一样的颜色先归成一组,再按需要的颜色数做 k-means,聚类时让稀有颜色也能单独成为一类。

格里菲斯上,两阶段输出 271 种颜色,合并后剩 26 种,肉眼几乎看不出差别。

文章配图

透明像素二值化

前面提到AI 图的轮廓可能存在半透明渐变。这里对他们进行二值化,透明度超过 50% 就完全透明,否则就按不透明走。

结果

最后看看效果吧,除了单手剑上有个像素点的缺陷,其他地方是不是都还不错。

文章配图

Benchmark

标题里,我提到这个方案可能是当前 SOTA 的开源方案,那必然得上评测来佐证了。

不过这块比较小众,找了很久也只看到一个 pixel-bench(和 fixer 是一家的,所以可以看到 fixer 在上面拿到的分很高)。它的原理是:

  1. 拿真实的原生像素图当输入。
  2. 用内置算法把它们弄成各种「假像素」损伤(放大、模糊、JPEG、漂移、AI mush 等)。
  3. 再测修复算法能不能把这些有问题的图还原回真实像素图。

但实际使用下来,没法覆盖我之前遇到的半透明度、大留白、格宽漂移等问题。因此我在上面增加了一组假像素处理(ai_fake):

类别模拟什么
gen_clean软边、格内近乎平坦、格宽 8–20
gen_sparse画只占画布的 20%–35%
gen_white灰白底代替透明
gen_soft_edge3–5 像素的半透明描边
gen_webp漂移、切割抖动、噪点、WebP
gen_halo_jpeg锐化光晕 + JPEG
gen_painterly格内笔触明暗
gen_small_pitch格宽 4–8
gen_large_pitch格宽 24–48
gen_wobble强漂移 + 强抖动
gen_pair一张画布上并排两个角色
gen_shadow角色脚下半透明投影

无论在原始 pixelbench 还是补充后的 AI 评测上,我的方案 Xprite Auto(因为放在我的 Xprite 网站里,作为自动模式预选,所以随便取了这个名字作为代号)取得了不错的成绩:

新增评测 ai_fake - 网格正确率(越大越好)
新增评测 ai_fake - 网格正确率(越大越好)
原始 pixel bench - 网格正确率(越大越好)
原始 pixel bench - 网格正确率(越大越好)
新增评测 ai_fake - 颜色差异(越小越好)
新增评测 ai_fake - 颜色差异(越小越好)
原始 pixel bench - 颜色差异(越小越好)
原始 pixel bench - 颜色差异(越小越好)

不过由于候选方案太多了,所以性能上目前是垫底 🧎🏻

文章配图

返回全部文章