contrek 1.4.3 → 1.4.4

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: 802afd86666e1705cc50240c6f7d79abfd9c7e93c315b1222199e79836fdd9e2
4
- data.tar.gz: e9f593c3ef9a661720f2587b44d65406839b25c2bc1690b8c4bfae477f967de6
3
+ metadata.gz: a2ca1b703822f8170679cfef42bdc5665fca4dcbeb49e0994efb78c1d325ff94
4
+ data.tar.gz: b64cf8c4f9110bcbac96e01c86fb4b923a19d342eefd11e56933fdfdde1889fd
5
5
  SHA512:
6
- metadata.gz: 8e4d8940a0f6cb93fc0ee685808c8889110bedbfd604fb8de3e3aca6b0fb9caa69d57bec518cc50ce15314a6ffa45e525fc947b60d52d71403510b6c77b7302f
7
- data.tar.gz: e12d7f30b9dec7ee474ab5c400f86f8331cc8eaa8e1571e6c8b895ddea4a4f0633806d2ccb0b4fdd12380d77caa714c4b6c85b9f347e3bf6ff70f39b0a1cb9d9
6
+ metadata.gz: 0e3e13330789b1d83025882e2179e54b2389092205ce9d0c5af8b7ba770118a1c585283f1367c15799c4ad3bb06ea1e97c6e15262c33ea229ef169fd343b03c1
7
+ data.tar.gz: fd08b11d3a2604724960d0bbd2e24516e7136f39cacb304df970c5c96670800f8863b561a85f4c64ff14d0a0c24a5b3d45f7a93070161f7f94ac97148e414d6b
data/CHANGELOG.md CHANGED
@@ -161,4 +161,9 @@ All notable changes to this project will be documented in this file.
161
161
  - Contrek takes care of the streaming process, providing an API that greatly simplifies the implementation of streaming raster processing.
162
162
 
163
163
  ## [1.4.3] - 2026-09-09
164
- - Added TIFF/GeoTIFF streaming support with geolocation metadata and direct WGS84 GeoJSON output.
164
+ - Added TIFF/GeoTIFF streaming support with geolocation metadata and direct WGS84 GeoJSON output.
165
+
166
+ ## [1.4.4] - 2026-09-19
167
+ - Fixed `VerticalMerger` handling of polygon orientation and starting points.
168
+ - Added `detach()` to `RawBitmap` on the C++ side.
169
+ - Added an OpenCV coordinate converter helper for converting contours to Contrek-compatible polygons.
@@ -8,13 +8,14 @@
8
8
  */
9
9
 
10
10
  #include "RawBitmap.h"
11
- #include "spng.h"
12
11
  #include <memory>
13
12
  #include <cstdio>
14
13
  #include <iostream>
15
14
  #include <cstring>
16
15
  #include <cerrno>
17
16
  #include <string>
17
+ #include <utility>
18
+ #include "spng.h"
18
19
 
19
20
  RawBitmap::RawBitmap() : Bitmap("", 0),
20
21
  width(0),
@@ -169,3 +170,16 @@ bool RawBitmap::save_to_png(const std::string& filename) {
169
170
 
170
171
  return (ret == 0);
171
172
  }
