Добавлены пропсы конвейера и стереодвижки, задействованные в прогоне
assets/conveyors (274 МБ) - ленты и угловая секция NVIDIA, на которые ссылается сцена относительным путём. Раньше исключались как перекачиваемые, но без них сцена не композится из коробки. cv/ - код стереодвижков, которые вызывает control_test, без весов: * defom-stereo - рабочий бейзлайн (DEFOM vitl, вход 480, iters 24) * crestereo - второй движок, точнее по габаритам (MAE 23.5 против 32.8 мм) * fast-foundationstereo - проверялся, в бейзлайн не вошёл * circular_section.py - показатель кругового сечения, перенесён в measure_plane.py: выравнивает облако по СОБСТВЕННЫМ главным осям и режет на пяти высотах вдоль каждой. Три самодельные версии (мировые оси, одно сечение) давали хуже; результаты проверки на эталонной геометрии - в circular_section_results.json Веса по-прежнему не в репозитории - источники в MODELS.md. Наборы кадров прежних прогонов (cv/flow_*, 1.26 ГБ) исключены: это выход, а не исходники. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a TensorRT engine from the FFSGWCVolume plugin ONNX.
|
||||
|
||||
This mirrors cpp/app/build_single_engine.cpp in Python. The ONNX parser still
|
||||
needs the custom FFSGWCVolume plugin creator registered before parsing, so the
|
||||
shared plugin library is auto-detected from cpp/build or can be passed with
|
||||
--plugin_lib.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PLUGIN_NAME = "FFSGWCVolume"
|
||||
PLUGIN_VERSION = "1"
|
||||
_LOADED_PLUGIN_LIBS = []
|
||||
|
||||
|
||||
def find_default_plugin_library() -> Path | None:
|
||||
repo_dir = Path(__file__).resolve().parents[1]
|
||||
candidates = [
|
||||
repo_dir / "cpp" / "build" / "libffs_gwc_plugin.so",
|
||||
repo_dir / "cpp" / "build" / "lib" / "libffs_gwc_plugin.so",
|
||||
repo_dir / "cpp" / "build" / "Release" / "libffs_gwc_plugin.so",
|
||||
]
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def load_plugin_library(path: str) -> None:
|
||||
"""Load an optional shared library that registers FFSGWCVolume."""
|
||||
lib = ctypes.CDLL(path, mode=ctypes.RTLD_GLOBAL)
|
||||
_LOADED_PLUGIN_LIBS.append(lib)
|
||||
|
||||
# The current C++ code registers via ffs_depth::registerFFSGWCPlugin().
|
||||
# A loadable Python plugin library should expose an extern "C" wrapper with
|
||||
# one of these names so ctypes can call it without C++ name mangling.
|
||||
for symbol in ("ffs_register_gwc_plugin", "registerFFSGWCPlugin"):
|
||||
try:
|
||||
fn = getattr(lib, symbol)
|
||||
except AttributeError:
|
||||
continue
|
||||
fn.restype = ctypes.c_bool
|
||||
if not fn():
|
||||
raise RuntimeError(f"{symbol}() returned false for {path}")
|
||||
return
|
||||
|
||||
# Some TensorRT plugin libraries register creators during library load. That
|
||||
# is not true for this repo's current static C++ helper, but allow it here.
|
||||
|
||||
|
||||
def find_plugin_creator(trt) -> bool:
|
||||
registry = trt.get_plugin_registry()
|
||||
creator = registry.get_plugin_creator(PLUGIN_NAME, PLUGIN_VERSION, "")
|
||||
return creator is not None
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Build a TensorRT engine from an ONNX graph containing FFSGWCVolume."
|
||||
)
|
||||
parser.add_argument("plugin_onnx", type=Path, help="Path to plugin ONNX file")
|
||||
parser.add_argument("output_engine", type=Path, help="Path to write TensorRT engine")
|
||||
parser.add_argument(
|
||||
"--plugin_lib",
|
||||
type=Path,
|
||||
default=None,
|
||||
help=(
|
||||
"Shared library that registers FFSGWCVolume. Defaults to "
|
||||
"cpp/build/libffs_gwc_plugin.so when present. Pure Python cannot "
|
||||
"provide this repo's CUDA plugin implementation."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fp32",
|
||||
action="store_true",
|
||||
help="Disable FP16 builder flag. Default allows FP16 when supported.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--workspace-mb",
|
||||
type=int,
|
||||
default=4096,
|
||||
help="TensorRT workspace memory limit in MiB.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if not args.plugin_onnx.exists():
|
||||
raise FileNotFoundError(f"ONNX file does not exist: {args.plugin_onnx}")
|
||||
args.output_engine.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
import tensorrt as trt
|
||||
|
||||
logger = trt.Logger(trt.Logger.INFO)
|
||||
trt.init_libnvinfer_plugins(logger, "")
|
||||
|
||||
plugin_lib = args.plugin_lib or find_default_plugin_library()
|
||||
if plugin_lib is not None:
|
||||
if not plugin_lib.exists():
|
||||
raise FileNotFoundError(f"Plugin library does not exist: {plugin_lib}")
|
||||
load_plugin_library(str(plugin_lib))
|
||||
|
||||
if not find_plugin_creator(trt):
|
||||
raise RuntimeError(
|
||||
f"{PLUGIN_NAME} plugin creator is not registered. "
|
||||
"Build/load a shared library for cpp/src/gwc_volume_plugin.cpp and pass "
|
||||
"--plugin_lib, or use cpp/build/ffs_build_single_engine."
|
||||
)
|
||||
|
||||
explicit_batch = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
|
||||
builder = trt.Builder(logger)
|
||||
network = builder.create_network(explicit_batch)
|
||||
parser = trt.OnnxParser(network, logger)
|
||||
|
||||
parsed = False
|
||||
if hasattr(parser, "parse_from_file"):
|
||||
parsed = parser.parse_from_file(str(args.plugin_onnx))
|
||||
else:
|
||||
parsed = parser.parse(args.plugin_onnx.read_bytes())
|
||||
if not parsed:
|
||||
for i in range(parser.num_errors):
|
||||
print(parser.get_error(i))
|
||||
raise RuntimeError(f"failed to parse ONNX: {args.plugin_onnx}")
|
||||
|
||||
config = builder.create_builder_config()
|
||||
config.set_memory_pool_limit(
|
||||
trt.MemoryPoolType.WORKSPACE, int(args.workspace_mb) * 1024 * 1024
|
||||
)
|
||||
if not args.fp32 and builder.platform_has_fast_fp16:
|
||||
config.set_flag(trt.BuilderFlag.FP16)
|
||||
|
||||
serialized = builder.build_serialized_network(network, config)
|
||||
if serialized is None:
|
||||
raise RuntimeError("build_serialized_network failed")
|
||||
|
||||
args.output_engine.write_bytes(bytes(serialized))
|
||||
precision = "FP32" if args.fp32 else "FP16 allowed"
|
||||
print(f"Built engine: {args.output_engine}")
|
||||
print(f"Precision: {precision}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import warnings, argparse, logging, os, sys,zipfile
|
||||
os.environ['TORCH_COMPILE_DISABLE'] = '1'
|
||||
os.environ['TORCHDYNAMO_DISABLE'] = '1'
|
||||
code_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
import omegaconf, yaml, torch,pdb
|
||||
from omegaconf import OmegaConf
|
||||
from core.foundation_stereo import FastFoundationStereo, TrtFeatureRunner, TrtPostRunner, build_gwc_volume_triton
|
||||
import Utils as U
|
||||
|
||||
|
||||
class FoundationStereoOnnx(FastFoundationStereo):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(self, left, right):
|
||||
""" Removes extra outputs and hyper-parameters """
|
||||
with torch.amp.autocast('cuda', enabled=True, dtype=U.AMP_DTYPE):
|
||||
disp = FastFoundationStereo.forward(self, left, right, iters=self.args.valid_iters, test_mode=True, optimize_build_volume=False)
|
||||
return disp
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
code_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parser.add_argument('--model_dir', type=str, default=f'{code_dir}/../weights/model_best_bp2_serialize.pth')
|
||||
parser.add_argument('--save_path', type=str, default=f'/home/bowen/debug/', help='Path to save results.')
|
||||
parser.add_argument('--height', type=int, default=448)
|
||||
parser.add_argument('--width', type=int, default=640)
|
||||
parser.add_argument('--valid_iters', type=int, default=8, help='number of flow-field updates during forward pass')
|
||||
parser.add_argument('--corr_levels', type=int, default=2, help="number of levels in the correlation pyramid")
|
||||
parser.add_argument('--mixed_precision', default=True, action='store_true', help='use mixed precision')
|
||||
parser.add_argument('--corr_radius', type=int, default=4, help="width of the correlation pyramid")
|
||||
parser.add_argument('--n_downsample', type=int, default=2, help="resolution of the disparity field (1/2^K)")
|
||||
parser.add_argument('--n_gru_layers', type=int, default=1, help="number of hidden GRU levels")
|
||||
parser.add_argument('--max_disp', type=int, default=192, help="max disp of geometry encoding volume")
|
||||
parser.add_argument('--low_memory', type=int, default=1, help='reduce memory usage')
|
||||
args = parser.parse_args()
|
||||
os.makedirs(os.path.dirname(args.save_path), exist_ok=True)
|
||||
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
|
||||
model = torch.load(args.model_dir, map_location='cpu', weights_only=False)
|
||||
model.args.max_disp = args.max_disp
|
||||
model.args.valid_iters = args.valid_iters
|
||||
model.cuda().eval()
|
||||
|
||||
feature_runner = TrtFeatureRunner(model)
|
||||
post_runner = TrtPostRunner(model)
|
||||
|
||||
feature_runner.cuda().eval()
|
||||
post_runner.cuda().eval()
|
||||
assert args.height % 32 == 0 and args.width % 32 == 0, "height and width must be divisible by 32"
|
||||
left_img = torch.randn(1, 3, args.height, args.width).cuda().float()*255
|
||||
right_img = torch.randn(1, 3, args.height, args.width).cuda().float()*255
|
||||
|
||||
torch.onnx.export(
|
||||
feature_runner,
|
||||
(left_img, right_img),
|
||||
args.save_path+'/feature_runner.onnx',
|
||||
opset_version=17,
|
||||
input_names = ['left', 'right'],
|
||||
output_names = ['features_left_04', 'features_left_08', 'features_left_16', 'features_left_32', 'features_right_04', 'stem_2x'],
|
||||
do_constant_folding=True,
|
||||
dynamo=False,
|
||||
)
|
||||
|
||||
features_left_04, features_left_08, features_left_16, features_left_32, features_right_04, stem_2x = feature_runner(left_img, right_img)
|
||||
gwc_volume = build_gwc_volume_triton(features_left_04.half(), features_right_04.half(), args.max_disp//4, model.cv_group)
|
||||
disp = post_runner(features_left_04.float(), features_left_08.float(), features_left_16.float(), features_left_32.float(), features_right_04.float(), stem_2x.float(), gwc_volume.float())
|
||||
|
||||
torch.onnx.export(
|
||||
post_runner,
|
||||
(features_left_04, features_left_08, features_left_16, features_left_32, features_right_04, stem_2x, gwc_volume),
|
||||
args.save_path+'/post_runner.onnx',
|
||||
opset_version=17,
|
||||
input_names = ['features_left_04', 'features_left_08', 'features_left_16', 'features_left_32', 'features_right_04', 'stem_2x', 'gwc_volume'],
|
||||
output_names = ['disp'],
|
||||
do_constant_folding=True,
|
||||
dynamo=False,
|
||||
)
|
||||
|
||||
with open(f'{args.save_path}/onnx.yaml', 'w') as f:
|
||||
cfg = OmegaConf.to_container(model.args)
|
||||
cfg['image_size'] = [args.height, args.width]
|
||||
yaml.safe_dump(cfg, f)
|
||||
@@ -0,0 +1,160 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ['TORCH_COMPILE_DISABLE'] = '1'
|
||||
os.environ['TORCHDYNAMO_DISABLE'] = '1'
|
||||
|
||||
code_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Export Fast-FoundationStereo as one ONNX with an FFSGWCVolume TensorRT plugin node')
|
||||
parser.add_argument('--model_dir', type=str,
|
||||
default=f'{code_dir}/../weights/23-36-37/model_best_bp2_serialize.pth')
|
||||
parser.add_argument('--save_path', type=str, default=f'{code_dir}/../output_plugin_onnx')
|
||||
parser.add_argument('--height', type=int, default=608)
|
||||
parser.add_argument('--width', type=int, default=960)
|
||||
parser.add_argument('--valid_iters', type=int, default=8)
|
||||
parser.add_argument('--max_disp', type=int, default=192)
|
||||
parser.add_argument('--onnx_name', type=str, default='fast_foundationstereo_plugin.onnx')
|
||||
return parser
|
||||
|
||||
|
||||
if any(arg in ('-h', '--help') for arg in sys.argv[1:]):
|
||||
build_parser().print_help()
|
||||
sys.exit(0)
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import yaml
|
||||
from omegaconf import OmegaConf
|
||||
from torch.onnx import symbolic_helper
|
||||
|
||||
from core.foundation_stereo import TrtFeatureRunner, TrtPostRunner
|
||||
|
||||
|
||||
class FFSGWCVolumeOp(torch.autograd.Function):
|
||||
@staticmethod
|
||||
def forward(ctx, features_left_04, features_right_04, max_disp, cv_group, normalize):
|
||||
# ONNX export only needs a tensor with the correct static shape here.
|
||||
# symbolic() emits the TensorRT plugin node that computes the real volume.
|
||||
batch, _, height, width = features_left_04.shape
|
||||
return features_left_04.new_zeros(
|
||||
(batch, int(cv_group), int(max_disp), height, width)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def symbolic(g, features_left_04, features_right_04, max_disp, cv_group, normalize):
|
||||
def as_int(value):
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
return symbolic_helper._parse_arg(value, 'i')
|
||||
|
||||
max_disp = as_int(max_disp)
|
||||
cv_group = as_int(cv_group)
|
||||
normalize = as_int(normalize)
|
||||
out = g.op(
|
||||
'FFSGWCVolume',
|
||||
features_left_04,
|
||||
features_right_04,
|
||||
max_disp_i=int(max_disp),
|
||||
cv_group_i=int(cv_group),
|
||||
normalize_i=int(normalize),
|
||||
)
|
||||
sizes = features_left_04.type().sizes()
|
||||
if sizes is not None and len(sizes) == 4:
|
||||
out.setType(features_left_04.type().with_sizes(
|
||||
[sizes[0], int(cv_group), int(max_disp), sizes[2], sizes[3]]))
|
||||
return out
|
||||
|
||||
|
||||
class FastFoundationStereoPluginOnnx(nn.Module):
|
||||
def __init__(self, model, max_disp_levels, cv_group, normalize):
|
||||
super().__init__()
|
||||
self.feature_runner = TrtFeatureRunner(model)
|
||||
self.post_runner = TrtPostRunner(model)
|
||||
self.max_disp_levels = int(max_disp_levels)
|
||||
self.cv_group = int(cv_group)
|
||||
self.normalize = int(bool(normalize))
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(self, left, right):
|
||||
features_left_04, features_left_08, features_left_16, features_left_32, features_right_04, stem_2x = (
|
||||
self.feature_runner(left, right)
|
||||
)
|
||||
gwc_volume = FFSGWCVolumeOp.apply(
|
||||
features_left_04,
|
||||
features_right_04,
|
||||
self.max_disp_levels,
|
||||
self.cv_group,
|
||||
self.normalize,
|
||||
)
|
||||
disp = self.post_runner(
|
||||
features_left_04.float(),
|
||||
features_left_08.float(),
|
||||
features_left_16.float(),
|
||||
features_left_32.float(),
|
||||
features_right_04.float(),
|
||||
stem_2x.float(),
|
||||
gwc_volume.float(),
|
||||
)
|
||||
return disp
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = build_parser().parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
||||
assert args.height % 32 == 0 and args.width % 32 == 0, 'height and width must be divisible by 32'
|
||||
os.makedirs(args.save_path, exist_ok=True)
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
|
||||
logging.info('Loading model: %s', args.model_dir)
|
||||
model = torch.load(args.model_dir, map_location='cpu', weights_only=False)
|
||||
model.args.max_disp = args.max_disp
|
||||
model.args.valid_iters = args.valid_iters
|
||||
model.cuda().eval()
|
||||
|
||||
cv_group = int(getattr(model, 'cv_group', getattr(model.args, 'cv_group', 8)))
|
||||
normalize = bool(getattr(model.args, 'normalize', True))
|
||||
wrapper = FastFoundationStereoPluginOnnx(
|
||||
model,
|
||||
max_disp_levels=args.max_disp // 4,
|
||||
cv_group=cv_group,
|
||||
normalize=normalize,
|
||||
).cuda().eval()
|
||||
|
||||
left = torch.randn(1, 3, args.height, args.width, device='cuda').float() * 255
|
||||
right = torch.randn(1, 3, args.height, args.width, device='cuda').float() * 255
|
||||
|
||||
onnx_name = args.onnx_name if args.onnx_name.endswith('.onnx') else f'{args.onnx_name}.onnx'
|
||||
onnx_path = os.path.join(args.save_path, onnx_name)
|
||||
logging.info('Exporting plugin ONNX: %s', onnx_path)
|
||||
|
||||
torch.onnx.export(
|
||||
wrapper,
|
||||
(left, right),
|
||||
onnx_path,
|
||||
opset_version=17,
|
||||
input_names=['left', 'right'],
|
||||
output_names=['disp'],
|
||||
do_constant_folding=True,
|
||||
dynamo=False,
|
||||
)
|
||||
|
||||
cfg = OmegaConf.to_container(model.args)
|
||||
cfg['image_size'] = [args.height, args.width]
|
||||
cfg['cv_group'] = cv_group
|
||||
cfg['normalize'] = normalize
|
||||
with open(os.path.join(args.save_path, 'onnx.yaml'), 'w') as f:
|
||||
yaml.safe_dump(cfg, f)
|
||||
|
||||
logging.info('ONNX model: %s', onnx_path)
|
||||
logging.info('Config : %s', os.path.join(args.save_path, 'onnx.yaml'))
|
||||
logging.info('Build with:')
|
||||
logging.info(' cpp/build/ffs_build_single_engine %s %s',
|
||||
onnx_path, os.path.join(args.save_path, 'fast_foundationstereo.engine'))
|
||||
@@ -0,0 +1,222 @@
|
||||
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
|
||||
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
Export Fast FoundationStereo as a **single** ONNX model.
|
||||
|
||||
Unlike make_onnx.py (which splits into feature_runner + post_runner with a
|
||||
Triton GWC kernel in between), this script produces one self-contained ONNX
|
||||
that can be converted to a single TensorRT engine via trtexec.
|
||||
|
||||
Key design choices:
|
||||
- The GWC and concat cost volumes are built with ONNX-compatible ops
|
||||
(pad + slice + stack). The upstream pytorch1 variants use
|
||||
Tensor.unfold / torch.flip which the ONNX exporter cannot handle.
|
||||
- ImageNet normalization is STRIPPED from the model so that it can be
|
||||
applied externally (e.g. via Isaac ROS ImageNormalizeNode). The ONNX
|
||||
model expects **pre-normalized** float inputs:
|
||||
pixel = (pixel_0_255 - mean) / std
|
||||
mean = [123.675, 116.28, 103.53] (ImageNet, in 0-255 scale)
|
||||
std = [ 58.395, 57.12, 57.375]
|
||||
- Inputs: left_image (1, 3, H, W) float32, ImageNet-normalised
|
||||
right_image (1, 3, H, W) float32, ImageNet-normalised
|
||||
- Output: disparity (1, 1, H, W) float32
|
||||
|
||||
Usage:
|
||||
python make_single_onnx.py \\
|
||||
--model_dir ../weights/model_best_bp2_serialize.pth \\
|
||||
--save_path ./output_single_onnx --height 480 --width 640
|
||||
|
||||
# Then build a TensorRT engine:
|
||||
trtexec --onnx=./output_single_onnx/fast_foundationstereo.onnx \\
|
||||
--saveEngine=fast_foundationstereo.engine --fp16
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ['TORCH_COMPILE_DISABLE'] = '1'
|
||||
os.environ['TORCHDYNAMO_DISABLE'] = '1'
|
||||
|
||||
code_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
|
||||
import yaml
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from omegaconf import OmegaConf
|
||||
import core.foundation_stereo as _fs_module
|
||||
from core.foundation_stereo import FastFoundationStereo
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ONNX-compatible cost-volume builders
|
||||
#
|
||||
# The upstream *_optimized_pytorch1 variants use Tensor.unfold + torch.flip
|
||||
# which the ONNX tracer cannot export. These replacements build the
|
||||
# disparity-shifted target volume with an explicit loop over disparities
|
||||
# using only F.pad, slicing, and torch.stack — all fully ONNX-exportable.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_gwc_volume_onnx(refimg_fea, targetimg_fea, maxdisp,
|
||||
num_groups, normalize=True):
|
||||
dtype = refimg_fea.dtype
|
||||
B, C, H, W = refimg_fea.shape
|
||||
channels_per_group = C // num_groups
|
||||
|
||||
ref_volume = refimg_fea.unsqueeze(2).expand(B, C, maxdisp, H, W)
|
||||
|
||||
shifted = [
|
||||
F.pad(targetimg_fea, (d, 0, 0, 0), 'constant', 0.0)[:, :, :, :W]
|
||||
for d in range(maxdisp)
|
||||
]
|
||||
target_volume = torch.stack(shifted, dim=2)
|
||||
|
||||
ref_volume = ref_volume.view(B, num_groups, channels_per_group,
|
||||
maxdisp, H, W)
|
||||
target_volume = target_volume.view(B, num_groups, channels_per_group,
|
||||
maxdisp, H, W)
|
||||
|
||||
if normalize:
|
||||
ref_volume = F.normalize(ref_volume.float(), dim=2).to(dtype)
|
||||
target_volume = F.normalize(target_volume.float(), dim=2).to(dtype)
|
||||
|
||||
return (ref_volume * target_volume).sum(dim=2).contiguous()
|
||||
|
||||
|
||||
def _build_concat_volume_onnx(refimg_fea, targetimg_fea, maxdisp):
|
||||
B, C, H, W = refimg_fea.shape
|
||||
|
||||
ref_volume = refimg_fea.unsqueeze(2).expand(B, C, maxdisp, H, W)
|
||||
|
||||
shifted = [
|
||||
F.pad(targetimg_fea, (d, 0, 0, 0), 'constant', 0.0)[:, :, :, :W]
|
||||
for d in range(maxdisp)
|
||||
]
|
||||
target_volume = torch.stack(shifted, dim=2)
|
||||
|
||||
return torch.cat((ref_volume, target_volume), dim=1).contiguous()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class FastFoundationStereoSingleOnnx(nn.Module):
|
||||
"""Thin wrapper that calls the full model with ONNX-compatible settings.
|
||||
|
||||
Before ONNX tracing the caller must monkey-patch:
|
||||
- normalize_image → identity (normalization done externally)
|
||||
- build_gwc_volume_* → _build_gwc_volume_onnx
|
||||
- build_concat_volume_* → _build_concat_volume_onnx
|
||||
"""
|
||||
|
||||
def __init__(self, model: FastFoundationStereo):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(self, left_image, right_image):
|
||||
return self.model.forward(
|
||||
left_image, right_image,
|
||||
iters=self.model.args.valid_iters,
|
||||
test_mode=True,
|
||||
optimize_build_volume='pytorch1',
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Export Fast FoundationStereo as a single ONNX model')
|
||||
parser.add_argument(
|
||||
'--model_dir', type=str,
|
||||
default=f'{code_dir}/../weights/model_best_bp2_serialize.pth',
|
||||
help='Path to the serialized .pth model')
|
||||
parser.add_argument(
|
||||
'--save_path', type=str,
|
||||
default=f'{code_dir}/output_single_onnx',
|
||||
help='Directory to save the ONNX model and config')
|
||||
parser.add_argument('--height', type=int, default=480)
|
||||
parser.add_argument('--width', type=int, default=640)
|
||||
parser.add_argument('--valid_iters', type=int, default=8,
|
||||
help='GRU refinement iterations')
|
||||
parser.add_argument('--max_disp', type=int, default=192,
|
||||
help='Maximum disparity (in pixels at full resolution)')
|
||||
parser.add_argument('--onnx_name', type=str, default='fast_foundationstereo',
|
||||
help='Base name for the saved ONNX file (without .onnx extension)')
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(message)s')
|
||||
|
||||
assert args.height % 32 == 0 and args.width % 32 == 0, \
|
||||
'height and width must be divisible by 32'
|
||||
|
||||
os.makedirs(args.save_path, exist_ok=True)
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
|
||||
if not os.path.isfile(args.model_dir):
|
||||
raise FileNotFoundError(f'Model file not found: {args.model_dir}')
|
||||
|
||||
logging.info(f'Loading model from {args.model_dir}')
|
||||
model = torch.load(args.model_dir, map_location='cpu', weights_only=False)
|
||||
model.args.max_disp = args.max_disp
|
||||
model.args.valid_iters = args.valid_iters
|
||||
model.args.mixed_precision = False
|
||||
model.cuda().eval()
|
||||
|
||||
wrapper = FastFoundationStereoSingleOnnx(model)
|
||||
wrapper.cuda().eval()
|
||||
|
||||
left_img = torch.randn(1, 3, args.height, args.width, device='cuda')
|
||||
right_img = torch.randn(1, 3, args.height, args.width, device='cuda')
|
||||
|
||||
onnx_name = args.onnx_name if args.onnx_name.endswith('.onnx') else f'{args.onnx_name}.onnx'
|
||||
onnx_path = os.path.join(args.save_path, onnx_name)
|
||||
logging.info(f'Exporting ONNX ({args.height}x{args.width}) → {onnx_path}')
|
||||
|
||||
# Monkey-patch non-ONNX-exportable functions before tracing
|
||||
_fs_module.normalize_image = lambda img: img
|
||||
_fs_module.build_gwc_volume_optimized_pytorch1 = _build_gwc_volume_onnx
|
||||
_fs_module.build_concat_volume_optimized_pytorch1 = _build_concat_volume_onnx
|
||||
|
||||
torch.onnx.export(
|
||||
wrapper,
|
||||
(left_img, right_img),
|
||||
onnx_path,
|
||||
opset_version=17,
|
||||
input_names=['left_image', 'right_image'],
|
||||
output_names=['disparity'],
|
||||
do_constant_folding=True,
|
||||
)
|
||||
|
||||
cfg = OmegaConf.to_container(model.args)
|
||||
cfg['image_size'] = [args.height, args.width]
|
||||
config_name = os.path.splitext(onnx_name)[0] + '.yaml'
|
||||
config_path = os.path.join(args.save_path, config_name)
|
||||
with open(config_path, 'w') as f:
|
||||
yaml.safe_dump(cfg, f)
|
||||
|
||||
logging.info(f'ONNX model : {onnx_path}')
|
||||
logging.info(f'Config : {config_path}')
|
||||
logging.info(f'Resolution : {args.height} x {args.width}')
|
||||
logging.info(
|
||||
f'Build TRT engine:\n'
|
||||
f' trtexec --onnx={onnx_path} '
|
||||
f'--saveEngine={args.save_path}/{os.path.splitext(onnx_name)[0]}.engine --fp16')
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# NVIDIA CORPORATION and its licensors retain all intellectual property
|
||||
# and proprietary rights in and to this software, related documentation
|
||||
# and any modifications thereto. Any use, reproduction, disclosure or
|
||||
# distribution of this software and related documentation without an express
|
||||
# license agreement from NVIDIA CORPORATION is strictly prohibited.
|
||||
|
||||
import os,sys
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
from omegaconf import OmegaConf
|
||||
from core.utils.utils import InputPadder
|
||||
import argparse, torch, logging, yaml, time
|
||||
import numpy as np
|
||||
from Utils import AMP_DTYPE, set_logging_format, set_seed
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--model_dir', default=f'{code_dir}/../weights/23-36-37/model_best_bp2_serialize.pth', type=str)
|
||||
parser.add_argument('--hiera', default=0, type=int)
|
||||
parser.add_argument('--valid_iters', type=int, default=8, help='number of flow-field updates during forward pass')
|
||||
parser.add_argument('--max_disp', type=int, default=192, help='maximum disparity')
|
||||
parser.add_argument('--warmup', type=int, default=15, help='number of warmup iterations')
|
||||
parser.add_argument('--total', type=int, default=30, help='total number of iterations')
|
||||
args = parser.parse_args()
|
||||
|
||||
set_logging_format()
|
||||
set_seed(0)
|
||||
torch.backends.cudnn.benchmark = True
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
|
||||
with open(f'{os.path.dirname(args.model_dir)}/cfg.yaml', 'r') as ff:
|
||||
cfg:dict = yaml.safe_load(ff)
|
||||
for k in args.__dict__:
|
||||
if args.__dict__[k] is not None:
|
||||
cfg[k] = args.__dict__[k]
|
||||
args = OmegaConf.create(cfg)
|
||||
model = torch.load(args.model_dir, map_location='cpu', weights_only=False)
|
||||
model.args.valid_iters = args.valid_iters
|
||||
model.args.max_disp = args.max_disp
|
||||
model.cuda().eval()
|
||||
|
||||
H, W = 480, 640
|
||||
img0 = torch.randint(0, 256, (1, 3, H, W), dtype=torch.float32).cuda()
|
||||
img1 = torch.randint(0, 256, (1, 3, H, W), dtype=torch.float32).cuda()
|
||||
padder = InputPadder(img0.shape, divis_by=32, force_square=False)
|
||||
img0, img1 = padder.pad(img0, img1)
|
||||
|
||||
logging.info(f"Image size: {H}x{W}, warmup: {args.warmup}, total: {args.total}")
|
||||
|
||||
times = []
|
||||
peak_memories = []
|
||||
with torch.amp.autocast('cuda', enabled=True, dtype=AMP_DTYPE):
|
||||
for i in range(args.total):
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
torch.cuda.synchronize()
|
||||
t0 = time.perf_counter()
|
||||
disp = model.forward(img0, img1, iters=args.valid_iters, test_mode=True, optimize_build_volume='triton')
|
||||
torch.cuda.synchronize()
|
||||
elapsed = time.perf_counter() - t0
|
||||
peak_mem = torch.cuda.max_memory_allocated() / (1024 ** 2)
|
||||
times.append(elapsed)
|
||||
peak_memories.append(peak_mem)
|
||||
logging.info(f"Iter {i:2d}: {elapsed*1000:.1f} ms, peak mem: {peak_mem:.1f} MB {'(warmup)' if i < args.warmup else ''}")
|
||||
|
||||
measure_times = times[args.warmup:]
|
||||
measure_mems = peak_memories[args.warmup:]
|
||||
avg = np.mean(measure_times) * 1000
|
||||
avg_mem = np.mean(measure_mems)
|
||||
max_mem = np.max(measure_mems)
|
||||
logging.info(f"Vanilla Pytorch speed average (after warmup): {avg:.1f}[ms] over {len(measure_times)} iters")
|
||||
logging.info(f"Peak GPU memory (after warmup): avg {avg_mem:.1f} MB, max {max_mem:.1f} MB")
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# NVIDIA CORPORATION and its licensors retain all intellectual property
|
||||
# and proprietary rights in and to this software, related documentation
|
||||
# and any modifications thereto. Any use, reproduction, disclosure or
|
||||
# distribution of this software and related documentation without an express
|
||||
# license agreement from NVIDIA CORPORATION is strictly prohibited.
|
||||
|
||||
import os,sys
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
from omegaconf import OmegaConf
|
||||
from core.utils.utils import InputPadder
|
||||
import argparse, torch, logging, yaml, time
|
||||
import numpy as np
|
||||
from Utils import AMP_DTYPE, set_logging_format, set_seed
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--model_dir', default=f'{code_dir}/../weights/23-36-37/model_best_bp2_serialize.pth', type=str)
|
||||
parser.add_argument('--hiera', default=0, type=int)
|
||||
parser.add_argument('--valid_iters', type=int, default=8, help='number of flow-field updates during forward pass')
|
||||
parser.add_argument('--max_disp', type=int, default=192, help='maximum disparity')
|
||||
parser.add_argument('--warmup', type=int, default=15, help='number of warmup iterations')
|
||||
parser.add_argument('--total', type=int, default=30, help='total number of iterations')
|
||||
args = parser.parse_args()
|
||||
|
||||
set_logging_format()
|
||||
set_seed(0)
|
||||
torch.backends.cudnn.benchmark = True
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
|
||||
with open(f'{os.path.dirname(args.model_dir)}/cfg.yaml', 'r') as ff:
|
||||
cfg:dict = yaml.safe_load(ff)
|
||||
for k in args.__dict__:
|
||||
if args.__dict__[k] is not None:
|
||||
cfg[k] = args.__dict__[k]
|
||||
args = OmegaConf.create(cfg)
|
||||
model = torch.load(args.model_dir, map_location='cpu', weights_only=False)
|
||||
model.args.valid_iters = args.valid_iters
|
||||
model.args.max_disp = args.max_disp
|
||||
model.cuda().eval()
|
||||
|
||||
H, W = 480, 640
|
||||
img0 = torch.randint(0, 256, (1, 3, H, W), dtype=torch.float32).cuda()
|
||||
img1 = torch.randint(0, 256, (1, 3, H, W), dtype=torch.float32).cuda()
|
||||
padder = InputPadder(img0.shape, divis_by=32, force_square=False)
|
||||
img0, img1 = padder.pad(img0, img1)
|
||||
|
||||
logging.info(f"Image size: {H}x{W}, warmup: {args.warmup}, total: {args.total}")
|
||||
|
||||
times = []
|
||||
with torch.amp.autocast('cuda', enabled=True, dtype=AMP_DTYPE):
|
||||
for i in range(args.total):
|
||||
torch.cuda.synchronize()
|
||||
t0 = time.perf_counter()
|
||||
disp = model.forward(img0, img1, iters=args.valid_iters, test_mode=True, optimize_build_volume='triton')
|
||||
torch.cuda.synchronize()
|
||||
elapsed = time.perf_counter() - t0
|
||||
times.append(elapsed)
|
||||
logging.info(f"Iter {i:2d}: {elapsed*1000:.1f} ms {'(warmup)' if i < args.warmup else ''}")
|
||||
|
||||
measure_times = times[args.warmup:]
|
||||
avg = np.mean(measure_times) * 1000
|
||||
logging.info(f"Vanilla Pytorch speed average (after warmup): {avg:.1f}[ms] over {len(measure_times)} iters")
|
||||
@@ -0,0 +1,66 @@
|
||||
import os,sys
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
from omegaconf import OmegaConf
|
||||
import argparse, torch, logging, yaml, time
|
||||
import numpy as np
|
||||
from Utils import set_logging_format, set_seed
|
||||
from core.foundation_stereo import TrtRunner
|
||||
|
||||
|
||||
def resolve_onnx_cfg_path(onnx_dir: str) -> str:
|
||||
direct = os.path.join(onnx_dir, 'onnx.yaml')
|
||||
if os.path.exists(direct):
|
||||
return direct
|
||||
parent = os.path.join(os.path.dirname(onnx_dir), 'onnx.yaml')
|
||||
if os.path.exists(parent):
|
||||
return parent
|
||||
raise FileNotFoundError(f"onnx.yaml not found in {onnx_dir} or its parent directory")
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--model_dir', default=None, type=str)
|
||||
parser.add_argument('--hiera', default=None, type=int)
|
||||
parser.add_argument('--valid_iters', type=int, default=None, help='number of flow-field updates during forward pass (default: from onnx.yaml)')
|
||||
parser.add_argument('--max_disp', type=int, default=None, help='maximum disparity (default: from onnx.yaml)')
|
||||
parser.add_argument('--warmup', type=int, default=15, help='number of warmup iterations')
|
||||
parser.add_argument('--total', type=int, default=30, help='total number of iterations')
|
||||
parser.add_argument('--build_volume_backend', default=None, choices=['pytorch1', 'triton'], help='backend for cost-volume build (default: from onnx.yaml)')
|
||||
parser.add_argument('--onnx_dir', default=f'{code_dir}/../output', type=str, help='directory containing TensorRT engines and onnx.yaml')
|
||||
args = parser.parse_args()
|
||||
|
||||
set_logging_format()
|
||||
set_seed(0)
|
||||
torch.backends.cudnn.benchmark = True
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
|
||||
cfg_path = resolve_onnx_cfg_path(args.onnx_dir)
|
||||
with open(cfg_path, 'r') as ff:
|
||||
cfg:dict = yaml.safe_load(ff)
|
||||
for k in args.__dict__:
|
||||
if args.__dict__[k] is not None:
|
||||
cfg[k] = args.__dict__[k]
|
||||
args = OmegaConf.create(cfg)
|
||||
|
||||
model = TrtRunner(args, args.onnx_dir+'/feature_runner.engine', args.onnx_dir+'/post_runner.engine')
|
||||
|
||||
H, W = int(args.image_size[0]), int(args.image_size[1])
|
||||
img0 = torch.randint(0, 256, (1, 3, H, W), dtype=torch.float32).cuda()
|
||||
img1 = torch.randint(0, 256, (1, 3, H, W), dtype=torch.float32).cuda()
|
||||
|
||||
logging.info(f"TensorRT image size: {H}x{W}, warmup: {args.warmup}, total: {args.total}")
|
||||
|
||||
times = []
|
||||
for i in range(args.total):
|
||||
torch.cuda.synchronize()
|
||||
t0 = time.perf_counter()
|
||||
_ = model.forward(img0, img1)
|
||||
torch.cuda.synchronize()
|
||||
elapsed = time.perf_counter() - t0
|
||||
times.append(elapsed)
|
||||
logging.info(f"Iter {i:2d}: {elapsed*1000:.1f} ms {'(warmup)' if i < args.warmup else ''}")
|
||||
|
||||
measure_times = times[args.warmup:]
|
||||
avg = np.mean(measure_times) * 1000
|
||||
logging.info(f"TensorRT speed average (after warmup): {avg:.1f}[ms] over {len(measure_times)} iters")
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# NVIDIA CORPORATION and its licensors retain all intellectual property
|
||||
# and proprietary rights in and to this software, related documentation
|
||||
# and any modifications thereto. Any use, reproduction, disclosure or
|
||||
# distribution of this software and related documentation without an express
|
||||
# license agreement from NVIDIA CORPORATION is strictly prohibited.
|
||||
|
||||
import os,sys
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
from omegaconf import OmegaConf
|
||||
from core.utils.utils import InputPadder
|
||||
import argparse, torch, imageio, logging, yaml
|
||||
import numpy as np
|
||||
from Utils import (
|
||||
AMP_DTYPE, set_logging_format, set_seed, vis_disparity,
|
||||
depth2xyzmap, toOpen3dCloud, o3d,
|
||||
)
|
||||
import cv2
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--model_dir', default=f'{code_dir}/../weights/23-36-37/model_best_bp2_serialize.pth', type=str)
|
||||
parser.add_argument('--left_file', default=f'{code_dir}/../demo_data/left.png', type=str)
|
||||
parser.add_argument('--right_file', default=f'{code_dir}/../demo_data/right.png', type=str)
|
||||
parser.add_argument('--intrinsic_file', default=f'{code_dir}/../demo_data/K.txt', type=str, help='camera intrinsic matrix and baseline file')
|
||||
parser.add_argument('--out_dir', default='/home/bowen/debug/stereo_output', type=str)
|
||||
parser.add_argument('--remove_invisible', default=1, type=int)
|
||||
parser.add_argument('--denoise_cloud', default=0, type=int)
|
||||
parser.add_argument('--denoise_nb_points', type=int, default=30, help='number of points to consider for radius outlier removal')
|
||||
parser.add_argument('--denoise_radius', type=float, default=0.03, help='radius to use for outlier removal')
|
||||
parser.add_argument('--scale', default=1, type=float)
|
||||
parser.add_argument('--hiera', default=0, type=int)
|
||||
parser.add_argument('--get_pc', type=int, default=1, help='save point cloud output')
|
||||
parser.add_argument('--valid_iters', type=int, default=8, help='number of flow-field updates during forward pass')
|
||||
parser.add_argument('--max_disp', type=int, default=192, help='maximum disparity')
|
||||
parser.add_argument('--zfar', type=float, default=100, help="max depth to include in point cloud")
|
||||
args = parser.parse_args()
|
||||
|
||||
set_logging_format()
|
||||
set_seed(0)
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
|
||||
os.system(f'rm -rf {args.out_dir} && mkdir -p {args.out_dir}')
|
||||
|
||||
with open(f'{os.path.dirname(args.model_dir)}/cfg.yaml', 'r') as ff:
|
||||
cfg:dict = yaml.safe_load(ff)
|
||||
for k in args.__dict__:
|
||||
if args.__dict__[k] is not None:
|
||||
cfg[k] = args.__dict__[k]
|
||||
args = OmegaConf.create(cfg)
|
||||
logging.info(f"args:\n{args}")
|
||||
model = torch.load(args.model_dir, map_location='cpu', weights_only=False)
|
||||
model.args.valid_iters = args.valid_iters
|
||||
model.args.max_disp = args.max_disp
|
||||
|
||||
model.cuda().eval()
|
||||
|
||||
scale = args.scale
|
||||
|
||||
img0 = imageio.imread(args.left_file)
|
||||
img1 = imageio.imread(args.right_file)
|
||||
if len(img0.shape)==2:
|
||||
img0 = np.tile(img0[...,None], (1,1,3))
|
||||
img1 = np.tile(img1[...,None], (1,1,3))
|
||||
img0 = img0[...,:3]
|
||||
img1 = img1[...,:3]
|
||||
H,W = img0.shape[:2]
|
||||
|
||||
img0 = cv2.resize(img0, fx=scale, fy=scale, dsize=None)
|
||||
img1 = cv2.resize(img1, dsize=(img0.shape[1], img0.shape[0]))
|
||||
H,W = img0.shape[:2]
|
||||
img0_ori = img0.copy()
|
||||
img1_ori = img1.copy()
|
||||
logging.info(f"img0: {img0.shape}")
|
||||
imageio.imwrite(f'{args.out_dir}/left.png', img0)
|
||||
imageio.imwrite(f'{args.out_dir}/right.png', img1)
|
||||
|
||||
img0 = torch.as_tensor(img0).cuda().float()[None].permute(0,3,1,2)
|
||||
img1 = torch.as_tensor(img1).cuda().float()[None].permute(0,3,1,2)
|
||||
padder = InputPadder(img0.shape, divis_by=32, force_square=False)
|
||||
img0, img1 = padder.pad(img0, img1)
|
||||
|
||||
logging.info(f"Start forward, 1st time run can be slow due to compilation")
|
||||
with torch.amp.autocast('cuda', enabled=True, dtype=AMP_DTYPE):
|
||||
if not args.hiera:
|
||||
disp = model.forward(img0, img1, iters=args.valid_iters, test_mode=True, optimize_build_volume='pytorch1')
|
||||
else:
|
||||
disp = model.run_hierachical(img0, img1, iters=args.valid_iters, test_mode=True, small_ratio=0.5)
|
||||
logging.info("forward done")
|
||||
disp = padder.unpad(disp.float())
|
||||
disp = disp.data.cpu().numpy().reshape(H,W).clip(0, None)
|
||||
|
||||
cmap = None
|
||||
min_val = None
|
||||
max_val = None
|
||||
vis = vis_disparity(disp, min_val=min_val, max_val=max_val, cmap=cmap, color_map=cv2.COLORMAP_TURBO)
|
||||
vis = np.concatenate([img0_ori, img1_ori, vis], axis=1)
|
||||
imageio.imwrite(f'{args.out_dir}/disp_vis.png', vis)
|
||||
s = 1280/vis.shape[1]
|
||||
resized_vis = cv2.resize(vis, (int(vis.shape[1]*s), int(vis.shape[0]*s)))
|
||||
cv2.imshow('disp', resized_vis[:,:,::-1])
|
||||
cv2.waitKey(0)
|
||||
|
||||
if args.remove_invisible:
|
||||
yy,xx = np.meshgrid(np.arange(disp.shape[0]), np.arange(disp.shape[1]), indexing='ij')
|
||||
us_right = xx-disp
|
||||
invalid = us_right<0
|
||||
disp[invalid] = np.inf
|
||||
|
||||
if args.get_pc:
|
||||
with open(args.intrinsic_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
K = np.array(list(map(float, lines[0].rstrip().split()))).astype(np.float32).reshape(3,3)
|
||||
baseline = float(lines[1])
|
||||
K[:2] *= scale
|
||||
depth = K[0,0]*baseline/disp
|
||||
np.save(f'{args.out_dir}/depth_meter.npy', depth)
|
||||
xyz_map = depth2xyzmap(depth, K)
|
||||
pcd = toOpen3dCloud(xyz_map.reshape(-1,3), img0_ori.reshape(-1,3))
|
||||
keep_mask = (np.asarray(pcd.points)[:,2]>0) & (np.asarray(pcd.points)[:,2]<=args.zfar)
|
||||
keep_ids = np.arange(len(np.asarray(pcd.points)))[keep_mask]
|
||||
pcd = pcd.select_by_index(keep_ids)
|
||||
o3d.io.write_point_cloud(f'{args.out_dir}/cloud.ply', pcd)
|
||||
logging.info(f"PCL saved to {args.out_dir}")
|
||||
|
||||
if args.denoise_cloud:
|
||||
logging.info("[Optional step] denoise point cloud...")
|
||||
pcd = pcd.voxel_down_sample(voxel_size=0.001)
|
||||
cl, ind = pcd.remove_radius_outlier(nb_points=args.denoise_nb_points, radius=args.denoise_radius)
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
o3d.io.write_point_cloud(f'{args.out_dir}/cloud_denoise.ply', inlier_cloud)
|
||||
pcd = inlier_cloud
|
||||
|
||||
logging.info("Visualizing point cloud. Press ESC to exit.")
|
||||
vis = o3d.visualization.Visualizer()
|
||||
vis.create_window()
|
||||
vis.add_geometry(pcd)
|
||||
vis.get_render_option().point_size = 1.0
|
||||
vis.get_render_option().background_color = np.array([0.5, 0.5, 0.5])
|
||||
ctr = vis.get_view_control()
|
||||
ctr.set_front([0, 0, -1])
|
||||
id = np.asarray(pcd.points)[:,2].argmin()
|
||||
ctr.set_lookat(np.asarray(pcd.points)[id])
|
||||
ctr.set_up([0, -1, 0])
|
||||
vis.run()
|
||||
vis.destroy_window()
|
||||
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the FFSGWCVolume-plugin TensorRT engine from Python.
|
||||
|
||||
This is the Python equivalent of cpp/src/ffs_single_depth_inference.cpp plus
|
||||
cpp/app/main.cpp for the single-engine plugin path:
|
||||
|
||||
- engine inputs: left, right
|
||||
- engine output: disp
|
||||
- input tensors are RGB, CHW, float32, raw 0-255 values
|
||||
- resize is aspect-ratio-preserving with right/bottom replicate padding
|
||||
- disparity is cropped, nearest-neighbor upsampled, and scaled back to the
|
||||
original input-image pixel units
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import imageio.v2 as imageio
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
code_dir = Path(__file__).resolve().parent
|
||||
repo_dir = code_dir.parent
|
||||
sys.path.append(str(repo_dir))
|
||||
|
||||
from Utils import set_logging_format, set_seed, vis_disparity
|
||||
from build_plugin_trt import (
|
||||
PLUGIN_NAME,
|
||||
find_default_plugin_library,
|
||||
find_plugin_creator,
|
||||
load_plugin_library,
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run Fast-FoundationStereo single TensorRT plugin engine."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model_dir",
|
||||
type=Path,
|
||||
default=repo_dir / "engine1_plug",
|
||||
help="Directory containing fast_foundationstereo.engine and one YAML config.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--engine_file",
|
||||
"--model_file",
|
||||
dest="engine_file",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Explicit TensorRT engine path. Defaults to <model_dir>/fast_foundationstereo.engine.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config_file",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Explicit YAML config path. Defaults to the single YAML in the engine directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plugin_lib",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path to libffs_gwc_plugin.so. Defaults to cpp/build/libffs_gwc_plugin.so.",
|
||||
)
|
||||
parser.add_argument("--left_file", type=Path, default=repo_dir / "demo_data" / "left.png")
|
||||
parser.add_argument("--right_file", type=Path, default=repo_dir / "demo_data" / "right.png")
|
||||
parser.add_argument(
|
||||
"--intrinsic_file",
|
||||
type=Path,
|
||||
default=repo_dir / "demo_data" / "K.txt",
|
||||
help="Text file with 3x3 K on line 1 and baseline in meters on line 2.",
|
||||
)
|
||||
parser.add_argument("--out_dir", type=Path, default=repo_dir / "output_plugin_trt")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def resolve_engine_path(model_dir: Path, engine_file: Path | None) -> Path:
|
||||
path = engine_file if engine_file is not None else model_dir / "fast_foundationstereo.engine"
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"TensorRT engine does not exist: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def resolve_config_path(engine_path: Path, model_dir: Path, config_file: Path | None) -> Path:
|
||||
if config_file is not None:
|
||||
if not config_file.exists():
|
||||
raise FileNotFoundError(f"Config file does not exist: {config_file}")
|
||||
return config_file
|
||||
|
||||
search_dir = model_dir if model_dir.exists() else engine_path.parent
|
||||
yaml_files = sorted(
|
||||
p for p in search_dir.iterdir()
|
||||
if p.is_file() and p.suffix.lower() in (".yaml", ".yml")
|
||||
)
|
||||
if not yaml_files:
|
||||
raise FileNotFoundError(f"No YAML config found in: {search_dir}")
|
||||
if len(yaml_files) > 1:
|
||||
names = " ".join(str(p) for p in yaml_files)
|
||||
raise RuntimeError(
|
||||
f"Expected exactly one YAML config in {search_dir}, found {len(yaml_files)}: {names}"
|
||||
)
|
||||
return yaml_files[0]
|
||||
|
||||
|
||||
def load_intrinsics(path: Path) -> tuple[np.ndarray, float]:
|
||||
with path.open("r") as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) < 2:
|
||||
raise RuntimeError("intrinsic file must contain K on line 1 and baseline on line 2")
|
||||
k = np.array(list(map(float, lines[0].strip().split())), dtype=np.float32).reshape(3, 3)
|
||||
baseline = float(lines[1])
|
||||
if not (k[0, 0] > 0.0 and baseline > 0.0):
|
||||
raise RuntimeError("invalid focal length or baseline in intrinsic file")
|
||||
return k, baseline
|
||||
|
||||
|
||||
def load_rgb(path: Path) -> np.ndarray:
|
||||
img = imageio.imread(path)
|
||||
if img.ndim == 2:
|
||||
img = np.tile(img[..., None], (1, 1, 3))
|
||||
img = img[..., :3]
|
||||
if img.dtype != np.uint8:
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return np.ascontiguousarray(img)
|
||||
|
||||
|
||||
def resize_uniform_and_pad_rgb(img: np.ndarray, target_h: int, target_w: int):
|
||||
src_h, src_w = img.shape[:2]
|
||||
if src_h == target_h and src_w == target_w:
|
||||
return img.copy(), target_h, target_w
|
||||
|
||||
scale = min(float(target_w) / src_w, float(target_h) / src_h)
|
||||
scaled_w = max(1, int(round(src_w * scale)))
|
||||
scaled_h = max(1, int(round(src_h * scale)))
|
||||
|
||||
scaled = cv2.resize(img, (scaled_w, scaled_h), interpolation=cv2.INTER_LINEAR)
|
||||
padded = cv2.copyMakeBorder(
|
||||
scaled,
|
||||
0,
|
||||
target_h - scaled_h,
|
||||
0,
|
||||
target_w - scaled_w,
|
||||
cv2.BORDER_REPLICATE,
|
||||
)
|
||||
return np.ascontiguousarray(padded), scaled_h, scaled_w
|
||||
|
||||
|
||||
def tensor_from_rgb_255(img: np.ndarray) -> torch.Tensor:
|
||||
arr = img.astype(np.float32, copy=False)
|
||||
return torch.as_tensor(arr, device="cuda").permute(2, 0, 1).unsqueeze(0).contiguous()
|
||||
|
||||
|
||||
def cpp_nearest_resize(src: np.ndarray, dst_h: int, dst_w: int) -> np.ndarray:
|
||||
src_h, src_w = src.shape
|
||||
xs = np.rint((np.arange(dst_w, dtype=np.float32) + 0.5) * src_w / dst_w - 0.5)
|
||||
ys = np.rint((np.arange(dst_h, dtype=np.float32) + 0.5) * src_h / dst_h - 0.5)
|
||||
xs = np.clip(xs.astype(np.int64), 0, src_w - 1)
|
||||
ys = np.clip(ys.astype(np.int64), 0, src_h - 1)
|
||||
return src[ys[:, None], xs[None, :]]
|
||||
|
||||
|
||||
def postprocess_disparity(
|
||||
raw_disp: np.ndarray,
|
||||
input_h: int,
|
||||
input_w: int,
|
||||
model_h: int,
|
||||
model_w: int,
|
||||
scaled_h: int,
|
||||
scaled_w: int,
|
||||
) -> np.ndarray:
|
||||
disp = raw_disp.reshape(model_h, model_w).astype(np.float32, copy=False)
|
||||
disp = np.maximum(disp, 0.0)
|
||||
|
||||
if input_h == model_h and input_w == model_w:
|
||||
return np.ascontiguousarray(disp)
|
||||
|
||||
cropped = disp[:scaled_h, :scaled_w]
|
||||
upsampled = cpp_nearest_resize(cropped, input_h, input_w)
|
||||
return np.ascontiguousarray(upsampled * (float(input_w) / scaled_w), dtype=np.float32)
|
||||
|
||||
|
||||
def disparity_to_depth_cpp(disp: np.ndarray, fx: float, baseline_m: float) -> np.ndarray:
|
||||
d = disp.astype(np.float32, copy=True)
|
||||
xs = np.arange(d.shape[1], dtype=np.float32)[None, :]
|
||||
d[(xs - d) < 0.0] = np.inf
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
depth = np.float32(fx * baseline_m) / d
|
||||
return np.ascontiguousarray(depth, dtype=np.float32)
|
||||
|
||||
|
||||
def save_float_matrix_bin(path: Path, values: np.ndarray) -> None:
|
||||
arr = np.ascontiguousarray(values, dtype=np.float32)
|
||||
dims = np.array(arr.shape[:2], dtype=np.int32)
|
||||
with path.open("wb") as f:
|
||||
dims.tofile(f)
|
||||
arr.tofile(f)
|
||||
|
||||
|
||||
def colorize_depth(depth: np.ndarray) -> np.ndarray:
|
||||
valid = np.isfinite(depth) & (depth > 0.0)
|
||||
if not np.any(valid):
|
||||
return np.zeros((*depth.shape, 3), dtype=np.uint8)
|
||||
|
||||
safe = np.zeros_like(depth, dtype=np.float32)
|
||||
safe[valid] = depth[valid]
|
||||
min_val = float(safe[valid].min())
|
||||
max_val = float(safe[valid].max())
|
||||
if max_val <= min_val:
|
||||
max_val = min_val + 1.0
|
||||
|
||||
depth_u8 = np.clip((safe - min_val) * (255.0 / (max_val - min_val)), 0, 255).astype(np.uint8)
|
||||
colored_bgr = cv2.applyColorMap(depth_u8, cv2.COLORMAP_TURBO)
|
||||
colored = colored_bgr[..., ::-1]
|
||||
colored[~valid] = 0
|
||||
return colored
|
||||
|
||||
|
||||
class PluginTensorRTRunner:
|
||||
def __init__(self, engine_path: Path, plugin_lib: Path):
|
||||
import tensorrt as trt
|
||||
|
||||
self.trt = trt
|
||||
self.logger = trt.Logger(trt.Logger.WARNING)
|
||||
trt.init_libnvinfer_plugins(self.logger, "")
|
||||
load_plugin_library(str(plugin_lib))
|
||||
if not find_plugin_creator(trt):
|
||||
raise RuntimeError(f"{PLUGIN_NAME} plugin creator is not registered after loading {plugin_lib}")
|
||||
|
||||
self.runtime = trt.Runtime(self.logger)
|
||||
self.engine = self.runtime.deserialize_cuda_engine(engine_path.read_bytes())
|
||||
if self.engine is None:
|
||||
raise RuntimeError(f"Failed to deserialize TensorRT engine: {engine_path}")
|
||||
self.context = self.engine.create_execution_context()
|
||||
if self.context is None:
|
||||
raise RuntimeError(f"Failed to create execution context: {engine_path}")
|
||||
|
||||
names = [self.engine.get_tensor_name(i) for i in range(self.engine.num_io_tensors)]
|
||||
missing = [name for name in ("left", "right", "disp") if name not in names]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"Plugin engine must expose tensors named left, right, and disp. "
|
||||
f"Missing {missing}; found {names}"
|
||||
)
|
||||
|
||||
def _torch_dtype(self, trt_dtype):
|
||||
trt = self.trt
|
||||
mapping = {
|
||||
trt.DataType.FLOAT: torch.float32,
|
||||
trt.DataType.HALF: torch.float16,
|
||||
trt.DataType.INT32: torch.int32,
|
||||
trt.DataType.INT8: torch.int8,
|
||||
trt.DataType.BOOL: torch.bool,
|
||||
}
|
||||
if hasattr(trt.DataType, "BF16"):
|
||||
mapping[trt.DataType.BF16] = torch.bfloat16
|
||||
if trt_dtype not in mapping:
|
||||
raise RuntimeError(f"Unsupported TensorRT dtype: {trt_dtype}")
|
||||
return mapping[trt_dtype]
|
||||
|
||||
def infer(self, left: torch.Tensor, right: torch.Tensor) -> torch.Tensor:
|
||||
inputs = {"left": left, "right": right}
|
||||
for name, tensor in list(inputs.items()):
|
||||
expected = self._torch_dtype(self.engine.get_tensor_dtype(name))
|
||||
if tensor.dtype != expected:
|
||||
tensor = tensor.to(expected)
|
||||
inputs[name] = tensor.contiguous()
|
||||
self.context.set_input_shape(name, tuple(inputs[name].shape))
|
||||
|
||||
out_shape = tuple(self.context.get_tensor_shape("disp"))
|
||||
out_dtype = self._torch_dtype(self.engine.get_tensor_dtype("disp"))
|
||||
output = torch.empty(out_shape, device="cuda", dtype=out_dtype)
|
||||
|
||||
self.context.set_tensor_address("left", int(inputs["left"].data_ptr()))
|
||||
self.context.set_tensor_address("right", int(inputs["right"].data_ptr()))
|
||||
self.context.set_tensor_address("disp", int(output.data_ptr()))
|
||||
|
||||
stream = torch.cuda.current_stream().cuda_stream
|
||||
if not self.context.execute_async_v3(stream):
|
||||
raise RuntimeError("TensorRT enqueue failed")
|
||||
return output
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
set_logging_format()
|
||||
set_seed(0)
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
engine_path = resolve_engine_path(args.model_dir, args.engine_file)
|
||||
config_path = resolve_config_path(engine_path, args.model_dir, args.config_file)
|
||||
plugin_lib = args.plugin_lib or find_default_plugin_library()
|
||||
if plugin_lib is None or not plugin_lib.exists():
|
||||
raise FileNotFoundError(
|
||||
"Could not find libffs_gwc_plugin.so. Build cpp first or pass "
|
||||
"--plugin_lib /path/to/libffs_gwc_plugin.so"
|
||||
)
|
||||
|
||||
with config_path.open("r") as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
model_h, model_w = [int(v) for v in cfg["image_size"]]
|
||||
|
||||
left = load_rgb(args.left_file)
|
||||
right = load_rgb(args.right_file)
|
||||
if left.shape != right.shape:
|
||||
raise RuntimeError(f"left/right image size mismatch: {left.shape} vs {right.shape}")
|
||||
input_h, input_w = left.shape[:2]
|
||||
|
||||
logging.info(f"Engine: {engine_path}")
|
||||
logging.info(f"Plugin: {plugin_lib}")
|
||||
logging.info(f"Config: {config_path}")
|
||||
logging.info(f"Input images: {input_w}x{input_h}")
|
||||
logging.info(f"Model target resolution: {model_w}x{model_h}")
|
||||
|
||||
left_model, scaled_h, scaled_w = resize_uniform_and_pad_rgb(left, model_h, model_w)
|
||||
right_model, right_scaled_h, right_scaled_w = resize_uniform_and_pad_rgb(right, model_h, model_w)
|
||||
if (scaled_h, scaled_w) != (right_scaled_h, right_scaled_w):
|
||||
raise RuntimeError("left/right images produced different resize scales")
|
||||
if (input_h, input_w) != (model_h, model_w):
|
||||
logging.info(
|
||||
f"Uniform resize+pad: {input_w}x{input_h} -> "
|
||||
f"{scaled_w}x{scaled_h} inside {model_w}x{model_h}"
|
||||
)
|
||||
|
||||
runner = PluginTensorRTRunner(engine_path, plugin_lib)
|
||||
t_left = tensor_from_rgb_255(left_model)
|
||||
t_right = tensor_from_rgb_255(right_model)
|
||||
|
||||
logging.info("Running TensorRT inference")
|
||||
disp_raw_t = runner.infer(t_left, t_right)
|
||||
torch.cuda.current_stream().synchronize()
|
||||
logging.info("Inference done")
|
||||
|
||||
disp_raw = disp_raw_t.float().detach().cpu().numpy()
|
||||
disp = postprocess_disparity(
|
||||
disp_raw,
|
||||
input_h=input_h,
|
||||
input_w=input_w,
|
||||
model_h=model_h,
|
||||
model_w=model_w,
|
||||
scaled_h=scaled_h,
|
||||
scaled_w=scaled_w,
|
||||
)
|
||||
|
||||
if not np.isfinite(disp).any():
|
||||
raise RuntimeError("Model produced no finite disparity values")
|
||||
|
||||
k, baseline = load_intrinsics(args.intrinsic_file)
|
||||
depth = disparity_to_depth_cpp(disp, float(k[0, 0]), baseline)
|
||||
|
||||
disparity_path = args.out_dir / "disparity.bin"
|
||||
depth_bin_path = args.out_dir / "depth_meter.bin"
|
||||
depth_npy_path = args.out_dir / "depth_meter.npy"
|
||||
disp_vis_path = args.out_dir / "disp_vis.png"
|
||||
depth_vis_path = args.out_dir / "depth_vis.png"
|
||||
|
||||
save_float_matrix_bin(disparity_path, disp)
|
||||
save_float_matrix_bin(depth_bin_path, depth)
|
||||
np.save(depth_npy_path, depth)
|
||||
|
||||
disp_color = vis_disparity(disp, color_map=cv2.COLORMAP_TURBO)
|
||||
disp_vis = np.concatenate([left, right, disp_color], axis=1)
|
||||
imageio.imwrite(disp_vis_path, disp_vis)
|
||||
imageio.imwrite(depth_vis_path, colorize_depth(depth))
|
||||
|
||||
logging.info(f"Saved: {disparity_path}")
|
||||
logging.info(f"Saved: {depth_bin_path}")
|
||||
logging.info(f"Saved: {depth_npy_path}")
|
||||
logging.info(f"Saved: {disp_vis_path}")
|
||||
logging.info(f"Saved: {depth_vis_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,348 @@
|
||||
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
|
||||
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
Run Fast FoundationStereo inference with the single ONNX model (or TRT engine)
|
||||
produced by make_single_onnx.py.
|
||||
|
||||
Supports two backends:
|
||||
- ONNX Runtime (default if --model_file points to an .onnx, or auto-detected)
|
||||
- TensorRT (if --model_file points to an .engine)
|
||||
|
||||
The model expects ImageNet-normalised inputs, so this script applies
|
||||
normalisation during preprocessing.
|
||||
|
||||
Usage:
|
||||
# Run directly with ONNX (no trtexec step needed):
|
||||
python run_demo_single_trt.py \
|
||||
--model_dir ./output_single_onnx \
|
||||
--left_file ../demo_data/left.png \
|
||||
--right_file ../demo_data/right.png
|
||||
|
||||
# Or with an explicit model file:
|
||||
python run_demo_single_trt.py \
|
||||
--model_dir ./output_single_onnx \
|
||||
--model_file ./output_single_onnx/fast_foundationstereo.onnx \
|
||||
--left_file ../demo_data/left.png \
|
||||
--right_file ../demo_data/right.png
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import cv2
|
||||
import imageio
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
|
||||
from Utils import (
|
||||
set_logging_format, set_seed, vis_disparity,
|
||||
depth2xyzmap, toOpen3dCloud, o3d,
|
||||
)
|
||||
|
||||
IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
|
||||
IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
||||
|
||||
|
||||
class SingleEngineTrtRunner:
|
||||
"""Minimal TensorRT runner for a single engine with named I/O."""
|
||||
|
||||
def __init__(self, engine_path):
|
||||
import tensorrt as trt
|
||||
self.trt = trt
|
||||
self.logger = trt.Logger(trt.Logger.WARNING)
|
||||
|
||||
with open(engine_path, 'rb') as f:
|
||||
self.engine = trt.Runtime(self.logger).deserialize_cuda_engine(f.read())
|
||||
if self.engine is None:
|
||||
raise RuntimeError(
|
||||
f'Failed to deserialize TRT engine from {engine_path}. '
|
||||
f'This usually means the engine was built with a different '
|
||||
f'TensorRT version (yours: {trt.__version__}). '
|
||||
f'Rebuild with: trtexec --onnx=<your .onnx> '
|
||||
f'--saveEngine={engine_path} --fp16')
|
||||
self.context = self.engine.create_execution_context()
|
||||
|
||||
def _trt_to_torch_dtype(self, dt):
|
||||
trt = self.trt
|
||||
mapping = {
|
||||
trt.DataType.FLOAT: torch.float32,
|
||||
trt.DataType.HALF: torch.float16,
|
||||
trt.DataType.BF16: torch.bfloat16,
|
||||
trt.DataType.INT32: torch.int32,
|
||||
trt.DataType.INT8: torch.int8,
|
||||
trt.DataType.BOOL: torch.bool,
|
||||
}
|
||||
if dt not in mapping:
|
||||
raise RuntimeError(f'Unsupported TRT dtype: {dt}')
|
||||
return mapping[dt]
|
||||
|
||||
def __call__(self, inputs: dict) -> dict:
|
||||
"""Run inference.
|
||||
|
||||
Args:
|
||||
inputs: {binding_name: torch.Tensor} for every input tensor.
|
||||
Returns:
|
||||
{binding_name: torch.Tensor} for every output tensor.
|
||||
"""
|
||||
trt = self.trt
|
||||
|
||||
for name, tensor in inputs.items():
|
||||
expected = self._trt_to_torch_dtype(self.engine.get_tensor_dtype(name))
|
||||
if tensor.dtype != expected:
|
||||
inputs[name] = tensor.to(expected)
|
||||
if not inputs[name].is_contiguous():
|
||||
inputs[name] = inputs[name].contiguous()
|
||||
self.context.set_input_shape(name, tuple(inputs[name].shape))
|
||||
|
||||
out_names = [
|
||||
self.engine.get_tensor_name(i)
|
||||
for i in range(self.engine.num_io_tensors)
|
||||
if self.engine.get_tensor_mode(self.engine.get_tensor_name(i))
|
||||
== trt.TensorIOMode.OUTPUT
|
||||
]
|
||||
|
||||
outputs = {}
|
||||
for name in out_names:
|
||||
shape = tuple(self.context.get_tensor_shape(name))
|
||||
dtype = self._trt_to_torch_dtype(self.engine.get_tensor_dtype(name))
|
||||
outputs[name] = torch.empty(shape, device='cuda', dtype=dtype)
|
||||
|
||||
for name, tensor in inputs.items():
|
||||
self.context.set_tensor_address(name, int(tensor.data_ptr()))
|
||||
for name, tensor in outputs.items():
|
||||
self.context.set_tensor_address(name, int(tensor.data_ptr()))
|
||||
|
||||
stream = torch.cuda.current_stream().cuda_stream
|
||||
assert self.context.execute_async_v3(stream)
|
||||
|
||||
return outputs
|
||||
|
||||
|
||||
class OnnxRuntimeRunner:
|
||||
"""Run inference via ONNX Runtime (GPU if available, else CPU)."""
|
||||
|
||||
def __init__(self, onnx_path):
|
||||
import onnxruntime as ort
|
||||
providers = []
|
||||
if 'CUDAExecutionProvider' in ort.get_available_providers():
|
||||
providers.append('CUDAExecutionProvider')
|
||||
providers.append('CPUExecutionProvider')
|
||||
logging.info(f'ONNX Runtime providers: {providers}')
|
||||
self.session = ort.InferenceSession(onnx_path, providers=providers)
|
||||
self.input_names = [inp.name for inp in self.session.get_inputs()]
|
||||
self.output_names = [out.name for out in self.session.get_outputs()]
|
||||
|
||||
def __call__(self, inputs: dict) -> dict:
|
||||
feed = {}
|
||||
for name in self.input_names:
|
||||
tensor = inputs[name]
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
tensor = tensor.cpu().float().numpy()
|
||||
feed[name] = tensor
|
||||
raw_outputs = self.session.run(self.output_names, feed)
|
||||
outputs = {}
|
||||
for name, arr in zip(self.output_names, raw_outputs):
|
||||
outputs[name] = torch.as_tensor(arr).cuda()
|
||||
return outputs
|
||||
|
||||
|
||||
def normalize_imagenet(img_uint8: np.ndarray) -> np.ndarray:
|
||||
"""Apply ImageNet normalization: (img/255 - mean) / std."""
|
||||
return ((img_uint8.astype(np.float32) / 255.0) - IMAGENET_MEAN) / IMAGENET_STD
|
||||
|
||||
|
||||
def resolve_config(model_path: str) -> str:
|
||||
"""Find the YAML config matching the model file, falling back to defaults."""
|
||||
model_dir = os.path.dirname(model_path)
|
||||
base = os.path.splitext(os.path.basename(model_path))[0]
|
||||
candidates = [
|
||||
os.path.join(model_dir, f'{base}.yaml'),
|
||||
os.path.join(model_dir, 'config.yaml'),
|
||||
os.path.join(model_dir, 'onnx.yaml'),
|
||||
]
|
||||
for p in candidates:
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
raise FileNotFoundError(
|
||||
f'No .yaml config found for {model_path}. '
|
||||
'Run make_single_onnx.py first.')
|
||||
|
||||
|
||||
def find_model(model_dir: str) -> str:
|
||||
"""Find an .engine or .onnx file in the directory (prefer .engine)."""
|
||||
for ext in ('.engine', '.onnx'):
|
||||
for f in os.listdir(model_dir):
|
||||
if f.endswith(ext):
|
||||
return os.path.join(model_dir, f)
|
||||
raise FileNotFoundError(
|
||||
f'No .engine or .onnx file found in {model_dir}. '
|
||||
'Run make_single_onnx.py first.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Run Fast FoundationStereo with ONNX Runtime or TensorRT')
|
||||
parser.add_argument('--model_dir', type=str,
|
||||
default=f'{code_dir}/output_single_onnx',
|
||||
help='Directory containing .onnx/.engine + config.yaml')
|
||||
parser.add_argument('--model_file', type=str, default='',
|
||||
help='Explicit path to .onnx or .engine file (overrides auto-search)')
|
||||
parser.add_argument('--left_file', type=str,
|
||||
default=f'{code_dir}/../demo_data/left.png')
|
||||
parser.add_argument('--right_file', type=str,
|
||||
default=f'{code_dir}/../demo_data/right.png')
|
||||
parser.add_argument('--intrinsic_file', type=str,
|
||||
default=f'{code_dir}/../demo_data/K.txt',
|
||||
help='Camera intrinsic matrix and baseline file')
|
||||
parser.add_argument('--out_dir', type=str,
|
||||
default=f'{code_dir}/../output_demo')
|
||||
parser.add_argument('--remove_invisible', type=int, default=1)
|
||||
parser.add_argument('--denoise_cloud', type=int, default=1)
|
||||
parser.add_argument('--denoise_nb_points', type=int, default=30)
|
||||
parser.add_argument('--denoise_radius', type=float, default=0.03)
|
||||
parser.add_argument('--get_pc', type=int, default=1,
|
||||
help='Generate and save point cloud')
|
||||
parser.add_argument('--zfar', type=float, default=100,
|
||||
help='Max depth (m) to include in point cloud')
|
||||
args = parser.parse_args()
|
||||
|
||||
set_logging_format()
|
||||
set_seed(0)
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
os.makedirs(args.out_dir, exist_ok=True)
|
||||
|
||||
# ── Find model and config ─────────────────────────────────────────────
|
||||
model_path = args.model_file if args.model_file else find_model(args.model_dir)
|
||||
cfg_path = resolve_config(model_path)
|
||||
with open(cfg_path, 'r') as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
target_h, target_w = cfg['image_size']
|
||||
logging.info(f'Model target resolution: {target_h} x {target_w}')
|
||||
|
||||
# ── Load model (ONNX Runtime or TensorRT) ────────────────────────────
|
||||
logging.info(f'Loading model: {model_path}')
|
||||
if model_path.endswith('.onnx'):
|
||||
runner = OnnxRuntimeRunner(model_path)
|
||||
else:
|
||||
runner = SingleEngineTrtRunner(model_path)
|
||||
|
||||
# ── Read images ───────────────────────────────────────────────────────
|
||||
img0 = imageio.imread(args.left_file)
|
||||
img1 = imageio.imread(args.right_file)
|
||||
|
||||
if img0.ndim == 2:
|
||||
img0 = np.tile(img0[..., None], (1, 1, 3))
|
||||
img1 = np.tile(img1[..., None], (1, 1, 3))
|
||||
img0 = img0[..., :3]
|
||||
img1 = img1[..., :3]
|
||||
|
||||
# ── Resize to model resolution (direct stretch) ────────────────────────
|
||||
orig_h, orig_w = img0.shape[:2]
|
||||
fx = target_w / orig_w
|
||||
fy = target_h / orig_h
|
||||
|
||||
if fx != 1 or fy != 1:
|
||||
logging.info(
|
||||
f'Resizing images: {orig_h}x{orig_w} → {target_h}x{target_w} '
|
||||
f'(fx={fx:.4f}, fy={fy:.4f})')
|
||||
img0 = cv2.resize(img0, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
|
||||
img1 = cv2.resize(img1, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
|
||||
H, W = img0.shape[:2]
|
||||
|
||||
img0_ori = img0.copy()
|
||||
img1_ori = img1.copy()
|
||||
logging.info(f'Image size after resize: {img0.shape}')
|
||||
imageio.imwrite(f'{args.out_dir}/left.png', img0)
|
||||
imageio.imwrite(f'{args.out_dir}/right.png', img1)
|
||||
|
||||
# ── Preprocess: ImageNet normalize → NCHW float tensor ────────────────
|
||||
img0_norm = normalize_imagenet(img0)
|
||||
img1_norm = normalize_imagenet(img1)
|
||||
|
||||
t_left = torch.as_tensor(img0_norm).cuda().float()[None].permute(0, 3, 1, 2)
|
||||
t_right = torch.as_tensor(img1_norm).cuda().float()[None].permute(0, 3, 1, 2)
|
||||
|
||||
# ── Inference ─────────────────────────────────────────────────────────
|
||||
logging.info('Running inference (first run may be slow due to TRT warmup)')
|
||||
outputs = runner({'left_image': t_left, 'right_image': t_right})
|
||||
disp = outputs['disparity']
|
||||
logging.info('Inference done')
|
||||
|
||||
disp = disp.float().cpu().numpy().reshape(H, W).clip(0, None) * (1.0 / fx)
|
||||
|
||||
# ── Visualise disparity ──────────────────────────────────────────────
|
||||
vis = vis_disparity(disp, color_map=cv2.COLORMAP_TURBO)
|
||||
vis = np.concatenate([img0_ori, img1_ori, vis], axis=1)
|
||||
imageio.imwrite(f'{args.out_dir}/disp_vis.png', vis)
|
||||
s = 1280 / vis.shape[1]
|
||||
resized_vis = cv2.resize(vis, (int(vis.shape[1] * s), int(vis.shape[0] * s)))
|
||||
cv2.imshow('disp', resized_vis[:, :, ::-1])
|
||||
cv2.waitKey(0)
|
||||
|
||||
# ── Remove invisible pixels ──────────────────────────────────────────
|
||||
if args.remove_invisible:
|
||||
_, xx = np.meshgrid(np.arange(H), np.arange(W), indexing='ij')
|
||||
invalid = (xx - disp) < 0
|
||||
disp[invalid] = np.inf
|
||||
|
||||
# ── Point cloud generation ───────────────────────────────────────────
|
||||
if args.get_pc:
|
||||
with open(args.intrinsic_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
K = (np.array(list(map(float, lines[0].rstrip().split())))
|
||||
.astype(np.float32).reshape(3, 3))
|
||||
baseline = float(lines[1])
|
||||
K[:2] *= np.array([fx, fy], dtype=np.float32)[:, np.newaxis]
|
||||
depth = K[0, 0] * baseline / disp
|
||||
np.save(f'{args.out_dir}/depth_meter.npy', depth)
|
||||
|
||||
xyz_map = depth2xyzmap(depth, K)
|
||||
pcd = toOpen3dCloud(xyz_map.reshape(-1, 3), img0_ori.reshape(-1, 3))
|
||||
pts = np.asarray(pcd.points)
|
||||
keep = (pts[:, 2] > 0) & (pts[:, 2] <= args.zfar)
|
||||
pcd = pcd.select_by_index(np.where(keep)[0])
|
||||
o3d.io.write_point_cloud(f'{args.out_dir}/cloud.ply', pcd)
|
||||
logging.info(f'Point cloud saved to {args.out_dir}')
|
||||
|
||||
if args.denoise_cloud:
|
||||
logging.info('Denoising point cloud...')
|
||||
_, ind = pcd.remove_radius_outlier(
|
||||
nb_points=args.denoise_nb_points,
|
||||
radius=args.denoise_radius)
|
||||
pcd = pcd.select_by_index(ind)
|
||||
o3d.io.write_point_cloud(f'{args.out_dir}/cloud_denoise.ply', pcd)
|
||||
|
||||
logging.info('Visualizing point cloud. Press ESC to exit.')
|
||||
vis = o3d.visualization.Visualizer()
|
||||
vis.create_window()
|
||||
vis.add_geometry(pcd)
|
||||
vis.get_render_option().point_size = 1.0
|
||||
vis.get_render_option().background_color = np.array([0.5, 0.5, 0.5])
|
||||
ctr = vis.get_view_control()
|
||||
ctr.set_front([0, 0, -1])
|
||||
closest = np.asarray(pcd.points)[:, 2].argmin()
|
||||
ctr.set_lookat(np.asarray(pcd.points)[closest])
|
||||
ctr.set_up([0, -1, 0])
|
||||
vis.run()
|
||||
vis.destroy_window()
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import os,sys
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.append(f'{code_dir}/../')
|
||||
from omegaconf import OmegaConf
|
||||
from core.utils.utils import InputPadder
|
||||
import argparse, torch, logging, yaml
|
||||
import imageio
|
||||
import numpy as np
|
||||
from Utils import (
|
||||
set_logging_format, set_seed, vis_disparity,
|
||||
depth2xyzmap, toOpen3dCloud, o3d,
|
||||
)
|
||||
from core.foundation_stereo import TrtRunner
|
||||
import cv2
|
||||
|
||||
|
||||
def resolve_onnx_cfg_path(onnx_dir: str) -> str:
|
||||
onnx_dir = os.path.normpath(onnx_dir)
|
||||
candidates = [
|
||||
os.path.join(onnx_dir, 'onnx.yaml'),
|
||||
os.path.join(os.path.dirname(onnx_dir), 'onnx.yaml'),
|
||||
]
|
||||
for p in candidates:
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
raise FileNotFoundError(
|
||||
f"onnx.yaml not found. Looked in: {candidates}. "
|
||||
"Please run scripts/make_onnx.py first to generate ONNX metadata."
|
||||
)
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
code_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--onnx_dir', default=f'{code_dir}/output', type=str)
|
||||
parser.add_argument('--left_file', default=f'{code_dir}/../assets/left.png', type=str)
|
||||
parser.add_argument('--right_file', default=f'{code_dir}/../assets/right.png', type=str)
|
||||
parser.add_argument('--intrinsic_file', default=f'{code_dir}/../assets/K.txt', type=str, help='camera intrinsic matrix and baseline file')
|
||||
parser.add_argument('--out_dir', default='/home/bowen/debug/stereo_output', type=str)
|
||||
parser.add_argument('--remove_invisible', default=1, type=int)
|
||||
parser.add_argument('--denoise_cloud', default=1, type=int)
|
||||
parser.add_argument('--denoise_nb_points', type=int, default=30, help='number of points to consider for radius outlier removal')
|
||||
parser.add_argument('--denoise_radius', type=float, default=0.03, help='radius to use for outlier removal')
|
||||
parser.add_argument('--get_pc', type=int, default=1, help='save point cloud output')
|
||||
parser.add_argument('--zfar', type=float, default=100, help="max depth to include in point cloud")
|
||||
args = parser.parse_args()
|
||||
|
||||
set_logging_format()
|
||||
set_seed(0)
|
||||
torch.autograd.set_grad_enabled(False)
|
||||
os.makedirs(args.out_dir, exist_ok=True)
|
||||
|
||||
onnx_cfg_path = resolve_onnx_cfg_path(args.onnx_dir)
|
||||
with open(onnx_cfg_path, 'r') as ff:
|
||||
cfg:dict = yaml.safe_load(ff)
|
||||
for k in args.__dict__:
|
||||
if args.__dict__[k] is not None:
|
||||
cfg[k] = args.__dict__[k]
|
||||
args = OmegaConf.create(cfg)
|
||||
logging.info(f"args:\n{args}")
|
||||
model = TrtRunner(args, args.onnx_dir+'/feature_runner.engine', args.onnx_dir+'/post_runner.engine')
|
||||
|
||||
img0 = imageio.imread(args.left_file)
|
||||
img1 = imageio.imread(args.right_file)
|
||||
if len(img0.shape)==2:
|
||||
img0 = np.tile(img0[...,None], (1,1,3))
|
||||
img1 = np.tile(img1[...,None], (1,1,3))
|
||||
img0 = img0[...,:3]
|
||||
img1 = img1[...,:3]
|
||||
H,W = img0.shape[:2]
|
||||
|
||||
fx = args.image_size[1] / img0.shape[1]
|
||||
fy = args.image_size[0] / img0.shape[0]
|
||||
if fx != 1 or fy != 1:
|
||||
logging.info(f">>>>>>>>>>>>>>>WARNING: resizing image to {args.image_size}, fx: {fx}, fy: {fy}, this is not recommended. It's best to make tensorrt engine with the same image size as the input image.")
|
||||
img0 = cv2.resize(img0, fx=fx, fy=fy, dsize=None)
|
||||
img1 = cv2.resize(img1, fx=fx, fy=fy, dsize=None)
|
||||
H,W = img0.shape[:2]
|
||||
img0_ori = img0.copy()
|
||||
img1_ori = img1.copy()
|
||||
logging.info(f"img0: {img0.shape}")
|
||||
imageio.imwrite(f'{args.out_dir}/left.png', img0)
|
||||
imageio.imwrite(f'{args.out_dir}/right.png', img1)
|
||||
|
||||
img0 = torch.as_tensor(img0).cuda().float()[None].permute(0,3,1,2)
|
||||
img1 = torch.as_tensor(img1).cuda().float()[None].permute(0,3,1,2)
|
||||
|
||||
logging.info(f"Start forward, 1st time run can be slow due to compilation")
|
||||
disp = model.forward(img0, img1)
|
||||
logging.info("forward done")
|
||||
disp = disp.data.cpu().numpy().reshape(H,W).clip(0, None) * 1/fx
|
||||
|
||||
cmap = None
|
||||
min_val = None
|
||||
max_val = None
|
||||
vis = vis_disparity(disp, min_val=min_val, max_val=max_val, cmap=cmap, color_map=cv2.COLORMAP_TURBO)
|
||||
vis = np.concatenate([img0_ori, img1_ori, vis], axis=1)
|
||||
imageio.imwrite(f'{args.out_dir}/disp_vis.png', vis)
|
||||
s = 1280/vis.shape[1]
|
||||
resized_vis = cv2.resize(vis, (int(vis.shape[1]*s), int(vis.shape[0]*s)))
|
||||
cv2.imshow('disp', resized_vis[:,:,::-1])
|
||||
cv2.waitKey(0)
|
||||
|
||||
if args.remove_invisible:
|
||||
yy,xx = np.meshgrid(np.arange(disp.shape[0]), np.arange(disp.shape[1]), indexing='ij')
|
||||
us_right = xx-disp
|
||||
invalid = us_right<0
|
||||
disp[invalid] = np.inf
|
||||
|
||||
if args.get_pc:
|
||||
with open(args.intrinsic_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
K = np.array(list(map(float, lines[0].rstrip().split()))).astype(np.float32).reshape(3,3)
|
||||
baseline = float(lines[1])
|
||||
K[:2] *= np.array([fx, fy], dtype=np.float32)[:, np.newaxis]
|
||||
depth = K[0,0]*baseline/disp
|
||||
np.save(f'{args.out_dir}/depth_meter.npy', depth)
|
||||
xyz_map = depth2xyzmap(depth, K)
|
||||
pcd = toOpen3dCloud(xyz_map.reshape(-1,3), img0_ori.reshape(-1,3))
|
||||
keep_mask = (np.asarray(pcd.points)[:,2]>0) & (np.asarray(pcd.points)[:,2]<=args.zfar)
|
||||
keep_ids = np.arange(len(np.asarray(pcd.points)))[keep_mask]
|
||||
pcd = pcd.select_by_index(keep_ids)
|
||||
o3d.io.write_point_cloud(f'{args.out_dir}/cloud.ply', pcd)
|
||||
logging.info(f"PCL saved to {args.out_dir}")
|
||||
|
||||
if args.denoise_cloud:
|
||||
logging.info("[Optional step] denoise point cloud...")
|
||||
cl, ind = pcd.remove_radius_outlier(nb_points=args.denoise_nb_points, radius=args.denoise_radius)
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
o3d.io.write_point_cloud(f'{args.out_dir}/cloud_denoise.ply', inlier_cloud)
|
||||
pcd = inlier_cloud
|
||||
|
||||
logging.info("Visualizing point cloud. Press ESC to exit.")
|
||||
vis = o3d.visualization.Visualizer()
|
||||
vis.create_window()
|
||||
vis.add_geometry(pcd)
|
||||
vis.get_render_option().point_size = 1.0
|
||||
vis.get_render_option().background_color = np.array([0.5, 0.5, 0.5])
|
||||
ctr = vis.get_view_control()
|
||||
ctr.set_front([0, 0, -1])
|
||||
id = np.asarray(pcd.points)[:,2].argmin()
|
||||
ctr.set_lookat(np.asarray(pcd.points)[id])
|
||||
ctr.set_up([0, -1, 0])
|
||||
vis.run()
|
||||
vis.destroy_window()
|
||||
Reference in New Issue
Block a user