contrek 1.4.3 → 1.4.5
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 +9 -1
- data/Gemfile.lock +1 -1
- data/README.md +62 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/Tests.cpp +7 -3
- 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/GeoJsonStreamingMerger.h +1 -1
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.cpp +37 -1
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/Merger.h +4 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/VerticalMerger.cpp +24 -6
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/shared/OpencvConverter.cpp +51 -0
- data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/shared/OpencvConverter.h +205 -0
- data/ext/cpp_polygon_finder/cpp_polygon_finder.cpp +1 -0
- data/lib/contrek/finder/concurrent/merger.rb +31 -2
- 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 +146 -0
- data/lib/contrek/version.rb +1 -1
- data/lib/contrek.rb +1 -1
- metadata +4 -2
- data/lib/contrek/map/mercator_projection.rb +0 -78
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9350d60698a69dda805ede85e42b80fcc38022a361e52674fd2780d696e0f9ff
|
|
4
|
+
data.tar.gz: '08d164d2e53bb3c96e7212ac6bc9d46bb673de8f10bb950d43f3d66d0c86d793'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c6d0b73ab8affa0ed17aa3fd803f7aed81cec846db6edd3de5dfcec8103bf23c9e23484819c4a981a4cb0c46da18f0b169122124213a9b4417df108f54c468a
|
|
7
|
+
data.tar.gz: 57817011679288782915a50ca1adf4155b71732ab73e330e2ac696ea504d7204c08935a6b83487000faac68b37f38aff5b5761d78fc85aab7703752f2700d65f
|
data/CHANGELOG.md
CHANGED
|
@@ -161,4 +161,12 @@ 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.
|
|
170
|
+
|
|
171
|
+
## [1.4.5] - 2026-10-11
|
|
172
|
+
- Added tile boundary validation for external geometries, with support for bypassing checks via `unsafe_mode: true`.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -676,6 +676,68 @@ result = Contrek.contour!(
|
|
|
676
676
|
|
|
677
677
|
The choice mainly affects how diagonally touching regions are interpreted.
|
|
678
678
|
|
|
679
|
+
## Merging External Geometry
|
|
680
|
+
|
|
681
|
+
Contrek's merger can process polygon data produced by external contour
|
|
682
|
+
extractors, provided that the input geometry follows the requirements below.
|
|
683
|
+
|
|
684
|
+
### The Laws of the Tiles
|
|
685
|
+
|
|
686
|
+
1. **Adjacent tiles must share an overlap and have identical exposed boundary sequences.**
|
|
687
|
+
Adjacent tiles must share a single scanline of overlap. The exposed contour
|
|
688
|
+
segments on their shared boundary must have identical coordinate sequences,
|
|
689
|
+
including all intermediate vertices, expressed in the same coordinate system.
|
|
690
|
+
Any mismatch between the two sequences makes the tiles incompatible for merging.
|
|
691
|
+
|
|
692
|
+
2. **Coordinates must describe cell boundaries.**
|
|
693
|
+
A cell is the square area occupied by a pixel in the raster. Coordinates
|
|
694
|
+
must describe the boundaries of these cells, rather than representing a
|
|
695
|
+
pixel by its center or by a single vertex.
|
|
696
|
+
|
|
697
|
+
3. **Contours must follow cell boundaries.**
|
|
698
|
+
Contour segments supplied to the merger must follow the horizontal and
|
|
699
|
+
vertical edges of the raster cells. Diagonal segments are not allowed in
|
|
700
|
+
the geometry passed to the merger.
|
|
701
|
+
|
|
702
|
+
4. **Polygon orientation (`versus`) must be consistent across all tiles.**
|
|
703
|
+
All polygons must follow the orientation declared by the result, either
|
|
704
|
+
clockwise or counterclockwise. The same orientation must be used by every
|
|
705
|
+
tile submitted to the merger.
|
|
706
|
+
|
|
707
|
+
5. **Every contour must start at the canonical point of its first scanline.**
|
|
708
|
+
A *scanline* is a horizontal raster row scanned from left to right, like
|
|
709
|
+
the electron beam of a CRT display. Consider the topmost scanline
|
|
710
|
+
intersecting the contour. On that scanline, a **counterclockwise** contour
|
|
711
|
+
must start from the **leftmost** point, while a **clockwise** contour must
|
|
712
|
+
start from the **rightmost** point.
|
|
713
|
+
|
|
714
|
+
6. **The bounding box must match the contour geometry.**
|
|
715
|
+
`min_x`, `max_x`, `min_y`, and `max_y` must correctly describe the spatial
|
|
716
|
+
extent of the contour.
|
|
717
|
+
|
|
718
|
+
7. **Tile metadata must match the supplied geometry.**
|
|
719
|
+
`width`, `height`, and `versus` must correctly describe the tile. In
|
|
720
|
+
particular, `height` must represent the actual height of the result.
|
|
721
|
+
|
|
722
|
+
8. **Polygons must be sorted in raster scan order.**
|
|
723
|
+
Polygons must be ordered by increasing **Y**, and then by increasing **X**
|
|
724
|
+
when they have the same Y: top to bottom, and left to right at the same
|
|
725
|
+
vertical position.
|
|
726
|
+
|
|
727
|
+
### Boundary Validation
|
|
728
|
+
|
|
729
|
+
By default, Contrek validates the shared boundaries of adjacent tiles when
|
|
730
|
+
merging externally supplied geometries. Boundary coordinates must match
|
|
731
|
+
exactly, including intermediate vertices. Otherwise, the merger raises a
|
|
732
|
+
`Tile boundary mismatch` error.
|
|
733
|
+
|
|
734
|
+
This validation can be disabled by setting `unsafe_mode: true`, avoiding
|
|
735
|
+
validation overhead when the input geometries are known to be valid.
|
|
736
|
+
|
|
737
|
+
**Important:** Boundary validation does not verify all the Laws of the Tiles.
|
|
738
|
+
Users supplying external geometries remain responsible for satisfying every
|
|
739
|
+
requirement listed above.
|
|
740
|
+
|
|
679
741
|
# Results
|
|
680
742
|
|
|
681
743
|
The tracing result contains two kinds of information:
|
|
@@ -294,7 +294,9 @@ void Tests::test_h()
|
|
|
294
294
|
PolygonFinder pl_right(&b_right, &matcher, nullptr, options);
|
|
295
295
|
ProcessResult *right_result = pl_right.process_info();
|
|
296
296
|
|
|
297
|
-
Options merger_options = {
|
|
297
|
+
Options merger_options = {
|
|
298
|
+
{"unsafe_mode", true}
|
|
299
|
+
};
|
|
298
300
|
HorizontalMerger hmerger(1, merger_options);
|
|
299
301
|
hmerger.add_tile(*left_result);
|
|
300
302
|
hmerger.add_tile(*right_result);
|
|
@@ -326,7 +328,9 @@ double get_peak_rss() {
|
|
|
326
328
|
|
|
327
329
|
void stream_png_image(const std::string& filepath, uint32_t stripe_height, bool generate_svg = false, bool generate_png = false) {
|
|
328
330
|
std::vector<ProcessResult*> result_clones;
|
|
329
|
-
Options options = {
|
|
331
|
+
Options options = {
|
|
332
|
+
{"unsafe_mode", true}
|
|
333
|
+
};
|
|
330
334
|
VerticalMerger vmerger(0, options);
|
|
331
335
|
|
|
332
336
|
PngSource source(filepath);
|
|
@@ -409,7 +413,7 @@ void stream_progressive_png_image(const std::string& filepath, uint32_t stripe_h
|
|
|
409
413
|
|
|
410
414
|
SvgStreamingMerger merger(
|
|
411
415
|
0,
|
|
412
|
-
{{"bounds", true}},
|
|
416
|
+
{{"bounds", true},{"unsafe_mode", true}},
|
|
413
417
|
&stream,
|
|
414
418
|
source.width(),
|
|
415
419
|
source.height());
|
|
@@ -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 {
|
data/ext/cpp_polygon_finder/PolygonFinder/src/polygon/finder/concurrent/GeoJsonStreamingMerger.h
CHANGED
|
@@ -34,7 +34,7 @@ class GeoJsonStreamingMerger : public StreamingMerger {
|
|
|
34
34
|
if (!transform || !crs) throw std::invalid_argument("Invalid geo_localization.");
|
|
35
35
|
|
|
36
36
|
const std::string authority = crs->get<std::string>("authority", "");
|
|
37
|
-
const auto code = crs->get("code",
|
|
37
|
+
const auto code = crs->get("code", std::int64_t{0});
|
|
38
38
|
if (authority != "EPSG" || code != 4326) throw std::invalid_argument("Unsupported CRS: " + authority + ":" + std::to_string(code));
|
|
39
39
|
const double x_origin = transform->get<double>("x_origin", 0.0);
|
|
40
40
|
const double y_origin = transform->get<double>("y_origin", 0.0);
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
#include <iostream>
|
|
11
11
|
#include <string>
|
|
12
12
|
#include <vector>
|
|
13
|
+
#include <algorithm>
|
|
14
|
+
#include <list>
|
|
13
15
|
#include "Merger.h"
|
|
14
16
|
|
|
15
17
|
Merger::Merger(int number_of_threads, const Options& options)
|
|
16
18
|
: Finder(number_of_threads, options) {
|
|
19
|
+
if (options.contains("versus")) {
|
|
20
|
+
std::cerr << "[Contrek WARNING] The given options 'versus' will be ignored." << std::endl;
|
|
21
|
+
}
|
|
17
22
|
if (!this->safe()) {
|
|
18
23
|
std::cerr << "[Contrek WARNING] Processing tile with 'unsafe_mode: true'. "
|
|
19
24
|
<< "Incompatible result options might lead to unexpected vector geometry.\n";
|
|
@@ -46,19 +51,26 @@ void Merger::add_tile(ProcessResult& result)
|
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
}
|
|
54
|
+
if (safe()) validate(result);
|
|
49
55
|
|
|
50
56
|
int end_x = this->current_x + result.width;
|
|
51
57
|
Tile* tile = new Tile(this, this->current_x, end_x, std::to_string(tiles_.size()), tiles_.size(), Benchmarks {0, 0});
|
|
52
58
|
tile->assign_raw_polygons(result.polygons, result.treemap);
|
|
53
59
|
tiles_.queue_push(tile);
|
|
54
60
|
|
|
61
|
+
if (safe()) {
|
|
62
|
+
previous_boundary_ = boundary_coordinates(result.polygons, end_x);
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
this->maximum_width_ = end_x;
|
|
56
66
|
this->current_x = end_x - 1;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
ProcessResult* Merger::process_info() {
|
|
60
70
|
this->process_tiles();
|
|
61
|
-
|
|
71
|
+
ProcessResult* result = Finder::process_info();
|
|
72
|
+
result->versus = this->versus;
|
|
73
|
+
return result;
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
void Merger::translate(ProcessResult& result, int offset) {
|
|
@@ -71,3 +83,27 @@ void Merger::translate(ProcessResult& result, int offset) {
|
|
|
71
83
|
polygon.bounds.max_x += offset;
|
|
72
84
|
}
|
|
73
85
|
}
|
|
86
|
+
|
|
87
|
+
std::vector<int> Merger::boundary_coordinates(const std::list<Polygon>& polygons, int boundary_x) {
|
|
88
|
+
std::vector<int> coordinates;
|
|
89
|
+
|
|
90
|
+
for (const auto& polygon : polygons) {
|
|
91
|
+
for (const auto& point : polygon.outer) {
|
|
92
|
+
if (point.x == boundary_x) {
|
|
93
|
+
coordinates.push_back(point.y);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
std::sort(coordinates.begin(), coordinates.end());
|
|
99
|
+
return coordinates;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
void Merger::validate(const ProcessResult& result) {
|
|
103
|
+
if (tiles_.size() == 0) return;
|
|
104
|
+
|
|
105
|
+
const auto current = boundary_coordinates(result.polygons, current_x);
|
|
106
|
+
if (previous_boundary_ != current) {
|
|
107
|
+
throw std::invalid_argument("Tile boundary mismatch: adjacent tiles have different edge coordinates");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
#pragma once
|
|
11
11
|
#include <string>
|
|
12
12
|
#include <vector>
|
|
13
|
+
#include <list>
|
|
13
14
|
#include "Finder.h"
|
|
14
15
|
#include "../Options.h"
|
|
15
16
|
|
|
@@ -23,4 +24,7 @@ class Merger : public Finder {
|
|
|
23
24
|
protected:
|
|
24
25
|
void translate(ProcessResult& result, int offset);
|
|
25
26
|
int current_x = 0;
|
|
27
|
+
void validate(const ProcessResult& result);
|
|
28
|
+
static std::vector<int> boundary_coordinates(const std::list<Polygon>& polygons, int boundary_x);
|
|
29
|
+
std::vector<int> previous_boundary_;
|
|
26
30
|
};
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OpencvConverter.cpp
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025-2026 Emanuele Cesaroni
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the GNU Affero General Public License v3 (AGPLv3).
|
|
7
|
+
* See the LICENSE file in this directory for the full license text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include "OpencvConverter.h"
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <functional>
|
|
14
|
+
#include <limits>
|
|
15
|
+
#include <utility>
|
|
16
|
+
#include <vector>
|
|
17
|
+
|
|
18
|
+
std::size_t OpencvConverter::EdgeKeyHash::operator()(const EdgeKey& key) const {
|
|
19
|
+
return std::hash<uint64_t>{}(key.a) ^
|
|
20
|
+
(std::hash<uint64_t>{}(key.b) << 1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
uint64_t OpencvConverter::point_key(const Point& p) {
|
|
24
|
+
return (static_cast<uint64_t>(static_cast<uint32_t>(p.x)) << 32) |
|
|
25
|
+
static_cast<uint32_t>(p.y);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
OpencvConverter::EdgeKey OpencvConverter::edge_key(const Point& a, const Point& b) {
|
|
29
|
+
uint64_t ka = point_key(a);
|
|
30
|
+
uint64_t kb = point_key(b);
|
|
31
|
+
if (ka > kb) {
|
|
32
|
+
std::swap(ka, kb);
|
|
33
|
+
}
|
|
34
|
+
return {ka, kb};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void OpencvConverter::rotate_to_scanline_start(std::vector<Point>& points, const RectBounds& bounds) {
|
|
38
|
+
if (points.empty()) return;
|
|
39
|
+
|
|
40
|
+
std::size_t index = points.size();
|
|
41
|
+
int min_x = std::numeric_limits<int>::max();
|
|
42
|
+
for (std::size_t i = 0; i < points.size(); ++i) {
|
|
43
|
+
if (points[i].y == bounds.min_y && points[i].x < min_x) {
|
|
44
|
+
min_x = points[i].x;
|
|
45
|
+
index = i;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (index != points.size() && index > 0) {
|
|
49
|
+
std::rotate(points.begin(), points.begin() + index, points.end());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OpencvConverter.h
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2025-2026 Emanuele Cesaroni
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the GNU Affero General Public License v3 (AGPLv3).
|
|
7
|
+
* See the LICENSE file in this directory for the full license text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#pragma once
|
|
11
|
+
|
|
12
|
+
#include <vector>
|
|
13
|
+
#include <unordered_map>
|
|
14
|
+
#include <unordered_set>
|
|
15
|
+
#include <cstdint>
|
|
16
|
+
#include <stdexcept>
|
|
17
|
+
#include <cstdlib>
|
|
18
|
+
|
|
19
|
+
#include "polygon/finder/Node.h"
|
|
20
|
+
#include "RectBounds.h"
|
|
21
|
+
|
|
22
|
+
class OpencvConverter {
|
|
23
|
+
private:
|
|
24
|
+
static constexpr int chain_dir[3][3] = {
|
|
25
|
+
{3, 4, 5},
|
|
26
|
+
{2, -1, 6},
|
|
27
|
+
{1, 0, 7}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
static constexpr int corner_x[4] = {0, 1, 1, 0};
|
|
31
|
+
static constexpr int corner_y[4] = {1, 1, 0, 0};
|
|
32
|
+
|
|
33
|
+
struct BoundaryEdge {
|
|
34
|
+
Point a;
|
|
35
|
+
Point b;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
struct EdgeKey {
|
|
39
|
+
uint64_t a;
|
|
40
|
+
uint64_t b;
|
|
41
|
+
bool operator==(const EdgeKey& other) const {
|
|
42
|
+
return a == other.a && b == other.b;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
struct EdgeKeyHash {
|
|
47
|
+
std::size_t operator()(const EdgeKey& key) const;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
static uint64_t point_key(const Point& p);
|
|
51
|
+
static EdgeKey edge_key(const Point& a, const Point& b);
|
|
52
|
+
static void rotate_to_scanline_start(std::vector<Point>& points, const RectBounds& bounds);
|
|
53
|
+
|
|
54
|
+
template<typename P>
|
|
55
|
+
static int chain_direction(const P& from, const P& to) {
|
|
56
|
+
const int dx = to.x - from.x;
|
|
57
|
+
const int dy = to.y - from.y;
|
|
58
|
+
|
|
59
|
+
if (dx == 0 && dy == 0)
|
|
60
|
+
throw std::invalid_argument("Zero-length contour step");
|
|
61
|
+
|
|
62
|
+
if (std::abs(dx) > 1 || std::abs(dy) > 1)
|
|
63
|
+
throw std::invalid_argument("Non-adjacent contour step");
|
|
64
|
+
|
|
65
|
+
return chain_dir[dx + 1][dy + 1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public:
|
|
69
|
+
template<typename P>
|
|
70
|
+
static std::vector<Point> contour_to_cell_boundary(const std::vector<P>& contour, const RectBounds& bounds) {
|
|
71
|
+
std::vector<Point> raw;
|
|
72
|
+
const std::size_t n = contour.size();
|
|
73
|
+
|
|
74
|
+
if (n == 0) {
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (n == 1) {
|
|
79
|
+
const int x = contour[0].x;
|
|
80
|
+
const int y = contour[0].y;
|
|
81
|
+
return {
|
|
82
|
+
Point(x, y + 1),
|
|
83
|
+
Point(x + 1, y + 1),
|
|
84
|
+
Point(x + 1, y),
|
|
85
|
+
Point(x, y)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
raw.reserve(n * 2);
|
|
90
|
+
|
|
91
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
92
|
+
const P& previous = contour[(i + n - 1) % n];
|
|
93
|
+
const P& current = contour[i];
|
|
94
|
+
const P& next = contour[(i + 1) % n];
|
|
95
|
+
const int incoming = chain_direction(previous, current);
|
|
96
|
+
const int outgoing = chain_direction(current, next);
|
|
97
|
+
int corner = incoming >> 1;
|
|
98
|
+
const int last_corner = ((outgoing + 1) >> 1) & 3;
|
|
99
|
+
for (;;) {
|
|
100
|
+
raw.emplace_back(current.x + corner_x[corner], current.y + corner_y[corner]);
|
|
101
|
+
if (corner == last_corner) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
corner = (corner + 1) & 3;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
std::vector<BoundaryEdge> edges;
|
|
109
|
+
edges.reserve(raw.size());
|
|
110
|
+
|
|
111
|
+
for (std::size_t i = 0; i < raw.size(); ++i) {
|
|
112
|
+
const Point& a = raw[i];
|
|
113
|
+
const Point& b = raw[(i + 1) % raw.size()];
|
|
114
|
+
if (a.x == b.x && a.y == b.y) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
edges.push_back({a, b});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
std::unordered_map<EdgeKey, int, EdgeKeyHash> counts;
|
|
121
|
+
|
|
122
|
+
for (const auto& edge : edges) {
|
|
123
|
+
++counts[edge_key(edge.a, edge.b)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
std::unordered_set<EdgeKey, EdgeKeyHash> kept;
|
|
127
|
+
std::vector<BoundaryEdge> filtered;
|
|
128
|
+
filtered.reserve(edges.size());
|
|
129
|
+
|
|
130
|
+
for (const auto& edge : edges) {
|
|
131
|
+
const EdgeKey key = edge_key(edge.a, edge.b);
|
|
132
|
+
if (((counts[key] & 1) == 0) ||
|
|
133
|
+
(kept.find(key) != kept.end())) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
kept.insert(key);
|
|
137
|
+
filtered.push_back(edge);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (filtered.empty()) {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
std::unordered_map<uint64_t, std::vector<std::size_t>> outgoing;
|
|
145
|
+
for (std::size_t i = 0; i < filtered.size(); ++i) {
|
|
146
|
+
outgoing[point_key(filtered[i].a)].push_back(i);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
std::vector<bool> used(filtered.size(), false);
|
|
150
|
+
std::size_t used_count = 0;
|
|
151
|
+
std::vector<Point> result;
|
|
152
|
+
result.reserve(filtered.size());
|
|
153
|
+
|
|
154
|
+
auto first_unused = [&]() {
|
|
155
|
+
for (std::size_t i = 0; i < used.size(); ++i) {
|
|
156
|
+
if (!used[i])
|
|
157
|
+
return i;
|
|
158
|
+
}
|
|
159
|
+
return used.size();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
std::size_t current = 0;
|
|
163
|
+
|
|
164
|
+
while (used_count < filtered.size()) {
|
|
165
|
+
if (used[current]) {
|
|
166
|
+
current = first_unused();
|
|
167
|
+
if (current == used.size()) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const BoundaryEdge& edge = filtered[current];
|
|
173
|
+
|
|
174
|
+
used[current] = true;
|
|
175
|
+
++used_count;
|
|
176
|
+
|
|
177
|
+
result.push_back(edge.a);
|
|
178
|
+
|
|
179
|
+
const uint64_t finish = point_key(edge.b);
|
|
180
|
+
|
|
181
|
+
auto it = outgoing.find(finish);
|
|
182
|
+
std::size_t next_edge = filtered.size();
|
|
183
|
+
|
|
184
|
+
if (it != outgoing.end()) {
|
|
185
|
+
for (std::size_t candidate : it->second) {
|
|
186
|
+
if (!used[candidate]) {
|
|
187
|
+
next_edge = candidate;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (next_edge != filtered.size()) {
|
|
194
|
+
current = next_edge;
|
|
195
|
+
} else {
|
|
196
|
+
current = first_unused();
|
|
197
|
+
if (current == used.size())
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
rotate_to_scanline_start(result, bounds);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
@@ -571,6 +571,7 @@ void Init_cpp_polygon_finder() {
|
|
|
571
571
|
rb_cstd_ofstream.define_method("closed?", [](OfstreamWrapper& self) { return self.closed(); });
|
|
572
572
|
rb_cstd_ofstream.define_method("rewind", [](OfstreamWrapper& self) { self.rewind(); });
|
|
573
573
|
rb_cstd_ofstream.define_method("read", [](OfstreamWrapper& self) { return self.read(); });
|
|
574
|
+
rb_cstd_ofstream.define_method("path", [](OfstreamWrapper& self) { return self.path; });
|
|
574
575
|
|
|
575
576
|
Data_Type<StreamingMerger> rb_cstreaming_merger = define_class<StreamingMerger, VerticalMerger>("CPPStreamingMerger");
|
|
576
577
|
rb_cstreaming_merger.define_method("add_tile", [](StreamingMerger& self, Object rb_result, bool flush) {
|
|
@@ -6,13 +6,19 @@ 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
|
|
16
|
+
@previous_boundary = nil
|
|
14
17
|
@user_options = options
|
|
15
|
-
|
|
18
|
+
if (warn_options = @user_options.keys & IGNORE_OPTIONS).any?
|
|
19
|
+
warn "[Contrek WARNING] The given options '#{warn_options.join(",")}' will be ignored."
|
|
20
|
+
end
|
|
21
|
+
@options = {unsafe_mode: false}.merge(@user_options).except(*IGNORE_OPTIONS)
|
|
16
22
|
unless safe?
|
|
17
23
|
warn "[Contrek WARNING] Processing tile with 'unsafe_mode: true'. Incompatible result options might lead to unexpected vector geometry."
|
|
18
24
|
end
|
|
@@ -23,12 +29,13 @@ module Contrek
|
|
|
23
29
|
raise ArgumentError, "All results must have the same height" if @height != result.metadata[:height] && safe?
|
|
24
30
|
@options[:versus] ||= result.metadata[:versus]
|
|
25
31
|
raise ArgumentError, "All results must have the same versus option" if @options[:versus] != result.metadata[:versus] && safe?
|
|
26
|
-
if safe? && result.metadata[:options]
|
|
32
|
+
if safe? && result.metadata[:options]&.key?(:compress)
|
|
27
33
|
if (result.metadata[:options][:compress].keys - [:uniq, :linear]).any?
|
|
28
34
|
raise ArgumentError, "Result with not supported postprocessing compression mode"
|
|
29
35
|
end
|
|
30
36
|
end
|
|
31
37
|
|
|
38
|
+
validate(result) if safe?
|
|
32
39
|
end_x = @current_x + result.metadata[:width]
|
|
33
40
|
|
|
34
41
|
tile = Tile.new(
|
|
@@ -40,6 +47,9 @@ module Contrek
|
|
|
40
47
|
tile.assign_raw_polygons!(result[:polygons])
|
|
41
48
|
|
|
42
49
|
@tiles << tile
|
|
50
|
+
|
|
51
|
+
@previous_boundary = boundary_coordinates(result.polygons, end_x) if safe?
|
|
52
|
+
|
|
43
53
|
@maximum_width = end_x
|
|
44
54
|
@current_x = end_x - 1
|
|
45
55
|
end
|
|
@@ -55,6 +65,25 @@ module Contrek
|
|
|
55
65
|
|
|
56
66
|
private
|
|
57
67
|
|
|
68
|
+
def validate(result)
|
|
69
|
+
return unless @previous_boundary
|
|
70
|
+
|
|
71
|
+
current = boundary_coordinates(result.polygons, @current_x)
|
|
72
|
+
unless @previous_boundary == current
|
|
73
|
+
raise ArgumentError, "Tile boundary mismatch: adjacent tiles have different edge coordinates"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def boundary_coordinates(polygons, boundary_x)
|
|
78
|
+
coordinates = []
|
|
79
|
+
polygons.each do |polygon|
|
|
80
|
+
polygon[:outer].each do |point|
|
|
81
|
+
coordinates << point[:y] if point[:x] == boundary_x
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
coordinates.sort!
|
|
85
|
+
end
|
|
86
|
+
|
|
58
87
|
def translate(result, offset)
|
|
59
88
|
result.polygons.each do |polygon|
|
|
60
89
|
polygon[:outer].each { |p| p[:x] += offset }
|
|
@@ -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,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Contrek
|
|
4
|
+
module Shared
|
|
5
|
+
class OpencvConverter
|
|
6
|
+
CHAIN_DIR = [
|
|
7
|
+
[3, 4, 5],
|
|
8
|
+
[2, -1, 6],
|
|
9
|
+
[1, 0, 7]
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
CORNER_X = [0, 1, 1, 0].freeze
|
|
13
|
+
CORNER_Y = [1, 1, 0, 0].freeze
|
|
14
|
+
|
|
15
|
+
def contour_to_cell_boundary(contour, rect)
|
|
16
|
+
raw = []
|
|
17
|
+
n = contour.size
|
|
18
|
+
|
|
19
|
+
return raw if n == 0
|
|
20
|
+
if n == 1
|
|
21
|
+
x = contour[0][:x]
|
|
22
|
+
y = contour[0][:y]
|
|
23
|
+
return [
|
|
24
|
+
{x: x, y: y + 1},
|
|
25
|
+
{x: x + 1, y: y + 1},
|
|
26
|
+
{x: x + 1, y: y},
|
|
27
|
+
{x: x, y: y}
|
|
28
|
+
]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
n.times do |i|
|
|
32
|
+
previous = contour[(i - 1) % n]
|
|
33
|
+
current = contour[i]
|
|
34
|
+
next_p = contour[(i + 1) % n]
|
|
35
|
+
|
|
36
|
+
incoming = chain_direction(previous, current)
|
|
37
|
+
outgoing = chain_direction(current, next_p)
|
|
38
|
+
|
|
39
|
+
corner = incoming >> 1
|
|
40
|
+
last_corner = ((outgoing + 1) >> 1) & 3
|
|
41
|
+
|
|
42
|
+
loop do
|
|
43
|
+
raw << {
|
|
44
|
+
x: current[:x] + CORNER_X[corner],
|
|
45
|
+
y: current[:y] + CORNER_Y[corner]
|
|
46
|
+
}
|
|
47
|
+
break if corner == last_corner
|
|
48
|
+
corner = (corner + 1) & 3
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# build path parts
|
|
53
|
+
edges = []
|
|
54
|
+
raw.size.times do |i|
|
|
55
|
+
a = raw[i]
|
|
56
|
+
b = raw[(i + 1) % raw.size]
|
|
57
|
+
next if a == b
|
|
58
|
+
|
|
59
|
+
key = [
|
|
60
|
+
[a[:x], a[:y]],
|
|
61
|
+
[b[:x], b[:y]]
|
|
62
|
+
].sort
|
|
63
|
+
edges << {a: a, b: b, key: key}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
counts = Hash.new(0)
|
|
67
|
+
edges.each do |edge|
|
|
68
|
+
counts[edge[:key]] += 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
kept = {}
|
|
72
|
+
edges.select! do |edge|
|
|
73
|
+
key = edge[:key]
|
|
74
|
+
next false if counts[key].even?
|
|
75
|
+
next false if kept[key]
|
|
76
|
+
|
|
77
|
+
kept[key] = true
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
return [] if edges.empty?
|
|
81
|
+
|
|
82
|
+
outgoing = Hash.new { |h, k| h[k] = [] }
|
|
83
|
+
edges.each_with_index do |edge, i|
|
|
84
|
+
outgoing[[edge[:a][:x], edge[:a][:y]]] << i
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
used = Array.new(edges.size, false)
|
|
88
|
+
result = []
|
|
89
|
+
current = 0
|
|
90
|
+
|
|
91
|
+
loop do
|
|
92
|
+
break if used.all?
|
|
93
|
+
if used[current]
|
|
94
|
+
current = used.index(false)
|
|
95
|
+
break unless current
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
edge = edges[current]
|
|
99
|
+
used[current] = true
|
|
100
|
+
result << edge[:a]
|
|
101
|
+
finish = edge[:b]
|
|
102
|
+
candidates = outgoing[[finish[:x], finish[:y]]]
|
|
103
|
+
next_edge = candidates.find { |index| !used[index] }
|
|
104
|
+
|
|
105
|
+
if next_edge
|
|
106
|
+
current = next_edge
|
|
107
|
+
else
|
|
108
|
+
current = used.index(false)
|
|
109
|
+
break unless current
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
rotate_to_scanline_start(result, rect)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def rect_to_bounds(rect)
|
|
116
|
+
{min_x: rect[:x],
|
|
117
|
+
max_x: rect[:x] + rect[:width],
|
|
118
|
+
min_y: rect[:y],
|
|
119
|
+
max_y: rect[:y] + rect[:height]}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def rotate_to_scanline_start(points, rect)
|
|
125
|
+
return points if points.empty?
|
|
126
|
+
|
|
127
|
+
min_y = rect[:y]
|
|
128
|
+
min_x = points.select { |p| p[:y] == min_y }.map { |p| p[:x] }.min
|
|
129
|
+
index = points.index { |p| p[:x] == min_x && p[:y] == min_y }
|
|
130
|
+
(index && index > 0) ? points.rotate(index) : points
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def chain_direction(from, to)
|
|
134
|
+
dx = to[:x] - from[:x]
|
|
135
|
+
dy = to[:y] - from[:y]
|
|
136
|
+
|
|
137
|
+
raise ArgumentError, "Zero-length contour step" if dx == 0 && dy == 0
|
|
138
|
+
if dx.abs > 1 || dy.abs > 1
|
|
139
|
+
raise ArgumentError, "Non-adjacent contour step"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
CHAIN_DIR[dx + 1][dy + 1]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
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"
|
|
@@ -50,7 +51,6 @@ require "contrek/finder/concurrent/streaming_merger"
|
|
|
50
51
|
require "contrek/finder/concurrent/svg_streaming_merger"
|
|
51
52
|
require "contrek/finder/concurrent/geo_json_streaming_merger"
|
|
52
53
|
require "contrek/finder/concurrent/cursor"
|
|
53
|
-
require "contrek/map/mercator_projection"
|
|
54
54
|
require "contrek/matchers/matcher"
|
|
55
55
|
require "contrek/matchers/matcher_hsb"
|
|
56
56
|
require "contrek/matchers/value_not_matcher"
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emanuele Cesaroni
|
|
@@ -263,6 +263,8 @@ files:
|
|
|
263
263
|
- ext/cpp_polygon_finder/PolygonFinder/src/polygon/reducers/UniqReducer.h
|
|
264
264
|
- ext/cpp_polygon_finder/PolygonFinder/src/polygon/reducers/VisvalingamReducer.cpp
|
|
265
265
|
- ext/cpp_polygon_finder/PolygonFinder/src/polygon/reducers/VisvalingamReducer.h
|
|
266
|
+
- ext/cpp_polygon_finder/PolygonFinder/src/polygon/shared/OpencvConverter.cpp
|
|
267
|
+
- ext/cpp_polygon_finder/PolygonFinder/src/polygon/shared/OpencvConverter.h
|
|
266
268
|
- ext/cpp_polygon_finder/cpp_polygon_finder.cpp
|
|
267
269
|
- ext/cpp_polygon_finder/extconf.rb
|
|
268
270
|
- lib/LICENSE-MIT.md
|
|
@@ -323,7 +325,6 @@ files:
|
|
|
323
325
|
- lib/contrek/finder/node_cluster.rb
|
|
324
326
|
- lib/contrek/finder/polygon_finder.rb
|
|
325
327
|
- lib/contrek/finder/result.rb
|
|
326
|
-
- lib/contrek/map/mercator_projection.rb
|
|
327
328
|
- lib/contrek/matchers/matcher.rb
|
|
328
329
|
- lib/contrek/matchers/matcher_hsb.rb
|
|
329
330
|
- lib/contrek/matchers/value_not_matcher.rb
|
|
@@ -333,6 +334,7 @@ files:
|
|
|
333
334
|
- lib/contrek/reducers/reducer.rb
|
|
334
335
|
- lib/contrek/reducers/uniq_reducer.rb
|
|
335
336
|
- lib/contrek/reducers/visvalingam_reducer.rb
|
|
337
|
+
- lib/contrek/shared/opencv_converter.rb
|
|
336
338
|
- lib/contrek/shared/result.rb
|
|
337
339
|
- lib/contrek/version.rb
|
|
338
340
|
homepage: https://github.com/runout77/contrek
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Map
|
|
4
|
-
class MercatorProjection
|
|
5
|
-
MERCATOR_RANGE = 256
|
|
6
|
-
|
|
7
|
-
class LatLng
|
|
8
|
-
attr_reader :lat, :lng
|
|
9
|
-
def initialize(lat:, lng:)
|
|
10
|
-
@lat = lat
|
|
11
|
-
@lng = lng
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
class Point
|
|
16
|
-
attr_accessor :x, :y
|
|
17
|
-
def initialize(x: 0, y: 0)
|
|
18
|
-
@x = x
|
|
19
|
-
@y = y
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def initialize
|
|
24
|
-
@pixel_origin = Point.new(x: MERCATOR_RANGE / 2, y: MERCATOR_RANGE / 2)
|
|
25
|
-
@pixels_per_lon_degree = MERCATOR_RANGE.to_f / 360
|
|
26
|
-
@pixels_per_lon_radian = MERCATOR_RANGE.to_f / (2 * Math::PI)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def self.get_corners(center, zoom, map_width, map_height)
|
|
30
|
-
scale = 2**zoom
|
|
31
|
-
proj = Map::MercatorProjection.new
|
|
32
|
-
center_px = proj.from_lat_lng_to_point(lat_lng: center)
|
|
33
|
-
sw_point = Point.new(x: center_px.x - ((map_width / 2).to_f / scale), y: center_px.y + ((map_height / 2).to_f / scale))
|
|
34
|
-
sw_lat_lon = proj.from_point_to_lat_lng(sw_point)
|
|
35
|
-
ne_point = Point.new(x: center_px.x + ((map_width / 2).to_f / scale), y: center_px.y - ((map_height / 2).to_f / scale))
|
|
36
|
-
ne_lat_lon = proj.from_point_to_lat_lng(ne_point)
|
|
37
|
-
{
|
|
38
|
-
N: ne_lat_lon.lat,
|
|
39
|
-
E: ne_lat_lon.lng,
|
|
40
|
-
S: sw_lat_lon.lat,
|
|
41
|
-
W: sw_lat_lon.lng
|
|
42
|
-
}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def from_lat_lng_to_point(lat_lng:, opt_point: nil)
|
|
46
|
-
point = opt_point.nil? ? Point.new(x: 0, y: 0) : opt_point
|
|
47
|
-
origin = @pixel_origin
|
|
48
|
-
point.x = origin.x + (lat_lng.lng * @pixels_per_lon_degree)
|
|
49
|
-
siny = bound(Math.sin(degreesToRadians(lat_lng.lat)), -0.9999, 0.9999)
|
|
50
|
-
point.y = origin.y + (0.5 * Math.log((1 + siny) / (1 - siny)) * -@pixels_per_lon_radian)
|
|
51
|
-
point
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def from_point_to_lat_lng(point)
|
|
55
|
-
origin = @pixel_origin
|
|
56
|
-
lng = (point.x - origin.x) / @pixels_per_lon_degree
|
|
57
|
-
lat_radians = (point.y - origin.y) / -@pixels_per_lon_radian
|
|
58
|
-
lat = radiansToDegrees(2 * Math.atan(Math.exp(lat_radians)) - (Math::PI / 2))
|
|
59
|
-
LatLng.new(lat: lat, lng: lng)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def degreesToRadians(deg)
|
|
65
|
-
deg * (Math::PI / 180)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def radiansToDegrees(rad)
|
|
69
|
-
rad / (Math::PI / 180)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def bound(value, opt_min, opt_max)
|
|
73
|
-
value = [value, opt_min].max if !opt_min.nil?
|
|
74
|
-
value = [value, opt_max].min if !opt_max.nil?
|
|
75
|
-
value
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|