Imported Upstream version 0.7.0
[platform/upstream/libjxl.git] / tools / scripts / demo_progressive_saliency_encoding.py
1 #!/usr/bin/env python3
2
3 # Copyright (c) the JPEG XL Project Authors. All rights reserved.
4 #
5 # Use of this source code is governed by a BSD-style
6 # license that can be found in the LICENSE file.
7
8 """Produces demos for how progressive-saliency encoding would look like.
9
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.
13
14 Method:
15
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
25    `convert` command.
26
27 """
28
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.
34 import os
35 import re
36 import shlex
37 import subprocess
38 import sys
39
40 _BLOCKSIZE = 8
41
42 _CONF_PARSERS = dict(
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(),
50 )
51
52
53 def parse_config(config_filename):
54   """Parses the configuration file."""
55   conf = {}
56   re_comment = re.compile(r'^\s*(?:#.*)?$')
57   re_param = re.compile(r'^(?P<option>\w+)\s*:\s*(?P<value>.*?)\s*$')
58   try:
59     with open(config_filename) as h:
60       for line in h:
61         if re_comment.match(line):
62           continue
63         m = re_param.match(line)
64         if not m:
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)
71   if missing_options:
72     raise ValueError('Missing configuration options: ' + ', '.join(
73         sorted(missing_options)))
74   return conf
75
76
77 def generate_demo_image(config, input_filename, output_filename):
78   tempfiles = []
79   #
80   def encode_img(input_filename, output_filename, num_steps,
81                  heatmap_filename=None):
82     replacements = {
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 '')
89         }
90     # Remove empty args. This removes the heatmap-argument if no heatmap
91     # is provided..
92     cmd = [
93         _f for _f in
94         [replacements.get(arg, arg) for arg in config['jpegxl_encoder']] if _f
95     ]
96     tempfiles.append(output_filename)
97     subprocess.call(cmd)
98   #
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)
103     subprocess.call(cmd)
104   #
105   def generate_heatmap(orig_image_filename, coarse_grained_filename,
106                        heatmap_filename):
107     cmd = config['heatmap_command'] + [
108         str(_BLOCKSIZE), orig_image_filename, coarse_grained_filename,
109         heatmap_filename]
110     tempfiles.append(heatmap_filename)
111     subprocess.call(cmd)
112   #
113   try:
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')
128     data_sizes = [
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')
144     subprocess.call(
145         ['convert', '-loop', '0', output_filename + '._step0.png'] +
146         [arg for args in [
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',
154          output_filename])
155   finally:
156     if not config['keep_tempfiles']:
157       for filename in tempfiles:
158         try:
159           os.unlink(filename)
160         except OSError:
161           pass  # May already have been deleted otherwise.
162
163
164 def main():
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')):
169     sys.exit(
170         'Usage: {} [config_options_file] [input.png] [output.gif]'.format(
171             sys.argv[0]))
172   try:
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:
177     sys.exit(exn)
178
179
180
181 if __name__ == '__main__':
182   main()