Browse Source

模型独立进程一版,需要修改为动态设置模型进程能处理几通道数据

zfbox
张龙 6 months ago
parent
commit
362544aa6b
  1. 6
      config.yaml
  2. 46
      core/ACLModelManager.py
  3. 27
      core/ChannelData.py
  4. 2
      core/ChannelManager.py
  5. 3
      core/DataStruct.py
  6. 10
      core/ModelManager.py
  7. 214
      core/ModelNode.py
  8. 6
      core/WarnManager.py
  9. 54
      model/plugins/RYRQ_Model_ACL/RYRQ_Model_ACL.py
  10. 5
      web/API/channel.py
  11. 34
      web/API/viedo.py
  12. 69
      web/API/warn.py
  13. 1
      web/main/static/resources/scripts/aiortc-client-new.js
  14. 242
      web/main/static/resources/scripts/warn_manager.js
  15. 12
      web/main/templates/login.html
  16. 103
      web/main/templates/warn_manager.html
  17. BIN
      zfbox.db

6
config.yaml

@ -30,7 +30,7 @@ RTSP_Check_Time : 600 #10分钟 -- 2024-7-8 取消使用
#max_channel_num #max_channel_num
max_channel_num : 8 #最大视频通道数量 max_channel_num : 8 #最大视频通道数量
encode_param : 50 #无参数默认是95 encode_param : 80 #无参数默认是95
mywidth: 640 mywidth: 640
myheight: 480 myheight: 480
@ -43,7 +43,7 @@ cap_sleep_time: 120 #单位秒 -- 5分钟
buffer_len: 100 #分析后画面缓冲区帧数 -- 可以与验证帧率结合确定缓冲区大小 buffer_len: 100 #分析后画面缓冲区帧数 -- 可以与验证帧率结合确定缓冲区大小
RESET_INTERVAL : 100000 #帧数重置上限 RESET_INTERVAL : 100000 #帧数重置上限
frame_rate : 20 #帧率参考值 -- 后续作用主要基于verify_rate进行帧率控制 frame_rate : 20 #帧率参考值 -- 后续作用主要基于verify_rate进行帧率控制
verify_rate : 10 #验证帧率--- 也就是视频输出的帧率 verify_rate : 6 #验证帧率--- 也就是视频输出的帧率
warn_video_path: /mnt/zfbox/model/warn/ warn_video_path: /mnt/zfbox/model/warn/
warn_interval: 120 #报警间隔--单位秒 warn_interval: 120 #报警间隔--单位秒
video_error_count: 10 #单位秒 ---根据验证帧率,判断10秒内都是空帧的话,视频源链接有问题。 video_error_count: 10 #单位秒 ---根据验证帧率,判断10秒内都是空帧的话,视频源链接有问题。
@ -54,5 +54,5 @@ wired_interface : eth0
wireless_interface : WLAN wireless_interface : WLAN
#独立模型线程相关 #独立模型线程相关
workType : 1 # 1--一通道一线程。2--模型独立线程 workType : 2 # 1--一通道一线程。2--模型独立线程

46
core/ACLModelManager.py

@ -66,3 +66,49 @@ class ACLModeManger:
return True return True
@staticmethod
def pro_init_acl(device_id):
'''
独立进程初始化acl资源
:param device_id:
:return:
'''
# '''acl初始化函数'''
ret = acl.init() # 0-成功,其它失败
if ret:
raise RuntimeError(ret)
ret = acl.rt.set_device(device_id) # 指定当前进程或线程中用于运算的Device。可以进程或线程中指定。*多设备时可以放线程*
# 在某一进程中指定Device,该进程内的多个线程可共用此Device显式创建Context(acl.rt.create_context接口)。
if ret:
raise RuntimeError(ret)
print('ACL init Device Successfully')
#显示创建context
context, ret = acl.rt.create_context(device_id) # 显式创建一个Context
if ret:
raise RuntimeError(ret)
print('Init TH-Context Successfully')
return context
@staticmethod
def pro_del_acl(device_id,context):
'''
独立进程反初始化acl资源
:param device_id:
:param context:
:return:
'''
# 释放context
if context:
ret = acl.rt.destroy_context(context) # 释放 Context
if ret:
raise RuntimeError(ret)
#acl去初始化
ret = acl.rt.reset_device(device_id) # 释放Device
if ret:
raise RuntimeError(ret)
ret = acl.finalize() # 去初始化 0-成功,其它失败 --官方文档不建议放析构函数中执行
if ret:
raise RuntimeError(ret)
print('ACL finalize Successfully')
return True

27
core/ChannelData.py

