contrek 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.rubocop.yml +11 -0
- data/CHANGELOG.md +7 -1
- data/Gemfile +2 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +2 -0
- data/contrek.gemspec +2 -0
- data/ext/cpp_polygon_finder/PolygonFinder/CMakeLists.txt +2 -2
- data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +108 -1
- data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.h +1 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h +73 -6
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Finder.h +1 -1
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.cpp +6 -6
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.h +3 -2
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.cpp +114 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.h +41 -0
- data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +52 -2
- data/ext/cpp_polygon_finder/extconf.rb +2 -0
- data/lib/contrek/bitmaps/bitmap.rb +2 -0
- data/lib/contrek/bitmaps/chunky_bitmap.rb +2 -0
- data/lib/contrek/bitmaps/painting.rb +2 -0
- data/lib/contrek/bitmaps/png_bitmap.rb +2 -0
- data/lib/contrek/bitmaps/raw_bitmap.rb +2 -0
- data/lib/contrek/bitmaps/rgb_color.rb +2 -0
- data/lib/contrek/bitmaps/rgb_cpp_color.rb +2 -0
- data/lib/contrek/bitmaps/sample_generator.rb +2 -0
- data/lib/contrek/cpp/cpp_concurrent_finder.rb +2 -0
- data/lib/contrek/cpp/cpp_concurrent_horizontal_merger.rb +2 -0
- data/lib/contrek/cpp/cpp_concurrent_merger.rb +2 -0
- data/lib/contrek/cpp/cpp_concurrent_streaming_merger.rb +11 -0
- data/lib/contrek/cpp/cpp_concurrent_vertical_merger.rb +2 -0
- data/lib/contrek/cpp/cpp_result.rb +2 -0
- data/lib/contrek/cpp/cpp_tempfile.rb +28 -0
- data/lib/contrek/finder/bounds.rb +2 -0
- data/lib/contrek/finder/concurrent/clipped_polygon_finder.rb +2 -0
- data/lib/contrek/finder/concurrent/cluster.rb +2 -0
- data/lib/contrek/finder/concurrent/cursor.rb +2 -0
- data/lib/contrek/finder/concurrent/end_point.rb +2 -0
- data/lib/contrek/finder/concurrent/fake_cluster.rb +2 -0
- data/lib/contrek/finder/concurrent/finder.rb +2 -0
- data/lib/contrek/finder/concurrent/horizontal_merger.rb +2 -0
- data/lib/contrek/finder/concurrent/hub.rb +2 -0
- data/lib/contrek/finder/concurrent/inner_polyline.rb +2 -0
- data/lib/contrek/finder/concurrent/listable.rb +2 -0
- data/lib/contrek/finder/concurrent/merger.rb +3 -0
- data/lib/contrek/finder/concurrent/part.rb +2 -0
- data/lib/contrek/finder/concurrent/partitionable.rb +2 -0
- data/lib/contrek/finder/concurrent/polyline.rb +2 -0
- data/lib/contrek/finder/concurrent/poolable.rb +2 -0
- data/lib/contrek/finder/concurrent/position.rb +2 -0
- data/lib/contrek/finder/concurrent/queueable.rb +2 -0
- data/lib/contrek/finder/concurrent/sequence.rb +2 -0
- data/lib/contrek/finder/concurrent/shape.rb +2 -0
- data/lib/contrek/finder/concurrent/streaming_merger.rb +89 -0
- data/lib/contrek/finder/concurrent/tile.rb +2 -0
- data/lib/contrek/finder/concurrent/vertical_merger.rb +4 -2
- data/lib/contrek/finder/list.rb +2 -0
- data/lib/contrek/finder/list_entry.rb +2 -0
- data/lib/contrek/finder/listable.rb +2 -0
- data/lib/contrek/finder/lists.rb +2 -0
- data/lib/contrek/finder/node.rb +2 -0
- data/lib/contrek/finder/node_cluster.rb +3 -1
- data/lib/contrek/finder/polygon_finder.rb +2 -0
- data/lib/contrek/finder/result.rb +2 -0
- data/lib/contrek/map/mercator_projection.rb +2 -0
- data/lib/contrek/matchers/matcher.rb +2 -0
- data/lib/contrek/matchers/matcher_hsb.rb +2 -0
- data/lib/contrek/matchers/value_not_matcher.rb +2 -0
- data/lib/contrek/reducers/linear_reducer.rb +2 -0
- data/lib/contrek/reducers/reducer.rb +2 -0
- data/lib/contrek/reducers/uniq_reducer.rb +2 -0
- data/lib/contrek/reducers/visvalingam_reducer.rb +2 -0
- data/lib/contrek/shared/result.rb +2 -0
- data/lib/contrek/version.rb +3 -1
- data/lib/contrek.rb +5 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f506a5908c11f7ac1de118b4772ad9e85cd00746e4ee57b5d9c1600faea371e
|
|
4
|
+
data.tar.gz: 81c2807712361d51398962c9496bf3c833b21eb42d34907069c923cd62654f62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 76c41016ce95c124ed72beacd5c838bf5fe960483e3cf7b78848d53c3d997948f5de0c8ecacdb08d3c38b8faf7349ec79cef9546415773e5d5201019b04fe3b2
|
|
7
|
+
data.tar.gz: 1823196730b740221b2e75be9915706b5b96afd91349dd898eda70d1ccce69f569270e134663137523ea3860e5d49a419b59a083a6a11be027bf4e14a1a0e30b
|
data/.gitignore
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
.ruby-version
|
|
4
4
|
# rspec failure tracking
|
|
5
5
|
.rspec_status
|
|
6
|
+
/spec/files/performance_history.json
|
|
6
7
|
|
|
7
8
|
/ext/cpp_polygon_finder/PolygonFinder/build/
|
|
8
9
|
/ext/cpp_polygon_finder/PolygonFinder/contrek
|
|
@@ -12,4 +13,4 @@
|
|
|
12
13
|
/ext/cpp_polygon_finder/cpp_polygon_finder.o
|
|
13
14
|
/ext/cpp_polygon_finder/cpp_polygon_finder.so
|
|
14
15
|
/ext/cpp_polygon_finder/mkmf.log
|
|
15
|
-
/lib/cpp_polygon_finder.so
|
|
16
|
+
/lib/cpp_polygon_finder.so
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -115,4 +115,10 @@ All notable changes to this project will be documented in this file.
|
|
|
115
115
|
|
|
116
116
|
## [1.2.7] - 2026-06-02
|
|
117
117
|
### Changed
|
|
118
|
-
- **Refactored `bounds` option:** Starting from this release, precalculated bounds for each polygon can now be requested in concurrent mode as well, in addition to single-threaded mode.
|
|
118
|
+
- **Refactored `bounds` option:** Starting from this release, precalculated bounds for each polygon can now be requested in concurrent mode as well, in addition to single-threaded mode.
|
|
119
|
+
|
|
120
|
+
## [1.2.8] - 2026-06-07
|
|
121
|
+
- **Optimize main pixel scanning loop:** Implemented 4-way loop unrolling to maximize L1 cache hits and eliminate redundant RAM lookups via direct register bit-casting.
|
|
122
|
+
|
|
123
|
+
## [1.2.9] - 2026-06-13
|
|
124
|
+
- **Streaming merger:** The streaming merger class extends VerticalMerger and adds a useful feature: the progressive extraction of contours into a disk buffer (SVG file). In this way, all extracted polygons that are no longer within the junction zone of the next stripe are removed from the system and streamed directly to disk. This incredibly reduces memory consumption, allowing the processing of very large files on machines with low memory availability, at the expense of increased processing times. An example of this technique is available in both C++ and Ruby in the repository.
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Contrek
|
|
2
|
-
Contrek is a Ruby gem powered by a <u>standalone C++17 core library</u> for fast contour tracing and edge detection in PNG images and raw memory streams. It employs a **topological approach** to extract polygonal contours, representing shapes as a connected graph of shared endpoints. This ensures perfect adjacency and structural integrity for shape analysis and raster-to-vector workflows, such as PNG to SVG conversion, managed via libspng (0.7.4) with multithreading support.
|
|
2
|
+
Contrek is a Ruby gem powered by a <u>[standalone C++17 core library](#-c-standalone-library-usage)</u> for fast contour tracing and edge detection in PNG images and raw memory streams. It employs a **topological approach** to extract polygonal contours, representing shapes as a connected graph of shared endpoints. This ensures perfect adjacency and structural integrity for shape analysis and raster-to-vector workflows, such as PNG to SVG conversion, managed via libspng (0.7.4) with multithreading support.
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
## About Contrek library
|
data/Rakefile
CHANGED
data/contrek.gemspec
CHANGED
|
@@ -11,8 +11,8 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|
|
11
11
|
list(FILTER CMAKE_CXX_FLAGS EXCLUDE REGEX "-DNDEBUG")
|
|
12
12
|
list(FILTER CMAKE_C_FLAGS EXCLUDE REGEX "-DNDEBUG")
|
|
13
13
|
else()
|
|
14
|
-
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread -march=native -DNDEBUG -Ofast -flto")
|
|
15
|
-
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -
|
|
14
|
+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread -march=native -fPIC -DNDEBUG -Ofast -flto -ftree-vectorize")
|
|
15
|
+
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -pthread -march=native -fPIC -DNDEBUG -Ofast -flto -ftree-vectorize")
|
|
16
16
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread -flto=auto -Wl,--no-as-needed")
|
|
17
17
|
find_library(TCMALLOC_LIB tcmalloc)
|
|
18
18
|
if(TCMALLOC_LIB)
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
#include "polygon/finder/concurrent/Finder.h"
|
|
35
35
|
#include "polygon/finder/concurrent/HorizontalMerger.h"
|
|
36
36
|
#include "polygon/finder/concurrent/VerticalMerger.h"
|
|
37
|
+
#include "polygon/finder/concurrent/StreamingMerger.h"
|
|
37
38
|
#include "polygon/finder/concurrent/Sequence.h"
|
|
38
39
|
#include "polygon/finder/concurrent/Position.h"
|
|
39
40
|
#include "polygon/finder/Polygon.h"
|
|
@@ -385,6 +386,112 @@ void stream_png_image(const std::string& filepath, uint32_t stripe_height, bool
|
|
|
385
386
|
}
|
|
386
387
|
|
|
387
388
|
void Tests::test_i() {
|
|
388
|
-
stream_png_image("../images/graphs_1024x1024.png", 300);
|
|
389
|
+
stream_png_image("../images/graphs_1024x1024.png", 300, true);
|
|
390
|
+
std::cout << "Memory usage peak: " << get_peak_rss() << " MB" << std::endl;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_height) {
|
|
394
|
+
std::vector<ProcessResult*> result_clones;
|
|
395
|
+
std::vector<std::string> varguments = {"--bounds"};
|
|
396
|
+
// opens image to stream
|
|
397
|
+
FILE* fp = fopen(filepath.c_str(), "rb");
|
|
398
|
+
if (!fp) {
|
|
399
|
+
std::cerr << "Unable open file: " << filepath << std::endl;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// exams image
|
|
404
|
+
spng_ctx *ctx = spng_ctx_new(0);
|
|
405
|
+
spng_set_png_file(ctx, fp);
|
|
406
|
+
struct spng_ihdr ihdr;
|
|
407
|
+
if (spng_get_ihdr(ctx, &ihdr)) {
|
|
408
|
+
fclose(fp);
|
|
409
|
+
spng_ctx_free(ctx);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
uint32_t total_width = ihdr.width;
|
|
413
|
+
uint32_t total_height = ihdr.height;
|
|
414
|
+
|
|
415
|
+
// allocates stripe buffer
|
|
416
|
+
RawBitmap stripe_bitmap;
|
|
417
|
+
stripe_bitmap.define(total_width, stripe_height, 4, true);
|
|
418
|
+
RGBNotMatcher not_matcher(-1);
|
|
419
|
+
if (spng_decode_image(ctx, NULL, 0, SPNG_FMT_RGBA8, SPNG_DECODE_PROGRESSIVE)) {
|
|
420
|
+
fclose(fp);
|
|
421
|
+
spng_ctx_free(ctx);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// allocates streaming svg buffer
|
|
426
|
+
std::string output_path = "streaming_buffer.svg";
|
|
427
|
+
std::ofstream shared_stream(output_path, std::ios::out | std::ios::binary);
|
|
428
|
+
if (!shared_stream) {
|
|
429
|
+
std::cerr << "Error: Unable creating output streaming file!" << std::endl;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
StreamingMerger vmerger(0, &varguments, &shared_stream, total_width, total_height);
|
|
433
|
+
try {
|
|
434
|
+
size_t row_size = static_cast<size_t>(total_width) * 4;
|
|
435
|
+
int stripe_count = 0;
|
|
436
|
+
// main stripes loop
|
|
437
|
+
for (uint32_t current_y_offset = 0; current_y_offset < total_height; current_y_offset += stripe_height) {
|
|
438
|
+
int uncovered_height = total_height - current_y_offset;
|
|
439
|
+
|
|
440
|
+
// copy previous last line to the next new one (each contigue stripe must share one pixel scanline)
|
|
441
|
+
if (current_y_offset > 0) {
|
|
442
|
+
unsigned char* last_row_prev = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(stripe_height - 1));
|
|
443
|
+
unsigned char* first_row_curr = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(0));
|
|
444
|
+
std::memcpy(first_row_curr, last_row_prev, row_size);
|
|
445
|
+
}
|
|
446
|
+
// clears the part of the stripe wont be overwritten by png data
|
|
447
|
+
if (uncovered_height < stripe_height)
|
|
448
|
+
{ stripe_bitmap.draw_filled_rectangle(0, 1, total_width, stripe_height - 1, 255, 255, 255);
|
|
449
|
+
}
|
|
450
|
+
// decoding data directly in the stripe buffer
|
|
451
|
+
uint32_t lines_to_read = std::min(stripe_height, total_height - current_y_offset);
|
|
452
|
+
for (uint32_t y = (current_y_offset == 0 ? 0 : 1); y < lines_to_read; y++) {
|
|
453
|
+
unsigned char* row_ptr = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(y));
|
|
454
|
+
int ret = spng_decode_row(ctx, row_ptr, row_size);
|
|
455
|
+
if (ret != 0 && ret != SPNG_EOI) break;
|
|
456
|
+
}
|
|
457
|
+
// stripe contour tracing
|
|
458
|
+
std::vector<std::string> finder_arguments = {
|
|
459
|
+
"--versus=a",
|
|
460
|
+
"--bounds",
|
|
461
|
+
"--strict_bounds", // <- this option is strictly needed when working with vertical merger
|
|
462
|
+
"--compress_uniq",
|
|
463
|
+
"--compress_linear"
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
PolygonFinder polygon_finder(&stripe_bitmap, ¬_matcher, nullptr, &finder_arguments);
|
|
467
|
+
ProcessResult *result = polygon_finder.process_info();
|
|
468
|
+
if (result) {
|
|
469
|
+
std::cout << "stripe " << stripe_count << ": found polygons " << result->groups << std::endl;
|
|
470
|
+
ProcessResult* safe_result = result->clone();
|
|
471
|
+
result_clones.push_back(safe_result);
|
|
472
|
+
vmerger.add_tile(*safe_result, !(current_y_offset + stripe_height < total_height));
|
|
473
|
+
delete result;
|
|
474
|
+
}
|
|
475
|
+
stripe_count++;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
ProcessResult *merged_result = vmerger.process_info();
|
|
479
|
+
std::cout << "total found polygons " << merged_result->groups << std::endl;
|
|
480
|
+
delete merged_result;
|
|
481
|
+
|
|
482
|
+
// frees memory
|
|
483
|
+
for (auto c : result_clones) {
|
|
484
|
+
delete c;
|
|
485
|
+
}
|
|
486
|
+
} catch (const std::exception& e) {
|
|
487
|
+
std::cerr << "\n[ERROR] Processing exception: " << e.what() << std::endl;
|
|
488
|
+
if (shared_stream.is_open()) shared_stream.close();
|
|
489
|
+
}
|
|
490
|
+
spng_ctx_free(ctx);
|
|
491
|
+
fclose(fp);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
void Tests::test_l() {
|
|
495
|
+
stream_progressive_png_image("../images/mixed_shapes_1024x1024.png", 300);
|
|
389
496
|
std::cout << "Memory usage peak: " << get_peak_rss() << " MB" << std::endl;
|
|
390
497
|
}
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
#include <sstream>
|
|
21
21
|
#include <stdexcept>
|
|
22
22
|
#include <limits>
|
|
23
|
+
#include <cstdint>
|
|
24
|
+
|
|
23
25
|
#include "../bitmaps/Bitmap.h"
|
|
24
26
|
#include "NodeCluster.h"
|
|
25
27
|
#include "Node.h"
|
|
@@ -210,15 +212,80 @@ class PolygonFinder {
|
|
|
210
212
|
void run_loop(M* specific_matcher, F&& fetch_color, int offset) {
|
|
211
213
|
int img_h = this->source_bitmap->h();
|
|
212
214
|
int bpp = this->source_bitmap->get_bytes_per_pixel();
|
|
215
|
+
|
|
213
216
|
for (int y = 0; y < img_h; y++) {
|
|
214
217
|
const unsigned char* row_ptr = this->source_bitmap->get_row_ptr(y);
|
|
215
218
|
const unsigned char* p = row_ptr + (this->start_x * bpp);
|
|
219
|
+
|
|
216
220
|
int min_x = 0;
|
|
217
221
|
bool matching = false;
|
|
218
222
|
unsigned char last_red_value = 0;
|
|
219
|
-
|
|
223
|
+
|
|
224
|
+
int x = this->start_x;
|
|
225
|
+
|
|
226
|
+
if (bpp == 4) {
|
|
227
|
+
for (; x <= this->end_x - 4; x += 4) {
|
|
228
|
+
// read 4 pixels (16 bytes)
|
|
229
|
+
unsigned int c0 = fetch_color(p);
|
|
230
|
+
unsigned int c1 = fetch_color(p + 4);
|
|
231
|
+
unsigned int c2 = fetch_color(p + 8);
|
|
232
|
+
unsigned int c3 = fetch_color(p + 12);
|
|
233
|
+
|
|
234
|
+
// extracts value (used as debugging segment label)
|
|
235
|
+
unsigned char v0 = static_cast<unsigned char>(c0);
|
|
236
|
+
unsigned char v1 = static_cast<unsigned char>(c1);
|
|
237
|
+
unsigned char v2 = static_cast<unsigned char>(c2);
|
|
238
|
+
unsigned char v3 = static_cast<unsigned char>(c3);
|
|
239
|
+
|
|
240
|
+
p += 16;
|
|
241
|
+
|
|
242
|
+
bool m0 = specific_matcher->match(c0);
|
|
243
|
+
bool m1 = specific_matcher->match(c1);
|
|
244
|
+
bool m2 = specific_matcher->match(c2);
|
|
245
|
+
bool m3 = specific_matcher->match(c3);
|
|
246
|
+
|
|
247
|
+
if (m0) {
|
|
248
|
+
if (!matching) {
|
|
249
|
+
min_x = x; last_red_value = v0; matching = true;
|
|
250
|
+
}
|
|
251
|
+
} else if (matching) {
|
|
252
|
+
this->node_cluster->add_node(min_x, x - 1, y, last_red_value, offset);
|
|
253
|
+
matching = false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (m1) {
|
|
257
|
+
if (!matching) {
|
|
258
|
+
min_x = x + 1; last_red_value = v1; matching = true;
|
|
259
|
+
}
|
|
260
|
+
} else if (matching) {
|
|
261
|
+
this->node_cluster->add_node(min_x, x, y, last_red_value, offset);
|
|
262
|
+
matching = false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (m2) {
|
|
266
|
+
if (!matching) {
|
|
267
|
+
min_x = x + 2; last_red_value = v2; matching = true;
|
|
268
|
+
}
|
|
269
|
+
} else if (matching) {
|
|
270
|
+
this->node_cluster->add_node(min_x, x + 1, y, last_red_value, offset);
|
|
271
|
+
matching = false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (m3) {
|
|
275
|
+
if (!matching) {
|
|
276
|
+
min_x = x + 3; last_red_value = v3; matching = true;
|
|
277
|
+
}
|
|
278
|
+
} else if (matching) {
|
|
279
|
+
this->node_cluster->add_node(min_x, x + 2, y, last_red_value, offset);
|
|
280
|
+
matching = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// remaining pixels (width not a multiple of 4)
|
|
286
|
+
for (; x < this->end_x; x++) {
|
|
220
287
|
unsigned int color = fetch_color(p);
|
|
221
|
-
unsigned char current_val =
|
|
288
|
+
unsigned char current_val = static_cast<unsigned char>(color);
|
|
222
289
|
p += bpp;
|
|
223
290
|
if (specific_matcher->match(color)) {
|
|
224
291
|
if (!matching) {
|
|
@@ -226,15 +293,15 @@ class PolygonFinder {
|
|
|
226
293
|
last_red_value = current_val;
|
|
227
294
|
matching = true;
|
|
228
295
|
}
|
|
229
|
-
if (x == this->end_x - 1) {
|
|
230
|
-
this->node_cluster->add_node(min_x, x, y, last_red_value, offset);
|
|
231
|
-
matching = false;
|
|
232
|
-
}
|
|
233
296
|
} else if (matching) {
|
|
234
297
|
this->node_cluster->add_node(min_x, x - 1, y, last_red_value, offset);
|
|
235
298
|
matching = false;
|
|
236
299
|
}
|
|
237
300
|
}
|
|
301
|
+
|
|
302
|
+
if (matching) {
|
|
303
|
+
this->node_cluster->add_node(min_x, this->end_x - 1, y, last_red_value, offset);
|
|
304
|
+
}
|
|
238
305
|
}
|
|
239
306
|
}
|
|
240
307
|
|
|
@@ -35,7 +35,6 @@ class Finder : public Poolable {
|
|
|
35
35
|
Matcher *matcher;
|
|
36
36
|
pf_Options options_;
|
|
37
37
|
std::vector<std::string> input_options;
|
|
38
|
-
Tile* whole_tile = nullptr;
|
|
39
38
|
std::queue<ClippedPolygonFinder*> finders;
|
|
40
39
|
std::mutex finders_mutex;
|
|
41
40
|
std::map<std::string, double> reports;
|
|
@@ -45,6 +44,7 @@ class Finder : public Poolable {
|
|
|
45
44
|
Queue<Tile*> tiles_;
|
|
46
45
|
int maximum_width_;
|
|
47
46
|
int height = 0;
|
|
47
|
+
Tile* whole_tile = nullptr;
|
|
48
48
|
void process_tiles();
|
|
49
49
|
|
|
50
50
|
public:
|
|
@@ -21,7 +21,7 @@ Polyline::Polyline(Tile* tile, const std::vector<Point*>& polygon, const std::op
|
|
|
21
21
|
tile(tile)
|
|
22
22
|
{ if (bounds.has_value()) {
|
|
23
23
|
min_x = bounds->min_x;
|
|
24
|
-
|
|
24
|
+
max_x_ = bounds->max_x;
|
|
25
25
|
min_y_ = bounds->min_y;
|
|
26
26
|
max_y_ = bounds->max_y;
|
|
27
27
|
} else {
|
|
@@ -32,17 +32,17 @@ Polyline::Polyline(Tile* tile, const std::vector<Point*>& polygon, const std::op
|
|
|
32
32
|
|
|
33
33
|
int Polyline::width() {
|
|
34
34
|
if (raw_.empty()) return 0;
|
|
35
|
-
return(
|
|
35
|
+
return(max_x_ - min_x);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
bool Polyline::boundary() {
|
|
39
|
-
return( tile->tg_border(Point{min_x, 0}) || tile->tg_border(Point{
|
|
39
|
+
return( tile->tg_border(Point{min_x, 0}) || tile->tg_border(Point{max_x_, 0}));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
void Polyline::find_boundary() {
|
|
43
43
|
if (raw_.empty()) return;
|
|
44
44
|
min_x = std::numeric_limits<int>::max();
|
|
45
|
-
|
|
45
|
+
max_x_ = -std::numeric_limits<int>::max();
|
|
46
46
|
min_y_ = std::numeric_limits<int>::max();
|
|
47
47
|
max_y_ = -std::numeric_limits<int>::max();
|
|
48
48
|
for (Point* p : raw_) {
|
|
@@ -50,7 +50,7 @@ void Polyline::find_boundary() {
|
|
|
50
50
|
int x = p->x;
|
|
51
51
|
int y = p->y;
|
|
52
52
|
if (x < min_x) min_x = x;
|
|
53
|
-
if (x >
|
|
53
|
+
if (x > max_x_) max_x_ = x;
|
|
54
54
|
if (y < min_y_) min_y_ = y;
|
|
55
55
|
if (y > max_y_) max_y_ = y;
|
|
56
56
|
}
|
|
@@ -98,7 +98,7 @@ std::string Polyline::named() {
|
|
|
98
98
|
|
|
99
99
|
void Polyline::fill_bounds(RectBounds& target_bounds) const {
|
|
100
100
|
target_bounds.min_x = this->min_x;
|
|
101
|
-
target_bounds.max_x = this->
|
|
101
|
+
target_bounds.max_x = this->max_x_;
|
|
102
102
|
target_bounds.min_y = this->min_y_;
|
|
103
103
|
target_bounds.max_y = this->max_y_;
|
|
104
104
|
}
|
|
@@ -40,10 +40,11 @@ class Polyline : public Partitionable {
|
|
|
40
40
|
int width();
|
|
41
41
|
Tile *tile = nullptr;
|
|
42
42
|
Shape* shape = nullptr;
|
|
43
|
-
std::vector<Point
|
|
43
|
+
const std::vector<Point*>& raw() const { return raw_; }
|
|
44
44
|
const std::vector<Part*>& parts() const { return parts_; }
|
|
45
45
|
const int max_y() const { return max_y_; }
|
|
46
46
|
const int min_y() const { return min_y_; }
|
|
47
|
+
const int max_x() const { return max_x_; }
|
|
47
48
|
void clear();
|
|
48
49
|
bool is_empty();
|
|
49
50
|
bool any_ancients = false;
|
|
@@ -56,7 +57,7 @@ class Polyline : public Partitionable {
|
|
|
56
57
|
|
|
57
58
|
private:
|
|
58
59
|
std::vector<Point*> raw_;
|
|
59
|
-
int min_x,
|
|
60
|
+
int min_x, max_x_, min_y_, max_y_;
|
|
60
61
|
void find_boundary();
|
|
61
62
|
uint32_t flags_ = 0;
|
|
62
63
|
std::string named_;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* StreamingMerger.cpp
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025-2026 Emanuele Cesaroni
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the GNU Affero General Public License v3 (AGPLv3).
|
|
7
|
+
* See the LICENSE file in this directory for the full license text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include "StreamingMerger.h"
|
|
11
|
+
#include <sstream>
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <vector>
|
|
14
|
+
#include <string>
|
|
15
|
+
|
|
16
|
+
StreamingMerger::StreamingMerger(int number_of_threads,
|
|
17
|
+
std::vector<std::string>* options,
|
|
18
|
+
std::ofstream* stream_to,
|
|
19
|
+
int total_width, int total_height)
|
|
20
|
+
: VerticalMerger(number_of_threads, options), stream(stream_to), total_width(total_width), total_height(total_height) {
|
|
21
|
+
if (!stream) {
|
|
22
|
+
throw std::invalid_argument("Streaming requires a valid destination output. stream_to cannot be null.");
|
|
23
|
+
}
|
|
24
|
+
if (total_width <= 0 || total_height <= 0) {
|
|
25
|
+
throw std::invalid_argument("Streaming requires valid canvas dimensions (width and height must be > 0).");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void StreamingMerger::add_tile(ProcessResult& result, bool flush)
|
|
30
|
+
{ VerticalMerger::add_tile(result);
|
|
31
|
+
if (tiles_.size() == 2) {
|
|
32
|
+
this->process_tiles();
|
|
33
|
+
this->tiles_.queue_push(this->whole_tile);
|
|
34
|
+
this->stream_polygons(this->whole_tile, flush);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ProcessResult* StreamingMerger::process_info() {
|
|
39
|
+
ProcessResult *pr = VerticalMerger::process_info();
|
|
40
|
+
pr->groups = this->moved;
|
|
41
|
+
return(pr);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void StreamingMerger::stream_polygons(Tile* tile, bool flush) {
|
|
45
|
+
ensure_header();
|
|
46
|
+
if (int tile_end_x = tile->end_x(); true) {
|
|
47
|
+
tile->shapes().erase(
|
|
48
|
+
std::remove_if(tile->shapes().begin(), tile->shapes().end(), [this, flush, tile_end_x](const Shape* shape) {
|
|
49
|
+
if (flush || shape->outer_polyline->max_x() < (tile_end_x - 1)) {
|
|
50
|
+
this->moved++;
|
|
51
|
+
this->stream_raw_polygon(shape);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}),
|
|
56
|
+
tile->shapes().end());
|
|
57
|
+
}
|
|
58
|
+
if (flush) {
|
|
59
|
+
ensure_footer();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
void StreamingMerger::stream_raw_polygon(const Shape* shape) {
|
|
64
|
+
std::ostringstream outer_oss;
|
|
65
|
+
const std::vector<Point*> points = shape->outer_polyline->raw();
|
|
66
|
+
for (size_t i = 0; i < points.size(); ++i) {
|
|
67
|
+
outer_oss << points[i]->y << "," << points[i]->x;
|
|
68
|
+
if (i < points.size() - 1) outer_oss << " ";
|
|
69
|
+
}
|
|
70
|
+
*stream << svg_outer_polygon_string(outer_oss.str());
|
|
71
|
+
|
|
72
|
+
for (const auto& inner_polyline : shape->inner_polylines) {
|
|
73
|
+
std::ostringstream inner_oss;
|
|
74
|
+
const std::vector<Point*> inner_points = inner_polyline->raw();
|
|
75
|
+
for (size_t i = 0; i < inner_points.size(); ++i) {
|
|
76
|
+
inner_oss << inner_points[i]->y << "," << inner_points[i]->x;
|
|
77
|
+
if (i < inner_points.size() - 1) inner_oss << " ";
|
|
78
|
+
}
|
|
79
|
+
*stream << svg_inner_polygon_string(inner_oss.str());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void StreamingMerger::ensure_header() {
|
|
84
|
+
if (stream && stream->tellp() == 0) {
|
|
85
|
+
*stream << svg_header_string();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
void StreamingMerger::ensure_footer() {
|
|
90
|
+
if (stream) {
|
|
91
|
+
*stream << svg_footer_string();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
std::string StreamingMerger::svg_css() {
|
|
96
|
+
return ".out{fill:none;stroke:red;stroke-width:1;}.in{fill:none;stroke:green;stroke-width:1;}.out:hover{stroke:yellow;}";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
std::string StreamingMerger::svg_header_string() {
|
|
100
|
+
return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + std::to_string(total_width) +
|
|
101
|
+
"\" height=\"" + std::to_string(total_height) + "\"><style>" + svg_css() + "</style>";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
std::string StreamingMerger::svg_footer_string() {
|
|
105
|
+
return "</svg>";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
std::string StreamingMerger::svg_outer_polygon_string(std::string_view points) {
|
|
109
|
+
return "<polygon points=\"" + std::string(points) + "\" class=\"out\"/>";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
std::string StreamingMerger::svg_inner_polygon_string(std::string_view points) {
|
|
113
|
+
return "<polygon points=\"" + std::string(points) + "\" class=\"in\"/>";
|
|
114
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* StreamingMerger.h
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025-2026 Emanuele Cesaroni
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the GNU Affero General Public License v3 (AGPLv3).
|
|
7
|
+
* See the LICENSE file in this directory for the full license text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#pragma once
|
|
11
|
+
#include "VerticalMerger.h"
|
|
12
|
+
#include <fstream>
|
|
13
|
+
#include <string>
|
|
14
|
+
#include <string_view>
|
|
15
|
+
#include <stdexcept>
|
|
16
|
+
#include <vector>
|
|
17
|
+
|
|
18
|
+
class StreamingMerger : public VerticalMerger {
|
|
19
|
+
private:
|
|
20
|
+
std::ofstream* stream;
|
|
21
|
+
int total_width;
|
|
22
|
+
int total_height;
|
|
23
|
+
int moved = 0;
|
|
24
|
+
void ensure_header();
|
|
25
|
+
void ensure_footer();
|
|
26
|
+
void stream_polygons(Tile* tile, bool flush = false);
|
|
27
|
+
void stream_raw_polygon(const Shape* shape);
|
|
28
|
+
virtual std::string svg_css();
|
|
29
|
+
virtual std::string svg_header_string();
|
|
30
|
+
virtual std::string svg_footer_string();
|
|
31
|
+
virtual std::string svg_outer_polygon_string(std::string_view points);
|
|
32
|
+
virtual std::string svg_inner_polygon_string(std::string_view points);
|
|
33
|
+
|
|
34
|
+
public:
|
|
35
|
+
StreamingMerger(int number_of_threads,
|
|
36
|
+
std::vector<std::string>* options,
|
|
37
|
+
std::ofstream* stream_to,
|
|
38
|
+
int total_width, int total_height);
|
|
39
|
+
void add_tile(ProcessResult& result, bool flush = false);
|
|
40
|
+
ProcessResult* process_info() override;
|
|
41
|
+
};
|
|
@@ -96,6 +96,8 @@
|
|
|
96
96
|
#include "PolygonFinder/src/polygon/finder/concurrent/HorizontalMerger.cpp"
|
|
97
97
|
#include "PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.h"
|
|
98
98
|
#include "PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.cpp"
|
|
99
|
+
#include "PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.h"
|
|
100
|
+
#include "PolygonFinder/src/polygon/finder/concurrent/StreamingMerger.cpp"
|
|
99
101
|
#include "PolygonFinder/src/polygon/finder/concurrent/ShapePool.h"
|
|
100
102
|
#include "PolygonFinder/src/polygon/finder/concurrent/ShapePool.cpp"
|
|
101
103
|
extern "C" {
|
|
@@ -290,6 +292,36 @@ ProcessResult ruby_result_to_process_result(Rice::Object rb_result) {
|
|
|
290
292
|
|
|
291
293
|
} // namespace Rice::detail
|
|
292
294
|
|
|
295
|
+
struct OfstreamWrapper {
|
|
296
|
+
std::string path;
|
|
297
|
+
std::ofstream stream;
|
|
298
|
+
OfstreamWrapper(std::string p) : path(p), stream(p, std::ios::out) {}
|
|
299
|
+
void close() { stream.close(); }
|
|
300
|
+
bool closed() { return !stream.is_open(); }
|
|
301
|
+
void rewind() { /* noop */ }
|
|
302
|
+
std::string read() {
|
|
303
|
+
stream.flush();
|
|
304
|
+
std::ifstream in(path);
|
|
305
|
+
return std::string((std::istreambuf_iterator<char>(in)),
|
|
306
|
+
std::istreambuf_iterator<char>());
|
|
307
|
+
}
|
|
308
|
+
std::ofstream& get_stream() { return stream; }
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
StreamingMerger* create_streaming_merger(Object self,
|
|
312
|
+
int number_of_threads,
|
|
313
|
+
std::vector<std::string>* options,
|
|
314
|
+
Object stream_obj,
|
|
315
|
+
int total_width,
|
|
316
|
+
int total_height) {
|
|
317
|
+
OfstreamWrapper* wrapper = Rice::detail::From_Ruby<OfstreamWrapper*>().convert(stream_obj.value());
|
|
318
|
+
return new StreamingMerger(number_of_threads, options, &wrapper->get_stream(), total_width, total_height);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
OfstreamWrapper* create_ofstream(Object self, std::string path) {
|
|
322
|
+
return new OfstreamWrapper(path);
|
|
323
|
+
}
|
|
324
|
+
|
|
293
325
|
extern "C"
|
|
294
326
|
void Init_cpp_polygon_finder() {
|
|
295
327
|
#ifdef HAVE_TCMALLOC
|
|
@@ -384,9 +416,27 @@ void Init_cpp_polygon_finder() {
|
|
|
384
416
|
define_class<HorizontalMerger, Merger>("CPPHorizontalMerger")
|
|
385
417
|
.define_constructor(Constructor<HorizontalMerger, int, std::vector<std::string>*>(), Arg("number_of_threads"), Arg("options") = nullptr, Arg("yield_gvl") = true);
|
|
386
418
|
|
|
387
|
-
|
|
419
|
+
Data_Type<VerticalMerger> rb_cVerticalMerger =
|
|
388
420
|
define_class<VerticalMerger, Merger>("CPPVerticalMerger")
|
|
389
|
-
.define_constructor(Constructor<VerticalMerger, int, std::vector<std::string>*>(), Arg("number_of_threads"), Arg("options") = nullptr, Arg("yield_gvl") = true)
|
|
421
|
+
.define_constructor(Constructor<VerticalMerger, int, std::vector<std::string>*>(), Arg("number_of_threads"), Arg("options") = nullptr, Arg("yield_gvl") = true)
|
|
422
|
+
.define_method("add_tile", [](VerticalMerger& self, Object rb_result) {
|
|
423
|
+
ProcessResult pr = Rice::detail::ruby_result_to_process_result(rb_result);
|
|
424
|
+
self.add_tile(pr);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
Data_Type<OfstreamWrapper> rb_cstd_ofstream = define_class<OfstreamWrapper>("CPPOfstream");
|
|
428
|
+
rb_cstd_ofstream.define_singleton_method("new", &create_ofstream);
|
|
429
|
+
rb_cstd_ofstream.define_method("close", [](OfstreamWrapper& self) { self.close(); });
|
|
430
|
+
rb_cstd_ofstream.define_method("closed?", [](OfstreamWrapper& self) { return self.closed(); });
|
|
431
|
+
rb_cstd_ofstream.define_method("rewind", [](OfstreamWrapper& self) { self.rewind(); });
|
|
432
|
+
rb_cstd_ofstream.define_method("read", [](OfstreamWrapper& self) { return self.read(); });
|
|
433
|
+
|
|
434
|
+
Data_Type<StreamingMerger> rb_cstreaming_merger = define_class<StreamingMerger, VerticalMerger>("CPPStreamingMerger");
|
|
435
|
+
rb_cstreaming_merger.define_method("add_tile", [](StreamingMerger& self, Object rb_result, bool flush) {
|
|
436
|
+
ProcessResult pr = Rice::detail::ruby_result_to_process_result(rb_result);
|
|
437
|
+
self.add_tile(pr, flush);
|
|
438
|
+
});
|
|
439
|
+
rb_cstreaming_merger.define_singleton_method("new", &create_streaming_merger);
|
|
390
440
|
|
|
391
441
|
Module mContrek = define_module("Contrek");
|
|
392
442
|
Module mCpp = define_module_under(mContrek, "Cpp");
|