contrek 1.0.2 → 1.0.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.
@@ -0,0 +1,119 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class Finder
4
+ prepend Poolable
5
+
6
+ attr_reader :maximum_width
7
+
8
+ # Supported options
9
+ # - number_of_threads: number of threads that can be used by the process. If set to 0, it works in
10
+ # single-thread mode.
11
+ # The process can take advantage of multiprocessing when computing the initial contours of the
12
+ # tiles into which the image is divided, and during the subsequent merging of tiles into pairs.
13
+ # Each merge is performed by any available thread as soon as two adjacent tiles have been computed.
14
+ # If set to nil, the process will use the maximum number of cpu available cores.
15
+ # - bitmap: PngBitmap containing the image.
16
+ # - matcher: specific Matcher used for pixel identification.
17
+ # - options: options include:
18
+ # - number_of_tiles: number of tiles into which the image is initially divided.
19
+ # It cannot exceed the image width, so the system works with tiles at least one pixel wide.
20
+ # Given the technique on which the process is based, using excessively narrow tiles is
21
+ # discouraged.
22
+
23
+ def initialize(bitmap:, matcher:, options: {}, &block)
24
+ @initialize_time = Benchmark.measure do
25
+ @block = block
26
+ @tiles = Queue.new
27
+ @bitmap = bitmap
28
+ @options = options
29
+ @clusters = []
30
+ @maximum_width = bitmap.w
31
+ @number_of_tiles = options[:number_of_tiles] || (raise "number_of_tiles params is needed!")
32
+
33
+ cw = @maximum_width.to_f / @number_of_tiles
34
+ raise "One pixel tile width minimum!" if cw < 1.0
35
+ x = 0
36
+ current_versus = options[:versus]
37
+ raise "Define versus!" if current_versus.nil?
38
+
39
+ @number_of_tiles.times do |tile_index|
40
+ tile_end_x = (cw * (tile_index + 1)).to_i
41
+
42
+ enqueue!(tile_index: tile_index,
43
+ tile_start_x: x,
44
+ tile_end_x: tile_end_x) do |payload|
45
+ finder = ClippedPolygonFinder.new(
46
+ bitmap: bitmap,
47
+ matcher: matcher,
48
+ options: {versus: current_versus},
49
+ start_x: payload[:tile_start_x],
50
+ end_x: payload[:tile_end_x]
51
+ )
52
+ tile = Tile.new(
53
+ finder: self,
54
+ start_x: payload[:tile_start_x],
55
+ end_x: payload[:tile_end_x],
56
+ name: payload[:tile_index].to_s
57
+ )
58
+ tile.initial_process!(finder)
59
+ @tiles << tile
60
+ end
61
+
62
+ x = tile_end_x - 1
63
+ end
64
+ @tile = process_tiles!(bitmap)
65
+ end.real
66
+ end
67
+
68
+ def process_info(bitmap = nil)
69
+ raw_polygons = @tile.to_raw_polygons
70
+
71
+ compress_time = Benchmark.measure do
72
+ if @options.has_key?(:compress)
73
+ FakeCluster.new(raw_polygons, @options).compress_coords
74
+ end
75
+ end.real
76
+
77
+ {polygons: raw_polygons,
78
+ benchmarks: {
79
+ total: ((@initialize_time + compress_time) * 1000).round(3),
80
+ init: (@initialize_time * 1000).round(3),
81
+ outer: (@tile.benchmarks[:outer] * 1000).round(3),
82
+ inner: (@tile.benchmarks[:inner] * 1000).round(3),
83
+ compress: ((compress_time * 1000).round(3) if @options.has_key?(:compress))
84
+ }.compact}
85
+ end
86
+
87
+ private
88
+
89
+ def process_tiles!(bitmap)
90
+ arriving_tiles = []
91
+ loop do
92
+ tile = @tiles.pop
93
+ return tile if tile.whole?
94
+
95
+ if (twin_tile = arriving_tiles.find { |b| (b.start_x == (tile.end_x - 1)) || ((b.end_x - 1) == tile.start_x) })
96
+ cluster = Cluster.new(finder: self, height: bitmap.h, width: bitmap.w)
97
+ if twin_tile.start_x == (tile.end_x - 1)
98
+ cluster.add(tile)
99
+ cluster.add(twin_tile)
100
+ else
101
+ cluster.add(twin_tile)
102
+ cluster.add(tile)
103
+ end
104
+ enqueue!(cluster: cluster) do |payload|
105
+ merged_tile = payload[:cluster].merge_tiles!
106
+ @tiles << merged_tile
107
+ # usefull external access to each merged_tile
108
+ @block&.call(merged_tile, bitmap)
109
+ end
110
+ arriving_tiles.delete(twin_tile)
111
+ next
112
+ end
113
+ arriving_tiles << tile
114
+ end
115
+ nil
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,12 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class Hub
4
+ attr_reader :payloads, :width
5
+ def initialize(height:, width:)
6
+ @width = width
7
+ # @payloads = Array.new(width * height)
8
+ @payloads = {}
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ module Contrek
2
+ module Concurrent
3
+ module Listable
4
+ attr_accessor :prev, :next, :owner
5
+ def initialize(*args, **kwargs, &block)
6
+ super
7
+ @next = nil
8
+ @prev = nil
9
+ @owner = nil
10
+ end
11
+
12
+ def payload
13
+ raise NoMethodError
14
+ end
15
+
16
+ def after_add(new_queue)
17
+ end
18
+
19
+ def before_rem(old_queue)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,72 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class Part
4
+ prepend Queueable
5
+
6
+ SEAM = 1
7
+ EXCLUSIVE = 0
8
+ ADDED = 2
9
+
10
+ attr_reader :polyline, :index, :touched
11
+ attr_accessor :next, :circular_next, :prev, :type, :passes, :inverts, :trasmuted
12
+ def initialize(type, polyline)
13
+ @type = type
14
+ @polyline = polyline
15
+ @next = nil
16
+ @circular_next = nil
17
+ @prev = nil
18
+ @passes = 0
19
+ @touched = false
20
+ @inverts = false
21
+ @trasmuted = false
22
+ end
23
+
24
+ def is?(type)
25
+ @type == type
26
+ end
27
+
28
+ def add_position(position, n)
29
+ add(Position.new(position: position, hub: polyline.tile.cluster.hub))
30
+ end
31
+
32
+ def next_position(force_position = nil)
33
+ if force_position
34
+ move_to_this = reverse_each { |pos| break pos if pos.payload == force_position }
35
+ next_of!(move_to_this)
36
+ force_position
37
+ else
38
+ return nil if iterator.nil?
39
+ position = iterator
40
+ @touched = true
41
+ forward!
42
+ position
43
+ end
44
+ end
45
+
46
+ def touch!
47
+ @touched = true
48
+ end
49
+
50
+ def name
51
+ {Part::EXCLUSIVE => "EXCLUSIVE",
52
+ Part::SEAM => "SEAM",
53
+ Part::ADDED => "ADDED"}[type]
54
+ end
55
+
56
+ def inspect
57
+ "part #{polyline.parts.index(self)} (inv=#{@inverts} trm=#{@trasmuted} touched=#{@touched} passes=#{@passes}, #{size}x) of #{polyline.info} (#{name}) (#{to_a.map { |e| "[#{e[:x]},#{e[:y]}]" }.join})"
58
+ end
59
+
60
+ def innerable?
61
+ (@touched == false) && is?(EXCLUSIVE)
62
+ end
63
+
64
+ def intersect_part?(other_part)
65
+ other_part.each do |position|
66
+ return true if position.end_point.queues.include?(self)
67
+ end
68
+ false
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,132 @@
1
+ module Contrek
2
+ module Concurrent
3
+ module Partitionable
4
+ attr_reader :parts
5
+
6
+ def initialize(*args, **kwargs, &block)
7
+ super
8
+ @parts = []
9
+ end
10
+
11
+ def add_part(new_part)
12
+ last = @parts.last
13
+ @parts << new_part
14
+ last.next = last.circular_next = new_part if last
15
+ new_part.circular_next = @parts.first
16
+ new_part.prev = last
17
+ end
18
+
19
+ def find_first_part_by_position(position)
20
+ @parts.find do |part|
21
+ part.is?(Part::SEAM) &&
22
+ part.passes == 0 &&
23
+ position.end_point.queues.include?(part)
24
+ end
25
+ end
26
+
27
+ def inspect_parts
28
+ [" "] + ["#{self.class} parts=#{@parts.size}"] + @parts.map { |p| p.inspect } + [" "]
29
+ end
30
+
31
+ def partition!
32
+ current_part = nil
33
+ @parts = []
34
+
35
+ @raw.each_with_index do |position, n|
36
+ if @tile.tg_border?(position)
37
+ if current_part.nil?
38
+ current_part = Part.new(Part::SEAM, self)
39
+ elsif !current_part.is?(Part::SEAM)
40
+ add_part(current_part)
41
+ current_part = Part.new(Part::SEAM, self)
42
+ end
43
+ elsif current_part.nil?
44
+ current_part = Part.new(Part::EXCLUSIVE, self)
45
+ elsif !current_part.is?(Part::EXCLUSIVE)
46
+ add_part(current_part)
47
+ current_part = Part.new(Part::EXCLUSIVE, self)
48
+ end
49
+ if n > 0 && @raw[n - 1] == position
50
+ current_part.inverts = true
51
+ end
52
+ current_part.add_position(position, n)
53
+ end
54
+ add_part(current_part)
55
+
56
+ trasmute_parts!
57
+ end
58
+
59
+ def sew!(intersection, other)
60
+ matching_part_indexes = []
61
+ parts.each_with_index do |part, index|
62
+ next if part.trasmuted
63
+ matching_part_indexes << index if part.intersection_with_array?(intersection)
64
+ end
65
+ other_matching_part_indexes = []
66
+ other.parts.each_with_index do |part, index|
67
+ next if part.trasmuted
68
+ other_matching_part_indexes << index if part.intersection_with_array?(intersection)
69
+ end
70
+ # other_matching_part_indexes and matching_part_indexes always contain at least one element
71
+ before_parts = other.parts[other_matching_part_indexes.last + 1..]
72
+ after_parts = other_matching_part_indexes.first.zero? ? [] : other.parts[0..other_matching_part_indexes.first - 1]
73
+ part_start = parts[matching_part_indexes.first]
74
+ part_end = parts[matching_part_indexes.last]
75
+
76
+ # They are inverted since they traverse in opposite directions
77
+ sequence = Sequence.new
78
+ sequence.add part_start.head
79
+ before_parts.each { |part| sequence.append(part) }
80
+ after_parts.each { |part| sequence.append(part) }
81
+ sequence.add part_end.tail if part_end.tail # nil when part_start == part_end
82
+
83
+ part_start.replace!(sequence)
84
+ part_start.type = Part::EXCLUSIVE
85
+ part_end.reset! if part_start != part_end
86
+
87
+ left = []
88
+ (matching_part_indexes.first + 1).upto(matching_part_indexes.last - 1) do |n|
89
+ left << parts[n].to_a if matching_part_indexes.index(n).nil?
90
+ end
91
+ (matching_part_indexes.last - 1).downto(matching_part_indexes.first + 1) do |n|
92
+ delete_part = parts[n]
93
+ delete_part.prev.next = delete_part.next if delete_part.prev
94
+ delete_part.next.prev = delete_part.prev if delete_part.next
95
+ parts.delete_at(n)
96
+ end
97
+ right = []
98
+ (other_matching_part_indexes.first + 1).upto(other_matching_part_indexes.last - 1) do |n|
99
+ right << other.parts[n].to_a if other_matching_part_indexes.index(n).nil?
100
+ end
101
+ [left, right]
102
+ end
103
+
104
+ private
105
+
106
+ # If there are SEAM parts and one is canceled out by another within the same polyline,
107
+ # meaning that all its points are repeated in another, longer sequence,
108
+ # then the shorter one is converted to EXCLUSIVE and marked as transmuted
109
+ def trasmute_parts!
110
+ insides = @parts.select { |p| p.is?(Part::SEAM) }
111
+ return if insides.size < 2
112
+
113
+ insides.each do |inside|
114
+ (insides - [inside]).each do |inside_compare|
115
+ next unless inside_compare.is?(Part::SEAM)
116
+
117
+ count = 0
118
+ inside.each do |position|
119
+ break unless position.end_point.queues.include?(inside_compare)
120
+ count += 1
121
+ end
122
+ if count == inside.size
123
+ inside.type = Part::EXCLUSIVE
124
+ inside.trasmuted = true
125
+ break
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,95 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class Polyline
4
+ prepend Partitionable
5
+
6
+ TRACKED_OUTER = 1 << 0
7
+ TRACKED_INNER = 1 << 1
8
+ Bounds = Struct.new(:min_x, :max_x, :min_y, :max_y)
9
+
10
+ attr_reader :raw, :name, :min_y, :max_y, :next_tile_eligible_shapes
11
+ attr_accessor :shape, :tile
12
+
13
+ def initialize(tile:, polygon:, shape: nil)
14
+ @tile = tile
15
+ @name = tile.shapes.count
16
+ @raw = polygon
17
+ @shape = shape
18
+ @flags = 0
19
+ find_boundary
20
+ end
21
+
22
+ def inspect
23
+ "#{self.class}[b#{@tile.name} S#{@name} #{"B" if boundary?}] (#{raw.count} => #{raw.inspect})"
24
+ end
25
+
26
+ def info
27
+ "w#{@tile.name} S#{@name}"
28
+ end
29
+
30
+ def turn_on(flag)
31
+ @flags |= flag
32
+ end
33
+
34
+ def turn_off(flag)
35
+ @flags &= ~flag
36
+ end
37
+
38
+ def on?(flag)
39
+ (@flags & flag) != 0
40
+ end
41
+
42
+ def intersection(other)
43
+ (@raw.compact & other.raw.compact)
44
+ end
45
+
46
+ def empty?
47
+ @raw.empty?
48
+ end
49
+
50
+ def boundary?
51
+ @tile.tg_border?({x: @min_x}) || @tile.tg_border?({x: @max_x})
52
+ end
53
+
54
+ def clear!
55
+ @raw = []
56
+ end
57
+
58
+ def vert_intersect?(other)
59
+ !(@max_y < other.min_y || other.max_y < @min_y)
60
+ end
61
+
62
+ def width
63
+ return 0 if empty?
64
+ @max_x - @min_x
65
+ end
66
+
67
+ # Pre-detects, for the current polyline, adjacent ones in the neighboring tile
68
+ # that vertically intersect.
69
+ def precalc!
70
+ @next_tile_eligible_shapes = @tile
71
+ .circular_next.boundary_shapes
72
+ .select { |s|
73
+ !s.outer_polyline.on?(Polyline::TRACKED_OUTER) &&
74
+ vert_intersect?(s.outer_polyline)
75
+ }
76
+ end
77
+
78
+ private
79
+
80
+ def find_boundary
81
+ return if @raw.empty?
82
+
83
+ bounds = @raw.compact.each_with_object(Bounds.new(Float::INFINITY, -Float::INFINITY, Float::INFINITY, -Float::INFINITY)) do |c, b|
84
+ x, y = c[:x], c[:y]
85
+ b.min_x = x if x < b.min_x
86
+ b.max_x = x if x > b.max_x
87
+ b.min_y = y if y < b.min_y
88
+ b.max_y = y if y > b.max_y
89
+ end
90
+
91
+ @min_x, @max_x, @min_y, @max_y = bounds.values
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,36 @@
1
+ require "concurrent-ruby"
2
+
3
+ module Contrek
4
+ module Concurrent
5
+ module Poolable
6
+ attr_reader :number_of_threads
7
+ def initialize(number_of_threads: 0, **kwargs)
8
+ @number_of_threads = number_of_threads || ::Concurrent.physical_processor_count
9
+ if @number_of_threads > 0
10
+ @threads = ::Concurrent::Array.new
11
+ @semaphore = ::Concurrent::Semaphore.new(@number_of_threads)
12
+ end
13
+ super(**kwargs)
14
+ end
15
+
16
+ def wait!
17
+ @threads.each(&:join)
18
+ end
19
+
20
+ def enqueue!(**payload, &block)
21
+ if @number_of_threads > 0
22
+ @threads << Thread.new do
23
+ @semaphore.acquire
24
+ begin
25
+ block.call(payload)
26
+ ensure
27
+ @semaphore.release
28
+ end
29
+ end
30
+ else
31
+ block.call(payload)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ module Contrek
2
+ module Concurrent
3
+ class Position
4
+ include Listable
5
+
6
+ attr_reader :end_point
7
+
8
+ def initialize(hub:, position:)
9
+ key = position[:y] * hub.width + position[:x]
10
+ @end_point = hub.payloads[key] ||= EndPoint.new
11
+ @position = position
12
+ end
13
+
14
+ def payload
15
+ @position
16
+ end
17
+
18
+ def after_add(new_queue)
19
+ @end_point.queues << new_queue
20
+ end
21
+
22
+ def before_rem(old_queue)
23
+ @end_point.queues.delete(old_queue)
24
+ end
25
+ end
26
+ end
27
+ end