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,436 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "candidate/dimension"
|
|
4
|
+
require_relative "candidate/strokes"
|
|
5
|
+
|
|
6
|
+
module QrReader
|
|
7
|
+
class Detector
|
|
8
|
+
# A possible QR symbol located from three finder patterns.
|
|
9
|
+
#
|
|
10
|
+
# The geometry is estimated when the candidate is built
|
|
11
|
+
# (Candidate.locate); sampling happens lazily in #bits and
|
|
12
|
+
# #alternatives, so a reader that stops at the first decodable
|
|
13
|
+
# candidate only pays for what it uses.
|
|
14
|
+
#
|
|
15
|
+
# bits - sampled N x N BitMatrix (1 = dark)
|
|
16
|
+
# points - symbol corners [TL, TR, BR, BL] in pixel coordinates
|
|
17
|
+
# finder_patterns - finder centres [TL, TR, BL] (for debugging)
|
|
18
|
+
# alternatives - Enumerator of further samplings to try when +bits+
|
|
19
|
+
# does not decode, most promising first
|
|
20
|
+
# dimension - estimated modules per side
|
|
21
|
+
#
|
|
22
|
+
# The module grid is mapped to the image by a homography through the
|
|
23
|
+
# three finder centres and a fourth reference point at module
|
|
24
|
+
# (d - 6.5, d - 6.5): the bottom-right alignment pattern if one was
|
|
25
|
+
# found (mode :alignment), else the intersection of the finder ring
|
|
26
|
+
# strokes that point there (:strokes); failing both, the parallelogram
|
|
27
|
+
# corner at (d - 3.5, d - 3.5) is used (:parallelogram). Versions 7+ are
|
|
28
|
+
# sampled piecewise along their alignment pattern grid (AlignmentGrid).
|
|
29
|
+
class Candidate
|
|
30
|
+
include Dimension
|
|
31
|
+
include Strokes
|
|
32
|
+
|
|
33
|
+
# Timing pattern agreement above which a grid counts as plausible.
|
|
34
|
+
PLAUSIBLE_TIMING = 0.6
|
|
35
|
+
|
|
36
|
+
attr_reader :dimension, :dimensions, :score, :tl, :tr, :bl, :module_size, :mode
|
|
37
|
+
|
|
38
|
+
# The candidate for a PatternSets::Triple, or nil if the triple cannot
|
|
39
|
+
# be a symbol.
|
|
40
|
+
def self.locate(detector, triple)
|
|
41
|
+
candidate = new(detector, triple)
|
|
42
|
+
candidate.located? ? candidate : nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(detector, triple)
|
|
46
|
+
@detector = detector
|
|
47
|
+
@matrix = detector.matrix
|
|
48
|
+
@bl = triple.bl
|
|
49
|
+
@tl = triple.tl
|
|
50
|
+
@tr = triple.tr
|
|
51
|
+
@score = triple.score
|
|
52
|
+
@alignment = nil
|
|
53
|
+
@stroke_point = nil
|
|
54
|
+
@version_confirmed = false
|
|
55
|
+
@transforms = {}
|
|
56
|
+
@located = estimate_geometry
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# False if the geometry is unusable (then nothing else may be called).
|
|
60
|
+
def located?
|
|
61
|
+
@located
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The most likely sampling of the symbol.
|
|
65
|
+
def bits
|
|
66
|
+
@bits ||= begin
|
|
67
|
+
# Piecewise sampling costs an alignment pattern search per grid
|
|
68
|
+
# point: only spend it on grids that look like a real symbol.
|
|
69
|
+
piecewise = @dimension >= 45 && (@version_confirmed || promising?(@dimension, @mode))
|
|
70
|
+
sample(@dimension, @mode, piecewise: piecewise)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def finder_patterns
|
|
75
|
+
[@tl.to_a, @tr.to_a, @bl.to_a]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# True if +pattern+ (a FinderPattern) is one of this candidate's finders.
|
|
79
|
+
def uses?(pattern)
|
|
80
|
+
pattern.equal?(@tl) || pattern.equal?(@tr) || pattern.equal?(@bl)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Corners of the symbol as [[x, y], ...]: TL, TR, BR, BL.
|
|
84
|
+
def points
|
|
85
|
+
@points ||= begin
|
|
86
|
+
t = transform_for(@dimension, @mode)
|
|
87
|
+
d = @dimension
|
|
88
|
+
[[0, 0], [d, 0], [d, d], [0, d]].map { |x, y| t.apply(x, y) || [Float::NAN, Float::NAN] }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Further samplings worth trying when #bits does not decode: the
|
|
93
|
+
# global instead of the piecewise transform and the grayscale image
|
|
94
|
+
# (if the detector has it) when #bits looks like a symbol at all, then
|
|
95
|
+
# the other reference points and the other dimension estimates whose
|
|
96
|
+
# timing patterns read plausibly.
|
|
97
|
+
def alternatives
|
|
98
|
+
return enum_for(:alternatives) unless block_given?
|
|
99
|
+
|
|
100
|
+
seen = [bits]
|
|
101
|
+
try = lambda do |matrix|
|
|
102
|
+
next if matrix.nil? || seen.include?(matrix)
|
|
103
|
+
|
|
104
|
+
seen << matrix
|
|
105
|
+
yield matrix
|
|
106
|
+
end
|
|
107
|
+
d = @dimension
|
|
108
|
+
if plausible?
|
|
109
|
+
try.call(sample(d, @mode, piecewise: false)) if d >= 45
|
|
110
|
+
try.call(sample(d, @mode, piecewise: true, luminance: true)) if @detector.image
|
|
111
|
+
# The displacement field on an affine base suits bent surfaces,
|
|
112
|
+
# on the full homography perspective plus bending.
|
|
113
|
+
try.call(warped_sample(d, :parallelogram))
|
|
114
|
+
try.call(warped_sample(d, @mode)) if @mode != :parallelogram
|
|
115
|
+
end
|
|
116
|
+
%i[alignment strokes parallelogram].each do |m|
|
|
117
|
+
next if m == @mode || (m != :parallelogram && !reference(m))
|
|
118
|
+
|
|
119
|
+
try.call(sample(d, m, piecewise: true)) if promising?(d, m)
|
|
120
|
+
end
|
|
121
|
+
# Other dimensions with the first reference point that suits them
|
|
122
|
+
# (the alignment pattern may be a look-alike found for the first).
|
|
123
|
+
@dimensions.drop(1).each do |other|
|
|
124
|
+
mode = [@mode, :strokes, :parallelogram].uniq.find do |m|
|
|
125
|
+
(m == :parallelogram || reference(m)) && promising?(other, m)
|
|
126
|
+
end
|
|
127
|
+
try.call(sample(other, mode, piecewise: true)) if mode
|
|
128
|
+
end
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Fraction of the timing pattern modules of #bits (row and column 6
|
|
133
|
+
# between the finders) with the expected colour: about 0.5 for false
|
|
134
|
+
# finder triples and wrong dimensions, close to 1 for real symbols.
|
|
135
|
+
def timing_quality
|
|
136
|
+
@timing_quality ||= begin
|
|
137
|
+
grid = bits
|
|
138
|
+
d = grid.width
|
|
139
|
+
good = 0
|
|
140
|
+
i = 8
|
|
141
|
+
while i <= d - 9
|
|
142
|
+
expected = i.even? ? 1 : 0
|
|
143
|
+
good += 1 if grid.get(i, 6) == expected
|
|
144
|
+
good += 1 if grid.get(6, i) == expected
|
|
145
|
+
i += 1
|
|
146
|
+
end
|
|
147
|
+
good.fdiv(2 * (d - 16))
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# True unless the primary grid's timing patterns look like noise.
|
|
152
|
+
def plausible?
|
|
153
|
+
timing_quality >= PLAUSIBLE_TIMING
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Cheap estimate of #plausible? that reads only the timing pattern
|
|
157
|
+
# modules through the global transform (no full sampling). Symbols
|
|
158
|
+
# whose version information confirmed the dimension count as likely
|
|
159
|
+
# even when bending defeats the global transform (chance agreement of
|
|
160
|
+
# an 18-bit version block with the measured dimension is rare).
|
|
161
|
+
def likely?
|
|
162
|
+
@likely = @version_confirmed || promising?(@dimension, @mode) if @likely.nil?
|
|
163
|
+
@likely
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Identifies the finder pattern triple (for de-duplication by callers).
|
|
167
|
+
def key
|
|
168
|
+
[@tl, @tr, @bl].map { |p| [p.x.round, p.y.round] }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def inspect
|
|
172
|
+
format("#<Candidate dim=%d mode=%s tl=(%.1f,%.1f) tr=(%.1f,%.1f) bl=(%.1f,%.1f) score=%.3f>",
|
|
173
|
+
@dimension, @mode, @tl.x, @tl.y, @tr.x, @tr.y, @bl.x, @bl.y, @score)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
# Estimates module sizes, the dimension candidates, the bottom-right
|
|
179
|
+
# reference point and, for versions 7+, the version. False if the
|
|
180
|
+
# geometry is unusable.
|
|
181
|
+
def estimate_geometry
|
|
182
|
+
right = unit(@tr.x - @tl.x, @tr.y - @tl.y)
|
|
183
|
+
down = unit(@bl.x - @tl.x, @bl.y - @tl.y)
|
|
184
|
+
@axes = [right, down]
|
|
185
|
+
# Module sizes of each finder along the symbol's axes.
|
|
186
|
+
@tl_right = line_module_size(@tl, *right) || @tl.module_size
|
|
187
|
+
@tl_down = line_module_size(@tl, *down) || @tl.module_size
|
|
188
|
+
@tr_left = line_module_size(@tr, -right[0], -right[1]) || @tr.module_size
|
|
189
|
+
@tr_down = line_module_size(@tr, *down) || @tr.module_size
|
|
190
|
+
@bl_up = line_module_size(@bl, -down[0], -down[1]) || @bl.module_size
|
|
191
|
+
@bl_right = line_module_size(@bl, *right) || @bl.module_size
|
|
192
|
+
# Under a projective map the distance between two points is the
|
|
193
|
+
# module distance times the geometric mean of the local scales at
|
|
194
|
+
# both ends, hence geometric means.
|
|
195
|
+
ms_top = Math.sqrt(@tl_right * @tr_left)
|
|
196
|
+
ms_left = Math.sqrt(@tl_down * @bl_up)
|
|
197
|
+
@module_size = (ms_top + ms_left) * 0.5
|
|
198
|
+
top = @tl.distance(@tr) / ms_top + 7
|
|
199
|
+
left = @tl.distance(@bl) / ms_left + 7
|
|
200
|
+
estimate = (top + left) * 0.5
|
|
201
|
+
return false unless estimate.finite? && estimate > 15 && estimate < 200
|
|
202
|
+
|
|
203
|
+
@dimensions = dimension_candidates(estimate, top, left)
|
|
204
|
+
return false if @dimensions.empty?
|
|
205
|
+
|
|
206
|
+
@stroke_point = stroke_intersection
|
|
207
|
+
locate_alignment(@dimensions.find { |d| d > 21 } || 25)
|
|
208
|
+
@mode = if @alignment
|
|
209
|
+
:alignment
|
|
210
|
+
elsif @stroke_point
|
|
211
|
+
:strokes
|
|
212
|
+
else
|
|
213
|
+
:parallelogram
|
|
214
|
+
end
|
|
215
|
+
if (version_dim = version_dimension(@dimensions))
|
|
216
|
+
@dimensions.delete(version_dim)
|
|
217
|
+
@dimensions.unshift(version_dim)
|
|
218
|
+
end
|
|
219
|
+
@dimension = @dimensions.first
|
|
220
|
+
!transform_for(@dimension, @mode).nil?
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def unit(x, y)
|
|
224
|
+
len = Math.hypot(x, y)
|
|
225
|
+
len.zero? ? [0.0, 0.0] : [x / len, y / len]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Looks for the bottom-right alignment pattern around its predicted
|
|
229
|
+
# position (from the stroke intersection if known, else from the
|
|
230
|
+
# parallelogram), verifying candidates with the local module vectors.
|
|
231
|
+
def locate_alignment(dimension)
|
|
232
|
+
t = (@stroke_point && transform_for(dimension, :strokes)) || parallelogram_transform(dimension)
|
|
233
|
+
return unless t
|
|
234
|
+
|
|
235
|
+
c = dimension - 6.5
|
|
236
|
+
x, y = t.apply(c, c)
|
|
237
|
+
x1, y1 = t.apply(c + 1, c)
|
|
238
|
+
x2, y2 = t.apply(c, c + 1)
|
|
239
|
+
return unless x && x1 && x2
|
|
240
|
+
|
|
241
|
+
@alignment = @detector.alignment_finder.find(x, y, 8 * @module_size + 2, x1 - x, y1 - y, x2 - x, y2 - y)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Pixel position of the reference point for +mode+ (nil if unknown).
|
|
245
|
+
def reference(mode)
|
|
246
|
+
case mode
|
|
247
|
+
when :alignment then @alignment
|
|
248
|
+
when :strokes then @stroke_point
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Module-to-pixel transform for dimension d and reference +mode+;
|
|
253
|
+
# nil if degenerate.
|
|
254
|
+
def transform_for(d, mode)
|
|
255
|
+
key = [d, mode]
|
|
256
|
+
return @transforms[key] if @transforms.key?(key)
|
|
257
|
+
|
|
258
|
+
ref = reference(mode)
|
|
259
|
+
@transforms[key] =
|
|
260
|
+
if ref
|
|
261
|
+
c = d - 6.5
|
|
262
|
+
checked(PerspectiveTransform.quad_to_quad(
|
|
263
|
+
[[3.5, 3.5], [d - 3.5, 3.5], [c, c], [3.5, d - 3.5]],
|
|
264
|
+
[@tl.to_a, @tr.to_a, ref, @bl.to_a]
|
|
265
|
+
), d)
|
|
266
|
+
else
|
|
267
|
+
parallelogram_transform(d)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def parallelogram_transform(d)
|
|
272
|
+
br = [@tr.x - @tl.x + @bl.x, @tr.y - @tl.y + @bl.y]
|
|
273
|
+
checked(PerspectiveTransform.quad_to_quad(
|
|
274
|
+
[[3.5, 3.5], [d - 3.5, 3.5], [d - 3.5, d - 3.5], [3.5, d - 3.5]],
|
|
275
|
+
[@tl.to_a, @tr.to_a, br, @bl.to_a]
|
|
276
|
+
), d)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# The transform if it is regular over the whole symbol (no fold or
|
|
280
|
+
# point at infinity inside), else nil.
|
|
281
|
+
def checked(t, d)
|
|
282
|
+
t if t&.regular_over?(0, 0, d, d)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Bit at continuous pixel position (x, y), 0 outside the image.
|
|
286
|
+
def pixel(x, y)
|
|
287
|
+
return 0 unless x >= 0 && y >= 0 && x < @matrix.width && y < @matrix.height
|
|
288
|
+
|
|
289
|
+
@matrix.bits.getbyte(y.to_i * @matrix.width + x.to_i)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Bit of module (mx, my) under transform t (0 when outside the image
|
|
293
|
+
# or at infinity). Same as pixel(*t.apply(...)) without allocating.
|
|
294
|
+
def module_at(t, mx, my)
|
|
295
|
+
m = t.m
|
|
296
|
+
u = mx + 0.5
|
|
297
|
+
v = my + 0.5
|
|
298
|
+
den = m[6] * u + m[7] * v + m[8]
|
|
299
|
+
return 0 if den.abs < 1e-12
|
|
300
|
+
|
|
301
|
+
pixel((m[0] * u + m[1] * v + m[2]) / den, (m[3] * u + m[4] * v + m[5]) / den)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Fraction of the timing pattern modules (row and column 6 between the
|
|
305
|
+
# finder patterns) that read as expected under the transform for d.
|
|
306
|
+
# Wrong dimensions and false finder triples score about 0.5.
|
|
307
|
+
def timing_score(d, mode)
|
|
308
|
+
t = transform_for(d, mode) or return 0.0
|
|
309
|
+
good = 0
|
|
310
|
+
i = 8
|
|
311
|
+
while i <= d - 9
|
|
312
|
+
expected = i.even? ? 1 : 0
|
|
313
|
+
good += 1 if module_at(t, i, 6) == expected
|
|
314
|
+
good += 1 if module_at(t, 6, i) == expected
|
|
315
|
+
i += 1
|
|
316
|
+
end
|
|
317
|
+
good.fdiv(2 * (d - 16))
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Cheap check whether sampling dimension d with +mode+ is worthwhile.
|
|
321
|
+
def promising?(d, mode)
|
|
322
|
+
timing_score(d, mode) >= PLAUSIBLE_TIMING
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Samples dimension d through the transform for +mode+ plus a pixel
|
|
326
|
+
# displacement field measured on the timing patterns: where row 6's
|
|
327
|
+
# (column 6's) module boundaries really are, compared to where the
|
|
328
|
+
# transform puts them, gives a displacement per module column (row);
|
|
329
|
+
# module (x, y) is shifted by the sum of its column's and its row's
|
|
330
|
+
# displacement. This follows symbols on curved surfaces (bottles,
|
|
331
|
+
# cans, fabric) whose stretch a homography cannot express. nil if a
|
|
332
|
+
# timing pattern cannot be traced.
|
|
333
|
+
def warped_sample(d, mode)
|
|
334
|
+
t = transform_for(d, mode) or return
|
|
335
|
+
cols = timing_shift(t, d, :row, 6.5) or return
|
|
336
|
+
# Trace column 6 where the row displacement says it is.
|
|
337
|
+
rows = timing_shift(t, d, :column, 6.5 + timing_offset(t, cols[6], 6.5, 6.5)) or return
|
|
338
|
+
GridSampler.sample_warped(@matrix, d, t, cols, rows, offset: GridSampler.vote_offsets(t, d))
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Per module column (+axis+ :row, tracing timing row 6 at module
|
|
342
|
+
# coordinate +across+) or row (:column) the pixel displacement [dx, dy]
|
|
343
|
+
# between where the timing pattern's module boundaries are and where
|
|
344
|
+
# the transform puts them. The pattern is sampled through +t+ at 1/8
|
|
345
|
+
# module steps from inside one finder ring to inside the other; its
|
|
346
|
+
# d - 13 boundaries (7 .. d - 7) are matched in order with the
|
|
347
|
+
# observed colour changes. Displacements are interpolated linearly
|
|
348
|
+
# (0 at the finder centres, which anchor the transform). nil unless
|
|
349
|
+
# exactly the expected number of boundaries is seen.
|
|
350
|
+
def timing_shift(t, d, axis, across)
|
|
351
|
+
step = 0.125
|
|
352
|
+
observed = []
|
|
353
|
+
previous = nil
|
|
354
|
+
u = 4.0
|
|
355
|
+
stop = d - 4.0
|
|
356
|
+
while u <= stop
|
|
357
|
+
x, y = axis == :row ? t.apply(u, across) : t.apply(across, u)
|
|
358
|
+
bit = x ? pixel(x, y) : 0
|
|
359
|
+
observed << u - step * 0.5 if previous && bit != previous
|
|
360
|
+
previous = bit
|
|
361
|
+
u += step
|
|
362
|
+
end
|
|
363
|
+
return unless observed.size == d - 13
|
|
364
|
+
|
|
365
|
+
point = ->(c) { axis == :row ? t.apply(c, across) : t.apply(across, c) }
|
|
366
|
+
knots = [[3.5, 0.0, 0.0]]
|
|
367
|
+
observed.each_with_index do |o, i|
|
|
368
|
+
nominal = point.call(7 + i) or return
|
|
369
|
+
actual = point.call(o) or return
|
|
370
|
+
knots << [7 + i, actual[0] - nominal[0], actual[1] - nominal[1]]
|
|
371
|
+
end
|
|
372
|
+
knots << [d - 3.5, 0.0, 0.0]
|
|
373
|
+
Array.new(d) { |i| interpolate(knots, i + 0.5) }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Module-space offset along +axis+ equivalent to a pixel shift at
|
|
377
|
+
# module (u, v): used to find where a timing line really runs.
|
|
378
|
+
def timing_offset(t, shift, u, v, axis = :row)
|
|
379
|
+
p0 = t.apply(u, v) or return 0.0
|
|
380
|
+
p1 = axis == :row ? t.apply(u + 1, v) : t.apply(u, v + 1)
|
|
381
|
+
return 0.0 unless p1
|
|
382
|
+
|
|
383
|
+
ax = p1[0] - p0[0]
|
|
384
|
+
ay = p1[1] - p0[1]
|
|
385
|
+
len2 = ax * ax + ay * ay
|
|
386
|
+
len2.zero? ? 0.0 : (shift[0] * ax + shift[1] * ay) / len2
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Piecewise linear interpolation of [dx, dy] in knots sorted by x
|
|
390
|
+
# ([[x, dx, dy], ...]), extrapolating the end segments.
|
|
391
|
+
def interpolate(knots, x)
|
|
392
|
+
i = if x <= knots.first[0]
|
|
393
|
+
1
|
|
394
|
+
elsif x >= knots.last[0]
|
|
395
|
+
knots.size - 1
|
|
396
|
+
else
|
|
397
|
+
knots.bsearch_index { |k| k[0] >= x }
|
|
398
|
+
end
|
|
399
|
+
x0, ax, ay = knots[i - 1]
|
|
400
|
+
x1, bx, by = knots[i]
|
|
401
|
+
f = (x - x0) / (x1 - x0)
|
|
402
|
+
[ax + (bx - ax) * f, ay + (by - ay) * f]
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Piecewise sampling regions for dimension d (versions 7+), memoized.
|
|
406
|
+
def regions(d, mode)
|
|
407
|
+
return if d < 45
|
|
408
|
+
|
|
409
|
+
@regions ||= {}
|
|
410
|
+
key = [d, mode]
|
|
411
|
+
return @regions[key] if @regions.key?(key)
|
|
412
|
+
|
|
413
|
+
t = transform_for(d, mode)
|
|
414
|
+
@regions[key] = t && AlignmentGrid.new(@detector, t, d, @module_size, ->(n) { finder_corners(n) }).regions
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Samples the grid for dimension d: piecewise along the alignment
|
|
418
|
+
# pattern grid for versions 7+ if requested; from the grayscale image
|
|
419
|
+
# with module-level thresholds if +luminance+, else from the binarized
|
|
420
|
+
# image (majority of five pixels per module along each axis in which
|
|
421
|
+
# modules span 3+ px).
|
|
422
|
+
def sample(d, mode, piecewise:, luminance: false)
|
|
423
|
+
t = transform_for(d, mode) or return
|
|
424
|
+
pieces = (regions(d, mode) if piecewise) || GridSampler.whole(d, t)
|
|
425
|
+
offset = GridSampler.vote_offsets(t, d)
|
|
426
|
+
if luminance
|
|
427
|
+
image = @detector.image or return
|
|
428
|
+
GridSampler.sample_luminance(image, d, pieces, @module_size, inverted: @detector.inverted_image?,
|
|
429
|
+
offset: offset)
|
|
430
|
+
else
|
|
431
|
+
GridSampler.sample(@matrix, d, pieces, offset: offset)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# A located finder pattern (or alignment pattern).
|
|
6
|
+
#
|
|
7
|
+
# x, y - centre in continuous pixel coordinates (pixel (i, j)
|
|
8
|
+
# covers [i, i + 1) x [j, j + 1), so its centre is i + 0.5)
|
|
9
|
+
# module_size - estimated module size in pixels (rotation compensated)
|
|
10
|
+
# count - number of scan rows that hit this pattern
|
|
11
|
+
# quality - 0..1, how consistent the cross-check spreads were
|
|
12
|
+
class FinderPattern
|
|
13
|
+
attr_reader :x, :y, :module_size, :count
|
|
14
|
+
attr_accessor :quality
|
|
15
|
+
|
|
16
|
+
def initialize(x, y, module_size, count = 1, quality = 1.0)
|
|
17
|
+
@x = x
|
|
18
|
+
@y = y
|
|
19
|
+
@module_size = module_size
|
|
20
|
+
@count = count
|
|
21
|
+
@quality = quality
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Records another scan row crossing this pattern.
|
|
25
|
+
def add_hit
|
|
26
|
+
@count += 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def distance(other)
|
|
30
|
+
Math.hypot(@x - other.x, @y - other.y)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def distance_to(x, y)
|
|
34
|
+
Math.hypot(@x - x, @y - y)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# True if (x, y) lies within +modules+ module sizes of the centre.
|
|
38
|
+
def near?(x, y, modules)
|
|
39
|
+
r = @module_size * modules
|
|
40
|
+
dx = @x - x
|
|
41
|
+
dy = @y - y
|
|
42
|
+
dx * dx + dy * dy < r * r
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Count-weighted average with another estimate of the same pattern.
|
|
46
|
+
def merge(other)
|
|
47
|
+
n = @count + other.count
|
|
48
|
+
FinderPattern.new((@x * @count + other.x * other.count) / n,
|
|
49
|
+
(@y * @count + other.y * other.count) / n,
|
|
50
|
+
(@module_size * @count + other.module_size * other.count) / n, n,
|
|
51
|
+
[@quality, other.quality].max)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_a
|
|
55
|
+
[@x, @y]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def inspect
|
|
59
|
+
format("#<FinderPattern (%.1f, %.1f) ms=%.2f n=%d>", @x, @y, @module_size, @count)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# Finds QR finder patterns - concentric 7x7, 5x5 and 3x3 squares that
|
|
6
|
+
# read dark-light-dark-light-dark in the ratio 1:1:3:1:1 along every
|
|
7
|
+
# line through their centre - in a binarized image.
|
|
8
|
+
#
|
|
9
|
+
# Rows are scanned as run lengths at C speed (String#index on the row's
|
|
10
|
+
# bytes). A 1:1:3:1:1 hit first passes a cheap vertical probe (stone
|
|
11
|
+
# height, light and dark ring), then cross-checks vertically,
|
|
12
|
+
# horizontally and along both diagonals; its centre is refined to the
|
|
13
|
+
# centroid of the 3x3 centre "stone" and its module size is estimated
|
|
14
|
+
# from the four spreads in a way that compensates for rotation. Along
|
|
15
|
+
# directions with modules of 3+ px, single noise pixels inside runs are
|
|
16
|
+
# bridged. Pixels need not be square (fax pages).
|
|
17
|
+
class FinderPatternFinder
|
|
18
|
+
DARK = BitMatrix::DARK
|
|
19
|
+
LIGHT = BitMatrix::LIGHT
|
|
20
|
+
# In normal mode rows are skipped so that a symbol of up to 97 modules
|
|
21
|
+
# (version 20) filling 3/4 of the image height is still hit 3 times.
|
|
22
|
+
MAX_MODULES_FAST = 97
|
|
23
|
+
MIN_SKIP = 3
|
|
24
|
+
# ... and then, where the image is not textured, every FINE_SKIP-th
|
|
25
|
+
# row, so that finder patterns down to about 2 px modules are still
|
|
26
|
+
# crossed by several rows on large page scans (whose coarser pyramid
|
|
27
|
+
# levels are too small for such symbols).
|
|
28
|
+
FINE_SKIP = 3
|
|
29
|
+
# A row neighbourhood whose sparse rows have more 1:1:3:1:1 ratio hits
|
|
30
|
+
# than this per 1000 px is texture (noise, halftone screens): those
|
|
31
|
+
# are expensive to scan densely and only scanned with try_harder.
|
|
32
|
+
# Documents and symbols have below 4.
|
|
33
|
+
TEXTURE_HITS = 8.0
|
|
34
|
+
# More patterns than this (noise, textures) are pruned by quality.
|
|
35
|
+
MAX_PATTERNS = 48
|
|
36
|
+
SQRT2 = Math.sqrt(2)
|
|
37
|
+
# Found patterns are indexed in a grid of cells of this many pixels
|
|
38
|
+
# for the "already found here?" test of every row hit.
|
|
39
|
+
CELL_SHIFT = 5
|
|
40
|
+
|
|
41
|
+
def initialize(matrix)
|
|
42
|
+
@bits = matrix.bits
|
|
43
|
+
@width = matrix.width
|
|
44
|
+
@height = matrix.height
|
|
45
|
+
# Patterns found so far, flat (see QrReaderKernels.finder_scan).
|
|
46
|
+
@state = []
|
|
47
|
+
@scanned = "\0".b * @height
|
|
48
|
+
# Ratio hits per scanned row.
|
|
49
|
+
@hits = Array.new(@height, 0)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Finder patterns found so far after scanning every few rows (or every
|
|
53
|
+
# row with try_harder), at most +limit+ (the most consistent ones).
|
|
54
|
+
# Rows already scanned by an earlier call are not scanned again.
|
|
55
|
+
def find(try_harder: false, limit: MAX_PATTERNS)
|
|
56
|
+
if try_harder
|
|
57
|
+
scan(rows_every(1))
|
|
58
|
+
else
|
|
59
|
+
skip = (3 * @height) / (4 * MAX_MODULES_FAST)
|
|
60
|
+
skip = MIN_SKIP if skip < MIN_SKIP
|
|
61
|
+
scan(rows_every(skip))
|
|
62
|
+
scan(quiet_rows(FINE_SKIP, 2 * skip)) if skip > FINE_SKIP
|
|
63
|
+
end
|
|
64
|
+
prune(merge_duplicates(patterns), limit)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Rows y = skip - 1, 2 * skip - 1, ... (all for skip 1) not scanned
|
|
70
|
+
# yet, as a String with 1 at their index (nil if none); marks them
|
|
71
|
+
# as scanned.
|
|
72
|
+
def rows_every(skip)
|
|
73
|
+
rows = nil
|
|
74
|
+
y = skip == 1 ? 0 : skip - 1
|
|
75
|
+
while y < @height
|
|
76
|
+
if @scanned.getbyte(y).zero?
|
|
77
|
+
@scanned.setbyte(y, 1)
|
|
78
|
+
(rows ||= "\0".b * @height).setbyte(y, 1)
|
|
79
|
+
end
|
|
80
|
+
y += skip
|
|
81
|
+
end
|
|
82
|
+
rows
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Like rows_every(skip), but only rows whose neighbourhood (+reach+
|
|
86
|
+
# rows up and down) had at most TEXTURE_HITS ratio hits per 1000 px
|
|
87
|
+
# in the rows scanned so far.
|
|
88
|
+
def quiet_rows(skip, reach)
|
|
89
|
+
# Prefix sums of hits and scanned rows.
|
|
90
|
+
hits = Array.new(@height + 1, 0)
|
|
91
|
+
count = Array.new(@height + 1, 0)
|
|
92
|
+
y = 0
|
|
93
|
+
while y < @height
|
|
94
|
+
scanned = @scanned.getbyte(y)
|
|
95
|
+
hits[y + 1] = hits[y] + (scanned == 1 ? @hits[y] : 0)
|
|
96
|
+
count[y + 1] = count[y] + scanned
|
|
97
|
+
y += 1
|
|
98
|
+
end
|
|
99
|
+
limit = TEXTURE_HITS * @width / 1000.0
|
|
100
|
+
rows = nil
|
|
101
|
+
y = skip - 1
|
|
102
|
+
while y < @height
|
|
103
|
+
if @scanned.getbyte(y).zero?
|
|
104
|
+
y0 = y > reach ? y - reach : 0
|
|
105
|
+
y1 = y + reach + 1 < @height ? y + reach + 1 : @height
|
|
106
|
+
n = count[y1] - count[y0]
|
|
107
|
+
if n.zero? || hits[y1] - hits[y0] <= limit * n
|
|
108
|
+
@scanned.setbyte(y, 1)
|
|
109
|
+
(rows ||= "\0".b * @height).setbyte(y, 1)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
y += skip
|
|
113
|
+
end
|
|
114
|
+
rows
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Scans +rows+ (see rows_every; nil: nothing to do).
|
|
118
|
+
def scan(rows)
|
|
119
|
+
return unless rows
|
|
120
|
+
|
|
121
|
+
result = QrReaderKernels.finder_scan(@bits, @width, @height, rows, @state)
|
|
122
|
+
values = result.size - @height
|
|
123
|
+
@state = result.first(values)
|
|
124
|
+
y = 0
|
|
125
|
+
while y < @height
|
|
126
|
+
@hits[y] = result[values + y].to_i if rows.getbyte(y) == 1
|
|
127
|
+
y += 1
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# FinderPatterns of the flat scan state, in the order found.
|
|
132
|
+
def patterns
|
|
133
|
+
state = @state
|
|
134
|
+
fields = QrReaderKernels::FINDER_FIELDS
|
|
135
|
+
out = []
|
|
136
|
+
i = 0
|
|
137
|
+
while i < state.size
|
|
138
|
+
out << FinderPattern.new(state[i], state[i + 1], state[i + 2], state[i + 4].to_i, state[i + 3])
|
|
139
|
+
i += fields
|
|
140
|
+
end
|
|
141
|
+
out
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Merges hits of the same pattern (centres within 2 modules, similar
|
|
145
|
+
# module sizes) into count-weighted averages; each hit joins the
|
|
146
|
+
# earliest matching pattern. Candidates come from a grid of cells, so
|
|
147
|
+
# textured photos with a thousand hits do not cost a million tests.
|
|
148
|
+
def merge_duplicates(patterns)
|
|
149
|
+
merged = []
|
|
150
|
+
grid = {}
|
|
151
|
+
patterns.each do |p|
|
|
152
|
+
i = mergeable(merged, grid, p)
|
|
153
|
+
if i
|
|
154
|
+
merged[i] = merged[i].merge(p)
|
|
155
|
+
else
|
|
156
|
+
i = merged.size
|
|
157
|
+
merged << p
|
|
158
|
+
end
|
|
159
|
+
q = merged[i]
|
|
160
|
+
(grid[cell_key(q.x, q.y)] ||= []) << i
|
|
161
|
+
end
|
|
162
|
+
merged
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Index of the earliest pattern in +merged+ that +p+ belongs to, or nil.
|
|
166
|
+
# A match lies within 2 of its own module sizes, at most 1.5 times
|
|
167
|
+
# p's, so cells within 3 of p's module sizes are searched.
|
|
168
|
+
def mergeable(merged, grid, p)
|
|
169
|
+
reach = 3 * p.module_size
|
|
170
|
+
x0 = ((p.x - reach).to_i >> CELL_SHIFT).clamp(0, @width >> CELL_SHIFT)
|
|
171
|
+
x1 = ((p.x + reach).to_i >> CELL_SHIFT).clamp(0, @width >> CELL_SHIFT)
|
|
172
|
+
y0 = ((p.y - reach).to_i >> CELL_SHIFT).clamp(0, @height >> CELL_SHIFT)
|
|
173
|
+
y1 = ((p.y + reach).to_i >> CELL_SHIFT).clamp(0, @height >> CELL_SHIFT)
|
|
174
|
+
best = nil
|
|
175
|
+
y0.upto(y1) do |cy|
|
|
176
|
+
x0.upto(x1) do |cx|
|
|
177
|
+
grid[(cy << 20) | cx]&.each do |i|
|
|
178
|
+
next if best && i >= best
|
|
179
|
+
|
|
180
|
+
q = merged[i]
|
|
181
|
+
best = i if q.near?(p.x, p.y, 2.0) && (p.module_size / q.module_size).between?(0.67, 1.5)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
best
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def cell_key(x, y)
|
|
189
|
+
((y.to_i >> CELL_SHIFT) << 20) | (x.to_i >> CELL_SHIFT)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def prune(patterns, limit)
|
|
193
|
+
return patterns if patterns.size <= limit
|
|
194
|
+
|
|
195
|
+
patterns.max_by(limit) { |p| p.quality * Math.sqrt(p.count) }
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|