@ -8,6 +8,7 @@ import cv2
import ffmpeg import ffmpeg
import subprocess import subprocess
import select import select
import multiprocessing
from collections import deque from collections import deque
from myutils.MyLogger_logger import LogHandler from myutils.MyLogger_logger import LogHandler
from core.CapManager import mCap from core.CapManager import mCap
@ -52,7 +53,8 @@ class ChannelData:
self.post_th = None #后处理线程句柄 self.post_th = None #后处理线程句柄
self.post_status = False #后处理线程状态 self.post_status = False #后处理线程状态
self.model_node= None #模型对象 -- inmq,outmq self.model_node= None #模型对象 -- inmq,outmq
self.out_mq = MyDeque(30) #放通道里面
self.out_mq = MyDeque(30) #分析结果存放MQ
#设置JPEG压缩基本 #设置JPEG压缩基本
self.encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), myCongif.get_data("encode_param")] # 50 是压缩质量(0到100) self.encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), myCongif.get_data("encode_param")] # 50 是压缩质量(0到100)
@ -279,9 +281,9 @@ class ChannelData:
if not self.cap: if not self.cap:
self.logger.error("采集线程未正常启动,不进行工作") self.logger.error("采集线程未正常启动,不进行工作")
return return
while self.model_node.model_th_status == 0: #避免模型没启动成功,模型线程在运行 while self.model_node.m_p_status.value == 0: #避免模型没启动成功,模型线程在运行
time.sleep(1) time.sleep(1)
if self.model_node.model_th_status == 1: if self.model_node.m_p_status.value == 1:
# 开始循环处理业务 # 开始循环处理业务
last_frame_time = time.time() # 初始化个读帧时间 last_frame_time = time.time() # 初始化个读帧时间
self.per_status = True self.per_status = True
@ -306,7 +308,7 @@ class ChannelData:
#图片预处理 #图片预处理
img,scale_ratio, pad_size = self.model_node.model.prework(frame) img,scale_ratio, pad_size = self.model_node.model.prework(frame)
indata = ModelinData(self.channel_id,img,frame,scale_ratio, pad_size) indata = ModelinData(self.channel_id,img,frame,scale_ratio, pad_size)
self.model_node.in_mq.myappend(indata) self.model_node.pro_add_data(indata) #数据入队列
else:# 不在计划则不进行验证,直接返回图片 --存在问题是:result 漏数据 else:# 不在计划则不进行验证,直接返回图片 --存在问题是:result 漏数据
ret, frame_bgr_webp = cv2.imencode('.jpg', frame,self.encode_param) ret, frame_bgr_webp = cv2.imencode('.jpg', frame,self.encode_param)
if not ret: if not ret:
@ -341,10 +343,12 @@ class ChannelData:
else: # 没有产生报警也需要记录,统一计算占比 else: # 没有产生报警也需要记录,统一计算占比
result.append(0) result.append(0)
#分析画面保存 #分析画面保存
ret, frame_bgr_webp = cv2.imencode('.jpg', out_data.image,self.encode_param) # ret, frame_bgr_webp = cv2.imencode('.jpg', out_data.image,self.encode_param)
buffer_bgr_webp = None # buffer_bgr_webp = None
if ret: # if ret:
buffer_bgr_webp = frame_bgr_webp.tobytes() # buffer_bgr_webp = frame_bgr_webp.tobytes()
ret, buffer_bgr_webp = self._encode_frame(out_data.image)
# 分析图片放入缓冲区内存中 # 分析图片放入缓冲区内存中
self.add_deque(out_data.image) # 缓冲区大小由maxlen控制 超上限后,删除最前的数据 self.add_deque(out_data.image) # 缓冲区大小由maxlen控制 超上限后,删除最前的数据
# 分析画面一直更新最新帧,提供网页端显示 # 分析画面一直更新最新帧,提供网页端显示
@ -522,6 +526,7 @@ class ChannelData:
print("线程结束!!!!") print("线程结束!!!!")
#2024-9-9 新增兼容独立model线程 根据self.model_node判断,None:1通道1线程,not None:独立线程 #2024-9-9 新增兼容独立model线程 根据self.model_node判断,None:1通道1线程,not None:独立线程
#2024-10-14 model调整为独立子进程执行
def _start_model_th(self,model_data,schedule,type=1): def _start_model_th(self,model_data,schedule,type=1):
verify_rate = myCongif.get_data("verify_rate") verify_rate = myCongif.get_data("verify_rate")
warn_interval = myCongif.get_data("warn_interval") warn_interval = myCongif.get_data("warn_interval")
@ -532,8 +537,10 @@ class ChannelData:
args=(model_data[3],model_data[4],verify_rate,warn_interval,model_data[7], args=(model_data[3],model_data[4],verify_rate,warn_interval,model_data[7],
model_data[1],model_data[2],model_data[8],model_data[9])) model_data[1],model_data[2],model_data[8],model_data[9]))
self.post_th.start() self.post_th.start()
#启动模型线程,若线程已启动,则+1 mq
# 启动模型线程,若线程已启动,则+1
self.model_node.start_model_th(self.channel_id,self.out_mq) self.model_node.start_model_th(self.channel_id,self.out_mq)
#启动预处理线程 #启动预处理线程
self.per_th = threading.Thread(target=self._pre_work_th,args=(schedule,)) self.per_th = threading.Thread(target=self._pre_work_th,args=(schedule,))
self.per_th.start() self.per_th.start()
@ -557,7 +564,9 @@ class ChannelData:
if self.post_th: if self.post_th:
self.post_th.join() self.post_th.join()
self.post_th = None self.post_th = None
#清空MQ
self.out_mq.myclear()#清空后处理mq中未处理的数据 self.out_mq.myclear()#清空后处理mq中未处理的数据
else: else:
if self.work_th: if self.work_th:
if self.b_model: if self.b_model:

2
core/ChannelManager.py

@ -131,12 +131,14 @@ class ChannelManager:
return img_base64 return img_base64
'''模型独立线程修改2024-9-9,要求是双模式兼容''' '''模型独立线程修改2024-9-9,要求是双模式兼容'''
'''2024-10-13修改独立线程为独立进程---acl初始化需要在子进程中初始化 -- 该方案无法兼容旧版本'''
def CreateModelNode(self, model_id, model_path, channel_id): def CreateModelNode(self, model_id, model_path, channel_id):
if model_id in self.model_list: if model_id in self.model_list:
modelN = self.model_list[model_id] modelN = self.model_list[model_id]
else: else:
modelN = ModelNode(self.device_id,model_path) modelN = ModelNode(self.device_id,model_path)
self.model_list[model_id] = modelN self.model_list[model_id] = modelN
#modelN = ModelNode(self.device_id, model_path,channel_id)
return modelN return modelN
def delModelNode(self): #关于modelnodel :1.考虑modelnode是否可以不删除,清空inmq即可,2.mdel_list是否需要加锁。#? def delModelNode(self): #关于modelnodel :1.考虑modelnode是否可以不删除,清空inmq即可,2.mdel_list是否需要加锁。#?

3
core/DataStruct.py

@ -34,11 +34,12 @@ class ModelinData:
pass pass
class ModeloutData: class ModeloutData:
def __init__(self,image,scale_ratio, pad_size,outputs): def __init__(self,image,scale_ratio, pad_size,outputs,channel_id):
self.image = image #原图 self.image = image #原图
self.outputs = outputs #模型推理后结果 self.outputs = outputs #模型推理后结果
self.scale_ratio = scale_ratio self.scale_ratio = scale_ratio
self.pad_size = pad_size self.pad_size = pad_size
self.channel_id = channel_id
def __del__(self): def __del__(self):
pass pass

10
core/ModelManager.py

@ -21,16 +21,18 @@ class ModelManager:
# acl初始化 -- 一个进程一个 # acl初始化 -- 一个进程一个
self.model_platform = myCongif.get_data("model_platform") self.model_platform = myCongif.get_data("model_platform")
if self.model_platform == "acl": if self.model_platform == "acl":
self.device_id = myCongif.get_data("device_id") if myCongif.get_data("workType") == 1:
ACLModeManger.init_acl(self.device_id) #acl -- 全程序初始化 self.device_id = myCongif.get_data("device_id")
#self.model_dic = {} # model_id model ACLModeManger.init_acl(self.device_id) #acl -- 全程序初始化
self.model_dic = {} # model_id model
def __del__(self): def __del__(self):
self.logger.debug("释放资源") self.logger.debug("释放资源")
self.stop_work(0) #停止所有工作 self.stop_work(0) #停止所有工作
del self.verify_list #应该需要深入的删除--待完善 del self.verify_list #应该需要深入的删除--待完善
if self.model_platform == "acl": #去初始化 if self.model_platform == "acl": #去初始化
ACLModeManger.del_acl(self.device_id) #acl -- 全程序反初始化 需要确保在执行析构前,其它资源已释放 if myCongif.get_data("workType") == 1:
ACLModeManger.del_acl(self.device_id) #acl -- 全程序反初始化 需要确保在执行析构前,其它资源已释放
def send_warn(self): def send_warn(self):
'''发送报警信息''' '''发送报警信息'''

