diff --git a/.idea/FristProject.iml b/.idea/FristProject.iml
index a35650a..719bacc 100644
--- a/.idea/FristProject.iml
+++ b/.idea/FristProject.iml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 7269ac0..55756c3 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/DB_table.xlsx b/DB_table.xlsx
index b05ea9f..b7086fd 100644
Binary files a/DB_table.xlsx and b/DB_table.xlsx differ
diff --git a/config.yaml b/config.yaml
index 959b2ce..f1fa27f 100644
--- a/config.yaml
+++ b/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 #线程队列对长度
\ No newline at end of file
diff --git a/core/ChannelManager.py b/core/ChannelManager.py
index e39710b..6cb3dae 100644
--- a/core/ChannelManager.py
+++ b/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]
diff --git a/core/DBManager.py b/core/DBManager.py
index 0418c4c..da16817 100644
--- a/core/DBManager.py
+++ b/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
diff --git a/core/ModelManager.py b/core/ModelManager.py
index fc814c1..c206507 100644
--- a/core/ModelManager.py
+++ b/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()}")
diff --git a/model/plugins/ModelBase.py b/model/plugins/ModelBase.py
index e88da01..02e147c 100644
--- a/model/plugins/ModelBase.py
+++ b/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
\ No newline at end of file
+ pass
+
+ @abstractmethod
+ def postwork(self,image,output,check_area,polygon,scale_ratio, pad_size): # 后处理
+ pass
+
+
+
+if __name__ =="__main__":
+ pass
\ No newline at end of file
diff --git a/model/plugins/Peo_ACL/yolov5s_bs1.om b/model/plugins/Peo_ACL/yolov5s_bs1.om
deleted file mode 100644
index 3334c7b..0000000
Binary files a/model/plugins/Peo_ACL/yolov5s_bs1.om and /dev/null differ
diff --git a/model/plugins/RYRQ_ACL/RYRQ_Model_ACL.py b/model/plugins/RYRQ_ACL/RYRQ_Model_ACL.py
index 106d193..1f9fedf 100644
--- a/model/plugins/RYRQ_ACL/RYRQ_Model_ACL.py
+++ b/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")
\ No newline at end of file
diff --git a/run.py b/run.py
index 97c7068..484905f 100644
--- a/run.py
+++ b/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())
-
diff --git a/web/API/channel.py b/web/API/channel.py
index ce14fed..83729b9 100644
--- a/web/API/channel.py
+++ b/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'])
diff --git a/web/API/user.py b/web/API/user.py
index 0d53778..94ae229 100644
--- a/web/API/user.py
+++ b/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
diff --git a/web/API/viedo.py b/web/API/viedo.py
index 821da64..640f389 100644
--- a/web/API/viedo.py
+++ b/web/API/viedo.py
@@ -144,23 +144,20 @@ async def get_stats(peer_connection):
@api.websocket('/ws/video_feed/')
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})
diff --git a/web/__init__.py b/web/__init__.py
index f4b3cd1..8c10270 100644
--- a/web/__init__.py
+++ b/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
diff --git a/web/main/__init__.py b/web/main/__init__.py
index bab07fb..3d5df65 100644
--- a/web/main/__init__.py
+++ b/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
diff --git a/web/main/routes.py b/web/main/routes.py
index 42f0fb5..162edc4 100644
--- a/web/main/routes.py
+++ b/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':
diff --git a/web/main/static/favicon.ico b/web/main/static/favicon.ico
index 37d0d9e..b8d74a1 100644
Binary files a/web/main/static/favicon.ico and b/web/main/static/favicon.ico differ
diff --git a/web/main/static/images/登录/zf.png b/web/main/static/images/登录/zf.png
deleted file mode 100644
index edeaf03..0000000
Binary files a/web/main/static/images/登录/zf.png and /dev/null differ
diff --git a/web/main/static/images/登录/zf.svg b/web/main/static/images/登录/zf.svg
deleted file mode 100644
index 193a974..0000000
--- a/web/main/static/images/登录/zf.svg
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
diff --git a/web/main/static/resources/css/headers.css b/web/main/static/resources/css/headers.css
deleted file mode 100644
index 8230c9a..0000000
--- a/web/main/static/resources/css/headers.css
+++ /dev/null
@@ -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;
-}
diff --git a/web/main/static/resources/css/sign-in.css b/web/main/static/resources/css/sign-in.css
deleted file mode 100644
index 67e301e..0000000
--- a/web/main/static/resources/css/sign-in.css
+++ /dev/null
@@ -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;
-}
diff --git a/web/main/static/resources/images/zf.png b/web/main/static/resources/images/zf.png
deleted file mode 100644
index edeaf03..0000000
Binary files a/web/main/static/resources/images/zf.png and /dev/null differ
diff --git a/web/main/static/resources/images/zf.svg b/web/main/static/resources/images/zf.svg
deleted file mode 100644
index 193a974..0000000
--- a/web/main/static/resources/images/zf.svg
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
diff --git a/web/main/static/resources/scripts/aiortc-client-new.js b/web/main/static/resources/scripts/aiortc-client-new.js
index 720c566..624b0df 100644
--- a/web/main/static/resources/scripts/aiortc-client-new.js
+++ b/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 = '';
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 += '
';
- html += '';
- }
- area_name = `${channel.area_name}`;
- html += `${area_name}`;
- html += '';
+ 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 += `- ${channel.channel_name}
`;
- html += `-
-
- ${channel.channel_name}
-
`;
});
- if(area_name !== ""){
- html += '
';
- html += '';
- }
- html += '';
- 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 += `
- `;
- }
- 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 {
- 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();
+ }
\ No newline at end of file
diff --git a/web/main/static/resources/scripts/base.js b/web/main/static/resources/scripts/base.js
deleted file mode 100644
index c906600..0000000
--- a/web/main/static/resources/scripts/base.js
+++ /dev/null
@@ -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;
- }
- }
-}
\ No newline at end of file
diff --git a/web/main/static/resources/scripts/channel_manager.js b/web/main/static/resources/scripts/channel_manager.js
deleted file mode 100644
index ee5f572..0000000
--- a/web/main/static/resources/scripts/channel_manager.js
+++ /dev/null
@@ -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 = `
- ${channel.ID} |
- ${channel.area_name} |
- ${channel.channel_name} |
- ${channel.ulr} |
- ${channel.model_name} |
-
-
-
-
- |
- `;
- 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 = `${i}`;
- 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);
- });
-}
-
-
-
-
-
diff --git a/web/main/static/resources/scripts/jquery-3.2.1.slim.min.js b/web/main/static/resources/scripts/jquery-3.2.1.slim.min.js
deleted file mode 100644
index 105d00e..0000000
--- a/web/main/static/resources/scripts/jquery-3.2.1.slim.min.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/*! jQuery v3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */
-!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a);
-}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/
-
- {% block script %}{% endblock %}
-
-