前回の記事では、scene-referred環境でのLUTの扱いについて整理しました。
今回は同じタイミングで問い合わせがあった
・外部で編集されたconfig.ocioがFlameで読み込めない
・CDLがベイクされたEDLの扱い
・.CCCファイルを用いた運用
を整理していきます。
外部で編集されたconfig.ocioが読み込めない
編集されたconfig.ocioをFlameにインポートしたときのログを確認しました。
CLRMGT: ERROR: The heuristics currently only support scene-referred color spaces. Please set the interchange roles. Failed loading '/Volumes/StorageMedia/Apple_Project/Import_CDL/setups/colour_mgmt/studio-config-v1.0.0_aces-v1.3_ocio-v2.1.ocio': [The heuristics currently only support scene-referred color spaces. Please set the interchange roles.]. Defaulting to RAW config.
1行目の意味
ディスプレイ変換を含む場合、自動推測では解決できないため、scene-referred以外の経路では、
interchange roleの明示が必要になります。
試しに、新規にダウンロードしたstudio.config.ocioを編集。
roles: aces_interchange: ACES2065-1 # cie_xyz_d65_interchange: CIE XYZ-D65 - Display-referred > コメントアウト color_picking: sRGB Encoded Rec.709 (sRGB) color_timing: ACEScct compositing_log: ACEScct data: Raw matte_paint: ACEScct scene_linear: ACEScg texture_paint: sRGB Encoded Rec.709 (sRGB)
同じエラーになることを確認
CLRMGT: ERROR: The heuristics currently only support scene-referred color spaces. Please set the interchange roles. Failed loading '/Volumes/StorageMedia/Apple_Project/Import_CDL/setups/colour_mgmt/studio-config-v1.0.0_aces-v1.3_ocio-v2.1.ocio': [The heuristics currently only support scene-referred color spaces. Please set the interchange roles.]. Defaulting to RAW config.
Flame(というよりOCIO 2.x)は、ディスプレイ変換の共通インターチェンジとしてXYZが使われる設計です。
入力(scene)
↓
reference / interchange space(CIE-XYZ D65)
↓
display transform
↓
表示
なぜXYZが必要になるか
ディスプレイは全部違う色域だからです:
- Rec.709
- P3
- sRGB
- HDR
これらを直接相互変換すると複雑になるので、全部いったんXYZに集めるという設計です。
補足
Flameが常にXYZで内部処理しているわけではありません。
ディスプレイパイプラインの解決時にXYZが中間として必要になります。
- 内部作業 → ACEScg
- 表示変換 → XYZ経由
DaVinci ResolveからCDLを作成
今回の検証を再現するために、DaVinci ResolveからCDLをベイクしたEDLを作成しました。
DaVinci Resolveカラーマネージメント設定

Primariesから調整


CDL調整から見た目、BT.1886 Rec.709のレンジにフィットするように、オリジナルのACES2065-1は、
コントラストとクロマを少しおさえています。



CDL書き出し(拡張子は.edl)


EDL確認
Figo:~ admin$ cat /Volumes/StorageMedia/Apple_Project/Import_CDL/setups/colour_mgmt/Resolve_CDL/edit.edl TITLE: edit.edl FCM: NON-DROP FRAME 001 AX V C 01:38:20:03 01:38:21:03 01:00:00:00 01:00:01:00 *ASC_SOP (1.628218 1.628218 1.628218)(-0.161355 -0.161355 -0.161355)(1.000000 1.000000 1.000000) *ASC_SAT 1.380000 002 AX V C 01:13:49:02 01:13:50:02 01:00:01:00 01:00:02:00 *ASC_SOP (1.401167 1.401167 1.401167)(-0.122730 -0.122730 -0.122730)(1.102000 1.102000 1.102000) *ASC_SAT 1.000000 003 AX V C 03:01:29:06 03:01:30:06 01:00:02:00 01:00:03:00 *ASC_SOP (1.354002 1.354002 1.354002)(0.000000 0.000000 0.000000)(1.384000 1.384000 1.384000) *ASC_SAT 1.400000 Figo:~ admin$
DeliverページのColor Space Tag / Gamma Tagがうまく反映されない?

