Browse Source

web初步版本一,基于一通道一线程上进行修改

master-one-c-one-t
张龙 9 months ago
parent
commit
398216ac9e
  1. 2
      .idea/FristProject.iml
  2. 2
      .idea/misc.xml
  3. BIN
      DB_table.xlsx
  4. 9
      config.yaml
  5. 258
      core/ChannelManager.py
  6. 15
      core/DBManager.py
  7. 270
      core/ModelManager.py
  8. 306
      model/plugins/ModelBase.py
  9. BIN
      model/plugins/Peo_ACL/yolov5s_bs1.om
  10. 90
      model/plugins/RYRQ_ACL/RYRQ_Model_ACL.py
  11. 35
      run.py
  12. 170
      web/API/channel.py
  13. 29
      web/API/user.py
  14. 104
      web/API/viedo.py
  15. 2
      web/__init__.py
  16. 2
      web/main/__init__.py
  17. 23
      web/main/routes.py
  18. BIN
      web/main/static/favicon.ico
  19. BIN
      web/main/static/images/登录/zf.png
  20. 26
      web/main/static/images/登录/zf.svg
  21. 15
      web/main/static/resources/css/headers.css
  22. 33
      web/main/static/resources/css/sign-in.css
  23. BIN
      web/main/static/resources/images/zf.png
  24. 26
      web/main/static/resources/images/zf.svg
  25. 337
      web/main/static/resources/scripts/aiortc-client-new.js
  26. 47
      web/main/static/resources/scripts/base.js
  27. 608
      web/main/static/resources/scripts/channel_manager.js
  28. 4
      web/main/static/resources/scripts/jquery-3.2.1.slim.min.js
  29. 5
      web/main/static/resources/scripts/popper.min.js
  30. 46
      web/main/templates/base.html
  31. 291
      web/main/templates/channel_manager.html
  32. 29
      web/main/templates/footer.html
  33. 35
      web/main/templates/header.html
  34. 205
      web/main/templates/index_webrtc.html
  35. 122
      web/main/templates/login.html
  36. 156
      web/main/templates/schedule.html
  37. 146
      web/main/templates/view_main.html
  38. BIN
      流程说明.docx

2
.idea/FristProject.iml

