Files

263 lines
8.3 KiB
Python
Raw Permalink 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.
"""
Mask R-CNN 实例分割推理与可视化脚本
用法:
# 对单张图片进行推理
python predict.py --image dataset/1.jpg --checkpoint checkpoints/mask_rcnn_best.pth
# 对整个数据集目录进行推理
python predict.py --dataset dataset --checkpoint checkpoints/mask_rcnn_best.pth
# 调整置信度阈值
python predict.py --image dataset/1.jpg --checkpoint checkpoints/mask_rcnn_best.pth --threshold 0.7
# 同时显示 ground truth 对比
python predict.py --image dataset/1.jpg --checkpoint checkpoints/mask_rcnn_best.pth --show-gt
"""
import argparse
import os
import sys
import json
from typing import Dict, Optional
import numpy as np
from PIL import Image
import torch
from utils import (
get_label_map,
get_image_json_pairs,
shapes_to_masks,
visualize_instances,
)
from dataset import get_val_transforms
from model import get_model
def load_checkpoint(checkpoint_path: str, device: torch.device):
"""
加载训练好的模型检查点。
Args:
checkpoint_path: 检查点文件路径
device: 计算设备
Returns:
model: 加载好权重的模型
label_map: 标签名→类别ID 映射
"""
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
label_map = checkpoint.get("label_map", {})
num_classes = max(label_map.values()) + 1 # +1 for background
model = get_model(num_classes=num_classes, pretrained=False)
model.load_state_dict(checkpoint["model_state_dict"])
model = model.to(device)
model.eval()
# 构建 id→name 映射
id_to_name = {v: k for k, v in label_map.items()}
return model, label_map, id_to_name
@torch.no_grad()
def predict_single(
model: torch.nn.Module,
image: np.ndarray,
device: torch.device,
threshold: float = 0.5,
) -> Dict:
"""
对单张图像进行推理。
Args:
model: 模型
image: [H, W, 3] RGB uint8 图像
device: 计算设备
threshold: 置信度阈值
Returns:
包含 boxes, masks, labels, scores 的字典
"""
# 预处理
image_float = image.astype(np.float32) / 255.0
image_tensor = torch.as_tensor(image_float, dtype=torch.float32)
image_tensor = image_tensor.permute(2, 0, 1).unsqueeze(0).to(device)
# 推理
outputs = model(image_tensor)
output = outputs[0]
# 过滤低置信度
keep = output["scores"] > threshold
boxes = output["boxes"][keep].cpu().numpy()
masks = output["masks"][keep].cpu().numpy()
labels = output["labels"][keep].cpu().numpy()
scores = output["scores"][keep].cpu().numpy()
# mask 阈值化(> 0.5
masks = (masks[:, 0, :, :] > 0.5).astype(np.uint8)
return {
"boxes": boxes,
"masks": masks,
"labels": labels,
"scores": scores,
}
def visualize_and_save(
image: np.ndarray,
result: Dict,
id_to_name: Dict[int, str],
output_path: str,
threshold: float = 0.5,
):
"""
可视化预测结果并保存图像。
Args:
image: 原始图像 [H, W, 3] RGB
result: 预测结果字典
id_to_name: 类别ID→名称映射
output_path: 输出路径
threshold: 置信度阈值
"""
vis_img = visualize_instances(
image,
result["boxes"],
result["masks"],
result["labels"],
result["scores"],
label_map=id_to_name,
score_threshold=threshold,
)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
Image.fromarray(vis_img).save(output_path)
print(f" -> 结果已保存: {output_path}")
def visualize_gt(
image: np.ndarray,
json_path: str,
label_map: Dict[str, int],
id_to_name: Dict[int, str],
output_path: str,
):
"""可视化 ground truth 标注"""
with open(json_path, "r", encoding="utf-8") as f:
anno = json.load(f)
height = anno.get("imageHeight", image.shape[0])
width = anno.get("imageWidth", image.shape[1])
masks, labels, boxes = shapes_to_masks(
anno.get("shapes", []), height, width, label_map
)
if len(labels) == 0:
print(" -> 无标注实例")
return
vis_img = visualize_instances(
image, boxes, masks, labels, None, id_to_name, 0.0
)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
Image.fromarray(vis_img).save(output_path)
print(f" -> Ground Truth 已保存: {output_path}")
def main():
parser = argparse.ArgumentParser(description="Mask R-CNN 实例分割推理")
parser.add_argument("--image", type=str, default=None,
help="单张图像路径")
parser.add_argument("--dataset", type=str, default=None,
help="数据集目录路径(批量推理)")
parser.add_argument("--checkpoint", type=str, default="checkpoints/mask_rcnn_best.pth",
help="模型检查点路径 (默认: checkpoints/mask_rcnn_best.pth)")
parser.add_argument("--output-dir", type=str, default="predictions",
help="输出目录 (默认: predictions)")
parser.add_argument("--threshold", type=float, default=0.5,
help="置信度阈值 (默认: 0.5)")
parser.add_argument("--show-gt", action="store_true",
help="同时可视化 ground truth 对比")
parser.add_argument("--min-size", type=int, default=800,
help="输入图像最小边尺寸 (默认: 800)")
parser.add_argument("--max-size", type=int, default=1333,
help="输入图像最大边尺寸 (默认: 1333)")
args = parser.parse_args()
# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")
# 检查检查点
ckpt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.checkpoint)
if not os.path.exists(ckpt_path):
print(f"错误: 检查点文件不存在: {ckpt_path}")
print("请先运行 train.py 训练模型,或使用 --checkpoint 指定正确的路径。")
sys.exit(1)
# 加载模型
model, label_map, id_to_name = load_checkpoint(ckpt_path, device)
print(f"模型已加载 | 标签映射: {label_map}")
print(f"置信度阈值: {args.threshold}")
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.output_dir)
os.makedirs(output_dir, exist_ok=True)
# 收集要推理的图像
if args.image:
image_paths = [args.image]
json_paths = [args.image.rsplit(".", 1)[0] + ".json"]
elif args.dataset:
dataset_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.dataset)
pairs = get_image_json_pairs(dataset_path)
image_paths = [p[0] for p in pairs]
json_paths = [p[1] for p in pairs]
else:
print("错误: 请指定 --image 或 --dataset")
sys.exit(1)
print(f"\n{len(image_paths)} 张图像待推理\n")
for idx, (image_path, json_path) in enumerate(zip(image_paths, json_paths)):
if not os.path.exists(image_path):
print(f"[{idx+1}] 跳过(图像不存在): {image_path}")
continue
print(f"[{idx+1}/{len(image_paths)}] {os.path.basename(image_path)}")
# 读取图像
image = np.array(Image.open(image_path).convert("RGB"))
# 推理
result = predict_single(model, image, device, args.threshold)
num_detections = len(result["scores"])
print(f" -> 检测到 {num_detections} 个实例")
for i in range(num_detections):
name = id_to_name.get(int(result["labels"][i]), str(result["labels"][i]))
print(f" {name}: {result['scores'][i]:.3f}")
# 可视化并保存
base_name = os.path.splitext(os.path.basename(image_path))[0]
output_path = os.path.join(output_dir, f"{base_name}_pred.png")
visualize_and_save(image, result, id_to_name, output_path, args.threshold)
# 可选:可视化 ground truth
if args.show_gt and os.path.exists(json_path):
gt_output_path = os.path.join(output_dir, f"{base_name}_gt.png")
visualize_gt(image, json_path, label_map, id_to_name, gt_output_path)
print(f"\n推理完成!结果保存在: {output_dir}")
if __name__ == "__main__":
main()