This commit is contained in:
yueliuli 2026-04-17 11:19:56 +08:00
commit c19a917122
21 changed files with 3900 additions and 0 deletions

59
.gitignore vendored Normal file
View File

@ -0,0 +1,59 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
.venv
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Output (运行生成的结果)
output/
results/
logs/
*.log
runs/
# Model files (大文件模型)
*.pth
*.pt
*.onnx
*.h5
models/
# OS
.DS_Store
Thumbs.db
# Temporary files
*.tmp
temp/
# link
input
model

301
Qwen_app.py Normal file
View File

@ -0,0 +1,301 @@
from lib.qwen_fun import get_annnotated_frame_for_ai_without_xyxy, hazard_inspection, merge_conflict_inspection_data, report_generator, search_knowledge_base
from encodings.punycode import T
"""
测试 在给定标注框与类别原始视频经过转换之后AI能否准确识别物体特征
"""
from tkinter import N
from datetime import datetime
import json
import os
from lib.qwen_fun import save_json_to_file, upload_files_and_get_urls_concurrently
from lib.sam3 import SAM3
import cv2
import json
import numpy as np
from pathlib import Path
import gradio as gr
fps = 0
<<<<<<< HEAD
VIDEO_FOLDER: str = "input"
def load_file_list() -> list[str]:
"""
加载指定文件夹下的所有文件名称不含子目录
返回文件名列表
"""
file_names: list[str] = []
if not os.path.isdir(VIDEO_FOLDER):
return file_names
for item in os.listdir(VIDEO_FOLDER):
item_path: str = os.path.join(VIDEO_FOLDER, item)
if os.path.isfile(item_path):
file_names.append(item)
return file_names
def get_full_vid_path(vid_file: str) -> str:
"""
获取视频绝对路径
"""
full_path: str = os.path.join(os.getcwd(), VIDEO_FOLDER, vid_file)
gr.Info(full_path)
return full_path
def update_preview(frame_idx: int, vid_file: str):
vid_name: str = Path(vid_file).stem
=======
def update_preview(frame_idx: int, vid_name: str):
>>>>>>> 7562de4 (2 预览图还有点问题)
output_dir: str = f"output/{vid_name}"
global fps
idx = int(frame_idx // fps)
img_path = f"{output_dir}/boxes/frame_{idx:04d}.jpg"
with open(f"{output_dir}/hazard_inspection.json", "r", encoding="utf-8") as f:
result = f.read()
dic = json.loads(result)
class_tag = get_class_tag_by_frame(dic, frame_idx, fps)
return img_path, class_tag
def run(vid_file: str, run_sam3: bool = True, run_inspection: bool = True, gen_report: bool = True,):
#================初始化=================
vid_name: str = Path(vid_file).stem
vid_end: str = Path(vid_file).suffix
vid_path: str = f"./input/{vid_name}{vid_end}"
cap = cv2.VideoCapture(vid_path)
global fps
fps = int(cap.get(cv2.CAP_PROP_FPS)) # 获取视频FPS
cap.release()
output_dir: str = f"output/{vid_name}"
# interval = int(fps / 5)
interval = fps
conf = 0.7
time_data: dict = {}
annotated_frames: SAM3 = SAM3()
result: dict = {}
if output_dir:
os.makedirs(output_dir, exist_ok=True) # exist_ok=True 防止重复创建报错
#================获取物体信息=================
# 保存开始时间字符串
time_data["start_time"] = str(datetime.now())
with open(f"{output_dir}/time.json", "w") as f:
f.write(json.dumps(time_data, ensure_ascii=False, indent=4))
if run_sam3:
# 针对厂房防火分区
annotated_frames.run(vid_path, output_dir, "lib/class_list/1.厂房防火.json", interval, conf)
else:
annotated_frames.load_from_json(f"{output_dir}/frame_all.json")
# print(annotated_frames.data)
# 提取ai能看到的部分
ai_frames: dict = get_annnotated_frame_for_ai_without_xyxy(annotated_frames.data(), 1, conf)
save_json_to_file(ai_frames, f"{output_dir}/frame_all_ai.json")
#================隐患检查=================
if run_inspection:
if vid_path.startswith("oss"):
video_url = vid_path
else:
video_url: str|None = None
video_url_file = f"{output_dir}/video_url.json"
# 检查URL是否已存在
if os.path.exists(video_url_file):
try:
with open(video_url_file, "r", encoding="utf-8") as f:
url_data = json.load(f)
if vid_name in url_data:
video_url = url_data[vid_name]
print(f"使用已存在的URL: {video_url}")
except Exception as e:
print(f"读取URL文件失败: {e}")
# 如果URL不存在上传文件
if video_url is None:
print(f"上传视频文件: {vid_path}")
video_url = upload_files_and_get_urls_concurrently(
file_path_list=[vid_path],
max_workers=8
)[0]
if video_url:
# 保存为JSON格式包含文件名和URL的键值对
url_data = {}
if os.path.exists(video_url_file):
try:
with open(video_url_file, "r", encoding="utf-8") as f:
url_data = json.load(f)
except:
pass
url_data[vid_name] = video_url
with open(video_url_file, "w", encoding="utf-8") as f:
json.dump(url_data, f, ensure_ascii=False, indent=4)
print(f"URL已保存: {video_url}")
if video_url is None:
raise ValueError("视频上传失败,无法获取 URL")
<<<<<<< HEAD
result_test: str
reason_test: str
reason_test, result_test = hazard_inspection(ai_frames, video_url, enable_thinking=True, fps=2)
result = json.loads(result_test)
=======
result = json.loads(hazard_inspection(ai_frames, video_url, enable_thinking=False, fps=2)[1])
>>>>>>> 7562de4 (2 预览图还有点问题)
# merged_result = merge_conflict_inspection_data(result)
result["class"] = ai_frames["class_list"]
with open(f"{output_dir}/hazard_inspection.json", "w", encoding="utf-8") as f:
f.write(json.dumps(result, ensure_ascii=False, indent=4))
with open(f"{output_dir}/hazard_inspection_reason.json", "w", encoding="utf-8") as f:
f.write(json.dumps(reason_test, ensure_ascii=False, indent=4))
# with open(f"{output_dir}/hazard_inspection_merged.json", "w", encoding="utf-8") as f:
# f.write(json.dumps(merged_result, ensure_ascii=False, indent=4))
#================生成报告=================
if gen_report:
if result == {}:
with open(f"{output_dir}/hazard_inspection.json", "r", encoding="utf-8") as f:
result = json.load(f)
with open(f"知识库/rule.json", "r", encoding="utf-8") as f:
rule_definitions = json.load(f)
report_generator(
video_path=vid_path, # 视频文件路径
detection_data=annotated_frames.data(), # 物体检测数据
hazard_results=result, # 隐患检查结果
rule_definitions=rule_definitions, # 规则定义
output_path=output_dir, # 输出文件夹
frame_interval=interval, # 帧间隔(可根据实际视频帧率调整)
)
# 保存结束时间字符串
time_data["end_time"] = str(datetime.now())
with open(f"{output_dir}/time.json", "w") as f:
f.write(json.dumps(time_data, ensure_ascii=False, indent=4))
#================更新预览=================
# 获取总帧数
cap = cv2.VideoCapture(vid_path)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
cap.release()
img_path, class_tag = update_preview(0, vid_name)
return gr.update(maximum=total_frames-1, value=0, step = interval), img_path, class_tag
# 创建 Gradio 页面
with gr.Blocks() as demo:
gr.Markdown("# 📸 隐患排查系统 (sam3 + qwen3.5-27b)")
with gr.Row():
with gr.Column():
<<<<<<< HEAD
initial_files: list[str] = load_file_list()
default_video = initial_files[0] if initial_files else None
vid_file = gr.Dropdown(
label="视频",
choices=initial_files,
value=default_video, # 默认选中第一个
interactive=True,
)
=======
vid_name = gr.Textbox(label="视频名称", value="Miehhuoxqih")
vid_end = gr.Textbox(label="视频后缀", value=".AVI")
>>>>>>> 7562de4 (2 预览图还有点问题)
run_sam3 = gr.Checkbox(label="1. 运行 SAM3 模型", value=True)
run_inspection = gr.Checkbox(label="2. 运行隐患排查", value=True)
gen_report = gr.Checkbox(label="3. 生成报告", value=True)
audio_recognition = gr.Checkbox(label="4. 运行音频识别", value=False)
run_button = gr.Button("运行", variant="primary")
get_vid_path_btn = gr.Button("获取视频路径")
full_path_text = gr.Textbox(visible=False)
with gr.Column():
preview = gr.Image(label="预览", scale=2)
img_slider = gr.Slider(label="帧索引", minimum=0, maximum=0, value=0, step=1)
textbox = gr.Textbox(label="隐患结果", lines=10)
run_button.click(fn=run, inputs=[vid_file, run_sam3, run_inspection, gen_report], outputs=[img_slider, preview, textbox])
img_slider.change(fn=update_preview, inputs=[img_slider, vid_file], outputs=[preview, textbox], show_progress="hidden")
get_vid_path_btn.click(fn=get_full_vid_path, inputs=vid_file, outputs=full_path_text)
def get_class_tag_by_frame(data, idx, fps):
"""
根据给定的帧索引 (idx)返回在该帧范围内的所有对象的 class:tag 信息
参数:
data (dict): 包含 'class', 'tag', 'objects' 的字典数据
idx (int): 需要查询的帧索引
返回:
str: 符合条件的 class:tag 列表多个结果之间用换行符分隔如果没有匹配项返回空字符串
"""
# 参数检查
if not isinstance(data, dict):
raise ValueError("数据必须是字典类型")
if 'class' not in data or 'tag' not in data or 'objects' not in data:
raise ValueError("数据必须包含 'class', 'tag''objects'")
class_list = data['class']
tag_list = data['tag']
objects = data['objects']
interval = fps
idx = int(idx / interval) #转换
# 用于存储符合条件的 class:tag 字符串
result = []
all_class_tag = []
# 遍历每个物体
for obj in objects:
# 根据 class_id 和 tag_id 获取对应的字符串
class_str = class_list[obj['class_id']]
tag_str = tag_list[obj['tag_id']]
location = obj.get('location', '')
# 检查帧范围是否包含 idx
if obj['start_frame'] == idx:
result.append(f"{class_str}:{tag_str} (位置: {location})")
all_class_tag.append(f"{class_str}:{tag_str} (开始帧: {obj['start_frame']*interval}, 位置: {location})")
# 使用换行符连接所有结果
output = f"当前帧隐患:\n"+"\n".join(result)+"\n\n"+"所有对象的 class:tag 信息:\n"+"\n".join(all_class_tag)
return output
# 启动应用
if __name__ == "__main__":
demo.launch(
debug=True,
allowed_paths=[VIDEO_FOLDER]
)

65
Qwen_cmd test.py Normal file
View File

@ -0,0 +1,65 @@
from lib.qwen_fun import hazard_inspection, search_knowledge_base
from encodings.punycode import T
"""
测试 在给定标注框与类别原始视频经过转换之后AI能否准确识别物体特征
"""
from tkinter import N
from datetime import datetime
import json
import os
from lib.qwen_fun import chat, get_annnotated_frame_for_ai, get_unique_track_id_count, save_json_to_file, search_knowledge_base, upload_files_and_get_urls_concurrently
from lib.qwen_fun_vid import create_mian_vid_for_ai, frame_all_to_obj_vid
from lib.sam3 import SAM3
from ultralytics.models.sam import SAM3VideoSemanticPredictor
import cv2
import json
import numpy as np
from pathlib import Path
def run(output_dir, vid_path, interval: int = 5):
if output_dir:
os.makedirs(output_dir, exist_ok=True) # exist_ok=True 防止重复创建报错
annotated_frames: SAM3 = SAM3()
annotated_frames.run(vid_path, output_dir, "lib/class_list/xiaofangz.json")
# annotated_frames.load_from_json(f"{output_dir}/frame_all.json")
# print(annotated_frames.data)
# 提取ai能看到的部分
ai_frames: dict = get_annnotated_frame_for_ai(annotated_frames.data(), interval)
save_json_to_file(ai_frames, f"{output_dir}/frame_all_ai.json")
if vid_path.startswith("oss"):
video_url = vid_path
else:
video_url: str|None = upload_files_and_get_urls_concurrently(
file_path_list=[vid_path],
max_workers=8
)[0]
if video_url:
with open(f"{output_dir}/video_url.json", "w") as f:
f.write(video_url)
if video_url is None:
raise ValueError("视频上传失败,无法获取 URL")
result = hazard_inspection(ai_frames, video_url)[1]
with open(f"{output_dir}/hazard_inspection.json", "w", encoding="utf-8") as f:
f.write(result)
# 启动应用
if __name__ == "__main__":
VID_NAME: str = "Peihdianhxiang"
VID_END: str = ".mp4"
interval: int = int(30 / 5)
output_dir: str = f"output/{VID_NAME}"
vid_path: str = f"./input/{VID_NAME}{VID_END}"
run(output_dir, vid_path, interval)

25
README YOLO.md Normal file
View File

@ -0,0 +1,25 @@
**获取miniconda安装包**
Invoke-WebRequest -Uri "https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe" -OutFile ".\Miniconda3-latest-Windows-x86_64.exe"
**激活conda仓库**
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r
conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/msys2
**在目录下进入conda环境**
%WINDIR%\System32\cmd.exe "/K" D:\ProgramData\miniconda3\Scripts\activate.bat D:\ProgramData\miniconda3
**创建虚拟环境**
conda create -n yolo python=3.13 -y
**激活**
conda activate yolo
**取消激活**
conda deactivate
**conda中执行**
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install -U ultralytics
pip install gradio
**使用yolo虚拟环境的python解释器**
D:\ProgramData\miniconda3\envs\yolo\python.exe

10
app_test.py Normal file
View File

@ -0,0 +1,10 @@
from pathlib import Path
import gradio as gr
gr.set_static_paths(paths=[Path.cwd().absolute()/"input"])
with gr.Blocks() as demo:
gr.HTML("<video src='/gradio_api/file=input/Miehhuoxqih.AVI'>")
demo.launch()

View File

@ -0,0 +1,9 @@
{
"Fire Extinguisher": "灭火器",
"Fire Hydrant": "消防栓",
"Safety Exit": "安全出口",
"Emergency Lighting": "应急照明灯",
"Evacuation Sign": "疏散指示标志",
"Obstructions Blocking Fire Lane": "堵塞通道的货物/电瓶车/杂物",
"Obstructions Blocking Safety Exit": "封堵安全出口的障碍物"
}

View File

@ -0,0 +1,3 @@
{
"Distribution Box": "配电箱"
}

1
lib/class_list/text.json Normal file
View File

@ -0,0 +1 @@
["object"]

53
lib/get_prompt.py Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
将Markdown格式提示词转换为AI推理可用格式换行替换为\n转义符
"""
import os
def get_prompt(md_path):
"""
转换Markdown格式提示词为AI推理可用格式
:param input_content: 输入内容md文件路径 md文本字符串
:param is_file: 是否为文件路径True文件路径False直接传入md文本
:param output_file: 输出文件路径可选指定则保存结果到文件
:return: 转换后的AI可用提示词字符串
"""
try:
# 1. 获取原始md内容
# 校验文件是否存在
if not os.path.exists(md_path):
raise FileNotFoundError(f"找不到指定的Markdown文件{md_path}")
# 读取md文件指定utf-8编码避免中文乱码
with open(md_path, 'r', encoding='utf-8') as f:
md_content = f.read()
# 2. 核心转换:将可视化换行(\r\n 或 \n替换为转义换行符 \n
# 先统一处理Windows换行符 \r\n 为 Unix换行符 \n
# ai_prompt = md_content.replace('\r\n', '\n')
# 可选如果需要将连续换行压缩为单个换行避免AI识别多余空行解开下面注释
# import re
# ai_prompt = re.sub(r'\n{2,}', '\n\n', ai_prompt)
# 3. 可选保留Markdown关键格式的轻微优化避免转义破坏md语法
# 例如:保留代码块、标题、列表的格式,仅转换内容中的换行
# 此处无需额外处理,\n本身就可以被大多数AI包括Kimi识别为md换行
# 5. 返回转换后的提示词
return md_content
except Exception as e:
print(f"转换过程中出现错误:{e}")
return None
from datetime import datetime
def _current_time_str():
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 示例用法
if __name__ == "__main__":
md_file_path = "./提示词 v2.md"
prompt = get_prompt(md_file_path)
if prompt:
print(f"[{_current_time_str()}][获取提示词] 转换成功AI可用提示词如下")
print(prompt)

47
lib/json_fun.py Normal file
View File

@ -0,0 +1,47 @@
import json
from collections import defaultdict
def f_detections_to_objects(detections_path: str, output_path: str):
"""
将输入文件中的yolo检测数据转换为object数据并保存到输出文件中
:param detections_path: 输入文件路径
:param output_path: 输出文件路径
:return: None
"""
with open(detections_path, 'r', encoding='utf-8') as f:
detections_data = json.load(f)
class_set = set()
track_info = defaultdict(lambda: {"class_id": None, "start_frame": float('inf'), "end_frame": 0})
for frame_str, detections in detections_data.items():
frame = int(frame_str)
for det in detections:
track_id = det["track_id"]
class_id = det["class_id"]
class_str = det["class_str"]
class_set.add(class_str)
if track_info[track_id]["class_id"] is None:
track_info[track_id]["class_id"] = class_id
if frame < track_info[track_id]["start_frame"]:
track_info[track_id]["start_frame"] = frame
if frame > track_info[track_id]["end_frame"]:
track_info[track_id]["end_frame"] = frame
result = {
"class_list": list(class_set),
"track_id_list": dict(track_info)
}
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
f_detections_to_objects(
"output/santai5/frame_detections.json",
"output/santai5/objects.json"
)

638
lib/qwen_fun copy.py Normal file
View File

@ -0,0 +1,638 @@
import json
import os
from pathlib import Path
import requests
from openai import BadRequestError, OpenAI
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
import cv2
# 图片列表:键为下拉选项名称,值为包含网络地址和本地地址的字典
# 注意请使用有效的图片URL或将图片下载到本地后使用本地路径
file_dict: dict = {}
# file_info_path: str = "file_dict.json"
file_info_path: str = r"D:\Userfile\Downloads\export_urls.csv"
# 设置环境变量(请替换为您自己的 API Key
os.environ["DASHSCOPE_API_KEY"] = 'sk-c7ffed1b32794284a5d21b046d7d0008'
os.environ["MAXKB_API_KEY"] = 'agent-300b841bd6a7cb3a0bf603c093c22398'
os.environ["MODEL_NAME"] = 'qwen3.5-27b'
def chat(prompt, file_path: str | list, enable_thinking=False, fps=10, thinking_budget: int=81920):
"""
调用 DashScope 接口进行图片理解与提示词检测
参数 img_info 为包含网络和本地路径的字典
"""
print("开始处理视频...")
print("当前文件: ", file_path)
print("============提示词============")
print(prompt)
enable_thinking = enable_thinking
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
messages = []
if type(file_path) == str:
if '.jpg' in file_path.lower():
file_path = [["img", file_path]]
elif '.mp4' or '.avi' in file_path.lower():
file_path = [["vid", file_path, fps]]
content: list = []
for item in file_path:
if item[0] == "img":
content.append({"type": "image_url", "image_url": {"url": item[1]}})
elif item[0] == "vid":
content.append({"type": "video_url", "video_url": {"url": item[1]}, "fps": item[2]})
else:
raise ValueError(f"不支持的文件类型:{item[0]}", 99999)
content.append({"type": "text", "text": prompt})
messages = [{
"role": "user",
"content": content
}]
# 开启流式输出
try:
completion = client.chat.completions.create(
model=os.getenv("MODEL_NAME"), # type: ignore
messages=messages, # type: ignore
extra_body={"enable_thinking": enable_thinking, "thinking_budget": thinking_budget},
stream=True,
# 支持访问图片和视频的OSS协议 URL
extra_headers={"X-DashScope-OssResourceResolve": "enable"}
) # type: ignore
except BadRequestError as e:
# 这里捕捉到特定的 400 错误
# 判断是否是图片相关的错误
if "Failed to download multimodal content" in str(e):
# 返回空的模型回复,并返回错误信息
raise ValueError("❌ 图片链接不存在或已过期")
# return "", "❌ 图片链接不存在或已过期"
else:
# 其他错误直接抛出或返回通用错误
raise ValueError(f"❌ 发生错误: {str(e)}")
except Exception as e:
# 捕捉其他未知错误
raise ValueError(f"❌ 未知错误: {str(e)}")
# 捕获流式数据
reasoning_text = ""
answer_text = ""
is_answering = False
if enable_thinking:
print("============思考过程============")
for chunk in completion:
delta = chunk.choices[0].delta
# 思考过程
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
reasoning_text += delta.reasoning_content
print(delta.reasoning_content, end="")
# 最终答案
if hasattr(delta, "content") and delta.content:
if not is_answering:
is_answering = True
print("============最终答案============")
answer_text += delta.content
print(delta.content, end="")
print() # 换行
# 返回格式化后的结果
return reasoning_text, answer_text
def upload_file_and_get_url(file_path: str) -> str:
"""
单个文件上传到 OSS 并返回 URL内部使用
"""
# 1. 获取上传策略
policy_resp = requests.get(
"https://dashscope.aliyuncs.com/api/v1/uploads",
headers={"Authorization": f"Bearer {os.getenv('DASHSCOPE_API_KEY')}"},
params={"action": "getPolicy", "model": os.getenv('MODEL_NAME')}
)
# 检查请求是否成功
if policy_resp.status_code != 200:
raise Exception(f"获取上传策略失败,状态码: {policy_resp.status_code}, 响应: {policy_resp.text}")
# 尝试获取 'data' 字段
policy_json = policy_resp.json()
if 'data' not in policy_json:
raise Exception(f"获取上传策略失败,返回内容不包含 'data' 字段: {policy_json}")
policy_data = policy_json['data']
# 2. 上传文件到 OSS
file_name = Path(file_path).name
key = f"{policy_data['upload_dir']}/{file_name}"
with open(file_path, 'rb') as f:
files = {
'OSSAccessKeyId': (None, policy_data['oss_access_key_id']),
'Signature': (None, policy_data['signature']),
'policy': (None, policy_data['policy']),
'x-oss-object-acl': (None, policy_data['x_oss_object_acl']),
'x-oss-forbid-overwrite': (None, policy_data['x_oss_forbid_overwrite']),
'key': (None, key),
'success_action_status': (None, '200'),
'file': (file_name, f)
}
response = requests.post(policy_data['upload_host'], files=files)
if response.status_code != 200:
raise Exception(f"文件上传失败: {file_path}, 状态码: {response.status_code}, 响应: {response.text}")
return f"oss://{key}"
def upload_files_and_get_urls_concurrently(
file_path_list: list[str],
max_workers: int | None = None,
timeout: int = 300
) -> list[str | None]:
"""
并发上传多个文件到 OSS 并返回对应的 URL 列表顺序对应
"""
# 初始化一个与文件列表长度相同的结果列表
results: list[str | None] = [None] * len(file_path_list)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 使用字典存储 future 与其对应的索引
future_to_index = {
executor.submit(
upload_file_and_get_url,
file_path
): idx
for idx, file_path in enumerate(file_path_list)
}
for future in as_completed(future_to_index, timeout=timeout):
idx = future_to_index[future]
file_path = file_path_list[idx]
try:
url = future.result()
results[idx] = url # 将结果放回对应的索引位置
print(f"[成功] {file_path} -> {url}")
except Exception as exc:
# 捕获异常并打印错误信息
print(f"[错误] {file_path} 上传失败: {exc}")
results[idx] = None # 保持为 None 或者设为错误标识
return results
def save_json_to_file(json_data, file_path: str) -> None:
"""
保存 JSON 数据到文件
参数:
json_data: 要保存的 JSON 数据
file_path: 文件路径
"""
# 1. 提取目录路径
dir_path = os.path.dirname(file_path)
# 2. 如果有目录路径,确保它存在
if dir_path:
os.makedirs(dir_path, exist_ok=True) # exist_ok=True 防止重复创建报错
# 3. 写入文件
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)
except Exception as e:
print(e)
return
print(f"已保存 JSON 数据到: {file_path}")
def load_json_data(json_path: str):
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
def get_unique_track_id_count(json_path: str) -> int:
"""
提取并计算JSON文件中不重复的track_id总数
参数:
json_path: JSON文件路径
返回:
不重复的track_id总数
"""
# 加载JSON数据
data = load_json_data(json_path)
# 存储唯一的track_id
unique_track_ids = set()
# 遍历所有帧
for frame_data in data.values():
# 遍历当前帧的所有检测结果
for detection in frame_data:
# 提取track_id并添加到集合中
if 'track_id' in detection:
unique_track_ids.add(detection['track_id'])
# 返回唯一track_id的数量
return len(unique_track_ids)
def get_annnotated_frame_for_ai(json_data: dict, interval: int = 5) -> dict:
"""
frame_all.json 文件中提取ai能看到的部分并重新计算帧号
按照指定格式重构返回结果
输出json_data格式
{
"class_list": ["class_name1", "class_name2", ..."], # 类别名称
"frame_list": {
"0": [ # frame_idx
{
"xyxy": [
1903.0,
167.0,
1919.0,
335.0
],
"track_id": 8,
"class_id": 0
},
...
],
},
}
"""
# 抽取0,5,10...并重命名帧号为0,1,2...
frame_ids: list = list(json_data.keys())
selected_frames: list = frame_ids[::interval]
# 提取所有类别名称(去重)
class_set = set()
for frame_id in selected_frames:
frame_data = json_data[frame_id]
for obj in frame_data:
class_set.add(obj["class_str"])
class_list = sorted(list(class_set))
# 建立类别名称到ID的映射
class_name_to_id = {name: idx for idx, name in enumerate(class_list)}
# 构建frame_list
frame_list = {}
for new_idx, frame_id in enumerate(selected_frames):
frame_data = json_data[frame_id]
obj_list = []
for obj in frame_data:
obj_list.append({
"xyxy": obj["xyxy"],
"track_id": obj["track_id"],
"class_id": class_name_to_id[obj["class_str"]]
})
frame_list[str(new_idx)] = obj_list
# 重构返回结果
result = {
"class_list": class_list,
"frame_list": frame_list
}
return result
def get_annnotated_frame_for_ai_without_xyxy(json_data: dict, interval: int = 5) -> dict:
"""
frame_all.json 文件中提取ai能看到的部分并重新计算帧号
按照指定格式重构返回结果
输出json_data格式
{
"class_list": ["class_name1", "class_name2", ..."], # 类别名称
"frame_list": {
"0": [ # frame_idx
{
"track_id": 8,
"class_id": 0
},
...
],
},
}
"""
# 抽取0,5,10...并重命名帧号为0,1,2...
frame_ids: list = list(json_data.keys())
selected_frames: list = frame_ids[::interval]
# 提取所有类别名称(去重)
class_set = set()
for frame_id in selected_frames:
frame_data = json_data[frame_id]
for obj in frame_data:
class_set.add(obj["class_str"])
class_list = sorted(list(class_set))
# 建立类别名称到ID的映射
class_name_to_id = {name: idx for idx, name in enumerate(class_list)}
# 构建frame_list
frame_list = {}
for new_idx, frame_id in enumerate(selected_frames):
frame_data = json_data[frame_id]
obj_list = []
for obj in frame_data:
obj_list.append({
"track_id": obj["track_id"],
"class_id": class_name_to_id[obj["class_str"]]
})
frame_list[str(new_idx)] = obj_list
# 重构返回结果
result = {
"class_list": class_list,
"frame_list": frame_list
}
return result
def search_knowledge_base(
question: str,
system_prompt: str = "你是科技有限公司 MaxKB 知识问答系统的智能小,你的工作是 MaxKB 用户解答中,用户找你回答问题时,你要把主题放在 MaxKB 知识问答系统身上",
**kwargs
):
"""
通过 MaxKB 知识库问答系统获取关于 'MaxKB 是什么?' 的答案
"""
# 1. 请求 URL
url = "http://localhost:8080/chat/api/019d5265-e90e-7663-b202-fc5a47d8601c/chat/completions"
# 2. 请求头
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer agent-300b841bd6a7cb3a0bf603c093c22398"
}
# 3. 请求体Payload
payload = {
"model": "qwen3.5-27b",
"messages": [
{
"role": system_prompt,
"content": question
}
]
}
# 4. 发送 POST 请求
response = requests.post(url, headers=headers, json=payload)
# 5. 处理响应
if response.status_code == 200:
try:
# 尝试解析为 JSON
data = response.json()
# 打印完整的响应结构(调试用)
print("Full Response:")
print(json.dumps(data, ensure_ascii=False, indent=2))
# 尝试提取回答内容(根据常见的结构)
# 这里假设答案在 data['choices'][0]['message']['content'] 中
if 'choices' in data and data['choices']:
answer = data['choices'][0].get('message', {}).get('content', '')
print("\n=== MaxKB 回答 ===")
print(answer.strip())
return answer.strip()
else:
print("\n未在响应中找到 'choices' 字段。")
return ""
except Exception as e:
print("解析响应时出错:", e)
print("原始响应:", response.text)
return ""
else:
print(f"请求失败,状态码: {response.status_code}")
print("错误信息:", response.text)
return ""
def hazard_inspection(
class_dict: dict,
video_url: str,
enable_thinking: bool = True,
fps: int = 10
) -> tuple[str, str]:
"""
检查视频中给定类型物体的隐患
2026.04.07
参数:
class_dict: 类别字典
input_video_path: 视频路径
enable_thinking: 是否开启思考模式
fps
class_dict:结构:
{
"class_list": ["class_name1", "class_name2", ..."], # 类别名称
"frame_list": {
"0": [ # frame_idx
{
"track_id": 8,
"class_id": 0
},
...
],
},
}
"""
class_data: str = str(class_dict)
# 筛选检查规则 知识库\rule.json
all_check_rule_dict: dict = load_json_data(r'知识库\rule.json')
check_rule_list: list = []
# 筛选出当前视频中存在的检查规则
for class_str in class_dict["class_list"]:
if class_str in all_check_rule_dict:
check_rule_list.append(all_check_rule_dict[class_str])
# 读取提示词文件 7_检测提示词_视频.md
prompt_str: str = ""
with open(r'prompt\7_检测提示词_视频_new3.md', 'r', encoding='utf-8') as file:
prompt_str = file.read()
prompt: str = f"""
# 每帧存在的物体类别\n
{class_data}
\n
# 检查规则\n
{check_rule_list}
\n
{prompt_str}
"""
# 获取大模型对话结果
return chat(prompt, video_url, enable_thinking, fps)
def merge_conflict_inspection_data(data: dict) -> dict:
"""
合并冲突数据相同 tag_id class_id 的重叠时间段
参数:
- data: 包含 "class", "tag", "objects" 键的字典
返回:
- 合并后的数据字典结构与输入相同 objects 已合并
"""
# 1. 按照 (tag_id, class_id) 对对象进行分组
groups = {}
for obj in data["objects"]:
key = (obj["tag_id"], obj["class_id"])
groups.setdefault(key, []).append(obj)
merged_objects = []
new_id = 1
# 2. 对每个分组进行合并
for (tag_id, class_id), objs in groups.items():
# 按 start_frame 排序
sorted_objs = sorted(objs, key=lambda x: x["start_frame"])
# 初始化第一个区间
cur_start = sorted_objs[0]["start_frame"]
cur_end = sorted_objs[0]["end_frame"]
cur_level = sorted_objs[0]["level"]
for obj in sorted_objs[1:]:
# 如果当前区间与下一个区间重叠或相连end >= start
if obj["start_frame"] <= cur_end:
# 合并区间:取结束帧的最大值
cur_end = max(cur_end, obj["end_frame"])
# 级别取最大值(如果级别不同)
cur_level = max(cur_level, obj["level"])
else:
# 当前区间结束,保存合并结果
merged_objects.append({
"hazard_track_id": new_id,
"tag_id": tag_id,
"class_id": class_id,
"level": cur_level,
"start_frame": cur_start,
"end_frame": cur_end
})
new_id += 1
# 开始新的区间
cur_start = obj["start_frame"]
cur_end = obj["end_frame"]
data["objects"] = merged_objects
return data
def report_generator(
video_path: str,
detection_data: dict,
hazard_results: dict,
rule_definitions: dict,
output_path: str = "output",
frame_interval: float = 1.0
):
"""
生成隐患报告的主函数
参数:
video_path (str): 视频文件路径
detection_data (dict): 物体检测数据格式为 {frame_id: [detection_info, ...]}
hazard_results (dict): 隐患检查结果
rule_definitions (dict): 隐患规则定义
output_path (str): 输出文件夹路径默认 'output'
frame_interval (float): 帧间隔时间用于计算实际帧号默认 1.0
功能:
1. 创建必要的文件夹结构
2. 根据隐患时间范围截取对应的图片
3. 生成 Markdown 报告
"""
# ----------------------- 1. 准备工作 -----------------------
# 创建文件夹结构
report_dir = os.path.join(output_path, "report")
assets_dir = os.path.join(report_dir, "_assets")
os.makedirs(assets_dir, exist_ok=True)
# 打开视频文件
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise IOError(f"无法打开视频文件: {video_path}")
# 获取视频总帧数(用于安全检查)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# ----------------------- 2. 处理隐患数据并截取图片 -----------------------
# 用于存储 Markdown 表格的行数据
md_table_rows = []
# 遍历所有隐患对象
for idx, obj in enumerate(hazard_results["objects"], start=1):
# 计算实际的起始帧(考虑帧间隔)
start_frame = int(obj["start_frame"] * frame_interval)
# 防止帧号越界
start_frame = max(0, min(start_frame, total_frames - 1))
# 获取隐患对应的类名和标签
class_name = hazard_results["class"][obj["class_id"]]
tag_name = hazard_results["tag"][obj["tag_id"]]
level = obj["level"]
location = obj.get("location", "")
# ----------------------- 2.1. 截取图片 -----------------------
# 默认选择 start_frame 作为截图帧
target_frame = start_frame
# 截图并保存
img_filename = f"{idx}_{target_frame}.jpg"
img_path = os.path.join(assets_dir, img_filename)
# 读取对应帧并保存整帧图片
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
ret, frame = cap.read()
if ret:
cv2.imwrite(img_path, frame)
# ----------------------- 2.2. 生成 Markdown 表格行 -----------------------
# 生成图片引用的 Markdown 语法
img_markdown = f"![](./_assets/{img_filename})"
# 隐患依据
rule_info = rule_definitions.get(class_name, {})
# 默认取第一个风险等级(如 '重大隐患'
level_key = next(iter(rule_info.keys())) if rule_info else ""
# 获取具体的隐患描述信息
hazard_desc = rule_info.get(level_key, {}).get(tag_name, {})
basis = hazard_desc.get("依据", [])
# 只取前 2 条依据作为展示
basis_text = "<br>".join(basis[:2]) if basis else "暂无依据"
# 组装表格行
row = f"|{idx}|{level}|{basis_text}|{obj['start_frame']}|{location}|{img_markdown}|"
md_table_rows.append(row)
# ----------------------- 3. 生成 Markdown 报告 -----------------------
md_content = f"""# 隐患报告
## 隐患清单
| 隐患序号 | 隐患等级 | 依据 | 开始帧 | 位置描述 | 照片 |
| :---: | :---: | :--- | :---: | :--- | :---: |
"""
md_content += "\n".join(md_table_rows)
# 写入报告文件
report_path = os.path.join(report_dir, "report.md")
with open(report_path, "w", encoding="utf-8") as f:
f.write(md_content)
# 释放资源
cap.release()
print(f"报告已生成: {report_path}")

760
lib/qwen_fun.py Normal file
View File

@ -0,0 +1,760 @@
import json
import os
from pathlib import Path
import requests
from openai import BadRequestError, OpenAI
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
import cv2
# 图片列表:键为下拉选项名称,值为包含网络地址和本地地址的字典
# 注意请使用有效的图片URL或将图片下载到本地后使用本地路径
file_dict: dict = {}
# file_info_path: str = "file_dict.json"
file_info_path: str = r"D:\Userfile\Downloads\export_urls.csv"
# 设置环境变量(请替换为您自己的 API Key
os.environ["DASHSCOPE_API_KEY"] = 'sk-c7ffed1b32794284a5d21b046d7d0008'
os.environ["MAXKB_API_KEY"] = 'agent-300b841bd6a7cb3a0bf603c093c22398'
os.environ["MODEL_NAME"] = 'qwen3.5-27b'
def chat(prompt, file_path: str | list, enable_thinking=False, fps=10, thinking_budget: int=81920):
"""
调用 DashScope 接口进行图片理解与提示词检测
参数 img_info 为包含网络和本地路径的字典
"""
print("开始处理视频...")
print("当前文件: ", file_path)
print("============提示词============")
print(prompt)
enable_thinking = enable_thinking
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
messages = []
if type(file_path) == str:
if '.jpg' in file_path.lower():
file_path = [["img", file_path]]
elif '.mp4' or '.avi' in file_path.lower():
file_path = [["vid", file_path, fps]]
content: list = []
for item in file_path:
if item[0] == "img":
content.append({"type": "image_url", "image_url": {"url": item[1]}})
elif item[0] == "vid":
content.append({"type": "video_url", "video_url": {"url": item[1]}, "fps": item[2]})
else:
raise ValueError(f"不支持的文件类型:{item[0]}", 99999)
content.append({"type": "text", "text": prompt})
messages = [{
"role": "user",
"content": content
}]
# 开启流式输出
try:
completion = client.chat.completions.create(
model=os.getenv("MODEL_NAME"), # type: ignore
messages=messages, # type: ignore
extra_body={"enable_thinking": enable_thinking, "thinking_budget": thinking_budget},
stream=True,
# 支持访问图片和视频的OSS协议 URL
extra_headers={"X-DashScope-OssResourceResolve": "enable"}
) # type: ignore
except BadRequestError as e:
# 这里捕捉到特定的 400 错误
# 判断是否是图片相关的错误
if "Failed to download multimodal content" in str(e):
# 返回空的模型回复,并返回错误信息
raise ValueError("❌ 图片链接不存在或已过期")
# return "", "❌ 图片链接不存在或已过期"
else:
# 其他错误直接抛出或返回通用错误
raise ValueError(f"❌ 发生错误: {str(e)}")
except Exception as e:
# 捕捉其他未知错误
raise ValueError(f"❌ 未知错误: {str(e)}")
# 捕获流式数据
reasoning_text = ""
answer_text = ""
is_answering = False
if enable_thinking:
print("============思考过程============")
for chunk in completion:
delta = chunk.choices[0].delta
# 思考过程
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
reasoning_text += delta.reasoning_content
print(delta.reasoning_content, end="")
# 最终答案
if hasattr(delta, "content") and delta.content:
if not is_answering:
is_answering = True
print("============最终答案============")
answer_text += delta.content
print(delta.content, end="")
print() # 换行
# 返回格式化后的结果
return reasoning_text, answer_text
def upload_file_and_get_url(file_path: str) -> str:
"""
单个文件上传到 OSS 并返回 URL内部使用
"""
# 1. 获取上传策略
policy_resp = requests.get(
"https://dashscope.aliyuncs.com/api/v1/uploads",
headers={"Authorization": f"Bearer {os.getenv('DASHSCOPE_API_KEY')}"},
params={"action": "getPolicy", "model": os.getenv('MODEL_NAME')}
)
# 检查请求是否成功
if policy_resp.status_code != 200:
raise Exception(f"获取上传策略失败,状态码: {policy_resp.status_code}, 响应: {policy_resp.text}")
# 尝试获取 'data' 字段
policy_json = policy_resp.json()
if 'data' not in policy_json:
raise Exception(f"获取上传策略失败,返回内容不包含 'data' 字段: {policy_json}")
policy_data = policy_json['data']
# 2. 上传文件到 OSS
file_name = Path(file_path).name
key = f"{policy_data['upload_dir']}/{file_name}"
with open(file_path, 'rb') as f:
files = {
'OSSAccessKeyId': (None, policy_data['oss_access_key_id']),
'Signature': (None, policy_data['signature']),
'policy': (None, policy_data['policy']),
'x-oss-object-acl': (None, policy_data['x_oss_object_acl']),
'x-oss-forbid-overwrite': (None, policy_data['x_oss_forbid_overwrite']),
'key': (None, key),
'success_action_status': (None, '200'),
'file': (file_name, f)
}
response = requests.post(policy_data['upload_host'], files=files)
if response.status_code != 200:
raise Exception(f"文件上传失败: {file_path}, 状态码: {response.status_code}, 响应: {response.text}")
return f"oss://{key}"
def upload_files_and_get_urls_concurrently(
file_path_list: list[str],
max_workers: int | None = None,
timeout: int = 300
) -> list[str | None]:
"""
并发上传多个文件到 OSS 并返回对应的 URL 列表顺序对应
"""
# 初始化一个与文件列表长度相同的结果列表
results: list[str | None] = [None] * len(file_path_list)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 使用字典存储 future 与其对应的索引
future_to_index = {
executor.submit(
upload_file_and_get_url,
file_path
): idx
for idx, file_path in enumerate(file_path_list)
}
for future in as_completed(future_to_index, timeout=timeout):
idx = future_to_index[future]
file_path = file_path_list[idx]
try:
url = future.result()
results[idx] = url # 将结果放回对应的索引位置
print(f"[成功] {file_path} -> {url}")
except Exception as exc:
# 捕获异常并打印错误信息
print(f"[错误] {file_path} 上传失败: {exc}")
results[idx] = None # 保持为 None 或者设为错误标识
return results
def save_json_to_file(json_data, file_path: str) -> None:
"""
保存 JSON 数据到文件
参数:
json_data: 要保存的 JSON 数据
file_path: 文件路径
"""
# 1. 提取目录路径
dir_path = os.path.dirname(file_path)
# 2. 如果有目录路径,确保它存在
if dir_path:
os.makedirs(dir_path, exist_ok=True) # exist_ok=True 防止重复创建报错
# 3. 写入文件
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)
except Exception as e:
print(e)
return
print(f"已保存 JSON 数据到: {file_path}")
def load_json_data(json_path: str):
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
def get_unique_track_id_count(json_path: str) -> int:
"""
提取并计算JSON文件中不重复的track_id总数
参数:
json_path: JSON文件路径
返回:
不重复的track_id总数
"""
# 加载JSON数据
data = load_json_data(json_path)
# 存储唯一的track_id
unique_track_ids = set()
# 遍历所有帧
for frame_data in data.values():
# 遍历当前帧的所有检测结果
for detection in frame_data:
# 提取track_id并添加到集合中
if 'track_id' in detection:
unique_track_ids.add(detection['track_id'])
# 返回唯一track_id的数量
return len(unique_track_ids)
def get_annnotated_frame_for_ai_without_xyxy(json_data: dict, interval: int = 5, conf: float = 0.7) -> dict:
"""
frame_all.json 文件中提取ai能看到的部分并重新计算帧号
按照指定格式重构返回结果
输出json_data格式
{
"class_list": ["class_name1", "class_name2", ..."], # 类别名称
"track_id_list": {
"0": { # track_id
"class_id": 0,
"start_frame": 0,
"end_frame": 10
},
...
},
}
"""
# 抽取0,5,10...并重命名帧号为0,1,2...
frame_ids: list = list(json_data.keys())
selected_frames: list = frame_ids[::interval]
# 提取所有类别名称(去重)
class_set = set()
for frame_id in selected_frames:
frame_data = json_data[frame_id]
for obj in frame_data:
class_set.add(obj["class_str"])
class_list = sorted(list(class_set))
# 建立类别名称到ID的映射
class_name_to_id = {name: idx for idx, name in enumerate(class_list)}
# 构建track_id_list
track_id_dict = {}
for new_idx, frame_id in enumerate(selected_frames):
frame_data = json_data[frame_id]
for obj in frame_data:
if obj["confidence"] < conf:
continue
track_id = obj["track_id"]
class_id = class_name_to_id[obj["class_str"]]
if track_id not in track_id_dict:
track_id_dict[track_id] = {
"class_id": class_id,
"start_frame": new_idx,
"end_frame": new_idx
}
else:
track_id_dict[track_id]["end_frame"] = new_idx
# 重构返回结果
result = {
"class_list": class_list,
"track_id_list": track_id_dict
}
return result
def search_knowledge_base(
question: str,
system_prompt: str = "你是科技有限公司 MaxKB 知识问答系统的智能小,你的工作是 MaxKB 用户解答中,用户找你回答问题时,你要把主题放在 MaxKB 知识问答系统身上",
**kwargs
):
"""
通过 MaxKB 知识库问答系统获取关于 'MaxKB 是什么?' 的答案
"""
# 1. 请求 URL
url = "http://localhost:8080/chat/api/019d5265-e90e-7663-b202-fc5a47d8601c/chat/completions"
# 2. 请求头
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer agent-300b841bd6a7cb3a0bf603c093c22398"
}
# 3. 请求体Payload
payload = {
"model": "qwen3.5-27b",
"messages": [
{
"role": system_prompt,
"content": question
}
]
}
# 4. 发送 POST 请求
response = requests.post(url, headers=headers, json=payload)
# 5. 处理响应
if response.status_code == 200:
try:
# 尝试解析为 JSON
data = response.json()
# 打印完整的响应结构(调试用)
print("Full Response:")
print(json.dumps(data, ensure_ascii=False, indent=2))
# 尝试提取回答内容(根据常见的结构)
# 这里假设答案在 data['choices'][0]['message']['content'] 中
if 'choices' in data and data['choices']:
answer = data['choices'][0].get('message', {}).get('content', '')
print("\n=== MaxKB 回答 ===")
print(answer.strip())
return answer.strip()
else:
print("\n未在响应中找到 'choices' 字段。")
return ""
except Exception as e:
print("解析响应时出错:", e)
print("原始响应:", response.text)
return ""
else:
print(f"请求失败,状态码: {response.status_code}")
print("错误信息:", response.text)
return ""
def hazard_inspection(
class_dict: dict,
video_url: str,
enable_thinking: bool = True,
fps: int = 10
) -> tuple[str, str]:
"""
检查视频中给定类型物体的隐患
2026.04.07
参数:
class_dict: 类别字典
input_video_path: 视频路径
enable_thinking: 是否开启思考模式
fps
class_dict:结构:
{
"class_list": ["class_name1", "class_name2", ..."], # 类别名称
"track_id_list": {
"0": { # track_id
"class_id": 0,
"start_frame": 0,
"end_frame": 10
},
...
},
}
返回:
tuple[str, str]: (reasoning_text, json_result)
json_result: JSON格式的隐患检测结果
"""
# 筛选检查规则 知识库\rule.json
all_check_rule_dict: dict = load_json_data(r'知识库\rule.json')
# 读取提示词文件 7_检测提示词_视频.md
prompt_str: str = ""
with open(r'prompt\7_检测提示词_视频_new3.md', 'r', encoding='utf-8') as file:
prompt_str = file.read()
# 存储所有类别的结果
all_reasoning_text = ""
# 存储所有类别的 JSON 结果
all_tags = []
all_bases = []
all_objects = []
# 保存每个类别的原始 tag 和 base 映射,用于后续重新映射
class_tag_mapping = {}
class_base_mapping = {}
# 遍历每个类别,分别进行 chat
<<<<<<< HEAD
print("类别列表:", class_dict["class_list"])
for class_str in class_dict["class_list"]:
print(f"当前类别: {class_str}")
# 检查该类别是否有检查规则
class_check_rule: str = ""
scene_list: list[str] = list(all_check_rule_dict.keys())
print(f"场景列表: {scene_list}")
for scene in scene_list:
if class_str in all_check_rule_dict[scene].keys():
class_check_rule = all_check_rule_dict[scene][class_str]
break
if class_check_rule == "":
# 该类别没有检查规则
continue
=======
for class_str in class_dict["class_list"]:
# 检查该类别是否有检查规则
if class_str not in all_check_rule_dict["(一)厂房防火分区"].keys():
continue
# 获取该类别的规则
class_check_rule = all_check_rule_dict["(一)厂房防火分区"][class_str]
>>>>>>> 7562de4 (2 预览图还有点问题)
# 筛选出该类别的 track_id_list
class_track_info = []
for track_id, track_data in class_dict["track_id_list"].items():
track_class_name = class_dict["class_list"][track_data["class_id"]]
if track_class_name == class_str:
class_track_info.append(f"Track ID {track_id}: {track_class_name} (帧 {track_data['start_frame']}-{track_data['end_frame']})")
# 如果该类别没有跟踪对象,跳过
if not class_track_info:
continue
track_data_str = "\n".join(class_track_info)
# 构建该类别的 prompt
prompt: str = f"""
# 类别: {class_str}
# 跟踪对象信息
{track_data_str}
# 检查规则
{class_check_rule}
{prompt_str}
"""
# 调用 chat 函数
reasoning_text, answer_text = chat(prompt, video_url, enable_thinking, fps)
# 整合思考过程
all_reasoning_text += f"\n\n# 类别: {class_str}\n{reasoning_text}"
# 解析 JSON 结果
try:
# 提取 JSON 部分(去除可能的代码块标记)
json_str = answer_text.strip()
if json_str.startswith("```json"):
json_str = json_str[7:]
if json_str.startswith("```"):
json_str = json_str[3:]
if json_str.endswith("```"):
json_str = json_str[:-3]
json_str = json_str.strip()
class_result = json.loads(json_str)
# 收集该类别的结果
if "tag" in class_result:
# 保存该类别的 tag 映射
class_tag_mapping[class_str] = class_result["tag"]
all_tags.extend(class_result["tag"])
if "base" in class_result:
# 保存该类别的 base 映射
class_base_mapping[class_str] = class_result["base"]
all_bases.extend(class_result["base"])
if "objects" in class_result:
all_objects.extend(class_result["objects"])
except json.JSONDecodeError as e:
print(f"解析类别 {class_str} 的 JSON 结果失败: {e}")
print(f"原始输出: {answer_text}")
continue
# 合并去重 tag 和 base
unique_tags = []
seen_tags = set()
for tag in all_tags:
if tag not in seen_tags:
seen_tags.add(tag)
unique_tags.append(tag)
unique_bases = []
seen_bases = set()
for base in all_bases:
if base not in seen_bases:
seen_bases.add(base)
unique_bases.append(base)
# 重新分配 tag_id 和 base_id
# 由于每个类别是独立处理的,需要根据 class_id 找到对应的类别,然后重新映射
# 但是 object 中的 class_id 是相对于 class_list 的,需要找到对应的类别名称
final_objects = []
for obj in all_objects:
new_obj = obj.copy()
new_obj["hazard_track_id"] = len(final_objects)
# 通过 class_id 找到类别名称
class_id = obj.get("class_id", 0)
if class_id < len(class_dict["class_list"]):
class_name = class_dict["class_list"][class_id]
# 重新映射 tag_id
if "tag_id" in obj and class_name in class_tag_mapping:
original_tags = class_tag_mapping[class_name]
if obj["tag_id"] < len(original_tags):
original_tag = original_tags[obj["tag_id"]]
if original_tag in unique_tags:
new_obj["tag_id"] = unique_tags.index(original_tag)
# 重新映射 base_id
if "base_id" in obj and class_name in class_base_mapping:
original_bases = class_base_mapping[class_name]
if obj["base_id"] < len(original_bases):
original_base = original_bases[obj["base_id"]]
if original_base in unique_bases:
new_obj["base_id"] = unique_bases.index(original_base)
final_objects.append(new_obj)
# 构建最终结果
final_result = {
"tag": unique_tags,
"base": unique_bases,
"objects": final_objects
}
# 转换为 JSON 字符串
json_result = json.dumps(final_result, ensure_ascii=False, indent=2)
return all_reasoning_text, json_result
def merge_conflict_inspection_data(data: dict) -> dict:
"""
合并冲突数据相同 tag_id class_id 的重叠时间段
参数:
- data: 包含 "class", "tag", "objects" 键的字典
返回:
- 合并后的数据字典结构与输入相同 objects 已合并
"""
# 1. 按照 (tag_id, class_id) 对对象进行分组
groups = {}
for obj in data["objects"]:
key = (obj["tag_id"], obj["class_id"])
groups.setdefault(key, []).append(obj)
merged_objects = []
new_id = 1
# 2. 对每个分组进行合并
for (tag_id, class_id), objs in groups.items():
# 按 start_frame 排序
sorted_objs = sorted(objs, key=lambda x: x["start_frame"])
# 初始化第一个区间
cur_start = sorted_objs[0]["start_frame"]
cur_end = sorted_objs[0]["end_frame"]
cur_level = sorted_objs[0]["level"]
for obj in sorted_objs[1:]:
# 如果当前区间与下一个区间重叠或相连end >= start
if obj["start_frame"] <= cur_end:
# 合并区间:取结束帧的最大值
cur_end = max(cur_end, obj["end_frame"])
# 级别取最大值(如果级别不同)
cur_level = max(cur_level, obj["level"])
else:
# 当前区间结束,保存合并结果
merged_objects.append({
"hazard_track_id": new_id,
"tag_id": tag_id,
"class_id": class_id,
"level": cur_level,
"start_frame": cur_start,
"end_frame": cur_end
})
new_id += 1
# 开始新的区间
cur_start = obj["start_frame"]
cur_end = obj["end_frame"]
data["objects"] = merged_objects
return data
def get_hazard_level_from_rule(rule_definitions: dict, class_name: str, tag_name: str) -> str:
"""
从知识库规则中获取隐患等级
参数:
rule_definitions (dict): 隐患规则定义
class_name (str): 隐患类别名称"灭火器"
tag_name (str): 隐患标签名称"灭火器被遮挡"
返回:
str: 隐患等级"重大隐患"未找到时返回"一般隐患"
"""
class_rules = rule_definitions.get(class_name, {})
if not class_rules:
return "一般隐患"
for level_name, level_content in class_rules.items():
if isinstance(level_content, dict) and tag_name in level_content:
return level_name
return "一般隐患"
def map_original_level(level: int) -> str:
"""
将原始 level 映射为相似度描述
参数:
level (int): 原始等级1表示一般2表示明显
返回:
str: 相似度描述"疑似""高度相似"
"""
return "高度相似" if level == 2 else "疑似"
def report_generator(
video_path: str,
detection_data: dict,
hazard_results: dict,
rule_definitions: dict,
output_path: str = "output",
frame_interval: float = 1.0
):
"""
生成隐患报告的主函数
参数:
video_path (str): 视频文件路径
detection_data (dict): 物体检测数据格式为 {frame_id: [detection_info, ...]}
hazard_results (dict): 隐患检查结果
rule_definitions (dict): 隐患规则定义
output_path (str): 输出文件夹路径默认 'output'
frame_interval (float): 帧间隔时间用于计算实际帧号默认 1.0
功能:
1. 创建必要的文件夹结构
2. 根据隐患时间范围截取对应的图片
3. 生成 Markdown 报告
"""
# ----------------------- 1. 准备工作 -----------------------
# 创建文件夹结构
report_dir = os.path.join(output_path, "report")
assets_dir = os.path.join(report_dir, "_assets")
os.makedirs(assets_dir, exist_ok=True)
# 打开视频文件
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise IOError(f"无法打开视频文件: {video_path}")
# 获取视频总帧数(用于安全检查)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# ----------------------- 2. 处理隐患数据并截取图片 -----------------------
# 用于存储 Markdown 表格的行数据
md_table_rows = []
# 遍历所有隐患对象
for idx, obj in enumerate(hazard_results["objects"], start=1):
# 计算实际的起始帧(考虑帧间隔)
start_frame = int(obj["start_frame"] * frame_interval)
# 防止帧号越界
start_frame = max(0, min(start_frame, total_frames - 1))
# 获取隐患对应的类名和标签
class_name = hazard_results["class"][obj["class_id"]]
tag_name = hazard_results["tag"][obj["tag_id"]]
original_level = obj["level"]
location = obj.get("location", "")
# 从知识库规则中获取隐患等级
rule_level = get_hazard_level_from_rule(rule_definitions, class_name, tag_name)
# 将原始 level 映射为相似度描述
similarity = map_original_level(original_level)
# ----------------------- 2.1. 截取图片 -----------------------
# 默认选择 start_frame 作为截图帧
target_frame = start_frame
# 截图并保存
img_filename = f"{idx}_{target_frame}.jpg"
img_path = os.path.join(assets_dir, img_filename)
# 读取对应帧并保存整帧图片
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
ret, frame = cap.read()
if ret:
cv2.imwrite(img_path, frame)
# ----------------------- 2.2. 生成 Markdown 表格行 -----------------------
# 生成图片引用的 Markdown 语法
img_markdown = f"![](./_assets/{img_filename})"
# 只取前 2 条依据作为展示
base_text = hazard_results["base"][obj["base_id"]]
# 组装表格行
row = f"|{idx}|{rule_level}|{similarity}|{base_text}|{start_frame}|{location}|{img_markdown}|"
md_table_rows.append(row)
# ----------------------- 3. 生成 Markdown 报告 -----------------------
md_content = f"""# 隐患报告
## 隐患清单
| 隐患序号 | 隐患等级 | 相似度 | 依据 | 开始帧 | 位置描述 | 照片 |
| :---: | :---: | :---: | :--- | :---: | :--- | :---: |
"""
md_content += "\n".join(md_table_rows)
# 写入报告文件
report_path = os.path.join(report_dir, "report.md")
with open(report_path, "w", encoding="utf-8") as f:
f.write(md_content)
# 释放资源
cap.release()
print(f"报告已生成: {report_path}")

491
lib/qwen_fun_vid.py Normal file
View File

@ -0,0 +1,491 @@
import os
from pathlib import Path
import numpy as np
import supervision as sv
import cv2
from concurrent.futures import ThreadPoolExecutor, as_completed
def generate_video_with_boxes(
boxes_data: list[dict],
input_video_path: str,
output_video_path: str,
frame_annotation_interval: int
) -> None:
"""
将提供的标注数据渲染到视频上支持按帧间隔进行标注
:param boxes_data: 包含标注信息的列表每个元素结构为:
{
"frame_id": int,
"boxes": List[Tuple[int, int, int, int, str]] # (x1, y1, x2, y2, label)
}
:param input_video_path: 输入视频文件路径
:param output_video_path: 输出视频文件路径
:param frame_annotation_interval: 标注间隔单位默认为 1每帧标注
:return:
"""
# -------------------------------------------------
# 1. 视频读取与基本配置
# -------------------------------------------------
cap = cv2.VideoCapture(input_video_path)
if not cap.isOpened():
raise FileNotFoundError(f"无法打开视频文件或流: {input_video_path}")
fps = cap.get(cv2.CAP_PROP_FPS)
video_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
video_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# -------------------------------------------------
# 2. 初始化 VideoWriter
# -------------------------------------------------
Path(os.path.dirname(output_video_path)).mkdir(parents=True, exist_ok=True)
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # type: ignore
out = cv2.VideoWriter(output_video_path, fourcc, fps, (video_width, video_height))
# -------------------------------------------------
# 3. 初始化标注工具
# -------------------------------------------------
box_annotator = sv.BoxAnnotator()
label_annotator = sv.RichLabelAnnotator(
font_path="C:/Windows/Fonts/simhei.ttf",
text_color=sv.Color.WHITE,
text_padding=5,
font_size=20
)
# -------------------------------------------------
# 4. 数据预处理:构建 frame_id -> boxes 的映射
# -------------------------------------------------
frame_to_boxes: dict[int, list[tuple[int, int, int, int, str]]] = {}
for entry in boxes_data:
frame_id = entry["frame_id"]
boxes = entry["boxes"]
# 确保每个框都有正确的结构
cleaned_boxes = []
for box in boxes:
if len(box) == 5:
cleaned_boxes.append(box) # (x1, y1, x2, y2, label)
frame_to_boxes[frame_id] = cleaned_boxes
# -------------------------------------------------
# 5. 主循环:逐帧读取并渲染
# -------------------------------------------------
frame_idx = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 计算当前帧对应的标注帧索引(考虑间隔)
annotation_frame_idx = frame_idx // frame_annotation_interval
# 如果当前帧没有标注数据,直接写入原始帧
if annotation_frame_idx not in frame_to_boxes:
out.write(frame)
frame_idx += 1
continue
# 获取当前帧的所有框信息
boxes_info = frame_to_boxes[annotation_frame_idx]
boxes = []
labels = []
for _, (x1, y1, x2, y2, label) in enumerate(boxes_info):
# 坐标
boxes.append([x1, y1, x2, y2])
# 使用索引作为唯一标识符,或直接使用 label
labels.append(label)
# 转换为 NumPy 数组
boxes_np = np.array(boxes, dtype=np.float64)
# 构建 Detections 对象
detections = sv.Detections(
xyxy=boxes_np,
confidence=np.ones(len(boxes_np)), # 默认置信度为 1.0
class_id=np.zeros(len(boxes_np), dtype=int) # 类别 ID 在此场景下不重要
)
detections.tracker_id = np.arange(len(boxes_np), dtype=int) # 使用索引作为 ID
# 绘制边框和标签
annotated_frame = box_annotator.annotate(scene=frame.copy(), detections=detections)
annotated_frame = label_annotator.annotate(
scene=annotated_frame,
detections=detections,
labels=labels
)
# 写入帧
out.write(annotated_frame)
frame_idx += 1
# -------------------------------------------------
# 6. 资源释放
# -------------------------------------------------
cap.release()
out.release()
cv2.destroyAllWindows()
print(f"视频渲染完成,已保存至: {output_video_path}")
def process_track_id(
track_id: int,
frame_list: list[tuple[int, list[int]]],
input_video_path: str,
output_video_root: str,
frame_width: int,
frame_height: int,
target_fps: int,
frame_interval: int
) -> str:
"""
处理单个track_id生成对应的视频
"""
# 计算需要生成的总帧数(确保覆盖所有物体帧且不少于两秒)
min_frames_for_2s = 25 * 2 # 2秒 @ 25fps
object_based_frames = len(frame_list) * frame_interval
max_output_frame = max(object_based_frames, min_frames_for_2s)
# 创建输出视频路径
output_path = os.path.join(output_video_root, f"{track_id}.mp4")
# 尝试使用GPU硬件编码
try:
# 对于不同平台的GPU编码使用不同的fourcc
# Windows平台使用h264_nvenc或h264_amf
# 如果GPU编码不可用会回退到CPU编码
fourcc = cv2.VideoWriter_fourcc(*'h264') # type: ignore
out = cv2.VideoWriter(output_path, fourcc, target_fps, (frame_width, frame_height))
# 检查是否成功打开
if not out.isOpened():
# 尝试其他编码方式
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # type: ignore
out = cv2.VideoWriter(output_path, fourcc, target_fps, (frame_width, frame_height))
except Exception:
# 异常时使用CPU编码
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # type: ignore
out = cv2.VideoWriter(output_path, fourcc, target_fps, (frame_width, frame_height))
if not out.isOpened():
print(f"无法创建视频文件: {output_path}")
return f"失败: {output_path}"
# 打开原视频(每个线程独立打开,避免线程安全问题)
cap = cv2.VideoCapture(input_video_path)
if not cap.isOpened():
out.release()
return f"失败: 无法打开视频 {input_video_path}"
# 生成视频帧
current_output_frame = 0
obj_frame_idx = 0
while current_output_frame < max_output_frame:
# 检查当前输出帧是否是5的倍数
if current_output_frame % frame_interval == 0 and obj_frame_idx < len(frame_list):
# 这是需要放置物体帧的位置
original_frame_id, xyxy = frame_list[obj_frame_idx]
# 设置原视频读取位置
cap.set(cv2.CAP_PROP_POS_FRAMES, original_frame_id)
ret, frame = cap.read()
if not ret:
# 读取失败,使用黑色帧
output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
else:
# 有数据,截取对应区域
x1, y1, x2, y2 = map(int, xyxy)
# 确保坐标在有效范围内
x1 = max(0, min(x1, frame_width))
y1 = max(0, min(y1, frame_height))
x2 = max(0, min(x2, frame_width))
y2 = max(0, min(y2, frame_height))
# 创建黑色背景
output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
# 将截取的区域放到输出帧中(保持原位置)
if x2 > x1 and y2 > y1:
cropped = frame[y1:y2, x1:x2]
output_frame[y1:y2, x1:x2] = cropped
# 移到下一个物体帧
obj_frame_idx += 1
else:
# 剩余帧留黑
output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
# 写入帧
out.write(output_frame)
current_output_frame += 1
# 释放资源
out.release()
cap.release()
print(f"已生成视频: {output_path}, 共 {current_output_frame}")
return f"成功: {output_path}"
def frame_all_to_obj_vid(
json_data: dict,
input_video_path: str,
output_video_root: str,
) -> None:
"""
根据标注数据从原视频中截取物体生成ai读取专用视频
参数:
json_data: 标注数据
input_video_path: 原视频路径
output_video_root: 输出视频根目录
"""
# 确保输出目录存在
os.makedirs(output_video_root, exist_ok=True)
# 1. 从 json_data 中提取数据,按 track_id 组织
track_dict: dict[int, list[tuple[int, list[int]]]] = {}
# 遍历每一帧
for frame_id_str, detections in json_data.items():
frame_id = int(frame_id_str)
for det in detections:
track_id = det.get("track_id", -1)
xyxy = det.get("xyxy", [0, 0, 0, 0])
if track_id not in track_dict:
track_dict[track_id] = []
track_dict[track_id].append((frame_id, xyxy))
# 2. 获取视频信息(只需要获取一次)
temp_cap = cv2.VideoCapture(input_video_path)
if not temp_cap.isOpened():
raise ValueError(f"无法打开视频: {input_video_path}")
frame_width = int(temp_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(temp_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
temp_cap.release()
# 目标fps为25每5帧取一帧0, 5, 10...
target_fps = 25
frame_interval = 5 # 每隔5帧取一帧
# 3. 使用多线程并行处理多个track_id
# 根据CPU核心数设置线程池大小
max_workers = min(os.cpu_count() or 4, len(track_dict))
print(f"使用 {max_workers} 个线程并行处理")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交所有任务
future_to_track = {
executor.submit(
process_track_id,
track_id,
frame_list,
input_video_path,
output_video_root,
frame_width,
frame_height,
target_fps,
frame_interval
):
track_id for track_id, frame_list in track_dict.items()
}
# 等待所有任务完成
for future in as_completed(future_to_track):
track_id = future_to_track[future]
try:
result = future.result()
print(f"Track ID {track_id}: {result}")
except Exception as e:
print(f"Track ID {track_id} 处理失败: {str(e)}")
# def frame_all_to_obj_vid(
# json_data: dict,
# input_video_path: str,
# output_video_root: str,
# ) -> None:
# """
# 根据标注数据从原视频中截取物体生成ai读取专用视频
# 参数:
# json_data: 标注数据
# input_video_path: 原视频路径
# output_video_root: 输出视频根目录
# """
# # 确保输出目录存在
# os.makedirs(output_video_root, exist_ok=True)
# # 1. 从 json_data 中提取数据,按 track_id 组织
# track_dict: dict[int, list[tuple[int, list[int]]]] = {}
# # 遍历每一帧
# for frame_id_str, detections in json_data.items():
# frame_id = int(frame_id_str)
# for det in detections:
# track_id = det.get("track_id", -1)
# xyxy = det.get("xyxy", [0, 0, 0, 0])
# if track_id not in track_dict:
# track_dict[track_id] = []
# track_dict[track_id].append((frame_id, xyxy))
# # 2. 打开原视频
# cap = cv2.VideoCapture(input_video_path)
# if not cap.isOpened():
# raise ValueError(f"无法打开视频: {input_video_path}")
# # 获取原视频信息
# original_fps = cap.get(cv2.CAP_PROP_FPS)
# total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
# frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# # 目标fps为25每5帧取一帧0, 5, 10...
# target_fps = 25
# frame_interval = 5 # 每隔5帧取一帧
# # 为每个 track_id 生成视频
# for track_id, frame_list in track_dict.items():
# # 计算需要生成的总帧数(确保覆盖所有物体帧且不少于两秒)
# min_frames_for_2s = 25 * 2 # 2秒 @ 25fps
# object_based_frames = len(frame_list) * frame_interval
# max_output_frame = max(object_based_frames, min_frames_for_2s)
# # 创建输出视频路径
# output_path = os.path.join(output_video_root, f"{track_id}.mp4")
# # 创建视频写入器
# fourcc = cv2.VideoWriter_fourcc(*'mp4v') # type: ignore
# out = cv2.VideoWriter(output_path, fourcc, target_fps, (frame_width, frame_height))
# if not out.isOpened():
# print(f"无法创建视频文件: {output_path}")
# continue
# # 生成视频帧
# current_output_frame = 0
# obj_frame_idx = 0
# while current_output_frame < max_output_frame:
# # 检查当前输出帧是否是5的倍数
# if current_output_frame % frame_interval == 0 and obj_frame_idx < len(frame_list):
# # 这是需要放置物体帧的位置
# original_frame_id, xyxy = frame_list[obj_frame_idx]
# # 设置原视频读取位置
# cap.set(cv2.CAP_PROP_POS_FRAMES, original_frame_id)
# ret, frame = cap.read()
# if not ret:
# # 读取失败,使用黑色帧
# output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
# else:
# # 有数据,截取对应区域
# x1, y1, x2, y2 = map(int, xyxy)
# # 确保坐标在有效范围内
# x1 = max(0, min(x1, frame_width))
# y1 = max(0, min(y1, frame_height))
# x2 = max(0, min(x2, frame_width))
# y2 = max(0, min(y2, frame_height))
# # 创建黑色背景
# output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
# # 将截取的区域放到输出帧中(保持原位置)
# if x2 > x1 and y2 > y1:
# cropped = frame[y1:y2, x1:x2]
# output_frame[y1:y2, x1:x2] = cropped
# # 移到下一个物体帧
# obj_frame_idx += 1
# else:
# # 剩余帧留黑
# output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
# # 写入帧
# out.write(output_frame)
# current_output_frame += 1
# # 释放视频写入器
# out.release()
# print(f"已生成视频: {output_path}, 共 {current_output_frame} 帧")
# # 释放原视频
# cap.release()
def create_mian_vid_for_ai(
input_video_path: str,
output_folder: str
) -> str:
"""
将原始视频的第0,1,2...帧映射到新视频的0,5,10...其他帧留黑
参数:
input_video_path: 原始视频路径
output_folder: 输出文件夹路径
返回:
str: 输出视频路径
"""
# 确保输出目录存在
os.makedirs(output_folder, exist_ok=True)
# 构建输出视频路径
output_video_path = os.path.join(output_folder, "mian_vid_ai.mp4")
# 打开原视频
cap = cv2.VideoCapture(input_video_path)
if not cap.isOpened():
raise ValueError(f"无法打开视频: {input_video_path}")
# 获取原视频信息
original_fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# 目标fps为25每5帧取一帧0, 5, 10...
target_fps = 25
frame_interval = 5
# 计算输出视频的总帧数
# 确保覆盖所有原始帧且不少于两秒
min_frames_for_2s = 25 * 2 # 2秒 @ 25fps
max_output_frame = max(total_frames * frame_interval, min_frames_for_2s)
# 创建视频写入器
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # type: ignore
out = cv2.VideoWriter(output_video_path, fourcc, target_fps, (frame_width, frame_height))
if not out.isOpened():
raise ValueError(f"无法创建视频文件: {output_video_path}")
# 生成视频帧
current_output_frame = 0
original_frame_idx = 0
while current_output_frame < max_output_frame:
# 检查当前输出帧是否是5的倍数
if current_output_frame % frame_interval == 0 and original_frame_idx < total_frames:
# 这是需要放置原始帧的位置
# 设置原视频读取位置
cap.set(cv2.CAP_PROP_POS_FRAMES, original_frame_idx)
ret, frame = cap.read()
if not ret:
# 读取失败,使用黑色帧
output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
else:
# 有数据,使用原始帧
output_frame = frame
# 移到下一个原始帧
original_frame_idx += 1
else:
# 剩余帧留黑
output_frame = np.zeros((frame_height, frame_width, 3), dtype=np.uint8)
# 写入帧
out.write(output_frame)
current_output_frame += 1
# 释放资源
cap.release()
out.release()
print(f"已生成视频: {output_video_path}, 共 {current_output_frame}")
return output_video_path

303
lib/sam3 copy.py Normal file
View File

@ -0,0 +1,303 @@
import os
from tkinter import N
from ultralytics.models.sam import SAM3VideoSemanticPredictor
import cv2
import json
import numpy as np
from pathlib import Path
class SAM3:
def __init__(self):
self._data: dict = {}
def run(self, vid_path: str, output_dir: str, class_file_path: str, conf: float = 0.6) -> dict:
"""
运行 SAM3 模型返回视频中每个帧的检测结果
参数:
vid_path: 视频路径
output_dir: 输出目录
class_file_path: 类别文件路径
返回:
每个帧的检测结果字典键为帧索引值为检测结果列表
字典结构:
{
"0": [ # frame_idx
{
"xyxy": [x1, y1, x2, y2], # 检测框坐标
"confidence": 0.9, # 置信度
"track_id": 123, # 跟踪ID
"class_id": 0, # 类别ID
"class_str": "class_name" # 类别名称
},
...
]
,
}
"""
# new_vid_path = extract_frames_to_video(vid_path, output_dir, interval)
self._data = {}
text_dict = json.load(open(class_file_path, "r", encoding="utf-8"))
text_list_en = list(text_dict.keys())
text_list_cn = list(text_dict.values())
overrides = dict(conf=conf, task="segment", mode="predict", imgsz=640, model="./model/sam3.pt", half=True, save=True, device=0)
predictor = SAM3VideoSemanticPredictor(overrides=overrides)
results = predictor(
source=vid_path,
text=text_list_en,
stream=True,
)
frame_idx = 0
# Path(f"{output_dir}/json").mkdir(parents=True, exist_ok=True)
annotated_frames: dict = {}
for r in results:
frame_data = []
if r.boxes is not None:
boxes = r.boxes.xyxy.cpu().numpy()
confs = r.boxes.conf.cpu().numpy()
cls_ids = r.boxes.cls.cpu().numpy()
track_ids = r.boxes.id.cpu().numpy()
for i in range(len(boxes)):
frame_data.append({
"xyxy": boxes[i].tolist(),
"confidence": float(confs[i]),
"track_id": int(track_ids[i]),
"class_id": int(cls_ids[i]),
"class_str": text_list_cn[int(cls_ids[i])]
})
annotated_frames[frame_idx] = frame_data
# with open(f"./output/json/frame_{frame_idx:04d}.json", "w", encoding="utf-8") as f:
# json.dump(frame_data, f, ensure_ascii=False, indent=2)
# 保存源图
source_frame = r.orig_img
source_path = f"{output_dir}/source/frame_{frame_idx:04d}.jpg"
Path(f"{output_dir}/source").mkdir(parents=True, exist_ok=True)
cv2.imwrite(source_path, source_frame)
# 保存标记图
mask_frame = r.plot()
mask_path = f"{output_dir}/boxes/frame_{frame_idx:04d}.jpg"
Path(f"{output_dir}/boxes").mkdir(parents=True, exist_ok=True)
cv2.imwrite(mask_path, mask_frame)
print(f"Frame {frame_idx}: {len(frame_data)} boxes")
# annotated_frame = r.plot()
frame_idx += 1
with open(f"{output_dir}/frame_all.json", "w", encoding="utf-8") as f:
json.dump(annotated_frames, f, ensure_ascii=False, indent=2)
print(f"\n总共处理 {frame_idx}")
print(f"结果已保存到 {output_dir}/frame_all.json")
print(f"标记图片已保存到 {output_dir}/boxes/")
cv2.destroyAllWindows()
self._data = annotated_frames
return annotated_frames
def load_from_json(self, json_path: str) -> dict:
"""
JSON 文件加载检测结果
参数:
json_path: JSON 文件路径
返回:
每个帧的检测结果字典键为帧索引值为检测结果列表
字典结构:
{
"0": [ # frame_idx
{
"xyxy": [x1, y1, x2, y2], # 检测框坐标
"confidence": 0.9, # 置信度
"track_id": 123, # 跟踪ID
"class_id": 0, # 类别ID
"class_str": "class_name" # 类别名称
},
...
]
,
}
"""
with open(json_path, "r", encoding="utf-8") as f:
self._data = json.load(f)
return self._data
def get_class_dict(self):
"""
只保留每帧的类列表不包含其他信息
格式1
{
"class_list": ["class_name1", "class_name2", ..."],
"frame_list": {
"0": [ # frame_idx
0, # 类别ID
1, # 类别ID
...
],
},
}
"""
if self._data is None:
raise ValueError("请先调用 run() 方法运行模型")
# 收集所有类别名称并去重
class_set = set() # 无序,唯一
for frame_data in self._data.values():
for item in frame_data:
class_set.add(item["class_str"])
class_list = sorted(list(class_set))
# 构建每帧的类别ID列表
frame_list = {}
for frame_idx, frame_data in self._data.items():
class_ids = set() # 无序,唯一
for item in frame_data:
class_name = item["class_str"]
class_id = class_list.index(class_name)
class_ids.add(class_id)
frame_list[str(frame_idx)] = sorted(list(class_ids))
return {
"class_list": class_list,
"frame_list": frame_list
}
def data(self):
return self._data
def extract_frames_to_video(video_path, output_dir, interval=1):
"""
抽帧生成新视频无任何图像处理
参数:
- video_path: 原始视频文件路径
- output_dir: 输出文件夹路径新视频将保存在此文件夹
- interval: 抽帧间隔 6 表示每隔 5 帧抽取 1
"""
print(f"开始抽帧: {video_path}")
print(f"抽帧间隔: {interval}")
print(f"输出目录: {output_dir}")
# 确保输出目录存在
Path(output_dir).mkdir(parents=True, exist_ok=True)
# 1. 打开原始视频
# 尝试使用硬件加速后端
backends = [
(cv2.CAP_FFMPEG, 'FFmpeg'), # 通用后端,支持硬件加速
(cv2.CAP_DSHOW, 'DirectShow'), # Windows 硬件加速
(cv2.CAP_ANY, 'Default') # 默认后端
]
cap = None
for backend, backend_name in backends:
try:
cap = cv2.VideoCapture(video_path, backend)
if cap.isOpened():
# 尝试启用硬件加速
if backend == cv2.CAP_FFMPEG:
# 设置FFmpeg硬件加速
try:
cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)
# 检查硬件加速是否启用
hw_accel = cap.get(cv2.CAP_PROP_HW_ACCELERATION)
print(f"FFmpeg硬件加速: {'已启用' if hw_accel > 0 else '未启用'}")
except Exception as e:
print(f"设置硬件加速失败: {e}")
print(f"使用后端: {backend_name}")
break
except Exception as e:
print(f"尝试{backend_name}后端失败: {e}")
continue
if not cap or not cap.isOpened():
raise Exception(f"无法打开视频文件: {video_path}")
# 2. 获取视频参数
fps = cap.get(cv2.CAP_PROP_FPS) # 原始帧率
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# 3. 计算新视频的帧率(保持原始播放速度)
# 新视频的帧率 = 原始帧率 / 抽帧间隔
new_fps = fps / max(interval, 1)
# 4. 初始化视频写入器
output_path = os.path.join(output_dir, "output_video.mp4")
# 使用更高效的编码格式
fourcc = cv2.VideoWriter_fourcc(*'avc1') # type: ignore # H.264 编码,更高效
# 尝试启用硬件加速写入
try:
# 尝试使用硬件加速的VideoWriter
out = cv2.VideoWriter(output_path, fourcc, new_fps, (width, height), isColor=True)
print("视频写入器初始化成功")
except Exception as e:
print(f"硬件加速写入失败,使用默认方式: {e}")
out = cv2.VideoWriter(output_path, fourcc, new_fps, (width, height))
# 5. 开始抽帧并写入新视频
frame_idx = 0
saved_frames = 0
print_interval = max(100, total_frames // 10) # 每处理100帧或10%进度打印一次
while cap.isOpened():
# 优化对于不需要的帧使用grab()跳过只对需要的帧使用retrieve()
if frame_idx % interval == 0:
# 需要处理的帧使用read()获取
ret, frame = cap.read()
if not ret:
break
out.write(frame) # 写入新视频
saved_frames += 1
else:
# 不需要处理的帧使用grab()跳过(更快)
ret = cap.grab()
if not ret:
break
frame_idx += 1
# 减少打印频率
if frame_idx % print_interval == 0:
progress = (frame_idx / total_frames) * 100
print(f"已处理帧 {frame_idx}/{total_frames} ({progress:.1f}%)")
# 6. 释放资源
cap.release()
out.release()
cv2.destroyAllWindows()
print(f"抽帧完成: 原视频 {total_frames} 帧, 抽取后 {saved_frames}")
print(f"新视频已保存至: {output_path}")
print(f"新视频帧率: {new_fps:.2f} fps (原始帧率: {fps:.2f} fps)")
return output_path
if __name__ == "__main__":
VID_NAME: str = "Peihdianhxiang"
VID_END: str = ".mp4"
conf: float = 0.6
output_dir: str = f"output/{VID_NAME}"
vid_path: str = f"./input/{VID_NAME}{VID_END}"
sam3: SAM3 = SAM3()
sam3.run(vid_path, output_dir, "1.厂房防火.json", conf)

291
lib/sam3.py Normal file
View File

@ -0,0 +1,291 @@
import os
from tkinter import N
from ultralytics.models.sam import SAM3VideoSemanticPredictor
import cv2
import json
import numpy as np
from pathlib import Path
class SAM3:
def __init__(self):
self._data: dict = {}
def run(self, vid_path: str, output_dir: str, class_file_path: str, interval: int = 1, conf: float = 0.6) -> dict:
"""
运行 SAM3 模型返回视频中每个帧的检测结果
参数:
vid_path: 视频路径
output_dir: 输出目录
class_file_path: 类别文件路径
返回:
每个帧的检测结果字典键为帧索引值为检测结果列表
字典结构:
{
"0": [ # frame_idx
{
"xyxy": [x1, y1, x2, y2], # 检测框坐标
"confidence": 0.9, # 置信度
"track_id": 123, # 跟踪ID
"class_id": 0, # 类别ID
"class_str": "class_name" # 类别名称
},
...
]
,
}
"""
new_vid_path = extract_frames_to_video(vid_path, output_dir, interval)
self._data = {}
text_dict = json.load(open(class_file_path, "r", encoding="utf-8"))
text_list_en = list(text_dict.keys())
text_list_cn = list(text_dict.values())
overrides = dict(conf=conf, task="segment", mode="predict", imgsz=640, model="./model/sam3.pt", half=True, save=True, device=0)
predictor = SAM3VideoSemanticPredictor(overrides=overrides)
results = predictor(
source=new_vid_path,
text=text_list_en,
stream=True,
)
frame_idx = 0
# Path(f"{output_dir}/json").mkdir(parents=True, exist_ok=True)
annotated_frames: dict = {}
for r in results:
frame_data = []
if r.boxes is not None:
boxes = r.boxes.xyxy.cpu().numpy()
confs = r.boxes.conf.cpu().numpy()
cls_ids = r.boxes.cls.cpu().numpy()
track_ids = r.boxes.id.cpu().numpy()
for i in range(len(boxes)):
frame_data.append({
"xyxy": boxes[i].tolist(),
"confidence": float(confs[i]),
"track_id": int(track_ids[i]),
"class_id": int(cls_ids[i]),
"class_str": text_list_cn[int(cls_ids[i])]
})
annotated_frames[frame_idx] = frame_data
# with open(f"./output/json/frame_{frame_idx:04d}.json", "w", encoding="utf-8") as f:
# json.dump(frame_data, f, ensure_ascii=False, indent=2)
# 保存源图
source_frame = r.orig_img
source_path = f"{output_dir}/source/frame_{frame_idx:04d}.jpg"
Path(f"{output_dir}/source").mkdir(parents=True, exist_ok=True)
cv2.imwrite(source_path, source_frame)
# 保存标记图
mask_frame = r.plot()
mask_path = f"{output_dir}/boxes/frame_{frame_idx:04d}.jpg"
Path(f"{output_dir}/boxes").mkdir(parents=True, exist_ok=True)
cv2.imwrite(mask_path, mask_frame)
print(f"Frame {frame_idx}: {len(frame_data)} boxes")
# annotated_frame = r.plot()
frame_idx += 1
with open(f"{output_dir}/frame_all.json", "w", encoding="utf-8") as f:
json.dump(annotated_frames, f, ensure_ascii=False, indent=2)
print(f"\n总共处理 {frame_idx}")
print(f"结果已保存到 {output_dir}/frame_all.json")
print(f"标记图片已保存到 {output_dir}/boxes/")
cv2.destroyAllWindows()
self._data = annotated_frames
return annotated_frames
def load_from_json(self, json_path: str) -> dict:
"""
JSON 文件加载检测结果
参数:
json_path: JSON 文件路径
返回:
每个帧的检测结果字典键为帧索引值为检测结果列表
字典结构:
{
"0": [ # frame_idx
{
"xyxy": [x1, y1, x2, y2], # 检测框坐标
"confidence": 0.9, # 置信度
"track_id": 123, # 跟踪ID
"class_id": 0, # 类别ID
"class_str": "class_name" # 类别名称
},
...
]
,
}
"""
with open(json_path, "r", encoding="utf-8") as f:
self._data = json.load(f)
return self._data
def get_class_dict(self):
"""
只保留每帧的类列表不包含其他信息
格式1
{
"class_list": ["class_name1", "class_name2", ..."],
"frame_list": {
"0": [ # frame_idx
0, # 类别ID
1, # 类别ID
...
],
},
}
"""
if self._data is None:
raise ValueError("请先调用 run() 方法运行模型")
# 收集所有类别名称并去重
class_set = set() # 无序,唯一
for frame_data in self._data.values():
for item in frame_data:
class_set.add(item["class_str"])
class_list = sorted(list(class_set))
# 构建每帧的类别ID列表
frame_list = {}
for frame_idx, frame_data in self._data.items():
class_ids = set() # 无序,唯一
for item in frame_data:
class_name = item["class_str"]
class_id = class_list.index(class_name)
class_ids.add(class_id)
frame_list[str(frame_idx)] = sorted(list(class_ids))
return {
"class_list": class_list,
"frame_list": frame_list
}
def data(self):
return self._data
def extract_frames_to_video(video_path, output_dir, interval=1):
"""
抽帧生成新视频无任何图像处理
参数:
- video_path: 原始视频文件路径
- output_dir: 输出文件夹路径新视频将保存在此文件夹
- interval: 抽帧间隔 6 表示每隔 5 帧抽取 1
"""
print(f"开始抽帧: {video_path}")
print(f"抽帧间隔: {interval}")
print(f"输出目录: {output_dir}")
# 确保输出目录存在
Path(output_dir).mkdir(parents=True, exist_ok=True)
# 1. 打开原始视频
# 尝试使用硬件加速后端
backends = [
(cv2.CAP_FFMPEG, 'FFmpeg'), # 通用后端,支持硬件加速
(cv2.CAP_DSHOW, 'DirectShow'), # Windows 硬件加速
(cv2.CAP_ANY, 'Default') # 默认后端
]
cap = None
for backend, backend_name in backends:
try:
cap = cv2.VideoCapture(video_path, backend)
if cap.isOpened():
# 尝试启用硬件加速
if backend == cv2.CAP_FFMPEG:
# 设置FFmpeg硬件加速
try:
cap.set(cv2.CAP_PROP_HW_ACCELERATION, cv2.VIDEO_ACCELERATION_ANY)
# 检查硬件加速是否启用
hw_accel = cap.get(cv2.CAP_PROP_HW_ACCELERATION)
print(f"FFmpeg硬件加速: {'已启用' if hw_accel > 0 else '未启用'}")
except Exception as e:
print(f"设置硬件加速失败: {e}")
print(f"使用后端: {backend_name}")
break
except Exception as e:
print(f"尝试{backend_name}后端失败: {e}")
continue
if not cap or not cap.isOpened():
raise Exception(f"无法打开视频文件: {video_path}")
# 2. 获取视频参数
fps = cap.get(cv2.CAP_PROP_FPS) # 原始帧率
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# 3. 计算新视频的帧率(保持原始播放速度)
# 新视频的帧率 = 原始帧率 / 抽帧间隔
new_fps = fps / max(interval, 1)
# 4. 初始化视频写入器
output_path = os.path.join(output_dir, "output_video.mp4")
# 使用更高效的编码格式
fourcc = cv2.VideoWriter_fourcc(*'avc1') # type: ignore # H.264 编码,更高效
# 尝试启用硬件加速写入
try:
# 尝试使用硬件加速的VideoWriter
out = cv2.VideoWriter(output_path, fourcc, new_fps, (width, height), isColor=True)
print("视频写入器初始化成功")
except Exception as e:
print(f"硬件加速写入失败,使用默认方式: {e}")
out = cv2.VideoWriter(output_path, fourcc, new_fps, (width, height))
# 5. 开始抽帧并写入新视频
frame_idx = 0
saved_frames = 0
print_interval = max(100, total_frames // 10) # 每处理100帧或10%进度打印一次
while cap.isOpened():
# 优化对于不需要的帧使用grab()跳过只对需要的帧使用retrieve()
if frame_idx % interval == 0:
# 需要处理的帧使用read()获取
ret, frame = cap.read()
if not ret:
break
out.write(frame) # 写入新视频
saved_frames += 1
else:
# 不需要处理的帧使用grab()跳过(更快)
ret = cap.grab()
if not ret:
break
frame_idx += 1
# 减少打印频率
if frame_idx % print_interval == 0:
progress = (frame_idx / total_frames) * 100
print(f"已处理帧 {frame_idx}/{total_frames} ({progress:.1f}%)")
# 6. 释放资源
cap.release()
out.release()
cv2.destroyAllWindows()
print(f"抽帧完成: 原视频 {total_frames} 帧, 抽取后 {saved_frames}")
print(f"新视频已保存至: {output_path}")
print(f"新视频帧率: {new_fps:.2f} fps (原始帧率: {fps:.2f} fps)")
return output_path

BIN
openh264-1.8.0-win64.dll Normal file

Binary file not shown.

View File

@ -0,0 +1,87 @@
# json输出数据模板
{
"tag": [
"这里是隐患标签",
"这里是隐患标签_1"
],
"base": [
"隐患依据1",
"隐患依据2",
...
]
"objects": [
{
"hazard_track_id": 0,
"tag_id": 0,
"class_id": 0,
"level": 0,
"start_frame": 0,
"base_id": 0,
"location": "这里是隐患位置描述"
},
...
]
}
# json输出示例
{
"tag": [
"灭火器未点检",
"灭火器被遮挡"
],
"base": [
"灭火器未点检依据1",
"灭火器被遮挡依据2",
],
"objects": [
{
"hazard_track_id": 0,
"tag_id": 0,
"class_id": 0,
"level": 0,
"start_frame": 0,
"base_id": 0,
"location": "这里是隐患位置描述"
},
{
"hazard_track_id": 1,
"tag_id": 1,
"class_id": 1,
"level": 1,
"start_frame": 0,
"base_id": 1,
"location": "这里是隐患位置描述"
}
]
}
# 输出格式注意事项
- 你的输出只能包含tag、base和objects三个键。
- tag是一个字符串数组每个元素是隐患标签必须为中文不能使用英文。每个隐患点只能有一个标签如果规则中存在多个标签必须选择最符合视频中情况的一个标签。
- objects是一个字典列表每个字典必须包含hazard_track_id整数、tag_id整数、class_id整数、level整数、start_frame整数、base_id整数、location字符串
- hazard_track_id分配规则根据视频画面每个连续出现的类型+隐患组合应分配单独的hazard_track_id。相同的tag_id+class_id组合在时间上不应相交应确保只存在一份连续的记录。绝对禁止出现多个hazard_track_id具有相同的tag_id和class_id且时间重叠的情况。
- level必须为0或1或2不能为其他整数。为0表示隐患等级为疑似为1表示隐患等级为低为2表示隐患等级为高。
- 不要输出bbox_2d、label或任何中文标点。
- 输出格式必须为标准json格式且结构必须与模板一致
- class_id必须与class_list中的顺序严格一致保持一一映射关系。
- 所有hazard_track_id都为独立隐患点不存在误检不得合并或拆分
- 有因为图像分辨率不足或视角问题导致的无法检测level必须为0
- 输出时只允许输出json内容不允许输出其他内容如```json ```
- 绝对禁止在输出中包含```json```或```等任何代码块标记
- start_frame表示该隐患在视频中的开始帧必须为整数
- location表示该隐患在视频画面中的位置描述必须为中文描述要准确、清晰能够明确指出隐患在画面中的相对位置。
# 任务1
- **帧级分析**根据提供的物体class、出现时间点提供的数据为每帧对应的物体列表与隐患识别规则在视频中同track_id的物体需持续观察对隐患进行识别每个隐患点分配一个hazard_track_id杜绝在同一个物体上重复识别隐患点
- **汇总处理**在完成所有帧的分析后基于各帧的分析结果为每个hazard_track_id确定最终的隐患标签、等级、位置描述以及开始帧位置
- **基本要求**只检测由class确定的物体每个hazard_track_id对应的字典中必须包含该hazard_track_id的tag_id、class_id、level、start_frame、base_id、location信息
- **匹配规则**如果物体与检测条目匹配就将该检测条目添加到objects列表中并设置相应的tag_id
- **关键约束**
1. **逐帧分析**必须分析每一帧中的每个物体用class匹配相应的检测规则根据隐患识别规则进行隐患识别
2. **语音识别**:必须对视频中的语音进行识别,辅助隐患识别
3. **规则参考**:严格参考知识库中的规则结构进行隐患识别,规则结构参考 `知识库/rule.json`
4. **全面识别**:必须对所有提供的物体进行隐患识别,不得遗漏任何物体
5. **准确匹配**根据物体的class与隐患识别规则进行准确匹配确定隐患标签和等级
6. **等级判定**根据规则中的匹配条件和依据合理判定匹配等级0-疑似1-确定)
7. **hazard_track_id分配**根据视频画面每个隐患点应分配单独的hazard_track_id。
8. **位置描述**大模型需在输出时提供隐患点相对于视频画面的位置location字段必须准确描述隐患在画面中的位置例如"画面左上角"、"画面中央偏右"等。

3
readme.md Normal file
View File

@ -0,0 +1,3 @@
### qwen试用
账号: 18237294717
https://bailian.console.aliyun.com/cn-beijing/?spm=a2c4g.11186623.0.0.2c2b2217h6fbio&tab=app#/app-center

32
test_optimization.py Normal file
View File

@ -0,0 +1,32 @@
import json
import time
from lib.qwen_fun_vid import frame_all_to_obj_vid
# 测试数据路径
input_video_path = "runs/segment/predict/04-01.avi"
json_path = "output_2/04-01/frame_all.json"
output_video_root = "output_2/04-01/test_optimized"
# 读取标注数据
print("读取标注数据...")
with open(json_path, 'r', encoding='utf-8') as f:
json_data = json.load(f)
print(f"标注数据包含 {len(json_data)}")
# 统计track_id数量
track_ids = set()
for frame_id_str, detections in json_data.items():
for det in detections:
track_id = det.get("track_id", -1)
track_ids.add(track_id)
print(f"共包含 {len(track_ids)} 个track_id")
# 测试优化后的函数
print("\n开始测试优化后的函数...")
start_time = time.time()
frame_all_to_obj_vid(json_data, input_video_path, output_video_root)
end_time = time.time()
print(f"\n测试完成,耗时: {end_time - start_time:.2f}")
print(f"输出视频保存到: {output_video_root}")

27
千问API数据.txt Normal file
View File

@ -0,0 +1,27 @@
Qwen3.5-27B
api_key="sk-c7ffed1b32794284a5d21b046d7d0008"
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
import os
from openai import OpenAI
try:
client = OpenAI(
# 若没有配置环境变量请用阿里云百炼API Key将下行替换为: api_key="sk-xxx",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
completion = client.chat.completions.create(
model="qwen3.5-27b", # 模型列表: https://help.aliyun.com/model-studio/getting-started/models
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': '你是谁?'}
]
)
print(completion.choices[0].message.content)
except Exception as e:
print(f"错误信息:{e}")
print("请参考文档https://help.aliyun.com/model-studio/developer-reference/error-code")

695
知识库/rule.json Normal file
View File

@ -0,0 +1,695 @@
{
"(一)厂房防火分区": {
"消防通道": {
"重大隐患": {
"安全出口堵塞": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。",
"*83. 存在 “两个通道” 堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["安全出口被货物、杂物、设备等堵塞,无法正常通行"]
},
"安全出口被上锁": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口门被锁闭、封堵,紧急情况下无法从内部开启"]
},
"安全出口设置卷帘门": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口采用卷帘门、转门、吊门、侧拉门等影响疏散的门型"]
},
"安全出口数量不足": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["生产车间、仓库等场所建筑面积超规,仅设置一个安全出口"]
},
"安全出口未安装安全出口标识": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口位置未设置醒目的安全出口指示标志"]
},
"疏散走道被占用、堵塞": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。",
"*83. 存在 “两个通道” 堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["疏散走道堆放货物、设备、杂物,净宽不足,影响人员疏散"]
},
"楼梯间堆放杂物": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["封闭楼梯间、防烟楼梯间内堆放物品,占用疏散空间"]
},
"疏散通道设置栅栏、障碍物": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["疏散走道、安全出口处设置铁栅栏、障碍物,阻碍人员疏散"]
},
"消防通道被占用、堵塞": {
"依据": [
"*55. 多产权建筑,具有多个使用方的甲、乙、丙类厂房或仓库,多业态混合生产经营场所未明确消防安全管理职责,未对消防车通道、涉及公共消防安全的疏散设施和其他消防设施明确统一管理单位。",
"*83. 存在 “两个通道” 堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": [
"厂区消防车通道停放车辆、堆放物品、设置障碍物,宽度不足 4 米"
]
},
"消防通道违章搭建": {
"依据": [
"*83. 存在 “两个通道” 堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["消防通道范围内违章搭建建筑物、构筑物,影响消防车通行"]
},
"安全出口被电气线路遮挡": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["电气线路、桥架、管线遮挡安全出口,影响疏散通行"]
},
"疏散门开启方向错误": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["疏散门未向疏散方向开启,紧急情况下难以推开"]
},
"安全出口标识缺失、损坏": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口指示标志脱落、损坏、不清晰,无法起到指示作用"]
},
"疏散通道净宽不足": {
"依据": [
"*42. 人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["疏散走道、安全出口净宽度不符合规范要求,小于规定值"]
},
"厂房内设置宿舍占用疏散通道": {
"依据": [
"*40. 劳动密集型企业的厂房、仓库内设置员工宿舍。",
"*87. 存在 “三合一” 情形:在厂房、库房、商场中设置员工宿舍,或是在居住等民用建筑中从事生产、储存、经营等活动,且不符合 GA703 的规定。"
],
"匹配条件": ["厂房、仓库内设置员工宿舍,占用、堵塞疏散通道与安全出口"]
}
}
},
"灭火器": {
"重大隐患": {
"灭火器未点检": {
"依据": [
"*55.多产权建筑,具有多个使用方的甲、乙、丙类厂房或仓库,多业态混合生产经营场所未明确消防安全管理职责,未对消防车通道、涉及公共消防安全的疏散设施和其他消防设施明确统一管理单位。",
"*60.具有火灾、爆炸风险的电池生产、储存场所或储能电站防火间距不足,未采取防火分隔措施,或未设置事故通风系统或自动灭火系统。",
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。",
"*89.企业在用的特种设备存在重大事故隐患的。",
"*85.企业在用的特种设备存在重大事故隐患的。",
"*79.在用的特种设备是未取得许可进行设计、制造、安装、改造、重大修理的。",
"*5.未对承包单位、承租单位定期进行安全检查。"
],
"匹配条件": ["灭火器上无检验单"]
},
"灭火器标识与实物不符": {
"依据": [
"*55.多产权建筑,具有多个使用方的甲、乙、丙类厂房或仓库,多业态混合生产经营场所未明确消防安全管理职责,未对消防车通道、涉及公共消防安全的疏散设施和其他消防设施明确统一管理单位。"
]
},
"灭火器喷管缠绕": {
"依据": [
"*55.多产权建筑,具有多个使用方的甲、乙、丙类厂房或仓库,多业态混合生产经营场所未明确消防安全管理职责,未对消防车通道、涉及公共消防安全的疏散设施和其他消防设施明确统一管理单位。"
]
},
"灭火器被遮挡": {
"依据": [
"*92.燃气管道、阀门、接头有泄漏、腐蚀、老化现象,或紧急切断阀不灵敏可靠。",
"*60.具有火灾、爆炸风险的电池生产、储存场所或储能电站防火间距不足,未采取防火分隔措施,或未设置事故通风系统或自动灭火系统。",
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
]
},
"灭火器压力不足": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
]
},
"灭火器喷管卷法不规范": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。",
"*3.生产、储存、经营易燃易爆危险品的场所与人员密集场所、居住场所设置在同一建筑物内或与人员密集场所、居住场所的防火间距小于国家工程建设消防技术标准规定值的75%。"
]
},
"灭火器被随意放置在地面上": {
"依据": [
"*89.企业在用的特种设备存在重大事故隐患的。",
"*78.存在未经许可,擅自从事移动式压力容器或者气瓶充装活动。"
]
},
"灭火器过期": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
]
},
"灭火器压力指示表损坏": {
"依据": [
"*5.易燃可燃液体、可燃气体储罐(区)未按国家工程建设消防技术标准的规定设置固定灭火、冷却、可燃气体浓度报警、火灾报警设施。",
"*40.压力管道安全阀、压力表、爆破片等安全附件、安全保护装置等缺少、失效或失灵。"
]
},
"灭火器破旧损坏": {
"依据": [
"*5.易燃可燃液体、可燃气体储罐(区)未按国家工程建设消防技术标准的规定设置固定灭火、冷却、可燃气体浓度报警、火灾报警设施。"
]
},
"灭火器点检频次不足": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
]
},
"积尘严重": {
"依据": [
"*20.存在粉尘爆炸危险的工贸企业未落实粉尘清理制度,造成作业现场积尘严重。"
],
"匹配条件": ["灭火器上可见明显灰尘"]
}
}
},
"消火栓": {
"重大隐患": {
"消火栓未点检": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。",
"*55.多产权建筑,具有多个使用方的甲、乙、丙类厂房或仓库,多业态混合生产经营场所未明确消防安全管理职责,未对消防车通道、涉及公共消防安全的疏散设施和其他消防设施明确统一管理单位。"
],
"匹配条件": ["消火栓箱体无定期检查记录、无检验标签"]
},
"消火栓被遮挡、被封堵": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。",
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["消火栓被货物、设备、杂物、围挡遮挡,无法正常开启使用"]
},
"消火栓无水/水压不足": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。",
"*60.具有火灾、爆炸风险的电池生产、储存场所或储能电站防火间距不足,未采取防火分隔措施,或未设置事故通风系统或自动灭火系统。"
],
"匹配条件": ["打开消火栓阀门无出水,或出水压力微弱,无法满足灭火要求"]
},
"消火栓水带缺失/破损/老化": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓箱内无消防水带,或水带破损、漏水、老化严重"]
},
"消火栓水枪缺失/损坏": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓箱内无消防水枪,或水枪断裂、接口损坏无法使用"]
},
"消火栓箱门损坏/无法开启": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓箱体变形、门锁损坏、箱门无法正常打开"]
},
"消火栓接口漏水/损坏": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓出水口、接口破损、锈蚀,连接水带时严重漏水"]
},
"消火栓积尘严重/未清理": {
"依据": [
"*20.存在粉尘爆炸危险的工贸企业未落实粉尘清理制度,造成作业现场积尘严重。",
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓箱体、水带、水枪表面积尘严重,未定期清理"]
},
"消火栓标识缺失/模糊不清": {
"依据": [
"*42.人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。",
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓无标识、标识脱落、文字模糊,无法快速识别"]
},
"消火栓随意堆放杂物": {
"依据": [
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。",
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓箱内/周边堆放工具、垃圾、物料等杂物"]
},
"消火栓阀门损坏/无法开关": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["消火栓控制阀门锈蚀、卡死、断裂,无法正常开启或关闭"]
}
}
},
"安全出口": {
"重大隐患": {
"安全出口数量不足": {
"依据": [
"*42.人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": [
"厂房、仓库建筑面积超规仅设置1个安全出口不满足规范要求"
]
},
"安全出口上锁无法开启": {
"依据": [
"*42.人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口安装门锁、链条锁闭,紧急情况下无法从内部开启"]
},
"安全出口设置卷帘门/转门": {
"依据": [
"*42.人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口采用卷帘门、转门、吊门、侧拉门,影响人员疏散"]
},
"安全出口净宽不足": {
"依据": [
"*42.人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口宽度小于规范要求,无法满足人员疏散通行"]
},
"安全出口开启方向错误": {
"依据": [
"*42.人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口门未向疏散方向开启,紧急情况下难以推开"]
}
}
},
"应急照明灯": {
"重大隐患": {
"应急照明灯缺失": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["疏散走道、楼梯间、安全出口位置未安装应急照明灯"]
},
"应急照明灯损坏/不亮": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["应急照明灯断电后无法点亮、灯光微弱、灯泡损坏"]
},
"应急照明灯遮挡/安装位置不当": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["应急照明灯被货物、设备遮挡,无法起到照明作用"]
},
"应急照明灯未定期点检": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["无应急照明点检记录,未定期测试功能"]
}
}
},
"疏散指示标志": {
"重大隐患": {
"疏散指示标志缺失": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["疏散走道、转角、安全出口未设置疏散指示标志"]
},
"疏散指示标志损坏/不亮": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["疏散指示标志断电不亮、指示灯损坏、方向指示错误"]
},
"疏散指示标志遮挡/脱落": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["疏散指示标志被遮挡、掉落、安装高度不符合要求"]
},
"疏散指示标志模糊不清": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": ["标志文字、箭头模糊不清,无法识别疏散方向"]
}
}
},
"堵塞通道的货物/电瓶车/杂物": {
"重大隐患": {
"疏散通道堆放货物堵塞": {
"依据": [
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": [
"疏散走道、楼梯间堆放原材料、成品、半成品等货物,占用疏散空间"
]
},
"电瓶车停放在疏散通道": {
"依据": [
"*87.在厂房、库房、商场中设置员工宿舍或是在居住等民用建筑中从事生产、储存、经营等活动且不符合GA703的规定。"
],
"匹配条件": [
"电动自行车、电动三轮车停放在疏散走道、安全出口、楼梯间内"
]
},
"疏散通道堆放杂物堵塞": {
"依据": [
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["疏散通道堆放垃圾、工具、纸箱、周转箱等杂物,净宽不足"]
},
"设备占用疏散通道": {
"依据": [
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["生产设备、货架、工作台摆放在疏散通道内,阻碍通行"]
}
}
},
"封堵安全出口的障碍物": {
"重大隐患": {
"安全出口设置铁栅栏封堵": {
"依据": [
"*42.人员密集场所的疏散走道、楼梯间、疏散门或安全出口设置影响疏散的栅栏、卷帘门。"
],
"匹配条件": ["安全出口外/内安装铁栅栏、防护网,完全封堵出口"]
},
"安全出口被墙体/板材封堵": {
"依据": [
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["安全出口被砌墙、封板、钉死,无法通行"]
},
"安全出口前设置固定障碍物": {
"依据": [
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["安全出口正前方放置货架、柜子、设备等固定障碍物"]
},
"安全出口被围挡/隔离栏封堵": {
"依据": [
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": ["安全出口被隔离栏、施工围挡、广告牌完全封堵"]
}
}
}
},
"(二)电气线路及配电设施": {
"配电箱": {
"重大隐患": {
"配电箱未使用防爆型": {
"依据": [
"*7.除尘器、收尘仓等划分为20区的粉尘爆炸危险场所电气设备不符合防爆要求。",
"*35.直接关系生产安全的监控、报警、防护等设施、设备、装置未保证正常运行、使用,失效或者无效。"
],
"匹配条件": [
"爆炸危险区域、喷漆间、粉尘场所、氢气使用场所未安装防爆配电箱"
]
},
"配电箱门损坏/缺失": {
"依据": [
"*89.企业在用的特种设备存在重大事故隐患的。",
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"配电箱箱体破损、箱门缺失、无法关闭"
]
},
"配电箱门口堆放杂物/易燃物": {
"依据": [
"*55.多产权建筑,具有多个使用方的甲、乙、丙类厂房或仓库,多业态混合生产经营场所未明确消防安全管理职责,未对消防车通道、涉及公共消防安全的疏散设施和其他消防设施明确统一管理单位。",
"*83.存在“两个通道”堵塞情形:存在严重占用防火间距、严重破坏防火分区、严重影响人员安全疏散和消防救援的情形。"
],
"匹配条件": [
"配电箱前1米内堆放纸箱、化学品、设备、垃圾等杂物"
]
},
"配电箱无警示标识/标识褪色": {
"依据": [
"*4.未对有限空间进行辨识、建立安全管理台账,并且未设置明显的安全警示标志。",
"*79.在用的特种设备是未取得许可进行设计、制造、安装、改造、重大修理的。"
],
"匹配条件": [
"配电箱未张贴“有电危险”警示标识,或标识模糊、脱落"
]
},
"配电箱内积尘严重": {
"依据": [
"*20.存在粉尘爆炸危险的工贸企业未落实粉尘清理制度,造成作业现场积尘严重。"
],
"匹配条件": [
"配电箱内部元器件表面积尘厚重,未清理"
]
},
"配电箱一闸多机/私拉乱接": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"一个回路连接多台设备,电线杂乱搭接"
]
}
}
},
"电线电缆": {
"重大隐患": {
"电线电缆老化/破损/裸露": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"电源线绝缘层开裂、铜线裸露、老化发黑"
]
},
"电线未穿管保护/明敷拖地": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"电线直接敷设在地面、设备边缘,未套管防护,易被碾压破损"
]
},
"防爆区域电线未做防爆密封": {
"依据": [
"*7.除尘器、收尘仓等划分为20区的粉尘爆炸危险场所电气设备不符合防爆要求。"
],
"匹配条件": [
"粉尘、易燃易爆场所电线未防爆穿管、未密封"
]
},
"电线私拉乱接/飞线": {
"依据": [
"*35.直接关系生产安全的监控、报警、防护等设施、设备、装置未保证正常运行、使用,失效或者无效。"
],
"匹配条件": [
"现场存在临时搭接、跨通道、跨设备的无规则电线"
]
},
"电线高温区域敷设未隔热": {
"依据": [
"*1.食品制造企业烘制、油炸设备未设置防过热自动切断装置。"
],
"匹配条件": [
"电线靠近烘道、烤箱、加热设备,未做防火隔热处理"
]
}
}
},
"插座": {
"重大隐患": {
"插座破损/松动/裸露": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"插座外壳破裂、铜片松动、接线裸露"
]
},
"防爆区域使用非防爆插座": {
"依据": [
"*7.除尘器、收尘仓等划分为20区的粉尘爆炸危险场所电气设备不符合防爆要求。"
],
"匹配条件": [
"喷漆、粉尘、易燃易爆场所使用普通民用插座"
]
},
"插座超负荷使用/串联插排": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"一个插座串联多个插排,带大功率设备"
]
},
"插座无漏电保护": {
"依据": [
"*1.存在粉尘爆炸危险的除尘系统采用重力沉降室除尘,或者采用干式巷道式构筑物作为除尘风道。"
],
"匹配条件": [
"车间、潮湿场所插座回路未安装漏电保护器"
]
}
}
},
"开关": {
"重大隐患": {
"开关破损/裸露/失效": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"开关外壳破损、接线裸露、无法正常通断"
]
},
"防爆区域使用普通开关": {
"依据": [
"*7.除尘器、收尘仓等划分为20区的粉尘爆炸危险场所电气设备不符合防爆要求。"
],
"匹配条件": [
"爆炸危险区域安装普通墙壁开关、拉线开关"
]
},
"开关控制多台大功率设备": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"单开关控制多台电机、加热器等设备,超负荷运行"
]
}
}
},
"接地装置": {
"重大隐患": {
"设备未做保护接地": {
"依据": [
"*40.压力管道安全阀、压力表、爆破片等安全附件、安全保护装置等缺少、失效或失灵。"
],
"匹配条件": [
"金属设备、配电箱、电机未连接PE保护接地线"
]
},
"接地线松动/脱落/断裂": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"接地端子松动、导线断裂、未可靠连接"
]
},
"易燃易爆区域未做防静电接地": {
"依据": [
"*5.易燃可燃液体、可燃气体储罐(区)未按国家工程建设消防技术标准的规定设置固定灭火、冷却、可燃气体浓度报警、火灾报警设施。"
],
"匹配条件": [
"化学品仓、粉尘设备、储罐未做防静电接地装置"
]
}
}
},
"大功率电器": {
"重大隐患": {
"大功率设备无专用回路": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"烤箱、焊机、加热器等大功率设备共用普通回路"
]
},
"大功率设备线路过热/老化": {
"依据": [
"*1.食品制造企业烘制、油炸设备未设置防过热自动切断装置。"
],
"匹配条件": [
"大功率设备电源线发热、绝缘层融化、老化"
]
},
"大功率设备无过热保护": {
"依据": [
"*35.直接关系生产安全的监控、报警、防护等设施、设备、装置未保证正常运行、使用,失效或者无效。"
],
"匹配条件": [
"烘道、加热设备未安装超温断电、过热保护装置"
]
}
}
},
"空气开关": {
"重大隐患": {
"空气开关缺失/损坏/失灵": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"空开无法合闸、跳闸失效、内部烧毁"
]
},
"空气开关规格不符/以大代小": {
"依据": [
"*227.消防设施设备缺失损坏,未定期开展检验,安装、配置不合理,处于不良好可用状态。"
],
"匹配条件": [
"用大电流空开替代小电流保护,失去过载保护作用"
]
},
"空开无漏电保护功能": {
"依据": [
"*1.存在粉尘爆炸危险的除尘系统采用重力沉降室除尘,或者采用干式巷道式构筑物作为除尘风道。"
],
"匹配条件": [
"车间、移动设备回路仅装空开,未装漏保"
]
},
"空气开关积尘严重/短路风险": {
"依据": [
"*20.存在粉尘爆炸危险的工贸企业未落实粉尘清理制度,造成作业现场积尘严重。"
],
"匹配条件": [
"空开表面粉尘厚重,易造成漏电、短路"
]
}
}
},
"防触电警示标识": {
"重大隐患": {
"防触电警示标识缺失": {
"依据": [
"*4.未对有限空间进行辨识、建立安全管理台账,并且未设置明显的安全警示标志。"
],
"匹配条件": [
"配电箱、变压器、高压设备未张贴防触电警示标志"
]
},
"警示标识模糊/破损/缺失": {
"依据": [
"*79.在用的特种设备是未取得许可进行设计、制造、安装、改造、重大修理的。"
],
"匹配条件": [
"警示标识褪色、破损、被遮挡,无法识别"
]
},
"危险电气区域无警示隔离": {
"依据": [
"*35.直接关系生产安全的监控、报警、防护等设施、设备、装置未保证正常运行、使用,失效或者无效。"
],
"匹配条件": [
"变压器、配电房未设置警示围栏和防触电标识"
]
}
}
}
}
}