qr_reader 0.0.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 +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +14 -0
- data/README.md +222 -0
- data/exe/qr_reader +66 -0
- data/lib/qr_reader/binarizer.rb +170 -0
- data/lib/qr_reader/bit_matrix.rb +120 -0
- data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
- data/lib/qr_reader/decoder/character_set.rb +163 -0
- data/lib/qr_reader/decoder/codewords.rb +164 -0
- data/lib/qr_reader/decoder/data_mask.rb +57 -0
- data/lib/qr_reader/decoder/format_reader.rb +204 -0
- data/lib/qr_reader/decoder/function_patterns.rb +62 -0
- data/lib/qr_reader/decoder/galois_field.rb +69 -0
- data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
- data/lib/qr_reader/decoder.rb +241 -0
- data/lib/qr_reader/detector/alignment_grid.rb +127 -0
- data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
- data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
- data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
- data/lib/qr_reader/detector/candidate.rb +436 -0
- data/lib/qr_reader/detector/finder_pattern.rb +63 -0
- data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
- data/lib/qr_reader/detector/geometry.rb +118 -0
- data/lib/qr_reader/detector/grid_sampler.rb +350 -0
- data/lib/qr_reader/detector/pattern_sets.rb +235 -0
- data/lib/qr_reader/detector/perspective_transform.rb +117 -0
- data/lib/qr_reader/detector/pure_detector.rb +197 -0
- data/lib/qr_reader/detector.rb +215 -0
- data/lib/qr_reader/errors.rb +14 -0
- data/lib/qr_reader/format_information.rb +79 -0
- data/lib/qr_reader/formats/bmp.rb +434 -0
- data/lib/qr_reader/formats/ccitt.rb +81 -0
- data/lib/qr_reader/formats/gif.rb +272 -0
- data/lib/qr_reader/formats/jpeg.rb +152 -0
- data/lib/qr_reader/formats/pdf.rb +2935 -0
- data/lib/qr_reader/formats/png.rb +311 -0
- data/lib/qr_reader/formats/pnm.rb +286 -0
- data/lib/qr_reader/formats/tiff.rb +1345 -0
- data/lib/qr_reader/formats.rb +279 -0
- data/lib/qr_reader/image.rb +231 -0
- data/lib/qr_reader/kernels/binarizer.rb +475 -0
- data/lib/qr_reader/kernels/ccitt.rb +554 -0
- data/lib/qr_reader/kernels/detector.rb +587 -0
- data/lib/qr_reader/kernels/image.rb +65 -0
- data/lib/qr_reader/kernels/jpeg.rb +3421 -0
- data/lib/qr_reader/kernels/lzw.rb +173 -0
- data/lib/qr_reader/kernels/pixels.rb +116 -0
- data/lib/qr_reader/kernels/png.rb +1138 -0
- data/lib/qr_reader/kernels.rb +14 -0
- data/lib/qr_reader/native_kernels.rb +130 -0
- data/lib/qr_reader/reader.rb +348 -0
- data/lib/qr_reader/result.rb +49 -0
- data/lib/qr_reader/symbol_version.rb +173 -0
- data/lib/qr_reader/version.rb +5 -0
- data/lib/qr_reader.rb +77 -0
- metadata +103 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
class Candidate
|
|
6
|
+
# Dimension estimation for a Candidate: module sizes measured across
|
|
7
|
+
# the finder patterns, timing pattern counts and version information
|
|
8
|
+
# (read through the global transform and through frames local to the
|
|
9
|
+
# finders). Mixed into Candidate; relies on its @tl/@tr/@bl, @axes,
|
|
10
|
+
# @matrix, the per-finder module sizes and #transform_for / #stroke.
|
|
11
|
+
module Dimension
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Dimension estimates, most likely first: the timing pattern count
|
|
15
|
+
# if row and column agree (or only one is readable and it lies
|
|
16
|
+
# within the range of the per-side distance estimates), else the
|
|
17
|
+
# rounded mean distance estimate; then the per-side estimates and
|
|
18
|
+
# +-1, +-2 versions.
|
|
19
|
+
def dimension_candidates(estimate, top, left)
|
|
20
|
+
dims = [round_dimension(estimate), round_dimension(top), round_dimension(left)]
|
|
21
|
+
row, col = timing_dimensions
|
|
22
|
+
low, high = [top, left].minmax
|
|
23
|
+
timing = [row, col].compact.uniq
|
|
24
|
+
if timing.size == 1 && (row == col || timing[0].between?(low - 4.5, high + 4.5))
|
|
25
|
+
dims.unshift(timing[0])
|
|
26
|
+
else
|
|
27
|
+
timing.each { |t| dims.insert(1, t) if (t - estimate).abs <= 8 }
|
|
28
|
+
end
|
|
29
|
+
first = dims.first
|
|
30
|
+
dims.push(first - 4, first + 4, first - 8, first + 8)
|
|
31
|
+
dims.select { |d| d.between?(21, 177) }.uniq
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Nearest dimension of the form 4v + 17.
|
|
35
|
+
def round_dimension(estimate)
|
|
36
|
+
((estimate - 17) / 4.0).round * 4 + 17
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Module size from the 1:1:3:1:1 spread of pattern +p+ along the unit
|
|
40
|
+
# direction (ux, uy); nil if the runs do not fit. If the exact runs
|
|
41
|
+
# do not fit, single noise pixels are bridged for patterns of 3+ px
|
|
42
|
+
# modules. (Not the other way round: modules along this direction
|
|
43
|
+
# may be much thinner - fax pages - and a one pixel light ring
|
|
44
|
+
# real; bridged, the runs can still pass the relaxed ratio test.)
|
|
45
|
+
def line_module_size(p, ux, uy)
|
|
46
|
+
limit = (p.module_size * 10).ceil + 2
|
|
47
|
+
spread_module_size(p, ux, uy, limit, false) ||
|
|
48
|
+
(p.module_size >= 3 && spread_module_size(p, ux, uy, limit, true)) || nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def spread_module_size(p, ux, uy, limit, noise)
|
|
52
|
+
fwd = Geometry.runs(@matrix, p.x, p.y, ux, uy, limit, noise: noise) or return
|
|
53
|
+
back = Geometry.runs(@matrix, p.x, p.y, -ux, -uy, limit, noise: noise) or return
|
|
54
|
+
a, b, c, d, e = back[2], back[1], fwd[0] + back[0] - 1, fwd[1], fwd[2]
|
|
55
|
+
return unless Geometry.finder_ratio?(a, b, c, d, e, relaxed: true)
|
|
56
|
+
|
|
57
|
+
(a + b + c + d + e) / 7.0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Dimensions counted along the timing patterns (row 6 from TL to TR,
|
|
61
|
+
# column 6 from TL to BL); each nil when unreadable.
|
|
62
|
+
def timing_dimensions
|
|
63
|
+
return [nil, nil] if @module_size < 1.2
|
|
64
|
+
|
|
65
|
+
right, down = @axes
|
|
66
|
+
row = count_timing(@tl.x + 3 * @tl_down * down[0], @tl.y + 3 * @tl_down * down[1],
|
|
67
|
+
@tr.x + 3 * @tr_down * down[0], @tr.y + 3 * @tr_down * down[1], @tl_right, @tr_left)
|
|
68
|
+
col = count_timing(@tl.x + 3 * @tl_right * right[0], @tl.y + 3 * @tl_right * right[1],
|
|
69
|
+
@bl.x + 3 * @bl_right * right[0], @bl.y + 3 * @bl_right * right[1], @tl_down, @bl_up)
|
|
70
|
+
[row, col]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Walks from (x0, y0) to (x1, y1) (inside the dark rings of two finder
|
|
74
|
+
# patterns, along a timing pattern) and returns 2 * light runs + 13,
|
|
75
|
+
# or nil if the runs are not a clean alternation. Run lengths may
|
|
76
|
+
# drift along the line (perspective, curved surfaces) but neighbours
|
|
77
|
+
# must be similar and the ends must match the finders' module sizes.
|
|
78
|
+
def count_timing(x0, y0, x1, y1, ms0, ms1)
|
|
79
|
+
len = Math.hypot(x1 - x0, y1 - y0)
|
|
80
|
+
step = [ms0, ms1].min / 4.0
|
|
81
|
+
step = 0.5 if step < 0.5
|
|
82
|
+
n = (len / step).floor
|
|
83
|
+
return if n < 8
|
|
84
|
+
|
|
85
|
+
sx = (x1 - x0) / n
|
|
86
|
+
sy = (y1 - y0) / n
|
|
87
|
+
bits = @matrix.bits
|
|
88
|
+
w = @matrix.width
|
|
89
|
+
h = @matrix.height
|
|
90
|
+
runs = []
|
|
91
|
+
color = nil
|
|
92
|
+
length = 0
|
|
93
|
+
i = 0
|
|
94
|
+
while i <= n
|
|
95
|
+
x = x0 + sx * i
|
|
96
|
+
y = y0 + sy * i
|
|
97
|
+
return unless x >= 0 && y >= 0 && x < w && y < h
|
|
98
|
+
|
|
99
|
+
c = bits.getbyte(y.to_i * w + x.to_i)
|
|
100
|
+
if c == color
|
|
101
|
+
length += 1
|
|
102
|
+
else
|
|
103
|
+
runs << length if color
|
|
104
|
+
color = c
|
|
105
|
+
length = 1
|
|
106
|
+
end
|
|
107
|
+
i += 1
|
|
108
|
+
end
|
|
109
|
+
runs << length
|
|
110
|
+
runs = merge_noise_runs(runs) if [ms0, ms1].min / step >= 3.5
|
|
111
|
+
# Runs alternate dark/light starting and ending with dark (the rings).
|
|
112
|
+
return if runs.size < 3 || runs.size.even?
|
|
113
|
+
|
|
114
|
+
inner = runs[1..-2]
|
|
115
|
+
unit = len / n
|
|
116
|
+
return unless (inner.first * unit / ms0).between?(0.4, 2.0) && (inner.last * unit / ms1).between?(0.4, 2.0)
|
|
117
|
+
|
|
118
|
+
inner.each_cons(2) { |a, b| return nil if a > 3 * b || b > 3 * a }
|
|
119
|
+
inner.each_cons(3) { |a, _, b| return nil if a > 1.7 * b + 1 || b > 1.7 * a + 1 }
|
|
120
|
+
inner.size + 14
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Merges runs of a single sample (noise when modules span several
|
|
124
|
+
# samples) into their neighbours: [5, 1, 4] becomes [10].
|
|
125
|
+
def merge_noise_runs(runs)
|
|
126
|
+
out = []
|
|
127
|
+
i = 0
|
|
128
|
+
while i < runs.size
|
|
129
|
+
if runs[i] == 1 && !out.empty? && i + 1 < runs.size
|
|
130
|
+
out[-1] += 1 + runs[i + 1]
|
|
131
|
+
i += 2
|
|
132
|
+
else
|
|
133
|
+
out << runs[i]
|
|
134
|
+
i += 1
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
out
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The dimension confirmed by the version information blocks (versions
|
|
141
|
+
# 7+): the first candidate dimension for which a block, read through
|
|
142
|
+
# the global transform or through a frame local to its finder
|
|
143
|
+
# pattern, decodes to a version of exactly that dimension. Else the
|
|
144
|
+
# version the first readable block names, if it is close.
|
|
145
|
+
def version_dimension(dims)
|
|
146
|
+
dims.each do |d|
|
|
147
|
+
next if d < 45
|
|
148
|
+
|
|
149
|
+
t = transform_for(d, @mode) or next
|
|
150
|
+
next unless read_version(t, d).any? { |v| v && v.dimension == d } ||
|
|
151
|
+
local_versions(d).any? { |v| v && v.dimension == d }
|
|
152
|
+
|
|
153
|
+
@version_confirmed = true
|
|
154
|
+
return d
|
|
155
|
+
end
|
|
156
|
+
d = dims.find { |x| x >= 45 } or return
|
|
157
|
+
t = transform_for(d, @mode) or return
|
|
158
|
+
v = read_version(t, d).compact.first
|
|
159
|
+
v && (v.dimension - d).abs <= 8 ? v.dimension : nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Version blocks read through affine frames centred on the TR and BL
|
|
163
|
+
# finders, whose axes follow the finders' own ring strokes; robust
|
|
164
|
+
# against curvature that bends the global transform near the edges.
|
|
165
|
+
def local_versions(d)
|
|
166
|
+
tr = local_frame(@tr, d - 3.5, 3.5, stroke(:tr_down), stroke(:tr_left), @tr_left, @tr_down)
|
|
167
|
+
bl = local_frame(@bl, 3.5, d - 3.5, stroke(:bl_up), stroke(:bl_right), @bl_right, @bl_up)
|
|
168
|
+
out = []
|
|
169
|
+
out << read_version_block(tr, d, :tr) if tr
|
|
170
|
+
out << read_version_block(bl, d, :bl) if bl
|
|
171
|
+
out
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Affine frame at finder +p+ (module centre (cx, cy)) mapping module
|
|
175
|
+
# (mx, my) to pixels (see read_version_block); +h_line+ / +v_line+
|
|
176
|
+
# are stroke lines running along the local x / y axes. nil without
|
|
177
|
+
# both strokes.
|
|
178
|
+
def local_frame(p, cx, cy, h_line, v_line, ms_x, ms_y)
|
|
179
|
+
return unless h_line && v_line
|
|
180
|
+
|
|
181
|
+
right, down = @axes
|
|
182
|
+
ex0 = h_line[2]
|
|
183
|
+
ex1 = h_line[3]
|
|
184
|
+
if ex0 * right[0] + ex1 * right[1] < 0
|
|
185
|
+
ex0 = -ex0
|
|
186
|
+
ex1 = -ex1
|
|
187
|
+
end
|
|
188
|
+
ey0 = v_line[2]
|
|
189
|
+
ey1 = v_line[3]
|
|
190
|
+
if ey0 * down[0] + ey1 * down[1] < 0
|
|
191
|
+
ey0 = -ey0
|
|
192
|
+
ey1 = -ey1
|
|
193
|
+
end
|
|
194
|
+
[p.x, p.y, cx, cy, ms_x, ms_y, ex0, ex1, ey0, ey1]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# One version block (:tr or :bl) read through a local frame.
|
|
198
|
+
def read_version_block(frame, d, which)
|
|
199
|
+
px, py, cx, cy, ms_x, ms_y, ex0, ex1, ey0, ey1 = frame
|
|
200
|
+
bits = 0
|
|
201
|
+
k = 17
|
|
202
|
+
while k >= 0
|
|
203
|
+
a = d - 11 + k % 3 + 0.5
|
|
204
|
+
b = k / 3 + 0.5
|
|
205
|
+
if which == :tr
|
|
206
|
+
mx = a
|
|
207
|
+
my = b
|
|
208
|
+
else
|
|
209
|
+
mx = b
|
|
210
|
+
my = a
|
|
211
|
+
end
|
|
212
|
+
u = (mx - cx) * ms_x
|
|
213
|
+
v = (my - cy) * ms_y
|
|
214
|
+
bits = (bits << 1) | pixel(px + u * ex0 + v * ey0, py + u * ex1 + v * ey1)
|
|
215
|
+
k -= 1
|
|
216
|
+
end
|
|
217
|
+
SymbolVersion.decode_version_info(bits)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Both 18-bit version information blocks decoded (SymbolVersion or nil).
|
|
221
|
+
def read_version(t, d)
|
|
222
|
+
bits1 = 0
|
|
223
|
+
bits2 = 0
|
|
224
|
+
k = 17
|
|
225
|
+
while k >= 0
|
|
226
|
+
a = d - 11 + k % 3
|
|
227
|
+
b = k / 3
|
|
228
|
+
bits1 = (bits1 << 1) | module_at(t, a, b)
|
|
229
|
+
bits2 = (bits2 << 1) | module_at(t, b, a)
|
|
230
|
+
k -= 1
|
|
231
|
+
end
|
|
232
|
+
[SymbolVersion.decode_version_info(bits1), SymbolVersion.decode_version_info(bits2)]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
class Candidate
|
|
6
|
+
# Geometry from the inner ring strokes of the finder patterns (the
|
|
7
|
+
# dark ring sides facing the symbol's interior), traced as straight
|
|
8
|
+
# lines - which they stay under perspective - in the spirit of
|
|
9
|
+
# zxing-cpp's TraceLine. Their intersections give the bottom-right
|
|
10
|
+
# alignment pattern position and the grid points next to the finders,
|
|
11
|
+
# their directions the finders' local axes. Mixed into Candidate.
|
|
12
|
+
module Strokes
|
|
13
|
+
# Offsets (in modules) along a stroke at which its centre is measured.
|
|
14
|
+
STROKE_OFFSETS = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0].freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Intersection of the centre lines of the BL finder's top ring stroke
|
|
19
|
+
# and the TR finder's left ring stroke, which meet at the centre of
|
|
20
|
+
# the bottom-right alignment pattern (module (d - 6.5, d - 6.5)) even
|
|
21
|
+
# under perspective. nil if the strokes cannot be traced.
|
|
22
|
+
def stroke_intersection
|
|
23
|
+
a = stroke(:bl_up) or return
|
|
24
|
+
b = stroke(:tr_left) or return
|
|
25
|
+
p = Geometry.intersect_lines(a, b) or return
|
|
26
|
+
guess = parallelogram_transform(@dimensions.first)&.apply(@dimensions.first - 6.5, @dimensions.first - 6.5)
|
|
27
|
+
return unless guess
|
|
28
|
+
|
|
29
|
+
reach = (@tl.distance(@tr) + @tl.distance(@bl)) * 0.25
|
|
30
|
+
Math.hypot(p[0] - guess[0], p[1] - guess[1]) < reach ? p : nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Centre line of one of the six inner ring strokes of the finders
|
|
34
|
+
# (memoized; nil if it cannot be traced or does not run along the
|
|
35
|
+
# expected symbol axis).
|
|
36
|
+
def stroke(name)
|
|
37
|
+
@strokes ||= {}
|
|
38
|
+
return @strokes[name] if @strokes.key?(name)
|
|
39
|
+
|
|
40
|
+
(rx, ry), (dx, dy) = @axes
|
|
41
|
+
p, ux, uy, ms, ax, ay =
|
|
42
|
+
case name
|
|
43
|
+
when :tl_right then [@tl, rx, ry, @tl_right, dx, dy]
|
|
44
|
+
when :tl_down then [@tl, dx, dy, @tl_down, rx, ry]
|
|
45
|
+
when :tr_left then [@tr, -rx, -ry, @tr_left, dx, dy]
|
|
46
|
+
when :tr_down then [@tr, dx, dy, @tr_down, rx, ry]
|
|
47
|
+
when :bl_up then [@bl, -dx, -dy, @bl_up, rx, ry]
|
|
48
|
+
when :bl_right then [@bl, rx, ry, @bl_right, dx, dy]
|
|
49
|
+
end
|
|
50
|
+
line = stroke_line(p, ux, uy, ms)
|
|
51
|
+
# The stroke must run roughly along the other symbol axis.
|
|
52
|
+
line = nil if line && (line[2] * ax + line[3] * ay).abs < 0.8
|
|
53
|
+
@strokes[name] = line
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Pixel positions of the grid points next to the finder patterns -
|
|
57
|
+
# modules (6.5, 6.5), (d - 6.5, 6.5) and (6.5, d - 6.5) - where the
|
|
58
|
+
# centre lines of each finder's two inner ring strokes cross.
|
|
59
|
+
# Returns {[gx, gy] => [x, y]} keyed by alignment grid index (n = last).
|
|
60
|
+
def finder_corners(n)
|
|
61
|
+
out = {}
|
|
62
|
+
[[@tl, %i[tl_right tl_down], [0, 0]], [@tr, %i[tr_left tr_down], [n, 0]],
|
|
63
|
+
[@bl, %i[bl_up bl_right], [0, n]]].each do |finder, (s1, s2), key|
|
|
64
|
+
a = stroke(s1) or next
|
|
65
|
+
b = stroke(s2) or next
|
|
66
|
+
p = Geometry.intersect_lines(a, b) or next
|
|
67
|
+
r = finder.distance_to(*p) / @module_size
|
|
68
|
+
out[key] = p if r > 3.0 && r < 5.5
|
|
69
|
+
end
|
|
70
|
+
out
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Centre line of the ring stroke of finder +p+ facing the unit
|
|
74
|
+
# direction (dx, dy), as [x, y, vx, vy]; nil if it cannot be traced.
|
|
75
|
+
# For points spread along the stroke, the walk starts inside the
|
|
76
|
+
# light ring and measures the dark stroke's extent along (dx, dy).
|
|
77
|
+
def stroke_line(p, dx, dy, ms)
|
|
78
|
+
return if ms < 1.5
|
|
79
|
+
|
|
80
|
+
bits = @matrix.bits
|
|
81
|
+
w = @matrix.width
|
|
82
|
+
h = @matrix.height
|
|
83
|
+
ex = -dy
|
|
84
|
+
ey = dx
|
|
85
|
+
step = ms / 6.0
|
|
86
|
+
step = 0.5 if step < 0.5
|
|
87
|
+
points = []
|
|
88
|
+
STROKE_OFFSETS.each do |s|
|
|
89
|
+
x0 = p.x + (ex * s + dx * 1.9) * ms
|
|
90
|
+
y0 = p.y + (ey * s + dy * 1.9) * ms
|
|
91
|
+
inside = nil
|
|
92
|
+
outside = nil
|
|
93
|
+
t = 0.0
|
|
94
|
+
while t <= 2.8 * ms
|
|
95
|
+
x = x0 + dx * t
|
|
96
|
+
y = y0 + dy * t
|
|
97
|
+
break unless x >= 0 && y >= 0 && x < w && y < h
|
|
98
|
+
|
|
99
|
+
dark = bits.getbyte(y.to_i * w + x.to_i) == 1
|
|
100
|
+
if inside.nil?
|
|
101
|
+
break if dark && t.zero?
|
|
102
|
+
|
|
103
|
+
inside = t if dark
|
|
104
|
+
break if !dark && t > 1.3 * ms
|
|
105
|
+
elsif !dark
|
|
106
|
+
outside = t
|
|
107
|
+
break
|
|
108
|
+
end
|
|
109
|
+
t += step
|
|
110
|
+
end
|
|
111
|
+
next unless inside && outside
|
|
112
|
+
|
|
113
|
+
width = outside - inside
|
|
114
|
+
next if width < 0.4 * ms || width > 1.8 * ms
|
|
115
|
+
|
|
116
|
+
mid = (inside + outside - step) * 0.5
|
|
117
|
+
points << [x0 + dx * mid, y0 + dy * mid]
|
|
118
|
+
end
|
|
119
|
+
return if points.size < 5
|
|
120
|
+
|
|
121
|
+
Geometry.fit_line(points, 0.4 * ms)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|