Color to color
Image-to-image color space conversion and tone mapping.
Summary:
usage: tcio-color2color [-h] [--tmcs ] [--igf ] [--ogf ] [--tonemapper ] [--keep-alpha]
input input-color-space output output-color-space
Convert the color space an image, with an optional tone mapping stage.
positional arguments:
input Input image file path
input-color-space Input color space
CHOICES:
cie_xyz, cie_xyy, srgb, srgb_lin,
rec709, rec2020, rec_2020_lin,
dci_p3, dci_p3_lin, display_p3,
acescg, acescc, aces2065_1,
lms, hsl, hsv, oklab, cielab
output Output image file path
output-color-space Output color space
CHOICES: [same as above]
optional arguments:
-h, --help show this help message and exit
--tmcs [] Tone mapping color space (default: srgb_lin)
CHOICES:
srgb_lin, rec2020_lin
dci_p3_lin, acescg, aces2065_1
--igf [] Input graphics format (default: unknown)
CHOICES:
sfloat16, sfloat32
uint8, uint16, uint32
--ogf [] Output graphics format (default: unknown)
CHOICES: [same as above]
--tonemapper [], -t []
Tone mapper (default: none)
CHOICES:
none, clamp, agx, agx_punchy
acescg, hable, reinhard
--keep-alpha, -a Preserve alpha channel
Example usage:
$ tcio-color2color -t agx input.tif cie_xyz output.png srgb
More explicit example:
$ tcio-color2color --igf uint8 --ogf sfloat16 --keep-alpha input.png srgb output.tif aces2065_1
Also, note that the ACEScg RRT+ODT will not be applied automatically, so in this case “acescg” needs to be repeated - once for the “output color space” and once for the “tonemapper” option:
$ tcio-color2color -t acescg input.exr acescg output.png srgb
Script:
#!/usr/bin/env python
"""Convert the color space an image, with an optional tone mapping stage."""
import os
import argparse
from argparse import RawTextHelpFormatter
import torch
from tinycio import ColorImage, ColorSpace, ToneMapping, fsio
def main_cli():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('input', type=str, help='Input image file path')
parser.add_argument(
'ics',
type=str,
choices=[
'cie_xyz', 'cie_xyy', 'srgb', 'srgb_lin',
'rec709', 'rec2020', 'rec2020_lin',
'dci_p3', 'dci_p3_lin', 'display_p3',
'acescg', 'aces2065_1', 'acescc', 'lms',
'hsl', 'hsv', 'oklab', 'cielab'],
metavar='input-color-space', # because bad formatting is bad :(
help='Input color space\n' + \
'CHOICES:\n' + \
' cie_xyz, cie_xyy, srgb, srgb_lin, \n' + \
' rec709, rec2020, rec_2020_lin, \n' + \
' dci_p3, dci_p3_lin, display_p3, \n' + \
' acescg, acescc, aces2065_1, \n' + \
' lms, hsl, hsv, oklab, cielab'
)
parser.add_argument('output', type=str, help='Output image file path')
parser.add_argument(
'ocs',
type=str,
choices=[
'cie_xyz', 'cie_xyy', 'srgb', 'srgb_lin',
'rec709', 'rec2020', 'rec2020_lin',
'dci_p3', 'dci_p3_lin', 'display_p3',
'acescg', 'aces2065_1', 'acescc', 'lms',
'hsl', 'hsv', 'oklab', 'cielab'],
metavar='output-color-space',
help='Output color space\n' + \
'CHOICES: [same as above]'
)
parser.add_argument(
'--tmcs',
type=str,
default="srgb_lin",
const="srgb_lin",
nargs='?',
choices=['srgb_lin', 'rec2020_lin', 'dci_p3_lin', 'acescg', 'aces2065_1'],
metavar='',
help='Tone mapping color space (default: %(default)s)\n' + \
'CHOICES:\n' + \
' srgb_lin, rec2020_lin \n' + \
' dci_p3_lin, acescg, aces2065_1 \n'
)
parser.add_argument(
'--igf',
type=str,
default="unknown",
const="unknown",
nargs='?',
choices=['sfloat16','sfloat32','uint8','uint16','uint32'],
metavar='',
help='Input graphics format (default: %(default)s)\n' + \
'CHOICES:\n' + \
' sfloat16, sfloat32 \n' + \
' uint8, uint16, uint32 \n'
)
parser.add_argument(
'--ogf',
type=str,
default="unknown",
const="unknown",
nargs='?',
choices=['sfloat16','sfloat32','uint8','uint16','uint32'],
metavar='',
help='Output graphics format (default: %(default)s)\n' + \
'CHOICES: [same as above]'
)
parser.add_argument(
'--tonemapper',
'-t',
type=str,
default="none",
const="unknown",
nargs='?',
choices=[
'none', 'clamp', 'agx', 'agx_punchy',
'acescg', 'hable', 'reinhard'],
metavar='',
help='Tone mapper (default: %(default)s)\n' + \
'CHOICES:\n' + \
' none, clamp, agx, agx_punchy \n' + \
' acescg, hable, reinhard'
)
parser.add_argument('--keep-alpha', '-a', action='store_true', help='Preserve alpha channel')
args = parser.parse_args()
fp_in = os.path.realpath(args.input)
fp_out = os.path.realpath(args.output)
gf_in = args.igf.strip().upper()
gf_out = args.ogf.strip().upper()
cs_in = args.ics.strip().upper()
cs_out = args.ocs.strip().upper()
tm = args.tonemapper.strip().upper()
alpha = args.keep_alpha
try:
im, ac = None, None
if alpha:
im = fsio.load_image(fp_in, graphics_format=fsio.GraphicsFormat[gf_in])
ac = im[3:4,...] if im.size(0) == 4 else None
im = ColorImage(fsio.truncate_image(im), color_space=cs_in)
else:
im = ColorImage.load(fp_in, color_space=cs_in, graphics_format=gf_in)
im = im.tone_map(tm, target_color_space=args.tmcs) # it's okay to pass "NONE"
im = im.to_color_space(cs_out)
if ac is not None: im = torch.cat([im, ac], dim=0)
im.save(fp_out, gf_out)
print(f'saved image to: {os.path.realpath(fp_out)}')
except Exception as e:
print(f'cannot convert: {e}')
if __name__ == '__main__':
main_cli()