# 生成算法说明 本文档描述当前代码实际算法。核心代码位于 `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.query_direct()` 8. C++ 写入字形占用:`IntegralGrid.stamp_and_rebuild()` 9. 计算填充率并必要时重试放大 10. 将小画布 layout 放大到高清画布并输出 PNG/SVG/DB/metrics ## 名单和重复填充 `N_REPETITIONS` 决定目标词数: ```text total_target = len(names) * N_REPETITIONS ``` 布局序列由 `_build_layout_sequence()` 生成。当前行为是按原名单循环追加: ```text [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` 时,系统渲染每个字符到 64x64 灰度图,用像素占用量估算复杂度。一个名字的笔画权重取其中最复杂字符的值。 `ENABLE_STROKE_WEIGHTS = False` 时跳过笔画权重。若没有 Excel 权重,所有名字权重默认为 `10`。 ## 字号范围估算 `calculate_font_by_area_model()` 使用可填充面积、目标填充率、packing efficiency、重复次数和名字长度估算 `min_font` / `max_font`。 公式思想: - 可填区域越大,字号越大 - 名字越多、重复次数越高,字号越小 - 字符越多,总占用质量越高,字号越小 - 权重越高,在 `log1p(weight)` 归一化后获得更高面积质量 最终 `max_font` 基于 `min_font * SIZE_RATIO` 计算。 ## 字号打分 当前 `build_log_rank_scores(..., per_word=True)` 会按姓名权重计算固定分数,然后映射到展开后的重复序列。 这意味着: - 同名副本的目标分数相同 - 同名副本的初始目标字号相同 - 权重相同时,所有姓名初始目标字号相同 但最终放置字号仍可能变小,因为放置失败时会逐词降字号。 ## 放置策略 每个词的放置流程: 1. 根据目标分数得到目标字号 2. 随机决定横排或竖排 3. 用 PIL 测量文字包围盒 4. 调用 C++ `query_direct(query_h, query_w, seed)` 找位置 5. 如果找不到,字号减 2 后重试,最低到目标字号的 40% 或 `min_font_size` 6. 放置成功后,取真实字形 bitmap 并调用 `stamp_and_rebuild()` 7. 主循环放不下的词进入 gap filling,用更小字号再尝试一次 ## C++ 积分图搜索 C++ `IntegralGrid` 维护两个核心结构: - `canvas`:真实占用像素,`1` 表示已占用或掩膜阻挡 - `data`:`canvas` 的积分图,用于 O(1) 判断矩形区域是否为空 `query_direct()` 的行为: 1. 如果画布完全空,随机返回一个位置 2. 先随机探测最多 16 个位置 3. 如果未命中,扫描所有可能位置 4. 对每个候选位置用积分图判断包围盒是否为空 5. 从所有可放位置中随机选一个 `stamp_and_rebuild()` 的行为: 1. 把真实字形像素写入 C++ `canvas` 2. 从字形左上角开始局部重建积分图 当前碰撞检测是“矩形找位置 + 字形像素落图”。找位置阶段要求文字包围盒矩形完全空;实际占用阶段只写入字形像素。 ## 填充率重试 一次布局完成后,`compute_fill_ratio_fast()` 重新渲染 layout 并计算填充率。 如果填充率低于 `MIN_ACCEPT_FILL_RATIO`,管线会尝试二分放大 `size_scale`,并可通过 `FILL_RETRY_RELAX_LARGE_CAP` 放宽大字号限制。 如果仍无法达到目标,会保留填充率最好的 layout。 ## 已知算法限制 - 当前没有真正的“整轮统一降字号”机制。重复填充虽然按轮展开,但每个词可以独立降字号。 - `LIMIT_LARGE_FONTS` 是全局计数,不区分姓名和轮次。 - `LAYOUT_ORDER_MODE_INTERLEAVED_RANDOM` 只改变展开序列的顺序,不改变 C++ 的空间采样策略。 - `ENABLE_STRATIFIED_SAMPLING` 调用的 `reorder_stratified()` 对 `query_direct()` 主路径无效。 - C++ `batch_query()` 会用矩形 `update_rect_add()` 更新,不走真实字形 `stamp_and_rebuild()`;当前 Python 主路径没有使用它。 ## 后续公平重复填充建议 如果目标是“每一轮名单整体公平变小”,建议新增独立模式,而不是继续微调当前逐词降字号: - `REPEAT_FILL_MODE = "ROUND_ROBIN_FAIR"` - 以轮为单位生成任务 - 同一轮使用统一字号或统一权重映射 - 某一轮放不下时,整轮降低字号重试 - 失败词统一进入下一档补位队列 - 大字号限制按姓名或轮次计数