new4
This commit is contained in:
commit
c19a917122
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Fire Extinguisher": "灭火器",
|
||||
"Fire Hydrant": "消防栓",
|
||||
"Safety Exit": "安全出口",
|
||||
"Emergency Lighting": "应急照明灯",
|
||||
"Evacuation Sign": "疏散指示标志",
|
||||
"Obstructions Blocking Fire Lane": "堵塞通道的货物/电瓶车/杂物",
|
||||
"Obstructions Blocking Safety Exit": "封堵安全出口的障碍物"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"Distribution Box": "配电箱"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
["object"]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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""
|
||||
|
||||
# 隐患依据
|
||||
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}")
|
||||
|
|
@ -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""
|
||||
|
||||
# 只取前 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}")
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
Binary file not shown.
|
|
@ -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字段必须准确描述隐患在画面中的位置,例如:"画面左上角"、"画面中央偏右"等。
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
### qwen试用
|
||||
账号: 18237294717
|
||||
https://bailian.console.aliyun.com/cn-beijing/?spm=a2c4g.11186623.0.0.2c2b2217h6fbio&tab=app#/app-center
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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.直接关系生产安全的监控、报警、防护等设施、设备、装置未保证正常运行、使用,失效或者无效。"
|
||||
],
|
||||
"匹配条件": [
|
||||
"变压器、配电房未设置警示围栏和防触电标识"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue