Rework layout engine around exact-glyph collision, add tests and docs sync
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>
This commit is contained in:
@@ -23,7 +23,8 @@ wc = EfficientWordCloud(
|
||||
height=600,
|
||||
font_path="/path/to/font.ttf",
|
||||
max_words=200,
|
||||
min_font_size=8,
|
||||
min_font_size=8,
|
||||
max_font_size=32,
|
||||
prefer_horizontal=0.9
|
||||
)
|
||||
|
||||
@@ -36,26 +37,18 @@ img.show()
|
||||
- `width` / `height`:画布尺寸。
|
||||
- `font_path`:字体路径。
|
||||
- `max_words`:最大词数。
|
||||
- `min_font_size`:最小字体。
|
||||
- `prefer_horizontal`:水平排版概率。
|
||||
- `use_spiral_search`:是否启用中心优先排序搜索。
|
||||
|
||||
## 4. 并行优化的使用说明
|
||||
### 4.1 Python bbox 预取
|
||||
- 自动启用,无需额外配置。
|
||||
- 内部使用 `ProcessPoolExecutor`,将未来词语的 bbox 计算并行化。
|
||||
- 运行 `generate` 时会输出预取/等待日志,便于观察并行效果。
|
||||
|
||||
### 4.2 C++ 并行搜索
|
||||
- 当 `use_spiral_search=True` 时启用。
|
||||
- 在 C++ 内部自动进行分块并行搜索,并保持中心优先排序的结果一致性。
|
||||
- `min_font_size` / `max_font_size`:本次整批布局可使用的硬字号边界。
|
||||
- `prefer_horizontal`:水平排版概率。
|
||||
- `relative_scaling`:权重对目标字号的影响比例。
|
||||
- `margin`:真实字形之间的最小工作网格间距。
|
||||
|
||||
## 4. 放置语义
|
||||
|
||||
- 每个词只使用权重映射得到的目标字号。
|
||||
- 放不下时只尝试同字号的另一方向,不会逐词缩字号。
|
||||
- 调用者需要检查 `layout_` 的数量;若不完整,应整批调整字号或扩大画布后创建新实例重排。
|
||||
- 项目正式流水线使用 C++ `place_glyph_exact()` 做真实字形碰撞;底层兼容类保留矩形搜索 API。
|
||||
|
||||
## 5. 常见问题
|
||||
### 5.1 为什么字体缩小时没有并行?
|
||||
缩小字体后 bbox 依赖当前失败状态,需要同步确认以确保正确性。
|
||||
|
||||
### 5.2 多进程是否会导致额外内存开销?
|
||||
是的,但任务仅用于 bbox 预取,且窗口大小有限,避免过度占用。
|
||||
|
||||
### 5.3 若没有字体文件怎么办?
|
||||
会回退到 PIL 默认字体,但测量与渲染效果可能不同。
|
||||
### 5.1 若没有字体文件怎么办?
|
||||
会回退到 PIL 默认字体,但测量与渲染效果可能不同。
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
#include <future>
|
||||
#include <atomic>
|
||||
#include <random>
|
||||
#include <functional>
|
||||
#include <numeric>
|
||||
#include <functional>
|
||||
#include <numeric>
|
||||
#include <limits>
|
||||
|
||||
// ==========================================
|
||||
// Thread Pool (avoid per-query thread creation)
|
||||
@@ -114,8 +115,13 @@ public:
|
||||
int w;
|
||||
};
|
||||
|
||||
// valid coordinates sorted by distance to center (for sorted/spiral search)
|
||||
std::vector<std::pair<int, int>> valid_coords;
|
||||
// Original mask-free coordinates. Sorting is lazy because the active
|
||||
// query_direct/query_near_center paths do not need the O(A log A) order.
|
||||
std::vector<std::pair<int, int>> valid_coords;
|
||||
bool valid_coords_center_sorted = false;
|
||||
double free_center_y = 0.0;
|
||||
double free_center_x = 0.0;
|
||||
int spiral_cursor = 1;
|
||||
|
||||
// Lazy update buffers
|
||||
std::vector<int32_t> diff;
|
||||
@@ -133,9 +139,9 @@ public:
|
||||
}
|
||||
|
||||
void init_from_buffer(unsigned char* raw_mask, int h, int w) {
|
||||
int center_y = h / 2;
|
||||
int center_x = w / 2;
|
||||
valid_coords.reserve(h * w / 2);
|
||||
valid_coords.reserve(h * w / 2);
|
||||
uint64_t free_y_sum = 0;
|
||||
uint64_t free_x_sum = 0;
|
||||
|
||||
// Initialize canvas from mask
|
||||
ensure_canvas();
|
||||
@@ -151,28 +157,48 @@ public:
|
||||
if (is_blocked) {
|
||||
canvas[i * width + j] = 1;
|
||||
}
|
||||
if (!is_blocked) {
|
||||
valid_coords.push_back({i, j});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(valid_coords.begin(), valid_coords.end(),
|
||||
[center_y, center_x](const std::pair<int, int>& a, const std::pair<int, int>& b) {
|
||||
long da = (long)(a.first - center_y)*(a.first - center_y) + (long)(a.second - center_x)*(a.second - center_x);
|
||||
long db = (long)(b.first - center_y)*(b.first - center_y) + (long)(b.second - center_x)*(b.second - center_x);
|
||||
return da < db;
|
||||
}
|
||||
);
|
||||
if (!is_blocked) {
|
||||
valid_coords.push_back({i, j});
|
||||
free_y_sum += (uint64_t)i;
|
||||
free_x_sum += (uint64_t)j;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!valid_coords.empty()) {
|
||||
free_center_y = (double)free_y_sum / (double)valid_coords.size();
|
||||
free_center_x = (double)free_x_sum / (double)valid_coords.size();
|
||||
} else {
|
||||
free_center_y = (double)h * 0.5;
|
||||
free_center_x = (double)w * 0.5;
|
||||
}
|
||||
valid_coords_center_sorted = false;
|
||||
spiral_cursor = 1;
|
||||
|
||||
std::fill(diff.begin(), diff.end(), 0);
|
||||
recent_rects.clear();
|
||||
dirty_count = 0;
|
||||
}
|
||||
|
||||
void reorder_stratified(int bands) {
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
const size_t len = valid_coords.size();
|
||||
dirty_count = 0;
|
||||
}
|
||||
|
||||
void ensure_valid_coords_center_sorted() {
|
||||
if (valid_coords_center_sorted) return;
|
||||
const double center_y = free_center_y;
|
||||
const double center_x = free_center_x;
|
||||
std::sort(valid_coords.begin(), valid_coords.end(),
|
||||
[center_y, center_x](const std::pair<int, int>& a, const std::pair<int, int>& b) {
|
||||
double ay = (double)a.first - center_y;
|
||||
double ax = (double)a.second - center_x;
|
||||
double by = (double)b.first - center_y;
|
||||
double bx = (double)b.second - center_x;
|
||||
return ay * ay + ax * ax < by * by + bx * bx;
|
||||
}
|
||||
);
|
||||
valid_coords_center_sorted = true;
|
||||
}
|
||||
// Legacy coordinate ordering remains available to compatibility callers.
|
||||
void reorder_stratified(int bands) {
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
ensure_valid_coords_center_sorted();
|
||||
const size_t len = valid_coords.size();
|
||||
if (bands <= 1 || len == 0) return;
|
||||
if ((size_t)bands > len) bands = (int)len;
|
||||
|
||||
@@ -279,8 +305,7 @@ public:
|
||||
std::fill(diff.begin(), diff.end(), 0);
|
||||
recent_rects.clear();
|
||||
dirty_count = 0;
|
||||
|
||||
for (int i = 0; i < height; ++i) {
|
||||
for (int i = 0; i < height; ++i) {
|
||||
uint32_t row_sum = 0;
|
||||
for (int j = 0; j < width; ++j) {
|
||||
uint32_t val = (raw_pixels[i * width + j] > 0) ? 1 : 0;
|
||||
@@ -318,10 +343,10 @@ public:
|
||||
}
|
||||
|
||||
// Rebuild integral from the internal canvas (partial from pos_r, pos_c)
|
||||
void rebuild_from_canvas(int pos_r, int pos_c) {
|
||||
if (canvas.empty()) return;
|
||||
rebuild_from_bitmap_partial(canvas.data(), pos_r, pos_c);
|
||||
}
|
||||
void rebuild_from_canvas(int pos_r, int pos_c) {
|
||||
if (canvas.empty()) return;
|
||||
rebuild_from_bitmap_partial(canvas.data(), pos_r, pos_c);
|
||||
}
|
||||
|
||||
// v4: Partial integral rebuild from position (pos_r, pos_c) downward
|
||||
// Optimized: use row-sum approach (like rebuild_from_bitmap) for the partial region
|
||||
@@ -425,8 +450,9 @@ public:
|
||||
return {-1, -1};
|
||||
}
|
||||
|
||||
DirectResult query_direct(int box_h, int box_w, uint32_t seed) {
|
||||
// Always flush before scanning
|
||||
DirectResult query_direct(int box_h, int box_w, uint32_t seed) {
|
||||
// Apply pending rectangle updates before reading the integral image.
|
||||
// Exact-glyph placement uses its own canvas-only path.
|
||||
flush();
|
||||
|
||||
int max_row = height - box_h;
|
||||
@@ -447,11 +473,12 @@ public:
|
||||
return {true, y, x};
|
||||
}
|
||||
|
||||
// Quick random probe: try a few random positions first
|
||||
// If the canvas is mostly empty, one of these will hit quickly
|
||||
// This avoids the full O(H*W) scan for early words
|
||||
{
|
||||
int n_probes = std::min(16, (int)total_positions);
|
||||
// Random probes avoid an O(H*W) scan during early and middle packing.
|
||||
// Increase the budget as occupancy rises.
|
||||
{
|
||||
const double occ_ratio = (double)total_occupied / (double)(width * height);
|
||||
const int probe_budget = (occ_ratio < 0.15) ? 32 : (occ_ratio < 0.40) ? 96 : 192;
|
||||
const int n_probes = (int)std::min<int64_t>(probe_budget, total_positions);
|
||||
for (int p = 0; p < n_probes; ++p) {
|
||||
int y = std::uniform_int_distribution<int>(0, max_row)(rng);
|
||||
int x = std::uniform_int_distribution<int>(0, max_col)(rng);
|
||||
@@ -469,27 +496,28 @@ public:
|
||||
}
|
||||
|
||||
if (nt <= 1) {
|
||||
// Single-thread: two-pass (count then pick) is faster than
|
||||
// reservoir sampling because it avoids per-position RNG calls
|
||||
uint64_t total_valid = 0;
|
||||
for (int i = 0; i <= max_row; ++i) {
|
||||
for (int j = 0; j <= max_col; ++j) {
|
||||
if (get_area_sum_fast(i, j, box_h, box_w) == 0)
|
||||
++total_valid;
|
||||
}
|
||||
}
|
||||
if (total_valid == 0) return {false, -1, -1};
|
||||
uint64_t target = std::uniform_int_distribution<uint64_t>(0, total_valid - 1)(rng);
|
||||
uint64_t count = 0;
|
||||
// Single-pass reservoir halves the work of count-then-pick. A
|
||||
// small LCG avoids invoking mt19937 for every valid position.
|
||||
uint64_t count = 0;
|
||||
int best_y = -1, best_x = -1;
|
||||
uint32_t state = seed ? seed : 1u;
|
||||
for (int i = 0; i <= max_row; ++i) {
|
||||
for (int j = 0; j <= max_col; ++j) {
|
||||
if (get_area_sum_fast(i, j, box_h, box_w) == 0) {
|
||||
if (count == target) return {true, i, j};
|
||||
++count;
|
||||
// Replace the current result with probability 1/count.
|
||||
state = state * 1664525u + 1013904223u;
|
||||
const bool replace = (state % count) == 0;
|
||||
if (replace) {
|
||||
best_y = i;
|
||||
best_x = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {false, -1, -1};
|
||||
return count == 0
|
||||
? DirectResult{false, -1, -1}
|
||||
: DirectResult{true, best_y, best_x};
|
||||
}
|
||||
|
||||
// Multi-thread path: parallel count then pick
|
||||
@@ -531,10 +559,193 @@ public:
|
||||
}
|
||||
cum += chunk_counts[t];
|
||||
}
|
||||
return {false, -1, -1};
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
return {false, -1, -1};
|
||||
}
|
||||
|
||||
// Sample legal positions and prefer the one whose box center is closest
|
||||
// to the free-mask centroid. This gives the visually important large
|
||||
// words a stable focal region without paying for a full spiral scan.
|
||||
DirectResult query_near_center(int box_h, int box_w, uint32_t seed, int probes) {
|
||||
flush();
|
||||
|
||||
int max_row = height - box_h;
|
||||
int max_col = width - box_w;
|
||||
if (max_row < 0 || max_col < 0) return {false, -1, -1};
|
||||
|
||||
std::mt19937 rng(seed);
|
||||
probes = std::max(16, std::min(probes, 1024));
|
||||
|
||||
int best_y = -1;
|
||||
int best_x = -1;
|
||||
double best_score = std::numeric_limits<double>::infinity();
|
||||
std::uniform_int_distribution<int> row_dist(0, max_row);
|
||||
std::uniform_int_distribution<int> col_dist(0, max_col);
|
||||
std::uniform_real_distribution<double> jitter(0.0, 1e-4);
|
||||
|
||||
// Test the centroid-aligned position first.
|
||||
int center_y = std::max(0, std::min(max_row, (int)std::lround(free_center_y - box_h * 0.5)));
|
||||
int center_x = std::max(0, std::min(max_col, (int)std::lround(free_center_x - box_w * 0.5)));
|
||||
if (get_area_sum_fast(center_y, center_x, box_h, box_w) == 0) {
|
||||
return {true, center_y, center_x};
|
||||
}
|
||||
|
||||
const double norm_y = std::max(1.0, (double)height);
|
||||
const double norm_x = std::max(1.0, (double)width);
|
||||
for (int p = 0; p < probes; ++p) {
|
||||
int y = row_dist(rng);
|
||||
int x = col_dist(rng);
|
||||
if (get_area_sum_fast(y, x, box_h, box_w) != 0) continue;
|
||||
double cy = (double)y + box_h * 0.5;
|
||||
double cx = (double)x + box_w * 0.5;
|
||||
double dy = (cy - free_center_y) / norm_y;
|
||||
double dx = (cx - free_center_x) / norm_x;
|
||||
double score = dy * dy + dx * dx + jitter(rng);
|
||||
if (score < best_score) {
|
||||
best_score = score;
|
||||
best_y = y;
|
||||
best_x = x;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_y >= 0) return {true, best_y, best_x};
|
||||
return query_direct(box_h, box_w, seed ^ 0x9E3779B9u);
|
||||
}
|
||||
|
||||
inline bool glyph_fits_exact(
|
||||
const unsigned char* glyph, int glyph_h, int glyph_w, int y, int x
|
||||
) const {
|
||||
for (int row = 0; row < glyph_h; ++row) {
|
||||
const unsigned char* glyph_row = glyph + row * glyph_w;
|
||||
const uint8_t* canvas_row = canvas.data() + (y + row) * width + x;
|
||||
for (int col = 0; col < glyph_w; ++col) {
|
||||
if (glyph_row[col] > 0 && canvas_row[col] != 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void reserve_glyph_exact(
|
||||
const unsigned char* glyph, int glyph_h, int glyph_w, int y, int x
|
||||
) {
|
||||
stamp_glyph(glyph, glyph_h, glyph_w, y, x);
|
||||
}
|
||||
|
||||
// Find and reserve a position using the actual (optionally dilated) glyph
|
||||
// bitmap. Unlike rectangle queries, transparent corners and gaps between
|
||||
// strokes may overlap safely. Search and reservation stay in one C++ call,
|
||||
// so no integral-image rebuild is needed between words.
|
||||
DirectResult place_glyph_exact(
|
||||
const unsigned char* collision_glyph,
|
||||
const unsigned char* stamp_glyph_data,
|
||||
int glyph_h,
|
||||
int glyph_w,
|
||||
uint32_t seed,
|
||||
int probes,
|
||||
int placement_mode
|
||||
) {
|
||||
ensure_canvas();
|
||||
const int max_row = height - glyph_h;
|
||||
const int max_col = width - glyph_w;
|
||||
if (max_row < 0 || max_col < 0) return {false, -1, -1};
|
||||
|
||||
std::mt19937 rng(seed);
|
||||
probes = std::max(32, std::min(probes, 2048));
|
||||
std::uniform_int_distribution<int> row_dist(0, max_row);
|
||||
std::uniform_int_distribution<int> col_dist(0, max_col);
|
||||
|
||||
int best_y = -1;
|
||||
int best_x = -1;
|
||||
double best_score = std::numeric_limits<double>::infinity();
|
||||
|
||||
if (placement_mode != 0) {
|
||||
const int center_y = std::max(
|
||||
0, std::min(max_row, (int)std::lround(free_center_y - glyph_h * 0.5))
|
||||
);
|
||||
const int center_x = std::max(
|
||||
0, std::min(max_col, (int)std::lround(free_center_x - glyph_w * 0.5))
|
||||
);
|
||||
if (glyph_fits_exact(collision_glyph, glyph_h, glyph_w, center_y, center_x)) {
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, center_y, center_x);
|
||||
return {true, center_y, center_x};
|
||||
}
|
||||
}
|
||||
|
||||
const double norm_y = std::max(1.0, (double)height);
|
||||
const double norm_x = std::max(1.0, (double)width);
|
||||
|
||||
// Mode 1 is a persistent centre-out Fermat spiral. Mode 2 skips the
|
||||
// spiral and selects a random legal candidate biased toward the centre.
|
||||
if (placement_mode == 1) {
|
||||
constexpr double golden_angle = 2.39996322972865332;
|
||||
constexpr double sample_spacing = 1.25;
|
||||
const int start_step = spiral_cursor;
|
||||
const int end_step = std::min(200000, start_step + 60000);
|
||||
int last_y = std::numeric_limits<int>::min();
|
||||
int last_x = std::numeric_limits<int>::min();
|
||||
for (int step = start_step; step <= end_step; ++step) {
|
||||
const double radius = sample_spacing * std::sqrt((double)step);
|
||||
const double theta = golden_angle * (double)step;
|
||||
const int y = (int)std::lround(
|
||||
free_center_y + radius * std::sin(theta) - glyph_h * 0.5
|
||||
);
|
||||
const int x = (int)std::lround(
|
||||
free_center_x + radius * std::cos(theta) - glyph_w * 0.5
|
||||
);
|
||||
if (y == last_y && x == last_x) continue;
|
||||
last_y = y;
|
||||
last_x = x;
|
||||
if (y < 0 || x < 0 || y > max_row || x > max_col) continue;
|
||||
if (!glyph_fits_exact(collision_glyph, glyph_h, glyph_w, y, x)) continue;
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, y, x);
|
||||
spiral_cursor = step;
|
||||
return {true, y, x};
|
||||
}
|
||||
}
|
||||
|
||||
for (int probe = 0; probe < probes; ++probe) {
|
||||
const int y = row_dist(rng);
|
||||
const int x = col_dist(rng);
|
||||
if (!glyph_fits_exact(collision_glyph, glyph_h, glyph_w, y, x)) continue;
|
||||
if (placement_mode == 0) {
|
||||
stamp_glyph(stamp_glyph_data, glyph_h, glyph_w, y, x);
|
||||
return {true, y, x};
|
||||
}
|
||||
const double cy = (double)y + glyph_h * 0.5;
|
||||
const double cx = (double)x + glyph_w * 0.5;
|
||||
const double dy = (cy - free_center_y) / norm_y;
|
||||
const double dx = (cx - free_center_x) / norm_x;
|
||||
const double score = dy * dy + dx * dx;
|
||||
if (score < best_score) {
|
||||
best_score = score;
|
||||
best_y = y;
|
||||
best_x = x;
|
||||
}
|
||||
}
|
||||
if (best_y >= 0) {
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, best_y, best_x);
|
||||
return {true, best_y, best_x};
|
||||
}
|
||||
|
||||
// Dense late-stage fallback. Start at a seeded offset to avoid a
|
||||
// top-left bias, but visit every possible origin so completeness is
|
||||
// deterministic whenever a legal position exists.
|
||||
const int n_rows = max_row + 1;
|
||||
const int n_cols = max_col + 1;
|
||||
const int row_start = row_dist(rng);
|
||||
const int col_start = col_dist(rng);
|
||||
for (int row_offset = 0; row_offset < n_rows; ++row_offset) {
|
||||
const int y = (row_start + row_offset) % n_rows;
|
||||
for (int col_offset = 0; col_offset < n_cols; ++col_offset) {
|
||||
const int x = (col_start + col_offset) % n_cols;
|
||||
if (!glyph_fits_exact(collision_glyph, glyph_h, glyph_w, y, x)) continue;
|
||||
reserve_glyph_exact(stamp_glyph_data, glyph_h, glyph_w, y, x);
|
||||
return {true, y, x};
|
||||
}
|
||||
}
|
||||
return {false, -1, -1};
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// v3: Batch query — process multiple (box_h, box_w) in one call
|
||||
// Returns vector of {found, y, x} for each query
|
||||
// =========================================================
|
||||
@@ -575,9 +786,13 @@ public:
|
||||
return {false, -1, -1, end_idx};
|
||||
}
|
||||
|
||||
std::pair<bool, std::pair<int, int>> find_spot_parallel(int box_h, int box_w, int step) {
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
std::pair<bool, std::pair<int, int>> find_spot_parallel(int box_h, int box_w, int step) {
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
ensure_valid_coords_center_sorted();
|
||||
}
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
if (dirty_count > 0 && dirty_count >= rebuild_interval) {
|
||||
lock.unlock();
|
||||
std::unique_lock<std::shared_mutex> write_lock(mutex_);
|
||||
@@ -679,19 +894,67 @@ static PyObject* Grid_query_reservoir(PyIntegralGrid* self, PyObject* args) {
|
||||
}
|
||||
|
||||
// v3: Direct pixel-grid scan with parallel counting
|
||||
static PyObject* Grid_query_direct(PyIntegralGrid* self, PyObject* args) {
|
||||
static PyObject* Grid_query_direct(PyIntegralGrid* self, PyObject* args) {
|
||||
int box_h, box_w;
|
||||
unsigned int seed = 0;
|
||||
if (!PyArg_ParseTuple(args, "ii|I", &box_h, &box_w, &seed)) return NULL;
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
// query_direct is GIL-free safe (no Python objects touched)
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
auto r = self->grid->query_direct(box_h, box_w, seed);
|
||||
auto r = self->grid->query_direct(box_h, box_w, seed);
|
||||
if (r.found) return Py_BuildValue("ii", r.y, r.x);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Grid_query_near_center(PyIntegralGrid* self, PyObject* args) {
|
||||
int box_h, box_w;
|
||||
unsigned int seed = 0;
|
||||
int probes = 160;
|
||||
if (!PyArg_ParseTuple(args, "ii|Ii", &box_h, &box_w, &seed, &probes)) return NULL;
|
||||
|
||||
auto r = self->grid->query_near_center(box_h, box_w, seed, probes);
|
||||
if (r.found) return Py_BuildValue("ii", r.y, r.x);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject* Grid_place_glyph_exact(PyIntegralGrid* self, PyObject* args) {
|
||||
PyObject* collision_obj;
|
||||
PyObject* stamp_obj;
|
||||
int glyph_h, glyph_w;
|
||||
unsigned int seed = 0;
|
||||
int probes = 256;
|
||||
int placement_mode = 0;
|
||||
if (!PyArg_ParseTuple(
|
||||
args, "OOiiIii", &collision_obj, &stamp_obj, &glyph_h, &glyph_w, &seed, &probes, &placement_mode
|
||||
)) return NULL;
|
||||
|
||||
Py_buffer collision_view;
|
||||
Py_buffer stamp_view;
|
||||
if (PyObject_GetBuffer(collision_obj, &collision_view, PyBUF_SIMPLE) < 0) return NULL;
|
||||
if (PyObject_GetBuffer(stamp_obj, &stamp_view, PyBUF_SIMPLE) < 0) {
|
||||
PyBuffer_Release(&collision_view);
|
||||
return NULL;
|
||||
}
|
||||
const Py_ssize_t required = (Py_ssize_t)glyph_h * (Py_ssize_t)glyph_w;
|
||||
if (glyph_h <= 0 || glyph_w <= 0 || collision_view.len < required || stamp_view.len < required) {
|
||||
PyBuffer_Release(&collision_view);
|
||||
PyBuffer_Release(&stamp_view);
|
||||
PyErr_SetString(PyExc_ValueError, "glyph buffer is smaller than glyph_h * glyph_w");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto result = self->grid->place_glyph_exact(
|
||||
(const unsigned char*)collision_view.buf,
|
||||
(const unsigned char*)stamp_view.buf,
|
||||
glyph_h,
|
||||
glyph_w,
|
||||
seed,
|
||||
probes,
|
||||
placement_mode
|
||||
);
|
||||
PyBuffer_Release(&collision_view);
|
||||
PyBuffer_Release(&stamp_view);
|
||||
if (result.found) return Py_BuildValue("ii", result.y, result.x);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// v3: Batch query — process multiple placements in one C++ call
|
||||
// Input: list of (box_h, box_w, seed) tuples
|
||||
@@ -768,16 +1031,16 @@ static PyObject* Grid_rebuild_from_bitmap_partial(PyIntegralGrid* self, PyObject
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// v4: Stamp glyph bitmap onto C++ canvas and rebuild integral
|
||||
static PyObject* Grid_stamp_and_rebuild(PyIntegralGrid* self, PyObject* args) {
|
||||
// Stamp glyph bitmap onto C++ canvas and rebuild the affected integral region.
|
||||
static PyObject* Grid_stamp_and_rebuild(PyIntegralGrid* self, PyObject* args) {
|
||||
PyObject* glyph_obj;
|
||||
int gh, gw, pos_r, pos_c;
|
||||
if (!PyArg_ParseTuple(args, "Oiiii", &glyph_obj, &gh, &gw, &pos_r, &pos_c)) return NULL;
|
||||
Py_buffer view;
|
||||
if (PyObject_GetBuffer(glyph_obj, &view, PyBUF_SIMPLE) < 0) return NULL;
|
||||
|
||||
self->grid->stamp_glyph((const unsigned char*)view.buf, gh, gw, pos_r, pos_c);
|
||||
self->grid->rebuild_from_canvas(pos_r, pos_c);
|
||||
self->grid->stamp_glyph((const unsigned char*)view.buf, gh, gw, pos_r, pos_c);
|
||||
self->grid->rebuild_from_canvas(pos_r, pos_c);
|
||||
|
||||
PyBuffer_Release(&view);
|
||||
Py_RETURN_NONE;
|
||||
@@ -786,8 +1049,10 @@ static PyObject* Grid_stamp_and_rebuild(PyIntegralGrid* self, PyObject* args) {
|
||||
static PyMethodDef Grid_methods[] = {
|
||||
{"reorder_stratified", (PyCFunction)Grid_reorder_stratified, METH_VARARGS, "Reorder valid coords with stratified interleaving."},
|
||||
{"query_sorted", (PyCFunction)Grid_query_sorted, METH_VARARGS, "Find position using sorted coordinate list (center-out, parallel)."},
|
||||
{"query_reservoir", (PyCFunction)Grid_query_reservoir, METH_VARARGS, "Find position using parallel direct-scan reservoir sampling."},
|
||||
{"query_direct", (PyCFunction)Grid_query_direct, METH_VARARGS, "Direct pixel-grid scan with parallel reservoir sampling."},
|
||||
{"query_reservoir", (PyCFunction)Grid_query_reservoir, METH_VARARGS, "Find position using parallel direct-scan reservoir sampling."},
|
||||
{"query_direct", (PyCFunction)Grid_query_direct, METH_VARARGS, "Direct pixel-grid scan with parallel reservoir sampling."},
|
||||
{"query_near_center", (PyCFunction)Grid_query_near_center, METH_VARARGS, "Sample legal positions and prefer the free-mask centroid."},
|
||||
{"place_glyph_exact", (PyCFunction)Grid_place_glyph_exact, METH_VARARGS, "Place and reserve an exact glyph bitmap without changing its size."},
|
||||
{"batch_query", (PyCFunction)Grid_batch_query, METH_VARARGS, "Batch placement: list of (bh,bw,seed) -> list of (y,x)|None."},
|
||||
{"update", (PyCFunction)Grid_update, METH_VARARGS, "Update grid with placed rectangle."},
|
||||
{"flush", (PyCFunction)Grid_flush, METH_NOARGS, "Force rebuild integral image."},
|
||||
|
||||
@@ -101,8 +101,6 @@ class EfficientWordCloud:
|
||||
Probability a word is placed horizontally (0–1).
|
||||
mode : str
|
||||
PIL image mode ('RGB', 'RGBA', …).
|
||||
use_spiral_search : bool
|
||||
Use center-out sorted search (True) or reservoir sampling (False).
|
||||
scale : float
|
||||
Scaling factor between layout computation and final rendering.
|
||||
``scale=2`` means the output image is 2× the canvas size in each
|
||||
@@ -142,8 +140,6 @@ class EfficientWordCloud:
|
||||
How much word frequency (vs rank) influences font size.
|
||||
0 = rank only, 1 = fully frequency-driven.
|
||||
When *repeat* is True, defaults to 0.
|
||||
font_step : int
|
||||
Step size when reducing font size to find a fit.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
@@ -156,16 +152,14 @@ class EfficientWordCloud:
|
||||
background_color="black",
|
||||
prefer_horizontal=0.9,
|
||||
mode="RGB",
|
||||
use_spiral_search=True,
|
||||
scale=1,
|
||||
scale=1,
|
||||
contour_width=0,
|
||||
contour_color="black",
|
||||
margin=2,
|
||||
color_func=None,
|
||||
colormap=None,
|
||||
random_state=None,
|
||||
relative_scaling="auto",
|
||||
font_step=1,
|
||||
relative_scaling="auto",
|
||||
repeat=False,
|
||||
stopwords=None,
|
||||
regexp=None,
|
||||
@@ -185,7 +179,6 @@ class EfficientWordCloud:
|
||||
self.background_color = background_color
|
||||
self.prefer_horizontal = prefer_horizontal
|
||||
self.mode = mode
|
||||
self.use_spiral_search = use_spiral_search
|
||||
self.scale = scale
|
||||
self.contour_width = contour_width
|
||||
self.contour_color = contour_color
|
||||
@@ -197,7 +190,6 @@ class EfficientWordCloud:
|
||||
else:
|
||||
self.relative_scaling = relative_scaling
|
||||
self.margin = margin
|
||||
self.font_step = font_step
|
||||
self.stopwords = stopwords if stopwords is not None else STOPWORDS
|
||||
self.regexp = regexp
|
||||
self.collocations = collocations
|
||||
@@ -319,10 +311,9 @@ class EfficientWordCloud:
|
||||
def _query(qh, qw):
|
||||
return self.grid.query_direct(qh, qw, rs.randint(0, 2**31))
|
||||
|
||||
# v4: Ref-like linear step-down placement with bitmap occupancy
|
||||
# After each word placement, stamp glyph bitmap into C++ canvas
|
||||
# and rebuild integral for pixel-accurate collision detection.
|
||||
# No PIL image drawn during placement — to_image() renders later.
|
||||
# Each word is tried at exactly its weight-derived target size. A
|
||||
# failed word may change orientation, but never receives a private
|
||||
# fallback size. Whole-cloud scaling belongs to the caller.
|
||||
|
||||
# Dummy draw for textbbox measurement
|
||||
_measure_img = Image.new("L", (1, 1))
|
||||
@@ -344,36 +335,26 @@ class EfficientWordCloud:
|
||||
else:
|
||||
orientation = Image.ROTATE_90
|
||||
|
||||
tried_other_orientation = False
|
||||
|
||||
while True:
|
||||
if font_size < self.min_font_size:
|
||||
break
|
||||
|
||||
font = _get_font(font_size)
|
||||
transposed = ImageFont.TransposedFont(font, orientation=orientation)
|
||||
bbox = _measure_draw.textbbox((0, 0), word, font=transposed)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
pos = None
|
||||
orientations = [orientation]
|
||||
if self.prefer_horizontal < 1:
|
||||
orientations.append(Image.ROTATE_90 if orientation is None else None)
|
||||
for candidate_orientation in orientations:
|
||||
font = _get_font(font_size)
|
||||
transposed = ImageFont.TransposedFont(font, orientation=candidate_orientation)
|
||||
bbox = _measure_draw.textbbox((0, 0), word, font=transposed)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
qh = th + self.margin
|
||||
qw = tw + self.margin
|
||||
|
||||
pos = _query(qh, qw)
|
||||
if pos is not None:
|
||||
break
|
||||
|
||||
# No position found — try alternate orientation, then reduce size
|
||||
if not tried_other_orientation and self.prefer_horizontal < 1:
|
||||
orientation = Image.ROTATE_90 if orientation is None else None
|
||||
tried_other_orientation = True
|
||||
else:
|
||||
font_size -= self.font_step
|
||||
orientation = None
|
||||
tried_other_orientation = False
|
||||
|
||||
if font_size < self.min_font_size:
|
||||
# Canvas full — no more words can fit
|
||||
break
|
||||
pos = _query(qh, qw)
|
||||
if pos is not None:
|
||||
orientation = candidate_orientation
|
||||
break
|
||||
|
||||
if pos is None:
|
||||
continue
|
||||
|
||||
y, x = pos
|
||||
# Adjust position for margin (like ref: x,y += margin // 2)
|
||||
|
||||
Reference in New Issue
Block a user