feat: TensorRT 固定 batch=4 重构

- tensorrt_engine.py 工业级 Buffer Pool
- preprocessor.py 添加 pad_to_batch4()
- postprocessor.py 支持批量输出
- settings.py 固定 batch_size=4
This commit is contained in:
2026-02-02 14:49:47 +08:00
parent 956bcbbc3e
commit 745cadc8e7
18 changed files with 68258 additions and 130 deletions

43
check_engine.py Normal file
View File

@@ -0,0 +1,43 @@
"""检查 TensorRT Engine 的实际 shape"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
import tensorrt as trt
engine_path = "./models/yolo11n.engine"
with open(engine_path, "rb") as f:
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
engine = runtime.deserialize_cuda_engine(f.read())
print("=" * 60)
print("Engine Binding Information")
print("=" * 60)
for i in range(engine.num_bindings):
name = engine.get_binding_name(i)
shape = engine.get_binding_shape(i)
dtype = trt.nptype(engine.get_binding_dtype(i))
is_input = engine.binding_is_input(i)
size = trt.volume(shape)
print(f"\nBinding {i}:")
print(f" Name: {name}")
print(f" Shape: {shape}")
print(f" Dtype: {dtype}")
print(f" Size: {size}")
print(f" Is Input: {is_input}")
if is_input:
print(f" Total Elements: {size}")
print(f" Expected Batch Size: {shape[0] if len(shape) > 0 else 'N/A'}")
print("\n" + "=" * 60)
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()

53
check_engine_output.py Normal file
View File

@@ -0,0 +1,53 @@
"""检查 TensorRT Engine 输出的实际 shape"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import numpy as np
try:
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
engine_path = "./models/yolo11n.engine"
with open(engine_path, "rb") as f:
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
engine = runtime.deserialize_cuda_engine(f.read())
context = engine.create_execution_context()
print("=" * 60)
print("Engine Binding Information")
print("=" * 60)
for i in range(engine.num_bindings):
name = engine.get_binding_name(i)
shape = engine.get_binding_shape(i)
dtype = trt.nptype(engine.get_binding_dtype(i))
is_input = engine.binding_is_input(i)
print(f"\nBinding {i}: {name}")
print(f" Shape: {shape}")
print(f" Dtype: {dtype}")
print(f" Is Input: {is_input}")
print("\n" + "=" * 60)
input_shape = engine.get_binding_shape(0)
output_shape = engine.get_binding_shape(1)
print(f"Input shape: {input_shape}")
print(f"Output shape: {output_shape}")
input_size = np.prod([max(1, s) for s in input_shape])
output_size = np.prod([max(1, s) for s in output_shape])
print(f"Input size: {input_size}")
print(f"Output size: {output_size}")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()

View File

@@ -75,8 +75,7 @@ class InferenceConfig:
model_path: str = "./models/yolo11n.engine"
input_width: int = 480
input_height: int = 480
batch_size: int = 1
max_batch_size: int = 8
batch_size: int = 4
conf_threshold: float = 0.5
nms_threshold: float = 0.45
device_id: int = 0
@@ -160,8 +159,7 @@ class Settings:
model_path=os.getenv("MODEL_PATH", "./models/yolo11n.engine"),
input_width=int(os.getenv("INPUT_WIDTH", "480")),
input_height=int(os.getenv("INPUT_HEIGHT", "480")),
batch_size=int(os.getenv("BATCH_SIZE", "1")),
max_batch_size=int(os.getenv("MAX_BATCH_SIZE", "8")),
batch_size=int(os.getenv("BATCH_SIZE", "4")),
conf_threshold=float(os.getenv("CONF_THRESHOLD", "0.5")),
nms_threshold=float(os.getenv("NMS_THRESHOLD", "0.45")),
)

View File

