Browse Source

test

master
张龙 10 months ago
parent
commit
0bdc57a783
  1. 1
      .idea/vcs.xml
  2. 41
      DBManager.py
  3. 37
      config.yaml
  4. 218
      core/DBManager.py
  5. 0
      core/DataStruct.py
  6. 167
      core/ModelManager.py
  7. 5
      core/SocketManager.py
  8. 0
      core/SystemManager.py
  9. 162
      core/Test.py
  10. 24
      core/Upload_file.py
  11. 59
      core/ViewManager.py
  12. 13
      main.py
  13. 64
      model/mode_test/yoloV5.py
  14. 49
      model/plugins/ModelBase.py
  15. 43
      model/plugins/RYRQ_Model.py
  16. 155
      model/plugins/yolov5-1.py
  17. 44
      myutils/ConfigManager.py
  18. 2
      myutils/MyLogger_logger.py
  19. 0
      myutils/MyLogger_logging.py
  20. 67
      myutils/ReManager.py
  21. 42
      myutils/loader.py
  22. 0
      myutils/logs/2024-05-25.log
  23. 0
      myutils/logs/app.log
  24. BIN
      requirements.txt
  25. 17
      run.py
  26. 4
      web/API/__init__.py
  27. 290
      web/API/channel.py
  28. 113
      web/API/model.py
  29. 108
      web/API/system.py
  30. 114
      web/API/user.py
  31. 312
      web/API/viedo.py
  32. 240
      web/__init__.py
  33. 0
      web/common/__init__.py
  34. 4
      web/common/errors.py
  35. 10
      web/common/models.py
  36. 44
      web/common/utils.py
  37. 2
      web/main/__init__.py
  38. 94
      web/main/routes.py
  39. 0
      web/main/static/data/document.js
  40. 0
      web/main/static/data/styles.css
  41. 0
      web/main/static/favicon.ico
  42. 0
      web/main/static/files/实时预览/data.js
  43. 0
      web/main/static/files/实时预览/styles.css
  44. 0
      web/main/static/files/用户管理/data.js
  45. 0
      web/main/static/files/用户管理/styles.css
  46. 0
      web/main/static/files/登录/data.js
  47. 0
      web/main/static/files/登录/styles.css
  48. 0
      web/main/static/files/算法管理/data.js
  49. 0
      web/main/static/files/算法管理/styles.css
  50. 0
      web/main/static/files/系统管理/data.js
  51. 0
      web/main/static/files/系统管理/styles.css
  52. 0
      web/main/static/files/通道管理/data.js
  53. 0
      web/main/static/files/通道管理/styles.css
  54. 0
      web/main/static/images/实时预览/u20_menu.png
  55. 0
      web/main/static/images/实时预览/u22.png
  56. 0
      web/main/static/images/实时预览/u26.png
  57. 0
      web/main/static/images/实时预览/u32.png
  58. 0
      web/main/static/images/实时预览/u48.png
  59. 0
      web/main/static/images/实时预览/u48_selected.png
  60. 0
      web/main/static/images/用户管理/u410.png
  61. 0
      web/main/static/images/用户管理/u411.png
  62. 0
      web/main/static/images/用户管理/u412.png
  63. 0
      web/main/static/images/用户管理/u413.png
  64. 0
      web/main/static/images/用户管理/u441.png
  65. 0
      web/main/static/images/用户管理/u442.png
  66. 0
      web/main/static/images/用户管理/u443.png
  67. 0
      web/main/static/images/用户管理/u444.png
  68. 0
      web/main/static/images/用户管理/u466.png
  69. 0
      web/main/static/images/用户管理/u467.png
  70. 0
      web/main/static/images/用户管理/u468.png
  71. 0
      web/main/static/images/用户管理/u469.png
  72. 0
      web/main/static/images/用户管理/u497.png
  73. 0
      web/main/static/images/用户管理/u498.png
  74. 0
      web/main/static/images/用户管理/u499.png
  75. 0
      web/main/static/images/用户管理/u500.png
  76. 0
      web/main/static/images/登录/u12.png
  77. 0
      web/main/static/images/登录/u4.svg
  78. 0
      web/main/static/images/算法管理/u252.png
  79. 0
      web/main/static/images/算法管理/u253.png
  80. 0
      web/main/static/images/算法管理/u254.png
  81. 0
      web/main/static/images/算法管理/u255.png
  82. 0
      web/main/static/images/算法管理/u256.png
  83. 0
      web/main/static/images/算法管理/u312.png
  84. 0
      web/main/static/images/算法管理/u313.png
  85. 0
      web/main/static/images/算法管理/u314.png
  86. 0
      web/main/static/images/算法管理/u315.png
  87. 0
      web/main/static/images/算法管理/u316.png
  88. 0
      web/main/static/images/算法管理/u363.svg
  89. 0
      web/main/static/images/算法管理/u363_disabled.svg
  90. 0
      web/main/static/images/算法管理/u363_selected.svg
  91. 0
      web/main/static/images/算法管理/u363_selectedDisabled.svg
  92. 0
      web/main/static/images/算法管理/u364.svg
  93. 0
      web/main/static/images/算法管理/u364_disabled.svg
  94. 0
      web/main/static/images/算法管理/u364_selected.svg
  95. 0
      web/main/static/images/算法管理/u364_selectedDisabled.svg
  96. 0
      web/main/static/images/算法管理/u366.svg
  97. 0
      web/main/static/images/算法管理/u366_disabled.svg
  98. 0
      web/main/static/images/算法管理/u366_selected.svg
  99. 0
      web/main/static/images/算法管理/u366_selectedDisabled.svg
  100. 0
      web/main/static/images/算法管理/u367.svg

1
.idea/vcs.xml

@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/model/base_model/yolov5" vcs="Git" />
</component>
</project>

41
DBManager.py

@ -1,41 +0,0 @@
import pymysql
import sqlite3
class DBManager():
#实例化数据库管理对象,并连接数据库
def __init__(self,ip,prot,user,passwd):
pass
#检查设备ID是否在数据库?
def checkDevID(self,cID):
pass
def test(self):
# 建立数据库连接
conn = pymysql.connect(
host='localhost',
port=3306,
user='username',
password='password',
database='database_name'
)
# 创建游标对象
cursor = conn.cursor()
# 执行 SQL 查询
query = "SELECT * FROM table_name"
cursor.execute(query)
# 获取查询结果
result = cursor.fetchall()
# 输出结果
for row in result:
print(row)
# 关闭游标和连接
cursor.close()
conn.close()

37
config.yaml

@ -1,27 +1,34 @@
#字典更新接口
url: http://192.168.2.61/api/
hyMD5: http://192.168.2.61/api/defaultMd5/download
hyfile: http://192.168.2.61/api/defaultPassword/download/
hyget: http://192.168.2.61/api/defaultPassword/export/
#主线程数
concurrency: 6 #同时执行的子任务数,--线程数
#超时配置
TagTime: 60 #任务执行超时时间
pocTime: 30 #一次poc执行超时时间,0不进行超时判断
nmapTime: 10 #一次nmap执行超时时间,0不进行超时判断
#日志记录
file_log_level: INFO #是否记录日志
show_log_level: DEBUG #日志记录级别
log_dir: logs
#远程数据库
#远程数据库 --- 香橙派用不了
DBType: 1 #0--mysql,1--sqlite
mysql:
host: 192.168.2.54
host: 192.168.3.45
port: 3306
user: root
pass: crnn@cz***
db: sfind
pass: ZFkj_123456
db: zfbox
#sqlit
sqlite: zfbox.db
#用户初始密码
pw: zfkj_123!@#
#上传限制
MAX_CONTENT_LENGTH : 100 # 100MB
UPLOAD_FOLDER : uploads
ALLOWED_EXTENSIONS : {'zip'}
#RTSP
RTSP_Check_Time : 600 #10分钟
#model
weight_path: /model/weights
yolov5_path: model/base_model/yolov5
cap_sleep_time: 300 #5分钟
buffer_len: 30 #分析后画面缓冲区帧数

218
core/DBManager.py

@ -0,0 +1,218 @@
import pymysql
import sqlite3
import threading
import os
from myutils.ConfigManager import myCongif
from myutils.MyLogger_logger import LogHandler
class DBManager():
#实例化数据库管理对象,并连接数据库
#itype=0 使用mysql数据库,1-使用sqlite数据库
def __init__(self):
self.logger = LogHandler().get_logger("DBManager")
self.lock = threading.Lock()
self.itype = myCongif.get_data("DBType")
if self.itype ==0:
self.host = myCongif.get_data('mysql.host')
self.port = myCongif.get_data('mysql.port')
self.user = myCongif.get_data('mysql.user')
self.passwd = myCongif.get_data('mysql.passwd')
self.database = myCongif.get_data('mysql.database')
self.connection = None
self.cursor = None
elif self.itype ==1:
self.dbfile = myCongif.get_data("sqlite")
if not os.path.exists(self.dbfile):
self.dbfile = "../" + self.dbfile #直接运行DBManager时初始路径不是在根目录
if not os.path.exists(self.dbfile):
raise FileNotFoundError(f"Database file {self.dbfile} does not exist.")
else:
self.logger.error("错误的数据库类型,请检查")
def __del__(self):
if self.ok:
self.cursor.close()
self.connection.close()
self.cursor = None
self.connection = None
self.logger.debug("DBManager销毁")
def connect(self):
try:
if self.itype ==0:
self.connection = pymysql.connect(host=self.host, port=self.port, user=self.user,
passwd=self.passwd, db=self.database,charset='utf8')
self.cursor = self.connection.cursor()
elif self.itype ==1:
self.connection = sqlite3.connect(self.dbfile)
self.cursor = self.connection.cursor()
self.ok = True
return True
except:
self.logger.error("服务器端数据库连接失败")
return False
# 判断数据库连接是否正常,若不正常则重连接
def Retest_conn(self):
if self.itype == 0: #除了mysql,sqlite3不需要判断连接状态
try:
self.connection.ping()
except:
return self.connect()
return True
# 执行数据库查询操作 1-只查询一条记录,其他所有记录
def do_select(self, strsql, itype=0):
# self.conn.begin()
self.lock.acquire()
data = None
if self.Retest_conn():
try:
self.cursor.execute(strsql)
self.connection.commit() # select要commit提交事务,是存在获取不到最新数据的问题(innoDB事务机制)
except Exception as e:
self.logger.error("do_select异常报错:%s" % str(e))
self.lock.release()
return None
if itype == 1:
data = self.cursor.fetchone()
else:
data = self.cursor.fetchall()
self.lock.release()
return data
# 执行数据库语句
def do_sql(self, strsql, data=None):
self.lock.acquire()
bok = False
if self.Retest_conn():
try:
# self.conn.begin()
if data:
iret = self.cursor.executemany(strsql, data) #批量执行sql语句
else:
iret = self.cursor.execute(strsql)
self.connection.commit()
if iret.rowcount > 0: # 要有修改成功的记录才返回true
bok = True
except Exception as e:
self.logger.error("执行数据库语句%s出错:%s" % (strsql, str(e)))
self.connection.rollback()
self.lock.release()
return bok
#---------------------特定数据库操作函数---------------------
#根据通道ID或者模型ID删除通道和模型间的关联数据 1-通道ID,2-模型ID
def delC2M(self,ID,itype):
#channel2model
if itype ==1:
strsql = f"select ID from channel2model where channel_id={ID};"
datas = self.do_select(strsql)
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
for data in datas:
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)
if ret == False:
return False
#channel
strsql = f"delete from channel where ID={ID};"
ret = self.do_sql(strsql)
if ret == False:
return False
return True
#修改视频通道和算法间的关联关系
#channel_id 通道ID
#modell_list 最新配置的模型id list
def updateC2M(self,channel_id,model_list):
strsql = f"select model_id from channel2model where channel_id={channel_id};"
datas = set(self.do_select(strsql))
data_new = set(model_list)
#计算要新增和修改的
need_add = data_new - datas
need_del = datas-data_new
#新增
for one in need_add:
strsql = f"insert into channel2model (channel_id,model_id) values ({channel_id},{one});"
if self.do_sql(strsql) == False:
return False
#差删除
for one in need_del:
strsql = f"select ID from channel2model where channel_id={channel_id} and model_id={one};"
c2m_id = mDBM.do_select(strsql,1)[0]
#删除布防计划数据
strsql = f"delete from schedule where channel2model_id={c2m_id};"
if self.do_sql(strsql) == False:
return False
#删除关联记录
strsql = f"delete from channel2model where ID = {c2m_id};"
if self.do_sql(strsql) == False:
return False
return True
#检查设备ID是否在数据库?
def checkDevID(self,cID):
pass
def test(self):
# 建立数据库连接
conn = pymysql.connect(
host='localhost',
port=3306,
user='username',
password='password',
database='database_name'
)
# 创建游标对象
cursor = conn.cursor()
# 执行 SQL 查询
query = "SELECT * FROM table_name"
cursor.execute(query)
# 获取查询结果
result = cursor.fetchall()
# 输出结果
for row in result:
print(row)
# 关闭游标和连接
cursor.close()
conn.close()
mDBM = DBManager()
mDBM.connect()
if __name__ == "__main__":
lista = set([1,2,3,4,5,6,7])
listb = set([4,6,8,9,10])
nadd = lista -listb
ndel = listb -lista
for one in nadd:
print(one)