214
core/ModelNode.py

@ -1,38 +1,104 @@
import threading import threading
import importlib.util import importlib.util
import time import time
from myutils.MyDeque import MyDeque import multiprocessing
from multiprocessing.managers import BaseManager
from myutils.ConfigManager import myCongif from myutils.ConfigManager import myCongif
from myutils.MyLogger_logger import LogHandler from myutils.MyLogger_logger import LogHandler
from core.ACLModelManager import ACLModeManger from core.ACLModelManager import ACLModeManger
from core.DataStruct import ModelinData,ModeloutData from core.DataStruct import ModelinData,ModeloutData
from threading import Lock from threading import Lock
#2024-10-14model处理调整为独立子进程
def model_process(device,model,model_platform,m_p_status,brun,in_mq,out_mq):
# 初始化模型运行资源
context = None
if model_platform == "acl": # ACL线程中初始化内容
context = ACLModeManger.pro_init_acl(device) # 初始化acl资源,并创建context
# 初始化模型资源 -- 加载模型文件
ret = model.init_acl_resource() # 加载和初始化离线模型文件--om文件
if not ret:
print("初始化模型资源出错,退出线程!")
m_p_status.value = 2
return
#执行工作
m_p_status.value = 1
s_time = time.time()
icount = 0
while brun.value:
try:
inData = in_mq.get(timeout=0.01) #空时-block,直到有值 #(self,channel_id,img,image,scale_ratio, pad_size):
except:
#print("in_mq_空")
continue
if inData:
outputs = model.execute([inData.img,])#创建input,执行模型,返回结果 --失败返回None
outdata = ModeloutData(inData.image,inData.scale_ratio,inData.pad_size,outputs,inData.channel_id)
del inData.img
#结果输出
if out_mq.full():
tmp = out_mq.get()
#print("model_输出mq满!")
del tmp
out_mq.put(outdata) # 需要确保out_mq只有在这里put
else: #正常情况不会执行到该条件
time.sleep(0.05)
icount += 1
if icount == 1000:
e_time = time.time()
use_time = (e_time - s_time) / 1000
print(f"model_process耗时--{use_time}")
s_time = time.time()
icount = 0
#结束进程,释放资源
m_p_status.value = 0
while not in_mq.empty():
try:
in_mq.get_nowait() # Get without blocking
except Exception as e:
break # In case of any unexpected errors
# 反初始化
if model_platform == "acl":
try:
model.release() # 释放模型资源资源
# 删除模型对象
del model
# 释放ACL资源
ACLModeManger.pro_del_acl(device,context)
except Exception as e:
print(e)
class ModelNode: class ModelNode:
def __init__(self,device,model_path): def __init__(self,device,model_path,channel_id):
self.device = device self.device = device
self.model_path = model_path self.model_path = model_path
self.channel_id = channel_id
self.model = None #模型对象 self.model = None #模型对象
self.model_th = None #模型线程句柄
self.brun = True #模型控制标识
self.model_th_status = 0 #模型线程运行状态 0--初始状态,1-线程执行成功,2-线程退出
self.in_mq = MyDeque(50) #
self.channel_list = {} #channel_id out_mq --需要线程安全
self.clist_Lock = Lock() #channel_list的维护锁
self.ch_count = 0 #关联启动的通道数量 self.ch_count = 0 #关联启动的通道数量
self.count_Lock = Lock() #count的维护锁 self.count_Lock = Lock() #count的维护锁
self.model_platform = myCongif.get_data("model_platform") self.model_platform = myCongif.get_data("model_platform")
self.logger = LogHandler().get_logger("ModelNode") self.logger = LogHandler().get_logger("ModelNode")
#分发线程相关
self.model_out_th = None
self.channel_dict = {}
self.cdict_Lock = Lock()
#独立进程方案--共享参数
self.process = None
self.in_mq = multiprocessing.Queue(maxsize=30)
self.out_mq = multiprocessing.Queue(maxsize=30) #调整结构,多线程(预处理)-》in_mq-子进程-out_mq-》线程分发outdata->多线程(后处理)
self.brun = multiprocessing.Value('b',True) #brun.value = False,brun.value = True
self.m_p_status = multiprocessing.Value('i',0)
def __del__(self): def __del__(self):
pass pass
def _reset(self): #重置数据
#self.model_th_status = 0 # 模型线程运行状态 0--初始状态,1-线程执行成功,2-线程退出
self.in_mq.myclear()
def _import_model(self,model_path,threshold=0.5,iou_thres=0.5): def _import_model(self,model_path,threshold=0.5,iou_thres=0.5):
''' '''
根据路径动态导入模块 根据路径动态导入模块
@ -61,73 +127,79 @@ class ModelNode:
print(f"An unexpected error occurred: {e}") print(f"An unexpected error occurred: {e}")
return None return None
def _model_th(self): def pro_add_data(self,data):
# 加载自定义模型文件 # try:
self.model = self._import_model(self.model_path) # 动态加载模型处理文件py # self.in_mq.put(data,timeout=0.1)
if not self.model: # except multiprocessing.queues.Full:
self.logger.error("自定义模型文件加载失败,退出model线程") # print("mdel_inmq输入满!")
self.model_th_status = 2 # del data
return if self.in_mq.full():
# 初始化模型运行资源 tmp = self.in_mq.get()
context = None #print("mdel_inmq输入满!")
if self.model_platform == "acl": # ACL线程中初始化内容 del tmp
context = ACLModeManger.th_inti_acl(self.device) # 创建context self.in_mq.put(data) # 需要确保out_mq只有在这里put
# 初始化模型资源 -- 加载模型文件
ret = self.model.init_acl_resource() # 加载和初始化离线模型文件--om文件
if not ret:
print("初始化模型资源出错,退出线程!")
self.model_th_status = 2
return
#执行工作
self.model_th_status = 1
while self.brun:
inData = self.in_mq.mypopleft() #空时,返回None #(self,channel_id,img,image,scale_ratio, pad_size):
if inData:
outputs = self.model.execute([inData.img,])#创建input,执行模型,返回结果 --失败返回None
outdata = ModeloutData(inData.image,inData.scale_ratio,inData.pad_size,outputs)
del inData.img
with self.clist_Lock:
if inData.channel_id in self.channel_list:
self.channel_list[inData.channel_id].myappend(outdata)
else:
time.sleep(0.05)
#结束线程,释放资源 def _modle_th(self):
self.model_th_status = 0 '''根据channel_id分发out_data到out_mq'''
self._reset() s_time = time.time()
# 反初始化 icount = 0
if self.model_platform == "acl": while self.brun.value:
try: try:
self.model.release() # 释放模型资源资源 outdata = self.out_mq.get(timeout=1)
# 删除模型对象 except:
del self.model continue
# 释放context with self.cdict_Lock:
if context: # ACL线程中反初始化内容 -- 若线程异常退出,这些资源就不能正常释放了 if outdata.channel_id in self.channel_dict:
# 再释放context self.channel_dict[outdata.channel_id].myappend(outdata) #后面就交给后处理线程了
ACLModeManger.th_del_acl(context) icount += 1
except Exception as e: if icount ==1000:
print(e) e_time = time.time()
use_time = (e_time-s_time) /1000
print(f"{self.channel_id}_modle_th耗时--{use_time}")
s_time = time.time()
icount = 0
#2024-10-14调整为独立进程执行 -- 一个线程一个MQ MyDeque
def start_model_th(self,channel_id,out_mq): def start_model_th(self,channel_id,out_mq):
with self.count_Lock: with self.count_Lock:
with self.clist_Lock: with self.cdict_Lock:
if channel_id in self.channel_list: if channel_id in self.channel_dict:
return #这个可以删除老的,新增新的 return #这个可以删除老的,新增新的--后续验证,若需要则进行修改
self.channel_list[channel_id] = out_mq self.channel_dict[channel_id] = out_mq #增加一个记录
if self.ch_count == 0: #第一次启动线程
self.brun = True if self.ch_count == 0: #第一次启动--需要启动处理线程和进程
self.model_th = threading.Thread(target=self._model_th) #加载自定义模型文件
self.model_th.start() self.model = self._import_model(self.model_path) # 动态加载模型处理文件py --置信阈值一直没使用
if not self.model:
self.logger.error("自定义模型文件加载失败,不启动model子进程")
self.m_p_status.value = 2
return
self.brun.value = True
#创建outMQ的分发线程
self.model_out_th = threading.Thread(target=self._modle_th)
self.model_out_th.start()
# 创建子进程
self.process = multiprocessing.Process(target=model_process,
args=(self.device,self.model,self.model_platform,
self.m_p_status,self.brun,self.in_mq,self.out_mq))
self.process.start()
self.ch_count += 1 #有通道调用一次就加一 self.ch_count += 1 #有通道调用一次就加一
def stop_model_th(self,channel_id): def stop_model_th(self,channel_id):
with self.count_Lock: with self.count_Lock:
with self.clist_Lock: with self.cdict_Lock:
if channel_id in self.channel_list: if channel_id in self.channel_dict:
del self.channel_list[channel_id] del self.channel_dict[channel_id]
self.ch_count -= 1 self.ch_count -= 1
if self.ch_count == 0: #所有通道结束 if self.ch_count == 0: #所有通道结束
self.brun = False self.brun.value = False
self.model_th.join() self.model_out_th.join() #等待线程结束
self.model_th = None self.model_out_th = None
self.process.join() #等待子进程结束
self.process = None

