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 +4 -4
- data/lib/image_svd.rb +1 -0
- data/lib/image_svd/cli.rb +109 -26
- data/lib/image_svd/image_matrix.rb +146 -55
- data/lib/image_svd/util.rb +16 -0
- data/lib/image_svd/version.rb +1 -1
- data/spec/cli_spec.rb +101 -19
- data/spec/fixtures/2x2_color.png +0 -0
- data/spec/image_matrix_spec.rb +5 -3
- data/spec/options_spec.rb +27 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66400751ae0908db2cc49a77f51d2aacaa0418f1
|
4
|
+
data.tar.gz: 40c59891f1db50ca96860464314c5b9f3d839cf4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24a2168981ee91080b1555416a17944e8c1e638c4aec9cf4bbd79c320b1a21ef7af1080aecb27127137f16f10de1cc1314592559f1aaaccc549c4a90ca151a75
|
7
|
+
data.tar.gz: 1dc02a67c48e6869952854d86cd16a2b96212d95c31378b91585ba0f9fb7647be461a544db513ba8e20befe66020f89eaebc7e0a0e54aed920121595982ed9cd
|
data/lib/image_svd.rb
CHANGED
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
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
|
-
#
|
7
|
-
#
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
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
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
data/lib/image_svd/version.rb
CHANGED
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/spec/image_matrix_spec.rb
CHANGED
@@ -8,9 +8,11 @@ describe ImageSvd::ImageMatrix do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
it 'recovers a 2x3 matrix' do
|
11
|
-
|
12
|
-
|
13
|
-
rounded_matrix = 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.
|
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-
|
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
|