Initial project baseline

This commit is contained in:
2026-07-04 02:40:45 +08:00
commit d5d8caef2f
86 changed files with 15590 additions and 0 deletions
@@ -0,0 +1,829 @@
/*
* EfficientWordCloud Core (ewc_core) v3.0
*
* v3 changes:
* - Direct pixel-grid scan (query_direct) — like ref, O(1) integral image
* check per position, sequential memory access, cache-friendly
* - Parallel reservoir sampling with thread pool (no std::async overhead)
* - Batch query API to eliminate Python↔C++ per-word overhead
* - Partial integral rebuild (only affected subregion)
*/
#include <Python.h>
#include <vector>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstdint>
#include <utility>
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <future>
#include <atomic>
#include <random>
#include <functional>
#include <numeric>
// ==========================================
// Thread Pool (avoid per-query thread creation)
// ==========================================
class ThreadPool {
std::vector<std::thread> workers;
std::vector<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
std::atomic<bool> stop{false};
public:
ThreadPool(unsigned int n) {
for (unsigned int i = 0; i < n; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return stop.load() || !tasks.empty(); });
if (stop.load() && tasks.empty()) return;
task = std::move(tasks.back());
tasks.pop_back();
}
task();
}
});
}
}
template<class F>
std::future<typename std::invoke_result<F>::type> submit(F&& f) {
using R = typename std::invoke_result<F>::type;
auto p = std::make_shared<std::promise<R>>();
auto fut = p->get_future();
{
std::lock_guard<std::mutex> lock(mtx);
if constexpr (std::is_void_v<R>) {
tasks.push_back([p, f=std::forward<F>(f)]() mutable {
try { f(); p->set_value(); }
catch (...) { p->set_exception(std::current_exception()); }
});
} else {
tasks.push_back([p, f=std::forward<F>(f)]() mutable {
try { p->set_value(f()); }
catch (...) { p->set_exception(std::current_exception()); }
});
}
}
cv.notify_one();
return fut;
}
~ThreadPool() {
stop.store(true);
cv.notify_all();
for (auto& w : workers) w.join();
}
};
static ThreadPool& get_pool() {
static unsigned int n = std::max(2u, std::thread::hardware_concurrency());
static ThreadPool pool(n);
return pool;
}
static unsigned int get_num_threads() {
static unsigned int n = std::max(2u, std::thread::hardware_concurrency());
return n;
}
// ==========================================
// Core Data Structure
// ==========================================
class IntegralGrid {
public:
uint32_t* data;
int width;
int height;
mutable std::shared_mutex mutex_;
struct Rect {
int r;
int c;
int h;
int w;
};
// valid coordinates sorted by distance to center (for sorted/spiral search)
std::vector<std::pair<int, int>> valid_coords;
// Lazy update buffers
std::vector<int32_t> diff;
std::vector<Rect> recent_rects;
int dirty_count = 0;
int rebuild_interval = 16; // v3: more frequent rebuilds (was 64), since rebuild is cheaper now
IntegralGrid(int h, int w) : width(w), height(h) {
data = new uint32_t[width * height]();
diff.resize(width * height, 0);
}
~IntegralGrid() {
if (data) delete[] data;
}
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);
// Initialize canvas from mask
ensure_canvas();
for (int i = 0; i < h; ++i) {
uint32_t row_sum = 0;
for (int j = 0; j < w; ++j) {
bool is_blocked = (raw_mask[i * w + j] > 0);
uint32_t val = is_blocked ? 1 : 0;
row_sum += val;
uint32_t prev_row = (i > 0) ? data[(i - 1) * width + j] : 0;
data[i * width + j] = row_sum + prev_row;
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;
}
);
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();
if (bands <= 1 || len == 0) return;
if ((size_t)bands > len) bands = (int)len;
std::vector<size_t> band_sizes(bands, 0);
size_t base = len / bands;
size_t rem = len % bands;
for (int b = 0; b < bands; ++b)
band_sizes[b] = base + (b < (int)rem ? 1 : 0);
std::vector<size_t> band_starts(bands, 0);
size_t offset = 0, max_band_size = 0;
for (int b = 0; b < bands; ++b) {
band_starts[b] = offset;
offset += band_sizes[b];
if (band_sizes[b] > max_band_size) max_band_size = band_sizes[b];
}
std::vector<std::pair<int, int>> reordered;
reordered.reserve(len);
for (size_t i = 0; i < max_band_size; ++i)
for (int b = 0; b < bands; ++b)
if (i < band_sizes[b])
reordered.push_back(valid_coords[band_starts[b] + i]);
valid_coords.swap(reordered);
}
// O(1) rectangle emptiness check via integral image + recent-rect overlap
inline uint32_t get_area_sum(int r, int c, int h, int w) const {
int r_bottom = r + h - 1;
int c_right = c + w - 1;
int r_top = r - 1;
int c_left = c - 1;
uint32_t A = (r_top >= 0 && c_left >= 0) ? data[r_top * width + c_left] : 0;
uint32_t B = (r_top >= 0) ? data[r_top * width + c_right] : 0;
uint32_t C = (c_left >= 0) ? data[r_bottom * width + c_left] : 0;
uint32_t D = data[r_bottom * width + c_right];
uint32_t base_sum = D - B - C + A;
if (base_sum > 0) return base_sum;
if (!recent_rects.empty()) {
int r2 = r + h;
int c2 = c + w;
for (const auto& rect : recent_rects) {
int rr2 = rect.r + rect.h;
int cc2 = rect.c + rect.w;
if (r < rr2 && r2 > rect.r && c < cc2 && c2 > rect.c) return 1;
}
}
return 0;
}
// O(1) fast check without recent_rects (for direct scan where we'll call flush first)
inline uint32_t get_area_sum_fast(int r, int c, int h, int w) const {
int r_bottom = r + h - 1;
int c_right = c + w - 1;
int r_top = r - 1;
int c_left = c - 1;
uint32_t A = (r_top >= 0 && c_left >= 0) ? data[r_top * width + c_left] : 0;
uint32_t B = (r_top >= 0) ? data[r_top * width + c_right] : 0;
uint32_t C = (c_left >= 0) ? data[r_bottom * width + c_left] : 0;
uint32_t D = data[r_bottom * width + c_right];
return D - B - C + A;
}
void rebuild_integral() {
if (dirty_count <= 0) return;
std::vector<int32_t> delta_prev_row(width, 0);
std::vector<int32_t> delta_row(width, 0);
std::vector<uint32_t> integral_prev_row(width, 0);
for (int i = 0; i < height; ++i) {
uint32_t row_sum = 0;
for (int j = 0; j < width; ++j) {
int idx = i * width + j;
int32_t up = (i > 0) ? delta_prev_row[j] : 0;
int32_t left = (j > 0) ? delta_row[j - 1] : 0;
int32_t up_left = (i > 0 && j > 0) ? delta_prev_row[j - 1] : 0;
int32_t delta = diff[idx] + up + left - up_left;
delta_row[j] = delta;
row_sum += (uint32_t)delta;
uint32_t prev = (i > 0) ? integral_prev_row[j] : 0;
uint32_t di = row_sum + prev;
data[idx] += di;
integral_prev_row[j] = di;
}
delta_prev_row.swap(delta_row);
}
std::fill(diff.begin(), diff.end(), 0);
recent_rects.clear();
dirty_count = 0;
}
// Force flush: rebuild integral so get_area_sum_fast is safe
void flush() {
if (dirty_count > 0) rebuild_integral();
}
// v4: Rebuild integral image from raw pixel array (bitmap occupancy like ref)
// raw_pixels: H×W uint8 array where >0 = occupied
void rebuild_from_bitmap(const unsigned char* raw_pixels) {
// Reset lazy update state since we're rebuilding everything
std::fill(diff.begin(), diff.end(), 0);
recent_rects.clear();
dirty_count = 0;
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;
row_sum += val;
uint32_t prev_row = (i > 0) ? data[(i - 1) * width + j] : 0;
data[i * width + j] = row_sum + prev_row;
}
}
}
// Persistent canvas bitmap (C++ side) — avoids PIL→numpy conversion overhead
std::vector<uint8_t> canvas;
void ensure_canvas() {
if (canvas.empty()) {
canvas.resize(width * height, 0);
}
}
// Stamp a small glyph bitmap onto the canvas at position (pos_r, pos_c)
// glyph: gh×gw uint8 array where >0 = occupied pixel
void stamp_glyph(const unsigned char* glyph, int gh, int gw, int pos_r, int pos_c) {
ensure_canvas();
for (int i = 0; i < gh; ++i) {
int ri = pos_r + i;
if (ri < 0 || ri >= height) continue;
for (int j = 0; j < gw; ++j) {
int cj = pos_c + j;
if (cj < 0 || cj >= width) continue;
if (glyph[i * gw + j] > 0) {
canvas[ri * width + cj] = 1;
}
}
}
}
// 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);
}
// v4: Partial integral rebuild from position (pos_r, pos_c) downward
// Optimized: use row-sum approach (like rebuild_from_bitmap) for the partial region
void rebuild_from_bitmap_partial(const unsigned char* raw_pixels, int pos_r, int pos_c) {
// Reset lazy update state
dirty_count = 0;
recent_rects.clear();
// Clamp to valid range
if (pos_r < 0) pos_r = 0;
if (pos_c < 0) pos_c = 0;
// For each row from pos_r, recompute integral from pos_c onward
// Using the fast row-sum approach
for (int i = pos_r; i < height; ++i) {
const unsigned char* pixel_row = raw_pixels + i * width;
uint32_t* data_row = data + i * width;
// Compute row_sum for columns [0, pos_c) from existing data
// row_sum at pos_c = data[i][pos_c-1] - data[i-1][pos_c-1] (if prev row available)
// But we need the actual row prefix sum, not the integral
// Instead: use the standard formula: integral(i,j) = pixel(i,j) + left + up - diag
// But optimize by accumulating row_sum separately
// row_sum = sum of pixel_row[pos_c..j] for current row
uint32_t row_sum;
if (pos_c > 0) {
// Get the row prefix sum at pos_c-1 from existing integral
uint32_t prev_row_integral = (i > 0) ? data[(i-1) * width + (pos_c - 1)] : 0;
row_sum = data_row[pos_c - 1] - prev_row_integral;
// But wait - data_row[pos_c-1] might be stale for rows > pos_r
// Actually for i == pos_r, row pos_r-1 hasn't been modified, so data[pos_r-1] is correct
// For i > pos_r, data[i-1] has been updated in previous iteration, so it's correct
// And data_row[pos_c-1] is unchanged (we don't modify columns < pos_c)
// So row_sum = integral[i][pos_c-1] - integral[i-1][pos_c-1]
// This gives us the row prefix sum for columns [0, pos_c-1]
} else {
row_sum = 0;
}
for (int j = pos_c; j < width; ++j) {
row_sum += (pixel_row[j] > 0) ? 1 : 0;
uint32_t prev_row = (i > 0) ? data[(i - 1) * width + j] : 0;
data_row[j] = row_sum + prev_row;
}
}
}
void update_rect_add(int r, int c, int h, int w) {
int end_r = std::min(height, r + h);
int end_c = std::min(width, c + w);
if (r < 0 || c < 0 || end_r <= r || end_c <= c) return;
recent_rects.push_back({r, c, end_r - r, end_c - c});
diff[r * width + c] += 1;
if (end_r < height) diff[end_r * width + c] -= 1;
if (end_c < width) diff[r * width + end_c] -= 1;
if (end_r < height && end_c < width) diff[end_r * width + end_c] += 1;
dirty_count++;
if (dirty_count >= rebuild_interval) rebuild_integral();
}
// =========================================================
// v3: Direct pixel-grid scan (like ref's query_integral_image)
// Scans all (i,j) where 0<=i<=H-bh, 0<=j<=W-bw in memory order
// Two-pass: count hits, then pick random one
// =========================================================
struct DirectResult {
bool found;
int y, x;
};
// Count valid positions in row range [row_start, row_end)
uint64_t count_valid_rows(int row_start, int row_end, int box_h, int box_w) const {
uint64_t count = 0;
int max_col = width - box_w;
for (int i = row_start; i < row_end; ++i) {
for (int j = 0; j <= max_col; ++j) {
if (get_area_sum_fast(i, j, box_h, box_w) == 0)
++count;
}
}
return count;
}
// Pick the nth valid position in row range [row_start, row_end)
std::pair<int,int> pick_nth_valid(int row_start, int row_end, int box_h, int box_w, uint64_t target) const {
uint64_t count = 0;
int max_col = width - box_w;
for (int i = row_start; i < row_end; ++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 {i, j};
++count;
}
}
}
return {-1, -1};
}
DirectResult query_direct(int box_h, int box_w, uint32_t seed) {
// Always flush before scanning
flush();
int max_row = height - box_h;
int max_col = width - box_w;
if (max_row < 0 || max_col < 0) return {false, -1, -1};
int n_rows = max_row + 1;
int n_cols = max_col + 1;
int64_t total_positions = (int64_t)n_rows * n_cols;
std::mt19937 rng(seed);
// Fast path: if the entire canvas is empty, all positions are valid
uint32_t total_occupied = data[(height - 1) * width + (width - 1)];
if (total_occupied == 0) {
int y = std::uniform_int_distribution<int>(0, max_row)(rng);
int x = std::uniform_int_distribution<int>(0, max_col)(rng);
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);
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);
if (get_area_sum_fast(y, x, box_h, box_w) == 0) {
return {true, y, x};
}
}
}
// Adaptive: use single thread for small canvases, multi for large
unsigned int nt = 1;
if (total_positions > 200000) {
nt = get_num_threads();
nt = std::min(nt, (unsigned int)n_rows);
}
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;
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;
}
}
}
return {false, -1, -1};
}
// Multi-thread path: parallel count then pick
std::vector<uint64_t> chunk_counts(nt, 0);
std::vector<int> chunk_starts(nt), chunk_ends(nt);
int rows_per = (n_rows + nt - 1) / nt;
for (unsigned int t = 0; t < nt; ++t) {
chunk_starts[t] = t * rows_per;
chunk_ends[t] = std::min(n_rows, (int)(t + 1) * rows_per);
}
auto& pool = get_pool();
std::vector<std::future<uint64_t>> futs;
futs.reserve(nt);
for (unsigned int t = 0; t < nt; ++t) {
if (chunk_starts[t] >= chunk_ends[t]) break;
futs.push_back(pool.submit([this, cs=chunk_starts[t], ce=chunk_ends[t], box_h, box_w]() {
return this->count_valid_rows(cs, ce, box_h, box_w);
}));
}
uint64_t total = 0;
for (size_t t = 0; t < futs.size(); ++t) {
chunk_counts[t] = futs[t].get();
total += chunk_counts[t];
}
if (total == 0) return {false, -1, -1};
uint64_t target = std::uniform_int_distribution<uint64_t>(0, total - 1)(rng);
uint64_t cum = 0;
for (size_t t = 0; t < futs.size(); ++t) {
if (cum + chunk_counts[t] > target) {
auto pos = pick_nth_valid(chunk_starts[t], chunk_ends[t], box_h, box_w, target - cum);
return {true, pos.first, pos.second};
}
cum += chunk_counts[t];
}
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
// =========================================================
std::vector<DirectResult> batch_query(const std::vector<std::tuple<int,int,uint32_t>>& queries) {
std::vector<DirectResult> results;
results.reserve(queries.size());
for (auto& [bh, bw, seed] : queries) {
auto r = query_direct(bh, bw, seed);
if (r.found) {
update_rect_add(r.y, r.x, bh, bw);
}
results.push_back(r);
}
return results;
}
// =========================================================
// Legacy methods (kept for backward compatibility)
// =========================================================
struct SearchResult {
bool found;
int y, x;
size_t index;
};
SearchResult search_range(size_t start_idx, size_t end_idx, int box_h, int box_w, int step) const {
for (size_t i = start_idx; i < end_idx; i += step) {
int y = valid_coords[i].first;
int x = valid_coords[i].second;
if (y + box_h <= height && x + box_w <= width) {
if (get_area_sum(y, x, box_h, box_w) == 0) {
return {true, y, x, i};
}
}
}
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_);
if (dirty_count > 0 && dirty_count >= rebuild_interval) {
lock.unlock();
std::unique_lock<std::shared_mutex> write_lock(mutex_);
if (dirty_count > 0 && dirty_count >= rebuild_interval)
rebuild_integral();
}
}
const size_t len = valid_coords.size();
if (len == 0) return {false, {-1, -1}};
SearchResult best = {false, -1, -1, len};
unsigned int nt = get_num_threads();
nt = std::min(nt, (unsigned int)len);
const size_t chunk = (len + nt - 1) / nt;
auto& pool = get_pool();
std::vector<std::future<SearchResult>> futs;
futs.reserve(nt);
Py_BEGIN_ALLOW_THREADS
for (unsigned int i = 0; i < nt; ++i) {
size_t s = i * chunk, e = std::min(len, s + chunk);
if (s >= e) break;
futs.push_back(pool.submit([this, s, e, box_h, box_w, step]() {
return this->search_range(s, e, box_h, box_w, step);
}));
}
for (auto& f : futs) {
auto res = f.get();
if (res.found && res.index < best.index) best = res;
}
Py_END_ALLOW_THREADS
if (best.found) return {true, {best.y, best.x}};
return {false, {-1, -1}};
}
};
// ==========================================
// Python Bindings
// ==========================================
typedef struct {
PyObject_HEAD
IntegralGrid* grid;
} PyIntegralGrid;
static void Grid_dealloc(PyIntegralGrid* self) {
if (self->grid) delete self->grid;
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject* Grid_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyIntegralGrid* self = (PyIntegralGrid*)type->tp_alloc(type, 0);
if (self) self->grid = NULL;
return (PyObject*)self;
}
static int Grid_init(PyIntegralGrid* self, PyObject* args, PyObject* kwds) {
PyObject* mask_obj;
int h, w;
if (!PyArg_ParseTuple(args, "Oii", &mask_obj, &h, &w)) return -1;
Py_buffer view;
if (PyObject_GetBuffer(mask_obj, &view, PyBUF_SIMPLE) < 0) return -1;
self->grid = new IntegralGrid(h, w);
self->grid->init_from_buffer((unsigned char*)view.buf, h, w);
PyBuffer_Release(&view);
return 0;
}
static PyObject* Grid_reorder_stratified(PyIntegralGrid* self, PyObject* args) {
int bands = 0;
if (!PyArg_ParseTuple(args, "i", &bands)) return NULL;
self->grid->reorder_stratified(bands);
Py_RETURN_NONE;
}
static PyObject* Grid_query_sorted(PyIntegralGrid* self, PyObject* args) {
int box_h, box_w, step = 1;
if (!PyArg_ParseTuple(args, "ii|i", &box_h, &box_w, &step)) return NULL;
auto result = self->grid->find_spot_parallel(box_h, box_w, step);
if (result.first) return Py_BuildValue("ii", result.second.first, result.second.second);
Py_RETURN_NONE;
}
static PyObject* Grid_query_reservoir(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;
// v3: delegate to query_direct for parallel reservoir
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;
}
// v3: Direct pixel-grid scan with parallel counting
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);
if (r.found) return Py_BuildValue("ii", r.y, r.x);
Py_RETURN_NONE;
}
// v3: Batch query — process multiple placements in one C++ call
// Input: list of (box_h, box_w, seed) tuples
// Output: list of (y, x) or None for each
static PyObject* Grid_batch_query(PyIntegralGrid* self, PyObject* args) {
PyObject* query_list;
if (!PyArg_ParseTuple(args, "O", &query_list)) return NULL;
PyObject* seq = PySequence_Fast(query_list, "expected a sequence");
if (!seq) return NULL;
Py_ssize_t n = PySequence_Fast_GET_SIZE(seq);
std::vector<std::tuple<int,int,uint32_t>> queries;
queries.reserve(n);
for (Py_ssize_t i = 0; i < n; ++i) {
PyObject* item = PySequence_Fast_GET_ITEM(seq, i);
int bh, bw;
unsigned int seed = 0;
if (!PyArg_ParseTuple(item, "ii|I", &bh, &bw, &seed)) {
Py_DECREF(seq);
return NULL;
}
queries.push_back({bh, bw, seed});
}
Py_DECREF(seq);
auto results = self->grid->batch_query(queries);
PyObject* result_list = PyList_New(n);
for (Py_ssize_t i = 0; i < n; ++i) {
if (results[i].found) {
PyList_SET_ITEM(result_list, i, Py_BuildValue("ii", results[i].y, results[i].x));
} else {
Py_INCREF(Py_None);
PyList_SET_ITEM(result_list, i, Py_None);
}
}
return result_list;
}
static PyObject* Grid_update(PyIntegralGrid* self, PyObject* args) {
int r, c, h, w;
if (!PyArg_ParseTuple(args, "iiii", &r, &c, &h, &w)) return NULL;
self->grid->update_rect_add(r, c, h, w);
Py_RETURN_NONE;
}
static PyObject* Grid_flush(PyIntegralGrid* self, PyObject* args) {
self->grid->flush();
Py_RETURN_NONE;
}
// v4: Rebuild integral from full bitmap array
static PyObject* Grid_rebuild_from_bitmap(PyIntegralGrid* self, PyObject* args) {
PyObject* arr_obj;
if (!PyArg_ParseTuple(args, "O", &arr_obj)) return NULL;
Py_buffer view;
if (PyObject_GetBuffer(arr_obj, &view, PyBUF_SIMPLE) < 0) return NULL;
self->grid->rebuild_from_bitmap((const unsigned char*)view.buf);
PyBuffer_Release(&view);
Py_RETURN_NONE;
}
// v4: Partial rebuild from position (pos_r, pos_c)
static PyObject* Grid_rebuild_from_bitmap_partial(PyIntegralGrid* self, PyObject* args) {
PyObject* arr_obj;
int pos_r, pos_c;
if (!PyArg_ParseTuple(args, "Oii", &arr_obj, &pos_r, &pos_c)) return NULL;
Py_buffer view;
if (PyObject_GetBuffer(arr_obj, &view, PyBUF_SIMPLE) < 0) return NULL;
self->grid->rebuild_from_bitmap_partial((const unsigned char*)view.buf, pos_r, pos_c);
PyBuffer_Release(&view);
Py_RETURN_NONE;
}
// v4: Stamp glyph bitmap onto C++ canvas and rebuild integral
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);
PyBuffer_Release(&view);
Py_RETURN_NONE;
}
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."},
{"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."},
{"rebuild_from_bitmap", (PyCFunction)Grid_rebuild_from_bitmap, METH_VARARGS, "Rebuild integral from pixel bitmap array."},
{"rebuild_from_bitmap_partial", (PyCFunction)Grid_rebuild_from_bitmap_partial, METH_VARARGS, "Partial integral rebuild from (pos_r, pos_c)."},
{"stamp_and_rebuild", (PyCFunction)Grid_stamp_and_rebuild, METH_VARARGS, "Stamp glyph + partial integral rebuild (avoids PIL->numpy)."},
{NULL}
};
static PyTypeObject PyIntegralGridType = {
PyVarObject_HEAD_INIT(NULL, 0)
"ewc_core.IntegralGrid",
sizeof(PyIntegralGrid),
0,
(destructor)Grid_dealloc,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
Py_TPFLAGS_DEFAULT,
"Integral Grid v3",
0, 0, 0, 0, 0, 0,
Grid_methods,
0, 0, 0, 0, 0, 0, 0,
(initproc)Grid_init,
0,
Grid_new,
};
static PyModuleDef ewc_module = {
PyModuleDef_HEAD_INIT, "ewc_core", "EfficientWordCloud Core v3", -1, NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC PyInit_ewc_core(void) {
PyObject* m;
if (PyType_Ready(&PyIntegralGridType) < 0) return NULL;
m = PyModule_Create(&ewc_module);
if (!m) return NULL;
Py_INCREF(&PyIntegralGridType);
if (PyModule_AddObject(m, "IntegralGrid", (PyObject *)&PyIntegralGridType) < 0) return NULL;
return m;
}