Initial project baseline
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user