qrio 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.
@@ -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