ACES Output Transform > No Output Transform
DeliverページのColor Space Tag / Gamma Tagは表示変換としては使われず、
元のscene-referredカラースペースが維持される

DaVinci Resolveのカラーパイプライン
- Color science : ACEScct
- Working space : ACEScct(ACESモードでは固定)
- Output Transform : None (No Output Transform)
つまり、DaVinciResolve内部は
ACES2065-1 (source)
↓ IDT
ACEScct (grading working)
↓ CDL
ACEScct
↓
ACES2065-1で書き出し (Output Transformが無効のため、display変換を通らずscene-referredのまま出力)
Tagged Colour Space > From File of Rules (OCIOコンフィグは、Flameデフォルト2.0)


DaVinci Resolveガイド用としてFlameにインポート

次に、CDL(.edl)をFlameからインポートしてDaVinci Resolveと同じ結果になるか確認してみましょう。
CDLインポート時の注意点とカラーマネージメント
DaVinci Resolveから書き出したCDL(.edl)をインポート

プロジェクト> setups/colour_mgmt/

EDLをインポートする場合、必ずフレームレートを確認
シーケンスレゾリューションも設定


Convert CDL Lookボタンを有効化してインポート


CDLデータがEDLにベイクされている場合、Lookノードがセグメントに追加され、CDL情報がLookとして引き継がれます。
ACES2065-1フッテージをコンフォーム



DaVinci Resolveのリザルト(Rec.709 BT.1886 )とミスマッチ

なぜ、ミスマッチなのか?
DaVinci Resolve > ワーキングカラースペースはACEScct
ACES2065-1 (source)
↓ IDT
ACEScct (grading working)
↓ CDL
ACEScct
Colour Management (緑色) ノードをセグメントのソースに追加



Input Transform > Input Colour Space > From Source (ACES2065-1)


Working Colour Space > ACEScct (DaVinci Resolveのワーキングと合わせる)


Waveformで確認すると、BT.1886 Rec.709のレンジに収まっています


Colour Managementノードを全てのタイムラインセグメントにコピー



DaVinci Resolveから書き出したACES2065-1


Flameシーケンスセグメント(CDL)


LookノードからCDL情報を確認


