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,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "decoder/galois_field"
|
|
4
|
+
require_relative "decoder/reed_solomon"
|
|
5
|
+
require_relative "decoder/function_patterns"
|
|
6
|
+
require_relative "decoder/data_mask"
|
|
7
|
+
require_relative "decoder/codewords"
|
|
8
|
+
require_relative "decoder/format_reader"
|
|
9
|
+
require_relative "decoder/character_set"
|
|
10
|
+
require_relative "decoder/bitstream_parser"
|
|
11
|
+
|
|
12
|
+
module QrReader
|
|
13
|
+
# Decodes a sampled QR code module grid (ISO/IEC 18004:2015, Model 2).
|
|
14
|
+
#
|
|
15
|
+
# result = QrReader::Decoder.decode(bit_matrix) # => Result or nil
|
|
16
|
+
# QrReader::Decoder.diagnose(bit_matrix) # => { status: :reed_solomon, ... }
|
|
17
|
+
#
|
|
18
|
+
# The pipeline is ordered so that garbage fails as early and cheaply as
|
|
19
|
+
# possible (the detector may try many candidate grids): dimension, format
|
|
20
|
+
# information (at most two candidates per orientation when the copies
|
|
21
|
+
# disagree), version information, then the RS blocks one at a time, and
|
|
22
|
+
# only then the bit stream.
|
|
23
|
+
module Decoder
|
|
24
|
+
# Diagnostic statuses in pipeline order (later = got further).
|
|
25
|
+
STATUSES = %i[dimension format_info version_info reed_solomon bitstream ok].freeze
|
|
26
|
+
|
|
27
|
+
# Array#pack formats "C<count>" that take the first +count+ codewords of
|
|
28
|
+
# an RS block (at most 123 data codewords per block).
|
|
29
|
+
DATA_FORMATS = Array.new(124) { |count| "C#{count}".freeze }.freeze
|
|
30
|
+
# Strength order of the EC levels.
|
|
31
|
+
EC_STRENGTH = { L: 0, M: 1, Q: 2, H: 3 }.freeze
|
|
32
|
+
private_constant :DATA_FORMATS, :EC_STRENGTH
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Decodes +bit_matrix+ (N x N, N = 21..177, 1 = dark, no quiet zone).
|
|
36
|
+
# Returns a Result (with points: nil) or nil when the grid is not a
|
|
37
|
+
# decodable symbol. With +try_mirrored+, the grid is also read
|
|
38
|
+
# transposed (first, if its format information reads better that way);
|
|
39
|
+
# a Result read so is marked mirrored.
|
|
40
|
+
#
|
|
41
|
+
# Never raises for any grid content; raises TypeError if +bit_matrix+
|
|
42
|
+
# is not a QrReader::BitMatrix.
|
|
43
|
+
def decode(bit_matrix, try_mirrored: true)
|
|
44
|
+
bits = module_bits(bit_matrix)
|
|
45
|
+
bits && run(bits, bit_matrix.width, try_mirrored, nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Runs the decoder with bookkeeping and returns a Hash describing how far
|
|
49
|
+
# it got: :status (one of STATUSES), :mirrored, :dimension, :format_bits,
|
|
50
|
+
# :ec_level (:l, :m, :q, :h), :mask, :format_distance, :version, :version_bits,
|
|
51
|
+
# :version_info (decoded version numbers of both copies), :blocks,
|
|
52
|
+
# :block_errors (corrected codewords per RS block, nil = uncorrectable),
|
|
53
|
+
# :errors_corrected, :error (bit stream message) and :result.
|
|
54
|
+
#
|
|
55
|
+
# Makes exactly the attempts #decode makes, so :status is :ok exactly
|
|
56
|
+
# when #decode returns a Result (then :result is that Result);
|
|
57
|
+
# otherwise the attempt that got furthest is reported.
|
|
58
|
+
def diagnose(bit_matrix, try_mirrored: true)
|
|
59
|
+
bits = module_bits(bit_matrix)
|
|
60
|
+
return { status: :dimension, mirrored: false, dimension: [bit_matrix.width, bit_matrix.height] } unless bits
|
|
61
|
+
|
|
62
|
+
diags = []
|
|
63
|
+
result = run(bits, bit_matrix.width, try_mirrored, diags)
|
|
64
|
+
return diags.find { |diag| diag[:result].equal?(result) } if result
|
|
65
|
+
|
|
66
|
+
diags.max_by.with_index { |diag, i| [STATUSES.index(diag[:status]), -i] }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Tries the format information candidates of the readings in turn and
|
|
72
|
+
# returns the first Result, or nil. With +diags+ (an Array), a Hash per
|
|
73
|
+
# attempt is appended (an attempt without format information too).
|
|
74
|
+
#
|
|
75
|
+
# A code word of a stronger EC level is also one of a weaker level with
|
|
76
|
+
# the same block layout (its generator has the weaker one's roots and
|
|
77
|
+
# more), so when damaged format information names both levels with the
|
|
78
|
+
# same mask, the weaker reading would succeed - with the wrong level and,
|
|
79
|
+
# for a symbol filled to capacity, parity bytes parsed as data. The
|
|
80
|
+
# same-mask runner-up is therefore tried as well and the stronger
|
|
81
|
+
# successful reading wins.
|
|
82
|
+
def run(bits, n, try_mirrored, diags)
|
|
83
|
+
readings(bits, n, try_mirrored).each do |mirrored, words, candidates|
|
|
84
|
+
candidates = [nil] if candidates.empty? && diags
|
|
85
|
+
candidates.each_with_index do |format, k|
|
|
86
|
+
result = attempt(bits, n, mirrored, words, format, diags && (diags << { mirrored: mirrored }).last)
|
|
87
|
+
next unless result
|
|
88
|
+
|
|
89
|
+
sibling = candidates[k + 1]
|
|
90
|
+
if sibling && stronger_reading?(sibling[0], format[0])
|
|
91
|
+
stronger = attempt(bits, n, mirrored, words, sibling, diags && (diags << { mirrored: mirrored }).last)
|
|
92
|
+
return stronger if stronger
|
|
93
|
+
end
|
|
94
|
+
return result
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# True if +other+ (FormatInformation) has the mask of +info+ and a
|
|
101
|
+
# stronger EC level.
|
|
102
|
+
def stronger_reading?(other, info)
|
|
103
|
+
other.mask == info.mask && EC_STRENGTH[other.ec_level] > EC_STRENGTH[info.ec_level]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The module String with only 0/1 bytes, or nil if the matrix cannot be
|
|
107
|
+
# a symbol (not square, wrong dimension, inconsistent storage).
|
|
108
|
+
def module_bits(matrix)
|
|
109
|
+
raise TypeError, "expected a QrReader::BitMatrix, got #{matrix.class}" unless matrix.is_a?(BitMatrix)
|
|
110
|
+
|
|
111
|
+
n = matrix.width
|
|
112
|
+
bits = matrix.bits
|
|
113
|
+
return nil unless n.is_a?(Integer) && bits.is_a?(String) && matrix.height == n &&
|
|
114
|
+
SymbolVersion.for_dimension(n) && bits.bytesize == n * n
|
|
115
|
+
|
|
116
|
+
# Byte-wise operations below need a binary String (the BitMatrix
|
|
117
|
+
# constructor ensures one, but the String is mutable).
|
|
118
|
+
bits = bits.b unless bits.encoding == Encoding::BINARY
|
|
119
|
+
# Any byte other than 1 counts as light (like BitMatrix#dark?).
|
|
120
|
+
bits.count("\x00\x01") == bits.bytesize ? bits : bits.tr("^\x01", "\x00")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The orientations worth trying, the one whose format information reads
|
|
124
|
+
# better first: Array of [mirrored, format_words, format_candidates].
|
|
125
|
+
#
|
|
126
|
+
# A symbol read the wrong way round yields format words at distance 2
|
|
127
|
+
# or more from every (masked) code word, so when both copies of one
|
|
128
|
+
# orientation read the same exact code word and the other orientation
|
|
129
|
+
# is at least 2 away, only the first is tried. (Copies written without
|
|
130
|
+
# the 0x5412 mask can read exactly both ways round; then both
|
|
131
|
+
# orientations are tried.)
|
|
132
|
+
def readings(bits, n, try_mirrored)
|
|
133
|
+
normal = reading(bits, n, false)
|
|
134
|
+
return [normal] unless try_mirrored
|
|
135
|
+
|
|
136
|
+
mirrored = reading(bits, n, true)
|
|
137
|
+
first, second = best_distance(mirrored) < best_distance(normal) ? [mirrored, normal] : [normal, mirrored]
|
|
138
|
+
words = first[1]
|
|
139
|
+
exact = words[0] == words[1] && best_distance(first).zero?
|
|
140
|
+
exact && best_distance(second) >= 2 ? [first] : [first, second]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def reading(bits, n, mirrored)
|
|
144
|
+
words = FormatReader.format_bits(bits, n, mirrored: mirrored)
|
|
145
|
+
[mirrored, words, FormatReader.format_candidates(*words)]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Smallest format distance of a reading (4 when nothing is within 3).
|
|
149
|
+
def best_distance(reading)
|
|
150
|
+
best = 4
|
|
151
|
+
reading[2].each { |_, distance| best = distance if distance < best }
|
|
152
|
+
best
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# One decoding pass in one orientation, given the format information
|
|
156
|
+
# words read for it and the format information to assume
|
|
157
|
+
# ([FormatInformation, distance] or nil). Returns a Result or nil;
|
|
158
|
+
# fills +diag+ if given.
|
|
159
|
+
def attempt(bits, n, mirrored, format_words, format, diag)
|
|
160
|
+
version = SymbolVersion.for_dimension(n)
|
|
161
|
+
diag&.merge!(dimension: n, version: version.number, format_bits: format_words)
|
|
162
|
+
return failure(diag, :format_info) unless format
|
|
163
|
+
|
|
164
|
+
info, distance = format
|
|
165
|
+
level = info.ec_level
|
|
166
|
+
mask = info.mask
|
|
167
|
+
diag&.merge!(ec_level: level.downcase, mask: mask, format_distance: distance)
|
|
168
|
+
return failure(diag, :version_info) unless version_info_consistent?(bits, n, version, mirrored, diag)
|
|
169
|
+
|
|
170
|
+
data, corrected = correct_blocks(bits, version, level, mask, mirrored, diag)
|
|
171
|
+
return failure(diag, :reed_solomon) unless data
|
|
172
|
+
|
|
173
|
+
content = begin
|
|
174
|
+
BitstreamParser.parse(data, version.number)
|
|
175
|
+
rescue FormatError => e
|
|
176
|
+
diag[:error] = e.message if diag
|
|
177
|
+
return failure(diag, :bitstream)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
result = Result.new(text: content.text, raw_bytes: content.raw_bytes, version: version.number,
|
|
181
|
+
ec_level: level.downcase, mask: mask, mirrored: mirrored, errors_corrected: corrected,
|
|
182
|
+
eci: content.eci, fnc1: content.fnc1, structured_append: content.structured_append,
|
|
183
|
+
symbology_identifier: content.symbology_identifier, segments: content.segments)
|
|
184
|
+
diag&.merge!(status: :ok, result: result)
|
|
185
|
+
result
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def failure(diag, status)
|
|
189
|
+
diag[:status] = status if diag
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Versions 7+ carry their version number twice. The dimension decides:
|
|
194
|
+
# the grid is rejected only when both copies name the same other
|
|
195
|
+
# version (it was most likely sampled at the wrong size - a hint for
|
|
196
|
+
# the detector). Unreadable copies, or a single copy damaged into
|
|
197
|
+
# another version's code word, fall back to the dimension; Reed-Solomon
|
|
198
|
+
# has the final word.
|
|
199
|
+
def version_info_consistent?(bits, n, version, mirrored, diag)
|
|
200
|
+
return true if version.number < 7
|
|
201
|
+
|
|
202
|
+
copies = FormatReader.version_bits(bits, n, mirrored: mirrored)
|
|
203
|
+
diag[:version_bits] = copies if diag
|
|
204
|
+
return true if copies.include?(version.version_info_bits) && !diag
|
|
205
|
+
|
|
206
|
+
first = FormatReader.decode_version(copies[0])
|
|
207
|
+
second = FormatReader.decode_version(copies[1])
|
|
208
|
+
diag[:version_info] = [first, second] if diag
|
|
209
|
+
first.nil? || first != second || first == version.number
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Reads, error-corrects and concatenates the data codewords of all RS
|
|
213
|
+
# blocks. Returns [data String, corrected count] or nil at the first
|
|
214
|
+
# uncorrectable block (with +diag+, all blocks are checked).
|
|
215
|
+
def correct_blocks(bits, version, level, mask, mirrored, diag)
|
|
216
|
+
layout = Codewords.layout(version.number)
|
|
217
|
+
order = mirrored ? layout.mirrored_order : layout.order
|
|
218
|
+
mask_codewords = layout.mask_codewords(mask)
|
|
219
|
+
ec_count = version.ec_blocks(level).ec_codewords_per_block
|
|
220
|
+
blocks = layout.blocks(level)
|
|
221
|
+
data = "".b
|
|
222
|
+
corrected = 0
|
|
223
|
+
block_errors = diag ? [] : nil
|
|
224
|
+
blocks.each do |data_count, indices|
|
|
225
|
+
codewords = Codewords.read_block(bits, order, mask_codewords, indices)
|
|
226
|
+
fixed = ReedSolomon.decode(codewords, ec_count)
|
|
227
|
+
block_errors << fixed if diag
|
|
228
|
+
if fixed
|
|
229
|
+
corrected += fixed
|
|
230
|
+
data << codewords.pack(DATA_FORMATS[data_count])
|
|
231
|
+
elsif !diag
|
|
232
|
+
return nil
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
failed = diag && block_errors.include?(nil)
|
|
236
|
+
diag&.merge!(blocks: blocks.size, block_errors: block_errors, errors_corrected: failed ? nil : corrected)
|
|
237
|
+
failed ? nil : [data, corrected]
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# Piecewise sampling support for versions 7+ (zxing-cpp's approach):
|
|
6
|
+
# locates every alignment pattern, predicting each one from its already
|
|
7
|
+
# located neighbours, and returns one local transform per cell of the
|
|
8
|
+
# alignment pattern grid. This follows bent, curved or unevenly
|
|
9
|
+
# stretched symbols far better than a single homography.
|
|
10
|
+
class AlignmentGrid
|
|
11
|
+
# +finder_corners+ - callable taking the last grid index n and
|
|
12
|
+
# returning measured positions of the grid points next to the finder
|
|
13
|
+
# patterns ({[x, y] => [px, py]}, possibly incomplete).
|
|
14
|
+
def initialize(detector, transform, dimension, module_size, finder_corners = nil)
|
|
15
|
+
@finder = detector.alignment_finder
|
|
16
|
+
@transform = transform
|
|
17
|
+
@dimension = dimension
|
|
18
|
+
@module_size = module_size
|
|
19
|
+
@finder_corners = finder_corners
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# [[x0, x1, y0, y1, transform], ...] covering the whole symbol, or nil
|
|
23
|
+
# if the version has no alignment grid or a cell is degenerate.
|
|
24
|
+
def regions
|
|
25
|
+
version = SymbolVersion.for_dimension(@dimension) or return
|
|
26
|
+
centres = version.alignment_centers
|
|
27
|
+
n = centres.size - 1
|
|
28
|
+
return if n < 1
|
|
29
|
+
|
|
30
|
+
positions = locate_all(centres, n)
|
|
31
|
+
return if positions.any? { |row| row.any?(&:nil?) }
|
|
32
|
+
|
|
33
|
+
out = []
|
|
34
|
+
n.times do |y|
|
|
35
|
+
n.times do |x|
|
|
36
|
+
x0 = centres[x]
|
|
37
|
+
x1 = centres[x + 1]
|
|
38
|
+
y0 = centres[y]
|
|
39
|
+
y1 = centres[y + 1]
|
|
40
|
+
t = PerspectiveTransform.quad_to_quad(
|
|
41
|
+
[[x0 + 0.5, y0 + 0.5], [x1 + 0.5, y0 + 0.5], [x1 + 0.5, y1 + 0.5], [x0 + 0.5, y1 + 0.5]],
|
|
42
|
+
[positions[y][x], positions[y][x + 1], positions[y + 1][x + 1], positions[y + 1][x]]
|
|
43
|
+
)
|
|
44
|
+
# Cells on the border also cover the modules out to the edge.
|
|
45
|
+
region = [x.zero? ? 0 : x0, x == n - 1 ? @dimension : x1,
|
|
46
|
+
y.zero? ? 0 : y0, y == n - 1 ? @dimension : y1]
|
|
47
|
+
return unless t&.regular_over?(region[0], region[2], region[1], region[3])
|
|
48
|
+
|
|
49
|
+
out << [*region, t]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
out
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Pixel positions of all grid points ([y][x]); points next to the
|
|
58
|
+
# finder patterns and points that cannot be found are projected.
|
|
59
|
+
def locate_all(centres, n)
|
|
60
|
+
found = Array.new(n + 1) { Array.new(n + 1) }
|
|
61
|
+
@finder_corners&.call(n)&.each { |(x, y), p| found[y][x] = p }
|
|
62
|
+
project = ->(x, y) { @transform.apply(centres[x] + 0.5, centres[y] + 0.5) }
|
|
63
|
+
best = ->(x, y) { found[y][x] || project.call(x, y) }
|
|
64
|
+
finder_adjacent = ->(x, y) { (x.zero? && y.zero?) || (x.zero? && y == n) || (x == n && y.zero?) }
|
|
65
|
+
|
|
66
|
+
(0..n).each do |y|
|
|
67
|
+
(0..n).each do |x|
|
|
68
|
+
next if finder_adjacent.call(x, y) || found[y][x]
|
|
69
|
+
|
|
70
|
+
guess =
|
|
71
|
+
if x.zero? || y.zero?
|
|
72
|
+
project.call(x, y)
|
|
73
|
+
else
|
|
74
|
+
a = best.call(x - 1, y)
|
|
75
|
+
b = best.call(x, y - 1)
|
|
76
|
+
c = best.call(x - 1, y - 1)
|
|
77
|
+
a && b && c ? [a[0] + b[0] - c[0], a[1] + b[1] - c[1]] : project.call(x, y)
|
|
78
|
+
end
|
|
79
|
+
found[y][x] = search(guess, centres[x], centres[y]) if guess
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
(0..n).map do |y|
|
|
84
|
+
(0..n).map { |x| found[y][x] || interpolate(found, x, y, n) || project.call(x, y) }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Intersection of the lines through the nearest two found neighbours
|
|
89
|
+
# in the same row and in the same column, if both exist.
|
|
90
|
+
def interpolate(found, x, y, n)
|
|
91
|
+
row = nearest(n) { |i| found[y][x + i] if (x + i).between?(0, n) }
|
|
92
|
+
col = nearest(n) { |i| found[y + i][x] if (y + i).between?(0, n) }
|
|
93
|
+
return unless row.size == 2 && col.size == 2
|
|
94
|
+
|
|
95
|
+
intersect(row[0], row[1], col[0], col[1])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def nearest(n)
|
|
99
|
+
out = []
|
|
100
|
+
(1..n).each do |d|
|
|
101
|
+
[-d, d].each do |i|
|
|
102
|
+
p = yield(i)
|
|
103
|
+
out << p if p && out.size < 2
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
out
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def intersect(p1, p2, p3, p4)
|
|
110
|
+
d = (p1[0] - p2[0]) * (p3[1] - p4[1]) - (p1[1] - p2[1]) * (p3[0] - p4[0])
|
|
111
|
+
return if d.abs < 1e-9
|
|
112
|
+
|
|
113
|
+
a = p1[0] * p2[1] - p1[1] * p2[0]
|
|
114
|
+
b = p3[0] * p4[1] - p3[1] * p4[0]
|
|
115
|
+
[(a * (p3[0] - p4[0]) - (p1[0] - p2[0]) * b) / d, (a * (p3[1] - p4[1]) - (p1[1] - p2[1]) * b) / d]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Alignment pattern near +guess+ (module vectors from the transform).
|
|
119
|
+
def search(guess, mx, my)
|
|
120
|
+
c = @transform.apply(mx + 0.5, my + 0.5) or return
|
|
121
|
+
u = @transform.apply(mx + 1.5, my + 0.5) or return
|
|
122
|
+
v = @transform.apply(mx + 0.5, my + 1.5) or return
|
|
123
|
+
@finder.find(guess[0], guess[1], 3 * @module_size + 1, u[0] - c[0], u[1] - c[1], v[0] - c[0], v[1] - c[1])
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# Finds alignment patterns (a dark module inside a light 3x3 ring inside
|
|
6
|
+
# a dark 5x5 ring) near a predicted position.
|
|
7
|
+
#
|
|
8
|
+
# Rows of a window around the prediction are scanned for light-dark-light
|
|
9
|
+
# runs of about one module each (from the middle row outwards), hits are
|
|
10
|
+
# cross-checked vertically and then verified by sampling the 5x5
|
|
11
|
+
# structure along the symbol's own axes, which rejects the many
|
|
12
|
+
# look-alikes in the data area. The verified hit closest to the
|
|
13
|
+
# prediction wins.
|
|
14
|
+
class AlignmentPatternFinder
|
|
15
|
+
DARK = BitMatrix::DARK
|
|
16
|
+
LIGHT = BitMatrix::LIGHT
|
|
17
|
+
# Of the 25 cells of the 5x5 pattern this many may read wrong.
|
|
18
|
+
MAX_MISMATCHES = 5
|
|
19
|
+
|
|
20
|
+
# Module offsets of the 8 light ring cells and the 16 dark ring cells.
|
|
21
|
+
LIGHT_RING = [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]].freeze
|
|
22
|
+
DARK_RING = ((-2..2).flat_map { |i| [[i, -2], [i, 2]] } + (-1..1).flat_map { |i| [[-2, i], [2, i]] }).freeze
|
|
23
|
+
|
|
24
|
+
def initialize(matrix)
|
|
25
|
+
@matrix = matrix
|
|
26
|
+
@bits = matrix.bits
|
|
27
|
+
@width = matrix.width
|
|
28
|
+
@height = matrix.height
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Searches the square window of +radius+ pixels around (x, y).
|
|
32
|
+
# +ux+, +uy+ / +vx+, +vy+ are the pixel vectors of one module step
|
|
33
|
+
# along the symbol's x and y axes at that location (for verification).
|
|
34
|
+
# Returns [cx, cy] or nil.
|
|
35
|
+
def find(x, y, radius, ux, uy, vx, vy)
|
|
36
|
+
module_size = Math.sqrt((ux * ux + uy * uy + vx * vx + vy * vy) * 0.5)
|
|
37
|
+
return if module_size < 0.8 || !x.finite? || !y.finite?
|
|
38
|
+
|
|
39
|
+
x0 = (x - radius).floor.clamp(0, @width - 1)
|
|
40
|
+
x1 = (x + radius).ceil.clamp(0, @width - 1)
|
|
41
|
+
y0 = (y - radius).floor.clamp(0, @height - 1)
|
|
42
|
+
y1 = (y + radius).ceil.clamp(0, @height - 1)
|
|
43
|
+
return if x1 - x0 < 3 || y1 - y0 < 3
|
|
44
|
+
|
|
45
|
+
best = nil
|
|
46
|
+
best_score = Float::INFINITY
|
|
47
|
+
# Rows through the same pattern confirm the same centre: verify each
|
|
48
|
+
# centre once. (Exact keys: verification samples whole pixels, so a
|
|
49
|
+
# centre a fraction of a pixel away can verify when this one fails.)
|
|
50
|
+
verified = {}
|
|
51
|
+
mid = y.floor.clamp(y0, y1)
|
|
52
|
+
max_offset = [mid - y0, y1 - mid].max
|
|
53
|
+
offset = 0
|
|
54
|
+
while offset <= max_offset
|
|
55
|
+
[mid - offset, mid + offset].uniq.each do |row|
|
|
56
|
+
next if row < y0 || row > y1
|
|
57
|
+
|
|
58
|
+
scan_row(row, x0, x1, module_size) do |cx|
|
|
59
|
+
centre = confirm(cx, row, module_size)
|
|
60
|
+
next unless centre
|
|
61
|
+
|
|
62
|
+
next if verified.key?(centre)
|
|
63
|
+
|
|
64
|
+
verified[centre] = true
|
|
65
|
+
mismatches = verify(centre[0], centre[1], ux, uy, vx, vy)
|
|
66
|
+
next if mismatches > MAX_MISMATCHES
|
|
67
|
+
|
|
68
|
+
score = Math.hypot(centre[0] - x, centre[1] - y) / module_size + mismatches
|
|
69
|
+
if score < best_score
|
|
70
|
+
best_score = score
|
|
71
|
+
best = centre
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
# Stop early once a clean pattern close to the prediction is known.
|
|
76
|
+
break if best && best_score < offset / module_size - 1.0
|
|
77
|
+
|
|
78
|
+
offset += 1
|
|
79
|
+
end
|
|
80
|
+
best
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Yields the centre column of every light-dark-light run triple with
|
|
86
|
+
# runs of about one module in row y between columns x0 and x1, where
|
|
87
|
+
# the dark ring continues on both sides.
|
|
88
|
+
def scan_row(y, x0, x1, module_size)
|
|
89
|
+
w = @width
|
|
90
|
+
off = y * w
|
|
91
|
+
row = @bits.byteslice(off, w)
|
|
92
|
+
max = module_size * 1.6 + 1
|
|
93
|
+
min = module_size * 0.4 - 1
|
|
94
|
+
pos = row.index(LIGHT, x0) or return
|
|
95
|
+
while pos < x1
|
|
96
|
+
d0 = row.index(DARK, pos) or return
|
|
97
|
+
d1 = row.index(LIGHT, d0) or return
|
|
98
|
+
l1 = row.index(DARK, d1) or return
|
|
99
|
+
a = d0 - pos
|
|
100
|
+
b = d1 - d0
|
|
101
|
+
c = l1 - d1
|
|
102
|
+
# The light run before must be preceded by dark (ring), which holds
|
|
103
|
+
# unless it starts the row segment.
|
|
104
|
+
if b <= max && b >= min && a <= max && a >= min && c <= max && c >= min &&
|
|
105
|
+
pos.positive? && row.getbyte(pos - 1) == 1
|
|
106
|
+
yield (d0 + d1) >> 1
|
|
107
|
+
end
|
|
108
|
+
pos = d1
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Vertical then horizontal cross-check through dark pixel (x, y).
|
|
113
|
+
# Returns the refined centre [cx, cy] or nil.
|
|
114
|
+
def confirm(x, y, module_size)
|
|
115
|
+
bits = @bits
|
|
116
|
+
w = @width
|
|
117
|
+
h = @height
|
|
118
|
+
max = module_size * 1.6 + 1
|
|
119
|
+
min = module_size * 0.4 - 1
|
|
120
|
+
# Vertical: dark centre, light above/below, dark ring beyond. The
|
|
121
|
+
# walks stop just past the longest acceptable run, so a long dark
|
|
122
|
+
# column (page border, large dark area) costs nothing extra.
|
|
123
|
+
top = y
|
|
124
|
+
top -= 1 while top > 0 && y - top <= max && bits.getbyte((top - 1) * w + x) == 1
|
|
125
|
+
bottom = y
|
|
126
|
+
bottom += 1 while bottom < h - 1 && bottom - y <= max && bits.getbyte((bottom + 1) * w + x) == 1
|
|
127
|
+
centre_v = bottom - top + 1
|
|
128
|
+
return if centre_v > max || centre_v < min
|
|
129
|
+
|
|
130
|
+
lt = top - 1
|
|
131
|
+
lt -= 1 while lt >= 0 && top - lt <= max + 1 && bits.getbyte(lt * w + x).zero?
|
|
132
|
+
lb = bottom + 1
|
|
133
|
+
lb += 1 while lb < h && lb - bottom <= max + 1 && bits.getbyte(lb * w + x).zero?
|
|
134
|
+
return if lt.negative? || lb >= h
|
|
135
|
+
|
|
136
|
+
light_top = top - lt - 1
|
|
137
|
+
light_bottom = lb - bottom - 1
|
|
138
|
+
return if light_top < min || light_top > max || light_bottom < min || light_bottom > max
|
|
139
|
+
|
|
140
|
+
cy = (top + bottom + 1) * 0.5
|
|
141
|
+
span_y = (lt + 1 + lb) * 0.5
|
|
142
|
+
iy = cy.floor
|
|
143
|
+
# Horizontal at the vertical centre.
|
|
144
|
+
row = bits.byteslice(iy * w, w)
|
|
145
|
+
return unless row.getbyte(x) == 1
|
|
146
|
+
|
|
147
|
+
r0 = row.index(LIGHT, x) or return
|
|
148
|
+
r1 = row.index(DARK, r0) or return
|
|
149
|
+
l0 = row.rindex(LIGHT, x) or return
|
|
150
|
+
l1 = row.rindex(DARK, l0) or return
|
|
151
|
+
centre_h = r0 - l0 - 1
|
|
152
|
+
return if centre_h > max || centre_h < min || r1 - r0 > max || l0 - l1 > max
|
|
153
|
+
|
|
154
|
+
cx = (l0 + 1 + r0) * 0.5
|
|
155
|
+
span_x = (l1 + 1 + r1) * 0.5
|
|
156
|
+
[(cx + span_x) * 0.5, (cy + span_y) * 0.5]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Number of cells of the 5x5 pattern centred at (x, y) that have the
|
|
160
|
+
# wrong colour (out of 25), sampling along the module vectors.
|
|
161
|
+
# Stops counting past MAX_MISMATCHES.
|
|
162
|
+
def verify(x, y, ux, uy, vx, vy)
|
|
163
|
+
bits = @bits
|
|
164
|
+
w = @width
|
|
165
|
+
h = @height
|
|
166
|
+
wrong = dark_at?(bits, w, h, x, y) ? 0 : 1
|
|
167
|
+
k = 0
|
|
168
|
+
while k < 8
|
|
169
|
+
i, j = LIGHT_RING[k]
|
|
170
|
+
wrong += 1 if dark_at?(bits, w, h, x + i * ux + j * vx, y + i * uy + j * vy)
|
|
171
|
+
k += 1
|
|
172
|
+
end
|
|
173
|
+
return wrong if wrong > MAX_MISMATCHES
|
|
174
|
+
|
|
175
|
+
k = 0
|
|
176
|
+
while k < 16
|
|
177
|
+
i, j = DARK_RING[k]
|
|
178
|
+
wrong += 1 unless dark_at?(bits, w, h, x + i * ux + j * vx, y + i * uy + j * vy)
|
|
179
|
+
return wrong if wrong > MAX_MISMATCHES
|
|
180
|
+
|
|
181
|
+
k += 1
|
|
182
|
+
end
|
|
183
|
+
wrong
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def dark_at?(bits, w, h, x, y)
|
|
187
|
+
x >= 0 && y >= 0 && x < w && y < h && bits.getbyte(y.to_i * w + x.to_i) == 1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|