Files
wordcloud/docs/ALGORITHM.md
T
2026-07-04 02:40:45 +08:00

136 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 生成算法说明
本文档描述当前代码实际算法。核心代码位于 `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"`
- 以轮为单位生成任务
- 同一轮使用统一字号或统一权重映射
- 某一轮放不下时,整轮降低字号重试
- 失败词统一进入下一档补位队列
- 大字号限制按姓名或轮次计数