栅格参考图 → SVG 规格
何时用
用户丢来一张图说「按这个改图案/图标」。肉眼估色估位必偏,而且低分辨率截图 (聊天软件压过的、缩放过的小图)会让「看着像」和「实际色值」差很远。全程用像素测量。
五步流程
1. 网格定位,不要手填坐标
参考图通常是等距排布的元素表。用浅灰分框线(低饱和 + 中亮度)做行/列投影找边界:
def is_line(r,g,b):
mx,mn = max(r,g,b), min(r,g,b)
return mx-mn < 16 and 170 <= mx <= 240 # 浅灰框线
# 对整列像素计数 → 峰值即竖线位置;相邻很近的两条合并(同一根线的两沿)
坑:别用 --window-size 之外的方式硬编码坐标——不同图的边距差几个像素,
换算到 viewBox 就是几度偏差。也别一次全图统一阈值,某些行的框线更淡会整行漏掉,
必要时逐行检测。
2. 配色:腐蚀一次取量化众数
# 分类:max-min < 50 或 s < 0.42 的算灰/白,不参与配色
# 腐蚀:只保留「上下左右四邻都是同色系」的像素 = 色块/笔画内部
mode.setdefault(family, Counter())[px] += 1
# 再按 8 级量化合并(压缩噪点会把同一个色散成好几个近似值),取众数
不要用「最饱和 N% 像素的中位数」——两个坑:
- 被混色暗边带偏(暗色像素的 HSV 饱和度反而更高,越暗越"饱和");
- 被更粗的元素带偏。实测一张麻将牌面参考图上,万字那一行的蓝是偏紫的
#241464,而筒点是#3c5474,根本不是同一个色。所以要逐行/逐区域统计, 混在一起只会得到一个谁都不像的中间值。
3. 元素测量:连通域 + 换算
4/8 连通域扫出每个图形元素,输出 bbox / 中心 / 尺寸,再线性换算到 viewBox:
vbx = cx / cell_w * 100 # viewBox 宽 100
vby = cy / cell_h * 138 # viewBox 高 138 —— 注意用**各自的高度**换算
关键:参考图的单格宽高比往往和目标不同(例:参考 84×105 = 0.80,实际牌面 42×58 = 0.72)。所以 x 按宽度百分比、y 按高度百分比分别映射,牌面会被"拉高", 这正是真实牌的样子;千万不要按同一个比例缩放(元素会变形或对不齐)。
连通域会碎片化(细笔画的环被走线切断),所以:
- 先按中心坐标聚类再读位置,而不是指望每根笔画出一个完整框;
- 同一元素被拆成 2~3 块时,取它们 bbox 的并集。
4. 比例关系靠「剖面」而不是目测
同心圆、环、渐变这类结构,沿半径统计彩色像素占比就能读出每层半径:
for r in [i/2 for i in range(0, 34)]:
hit = sum(1 for a in range(72) if colored(cx + r*cos(a), cy + r*sin(a)))
print(r, hit*100/72) # 100=纯色环,≈0=白环
实测得到「实心圆 → 白 0.63R → 彩 0.50R → 白 0.24R → 彩芯 0.13R」这种精确比例, 比"看起来大概是一半"靠谱得多。
5. 验证:并排对照页 + 结构计数断言
(a) 并排对照页——用 CSS background-position 裁参考图,和本项目渲染并排:
<div class="ref" style="
background-image:url('ref.png');
background-size:1066px 536px; /* 原图尺寸 × 放大系数 */
background-position:-74px -32px; /* 单格左上角坐标 × 系数,负值 */
padding-top:125%;"></div> /* 单格高/宽 百分比,保持比例 */
整页截图后一眼就能看出「哪一张对不上」。这是唯一能证伪的手段—— DOM 断言只能证明 SVG 里有这个字符串,证明不了它画出来像不像。
(b) 结构计数断言——把「语义」变成可数的结构,写进测试:
n + '筒' → 恰好 n 个圆点(每点 5 层同心圆)→ 数 <circle>
n + '条' → 恰好 n 根竹 → 数 <rect>
六条应是 3 列(不是 2 列)→ new Set(x 坐标).size === 3
九条中间一列为红 / 九筒中间一行为红 → 数色值出现次数
这类错误肉眼极难发现(六条排成 2×3 看着也挺自然),必须靠计数兜住。
新断言必须亲手改坏一次确认它会失败。 验证时注意:改「点的排列顺序」不会 触发列数断言(列数没变),要真的改列数。永远通过的新断言等于没有断言。
描边宽度的亚像素陷阱
SVG 单位 ≠ 屏幕像素:牌面 42px 宽时缩放比只有 0.42,viewBox 里 3 个单位的描边 只有 ~1.26px,再细就会被抗锯齿糊成浅色。所以:
- 描边宽度设下限:
max(3, min(w,h) * 0.24); - 参考图上很亮的颜色(对比度 < 4.5:1)直接用会显得"没画上去",
按目标尺寸适当压暗并记录理由(本项目把参考图的绿
#3c844c(4.23:1) 压到#16663a(6.27:1))。
配套脚本
项目里留一份 scripts/read_ref_tiles.py(同类项目可直接抄):网格定位 + 逐行配色 +
逐格连通域测量,输出已换算好的 viewBox 坐标。
工作流顺序(省时间的排法)
- 先跑测量脚本,把全部尺寸色值拿到手(别边看边猜);
- 按测量值一次性写完绘制代码,
node --check过语法; - 生成对照页截图 → 修正偏差;
- 补结构计数断言 + 负向验证;
- 最后才做真机尺寸(1:1)可读性检查:用「外层 div 精确裁剪 + 内层 iframe 负 margin」 绕过无头浏览器视口钳制(Windows 无头最小视口宽 ~504),才能看到真实像素。
