contrek 1.1.5 → 1.1.6

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/README.md +59 -0
  4. data/docs/images/strict_bounds_off.png +0 -0
  5. data/docs/images/strict_bounds_on.png +0 -0
  6. data/docs/images/stripes/whole.png +0 -0
  7. data/docs/images/stripes/whole_0.png +0 -0
  8. data/docs/images/stripes/whole_256.png +0 -0
  9. data/docs/images/stripes/whole_512.png +0 -0
  10. data/docs/images/stripes/whole_768.png +0 -0
  11. data/ext/cpp_polygon_finder/PolygonFinder/examples/example.cpp +1 -0
  12. data/ext/cpp_polygon_finder/PolygonFinder/images/graphs_1024x1024.png +0 -0
  13. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +113 -0
  14. data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.h +1 -0
  15. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/RawBitmap.cpp +100 -1
  16. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/RawBitmap.h +4 -0
  17. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/FinderUtils.cpp +7 -1
  18. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.cpp +16 -3
  19. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.h +2 -0
  20. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PointPool.cpp +19 -0
  21. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PointPool.h +24 -0
  22. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.cpp +30 -0
  23. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h +39 -1
  24. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/RectBounds.h +12 -0
  25. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Part.cpp +1 -1
  26. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.cpp +1 -1
  27. data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +19 -5
  28. data/lib/contrek/bitmaps/chunky_bitmap.rb +50 -2
  29. data/lib/contrek/cpp/cpp_result.rb +42 -0
  30. data/lib/contrek/finder/concurrent/part.rb +9 -3
  31. data/lib/contrek/finder/concurrent/vertical_merger.rb +2 -2
  32. data/lib/contrek/finder/node_cluster.rb +15 -6
  33. data/lib/contrek/finder/polygon_finder.rb +1 -1
  34. data/lib/contrek/finder/result.rb +1 -5
  35. data/lib/contrek/version.rb +1 -1
  36. data/lib/contrek.rb +1 -1
  37. metadata +13 -3
  38. data/lib/contrek/results/cpp_result.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22d05e676f5c4a0f22f02ec2e915257bf1f5da224d468cb0e2f46fc397360242
4
- data.tar.gz: '0997b63d2839cc3ba613884eff2e3a565d96502e4606db0bb72d2aee228e514d'
3
+ metadata.gz: 1646e5fe0beaeafefcf2d60230a12ead391c9d75c771807332de7e06128198e4
4
+ data.tar.gz: a42d904e10ca321d8091263af986193332ac1507cdb2155e92373cf222faa864
5
5
  SHA512:
6
- metadata.gz: 57bf5e08937ea8fb4cee39e383a8f760e3a36714643c583408af6d02c7fbb84515af8d3089c753319677b8d5223c7a6a07f505c4356e21cd10570abbec358210
7
- data.tar.gz: 0ec4babf59251515b05857a459adfd7365b930feaf58b17ca2215ab4b534abe3ac41ae45e7643270ec8acba4c699fbcd311b12eb3ddab6789e5dd1b7275ad220
6
+ metadata.gz: 32495e09d3c742d1c8a1011df2c08a204a510766a0bc4fa5bf9f647fcba5c15fe691fc5ef7207f1caeb9c9caa0ff041948cc12f6e042261dd1b1d49e69c8f430
7
+ data.tar.gz: 28a0e9ff0cc67cfbe7f56f73ae660d68a1ca82243ac656a67cff4c9aefff506eaf0230edc7973de9d900cb1f63e4946f9eed8c39cd9a04de01861dbcf04cb6cf
data/CHANGELOG.md CHANGED
@@ -63,3 +63,8 @@ All notable changes to this project will be documented in this file.
63
63
  ### Changed
64
64
  - **RawBitmap Integration (Ruby/C++):** Introduced a native buffer class for direct contour extraction, bypassing PNG encoding to significantly reduce latency and memory overhead.
65
65
  - **Tiled Polygon Merger:** Added a spatial merger for `Finder` outputs to stitch polygons from discrete sub-areas (requiring only a 1px overlap). This multi-phase workflow supports coordinate translation and eliminates monolithic buffer requirements, optimizing the peak memory footprint.
66
+
67
+ ## [1.1.6] - 2026-03-21
68
+ ### Changed
69
+ - Added strict_bounds tracing mode: enables more accurate shape tracing by strictly adhering to pixel boundaries.
70
+ - Topological Consistency Fixes: improved the Topologically Consistent Merging algorithm to support progressive polygon tracing during sequential data streaming.
data/README.md CHANGED
@@ -9,6 +9,45 @@ Contrek (**con**tour **trek**king) simply scans your png/raw bitmap and returns
9
9
  In the following image all the non-white pixels have been examined and the result is the red polygon for the outer contour and the green one for the inner one
10
10
  ![alt text](contrek.png "Contour tracing")
11
11
 
12
+ ## 🚀 Why Contrek?
13
+
14
+ Contrek is a high-performance vectorization engine designed for massive datasets where memory efficiency and multi-core utilization are critical. Unlike traditional sequential contour tracers, Contrek is built on a **Stripe-Merging Architecture** that allows it to process images in independent segments and reconstruct the resulting polygons with perfect topological integrity.
15
+
16
+ ### 1. Parallel Processing Power
17
+ Contrek is designed to saturate all available CPU cores. By partitioning a single image into stripes, it can assign each segment to a different thread.
18
+ * **The Result:** On multi-core systems, Contrek achieves unmatched extraction speeds by parallelizing the workload.
19
+ * **Scalability:** Performance scales directly with your hardware, making it the ideal choice for high-throughput processing servers.
20
+
21
+ ### 2. Streamed Memory Management
22
+ Standard vectorization requires loading entire high-resolution images into RAM, which often leads to memory saturation.
23
+ * **The Solution:** Contrek enables **streamed processing** (see the C++ example in the repo). You can define a specific stripe height (buffer) and process the image incrementally, consuming only the memory allocated for that buffer.
24
+ * **The Benefit:** Vectorize massive, gigapixel-scale images or raw datasets directly from memory buffers. This allows for high-performance processing even on systems with limited memory that would otherwise fail.
25
+
26
+ ### 3. Intelligent "Stitching" Technology
27
+ The core strength of Contrek is its **Topologically Consistent Merging** algorithm, which maintains a deep understanding of polygon connectivity across stripe boundaries.
28
+ * Whether you are optimizing for **speed** (parallel cores) or **memory** (sequential streaming), the output is always a seamless, continuous set of vector data, free from gaps or duplicates at the seams.
29
+
30
+ <table>
31
+ <tr>
32
+ <td><img src="docs/images/stripes/whole_0.png" alt="Contour tracing stripe 0" width="50%"></td>
33
+ <td rowspan="4" align="center">
34
+ <img src="docs/images/stripes/whole.png" alt="Contour tracing stripe 0" width="50%">
35
+ </td>
36
+ </tr>
37
+ <tr>
38
+ <td><img src="docs/images/stripes/whole_256.png" alt="Contour tracing stripe 0" width="50%"></td>
39
+ </tr>
40
+ <tr>
41
+ <td><img src="docs/images/stripes/whole_512.png" alt="Contour tracing stripe 0" width="50%"></td>
42
+ </tr>
43
+ <tr>
44
+ <td><img src="docs/images/stripes/whole_768.png" alt="Contour tracing stripe 0" width="50%"></td>
45
+ </tr>
46
+ <tr><td colspan="2"><em><b>Left:</b> The image is split into 4 independent memory buffers (stripes).
47
+ <br><b>Right:</b> Contrek ensures <b>perfect topological continuity</b> during merging.</em>
48
+ <br><span style="color: #d32f2f;">■</span> <b>Red:</b> Outer contours |
49
+ <span style="color: #2e7d32;">■</span> <b>Dark Green:</b> Inner zones</td></tr>
50
+ </table>
12
51
 