@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="acl392" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Remote Python 3.9.2 (sftp://root@192.168.3.48:22/usr/local/miniconda3/bin/python)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

2
.idea/misc.xml

@ -3,7 +3,7 @@
<component name="Black">
<option name="sdkName" value="PyTorch" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="acl392" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Remote Python 3.9.2 (sftp://root@192.168.3.48:22/usr/local/miniconda3/bin/python)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>

BIN
DB_table.xlsx

Binary file not shown.

9
config.yaml

@ -27,19 +27,16 @@ ALLOWED_EXTENSIONS : {'zip'}
#RTSP
RTSP_Check_Time : 600 #10分钟 -- 2024-7-8 取消使用
#max_channel_num
max_channel_num : 4 #最大视频通道数量
#model
model_platform : cpu #acl gpu cpu
model_platform : acl #acl gpu cpu
device_id : 0 #单设备配置
weight_path: /model/weights
yolov5_path: D:/Project/FristProject/model/base_model/yolov5 #使用绝对路径,不同的部署环境需要修改!
cap_sleep_time: 120 #单位秒 -- 5分钟
cap_sleep_time: 120 #5分钟
buffer_len: 100 #分析后画面缓冲区帧数 -- 可以与验证帧率结合确定缓冲区大小
RESET_INTERVAL : 100000 #帧数重置上限
frame_rate : 20 #帧率参考值 -- 后续作用主要基于verify_rate进行帧率控制
verify_rate : 8 #验证帧率--- 也就是视频输出的帧率
warn_video_path: /mnt/zfbox/model/warn/
warn_interval: 120 #报警间隔--单位秒
video_error_count: 3 #单位秒 ---根据验证帧率,判断3秒内都是空帧的话,视频源链接有问题。
q_size : 30 #线程队列对长度

258
core/ChannelManager.py

@ -4,20 +4,72 @@ import numpy as np
import time
import copy
import queue
import cv2
from datetime import datetime, timedelta
from myutils.ConfigManager import myCongif
from core.WarnManager import WarnData
from myutils.MyLogger_logger import LogHandler
#--2024-7-12调整规则,一个视频通道只允许配置一个算法模型,一个算法模型可以配置给多路通道
class ChannelData:
def __init__(self, str_url, int_type, bool_run, deque_length,icount_max):
self.cap = None
def __init__(self, channel_id,str_url, int_type, bool_run, deque_length,icount_max):
self.channel_id = channel_id
self.str_url = str_url #视频源地址
self.int_type = int_type #视频源类型,0-usb,1-rtsp,2-hksdk
self.bool_run = bool_run #线程运行标识
self.deque_frame = deque(maxlen=deque_length)
self.last_frame = None # 保存图片数据
self.frame_queue = queue.Queue(maxsize=1)
self.counter = 0 #帧序列号--保存报警录像使用
self.icount_max = icount_max #帧序列号上限
self.lock = threading.RLock() # 用于保证线程安全
self.bModel = False #该通道是否有模型标识
self.frame_interval = 1.0 / int(myCongif.get_data("verify_rate"))
self.mylogger = LogHandler().get_logger("ChannelData")
#通道-模型推理输入数据
self.cap = None
self.in_check_area = None
self.in_polygon = None
self.last_frame_time = time.time()
self.preimg_q = queue.Queue(maxsize=myCongif.get_data("q_size")) #这是预处理后的图片--模型推理使用
self.img_q = queue.Queue(maxsize=myCongif.get_data("q_size")) # 原图-后处理使用
#图片的缩放比例
self.cale_ratio = None
self.pad_size = None
self.schedule = None # 布放计划
self.result = None # 结果记录
self.proportion = None #报警占比
self.warn_save_count = 0 #保存录像的最新帧初始化为0
self.model_name = None #关联模型的名字
self.warn_last_time = time.time()
#通道-模型推理数据
self.last_infer_time = time.time()
self.infer_img_q = queue.Queue(maxsize=myCongif.get_data("q_size")) # 原图-后处理使用
#通道-模型推理输出数据
self.last_out_time = time.time()
self.output_q = queue.Queue(maxsize=myCongif.get_data("q_size"))
self.deque_frame = deque(maxlen=deque_length) #推理后的缓冲区数据
self.counter = 0 # 帧序列号--保存报警录像使用
self.icount_max = icount_max # 帧序列号上限
self.last_frame = None # 保存图片数据 方案一
self.frame_queue = queue.Queue(maxsize=1) #保持图片数据 方案二
self.lock = threading.RLock() # 用于保证线程安全 -- 输出数据锁
self.mMM = None #ModelManager实例对象
def __del__(self):
self.cap.release() #停止视频采集线程
def cleardata(self): #清理数据
pass
'''输入数据相关函数'''
def set_in_data(self,in_check_area,in_polygon):
self.in_check_area = in_check_area
self.in_polygon = in_polygon
def set_in_cale_ratio(self,cale_ratio,pad_size):
if self.cale_ratio is None:
self.cale_ratio = cale_ratio
self.pad_size = pad_size
'''输出数据相关函数'''
#添加一帧图片
def add_deque(self, value):
self.deque_frame.append(value) #deque 满了以后会把前面的数据移除
@ -28,26 +80,37 @@ class ChannelData:
#获取最后一帧图片
def get_last_frame(self):
with self.lock:
frame = self.last_frame
if self.bModel:
#print("取画面")
# with self.lock:
# frame = self.last_frame
if not self.frame_queue.empty():
return self.frame_queue.get()
else:
return None
else:
ret,img = self.cap.read()
if not ret:
return None
#img_bgr_ndarray = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
# 在线程里面完成应该可以减少网页端处理时间
ret, frame_bgr_webp = cv2.imencode('.jpg', img)
if not ret:
frame = None
else:
frame = frame_bgr_webp.tobytes()
return frame
# if not self.frame_queue.empty():
# return self.frame_queue.get()
# else:
# return None
def update_last_frame(self,buffer):
if buffer:
with self.lock:
self.last_frame = None
self.last_frame = buffer
# if not self.frame_queue.full():
# self.frame_queue.put(buffer)
# else:
# self.frame_queue.get() # 丢弃最旧的帧
# self.frame_queue.put(buffer)
# with self.lock:
# self.last_frame = None
# self.last_frame = buffer
if not self.frame_queue.full():
self.frame_queue.put(buffer)
else:
self.frame_queue.get() # 丢弃最旧的帧
self.frame_queue.put(buffer)
#帧序列号自增 一个线程中处理,不用加锁
def increment_counter(self):
@ -69,6 +132,145 @@ class ChannelData:
def stop_run(self):
self.bool_run = False
def th_sleep(self,frame_interval,last_time):
# 控制帧率 -- 推理帧率必须所有模型一致,若模型推理耗时一致,该方案还算可以。
current_time = time.time()
elapsed_time = current_time - last_time
if elapsed_time < frame_interval:
time.sleep(frame_interval - elapsed_time) # 若小于间隔时间则休眠
def is_in_schedule(self):
'''判断当前时间是否在该通道的工作计划时间内'''
# 验证检测计划,是否在布防时间内
now = datetime.now() # 获取当前日期和时间
weekday = now.weekday() # 获取星期几,星期一是0,星期天是6
hour = now.hour
if self.schedule[weekday][hour] == 1: # 不在计划则不进行验证,直接返回图片
return 0
else:
next_hour = (now + timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
seconds_until_next_hour = (next_hour - now).seconds
return seconds_until_next_hour
def th_prework(self,model):
last_pre_time = time.time()
while self.bool_run:
#start_do_time = time.time()
# 控制帧率 -- 推理帧率必须所有模型一致,
self.th_sleep(self.frame_interval, last_pre_time)
last_pre_time = time.time()
#判断是否在布防计划内
sleep_time = self.is_in_schedule()
if sleep_time == 0: #判断是否工作计划内-- 后来判断下是否推理和后处理线程需要休眠
self.bModel = True
else:
self.bModel = False
time.sleep(sleep_time) #工作计划以小时为单位,休息到该小时结束
continue
# 读取图片进行推理
ret, img = self.cap.read()
if not ret:
continue
#img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) #这个要确认下什么模式输入进预处理 #?
preimg,scale_ratio, pad_size = model.prework(img) #子类实现-根据每个模型的需要进行预处理
if not self.preimg_q.full():
self.preimg_q.put(preimg)
self.img_q.put(img)
self.set_in_cale_ratio(scale_ratio, pad_size)
else:
self.mylogger.debug("preimg_q--预处理队列满了! infer线程处理过慢!")
#end_do_time = time.time()
# # 计算执行时间(秒)
# execution_time = end_do_time - start_do_time
# # 输出执行时间
# print(f"预处理代码执行时间为:{execution_time:.6f} 秒")
def th_postwork(self,model):
warn_interval = int(myCongif.get_data("warn_interval"))
last_out_time = time.time()
while self.bool_run:
#do_strat_time = time.time()
# # 控制帧率 ,
# self.th_sleep(self.frame_interval, last_out_time)
# last_out_time = time.time()
#执行后处理
output = self.output_q.get()#为空时会阻塞在get
img = self.infer_img_q.get()
# 子类实现--具体的报警逻辑
filtered_pred_all, bwarn, warn_text = model.postwork(img, output, self.in_check_area,self.in_polygon,
self.cale_ratio,self.pad_size)
# img的修改应该在原内存空间修改的
self.result.pop(0) # 先把最早的结果推出数组,保障结果数组定长
if bwarn:
# 绘制报警文本
#cv2.putText(img, warn_text, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
self.result.append(1)
else: # 没有产生报警也需要记录,统一计算占比
self.result.append(0)
# 在线程里面完成应该可以减少网页端处理时间
ret, frame_bgr_webp = cv2.imencode('.jpg', img)
if not ret:
buffer_bgr_webp = None
else:
buffer_bgr_webp = frame_bgr_webp.tobytes()
# 分析图片放入内存中
self.add_deque(img)
self.increment_counter() # 帧序列加一
# 一直更新最新帧,提供网页端显示
self.update_last_frame(buffer_bgr_webp)
# print(f"{channel_id}--Frame updated at:",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
if bwarn:
count_one = float(sum(self.result)) # 1,0 把1累加的和就是1的数量
ratio_of_ones = count_one / len(self.result)
# self.logger.debug(result)
if ratio_of_ones >= self.proportion: # 触发报警
# 基于时间间隔判断
current_time = time.time()
elapsed_time = current_time - self.warn_last_time
if elapsed_time < warn_interval:
continue
self.warn_last_time = current_time
# 处理报警
warn_data = WarnData()
warn_data.model_name = self.model_name
warn_data.warn_text = warn_text
warn_data.img_buffer = self.copy_deque() #深度复制缓冲区
warn_data.width = self.cap.width
warn_data.height = self.cap.height
warn_data.channel_id = self.channel_id
self.mMM.add_warm_data(warn_data)
# model_name = self.model_name
# w_s_count = self.warn_save_count # 上次保存的缓冲帧序号
# buffer_count = self.get_counter()
# # 线程?
# self.mMM.save_warn(model_name, w_s_count, buffer_count, self.copy_deque(),
# self.cap.width, self.cap.height, self.channel_id,
# self.mMM.FPS, self.mMM.fourcc)
# self.mMM.send_warn()
# # 更新帧序列号
# self.warn_save_count = buffer_count
# 结果记录要清空
for i in range(len(self.result)):
self.result[i] = 0
# do_end_time = time.time()
# # 计算执行时间(秒)
# execution_time = do_end_time - do_strat_time
# # 输出执行时间
# print(f"*************************************后处理代码执行时间为:{execution_time:.6f} 秒")
#执行预和后处理线程,每个通道一个
def start_channel_thread(self,mMM,model):
self.mMM = mMM
th_pre = threading.Thread(target=self.th_prework, args=(model,)) # 一个视频通道一个线程,线程句柄暂时部保留
th_pre.start()
th_post = threading.Thread(target=self.th_postwork,args=(model,)) # 一个视频通道一个线程,线程句柄暂时部保留
th_post.start()
class ChannelManager:
def __init__(self):
@ -81,7 +283,7 @@ class ChannelManager:
if channel_id in self.channels: #若已经有数据,先删除后再增加
self.channels[channel_id].clear() # 手动清理资源
del self.channels[channel_id]
ch_data = ChannelData(str_url, int_type, bool_run, deque_length, icount_max)
ch_data = ChannelData(channel_id,str_url, int_type, bool_run, deque_length,icount_max)
self.channels[channel_id] = ch_data
return ch_data
@ -97,17 +299,15 @@ class ChannelManager:
with self.lock:
return self.channels.get(channel_id)
#停止工作线程---要把视频采集线程停止掉
#停止工作线程
def stop_channel(self,channel_id):
with self.lock:
if channel_id == 0:
for clannel_id,clannel_data in self.channels.items():
clannel_data.cap.running = False
clannel_data.clear() #clear 里面已经停止了通道的工作线程
clannel_data.clear()
del self.channels
else:
if channel_id in self.channels:
self.channels[channel_id].cap.running = False
self.channels[channel_id].clear() # 手动清理资源
del self.channels[channel_id]

15
core/DBManager.py

@ -104,13 +104,8 @@ class DBManager():
return bok
#---------------------特定数据库操作函数---------------------
#根据通道ID或者模型ID删除通道和模型间的关联数据 1-通道ID,2-模型ID
def delC2M(self,ID,itype):
'''
#根据通道ID或者模型ID删除通道和模型间的关联数据 1-通道ID,2-模型ID ,注意会有删除没有数据的情况
:param ID:
:param itype:
:return:
'''
#channel2model
if itype ==1:
strsql = f"select ID from channel2model where channel_id={ID};"
@ -118,12 +113,16 @@ class DBManager():
strsql = f"delete from channel2model where channel_id={ID};"
ret = self.do_sql(strsql)
if ret == False:
return False
elif itype ==2:
strsql = f"select ID from channel2model where model_id={ID};"
datas = self.do_select(strsql)
strsql = f"delete from channel2model where model_id={ID};"
ret = self.do_sql(strsql)
if ret == False:
return False
else:
return False
#schedule
@ -131,11 +130,13 @@ class DBManager():
c2m_id = data[0]
strsql = f"delete from schedule where channel2model_id={c2m_id};"
ret = self.do_sql(strsql)
if ret == False:
return False
return True
#删除通道,需要关联删除布防时间,通道和算法的关联表
def delchannel(self,ID):
ret = self.delC2M(ID,1)
ret = self.delC2M(ID)
if ret == False:
return False
#channel

270
core/ModelManager.py

@ -8,7 +8,6 @@ import threading
import importlib.util
import datetime
import math
import copy
import queue
from collections import deque
from core.DBManager import mDBM,DBManager
@ -17,8 +16,7 @@ from myutils.ConfigManager import myCongif
from model.plugins.ModelBase import ModelBase
from core.ChannelManager import ChannelManager
from core.ACLModelManager import ACLModeManger
from core.WarnManager import WarnManager,WarnData
from core.WarnManager import WarnManager
from PIL import Image
@ -28,21 +26,11 @@ class VideoCaptureWithFPS:
self.source = source
self.width = None
self.height = None
# GStreamer
#rtsp_stream = f"rtspsrc location={self.source} ! decodebin ! videoconvert ! appsink"
pipeline = (
f"rtspsrc location={self.source} latency=0 ! "
"rtph264depay ! "
"h264parse ! "
"avdec_h264 ! " # 使用 avdec_h264 代替其他解码器
"videoscale ! "
"video/x-raw,width=640,height=480,framerate=10/1 ! " # 降低分辨率和帧率
"videoconvert ! "
"appsink"
)
self.cap = cv2.VideoCapture(pipeline, cv2.CAP_GSTREAMER)
# opencv
# self.cap = cv2.VideoCapture(self.source)
#GStreamer
rtsp_stream = f"rtspsrc location={self.source} ! decodebin ! videoconvert ! appsink"
self.cap = cv2.VideoCapture(rtsp_stream, cv2.CAP_GSTREAMER)
#opencv
#self.cap = cv2.VideoCapture(self.source)
if self.cap.isOpened(): #若没有打开成功,在读取画面的时候,已有判断和处理 -- 这里也要检查下内存的释放情况
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
@ -60,7 +48,10 @@ class VideoCaptureWithFPS:
def update(self):
icount = 0
while self.running:
#start_time = time.time()
ret, frame = self.cap.read()
# end_time = time.time()
# print(f"read()耗时:{(end_time-start_time):.6f}")
if not ret:
icount += 1
if icount > 5: #重连
@ -69,15 +60,13 @@ class VideoCaptureWithFPS:
if self.cap.isOpened():
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(self.width,self.height)
# self.fps = fps # 线程保持最大帧率的刷新画面---过高的帧率会影响CPU性能,但过地的帧率会造成帧积压
self.fps = math.ceil(
self.cap.get(cv2.CAP_PROP_FPS) / float(myCongif.get_data("verify_rate"))) # 向上取整。
icount = 0
else:
sleep_time = myCongif.get_data("cap_sleep_time")
print(f"{self.source}视频流,将于{sleep_time}秒后重连!")
time.sleep(sleep_time)
print(f"{self.source}视频流,将于5分钟后重连!")
time.sleep(myCongif.get_data("cap_sleep_time"))
continue
#resized_frame = cv2.resize(frame, (int(self.width / 2), int(self.height / 2)))
with self.read_lock:
@ -94,6 +83,7 @@ class VideoCaptureWithFPS:
def read(self):
with self.read_lock:
frame = self.frame.copy() if self.frame is not None else None
#frame = self.frame
if frame is not None:
return True, frame
else:
@ -108,6 +98,7 @@ class VideoCaptureWithFPS:
self.thread.join()
self.cap.release()
class ModelManager:
def __init__(self):
self.verify_list = ChannelManager() #模型的主要数据 -- 2024-7-5修改为类管理通道数据
@ -118,20 +109,18 @@ class ModelManager:
#self.buflen = myCongif.get_data("buffer_len")
self.icout_max = myCongif.get_data("RESET_INTERVAL") #跟视频帧序用一个变量
self.frame_rate = myCongif.get_data("frame_rate")
self.frame_interval = 1.0 / int(myCongif.get_data("verify_rate"))
#保存视频相关内容
self.FPS = myCongif.get_data("verify_rate") # 视频帧率--是否能实现动态帧率
self.fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 使用 mp4 编码
#基于模型运行环境进行相应初始化工作
self.model_platform = myCongif.get_data("model_platform")
self.device_id = myCongif.get_data("device_id")
# acl初始化 -- 一个线程一个 -- 需要验证
# acl资源初始化
if self.model_platform == "acl":
ACLModeManger.init_acl(self.device_id) #acl -- 全程序初始化
self.model_dic = {} # model_id model
# 报警处理线程-全进程独立一个线程处理
self.model_dic = {} #model_id model
#报警处理线程-全进程独立一个线程处理
self.warnM = None
def __del__(self):
self.logger.debug("释放资源")
del self.verify_list #应该需要深入的删除--待完善
@ -162,7 +151,7 @@ class ModelManager:
return None
module = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(module)
md = getattr(module, "Model")(model_path,threshold) #实例化
md = getattr(module, "Model")(model_path,self,threshold) #实例化Model
if not isinstance(md, ModelBase):
self.logger.error("{} not zf_model".format(md))
return None
@ -173,7 +162,6 @@ class ModelManager:
else:
self.logger.error("{}文件不存在".format(model_path))
return None
self.logger.debug(f"{model_path} 加载成功!!!!")
return md
def getschedule(self,c2m_id,myDBM):
@ -222,193 +210,93 @@ class ModelManager:
def set_last_img(self,):
pass
def verify(self,frame,model,model_data,channel_id,schedule,result,isdraw=1):
'''验证执行主函数,实现遍历通道关联的模型,调用对应模型执行验证'''
#img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = frame
#验证检测计划,是否在布防时间内
now = datetime.datetime.now() # 获取当前日期和时间
weekday = now.weekday() # 获取星期几,星期一是0,星期天是6
hour = now.hour
result.pop(0) # 保障结果数组定长 --先把最早的结果推出数组
detections = None
bwarn = False
warntext = ""
if model and schedule[weekday][hour] == 1: #不在计划则不进行验证,直接返回图片
# 调用模型,进行检测,model是动态加载的,具体的判断标准由模型内执行 ---- *********
#isverify = True
detections, bwarn, warntext = model.verify(img, model_data,isdraw) #****************重要
# 对识别结果要部要进行处理
if bwarn:
# 绘制报警文本
cv2.putText(img, warntext, (50, 50),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
result.append(1) #要验证数组修改,是地址修改吗?
else: #没有产生报警也需要记录,统一计算占比
warntext = ""
result.append(0)
else:
result.append(0)
# 将检测结果图像转换为帧--暂时用不到AVFrame--2024-7-5
# new_frame_rgb_avframe = av.VideoFrame.from_ndarray(img, format="rgb24") # AVFrame
# new_frame_rgb_avframe.pts = None # 添加此行确保有pts属性
# if isinstance(img, np.ndarray): -- 留个纪念
#处理完的图片后返回-bgr模式
#img_bgr_ndarray = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
# 将检查结果转换为WebP格式图片 --在线程里面完成应该可以减少网页端处理时间
ret,frame_bgr_webp=cv2.imencode('.jpg', img)
if not ret:
buffer_bgr_webp = None
else:
buffer_bgr_webp = frame_bgr_webp.tobytes()
return buffer_bgr_webp,img,warntext
def dowork_thread(self,channel_id):
'''一个通道一个线程,关联的模型在一个线程检测,局部变量都是一个通道独有'''
channel_data = self.verify_list.get_channel(channel_id) #是对ChannelData 对象的引用
context = None
# 线程ACL初始化
if self.model_platform == "acl": # ACL线程中初始化内容
context = ACLModeManger.th_inti_acl(self.device_id)
def start_model_thread(self,channel_id): #通道ID会重吗?
'''实例化模型组件,启动模型推理线程'''
channel_data = self.verify_list.get_channel(channel_id) # 是对ChannelData 对象的引用
#查询关联的模型 --- 在循环运行前把基础数据都准备好
myDBM = DBManager()
myDBM.connect()
strsql = (f"select t1.model_id,t1.check_area,t1.polygon ,t2.duration_time,t2.proportion,t2.model_path,t1.ID,"
f"t2.model_name,t1.conf_threshold "
f"from channel2model t1 left join model t2 on t1.model_id = t2.ID where t1.channel_id ={channel_id};")
#print(strsql)
model = myDBM.do_select(strsql,1) #2024-7-12调整规则,一个通道只关联一个模型,表结构暂时不动
if len(model) ==0:
print(f"{channel_id}视频通道没有关联模型,结束线程!")
# print(strsql)
model = mDBM.do_select(strsql,1) #2024-7-12调整规则,一个通道只关联一个模型,表结构暂时不动
if model:
strMID = str(model[0])
m = None
if strMID in self.model_dic:#该模型线程已启动
m = self.model_dic[strMID]
else:
# 基于基类实例化模块类
m = self._import_model("", model[5], model[8]) # 动态加载模型处理文件py --需要验证模型文件是否能加载
if m:
# 开始工作线程---推理线程需不需要全进程唯一
m.run = True
m.start_th() #模型里跑两个线程
#添加模型对象到字典中
self.model_dic[strMID] = m
else:
self.logger.error(f"{model[5]}没有实例化成功")
return #模型没加载成功--原画输出
# 更新该模型对应视频通道的数据
channel_data.set_in_data(model[1], model[2]) #chack_area,ploygon
channel_data.schedule = self.getschedule(model[6], mDBM) # 布放计划-c_m_id
channel_data.result = [0 for _ in
range(model[3] * myCongif.get_data("verify_rate"))] # 初始化时间*验证帧率数量的结果list
channel_data.proportion = model[4] # 报警占比
channel_data.warn_last_time = time.time() # 最后次触发报警的时间
channel_data.model_name = model[7]
# 添加通道
m.addChannel(channel_id, channel_data) #删除时不能手动清空内存 2024-7-14
#启动视频通道预处理和后处理线程
channel_data.start_channel_thread(self,m) #一个视频通道,一个预和后处理线程
#删除还没完善 del self.model_dic[strMID]
else: #没有模型数据--channel_data.bModel = Flase ,不需要添加数据,直接原画输出
return
#基于基类实例化模块类
m = self._import_model("",model[5],model[8]) #动态加载模型处理文件py
schedule = self.getschedule(model[6], myDBM)
result = [0 for _ in range(model[3] * myCongif.get_data("verify_rate"))] # 初始化时间*验证帧率数量的结果list
#model[6] -- c2m_id --布防计划 0-周一,6-周日
warn_last_time = time.time()
proportion = model[4] #判断是否报警的占比
warn_save_count = 0 #保存录像的最新帧初始化为0
#开始拉取画面循环检测
cap = channel_data.cap
last_frame_time = time.time() #初始化个读帧时间
#可以释放数据库资源
del myDBM
warn_interval = myCongif.get_data("warn_interval")
while channel_data.bool_run: #基于tag 作为运行标识。 线程里只是读,住线程更新,最多晚一轮,应该不用线程锁。需验证
# 帧率控制帧率
current_time = time.time()
elapsed_time = current_time - last_frame_time
if elapsed_time < self.frame_interval:
time.sleep(self.frame_interval - elapsed_time) #若小于间隔时间则休眠
last_frame_time = time.time()
#*********取画面*************
ret,frame = cap.read() #除了第一帧,其它应该都是有画面的
if not ret:
continue #没读到画面继续
#执行图片推理
buffer_bgr_webp,img_bgr_ndarray,warn_text = self.verify(frame,m,model,channel_id,schedule,result)
#分析图片放入内存中
channel_data.add_deque(img_bgr_ndarray) # 缓冲区大小由maxlen控制 超上限后,删除最前的数据
#channel_data.increment_counter() #帧序列加一
# 一直更新最新帧,提供网页端显示
channel_data.update_last_frame(buffer_bgr_webp)
#print(f"{channel_id}--Frame updated at:",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
if warn_text:
#验证result -是否触发报警要求 --遍历每个模型执行的result
count_one = float(sum(result)) #1,0 把1累加的和就是1的数量
ratio_of_ones = count_one / len(result)
#self.logger.debug(result)
if ratio_of_ones >= proportion: #触发报警
# 基于时间间隔判断
current_time = time.time()
elapsed_time = current_time - warn_last_time
if elapsed_time < warn_interval:
continue
warn_last_time = current_time
# 处理报警
warn_data = WarnData()
warn_data.model_name = model[7]
warn_data.warn_text = warn_text
warn_data.img_buffer = channel_data.copy_deque() # 深度复制缓冲区
warn_data.width = cap.width
warn_data.height = cap.height
warn_data.channel_id = channel_id
self.warnM.add_warn_data(warn_data)
# #更新帧序列号 #加了报警间隔 --buffer的长度小于间隔
# warn_save_count = buffer_count
#结果记录要清空
for i in range(len(result)):
result[i] = 0
# end_time = time.time() # 结束时间
# print(f"Processing time: {end_time - start_time} seconds")
# 本地显示---测试使用
# if channel_id == 2:
# cv2.imshow(str(channel_id), img)
# if cv2.waitKey(1) & 0xFF == ord('q'):
# break
#结束线程
cap.release() #视频采集线程结束
if context:#ACL线程中反初始化内容 -- 若线程异常退出,这些资源就不能正常释放了
#先释放每个模型资源
del model
#再释放context
ACLModeManger.th_del_acl(context)
#cv2.destroyAllWindows()
def send_warn(self):
'''发送报警信息'''
pass
def stop_model_thread(self,channel_id,model_id):
'''某个视频通道结束工作'''
channel_data = self.verify_list.get_channel(channel_id) # 是对ChannelData 对象的引用
m = self.model_dic.get(model_id)
if m:
m.strop_th()
channel_data.cap.release()
def save_frame_to_video(self):
'''把缓冲区中的画面保存为录像'''
pass
def start_work(self,channel_id=0):
def start_work(self,channel_id=0): #还涉及单通道对开启和关闭
'''算法模型是在后台根据画面实时分析的
1.布防开关需要触发通道关闭和开启
2.布防策略的调整也需要关闭和重启工作
'''
#pre-thread
if channel_id ==0:
strsql = "select id,ulr,type from channel where is_work = 1;" #执行所有通道
else:
strsql = f"select id,ulr,type from channel where is_work = 1 and id = {channel_id};" #单通道启动检测线程
datas = mDBM.do_select(strsql)
for data in datas:
# channel_id, str_url, int_type, bool_run, deque_length
c_data = self.verify_list.add_channel(data[0],data[1],data[2],True,
myCongif.get_data("buffer_len"),myCongif.get_data("RESET_INTERVAL"))
# 创建channel_data
ch_data = self.verify_list.add_channel(data[0],data[1],data[2],True,
myCongif.get_data("buffer_len"),myCongif.get_data("RESET_INTERVAL"))
#启动该通道的视频捕获线程
ch_data.cap = self._open_view(ch_data.str_url,ch_data.int_type) #创建子线程读画面-把cap给模型就行--
ch_data.mMM = self
# 启动该通道的视频捕获线程 --把视频捕获线程,放主线程创建
c_data.cap = self._open_view(c_data.str_url, c_data.int_type) # 创建子线程读画面-把cap给模型就行--
#目前一个模型两个线程
self.start_model_thread(data[0])
#启动告警线程
self.warnM = WarnManager()
self.warnM.start_warnmanager_th()
th_chn = threading.Thread(target=self.dowork_thread, args=(data[0],)) #一个视频通道一个线程,线程句柄暂时部保留
th_chn.start()
# 启动告警线程
if self.warnM is None:
self.warnM = WarnManager()
self.warnM.start_warnmanager_th()
def add_warm_data(self,warn_data):
self.warnM.add_warn_data(warn_data)
def stop_work(self,channel_id=0):
'''停止工作线程,0-停止所有,非0停止对应通道ID的线程'''
self.verify_list.stop_channel(channel_id)
if channel_id == 0:
#停止告警线程
self.warnM.brun = False
def restartC2M(self,channel_id):
'''
修改通道管理的算法模型后需要对该通道算法执行部分重新加载执行
:param channel_id:
:return:
'''
pass
#print(f"Current working directory (ModelManager.py): {os.getcwd()}")

306
model/plugins/ModelBase.py

@ -4,6 +4,10 @@ from myutils.ConfigManager import myCongif
from myutils.MyLogger_logger import LogHandler
import numpy as np
import cv2
import time
import queue
from datetime import datetime, timedelta
import threading
import ast
if myCongif.get_data("model_platform") == "acl":
import acl
@ -14,30 +18,293 @@ SUCCESS = 0 # 成功状态值
FAILED = 1 # 失败状态值
ACL_MEM_MALLOC_NORMAL_ONLY = 2 # 申请内存策略, 仅申请普通页
class ModelRunData:
def __init__(self):
self.channel_id = None
class ModelBase(ABC):
def __init__(self,path):
def __init__(self,path,mMM):
'''
模型类实例化
:param path: 模型文件本身的路径
:param threshold: 模型的置信阈值
'''
self.model_path = path # 模型路径
self.mMM = mMM # ModelManager
self.mylogger = LogHandler().get_logger("ModelManager")
self.name = None #基于name来查询,用户对模型的配置参数,代表着模型名称需要唯一 2024-6-18 -逻辑还需要完善和验证
self.version = None
self.model_type = None # 模型类型 1-图像分类,2-目标检测(yolov5),3-分割模型,4-关键点
self.system = myCongif.get_data("model_platform") #platform.system() #获取系统平台
self.do_map = { # 定义插件的入口函数 --
# POCType.POC: self.do_verify,
# POCType.SNIFFER: self.do_sniffer,
# POCType.BRUTE: self.do_brute
}
self.model_path = path # 模型路径
#--2024-7-12调整规则,一个视频通道只允许配置一个算法模型,一个算法模型可以配置给多路通道
self.channel_list = [] #该模型需要处理的视频通道
self.cid_copy_list = [] #基于channel_list的备份,提供给遍历线程
self.channel_data_list =[] #该模型,针对每个通道配置的执行参数 ChannelData --包含了输入和输出数据
self.cda_copy_list = [] #基于channel_data_list的备份,提供给遍历线程
self.frame_interval = 1.0 / int(myCongif.get_data("verify_rate"))
self.init_ok = False
#启动推理线程 -- 实例化后是否启动工作线程 -- acl这么搞
self.run = False
#创建线程锁
self.list_lock = threading.Lock() #list修改锁
self.copy_lock = threading.Lock() #副本拷贝锁
self.read_lock = threading.Lock() #遍历线程锁
self.readers = 0 #并发读个数
def __del__(self):
print("资源释放")
def addChannel(self,channel_id,channel_data): #这两个参数有点重复,后续考虑优化
bfind = False
with self.list_lock:
for cid in self.channel_list:
if cid == channel_id:
bfind = True
if not bfind:
self.channel_data_list.append(channel_data)
self.channel_list.append(channel_id)
#复制备份
self.set_copy_list()
def delChannel(self,channel_id):#调用删除通道的地方,若channel为空,则停止线程,删除该模型对象
with self.list_lock:
for i,cid in enumerate(self.channel_list):
if cid == channel_id:
self.channel_list.remove(channel_id)
#self.channel_data_list[i].cleardata() #释放内存
self.channel_data_list.pop(i)
# 复制备份
self.set_copy_list()
def set_copy_list(self): #把list拷贝一个副本
with self.copy_lock:
self.cid_copy_list = self.channel_list.copy()
self.cda_copy_list = self.channel_data_list.copy()
# copy使用读写锁,这样能避免三个遍历线程间相互竞争
def acquire_read(self):
with self.read_lock:
self.readers += 1
if self.readers == 1:
self.copy_lock.acquire()
def release_read(self):
with self.read_lock:
self.readers -= 1
if self.readers == 0:
self.copy_lock.release()
def get_copy_list(self): #线程中拷贝一个本地副本 其实读线程里面执行的时间复杂度也不高,直接用copy锁一样
self.acquire_read()
local_id = self.cid_copy_list.copy()
local_data = self.cda_copy_list.copy()
self.release_read()
return local_id,local_data
def strop_th(self):
'''停止该模型的工作线程'''
self.run = False
time.sleep(1) #确认下在哪执行
#删除list
del self.channel_list
def start_th(self):
#要确保三个线程对channel_data的读取和修改是线程安全的,或是独立分开的。
#预处理
# th_pre = threading.Thread(target=self.th_prework) # 一个视频通道一个线程,线程句柄暂时部保留
# th_pre.start()
#推理
th_infer = threading.Thread(target=self.th_startwork) # 一个视频通道一个线程,线程句柄暂时部保留
th_infer.start()
#后处理
# th_post = threading.Thread(target=self.th_postwork) # 一个视频通道一个线程,线程句柄暂时部保留
# th_post.start()
def th_sleep(self,frame_interval,last_time):
# 控制帧率 -- 推理帧率必须所有模型一致,若模型推理耗时一致,该方案还算可以。
current_time = time.time()
elapsed_time = current_time - last_time
if elapsed_time < frame_interval:
time.sleep(frame_interval - elapsed_time) # 若小于间隔时间则休眠
def is_in_schedule(self,channel_data):
'''判断当前时间是否在该通道的工作计划时间内'''
# 验证检测计划,是否在布防时间内
now = datetime.now() # 获取当前日期和时间
weekday = now.weekday() # 获取星期几,星期一是0,星期天是6
hour = now.hour
if channel_data.schedule[weekday][hour] == 1: # 不在计划则不进行验证,直接返回图片
return 0
else:
next_hour = (now + timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
seconds_until_next_hour = (next_hour - now).seconds
return seconds_until_next_hour
def th_prework(self):
last_pre_time = time.time()
while self.run:
#start_do_time = time.time()
# 控制帧率 -- 推理帧率必须所有模型一致,
self.th_sleep(self.frame_interval, last_pre_time)
last_pre_time = time.time()
#拷贝副本到线程本地
with self.copy_lock:
#local_cid = self.cid_copy_list.copy()
local_cdata = self.cda_copy_list.copy()
for channel_data in local_cdata: # 如果没有视频通道结束线程
#判断是否在布防计划内
sleep_time = self.is_in_schedule(channel_data)
if sleep_time == 0: #判断是否工作计划内-- 后来判断下是否推理和后处理线程需要休眠
channel_data.bModel = True
else:
channel_data.bModel = False
time.sleep(sleep_time)
continue
# 读取图片进行推理
ret, img = channel_data.cap.read()
if not ret:
continue
#img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) #这个要确认下什么模式输入进预处理 #?
preimg,scale_ratio, pad_size = self.prework(img) #子类实现-根据每个模型的需要进行预处理
if not channel_data.preimg_q.full():
channel_data.preimg_q.put(preimg)
channel_data.img_q.put(img)
channel_data.set_in_cale_ratio(scale_ratio, pad_size)
else:
self.mylogger.debug("preimg_q--预处理队列满了! infer线程处理过慢!")
#end_do_time = time.time()
# # 计算执行时间(秒)
# execution_time = end_do_time - start_do_time
# # 输出执行时间
# print(f"预处理代码执行时间为:{execution_time:.6f} 秒")
def th_startwork(self):
'''模型工作线程 由于有多输入通道,需要将执行任务降低到最少'''
self._init_acl() #创建context
if not self._init_resource(): #加载模型文件 -- 正常来说这里不应该失败--上层函数暂时都认为是成功的!需完善
self.mylogger.error("模型文件初始化加载失败!")
return
last_infer_time = time.time()
#开始工作--- 尽量精简!
while self.run:
#start_do_time = time.time()
# 控制帧率 -- 推理帧率必须所有模型一致,
self.th_sleep(self.frame_interval, last_infer_time)
last_infer_time = time.time()
# 拷贝副本到线程本地
with self.copy_lock:
# local_cid = self.cid_copy_list.copy()
local_cdata = self.cda_copy_list.copy()
for channel_data in local_cdata: #如果没有视频通道可以结束线程#?
if channel_data.preimg_q.empty():
continue
img = channel_data.preimg_q.get()
src_img = channel_data.img_q.get()
#就执行推理,针对结果的逻辑判断交给后处理线程处理。-- 需要确认除目标识别外其他模型的执行方式
output = self.execute([img,])[0] # 执行推理
if len(output) > 0:
if not channel_data.output_q.full():
channel_data.output_q.put(output)
channel_data.infer_img_q.put(src_img) #原图交给后处理线程
else:
self.mylogger.debug("output_q--后处理队列满了!后处理过慢!")
# end_do_time= time.time()
# # 计算执行时间(秒)
# execution_time = end_do_time - start_do_time
# # 输出执行时间
# print(f"****************推理代码执行时间为:{execution_time:.6f} 秒")
#结束工作-开始释放资源
self.release()
self._del_acl()
def th_postwork(self):
warn_interval = int(myCongif.get_data("warn_interval"))
last_out_time = time.time()
while self.run:
# 控制帧率 -- 推理帧率必须所有模型一致,
self.th_sleep(self.frame_interval, last_out_time)
last_out_time = time.time()
# 拷贝副本到线程本地
with self.copy_lock:
local_cid = self.cid_copy_list.copy()
local_cdata = self.cda_copy_list.copy()
for i, channel_data in enumerate(local_cdata):
# 控制帧率 -- 推理帧率必须所有模型一致,若模型推理耗时一致,该方案还算可以。
if channel_data.output_q.empty():
continue
#执行后处理
output = channel_data.output_q.get()
img = channel_data.infer_img_q.get()
cale_ratio = channel_data.cale_ratio
pad_size = channel_data.pad_size
if len(output) <1:
continue
filtered_pred_all, bwarn, warn_text = self.postwork(img,output,channel_data.in_check_area,
channel_data.in_polygon,cale_ratio,pad_size) #子类实现--具体的报警逻辑
#img的修改应该在原内存空间修改的
channel_data.result.pop(0) #先把最早的结果推出数组,保障结果数组定长
if bwarn: # 整个识别有产生报警
#根据模型设定的时间和占比判断是否
# 绘制报警文本
cv2.putText(img, warn_text, (50,50),cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
channel_data.result.append(1)
else: #没有产生报警也需要记录,统一计算占比
channel_data.result.append(0)
# 处理完的图片后返回-bgr模式 --一头一尾是不是抵消了,可以不做处理#?
#img_bgr_ndarray = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
# 在线程里面完成应该可以减少网页端处理时间
ret, frame_bgr_webp = cv2.imencode('.jpg', img)
if not ret:
buffer_bgr_webp = None
else:
buffer_bgr_webp = frame_bgr_webp.tobytes()
# 分析图片放入内存中
#channel_data.add_deque(img_bgr_ndarray) # 缓冲区大小由maxlen控制 超上限后,删除最前的数据
channel_data.add_deque(img)
channel_data.increment_counter() # 帧序列加一
# 一直更新最新帧,提供网页端显示
channel_data.update_last_frame(buffer_bgr_webp)
# print(f"{channel_id}--Frame updated at:",time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
# 验证result_list -是否触发报警要求 --遍历每个模型执行的result
if bwarn:
result = channel_data.result #最近的检测记录
proportion = channel_data.proportion #判断报警的占比
count_one = float(sum(result)) # 1,0 把1累加的和就是1的数量
ratio_of_ones = count_one / len(result)
# self.logger.debug(result)
if ratio_of_ones >= proportion: # 触发报警
# 基于时间间隔判断
current_time = time.time()
elapsed_time = current_time - channel_data.warn_last_time
if elapsed_time < warn_interval:
continue
#处理报警
channel_data.warn_last_time = current_time
model_name = channel_data.model_name
w_s_count = channel_data.warn_save_count #上次保存的缓冲帧序号
buffer_count = channel_data.get_counter()
#线程?
self.mMM.save_warn(model_name, w_s_count, buffer_count, channel_data.copy_deque(),
channel_data.cap.width, channel_data.cap.height, local_cid[i],
self.mMM.FPS, self.mMM.fourcc)
self.mMM.send_warn()
# 更新帧序列号
channel_data.warn_save_count = buffer_count
# 结果记录要清空
for i in range(len(result)):
result[i] = 0
def draw_polygon(self, img, polygon_points,color=(0, 255, 0)):
self.polygon = Polygon(ast.literal_eval(polygon_points))
@ -52,8 +319,7 @@ class ModelBase(ABC):
return False
#acl ----- 相关-----
def _init_acl(self):
device_id = 0
def _init_acl(self,device_id=0):
self.context, ret = acl.rt.create_context(device_id) # 显式创建一个Context
if ret:
raise RuntimeError(ret)
@ -66,10 +332,8 @@ class ModelBase(ABC):
if ret:
raise RuntimeError(ret)
print('Deinit TH-Context Successfully')
print('ACL finalize Successfully')
def _init_resource(self):
#self._init_acl() #测试使用
''' 初始化模型、输出相关资源。相关数据类型: aclmdlDesc aclDataBuffer aclmdlDataset'''
print("Init model resource")
# 加载模型文件
@ -212,17 +476,25 @@ class ModelBase(ABC):
ret = acl.destroy_data_buffer(data_buf) # 释放buffer
ret = acl.mdl.destroy_dataset(dataset) # 销毁数据集
# @abstractmethod
# def infer(self, inputs): # 保留接口, 子类必须重写
# pass
@abstractmethod
def prework(self, image): # 预处理
pass
@abstractmethod
def verify(self,image,data,isdraw=1):
def verify(self,image,data,isdraw=1): #
'''
:param image: 需要验证的图片
:param data: select t1.model_id,t1.check_area,t1.polygon ,t2.duration_time,t2.proportion,t2.model_path
:param isdraw: 是否需要绘制线框0-不绘制1-绘制
:return: detections,bwarn,warntext bwarn:0-没有识别到符合要求的目标1-没有识别到符合要求的目标
'''
pass
pass
@abstractmethod
def postwork(self,image,output,check_area,polygon,scale_ratio, pad_size): # 后处理
pass
if __name__ =="__main__":
pass

BIN
model/plugins/Peo_ACL/yolov5s_bs1.om

Binary file not shown.

90
model/plugins/RYRQ_ACL/RYRQ_Model_ACL.py

@ -8,12 +8,12 @@ import torch # 深度学习运算框架,此处主要用来处理数据
from core.ACLModelManager import ACLModeManger
class Model(ModelBase):
def __init__(self,path,threshold=0.5):
def __init__(self,path,mMM,threshold=0.5):
# 找pt模型路径 -- 一个约束py文件和模型文件的路径关系需要固定, -- 上传模型时,要解压好路径
dirpath, filename = os.path.split(path)
self.model_file = os.path.join(dirpath, "yolov5s_bs1.om") # 目前约束模型文件和py文件在同一目录
self.coco_file = os.path.join(dirpath, "coco_names.txt")
super().__init__(self.model_file) #acl环境初始化基类负责类的实例化
super().__init__(self.model_file,mMM) #acl环境初始化基类负责类的实例化
self.model_id = None # 模型 id
self.input_dataset = None # 输入数据结构
self.output_dataset = None # 输出数据结构
@ -30,6 +30,7 @@ class Model(ModelBase):
self.netw = 640 # 缩放的目标宽度, 也即模型的输入宽度
self.conf_threshold = threshold # 置信度阈值
#加载ACL模型文件---模型加载、模型执行、模型卸载的操作必须在同一个Context下
if self._init_resource(): #加载离线模型,创建输出缓冲区
print("加载模型文件成功!")
@ -41,54 +42,61 @@ class Model(ModelBase):
if self.init_ok:
self.release()
def verify(self,image,data,isdraw=1):
labels_dict = get_labels_from_txt('/mnt/zfbox/model/plugins/RYRQ_ACL/coco_names.txt') # 得到类别信息,返回序号与类别对应的字典
#针对推理图片的前处理
def prework(self,image):
# 数据前处理
img, scale_ratio, pad_size = letterbox(image, new_shape=[640, 640]) # 对图像进行缩放与填充
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 # 转换为内存连续存储的数组
return img,scale_ratio, pad_size
#针对推理结果的后处理
def postwork(self,image,output,check_area,polygon,scale_ratio, pad_size):
labels_dict = get_labels_from_txt('/mnt/zfbox/model/plugins/RYRQ_ACL/coco_names.txt') # 得到类别信息,返回序号与类别对应的字典
# 后处理 -- boxout 是 tensor-list: [tensor([[],[].[]])] --[x1,y1,x2,y2,置信度,coco_index]
boxout = nms(torch.tensor(output), conf_thres=0.3,
iou_thres=0.5) # 利用非极大值抑制处理模型输出,conf_thres 为置信度阈值,iou_thres 为iou阈值
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)) # 将推理结果缩放到原始图片大小
# 模型推理, 得到模型输出
outputs = None
outputs = self.execute([img,])#创建input,执行模型,返回结果 --失败返回None
# 是否有检测区域,有先绘制检测区域 由于在该函数生成了polygon对象,所有需要在检测区域前调用。
if check_area == 1:
self.draw_polygon(image, polygon, (0, 0, 255))
# 过滤掉不是目标标签的数据 -- 序号0-- person
filtered_pred_all = pred_all[pred_all[:, 5] == 0]
bwarn = False
warn_text = ""
# 绘制检测结果 --- 也需要封装在类里,
for pred in filtered_pred_all:
x1, y1, x2, y2 = int(pred[0]), int(pred[1]), int(pred[2]), int(pred[3])
# # 绘制目标识别的锚框 --已经在draw_bbox里处理
# cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
if check_area == 1: # 指定了检测区域
x_center = (x1 + x2) / 2
y_center = (y1 + y2) / 2
# 绘制中心点?
cv2.circle(image, (int(x_center), int(y_center)), 5, (0, 0, 255), -1)
# 判断是否区域点
if not self.is_point_in_region((x_center, y_center)):
continue # 没产生报警-继续
# 产生报警 -- 有一个符合即可
bwarn = True
warn_text = "Intruder detected!"
img_dw = draw_bbox(filtered_pred_all, image, (0, 255, 0), 2, labels_dict) # 画出检测框、类别、概率
# cv2.imwrite('img_res.png', img_dw)
return filtered_pred_all, bwarn, warn_text
def verify(self,image,outputs,isdraw=1):
labels_dict = get_labels_from_txt('/mnt/zfbox/model/plugins/RYRQ_ACL/coco_names.txt') # 得到类别信息,返回序号与类别对应的字典
#后处理部分了
filtered_pred_all = None
bwarn = False
warn_text = ""
# 是否有检测区域,有先绘制检测区域 由于在该函数生成了polygon对象,所有需要在检测区域前调用。
if data[1] == 1:
self.draw_polygon(image, data[2], (255, 0, 0))
if outputs:
output = outputs[0] #只放了一张图片
# 后处理 -- boxout 是 tensor-list: [tensor([[],[].[]])] --[x1,y1,x2,y2,置信度,coco_index]
boxout = nms(torch.tensor(output), conf_thres=0.3,
iou_thres=0.5) # 利用非极大值抑制处理模型输出,conf_thres 为置信度阈值,iou_thres 为iou阈值
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
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里处理
# cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
if data[1] == 1: # 指定了检测区域
x_center = (x1 + x2) / 2
y_center = (y1 + y2) / 2
#绘制中心点?
cv2.circle(image, (int(x_center), int(y_center)), 5, (0, 0, 255), -1)
#判断是否区域点
if not self.is_point_in_region((x_center, y_center)):
continue #没产生报警-继续
#产生报警 -- 有一个符合即可
bwarn = True
warn_text = "People Intruder detected!"
img_dw = draw_bbox(filtered_pred_all, image, (0, 255, 0), 2, labels_dict) # 画出检测框、类别、概率
#cv2.imwrite('img_res.png', img_dw)
return filtered_pred_all, bwarn, warn_text
def testRun(self):
print("1111")

35
run.py

@ -1,29 +1,43 @@
from core.ViewManager import mVManager
from web import create_app
from core.ModelManager import mMM
import os
import platform
import shutil
import queue
import time
import asyncio
import threading
from hypercorn.asyncio import serve
from hypercorn.config import Config
from core.DBManager import mDBM
print(f"Current working directory (run.py): {os.getcwd()}")
def test():
test_q = queue.Queue(maxsize=10)
test_12 = queue.Queue(maxsize=10)
test_q.put("11")
test_q.put("22")
test_12.put("aa")
test_12.put("bb")
q_list = []
q_list.append(test_q)
q_list.append(test_12)
while True:
for i,q in enumerate(q_list):
if q.empty():
continue
print(q.get())
print("执行一次")
time.sleep(1)
web = create_app()
async def run_quart_app():
config = Config()
config.bind = ["0.0.0.0:5001"]
await serve(web, config)
def test():
area_id = 1
c_name = "55"
strsql = f"select ID from channel where area_id={area_id} and channel_name={c_name};"
data = mDBM.do_select(strsql, 1)
if data:
print("有值")
else:
print("无值")
if __name__ == '__main__':
system = platform.system()
@ -41,4 +55,3 @@ if __name__ == '__main__':
asyncio.run(run_quart_app())

170
web/API/channel.py

@ -4,9 +4,6 @@ from . import api
from web.common.utils import login_required
from core.DBManager import mDBM
from myutils.ReManager import mReM
from core.ModelManager import mMM
import cv2
import base64
@api.route('/channel/tree',methods=['GET'])
@login_required
@ -18,15 +15,11 @@ async def channel_tree(): #获取通道树
@api.route('/channel/list',methods=['GET'])
async def channel_list(): #获取通道列表 --分页查询,支持区域和通道名称关键字查询
strsql = ("select t2.area_name,t1.ID,t1.channel_name,t1.ulr,t1.'type',t1.status,t1.element_id,t4.model_name "
"from channel t1 left join area t2 on t1.area_id = t2.id "
"left JOIN channel2model t3 on t1.ID = t3.channel_id "
"left JOIN model t4 on t3.model_id = t4.ID "
"where t1.is_work=1 order by area_name desc;")
strsql = ("select t2.area_name ,t1.ID ,t1.channel_name ,t1.ulr ,t1.'type' ,t1.status ,t1.element_id"
" from channel t1 left join area t2 on t1.area_id = t2.id where t1.is_work=1;")
data = mDBM.do_select(strsql)
channel_list = [{"area_name": channel[0], "ID": channel[1], "channel_name": channel[2], "ulr": channel[3],
"type": channel[4], "status": channel[5],
"element_id": channel[6],"model_name":channel[7]} for channel in data]
"type": channel[4], "status": channel[5], "element_id": channel[6]} for channel in data]
return jsonify(channel_list)
@api.route('/channel/info',methods=['GET'])
@ -36,85 +29,34 @@ async def channel_info(): #获取通道信息 ---- list已获取详情
@api.route('/channel/add',methods=['POST'])
@login_required
async def channel_add(): #新增通道 -- 2024-8-1修改为与修改通道用一个接口
json_data = await request.get_json()
area = json_data.get('area')
cName = json_data.get('cName')
Rtsp = json_data.get('Rtsp')
cid = int(json_data.get('cid'))
if mReM.is_valid_rtsp_url(Rtsp) is not True:
async def channel_add(): #新增通道
area_id = (await request.form)['area_id']
channel_name = (await request.form)['channel_name']
url = (await request.form)['url']
if mReM.is_valid_rtsp_url(url) is not True:
reStatus = 0
reMsg = 'rtsp地址不合法'
return jsonify({'status': reStatus, 'msg': reMsg})
strsql = f"select id from area where area_name = '{area}';"
strsql = f"select area_name from area where id = {area_id};"
ret = mDBM.do_select(strsql,1)
if ret:
strsql = f"select ID from channel where area_id={ret[0]} and channel_name={cName};"
data = mDBM.do_select(strsql, 1)
if data: #有值--代表重复了
reStatus = 0
reMsg = "同一区域内的通道名称不能相同!"
strsql = (f"INSERT INTO channel (area_id,channel_name,ulr,'type',status) values "
f"({area_id},'{channel_name}','{url}',1,0);")
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '添加通道成功'
else:
if cid == -1:
strsql = (f"INSERT INTO channel (area_id,channel_name,ulr,'type',status) values "
f"({ret[0]},'{cName}','{Rtsp}',1,0);")
else:
strsql = (f"UPDATE channel SET area_id={ret[0]},channel_name='{cName}'"
f",ulr='{Rtsp}' where ID={cid};")
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
if cid == -1:
reMsg = '添加通道成功'
# 对新增的视频通道进行视频采集和
strsql = f"select ID from channel where area_id={ret[0]} and channel_name={cName};"
data = mDBM.do_select(strsql, 1)
if data:
cid = data[0]
mMM.start_work(cid)
else:
print("这里不应该没有值!!!!")
else:
reMsg = '修改通道成功'
#需要先停再启动---不区分有没有修改URL了,就当有修改使用
mMM.stop_work(cid)
mMM.start_work(cid)
else:
reStatus = 0
if cid == -1:
reMsg = '添加通道失败,请联系技术支持!'
else:
reMsg = '修改通道信息失败,请联系技术支持!'
reStatus = 0
reMsg = '添加通道失败,请联系技术支持!'
else:
reStatus = 0
reMsg = '错误的区域ID,请修改'
reMsg = '错误的通道ID,请修改'
return jsonify({'status':reStatus,'msg':reMsg})
@api.route('/channel/img',methods=['POST'])
@login_required
async def channel_img():
json_data = await request.get_json()
cid = int(json_data.get('cid'))
channel_data = mMM.verify_list.get_channel(cid)
if channel_data:
# 执行视频传输
ret,frame = channel_data.cap.read()
if ret:
# 将帧转换为JPEG格式
_, buffer = cv2.imencode('.jpg', frame)
# 将图像数据编码为Base64
img_base64 = base64.b64encode(buffer).decode('utf-8')
# 返回JSON响应
return jsonify({"image": img_base64})
else:
return jsonify({"error": "Failed to capture frame"}), 404
else:
return jsonify({"error": "Channel not found"}), 500
@api.route('/channel/change',methods=['POST'])
@login_required
async def channel_change(): #修改通道信息 -- 已弃用
async def channel_change(): #修改通道信息
area_id = (await request.form)['area_id']
channel_id = (await request.form)['channel_id']
channel_name = (await request.form)['channel_name']
@ -142,11 +84,9 @@ async def channel_change(): #修改通道信息 -- 已弃用
@api.route('/channel/del',methods=['POST'])
@login_required
async def channel_del(): #删除通道
json_data = await request.get_json()
cid = int(json_data.get('cid'))
mMM.stop_work(cid)
channel_id = (await request.form)['channel_id']
#删除该通道和算法的关联信息:布防时间,算法模型数据----使用外键级联删除会方便很多,只要一个删除就可以
ret = mDBM.delchannel(cid)
ret = mDBM.delchannel(channel_id)
if ret == True:
reStatus = 1
reMsg = '删除通道信息成'
@ -157,7 +97,7 @@ async def channel_del(): #删除通道
@api.route('/channel/check',methods=['GET'])
@login_required
async def channel_check(): #检查通道视频的在线情况--可以获取全部 ID-0,全部,1*具体通道ID--10分钟会更新一次在线状态-- 要验证
async def channel_check(): #检查通道视频的在线情况--可以获取全部 ID-0,全部,1*具体通道ID--10分钟会更新一次在线状态
ID = request.args.get('ID')
if ID:
ID = int(ID)
@ -178,40 +118,6 @@ async def channel_check(): #检查通道视频的在线情况--可以获取全
reMsg = "参数错误"
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/C2M',methods=['POST'])
@login_required
async def channel_to_model():
'''获取通道关联模型的相关数据'''
json_data = await request.get_json()
cid = int(json_data.get('cid'))
#获取算法数据
strsql = "select ID,model_name from model;"
model_datas = mDBM.do_select(strsql)
m_datas = []
if model_datas:
m_datas = [{'ID':row[0],'model_name':row[1]} for row in model_datas]
#获取c2m数据
strsql = f"select ID,model_id,check_area,polygon,conf_thres,iou_thres from channel2model where channel_id = {cid};"
c2m_data = mDBM.do_select(strsql,1)
#schedule数据
schedule = []
if c2m_data:
c2m_data_json = [{'model_id':c2m_data[1],'check_area':c2m_data[2],'polygon':c2m_data[3],
'conf_thres':c2m_data[4],'iou_thres':c2m_data[5]}]
c2m_id = c2m_data[0]
strsql = f"select day,hour,status from schedule where channel2model_id ={c2m_id} order by hour asc,day asc;"
schedule_datas = mDBM.do_select(strsql)
if schedule_datas:
schedule = [{'day': row[0], 'hour': row[1], 'status': row[2]} for row in schedule_datas]
else:
schedule = []
else:
schedule = []
c2m_data_json = []
#返回数据
redata = {"m_datas":m_datas,"c2m_data":c2m_data_json,"schedule":schedule}
return jsonify(redata)
@api.route('/channel/area/list',methods=['GET'])
@login_required
async def channel_area_list(): #获取区域列表
@ -269,29 +175,29 @@ async def channel_area_add(): #添加区域
reMsg = '新增区域失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/model/list',methods=['GET'])
@api.route('/channel/model/linklist',methods=['GET'])
@login_required
async def channel_model_list(): #获取算法列表
strsql = "select ID,model_name,version from model;"
async def channel_model_linklist(): #获取算法列表 --关联算法时展示
ID = request.args.get('ID') #通道ID
strsql = (f"select t1.ID,t2.model_name from channel2model t1 left join model t2 "
f"on t1.model_id=t2.ID where channel_id={ID};")
datas = mDBM.do_select(strsql)
reMsg = {}
if datas:
reMsg = [{'ID': data[0], 'model_name': data[1],'version':data[2]} for data in datas]
reMsg = [{'ID': data[0], 'model_name': data[1]} for data in datas]
return jsonify(reMsg)
@api.route('/channel/model/linklist',methods=['GET'])
@api.route('/channel/model/list',methods=['GET'])
@login_required
async def channel_model_linklist(): #获取算法列表 --关联算法时展示 --没调用。。
ID = request.args.get('ID') #通道ID
strsql = (f"select t1.ID,t2.model_name from channel2model t1 left join model t2 "
f"on t1.model_id=t2.ID where channel_id={ID};")
async def channel_model_list(): #获取算法列表
strsql = "select ID,model_name,version from model;"
datas = mDBM.do_select(strsql)
reMsg = {}
if datas:
reMsg = [{'ID': data[0], 'model_name': data[1]} for data in datas]
reMsg = [{'ID': data[0], 'model_name': data[1],'version':data[2]} for data in datas]
return jsonify(reMsg)
@api.route('/channel/model/linkmodel',methods=['POST']) #--没调用。。
@api.route('/channel/model/linkmodel',methods=['POST'])
@login_required
async def channel_model_linkmodel(): #获取算法列表 --关联算法时展示 #?关联算法时需要初始化布防计划,同样删除的需要删除
channel_id = (await request.form)['channel_id']
@ -328,7 +234,7 @@ async def channel_model_getarea(): #获取算法区域的备注信息 --- 同时
@api.route('/channel/model/changearea',methods=['POST'])
@login_required
async def channel_model_changearea(): #修改算法检测区域信息
async def channel_model_changearea(): #修改算法区域信息
ID = (await request.form)['ID']
check_area = (await request.form)['check_area']
check_x = (await request.form)['polygon']
@ -337,8 +243,6 @@ async def channel_model_changearea(): #修改算法检测区域信息
if ret == True:
reStatus = 1
reMsg = '修改算法检测区域成功'
#需要重启视频通道的执行程序 --需要cid
#?
else:
reStatus = 0
reMsg = '新修改算法检测区域失败,请联系技术支持!'
@ -346,7 +250,7 @@ async def channel_model_changearea(): #修改算法检测区域信息
@api.route('/channel/model/changethreshold',methods=['POST'])
@login_required
async def channel_model_changthreshold(): #修改算法阈值
async def channel_model_changthreshold(): #修改算法区域信息
ID = (await request.form)['ID']
conf_threshold = (await request.form)['conf_threshold']
@ -354,10 +258,10 @@ async def channel_model_changthreshold(): #修改算法阈值
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '修改算法的阈值成功'
reMsg = '修改算法检测区域成功'
else:
reStatus = 0
reMsg = '新修改算法的阈值失败,请联系技术支持!'
reMsg = '新修改算法检测区域失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/model/getschedule',methods=['GET'])

