contrek 1.3.0 → 1.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58d88411970256b3edc37da05dd9304aaa4593b320e7d73db5966b69d403c4cf
4
- data.tar.gz: 6a05687f7ffba4b22eb7ebf02abf2a622931bc3250968fa785fb13720036118e
3
+ metadata.gz: 0fff9996602ac84d2a5d96e840150e59cb457bc8050c53758efac88f170bef79
4
+ data.tar.gz: 40958fef1d22e2458beae7291cfdca85102eda4983bf01ef468c334ddce99ec8
5
5
  SHA512:
6
- metadata.gz: 7150e06efce7580fdefd55725db0de52e2805b909e800679a0f99481c3e52333209d1b5ceb750c1fc63a28fa6aaaad7f7662d3baf3ebf377b29cfe4dacf6d19d
7
- data.tar.gz: 988e85fd99234df2b3cd0e8bec0d8f68ba0f631111a3fda62e114e185dd1619589223266b5e3db71a2a5c478d2871f862209489df997a13a9da866b84718c609
6
+ metadata.gz: 7f8f1f1368c472fb9770a892874f54817a67745929a0d772c5a9cc3e387fcd868e5662ad67e341d6a53232b6be4eb028438545535adfb05be3d69c497c3918da
7
+ data.tar.gz: cc7991d57ce63d43c98165c1f3684d1f31d05993a6e4a20e40437d70d5b251a1de0032ca354f44d2bb8efbf30286423f6b2925e01da0f039c71661427f4d2716
data/CHANGELOG.md CHANGED
@@ -122,3 +122,10 @@ All notable changes to this project will be documented in this file.
122
122
 
123
123
  ## [1.2.9] - 2026-06-13
124
124
  - **Streaming merger:** The streaming merger class extends VerticalMerger and adds a useful feature: the progressive extraction of contours into a disk buffer (SVG file). In this way, all extracted polygons that are no longer within the junction zone of the next stripe are removed from the system and streamed directly to disk. This incredibly reduces memory consumption, allowing the processing of very large files on machines with low memory availability, at the expense of increased processing times. An example of this technique is available in both C++ and Ruby in the repository.
125
+
126
+ ## [1.3.0] - 2026-06-17
127
+ - **Streaming merger:** Improvements and bug fixing.
128
+ - **CPP code:** All structures now own 'Point' instances by value instead of raw pointers. Removed now-redundant `clone()` method; results from `process_info()` are already self-contained since points are owned by value, so the defensive deep copy is no longer needed.
129
+
130
+ ## [1.3.1] - 2026-06-20
131
+ - **Streaming merger:** The progressive streaming extraction mode has now reached new heights of efficiency on the C++ side. This mode allows the data source to be processed in contiguous blocks. All isolated polygons, as well as those extending into the bottom strips, are removed from the list and streamed directly to the SVG file. This drastically reduces RAM requirements. An extreme test on an 81920×81920 pixel image containing a massive number of polygons (20,000,000) was processed using roughly 40 strips of 2000 pixels each in less than 300 seconds, peaking at a RAM usage of just 13GB.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- contrek (1.3.0)
4
+ contrek (1.3.1)
5
5
  chunky_png (~> 1.4)
6
6
  concurrent-ruby (~> 1.3.5)
7
7
  rice (= 4.5.0)
@@ -461,20 +461,14 @@ void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_h
461
461
  ProcessResult *result = polygon_finder.process_info();
462
462
  if (result) {
463
463
  std::cout << "stripe " << stripe_count << ": found polygons " << result->groups << std::endl;
464
- result_clones.push_back(result);
465
464
  vmerger.add_tile(*result, !(current_y_offset + stripe_height < total_height));
465
+ delete result;
466
466
  }
467
467
  stripe_count++;
468
468
  }
469
-
470
469
  ProcessResult *merged_result = vmerger.process_info();
471
470
  std::cout << "total found polygons " << merged_result->groups << std::endl;
472
471
  delete merged_result;
