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