6
core/WarnManager.py

@ -42,7 +42,7 @@ class WarnManager:
except queue.Empty: except queue.Empty:
continue continue
if warn_data: if warn_data:
self.save_warn(warn_data.model_name,warn_data.img_buffer,warn_data.width,warn_data.height, ret = self.save_warn(warn_data.model_name,warn_data.img_buffer,warn_data.width,warn_data.height,
warn_data.channel_id,self.FPS,self.fourcc,myDBM) warn_data.channel_id,self.FPS,self.fourcc,myDBM)
self.send_warn() self.send_warn()
del warn_data.img_buffer del warn_data.img_buffer
@ -80,7 +80,9 @@ class WarnManager:
filename = f"{channnel_id}_{current_time_str}" filename = f"{channnel_id}_{current_time_str}"
save_path = myCongif.get_data("warn_video_path") save_path = myCongif.get_data("warn_video_path")
# 保存视频 # 保存视频
video_writer = cv2.VideoWriter(f"{save_path}{filename}.mp4", fourcc, FPS, (width, height)) str_video = f"{save_path}{filename}.mp4"
video_writer = cv2.VideoWriter(str_video, fourcc, FPS, (width, height))
#print(f"File: {str_video}, FourCC: {fourcc}, FPS: {FPS}, Size: ({width}, {height})")
if not video_writer.isOpened(): if not video_writer.isOpened():
print(f"Failed to open video writer for model/warn/{filename}.mp4") print(f"Failed to open video writer for model/warn/{filename}.mp4")
return False return False

54
model/plugins/RYRQ_Model_ACL/RYRQ_Model_ACL.py