473
-
474
- // frees memory
475
- for (auto c : result_clones) {
476
- delete c;
477
- }
478
472
  } catch (const std::exception& e) {
479
473
  std::cerr << "\n[ERROR] Processing exception: " << e.what() << std::endl;
480
474
  if (shared_stream.is_open()) shared_stream.close();
@@ -34,5 +34,5 @@ class Cluster {
34
34
  static void list_to_string(std::vector<Point> list);
35
35
  PartPool parts_pool;
36
36
  std::deque<Position> positions_pool;
37
- const Finder* finder() const { return finder_; };
37
+ const Finder* finder() const { return finder_; }
38
38
  };
@@ -61,11 +61,6 @@ void Cursor::traverse_outer(Part* act_part,
61
61
  if (act_part->size == 0) return;
62
62
 
63
63
  while (Position *position = act_part->next_position(nullptr)) {
64
- if (outer_joined_polyline->size > 1 &&
65
- outer_joined_polyline->head->payload == position->payload &&
66
- act_part == all_parts.front()) {
67
- return;
68
- }
69
64
  outer_joined_polyline->add(position);
70
65
  }
71
66
  } else {
@@ -75,6 +75,36 @@ void Part::orient()
75
75
  }
76
76
  }
77
77
 
78
+ void Part::try_transmutation() {
79
+ auto& head_queues = static_cast<Position*>(this->head)->end_point()->queues();
80
+ if (head_queues.size() == 1) {
81
+ return;
82
+ }
83
+
84
+ auto other_head_it = std::find_if(head_queues.begin(), head_queues.end(), [this](auto* queueable_ptr) {
85
+ Part* part = static_cast<Part*>(queueable_ptr);
86
+ return part != this && part->polyline()->tile == this->polyline()->tile;
87
+ });
88
+
89
+ if (other_head_it != head_queues.end()) {
90
+ Part* other_head_part = static_cast<Part*>(*other_head_it);
91
+ auto& tail_queues = static_cast<Position*>(this->tail)->end_point()->queues();
92
+ auto found_in_tail = std::find(tail_queues.begin(), tail_queues.end(), other_head_part);
93
+ if (found_in_tail != tail_queues.end()) {
94
+ if ( (other_head_part->tail->payload.y == tail->payload.y && other_head_part->head->payload.y == head->payload.y) ||
95
+ (other_head_part->tail->payload.y == head->payload.y && other_head_part->head->payload.y == tail->payload.y)) {
96
+ if (this->next == nullptr && other_head_part->prev == nullptr) {
97
+ this->mirror = true;
98
+ }
99
+ } else {
100
+ this->type = Part::EXCLUSIVE;
101
+ this->trasmuted = true;
102
+ other_head_part->transmutation_skip = true;
103
+ }
104
+ }
105
+ }
106
+ }
107
+
78
108
  bool Part::within(Part* other) {
79
109
  const auto [self_min, self_max] = std::minmax(this->head->payload.y, this->tail->payload.y);
80
110
  const auto [other_min, other_max] = std::minmax(other->head->payload.y, other->tail->payload.y);
@@ -32,9 +32,11 @@ class Part : public Queueable<Point> {
32
32
  bool is(Types type);
33
33
  bool inverts = false;
34
34
  bool trasmuted = false;
35
+ bool transmutation_skip = false;
35
36
  bool dead_end = false;
36
37
  bool mirror = false;
37
38
  Part* next = nullptr;
39
+ Part* next_seam = nullptr;
38
40
  Part* prev = nullptr;
39
41
  Part* circular_next = nullptr;
40
42
  std::string toString() const { return "Part type = " + std::to_string(static_cast<uint32_t>(type)); }
@@ -51,6 +53,7 @@ class Part : public Queueable<Point> {
51
53
  std::vector<EndPoint*> continuum_to(const Part& other_part) const;
52
54
  bool within(Part* other);
53
55
  bool same_length(Part* other);
56
+ void try_transmutation();
54
57
 
55
58
  private:
56
59
  int versus_ = 0;
@@ -32,11 +32,23 @@ void Partitionable::add_part(Part* new_part)
32
32
  new_part->prev = last;
33
33
  new_part->circular_next = this->parts_.front();
34
34
 
35
- if (new_part->is(Part::SEAM)) new_part->orient();
35
+ if (new_part->is(Part::SEAM)) {
36
+ if (!this->first_seam) {
37
+ this->first_seam = new_part;
38
+ }
39
+ if (this->last_seam) {
40
+ this->last_seam->next_seam = new_part;
41
+ }
42
+ this->last_seam = new_part;
43
+ new_part->orient();
44
+ }
36
45
  }
37
46
 
38
47
  void Partitionable::partition()
39
48
  { this->parts_.clear();
49
+ this->first_seam = nullptr;
50
+ this->last_seam = nullptr;
51
+
40
52
  Polyline *polyline = static_cast<Polyline*>(this);
41
53
  PartPool& pool = polyline->tile->cluster->parts_pool;
42
54
  Part *current_part = nullptr;
@@ -65,54 +77,42 @@ void Partitionable::partition()
65
77
  }
66
78
  this->add_part(current_part);
67
79
 
68
- this->trasmute_parts();
80
+ this->transmute_parts();
69
81
  }
70
82
 
71
- void Partitionable::trasmute_parts()
83
+ void Partitionable::transmute_transposed_part(Part* part) {
84
+ if (Part* current_seam = this->first_seam; current_seam != nullptr) {
85
+ while (current_seam != nullptr) {
86
+ if (current_seam != part) {
87
+ if (part->within(current_seam)) {
88
+ if (!part->same_length(current_seam)) {
89
+ part->type = Part::EXCLUSIVE;
90
+ part->trasmuted = true;
91
+ std::vector<Queueable<Point>*>& a = static_cast<Position*>(part->head)->end_point()->queues();
92
+ a.erase(std::remove(a.begin(), a.end(), part), a.end());
93
+ std::vector<Queueable<Point>*>& b = static_cast<Position*>(part->tail)->end_point()->queues();
94
+ b.erase(std::remove(b.begin(), b.end(), part), b.end());
95
+ }
96
+ }
97
+ }
98
+ current_seam = current_seam->next_seam;
99
+ }
100
+ }
101
+ }
102
+
103
+ void Partitionable::transmute_parts()
72
104
  { Polyline *polyline = static_cast<Polyline*>(this);
73
105
  bool transpose = polyline->tile->cluster->finder()->transpose();
74
-
75
- for (Part* inside : parts_) {
76
- if (!inside->is(Part::SEAM)) continue;
77
- for (Part* inside_compare : parts_) {
78
- if (inside == inside_compare || !inside_compare->is(Part::SEAM)) continue;
79
-
106
+ if (Part* current_seam = this->first_seam; current_seam != nullptr) {
107
+ while (current_seam != nullptr) {
80
108
  if (transpose) {
81
- if (inside->within(inside_compare)) {
82
- Part* target_part;
83
- if (!inside->same_length(inside_compare)) {
84
- target_part = inside;
85
- target_part->type = Part::EXCLUSIVE;
86
- target_part->trasmuted = true;
87
- std::vector<Queueable<Point>*>& a = static_cast<Position*>(target_part->head)->end_point()->queues();
88
- a.erase(std::remove(a.begin(), a.end(), target_part), a.end());
89
- std::vector<Queueable<Point>*>& b = static_cast<Position*>(target_part->tail)->end_point()->queues();
90
- b.erase(std::remove(b.begin(), b.end(), target_part), b.end());
91
- break;
92
- }
93
- }
109
+ transmute_transposed_part(current_seam);
94
110
  } else {
95
- int count = 0;
96
- inside->each([&](QNode<Point>* pos) -> bool {
97
- Position *position = static_cast<Position*>(pos);
98
- if (position->end_point()->queues_include(inside_compare))
99
- { count++;
100
- return true;
101
- }
102
- return false;
103
- });
104
- if (count == inside->size) {
105
- if (count < inside_compare->size) {
106
- inside->type = Part::EXCLUSIVE;
107
- inside->trasmuted = true;
108
- break;
109
- } else if ( count == inside_compare->size &&
110
- inside->next == nullptr &&
111
- inside_compare->prev == nullptr) {
112
- inside->mirror = true;
113
- }
111
+ if (!current_seam->transmutation_skip) {
112
+ current_seam->try_transmutation();
114
113
  }
115
114
  }
115
+ current_seam = current_seam->next_seam;
116
116
  }
117
117
  }
118
118
  }
@@ -21,8 +21,10 @@ class Partitionable {
21
21
 
22
22
  protected:
23
23
  std::vector<Part*> parts_;
24
-
24
+ Part* first_seam = nullptr;
25
+ Part* last_seam = nullptr;
25
26
  private:
26
27
  void add_part(Part* new_part);
27
- void trasmute_parts();
28
+ void transmute_parts();
29
+ void transmute_transposed_part(Part* part);
28
30
  };
@@ -12,6 +12,7 @@
12
12
  #include <string>
13
13
  #include <unordered_set>
14
14
  #include <sstream>
15
+ #include <utility>
15
16
  #include "Polyline.h"
16
17
  #include "Tile.h"
17
18
  #include "Shape.h"
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  #include <vector>
11
+ #include <utility>
11
12
  #include "Shape.h"
12
13
  #include "ShapePool.h"
13
14
  #include "Tile.h"
@@ -102,9 +102,6 @@ module Contrek
102
102
  if act_part.is?(Part::EXCLUSIVE)
103
103
  return if act_part.size == 0
104
104
  while (position = act_part.next_position)
105
- return if outer_joined_polyline.size > 1 &&
106
- outer_joined_polyline.head.payload == position.payload &&
107
- act_part == all_parts.first
108
105
  outer_joined_polyline.add(position)
109
106
  end
110
107
  else
@@ -10,11 +10,13 @@ module Contrek
10
10
  ADDED = 2
11
11
 
12
12
  attr_reader :polyline, :touched
13
- attr_accessor :next, :circular_next, :prev, :type, :dead_end, :inverts, :trasmuted, :versus, :mirror
13
+ attr_accessor :next, :circular_next, :prev, :type, :dead_end, :inverts,
14
+ :trasmuted, :versus, :mirror, :next_seam, :transmutation_skip
14
15
  def initialize(type, polyline)
15
16
  @type = type
16
17
  @polyline = polyline
17
18
  @next = nil
19
+ @next_seam = nil
18
20
  @circular_next = nil
19
21
  @prev = nil
20
22
  @dead_end = false
@@ -23,6 +25,7 @@ module Contrek
23
25
  @trasmuted = false
24
26
  @versus = 0
25
27
  @mirror = false
28
+ @transmutation_skip = false
26
29
  end
27
30
 
28
31
  def is?(type)
@@ -80,6 +83,27 @@ module Contrek
80
83
  end
81
84
  end
82
85
 
86
+ def try_transmutation!
87
+ head_queues = head.end_point.queues
88
+ return if head_queues.size == 1
89
+ other_head_part = head_queues.find { |part| part.polyline.tile == polyline.tile && part != self }
90
+ if other_head_part
91
+ tail_queues = tail.end_point.queues
92
+ if tail_queues.find { |part| part == other_head_part }
93
+ if (other_head_part.tail.payload[:y] == tail.payload[:y] && other_head_part.head.payload[:y] == head.payload[:y]) ||
94
+ (other_head_part.tail.payload[:y] == head.payload[:y] && other_head_part.head.payload[:y] == tail.payload[:y])
95
+ if self.next.nil? && other_head_part.prev.nil?
96
+ self.mirror = true
97
+ end
98
+ else
99
+ self.type = Part::EXCLUSIVE
100
+ self.trasmuted = true
101
+ other_head_part.transmutation_skip = true
102
+ end
103
+ end
104
+ end
105
+ end
106
+
83
107
  def within?(other)
84
108
  self_min, self_max = [head.payload[:y], tail.payload[:y]].minmax
85
109
  other_min, other_max = [other.head.payload[:y], other.tail.payload[:y]].minmax
@@ -8,6 +8,8 @@ module Contrek
8
8
  def initialize(*args, **kwargs, &block)
9
9
  super
10
10
  @parts = []
11
+ @first_seam = nil
12
+ @last_seam = nil
11
13
  end
12
14
 
13
15
  def add_part(new_part)
@@ -16,8 +18,14 @@ module Contrek
16
18
  last.next = last.circular_next = new_part if last
17
19
  new_part.circular_next = @parts.first
18
20
  new_part.prev = last
19
-
20
- new_part.orient! if new_part.is?(Part::SEAM)
21
+ if new_part.is?(Part::SEAM)
22
+ @first_seam ||= new_part
23
+ if !@last_seam.nil?
24
+ @last_seam.next_seam = new_part
25
+ end
26
+ @last_seam = new_part
27
+ new_part.orient!
28
+ end
21
29
  end
22
30
 
23
31
  def inspect_parts
@@ -27,6 +35,8 @@ module Contrek
27
35
  def partition!
28
36
  current_part = nil
29
37
  @parts = []
38
+ @first_seam = nil
39
+ @last_seam = nil
30
40
 
31
41
  @raw.each_with_index do |position, n|
32
42
  if @tile.tg_border?(position)
@@ -49,7 +59,7 @@ module Contrek
49
59
  end
50
60
  add_part(current_part)
51
61
 
52
- trasmute_parts!
62
+ transmute_parts!
53
63
  end
54
64
 
55
65
  private
@@ -57,42 +67,37 @@ module Contrek
57
67
  # If there are SEAM parts and one is canceled out by another within the same polyline,
58
68
  # meaning that all its points are repeated in another, longer sequence,
59
69
  # then the shorter one is converted to EXCLUSIVE and marked as transmuted
60
- def trasmute_parts!
70
+ def transmute_parts!
61
71
  transpose = tile.cluster.finder.transpose?
62
-
63
- @parts.each do |inside|
64
- next unless inside.is?(Part::SEAM)
65
- @parts.each do |inside_compare|
66
- next if inside == inside_compare
67
- next unless inside_compare.is?(Part::SEAM)
68
-
72
+ if (current_seam = @first_seam)
73
+ loop do
69
74
  if transpose
70
- if inside.within?(inside_compare)
71
- if !inside.same_length?(inside_compare)
72
- inside.type = Part::EXCLUSIVE
73
- inside.trasmuted = true
74
- inside.head.end_point.queues.delete(inside)
75
- inside.tail.end_point.queues.delete(inside)
76
- break
77
- end
78
- end
79
- else
80
- count = 0
81
- inside.each do |position|
82
- inclusion = position.end_point.queues.include?(inside_compare)
83
- count += 1 if inclusion
84
- end
85
- if count == inside.size
86
- if count < inside_compare.size
87
- inside.type = Part::EXCLUSIVE
88
- inside.trasmuted = true
89
- break
90
- end
91
- if count == inside_compare.size && inside.next.nil? && inside_compare.prev.nil?
92
- inside.mirror = true
75
+ transmute_transposed_part(current_seam)
76
+ elsif !current_seam.transmutation_skip
77
+ current_seam.try_transmutation!
78
+ end
79
+ current_seam = current_seam.next_seam
80
+ break if current_seam.nil?
81
+ end
82
+ end
83
+ end
84
+
85
+ def transmute_transposed_part(part)
86
+ if (current_seam = @first_seam)
87
+ loop do
88
+ if current_seam != part
89
+ if part.within?(current_seam)
90
+ if !part.same_length?(current_seam)
91
+ part.type = Part::EXCLUSIVE
92
+ part.trasmuted = true
93
+ part.head.end_point.queues.delete(part)
94
+ part.tail.end_point.queues.delete(part)
95
+ return
93
96
  end
94
97
  end
95
98
  end
99
+ current_seam = current_seam.next_seam
100
+ break if current_seam.nil?
96
101
  end
97
102
  end
98
103
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Contrek
4
- VERSION = "1.3.0"
4
+ VERSION = "1.3.1"
5
5
  end
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.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-17 00:00:00.000000000 Z
11
+ date: 2026-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -307,7 +307,7 @@ metadata:
307
307
  homepage_uri: https://github.com/runout77/contrek
308
308
  documentation_uri: https://github.com/runout77/contrek#readme
309
309
  changelog_uri: https://github.com/runout77/contrek/blob/main/CHANGELOG.md
310
- post_install_message:
310
+ post_install_message:
311
311
  rdoc_options: []
312
312
  require_paths:
313
313
  - lib
@@ -323,7 +323,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
323
323
  version: '0'
324
324
  requirements: []
325
325
  rubygems_version: 3.5.22
326
- signing_key:
326
+ signing_key:
327
327
  specification_version: 4
328
328
  summary: Fast PNG contour tracing and shape detection for Ruby
329
329
  test_files: []