image_svd 0.0.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c6be55c37295a68be501d5eb609175825105b52
4
- data.tar.gz: 33fa7ce261ed05d54123c04ae9bbca9be486b5ba
3
+ metadata.gz: 66400751ae0908db2cc49a77f51d2aacaa0418f1
4
+ data.tar.gz: 40c59891f1db50ca96860464314c5b9f3d839cf4
5
5
  SHA512:
6
- metadata.gz: fbca7cbe89abcccbf9f9a916163b1b3ee122fdd3060b821c1a1e237e783f239b85aa63f081bae7d8574337a00f30196cf9ce11e00a706ab964f3d292de28d479
7
- data.tar.gz: 23083c717ed0fc46950cabd0551c82d38f5ceef2ca0c8dc1a5c3fad0cf1f96f7d3ce1f67c7809aae48b2e301eab9abba4a18fe1ddfaefa5285b4db3b02395dec
6
+ metadata.gz: 24a2168981ee91080b1555416a17944e8c1e638c4aec9cf4bbd79c320b1a21ef7af1080aecb27127137f16f10de1cc1314592559f1aaaccc549c4a90ca151a75
7
+ data.tar.gz: 1dc02a67c48e6869952854d86cd16a2b96212d95c31378b91585ba0f9fb7647be461a544db513ba8e20befe66020f89eaebc7e0a0e54aed920121595982ed9cd
data/lib/image_svd.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'image_svd/version'
4
+ require 'image_svd/util'
4
5
  require 'image_svd/cli'
5
6
  require 'image_svd/image_matrix'
data/lib/image_svd/cli.rb CHANGED
@@ -4,57 +4,81 @@ module ImageSvd
4
4
  # This class is responsible for handling the command line interface
5
5
  class CLI
6
6
  # The entry point for the application logic
7
- # rubocop:disable MethodLength
8
- def run(opts)
9
- if opts[:read] == true
10
- app = ImageSvd::ImageMatrix.new_from_svd_savefile(opts)
11
- app.to_image(opts[:output_name])
12
- else
13
- opts = process_options(opts)
14
- app = ImageSvd::ImageMatrix.new(opts[:singular_values])
15
- app.read_image(opts[:input_file])
16
- if opts[:archive] == true
17
- app.save_svd(opts[:output_name])
18
- elsif opts[:convert] == true
19
- app.to_image(opts[:output_name])
7
+ def run(options)
8
+ Options.iterate_on_input(options) do |o|
9
+ if o[:directory]
10
+ fork { run_single_image(o) }
11
+ else
12
+ run_single_image(o)
20
13
  end
21
14
  end
15
+ Process.waitall
22
16
  end
23
17
 
24
- def process_options(opts)
25
- i, valid_i = [opts[:num_singular_values].to_s, /^\d+\.\.\d+$|^\d+$/]
26
- fail 'invalid --num-singular-values option' unless i.match valid_i
27
- vs = i.split('..').map(&:to_i)
28
- vs = (vs.length == 1 ? vs : ((vs.first)..(vs.last)).to_a)
29
- opts.merge(singular_values: vs)
18
+ # rubocop:disable MethodLength
19
+ def run_single_image(o)
20
+ if o[:read] == true
21
+ app = ImageSvd::ImageMatrix.new_from_svd_savefile(o)
22
+ app.to_image(o[:output_name])
23
+ else
24
+ app = ImageSvd::ImageMatrix.new(o[:singular_values], o[:grayscale])
25
+ app.read_image(o[:input_file])
26
+ if o[:archive] == true
27
+ app.save_svd(o[:output_name])
28
+ elsif o[:convert] == true
29
+ app.to_image(o[:output_name])
30
+ end
31
+ end
30
32
  end
31
33
  end
32
34
  # rubocop:enable MethodLength
33
35
 
34
36
  # This module holds custom behavior for dealing with the gem trollop
35
37
  module Options
38
+ extend Util
39
+
36
40
  # rubocop:disable MethodLength
37
41
  def self.get
38
42
  proc do
39
43
  version "Image Svd #{ImageSvd::VERSION} (c) 2014 Ilya Kavalerov"
40
44
  banner <<-EOS
