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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +14 -0
  5. data/README.md +222 -0
  6. data/exe/qr_reader +66 -0
  7. data/lib/qr_reader/binarizer.rb +170 -0
  8. data/lib/qr_reader/bit_matrix.rb +120 -0
  9. data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
  10. data/lib/qr_reader/decoder/character_set.rb +163 -0
  11. data/lib/qr_reader/decoder/codewords.rb +164 -0
  12. data/lib/qr_reader/decoder/data_mask.rb +57 -0
  13. data/lib/qr_reader/decoder/format_reader.rb +204 -0
  14. data/lib/qr_reader/decoder/function_patterns.rb +62 -0
  15. data/lib/qr_reader/decoder/galois_field.rb +69 -0
  16. data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
  17. data/lib/qr_reader/decoder.rb +241 -0
  18. data/lib/qr_reader/detector/alignment_grid.rb +127 -0
  19. data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
  20. data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
  21. data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
  22. data/lib/qr_reader/detector/candidate.rb +436 -0
  23. data/lib/qr_reader/detector/finder_pattern.rb +63 -0
  24. data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
  25. data/lib/qr_reader/detector/geometry.rb +118 -0
  26. data/lib/qr_reader/detector/grid_sampler.rb +350 -0
  27. data/lib/qr_reader/detector/pattern_sets.rb +235 -0
  28. data/lib/qr_reader/detector/perspective_transform.rb +117 -0
  29. data/lib/qr_reader/detector/pure_detector.rb +197 -0
  30. data/lib/qr_reader/detector.rb +215 -0
  31. data/lib/qr_reader/errors.rb +14 -0
  32. data/lib/qr_reader/format_information.rb +79 -0
  33. data/lib/qr_reader/formats/bmp.rb +434 -0
  34. data/lib/qr_reader/formats/ccitt.rb +81 -0
  35. data/lib/qr_reader/formats/gif.rb +272 -0
  36. data/lib/qr_reader/formats/jpeg.rb +152 -0
  37. data/lib/qr_reader/formats/pdf.rb +2935 -0
  38. data/lib/qr_reader/formats/png.rb +311 -0
  39. data/lib/qr_reader/formats/pnm.rb +286 -0
  40. data/lib/qr_reader/formats/tiff.rb +1345 -0
  41. data/lib/qr_reader/formats.rb +279 -0
  42. data/lib/qr_reader/image.rb +231 -0
  43. data/lib/qr_reader/kernels/binarizer.rb +475 -0
  44. data/lib/qr_reader/kernels/ccitt.rb +554 -0
  45. data/lib/qr_reader/kernels/detector.rb +587 -0
  46. data/lib/qr_reader/kernels/image.rb +65 -0
  47. data/lib/qr_reader/kernels/jpeg.rb +3421 -0
  48. data/lib/qr_reader/kernels/lzw.rb +173 -0
  49. data/lib/qr_reader/kernels/pixels.rb +116 -0
  50. data/lib/qr_reader/kernels/png.rb +1138 -0
  51. data/lib/qr_reader/kernels.rb +14 -0
  52. data/lib/qr_reader/native_kernels.rb +130 -0
  53. data/lib/qr_reader/reader.rb +348 -0
  54. data/lib/qr_reader/result.rb +49 -0
  55. data/lib/qr_reader/symbol_version.rb +173 -0
  56. data/lib/qr_reader/version.rb +5 -0
  57. data/lib/qr_reader.rb +77 -0
  58. 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