Compare commits
4 Commits
956bcbbc3e
...
4a58d190c0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a58d190c0 | |||
| c17f983ab3 | |||
| 3dd4e56f99 | |||
| 745cadc8e7 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -49,3 +49,5 @@ README.md
|
||||
# 数据目录(不提交)
|
||||
data/
|
||||
captures/
|
||||
/logs/
|
||||
/tests/
|
||||
|
||||
56
analyze_latency.py
Normal file
56
analyze_latency.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""详细延迟分析 - 简化版"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
from config.settings import get_settings
|
||||
from core.preprocessor import ImagePreprocessor
|
||||
|
||||
settings = get_settings()
|
||||
preprocessor = ImagePreprocessor(settings.inference)
|
||||
|
||||
# 模拟 100 次推理
|
||||
img = np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8)
|
||||
roi_mock = type('ROI', (), {'x1': 300, 'y1': 100, 'x2': 1000, 'y2': 800, 'enabled': True})()
|
||||
|
||||
times_preprocess = []
|
||||
times_single = []
|
||||
times_batch = []
|
||||
|
||||
for _ in range(100):
|
||||
# 1. preprocess_single
|
||||
start = time.perf_counter()
|
||||
cropped = preprocessor.preprocess_single(img, roi_mock)
|
||||
t = (time.perf_counter() - start) * 1000
|
||||
times_single.append(t)
|
||||
|
||||
# 2. preprocess_batch (1→4)
|
||||
start = time.perf_counter()
|
||||
batch_data, _ = preprocessor._batch_preprocessor.preprocess_batch([cropped[0]])
|
||||
t = (time.perf_counter() - start) * 1000
|
||||
times_batch.append(t)
|
||||
|
||||
# 3. 完整 preprocess (single + batch)
|
||||
start = time.perf_counter()
|
||||
cropped = preprocessor.preprocess_single(img, roi_mock)
|
||||
batch_data, _ = preprocessor._batch_preprocessor.preprocess_batch([cropped[0]])
|
||||
t = (time.perf_counter() - start) * 1000
|
||||
times_preprocess.append(t)
|
||||
|
||||
print("延迟分析 (100次平均):")
|
||||
print(f" preprocess_single (ROI + resize): {np.mean(times_single):.2f}ms")
|
||||
print(f" preprocess_batch (padding 1→4): {np.mean(times_batch):.2f}ms")
|
||||
print(f" 完整预处理: {np.mean(times_preprocess):.2f}ms")
|
||||
print()
|
||||
print(f"TensorRT 推理 (batch=1): ~2.5ms (基准测试)")
|
||||
print(f"TensorRT 推理 (batch=4): ~5.0ms (基准测试)")
|
||||
print()
|
||||
print("推算总延迟:")
|
||||
print(f" 方案A (batch=1): {np.mean(times_single):.2f} + 2.5 + 后处理 ≈ 10-15ms")
|
||||
print(f" 方案B (batch=4 实际只推理1帧): {np.mean(times_preprocess):.2f} + 5 + 后处理 ≈ 55-65ms")
|
||||
print()
|
||||
print("结论:延迟主要来自 batch padding 和不必要的 4帧推理开销")
|
||||
44
analyze_latency_batch1.py
Normal file
44
analyze_latency_batch1.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""延迟分析 - batch=1 优化后"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from config.settings import get_settings
|
||||
from core.preprocessor import ImagePreprocessor, BatchPreprocessor
|
||||
|
||||
settings = get_settings()
|
||||
preprocessor = ImagePreprocessor(settings.inference)
|
||||
|
||||
img = np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8)
|
||||
roi_mock = type('ROI', (), {'x1': 300, 'y1': 100, 'x2': 1000, 'y2': 800, 'enabled': True, 'roi_type': 0})()
|
||||
|
||||
times_preprocess_single = []
|
||||
times_preprocess_batch = []
|
||||
|
||||
for _ in range(100):
|
||||
# 1. preprocess_single
|
||||
start = time.perf_counter()
|
||||
cropped = preprocessor.preprocess_single(img, roi_mock)
|
||||
t = (time.perf_counter() - start) * 1000
|
||||
times_preprocess_single.append(t)
|
||||
|
||||
# 2. preprocess_batch (batch=1)
|
||||
start = time.perf_counter()
|
||||
batch_data, _ = preprocessor._batch_preprocessor.preprocess_batch([cropped[0]])
|
||||
t = (time.perf_counter() - start) * 1000
|
||||
times_preprocess_batch.append(t)
|
||||
|
||||
print("延迟分析 (batch=1 优化后):")
|
||||
print(f" preprocess_single: {np.mean(times_preprocess_single):.2f}ms")
|
||||
print(f" preprocess_batch: {np.mean(times_preprocess_batch):.2f}ms")
|
||||
print(f" 总预处理: {np.mean(times_preprocess_single) + np.mean(times_preprocess_batch):.2f}ms")
|
||||
print()
|
||||
print(f"TensorRT batch=1 推理: ~2.5ms")
|
||||
print(f"TensorRT batch=4 推理: ~5.0ms")
|
||||
print()
|
||||
print("推算总延迟:")
|
||||
print(f" batch=1: {np.mean(times_preprocess_single) + np.mean(times_preprocess_batch):.2f} + 2.5 ≈ 8-12ms")
|
||||
print(f" batch=4: {np.mean(times_preprocess_single) + np.mean(times_preprocess_batch):.2f} + 5 ≈ 10-15ms")
|
||||
96
benchmark_trt.py
Normal file
96
benchmark_trt.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""TensorRT 纯推理延迟测试"""
|
||||
import numpy as np
|
||||
import tensorrt as trt
|
||||
import pycuda.driver as cuda
|
||||
import pycuda.autoinit
|
||||
import time
|
||||
|
||||
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()
|
||||
|
||||
input_shape = (1, 3, 480, 480)
|
||||
input_data = np.random.randn(*input_shape).astype(np.float32)
|
||||
|
||||
context.set_input_shape('images', input_shape)
|
||||
|
||||
output_shape = tuple(max(1, s) for s in engine.get_binding_shape(1))
|
||||
output_size = int(np.prod(output_shape))
|
||||
|
||||
h_input = cuda.pagelocked_empty(input_data.size, np.float32)
|
||||
h_output = cuda.pagelocked_empty(output_size, np.float32)
|
||||
|
||||
np.copyto(h_input, input_data.ravel())
|
||||
|
||||
d_input = cuda.mem_alloc(h_input.nbytes)
|
||||
d_output = cuda.mem_alloc(h_output.nbytes)
|
||||
|
||||
bindings = [int(d_input), int(d_output)]
|
||||
|
||||
# Warmup
|
||||
for _ in range(10):
|
||||
cuda.memcpy_htod(d_input, h_input)
|
||||
context.execute_v2(bindings=bindings)
|
||||
cuda.memcpy_dtoh(h_output, d_output)
|
||||
|
||||
# Benchmark
|
||||
times = []
|
||||
for _ in range(100):
|
||||
start = time.perf_counter()
|
||||
cuda.memcpy_htod(d_input, h_input)
|
||||
context.execute_v2(bindings=bindings)
|
||||
cuda.memcpy_dtoh(h_output, d_output)
|
||||
times.append((time.perf_counter() - start) * 1000)
|
||||
|
||||
print(f'TensorRT 纯推理延迟 (batch=1):')
|
||||
print(f' 平均: {np.mean(times):.2f}ms')
|
||||
print(f' 中位数: {np.median(times):.2f}ms')
|
||||
print(f' 最小: {np.min(times):.2f}ms')
|
||||
print(f' 最大: {np.max(times):.2f}ms')
|
||||
print(f' P95: {np.percentile(times, 95):.2f}ms')
|
||||
print()
|
||||
|
||||
# 再测试 batch=4
|
||||
print("测试 batch=4...")
|
||||
input_shape_4 = (4, 3, 480, 480)
|
||||
input_data_4 = np.random.randn(*input_shape_4).astype(np.float32)
|
||||
context.set_input_shape('images', input_shape_4)
|
||||
|
||||
output_shape_4 = (4, 84, 4725)
|
||||
output_size_4 = int(np.prod(output_shape_4))
|
||||
|
||||
h_input_4 = cuda.pagelocked_empty(input_data_4.size, np.float32)
|
||||
h_output_4 = cuda.pagelocked_empty(output_size_4, np.float32)
|
||||
|
||||
np.copyto(h_input_4, input_data_4.ravel())
|
||||
|
||||
d_input_4 = cuda.mem_alloc(h_input_4.nbytes)
|
||||
d_output_4 = cuda.mem_alloc(h_output_4.nbytes)
|
||||
|
||||
bindings_4 = [int(d_input_4), int(d_output_4)]
|
||||
|
||||
# Warmup
|
||||
for _ in range(10):
|
||||
cuda.memcpy_htod(d_input_4, h_input_4)
|
||||
context.execute_v2(bindings=bindings_4)
|
||||
cuda.memcpy_dtoh(h_output_4, d_output_4)
|
||||
|
||||
# Benchmark
|
||||
times_4 = []
|
||||
for _ in range(100):
|
||||
start = time.perf_counter()
|
||||
cuda.memcpy_htod(d_input_4, h_input_4)
|
||||
context.execute_v2(bindings=bindings_4)
|
||||
cuda.memcpy_dtoh(h_output_4, d_output_4)
|
||||
times_4.append((time.perf_counter() - start) * 1000)
|
||||
|
||||
print(f'TensorRT 纯推理延迟 (batch=4):')
|
||||
print(f' 平均: {np.mean(times_4):.2f}ms')
|
||||
print(f' 中位数: {np.median(times_4):.2f}ms')
|
||||
print(f' 最小: {np.min(times_4):.2f}ms')
|
||||
print(f' 最大: {np.max(times_4):.2f}ms')
|
||||
print(f' P95: {np.percentile(times_4, 95):.2f}ms')
|
||||
43
check_engine.py
Normal file
43
check_engine.py
Normal 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
53
check_engine_output.py
Normal 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()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -76,7 +76,6 @@ class InferenceConfig:
|
||||
input_width: int = 480
|
||||
input_height: int = 480
|
||||
batch_size: int = 1
|
||||
max_batch_size: int = 8
|
||||
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")),
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -22,18 +22,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NMSProcessor:
|
||||
"""非极大值抑制处理器
|
||||
"""非极大值抑制处理器 (向量化版本)
|
||||
|
||||
实现高效的NMS算法去除冗余检测框
|
||||
使用纯 NumPy 向量化操作,避免 Python 循环
|
||||
"""
|
||||
|
||||
def __init__(self, nms_threshold: float = 0.45):
|
||||
"""
|
||||
初始化NMS处理器
|
||||
|
||||
Args:
|
||||
nms_threshold: NMS阈值
|
||||
"""
|
||||
self.nms_threshold = nms_threshold
|
||||
self._logger = get_logger("postprocessor")
|
||||
|
||||
@@ -45,7 +39,7 @@ class NMSProcessor:
|
||||
max_output_size: int = 300
|
||||
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""
|
||||
执行NMS
|
||||
执行NMS (向量化版本)
|
||||
|
||||
Args:
|
||||
boxes: 检测框数组 [N, 4] (x1, y1, x2, y2)
|
||||
@@ -59,48 +53,56 @@ class NMSProcessor:
|
||||
if len(boxes) == 0:
|
||||
return np.array([], dtype=np.int32), np.array([]), np.array([])
|
||||
|
||||
x1 = boxes[:, 0]
|
||||
y1 = boxes[:, 1]
|
||||
x2 = boxes[:, 2]
|
||||
y2 = boxes[:, 3]
|
||||
order = np.argsort(scores)[::-1]
|
||||
|
||||
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
|
||||
keep_mask = np.zeros(len(boxes), dtype=bool)
|
||||
|
||||
order = scores.argsort()[::-1]
|
||||
|
||||
keep_indices = []
|
||||
|
||||
while len(order) > 0:
|
||||
if len(keep_indices) >= max_output_size:
|
||||
i = 0
|
||||
while i < len(order) and i < max_output_size:
|
||||
idx = order[i]
|
||||
if keep_mask[idx]:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
keep_mask[idx] = True
|
||||
|
||||
remaining = order[i + 1:]
|
||||
if len(remaining) == 0:
|
||||
break
|
||||
|
||||
i = order[0]
|
||||
keep_indices.append(i)
|
||||
|
||||
if len(order) == 1:
|
||||
remaining_mask = ~keep_mask[remaining]
|
||||
if not np.any(remaining_mask):
|
||||
break
|
||||
|
||||
remaining = order[1:]
|
||||
remaining = remaining[remaining_mask]
|
||||
|
||||
xx1 = np.maximum(x1[i], x1[remaining])
|
||||
yy1 = np.maximum(y1[i], y1[remaining])
|
||||
xx2 = np.minimum(x2[i], x2[remaining])
|
||||
yy2 = np.minimum(y2[i], y2[remaining])
|
||||
xx1 = np.maximum(boxes[idx, 0], boxes[remaining, 0])
|
||||
yy1 = np.maximum(boxes[idx, 1], boxes[remaining, 1])
|
||||
xx2 = np.minimum(boxes[idx, 2], boxes[remaining, 2])
|
||||
yy2 = np.minimum(boxes[idx, 3], boxes[remaining, 3])
|
||||
|
||||
w = np.maximum(0.0, xx2 - xx1 + 1)
|
||||
h = np.maximum(0.0, yy2 - yy1 + 1)
|
||||
|
||||
inter = w * h
|
||||
ovr = inter / (areas[i] + areas[remaining] - inter)
|
||||
|
||||
indices = np.where(ovr <= self.nms_threshold)[0]
|
||||
areas = (boxes[:, 2] - boxes[:, 0] + 1) * (boxes[:, 3] - boxes[:, 1] + 1)
|
||||
ovr = inter / (areas[idx] + areas[remaining] - inter + 1e-6)
|
||||
|
||||
order = remaining[indices]
|
||||
suppress_mask = ovr > self.nms_threshold
|
||||
for j in np.where(suppress_mask)[0]:
|
||||
keep_mask[remaining[j]] = True
|
||||
|
||||
i += 1
|
||||
|
||||
keep_indices = np.array(keep_indices, dtype=np.int32)
|
||||
keep_indices = np.where(keep_mask)[0]
|
||||
|
||||
if len(keep_indices) > max_output_size:
|
||||
top_k = np.argsort(scores[keep_indices])[::-1][:max_output_size]
|
||||
keep_indices = keep_indices[top_k]
|
||||
|
||||
return (
|
||||
keep_indices,
|
||||
keep_indices.astype(np.int32),
|
||||
scores[keep_indices],
|
||||
class_ids[keep_indices] if class_ids is not None else np.array([])
|
||||
)
|
||||
@@ -584,7 +586,7 @@ class PostProcessor:
|
||||
outputs: List[np.ndarray]
|
||||
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""
|
||||
解析YOLO模型输出
|
||||
解析YOLO模型输出 - 向量化版本
|
||||
|
||||
Args:
|
||||
outputs: 模型输出列表
|
||||
@@ -597,56 +599,45 @@ class PostProcessor:
|
||||
|
||||
output = outputs[0]
|
||||
|
||||
if len(output.shape) == 3:
|
||||
if output.ndim == 3:
|
||||
output = output[0]
|
||||
|
||||
num_detections = output.shape[0]
|
||||
|
||||
boxes = []
|
||||
scores = []
|
||||
class_ids = []
|
||||
|
||||
for i in range(num_detections):
|
||||
detection = output[i]
|
||||
|
||||
if len(detection) < 6:
|
||||
continue
|
||||
|
||||
x_center = detection[0]
|
||||
y_center = detection[1]
|
||||
width = detection[2]
|
||||
height = detection[3]
|
||||
|
||||
obj_conf = detection[4]
|
||||
|
||||
class_scores = detection[5:]
|
||||
if len(class_scores) == 0:
|
||||
continue
|
||||
|
||||
class_id = np.argmax(class_scores)
|
||||
class_conf = class_scores[class_id]
|
||||
|
||||
total_conf = obj_conf * class_conf
|
||||
|
||||
if total_conf < 0.0:
|
||||
continue
|
||||
|
||||
x1 = x_center - width / 2
|
||||
y1 = y_center - height / 2
|
||||
x2 = x_center + width / 2
|
||||
y2 = y_center + height / 2
|
||||
|
||||
boxes.append([x1, y1, x2, y2])
|
||||
scores.append(total_conf)
|
||||
class_ids.append(class_id)
|
||||
|
||||
if not boxes:
|
||||
if output.ndim != 2:
|
||||
return np.array([]), np.array([]), np.array([])
|
||||
|
||||
if output.shape[0] != 84:
|
||||
return np.array([]), np.array([]), np.array([])
|
||||
|
||||
num_boxes = output.shape[1]
|
||||
|
||||
boxes_xywh = output[0:4, :].T
|
||||
|
||||
obj_conf = output[4, :]
|
||||
|
||||
cls_scores = output[5:, :]
|
||||
|
||||
person_scores = cls_scores[0, :]
|
||||
|
||||
scores = obj_conf * person_scores
|
||||
|
||||
valid_mask = scores > self._conf_threshold
|
||||
|
||||
if not np.any(valid_mask):
|
||||
return np.array([]), np.array([]), np.array([])
|
||||
|
||||
boxes = boxes_xywh[valid_mask]
|
||||
scores_filtered = scores[valid_mask]
|
||||
|
||||
boxes_xyxy = np.zeros_like(boxes)
|
||||
boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2
|
||||
boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2
|
||||
boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2
|
||||
boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2
|
||||
|
||||
return (
|
||||
np.array(boxes),
|
||||
np.array(scores),
|
||||
np.array(class_ids)
|
||||
boxes_xyxy.astype(np.float32),
|
||||
scores_filtered.astype(np.float32),
|
||||
np.zeros(len(boxes), dtype=np.int32)
|
||||
)
|
||||
|
||||
def filter_by_roi(
|
||||
|
||||
@@ -225,126 +225,70 @@ class LetterboxPreprocessor:
|
||||
|
||||
|
||||
class BatchPreprocessor:
|
||||
"""Batch预处理器类
|
||||
"""Batch预处理器类 (batch=1)"""
|
||||
|
||||
支持动态Batch大小,转换为NCHW格式,FP16精度
|
||||
"""
|
||||
BATCH_SIZE = 1
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target_size: Tuple[int, int] = (480, 480),
|
||||
max_batch_size: int = 8,
|
||||
fp16_mode: bool = True
|
||||
):
|
||||
self.target_size = target_size
|
||||
self.fp16_mode = fp16_mode
|
||||
self.batch_size = self.BATCH_SIZE
|
||||
|
||||
self._logger = get_logger("preprocessor")
|
||||
|
||||
self._logger.info(
|
||||
f"Batch预处理器: batch={self.batch_size}, "
|
||||
f"target_size={target_size}, fp16={fp16_mode}"
|
||||
)
|
||||
|
||||
def preprocess_single(
|
||||
self,
|
||||
image: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
初始化Batch预处理器
|
||||
预处理单帧图像
|
||||
|
||||
Args:
|
||||
target_size: 目标尺寸 (width, height)
|
||||
max_batch_size: 最大Batch大小
|
||||
fp16_mode: 是否使用FP16精度
|
||||
image: numpy 数组
|
||||
|
||||
Returns:
|
||||
np.ndarray: [1, 3, H, W]
|
||||
"""
|
||||
self.target_size = target_size
|
||||
self.max_batch_size = max_batch_size
|
||||
self.fp16_mode = fp16_mode
|
||||
normalized = image.astype(np.float32) / 255.0
|
||||
transposed = np.transpose(normalized, (2, 0, 1))
|
||||
batched = transposed[None, ...]
|
||||
|
||||
self._letterbox = LetterboxPreprocessor(target_size)
|
||||
self._logger = get_logger("preprocessor")
|
||||
self._lock = threading.Lock()
|
||||
if self.fp16_mode:
|
||||
batched = batched.astype(np.float16)
|
||||
|
||||
self._memory_pool: List[np.ndarray] = []
|
||||
self._preallocated_size = max_batch_size
|
||||
return batched
|
||||
|
||||
def preprocess_batch(
|
||||
self,
|
||||
images: List[np.ndarray]
|
||||
) -> Tuple[np.ndarray, List[Tuple[float, float, float, float]]]:
|
||||
"""
|
||||
预处理一个批次的图像
|
||||
预处理批次图像 (batch=1)
|
||||
|
||||
Args:
|
||||
images: 图像列表
|
||||
images: 图像列表 (只处理第一帧)
|
||||
|
||||
Returns:
|
||||
tuple: (批次数据, 缩放信息列表)
|
||||
tuple: (批次数据 [1, 3, H, W], 缩放信息列表)
|
||||
"""
|
||||
batch_size = len(images)
|
||||
batch_size = min(batch_size, self.max_batch_size)
|
||||
if not images:
|
||||
raise ValueError("Empty images list")
|
||||
|
||||
scale_info_list = []
|
||||
processed_images = []
|
||||
letterbox = LetterboxPreprocessor(self.target_size)
|
||||
processed, scale_info = letterbox.preprocess(images[0])
|
||||
|
||||
for i in range(batch_size):
|
||||
if i >= len(images):
|
||||
break
|
||||
|
||||
processed, scale_info = self._letterbox.preprocess(images[i])
|
||||
processed_images.append(processed)
|
||||
scale_info_list.append(scale_info)
|
||||
batch_data = self.preprocess_single(processed)
|
||||
|
||||
batch_data = self._stack_and_normalize(processed_images)
|
||||
|
||||
return batch_data, scale_info_list
|
||||
|
||||
def _stack_and_normalize(self, images: List[np.ndarray]) -> np.ndarray:
|
||||
"""堆叠并归一化图像"""
|
||||
stacked = np.stack(images, axis=0)
|
||||
|
||||
stacked = stacked.astype(np.float32) / 255.0
|
||||
|
||||
stacked = np.transpose(stacked, (0, 3, 1, 2))
|
||||
|
||||
if self.fp16_mode:
|
||||
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)
|
||||
}
|
||||
return batch_data, [scale_info]
|
||||
|
||||
|
||||
class ImagePreprocessor:
|
||||
@@ -372,7 +316,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 +323,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 +359,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 +381,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 +408,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("预处理器资源已释放")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
TensorRT推理引擎模块
|
||||
固定 batch=4, FP16, 3×480×480
|
||||
工业级实现:Buffer Pool、异步推理、性能监控
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
@@ -38,8 +38,24 @@ class HostDeviceMem:
|
||||
return f"Host:{self.host.shape}, Device:{int(self.device)}"
|
||||
|
||||
|
||||
def pad_to_batch4(frames: List[np.ndarray]) -> np.ndarray:
|
||||
"""
|
||||
Padding 到 batch=N,重复最后一帧(已弃用,改用 batch=1)
|
||||
|
||||
Args:
|
||||
frames: list of [3, 480, 480] numpy arrays
|
||||
|
||||
Returns:
|
||||
np.ndarray: [N, 3, 480, 480]
|
||||
"""
|
||||
if len(frames) == 0:
|
||||
raise ValueError("Empty frames list")
|
||||
|
||||
return np.stack(frames)
|
||||
|
||||
|
||||
class TensorRTEngine:
|
||||
"""工业级 TensorRT 引擎
|
||||
"""TensorRT 引擎 (batch=1, FP16, 3×480×480)
|
||||
|
||||
特性:
|
||||
- Buffer Pool: bindings 只在 init 阶段分配一次
|
||||
@@ -47,6 +63,9 @@ class TensorRTEngine:
|
||||
- Async API: CUDA stream + async memcpy + execute_async_v2
|
||||
"""
|
||||
|
||||
BATCH_SIZE = 1
|
||||
INPUT_SHAPE = (3, 480, 480)
|
||||
|
||||
def __init__(self, config: Optional[InferenceConfig] = None):
|
||||
if not TRT_AVAILABLE:
|
||||
raise RuntimeError("TensorRT 未安装,请先安装 tensorrt 库")
|
||||
@@ -68,7 +87,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 +99,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 +131,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 +140,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 +178,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 +195,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 +221,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
|
||||
|
||||
|
||||
69
debug_output_shape.py
Normal file
69
debug_output_shape.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""调试 TensorRT 输出 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()
|
||||
|
||||
input_shape = (1, 3, 480, 480)
|
||||
input_data = np.random.randn(*input_shape).astype(np.float32)
|
||||
|
||||
input_binding_idx = 0
|
||||
output_binding_idx = 1
|
||||
|
||||
output_shape = engine.get_binding_shape(output_binding_idx)
|
||||
print(f"Engine 定义 output shape: {output_shape}")
|
||||
|
||||
context.set_input_shape(engine.get_binding_name(0), input_shape)
|
||||
|
||||
output_size = int(np.prod([max(1, s) for s in output_shape]))
|
||||
|
||||
h_input = cuda.pagelocked_empty(input_data.size, np.float32)
|
||||
h_output = cuda.pagelocked_empty(output_size, np.float32)
|
||||
|
||||
np.copyto(h_input, input_data.ravel())
|
||||
|
||||
d_input = cuda.mem_alloc(h_input.nbytes)
|
||||
d_output = cuda.mem_alloc(h_output.nbytes)
|
||||
|
||||
bindings = [int(d_input), int(d_output)]
|
||||
|
||||
cuda.memcpy_htod(d_input, h_input)
|
||||
context.execute_v2(bindings=bindings)
|
||||
cuda.memcpy_dtoh(h_output, d_output)
|
||||
|
||||
output_array = h_output.reshape(output_shape)
|
||||
|
||||
print(f"\n实际输出:")
|
||||
print(f" dtype: {output_array.dtype}")
|
||||
print(f" shape: {output_array.shape}")
|
||||
print(f" ndim: {output_array.ndim}")
|
||||
|
||||
if output_array.ndim == 1:
|
||||
print(f" total elements: {output_array.shape[0]}")
|
||||
print(f" expected (84*4725): {84 * 4725}")
|
||||
elif output_array.ndim == 2:
|
||||
print(f" shape[0]: {output_array.shape[0]} (detections)")
|
||||
print(f" shape[1]: {output_array.shape[1]} (features)")
|
||||
elif output_array.ndim == 3:
|
||||
print(f" shape[0]: {output_array.shape[0]} (batch)")
|
||||
print(f" shape[1]: {output_array.shape[1]} (classes+coords)")
|
||||
print(f" shape[2]: {output_array.shape[2]} (num_boxes)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
123
docs/表结构对比报告.md
Normal file
123
docs/表结构对比报告.md
Normal 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 已添加** ✅
|
||||
48442
logs/main.log
48442
logs/main.log
File diff suppressed because it is too large
Load Diff
43096
logs/main_error.log
43096
logs/main_error.log
File diff suppressed because it is too large
Load Diff
4
main.py
4
main.py
@@ -204,7 +204,7 @@ class EdgeInferenceService:
|
||||
frame: VideoFrame,
|
||||
roi
|
||||
):
|
||||
"""处理ROI帧"""
|
||||
"""处理ROI帧,batch=1 推理"""
|
||||
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]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user