Replace the old bbox/heuristic placement (scale search rounds, large-font capping, stratified sampling, fill-retry ladders) with an area-model font sizing pass feeding a C++ exact-glyph collision engine (centroid-biased spiral + random probing, HD clearance refinement, density/hole optimization). Simplify the frontend advanced-params panel and JobParams type to match the surviving config surface, add a layout-constraints test suite and a repeatable benchmark tool, and bring docs/*.md back in sync with current code (plus new TESTING.md and DEPLOYMENT.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.2 KiB
生成算法说明
本文档描述当前代码实际算法。核心代码位于 backend/core/pipeline.py、backend/core/layout.py、backend/core/weights.py、backend/EfficientWordCloud/efficient_wordcloud/src/ewc_core.cpp。
总流程
1. 读取 Excel 名单 → pipeline.main()
2. 智能画幅计算 → mask.calculate_dynamic_dimensions()
3. 生成并归一化掩膜 → mask.prepare_mask()
4. 计算权重 → weights.extract_weights_from_df() + weights.get_stroke_complexity_batch()
5. 估算字号范围 → weights.calculate_font_by_area_model()
6. 小画布布局 → layout.OptimizedEfficientWordCloud.generate_from_frequencies()
7. C++ 按真实字形找位置并原子写入 → IntegralGrid.place_glyph_exact()
8. 整批未完整放入 → 统一缩放字号或扩大画布后重排
9. 密度优化 → 探测更大字号并保留完整率不下降的候选
10. 高清精修 → render.refine_layout_with_hd_clearance() 加入隔离带局部微调
11. 输出 PNG / SVG / DB / metrics
名单与重复填充
N_REPETITIONS 决定目标词数:
total_target = len(names) * N_REPETITIONS
布局序列由 _build_layout_sequence() 生成。行为:按原名单循环追加,不是把同一个名字所有副本先放完。
[A, B, C], N_REPETITIONS=4
→ [A, B, C, A, B, C, A, B, C, A, B, C]
同名副本在同一批布局中使用相同目标字号。放置失败不会触发逐词缩字号。
权重逻辑
Excel 权重
WEIGHT_COL_NAME优先于WEIGHT_COL_INDEX- 有效权重必须是可转数字且大于
0 REMOVE_DUPLICATES=True时,同名权重取最大值REMOVE_DUPLICATES=False时,仍按名字聚合成权重映射
笔画权重
ENABLE_STROKE_WEIGHTS=True时,系统渲染每个字符到64×64灰度图,用像素占用量估算复杂度- 一个名字的笔画权重取其中最复杂字符的值
- 若同时存在 Excel 权重:Excel 值作为基础权重,笔画复杂度除以全体中位数后作为乘数
- 这样手动权重比例仍保留,且 Excel 权重全为
1时笔画开关也不会失效 ENABLE_STROKE_WEIGHTS=False且无 Excel 权重时,所有名字权重默认为10
字号范围估算
calculate_font_by_area_model() 使用以下输入估算 min_font / max_font:
- 可填充面积(掩膜中
0的像素数) - 目标填充率
TARGET_FILL_RATIO - 打包效率
PACKING_EFFICIENCY - 重复次数
N_REPETITIONS - 名字长度、各名字权重
公式思想:
- 可填区域越大,字号越大
- 名字越多、重复次数越高,字号越小
- 字符越多,总字符质量越高,字号越小
- 权重越高,在
log1p(weight)归一化后获得更高面积质量
最终 max_font = min_font × SIZE_RATIO。
字号打分
build_log_rank_scores(..., per_word=True) 按姓名权重计算固定分数,映射到展开后的重复序列。结果是:
- 同名副本目标分数相同
- 同名副本初始目标字号相同
- 权重相同时,所有姓名初始目标字号相同
SIZE_RATIO=1 时,min_font == max_font,所有同权重姓名在重试前后都保持完全相同字号。
等字号模式
SIZE_RATIO=1 时,面积估算阶段直接令 min_font = max_font。所有字号重试也只改变单一字号值。
等字号批次的特殊优化:
- 视作整批同字号布局,使用整批字号打分(
per_word=False) - 密度优化阶段在完整候选中比较字符级最大空洞(
largest_empty_square_size),并尝试不同布局种子降低空洞 - 高清精修阶段允许优先级回溯(把失败词提前到队列最前)
放置策略
每个词的放置流程:
- 根据目标分数得到目标字号(线性插值于
min_font到max_font) - 随机决定横排或竖排(
prefer_horizontal控制概率) - 用 PIL 渲染真实字形 bitmap,施加单侧安全边距(margin)生成碰撞 mask
- 调用 C++
place_glyph_exact()搜索合法位置并原子写入 - 当前方向找不到时,只尝试同字号的另一方向
- 任意姓名失败即视为本次整批布局不完整
- 不逐词缩字号。Python 统一缩放整批字号范围后重新布局
- 触及字号下限仍失败时,按
CANVAS_RETRY_GROWTH扩大画布并整批重排 - 将完整布局映射到高清字号和坐标
- 高清画布进行逐词隔离带精修,局部无合法位置时扩大画布并整批重排
C++ 真实字形搜索
IntegralGrid 维护两个核心结构:
canvas:真实占用像素(1表示已占用或掩膜阻挡)data:兼容旧矩形查询的积分图;当前主路径不依赖逐词重建
place_glyph_exact() 行为:
- 等字号批次前
70%姓名优先选择靠近可填区域质心的合法位置,避免少量姓名接受第一个随机空位而形成大块空洞 - 多字号批次只对前
25%以及高权重(score ≥ 0.80)姓名启用中心偏好 - 少于
100人的等字号批次中心候选数提高到768;100–300人提高到512;其余保持256 - 随机探测失败后,从种子决定的偏移开始完整扫描
- 对每个候选位置逐像素比较碰撞 mask 与 C++
canvas - 命中后只写入真实字形,占位查询和写入在同一次 C++ 调用中完成
真实字形搜索允许透明角落和笔画间空隙安全交错,比外接矩形碰撞更密。安全边距只参与候选检查,不会被双侧累计放大。
高清精修(隔离带)
正式流水线不在低清工作网格增加边距,因为 1 个工作像素会被放大成约 5–6 个高清像素并显著损失容量。
隔离带优先作用在最终高清画布,宽度为 1px,不是用户参数。精修流程:
- 小画布完整布局放大到高清
- 逐词在高清画布上验证,加入
1px隔离带 - 碰撞时只允许最大
max_shift=24px的局部微位移 - 精修失败时,最多进行
1次确定性整批优先级回溯(把失败词移到队列最前) - 若轮廓过窄无法容纳额外隔离带,降为精确零间隙碰撞(
clearance=0) - 全部候选无解时,由外层扩大画布后整批重新布局,不进行远距离单词搬移
密度搜索会保留各档完整整批候选。最高密度候选若无法在有限位置修正范围内通过高清隔离验收,则改用上一档完整整批候选;不会对碰撞姓名单独缩字号,也不会接受带重叠的高密度结果。
填充率重试
一次布局完成后,compute_fill_ratio_fast() 重新渲染 layout 并计算填充率。
- 面积模型首次完整放入但明显低于
TARGET_FILL_RATIO时,只允许一次整批等比例增字号尝试。新布局必须仍然完整且真实填充率更高才会采用 - 整批未完整放入时,流水线不会输出半成品:先整批等比例调整字号;触及最小字号仍失败时按
CANVAS_RETRY_GROWTH扩大画布并重新生成
字号硬约束
- 不存在逐词缩字号、gap filling 或大字号自动压缩路径
USER_MIN_FONT_SIZE和USER_MAX_FONT_SIZE是不可越过的边界;冲突时直接报错SIZE_RATIO=1在面积估算、整批重试和高清放大阶段始终保持单一字号- 完整名单不可关闭;放不下时只能整批重排、整批等比例调整或扩大画布
空洞优化(等字号模式)
等字号模式下,字符级最大空洞(largest_empty_square_size)用于判断是否存在"可放字符级空洞":
- 当前
largest_empty_square ≥ ceil(font_size * 1.25)时视为存在字符级空洞 - 流水线会在同一字号档位尝试最多
3种不同布局种子(按人数递减预算) - 目标是最小化最大空洞尺寸,不改变任何字号
自动画幅
calculate_dynamic_dimensions() 先对 BASE_HD_WIDTH × BASE_HD_HEIGHT 做一次 probe 掩膜,计算可填比例 free_ratio,再用以下公式扩展:
area_per_word = (MIN_READABLE_HEIGHT_PX²) × avg_len × 1.05
required_area = num_words × area_per_word × N_REPETITIONS / TARGET_FILL_RATIO
required_area /= free_ratio
scale_factor = √(required_area / current_area)
new_w = clamp(base_w × scale_factor, max_edge=6000)
- 扩展后宽高向上取整到最近的
100 - 最大边长限制为
6000px,避免 SVG/PNG 过度膨胀 - 掩膜只生成一次,扩展后复用