173
+
174
+ std::unique_ptr<RawBitmap> RawBitmap::detach() {
175
+ auto detached = std::make_unique<RawBitmap>();
176
+
177
+ detached->width = width;
178
+ detached->height = height;
179
+ detached->bpp = bpp;
180
+ detached->image = std::move(image);
181
+
182
+ define(width, height, bpp, false);
183
+
184
+ return detached;
185
+ }
@@ -32,6 +32,7 @@ class RawBitmap : public Bitmap {
32
32
  void fill(unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255);
33
33
  void draw_line(int x0, int y0, int x1, int y1, unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255);
34
34
  void draw_filled_rectangle(int x, int y, int w, int h, unsigned char r, unsigned char g, unsigned char b, unsigned char a = 255);
35
+ std::unique_ptr<RawBitmap> detach();
35
36
 
36
37
  protected:
37
38
  std::unique_ptr<unsigned char[]> image;
@@ -40,17 +40,10 @@ class RasterStreamer {
40
40
  const uint32_t bytes_per_pixel = source_.get_bytes_per_pixel();
41
41
  const std::size_t row_size = static_cast<std::size_t>(width) * static_cast<std::size_t>(bytes_per_pixel);
42
42
  uint32_t source_row = 0;
43
- uint32_t previous_buffer_rows = 0;
44
43
  bool first_stripe = true;
44
+ std::vector<unsigned char> overlap_row(row_size);
45
45
 
46
46
  while (source_row < height) {
47
- std::vector<unsigned char> overlap_row;
48
- if (!first_stripe) {
49
- overlap_row.resize(row_size);
50
- const unsigned char* previous_last_row = buffer.get_row_ptr(previous_buffer_rows - 1);
51
- std::memcpy(overlap_row.data(), previous_last_row, row_size);
52
- }
53
-
54
47
  const uint32_t overlap_rows = first_stripe ? 0 : OVERLAP;
55
48
  const uint32_t available_rows = stripe_height_ - overlap_rows;
56
49
  const uint32_t remaining_rows = height - source_row;
@@ -80,8 +73,10 @@ class RasterStreamer {
80
73
  }
81
74
  const std::size_t buffer_size = static_cast<std::size_t>(buffer_rows) * row_size;
82
75
 
76
+ const unsigned char* last_row = buffer.get_row_ptr(buffer_rows - 1);
77
+ std::memcpy(overlap_row.data(), last_row, row_size);
78
+
83
79
  std::forward<Callback>(callback)(buffer, buffer_rows, buffer_size, rows_read);
84
- previous_buffer_rows = buffer_rows;
85
80
  first_stripe = false;
86
81
  }
87
82
  }
@@ -154,6 +154,15 @@ struct ProcessResult {
154
154
  to_svg_stream(file);
155
155
  file.close();
156
156
  }
157
+
158
+ void sort_polygons() {
159
+ polygons.sort([](const Polygon& a, const Polygon& b) {
160
+ if (a.bounds.min_y != b.bounds.min_y) {
161
+ return a.bounds.min_y < b.bounds.min_y;
162
+ }
163
+ return a.bounds.min_x < b.bounds.min_x;
164
+ });
165
+ }
157
166
  };
158
167
 