@@ -597,20 +597,21 @@ class PostProcessor:
output = outputs[0]
if len(output.shape) == 3:
if output.ndim == 3:
output = output[0]
num_detections = output.shape[0]
if output.ndim == 2:
output = output.reshape(-1)
num_detections = output.shape[0] // 85
boxes = []
scores = []
class_ids = []
for i in range(num_detections):
detection = output[i]
if len(detection) < 6:
continue
start_idx = i * 85
detection = output[start_idx:start_idx + 85]
x_center = detection[0]
y_center = detection[1]
@@ -637,16 +638,16 @@ class PostProcessor:
y2 = y_center + height / 2
boxes.append([x1, y1, x2, y2])
scores.append(total_conf)
class_ids.append(class_id)
scores.append(float(total_conf))
class_ids.append(int(class_id))
if not boxes:
return np.array([]), np.array([]), np.array([])
return (
np.array(boxes),
np.array(scores),
np.array(class_ids)
np.array(boxes, dtype=np.float32),
np.array(scores, dtype=np.float32),
np.array(class_ids, dtype=np.int32)
)
def filter_by_roi(

View File

@@ -227,13 +227,14 @@ class LetterboxPreprocessor:
class BatchPreprocessor:
"""Batch预处理器类
支持动态Batch大小转换为NCHW格式FP16精度
固定 batch=4支持 padding 到 batch=4
"""
BATCH_SIZE = 4
def __init__(
self,
target_size: Tuple[int, int] = (480, 480),
max_batch_size: int = 8,
fp16_mode: bool = True
):
"""
@@ -241,44 +242,72 @@ class BatchPreprocessor:
Args:
target_size: 目标尺寸 (width, height)
max_batch_size: 最大Batch大小
fp16_mode: 是否使用FP16精度
"""
self.target_size = target_size
self.max_batch_size = max_batch_size
self.fp16_mode = fp16_mode
self.batch_size = self.BATCH_SIZE
self._letterbox = LetterboxPreprocessor(target_size)
self._logger = get_logger("preprocessor")
self._lock = threading.Lock()
self._memory_pool: List[np.ndarray] = []
self._preallocated_size = max_batch_size
self._logger.info(
f"Batch预处理器: batch={self.batch_size}, "
f"target_size={target_size}, fp16={fp16_mode}"
)
@staticmethod
def pad_to_batch4(frames: List[np.ndarray]) -> np.ndarray:
"""
Padding 到 batch=4重复最后一帧
Args:
frames: list of [3, 480, 480] numpy arrays
Returns:
np.ndarray: [4, 3, 480, 480]
"""
if len(frames) == 0:
raise ValueError("Empty frames list")
if len(frames) == 4:
return np.stack(frames)
pad_frame = frames[-1].copy()
while len(frames) < 4:
frames.append(pad_frame)
return np.stack(frames)
def preprocess_batch(
self,
images: List[np.ndarray]
) -> Tuple[np.ndarray, List[Tuple[float, float, float, float]]]:
"""
预处理一个批次图像
预处理批次图像,自动 padding 到 batch=4
Args:
images: 图像列表
Returns:
tuple: (批次数据, 缩放信息列表)
tuple: (批次数据 [4, 3, H, W], 缩放信息列表)
"""
batch_size = len(images)
batch_size = min(batch_size, self.max_batch_size)
batch_data, scale_info_list = self._preprocess_batch(images)
return batch_data, scale_info_list
def _preprocess_batch(
self,
images: List[np.ndarray]
) -> Tuple[np.ndarray, List[Tuple[float, float, float, float]]]:
"""内部预处理实现"""
padded_images = self.pad_to_batch4(images)
scale_info_list = []
processed_images = []
for i in range(batch_size):
if i >= len(images):
break
processed, scale_info = self._letterbox.preprocess(images[i])
for i in range(self.batch_size):
processed, scale_info = self._letterbox.preprocess(padded_images[i])
processed_images.append(processed)
scale_info_list.append(scale_info)
@@ -298,53 +327,6 @@ class BatchPreprocessor:
stacked = stacked.astype(np.float16)
return stacked
def allocate_batch_memory(self, batch_size: int) -> np.ndarray:
"""
分配批次内存
Args:
batch_size: 批次大小
Returns:
预分配的numpy数组
"""
batch_size = min(batch_size, self.max_batch_size)
with self._lock:
for mem in self._memory_pool:
if mem.shape[0] == batch_size:
return mem
height, width = self.target_size
shape = (batch_size, 3, height, width)
if self.fp16_mode:
mem = np.zeros(shape, dtype=np.float16)
else:
mem = np.zeros(shape, dtype=np.float32)
self._memory_pool.append(mem)
return mem
def release_memory(self):
"""释放内存池"""
with self._lock:
self._memory_pool.clear()
self._logger.info("预处理内存池已释放")
def get_memory_usage(self) -> Dict[str, int]:
"""获取内存使用情况"""
with self._lock:
total_bytes = sum(
mem.nbytes for mem in self._memory_pool
)
return {
"total_bytes": total_bytes,
"total_mb": total_bytes / (1024 ** 2),
"block_count": len(self._memory_pool)
}
class ImagePreprocessor:
@@ -372,7 +354,6 @@ class ImagePreprocessor:
)
self._batch_preprocessor = BatchPreprocessor(
target_size=(config.input_width, config.input_height),
max_batch_size=config.max_batch_size,
fp16_mode=config.fp16_mode
)
@@ -380,7 +361,7 @@ class ImagePreprocessor:
self._logger.info(
f"图像预处理器初始化完成: "
f"输入尺寸 {config.input_width}x{config.input_height}, "
f"Batch大小 {config.batch_size}-{config.max_batch_size}, "
f"Batch大小 {self._batch_preprocessor.batch_size}, "
f"FP16模式 {config.fp16_mode}"
)
@@ -416,15 +397,17 @@ class ImagePreprocessor:
rois: Optional[List[Optional[ROIInfo]]] = None
) -> Tuple[np.ndarray, List[Tuple[float, float, float, float]]]:
"""
预处理批次图像
预处理批次图像,自动 padding 到 batch=4
Args:
images: 原始图像列表
rois: 可选的ROI配置列表
Returns:
tuple: (批次数据, 缩放信息列表)
tuple: (批次数据 [4, 3, H, W], 缩放信息列表)
"""
from core.tensorrt_engine import pad_to_batch4
if rois is None:
rois = [None] * len(images)
@@ -436,7 +419,7 @@ class ImagePreprocessor:
processed_images.append(processed)
scale_info_list.append(scale_info)
batch_data = self._batch_preprocessor._stack_and_normalize(processed_images)
batch_data = self._batch_preprocessor.preprocess_batch(processed_images)
return batch_data, scale_info_list
@@ -463,13 +446,11 @@ class ImagePreprocessor:
"config": {
"input_width": self.config.input_width,
"input_height": self.config.input_height,
"batch_size": self.config.batch_size,
"max_batch_size": self.config.max_batch_size,
"batch_size": self._batch_preprocessor.batch_size,
"fp16_mode": self.config.fp16_mode,
},
"memory": self._batch_preprocessor.get_memory_usage(),
}
def release_resources(self):
"""释放资源"""
self._batch_preprocessor.release_memory()
self._logger.info("预处理器资源已释放")

