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,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# Small geometric helpers shared by the detector parts.
|
|
6
|
+
module Geometry
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Lengths (in 1 px steps) of the first three runs - dark, light, dark -
|
|
10
|
+
# met walking from (x, y) along the unit vector (ux, uy); the first
|
|
11
|
+
# run includes the start point, which must be dark. The last run may
|
|
12
|
+
# end at the image border. nil if the start is light, the walk leaves
|
|
13
|
+
# the image earlier, or the runs exceed +limit+ steps. With +noise+, a
|
|
14
|
+
# single step of the other colour followed by two of the run's colour
|
|
15
|
+
# does not end a run.
|
|
16
|
+
def runs(matrix, x, y, ux, uy, limit, noise: false)
|
|
17
|
+
bits = matrix.bits
|
|
18
|
+
w = matrix.width
|
|
19
|
+
h = matrix.height
|
|
20
|
+
counts = [0, 0, 0]
|
|
21
|
+
state = 0
|
|
22
|
+
colour = 1
|
|
23
|
+
t = 0
|
|
24
|
+
while t <= limit
|
|
25
|
+
px = x + ux * t
|
|
26
|
+
py = y + uy * t
|
|
27
|
+
return state == 2 ? counts : nil unless px >= 0 && py >= 0 && px < w && py < h
|
|
28
|
+
|
|
29
|
+
c = bits.getbyte(py.to_i * w + px.to_i)
|
|
30
|
+
if c != colour
|
|
31
|
+
if noise && t.positive? && bit(bits, w, h, x + ux * (t + 1), y + uy * (t + 1)) == colour &&
|
|
32
|
+
bit(bits, w, h, x + ux * (t + 2), y + uy * (t + 2)) == colour
|
|
33
|
+
c = colour
|
|
34
|
+
else
|
|
35
|
+
return if t.zero?
|
|
36
|
+
|
|
37
|
+
state += 1
|
|
38
|
+
return counts if state == 3
|
|
39
|
+
|
|
40
|
+
colour = c
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
counts[state] += 1
|
|
44
|
+
t += 1
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Bit at continuous position (x, y); nil outside the image.
|
|
50
|
+
def bit(bits, w, h, x, y)
|
|
51
|
+
bits.getbyte(y.to_i * w + x.to_i) if x >= 0 && y >= 0 && x < w && y < h
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# 1:1:3:1:1 test: each run within 0.5 module (0.75 if relaxed) plus
|
|
55
|
+
# half a pixel of its nominal width, the centre run 0.25 module more;
|
|
56
|
+
# or the dot gain tolerant test (dark and light runs measured against
|
|
57
|
+
# their own module sizes, see QrReaderKernels::FinderScanner#gain_ratio?).
|
|
58
|
+
def finder_ratio?(a, b, c, d, e, relaxed: false)
|
|
59
|
+
total = a + b + c + d + e
|
|
60
|
+
return false if total < 7
|
|
61
|
+
|
|
62
|
+
# In units of 1/28 module.
|
|
63
|
+
tol = (relaxed ? 3 * total : 2 * total) + 14
|
|
64
|
+
return true if (28 * a - 4 * total).abs <= tol &&
|
|
65
|
+
(28 * b - 4 * total).abs <= tol &&
|
|
66
|
+
(28 * c - 12 * total).abs <= tol + total &&
|
|
67
|
+
(28 * d - 4 * total).abs <= tol &&
|
|
68
|
+
(28 * e - 4 * total).abs <= tol
|
|
69
|
+
|
|
70
|
+
gain_ratio?(a, b, c, d, e)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 1:1:3:1:1 under dot gain or loss (see finder_ratio?).
|
|
74
|
+
def gain_ratio?(a, b, c, d, e)
|
|
75
|
+
dark = a + c + e
|
|
76
|
+
light = b + d
|
|
77
|
+
return false if dark < 10 || light.zero? || dark > 10 * light || 5 * light > 8 * dark
|
|
78
|
+
|
|
79
|
+
tol = 2 * dark + 10
|
|
80
|
+
(20 * a - 4 * dark).abs <= tol && (20 * c - 12 * dark).abs <= tol && (20 * e - 4 * dark).abs <= tol &&
|
|
81
|
+
(6 * b - 3 * light).abs <= light + 3 && (6 * d - 3 * light).abs <= light + 3
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Least-squares line through +points+ as [x, y, vx, vy] (centroid and
|
|
85
|
+
# unit direction); nil if the RMS distance of the points from the
|
|
86
|
+
# line exceeds +max_rms+.
|
|
87
|
+
def fit_line(points, max_rms)
|
|
88
|
+
n = points.size.to_f
|
|
89
|
+
mx = points.sum { |x, _| x } / n
|
|
90
|
+
my = points.sum { |_, y| y } / n
|
|
91
|
+
sxx = sxy = syy = 0.0
|
|
92
|
+
points.each do |x, y|
|
|
93
|
+
dx = x - mx
|
|
94
|
+
dy = y - my
|
|
95
|
+
sxx += dx * dx
|
|
96
|
+
sxy += dx * dy
|
|
97
|
+
syy += dy * dy
|
|
98
|
+
end
|
|
99
|
+
angle = 0.5 * Math.atan2(2 * sxy, sxx - syy)
|
|
100
|
+
vx = Math.cos(angle)
|
|
101
|
+
vy = Math.sin(angle)
|
|
102
|
+
# Residual = spread perpendicular to the direction.
|
|
103
|
+
rms = Math.sqrt(((vy * vy * sxx) - (2 * vx * vy * sxy) + (vx * vx * syy)).abs / n)
|
|
104
|
+
rms > max_rms ? nil : [mx, my, vx, vy]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Intersection point [x, y] of two lines given as [x, y, vx, vy];
|
|
108
|
+
# nil if they are (nearly) parallel.
|
|
109
|
+
def intersect_lines(a, b)
|
|
110
|
+
cross = a[2] * b[3] - a[3] * b[2]
|
|
111
|
+
return if cross.abs < 0.05
|
|
112
|
+
|
|
113
|
+
t = ((b[0] - a[0]) * b[3] - (b[1] - a[1]) * b[2]) / cross
|
|
114
|
+
[a[0] + a[2] * t, a[1] + a[3] * t]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# Reads the module grid of a symbol through a PerspectiveTransform
|
|
6
|
+
# that maps module coordinates (module (i, j) covers [i, i + 1) x
|
|
7
|
+
# [j, j + 1)) to continuous pixel coordinates:
|
|
8
|
+
#
|
|
9
|
+
# * sample - from the binarized image, one or more transforms
|
|
10
|
+
# (piecewise), optionally voting over five pixels
|
|
11
|
+
# * sample_warped - the same plus a separable displacement field
|
|
12
|
+
# * sample_luminance - from the grayscale image, thresholding module
|
|
13
|
+
# luminances against their neighbourhood
|
|
14
|
+
#
|
|
15
|
+
# Modules whose centre falls outside the image read as light.
|
|
16
|
+
module GridSampler
|
|
17
|
+
# Scale of the fixed-point arithmetic of sample_row / vote_row.
|
|
18
|
+
FIXED_ONE = (1 << 24).to_f
|
|
19
|
+
# Scaled coordinates beyond this (pixels about 2**30 away) are not
|
|
20
|
+
# sampled, so that the stepping stays within Fixnum range.
|
|
21
|
+
FIXED_LIMIT = 2.0**54
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Samples a dimension x dimension grid. +regions+ lists
|
|
26
|
+
# [x0, x1, y0, y1, transform], each covering modules x0...x1 and
|
|
27
|
+
# y0...y1 (use whole(d, transform) for a single transform). With an
|
|
28
|
+
# +offset+ (pixels; an Integer or [horizontal, vertical], see
|
|
29
|
+
# vote_offsets) a module is the majority vote of its centre pixel and
|
|
30
|
+
# the four pixels that far left, right, above and below it, which
|
|
31
|
+
# ignores isolated noise.
|
|
32
|
+
def sample(matrix, dimension, regions, offset: 0)
|
|
33
|
+
out = "\0".b * (dimension * dimension)
|
|
34
|
+
bits = matrix.bits
|
|
35
|
+
w = matrix.width
|
|
36
|
+
h = matrix.height
|
|
37
|
+
ox, oy = offset_pair(offset)
|
|
38
|
+
regions.each do |x0, x1, y0, y1, transform|
|
|
39
|
+
m = transform.m
|
|
40
|
+
my = y0
|
|
41
|
+
while my < y1
|
|
42
|
+
if ox > 0 || oy > 0
|
|
43
|
+
vote_row(bits, w, h, out, my * dimension, m, my + 0.5, x0, x1, ox, oy)
|
|
44
|
+
else
|
|
45
|
+
sample_row(bits, w, h, out, my * dimension, m, my + 0.5, x0, x1)
|
|
46
|
+
end
|
|
47
|
+
my += 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
BitMatrix.new(dimension, dimension, out)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Samples a grid through +transform+ plus a separable pixel
|
|
54
|
+
# displacement: module (mx, my) is read at
|
|
55
|
+
# transform(mx + 0.5, my + 0.5) + col_shift[mx] + row_shift[my], where
|
|
56
|
+
# the shifts are [dx, dy] pairs (see Candidate#warped_sample).
|
|
57
|
+
def sample_warped(matrix, dimension, transform, col_shift, row_shift, offset: 0)
|
|
58
|
+
out = "\0".b * (dimension * dimension)
|
|
59
|
+
bits = matrix.bits
|
|
60
|
+
w = matrix.width
|
|
61
|
+
h = matrix.height
|
|
62
|
+
ox, oy = offset_pair(offset)
|
|
63
|
+
m = transform.m
|
|
64
|
+
my = 0
|
|
65
|
+
while my < dimension
|
|
66
|
+
v = my + 0.5
|
|
67
|
+
rdx, rdy = row_shift[my]
|
|
68
|
+
base = my * dimension
|
|
69
|
+
mx = 0
|
|
70
|
+
while mx < dimension
|
|
71
|
+
u = mx + 0.5
|
|
72
|
+
den = m[6] * u + m[7] * v + m[8]
|
|
73
|
+
if den > 1e-9 || den < -1e-9
|
|
74
|
+
cdx, cdy = col_shift[mx]
|
|
75
|
+
px = (m[0] * u + m[1] * v + m[2]) / den + cdx + rdx
|
|
76
|
+
py = (m[3] * u + m[4] * v + m[5]) / den + cdy + rdy
|
|
77
|
+
if px >= 0.0 && py >= 0.0 && px < w && py < h && dark_vote?(bits, w, h, px.to_i, py.to_i, ox, oy)
|
|
78
|
+
out.setbyte(base + mx, 1)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
mx += 1
|
|
82
|
+
end
|
|
83
|
+
my += 1
|
|
84
|
+
end
|
|
85
|
+
BitMatrix.new(dimension, dimension, out)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Samples the grid from the grayscale +image+ instead of the binarized
|
|
89
|
+
# one: each module's luminance is the mean of its centre pixel and the
|
|
90
|
+
# pixels a quarter module away (bilinear at the centre for modules
|
|
91
|
+
# under 3 px), and a module is dark if it is darker than the mean of
|
|
92
|
+
# the (2r+1) x (2r+1) modules around it. +regions+ as in
|
|
93
|
+
# sample. Keeps information the binarization lost (blur, uneven light,
|
|
94
|
+
# small modules). +inverted+ reads light-on-dark symbols. +offset+
|
|
95
|
+
# overrides the quarter module offsets derived from +module_size+
|
|
96
|
+
# (see vote_offsets).
|
|
97
|
+
def sample_luminance(image, dimension, regions, module_size, radius: 4, inverted: false, offset: nil)
|
|
98
|
+
lum = Array.new(dimension * dimension, 0)
|
|
99
|
+
ox, oy = offset_pair(offset || vote_offset(module_size))
|
|
100
|
+
regions.each do |x0, x1, y0, y1, transform|
|
|
101
|
+
m = transform.m
|
|
102
|
+
my = y0
|
|
103
|
+
while my < y1
|
|
104
|
+
luminance_row(image.data, image.width, image.height, lum, my * dimension, m, my + 0.5, x0, x1, ox, oy)
|
|
105
|
+
my += 1
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
grid = threshold_grid(lum, dimension, radius)
|
|
109
|
+
inverted ? grid.invert : grid
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The region list covering a whole grid with one transform.
|
|
113
|
+
def whole(dimension, transform)
|
|
114
|
+
[[0, dimension, 0, dimension, transform]]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Majority sampling offset for a module size: a quarter module, none
|
|
118
|
+
# below 3 px.
|
|
119
|
+
def vote_offset(module_size)
|
|
120
|
+
module_size >= 3 ? (module_size / 4.0).round : 0
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# [horizontal, vertical] majority sampling offsets for the modules
|
|
124
|
+
# of +transform+ at the centre of a dimension x dimension grid: a
|
|
125
|
+
# quarter of the module's horizontal (vertical) chord through its
|
|
126
|
+
# centre, none below 3 px. Unlike vote_offset this keeps the samples
|
|
127
|
+
# inside modules that are not square in the image - fax pages
|
|
128
|
+
# (204 x 98 dpi), strong perspective.
|
|
129
|
+
def vote_offsets(transform, dimension)
|
|
130
|
+
c = dimension * 0.5
|
|
131
|
+
o = transform.apply(c, c) or return [0, 0]
|
|
132
|
+
u = transform.apply(c + 1, c) or return [0, 0]
|
|
133
|
+
v = transform.apply(c, c + 1) or return [0, 0]
|
|
134
|
+
ux = u[0] - o[0]
|
|
135
|
+
uy = u[1] - o[1]
|
|
136
|
+
vx = v[0] - o[0]
|
|
137
|
+
vy = v[1] - o[1]
|
|
138
|
+
area = (ux * vy - uy * vx).abs
|
|
139
|
+
horizontal = area / [uy.abs, vy.abs].max
|
|
140
|
+
vertical = area / [ux.abs, vx.abs].max
|
|
141
|
+
[chord_offset(horizontal), chord_offset(vertical)]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def chord_offset(chord)
|
|
145
|
+
chord.finite? && chord >= 3 ? (chord / 4.0).round : 0
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# [ox, oy] from an Integer or a pair.
|
|
149
|
+
def offset_pair(offset)
|
|
150
|
+
offset.is_a?(Array) ? offset : [offset, offset]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Like sample_row, voting over five pixels per module: the centre and
|
|
154
|
+
# those +ox+ left and right, +oy+ above and below.
|
|
155
|
+
def vote_row(bits, w, h, out, off, m, v, x0, x1, ox, oy)
|
|
156
|
+
nx, ny, nd, sx, sy, sd = fixed_row(m, v, x0, x1)
|
|
157
|
+
return unless nx
|
|
158
|
+
|
|
159
|
+
max_x = w - 1
|
|
160
|
+
max_y = h - 1
|
|
161
|
+
mx = x0
|
|
162
|
+
while mx < x1
|
|
163
|
+
if nd != 0
|
|
164
|
+
ix = nx / nd
|
|
165
|
+
iy = ny / nd
|
|
166
|
+
if ix >= 0 && iy >= 0 && ix < w && iy < h
|
|
167
|
+
row = iy * w
|
|
168
|
+
l = ix - ox
|
|
169
|
+
l = 0 if l < 0
|
|
170
|
+
r = ix + ox
|
|
171
|
+
r = max_x if r > max_x
|
|
172
|
+
t = iy - oy
|
|
173
|
+
t = 0 if t < 0
|
|
174
|
+
b = iy + oy
|
|
175
|
+
b = max_y if b > max_y
|
|
176
|
+
votes = bits.getbyte(row + ix) + bits.getbyte(row + l) + bits.getbyte(row + r) +
|
|
177
|
+
bits.getbyte(t * w + ix) + bits.getbyte(b * w + ix)
|
|
178
|
+
out.setbyte(off + mx, 1) if votes >= 3
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
nx += sx
|
|
182
|
+
ny += sy
|
|
183
|
+
nd += sd
|
|
184
|
+
mx += 1
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Fixed-point stepping along a module row: the transform's numerators
|
|
189
|
+
# and denominator at module (x0 + 0.5, v) and their increments per
|
|
190
|
+
# module, as Integers scaled so that the denominator's magnitude is
|
|
191
|
+
# about FIXED_ONE. Pixel (nx / nd, ny / nd) is then found with integer
|
|
192
|
+
# floor division, which is exact up to rounding of the scaled values
|
|
193
|
+
# and 3x faster than Float arithmetic. nil if the row is degenerate.
|
|
194
|
+
def fixed_row(m, v, x0, x1)
|
|
195
|
+
u = x0 + 0.5
|
|
196
|
+
d0 = m[6] * u + m[7] * v + m[8]
|
|
197
|
+
d1 = d0 + m[6] * (x1 - x0 - 1)
|
|
198
|
+
d0a = d0.abs
|
|
199
|
+
d1a = d1.abs
|
|
200
|
+
mag = d0a > d1a ? d0a : d1a
|
|
201
|
+
return unless mag > 1e-12 && mag < Float::INFINITY
|
|
202
|
+
|
|
203
|
+
s = FIXED_ONE / mag
|
|
204
|
+
nx = (m[0] * u + m[1] * v + m[2]) * s
|
|
205
|
+
ny = (m[3] * u + m[4] * v + m[5]) * s
|
|
206
|
+
# Rows far outside the image would need Bignums; they read as light.
|
|
207
|
+
return unless nx.abs < FIXED_LIMIT && ny.abs < FIXED_LIMIT && (m[0] * s).abs < FIXED_LIMIT &&
|
|
208
|
+
(m[3] * s).abs < FIXED_LIMIT
|
|
209
|
+
|
|
210
|
+
[nx.round, ny.round, (d0 * s).round, (m[0] * s).round, (m[3] * s).round, (m[6] * s).round]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Mean luminance (x 4, as Integer) of modules x0...x1 of one module
|
|
214
|
+
# row: half the centre pixel, an eighth each of the pixels +ox+ left
|
|
215
|
+
# and right, +oy+ above and below (bilinear at the centre if both are 0).
|
|
216
|
+
def luminance_row(data, w, h, lum, off, m, v, x0, x1, ox, oy)
|
|
217
|
+
bx = m[1] * v + m[2]
|
|
218
|
+
by = m[4] * v + m[5]
|
|
219
|
+
bd = m[7] * v + m[8]
|
|
220
|
+
m0 = m[0]
|
|
221
|
+
m3 = m[3]
|
|
222
|
+
m6 = m[6]
|
|
223
|
+
mx = x0
|
|
224
|
+
while mx < x1
|
|
225
|
+
u = mx + 0.5
|
|
226
|
+
den = m6 * u + bd
|
|
227
|
+
value = 1020
|
|
228
|
+
if den > 1e-9 || den < -1e-9
|
|
229
|
+
px = (m0 * u + bx) / den
|
|
230
|
+
py = (m3 * u + by) / den
|
|
231
|
+
if px >= 0.0 && py >= 0.0 && px < w && py < h
|
|
232
|
+
ix = px.to_i
|
|
233
|
+
iy = py.to_i
|
|
234
|
+
value =
|
|
235
|
+
if ox == 0 && oy == 0
|
|
236
|
+
bilinear4(data, w, h, px - 0.5, py - 0.5)
|
|
237
|
+
else
|
|
238
|
+
l = ix - ox
|
|
239
|
+
l = 0 if l < 0
|
|
240
|
+
r = ix + ox
|
|
241
|
+
r = w - 1 if r >= w
|
|
242
|
+
t = iy - oy
|
|
243
|
+
t = 0 if t < 0
|
|
244
|
+
b = iy + oy
|
|
245
|
+
b = h - 1 if b >= h
|
|
246
|
+
c = iy * w
|
|
247
|
+
(4 * data.getbyte(c + ix) + data.getbyte(c + l) + data.getbyte(c + r) +
|
|
248
|
+
data.getbyte(t * w + ix) + data.getbyte(b * w + ix)) >> 1
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
lum[off + mx] = value
|
|
253
|
+
mx += 1
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Bilinearly interpolated luminance x 4 at continuous pixel position
|
|
258
|
+
# (x, y) measured from pixel centres.
|
|
259
|
+
def bilinear4(data, w, h, x, y)
|
|
260
|
+
x = 0.0 if x.negative?
|
|
261
|
+
y = 0.0 if y.negative?
|
|
262
|
+
ix = x.to_i
|
|
263
|
+
iy = y.to_i
|
|
264
|
+
ix1 = ix + 1 < w ? ix + 1 : ix
|
|
265
|
+
iy1 = iy + 1 < h ? iy + 1 : iy
|
|
266
|
+
fx = x - ix
|
|
267
|
+
fy = y - iy
|
|
268
|
+
top = data.getbyte(iy * w + ix) * (1 - fx) + data.getbyte(iy * w + ix1) * fx
|
|
269
|
+
bottom = data.getbyte(iy1 * w + ix) * (1 - fx) + data.getbyte(iy1 * w + ix1) * fx
|
|
270
|
+
((top + (bottom - top) * fy) * 4).round
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# BitMatrix of modules darker than their neighbourhood mean (summed
|
|
274
|
+
# area table over the module luminances).
|
|
275
|
+
def threshold_grid(lum, n, radius)
|
|
276
|
+
stride = n + 1
|
|
277
|
+
sat = Array.new(stride * stride, 0)
|
|
278
|
+
y = 0
|
|
279
|
+
while y < n
|
|
280
|
+
row_sum = 0
|
|
281
|
+
x = 0
|
|
282
|
+
while x < n
|
|
283
|
+
row_sum += lum[y * n + x]
|
|
284
|
+
sat[(y + 1) * stride + x + 1] = sat[y * stride + x + 1] + row_sum
|
|
285
|
+
x += 1
|
|
286
|
+
end
|
|
287
|
+
y += 1
|
|
288
|
+
end
|
|
289
|
+
out = "\0".b * (n * n)
|
|
290
|
+
y = 0
|
|
291
|
+
while y < n
|
|
292
|
+
y0 = y - radius
|
|
293
|
+
y0 = 0 if y0.negative?
|
|
294
|
+
y1 = y + radius + 1
|
|
295
|
+
y1 = n if y1 > n
|
|
296
|
+
x = 0
|
|
297
|
+
while x < n
|
|
298
|
+
x0 = x - radius
|
|
299
|
+
x0 = 0 if x0.negative?
|
|
300
|
+
x1 = x + radius + 1
|
|
301
|
+
x1 = n if x1 > n
|
|
302
|
+
sum = sat[y1 * stride + x1] - sat[y0 * stride + x1] - sat[y1 * stride + x0] + sat[y0 * stride + x0]
|
|
303
|
+
out.setbyte(y * n + x, 1) if lum[y * n + x] * (y1 - y0) * (x1 - x0) < sum
|
|
304
|
+
x += 1
|
|
305
|
+
end
|
|
306
|
+
y += 1
|
|
307
|
+
end
|
|
308
|
+
BitMatrix.new(n, n, out)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Majority of the pixel (ix, iy) and the pixels +ox+ left and right,
|
|
312
|
+
# +oy+ above and below (just the pixel when both are 0).
|
|
313
|
+
def dark_vote?(bits, w, h, ix, iy, ox, oy)
|
|
314
|
+
row = iy * w
|
|
315
|
+
return bits.getbyte(row + ix) == 1 if ox == 0 && oy == 0
|
|
316
|
+
|
|
317
|
+
l = ix - ox
|
|
318
|
+
l = 0 if l < 0
|
|
319
|
+
r = ix + ox
|
|
320
|
+
r = w - 1 if r >= w
|
|
321
|
+
t = iy - oy
|
|
322
|
+
t = 0 if t < 0
|
|
323
|
+
b = iy + oy
|
|
324
|
+
b = h - 1 if b >= h
|
|
325
|
+
bits.getbyte(row + ix) + bits.getbyte(row + l) + bits.getbyte(row + r) +
|
|
326
|
+
bits.getbyte(t * w + ix) + bits.getbyte(b * w + ix) >= 3
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Samples modules x0...x1 of the module row at height v (a module
|
|
330
|
+
# centre) into out[off + x].
|
|
331
|
+
def sample_row(bits, w, h, out, off, m, v, x0, x1)
|
|
332
|
+
nx, ny, nd, sx, sy, sd = fixed_row(m, v, x0, x1)
|
|
333
|
+
return unless nx
|
|
334
|
+
|
|
335
|
+
mx = x0
|
|
336
|
+
while mx < x1
|
|
337
|
+
if nd != 0
|
|
338
|
+
ix = nx / nd
|
|
339
|
+
iy = ny / nd
|
|
340
|
+
out.setbyte(off + mx, 1) if ix >= 0 && iy >= 0 && ix < w && iy < h && bits.getbyte(iy * w + ix) == 1
|
|
341
|
+
end
|
|
342
|
+
nx += sx
|
|
343
|
+
ny += sy
|
|
344
|
+
nd += sd
|
|
345
|
+
mx += 1
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
class Detector
|
|
5
|
+
# Chooses which finder patterns may form a symbol (like zxing-cpp's
|
|
6
|
+
# GenerateFinderPatternSets): every combination of three patterns with
|
|
7
|
+
# similar module sizes is scored by how close it comes to an isosceles
|
|
8
|
+
# right triangle and how well the module sizes agree; implausible ones
|
|
9
|
+
# (angle off by more than 45 degrees, legs differing by more than 2.6x,
|
|
10
|
+
# impossible module counts) are dropped.
|
|
11
|
+
module PatternSets
|
|
12
|
+
COS_45 = Math.cos(Math::PI / 4)
|
|
13
|
+
# Legs may differ by up to this factor: perspective, and pixels that
|
|
14
|
+
# are not square (standard resolution fax pages: 204 x 98 dpi).
|
|
15
|
+
MAX_LEG_RATIO = 2.6
|
|
16
|
+
# Module sizes within one symbol may differ by up to this factor.
|
|
17
|
+
MAX_SIZE_RATIO = 2.5
|
|
18
|
+
|
|
19
|
+
# A scored triple. bl, tl, tr are ordered so that tl is the corner and
|
|
20
|
+
# (tr - tl) x (bl - tl) > 0 (clockwise tl -> tr -> bl on screen) -
|
|
21
|
+
# a mirrored symbol appears with tr and bl swapped.
|
|
22
|
+
Triple = Struct.new(:bl, :tl, :tr, :score) do
|
|
23
|
+
def patterns
|
|
24
|
+
[bl, tl, tr]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Up to +limit+ triples, best first.
|
|
31
|
+
def generate(patterns, limit)
|
|
32
|
+
sorted = patterns.sort_by(&:module_size)
|
|
33
|
+
n = sorted.size
|
|
34
|
+
sets = []
|
|
35
|
+
i = 0
|
|
36
|
+
while i < n - 2
|
|
37
|
+
a = sorted[i]
|
|
38
|
+
j = i + 1
|
|
39
|
+
while j < n - 1
|
|
40
|
+
b = sorted[j]
|
|
41
|
+
break if b.module_size > a.module_size * MAX_SIZE_RATIO
|
|
42
|
+
|
|
43
|
+
k = j + 1
|
|
44
|
+
while k < n
|
|
45
|
+
c = sorted[k]
|
|
46
|
+
break if c.module_size > a.module_size * MAX_SIZE_RATIO
|
|
47
|
+
|
|
48
|
+
set = evaluate(a, b, c)
|
|
49
|
+
sets << set if set
|
|
50
|
+
k += 1
|
|
51
|
+
end
|
|
52
|
+
j += 1
|
|
53
|
+
end
|
|
54
|
+
i += 1
|
|
55
|
+
end
|
|
56
|
+
sets.sort_by!(&:score)
|
|
57
|
+
sets.first(limit)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Triples formed by each pattern and two of its +k+ nearest neighbours
|
|
61
|
+
# of similar module size, best first. Unlike #generate this scales to
|
|
62
|
+
# pages with many symbols (label sheets), where the best-shaped
|
|
63
|
+
# triples of a global search mostly join finders of different symbols
|
|
64
|
+
# laid out on a regular grid.
|
|
65
|
+
def neighbourhood(patterns, k)
|
|
66
|
+
sets = {}
|
|
67
|
+
patterns.each do |b|
|
|
68
|
+
nearest(patterns, b, k).combination(2) do |a, c|
|
|
69
|
+
next unless similar_size?(a, c)
|
|
70
|
+
|
|
71
|
+
set = evaluate(a, b, c) or next
|
|
72
|
+
sets[[set.bl, set.tl, set.tr].map(&:object_id)] ||= set
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
sets.values.sort_by!(&:score)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The (up to) k patterns of a similar module size nearest to +b+.
|
|
79
|
+
def nearest(patterns, b, k)
|
|
80
|
+
near = []
|
|
81
|
+
dists = []
|
|
82
|
+
bx = b.x
|
|
83
|
+
by = b.y
|
|
84
|
+
size = b.module_size
|
|
85
|
+
i = 0
|
|
86
|
+
n = patterns.size
|
|
87
|
+
while i < n
|
|
88
|
+
p = patterns[i]
|
|
89
|
+
i += 1
|
|
90
|
+
next if p.equal?(b) || p.module_size >= size * MAX_SIZE_RATIO || size >= p.module_size * MAX_SIZE_RATIO
|
|
91
|
+
|
|
92
|
+
dx = p.x - bx
|
|
93
|
+
dy = p.y - by
|
|
94
|
+
d = dx * dx + dy * dy
|
|
95
|
+
next if near.size == k && d >= dists[-1]
|
|
96
|
+
|
|
97
|
+
j = dists.bsearch_index { |e| e > d } || dists.size
|
|
98
|
+
dists.insert(j, d)
|
|
99
|
+
near.insert(j, p)
|
|
100
|
+
if near.size > k
|
|
101
|
+
dists.pop
|
|
102
|
+
near.pop
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
near
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Splits +triples+ into [clean, foreign]: foreign triples have at least
|
|
109
|
+
# two other patterns of a similar module size inside (or within 1.5
|
|
110
|
+
# modules of) their symbol area, the parallelogram of the three
|
|
111
|
+
# finder centres. A triple joining finders of neighbouring symbols on
|
|
112
|
+
# a sheet of labels always has (the finders of the symbol it spans lie
|
|
113
|
+
# on its edges); a real symbol has at most a false pattern or so in
|
|
114
|
+
# its data area - its neighbours' finders are 7 modules plus the quiet
|
|
115
|
+
# zone away. Foreign triples come with the fewest such patterns first.
|
|
116
|
+
def partition_clean(triples, patterns)
|
|
117
|
+
by_x = patterns.sort_by(&:x)
|
|
118
|
+
xs = by_x.map(&:x)
|
|
119
|
+
counted = triples.map { |t| [foreign_count(t, by_x, xs), t] }
|
|
120
|
+
clean, foreign = counted.partition { |count, _| count < 2 }
|
|
121
|
+
[clean.map(&:last), foreign.sort_by { |count, t| [count, t.score] }.map(&:last)]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# True if +triple+ is foreign (see partition_clean). +by_x+ are the
|
|
125
|
+
# patterns sorted by x, +xs+ their x coordinates.
|
|
126
|
+
def foreign?(triple, by_x, xs)
|
|
127
|
+
foreign_count(triple, by_x, xs) >= 2
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Number of patterns (other than the triple's own, of similar module
|
|
131
|
+
# size) inside the triple's parallelogram or within 1.5 modules of it,
|
|
132
|
+
# counted up to +cap+. Points are tested in the parallelogram's own
|
|
133
|
+
# coordinates (tl + s * (tr - tl) + t * (bl - tl)), with the margin
|
|
134
|
+
# converted to s and t units.
|
|
135
|
+
def foreign_count(triple, by_x, xs, cap = 4)
|
|
136
|
+
tl = triple.tl
|
|
137
|
+
tr = triple.tr
|
|
138
|
+
bl = triple.bl
|
|
139
|
+
x0 = tl.x
|
|
140
|
+
y0 = tl.y
|
|
141
|
+
ux = tr.x - x0
|
|
142
|
+
uy = tr.y - y0
|
|
143
|
+
vx = bl.x - x0
|
|
144
|
+
vy = bl.y - y0
|
|
145
|
+
det = ux * vy - uy * vx
|
|
146
|
+
return 0 if det.zero?
|
|
147
|
+
|
|
148
|
+
size = (tl.module_size + tr.module_size + bl.module_size) / 3.0
|
|
149
|
+
margin = 1.5 * size
|
|
150
|
+
ms = margin * Math.hypot(vx, vy) / det.abs
|
|
151
|
+
mt = margin * Math.hypot(ux, uy) / det.abs
|
|
152
|
+
corners_x = [x0, x0 + ux, x0 + ux + vx, x0 + vx]
|
|
153
|
+
corners_y = [y0, y0 + uy, y0 + uy + vy, y0 + vy]
|
|
154
|
+
min_x = corners_x.min - margin
|
|
155
|
+
max_x = corners_x.max + margin
|
|
156
|
+
min_y = corners_y.min - margin
|
|
157
|
+
max_y = corners_y.max + margin
|
|
158
|
+
i = xs.bsearch_index { |x| x >= min_x } or return 0
|
|
159
|
+
count = 0
|
|
160
|
+
n = by_x.size
|
|
161
|
+
while i < n
|
|
162
|
+
p = by_x[i]
|
|
163
|
+
i += 1
|
|
164
|
+
px = p.x
|
|
165
|
+
break if px > max_x
|
|
166
|
+
|
|
167
|
+
py = p.y
|
|
168
|
+
next if py < min_y || py > max_y || p.equal?(tl) || p.equal?(tr) || p.equal?(bl)
|
|
169
|
+
next unless p.module_size < size * MAX_SIZE_RATIO && p.module_size * MAX_SIZE_RATIO > size
|
|
170
|
+
|
|
171
|
+
dx = px - x0
|
|
172
|
+
dy = py - y0
|
|
173
|
+
s = (dx * vy - dy * vx) / det
|
|
174
|
+
t = (ux * dy - uy * dx) / det
|
|
175
|
+
next unless s > -ms && s < 1 + ms && t > -mt && t < 1 + mt
|
|
176
|
+
|
|
177
|
+
count += 1
|
|
178
|
+
return count if count >= cap
|
|
179
|
+
end
|
|
180
|
+
count
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def similar_size?(p, q)
|
|
184
|
+
p.module_size < q.module_size * MAX_SIZE_RATIO && q.module_size < p.module_size * MAX_SIZE_RATIO
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Scores one combination; nil if it cannot be a symbol.
|
|
188
|
+
def evaluate(a, b, c)
|
|
189
|
+
ab = dist2(a, b)
|
|
190
|
+
bc = dist2(b, c)
|
|
191
|
+
ac = dist2(a, c)
|
|
192
|
+
# Make b the corner (opposite the longest side, which becomes ac).
|
|
193
|
+
if bc >= ab && bc >= ac
|
|
194
|
+
a, b = b, a
|
|
195
|
+
bc, ac = ac, bc
|
|
196
|
+
elsif ab >= bc && ab >= ac
|
|
197
|
+
b, c = c, b
|
|
198
|
+
ab, ac = ac, ab
|
|
199
|
+
end
|
|
200
|
+
return if ab <= 0 || bc <= 0
|
|
201
|
+
|
|
202
|
+
dab = Math.sqrt(ab)
|
|
203
|
+
dbc = Math.sqrt(bc)
|
|
204
|
+
return if dab > MAX_LEG_RATIO * dbc || dbc > MAX_LEG_RATIO * dab
|
|
205
|
+
|
|
206
|
+
mean_size = (a.module_size + b.module_size + c.module_size) / 3.0
|
|
207
|
+
modules = (dab + dbc) / (2 * mean_size) + 7
|
|
208
|
+
return if modules < 21 * 0.8 || modules > 177 * 1.4
|
|
209
|
+
|
|
210
|
+
cos = (ab + bc - ac) / (2 * dab * dbc)
|
|
211
|
+
return if cos.abs > COS_45
|
|
212
|
+
|
|
213
|
+
# 0 for an isosceles right triangle, scale invariant.
|
|
214
|
+
shape = ((ac - 2 * ab).abs + (ac - 2 * bc).abs) / ac
|
|
215
|
+
sizes = [a.module_size, b.module_size, c.module_size]
|
|
216
|
+
size_spread = sizes.max / sizes.min - 1.0
|
|
217
|
+
score = shape + 2 * size_spread + 0.5 * (1.0 - [a.quality, b.quality, c.quality].min)
|
|
218
|
+
|
|
219
|
+
a, c = c, a if cross(b, c, a).negative?
|
|
220
|
+
Triple.new(a, b, c, score)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def dist2(p, q)
|
|
224
|
+
dx = p.x - q.x
|
|
225
|
+
dy = p.y - q.y
|
|
226
|
+
dx * dx + dy * dy
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# z component of (c - b) x (a - b).
|
|
230
|
+
def cross(b, c, a)
|
|
231
|
+
(c.x - b.x) * (a.y - b.y) - (c.y - b.y) * (a.x - b.x)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|