"""DeepTIAS 2.7 の検出モデル(TF1 物体検出の frozen graph `Detection_Normal.pb`)を ONNX にする.
TensorFlow と tf2onnx は本体の依存に入れず,実行するときだけ一時的に入れる(サーバで実行する想定):
uv run --with tensorflow --with tf2onnx python scripts/convert_detection_to_onnx.py \\
--pb ~/models/TongueSegmentation/Detection_Normal.pb \\
--out ~/models/TongueSegmentation/Detection_Normal.onnx
入出力(TF1 物体検出 API の既定.tf2onnx はテンソル名に `:0` を付けたまま ONNX の名前にする):
- 入力 `image_tensor:0`: `(1, H, W, 3)` の uint8 画像(TF の物体検出 API の既定は RGB)
- 出力 `detection_boxes:0`: `(1, N, 4)` の float32.正規化 `[ymin, xmin, ymax, xmax]`
- 出力 `detection_scores:0`: `(1, N)` の float32(降順)
- 出力 `detection_classes:0`・`num_detections:0`: 変換はするが評価では使わない
`tias-seg eval` の `detection.input_name`・`boxes_name`・`scores_name` は `:0` の有無を問わずに
探すので,既定(`image_tensor`・`detection_boxes`・`detection_scores`)のままでよい.
名前が既定と違うグラフは `--list-nodes` で Placeholder と出力の候補を確かめ,`--inputs`・
`--outputs` で指定する.変換後に onnxruntime で黒い画像を 1 枚推論し,出力の形状を表示する.
変換に失敗した場合でも `tias-seg eval` は検出なし・正解の外接矩形の方式だけで完走する.
"""
from __future__ import annotations
import argparse
import logging
import sys
from pathlib import Path
import numpy as np
logger = logging.getLogger(__name__)
DEFAULT_INPUTS = ("image_tensor:0",)
DEFAULT_OUTPUTS = (
"detection_boxes:0",
"detection_scores:0",
"detection_classes:0",
"num_detections:0",
)
# TF の物体検出 API の NMS(ループ・制御フロー)を変換できる opset.
DEFAULT_OPSET = 13
# 変換後の確認に使う黒い画像の大きさ(高さ・幅).
CHECK_IMAGE_SIZE = (480, 640)
def _load_graph_def(path: Path): # noqa: ANN202 TensorFlow の型は一時的な依存のため書かない
import tensorflow as tf
graph_def = tf.compat.v1.GraphDef()
graph_def.ParseFromString(path.read_bytes())
return graph_def
def _list_nodes(graph_def) -> None: # noqa: ANN001
consumed = {i.split(":")[0].lstrip("^") for n in graph_def.node for i in n.input}
placeholders = [n.name for n in graph_def.node if n.op == "Placeholder"]
leaves = [n.name for n in graph_def.node if n.name not in consumed and n.op != "Const"]
logger.info("Placeholder(入力の候補): %s", placeholders)
logger.info("どこからも使われないノード(出力の候補): %s", leaves[:50])
def _check_onnx(path: Path, input_name: str) -> None:
import onnxruntime as ort
session = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
for i in session.get_inputs():
logger.info("ONNX の入力: %s %s %s", i.name, i.shape, i.type)
for o in session.get_outputs():
logger.info("ONNX の出力: %s %s %s", o.name, o.shape, o.type)
image = np.zeros((1, *CHECK_IMAGE_SIZE, 3), dtype=np.uint8)
outputs = session.run(None, {input_name: image})
for o, value in zip(session.get_outputs(), outputs, strict=True):
logger.info("黒い画像での出力 %s: 形状 %s", o.name, np.asarray(value).shape)
def _parse_args(argv: list[str] | None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="TF1 物体検出の frozen graph を ONNX に変換する")
parser.add_argument("--pb", type=Path, required=True, help="Detection_Normal.pb のパス")
parser.add_argument("--out", type=Path, help="書き出す ONNX のパス(--list-nodes 以外で必須)")
parser.add_argument(
"--inputs", default=",".join(DEFAULT_INPUTS), help="入力テンソル名(カンマ区切り)"
)
parser.add_argument(
"--outputs", default=",".join(DEFAULT_OUTPUTS), help="出力テンソル名(カンマ区切り)"
)
parser.add_argument("--opset", type=int, default=DEFAULT_OPSET, help="ONNX の opset")
parser.add_argument(
"--list-nodes", action="store_true", help="入力・出力の候補を表示するだけで変換しない"
)
args = parser.parse_args(argv)
if not args.list_nodes and args.out is None:
parser.error("--out を指定する")
return args
def main(argv: list[str] | None = None) -> int:
"""エントリポイント.
Args:
argv: コマンドライン引数(None なら `sys.argv[1:]`).
Returns:
終了コード(成功 0・失敗 1).
"""
logging.basicConfig(
level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
args = _parse_args(argv)
pb = args.pb.expanduser()
if not pb.is_file():
logger.error("frozen graph が無い: %s", pb)
return 1
try:
graph_def = _load_graph_def(pb)
except ImportError:
logger.error(
"tensorflow が無い.uv run --with tensorflow --with tf2onnx python %s ... で実行する",
Path(__file__).as_posix(),
)
return 1
if args.list_nodes:
_list_nodes(graph_def)
return 0
import tf2onnx
out = args.out.expanduser()
out.parent.mkdir(parents=True, exist_ok=True)
inputs = [s.strip() for s in args.inputs.split(",") if s.strip()]
outputs = [s.strip() for s in args.outputs.split(",") if s.strip()]
logger.info("変換する: 入力 %s・出力 %s・opset %d", inputs, outputs, args.opset)
tf2onnx.convert.from_graph_def(
graph_def,
input_names=inputs,
output_names=outputs,
opset=args.opset,
output_path=str(out),
)
logger.info("書き出した: %s", out)
_check_onnx(out, inputs[0])
return 0
if __name__ == "__main__":
sys.exit(main())