qttk 0.1.0
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 +7 -0
- data/.gitignore +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +36 -0
- data/README.markdown +131 -0
- data/Rakefile +17 -0
- data/TODO.markdown +236 -0
- data/bin/qt +7 -0
- data/etc/gutenprint/gp-tool.rb +56 -0
- data/etc/gutenprint/gutenprint-filter.c +400 -0
- data/etc/gutenprint/gutenprint.rb +86 -0
- data/etc/gutenprint/stp-test +326 -0
- data/etc/images/3551599565_db282cf840_o.jpg +0 -0
- data/etc/images/4843122063_d582c569e9_o.jpg +0 -0
- data/etc/images/4843128953_83c1770907_o.jpg +0 -0
- data/lib/quadtone.rb +56 -0
- data/lib/quadtone/cgats.rb +137 -0
- data/lib/quadtone/cluster_calculator.rb +81 -0
- data/lib/quadtone/color.rb +83 -0
- data/lib/quadtone/color/cmyk.rb +112 -0
- data/lib/quadtone/color/device_n.rb +23 -0
- data/lib/quadtone/color/gray.rb +46 -0
- data/lib/quadtone/color/lab.rb +150 -0
- data/lib/quadtone/color/qtr.rb +71 -0
- data/lib/quadtone/color/rgb.rb +71 -0
- data/lib/quadtone/color/xyz.rb +80 -0
- data/lib/quadtone/curve.rb +138 -0
- data/lib/quadtone/curve_set.rb +196 -0
- data/lib/quadtone/descendants.rb +9 -0
- data/lib/quadtone/environment.rb +5 -0
- data/lib/quadtone/extensions/math.rb +11 -0
- data/lib/quadtone/extensions/pathname3.rb +11 -0
- data/lib/quadtone/printer.rb +106 -0
- data/lib/quadtone/profile.rb +217 -0
- data/lib/quadtone/quad_file.rb +59 -0
- data/lib/quadtone/renderer.rb +139 -0
- data/lib/quadtone/run.rb +10 -0
- data/lib/quadtone/sample.rb +32 -0
- data/lib/quadtone/separator.rb +36 -0
- data/lib/quadtone/target.rb +277 -0
- data/lib/quadtone/tool.rb +61 -0
- data/lib/quadtone/tools/add_printer.rb +73 -0
- data/lib/quadtone/tools/characterize.rb +43 -0
- data/lib/quadtone/tools/chart.rb +31 -0
- data/lib/quadtone/tools/check.rb +16 -0
- data/lib/quadtone/tools/dir.rb +15 -0
- data/lib/quadtone/tools/edit.rb +23 -0
- data/lib/quadtone/tools/init.rb +82 -0
- data/lib/quadtone/tools/install.rb +15 -0
- data/lib/quadtone/tools/linearize.rb +28 -0
- data/lib/quadtone/tools/list.rb +19 -0
- data/lib/quadtone/tools/print.rb +38 -0
- data/lib/quadtone/tools/printer_options.rb +40 -0
- data/lib/quadtone/tools/rename.rb +17 -0
- data/lib/quadtone/tools/render.rb +43 -0
- data/lib/quadtone/tools/rewrite.rb +15 -0
- data/lib/quadtone/tools/separate.rb +71 -0
- data/lib/quadtone/tools/show.rb +15 -0
- data/lib/quadtone/tools/test.rb +26 -0
- data/qttk.gemspec +34 -0
- metadata +215 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
module Quadtone
|
2
|
+
|
3
|
+
class QuadFile
|
4
|
+
|
5
|
+
attr_accessor :curve_set
|
6
|
+
|
7
|
+
ChannelAliases = {
|
8
|
+
'C' => 'c',
|
9
|
+
'M' => 'm',
|
10
|
+
'Y' => 'y',
|
11
|
+
'K' => 'k',
|
12
|
+
'c' => 'lc',
|
13
|
+
'm' => 'lm',
|
14
|
+
'k' => 'lk',
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize(profile)
|
18
|
+
@profile = profile
|
19
|
+
@curve_set = CurveSet.new(channels: [], profile: @profile, type: :separation)
|
20
|
+
load(@profile.quad_file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Read QTR quad (curve) file
|
24
|
+
|
25
|
+
def load(quad_file)
|
26
|
+
;;warn "reading #{quad_file}"
|
27
|
+
lines = Pathname.new(quad_file).open.readlines.map { |line| line.chomp.force_encoding('ISO-8859-1') }
|
28
|
+
# process header
|
29
|
+
channels = parse_channel_list(lines.shift)
|
30
|
+
channels.each do |channel|
|
31
|
+
samples = (0..255).to_a.map do |input|
|
32
|
+
lines.shift while lines.first =~ /^#/
|
33
|
+
line = lines.shift
|
34
|
+
line =~ /^(\d+)$/ or raise "Unexpected value: #{line.inspect}"
|
35
|
+
output = $1.to_i
|
36
|
+
Sample.new(input: Color::Gray.new(k: 100 * (input / 255.0)), output: Color::Gray.new(k: 100 * (output / 65535.0)))
|
37
|
+
end
|
38
|
+
if @profile.inks.include?(channel)
|
39
|
+
@curve_set.curves << Curve.new(channel: channel, samples: samples)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_channel_list(line)
|
45
|
+
# "## QuadToneRIP K,C,M,Y,LC,LM"
|
46
|
+
# "## QuadToneRIP KCMY"
|
47
|
+
line =~ /^##\s+QuadToneRIP\s+(.*)$/ or raise "Unexpected header line: #{line.inspect}"
|
48
|
+
channel_list = $1
|
49
|
+
case channel_list
|
50
|
+
when /,/
|
51
|
+
channel_list.split(',')
|
52
|
+
else
|
53
|
+
channel_list.chars.map { |c| ChannelAliases[c] }
|
54
|
+
end.map { |c| c.downcase.to_sym }
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Quadtone
|
2
|
+
|
3
|
+
class Renderer
|
4
|
+
|
5
|
+
attr_accessor :gamma
|
6
|
+
attr_accessor :rotate
|
7
|
+
attr_accessor :compress
|
8
|
+
attr_accessor :page_size
|
9
|
+
attr_accessor :resolution
|
10
|
+
attr_accessor :desired_size
|
11
|
+
|
12
|
+
def initialize(params={})
|
13
|
+
@compress = true
|
14
|
+
@resolution = 720
|
15
|
+
params.each { |k, v| send("#{k}=", v) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def render(input_path)
|
19
|
+
@input_path = input_path
|
20
|
+
|
21
|
+
raise "Page size required" unless @page_size
|
22
|
+
|
23
|
+
# Scale measurements to specified resolution
|
24
|
+
|
25
|
+
@page_size.width = (@page_size.width / 72.0 * @resolution).to_i
|
26
|
+
@page_size.height = (@page_size.height / 72.0 * @resolution).to_i
|
27
|
+
@page_size.imageable_width = (@page_size.imageable_width / 72.0 * @resolution).to_i
|
28
|
+
@page_size.imageable_height = (@page_size.imageable_height / 72.0 * @resolution).to_i
|
29
|
+
@page_size.margin.left = (@page_size.margin.left / 72.0 * @resolution).to_i
|
30
|
+
@page_size.margin.right = (@page_size.margin.right / 72.0 * @resolution).to_i
|
31
|
+
@page_size.margin.top = (@page_size.margin.top / 72.0 * @resolution).to_i
|
32
|
+
@page_size.margin.bottom = (@page_size.margin.bottom / 72.0 * @resolution).to_i
|
33
|
+
|
34
|
+
if @desired_size
|
35
|
+
@desired_size.width = (@desired_size.width * @resolution).to_i
|
36
|
+
@desired_size.height = (@desired_size.height * @resolution).to_i
|
37
|
+
if @desired_size.width > @page_size.imageable_width || @desired_size.height > @page_size.imageable_height
|
38
|
+
raise "Image too large for page size (#{@page_size.name})"
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@desired_size = HashStruct.new(width: @page_size.imageable_width, height: @page_size.imageable_height)
|
42
|
+
end
|
43
|
+
|
44
|
+
;;warn "Reading #{@input_path} @ #{@resolution}dpi"
|
45
|
+
r = @resolution # have to alias to avoid referring to ImageList object
|
46
|
+
image_list = Magick::ImageList.new(@input_path) {
|
47
|
+
self.density = r
|
48
|
+
}
|
49
|
+
output_paths = []
|
50
|
+
image_list.each_with_index do |image, image_index|
|
51
|
+
@current_image = image
|
52
|
+
@current_image_index = image_index
|
53
|
+
;;warn "\t" + "Processing sub-image \##{@current_image_index}"
|
54
|
+
show_info
|
55
|
+
delete_profiles
|
56
|
+
convert_to_16bit
|
57
|
+
apply_gamma
|
58
|
+
rotate
|
59
|
+
resize
|
60
|
+
extend_to_page
|
61
|
+
crop_to_imageable_area
|
62
|
+
show_info
|
63
|
+
output_paths << write_to_file
|
64
|
+
end
|
65
|
+
output_paths
|
66
|
+
end
|
67
|
+
|
68
|
+
def output_path
|
69
|
+
params = []
|
70
|
+
params << "#{@desired_size.width}x#{@desired_size.height}"
|
71
|
+
params << @page_size.name
|
72
|
+
params << "@#{@resolution}"
|
73
|
+
params << "g#{@gamma}" if @gamma
|
74
|
+
@input_path.with_extname(".out-#{params.join('-')}.#{@current_image_index}.png")
|
75
|
+
end
|
76
|
+
|
77
|
+
def show_info
|
78
|
+
;;warn "\t\t" + @current_image.inspect
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete_profiles
|
82
|
+
;;warn "\t\t" + "Deleting profiles"
|
83
|
+
@current_image.delete_profile('*')
|
84
|
+
end
|
85
|
+
|
86
|
+
def convert_to_16bit
|
87
|
+
;;warn "\t\t" + "Changing to grayscale"
|
88
|
+
@current_image = @current_image.quantize(2 ** 16, Magick::GRAYColorspace)
|
89
|
+
end
|
90
|
+
|
91
|
+
def apply_gamma
|
92
|
+
if @gamma
|
93
|
+
;;warn "\t\t" + "Applying gamma #{@gamma}"
|
94
|
+
@current_image = @current_image.gamma_correct(@gamma)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def rotate
|
99
|
+
if @rotate
|
100
|
+
;;warn "\t\t" + "Rotating #{@rotate}°"
|
101
|
+
@current_image.rotate!(@rotate)
|
102
|
+
elsif (@current_image.columns.to_f / @current_image.rows) > (@page_size.width.to_f / @page_size.height)
|
103
|
+
;;warn "\t\t" + "Auto-rotating 90°"
|
104
|
+
@current_image.rotate!(90)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def resize
|
109
|
+
;;warn "\t\t" + "Resizing to desired size"
|
110
|
+
@current_image.resize_to_fit!(@desired_size.width, @desired_size.height)
|
111
|
+
end
|
112
|
+
|
113
|
+
def extend_to_page
|
114
|
+
;;warn "\t\t" + "Extending canvas to page area"
|
115
|
+
@current_image = @current_image.extent(
|
116
|
+
@page_size.width,
|
117
|
+
@page_size.height,
|
118
|
+
-(@page_size.width - @current_image.columns) / 2,
|
119
|
+
-(@page_size.height - @current_image.rows) / 2)
|
120
|
+
end
|
121
|
+
|
122
|
+
def crop_to_imageable_area
|
123
|
+
x, y, w, h = @page_size.margin.left, @page_size.height - @page_size.margin.top, @page_size.imageable_width, @page_size.imageable_height
|
124
|
+
;;warn "\t\t" + "Cropping to imageable area (x,y = #{x},#{y}, w,h = #{w},#{h})"
|
125
|
+
@current_image.crop!(x, y, w, h)
|
126
|
+
end
|
127
|
+
|
128
|
+
def write_to_file
|
129
|
+
path = output_path
|
130
|
+
;;warn "\t\t" + "Writing image to #{path}"
|
131
|
+
@current_image.write(path) do
|
132
|
+
self.compression = @compress ? Magick::ZipCompression : Magick::NoCompression
|
133
|
+
end
|
134
|
+
path
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
data/lib/quadtone/run.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Quadtone
|
2
|
+
|
3
|
+
class Sample
|
4
|
+
|
5
|
+
attr_accessor :input
|
6
|
+
attr_accessor :output
|
7
|
+
attr_accessor :error
|
8
|
+
attr_accessor :label
|
9
|
+
|
10
|
+
def initialize(params={})
|
11
|
+
params.each { |key, value| send("#{key}=", value) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def input_value
|
15
|
+
@input.value
|
16
|
+
end
|
17
|
+
|
18
|
+
def output_value
|
19
|
+
@output.value
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"%s / %s%s" % [
|
24
|
+
input,
|
25
|
+
output,
|
26
|
+
label ? " [#{label}]" : '',
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Quadtone
|
2
|
+
|
3
|
+
class Separator
|
4
|
+
|
5
|
+
attr_accessor :luts
|
6
|
+
|
7
|
+
def initialize(curve_set)
|
8
|
+
@luts = {}
|
9
|
+
curve_set.curves.each do |curve|
|
10
|
+
color_map = Magick::Image.new(curve.num_samples, 1) do
|
11
|
+
self.colorspace = Magick::GRAYColorspace
|
12
|
+
end
|
13
|
+
color_map.pixel_interpolation_method = Magick::IntegerInterpolatePixel
|
14
|
+
color_map.view(0, 0, curve.num_samples, 1) do |view|
|
15
|
+
curve.samples.each_with_index do |sample, i|
|
16
|
+
value = ((1 - sample.output.value) * 65535).to_i
|
17
|
+
view[0][curve.num_samples - 1 - i] = Magick::Pixel.new(value, value, value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
@luts[curve.channel] = color_map
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def separate(image)
|
25
|
+
images = {}
|
26
|
+
@luts.each do |channel, lut|
|
27
|
+
image2 = image.copy.clut_channel(lut)
|
28
|
+
image2['Label'] = channel.to_s.upcase
|
29
|
+
images[channel] = image2
|
30
|
+
end
|
31
|
+
images
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
module Quadtone
|
2
|
+
|
3
|
+
class Target
|
4
|
+
|
5
|
+
attr_accessor :base_dir
|
6
|
+
attr_accessor :channels
|
7
|
+
attr_accessor :type
|
8
|
+
attr_accessor :name
|
9
|
+
attr_accessor :ink_limits
|
10
|
+
attr_accessor :samples
|
11
|
+
|
12
|
+
def initialize(params={})
|
13
|
+
params.each { |k, v| send("#{k}=", v) }
|
14
|
+
raise "Base directory must be specified" unless @base_dir
|
15
|
+
raise "Channels must be specified" unless @channels
|
16
|
+
raise "Type must be specified" unless @type
|
17
|
+
raise "Name must be specified" unless @name
|
18
|
+
end
|
19
|
+
|
20
|
+
def build
|
21
|
+
;;warn "Making target for channels #{@channels.inspect}"
|
22
|
+
cleanup_files(:all)
|
23
|
+
|
24
|
+
resolution = 360
|
25
|
+
page_size = HashStruct.new(width: 8, height: 10.5)
|
26
|
+
# total_patches = 42
|
27
|
+
;;total_patches = 14
|
28
|
+
patches_per_row = 14
|
29
|
+
total_rows = total_patches / patches_per_row
|
30
|
+
row_height = 165
|
31
|
+
target_size = [
|
32
|
+
((total_rows + 1) * row_height).to_f / resolution,
|
33
|
+
page_size.height,
|
34
|
+
].map { |n| (n * 25.4).to_i }.join('x')
|
35
|
+
|
36
|
+
image_list = Magick::ImageList.new
|
37
|
+
@channels.each do |channel|
|
38
|
+
sub_path = base_file(channel)
|
39
|
+
sub_image_path = image_file(channel)
|
40
|
+
;;warn "Making target #{sub_path.inspect} at #{sub_image_path}"
|
41
|
+
Quadtone.run('targen',
|
42
|
+
# '-v', # Verbose mode [optional level 1..N]
|
43
|
+
'-d', 0, # generate grayscale target
|
44
|
+
'-e', 0, # White test patches (default 4)
|
45
|
+
'-B', 0, # Black test patches (default 4 Grey/RGB, else 0)
|
46
|
+
'-s', total_patches, # Single channel steps (default grey 50, color 0)
|
47
|
+
sub_path)
|
48
|
+
Quadtone.run('printtarg',
|
49
|
+
# '-v', # Verbose mode [optional level 1..N]
|
50
|
+
'-a', 1.45, # Scale patch size and spacers by factor (e.g. 0.857 or 1.5 etc.)
|
51
|
+
'-r', # Don't randomize patch location
|
52
|
+
'-i', 'i1', # set instrument to EyeOne (FIXME: make configurable)
|
53
|
+
'-t', resolution, # generate 16-bit TIFF @ 360 ppi
|
54
|
+
'-m', 0, # Set a page margin in mm (default 6.0 mm)
|
55
|
+
'-L', # Suppress any left paper clip border
|
56
|
+
'-p', target_size, # Select page size
|
57
|
+
sub_path)
|
58
|
+
image = Magick::Image.read(sub_image_path).first
|
59
|
+
# image.background_color = 'transparent'
|
60
|
+
# image = image.transparent('white')
|
61
|
+
case @type
|
62
|
+
when :characterization
|
63
|
+
if @ink_limits && (limit = @ink_limits[channel]) && limit != 1
|
64
|
+
;;warn "\t" + "#{channel.to_s.upcase}: Applying limit of #{limit}"
|
65
|
+
levels = [1.0 - limit, 1.0].map { |n| n * Magick::QuantumRange }
|
66
|
+
image = image.levelize_channel(*levels)
|
67
|
+
end
|
68
|
+
# calculate a black RGB pixel for this channel in QTR calibration mode
|
69
|
+
black_qtr = Color::QTR.new(channel: channel, value: 0)
|
70
|
+
black_rgb = black_qtr.to_rgb
|
71
|
+
;;warn "\t" + "#{channel.to_s.upcase}: Colorizing to #{black_rgb}"
|
72
|
+
image = image.colorize(1, 0, 1, black_rgb.to_pixel)
|
73
|
+
end
|
74
|
+
image_list << image
|
75
|
+
end
|
76
|
+
|
77
|
+
if @type == :linearization || @type == :test
|
78
|
+
|
79
|
+
width = (page_size.width * resolution).to_i - image_list.first.columns
|
80
|
+
height = page_size.height * resolution
|
81
|
+
|
82
|
+
;;width = (page_size.width / 2) * resolution
|
83
|
+
|
84
|
+
linear_scale_height = 1 * resolution
|
85
|
+
radial_scale_height = (height - linear_scale_height) / 2
|
86
|
+
sample_image_height = (height - linear_scale_height) / 2
|
87
|
+
|
88
|
+
test_images = Magick::ImageList.new
|
89
|
+
test_images << linear_gradation_scale_image([width, linear_scale_height])
|
90
|
+
test_images << radial_gradation_scale_image([width, radial_scale_height])
|
91
|
+
test_images << sample_image([width, sample_image_height])
|
92
|
+
image_list << test_images.append(true)
|
93
|
+
end
|
94
|
+
|
95
|
+
# ;;warn "montaging images"
|
96
|
+
# image_list = image_list.montage do
|
97
|
+
# self.geometry = Magick::Geometry.new(page_size.width * resolution, page_size.height * resolution)
|
98
|
+
# self.tile = Magick::Geometry.new(image_list.length, 1)
|
99
|
+
# end
|
100
|
+
|
101
|
+
# ;;warn "writing target image"
|
102
|
+
# image_list.write(image_file) do
|
103
|
+
# self.depth = (@type == :characterization) ? 8 : 16
|
104
|
+
# self.compression = Magick::ZipCompression
|
105
|
+
# end
|
106
|
+
|
107
|
+
;;warn "writing target image"
|
108
|
+
image_list.append(false).write(image_file) do
|
109
|
+
self.depth = (@type == :characterization) ? 8 : 16
|
110
|
+
self.compression = Magick::ZipCompression
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
def linear_gradation_scale_image(size)
|
116
|
+
;;warn "\t" + "generating linear gradation scale of size #{size.inspect}"
|
117
|
+
bounds = Magick::Rectangle.new(*size, 0, 0)
|
118
|
+
image1 = Magick::Image.new(bounds.width, bounds.height/2, Magick::GradientFill.new(0, 0, 0, bounds.height/2, 'white', 'black'))
|
119
|
+
image2 = image1.posterize(21)
|
120
|
+
image3 = image1.posterize(256)
|
121
|
+
ilist = Magick::ImageList.new
|
122
|
+
ilist << image1
|
123
|
+
ilist << image2
|
124
|
+
ilist << image3
|
125
|
+
ilist.append(true)
|
126
|
+
end
|
127
|
+
|
128
|
+
def radial_gradation_scale_image(size)
|
129
|
+
;;warn "\t" + "generating radial gradation scale of size #{size.inspect}"
|
130
|
+
bounds = Magick::Rectangle.new(*size, 0, 0)
|
131
|
+
image1 = Magick::Image.new(bounds.width, bounds.height/2, Magick::GradientFill.new(bounds.width/2, bounds.height/2, bounds.width/2, bounds.height/2, 'black', 'white'))
|
132
|
+
image2 = image1.posterize(21).flip
|
133
|
+
ilist = Magick::ImageList.new
|
134
|
+
ilist << image1
|
135
|
+
ilist << image2
|
136
|
+
ilist.append(true)
|
137
|
+
end
|
138
|
+
|
139
|
+
def sample_image(size)
|
140
|
+
;;warn "\t" + "generating sample image of size #{size.inspect}"
|
141
|
+
bounds = Magick::Rectangle.new(*size, 0, 0)
|
142
|
+
ilist = Magick::ImageList.new(Pathname.new(ENV['HOME']) + 'Desktop' + '121213b.01.tif')
|
143
|
+
ilist.first.resize_to_fill(*size)
|
144
|
+
end
|
145
|
+
|
146
|
+
def measure(options={})
|
147
|
+
options = HashStruct.new(options)
|
148
|
+
channels_to_measure = options.channels || @channels
|
149
|
+
channels_to_measure.each_with_index do |channel, i|
|
150
|
+
measure_channel(channel, options.merge(disable_calibration: i > 0))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def measure_channel(channel, options=HashStruct.new)
|
155
|
+
options = HashStruct.new(options)
|
156
|
+
if options.remeasure
|
157
|
+
pass = options.remeasure
|
158
|
+
else
|
159
|
+
pass = ti2_files(channel).length
|
160
|
+
end
|
161
|
+
base = base_file(channel, pass)
|
162
|
+
FileUtils.cp(ti2_file(channel), base.with_extname('.ti2')) unless options.remeasure
|
163
|
+
;;warn "Measuring target #{base.inspect}"
|
164
|
+
Quadtone.run('chartread',
|
165
|
+
# '-v', # Verbose mode [optional level 1..N]
|
166
|
+
'-p', # Measure patch by patch rather than strip
|
167
|
+
'-n', # Don't save spectral information (default saves spectral)
|
168
|
+
'-l', # Save CIE as D50 L*a*b* rather than XYZ
|
169
|
+
options.disable_calibration ? '-N' : nil, # Disable initial calibration of instrument if possible
|
170
|
+
options.remeasure ? '-r' : nil, # Resume reading partly read chart
|
171
|
+
base)
|
172
|
+
end
|
173
|
+
|
174
|
+
def read
|
175
|
+
;;warn "reading samples for #{@channels.join(', ')} from CGATS files"
|
176
|
+
@samples = {}
|
177
|
+
@channels.each do |channel|
|
178
|
+
samples = {}
|
179
|
+
ti3_files(channel).map do |file|
|
180
|
+
;;warn "reading #{file}"
|
181
|
+
cgats = CGATS.new_from_file(file)
|
182
|
+
cgats.sections.first.data.each do |set|
|
183
|
+
id = set['SAMPLE_LOC'].gsub(/"/, '')
|
184
|
+
samples[id] ||= []
|
185
|
+
samples[id] << Sample.new(input: Color::Gray.from_cgats(set), output: Color::Lab.from_cgats(set))
|
186
|
+
end
|
187
|
+
end
|
188
|
+
@samples[channel] = samples.map do |id, samples|
|
189
|
+
cc = ClusterCalculator.new(samples: samples, max_clusters: samples.length > 2 ? 2 : 1)
|
190
|
+
cc.cluster!
|
191
|
+
clusters = cc.clusters.sort_by(&:size).reverse
|
192
|
+
# ;;warn "Clusters:"
|
193
|
+
# clusters.each do |cluster|
|
194
|
+
# warn "\t" + cluster.center.to_s
|
195
|
+
# cluster.samples.each do |sample|
|
196
|
+
# warn "\t\t" + "#{sample.to_s}"
|
197
|
+
# end
|
198
|
+
# end
|
199
|
+
# ;;
|
200
|
+
cluster = clusters.shift
|
201
|
+
raise "Too much spread" if cluster.samples.length < 2 && samples.length > 2
|
202
|
+
unless clusters.empty?
|
203
|
+
warn "Dropped #{clusters.length} out of range sample(s) at patch #{channel}-#{id}"
|
204
|
+
end
|
205
|
+
output = cluster.center
|
206
|
+
Sample.new(input: samples.first.input, output: output, label: "#{channel}-#{id}")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# private
|
212
|
+
|
213
|
+
def base_file(channel=nil, n=nil)
|
214
|
+
@base_dir + (@name.to_s + (channel ? "-#{channel}" : '') + (n ? "-#{n}" : ''))
|
215
|
+
end
|
216
|
+
|
217
|
+
def ti1_file(channel)
|
218
|
+
base_file(channel).with_extname('.ti1')
|
219
|
+
end
|
220
|
+
|
221
|
+
def ti2_file(channel, n=nil)
|
222
|
+
base_file(channel, n).with_extname('.ti2')
|
223
|
+
end
|
224
|
+
|
225
|
+
def ti3_file(channel, n=nil)
|
226
|
+
base_file(channel, n).with_extname('.ti3')
|
227
|
+
end
|
228
|
+
|
229
|
+
def image_file(channel=nil)
|
230
|
+
base_file(channel).with_extname('.tif')
|
231
|
+
end
|
232
|
+
|
233
|
+
def values_file(channel=nil)
|
234
|
+
base_file(channel).with_extname('.txt')
|
235
|
+
end
|
236
|
+
|
237
|
+
def ti_files
|
238
|
+
Pathname.glob(base_file.with_extname('*.ti[123]'))
|
239
|
+
end
|
240
|
+
|
241
|
+
def ti2_files(channel)
|
242
|
+
Pathname.glob(base_file(channel).with_extname('*.ti2'))
|
243
|
+
end
|
244
|
+
|
245
|
+
def ti3_files(channel)
|
246
|
+
Pathname.glob(base_file(channel).with_extname('*.ti3'))
|
247
|
+
end
|
248
|
+
|
249
|
+
def image_files
|
250
|
+
Pathname.glob(base_file.with_extname('*.tif'))
|
251
|
+
end
|
252
|
+
|
253
|
+
def cleanup_files(files)
|
254
|
+
;;warn "deleting files: #{files.inspect}"
|
255
|
+
files = [files].flatten
|
256
|
+
until files.empty?
|
257
|
+
file = files.shift
|
258
|
+
case file
|
259
|
+
when :all
|
260
|
+
files << :ti
|
261
|
+
files += image_files
|
262
|
+
when :ti
|
263
|
+
files += ti_files
|
264
|
+
when Pathname
|
265
|
+
if file.exist?
|
266
|
+
# ;;warn "\t" + file
|
267
|
+
file.unlink
|
268
|
+
end
|
269
|
+
else
|
270
|
+
raise "Unknown file to cleanup: #{file}"
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|