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,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