29
web/API/user.py

@ -1,5 +1,5 @@
import os
from quart import Quart, render_template, request, session, redirect, url_for,jsonify,send_file,flash
from quart import Quart, render_template, request, session, redirect, url_for,jsonify,send_file
from quart_sqlalchemy import SQLAlchemy
from quart_session import Session
from web.common.utils import generate_captcha,login_required
@ -19,23 +19,13 @@ async def user_get_code(): #获取验证码
@api.route('/user/login',methods=['POST'])
async def user_login(): #用户登录
try:
form = await request.form
username = form['username']
password = form['password']
captcha = form['captcha']
except Exception as e:
await flash('请求数据格式错误', 'error')
return redirect(url_for('main.login'))
#return jsonify({'error': '请求数据格式错误'}), 400
username = (await request.form)['username']
password = (await request.form)['password']
captcha = (await request.form)['captcha']
if captcha != session.get('captcha'):
# 验证码验证过后,需要失效
session.pop('captcha', None)
await flash('验证码错误', 'error')
return redirect(url_for('main.login'))
#return jsonify({'error': '验证码错误'}), 400
#return 'captcha error!', 400
#验证码验证过后,需要失效
print(session.get('captcha'))
return '验证码错误', 400
#比对用户名和密码
strsql = f"select password from user where username = '{username}'"
db_password = mDBM.do_select(strsql,1)
@ -43,9 +33,8 @@ async def user_login(): #用户登录
if db_password[0] == password: #后续需要对密码进行MD5加默
print("登录成功")
session['user'] = username
return redirect(url_for('main.get_html', html='view_main.html'))
await flash('用户名或密码错误', 'error')
return redirect(url_for('main.login'))
return redirect(url_for('main.get_html', html='实时预览.html'))
return '用户名或密码错误', 400
@api.route('/user/userinfo',methods=['GET'])
@login_required

