一张 2048×1024 的角色参考表整版,四格横排。
如果按等分切,每格正好 512。可我把切出来的分图翻出来一看——文件名上的宽度是 476 / 554 / 556 / 462。
四个数,一个都不等于 512,最大差 50px。
我第一次看到的时候以为是模型随机性。后来才想明白:这不是等分切的,是"量着缝"切的。整版是模型一次生成的,人物站在哪儿、留多少空,模型自己说了算;你只能先量出画面里人与人的空隙在哪,再沿着空隙下刀。
而"怎么量"这件事,我前后翻过两次车。这篇把两次翻车和最后跑通的算法都摊开讲。
最直觉的做法当然是四等分。我把等分线画到整版上,和实际用的裁切线并排——

红虚线是等分点(512 / 1024 / 1536),绿实线是实际下刀的位置(476 / 1030 / 1586)。
看清了:红虚线直接压在脸上,绿线落在两张脸之间的空隙里。第三处差得最狠——50px,在 2048 宽的图上看着不多,但那一刀落在脸颊边缘,切出来的分图就会缺一块下巴。
一处的偏差不算事,问题是我把这八张整版全量了一遍:

整版 | 尺寸 | 等分点 | 实际裁切边 | 偏移 |
|---|---|---|---|---|
角色 A · 头 | 2048×1024 | 512 / 1024 / 1536 | 476 / 1030 / 1586 | −36 / +6 / +50 |
角色 A · 身 | 1536×1536 | 384 / 768 / 1152 | 398 / 830 / 1126 | +14 / +62 / −26 |
角色 B · 头 | 2048×1024 | 512 / 1024 / 1536 | 471 / 1038 / 1579 | −41 / +14 / +43 |
角色 B · 身 | 1536×1536 | 384 / 768 / 1152 | 304 / 700 / 1170 | −80 / −68 / +18 |
角色 C · 头 | 2048×1024 | 512 / 1024 / 1536 | 527 / 1067 / 1574 | +15 / +43 / +38 |
角色 C · 身 | 1536×1536 | 384 / 768 / 1152 | 465 / 811 / 1075 | +81 / +43 / −77 |
角色 D · 头 | 2048×1024 | 512 / 1024 / 1536 | 490 / 1028 / 1541 | −22 / +4 / +5 |
角色 D · 身 | 1536×1536 | 384 / 768 / 1152 | 384 / 768 / 1152 | 0 / 0 / 0 |
八张里只有一张是能等分的(角色 D 的身体图)。其余七张的最大偏移是 −80 和 +81——在 1536 宽的身体图上,等于把五分之一格宽整个挪走。
结论:等分切法在这类整版上不可用。
既然不能等分,那就找缝。缝是什么?是两格之间的背景。
那么,"背景"怎么认?我第一反应是——背景当然是白的,三通道都大于 245 就算背景。
这个假设错得离谱。

我把八张整版的实际底色采出来:最亮通道只有 110 ~ 190。
全部落在 245 以下。 按 >245 判背景,这些底色会被整体判成"前景",一列干净缝都找不出来。
原因很朴素:这些整版根本不是纯白底,而是浅灰蓝 / 浅灰粉的影棚渐变底,底下还压着一层投影。我拿一次"想当然"去假定生成底的颜色,直接被自己的采样打脸。
纪律:不要对生成的底色做任何先验假设。要判背景,一律"就地采样"。
正确的做法是:不定义"背景是什么颜色",而是"每行各自取一个背景基色,再比差"。
整版是纵向渐变的,所以基色必须逐行取——全局取一个色,会把整片渐变背景误判成前景。
一个反直觉的点:我从每行最右侧 20 列取中位色当作该行的背景基色。为什么是最右边?因为四格横排时,第 4 格的右侧一定是纯背景区。这一招对头图、身体图都成立。
import numpy as np
from PIL import Image
BG_TOL = 22 # 与逐行背景基色的最大通道差,超过算前景
SMOOTH = 9 # 列密度的平滑窗
def density(path):
a = np.asarray(Image.open(path).convert("RGB")).astype(np.float32)
h, w, _ = a.shape
bg = np.median(a[:, w - 20:, :], axis=1) # (h,3) 每行的背景基色
diff = np.abs(a - bg[:, None, :]).max(axis=2) # 每像素与所在行基色的最大通道差
fg = (diff > BG_TOL) # 前景掩码
y0, y1 = int(h * 0.06), int(h * 0.96) # 掐掉顶部 6% 与底部 4%
cnt = fg[y0:y1, :].sum(axis=0).astype(np.float32) # 每列的前景像素数
sm = np.convolve(cnt, np.ones(SMOOTH) / SMOOTH, mode="same")
return sm, w, h四个参数,每一个都有来历:
参数 | 取值 | 为什么 |
|---|---|---|
背景采样位置 | 每行最右 20 列 | 第 4 格右侧一定是纯背景;全局取色会被纵向渐变骗 |
容差 | 22 | 对浅灰蓝底最稳。调低会把渐变噪声算成前景,调高会漏掉薄纱 |
掐头去尾 | y 6% ~ 96% | 避开顶部的标注文字与底部的投影/鞋 |
平滑窗 | 9 | 抹掉单列噪声,又不至于把 100px 级的窄缝抹平 |
把每列前景像素数画成曲线,答案就自己浮出来了:

四个峰 = 四个视图,三个谷 = 三处缝。
红虚线是等分点,绿实线是谷底。可以看到绿线每次都精准落在谷的最深处——而且,谷底就是实际裁切边。角色 A 头图自动量出的 476 / 1030 / 1586,跟文件名里那三个数一分不差。
这就是开头那个问题的答案:那些宽度不是随手定的,是量出来的。
这里有个我踩过的坑:"干净"的标准不能一概而论。

同一条"残留 ≤ 20 即干净背景"的标准:
头图为什么天然脏?因为头像是占满整格高度的,两张脸之间那道"缝"里,上半段是头发、下半段还是头发,根本不是干净背景。
所以判据要分两套:
图型 | 谷底残留参考 | 含义 |
|---|---|---|
身体图(纱不飘) | ≤ 20 | 干净缝, |
身体图(纱/裙摆飘) | 400 ~ 700 | 缝是"脏"的,必须人工复核 |
头图(四格横排) | 300 ~ 560 属正常 | 谷底不干净是必然,别拿 ≤20 去套 |
关键认知:「谷底残留高」不等于「切错了」。 头图谷底残留 534 / 564 / 542,看着很脏——但那三个位置恰恰就是正确的裁切边。判据的意义是"要不要人工复核",不是"对不对"。
把这套算法封成三个模式,按需调用:
# 1) scan:量化边界,自动报三处缝的谷底 x 坐标(先跑这个)
set CS_CROP_SRC=<整版图绝对路径>
python crop_sheet.py scan
# → 直接可用的输出: CS_X=476,1030,1586
# 2) probe:在整版上盖坐标网格,出标尺图供人工目视(scan 有歧义时用)
python crop_sheet.py probe
# 3) cut:按 CS_X 给出的三条缝切开 4 段
python crop_sheet.py cut CS_X=476,1030,1586cut 只做一件事:纯裁切,不重绘、不缩放。这是"零漂移"的前提——一旦在裁切这步引入缩放或重绘,四格的脸就不再是同一张脸了。
切出来的分图,文件名直接带上裁切尺寸:
P1_正面_476x1024.png
P2_四分之三侧_554x1024.png
P3_九十度侧面_556x1024.png
P4_背面_462x1024.png文件名自带尺寸,本身就是"这是量出来的"的存档证据——哪天有人问"为什么不是 512",翻文件名就行。
自动跑的结果能不能直接信?我用一套手工量过的整版对过账:
缝 1 | 缝 2 | 缝 3 | |
|---|---|---|---|
| 890 | 1295 | 1642 |
手工目视网格 + 逐列量化 | 889 | 1296 | 1640 |
差 | 1 | 1 | 2 |
相差 1 ~ 2px。
所以结论是:scan 的结果可以直接采信,不必再手工量一遍。只有在两种情况下才需要动 probe 目视复核:
scan 报"找到的缝不是 3 处"(比如只找到 2 处)头图是四格横排,谷底天然不干净(上一节讲过,残留 300~560)。如果判定逻辑里带一条"深度必须低于峰值 35%"的门槛,头图会全军覆没——所有候选谷底都因为不够深而被滤掉,scan 直接报"只找到 0 处缝"。
我实测就撞上了:用带深度门槛的版本跑头图,八张里有三张返回空。
头图的正确做法是换一把刀——十字刀:
w/2 ± 130px 内找h/2 ± 130px 内找田字格布局下只需找两条线,比四格横排的"三处缝"简单得多,而且不受"谷底不够深"的干扰,因为我们不再要求谷底干净,只要求在给定窗口里取最小。
身体图曾经反复出现"脚被切掉"。我一度以为是画幅太宽导致的固有边界。实测下来,元凶是人物被画得太大。
几何账很简单:全身入镜时人物高约 950px,身高 : 身宽 ≈ 4 : 1,所以人物宽必须压在 380 ~ 400px 以内。
而人物为什么会被画大?因为提示词里写了"四格并排铺满整幅画面"。
"铺满"这个词是在鼓励模型填满画格,人物一放大,高度必然溢出。改掉它,换成:
每格四周留出大面积纯色背景、从头顶到脚底完整入镜、双脚与鞋清晰可见出完还得验:用垂直前景密度判触底——最底行仍是前景 = 脚被切,别只靠肉眼。
切开不等于切对。四张分图出来以后,有一道不能省的工序:
z = im.crop((x0, y0, x1, y1)).resize(((x1 - x0) * 3, y1 - y0), Image.LANCZOS)实测放大后多数是绒边 / 纱边 / 手臂外缘——正常现象,不是残留。只有真看清了再改坐标。
优先级永远是"人物完整" > "画面留白"。纱尖、袖角出画,视觉上是自然出画;带进隔壁半张脸,那是事故。
scan 报出三处谷底 x 坐标cut 纯裁切,不重绘不缩放 → 四张分图,文件名带实测宽度
到这一步,四张分图的宽度各不相同(476 / 554 / 556 / 462),但它们来自同一张图、同一次生成、零重绘——所以四格脸的是一致的。宽度不同恰恰不是缺陷,是"沿着真实空隙下刀"的结果。
而这条链上的每一步——调 ComfyUI 出整版、跑量缝脚本、批量裁切、检查脚有没有被切、归档三态目录——都是我在 WorkBuddy 里用自然语言驱动完成的。脚本是本机跑的,图片是本机生成的,整条链零云端调用、零积分。
症状 | 根因 | 解法 |
|---|---|---|
判"三通道 >245 = 背景",一列都没命中 | 生成底是浅灰蓝渐变 + 投影,不是纯白 | 改用逐行自适应基色,别再假设底色 |
全局取一个底色,半张图被当成前景 | 影棚底是纵向渐变的 | 背景基色必须逐行取 |
谷底找不准,缝跑到人的肩膀里 | 容差太小,把渐变噪声算成前景;或没掐头去尾 | 容差 22;y 取 6%~96% |
头图 | 头图谷底天然不干净,被"深度 < 峰值 35%"的门槛滤光 | 头图换十字刀,只找中线上两条 |
身体图谷底残留 400~700 | 裙摆 / 纱边飘起来,跟隔壁粘连 | 不采信自动结果,动 |
脚被切掉 | 提示词里"四格铺满画面"把人物撑大了 | 写"每格留大面积背景、完整入镜";画幅按每格 384 取 |
分图边缘有可疑淡竖条 | 可能是绒边/纱边,也可能真带进了隔壁 | 放大 3 倍看清再动刀;宁可切衣角不带人 |
重裁后新旧分图混在一起 | 没删旧文件 | 改坐标后先删旧分图再重裁 |
这篇的起点只是一个很具体的问题:为什么四格的宽度是 476 / 554 / 556 / 462,而不是 512?
答案朴素到有点无聊——因为这四个格子是模型自己排的,缝在哪只有量了才知道。但把这件事做对,我翻了两次车:一次想当然地等分,一次想当然地判白。
两条纪律值得带走:
剩下的就交给数字。截图和曲线的每一处标注,都能在脚本里复现。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。