ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

人群计数模型测试实战:从评估指标到工程化部署

人群计数模型测试实战:从评估指标到工程化部署 1. 项目概述从“数人头”到智能感知在安防监控、大型活动管理、交通枢纽人流分析乃至零售门店的客流量统计中一个看似简单却极具挑战性的任务反复出现如何快速、准确地统计图像或视频中的人群数量这就是“人群计数”的核心命题。传统的人工计数或基于检测框的方法在密集、遮挡严重的场景下往往力不从心效率低下且误差大。而“Crowd Counting-计数模型测试Code”这个项目正是切入这个痛点它不是一个从零开始的模型训练项目而是一个聚焦于模型测试、评估与工程化部署的实战工具箱。它的价值在于当你拿到一个预训练的人群计数模型比如热门的CANNet、CSRNet、MCNN等后如何验证其在你特定场景下的表现如何将其集成到你的应用流水线中以及如何解读那些看似神秘的密度图。简单来说这个项目解决的是“模型落地最后一公里”的问题。它适合已经对深度学习有基本了解并希望将人群计数技术应用于实际产品的开发者、算法工程师以及需要进行算法选型的技术决策者。通过这套代码你可以系统性地评估不同模型在你自己数据集上的精度如MAE、MSE、速度FPS和鲁棒性如光照变化、视角变化下的表现从而为项目选型提供坚实的数据支撑避免“纸上谈兵”。2. 核心模型与测试框架解析人群计数领域经过多年发展衍生出多种技术路线测试代码必须能兼容主流模型并理解其输出差异才能进行公平比较。2.1 主流计数模型原理与输出形式目前主流的人群计数模型主要分为以下几类我们的测试代码需要能处理它们各自的输出格式基于密度图估计的模型如MCNN, CSRNet, CANNet这是当前的主流范式。模型不直接输出人数而是为输入图像生成一张“密度图”。密度图上每个像素的值代表该位置出现人头的概率密度。对整张密度图进行积分即求和就得到了估计的总人数。这类模型的优势是能更好地处理密集和遮挡。MCNNMulti-column CNN早期经典使用不同尺寸的卷积核来捕捉不同尺度的人头特征。CSRNet采用扩张卷积Dilated Convolution来扩大感受野在保持分辨率的同时捕获上下文信息在多个公开数据集上表现出色。CANNetContext-Aware Network强调上下文感知通过融合全局和局部特征来提升在复杂场景下的计数精度。测试时需要特别注意CANNet等较新模型可能输出多尺度特征或注意力图最终密度图需要从特定层提取。基于检测的模型如Faster R-CNN、YOLO系列的变种。这类模型直接输出图中每个人的边界框通过统计框的数量来计数。在稀疏场景下精度高且可提供位置信息但在极度密集、小人头场景下召回率会急剧下降重叠框的处理NMS也会影响计数准确性。测试时我们需要处理模型的检测结果通常是[x1, y1, x2, y2, score]的列表并过滤低置信度的预测。基于回归的模型一些早期方法或轻量级模型直接从图像特征回归出一个总人数标量。这种方法简单快速但丢失了空间分布信息可解释性差且对图像内容变化敏感。对于测试框架而言关键是要定义一个统一的接口。无论底层是哪种模型测试代码都应将其“适配”到同一个评估流程中。例如对于密度图模型接口函数predict(image)应返回密度图数组对于检测模型则应返回边界框列表内部再统一转换成估计人数与真实人数进行比对。2.2 测试评估指标详解评估一个人群计数模型绝不能只看一个数字。我们需要一套多维度的指标来全面衡量其性能平均绝对误差MAE, Mean Absolute ErrorMAE (1/N) * Σ |y_i - ŷ_i|其中N是测试图片数量y_i是第i张图片的真实人数ŷ_i是模型预测的人数。MAE反映的是模型计数的平均偏差水平单位是“人”。例如MAE3.5意味着平均每张图会差3.5个人。这是最直观、最常用的指标。均方根误差RMSE, Root Mean Square ErrorRMSE sqrt( (1/N) * Σ (y_i - ŷ_i)^2 )RMSE由于平方项的存在会对较大的误差给予更重的惩罚。RMSE更能反映模型预测的稳定性。一个RMSE远大于MAE的模型说明它可能在大多数图片上预测得不错但在少数图片上出现了严重错误即“翻车”这对于要求稳定的线上系统是致命的。平均绝对相对误差MARE或平均相对误差MREMARE (1/N) * Σ |y_i - ŷ_i| / y_i这个指标考虑了人数规模。在真实人数很少如1-2人和很多如1000人的场景下同样的绝对误差如差5人意义完全不同。MARE能更好地评估模型在不同密度下的相对精度。帧率FPS, Frames Per Second 在GPU/CPU上的推理速度直接决定了模型的实时性。测试时需要在固定的硬件环境和输入尺寸下测量。一个MAE低但FPS也极低的模型在很多实时应用场景中是不可用的。可视化分析 指标是冰冷的可视化才能发现真正的问题。测试代码必须包含生成预测密度图与真实密度图如有对比、预测人数与真实人数散点图、误差分布直方图等功能。通过看图我们能快速发现模型在哪些场景下容易失效如夜景、透视畸变严重、非常规服饰等。实操心得不要迷信论文里报告的SOTAState-of-The-Art指标。一定要用你自己的、或者与目标场景相似的数据集重新测试。论文中的指标往往是在标准公开数据集如ShanghaiTech, UCF-QNRF的特定划分上取得的与你的实际数据分布可能存在“域差异”Domain Gap。我曾遇到一个在ShanghaiTech Part A上MAE2的模型在我们自己的商场摄像头数据上MAE直接飙到15以上原因就是视角和人群服饰差异太大。3. 测试环境搭建与代码结构剖析一个健壮的测试代码库其价值一半在于严谨的环境配置与清晰的工程结构。3.1 环境配置与依赖管理人群计数模型通常基于PyTorch或TensorFlow。这里以PyTorch环境为例因为目前大部分SOTA模型和研究都基于此框架。核心依赖清单# 基础框架 torch1.7.0 torchvision0.8.0 # 科学计算与图像处理 numpy opencv-python Pillow scipy # 用于一些密度图生成时的高斯滤波 # 数据加载与处理 pycocotools # 如果使用COCO格式的标注 scikit-image # 可视化与进度 matplotlib tqdm # 用于显示测试进度条 # 工程与工具 pyyaml # 用于配置文件 tensorboard # 可选用于记录测试过程环境隔离是关键。强烈建议使用conda或venv创建独立的Python环境并使用requirements.txt或environment.yml文件记录精确的版本号。不同版本的PyTorch和CUDA可能导致模型加载失败或结果不一致。# 使用conda创建环境示例 conda create -n crowd_count_test python3.8 conda activate crowd_count_test pip install -r requirements.txt3.2 代码仓库结构设计一个清晰的目录结构能让测试工作事半功倍也便于团队协作和后续迭代。crowd_counting_test_code/ ├── configs/ # 配置文件目录 │ ├── test_shanghaitech.yaml │ ├── test_mall.yaml │ └── model_cannet.yaml ├── data/ # 数据相关通常软链接到实际数据集 │ ├── shanghaitech/ │ │ ├── part_A/ │ │ └── part_B/ │ └── mall/ │ ├── images/ │ └── ground_truth/ ├── models/ # 模型定义与加载 │ ├── __init__.py │ ├── base_tester.py # 测试器基类 │ ├── cannettester.py │ ├── csrnettester.py │ └── model_zoo.py # 统一模型加载入口 ├── utils/ # 工具函数 │ ├── density_tools.py # 密度图生成、转换 │ ├── eval_metrics.py # MAE, RMSE等计算 │ ├── visualization.py # 画图函数 │ └── data_preprocess.py # 数据预处理缩放、归一化等 ├── scripts/ # 可执行脚本 │ ├── test.py # 主测试脚本 │ └── export_onnx.py # 模型导出脚本可选 ├── outputs/ # 测试输出自动生成 │ ├── 20240527_ShanghaiTech_A_CANNet/ │ │ ├── logs/ │ │ ├── visuals/ # 保存可视化图片 │ │ └── results.json # 结构化测试结果 ├── requirements.txt └── README.md关键设计思想配置驱动所有测试参数数据集路径、模型路径、批大小、是否可视化等都写在YAML配置文件中。修改配置即可切换不同测试任务无需改动代码。模块化将数据加载、模型推理、指标计算、可视化等功能解耦成独立模块通过基类和接口定义规范方便扩展新的模型或评估指标。结果可复现每次测试生成一个带有时间戳的独立输出目录包含完整的配置文件副本、日志和结果文件确保任何测试结果都能被追溯和复现。4. 核心测试流程与代码实现详解下面我们深入测试脚本test.py的核心逻辑看看一个完整的模型评估是如何进行的。4.1 数据加载与预处理流水线数据是测试的基石。人群计数数据集通常提供图片和标注文件。标注可能是点标注每个头一个坐标也可能是已经生成好的密度图。# utils/data_preprocess.py 节选 import cv2 import numpy as np from scipy.ndimage import gaussian_filter def generate_density_map(image_shape, points, sigma15): 根据人头坐标点生成密度图。 image_shape: (H, W) points: list of (x, y) coordinates sigma: 高斯核标准差控制密度扩散范围 density_map np.zeros(image_shape, dtypenp.float32) h, w image_shape if len(points) 0: return density_map for point in points: x, y int(point[0]), int(point[1]) if 0 x w and 0 y h: density_map[y, x] 1 # 在坐标点处放置一个脉冲 # 使用高斯滤波将脉冲扩散成密度区域 density_map gaussian_filter(density_map, sigmasigma, modeconstant) # 归一化使得密度图积分等于总人数 density_map density_map * (len(points) / (density_map.sum() 1e-8)) return density_map def preprocess_image(image, target_size(512, 512), mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]): 模型通用的预处理缩放、归一化、转Tensor。 # 缩放 image cv2.resize(image, target_size) # BGR to RGB image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 归一化 [0,255] - [0,1] image image.astype(np.float32) / 255.0 # 标准化 (使用ImageNet均值和标准差是常见做法) image (image - mean) / std # HWC to CHW image image.transpose(2, 0, 1) return torch.from_numpy(image).float()数据加载器DataLoader需要返回预处理后的图像Tensor和对应的真实人数或密度图。为了提高测试效率尤其是处理大量图片时要利用PyTorch的DataLoader进行多进程加载。# 在测试脚本中 from torch.utils.data import DataLoader from your_dataset_module import CrowdCountingDataset test_dataset CrowdCountingDataset(data_rootpath/to/data, modetest, transformpreprocess_transform) test_loader DataLoader(test_dataset, batch_size1, shuffleFalse, num_workers4, pin_memoryTrue)注意事项测试时shuffle必须设为False以保证结果的可复现性。batch_size通常设为1因为不同图片的原始尺寸可能不同但经过预处理后尺寸统一也可以根据GPU内存适当调大以加速。4.2 模型加载与推理引擎封装模型加载需要处理预训练权重的加载以及将模型设置为评估模式model.eval()。# models/model_zoo.py import torch import models.cannet as cannet_module import models.csrnet as csrnet_module def load_model(model_name, checkpoint_path, devicecuda): 根据模型名称加载预训练模型。 model None if model_name.lower() cannet: model cannet_module.CANNet() elif model_name.lower() csrnet: model csrnet_module.CSRNet() else: raise ValueError(fUnsupported model: {model_name}) # 加载预训练权重 if checkpoint_path: checkpoint torch.load(checkpoint_path, map_locationdevice) # 处理可能的键名不匹配如多GPU训练保存的权重带有module.前缀 state_dict checkpoint[model] if model in checkpoint else checkpoint new_state_dict {} for k, v in state_dict.items(): name k[7:] if k.startswith(module.) else k # 去除module. new_state_dict[name] v model.load_state_dict(new_state_dict, strictFalse) # strictFalse更鲁棒 print(fLoaded checkpoint from {checkpoint_path}) model.to(device) model.eval() # 至关重要关闭Dropout和BatchNorm的随机性 return model推理过程需要封装在with torch.no_grad():上下文管理器中以禁用梯度计算节省内存和计算资源。# models/base_tester.py 节选 class BaseTester: def __init__(self, model, device): self.model model self.device device def predict_single(self, image_tensor): 对单张图片进行预测 image_tensor image_tensor.unsqueeze(0).to(self.device) # 增加batch维度 with torch.no_grad(): output self.model(image_tensor) # 不同模型输出格式不同需要统一处理为密度图 density_map self._parse_output(output) pred_count density_map.sum().item() return pred_count, density_map def _parse_output(self, output): 子类需重写此方法以适配不同模型的输出格式 raise NotImplementedError4.3 评估循环与结果记录这是测试脚本的主循环遍历测试集收集预测结果并计算最终指标。# scripts/test.py 核心循环 import time from tqdm import tqdm from utils.eval_metrics import compute_mae, compute_rmse def evaluate_model(tester, test_loader, config): all_pred_counts [] all_gt_counts [] total_time 0.0 for batch_idx, (images, gt_counts, img_names) in enumerate(tqdm(test_loader)): gt_count gt_counts.item() if torch.is_tensor(gt_counts) else gt_counts all_gt_counts.append(gt_count) # 推理计时 start_time time.time() pred_count, density_map tester.predict_single(images) end_time time.time() if batch_idx 0: # 跳过第一张可能包含初始化开销的时间 total_time (end_time - start_time) all_pred_counts.append(pred_count) # 可选保存可视化结果每N张或误差较大的图 if config[save_visuals] and (batch_idx % config[visual_interval] 0 or abs(pred_count - gt_count) config[large_error_threshold]): save_visualization(images, density_map, gt_count, pred_count, img_names[0], config[output_dir]) # 计算指标 mae compute_mae(all_pred_counts, all_gt_counts) rmse compute_rmse(all_pred_counts, all_gt_counts) # 计算平均FPS num_frames len(test_loader) - 1 # 减去第一帧 avg_fps num_frames / total_time if total_time 0 else 0 # 打印并保存结果 results { MAE: round(mae, 4), RMSE: round(rmse, 4), Avg_FPS: round(avg_fps, 2), Total_Images: len(all_gt_counts) } print(\n *50) print(fEvaluation Results on {config[dataset_name]}) print(fModel: {config[model_name]}) for k, v in results.items(): print(f{k}: {v}) print(*50) # 将结果保存为JSON文件 import json result_file os.path.join(config[output_dir], results.json) with open(result_file, w) as f: json.dump(results, f, indent4) # 额外保存详细的每张图预测结果用于深入分析 detail_results list(zip(img_names_all, all_gt_counts, all_pred_counts)) detail_file os.path.join(config[output_dir], detailed_results.csv) pd.DataFrame(detail_results, columns[image_name, gt_count, pred_count]).to_csv(detail_file, indexFalse) return results5. 高级测试技巧与结果深度分析完成了基础测试循环工作只完成了一半。如何从测试结果中挖掘出更深层次的信息指导模型优化和选型才是体现测试代码价值的关键。5.1 跨场景鲁棒性测试一个模型在标准数据集上表现好不代表它在你的场景里也好。我们需要设计针对性的测试集来评估其鲁棒性。光照变化测试集收集同一场景在不同时间清晨、正午、黄昏、夜晚、不同天气晴天、阴天、雨天下的图片。分析模型MAE和RMSE随光照条件变化的趋势。你会发现很多模型在低光照下性能会显著下降。视角与尺度变化测试集人群计数对透视效应非常敏感。收集从高空俯视、平视、低角度仰视的图片或者同一人群由近及远走动的视频帧。计算模型在不同尺度人头像素大小下的计数误差。通常模型对小尺度远处小人头的计数能力更弱。密度极端情况测试分别测试模型在极稀疏10人和极密集500人场景下的表现。有些模型在密集区域会“饱和”预测人数低于真实人数有些则在稀疏区域容易“过拟合”噪声预测出虚影。实操方法在你的测试代码中可以增加一个“场景标签”字段。在数据加载时根据图片文件名或路径为其分配标签如lightingnight,densityhigh。最后除了输出全局指标再分组输出各子场景的指标制作成如下表格场景类别图片数量MAERMSE备注全局5008.515.2-光照-白天3005.19.3表现良好光照-夜晚20013.222.8性能显著下降密度-稀疏1502.13.5精度高密度-密集15014.825.1严重低估通过这样的表格你可以一目了然地看出模型的弱点所在从而决定是寻找更鲁棒的模型还是针对特定弱点进行数据增强或模型微调。5.2 误差分析与可视化诊断当发现某张图片预测误差巨大时不要仅仅记录一个数字一定要保存可视化结果进行“尸检”。可视化诊断包应包含原始输入图像模型预测的密度图使用热力图jet色彩映射显示真实密度图如果有的话误差热图预测密度图 - 真实密度图红色区域表示模型高估蓝色区域表示低估。在原始图上叠加预测的高密度区域轮廓。通过观察这些图你可以发现许多典型问题背景误识别模型将树叶、窗户格子、地面纹理等误识为人头在密度图上形成大片虚假响应。这通常是因为训练数据背景单一模型过拟合了。漏检与合并在极度密集区域多个头被模型识别成一个“大团”。这是因为感受野有限或下采样倍数太高丢失了细节。尺度不适应对于远处非常小的人头模型完全没有响应或者对于近处特大的人头响应区域异常大。边界效应在图像边缘的人头由于上下文信息不全常常被低估或漏检。踩坑记录有一次测试一个模型发现其在某类图片上总是严重高估。通过可视化发现该场景中有大量游客打着统一的、颜色鲜艳的遮阳伞。模型将每把伞的中心点都识别成了一个人头。这说明模型过度依赖了颜色和圆形纹理特征。解决方案是在训练数据中增加类似干扰物的负样本或者使用注意力机制让模型更关注头部形状而非颜色。5.3 模型效率分析与优化建议除了精度效率是工程落地的生命线。测试报告里必须包含详细的效率分析。计算复杂度分析参数量Params使用torchsummary或手动计算。参数量大的模型不一定慢但部署到资源受限的边缘设备时是个问题。浮点运算数FLOPs使用thop或ptflops库计算。FLOPs更能反映模型的理论计算开销。对比不同输入分辨率如224x224 vs 512x512下的FLOPs增长情况判断模型是否适合处理高分辨率图片。内存占用Memory在推理时监控GPU内存使用情况。这决定了你的服务能同时处理多少路视频流。实际推理速度测试预热在正式计时前先运行几十次推理让GPU和CUDA达到稳定状态。批处理Batch Inference测试不同批大小1, 2, 4, 8...下的吞吐量images/sec。找到在给定GPU内存下的最优批大小。对于视频流通常batch_size1是常态但如果是离线处理图片大batch能极大提升效率。端到端延迟计时应包含数据从CPU加载到GPU、预处理、模型推理、后处理密度图求和的全过程。这才是真实的延迟。优化建议输出基于以上分析测试代码可以给出自动化建议例如“该模型在输入512x512时FLOPs是224x224的5倍但MAE仅提升5%。建议在实时场景中将输入缩放到384x384以平衡速度与精度。”“模型参数量达120M其中80%集中在后端特征融合模块。可尝试对该部分进行通道剪枝Channel Pruning。”“使用TensorRT进行FP16量化后在T4 GPU上FPS可从25提升至55精度损失小于1%。”6. 工程化集成与持续测试测试的最终目的是为了部署。一套好的测试代码应该能平滑地过渡到工程化集成阶段。6.1 模型格式转换与部署测试许多生产环境不使用原始的PyTorch模型文件.pth而是需要转换成更高效的推理格式。导出为TorchScriptPyTorch自带的序列化格式可以脱离Python环境运行便于C集成。# scripts/export_torchscript.py model.eval() example_input torch.rand(1, 3, 512, 512).to(device) traced_script_module torch.jit.trace(model, example_input) traced_script_module.save(cannet_deploy.pt)测试要点导出后必须用同样的测试集对比原始模型和TorchScript模型的输出是否一致允许极小的数值误差。导出为ONNX开放神经网络交换格式可以被TensorRT, OpenVINO, ONNX Runtime等多种推理引擎支持。torch.onnx.export(model, example_input, cannet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}})测试要点使用ONNX Runtime加载.onnx文件进行推理并与PyTorch结果对比。特别注意模型中的动态操作如torch.sum、view在导出时是否被正确支持。使用TensorRT加速对于NVIDIA GPU这是终极提速方案。将ONNX模型用TensorRT构建引擎并进行FP16或INT8量化。测试流程在TensorRT上运行测试集记录精度损失和速度提升。INT8量化需要校准集要确保校准集能代表真实数据分布。6.2 构建持续集成CI测试流水线对于需要频繁迭代模型或处理多版本的项目手动测试是不可持续的。应将测试代码集成到CI/CD流水线中。一个简单的GitLab CI/CD.gitlab-ci.yml配置示例stages: - test crowd_counting_test: stage: test image: pytorch/pytorch:1.12.0-cuda11.3-cudnn8-runtime # 使用固定的Docker镜像保证环境一致 script: - pip install -r requirements.txt - python scripts/test.py --config configs/test_smoke.yaml # 快速冒烟测试 - python scripts/test.py --config configs/test_full.yaml # 完整测试 artifacts: paths: - outputs/* # 将测试结果和可视化图片保存为制品供后续查看 expire_in: 1 week only: - merge_requests # 仅在合并请求时触发确保新代码不会降低模型性能 - main # 主分支更新后也触发流水线可以做的更多性能回归测试每次提交代码自动运行测试并与基准如main分支的上一次结果比较MAE和FPS。如果误差超过阈值如MAE增加10%则自动标记合并请求为失败。内存/显存泄漏检测在测试循环中插入内存监控确保长时间运行不会泄漏。自动生成测试报告使用pytestallure或简单的HTML生成器将本次测试的指标、图表、与历史数据的对比自动生成一份可视化报告附在合并请求的评论中。6.3 测试中的常见陷阱与应对策略即使有了完善的代码测试过程中依然会遇到各种“坑”这里分享几个高频问题的解决思路“为什么我加载预训练模型后结果和论文里差很多”检查数据预处理这是最常见的原因。论文作者使用的图像归一化均值/标准差mean/std是什么是[0.485, 0.456, 0.406], [0.229, 0.224, 0.225]ImageNet标准还是[0.5,0.5,0.5], [0.5,0.5,0.5]输入图片尺寸是否和训练时一致一个像素的差值都可能导致输出天差地别。务必找到原始代码库中的预处理函数并完全复现。检查密度图生成参数对于需要从点标注生成密度图的测试高斯核的sigma值至关重要。不同数据集、甚至同一数据集的不同部分可能使用不同的sigma。使用错误的sigma生成的“真实”密度图去评估模型结果自然不准。“模型推理速度波动很大第一次很慢后面变快”GPU Warm-upGPU有功耗和频率调整策略。前几次推理时GPU可能未达到最大频率并且CUDA kernel需要首次编译加载。正式计时前进行足够次数如100次的“预热”推理。CPU数据加载瓶颈如果数据加载特别是解码高分辨率图片太慢会导致GPU等数据吞吐量上不去。使用DataLoader的num_workers参数增加加载进程并使用pin_memoryTrue加速CPU到GPU的数据传输。“同一个模型两次测试结果有微小差异”确定性设置在PyTorch中即使model.eval()一些操作在GPU上也可能有非确定性。为了完全可复现可以设置torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False torch.manual_seed(42) np.random.seed(42)注意float32的精度GPU上浮点数运算的顺序可能不同导致1e-6级别的微小差异这通常是可接受的。“测试集上的指标很好但部署到真实视频流效果很差”域差异Domain Gap测试集通常是精心挑选的静态图片和真实视频流动态、模糊、压缩失真、不同摄像头存在巨大差异。解决之道是构建一个“仿真上线测试集”从真实摄像头中采样一段时间内的视频帧并人工标注一部分作为测试基准。用这个基准来评估模型结果才更有说服力。后处理差异测试代码中的后处理如密度图求和和部署代码中是否完全一致检查是否有四舍五入、取整等操作的差异。人群计数模型的测试远不止于运行一个脚本、得到一个MAE数字。它是一个系统的工程涉及对模型原理的深刻理解、对数据的严谨处理、对指标的全面考量以及对工程现实的清醒认识。这套“Crowd Counting-计数模型测试Code”的价值就在于它将这个系统工程中的最佳实践和常见陷阱固化成了可重复、可扩展的代码让算法开发者能更专注于模型本身的创新而将评估验证的繁重工作交给可靠的工具。记住没有经过严格、全面测试的模型就像没有经过试飞的飞机性能再漂亮也不敢让它真正上天。
返回列表