0
DataStruct.py → core/DataStruct.py

167
core/ModelManager.py

@ -0,0 +1,167 @@
# 导入代码依赖
import time
import av
import os
import cv2
import torch
import numpy as np
import threading
import importlib.util
from core.DBManager import mDBM,DBManager
from myutils.MyLogger_logger import LogHandler
from myutils.ConfigManager import myCongif
class VideoCaptureWithFPS:
def __init__(self, src=0, target_fps=10):
self.cap = cv2.VideoCapture(src)
self.target_fps = target_fps
self.frame_interval = 1.0 / target_fps
self.last_frame_time = time.time()
def read(self):
current_time = time.time()
elapsed_time = current_time - self.last_frame_time
if elapsed_time < self.frame_interval: #小于间隔时间会休眠
time.sleep(self.frame_interval - elapsed_time)
self.last_frame_time = time.time()
ret, frame = self.cap.read()
return ret, frame
def release(self):
self.cap.release()
self.cap = None
class ModelManager:
def __init__(self):
self.verify_list = {}
self.bRun = True
self.logger = LogHandler().get_logger("ModelManager")
# 本地YOLOv5仓库路径
self.yolov5_path = myCongif.get_data("yolov5_path")
self.buflen = myCongif.get_data("buffer_len")
def _open_view(self,url,itype): #打开摄像头 0--USB摄像头,1-RTSP,2-海康SDK
if itype == 0:
cap = VideoCaptureWithFPS(int(url))
elif itype == 1:
cap = VideoCaptureWithFPS(url)
else:
raise Exception("视频参数错误!")
return cap
def _import_model(self,model_name,model_path):
'''根据路径,动态导入模块'''
if os.path.exists(model_path):
module_spec = importlib.util.spec_from_file_location(model_name, model_path)
module = importlib.util.module_from_spec(module_spec)
module_spec.loader.exec_module(module)
else:
self.logger.error("{}文件不存在".format(model_path))
return None
return module
def dowork_thread(self,channel_id):
'''一个通道一个线程,关联的模型在一个线程检测'''
cap = None
#查询关联的模型 --- 在循环运行前把基础数据都准备好
myDBM = DBManager()
myDBM.connection()
strsql = (f"select t1.model_id,t1.check_area,t1.polygon ,t2.duration_time,t2.proportion,t2.model_path "
f"from channel2model t1 left join model t2 on t1.model_id = t2.ID where t1.channel_id ={channel_id};")
myModels = myDBM.do_select(strsql)
#加载模型 --- 是不是要做个限制,一个视频通道关联算法模块的上限 --- 关联多了一个线程执行耗时较多,造成帧率太低,或者再多线程并发 #?
myModle_list = [] #存放模型对象List
myModle_data = [] #存放检测参数
for model in myModels:
#基于基类实例化模块类
m = self._import_model("",model[5]) #动态加载模型 -- 待完善
myModle_list.append(m)
myModle_data.append(model)
#开始循环检测
#print(mydata[0],mydata[1],mydata[2],mydata[3]) # url type tag img_buffer
#[url,type,True,img_buffer]
while self.verify_list[channel_id][2]: #基于tag 作为运行标识。 线程里只是读,住线程更新,最多晚一轮,应该不用线程锁。需验证
if not cap: #还没连接视频源
try:
cap = self._open_view(self.verify_list[channel_id][0],self.verify_list[channel_id][1])
except:
self.logger.error("参数错误,终止线程")
return
ret,frame = cap.read()
if not ret:
self.logger.warning("view disconnected. Reconnecting...")
cap.release()
cap = None
time.sleep(myCongif.get_data("cap_sleep_time"))
continue #没读到画面继续
#图像处理
img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# img = frame.to_ndarray(format="bgr24")
#检查布防时间是否在布防时间内
# 使用 模型 进行目标检测
i_warn_count = 0
for i in range(len(myModle_list)):#如果比较多可以考虑做线程进行检测
model = myModle_list[i]
data = myModle_data[i]
# 进行模型检测
detections,bwarn,warntext = model.verify(img,data)
#对识别结果要部要进行处理
if bwarn: # 整个识别有产生报警
# 绘制报警文本
cv2.putText(img, 'Intruder detected!', (50, (i_warn_count+1)*50),
cv2.FONT_HERSHEY_SIMPLEX, 1,(0, 0, 255), 2)
i_warn_count += 1
# 保存报警信息? --- 待完成
self.save_warn()
# 推送报警? --- 待完成
self.send_warn()
# 将检测结果图像转换为帧 -- 需要确认前面对img的处理都是累加的。
new_frame = av.VideoFrame.from_ndarray(img, format="rgb24")
# 分析图片放入内存中 ---
#new_frame = cv2.resize(new_frame, (640, 480)) # 降低视频分辨率
self.verify_list[channel_id][3].append(new_frame)
if len(self.verify_list[channel_id]) > self.buflen: # 保持缓冲区大小不超过10帧
self.verify_list[channel_id][3].pop(0)
self.logger.debug("drop one frame!")
def start_work(self,channel_id=0):
'''算法模型是在后台根据画面实时分析的'''
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:
img_buffer = []
run_data = [data[1],data[2],True,img_buffer]
self.verify_list[data[0]] = run_data #需要验证重复情况
th_chn = threading.Thread(target=self.dowork_thread, args=(data[0],)) #一个视频通道一个线程,线程句柄暂时部保留
th_chn.start()
def stop_work(self,channel_id=0):
if channel_id ==0: #所有线程停止
for data in self.verify_list:
data[2] = False
del data[3]
time.sleep(2)
self.verify_list.clear()
else:
data = self.verify_list[channel_id]
data[2] = False
del data[3]
time.sleep(1)
del self.verify_list[channel_id]
if __name__ == "__main__":
ModelManager().start_work()

5
SocketManager.py → core/SocketManager.py

@ -1,9 +1,8 @@
import threading
import socket
import socketserver
from DataStruct import *
from util.MyLogger_logging import MyLogger
from DBManager import DBManager
from myutils.MyLogger_logging import MyLogger
from core.DBManager import DBManager
import time
m_BRun = False #线程运行标识

0
SystemManager.py → core/SystemManager.py

162
core/Test.py

