contrek 1.3.7 → 1.3.8
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/CHANGELOG.md +3 -0
- data/README.md +509 -195
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.cpp +9 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.h +1 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.cpp +7 -8
- data/lib/contrek/finder/node.rb +11 -0
- data/lib/contrek/finder/node_cluster.rb +5 -9
- data/lib/contrek/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68bd09217262e7b3495d05110af5a6b9c0c97bd493be54138706e3ec298847a7
|
|
4
|
+
data.tar.gz: a5dbd685e38170bc830fbbbf1e17c1725eaca367c53c229e6d233564a12337a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6179fce1a4e54c1fbcc14a25939ff769d74f9804ea6bc09f84fac511f0b3f7be435817e7467f2a2556d215ddc4aec36edd0e4ea2b4791ba01d09e8f9d748c3b
|
|
7
|
+
data.tar.gz: e0694addda28d20ca8d57108555a902b18ad0fb1765662ebad035c7637b47ae1d55fe6e87715e05d21a8793cfbb7a9a86156ef13f0333e9c2fc316eef6f379b5
|
data/CHANGELOG.md
CHANGED
|
@@ -144,3 +144,6 @@ All notable changes to this project will be documented in this file.
|
|
|
144
144
|
- Added polygon compression to the progressive streaming pipeline (StreamingMerger).
|
|
145
145
|
- Added strict validation in progressive streaming. Results passed to add_tile() are now validated to ensure they contain compatible settings (such as versus conventions and compression options).
|
|
146
146
|
- Added unsafe_mode flag to streaming mergers. This allows bypassing compatibility checks when using custom or arbitrary input data.
|
|
147
|
+
|
|
148
|
+
## [1.3.8] - 2026-08-08
|
|
149
|
+
- Fixed issue found on treemap determination.
|
data/README.md
CHANGED
|
@@ -1,31 +1,60 @@
|
|
|
1
1
|
# Contrek
|
|
2
|
-
**Contrek** is a standalone <u>[C++17 topology-preserving streaming polygonization engine](#-c-standalone-library-usage)</u> for raster-to-vector conversion. It extracts polygonal contours from PNG images and raw memory streams using programmable pixel matchers, representing shapes as connected graphs of shared vertices to guarantee topological consistency. Its streaming architecture supports gigapixel-scale datasets, multithreaded execution, and incremental vector serialization to multiple formats such as SVG while keeping memory consumption bounded. A fully featured Ruby wrapper is also available, exposing the complete C++ API through an idiomatic Ruby interface.
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
**Contrek** is a standalone **C++17 contour tracing and polygonization library** for raster images.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
It was originally developed to solve a practical problem: extracting polygons from very large raster images without loading the entire image into memory. Over time the implementation evolved into a reusable library that can process PNG images as well as raw memory buffers, while preserving polygon topology during the entire tracing process.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
The engine is based on programmable pixel matchers, allowing the caller to decide which pixels belong to the regions being traced. Polygon coordinates can be streamed as they are produced, making the library suitable for datasets that would otherwise require a large amount of RAM.
|
|
8
|
+
|
|
9
|
+
Although the tracing engine is written in C++, Contrek is also distributed as a Ruby gem exposing almost the complete native API through an idiomatic Ruby interface.
|
|
10
|
+
|
|
11
|
+
## About Contrek
|
|
12
|
+
|
|
13
|
+
**Contrek** (**CON**tour **TREK**king) at its simplest, the library scans a bitmap and returns the contours of all regions matching a user-defined criterion. Each region is represented by one outer polygon and, when necessary, one or more inner polygons describing holes.
|
|
14
|
+
|
|
15
|
+
Besides extracting contours, Contrek can also determine the nesting relationship between polygons and build a tree describing which polygons contain others. This is useful when reconstructing the topology of complex shapes.
|
|
16
|
+
|
|
17
|
+
Several coordinate-compression algorithms are available to reduce polygon size after tracing. The library can also split the work across multiple threads by processing independent vertical stripes and merging the results afterwards into a single topologically consistent representation.
|
|
18
|
+
|
|
19
|
+
The image below shows a simple example. Every non-white pixel is considered part of the traced region. The resulting outer contour is shown in red, while inner contours are shown in green.
|
|
9
20
|
|
|
10
21
|

|
|
11
22
|
|
|
12
|
-
## 🚀 Why Contrek?
|
|
13
23
|
|
|
14
|
-
|
|
24
|
+
# Why Contrek?
|
|
25
|
+
|
|
26
|
+
Most contour tracing libraries process an image sequentially. That approach is perfectly adequate for many workloads, but it becomes less practical once images become very large or when memory usage starts to matter.
|
|
27
|
+
|
|
28
|
+
Contrek follows a different strategy.
|
|
29
|
+
|
|
30
|
+
Instead of processing the whole image at once, it can split it into independent vertical stripes. Each stripe is traced separately and the resulting polygons are merged afterwards. Most of the implementation complexity lies in this merge phase, whose purpose is to reconstruct polygons crossing stripe boundaries without breaking their topology.
|
|
31
|
+
|
|
32
|
+
The same design makes it possible to use the library in two different ways:
|
|
33
|
+
|
|
34
|
+
- **Parallel processing**, where several stripes are traced simultaneously on different CPU cores.
|
|
35
|
+
- **Streaming processing**, where only a small portion of the image is kept in memory at any given time.
|
|
36
|
+
|
|
37
|
+
The tracing algorithm itself is identical in both cases; only the execution strategy changes.
|
|
38
|
+
|
|
39
|
+
## Parallel execution
|
|
15
40
|
|
|
16
|
-
|
|
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.
|
|
41
|
+
When multiple CPU cores are available, independent stripes can be processed concurrently. The actual speedup depends on the image content and on how much work is required during the merge stage, but large datasets generally benefit from additional cores.
|
|
20
42
|
|
|
21
|
-
|
|
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.
|
|
43
|
+
This approach allows Contrek to make effective use of modern multicore processors without changing the resulting geometry.
|
|
25
44
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
## Streaming large datasets
|
|
46
|
+
|
|
47
|
+
Loading an entire gigapixel image into memory is often unnecessary.
|
|
48
|
+
|
|
49
|
+
Contrek can instead process the image incrementally by reading one stripe at a time. Only the current working buffer needs to be allocated, making memory consumption predictable regardless of the final image height.
|
|
50
|
+
|
|
51
|
+
This execution mode is particularly useful when processing images that are too large to fit comfortably into RAM.
|
|
52
|
+
|
|
53
|
+
## Merging polygons
|
|
54
|
+
|
|
55
|
+
Whenever a polygon crosses the boundary between two adjacent stripes, the library reconstructs it during the merge phase.
|
|
56
|
+
|
|
57
|
+
This reconstruction preserves polygon connectivity across stripe boundaries, so the final output is equivalent to tracing the entire image in a single pass.
|
|
29
58
|
|
|
30
59
|
<table>
|
|
31
60
|
<tr>
|
|
@@ -49,47 +78,67 @@ The core strength of Contrek is its **Topologically Consistent Merging** algorit
|
|
|
49
78
|
</tr>
|
|
50
79
|
</table>
|
|
51
80
|
|
|
52
|
-
##
|
|
53
|
-
|
|
81
|
+
## Benchmarking
|
|
82
|
+
|
|
83
|
+
Contrek has been benchmarked mainly against OpenCV, which is probably the most widely used library for contour extraction.
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
85
|
+
The goal was not simply to compare execution times, but also to verify that the reconstructed polygons remain topologically correct after stripe merging, even when processing very large images.
|
|
86
|
+
|
|
87
|
+
The benchmark suite measures three aspects:
|
|
88
|
+
|
|
89
|
+
- execution time;
|
|
90
|
+
- memory usage;
|
|
91
|
+
- consistency of the generated polygons.
|
|
92
|
+
|
|
93
|
+
The benchmark source code and the datasets used for testing are available here:
|
|
59
94
|
|
|
60
|
-
The complete testing suite, source code, and raw benchmarks are available here:
|
|
61
95
|
👉 **[test_contrek](https://github.com/runout77/test_contrek)**
|
|
62
96
|
|
|
97
|
+
|
|
63
98
|
## Prerequisites
|
|
64
99
|
|
|
65
|
-
For
|
|
100
|
+
For small and medium-sized images no special configuration is required.
|
|
101
|
+
|
|
102
|
+
If you expect to process very large images (roughly 20k pixels or more), installing **tcmalloc** is recommended. It generally provides more stable memory behaviour than the default allocator during long-running processing.
|
|
103
|
+
|
|
104
|
+
### Ubuntu / Debian
|
|
66
105
|
|
|
67
|
-
**Ubuntu / Debian:**
|
|
68
106
|
```bash
|
|
69
107
|
sudo apt-get install libgoogle-perftools-dev
|
|
70
108
|
```
|
|
71
|
-
> For advanced performance tuning (zlib-ng, tcmalloc, thread configuration) see [PERFORMANCE.md](PERFORMANCE.md).
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
> Windows is not supported due to the use of POSIX threading primitives and platform-specific
|
|
75
|
-
> memory management. On Windows, consider using WSL2 (Windows Subsystem for Linux).
|
|
110
|
+
For additional tuning options (zlib-ng, thread configuration, memory allocator and other performance-related settings), see **[PERFORMANCE.md](PERFORMANCE.md)**.
|
|
76
111
|
|
|
77
|
-
|
|
112
|
+
> **Platform support**
|
|
113
|
+
>
|
|
114
|
+
> Native extensions are currently supported on **Linux** and **macOS**.
|
|
115
|
+
>
|
|
116
|
+
> Windows is not supported because the implementation relies on POSIX threading primitives and platform-specific memory management. Running under **WSL2** is currently the recommended approach for Windows users.
|
|
78
117
|
|
|
79
|
-
|
|
118
|
+
|
|
119
|
+
# Installation
|
|
120
|
+
|
|
121
|
+
Add the gem to your application's Gemfile:
|
|
80
122
|
|
|
81
123
|
```ruby
|
|
82
|
-
gem
|
|
124
|
+
gem "contrek"
|
|
83
125
|
```
|
|
84
126
|
|
|
85
|
-
|
|
127
|
+
and install it normally:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bundle install
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
During installation the native C++ extension is compiled automatically.
|
|
134
|
+
|
|
135
|
+
Once installed, the C++ implementation is used by default. A pure Ruby implementation is also included and can be selected explicitly whenever native extensions are not desired.
|
|
86
136
|
|
|
87
|
-
|
|
137
|
+
# Usage
|
|
88
138
|
|
|
89
|
-
|
|
139
|
+
The simplest way to use Contrek is through the `Contrek.contour!` helper.
|
|
90
140
|
|
|
91
|
-
|
|
92
|
-
In this example we are asking to examine any pixel that does not have the red color.
|
|
141
|
+
In the example below, every pixel except pure red is considered part of the region to be traced.
|
|
93
142
|
|
|
94
143
|
```ruby
|
|
95
144
|
result = Contrek.contour!(
|
|
@@ -101,65 +150,200 @@ result = Contrek.contour!(
|
|
|
101
150
|
}
|
|
102
151
|
)
|
|
103
152
|
```
|
|
104
|
-
The resulting metadata information contains the execution times (microseconds), the count of polygons found and the nesting tree map. You can access polygons coordinates too (see later).
|
|
105
153
|
|
|
106
|
-
|
|
107
|
-
|
|
154
|
+
The returned object contains both the extracted polygons and some metadata describing the execution.
|
|
155
|
+
|
|
156
|
+
Metadata includes timing information (expressed in milliseconds), the number of detected polygon groups and, if requested, the nesting tree.
|
|
108
157
|
|
|
158
|
+
```ruby
|
|
159
|
+
{
|
|
160
|
+
:benchmarks=>{
|
|
161
|
+
"build_tangs_sequence"=>0.129,
|
|
162
|
+
"compress"=>0.037,
|
|
163
|
+
"plot"=>0.198,
|
|
164
|
+
"scan"=>0.114,
|
|
165
|
+
"total"=>0.478
|
|
166
|
+
},
|
|
167
|
+
:groups=>2,
|
|
168
|
+
:named_sequence=>"",
|
|
169
|
+
:treemap=>[]
|
|
170
|
+
}
|
|
109
171
|
```
|
|
110
172
|
|
|
111
|
-
|
|
173
|
+
The actual polygon coordinates are discussed later in the **Result** section.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
## Pure Ruby implementation
|
|
177
|
+
|
|
178
|
+
By default Contrek uses the native C++ implementation.
|
|
179
|
+
|
|
180
|
+
A pure Ruby version is included mainly for portability, debugging and environments where compiling native extensions is not possible.
|
|
181
|
+
|
|
182
|
+
It exposes the same public API, although execution is considerably slower.
|
|
183
|
+
|
|
112
184
|
```ruby
|
|
113
185
|
result = Contrek.contour!(
|
|
114
186
|
png_file_path: "labyrinth3.png",
|
|
115
187
|
options: {
|
|
116
|
-
native: false,
|
|
188
|
+
native: false,
|
|
117
189
|
class: "value_not_matcher",
|
|
118
190
|
color: {r: 241, g: 156, b: 156, a: 255}
|
|
119
191
|
}
|
|
192
|
+
)
|
|
193
|
+
```
|
|
120
194
|
|
|
195
|
+
## Performance Native vs Pure
|
|
196
|
+
|
|
197
|
+
One of the largest examples included in the test suite processes the image `sample_1200x800.png`.
|
|
198
|
+
|
|
199
|
+
Pure Ruby:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
{
|
|
203
|
+
scan: 775.435,
|
|
204
|
+
build_tangs_sequence: 38.916,
|
|
205
|
+
plot: 101.876,
|
|
206
|
+
compress: 0.002,
|
|
207
|
+
total: 916.229
|
|
208
|
+
}
|
|
121
209
|
```
|
|
122
210
|
|
|
123
|
-
|
|
211
|
+
Native C++:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
{
|
|
215
|
+
scan: 5.077,
|
|
216
|
+
build_tangs_sequence: 0.697,
|
|
217
|
+
plot: 2.004,
|
|
218
|
+
compress: 0.000,
|
|
219
|
+
total: 7.781
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
On the reference machine used for development (AMD Ryzen 7 3700X, Ubuntu, 64 GB RAM), the native implementation is roughly 130× faster for this workload.
|
|
224
|
+
|
|
225
|
+
As always with benchmarks, these numbers should be considered indicative rather than absolute. Actual performance depends on image content, compression settings and hardware.
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
## Working with the C++ API
|
|
229
|
+
|
|
230
|
+
The helper method is convenient for most applications, but it is also possible to access the native classes directly.
|
|
231
|
+
|
|
232
|
+
This gives more control over bitmap creation, matcher selection and polygon finder configuration.
|
|
124
233
|
|
|
125
234
|
```ruby
|
|
126
235
|
png_bitmap = CPPPngBitMap.new("labyrinth3.png")
|
|
127
|
-
|
|
128
|
-
|
|
236
|
+
|
|
237
|
+
rgb_matcher = CPPRGBNotMatcher.new(
|
|
238
|
+
png_bitmap.rgb_value_at(0, 0)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
polygonfinder = CPPPolygonFinder.new(
|
|
242
|
+
png_bitmap,
|
|
129
243
|
rgb_matcher,
|
|
130
244
|
nil,
|
|
131
|
-
{
|
|
245
|
+
{
|
|
246
|
+
versus: :a,
|
|
247
|
+
compress: {
|
|
248
|
+
visvalingam: true,
|
|
249
|
+
visvalingam_tolerance: 1.5
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
|
|
132
254
|
result = polygonfinder.process_info
|
|
133
|
-
|
|
134
|
-
Contrek::Bitmaps::Painting.direct_draw_polygons(
|
|
135
|
-
|
|
255
|
+
|
|
256
|
+
Contrek::Bitmaps::Painting.direct_draw_polygons(
|
|
257
|
+
result.points,
|
|
258
|
+
png_image
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
png_image.save("result.png")
|
|
136
262
|
```
|
|
137
263
|
|
|
138
|
-
|
|
264
|
+
Using the low-level API is useful when building custom processing pipelines or when more control over the tracing process is required.
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
## Reading PNG images from Base64
|
|
268
|
+
|
|
269
|
+
When image data is already available as a Base64 string there is no need to write it to disk first.
|
|
270
|
+
|
|
139
271
|
```ruby
|
|
140
|
-
png_bitmap = CPPRemotePngBitMap.new(
|
|
272
|
+
png_bitmap = CPPRemotePngBitMap.new(
|
|
273
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P+/HgAFhAJ/wlseKgAAAABJRU5ErkJggg=="
|
|
274
|
+
)
|
|
141
275
|
```
|
|
142
276
|
|
|
143
|
-
|
|
277
|
+
|
|
278
|
+
## Processing raw memory buffers
|
|
279
|
+
|
|
280
|
+
Contrek is not limited to PNG files.
|
|
281
|
+
|
|
282
|
+
Images can also be created directly in memory, making the library suitable for integration with image-processing pipelines that never touch the filesystem.
|
|
283
|
+
|
|
284
|
+
The following example creates a small bitmap, draws a rectangle and extracts its contour.
|
|
285
|
+
|
|
144
286
|
```ruby
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
end
|
|
287
|
+
raw_bitmap = CPPRawBitMap.new
|
|
288
|
+
|
|
289
|
+
raw_bitmap.define(20,30,4,true)
|
|
290
|
+
|
|
291
|
+
4.upto(5) do |y|
|
|
292
|
+
5.upto(8) do |x|
|
|
293
|
+
raw_bitmap.draw_pixel(x, y, 1, 0, 0, 0)
|
|
153
294
|
end
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
not_matcher = CPPRGBNotMatcher.new(
|
|
298
|
+
raw_bitmap.rgb_value_at(0,0)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
result = CPPPolygonFinder.new(
|
|
302
|
+
raw_bitmap,
|
|
303
|
+
not_matcher,
|
|
304
|
+
nil,
|
|
305
|
+
{
|
|
306
|
+
compress:{
|
|
307
|
+
uniq:true,
|
|
308
|
+
linear:true
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
).process_info
|
|
312
|
+
|
|
313
|
+
puts result.points.inspect
|
|
159
314
|
```
|
|
160
315
|
|
|
161
|
-
|
|
162
|
-
|
|
316
|
+
which produces
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
[
|
|
320
|
+
{
|
|
321
|
+
:outer=>[
|
|
322
|
+
{x:5,y:4},
|
|
323
|
+
{x:5,y:6},
|
|
324
|
+
{x:9,y:6},
|
|
325
|
+
{x:9,y:4}
|
|
326
|
+
],
|
|
327
|
+
:inner=>[]
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
This interface is particularly useful when image data comes from another application, a network stream or an in-memory processing stage rather than from a PNG file.
|
|
333
|
+
|
|
334
|
+
# Multithreading
|
|
335
|
+
|
|
336
|
+
Both the native C++ implementation and the pure Ruby implementation expose the same multithreading interface.
|
|
337
|
+
|
|
338
|
+
There is, however, an important difference between the two.
|
|
339
|
+
|
|
340
|
+
The native implementation performs contour extraction in parallel and can take advantage of all available CPU cores.
|
|
341
|
+
|
|
342
|
+
The pure Ruby implementation, on the other hand, is limited by MRI's Global Interpreter Lock (GIL). Although multiple threads can be created, they do not execute Ruby code simultaneously, so CPU-bound workloads remain effectively sequential.
|
|
343
|
+
|
|
344
|
+
Alternative runtimes such as JRuby or TruffleRuby do not have this limitation, although they have not been tested with Contrek.
|
|
345
|
+
|
|
346
|
+
A simple multithreaded example:
|
|
163
347
|
|
|
164
348
|
```ruby
|
|
165
349
|
result = Contrek.contour!(
|
|
@@ -169,21 +353,60 @@ result = Contrek.contour!(
|
|
|
169
353
|
native: false,
|
|
170
354
|
class: "value_not_matcher",
|
|
171
355
|
color: {r: 255, g: 255, b: 255, a: 255},
|
|
172
|
-
finder: {
|
|
356
|
+
finder: {
|
|
357
|
+
number_of_tiles: 2,
|
|
358
|
+
compress: {
|
|
359
|
+
uniq: true,
|
|
360
|
+
linear: true
|
|
361
|
+
}
|
|
362
|
+
}
|
|
173
363
|
}
|
|
174
364
|
)
|
|
175
365
|
```
|
|
176
|
-
|
|
366
|
+
|
|
367
|
+
In this example the image is divided into two vertical stripes and each stripe is assigned to a worker thread.
|
|
368
|
+
|
|
369
|
+
Instead of specifying the number of threads explicitly, you can also let Contrek choose a suitable value automatically.
|
|
177
370
|
|
|
178
371
|
```ruby
|
|
179
|
-
|
|
372
|
+
number_of_threads: nil
|
|
180
373
|
```
|
|
181
374
|
|
|
182
|
-
|
|
375
|
+
The native implementation uses the number of available CPU cores reported by the operating system.
|
|
183
376
|
|
|
184
|
-
|
|
377
|
+
## A note about determinism
|
|
185
378
|
|
|
186
|
-
|
|
379
|
+
The tracing stage itself is deterministic.
|
|
380
|
+
|
|
381
|
+
The merge stage is not.
|
|
382
|
+
|
|
383
|
+
Each stripe is processed independently and adjacent stripes are merged as soon as they become available. Depending on thread scheduling, the merge order may differ from one execution to another.
|
|
384
|
+
|
|
385
|
+
For example, given three stripes:
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
B1 B2 B3
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
one execution may merge them like this:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
(B1 + B2) + B3
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
while another execution may produce:
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
B1 + (B2 + B3)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Both executions generate equivalent polygons, but the order of intermediate merge operations is differentso the final coordinate sequence is not guaranteed to be byte-for-byte identical across executions.
|
|
404
|
+
|
|
405
|
+
## Native execution
|
|
406
|
+
|
|
407
|
+
When the `native` option is omitted, Contrek automatically uses the C++ implementation.
|
|
408
|
+
|
|
409
|
+
For example, the following configuration processes a 105 MP image using four worker threads and four processing tiles.
|
|
187
410
|
|
|
188
411
|
```ruby
|
|
189
412
|
result = Contrek.contour!(
|
|
@@ -192,135 +415,205 @@ result = Contrek.contour!(
|
|
|
192
415
|
number_of_threads: 4,
|
|
193
416
|
class: "value_not_matcher",
|
|
194
417
|
color: {r: 255, g: 255, b: 255, a: 255},
|
|
195
|
-
finder: {
|
|
418
|
+
finder: {
|
|
419
|
+
number_of_tiles: 4,
|
|
420
|
+
compress: {
|
|
421
|
+
uniq: true
|
|
422
|
+
}
|
|
423
|
+
}
|
|
196
424
|
}
|
|
197
425
|
)
|
|
198
|
-
puts result.metadata[:benchmarks].inspect
|
|
199
426
|
|
|
200
|
-
|
|
427
|
+
puts result.metadata[:benchmarks]
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Example timings:
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
{
|
|
434
|
+
compress: 5.3933,
|
|
201
435
|
init: 322.749,
|
|
202
436
|
inner: 4.795,
|
|
203
437
|
outer: 81.546,
|
|
204
438
|
total: 328.142
|
|
205
439
|
}
|
|
206
|
-
|
|
207
440
|
```
|
|
208
441
|
|
|
209
|
-
|
|
442
|
+
The reported timings include tracing, polygon reconstruction and coordinate compression.
|
|
443
|
+
|
|
444
|
+
# Tracking model
|
|
210
445
|
|
|
211
|
-
Contrek
|
|
446
|
+
Contrek works on a cell-based representation. Each pixel is treated as a square with four explicit vertices. During tracing, polygons are built by walking along the edges separating matching and non-matching cells.
|
|
212
447
|
|
|
448
|
+
This representation makes it easier to preserve topology and simplifies the reconstruction of polygons when different image stripes are merged together.
|
|
213
449
|
|
|
214
|
-
### Pixel
|
|
450
|
+
### Pixel geometry
|
|
215
451
|
|
|
216
452
|
```text
|
|
217
453
|
(x, y) Top Edge (x+1, y)
|
|
218
454
|
O--------------------------------O
|
|
219
455
|
| |
|
|
220
456
|
| |
|
|
221
|
-
|
|
222
|
-
|
|
457
|
+
Left | PIXEL | Right
|
|
458
|
+
Edge | (x, y) | Edge
|
|
223
459
|
| |
|
|
224
460
|
| |
|
|
225
461
|
O--------------------------------O
|
|
226
|
-
(x, y+1)
|
|
227
|
-
|
|
462
|
+
(x, y+1) Bottom Edge (x+1, y+1)
|
|
228
463
|
```
|
|
229
464
|
|
|
230
|
-
### Connectivity Modes
|
|
231
465
|
|
|
232
|
-
|
|
466
|
+
## Connectivity
|
|
467
|
+
|
|
468
|
+
Neighbouring pixels can be connected using either **4-connectivity** or **8-connectivity**.
|
|
233
469
|
|
|
234
|
-
|
|
235
|
-
| :--- | :--- | :--- | :--- |
|
|
236
|
-
| `4` | **Orthogonal** | Default | Only pixels sharing an edge (N, S, E, W) are connected. |
|
|
237
|
-
| `8` | **Omnidirectional** | Extended | Includes diagonal neighbors (sharing a corner). |
|
|
470
|
+
With **4-connectivity**, only pixels sharing an edge belong to the same region.
|
|
238
471
|
|
|
472
|
+
With **8-connectivity**, diagonal neighbours are also considered connected.
|
|
239
473
|
|
|
240
|
-
|
|
474
|
+
| Value | Description |
|
|
475
|
+
|------:|-------------|
|
|
476
|
+
| `4` | Orthogonal connectivity (default) |
|
|
477
|
+
| `8` | Includes diagonal neighbours |
|
|
478
|
+
|
|
479
|
+
To enable 8-connectivity:
|
|
241
480
|
|
|
242
481
|
```ruby
|
|
243
482
|
result = Contrek.contour!(
|
|
244
|
-
png_file_path: "
|
|
483
|
+
png_file_path: "...",
|
|
245
484
|
options: {
|
|
246
|
-
number_of_threads: 4,
|
|
247
|
-
class: "value_not_matcher",
|
|
248
|
-
color: {r: 255, g: 255, b: 255, a: 255},
|
|
249
485
|
finder: {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
compress: {uniq: true}}
|
|
486
|
+
connectivity: 8
|
|
487
|
+
}
|
|
253
488
|
}
|
|
254
489
|
)
|
|
255
490
|
```
|
|
256
491
|
|
|
257
|
-
|
|
492
|
+
The choice mainly affects how diagonally touching regions are interpreted.
|
|
493
|
+
|
|
494
|
+
# Results
|
|
495
|
+
|
|
496
|
+
The tracing result contains two kinds of information:
|
|
258
497
|
|
|
259
|
-
|
|
498
|
+
- the extracted polygons;
|
|
499
|
+
- metadata describing how they were produced.
|
|
500
|
+
|
|
501
|
+
Polygon coordinates are available through
|
|
260
502
|
|
|
261
503
|
```ruby
|
|
262
504
|
result.polygons
|
|
263
|
-
|
|
264
|
-
[{:outer=>[11, 2, 11, 5, 6, 5, 6, 2],
|
|
265
|
-
:inner=>[[10, 3, 7, 3, 7, 4, 10, 4]]}]
|
|
266
505
|
```
|
|
267
506
|
|
|
268
|
-
|
|
507
|
+
The native implementation stores coordinates as an interleaved array
|
|
269
508
|
|
|
270
|
-
```
|
|
509
|
+
```text
|
|
271
510
|
[x0, y0, x1, y1, ...]
|
|
272
511
|
```
|
|
273
512
|
|
|
274
|
-
|
|
513
|
+
This representation is compact and avoids allocating thousands of small point objects.
|
|
514
|
+
|
|
515
|
+
If you prefer a more Ruby-friendly representation you can use
|
|
275
516
|
|
|
276
517
|
```ruby
|
|
277
518
|
result.points
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
which converts the same data into hashes containing `x` and `y`.
|
|
278
522
|
|
|
279
|
-
|
|
280
|
-
|
|
523
|
+
```ruby
|
|
524
|
+
[
|
|
525
|
+
{
|
|
526
|
+
outer: [
|
|
527
|
+
{x:11,y:2},
|
|
528
|
+
{x:11,y:5},
|
|
529
|
+
{x:6,y:5},
|
|
530
|
+
{x:6,y:2}
|
|
531
|
+
],
|
|
532
|
+
inner: [
|
|
533
|
+
[
|
|
534
|
+
{x:10,y:3},
|
|
535
|
+
{x:7,y:3},
|
|
536
|
+
{x:7,y:4},
|
|
537
|
+
{x:10,y:4}
|
|
538
|
+
]
|
|
539
|
+
]
|
|
540
|
+
}
|
|
541
|
+
]
|
|
281
542
|
```
|
|
282
543
|
|
|
283
|
-
|
|
284
|
-
|
|
544
|
+
The conversion is performed only on the Ruby side. Internally, the native implementation continues to use the compact coordinate array.
|
|
545
|
+
|
|
546
|
+
For pure Ruby (`native: false`) both methods return the same point-based representation.
|
|
285
547
|
|
|
286
|
-
For pure Ruby implementations (`{ native: false }`), coordinates are always expressed as points.
|
|
287
|
-
In this case, both `polygons` and `points` return the same data, represented as hashes with `x` and `y` keys.
|
|
288
548
|
|
|
289
|
-
|
|
549
|
+
## Bounding boxes
|
|
550
|
+
|
|
551
|
+
If bounding boxes are required they can be generated during tracing instead of computing them afterwards.
|
|
552
|
+
|
|
553
|
+
Enable them with
|
|
554
|
+
|
|
290
555
|
```ruby
|
|
291
|
-
{
|
|
556
|
+
{
|
|
557
|
+
bounds: true
|
|
558
|
+
}
|
|
292
559
|
```
|
|
293
|
-
|
|
560
|
+
|
|
561
|
+
Each polygon will then include an additional `bounds` entry.
|
|
562
|
+
|
|
294
563
|
```ruby
|
|
295
|
-
|
|
296
|
-
:
|
|
297
|
-
|
|
564
|
+
{
|
|
565
|
+
bounds:{
|
|
566
|
+
min_x:3,
|
|
567
|
+
max_x:11,
|
|
568
|
+
min_y:1,
|
|
569
|
+
max_y:3
|
|
570
|
+
},
|
|
571
|
+
outer:[...],
|
|
572
|
+
inner:[...]
|
|
573
|
+
}
|
|
298
574
|
```
|
|
299
575
|
|
|
576
|
+
Since bounds are computed while polygons are being built, requesting them has almost no additional cost.
|
|
577
|
+
|
|
300
578
|
|
|
301
|
-
|
|
579
|
+
# Metadata
|
|
302
580
|
|
|
303
|
-
|
|
581
|
+
Execution metadata is available through
|
|
304
582
|
|
|
305
583
|
```ruby
|
|
306
584
|
result.metadata
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Typical information includes
|
|
307
588
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
589
|
+
- execution times;
|
|
590
|
+
- tracing options;
|
|
591
|
+
- number of polygon groups;
|
|
592
|
+
- nesting information.
|
|
593
|
+
|
|
594
|
+
```ruby
|
|
595
|
+
{
|
|
596
|
+
benchmarks:{...},
|
|
597
|
+
groups:1,
|
|
598
|
+
treemap:[],
|
|
599
|
+
options:{...}
|
|
317
600
|
}
|
|
318
601
|
```
|
|
319
|
-
The options are the original options passed by user.
|
|
320
602
|
|
|
321
|
-
|
|
603
|
+
The content of `options` is simply the configuration originally passed to the tracing engine.
|
|
604
|
+
|
|
322
605
|
|
|
323
|
-
|
|
606
|
+
# Treemap
|
|
607
|
+
|
|
608
|
+
Besides returning polygon coordinates, Contrek can also describe how polygons are nested inside one another.
|
|
609
|
+
|
|
610
|
+
This information is stored in the **treemap**, which records the parent-child relationship between every detected polygon.
|
|
611
|
+
|
|
612
|
+
Rather than rebuilding the hierarchy afterwards through geometric containment tests, the information is collected during tracing, making it immediately available once processing completes.
|
|
613
|
+
|
|
614
|
+
Each entry in the treemap refers to the corresponding polygon returned by `result.polygons`.
|
|
615
|
+
|
|
616
|
+
For example consider the above image traced clockwise (o).
|
|
324
617
|
|
|
325
618
|
```ruby
|
|
326
619
|
"AAAAAAAAAAAAAAAAAAAAAA" \
|
|
@@ -339,19 +632,18 @@ The treemap is a data structure that represents the containment hierarchy of pol
|
|
|
339
632
|
```ruby
|
|
340
633
|
result.metadata[:treemap]
|
|
341
634
|
|
|
342
|
-
[
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
635
|
+
[
|
|
636
|
+
[-1, -1], # A
|
|
637
|
+
[0, 0], # B
|
|
638
|
+
[1, 1], # C
|
|
639
|
+
[1, 0] # D
|
|
640
|
+
]
|
|
346
641
|
```
|
|
347
642
|
|
|
348
|
-
|
|
349
|
-
The order matches the one provided in `result.polygons`.
|
|
350
|
-
|
|
351
|
-
Each entry has the structure:
|
|
643
|
+
Each element has the form
|
|
352
644
|
|
|
353
|
-
```
|
|
354
|
-
[
|
|
645
|
+
```text
|
|
646
|
+
[parent_polygon_index, parent_inner_sequence_index]
|
|
355
647
|
```
|
|
356
648
|
|
|
357
649
|
- Polygon **A** (index `0`) has no parent and is represented as `[-1, -1]`
|
|
@@ -388,11 +680,14 @@ result.metadata[:treemap]
|
|
|
388
680
|
C, D and E will get the same pair (**[1, 0]**) because are all placed inside the first (0) inner sequence of B.
|
|
389
681
|
|
|
390
682
|
|
|
391
|
-
|
|
683
|
+
# How stripe merging works
|
|
392
684
|
|
|
393
|
-
The
|
|
685
|
+
The contour tracing itself is relatively straightforward.
|
|
686
|
+
|
|
687
|
+
The more challenging part is reconstructing polygons that cross the boundary between two independently processed image stripes.
|
|
688
|
+
|
|
689
|
+
This reconstruction step is what allows Contrek to support both streaming and parallel execution without changing the resulting geometry.
|
|
394
690
|
|
|
395
|
-
The input image is divided into multiple vertical bands—at least two—where each band shares a one-pixel-wide vertical strip with its adjacent bands.
|
|
396
691
|
For example, if the image is 20 pixels wide and split into two bands, the first band may span from x=0 to x=9 (10 pixels), and the second from x=9 to x=19 (11 pixels).
|
|
397
692
|
The vertical column at x=9 is therefore the shared region.
|
|
398
693
|
|
|
@@ -401,62 +696,74 @@ The vertical column at x=9 is therefore the shared region.
|
|
|
401
696
|
---------*----------
|
|
402
697
|
```
|
|
403
698
|
|
|
404
|
-
Each
|
|
405
|
-
Whenever two adjacent bands have completed processing, they are assigned to an available thread that performs the merge of their coordinates.
|
|
699
|
+
Each stripe is processed independently, exactly as if it were a separate image.
|
|
406
700
|
|
|
407
|
-
|
|
408
|
-
B1 – B2 – B3
|
|
409
|
-
the merging sequence might be:
|
|
701
|
+
The shared column guarantees that contours intersecting the boundary are visible from both sides.
|
|
410
702
|
|
|
411
|
-
|
|
412
|
-
or
|
|
703
|
+
Once tracing has completed, adjacent stripes are merged.
|
|
413
704
|
|
|
414
|
-
|
|
705
|
+
The merge algorithm identifies polygons that continue across the shared boundary, splits them into matching segments and reconnects them into a single continuous contour.
|
|
415
706
|
|
|
416
|
-
The
|
|
707
|
+
The same operation is then repeated recursively until only one stripe remains.
|
|
417
708
|
|
|
418
|
-
|
|
419
|
-
The first stage merges the outer boundary; the second stage merges the disconnected inner parts, which are inserted where needed into the outer sequences produced in the first stage.
|
|
709
|
+
In other words, Contrek never attempts to trace the whole image at once.
|
|
420
710
|
|
|
421
|
-
|
|
711
|
+
Instead, it repeatedly combines smaller topologically consistent pieces into a larger one.
|
|
712
|
+
|
|
713
|
+
This strategy makes it possible to process images whose full size would otherwise exceed the available memory.
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
## Merge order
|
|
717
|
+
|
|
718
|
+
The merge order is intentionally left to the thread scheduler.
|
|
719
|
+
|
|
720
|
+
For three stripes, both of the following execution orders are valid:
|
|
422
721
|
|
|
423
|
-
## Performances native vs pure
|
|
424
|
-
One of the most complex test you can find under the spec folder is named "scans poly 1200x800", scans this [image](spec/files/images/sample_1200x800.png) computing coordinates to draw polygons drawn in this [result](spec/files/stored_samples/sample_1200x800.png).
|
|
425
|
-
On pure ruby implementation kept time
|
|
426
|
-
```ruby
|
|
427
|
-
{ :scan=>775.435,
|
|
428
|
-
:build_tangs_sequence=>38.916,
|
|
429
|
-
:plot=>101.876,
|
|
430
|
-
:compress=>0.002,
|
|
431
|
-
:total=>916.229
|
|
432
|
-
}
|
|
433
722
|
```
|
|
434
|
-
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
compress: 0.00071,
|
|
440
|
-
total: 7.780602
|
|
441
|
-
}
|
|
723
|
+
(B1 + B2) + B3
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
or
|
|
727
|
+
|
|
442
728
|
```
|
|
729
|
+
B1 + (B2 + B3)
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Although the intermediate merge sequence changes, the reconstructed polygons remain geometrically equivalent.
|
|
733
|
+
|
|
734
|
+
The exact ordering of coordinates may differ because polygon simplification is applied after merging.
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
## Polygon reconstruction
|
|
738
|
+
|
|
739
|
+
Internally, polygons crossing a stripe boundary are temporarily divided into smaller pieces.
|
|
443
740
|
|
|
444
|
-
|
|
741
|
+
The merge stage reconnects these pieces by matching their endpoints inside the shared region.
|
|
445
742
|
|
|
446
|
-
|
|
743
|
+
Outer boundaries are reconstructed first.
|
|
447
744
|
|
|
448
|
-
|
|
745
|
+
Inner boundaries (holes) are processed afterwards and inserted back into their corresponding outer polygon.
|
|
449
746
|
|
|
450
|
-
|
|
747
|
+
This separation considerably simplifies the reconstruction logic while preserving the topology of the original image.
|
|
451
748
|
|
|
749
|
+
Once every stripe has been merged into a single dataset, the optional coordinate-compression algorithms are applied.
|
|
452
750
|
|
|
453
|
-
|
|
454
|
-
* **CMake** (3.10+)
|
|
455
|
-
* **ZLIB** (Required for PNG processing)
|
|
456
|
-
* **C++17 Compiler**
|
|
751
|
+
At this point the resulting polygons are identical to those that would have been obtained by processing the image in a single pass, while requiring substantially less memory.
|
|
457
752
|
|
|
458
|
-
|
|
459
|
-
|
|
753
|
+
|
|
754
|
+
# Standalone C++ library
|
|
755
|
+
|
|
756
|
+
Although Contrek is distributed as a Ruby gem, the tracing engine itself is a standalone C++17 library.
|
|
757
|
+
|
|
758
|
+
The C++ code has no dependency on Ruby and can be integrated into other projects directly.
|
|
759
|
+
|
|
760
|
+
## Requirements
|
|
761
|
+
|
|
762
|
+
- CMake 3.10 or newer
|
|
763
|
+
- A C++17 compiler
|
|
764
|
+
- ZLIB (required for PNG support)
|
|
765
|
+
|
|
766
|
+
## Building the examples
|
|
460
767
|
|
|
461
768
|
```bash
|
|
462
769
|
# Navigate to the core folder
|
|
@@ -473,12 +780,12 @@ make
|
|
|
473
780
|
./contrek_test
|
|
474
781
|
```
|
|
475
782
|
|
|
476
|
-
|
|
783
|
+
The examples are a good starting point for understanding the native API and evaluating the library without involving Ruby.
|
|
477
784
|
|
|
478
|
-
To use Contrek's engine in your own C++ application without Ruby:
|
|
479
785
|
|
|
480
|
-
|
|
481
|
-
|
|
786
|
+
## Integrating the library
|
|
787
|
+
|
|
788
|
+
The easiest way to embed Contrek into an existing C++ project is to copy the `PolygonFinder` directory into your source tree and add it as a CMake subdirectory.
|
|
482
789
|
|
|
483
790
|
```cmake
|
|
484
791
|
# Tell CMake to include Contrek
|
|
@@ -489,9 +796,12 @@ add_executable(my_app main.cpp)
|
|
|
489
796
|
target_link_libraries(my_app PRIVATE ContrekLib)
|
|
490
797
|
```
|
|
491
798
|
|
|
492
|
-
|
|
799
|
+
No Ruby components are required.
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
## Basic example
|
|
493
803
|
|
|
494
|
-
|
|
804
|
+
A minimal program looks like this:
|
|
495
805
|
|
|
496
806
|
```cpp
|
|
497
807
|
#include "ContrekApi.h"
|
|
@@ -515,7 +825,10 @@ int main() {
|
|
|
515
825
|
}
|
|
516
826
|
```
|
|
517
827
|
|
|
518
|
-
|
|
828
|
+
The API intentionally stays close to the concepts used by the Ruby wrapper, making it easy to switch between the two interfaces.
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
# License
|
|
519
832
|
|
|
520
833
|
Contrek uses a dual-license model:
|
|
521
834
|
|
|
@@ -524,11 +837,12 @@ Contrek uses a dual-license model:
|
|
|
524
837
|
|
|
525
838
|
See [LICENSE.md](LICENSE.md) for full details.
|
|
526
839
|
|
|
527
|
-
|
|
840
|
+
|
|
841
|
+
# Changelog
|
|
528
842
|
|
|
529
843
|
See [CHANGELOG.md](CHANGELOG.md) for a complete list of changes.
|
|
530
844
|
|
|
531
|
-
|
|
845
|
+
# History
|
|
532
846
|
The algorithm was originally developed by me in 2018 when I was commissioned to create a Rails web application whose main objective was to census buildings from GoogleMAPS; the end user had to be able to select their home building by clicking its roof on the map which had to be identified as a clickable polygon. The solution was to configure GoogleMAPS to render buildings of a well-defined color (red), and at each refresh of the same to transform the div into an image (html2canvas) then process it server side returning the polygons to be superimposed again on the map. This required very fast polygons determination. Searching for a library for tracing the contours I was not able to find anything better except OpenCV which however seemed to me a very heavy dependency. So I decided to write my algorithm directly in the context of the ROR application. Once perfected, it was already usable but a bit slow in the higher image resolutions. So I decided to write the counterpart in C++, which came out much faster and which I then used as an extension on Ruby by means of Rice.
|
|
533
847
|
|
|
534
848
|
I thought that the algorithm had excellent qualities but I never had the time to develop it further. A few months ago I decided to publish it as a gem, freely usable. The gem includes the C++ extension. There is also a [Rails 7.1 demo project](https://github.com/runout77/contrek_rails) that uses the gem and proposes the same scheme I described above. Starting from a GoogleMAPS map, the server receives the image and returns the outlines to be drawn again on the same. It is a great way to test an applicative use of this gem. Enjoy!.
|
|
@@ -132,3 +132,12 @@ Node* Node::get_tangent_node_by_virtual_index(int virtual_index) {
|
|
|
132
132
|
return &(this->cluster->vert_nodes[y + T_DOWN][virtual_index]);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
void Node::assign_lateral_inner_index(Node* my_prev_node, Node* end_node, int versus) {
|
|
137
|
+
bool first_is_max = ((this->y > my_prev_node->y) == (versus == Node::A));
|
|
138
|
+
if (first_is_max) {
|
|
139
|
+
this->inner_right_index = end_node->inner_index;
|
|
140
|
+
} else {
|
|
141
|
+
this->inner_left_index = end_node->inner_index;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -113,6 +113,7 @@ class Node : public Listable {
|
|
|
113
113
|
int min_x, max_x;
|
|
114
114
|
Node(int min_x, int max_x, int y, NodeCluster* cluster, char name);
|
|
115
115
|
void precalc_tangs_sequences(NodeCluster& cluster);
|
|
116
|
+
void assign_lateral_inner_index(Node* my_prev_node, Node* end_node, int versus);
|
|
116
117
|
bool processed = false;
|
|
117
118
|
char name;
|
|
118
119
|
int inner_left_index = -1;
|
|
@@ -292,13 +292,6 @@ Node* NodeCluster::plot_inner_node(std::vector<Point>& sequence_coords, Node *no
|
|
|
292
292
|
Node *next_node = current_node->my_next_inner(last_node, versus);
|
|
293
293
|
plot_sequence.push_back(current_node);
|
|
294
294
|
|
|
295
|
-
bool first_is_max = ((current_node->y > last_node->y) == (versus == Node::A));
|
|
296
|
-
if (first_is_max) {
|
|
297
|
-
if (current_node->inner_right_index == -1) current_node->inner_right_index = stop_at->inner_index;
|
|
298
|
-
} else {
|
|
299
|
-
if (current_node->inner_left_index == -1) current_node->inner_left_index = stop_at->inner_index;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
295
|
bool plot = true;
|
|
303
296
|
if (next_node->y == last_node->y) {
|
|
304
297
|
Node *n;
|
|
@@ -309,6 +302,7 @@ Node* NodeCluster::plot_inner_node(std::vector<Point>& sequence_coords, Node *no
|
|
|
309
302
|
plot = (n == next_node);
|
|
310
303
|
}
|
|
311
304
|
if (plot) {
|
|
305
|
+
current_node->assign_lateral_inner_index(last_node, stop_at, versus);
|
|
312
306
|
Point first_point = last_node->coords_entering_to(current_node, versus_inverter[versus], Node::INNER);
|
|
313
307
|
if (sequence_coords.back() != first_point) {
|
|
314
308
|
sequence_coords.push_back(first_point);
|
|
@@ -338,7 +332,12 @@ Node* NodeCluster::plot_inner_node(std::vector<Point>& sequence_coords, Node *no
|
|
|
338
332
|
} else {
|
|
339
333
|
inner_new->remove(current_node);
|
|
340
334
|
}
|
|
341
|
-
if (next_node == stop_at)
|
|
335
|
+
if (next_node == stop_at) {
|
|
336
|
+
if (plot) {
|
|
337
|
+
next_node->assign_lateral_inner_index(current_node, stop_at, versus);
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
342
341
|
current_node = next_node;
|
|
343
342
|
}
|
|
344
343
|
return current_node;
|
data/lib/contrek/finder/node.rb
CHANGED
|
@@ -105,6 +105,17 @@ module Contrek
|
|
|
105
105
|
get_tangent_node_by_virtual_index(@tangs_sequence.at(last_node_index))
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
def assign_lateral_inner_index(my_prev_node, end_node, versus)
|
|
109
|
+
is_moving_up = y > my_prev_node.y
|
|
110
|
+
is_forward = (versus == :a)
|
|
111
|
+
first_is_max = (is_moving_up == is_forward)
|
|
112
|
+
if first_is_max
|
|
113
|
+
self.inner_right_index = end_node.inner_index
|
|
114
|
+
else
|
|
115
|
+
self.inner_left_index = end_node.inner_index
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
108
119
|
def coords_entering_to(enter_to, enter_mode, tracking)
|
|
109
120
|
enter_to_index = if enter_to.y < y
|
|
110
121
|
enter_to.abs_x_index + @up_indexer
|
|
@@ -233,13 +233,6 @@ module Contrek
|
|
|
233
233
|
next_node = node.my_next(last_node, versus, :inner)
|
|
234
234
|
@plot_sequence << node
|
|
235
235
|
|
|
236
|
-
first_is_max = ((node.y > last_node.y) == (versus == :a))
|
|
237
|
-
if first_is_max
|
|
238
|
-
node.inner_right_index = stop_at.inner_index if node.inner_right_index == -1
|
|
239
|
-
elsif node.inner_left_index == -1
|
|
240
|
-
node.inner_left_index = stop_at.inner_index
|
|
241
|
-
end
|
|
242
|
-
|
|
243
236
|
plot = true
|
|
244
237
|
if next_node.y == last_node.y
|
|
245
238
|
virtual_index = node.tangs_sequence.send((versus == :a) ? :first : :last)
|
|
@@ -247,6 +240,7 @@ module Contrek
|
|
|
247
240
|
end
|
|
248
241
|
|
|
249
242
|
if plot
|
|
243
|
+
node.assign_lateral_inner_index(last_node, stop_at, versus)
|
|
250
244
|
first_point = last_node.coords_entering_to(node, VERSUS_INVERTER[versus], Contrek::Finder::Node::INNER)
|
|
251
245
|
@sequence_coords << first_point if @sequence_coords.last != first_point
|
|
252
246
|
if next_node.y == last_node.y
|
|
@@ -271,8 +265,10 @@ module Contrek
|
|
|
271
265
|
else
|
|
272
266
|
@inner_new.delete(node)
|
|
273
267
|
end
|
|
274
|
-
|
|
275
|
-
|
|
268
|
+
if next_node == stop_at
|
|
269
|
+
next_node.assign_lateral_inner_index(node, stop_at, versus) if plot
|
|
270
|
+
return node
|
|
271
|
+
end
|
|
276
272
|
plot_inner_node(next_node, versus, stop_at, start_node)
|
|
277
273
|
end
|
|
278
274
|
|
data/lib/contrek/version.rb
CHANGED
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.3.
|
|
4
|
+
version: 1.3.8
|
|
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-
|
|
11
|
+
date: 2026-08-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|