libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

demo_progressive_saliency_encoding.py (6944B)


      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()