image_svd 0.0.2 → 0.1.4

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