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,587 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Detector kernels: finder pattern search in a binarized image. Kernel files
|
|
4
|
+
# are plain Ruby in the subset that Spinel compiles (see DESIGN.md,
|
|
5
|
+
# "Kernels"): the optional qr_reader-native gem builds them into a C
|
|
6
|
+
# extension, otherwise they run as they are.
|
|
7
|
+
module QrReaderKernels
|
|
8
|
+
# Values per pattern in the flat arrays of finder_scan: x, y, module size,
|
|
9
|
+
# quality, hit count.
|
|
10
|
+
FINDER_FIELDS = 5
|
|
11
|
+
|
|
12
|
+
# Scans the rows y with rows.getbyte(y) == 1 of a width x height bit
|
|
13
|
+
# matrix (one byte per pixel, 1 = dark) for QR finder patterns.
|
|
14
|
+
# +known+ holds the patterns of earlier scans of the same matrix as flat
|
|
15
|
+
# [x, y, module_size, quality, count, ...] (FINDER_FIELDS per pattern).
|
|
16
|
+
# Returns them (counts updated with the new row hits) followed by the
|
|
17
|
+
# patterns found in these rows, in the order found, followed by +height+
|
|
18
|
+
# values: the number of 1:1:3:1:1 run sequences (hits) in each row (0
|
|
19
|
+
# for rows not scanned), a texture measure. See
|
|
20
|
+
# QrReader::Detector::FinderPatternFinder for the method.
|
|
21
|
+
# spinel-sample: QrReaderKernels.finder_scan("\x00".b * 64, 8, 8, "\x01".b * 8, [1.5, 1.5, 1.0, 1.0, 1.0])
|
|
22
|
+
def self.finder_scan(bits, width, height, rows, known)
|
|
23
|
+
scanner = FinderScanner.new(bits, width, height, known)
|
|
24
|
+
y = 0
|
|
25
|
+
out = []
|
|
26
|
+
hits = []
|
|
27
|
+
while y < height
|
|
28
|
+
n = 0
|
|
29
|
+
n = scanner.scan_row(y) if rows.getbyte(y) == 1
|
|
30
|
+
hits << n.to_f
|
|
31
|
+
y += 1
|
|
32
|
+
end
|
|
33
|
+
scanner.append_patterns(out)
|
|
34
|
+
i = 0
|
|
35
|
+
while i < height
|
|
36
|
+
out << hits[i]
|
|
37
|
+
i += 1
|
|
38
|
+
end
|
|
39
|
+
out
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# State of one finder_scan call: the patterns found so far, indexed in a
|
|
43
|
+
# grid of 32 x 32 pixel cells (one linked list of pattern numbers per
|
|
44
|
+
# cell, in insertion order) for the "already found here?" test of every
|
|
45
|
+
# row hit. Multiple results of the cross-checks are passed in instance
|
|
46
|
+
# variables (no allocation per hit).
|
|
47
|
+
class FinderScanner
|
|
48
|
+
SQRT2 = Math.sqrt(2)
|
|
49
|
+
CELL_SHIFT = 5
|
|
50
|
+
DARK = "\x01".b.freeze
|
|
51
|
+
LIGHT = "\x00".b.freeze
|
|
52
|
+
|
|
53
|
+
def initialize(bits, width, height, known)
|
|
54
|
+
@bits = bits
|
|
55
|
+
@width = width
|
|
56
|
+
@height = height
|
|
57
|
+
@xs = []
|
|
58
|
+
@ys = []
|
|
59
|
+
@sizes = []
|
|
60
|
+
@qualities = []
|
|
61
|
+
@counts = []
|
|
62
|
+
@cell_cols = (width >> CELL_SHIFT) + 1
|
|
63
|
+
cells = @cell_cols * ((height >> CELL_SHIFT) + 1)
|
|
64
|
+
@heads = Array.new(cells, -1)
|
|
65
|
+
@tails = Array.new(cells, -1)
|
|
66
|
+
@link_next = []
|
|
67
|
+
@link_pattern = []
|
|
68
|
+
# Results of walk / cross_check / probe.
|
|
69
|
+
@run0 = 0
|
|
70
|
+
@run1 = 0
|
|
71
|
+
@run2 = 0
|
|
72
|
+
@centre = 0.0
|
|
73
|
+
@spread = 0
|
|
74
|
+
@diagonal = 0.0
|
|
75
|
+
@probe_centre = 0.0
|
|
76
|
+
@probe_spread = 0
|
|
77
|
+
@probe_bridge = false
|
|
78
|
+
i = 0
|
|
79
|
+
n = known.size
|
|
80
|
+
while i + 4 < n
|
|
81
|
+
add(known[i], known[i + 1], known[i + 2], known[i + 3], known[i + 4].to_i)
|
|
82
|
+
i += FINDER_FIELDS
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Appends all patterns to +out+ as flat [x, y, module_size, quality,
|
|
87
|
+
# count, ...].
|
|
88
|
+
def append_patterns(out)
|
|
89
|
+
i = 0
|
|
90
|
+
n = @xs.size
|
|
91
|
+
while i < n
|
|
92
|
+
out << @xs[i] << @ys[i] << @sizes[i] << @qualities[i] << @counts[i].to_f
|
|
93
|
+
i += 1
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Finds 1:1:3:1:1 run sequences in row y and returns their number. p0..p5
|
|
98
|
+
# are the boundaries of the current window of five runs (dark, light,
|
|
99
|
+
# dark, light, dark). Runs of one or two pixels (textures) are read
|
|
100
|
+
# directly, longer ones are searched for at C speed.
|
|
101
|
+
def scan_row(y)
|
|
102
|
+
bits = @bits
|
|
103
|
+
w = @width
|
|
104
|
+
off = y * w
|
|
105
|
+
row = bits.byteslice(off, w)
|
|
106
|
+
p0 = row.index(DARK)
|
|
107
|
+
return 0 if p0.nil?
|
|
108
|
+
|
|
109
|
+
p1 = row.index(LIGHT, p0)
|
|
110
|
+
return 0 if p1.nil?
|
|
111
|
+
|
|
112
|
+
p2 = row.index(DARK, p1)
|
|
113
|
+
return 0 if p2.nil?
|
|
114
|
+
|
|
115
|
+
p3 = row.index(LIGHT, p2)
|
|
116
|
+
return 0 if p3.nil?
|
|
117
|
+
|
|
118
|
+
p4 = row.index(DARK, p3)
|
|
119
|
+
return 0 if p4.nil?
|
|
120
|
+
|
|
121
|
+
p5 = row.index(LIGHT, p4)
|
|
122
|
+
p5 = w if p5.nil?
|
|
123
|
+
hits = 0
|
|
124
|
+
while true
|
|
125
|
+
total = p5 - p0
|
|
126
|
+
# Ratio test in units of 1/14 module: each run must be within
|
|
127
|
+
# half a module (+0.5 px) of its nominal width, the centre within
|
|
128
|
+
# 0.75 modules.
|
|
129
|
+
tol = total + 7
|
|
130
|
+
if total >= 7 &&
|
|
131
|
+
(14 * (p1 - p0) - 2 * total).abs <= tol &&
|
|
132
|
+
(14 * (p2 - p1) - 2 * total).abs <= tol &&
|
|
133
|
+
(14 * (p3 - p2) - 6 * total).abs <= tol + (total >> 1) &&
|
|
134
|
+
(14 * (p4 - p3) - 2 * total).abs <= tol &&
|
|
135
|
+
(14 * (p5 - p4) - 2 * total).abs <= tol
|
|
136
|
+
hit((p2 + p3) >> 1, y, total)
|
|
137
|
+
hits += 1
|
|
138
|
+
end
|
|
139
|
+
break if p5 >= w
|
|
140
|
+
|
|
141
|
+
p0 = p2
|
|
142
|
+
p1 = p3
|
|
143
|
+
p2 = p4
|
|
144
|
+
p3 = p5
|
|
145
|
+
p4 = p3 + 1
|
|
146
|
+
if p4 < w && bits.getbyte(off + p4) == 0
|
|
147
|
+
p4 += 1
|
|
148
|
+
if p4 < w && bits.getbyte(off + p4) == 0
|
|
149
|
+
p4 = row.index(DARK, p4)
|
|
150
|
+
break if p4.nil?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
break if p4 >= w
|
|
154
|
+
|
|
155
|
+
p5 = p4 + 1
|
|
156
|
+
if p5 < w && bits.getbyte(off + p5) == 1
|
|
157
|
+
p5 += 1
|
|
158
|
+
if p5 < w && bits.getbyte(off + p5) == 1
|
|
159
|
+
p5 = row.index(LIGHT, p5)
|
|
160
|
+
p5 = w if p5.nil?
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
hits
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# A row hit at dark pixel (x, y) spanning +total+ pixels: counts it for
|
|
168
|
+
# a pattern found before, or verifies it and adds a new pattern.
|
|
169
|
+
def hit(x, y, total)
|
|
170
|
+
known = known_pattern(x + 0.5, y + 0.5)
|
|
171
|
+
if known >= 0
|
|
172
|
+
@counts[known] += 1
|
|
173
|
+
elsif probe(x, y, total)
|
|
174
|
+
locate(x, total, @probe_centre, @probe_spread, @probe_bridge)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Number of the first pattern found whose centre lies within 3.5 module
|
|
179
|
+
# sizes of (x, y), or -1.
|
|
180
|
+
def known_pattern(x, y)
|
|
181
|
+
link = @heads[((y.to_i >> CELL_SHIFT) * @cell_cols) + (x.to_i >> CELL_SHIFT)]
|
|
182
|
+
while link >= 0
|
|
183
|
+
i = @link_pattern[link]
|
|
184
|
+
r = @sizes[i] * 3.5
|
|
185
|
+
dx = @xs[i] - x
|
|
186
|
+
dy = @ys[i] - y
|
|
187
|
+
return i if dx * dx + dy * dy < r * r
|
|
188
|
+
|
|
189
|
+
link = @link_next[link]
|
|
190
|
+
end
|
|
191
|
+
-1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def add(x, y, module_size, quality, count)
|
|
195
|
+
i = @xs.size
|
|
196
|
+
@xs << x
|
|
197
|
+
@ys << y
|
|
198
|
+
@sizes << module_size
|
|
199
|
+
@qualities << quality
|
|
200
|
+
@counts << count
|
|
201
|
+
r = module_size * 3.5
|
|
202
|
+
y0 = clamp((y - r).floor, 0, @height - 1) >> CELL_SHIFT
|
|
203
|
+
y1 = clamp((y + r).ceil, 0, @height - 1) >> CELL_SHIFT
|
|
204
|
+
x0 = clamp((x - r).floor, 0, @width - 1) >> CELL_SHIFT
|
|
205
|
+
x1 = clamp((x + r).ceil, 0, @width - 1) >> CELL_SHIFT
|
|
206
|
+
cy = y0
|
|
207
|
+
while cy <= y1
|
|
208
|
+
cx = x0
|
|
209
|
+
while cx <= x1
|
|
210
|
+
link_cell(cy * @cell_cols + cx, i)
|
|
211
|
+
cx += 1
|
|
212
|
+
end
|
|
213
|
+
cy += 1
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def link_cell(cell, pattern)
|
|
218
|
+
link = @link_pattern.size
|
|
219
|
+
@link_pattern << pattern
|
|
220
|
+
@link_next << -1
|
|
221
|
+
tail = @tails[cell]
|
|
222
|
+
if tail < 0
|
|
223
|
+
@heads[cell] = link
|
|
224
|
+
else
|
|
225
|
+
@link_next[tail] = link
|
|
226
|
+
end
|
|
227
|
+
@tails[cell] = link
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def clamp(v, lo, hi)
|
|
231
|
+
return lo if v < lo
|
|
232
|
+
return hi if v > hi
|
|
233
|
+
|
|
234
|
+
v
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Vertical check of a row hit at dark pixel (x, y) whose pattern spans
|
|
238
|
+
# +total+ pixels (see FinderPatternFinder#probe). On success sets
|
|
239
|
+
# @probe_centre, @probe_spread, @probe_bridge.
|
|
240
|
+
def probe(x, y, total)
|
|
241
|
+
m = total / 7.0
|
|
242
|
+
limit = 7 * m + 1
|
|
243
|
+
top = stone_end(x, y, -1, limit, false)
|
|
244
|
+
bottom = stone_end(x, y, 1, limit, false)
|
|
245
|
+
if bottom - top + 2 < 1.2 * m && total >= 21
|
|
246
|
+
# A noise hole may cut a stone short.
|
|
247
|
+
top = stone_end(x, y, -1, limit, true)
|
|
248
|
+
bottom = stone_end(x, y, 1, limit, true)
|
|
249
|
+
end
|
|
250
|
+
stone = bottom - top + 1
|
|
251
|
+
return false if stone < 1.2 * m - 1 || stone > limit
|
|
252
|
+
return false unless rings_sampled?(x, top, bottom, total >= 21) ||
|
|
253
|
+
(total >= 14 && ring?(x, top - 1, -1, stone) && ring?(x, bottom + 1, 1, stone))
|
|
254
|
+
|
|
255
|
+
bridge = stone >= 9
|
|
256
|
+
return false unless cross_check(x, y, 0, 1, 3 * (total > stone * 3 ? total : stone * 3), bridge, false)
|
|
257
|
+
|
|
258
|
+
spread = @spread
|
|
259
|
+
return false unless spread >= 0.38 * total - 1 && spread <= 2.6 * total + 1
|
|
260
|
+
|
|
261
|
+
@probe_centre = @centre
|
|
262
|
+
@probe_spread = spread
|
|
263
|
+
@probe_bridge = bridge
|
|
264
|
+
true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Last row of the dark run through dark pixel (x, y) in vertical
|
|
268
|
+
# direction +dir+, at most +limit+ + 1 rows away; with +bridge+,
|
|
269
|
+
# single light pixels followed by two dark ones do not end it.
|
|
270
|
+
def stone_end(x, y, dir, limit, bridge)
|
|
271
|
+
bits = @bits
|
|
272
|
+
w = @width
|
|
273
|
+
h = @height
|
|
274
|
+
k = 0
|
|
275
|
+
while k <= limit
|
|
276
|
+
n = y + dir
|
|
277
|
+
break if n < 0 || n >= h
|
|
278
|
+
|
|
279
|
+
if bits.getbyte(n * w + x) != 1
|
|
280
|
+
n3 = n + 2 * dir
|
|
281
|
+
break unless bridge && n3 >= 0 && n3 < h && bits.getbyte((n + dir) * w + x) == 1 &&
|
|
282
|
+
bits.getbyte(n3 * w + x) == 1
|
|
283
|
+
end
|
|
284
|
+
y = n
|
|
285
|
+
k += 1
|
|
286
|
+
end
|
|
287
|
+
y
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Ring samples for the stone spanning rows top..bottom at column x:
|
|
291
|
+
# light 2, dark 3 vertical modules above and below its middle, at most
|
|
292
|
+
# one miss; with +vote+ each sample is the majority of three
|
|
293
|
+
# horizontal neighbours.
|
|
294
|
+
def rings_sampled?(x, top, bottom, vote)
|
|
295
|
+
h = @height
|
|
296
|
+
mv = (bottom - top + 1) / 3.0
|
|
297
|
+
centre = (top + bottom) >> 1
|
|
298
|
+
light = (2 * mv).round
|
|
299
|
+
dark = (3 * mv).round
|
|
300
|
+
misses = 0
|
|
301
|
+
misses += 1 unless centre - light >= 0 && colour_at?(x, centre - light, 0, vote)
|
|
302
|
+
misses += 1 unless centre + light < h && colour_at?(x, centre + light, 0, vote)
|
|
303
|
+
return false if misses > 1
|
|
304
|
+
|
|
305
|
+
misses += 1 unless centre - dark >= 0 && colour_at?(x, centre - dark, 1, vote)
|
|
306
|
+
return false if misses > 1
|
|
307
|
+
|
|
308
|
+
misses += 1 unless centre + dark < h && colour_at?(x, centre + dark, 1, vote)
|
|
309
|
+
misses <= 1
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# True if (x, y) has +colour+; with +vote+ the majority of (x - 1..x + 1, y).
|
|
313
|
+
def colour_at?(x, y, colour, vote)
|
|
314
|
+
off = y * @width
|
|
315
|
+
return @bits.getbyte(off + x) == colour unless vote && x > 0 && x < @width - 1
|
|
316
|
+
|
|
317
|
+
(@bits.getbyte(off + x - 1) == colour ? 1 : 0) + (@bits.getbyte(off + x) == colour ? 1 : 0) +
|
|
318
|
+
(@bits.getbyte(off + x + 1) == colour ? 1 : 0) >= 2
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# True if walking vertically from (x, y) in direction +dir+ meets a
|
|
322
|
+
# light run and then a dark run of 1..(3/4 * stone + 1) pixels each
|
|
323
|
+
# (the dark run may end at the image border).
|
|
324
|
+
def ring?(x, y, dir, stone)
|
|
325
|
+
bits = @bits
|
|
326
|
+
w = @width
|
|
327
|
+
h = @height
|
|
328
|
+
max = (stone * 3 >> 2) + 1
|
|
329
|
+
light = 0
|
|
330
|
+
while y >= 0 && y < h && bits.getbyte(y * w + x) == 0
|
|
331
|
+
light += 1
|
|
332
|
+
return false if light > max
|
|
333
|
+
|
|
334
|
+
y += dir
|
|
335
|
+
end
|
|
336
|
+
return false if light == 0 || y < 0 || y >= h
|
|
337
|
+
|
|
338
|
+
dark = 0
|
|
339
|
+
while y >= 0 && y < h && bits.getbyte(y * w + x) == 1
|
|
340
|
+
dark += 1
|
|
341
|
+
return false if dark > max
|
|
342
|
+
|
|
343
|
+
y += dir
|
|
344
|
+
end
|
|
345
|
+
true
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Confirms a row hit at dark pixel (x, y) whose pattern spans +total+
|
|
349
|
+
# pixels horizontally, centre row +cy+ and +spread_v+ pixels
|
|
350
|
+
# vertically, and adds the pattern (see FinderPatternFinder#locate).
|
|
351
|
+
def locate(x, total, cy, spread_v, noise_v)
|
|
352
|
+
range = 3 * (total > spread_v ? total : spread_v)
|
|
353
|
+
noise_h = total >= 21
|
|
354
|
+
iy = cy.floor
|
|
355
|
+
return unless cross_check(x, iy, 1, 0, range, noise_h, false)
|
|
356
|
+
|
|
357
|
+
cx = @centre
|
|
358
|
+
spread_h = @spread
|
|
359
|
+
ix = cx.floor
|
|
360
|
+
return unless cross_check(ix, iy, 0, 1, range, noise_v, false)
|
|
361
|
+
|
|
362
|
+
cy = @centre
|
|
363
|
+
spread_v = @spread
|
|
364
|
+
iy = cy.floor
|
|
365
|
+
noise_d = noise_h && noise_v
|
|
366
|
+
return unless cross_check(ix, iy, 1, 1, 2 * range, noise_d, true)
|
|
367
|
+
|
|
368
|
+
spread_d1 = @diagonal
|
|
369
|
+
return unless cross_check(ix, iy, 1, -1, 2 * range, noise_d, true)
|
|
370
|
+
|
|
371
|
+
spread_d2 = @diagonal
|
|
372
|
+
v = spread_v.to_f
|
|
373
|
+
hz = spread_h.to_f
|
|
374
|
+
min = v
|
|
375
|
+
min = hz if hz < min
|
|
376
|
+
min = spread_d1 if spread_d1 < min
|
|
377
|
+
min = spread_d2 if spread_d2 < min
|
|
378
|
+
max = v
|
|
379
|
+
max = hz if hz > max
|
|
380
|
+
max = spread_d1 if spread_d1 > max
|
|
381
|
+
max = spread_d2 if spread_d2 > max
|
|
382
|
+
return if max > 3 * min
|
|
383
|
+
|
|
384
|
+
size = square_size((spread_v + spread_h) * 0.5, (spread_d1 + spread_d2) * 0.5)
|
|
385
|
+
module_size = size / 7.0
|
|
386
|
+
if stone_centroid(ix, iy, module_size)
|
|
387
|
+
cx = @centre
|
|
388
|
+
cy = @diagonal
|
|
389
|
+
end
|
|
390
|
+
add(cx, cy, module_size, min / max, 1)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Side length of a square whose chords through the centre measure +h+
|
|
394
|
+
# along the axes and +g+ along the diagonals.
|
|
395
|
+
def square_size(h, g)
|
|
396
|
+
a = 1.0 / h
|
|
397
|
+
b = SQRT2 / g - a
|
|
398
|
+
1.0 / Math.sqrt(a * a + b * b)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Cross-check through dark pixel (x, y) along (dx, dy) and back. For
|
|
402
|
+
# axis directions sets @centre (centre of the middle run along the
|
|
403
|
+
# line's main axis, continuous coordinates) and @spread (the pattern's
|
|
404
|
+
# extent in pixels); for diagonals @diagonal (the extent, scaled to
|
|
405
|
+
# pixels along the axes). False if no 1:1:3:1:1 pattern. With +noise+
|
|
406
|
+
# (single pixels of the other colour inside runs ignored), axis
|
|
407
|
+
# directions also take the runs as they are if that fails: a light ring
|
|
408
|
+
# blurred or inked down to one pixel is no noise. (Not the diagonals:
|
|
409
|
+
# there one pixel steps across module corners are structure, and
|
|
410
|
+
# without bridging, look-alikes in the data area pass.)
|
|
411
|
+
def cross_check(x, y, dx, dy, range, noise, relaxed)
|
|
412
|
+
return true if cross_check_runs(x, y, dx, dy, range, noise, relaxed)
|
|
413
|
+
|
|
414
|
+
noise && (dx == 0 || dy == 0) && cross_check_runs(x, y, dx, dy, range, false, relaxed)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# cross_check with or without noise bridging.
|
|
418
|
+
def cross_check_runs(x, y, dx, dy, range, noise, relaxed)
|
|
419
|
+
return false unless walk(x, y, dx, dy, range, noise)
|
|
420
|
+
|
|
421
|
+
f0 = @run0
|
|
422
|
+
f1 = @run1
|
|
423
|
+
f2 = @run2
|
|
424
|
+
return false unless walk(x, y, -dx, -dy, range, noise)
|
|
425
|
+
|
|
426
|
+
centre = f0 + @run0 - 1
|
|
427
|
+
return false unless finder_ratio?(@run2, @run1, centre, f1, f2, relaxed)
|
|
428
|
+
|
|
429
|
+
spread = @run2 + @run1 + centre + f1 + f2
|
|
430
|
+
if dx != 0 && dy != 0
|
|
431
|
+
@diagonal = spread * SQRT2
|
|
432
|
+
else
|
|
433
|
+
start = dx == 0 ? y - @run0 + 1 : x - @run0 + 1
|
|
434
|
+
@centre = start + centre * 0.5
|
|
435
|
+
@spread = spread
|
|
436
|
+
end
|
|
437
|
+
true
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# 1:1:3:1:1 test: each run within 0.5 module (0.75 if relaxed) plus
|
|
441
|
+
# half a pixel of its nominal width, the centre run 0.25 module more.
|
|
442
|
+
def finder_ratio?(a, b, c, d, e, relaxed)
|
|
443
|
+
total = a + b + c + d + e
|
|
444
|
+
return false if total < 7
|
|
445
|
+
|
|
446
|
+
# In units of 1/28 module.
|
|
447
|
+
tol = (relaxed ? 3 * total : 2 * total) + 14
|
|
448
|
+
return true if (28 * a - 4 * total).abs <= tol &&
|
|
449
|
+
(28 * b - 4 * total).abs <= tol &&
|
|
450
|
+
(28 * c - 12 * total).abs <= tol + total &&
|
|
451
|
+
(28 * d - 4 * total).abs <= tol &&
|
|
452
|
+
(28 * e - 4 * total).abs <= tol
|
|
453
|
+
|
|
454
|
+
gain_ratio?(a, b, c, d, e)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# 1:1:3:1:1 test under dot gain (ink spread, thresholded faxes) or
|
|
458
|
+
# loss, which widen every dark run and narrow every light run by about
|
|
459
|
+
# the same amount: dark runs are compared with the dark module size
|
|
460
|
+
# (each within 0.75 of it + 0.5 px), light runs with the light one
|
|
461
|
+
# (within a third + 0.5 px), and the two module sizes may differ by up
|
|
462
|
+
# to 4x - zxing-cpp's edge-to-edge pattern test.
|
|
463
|
+
def gain_ratio?(a, b, c, d, e)
|
|
464
|
+
dark = a + c + e
|
|
465
|
+
light = b + d
|
|
466
|
+
return false if dark < 10 || light == 0 || dark > 10 * light || 5 * light > 8 * dark
|
|
467
|
+
|
|
468
|
+
# Dark module dark / 5, light module light / 2.
|
|
469
|
+
tol = 2 * dark + 10
|
|
470
|
+
(20 * a - 4 * dark).abs <= tol && (20 * c - 12 * dark).abs <= tol && (20 * e - 4 * dark).abs <= tol &&
|
|
471
|
+
(6 * b - 3 * light).abs <= light + 3 && (6 * d - 3 * light).abs <= light + 3
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Lengths of the first three runs (dark, light, dark) met walking from
|
|
475
|
+
# dark pixel (x, y) in direction (dx, dy) into @run0..@run2. The last
|
|
476
|
+
# run may end at the image border; false if the walk leaves the image
|
|
477
|
+
# earlier or exceeds +range+ pixels. With +noise+, a single pixel of
|
|
478
|
+
# the other colour inside a run (followed by two more of the run's
|
|
479
|
+
# colour) does not end the run.
|
|
480
|
+
def walk(x, y, dx, dy, range, noise)
|
|
481
|
+
bits = @bits
|
|
482
|
+
w = @width
|
|
483
|
+
# Steps before the walk leaves the image or exceeds the range.
|
|
484
|
+
room = range + 1
|
|
485
|
+
if dx > 0
|
|
486
|
+
room = w - x if w - x < room
|
|
487
|
+
elsif dx < 0
|
|
488
|
+
room = x + 1 if x + 1 < room
|
|
489
|
+
end
|
|
490
|
+
if dy > 0
|
|
491
|
+
room = @height - y if @height - y < room
|
|
492
|
+
elsif dy < 0
|
|
493
|
+
room = y + 1 if y + 1 < room
|
|
494
|
+
end
|
|
495
|
+
border = room <= range
|
|
496
|
+
step = dy * w + dx
|
|
497
|
+
idx = y * w + x
|
|
498
|
+
r0 = 0
|
|
499
|
+
r1 = 0
|
|
500
|
+
run = 0
|
|
501
|
+
k = 0
|
|
502
|
+
n = 0
|
|
503
|
+
colour = 1
|
|
504
|
+
while n < room
|
|
505
|
+
c = bits.getbyte(idx)
|
|
506
|
+
if c != colour &&
|
|
507
|
+
!(noise && run > 0 && n + 2 < room && bits.getbyte(idx + step) == colour &&
|
|
508
|
+
bits.getbyte(idx + 2 * step) == colour)
|
|
509
|
+
if k == 0
|
|
510
|
+
r0 = run
|
|
511
|
+
elsif k == 1
|
|
512
|
+
r1 = run
|
|
513
|
+
else
|
|
514
|
+
@run0 = r0
|
|
515
|
+
@run1 = r1
|
|
516
|
+
@run2 = run
|
|
517
|
+
return true
|
|
518
|
+
end
|
|
519
|
+
k += 1
|
|
520
|
+
colour = c
|
|
521
|
+
run = 0
|
|
522
|
+
end
|
|
523
|
+
run += 1
|
|
524
|
+
idx += step
|
|
525
|
+
n += 1
|
|
526
|
+
end
|
|
527
|
+
return false unless border && k == 2
|
|
528
|
+
|
|
529
|
+
@run0 = r0
|
|
530
|
+
@run1 = r1
|
|
531
|
+
@run2 = run
|
|
532
|
+
true
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Centroid of the dark 3x3 centre square containing (x, y), gathered
|
|
536
|
+
# from its horizontal runs, into @centre (x) and @diagonal (y). False
|
|
537
|
+
# if the stone is not cleanly separated from the surrounding ring.
|
|
538
|
+
def stone_centroid(x, y, module_size)
|
|
539
|
+
max_run = (module_size * 5).ceil
|
|
540
|
+
max_rows = (module_size * 2.6).ceil + 1
|
|
541
|
+
@area = 0
|
|
542
|
+
@sum_x = 0
|
|
543
|
+
@sum_y = 0
|
|
544
|
+
return false unless stone_half(x, y, -1, max_run, max_rows)
|
|
545
|
+
return false unless stone_half(x, y + 1, 1, max_run, max_rows)
|
|
546
|
+
|
|
547
|
+
area = @area
|
|
548
|
+
nominal = 9 * module_size * module_size
|
|
549
|
+
return false if area < nominal * 0.3 || area > nominal * 2.5
|
|
550
|
+
|
|
551
|
+
@centre = @sum_x * 0.5 / area
|
|
552
|
+
@diagonal = @sum_y * 0.5 / area
|
|
553
|
+
true
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Adds the dark runs of rows yy, yy + dir, ... that continue the stone
|
|
557
|
+
# (each starting below/above the middle of the previous one) to @area,
|
|
558
|
+
# @sum_x, @sum_y. False if a run is too long or there are too many rows.
|
|
559
|
+
def stone_half(xx, yy, dir, max_run, max_rows)
|
|
560
|
+
bits = @bits
|
|
561
|
+
w = @width
|
|
562
|
+
rows = 0
|
|
563
|
+
while yy >= 0 && yy < @height
|
|
564
|
+
off = yy * w
|
|
565
|
+
break unless bits.getbyte(off + xx) == 1
|
|
566
|
+
|
|
567
|
+
left = off + xx
|
|
568
|
+
left -= 1 while left > off && bits.getbyte(left - 1) == 1
|
|
569
|
+
right = off + xx
|
|
570
|
+
stop = off + w
|
|
571
|
+
right += 1 while right < stop && bits.getbyte(right) == 1
|
|
572
|
+
len = right - left
|
|
573
|
+
return false if len > max_run
|
|
574
|
+
|
|
575
|
+
@area += len
|
|
576
|
+
@sum_x += (left + right - 2 * off) * len
|
|
577
|
+
@sum_y += (2 * yy + 1) * len
|
|
578
|
+
xx = ((left + right) >> 1) - off
|
|
579
|
+
rows += 1
|
|
580
|
+
return false if rows > max_rows
|
|
581
|
+
|
|
582
|
+
yy += dir
|
|
583
|
+
end
|
|
584
|
+
true
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Image kernels. Kernel files are plain Ruby in the subset that Spinel
|
|
4
|
+
# compiles (see DESIGN.md, "Kernels"): the optional qr_reader-native gem
|
|
5
|
+
# builds them into a C extension, otherwise they run as they are.
|
|
6
|
+
module QrReaderKernels
|
|
7
|
+
# Box-filter downscale of a width x height grayscale buffer by an integer
|
|
8
|
+
# factor (partial edge blocks are dropped). Returns the reduced buffer.
|
|
9
|
+
# spinel-sample: QrReaderKernels.downscale("\x00\x01\x02\x03".b, 2, 2, 2)
|
|
10
|
+
def self.downscale(gray, width, height, factor)
|
|
11
|
+
w = width / factor
|
|
12
|
+
h = height / factor
|
|
13
|
+
out = String.new(capacity: w * h, encoding: Encoding::BINARY)
|
|
14
|
+
step = factor * width
|
|
15
|
+
off = 0
|
|
16
|
+
y = 0
|
|
17
|
+
while y < h
|
|
18
|
+
out << (factor == 2 ? downscale2_row(gray, off, width, w) : downscale_row(gray, off, width, w, factor))
|
|
19
|
+
off += step
|
|
20
|
+
y += 1
|
|
21
|
+
end
|
|
22
|
+
out
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# One 2x-reduced row whose top source row starts at +off+.
|
|
26
|
+
def self.downscale2_row(gray, off, width, w)
|
|
27
|
+
row = String.new(capacity: w, encoding: Encoding::BINARY)
|
|
28
|
+
a = off
|
|
29
|
+
b = off + width
|
|
30
|
+
e = off + 2 * w
|
|
31
|
+
while a < e
|
|
32
|
+
row << ((gray.getbyte(a) + gray.getbyte(a + 1) + gray.getbyte(b) + gray.getbyte(b + 1) + 2) >> 2)
|
|
33
|
+
a += 2
|
|
34
|
+
b += 2
|
|
35
|
+
end
|
|
36
|
+
row
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# One row reduced by +factor+ whose top source row starts at +off+.
|
|
40
|
+
def self.downscale_row(gray, off, width, w, factor)
|
|
41
|
+
row = String.new(capacity: w, encoding: Encoding::BINARY)
|
|
42
|
+
area = factor * factor
|
|
43
|
+
half = area / 2
|
|
44
|
+
x = 0
|
|
45
|
+
start = off
|
|
46
|
+
while x < w
|
|
47
|
+
s = 0
|
|
48
|
+
r = start
|
|
49
|
+
r_end = start + factor * width
|
|
50
|
+
while r < r_end
|
|
51
|
+
i = r
|
|
52
|
+
e = r + factor
|
|
53
|
+
while i < e
|
|
54
|
+
s += gray.getbyte(i)
|
|
55
|
+
i += 1
|
|
56
|
+
end
|
|
57
|
+
r += width
|
|
58
|
+
end
|
|
59
|
+
row << (s + half) / area
|
|
60
|
+
start += factor
|
|
61
|
+
x += 1
|
|
62
|
+
end
|
|
63
|
+
row
|
|
64
|
+
end
|
|
65
|
+
end
|