159
168
  class PolygonFinder {
@@ -14,6 +14,9 @@
14
14
 
15
15
  Merger::Merger(int number_of_threads, const Options& options)
16
16
  : Finder(number_of_threads, options) {
17
+ if (options.contains("versus")) {
18
+ std::cerr << "[Contrek WARNING] The given options 'versus' will be ignored." << std::endl;
19
+ }
17
20
  if (!this->safe()) {
18
21
  std::cerr << "[Contrek WARNING] Processing tile with 'unsafe_mode: true'. "
19
22
  << "Incompatible result options might lead to unexpected vector geometry.\n";
@@ -58,7 +61,9 @@ void Merger::add_tile(ProcessResult& result)
58
61
 
59
62
  ProcessResult* Merger::process_info() {
60
63
  this->process_tiles();
61
- return(Finder::process_info());
64
+ ProcessResult* result = Finder::process_info();
65
+ result->versus = this->versus;
66
+ return result;
62
67
  }
63
68
 
64
69
  void Merger::translate(ProcessResult& result, int offset) {
@@ -20,10 +20,10 @@ VerticalMerger::VerticalMerger(int number_of_threads, const Options& options)
20
20
 
21
21
  void VerticalMerger::add_tile(ProcessResult& result)
22
22
  { transpose(result);
23
+ adjust(result);
23
24
  if (this->tiles_.size() > 0) {
24
25
  translate(result, this->current_x);
25
26
  }
26
- adjust(result);
27
27
  Merger::add_tile(result);
28
28
  }
29
29
 
@@ -50,14 +50,32 @@ void VerticalMerger::transpose(ProcessResult& result) {
50
50
  }
51
51
 
52
52
  void VerticalMerger::adjust(ProcessResult& result) {
53
+ const int tile_width = result.width;
54
+ const auto number_of_tiles = this->tiles_.size();
55
+
53
56
  for (auto& polygon : result.polygons) {
54
- if (!polygon.outer.empty()) {
55
- std::rotate(polygon.outer.begin(), polygon.outer.begin() + 1, polygon.outer.end());
57
+ const auto& bounds = polygon.bounds;
58
+ const bool needs_left = number_of_tiles > 0 && bounds.min_x == 0;
59
+ const bool needs_right = bounds.max_x == tile_width;
60
+
61
+ if (!needs_left && !needs_right) {
62
+ continue;
56
63
  }
57
- for (auto& sequence : polygon.inner) {
58
- if (!sequence.empty()) {
59
- std::rotate(sequence.begin(), sequence.begin() + 1, sequence.end());
64
+
65
+ auto& sequence = polygon.outer;
66
+ auto best = sequence.end();
67
+ for (auto it = sequence.begin(); it != sequence.end(); ++it) {
68
+ if (it->y != bounds.min_y) {
69
+ continue;
70
+ }
71
+ if (best == sequence.end() ||
72
+ (result.versus == Node::A ? it->x > best->x : it->x < best->x)) {
73
+ best = it;
60
74
  }
61
75
  }
76
+
77
+ if (best != sequence.end() && best != sequence.begin()) {
78
+ std::rotate(sequence.begin(), best, sequence.end());
79
+ }
62
80
  }
63
81
  }
@@ -6,13 +6,18 @@ module Contrek
6
6
  attr_reader :tiles
7
7
  prepend Poolable
8
8
 
9
+ IGNORE_OPTIONS = [:versus]
10
+
9
11
  def initialize(options: {})
10
12
  @initialize_time = 0
11
13
  @current_x = 0
12
14
  @tiles = Queue.new
13
15
  @whole_tile = nil
14
16
  @user_options = options
15
- @options = {unsafe_mode: false}.merge(@user_options)
17
+ if (warn_options = @user_options.keys & IGNORE_OPTIONS).any?
18
+ warn "[Contrek WARNING] The given options '#{warn_options.join(",")}' will be ignored."
19
+ end
20
+ @options = {unsafe_mode: false}.merge(@user_options).except(*IGNORE_OPTIONS)
16
21
  unless safe?
17
22
  warn "[Contrek WARNING] Processing tile with 'unsafe_mode: true'. Incompatible result options might lead to unexpected vector geometry."
18
23
  end
@@ -5,10 +5,10 @@ module Contrek
5
5
  class VerticalMerger < Merger
6
6
  def add_tile(result)
7
7
  transpose(result)
8
+ adjust(result)
8
9
  if @tiles.size > 0
9
10
  translate(result, @current_x)
10
11
  end
11
- adjust(result)
12
12
  super
13
13
  end
14
14
 
@@ -22,12 +22,32 @@ module Contrek
22
22
 
23
23
  private
24
24
 
25
+ # After transposing the polygon, select the correct starting vertex according to its versus.
26
+ # On the topmost scanline, use the rightmost vertex for counterclockwise polygons and the leftmost
27
+ # vertex for clockwise polygons.
28
+ # TODO: is useless the really rotate coords. Probably you can save rotation index here computed
29
+ # using later in partition() to get the starting point sequence parts begins
25
30
  def adjust(result)
31
+ versus = result.metadata[:versus]
32
+ tile_width = result.metadata[:width]
33
+ number_of_tiles = @tiles.size
34
+
26
35
  result.polygons.each do |polygon|
27
- polygon[:outer].rotate!(1)
28
- polygon[:inner].each do |sequence|
29
- sequence.rotate!(1)
36
+ bounds = polygon[:bounds]
37
+ needs_left = number_of_tiles > 0 && bounds[:min_x] == 0
38
+ needs_right = bounds[:max_x] == tile_width
39
+ next unless needs_left || needs_right
40
+ sequence = polygon[:outer]
41
+ index = nil
42
+ best_x = nil
43
+ sequence.each_with_index do |point, i|
44
+ next unless point[:y] == bounds[:min_y]
45
+ if index.nil? || ((versus == :a) ? point[:x] > best_x : point[:x] < best_x)
46
+ index = i
47
+ best_x = point[:x]
48
+ end
30
49
  end
50
+ sequence.rotate!(index) if index && index != 0
31
51
  end
32
52
  end
33
53
 
@@ -8,6 +8,12 @@ module Contrek
8
8
  def points
9
9
  polygons
10
10
  end
11
+
12
+ def sort_polygons!
13
+ polygons.sort_by! do |polygon|
14
+ [polygon[:bounds][:min_y], polygon[:bounds][:min_x]]
15
+ end
16
+ end
11
17
  end
12
18
  end
13
19
  end
@@ -0,0 +1,144 @@
1
+ module Contrek
2
+ module Shared
3
+ class OpencvConverter
4
+ CHAIN_DIR = [
5
+ [3, 4, 5],
6
+ [2, -1, 6],
7
+ [1, 0, 7]
8
+ ].freeze
9
+
10
+ CORNER_X = [0, 1, 1, 0].freeze
11
+ CORNER_Y = [1, 1, 0, 0].freeze
12
+
13
+ def contour_to_cell_boundary(contour, rect)
14
+ raw = []
15
+ n = contour.size
16
+
17
+ return raw if n == 0
18
+ if n == 1
19
+ x = contour[0][:x]
20
+ y = contour[0][:y]
21
+ return [
22
+ {x: x, y: y + 1},
23
+ {x: x + 1, y: y + 1},
24
+ {x: x + 1, y: y},
25
+ {x: x, y: y}
26
+ ]
27
+ end
28
+
29
+ n.times do |i|
30
+ previous = contour[(i - 1) % n]
31
+ current = contour[i]
32
+ next_p = contour[(i + 1) % n]
33
+
34
+ incoming = chain_direction(previous, current)
35
+ outgoing = chain_direction(current, next_p)
36
+
37
+ corner = incoming >> 1
38
+ last_corner = ((outgoing + 1) >> 1) & 3
39
+
40
+ loop do
41
+ raw << {
42
+ x: current[:x] + CORNER_X[corner],
43
+ y: current[:y] + CORNER_Y[corner]
44
+ }
45
+ break if corner == last_corner
46
+ corner = (corner + 1) & 3
47
+ end
48
+ end
49
+
50
+ # build path parts
51
+ edges = []
52
+ raw.size.times do |i|
53
+ a = raw[i]
54
+ b = raw[(i + 1) % raw.size]
55
+ next if a == b
56
+
57
+ key = [
58
+ [a[:x], a[:y]],
59
+ [b[:x], b[:y]]
60
+ ].sort
61
+ edges << {a: a, b: b, key: key}
62
+ end
63
+
64
+ counts = Hash.new(0)
65
+ edges.each do |edge|
66
+ counts[edge[:key]] += 1
67
+ end
68
+
69
+ kept = {}
70
+ edges.select! do |edge|
71
+ key = edge[:key]
72
+ next false if counts[key].even?
73
+ next false if kept[key]
74
+
75
+ kept[key] = true
76
+ true
77
+ end
78
+ return [] if edges.empty?
79
+
80
+ outgoing = Hash.new { |h, k| h[k] = [] }
81
+ edges.each_with_index do |edge, i|
82
+ outgoing[[edge[:a][:x], edge[:a][:y]]] << i
83
+ end
84
+
85
+ used = Array.new(edges.size, false)
86
+ result = []
87
+ current = 0
88
+
89
+ loop do
90
+ break if used.all?
91
+ if used[current]
92
+ current = used.index(false)
93
+ break unless current
94
+ end
95
+
96
+ edge = edges[current]
97
+ used[current] = true
98
+ result << edge[:a]
99
+ finish = edge[:b]
100
+ candidates = outgoing[[finish[:x], finish[:y]]]
101
+ next_edge = candidates.find { |index| !used[index] }
102
+
103
+ if next_edge
104
+ current = next_edge
105
+ else
106
+ current = used.index(false)
107
+ break unless current
108
+ end
109
+ end
110
+ rotate_to_scanline_start(result, rect)
111
+ end
112
+
113
+ def rect_to_bounds(rect)
114
+ {min_x: rect[:x],
115
+ max_x: rect[:x] + rect[:width],
116
+ min_y: rect[:y],
117
+ max_y: rect[:y] + rect[:height]}
118
+ end
119
+
120
+ private
121
+
122
+ def rotate_to_scanline_start(points, rect)
123
+ return points if points.empty?
124
+
125
+ min_y = rect[:y]
126
+ min_x = points.select { |p| p[:y] == min_y }.map { |p| p[:x] }.min
127
+ index = points.index { |p| p[:x] == min_x && p[:y] == min_y }
128
+ (index && index > 0) ? points.rotate(index) : points
129
+ end
130
+
131
+ def chain_direction(from, to)
132
+ dx = to[:x] - from[:x]
133
+ dy = to[:y] - from[:y]
134
+
135
+ raise ArgumentError, "Zero-length contour step" if dx == 0 && dy == 0
136
+ if dx.abs > 1 || dy.abs > 1
137
+ raise ArgumentError, "Non-adjacent contour step"
138
+ end
139
+
140
+ CHAIN_DIR[dx + 1][dy + 1]
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Contrek
4
- VERSION = "1.4.3"
4
+ VERSION = "1.4.4"
5
5
  end
data/lib/contrek.rb CHANGED
@@ -17,6 +17,7 @@ require "contrek/bitmaps/streaming/sources/ascii"
17
17
  require "contrek/bitmaps/streaming/sources/png"
18
18
  require "contrek/bitmaps/streaming/sources/tiff"
19
19
  require "contrek/shared/result"
20
+ require "contrek/shared/opencv_converter"
20
21
  require "contrek/finder/bounds"
21
22
  require "contrek/finder/list"
22
23
  require "contrek/finder/list_entry"
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.3
4
+ version: 1.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuele Cesaroni
@@ -333,6 +333,7 @@ files:
333
333
  - lib/contrek/reducers/reducer.rb
334
334
  - lib/contrek/reducers/uniq_reducer.rb
335
335
  - lib/contrek/reducers/visvalingam_reducer.rb
336
+ - lib/contrek/shared/opencv_converter.rb
336
337
  - lib/contrek/shared/result.rb
337
338
  - lib/contrek/version.rb
338
339
  homepage: https://github.com/runout77/contrek