Files
blackbird/actpred/predict.py
T
qing d17677bade fix(actpred): ONNX opset 升级 + NumPy 兼容 + 断点续训 + MobileNetV2 离线弹性
- export_onnx: opset 17→18,dynamic_axes 改 dynamic_shapes 消除弃用警告
- predict: action_stats 改为 torch tensor,消除 NumPy 2.0 __array_wrap__ 警告
- train/pretrain: 每 epoch 保存 last.pt,新增 --resume 断点续训练
- model: MobileNetEncoder 下载失败时回退到已缓存权重
- _set.sh: 安装时预缓存 MobileNetV2 权重,失败给出明确指引
- test: 新增 TestResumeCheckpoint(2 用例),总计 58 测试全绿
2026-08-10 04:07:37 +00:00

138 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import argparse
import json
import os
import numpy as np
import torch
from PIL import Image
from torchvision import transforms
from config import parse_checkpoint_config
from model import ActionPredictor
class ActionPredictorInference:
def __init__(self, checkpoint_path: str, device: str = None):
checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True)
cfg = parse_checkpoint_config(checkpoint["config"])
self._data_cfg = cfg.data
self._model_cfg = cfg.model
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
self._device = torch.device(device)
self._model = ActionPredictor(
self._model_cfg, self._data_cfg, pretrained=False).to(self._device)
self._model.load_state_dict(checkpoint["model_state_dict"])
self._model.eval()
self.action_dim = self._data_cfg.action_dim
self.history_len = self._data_cfg.history_len
self.frame_len = self._data_cfg.frame_len
self._transform = transforms.Compose([
transforms.Resize((self._data_cfg.image_size, self._data_cfg.image_size)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
self._action_mean = None
self._action_std = None
if self._data_cfg.normalize_actions:
# 优先使用 checkpoint config 记录的 stats 路径(相对路径按 checkpoint 目录解析),
# 旧 checkpoint 无该字段时回退到 checkpoint 同目录的 action_stats.npy
stats_path = self._data_cfg.action_stats_path
if stats_path:
if not os.path.isabs(stats_path):
stats_path = os.path.join(
os.path.dirname(os.path.abspath(checkpoint_path)), stats_path)
else:
stats_path = os.path.join(
os.path.dirname(os.path.abspath(checkpoint_path)),
"action_stats.npy")
if not os.path.exists(stats_path):
raise FileNotFoundError(
f"模型训练时启用了动作归一化,但未找到统计文件: {stats_path}")
stats = np.load(stats_path)
if stats.shape != (2, self._data_cfg.action_dim):
raise ValueError(
f"action_stats.npy 形状应为 (2, {self._data_cfg.action_dim}),"
f"实际为 {stats.shape},请检查文件: {stats_path}")
self._action_mean = torch.from_numpy(stats[0].astype(np.float32)).to(self._device)
self._action_std = torch.from_numpy(stats[1].astype(np.float32)).to(self._device)
@torch.no_grad()
def predict(self, images, action_history: np.ndarray) -> np.ndarray:
"""images: 长度为 frame_len 的 RGB 图像序列(旧→新,末帧为当前帧)"""
if not isinstance(images, (list, tuple)):
images = [images]
if len(images) != self._data_cfg.frame_len:
raise ValueError(
f"images 应为 {self._data_cfg.frame_len} 帧,实际为 {len(images)} 帧。"
f"冷启动时可重复传入同一帧。")
expected_shape = (self._data_cfg.history_len, self._data_cfg.action_dim)
if action_history.shape != expected_shape:
raise ValueError(
f"action_history 形状应为 {expected_shape},实际为 {action_history.shape}")
frames = []
for image in images:
img = Image.fromarray(image) if isinstance(image, np.ndarray) else image
frames.append(self._transform(img))
img_tensor = torch.stack(frames).unsqueeze(0).to(self._device) # (1, F, 3, S, S)
history = torch.tensor(action_history, dtype=torch.float32)
if self._action_mean is not None:
history = (history - self._action_mean) / self._action_std
history = history.unsqueeze(0).to(self._device)
pred = self._model(img_tensor, history).squeeze(0)
if self._action_mean is not None:
pred = pred * self._action_std + self._action_mean
return pred.cpu().numpy()
def parse_action_history(s: str, action_dim: int) -> np.ndarray:
steps = s.split(":")
history = []
for step in steps:
values = [float(v) for v in step.split(",")]
if len(values) != action_dim:
raise ValueError(
f"Expected {action_dim} values per step, got {len(values)}")
history.append(values)
return np.array(history, dtype=np.float32)
def main():
parser = argparse.ArgumentParser(description="actpred inference")
parser.add_argument("--model", required=True, help="checkpoint path")
parser.add_argument("--images", required=True, nargs="+",
help="按时间顺序(旧→新)传入 frame_len 个图像路径;"
"冷启动时可重复传同一帧路径")
parser.add_argument("--action-history", required=True,
help="冒号分隔各步,每步 action_dim 个值,共 history_len 步。"
"e.g. '0,0,0,0:1,0,0,0' (action_dim=4, history_len=2)")
parser.add_argument("--action-dim", type=int, default=None,
help="override action dim (auto-detected from checkpoint if omitted)")
parser.add_argument("--output-format", choices=["json", "raw"], default="json")
parser.add_argument("--device", type=str, default=None)
args = parser.parse_args()
predictor = ActionPredictorInference(args.model, args.device)
action_dim = args.action_dim or predictor.action_dim
if len(args.images) != predictor.frame_len:
raise SystemExit(
f"--images 需传入 {predictor.frame_len} 个路径(旧→新),"
f"实际为 {len(args.images)} 个。冷启动时可重复传同一帧路径。")
images = [np.array(Image.open(p).convert("RGB")) for p in args.images]
history = parse_action_history(args.action_history, action_dim)
predicted = predictor.predict(images, history)
if args.output_format == "json":
print(json.dumps({"predicted_action": predicted.tolist()}, indent=2))
else:
print(" ".join(f"{v:.4f}" for v in predicted))
if __name__ == "__main__":
main()