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 +4 -4
- data/CHANGELOG.md +6 -1
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/RawBitmap.cpp +15 -1
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/RawBitmap.h +1 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/bitmaps/streaming/RasterStreamer.h +4 -9
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/PolygonFinder.h +9 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.cpp +6 -1
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.cpp +24 -6
- data/lib/contrek/finder/concurrent/merger.rb +6 -1
- data/lib/contrek/finder/concurrent/vertical_merger.rb +24 -4
- data/lib/contrek/finder/result.rb +6 -0
- data/lib/contrek/shared/opencv_converter.rb +144 -0
- data/lib/contrek/version.rb +1 -1
- data/lib/contrek.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2ca1b703822f8170679cfef42bdc5665fca4dcbeb49e0994efb78c1d325ff94
|
|
4
|
+
data.tar.gz: b64cf8c4f9110bcbac96e01c86fb4b923a19d342eefd11e56933fdfdde1889fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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[:
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
|
@@ -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
|
data/lib/contrek/version.rb
CHANGED
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.
|
|
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
|