@ -29,10 +29,10 @@ class Model(ModelBase):
def prework(self,image): def prework(self,image):
'''模型输入图片数据前处理 --- 针对每个模型特有的预处理内容 -''' '''模型输入图片数据前处理 --- 针对每个模型特有的预处理内容 -'''
img, scale_ratio, pad_size = letterbox(image, new_shape=[640, 640]) # 对图像进行缩放与填充 img, scale_ratio, dw,dh = letterbox(image, new_shape=[self.netw, self.neth]) # 对图像进行缩放与填充
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW #图片在输入时已经做了转换 img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW #图片在输入时已经做了转换
img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 # 转换为内存连续存储的数组 img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 # 转换为内存连续存储的数组
return img,scale_ratio, pad_size return img,scale_ratio, (dw,dh)
def postwork(self,image,outputs,scale_ratio,pad_size,check_area,polygon,conf_threshold,iou_thres): def postwork(self,image,outputs,scale_ratio,pad_size,check_area,polygon,conf_threshold,iou_thres):
''' '''
@ -47,6 +47,7 @@ class Model(ModelBase):
:param iou_thres: :param iou_thres:
:return: :return:
''' '''
filtered_pred_all = None
bwarn = False bwarn = False
warn_text = "" warn_text = ""
# 是否有检测区域,有先绘制检测区域 由于在该函数生成了polygon对象,所有需要在检测区域前调用。 # 是否有检测区域,有先绘制检测区域 由于在该函数生成了polygon对象,所有需要在检测区域前调用。
@ -54,40 +55,31 @@ class Model(ModelBase):
self.draw_polygon(image, polygon, (255, 0, 0)) self.draw_polygon(image, polygon, (255, 0, 0))
if outputs: if outputs:
output = outputs[0] # 只放了一张图片 -- #是否能批量验证? output = np.squeeze(outputs[0]) # 移除张量为1的维度 --暂时不明白其具体意义
# 后处理 -- boxout 是 tensor-list: [tensor([[],[].[]])] --[x1,y1,x2,y2,置信度,coco_index] dw,dh = pad_size
# 利用非极大值抑制处理模型输出,conf_thres 为置信度阈值,iou_thres 为iou阈值 pred_all = non_max_suppression_v10(output, self.conf_threshold, scale_ratio, dw, dh)
output_torch = torch.tensor(output) for xmin, ymin, xmax, ymax, confidence, label in pred_all:
boxout = nms(output_torch, conf_thres=conf_threshold, iou_thres=iou_thres)
del output_torch
pred_all = boxout[0].numpy() # 转换为numpy数组 -- [[],[],[]] --[x1,y1,x2,y2,置信度,coco_index]
# pred_all[:, :4] 取所有行的前4列,pred_all[:,1]--第一列
scale_coords([640, 640], pred_all[:, :4], image.shape, ratio_pad=(scale_ratio, pad_size)) # 将推理结果缩放到原始图片大小
# 过滤掉不是目标标签的数据 -- 序号0-- person self.labels_dict --这个考虑下是否可以放到nms前面#?
filtered_pred_all = pred_all[pred_all[:, 5] == 0]
# 绘制检测结果 --- 也需要封装在类里,
for pred in filtered_pred_all:
x1, y1, x2, y2 = int(pred[0]), int(pred[1]), int(pred[2]), int(pred[3])
# # 绘制目标识别的锚框 --已经在draw_bbox里处理 # # 绘制目标识别的锚框 --已经在draw_bbox里处理
# cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2) # cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
if check_area == 1: # 指定了检测区域 draw_box(image, [xmin, ymin, xmax, ymax], confidence, label) # 画出检测框、类别、概率
x_center = (x1 + x2) / 2 if label == 0: # person
y_center = (y1 + y2) / 2 # 判断是否产生告警
# 绘制中心点? x1, y1, x2, y2 = int(xmin), int(ymin), int(xmax), int(ymax)
cv2.circle(image, (int(x_center), int(y_center)), 5, (0, 0, 255), -1) if check_area == 1: # 指定了检测区域
# 判断是否区域点 x_center = (x1 + x2) / 2
if not self.is_point_in_region((x_center, y_center)): y_center = (y1 + y2) / 2
continue # 没产生报警-继续 # 绘制中心点?
# 产生报警 -- 有一个符合即可 cv2.circle(image, (int(x_center), int(y_center)), 5, (0, 0, 255), -1)
bwarn = True # 判断是否区域点
warn_text = "People Intruder detected!" if not self.is_point_in_region((x_center, y_center)):
draw_bbox(filtered_pred_all, image, (0, 255, 0), 2, self.labels_dict) # 画出检测框、类别、概率 continue # 没产生报警-继续
# 产生报警 -- 有一个符合即可
bwarn = True
warn_text = "People Intruder detected!"
# 清理内存 # 清理内存
del outputs, output del outputs, output
del boxout
del pred_all, filtered_pred_all del pred_all, filtered_pred_all
#图片绘制是在原图生效 image # cv2.imwrite('img_res.png', img_dw)
return bwarn, warn_text return bwarn, warn_text
def verify(self,image,data,isdraw=1): def verify(self,image,data,isdraw=1):

5
web/API/channel.py

@ -52,11 +52,12 @@ async def channel_add(): #新增通道 -- 2024-8-1修改为与修改通道用一
area = mDBM.do_select(strsql,1) area = mDBM.do_select(strsql,1)
if area: if area:
area_id = area[0] area_id = area[0]
strsql = f"select ID from channel where area_id={area_id} and channel_name='{cName}';" #strsql = f"select ID from channel where area_id={area_id} and channel_name='{cName}';"
strsql = f"select ID from channel where channel_name='{cName}';"
data = mDBM.do_select(strsql, 1) data = mDBM.do_select(strsql, 1)
if data and data[0] != cid: #有值--代表重复 或者是它自己(只修改了RTSP地址) if data and data[0] != cid: #有值--代表重复 或者是它自己(只修改了RTSP地址)
reStatus = 0 reStatus = 0
reMsg = "同一区域内的通道名称不能相同!" reMsg = "通道名称不能相同!"
else: else:
if cid == -1: if cid == -1:
max_count = myCongif.get_data("max_channel_num") max_count = myCongif.get_data("max_channel_num")

34
web/API/viedo.py

@ -201,24 +201,22 @@ async def handle_channel(channel_id,websocket):
await asyncio.sleep(sleep_time) # 等待视频重连时间 await asyncio.sleep(sleep_time) # 等待视频重连时间
#----------输出时间----------- #----------输出时间-----------
frame_count += 1 # frame_count += 1
end_time = time.time() # end_time = time.time()
# 计算时间差 # # 计算时间差
el_time = end_time - start_time # el_time = end_time - start_time
all_time = all_time + (end_time - current_time) # all_time = all_time + (end_time - current_time)
# 每隔一定时间(比如5秒)计算一次帧率 # # 每隔一定时间(比如5秒)计算一次帧率
if el_time >= 10: # if el_time >= 10:
fps = frame_count / el_time # fps = frame_count / el_time
print(f"{channel_id}当前帧率: {fps} FPS,循环次数:{frame_count},花费总耗时:{all_time}S,get耗时:{get_all_time},send耗时:{send_all_time}") # print(f"{channel_id}当前帧率: {fps} FPS,循环次数:{frame_count},花费总耗时:{all_time}S,get耗时:{get_all_time},send耗时:{send_all_time}")
# 重置计数器和时间 # # 重置计数器和时间
frame_count = 0 # frame_count = 0
all_time = 0 # all_time = 0
get_all_time = 0 # get_all_time = 0
send_all_time = 0 # send_all_time = 0
start_time = time.time() # start_time = time.time()
# print(f"get_frame:{round(get_etime-get_stime,5)}Sceond;"
# f"send_frame:{round(send_etime-send_stime,5)}Sceond;"
# f"All_time={round(end_time-current_time,5)}")
except asyncio.CancelledError: except asyncio.CancelledError:
print(f"WebSocket connection for channel {channel_id} closed by client") print(f"WebSocket connection for channel {channel_id} closed by client")
raise raise

69
web/API/warn.py