41
- Image Svd is a utilty that performs Singular Value Decomposition
42
- on a grayscale image. It can be useful for compressing images, or
43
- creating interesting visual effects to distort images when
44
- compression is set very high.
45
+
46
+ _____________ ____ ____ ______
47
+ \\ ________\\ \\ \\ / / / __ \\
48
+ \\ \\ \\ \\ / / / / \\ \\
49
+ \\ \\________ \\ \\ / / / / \\ \\
50
+ \\_________ \\ \\ \\ / / / / / /
51
+ \\ \\ \\ \\/ / / / / /
52
+ _________\\ \\ \\ / / /_____/ /
53
+ \\_____________\\ \\____/ /___________/
54
+
55
+
56
+ Image Svd is a utilty for compressing images, or creating
57
+ interesting visual effects to distort images when compression is
58
+ set very high. Image Svd performs Singular Value Decomposition
59
+ on any image, grayscale or color.
45
60
 
46
61
  Usage:
47
62
  image_svd [options]
48
63
  where [options] are:
64
+
49
65
  EOS
50
66
  opt :input_file,
51
- 'An input file (Preferably a jpg).',
67
+ 'An input file (Preferably a jpg). If you also specify'\
68
+ ' --directory or -d, you may provide the path to a directory'\
69
+ ' (which must end with a "/") instead of a file.',
52
70
  type: :io,
53
71
  required: true
72
+ opt :grayscale,
73
+ 'Do not preserve the colors in the input image. Specify'\
74
+ ' --no-grayscale when you want an output image in color.'\
75
+ ' Expect processing time to increase 3-fold for color images.',
76
+ default: true,
77
+ short: '-g'
54
78
  opt :num_singular_values,
55
79
  'The number of singular values to keep for an image. Lower'\
56
- ' numbers mean lossier compression; smaller files and more'\
57
- ' distorted images. You may also provide a range ruby style,'\
80
+ ' numbers mean lossier compression, smaller files and more'\
81
+ ' distorted images. You may also provide a range ruby style'\
58
82
  ' (ex: 1..9) in which case many images will be output.',
59
83
  default: '50',
60
84
  short: '-n'
@@ -64,6 +88,13 @@ module ImageSvd
64
88
  ' the current directory',
65
89
  default: 'svd_image_output',
66
90
  short: '-o'
91
+ opt :directory,
92
+ 'The input provided is a directory instead of a file. In this'\
93
+ ' case every valid image inside the directory provided with'\
94
+ ' the option -i will be compressed, and placed into a folder'\
95
+ ' named "out" inside the directory specified.',
96
+ default: false,
97
+ short: '-d'
67
98
  opt :convert,
68
99
  'Convert the input file now.',
69
100
  default: true,
@@ -80,5 +111,57 @@ module ImageSvd
80
111
  end
81
112
  end
82
113
  # rubocop:enable MethodLength
