3 # Copyright (c) the JPEG XL Project Authors. All rights reserved.
5 # Use of this source code is governed by a BSD-style
6 # license that can be found in the LICENSE file.
8 """Produces demos for how progressive-saliency encoding would look like.
10 As long as we do not have a progressive decoder that allows showing images
11 generated from partially-available data, we can resort to building
12 animated gifs that show how progressive loading would look like.
16 1. JPEG-XL encode the image, but stop at the pre-final (2nd) step.
17 2. Use separate tool to compute a heatmap which shows where differences between
18 the pre-final and final image are expected to be perceptually worst.
19 3. Use this heatmap to JPEG-XL encode the image with the final step split into
20 'salient parts only' and 'non-salient parts'. Generate a sequence of images
21 that stop decoding after the 1st, 2nd, 3rd, 4th step. JPEG-XL decode these
22 truncated images back to PNG.
23 4. Measure byte sizes of the truncated-encoded images.
24 5. Build an animated GIF with variable delays by calling ImageMagick's
29 from __future__ import absolute_import
30 from __future__ import division
31 from __future__ import print_function
32 from six.moves import zip
33 import ast # For ast.literal_eval() only.
43 keep_tempfiles=lambda s: bool(ast.literal_eval(s)),
44 heatmap_command=shlex.split,
45 simulated_progressive_loading_time_sec=float,
46 simulated_progressive_loading_delay_until_looparound_sec=float,
47 jpegxl_encoder=shlex.split,
48 jpegxl_decoder=shlex.split,
49 blurring=lambda s: s.split(),
53 def parse_config(config_filename):
54 """Parses the configuration file."""
56 re_comment = re.compile(r'^\s*(?:#.*)?$')
57 re_param = re.compile(r'^(?P<option>\w+)\s*:\s*(?P<value>.*?)\s*$')
59 with open(config_filename) as h:
61 if re_comment.match(line):
63 m = re_param.match(line)
65 raise ValueError('Syntax error')
66 conf[m.group('option')] = (
67 _CONF_PARSERS[m.group('option')](m.group('value')))
68 except Exception as exn:
69 raise ValueError('Bad Configuration line ({}): {}'.format(exn, line))
70 missing_options = set(_CONF_PARSERS) - set(conf)
72 raise ValueError('Missing configuration options: ' + ', '.join(
73 sorted(missing_options)))
77 def generate_demo_image(config, input_filename, output_filename):
80 def encode_img(input_filename, output_filename, num_steps,
81 heatmap_filename=None):
83 '${INPUT}': input_filename,
84 '${OUTPUT}': output_filename,
85 '${STEPS}': str(num_steps),
86 # Heatmap argument will be provided in --param=value form.
87 '${HEATMAP_ARG}': ('--saliency_map_filename=' + heatmap_filename
88 if heatmap_filename is not None else '')
90 # Remove empty args. This removes the heatmap-argument if no heatmap
94 [replacements.get(arg, arg) for arg in config['jpegxl_encoder']] if _f
96 tempfiles.append(output_filename)
99 def decode_img(input_filename, output_filename):
100 replacements = {'${INPUT}': input_filename, '${OUTPUT}': output_filename}
101 cmd = [replacements.get(arg, arg) for arg in config['jpegxl_decoder']]
102 tempfiles.append(output_filename)
105 def generate_heatmap(orig_image_filename, coarse_grained_filename,
107 cmd = config['heatmap_command'] + [
108 str(_BLOCKSIZE), orig_image_filename, coarse_grained_filename,
110 tempfiles.append(heatmap_filename)
114 encode_img(input_filename, output_filename + '._step1.pik', 1)
115 decode_img(output_filename + '._step1.pik', output_filename + '._step1.png')
116 encode_img(input_filename, output_filename + '._step2.pik', 2)
117 decode_img(output_filename + '._step2.pik', output_filename + '._step2.png')
118 generate_heatmap(input_filename, output_filename + '._step2.png',
119 output_filename + '._heatmap.png')
120 encode_img(input_filename,
121 output_filename + '._step3.pik', 3,
122 output_filename + '._heatmap.png')
123 encode_img(input_filename,
124 output_filename + '._step4.pik', 4,
125 output_filename + '._heatmap.png')
126 decode_img(output_filename + '._step3.pik', output_filename + '._step3.png')
127 decode_img(output_filename + '._step4.pik', output_filename + '._step4.png')
129 os.stat('{}._step{}.pik'.format(output_filename, num_step)).st_size
130 for num_step in (1, 2, 3, 4)]
131 time_offsets = [0] + [
132 # Imagemagick's `convert` accepts delays in units of 1/100 sec.
133 round(100 * config['simulated_progressive_loading_time_sec'] * size /
134 data_sizes[-1]) for size in data_sizes]
135 time_delays = [t_next - t_prev
136 for t_next, t_prev in zip(time_offsets[1:], time_offsets)]
137 # Add a fake white initial image. As long as no usable image data is
138 # available, the user will see a white background.
139 subprocess.call(['convert',
140 output_filename + '._step1.png',
141 '-fill', 'white', '-colorize', '100%',
142 output_filename + '._step0.png'])
143 tempfiles.append(output_filename + '._step0.png')
145 ['convert', '-loop', '0', output_filename + '._step0.png'] +
147 ['-delay', str(time_delays[n - 1]),
148 '-blur', config['blurring'][n - 1],
149 '{}._step{}.png'.format(output_filename, n)]
150 for n in (1, 2, 3, 4)] for arg in args] +
151 ['-delay', str(round(100 * config[
152 'simulated_progressive_loading_delay_until_looparound_sec'])),
153 output_filename + '._step4.png',
156 if not config['keep_tempfiles']:
157 for filename in tempfiles:
161 pass # May already have been deleted otherwise.
165 if sys.version.startswith('2.'):
166 sys.exit('This is a python3-only script.')
167 if (len(sys.argv) != 4 or not sys.argv[-1].endswith('.gif')
168 or not sys.argv[-2].endswith('.png')):
170 'Usage: {} [config_options_file] [input.png] [output.gif]'.format(
173 _, config_filename, input_filename, output_filename = sys.argv
174 config = parse_config(config_filename)
175 generate_demo_image(config, input_filename, output_filename)
176 except ValueError as exn:
181 if __name__ == '__main__':