Yolo26-Depth / fold_depth_log.py
KevinHsuWavematrix's picture
First release
6cdca59
Raw History Blame Contribute Delete
3.47 kB
#!/usr/bin/env python3
"""Remove the unsupported Log branch from this YOLO depth ONNX export.
For the depth head exported by this project:
Log(Clip(Exp(Clip(x, lo, hi)), floor, +inf))
is equivalent to Clip(x, max(lo, log(floor)), hi). The generated AX637
model then contains no AxQuantizedLog CPU fallback.
"""
from __future__ import annotations
import argparse
import math
from pathlib import Path
import numpy as np
import onnx
from onnx import numpy_helper
def scalar_initializer(model: onnx.ModelProto, name: str) -> float:
"""Return a scalar initializer and reject models with a different topology."""
for initializer in model.graph.initializer:
if initializer.name == name:
value = numpy_helper.to_array(initializer)
if value.size != 1:
raise ValueError(f"{name} must be scalar, got shape {value.shape}")
return float(value.reshape(-1)[0])
raise ValueError(f"missing initializer: {name}")
def fold_depth_log(model: onnx.ModelProto) -> None:
"""Fold the Clip -> Exp -> Clip -> Log sequence used by the depth head."""
nodes = list(model.graph.node)
producers = {output: node for node in nodes for output in node.output if output}
log_node = next(
(node for node in nodes if node.name == "/model.23/Log" and node.op_type == "Log"),
None,
)
if log_node is None or len(log_node.input) != 1 or len(log_node.output) != 1:
raise ValueError("expected a single-input /model.23/Log node")
outer_clip = producers.get(log_node.input[0])
exp_node = producers.get(outer_clip.input[0]) if outer_clip is not None and outer_clip.input else None
inner_clip = producers.get(exp_node.input[0]) if exp_node is not None and exp_node.input else None
if (
outer_clip is None
or outer_clip.op_type != "Clip"
or exp_node is None
or exp_node.op_type != "Exp"
or inner_clip is None
or inner_clip.op_type != "Clip"
or len(inner_clip.input) != 3
or len(outer_clip.input) < 2
):
raise ValueError("unexpected depth calibration topology around /model.23/Log")
inner_min = scalar_initializer(model, inner_clip.input[1])
floor = scalar_initializer(model, outer_clip.input[1])
if floor <= 0.0:
raise ValueError(f"Clip floor must be positive, got {floor}")
# Do not mutate a potentially shared initializer from elsewhere in the graph.
folded_min = max(inner_min, math.log(floor))
min_name = "depth_log_folded_min"
model.graph.initializer.append(numpy_helper.from_array(np.array(folded_min, dtype=np.float32), min_name))
inner_clip.input[1] = min_name
inner_clip.name = "/model.23/Clip_log_folded"
inner_clip.output[0] = log_node.output[0]
remove_ids = {id(outer_clip), id(exp_node), id(log_node)}
del model.graph.node[:]
model.graph.node.extend(node for node in nodes if id(node) not in remove_ids)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path, help="source ONNX model")
parser.add_argument("output", type=Path, help="rewritten ONNX model")
args = parser.parse_args()
model = onnx.load(args.input)
fold_depth_log(model)
onnx.checker.check_model(model)
args.output.parent.mkdir(parents=True, exist_ok=True)
onnx.save(model, args.output)
print(f"wrote {args.output}")
if __name__ == "__main__":
main()