video_monitor/app/models.py

429 lines
20 KiB
Python
Raw Normal View History

2026-08-30 22:22:11 +08:00
from django.db import models
from app.utils.Database import g_dbLock
import json
class ThreadSafetyManager(models.Manager):
def get_queryset(self):
with g_dbLock:
ret = super(ThreadSafetyManager, self).get_queryset()
return ret
class StreamModel(models.Model):
"""视频流模型(摄像头管理)"""
objects = ThreadSafetyManager()
user_id = models.IntegerField(verbose_name='用户')
sort = models.IntegerField(verbose_name='排序')
code = models.CharField(max_length=50, verbose_name='编号')
app = models.CharField(max_length=50, verbose_name='流分组')
name = models.CharField(max_length=50, verbose_name='流名称')
pull_stream_url = models.CharField(max_length=300, verbose_name='视频流源地址')
pull_stream_type = models.IntegerField(verbose_name='视频流来源类型') # 0:未知,1:RTSP,2:RTMP,3:FLV,4:HLS,21:GB28181,31:被动RTSP,32:被动RTMP
pull_stream_transfer_mode = models.IntegerField(verbose_name='视频流传输模式') # 0:UDP,1:TCP被动,2:TCP主动
pull_stream_ip = models.CharField(max_length=50, verbose_name='拉流IP')
pull_stream_port = models.IntegerField(verbose_name='拉流端口')
pull_stream_username = models.CharField(max_length=512, verbose_name='拉流用户名')
pull_stream_password = models.CharField(max_length=512, verbose_name='拉流密码')
nickname = models.CharField(max_length=200, verbose_name='视频流昵称')
remark = models.CharField(max_length=200, verbose_name='备注')
forward_state = models.IntegerField(verbose_name='转发状态') # 0:未转发 1:转发中
is_audio = models.IntegerField(default=0, verbose_name='音频传输类型') # 0:静音 1:原始声音
snap_filepath = models.CharField(max_length=200, verbose_name='快照文件路径')
snap_time = models.DateTimeField(auto_now_add=True, verbose_name='快照时间')
camera_sum_num = models.IntegerField(default=0, verbose_name='通道总数')
camera_name = models.CharField(max_length=100, verbose_name='摄像头名称')
camera_manufacturer = models.CharField(max_length=100, verbose_name='摄像头厂商')
camera_owner = models.CharField(max_length=50, verbose_name='摄像头所属者')
camera_model = models.CharField(max_length=50, verbose_name='摄像头型号')
camera_device_id = models.CharField(max_length=50, verbose_name='GB28181设备ID') # gb28181注册的client_id
camera_parent_id = models.CharField(max_length=50, verbose_name='GB28181父设备ID')
camera_civilcode = models.CharField(max_length=50, verbose_name='行政区划码')
camera_last_keepalive_time = models.DateTimeField(auto_now_add=True, verbose_name='最近一次心跳时间')
camera_last_register_time = models.DateTimeField(auto_now_add=True, verbose_name='最近一次注册时间')
# 向上级联国标编号字段v1.0新增start
cascade_device_id = models.CharField(max_length=50, default='', verbose_name='向上级联国标编号') # 自定义向上级联的国标编号为空则使用camera_device_id
cascade_enable = models.IntegerField(default=0, verbose_name='是否启用向上级联') # 0:不启用 1:启用
# 向上级联国标编号字段 end
# 视频分析字段v1.0新增start
algorithm = models.ForeignKey('AlgorithmModel', on_delete=models.SET_NULL, null=True, blank=True,
related_name='streams', verbose_name='分析算法') # null=走默认算法
record_enable = models.IntegerField(default=0, verbose_name='启用24/7录像') # 0:否 1:是
# 视频分析字段 end
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
last_update_time = models.DateTimeField(auto_now_add=True, verbose_name='更新时间')
add_type = models.IntegerField(default=0, verbose_name='添加类型') # 0:手动添加 1:批量导入 10:接口添加 21:GB28181自动添加
state = models.IntegerField(default=0, verbose_name='状态')
def __repr__(self):
return self.nickname
def __str__(self):
return self.nickname
def delete(self, using=None, keep_parents=False):
with g_dbLock:
ret = super(StreamModel, self).delete(using, keep_parents)
return ret
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
with g_dbLock:
ret = super(StreamModel, self).save(force_insert, force_update, using, update_fields)
return ret
class Meta:
db_table = 'av_stream'
verbose_name = '视频流'
verbose_name_plural = '视频流'
class AlgorithmModel(models.Model):
"""算法模型 — 检测算法的元数据与运行时参数(每路摄像头可独立选择)"""
objects = ThreadSafetyManager()
ENGINE_YOLO_PYTORCH = 'yolo_pytorch'
ENGINE_ONNXRUNTIME = 'onnxruntime'
ENGINE_OPENVINO = 'openvino'
ENGINE_CHOICES = (
(ENGINE_YOLO_PYTORCH, 'Yolo-PyTorch'),
(ENGINE_ONNXRUNTIME, 'OnnxRuntime'),
(ENGINE_OPENVINO, 'OpenVINO'),
)
# 算法类型YOLO 检测系列 + ReID 特征系列
ALGO_TYPE_YOLO5 = 'yolo5'
ALGO_TYPE_YOLO8 = 'yolo8'
ALGO_TYPE_YOLO11 = 'yolo11'
ALGO_TYPE_YOLO26 = 'yolo26'
ALGO_TYPE_OSNET = 'osnet'
ALGO_TYPE_CHOICES = (
(ALGO_TYPE_YOLO5, 'YOLOv5'),
(ALGO_TYPE_YOLO8, 'YOLOv8'),
(ALGO_TYPE_YOLO11, 'YOLOv11'),
(ALGO_TYPE_YOLO26, 'YOLO26'),
(ALGO_TYPE_OSNET, 'OSNet ReID'),
)
# 任务类型
TASK_DETECT = 'detect'
TASK_SEGMENT = 'segment'
TASK_CLASSIFY = 'classify'
TASK_POSE = 'pose'
TASK_OBB = 'obb'
TASK_REID = 'reid'
TASK_CHOICES = (
(TASK_DETECT, 'Detect'),
(TASK_SEGMENT, 'Segment'),
(TASK_CLASSIFY, 'Classify'),
(TASK_POSE, 'Pose'),
(TASK_OBB, 'OBB'),
(TASK_REID, 'ReID'),
)
# 推理设备
DEVICE_CPU = 'cpu'
DEVICE_CUDA = 'cuda'
DEVICE_GPU = 'gpu'
DEVICE_CHOICES = (
(DEVICE_CPU, 'CPU'),
(DEVICE_CUDA, 'CUDA'),
(DEVICE_GPU, 'GPU'),
)
name = models.CharField(max_length=100, verbose_name='算法名称')
algorithm_type = models.CharField(max_length=30, default='yolo8', choices=ALGO_TYPE_CHOICES, verbose_name='算法类型')
task_type = models.CharField(max_length=20, default=TASK_DETECT, choices=TASK_CHOICES, verbose_name='任务类型')
inference_engine = models.CharField(max_length=20, default=ENGINE_YOLO_PYTORCH, choices=ENGINE_CHOICES, verbose_name='推理引擎')
device = models.CharField(max_length=20, default=DEVICE_CPU, choices=DEVICE_CHOICES, verbose_name='推理设备')
model_file = models.CharField(max_length=300, default='', verbose_name='模型文件相对路径') # 相对 uploadDir/weight/
model_file_size = models.IntegerField(default=0, verbose_name='模型文件大小(字节)')
input_width = models.IntegerField(default=640, verbose_name='输入宽度')
input_height = models.IntegerField(default=640, verbose_name='输入高度')
conf_threshold = models.FloatField(default=0.4, verbose_name='置信度阈值')
iou_threshold = models.FloatField(default=0.5, verbose_name='NMS IoU 阈值')
labels = models.TextField(default='[]', verbose_name='支持类别JSON数组') # ["person","car",...]
is_default = models.IntegerField(default=0, verbose_name='是否默认算法') # 1=全局兜底
state = models.IntegerField(default=1, verbose_name='状态') # 0=禁用 1=启用
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
last_update_time = models.DateTimeField(auto_now_add=True, verbose_name='更新时间')
def __repr__(self):
return self.name
def __str__(self):
return self.name
def delete(self, using=None, keep_parents=False):
with g_dbLock:
ret = super(AlgorithmModel, self).delete(using, keep_parents)
return ret
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
with g_dbLock:
ret = super(AlgorithmModel, self).save(force_insert, force_update, using, update_fields)
return ret
class Meta:
db_table = 'av_algorithm'
verbose_name = '小模型'
verbose_name_plural = '小模型'
class BizAlgorithmModel(models.Model):
"""业务算法 — 小模型/大模型推理 + 后处理业务逻辑(布控绑定此表)"""
objects = ThreadSafetyManager()
FLOW_SMALL = 1
FLOW_LLM = 2
FLOW_BOTH = 3
FLOW_DETECT_REID = 4
FLOW_CHOICES = (
(FLOW_SMALL, '小模型+后处理'),
(FLOW_LLM, '大模型+后处理'),
(FLOW_BOTH, '小模型+大模型+后处理'),
(FLOW_DETECT_REID, '检测+ReID+后处理'),
)
POST_AREA = 'AREA' # 区域入侵:目标中心在多边形内
POST_LINE_CROSS = 'LINE_CROSS' # 越线检测:轨迹跨过有向线段
POST_LINE_COUNT = 'LINE_COUNT' # 越线计数:正向/逆向分别累计,超阈值报警
POST_DIRECTION = 'DIRECTION' # 方向入侵:移动方向匹配设定方向
POST_DENSITY = 'DENSITY' # 密度报警:区域内目标数 >= 阈值
POST_DWELL = 'DWELL' # 滞留报警:在区域内停留 >= 阈值秒
POST_CHOICES = (
(POST_AREA, '区域入侵'),
(POST_LINE_CROSS, '越线检测'),
(POST_LINE_COUNT, '越线计数'),
(POST_DIRECTION, '方向入侵'),
(POST_DENSITY, '密度报警'),
(POST_DWELL, '滞留报警'),
)
name = models.CharField(max_length=100, verbose_name='算法名称')
flow_type = models.IntegerField(default=FLOW_SMALL, choices=FLOW_CHOICES, verbose_name='流程类型')
small_model = models.ForeignKey(
'AlgorithmModel', on_delete=models.SET_NULL, null=True, blank=True,
related_name='biz_algorithms', verbose_name='小模型',
)
detector_model = models.ForeignKey(
'AlgorithmModel', on_delete=models.SET_NULL, null=True, blank=True,
related_name='biz_algorithms_as_detector', verbose_name='检测小模型(YOLO)',
)
target_labels = models.TextField(default='[]', verbose_name='目标类别JSON') # ["person","car"]
llm = models.ForeignKey(
'LLMModel', on_delete=models.SET_NULL, null=True, blank=True,
related_name='biz_algorithms', verbose_name='大模型',
)
llm_prompt = models.TextField(default='', verbose_name='大模型提示词')
llm_validate = models.TextField(default='', verbose_name='提示词校验值') # 逗号分隔关键词
post_process = models.CharField(max_length=30, default=POST_AREA, choices=POST_CHOICES, verbose_name='后处理逻辑')
# DIRECTION 后处理参数:参考角度(0°=右,90°=下,180°=左,270°=上) 与容差
ref_angle = models.FloatField(default=90.0, verbose_name='方向参考角度')
angle_tolerance = models.FloatField(default=45.0, verbose_name='方向容差(度)')
forward_count_threshold = models.IntegerField(default=0, verbose_name='正向计数报警阈值') # 0=不报警
reverse_count_threshold = models.IntegerField(default=0, verbose_name='逆向计数报警阈值') # 0=不报警
state = models.IntegerField(default=1, verbose_name='状态') # 0=禁用 1=启用
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
last_update_time = models.DateTimeField(auto_now_add=True, verbose_name='更新时间')
def __repr__(self):
return self.name
def __str__(self):
return self.name
def delete(self, using=None, keep_parents=False):
with g_dbLock:
ret = super(BizAlgorithmModel, self).delete(using, keep_parents)
return ret
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
with g_dbLock:
ret = super(BizAlgorithmModel, self).save(force_insert, force_update, using, update_fields)
return ret
class Meta:
db_table = 'av_biz_algorithm'
verbose_name = '业务算法'
verbose_name_plural = '业务算法'
class ZoneModel(models.Model):
"""摄像头区域(多边形)— 跨摄像头追踪/告警规则的区域定义"""
objects = ThreadSafetyManager()
stream = models.ForeignKey(StreamModel, on_delete=models.CASCADE, verbose_name='所属摄像头')
name = models.CharField(max_length=100, verbose_name='区域名称')
coordinates = models.TextField(verbose_name='多边形坐标') # JSON: [[x1,y1],[x2,y2],...]
is_required = models.IntegerField(default=1, verbose_name='是否必需区域') # 1:目标必须在区域内才触发区域类后处理
loiter_threshold = models.IntegerField(default=0, verbose_name='滞留阈值(秒)') # 0=不检测滞留
detect_interval_sec = models.FloatField(default=1.0, verbose_name='检测间隔(秒)') # 每 N 秒
detect_frames = models.IntegerField(default=1, verbose_name='检测帧数') # 分析 M 帧,频率=M/N fps
2026-09-04 18:16:14 +08:00
alarm_repeat_sec = models.FloatField(default=30.0, verbose_name='重复报警间隔(秒)') # 0=同次停留不重复
2026-08-30 22:22:11 +08:00
color = models.CharField(max_length=20, default='#169F85', verbose_name='显示颜色')
# LINE_CROSS 后处理:警戒线段两端点(归一化坐标0~1)JSON: [x,y]
line_a = models.TextField(default='', verbose_name='警戒线端点A') # JSON: [x,y] 归一化
line_b = models.TextField(default='', verbose_name='警戒线端点B') # JSON: [x,y] 归一化
# DENSITY 后处理:密度报警阈值(区域内目标数)
density_threshold = models.IntegerField(default=0, verbose_name='密度阈值') # 0=不检测密度
algorithms = models.ManyToManyField('BizAlgorithmModel', blank=True, related_name='zones', verbose_name='分析算法')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
last_update_time = models.DateTimeField(auto_now_add=True, verbose_name='更新时间')
state = models.IntegerField(default=1, verbose_name='状态') # 1:启用 0:禁用
def __repr__(self):
return self.name
def __str__(self):
return self.name
def delete(self, using=None, keep_parents=False):
with g_dbLock:
ret = super(ZoneModel, self).delete(using, keep_parents)
return ret
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
with g_dbLock:
ret = super(ZoneModel, self).save(force_insert, force_update, using, update_fields)
return ret
class Meta:
db_table = 'av_zone'
verbose_name = '区域'
verbose_name_plural = '区域'
class AlarmModel(models.Model):
"""报警记录 — 布控分析触发的报警事件"""
objects = ThreadSafetyManager()
EVENT_TYPES = (
('entered_zone', '进入区域'),
('loiter', '滞留告警'),
)
stream = models.ForeignKey(StreamModel, null=True, on_delete=models.CASCADE, verbose_name='摄像头')
event_type = models.CharField(max_length=32, default='entered_zone', verbose_name='报警类型')
description = models.CharField(max_length=300, default='', verbose_name='描述')
timestamp = models.DateTimeField(verbose_name='发生时间')
metadata = models.TextField(default='{}', verbose_name='元数据JSON')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='入库时间')
def __repr__(self):
return self.event_type
def __str__(self):
return self.event_type
def delete(self, using=None, keep_parents=False):
with g_dbLock:
ret = super(AlarmModel, self).delete(using, keep_parents)
return ret
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
with g_dbLock:
ret = super(AlarmModel, self).save(force_insert, force_update, using, update_fields)
return ret
class Meta:
db_table = 'av_alarm'
verbose_name = '报警'
verbose_name_plural = '报警'
indexes = [
models.Index(fields=['-timestamp'], name='av_alarm_ts_idx'),
models.Index(fields=['stream', 'timestamp'], name='av_alarm_st_idx'),
]
class RecordingModel(models.Model):
"""24/7 录像分段索引"""
objects = ThreadSafetyManager()
stream = models.ForeignKey(StreamModel, on_delete=models.CASCADE, verbose_name='摄像头')
file_path = models.CharField(max_length=500, verbose_name='文件路径')
start_time = models.DateTimeField(verbose_name='开始时间')
end_time = models.DateTimeField(verbose_name='结束时间')
duration = models.FloatField(default=0, verbose_name='时长(秒)')
file_size = models.BigIntegerField(default=0, verbose_name='文件大小(字节)')
has_motion = models.IntegerField(default=0, verbose_name='含运动')
has_object = models.IntegerField(default=0, verbose_name='含目标')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='入库时间')
class Meta:
db_table = 'av_recording'
verbose_name = '录像分段'
verbose_name_plural = '录像分段'
indexes = [
models.Index(fields=['stream', 'start_time'], name='av_recording_st_idx'),
]
class LLMModel(models.Model):
"""大模型配置OpenAI 兼容 API"""
objects = ThreadSafetyManager()
user_id = models.IntegerField(verbose_name='用户')
sort = models.IntegerField(default=0, verbose_name='排序')
code = models.CharField(max_length=50, verbose_name='编号')
name = models.CharField(max_length=50, default='', verbose_name='名称')
model_name = models.CharField(max_length=200, verbose_name='模型名称')
api_url = models.CharField(max_length=500, verbose_name='API地址')
api_key = models.CharField(max_length=512, default='', verbose_name='API密钥')
timeout = models.IntegerField(default=30, verbose_name='超时时间(秒)')
inference_tool = models.CharField(max_length=100, default='OpenAI', verbose_name='推理工具')
remark = models.TextField(default='', verbose_name='备注')
state = models.IntegerField(default=1, verbose_name='状态') # 0=禁用 1=启用
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
last_update_time = models.DateTimeField(auto_now_add=True, verbose_name='更新时间')
def delete(self, using=None, keep_parents=False):
with g_dbLock:
ret = super(LLMModel, self).delete(using, keep_parents)
return ret
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
with g_dbLock:
ret = super(LLMModel, self).save(force_insert, force_update, using, update_fields)
return ret
class Meta:
db_table = 'av_llm'
verbose_name = '大模型'
verbose_name_plural = '大模型'
class LogModel(models.Model):
"""管理员操作日志"""
objects = ThreadSafetyManager()
user_id = models.IntegerField(verbose_name='用户ID')
log_type = models.IntegerField(verbose_name='日志类型') # 1:添加 2:编辑 3:删除 10:系统操作 100:系统重置
content = models.CharField(max_length=200, verbose_name='日志内容')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
state = models.IntegerField(verbose_name='状态') # 1:成功 0:失败
def __repr__(self):
return self.content
def __str__(self):
return self.content
def delete(self, using=None, keep_parents=False):
with g_dbLock:
ret = super(LogModel, self).delete(using, keep_parents)
return ret
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
with g_dbLock:
ret = super(LogModel, self).save(force_insert, force_update, using, update_fields)
return ret
class Meta:
db_table = 'av_log'
verbose_name = '管理员日志'
verbose_name_plural = '管理员日志'