diff --git a/src/apps/pyocioamf/README.md b/src/apps/pyocioamf/README.md
index 4a66cdd1a6..36993d87dd 100644
--- a/src/apps/pyocioamf/README.md
+++ b/src/apps/pyocioamf/README.md
@@ -4,20 +4,47 @@
pyocioamf
=========
-**Work in progress**. Script to convert an ACES Metadata File (AMF) into OCIO's
-native CTF file format. The CTF may then be used with any of the other OCIO tools
-to process pixels.
+Script to convert an ACES Metadata File (AMF) into OCIO's native CTF file format.
+The CTF may then be used with any of the other OCIO tools to process pixels.
-An example AMF file named ``example.amf`` is provided. It references the LUT file
-``example_referenced_lut.clf``, which is in the Academy/ASC Common LUT Format (CLF).
+**Supports both AMF v1.0 and v2.0** with automatic version detection.
+
+Example files are provided:
+- ``example.amf`` - AMF v1.0 format, references ``example_referenced_lut.clf``
+- ``example_v2.amf`` - AMF v2.0 format
+
+The referenced LUT file is in the Academy/ASC Common LUT Format (CLF).
Usage
-----
-1. Install dependencies on ``PYTHONPATH``
-2. Run ``python pyocioamf.py example.amf`` (or any other AMF file).
-3. The CTF file will have the same name as the AMF file but with a .ctf extension, in
-this case, ``example.ctf``.
+Basic usage:
+
+```bash
+python pyocioamf.py example.amf
+```
+
+The CTF file will have the same name as the AMF file but with a .ctf extension.
+
+### Command-Line Options
+
+```bash
+# Use a specific OCIO config file
+python pyocioamf.py example.amf --config path/to/config.ocio
+
+# Exclude specific components from conversion
+python pyocioamf.py example.amf --no-idt # Exclude input transform
+python pyocioamf.py example.amf --no-lmt # Exclude look transforms
+python pyocioamf.py example.amf --no-odt # Exclude output transform
+python pyocioamf.py example.amf --no-lmt --no-odt # Exclude multiple
+
+# Split CTF generation for AMF v2.0 workingLocation marker
+python pyocioamf.py example.amf --split-by-working-location
+```
+
+The ``--split-by-working-location`` option generates two CTF files:
+- ``*_before.ctf``: IDT + look transforms before the workingLocation marker
+- ``*_after.ctf``: Look transforms after the marker + ODT
Dependencies
------------
@@ -27,6 +54,11 @@ Dependencies
Implementation notes
--------------------
+* Automatically detects AMF version (v1.0 or v2.0) from namespace URI or version attribute
+* Supports both ASC CDL element naming conventions (``SOPNode``/``SatNode`` and ``ASC_SOP``/``ASC_SAT``)
* Uses a prototype ACES config ``config-aces-reference.yaml`` to interpret the
- ACES Transform IDs encountered in the AMF file. This file should be in the
+ ACES Transform IDs encountered in the AMF file. This file should be in the
same directory as the script.
+* Alternatively, use ``--config`` to specify a custom OCIO config file
+* For OCIO 2.5+ configs, uses the ``amf_transform_ids`` interchange attribute for transform lookup
+* For OCIO 2.1-2.4 configs, searches transform IDs in the description field
diff --git a/src/apps/pyocioamf/example_v2.amf b/src/apps/pyocioamf/example_v2.amf
new file mode 100644
index 0000000000..11fb793137
--- /dev/null
+++ b/src/apps/pyocioamf/example_v2.amf
@@ -0,0 +1,71 @@
+
+
+
+ Example Movie
+
+ Foo Bar
+ Foobar@onset.com
+
+
+ 2019-09-19T13:20:00
+ 2019-11-27T13:20:00Z
+
+ urn:uuid:afe122be-59d3-4360-ad69-33c10108fa7a
+
+
+ A001C012
+ A001_C012_AE0306_###.exr
+
+
+
+ Example Movie Final DI
+
+ 2019-09-19T13:20:00
+ 2019-11-27T13:20:00Z
+
+ urn:uuid:be6Ec2ea-a6DC-6cBC-ff0D-AfCED5FF3Dd8
+
+ 1
+ 0
+ 3
+
+
+
+ IDT from Acme Camera Company
+ 1531ea6ef06c5b0a5bea80c94f60c7b68e3989e3c90b8ebd25c28aa4670c30f8
+ urn:ampas:aces:transformId:v1.5:IDT.Acme.Camera.a1.v1
+
+
+ Technical Grade
+
+
+ urn:ampas:aces:transformId:v1.5:ACEScsc.Academy.ACEScct_to_ACES.a1.0.3
+
+
+
+ 2.0 2.0 2.0
+ 0.1 0.1 0.1
+ 1 1 1
+
+
+ 1
+
+
+
+
+ ACES v1.0.3 RRT
+ c81af4fb4a22ee0353308e4582708951df4682bf73f838c24bf44e585fc3bb61
+ urn:ampas:aces:transformId:v1.5:RRT.a1.0.3
+
+
+ P3D60 ODT
+ efd279a82c2d52ee8c49dc0793499dc86bb1a4a3fa0dfb420d59c2814c55aea6
+ urn:ampas:aces:transformId:v1.5:ODT.Academy.P3D60_48nits.a1.0.3
+
+
+
+
diff --git a/src/apps/pyocioamf/pyocioamf.py b/src/apps/pyocioamf/pyocioamf.py
index 853bf8683c..d03d0730dd 100644
--- a/src/apps/pyocioamf/pyocioamf.py
+++ b/src/apps/pyocioamf/pyocioamf.py
@@ -7,45 +7,177 @@
This is a prototype for AMF support in OCIO. It utilizes a tweaked version of the OCIO v2
ACES Reference config as a database of AMF Transform ID strings and accompanying transforms.
-Usage: % python pyocioamf.py
+Usage: % python pyocioamf.py [options]
+
+Options:
+ --no-idt Exclude Input Transform (IDT) from conversion
+ --no-lmt Exclude Look Transform(s) (LMT) from conversion
+ --no-odt Exclude Output Transform (ODT) from conversion
+ --split-by-working-location Generate split CTFs based on workingLocation marker (AMF v2.0)
The exported CTF file is written to the same directory as the AMF_FILE and has the same
-name, but uses .ctf as the extension. The CTF file may then be used with any of the
-other OCIO tools such as ocioconvert or ociochecklut to apply the AMF color pipeline.
+name, but uses .ctf as the extension. When using --split-by-working-location, two files
+are generated: *_before.ctf (IDT + looks before marker) and *_after.ctf (looks after
+marker + ODT).
+
+The CTF file may then be used with any of the other OCIO tools such as ocioconvert or
+ociochecklut to apply the AMF color pipeline.
"""
+import argparse
import xml.etree.ElementTree as ET
import PyOpenColorIO as ocio
-# Specify the color space name for ACES2065-1 in the ACES config.
-ACES = 'ACES - ACES2065-1'
-# Setup the namespaces used in the AMF XML file.
-NS = {'aces': 'urn:ampas:aces:amf:v1.0', 'cdl': 'urn:ASC:CDL:v1.01'}
+# Namespace URIs for AMF versions
+AMF_NS_V1 = 'urn:ampas:aces:amf:v1.0'
+AMF_NS_V2 = 'urn:ampas:aces:amf:v2.0'
+CDL_NS = 'urn:ASC:CDL:v1.01'
+
+# Default namespace dict (will be updated based on detected version)
+NS = {'aces': AMF_NS_V1, 'cdl': CDL_NS}
+
+
+def get_ocio_major_minor_version(config):
+ """
+ Get the OCIO config profile version as a tuple (major, minor).
+ Returns (2, 1) as minimum if version cannot be determined.
+ """
+ try:
+ # getMajorVersion() and getMinorVersion() available in OCIO 2.x
+ major = config.getMajorVersion()
+ minor = config.getMinorVersion()
+ return (major, minor)
+ except AttributeError:
+ # Fallback for older OCIO versions
+ return (2, 1)
+
+
+def find_aces_colorspace_name(config):
+ """
+ Find the ACES2065-1 colorspace name in the config.
+ Uses the aces_interchange role which is defined in all ACES configs.
+ """
+ if config.hasRole('aces_interchange'):
+ return config.getRoleColorSpace('aces_interchange')
+ # Fallback for non-standard configs
+ return 'ACES2065-1'
+
+
+def has_amf_transform_ids_support(config):
+ """
+ Check if the OCIO config supports the amf_transform_ids attribute (OCIO 2.5+).
+ """
+ major, minor = get_ocio_major_minor_version(config)
+ return (major, minor) >= (2, 5)
+
+
+def get_amf_transform_ids(element):
+ """
+ Get AMF Transform IDs from an OCIO config element (colorspace, view transform, look).
+ Works with OCIO 2.5+ configs that have the interchange/amf_transform_ids attribute.
+ Returns a list of transform ID strings, or empty list if not available.
+ """
+ try:
+ # OCIO 2.5+ API: getInterchangeAttribute("amf_transform_ids")
+ # Returns a newline-separated string of transform IDs
+ if hasattr(element, 'getInterchangeAttribute'):
+ amf_ids_str = element.getInterchangeAttribute("amf_transform_ids")
+ if amf_ids_str:
+ # Split by newlines and filter out empty strings
+ return [id.strip() for id in amf_ids_str.split('\n') if id.strip()]
+ except:
+ pass
+ return []
+
+
+def detect_amf_version(root):
+ """
+ Detect AMF version from the root element's namespace or version attribute.
+ Returns the version string ('1.0' or '2.0') and updates the global NS dict.
+ """
+ global NS
-def search_colorspaces(config, aces_id):
- """ Search the config for the supplied ACES ID, return the color space name. """
+ # Check the namespace of the root element
+ ns_uri = root.tag.split('}')[0].strip('{') if '}' in root.tag else ''
+
+ # Also check the version attribute
+ version_attr = root.attrib.get('version', '')
+
+ if ns_uri == AMF_NS_V2 or version_attr == '2.0':
+ NS = {'aces': AMF_NS_V2, 'cdl': CDL_NS}
+ return '2.0'
+ else:
+ NS = {'aces': AMF_NS_V1, 'cdl': CDL_NS}
+ return '1.0'
+
+def search_colorspaces(config, aces_id, use_amf_ids=False):
+ """
+ Search the config for the supplied ACES ID, return the color space name.
+
+ Args:
+ config: OCIO config object
+ aces_id: ACES Transform ID to search for
+ use_amf_ids: If True, search in amf_transform_ids (OCIO 2.5+), else search in description
+ """
for cs in config.getColorSpaces():
- desc = cs.getDescription()
- if aces_id in desc:
- return cs.getName()
+ if use_amf_ids:
+ # OCIO 2.5+: Search in amf_transform_ids
+ amf_ids = get_amf_transform_ids(cs)
+ if aces_id in amf_ids:
+ return cs.getName()
+ else:
+ # Legacy: Search in description
+ desc = cs.getDescription()
+ if aces_id in desc:
+ return cs.getName()
return None
-def search_viewtransforms(config, aces_id):
- """ Search the config for the supplied ACES ID, return the view transform name. """
+
+def search_viewtransforms(config, aces_id, use_amf_ids=False):
+ """
+ Search the config for the supplied ACES ID, return the view transform name.
+
+ Args:
+ config: OCIO config object
+ aces_id: ACES Transform ID to search for
+ use_amf_ids: If True, search in amf_transform_ids (OCIO 2.5+), else search in description
+ """
for vt in config.getViewTransforms():
- desc = vt.getDescription()
- if aces_id in desc:
- return vt.getName()
+ if use_amf_ids:
+ # OCIO 2.5+: Search in amf_transform_ids
+ amf_ids = get_amf_transform_ids(vt)
+ if aces_id in amf_ids:
+ return vt.getName()
+ else:
+ # Legacy: Search in description
+ desc = vt.getDescription()
+ if aces_id in desc:
+ return vt.getName()
return None
-def search_looktransforms(config, aces_id):
- """ Search the config for the supplied ACES ID, return the look name. """
+
+def search_looktransforms(config, aces_id, use_amf_ids=False):
+ """
+ Search the config for the supplied ACES ID, return the look name.
+
+ Args:
+ config: OCIO config object
+ aces_id: ACES Transform ID to search for
+ use_amf_ids: If True, search in amf_transform_ids (OCIO 2.5+), else search in description
+ """
for lt in config.getLooks():
- desc = lt.getDescription()
- if aces_id in desc:
- return lt.getName()
+ if use_amf_ids:
+ # OCIO 2.5+: Search in amf_transform_ids
+ amf_ids = get_amf_transform_ids(lt)
+ if aces_id in amf_ids:
+ return lt.getName()
+ else:
+ # Legacy: Search in description
+ desc = lt.getDescription()
+ if aces_id in desc:
+ return lt.getName()
return None
def must_apply(elem, type):
@@ -86,6 +218,57 @@ def name_ctf_output_file(amf_path):
abs_path = os.path.join(prefix, fname + '.ctf')
return abs_path
+
+def name_ctf_output_file_split(amf_path, suffix):
+ """ Return the path for a split CTF file with _before or _after suffix. """
+ import os.path
+ prefix, basename = os.path.split(amf_path)
+ fname, ext = os.path.splitext(basename)
+ return os.path.join(prefix, f'{fname}_{suffix}.ctf')
+
+
+def split_look_transforms_by_working_location(root, ns):
+ """
+ Split look transforms based on workingLocation marker position.
+
+ Iterates through pipeline children to find the workingLocation marker
+ and categorize lookTransform elements as before or after it.
+
+ Args:
+ root: XML root element
+ ns: Namespace dictionary
+
+ Returns:
+ tuple: (before_looks, after_looks, has_working_location)
+ - before_looks: List of lookTransform elements before workingLocation
+ - after_looks: List of lookTransform elements after workingLocation
+ - has_working_location: Boolean indicating if marker was found
+ """
+ pipeline = root.find('./aces:pipeline', namespaces=ns)
+ if pipeline is None:
+ return [], [], False
+
+ before_looks = []
+ after_looks = []
+ found_working_location = False
+
+ for child in pipeline:
+ # Get local name by stripping namespace
+ local_name = child.tag.split('}')[-1] if '}' in child.tag else child.tag
+
+ if local_name == 'workingLocation':
+ found_working_location = True
+ continue
+
+ if local_name == 'lookTransform':
+ if found_working_location:
+ after_looks.append(child)
+ else:
+ before_looks.append(child)
+
+ return before_looks, after_looks, found_working_location
+
+
def write_ctf(grp, config, amf_path, fname, id):
""" Take a GroupTransform and some metadata and write a CTF file. """
@@ -123,15 +306,21 @@ def extract_three_floats(elem):
raise
return None
-def parse_cdl(look_elem):
- """ Return the CDL slope, offset, power, and saturation values from a look element. """
+def parse_cdl(look_elem, amf_version):
+ """ Return the CDL slope, offset, power, and saturation values from a look element.
+ Supports both ASC CDL element naming conventions: SOPNode/SatNode and ASC_SOP/ASC_SAT.
+ """
slopes = [1., 1., 1.]
offsets = [0., 0., 0.]
powers = [1., 1., 1.]
sat = 1.
has_cdl = False
- sop_elem = look_elem.find('./cdl:SOPNode', namespaces=NS)
+ # Support both ASC CDL element naming conventions
+ sop_elem = look_elem.find('./cdl:ASC_SOP', namespaces=NS)
+ if sop_elem is None:
+ sop_elem = look_elem.find('./cdl:SOPNode', namespaces=NS)
+
if sop_elem is not None:
slope_elem = sop_elem.find('./cdl:Slope', namespaces=NS)
if slope_elem is not None:
@@ -144,7 +333,11 @@ def parse_cdl(look_elem):
powers = extract_three_floats(power_elem)
has_cdl = True
- sat_elem = look_elem.find('./cdl:SatNode', namespaces=NS)
+ # Support both ASC CDL saturation element naming conventions
+ sat_elem = look_elem.find('./cdl:ASC_SAT', namespaces=NS)
+ if sat_elem is None:
+ sat_elem = look_elem.find('./cdl:SatNode', namespaces=NS)
+
if sat_elem is not None:
saturation_elem = sat_elem.find('./cdl:Saturation', namespaces=NS)
if saturation_elem is not None:
@@ -157,7 +350,7 @@ def parse_cdl(look_elem):
return has_cdl, slopes, offsets, powers, sat
-def load_cdl_working_space_transform(config, base_path, look_elem, is_to_direc):
+def load_cdl_working_space_transform(config, base_path, look_elem, is_to_direc, use_amf_ids=False, aces_cs_name='ACES2065-1'):
""" Return an OCIO transform for a CDL to/from working space transform. """
if is_to_direc:
token = 'aces:toCdlWorkingSpace'
@@ -173,14 +366,14 @@ def load_cdl_working_space_transform(config, base_path, look_elem, is_to_direc):
if id_elem is not None:
aces_id = id_elem.text
- cs_name = search_colorspaces(config, aces_id)
+ cs_name = search_colorspaces(config, aces_id, use_amf_ids)
if cs_name is not None:
if is_to_direc:
print(' Loading To-CDL-Working-Space Transform from ACES2065-1 to', cs_name)
- transform = ocio.ColorSpaceTransform(ACES, cs_name, ocio.TRANSFORM_DIR_FORWARD)
+ transform = ocio.ColorSpaceTransform(aces_cs_name, cs_name, ocio.TRANSFORM_DIR_FORWARD)
else:
print(' Loading From-CDL-Working-Space Transform from', cs_name, 'to ACES2065-1')
- transform = ocio.ColorSpaceTransform(cs_name, ACES, ocio.TRANSFORM_DIR_FORWARD)
+ transform = ocio.ColorSpaceTransform(cs_name, aces_cs_name, ocio.TRANSFORM_DIR_FORWARD)
return transform
else:
raise ValueError("Could not find transform for transformId element: " + aces_id)
@@ -200,7 +393,7 @@ def load_cdl_working_space_transform(config, base_path, look_elem, is_to_direc):
return transform
return None
-def process_look(config, gt, base_path, look_elem):
+def process_look(config, gt, base_path, look_elem, amf_version='1.0', use_amf_ids=False, aces_cs_name='ACES2065-1'):
""" Build a look transform and add it to the supplied OCIO group transform. """
if not must_apply(look_elem, 'Look'):
return
@@ -211,10 +404,10 @@ def process_look(config, gt, base_path, look_elem):
if id_elem is not None:
aces_id = id_elem.text
- look_name = search_looktransforms(config, aces_id)
+ look_name = search_looktransforms(config, aces_id, use_amf_ids)
if look_name is not None:
print('Adding Look Transform:', look_name)
- gt.appendTransform( ocio.LookTransform(ACES, ACES, look_name, False, ocio.TRANSFORM_DIR_FORWARD) )
+ gt.appendTransform( ocio.LookTransform(aces_cs_name, aces_cs_name, look_name, False, ocio.TRANSFORM_DIR_FORWARD) )
return
else:
raise ValueError("Could not find transform for transformId element: " + aces_id)
@@ -240,7 +433,7 @@ def process_look(config, gt, base_path, look_elem):
#
# LookTransform ASC CDL case.
#
- has_cdl, slopes, offsets, powers, sat = parse_cdl(look_elem)
+ has_cdl, slopes, offsets, powers, sat = parse_cdl(look_elem, amf_version)
if has_cdl:
ws_elem = look_elem.find('./aces:cdlWorkingSpace', namespaces=NS)
if ws_elem is None:
@@ -250,8 +443,8 @@ def process_look(config, gt, base_path, look_elem):
#
# Attempt to load the working space transforms.
#
- to_transform = load_cdl_working_space_transform(config, base_path, ws_elem, True)
- from_transform = load_cdl_working_space_transform(config, base_path, ws_elem, False)
+ to_transform = load_cdl_working_space_transform(config, base_path, ws_elem, True, use_amf_ids, aces_cs_name)
+ from_transform = load_cdl_working_space_transform(config, base_path, ws_elem, False, use_amf_ids, aces_cs_name)
#
# Handle the four possible scenarios of working space transform availability.
#
@@ -285,20 +478,20 @@ def process_look(config, gt, base_path, look_elem):
print('Adding From-CDL-Working-Space Transform')
gt.appendTransform(from_transform)
-def build_output_transform_from_id_elem(config, gt, id_elem, msg, transform_dir):
+def build_output_transform_from_id_elem(config, gt, id_elem, msg, transform_dir, use_amf_ids=False, aces_cs_name='ACES2065-1'):
""" Add an OCIO transform to the supplied group transform using a transform ID. """
aces_id = id_elem.text
- dcs_name = search_colorspaces(config, aces_id)
- vt_name = search_viewtransforms(config, aces_id)
+ dcs_name = search_colorspaces(config, aces_id, use_amf_ids)
+ vt_name = search_viewtransforms(config, aces_id, use_amf_ids)
if dcs_name is not None and vt_name is not None:
print( msg % (dcs_name, vt_name) )
- gt.appendTransform( ocio.DisplayViewTransform(ACES, dcs_name, vt_name, direction=transform_dir) )
+ gt.appendTransform( ocio.DisplayViewTransform(aces_cs_name, dcs_name, vt_name, direction=transform_dir) )
else:
raise ValueError("Could not process transformId element: " + aces_id)
-def load_output_transform(config, gt, base_path, elem, is_inverse):
+def load_output_transform(config, gt, base_path, elem, is_inverse, use_amf_ids=False, aces_cs_name='ACES2065-1'):
""" Add an OCIO transform to the supplied group transform to implement an ACES Output Transform. """
# Setup some variables based on whether it is an Output or Inverse Output Transform.
if not is_inverse:
@@ -322,7 +515,7 @@ def load_output_transform(config, gt, base_path, elem, is_inverse):
#
id_elem = elem.find(ot_transformId_token, namespaces=NS)
if id_elem is not None:
- build_output_transform_from_id_elem(config, gt, id_elem, msg, transform_dir)
+ build_output_transform_from_id_elem(config, gt, id_elem, msg, transform_dir, use_amf_ids, aces_cs_name)
return
#
# OutputTransform external LUT file case.
@@ -345,7 +538,7 @@ def load_output_transform(config, gt, base_path, elem, is_inverse):
#
id_elem = odt_elem.find('./aces:transformId', namespaces=NS)
if id_elem is not None:
- build_output_transform_from_id_elem(config, gt, id_elem, msg, transform_dir)
+ build_output_transform_from_id_elem(config, gt, id_elem, msg, transform_dir, use_amf_ids, aces_cs_name)
return
# TODO: Could validate that the referenceRenderingTransform element exists and is
# the expected version (although since there is only one, it is not currently used).
@@ -383,7 +576,7 @@ def load_output_transform(config, gt, base_path, elem, is_inverse):
print('Adding ODT LUT file:', odt_path)
gt.appendTransform( odt_transform )
-def load_input_transform(config, gt, base_path, input_elem):
+def load_input_transform(config, gt, base_path, input_elem, use_amf_ids=False, aces_cs_name='ACES2065-1'):
""" Add an OCIO transform to the supplied group transform to implement an ACES Input Transform. """
#
# InputTransform ACES transformId case.
@@ -392,10 +585,10 @@ def load_input_transform(config, gt, base_path, input_elem):
if id_elem is not None:
aces_id = id_elem.text
- cs_name = search_colorspaces(config, aces_id)
+ cs_name = search_colorspaces(config, aces_id, use_amf_ids)
if cs_name is not None:
print('Adding Input Transform from', cs_name, 'to ACES2065-1')
- gt.appendTransform( ocio.ColorSpaceTransform(cs_name, ACES, ocio.TRANSFORM_DIR_FORWARD) )
+ gt.appendTransform( ocio.ColorSpaceTransform(cs_name, aces_cs_name, ocio.TRANSFORM_DIR_FORWARD) )
return
else:
raise ValueError("Could not find transform for transformId element: " + aces_id)
@@ -413,34 +606,76 @@ def load_input_transform(config, gt, base_path, input_elem):
#
# InputTransform is an inverse OutputTransform case.
#
- load_output_transform(config, gt, base_path, input_elem, is_inverse=True)
+ load_output_transform(config, gt, base_path, input_elem, is_inverse=True, use_amf_ids=use_amf_ids, aces_cs_name=aces_cs_name)
+
+def load_aces_ref_config(config_path=None):
+ """
+ Return an OCIO config object for the ACES reference config required to decode transform IDs.
-def load_aces_ref_config():
- """ Return an OCIO config object for the ACES reference config required to decode transform IDs. """
- ACES_REF_CONFIG = 'config-aces-reference.yaml'
+ Args:
+ config_path: Optional path to OCIO config file. If None, uses default config.
+ """
import os
- # Get the path to this script, see if the config is in the same directory.
- file_path = os.path.realpath(__file__)
- prefix = os.path.dirname(file_path)
- config_path = os.path.join(prefix, ACES_REF_CONFIG)
+
+ if config_path is None:
+ # Default: look for config-aces-reference.yaml in the same directory as this script
+ ACES_REF_CONFIG = 'config-aces-reference.yaml'
+ file_path = os.path.realpath(__file__)
+ prefix = os.path.dirname(file_path)
+ config_path = os.path.join(prefix, ACES_REF_CONFIG)
+
+ if not os.path.isfile(config_path):
+ raise ValueError(f'OCIO config file not found: {config_path}')
+
config = ocio.Config().CreateFromFile(config_path)
- # TODO: Try other ways of finding the config.
return config
-def build_ctf(amf_path):
- """ Build a color pipeline that implements the supplied AMF file and write out a CTF file. """
+def build_ctf(amf_path, exclude_idt=False, exclude_lmt=False, exclude_odt=False, config_path=None):
+ """ Build a color pipeline that implements the supplied AMF file and write out a CTF file.
+
+ Args:
+ amf_path: Path to the AMF file
+ exclude_idt: If True, exclude Input Transform (IDT) from conversion
+ exclude_lmt: If True, exclude Look Transform(s) (LMT) from conversion
+ exclude_odt: If True, exclude Output Transform (ODT) from conversion
+ config_path: Optional path to OCIO config file. If None, uses default config.
+ """
print('\nProcessing file: ', amf_path, '\n')
# Use Python to parse the AMF XML file and return an Element Tree object.
tree = ET.parse(amf_path)
root = tree.getroot()
+ # Detect AMF version and configure namespaces accordingly
+ amf_version = detect_amf_version(root)
+ print('Detected AMF version:', amf_version)
+
+ # Print exclusion status
+ if exclude_idt or exclude_lmt or exclude_odt:
+ excluded = []
+ if exclude_idt:
+ excluded.append('IDT')
+ if exclude_lmt:
+ excluded.append('LMT')
+ if exclude_odt:
+ excluded.append('ODT')
+ print('Excluding components:', ', '.join(excluded))
+
# Clear the OCIO file cache (helpful if someone is editing files in between running this).
ocio.ClearAllCaches()
# Load the ACES Reference config that will be used to implement any Transform ID strings
# encountered in the AMF file.
- config = load_aces_ref_config()
+ config = load_aces_ref_config(config_path)
+
+ # Detect OCIO config version and capabilities
+ ocio_version = get_ocio_major_minor_version(config)
+ use_amf_ids = has_amf_transform_ids_support(config)
+ aces_cs_name = find_aces_colorspace_name(config)
+
+ print(f'OCIO config version: {ocio_version[0]}.{ocio_version[1]}')
+ print(f'Using AMF Transform IDs attribute: {use_amf_ids}')
+ print(f'ACES2065-1 colorspace name: {aces_cs_name}')
# Initialize a group transform to hold the results.
gt = ocio.GroupTransform()
@@ -448,20 +683,29 @@ def build_ctf(amf_path):
#
# Handle the AMF Input Transform.
#
- input_elem = root.find('./aces:pipeline/aces:inputTransform', namespaces=NS)
- if must_apply(input_elem, 'Input'):
- load_input_transform(config, gt, amf_path, input_elem)
+ if not exclude_idt:
+ input_elem = root.find('./aces:pipeline/aces:inputTransform', namespaces=NS)
+ if must_apply(input_elem, 'Input'):
+ load_input_transform(config, gt, amf_path, input_elem, use_amf_ids, aces_cs_name)
+ else:
+ print('Skipping Input Transform (IDT) - excluded by user')
#
# Handle all the AMF Look Transforms.
#
- for look_elem in root.findall('./aces:pipeline/aces:lookTransform', namespaces=NS):
- process_look(config, gt, amf_path, look_elem)
+ if not exclude_lmt:
+ for look_elem in root.findall('./aces:pipeline/aces:lookTransform', namespaces=NS):
+ process_look(config, gt, amf_path, look_elem, amf_version, use_amf_ids, aces_cs_name)
+ else:
+ print('Skipping Look Transform(s) (LMT) - excluded by user')
#
# Handle the AMF Output Transform.
#
- output_elem = root.find('./aces:pipeline/aces:outputTransform', namespaces=NS)
- if must_apply(output_elem, 'Output'):
- load_output_transform(config, gt, amf_path, output_elem, is_inverse=False)
+ if not exclude_odt:
+ output_elem = root.find('./aces:pipeline/aces:outputTransform', namespaces=NS)
+ if must_apply(output_elem, 'Output'):
+ load_output_transform(config, gt, amf_path, output_elem, is_inverse=False, use_amf_ids=use_amf_ids, aces_cs_name=aces_cs_name)
+ else:
+ print('Skipping Output Transform (ODT) - excluded by user')
# Print the OCIO transforms in the group transform.
print('\n', gt)
@@ -471,11 +715,183 @@ def build_ctf(amf_path):
write_ctf(gt, config, amf_path, ctf_path, 'none')
+def build_ctf_split(amf_path, exclude_idt=False, exclude_lmt=False, exclude_odt=False, config_path=None):
+ """
+ Build two CTF files split by workingLocation marker.
+
+ Generates:
+ - *_before.ctf: IDT + lookTransforms before workingLocation
+ - *_after.ctf: lookTransforms after workingLocation + ODT
+
+ If no workingLocation is found, falls back to generating a single CTF file.
+
+ Args:
+ amf_path: Path to the AMF file
+ exclude_idt: If True, exclude Input Transform from before CTF
+ exclude_lmt: If True, exclude all Look Transforms from both CTFs
+ exclude_odt: If True, exclude Output Transform from after CTF
+ config_path: Optional path to OCIO config file
+ """
+ print('\nProcessing file (split mode): ', amf_path, '\n')
+
+ # Parse AMF XML
+ tree = ET.parse(amf_path)
+ root = tree.getroot()
+
+ # Detect AMF version
+ amf_version = detect_amf_version(root)
+ print('Detected AMF version:', amf_version)
+
+ # Check for workingLocation
+ before_looks, after_looks, has_working_location = split_look_transforms_by_working_location(root, NS)
+
+ if not has_working_location:
+ print('Warning: No workingLocation element found in AMF file.')
+ print('Falling back to generating single CTF file.')
+ print('')
+ build_ctf(amf_path, exclude_idt, exclude_lmt, exclude_odt, config_path)
+ return
+
+ print(f'Found workingLocation marker:')
+ print(f' - {len(before_looks)} look transform(s) before workingLocation')
+ print(f' - {len(after_looks)} look transform(s) after workingLocation')
+
+ # Print exclusion status
+ if exclude_idt or exclude_lmt or exclude_odt:
+ excluded = []
+ if exclude_idt:
+ excluded.append('IDT')
+ if exclude_lmt:
+ excluded.append('LMT')
+ if exclude_odt:
+ excluded.append('ODT')
+ print('Excluding components:', ', '.join(excluded))
+
+ # Clear OCIO cache and load config
+ ocio.ClearAllCaches()
+ config = load_aces_ref_config(config_path)
+
+ # Detect OCIO config capabilities
+ ocio_version = get_ocio_major_minor_version(config)
+ use_amf_ids = has_amf_transform_ids_support(config)
+ aces_cs_name = find_aces_colorspace_name(config)
+
+ print(f'OCIO config version: {ocio_version[0]}.{ocio_version[1]}')
+ print(f'Using AMF Transform IDs attribute: {use_amf_ids}')
+ print(f'ACES2065-1 colorspace name: {aces_cs_name}')
+
+ # ===== BUILD "BEFORE" CTF =====
+ print('\n--- Building BEFORE CTF ---')
+ gt_before = ocio.GroupTransform()
+ has_before_content = False
+
+ # Add IDT to before CTF
+ if not exclude_idt:
+ input_elem = root.find('./aces:pipeline/aces:inputTransform', namespaces=NS)
+ if must_apply(input_elem, 'Input'):
+ load_input_transform(config, gt_before, amf_path, input_elem, use_amf_ids, aces_cs_name)
+ has_before_content = True
+ else:
+ print('Skipping Input Transform (IDT) - excluded by user')
+
+ # Add look transforms BEFORE workingLocation
+ if not exclude_lmt:
+ for look_elem in before_looks:
+ if must_apply(look_elem, 'Look'):
+ process_look(config, gt_before, amf_path, look_elem, amf_version, use_amf_ids, aces_cs_name)
+ has_before_content = True
+ else:
+ print('Skipping Look Transform(s) (LMT) - excluded by user')
+
+ # Write before CTF if it has content
+ if has_before_content:
+ print('\n', gt_before)
+ ctf_path_before = name_ctf_output_file_split(amf_path, 'before')
+ write_ctf(gt_before, config, amf_path, ctf_path_before, 'before')
+ else:
+ print('\nNo transforms in BEFORE CTF - skipping file generation')
+
+ # ===== BUILD "AFTER" CTF =====
+ print('\n--- Building AFTER CTF ---')
+ gt_after = ocio.GroupTransform()
+ has_after_content = False
+
+ # Add look transforms AFTER workingLocation
+ if not exclude_lmt:
+ for look_elem in after_looks:
+ if must_apply(look_elem, 'Look'):
+ process_look(config, gt_after, amf_path, look_elem, amf_version, use_amf_ids, aces_cs_name)
+ has_after_content = True
+ else:
+ print('Skipping Look Transform(s) (LMT) - excluded by user')
+
+ # Add ODT to after CTF
+ if not exclude_odt:
+ output_elem = root.find('./aces:pipeline/aces:outputTransform', namespaces=NS)
+ if must_apply(output_elem, 'Output'):
+ load_output_transform(config, gt_after, amf_path, output_elem, is_inverse=False,
+ use_amf_ids=use_amf_ids, aces_cs_name=aces_cs_name)
+ has_after_content = True
+ else:
+ print('Skipping Output Transform (ODT) - excluded by user')
+
+ # Write after CTF if it has content
+ if has_after_content:
+ print('\n', gt_after)
+ ctf_path_after = name_ctf_output_file_split(amf_path, 'after')
+ write_ctf(gt_after, config, amf_path, ctf_path_after, 'after')
+ else:
+ print('\nNo transforms in AFTER CTF - skipping file generation')
+
+ # Summary
+ print('\n--- Split CTF Generation Complete ---')
+ if has_before_content:
+ print(f' Before CTF: {name_ctf_output_file_split(amf_path, "before")}')
+ if has_after_content:
+ print(f' After CTF: {name_ctf_output_file_split(amf_path, "after")}')
+
+
def main():
- import sys
- if len( sys.argv ) != 2:
- raise ValueError( "USAGE: python3 amf_to_ocio.py " )
- build_ctf( sys.argv[1] )
+ parser = argparse.ArgumentParser(
+ description='Convert an ACES Metadata File (AMF) into an OCIO Color Transform File (CTF).',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog='''
+Examples:
+ python pyocioamf.py example.amf # Convert full pipeline
+ python pyocioamf.py example.amf --no-idt # Exclude input transform
+ python pyocioamf.py example.amf --no-lmt --no-odt # Exclude LMT and ODT
+ python pyocioamf.py example.amf --config my_config.ocio # Use custom OCIO config
+ python pyocioamf.py example.amf --split-by-working-location # Generate split CTFs
+ '''
+ )
+ parser.add_argument('amf_file', help='Path to the AMF file to convert')
+ parser.add_argument('--config', '-c', dest='config_path',
+ help='Path to OCIO config file (supports OCIO 2.1+, with enhanced support for 2.5+)')
+ parser.add_argument('--no-idt', action='store_true',
+ help='Exclude Input Transform (IDT) from conversion')
+ parser.add_argument('--no-lmt', action='store_true',
+ help='Exclude Look Transform(s) (LMT) from conversion')
+ parser.add_argument('--no-odt', action='store_true',
+ help='Exclude Output Transform (ODT) from conversion')
+ parser.add_argument('--split-by-working-location', action='store_true',
+ help='Generate two CTF files split by workingLocation marker (AMF v2.0). '
+ 'Creates *_before.ctf (IDT + looks before marker) and '
+ '*_after.ctf (looks after marker + ODT)')
+
+ args = parser.parse_args()
+
+ if args.split_by_working_location:
+ build_ctf_split(args.amf_file,
+ exclude_idt=args.no_idt,
+ exclude_lmt=args.no_lmt,
+ exclude_odt=args.no_odt,
+ config_path=args.config_path)
+ else:
+ build_ctf(args.amf_file,
+ exclude_idt=args.no_idt,
+ exclude_lmt=args.no_lmt,
+ exclude_odt=args.no_odt,
+ config_path=args.config_path)
if __name__=='__main__':