qrio 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fd4e00a1a67e02acd047f575462bc7998fdf534d
4
+ data.tar.gz: e2f71bd39d091c83d3266ed82817dd04bd509717
5
+ SHA512:
6
+ metadata.gz: 1ee2434d16b4c5aecc718adc92aa33720b8f50b56057b3613f2d0484329c7e981c293952a14f03e86d2a5d141e064b51722fd728d5faa98e46e9277edf795005
7
+ data.tar.gz: 674c0edb17097ede3282e38c6585ec12a3cd005da0d9f46104f5f8c7efd38cc4b629636b2f26a490892a192ba5b5a9a1ad961d4694e1b4a9035e9662e49ea4d2
File without changes
@@ -0,0 +1,5 @@
1
+ .bundle
2
+ .rvmrc
3
+ Gemfile.lock
4
+ vendor/bundle
5
+ tmp
@@ -0,0 +1 @@
1
+ rvm use 1.9.2@qrio --create
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,66 @@
1
+ # QRio
2
+
3
+ QRio is a QR code decoder for Ruby
4
+
5
+ ## Usage
6
+
7
+ QRio can extract QR contents in one step:
8
+
9
+ require 'qrio'
10
+ puts Qrio::Qr.load("some-image.png").qr.text
11
+
12
+ If you know / are curious about the decoding process, QRio can generate
13
+ an image illustrating the intermediate steps to decoding:
14
+
15
+ require 'qrio'
16
+ qr = Qrio::Qr.load("some-image.png")
17
+
18
+ qr.save_image(
19
+ "some-image-annotated.png",
20
+ :crop => true, # crop output image to detected QR bounds
21
+ :annotate => [
22
+ :finder_patterns, # outline detected finder patterns
23
+ :angles # draw lines connecting finder pattern centers
24
+ ]
25
+ )
26
+
27
+
28
+
29
+
30
+ ## Dependencies
31
+
32
+ * Ruby 1.9.2 (will be backported to 1.8.7)
33
+ * ChunkyPNG (tested with version 1.2.0)
34
+
35
+ ## STATUS
36
+
37
+ *NOTE* : QRio is not yet fully functional. If you'd like to help out, fork and
38
+ submit a tested pull request. :)
39
+
40
+ ### TODO
41
+
42
+ * support numeric / alphanumeric / kanji mode QR codes
43
+ * refine alignment pattern location and adjust module sampling grid
44
+ accordingly
45
+ * error correction support
46
+ * support more image formats (limited to PNG at the moment)
47
+ * native thresholding for input images
48
+ * support more QR versions
49
+ * speed improvements
50
+
51
+ ### Does *anything* work?
52
+
53
+ Yeah, it's coming along. Here's what should be working now:
54
+
55
+ * find and extract a QR code from an image. I've been cheating
56
+ somewhat, using image magick to threshold the image for me:
57
+
58
+ convert raw.jpg -colorspace Gray -lat 90x90-3% -median 1x1 cooked.png
59
+
60
+ * detect and correct orientation of extracted QR
61
+ * extract modules via a sampling grid
62
+ * extract raw bytes from data / error correction blocks
63
+ * de-interlace blocks into final bitstream
64
+ * extract text from bitstream (ascii mode only)
65
+
66
+
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new('test') do |t|
4
+ t.pattern = 'test/**/*_test.rb'
5
+ t.warning = true
6
+ end
7
+
8
+ task :default => :test
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/qrio'
3
+ $stdout.sync = true
4
+
5
+ filename = ARGV.shift || nil
6
+ if filename.nil?
7
+ puts "Usage #{ $0 } filename"
8
+ exit 1
9
+ end
10
+
11
+ qr = Qrio::Qr.load(filename)
12
+
13
+ qr.save_image(
14
+ 'debug.png',
15
+ :crop => true,
16
+ :annotate => [:matches, :finder_patterns, :angles,
17
+ :alignment_patterns, :extracted_pixels]
18
+ )
@@ -0,0 +1,15 @@
1
+ require 'chunky_png'
2
+
3
+ require_relative 'qrio/region'
4
+ require_relative 'qrio/finder_pattern_slice'
5
+ require_relative 'qrio/horizontal_match'
6
+ require_relative 'qrio/vertical_match'
7
+ require_relative 'qrio/neighbor'
8
+ require_relative 'qrio/matrix'
9
+ require_relative 'qrio/qr_matrix'
10
+ require_relative 'qrio/sampling_grid'
11
+
12
+ require_relative 'qrio/image_dumper'
13
+ require_relative 'qrio/image_loader/png_image_loader'
14
+
15
+ require_relative 'qrio/qr'
@@ -0,0 +1,136 @@
1
+ module Qrio
2
+ # QR codes have a finder pattern in three corners. any horizontal or
3
+ # vertical slice through the center square will be a band of:
4
+ # black, white, black, white, black, with widths matching ratio:
5
+ # 1, 1, 3, 1, 1. According to spec, the tolerance should be +/- 0.5.
6
+ class FinderPatternSlice < Region
7
+ ONE = 0.5..1.5
8
+ THREE = 2.1..3.9 # not to spec, but required for some "in-the-wild" QR
9
+
10
+ ENDPOINT_TOLERANCE = 0.05 # origin/terminus delta between adjacent slices
11
+ OFFSET_TOLERANCE = 0.25 # how many non-matching slices can be skipped?
12
+
13
+ LENGTH_TOLERANCE = 0.35 # allowed length delta bewteen 2 intersecting slices
14
+ WIDTH_TOLERANCE = 0.15 # allowed width delta bewteen 2 intersecting slices
15
+
16
+ attr_accessor :neighbors
17
+ attr_reader :offset, :origin, :terminus
18
+
19
+ def initialize(*args)
20
+ @neighbors = []
21
+ super
22
+ end
23
+
24
+ def to_s
25
+ "#{ class_name_prefix }[#{ to_coordinates.join(',') }]"
26
+ end
27
+
28
+ def class_name_prefix
29
+ self.class.to_s.gsub(/^.*::/, '')[0,1]
30
+ end
31
+
32
+ def aspect_ratio
33
+ breadth / length.to_f
34
+ end
35
+
36
+ # based on the 1, 1, 3, 1, 1 width ratio, a finder pattern has total
37
+ # width of 7. an ideal grouped slice would then have aspect ratio
38
+ # of 3/7, since slice breadth would be 3 (just the center square)
39
+ # and length would be 7 (entire slice)
40
+ def matches_aspect_ratio?
41
+ (0.25..0.59).include? aspect_ratio
42
+ end
43
+
44
+
45
+ class << self
46
+ # given a width buffer extracted from a given coordinate, test
47
+ # for ratio matching. if it matches, return a match object of
48
+ # the appropriate orientation
49
+ def build_matching(offset, origin, widths, direction)
50
+ return nil unless matches_ratio?(widths)
51
+
52
+ match_class = direction == :horizontal ? HorizontalMatch : VerticalMatch
53
+ terminus = origin + widths.inject(0){|s,w| s + w } - 1
54
+
55
+ match_class.build(offset, origin, terminus)
56
+ end
57
+
58
+ def matches_ratio?(widths)
59
+ norm = normalized_ratio(widths)
60
+
61
+ ONE.include?(norm[0]) &&
62
+ ONE.include?(norm[1]) &&
63
+ THREE.include?(norm[2]) &&
64
+ ONE.include?(norm[3]) &&
65
+ ONE.include?(norm[4])
66
+ end
67
+
68
+ def normalized_ratio(widths)
69
+ scale = (widths[0] + widths[1] + widths[3] + widths[4]) / 4.0
70
+ widths.map{|w| w / scale }
71
+ end
72
+ end
73
+
74
+ def <=>(other)
75
+ return -1 if offset < other.offset
76
+ return 1 if offset > other.offset
77
+ origin <=> other.origin
78
+ end
79
+
80
+ def intersects?(other)
81
+ ! orientation_matches?(other) &&
82
+ (other.origin..other.terminus).include?(offset) &&
83
+ (origin..terminus).include?(other.offset) &&
84
+ length_matches?(other) &&
85
+ breadth_matches?(other)
86
+ end
87
+
88
+ def adjacent?(other)
89
+ endpoints_match?(other) && offset_matches?(other)
90
+ end
91
+
92
+ def endpoints_match?(other)
93
+ origin_matches?(other) && terminus_matches?(other)
94
+ end
95
+
96
+ def origin_diff(other)
97
+ (origin - other.origin).abs / length.to_f
98
+ end
99
+
100
+ def origin_matches?(other)
101
+ origin_diff(other) <= ENDPOINT_TOLERANCE
102
+ end
103
+
104
+ def terminus_diff(other)
105
+ (terminus - other.terminus).abs / length.to_f
106
+ end
107
+
108
+ def terminus_matches?(other)
109
+ terminus_diff(other) <= ENDPOINT_TOLERANCE
110
+ end
111
+
112
+ def normalized_offset_diff(other)
113
+ offset_diff(other) / length.to_f
114
+ end
115
+
116
+ def offset_matches?(other)
117
+ normalized_offset_diff(other) <= OFFSET_TOLERANCE
118
+ end
119
+
120
+ def length_diff(other)
121
+ (length - other.length).abs / length.to_f
122
+ end
123
+
124
+ def length_matches?(other)
125
+ length_diff(other) <= LENGTH_TOLERANCE
126
+ end
127
+
128
+ def breadth_diff(other)
129
+ (breadth - other.breadth).abs / breadth.to_f
130
+ end
131
+
132
+ def breadth_matches?(other)
133
+ breadth_diff(other) <= WIDTH_TOLERANCE
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,31 @@
1
+ module Qrio
2
+ class HorizontalMatch < FinderPatternSlice
3
+ def self.build(offset, origin, terminus)
4
+ new(origin, offset, terminus, offset)
5
+ end
6
+
7
+ def offset; top; end
8
+ def origin; left; end
9
+ def terminus; right; end
10
+
11
+ def length
12
+ width
13
+ end
14
+
15
+ def breadth
16
+ height
17
+ end
18
+
19
+ def above?(other)
20
+ other.top > bottom
21
+ end
22
+
23
+ def below?(other)
24
+ other.bottom < top
25
+ end
26
+
27
+ def offset_diff(other)
28
+ above?(other) ? other.top - bottom : top - other.bottom
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,97 @@
1
+ module Qrio
2
+ # this module contains the code to dump images of QR decoding
3
+ # progress. useful for debugging or those curious about the
4
+ # detection / decoding algorithms
5
+ module ImageDumper
6
+ def save_image(filename, options={})
7
+ if matrix = @extracted_matrix
8
+ # extracted matrix is already cropped
9
+ options[:crop] = false
10
+ @features = {
11
+ :candidates => {},
12
+ :matches => @translated_matches,
13
+ :finder_patterns => @sampling_grid.finder_patterns
14
+ }
15
+ else
16
+ matrix = @input_matrix
17
+ @features = {
18
+ :candidates => @candidates,
19
+ :matches => @matches,
20
+ :finder_patterns => @finder_patterns
21
+ }
22
+ end
23
+
24
+ save_to_image(matrix, filename, options)
25
+ end
26
+
27
+ def save_to_image(matrix, filename, options={})
28
+ png = ChunkyPNG::Image.new(
29
+ matrix.width,
30
+ matrix.height,
31
+ ChunkyPNG::Color::WHITE
32
+ )
33
+
34
+ (0..(matrix.width - 1)).to_a.each do |x|
35
+ (0..(matrix.height - 1)).to_a.each do |y|
36
+ png[x, y] = ChunkyPNG::Color::BLACK if matrix[x, y]
37
+ end
38
+ end
39
+
40
+ if options[:annotate].include?(:candidates)
41
+ @features[:candidates][:horizontal].each do |hmatch|
42
+ png.rect(*hmatch.to_coordinates, color(:green))
43
+ end
44
+
45
+ @features[:candidates][:vertical].each do |vmatch|
46
+ png.rect(*vmatch.to_coordinates, color(:magenta))
47
+ end
48
+ end
49
+
50
+ if options[:annotate].include?(:matches)
51
+ @features[:matches][:horizontal].each do |hmatch|
52
+ png.rect(*hmatch.to_coordinates, color(:green))
53
+ end
54
+
55
+ @features[:matches][:vertical].each do |vmatch|
56
+ png.rect(*vmatch.to_coordinates, color(:magenta))
57
+ end
58
+ end
59
+
60
+ if options[:annotate].include?(:finder_patterns)
61
+ @features[:finder_patterns].each do |finder_pattern|
62
+ png.rect(*finder_pattern.to_coordinates, color(:red))
63
+ end
64
+ end
65
+
66
+ if options[:annotate].include?(:angles)
67
+ @sampling_grid.angles[0, 100].each do |angle|
68
+ png.line_xiaolin_wu(*angle.to_coordinates, color(:cyan))
69
+ end
70
+ end
71
+
72
+ if options[:annotate].include?(:alignment_patterns)
73
+ png.rect(*@alignment_pattern.to_coordinates, color(:magenta))
74
+ end
75
+
76
+ if options[:annotate].include?(:extracted_pixels)
77
+ @sampling_grid.extracted_pixels do |x, y|
78
+ png.circle(x, y, 1, color(:cyan))
79
+ end
80
+ end
81
+
82
+ png = png.crop(*@qr_bounds.to_point_size) if options[:crop]
83
+ png.save(filename, :fast_rgba)
84
+ end
85
+
86
+ def color(name)
87
+ rgb = {
88
+ :green => [ 0, 255, 0],
89
+ :red => [255, 0, 0],
90
+ :magenta => [227, 91, 216],
91
+ :cyan => [ 0, 255, 255],
92
+ }[name]
93
+
94
+ ChunkyPNG::Color.rgb(*rgb)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,18 @@
1
+ module Qrio
2
+ module ImageLoader
3
+ class PNGImageLoader
4
+ def self.load(filename)
5
+ image = ChunkyPNG::Image.from_file(filename)
6
+
7
+ bits = image.pixels.map do |pixel|
8
+ grayscale = ChunkyPNG::Color.to_grayscale(pixel)
9
+ level = ChunkyPNG::Color.r(grayscale)
10
+ level <= 126
11
+ end
12
+
13
+ Matrix.new(bits, image.width, image.height)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,72 @@
1
+ module Qrio
2
+ class Matrix
3
+ attr_reader :width, :height
4
+
5
+ def initialize(bits, width, height)
6
+ @bits = bits
7
+ @width = width
8
+ @height = height
9
+ end
10
+
11
+ def to_s
12
+ "<Matrix width:#{ width }, height: #{ height }>"
13
+ end
14
+
15
+ def [](x, y)
16
+ rows[y][x] rescue nil
17
+ end
18
+
19
+ def []=(x, y, value)
20
+ raise "Matrix index out of bounds" if x >= width || y >= height
21
+ @bits[(width * y) + x] = value
22
+ @rows = @columns = nil
23
+ end
24
+
25
+ def rows
26
+ @rows ||= begin
27
+ rows = []
28
+ bits = @bits.dup
29
+
30
+ while row = bits.slice!(0, @width)
31
+ break if row.nil? || row.empty?
32
+ rows << row
33
+ end
34
+
35
+ rows
36
+ end
37
+ end
38
+
39
+ def columns
40
+ @columns ||= begin
41
+ columns = []
42
+ width.times do |index|
43
+ column = []
44
+
45
+ rows.each do |row|
46
+ column << row[index]
47
+ end
48
+
49
+ columns << column
50
+ end
51
+
52
+ columns
53
+ end
54
+ end
55
+
56
+ def rotate
57
+ new_bits = []
58
+ columns.each do |column|
59
+ new_bits += column.reverse
60
+ end
61
+ self.class.new(new_bits, @height, @width)
62
+ end
63
+
64
+ def extract(x, y, width, height)
65
+ new_bits = []
66
+ height.times do |offset|
67
+ new_bits += rows[y + offset].slice(x, width)
68
+ end
69
+ self.class.new(new_bits, width, height)
70
+ end
71
+ end
72
+ end