13
52
  ## Prerequisites
14
53
 
@@ -180,6 +219,26 @@ result = Contrek.contour!(
180
219
  )
181
220
  ```
182
221
 
222
+ ## Tracing Modes
223
+
224
+ Contrek provides two levels of precision for polygon generation to balance data density and topological accuracy.
225
+
226
+ ### **Standard Mode** (Default)
227
+ Optimized for **speed** and **lightweight** output.
228
+ * **Behavior:** Traces outer boundaries while simplifying collinear segments.
229
+ * **Result:** Fewer vertices and smaller memory footprint.
230
+
231
+ ### **Strict Mode** (`strict_bounds: true`)
232
+ Engineered for **Pixel-Perfect** precision.
233
+ * **Behavior:** Preserves every junction and internal connectivity change.
234
+ * **Result:** 100% topologically faithful geometry with no micro-gaps between adjacent polygons.
235
+
236
+ Below are two images illustrating the difference in tracing modes. In the first case, with **strict_bounds ON**, the anti-clockwise sequence includes two additional points, **H** and **I**, which trace the shape more accurately. In the second case, the transition between **G** and **H** is approximated, omitting the indentation.
237
+ | Strict Bounds ON | Strict Bounds OFF |
238
+ |:---:|:---:|
239
+ | ![Originale](./docs/images/strict_bounds_on.png) | ![Poligoni](./docs/images/strict_bounds_off.png) |
240
+
241
+
183
242
  ## Result
184
243
 
185
244
  The result structure contains polygon coordinates and a set of metadata. Polygon coordinates can be accessed via:
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -24,6 +24,7 @@ void run_test() {
24
24
  // test_suite.test_f();
25
25
  // test_suite.test_g();
26
26
  // test_suite.test_h();
27
+ test_suite.test_i();
27
28
  std::cout << "compute time =" << cpu_timer.stop() << std::endl;
28
29
  }
29
30
 
@@ -14,12 +14,16 @@
14
14
  #include <map>
15
15
  #include <vector>
16
16
  #include <string>
17
+ #include <cstring>
18
+ #include <algorithm>
19
+ #include <cstdio>
17
20
  #include "polygon/finder/PolygonFinder.h"
18
21
  #include "polygon/finder/concurrent/ClippedPolygonFinder.h"
19
22
  #include "polygon/bitmaps/Bitmap.h"
20
23
  #include "polygon/bitmaps/FastPngBitmap.h"
21
24
  #include "polygon/bitmaps/RawBitmap.h"
22
25
  #include "polygon/bitmaps/RemoteFastPngBitmap.h"
26
+ #include "polygon/bitmaps/spng.h"
23
27
  #include "polygon/matchers/Matcher.h"
24
28
  #include "polygon/matchers/RGBMatcher.h"
25
29
  #include "polygon/matchers/RGBNotMatcher.h"
@@ -27,6 +31,7 @@
27
31
  #include "polygon/finder/optionparser.h"
28
32
  #include "polygon/finder/concurrent/Finder.h"
29
33
  #include "polygon/finder/concurrent/HorizontalMerger.h"
34
+ #include "polygon/finder/concurrent/VerticalMerger.h"
30
35
  #include "polygon/finder/concurrent/Sequence.h"
31
36
  #include "polygon/finder/concurrent/Position.h"
32
37
  #include "polygon/finder/Polygon.h"
@@ -46,6 +51,7 @@ void Tests::test_a()
46
51
  Bitmap b(chunk, 16);
47
52
  PolygonFinder pl(&b, &matcher, nullptr, &arguments);
48
53
  ProcessResult *o = pl.process_info();
54
+ // o->print_polygons();
49
55
  std::vector<int> outer_array{11, 1, 6, 2, 6, 3, 6, 4, 6, 5, 11, 5, 11, 4, 11, 3, 11, 2, 11, 1};
50
56
  std::vector<int> inner_array{7, 3, 10, 3, 10, 4, 7, 4};
51
57
  std::vector<int> array_compare;
@@ -253,3 +259,110 @@ void Tests::test_h()
253
259
  delete left_result;
254
260
  delete right_result;
255
261
  }
262
+
263
+ /* In this example, PNG data is read by streaming into a user-defined buffer height.
264
+ Contrek scans each stripe and extracts the polygons. Finally, it merges all
265
+ polygons from every stripe as if they had been read from a single image and saves
266
+ the result polygons on a png image.
267
+ This approach allows for processing large PNG files on systems where memory
268
+ would otherwise be insufficient.
269
+ */
270
+ void stream_png_image(const std::string& filepath, uint32_t stripe_height) {
271
+ std::vector<ProcessResult*> result_clones;
272
+ std::vector<std::string> varguments = {};
273
+ VerticalMerger vmerger(0, &varguments);
274
+
275
+ // opens image to stream
276
+ FILE* fp = fopen(filepath.c_str(), "rb");
277
+ if (!fp) {
278
+ std::cerr << "Unable open file: " << filepath << std::endl;
279
+ return;
280
+ }
281
+
282
+ // exams image
283
+ spng_ctx *ctx = spng_ctx_new(0);
284
+ spng_set_png_file(ctx, fp);
285
+ struct spng_ihdr ihdr;
286
+ if (spng_get_ihdr(ctx, &ihdr)) {
287
+ fclose(fp);
288
+ spng_ctx_free(ctx);
289
+ return;
290
+ }
291
+ uint32_t total_width = ihdr.width;
292
+ uint32_t total_height = ihdr.height;
293
+
294
+ // allocates stripe buffer
295
+ RawBitmap stripe_bitmap;
296
+ stripe_bitmap.define(total_width, stripe_height, 4, true);
297
+ RGBNotMatcher not_matcher(-1);
298
+
299
+ if (spng_decode_image(ctx, NULL, 0, SPNG_FMT_RGBA8, SPNG_DECODE_PROGRESSIVE)) {
300
+ fclose(fp);
301
+ spng_ctx_free(ctx);
302
+ return;
303
+ }
304
+
305
+ size_t row_size = static_cast<size_t>(total_width) * 4;
306
+ // main strpes loop
307
+ for (uint32_t current_y_offset = 0; current_y_offset < total_height; current_y_offset += stripe_height) {
308
+ int uncovered_height = total_height - current_y_offset;
309
+
310
+ // copy previous last line to the next new one (each contigue stripe must share one pixel scanline)
311
+ if (current_y_offset > 0) {
312
+ unsigned char* last_row_prev = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(stripe_height - 1));
313
+ unsigned char* first_row_curr = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(0));
314
+ std::memcpy(first_row_curr, last_row_prev, row_size);
315
+ }
316
+ // clears the part of the stripe wont be overwritten by png data
317
+ if (uncovered_height < stripe_height)
318
+ { stripe_bitmap.draw_filled_rectangle(0, 1, total_width, stripe_height - 1, 255, 255, 255);
319
+ }
320
+ // decoding data directly in the stripe buffer
321
+ uint32_t lines_to_read = std::min(stripe_height, total_height - current_y_offset);
322
+ for (uint32_t y = (current_y_offset == 0 ? 0 : 1); y < lines_to_read; y++) {
323
+ unsigned char* row_ptr = const_cast<unsigned char*>(stripe_bitmap.get_row_ptr(y));
324
+ int ret = spng_decode_row(ctx, row_ptr, row_size);
325
+ if (ret != 0 && ret != SPNG_EOI) break;
326
+ }
327
+ // stripe contour tracing
328
+ std::vector<std::string> finder_arguments = {
329
+ "--versus=a",
330
+ "--strict_bounds" // <- this option is strictly needed when working with vertical merger
331
+ };
332
+ PolygonFinder polygon_finder(&stripe_bitmap, &not_matcher, nullptr, &finder_arguments);
333
+ ProcessResult *result = polygon_finder.process_info();
334
+ if (result) {
335
+ std::cout << "Founds polygons: " << result->groups << std::endl;
336
+ ProcessResult* safe_result = result->clone();
337
+ result_clones.push_back(safe_result);
338
+ vmerger.add_tile(*safe_result);
339
+ delete result;
340
+ }
341
+ }
342
+
343
+ std::cout << "Merging polygons ..." << std::endl;
344
+ ProcessResult *merged_result = vmerger.process_info();
345
+ std::cout << "Founds total polygons: " << merged_result->groups << std::endl;
346
+
347
+ if (merged_result) {
348
+ RawBitmap full_bitmap;
349
+ full_bitmap.define(total_width, total_height, 4, true);
350
+ full_bitmap.fill(255, 255, 255);
351
+ merged_result->draw_on_bitmap(full_bitmap);
352
+ std::cout << "Saving whole png ..." << std::endl;
353
+ if (full_bitmap.save_to_png("whole.png")) {
354
+ std::cout << "Png saved!" << std::endl;
355
+ }
356
+ }
357
+ delete merged_result;
358
+ // frees memory
359
+ for (auto c : result_clones) {
360
+ delete c;
361
+ }
362
+ spng_ctx_free(ctx);
363
+ fclose(fp);
364
+ }
365
+
366
+ void Tests::test_i() {
367
+ stream_png_image("../images/graphs_1024x1024.png", 300);
368
+ }
@@ -19,4 +19,5 @@ class Tests {
19
19
  virtual void test_f();
20
20
  virtual void test_g();
21
21
  virtual void test_h();
22
+ virtual void test_i();
22
23
  };
@@ -8,6 +8,12 @@
8
8
  */
9
9
 
10
10
  #include <memory>
11
+ #include <cstdio>
12
+ #include <iostream>
13
+ #include <cstring>
14
+ #include <cerrno>
15
+ #include <string>
16
+ #include "spng.h"
11
17
  #include "RawBitmap.h"
12
18
 
13
19
  RawBitmap::RawBitmap() : Bitmap("", 0),
@@ -42,7 +48,7 @@ unsigned int RawBitmap::rgb_value_at(int x, int y) {
42
48
  }
43
49
 
44
50
  void RawBitmap::draw_pixel(int x, int y, unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
45
- if (x >= width || y >= height) return;
51
+ if (x < 0 || x >= width || y < 0 || y >= height) return;
46
52
  unsigned char* p = &image[(static_cast<size_t>(y) * width + x) * bpp];
47
53
  p[0] = r;
48
54
  if (bpp > 1) p[1] = g;
@@ -62,3 +68,96 @@ uint32_t RawBitmap::define(uint width, uint height, int bytes_per_pixel, bool cl
62
68
  return dimension;
63
69
  }
64
70
 
71
+ void RawBitmap::fill(unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
72
+ if (!image) return;
73
+ size_t size = static_cast<size_t>(width) * height * bpp;
74
+
75
+ if (r == g && g == b && b == a) {
76
+ std::memset(image.get(), r, size);
77
+ } else {
78
+ for (size_t i = 0; i < size; i += bpp) {
79
+ image[i] = r;
80
+ if (bpp > 1) image[i+1] = g;
81
+ if (bpp > 2) image[i+2] = b;
82
+ if (bpp > 3) image[i+3] = a;
83
+ }
84
+ }
85
+ }
86
+
87
+ void RawBitmap::draw_line(int x0, int y0, int x1, int y1, unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
88
+ int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
89
+ int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
90
+ int err = dx + dy, e2;
91
+ while (true) {
92
+ draw_pixel(x0, y0, r, g, b, a);
93
+ if (x0 == x1 && y0 == y1) break;
94
+ e2 = 2 * err;
95
+ if (e2 >= dy) {
96
+ err += dy; x0 += sx;
97
+ }
98
+ if (e2 <= dx) {
99
+ err += dx; y0 += sy;
100
+ }
101
+ }
102
+ }
103
+
104
+ void RawBitmap::draw_filled_rectangle(int x, int y, int w, int h, unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
105
+ if (w <= 0 || h <= 0) return;
106
+
107
+ for (int i = 0; i < h; ++i) {
108
+ int current_y = y + i;
109
+ this->draw_line(x, current_y, x + w - 1, current_y, r, g, b, a);
110
+ }
111
+ }
112
+
113
+ bool RawBitmap::save_to_png(const std::string& filename) {
114
+ if (this->width == 0 || this->height == 0 || !this->image) {
115
+ std::cerr << "Error: Bitmap empty or uninitialized" << std::endl;
116
+ return false;
117
+ }
118
+ FILE *fp = fopen(filename.c_str(), "wb");
119
+ if (!fp) {
120
+ std::cerr << "Unable open file" << filename << ": "
121
+ << std::strerror(errno) << std::endl;
122
+ return false;
123
+ }
124
+ spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER);
125
+ if (!ctx) {
126
+ fclose(fp);
127
+ return false;
128
+ }
129
+ spng_set_png_file(ctx, fp);
130
+ struct spng_ihdr ihdr;
131
+ std::memset(&ihdr, 0, sizeof(struct spng_ihdr));
132
+
133
+ ihdr.width = this->width;
134
+ ihdr.height = this->height;
135
+ ihdr.bit_depth = 8;
136
+
137
+ // color type by bpp
138
+ // 6 = RGBA (Truecolor with Alpha)
139
+ // 2 = RGB (Truecolor)
140
+ ihdr.color_type = (this->bpp == 4) ? 6 : 2;
141
+
142
+ int ret = spng_set_ihdr(ctx, &ihdr);
143
+ if (ret) {
144
+ std::cerr << "Error spng_set_ihdr: " << spng_strerror(ret) << std::endl;
145
+ spng_ctx_free(ctx);
146
+ fclose(fp);
147
+ return false;
148
+ }
149
+
150
+ // encoding
151
+ size_t image_size = static_cast<size_t>(this->width) * this->height * this->bpp;
152
+ ret = spng_encode_image(ctx, this->image.get(), image_size, SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE);
153
+ if (ret) {
154
+ std::cerr << "Encoding error: " << spng_strerror(ret) << " (code: " << ret << ")" << std::endl;
155
+ std::cerr << "Data W: " << width << " H: " << height << " BPP: " << bpp << " Size: " << image_size << std::endl;
156
+ }
157
+
158
+ spng_ctx_free(ctx);
159
+ fflush(fp);
160
+ fclose(fp);
161
+
162
+ return (ret == 0);
163
+ }
@@ -27,6 +27,10 @@ class RawBitmap : public Bitmap {
27
27
  int get_bytes_per_pixel() const;
28
28
  uint32_t define(uint width, uint height, int bytes_per_pixel, bool clear = false);
29
29
  void draw_pixel(int x, int y, unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255);
30
+ bool save_to_png(const std::string& filename);
31
+ void fill(unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255);
32
+ void draw_line(int x0, int y0, int x1, int y1, unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255);
33
+ void draw_filled_rectangle(int x, int y, int w, int h, unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255);
30
34
 
31
35
  protected:
32
36
  std::unique_ptr<unsigned char[]> image;
@@ -22,7 +22,9 @@ void FinderUtils::sanitize_options(pf_Options& options, std::vector<std::string>
22
22
  char** argv = &argv0[0];
23
23
  int argc = argv0.size() -1;
24
24
 
25
- enum optionIndex { COMPRESS_UNIQ, VERSUS, COMPRESS_VISVALINGAM, COMPRESS_LINEAR, NUMBER_OF_TILES, COMPRESS_VISVALINGAM_TOLERANCE, TREEMAP, NAMED_SEQUENCES, BOUNDS, CONNECTIVITY};
25
+ enum optionIndex { COMPRESS_UNIQ, VERSUS, COMPRESS_VISVALINGAM, COMPRESS_LINEAR, NUMBER_OF_TILES,
26
+ COMPRESS_VISVALINGAM_TOLERANCE, TREEMAP, NAMED_SEQUENCES, BOUNDS, CONNECTIVITY,
27
+ STRICT_BOUNDS };
26
28
  const option::Descriptor usage[] = {
27
29
  // {UNKNOWN, 0,"" , "" ,option::Arg::None, 0},
28
30
  {COMPRESS_VISVALINGAM, 0, "" , "compress_visvalingam", option::Arg::None, 0},
@@ -33,6 +35,7 @@ void FinderUtils::sanitize_options(pf_Options& options, std::vector<std::string>
33
35
  {TREEMAP, 0, "", "treemap", option::Arg::None, 0},
34
36
  {NAMED_SEQUENCES, 0, "", "named_sequences", option::Arg::None, 0},
35
37
  {BOUNDS, 0, "", "bounds", option::Arg::None, 0},
38
+ {STRICT_BOUNDS, 0, "", "strict_bounds", option::Arg::None, 0},
36
39
  {VERSUS, 0, "v", "versus", option::Arg::Optional, 0},
37
40
  {CONNECTIVITY, 0, "c", "connectivity", option::Arg::Optional, 0},
38
41
  {0, 0, 0, 0, 0, 0}
@@ -86,6 +89,9 @@ void FinderUtils::sanitize_options(pf_Options& options, std::vector<std::string>
86
89
  if (ioptions[BOUNDS].count() > 0)
87
90
  { options.bounds = true;
88
91
  }
92
+ if (ioptions[STRICT_BOUNDS].count() > 0)
93
+ { options.strict_bounds = true;
94
+ }
89
95
  // COMPRESS LINEAR
90
96
  if (ioptions[COMPRESS_LINEAR].count() > 0)
91
97
  { options.compress_linear = true;
@@ -18,6 +18,7 @@
18
18
  #include "NodeCluster.h"
19
19
  #include "Node.h"
20
20
  #include "RectBounds.h"
21
+ #include "PointPool.h"
21
22
  #include "../reducers/UniqReducer.h"
22
23
  #include "../reducers/LinearReducer.h"
23
24
  #include "../reducers/VisvalingamReducer.h"
@@ -243,7 +244,7 @@ std::pair<int, int> NodeCluster::test_in_hole_o(Node* node)
243
244
 
244
245
  void NodeCluster::plot_inner_node(std::vector<Point*>& sequence_coords, Node *node, int versus, Node *stop_at, Node *start_node) {
245
246
  Node *current_node = node;
246
-
247
+ bool strict_bounds = this->options->strict_bounds;
247
248
  while (current_node != nullptr) {
248
249
  current_node->outer_index = start_node->outer_index;
249
250
  current_node->inner_index = stop_at->inner_index;
@@ -271,7 +272,12 @@ void NodeCluster::plot_inner_node(std::vector<Point*>& sequence_coords, Node *no
271
272
  sequence_coords.push_back(next_node->coords_entering_to(current_node, versus, Node::INNER));
272
273
  }
273
274
  }
274
- }
275
+ } else if (strict_bounds) {
276
+ bool first_is_max = ((current_node->y > last_node->y) == (versus == Node::A));
277
+ sequence_coords.push_back(this->points_pool.acquire((first_is_max ? last_node->max_x : last_node->min_x), current_node->y));
278
+ sequence_coords.push_back(this->points_pool.acquire((first_is_max ? next_node->min_x : next_node->max_x), current_node->y));
279
+ }
280
+
275
281
  if (current_node->track_uncomplete()) {
276
282
  this->inner_new->push_back(current_node);
277
283
  } else {
@@ -284,6 +290,7 @@ void NodeCluster::plot_inner_node(std::vector<Point*>& sequence_coords, Node *no
284
290
 
285
291
  void NodeCluster::plot_node(std::vector<Point*>& sequence_coords, Node *node, Node *start_node, int versus, RectBounds& bounds) {
286
292
  Node *current_node = node;
293
+ bool strict_bounds = this->options->strict_bounds;
287
294
 
288
295
  while (current_node != nullptr) {
289
296
  root_nodes->remove(current_node);
@@ -315,7 +322,13 @@ void NodeCluster::plot_node(std::vector<Point*>& sequence_coords, Node *node, No
315
322
  inner_plot->contains(current_node) ? inner_plot->remove(current_node) : inner_plot->push_back(current_node);
316
323
  }
317
324
  }
318
- }
325
+ } else if (strict_bounds) {
326
+ bool is_down = current_node->y > last_node->y;
327
+ bool is_a = (versus == Node::A);
328
+ sequence_coords.push_back(this->points_pool.acquire((is_down == is_a ? last_node->min_x : last_node->max_x), current_node->y));
329
+ sequence_coords.push_back(this->points_pool.acquire((is_down == is_a ? next_node->max_x : next_node->min_x), current_node->y));
330
+ }
331
+
319
332
  if (current_node == start_node) {
320
333
  if (current_node->track_complete()) break;
321
334
  }
@@ -18,6 +18,7 @@
18
18
  #include "Lists.h"
19
19
  #include "RectBounds.h"
20
20
  #include "Polygon.h"
21
+ #include "PointPool.h"
21
22
 
22
23
  class Node;
23
24
  struct Point;
@@ -34,6 +35,7 @@ class NodeCluster {
34
35
  int count = 0;
35
36
  int nodes;
36
37
  int width;
38
+ PointPool points_pool;
37
39
 
38
40
  public:
39
41
  pf_Options *options;
@@ -0,0 +1,19 @@
1
+ /*
2
+ * PointPool.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 "PointPool.h"
11
+
12
+ Point* PointPool::acquire(int x, int y) {
13
+ storage.emplace_back(x, y);
14
+ return &storage.back();
15
+ }
16
+
17
+ void PointPool::clear() {
18
+ storage.clear();
19
+ }
@@ -0,0 +1,24 @@
1
+ /*
2
+ * PointPool.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 <deque>
12
+ #include <cstdint>
13
+ #include <string>
14
+ #include "Node.h"
15
+
16
+ class PointPool {
17
+ private:
18
+ std::deque<Point> storage;
19
+
20
+ public:
21
+ Point* acquire(int x, int y);
22
+ void clear();
23
+ size_t size() const { return storage.size(); }
24
+ };
@@ -17,6 +17,8 @@
17
17
  #include <utility>
18
18
  #include "PolygonFinder.h"
19
19
  #include "../bitmaps/Bitmap.h"
20
+ #include "../bitmaps/RawBitmap.h"
21
+
20
22
  #include "../matchers/Matcher.h"
21
23
  #include "../matchers/RGBMatcher.h"
22
24
  #include "../matchers/RGBNotMatcher.h"
@@ -124,3 +126,31 @@ ProcessResult* PolygonFinder::process_info() {
124
126
  else pr->named_sequence = "";
125
127
  return(pr);
126
128
  }
129
+
130
+ void ProcessResult::draw_on_bitmap(RawBitmap& bitmap) const {
131
+ for (const auto& poly : polygons) {
132
+ // --- OUTER ---
133
+ if (!poly.outer.empty()) {
134
+ for (size_t i = 0; i < poly.outer.size() - 1; ++i) {
135
+ Point* p1 = poly.outer[i];
136
+ Point* p2 = poly.outer[i+1];
137
+ bitmap.draw_line(p1->x, p1->y, p2->x, p2->y, 255, 0, 0, 255);
138
+ }
139
+ Point* last = poly.outer.back();
140
+ Point* first = poly.outer.front();
141
+ bitmap.draw_line(last->x, last->y, first->x, first->y, 255, 0, 0, 255);
142
+ }
143
+ // --- INNER ---
144
+ for (const auto& sequence : poly.inner) {
145
+ if (sequence.empty()) continue;
146
+ for (size_t i = 0; i < sequence.size() - 1; ++i) {
147
+ Point* p1 = sequence[i];
148
+ Point* p2 = sequence[i+1];
149
+ bitmap.draw_line(p1->x, p1->y, p2->x, p2->y, 0, 128, 0, 255);
150
+ }
151
+ Point* last = sequence.back();
152
+ Point* first = sequence.front();
153
+ bitmap.draw_line(last->x, last->y, first->x, first->y, 0, 128, 0, 255);
154
+ }
155
+ }
156
+ }
@@ -27,6 +27,7 @@ class Matcher;
27
27
  class RGBMatcher;
28
28
  class NodeCluster;
29
29
  class Node;
30
+ class RawBitmap;
30
31
  struct Point;
31
32
 
32
33
  struct ShapeLine {
@@ -41,6 +42,7 @@ struct pf_Options {
41
42
  bool compress_visvalingam = false;
42
43
  bool named_sequences = false;
43
44
  bool bounds = false;
45
+ bool strict_bounds = false;
44
46
  int connectivity_offset = 0;
45
47
  float compress_visvalingam_tolerance = 10.0;
46
48
  int number_of_tiles = 1;
@@ -48,6 +50,7 @@ struct pf_Options {
48
50
  return(versus == Node::A ? "a" : "o");
49
51
  }
50
52
  };
53
+
51
54
  struct ProcessResult {
52
55
  int groups;
53
56
  int width, height;
@@ -55,6 +58,7 @@ struct ProcessResult {
55
58
  std::list<Polygon> polygons;
56
59
  std::string named_sequence;
57
60
  std::vector<std::pair<int, int>> treemap;
61
+ void draw_on_bitmap(RawBitmap& canvas) const;
58
62
 
59
63
  void print_polygons() {
60
64
  int counter = 0;
@@ -67,7 +71,7 @@ struct ProcessResult {
67
71
  first = false;
68
72
  for (const Point* p : seq) std::cout << p->toString();
69
73
  }
70
- std::cout << "\n";
74
+ std::cout << "\n" << polygon.bounds.to_string() <<"\n";
71
75
  counter++;
72
76
  }
73
77
  }
@@ -89,6 +93,40 @@ struct ProcessResult {
89
93
  polygon.bounds.max_x += x;
90
94
  }
91
95
  }
96
+
97
+ ProcessResult* clone() const {
98
+ ProcessResult* new_res = new ProcessResult();
99
+ new_res->groups = this->groups;
100
+ new_res->width = this->width;
101
+ new_res->height = this->height;
102
+ new_res->benchmarks = this->benchmarks;
103
+ new_res->named_sequence = this->named_sequence;
104
+ new_res->treemap = this->treemap;
105
+
106
+ for (const auto& poly : this->polygons) {
107
+ Polygon new_poly;
108
+ // Bounds
109
+ new_poly.bounds = poly.bounds;
110
+ // outer
111
+ for (const Point* p : poly.outer) {
112
+ if (p) {
113
+ new_poly.outer.push_back(new Point(p->x, p->y));
114
+ }
115
+ }
116
+ // inner
117
+ for (const auto& seq : poly.inner) {
118
+ std::vector<Point*> new_seq;
119
+ for (const Point* p : seq) {
120
+ if (p) {
121
+ new_seq.push_back(new Point(p->x, p->y));
122
+ }
123
+ }
124
+ new_poly.inner.push_back(new_seq);
125
+ }
126
+ new_res->polygons.push_back(new_poly);
127
+ }
128
+ return new_res;
129
+ }
92
130
  };
93
131
 
94
132
  class PolygonFinder {
@@ -10,6 +10,9 @@
10
10
  #pragma once
11
11
  #include <limits>
12
12
  #include <algorithm>
13
+ #include <string>
14
+ #include <sstream>
15
+ #include <iostream>
13
16
 
14
17
  struct RectBounds {
15
18
  int min_x = std::numeric_limits<int>::max();
@@ -37,4 +40,13 @@ struct RectBounds {
37
40
  if (is_empty()) return 0;
38
41
  return(max_x - min_x);
39
42
  }
43
+
44
+ std::string to_string() const {
45
+ if (is_empty()) return "RectBounds: empty";
46
+ std::stringstream ss;
47
+ ss << "RectBounds ["
48
+ << "X(min=" << min_x << ",max=" << max_x << ") "
49
+ << "Y(min=" << min_y << ",max=" << max_y << ")]";
50
+ return ss.str();
51
+ }
40
52
  };
@@ -98,7 +98,7 @@ std::vector<EndPoint*> Part::remove_adjacent_pairs(const std::vector<EndPoint*>&
98
98
  }
99
99
 
100
100
  void Part::orient()
101
- { if (this->size <= 1) {
101
+ { if (this->size <= 1 || (this->size == 2 && this->inverts)) {
102
102
  this->versus_ = 0;
103
103
  } else {
104
104
  this->versus_ = (this->tail->payload->y - this->head->payload->y) > 0 ? 1 : -1;
@@ -22,8 +22,8 @@ void VerticalMerger::add_tile(ProcessResult& result)
22
22
  { transpose(result);
23
23
  if (this->tiles.size() > 0) {
24
24
  translate(result, this->current_x);
25
- adjust(result);
26
25
  }
26
+ adjust(result);
27
27
  Merger::add_tile(result);
28
28
  }
29
29
 
@@ -24,6 +24,8 @@
24
24
  #include "PolygonFinder/src/polygon/finder/List.h"
25
25
  #include "PolygonFinder/src/polygon/finder/Lists.cpp"
26
26
  #include "PolygonFinder/src/polygon/finder/Lists.h"
27
+ #include "PolygonFinder/src/polygon/finder/PointPool.h"
28
+ #include "PolygonFinder/src/polygon/finder/PointPool.cpp"
27
29
  #include "PolygonFinder/src/polygon/finder/Polygon.h"
28
30
  #include "PolygonFinder/src/polygon/bitmaps/Bitmap.h"
29
31
  #include "PolygonFinder/src/polygon/bitmaps/Bitmap.cpp"
@@ -219,7 +221,7 @@ class To_Ruby<ProcessResult*>
219
221
  return_me[Symbol("treemap")] = tmapout;
220
222
  rr->metadata = return_me;
221
223
 
222
- // Protects objects 'out' e 'return_me' linking them to the ruby instance preventing GC
224
+ // Protects objects 'out' and 'return_me' linking them to the ruby instance preventing GC
223
225
  // garbage collector to free them before the instance itself.
224
226
  Rice::Object ruby_obj = rb_result;
225
227
  ruby_obj.iv_set("@polygons_storage", out);
@@ -240,7 +242,6 @@ ProcessResult ruby_result_to_process_result(Rice::Object rb_result) {
240
242
 
241
243
  Rice::Array rb_polygons = rb_result.iv_get("@polygons_storage");
242
244
  for (size_t i = 0; i < rb_polygons.size(); ++i) {
243
- // Cast esplicito a Rice::Hash per evitare il Proxy
244
245
  Rice::Hash rb_poly = (Rice::Hash)rb_polygons[i];
245
246
  Polygon poly;
246
247
  // BOUNDS
@@ -371,9 +372,22 @@ void Init_cpp_polygon_finder() {
371
372
  define_class<VerticalMerger, Merger>("CPPVerticalMerger")
372
373
  .define_constructor(Constructor<VerticalMerger, int, std::vector<std::string>*>(), Arg("number_of_threads"), Arg("options") = nullptr, Arg("yield_gvl") = true);
373
374
 
374
- Data_Type<RubyResult> rb_cResult =
375
- define_class_under<RubyResult>(rb_cFinder, "Result")
375
+ Module mContrek = define_module("Contrek");
376
+ Module mCpp = define_module_under(mContrek, "Cpp");
377
+ Data_Type<RubyResult> rb_cResult = define_class_under<RubyResult>(mCpp, "CPPResult")
376
378
  .define_constructor(Constructor<RubyResult>())
377
379
  .define_method("polygons", [](RubyResult& rr) { return rr.polygons; })
378
- .define_method("metadata", [](RubyResult& rr) { return rr.metadata; });
380
+ .define_method("metadata", [](RubyResult& rr) { return rr.metadata; })
381
+ .define_method("polygons=", [](RubyResult& rr, Object value) {
382
+ rr.polygons = value;
383
+ Rice::Data_Object<RubyResult> self(&rr);
384
+ self.iv_set("@polygons_storage", value);
385
+ return value;
386
+ })
387
+ .define_method("metadata=", [](RubyResult& rr, Object value) {
388
+ rr.metadata = (Rice::Hash)value;
389
+ Rice::Data_Object<RubyResult> self(&rr);
390
+ self.iv_set("@metadata_storage", value);
391
+ return value;
392
+ });
379
393
  }
@@ -33,6 +33,16 @@ module Contrek
33
33
  ChunkyBitmap.new(@raw, @module)
34
34
  end
35
35
 
36
+ def draw_rect(x:, y:, width:, height:, color: "o", filled: true)
37
+ (y...(y + height)).each do |row|
38
+ (x...(x + width)).each do |col|
39
+ if filled || row == y || row == y + height - 1 || col == x || col == x + width - 1
40
+ value_set(col, row, color)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
36
46
  def draw_polygons(polygons)
37
47
  polygons.each do |polygon|
38
48
  [[:outer, "o"], [:inner, "i"]].each do |side, color|
@@ -47,16 +57,54 @@ module Contrek
47
57
  end
48
58
  end
49
59
 
50
- def to_terminal
60
+ def draw_numbered_polygons(polygons)
61
+ polygons.each do |polygon|
62
+ color = "A"
63
+ polygon[:outer].each_with_index do |position, index|
64
+ value_set(position[:x], position[:y], color)
65
+ color = next_color(color)
66
+ end
67
+ polygon[:inner].each do |sequence|
68
+ color = "a"
69
+ sequence.each_with_index do |position, index|
70
+ value_set(position[:x], position[:y], color)
71
+ color = next_color(color)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def to_terminal(label = nil)
78
+ puts label if label
51
79
  puts " " + (0...@module).map { |i| (i % 10).to_s }.join
52
80
  n = 0
53
81
  @raw.scan(/.{1,#{@module}}/).each do |line|
54
- puts n.to_s + " " + line
82
+ colored_line = line.chars.map { |c| colorize_char(c) }.join
83
+ puts "#{n} #{colored_line}"
55
84
  n += 1
56
85
  n = 0 if n >= 10
57
86
  end
58
87
  puts
59
88
  end
89
+
90
+ private
91
+
92
+ def next_color(color)
93
+ return "A" if color == "Z"
94
+ return "a" if color == "z"
95
+ color.next
96
+ end
97
+
98
+ def colorize_char(char)
99
+ case char
100
+ when "A".."Z"
101
+ "\e[91;1m#{char}\e[0m"
102
+ when "a".."z"
103
+ "\e[92;1m#{char}\e[0m"
104
+ else
105
+ char
106
+ end
107
+ end
60
108
  end
61
109
  end
62
110
  end
@@ -0,0 +1,42 @@
1
+ module Contrek
2
+ module Cpp
3
+ class CPPResult
4
+ def polygons=(list)
5
+ @polygons_storage = list
6
+ end
7
+
8
+ def metadata=(map)
9
+ @metadata_storage = {
10
+ width: 0,
11
+ height: 0,
12
+ benchmarks: {},
13
+ treemap: []
14
+ }.merge(map)
15
+ end
16
+
17
+ def points
18
+ raw_list = polygons.to_a
19
+ @to_points ||= raw_list.map do |polygon|
20
+ {outer: self.class.to_points(polygon[:outer]),
21
+ inner: polygon[:inner].map { |s| self.class.to_points(s) }}
22
+ end
23
+ end
24
+
25
+ def total_time
26
+ metadata[:benchmarks].values.sum
27
+ end
28
+
29
+ def self.to_points(flat_polygon)
30
+ flat_polygon.each_slice(2).map { |x, y| {x: x, y: y} }
31
+ end
32
+
33
+ def self.to_numpy(polygons_data)
34
+ polygons_data.map do |poly|
35
+ {outer: poly[:outer].flat_map { |p| [p[:x], p[:y]] },
36
+ inner: (poly[:inner] || []).map { |hole| hole.flat_map { |p| [p[:x], p[:y]] } },
37
+ bounds: poly[:bounds]}
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -69,10 +69,16 @@ module Contrek
69
69
  end
70
70
 
71
71
  def intersect_part?(other_part)
72
+ intersect = false
72
73
  other_part.each do |position|
73
- return true if position.end_point.queues.include?(self)
74
+ if position.end_point.queues.include?(self)
75
+ intersect = true
76
+ false
77
+ else
78
+ true
79
+ end
74
80
  end
75
- false
81
+ intersect
76
82
  end
77
83
 
78
84
  def to_endpoints
@@ -80,7 +86,7 @@ module Contrek
80
86
  end
81
87
 
82
88
  def orient!
83
- @versus = if size <= 1
89
+ @versus = if size <= 1 || (size == 2 && @inverts)
84
90
  0
85
91
  else
86
92
  (tail.payload[:y] - head.payload[:y]).positive? ? 1 : -1
@@ -5,8 +5,8 @@ module Contrek
5
5
  transpose(result)
6
6
  if @tiles.size > 0
7
7
  translate(result, @current_x)
8
- adjust(result)
9
8
  end
9
+ adjust(result)
10
10
  super
11
11
  end
12
12
 
@@ -26,7 +26,7 @@ module Contrek
26
26
  end
27
27
 
28
28
  def transpose(result)
29
- result.metadata_hash[:width], result.metadata_hash[:height] = result.metadata_hash[:height], result.metadata_hash[:width]
29
+ result.metadata[:width], result.metadata[:height] = result.metadata[:height], result.metadata[:width]
30
30
  result.polygons.each do |polygon|
31
31
  polygon[:outer].each { |p| p[:x], p[:y] = p[:y], p[:x] }
32
32
  polygon[:inner].each do |sequence|
@@ -73,7 +73,7 @@ module Contrek
73
73
  @sequence_coords << coord
74
74
  bounds.expand(x: coord[:x], y: coord[:y])
75
75
  end
76
- plot_node(next_node, root_node, bounds, versus) if @nodes > 0 && !next_node.nil?
76
+ plot_node(next_node, root_node, bounds, versus, @options[:strict_bounds]) if @nodes > 0 && !next_node.nil?
77
77
 
78
78
  draw_sequence(bitmap, "X") unless bitmap.nil?
79
79
  @polygons << {outer: @sequence_coords, inner: [], bounds: (bounds.to_h if @options[:bounds])}.compact if @sequence_coords.size >= 2
@@ -115,7 +115,7 @@ module Contrek
115
115
  @sequence_coords << next_node.coords_entering_to(first, inner_v, Contrek::Finder::Node::INNER)
116
116
  end
117
117
 
118
- plot_inner_node(next_node, inner_v, first, root_node) if !next_node.nil?
118
+ plot_inner_node(next_node, inner_v, first, root_node, options[:strict_bounds]) if !next_node.nil?
119
119
 
120
120
  draw_sequence(bitmap, "+") unless bitmap.nil?
121
121
 
@@ -189,7 +189,7 @@ module Contrek
189
189
  # inner way
190
190
  # nodes in @plot_sequence
191
191
  # coordinates in @sequence_coords
192
- def plot_inner_node(node, versus, stop_at, start_node)
192
+ def plot_inner_node(node, versus, stop_at, start_node, strict_bounds = false)
193
193
  node.outer_index = start_node.outer_index
194
194
  node.inner_index = stop_at.inner_index
195
195
  @root_nodes.delete(node)
@@ -210,6 +210,10 @@ module Contrek
210
210
  @sequence_coords << next_node.coords_entering_to(node, versus, Contrek::Finder::Node::INNER)
211
211
  end
212
212
  end
213
+ elsif strict_bounds
214
+ first_is_max = ((node.y > last_node.y) == (versus == :a))
215
+ @sequence_coords << {y: node.y, x: (first_is_max ? last_node.max_x : last_node.min_x)}
216
+ @sequence_coords << {y: node.y, x: (first_is_max ? next_node.min_x : next_node.max_x)}
213
217
  end
214
218
 
215
219
  if node.track_uncomplete
@@ -219,11 +223,11 @@ module Contrek
219
223
  end
220
224
 
221
225
  return if next_node == stop_at
222
- plot_inner_node(next_node, versus, stop_at, start_node)
226
+ plot_inner_node(next_node, versus, stop_at, start_node, strict_bounds)
223
227
  end
224
228
 
225
229
  # contour tracing core logic loop
226
- def plot_node(node, start_node, bounds, versus = :a)
230
+ def plot_node(node, start_node, bounds, versus = :a, strict_bounds = false)
227
231
  @root_nodes.delete(node)
228
232
 
229
233
  node.outer_index = start_node.outer_index
@@ -252,13 +256,18 @@ module Contrek
252
256
  @inner_plot.contains(node) ? @inner_plot.delete(node) : @inner_plot << node
253
257
  end
254
258
  end
259
+ elsif strict_bounds
260
+ is_down = node.y > last_node.y
261
+ is_a = (versus == :a)
262
+ @sequence_coords << {y: node.y, x: ((is_down == is_a) ? last_node.min_x : last_node.max_x)}
263
+ @sequence_coords << {y: node.y, x: ((is_down == is_a) ? next_node.max_x : next_node.min_x)}
255
264
  end
256
265
  # exit if root_node
257
266
 
258
267
  if node == start_node
259
268
  return if node.track_complete
260
269
  end
261
- plot_node(next_node, start_node, bounds, versus)
270
+ plot_node(next_node, start_node, bounds, versus, strict_bounds)
262
271
  end
263
272
 
264
273
  def add_node(node, offset)
@@ -4,7 +4,7 @@ module Contrek
4
4
  module Finder
5
5
  class PolygonFinder
6
6
  def initialize(bitmap, matcher, test_bitmap = nil, options = {})
7
- @options = {versus: :a}.merge(options)
7
+ @options = {versus: :a, strict_bounds: false}.merge(options)
8
8
  sanitize_options
9
9
  @source_bitmap = bitmap
10
10
  @matcher = matcher
@@ -1,13 +1,9 @@
1
1
  module Contrek
2
2
  module Finder
3
- Result = Struct.new(:polygons, :metadata_hash) do
3
+ Result = Struct.new(:polygons, :metadata) do
4
4
  def points
5
5
  polygons
6
6
  end
7
-
8
- def metadata
9
- metadata_hash
10
- end
11
7
  end
12
8
  end
13
9
  end
@@ -1,3 +1,3 @@
1
1
  module Contrek
2
- VERSION = "1.1.5"
2
+ VERSION = "1.1.6"
3
3
  end
data/lib/contrek.rb CHANGED
@@ -49,7 +49,7 @@ require "contrek/cpp/cpp_concurrent_finder"
49
49
  require "contrek/cpp/cpp_concurrent_merger"
50
50
  require "contrek/cpp/cpp_concurrent_horizontal_merger"
51
51
  require "contrek/cpp/cpp_concurrent_vertical_merger"
52
- require "contrek/results/cpp_result"
52
+ require "contrek/cpp/cpp_result"
53
53
 
54
54
  module Contrek
55
55
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrek
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 1.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-08 00:00:00.000000000 Z
11
+ date: 2026-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -141,9 +141,17 @@ files:
141
141
  - Rakefile
142
142
  - contrek.gemspec
143
143
  - contrek.png
144
+ - docs/images/strict_bounds_off.png
145
+ - docs/images/strict_bounds_on.png
146
+ - docs/images/stripes/whole.png
147
+ - docs/images/stripes/whole_0.png
148
+ - docs/images/stripes/whole_256.png
149
+ - docs/images/stripes/whole_512.png
150
+ - docs/images/stripes/whole_768.png
144
151
  - ext/cpp_polygon_finder/PolygonFinder/CMakeLists.txt
145
152
  - ext/cpp_polygon_finder/PolygonFinder/LICENSE_AGPL.txt
146
153
  - ext/cpp_polygon_finder/PolygonFinder/examples/example.cpp
154
+ - ext/cpp_polygon_finder/PolygonFinder/images/graphs_1024x1024.png
147
155
  - ext/cpp_polygon_finder/PolygonFinder/images/labyrinth.png
148
156
  - ext/cpp_polygon_finder/PolygonFinder/images/sample_10240x10240.png
149
157
  - ext/cpp_polygon_finder/PolygonFinder/src/ContrekApi.h
@@ -170,6 +178,8 @@ files:
170
178
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.h
171
179
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.cpp
172
180
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.h
181
+ - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PointPool.cpp
182
+ - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PointPool.h
173
183
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Polygon.h
174
184
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.cpp
175
185
  - ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h
@@ -246,6 +256,7 @@ files:
246
256
  - lib/contrek/cpp/cpp_concurrent_horizontal_merger.rb
247
257
  - lib/contrek/cpp/cpp_concurrent_merger.rb
248
258
  - lib/contrek/cpp/cpp_concurrent_vertical_merger.rb
259
+ - lib/contrek/cpp/cpp_result.rb
249
260
  - lib/contrek/finder/bounds.rb
250
261
  - lib/contrek/finder/concurrent/clipped_polygon_finder.rb
251
262
  - lib/contrek/finder/concurrent/cluster.rb
@@ -283,7 +294,6 @@ files:
283
294
  - lib/contrek/reducers/reducer.rb
284
295
  - lib/contrek/reducers/uniq_reducer.rb
285
296
  - lib/contrek/reducers/visvalingam_reducer.rb
286
- - lib/contrek/results/cpp_result.rb
287
297
  - lib/contrek/version.rb
288
298
  homepage: https://github.com/runout77/contrek
289
299
  licenses:
@@ -1,21 +0,0 @@
1
- module Contrek
2
- module Results
3
- class CPPFinder::Result
4
- def points
5
- raw_list = polygons.to_a
6
- @to_points ||= raw_list.map do |polygon|
7
- {outer: self.class.to_points(polygon[:outer]),
8
- inner: polygon[:inner].map { |s| self.class.to_points(s) }}
9
- end
10
- end
11
-
12
- def total_time
13
- metadata[:benchmarks].values.sum
14
- end
15
-
16
- def self.to_points(flat_polygon)
17
- flat_polygon.each_slice(2).map { |x, y| {x: x, y: y} }
18
- end
19
- end
20
- end
21
- end