View File

@@ -1,9 +1,9 @@
"""
TensorRT推理引擎模块
固定 batch=4, FP16, 3×480×480
工业级实现Buffer Pool、异步推理、性能监控
"""
import ctypes
import logging
import threading
import time
@@ -38,8 +38,31 @@ class HostDeviceMem:
return f"Host:{self.host.shape}, Device:{int(self.device)}"
def pad_to_batch4(frames: List[np.ndarray]) -> np.ndarray:
"""
Padding 到 batch=4重复最后一帧
Args:
frames: list of [3, 480, 480] numpy arrays
Returns:
np.ndarray: [4, 3, 480, 480]
"""
if len(frames) == 0:
raise ValueError("Empty frames list")
if len(frames) == 4:
return np.stack(frames)
pad_frame = frames[-1].copy()
while len(frames) < 4:
frames.append(pad_frame)
return np.stack(frames)
class TensorRTEngine:
"""工业级 TensorRT 引擎
"""固定 batch TensorRT 引擎 (batch=4, FP16, 3×480×480)
特性:
- Buffer Pool: bindings 只在 init 阶段分配一次
@@ -47,6 +70,9 @@ class TensorRTEngine:
- Async API: CUDA stream + async memcpy + execute_async_v2
"""
BATCH_SIZE = 4
INPUT_SHAPE = (3, 480, 480)
def __init__(self, config: Optional[InferenceConfig] = None):
if not TRT_AVAILABLE:
raise RuntimeError("TensorRT 未安装,请先安装 tensorrt 库")
@@ -68,7 +94,6 @@ class TensorRTEngine:
self._bindings: List[int] = []
self._inputs: List[HostDeviceMem] = []
self._outputs: List[HostDeviceMem] = []
self._binding_names: Dict[int, str] = {}
self._performance_stats = {
"inference_count": 0,
@@ -81,8 +106,8 @@ class TensorRTEngine:
self._logger.info(
f"TensorRT 引擎初始化: "
f"{config.model_path}, "
f"{config.input_width}x{config.input_height}, "
f"batch={config.batch_size}, "
f"batch={self.BATCH_SIZE}, "
f"shape={self.INPUT_SHAPE}, "
f"fp16={config.fp16_mode}"
)
@@ -113,7 +138,7 @@ class TensorRTEngine:
"load", "TensorRT", engine_path, True
)
self._logger.info(f"TensorRT 引擎加载成功: {engine_path}")
self._logger.info(f" 输入: {len(self._inputs)}, 输出: {len(self._outputs)}")
self._logger.info(f" 输入: {len(self._inputs)}, 输出: {len(self._outputs)}, batch={self.BATCH_SIZE}")
return True
@@ -122,30 +147,31 @@ class TensorRTEngine:
return False
def _allocate_buffers(self):
"""Buffer Pool: 初始化阶段一次性分配所有 bindings(工业级关键点)"""
"""Buffer Pool: 初始化阶段一次性分配所有 bindings
对于动态 shape engine使用配置中的 batch_size 作为默认大小
"""
self._bindings = []
self._inputs = []
self._outputs = []
self._binding_names = {}
for binding_idx in range(self._engine.num_bindings):
name = self._engine.get_binding_name(binding_idx)
shape = list(self._engine.get_binding_shape(binding_idx))
dtype = trt.nptype(self._engine.get_binding_dtype(binding_idx))
shape = self._engine.get_binding_shape(binding_idx)
self._binding_names[binding_idx] = name
if shape[0] == -1:
shape[0] = self.BATCH_SIZE
shape = tuple(max(1, s) if s < 0 else s for s in shape)
size = trt.volume(shape)
try:
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
except Exception as e:
self._logger.warning(f"pagelocked memory 分配失败,回退到普通 numpy: {e}")
host_mem = np.zeros(size, dtype=dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
device_mem = cuda.mem_alloc(host_mem.nbytes)
self._bindings.append(int(device_mem))
mem_pair = HostDeviceMem(host_mem, device_mem)
@@ -159,24 +185,13 @@ class TensorRTEngine:
raise RuntimeError("No input bindings found")
if len(self._outputs) == 0:
raise RuntimeError("No output bindings found")
self._logger.debug(
f"Buffer Pool 分配完成: "
f"inputs={[int(i.device) for i in self._inputs]}, "
f"outputs={[int(o.device) for o in self._outputs]}"
)
def _get_output_shape(self, binding_idx: int) -> Tuple[int, ...]:
"""获取输出的 shape"""
name = self._binding_names[binding_idx]
return self._engine.get_binding_shape(name)
def infer(self, input_np: np.ndarray) -> Tuple[List[np.ndarray], float]:
def infer(self, input_batch: np.ndarray) -> Tuple[List[np.ndarray], float]:
"""
执行推理(工业级 async 模式)
Args:
input_np: numpy 输入shape 必须与 engine 一致
input_batch: numpy 输入shape = [batch, 3, 480, 480]dtype = np.float16
Returns:
tuple: (输出列表, 推理耗时ms)
@@ -187,17 +202,20 @@ class TensorRTEngine:
if len(self._inputs) == 0:
raise RuntimeError("未分配输入 buffer")
batch_size = input_batch.shape[0]
start_time = time.perf_counter()
self._cuda_context.push()
try:
input_np = np.ascontiguousarray(input_np)
input_batch = np.ascontiguousarray(input_batch)
input_name = self._binding_names[0]
self._context.set_input_shape(input_name, input_np.shape)
input_name = self._engine.get_binding_name(0)
actual_shape = list(input_batch.shape)
self._context.set_input_shape(input_name, actual_shape)
np.copyto(self._inputs[0].host, input_np.ravel())
np.copyto(self._inputs[0].host, input_batch.ravel())
cuda.memcpy_htod_async(
self._inputs[0].device,
@@ -210,28 +228,20 @@ class TensorRTEngine:
stream_handle=self._stream.handle
)
results = []
for out in self._outputs:
cuda.memcpy_dtoh_async(
out.host,
out.device,
self._stream
)
results.append(out.host.copy())
self._stream.synchronize()
inference_time_ms = (time.perf_counter() - start_time) * 1000
batch_size = input_np.shape[0]
self._update_performance_stats(inference_time_ms, batch_size)
output_shapes = []
for i in range(len(self._inputs), self._engine.num_bindings):
output_shapes.append(self._get_output_shape(i))
results = []
for idx, out in enumerate(self._outputs):
shape = output_shapes[idx] if idx < len(output_shapes) else out.host.shape
results.append(out.host.reshape(shape))
self._update_performance_stats(inference_time_ms, self.BATCH_SIZE)
return results, inference_time_ms