CDLはショットごとの色補正をSlope・Offset・Power・Saturationの値で記録する標準フォーマットです。
今回確認できたこと
- Resolve ワーキング = ACEScct
- Resolve 書き出し = ACES2065-1
- Flame シーケンス = ACEScct
- CDL適用 → 同ポジ
つまり
scene-referred (LOGエンコード) ↔ scene-referred (リニアエンコード)間でもCDLは一致
まとめ
ここまでで、DaVinci Resolveから書き出したCDL(.edl)をFlameにインポート、
scene-referred環境でも同じ結果になることが確認できました。
ただし、EDLベースのCDL情報はイベント単位で記録されており、
そのままでは複数ショットをまとめて扱うには向いていません。
そこで次は、CDLを.cccファイルとしてまとめて、複数ショットの補正をひとつのファイルで管理できる形にしていきます。
CDL(.edl)から.cccファイルにまとめる
CDLはSlope・Offset・Power・Saturationの値で色補正を記録します。
.cccファイルは、cccidをキーにCDL値とショットを識別するファイル名をまとめて管理するフォーマットです。
.cccファイルフォーマット
<ColorCorrectionCollection xmlns="urn:ASC:CDL:v1.2">
<ColorCorrection id="A230C0881_154515-154545">
<Description>A230C0881[154515-154545].exr</Description>
<SOPNode>
<Slope>1.29 0.855 1.087</Slope>
<Offset>0.029 -0.0145 0.0</Offset>
<Power>1.0 1.058 1.0</Power>
</SOPNode>
<SATNode>
<Saturation>1.116</Saturation>
</SATNode>
</ColorCorrection>
</ColorCorrectionCollection>
.cccファイルを作成するには、CDL値に加えてショットを識別するファイル名が必要になります。
まず、DaVinci ResolveからCDL(.edl)を書き出しましたが、ファイル名の情報は含まれていません。
Figo:~ admin$ cat /Volumes/StorageMedia/Apple_Project/Import_CDL/setups/colour_mgmt/Resolve_CDL/edit.edl TITLE: edit.edl FCM: NON-DROP FRAME 001 AX V C 01:38:20:03 01:38:21:03 01:00:00:00 01:00:01:00 *ASC_SOP (1.628218 1.628218 1.628218)(-0.161355 -0.161355 -0.161355)(1.000000 1.000000 1.000000) *ASC_SAT 1.380000 Figo:~ admin$
.drx (DaVinci Resolve eXchange)も確認しましたが、
今回の用途ではファイル名が取得できなかったため諦めました。
最終的に、EDLからCDL値とソースタイムコードを取得し、XMLからファイル名とソースタイムコードを取得します。
両者のソースタイムコードをキーとして照合することで、CDL値とファイル名を紐付けることができます。
役割分担
EDL > CDL情報/ソースタイムコード
001 AX V C 01:38:20:03 01:38:21:03 01:00:00:00 01:00:01:00 *ASC_SOP (1.628218 1.628218 1.628218)(-0.161355 -0.161355 -0.161355)(1.000000 1.000000 1.000000) *ASC_SAT 1.38000
XML > ファイルネーム/ソースタイムコード
<name>A001C002_0904242F[141603-141626].exr</name>
<pathurl>file://ACES2065-1/A001C002_0904242F[141603-141626].exr</pathurl>
<timecode>
<string>01:38:20:03</string>
<displayformat>NDF</displayformat>
<rate>
CDL(.edl)とXMLを元に.cccファイルを生成する簡易スクリプトも作成しました。
参考用として公開します。
edl_xml_to_ccc.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
ASC_CDL_NS = "urn:ASC:CDL:v1.2"
ET.register_namespace("", ASC_CDL_NS)
def qname(tag):
return f"{{{ASC_CDL_NS}}}{tag}"
def indent(elem, level=0):
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
for child in elem:
indent(child, level + 1)
if not child.tail or not child.tail.strip():
child.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def make_cccid(base_name, used_ids):
stem = Path(base_name).stem
cccid = re.sub(r"[^A-Za-z0-9_-]+", "_", stem)
if not cccid:
cccid = "SHOT"
original = cccid
index = 1
while cccid in used_ids:
cccid = f"{original}_{index:03d}"
index += 1
used_ids.add(cccid)
return cccid
def parse_triplet_text(text):
parts = text.replace(",", " ").split()
if len(parts) != 3:
raise ValueError(f"Expected 3 values, got: {text}")
return tuple(float(x) for x in parts)
def parse_edl_cdl_events(edl_path):
"""
Resolve EDL から ASC_SOP / ASC_SAT をイベント順で拾う
"""
events = []
current = None
sop_re = re.compile(
r"\*+\s*ASC_SOP\s*\(\s*([^)]+?)\s*\)\s*\(\s*([^)]+?)\s*\)\s*\(\s*([^)]+?)\s*\)",
re.IGNORECASE
)
sat_re = re.compile(
r"\*+\s*ASC_SAT\s+([^\s]+)",
re.IGNORECASE
)
event_re = re.compile(r"^\s*\d+\s+\S+\s+[VA]\s+[CDWKMFB]")
with open(edl_path, "r", encoding="utf-8", errors="ignore") as f:
for raw_line in f:
line = raw_line.rstrip("\n")
if event_re.match(line):
if current and "slope" in current and "saturation" in current:
events.append(current)
current = {}
continue
if current is None:
continue
m = sop_re.search(line)
if m:
current["slope"] = parse_triplet_text(m.group(1))
current["offset"] = parse_triplet_text(m.group(2))
current["power"] = parse_triplet_text(m.group(3))
continue
m = sat_re.search(line)
if m:
current["saturation"] = float(m.group(1))
continue
if current and "slope" in current and "saturation" in current:
events.append(current)
return events
def text_or_none(elem, path):
child = elem.find(path)
if child is not None and child.text is not None:
return child.text.strip()
return None
def parse_xml_file_names(xml_path):
"""
XML から clipitem 順の file name を拾う
同じ file id の再利用があっても clipitem 順で並べる
"""
tree = ET.parse(xml_path)
root = tree.getroot()
names = []
for clipitem in root.findall(".//clipitem"):
file_elem = clipitem.find("./file")
if file_elem is None:
continue
file_name = text_or_none(file_elem, "./name")
if file_name:
names.append(file_name)
return names
def add_cc(root, cccid, description, slope, offset, power, saturation):
cc = ET.SubElement(root, qname("ColorCorrection"), {"id": cccid})
desc = ET.SubElement(cc, qname("Description"))
desc.text = description
sop = ET.SubElement(cc, qname("SOPNode"))
slope_node = ET.SubElement(sop, qname("Slope"))
slope_node.text = f"{slope[0]:.10g} {slope[1]:.10g} {slope[2]:.10g}"
offset_node = ET.SubElement(sop, qname("Offset"))
offset_node.text = f"{offset[0]:.10g} {offset[1]:.10g} {offset[2]:.10g}"
power_node = ET.SubElement(sop, qname("Power"))
power_node.text = f"{power[0]:.10g} {power[1]:.10g} {power[2]:.10g}"
sat = ET.SubElement(cc, qname("SATNode"))
sat_node = ET.SubElement(sat, qname("Saturation"))
sat_node.text = f"{saturation:.10g}"
def build_ccc_from_edl_and_xml(edl_path, xml_path, output_path):
edl_events = parse_edl_cdl_events(edl_path)
xml_names = parse_xml_file_names(xml_path)
count = min(len(edl_events), len(xml_names))
print(f"EDL CDL events : {len(edl_events)}")
print(f"XML file names : {len(xml_names)}")
print(f"Matched shots : {count}")
root = ET.Element(qname("ColorCorrectionCollection"))
used_ids = set()
for i in range(count):
ev = edl_events[i]
file_name = xml_names[i]
cccid = make_cccid(file_name, used_ids)
add_cc(
root=root,
cccid=cccid,
description=file_name,
slope=ev["slope"],
offset=ev["offset"],
power=ev["power"],
saturation=ev["saturation"],
)
indent(root)
ET.ElementTree(root).write(output_path, encoding="utf-8", xml_declaration=True)
print("Done")
print(f"EDL : {edl_path}")
print(f"XML : {xml_path}")
print(f"CCC : {output_path}")
def main():
if len(sys.argv) != 4:
print("Usage:")
print(" ./edl_xml_to_ccc.py input.edl input.xml output.ccc")
sys.exit(1)
edl_path = sys.argv[1]
xml_path = sys.argv[2]
output_path = sys.argv[3]
build_ccc_from_edl_and_xml(edl_path, xml_path, output_path)
if __name__ == "__main__":
main()
.cccファイル作成
edit.edl / edit.xml / edl_xml_to_ccc.pyを同じ階層に配置
Figo:~ admin$ cd /Volumes/StorageMedia/Apple_Project/Import_CDL/setups/colour_mgmt/Resolve_CDL/ Figo:Resolve_CDL admin$ tree . . ├── edit.edl ├── edit.xml └── edl_xml_to_ccc.py 1 directory, 3 files Figo:Resolve_CDL admin$
python実行 > edl / xml / cccファイル名
Figo:Resolve_CDL admin$ python3 edl_xml_to_ccc.py edit.edl edit.xml edit.ccc EDL CDL events : 3 XML file names : 3 Matched shots : 3 Done EDL : edit.edl XML : edit.xml CCC : edit.ccc Figo:Resolve_CDL admin$
生成された.cccファイル
生成されたedit.cccファイルの内容は以下のようになります。
生成された.cccファイルの先頭部分(抜粋)
<ColorCorrectionCollection xmlns="urn:ASC:CDL:v1.2">
<ColorCorrection id="A001C002_0904242F_141603-141626_"> > id = cccid
<Description>A001C002_0904242F[141603-141626].exr
<SOPNode>
<Slope>1.628218 1.628218 1.628218
<Offset>-0.161355 -0.161355 -0.161355
<Power>1 1 1
</SOPNode>
<Saturation>1.38
</SATNode>
edit.ccc
<?xml version='1.0' encoding='utf-8'?>
<ColorCorrectionCollection xmlns="urn:ASC:CDL:v1.2">
<ColorCorrection id="A001C002_0904242F_141603-141626_">
<Description>A001C002_0904242F[141603-141626].exr</Description>
<SOPNode>
<Slope>1.628218 1.628218 1.628218</Slope>
<Offset>-0.161355 -0.161355 -0.161355</Offset>
<Power>1 1 1</Power>
</SOPNode><SATNode>
<Saturation>1.38</Saturation>
</SATNode>
</ColorCorrection><ColorCorrection id="B004C0023_106298-106321_">
<Description>B004C0023.[106298-106321].exr</Description>
<SOPNode>
<Slope>1.401167 1.401167 1.401167</Slope>
<Offset>-0.12273 -0.12273 -0.12273</Offset>
<Power>1.102 1.102 1.102</Power>
</SOPNode><SATNode>
<Saturation>1</Saturation>
</SATNode>
</ColorCorrection><ColorCorrection id="A003C013_220415_R07B_261342-261365_">
<Description>A003C013_220415_R07B[261342-261365].exr</Description>
<SOPNode>
<Slope>1.354002 1.354002 1.354002</Slope>
<Offset>0 0 0</Offset>
<Power>1.384 1.384 1.384</Power>
</SOPNode><SATNode>
<Saturation>1.4</Saturation>
</SATNode>
</ColorCorrection>
</ColorCorrectionCollection>
.cccファイルをインポート
先ほど作成した.cccファイルは、3カットのColorCorrectionだったので、ダミーのColorCorrectionを追加し、
Lookノードでの切り替え確認を行うための簡易スクリプトも作りました。
Figo:Resolve_CDL admin$ tree . . ├── add_dummy_ccc.py > ダミーのColorCorrectionを追加する簡易スクリプト ├── edit.ccc ├── edit.edl ├── edit.xml └── edl_xml_to_ccc.py 1 directory, 5 files Figo:Resolve_CDL admin$
add_dummy_ccc.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import random
import re
import xml.etree.ElementTree as ET
from pathlib import Path
ASC_CDL_NS = "urn:ASC:CDL:v1.2"
ET.register_namespace("", ASC_CDL_NS)
def qname(tag):
return f"{{{ASC_CDL_NS}}}{tag}"
def indent(elem, level=0):
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
for child in elem:
indent(child, level + 1)
if not child.tail or not child.tail.strip():
child.tail = i + " "
if not elem[-1].tail or not elem[-1].tail.strip():
elem[-1].tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def make_dummy_values(index):
base = index * 0.01
slope = (
round(1.0 + base, 6),
round(1.0 - base * 0.5, 6),
round(1.0 + base * 0.3, 6)
)
offset = (
round(base * 0.1, 6),
round(-base * 0.05, 6),
round(0.0, 6)
)
power = (
1.0,
round(1.0 + base * 0.2, 6),
1.0
)
saturation = round(1.0 + base * 0.4, 6)
return slope, offset, power, saturation
def make_cccid_from_description(description):
stem = Path(description).stem
cccid = re.sub(r"[^A-Za-z0-9_-]+", "_", stem)
cccid = cccid.strip("_")
if not cccid:
cccid = "SHOT"
return cccid
def make_unique_dummy_description(existing_descriptions):
"""
既存Descriptionと絶対に重複しない名前を作る
"""
while True:
cam = random.choice(["A", "B", "C"])
roll = random.randint(1, 999)
cut = random.randint(1, 9999)
start = random.randint(100000, 999999)
end = start + random.randint(20, 40)
description = f"{cam}{roll:03d}C{cut:04d}[{start}-{end}].exr"
if description not in existing_descriptions:
existing_descriptions.add(description)
return description
def make_unique_cccid(description, existing_ids):
"""
Descriptionベースでidを作り、既存idと重複しないようにする
"""
base_id = make_cccid_from_description(description)
cccid = base_id
suffix = 1
while cccid in existing_ids:
cccid = f"{base_id}_{suffix:03d}"
suffix += 1
existing_ids.add(cccid)
return cccid
def add_color_correction(root, cccid, description, slope, offset, power, saturation):
cc = ET.SubElement(root, qname("ColorCorrection"), {"id": cccid})
desc = ET.SubElement(cc, qname("Description"))
desc.text = description
sop = ET.SubElement(cc, qname("SOPNode"))
slope_node = ET.SubElement(sop, qname("Slope"))
slope_node.text = f"{slope[0]} {slope[1]} {slope[2]}"
offset_node = ET.SubElement(sop, qname("Offset"))
offset_node.text = f"{offset[0]} {offset[1]} {offset[2]}"
power_node = ET.SubElement(sop, qname("Power"))
power_node.text = f"{power[0]} {power[1]} {power[2]}"
sat_parent = ET.SubElement(cc, qname("SATNode"))
sat_node = ET.SubElement(sat_parent, qname("Saturation"))
sat_node.text = f"{saturation}"
def add_dummy_entries(input_ccc, output_ccc, count):
tree = ET.parse(input_ccc)
root = tree.getroot()
cc_path = f".//{qname('ColorCorrection')}"
desc_path = f"./{qname('Description')}"
existing_ids = set()
existing_descriptions = set()
for cc in root.findall(cc_path):
cccid = cc.attrib.get("id")
if cccid:
existing_ids.add(cccid)
desc = cc.find(desc_path)
if desc is not None and desc.text:
existing_descriptions.add(desc.text.strip())
for i in range(1, count + 1):
description = make_unique_dummy_description(existing_descriptions)
cccid = make_unique_cccid(description, existing_ids)
slope, offset, power, saturation = make_dummy_values(i)
add_color_correction(
root=root,
cccid=cccid,
description=description,
slope=slope,
offset=offset,
power=power,
saturation=saturation,
)
cc_list = root.findall(qname("ColorCorrection"))
for cc in cc_list:
root.remove(cc)
random.shuffle(cc_list)
for cc in cc_list:
root.append(cc)
indent(root)
tree.write(output_ccc, encoding="utf-8", xml_declaration=True)
print("Done")
print(f"Input : {input_ccc}")
print(f"Output: {output_ccc}")
print(f"Added : {count} dummy entries")
print(f"Total : {len(cc_list)} ColorCorrection entries")
def main():
if len(sys.argv) != 3:
print("Usage:")
print(" ./add_dummy_ccc.py input.ccc output.ccc")
sys.exit(1)
input_ccc = sys.argv[1]
output_ccc = sys.argv[2]
add_dummy_entries(input_ccc, output_ccc, 30)
if __name__ == "__main__":
main()
ColorCorrectionを追加した.cccファイル(edit_33.ccc)を作成
Figo:Resolve_CDL admin$ python3 add_dummy_ccc.py edit.ccc edit_33.ccc Done Input : edit.ccc Output: edit_33.ccc Added : 30 dummy entries Total : 33 ColorCorrection entries Figo:Resolve_CDL admin$ tree . . ├── add_dummy_ccc.py ├── edit_33.ccc >>>>> ColorCorrection30追加 ├── edit.ccc ├── edit.edl ├── edit.xml └── edl_xml_to_ccc.py 1 directory, 6 files Figo:Resolve_CDL admin$
追加した.cccファイルをFlameからインポート
Convert CDL to Lookは無効化



無効化
シーケンス名を変更してコンフォーム


Colour Managementノード(緑色)追加してDaVinci ResolveワーキングカラースペースACEScctにトランスフォーム


Lookノード追加



Importボタンから.cccファイルのロケーションに移動



.cccファイルを選択すると、cc ID(cccid)がハイライト


Flameから.cccファイルのインポートは可能です。
ただし、cccidによる各セグメントへの自動適用には対応しておらず、手動での切り替えとなります。

まとめ
.cccファイルはLookノードから読み込むことができ、
複数のCDLを手動で切り替える運用が可能になります。
ただし、cccidとタイムラインセグメントを自動的に紐付ける仕組みはなく、
実運用ではショット数が増えると管理が煩雑になります。
なお、PythonからLookノードへ自動振り分ける方法についても確認していますが、
これについては次回以降の記事で整理します。
次回は、scene-referred環境におけるACESの役割を整理し、
CDLおよび.cccファイル運用の前提となるカラーマネージメントについて確認します。