qrio 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +5 -0
- data/.rvmrc.example +1 -0
- data/Gemfile +3 -0
- data/README.md +66 -0
- data/Rakefile +8 -0
- data/examples/extract_qr +18 -0
- data/lib/qrio.rb +15 -0
- data/lib/qrio/finder_pattern_slice.rb +136 -0
- data/lib/qrio/horizontal_match.rb +31 -0
- data/lib/qrio/image_dumper.rb +97 -0
- data/lib/qrio/image_loader/png_image_loader.rb +18 -0
- data/lib/qrio/matrix.rb +72 -0
- data/lib/qrio/neighbor.rb +40 -0
- data/lib/qrio/qr.rb +207 -0
- data/lib/qrio/qr_matrix.rb +403 -0
- data/lib/qrio/region.rb +113 -0
- data/lib/qrio/sampling_grid.rb +152 -0
- data/lib/qrio/version.rb +3 -0
- data/lib/qrio/vertical_match.rb +31 -0
- data/qrio.gemspec +23 -0
- data/test/finder_pattern_slice_test.rb +121 -0
- data/test/fixtures/block_test.qr +25 -0
- data/test/fixtures/finder_pattern1.png +0 -0
- data/test/fixtures/finder_pattern2.png +0 -0
- data/test/fixtures/finder_pattern3.png +0 -0
- data/test/fixtures/finder_pattern4.png +0 -0
- data/test/fixtures/masked0.qr +25 -0
- data/test/fixtures/no_finder_pattern1.png +0 -0
- data/test/fixtures/qrio-codewords.txt +100 -0
- data/test/fixtures/qrio.qr +33 -0
- data/test/horizontal_match_test.rb +34 -0
- data/test/matrix_test.rb +66 -0
- data/test/qr_matrix_test.rb +164 -0
- data/test/qr_test.rb +107 -0
- data/test/region_test.rb +87 -0
- data/test/sampling_grid_test.rb +78 -0
- metadata +111 -0
data/lib/qrio/region.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
module Qrio
|
2
|
+
# a rectangular matrix of bits
|
3
|
+
class Region
|
4
|
+
attr_reader :x1, :y1, :x2, :y2, :orientation
|
5
|
+
|
6
|
+
def initialize(x1, y1, x2, y2)
|
7
|
+
@x1 = x1
|
8
|
+
@y1 = y1
|
9
|
+
@x2 = x2
|
10
|
+
@y2 = y2
|
11
|
+
|
12
|
+
set_orientation
|
13
|
+
end
|
14
|
+
|
15
|
+
def left; x1; end
|
16
|
+
def right; x2; end
|
17
|
+
def top; y1; end
|
18
|
+
def bottom; y2; end
|
19
|
+
|
20
|
+
def top_left
|
21
|
+
[x1, y1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def bottom_right
|
25
|
+
[x2, y2]
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_coordinates
|
29
|
+
[top_left, bottom_right].flatten
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_point_size
|
33
|
+
[top_left, width, height].flatten
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"R[#{ to_coordinates.join(',') }]"
|
38
|
+
end
|
39
|
+
|
40
|
+
def hash
|
41
|
+
to_s.hash
|
42
|
+
end
|
43
|
+
|
44
|
+
def eql?(other)
|
45
|
+
self == other
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
to_s == other.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
def width
|
53
|
+
x2 - x1 + 1
|
54
|
+
end
|
55
|
+
|
56
|
+
def height
|
57
|
+
y2 - y1 + 1
|
58
|
+
end
|
59
|
+
|
60
|
+
def center
|
61
|
+
[left + width / 2, top + height / 2]
|
62
|
+
end
|
63
|
+
|
64
|
+
def horizontal?; orientation == :horizontal; end
|
65
|
+
def vertical?; orientation == :vertical; end
|
66
|
+
|
67
|
+
def orientation_matches?(other)
|
68
|
+
orientation == other.orientation
|
69
|
+
end
|
70
|
+
|
71
|
+
def union(other)
|
72
|
+
self.class.new(
|
73
|
+
[left, other.left].min,
|
74
|
+
[top, other.top].min,
|
75
|
+
[right, other.right].max,
|
76
|
+
[bottom, other.bottom].max
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def translate(xoffset, yoffset)
|
81
|
+
self.class.new(
|
82
|
+
left - xoffset,
|
83
|
+
top - yoffset,
|
84
|
+
right - xoffset,
|
85
|
+
bottom - yoffset
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
# return a new region that would be the result of rotating
|
90
|
+
# a matrix of width x height containing this region
|
91
|
+
def rotate(mw, mh)
|
92
|
+
self.class.new(
|
93
|
+
mh - bottom - 1,
|
94
|
+
left,
|
95
|
+
mh - top - 1,
|
96
|
+
right
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def set_orientation
|
103
|
+
@orientation = case
|
104
|
+
when width > height
|
105
|
+
:horizontal
|
106
|
+
when height > width
|
107
|
+
:vertical
|
108
|
+
else
|
109
|
+
:square
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module Qrio
|
2
|
+
class SamplingGrid
|
3
|
+
attr_reader :origin_corner, :top_right, :bottom_left,
|
4
|
+
:orientation, :bounds, :angles,
|
5
|
+
:block_width, :block_height, :provisional_version,
|
6
|
+
:finder_patterns, :matrix
|
7
|
+
|
8
|
+
def initialize(matrix, finder_patterns)
|
9
|
+
@matrix = matrix
|
10
|
+
@finder_patterns = finder_patterns
|
11
|
+
@angles = []
|
12
|
+
|
13
|
+
find_origin_corner
|
14
|
+
detect_orientation
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_origin_corner(normalized=false)
|
18
|
+
build_finder_pattern_neighbors
|
19
|
+
|
20
|
+
shared_corners = @finder_patterns.select do |fp|
|
21
|
+
fp.neighbors.select(&:right_angle?).count > 1
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO : handle multiple possible matches
|
25
|
+
if @origin_corner = shared_corners.first
|
26
|
+
if normalized
|
27
|
+
# we have correct orientation, identify fp positions
|
28
|
+
@top_right = non_origin_finder_patterns.map(&:destination).detect{|fp| fp.center.last < @matrix.height / 2.0 }
|
29
|
+
@bottom_left = non_origin_finder_patterns.map(&:destination).detect{|fp| fp.center.first < @matrix.width / 2.0 }
|
30
|
+
else
|
31
|
+
set_bounds
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_bounds
|
37
|
+
@bounds = @origin_corner.dup
|
38
|
+
@bounds.neighbors.select(&:right_angle?).each do |n|
|
39
|
+
@bounds = @bounds.union(n.destination)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# which way is the QR rotated?
|
44
|
+
# 0) normal - shared finder patterns in top left
|
45
|
+
# 1) - shared finder patterns in top right
|
46
|
+
# 2) - shared finder patterns in bottom right
|
47
|
+
# 3) - shared finder patterns in bottom left
|
48
|
+
def detect_orientation
|
49
|
+
# TODO : handle multiple possible matches
|
50
|
+
other_corners = non_origin_finder_patterns
|
51
|
+
|
52
|
+
dc = other_corners.map(&:distance).inject(0){|s,d| s + d } / 2.0
|
53
|
+
threshold = dc / 2.0
|
54
|
+
|
55
|
+
other_corners = other_corners.map(&:destination)
|
56
|
+
|
57
|
+
set_block_dimensions(@origin_corner, *other_corners)
|
58
|
+
@provisional_version = ((dc / @block_width).round - 10) / 4
|
59
|
+
|
60
|
+
xs = other_corners.map{|fp| fp.center.first }
|
61
|
+
ys = other_corners.map{|fp| fp.center.last }
|
62
|
+
|
63
|
+
above = ys.select{|y| y < (@origin_corner.center.last - threshold) }
|
64
|
+
left = xs.select{|x| x < (@origin_corner.center.first - threshold) }
|
65
|
+
|
66
|
+
@orientation = if above.any?
|
67
|
+
left.any? ? 2 : 3
|
68
|
+
else
|
69
|
+
left.any? ? 1 : 0
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def non_origin_finder_patterns
|
74
|
+
@origin_corner.neighbors.select(&:right_angle?)[0,2]
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_finder_pattern_neighbors
|
78
|
+
@finder_patterns.each do |source|
|
79
|
+
@finder_patterns.each do |destination|
|
80
|
+
next if source.center == destination.center
|
81
|
+
@angles << Neighbor.new(source, destination)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_block_dimensions(*finder_patterns)
|
87
|
+
@block_width = finder_patterns.inject(0){|s,f| s + f.width } / 21.0
|
88
|
+
@block_height = finder_patterns.inject(0){|s,f| s + f.height } / 21.0
|
89
|
+
end
|
90
|
+
|
91
|
+
def logical_width
|
92
|
+
@matrix.width / @block_width
|
93
|
+
end
|
94
|
+
|
95
|
+
def logical_height
|
96
|
+
@matrix.height / @block_height
|
97
|
+
end
|
98
|
+
|
99
|
+
def normalize
|
100
|
+
@matrix = @matrix.extract(*@bounds.to_point_size)
|
101
|
+
translate(*@bounds.top_left)
|
102
|
+
|
103
|
+
if @orientation > 0
|
104
|
+
(4 - @orientation).times do
|
105
|
+
rotate
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
find_origin_corner(true)
|
110
|
+
end
|
111
|
+
|
112
|
+
def translate(x, y)
|
113
|
+
@angles = []
|
114
|
+
other_corners = non_origin_finder_patterns.map(&:destination)
|
115
|
+
|
116
|
+
@origin_corner = @origin_corner.translate(x, y)
|
117
|
+
translated = [@origin_corner]
|
118
|
+
translated += other_corners.map{|c| c.translate(x, y) }
|
119
|
+
|
120
|
+
@finder_patterns = translated
|
121
|
+
end
|
122
|
+
|
123
|
+
def rotate
|
124
|
+
@matrix = @matrix.rotate
|
125
|
+
@finder_patterns.map!{|f| f.rotate(@bounds.width, @bounds.height) }
|
126
|
+
end
|
127
|
+
|
128
|
+
def extracted_pixels
|
129
|
+
start_x = @origin_corner.center.first - (@block_width * 3.0)
|
130
|
+
start_y = @origin_corner.center.last - (@block_height * 3.0)
|
131
|
+
|
132
|
+
dest_x = @bottom_left.center.first - (@block_width * 3.0)
|
133
|
+
dest_y = @top_right.center.last - (@block_height * 3.0)
|
134
|
+
|
135
|
+
# TODO : take bottom right alignment pattern into consideration
|
136
|
+
dy = (dest_y - start_y) / logical_width
|
137
|
+
dx = (dest_x - start_x) / logical_height
|
138
|
+
|
139
|
+
logical_height.round.times do |row_index|
|
140
|
+
row_start_x = start_x + row_index * dx
|
141
|
+
row_start_y = start_y + (row_index * @block_height)
|
142
|
+
|
143
|
+
logical_width.round.times do |col_index|
|
144
|
+
x = row_start_x + (col_index * @block_width)
|
145
|
+
y = row_start_y + (col_index * dy)
|
146
|
+
|
147
|
+
yield x.round, y.round
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
data/lib/qrio/version.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Qrio
|
2
|
+
class VerticalMatch < FinderPatternSlice
|
3
|
+
def self.build(offset, origin, terminus)
|
4
|
+
new(offset, origin, offset, terminus)
|
5
|
+
end
|
6
|
+
|
7
|
+
def offset; left; end
|
8
|
+
def origin; top; end
|
9
|
+
def terminus; bottom; end
|
10
|
+
|
11
|
+
def length
|
12
|
+
height
|
13
|
+
end
|
14
|
+
|
15
|
+
def breadth
|
16
|
+
width
|
17
|
+
end
|
18
|
+
|
19
|
+
def left_of?(other)
|
20
|
+
right < other.left
|
21
|
+
end
|
22
|
+
|
23
|
+
def right_of?(other)
|
24
|
+
left > other.right
|
25
|
+
end
|
26
|
+
|
27
|
+
def offset_diff(other)
|
28
|
+
left_of?(other) ? other.left - right : left - other.right
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/qrio.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "qrio/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "qrio"
|
7
|
+
s.version = Qrio::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Solomon White"]
|
10
|
+
s.email = ["rubysolo@gmail.com"]
|
11
|
+
s.homepage = "http://github.com/rubysolo/qrio"
|
12
|
+
s.licenses = %w(MIT)
|
13
|
+
s.summary = %q(QR code decoder)
|
14
|
+
s.description = %q(QR code decoder in pure Ruby)
|
15
|
+
|
16
|
+
s.add_runtime_dependency "chunky_png"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require_relative '../lib/qrio'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class TestFinderPatternSlice < Test::Unit::TestCase
|
5
|
+
def test_ratio_matching
|
6
|
+
[
|
7
|
+
[ 1, 1, 3, 1, 1],
|
8
|
+
[10,10,30,10,10],
|
9
|
+
[11, 9,28,10,12],
|
10
|
+
[ 8, 8,19, 9, 8],
|
11
|
+
[11,12,34,13,12],
|
12
|
+
[12,20,46,19,14],
|
13
|
+
[10,13,32,14, 8],
|
14
|
+
[10,12,34,13, 7],
|
15
|
+
[11,13,32,14, 8],
|
16
|
+
].each{|b| assert_matches_ratio b }
|
17
|
+
|
18
|
+
(9..12).to_a.each do |a|
|
19
|
+
(11..13).to_a.each do |b|
|
20
|
+
(31..34).to_a.each do |c|
|
21
|
+
(13..15).to_a.each do |d|
|
22
|
+
(7..9).to_a.each do |e|
|
23
|
+
assert_matches_ratio [a, b, c, d, e]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_intersection_detection
|
32
|
+
slice1 = Qrio::HorizontalMatch.build(27, 16, 62)
|
33
|
+
slice1 = slice1.union Qrio::HorizontalMatch.build(45, 16, 62)
|
34
|
+
|
35
|
+
slice2 = Qrio::VerticalMatch.build(27, 16, 62)
|
36
|
+
slice2 = slice2.union Qrio::VerticalMatch.build(44, 16, 62)
|
37
|
+
|
38
|
+
assert slice1.length_matches?(slice2)
|
39
|
+
assert slice1.breadth_matches?(slice2)
|
40
|
+
assert slice1.intersects? slice2
|
41
|
+
|
42
|
+
slice1 = Qrio::HorizontalMatch.build(21, 5, 57)
|
43
|
+
slice1 = slice1.union Qrio::HorizontalMatch.build(35, 5, 57)
|
44
|
+
|
45
|
+
slice2 = Qrio::VerticalMatch.build(22, 3, 54)
|
46
|
+
slice2 = slice2.union Qrio::VerticalMatch.build(38, 3, 54)
|
47
|
+
|
48
|
+
assert slice1.intersects? slice2
|
49
|
+
end
|
50
|
+
|
51
|
+
=begin
|
52
|
+
def test_slice_ratio
|
53
|
+
correct = [
|
54
|
+
[16, 27, 62, 45],
|
55
|
+
[27, 16, 44, 62],
|
56
|
+
[10, 27, 62, 45],
|
57
|
+
[27, 10, 44, 62],
|
58
|
+
[ 5, 21, 57, 35]
|
59
|
+
].map{|a| @s.new(*a) }
|
60
|
+
|
61
|
+
correct.each do |slice|
|
62
|
+
assert slice.has_correct_ratio?, slice.ratio
|
63
|
+
end
|
64
|
+
|
65
|
+
incorrect = [
|
66
|
+
[16, 27, 62, 28],
|
67
|
+
[27, 16, 30, 62],
|
68
|
+
].map{|a| @s.new(*a) }
|
69
|
+
|
70
|
+
incorrect.each do |slice|
|
71
|
+
assert ! slice.has_correct_ratio?, slice.ratio
|
72
|
+
end
|
73
|
+
end
|
74
|
+
=end
|
75
|
+
|
76
|
+
def test_slice_builder
|
77
|
+
slice = Qrio::FinderPatternSlice.build_matching(
|
78
|
+
10, 5,
|
79
|
+
[1, 1, 3, 1, 1],
|
80
|
+
:horizontal
|
81
|
+
)
|
82
|
+
|
83
|
+
assert slice.is_a?(Qrio::HorizontalMatch)
|
84
|
+
assert_equal 10, slice.offset
|
85
|
+
assert_equal 5, slice.origin
|
86
|
+
assert_equal 11, slice.terminus
|
87
|
+
assert_equal 5, slice.left
|
88
|
+
assert_equal 11, slice.right
|
89
|
+
assert_equal 10, slice.top
|
90
|
+
assert_equal 10, slice.bottom
|
91
|
+
assert_equal 1, slice.height
|
92
|
+
assert_equal 7, slice.width
|
93
|
+
|
94
|
+
slice = Qrio::FinderPatternSlice.build_matching(
|
95
|
+
23, 15,
|
96
|
+
[1, 1, 3, 1, 1],
|
97
|
+
:vertical
|
98
|
+
)
|
99
|
+
|
100
|
+
assert slice.is_a?(Qrio::VerticalMatch)
|
101
|
+
assert_equal 23, slice.offset
|
102
|
+
assert_equal 15, slice.origin
|
103
|
+
assert_equal 21, slice.terminus
|
104
|
+
assert_equal 23, slice.left
|
105
|
+
assert_equal 23, slice.right
|
106
|
+
assert_equal 15, slice.top
|
107
|
+
assert_equal 21, slice.bottom
|
108
|
+
assert_equal 7, slice.height
|
109
|
+
assert_equal 1, slice.width
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def assert_matches_ratio(widths)
|
115
|
+
norm = Qrio::FinderPatternSlice.normalized_ratio(widths)
|
116
|
+
norm = norm.map{|n| '%.2f' % n }
|
117
|
+
msg = "Expected #{ widths.join('|') } [#{ norm.join('|') }]"
|
118
|
+
msg << " to match finder pattern ratio"
|
119
|
+
assert Qrio::FinderPatternSlice.matches_ratio?(widths), msg
|
120
|
+
end
|
121
|
+
end
|