View File

@@ -0,0 +1,123 @@
## 表结构对比报告
---
### 一、原始表结构SQLAlchemy ORM云边同步型
#### 1. Camera 摄像头表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Integer | 主键 |
| cloud_id | Integer | 云端ID |
| name | String(64) | 名称 |
| rtsp_url | Text | RTSP地址 |
| enabled | Boolean | 启用 |
| fps_limit | Integer | 帧率限制 |
| process_every_n_frames | Integer | 跳帧处理 |
| pending_sync | Boolean | 待同步 |
| sync_failed_at | DateTime | 失败时间 |
| sync_retry_count | Integer | 重试次数 |
#### 2. CameraStatus 运行状态表
| 字段 | 类型 | 说明 |
|------|------|------|
| is_running | Boolean | 运行状态 |
| last_frame_time | DateTime | 最后帧时间 |
| fps | Float | 当前帧率 |
| error_message | Text | 错误信息 |
#### 3. ROI 规则+行为表
| 字段 | 类型 | 说明 |
|------|------|------|
| roi_id | String(64) | 唯一标识 |
| name | String(128) | 名称 |
| roi_type | String | ROI类型 |
| points | Text | 坐标(JSON) |
| rule_type | String | 规则类型 |
| stay_time | Integer | 停留时间 |
| threshold_sec | Integer | 确认阈值 |
| confirm_sec | Integer | 确认时间 |
| return_sec | Integer | 恢复时间 |
| working_hours | Text | 工作时段 |
#### 4. Alarm 告警表
| 字段 | 类型 | 说明 |
|------|------|------|
| cloud_id | Integer | 云端ID |
| upload_status | Text | 上传状态 |
| llm_checked | Boolean | LLM审核 |
| processed | Boolean | 处理标记 |
---
### 二、当前项目表结构SQLite边缘实时型
#### 1. camera_configs 摄像头配置
| 字段 | 类型 | 说明 |
|------|------|------|
| camera_id | TEXT PK | 主键 |
| rtsp_url | TEXT | RTSP地址 |
| camera_name | TEXT | 名称 |
| status | BOOLEAN | 状态 |
| enabled | BOOLEAN | 启用 |
#### 2. roi_configs ROI+算法配置 ⭐
| 字段 | 类型 | 说明 |
|------|------|------|
| roi_id | TEXT PK | 主键 |
| camera_id | TEXT | 摄像头ID |
| coordinates | TEXT | 坐标 |
| **algorithm_type** | TEXT | **算法类型** |
| confirm_on_duty_sec | INTEGER | 在职确认 |
| confirm_leave_sec | INTEGER | 离岗确认 |
| cooldown_sec | INTEGER | 冷却时间 |
| target_class | TEXT | 目标类别 |
#### 3. alert_records 告警记录
| 字段 | 类型 | 说明 |
|------|------|------|
| alert_id | TEXT UK | 告警UUID |
| camera_id | TEXT | 摄像头 |
| alert_type | TEXT | 类型 |
| confidence | REAL | 置信度 |
| bbox | TEXT | 边界框 |
| **duration_minutes** | REAL | **离岗时长** |
| status | TEXT | 状态 |
#### 4. config_update_log 配置日志(新增)
| 字段 | 类型 | 说明 |
|------|------|------|
| config_type | TEXT | 配置类型 |
| old_value/new_value | TEXT | 变更前后 |
---
### 三、核心差异总结
| 维度 | 原始设计 | 当前设计 |
|------|---------|---------|
| 定位 | 云端主控 | 边缘实时 |
| ORM | SQLAlchemy强关联 | 无(扁平化) |
| 云边同步 | cloud_id/sync_version | 未实现 |
| ROI语义 | 规则驱动 | **算法驱动** ⭐ |
| 运维状态 | 独立CameraStatus表 | 无 |
| 配置审计 | 无 | **有** ⭐ |
| 离岗时长 | 隐含字段 | **显式字段** ⭐ |
---
### 四、当前项目优势
1. **algorithm_type 字段** - 支持多算法多ROI
2. **config_update_log** - 可审计可追溯
3. **异步写入队列** - 高性能
4. **WAL模式** - 提升写入性能
5. **7天自动清理** - 磁盘管理
---
### 五、建议补强
1. 添加 `camera_status` 表记录运行状态
2. 可扩展云边同步模块
3. **duration_minutes 已添加**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -204,7 +204,7 @@ class EdgeInferenceService:
frame: VideoFrame,
roi
):
"""处理ROI帧"""
"""处理ROI帧,固定 batch=4 推理"""
try:
if not roi.enabled:
return
@@ -213,7 +213,7 @@ class EdgeInferenceService:
processed_image, scale_info = cropped
batch_data = self._preprocessor._batch_preprocessor._stack_and_normalize(
batch_data, _ = self._preprocessor._batch_preprocessor.preprocess_batch(
[processed_image]
)