@ -1,32 +1,33 @@
import os
from . import api from . import api
from web.common.utils import login_required from web.common.utils import login_required
from quart import jsonify, request from quart import jsonify, request,send_file
from core.DBManager import mDBM from core.DBManager import mDBM
@api.route('/warn/search_warn',methods=['POST']) @api.route('/warn/search_warn',methods=['POST'])
@login_required @login_required
async def warn_get(): #新增算法 async def warn_search(): #查询报警
#获取查询参数 #获取查询参数
json_data = await request.get_json() json_data = await request.get_json()
s_count = json_data.get('s_count','') s_count = json_data.get('s_count','')
e_count = json_data.get('e_count','') e_count = json_data.get('e_count','')
model_name = json_data.get('model_name','') model_name = json_data.get('model_name','')
channel_id = json_data.get('channel_id','') channel_name = json_data.get('channel_name','')
start_time = json_data.get('start_time','') start_time = json_data.get('start_time','')
end_time = json_data.get('end_time','') end_time = json_data.get('end_time','')
# 动态拼接 SQL 语句 # 动态拼接 SQL 语句
sql = "SELECT * FROM warn WHERE 1=1" sql = "SELECT t1.*,t2.channel_name FROM warn t1 LEFT JOIN channel t2 ON t1.channel_id = t2.element_id WHERE 1=1"
if model_name: if model_name:
sql += f" AND model_name = {model_name}" sql += f" AND t1.model_name = {model_name}"
if channel_id: if channel_name:
sql += f" AND channel_id = {channel_id}" sql += f" AND t2.channel_name = {channel_name}"
if start_time and end_time: if start_time and end_time:
sql += f" AND creat_time BETWEEN {start_time} AND {end_time}" sql += f" AND t1.creat_time BETWEEN {start_time} AND {end_time}"
# 增加倒序排列和分页 # 增加倒序排列和分页
sql += f" ORDER BY creat_time DESC LIMIT {e_count} OFFSET {s_count}" sql += f" ORDER BY t1.creat_time DESC LIMIT {e_count} OFFSET {s_count}"
# 使用SQLAlchemy执行查询 # 使用SQLAlchemy执行查询
try: try:
@ -34,7 +35,55 @@ async def warn_get(): #新增算法
data = mDBM.do_select(sql) data = mDBM.do_select(sql)
# 将数据转换为JSON格式返回给前端 # 将数据转换为JSON格式返回给前端
warn_list = [{"ID": warn[0], "model_name": warn[1], "video_path": warn[2], "img_path": warn[3], warn_list = [{"ID": warn[0], "model_name": warn[1], "video_path": warn[2], "img_path": warn[3],
"creat_time": warn[4], "channel_id": warn[5]} for warn in data] "creat_time": warn[4], "channel_name": warn[6]} for warn in data]
return jsonify(warn_list) return jsonify(warn_list)
except Exception as e: except Exception as e:
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@api.route('/warn/warn_img')
@login_required
async def warn_img():
# 获取图片路径参数
image_path = request.args.get('path')
if image_path and os.path.exists(image_path):
# 返回图片文件
return await send_file(image_path)
else:
# 图片不存在时,返回 404
return 'Image not found', 404
@api.route('/warn/warn_video')
@login_required
async def warn_video():
# 获取视频路径参数
video_path = request.args.get('path')
if video_path and os.path.exists(video_path):
# 返回视频文件流
return await send_file(video_path, as_attachment=True)
else:
# 视频文件不存在时,返回 404
return 'Video not found', 404
@api.route('/warn/warn_del',methods=['POST'])
@login_required
async def warn_del():
# 获取请求体中的报警ID数组
data = await request.get_json()
alarm_ids = data.get('alarmIds')
if not alarm_ids:
return jsonify({'success': False, 'message': '没有提供报警ID'}), 400
try:
# 根据报警ID进行删除数据的操作,这里是假设数据库删除操作
# 例如:delete_from_database(alarm_ids)
# 模拟删除成功
success = True
if success:
return jsonify({'success': True, 'message': '删除成功'})
else:
return jsonify({'success': False, 'message': '删除失败'})
except Exception as e:
return jsonify({'success': False, 'message': f'发生异常: {str(e)}'}), 500

1
web/main/static/resources/scripts/aiortc-client-new.js

