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,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# A planar projective transform (homography):
|
|
6
|
+
#
|
|
7
|
+
# x' = (m0 x + m1 y + m2) / (m6 x + m7 y + m8)
|
|
8
|
+
# y' = (m3 x + m4 y + m5) / (m6 x + m7 y + m8)
|
|
9
|
+
#
|
|
10
|
+
# Built from four point correspondences, like ZXing's PerspectiveTransform.
|
|
11
|
+
class PerspectiveTransform
|
|
12
|
+
attr_reader :m
|
|
13
|
+
|
|
14
|
+
# +m+ - the 9 coefficients, row-major.
|
|
15
|
+
def initialize(m)
|
|
16
|
+
@m = m.freeze
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Maps quad (x0,y0)..(x3,y3) onto quad (u0,v0)..(u3,v3); points
|
|
22
|
+
# correspond in order. Returns nil for degenerate quads.
|
|
23
|
+
def quad_to_quad(src, dst)
|
|
24
|
+
to_square = square_to_quad(src)&.adjoint
|
|
25
|
+
from_square = square_to_quad(dst)
|
|
26
|
+
return unless to_square && from_square
|
|
27
|
+
|
|
28
|
+
t = from_square * to_square
|
|
29
|
+
t.finite? ? t : nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Maps the unit square (0,0), (1,0), (1,1), (0,1) onto the quad
|
|
33
|
+
# [[x0, y0], [x1, y1], [x2, y2], [x3, y3]]. nil if degenerate.
|
|
34
|
+
def square_to_quad(quad)
|
|
35
|
+
(x0, y0), (x1, y1), (x2, y2), (x3, y3) = quad
|
|
36
|
+
# Three corners on a line make the quad degenerate.
|
|
37
|
+
return if collinear?(x3, y3, x0, y0, x1, y1) || collinear?(x0, y0, x1, y1, x2, y2) ||
|
|
38
|
+
collinear?(x1, y1, x2, y2, x3, y3) || collinear?(x2, y2, x3, y3, x0, y0)
|
|
39
|
+
|
|
40
|
+
dx3 = x0 - x1 + x2 - x3
|
|
41
|
+
dy3 = y0 - y1 + y2 - y3
|
|
42
|
+
if dx3.abs < 1e-9 && dy3.abs < 1e-9
|
|
43
|
+
new([x1 - x0, x3 - x0, x0, y1 - y0, y3 - y0, y0, 0.0, 0.0, 1.0])
|
|
44
|
+
else
|
|
45
|
+
dx1 = x1 - x2
|
|
46
|
+
dx2 = x3 - x2
|
|
47
|
+
dy1 = y1 - y2
|
|
48
|
+
dy2 = y3 - y2
|
|
49
|
+
den = (dx1 * dy2 - dx2 * dy1).to_f
|
|
50
|
+
return if den.abs < 1e-12
|
|
51
|
+
|
|
52
|
+
g = (dx3 * dy2 - dx2 * dy3) / den
|
|
53
|
+
h = (dx1 * dy3 - dx3 * dy1) / den
|
|
54
|
+
new([x1 - x0 + g * x1, x3 - x0 + h * x3, x0.to_f,
|
|
55
|
+
y1 - y0 + g * y1, y3 - y0 + h * y3, y0.to_f,
|
|
56
|
+
g, h, 1.0])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# True if (bx, by) lies on the line through (ax, ay) and (cx, cy).
|
|
62
|
+
def self.collinear?(ax, ay, bx, by, cx, cy)
|
|
63
|
+
((bx - ax) * (cy - ay) - (by - ay) * (cx - ax)).abs < 1e-9
|
|
64
|
+
end
|
|
65
|
+
private_class_method :collinear?
|
|
66
|
+
|
|
67
|
+
# Transformed point as [x', y'] (nil if the point maps to infinity).
|
|
68
|
+
def apply(x, y)
|
|
69
|
+
m = @m
|
|
70
|
+
den = m[6] * x + m[7] * y + m[8]
|
|
71
|
+
return if den.abs < 1e-12
|
|
72
|
+
|
|
73
|
+
[(m[0] * x + m[1] * y + m[2]) / den, (m[3] * x + m[4] * y + m[5]) / den]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Matrix product (self after other).
|
|
77
|
+
def *(other)
|
|
78
|
+
a = @m
|
|
79
|
+
b = other.m
|
|
80
|
+
PerspectiveTransform.new([
|
|
81
|
+
a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
|
|
82
|
+
a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
|
|
83
|
+
a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
|
|
84
|
+
a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
|
|
85
|
+
a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
|
|
86
|
+
a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
|
|
87
|
+
a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
|
|
88
|
+
a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
|
|
89
|
+
a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
|
|
90
|
+
])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Adjugate matrix: the inverse up to a scale factor, which is all a
|
|
94
|
+
# homography needs.
|
|
95
|
+
def adjoint
|
|
96
|
+
a0, a1, a2, a3, a4, a5, a6, a7, a8 = @m
|
|
97
|
+
PerspectiveTransform.new([
|
|
98
|
+
a4 * a8 - a5 * a7, a2 * a7 - a1 * a8, a1 * a5 - a2 * a4,
|
|
99
|
+
a5 * a6 - a3 * a8, a0 * a8 - a2 * a6, a2 * a3 - a0 * a5,
|
|
100
|
+
a3 * a7 - a4 * a6, a1 * a6 - a0 * a7, a0 * a4 - a1 * a3
|
|
101
|
+
])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def finite?
|
|
105
|
+
@m.all? { |v| v.finite? }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# True if the denominator keeps one sign (no point maps through
|
|
109
|
+
# infinity) over the quad with corners (x0, y0) and (x1, y1).
|
|
110
|
+
def regular_over?(x0, y0, x1, y1)
|
|
111
|
+
m = @m
|
|
112
|
+
dens = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]].map { |x, y| m[6] * x + m[7] * y + m[8] }
|
|
113
|
+
dens.all? { |d| d > 1e-9 } || dens.all? { |d| d < -1e-9 }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# Fast path for clean, axis-aligned, unrotated-or-quarter-turned symbols
|
|
6
|
+
# such as generated images: the symbol is the bounding box of all dark
|
|
7
|
+
# pixels, three of its corners must hold finder patterns (checked
|
|
8
|
+
# along the corner diagonals, which also yields the module size), and
|
|
9
|
+
# modules are sampled at their centres. Costs a few String#index calls
|
|
10
|
+
# when the image is not such a symbol.
|
|
11
|
+
module PureDetector
|
|
12
|
+
DARK = BitMatrix::DARK
|
|
13
|
+
|
|
14
|
+
# A detected pure symbol; same interface as Candidate.
|
|
15
|
+
class Result
|
|
16
|
+
attr_reader :bits, :points, :finder_patterns, :dimension, :module_size
|
|
17
|
+
|
|
18
|
+
def initialize(bits, points, finder_patterns, module_size)
|
|
19
|
+
@bits = bits
|
|
20
|
+
@points = points
|
|
21
|
+
@finder_patterns = finder_patterns
|
|
22
|
+
@dimension = bits.width
|
|
23
|
+
@module_size = module_size
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def alternatives
|
|
27
|
+
return enum_for(:alternatives) unless block_given?
|
|
28
|
+
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def uses?(_pattern)
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def key
|
|
37
|
+
@finder_patterns.map { |x, y| [x.round, y.round] }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"#<PureDetector::Result dim=#{@dimension} module_size=#{@module_size.round(2)}>"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# Result or nil.
|
|
48
|
+
def detect(matrix)
|
|
49
|
+
box = bounding_box(matrix) or return
|
|
50
|
+
left, top, right, bottom = box
|
|
51
|
+
width = right - left
|
|
52
|
+
height = bottom - top
|
|
53
|
+
return if width < 21 || height < 21 || (width - height).abs > 2 + width / 50
|
|
54
|
+
|
|
55
|
+
# Diagonal walks into each corner: TL, TR, BR, BL.
|
|
56
|
+
corners = [[left, top, 1, 1], [right - 1, top, -1, 1],
|
|
57
|
+
[right - 1, bottom - 1, -1, -1], [left, bottom - 1, 1, -1]]
|
|
58
|
+
sizes = corners.map { |x, y, dx, dy| corner_finder(matrix, x, y, dx, dy, width) }
|
|
59
|
+
missing = sizes.each_index.select { |i| sizes[i].nil? }
|
|
60
|
+
return unless missing.size == 1
|
|
61
|
+
|
|
62
|
+
module_size = sizes.compact.sum / 3.0
|
|
63
|
+
dimension = ((width + height) * 0.5 / module_size).round
|
|
64
|
+
return unless SymbolVersion.for_dimension(dimension)
|
|
65
|
+
|
|
66
|
+
mx = width.to_f / dimension
|
|
67
|
+
my = height.to_f / dimension
|
|
68
|
+
return if (mx / module_size - 1).abs > 0.2 || (my / module_size - 1).abs > 0.2
|
|
69
|
+
|
|
70
|
+
bits = sample(matrix, left, top, mx, my, dimension)
|
|
71
|
+
# Turn the symbol so that the corner without finder is bottom-right.
|
|
72
|
+
quarter_turns = (2 - missing.first) % 4
|
|
73
|
+
bits = rotate(bits, quarter_turns)
|
|
74
|
+
points = [[left, top], [right, top], [right, bottom], [left, bottom]].rotate(-quarter_turns % 4)
|
|
75
|
+
mid_x = (left + right) * 0.5
|
|
76
|
+
mid_y = (top + bottom) * 0.5
|
|
77
|
+
centre = ->(px, py) { [px + (mid_x <=> px) * 3.5 * mx, py + (mid_y <=> py) * 3.5 * my] }
|
|
78
|
+
finders = [centre.call(*points[0]), centre.call(*points[1]), centre.call(*points[3])]
|
|
79
|
+
Result.new(bits, points.map { |x, y| [x.to_f, y.to_f] }, finders, module_size)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# [left, top, right, bottom] (right/bottom exclusive) of the dark pixels.
|
|
83
|
+
def bounding_box(matrix)
|
|
84
|
+
bits = matrix.bits
|
|
85
|
+
w = matrix.width
|
|
86
|
+
first = bits.index(DARK) or return
|
|
87
|
+
last = bits.rindex(DARK)
|
|
88
|
+
top = first / w
|
|
89
|
+
bottom = last / w + 1
|
|
90
|
+
left = w
|
|
91
|
+
right = 0
|
|
92
|
+
y = top
|
|
93
|
+
off = top * w
|
|
94
|
+
while y < bottom
|
|
95
|
+
# No substring per row: the first dark pixel at or after the row
|
|
96
|
+
# start is in this row, or rows without any are skipped at once.
|
|
97
|
+
l = bits.index(DARK, off)
|
|
98
|
+
if l >= off + w
|
|
99
|
+
y = l / w
|
|
100
|
+
off = y * w
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
left = l - off if l - off < left
|
|
104
|
+
r = bits.rindex(DARK, off + w - 1)
|
|
105
|
+
right = r - off + 1 if r - off >= right
|
|
106
|
+
off += w
|
|
107
|
+
y += 1
|
|
108
|
+
end
|
|
109
|
+
[left, top, right, bottom]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Module size of a finder pattern whose outer corner is at (x, y),
|
|
113
|
+
# from the 1:1:3:1:1 runs along the diagonal (dx, dy); the edges
|
|
114
|
+
# leaving the corner must be dark for 7 modules, then light. nil if
|
|
115
|
+
# there is no finder pattern.
|
|
116
|
+
def corner_finder(matrix, x, y, dx, dy, limit)
|
|
117
|
+
runs = diagonal_runs(matrix, x, y, dx, dy, limit)
|
|
118
|
+
return unless runs.size == 5 && Geometry.finder_ratio?(*runs, relaxed: true)
|
|
119
|
+
|
|
120
|
+
module_size = runs.sum / 7.0
|
|
121
|
+
return unless edge_run?(matrix, x, y, dx, 0, module_size) && edge_run?(matrix, x, y, 0, dy, module_size)
|
|
122
|
+
|
|
123
|
+
module_size
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Up to five alternating run lengths along a diagonal, starting dark.
|
|
127
|
+
def diagonal_runs(matrix, x, y, dx, dy, limit)
|
|
128
|
+
bits = matrix.bits
|
|
129
|
+
w = matrix.width
|
|
130
|
+
h = matrix.height
|
|
131
|
+
runs = [0]
|
|
132
|
+
dark = true
|
|
133
|
+
steps = 0
|
|
134
|
+
while steps < limit && x >= 0 && y >= 0 && x < w && y < h
|
|
135
|
+
if (bits.getbyte(y * w + x) == 1) == dark
|
|
136
|
+
runs[-1] += 1
|
|
137
|
+
else
|
|
138
|
+
break if runs.size == 5
|
|
139
|
+
|
|
140
|
+
runs << 1
|
|
141
|
+
dark = !dark
|
|
142
|
+
end
|
|
143
|
+
x += dx
|
|
144
|
+
y += dy
|
|
145
|
+
steps += 1
|
|
146
|
+
end
|
|
147
|
+
runs
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# True if the dark run from (x, y) along (dx, dy) is 7 modules long.
|
|
151
|
+
def edge_run?(matrix, x, y, dx, dy, module_size)
|
|
152
|
+
bits = matrix.bits
|
|
153
|
+
w = matrix.width
|
|
154
|
+
h = matrix.height
|
|
155
|
+
n = 0
|
|
156
|
+
while x >= 0 && y >= 0 && x < w && y < h && bits.getbyte(y * w + x) == 1
|
|
157
|
+
n += 1
|
|
158
|
+
x += dx
|
|
159
|
+
y += dy
|
|
160
|
+
end
|
|
161
|
+
(n / module_size - 7).abs < 1.0
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def sample(matrix, left, top, mx, my, dimension)
|
|
165
|
+
bits = matrix.bits
|
|
166
|
+
w = matrix.width
|
|
167
|
+
out = "\0".b * (dimension * dimension)
|
|
168
|
+
j = 0
|
|
169
|
+
while j < dimension
|
|
170
|
+
y = (top + (j + 0.5) * my).to_i
|
|
171
|
+
off = y * w
|
|
172
|
+
i = 0
|
|
173
|
+
while i < dimension
|
|
174
|
+
out.setbyte(j * dimension + i, 1) if bits.getbyte(off + (left + (i + 0.5) * mx).to_i) == 1
|
|
175
|
+
i += 1
|
|
176
|
+
end
|
|
177
|
+
j += 1
|
|
178
|
+
end
|
|
179
|
+
BitMatrix.new(dimension, dimension, out)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Rotates a square matrix by +turns+ quarter turns clockwise.
|
|
183
|
+
def rotate(matrix, turns)
|
|
184
|
+
turns.times do
|
|
185
|
+
n = matrix.width
|
|
186
|
+
src = matrix.bits
|
|
187
|
+
out = "\0".b * (n * n)
|
|
188
|
+
n.times do |y|
|
|
189
|
+
n.times { |x| out.setbyte(x * n + (n - 1 - y), src.getbyte(y * n + x)) }
|
|
190
|
+
end
|
|
191
|
+
matrix = BitMatrix.new(n, n, out)
|
|
192
|
+
end
|
|
193
|
+
matrix
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "binarizer"
|
|
4
|
+
require_relative "kernels/detector"
|
|
5
|
+
require_relative "detector/geometry"
|
|
6
|
+
require_relative "detector/finder_pattern"
|
|
7
|
+
require_relative "detector/finder_pattern_finder"
|
|
8
|
+
require_relative "detector/pattern_sets"
|
|
9
|
+
require_relative "detector/perspective_transform"
|
|
10
|
+
require_relative "detector/grid_sampler"
|
|
11
|
+
require_relative "detector/alignment_pattern_finder"
|
|
12
|
+
require_relative "detector/alignment_grid"
|
|
13
|
+
require_relative "detector/candidate"
|
|
14
|
+
require_relative "detector/pure_detector"
|
|
15
|
+
|
|
16
|
+
module QrReader
|
|
17
|
+
# Locates QR symbols in a binarized image and samples their module grids.
|
|
18
|
+
#
|
|
19
|
+
# detector = QrReader::Detector.new(QrReader::Binarizer.hybrid(image), image: image)
|
|
20
|
+
# detector.each_candidate do |candidate|
|
|
21
|
+
# result = QrReader::Decoder.decode(candidate.bits) ||
|
|
22
|
+
# candidate.alternatives.lazy.filter_map { |bits| QrReader::Decoder.decode(bits) }.first
|
|
23
|
+
# break result if result
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# Candidates are produced lazily, most likely first: finder patterns are
|
|
27
|
+
# searched once per mode and cached; every plausible triple of them is
|
|
28
|
+
# scored (PatternSets) and turned into a Candidate whose module grid is
|
|
29
|
+
# only sampled when asked for.
|
|
30
|
+
class Detector
|
|
31
|
+
# Finder pattern triples tried per mode.
|
|
32
|
+
FAST_TRIPLES = 12
|
|
33
|
+
HARD_TRIPLES = 40
|
|
34
|
+
MULTI_TRIPLES = 120
|
|
35
|
+
# With multi, up to this many finder patterns are considered...
|
|
36
|
+
MULTI_PATTERNS = 400
|
|
37
|
+
# ... and at most this many "foreign" triples (with other finder
|
|
38
|
+
# patterns inside, see PatternSets.partition_clean): on pages with
|
|
39
|
+
# several symbols nearly all triples joining finders of different
|
|
40
|
+
# symbols are foreign, real symbols practically never.
|
|
41
|
+
MULTI_FOREIGN = 12
|
|
42
|
+
# Score penalty of foreign triples in single-symbol mode (a perfect
|
|
43
|
+
# triple scores 0, see PatternSets.evaluate).
|
|
44
|
+
FOREIGN_PENALTY = 0.5
|
|
45
|
+
# Besides the best scored triples overall, each finder pattern is
|
|
46
|
+
# combined with pairs of this many of its nearest neighbours.
|
|
47
|
+
NEIGHBOURS = 8
|
|
48
|
+
|
|
49
|
+
attr_reader :matrix, :image
|
|
50
|
+
|
|
51
|
+
# Fast path for clean axis-aligned symbols (see PureDetector);
|
|
52
|
+
# returns a candidate or nil.
|
|
53
|
+
def self.detect_pure(matrix)
|
|
54
|
+
PureDetector.detect(matrix)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# +matrix+ - binarized image (BitMatrix, 1 = dark)
|
|
58
|
+
# +image+ - the grayscale Image it came from (optional; ignored unless
|
|
59
|
+
# it has the matrix's size)
|
|
60
|
+
def initialize(matrix, image: nil)
|
|
61
|
+
@matrix = matrix
|
|
62
|
+
@image = image if image && image.width == matrix.width && image.height == matrix.height
|
|
63
|
+
@finder = FinderPatternFinder.new(matrix)
|
|
64
|
+
@confirmed = []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Yields Candidates, most likely first (Enumerator without a block).
|
|
68
|
+
# Candidates whose timing patterns read like noise (false finder
|
|
69
|
+
# triples, mostly) are only produced with try_harder, after all others
|
|
70
|
+
# (with multi, the FAST_TRIPLES best of those that span no other finder
|
|
71
|
+
# pattern).
|
|
72
|
+
#
|
|
73
|
+
# try_harder: scan every row and try more finder pattern triples.
|
|
74
|
+
# multi: look for all symbols - also triples of neighbouring finder
|
|
75
|
+
# patterns (pages with many symbols), skipping triples that share a
|
|
76
|
+
# finder pattern with (or lie inside) a candidate passed to #confirm.
|
|
77
|
+
def each_candidate(try_harder: false, multi: false)
|
|
78
|
+
return enum_for(:each_candidate, try_harder: try_harder, multi: multi) unless block_given?
|
|
79
|
+
|
|
80
|
+
patterns, triples = multi ? multi_triples(try_harder) : single_triples(try_harder)
|
|
81
|
+
deferred = []
|
|
82
|
+
triples.each do |triple|
|
|
83
|
+
next if multi && confirmed?(triple)
|
|
84
|
+
|
|
85
|
+
candidate = Candidate.locate(self, triple) or next
|
|
86
|
+
if candidate.likely?
|
|
87
|
+
yield candidate
|
|
88
|
+
elsif try_harder && !(multi && spans_others?(candidate, patterns))
|
|
89
|
+
deferred << candidate
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
deferred = deferred.first(FAST_TRIPLES) if multi
|
|
93
|
+
deferred.each { |candidate| yield candidate unless multi && confirmed?(candidate) }
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Records a decoded symbol, so that multi-symbol detection skips
|
|
98
|
+
# triples that reuse its finder patterns or lie inside it: a Candidate
|
|
99
|
+
# (or detect_pure result), anything else with #points (e.g. a Result
|
|
100
|
+
# decoded earlier from another binarization of the same image, its
|
|
101
|
+
# points scaled to this matrix), or the corner quad [[x, y], ...]
|
|
102
|
+
# itself. Returns self.
|
|
103
|
+
def confirm(symbol)
|
|
104
|
+
quad = symbol.is_a?(Array) ? symbol : symbol.points
|
|
105
|
+
if quad.is_a?(Array) && quad.size == 4 && quad.flatten.all? { |v| v.is_a?(Numeric) && v.finite? }
|
|
106
|
+
@confirmed << [quad, symbol.respond_to?(:uses?) ? symbol : nil]
|
|
107
|
+
end
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Finder patterns found in the image (cached per mode).
|
|
112
|
+
def finder_patterns(try_harder: false)
|
|
113
|
+
@finder.find(try_harder: try_harder)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def alignment_finder
|
|
117
|
+
@alignment_finder ||= AlignmentPatternFinder.new(@matrix)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# True if the bit matrix is the negative of the image (dark cells are
|
|
121
|
+
# brighter on average), e.g. when a caller inverted it to read
|
|
122
|
+
# light-on-dark symbols; luminance sampling then flips its comparison.
|
|
123
|
+
def inverted_image?
|
|
124
|
+
return @inverted_image unless @inverted_image.nil?
|
|
125
|
+
|
|
126
|
+
@inverted_image = false
|
|
127
|
+
return false unless @image && @image.width == @matrix.width && @image.height == @matrix.height
|
|
128
|
+
|
|
129
|
+
data = @image.data
|
|
130
|
+
bits = @matrix.bits
|
|
131
|
+
step = [data.bytesize / 4096, 1].max
|
|
132
|
+
sums = [0, 0]
|
|
133
|
+
counts = [0, 0]
|
|
134
|
+
i = 0
|
|
135
|
+
while i < data.bytesize
|
|
136
|
+
b = bits.getbyte(i)
|
|
137
|
+
sums[b] += data.getbyte(i)
|
|
138
|
+
counts[b] += 1
|
|
139
|
+
i += step
|
|
140
|
+
end
|
|
141
|
+
@inverted_image = counts.all?(&:positive?) && sums[1] * counts[0] > sums[0] * counts[1]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# [patterns, triples] for single-symbol detection: the best scored
|
|
147
|
+
# triples of the most consistent finder patterns and of each pattern's
|
|
148
|
+
# nearest neighbours, those with other symbols' finder patterns inside
|
|
149
|
+
# (foreign, see PatternSets.partition_clean) ranked as if their score
|
|
150
|
+
# were FOREIGN_PENALTY worse: on a sheet of labels the best shaped
|
|
151
|
+
# triples join finders of neighbouring symbols, but a large symbol
|
|
152
|
+
# with small modules can have finder look-alikes in its data area and
|
|
153
|
+
# must still beat the ill-shaped triples of those look-alikes.
|
|
154
|
+
def single_triples(try_harder)
|
|
155
|
+
patterns = finder_patterns(try_harder: try_harder)
|
|
156
|
+
limit = try_harder ? HARD_TRIPLES : FAST_TRIPLES
|
|
157
|
+
clean, foreign = ranked_triples(PatternSets.generate(patterns, 4 * limit), patterns, patterns)
|
|
158
|
+
ranked = clean.each_with_index.map { |t, i| [t.score, i, t] }
|
|
159
|
+
foreign.each_with_index { |t, i| ranked << [t.score + FOREIGN_PENALTY, clean.size + i, t] }
|
|
160
|
+
[patterns, ranked.sort!.first(limit).map(&:last)]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# [patterns, triples] for multi-symbol detection: as for a single
|
|
164
|
+
# symbol, but from up to MULTI_PATTERNS patterns and MULTI_TRIPLES
|
|
165
|
+
# triples (a page with that many symbols), MULTI_FOREIGN of them
|
|
166
|
+
# foreign.
|
|
167
|
+
def multi_triples(try_harder)
|
|
168
|
+
all = @finder.find(try_harder: try_harder, limit: MULTI_PATTERNS)
|
|
169
|
+
best = if all.size > FinderPatternFinder::MAX_PATTERNS
|
|
170
|
+
all.max_by(FinderPatternFinder::MAX_PATTERNS) { |p| p.quality * Math.sqrt(p.count) }
|
|
171
|
+
else
|
|
172
|
+
all
|
|
173
|
+
end
|
|
174
|
+
clean, foreign = ranked_triples(PatternSets.generate(best, MULTI_TRIPLES), all, all)
|
|
175
|
+
[all, (clean + foreign.first(MULTI_FOREIGN)).first(MULTI_TRIPLES)]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# +triples+ plus the neighbourhood triples of +near+, split into
|
|
179
|
+
# [clean, foreign] (see PatternSets.partition_clean against +all+), the
|
|
180
|
+
# clean ones best first.
|
|
181
|
+
def ranked_triples(triples, near, all)
|
|
182
|
+
seen = triples.to_h { |t| [t.patterns.map(&:object_id), true] }
|
|
183
|
+
PatternSets.neighbourhood(near, NEIGHBOURS).each do |t|
|
|
184
|
+
triples << t unless seen[t.patterns.map(&:object_id)]
|
|
185
|
+
end
|
|
186
|
+
clean, foreign = PatternSets.partition_clean(triples, all)
|
|
187
|
+
[clean.sort_by!(&:score), foreign]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# True if a triple (or candidate) shares a finder pattern with a
|
|
191
|
+
# confirmed symbol or has one inside it.
|
|
192
|
+
def confirmed?(set)
|
|
193
|
+
patterns = set.respond_to?(:patterns) ? set.patterns : [set.tl, set.tr, set.bl]
|
|
194
|
+
@confirmed.any? do |quad, owner|
|
|
195
|
+
patterns.any? { |p| owner&.uses?(p) || inside?(quad, p.x, p.y) }
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# True if a finder pattern other than the candidate's own lies inside
|
|
200
|
+
# its symbol area.
|
|
201
|
+
def spans_others?(candidate, patterns)
|
|
202
|
+
quad = candidate.points
|
|
203
|
+
patterns.any? { |p| !candidate.uses?(p) && inside?(quad, p.x, p.y) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Point-in-convex-quad test (either winding).
|
|
207
|
+
def inside?(quad, x, y)
|
|
208
|
+
signs = quad.each_with_index.map do |(x0, y0), i|
|
|
209
|
+
x1, y1 = quad[(i + 1) % 4]
|
|
210
|
+
(x1 - x0) * (y - y0) - (y1 - y0) * (x - x0)
|
|
211
|
+
end
|
|
212
|
+
signs.all?(&:positive?) || signs.all?(&:negative?)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# The input could not be turned into an image (unknown or corrupt format).
|
|
7
|
+
class ImageError < Error; end
|
|
8
|
+
|
|
9
|
+
# The image format was recognised but is not supported (e.g. arithmetic-coded JPEG).
|
|
10
|
+
class UnsupportedImageError < ImageError; end
|
|
11
|
+
|
|
12
|
+
# Raised by QrReader.decode! when no QR code could be read.
|
|
13
|
+
class NotFoundError < Error; end
|
|
14
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
# The 15-bit format information word: error correction level + mask pattern,
|
|
5
|
+
# protected by a BCH(15,5) code and XOR-masked with 0x5412.
|
|
6
|
+
class FormatInformation
|
|
7
|
+
MASK = 0x5412
|
|
8
|
+
GENERATOR = 0x537
|
|
9
|
+
|
|
10
|
+
attr_reader :ec_level, :mask, :bits
|
|
11
|
+
|
|
12
|
+
def initialize(data_bits)
|
|
13
|
+
@ec_level = SymbolVersion::EC_LEVEL_FOR_BITS[(data_bits >> 3) & 0x03]
|
|
14
|
+
@mask = data_bits & 0x07
|
|
15
|
+
@bits = self.class.encode(data_bits)
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(FormatInformation) && other.bits == bits
|
|
21
|
+
end
|
|
22
|
+
alias eql? ==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
bits.hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inspect
|
|
29
|
+
"#<QrReader::FormatInformation ec=#{@ec_level} mask=#{@mask}>"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# 15-bit masked code word for 5 data bits (2 EC level bits + 3 mask bits).
|
|
34
|
+
def encode(data_bits)
|
|
35
|
+
rem = data_bits
|
|
36
|
+
10.times { rem = (rem << 1) ^ ((rem >> 9) * GENERATOR) }
|
|
37
|
+
((data_bits << 10) | rem) ^ MASK
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Decodes the two 15-bit copies read from a symbol (pass the same value
|
|
41
|
+
# twice when only one copy is available). Returns the FormatInformation
|
|
42
|
+
# closest in Hamming distance to either copy if that distance is <= 3,
|
|
43
|
+
# else nil. Also accepts copies that lack the 0x5412 XOR mask, as
|
|
44
|
+
# written by some broken encoders.
|
|
45
|
+
def decode(bits1, bits2 = bits1)
|
|
46
|
+
closest(bits1, bits2) || closest(bits1 ^ MASK, bits2 ^ MASK)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def closest(bits1, bits2)
|
|
50
|
+
best = nil
|
|
51
|
+
best_distance = 4
|
|
52
|
+
ALL.each do |info|
|
|
53
|
+
code = info.bits
|
|
54
|
+
return info if code == bits1 || code == bits2
|
|
55
|
+
|
|
56
|
+
distance = popcount(bits1 ^ code)
|
|
57
|
+
d2 = popcount(bits2 ^ code)
|
|
58
|
+
distance = d2 if d2 < distance
|
|
59
|
+
if distance < best_distance
|
|
60
|
+
best = info
|
|
61
|
+
best_distance = distance
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
best
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def popcount(value)
|
|
68
|
+
count = 0
|
|
69
|
+
while value != 0
|
|
70
|
+
value &= value - 1
|
|
71
|
+
count += 1
|
|
72
|
+
end
|
|
73
|
+
count
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
ALL = (0..31).map { |d| new(d) }.freeze
|
|
78
|
+
end
|
|
79
|
+
end
|