114
+
115
+ # this method chooses which number of singular values are valid to output
116
+ # to an image file from an archive file provided. @returns Array[Int]
117
+ def self.num_sing_val_out_from_archive(requests, available)
118
+ valid_svals = requests.reject { |v| v > available }
119
+ valid_svals.empty? ? [available] : valid_svals
120
+ end
121
+
122
+ def self.process(opts)
123
+ vs = format_num_sing_vals(opts[:num_singular_values].to_s)
124
+ opts.merge(singular_values: vs)
125
+ end
126
+
127
+ # reformats the string cmd line option to an array
128
+ def self.format_num_sing_vals(str)
129
+ i, valid_i_regex = [str, /^\d+\.\.\d+$|^\d+$/]
130
+ fail 'invalid --num-singular-values option' unless i.match valid_i_regex
131
+ vs = i.split('..').map(&:to_i)
132
+ vs.length == 1 ? vs : ((vs.first)..(vs.last)).to_a
133
+ end
134
+
135
+ # reformats directory inputs into an array of files, or repackages file
136
+ # inputs to be contained inside an array
137
+ def self.expand_input_files(opts)
138
+ if opts[:directory]
139
+ path = opts[:input_file].path
140
+ names = Dir.new(path).to_a
141
+ images = names.select { |name| name =~ Util::VALID_INPUT_EXT_REGEX }
142
+ images.map { |name| File.new(path + name) }
143
+ else
144
+ [opts[:input_file]]
145
+ end
146
+ end
147
+
148
+ # ignore provided output_name in the case that a directory is input
149
+ def self.output_dir_path_for_input_file(dir)
150
+ path_components = dir.path.split('/')
151
+ filename = path_components.pop
152
+ (path_components << 'out' << filename).join('/')
153
+ end
154
+
155
+ def self.iterate_on_input(opts)
156
+ %x(mkdir #{opts[:input_file].path + 'out'}) if opts[:directory]
157
+ expand_input_files(opts).each do |file|
158
+ new_options = { input_file: file }
159
+ if opts[:directory]
160
+ new_options.merge!(output_name: output_dir_path_for_input_file(file))
161
+ end
162
+ o = process(opts).merge(new_options)
163
+ yield o
164
+ end
165
+ end
83
166
  end
84
167
  end
@@ -3,44 +3,28 @@ require 'json'
3
3
  require 'pnm'
4
4
 
5
5
  module ImageSvd
6
- # This class is responsible for almost everything :(
7
- # Reading an image or archive to a matrix
8
- # Saving a matrix to an image
9
- # Performing Singular Value Decomposition on a matrix
10
- class ImageMatrix
11
- # rubocop:disable SymbolName
12
- # rubocop:disable VariableName
13
- attr_reader :singular_values
14
- attr_accessor :sigma_vTs, :us, :m, :n
6
+ # rubocop:disable SymbolName
7
+ # rubocop:disable VariableName
15
8
 
16
- def initialize(singular_values)
17
- fail 'not enough singular values' if singular_values.length.zero?
18
- @singular_values = singular_values
19
- @num_singular_values = singular_values.max
20
- end
21
-
22
- def read_image(image_path)
23
- puts 'Reading image and converting to matrix...'
24
- intermediate = extension_swap(image_path.path, 'pgm')
25
- %x(convert #{image_path.path} #{intermediate})
26
- image = PNM.read intermediate
27
- decompose Matrix[*image.pixels]
28
- %x(rm #{intermediate})
29
- self
30
- end
9
+ # This class is responsible for manipulating matricies that correspond
10
+ # to the color channels in images, which includes performing Singular
11
+ # Value Decomposition on a matrix
12
+ class Channel
13
+ attr_accessor :sigma_vTs, :us, :m, :n
14
+ attr_reader :num_singular_values
31
15
 
32
- # @todo abstract this to another class
33
- # always place the new extension, even if there is nothing to swap out
34
- def extension_swap(path, new_ext, suffix = '')
35
- head = path.gsub(/\..{1,5}$/, '')
36
- "#{head}#{suffix}.#{new_ext}"
16
+ def initialize(matrix, num_singular_values)
17
+ fail 'Channel initialized without a matrix' unless matrix.is_a? Matrix
18
+ @matrix = matrix
19
+ @num_singular_values = num_singular_values
37
20
  end
38
21
 
39
22
  # The most time consuming method
40
23
  # Launches the decomposition and saves the two lists
41
24
  # of vectors needed to reconstruct the image
42
25
  # rubocop:disable MethodLength
43
- def decompose(m_A)
26
+ def decompose(m_A = nil)
27
+ m_A ||= @matrix
44
28
  m_AT = m_A.transpose
45
29
  @m, @n = m_A.to_a.length, m_A.to_a.first.length
46
30
  m_ATA = m_AT * m_A
@@ -60,6 +44,7 @@ module ImageSvd
60
44
  end
61
45
  @sigma_vTs = both.map { |p| p.last }
62
46
  @us = both.map { |p| p.first }
47
+ self
63
48
  end
64
49
  # rubocop:enable MethodLength
65
50
 
@@ -71,22 +56,126 @@ module ImageSvd
71
56
  end.transpose
72
57
  end
73
58
 
59
+ # returns all the information necessary to serialize this channel
60
+ def to_h
61
+ {
62
+ 'sigma_vTs' => @sigma_vTs.map(&:to_a),
63
+ 'us' => @us.map(&:to_a),
64
+ 'm' => @m,
65
+ 'n' => @n
66
+ }
67
+ end
68
+
69
+ # can initialize with the result of #to_h
70
+ def self.apply_h(hash, num_singular_values)
71
+ c = new(Matrix[], num_singular_values)
72
+ c.sigma_vTs = hash['sigma_vTs']
73
+ .map { |arr| Vector[*arr.flatten].covector }
74
+ c.us = hash['us'].map { |arr| Vector[*arr.flatten] }
75
+ c.m = hash['m']
76
+ c.n = hash['n']
77
+ c
78
+ end
79
+ end
80
+
81
+ # This class is responsible for:
82
+ # Reading an image or archive to a matrix
83
+ # Saving a matrix to an image
84
+ class ImageMatrix
85
+ include ImageSvd::Util
86
+
87
+ attr_reader :singular_values, :grayscale
88
+ attr_accessor :channels
89
+
90
+ def initialize(singular_values, grayscale)
91
+ fail 'not enough singular values' if singular_values.length.zero?
92
+ @singular_values = singular_values
93
+ @num_singular_values = singular_values.max
94
+ @grayscale = grayscale
95
+ @channels = []
96
+ end
97
+
98
+ def get_image_channels(image_path)
99
+ extension = @grayscale ? 'pgm' : 'ppm'
100
+ intermediate = extension_swap(image_path.path, extension)
101
+ %x(convert #{image_path.path} #{intermediate})
102
+ if @grayscale
103
+ channels = [PNM.read(intermediate).pixels]
104
+ else
105
+ channels = ImageMatrix.ppm_to_rgb(PNM.read(intermediate).pixels)
106
+ end
107
+ %x(rm #{intermediate})
108
+ channels.map { |c| Matrix[*c] }
109
+ end
110
+
111
+ def read_image(image_path)
112
+ channels = get_image_channels(image_path)
113
+ @channels = channels.map { |m| Channel.new(m, @num_singular_values) }
114
+ @channels.each(&:decompose)
115
+ end
116
+
74
117
  def to_image(path)
118
+ if @grayscale
119
+ to_grayscale_image(path)
120
+ else
121
+ to_color_image(path)
122
+ end
123
+ end
124
+
125
+ def to_grayscale_image(path)
75
126
  puts 'writing images...' if @singular_values.length > 1
76
127
  @singular_values.each do |sv|
77
128
  out_path = extension_swap(path, 'jpg', "_#{sv}_svs")
78
129
  intermediate = extension_swap(path, 'pgm', '_tmp_outfile')
79
- cleansed_matrix = matrix_to_valid_pixels(reconstruct_matrix(sv))
80
- PNM::Image.new(cleansed_matrix).write(intermediate)
130
+ reconstructed_mtrx = @channels.first.reconstruct_matrix(sv)
131
+ cleansed_mtrx = ImageMatrix.matrix_to_valid_pixels(reconstructed_mtrx)
132
+ PNM::Image.new(cleansed_mtrx).write(intermediate)
81
133
  %x(convert #{intermediate} #{out_path})
82
134
  %x(rm #{intermediate})
83
135
  end
84
- true
136
+ end
137
+
138
+ def to_color_image(path)
139
+ puts 'writing images...' if @singular_values.length > 1
140
+ @singular_values.each do |sv|
141
+ out_path = extension_swap(path, 'jpg', "_#{sv}_svs")
142
+ intermediate = extension_swap(path, 'ppm', '_tmp_outfile')
143
+ ms = @channels.map { |c| c.reconstruct_matrix(sv) }
144
+ cleansed_mtrxs = ms.map { |m| ImageMatrix.matrix_to_valid_pixels(m) }
145
+ ppm_matrix = ImageMatrix.rgb_to_ppm(*cleansed_mtrxs)
146
+ PNM::Image.new(ppm_matrix).write(intermediate)
147
+ %x(convert #{intermediate} #{out_path})
148
+ %x(rm #{intermediate})
149
+ end
150
+ end
151
+
152
+ def save_svd(path)
153
+ out_path = extension_swap(path, 'svdim')
154
+ string = @channels.map(&:to_h).to_json
155
+ File.open(out_path, 'w') do |f|
156
+ f.puts string
157
+ end
158
+ end
159
+
160
+ # breaks a ppm image into 3 separate channels
161
+ def self.ppm_to_rgb(arr)
162
+ (0..2).to_a.map do |i|
163
+ arr.map { |row| row.map { |pix| pix[i] } }
164
+ end
165
+ end
166
+
167
+ # combines 3 separate channels into the ppm scheme
168
+ def self.rgb_to_ppm(r, g, b)
169
+ r.each_with_index.map do |row, row_i|
170
+ row.each_with_index.map do |_, pix_i|
171
+ [r[row_i][pix_i], g[row_i][pix_i], b[row_i][pix_i]]
172
+ end
173
+ end
85
174
  end
86
175
 
87
176
  # conforms a matrix to pnm requirements for pixels: positive integers
88
177
  # rubocop:disable MethodLength
89
- def matrix_to_valid_pixels(matrix)
178
+ def self.matrix_to_valid_pixels(matrix)
90
179
  matrix.to_a.map do |row|
91
180
  row.map do |number|
92
181
  rounded = number.round
@@ -102,34 +191,36 @@ module ImageSvd
102
191
  end
103
192
  # rubocop:enable MethodLength
104
193
 
105
- def save_svd(path)
106
- out_path = extension_swap(path, 'svdim')
107
- string = {
108
- 'sigma_vTs' => @sigma_vTs.map(&:to_a),
109
- 'us' => @us.map(&:to_a),
110
- 'm' => @m,
111
- 'n' => @n
112
- }.to_json
113
- File.open(out_path, 'w') do |f|
114
- f.puts string
194
+ def self.new_saved_grayscale_svd(opts, h)
195
+ svals = [opts[:singular_values], h['sigma_vTs'].size]
196
+ valid_svals = ImageSvd::Options.num_sing_val_out_from_archive(*svals)
197
+ instance = new(valid_svals, true)
198
+ instance.channels << Channel.apply_h(h, valid_svals)
199
+ instance
200
+ end
201
+
202
+ def self.new_saved_color_svd(opts, hs)
203
+ svals = [opts[:singular_values], hs.first['sigma_vTs'].size]
204
+ valid_svals = ImageSvd::Options.num_sing_val_out_from_archive(*svals)
205
+ instance = new(valid_svals, false)
206
+ 3.times do |i|
207
+ chan = Channel.apply_h(hs[i], valid_svals)
208
+ instance.channels << chan
115
209
  end
210
+ instance
116
211
  end
117
212
 
118
213
  # @todo error handling code here
119
214
  # @todo serialization is kind of silly as is
120
215
  def self.new_from_svd_savefile(opts)
121
216
  h = JSON.parse(File.open(opts[:input_file], &:readline))
122
- svals = [opts[:singular_values], h['sigma_vTs'].size].compact.sort.uniq
123
- valid_svals = svals.reject { |v| v > h['sigma_vTs'].size }
124
- instance = new(valid_svals)
125
- instance.sigma_vTs = h['sigma_vTs']
126
- .map { |arr| Vector[*arr.flatten].covector }
127
- instance.us = h['us'].map { |arr| Vector[*arr.flatten] }
128
- instance.n = h['n']
129
- instance.m = h['m']
130
- instance
217
+ if h.length == 1 # grayscale
218
+ new_saved_grayscale_svd(opts, h.first)
219
+ else
220
+ new_saved_color_svd(opts, h)
221
+ end
131
222
  end
132
- # rubocop:enable SymbolName
133
- # rubocop:enable VariableName
134
223
  end
224
+ # rubocop:enable SymbolName
225
+ # rubocop:enable VariableName
135
226
  end
@@ -0,0 +1,16 @@
1
+ module ImageSvd
2
+ # This module holds useful miscellaneous methods
3
+ module Util
4
+ # rubocop:disable LineLength
5
+ # imagemagick supported formats from running `convert -list format`,
6
+ # plus this application's native archive extension '.svdim'
7
+ VALID_INPUT_EXT_REGEX = /\.svdim|\.3FR|\.A|\.AAI|\.AI|\.ART|\.ARW|\.AVI|\.AVS|\.B|\.BGR|\.BGRA|\.BMP|\.BMP2|\.BMP3|\.BRF|\.C|\.CAL|\.CALS|\.CANVAS|\.CAPTION|\.CIN|\.CIP|\.CLIP|\.CMYK|\.CMYKA|\.CR2|\.CRW|\.CUR|\.CUT|\.DCM|\.DCR|\.DCX|\.DDS|\.DFONT|\.DNG|\.DOT|\.DPX|\.DXT1|\.DXT5|\.EPDF|\.EPI|\.EPS|\.EPS2|\.EPS3|\.EPSF|\.EPSI|\.ERF|\.FAX|\.FITS|\.FRACTAL|\.FTS|\.G|\.G3|\.GIF|\.GIF87|\.GRADIENT|\.GRAY|\.GV|\.HALD|\.HDR|\.HISTOGRAM|\.HRZ|\.HTM|\.HTML|\.ICB|\.ICO|\.ICON|\.INFO|\.INLINE|\.IPL|\.ISOBRL|\.JNG|\.JNX|\.JPEG|\.JPG|\.K|\.K25|\.KDC|\.LABEL|\.M|\.M2V|\.M4V|\.MAC|\.MAP|\.MASK|\.MAT|\.MATTE|\.MEF|\.MIFF|\.MNG|\.MONO|\.MOV|\.MP4|\.MPC|\.MPEG|\.MPG|\.MRW|\.MSL|\.MSVG|\.MTV|\.MVG|\.NEF|\.NRW|\.NULL|\.O|\.ORF|\.OTB|\.OTF|\.PAL|\.PALM|\.PAM|\.PANGO|\.PATTERN|\.PBM|\.PCD|\.PCDS|\.PCL|\.PCT|\.PCX|\.PDB|\.PDF|\.PDFA|\.PEF|\.PES|\.PFA|\.PFB|\.PFM|\.PGM|\.PICON|\.PICT|\.PIX|\.PJPEG|\.PLASMA|\.PNG|\.PNG00|\.PNG24|\.PNG32|\.PNG48|\.PNG64|\.PNG8|\.PNM|\.PPM|\.PREVIEW|\.PS|\.PS2|\.PS3|\.PSB|\.PSD|\.PWP|\.R|\.RAF|\.RAS|\.RGB|\.RGBA|\.RGBO|\.RGF|\.RLA|\.RLE|\.RW2|\.SCR|\.SCT|\.SFW|\.SGI|\.SHTML|\.SPARSE|\.SR2|\.SRF|\.STEGANO|\.SUN|\.SVG|\.SVGZ|\.TEXT|\.TGA|\.THUMBNAIL|\.TILE|\.TIM|\.TTC|\.TTF|\.TXT|\.UBRL|\.UIL|\.UYVY|\.VDA|\.VICAR|\.VID|\.VIFF|\.VST|\.WBMP|\.WMV|\.WPG|\.X3F|\.XBM|\.XC|\.XCF|\.XPM|\.XPS|\.XV|\.Y|\.YCbCr|\.YCbCrA|\.YUV/i
8
+ # rubocop:enable LineLength
9
+
10
+ # always place the new extension, even if there is nothing to swap out
11
+ def extension_swap(path, new_ext, suffix = '')
12
+ head = path.gsub(/\..{1,5}$/, '')
13
+ "#{head}#{suffix}.#{new_ext}"
14
+ end
15
+ end
16
+ end
@@ -1,4 +1,4 @@
1
1
  # This module holds the gem's code
2
2
  module ImageSvd
3
- VERSION = '0.0.2'
3
+ VERSION = '0.1.4'
4
4
  end
data/spec/cli_spec.rb CHANGED
@@ -3,23 +3,28 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe 'CLI' do
6
- describe 'integration spec' do
6
+ describe 'integration spec for grayscale images' do
7
7
  let(:cli) { ImageSvd::CLI.new }
8
8
  let(:orig) { File.new('./spec/fixtures/2x2.jpg') }
9
+ let(:default_opts) do
10
+ {
11
+ input_file: orig,
12
+ convert: true,
13
+ num_singular_values: '2',
14
+ grayscale: true
15
+ }
16
+ end
9
17
 
10
18
  it 'converts an image without too great errors' do
11
19
  conv = './spec/fixtures/svd_image_output'
12
- cli.run(
13
- input_file: orig,
14
- convert: true,
15
- num_singular_values: 2,
16
- output_name: conv
17
- )
18
- i = ImageSvd::ImageMatrix.new([2])
20
+ cli.run(default_opts.merge(output_name: conv))
21
+ i = ImageSvd::ImageMatrix.new([2], true)
19
22
  i.read_image(orig)
20
- i2 = ImageSvd::ImageMatrix.new([2])
23
+ i2 = ImageSvd::ImageMatrix.new([2], true)
21
24
  i2.read_image(File.new("#{conv}_2_svs.jpg"))
22
- diff_matrix = i.reconstruct_matrix - i2.reconstruct_matrix
25
+ m = i.channels.first.reconstruct_matrix
26
+ m2 = i2.channels.first.reconstruct_matrix
27
+ diff_matrix = m - m2
23
28
  diff_matrix.to_a.flatten.each do |diff_component|
24
29
  diff_component.abs.should be < 5
25
30
  end
@@ -31,23 +36,23 @@ describe 'CLI' do
31
36
  it 'archives, reads, and converts an image without too great errors' do
32
37
  conv = './spec/fixtures/svd_image_output'
33
38
  # archive
34
- cli.run(
35
- input_file: orig,
39
+ cli.run(default_opts.merge(
36
40
  archive: true,
37
- num_singular_values: '2',
38
41
  output_name: conv
39
- )
42
+ ))
40
43
  # read archive and write and image
41
- cli.run(
44
+ cli.run(default_opts.merge(
42
45
  input_file: "#{conv}.svdim",
43
46
  read: true,
44
47
  output_name: "#{conv}_two"
45
- )
46
- i = ImageSvd::ImageMatrix.new([2])
48
+ ))
49
+ i = ImageSvd::ImageMatrix.new([2], true)
47
50
  i.read_image(orig)
48
- i2 = ImageSvd::ImageMatrix.new([2])
51
+ i2 = ImageSvd::ImageMatrix.new([2], true)
49
52
  i2.read_image(File.new("#{conv}_two_2_svs.jpg"))
50
- diff_matrix = i.reconstruct_matrix - i2.reconstruct_matrix
53
+ m = i.channels.first.reconstruct_matrix
54
+ m2 = i2.channels.first.reconstruct_matrix
55
+ diff_matrix = m - m2
51
56
  diff_matrix.to_a.flatten.each do |diff_component|
52
57
  diff_component.abs.should be < 5
53
58
  end
@@ -55,4 +60,81 @@ describe 'CLI' do
55
60
  %x(rm #{conv}_two_2_svs.jpg #{conv}.svdim)
56
61
  end
57
62
  end
63
+
64
+ describe 'integration spec for color images' do
65
+ let(:cli) { ImageSvd::CLI.new }
66
+ let(:orig) { File.new('./spec/fixtures/2x2_color.png') }
67
+ let(:default_opts) do
68
+ {
69
+ input_file: orig,
70
+ convert: true,
71
+ num_singular_values: '2',
72
+ grayscale: false
73
+ }
74
+ end
75
+
76
+ it 'converts an image without too great errors' do
77
+ conv = './spec/fixtures/svd_image_output'
78
+ cli.run(default_opts.merge(output_name: conv))
79
+ i = ImageSvd::ImageMatrix.new([2], false)
80
+ i.read_image(orig)
81
+ i2 = ImageSvd::ImageMatrix.new([2], false)
82
+ i2.read_image(File.new("#{conv}_2_svs.jpg"))
83
+ m = i.channels.map { |c| c.reconstruct_matrix }
84
+ m2 = i2.channels.map { |c| c.reconstruct_matrix }
85
+ diff_matricies = (0..2).to_a.map { |idx| m[idx] - m2[idx] }
86
+ diff_matricies.map(&:to_a).flatten.each do |diff_component|
87
+ diff_component.abs.should be < 5
88
+ end
89
+ # cleanup
90
+ %x(rm #{conv}_2_svs.jpg)
91
+ end
92
+
93
+ it 'archives, reads, and converts an image without too great errors' do
94
+ conv = './spec/fixtures/svd_image_output'
95
+ # archive
96
+ cli.run(default_opts.merge(
97
+ archive: true,
98
+ output_name: conv
99
+ ))
100
+ # read archive and write and image
101
+ cli.run(default_opts.merge(
102
+ input_file: "#{conv}.svdim",
103
+ read: true,
104
+ output_name: "#{conv}_two"
105
+ ))
106
+ i = ImageSvd::ImageMatrix.new([2], false)
107
+ i.read_image(orig)
108
+ i2 = ImageSvd::ImageMatrix.new([2], false)
109
+ i2.read_image(File.new("#{conv}_two_2_svs.jpg"))
110
+ m = i.channels.map { |c| c.reconstruct_matrix }
111
+ m2 = i2.channels.map { |c| c.reconstruct_matrix }
112
+ diff_matricies = (0..2).to_a.map { |idx| m[idx] - m2[idx] }
113
+ diff_matricies.map(&:to_a).flatten.each do |diff_component|
114
+ diff_component.abs.should be < 5
115
+ end
116
+ # cleanup
117
+ %x(rm #{conv}_two_2_svs.jpg #{conv}.svdim)
118
+ end
119
+ end
120
+
121
+ describe 'expand_input_files' do
122
+ it 'packages a file input into an array container' do
123
+ file = File.new('./spec/fixtures/2x2.jpg')
124
+ opts = { input_file: file, directory: false }
125
+ formatted = ImageSvd::Options.expand_input_files(opts)
126
+ formatted.should eq([file])
127
+ end
128
+ it 'expands valid files in a directory into an array' do
129
+ # This spec will break if any more fixtures are added
130
+ in_dir = File.new('./spec/fixtures/') # the output of trollop
131
+ contents = [
132
+ './spec/fixtures/2x2.jpg',
133
+ './spec/fixtures/2x2_color.png'
134
+ ]
135
+ opts = { input_file: in_dir, directory: true }
136
+ formatted = ImageSvd::Options.expand_input_files(opts)
137
+ formatted.map(&:path).should eq(contents)
138
+ end
139
+ end
58
140
  end
Binary file
@@ -8,9 +8,11 @@ describe ImageSvd::ImageMatrix do
8
8
  end
9
9
 
10
10
  it 'recovers a 2x3 matrix' do
11
- i = ImageSvd::ImageMatrix.new([2])
12
- i.decompose(@m)
13
- rounded_matrix = Matrix[*i.matrix_to_valid_pixels(i.reconstruct_matrix)]
11
+ c = ImageSvd::Channel.new(@m, 2)
12
+ c.decompose
13
+ rounded_matrix = Matrix[
14
+ *ImageSvd::ImageMatrix.matrix_to_valid_pixels(c.reconstruct_matrix)
15
+ ]
14
16
  # due to numerical instability, even a 2x3 matrix needs to be rounded
15
17
  @m.should eq(rounded_matrix)
16
18
  end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'ImageSvd::Options' do
6
+ describe 'num_sing_val_out_from_archive' do
7
+ it 'returns the number of sing vals available when none are requested' do
8
+ o = ImageSvd::Options.num_sing_val_out_from_archive([6], 20)
9
+ o.should eq([6])
10
+ end
11
+
12
+ it 'allows the requested number of sing vals when more are available' do
13
+ o = ImageSvd::Options.num_sing_val_out_from_archive([6], 20)
14
+ o.should eq([6])
15
+ end
16
+
17
+ it 'returns the number of sing vals available if more are requested' do
18
+ o = ImageSvd::Options.num_sing_val_out_from_archive([400], 20)
19
+ o.should eq([20])
20
+ end
21
+
22
+ it 'returns the valid segment of a range of sing vals requested' do
23
+ o = ImageSvd::Options.num_sing_val_out_from_archive((1..40).to_a, 20)
24
+ o.should eq((1..20).to_a)
25
+ end
26
+ end
27
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_svd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilya Kavalerov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-23 00:00:00.000000000 Z
11
+ date: 2014-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pnm
@@ -114,10 +114,13 @@ files:
114
114
  - lib/image_svd.rb
115
115
  - lib/image_svd/cli.rb
116
116
  - lib/image_svd/image_matrix.rb
117
+ - lib/image_svd/util.rb
117
118
  - lib/image_svd/version.rb
118
119
  - spec/cli_spec.rb
119
120
  - spec/fixtures/2x2.jpg
121
+ - spec/fixtures/2x2_color.png
120
122
  - spec/image_matrix_spec.rb
123
+ - spec/options_spec.rb
121
124
  - spec/spec_helper.rb
122
125
  homepage: https://github.com/ilyakava/image_svd
123
126
  licenses:
@@ -146,5 +149,7 @@ summary: Compress images with Linear Algebra.
146
149
  test_files:
147
150
  - spec/cli_spec.rb
148
151
  - spec/fixtures/2x2.jpg
152
+ - spec/fixtures/2x2_color.png
149
153
  - spec/image_matrix_spec.rb
154
+ - spec/options_spec.rb
150
155
  - spec/spec_helper.rb