@ -294,6 +294,7 @@ function connect(channel_id,element_id,imgcanvas,ctx,offscreenCtx,offscreenCanva
//如有错误信息显示 -- 清除错误信息 //如有错误信息显示 -- 清除错误信息
if(berror_state_list[el_id]){ if(berror_state_list[el_id]){
removeErrorMessage(imgcanvas); removeErrorMessage(imgcanvas);
console.log("清除错误信息!")
berror_state_list[el_id] = false; berror_state_list[el_id] = false;
} }
// 接收到 JPG 图像数据,转换为 Blob // 接收到 JPG 图像数据,转换为 Blob

242
web/main/static/resources/scripts/warn_manager.js

@ -1,23 +1,74 @@
let modelMap = []; //model_name model_id let modelMap = []; //model_name model_id
let channelMap = []; //channel_name channel_id let channelMap = []; //channel_name channel_id
let warn_data = []; //查询到的报警数据
let page_data = [];
let currentEditingRow = null;
let currentPage = 1;
const rowsPerPage = 30;
//页面加载初始化 //页面加载初始化
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
perWarnHtml() perWarnHtml()
document.getElementById('delPageButton').addEventListener('click', function () {
delpageWarn();
});
}); });
//搜索按钮点击 //搜索按钮点击
document.getElementById('searchMButton').addEventListener('click', function() { document.getElementById('searchMButton').addEventListener('click', function() {
shearchWarn();
});
//查询数据
async function shearchWarn(){
//查询告警数据
let modelName = document.getElementById('modelSelect').value;
let channelName = document.getElementById('channelSelect').value;
const startTime = document.getElementById('startTime').value; const startTime = document.getElementById('startTime').value;
const endTime = document.getElementById('endTime').value; const endTime = document.getElementById('endTime').value;
const sCount = 0; // 起始记录数从0开始
const eCount = 5000; // 每页显示10条记录
if (startTime && endTime) { if(modelName == "请选择"){
console.log(`开始时间: ${startTime}, 结束时间: ${endTime}`); modelName = "";
// 在这里执行其他逻辑,例如根据时间范围查询数据 }
} else { if(channelName == "请选择"){
alert('请选择完整的时间区间'); channelName = "";
} }
});
// 构造请求体
const requestData = {
model_name: modelName || "", // 如果为空,则传空字符串
channel_name: channelName || "",
start_time: startTime || "",
end_time: endTime || "",
s_count: sCount,
e_count: eCount
};
try{
// 发送POST请求到后端
const response = await fetch('/api/warn/search_warn', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData) // 将数据转为JSON字符串
});
// 检查响应是否成功
if (response.ok) {
warn_data = await response.json();
// 在这里处理查询结果,比如更新表格显示数据
currentPage = 1; // 重置当前页为第一页
renderTable(); //刷新表格
renderPagination();
} else {
console.error('查询失败:', response.status);
}
} catch (error) {
console.error('请求出错:', error);
}
}
async function perWarnHtml() { async function perWarnHtml() {
//获取算法和通道列表,在下拉框显示 //获取算法和通道列表,在下拉框显示
@ -47,104 +98,50 @@ async function perWarnHtml() {
channelMap[option.channel_name] = option.ID; channelMap[option.channel_name] = option.ID;
}); });
set_select_data("channelSelect",channel_select_datas); set_select_data("channelSelect",channel_select_datas);
//查询数据
//查询告警数据 shearchWarn()
let modelName = document.getElementById('modelSelect').value;
let channelId = document.getElementById('channelSelect').value;
const startTime = document.getElementById('startTime').value;
const endTime = document.getElementById('endTime').value;
const sCount = 0; // 起始记录数从0开始
const eCount = 100; // 每页显示10条记录
if(modelName == "请选择"){
modelName = "";
}
if(channelId == "请选择"){
channelId = "";
}
// 构造请求体
const requestData = {
model_name: modelName || "", // 如果为空,则传空字符串
channel_id: channelId || "",
start_time: startTime || "",
end_time: endTime || "",
s_count: sCount,
e_count: eCount
};
try{
// 发送POST请求到后端
const response = await fetch('/api/warn/search_warn', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData) // 将数据转为JSON字符串
});
// 检查响应是否成功
if (response.ok) {
const data = await response.json();
console.log('查询结果:', data);
// 在这里处理查询结果,比如更新表格显示数据
//updateTableWithData(data);
} else {
console.error('查询失败:', response.status);
}
} catch (error) {
console.error('请求出错:', error);
}
}catch (error) { }catch (error) {
console.error('Error fetching model data:', error); console.error('Error fetching model data:', error);
} }
//读取报警数据并进行显示--要分页显示
// modelData_bak = modelData;
// currentPage = 1; // 重置当前页为第一页
// renderTable(); //刷新表格
// renderPagination();
//操作-删除,图片,视频,审核(灰)
} }
//刷新表单页面数据 //刷新表单页面数据
function renderTable() { function renderTable() {
const tableBody = document.getElementById('table-body-model'); const tableBody = document.getElementById('table-body-warn');
tableBody.innerHTML = ''; //清空 tableBody.innerHTML = ''; //清空
const start = (currentPage - 1) * rowsPerPage; const start = (currentPage - 1) * rowsPerPage;
const end = start + rowsPerPage; const end = start + rowsPerPage;
const pageData = modelData.slice(start, end); pageData = warn_data.slice(start, end);
const surplus_count = rowsPerPage - pageData.length; const surplus_count = rowsPerPage - pageData.length;
pageData.forEach((model) => { pageData.forEach((warn) => {
const row = document.createElement('tr'); const row = document.createElement('tr');
row.innerHTML = ` row.innerHTML = `
<td>${model.ID}</td> <td>${warn.ID}</td>
<td>${model.name}</td> <td>${warn.model_name}</td>
<td>${model.version}</td> <td>${warn.channel_name}</td>
<td>${model.duration_time}</td> <td>${warn.creat_time}</td>
<td>${model.proportion}</td>
<td> <td>
<button class="btn btn-primary btn-sm modify-btn">升级</button> <button class="btn btn-primary btn-sm warn-show-btn">查看</button>
<button class="btn btn-secondary btn-sm algorithm-btn">配置</button> <button class="btn btn-secondary btn-sm warn-video-btn">视频</button>
<button class="btn btn-danger btn-sm delete-btn">删除</button> <button class="btn btn-danger btn-sm warn-delete-btn">删除</button>
</td> </td>
`; `;
tableBody.appendChild(row); tableBody.appendChild(row);
row.querySelector('.modify-btn').addEventListener('click', () => modifyModel(row)); row.querySelector('.warn-show-btn').addEventListener('click', () => showWarn(row));
row.querySelector('.algorithm-btn').addEventListener('click', () => configureModel(row)); row.querySelector('.warn-video-btn').addEventListener('click', () => videoWarn(row));
row.querySelector('.delete-btn').addEventListener('click', () => deleteModel(row)); row.querySelector('.warn-delete-btn').addEventListener('click', () => deleteWarn(row));
}); });
} }
//刷新分页标签 //刷新分页标签
function renderPagination() { function renderPagination() {
const pagination = document.getElementById('pagination-model'); const pagination = document.getElementById('pagination-warn');
pagination.innerHTML = ''; pagination.innerHTML = '';
const totalPages = Math.ceil(modelData.length / rowsPerPage); const totalPages = Math.ceil(warn_data.length / rowsPerPage);
for (let i = 1; i <= totalPages; i++) { for (let i = 1; i <= totalPages; i++) {
const pageItem = document.createElement('li'); const pageItem = document.createElement('li');
pageItem.className = 'page-item' + (i === currentPage ? ' active' : ''); pageItem.className = 'page-item' + (i === currentPage ? ' active' : '');
@ -158,3 +155,92 @@ function renderPagination() {
pagination.appendChild(pageItem); pagination.appendChild(pageItem);
} }
} }
//显示报警信息详情
function showWarn(row){
currentEditingRow = row;
model_name = row.cells[1].innerText;
channel_name = row.cells[2].innerText;
create_time = row.cells[3].innerText;
img_path = pageData[row.rowIndex-1].img_path;
document.getElementById('modelName').innerText = `${model_name}`;
document.getElementById('channleName').innerText = `${channel_name}`;
document.getElementById('warnTime').innerText = `${create_time}`;
document.getElementById('warnImg').src = `/api/warn/warn_img?path=${img_path}`; // 设置图片的 src
$('#showWarn').modal('show');
}
//下载视频按钮
function videoWarn(row){
video_path = pageData[row.rowIndex-1].video_path;
// const videoPlayer = document.getElementById('videoPlayer');
// videoPlayer.src = `/api/warn/warn_video?path=${encodeURIComponent(video_path)}`;
// videoPlayer.load(); // 确保重新加载视频
// $('#showVideo').modal('show');
// 创建下载链接
const downloadUrl = `/api/warn/warn_video?path=${encodeURIComponent(video_path)}`;
const a = document.createElement('a');
a.href = downloadUrl;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
//删除一条报警数据
function deleteWarn(row){
if (confirm('确定删除此报警吗?')) {
let alarmIds=[];
warn_id = row.cells[0].innerText;
alarmIds.push(warn_id);
// 发送POST请求到后端
fetch('/api/warn/warn_del', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ alarmIds: alarmIds })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('删除成功');
} else {
alert('删除失败');
}
})
.catch(error => {
console.error('Error:', error);
});
}
}
//删除当前页的报警数据
function delpageWarn(){
if (confirm('确定删除此页报警吗?')) {
let alarmIds=[];
pageData.forEach((warn) => {
alarmIds.push(warn.ID)
});
// 发送POST请求到后端
fetch('/api/warn/warn_del', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ alarmIds: alarmIds })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('删除成功');
} else {
alert('删除失败');
}
})
.catch(error => {
console.error('Error:', error);
});
}
}

