contrek 1.4.6 → 1.4.7

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 +3 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +23 -0
  5. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/FinderUtils.cpp +1 -0
  6. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.cpp +23 -26
  7. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Node.h +2 -2
  8. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.cpp +7 -4
  9. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/NodeCluster.h +1 -1
  10. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/Polygon.h +1 -0
  11. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.cpp +24 -11
  12. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h +66 -0
  13. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cluster.cpp +1 -1
  14. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Cursor.cpp +8 -2
  15. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Finder.cpp +3 -0
  16. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/GeoJsonStreamingMerger.h +3 -5
  17. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.cpp +3 -2
  18. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Polyline.h +3 -1
  19. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Shape.cpp +1 -0
  20. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.cpp +2 -2
  21. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/ShapePool.h +1 -1
  22. data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Tile.cpp +1 -1
  23. data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +9 -3
  24. data/lib/contrek/cpp/cpp_geo_json_concurrent_streaming_merger.rb +2 -2
  25. data/lib/contrek/cpp/cpp_result.rb +2 -1
  26. data/lib/contrek/finder/concurrent/cluster.rb +1 -1
  27. data/lib/contrek/finder/concurrent/cursor.rb +3 -1
  28. data/lib/contrek/finder/concurrent/finder.rb +2 -1
  29. data/lib/contrek/finder/concurrent/geo_json_streaming_merger.rb +3 -4
  30. data/lib/contrek/finder/concurrent/polyline.rb +3 -2
  31. data/lib/contrek/finder/concurrent/shape.rb +2 -1
  32. data/lib/contrek/finder/concurrent/streaming_merger.rb +1 -1
  33. data/lib/contrek/finder/concurrent/tile.rb +9 -2
  34. data/lib/contrek/finder/node.rb +13 -23
  35. data/lib/contrek/finder/node_cluster.rb +17 -5
  36. data/lib/contrek/finder/polygon_finder.rb +29 -1
  37. data/lib/contrek/version.rb +1 -1
  38. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6ed7af7c2661396d55b6ddacd89dfc6c08c4c54744d1443d03a6ba533605ed4
4
- data.tar.gz: 3308bedea2f5e89fe8031a65c404c250490542142101dba32fd09f8510cb8cfb
3
+ metadata.gz: 2d28d3b71b3d867f2899e2f49654decf11a0da8d5d7fa2fff5e08cb3eba265ab
4
+ data.tar.gz: acee94a59b33f775cd35f0bd28fa1f76b9cf1bce254cb51ff728c7f20b56dcb3
5
5
  SHA512:
6
- metadata.gz: 250b0ed1bce589beaae764e03226206853f9e9c3bb356fe3bf827cd25c69d72f33ad820f3afa5d9d31aa83e628e67dd8c4500782dcdb668f44b0e098932f6c4f
7
- data.tar.gz: 7cf17a172afd9ec9b1788f2abe1779421d204886b99fd54e4729fbf9b616cc3968bb7586cafa5b17da6b5cbcf9641e3f210ee6d3d320e4be67e945022964af13
6
+ metadata.gz: f43fbf8121cabf433296aaf9a2ea52a77a9564271a3a34a6954600ebc06004602b9a81a53ec784de49ec32e0a5dd70e015d405359627ce97c28df969d29c5c71
7
+ data.tar.gz: de2c37ca93f6096d202360995b752300a7c69680a50e9a06f2f4e23f9b983afc3ed51f06b05ef6e2a96f2066e1c477896b83cb1e6c0c184237ba8ba9a80ea31c
data/CHANGELOG.md CHANGED
@@ -173,3 +173,6 @@ All notable changes to this project will be documented in this file.
173
173
 
174
174
  ## [1.4.6] - 2026-10-18
175
175
  - Added tile metadata, polygon bounds, ordering, and shared boundary validation for Ruby and C++ mergers, with `unsafe_mode: true` to bypass validation.
176
+
177
+ ## [1.4.7] - 2026-11-07
178
+ - Added multiclass raster polygonization support, preserving class IDs throughout polygon extraction and tile merging, including streaming mergers.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.4.6)
4
+ contrek (1.4.7)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  ffi (~> 1.17)
data/README.md CHANGED
@@ -18,6 +18,8 @@ Besides extracting contours, Contrek can also determine the nesting relationship
18
18
 
19
19
  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.
20
20
 
21
+ Contrek also supports multiclass polygonization, extracting multiple raster classes in a single pass while preserving their identifiers throughout the merging process.
22
+
21
23
  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.
22
24
 
23
25
  ![alt text](contrek.png "Contour tracing")