@ -0,0 +1,162 @@
# 导入代码依赖
import cv2
import numpy as np
import ipywidgets as widgets
from IPython.display import display
import torch
from skvideo.io import vreader, FFmpegWriter
import IPython.display
from ais_bench.infer.interface import InferSession
from det_utils import letterbox, scale_coords, nms
def preprocess_image(image, cfg, bgr2rgb=True):
"""图片预处理"""
img, scale_ratio, pad_size = letterbox(image, new_shape=cfg['input_shape'])
if bgr2rgb:
img = img[:, :, ::-1]
img = img.transpose(2, 0, 1) # HWC2CHW
img = np.ascontiguousarray(img, dtype=np.float32)
return img, scale_ratio, pad_size
def draw_bbox(bbox, img0, color, wt, names):
"""在图片上画预测框"""
det_result_str = ''
for idx, class_id in enumerate(bbox[:, 5]):
if float(bbox[idx][4] < float(0.05)):
continue
img0 = cv2.rectangle(img0, (int(bbox[idx][0]), int(bbox[idx][1])), (int(bbox[idx][2]), int(bbox[idx][3])),
color, wt)
img0 = cv2.putText(img0, str(idx) + ' ' + names[int(class_id)], (int(bbox[idx][0]), int(bbox[idx][1] + 16)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
img0 = cv2.putText(img0, '{:.4f}'.format(bbox[idx][4]), (int(bbox[idx][0]), int(bbox[idx][1] + 32)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
det_result_str += '{} {} {} {} {} {}\n'.format(
names[bbox[idx][5]], str(bbox[idx][4]), bbox[idx][0], bbox[idx][1], bbox[idx][2], bbox[idx][3])
return img0
def get_labels_from_txt(path):
"""从txt文件获取图片标签"""
labels_dict = dict()
with open(path) as f:
for cat_id, label in enumerate(f.readlines()):
labels_dict[cat_id] = label.strip()
return labels_dict
def draw_prediction(pred, image, labels):
"""在图片上画出预测框并进行可视化展示"""
imgbox = widgets.Image(format='jpg', height=720, width=1280)
img_dw = draw_bbox(pred, image, (0, 255, 0), 2, labels)
imgbox.value = cv2.imencode('.jpg', img_dw)[1].tobytes()
display(imgbox)
def infer_image(img_path, model, class_names, cfg):
"""图片推理"""
# 图片载入
image = cv2.imread(img_path)
# 数据预处理
img, scale_ratio, pad_size = preprocess_image(image, cfg)
# 模型推理
output = model.infer([img])[0]
output = torch.tensor(output)
# 非极大值抑制后处理
boxout = nms(output, conf_thres=cfg["conf_thres"], iou_thres=cfg["iou_thres"])
pred_all = boxout[0].numpy()
# 预测坐标转换
scale_coords(cfg['input_shape'], pred_all[:, :4], image.shape, ratio_pad=(scale_ratio, pad_size))
# 图片预测结果可视化
draw_prediction(pred_all, image, class_names)
def infer_frame_with_vis(image, model, labels_dict, cfg, bgr2rgb=True):
# 数据预处理
img, scale_ratio, pad_size = preprocess_image(image, cfg, bgr2rgb)
# 模型推理
output = model.infer([img])[0]
output = torch.tensor(output)
# 非极大值抑制后处理
boxout = nms(output, conf_thres=cfg["conf_thres"], iou_thres=cfg["iou_thres"])
pred_all = boxout[0].numpy()
# 预测坐标转换
scale_coords(cfg['input_shape'], pred_all[:, :4], image.shape, ratio_pad=(scale_ratio, pad_size))
# 图片预测结果可视化
img_vis = draw_bbox(pred_all, image, (0, 255, 0), 2, labels_dict)
return img_vis
def img2bytes(image):
"""将图片转换为字节码"""
return bytes(cv2.imencode('.jpg', image)[1])
def infer_video(video_path, model, labels_dict, cfg):
"""视频推理"""
image_widget = widgets.Image(format='jpeg', width=800, height=600)
display(image_widget)
# 读入视频
cap = cv2.VideoCapture(video_path)
while True:
ret, img_frame = cap.read()
if not ret:
break
# 对视频帧进行推理
image_pred = infer_frame_with_vis(img_frame, model, labels_dict, cfg, bgr2rgb=True)
image_widget.value = img2bytes(image_pred)
def infer_camera(model, labels_dict, cfg):
"""外设摄像头实时推理"""
def find_camera_index():
max_index_to_check = 10 # Maximum index to check for camera
for index in range(max_index_to_check):
cap = cv2.VideoCapture(index)
if cap.read()[0]:
cap.release()
return index
# If no camera is found
raise ValueError("No camera found.")
# 获取摄像头
camera_index = find_camera_index()
cap = cv2.VideoCapture(camera_index)
# 初始化可视化对象
image_widget = widgets.Image(format='jpeg', width=1280, height=720)
display(image_widget)
while True:
# 对摄像头每一帧进行推理和可视化
_, img_frame = cap.read()
image_pred = infer_frame_with_vis(img_frame, model, labels_dict, cfg)
image_widget.value = img2bytes(image_pred)
if __name__ == "__main__":
cfg = {
'conf_thres': 0.4, # 模型置信度阈值,阈值越低,得到的预测框越多
'iou_thres': 0.5, # IOU阈值,高于这个阈值的重叠预测框会被过滤掉
'input_shape': [640, 640], # 模型输入尺寸
}
model_path = 'yolo.om'
label_path = './coco_names.txt'
# 初始化推理模型
model = InferSession(0, model_path)
labels_dict = get_labels_from_txt(label_path)
infer_mode = 'video'
if infer_mode == 'image':
img_path = 'world_cup.jpg'
infer_image(img_path, model, labels_dict, cfg)
elif infer_mode == 'camera':
infer_camera(model, labels_dict, cfg)
elif infer_mode == 'video':
video_path = 'racing.mp4'
infer_video(video_path, model, labels_dict, cfg)

24
core/Upload_file.py

@ -0,0 +1,24 @@
from quart import Blueprint, render_template, request, redirect, url_for, flash, current_app
import os
import subprocess
from werkzeug.utils import secure_filename
from myutils.ConfigManager import myCongif
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in myCongif.get_data('ALLOWED_EXTENSIONS')
#对上传的系统升级包进行检查 type:1--系统升级包,2--算法升级包
def check_file(filepath,type):
pass
def update_system(filepath): #系统升级
pass
def updata_model(filepath): #算法模型升级或新增
try:
# 假设我们解压并运行一个升级脚本
subprocess.run(['unzip', '-o', filepath, '-d', '/path/to/upgrade/directory'], check=True)
subprocess.run(['/path/to/upgrade/directory/upgrade_script.sh'], check=True)
return True, 'Upgrade completed successfully.'
except subprocess.CalledProcessError as e:
return False, str(e)

59
core/ViewManager.py

@ -0,0 +1,59 @@
import requests
import time
import threading
from core.DBManager import DBManager
from myutils.MyLogger_logger import LogHandler
from myutils.ConfigManager import myCongif
class ViewManager:
def __init__(self):
self.thcheck = False
self.lock = threading.Lock()
self.logger = LogHandler().get_logger("ViewManager")
def check_rtsp_url(self,rtsp_url): #通过发送http请求判断rtsp流的在线状态
http_url = rtsp_url.replace("rtsp://", "http://")
try:
response = requests.options(http_url, timeout=5)
if response.status_code == 200:
return True
except requests.RequestException:
pass
return False
def check_all_urls(self): #?
strsql = "select ID,ulr from channel;"
datas = self.mDBM.do_select(strsql)
IDs = [row[0] for row in datas]
urls = [row[1] for row in datas]
#print(IDs,urls)
for i in range(len(urls)):
url = urls[i]
ID = IDs[i]
result = self.check_rtsp_url(url)
iret = 1 if result else 0
strsql = f"update channel set status={iret} where ID={ID};"
self.logger.debug(strsql)
self.mDBM.do_sql(strsql)
def start_check_rtsp(self, interval=myCongif.get_data('RTSP_Check_Time')): #通过线程循环检查添加的RTSP地址在线情况
def run():
self.mDBM = DBManager()
self.mDBM.connect()
while True:
self.check_all_urls()
time.sleep(interval)
if self.thcheck == False:
thread = threading.Thread(target=run)
thread.daemon = True
thread.start()
self.thcheck = True
mVManager = ViewManager()
if __name__ =='__main__':
mVManager.start_check_rtsp()
time.sleep(65)

13
main.py

@ -1,13 +0,0 @@
from util.ConfigManager import myCongif
from util.MyLogger_logger import LogHandler
from web import create_app
app = create_app()
if __name__ == '__main__':
# logger = LogHandler().get_logger("main")
# logger.debug("debug")
# logger.info("info")
app.run(debug=True)

64
model/mode_test/yoloV5.py

@ -0,0 +1,64 @@
import torch
import cv2
import numpy as np
from shapely.geometry import Point, Polygon
import os
# # 自定义模型文件的路径
# model_path = 'yolov5s.pt' # 假设模型文件名为 yolov5s.pt 并与执行文件在同一个目录
# # 本地YOLOv5仓库路径
# repo_path = '../base_model/yolov5'
# 自定义模型文件的路径
print(f"Current working directory (yolov5.py): {os.getcwd()}")
model_path = 'D:/Project/FristProject/model/mode_test/yolov5s.pt' # 假设模型文件名为 yolov5s.pt 并与执行文件在同一个目录
# 本地YOLOv5仓库路径
repo_path = 'D:/Project/FristProject/model/base_model/yolov5'
# 加载自定义模型
model = torch.hub.load(repo_path, 'custom', path=model_path, source='local')
# 定义监控区域(多边形顶点)
region_points = [(100, 100), (500, 100), (500, 400), (100, 400)]
polygon = Polygon(region_points)
# 打开摄像头
cap = cv2.VideoCapture(0)
def is_point_in_polygon(point, polygon):
return polygon.contains(Point(point))
while True:
ret, frame = cap.read()
if not ret:
break
# 进行推理
results = model(frame)
detections = results.pandas().xyxy[0]
# 绘制监控区域
cv2.polylines(frame, [np.array(region_points, np.int32)], isClosed=True, color=(0, 255, 0), thickness=2)
for _, row in detections.iterrows():
if row['name'] == 'person':
# 获取人员检测框的中心点
x_center = (row['xmin'] + row['xmax']) / 2
y_center = (row['ymin'] + row['ymax']) / 2
if is_point_in_polygon((x_center, y_center), polygon):
# 触发报警
cv2.putText(frame, 'Alert: Intrusion Detected', (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
print('Alert: Intrusion Detected')
# 绘制检测框
cv2.rectangle(frame, (int(row['xmin']), int(row['ymin'])), (int(row['xmax']), int(row['ymax'])), (255, 0, 0), 2)
cv2.circle(frame, (int(x_center), int(y_center)), 5, (0, 0, 255), -1)
# 显示结果
cv2.imshow('Frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()

49
model/plugins/ModelBase.py

@ -1,17 +1,48 @@
from abc import abstractmethod
from abc import abstractmethod,ABC
from shapely.geometry import Point, Polygon
import numpy as np
import cv2
class ModelBase(object):
class ModelBase(ABC):
def __init__(self):
pass
self.name = None #基于name来查询,用户对模型的配置参数,代表着模型名称需要唯一 2024-6-18 -逻辑还需要完善和验证
self.version = None
self.model_type = None # 模型类型 1-图像分类,2-目标检测(yolov5),3-分割模型,4-关键点
self.do_map = { # 定义插件的入口函数 --
# POCType.POC: self.do_verify,
# POCType.SNIFFER: self.do_sniffer,
# POCType.BRUTE: self.do_brute
}
def __del__(self):
print("资源释放")
def draw_polygon(self, img, polygon_points,color=(0, 255, 0)):
self.polygon = Polygon(polygon_points)
points = np.array([self.polygon.exterior.coords], dtype=np.int32)
cv2.polylines(img, points, isClosed=True, color=color, thickness=2)
def is_point_in_region(self, point):
'''判断点是否在区域内,需要先执行draw_polygon'''
if self.polygon:
return self.polygon.contains(Point(point))
else:
return False
def save_warn(self):
'''保存报警信息'''
pass
def send_warn(self):
'''发送报警信息'''
pass
'''
模型处理的标准接口
1.输入标准化
2.输出标准化
'''
@abstractmethod
def doWork(self,image,mode,class_name,cfg,isdraw_box=False,bgr2rgb=True):
def verify(self,image,data):
'''
:param image: 需要验证的图片
:param data: select t1.model_id,t1.check_area,t1.polygon ,t2.duration_time,t2.proportion,t2.model_path
:return: detections,bwarn,warntext
'''
pass

43
model/plugins/RYRQ_Model.py

@ -1,5 +1,42 @@
from model.plugins.ModelBase import ModelBase
from myutils.ConfigManager import myCongif
from shapely.geometry import Point, Polygon
import torch
import cv2
import av
import numpy as np
class Model(ModelBase):
def doWork(self,image,mode,class_name,cfg,isdraw_box=False,bgr2rgb=True):
print("RYRQ_Model")
def __init__(self,path):
super().__init__()
self.name = "人员入侵"
self.version = "V1.0"
self.model_type = 2
#实例化模型
self.model = torch.hub.load(myCongif.get_data("yolov5_path"), 'custom', path=path, source='local')
def verify(self,image,data):
results = self.model(image) # 进行模型检测 --- 需要统一接口
detections = results.pandas().xyxy[0].to_dict(orient="records")
bwarn = False
warn_text = ""
#是否有检测区域,有先绘制检测区域 由于在该函数生成了polygon对象,所有需要在检测区域前调用。
if data[1] == 1:
self.draw_polygon(image,data[2],(0, 255, 0))
# 绘制检测结果 --- 也需要封装在类里,
for det in detections:
if det['name'] == 'person': #标签是人
x1, y1, x2, y2 = int(det['xmin']), int(det['ymin']), int(det['xmax']), int(det['ymax'])
# 绘制目标识别的锚框
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 = "Intruder detected!"
return detections,bwarn,warn_text

155
model/plugins/yolov5-1.py

@ -0,0 +1,155 @@
# 导入代码依赖
import cv2
import numpy as np
from myutils.ConfigManager import myCongif
import os
import torch
import ipywidgets as widgets
from IPython.display import display
from skvideo.io import vreader, FFmpegWriter
import IPython.display
from ais_bench.infer.interface import InferSession
from det_utils import letterbox, scale_coords, nms
def preprocess_image(image, cfg, bgr2rgb=True):
"""图片预处理"""
img, scale_ratio, pad_size = letterbox(image, new_shape=cfg['input_shape'])
if bgr2rgb:
img = img[:, :, ::-1]
img = img.transpose(2, 0, 1) # HWC2CHW
img = np.ascontiguousarray(img, dtype=np.float32)
return img, scale_ratio, pad_size
def draw_bbox(bbox, img0, color, wt, names):
"""在图片上画预测框"""
det_result_str = ''
for idx, class_id in enumerate(bbox[:, 5]):
if float(bbox[idx][4] < float(0.05)):
continue
img0 = cv2.rectangle(img0, (int(bbox[idx][0]), int(bbox[idx][1])), (int(bbox[idx][2]), int(bbox[idx][3])),
color, wt)
img0 = cv2.putText(img0, str(idx) + ' ' + names[int(class_id)], (int(bbox[idx][0]), int(bbox[idx][1] + 16)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
img0 = cv2.putText(img0, '{:.4f}'.format(bbox[idx][4]), (int(bbox[idx][0]), int(bbox[idx][1] + 32)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
det_result_str += '{} {} {} {} {} {}\n'.format(
names[bbox[idx][5]], str(bbox[idx][4]), bbox[idx][0], bbox[idx][1], bbox[idx][2], bbox[idx][3])
return img0
def get_labels_from_txt(path):
"""从txt文件获取图片标签"""
labels_dict = dict()
with open(path) as f:
for cat_id, label in enumerate(f.readlines()):
labels_dict[cat_id] = label.strip()
return labels_dict
def draw_prediction(pred, image, labels):
"""在图片上画出预测框并进行可视化展示"""
imgbox = widgets.Image(format='jpg', height=720, width=1280)
img_dw = draw_bbox(pred, image, (0, 255, 0), 2, labels)
imgbox.value = cv2.imencode('.jpg', img_dw)[1].tobytes()
display(imgbox)
def infer_image(img_path, model, class_names, cfg):
"""图片推理"""
# 图片载入
image = cv2.imread(img_path)
# 数据预处理
img, scale_ratio, pad_size = preprocess_image(image, cfg)
# 模型推理
output = model.infer([img])[0]
output = torch.tensor(output)
# 非极大值抑制后处理
boxout = nms(output, conf_thres=cfg["conf_thres"], iou_thres=cfg["iou_thres"])
pred_all = boxout[0].numpy()
# 预测坐标转换
scale_coords(cfg['input_shape'], pred_all[:, :4], image.shape, ratio_pad=(scale_ratio, pad_size))
# 图片预测结果可视化
draw_prediction(pred_all, image, class_names)
def infer_frame_with_vis(image, model, labels_dict, cfg, bgr2rgb=True):
# 数据预处理
img, scale_ratio, pad_size = preprocess_image(image, cfg, bgr2rgb)
# 模型推理
output = model.infer([img])[0]
output = torch.tensor(output)
# 非极大值抑制后处理
boxout = nms(output, conf_thres=cfg["conf_thres"], iou_thres=cfg["iou_thres"])
pred_all = boxout[0].numpy()
# 预测坐标转换
scale_coords(cfg['input_shape'], pred_all[:, :4], image.shape, ratio_pad=(scale_ratio, pad_size))
# 图片预测结果可视化
img_vis = draw_bbox(pred_all, image, (0, 255, 0), 2, labels_dict)
return img_vis
def img2bytes(image):
"""将图片转换为字节码"""
return bytes(cv2.imencode('.jpg', image)[1])
def infer_video(video_path, model, labels_dict, cfg):
"""视频推理"""
image_widget = widgets.Image(format='jpeg', width=800, height=600)
display(image_widget)
# 读入视频
cap = cv2.VideoCapture(video_path)
while True:
ret, img_frame = cap.read()
if not ret:
break
# 对视频帧进行推理
image_pred = infer_frame_with_vis(img_frame, model, labels_dict, cfg, bgr2rgb=True)
image_widget.value = img2bytes(image_pred)
def infer_camera(model, labels_dict, cfg):
"""外设摄像头实时推理"""
def find_camera_index():
max_index_to_check = 10 # Maximum index to check for camera
for index in range(max_index_to_check):
cap = cv2.VideoCapture(index)
if cap.read()[0]:
cap.release()
return index
# If no camera is found
raise ValueError("No camera found.")
# 获取摄像头
camera_index = find_camera_index()
cap = cv2.VideoCapture(camera_index)
# 初始化可视化对象
image_widget = widgets.Image(format='jpeg', width=1280, height=720)
display(image_widget)
while True:
# 对摄像头每一帧进行推理和可视化
_, img_frame = cap.read()
image_pred = infer_frame_with_vis(img_frame, model, labels_dict, cfg)
image_widget.value = img2bytes(image_pred)
if __name__ == "__main__":
cfg = {
'conf_thres': 0.4, # 模型置信度阈值,阈值越低,得到的预测框越多
'iou_thres': 0.5, # IOU阈值,高于这个阈值的重叠预测框会被过滤掉
'input_shape': [640, 640], # 模型输入尺寸
}
model_path = os.path.join(myCongif.get_data('weight_path'),'/yolov5-1/yolov5s.pt')
label_path = os.path.join(myCongif.get_data('weight_path'),'/yolov5-1/coco_names.txt')
# 初始化推理模型
model = InferSession(0, model_path)
labels_dict = get_labels_from_txt(label_path)
infer_mode = 'video'
if infer_mode == 'image':
img_path = 'world_cup.jpg'
infer_image(img_path, model, labels_dict, cfg)
elif infer_mode == 'camera':
infer_camera(model, labels_dict, cfg)
elif infer_mode == 'video':
video_path = 'racing.mp4'
infer_video(video_path, model, labels_dict, cfg)

44
util/ConfigManager.py → myutils/ConfigManager.py

@ -7,16 +7,15 @@ import yaml,os
class ConfigManager():
def __init__(self,congif_path='config.yaml'):
self.ok = False
if os.path.exists(congif_path):
self.file_path = congif_path
self.data = self.read_yaml()
self.ok = True
self.ok = self.read_yaml()
else:
congif_path = "../" + congif_path
if os.path.exists(congif_path):
self.file_path = congif_path
self.data = self.read_yaml()
self.ok = True
self.ok= self.read_yaml()
else:
raise Exception('没有找到%s文件路径'%congif_path)
print("ConfigManager实例化")
@ -24,31 +23,34 @@ class ConfigManager():
def __del__(self):
print("ConfigManager销毁")
#已调整方案--配置文件的初始化工作放到构造函数中处理。
def my_init(self,file_path):
if os.path.exists(file_path):
self.file_path = file_path
self.data = self.read_yaml()
self.ok = True
else:
raise Exception('没有找到%s文件路径'%file_path)
def read_yaml(self):
with open(self.file_path,'r',encoding='utf_8') as f:
p = f.read()
return p
self.data = yaml.safe_load(f)
return True
return False
'''
.作为层级节点分割符
'''
def get_data(self,pwd=None):
if self.ok:
result = yaml.load(self.data,Loader=yaml.FullLoader)
if pwd == None:
return result
else:
return result.get(pwd)
if pwd is None:
return None
nodes = pwd.split('.')
current_node= self.data
for node in nodes:
if node in current_node:
current_node = current_node[node]
else:
print(f"Node {node} not found in the YAML data.")
return None
return current_node
#这种方法实现的单例优点是简单,但不能动态创建实例,程序加载时就已实例化。
myCongif = ConfigManager()
if __name__ == '__main__':
r = myCongif.get_data('url')
r = myCongif.get_data('mysql.host1')
print(r)

2
util/MyLogger_logger.py → myutils/MyLogger_logger.py

@ -1,7 +1,7 @@
from loguru import logger
import os
from functools import wraps
from util.ConfigManager import myCongif
from myutils.ConfigManager import myCongif
'''
@logger.catch() 异常捕获注解

0
util/MyLogger_logging.py → myutils/MyLogger_logging.py

67
myutils/ReManager.py

@ -0,0 +1,67 @@
import ipaddress
import re
'''
正则管理类用来实现对各类字符串进行合法性校验
'''
class ReManager():
def is_valid_ip(self,ip):
try:
ipaddress.ip_address(ip)
return True
except ValueError:
return False
def is_valid_subnet_mask(self,subnet_mask):
# 将子网掩码字符串转换为整数列表
bits = [int(b) for b in subnet_mask.split('.')]
if len(bits) != 4:
return False
# 检查每个部分是否在0-255范围内
if not all(0 <= b <= 255 for b in bits):
return False
# 检查子网掩码是否连续为1后跟连续为0
mask_int = (bits[0] << 24) + (bits[1] << 16) + (bits[2] << 8) + bits[3]
binary_mask = bin(mask_int)[2:].zfill(32)
ones_count = binary_mask.count('1')
if not all(bit == '1' for bit in binary_mask[:ones_count]) or not all(
bit == '0' for bit in binary_mask[ones_count:]):
return False
return True
def is_valid_port(self,port):
try:
port = int(port) # 尝试将端口转换为整数
if 1 <= port <= 65535: # 检查端口号是否在合法范围内
return True
else:
return False
except ValueError:
# 如果端口不是整数,返回False
return False
def is_valid_rtsp_url(self,url):
pattern = re.compile(
r'^(rtsp:\/\/)' # Start with rtsp://
r'(?P<host>(?:[a-zA-Z0-9_\-\.]+)|(?:\[[a-fA-F0-9:]+\]))' # Hostname or IPv4/IPv6 address
r'(?::(?P<port>\d+))?' # Optional port number
r'(?P<path>\/[a-zA-Z0-9_\-\.\/]*)?$' # Optional path
)
match = pattern.match(url)
return match is not None
mReM = ReManager()
if __name__ == "__main__":
urls = [
"rtsp://192.168.1.1:554/stream1",
"rtsp://example.com/stream",
"rtsp://[2001:db8::1]:554/stream",
"rtsp://localhost",
"http://example.com",
"ftp://example.com/stream"
]
for url in urls:
print(f"{url}: {mReM.is_valid_rtsp_url(url)}")

42
myutils/loader.py

@ -0,0 +1,42 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'ZL'
import importlib.util
import os
def import_source(module_name,module_path):
module_spec = importlib.util.spec_from_file_location(module_name, module_path) #根据给定的模块名和路径创建一个模块规范对象
module = importlib.util.module_from_spec(module_spec) #根据模块规范创建一个新模块对象
module_spec.loader.exec_module(module) #执行模块的代码,将其导入到新创建的模块对象中
return module
#遍历指定目录的文件,并去除掉一部分文件
def walk_py(path):
for dir_path, dir_names, filenames in os.walk(path):
if dir_path.endswith("__pycache__"):
continue
for f in filenames:
if f.startswith('_') or f.endswith('_.py'):
continue
split = f.split('.')
if len(split) == 2 and split[1] == 'py':
abspath = os.path.abspath(os.path.join(dir_path, f))
yield abspath, split[0]
def load_plugins(path):
plugins = []
for file_path, name in walk_py(path):
try:
module = import_source(spec="repoc_plugins", path=file_path)
Plugin = getattr(module, 'Plugin')
plugin = Plugin()
setattr(plugin, '_plugin_name', name)
plugins.append(plugin)
except Exception as e:
print("load plugin error from {}".format(file_path))
return plugins

0
util/logs/2024-05-25.log → myutils/logs/2024-05-25.log

0
util/logs/app.log → myutils/logs/app.log

BIN
requirements.txt

Binary file not shown.

17
run.py

@ -0,0 +1,17 @@
from myutils.ConfigManager import myCongif
from myutils.MyLogger_logger import LogHandler
from core.ViewManager import mVManager
from web import create_app
import os
#print(f"Current working directory (run.py): {os.getcwd()}")
web = create_app()
if __name__ == '__main__':
# logger = LogHandler().get_logger("main")
# logger.debug("debug")
# logger.info("info")
mVManager.start_check_rtsp()
web.run(debug=True,port=5001)

4
web/API/__init__.py

@ -0,0 +1,4 @@
from quart import Blueprint
#定义模块
api = Blueprint('api',__name__)
from . import user,system,viedo,channel,model

290
web/API/channel.py

@ -0,0 +1,290 @@
import json
from quart import jsonify, request
from . import api
from web.common.utils import login_required
from core.DBManager import mDBM
from myutils.ReManager import mReM
@api.route('/channel/tree',methods=['GET'])
@login_required
async def channel_tree(): #获取通道树
strsql = ("select t2.area_name ,t1.ID ,t1.channel_name from channel t1 left join area t2 on t1.area_id = t2.id;")
data = mDBM.do_select(strsql)
channel_tree = [{"area_name": channel[0], "ID": channel[1],"channel_name":channel[2]} for channel in data]
return jsonify(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"
" from channel t1 left join area t2 on t1.area_id = t2.id;")
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]} for channel in data]
return jsonify(channel_list)
@api.route('/channel/info',methods=['GET'])
@login_required
async def channel_info(): #获取通道信息 ---- list已获取详情
return jsonify(1)
@api.route('/channel/add',methods=['POST'])
@login_required
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 area_name from area where id = {area_id};"
ret = mDBM.do_select(strsql,1)
if ret:
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:
reStatus = 0
reMsg = '添加通道失败,请联系技术支持!'
else:
reStatus = 0
reMsg = '错误的通道ID,请修改'
return jsonify({'status':reStatus,'msg':reMsg})
@api.route('/channel/change',methods=['POST'])
@login_required
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']
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 area_name from area where id = {area_id};"
ret = mDBM.do_select(strsql, 1)
if ret:
strsql = f"UPDATE channel SET area_id={area_id},channel_name='{channel_name}',ulr='{url}' where ID={channel_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '修改通道信息成'
else:
reStatus = 0
reMsg = '修改通道信息失败,请联系技术支持!'
else:
reStatus = 0
reMsg = '错误的通道ID,请修改'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/del',methods=['POST'])
@login_required
async def channel_del(): #删除通道
channel_id = (await request.form)['channel_id']
#删除该通道和算法的关联信息:布防时间,算法模型数据----使用外键级联删除会方便很多,只要一个删除就可以
ret = mDBM.delchannel(channel_id)
if ret == True:
reStatus = 1
reMsg = '删除通道信息成'
else:
reStatus = 0
reMsg = '删除通道信息失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/check',methods=['GET'])
@login_required
async def channel_check(): #检查通道视频的在线情况--可以获取全部 ID-0,全部,1*具体通道ID--10分钟会更新一次在线状态
ID = request.args.get('ID')
if ID:
ID = int(ID)
if ID==0:
strsql = "select ID,status from channel;"
elif ID > 0:
strsql = f"select ID,status from channel where ID={ID};"
else:
reStatus = 0
reMsg = "参数错误"
return jsonify({'status': reStatus, 'msg': reMsg})
datas = mDBM.do_select(strsql)
reStatus = 1
reMsg = [{'ID': data[0], 'status': data[1]} for data in datas]
return jsonify({'status': reStatus, 'msg': reMsg})
else:
reStatus = 0
reMsg = "参数错误"
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/area/list',methods=['GET'])
@login_required
async def channel_area_list(): #获取区域列表
strsql = "select * from area;"
datas = mDBM.do_select(strsql)
reMsg = [{'id': data[0], 'area_name': data[1]} for data in datas]
return jsonify(reMsg)
@api.route('/channel/area/change',methods=['POST'])
@login_required
async def channel_area_change(): #修改区域名称
area_id = (await request.form)['id']
area_name = (await request.form)['name']
strsql = f"update area set area_name='{area_name}' where id = {area_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '修改通道名称成功'
else:
reStatus = 0
reMsg = '修改通道名称失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/area/del',methods=['POST'])
@login_required
async def channel_area_del(): #删除区域
area_id = (await request.form)['id']
strsql = f"select * from channel where area_id={area_id};"
datas = mDBM.do_select(strsql)
if datas:
reStatus = 0
reMsg = '该区域下配置了通道,请先删除通道后,再删除区域!'
else:
strsql = f"delete from area where id={area_id};"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '删除区域成功'
else:
reStatus = 0
reMsg = '删除区域失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/area/add',methods=['POST'])
@login_required
async def channel_area_add(): #添加区域
area_name = (await request.form)['name']
strsql = f"insert into area (area_name) values ('{area_name}');"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '新增区域成功'
else:
reStatus = 0
reMsg = '新增区域失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/model/linklist',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};")
datas = mDBM.do_select(strsql)
reMsg = {}
if datas:
reMsg = [{'ID': data[0], 'model_name': data[1]} for data in datas]
return jsonify(reMsg)
@api.route('/channel/model/list',methods=['GET'])
@login_required
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],'version':data[2]} for data in datas]
return jsonify(reMsg)
@api.route('/channel/model/linkmodel',methods=['POST'])
@login_required
async def channel_model_linkmodel(): #获取算法列表 --关联算法时展示
channel_id = (await request.form)['channel_id']
model_list = json.loads((await request.form)['model_list'])
#? 需要对channel_id和model_list的是否在数据库要进行一个检查
reStatus = 0
reMsg = '提交的参数错误,请检查!'
strsql = f"select ID from channel where ID={channel_id};"
if mDBM.do_select(strsql,1): #通道号在数据库中
for model_id in model_list:
strsql = f"select ID from model where ID = {model_id};"
if not mDBM.do_select(strsql,1):
return jsonify({'status': reStatus, 'msg': reMsg})
ret =mDBM.updateC2M(channel_id,model_list)
if ret == True:
reStatus = 1
reMsg = '修改关联算法成功'
else:
reStatus = 0
reMsg = '修改关联算法失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/model/getarea',methods=['GET'])
@login_required
async def channel_model_getarea(): #获取算法区域的备注信息
ID = request.args.get('ID')
strsql = f"select check_area,check_x,check_y,check_width,check_height from channel2model where ID={ID};"
data = mDBM.do_select(strsql,1)
if data:
reMsg = {'ID':ID,'check_area':data[0],'check_x':data[1],'check_y':data[2],
'check_width':data[3],'check_height':data[4]}
else:
reMsg = {}
return jsonify(reMsg)
@api.route('/channel/model/changearea',methods=['POST'])
@login_required
async def channel_model_changearea(): #修改算法区域信息
ID = (await request.form)['ID']
check_area = (await request.form)['check_area']
check_x = (await request.form)['check_x']
check_y = (await request.form)['check_y']
check_width = (await request.form)['check_width']
check_height = (await request.form)['check_height']
strsql = (f"update channel2model set check_area={check_area},check_x={check_x},check_y={check_y}"
f",check_width={check_width},check_height={check_height} where ID={ID};")
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '修改算法检测区域成功'
else:
reStatus = 0
reMsg = '新修改算法检测区域失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/channel/model/getschedule',methods=['GET'])
@login_required
async def channel_model_getschedule(): #获取算法的布防时间
ID = request.args.get('ID')
strsql = f"select day,hour,status from schedule where channel2model_id={ID};"
datas = mDBM.do_select(strsql)
if datas:
reMsg = [{'day': data[0], 'hour': data[1],'status':data[2]} for data in datas]
else:
reMsg = {}
return jsonify(reMsg)
@api.route('/channel/model/changeschedule',methods=['POST'])
@login_required
async def channel_model_changeschedule(): #修改算法的布防时间
ID = (await request.form)['ID']
schedule_data_str = (await request.form)['schedule_data']
schedule_data = json.loads(schedule_data_str.replace("'", '"'))
for day,hours in schedule_data.items():
for hour,status in enumerate(hours):
strsql = (f"insert into schedule (channel2model_id,day,hour,status) values ({ID},'{day}',{hour},{status})"
f" on conflict(channel2model_id,day,hour) do update set status=excluded.status;")
ret = mDBM.do_sql(strsql)
if ret == False:
reStatus = 0
reMsg = '修改监测区域失败,请联系技术支持!'
return jsonify({'status': reStatus, 'msg': reMsg})
reStatus = 1
reMsg = '修改监测区域成功'
return jsonify({'status': reStatus, 'msg': reMsg})

113
web/API/model.py

@ -0,0 +1,113 @@
import os
from quart import jsonify, request,session,flash,redirect
from . import api
from web.common.utils import login_required
from core.DBManager import mDBM
from core.Upload_file import allowed_file,check_file,updata_model
from myutils.ConfigManager import myCongif
from werkzeug.utils import secure_filename
@api.route('/model/list',methods=['GET'])
@login_required
async def model_list(): #获取算法列表
strsql = "select ID,model_name,version,duration_time,proportion from model;"
datas = mDBM.do_select(strsql)
reMsg = [{"ID":data[0],"name":data[1],"version":data[2],"duration_time":data[3],
"proportion":data[4]} for data in datas]
return jsonify(reMsg)
@api.route('/model/upload',methods=['POST'])
@login_required
async def model_upload(): #上传算法文件--需要进行文件校验
form = await request.form
model_name = form['model_name']
files = await request.files
if 'file' not in files:
flash('参数错误')
return redirect(request.url)
file = files['file']
if file.filename == '':
flash('没有选择文件')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_path = os.path.join(myCongif.get_data('UPLOAD_FOLDER'), filename)
await file.save(file_path)
if check_file(file_path,2): #itype--2 是算法模型升级
strsql = (f"insert into upgrade (itype,filepath,model_name) values (2,'{file_path}','{model_name}')"
f" on conflict(itype,model_name) do update set filepath=excluded.filepath;")
ret = mDBM.do_sql(strsql)
# session['model'] = file_path
if ret:
strsql = f"select id from upgrade where itype=2 and model_name='{model_name}';"
data=mDBM.do_select(strsql,1)
reStatus = data[0]
reMsg = "升级包上传成功"
else:
reStatus = 0
reMsg = "升级包上传失败"
return jsonify({'status': reStatus, 'msg': reMsg})
else:
flash('升级包不合法,请重新上传')
# return redirect(url_for('main.get_html', html='系统管理.html'))
return redirect(request.url)
else:
flash('只允许上传zip文件')
return redirect(request.url)
@api.route('/model/add',methods=['POST'])
@login_required
async def model_add(): #新增算法,需要先上传算法文件
model_name = (await request.form)['model_name']
upgrade_id = (await request.form)['upgrade_id'] #升级文件ID
###---需要根据升级包获取模型文件路径,算法版本号等信息,若根据upgrade_id获取不了对应的信息,则返回报错
###
strsql = f"select ID from model where model_name='{model_name}';"
data = mDBM.do_select(strsql,1)
if data:
reStatus = 0
reMsg = "算法名称重复,请修改!"
else:
strsql = f"insert into model (model_name) values ('{model_name}');" #还有参数未写全
ret = mDBM.do_sql(strsql)
if ret:
reStatus = 1
reMsg = "新增算法成功!"
else:
reStatus = 0
reMsg = "新增算法失败,请联系技术支持!"
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/model/upgrade',methods=['POST'])
@login_required
async def model_upgrade(): #升级算法,需要先上传算法文件
return jsonify(1)
@api.route('/model/config',methods=['GET'])
@login_required
async def model_config(): #获取算法的配置信息 --- list已经获取
ID = request.args.get('ID')
strsql = f"select model_name,version,duration_time,proportion from model where ID={ID};"
data = mDBM.do_select(strsql,1)
reMsg = {"model_name":data[0],"version":data[1],"duration_time":data[2],"proportion":data[3]}
return jsonify(reMsg)
@api.route('/model/changecnf',methods=['POST'])
@login_required
async def model_config_change(): #修改算法的配置信息
ID = (await request.form)['ID']
duration_time = (await request.form)['duration_time']
proportion = float((await request.form)['proportion'])
if proportion>0 and proportion < 1:
strsql = f"update model set duration_time='{duration_time}',proportion='{proportion}' where ID={ID};"
ret = mDBM.do_sql(strsql)
if ret:
reStatus = 1
reMsg = "修复算法配置成功"
else:
reStatus = 0
reMsg = "修复算法配置失败"
else:
reStatus = 0
reMsg = "占比需要为大于0,小于1的值"
return jsonify({'status': reStatus, 'msg': reMsg})

108
web/API/system.py

@ -0,0 +1,108 @@
import os
from quart import jsonify, request,flash,redirect,url_for,session
from . import api
from web.common.utils import login_required
from core.DBManager import mDBM
from core.Upload_file import allowed_file,check_file,update_system
from myutils.ConfigManager import myCongif
from myutils.ReManager import mReM
from werkzeug.utils import secure_filename
@api.route('/system/info',methods=['GET'])
@login_required
async def system_info(): #获取系统信息
strsql = "select * from device;"
data = mDBM.do_select(strsql,1)
strRet = ""
if data:
strRet = {"ID":data[0],"version":data[1],"dev_ip":data[2],"dev_mask":data[3],"dev_gateway":data[4],
"server_ip":data[5],"server_port":data[6]}
return jsonify(strRet) #它将Python对象转换为JSON格式的字符串,并将其作为HTTP响应的主体返回给客户端,
# 同时设置正确的Content-Type响应头,表明响应主体是JSON格式的数据。
@api.route('/system/upload',methods=['POST'])
@login_required
async def system_upload(): #上传系统升级文件
form = await request.form
files = await request.files
if 'file' not in files:
flash('参数错误')
return redirect(request.url)
file = files['file']
if file.filename == '':
flash('没有选择文件')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_path = os.path.join(myCongif.get_data('UPLOAD_FOLDER'), filename)
await file.save(file_path)
if check_file(file_path,1):
session['file'] = file_path
flash('升级包上传成功')
else:
flash('升级包不合法,请重新上传')
#return redirect(url_for('main.get_html', html='系统管理.html'))
return redirect(request.url)
else:
flash('只允许上传zip文件')
return redirect(request.url)
@api.route('/system/upgrade',methods=['POST'])
@login_required
async def system_upgrade(): #系统升级
if 'file' not in session:
reStatus = 0
reMsg = "请先上传升级包文件"
else:
filepath = session['file']
if update_system(filepath):
reStatus = 1
reMsg = "系统升级成功"
else:
reStatus = 0
reMsg = "系统升级失败"
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/system/netinfo',methods=['GET']) #获取系统信息 时已经获取,不再处理
@login_required
async def system_net_info(): #获取网络信息
return jsonify(1)
@api.route('/system/changenet',methods=['POST'])
@login_required
async def system_net_change(): #修改网络信息
ip = (await request.form)['ip']
mask = (await request.form)['mask']
gateway = (await request.form)['gateway']
#对ip,mask.geteway的合法性进行判断
if mReM.is_valid_ip(ip) and mReM.is_valid_ip(gateway) and mReM.is_valid_subnet_mask(mask):
strsql = f"update device set dev_ip='{ip}',dev_mask='{mask}',dev_gateway='{gateway}'"
if mDBM.do_sql(strsql):
reStatus = 1
reMsg = '修改成功,请确认无误后,重启设备!'
else:
reStatus = 0
reMsg = '修改失败,请联系技术支持!'
else:
reStatus = 0
reMsg= '输入的参数不合法,请检查!'
return jsonify({'status':reStatus,'msg':reMsg})
@api.route('/system/changeserver',methods=['POST'])
@login_required
async def system_server_change(): #修改服务器信息
ip = (await request.form)['ip']
port = (await request.form)['port']
# 对ip,mask.geteway的合法性进行判断
if mReM.is_valid_ip(ip) and mReM.is_valid_port(port):
strsql = f"update device set service_ip='{ip}',service_port='{port}';"
if mDBM.do_sql(strsql):
reStatus = 1
reMsg = '修改成功,请确认无误后,重启设备!'
else:
reStatus = 0
reMsg = '修改失败,请联系技术支持!'
else:
reStatus = 0
reMsg = '输入的参数不合法,请检查!'
return jsonify({'status': reStatus, 'msg': reMsg})

114
web/API/user.py

@ -0,0 +1,114 @@
import os
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
from myutils.ConfigManager import myCongif
from core.DBManager import mDBM
from . import api
from web.common.errors import handle_error
@api.route('/user/code',methods=['GET'])
async def user_get_code(): #获取验证码
captcha_text, buffer = generate_captcha()
print(captcha_text)
session['captcha'] = captcha_text # 记录验证码?
return await send_file(buffer, mimetype='image/png')
@api.route('/user/login',methods=['POST'])
async def user_login(): #用户登录
username = (await request.form)['username']
password = (await request.form)['password']
captcha = (await request.form)['captcha']
if captcha != session.get('captcha'):
#验证码验证过后,需要失效
#?
return '验证码错误', 400
#比对用户名和密码
strsql = f"select password from user where username = '{username}'"
db_password = mDBM.do_select(strsql,1)
if db_password:
if db_password[0] == password: #后续需要对密码进行MD5加默
print("登录成功")
session['user'] = username
return redirect(url_for('main.get_html', html='实时预览.html'))
return '用户名或密码错误', 400
@api.route('/user/userinfo',methods=['GET'])
@login_required
async def user_info(): #获取用户列表
strsql = "select username,status,people,tellnum from user;";
data = mDBM.do_select(strsql)
if data:
user_list = [{"username": user[0], "status": user[1],
"people":user[2],"tellnum":user[3]} for user in data]
return jsonify(user_list)
else:
return jsonify(0)
@api.route('/user/adduser',methods=['POST'])
@login_required
async def user_adduser(): #新增用户
username = (await request.form)['username']
people = (await request.form)['people']
tellnum = (await request.form)['tellnum']
strsql = f"select username from user where username = '{username}';"
data = mDBM.do_select(strsql)
if data:
reStatus = 0
reMsg = '用户名重复,请重新输入!'
else:
strsql = (f"INSERT INTO user (username ,password ,status,people,tellnum ) VALUES "
f"('{username}','{myCongif.get_data("pw")}',1,'{people}','{tellnum}');")
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '添加用户成功'
else:
reStatus = 0
reMsg = '添加用户异常,请联系管理员处理!'
return jsonify({'status':reStatus,'msg':reMsg})
@api.route('/user/passwd',methods=['POST'])
@login_required
async def user_change_passwd(): #重置密码
username = (await request.form)['username']
strsql = f"update user set password='{myCongif.get_data("pw")}' where username='{username}';"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '重置密码成功'
else:
reStatus = 0
reMsg = '重置密码失败,请联系管理员处理!'
return jsonify({'status':reStatus,'msg':reMsg})
@api.route('/user/changeuser',methods=['POST'])
@login_required
async def user_change_user_info(): #修改用户信息
username = (await request.form)['username']
people = (await request.form)['people']
tellnum = (await request.form)['tellnum']
strsql = f"update user set people='{people}',tellnum='{tellnum}' where username='{username}';"
ret = mDBM.do_sql(strsql)
if ret == True:
reStatus = 1
reMsg = '修改用户信息成功'
else:
reStatus = 0
reMsg = '修改失败,请联系管理员处理!'
return jsonify({'status': reStatus, 'msg': reMsg})
@api.route('/user/<int:user_id>', methods=['GET'])
async def get_user(user_id):
try:
user = user_id
if user:
return jsonify(user)
else:
return jsonify({'error': 'User not found'}), 404
except Exception as e:
return handle_error(e)

312
web/API/viedo.py

@ -0,0 +1,312 @@
import cv2
import asyncio
import av
import torch
import json
import time
import os
import sys
import numpy as np
from . import api
from quart import jsonify, request,render_template, Response,websocket
from aiortc import RTCPeerConnection, RTCSessionDescription, VideoStreamTrack
from aiortc.contrib.media import MediaBlackhole, MediaPlayer, MediaRecorder
from fractions import Fraction
from shapely.geometry import Point, Polygon
import threading
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
rtsp_url = 'rtsp://127.0.0.1/live1'
#-------------------基于WEBRTC实现拉流
pcs = set() #创建一个空的集合,去重复且无序
'''
--------------基础信息--后续封装在函数里---------------
'''
#print(f"Current working directory (video.py): {os.getcwd()}")
# 自定义模型文件的路径
model_path = 'model/mode_test/yolov5s.pt' # 假设模型文件名为 yolov5s.pt 并与执行文件在同一个目录
# 本地YOLOv5仓库路径
repo_path = 'model/base_model/yolov5'
# 加载自定义模型
model = torch.hub.load(repo_path, 'custom', path=model_path, source='local')
# 定义监控区域(多边形顶点)
region_points = [(100, 100), (500, 100), (500, 400), (100, 400)]
polygon = Polygon(region_points)
# 打开摄像头
def get_camera():
cap = cv2.VideoCapture(0)
if not cap.isOpened():
raise IOError("Cannot open webcam")
return cap
# cap = get_camera()
'''
---------------------传输--------------------------
'''
class VideoTransformTrack(VideoStreamTrack):
kind = "video"
def __init__(self,strUrl,itype=1): #0-usb,1-RTSP,2-海康SDK
super().__init__()
self.cap = cv2.VideoCapture(strUrl)
if not self.cap.isOpened():
raise Exception("Unable to open video source")
# Set desired resolution and frame rate
# self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # Set width to 1280
# self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # Set height to 720
# self.cap.set(cv2.CAP_PROP_FPS, 30) # Set frame rate to 30 FPS
self.frame_rate = int(self.cap.get(cv2.CAP_PROP_FPS)) or 15 # Default to 30 if unable to get FPS
self.time_base = Fraction(1, self.frame_rate)
async def recv(self):
start_time = time.time() # 开始时间
ret, frame = self.cap.read()
if not ret:
raise Exception("Unable to read frame")
# # Convert frame to RGB
# frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
#
# # Convert to VideoFrame
# video_frame = av.VideoFrame.from_ndarray(frame_rgb, format="rgb24")
# # Set timestamp
# video_frame.pts = int(self.cap.get(cv2.CAP_PROP_POS_FRAMES))
# video_frame.time_base = self.time_base
img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
#img = frame.to_ndarray(format="bgr24")
# 使用 Yolov5 进行目标检测
results = model(img)
detections = results.pandas().xyxy[0].to_dict(orient="records")
# 绘制检测结果
for det in detections:
if det['name'] == 'person': # 只检测人员
x1, y1, x2, y2 = int(det['xmin']), int(det['ymin']), int(det['xmax']), int(det['ymax'])
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(img, f"{det['name']} {det['confidence']:.2f}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9,
(0, 255, 0), 2)
# 将检测结果图像转换为帧
new_frame = av.VideoFrame.from_ndarray(img, format="rgb24")
new_frame.pts = int(self.cap.get(cv2.CAP_PROP_POS_FRAMES))
new_frame.time_base = self.time_base
end_time = time.time() # 结束时间
print(f"Processing time: {end_time - start_time} seconds")
return new_frame
@api.route('/offer', methods=['POST'])
async def offer():
#接收客户端的连接请求
params = await request.json
url_type = params["url_type"]
url = params["url"]
#提取客户端发来的SDP,生成服务器端的SDP
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
# # 配置 STUN 服务器
# ice_servers = [{"urls": ["stun:stun.voipbuster.com"]}]
# pc = RTCPeerConnection(configuration={"iceServers": ice_servers})
pc = RTCPeerConnection() #实例化一个rtcp对象
pcs.add(pc) #集合中添加一个对象,若已存在则不添加
@pc.on("datachannel")
def on_datachannel(channel):
@channel.on("message")
def on_message(message):
print(f'Message received: {message}')
# 处理接收到的数据
if message == 'pause':
# 执行暂停操作
pass
elif message == 'resume':
# 执行继续操作
pass
#监听RTC连接状态
@pc.on("iconnectionstatechange") #当ice连接状态发生变化时
async def iconnectionstatechange():
if pc.iceConnectionState == "failed":
await pc.close()
pcs.discard(pc) #移除对象
# 添加视频轨道
video_track = VideoTransformTrack(url,url_type)
pc.addTrack(video_track)
# @pc.on('track') --- stream.getTracks().forEach(track => pc.addTrack(track, stream)); 猜测是这里触发的添加了摄像头通道
# def on_track(track):
# if track.kind == 'video':
# local_video = VideoTransformTrack(track)
# pc.addTrack(local_video)
# 记录客户端 SDP
await pc.setRemoteDescription(offer)
# 生成本地 SDP
answer = await pc.createAnswer()
# 记录本地 SDP
await pc.setLocalDescription(answer)
print("返回sdp")
return jsonify({
"sdp": pc.localDescription.sdp,
"type": pc.localDescription.type
})
@api.route('/shutdown', methods=['POST'])
async def shutdown():
coros = [pc.close() for pc in pcs]
await asyncio.gather(*coros)
pcs.clear()
return 'Server shutting down...'
'''
----------------------实现方式二网页显示分析画面--基础版
'''
def is_point_in_polygon(point, polygon):
return polygon.contains(Point(point))
frame_buffer = []
def camera_thread():
global cap, frame_buffer
while True:
try:
ret, frame = cap.read()
if not ret:
logger.warning("Camera disconnected. Reconnecting...")
cap.release()
cap = get_camera()
continue
frame = cv2.resize(frame, (640, 480)) # 降低视频分辨率
frame_buffer.append(frame)
if len(frame_buffer) > 10: # 保持缓冲区大小不超过10帧
frame_buffer.pop(0)
logger.debug("drop one frame!")
except Exception as e:
logger.error(f"Error in camera thread: {e}")
continue
#threading.Thread(target=camera_thread, daemon=True).start()
async def generate_frames():
global frame_buffer
while True:
frame = None
buffer = None
try:
if frame_buffer:
frame = frame_buffer.pop(0)
# 进行推理
results = model(frame)
detections = results.pandas().xyxy[0]
# 绘制监控区域
cv2.polylines(frame, [np.array(region_points, np.int32)], isClosed=True, color=(0, 255, 0), thickness=2)
for _, row in detections.iterrows():
if row['name'] == 'person':
# 获取人员检测框的中心点
x_center = (row['xmin'] + row['xmax']) / 2
y_center = (row['ymin'] + row['ymax']) / 2
if is_point_in_polygon((x_center, y_center), polygon):
# 触发报警
cv2.putText(frame, 'Alert: Intrusion Detected', (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1,
(0, 0, 255), 2)
#logger.info('Alert: Intrusion Detected')
# 绘制检测框
cv2.rectangle(frame, (int(row['xmin']), int(row['ymin'])), (int(row['xmax']), int(row['ymax'])),
(255, 0, 0), 2)
cv2.circle(frame, (int(x_center), int(y_center)), 5, (0, 0, 255), -1)
ret, buffer = cv2.imencode('.jpg', frame)
if not ret:
continue
frame_bytes = buffer.tobytes()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
await asyncio.sleep(0.2)
except Exception as e:
logger.error(f"Error in generate_frames: {e}")
finally:
if frame is not None:
del frame
if buffer is not None:
del buffer
@api.route('/video_feed')
async def video_feed():
#return jsonify(1)
return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
'''
----------------------实现方式三网页显示分析画面--WEBRTC
'''
class VideoTransformTrack_new(VideoStreamTrack):
def __init__(self, track):
super().__init__()
self.track = track
async def recv(self):
frame = await self.track.recv()
img = frame.to_ndarray(format="bgr24")
# 使用 Yolov5 进行目标检测
results = model(img)
detections = results.pandas().xyxy[0].to_dict(orient="records")
# 绘制检测结果
for det in detections:
if det['name'] == 'person': # 只检测人员
x1, y1, x2, y2 = int(det['xmin']), int(det['ymin']), int(det['xmax']), int(det['ymax'])
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(img, f"{det['name']} {det['confidence']:.2f}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9,
(0, 255, 0), 2)
# 将检测结果图像转换为帧
new_frame = frame.from_ndarray(img, format="bgr24")
return new_frame
pcs = set()
@api.websocket('/ws')
async def ws():
params = await websocket.receive_json()
offer = RTCSessionDescription(sdp=params['sdp'], type=params['type'])
pc = RTCPeerConnection()
pcs.add(pc)
@pc.on('iceconnectionstatechange')
async def on_iceconnectionstatechange():
if pc.iceConnectionState == 'failed':
await pc.close()
pcs.discard(pc)
@pc.on('track')
def on_track(track):
if track.kind == 'video':
local_video = VideoTransformTrack(track)
pc.addTrack(local_video)
await pc.setRemoteDescription(offer)
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
await websocket.send(json.dumps({
'sdp': pc.localDescription.sdp,
'type': pc.localDescription.type
}))
try:
while True:
await asyncio.sleep(1)
finally:
await pc.close()
pcs.discard(pc)

240
web/__init__.py

@ -1,213 +1,47 @@
import os
import sqlite3
import cv2
import asyncio
import av
from aiohttp import web
from quart import Quart, render_template, request, jsonify,send_from_directory
from aiortc import RTCPeerConnection, RTCSessionDescription, VideoStreamTrack, RTCConfiguration, RTCIceServer
from fractions import Fraction
from .main import main as main_blueprint
from quart import Quart, render_template, request, jsonify,send_from_directory,session,redirect, url_for
from quart_session import Session
from .main import main
from .API import api
from functools import wraps
from quart_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
#app.config['SECRET_KEY'] = 'mysecret' #密钥 --需要放配置文件
#socketio = SocketIO(app)
'''
************************
功能函数
'''
rtsp_url = 'rtsp://127.0.0.1/live1'
#-------------------基于WEBRTC实现拉流
pcs = set() #创建一个空的集合,去重复且无序
def create_app():
app = Quart(__name__)
# 注册蓝图
app.register_blueprint(main_blueprint)
#相关配置--设置各种配置选项,这些选项会在整个应用程序中被访问和使用。
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'zfxxkj_2024_!@#'
app.config['SESSION_TYPE'] = 'redis' # session类型
app.config['SESSION_FILE_DIR'] = './sessions' # session保存路径
app.config['SESSION_PERMANENT'] = True # 如果设置为True,则关闭浏览器session就失效。
app.config['SESSION_USE_SIGNER'] = False # 是否对发送到浏览器上session的cookie值进行加密
Session(app)
#ORM数据库管理
# db = SQLAlchemy()
# migrate = Migrate()
# db.init_app(app)
# migrate.init_app(app, db) # 创建对象,使用它来将ORM模型映射到数据库 -- 效果是及时更新数据的修改到文档?
# 注册main
app.register_blueprint(main)
#注册API模块
app.register_blueprint(api,url_prefix = '/api')
return app
class VideoTransformTrack(VideoStreamTrack):
kind = "video"
def __init__(self):
super().__init__()
self.cap = cv2.VideoCapture('rtsp://127.0.0.1/live1')
if not self.cap.isOpened():
raise Exception("Unable to open video source")
# Set desired resolution and frame rate
# self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # Set width to 1280
# self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # Set height to 720
# self.cap.set(cv2.CAP_PROP_FPS, 30) # Set frame rate to 30 FPS
self.frame_rate = int(self.cap.get(cv2.CAP_PROP_FPS)) or 30 # Default to 30 if unable to get FPS
self.time_base = Fraction(1, self.frame_rate)
async def recv(self):
ret, frame = self.cap.read()
if not ret:
raise Exception("Unable to read frame")
# Convert frame to RGB
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Convert to VideoFrame
video_frame = av.VideoFrame.from_ndarray(frame_rgb, format="rgb24")
# Set timestamp
video_frame.pts = int(self.cap.get(cv2.CAP_PROP_POS_FRAMES))
video_frame.time_base = self.time_base
return video_frame
'''
************************
页面路由
'''
@app.route('/')
async def index():
#return await render_template('登录.html')
return await render_template('登录.html')
@app.route('/favicon.ico')
async def favicon():
return await send_from_directory('static', 'favicon.ico')
@app.route('/<html>')
async def get_html(html):
return await render_template(html)
#--------------------webrtc-----------------
async def server(pc, offer):
# 监听 RTC 连接状态
@pc.on("connectionstatechange")
async def on_connectionstatechange():
print("Connection state is %s" % pc.connectionState)
# 当 RTC 连接中断后将连接关闭
if pc.connectionState == "failed":
await pc.close()
pcs.discard(pc)
# 监听客户端发来的视频流
@pc.on("track")
def on_track(track):
print("======= received track: ", track)
if track.kind == "video":
# # 对视频流进行人脸替换
# t = FaceSwapper(track)
# 绑定替换后的视频流
pc.addTrack(track)
# 记录客户端 SDP
await pc.setRemoteDescription(offer)
# 生成本地 SDP
answer = await pc.createAnswer()
# 记录本地 SDP
await pc.setLocalDescription(answer)
@app.route('/offer', methods=['POST'])
async def offer():
#接收客户端的连接请求
params = await request.json
#params = await request.json()
print(params)
#提取客户端发来的SDP,生成服务器端的SDP
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
# Configure ICE servers if necessary
#config = RTCConfiguration([RTCIceServer(urls=['stun:stun.voiparound.com'])])
pc = RTCPeerConnection() #实例化一个rtcp对象
pcs.add(pc) #集合中添加一个对象,若已存在则不添加
print(pc)
@pc.on("datachannel")
def on_datachannel(channel):
@channel.on("message")
def on_message(message):
if isinstance(message, str) and message.startswith("ping"):
channel.send("pong" + message[4:])
#监听RTC连接状态
@pc.on("iconnectionstatechange") #当ice连接状态发生变化时
async def iconnectionstatechange():
if pc.iceConnectionState == "failed":
await pc.close()
pcs.discard(pc) #移除对象
# 添加视频轨道
video_track = VideoTransformTrack()
pc.addTrack(video_track)
print("完成视频轨道的添加")
# 记录客户端 SDP
await pc.setRemoteDescription(offer)
# 生成本地 SDP
answer = await pc.createAnswer()
# 记录本地 SDP
await pc.setLocalDescription(answer)
return jsonify({
"sdp": pc.localDescription.sdp,
"type": pc.localDescription.type
})
@app.route('/shutdown', methods=['POST'])
async def shutdown():
coros = [pc.close() for pc in pcs]
await asyncio.gather(*coros)
pcs.clear()
return 'Server shutting down...'
'''3333
各种配置文件路由
'''
@app.route('/data/<file>')
async def data(file):
return await send_from_directory('static/data',file)
@app.route('/files/<path:subdir>/<file>')
async def files(subdir,file):
return await send_from_directory(os.path.join('static/files', subdir),file)
@app.route('/images/<path:subdir>/<file>')
async def images(subdir,file):
return await send_from_directory(os.path.join('static/images', subdir),file)
@app.route('/resources/<file>')
async def resources(file):
return await send_from_directory('static/resources',file)
@app.route('/resources/<path:subdir>/<file>')
async def resources_dir(subdir,file):
return await send_from_directory(os.path.join('static/resources', subdir),file)
@app.route('/resources/css/<path:subdir>/<file>')
async def resources_css_dir(subdir,file):
return await send_from_directory(os.path.join('static/resources/css', subdir),file)
@app.route('/resources/scripts/<path:subdir>/<file>')
async def resources_scripts_dir(subdir,file):
return await send_from_directory(os.path.join('static/resources/scripts', subdir),file)
'''
后端数据接口
'''
@app.route('/submit', methods=['POST'])
def submit():
if request.method == 'POST':
suggestion = request.form['suggestion']
file = request.files['attachment']
if file:
filename = file.filename
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
# 将意见和附件的文件名保存到数据库
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()
cursor.execute('INSERT INTO suggestions (suggestion, attachment_filename) VALUES (?, ?)',
(suggestion, filename))
conn.commit()
conn.close()
return "Thanks for your suggestion and attachment! Data saved to the database."
# 如果没有附件上传的逻辑处理
return "Data saved!"
def login_required(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('main.login'))
return await f(*args, **kwargs)
return decorated_function
if __name__ == '__main__':
#app.run(debug=True)
app.run(debug=True, host='0.0.0.0', port=5000)

0
ViewManager.py → web/common/__init__.py

4
web/common/errors.py

@ -0,0 +1,4 @@
from quart import jsonify
def handle_error(e):
return jsonify({'error':str(e)}),500

10
web/common/models.py

@ -0,0 +1,10 @@
from app import db
import datetime
class Captcha(db.Model):
id = db.Column(db.Integer, primary_key=True)
captcha_text = db.Column(db.String(10), nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow)
def __init__(self, captcha_text):
self.captcha_text = captcha_text

44
web/common/utils.py

@ -0,0 +1,44 @@
from PIL import Image, ImageDraw, ImageFont,ImageFilter
import random
import string
import os
import io
from functools import wraps
from quart import session, redirect, url_for
def generate_captcha():
characters = string.ascii_uppercase + string.digits
captcha_text = ''.join(random.choices(characters, k=6))
font = ImageFont.truetype("arial.ttf", 36)
image = Image.new('RGB', (200, 60), color=(255, 255, 255))
draw = ImageDraw.Draw(image)
for i in range(6):
draw.text((10 + i * 30, 10), captcha_text[i], font=font,
fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))
# 模糊化处理
image = image.filter(ImageFilter.BLUR)
# 将图片保存到BytesIO流中
buffer = io.BytesIO()
image.save(buffer, 'jpeg')
buffer.seek(0)
# captcha_path = os.path.join('static', 'captcha', f'{captcha_text}.png')
# image.save(captcha_path)
# return captcha_text, f'static/captcha/{captcha_text}.png'
return captcha_text, buffer
def verify_captcha(user_input, actual_captcha):
return user_input == actual_captcha
def login_required(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('main.index',error='未登录,请重新登录'))
return await f(*args, **kwargs)
return decorated_function

2
web/main/__init__.py

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

94
web/main/routes.py

@ -1,10 +1,92 @@
from quart import render_template
from . import main
import os
from web.main import main
from quart import render_template, send_from_directory,request
from quart import session, redirect, url_for,flash
from functools import wraps
from core.Upload_file import allowed_file
from myutils.ConfigManager import myCongif
from werkzeug.utils import secure_filename
'''
页面路由
'''
def login_required(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('main.index',error='未登录,请重新登录'))
return await f(*args, **kwargs)
return decorated_function
@main.route('/')
async def index():
return await render_template('index.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('/', methods=['GET', 'POST'])
# async def upload_file():
# if request.method == 'POST':
# form = await request.form
# files = await request.files
# if 'file' not in files:
# flash('No file part')
# return redirect(request.url)
# file = files['file']
# if file.filename == '':
# flash('No selected file')
# return redirect(request.url)
# if file and allowed_file(file.filename):
# print("上传文件")
# filename = secure_filename(file.filename)
# file_path = os.path.join(myCongif.get_data('UPLOAD_FOLDER'), filename)
# await file.save(file_path)
# flash('File successfully uploaded')
# return redirect(url_for('main.upload_file'))
# else:
# flash('Allowed file types are zip')
# return redirect(request.url)
# return await render_template('upload.html')
@main.route('/favicon.ico')
async def favicon():
return await send_from_directory('web/main/static', 'favicon.ico')
@main.route('/<html>')
@login_required
async def get_html(html):
return await render_template(html)
'''
各种配置文件路由
'''
@main.route('/data/<file>')
async def data(file):
return await send_from_directory('web/main/static/data', file)
@main.route('/files/<path:subdir>/<file>')
async def files(subdir,file):
return await send_from_directory(os.path.join('web/main/static/files', subdir), file)
@main.route('/images/<path:subdir>/<file>')
async def images(subdir,file):
return await send_from_directory(os.path.join('web/main/static/images', subdir), file)
@main.route('/resources/<file>')
async def resources(file):
return await send_from_directory('web/main/static/resources', file)
@main.route('/resources/<path:subdir>/<file>')
async def resources_dir(subdir,file):
return await send_from_directory(os.path.join('web/main/static/resources', subdir), file)
@main.route('/resources/css/<path:subdir>/<file>')
async def resources_css_dir(subdir,file):
return await send_from_directory(os.path.join('web/main/static/resources/css', subdir), file)
@main.route('/about')
async def about():
return await render_template('about.html')
@main.route('/resources/scripts/<path:subdir>/<file>')
async def resources_scripts_dir(subdir,file):
return await send_from_directory(os.path.join('web/main/static/resources/scripts', subdir), file)

0
web/static/data/document.js → web/main/static/data/document.js

0
web/static/data/styles.css → web/main/static/data/styles.css

0
web/static/favicon.ico → web/main/static/favicon.ico

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

0
web/static/files/实时预览/data.js → web/main/static/files/实时预览/data.js

0
web/static/files/实时预览/styles.css → web/main/static/files/实时预览/styles.css

0
web/static/files/用户管理/data.js → web/main/static/files/用户管理/data.js

0
web/static/files/用户管理/styles.css → web/main/static/files/用户管理/styles.css

0
web/static/files/登录/data.js → web/main/static/files/登录/data.js

0
web/static/files/登录/styles.css → web/main/static/files/登录/styles.css

0
web/static/files/算法管理/data.js → web/main/static/files/算法管理/data.js

0
web/static/files/算法管理/styles.css → web/main/static/files/算法管理/styles.css

0
web/static/files/系统管理/data.js → web/main/static/files/系统管理/data.js

0
web/static/files/系统管理/styles.css → web/main/static/files/系统管理/styles.css

0
web/static/files/通道管理/data.js → web/main/static/files/通道管理/data.js

0
web/static/files/通道管理/styles.css → web/main/static/files/通道管理/styles.css

0
web/static/images/实时预览/u20_menu.png → web/main/static/images/实时预览/u20_menu.png

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 429 B

0
web/static/images/实时预览/u22.png → web/main/static/images/实时预览/u22.png

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 278 B

0
web/static/images/实时预览/u26.png → web/main/static/images/实时预览/u26.png

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 283 B

0
web/static/images/实时预览/u32.png → web/main/static/images/实时预览/u32.png

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

0
web/static/images/实时预览/u48.png → web/main/static/images/实时预览/u48.png

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

0
web/static/images/实时预览/u48_selected.png → web/main/static/images/实时预览/u48_selected.png

Before

Width:  |  Height:  |  Size: 176 B

After

Width:  |  Height:  |  Size: 176 B

0
web/static/images/用户管理/u410.png → web/main/static/images/用户管理/u410.png

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 152 B

0
web/static/images/用户管理/u411.png → web/main/static/images/用户管理/u411.png

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

0
web/static/images/用户管理/u412.png → web/main/static/images/用户管理/u412.png

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 220 B

0
web/static/images/用户管理/u413.png → web/main/static/images/用户管理/u413.png

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

0
web/static/images/用户管理/u441.png → web/main/static/images/用户管理/u441.png

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 152 B

0
web/static/images/用户管理/u442.png → web/main/static/images/用户管理/u442.png

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 257 B

0
web/static/images/用户管理/u443.png → web/main/static/images/用户管理/u443.png

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 220 B

0
web/static/images/用户管理/u444.png → web/main/static/images/用户管理/u444.png

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

0
web/static/images/用户管理/u466.png → web/main/static/images/用户管理/u466.png

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 156 B

0
web/static/images/用户管理/u467.png → web/main/static/images/用户管理/u467.png

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 260 B

0
web/static/images/用户管理/u468.png → web/main/static/images/用户管理/u468.png

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 222 B

0
web/static/images/用户管理/u469.png → web/main/static/images/用户管理/u469.png

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

0
web/static/images/用户管理/u497.png → web/main/static/images/用户管理/u497.png

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 156 B

0
web/static/images/用户管理/u498.png → web/main/static/images/用户管理/u498.png

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 260 B

0
web/static/images/用户管理/u499.png → web/main/static/images/用户管理/u499.png

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 222 B

0
web/static/images/用户管理/u500.png → web/main/static/images/用户管理/u500.png

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

0
web/static/images/登录/u12.png → web/main/static/images/登录/u12.png

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 232 KiB

0
web/static/images/登录/u4.svg → web/main/static/images/登录/u4.svg

0
web/static/images/算法管理/u252.png → web/main/static/images/算法管理/u252.png

Before

Width:  |  Height:  |  Size: 153 B

After

Width:  |  Height:  |  Size: 153 B

0
web/static/images/算法管理/u253.png → web/main/static/images/算法管理/u253.png

Before

Width:  |  Height:  |  Size: 231 B

After

Width:  |  Height:  |  Size: 231 B

0
web/static/images/算法管理/u254.png → web/main/static/images/算法管理/u254.png

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 240 B

0
web/static/images/算法管理/u255.png → web/main/static/images/算法管理/u255.png

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

0
web/static/images/算法管理/u256.png → web/main/static/images/算法管理/u256.png

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

0
web/static/images/算法管理/u312.png → web/main/static/images/算法管理/u312.png

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 157 B

0
web/static/images/算法管理/u313.png → web/main/static/images/算法管理/u313.png

Before

Width:  |  Height:  |  Size: 236 B

After

Width:  |  Height:  |  Size: 236 B

0
web/static/images/算法管理/u314.png → web/main/static/images/算法管理/u314.png

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

0
web/static/images/算法管理/u315.png → web/main/static/images/算法管理/u315.png

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

0
web/static/images/算法管理/u316.png → web/main/static/images/算法管理/u316.png

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

0
web/static/images/算法管理/u363.svg → web/main/static/images/算法管理/u363.svg

0
web/static/images/算法管理/u363_disabled.svg → web/main/static/images/算法管理/u363_disabled.svg

0
web/static/images/算法管理/u363_selected.svg → web/main/static/images/算法管理/u363_selected.svg

0
web/static/images/算法管理/u363_selectedDisabled.svg → web/main/static/images/算法管理/u363_selectedDisabled.svg

0
web/static/images/算法管理/u364.svg → web/main/static/images/算法管理/u364.svg

0
web/static/images/算法管理/u364_disabled.svg → web/main/static/images/算法管理/u364_disabled.svg

0
web/static/images/算法管理/u364_selected.svg → web/main/static/images/算法管理/u364_selected.svg

0
web/static/images/算法管理/u364_selectedDisabled.svg → web/main/static/images/算法管理/u364_selectedDisabled.svg

0
web/static/images/算法管理/u366.svg → web/main/static/images/算法管理/u366.svg

0
web/static/images/算法管理/u366_disabled.svg → web/main/static/images/算法管理/u366_disabled.svg

0
web/static/images/算法管理/u366_selected.svg → web/main/static/images/算法管理/u366_selected.svg

0
web/static/images/算法管理/u366_selectedDisabled.svg → web/main/static/images/算法管理/u366_selectedDisabled.svg

0
web/static/images/算法管理/u367.svg → web/main/static/images/算法管理/u367.svg

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save