12
web/main/templates/login.html

@ -80,6 +80,7 @@
</div> </div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<form id="loginForm" method="post" action="/api/user/login"> <form id="loginForm" method="post" action="/api/user/login">
<img class="mb-4" src="images/登录/zf.svg" alt="" width="72" height="57"> <img class="mb-4" src="images/登录/zf.svg" alt="" width="72" height="57">
<h1 class="h3 mb-3 fw-normal">ZF-AI</h1> <h1 class="h3 mb-3 fw-normal">ZF-AI</h1>
@ -105,6 +106,11 @@
<p class="mt-5 mb-3 text-muted">&copy; 2024–2025 ZFKJ All Rights Reserved</p> <p class="mt-5 mb-3 text-muted">&copy; 2024–2025 ZFKJ All Rights Reserved</p>
</form> </form>
</main> </main>
<!-- 重置按钮 -->
<button id="resetButton" class="btn btn-secondary" onclick="resetSystem()" style="position: absolute; top: 20px; right: 20px;">
重置
</button>
<script> <script>
const form = document.getElementById('loginForm'); const form = document.getElementById('loginForm');
const captchaImage = document.getElementById('captchaImage'); const captchaImage = document.getElementById('captchaImage');
@ -117,6 +123,12 @@
function showError(errorText) { function showError(errorText) {
alert(errorText); alert(errorText);
} }
function resetSystem() {
if (confirm('确定要重置系统吗?重置系统将清空除IP设置外的所有数据!')) {
console.log("重置系统")
}
}
</script> </script>
</body> </body>
</html> </html>

103
web/main/templates/warn_manager.html

@ -5,19 +5,103 @@
{% block style %} {% block style %}
.table-container { .table-container {
min-height: 400px; /* 设置最小高度,可以根据需要调整 */ min-height: 400px; /* 设置最小高度,可以根据需要调整 */
max-height: 650px;
overflow-y: auto; /* 启用垂直滚动条 */
border: 1px solid #ddd; /* 可选:为容器添加边框 */
}
table {
width: 100%;
border-collapse: collapse; /* 防止表格出现双线边框 */
}
thead th {
position: sticky;
top: 0;
background-color: #f8f9fa; /* 表头背景色,防止滚动时透明 */
z-index: 1;
} }
/* 缩小表格行高 */ /* 缩小表格行高 */
.table-sm th, .table-sm th,
.table-sm td { .table-sm td {
padding: 0.2rem; /* 调整这里的值来改变行高 */ padding: 0.2rem; /* 调整这里的值来改变行高 */
} }
.form-label {
font-size: 1em; /* 标签的字体大小 */
}
.form-value {
font-size: 0.5em; /* 值的字体稍小 */
}
#warnImg {
max-width: 100%; /* 图片宽度最多为模态框的宽度 */
height: auto; /* 保持图片的宽高比 */
display: block; /* 保证图片独占一行 */
margin: 0 auto; /* 居中显示 */
}
/* 使视频播放器适应模态框 */
#videoPlayer {
width: 100%;
height: auto; /* 保持视频比例 */
}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<!-- 模态框区域 --> <!-- 模态框区域 -->
<!-- 查看报警信息模态框 -->
<div class="modal fade" id="showWarn" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">报警详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group mb-2">
<label for="modelName" class="form-label">算法名称:</label>
<label class="form-label" class="form-value" id="modelName"></label>
</div>
<div class="form-group mb-2">
<label for="channleName" class="form-label">视频通道:</label>
<label class="form-label" class="form-value" id="channleName"></label>
</div>
<div class="form-group mb-2">
<label for="warnTime" class="form-label">报警时间:</label>
<label class="form-label" class="form-value" id="warnTime"></label>
</div>
<div class="form-group mb-2">
<label for="warnImg" class="form-label">报警图片:</label>
<img id="warnImg"></img>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="cancelButton_model">关闭</button>
</div>
</div>
</div>
</div>
<!-- 查看报警视频模态框 -->
<div class="modal fade" id="showVideo" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">报警视频</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<video id="videoPlayer" controls width="640">Your video tag.</video>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="cancelButton_viedeo">关闭</button>
</div>
</div>
</div>
</div>
<!-- 搜索区 --> <!-- 搜索区 -->
<div class="container d-flex flex-column" > <div class="container d-flex flex-column" >
<div class="row justify-content-center align-items-center mb-3"> <div class="row justify-content-center align-items-center mb-3">
<div class="col-md-1 text-end"><label class="col-form-label form-label">算法名称:</label></div> <div class="col-md-1 text-end"><label class="col-form-label form-label">算法名称:</label></div>
<div class="col-md-2"> <div class="col-md-2">
@ -34,12 +118,10 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<button id="delButton" type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#channelModal"> <button id="delPageButton" type="button" class="btn btn-primary">清除报警</button>
清除报警 <!-- <button id="exportButton" type="button" class="btn btn-primary">-->
</button> <!-- 导出报警-->
<button id="exportButton" type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#channelModal"> <!-- </button>-->
导出报警
</button>
</div> </div>
<div class="table-container"> <div class="table-container">
@ -53,20 +135,21 @@
<th scope="col">操作</th> <th scope="col">操作</th>
</tr> </tr>
</thead> </thead>
<tbody id="table-body" class="table-group-divider"> <tbody id="table-body-warn" class="table-group-divider">
<!-- 表格数据动态填充 --> <!-- 表格数据动态填充 -->
</tbody> </tbody>
</table> </table>
</div>
<nav> <nav>
<ul id="pagination" class="pagination"> <ul id="pagination-warn" class="pagination">
<!-- 分页控件将动态生成 --> <!-- 分页控件将动态生成 -->
</ul> </ul>
</nav> </nav>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script src="{{ url_for('main.static', filename='scripts/warn_manager.js') }}"></script> <script src="{{ url_for('main.static', filename='scripts/warn_manager.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/jquery-3.2.1.slim.min.js') }}"></script>
{% endblock %} {% endblock %}

BIN
zfbox.db

Binary file not shown.
Loading…
Cancel
Save