104
web/API/viedo.py

@ -144,23 +144,20 @@ async def get_stats(peer_connection):
@api.websocket('/ws/video_feed/<int:channel_id>')
async def ws_video_feed(channel_id):
print(f"New connection for channel: {channel_id}")
channel_data = mMM.verify_list.get_channel(channel_id)
if channel_data:
verify_rate = int(myCongif.get_data("verify_rate"))
frame_interval = 1.0 / verify_rate #用于帧率控制
error_max_count = verify_rate * int(myCongif.get_data("video_error_count")) #视频帧捕获失败触发提示的上限
sleep_time = int(myCongif.get_data("cap_sleep_time"))
last_frame_time = time.time() # 初始化个读帧时间
icount = 0
while channel_data.bool_run: #这里的多线程并发,还需要验证检查
# 帧率控制帧率
last_time=time.time()
frame_interval = 1.0 / int(myCongif.get_data("verify_rate"))
if channel_data is not None:
verify_rate = myCongif.get_data("verify_rate")
while channel_data.bool_run: #这里的多协程并发,还需要验证检查
#控制帧率
current_time = time.time()
elapsed_time = current_time - last_frame_time
elapsed_time = current_time - last_time
if elapsed_time < frame_interval:
await asyncio.sleep(frame_interval - elapsed_time) # 若小于间隔时间则休眠
last_frame_time = time.time()
#执行视频传输
last_time = time.time()
#读取最新的一帧发送
frame = channel_data.get_last_frame()
if frame is not None:
#img = frame.to_ndarray(format="bgr24")
@ -168,20 +165,8 @@ async def ws_video_feed(channel_id):
# if not ret:
# continue
# frame = buffer.tobytes()
icount = 0
await websocket.send(frame)
else:
icount += 1
if icount > error_max_count:
icount = 0
error_message = b"video_error"
await websocket.send(error_message)
await asyncio.sleep(sleep_time*1000) #等待视频重连时间
else:
print("没有通道数据!")
error_message = b"client_error"
await websocket.send(error_message)
await asyncio.sleep(0.1) #等0.1秒前端处理时间
#await asyncio.sleep(1.0 / verify_rate) # Adjust based on frame rate
@api.route('/shutdown', methods=['POST'])
async def shutdown():#这是全关 --需要修改
@ -190,59 +175,24 @@ async def shutdown():#这是全关 --需要修改
pcs.clear()
return 'Server shutting down...'
@api.route('/viewlist', methods=['GET'])
async def viewlist():#视频列表
count = request.args.get('count')
channel_list = []
element_list = []
name_list = []
if count:
strsql = (f"select t1.ID,t1.element_id,t1.channel_name,t2.area_name from channel t1 left join "
f"area t2 on t1.area_id =t2.id where element_id between 0 and {count};")
datas = mDBM.do_select(strsql)
for data in datas:
channel_list.append(data[0])
element_list.append(data[1])
name_list.append(f"{data[3]}--{data[2]}")
return jsonify({'clist': channel_list, 'elist': element_list,'nlist':name_list})
@api.route('/start_stream', methods=['POST'])
async def start_stream(): #开启视频通道,把视频通道编号和元素编号进行关联
json_data = await request.get_json()
channel_id = json_data.get('channel_id')
element_id = json_data.get('element_id')
reStatus = 0
reMsg = ""
strsql = f"select element_id from channel where ID = {channel_id};"
data = mDBM.do_select(strsql,1)
if data:
if data[0] == '':
strsql = f"update channel set element_id = '{element_id}' where ID={channel_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '播放视频配置成功!'
else:
reMsg = '播放视频配置失败,请联系技术支持!'
else:
index = int(data[0]) +1
reMsg = f"该视频通道已在:Video Stream {index}播放,请先关闭"
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/close_stream', methods=['POST'])
async def close_stream(): #关闭视频通道
json_data = await request.get_json()
element_id = json_data.get('element_id')
async def close_stream(): # 需要修改
channel_id = (await request.form)['channel_id']
reStatus = 0
reMsg =""
#?关闭视频播放--业务逻辑-待确认后端是否需要执行
#数据库中该通道的关联关系要清除
strsql = f"update channel set element_id = '' where element_id={element_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '关闭画面成功!'
if channel_id in pcs:
await pcs[channel_id].close()
pcs.pop(channel_id, None)
print(f'Stream {channel_id} closed.')
#数据库中该通道的关联关系要清除
strsql = f"update channel set element_id = NULL where ID={channel_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '关闭画面成功!'
else:
reStatus = 0
reMsg = '删除通道与组件关联关系失败,请联系技术支持!'
else:
reMsg = '删除通道与组件关联关系失败,请联系技术支持!'
reMsg = "通道编号不在系统内,请检查!"
return jsonify({'status': reStatus, 'msg': reMsg})

2
web/__init__.py

@ -1,6 +1,5 @@
from quart import Quart,session,redirect, url_for
from quart_session import Session
from quart_cors import cors
from pymemcache.client import base
from .main import main
from .API import api
@ -32,7 +31,6 @@ class MemcachedSessionInterface: #只是能用,不明所以
def create_app():
app = Quart(__name__)
#app = cors(app, allow_credentials=True) #allow_origin:指定允许跨域访问的来源
#相关配置--设置各种配置选项,这些选项会在整个应用程序中被访问和使用。
# app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

2
web/main/__init__.py

@ -1,5 +1,5 @@
from quart import Blueprint
main = Blueprint('main', __name__,static_folder='static/resources',template_folder='templates')
main = Blueprint('main', __name__,template_folder='templates')
from . import routes

23
web/main/routes.py

@ -14,6 +14,7 @@ from werkzeug.utils import secure_filename
def login_required(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
print("decorated_function3")
if 'user' not in session:
return redirect(url_for('main.index',error='未登录,请重新登录'))
return await f(*args, **kwargs)
@ -21,27 +22,11 @@ def login_required(f):
@main.route('/')
async def index():
#return await render_template('实时预览.html')
return await render_template('login.html')
#error = request.args.get('error')
return await render_template('实时预览.html')
#return await render_template('登录.html',error=error)
#return await render_template('index_webrtc.html')
@main.route('/login', methods=['GET', 'POST'])
async def login():
if request.method == 'POST':
form = await request.form
username = form.get('username')
password = form.get('password')
# Add your login logic here
if username == 'admin' and password == 'password':
return redirect(url_for('main.dashboard')) # Assuming you have a dashboard route
else:
return "Invalid credentials", 401
return await render_template('login.html')
@main.route('/dashboard')
async def dashboard():
return "Welcome to the dashboard!"
# @main.route('/', methods=['GET', 'POST'])
# async def upload_file():
# if request.method == 'POST':

BIN
web/main/static/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
web/main/static/images/登录/zf.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

26
web/main/static/images/登录/zf.svg

@ -1,26 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="410.000000pt" height="410.000000pt" viewBox="0 0 410.000000 410.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,410.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1815 3864 c-254 -30 -508 -114 -724 -241 -103 -60 -231 -153 -296
-215 -22 -21 -43 -38 -47 -38 -13 0 -187 -201 -240 -278 -160 -230 -258 -471
-309 -762 -18 -102 -18 -500 -1 -600 35 -194 84 -354 156 -506 254 -534 768
-909 1376 -1004 142 -23 426 -27 557 -9 598 81 1142 456 1394 959 152 304 207
565 196 920 -23 713 -407 1305 -1042 1605 -264 125 -502 176 -815 173 -96 0
-188 -3 -205 -4z m1320 -1023 c-11 -9 -236 -182 -356 -273 -42 -32 -103 -90
-135 -130 -32 -40 -90 -107 -129 -148 -38 -42 -137 -150 -220 -240 -82 -91
-186 -203 -231 -249 l-82 -85 -121 -4 -121 -4 -78 -89 c-78 -90 -142 -145
-352 -298 -169 -124 -451 -313 -457 -307 -7 6 27 55 91 131 71 83 378 419 500
545 311 324 728 767 742 791 12 19 253 137 397 193 131 51 325 115 452 150
102 28 113 30 100 17z m-1065 -89 c44 -22 62 -56 56 -106 -6 -47 -40 -85 -87
-94 -19 -4 -201 -7 -406 -7 l-372 0 -30 29 c-57 55 -50 128 18 173 34 23 35
23 410 23 338 0 380 -2 411 -18z m402 -1202 l330 0 34 -34 c29 -29 34 -41 34
-81 0 -42 -5 -52 -36 -81 l-35 -34 -385 0 -384 0 -38 34 c-32 30 -37 40 -37
80 0 38 5 51 31 77 34 34 82 50 128 43 16 -2 177 -4 358 -4z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

15
web/main/static/resources/css/headers.css

@ -1,15 +0,0 @@
.form-control-dark {
border-color: var(--bs-gray);
}
.form-control-dark:focus {
border-color: #fff;
box-shadow: 0 0 0 .25rem rgba(255, 255, 255, .25);
}
.text-small {
font-size: 85%;
}
.dropdown-toggle {
outline: 0;
}

33
web/main/static/resources/css/sign-in.css

@ -1,33 +0,0 @@
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
max-width: 400px;
padding: 15px;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: 5px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}

BIN
web/main/static/resources/images/zf.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

26
web/main/static/resources/images/zf.svg

@ -1,26 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="410.000000pt" height="410.000000pt" viewBox="0 0 410.000000 410.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,410.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1815 3864 c-254 -30 -508 -114 -724 -241 -103 -60 -231 -153 -296
-215 -22 -21 -43 -38 -47 -38 -13 0 -187 -201 -240 -278 -160 -230 -258 -471
-309 -762 -18 -102 -18 -500 -1 -600 35 -194 84 -354 156 -506 254 -534 768
-909 1376 -1004 142 -23 426 -27 557 -9 598 81 1142 456 1394 959 152 304 207
565 196 920 -23 713 -407 1305 -1042 1605 -264 125 -502 176 -815 173 -96 0
-188 -3 -205 -4z m1320 -1023 c-11 -9 -236 -182 -356 -273 -42 -32 -103 -90
-135 -130 -32 -40 -90 -107 -129 -148 -38 -42 -137 -150 -220 -240 -82 -91
-186 -203 -231 -249 l-82 -85 -121 -4 -121 -4 -78 -89 c-78 -90 -142 -145
-352 -298 -169 -124 -451 -313 -457 -307 -7 6 27 55 91 131 71 83 378 419 500
545 311 324 728 767 742 791 12 19 253 137 397 193 131 51 325 115 452 150
102 28 113 30 100 17z m-1065 -89 c44 -22 62 -56 56 -106 -6 -47 -40 -85 -87
-94 -19 -4 -201 -7 -406 -7 l-372 0 -30 29 c-57 55 -50 128 18 173 34 23 35
23 410 23 338 0 380 -2 411 -18z m402 -1202 l330 0 34 -34 c29 -29 34 -41 34
-81 0 -42 -5 -52 -36 -81 l-35 -34 -385 0 -384 0 -38 34 c-32 30 -37 40 -37
80 0 38 5 51 31 77 34 34 82 50 128 43 16 -2 177 -4 358 -4z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

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

@ -1,12 +1,9 @@
let video_list = {}; //element_id -- socket
let run_list = {}; //element_id -- runtag(替换berror)
var pc_list = {};
var channel_list = null;
const fourViewButton = document.getElementById('fourView');
const nineViewButton = document.getElementById('nineView');
document.addEventListener('DOMContentLoaded', async function() {
console.log('DOM fully loaded and parsed');
// 发送请求获取额外数据 --- 这个接口功能有点大了---暂时只是更新通道树2024-7-29
// 发送请求获取额外数据
try {
let response = await fetch('/api/channel/list');
if (!response.ok) {
@ -14,310 +11,50 @@ document.addEventListener('DOMContentLoaded', async function() {
}
channel_list = await response.json();
// 遍历输出每个元素的信息
let area_name = ""
let html = '<ul class="list-group">';
channel_list.forEach(channel => {
// console.log(`Area Name: ${channel.area_name}`);
// console.log(`ID: ${channel.ID}`);
// console.log(`Channel Name: ${channel.channel_name}`);
// console.log(`URL: ${channel.url}`);
// console.log(`Type: ${channel.type}`);
// console.log(`Status: ${channel.status}`);
// console.log(`Element ID: ${channel.element_id}`);
if(area_name !== `${channel.area_name}`){
if(area_name !== ""){
html += '</ul>';
html += '</li>';
}
area_name = `${channel.area_name}`;
html += `<li class="list-group-item"><strong>${area_name}</strong>`;
html += '<ul class="list-group">';
if(channel.element_id){ //""空为false,非空为true
console.log(`Area Name: ${channel.area_name}`);
console.log(`ID: ${channel.ID}`);
console.log(`Channel Name: ${channel.channel_name}`);
console.log(`URL: ${channel.url}`);
console.log(`Type: ${channel.type}`);
console.log(`Status: ${channel.status}`);
console.log(`Element ID: ${channel.element_id}`);
connectToStream(channel.element_id,channel.ID,channel.area_name,channel.channel_name)
}
//html += `<li class="list-group-item">${channel.channel_name}</li>`;
html += `<li class="list-group-item" draggable="true" ondragstart="drag(event)"
data-node-id="${channel.ID}" data-node-name="${area_name}--${channel.channel_name}">
<svg class="bi" width="16" height="16"><use xlink:href="#view"/></svg>
${channel.channel_name}
</li>`;
});
if(area_name !== ""){
html += '</ul>';
html += '</li>';
}
html += '</ul>';
const treeView = document.getElementById('treeView');
treeView.innerHTML = html
generateVideoNodes(4);
} catch (error) {
console.error('Failed to fetch data:', error);
}
});
//视频窗口
document.getElementById('fourView').addEventListener('click', function() {
if (fourViewButton.classList.contains('btn-primary')) {
return; // 如果按钮已经是选中状态,直接返回
}
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('nine');
videoGrid.classList.add('four');
generateVideoNodes(4);
//更新按钮点击状态
fourViewButton.classList.remove('btn-secondary');
fourViewButton.classList.add('btn-primary');
nineViewButton.classList.remove('btn-primary');
nineViewButton.classList.add('btn-secondary');
});
document.getElementById('nineView').addEventListener('click', function() {
if (nineViewButton.classList.contains('btn-primary')) {
return; // 如果按钮已经是选中状态,直接返回
}
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('four');
videoGrid.classList.add('nine');
generateVideoNodes(9);
//更新按钮点击状态
fourViewButton.classList.remove('btn-primary');
fourViewButton.classList.add('btn-secondary');
nineViewButton.classList.remove('btn-secondary');
nineViewButton.classList.add('btn-primary');
});
function generateVideoNodes(count) { //在这里显示视频-初始化
//结束在播放的socket
for(let key in video_list){
delete run_list[key];
video_list[key].close();
delete video_list[key];
}
//切换窗口布局
const videoGrid = document.getElementById('videoGrid');
let html = '';
for (let i = 0; i < count; i++) {
let frameWidth = count === 4 ? 'calc(50% - 10px)' : 'calc(33.33% - 10px)';
html += `
<div class="video-frame" data-frame-id="${i}" style="width: ${frameWidth};"
ondrop="drop(event)" ondragover="allowDrop(event)">
<div class="video-header">
<div class="video-title">Video Stream ${i+1}</div>
<div class="video-buttons">
<button onclick="toggleFullScreen(${i})">🔲</button>
<button onclick="closeVideo(${i})"></button>
</div>
</div>
<div class="video-area"><img id="video-${i}" alt="Video Stream" /></div>
</div>`;
}
videoGrid.innerHTML = html;
//获取视频接口
const url = `/api/viewlist?count=${count}`;
fetch(url)
.then(response => response.json())
.then(data => {
console.log('Success:', data);
clist = data.clist;
elist = data.elist;
nlist = data.nlist;
for(let i=0;i<clist.length;i++){
if(parseInt(elist[i]) < count){
connectToStream(elist[i],clist[i],nlist[i])
}
}
})
.catch(error => {
console.error('Error:', error);
});
}
function toggleFullScreen(id) {
console.log('toggleFullScreen');
const videoFrame = document.querySelector(`[data-frame-id="${id}"]`);
if (!document.fullscreenElement) {
videoFrame.requestFullscreen().catch(err => {
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
});
} else {
document.exitFullscreen();
};
}
function closeVideo(id) {
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
if (titleElement.textContent === `Video Stream ${Number(id)+1}`) {
showModal('当前视频窗口未播放视频。');
return;
};
console.log('closeVideo');
//发送视频链接接口
const url = '/api/close_stream';
const data = {"element_id":id};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
console.log('Success:', data);
const istatus = data.status;
if(istatus === 0){
showModal(data.msg); // 使用 Modal 显示消息
return;
}
else{
const videoFrame = document.querySelector(`[data-frame-id="${id}"] .video-area img`);
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
run_list[id] = false;
video_list[id].close();
videoFrame.src = ''; // 清空画面
videoFrame.style.display = 'none'; // 停止播放时隐藏 img 元素
titleElement.textContent = `Video Stream ${id+1}`;
removeErrorMessage(videoFrame)
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
}
function allowDrop(event) {
event.preventDefault();
}
function drag(event) {
event.dataTransfer.setData("text", event.target.dataset.nodeId);
event.dataTransfer.setData("name", event.target.dataset.nodeName);
}
function drop(event) {
event.preventDefault();
const nodeId = event.dataTransfer.getData("text");
const nodeName = event.dataTransfer.getData("name");
const frameId = event.currentTarget.dataset.frameId;
//需要判断下当前窗口是否已经在播放视频
const imgElement = document.getElementById(`video-${frameId}`);
const titleElement = document.querySelector(`[data-frame-id="${frameId}"] .video-title`);
if (titleElement.textContent !== `Video Stream ${Number(frameId)+1}`) {
showModal('请先关闭当前窗口视频,然后再播放新的视频。');
return;
};
//发送视频链接接口
const url = '/api/start_stream';
const data = {"channel_id":nodeId,"element_id":frameId};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const istatus = data.status;
if(istatus === 0){
showModal(data.msg); // 使用 Modal 显示消息
return;
}
else{
//获取视频流
connectToStream(frameId,nodeId,nodeName);
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
//console.log('retrun 只是把fetch结束,这里的代码还是会执行');
}
function connectToStream(element_id,channel_id,channel_name) {
console.log("开始连接视频",channel_id);
// 设置视频区域的标题
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`);
titleElement.textContent = channel_name;
//获取视频
const imgElement = document.getElementById(`video-${element_id}`);
imgElement.alt = `Stream ${channel_name}`;
const streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`;
let berror = false;
function connect() {
const socket = new WebSocket(streamUrl);
video_list[element_id] = socket;
run_list[element_id] = true;
// 处理连接打开事件
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onmessage = function(event) {
// 释放旧的对象URL---需要吗?
// if (imgElement.src) {
// URL.revokeObjectURL(imgElement.src);
// }
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
const decoder = new TextDecoder("utf-8");
const decodedData = decoder.decode(arrayBuffer);
function connectToStream(element_id,channel_id,area_name,channel_name) {
const imgElement = document.getElementById(element_id);
imgElement.alt = `Stream ${area_name}--${channel_name}`;
const streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`;
console.log(streamUrl);
function connect() {
const socket = new WebSocket(streamUrl);
socket.onmessage = function(event) {
//const blob = new Blob([event.data], { type: 'image/jpeg' });
//imgElement.src = URL.createObjectURL(blob);
// 释放旧的对象URL
if (imgElement.src) {
URL.revokeObjectURL(imgElement.src);
}
imgElement.src = URL.createObjectURL(event.data);
};
if (decodedData === "video_error") { //video_error
displayErrorMessage(imgElement, "该视频源未获取到画面,请检查,默认5分钟后重新链接视频源。");
} else if(decodedData === "client_error"){ //client_error
run_list[element_id] = false;
displayErrorMessage(imgElement, "该通道节点数据存在问题,请重启或联系技术支持!");
socket.close(); // 停止连接
}
else {
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
const imageUrl = URL.createObjectURL(blob);
imgElement.src = imageUrl;
imgElement.style.display = 'block';
// imgElement.src = URL.createObjectURL(result);
// imgElement.style.display = 'block';
}
};
reader.readAsArrayBuffer(event.data);
};
socket.onclose = function() {
setTimeout(connect, 1000); // 尝试在1秒后重新连接
};
socket.onclose = function() {
if(run_list[element_id]){
console.log(`尝试重新连接... Channel ID: ${channel_id}`);
setTimeout(connect, 1000*10); // 尝试在10秒后重新连接
socket.onerror = function() {
console.log(`WebSocket错误,尝试重新连接... Channel ID: ${channel_id}`);
socket.close();
};
}
};
socket.onerror = function() {
console.log(`WebSocket错误,Channel ID: ${channel_id}`);
socket.close();
};
};
connect();
}
function displayErrorMessage(imgElement, message) {
removeErrorMessage(imgElement)
imgElement.style.display = 'none'; // 隐藏图片
const errorElement = document.createElement('div');
errorElement.textContent = message;
errorElement.classList.add('error-message');
imgElement.parentNode.appendChild(errorElement);
}
function removeErrorMessage(imgElement) {
const errorElement = imgElement.parentNode.querySelector('.error-message');
if (errorElement) {
imgElement.parentNode.removeChild(errorElement);
}
}
connect();
}

47
web/main/static/resources/scripts/base.js

@ -1,47 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
const links = document.querySelectorAll('.nav-pills .nav-link');
// Get the current page path
const currentPath = window.location.pathname;
links.forEach(link => {
// Compare the link's href with the current path
if (link.href.endsWith(currentPath)) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
});
function showModal(message) {
// 设置 Modal 中的消息
document.getElementById('modalMessage').innerText = message;
// 显示 Modal
const responseModal = new bootstrap.Modal(document.getElementById('responseModal'));
responseModal.show();
}
//动态填充select控件
function set_select_data(select_ele_id,datas){
const select_Ele = document.getElementById(select_ele_id);
//清空老数据
select_Ele.innerHTML = '';
//添加列表
datas.forEach(option => {
const optionElement = document.createElement('option');
optionElement.textContent = option;
select_Ele.appendChild(optionElement);
});
}
//设定选项选中状态
function set_select_selct(select_ele_id,option_str){
const select_Ele = document.getElementById(select_ele_id);
for(let i=0;i< select_Ele.options.length;i++){
if(select_Ele.options[i].value === option_str){
select_Ele.options[i].selected = true;
break;
}
}
}

608
web/main/static/resources/scripts/channel_manager.js

@ -1,608 +0,0 @@
const apiEndpoint = '/api/channel/list';
const rowsPerPage = 10;
//算法配置窗口部分控件
const searchEndpoint = '/api/channel/select';
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const backgroundCanvas = document.getElementById('backgroundCanvas');
const backgroundCtx = backgroundCanvas.getContext('2d');
const img = new Image();
const tbody = document.getElementById('schedule-body');//布防计划
let currentPage = 1;
let channelData = [];
let channelData_bak = [];
let areaData = ["请选择"];
let currentEditingRow = null;
let cid_schedule = "-1";
let m_polygon = "";
let check_area = 0;
let draw_status = false; //是否是绘制状态,处于绘制状态才能开始绘制
let b_img = false; //有没有加载图片成功,如果没有初始化的时候就不绘制线条了。
let points = [];
document.addEventListener('DOMContentLoaded', function () {
fetchChannelData(); //初始化页面元素数据
document.getElementById('searchButton').addEventListener('click', function () {
performSearch();
});
//新增通道
document.getElementById('saveButton').addEventListener('click', function () {
addChannel(1);
});
//修改通道
document.getElementById('saveButton_cc').addEventListener('click', function () {
addChannel(2);
});
//算法配置
document.getElementById('cancelButton_mx').addEventListener('click', function () {
close_mx_model();
});
//保存算法配置
document.getElementById('saveButton_mx').addEventListener('click', function () {
save_mx_model();
});
//开始绘制区域按钮
document.getElementById('but_hzqy').addEventListener('click', function () {
startDraw();
});
});
//添加和修改通道 1--新增,2--修改
function addChannel(itype) {
let area;
let cName;
let Rtsp;
let cid;
if(itype ==1){
const saveButton = document.getElementById('saveButton');
const CNameInput = document.getElementById('CNameInput');
const RTSPInput = document.getElementById('RTSPInput');
area = document.getElementById('areaSelect_M').value;
cName = CNameInput.value.trim();
Rtsp = RTSPInput.value.trim();
cid = -1
}
else if(itype ==2){
const saveButton = document.getElementById('saveButton_cc');
const CNameInput = document.getElementById('CNameInput_cc');
const RTSPInput = document.getElementById('RTSPInput_cc');
area = document.getElementById('areaSelect_CC').value;
cName = CNameInput.value.trim();
Rtsp = RTSPInput.value.trim();
cid = currentEditingRow.cells[0].innerText;
}
if(area === "请选择"){
alert('请选择所属区域');
}
else{
if (cName && Rtsp) {
saveButton.disabled = true;
//发送视频链接接口
const url = '/api/channel/add';
const data = {"area":area,"cName":cName,"Rtsp":Rtsp,"cid":cid};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const istatus = data.status;
if(istatus === 0){
alert(data.msg); // 使用 Modal 显示消息
// 启用保存按钮
saveButton.disabled = false;
return;
}
else{
// 启用保存按钮
saveButton.disabled = false;
//刷新列表
fetchChannelData();
if(itype ==1){
//添加通道成功
$('#channelModal').modal('hide');
alert("添加通道成功!"); // 使用 Modal 显示消息
}
else if(itype==2){
//修改通道成功
currentEditingRow = null;
$('#ChangeC').modal('hide');
alert("修改通道成功!"); // 使用 Modal 显示消息
}
}
})
.catch((error) => {
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
// 启用保存按钮
saveButton.disabled = false;
return;
});
} else {
alert('通道名称和RTSP地址不能为空');
}
}
}
async function fetchChannelData() { //刷新通道数据
try {
const response = await fetch(apiEndpoint);
channelData = await response.json();
channelData_bak = channelData;
renderTable(); //读取通道list接口,刷新表格
renderPagination(); //刷新分页元素
renderAreaOptions(); //所属区域下来框
} catch (error) {
console.error('Error fetching channel data:', error);
}
}
//关键字查询数据
async function performSearch() {
try {
const area = document.getElementById('areaSelect').value;
const channelName = document.getElementById('channelNameInput').value;
if(area === "请选择" && channelName===""){
channelData = channelData_bak;
}
else if(area === "请选择"){
channelData = [];
channelData_bak.forEach((channel) => {
if(channelName === channel.channel_name){
channelData.push(channel);
}
});
}
else if(channelName === ""){
channelData = [];
channelData_bak.forEach((channel) => {
if(area === channel.area_name){
channelData.push(channel);
}
});
}
else{
channelData = [];
channelData_bak.forEach((channel) => {
if(area === channel.area_name && channelName === channel.channel_name){
channelData.push(channel);
}
});
}
// 渲染表格和分页控件
currentPage = 1; // 重置当前页为第一页
renderTable();
renderPagination();
} catch (error) {
console.error('Error performing search:', error);
}
}
//刷新表单页面数据
function renderTable() {
const tableBody = document.getElementById('table-body');
tableBody.innerHTML = '';
const start = (currentPage - 1) * rowsPerPage;
const end = start + rowsPerPage;
const pageData = channelData.slice(start, end);
const surplus_count = rowsPerPage - pageData.length;
let area_name = "";
pageData.forEach((channel) => {
if(area_name!==channel.area_name){ //这里要求区域名称一样的要在一起
area_name = channel.area_name;
areaData.push(area_name);
}
const row = document.createElement('tr');
row.innerHTML = `
<td>${channel.ID}</td>
<td>${channel.area_name}</td>
<td>${channel.channel_name}</td>
<td>${channel.ulr}</td>
<td>${channel.model_name}</td>
<td>
<button class="btn btn-primary btn-sm modify-btn">修改</button>
<button class="btn btn-secondary btn-sm algorithm-btn">算法</button>
<button class="btn btn-danger btn-sm delete-btn">删除</button>
</td>
`;
tableBody.appendChild(row);
row.querySelector('.modify-btn').addEventListener('click', () => modifyChannel(row));
row.querySelector('.algorithm-btn').addEventListener('click', () => configureAlgorithm(row));
row.querySelector('.delete-btn').addEventListener('click', () => deleteChannel(row));
});
}
//修改通道信息
function modifyChannel(row) {
// const cid = row.cells[0].innerText;
const areaName = row.cells[1].innerText;
const channelName = row.cells[2].innerText;
const url = row.cells[3].innerText;
const area = document.getElementById('areaSelect_CC');
const CName = document.getElementById('CNameInput_cc');
const RTSP = document.getElementById('RTSPInput_cc');
for(let i=0;i< area.options.length;i++){
if(area.options[i].value === areaName){
area.options[i].selected = true;
break;
}
}
CName.value = channelName;
RTSP.value = url;
currentEditingRow = row;
$('#ChangeC').modal('show');
}
//算法配置 -- 点击算法按钮
function configureAlgorithm(row) {
//获取当前行信息
currentEditingRow = row;
const cid = row.cells[0].innerText;
//清除数据,若需要的话
ctx.clearRect(0, 0, canvas.width, canvas.height); //清除左侧绘画和画线信息
tbody.innerHTML = ''; //清空布防控件数据
points = []; //清空绘制检测区域
draw_status = false;
b_img = false;
document.getElementById('but_hzqy').textContent = "绘制区域";
//开始初始化算法管理模块
show_channel_img(cid); //显示一帧图片 -- 获取不到图片就是黑画面
show_channel_model_schedule(cid); //显示结构化数据
//显示窗口
$('#MX_M').modal('show');
}
//获取一帧图片
function show_channel_img(cid){
const data = {"cid":cid};
fetch('/api/channel/img', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.image) {
b_img = true;
img.src = 'data:image/jpeg;base64,' + data.image;
} else {
console.error('Error:', data.error);
}
})
.catch(error => console.error('Error:', error));
}
//图片加载事项
img.onload = () => { //清除、画图和画线应该分开
// 设置画布宽高
backgroundCanvas.width = canvas.width = img.width;
backgroundCanvas.height = canvas.height = img.height;
// 将图片绘制到背景画布上
backgroundCtx.drawImage(img, 0, 0, img.width, img.height);
// 将背景画布的内容复制到前台画布上
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); //绘制画面
};
//开始和重新绘制
function startDraw(){
if(!document.getElementById('zdjc').checked){
alert("请先选择指定区域!");
return;
}
let but = document.getElementById('but_hzqy');
if(!draw_status){//开始绘制
if(points.length >0){
if (confirm('开始绘制将清除未提交保存的绘制数据,是否继续?')) {
draw_status = true;
points = [];
//按钮文字调整为结束绘制
but.textContent = '结 束 绘 制';
// 清除前台画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 将背景画布的内容复制到前台画布上
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height);
}
}
else{
draw_status = true;
but.textContent = '结束绘制';
}
}
else{//结束绘制
draw_status = false;
but.textContent = '绘制区域';
}
}
//单选按钮点击事件处理
function handleRadioClick(event) {
const selectedRadio = event.target;
console.log('Selected Radio:', selectedRadio.id);
// 根据选中的单选按钮执行相应操作
if (selectedRadio.id === 'qjjc') {
console.log("points.length",points.length);
// 处理全画面生效的逻辑
if(points.length>0){
if (!confirm('已经绘制了检测区域,确认要切换到全画面生效吗?')) {
document.getElementById('zdjc').checked = true;
}
}
console.log('全画面生效');
} else if (selectedRadio.id === 'zdjc') {
// 处理指定区域的逻辑
console.log('指定区域');
}
}
// 鼠标点击事件处理--动态绘图
canvas.addEventListener('click', (event) => {
if(draw_status){
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
// 获取鼠标相对于canvas的位置
const x = (event.clientX - rect.left) * scaleX;
const y = (event.clientY - rect.top) * scaleY;
points.push({ x, y });
//绘制线条
drawLines();
}
});
//初始化读取该视频通道与算法配置的相关信息 --- 这里用GET会更加贴切一些
function show_channel_model_schedule(cid){
//发送视频链接接口
const url = '/api/channel/C2M';
const data = {"cid":cid};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const m_datas = data.m_datas;
const c2m_data = data.c2m_data;
const schedule = data.schedule;
//console.log("m_datas--",m_datas);
//console.log("c2m_data--",c2m_data);
//console.log("schedule--",schedule);
//配置算法下拉清单
select_datas = ["请选择"];
m_datas.forEach(option => {
select_datas.push(option.model_name);
});
set_select_data("model_select",select_datas);
select_str = currentEditingRow.cells[4].innerText;
//检测区域
if(c2m_data.length >0){
model_id = c2m_data[0].model_id;
model_name = currentEditingRow.cells[4].innerText;
set_select_selct("model_select",model_name);
check_area = c2m_data[0].check_area
if( check_area == 0){ //全画面生效
document.getElementById('qjjc').checked = true;
m_polygon = "";
}
else{//指定区域
document.getElementById('zdjc').checked = true;
m_polygon = c2m_data[0].polygon;
if(m_polygon !== ""){ //指定区域了,一般是会有数据的。
const coords = parseCoordStr(m_polygon);
points = coords;
drawLines();
}
}
console.log("m_polygon",m_polygon);
//阈值
document.getElementById('zxyz').value = c2m_data[0].conf_thres
document.getElementById('iouyz').value = c2m_data[0].iou_thres
}
const days = ['一', '二', '三', '四', '五', '六','日'];
const num_days=['0','1','2','3','4','5','6']
days.forEach((day, dayIndex) => {
const row = document.createElement('tr');
const dayCell = document.createElement('th');
dayCell.textContent = day;
row.appendChild(dayCell);
num_day = num_days[dayIndex]
for (let hour = 0; hour < 24; hour++) {
const cell = document.createElement('td');
if(schedule.length >0){
const status = schedule.find(item => item.day === num_day && item.hour === hour);
if (status && status.status === 1) {
cell.classList.add('blocked');
} else {
cell.classList.add('allowed');
}
}
else{
cell.classList.add('blocked');
}
row.appendChild(cell);
cell.addEventListener('click', () => {
if (cell.classList.contains('blocked')) {
cell.classList.remove('blocked');
cell.classList.add('allowed');
// Update status in the database
//updateStatus(day, hour, 0);
} else {
cell.classList.remove('allowed');
cell.classList.add('blocked');
// Update status in the database
//updateStatus(day, hour, 1);
}
});
}
tbody.appendChild(row);
});
})
.catch((error) => {
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
}
// 将字符串转换为数组
function parseCoordStr(str) {
return str.match(/\(([^)]+)\)/g).map(pair => {
const [x, y] = pair.replace(/[()]/g, '').split(',').map(Number);
return { x, y };
});
}
// 绘制区域,各点连接
function drawLines() {
if(b_img){
// 清除前台画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 将背景画布的内容复制到前台画布上
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height);
// 绘制点和线
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
if (points.length > 0) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
// 连接最后一个点到起点
ctx.lineTo(points[0].x, points[0].y);
ctx.stroke();
}
points.forEach(point => {
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
});
}
}
//关闭算法配置窗口
function close_mx_model(){
if (confirm('确定退出窗口吗?未保存的修改将丢失!')) {
$('#MX_M').modal('hide');
}
}
//保存算法配置窗口数据
function save_mx_model(){
//?
currentEditingRow =null;
}
//删除通道
function deleteChannel(row) {
if (confirm('确定删除此区域吗?')) {
cid = row.cells[0].innerText;
//发送视频链接接口
const url = '/api/channel/del';
const data = {"cid":cid};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const istatus = data.status;
if(istatus === 0){
alert(data.msg); // 使用 Modal 显示消息
// 启用保存按钮
saveButton.disabled = false;
return;
}
else{
// 启用保存按钮
saveButton.disabled = false;
//刷新列表
row.remove();
alert("删除通道成功!");
}
})
.catch((error) => {
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
// 启用保存按钮
saveButton.disabled = false;
return;
});
}
}
//刷新分页标签
function renderPagination() {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
const totalPages = Math.ceil(channelData.length / rowsPerPage);
for (let i = 1; i <= totalPages; i++) {
const pageItem = document.createElement('li');
pageItem.className = 'page-item' + (i === currentPage ? ' active' : '');
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`;
pageItem.addEventListener('click', (event) => {
event.preventDefault();
currentPage = i;
renderTable();
renderPagination();
});
pagination.appendChild(pageItem);
}
}
//刷新区域下拉控件
function renderAreaOptions() {
const areaSelect = document.getElementById('areaSelect');
const areaSelect_M = document.getElementById('areaSelect_M')
const areaSelect_CC = document.getElementById('areaSelect_CC')
areaData.forEach(option => {
const optionElement = document.createElement('option');
optionElement.textContent = option;
areaSelect.appendChild(optionElement);
const optionElement_m = document.createElement('option');
optionElement_m.textContent = option;
areaSelect_M.appendChild(optionElement_m);
const optionElement_cc = document.createElement('option');
optionElement_cc.textContent = option;
areaSelect_CC.appendChild(optionElement_cc);
});
}

4
web/main/static/resources/scripts/jquery-3.2.1.slim.min.js

File diff suppressed because one or more lines are too long

5
web/main/static/resources/scripts/popper.min.js

File diff suppressed because one or more lines are too long

46
web/main/templates/base.html

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Website{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<link href="{{ url_for('main.static', filename='css/headers.css') }}" rel="stylesheet">
<style>
.bi {
vertical-align: -.125em;
fill: currentColor;
}
{% block style %}{% endblock %}
</style>
</head>
<body>
{% include 'header.html' %}
<!-- Modal Structure -->
<div class="modal fade" id="responseModal" tabindex="-1" aria-labelledby="responseModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="responseModalLabel">提示信息</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="modalMessage">
<!-- The message from the server will be inserted here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<main class="p-0 mb-0">
{% block content %}{% endblock %}
</main>
{% include 'footer.html' %}
<script src="{{ url_for('main.static', filename='scripts/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/base.js') }}"></script>
{% block script %}{% endblock %}
</body>
</html>

291
web/main/templates/channel_manager.html

@ -1,291 +0,0 @@
{% extends 'base.html' %}
{% block title %}ZFBOX{% endblock %}
{% block style %}
.pagination {
justify-content: flex-end; /* 右对齐 */
}
.page-item .page-link {
padding: 0.25rem 0.5rem; /* 缩小按钮 */
font-size: 0.875rem; /* 调整字体大小 */
}
.btn-group-sm > .btn, .btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
}
.btn-group-sm .btn {
margin-right: 5px; /* 按钮之间的间距 */
}
.form-group-right h5 {
text-align: right;
margin-bottom: 0;
}
.table-container {
min-height: 400px; /* 设置最小高度,可以根据需要调整 */
}
.video-area {
width: 100%;
position: relative;
background-color: #000;
border: 1px solid #ddd;
}
.video-area::before {
content: "";
display: block;
padding-top: 75%; /* 4:3 ratio */
}
.video-area canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
.schedule-table {
width: 100%;
table-layout: fixed;
}
.schedule-table th, .schedule-table td {
text-align: center;
vertical-align: middle;
cursor: pointer;
border: 1px solid #dee2e6;
padding: 8px;
}
.schedule-table td.allowed {
background-color: white;
}
.schedule-table td.blocked {
background-color: blue;
}
/* 缩小表格行高 */
.table-sm th,
.table-sm td {
padding: 0.2rem; /* 调整这里的值来改变行高 */
}
canvas {
border: 1px solid black;
}
{% endblock %}
{% block content %}
<div class="container d-flex flex-column" >
<div class="row mb-3 d-flex justify-content-center align-items-center ">
<div class="col"><h5 class="form-group-right">所属区域:</h5></div>
<div class="col-3 mr-2">
<select id="areaSelect" class="form-select mr-2" aria-label="Default select example">
<!-- 数据动态填充 -->
</select></div>
<div class="col"><h5 class="form-group-right">通道名称:</h5></div>
<div class="col-5 mr-2">
<input id="channelNameInput" type="text" class="form-control mr-2" placeholder="Channel_name" aria-label="Channel_name"></div>
<div class="col-1"><button id="searchButton" type="button" class="btn btn-primary">查 询</button></div>
</div>
<div class="mb-3">
<!-- <button id="manageAreaButton" type="button" class="btn btn-primary mr-2">区域管理</button>-->
<button id="addChannelButton" type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#channelModal">
新增通道
</button>
</div>
<!-- 新增通道模态框 -->
<div class="modal fade" id="channelModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="add_channel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="channelModalLabel">新增通道</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="areaSelect_M">所属区域:</label>
<select id="areaSelect_M" class="form-select" aria-label="D">
<!-- 数据动态填充 -->
</select>
</div>
<div class="form-group">
<label for="CNameInput">通道名称:</label>
<input type="text" class="form-control" id="CNameInput">
</div>
<div class="form-group">
<label for="RTSPInput">RTSP地址:</label>
<input type="text" class="form-control" id="RTSPInput">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveButton">保存</button>
</div>
</div>
</div>
</div>
<!-- 修改通道模态框 -->
<div class="modal fade" id="ChangeC" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="change_c" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ChangeChannel">修改通道</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="areaSelect_CC">所属区域:</label>
<select id="areaSelect_CC" class="form-select" aria-label="D">
<!-- 数据动态填充 -->
</select>
</div>
<div class="form-group">
<label for="CNameInput_cc">通道名称:</label>
<input type="text" class="form-control" id="CNameInput_cc">
</div>
<div class="form-group">
<label for="RTSPInput_cc">RTSP地址:</label>
<input type="text" class="form-control" id="RTSPInput_cc">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveButton_cc">保存</button>
</div>
</div>
</div>
</div>
<!-- 算法管理模态框 -->
<div class="modal fade" id="MX_M" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="m_mx" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="MX_Title">配置算法</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-6">
<div class="video-area">
<!-- <img id="video-mx" alt="Video Stream" />-->
<canvas id="backgroundCanvas" style="display: none;"></canvas>
<canvas id="myCanvas"></canvas>
</div>
</div>
<div class="col-6 ms-auto">
<!-- 配置算法 -->
<div class="row align-items-center mb-2">
<div class="col-3"><label for="model_select">配置算法:</label></div>
<div class="col-9">
<select id="model_select" class="form-select" aria-label="D">
<!-- 数据动态填充 -->
</select>
</div>
</div>
<!-- 检测区域 -->
<div class="row align-items-center mb-2">
<div class="col-3"><label for="model_select">检测区域:</label></div>
<div class="col-9">
<div class="row">
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="jcqy"
id="qjjc" checked onclick="handleRadioClick(event)">
<label class="form-check-label" for="qjjc">全画面生效</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="jcqy"
id="zdjc" onclick="handleRadioClick(event)">
<label class="form-check-label" for="zdjc">指定区域</label>
</div>
</div>
</div>
<div class="row">
<button type="button" class="btn btn-primary" id="but_hzqy" style="--bs-btn-padding-y:.20rem; --bs-btn-font-size: .70rem;">
绘制区域</button>
</div>
</div>
</div>
<!-- 检测阈值 -->
<div class="row align-items-center mb-2">
<div class="col-3">
<label>置信阈值:</label>
</div>
<div class="col-3">
<input type="text" class="form-control" id="zxyz">
</div>
<div class="col-3">
<label>IOU阈值:</label>
</div>
<div class="col-3">
<input type="text" class="form-control" id="iouyz">
</div>
</div>
<!-- 布放计划 -->
<div class="row form-group">
<label for="11">布放计划:</label>
<div id="11">
<table class="schedule-table table table-sm table-bordered" style="font-size: 7px;">
<thead>
<tr>
<th></th><th>00</th><th>01</th><th>02</th><th>03</th><th>04</th>
<th>05</th><th>06</th><th>07</th><th>08</th><th>09</th><th>10</th>
<th>11</th><th>12</th><th>13</th><th>14</th><th>15</th><th>16</th>
<th>17</th><th>18</th><th>19</th><th>20</th><th>21</th><th>22</th>
<th>23</th>
</tr>
</thead>
<tbody id="schedule-body">
<!-- 表格数据将由JavaScript动态填充 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="cancelButton_mx">取消</button>
<button type="button" class="btn btn-primary" id="saveButton_mx">保存</button>
</div>
</div>
</div>
</div>
<div class="table-container">
<table class="table">
<thead class="table-light">
<tr>
<th scope="col">ID</th>
<th scope="col">所属区域</th>
<th scope="col">通道名称</th>
<th scope="col">RTSP地址</th>
<th scope="col">配置算法</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody id="table-body" class="table-group-divider">
<!-- 表格数据动态填充 -->
</tbody>
</table>
<nav>
<ul id="pagination" class="pagination">
<!-- 分页控件将动态生成 -->
</ul>
</nav>
</div>
</div>
{% endblock %}
{% block script %}
<script src="{{ url_for('main.static', filename='scripts/jquery-3.2.1.slim.min.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/popper.min.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/channel_manager.js') }}"></script>
{% endblock %}

29
web/main/templates/footer.html

@ -1,29 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="facebook" viewBox="0 0 16 16">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"/>
</symbol>
<symbol id="instagram" viewBox="0 0 16 16">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334z"/>
</symbol>
<symbol id="twitter" viewBox="0 0 16 16">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"/>
</symbol>
<symbol id="view" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z"/>
</symbol>
</svg>
<footer class="py-3 my-4 border-top">
<div class="container justify-content-between align-items-center d-flex flex-wrap">
<div class="col-md-4 d-flex align-items-center">
<img src="{{ url_for('main.static', filename='images/zf.svg') }}" alt="zfai" width="30" height="24">
<span class="mb-3 mb-md-0 text-muted">&copy; ZFKJ All Rights Reserved</span>
</div>
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
<li class="ms-3"><a class="text-muted" ><svg class="bi" width="24" height="24"><use xlink:href="#twitter"/></svg></a></li>
<li class="ms-3"><a class="text-muted" ><svg class="bi" width="24" height="24"><use xlink:href="#instagram"/></svg></a></li>
<li class="ms-3"><a class="text-muted" ><svg class="bi" width="24" height="24"><use xlink:href="#facebook"/></svg></a></li>
</ul>
</div>
</footer>

Before

Width:  |  Height:  |  Size: 3.8 KiB

35
web/main/templates/header.html

@ -1,35 +0,0 @@
<header class="p-3 mb-3 border-bottom">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<img src="images/登录/zf.svg" alt="" width="40" height="40">
<!-- <svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"/></svg>-->
<span class="fs-4">ZF_BOX</span>
</a>
<ul class="nav nav-pills">
<li class="nav-item"><a href="/view_main.html" class="nav-link active" aria-current="page">实时预览</a></li>
<li class="nav-item"><a href="/channel_manager.html" class="nav-link">通道管理</a></li>
<li class="nav-item"><a href="/schedule.html" class="nav-link">算法管理</a></li>
<li class="nav-item"><a href="#" class="nav-link">系统管理</a></li>
<li class="nav-item"><a href="#" class="nav-link">用户管理</a></li>
</ul>
<div class="dropdown text-end">
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-person-bounding-box" viewBox="0 0 16 16">
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5z"/>
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm8-9a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
</svg>
<!-- <img src="https://github.com/mdo.png" alt="mdo" width="32" height="32" class="rounded-circle">-->
</a>
<ul class="dropdown-menu text-small">
<li><a class="dropdown-item" href="#">修改密码</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">退 出</a></li>
</ul>
</div>
</div>
</div>
</header>

205
web/main/templates/index_webrtc.html

@ -1,176 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZFBOX</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<link href="../static/resources/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
display: flex;
flex-direction: column;
height: 100vh;
}
header {
background-color: #007bff;
color: white;
padding: 10px 0;
}
.navbar-nav .nav-link {
color: white;
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
.tree-view {
border-right: 1px solid #ddd;
padding: 10px;
overflow-y: auto;
}
.video-content {
padding: 10px;
overflow-y: auto;
flex: 1;
}
footer {
background-color: #f8f9fa;
text-align: center;
padding: 10px 0;
}
.video-frame {
background-color: #f8f9fa;
border: 1px solid #ddd;
margin-bottom: 10px;
}
.video-frame img {
width: 100%;
height: auto;
}
.video-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.video-grid.eight {
grid-template-columns: repeat(4, 1fr);
}
.toggle-buttons {
margin-bottom: 10px;
}
</style>
<title>WebRTC Video Stream</title>
</head>
<body>
<header>
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">智凡BOX</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#">实时预览</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">通道管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">算法管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">系统管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">用户管理</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="#">张三 退出</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<main>
<div class="tree-view col-md-3">
<ul class="list-group">
<li class="list-group-item">一区
<ul class="list-group">
<li class="list-group-item">北门通道一</li>
<li class="list-group-item">南门通道二</li>
<li class="list-group-item">通道三</li>
</ul>
</li>
<li class="list-group-item">二区域
<ul class="list-group">
<li class="list-group-item">通道一</li>
</ul>
</li>
</ul>
</div>
<div class="video-content col-md-9">
<div class="toggle-buttons">
<button id="fourView" class="btn btn-primary">四画面</button>
<button id="eightView" class="btn btn-secondary">八画面</button>
</div>
<div id="videoGrid" class="video-grid">
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
</div>
</div>
</main>
<footer>
&copy; 2024 ZFKJ All Rights Reserved
</footer>
<script src="{{ url_for('main.static', filename='js/bootstrap.bundle.min.js') }}"></script>
<h1>WebRTC Video Stream</h1>
<video id="video" autoplay playsinline></video>
<script>
document.getElementById('fourView').addEventListener('click', function() {
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('eight');
videoGrid.classList.add('four');
videoGrid.innerHTML = `
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
`;
});
document.getElementById('eightView').addEventListener('click', function() {
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('four');
videoGrid.classList.add('eight');
videoGrid.innerHTML = `
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div>
`;
});
const pc = new RTCPeerConnection();
const video = document.getElementById('video');
pc.ontrack = (event) => {
video.srcObject = event.streams[0];
};
const ws = new WebSocket('ws://' + window.location.host + '/api/ws');
ws.onmessage = async (event) => {
const data = JSON.parse(event.data);
await pc.setRemoteDescription(new RTCSessionDescription(data));
if (data.type === 'offer') {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({
'sdp': pc.localDescription.sdp,
'type': pc.localDescription.type
}));
}
};
ws.onopen = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({
'sdp': pc.localDescription.sdp,
'type': pc.localDescription.type
}));
};
</script>
</body>
</html>

122
web/main/templates/login.html

@ -1,122 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZFBOX</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<!-- <link href="../static/resources/css/bootstrap.min.css" rel="stylesheet"> -->
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.captcha-group {
display: flex;
align-items: center;
gap: 10px;
}
</style>
<!-- Custom styles for this template -->
<link href="{{ url_for('main.static', filename='css/sign-in.css') }}" rel="stylesheet">
<!-- <link href="../static/resources/css/sign-in.css" rel="stylesheet"> -->
</head>
<body class="text-center">
<main class="form-signin w-100 m-auto">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="alert alert-danger" role="alert">
{% for category, message in messages %}
{{ message }}
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form id="loginForm" method="post" action="/api/user/login">
<img class="mb-4" src="images/登录/zf.svg" alt="" width="72" height="57">
<h1 class="h3 mb-3 fw-normal">ZF-AI</h1>
<div class="form-floating">
<input type="text" class="form-control" id="username" name="username" placeholder="UserName" required>
<label for="username">UserName</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
<label for="password">Password</label>
</div>
<div class="captcha-group">
<div class="form-floating captcha">
<input type="text" class="form-control" id="captcha" name="captcha" placeholder="Captcha" required>
<label for="captcha">Captcha</label>
</div>
<img id="captchaImage" src="" alt="Captcha" class="captcha">
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">登 录</button>
<p class="mt-5 mb-3 text-muted">&copy; 2024–2025 ZFKJ All Rights Reserved</p>
</form>
</main>
<script>
const form = document.getElementById('loginForm');
const captchaImage = document.getElementById('captchaImage');
captchaImage.src = '/api/user/code?' + new Date().getTime();
form.addEventListener('submit', function(event) {
});
function showError(errorText) {
alert(errorText);
}
</script>
</body>
</html>

156
web/main/templates/schedule.html

@ -1,156 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>布防时间计划</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<style>
.schedule-table {
width: 100%;
table-layout: fixed;
}
.schedule-table th, .schedule-table td {
text-align: center;
vertical-align: middle;
cursor: pointer;
border: 1px solid #dee2e6;
padding: 8px;
}
.schedule-table td.allowed {
background-color: white;
}
.schedule-table td.blocked {
background-color: blue;
}
</style>
</head>
<body>
<div class="container mt-5">
<table class="schedule-table table table-bordered">
<thead>
<tr>
<th>小时</th>
<th>00</th>
<th>01</th>
<th>02</th>
<th>03</th>
<th>04</th>
<th>05</th>
<th>06</th>
<th>07</th>
<th>08</th>
<th>09</th>
<th>10</th>
<th>11</th>
<th>12</th>
<th>13</th>
<th>14</th>
<th>15</th>
<th>16</th>
<th>17</th>
<th>18</th>
<th>19</th>
<th>20</th>
<th>21</th>
<th>22</th>
<th>23</th>
</tr>
</thead>
<tbody>
<!-- Repeat above row for each day of the week (Monday to Saturday) -->
<tr>
<th>星期一</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期二</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期三</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期四</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期五</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期六</th>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
<tr>
<th>星期日</th>
<!-- 24 cells for each hour -->
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
<td class="blocked"></td><td class="blocked"></td><td class="blocked"></td><td class="blocked"></td>
</tr>
</tbody>
</table>
<div class="mt-3">
<div class="d-inline-block mr-3">
<div class="d-inline-block align-middle" style="width: 20px; height: 20px; background-color: white; border: 1px solid black;"></div>
<span class="align-middle">允许</span>
</div>
<div class="d-inline-block">
<div class="d-inline-block align-middle" style="width: 20px; height: 20px; background-color: blue;"></div>
<span class="align-middle">阻止</span>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const cells = document.querySelectorAll('.schedule-table tbody td');
cells.forEach(cell => {
cell.addEventListener('click', () => {
if (cell.classList.contains('blocked')) {
cell.classList.remove('blocked');
cell.classList.add('allowed');
} else {
cell.classList.remove('allowed');
cell.classList.add('blocked');
}
});
});
});
</script>
</body>
</html>

146
web/main/templates/view_main.html

@ -1,146 +0,0 @@
{% extends 'base.html' %}
{% block title %}ZFBOX{% endblock %}
{% block style %}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.blue-svg {
color: blue; /* 这将影响所有使用currentColor的fill属性 */
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
.tree-view {
border-right: 1px solid #ddd;
padding: 10px;
overflow-y: auto;
}
.video-frame {
position: relative;
width: calc(50% - 10px); /* 默认4画面布局,每行2个视频框架 */
margin: 5px;
float: left;
background-color: #ccc; /* 默认灰色填充 */
border: 1px solid #000; /* 边框 */
}
.video-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #2c3e50;
color: #fff;
padding: 5px;
}
.video-area {
width: 100%;
padding-bottom: 75%; /* 4:3 比例 */
background-color: #000; /* 默认灰色填充 */
position: relative;
border: 1px solid #ddd; /* 视频区域边框 */
}
.video-area img {
display: none; /* 初始隐藏 */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.video-buttons {
display: flex;
gap: 10px;
}
.video-buttons button {
background: none;
border: none;
color: white;
cursor: pointer;
}
.video-buttons button:hover {
color: #f39c12;
}
.dropdown-menu {
min-width: 100px;
}
.video-frame img {
width: 100%;
height: auto;
}
#videoGrid.four .video-frame {
width: calc(50% - 10px); /* 每行4个视频框架 */
}
#videoGrid.eight .video-frame {
width: calc(12.5% - 10px); /* 每行8个视频框架 */
}
#videoGrid.nine .video-frame {
width: calc(33.33% - 10px); /* 每行3个视频框架,9画面布局 */
}
.toggle-buttons {
margin-bottom: 5px;
}
.btn-small {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: red;
background-color: rgba(255, 255, 255, 0.8); /* 半透明背景 */
padding: 10px;
border-radius: 5px;
}
{% endblock %}
{% block content %}
<div class="container d-flex flex-wrap" >
<div id="treeView" class="tree-view col-md-3 ">
<!-- 动态树视图 -->
</div>
<div class="video-content col-md-9">
<div id="videoGrid" class="row four">
<!-- 动态视频节点 -->
</div>
<div class="toggle-buttons">
<button id="fourView" class="btn btn-primary btn-small">四画面</button>
<button id="nineView" class="btn btn-secondary btn-small">九画面</button>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script src="{{ url_for('main.static', filename='scripts/aiortc-client-new.js') }}"></script>
{% endblock %}

BIN
流程说明.docx

Binary file not shown.
Loading…
Cancel
Save