@@ -676,6 +678,27 @@ result = Contrek.contour!(
676
678
 
677
679
  The choice mainly affects how diagonally touching regions are interpreted.
678
680
 
681
+
682
+ ## Multiclass Polygonization
683
+
684
+ Contrek supports multiclass polygonization, extracting contours for multiple pixel classes in a single processing pass.
685
+ Enable it using the `multiclass: true` option:
686
+
687
+ ```ruby
688
+ result = Contrek.contour!(
689
+ png_file_path: "classified.png",
690
+ options: {
691
+ finder: {
692
+ multiclass: true,
693
+ connectivity: 8
694
+ }
695
+ }
696
+ )
697
+ ```
698
+
699
+ Each polygon retains its original pixel value in the `class` attribute. Regions belonging to different classes remain separate, while regions of the same class can be merged across tile boundaries.
700
+ Multiclass processing is supported by both the Ruby and native C++ implementations, including streaming mergers.
701
+
679
702
  ## Merging External Geometry
680
703
 
681
704
  Contrek's merger can process polygon data produced by external contour
@@ -24,6 +24,7 @@ void FinderUtils::sanitize_options(pf_Options& options, const Options& incoming_
24
24
  options.named_sequences = incoming_options.get<bool>(
25
25
  "named_sequences", options.named_sequences);
26
26
  options.bounds = incoming_options.get<bool>("bounds", options.bounds);
27
+ options.multiclass = incoming_options.get<bool>("multiclass", options.multiclass);
27
28
  options.unsafe_mode = incoming_options.get<bool>("unsafe_mode", options.unsafe_mode);
28
29
  options.deterministic = incoming_options.get<bool>("deterministic", options.deterministic);
29
30
  options.processing_height = static_cast<int>(incoming_options.get<std::int64_t>(
@@ -19,16 +19,15 @@
19
19
 
20
20
  static const int TURNER[2][2] = {{Node::OMAX, Node::OMIN}, {Node::TURN_MAX, Node::TURN_MIN}};
21
21
 
22
- Node::Node(int min_x, int max_x, int y, NodeCluster* cluster, char name)
23
- : cluster(cluster),
22
+ Node::Node(int min_x, int max_x, int y, NodeCluster* cluster, char name, unsigned int class_id)
23
+ : class_id(class_id),
24
+ cluster(cluster),
24
25
  min_x(min_x),
25
26
  max_x(max_x + 1) {
26
27
  this->y = y;
27
28
  this->name = name;
28
29
  this->tangs_count = 0;
29
30
  this->abs_x_index = 0;
30
- this->down_indexer = 0;
31
- this->up_indexer = 0;
32
31
  this->track = 0;
33
32
  this->outer_index = -1;
34
33
  this->inner_index = -1;
@@ -63,47 +62,45 @@ void Node::precalc_tangs_sequences(NodeCluster& cluster) {
63
62
  int upper_size = this->upper_end >= 0 ? (this->upper_end - this->upper_start + 1) : 0;
64
63
  this->tangs_sequence.reserve(lower_size + upper_size);
65
64
 
66
- if (this->upper_end >= 0) {
67
- this->up_indexer = -cluster.vert_nodes[y + T_UP][this->upper_start].abs_x_index;
68
- }
69
65
  // --- CLOCKWISE (UP) ---
70
66
  for (int upper_pos = this->upper_start; upper_pos <= this->upper_end; upper_pos++) {
71
- tangs_sequence.push_back(-(upper_pos + 1));
72
- }
73
- if (this->lower_end >= 0) {
74
- this->down_indexer = (cluster.vert_nodes[y + T_DOWN][this->lower_start].abs_x_index + lower_size + upper_size - 1);
67
+ if (cluster.vert_nodes[y + T_UP][upper_pos].class_id == this->class_id) {
68
+ tangs_sequence.push_back(-(upper_pos + 1));
69
+ }
75
70
  }
71
+
76
72
  // --- COUNTER-CLOCKWISE (DOWN) ---
77
73
  for (int lower_pos = this->lower_end; lower_pos >= this->lower_start; lower_pos--) {
78
- tangs_sequence.push_back(lower_pos);
74
+ if (cluster.vert_nodes[y + T_DOWN][lower_pos].class_id == this->class_id) {
75
+ tangs_sequence.push_back(lower_pos);
76
+ }
79
77
  }
80
78
  this->tangs_count = this->tangs_sequence.size();
81
79
  }
82
80
 
83
81
  Node* Node::my_next_inner(Node *last, int versus) {
84
- unsigned int last_node_index;
85
- if (last->y < this->y) last_node_index = last->abs_x_index + this->up_indexer;
86
- else last_node_index = this->down_indexer - last->abs_x_index;
87
- if (versus == Node::O) last_node_index == 0 ? last_node_index = this->tangs_sequence.size() - 1 : last_node_index--;
88
- else last_node_index == this->tangs_sequence.size() - 1 ? last_node_index = 0 : last_node_index++;
82
+ int virtual_index = (last->y < this->y) ? -(last->abs_x_index + 1) : last->abs_x_index;
83
+ auto it = std::find(this->tangs_sequence.begin(), this->tangs_sequence.end(), virtual_index);
84
+ size_t last_node_index = std::distance(this->tangs_sequence.begin(), it);
85
+ size_t size = this->tangs_sequence.size();
86
+ if (versus == Node::O) last_node_index = (last_node_index == 0) ? size - 1 : last_node_index - 1;
87
+ else last_node_index = (last_node_index == size - 1) ? 0 : last_node_index + 1;
89
88
  return get_tangent_node_by_virtual_index(this->tangs_sequence[last_node_index]);
90
89
  }
91
90
 
92
91
  Node* Node::my_next_outer(Node *last, int versus) {
93
- unsigned int last_node_index;
94
- if (last->y < this->y) last_node_index = last->abs_x_index + this->up_indexer;
95
- else last_node_index = this->down_indexer - last->abs_x_index;
96
- if (versus == Node::O) last_node_index == this->tangs_sequence.size() - 1 ? last_node_index = 0 : last_node_index++;
97
- else last_node_index == 0 ? last_node_index = this->tangs_sequence.size() - 1 : last_node_index--;
92
+ int virtual_index = (last->y < this->y) ? -(last->abs_x_index + 1) : last->abs_x_index;
93
+ auto it = std::find(this->tangs_sequence.begin(), this->tangs_sequence.end(), virtual_index);
94
+ size_t last_node_index = std::distance(this->tangs_sequence.begin(), it);
95
+ size_t size = this->tangs_sequence.size();
96
+ if (versus == Node::O) last_node_index = (last_node_index == size - 1) ? 0 : last_node_index + 1;
97
+ else last_node_index = (last_node_index == 0) ? size - 1 : last_node_index - 1;
98
98
  return get_tangent_node_by_virtual_index(this->tangs_sequence[last_node_index]);
99
99
  }
100
100
 
101
101
  Point Node::coords_entering_to(Node *enter_to, int mode, int tracking) {
102
- int enter_to_index;
103
- if (enter_to->y < this->y) enter_to_index = enter_to->abs_x_index + this->up_indexer;
104
- else enter_to_index = this->down_indexer - enter_to->abs_x_index;
102
+ int tg_index = (enter_to->y < this->y) ? -(enter_to->abs_x_index + 1) : enter_to->abs_x_index;
105
103
 
106
- int tg_index = this->tangs_sequence[enter_to_index];
107
104
  if (tg_index < 0) {
108
105
  Node& node_up = cluster->vert_nodes[y + T_UP][-(tg_index + 1)];
109
106
  if (mode == Node::A) {
@@ -86,7 +86,6 @@ class Node : public Listable {
86
86
 
87
87
  int y;
88
88
  int abs_x_index;
89
- int up_indexer, down_indexer;
90
89
  int tangs_count;
91
90
  int track;
92
91
  int outer_index, inner_index;
@@ -94,6 +93,7 @@ class Node : public Listable {
94
93
  int upper_end = -1;
95
94
  int lower_start = std::numeric_limits<int>::max();
96
95
  int lower_end = -1;
96
+ unsigned int class_id;
97
97
  NodeCluster* cluster;
98
98
  void add_intersection(Node& other_node, int other_node_index);
99
99
  SmallVec tangs_sequence;
@@ -111,7 +111,7 @@ class Node : public Listable {
111
111
 
112
112
  public:
113
113
  int min_x, max_x;
114
- Node(int min_x, int max_x, int y, NodeCluster* cluster, char name);
114
+ Node(int min_x, int max_x, int y, NodeCluster* cluster, char name, unsigned int class_id = 0);
115
115
  void precalc_tangs_sequences(NodeCluster& cluster);
116
116
  void assign_lateral_inner_index(Node* my_prev_node, Node* end_node, int versus);
117
117
  bool processed = false;
@@ -85,8 +85,8 @@ void NodeCluster::build_tangs_sequence() {
85
85
  }
86
86
  }
87
87
 
88
- Node* NodeCluster::add_node(int min_x, int max_x, int y, char name, int offset) {
89
- vert_nodes[y].emplace_back(min_x, max_x, y, this, name);
88
+ Node* NodeCluster::add_node(int min_x, int max_x, int y, char name, int offset, unsigned int class_id) {
89
+ vert_nodes[y].emplace_back(min_x, max_x, y, this, name, class_id);
90
90
 
91
91
  Node& node = vert_nodes[y].back();
92
92
  node.abs_x_index = vert_nodes[y].size() - 1;
@@ -104,8 +104,10 @@ Node* NodeCluster::add_node(int min_x, int max_x, int y, char name, int offset)
104
104
 
105
105
  while (it != up_nodes.end()) {
106
106
  if ((it->min_x - offset) > (node.max_x - 1)) break;
107
- node.add_intersection(*it, it->abs_x_index);
108
- it->add_intersection(node, node.abs_x_index);
107
+ if (!this->options->multiclass || node.class_id == it->class_id) {
108
+ node.add_intersection(*it, it->abs_x_index);
109
+ it->add_intersection(node, node.abs_x_index);
110
+ }
109
111
  ++it;
110
112
  }
111
113
  }
@@ -123,6 +125,7 @@ void NodeCluster::plot(int versus) {
123
125
  this->plot_sequence.clear();
124
126
  this->plot_sequence.push_back(root_node);
125
127
  Polygon poly;
128
+ poly.class_id = root_node->class_id;
126
129
  Node *next_node = nullptr;
127
130
  if ((root_node)->tangs_sequence.size() > 0) // front() or back() on empty list is undefined
128
131
  { versus == Node::A ?
@@ -47,7 +47,7 @@ class NodeCluster {
47
47
  std::list<Polygon> polygons;
48
48
  NodeCluster(int h, int w, pf_Options *options);
49
49
  virtual ~NodeCluster();
50
- Node* add_node(int min_x, int max_x, int y, char name, int offset);
50
+ Node* add_node(int min_x, int max_x, int y, char name, int offset, unsigned int class_id = 0);
51
51
  void calc_root_nodes();
52
52
  void build_tangs_sequence();
53
53
  void plot(int versus);
@@ -17,5 +17,6 @@ struct Polygon {
17
17
  std::vector<Point> outer;
18
18
  std::list<std::vector<Point>> inner;
19
19
  RectBounds bounds;
20
+ unsigned int class_id = 0;
20
21
  Polygon() : bounds(RectBounds::empty()) {}
21
22
  };
@@ -49,7 +49,7 @@ PolygonFinder::PolygonFinder(Bitmap *bitmap,
49
49
 
50
50
  //= SCAN ==============//
51
51
  cpu_timer.start();
52
- scan();
52
+ this->options.multiclass ? multiclass_scan() : scan();
53
53
  reports["scan"] = cpu_timer.stop();
54
54
  //=====================//
55
55
 
@@ -89,20 +89,33 @@ PolygonFinder::~PolygonFinder() {
89
89
  }
90
90
 
91
91
  void PolygonFinder::scan() {
92
- int bpp = this->source_bitmap->get_bytes_per_pixel();
93
- int offset = options.connectivity_offset;
92
+ int bpp = this->source_bitmap->get_bytes_per_pixel();
93
+ int offset = options.connectivity_offset;
94
+
95
+ RGBMatcher* rgb_m = dynamic_cast<RGBMatcher*>(this->matcher);
96
+
97
+ if (bpp == 1) {
98
+ auto fetcher = [](const unsigned char* p) { return static_cast<unsigned int>(p[0]); };
99
+ if (rgb_m) run_loop(rgb_m, fetcher, offset);
100
+ else run_loop(this->matcher, fetcher, offset);
101
+ } else {
102
+ auto fetcher = [](const unsigned char* p) { return *reinterpret_cast<const uint32_t*>(p); };
103
+ if (rgb_m) run_loop(rgb_m, fetcher, offset);
104
+ else run_loop(this->matcher, fetcher, offset);
105
+ }
106
+ }
94
107
 
95
- RGBMatcher* rgb_m = dynamic_cast<RGBMatcher*>(this->matcher);
108
+ void PolygonFinder::multiclass_scan() {
109
+ int bpp = this->source_bitmap->get_bytes_per_pixel();
110
+ int offset = options.connectivity_offset;
96
111
 
97
- if (bpp == 1) {
112
+ if (bpp == 1) {
98
113
  auto fetcher = [](const unsigned char* p) { return static_cast<unsigned int>(p[0]); };
99
- if (rgb_m) run_loop(rgb_m, fetcher, offset);
100
- else run_loop(this->matcher, fetcher, offset);
101
- } else {
114
+ run_multiclass_loop(fetcher, offset);
115
+ } else {
102
116
  auto fetcher = [](const unsigned char* p) { return *reinterpret_cast<const uint32_t*>(p); };
103
- if (rgb_m) run_loop(rgb_m, fetcher, offset);
104
- else run_loop(this->matcher, fetcher, offset);
105
- }
117
+ run_multiclass_loop(fetcher, offset);
118
+ }
106
119
  }
107
120
 
108
121
  ProcessResult* PolygonFinder::process_info() {
@@ -53,6 +53,7 @@ struct pf_Options {
53
53
  bool named_sequences = false;
54
54
  bool unsafe_mode = false;
55
55
  bool bounds = false;
56
+ bool multiclass = false;
56
57
  bool deterministic = false;
57
58
  int processing_height = 0;
58
59
  int connectivity_offset = 0;
@@ -181,6 +182,7 @@ class PolygonFinder {
181
182
  pf_Options options;
182
183
  std::map<std::string, double> reports;
183
184
  void scan();
185
+ void multiclass_scan();
184
186
  CpuTimer cpu_timer;
185
187
 
186
188
  template <typename M, typename F>
@@ -280,6 +282,70 @@ class PolygonFinder {
280
282
  }
281
283
  }
282
284
 
285
+ template <typename F>
286
+ void run_multiclass_loop(F&& fetch_color, int offset) {
287
+ int img_h = this->processing_height;
288
+ int bpp = this->source_bitmap->get_bytes_per_pixel();
289
+
290
+ for (int y = 0; y < img_h; y++) {
291
+ const unsigned char* row_ptr = this->source_bitmap->get_row_ptr(y);
292
+ const unsigned char* p = row_ptr + (this->start_x * bpp);
293
+
294
+ int min_x = this->start_x;
295
+ bool has_color = false;
296
+ unsigned int current_color = 0;
297
+ int x = this->start_x;
298
+ if (bpp == 4) {
299
+ for (; x <= this->end_x - 4; x += 4) {
300
+ unsigned int c0 = fetch_color(p);
301
+ unsigned int c1 = fetch_color(p + 4);
302
+ unsigned int c2 = fetch_color(p + 8);
303
+ unsigned int c3 = fetch_color(p + 12);
304
+ p += 16;
305
+
306
+ if (!has_color) {
307
+ min_x = x; current_color = c0; has_color = true;
308
+ } else if (c0 != current_color) {
309
+ this->node_cluster->add_node(min_x, x - 1, y, current_color, offset, current_color);
310
+ min_x = x; current_color = c0;
311
+ }
312
+
313
+ if (c1 != current_color) {
314
+ this->node_cluster->add_node(min_x, x, y, current_color, offset, current_color);
315
+ min_x = x + 1; current_color = c1;
316
+ }
317
+
318
+ if (c2 != current_color) {
319
+ this->node_cluster->add_node(min_x, x + 1, y, current_color, offset, current_color);
320
+ min_x = x + 2; current_color = c2;
321
+ }
322
+
323
+ if (c3 != current_color) {
324
+ this->node_cluster->add_node(min_x, x + 2, y, current_color, offset, current_color);
325
+ min_x = x + 3; current_color = c3;
326
+ }
327
+ }
328
+ }
329
+
330
+ for (; x < this->end_x; x++) {
331
+ unsigned int color = fetch_color(p);
332
+ p += bpp;
333
+ if (!has_color) {
334
+ min_x = x;
335
+ current_color = color;
336
+ has_color = true;
337
+ } else if (color != current_color) {
338
+ this->node_cluster->add_node(min_x, x - 1, y, current_color, offset, current_color);
339
+ min_x = x;
340
+ current_color = color;
341
+ }
342
+ }
343
+ if (has_color) {
344
+ this->node_cluster->add_node(min_x, this->end_x - 1, y, current_color, offset, current_color);
345
+ }
346
+ }
347
+ }
348
+
283
349
  public:
284
350
  PolygonFinder(Bitmap *bitmap,
285
351
  Matcher *matcher,
@@ -100,7 +100,7 @@ Tile* Cluster::merge_tiles() {
100
100
  }
101
101
  shape->clear_inner();
102
102
 
103
- Polyline* polyline = tile->shapes_pool->acquire_polyline(tile, new_outer.to_vector(), std::nullopt);
103
+ Polyline* polyline = tile->shapes_pool->acquire_polyline(tile, new_outer.to_vector(), shape->outer_polyline->class_id(), std::nullopt);
104
104
  Shape* inserting_new_shape = tile->shapes_pool->acquire_shape(polyline, new_inners);
105
105
  new_shapes.push_back(inserting_new_shape);
106
106
  polyline->shape = inserting_new_shape;
@@ -102,11 +102,17 @@ void Cursor::traverse_outer(Part* start_part,
102
102
  return;
103
103
  }
104
104
  new_position->end_point()->tracked_outer = true;
105
- int versus = act_part->versus();
105
+ int opposite_versus = -act_part->versus();
106
106
  auto& q_set = new_position->end_point()->queues();
107
+ auto* act_part_polyline = act_part->polyline();
107
108
  auto it = std::find_if(q_set.begin(), q_set.end(), [&](Queueable<Point>* q) {
108
109
  Part* p = static_cast<Part*>(q);
109
- return (p->versus() == -versus) && p->polyline()->tile != act_part->polyline()->tile;
110
+ if (p->versus() != opposite_versus) {
111
+ return false;
112
+ }
113
+ auto* polyline = p->polyline();
114
+ return polyline->tile != act_part_polyline->tile &&
115
+ polyline->class_id() == act_part_polyline->class_id();
110
116
  });
111
117
 
112
118
  Part* part = nullptr;
@@ -60,6 +60,9 @@ Finder::Finder(int number_of_threads, Bitmap *bitmap, Matcher *matcher, const Op
60
60
  if (this->options_.processing_height != 0) {
61
61
  base_arguments["processing_height"] = this->options_.processing_height;
62
62
  }
63
+ if (this->options_.multiclass) {
64
+ base_arguments["multiclass"] = true;
65
+ }
63
66
 
64
67
  CpuTimer t;
65
68
  t.start();
@@ -76,10 +76,8 @@ class GeoJsonStreamingMerger : public StreamingMerger {
76
76
  public:
77
77
  GeoJsonStreamingMerger(int number_of_threads,
78
78
  const Options& options,
79
- std::ofstream* stream_to,
80
- unsigned int pixel_value)
81
- : StreamingMerger(number_of_threads, options, stream_to),
82
- target_value(pixel_value) {
79
+ std::ofstream* stream_to)
80
+ : StreamingMerger(number_of_threads, options, stream_to) {
83
81
  configure_point_writer(options);
84
82
  }
85
83
 
@@ -90,7 +88,7 @@ class GeoJsonStreamingMerger : public StreamingMerger {
90
88
  }
91
89
  is_first_feature = false;
92
90
 
93
- *stream << "{\"type\":\"Feature\",\"properties\":{\"PixelVal\":" << target_value
91
+ *stream << "{\"type\":\"Feature\",\"properties\":{\"PixelVal\":" << polygon.class_id
94
92
  << "},\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[";
95
93
  const std::vector<Point>& points = polygon.outer;
96
94
  const size_t points_size = points.size();
@@ -17,10 +17,11 @@
17
17
  #include "Tile.h"
18
18
  #include "Shape.h"
19
19
 
20
- Polyline::Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds)
20
+ Polyline::Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds, unsigned int class_id)
21
21
  : tile(tile),
22
22
  shape_pool(shape_pool),
23
- raw_(std::move(polygon))
23
+ raw_(std::move(polygon)),
24
+ class_id_(class_id)
24
25
  { if (bounds.has_value()) {
25
26
  min_x = bounds->min_x;
26
27
  max_x_ = bounds->max_x;
@@ -29,7 +29,7 @@ class InnerPolyline;
29
29
  class Polyline : public Partitionable {
30
30
  public:
31
31
  using Partitionable::Partitionable;
32
- Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds = std::nullopt);
32
+ Polyline(ShapePool* shape_pool, Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds = std::nullopt, unsigned int class_id = 0);
33
33
  enum Flags : uint32_t {
34
34
  TRACKED_OUTER = 1 << 0
35
35
  };
@@ -47,6 +47,7 @@ class Polyline : public Partitionable {
47
47
  int max_y() const { return max_y_; }
48
48
  int min_y() const { return min_y_; }
49
49
  int max_x() const { return max_x_; }
50
+ unsigned int class_id() const { return class_id_; }
50
51
  void clear();
51
52
  bool is_empty();
52
53
  bool any_ancients = false;
@@ -63,5 +64,6 @@ class Polyline : public Partitionable {
63
64
  void find_boundary();
64
65
  uint32_t flags_ = 0;
65
66
  int name;
67
+ unsigned int class_id_;
66
68
  std::string named_;
67
69
  };
@@ -49,6 +49,7 @@ std::string Shape::name() {
49
49
  Polygon Shape::to_raw_polygon(bool bounds) {
50
50
  Polygon poly;
51
51
  poly.outer = this->outer_polyline->raw();
52
+ poly.class_id = this->outer_polyline->class_id();
52
53
  if (bounds) {
53
54
  this->outer_polyline->fill_bounds(poly.bounds);
54
55
  }
@@ -19,8 +19,8 @@ Shape* ShapePool::acquire_shape(Polyline* outer_polyline, const std::vector<Inne
19
19
  return shape;
20
20
  }
21
21
 
22
- Polyline* ShapePool::acquire_polyline(Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds = std::nullopt) {
23
- return this->polylines.acquire(this, tile, std::move(polygon), bounds);
22
+ Polyline* ShapePool::acquire_polyline(Tile* tile, std::vector<Point> polygon, unsigned int class_id, const std::optional<RectBounds>& bounds = std::nullopt) {
23
+ return this->polylines.acquire(this, tile, std::move(polygon), bounds, class_id);
24
24
  }
25
25
 
26
26
  InnerPolyline* ShapePool::acquire_inner_polyline(std::vector<Point> coords, Shape* shape) {
@@ -29,7 +29,7 @@ class ShapePool {
29
29
  Shape* acquire_shape(Polyline* outer_polyline, const std::vector<InnerPolyline*>& inner_polylines);
30
30
  InnerPolyline* acquire_inner_polyline(std::vector<Point> coords, Shape* s);
31
31
  InnerPolyline* acquire_inner_polyline(Sequence* seq);
32
- Polyline* acquire_polyline(Tile* tile, std::vector<Point> polygon, const std::optional<RectBounds>& bounds);
32
+ Polyline* acquire_polyline(Tile* tile, std::vector<Point> polygon, unsigned int class_id, const std::optional<RectBounds>& bounds);
33
33
  void set_owner(Tile* tile);
34
34
  void detach_shape();
35
35
  void detach_polyline();
@@ -60,7 +60,7 @@ void Tile::assign_raw_polygons(const std::list<Polygon>& raw_polylines, const st
60
60
  std::unordered_map<int, Shape*> shapes_map;
61
61
  int polyline_index = 0;
62
62
  for (const auto& raw_polyline : raw_polylines)
63
- { Polyline* polyline = this->shapes_pool->acquire_polyline(this, raw_polyline.outer, raw_polyline.bounds);
63
+ { Polyline* polyline = this->shapes_pool->acquire_polyline(this, raw_polyline.outer, raw_polyline.class_id, raw_polyline.bounds);
64
64
  std::vector<InnerPolyline*> inner_polylines_list;
65
65
  for (auto& raw_points : raw_polyline.inner) {
66
66
  inner_polylines_list.push_back(this->shapes_pool->acquire_inner_polyline(raw_points, nullptr));
@@ -277,6 +277,9 @@ class To_Ruby<ProcessResult*>
277
277
  poly_hash[Symbol("bounds")] = bounds_hash;
278
278
  }
279
279
 
280
+ if(pr->options.contains("multiclass")) {
281
+ poly_hash[Symbol("class")] = x.class_id;
282
+ }
280
283
  out.push(poly_hash);
281
284
  }
282
285
  rr->polygons = out;
@@ -317,6 +320,10 @@ ProcessResult ruby_result_to_process_result(Rice::Object rb_result) {
317
320
  for (size_t i = 0; i < rb_polygons.size(); ++i) {
318
321
  Rice::Hash rb_poly = (Rice::Hash)rb_polygons[i];
319
322
  Polygon poly;
323
+ Rice::Object rb_class = rb_poly[Symbol("class")];
324
+ if (!rb_class.is_nil()) {
325
+ poly.class_id = detail::From_Ruby<unsigned int>().convert(rb_class.value());
326
+ }
320
327
  // BOUNDS
321
328
  Rice::Object rb_bounds_obj = rb_poly[Symbol("bounds")];
322
329
  if (!rb_bounds_obj.is_nil()) {
@@ -382,10 +389,9 @@ SvgStreamingMerger* create_svg_streaming_merger(Object self,
382
389
  GeoJsonStreamingMerger* create_geo_json_streaming_merger(Object self,
383
390
  int number_of_threads,
384
391
  Options options,
385
- Object stream_obj,
386
- int pixel_val) {
392
+ Object stream_obj) {
387
393
  OfstreamWrapper* wrapper = Rice::detail::From_Ruby<OfstreamWrapper*>().convert(stream_obj.value());
388
- return new GeoJsonStreamingMerger(number_of_threads, options, &wrapper->get_stream(), pixel_val);
394
+ return new GeoJsonStreamingMerger(number_of_threads, options, &wrapper->get_stream());
389
395
  }
390
396
 
391
397
  OfstreamWrapper* create_ofstream(Object self, std::string path) {
@@ -3,8 +3,8 @@
3
3
  module Contrek
4
4
  module Cpp
5
5
  class CPPGeoJsonConcurrentStreamingMerger < CPPGeoJsonStreamingMerger
6
- def self.new(stream_to:, number_of_threads: 0, options: nil, pixel_val: 0)
7
- super(number_of_threads, options, stream_to, pixel_val)
6
+ def self.new(stream_to:, number_of_threads: 0, options: nil)
7
+ super(number_of_threads, options, stream_to)
8
8
  end
9
9
  end
10
10
  end
@@ -23,7 +23,8 @@ module Contrek
23
23
  @to_points ||= raw_list.map do |polygon|
24
24
  {bounds: (polygon[:bounds] if polygon.key?(:bounds)),
25
25
  outer: self.class.to_points(polygon[:outer]),
26
- inner: polygon[:inner].map { |s| self.class.to_points(s) }}.compact
26
+ inner: polygon[:inner].map { |s| self.class.to_points(s) },
27
+ class: (polygon[:class] if polygon.key?(:class))}.compact
27
28
  end
28
29
  end
29
30
 
@@ -69,7 +69,7 @@ module Contrek
69
69
  end.real
70
70
  shape.clear_inner!
71
71
 
72
- polyline = Polyline.new(tile: tile, polygon: new_outer.to_a)
72
+ polyline = Polyline.new(tile: tile, polygon: new_outer.to_a, class_id: shape.outer_polyline.class_id)
73
73
  inserting_new_shape = Shape.init_by(polyline, new_inners)
74
74
  new_shapes << inserting_new_shape
75
75
  polyline.shape = inserting_new_shape
@@ -91,7 +91,9 @@ module Contrek
91
91
  new_position.end_point.tracked_outer = true
92
92
  versus = act_part.versus
93
93
  part = new_position.end_point.queues.find do |p|
94
- (p.versus == -versus) && p.polyline.tile != act_part.polyline.tile
94
+ (p.versus == -versus) &&
95
+ p.polyline.tile != act_part.polyline.tile &&
96
+ p.polyline.class_id == act_part.polyline.class_id
95
97
  end
96
98
  if part
97
99
  if all_parts[-2] != part
@@ -55,7 +55,8 @@ module Contrek
55
55
  versus: current_versus,
56
56
  bounds: true,
57
57
  treemap: @options[:treemap],
58
- connectivity: @options[:connectivity]
58
+ connectivity: @options[:connectivity],
59
+ multiclass: @options[:multiclass]
59
60
  }.compact,
60
61
  start_x: payload[:tile_start_x],
61
62
  end_x: payload[:tile_end_x]
@@ -3,11 +3,10 @@
3
3
  module Contrek
4
4
  module Concurrent
5
5
  class GeoJsonStreamingMerger < StreamingMerger
6
- def initialize(stream_to:, pixel_val:, options: {})
7
- @pixel_val = pixel_val
6
+ def initialize(stream_to:, options: {})
8
7
  @point_converter = self.class.coordinates_converter(options[:geo_localization])
9
8
 
10
- super(stream_to:, options:)
9
+ super
11
10
  end
12
11
 
13
12
  def write_header
@@ -35,7 +34,7 @@ module Contrek
35
34
  end
36
35
  feature_hash = {
37
36
  type: "Feature",
38
- properties: {PixelVal: @pixel_val},
37
+ properties: {PixelVal: polygon[:class]},
39
38
  geometry: {
40
39
  type: "Polygon",
41
40
  coordinates: polygon_coordinates
@@ -7,10 +7,10 @@ module Contrek
7
7
 
8
8
  TRACKED_OUTER = 1 << 0
9
9
 
10
- attr_reader :raw, :name, :min_y, :max_y
10
+ attr_reader :raw, :name, :min_y, :max_y, :class_id
11
11
  attr_accessor :shape, :tile, :any_ancients, :inside_inner_polyline
12
12
 
13
- def initialize(tile:, polygon:, shape: nil, bounds: nil, force_named: nil)
13
+ def initialize(tile:, polygon:, shape: nil, bounds: nil, force_named: nil, class_id: 0)
14
14
  @tile = tile
15
15
  @name = tile.shapes.count
16
16
  @named = force_named
@@ -19,6 +19,7 @@ module Contrek
19
19
  @flags = 0
20
20
  @any_ancients = false
21
21
  @inside_inner_polyline = nil
22
+ @class_id = class_id
22
23
 
23
24
  if bounds.nil?
24
25
  find_boundary
@@ -40,8 +40,9 @@ module Contrek
40
40
  "Shape (outer_polyline = #{outer_polyline.named}, children count = #{@children_shapes.size}"
41
41
  end
42
42
 
43
- def to_raw_polygon(bounds: false)
43
+ def to_raw_polygon(bounds: false, multiclass: false)
44
44
  {bounds: (outer_polyline.get_bounds if bounds),
45
+ class: (outer_polyline.class_id if multiclass),
45
46
  outer: outer_polyline.raw,
46
47
  inner: inner_polylines.map(&:raw)}.compact
47
48
  end
@@ -37,7 +37,7 @@ module Contrek
37
37
  bounds = shape.outer_polyline.get_bounds
38
38
  if flush || bounds[:max_x] < safe_boundary_x
39
39
  @moved += 1
40
- polygon = shape.to_raw_polygon
40
+ polygon = shape.to_raw_polygon(multiclass: true)
41
41
  polygons_to_stream << polygon
42
42
  false
43
43
  else
@@ -57,7 +57,8 @@ module Contrek
57
57
 
58
58
  def to_raw_polygons
59
59
  bounds = @finder.options[:bounds]
60
- @shapes.map { |s| s.to_raw_polygon(bounds:) }
60
+ multiclass = @finder.options[:multiclass]
61
+ @shapes.map { |s| s.to_raw_polygon(bounds:, multiclass:) }
61
62
  end
62
63
 
63
64
  def compute_treemap
@@ -93,7 +94,13 @@ module Contrek
93
94
  shapes_map = {}
94
95
  raw_polylines.each_with_index do |raw_polyline, polyline_index|
95
96
  shape = Shape.new.tap do |shape|
96
- shape.outer_polyline = Polyline.new(tile: self, polygon: raw_polyline[:outer], shape: shape, bounds: raw_polyline[:bounds])
97
+ shape.outer_polyline = Polyline.new(
98
+ tile: self,
99
+ polygon: raw_polyline[:outer],
100
+ shape: shape,
101
+ bounds: raw_polyline[:bounds],
102
+ class_id: raw_polyline[:class] || 0
103
+ )
97
104
  shape.inner_polylines = raw_polyline[:inner].map { |raw| InnerPolyline.new(shape: shape, raw_coordinates: raw) }
98
105
  end
99
106
  @shapes << shape
@@ -6,7 +6,7 @@ module Contrek
6
6
  include Listable
7
7
 
8
8
  attr_reader :min_x, :max_x, :y, :name, :tangs_sequence, :tangs_count, :data_pointer,
9
- :upper_start, :upper_end, :lower_start, :lower_end
9
+ :upper_start, :upper_end, :lower_start, :lower_end, :class_id
10
10
  attr_accessor :track, :abs_x_index, :outer_index, :inner_index, :inner_left_index, :inner_right_index
11
11
 
12
12
  T_UP = -1
@@ -27,8 +27,9 @@ module Contrek
27
27
  OUTER = 0
28
28
  INNER = 1
29
29
 
30
- def initialize(cluster, min_x, max_x, y, name, connectivity_offset = 0)
30
+ def initialize(cluster, min_x, max_x, y, name, connectivity_offset = 0, class_id = 0)
31
31
  @name = name
32
+ @class_id = class_id
32
33
  @min_x = min_x
33
34
  @max_x = max_x + 1
34
35
  @y = y
@@ -37,8 +38,6 @@ module Contrek
37
38
  @track = 0
38
39
  @abs_x_index = 0
39
40
  @data_pointer = cluster.lists.get_data_pointer
40
- @up_indexer = 0
41
- @down_indexer = 0
42
41
  @outer_index = -1
43
42
  @inner_index = -1
44
43
  @inner_left_index = -1
@@ -81,11 +80,8 @@ module Contrek
81
80
  end
82
81
 
83
82
  def my_next(last, versus, mode)
84
- last_node_index = if last.y < y
85
- last.abs_x_index + @up_indexer
86
- else
87
- @down_indexer - last.abs_x_index
88
- end
83
+ virtual_index = (last.y < y) ? -(last.abs_x_index + 1) : last.abs_x_index
84
+ last_node_index = @tangs_sequence.index(virtual_index)
89
85
 
90
86
  case mode
91
87
  when :outer
@@ -117,13 +113,8 @@ module Contrek
117
113
  end
118
114
 
119
115
  def coords_entering_to(enter_to, enter_mode, tracking)
120
- enter_to_index = if enter_to.y < y
121
- enter_to.abs_x_index + @up_indexer
122
- else
123
- @down_indexer - enter_to.abs_x_index
124
- end
116
+ tg_index = (enter_to.y < y) ? -(enter_to.abs_x_index + 1) : enter_to.abs_x_index
125
117
 
126
- tg_index = @tangs_sequence[enter_to_index]
127
118
  if tg_index < 0
128
119
  node_up = @cluster.vert_nodes[y + T_UP][-(tg_index + 1)]
129
120
  if enter_mode == :a
@@ -166,20 +157,19 @@ module Contrek
166
157
 
167
158
  def precalc_tangs_sequences(cluster:)
168
159
  @tangs_sequence = []
169
- @up_indexer = -cluster.vert_nodes[@y + T_UP][@upper_start].abs_x_index if @upper_end >= 0
170
160
  if @upper_end >= 0
171
161
  (@upper_start..@upper_end).each do |upper_pos|
172
- @tangs_sequence << -(upper_pos + 1)
162
+ if cluster.vert_nodes[@y + T_UP][upper_pos].class_id == class_id
163
+ @tangs_sequence << -(upper_pos + 1)
164
+ end
173
165
  end
174
166
  end
175
- if @lower_end >= 0
176
- lower_size = (@lower_end >= 0) ? (@lower_end - @lower_start + 1) : 0
177
- upper_size = (@upper_end >= 0) ? (@upper_end - @upper_start + 1) : 0
178
- @down_indexer = (cluster.vert_nodes[@y + T_DOWN][@lower_start].abs_x_index + lower_size + upper_size - 1)
179
- end
167
+
180
168
  if @lower_end >= 0
181
169
  @lower_end.downto(@lower_start).each do |lower_pos|
182
- @tangs_sequence << lower_pos
170
+ if cluster.vert_nodes[@y + T_DOWN][lower_pos].class_id == class_id
171
+ @tangs_sequence << lower_pos
172
+ end
183
173
  end
184
174
  end
185
175
  @tangs_count = tangs_sequence.size
@@ -57,6 +57,8 @@ module Contrek
57
57
  versus = @options[:versus]
58
58
  inner_v = VERSUS_INVERTER[versus]
59
59
  index_order = 0
60
+ multiclass = @options[:multiclass]
61
+
60
62
  while @root_nodes.size > 0
61
63
  root_node = @root_nodes.shift
62
64
 
@@ -110,7 +112,12 @@ module Contrek
110
112
  draw_sequence(bitmap, "X") unless bitmap.nil?
111
113
 
112
114
  if @sequence_coords.any?
113
- @polygons << {outer: @sequence_coords, inner: [], bounds: (bounds.to_h if @options[:bounds])}.compact
115
+ @polygons << {
116
+ outer: @sequence_coords,
117
+ inner: [],
118
+ bounds: (bounds.to_h if @options[:bounds]),
119
+ class: (root_node.class_id if multiclass)
120
+ }.compact
114
121
  @sequences << @plot_sequence
115
122
 
116
123
  @count = 0
@@ -332,6 +339,7 @@ module Contrek
332
339
  end
333
340
 
334
341
  def add_node(node, offset)
342
+ multiclass = options[:multiclass]
335
343
  @nodes += 1
336
344
  node.abs_x_index = @vert_nodes[node.y].size
337
345
 
@@ -348,15 +356,19 @@ module Contrek
348
356
  up_node = up_nodes[index]
349
357
  if ((up_node.max_x - 1) + offset) >= node.min_x
350
358
  if (up_node.min_x - offset) <= (node.max_x - 1)
351
- node.add_intersection(up_node, index)
352
- up_node.add_intersection(node, node.abs_x_index)
359
+ unless multiclass && node.class_id != up_node.class_id
360
+ node.add_intersection(up_node, index)
361
+ up_node.add_intersection(node, node.abs_x_index)
362
+ end
353
363
  end
354
364
  return if (index += 1) == up_nodes_count
355
365
  loop do
356
366
  up_node = up_nodes[index]
357
367
  if (up_node.min_x - offset) <= (node.max_x - 1)
358
- node.add_intersection(up_node, index)
359
- up_node.add_intersection(node, node.abs_x_index)
368
+ unless multiclass && node.class_id != up_node.class_id
369
+ node.add_intersection(up_node, index)
370
+ up_node.add_intersection(node, node.abs_x_index)
371
+ end
360
372
  else
361
373
  return
362
374
  end
@@ -21,7 +21,7 @@ module Contrek
21
21
 
22
22
  # 1 finds matching blocks
23
23
  @reports[:scan] = Benchmark.measure do
24
- scan
24
+ @options[:multiclass] ? multiclass_scan : scan
25
25
  end
26
26
 
27
27
  # 2 builds relational spatially tree map
@@ -129,6 +129,34 @@ module Contrek
129
129
  end
130
130
  end
131
131
 
132
+ def multiclass_scan
133
+ offset = (@node_cluster.options[:connectivity] == 8) ? 1 : 0
134
+ current_color = nil
135
+ min_x = start_x
136
+ current_y = nil
137
+ last_x = end_x - 1
138
+
139
+ @source_bitmap.scan(start_x: start_x, end_x: end_x, end_y: @processing_height) do |x, y, color|
140
+ if current_y != y || (current_color && color != current_color)
141
+ unless current_color.nil?
142
+ Contrek::Finder::Node.new(@node_cluster, min_x, ((current_y == y) ? x - 1 : last_x), current_y, current_color, offset, current_color.ord)
143
+ end
144
+ min_x = x if current_y == y
145
+ end
146
+
147
+ if current_y != y
148
+ current_y = y
149
+ min_x = x
150
+ end
151
+
152
+ current_color = color
153
+ if x == last_x && current_color
154
+ Contrek::Finder::Node.new(@node_cluster, min_x, last_x, current_y, current_color, offset, current_color.ord)
155
+ current_color = nil
156
+ end
157
+ end
158
+ end
159
+
132
160
  def format_benchmarks
133
161
  r = {}
134
162
  total = 0
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Contrek
4
- VERSION = "1.4.6"
4
+ VERSION = "1.4.7"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrek
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.6
4
+ version: 1.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni